<h1 align="center">
  <a href="https://www.bytescale.com/docs/upload-widget/vue">
    <img alt="Bytescale Upload Widget for Vue" width="524" height="80" src="https://raw.githubusercontent.com/bytescale/bytescale-upload-widget-vue/main/.github/assets/bytescale-upload-widget-vue.svg">
  </a>
</h1>
<p align="center"><b>Beautiful File Upload Widget for Vue</b><br/> (With Built-in Cloud Storage)</p>
<br/>
<p align="center">
  <a href="https://www.npmjs.com/package/@bytescale/upload-widget-vue">
    <img src="https://img.shields.io/badge/%40bytescale%2Fupload--widget--vue-npm-4ba0f6" />
  </a>

  <a href="https://github.com/bytescale/bytescale-upload-widget-vue/actions/workflows/ci.yml">
    <img src="https://img.shields.io/badge/build-passing-4ba0f6" />
  </a>

  <a href="https://github.com/bytescale/bytescale-upload-widget-vue/">
    <img src="https://img.shields.io/badge/gzipped-33%20kb-4ba0f6" />
  </a>

  <a href="https://www.npmjs.com/package/@bytescale/upload-widget-vue">
    <img src="https://img.shields.io/npm/dt/uploader?color=%234ba0f6" />
  </a>
  <br/>

  <a href="https://www.npmjs.com/package/@bytescale/upload-widget-vue">
    <img src="https://img.shields.io/badge/TypeScript-included-4ba0f6" />
  </a>

  <a href="https://github.com/bytescale/bytescale-upload-widget-vue/actions/workflows/ci.yml">
    <img src="https://img.shields.io/npms-io/maintenance-score/uploader?color=4ba0f6" />
  </a>

  <a target="_blank" href="https://twitter.com/intent/tweet?text=This%20was%20a%20great%20find...%0A%0Ahttps%3A%2F%2Fgithub.com%2Fbytescale%2Fbytescale-upload-widget-vue">
    <img alt="Twitter URL" src="https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Fbytescale%2Fbytescale-upload-widget-vue" />
  </a>

</p>
<h1 align="center">
  Get Started —
  <a href="https://codepen.io/bytescale/pen/yLpvYew?editors=0010">
    Try on CodePen
  </a>
</h1>

<p align="center"><a href="https://www.bytescale.com/docs/upload-widget"><img alt="Upload Widget Demo" width="100%" src="https://raw.githubusercontent.com/bytescale/bytescale-upload-widget/main/.github/assets/demo.gif"></a></p>

<p align="center">100% Serverless File Upload Widget  <br /> Powered by <a href="https://www.bytescale.com/">Bytescale</a><br/><br/></p>


<hr/>

<p align="center">
  <b>Supports:</b> Image Cropping, Video Previews, Document Previews, Drag & Drop, and more...
  <br />
  <br />
  <a href="https://www.bytescale.com/docs/upload-widget/vue" rel="nofollow"><b>Full Documentation</b></a> • <a href="https://www.bytescale.com/docs/sdks/javascript" rel="nofollow">Headless SDK</a> • <a href="https://www.bytescale.com/docs/media-processing-apis" rel="nofollow">Media Processing APIs</a> • <a href="https://www.bytescale.com/docs/storage/sources" rel="nofollow">Storage</a> • <a href="https://www.bytescale.com/docs/cdn" rel="nofollow">CDN</a>
</p>

<hr/>

<br />

## Installation

Install via NPM:

```shell
npm install @bytescale/upload-widget-vue
```

Or via YARN:

```shell
yarn add @bytescale/upload-widget-vue
```

Or via a `<script>` tag:

```html
<script src="https://js.bytescale.com/upload-widget-vue/v4"></script>
```

## Usage

### Option 1) File Upload Button — [Try on CodePen](https://codepen.io/bytescale/pen/yLpvYew?editors=1000)

Create a file upload button using the `openUploadModal` helper:

```html
<template>
  <button @click="uploadFile">Upload a file...</button>
</template>

<script lang="ts">
  import { openUploadModal } from "@bytescale/upload-widget-vue";
  import type { UploadWidgetConfig, UploadWidgetResult } from "@bytescale/upload-widget";
  import type { PreventableEvent } from "@bytescale/upload-widget-vue";

  // Full Configuration:
  // https://www.bytescale.com/docs/upload-widget#configuration
  const options: UploadWidgetConfig = {
    apiKey: "free", // Get API keys from: www.bytescale.com
    maxFileCount: 10
  };

  export default {
    name: "App",
    methods: {
      uploadFile(event: PreventableEvent) {
        openUploadModal({
          event,
          options,
          onComplete: (files: UploadWidgetResult[]) => {
            if (files.length === 0) {
              alert("No files selected.");
            } else {
              alert(files.map(f => f.fileUrl).join("\n"));
            }
          }
        })
      }
    }
  };
</script>
```

### Option 2) Dropzone — [Try on CodePen](https://codepen.io/bytescale/pen/RwJvpKG?editors=1000)

Create a file upload dropzone using the `UploadDropzone` component:

```html
<template>
  <UploadDropzone :options="options"
                  :on-complete="onFilesUploaded"
                  :on-update="onFileListChanged"
                  width="600px"
                  height="375px" />
</template>

<script lang="ts">
import { UploadDropzone } from "@bytescale/upload-widget-vue";
import type { UploadWidgetConfig, UploadWidgetResult, UploadWidgetOnUpdateEvent } from "@bytescale/upload-widget";

// Full Configuration:
// https://www.bytescale.com/docs/upload-widget#configuration
const options: UploadWidgetConfig = {
  apiKey: "free", // Get API keys from: www.bytescale.com
  maxFileCount: 10,
  showFinishButton: true
};

export default {
  name: "App",
  components: {
    UploadDropzone
  },
  data() {
    return {
      options
    };
  },
  methods: {
    onFileListChanged({ uploadedFiles, pendingFiles, failedFiles }: UploadWidgetOnUpdateEvent) {
      const uploadedFileUrls = uploadedFiles.map(x => x.fileUrl).join("\n");
      console.log(uploadedFileUrls);
    },
    onFilesUploaded(files: UploadWidgetResult[]) {
      if (files.length === 0) {
        alert("No files selected.");
      } else {
        alert(files.map(f => f.fileUrl).join("\n"));
      }
    }
  }
};
</script>
```

> **Special behaviour for dropzones:**
>
> `on-complete` only fires if `showFinishButton = true` (when the user clicks "Finish").
>
> `on-update` must be used when `showFinishButton = false`.
>
> Default value: `showFinishButton = false`

## Result

The callbacks receive a `Array<UploadWidgetResult>`:

```javascript
{
  fileUrl: "https://upcdn.io/FW25...",   // URL to use when serving this file.
  filePath: "/uploads/example.jpg",      // File path (we recommend saving this to your database).

  accountId: "FW251aX",                  // Bytescale account the file was uploaded to.

  editedFile: undefined,                 // Edited file (for image crops). Same structure as below.

  originalFile: {
    fileUrl: "https://upcdn.io/FW25...", // Uploaded file URL.
    filePath: "/uploads/example.jpg",    // Uploaded file path (relative to your raw file directory).
    accountId: "FW251aX",                // Bytescale account the file was uploaded to.
    originalFileName: "example.jpg",     // Original file name from the user's machine.
    file: { ... },                       // Original DOM file object from the <input> element.
    size: 12345,                         // File size in bytes.
    lastModified: 1663410542397,         // Epoch timestamp of when the file was uploaded or updated.
    mime: "image/jpeg",                  // File MIME type.
    metadata: {
      ...                                // User-provided JSON object.
    },
    tags: [
      "tag1",                            // User-provided & auto-generated tags.
      "tag2",
      ...
    ]
  }
}
```

## ⚙️ Configuration

All configuration is optional (except for the `apiKey` field, which is required).

```javascript
const options = {
  apiKey: "free",                 // Get API keys from: www.bytescale.com
  locale: myCustomLocale,         // EN_US by default. (See "Localization" section below.)
  maxFileCount: 5,                // Unlimited by default (or 1 if multi: false).
  maxFileSizeBytes: 1024 ** 2,    // Unlimited by default.
  mimeTypes: ["image/*"],         // Unrestricted by default. Supports * wildcard suffix.
  multi: false,                   // False by default.
  onInit: ({                      // Exposes lifecycle methods for the component.
    close,                        // Closes the widget when called.
    reset,                        // Resets the widget when called.
    updateConfig                  // Updates the widget's config by passing a new config
  }) => {},                       // object to the method's first parameter.
  onUpdate: (event) => {          // Called each time the Upload Widget's list of files change.
    // event.pendingFiles         // Array of files that are either uploading or queued.
    // event.failedFiles          // Array of files that failed to upload (due to network or validation reasons).
    // event.uploadedFiles        // Array of files that have been uploaded and not removed.
  },
  onPreUpload: async file => ({
    errorMessage: "Uh oh!",       // Displays this validation error to the user (if set).
    transformedFile: file         // Uploads 'transformedFile' instead of 'file' (if set).
  }),
  showFinishButton: true,         // Show/hide the "finish" button in the widget.
  showRemoveButton: true,         // Show/hide the "remove" button next to each file.
  styles: {
    breakpoints: {
      fullScreenWidth: 750,       // Full-screen mode activates when the screen is at or below this width.
      fullScreenHeight: 420       // Full-screen mode activates when the screen is at or below this height.
    },
    colors: {
      primary: "#377dff",         // Primary buttons & links
      active: "#528fff",          // Primary buttons & links (hover). Inferred if undefined.
      error: "#d23f4d",           // Error messages
      shade100: "#333",           // Standard text
      shade200: "#7a7a7a",        // Secondary button text
      shade300: "#999",           // Secondary button text (hover)
      shade400: "#a5a6a8",        // Welcome text
      shade500: "#d3d3d3",        // Modal close button
      shade600: "#dddddd",        // Border
      shade700: "#f0f0f0",        // Progress indicator background
      shade800: "#f8f8f8",        // File item background
      shade900: "#fff"            // Various (draggable crop buttons, etc.)
    },
    fontFamilies: {
      base: "arial, sans-serif"   // Base font family (comma-delimited).
    },
    fontSizes: {
      base: 16                    // Base font size (px).
    }
  },
  path: {                         // Optional: a string (full file path) or object like so:
    fileName: "Example.jpg",      // Supports path variables (e.g. {ORIGINAL_FILE_EXT}).
    folderPath: "/uploads"        // Please refer to docs for all path variables.
  },
  metadata: {
    hello: "world"                // Arbitrary JSON metadata (saved against the file).
  },
  tags: ["profile_picture"],      // Requires a Bytescale account.
  editor: {
    images: {
      allowResizeOnMove: true,    // True by default. If false, prevents cropper from resizing when moved.
      preview: true,              // True by default if cropping is enabled. Previews PDFs and videos too.
      crop: true,                 // True by default.
      cropFilePath: image => {    // Choose the file path used for JSON image crop files.
        const {filePath} = image  // In:  https://www.bytescale.com/docs/types/UploadedFile
        return `${filePath}.crop` // Out: https://www.bytescale.com/docs/types/FilePathDefinition
      },
      cropRatio: 4 / 3,           // Width / Height. Undefined enables freeform (default).
      cropShape: "rect"           // "rect" (default) or "circ".
    }
  },
}
```

### 🏳️ Localization

Default is [EN_US](https://github.com/bytescale/bytescale-upload-widget/blob/main/lib/src/modules/locales/EN_US.ts):

```javascript
const myCustomLocale = {
  addAnotherFileBtn: "Add another file...",
  addAnotherImageBtn: "Add another image...",
  cancelBtn: "cancel",
  cancelBtnClicked: "cancelled",
  cancelPreviewBtn: "Cancel",
  continueBtn: "Continue",
  cropBtn: "Crop",
  customValidationFailed: "Failed to validate file.",
  doneBtn: "Done",
  fileSizeLimitPrefix: "File size limit:",
  finishBtn: "Finished",
  finishBtnIcon: true,
  imageCropNumberPrefix: "Image",
  maxFilesReachedPrefix: "Maximum number of files:",
  maxImagesReachedPrefix: "Maximum number of images:",
  orDragDropFile: "...or drag and drop a file.",
  orDragDropFileMulti: "...or drag and drop files.",
  orDragDropImage: "...or drag and drop an image.",
  orDragDropImageMulti: "...or drag and drop images.",
  processingFile: "Processing file...",
  removeBtn: "remove",
  removeBtnClicked: "removed",
  submitBtnError: "Error!",
  submitBtnLoading: "Please wait...",
  unsupportedFileType: "File type not supported.",
  uploadFileBtn: "Upload a File",
  uploadFileMultiBtn: "Upload Files",
  uploadImageBtn: "Upload an Image",
  uploadImageMultiBtn: "Upload Images",
  xOfY: "of"
}
```

# 🌐 API Support

## 🌐 File Management APIs

Bytescale provides a wide range of [File Management APIs](https://www.bytescale.com/docs/apis):

- **[File Listing](https://www.bytescale.com/docs/folder-api/ListFolder)**
- **[File Deleting](https://www.bytescale.com/docs/file-api/DeleteFile)**
- **[File Copying](https://www.bytescale.com/docs/file-api/CopyFile)**
- [And more...](https://www.bytescale.com/docs/apis)

## 🌐 Media Processing APIs (Image/Video/Audio)

Bytescale also provides real-time [Media Processing APIs](https://www.bytescale.com/docs/media-processing-apis):

- **[Image Processing APIs](https://www.bytescale.com/docs/image-processing-api)** ([resize](https://www.bytescale.com/docs/image-processing-api#image-resizing-api), [crop](https://www.bytescale.com/docs/image-processing-api#image-cropping-api), [convert](https://www.bytescale.com/docs/image-processing-api#f), [compress](https://www.bytescale.com/docs/image-processing-api#image-compression-api) & [watermark](https://www.bytescale.com/docs/image-processing-api#Text-layering-api))
- **[Video Processing APIs](https://www.bytescale.com/docs/video-processing-api)** ([transcode](https://www.bytescale.com/docs/video-processing-api#video-transcoding-api), [optimize](https://www.bytescale.com/docs/video-processing-api#video-compression-api), [resize](https://www.bytescale.com/docs/video-processing-api#video-resizing-api) & [extract metadata](https://www.bytescale.com/docs/video-processing-api#video-metadata-api))
- **[Audio Processing APIs](https://www.bytescale.com/docs/audio-processing-api)** ([transcode](https://www.bytescale.com/docs/audio-processing-api#audio-transcoding-api), [optimize](https://www.bytescale.com/docs/audio-processing-api#audio-compression-api), [trim](https://www.bytescale.com/docs/audio-processing-api#audio-trimming-api) & [extract metadata](https://www.bytescale.com/docs/audio-processing-api#audio-metadata-api))

### Image Processing API (Original Image)

Here's an example using [a photo of Chicago](https://upcdn.io/W142hJk/raw/example/city-landscape.jpg):

<img src="https://upcdn.io/W142hJk/raw/example/city-landscape.jpg" />

```
https://upcdn.io/W142hJk/raw/example/city-landscape.jpg
```

### Image Processing API (Transformed Image)

Using the [Image Processing API](https://www.bytescale.com/docs/image-processing-api), you can produce [this image](https://upcdn.io/W142hJk/image/example/city-landscape.jpg?w=900&h=600&fit=crop&f=webp&q=80&blur=4&text=WATERMARK&layer-opacity=80&blend=overlay&layer-rotate=315&font-size=100&padding=10&font-weight=900&color=ffffff&repeat=true&text=Chicago&gravity=bottom&padding-x=50&padding-bottom=20&font=/example/fonts/Lobster.ttf&color=ffe400):

<img src="https://upcdn.io/W142hJk/image/example/city-landscape.jpg?w=900&h=600&fit=crop&f=webp&q=80&blur=4&text=WATERMARK&layer-opacity=80&blend=overlay&layer-rotate=315&font-size=100&padding=10&font-weight=900&color=ffffff&repeat=true&text=Chicago&gravity=bottom&padding-x=50&padding-bottom=20&font=/example/fonts/Lobster.ttf&color=ffe400" />

```
https://upcdn.io/W142hJk/image/example/city-landscape.jpg
  ?w=900
  &h=600
  &fit=crop
  &f=webp
  &q=80
  &blur=4
  &text=WATERMARK
  &layer-opacity=80
  &blend=overlay
  &layer-rotate=315
  &font-size=100
  &padding=10
  &font-weight=900
  &color=ffffff
  &repeat=true
  &text=Chicago
  &gravity=bottom
  &padding-x=50
  &padding-bottom=20
  &font=/example/fonts/Lobster.ttf
  &color=ffe400
```

## Authentication

Bytescale supports two types of authentication:

### API Keys

The Bytescale Upload Widget uses the `apiKey` parameter to authenticate with [Bytescale](https://www.bytescale.com/).

With API key auth, the requester has access to the resources available to the API key:

- Secret API keys (`secret_***`) can perform all API operations (see: [Bytescale JavaScript SDK](https://www.bytescale.com/docs/sdks/javascript)).

- Public API keys (`public_***`) can perform file uploads and file downloads only. File overwrites, file deletes, and all other destructive operations cannot be performed using public API keys.

You must always use **public API keys** (e.g. `public_***`) in your client-side code.

Each API key can have its read/write access limited to a subset of files/folders.

### JWTs

JWTs are optional.

With JWTs, users can download private files directly via the URL, as authentication is performed implicitly via a session cookie _or_ via an `authorization` header if service workers are enabled (see the `serviceWorkerScript` param on the `AuthManager.beginAuthSession` method). This allows the browser to display private files in `<img>`, `<video>`, and other elements.

With JWTs, users can upload files to per-user folders. This is because the permissions in the [JWT's payload](https://www.bytescale.com/docs/types/BytescaleJwt) can be generated at runtime. The [Bytescale Upload Widget](https://www.bytescale.com/docs/upload-widget) internally uses the [Bytescale JavaScript SDK](https://www.bytescale.com/docs/sdks/javascript) to perform file uploads: the Bytescale JavaScript SDK handles the JWT refresh process with your API, requesting new JWTs when required, and includes your JWTs in all subsequent requests to the Bytescale API.

[Learn more about the `AuthManager` and JWTs »](https://www.bytescale.com/docs/auth)

## UrlBuilder

The [Bytescale JavaScript SDK](https://github.com/bytescale/bytescale-javascript-sdk) exports a `UrlBuilder` to construct URLs from `filePaths`:

```javascript
import { UrlBuilder } from "@bytescale/sdk";
```

**Recommended:** you should always save `filePaths` to your DB instead of `fileUrls`.

#### Raw Files

To get the URL for the uploaded image `/example.jpg` in its original form, use the following:

```javascript
// Returns: "https://upcdn.io/1234abc/raw/example.jpg"
UrlBuilder.url({
  accountId: "1234abc",
  filePath: "/example.jpg"
});
```

#### Images

To resize the uploaded image `/example.jpg` to 800x600, use the following:

```javascript
// Returns: "https://upcdn.io/1234abc/image/example.jpg?w=800&h=600"
UrlBuilder.url({
  accountId: "1234abc",
  filePath: "/example.jpg",
  options: {
    transformation: "image",
    transformationParams: {
      w: 800,
      h: 600
    }
  }
});
```

[Image Processing API Docs »](https://www.bytescale.com/docs/image-processing-api)

#### Videos

To transcode the uploaded video `/example.mov` to MP4/H.264 in HD, use the following:

```javascript
// Returns: "https://upcdn.io/1234abc/video/example.mov?f=mp4-h264&h=1080"
UrlBuilder.url({
  accountId: "1234abc",
  filePath: "/example.mov",
  options: {
    transformation: "video",
    transformationParams: {
      f: "mp4-h264",
      h: 1080
    }
  }
});
```

[Video Processing API Docs »](https://www.bytescale.com/docs/video-processing-api)

#### Audio

To transcode the uploaded audio `/example.wav` to AAC in 192kbps, use the following:

```javascript
// Returns: "https://upcdn.io/1234abc/audio/example.wav?f=aac&br=192"
UrlBuilder.url({
  accountId: "1234abc",
  filePath: "/example.wav",
  options: {
    transformation: "audio",
    transformationParams: {
      f: "aac",
      br: 192
    }
  }
});
```

[Audio Processing API Docs »](https://www.bytescale.com/docs/audio-processing-api)

#### Archives

To extract the file `document.docx` from the uploaded ZIP file `/example.zip`:

```javascript
// Returns: "https://upcdn.io/1234abc/archive/example.zip?m=extract&artifact=/document.docx"
UrlBuilder.url({
  accountId: "1234abc",
  filePath: "/example.zip",
  options: {
    transformation: "archive",
    transformationParams: {
      m: "extract"
    },
    artifact: "/document.docx"
  }
});
```

[Archive Processing API Docs »](https://www.bytescale.com/docs/archive-processing-api)

## 🙋 Can I use my own storage?

Bytescale supports AWS S3, Cloudflare R2, Google Storage, DigitalOcean, and Bytescale Storage.

**[Bytescale Storage Docs »](https://www.bytescale.com/docs/storage/sources)**

## 👋 Create your Bytescale Account

Bytescale is the best way to upload, transform, and serve images, videos, and audio at scale.

**[Create a Bytescale account »](https://www.bytescale.com/get-started)**

#### Full Documentation

- **[Bytescale Upload Widget Docs »](https://www.bytescale.com/docs/upload-widget)**
- **[Bytescale Upload Widget (React) Docs »](https://www.bytescale.com/docs/upload-widget/frameworks/react)**
- **[Bytescale Upload Widget (Angular) Docs »](https://www.bytescale.com/docs/upload-widget/frameworks/angular)**
- **[Bytescale Upload Widget (Vue.js) Docs »](https://www.bytescale.com/docs/upload-widget/frameworks/vue)**
- **[Bytescale Upload Widget (jQuery) Docs »](https://www.bytescale.com/docs/upload-widget/frameworks/jquery)**

## License

[MIT](LICENSE)
