├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .gitpod.yml ├── .husky └── pre-commit ├── .lintstagedrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .releaserc.json ├── .staartrc ├── .stylelintignore ├── .stylelintrc ├── CNAME ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── blog │ ├── cloudinary.png │ ├── filestack.png │ ├── uploadcare.png │ └── v1-demo.gif ├── icon-dark.png ├── icon-dark.svg ├── icon-ph.svg ├── icon-white.svg ├── logo-dark.svg ├── logo-light.svg ├── screenshots │ ├── 9gag.png │ ├── artstation.png │ ├── blur.png │ ├── brightness.png │ ├── camera.png │ ├── contrast.png │ ├── crop.png │ ├── deviantart.png │ ├── facebook.png │ ├── filters.png │ ├── flickr.png │ ├── flip.png │ ├── flipboard.png │ ├── fotki.png │ ├── giphy.png │ ├── grayscale.png │ ├── home-scrolled.png │ ├── home.png │ ├── hue-rotate.png │ ├── instagram.png │ ├── invert.png │ ├── linkedin.png │ ├── local.png │ ├── pexels.png │ ├── pinterest.png │ ├── pixabay.png │ ├── reddit.png │ ├── rotate.png │ ├── saturate.png │ ├── screenshot.png │ ├── sepia.png │ ├── tumblr.png │ ├── twitter.png │ ├── twtter.png │ ├── unsplash.png │ ├── url.png │ ├── weheartit.png │ ├── wip-1.png │ ├── wip-2.png │ ├── wip-3.png │ └── wip-4.png ├── smo.png └── uppload.sketch ├── content ├── @ │ ├── anand.md │ └── michael.md ├── a11y.md ├── api.md ├── backends.md ├── blog │ ├── index.md │ ├── introducing-uppload-v2.md │ └── introducing-uppload.md ├── bundle-size.md ├── compare.md ├── compression.md ├── configuration.md ├── effects │ ├── crop.md │ ├── filter.md │ ├── flip.md │ ├── index.md │ └── rotate.md ├── examples │ └── index.md ├── faq.md ├── getting-started.md ├── help │ ├── index.md │ └── services │ │ ├── camera.md │ │ ├── import-from-web-service │ │ ├── 9gag.md │ │ ├── artstation.md │ │ ├── deviantart.md │ │ ├── facebook.md │ │ ├── flickr.md │ │ ├── flipboard.md │ │ ├── fotki.md │ │ ├── index.md │ │ ├── instagram.md │ │ ├── linkedin.md │ │ ├── pinterest.md │ │ ├── reddit.md │ │ ├── tumblr.md │ │ ├── twitter.md │ │ └── weheartit.md │ │ ├── local.md │ │ ├── screenshot.md │ │ ├── search │ │ ├── giphy.md │ │ ├── index.md │ │ ├── pexels.md │ │ ├── pixabay.md │ │ └── unsplash.md │ │ └── url.md ├── i18n.md ├── index.md ├── listening-to-events.md ├── migrating-from-1x.md ├── multiple-files.md ├── multiple-instances.md ├── services │ ├── camera.md │ ├── import-from-web-service.md │ ├── index.md │ ├── local.md │ └── search-for-images.md ├── themes.md ├── treeshaking.md ├── uploaders │ ├── custom-uploader.md │ ├── fetch.md │ ├── firebase.md │ ├── index.md │ └── xhr.md └── wrappers │ └── index.md ├── demo ├── demo.scss ├── index.ts └── package.json ├── dts-bundle-generator.config.ts ├── index.html ├── jest.config.ts ├── netlify.toml ├── package-lock.json ├── package.json ├── scripts ├── build-demo.js ├── build-examples.js ├── build-lang.js └── build-scss.js ├── src ├── browser.ts ├── effect.ts ├── effects │ ├── crop │ │ └── index.ts │ ├── filter │ │ ├── blur.ts │ │ ├── brightness.ts │ │ ├── contrast.ts │ │ ├── grayscale.ts │ │ ├── hue-rotate.ts │ │ ├── invert.ts │ │ ├── saturate.ts │ │ └── sepia.ts │ ├── flip │ │ └── index.ts │ ├── preview │ │ └── index.ts │ └── rotate │ │ └── index.ts ├── helpers │ ├── assets.ts │ ├── elements.ts │ ├── files.ts │ ├── filter.ts │ ├── http.ts │ ├── i18n.ts │ ├── interfaces.ts │ ├── microlink.ts │ ├── search.ts │ └── utils.ts ├── i18n │ ├── de.ts │ ├── en.ts │ ├── es.ts │ ├── fa.ts │ ├── fr.ts │ ├── hi.ts │ ├── index.ts │ ├── it.ts │ ├── nl.ts │ ├── pt.ts │ ├── ro.ts │ ├── ru.ts │ ├── tr.ts │ ├── uk.ts │ └── zh-TW.ts ├── index.ts ├── service.ts ├── services │ ├── camera.ts │ ├── local.ts │ ├── microlink │ │ ├── 9gag.ts │ │ ├── artstation.ts │ │ ├── deviantart.ts │ │ ├── facebook.ts │ │ ├── flickr.ts │ │ ├── flipboard.ts │ │ ├── fotki.ts │ │ ├── instagram.ts │ │ ├── linkedin.ts │ │ ├── pinterest.ts │ │ ├── reddit.ts │ │ ├── screenshot.ts │ │ ├── tumblr.ts │ │ ├── twitter.ts │ │ ├── url.ts │ │ └── weheartit.ts │ └── search │ │ ├── giphy.ts │ │ ├── pexels.ts │ │ ├── pixabay.ts │ │ └── unsplash.ts ├── styles │ ├── effects.scss │ ├── input-range.scss │ ├── mobile.scss │ ├── modal.scss │ ├── services.scss │ ├── services │ │ └── cropper.scss │ ├── ui.scss │ └── uppload.scss ├── themes │ ├── dark.scss │ ├── light.scss │ └── theme.scss ├── uploaders │ └── xhr.ts ├── uppload.ts └── vite-env.d.ts ├── tests ├── __mocks__ │ └── tabbable.ts ├── effect.test.ts ├── helpers │ ├── assets.test.ts │ ├── elements.test.ts │ ├── i18n.test.ts │ ├── microlink.test.ts │ └── search.test.ts ├── mocks.ts ├── service.test.ts ├── services │ ├── local.test.ts │ └── search │ │ ├── giphy.test.ts │ │ ├── pexels.test.ts │ │ ├── pixabay.test.ts │ │ └── unsplash.test.ts ├── uploaders │ └── xhr.test.ts └── uppload.test.ts ├── tsconfig.json └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | .history 2 | .husky 3 | .vscode 4 | coverage 5 | dist 6 | node_modules 7 | vite.config.ts 8 | jest.config.ts 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "prettier"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier" 10 | ], 11 | "env": { 12 | "browser": true, 13 | "node": true 14 | }, 15 | "rules": { 16 | "prettier/prettier": "error" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Describe the bug** 7 | A clear and concise description of what the bug is. Include whether you're using Uppload version 1.x or 2.x. 8 | 9 | **To Reproduce** 10 | Steps to reproduce the behavior: 11 | 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | 25 | - OS: \[e.g. iOS] 26 | - Browser \[e.g. chrome, safari] 27 | - Version \[e.g. 22] 28 | 29 | **Smartphone (please complete the following information):** 30 | 31 | - Device: \[e.g. iPhone6] 32 | - OS: \[e.g. iOS8.1] 33 | - Browser \[e.g. stock browser, safari] 34 | - Version \[e.g. 22] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: "lts/*" 19 | cache: npm 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Release 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | run: npx semantic-release 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: "lts/*" 22 | cache: npm 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Build 26 | run: npm run build 27 | - name: Run tests 28 | run: npm run test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | public 14 | *.local 15 | coverage 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | .history/* 20 | !.vscode/extensions.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | 29 | # Examples 30 | content/examples/* 31 | !content/examples/index.md 32 | scripts/uppload-examples-master 33 | scripts/master.zip 34 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: npm install 3 | command: npm run dev 4 | ports: 5 | - port: 1234 6 | onOpen: open-preview 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "./**/*.{ts,html,json}": "npm run format:scripts", 3 | "./**/*.{css,scss}": "npm run format:styles" 4 | } 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v19.7.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .history 2 | .husky 3 | .vscode 4 | coverage 5 | dist 6 | node_modules 7 | vite.config.ts 8 | jest.config.ts 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "singleQuote": false, 5 | "trailingComma": "es5", 6 | "arrowParens": "avoid", 7 | "bracketSpacing": true, 8 | "useTabs": false, 9 | "endOfLine": "auto", 10 | "singleAttributePerLine": false, 11 | "bracketSameLine": false, 12 | "jsxBracketSameLine": false, 13 | "jsxSingleQuote": false, 14 | "quoteProps": "as-needed", 15 | "semi": true 16 | } 17 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "semantic-release-gitmoji", 5 | { 6 | "releaseRules": { 7 | "patch": { 8 | "include": [":bento:", ":recycle:"] 9 | } 10 | } 11 | } 12 | ], 13 | "@semantic-release/github", 14 | "@semantic-release/npm", 15 | [ 16 | "@semantic-release/git", 17 | { 18 | "message": ":bookmark: v${nextRelease.version} [skip ci]\n\nhttps://github.com/elninotech/uppload/releases/tag/${nextRelease.gitTag}" 19 | } 20 | ] 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.staartrc: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Uppload", 3 | "baseUrl": "https://uppload.js.org", 4 | "themeColor": "#f05d5e", 5 | "textColor": "#1b0000", 6 | "linkColor": "#dc3545", 7 | "googleAnalytics": "UA-15148382-2", 8 | "agastyaApiKey": "uppload", 9 | "algoliaApiKey": "a6a7714b2b52dbd3313ebef7075746d5", 10 | "algoliaIndex": "uppload", 11 | "navbar": [ 12 | "[Docs](/)", 13 | "[Getting started](/getting-started)", 14 | "[Blog](/blog)", 15 | "[GitHub](https://github.com/elninotech/uppload)" 16 | ], 17 | "twitterPublisher": "elninoict", 18 | "scripts": ["/uppload-demo.js"], 19 | "stylesheets": ["/uppload-demo.css", "/assets/demo.css"], 20 | "footerNavbar": ["[Hosted by Netlify](https://netlify.com)"], 21 | "fancyLinks": true 22 | } 23 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | .history 2 | .husky 3 | .vscode 4 | coverage 5 | dist 6 | node_modules 7 | vite.config.ts 8 | jest.config.ts 9 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-sass-guidelines" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": ["**/*.scss"], 9 | "customSyntax": "postcss-scss" 10 | } 11 | ], 12 | "rules": { 13 | "function-parentheses-space-inside": null, 14 | "no-descending-specificity": null, 15 | "max-nesting-depth": 2, 16 | "selector-max-id": 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | uppload.netlify.com 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@elnino.tech or on our [contact page](https://www.elnino.tech/samenwerken). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 El Niño BV 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 | -------------------------------------------------------------------------------- /assets/blog/cloudinary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/blog/cloudinary.png -------------------------------------------------------------------------------- /assets/blog/filestack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/blog/filestack.png -------------------------------------------------------------------------------- /assets/blog/uploadcare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/blog/uploadcare.png -------------------------------------------------------------------------------- /assets/blog/v1-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/blog/v1-demo.gif -------------------------------------------------------------------------------- /assets/icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/icon-dark.png -------------------------------------------------------------------------------- /assets/icon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icon-ph.svg: -------------------------------------------------------------------------------- 1 | Product Hunt#1 Product of the Day 2 | -------------------------------------------------------------------------------- /assets/icon-white.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/screenshots/9gag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/9gag.png -------------------------------------------------------------------------------- /assets/screenshots/artstation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/artstation.png -------------------------------------------------------------------------------- /assets/screenshots/blur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/blur.png -------------------------------------------------------------------------------- /assets/screenshots/brightness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/brightness.png -------------------------------------------------------------------------------- /assets/screenshots/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/camera.png -------------------------------------------------------------------------------- /assets/screenshots/contrast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/contrast.png -------------------------------------------------------------------------------- /assets/screenshots/crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/crop.png -------------------------------------------------------------------------------- /assets/screenshots/deviantart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/deviantart.png -------------------------------------------------------------------------------- /assets/screenshots/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/facebook.png -------------------------------------------------------------------------------- /assets/screenshots/filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/filters.png -------------------------------------------------------------------------------- /assets/screenshots/flickr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/flickr.png -------------------------------------------------------------------------------- /assets/screenshots/flip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/flip.png -------------------------------------------------------------------------------- /assets/screenshots/flipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/flipboard.png -------------------------------------------------------------------------------- /assets/screenshots/fotki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/fotki.png -------------------------------------------------------------------------------- /assets/screenshots/giphy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/giphy.png -------------------------------------------------------------------------------- /assets/screenshots/grayscale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/grayscale.png -------------------------------------------------------------------------------- /assets/screenshots/home-scrolled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/home-scrolled.png -------------------------------------------------------------------------------- /assets/screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/home.png -------------------------------------------------------------------------------- /assets/screenshots/hue-rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/hue-rotate.png -------------------------------------------------------------------------------- /assets/screenshots/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/instagram.png -------------------------------------------------------------------------------- /assets/screenshots/invert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/invert.png -------------------------------------------------------------------------------- /assets/screenshots/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/linkedin.png -------------------------------------------------------------------------------- /assets/screenshots/local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/local.png -------------------------------------------------------------------------------- /assets/screenshots/pexels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/pexels.png -------------------------------------------------------------------------------- /assets/screenshots/pinterest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/pinterest.png -------------------------------------------------------------------------------- /assets/screenshots/pixabay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/pixabay.png -------------------------------------------------------------------------------- /assets/screenshots/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/reddit.png -------------------------------------------------------------------------------- /assets/screenshots/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/rotate.png -------------------------------------------------------------------------------- /assets/screenshots/saturate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/saturate.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/screenshot.png -------------------------------------------------------------------------------- /assets/screenshots/sepia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/sepia.png -------------------------------------------------------------------------------- /assets/screenshots/tumblr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/tumblr.png -------------------------------------------------------------------------------- /assets/screenshots/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/twitter.png -------------------------------------------------------------------------------- /assets/screenshots/twtter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/twtter.png -------------------------------------------------------------------------------- /assets/screenshots/unsplash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/unsplash.png -------------------------------------------------------------------------------- /assets/screenshots/url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/url.png -------------------------------------------------------------------------------- /assets/screenshots/weheartit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/weheartit.png -------------------------------------------------------------------------------- /assets/screenshots/wip-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/wip-1.png -------------------------------------------------------------------------------- /assets/screenshots/wip-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/wip-2.png -------------------------------------------------------------------------------- /assets/screenshots/wip-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/wip-3.png -------------------------------------------------------------------------------- /assets/screenshots/wip-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/screenshots/wip-4.png -------------------------------------------------------------------------------- /assets/smo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/smo.png -------------------------------------------------------------------------------- /assets/uppload.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elninotech/uppload/eeb20d1fc693de5bfb75c8459d4dae47a1b67b79/assets/uppload.sketch -------------------------------------------------------------------------------- /content/@/anand.md: -------------------------------------------------------------------------------- 1 | --- 2 | linkedin: AnandChowdhary 3 | twitter: AnandChowdhary 4 | github: AnandChowdhary 5 | website: https://anandchowdhary.com 6 | --- 7 | 8 | # Anand Chowdhary 9 | 10 | Anand Chowdhary built Uppload as a consultant to El Niño. He is a creative technologist and entrepreneur, and the co-founder and CEO of Oswald Labs, an award-winning accessibility technology company that builds products for people with disabilities. 11 | 12 | 13 | 14 | He is best known for his work on web accessibility and is also an active open-source contributor. As a developer advocate, he has spoken at 20+ events around the world about startups, technology, and design, and is also a consultant to several startups and studios like El Niño, the developers of Uppload. At El Niño, he was a consultant specializing in frontend engineering and accessibility. 15 | 16 | In April 2018, he made the first commit to Uppload ([56bd930](https://github.com/elninotech/uppload/commit/56bd9307a020c692ed1383f0f4731690c00d90c9)), and in September 2019, to Uppload v2 ([073a091](https://github.com/elninotech/uppload/commit/073a0914ef9026036a2e52ce9c6795404ddefcf5)). Since then, he has been actively managing issues and merging pull requests. 17 | -------------------------------------------------------------------------------- /content/@/michael.md: -------------------------------------------------------------------------------- 1 | --- 2 | linkedin: magroeneveld 3 | website: https://www.elnino.tech 4 | --- 5 | 6 | # Michael Angelo Groeneveld 7 | 8 | Michael Angelo Groeneveld is the founder of El Niño, the Dutch digital development studio that built Uppload. Always looking for challenges in new and existing projects, Michael also co-founded Stichting Apsara, a non-profit to support charity projects in Cambodia. 9 | -------------------------------------------------------------------------------- /content/a11y.md: -------------------------------------------------------------------------------- 1 | # Accessibility 2 | 3 | Uppload is designed to be highly accessible. It follows the WCAG specification, uses semantic HTML5 tags, and has first-class keyboard navigation support. 4 | 5 | Some accessibility features include: 6 | 7 | - Radio input-based navbars with keyboard navigation 8 | - Trapped focus when modal is open 9 | - Tabbable native buttons on home 10 | - Use "Escape" key to close modal 11 | - Accessibility Tree-hidden decorative SVGs 12 | -------------------------------------------------------------------------------- /content/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | Using the Uppload API, you can programatically control functionality. First, initialize your package, like this: 4 | 5 | ```ts 6 | import { Uppload, Local, xhrUploader, en } from "uppload"; 7 | 8 | const uploader = new Uppload({ 9 | lang: en, 10 | uploader: xhrUploader({ 11 | endpoint: "https://example.com/upload", 12 | }), 13 | }); 14 | uploader.use(new Local()); 15 | ``` 16 | 17 | Now, you're ready to use the API. 18 | 19 | ## Widget manipulation 20 | 21 | You can open, close, or toggle the widget like this: 22 | 23 | ```ts 24 | uppload.open(); 25 | uppload.close(); 26 | uppload.toggle(); 27 | ``` 28 | 29 | To get the current state (open or closed), the recommended way is using the `modalOpen()` function: 30 | 31 | ```ts 32 | const isOpen = uppload.modalOpen(); 33 | // `isOpen` is true or false 34 | ``` 35 | 36 | ## Navigation 37 | 38 | To navigate to a specific service, like Local, you can do this: 39 | 40 | ```ts 41 | uppload.navigate("local"); 42 | ``` 43 | 44 | ## Uploads 45 | 46 | Lastly, you can also upload a file: 47 | 48 | ```ts 49 | const file = new Blob(); 50 | 51 | uppload 52 | .upload(file) 53 | .then(url => { 54 | console.log("Uploaded URL", url); 55 | }) 56 | .catch(error => { 57 | console.error("ERR", error); 58 | }); 59 | ``` 60 | 61 | ## Changing settings 62 | 63 | If you've already initialized the plugin using `new Uppload()`, you can update its settings (such as language) using the following: 64 | 65 | ```ts 66 | uppload.updateSettings({ 67 | lang: nl 68 | uploader: aDifferentUploader 69 | }); 70 | ``` 71 | 72 | ## Updating active plugins 73 | 74 | ### Removing a plugin 75 | 76 | If you want to remove a specific plugin (like a service or effect), use the `remove` function: 77 | 78 | ```ts 79 | uppload.remove("instagram"); 80 | uppload.remove("crop"); 81 | ``` 82 | 83 | ### Updating plugins' list: 84 | 85 | The `updatePlugins` function takes a function as the argument; that function takes an array of active plugins as its argument and returns the plugins to keep. 86 | 87 | For example, you can use the `Array.filter()` function to create a list of active plugins: 88 | 89 | ```ts 90 | uppload.updatePlugins(plugins => 91 | plugins.filter(plugin => { 92 | plugin.name === "instagram"; 93 | }) 94 | ); 95 | ``` 96 | 97 | Or, come up with your own logic that returns a list of plugins: 98 | 99 | ```ts 100 | uppload.updatePlugins(plugins => { 101 | const result = plugins.length > 4 ? [] : plugins; 102 | result.push(new Crop(), new Unsplash()); 103 | return result; 104 | }); 105 | ``` 106 | 107 | ### Removing all plugins 108 | 109 | If you want to reset your Uppload instance's plugins by removing all of them, you can return an empty array in `updatePlugins`: 110 | 111 | ```ts 112 | uppload.updatePlugins(plugins => []); 113 | ``` 114 | -------------------------------------------------------------------------------- /content/backends.md: -------------------------------------------------------------------------------- 1 | # Backends 2 | 3 | By default, Uppload works with any file uploading backend because it sends `FormData`. Uppload does **not** provide the server side implementation of handling the files, but the way files are uploaded is identical to simple file upload forms like this: 4 | 5 | ```html 6 |
7 | 8 |
9 | ``` 10 | 11 | Here are some common backend templates to get your started: 12 | 13 | - [Ruby on Rails](http://guides.rubyonrails.org/form_helpers.html#uploading-files) 14 | - [PHP (Basic)](http://www.startutorial.com/articles/view/how-to-build-a-file-upload-form-using-dropzonejs-and-php) 15 | - [Laravel](http://maxoffsky.com/code-blog/howto-ajax-multiple-file-upload-in-laravel/) 16 | - [Symfony 2 and AWS S3](http://www.jesuisundev.fr/upload-drag-drop-via-dropzonejs-symfony2-on-cloud-amazon-s3/) 17 | - [ASP.NET](https://www.codeproject.com/Articles/874215/File-upload-in-ASP-NET-MVC-using-Dropzone-JS-and-H) 18 | - [ServiceStack](http://www.buildclassifieds.com/2016/01/08/uploading-images-servicestack-and-dropzone/) 19 | - [Golang](https://hackernoon.com/how-to-build-a-file-upload-form-using-dropzonejs-and-go-8fb9f258a991) 20 | -------------------------------------------------------------------------------- /content/blog/index.md: -------------------------------------------------------------------------------- 1 | # Uppload Blog 2 | -------------------------------------------------------------------------------- /content/bundle-size.md: -------------------------------------------------------------------------------- 1 | # Bundle size 2 | 3 | ## Compared to v1 4 | 5 | _This section is currently in development._ 6 | 7 | | Bundle type | Uppload v1 | Uppload v2 | 8 | | --------------- | ---------- | ------------ | 9 | | Lean build | 39.8 kB | some | 10 | | True build | 59.1 kB | some plugins | 11 | | Full build | 134 kB | 137 kB | 12 | | True full build | 154 kB | 137 kB | 13 | 14 | - In Uppload v1, Lean bundle includes no polyfills; in Uppload v2, it includes no plugins 15 | - Uppload v1 included lazy-loaded components, True builds includes them too 16 | - In Uppload v2, True build includes 6 services and 5 effects, the recommended 17 | - True full builds include all polyfills and plugins 18 | - Uppload v1 included CSS styles while v2 requires loading them separately 19 | -------------------------------------------------------------------------------- /content/compare.md: -------------------------------------------------------------------------------- 1 | # Compare Uppload 2 | 3 | | Feature | Uploadcare | Dropzone.js | Uppy | Uppload | 4 | | --------------------- | ---------- | ----------- | ---- | ------- | 5 | | Responsive | ✅ | ✅ | ✅ | ✅ | 6 | | Drag and drop | ✅ | ✅ | ✅ | ✅ | 7 | | File previews | ✅ | ✅ | ✅ | ✅ | 8 | | Backend-free | ❌ | ✅ | ❌ | ✅ | 9 | | Import from URL | ✅ | ❌ | ✅ | ✅ | 10 | | TypeScript support | ❌ | ✅ | ✅ | ✅ | 11 | | Import from Instagram | ✅ | ❌ | ✅ | ✅ | 12 | | Import from Facebook | ✅ | ❌ | ✅ | ✅ | 13 | | Search on Unsplash | ❌ | ❌ | ❌ | ✅ | 14 | | Search on Pexels | ❌ | ❌ | ❌ | ✅ | 15 | | Built-in crop | ✅ | ❌ | ❌ | ✅ | 16 | | Built-in filters | ❌ | ❌ | ❌ | ✅ | 17 | | Built-in rotate | ❌ | ❌ | ❌ | ✅ | 18 | | Click photo | ✅ | ❌ | ✅ | ✅ | 19 | | Custom i18n | ❌ | ✅ | ✅ | ✅ | 20 | | Custom backends | ❌ | ✅ | ✅ | ✅ | 21 | | jQuery-free | ❌ | ✅ | ✅ | ✅ | 22 | | Custom plugins | ❌ | ❌ | ✅ | ✅ | 23 | | Upload to Firebase | ❌ | ❌ | ✅ | ✅ | 24 | | Free and open-source | ✅ | ✅ | ✅ | ✅ | 25 | -------------------------------------------------------------------------------- /content/compression.md: -------------------------------------------------------------------------------- 1 | # Image compression 2 | 3 | To save on bandwidth and storage costs, you may want to upload resized and compressed images by default. If this is the case, you can [configure](/configuration) Uppload to do so: 4 | 5 | ```ts 6 | import { Uppload } from "uppload"; 7 | 8 | const uploader = new Uppload({ 9 | maxSize: [800, 600], 10 | }); 11 | ``` 12 | 13 | In the above example, images will be resized to a maximum of 800px in width and 600px in height. You can also specify only one of the two constraints using `maxWidth` or `maxHeight`. 14 | 15 | If you want to apply compression, you can specify the `compression` factor (from 0 to 1), and Uppload will apply a lossy compression before uploading the image. This means that even if users upload a PNG, we will convert it to `image/jpeg` and upload it. Alternately, by specifying the `compressionToMime`, you can use WebP instead of JPEG. 16 | 17 | ```ts 18 | const uploader = new Uppload({ 19 | compression: 0.8, 20 | compressionToMime: "image/webp", 21 | }); 22 | ``` 23 | 24 | If you only want specific file types to be compress, for example only JPEG and WEBP images, but not PNGs, you can use the `compressionFromMimes` property: 25 | 26 | ```ts 27 | const uploader = new Uppload({ 28 | compression: 0.8, 29 | compressionFromMimes: ["image/jpeg", "image/webp"], 30 | compressionToMime: "image/webp", 31 | }); 32 | ``` 33 | 34 | If you want to perform your own image compression, perhaps by using an external library, you can specify a function instead: 35 | 36 | ```ts 37 | const uploader = new Uppload({ 38 | compressor: (file: Blob) => { 39 | return new Promise((resolve, reject) => { 40 | // Perform your compression here 41 | resolve(file); 42 | }); 43 | }, 44 | }); 45 | ``` 46 | -------------------------------------------------------------------------------- /content/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | In the following example, the `value` property is used in the configuration object to specify the default image value. 4 | 5 | ```ts 6 | import { Uppload } from "uppload"; 7 | 8 | const uploader = new Uppload({ 9 | value: "https://example.com/image.jpg", 10 | }); 11 | ``` 12 | 13 | The keys you can use in the configuration object are: 14 | 15 | | Key | Type | Description | 16 | | ---------------------- | ----------- | ---------------------------------------------------------------------------------- | 17 | | `value` | String | Default/current image URL | 18 | | `bind` | _IElements_ | Set value of these elements | 19 | | `call` | _IElements_ | Clicking on these elements should open modal | 20 | | `defaultService` | String | Default service to open widget on | 21 | | `lang` | Object | [Language pack](/i18n) | 22 | | `uploader` | Function | Function to [upload files](/uploaders) | 23 | | `inline` | Boolean | Show widget inline instead of modal | 24 | | `customClass` | String | Adds an additional class to the container | 25 | | `multiple` | Boolean | Allow multiple file uploads [#59](https://github.com/elninotech/uppload/issues/59) | 26 | | `compression` | Number | Compress image with this factor (0-1) | 27 | | `compressionFromMimes` | String[] | Only compress these types of images | 28 | | `compressionToMime` | String | Type of image to return, `image/jpeg` or `image/webp` | 29 | | `maxWidth` | Number | Resize image to maximum width | 30 | | `maxHeight` | Number | Resize image to maximum height | 31 | | `maxSize` | Number[] | Resize image to maximum \[width, height] | 32 | | `compressor` | Function | Function to [compress files](/compression) | 33 | 34 | \*The _IElements_ type can be any of the following: 35 | 36 | - String (e.g., a query selector like `".image"`) 37 | - String[] (e.g., a query selector array like `[".image", ".img"]`) 38 | - Element (e.g., a DOM element like `document.getElementById("img")`) 39 | - Element[] (e.g., a DOM element array like `[document.createElement("div")]`) 40 | - An array consisting Strings and DOM elements 41 | -------------------------------------------------------------------------------- /content/effects/crop.md: -------------------------------------------------------------------------------- 1 | # Crop 2 | 3 | The Crop effect lets users crop their images before uploading them. Users can also choose which aspect ratio they want to crop by -- free, square, or 16:9. 4 | 5 | ```ts 6 | import { Uppload, Crop } from "uppload"; 7 | 8 | const profilePicture = new Uppload(); 9 | profilePicture.use(new Crop()); 10 | ``` 11 | 12 | You can force the aspect ratio. For example, only allow squares (1:1): 13 | 14 | ```ts 15 | profilePicture.use( 16 | new Crop({ 17 | aspectRatio: 1, 18 | }) 19 | ); 20 | ``` 21 | 22 | You can also customize the aspect ratio options given to users. By default, the free aspect ratio has the value `NaN`: 23 | 24 | ```ts 25 | profilePicture.use( 26 | new Crop({ 27 | aspectRatioOptions: { 28 | free: NaN, 29 | square: 1, 30 | "16:9": 16 / 9, 31 | }, 32 | }) 33 | ); 34 | ``` 35 | 36 | If you don't want users to edit the aspect ratio, you can use the `hideAspectRatioSettings` property. This is the default case if you specify an aspect ratio: 37 | 38 | ```ts 39 | profilePicture.use( 40 | new Crop({ 41 | hideAspectRatioSettings: true 42 | ); 43 | ``` 44 | 45 | ![Screenshot of the Crop effect](/assets/screenshots/crop.png) 46 | -------------------------------------------------------------------------------- /content/effects/filter.md: -------------------------------------------------------------------------------- 1 | # Filter effects 2 | 3 | Filter effects use CSS filters on SVG and allow users to drag an input range to add a filter. Available filter effects are: 4 | 5 | | Service name | Class name | 6 | | ------------ | ------------ | 7 | | Blur | `Blur` | 8 | | Brightness | `Brightness` | 9 | | Contrast | `Contrast` | 10 | | Grayscale | `Grayscale` | 11 | | HueRotate | `HueRotate` | 12 | | Invert | `Invert` | 13 | | Saturate | `Saturate` | 14 | | Sepia | `Sepia` | 15 | 16 | In the following example, we're using the Blur effect is used, but all filter effects have the same usage: 17 | 18 | ```ts 19 | import { Uppload, Blur } from "uppload"; 20 | 21 | const profilePicture = new Uppload(); 22 | profilePicture.use(new Blur()); 23 | ``` 24 | 25 | ## Development 26 | 27 | All filter effects are inherited from the `UpploadFilterBaseClass` class, and you can create your own filter effects too: 28 | 29 | ```ts 30 | import { UpploadFilterBaseClass } from "uppload"; 31 | 32 | class Brightness extends UpploadFilterBaseClass { 33 | name = "brightness"; 34 | icon = "your-svg-icon-string"; 35 | cssFilter = "brightness"; 36 | unit = "%"; 37 | value = 0; 38 | min = 0; 39 | max = 100; 40 | } 41 | ``` 42 | 43 | In the above example, the CSS filter applied to the SVG will be `filter: brightness(10%)` if the value is `10`. 44 | 45 | ![Screenshot of the Brightness effect](/assets/screenshots/brightness.png) 46 | -------------------------------------------------------------------------------- /content/effects/flip.md: -------------------------------------------------------------------------------- 1 | # Flip 2 | 3 | The Flip effect lets users flip their images before uploading them. Users can choose to flip vertically or horizontally. 4 | 5 | ```ts 6 | import { Uppload, Flip } from "uppload"; 7 | 8 | const profilePicture = new Uppload(); 9 | profilePicture.use(new Flip()); 10 | ``` 11 | 12 | ![Screenshot of the Flip effect](/assets/screenshots/flip.png) 13 | -------------------------------------------------------------------------------- /content/effects/index.md: -------------------------------------------------------------------------------- 1 | # Effects 2 | 3 | Uppload effects let users edit their images before uploading them. Uppload comes with 10+ effects built-in, and you can pick and choose the ones you'd like to keep. This way, your bundle will be lean and no unnecessary code will be loaded. 4 | -------------------------------------------------------------------------------- /content/effects/rotate.md: -------------------------------------------------------------------------------- 1 | # Rotate 2 | 3 | The Rotate effect lets users rotate their images before uploading them. 4 | 5 | ```ts 6 | import { Uppload, Rotate } from "uppload"; 7 | 8 | const profilePicture = new Uppload(); 9 | profilePicture.use(new Rotate()); 10 | ``` 11 | 12 | ![Screenshot of the Rotate effect](/assets/screenshots/rotate.png) 13 | -------------------------------------------------------------------------------- /content/examples/index.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | These are some examples to help you get started with Uppload quickly. [elninotech/uppload-examples](https://github.com/elninotech/uppload-examples) is the monorepository containing them: 4 | -------------------------------------------------------------------------------- /content/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | - [Can there be multiple instances of Uppload on a page?](/multiple-instances) 4 | - [How can I migrate from from Uppload v1.x to v2?](/migrating-from-1x) 5 | - [Can I upload multiple files with Uppload](/multiple-files) 6 | - [How does Uppload compare to Uploadcare or Uppy?](/compare) 7 | - [How do I configure a maximum file size?](/services/local) 8 | - [How do I configure which file types are allowed?](/services/local) 9 | -------------------------------------------------------------------------------- /content/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | Uppload 2 lets you build a custom Uppload package to make sure you only bundle the code you actually need. To get started, add Uppload to your project: 4 | 5 | ```bash 6 | npm install uppload 7 | ``` 8 | 9 | Most commonly, you'd want to add the drag-and-drop and file select service (known as [Local](/services/local)), along with the [XHR Uploader](/uploaders/xhr) which will send an HTTP POST request to your backend. 10 | 11 | Then, you can initialize your plugin with these plugins and your language of choice. In this example, we're also adding the "Import from Instagram" service: 12 | 13 | ```ts 14 | import { Uppload, Local, Instagram, xhrUploader, en } from "uppload"; 15 | 16 | const uploader = new Uppload({ 17 | lang: en, 18 | uploader: xhrUploader({ 19 | endpoint: "https://example.com/upload", 20 | }), 21 | }); 22 | uploader.use([new Local(), new Instagram()]); 23 | ``` 24 | 25 | You should also include the styles required for Uppload. You can use a bundler like Webpack and import it in Sass, or directly import it your HTML. You need to import `uppload.css` and a [theme](/themes): 26 | 27 | ```scss 28 | @import "uppload/dist/uppload.css"; 29 | @import "uppload/dist/themes/light.css"; 30 | ``` 31 | 32 | Now, let's say you have an HTML webpage like the following. Here, we have a ` 38 | 39 | ``` 40 | 41 | You can add properties in the Uppload constructor to automate that functionality: 42 | 43 | ```ts 44 | const uploader = new Uppload({ 45 | lang: en, 46 | uploader: xhrUploader({ 47 | endpoint: "https://example.com/upload", 48 | }), 49 | bind: document.querySelector("img.profile-pic"), 50 | call: document.querySelector("button.pic-btn"), 51 | }); 52 | uploader.use([new Local(), new Instagram()]); 53 | ``` 54 | 55 | Instead of using the `bind` and `call` properties, you can also directly use the [Uppload API](/api). For example, if you want to programatically open or close the widget: 56 | 57 | ```ts 58 | // Open the plugin 59 | uppload.open(); 60 | 61 | setTimeout(() => { 62 | // Close the plugin 63 | uppload.close(); 64 | }, 5000); 65 | ``` 66 | 67 | Now that you're ready, it's time to add some [Services](/services) or view [Examples](/examples). 68 | -------------------------------------------------------------------------------- /content/help/index.md: -------------------------------------------------------------------------------- 1 | # Help 2 | 3 | These are the usage guidelines for users uploading images using the Uppload widget. If you're a developer, you might want to read the [Documentation](/) instead. 4 | -------------------------------------------------------------------------------- /content/help/services/camera.md: -------------------------------------------------------------------------------- 1 | # Camera 2 | 3 | ## How do I take a photo? 4 | 5 | - If your browser supports the camera feature, you will see the icon when you open the widget 6 | - Click on that icon and grant the required permission 7 | - Press the "Click photo" button to take a photo 8 | 9 | ## Why don't I see the camera icon? 10 | 11 | If you don't see the camera icon, it could be because: 12 | 13 | - Your browser does not support reading your camera's video; if this is the case, you should update your browser 14 | - The website has not enabled the camera feature 15 | 16 | ## Why can't I see my camera image? 17 | 18 | If you've clicked on the camera icon, but you don't see your picture, this could be because: 19 | 20 | - You have not granted the sufficient permission to access your camera 21 | - Your browser does not support reading your camera's video 22 | - Your camera does not work 23 | 24 | ### Grant permission 25 | 26 | Click on the information icon (an "i" or _lock_ icon on the top left of your web browser), and choose "Allow" under "Camera". This option may be in a different place depending on your browser. 27 | 28 | ### Update your browser 29 | 30 | If you're not using a modern web browser like Chrome, Firefox, Safari, Opera, or Edge, you should download the most recent version of any of those browser. If you're already using one of them, update them by going to your browser settings. 31 | 32 | ![Camera of the Camera service](/assets/screenshots/camera.png) 33 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/9gag.md: -------------------------------------------------------------------------------- 1 | # Import image from 9GAG 2 | 3 | ## How do I upload an image using 9GAG? 4 | 5 | You can import an image using its 9GAG URL and then perform manipulations (like cropping or filters). 9GAG URLs start with "9gag.com". 6 | 7 | 1. Find the 9GAG URL of your photo and copy it 8 | 2. Click on the "9GAG" button in the widget 9 | 3. Paste the 9GAG URL in the textbox 10 | 4. Click on the "Import from 9GAG" button 11 | 12 | ## How do I find the 9GAG URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the 9GAG icon? 17 | 18 | If you don't see the 9GAG icon, it could be because: 19 | 20 | - Your browser does not support importing an image from 9GAG; if this is the case, you should update your browser 21 | - The website has not enabled the 9GAG import feature 22 | 23 | ![Screenshot of the 9GAG service](/assets/screenshots/9gag.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/artstation.md: -------------------------------------------------------------------------------- 1 | # Import image from ArtStation 2 | 3 | ## How do I upload an image using ArtStation? 4 | 5 | You can import an image using its ArtStation URL and then perform manipulations (like cropping or filters). ArtStation URLs start with "artstation.com". 6 | 7 | 1. Find the ArtStation URL of your photo and copy it 8 | 2. Click on the "ArtStation" button in the widget 9 | 3. Paste the ArtStation URL in the textbox 10 | 4. Click on the "Import from ArtStation" button 11 | 12 | ## How do I find the ArtStation URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the ArtStation icon? 17 | 18 | If you don't see the ArtStation icon, it could be because: 19 | 20 | - Your browser does not support importing an image from ArtStation; if this is the case, you should update your browser 21 | - The website has not enabled the ArtStation import feature 22 | 23 | ![Screenshot of the ArtStation service](/assets/screenshots/artstation.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/deviantart.md: -------------------------------------------------------------------------------- 1 | # Import image from DeviantArt 2 | 3 | ## How do I upload an image using DeviantArt? 4 | 5 | You can import an image using its DeviantArt URL and then perform manipulations (like cropping or filters). DeviantArt URLs start with "deviantart.com" or "fav.me". 6 | 7 | 1. Find the DeviantArt URL of your photo and copy it 8 | 2. Click on the "DeviantArt" button in the widget 9 | 3. Paste the DeviantArt URL in the textbox 10 | 4. Click on the "Import from DeviantArt" button 11 | 12 | ## How do I find the DeviantArt URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the DeviantArt icon? 17 | 18 | If you don't see the DeviantArt icon, it could be because: 19 | 20 | - Your browser does not support importing an image from DeviantArt; if this is the case, you should update your browser 21 | - The website has not enabled the DeviantArt import feature 22 | 23 | ![Screenshot of the DeviantArt service](/assets/screenshots/deviantart.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/facebook.md: -------------------------------------------------------------------------------- 1 | # Import image from Facebook 2 | 3 | ## How do I upload an image using Facebook? 4 | 5 | You can import an image using its Facebook URL and then perform manipulations (like cropping or filters). Facebook URLs start with "facebook.com", "fb.com", or "fb.me". 6 | 7 | 1. Find the Facebook URL of your photo and copy it 8 | 2. Click on the "Facebook" button in the widget 9 | 3. Paste the Facebook URL in the textbox 10 | 4. Click on the "Import from Facebook" button 11 | 12 | ## How do I find the Facebook URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the Facebook icon? 17 | 18 | If you don't see the Facebook icon, it could be because: 19 | 20 | - Your browser does not support importing an image from Facebook; if this is the case, you should update your browser 21 | - The website has not enabled the Facebook import feature 22 | 23 | ![Screenshot of the Facebook service](/assets/screenshots/facebook.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/flickr.md: -------------------------------------------------------------------------------- 1 | # Import image from Flickr 2 | 3 | ## How do I upload an image using Flickr? 4 | 5 | You can import an image using its Flickr URL and then perform manipulations (like cropping or filters). Flickr URLs start with "flickr.com" or "flic.kr". 6 | 7 | 1. Find the Flickr URL of your photo and copy it 8 | 2. Click on the "Flickr" button in the widget 9 | 3. Paste the Flickr URL in the textbox 10 | 4. Click on the "Import from Flickr" button 11 | 12 | ## How do I find the Flickr URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the Flickr icon? 17 | 18 | If you don't see the Flickr icon, it could be because: 19 | 20 | - Your browser does not support importing an image from Flickr; if this is the case, you should update your browser 21 | - The website has not enabled the Flickr import feature 22 | 23 | ![Screenshot of the Flickr service](/assets/screenshots/flickr.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/flipboard.md: -------------------------------------------------------------------------------- 1 | # Import image from Flipboard 2 | 3 | ## How do I upload an image using Flipboard? 4 | 5 | You can import an image using its Flipboard URL and then perform manipulations (like cropping or filters). Flipboard URLs start with "flipboard.com" or "flip.it". 6 | 7 | 1. Find the Flipboard URL of your photo and copy it 8 | 2. Click on the "Flipboard" button in the widget 9 | 3. Paste the Flipboard URL in the textbox 10 | 4. Click on the "Import from Flipboard" button 11 | 12 | ## How do I find the Flipboard URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the Flipboard icon? 17 | 18 | If you don't see the Flipboard icon, it could be because: 19 | 20 | - Your browser does not support importing an image from Flipboard; if this is the case, you should update your browser 21 | - The website has not enabled the Flipboard import feature 22 | 23 | ![Screenshot of the Flipboard service](/assets/screenshots/flipboard.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/fotki.md: -------------------------------------------------------------------------------- 1 | # Import image from Fotki 2 | 3 | ## How do I upload an image using Fotki? 4 | 5 | You can import an image using its Fotki URL and then perform manipulations (like cropping or filters). Fotki URLs start with "fotki.com". 6 | 7 | 1. Find the Fotki URL of your photo and copy it 8 | 2. Click on the "Fotki" button in the widget 9 | 3. Paste the Fotki URL in the textbox 10 | 4. Click on the "Import from Fotki" button 11 | 12 | ## How do I find the Fotki URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the Fotki icon? 17 | 18 | If you don't see the Fotki icon, it could be because: 19 | 20 | - Your browser does not support importing an image from Fotki; if this is the case, you should update your browser 21 | - The website has not enabled the Fotki import feature 22 | 23 | ![Screenshot of the Fotki service](/assets/screenshots/fotki.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/index.md: -------------------------------------------------------------------------------- 1 | # Import from web service 2 | 3 | You can import an image from popular web services. 4 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/instagram.md: -------------------------------------------------------------------------------- 1 | # Import image from Instagram 2 | 3 | ## How do I upload an image using Instagram? 4 | 5 | You can import an image using its Instagram URL and then perform manipulations (like cropping or filters). Instagram URLs start with "instagram.com" or "instagr.am". 6 | 7 | 1. Find the Instagram URL of your photo and copy it 8 | 2. Click on the "Instagram" button in the widget 9 | 3. Paste the Instagram URL in the textbox 10 | 4. Click on the "Import from Instagram" button 11 | 12 | ## How do I find the Instagram URL of an image? 13 | 14 | ### Web 15 | 16 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 17 | 18 | ### Mobile 19 | 20 | Using the Instagram app, click on the "More" (three dot) icon on the top-right of an image and select "Copy Link". 21 | 22 | ## Why don't I see the Instagram icon? 23 | 24 | If you don't see the Instagram icon, it could be because: 25 | 26 | - Your browser does not support importing an image from Instagram; if this is the case, you should update your browser 27 | - The website has not enabled the Instagram import feature 28 | 29 | ![Screenshot of the Instagram service](/assets/screenshots/instagram.png) 30 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/linkedin.md: -------------------------------------------------------------------------------- 1 | # Import image from LinkedIn 2 | 3 | ## How do I upload an image using LinkedIn? 4 | 5 | You can import an image using its LinkedIn URL and then perform manipulations (like cropping or filters). LinkedIn URLs start with "linkedin.com". 6 | 7 | 1. Find the LinkedIn URL of your photo and copy it 8 | 2. Click on the "LinkedIn" button in the widget 9 | 3. Paste the LinkedIn URL in the textbox 10 | 4. Click on the "Import from LinkedIn" button 11 | 12 | ## How do I find the LinkedIn URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the LinkedIn icon? 17 | 18 | If you don't see the LinkedIn icon, it could be because: 19 | 20 | - Your browser does not support importing an image from LinkedIn; if this is the case, you should update your browser 21 | - The website has not enabled the LinkedIn import feature 22 | 23 | ![Screenshot of the LinkedIn service](/assets/screenshots/linkedin.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/pinterest.md: -------------------------------------------------------------------------------- 1 | # Import image from Pinterest 2 | 3 | ## How do I upload an image using Pinterest? 4 | 5 | You can import an image using its Pinterest URL and then perform manipulations (like cropping or filters). Pinterest URLs start with "pinterest.com" or "pin.it". 6 | 7 | 1. Find the Pinterest URL of your photo and copy it 8 | 2. Click on the "Pinterest" button in the widget 9 | 3. Paste the Pinterest URL in the textbox 10 | 4. Click on the "Import from Pinterest" button 11 | 12 | ## How do I find the Pinterest URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the Pinterest icon? 17 | 18 | If you don't see the Pinterest icon, it could be because: 19 | 20 | - Your browser does not support importing an image from Pinterest; if this is the case, you should update your browser 21 | - The website has not enabled the Pinterest import feature 22 | 23 | ![Screenshot of the Pinterest service](/assets/screenshots/pinterest.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/reddit.md: -------------------------------------------------------------------------------- 1 | # Import image from Reddit 2 | 3 | ## How do I upload an image using Reddit? 4 | 5 | You can import an image using its Reddit URL and then perform manipulations (like cropping or filters). Reddit URLs start with "reddit.com". 6 | 7 | 1. Find the Reddit URL of your photo and copy it 8 | 2. Click on the "Reddit" button in the widget 9 | 3. Paste the Reddit URL in the textbox 10 | 4. Click on the "Import from Reddit" button 11 | 12 | ## How do I find the Reddit URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the Reddit icon? 17 | 18 | If you don't see the Reddit icon, it could be because: 19 | 20 | - Your browser does not support importing an image from Reddit; if this is the case, you should update your browser 21 | - The website has not enabled the Reddit import feature 22 | 23 | ![Screenshot of the Reddit service](/assets/screenshots/reddit.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/tumblr.md: -------------------------------------------------------------------------------- 1 | # Import image from Tumblr 2 | 3 | ## How do I upload an image using Tumblr? 4 | 5 | You can import an image using its Tumblr URL and then perform manipulations (like cropping or filters). Tumblr URLs start with "tumblr.com". 6 | 7 | 1. Find the Tumblr URL of your photo and copy it 8 | 2. Click on the "Tumblr" button in the widget 9 | 3. Paste the Tumblr URL in the textbox 10 | 4. Click on the "Import from Tumblr" button 11 | 12 | ## How do I find the Tumblr URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the Tumblr icon? 17 | 18 | If you don't see the Tumblr icon, it could be because: 19 | 20 | - Your browser does not support importing an image from Tumblr; if this is the case, you should update your browser 21 | - The website has not enabled the Tumblr import feature 22 | 23 | ![Screenshot of the Tumblr service](/assets/screenshots/tumblr.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/twitter.md: -------------------------------------------------------------------------------- 1 | # Import image from Twitter 2 | 3 | ## How do I upload an image using Twitter? 4 | 5 | You can import an image using its Twitter URL and then perform manipulations (like cropping or filters). Twitter URLs start with "twitter.com" or "t.co". 6 | 7 | 1. Find the Twitter URL of your photo and copy it 8 | 2. Click on the "Twitter" button in the widget 9 | 3. Paste the Twitter URL in the textbox 10 | 4. Click on the "Import from Twitter" button 11 | 12 | ## How do I find the Twitter URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the Twitter icon? 17 | 18 | If you don't see the Twitter icon, it could be because: 19 | 20 | - Your browser does not support importing an image from Twitter; if this is the case, you should update your browser 21 | - The website has not enabled the Twitter import feature 22 | 23 | ![Screenshot of the Twitter service](/assets/screenshots/twitter.png) 24 | -------------------------------------------------------------------------------- /content/help/services/import-from-web-service/weheartit.md: -------------------------------------------------------------------------------- 1 | # Import image from We Heart It 2 | 3 | ## How do I upload an image using We Heart It? 4 | 5 | You can import an image using its We Heart It URL and then perform manipulations (like cropping or filters). We Heart It URLs start with "weheartit.com". 6 | 7 | 1. Find the We Heart It URL of your photo and copy it 8 | 2. Click on the "We Heart It" button in the widget 9 | 3. Paste the We Heart It URL in the textbox 10 | 4. Click on the "Import from We Heart It" button 11 | 12 | ## How do I find the We Heart It URL of an image? 13 | 14 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 15 | 16 | ## Why don't I see the We Heart It icon? 17 | 18 | If you don't see the We Heart It icon, it could be because: 19 | 20 | - Your browser does not support importing an image from We Heart It; if this is the case, you should update your browser 21 | - The website has not enabled the We Heart It import feature 22 | 23 | ![Screenshot of the We Heart It service](/assets/screenshots/weheartit.png) 24 | -------------------------------------------------------------------------------- /content/help/services/local.md: -------------------------------------------------------------------------------- 1 | # Select a local file 2 | 3 | ## How do I upload a file from my device? 4 | 5 | You can upload a file from your computer by dragging and dropping the file in the "Drop files here" area, or clicking the "Select a file" button. 6 | 7 | 1. Click on the "Select a file" button 8 | 2. Select the file from your device's file manager 9 | 3. Click on the "Upload" button 10 | 11 | ## How do I find a file? 12 | 13 | To learn how to select a file from the file manager on your device, follow the guidelines for your operating system below. 14 | 15 | - Finder on macOS: [Get to know the Finder on your Mac](https://support.apple.com/en-ca/HT201732) on apple.com 16 | - Files on Android: [Find & delete files on Android](https://support.google.com/android/answer/9110661?hl=en) on google.com 17 | - File Explorer on Windows 10: [Find your documents in Windows 10](https://support.microsoft.com/en-us/help/4026289/windows-10-find-your-documents) on microsoft.com 18 | - Files on iOS: [View files and folders in Files on iPhone](https://support.apple.com/guide/iphone/view-files-and-folders-iphc61044c11/ios) on apple.com 19 | - Files on iPadOS: [View files and folders in Files on iPad](https://support.apple.com/guide/ipad/view-files-and-folders-ipad49b77901/ipados) on apple.com 20 | 21 | ## Why don't I see the "Choose file" icon? 22 | 23 | If you don't see the "Choose file" icon, it could be because: 24 | 25 | - Your browser does not support selecting an image from a device; if this is the case, you should update your browser 26 | - The website has not enabled the "Choose file" feature 27 | 28 | ![Screenshot of the Local service](/assets/screenshots/local.png) 29 | -------------------------------------------------------------------------------- /content/help/services/screenshot.md: -------------------------------------------------------------------------------- 1 | # Take a webpage screenshot 2 | 3 | ## How do I upload an image from a screenshot? 4 | 5 | You can take a webpage's screenshot and then perform manipulations (like cropping or filters). Webpage URLs start with "http://" or "https://". 6 | 7 | 1. Find the URL of the webpage you want to screenshot and copy it 8 | 2. Click on the "Screenshot" button in the widget 9 | 3. Paste the webpage URL in the textbox 10 | 4. Click on the "Take screenshot" button 11 | 12 | ## How do I find the URL of a webpage? 13 | 14 | ### Web 15 | 16 | On the web, open webpage you want to screenshot and copy the URL from your browser's address bar. 17 | 18 | ## Why don't I see the screenshot icon? 19 | 20 | If you don't see the Screenshot icon, it could be because: 21 | 22 | - Your browser does not support take a URL's screenshot; if this is the case, you should update your browser 23 | - The website has not enabled the Screenshot feature 24 | 25 | ![Screenshot of the Screenshot service](/assets/screenshots/screenshot.png) 26 | -------------------------------------------------------------------------------- /content/help/services/search/giphy.md: -------------------------------------------------------------------------------- 1 | # Search GIPHY for photos 2 | 3 | ## How do I upload an image using GIPHY? 4 | 5 | You can search for images on GIPHY and then edit and upload the image. 6 | 7 | 1. Click on the "GIPHY" button in the widget 8 | 2. Type your search query in the text box 9 | 3. Click on the "Search on GIPHY" button 10 | 4. Click on the image you want to import from the results 11 | 12 | ## Why don't I see the GIPHY icon? 13 | 14 | If you don't see the GIPHY icon, it could be because: 15 | 16 | - Your browser does not support importing an image from GIPHY; if this is the case, you should update your browser 17 | - The website has not enabled the GIPHY import feature 18 | 19 | ## Why don't I see any search results? 20 | 21 | If you've typed your query and click on the search button but you don't see any search results, it could be because: 22 | 23 | - Your browser does not support searching an image on GIPHY; if this is the case, you should update your browser 24 | - The website has not enabled the searching GIPHY feature 25 | 26 | ![Screenshot of the GIPHY service](/assets/screenshots/giphy.png) 27 | -------------------------------------------------------------------------------- /content/help/services/search/index.md: -------------------------------------------------------------------------------- 1 | # Search for images 2 | 3 | You can search for images from popular photography galleries. 4 | -------------------------------------------------------------------------------- /content/help/services/search/pexels.md: -------------------------------------------------------------------------------- 1 | # Search Pexels for photos 2 | 3 | ## How do I upload an image using Pexels? 4 | 5 | You can search for images on Pexels and then edit and upload the image. 6 | 7 | 1. Click on the "Pexels" button in the widget 8 | 2. Type your search query in the text box 9 | 3. Click on the "Search on Pexels" button 10 | 4. Click on the image you want to import from the results 11 | 12 | ## Why don't I see the Pexels icon? 13 | 14 | If you don't see the Pexels icon, it could be because: 15 | 16 | - Your browser does not support importing an image from Pexels; if this is the case, you should update your browser 17 | - The website has not enabled the Pexels import feature 18 | 19 | ## Why don't I see any search results? 20 | 21 | If you've typed your query and click on the search button but you don't see any search results, it could be because: 22 | 23 | - Your browser does not support searching an image on Pexels; if this is the case, you should update your browser 24 | - The website has not enabled the searching Pexels feature 25 | 26 | ![Screenshot of the Pexels service](/assets/screenshots/pexels.png) 27 | -------------------------------------------------------------------------------- /content/help/services/search/pixabay.md: -------------------------------------------------------------------------------- 1 | # Search Pixabay for photos 2 | 3 | ## How do I upload an image using Pixabay? 4 | 5 | You can search for images on Pixabay and then edit and upload the image. 6 | 7 | 1. Click on the "Pixabay" button in the widget 8 | 2. Type your search query in the text box 9 | 3. Click on the "Search on Pixabay" button 10 | 4. Click on the image you want to import from the results 11 | 12 | ## Why don't I see the Pixabay icon? 13 | 14 | If you don't see the Pixabay icon, it could be because: 15 | 16 | - Your browser does not support importing an image from Pixabay; if this is the case, you should update your browser 17 | - The website has not enabled the Pixabay import feature 18 | 19 | ## Why don't I see any search results? 20 | 21 | If you've typed your query and click on the search button but you don't see any search results, it could be because: 22 | 23 | - Your browser does not support searching an image on Pixabay; if this is the case, you should update your browser 24 | - The website has not enabled the searching Pixabay feature 25 | 26 | ![Screenshot of the Pixabay service](/assets/screenshots/pixabay.png) 27 | -------------------------------------------------------------------------------- /content/help/services/search/unsplash.md: -------------------------------------------------------------------------------- 1 | # Search Unsplash for photos 2 | 3 | ## How do I upload an image using Unsplash? 4 | 5 | You can search for images on Unsplash and then edit and upload the image. 6 | 7 | 1. Click on the "Unsplash" button in the widget 8 | 2. Type your search query in the text box 9 | 3. Click on the "Search on Unsplash" button 10 | 4. Click on the image you want to import from the results 11 | 12 | ## Why don't I see the Unsplash icon? 13 | 14 | If you don't see the Unsplash icon, it could be because: 15 | 16 | - Your browser does not support importing an image from Unsplash; if this is the case, you should update your browser 17 | - The website has not enabled the Unsplash import feature 18 | 19 | ## Why don't I see any search results? 20 | 21 | If you've typed your query and click on the search button but you don't see any search results, it could be because: 22 | 23 | - Your browser does not support searching an image on Unsplash; if this is the case, you should update your browser 24 | - The website has not enabled the searching Unsplash feature 25 | 26 | ![Screenshot of the Unsplash service](/assets/screenshots/unsplash.png) 27 | -------------------------------------------------------------------------------- /content/help/services/url.md: -------------------------------------------------------------------------------- 1 | # Import image from URL 2 | 3 | ## How do I upload an image from a URL? 4 | 5 | You can import an image using its URL and then perform manipulations (like cropping or filters). URL start with "http://" or "https://". 6 | 7 | 1. Find the URL of your photo and copy it 8 | 2. Click on the "Direct URL" button in the widget 9 | 3. Paste the URL in the textbox 10 | 4. Click on the "Import from URL" button 11 | 12 | ## How do I find the URL of an image? 13 | 14 | ### Web 15 | 16 | On the web, open the picture you want to select and copy the URL from your browser's address bar. 17 | 18 | ## Why don't I see the Direct URL icon? 19 | 20 | If you don't see the Direct URL icon, it could be because: 21 | 22 | - Your browser does not support importing an image from URL; if this is the case, you should update your browser 23 | - The website has not enabled the Direct URL import feature 24 | 25 | ![Screenshot of the URL service](/assets/screenshots/url.png) 26 | -------------------------------------------------------------------------------- /content/i18n.md: -------------------------------------------------------------------------------- 1 | # Internationalization 2 | 3 | Uppload is very easy to localize. For example, if you want to use Uppload in English, simply import `en` and specify it in the `lang` parameter of the constructor: 4 | 5 | ```ts 6 | import { Uppload, en } from "uppload"; 7 | 8 | const profilePicture = new Uppload({ 9 | lang: en, 10 | }); 11 | ``` 12 | 13 | If you want to overwrite some keys, you can do that too: 14 | 15 | ```ts 16 | const lang = en; 17 | lang.services.local.drop = "Drag and drop files here"; 18 | 19 | const profilePicture = new Uppload({ lang }); 20 | ``` 21 | 22 | Similarly, you can add your own language pack: 23 | 24 | ```ts 25 | const nl = { 26 | errors.response_not_ok: "Error", 27 | imagesPoweredBy: "Images powered by $1$" 28 | // ...and so on 29 | }; 30 | 31 | const profilePicture = new Uppload({ lang: nl }); 32 | ``` 33 | 34 | As you'll see in the code sample above, terms such as `$1$` (in general, `$n$` where `n` is a number) are replaced by variables in templates using the [i18n helper](https://github.com/elninotech/uppload/tree/master/src/helpers/i18n.ts). 35 | 36 | Finally, if you're building your own language pack for Uppload, you can also contribute it to the open-source project. Simply go ahead and create a `lang.ts` file in the `./src/i18n` directory (where `lang` is the two letter ISO code for the language) using `en.ts` as the base. 37 | 38 | ## Dynamically changing language 39 | 40 | If you want to change the language once the plugin has already initialized or opened, you can use the following: 41 | 42 | ```ts 43 | import { Uppload, en, nl } from "uppload"; 44 | 45 | const profilePicture = new Uppload({ lang: en }); 46 | 47 | // When users click on the button, change language to dutch 48 | const button = document.querySelector("button"); 49 | if (button) 50 | button.addEventListener("click", () => 51 | profilePicture.updateSettings({ lang: nl }) 52 | ); 53 | ``` 54 | -------------------------------------------------------------------------------- /content/index.md: -------------------------------------------------------------------------------- 1 | # Uppload 2 | 3 | Uppload is a better JavaScript image uploader. It's highly customizable with 30+ plugins, completely free and open-source, and can be used with any file uploading backend. 4 | 5 | - [Getting started](/getting-started) 6 | - [Configuration](/configuration) 7 | - [Examples](/examples) 8 | - [A-la-carte (treeshaking) plugins](/treeshaking) 9 | - [Uppload API](/api) 10 | - [Listening to events](/listening-to-events) 11 | - [Services](/services) (20+ ways to select a file) 12 | - [Effects](/effects) (10+ ways to edit a file) 13 | - [Uploaders](/uploaders) (ways to send a file to the server) 14 | - [Themes](/themes) 15 | - [Backends](/backends) 16 | - [Frontend frameworks](/wrappers) 17 | - [Blog](/blog) 18 | - [Image compression](/compression) 19 | - [Internationalization](/i18n) 20 | - [Accessibility](/a11y) 21 | - [Compare Uppload](/compare) 22 | - [FAQs](/faq) 23 | -------------------------------------------------------------------------------- /content/listening-to-events.md: -------------------------------------------------------------------------------- 1 | # Listening to events 2 | 3 | You can listen to events by using the `on` function: 4 | 5 | ```ts 6 | import { Uppload, en } from "uppload"; 7 | const uploader = new Uppload({ lang: en }); 8 | 9 | uploader.on("upload", (url: string) => { 10 | console.log(`The URL of the uploaded file is: ${url}`); 11 | }); 12 | ``` 13 | 14 | If you want to stop listening to the event, you can use the `off` function: 15 | 16 | ```ts 17 | const errorLogger = (error: string) => { 18 | console.log("The error message is", error); 19 | }; 20 | uploader.on("error", errorLogger); // Start listening 21 | /* . . . */ 22 | uploader.off("error", errorLogger); // Stop listening 23 | ``` 24 | 25 | You can listen to the following event: 26 | 27 | | Event | Description | 28 | | --------------- | --------------------------------- | 29 | | `ready` | Plugin is ready and initialized | 30 | | `bind` | The value of a new URL is applied | 31 | | `open` | Plugin is opened | 32 | | `close` | Plugin is closed | 33 | | `before-upload` | File upload has started | 34 | | `upload` | File upload has completed | 35 | | `error` | File upload got an error | 36 | -------------------------------------------------------------------------------- /content/migrating-from-1x.md: -------------------------------------------------------------------------------- 1 | # Migrating from Uppload 1.x 2 | 3 | Uppload is rewritten from the ground up in TypeScript, and it's not backwards-compatible with Uppload 1.x. 4 | 5 | To make sure there are no collisions, even the import style is different. For Uppload 1.x, you would use the default import as the class: 6 | 7 | ```js 8 | import Uppload from "uppload"; 9 | ``` 10 | 11 | But Uppload 2.x requires you to import the Uppload class like this: 12 | 13 | ```ts 14 | import { Uppload } from "uppload"; 15 | ``` 16 | 17 | Uppload 1.x had all packages built-in, so you could initialize it like this: 18 | 19 | ```js 20 | const profilePicture = new Uppload({ 21 | value: "https://randomuser.me/api/portraits/women/17.jpg", 22 | }); 23 | ``` 24 | 25 | In Uppload 2.x, however, you have to import the features you require. 26 | 27 | ```ts 28 | import { Uppload, Instagram, Pexels } from "uppload"; 29 | const profilePicture = new Uppload({ 30 | value: "https://randomuser.me/api/portraits/women/17.jpg", 31 | }); 32 | profilePicture.use(new Instagram(), new Pexels()); 33 | ``` 34 | 35 | Finally, Uppload 1.x didn't support TypeScript, but Uppload 2.x is written in TypeScript so it works with both JavaScript (ES6) and TypeScript. 36 | -------------------------------------------------------------------------------- /content/multiple-files.md: -------------------------------------------------------------------------------- 1 | # Uploading multiple files 2 | 3 | Uppload is designed for single-image uploads, like profile and cover photos. However, you can use the `multiple` property in the constructor object, but this is not recommended because effects only work with single images. 4 | 5 | ```ts 6 | const uploader = new Uppload({ 7 | multiple: true, 8 | }); 9 | ``` 10 | 11 | If users select only a single image, effects like crop and filters continue to work, but when users select more than one file, effects are skipped and the images are directly uploaded. You'll also have to use a custom uploader which supports uploading multiple files at once. 12 | -------------------------------------------------------------------------------- /content/multiple-instances.md: -------------------------------------------------------------------------------- 1 | # Multiple Uppload instances 2 | 3 | You can have multiple instances of Uppload on a webpage. 4 | 5 | ```ts 6 | import { Uppload, Local, Fetch, en } from "uppload"; 7 | 8 | const profilePicture = new Uppload({ lang: en }); 9 | profilePicture.use([new Local(), new Fetch("http://localhost:3000/upload")]); 10 | 11 | const coverImage = new Uppload({ lang: en }); 12 | coverImage.use([new Local(), new Fetch("http://localhost:3000/upload")]); 13 | ``` 14 | -------------------------------------------------------------------------------- /content/services/camera.md: -------------------------------------------------------------------------------- 1 | # Camera service 2 | 3 | Uppload's Camera service allows users to click a photo using their device's camera. 4 | 5 | ![Screenshot of Camera service](/assets/screenshots/camera.png) 6 | 7 | To add the Camera service to your build, simply import it, initialize the class to an object, and use the `Uppload.use()` function: 8 | 9 | ```ts 10 | import { Uppload, Camera } from "uppload"; 11 | 12 | const picture = new Uppload(); 13 | picture.use(new Camera()); 14 | ``` 15 | 16 | This service is built for non-mobile devices (like laptops with webcams) since the native file picker (for Uppload, that is the [Local](/services/local) service) allows users to click photos in all major mobile operating systems. 17 | -------------------------------------------------------------------------------- /content/services/import-from-web-service.md: -------------------------------------------------------------------------------- 1 | # Import from web service 2 | 3 | Users can import photos from web services in Uppload by specifying the resource URL. Currently supported services are: 4 | 5 | | Service name | Class name | 6 | | ------------------ | ------------ | 7 | | Direct URL | `URL` | 8 | | Instagram | `Instagram` | 9 | | Facebook | `Facebook` | 10 | | Twitter | `Twitter` | 11 | | Flickr | `Flickr` | 12 | | 9GAG | `NineGag` | 13 | | Pinterest | `Pinterest` | 14 | | DeviantArt | `DeviantArt` | 15 | | ArtStation | `ArtStation` | 16 | | Flipboard | `Flipboard` | 17 | | Fotki | `Fotki` | 18 | | LinkedIn | `LinkedIn` | 19 | | Reddit | `Reddit` | 20 | | Tumblr | `Tumblr` | 21 | | WeHeartIt | `WeHeartIt` | 22 | | Webpage screenshot | `Screenshot` | 23 | 24 | ![Screenshot of Instagram service](/assets/screenshots/instagram.png) 25 | 26 | All web services work the same way. For example, to add the Instagram service to your build, simply import it, initialize the class to an object, and use the `Uppload.use()` function: 27 | 28 | ```ts 29 | import { Uppload, Instagram } from "uppload"; 30 | 31 | const picture = new Uppload(); 32 | picture.use(new Instagram()); 33 | ``` 34 | 35 | ## Development 36 | 37 | Under the hood, all of these web service importers use the [Microlink API](https://microlink.io), so they have a shared codebase. Uppload also caches API responses in local storage. 38 | 39 | If you want to build your own Microlink-based web service importer, you can do this: 40 | 41 | ```ts 42 | import { MicrolinkBaseClass } from "uppload"; 43 | 44 | export default class ServiceName extends MicrolinkBaseClass { 45 | name = "service-name"; 46 | icon = "your-svg-icon"; 47 | color = "#cc3366"; 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /content/services/index.md: -------------------------------------------------------------------------------- 1 | # Services 2 | 3 | Uppload comes with several services built-in, and you can pick and choose the ones you'd like to keep. This way, your bundle will be lean and no unnecessary code will be loaded. 4 | -------------------------------------------------------------------------------- /content/services/local.md: -------------------------------------------------------------------------------- 1 | # Local service 2 | 3 | Uppload's Local service allows users to upload images from their computer. Users can drag and drop files or click on the "Select a file" button to open a native file browser. 4 | 5 | ![Screenshot of Local service](/assets/screenshots/local.png) 6 | 7 | To add the Local service to your build, simply import it, initialize the class to an object, and use the `Uppload.use()` function: 8 | 9 | ```ts 10 | import { Uppload, Local } from "uppload"; 11 | 12 | const picture = new Uppload(); 13 | picture.use(new Local()); 14 | ``` 15 | 16 | In the constructor parameter, you can specify the mime types you want to support and the maximum file size. By default, JPG, PNG, and GIF images are allowed, since Uppload is primarily an image uploader (but you can easily extend it to be a general-purpose file uploader). 17 | 18 | ```ts 19 | const localServiceWithVideo = new Local({ 20 | maxFileSize: 25000, 21 | mimeTypes: ["image/png", "image/jpeg", "video/mp4"], 22 | }); 23 | ``` 24 | -------------------------------------------------------------------------------- /content/services/search-for-images.md: -------------------------------------------------------------------------------- 1 | # Search for images 2 | 3 | Users can search for photos from web services which have a free API. This is particulatly useful for letting users upload cover photos or photos in blog posts. Currently supported services are: 4 | 5 | | Service name | Class name | 6 | | ------------ | ---------- | 7 | | Unsplash | `Unsplash` | 8 | | Pexels | `Pexels` | 9 | | Pixabay | `Pixabay` | 10 | | GIPHY | `GIPHY` | 11 | 12 | You can sign up for a free API key for all these services, and they are also CORS-friendly. Under the hood, Uppload caches API responses in local storage. 13 | 14 | ![Screenshot of Unsplash service](/assets/screenshots/unsplash.png) 15 | 16 | All search services work the same way. For example, to add the Unsplash service to your build, simply import it, initialize the class with an API key, and use the `Uppload.use()` function: 17 | 18 | ```ts 19 | import { Uppload, Unsplash } from "uppload"; 20 | 21 | const picture = new Uppload(); 22 | picture.use(new Unsplash("your-api-key")); 23 | ``` 24 | -------------------------------------------------------------------------------- /content/themes.md: -------------------------------------------------------------------------------- 1 | # Themes 2 | 3 | Uppload can be themed based on your liking. You can choose from the `dark` or `light` theme, or write your own theme. 4 | 5 | You can import the CSS in your JavaScript using a bundler like Webpack: 6 | 7 | ```ts 8 | import { Uppload } from "uppload"; 9 | import "uppload/dist/uppload.css"; // Basic styles 10 | import "uppload/dist/themes/light.css"; // Light theme 11 | 12 | const uppload = new Uppload(); 13 | ``` 14 | 15 | You can also use the Sass source code instead: 16 | 17 | ```css 18 | @import "uppload/src/styles/uppload.scss"; 19 | @import "uppload/src/themes/light.scss"; 20 | ``` 21 | 22 | Or, use a CDN for styling: 23 | 24 | ```html 25 | 26 | 27 | 31 | 32 | ``` 33 | 34 | ## Custom theme 35 | 36 | Instead of importing a theme, you can also design a custom theme. The best way is to look at the source code of [light.scss](https://github.com/elninotech/uppload/blob/master/src/themes/light.scss) and use the same CSS classes. You'll only have to change the color variables. 37 | 38 | Alternately, you can overwrite some rules instead of building your own theme. The [theme.scss](https://github.com/elninotech/uppload/blob/master/src/themes/theme.scss) file applies the color variables to the widget, so that's a good starting point for that. 39 | -------------------------------------------------------------------------------- /content/treeshaking.md: -------------------------------------------------------------------------------- 1 | # A-la-carte (treeshaking) plugins 2 | 3 | It's very easy to create your own custom build of Uppload, so you only load the feature you want without bloating up your build size. 4 | 5 | For example, you can add the following services: 6 | 7 | - Local (select file from computer) 8 | - Direct URL 9 | - Search on Unsplash and Pexels 10 | 11 | You can import the required services from the `uppload` package and create your package: 12 | 13 | ```ts 14 | import { Uppload, Local, URL, Unsplash, Pexels, en } from "uppload"; 15 | 16 | const uploader = new Uppload({ lang: en }); 17 | uploader.use([ 18 | new Local(), 19 | new URL(), 20 | new Unsplash("unsplash-api-key"), 21 | new Pexels("pexels-api-key"), 22 | ]); 23 | ``` 24 | 25 | Similarly, you can import effects and uploaders and use the `Uppload.use()` function to build your package. 26 | -------------------------------------------------------------------------------- /content/uploaders/custom-uploader.md: -------------------------------------------------------------------------------- 1 | # Writing a custom uploader 2 | 3 | Instead of using the [XHR Uploader](/uploaders/xhr) or [Fetch Uploader](/uploaders/fetch), you can also write your own uploader. 4 | 5 | The uploader function should return a `Promise` which resolves to the URL of the uploaded file on the server. 6 | 7 | ```ts 8 | import { Uppload } from "uppload"; 9 | 10 | const customUploader = (file: Blob): Promise => { 11 | // Here, we take the file and send it to the server 12 | return new Promise((resolve, reject) => { 13 | // Send the file and get the URL 14 | resolve("uploaded-file-url"); 15 | }); 16 | }; 17 | 18 | const profilePicture = new Uppload({ 19 | uploader: customUploader, 20 | }); 21 | ``` 22 | 23 | If your custom uploader supports reporting progress, you can do so too: 24 | 25 | ```ts 26 | const customUploader = ( 27 | file: Blob, 28 | updateProgress: (progress: number) => void 29 | ): Promise => { 30 | // Here, we take the file and send it to the server 31 | return new Promise((resolve, reject) => { 32 | // Keep updating the progress 33 | updateProgress(10); // Progress is now at 10% 34 | // Send the file as soon as it is uploaded 35 | resolve("uploaded-file-url"); 36 | }); 37 | }; 38 | ``` 39 | 40 | For example, a custom XHR request uploader could look like this: 41 | 42 | ```ts 43 | const customUploader = ( 44 | file: Blob, 45 | updateProgress: (progress: number) => void 46 | ): Promise => { 47 | // Here, we take the file and send it to the server 48 | return new Promise((resolve, reject) => { 49 | // First, we create form data with the file 50 | const formData = new FormData(); 51 | formData.append("file", file); 52 | 53 | // Then, we upload the file 54 | const xmlHttp = new XMLHttpRequest(); 55 | xmlHttp.open("POST", "https://example.com/upload", true); 56 | 57 | // We also report the progress back 58 | xmlHttp.addEventListener("progress", event => 59 | updateProgress(event.loaded / event.total); 60 | ); 61 | 62 | // Finally, we report the uploaded file URL 63 | xmlHttp.addEventListener("load", () => 64 | resolve(JSON.parse(xmlHttp.responseText).url); 65 | ); 66 | 67 | // Handle errors 68 | xmlHttp.addEventListener("error", () => reject("errors.response_not_ok")); 69 | xmlHttp.addEventListener("abort", () => reject("errors.upload_aborted")); 70 | 71 | // Send the request 72 | xmlHttp.send(formData); 73 | }); 74 | } 75 | ``` 76 | -------------------------------------------------------------------------------- /content/uploaders/fetch.md: -------------------------------------------------------------------------------- 1 | # Fetch Uploader 2 | 3 | The Fetch uploader uses a `Fetch` request to send the selected file to a server. This is can be used instead of the [XHR Uploader](/uploaders/xhr) if your use case does not need backwards compatibility with older browsers (like IE 11). 4 | 5 | This uploader also does not support progress (%) and will only display an "Uploading..." message instead of live progress, since `Fetch` does not have an `onprogress` event like `XMLHttpRequest` does. [XHR Uploader](/uploaders/xhr) is therefore recommended over this. 6 | 7 | ```ts 8 | import { Uppload, fetchUploader } from "uppload"; 9 | 10 | const profilePicture = new Uppload({ 11 | uploader: fetchUploader({ 12 | endpoint: "https://your-backend.com/upload" 13 | }); 14 | }); 15 | ``` 16 | 17 | By default, this will send a POST request with form data with the `file` key and expect a JSON response with the URL, like this: 18 | 19 | ```json 20 | { 21 | "url": "https://your-backend.com/file.jpg" 22 | } 23 | ``` 24 | 25 | You can customize the key used in the form data in the configuration: 26 | 27 | ```ts 28 | const profilePicture = new Uppload({ 29 | uploader: fetchUploader({ 30 | endpoint: "https://your-backend.com/upload", 31 | fileKeyName: "image" 32 | }); 33 | }); 34 | ``` 35 | 36 | If you don't want to send a POST request, you can specify the HTTP verb as well: 37 | 38 | ```ts 39 | const profilePicture = new Uppload({ 40 | uploader: fetchUploader({ 41 | endpoint: "https://your-backend.com/upload", 42 | fileKeyName: "image", 43 | method: "PUT" 44 | }); 45 | }); 46 | ``` 47 | 48 | To add custom headers, methods, etc., you can provide additional configuration for the Fetch request. In this case, you will have to also add the body of the request, like so: 49 | 50 | ```ts 51 | const profilePicture = new Uppload({ 52 | uploader: fetchUploader({ 53 | endpoint: "https://your-backend.com/upload", 54 | settingsFunction: file => { 55 | // You get the file blob 56 | // For example, add a custom header for auth and send a PUT request 57 | const formData = new FormData(); 58 | formData.append("file", file); 59 | return { 60 | method: "PUT", 61 | body: formData, 62 | headers: { 63 | "Authorization": "Bearer YOUR_TOKEN" 64 | } 65 | }; 66 | } 67 | }); 68 | }); 69 | ``` 70 | 71 | If your server responds with a different JSON schema which does not use the `url` key, you can use the `responseKey` property: 72 | 73 | ```ts 74 | const profilePicture = new Uppload({ 75 | uploader: fetchUploader({ 76 | endpoint: "https://your-backend.com/upload", 77 | responseKey: "imageUrl" 78 | }); 79 | }); 80 | ``` 81 | 82 | Or, if it's more complicated than a simple key, you can parse your own response and return the URL: 83 | 84 | ```ts 85 | const profilePicture = new Uppload({ 86 | uploader: fetchUploader({ 87 | endpoint: "https://your-backend.com/upload", 88 | responseFunction: json => { 89 | // Has the response.json() result as the argument 90 | return json.results[0].url; // Use your JSON response 91 | } 92 | }); 93 | }); 94 | ``` 95 | -------------------------------------------------------------------------------- /content/uploaders/firebase.md: -------------------------------------------------------------------------------- 1 | # Firebase Uploader 2 | 3 | This is an example of a custom uploader that sends your file to Firebase Storage. Firebase is a great way to have zero-config file uploads on your website. 4 | 5 | ```ts 6 | import * as firebase from "firebase/app"; 7 | import "firebase/storage"; 8 | 9 | const config = { 10 | apiKey: "your-firebase-api-key", 11 | projectId: "your-project-id", 12 | storageBucket: "your-project-id.appspot.com", 13 | childPath: "path/to/dir", 14 | }; 15 | 16 | const firebaseUploader = (file, updateProgress) => 17 | new Promise((resolve, reject) => { 18 | // Initialize a new Firebase app based on your config 19 | firebase.initializeApp(config); 20 | 21 | const storageReference = firebase.storage().ref(config.path); 22 | 23 | // Generate a file name based on current date and random number 24 | const reference = storageReference.child( 25 | `${config.childPath || ""}/${Math.random() 26 | .toString() 27 | .replace("0.", "") 28 | .substr(0, 7)}-${new Date().getTime()}.png` 29 | ); 30 | 31 | // Upload the file to the storage reference 32 | const uploadTask = reference.put(file); 33 | 34 | // Report upload progress 35 | uploadTask.on( 36 | "state_changed", 37 | snapshot => { 38 | const progress = 39 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 40 | if (updateProgress) updateProgress(progress); 41 | }, 42 | error => { 43 | console.log("Got error", error); 44 | return reject(new Error("unable_to_upload")); 45 | }, 46 | () => { 47 | uploadTask.snapshot.ref 48 | .getDownloadURL() 49 | .then(url => resolve(url)) // Return uploaded file's URL 50 | .catch(() => reject(new Error("unable_to_upload"))); 51 | } 52 | ); 53 | }); 54 | ``` 55 | 56 | And then you can use `firebaseUploader` like any other custom uploader. 57 | 58 | ```ts 59 | import { Uppload } from "uppload"; 60 | 61 | const profilePicture = new Uppload({ 62 | uploader: firebaseUploader, 63 | }); 64 | ``` 65 | 66 | You can also [view a live demo](https://codesandbox.io/s/uppload-firebase-3zd9b) of the Firebase uploader on CodeSandbox. 67 | -------------------------------------------------------------------------------- /content/uploaders/index.md: -------------------------------------------------------------------------------- 1 | # Uploaders 2 | 3 | Uploaders are ways to upload a file to a server. You can either import an uploader from Uppload or build your own uploader, depending on your [server configuration](/backends). 4 | -------------------------------------------------------------------------------- /content/uploaders/xhr.md: -------------------------------------------------------------------------------- 1 | # XHR Uploader 2 | 3 | The XHR uploader uses an `XMLHttpRequest` to send the selected file to a server. This is the preferred way to upload files in Uppload since it supports returning progress (%) too, unlike the [Fetch uploader](/uploaders/fetch): 4 | 5 | ```ts 6 | import { Uppload, xhrUploader } from "uppload"; 7 | 8 | const profilePicture = new Uppload({ 9 | uploader: xhrUploader({ 10 | endpoint: "https://your-backend.com/upload" 11 | }); 12 | }); 13 | ``` 14 | 15 | By default, this will send a POST request with form data with the `file` key and expect a JSON response with the URL, like this: 16 | 17 | ```json 18 | { 19 | "url": "https://your-backend.com/file.jpg" 20 | } 21 | ``` 22 | 23 | You can customize the key used in the form data in the configuration: 24 | 25 | ```ts 26 | const profilePicture = new Uppload({ 27 | uploader: xhrUploader({ 28 | endpoint: "https://your-backend.com/upload", 29 | fileKeyName: "image" 30 | }); 31 | }); 32 | ``` 33 | 34 | If you don't want to send a POST request, you can specify the HTTP verb as well: 35 | 36 | ```ts 37 | const profilePicture = new Uppload({ 38 | uploader: xhrUploader({ 39 | endpoint: "https://your-backend.com/upload", 40 | fileKeyName: "image", 41 | method: "PUT" 42 | }); 43 | }); 44 | ``` 45 | 46 | To add custom headers, methods, etc., to the XHR object, you can do this: 47 | 48 | ```ts 49 | const profilePicture = new Uppload({ 50 | uploader: xhrUploader({ 51 | endpoint: "https://your-backend.com/upload", 52 | settingsFunction: xmlHttp => { 53 | // You get the XMLHttpRequest() object as the argument 54 | // For example, add a custom header for auth 55 | xmlHttp.setRequestHeader("Authorization", "Bearer MY_TOKEN") 56 | } 57 | }); 58 | }); 59 | ``` 60 | 61 | If your server responds with a different JSON schema which does not use the `url` key, you can use the `responseKey` property: 62 | 63 | ```ts 64 | const profilePicture = new Uppload({ 65 | uploader: xhrUploader({ 66 | endpoint: "https://your-backend.com/upload", 67 | responseKey: "imageUrl" 68 | }); 69 | }); 70 | ``` 71 | 72 | Or, if it's more complicated than a simple key, you can parse your own response and return the URL: 73 | 74 | ```ts 75 | const profilePicture = new Uppload({ 76 | uploader: xhrUploader({ 77 | endpoint: "https://your-backend.com/upload", 78 | responseFunction: responseText => { 79 | // Return the URL from the response text yourself 80 | const json = JSON.parse(responseText); 81 | return json.results[0].url; // Use your JSON response 82 | } 83 | }); 84 | }); 85 | ``` 86 | -------------------------------------------------------------------------------- /content/wrappers/index.md: -------------------------------------------------------------------------------- 1 | # Frontend frameworks 2 | 3 | Uppload doesn't depend on a frontend framework like React or Vue.js, but is very easy to use with them. The following examples use the `upload` package with popular frontend frameworks: 4 | 5 | - [React](https://codesandbox.io/s/ecstatic-lake-9kqgo) 6 | - [Vue.js](https://codesandbox.io/s/uppload-vue-6i393) 7 | 8 | It's very easy to create your own wrapper around Uppload based on your usage using the [API](/api) and [listening to events](/listening-to-events). 9 | -------------------------------------------------------------------------------- /demo/demo.scss: -------------------------------------------------------------------------------- 1 | @import "../src/styles/uppload"; 2 | @import "../src/themes/light"; 3 | 4 | body > * { 5 | display: block; 6 | margin: 20px; 7 | } 8 | -------------------------------------------------------------------------------- /demo/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Uppload, 3 | Instagram, 4 | Facebook, 5 | Camera, 6 | URL, 7 | Local, 8 | GIPHY, 9 | Unsplash, 10 | Pixabay, 11 | Pexels, 12 | Screenshot, 13 | Crop, 14 | Rotate, 15 | Flip, 16 | Blur, 17 | Flickr, 18 | NineGag, 19 | Pinterest, 20 | en, 21 | DeviantArt, 22 | ArtStation, 23 | Twitter, 24 | Flipboard, 25 | Fotki, 26 | LinkedIn, 27 | Reddit, 28 | Tumblr, 29 | WeHeartIt, 30 | Brightness, 31 | Contrast, 32 | Grayscale, 33 | HueRotate, 34 | Invert, 35 | Saturate, 36 | Sepia, 37 | } from "../src"; 38 | 39 | const button = document.createElement("div"); 40 | button.innerHTML = `
`; 41 | const header = document.querySelector("header#masthead .masthead"); 42 | if (header) header.appendChild(button); 43 | 44 | const uppload = new Uppload({ 45 | value: "https://via.placeholder.com/150x150", 46 | bind: [".uppload-image", "img.icon"], 47 | call: ".try-uppload", 48 | lang: en, 49 | maxSize: [256, 256], 50 | uploader: (file, updateProgress) => 51 | new Promise(resolve => { 52 | console.log("Uploading file...", file); 53 | setTimeout(() => resolve(window.URL.createObjectURL(file)), 2750); 54 | let progress = 0; 55 | const interval = setInterval(() => { 56 | if (progress > 99) clearInterval(interval); 57 | updateProgress && updateProgress(progress++); 58 | }, 25); 59 | }), 60 | }); 61 | 62 | // These are our public demo API keys 63 | // You should create your own (free!) account on these services and use your own API keys 64 | const GIPHY_API_KEY = "Oxp1XWdrjdIVi2NUSD93h4HTuVpmIOAy"; 65 | const PIXABAY_API_KEY = "14234762-6301dcca06f491e77f115de8e"; 66 | const UNSPLASH_API_KEY = 67 | "3135681ed1e271e3d3d167e184aecfb0ad74d2043f6f378bf19a23a6647954d8"; 68 | const PEXELS_API_KEY = 69 | "563492ad6f9170000100000172ccefc96f674d01869ba24acc62a573"; 70 | 71 | uppload.use([ 72 | new Local({ 73 | mimeTypes: [ 74 | "image/gif", 75 | "image/jpeg", 76 | "image/jpg", 77 | "image/png", 78 | "application/pdf", 79 | ], 80 | }), 81 | new Camera(), 82 | new Instagram(), 83 | new URL(), 84 | new Facebook(), 85 | new Screenshot(), 86 | new GIPHY(GIPHY_API_KEY), 87 | new Unsplash(UNSPLASH_API_KEY), 88 | new Pixabay(PIXABAY_API_KEY), 89 | new Pexels(PEXELS_API_KEY), 90 | new Pinterest(), 91 | new Flickr(), 92 | new Twitter(), 93 | new NineGag(), 94 | new DeviantArt(), 95 | new ArtStation(), 96 | new Flipboard(), 97 | new Fotki(), 98 | new LinkedIn(), 99 | new Reddit(), 100 | new Tumblr(), 101 | new WeHeartIt(), 102 | ]); 103 | 104 | uppload.use([ 105 | new Crop({ 106 | aspectRatio: 1, 107 | }), 108 | new Rotate(), 109 | new Blur(), 110 | new Brightness(), 111 | new Flip(), 112 | new Contrast(), 113 | new Grayscale(), 114 | new HueRotate(), 115 | new Invert(), 116 | new Saturate(), 117 | new Sepia(), 118 | ]); 119 | 120 | uppload.on("*", (...args: any) => { 121 | console.log("Uppload event", ...args); 122 | }); 123 | 124 | setTimeout(function () { 125 | // @ts-ignore-next-line 126 | document.querySelector(".try-uppload").click(); 127 | }, 250); 128 | 129 | (window as any).uppload = uppload; 130 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "private": true, 4 | "source": "index.html" 5 | } 6 | -------------------------------------------------------------------------------- /dts-bundle-generator.config.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const packageJson = require("./package.json"); 3 | 4 | const getPackageName = () => { 5 | return packageJson.name; 6 | }; 7 | 8 | const config = { 9 | entries: [ 10 | { 11 | filePath: "./src/index.ts", 12 | outFile: `./dist/${getPackageName()}.d.ts`, 13 | noCheck: false, 14 | }, 15 | ], 16 | }; 17 | 18 | module.exports = config; 19 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Uppload 5 | 6 | 7 | 12 | 13 | 14 |

Hello, world!

15 | Uppload image 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "jsdom", 4 | transform: { 5 | "^.+\\.(ts|tsx)$": "ts-jest", 6 | }, 7 | modulePathIgnorePatterns: ["./dist/", "./test/mocks.ts"], 8 | coveragePathIgnorePatterns: ["./test/mocks.ts"], 9 | automock: false, 10 | setupFiles: ["./tests/mocks.ts"], 11 | "roots": [ 12 | "/src/", 13 | "/tests/" 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | publish = "public" 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uppload", 3 | "version": "0.0.0-development", 4 | "description": "Uppload is a JavaScript widget for better file uploading on the web.", 5 | "keywords": [ 6 | "uppload", 7 | "typescript", 8 | "css", 9 | "scss", 10 | "el nino", 11 | "file upload", 12 | "image upload", 13 | "crop", 14 | "uploader", 15 | "upload", 16 | "javascript" 17 | ], 18 | "main": "dist/index.umd.js", 19 | "module": "dist/index.js", 20 | "types": "dist/index.d.ts", 21 | "files": [ 22 | "dist", 23 | "src" 24 | ], 25 | "repository": "https://github.com/elninotech/uppload", 26 | "homepage": "https://uppload.js.org", 27 | "bugs": { 28 | "url": "https://github.com/elninotech/uppload/issues" 29 | }, 30 | "author": "El Niño ", 31 | "license": "MIT", 32 | "scripts": { 33 | "dev": "vite --host", 34 | "build": "npm run build:package && npm run build:styles", 35 | "build:package": "tsc && vite build && dts-bundle-generator --config ./dts-bundle-generator.config.ts", 36 | "build:styles": "node scripts/build-scss.js", 37 | "build:site": "node scripts/build-examples.js && site", 38 | "test": "jest --runInBand", 39 | "test:coverage": "jest --runInBand --coverage", 40 | "lint:scripts": "eslint . --ext .ts", 41 | "lint:styles": "stylelint ./**/*.{css,scss}", 42 | "format:scripts": "prettier . --write", 43 | "format:styles": "stylelint ./**/*.{css,scss} --fix", 44 | "format": "npm run format:scripts && npm run format:styles", 45 | "prepare": "husky install && husky set .husky/pre-commit 'npx lint-staged' && git add .husky/pre-commit", 46 | "uninstall-husky": "npm uninstall husky --no-save && git config --unset core.hooksPath && npx rimraf .husky" 47 | }, 48 | "devDependencies": { 49 | "@semantic-release/git": "^10.0.1", 50 | "@semantic-release/npm": "^9.0.2", 51 | "@staart/site": "^3.0.0-beta.20", 52 | "@types/estree": "^1.0.0", 53 | "@types/jest": "^29.2.5", 54 | "@types/jsdom": "^21.1.0", 55 | "@types/node": "^18.11.18", 56 | "@typescript-eslint/eslint-plugin": "^5.48.2", 57 | "@typescript-eslint/parser": "^5.48.2", 58 | "auto-i18n": "^1.0.0", 59 | "coveralls": "^3.1.0", 60 | "cssnano": "^5.1.7", 61 | "dts-bundle-generator": "^7.1.0", 62 | "eslint": "^8.32.0", 63 | "eslint-config-prettier": "^8.6.0", 64 | "eslint-plugin-prettier": "^4.2.1", 65 | "extract-zip": "^2.0.1", 66 | "gitmoji-changelog": "^2.1.0", 67 | "husky": "^8.0.3", 68 | "jest": "^29.3.1", 69 | "jest-canvas-mock": "^2.4.0", 70 | "jest-environment-jsdom": "^29.4.3", 71 | "jest-fetch-mock": "^3.0.3", 72 | "lint-staged": "^13.1.0", 73 | "prettier": "^2.8.3", 74 | "sass": "^1.58.3", 75 | "semantic-release": "^20.1.1", 76 | "semantic-release-gitmoji": "^1.6.3", 77 | "stylelint": "^14.16.1", 78 | "stylelint-config-recommended": "^9.0.0", 79 | "stylelint-config-sass-guidelines": "^9.0.1", 80 | "ts-jest": "^29.0.5", 81 | "ts-node": "^10.9.1", 82 | "typescript": "^4.9.4", 83 | "vite": "^4.1.4", 84 | "xhr-mock": "^2.5.1" 85 | }, 86 | "dependencies": { 87 | "cropperjs": "^1.5.7", 88 | "download": "^8.0.0", 89 | "focus-trap": "^7.3.1", 90 | "mitt": "^3.0.0" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /scripts/build-demo.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | const { writeFileSync, readdirSync, readFileSync, mkdirSync } = require("fs"); 3 | 4 | const siteFiles = readdirSync(join(__dirname, "..", "dist")).filter( 5 | file => 6 | file.startsWith("demo.") && (file.endsWith(".css") || file.endsWith(".js")) 7 | ); 8 | 9 | siteFiles.forEach(file => { 10 | const content = readFileSync(join(__dirname, "..", "dist", file)).toString(); 11 | if (file.endsWith(".js")) { 12 | writeFileSync(join(__dirname, "..", "public", "uppload-demo.js"), content); 13 | } else { 14 | writeFileSync(join(__dirname, "..", "public", "uppload-demo.css"), content); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /scripts/build-examples.js: -------------------------------------------------------------------------------- 1 | const download = require("download"); 2 | const extract = require("extract-zip"); 3 | const { exec } = require("child_process"); 4 | const { join } = require("path"); 5 | const { 6 | readdirSync, 7 | copyFileSync, 8 | mkdirSync, 9 | writeFileSync, 10 | readFileSync, 11 | } = require("fs"); 12 | 13 | const examplesDir = 14 | "https://github.com/elninotech/uppload-examples/archive/master.zip"; 15 | 16 | (async () => { 17 | console.log("Downloading examples..."); 18 | await download(examplesDir, __dirname, { filename: "master.zip" }); 19 | 20 | console.log("Extracting examples..."); 21 | await extract(join(__dirname, "master.zip"), { dir: __dirname }); 22 | 23 | const examples = readdirSync( 24 | join(__dirname, "uppload-examples-master", "examples") 25 | ); 26 | 27 | mkdirSync(join(__dirname, "..", "content", "examples"), { 28 | recursive: true, 29 | }); 30 | 31 | examples.forEach(example => { 32 | copyFileSync( 33 | join( 34 | __dirname, 35 | "uppload-examples-master", 36 | "examples", 37 | example, 38 | "README.md" 39 | ), 40 | join(__dirname, "..", "content", "examples", `${example}.md`) 41 | ); 42 | 43 | writeFileSync( 44 | join(__dirname, "..", "content", "examples", `${example}.md`), 45 | readFileSync( 46 | join(__dirname, "..", "content", "examples", `${example}.md`) 47 | ) 48 | .toString() 49 | .replace( 50 | "##", 51 | `You can [**view the source code on GitHub**](https://github.com/elninotech/uppload-examples/tree/master/examples/${example}) or [try it on CodeSandbox](https://codesandbox.io/s/github/elninotech/uppload-examples/tree/master/examples/${example}).\n\n##` 52 | ) 53 | ); 54 | }); 55 | console.log("Added examples!"); 56 | })(); 57 | -------------------------------------------------------------------------------- /scripts/build-lang.js: -------------------------------------------------------------------------------- 1 | const { translateObject } = require("auto-i18n"); 2 | const { writeFileSync, readFileSync } = require("fs"); 3 | const { join } = require("path"); 4 | 5 | let lang; 6 | eval( 7 | "lang = " + 8 | readFileSync(join("src", "i18n", "nl.ts")) 9 | .toString() 10 | .replace("export default ", "") 11 | ); 12 | 13 | translateObject(lang, "de") 14 | .then(translation => { 15 | const content = `export default ${JSON.stringify(translation, null, 2)};\n`; 16 | writeFileSync(join("src", "i18n", "de.ts"), content); 17 | }) 18 | .catch(error => { 19 | console.log("Got an error", error); 20 | }); 21 | -------------------------------------------------------------------------------- /scripts/build-scss.js: -------------------------------------------------------------------------------- 1 | const sass = require("sass"); 2 | const { join } = require("path"); 3 | const { writeFileSync, readdirSync, mkdirSync } = require("fs"); 4 | 5 | const result = sass.renderSync({ 6 | file: join(__dirname, "..", "src", "styles", "uppload.scss"), 7 | }); 8 | writeFileSync(join(__dirname, "..", "dist", "uppload.css"), result.css); 9 | 10 | const themes = readdirSync(join(__dirname, "..", "src", "themes")).filter( 11 | theme => theme !== "theme.scss" 12 | ); 13 | mkdirSync(join(__dirname, "..", "dist", "themes"), { 14 | recursive: true, 15 | }); 16 | themes.forEach(theme => { 17 | const result = sass.renderSync({ 18 | file: join(__dirname, "..", "src", "themes", theme), 19 | }); 20 | writeFileSync( 21 | join(__dirname, "..", "dist", "themes", theme.replace(".scss", ".css")), 22 | result.css 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /src/effect.ts: -------------------------------------------------------------------------------- 1 | import { IHandlersParams, ITemplateParams } from "./helpers/interfaces"; 2 | 3 | export class UpploadEffect { 4 | type = "effect"; 5 | name: string = ""; 6 | invisible = false; 7 | noRecolor = false; 8 | color = "#000"; 9 | icon: string = ""; 10 | template: (props: ITemplateParams) => string = () => ""; 11 | handlers: (params: IHandlersParams) => void = () => {}; 12 | supports: () => boolean = () => true; 13 | } 14 | -------------------------------------------------------------------------------- /src/effects/filter/blur.ts: -------------------------------------------------------------------------------- 1 | import UpploadFilterBaseClass from "../../helpers/filter"; 2 | 3 | export default class Blur extends UpploadFilterBaseClass { 4 | name = "blur"; 5 | icon = ``; 6 | cssFilter = "blur"; 7 | unit = "px"; 8 | } 9 | -------------------------------------------------------------------------------- /src/effects/filter/brightness.ts: -------------------------------------------------------------------------------- 1 | import UpploadFilterBaseClass from "../../helpers/filter"; 2 | 3 | export default class Brightness extends UpploadFilterBaseClass { 4 | name = "brightness"; 5 | icon = ``; 6 | cssFilter = "brightness"; 7 | unit = "%"; 8 | value = 100; 9 | max = 200; 10 | } 11 | -------------------------------------------------------------------------------- /src/effects/filter/contrast.ts: -------------------------------------------------------------------------------- 1 | import UpploadFilterBaseClass from "../../helpers/filter"; 2 | 3 | export default class Contrast extends UpploadFilterBaseClass { 4 | name = "contrast"; 5 | icon = ``; 6 | cssFilter = "contrast"; 7 | unit = "%"; 8 | value = 100; 9 | max = 200; 10 | } 11 | -------------------------------------------------------------------------------- /src/effects/filter/grayscale.ts: -------------------------------------------------------------------------------- 1 | import UpploadFilterBaseClass from "../../helpers/filter"; 2 | 3 | export default class Grayscale extends UpploadFilterBaseClass { 4 | name = "grayscale"; 5 | icon = ``; 6 | cssFilter = "grayscale"; 7 | unit = "%"; 8 | value = 0; 9 | max = 100; 10 | } 11 | -------------------------------------------------------------------------------- /src/effects/filter/hue-rotate.ts: -------------------------------------------------------------------------------- 1 | import UpploadFilterBaseClass from "../../helpers/filter"; 2 | 3 | export default class HueRotate extends UpploadFilterBaseClass { 4 | name = "hue-rotate"; 5 | icon = ``; 6 | cssFilter = "hue-rotate"; 7 | unit = "deg"; 8 | value = 0; 9 | max = 360; 10 | } 11 | -------------------------------------------------------------------------------- /src/effects/filter/invert.ts: -------------------------------------------------------------------------------- 1 | import UpploadFilterBaseClass from "../../helpers/filter"; 2 | 3 | export default class Invert extends UpploadFilterBaseClass { 4 | name = "invert"; 5 | icon = ``; 6 | cssFilter = "invert"; 7 | unit = "%"; 8 | value = 0; 9 | max = 100; 10 | } 11 | -------------------------------------------------------------------------------- /src/effects/filter/saturate.ts: -------------------------------------------------------------------------------- 1 | import UpploadFilterBaseClass from "../../helpers/filter"; 2 | 3 | export default class Saturate extends UpploadFilterBaseClass { 4 | name = "saturate"; 5 | icon = ``; 6 | cssFilter = "saturate"; 7 | unit = "%"; 8 | value = 100; 9 | max = 200; 10 | } 11 | -------------------------------------------------------------------------------- /src/effects/filter/sepia.ts: -------------------------------------------------------------------------------- 1 | import UpploadFilterBaseClass from "../../helpers/filter"; 2 | 3 | export default class Sepia extends UpploadFilterBaseClass { 4 | name = "sepia"; 5 | icon = ``; 6 | cssFilter = "sepia"; 7 | unit = "%"; 8 | value = 0; 9 | max = 100; 10 | } 11 | -------------------------------------------------------------------------------- /src/effects/preview/index.ts: -------------------------------------------------------------------------------- 1 | import { UpploadEffect } from "../../effect"; 2 | import { fitImageToContainer } from "../../helpers/elements"; 3 | import { IHandlersParams, ITemplateParams } from "../../helpers/interfaces"; 4 | 5 | export default class Preview extends UpploadEffect { 6 | name = "preview"; 7 | icon = ``; 8 | 9 | template = ({ file }: ITemplateParams) => { 10 | const image = URL.createObjectURL(file.blob); 11 | return ` 12 |
13 | 14 |
15 | `; 16 | }; 17 | 18 | handlers = (params: IHandlersParams) => { 19 | const image = params.uppload.container.querySelector( 20 | ".uppload-preview-element img" 21 | ) as HTMLImageElement | null; 22 | if (image) fitImageToContainer(params, image); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/effects/rotate/index.ts: -------------------------------------------------------------------------------- 1 | import { UpploadEffect } from "../../effect"; 2 | import Cropper from "cropperjs"; 3 | import { 4 | IHandlersParams, 5 | ITemplateParams, 6 | IUpploadFile, 7 | } from "../../helpers/interfaces"; 8 | import { 9 | safeListen, 10 | fitImageToContainer, 11 | canvasToBlob, 12 | } from "../../helpers/elements"; 13 | 14 | export default class Rotate extends UpploadEffect { 15 | name = "rotate"; 16 | icon = ``; 17 | value = 0; 18 | max = 360; 19 | unit = "deg"; 20 | originalFile: IUpploadFile = { blob: new Blob() }; 21 | 22 | template = ({ file, translate }: ITemplateParams) => { 23 | const image = URL.createObjectURL(file.blob); 24 | this.originalFile = file; 25 | return ` 26 |
27 | 28 |
29 |
30 | 31 | 0${ 32 | translate(`units.${this.unit}`) || this.unit 33 | } 34 |
35 | `; 36 | }; 37 | 38 | handlers = (params: IHandlersParams) => { 39 | const rotatorElement = params.uppload.container.querySelector( 40 | ".uppload-rotating-element img" 41 | ) as HTMLImageElement | null; 42 | const originalFile = this.originalFile; 43 | if (rotatorElement) { 44 | fitImageToContainer(params, rotatorElement).then(() => { 45 | const rotator = new Cropper(rotatorElement, { 46 | autoCropArea: 1, 47 | viewMode: 1, 48 | dragMode: "none", 49 | cropBoxMovable: false, 50 | cropBoxResizable: false, 51 | toggleDragModeOnDblclick: false, 52 | ready() { 53 | params.uppload.emitter.emit("processing"); 54 | canvasToBlob(rotator.getCroppedCanvas()).then(blob => { 55 | originalFile.blob = blob; 56 | params.uppload.emitter.emit("process"); 57 | params.next(originalFile); 58 | }); 59 | }, 60 | }); 61 | const range = params.uppload.container.querySelector( 62 | ".settings input[type='range']" 63 | ) as HTMLInputElement; 64 | if (range) 65 | safeListen(range, "change", () => { 66 | let value = 0; 67 | const range = params.uppload.container.querySelector( 68 | ".settings input[type='range']" 69 | ) as HTMLInputElement; 70 | if (range) value = parseInt(range.value); 71 | const displayer = params.uppload.container.querySelector( 72 | ".settings .value span" 73 | ); 74 | if (displayer) displayer.innerHTML = value.toString(); 75 | rotator.rotate(value - this.value); 76 | this.value = value; 77 | params.uppload.emitter.emit("processing"); 78 | canvasToBlob(rotator.getCroppedCanvas()).then(blob => { 79 | originalFile.blob = blob; 80 | params.uppload.emitter.emit("process"); 81 | params.next(originalFile); 82 | }); 83 | }); 84 | }); 85 | } 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/helpers/assets.ts: -------------------------------------------------------------------------------- 1 | import { UpploadService } from "../service"; 2 | import { UpploadEffect } from "../effect"; 3 | 4 | /** 5 | * Colors an SVG icon with the brand color for a service or effect 6 | * @param svg - SVG template string 7 | * @param service - Uppload service object 8 | */ 9 | export const colorSVG = ( 10 | svg: string, 11 | service: UpploadService | UpploadEffect 12 | ) => (service.noRecolor ? svg : svg.replace(/#000/g, service.color || "#000")); 13 | -------------------------------------------------------------------------------- /src/helpers/files.ts: -------------------------------------------------------------------------------- 1 | import { IUpploadFile } from "./interfaces"; 2 | 3 | /** 4 | * Convert a blob to a native File 5 | * @param blob - Blob to convert to file 6 | * @param fileName - Name of the file 7 | * @param lastModified - Date modified 8 | */ 9 | const safeBlobToFile = (blob: Blob, fileName?: string, lastModified?: Date) => { 10 | try { 11 | return new File([blob], fileName || "file_name", { 12 | lastModified: (lastModified || new Date()).getTime(), 13 | type: blob.type, 14 | }); 15 | } catch (error) { 16 | return blob; 17 | } 18 | }; 19 | 20 | export const blobToUpploadFile = ( 21 | blob: Blob, 22 | name?: string, 23 | type?: string, 24 | lastModified?: Date 25 | ) => { 26 | const result: IUpploadFile = { 27 | name, 28 | blob, 29 | lastModified, 30 | type, 31 | }; 32 | return result; 33 | }; 34 | 35 | export const safeUpploadFileToFile = (file: IUpploadFile) => { 36 | const blob = file.blob; 37 | file.lastModified = file.lastModified || new Date(); 38 | return safeBlobToFile(blob, file.name, file.lastModified); 39 | }; 40 | -------------------------------------------------------------------------------- /src/helpers/filter.ts: -------------------------------------------------------------------------------- 1 | import { UpploadEffect } from "../effect"; 2 | import { 3 | safeListen, 4 | fitImageToContainer, 5 | canvasToBlob, 6 | } from "../helpers/elements"; 7 | import { IHandlersParams, ITemplateParams, IUpploadFile } from "./interfaces"; 8 | 9 | export default class UpploadFilterBaseClass extends UpploadEffect { 10 | canvas: HTMLCanvasElement = document.createElement("canvas"); 11 | originalfileURL = ""; 12 | originalFile: IUpploadFile = { blob: new Blob() }; 13 | cssFilter = ""; 14 | max = 10; 15 | unit = "px"; 16 | value = 0; 17 | supports = () => 18 | !!( 19 | this.canvas.getContext && 20 | this.canvas.getContext("2d") && 21 | typeof this.canvas.getContext("2d")?.filter === "string" 22 | ); 23 | 24 | template = ({ file, translate }: ITemplateParams) => { 25 | const image = URL.createObjectURL(file.blob); 26 | this.originalfileURL = image; 27 | this.originalFile = file; 28 | return ` 29 |
30 | 31 |
32 |
33 | 34 | 0${ 35 | translate(`units.${this.unit}`) || this.unit 36 | } 37 |
38 | `; 39 | }; 40 | 41 | imageToCanvasBlob( 42 | params: IHandlersParams, 43 | filters: string 44 | ): Promise { 45 | params.uppload.emitter.emit("processing"); 46 | return new Promise(resolve => { 47 | this.canvas = document.createElement("canvas"); 48 | const image = document.createElement("img"); 49 | image.src = this.originalfileURL; 50 | image.onload = () => { 51 | this.canvas.width = image.width; 52 | this.canvas.height = image.height; 53 | const context = this.canvas.getContext("2d"); 54 | if (!context) return; 55 | context.clearRect(0, 0, this.canvas.width, this.canvas.height); 56 | context.filter = filters; 57 | context.drawImage(image, 0, 0); 58 | canvasToBlob(this.canvas).then(blob => { 59 | params.uppload.emitter.emit("process"); 60 | return resolve(blob); 61 | }); 62 | }; 63 | }); 64 | } 65 | 66 | handlers = (params: IHandlersParams) => { 67 | const hueElement = params.uppload.container.querySelector( 68 | ".uppload-hue-image img" 69 | ) as HTMLImageElement | null; 70 | if (hueElement) { 71 | fitImageToContainer(params, hueElement).then(() => { 72 | const range = params.uppload.container.querySelector( 73 | ".settings input[type='range']" 74 | ) as HTMLInputElement; 75 | if (range) safeListen(range, "change", this.update.bind(this, params)); 76 | }); 77 | } 78 | }; 79 | 80 | update(params: IHandlersParams) { 81 | let value = 0; 82 | const range = params.uppload.container.querySelector( 83 | ".settings input[type='range']" 84 | ) as HTMLInputElement; 85 | if (range) value = parseInt(range.value); 86 | const displayer = params.uppload.container.querySelector( 87 | ".settings .value span" 88 | ); 89 | if (displayer) displayer.innerHTML = value.toString(); 90 | const hueElement = params.uppload.container.querySelector( 91 | ".uppload-hue-image img" 92 | ) as HTMLImageElement | null; 93 | if (!hueElement) return; 94 | this.imageToCanvasBlob( 95 | params, 96 | `${this.cssFilter}(${range.value}${this.unit})` 97 | ).then(blob => { 98 | if (!blob) return; 99 | this.originalFile.blob = blob; 100 | params.next(this.originalFile); 101 | const image = URL.createObjectURL(blob); 102 | hueElement.setAttribute("src", image); 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/helpers/http.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Make an HTTP request with the Fetch API and cache results 3 | * @param input API endpoint 4 | * @param settings HTTP Fetch configuration 5 | */ 6 | export function cachedFetch( 7 | input: RequestInfo, 8 | settings?: RequestInit 9 | ): Promise { 10 | const storage = sessionStorage; 11 | return new Promise((resolve, reject) => { 12 | const key = `uppload_cache_${JSON.stringify(input)}`; 13 | const maxTTL = new Date(); 14 | maxTTL.setDate(maxTTL.getDate() + 1); 15 | const cachedResult = storage.getItem(key); 16 | if (cachedResult) { 17 | const cachedResultData = JSON.parse(cachedResult); 18 | if ( 19 | cachedResultData.ttl && 20 | new Date(cachedResultData.ttl).getTime() > new Date().getTime() 21 | ) 22 | return resolve(cachedResultData.result); 23 | } 24 | window 25 | .fetch(input, settings) 26 | .then(response => { 27 | if (!response.ok) throw new Error("errors.response_not_ok"); 28 | return response.json(); 29 | }) 30 | .then(result => { 31 | storage.setItem( 32 | key, 33 | JSON.stringify({ 34 | ttl: maxTTL, 35 | updatedAt: new Date(), 36 | result, 37 | }) 38 | ); 39 | resolve(result); 40 | }) 41 | .catch(error => reject(error)); 42 | }); 43 | } 44 | 45 | /** 46 | * Get a file Blob from an image URL 47 | * @param url - URL of an image 48 | */ 49 | export const imageUrlToBlob = (url: string): Promise => { 50 | return new Promise((resolve, reject) => { 51 | window 52 | .fetch(`https://wsrv.nl/?url=${encodeURIComponent(url)}`) 53 | .then(response => { 54 | if (!response.ok) throw new Error("errors.response_not_ok"); 55 | return response.blob(); 56 | }) 57 | .then(blob => resolve(blob)) 58 | .catch(error => reject(error)); 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /src/helpers/i18n.ts: -------------------------------------------------------------------------------- 1 | let i18n: any = {}; 2 | 3 | export const flattenObject = (ob: any) => { 4 | const toReturn: any = {}; 5 | for (const i in ob) { 6 | if (!ob.hasOwnProperty(i)) continue; 7 | if (typeof ob[i] == "object") { 8 | const flatObject = flattenObject(ob[i]); 9 | for (const x in flatObject) { 10 | if (!flatObject.hasOwnProperty(x)) continue; 11 | toReturn[i + "." + x] = flatObject[x]; 12 | } 13 | } else { 14 | toReturn[i] = ob[i]; 15 | } 16 | } 17 | return toReturn; 18 | }; 19 | 20 | /** 21 | * 22 | * @param translations 23 | */ 24 | export const setI18N = (translations: any) => { 25 | i18n = flattenObject(translations); 26 | }; 27 | 28 | /** 29 | * Get a translation from i18n setting 30 | * @param key - Translation key 31 | * @param params - Single term or array of variables 32 | */ 33 | export const translate = (key: string, params?: string | string[]) => { 34 | try { 35 | let term = i18n[key] as string; 36 | if (typeof params === "string") params = [params]; 37 | if (params) 38 | params.forEach((param, index) => { 39 | term = term.replace(`$${index + 1}$`, param); 40 | }); 41 | if (i18n.helper && typeof i18n.helper === "function") 42 | term = i18n.helper(term); 43 | return term; 44 | } catch (error) { 45 | return ""; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/helpers/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Uppload, UpploadEffect, UpploadService } from "../"; 2 | 3 | export type IElements = string | string[] | Element | Element[]; 4 | 5 | export type ITranslator = (key: string, params?: string | string[]) => string; 6 | export interface IUppload { 7 | services: UpploadService[]; 8 | effects: UpploadEffect[]; 9 | isOpen: boolean; 10 | error?: string; 11 | activeService: string; 12 | activeEffect: string; 13 | settings: IUpploadSettings; 14 | container: HTMLDivElement; 15 | } 16 | 17 | export interface IUpploadSettings { 18 | value?: string; 19 | bind?: IElements; 20 | call?: IElements; 21 | defaultService?: string; 22 | lang?: ILanguage; 23 | uploader?: IUploader; 24 | inline?: boolean; 25 | customClass?: string; 26 | multiple?: boolean; 27 | compression?: number; 28 | compressionToMime?: string; 29 | compressionFromMimes?: string[]; 30 | maxWidth?: number; 31 | maxHeight?: number; 32 | maxSize?: [number, number]; 33 | compressor?: (file: Blob) => Promise; 34 | transitionDuration?: number; 35 | disableModalClickClose?: boolean; 36 | } 37 | 38 | export interface IHandlersParams { 39 | upload: (file: Blob) => Promise; 40 | uploadMultiple: (file: Blob[]) => Promise; 41 | next: (file: IUpploadFile) => void; 42 | showHelp: (url: string) => void; 43 | handle: (error: any) => void; 44 | uppload: Uppload; 45 | translate: ITranslator; 46 | } 47 | 48 | export interface ITemplateParams { 49 | file: IUpploadFile; 50 | translate: ITranslator; 51 | } 52 | export interface IServiceTemplateParams { 53 | uppload: Uppload; 54 | translate: ITranslator; 55 | } 56 | 57 | export type IUploader = ( 58 | file: Blob, 59 | updateProgress?: (progress: number) => void 60 | ) => Promise; 61 | 62 | export type IMultipleUploader = ( 63 | file: Blob[], 64 | updateProgress?: (progress: number) => void 65 | ) => Promise; 66 | 67 | export type ILanguageHelper = (text: string) => string; 68 | 69 | export interface ILanguage { 70 | [index: string]: string | ILanguage | ILanguageHelper | any; 71 | } 72 | 73 | export interface IUpploadFile { 74 | blob: Blob; 75 | name?: string; 76 | type?: string; 77 | size?: number; 78 | lastModified?: Date; 79 | } 80 | 81 | export type IUpploadPlugins = 82 | | UpploadService[] 83 | | UpploadEffect[] 84 | | (UpploadService | UpploadEffect)[]; 85 | 86 | export type IPluginUpdateFunction = ( 87 | plugins: IUpploadPlugins 88 | ) => IUpploadPlugins; 89 | -------------------------------------------------------------------------------- /src/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Minified a string of HTML 3 | * @param html - HTML string to minify 4 | * @returns Minified string 5 | */ 6 | export const minifyHTML = (html: string) => 7 | html.replace(/\n/g, "").replace(/ /g, ""); 8 | 9 | /** 10 | * Returns a formatted version of the bytes 11 | * @param bytes - The bytes to format 12 | * @param decimals - The number of decimals to show 13 | * @returns The formatted string 14 | */ 15 | export const formatBytes = (bytes: number, decimals: number = 2) => { 16 | if (bytes === 0) { 17 | return "0 Bytes"; 18 | } 19 | const base: number = 1024; 20 | const dm: number = decimals < 0 ? 0 : decimals; 21 | const sizes: string[] = [ 22 | "Bytes", 23 | "KB", 24 | "MB", 25 | "GB", 26 | "TB", 27 | "PB", 28 | "EB", 29 | "ZB", 30 | "YB", 31 | ]; 32 | const index: number = Math.floor(Math.log(bytes) / Math.log(base)); 33 | const size: string = sizes[index]; 34 | return parseFloat((bytes / Math.pow(base, index)).toFixed(dm)) + " " + size; 35 | }; 36 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export { de } from "./de"; 2 | export { en } from "./en"; 3 | export { es } from "./es"; 4 | export { fa } from "./fa"; 5 | export { fr } from "./fr"; 6 | export { hi } from "./hi"; 7 | export { it } from "./it"; 8 | export { nl } from "./nl"; 9 | export { pt } from "./pt"; 10 | export { ro } from "./ro"; 11 | export { ru } from "./ru"; 12 | export { tr } from "./tr"; 13 | export { uk } from "./uk"; 14 | export { zhTW } from "./zh-TW"; 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Main class 2 | export { Uppload } from "./uppload"; 3 | 4 | // Base classes 5 | export { UpploadService } from "./service"; 6 | export { UpploadEffect } from "./effect"; 7 | 8 | // Helpers 9 | export * from "./helpers/elements"; 10 | export * from "./helpers/interfaces"; 11 | export * from "./helpers/http"; 12 | export * from "./helpers/i18n"; 13 | export * from "./helpers/microlink"; 14 | export * from "./helpers/search"; 15 | 16 | // Language packs 17 | export * from "./i18n"; 18 | 19 | // Uploaders 20 | export * from "./uploaders/xhr"; 21 | 22 | // Services 23 | import Camera from "./services/camera"; 24 | import Instagram from "./services/microlink/instagram"; 25 | import Facebook from "./services/microlink/facebook"; 26 | import Local from "./services/local"; 27 | import GIPHY from "./services/search/giphy"; 28 | import Pixabay from "./services/search/pixabay"; 29 | import Unsplash from "./services/search/unsplash"; 30 | import Pexels from "./services/search/pexels"; 31 | import URL from "./services/microlink/url"; 32 | import Screenshot from "./services/microlink/screenshot"; 33 | import Flickr from "./services/microlink/flickr"; 34 | import Pinterest from "./services/microlink/pinterest"; 35 | import DeviantArt from "./services/microlink/deviantart"; 36 | import NineGag from "./services/microlink/9gag"; 37 | import ArtStation from "./services/microlink/artstation"; 38 | import Twitter from "./services/microlink/twitter"; 39 | import Flipboard from "./services/microlink/flipboard"; 40 | import Fotki from "./services/microlink/fotki"; 41 | import LinkedIn from "./services/microlink/linkedin"; 42 | import Reddit from "./services/microlink/reddit"; 43 | import Tumblr from "./services/microlink/tumblr"; 44 | import WeHeartIt from "./services/microlink/weheartit"; 45 | export { 46 | Camera, 47 | Instagram, 48 | Facebook, 49 | Pixabay, 50 | Local, 51 | URL, 52 | Screenshot, 53 | GIPHY, 54 | Unsplash, 55 | Pexels, 56 | Pinterest, 57 | Flickr, 58 | NineGag, 59 | DeviantArt, 60 | ArtStation, 61 | Twitter, 62 | Flipboard, 63 | Fotki, 64 | LinkedIn, 65 | Reddit, 66 | Tumblr, 67 | WeHeartIt, 68 | }; 69 | 70 | // Effects 71 | import Crop from "./effects/crop"; 72 | import Rotate from "./effects/rotate"; 73 | import Flip from "./effects/flip"; 74 | import Preview from "./effects/preview"; 75 | import Brightness from "./effects/filter/brightness"; 76 | import Blur from "./effects/filter/blur"; 77 | import Contrast from "./effects/filter/contrast"; 78 | import Grayscale from "./effects/filter/grayscale"; 79 | import HueRotate from "./effects/filter/hue-rotate"; 80 | import Invert from "./effects/filter/invert"; 81 | import Sepia from "./effects/filter/sepia"; 82 | import Saturate from "./effects/filter/saturate"; 83 | export { 84 | Brightness, 85 | Crop, 86 | Rotate, 87 | Flip, 88 | Preview, 89 | Blur, 90 | Contrast, 91 | Grayscale, 92 | HueRotate, 93 | Invert, 94 | Sepia, 95 | Saturate, 96 | }; 97 | -------------------------------------------------------------------------------- /src/service.ts: -------------------------------------------------------------------------------- 1 | import { IHandlersParams, IServiceTemplateParams } from "./helpers/interfaces"; 2 | 3 | export class UpploadService { 4 | type = "service"; 5 | name: string = ""; 6 | invisible = false; 7 | noRecolor = false; 8 | icon: string = ""; 9 | color = "#333"; 10 | template: (params: IServiceTemplateParams) => string = () => ""; 11 | handlers: (params: IHandlersParams) => void = () => {}; 12 | stop: () => void = () => {}; 13 | supports: () => boolean = () => true; 14 | } 15 | -------------------------------------------------------------------------------- /src/services/microlink/9gag.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class NineGag extends MicrolinkBaseClass { 4 | name = "ninegag"; 5 | icon = ``; 6 | color = "#000"; 7 | exampleURL = "https://9gag.com/gag/awoBXb8"; 8 | validator = (input: string) => 9 | /(https?:\/\/(.+?\.)?9gag\.com(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 10 | input 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/services/microlink/artstation.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class ArtStation extends MicrolinkBaseClass { 4 | name = "artstation"; 5 | icon = ``; 6 | color = "#3ea2cf"; 7 | exampleURL = "https://www.artstation.com/artwork/VdGOkZ"; 8 | validator = (input: string) => 9 | /(https?:\/\/(.+?\.)?artstation\.com(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 10 | input 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/services/microlink/deviantart.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class DeviantArt extends MicrolinkBaseClass { 4 | name = "deviantart"; 5 | icon = ``; 6 | color = "#00d159"; 7 | exampleURL = 8 | "https://www.deviantart.com/artbycatherineradley/art/Despair-820869682"; 9 | validator = (input: string) => 10 | /(https?:\/\/(.+?\.)?(deviantart|fav)\.(com|me)(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 11 | input 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/services/microlink/facebook.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class Facebook extends MicrolinkBaseClass { 4 | name = "facebook"; 5 | icon = ``; 6 | color = "#1b69f6"; 7 | exampleURL = 8 | "https://www.facebook.com/elninotech/photos/a.2066268863489861/2066268886823192/?type=3&theater"; 9 | validator = (input: string) => 10 | /(https?:\/\/(.+?\.)?(facebook|fb)\.(com|me)(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 11 | input 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/services/microlink/flickr.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class Flickr extends MicrolinkBaseClass { 4 | name = "flickr"; 5 | icon = ``; 6 | noRecolor = true; 7 | color = "#ff0084"; 8 | exampleURL = "https://www.flickr.com/photos/renewolf/26111951000/"; 9 | validator = (input: string) => 10 | /(https?:\/\/(.+?\.)?(flickr|flic)\.(com|kr)(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 11 | input 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/services/microlink/flipboard.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class Flipboard extends MicrolinkBaseClass { 4 | name = "flipboard"; 5 | icon = ``; 6 | color = "#e12828"; 7 | exampleURL = 8 | "https://flipboard.com/@bbcfuture/how-climate-change-could-kill-the-red-apple/f-c8d499b4ca%2Fbbc.com"; 9 | validator = (input: string) => 10 | /(https?:\/\/(.+?\.)?(flipboard|flip)\.(com|it)(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 11 | input 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/services/microlink/fotki.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class Fotki extends MicrolinkBaseClass { 4 | name = "fotki"; 5 | icon = ``; 6 | color = "#5471B9"; 7 | exampleURL = 8 | "https://public.fotki.com/EricAnke/holland/molens/20170928-162510.html"; 9 | validator = (input: string) => 10 | /(https?:\/\/(.+?\.)?fotki\.com(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 11 | input 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/services/microlink/instagram.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class Instagram extends MicrolinkBaseClass { 4 | name = "instagram"; 5 | icon = ``; 6 | color = "#cc3366"; 7 | exampleURL = "https://www.instagram.com/p/Bu_T4RihQFB/"; 8 | validator = (input: string) => 9 | /(https?:\/\/(.+?\.)?(instagram|instagr)\.(com|am)(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 10 | input 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/services/microlink/linkedin.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class LinkedIn extends MicrolinkBaseClass { 4 | name = "linkedin"; 5 | icon = ``; 6 | color = "#0e76a8"; 7 | exampleURL = 8 | "https://www.linkedin.com/posts/explorius-vastgoedontwikkeling-b-v-_el-nino-huurt-kantoor-in-enschede-activity-6480386878641180672-7DC_"; 9 | validator = (input: string) => 10 | /(https?:\/\/(.+?\.)?linkedin\.com(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 11 | input 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/services/microlink/pinterest.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class Pinterest extends MicrolinkBaseClass { 4 | name = "pinterest"; 5 | icon = ``; 6 | color = "#e60023"; 7 | exampleURL = "https://pinterest.com/pin/437201076327078006/"; 8 | validator = (input: string) => 9 | /(https?:\/\/(.+?\.)?(pinterest|pin)\.(com|it)(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 10 | input 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/services/microlink/reddit.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class Reddit extends MicrolinkBaseClass { 4 | name = "reddit"; 5 | icon = ``; 6 | color = "#ff4301"; 7 | exampleURL = 8 | "https://www.reddit.com/r/thenetherlands/comments/dz1myk/a_beautiful_morning_in_ermelo/"; 9 | validator = (input: string) => 10 | /(https?:\/\/(.+?\.)?reddit\.com(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 11 | input 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/services/microlink/screenshot.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class Screenshot extends MicrolinkBaseClass { 4 | name = "screenshot"; 5 | icon = ``; 6 | color = "#e67e22"; 7 | } 8 | -------------------------------------------------------------------------------- /src/services/microlink/tumblr.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class Tumblr extends MicrolinkBaseClass { 4 | name = "tumblr"; 5 | icon = ``; 6 | color = "#34526f"; 7 | exampleURL = 8 | "https://germanpostwarmodern.tumblr.com/post/186653088149/cubicus-building-of-twente-university-1969-73-in"; 9 | validator = (input: string) => 10 | /(https?:\/\/(.+?\.)?tumblr\.com(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 11 | input 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/services/microlink/twitter.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class Twitter extends MicrolinkBaseClass { 4 | name = "twitter"; 5 | icon = ``; 6 | color = "#1da1f2"; 7 | exampleURL = "https://twitter.com/elninoict/status/1106176415622418433"; 8 | validator = (input: string) => 9 | /(https?:\/\/(.+?\.)?(twitter|t)\.(co|com)(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 10 | input 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/services/microlink/url.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class URL extends MicrolinkBaseClass { 4 | name = "url"; 5 | icon = ``; 6 | color = "#8e44ad"; 7 | } 8 | -------------------------------------------------------------------------------- /src/services/microlink/weheartit.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../helpers/microlink"; 2 | 3 | export default class WeHeartIt extends MicrolinkBaseClass { 4 | name = "weheartit"; 5 | icon = ``; 6 | color = "#ff5464"; 7 | exampleURL = "https://weheartit.com/entry/221671573"; 8 | validator = (input: string) => 9 | /(https?:\/\/(.+?\.)?weheartit\.com(\/[A-Za-z0-9\-\._~:\/\?#\[\]@!$&'\(\)\*\+,;\=]*)?)/.test( 10 | input 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/services/search/giphy.ts: -------------------------------------------------------------------------------- 1 | import { SearchBaseClass } from "../../helpers/search"; 2 | 3 | export interface GIPHYResult { 4 | id: string; 5 | title: string; 6 | url: string; 7 | images: { 8 | downsized_large: { url: string }; 9 | preview_gif: { url: string }; 10 | }; 11 | user?: { 12 | avatar_url: string; 13 | display_name: string; 14 | profile_url: string; 15 | }; 16 | } 17 | 18 | export default class GIPHY extends SearchBaseClass { 19 | constructor(apiKey: string) { 20 | super({ 21 | apiKey, 22 | name: "giphy", 23 | icon: ``, 24 | color: "#a800ff", 25 | noRecolor: true, 26 | poweredByUrl: "https://giphy.com", 27 | popularEndpoint: (apiKey: string) => 28 | `https://api.giphy.com/v1/gifs/trending?api_key=${apiKey}&limit=18&rating=G`, 29 | searchEndpoint: (apiKey: string, query: string) => 30 | `https://api.giphy.com/v1/gifs/search?api_key=${apiKey}&q=${encodeURIComponent( 31 | query 32 | )}&limit=18&offset=0&rating=G&lang=en`, 33 | getButton: (image: GIPHYResult) => `
34 |
`, 35 | getSearchResults: (response: { data: GIPHYResult[] }) => response.data, 36 | getPopularResults: (response: { data: GIPHYResult[] }) => response.data, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/services/search/pexels.ts: -------------------------------------------------------------------------------- 1 | import { SearchBaseClass } from "../../helpers/search"; 2 | 3 | export interface PexelsResult { 4 | url: string; 5 | photographer: string; 6 | src: { 7 | original: string; 8 | large2x: string; 9 | tiny: string; 10 | }; 11 | } 12 | 13 | export default class Pexels extends SearchBaseClass { 14 | constructor(apiKey: string) { 15 | super({ 16 | apiKey, 17 | name: "pexels", 18 | icon: ``, 19 | color: "#05a081", 20 | poweredByUrl: "https://pexels.com", 21 | popularEndpoint: (_apiKey: string) => 22 | `https://api.pexels.com/v1/curated?per_page=9&page=1`, 23 | searchEndpoint: (_apiKey: string, query: string) => 24 | `https://api.pexels.com/v1/search?query=${encodeURIComponent( 25 | query 26 | )}&per_page=12&page=1`, 27 | getButton: (image: PexelsResult) => `
28 | 33 | ${image.photographer} 34 |
`, 35 | getSearchResults: (response: { photos: PexelsResult[] }) => 36 | response.photos, 37 | getPopularResults: (response: { photos: PexelsResult[] }) => 38 | response.photos, 39 | fetchSettings: (apiKey: string) => ({ 40 | headers: { 41 | Authorization: apiKey, 42 | }, 43 | }), 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/services/search/pixabay.ts: -------------------------------------------------------------------------------- 1 | import { SearchBaseClass } from "../../helpers/search"; 2 | 3 | export interface PixabayResult { 4 | id: number; 5 | largeImageURL: string; 6 | previewURL: string; 7 | user: string; 8 | userImageURL: string; 9 | pageURL: string; 10 | tags: string; 11 | } 12 | 13 | export default class Pixabay extends SearchBaseClass { 14 | constructor(apiKey: string) { 15 | super({ 16 | apiKey, 17 | name: "pixabay", 18 | icon: ``, 19 | color: "#2ec66d", 20 | poweredByUrl: "https://pixabay.com", 21 | popularEndpoint: (apiKey: string) => 22 | `https://pixabay.com/api/?key=${apiKey}&per_page=18&image_type=photo`, 23 | searchEndpoint: (apiKey: string, query: string) => 24 | `https://pixabay.com/api/?key=${apiKey}&per_page=18&q=${encodeURIComponent( 25 | query 26 | )}&image_type=photo`, 27 | getButton: (image: PixabayResult) => `
28 | 29 | 30 | ${image.user} 31 |
`, 32 | getSearchResults: (response: { hits: PixabayResult[] }) => response.hits, 33 | getPopularResults: (response: { hits: PixabayResult[] }) => response.hits, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/services/search/unsplash.ts: -------------------------------------------------------------------------------- 1 | import { SearchBaseClass } from "../../helpers/search"; 2 | 3 | export interface UnsplashResult { 4 | id: string; 5 | alt_description: string; 6 | description: string; 7 | // color: string; 8 | urls: { 9 | regular: string; 10 | thumb: string; 11 | }; 12 | user: { 13 | name: string; 14 | profile_image: { 15 | small: string; 16 | }; 17 | }; 18 | } 19 | 20 | export default class Unsplash extends SearchBaseClass { 21 | constructor(apiKey: string) { 22 | super({ 23 | apiKey, 24 | name: "unsplash", 25 | icon: ``, 26 | color: "#333", 27 | poweredByUrl: "https://unsplash.com", 28 | popularEndpoint: (apiKey: string) => 29 | `https://api.unsplash.com/photos?client_id=${apiKey}`, 30 | searchEndpoint: (apiKey: string, query: string) => 31 | `https://api.unsplash.com/search/photos?client_id=${apiKey}&page=1&query=${encodeURIComponent( 32 | query 33 | )}`, 34 | getButton: (image: UnsplashResult) => `
35 | 40 | 41 | 42 | ${image.user.name} 43 | 44 |
`, 45 | getSearchResults: (response: { results: UnsplashResult[] }) => 46 | response.results, 47 | getPopularResults: (response: UnsplashResult[]) => response, 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/styles/effects.scss: -------------------------------------------------------------------------------- 1 | @import "./input-range.scss"; 2 | 3 | .uppload-container { 4 | .active-effect-container { 5 | flex: 1 0 0; 6 | text-align: center; 7 | } 8 | 9 | .effects-continue button.uppload-button { 10 | display: inline-block; 11 | margin: 0 1rem; 12 | } 13 | 14 | footer.effects-nav { 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | overflow: hidden; 19 | 20 | .effects-tabs { 21 | // This width is updated with JavaScript 22 | width: 100px; 23 | display: flex; 24 | overflow-x: auto; 25 | flex-wrap: nowrap; 26 | } 27 | 28 | .effects-tabs-flow { 29 | display: flex; 30 | flex-wrap: nowrap; 31 | white-space: nowrap; 32 | } 33 | 34 | input[type="radio"] { 35 | position: absolute; 36 | opacity: 0; 37 | } 38 | 39 | label { 40 | display: block; 41 | padding: 0.5rem 0; 42 | text-align: center; 43 | width: 4.5rem; 44 | font-size: 120%; 45 | transition: 0.2s; 46 | 47 | span { 48 | font-size: 55%; 49 | display: block; 50 | } 51 | svg { 52 | display: block; 53 | margin: 0.2rem auto; 54 | height: 1.25rem; 55 | } 56 | } 57 | } 58 | 59 | .uppload-effect { 60 | flex: 1 0 0; 61 | display: flex; 62 | flex-direction: column; 63 | justify-content: space-between; 64 | opacity: 0; 65 | transition: opacity 0.2s; 66 | .active-effect-container { 67 | flex: 1 0 0; 68 | display: flex; 69 | flex-direction: column; 70 | justify-content: space-between; 71 | > div:first-child { 72 | flex: 1 0 0; 73 | } 74 | 75 | .settings { 76 | text-align: center; 77 | padding: 1rem 0; 78 | 79 | button.flip-btn-horizontal, 80 | button.flip-btn-vertical { 81 | font: inherit; 82 | border: none; 83 | line-height: 1; 84 | padding: 0.5rem 1rem; 85 | margin: 0 0.25rem; 86 | border-radius: 5rem; 87 | } 88 | 89 | .value { 90 | display: inline-block; 91 | vertical-align: middle; 92 | margin-left: 0.5rem; 93 | } 94 | } 95 | 96 | input[type="range"] { 97 | margin: 0 auto; 98 | width: 75%; 99 | } 100 | } 101 | } 102 | 103 | .uppload-preview-element { 104 | text-align: center; 105 | } 106 | 107 | .uppload-hue-image { 108 | text-align: center; 109 | } 110 | 111 | .uppload-actions { 112 | text-align: center; 113 | margin-top: 0.5rem; 114 | label { 115 | position: relative; 116 | display: inline-block; 117 | padding: 0.5rem 1rem; 118 | margin: 0 -0.25rem; 119 | &:first-of-type { 120 | padding-left: 1.5rem; 121 | border-radius: 2rem 0 0 2rem; 122 | } 123 | &:last-of-type { 124 | padding-right: 1.5rem; 125 | border-radius: 0 2rem 2rem 0; 126 | } 127 | } 128 | input[type="radio"] { 129 | opacity: 0; 130 | position: absolute; 131 | &:checked + label { 132 | font-weight: bold; 133 | } 134 | &:focus + label { 135 | z-index: 1; 136 | } 137 | } 138 | } 139 | 140 | .uppload-effect--rotate .cropper-drag-box { 141 | background-color: transparent; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/styles/input-range.scss: -------------------------------------------------------------------------------- 1 | // Styling Cross-Browser Compatible Range Inputs with Sass 2 | // Github: https://github.com/darlanrod/input-range-sass 3 | // Author: Darlan Rod https://github.com/darlanrod 4 | // Version 1.5.2 5 | // MIT License 6 | 7 | $track-color: #aaa !default; 8 | $thumb-color: #fff !default; 9 | 10 | $thumb-radius: 1rem !default; 11 | $thumb-height: 1rem !default; 12 | $thumb-width: 1rem !default; 13 | 14 | $track-width: 100% !default; 15 | $track-height: 0.25rem !default; 16 | 17 | $track-radius: 1rem !default; 18 | $contrast: 5% !default; 19 | 20 | $ie-bottom-track-color: darken($track-color, $contrast) !default; 21 | 22 | @mixin track { 23 | cursor: default; 24 | height: $track-height; 25 | transition: all 0.2s ease; 26 | width: $track-width; 27 | } 28 | 29 | @mixin thumb { 30 | background: $thumb-color; 31 | box-shadow: 0 0.1rem 0.25rem rgba(0, 0, 0, 0.5); 32 | border-radius: $thumb-radius; 33 | box-sizing: border-box; 34 | cursor: default; 35 | height: $thumb-height; 36 | width: $thumb-width; 37 | } 38 | 39 | .uppload-modal .uppload-effect [type="range"] { 40 | -webkit-appearance: none; 41 | background: transparent; 42 | margin: $thumb-height * 0.5 0; 43 | width: $track-width; 44 | 45 | &::-moz-focus-outer { 46 | border: 0; 47 | } 48 | 49 | &:focus { 50 | outline: 0; 51 | 52 | &::-webkit-slider-runnable-track { 53 | background: lighten($track-color, $contrast); 54 | } 55 | 56 | &::-ms-fill-lower { 57 | background: $track-color; 58 | } 59 | 60 | &::-ms-fill-upper { 61 | background: lighten($track-color, $contrast); 62 | } 63 | } 64 | 65 | &::-webkit-slider-runnable-track { 66 | @include track; 67 | background: $track-color; 68 | border-radius: $track-radius; 69 | } 70 | 71 | &::-webkit-slider-thumb { 72 | @include thumb; 73 | -webkit-appearance: none; 74 | margin-top: (($track-height) * 0.5 - $thumb-height * 0.5); 75 | } 76 | 77 | &::-moz-range-track { 78 | @include track; 79 | background: $track-color; 80 | border-radius: $track-radius; 81 | height: $track-height * 0.5; 82 | } 83 | 84 | &::-moz-range-thumb { 85 | @include thumb; 86 | } 87 | 88 | &::-ms-track { 89 | @include track; 90 | background: transparent; 91 | border-color: transparent; 92 | border-width: ($thumb-height * 0.5) 0; 93 | color: transparent; 94 | } 95 | 96 | &::-ms-fill-lower { 97 | background: $ie-bottom-track-color; 98 | border-radius: ($track-radius * 2); 99 | } 100 | 101 | &::-ms-fill-upper { 102 | background: $track-color; 103 | border-radius: ($track-radius * 2); 104 | } 105 | 106 | &::-ms-thumb { 107 | @include thumb; 108 | margin-top: $track-height * 0.25; 109 | } 110 | 111 | &:disabled { 112 | &::-webkit-slider-thumb, 113 | &::-moz-range-thumb, 114 | &::-ms-thumb, 115 | &::-webkit-slider-runnable-track, 116 | &::-ms-fill-lower, 117 | &::-ms-fill-upper { 118 | cursor: not-allowed; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/styles/mobile.scss: -------------------------------------------------------------------------------- 1 | @media (max-height: 500px) { 2 | .uppload-modal { 3 | height: 90%; 4 | } 5 | } 6 | 7 | @media (max-width: 850px) { 8 | .uppload-modal { 9 | transform: none; 10 | left: 0; 11 | right: 0; 12 | width: 100%; 13 | border-radius: 0; 14 | bottom: 0; 15 | height: auto; 16 | top: 10%; 17 | flex-direction: column; 18 | .uppload-service--default .uppload-services .uppload-service-name { 19 | width: 47.5%; 20 | } 21 | aside { 22 | height: auto; 23 | width: 100%; 24 | .uppload-services { 25 | display: flex; 26 | } 27 | nav .uppload-service-name label { 28 | white-space: nowrap; 29 | } 30 | } 31 | footer.effects-nav { 32 | flex-direction: column; 33 | padding: 1rem 0; 34 | .effects-tabs { 35 | width: 100% !important; 36 | margin: 1rem 0 !important; 37 | } 38 | } 39 | .effects-continue { 40 | width: 90%; 41 | } 42 | .effects-continue button { 43 | margin: 0 !important; 44 | width: 100%; 45 | box-sizing: border-box; 46 | } 47 | section .uppload-active-container footer button { 48 | display: block !important; 49 | margin: 0.5rem 0 0 0 !important; 50 | width: 100%; 51 | box-sizing: border-box; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/styles/ui.scss: -------------------------------------------------------------------------------- 1 | .uppload-modal { 2 | p { 3 | margin: 0; 4 | margin-bottom: 1rem; 5 | &:last-child { 6 | margin-bottom: 0; 7 | } 8 | } 9 | 10 | .uppload-error { 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | right: 0; 15 | padding: 1rem; 16 | text-align: center; 17 | } 18 | 19 | form { 20 | text-align: center; 21 | margin: 2rem 0; 22 | input { 23 | width: 75%; 24 | border: 0.1rem solid; 25 | } 26 | } 27 | 28 | form input, 29 | form button, 30 | button.uppload-button, 31 | .effects-continue button { 32 | -webkit-appearance: none; 33 | appearance: none; 34 | font: inherit; 35 | padding: 0.75rem 1rem; 36 | border-radius: 0.2rem; 37 | font-size: 135%; 38 | display: block; 39 | margin: 1rem auto; 40 | transition: 0.2s; 41 | } 42 | form button, 43 | button.uppload-button, 44 | .effects-continue button { 45 | border: none; 46 | } 47 | .effects-continue button { 48 | margin: 0 1rem; 49 | } 50 | form button[type="submit"]::after, 51 | .uppload-button--cta::after, 52 | .effects-continue--upload::after { 53 | content: "→"; 54 | margin-left: 0.5rem; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/styles/uppload.scss: -------------------------------------------------------------------------------- 1 | @import "./modal.scss"; 2 | @import "./ui.scss"; 3 | @import "./services.scss"; 4 | @import "./effects.scss"; 5 | @import "./mobile.scss"; 6 | 7 | /** 8 | * Initially, the widget will be appended to body but invisible 9 | */ 10 | .uppload-container { 11 | display: none; 12 | &.visible { 13 | display: block; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/themes/dark.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Dark theme 3 | */ 4 | 5 | $colorBlack: #000; 6 | $colorWhite: #fff; 7 | $colorLightGray: #222; 8 | $colorDarkGray: #111; 9 | 10 | /** 11 | * All variables 12 | */ 13 | 14 | $modalBackground: rgba(125, 125, 125, 0.25); 15 | $modalElementBackground: $colorBlack; 16 | $modalElementForeground: $colorWhite; 17 | $modalElementBoxShadow: 0 5rem 10rem rgba(0, 0, 0, 0.3); 18 | $linkColor: inherit; 19 | 20 | $sidebarNavBackground: $colorDarkGray; 21 | $sidebarNavForeground: $colorWhite; 22 | $sidebarNavActiveBackground: $colorBlack; 23 | $sidebarNavActiveForeground: $colorWhite; 24 | $sidebarNavFocusBoxShadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 25 | 26 | $errorBackground: #c33; 27 | $errorForeground: $colorBlack; 28 | 29 | $inputBorderColor: rgba(0, 0, 0, 0.3); 30 | $buttonDefaultBackground: #333; 31 | $buttonDefaultForeground: $colorWhite; 32 | 33 | $effectsButtonBackground: $colorLightGray; 34 | $effectsButtonForeground: $colorWhite; 35 | $effectsCTAButtonBackground: #3498db; 36 | $effectsCTAButtonForeground: $colorWhite; 37 | $effectsNavBackground: $colorDarkGray; 38 | $effectsNavForeground: $colorWhite; 39 | $effectsNavActiveBackground: $colorBlack; 40 | $effectsNavActiveForeground: $colorWhite; 41 | $effectsNavItemIconColor: $colorWhite; 42 | $effectsLabelBackground: $colorDarkGray; 43 | $effectsLabelColor: inherit; 44 | 45 | $HomeNavItemBackground: $colorLightGray; 46 | $HomeNavItemForeground: $colorWhite; 47 | $HomeNavItemHoverBackground: $colorDarkGray; 48 | $HomeNavItemHoverForeground: $colorWhite; 49 | 50 | $loaderColor: #333; 51 | $dropAreaBorder: 3px dashed rgba(255, 255, 255, 0.1); 52 | $dropAreaBackground: transparent; 53 | $dropAreaColor: inherit; 54 | $dropAreaActiveBorder: 3px dashed rgba(0, 0, 0, 0.25); 55 | $dropAreaActiveBackground: $colorDarkGray; 56 | $dropAreaActiveColor: inherit; 57 | 58 | @import "./theme.scss"; 59 | -------------------------------------------------------------------------------- /src/themes/light.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Light theme (default) 3 | */ 4 | 5 | $colorWhite: #fff; 6 | $colorLightGray: whitesmoke; 7 | $colorDarkGray: #dfe6e9; 8 | $colorBlack: #1b0000; 9 | 10 | /** 11 | * All variables 12 | */ 13 | 14 | $modalBackground: rgba(125, 125, 125, 0.25); 15 | $modalElementBackground: $colorWhite; 16 | $modalElementForeground: $colorBlack; 17 | $modalElementBoxShadow: 0 5rem 10rem rgba(0, 0, 0, 0.3); 18 | $linkColor: inherit; 19 | 20 | $sidebarNavBackground: $colorDarkGray; 21 | $sidebarNavForeground: inherit; 22 | $sidebarNavActiveBackground: $colorWhite; 23 | $sidebarNavActiveForeground: inherit; 24 | $sidebarNavFocusBoxShadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 25 | 26 | $errorBackground: #c33; 27 | $errorForeground: $colorWhite; 28 | 29 | $inputBorderColor: rgba(0, 0, 0, 0.1); 30 | $buttonDefaultBackground: #333; 31 | $buttonDefaultForeground: $colorWhite; 32 | 33 | $effectsButtonBackground: $colorLightGray; 34 | $effectsButtonForeground: inherit; 35 | $effectsCTAButtonBackground: #3498db; 36 | $effectsCTAButtonForeground: $colorWhite; 37 | $effectsNavBackground: $colorDarkGray; 38 | $effectsNavForeground: inherit; 39 | $effectsNavActiveBackground: $colorWhite; 40 | $effectsNavActiveForeground: inherit; 41 | $effectsNavItemIconColor: inherit; 42 | $effectsLabelBackground: $colorDarkGray; 43 | $effectsLabelColor: inherit; 44 | 45 | $HomeNavItemBackground: $colorLightGray; 46 | $HomeNavItemForeground: inherit; 47 | $HomeNavItemHoverBackground: $colorDarkGray; 48 | $HomeNavItemHoverForeground: inherit; 49 | 50 | $loaderColor: #333; 51 | $dropAreaBorder: 3px dashed rgba(0, 0, 0, 0.1); 52 | $dropAreaBackground: transparent; 53 | $dropAreaColor: inherit; 54 | $dropAreaActiveBorder: 3px dashed rgba(0, 0, 0, 0.25); 55 | $dropAreaActiveBackground: $colorLightGray; 56 | $dropAreaActiveColor: inherit; 57 | 58 | @import "./theme.scss"; 59 | -------------------------------------------------------------------------------- /src/uploaders/xhr.ts: -------------------------------------------------------------------------------- 1 | import { IUploader } from "../helpers/interfaces"; 2 | 3 | export const xhrUploader = ({ 4 | endpoint, 5 | fileKeyName = "file", 6 | method = "POST", 7 | responseKey = "url", 8 | responseFunction, 9 | settingsFunction, 10 | }: { 11 | endpoint: string; 12 | fileKeyName?: string; 13 | method?: string; 14 | responseKey?: string; 15 | responseFunction?: (responseText: string) => string; 16 | settingsFunction?: (xmlHttp: XMLHttpRequest) => void | XMLHttpRequest; 17 | }): IUploader => { 18 | return (file, updateProgress) => 19 | new Promise((resolve, reject) => { 20 | const formData = new FormData(); 21 | formData.append(fileKeyName, file); 22 | const xmlHttp = new XMLHttpRequest(); 23 | xmlHttp.open(method, endpoint, true); 24 | if (typeof settingsFunction === "function") settingsFunction(xmlHttp); 25 | xmlHttp.addEventListener("progress", event => { 26 | if (typeof updateProgress === "function") 27 | updateProgress(event.loaded / event.total); 28 | }); 29 | xmlHttp.addEventListener("load", () => { 30 | const responseText = xmlHttp.responseText; 31 | if (typeof responseFunction === "function") 32 | return resolve(responseFunction(responseText)); 33 | const json = JSON.parse(responseText); 34 | return resolve(json[responseKey]); 35 | }); 36 | xmlHttp.addEventListener("error", () => reject("errors.response_not_ok")); 37 | xmlHttp.addEventListener("abort", () => reject("errors.upload_aborted")); 38 | xmlHttp.send(formData); 39 | }); 40 | }; 41 | 42 | export const fetchUploader = ({ 43 | endpoint, 44 | settingsFunction, 45 | method = "POST", 46 | fileKeyName = "file", 47 | responseKey = "url", 48 | responseFunction, 49 | }: { 50 | endpoint: RequestInfo; 51 | settingsFunction?: (file: Blob) => RequestInit; 52 | method?: string; 53 | fileKeyName?: string; 54 | responseKey?: string; 55 | responseFunction?: (responseText: string) => string; 56 | }): IUploader => { 57 | return file => 58 | new Promise((resolve, reject) => { 59 | const formData = new FormData(); 60 | formData.append(fileKeyName, file); 61 | window 62 | .fetch( 63 | endpoint, 64 | settingsFunction 65 | ? settingsFunction(file) 66 | : { 67 | method, 68 | body: formData, 69 | } 70 | ) 71 | .then(response => { 72 | if (!response.ok) throw new Error("errors.response_not_ok"); 73 | return response.json(); 74 | }) 75 | .then(json => { 76 | if (typeof responseFunction === "function") 77 | return resolve(responseFunction(json)); 78 | return resolve(json[responseKey]); 79 | }) 80 | .catch(() => reject("errors.response_not_ok")); 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tests/__mocks__/tabbable.ts: -------------------------------------------------------------------------------- 1 | const lib = jest.requireActual("tabbable"); 2 | 3 | const tabbable = { 4 | ...lib, 5 | tabbable: (node: any, options: any) => 6 | lib.tabbable(node, { ...options, displayCheck: "none" }), 7 | focusable: (node: any, options: any) => 8 | lib.focusable(node, { ...options, displayCheck: "none" }), 9 | isFocusable: (node: any, options: any) => 10 | lib.isFocusable(node, { ...options, displayCheck: "none" }), 11 | isTabbable: (node: any, options: any) => 12 | lib.isTabbable(node, { ...options, displayCheck: "none" }), 13 | }; 14 | 15 | module.exports = tabbable; 16 | -------------------------------------------------------------------------------- /tests/effect.test.ts: -------------------------------------------------------------------------------- 1 | import { UpploadEffect, Uppload } from "../src"; 2 | import { 3 | Crop, 4 | Preview, 5 | Flip, 6 | Blur, 7 | Brightness, 8 | Contrast, 9 | Grayscale, 10 | HueRotate, 11 | Invert, 12 | Saturate, 13 | Sepia, 14 | } from "../src"; 15 | import { effectTemplateParams } from "./mocks"; 16 | import "jest-canvas-mock"; 17 | 18 | const effect = new UpploadEffect(); 19 | const effects = [ 20 | new Crop(), 21 | new Preview(), 22 | new Flip(), 23 | new Blur(), 24 | new Brightness(), 25 | new Contrast(), 26 | new Grayscale(), 27 | new HueRotate(), 28 | new Invert(), 29 | new Saturate(), 30 | new Sepia(), 31 | ]; 32 | 33 | describe("effect template", () => { 34 | it("is a function", () => expect(typeof effect.template).toBe("function")); 35 | it("gives a string", () => 36 | expect(typeof effect.template(effectTemplateParams)).toBe("string")); 37 | }); 38 | 39 | const uppload = new Uppload(); 40 | uppload.use(effects); 41 | 42 | window.URL.createObjectURL = jest.fn(); 43 | window.URL.createObjectURL = jest.fn(() => "fake-url"); 44 | 45 | for (const currentEffect of effects) { 46 | describe(`${currentEffect.name} effect`, () => { 47 | it("template is a function", () => 48 | expect(typeof currentEffect.template).toBe("function")); 49 | it("template gives a string", () => 50 | expect(typeof currentEffect.template(effectTemplateParams)).toBe( 51 | "string" 52 | )); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /tests/helpers/assets.test.ts: -------------------------------------------------------------------------------- 1 | import { colorSVG } from "../../src/helpers/assets"; 2 | import { UpploadService } from "../../src/service"; 3 | import { UpploadEffect } from "../../src/effect"; 4 | 5 | const sampleService = new UpploadService(); 6 | sampleService.icon = ``; 7 | sampleService.color = "#9d2e2c"; 8 | 9 | test("colors an svg string from a service", () => { 10 | const SVG = sampleService.icon; 11 | expect(colorSVG(SVG, sampleService)).toContain("#9d2e2c"); 12 | }); 13 | 14 | test("remove original color from an svg string from a service", () => { 15 | const SVG = sampleService.icon; 16 | expect(colorSVG(SVG, sampleService)).not.toContain("#000"); 17 | }); 18 | 19 | const sampleEffect = new UpploadEffect(); 20 | sampleEffect.icon = ``; 21 | sampleEffect.color = "#1abc9c"; 22 | 23 | test("colors an svg string from a effect", () => { 24 | const SVG = sampleEffect.icon; 25 | expect(colorSVG(SVG, sampleEffect)).toContain("#1abc9c"); 26 | }); 27 | 28 | test("remove original color from an svg string from a effect", () => { 29 | const SVG = sampleEffect.icon; 30 | expect(colorSVG(SVG, sampleEffect)).not.toContain("#000"); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/helpers/elements.test.ts: -------------------------------------------------------------------------------- 1 | import { getElements, safeListen } from "../../src/helpers/elements"; 2 | 3 | test("get elements", () => 4 | expect(getElements(".no-elements-returned")).toEqual([])); 5 | 6 | test("safe listen", () => 7 | expect( 8 | safeListen(document.createElement("div"), "click", () => {}) 9 | ).toBeUndefined()); 10 | 11 | test("safe listen adds listener", () => { 12 | let completed = false; 13 | const button = document.createElement("button"); 14 | safeListen(button, "click", () => (completed = true)); 15 | button.click(); 16 | expect(completed).toBeTruthy(); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/helpers/microlink.test.ts: -------------------------------------------------------------------------------- 1 | import { MicrolinkBaseClass } from "../../src/helpers/microlink"; 2 | import { 3 | NineGag, 4 | ArtStation, 5 | DeviantArt, 6 | Facebook, 7 | Flickr, 8 | Instagram, 9 | Pinterest, 10 | Screenshot, 11 | Twitter, 12 | URL, 13 | Flipboard, 14 | Fotki, 15 | LinkedIn, 16 | Reddit, 17 | Tumblr, 18 | WeHeartIt, 19 | } from "../../src"; 20 | import { serviceTemplateParams } from "../mocks"; 21 | 22 | const services = [ 23 | new NineGag(), 24 | new ArtStation(), 25 | new DeviantArt(), 26 | new Facebook(), 27 | new Flickr(), 28 | new Instagram(), 29 | new Pinterest(), 30 | new Screenshot(), 31 | new Twitter(), 32 | new URL(), 33 | new Flipboard(), 34 | new Fotki(), 35 | new LinkedIn(), 36 | new Reddit(), 37 | new Tumblr(), 38 | new WeHeartIt(), 39 | ]; 40 | 41 | const microlink = new MicrolinkBaseClass(); 42 | 43 | describe("validator", () => { 44 | it("is a function", () => 45 | expect(typeof microlink.validator).toBe("function")); 46 | it("returns a bool", () => 47 | expect(typeof microlink.validator("")).toBe("boolean")); 48 | }); 49 | 50 | describe("template", () => { 51 | it("is a function", () => expect(typeof microlink.template).toBe("function")); 52 | it("returns a string", () => 53 | expect(typeof microlink.template(serviceTemplateParams)).toBe("string")); 54 | it("has a form", () => 55 | expect(microlink.template(serviceTemplateParams)).toContain(" 57 | expect(microlink.template(serviceTemplateParams)).toContain(" { 62 | it("validator is a function", () => 63 | expect(typeof currentService.validator).toBe("function")); 64 | it("validator returns bool", () => 65 | expect(typeof currentService.validator("")).toBe("boolean")); 66 | it("validator validates url", () => 67 | expect( 68 | typeof currentService.validator(currentService.exampleURL) 69 | ).toBeTruthy()); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /tests/helpers/search.test.ts: -------------------------------------------------------------------------------- 1 | import { SearchBaseClass } from "../../src/helpers/search"; 2 | import { GIPHY, Pexels, Pixabay, Unsplash } from "../../src"; 3 | 4 | interface IExampleImage { 5 | name: string; 6 | image: string; 7 | } 8 | const exampleImage: IExampleImage = { 9 | name: "user-name", 10 | image: "image-url", 11 | }; 12 | 13 | const exampleClass = new SearchBaseClass({ 14 | apiKey: "API_KEY", 15 | name: "example", 16 | color: "#000", 17 | icon: "", 18 | poweredByUrl: "https://example.com", 19 | popularEndpoint: (apiKey: string) => `https://example.com?key=${apiKey}`, 20 | searchEndpoint: (apiKey: string, q: string) => 21 | `https://example.com/search?key=${apiKey}&q=${q}`, 22 | getButton: (button: IExampleImage) => ``, 23 | getPopularResults: (response: IExampleImage[]) => response, 24 | getSearchResults: (response: IExampleImage[]) => response, 25 | }); 26 | 27 | const searchClasses = [ 28 | exampleClass, 29 | new GIPHY("API_KEY"), 30 | new Pexels("API_KEY"), 31 | new Pixabay("API_KEY"), 32 | new Unsplash("API_KEY"), 33 | ]; 34 | 35 | describe("example search service", () => { 36 | describe("popularEndpoint", () => { 37 | it("is a string", () => 38 | expect(typeof exampleClass.popularEndpoint).toBe("string")); 39 | it("is a button", () => 40 | expect(exampleClass.popularEndpoint).toBe( 41 | "https://example.com?key=API_KEY" 42 | )); 43 | }); 44 | describe("searchEndpoint", () => { 45 | it("is a function", () => 46 | expect(typeof exampleClass.searchEndpoint).toBe("function")); 47 | it("is a button", () => 48 | expect(exampleClass.searchEndpoint("API_KEY", "QUERY")).toBe( 49 | "https://example.com/search?key=API_KEY&q=QUERY" 50 | )); 51 | }); 52 | describe("getButton", () => { 53 | it("returns a string", () => 54 | expect(typeof exampleClass.getButton(exampleImage)).toBe("string")); 55 | it("returns a button", () => 56 | expect(exampleClass.getButton(exampleImage)).toContain(" { 59 | it("returns an object array", () => 60 | expect(typeof exampleClass.getPopularResults([exampleImage])).toBe( 61 | "object" 62 | )); 63 | it("is an array", () => 64 | expect( 65 | Array.isArray(exampleClass.getPopularResults([exampleImage])) 66 | ).toBeTruthy()); 67 | it("returns the same", () => 68 | expect(exampleClass.getPopularResults([exampleImage])).toEqual([ 69 | exampleImage, 70 | ])); 71 | }); 72 | describe("getSearchResults", () => { 73 | it("returns an object array", () => 74 | expect(typeof exampleClass.getSearchResults([exampleImage])).toBe( 75 | "object" 76 | )); 77 | it("is an array", () => 78 | expect( 79 | Array.isArray(exampleClass.getSearchResults([exampleImage])) 80 | ).toBeTruthy()); 81 | it("returns the same", () => 82 | expect(exampleClass.getSearchResults([exampleImage])).toEqual([ 83 | exampleImage, 84 | ])); 85 | }); 86 | }); 87 | 88 | for (const currentService of searchClasses) { 89 | describe(`${currentService.name} service`, () => { 90 | it("getButton is a function", () => 91 | expect(typeof currentService.getButton).toBe("function")); 92 | it("getPopularResults is a function", () => 93 | expect(typeof currentService.getPopularResults).toBe("function")); 94 | it("getSearchResults is a function", () => 95 | expect(typeof currentService.getSearchResults).toBe("function")); 96 | }); 97 | } 98 | -------------------------------------------------------------------------------- /tests/mocks.ts: -------------------------------------------------------------------------------- 1 | import { GlobalWithFetchMock } from "jest-fetch-mock"; 2 | import { 3 | IServiceTemplateParams, 4 | IHandlersParams, 5 | ITemplateParams, 6 | translate, 7 | Uppload, 8 | } from "../src"; 9 | const uppload = new Uppload(); 10 | import xhr from "xhr-mock"; 11 | 12 | const customGlobal: GlobalWithFetchMock = 13 | global as unknown as GlobalWithFetchMock; 14 | customGlobal.fetch = require("jest-fetch-mock"); 15 | customGlobal.fetchMock = customGlobal.fetch; 16 | export const fetch = customGlobal.fetch; 17 | 18 | const serviceTemplateParams: IServiceTemplateParams = { 19 | uppload, 20 | translate, 21 | }; 22 | const handlersParams: IHandlersParams = { 23 | upload: () => new Promise(() => {}), 24 | uploadMultiple: () => new Promise(() => {}), 25 | next: () => {}, 26 | handle: () => {}, 27 | showHelp: () => {}, 28 | uppload, 29 | translate, 30 | }; 31 | const effectTemplateParams: ITemplateParams = { 32 | file: { blob: new Blob() }, 33 | translate, 34 | }; 35 | 36 | export { xhr, serviceTemplateParams, handlersParams, effectTemplateParams }; 37 | -------------------------------------------------------------------------------- /tests/service.test.ts: -------------------------------------------------------------------------------- 1 | import { UpploadService, Uppload } from "../src"; 2 | import { 3 | Camera, 4 | Local, 5 | GIPHY, 6 | Pexels, 7 | Pixabay, 8 | Unsplash, 9 | NineGag, 10 | ArtStation, 11 | DeviantArt, 12 | Facebook, 13 | Flickr, 14 | Instagram, 15 | Pinterest, 16 | Screenshot, 17 | Twitter, 18 | URL, 19 | } from "../src"; 20 | import { serviceTemplateParams } from "./mocks"; 21 | 22 | const service = new UpploadService(); 23 | const services = [ 24 | new Camera(), 25 | new Local(), 26 | new GIPHY("giphy-api-key"), 27 | new Pexels("pexels-api-key"), 28 | new Pixabay("pixabay-api-key"), 29 | new Unsplash("unsplash-api-key"), 30 | new NineGag(), 31 | new ArtStation(), 32 | new DeviantArt(), 33 | new Facebook(), 34 | new Flickr(), 35 | new Instagram(), 36 | new Pinterest(), 37 | new Screenshot(), 38 | new Twitter(), 39 | new URL(), 40 | ]; 41 | 42 | describe("service template", () => { 43 | it("is a function", () => expect(typeof service.template).toBe("function")); 44 | it("gives a string", () => 45 | expect(typeof service.template(serviceTemplateParams)).toBe("string")); 46 | }); 47 | 48 | const uppload = new Uppload(); 49 | uppload.use(services); 50 | 51 | for (const currentService of services) { 52 | describe(`${currentService.name} service`, () => { 53 | it("template is a function", () => 54 | expect(typeof currentService.template).toBe("function")); 55 | it("template gives a string", () => 56 | expect(typeof currentService.template(serviceTemplateParams)).toBe( 57 | "string" 58 | )); 59 | it("service is registered", () => 60 | expect( 61 | // The camera service would not be registered because no Media Devices API 62 | currentService.name === "camera" 63 | ? !uppload.services.includes(currentService) 64 | : uppload.services.includes(currentService) 65 | ).toBeTruthy()); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /tests/services/local.test.ts: -------------------------------------------------------------------------------- 1 | import { Local } from "../../src"; 2 | import { handlersParams } from "../mocks"; 3 | 4 | const service = new Local(); 5 | 6 | describe("template", () => { 7 | it("is a function", () => expect(typeof service.template).toBe("function")); 8 | it("returns a string", () => 9 | expect(typeof service.template(handlersParams)).toBe("string")); 10 | it("has a drop area", () => 11 | expect(service.template(handlersParams)).toContain("drop-area")); 12 | it("has an input", () => 13 | expect(service.template(handlersParams)).toContain(" new Blob(), 23 | }, 24 | ], 25 | }, 26 | dataTransfer: { 27 | items: [ 28 | { 29 | kind: "file", 30 | type: "image/png", 31 | getAsFile: () => new Blob(), 32 | }, 33 | ], 34 | }, 35 | preventDefault: () => {}, 36 | }; 37 | 38 | test("drop handler", () => { 39 | let file = null; 40 | service.dropHandler(handlersParams, fakeEvent as any); 41 | expect(file).toBeDefined(); 42 | }); 43 | 44 | test("get file", () => { 45 | let file = null; 46 | service.getFile(handlersParams, fakeEvent as any); 47 | expect(file).toBeDefined(); 48 | }); 49 | 50 | test("drag handler", () => 51 | expect( 52 | service.dragHandler(handlersParams, fakeEvent as any) 53 | ).toBeUndefined()); 54 | 55 | test("file select", () => 56 | expect(service.fileSelect(handlersParams, fakeEvent as any)).toBeUndefined()); 57 | 58 | test("handlers", () => 59 | expect(service.handlers(handlersParams)).toBeUndefined()); 60 | -------------------------------------------------------------------------------- /tests/services/search/giphy.test.ts: -------------------------------------------------------------------------------- 1 | import GIPHY, { GIPHYResult } from "../../../src/services/search/giphy"; 2 | import { minifyHTML } from "../../../src/helpers/utils"; 3 | 4 | const service = new GIPHY("API_KEY"); 5 | 6 | const giphyResult: GIPHYResult = { 7 | id: "1", 8 | title: "An image", 9 | url: "https://giphy.com/image.gif", 10 | images: { 11 | downsized_large: { url: "https://giphy.com/large.gif" }, 12 | preview_gif: { url: "https://giphy.com/preview.gif" }, 13 | }, 14 | user: { 15 | avatar_url: "https://giphy.com/avatar.jpg", 16 | display_name: "Anand Chowdhary", 17 | profile_url: "https://giphy.com/anand", 18 | }, 19 | }; 20 | 21 | test("popular endpoint", () => 22 | expect(service.popularEndpoint).toBe( 23 | "https://api.giphy.com/v1/gifs/trending?api_key=API_KEY&limit=18&rating=G" 24 | )); 25 | 26 | test("search endpoint", () => 27 | expect(service.searchEndpoint("API_KEY", "QUERY")).toBe( 28 | "https://api.giphy.com/v1/gifs/search?api_key=API_KEY&q=QUERY&limit=18&offset=0&rating=G&lang=en" 29 | )); 30 | 31 | test("gets search results", () => 32 | expect(service.getSearchResults({ data: [giphyResult] })).toEqual([ 33 | giphyResult, 34 | ])); 35 | 36 | test("gets popular items", () => 37 | expect(service.getPopularResults({ data: [giphyResult] })).toEqual([ 38 | giphyResult, 39 | ])); 40 | 41 | test("gets button HTML", () => 42 | expect(minifyHTML(service.getButton(giphyResult))).toBe( 43 | `
` 44 | )); 45 | -------------------------------------------------------------------------------- /tests/services/search/pexels.test.ts: -------------------------------------------------------------------------------- 1 | import Pexels, { PexelsResult } from "../../../src/services/search/pexels"; 2 | import { minifyHTML } from "../../../src/helpers/utils"; 3 | 4 | const service = new Pexels("API_KEY"); 5 | 6 | const pexelsResult: PexelsResult = { 7 | url: "https://pexels.com/image", 8 | photographer: "Anand Chowdhary", 9 | src: { 10 | original: "https://pexels.com/original.jpg", 11 | large2x: "https://pexels.com/large.jpg", 12 | tiny: "https://pexels.com/tiny.jpg", 13 | }, 14 | }; 15 | 16 | test("popular endpoint", () => 17 | expect(service.popularEndpoint).toBe( 18 | "https://api.pexels.com/v1/curated?per_page=9&page=1" 19 | )); 20 | 21 | test("search endpoint", () => 22 | expect(service.searchEndpoint("API_KEY", "QUERY")).toBe( 23 | "https://api.pexels.com/v1/search?query=QUERY&per_page=12&page=1" 24 | )); 25 | 26 | test("gets search results", () => 27 | expect(service.getSearchResults({ photos: [pexelsResult] })).toEqual([ 28 | pexelsResult, 29 | ])); 30 | 31 | test("gets popular items", () => 32 | expect(service.getPopularResults({ photos: [pexelsResult] })).toEqual([ 33 | pexelsResult, 34 | ])); 35 | 36 | test("gets button HTML", () => 37 | expect(minifyHTML(service.getButton(pexelsResult))).toBe( 38 | `
Anand Chowdhary
` 39 | )); 40 | -------------------------------------------------------------------------------- /tests/services/search/pixabay.test.ts: -------------------------------------------------------------------------------- 1 | import Pixabay, { PixabayResult } from "../../../src/services/search/pixabay"; 2 | import { minifyHTML } from "../../../src/helpers/utils"; 3 | 4 | const service = new Pixabay("API_KEY"); 5 | 6 | const pixabayResult: PixabayResult = { 7 | id: 1, 8 | largeImageURL: "https://pixabay.com/large.jpg", 9 | previewURL: "https://pixabay.com/preview.jpg", 10 | user: "Anand Chowdhary", 11 | userImageURL: "https://pixabay.com/user.jpg", 12 | pageURL: "https://pixabay.com/file", 13 | tags: "photo, example", 14 | }; 15 | 16 | test("popular endpoint", () => 17 | expect(service.popularEndpoint).toBe( 18 | "https://pixabay.com/api/?key=API_KEY&per_page=18&image_type=photo" 19 | )); 20 | 21 | test("search endpoint", () => 22 | expect(service.searchEndpoint("API_KEY", "QUERY")).toBe( 23 | "https://pixabay.com/api/?key=API_KEY&per_page=18&q=QUERY&image_type=photo" 24 | )); 25 | 26 | test("gets search results", () => 27 | expect(service.getSearchResults({ hits: [pixabayResult] })).toEqual([ 28 | pixabayResult, 29 | ])); 30 | 31 | test("gets popular items", () => 32 | expect(service.getPopularResults({ hits: [pixabayResult] })).toEqual([ 33 | pixabayResult, 34 | ])); 35 | 36 | test("gets button HTML", () => 37 | expect(minifyHTML(service.getButton(pixabayResult))).toBe( 38 | `
Anand Chowdhary
` 39 | )); 40 | -------------------------------------------------------------------------------- /tests/services/search/unsplash.test.ts: -------------------------------------------------------------------------------- 1 | import Unsplash, { 2 | UnsplashResult, 3 | } from "../../../src/services/search/unsplash"; 4 | import { minifyHTML } from "../../../src/helpers/utils"; 5 | 6 | const service = new Unsplash("API_KEY"); 7 | 8 | const unsplashResult: UnsplashResult = { 9 | id: "1", 10 | description: "Tree", 11 | alt_description: "An image of a tree", 12 | urls: { 13 | regular: "https://unsplash.com/regular.jpg", 14 | thumb: "https://unsplash.com/thumb.jpg", 15 | }, 16 | user: { 17 | name: "Anand Chowdhary", 18 | profile_image: { 19 | small: "https://unsplash.com/user.jpg", 20 | }, 21 | }, 22 | }; 23 | 24 | test("popular endpoint", () => 25 | expect(service.popularEndpoint).toBe( 26 | "https://api.unsplash.com/photos?client_id=API_KEY" 27 | )); 28 | 29 | test("search endpoint", () => 30 | expect(service.searchEndpoint("API_KEY", "QUERY")).toBe( 31 | "https://api.unsplash.com/search/photos?client_id=API_KEY&page=1&query=QUERY" 32 | )); 33 | 34 | test("gets search results", () => 35 | expect(service.getSearchResults({ results: [unsplashResult] })).toEqual([ 36 | unsplashResult, 37 | ])); 38 | 39 | test("gets popular items", () => 40 | expect(service.getPopularResults([unsplashResult])).toEqual([ 41 | unsplashResult, 42 | ])); 43 | 44 | test("gets button HTML", () => 45 | expect(minifyHTML(service.getButton(unsplashResult))).toBe( 46 | `
Anand Chowdhary
` 47 | )); 48 | -------------------------------------------------------------------------------- /tests/uppload.test.ts: -------------------------------------------------------------------------------- 1 | import { Uppload, en, Instagram, Crop } from "../src"; 2 | 3 | const uppload = new Uppload({ lang: en }); 4 | 5 | test("adds an Uppload service", () => { 6 | uppload.use(new Instagram()); 7 | expect( 8 | uppload.services.filter(services => services.name === "instagram").length 9 | ).toBe(1); 10 | }); 11 | 12 | test("adds an Uppload effect", () => { 13 | uppload.use(new Crop()); 14 | expect( 15 | uppload.effects.filter(effects => effects.name === "crop").length 16 | ).toBe(1); 17 | }); 18 | 19 | describe("event emitter", () => { 20 | it("on should be a function", () => 21 | expect(typeof uppload.on).toBe("function")); 22 | it("off should be a function", () => 23 | expect(typeof uppload.off).toBe("function")); 24 | it("should emit open event", () => { 25 | let completed = false; 26 | uppload.on("open", () => { 27 | completed = true; 28 | }); 29 | uppload.open(); 30 | expect(completed).toBeTruthy(); 31 | }); 32 | }); 33 | 34 | describe("modal functions", () => { 35 | it("opens", () => { 36 | uppload.open(); 37 | expect(uppload.modalOpen()).toBeTruthy(); 38 | }); 39 | it("has a default active service", () => { 40 | expect(uppload.activeService).toBe("instagram"); 41 | }); 42 | it("has a default active effect", () => { 43 | expect(uppload.activeEffect).toBeFalsy(); 44 | }); 45 | it("navigates to service", () => { 46 | uppload.navigate("instagram"); 47 | expect(uppload.activeService).toBe("instagram"); 48 | }); 49 | it("navigates to service", () => { 50 | expect(() => uppload.navigate("unknown-service")).toThrow(); 51 | }); 52 | it("navigates to effect", () => { 53 | uppload.navigate("instagram"); 54 | expect(uppload.activeService).toBe("instagram"); 55 | }); 56 | it("closes", () => { 57 | uppload.close(); 58 | expect(uppload.modalOpen()).toBeFalsy(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "lib": ["ESNext", "DOM"], 8 | "moduleResolution": "Node", 9 | "strict": true, 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "types": ["vite/client", "node", "jest"] 19 | }, 20 | "include": ["src"], 21 | "exclude": ["**/*.test.ts", "node_modules", "test/**", ".history/**"] 22 | } 23 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import {defineConfig} from "vite"; 3 | 4 | module.exports = defineConfig({ 5 | base: "./", 6 | build: { 7 | lib: { 8 | entry: path.resolve(__dirname, "src/index.ts"), 9 | name: 'Uppload', 10 | formats: ['es', 'cjs', 'umd', 'iife'], 11 | fileName: 'uppload', 12 | }, 13 | rollupOptions: { 14 | external: ["focus-trap", "mitt", "cropperjs"], 15 | output: { 16 | globals: { 17 | "focus-trap": "createFocusTrap", 18 | mitt: "mitt", 19 | cropperjs: "Cropper", 20 | }, 21 | }, 22 | }, 23 | }, 24 | }); 25 | --------------------------------------------------------------------------------