egroupware/phpgwapi/js/ckeditor/plugins/filetools/plugin.js

764 lines
24 KiB
JavaScript
Raw Normal View History

/**
* @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or http://ckeditor.com/license
*/
'use strict';
( function() {
CKEDITOR.plugins.add( 'filetools', {
lang: 'cs,da,de,en,eo,fr,gl,it,ko,nb,nl,pl,pt-br,ru,sv,tr,zh,zh-cn', // %REMOVE_LINE_CORE%
beforeInit: function( editor ) {
/**
* An instance of the {@link CKEDITOR.fileTools.uploadRepository upload repository}.
* It allows you to create and get {@link CKEDITOR.fileTools.fileLoader file loaders}.
*
* var loader = editor.uploadRepository.create( file );
* loader.loadAndUpload( 'http://foo/bar' );
*
* @since 4.5
* @readonly
* @property {CKEDITOR.fileTools.uploadRepository} uploadRepository
* @member CKEDITOR.editor
*/
editor.uploadRepository = new UploadRepository( editor );
/**
* Event fired when the {@link CKEDITOR.fileTools.fileLoader file loader} should send XHR. If the event is not
* {@link CKEDITOR.eventInfo#stop stopped} or {@link CKEDITOR.eventInfo#cancel canceled}, the default request
* will be sent. To learn more refer to the [Uploading Dropped or Pasted Files](#!/guide/dev_file_upload) article.
*
* @since 4.5
* @event fileUploadRequest
* @member CKEDITOR.editor
* @param data
* @param {CKEDITOR.fileTools.fileLoader} data.fileLoader File loader instance.
*/
editor.on( 'fileUploadRequest', function( evt ) {
var fileLoader = evt.data.fileLoader;
fileLoader.xhr.open( 'POST', fileLoader.uploadUrl, true );
}, null, null, 5 );
editor.on( 'fileUploadRequest', function( evt ) {
var fileLoader = evt.data.fileLoader,
formData = new FormData();
formData.append( 'upload', fileLoader.file, fileLoader.fileName );
fileLoader.xhr.send( formData );
}, null, null, 999 );
/**
* Event fired when the {CKEDITOR.fileTools.fileLoader file upload} response is received and needs to be parsed.
* If the event is not {@link CKEDITOR.eventInfo#stop stopped} or {@link CKEDITOR.eventInfo#cancel canceled},
* the default response handler will be used. To learn more refer to the
* [Uploading Dropped or Pasted Files](#!/guide/dev_file_upload) article.
*
* @since 4.5
* @event fileUploadResponse
* @member CKEDITOR.editor
* @param data
* @param {CKEDITOR.fileTools.fileLoader} data.fileLoader A file loader instance.
* @param {String} data.message The message from the server. Needs to be set in the listener — see the example above.
* @param {String} data.fileName The file name on server. Needs to be set in the listener — see the example above.
* @param {String} data.url The URL to the uploaded file. Needs to be set in the listener — see the example above.
*/
editor.on( 'fileUploadResponse', function( evt ) {
var fileLoader = evt.data.fileLoader,
xhr = fileLoader.xhr,
data = evt.data;
try {
var response = JSON.parse( xhr.responseText );
// Error message does not need to mean that upload finished unsuccessfully.
// It could mean that ex. file name was changes during upload due to naming collision.
if ( response.error && response.error.message ) {
data.message = response.error.message;
}
// But !uploaded means error.
if ( !response.uploaded ) {
evt.cancel();
} else {
data.fileName = response.fileName;
data.url = response.url;
}
} catch ( err ) {
// Response parsing error.
data.message = fileLoader.lang.filetools.responseError;
window.console && window.console.log( xhr.responseText );
evt.cancel();
}
}, null, null, 999 );
}
} );
/**
* File loader repository. It allows you to create and get {@link CKEDITOR.fileTools.fileLoader file loaders}.
*
* An instance of the repository is available as the {@link CKEDITOR.editor#uploadRepository}.
*
* var loader = editor.uploadRepository.create( file );
* loader.loadAndUpload( 'http://foo/bar' );
*
* To find more information about handling files see the {@link CKEDITOR.fileTools.fileLoader} class.
*
* @since 4.5
* @class CKEDITOR.fileTools.uploadRepository
* @mixins CKEDITOR.event
* @constructor Creates an instance of the repository.
* @param {CKEDITOR.editor} editor Editor instance. Used only to get the language data.
*/
function UploadRepository( editor ) {
this.editor = editor;
this.loaders = [];
}
UploadRepository.prototype = {
/**
* Creates a {@link CKEDITOR.fileTools.fileLoader file loader} instance with a unique ID.
* The instance can be later retrieved from the repository using the {@link #loaders} array.
*
* Fires the {@link CKEDITOR.fileTools.uploadRepository#instanceCreated instanceCreated} event.
*
* @param {Blob/String} fileOrData See {@link CKEDITOR.fileTools.fileLoader}.
* @param {String} fileName See {@link CKEDITOR.fileTools.fileLoader}.
* @returns {CKEDITOR.fileTools.fileLoader} The created file loader instance.
*/
create: function( fileOrData, fileName ) {
var id = this.loaders.length,
loader = new FileLoader( this.editor, fileOrData, fileName );
loader.id = id;
this.loaders[ id ] = loader;
this.fire( 'instanceCreated', loader );
return loader;
},
/**
* Returns `true` if all loaders finished their jobs.
*
* @returns {Boolean} `true` if all loaders finished their job, `false` otherwise.
*/
isFinished: function() {
for ( var id = 0; id < this.loaders.length; ++id ) {
if ( !this.loaders[ id ].isFinished() ) {
return false;
}
}
return true;
}
/**
* Array of loaders created by the {@link #create} method. Loaders' {@link CKEDITOR.fileTools.fileLoader#id IDs}
* are indexes.
*
* @readonly
* @property {CKEDITOR.fileTools.fileLoader[]} loaders
*/
/**
* Event fired when the {@link CKEDITOR.fileTools.fileLoader file loader} is created.
*
* @event instanceCreated
* @param {CKEDITOR.fileTools.fileLoader} data Created file loader.
*/
};
/**
* The `FileLoader` class is a wrapper which handles two file operations: loading the content of the file stored on
* the user's device into the memory and uploading the file to the server.
*
* There are two possible ways to crate a `FileLoader` instance: with a [Blob](https://developer.mozilla.org/en/docs/Web/API/Blob)
* (e.g. acquired from the {@link CKEDITOR.plugins.clipboard.dataTransfer#getFile} method) or with data as a Base64 string.
* Note that if the constructor gets the data as a Base64 string, there is no need to load the data, the data is already loaded.
*
* The `FileLoader` is created for a single load and upload process so if you abort the process,
* you need to create a new `FileLoader`.
*
* All process parameters are stored in public properties.
*
* `FileLoader` implements events so you can listen to them to react to changes. There are two types of events:
* events to notify the listeners about changes and an event that lets the listeners synchronize with current {@link #status}.
*
* The first group of events contains {@link #event-loading}, {@link #event-loaded}, {@link #event-uploading},
* {@link #event-uploaded}, {@link #event-error} and {@link #event-abort}. These events are called only once,
* when the {@link #status} changes.
*
* The second type is the {@link #event-update} event. It is fired every time the {@link #status} changes, the progress changes
* or the {@link #method-update} method is called. Is is created to synchronize the visual representation of the loader with
* its status. For example if the dialog window shows the upload progress, it should be refreshed on
* the {@link #event-update} listener. Then when the user closes and reopens this dialog, the {@link #method-update} method should
* be called to refresh the progress.
*
* Default request and response formats will work with CKFinder 2.4.3 and above. If you need a custom request
* or response handling you need to overwrite the default behavior using the {@link CKEDITOR.editor#fileUploadRequest} and
* {@link CKEDITOR.editor#fileUploadResponse} events. For more information see their documentation.
*
* To create a `FileLoader` instance, use the {@link CKEDITOR.fileTools.uploadRepository} class.
*
* Here is a simple `FileLoader` usage example:
*
* editor.on( 'paste', function( evt ) {
* for ( var i = 0; i < evt.data.dataTransfer.getFilesCount(); i++ ) {
* var file = evt.data.dataTransfer.getFile( i );
*
* if ( CKEDITOR.fileTools.isTypeSupported( file, /image\/png/ ) ) {
* var loader = editor.uploadRepository.create( file );
*
* loader.on( 'update', function() {
* document.getElementById( 'uploadProgress' ).innerHTML = loader.status;
* } );
*
* loader.on( 'error', function() {
* alert( 'Error!' );
* } );
*
* loader.loadAndUpload( 'http://upload.url/' );
*
* evt.data.dataValue += 'loading...'
* }
* }
* } );
*
* Note that `FileLoader` uses the native file API which is supported **since Internet Explorer 10**.
*
* @since 4.5
* @class CKEDITOR.fileTools.fileLoader
* @mixins CKEDITOR.event
* @constructor Creates an instance of the class and sets initial values for all properties.
* @param {CKEDITOR.editor} editor The editor instance. Used only to get language data.
* @param {Blob/String} fileOrData A [blob object](https://developer.mozilla.org/en/docs/Web/API/Blob) or a data
* string encoded with Base64.
* @param {String} [fileName] The file name. If not set and the second parameter is a file, then its name will be used.
* If not set and the second parameter is a Base64 data string, then the file name will be created based on
* the MIME type by replacing '/' with '.', for example for `image/png` the file name will be `image.png`.
*/
function FileLoader( editor, fileOrData, fileName ) {
this.editor = editor;
this.lang = editor.lang;
if ( typeof fileOrData === 'string' ) {
// Data are already loaded from disc.
this.data = fileOrData;
this.file = dataToFile( this.data );
this.total = this.file.size;
this.loaded = this.total;
} else {
this.data = null;
this.file = fileOrData;
this.total = this.file.size;
this.loaded = 0;
}
if ( fileName ) {
this.fileName = fileName;
} else if ( this.file.name ) {
this.fileName = this.file.name;
} else {
// If file has no name create a name based on the mime type.
this.fileName = this.file.type.replace( '/', '.' );
}
this.uploaded = 0;
this.status = 'created';
this.abort = function() {
this.changeStatus( 'abort' );
};
}
/**
* The loader status. Possible values:
*
* * `created` &ndash; The loader was created, but neither load nor upload started.
* * `loading` &ndash; The file is being loaded from the user's storage.
* * `loaded` &ndash; The file was loaded, the process is finished.
* * `uploading` &ndash; The file is being uploaded to the server.
* * `uploaded` &ndash; The file was uploaded, the process is finished.
* * `error` &ndash; The process stops because of an error, more details are available in the {@link #message} property.
* * `abort` &ndash; The process was stopped by the user.
*
* @property {String} status
*/
/**
* String data encoded with Base64. If the `FileLoader` is created with a Base64 string, the `data` is that string.
* If a file was passed to the constructor, the data is `null` until loading is completed.
*
* @readonly
* @property {String} data
*/
/**
* File object which represents the handled file. This property is set for both constructor options (file or data).
*
* @readonly
* @property {Blob} file
*/
/**
* The name of the file. If there is no file name, it is created from the MIME type.
* For example for the `image/png` MIME type, the file name will be `image.png`.
*
* @readonly
* @property {String} fileName
*/
/**
* The number of loaded bytes. If the `FileLoader` was created with a data string,
* the loaded value equals the {@link #total} value.
*
* @readonly
* @property {Number} loaded
*/
/**
* The number of uploaded bytes.
*
* @readonly
* @property {Number} uploaded
*/
/**
* The total file size in bytes.
*
* @readonly
* @property {Number} total
*/
/**
* The error message or additional information received from the server.
*
* @readonly
* @property {String} message
*/
/**
* The URL to the file when it is uploaded or received from the server.
*
* @readonly
* @property {String} url
*/
/**
* The target of the upload.
*
* @readonly
* @property {String} uploadUrl
*/
/**
*
* Native `FileReader` reference used to load the file.
*
* @readonly
* @property {FileReader} reader
*/
/**
* Native `XMLHttpRequest` reference used to upload the file.
*
* @readonly
* @property {XMLHttpRequest} xhr
*/
/**
* If `FileLoader` was created using {@link CKEDITOR.fileTools.uploadRepository},
* it gets an identifier which is stored in this property.
*
* @readonly
* @property {Number} id
*/
/**
* Aborts the process.
*
* This method has a different behavior depending on the current {@link #status}.
*
* * If the {@link #status} is `loading` or `uploading`, current operation will be aborted.
* * If the {@link #status} is `created`, `loading` or `uploading`, the {@link #status} will be changed to `abort`
* and the {@link #event-abort} event will be called.
* * If the {@link #status} is `loaded`, `uploaded`, `error` or `abort`, this method will do nothing.
*
* @method abort
*/
FileLoader.prototype = {
/**
* Loads a file from the storage on the user's device to the `data` attribute and uploads it to the server.
*
* The order of {@link #status statuses} for a successful load and upload is:
*
* * `created`,
* * `loading`,
* * `uploading`,
* * `uploaded`.
*
* @param {String} url The upload URL.
*/
loadAndUpload: function( url ) {
var loader = this;
this.once( 'loaded', function( evt ) {
// Cancel both 'loaded' and 'update' events,
// because 'loaded' is terminated state.
evt.cancel();
loader.once( 'update', function( evt ) {
evt.cancel();
}, null, null, 0 );
// Start uploading.
loader.upload( url );
}, null, null, 0 );
this.load();
},
/**
* Loads a file from the storage on the user's device to the `data` attribute.
*
* The order of the {@link #status statuses} for a successful load is:
*
* * `created`,
* * `loading`,
* * `loaded`.
*/
load: function() {
var loader = this;
this.reader = new FileReader();
var reader = this.reader;
loader.changeStatus( 'loading' );
this.abort = function() {
loader.reader.abort();
};
reader.onabort = function() {
loader.changeStatus( 'abort' );
};
reader.onerror = function() {
loader.message = loader.lang.filetools.loadError;
loader.changeStatus( 'error' );
};
reader.onprogress = function( evt ) {
loader.loaded = evt.loaded;
loader.update();
};
reader.onload = function() {
loader.loaded = loader.total;
loader.data = reader.result;
loader.changeStatus( 'loaded' );
};
reader.readAsDataURL( this.file );
},
/**
* Uploads a file to the server.
*
* The order of the {@link #status statuses} for a successful upload is:
*
* * `created`,
* * `uploading`,
* * `uploaded`.
*
* @param {String} url The upload URL.
*/
upload: function( url ) {
if ( !url ) {
this.message = this.lang.filetools.noUrlError;
this.changeStatus( 'error' );
} else {
this.uploadUrl = url;
this.xhr = new XMLHttpRequest();
this.attachRequestListeners();
if ( this.editor.fire( 'fileUploadRequest', { fileLoader: this } ) ) {
this.changeStatus( 'uploading' );
}
}
},
/**
* Attaches listeners to the XML HTTP request object.
*
* @private
* @param {XMLHttpRequest} xhr XML HTTP request object.
*/
attachRequestListeners: function() {
var loader = this,
xhr = this.xhr;
loader.abort = function() {
xhr.abort();
};
xhr.onabort = function() {
loader.changeStatus( 'abort' );
};
xhr.onerror = function() {
loader.message = loader.lang.filetools.networkError;
loader.changeStatus( 'error' );
};
xhr.onprogress = function( evt ) {
loader.uploaded = evt.loaded;
loader.update();
};
xhr.onload = function() {
loader.uploaded = loader.total;
if ( xhr.status < 200 || xhr.status > 299 ) {
loader.message = loader.lang.filetools[ 'httpError' + xhr.status ];
if ( !loader.message ) {
loader.message = loader.lang.filetools.httpError.replace( '%1', xhr.status );
}
loader.changeStatus( 'error' );
} else {
var data = {
fileLoader: loader
},
// Values to copy from event to FileLoader.
valuesToCopy = [ 'message', 'fileName', 'url' ],
success = loader.editor.fire( 'fileUploadResponse', data );
for ( var i = 0; i < valuesToCopy.length; i++ ) {
var key = valuesToCopy[ i ];
if ( typeof data[ key ] === 'string' ) {
loader[ key ] = data[ key ];
}
}
if ( success === false ) {
loader.changeStatus( 'error' );
} else {
loader.changeStatus( 'uploaded' );
}
}
};
},
/**
* Changes {@link #status} to the new status, updates the {@link #method-abort} method if needed and fires two events:
* new status and {@link #event-update}.
*
* @private
* @param {String} newStatus New status to be set.
*/
changeStatus: function( newStatus ) {
this.status = newStatus;
if ( newStatus == 'error' || newStatus == 'abort' ||
newStatus == 'loaded' || newStatus == 'uploaded' ) {
this.abort = function() {};
}
this.fire( newStatus );
this.update();
},
/**
* Updates the state of the `FileLoader` listeners. This method should be called if the state of the visual representation
* of the upload process is out of synchronization and needs to be refreshed (e.g. because of an undo operation or
* because the dialog window with the upload is closed and reopened). Fires the {@link #event-update} event.
*/
update: function() {
this.fire( 'update' );
},
/**
* Returns `true` if the loading and uploading finished (successfully or not), so the {@link #status} is
* `loaded`, `uploaded`, `error` or `abort`.
*
* @returns {Boolean} `true` if the loading and uploading finished.
*/
isFinished: function() {
return !!this.status.match( /^(?:loaded|uploaded|error|abort)$/ );
}
/**
* Event fired when the {@link #status} changes to `loading`. It will be fired once for the `FileLoader`.
*
* @event loading
*/
/**
* Event fired when the {@link #status} changes to `loaded`. It will be fired once for the `FileLoader`.
*
* @event loaded
*/
/**
* Event fired when the {@link #status} changes to `uploading`. It will be fired once for the `FileLoader`.
*
* @event uploading
*/
/**
* Event fired when the {@link #status} changes to `uploaded`. It will be fired once for the `FileLoader`.
*
* @event uploaded
*/
/**
* Event fired when the {@link #status} changes to `error`. It will be fired once for the `FileLoader`.
*
* @event error
*/
/**
* Event fired when the {@link #status} changes to `abort`. It will be fired once for the `FileLoader`.
*
* @event abort
*/
/**
* Event fired every time the `FileLoader` {@link #status} or progress changes or the {@link #method-update} method is called.
* This event was designed to allow showing the visualization of the progress and refresh that visualization
* every time the status changes. Note that multiple `update` events may be fired with the same status.
*
* @event update
*/
};
CKEDITOR.event.implementOn( UploadRepository.prototype );
CKEDITOR.event.implementOn( FileLoader.prototype );
var base64HeaderRegExp = /^data:(\S*?);base64,/;
// Transforms Base64 string data into file and creates name for that file based on the mime type.
//
// @private
// @param {String} data Base64 string data.
// @returns {Blob} File.
function dataToFile( data ) {
var contentType = data.match( base64HeaderRegExp )[ 1 ],
base64Data = data.replace( base64HeaderRegExp, '' ),
byteCharacters = atob( base64Data ),
byteArrays = [],
sliceSize = 512,
offset, slice, byteNumbers, i, byteArray;
for ( offset = 0; offset < byteCharacters.length; offset += sliceSize ) {
slice = byteCharacters.slice( offset, offset + sliceSize );
byteNumbers = new Array( slice.length );
for ( i = 0; i < slice.length; i++ ) {
byteNumbers[ i ] = slice.charCodeAt( i );
}
byteArray = new Uint8Array( byteNumbers );
byteArrays.push( byteArray );
}
return new Blob( byteArrays, { type: contentType } );
}
//
// PUBLIC API -------------------------------------------------------------
//
// Two plugins extend this object.
if ( !CKEDITOR.fileTools ) {
/**
* Helpers to load and upload a file.
*
* @since 4.5
* @singleton
* @class CKEDITOR.fileTools
*/
CKEDITOR.fileTools = {};
}
CKEDITOR.tools.extend( CKEDITOR.fileTools, {
uploadRepository: UploadRepository,
fileLoader: FileLoader,
/**
* Gets the upload URL from the {@link CKEDITOR.config configuration}. Because of backward compatibility
* the URL can be set using multiple configuration options.
*
* If the `type` is defined, then four configuration options will be checked in the following order
* (examples for `type='image'`):
*
* * `[type]UploadUrl`, e.g. {@link CKEDITOR.config#imageUploadUrl},
* * {@link CKEDITOR.config#uploadUrl},
* * `filebrowser[uppercased type]uploadUrl`, e.g. {@link CKEDITOR.config#filebrowserImageUploadUrl},
* * {@link CKEDITOR.config#filebrowserUploadUrl}.
*
* If the `type` is not defined, two configuration options will be checked:
*
* * {@link CKEDITOR.config#uploadUrl},
* * {@link CKEDITOR.config#filebrowserUploadUrl}.
*
* `filebrowser[type]uploadUrl` and `filebrowserUploadUrl` are checked for backward compatibility with the
* `filebrowser` plugin.
*
* For both `filebrowser[type]uploadUrl` and `filebrowserUploadUrl` `&responseType=json` is added to the end of the URL.
*
* @param {Object} config The configuration file.
* @param {String} [type] Upload file type.
* @returns {String/null} Upload URL or `null` if none of the configuration options were defined.
*/
getUploadUrl: function( config, type ) {
var capitalize = CKEDITOR.tools.capitalize;
if ( type && config[ type + 'UploadUrl' ] ) {
return config[ type + 'UploadUrl' ];
} else if ( config.uploadUrl ) {
return config.uploadUrl;
} else if ( type && config[ 'filebrowser' + capitalize( type, 1 ) + 'UploadUrl' ] ) {
return config[ 'filebrowser' + capitalize( type, 1 ) + 'UploadUrl' ] + '&responseType=json';
} else if ( config.filebrowserUploadUrl ) {
return config.filebrowserUploadUrl + '&responseType=json';
}
return null;
},
/**
* Checks if the MIME type of the given file is supported.
*
* CKEDITOR.fileTools.isTypeSupported( { type: 'image/png' }, /image\/(png|jpeg)/ ); // true
* CKEDITOR.fileTools.isTypeSupported( { type: 'image/png' }, /image\/(gif|jpeg)/ ); // false
*
* @param {Blob} file The file to check.
* @param {RegExp} supportedTypes A regular expression to check the MIME type of the file.
* @returns {Boolean} `true` if the file type is supported.
*/
isTypeSupported: function( file, supportedTypes ) {
return !!file.type.match( supportedTypes );
}
} );
} )();
/**
* The URL where files should be uploaded.
*
* An empty string means that the option is disabled.
*
* @since 4.5
* @cfg {String} [uploadUrl='']
* @member CKEDITOR.config
*/