mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 18:12:29 +08:00
chore(ckeditor5/plugins): integrate file-upload
This commit is contained in:
parent
26c060bd22
commit
0ccbf75fa1
@ -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": {}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@ -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,
|
||||||
|
@ -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' );
|
@ -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 );
|
@ -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']
|
||||||
}
|
}
|
@ -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() {
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user