mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 10:02:59 +08:00
chore(ckeditor5/plugins): add file-upload
Original commit: a440988df452ab4367f5288269ba3c05aa5e6b8e
This commit is contained in:
parent
68456f0488
commit
26c060bd22
18
_regroup/ckeditor5-file-upload/package.json
Normal file
18
_regroup/ckeditor5-file-upload/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@ckeditor/ckeditor5-file-upload",
|
||||
"version": "1.0.0",
|
||||
"description": "File Upload for CKEditor5",
|
||||
"main": "src/fileupload.js",
|
||||
"keywords": [ "ckeditor5" ],
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-core": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-upload": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-ui": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-clipboard": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-engine": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-utils": "^38.0.1",
|
||||
"@ckeditor/ckeditor5-widget": "^38.0.1"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {}
|
||||
}
|
53
_regroup/ckeditor5-file-upload/src/fileuploadcommand.js
Normal file
53
_regroup/ckeditor5-file-upload/src/fileuploadcommand.js
Normal file
@ -0,0 +1,53 @@
|
||||
import { FileRepository } from 'ckeditor5/src/upload';
|
||||
import { Command } from 'ckeditor5/src/core';
|
||||
|
||||
export default class FileUploadCommand extends Command {
|
||||
refresh() {
|
||||
this.isEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the command.
|
||||
*
|
||||
* @fires execute
|
||||
* @param {Object} options Options for the executed command.
|
||||
* @param {File|Array.<File>} options.file The file or an array of files to upload.
|
||||
*/
|
||||
execute( options ) {
|
||||
const editor = this.editor;
|
||||
const model = editor.model;
|
||||
|
||||
const fileRepository = editor.plugins.get( FileRepository );
|
||||
|
||||
model.change( writer => {
|
||||
const filesToUpload = options.file;
|
||||
for ( const file of filesToUpload ) {
|
||||
uploadFile( writer, model, fileRepository, file );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles uploading single file.
|
||||
*
|
||||
* @param {module:engine/model/writer~writer} writer
|
||||
* @param {module:engine/model/model~Model} model
|
||||
* @param {File} file
|
||||
*/
|
||||
function uploadFile( writer, model, fileRepository, file ) {
|
||||
const loader = fileRepository.createLoader( file );
|
||||
|
||||
// Do not throw when upload adapter is not set. FileRepository will log an error anyway.
|
||||
if ( !loader ) {
|
||||
return;
|
||||
}
|
||||
|
||||
insertFileLink( writer, model, { href: '', uploadId: loader.id }, file );
|
||||
}
|
||||
|
||||
function insertFileLink( writer, model, attributes = {}, file ) {
|
||||
const placeholder = writer.createElement( 'reference', attributes );
|
||||
model.insertContent( placeholder, model.document.selection );
|
||||
writer.insertText( ' ', placeholder, 'after' );
|
||||
}
|
261
_regroup/ckeditor5-file-upload/src/fileuploadediting.js
Normal file
261
_regroup/ckeditor5-file-upload/src/fileuploadediting.js
Normal file
@ -0,0 +1,261 @@
|
||||
import { Plugin } from 'ckeditor5/src/core';
|
||||
import { FileRepository } from 'ckeditor5/src/upload';
|
||||
import { Notification } from 'ckeditor5/src/ui';
|
||||
import { Clipboard } from 'ckeditor5/src/clipboard';
|
||||
import { UpcastWriter } from 'ckeditor5/src/engine';
|
||||
|
||||
import FileUploadCommand from './fileuploadcommand';
|
||||
|
||||
export default class FileUploadEditing extends Plugin {
|
||||
static get requires() {
|
||||
return [ FileRepository, Notification, Clipboard ];
|
||||
}
|
||||
|
||||
static get pluginName() {
|
||||
return 'FileUploadEditing';
|
||||
}
|
||||
|
||||
init() {
|
||||
const editor = this.editor;
|
||||
const doc = editor.model.document;
|
||||
const conversion = editor.conversion;
|
||||
const fileRepository = editor.plugins.get( FileRepository );
|
||||
|
||||
// Register fileUpload command.
|
||||
editor.commands.add( 'fileUpload', new FileUploadCommand( editor ) );
|
||||
|
||||
// Register upcast converter for uploadId.
|
||||
conversion.for( 'upcast' )
|
||||
.attributeToAttribute( {
|
||||
view: {
|
||||
name: 'a',
|
||||
key: 'uploadId'
|
||||
},
|
||||
model: 'uploadId'
|
||||
} );
|
||||
|
||||
this.listenTo( editor.editing.view.document, 'clipboardInput', ( evt, data ) => {
|
||||
// Skip if non-empty HTML data is included.
|
||||
// https://github.com/ckeditor/ckeditor5-upload/issues/68
|
||||
if ( isHtmlIncluded( data.dataTransfer ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from( data.dataTransfer.files );
|
||||
|
||||
editor.model.change( writer => {
|
||||
// Set selection to paste target.
|
||||
if ( data.targetRanges ) {
|
||||
writer.setSelection( data.targetRanges.map( viewRange => editor.editing.mapper.toModelRange( viewRange ) ) );
|
||||
}
|
||||
|
||||
if ( files.length ) {
|
||||
evt.stop();
|
||||
|
||||
// Upload files after the selection has changed in order to ensure the command's state is refreshed.
|
||||
editor.model.enqueueChange( 'default', () => {
|
||||
editor.execute( 'fileUpload', { file: files } );
|
||||
} );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
|
||||
this.listenTo( editor.plugins.get( Clipboard ), 'inputTransformation', ( evt, data ) => {
|
||||
const fetchableFiles = Array.from( editor.editing.view.createRangeIn( data.content ) )
|
||||
.filter( value => isLocalFile( value.item ) && !value.item.getAttribute( 'uploadProcessed' ) )
|
||||
.map( value => {
|
||||
return { promise: fetchLocalFile( value.item ), fileElement: value.item };
|
||||
} );
|
||||
|
||||
if ( !fetchableFiles.length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const writer = new UpcastWriter();
|
||||
|
||||
for ( const fetchableFile of fetchableFiles ) {
|
||||
// Set attribute marking that the file was processed already.
|
||||
writer.setAttribute( 'uploadProcessed', true, fetchableFile.fileElement );
|
||||
|
||||
const loader = fileRepository.createLoader( fetchableFile.promise );
|
||||
|
||||
if ( loader ) {
|
||||
writer.setAttribute( 'href', '', fetchableFile.fileElement );
|
||||
writer.setAttribute( 'uploadId', loader.id, fetchableFile.fileElement );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
// Prevents from the browser redirecting to the dropped file.
|
||||
editor.editing.view.document.on( 'dragover', ( evt, data ) => {
|
||||
data.preventDefault();
|
||||
} );
|
||||
|
||||
// Upload placeholder files that appeared in the model.
|
||||
doc.on( 'change', () => {
|
||||
const changes = doc.differ.getChanges( { includeChangesInGraveyard: true } );
|
||||
for ( const entry of changes ) {
|
||||
if ( entry.type == 'insert' ) {
|
||||
const item = entry.position.nodeAfter;
|
||||
if ( item ) {
|
||||
const isInGraveyard = entry.position.root.rootName == '$graveyard';
|
||||
for ( const file of getFileLinksFromChangeItem( editor, item ) ) {
|
||||
// Check if the file element still has upload id.
|
||||
const uploadId = file.getAttribute( 'uploadId' );
|
||||
if ( !uploadId ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the file is loaded on this client.
|
||||
const loader = fileRepository.loaders.get( uploadId );
|
||||
|
||||
if ( !loader ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( isInGraveyard ) {
|
||||
// If the file was inserted to the graveyard - abort the loading process.
|
||||
loader.abort();
|
||||
} else if ( loader.status == 'idle' ) {
|
||||
// If the file was inserted into content and has not been loaded yet, start loading it.
|
||||
this._readAndUpload( loader, file );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
_readAndUpload( loader, fileElement ) {
|
||||
const editor = this.editor;
|
||||
const model = editor.model;
|
||||
const t = editor.locale.t;
|
||||
const fileRepository = editor.plugins.get( FileRepository );
|
||||
const notification = editor.plugins.get( Notification );
|
||||
|
||||
model.enqueueChange( 'transparent', writer => {
|
||||
writer.setAttribute( 'uploadStatus', 'reading', fileElement );
|
||||
} );
|
||||
|
||||
return loader.read()
|
||||
.then( () => {
|
||||
const promise = loader.upload();
|
||||
|
||||
model.enqueueChange( 'transparent', writer => {
|
||||
writer.setAttribute( 'uploadStatus', 'uploading', fileElement );
|
||||
} );
|
||||
|
||||
return promise;
|
||||
} )
|
||||
.then( data => {
|
||||
model.enqueueChange( 'transparent', writer => {
|
||||
writer.setAttributes( { uploadStatus: 'complete', href: data.default }, fileElement );
|
||||
} );
|
||||
|
||||
clean();
|
||||
|
||||
// wait a bit so that froca has time to load the changes
|
||||
return new Promise(res => setTimeout(res, 100));
|
||||
} )
|
||||
.then(() => {
|
||||
// we're correctly updating the model, but the view remains broken,
|
||||
// hack around it is to force CKEditor to reload the now updated HTML
|
||||
editor.setData(editor.getData());
|
||||
})
|
||||
.catch( error => {
|
||||
// If status is not 'error' nor 'aborted' - throw error, because it means that something else went wrong,
|
||||
// it might be a generic error, and it would be real pain to find what is going on.
|
||||
if ( loader.status !== 'error' && loader.status !== 'aborted' ) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Might be 'aborted'.
|
||||
if ( loader.status == 'error' && error ) {
|
||||
notification.showWarning( error, {
|
||||
title: t( 'Upload failed' ),
|
||||
namespace: 'upload'
|
||||
} );
|
||||
}
|
||||
|
||||
clean();
|
||||
|
||||
// Permanently remove file from insertion batch.
|
||||
model.enqueueChange( 'transparent', writer => {
|
||||
writer.remove( fileElement );
|
||||
} );
|
||||
} );
|
||||
|
||||
function clean() {
|
||||
model.enqueueChange( 'transparent', writer => {
|
||||
writer.removeAttribute( 'uploadId', fileElement );
|
||||
writer.removeAttribute( 'uploadStatus', fileElement );
|
||||
} );
|
||||
|
||||
fileRepository.destroyLoader( loader );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fetchLocalFile( link ) {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const href = link.getAttribute( 'href' );
|
||||
|
||||
// Fetch works asynchronously and so does not block the browser UI when processing data.
|
||||
fetch( href )
|
||||
.then( resource => resource.blob() )
|
||||
.then( blob => {
|
||||
const mimeType = getFileMimeType( blob, href );
|
||||
const ext = mimeType.replace( 'file/', '' );
|
||||
const filename = `file.${ ext }`;
|
||||
const file = createFileFromBlob( blob, filename, mimeType );
|
||||
|
||||
file ? resolve( file ) : reject();
|
||||
} )
|
||||
.catch( reject );
|
||||
} );
|
||||
}
|
||||
|
||||
function isLocalFile( node ) {
|
||||
if ( !node.is( 'element', 'a' ) || !node.getAttribute( 'href' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return node.getAttribute( 'href' );
|
||||
}
|
||||
|
||||
function getFileMimeType( blob, src ) {
|
||||
if ( blob.type ) {
|
||||
return blob.type;
|
||||
} else if ( src.match( /data:(image\/\w+);base64/ ) ) {
|
||||
return src.match( /data:(image\/\w+);base64/ )[ 1 ].toLowerCase();
|
||||
} else {
|
||||
throw new Error( 'Could not retrieve mime type for file.' );
|
||||
}
|
||||
}
|
||||
|
||||
function createFileFromBlob( blob, filename, mimeType ) {
|
||||
try {
|
||||
return new File( [ blob ], filename, { type: mimeType } );
|
||||
} catch ( err ) {
|
||||
// Edge does not support `File` constructor ATM, see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9551546/.
|
||||
// However, the `File` function is present (so cannot be checked with `!window.File` or `typeof File === 'function'`), but
|
||||
// calling it with `new File( ... )` throws an error. This try-catch prevents that. Also when the function will
|
||||
// be implemented correctly in Edge the code will start working without any changes (see #247).
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns `true` if non-empty `text/html` is included in the data transfer.
|
||||
//
|
||||
// @param {module:clipboard/datatransfer~DataTransfer} dataTransfer
|
||||
// @returns {Boolean}
|
||||
export function isHtmlIncluded( dataTransfer ) {
|
||||
return Array.from( dataTransfer.types ).includes( 'text/html' ) && dataTransfer.getData( 'text/html' ) !== '';
|
||||
}
|
||||
|
||||
function getFileLinksFromChangeItem( editor, item ) {
|
||||
return Array.from( editor.model.createRangeOn( item ) )
|
||||
.filter( value => value.item.hasAttribute( 'href' ) )
|
||||
.map( value => value.item );
|
||||
}
|
30
_regroup/ckeditor5-file-upload/theme/fileupload.css
Normal file
30
_regroup/ckeditor5-file-upload/theme/fileupload.css
Normal file
@ -0,0 +1,30 @@
|
||||
.ck.ck-sticky-panel > .ck-progress-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
border-left: 1px solid var(--ck-color-base-border);
|
||||
border-top: 1px solid var(--ck-color-base-border);
|
||||
border-right: 1px solid var(--ck-color-base-border);
|
||||
}
|
||||
|
||||
.ck.ck-sticky-panel > .ck-progress-bar > .ck-uploading-progress {
|
||||
align-self: center;
|
||||
padding: 3px 5px;
|
||||
font-weight: bold;
|
||||
|
||||
color: var(--ck-color-base-foreground);
|
||||
background-color: var(--ck-color-base-border);
|
||||
|
||||
transition-property: width;
|
||||
transition-duration: .5s;
|
||||
transition-timing-function: linear;
|
||||
|
||||
}
|
||||
|
||||
.ck.ck-sticky-panel > .ck-progress-bar > .ck-uploading-cancel {
|
||||
align-self: flex-end;
|
||||
padding: 0 5px;
|
||||
font-weight: bold;
|
||||
color: var(--ck-color-base-error);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
|
||||
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
|
||||
<g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path d="M1443.6,4990.4c-26.9-11.5-69.2-38.4-92.2-61.5c-107.6-99.9-99.9,232.5-99.9-4823.9v-4677.8l42.3-63.4c25-34.6,69.2-82.6,98-103.8c53.8-40.3,57.7-40.3,1966.1-40.3h1910.3l51.9,53.8c51.9,50,53.8,59.6,53.8,246v194.1l-63.4,55.7l-61.5,55.7H3557.7H1866.4V110.8v4285.8H3894h2027.6v-945.6c0-622.7,7.7-974.4,21.2-1024.3c11.5-42.3,44.2-101.9,71.1-132.6c101.9-107.6,92.2-107.6,1147.4-107.6h970.5V716.1c0-1633.6-5.8-1560.6,124.9-1616.3c84.6-34.6,280.6-34.6,363.2,0c136.4,55.7,126.8-67.3,126.8,1795c0,1620.1-1.9,1679.7-36.5,1748.9c-55.7,105.7-2262,2300.5-2350.4,2338.9c-63.4,25-351.7,28.8-2469.6,26.9C2345,5009.6,1474.4,5001.9,1443.6,4990.4z M7132.3,3368.3l567-567h-572.7h-570.8v567c0,311.3,1.9,567,5.8,567C6563.4,3935.3,6821,3679.6,7132.3,3368.3z"/><path d="M6242.5-1670.8c-411.3-411.3-772.6-784.1-803.3-830.2c-157.6-242.1,55.7-536.2,334.4-463.2c69.2,19.2,144.1,86.5,522.7,463.2c244.1,242.2,449.7,440.1,457.4,440.1c5.8,0,15.4-572.7,17.3-1272.3c5.8-1239.6,7.7-1276.1,44.2-1326.1c48-67.3,174.9-130.7,257.5-130.7c90.3,0,203.7,65.4,253.7,148l46.1,73l5.8,1256.9l5.8,1258.8l445.9-443.9c388.2-388.2,457.4-447.8,532.4-467c251.8-67.3,468.9,192.2,355.5,430.5c-15.4,32.7-376.7,407.4-803.3,834.1C7157.3-944.4,7140-927.1,7065-927.1S6972.8-944.4,6242.5-1670.8z"/></g></g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
68
_regroup/ckeditor5-file-upload/theme/progressbarview.js
Normal file
68
_regroup/ckeditor5-file-upload/theme/progressbarview.js
Normal file
@ -0,0 +1,68 @@
|
||||
import View from "@ckeditor/ckeditor5-ui/src/view";
|
||||
import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
|
||||
import ButtonView from "@ckeditor/ckeditor5-ui/src/button/buttonview";
|
||||
|
||||
import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
|
||||
|
||||
const toPx = toUnit('%');
|
||||
|
||||
export default class ProgressBarView extends View {
|
||||
constructor(locale) {
|
||||
super(locale);
|
||||
|
||||
const bind = this.bindTemplate;
|
||||
this.cancelButton = this._createCancelButton(locale);
|
||||
|
||||
// Views define their interface (state) using observable attributes.
|
||||
this.set('width', 100);
|
||||
this.set('customWidth', 0);
|
||||
|
||||
|
||||
this.setTemplate({
|
||||
tag: 'div',
|
||||
|
||||
// The element of the view can be defined with its children.
|
||||
children: [
|
||||
{
|
||||
tag: 'div',
|
||||
children: ['Uploading...'],
|
||||
attributes: {
|
||||
class: ['ck-uploading-progress'],
|
||||
style: {
|
||||
width: bind.to('customWidth', toPx),
|
||||
}
|
||||
}
|
||||
},
|
||||
this.cancelButton,
|
||||
],
|
||||
attributes: {
|
||||
class: [
|
||||
'ck-progress-bar',
|
||||
|
||||
// Observable attributes control the state of the view in DOM.
|
||||
bind.to('elementClass')
|
||||
],
|
||||
style: {
|
||||
width: bind.to('width', toPx),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_createCancelButton(locale) {
|
||||
const view = new ButtonView(locale);
|
||||
view.set({
|
||||
icon: cancelIcon,
|
||||
tooltip: true,
|
||||
label: 'Cancel',
|
||||
attributes: {
|
||||
class: ['ck', 'ck-button', 'ck-off', 'ck-button-cancel', 'ck-uploading-cancel']
|
||||
}
|
||||
});
|
||||
|
||||
view.on('execute', () => {
|
||||
this.fire('cancel')
|
||||
});
|
||||
return view;
|
||||
}
|
||||
}
|
12
_regroup/ckeditor5-file-upload/uploadfileplugin.js
Normal file
12
_regroup/ckeditor5-file-upload/uploadfileplugin.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { Plugin } from 'ckeditor5/src/core';
|
||||
import FileUploadEditing from './src/fileuploadediting';
|
||||
|
||||
export default class Uploadfileplugin extends Plugin {
|
||||
static get requires() {
|
||||
return [ FileUploadEditing ];
|
||||
}
|
||||
|
||||
static get pluginName() {
|
||||
return 'fileUploadPlugin';
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user