PageSourceSearch

https://www.ptc.edu.tw/src/ckeditor5/plugins/customLink/customLinkImageEditing.js

js ptc.edu.tw collected 2026-09-27 20:57:27 UTC 10,489 bytes, 282 lines download raw bytes

1import { Plugin, Matcher, toMap } from 'ckeditor5';
2import CustomLinkEditing from './customLinkEditing.js';
3
4export default class LinkImageEditing extends Plugin {
5	/**
6	 * @inheritDoc
7	 */
8	static get requires() {
9		return [ 'ImageEditing', 'ImageUtils', CustomLinkEditing ];
10	}
11
12	/**
13	 * @inheritDoc
14	 */
15	static get pluginName() {
16		return 'CustomLinkImageEditing';
17	}
18
19	init() {
20		const editor = this.editor;
21		const schema = editor.model.schema;
22
23		if ( editor.plugins.has( 'ImageBlockEditing' ) ) {
24			schema.extend( 'imageBlock', { allowAttributes: [ 'linkHref', 'linkTitle' ] } );
25		}
26
27		editor.conversion.for( 'upcast' ).add( upcastLink( editor ) );
28		editor.conversion.for( 'downcast' ).add( downcastImageLink( editor ) );
29
30		// Definitions for decorators are provided by the `link` command and the `LinkEditing` plugin.
31		this._enableAutomaticDecorators();
32		this._enableManualDecorators();
33	}
34
35	/**
36	 * Processes {@link module:link/link~LinkDecoratorAutomaticDefinition automatic decorators} definitions and
37	 * attaches proper converters that will work when linking an image.`
38	 *
39	 * @private
40	 */
41	_enableAutomaticDecorators() {
42		const editor = this.editor;
43		const command = editor.commands.get( 'link' );
44		const automaticDecorators = command.automaticDecorators;
45
46		if ( automaticDecorators.length ) {
47			editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcherForLinkedImage() );
48		}
49	}
50
51	/**
52	 * Processes transformed {@link module:link/utils~ManualDecorator} instances and attaches proper converters
53	 * that will work when linking an image.
54	 *
55	 * @private
56	 */
57	_enableManualDecorators() {
58		const editor = this.editor;
59		const command = editor.commands.get( 'link' );
60
61		for ( const decorator of command.manualDecorators ) {
62			if ( editor.plugins.has( 'ImageBlockEditing' ) ) {
63				editor.model.schema.extend( 'imageBlock', { allowAttributes: decorator.id } );
64			}
65
66			if ( editor.plugins.has( 'ImageInlineEditing' ) ) {
67				editor.model.schema.extend( 'imageInline', { allowAttributes: decorator.id } );
68			}
69
70			editor.conversion.for( 'downcast' ).add( downcastImageLinkManualDecorator( decorator ) );
71			editor.conversion.for( 'upcast' ).add( upcastImageLinkManualDecorator( editor, decorator ) );
72		}
73	}
74}
75
76// Returns a converter for linked block images that consumes the "href" attribute
77// if a link contains an image.
78//
79// @private
80// @param {module:core/editor/editor~Editor} editor The editor instance.
81// @returns {Function}
82function upcastLink( editor ) {
83	const isImageInlinePluginLoaded = editor.plugins.has( 'ImageInlineEditing' );
84	const imageUtils = editor.plugins.get( 'ImageUtils' );
85
86	return dispatcher => {
87		dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
88			const viewLink = data.viewItem;
89			const imageInLink = imageUtils.findViewImgElement( viewLink );
90
91			if ( !imageInLink ) {
92				return;
93			}
94
95			const blockImageView = imageInLink.findAncestor( element => imageUtils.isBlockImageView( element ) );
96
97			// There are four possible cases to consider here
98			//
99			// 1. A "root > ... > figure.image > a > img" structure.
100			// 2. A "root > ... > figure.image > a > picture > img" structure.
101			// 3. A "root > ... > block > a > img" structure.
102			// 4. A "root > ... > block > a > picture > img" structure.
103			//
104			// but the last 2 cases should only be considered by this converter when the inline image plugin
105			// is NOT loaded in the editor (because otherwise, that would be a plain, linked inline image).
106			if ( isImageInlinePluginLoaded && !blockImageView ) {
107				return;
108			}
109
110			// There's an image inside an <a> element - we consume it so it won't be picked up by the Link plugin.
111			const consumableAttributes = { attributes: [ 'href', 'title' ] };
112
113			// Consume the `href` attribute so the default one will not convert it to $text attribute.
114			if ( !conversionApi.consumable.consume( viewLink, consumableAttributes ) ) {
115				// Might be consumed by something else - i.e. other converter with priority=highest - a standard check.
116				return;
117			}
118
119			const linkHref = viewLink.getAttribute( 'href' );
120			const linkTitle = viewLink.getAttribute( 'title' );
121
122			// Missing the 'href' attribute.
123			if ( !linkHref ) {
124				return;
125			}
126
127			// A full definition of the image feature.
128			// figure > a > img: parent of the view link element is an image element (figure).
129			let modelElement = data.modelCursor.parent;
130
131			if ( !modelElement.is( 'element', 'imageBlock' ) ) {
132				// a > img: parent of the view link is not the image (figure) element. We need to convert it manually.
133				const conversionResult = conversionApi.convertItem( imageInLink, data.modelCursor );
134
135				// Set image range as conversion result.
136				data.modelRange = conversionResult.modelRange;
137
138				// Continue conversion where image conversion ends.
139				data.modelCursor = conversionResult.modelCursor;
140
141				modelElement = data.modelCursor.nodeBefore;
142			}
143
144			if ( modelElement && modelElement.is( 'element', 'imageBlock' ) ) {
145				// Set the linkHref attribute from link element on model image element.
146				conversionApi.writer.setAttribute( 'linkHref', linkHref, modelElement );
147				conversionApi.writer.setAttribute( 'linkTitle', linkTitle, modelElement );
148			}
149		}, { priority: 'high' } );
150		// Using the same priority that `upcastImageLinkManualDecorator()` converter guarantees
151		// that manual decorators will decorate the proper element.
152	};
153}
154
155// Creates a converter that adds `<a>` to linked block image view elements.
156//
157// @private
158function downcastImageLink( editor ) {
159	const imageUtils = editor.plugins.get( 'ImageUtils' );
160
161	return dispatcher => {
162		dispatcher.on( 'attribute:linkHref:imageBlock', ( evt, data, conversionApi ) => {
163			if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
164				return;
165			}
166
167			// The image will be already converted - so it will be present in the view.
168			const viewFigure = conversionApi.mapper.toViewElement( data.item );
169			const writer = conversionApi.writer;
170
171			// But we need to check whether the link element exists.
172			const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
173			const viewImage = imageUtils.findViewImgElement( viewFigure );
174			// <picture>...<img/></picture> or <img/>
175			const viewImgOrPicture = viewImage.parent.is( 'element', 'picture' ) ? viewImage.parent : viewImage;
176
177			// If so, update the attribute if it's defined or remove the entire link if the attribute is empty.
178			if ( linkInImage ) {
179				if ( data.attributeNewValue ) {
180					writer.setAttribute( 'href', data.attributeNewValue, linkInImage );
181					writer.setAttribute( 'title', data.item.getAttribute( 'linkTitle' ), linkInImage );
182				} else {
183					writer.move( writer.createRangeOn( viewImgOrPicture ), writer.createPositionAt( viewFigure, 0 ) );
184					writer.remove( linkInImage );
185				}
186			} else {
187				// But if it does not exist. Let's wrap already converted image by newly created link element.
188				// 1. Create an empty link element.
189				const linkElement = writer.createContainerElement( 'a', { href: data.attributeNewValue, title: data.item.getAttribute( 'linkTitle' ) } );
190
191				// 2. Insert link inside the associated image.
192				writer.insert( writer.createPositionAt( viewFigure, 0 ), linkElement );
193
194				// 3. Move the image to the link.
195				writer.move( writer.createRangeOn( viewImgOrPicture ), writer.createPositionAt( linkElement, 0 ) );
196			}
197		}, { priority: 'high' } );
198	};
199}
200
201// Returns a converter that decorates the `<a>` element when the image is the link label.
202//
203// @private
204// @returns {Function}
205function downcastImageLinkManualDecorator( decorator ) {
206	return dispatcher => {
207		dispatcher.on( `attribute:${ decorator.id }:imageBlock`, ( evt, data, conversionApi ) => {
208			const viewFigure = conversionApi.mapper.toViewElement( data.item );
209			const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
210
211			// The <a> element was removed by the time this converter is executed.
212			// It may happen when the base `linkHref` and decorator attributes are removed
213			// at the same time (see #8401).
214			if ( !linkInImage ) {
215				return;
216			}
217
218			for ( const [ key, val ] of toMap( decorator.attributes ) ) {
219				conversionApi.writer.setAttribute( key, val, linkInImage );
220			}
221
222			if ( decorator.classes ) {
223				conversionApi.writer.addClass( decorator.classes, linkInImage );
224			}
225
226			for ( const key in decorator.styles ) {
227				conversionApi.writer.setStyle( key, decorator.styles[ key ], linkInImage );
228			}
229            conversionApi.writer.setAttribute( 'title', data.item.getAttribute( 'linkTitle' ), linkInImage );
230		} );
231	};
232}
233
234// Returns a converter that checks whether manual decorators should be applied to the link.
235//
236// @private
237// @returns {Function}
238function upcastImageLinkManualDecorator( editor, decorator ) {
239	const isImageInlinePluginLoaded = editor.plugins.has( 'ImageInlineEditing' );
240	const imageUtils = editor.plugins.get( 'ImageUtils' );
241
242	return dispatcher => {
243		dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
244			const viewLink = data.viewItem;
245			const imageInLink = imageUtils.findViewImgElement( viewLink );
246
247			// We need to check whether an image is inside a link because the converter handles
248			// only manual decorators for linked images. See #7975.
249			if ( !imageInLink ) {
250				return;
251			}
252
253			const blockImageView = imageInLink.findAncestor( element => imageUtils.isBlockImageView( element ) );
254
255			if ( isImageInlinePluginLoaded && !blockImageView ) {
256				return;
257			}
258
259			const matcher = new Matcher( decorator._createPattern() );
260			const result = matcher.match( viewLink );
261
262			// The link element does not have required attributes or/and proper values.
263			if ( !result ) {
264				return;
265			}
266
267			// Check whether we can consume those attributes.
268			if ( !conversionApi.consumable.consume( viewLink, result.match ) ) {
269				return;
270			}
271
272			// At this stage we can assume that we have the `<imageBlock>` element.
273			// `nodeBefore` comes after conversion: `<a><img></a>`.
274			// `parent` comes with full image definition: `<figure><a><img></a></figure>.
275			// See the body of the `upcastLink()` function.
276			const modelElement = data.modelCursor.nodeBefore || data.modelCursor.parent;
277
278			conversionApi.writer.setAttribute( decorator.id, true, modelElement );
279		}, { priority: 'high' } );
280		// Using the same priority that `upcastLink()` converter guarantees that the linked image was properly converted.
281	};
282}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.