chore(ckeditor5/plugins): integrate file-upload

This commit is contained in:
Elian Doran 2025-05-03 23:39:30 +03:00
parent 26c060bd22
commit 0ccbf75fa1
No known key found for this signature in database
8 changed files with 48 additions and 67 deletions

View File

@ -1,18 +0,0 @@
{
"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": {}
}

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -12,6 +12,7 @@ import IndentBlockShortcutPlugin from "./plugins/indent_block_shortcut.js";
import MarkdownImportPlugin from "./plugins/markdownimport.js"; import MarkdownImportPlugin from "./plugins/markdownimport.js";
import MentionCustomization from "./plugins/mention_customization.js"; import MentionCustomization from "./plugins/mention_customization.js";
import IncludeNote from "./plugins/includenote.js"; import IncludeNote from "./plugins/includenote.js";
import Uploadfileplugin from "./plugins/file_upload/uploadfileplugin.js";
const TRILIUM_PLUGINS: typeof Plugin[] = [ const TRILIUM_PLUGINS: typeof Plugin[] = [
CutToNotePlugin, CutToNotePlugin,
@ -25,7 +26,8 @@ const TRILIUM_PLUGINS: typeof Plugin[] = [
IndentBlockShortcutPlugin, IndentBlockShortcutPlugin,
MarkdownImportPlugin, MarkdownImportPlugin,
MentionCustomization, MentionCustomization,
IncludeNote IncludeNote,
Uploadfileplugin
]; ];
export const COMMON_PLUGINS: typeof Plugin[] = [ export const COMMON_PLUGINS: typeof Plugin[] = [
@ -70,7 +72,6 @@ export const COMMON_PLUGINS: typeof Plugin[] = [
IndentBlock, IndentBlock,
ParagraphButtonUI, ParagraphButtonUI,
HeadingButtonsUI, HeadingButtonsUI,
//Uploadfileplugin
TextTransformation, TextTransformation,
Font, Font,
FontColor, FontColor,
@ -83,7 +84,6 @@ export const COMMON_PLUGINS: typeof Plugin[] = [
SpecialCharactersEssentials, SpecialCharactersEssentials,
FindAndReplace, FindAndReplace,
Mention, Mention,
// IncludeNote,
PageBreak, PageBreak,
GeneralHtmlSupport, GeneralHtmlSupport,
TextPartLanguage, TextPartLanguage,

View File

@ -1,5 +1,8 @@
import { FileRepository } from 'ckeditor5/src/upload'; import { Command, FileRepository, Model, type NodeAttributes, type Writer } from "ckeditor5";
import { Command } from 'ckeditor5/src/core';
interface FileUploadOpts {
file: File[];
}
export default class FileUploadCommand extends Command { export default class FileUploadCommand extends Command {
refresh() { refresh() {
@ -13,7 +16,7 @@ export default class FileUploadCommand extends Command {
* @param {Object} options Options for the executed command. * @param {Object} options Options for the executed command.
* @param {File|Array.<File>} options.file The file or an array of files to upload. * @param {File|Array.<File>} options.file The file or an array of files to upload.
*/ */
execute( options ) { execute( options: FileUploadOpts ) {
const editor = this.editor; const editor = this.editor;
const model = editor.model; const model = editor.model;
@ -30,12 +33,8 @@ export default class FileUploadCommand extends Command {
/** /**
* Handles uploading single 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 ) { function uploadFile( writer: Writer, model: Model, fileRepository: FileRepository, file: File ) {
const loader = fileRepository.createLoader( file ); const loader = fileRepository.createLoader( file );
// Do not throw when upload adapter is not set. FileRepository will log an error anyway. // Do not throw when upload adapter is not set. FileRepository will log an error anyway.
@ -46,7 +45,7 @@ function uploadFile( writer, model, fileRepository, file ) {
insertFileLink( writer, model, { href: '', uploadId: loader.id }, file ); insertFileLink( writer, model, { href: '', uploadId: loader.id }, file );
} }
function insertFileLink( writer, model, attributes = {}, file ) { function insertFileLink( writer: Writer, model: Model, attributes: NodeAttributes = {}, file: File ) {
const placeholder = writer.createElement( 'reference', attributes ); const placeholder = writer.createElement( 'reference', attributes );
model.insertContent( placeholder, model.document.selection ); model.insertContent( placeholder, model.document.selection );
writer.insertText( ' ', placeholder, 'after' ); writer.insertText( ' ', placeholder, 'after' );

View File

@ -1,12 +1,8 @@
import { Plugin } from 'ckeditor5/src/core'; import { Clipboard, FileRepository, Notification, Plugin, UpcastWriter, ViewElement, type Editor, type FileLoader, type Item, type Node, type ViewItem, type ViewRange } from 'ckeditor5';
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'; import FileUploadCommand from './fileuploadcommand';
export default class FileUploadEditing extends Plugin { export default class FileUploadEditing extends Plugin {
static get requires() { static get requires() {
return [ FileRepository, Notification, Clipboard ]; return [ FileRepository, Notification, Clipboard ];
} }
@ -46,14 +42,14 @@ export default class FileUploadEditing extends Plugin {
editor.model.change( writer => { editor.model.change( writer => {
// Set selection to paste target. // Set selection to paste target.
if ( data.targetRanges ) { if ( data.targetRanges ) {
writer.setSelection( data.targetRanges.map( viewRange => editor.editing.mapper.toModelRange( viewRange ) ) ); writer.setSelection( data.targetRanges.map( (viewRange: ViewRange) => editor.editing.mapper.toModelRange( viewRange ) ) );
} }
if ( files.length ) { if ( files.length ) {
evt.stop(); evt.stop();
// Upload files after the selection has changed in order to ensure the command's state is refreshed. // Upload files after the selection has changed in order to ensure the command's state is refreshed.
editor.model.enqueueChange( 'default', () => { editor.model.enqueueChange(() => {
editor.execute( 'fileUpload', { file: files } ); editor.execute( 'fileUpload', { file: files } );
} ); } );
} }
@ -62,15 +58,16 @@ export default class FileUploadEditing extends Plugin {
this.listenTo( editor.plugins.get( Clipboard ), 'inputTransformation', ( evt, data ) => { this.listenTo( editor.plugins.get( Clipboard ), 'inputTransformation', ( evt, data ) => {
const fetchableFiles = Array.from( editor.editing.view.createRangeIn( data.content ) ) const fetchableFiles = Array.from( editor.editing.view.createRangeIn( data.content ) )
.filter( value => isLocalFile( value.item ) && !value.item.getAttribute( 'uploadProcessed' ) ) .filter( value => isLocalFile( value.item ) && !(value.item as unknown as Node).getAttribute( 'uploadProcessed' ) )
.map( value => { .map( value => {
return { promise: fetchLocalFile( value.item ), fileElement: value.item }; return { promise: fetchLocalFile( value.item ), fileElement: value as unknown as ViewElement };
} ); } );
if ( !fetchableFiles.length ) { if ( !fetchableFiles.length ) {
return; return;
} }
//@ts-expect-error Missing document.
const writer = new UpcastWriter(); const writer = new UpcastWriter();
for ( const fetchableFile of fetchableFiles ) { for ( const fetchableFile of fetchableFiles ) {
@ -101,7 +98,7 @@ export default class FileUploadEditing extends Plugin {
const isInGraveyard = entry.position.root.rootName == '$graveyard'; const isInGraveyard = entry.position.root.rootName == '$graveyard';
for ( const file of getFileLinksFromChangeItem( editor, item ) ) { for ( const file of getFileLinksFromChangeItem( editor, item ) ) {
// Check if the file element still has upload id. // Check if the file element still has upload id.
const uploadId = file.getAttribute( 'uploadId' ); const uploadId = file.getAttribute( 'uploadId' ) as string | number;
if ( !uploadId ) { if ( !uploadId ) {
continue; continue;
} }
@ -127,14 +124,14 @@ export default class FileUploadEditing extends Plugin {
} ); } );
} }
_readAndUpload( loader, fileElement ) { _readAndUpload( loader: FileLoader, fileElement: Item ) {
const editor = this.editor; const editor = this.editor;
const model = editor.model; const model = editor.model;
const t = editor.locale.t; const t = editor.locale.t;
const fileRepository = editor.plugins.get( FileRepository ); const fileRepository = editor.plugins.get( FileRepository );
const notification = editor.plugins.get( Notification ); const notification = editor.plugins.get( Notification );
model.enqueueChange( 'transparent', writer => { model.enqueueChange(writer => {
writer.setAttribute( 'uploadStatus', 'reading', fileElement ); writer.setAttribute( 'uploadStatus', 'reading', fileElement );
} ); } );
@ -142,14 +139,14 @@ export default class FileUploadEditing extends Plugin {
.then( () => { .then( () => {
const promise = loader.upload(); const promise = loader.upload();
model.enqueueChange( 'transparent', writer => { model.enqueueChange(writer => {
writer.setAttribute( 'uploadStatus', 'uploading', fileElement ); writer.setAttribute( 'uploadStatus', 'uploading', fileElement );
} ); } );
return promise; return promise;
} ) } )
.then( data => { .then( data => {
model.enqueueChange( 'transparent', writer => { model.enqueueChange(writer => {
writer.setAttributes( { uploadStatus: 'complete', href: data.default }, fileElement ); writer.setAttributes( { uploadStatus: 'complete', href: data.default }, fileElement );
} ); } );
@ -181,13 +178,13 @@ export default class FileUploadEditing extends Plugin {
clean(); clean();
// Permanently remove file from insertion batch. // Permanently remove file from insertion batch.
model.enqueueChange( 'transparent', writer => { model.enqueueChange(writer => {
writer.remove( fileElement ); writer.remove( fileElement );
} ); } );
} ); } );
function clean() { function clean() {
model.enqueueChange( 'transparent', writer => { model.enqueueChange(writer => {
writer.removeAttribute( 'uploadId', fileElement ); writer.removeAttribute( 'uploadId', fileElement );
writer.removeAttribute( 'uploadStatus', fileElement ); writer.removeAttribute( 'uploadStatus', fileElement );
} ); } );
@ -197,16 +194,16 @@ export default class FileUploadEditing extends Plugin {
} }
} }
function fetchLocalFile( link ) { function fetchLocalFile( link: ViewItem ) {
return new Promise( ( resolve, reject ) => { return new Promise<File>( ( resolve, reject ) => {
const href = link.getAttribute( 'href' ); const href = (link as unknown as Node).getAttribute( 'href' ) as string;
// Fetch works asynchronously and so does not block the browser UI when processing data. // Fetch works asynchronously and so does not block the browser UI when processing data.
fetch( href ) fetch( href )
.then( resource => resource.blob() ) .then( resource => resource.blob() )
.then( blob => { .then( blob => {
const mimeType = getFileMimeType( blob, href ); const mimeType = getFileMimeType( blob, href ) ?? "";
const ext = mimeType.replace( 'file/', '' ); const ext = mimeType?.replace( 'file/', '' );
const filename = `file.${ ext }`; const filename = `file.${ ext }`;
const file = createFileFromBlob( blob, filename, mimeType ); const file = createFileFromBlob( blob, filename, mimeType );
@ -216,7 +213,7 @@ function fetchLocalFile( link ) {
} ); } );
} }
function isLocalFile( node ) { function isLocalFile( node: ViewItem ) {
if ( !node.is( 'element', 'a' ) || !node.getAttribute( 'href' ) ) { if ( !node.is( 'element', 'a' ) || !node.getAttribute( 'href' ) ) {
return false; return false;
} }
@ -224,17 +221,17 @@ function isLocalFile( node ) {
return node.getAttribute( 'href' ); return node.getAttribute( 'href' );
} }
function getFileMimeType( blob, src ) { function getFileMimeType( blob: Blob, src: string ) {
if ( blob.type ) { if ( blob.type ) {
return blob.type; return blob.type;
} else if ( src.match( /data:(image\/\w+);base64/ ) ) { } else if ( src.match( /data:(image\/\w+);base64/ ) ) {
return src.match( /data:(image\/\w+);base64/ )[ 1 ].toLowerCase(); return src.match( /data:(image\/\w+);base64/ )?.[ 1 ].toLowerCase();
} else { } else {
throw new Error( 'Could not retrieve mime type for file.' ); throw new Error( 'Could not retrieve mime type for file.' );
} }
} }
function createFileFromBlob( blob, filename, mimeType ) { function createFileFromBlob( blob: BlobPart, filename: string, mimeType: string ) {
try { try {
return new File( [ blob ], filename, { type: mimeType } ); return new File( [ blob ], filename, { type: mimeType } );
} catch ( err ) { } catch ( err ) {
@ -250,11 +247,11 @@ function createFileFromBlob( blob, filename, mimeType ) {
// //
// @param {module:clipboard/datatransfer~DataTransfer} dataTransfer // @param {module:clipboard/datatransfer~DataTransfer} dataTransfer
// @returns {Boolean} // @returns {Boolean}
export function isHtmlIncluded( dataTransfer ) { export function isHtmlIncluded( dataTransfer: DataTransfer ) {
return Array.from( dataTransfer.types ).includes( 'text/html' ) && dataTransfer.getData( 'text/html' ) !== ''; return Array.from( dataTransfer.types ).includes( 'text/html' ) && dataTransfer.getData( 'text/html' ) !== '';
} }
function getFileLinksFromChangeItem( editor, item ) { function getFileLinksFromChangeItem( editor: Editor, item: Item ) {
return Array.from( editor.model.createRangeOn( item ) ) return Array.from( editor.model.createRangeOn( item ) )
.filter( value => value.item.hasAttribute( 'href' ) ) .filter( value => value.item.hasAttribute( 'href' ) )
.map( value => value.item ); .map( value => value.item );

View File

@ -1,13 +1,14 @@
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'; import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
import { ButtonView, Locale, toUnit, View } from 'ckeditor5';
const toPx = toUnit('%'); const toPx = toUnit('%');
export default class ProgressBarView extends View { export default class ProgressBarView extends View {
constructor(locale) { private cancelButton: ButtonView;
width!: number;
customWidth!: number;
constructor(locale: Locale) {
super(locale); super(locale);
const bind = this.bindTemplate; const bind = this.bindTemplate;
@ -40,6 +41,7 @@ export default class ProgressBarView extends View {
'ck-progress-bar', 'ck-progress-bar',
// Observable attributes control the state of the view in DOM. // Observable attributes control the state of the view in DOM.
//@ts-expect-error Type 'ListenerBinding' is not assignable to type 'TemplateSimpleValueSchema'
bind.to('elementClass') bind.to('elementClass')
], ],
style: { style: {
@ -49,12 +51,13 @@ export default class ProgressBarView extends View {
}); });
} }
_createCancelButton(locale) { _createCancelButton(locale: Locale) {
const view = new ButtonView(locale); const view = new ButtonView(locale);
view.set({ view.set({
icon: cancelIcon, icon: cancelIcon,
tooltip: true, tooltip: true,
label: 'Cancel', label: 'Cancel',
//@ts-expect-error Object literal may only specify known properties, and 'attributes' does not exist in type
attributes: { attributes: {
class: ['ck', 'ck-button', 'ck-off', 'ck-button-cancel', 'ck-uploading-cancel'] class: ['ck', 'ck-button', 'ck-off', 'ck-button-cancel', 'ck-uploading-cancel']
} }

View File

@ -1,5 +1,5 @@
import { Plugin } from 'ckeditor5/src/core'; import { Plugin } from "ckeditor5";
import FileUploadEditing from './src/fileuploadediting'; import FileUploadEditing from "./fileuploadediting";
export default class Uploadfileplugin extends Plugin { export default class Uploadfileplugin extends Plugin {
static get requires() { static get requires() {

View File

@ -9,7 +9,7 @@
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"lib": ["DOM", "ES2015"], "lib": ["DOM", "ES2020"],
"types": [ "types": [
"vite/client", "vite/client",
"jquery" "jquery"