Published
Edited
Apr 9, 2022
Importers
2 stars
Insert cell
Insert cell
viewof value = fileInput({
title: "Upload a PNG",
validate: (file) => file.name.endsWith(".png"),
// process: (file) => file.name,
readAs: "localFile"
})
Insert cell
value.image({ width: 300 })
Insert cell
viewof value2 = fileInput({
title: "Upload anything!",
readAs: "readAsArrayBuffer"
})
Insert cell
value2
Insert cell
viewof value3 = fileInput({
title: "Upload multiple!",
multiple: true,
readAs: "readAsText"
})
Insert cell
function fileInput({
title,
validate,
process,
multiple = false,
// pass null to receive only the file
// pass the special value "localFile" to read as a LocalFile instance
readAs = "readAsText"
}) {
const id = DOM.uid().id;
const urls = [];

const area = html`<div id=${id} tabindex=-1>`;
area.ondragover = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
};

const input = html`<input type="file">`;
input.multiple = multiple;

const fileLabel = html`<span hidden>`;
area.appendChild(fileLabel);

const button = html`<button>click here to select a file`;
button.onmousedown = () => {
setTimeout(() => area.blur());
};
button.onclick = () => {
input.click();
};
area.appendChild(button);

let timeout;
let resolve;

async function updateValue(files) {
clearTimeout(timeout);
let file, value;
if (multiple) {
file = 0;
value = [];
}

for (let item of files) {
try {
if (item && (!validate || validate(item))) {
file = multiple ? file + 1 : item;
let processed;
if (readAs) {
const content = await readFile(item, readAs);
processed = process ? process(content, item) : content;
} else {
processed = process ? process(item) : item;
}

if (multiple) {
value.push(processed);
} else {
value = processed;
break;
}
}
} catch (_) {
file = null;
}
}

if (file) {
input.value = null;
if (resolve) resolve(value);
else view.value = value;

view.dispatchEvent(new CustomEvent("input"));
area.blur();
area.classList.remove("invalid");
area.classList.add("valid");
button.textContent = "Select another";
fileLabel.textContent = multiple
? file + " file" + (file === 1 ? "" : "s")
: file.name;
fileLabel.hidden = false;
} else {
// waits forever
view.value = new Promise((res) => {
resolve = res;
});
view.dispatchEvent(new CustomEvent("input"));
area.classList.add("invalid");
timeout = setTimeout(() => area.classList.remove("invalid"), 1500);
}
}

area.ondrop = (e) => {
e.preventDefault();
area.classList.remove("dragging");
updateValue(e.dataTransfer.files);
};
area.ondragenter = (e) => {
area.classList.add("dragging");
};
area.ondragleave = (e) => {
area.classList.remove("dragging");
};
area.onpaste = (e) => {
updateValue(e.clipboardData.files);
};
input.onchange = () => {
updateValue(input.files);
};

const view = html`<div style="padding: 0.1px">${
title ? md`### ${title}` : ""
}${area}<style>${style(id)}</style></div>`;
return view;
}
Insert cell
readFile = (file, readAs = "readAsText") =>
new Promise((resolve, reject) => {
if (readAs === "localFile") {
resolve(new LocalFile(file));
} else {
var reader = new FileReader();
reader.addEventListener("loadend", function () {
resolve(reader.result);
});
reader.addEventListener("error", reject);

reader[readAs](file);
}
})
Insert cell
style = (id) => `
@keyframes ${id}-flash {
0% { opacity: 0 }
}

@keyframes ${id}-wiggle {
${(100 / 6) * 0}% { left: 0 }
${(100 / 6) * 1}% { left: 0.75em }
${(100 / 6) * 2}% { left: 0 }
${(100 / 6) * 3}% { left: -0.75em }
${(100 / 6) * 4}% { left: 0 }
${(100 / 6) * 5}% { left: 0.75em }
${(100 / 6) * 6}% { left: 0 }
}

#${id} {
--color: hsl(150,0%,45%);
--bg: hsla(150,0%,60%,20%);
width: 100%;
height: 140px;
max-width: 360px;
box-sizing: border-box;
padding: 1em;
margin: 0.5em 0;
background: repeating-linear-gradient(-45deg, transparent 10px 30px, var(--bg) 30px 50px);
border: 4px solid var(--color);
border-radius: 1vmin;
outline: none;
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: column;
position: relative;
}
#${id}.valid:not(.invalid):not(:focus):not(.dragging) {
background: var(--bg);
border-width: 2px;
padding: calc(1em + 2px);
}
#${id}.valid { --color: hsl(140, 53%, 40%); --bg: hsla(140, 53%, 40%, 15%); }
#${id}:focus, #${id}.dragging, #${id}:not(.invalid) button { --color: hsl(224, 53%, 49%); --bg: hsla(224, 53%, 49%, 15%); }
#${id}.invalid { --color: hsl(0, 53%, 49%); --bg: hsla(0, 53%, 49%, 15%); }
#${id}.dragging { background: var(--bg); }

#${id}.valid::before { content: "file selected:" }
#${id}:focus::before { content: "Paste away! or" }
#${id}.invalid::before { content: "Invalid" }

#${id}::before {
content: "drag and drop, paste, or";
display: inline-block;
vertical-align: middle;
text-align: center;
font-size: 1.1em;
font-weight: bold;
position: relative;
font-family: sans-serif;
color: var(--color);
white-space: pre-wrap;
text-transform: uppercase;
}
#${id}.dragging::before {
margin: auto;
content: "drop here";
font-size: 2em;
opacity: 0.6;
}

#${id}.invalid {
animation: 0.3s ease-out ${id}-wiggle;
content: attr(data-error-message);
}

#${id} button {
cursor: pointer;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: hsl(224, 43%, 80%);
color: hsl(224, 60%, 40%);
margin: 0;
padding: .2em .5em;
border-radius: 10px;
border: none;
font-size: 1.2em;
text-transform: uppercase;
font-weight: bold;
}
#${id}:focus button {
background: hsl(224, 43%, 80%, 75%);
}
#${id}.valid:not(:focus) button {
background: transparent;
color: var(--color);
font-size: 1em;
}
#${id}.dragging button, #${id}.dragging span {
display: none;
}

#${id} span {
color: var(--color);
font-size: 0.9em;
text-align: center;
font-weight: bold;
font-family: sans-serif;
}
#${id}:focus span,
#${id}.invalid span {
visibility: hidden
}
`
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more