Browse Source

Initial implementation

master
David Larlet 4 years ago
commit
f58a9f889f
No known key found for this signature in database
7 changed files with 2231 additions and 0 deletions
  1. 6
    0
      css/custom.css
  2. 62
    0
      css/upload_images.css
  3. 61
    0
      index.html
  4. 1960
    0
      js/stimulus.js
  5. 113
    0
      js/upload_images_controller.js
  6. 1
    0
      requirements.txt
  7. 28
    0
      server.py

+ 6
- 0
css/custom.css View File

@@ -0,0 +1,6 @@
form {
max-width: 40rem;
margin: 2rem auto;
font-size: 2rem;
text-align: center;
}

+ 62
- 0
css/upload_images.css View File

@@ -0,0 +1,62 @@
[data-target="upload-images.input"] {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
[data-target="upload-images.input"] + label {
font-size: 1.25em;
font-weight: 700;
color: white;
background-color: black;
display: inline-block;
padding: 1rem;
}
[data-target="upload-images.input"]:focus + label,
[data-target="upload-images.input"] + label:hover {
background-color: red;
}
[data-target="upload-images.input"] + label {
cursor: pointer; /* "hand" cursor */
}
[data-target="upload-images.input"]:focus + label {
outline: thin dotted;
outline: -webkit-focus-ring-color auto 5px;
}
[data-target="upload-images.input"]:focus-within + label {
outline: thin dotted;
outline: -webkit-focus-ring-color auto 5px;
}
[data-target="upload-images.input"] + label * {
pointer-events: none;
}

[data-target="upload-images.output"] img {
max-width: 30rem;
}
[data-target="upload-images.output"].oversized {
background: red;
height: 4rem;
padding: 1rem 2rem;
}

[data-target="upload-images.dropzone"] {
border: 5px solid black;
margin: 2rem auto;
min-width: 30rem;
}
[data-target="upload-images.dropzone"].hidden {
display: none;
}
[data-target="upload-images.dropzone"]:hover,
[data-target="upload-images.dropzone"].active {
border-color: red;
cursor: pointer; /* "hand" cursor */
}

[data-target="upload-images.submit"] {
width: 30rem;
font-size: 2rem;
}

+ 61
- 0
index.html View File

@@ -0,0 +1,61 @@
<!doctype html><!-- This is a valid HTML5 document. -->
<!-- Screen readers, SEO, extensions and so on. -->
<html lang=en>
<!-- Has to be within the first 1024 bytes, hence before the <title>
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
<meta charset=utf-8>
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
<!-- The viewport meta is quite crowded and we are responsible for that.
See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
<meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
<!-- Avoid indexation of the content as it is private. -->
<meta name="robots" content="noindex, nofollow">
<!-- Prevent access via referrers, only the domain will be available. -->
<meta content="origin-when-cross-origin" name="referrer">
<!-- Required to make a valid HTML5 document. -->
<title>Upload images form</title>
<!-- Lightest blank gif, avoids an extra query to the server. -->
<link rel=icon href="data:;base64,iVBORw0KGgo=">

<link rel="stylesheet" href="/static/css/upload_images.css">
<link rel="stylesheet" href="/static/css/custom.css">

<script src="/static/js/stimulus.js"></script>
<script type="text/javascript">
// In your real world application, you probably do not want to do that.
;(() => window.application = Stimulus.Application.start())()
</script>
<script src="/static/js/upload_images_controller.js"></script>

<form
action="/upload_images" method="post" enctype="multipart/form-data"
data-controller="upload-images"
data-upload-images-max-size-kb="500">
<fieldset>
<input
type="file" name="images" id="images"
accept="image/*" multiple
data-target="upload-images.input"
data-action="upload-images#handleImages">
<label for="images"
data-target="upload-images.label">
Click to upload image(s) or drag & drop below
</label>
<output data-target="upload-images.output"></output>
<canvas
data-target="upload-images.dropzone"
data-action="click->upload-images#openFileSelector
drop->upload-images#handleImages
dragenter->upload-images#addActiveClass
dragover->upload-images#addActiveClass
focus->upload-images#addActiveClass
dragleave->upload-images#removeActiveClass
blur->upload-images#removeActiveClass"
></canvas>
</fieldset>
<input type="submit" name="submit"
data-target="upload-images.submit"
data-action="upload-images#submit">
</form>

</html>

+ 1960
- 0
js/stimulus.js
File diff suppressed because it is too large
View File


+ 113
- 0
js/upload_images_controller.js View File

@@ -0,0 +1,113 @@
/*
Resources:
- https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications
- https://tympanus.net/codrops/2015/09/15/styling-customizing-file-inputs-smart-way/
- https://eloquentjavascript.net/18_http.html
*/

application.register(
'upload-images',
class extends Stimulus.Controller {
static get targets() {
return ['input', 'output', 'dropzone']
}

openFileSelector() {
this.inputTarget.click()
}
addActiveClass(event) {
event.preventDefault()
this.dropzoneTarget.classList.add('active')
}
removeActiveClass() {
this.dropzoneTarget.classList.remove('active')
}

handleImages(event) {
event.preventDefault()
let images = []
if (event.type == 'change') {
images = event.target.files
} else if (event.type == 'drop') {
images = event.dataTransfer.files
}
Array.from(images).forEach(image => {
const imageName = image.name
const imageSize = image.size
const isOversized =
Math.round(imageSize / 1024) >
Number(this.data.get('maxSizeKb'))

if (isOversized) {
this.outputTarget.classList.add('oversized')
this.outputTarget.innerText = `Oversized!`
this.inputTarget.value = ''
return
} else {
this.outputTarget.classList.remove('oversized')
this.dropzoneTarget.classList.add('hidden')
}

const figure = document.createElement('figure')

const img = document.createElement('img')
img.file = image // Required for future upload, fragile?
img.src = window.URL.createObjectURL(image)
img.onload = event =>
window.URL.revokeObjectURL(event.target.src)
img.alt = 'Picture preview'
figure.appendChild(img)

const figcaption = document.createElement('figcaption')
figcaption.innerText = imageName
figure.appendChild(figcaption)

this.outputTarget.appendChild(figure)
})
}

submit(event) {
event.preventDefault()
const images = this.outputTarget.querySelectorAll('img')
Array.from(images).forEach(image => {
const formdata = new FormData()
formdata.append('images', image.file)
const options = {
method: this.element.method,
body: formdata
}
console.log(`Uploading "${image.file.name}"`)
this._request(
this.element.action,
options,
this._displayPercentage
)
.then(response => console.log('Uploaded!'))
.catch(console.log.bind(console))
})
}

_request(url, options = {}, onProgress) {
// See https://github.com/github/fetch/issues/89#issuecomment-256610849
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(options.method || 'get', url)
for (let header in options.headers || {})
xhr.setRequestHeader(header, options.headers[header])
xhr.onload = event => resolve(event.target.responseText)
xhr.onerror = reject
if (xhr.upload && onProgress) xhr.upload.onprogress = onProgress
xhr.send(options.body)
})
}

_displayPercentage(event) {
if (event.lengthComputable) {
const percentage = Math.round(
(event.loaded * 100) / event.total
)
console.log(percentage)
}
}
}
)

+ 1
- 0
requirements.txt View File

@@ -0,0 +1 @@
roll==0.11.0

+ 28
- 0
server.py View File

@@ -0,0 +1,28 @@
from http import HTTPStatus
from pathlib import Path

from roll import Roll
from roll.extensions import simple_server, static

app = Roll()
static(app)


@app.route("/")
async def home(request, response):
response.headers["Content-Type"] = "text/html; charset=utf-8"
response.body = open("index.html").read()


@app.route("/upload_images", methods=["POST"])
async def upload_images(request, response):
images = request.files.list("images")
for image in images:
filepath = Path("uploaded_images") / image.filename
filepath.write_bytes(image.read())
response.status = HTTPStatus.FOUND
response.headers["Location"] = "/"


if __name__ == "__main__":
simple_server(app)

Loading…
Cancel
Save