-
-
Notifications
You must be signed in to change notification settings - Fork 432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Image Uploads #287
Open
atomdmac
wants to merge
26
commits into
lovasoa:master
Choose a base branch
from
atomdmac:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Image Uploads #287
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
714c73e
Add library multiparty to parse form data.
b166eb8
Use BoardDataList instead of plain object
8fa13cb
Return public interface for Sockets object
073ff19
Image asset uploads and updates.
b23bf11
Validate image size before allowing upload.
7abbda4
Get MIME type before returning image assets.
ff2768f
Remove old TODO
f278e68
Use XHR to upload images. Allows access to progress.
6b847cf
Move all image upload stuff to image tool
bf67be1
Use image tool to initiate upload
15781ee
Delete images if they are no longer in use.
f1cd94d
Drop onto board, not canvas.
daed5e3
Remove unnecessary console.log
962bb22
Use cell cursor for image tool.
9bf0e3f
Disable image upload if Image tool is in block list
5d82a78
Remove tmp file after image upload.
d09411b
Don't block main handler fn when serving board image assets.
84ff1e1
Validate image uploads on client and server.
1a815a6
Add log for when unused images are purged
c321964
Ensure that board image asset URLs always point to current domain.
1e3708e
Use fs.promises to read board image assets.
7fe1fe3
Use board name and asset ID to build image href
b0e47a1
Fix image purge.
231d3fd
Attach click and drag/drop handlers to canvas, not drawingArea
dfd96b1
Add tests for image upload tool.
05d0ea2
Merge branch 'master' into master
atomdmac File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,293 @@ | ||
|
||
/** | ||
* WHITEBOPHIR | ||
********************************************************* | ||
* @licstart The following is the entire license notice for the | ||
* JavaScript code in this page. | ||
* | ||
* Copyright (C) 2013 Ophir LOJKINE | ||
* | ||
* | ||
* The JavaScript code in this page is free software: you can | ||
* redistribute it and/or modify it under the terms of the GNU | ||
* General Public License (GNU GPL) as published by the Free Software | ||
* Foundation, either version 3 of the License, or (at your option) | ||
* any later version. The code is distributed WITHOUT ANY WARRANTY; | ||
* without even the implied warranty of MERCHANTABILITY or FITNESS | ||
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. | ||
* | ||
* As additional permission under GNU GPL version 3 section 7, you | ||
* may distribute non-source (e.g., minimized or compacted) forms of | ||
* that code without the copy of the GNU GPL normally required by | ||
* section 4, provided you include this license notice and a URL | ||
* through which recipients can access the Corresponding Source. | ||
* | ||
* @licend | ||
*/ | ||
|
||
(function () { //Code isolation | ||
const drawingArea = document.getElementById('board'); | ||
const canvas = document.getElementById('canvas'); | ||
const newImageDropPoint = { | ||
x: 0, | ||
y: 0, | ||
}; | ||
|
||
const fileInput = document.createElement('input'); | ||
fileInput.type = 'file'; | ||
fileInput.accept = 'image/*'; | ||
|
||
function onFileInputChange(event) { | ||
uploadImage(event.target.files[0], { | ||
x: newImageDropPoint.x, | ||
y: newImageDropPoint.y, | ||
}); | ||
} | ||
|
||
function promptForImage(x, y, event) { | ||
// Get the position of the click on the canvas so when the user uploads | ||
// an image, we can draw it at the same position. | ||
newImageDropPoint.x = x; | ||
newImageDropPoint.y = y; | ||
fileInput.click(); | ||
} | ||
|
||
function draw(data) { | ||
Tools.drawingEvent = true; | ||
switch (data.type) { | ||
case "image": | ||
createImageElement(data); | ||
break; | ||
case "update": | ||
var image = svg.getElementById(data['id']); | ||
if (!image) { | ||
console.error("Image: No image provided!", data['id']); | ||
} | ||
updateImageElement(image, data); | ||
break; | ||
default: | ||
console.error("Image: Draw instruction with unknown type. ", data); | ||
break; | ||
} | ||
} | ||
|
||
/** | ||
* Gets the absolute URL of a relative URL. Ensures that the URL points to | ||
* the same origin as the current page. | ||
*/ | ||
function getAbsoluteImageUrl(relativeUrl) { | ||
const normalizedUrl = relativeUrl.startsWith('/') ? relativeUrl : `/${relativeUrl}`; | ||
return `${window.location.origin}${normalizedUrl}`; | ||
} | ||
|
||
var svg = Tools.svg; | ||
/** | ||
* Creates a new image element on the canvas, or updates an existing image | ||
* with new information. | ||
* @param {Object} data - The data to use to create the image. | ||
*/ | ||
function createImageElement(data) { | ||
var img = svg.getElementById(data.id) || Tools.createSVGElement("image"); | ||
img.setAttribute("id", data.id); | ||
img.setAttribute("href", getAbsoluteImageUrl(data.src)); | ||
img.setAttribute("x", data.x); | ||
img.setAttribute("y", data.y); | ||
|
||
updateImageElement(img, data); | ||
Tools.drawingArea.appendChild(img); | ||
return img; | ||
} | ||
|
||
/** | ||
* Updates the image element with new data. | ||
*/ | ||
function updateImageElement(imageElement, data) { | ||
imageElement.x.baseVal.value = Math.min(data['x2'], data['x']); | ||
imageElement.y.baseVal.value = Math.min(data['y2'], data['y']); | ||
imageElement.width.baseVal.value = Math.abs(data['x2'] - data['x']); | ||
imageElement.height.baseVal.value = Math.abs(data['y2'] - data['y']); | ||
} | ||
|
||
/** | ||
* Get the name of the current board based on the current URL. | ||
* @returns {string} - The name of the current board. | ||
*/ | ||
function getCurrentBoardName() { | ||
let boardName = window.location.pathname.split('/'); | ||
boardName = boardName[boardName.length - 1]; | ||
boardName = boardName.split('#').shift(); | ||
return boardName; | ||
} | ||
|
||
/** | ||
* Loads the image from the filesystem to generate a preview while uploading | ||
* occurs as well as to get the dimensions of the image. | ||
* @param {File} image - The image to preview. | ||
* @returns {Promise} - A promise that resolves with the image preview. | ||
*/ | ||
async function previewImage(image) { | ||
return new Promise((resolve, reject) => { | ||
const reader = new FileReader(); | ||
reader.onload = function (event) { | ||
const img = new Image(); | ||
img.src = event.target.result; | ||
img.onload = function () { | ||
resolve(img); | ||
} | ||
img.onerror = function (error) { | ||
reject(error); | ||
} | ||
} | ||
reader.readAsDataURL(image); | ||
}); | ||
} | ||
|
||
/** | ||
* Uploads an image to the server, draws it on the canvas optimistically. | ||
* @param {File} image - The image to upload. | ||
* @param {Object} position - The position to draw the image on the canvas. | ||
* @param {number} position.x - The x coordinate of the image. | ||
* @param {number} position.y - The y coordinate of the image. | ||
* @returns {Promise} - A promise that resolves when the image has been uploaded. | ||
*/ | ||
async function uploadImage(image, position) { | ||
const id = Tools.generateUID(); | ||
const ImageTool = Tools.list["Image"]; | ||
|
||
// Get a preview of the image | ||
const previewElement = await previewImage(image); | ||
|
||
const dimensions = { | ||
x: previewElement.width, | ||
y: previewElement.height | ||
}; | ||
|
||
// Optimistically draw the image on the canvas before uploading. | ||
ImageTool.draw({ | ||
id, | ||
type: "image", | ||
src: previewElement.src, | ||
opacity: 0.5, | ||
x: position.x, | ||
y: position.y, | ||
x2: position.x + dimensions.x, | ||
y2: position.y + dimensions.y, | ||
}); | ||
|
||
// Upload the image to the server | ||
const formData = new FormData(); | ||
formData.append('image', image); | ||
formData.append('id', id); | ||
formData.append('position', JSON.stringify(position)); | ||
formData.append('dimensions', JSON.stringify(dimensions)); | ||
|
||
return new Promise((resolve, reject) => { | ||
const xhr = new XMLHttpRequest(); | ||
|
||
function onError(error) { | ||
alert('An error occurred while attempti') | ||
console.log('error: ', error); | ||
reject(error); | ||
} | ||
|
||
function onProgress(event) { | ||
// TODO: Show a loading indicator while the image is uploading. | ||
console.log('progress: ', event); | ||
} | ||
|
||
function onLoad(response) { | ||
if (xhr.status >= 400) { | ||
alert('A server error occurred while uploading the image.') | ||
reject(response); | ||
console.log('onLoad: ', response); | ||
} | ||
resolve(response); | ||
} | ||
|
||
xhr.open('POST', `/image-upload/${getCurrentBoardName()}`, true); | ||
xhr.onerror = onError; | ||
xhr.onprogress = onProgress; | ||
xhr.onload = onLoad; | ||
xhr.send(formData); | ||
}); | ||
} | ||
|
||
/** | ||
* Handles the drop event on the canvas. | ||
* @param {Event} event - The drop event. | ||
*/ | ||
function onUploadEvent(event) { | ||
const scale = Tools.getScale(); | ||
const position = { | ||
x: event.clientX / scale, | ||
y: event.clientY / scale | ||
}; | ||
uploadImage(event.dataTransfer.files[0], position); | ||
} | ||
|
||
function checkFileIsImage(file) { | ||
return file.type.startsWith('image/'); | ||
} | ||
|
||
function onDrop(event) { | ||
if (!checkFileIsImage(event.dataTransfer.files[0])) { | ||
alert('File type not supported.'); | ||
return; | ||
} | ||
onUploadEvent(event); | ||
} | ||
|
||
/** | ||
* Called when the tool is selected. | ||
*/ | ||
function onStart() { | ||
fileInput.addEventListener('change', onFileInputChange); | ||
} | ||
|
||
/** | ||
* Called when the tool is deselected. | ||
*/ | ||
function onQuit() { | ||
fileInput.removeEventListener('change', onFileInputChange); | ||
} | ||
|
||
// List of all drag/drop events. | ||
const events = [ | ||
"drag", | ||
"dragend", | ||
"dragenter", | ||
"dragleave", | ||
"dragover", | ||
"dragstart", | ||
"drop" | ||
]; | ||
|
||
// Ignore all default handling of drag/drop events on the canvas. | ||
function preventDefault(e) { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
} | ||
|
||
events.forEach((eventName) => { | ||
drawingArea.addEventListener(eventName, preventDefault, false); | ||
}); | ||
|
||
drawingArea.addEventListener("drop", onDrop, false); | ||
|
||
var imageTool = { | ||
"name": "Image", | ||
"shortcut": "i", | ||
"listeners": { | ||
"press": promptForImage, | ||
}, | ||
"onstart": onStart, | ||
"onquit": onQuit, | ||
"secondary": null, | ||
"draw": draw, | ||
"mouseCursor": "cell", | ||
"icon": "tools/image/icon.svg", | ||
"stylesheet": "" | ||
}; | ||
Tools.add(imageTool); | ||
|
||
})(); //End of code isolation |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is complicated. Can we remove the src altogether ?
that would stress me less :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would the browser know where to pull the image to display from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The browser would make a request to
boards/{boardname}/images/{image_id}
, the server would check that the image exists, and if so, serve it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yours is a much better approach 😅 This has been updated.