hedgedoc/public/vendor/inlineAttachment/inline-attachment.js
2015-07-02 00:10:20 +08:00

435 lines
No EOL
12 KiB
JavaScript
Executable file

/*jslint newcap: true */
/*global XMLHttpRequest: false, FormData: false */
/*
* Inline Text Attachment
*
* Author: Roy van Kaathoven
* Contact: ik@royvankaathoven.nl
*/
(function(document, window) {
'use strict';
var inlineAttachment = function(options, instance) {
this.settings = inlineAttachment.util.merge(options, inlineAttachment.defaults);
this.editor = instance;
this.filenameTag = '{filename}';
this.lastValue = null;
};
/**
* Will holds the available editors
*
* @type {Object}
*/
inlineAttachment.editors = {};
/**
* Utility functions
*/
inlineAttachment.util = {
/**
* Simple function to merge the given objects
*
* @param {Object[]} object Multiple object parameters
* @returns {Object}
*/
merge: function() {
var result = {};
for (var i = arguments.length - 1; i >= 0; i--) {
var obj = arguments[i];
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
result[k] = obj[k];
}
}
}
return result;
},
/**
* Append a line of text at the bottom, ensuring there aren't unnecessary newlines
*
* @param {String} appended Current content
* @param {String} previous Value which should be appended after the current content
*/
appendInItsOwnLine: function(previous, appended) {
return (previous + "\n\n[[D]]" + appended)
.replace(/(\n{2,})\[\[D\]\]/, "\n\n")
.replace(/^(\n*)/, "");
},
/**
* Inserts the given value at the current cursor position of the textarea element
*
* @param {HtmlElement} el
* @param {String} value Text which will be inserted at the cursor position
*/
insertTextAtCursor: function(el, text) {
var scrollPos = el.scrollTop,
strPos = 0,
browser = false,
range;
if ((el.selectionStart || el.selectionStart === '0')) {
browser = "ff";
} else if (document.selection) {
browser = "ie";
}
if (browser === "ie") {
el.focus();
range = document.selection.createRange();
range.moveStart('character', -el.value.length);
strPos = range.text.length;
} else if (browser === "ff") {
strPos = el.selectionStart;
}
var front = (el.value).substring(0, strPos);
var back = (el.value).substring(strPos, el.value.length);
el.value = front + text + back;
strPos = strPos + text.length;
if (browser === "ie") {
el.focus();
range = document.selection.createRange();
range.moveStart('character', -el.value.length);
range.moveStart('character', strPos);
range.moveEnd('character', 0);
range.select();
} else if (browser === "ff") {
el.selectionStart = strPos;
el.selectionEnd = strPos;
el.focus();
}
el.scrollTop = scrollPos;
}
};
/**
* Default configuration options
*
* @type {Object}
*/
inlineAttachment.defaults = {
/**
* URL where the file will be send
*/
uploadUrl: 'uploadimage',
/**
* Which method will be used to send the file to the upload URL
*/
uploadMethod: 'POST',
/**
* Name in which the file will be placed
*/
uploadFieldName: 'image',
/**
* Extension which will be used when a file extension could not
* be detected
*/
defualtExtension: 'png',
/**
* JSON field which refers to the uploaded file URL
*/
jsonFieldName: 'link',
/**
* Allowed MIME types
*/
allowedTypes: [
'image/jpeg',
'image/png',
'image/jpg',
'image/gif'
],
/**
* Text which will be inserted when dropping or pasting a file.
* Acts as a placeholder which will be replaced when the file is done with uploading
*/
progressText: '![Uploading file...{filename}]()',
/**
* When a file has successfully been uploaded the progressText
* will be replaced by the urlText, the {filename} tag will be replaced
* by the filename that has been returned by the server
*/
urlText: "![]({filename})",
/**
* Text which will be used when uploading has failed
*/
errorText: "Error uploading file",
/**
* Extra parameters which will be send when uploading a file
*/
extraParams: {},
/**
* Extra headers which will be send when uploading a file
*/
extraHeaders: {},
/**
* Before the file is send
*/
beforeFileUpload: function() {
return true;
},
/**
* Triggers when a file is dropped or pasted
*/
onFileReceived: function() {},
/**
* Custom upload handler
*
* @return {Boolean} when false is returned it will prevent default upload behavior
*/
onFileUploadResponse: function() {
return true;
},
/**
* Custom error handler. Runs after removing the placeholder text and before the alert().
* Return false from this function to prevent the alert dialog.
*
* @return {Boolean} when false is returned it will prevent default error behavior
*/
onFileUploadError: function() {
return true;
},
/**
* When a file has succesfully been uploaded
*/
onFileUploaded: function() {}
};
/**
* Uploads the blob
*
* @param {Blob} file blob data received from event.dataTransfer object
* @return {XMLHttpRequest} request object which sends the file
*/
inlineAttachment.prototype.uploadFile = function(file, id) {
var me = this,
formData = new FormData(),
xhr = new XMLHttpRequest(),
id = id,
settings = this.settings,
extension = settings.defualtExtension;
if (typeof settings.setupFormData === 'function') {
settings.setupFormData(formData, file);
}
// Attach the file. If coming from clipboard, add a default filename (only works in Chrome for now)
// http://stackoverflow.com/questions/6664967/how-to-give-a-blob-uploaded-as-formdata-a-file-name
if (file.name) {
var fileNameMatches = file.name.match(/\.(.+)$/);
if (fileNameMatches) {
extension = fileNameMatches[1];
}
}
var remoteFilename = "image-" + Date.now() + "." + extension;
if (typeof settings.remoteFilename === 'function') {
remoteFilename = settings.remoteFilename(file);
}
formData.append(settings.uploadFieldName, file, remoteFilename);
// Append the extra parameters to the formdata
if (typeof settings.extraParams === "object") {
for (var key in settings.extraParams) {
if (settings.extraParams.hasOwnProperty(key)) {
formData.append(key, settings.extraParams[key]);
}
}
}
xhr.open('POST', settings.uploadUrl);
// Add any available extra headers
if (typeof settings.extraHeaders === "object") {
for (var header in settings.extraHeaders) {
if (settings.extraHeaders.hasOwnProperty(header)) {
xhr.setRequestHeader(header, settings.extraHeaders[header]);
}
}
}
xhr.onload = function() {
// If HTTP status is OK or Created
if (xhr.status === 200 || xhr.status === 201) {
me.onFileUploadResponse(xhr, id);
} else {
me.onFileUploadError(xhr, id);
}
};
if (settings.beforeFileUpload(xhr) !== false) {
xhr.send(formData);
}
return xhr;
};
/**
* Returns if the given file is allowed to handle
*
* @param {File} clipboard data file
*/
inlineAttachment.prototype.isFileAllowed = function(file) {
if (this.settings.allowedTypes.indexOf('*') === 0){
return true;
} else {
return this.settings.allowedTypes.indexOf(file.type) >= 0;
}
};
/**
* Handles upload response
*
* @param {XMLHttpRequest} xhr
* @return {Void}
*/
inlineAttachment.prototype.onFileUploadResponse = function(xhr, id) {
if (this.settings.onFileUploadResponse.call(this, xhr) !== false) {
var result = JSON.parse(xhr.responseText),
filename = result[this.settings.jsonFieldName];
if (result && filename) {
var replacements = [];
var string = this.settings.progressText.replace(this.filenameTag, id);
var lines = this.editor.getValue().split('\n');
var newValue = this.settings.urlText.replace(this.filenameTag, filename);
for(var i = 0; i < lines.length; i++) {
var ch = lines[i].indexOf(string);
if(ch != -1)
replacements.push({replacement:newValue, from:{line:i, ch:ch}, to:{line:i, ch:ch + string.length}});
}
for(var i = 0; i < replacements.length; i++)
this.editor.replaceRange(replacements[i]);
}
}
};
/**
* Called when a file has failed to upload
*
* @param {XMLHttpRequest} xhr
* @return {Void}
*/
inlineAttachment.prototype.onFileUploadError = function(xhr, id) {
if (this.settings.onFileUploadError.call(this, xhr) !== false) {
var replacements = [];
var string = this.settings.progressText.replace(this.filenameTag, id);
var lines = this.editor.getValue().split('\n');
for(var i = 0; i < lines.length; i++) {
var ch = lines[i].indexOf(this.lastValue);
if(ch != -1)
replacements.push({replacement:"", from:{line:i, ch:ch}, to:{line:i, ch:ch + string.length}});
}
for(var i = 0; i < replacements.length; i++)
this.editor.replaceRange(replacements[i]);
}
};
/**
* Called when a file has been inserted, either by drop or paste
*
* @param {File} file
* @return {Void}
*/
inlineAttachment.prototype.onFileInserted = function(file, id) {
if (this.settings.onFileReceived.call(this, file) !== false) {
this.lastValue = this.settings.progressText.replace(this.filenameTag, id);
this.editor.insertValue(this.lastValue + "\n");
}
};
/**
* Called when a paste event occured
* @param {Event} e
* @return {Boolean} if the event was handled
*/
inlineAttachment.prototype.onPaste = function(e) {
var result = false,
clipboardData = e.clipboardData,
items;
if (typeof clipboardData === "object") {
items = clipboardData.items || clipboardData.files || [];
for (var i = 0; i < items.length; i++) {
var item = items[i];
if (this.isFileAllowed(item)) {
result = true;
var id = ID();
this.onFileInserted(item.getAsFile(), id);
this.uploadFile(item.getAsFile(), id);
}
}
}
if (result) { e.preventDefault(); }
return result;
};
/**
* Called when a drop event occures
* @param {Event} e
* @return {Boolean} if the event was handled
*/
inlineAttachment.prototype.onDrop = function(e) {
var result = false;
for (var i = 0; i < e.dataTransfer.files.length; i++) {
var file = e.dataTransfer.files[i];
if (this.isFileAllowed(file)) {
result = true;
var id = ID();
this.onFileInserted(file, id);
this.uploadFile(file, id);
}
}
return result;
};
window.inlineAttachment = inlineAttachment;
})(document, window);
// Generate unique IDs for use as pseudo-private/protected names.
// Similar in concept to
// <http://wiki.ecmascript.org/doku.php?id=strawman:names>.
//
// The goals of this function are twofold:
//
// * Provide a way to generate a string guaranteed to be unique when compared
// to other strings generated by this function.
// * Make the string complex enough that it is highly unlikely to be
// accidentally duplicated by hand (this is key if you're using `ID`
// as a private/protected name on an object).
//
// Use:
//
// var privateName = ID();
// var o = { 'public': 'foo' };
// o[privateName] = 'bar';
var ID = function () {
// Math.random should be unique because of its seeding algorithm.
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
// after the decimal.
return '_' + Math.random().toString(36).substr(2, 9);
};