Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/inject-styles-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"dropzone": minor
---

Add `injectStyles`, which makes Dropzone add its own stylesheet to the document so you no longer have to include the CSS yourself.

- `false` — add nothing. The default.
- `true` or `"full"` — `dropzone.css`, the ready-to-go styling.
- `"basic"` — `basic.css`, layout only, for styling it yourself.

The two stylesheets are alternatives rather than layers: `basic.css` is not a subset of `dropzone.css` and each carries rules the other does not, so picking one excludes the other.

The CSS is inserted once per page however many dropzones exist, and first in `<head>`, so your own rules still win on equal specificity without `!important`. If two dropzones ask for different stylesheets, the first one constructed wins.

It defaults to `false` so that upgrading changes nothing. That is the only reason it is off: the stylesheets travel inside the JavaScript whether or not the option is used, because a runtime flag cannot be tree-shaken, so importing the CSS through a bundler as well ships it twice. If you bundle, switching this on is about 1.2 kB gzipped smaller than importing the stylesheet.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Here is a list of all available options for Dropzone. In case any of this inform
| `acceptedMimeTypes` | `null` | **Deprecated!** Use acceptedFiles instead. |
| `autoProcessQueue` | `true` | <p>If false, files will be added to the queue but the queue will not be processed automatically. This can be useful if you need some additional user input before sending files (or if you want want all files sent at once). If you're ready to send the file simply call <code>myDropzone.processQueue()</code>.<br />See the [Upload Queue](./upload-queue.md) documentation section for more information.</p> |
| `autoQueue` | `true` | If false, files added to the dropzone will not be queued by default. You'll have to call `enqueueFile(file)` manually. |
| `injectStyles` | `false` | Adds Dropzone's stylesheet to the document, so you don't have to include it yourself. `true` or `"full"` adds `dropzone.css`; `"basic"` adds `basic.css` instead — they are alternatives, not layers. Inserted once per page, and first in `<head>`, so your own rules still win on equal specificity. See [Installation](../../getting-started/installation/package-manager.md#css). |
| `addRemoveLinks` | `false` | If `true`, this will add a link to every file preview to remove or cancel (if already uploading) the file. The `dictCancelUpload`, `dictCancelUploadConfirmation` and `dictRemoveFile` options are used for the wording. |
| `previewsContainer` | `null` | Defines where to display the file previews – if `null` the Dropzone element itself is used. Can be a plain `HTMLElement` or a CSS selector. The element should have the `dropzone-previews` class so the previews are displayed properly. |
| `disablePreviews` | `false` | Set this to `true` if you don't want previews to be shown. |
Expand Down
39 changes: 39 additions & 0 deletions apps/docs/docs/getting-started/installation/package-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,45 @@ Importing this CSS file greatly depends on the bundler or framework that you are

You can also simply include the CSS file in your html. Refer to the [Stand-alone file](stand-alone.md) section for this.

### Letting Dropzone add the CSS for itself

If you would rather not wire the stylesheet up at all, set `injectStyles` and Dropzone inserts it for you:

```javascript
new Dropzone("#my-form", { url: "/file/post", injectStyles: true });
```

It takes which stylesheet you want:

| value | what it adds |
| --- | --- |
| `false` | nothing. The default |
| `true` or `"full"` | `dropzone.css`, the ready-to-go styling |
| `"basic"` | `basic.css`, layout only, for styling it yourself |

The two are **alternatives, not layers** — `basic.css` is not a subset of `dropzone.css`, and each carries rules the other does not, so picking one excludes the other.

It is added once per page however many dropzones you create, and inserted **first** in `<head>`, so your own stylesheet still wins on equal specificity without needing `!important`. If two dropzones ask for different stylesheets, the first one constructed wins.

:::caution

A `<link>` blocks the first paint; this cannot. The styles arrive when the script does, so on a slow connection the browser may paint before them.

Everything Dropzone builds — the message, the previews — is created *after* the styles go in, so it is never seen unstyled, and an empty `<div class="dropzone">` has nothing to show either way. But markup **you** put inside the dropzone element is visible unstyled until the script runs, and shifts when the styling lands. Link the stylesheet if that matters to you.

:::

It is off by default so that upgrading changes nothing for anyone. That is the only reason — it is **not** the cheaper setting.

The stylesheets travel inside the JavaScript whether or not you switch the option on, because a runtime flag cannot be tree-shaken. So importing the CSS through your bundler as well ships it **twice**: once as a string inside the script, once as a stylesheet. Measured on a minimal app:

| | ships | gzipped |
| --- | --- | --- |
| `import "dropzone/dist/dropzone.css"` | script + stylesheet | 15,518&nbsp;B |
| `injectStyles: true` | script only | **14,279&nbsp;B** |

If you are bundling, turning it on is the smaller choice. The reason to link the stylesheet instead is the timing above, not the size.

:::info

You can check out the [examples repository](https://github.com/dropzone/dropzone-examples) for ways to handle this with different bundlers.
Expand Down
44 changes: 44 additions & 0 deletions packages/dropzone/e2e/inject-styles.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { test, expect } from "@playwright/test";

// The page links no stylesheet at all, so anything that computes to a styled
// value can only have come from the injection -- through the built standalone
// bundle, where the CSS has to have survived bundling as a string.
test.describe("Dropzone with injectStyles", () => {
test("styles the dropzone without a linked stylesheet", async ({ page }) => {
await page.goto("/1-basic/inject_styles.html");

await expect(page.locator("link[rel=stylesheet]")).toHaveCount(0);
await expect(page.locator("style[data-dropzone]")).toHaveCount(1);

const minHeight = await page
.locator(".dropzone")
.evaluate((el) => getComputedStyle(el).minHeight);
expect(minHeight).toBe("150px");
});

test("puts its stylesheet first so the page can override it", async ({ page }) => {
await page.goto("/1-basic/inject_styles.html");

const isFirst = await page.evaluate(
() => document.head.firstElementChild?.matches("style[data-dropzone]") ?? false,
);
expect(isFirst).toBe(true);
});

test("adds the basic stylesheet instead when asked", async ({ page }) => {
await page.goto("/1-basic/inject_styles_basic.html");

await expect(page.locator("link[rel=stylesheet]")).toHaveCount(0);
await expect(page.locator("style[data-dropzone]")).toHaveAttribute("data-dropzone", "basic");

// basic.css sets position: relative on .dropzone; the full sheet does not,
// and sets a min-height that basic leaves alone. So each is identifiable
// by what the other lacks.
const style = await page.locator(".dropzone").evaluate((el) => {
const s = getComputedStyle(el);
return { position: s.position, minHeight: s.minHeight };
});
expect(style.position).toBe("relative");
expect(style.minHeight).not.toBe("150px");
});
});
36 changes: 36 additions & 0 deletions packages/dropzone/src/dropzone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import extend from "./extend";
import Emitter from "./emitter";
import defaultOptions from "./options";
import type { DropzoneOptions, ResolvedDropzoneOptions } from "./options";
// The stylesheet as text rather than a side-effecting import, so nothing is
// added to the document unless injectStyles asks for it.
import fullStyles from "./dropzone.css?inline";
import basicStyles from "./basic.css?inline";

export type { DropzoneOptions, ResolvedDropzoneOptions };

Expand Down Expand Up @@ -74,6 +78,13 @@ export type DropzoneThumbnailCallback = (
canvas?: HTMLCanvasElement | null,
) => void;

/**
* Which stylesheet `injectStyles` should add. The two are alternatives rather
* than layers -- `basic` is not a subset of `full` -- so picking one excludes
* the other. `true` means `"full"`.
*/
export type DropzoneStyles = boolean | "basic" | "full";

/** One element and the handlers bound to it, as tracked for removal. */
export type DropzoneListener = {
element: HTMLElement | Document;
Expand Down Expand Up @@ -219,6 +230,12 @@ export default class Dropzone extends Emitter {

this.options.previewTemplate = this.options.previewTemplate.replace(/\n*/g, "");

// Before the fallback check: the fallback form is styled by the same
// stylesheet, so it needs this too.
if (this.options.injectStyles) {
Dropzone.injectStyles(this.options.injectStyles);
}

// If the browser failed, just call the fallback and leave
if (this.options.forceFallback || !Dropzone.isBrowserSupported()) {
return this.options.fallback.call(this);
Expand Down Expand Up @@ -2010,6 +2027,25 @@ export default class Dropzone extends Emitter {
return new Blob([ab], { type: mimeString });
}

// Adds the stylesheet to the document, at most once, however many dropzones
// ask for it. Prepended rather than appended so that a stylesheet the page
// already links wins on equal specificity -- injecting last would quietly
// override styling that used to work.
static injectStyles(which: DropzoneStyles = true): void {
if (typeof document === "undefined") return;
if (which === false) return;
if (document.querySelector("style[data-dropzone]")) return;

let variant = which === "basic" ? "basic" : "full";

let style = document.createElement("style");
// Names which one went in, so a second dropzone asking for the other can
// be seen to have been ignored rather than silently doubling up.
style.setAttribute("data-dropzone", variant);
style.textContent = variant === "basic" ? basicStyles : fullStyles;
document.head.insertBefore(style, document.head.firstChild);
}

// Creates an element from string
static createElement(string: string): HTMLElement {
let div = document.createElement("div");
Expand Down
45 changes: 44 additions & 1 deletion packages/dropzone/src/options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import Dropzone from "./dropzone";
import type { DropzoneAcceptCallback, DropzoneFile, DropzoneTransformCallback } from "./dropzone";
import type {
DropzoneAcceptCallback,
DropzoneFile,
DropzoneStyles,
DropzoneTransformCallback,
} from "./dropzone";
import defaultPreviewTemplate from "./preview-template.html?raw";

let defaultOptions = {
Expand Down Expand Up @@ -247,6 +252,44 @@ let defaultOptions = {
*/
autoQueue: true,

/**
* Adds Dropzone's stylesheet to the document itself, so there is no `<link>`
* to remember and no path to keep in step with the package.
*
* - `false` — add nothing. The default.
* - `true` or `"full"` — `dist/dropzone.css`, the ready-to-go styling.
* - `"basic"` — `dist/basic.css`, layout only, for styling it yourself.
*
* The two stylesheets are alternatives rather than layers: `basic` is not a
* subset of `full`, so picking one excludes the other.
*
* It is inserted once into `<head>` however many dropzones are on the page,
* and inserted *first*, so your own rules win on equal specificity without
* needing `!important`. If two dropzones ask for different ones, the first
* to be constructed wins.
*
* Defaults to false so that nothing changes for anyone on an upgrade.
*
* It is not the cheaper setting, though. The stylesheets travel inside the
* JavaScript whether or not this is switched on -- a runtime flag cannot be
* tree-shaken -- so importing the CSS through a bundler as well ships it
* twice. Turning this on is about 1.2 kB gzipped smaller than importing it.
*
* One thing a `<link>` does that this cannot: block the first paint. These
* styles arrive when the script does, so if the script is slow the browser
* can paint before them. Everything Dropzone creates is made after the
* styles go in and is never seen unstyled, and an empty dropzone element
* has nothing to show either way -- but your own markup inside it will be
* visible unstyled until then, and will shift when the styling lands. Link
* the stylesheet if that matters.
*
* ```js
* new Dropzone("#my-form", { url: "/upload", injectStyles: true });
* new Dropzone("#other", { url: "/upload", injectStyles: "basic" });
* ```
*/
injectStyles: false as DropzoneStyles,

/**
* If `true`, this will add a link to every file preview to remove or cancel (if
* already uploading) the file. The `dictCancelUpload`, `dictCancelUploadConfirmation`
Expand Down
7 changes: 7 additions & 0 deletions packages/dropzone/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ declare module "*.html?raw" {
export default content;
}

// ?inline hands back the processed stylesheet as a string, which is how the
// injectStyles option carries the CSS without importing it for its side effect.
declare module "*.css?inline" {
const content: string;
export default content;
}

// Optional globals. Neither is a dependency: Dropzone uses EXIF only when the
// page already loaded exif.js, and registers a jQuery plugin only when jQuery
// is present.
Expand Down
12 changes: 12 additions & 0 deletions packages/dropzone/test/test-sites/1-basic/inject_styles.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dropzone Test — injected styles</title>

<!-- Deliberately no stylesheet link: injectStyles is what has to provide it. -->
<script src="../dist/dropzone-min.js"></script>

<form class="dropzone" action="/"></form>

<script>
new Dropzone(".dropzone", { injectStyles: true });
</script>
12 changes: 12 additions & 0 deletions packages/dropzone/test/test-sites/1-basic/inject_styles_basic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dropzone Test — injected basic styles</title>

<!-- Deliberately no stylesheet link: injectStyles is what has to provide it. -->
<script src="../dist/dropzone-min.js"></script>

<form class="dropzone" action="/"></form>

<script>
new Dropzone(".dropzone", { injectStyles: "basic" });
</script>
Loading
Loading