-
Notifications
You must be signed in to change notification settings - Fork 3.6k
/
imageblockediting.ts
201 lines (174 loc) · 6.56 KB
/
imageblockediting.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module image/image/imageblockediting
*/
import { Plugin } from 'ckeditor5/src/core.js';
import { ClipboardPipeline, type ClipboardInputTransformationEvent, type ClipboardContentInsertionEvent } from 'ckeditor5/src/clipboard.js';
import { UpcastWriter, type ViewElement } from 'ckeditor5/src/engine.js';
import {
downcastImageAttribute,
downcastSrcsetAttribute,
upcastImageFigure
} from './converters.js';
import ImageEditing from './imageediting.js';
import ImageSizeAttributes from '../imagesizeattributes.js';
import ImageTypeCommand from './imagetypecommand.js';
import ImageUtils from '../imageutils.js';
import {
getImgViewElementMatcher,
createBlockImageViewElement,
determineImageTypeForInsertionAtSelection
} from './utils.js';
import ImagePlaceholder from './imageplaceholder.js';
/**
* The image block plugin.
*
* It registers:
*
* * `<imageBlock>` as a block element in the document schema, and allows `alt`, `src` and `srcset` attributes.
* * converters for editing and data pipelines.,
* * {@link module:image/image/imagetypecommand~ImageTypeCommand `'imageTypeBlock'`} command that converts inline images into
* block images.
*/
export default class ImageBlockEditing extends Plugin {
/**
* @inheritDoc
*/
public static get requires() {
return [ ImageEditing, ImageSizeAttributes, ImageUtils, ImagePlaceholder, ClipboardPipeline ] as const;
}
/**
* @inheritDoc
*/
public static get pluginName() {
return 'ImageBlockEditing' as const;
}
/**
* @inheritDoc
*/
public init(): void {
const editor = this.editor;
const schema = editor.model.schema;
// Converters 'alt' and 'srcset' are added in 'ImageEditing' plugin.
schema.register( 'imageBlock', {
inheritAllFrom: '$blockObject',
allowAttributes: [ 'alt', 'src', 'srcset' ]
} );
this._setupConversion();
if ( editor.plugins.has( 'ImageInlineEditing' ) ) {
editor.commands.add( 'imageTypeBlock', new ImageTypeCommand( this.editor, 'imageBlock' ) );
this._setupClipboardIntegration();
}
}
/**
* Configures conversion pipelines to support upcasting and downcasting
* block images (block image widgets) and their attributes.
*/
private _setupConversion(): void {
const editor = this.editor;
const t = editor.t;
const conversion = editor.conversion;
const imageUtils: ImageUtils = editor.plugins.get( 'ImageUtils' );
conversion.for( 'dataDowncast' )
.elementToStructure( {
model: 'imageBlock',
view: ( modelElement, { writer } ) => createBlockImageViewElement( writer )
} );
conversion.for( 'editingDowncast' )
.elementToStructure( {
model: 'imageBlock',
view: ( modelElement, { writer } ) => imageUtils.toImageWidget(
createBlockImageViewElement( writer ), writer, t( 'image widget' )
)
} );
conversion.for( 'downcast' )
.add( downcastImageAttribute( imageUtils, 'imageBlock', 'src' ) )
.add( downcastImageAttribute( imageUtils, 'imageBlock', 'alt' ) )
.add( downcastSrcsetAttribute( imageUtils, 'imageBlock' ) );
// More image related upcasts are in 'ImageEditing' plugin.
conversion.for( 'upcast' )
.elementToElement( {
view: getImgViewElementMatcher( editor, 'imageBlock' ),
model: ( viewImage, { writer } ) => writer.createElement(
'imageBlock',
viewImage.hasAttribute( 'src' ) ? { src: viewImage.getAttribute( 'src' ) } : undefined
)
} )
.add( upcastImageFigure( imageUtils ) );
}
/**
* Integrates the plugin with the clipboard pipeline.
*
* Idea is that the feature should recognize the user's intent when an **inline** image is
* pasted or dropped. If such an image is pasted/dropped:
*
* * into an empty block (e.g. an empty paragraph),
* * on another object (e.g. some block widget).
*
* it gets converted into a block image on the fly. We assume this is the user's intent
* if they decided to put their image there.
*
* See the `ImageInlineEditing` for the similar integration that works in the opposite direction.
*
* The feature also sets image `width` and `height` attributes on paste.
*/
private _setupClipboardIntegration(): void {
const editor = this.editor;
const model = editor.model;
const editingView = editor.editing.view;
const imageUtils: ImageUtils = editor.plugins.get( 'ImageUtils' );
const clipboardPipeline: ClipboardPipeline = editor.plugins.get( 'ClipboardPipeline' );
this.listenTo<ClipboardInputTransformationEvent>(
clipboardPipeline,
'inputTransformation',
( evt, data ) => {
const docFragmentChildren = Array.from( data.content.getChildren() as IterableIterator<ViewElement> );
let modelRange;
// Make sure only <img> elements are dropped or pasted. Otherwise, if there some other HTML
// mixed up, this should be handled as a regular paste.
if ( !docFragmentChildren.every( imageUtils.isInlineImageView ) ) {
return;
}
// When drag and dropping, data.targetRanges specifies where to drop because
// this is usually a different place than the current model selection (the user
// uses a drop marker to specify the drop location).
if ( data.targetRanges ) {
modelRange = editor.editing.mapper.toModelRange( data.targetRanges[ 0 ] );
}
// Pasting, however, always occurs at the current model selection.
else {
modelRange = model.document.selection.getFirstRange();
}
const selection = model.createSelection( modelRange );
// Convert inline images into block images only when the currently selected block is empty
// (e.g. an empty paragraph) or some object is selected (to replace it).
if ( determineImageTypeForInsertionAtSelection( model.schema, selection ) === 'imageBlock' ) {
const writer = new UpcastWriter( editingView.document );
// Wrap <img ... /> -> <figure class="image"><img .../></figure>
const blockViewImages = docFragmentChildren.map(
inlineViewImage => writer.createElement( 'figure', { class: 'image' }, inlineViewImage )
);
data.content = writer.createDocumentFragment( blockViewImages );
}
} );
this.listenTo<ClipboardContentInsertionEvent>(
clipboardPipeline,
'contentInsertion',
( evt, data ) => {
if ( data.method !== 'paste' ) {
return;
}
model.change( writer => {
const range = writer.createRangeIn( data.content );
for ( const item of range.getItems() ) {
if ( item.is( 'element', 'imageBlock' ) ) {
imageUtils.setImageNaturalSizeAttributes( item );
}
}
} );
} );
}
}