@ckeditor/ckeditor5-ckbox 34.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,406 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+
6
+ /* global document, window, setTimeout, URL */
7
+
8
+ /**
9
+ * @module ckbox/ckboxcommand
10
+ */
11
+
12
+ import { Command } from 'ckeditor5/src/core';
13
+ import { createElement, toMap } from 'ckeditor5/src/utils';
14
+ import { getEnvironmentId, getImageUrls } from './utils';
15
+
16
+ // Defines the waiting time (in milliseconds) for inserting the chosen asset into the model. The chosen asset is temporarily stored in the
17
+ // `CKBoxCommand#_chosenAssets` and it is removed from there automatically after this time. See `CKBoxCommand#_chosenAssets` for more
18
+ // details.
19
+ const ASSET_INSERTION_WAIT_TIMEOUT = 1000;
20
+
21
+ /**
22
+ * The CKBox command. It is used by the {@link module:ckbox/ckboxediting~CKBoxEditing CKBox editing feature} to open the CKBox file manager.
23
+ * The file manager allows inserting an image or a link to a file into the editor content.
24
+ *
25
+ * editor.execute( 'ckbox' );
26
+ *
27
+ * **Note:** This command uses other features to perform the following tasks:
28
+ * - To insert images it uses the {@link module:image/image/insertimagecommand~InsertImageCommand 'insertImage'} command from the
29
+ * {@link module:image/image~Image Image feature}.
30
+ * - To insert links to other files it uses the {@link module:link/linkcommand~LinkCommand 'link'} command from the
31
+ * {@link module:link/link~Link Link feature}.
32
+ *
33
+ * @extends module:core/command~Command
34
+ */
35
+ export default class CKBoxCommand extends Command {
36
+ /**
37
+ * @inheritDoc
38
+ */
39
+ constructor( editor ) {
40
+ super( editor );
41
+
42
+ /**
43
+ * A set of all chosen assets. They are stored temporarily and they are automatically removed 1 second after being chosen.
44
+ * Chosen assets have to be "remembered" for a while to be able to map the given asset with the element inserted into the model.
45
+ * This association map is then used to set the ID on the model element.
46
+ *
47
+ * All chosen assets are automatically removed after the timeout, because (theoretically) it may happen that they will never be
48
+ * inserted into the model, even if the {@link module:link/linkcommand~LinkCommand `'link'`} command or the
49
+ * {@link module:image/image/insertimagecommand~InsertImageCommand `'insertImage'`} command is enabled. Such a case may arise when
50
+ * another plugin blocks the command execution. Then, in order not to keep the chosen (but not inserted) assets forever, we delete
51
+ * them automatically to prevent memory leakage. The 1 second timeout is enough to insert the asset into the model and extract the
52
+ * ID from the chosen asset.
53
+ *
54
+ * The assets are stored only if
55
+ * the {@link module:ckbox/ckbox~CKBoxConfig#ignoreDataId `config.ckbox.ignoreDataId`} option is set to `false` (by default).
56
+ *
57
+ * @protected
58
+ * @member {Set.<module:ckbox/ckbox~CKBoxAssetDefinition>} #_chosenAssets
59
+ */
60
+ this._chosenAssets = new Set();
61
+
62
+ /**
63
+ * The DOM element that acts as a mounting point for the CKBox dialog.
64
+ *
65
+ * @private
66
+ * @member {Element|null} #_wrapper
67
+ */
68
+ this._wrapper = null;
69
+
70
+ this._initListeners();
71
+ }
72
+
73
+ /**
74
+ * @inheritDoc
75
+ */
76
+ refresh() {
77
+ this.value = this._getValue();
78
+ this.isEnabled = this._checkEnabled();
79
+ }
80
+
81
+ /**
82
+ * @inheritDoc
83
+ */
84
+ execute() {
85
+ this.fire( 'ckbox:open' );
86
+ }
87
+
88
+ /**
89
+ * Indicates if the CKBox dialog is already opened.
90
+ *
91
+ * @protected
92
+ * @returns {Boolean}
93
+ */
94
+ _getValue() {
95
+ return this._wrapper !== null;
96
+ }
97
+
98
+ /**
99
+ * Checks whether the command can be enabled in the current context.
100
+ *
101
+ * @protected
102
+ * @returns {Boolean}
103
+ */
104
+ _checkEnabled() {
105
+ const imageCommand = this.editor.commands.get( 'insertImage' );
106
+ const linkCommand = this.editor.commands.get( 'link' );
107
+
108
+ if ( !imageCommand.isEnabled && !linkCommand.isEnabled ) {
109
+ return false;
110
+ }
111
+
112
+ return true;
113
+ }
114
+
115
+ /**
116
+ * Creates the options object for the CKBox dialog.
117
+ *
118
+ * @protected
119
+ * @returns {Object} options
120
+ * @returns {String} options.theme The theme for CKBox dialog.
121
+ * @returns {String} options.language The language for CKBox dialog.
122
+ * @returns {String} options.tokenUrl The token endpoint URL.
123
+ * @returns {String} options.serviceOrigin The base URL of the API service.
124
+ * @returns {String} options.assetsOrigin The base URL for assets inserted into the editor.
125
+ * @returns {Object} options.dialog
126
+ * @returns {Function} options.dialog.onClose The callback function invoked after closing the CKBox dialog.
127
+ * @returns {Object} options.assets
128
+ * @returns {Function} options.assets.onChoose The callback function invoked after choosing the assets.
129
+ */
130
+ _prepareOptions() {
131
+ const editor = this.editor;
132
+ const ckboxConfig = editor.config.get( 'ckbox' );
133
+
134
+ return {
135
+ theme: ckboxConfig.theme,
136
+ language: ckboxConfig.language,
137
+ tokenUrl: ckboxConfig.tokenUrl,
138
+ serviceOrigin: ckboxConfig.serviceOrigin,
139
+ assetsOrigin: ckboxConfig.assetsOrigin,
140
+ dialog: {
141
+ onClose: () => this.fire( 'ckbox:close' )
142
+ },
143
+ assets: {
144
+ onChoose: assets => this.fire( 'ckbox:choose', assets )
145
+ }
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Initializes various event listeners for the `ckbox:*` events, because all functionality of the `ckbox` command is event-based.
151
+ *
152
+ * @protected
153
+ */
154
+ _initListeners() {
155
+ const editor = this.editor;
156
+ const model = editor.model;
157
+ const shouldInsertDataId = !editor.config.get( 'ckbox.ignoreDataId' );
158
+
159
+ // Refresh the command after firing the `ckbox:*` event.
160
+ this.on( 'ckbox', () => {
161
+ this.refresh();
162
+ }, { priority: 'low' } );
163
+
164
+ // Handle opening of the CKBox dialog.
165
+ this.on( 'ckbox:open', () => {
166
+ if ( !this.isEnabled || this.value ) {
167
+ return;
168
+ }
169
+
170
+ this._wrapper = createElement( document, 'div', { class: 'ck ckbox-wrapper' } );
171
+ document.body.appendChild( this._wrapper );
172
+
173
+ window.CKBox.mount( this._wrapper, this._prepareOptions() );
174
+ } );
175
+
176
+ // Handle closing of the CKBox dialog.
177
+ this.on( 'ckbox:close', () => {
178
+ if ( !this.value ) {
179
+ return;
180
+ }
181
+
182
+ this._wrapper.remove();
183
+ this._wrapper = null;
184
+ } );
185
+
186
+ // Handle choosing the assets.
187
+ this.on( 'ckbox:choose', ( evt, assets ) => {
188
+ if ( !this.isEnabled ) {
189
+ return;
190
+ }
191
+
192
+ const imageCommand = editor.commands.get( 'insertImage' );
193
+ const linkCommand = editor.commands.get( 'link' );
194
+ const ckboxEditing = editor.plugins.get( 'CKBoxEditing' );
195
+ const assetsOrigin = editor.config.get( 'ckbox.assetsOrigin' );
196
+
197
+ const assetsToProcess = prepareAssets( {
198
+ assets,
199
+ origin: assetsOrigin,
200
+ token: ckboxEditing.getToken(),
201
+ isImageAllowed: imageCommand.isEnabled,
202
+ isLinkAllowed: linkCommand.isEnabled
203
+ } );
204
+
205
+ if ( assetsToProcess.length === 0 ) {
206
+ return;
207
+ }
208
+
209
+ // All assets are inserted in one undo step.
210
+ model.change( writer => {
211
+ for ( const asset of assetsToProcess ) {
212
+ const isLastAsset = asset === assetsToProcess[ assetsToProcess.length - 1 ];
213
+
214
+ this._insertAsset( asset, isLastAsset, writer );
215
+
216
+ // If asset ID must be set for the inserted model element, store the asset temporarily and remove it automatically
217
+ // after the timeout.
218
+ if ( shouldInsertDataId ) {
219
+ setTimeout( () => this._chosenAssets.delete( asset ), ASSET_INSERTION_WAIT_TIMEOUT );
220
+
221
+ this._chosenAssets.add( asset );
222
+ }
223
+ }
224
+ } );
225
+ } );
226
+
227
+ // Clean up after the editor is destroyed.
228
+ this.listenTo( editor, 'destroy', () => {
229
+ this.fire( 'ckbox:close' );
230
+ this._chosenAssets.clear();
231
+ } );
232
+ }
233
+
234
+ /**
235
+ * Inserts the asset into the model.
236
+ *
237
+ * @protected
238
+ * @param {Object} asset The asset to be inserted.
239
+ * @param {Boolean} isLastAsset Indicates if the current asset is the last one from the chosen set.
240
+ * @param {module:engine/model/writer~Writer} writer An instance of the model writer.
241
+ */
242
+ _insertAsset( asset, isLastAsset, writer ) {
243
+ const editor = this.editor;
244
+ const model = editor.model;
245
+ const selection = model.document.selection;
246
+
247
+ // Remove the `linkHref` attribute to not affect the asset to be inserted.
248
+ writer.removeSelectionAttribute( 'linkHref' );
249
+
250
+ if ( asset.type === 'image' ) {
251
+ this._insertImage( asset );
252
+ } else {
253
+ this._insertLink( asset, writer );
254
+ }
255
+
256
+ // Except for the last chosen asset, move the selection to the end of the current range to avoid overwriting other, already
257
+ // inserted assets.
258
+ if ( !isLastAsset ) {
259
+ writer.setSelection( selection.getLastPosition() );
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Inserts the image by calling the `insertImage` command.
265
+ *
266
+ * @protected
267
+ * @param {module:ckbox/ckbox~CKBoxAssetDefinition} asset The asset to be inserted.
268
+ */
269
+ _insertImage( asset ) {
270
+ const editor = this.editor;
271
+ const { imageFallbackUrl, imageSources, imageTextAlternative } = asset.attributes;
272
+
273
+ editor.execute( 'insertImage', {
274
+ source: {
275
+ src: imageFallbackUrl,
276
+ sources: imageSources,
277
+ alt: imageTextAlternative
278
+ }
279
+ } );
280
+ }
281
+
282
+ /**
283
+ * Inserts the link to the asset by calling the `link` command.
284
+ *
285
+ * @protected
286
+ * @param {module:ckbox/ckbox~CKBoxAssetDefinition} asset The asset to be inserted.
287
+ * @param {module:engine/model/writer~Writer} writer An instance of the model writer.
288
+ */
289
+ _insertLink( asset, writer ) {
290
+ const editor = this.editor;
291
+ const model = editor.model;
292
+ const selection = model.document.selection;
293
+ const { linkName, linkHref } = asset.attributes;
294
+
295
+ // If the selection is collapsed, insert the asset name as the link label and select it.
296
+ if ( selection.isCollapsed ) {
297
+ const selectionAttributes = toMap( selection.getAttributes() );
298
+ const textNode = writer.createText( linkName, selectionAttributes );
299
+ const range = model.insertContent( textNode );
300
+
301
+ writer.setSelection( range );
302
+ }
303
+
304
+ editor.execute( 'link', linkHref );
305
+ }
306
+ }
307
+
308
+ // Parses the chosen assets into the internal data format. Filters out chosen assets that are not allowed.
309
+ //
310
+ // @private
311
+ // @param {Object} data
312
+ // @param {Array.<module:ckbox/ckbox~CKBoxRawAssetDefinition>} data.assets
313
+ // @param {String} data.origin The base URL for assets inserted into the editor.
314
+ // @param {module:cloud-services/token~Token} data.token
315
+ // @param {Boolean} data.isImageAllowed
316
+ // @param {Boolean} data.isLinkAllowed
317
+ // @returns {Array.<module:ckbox/ckbox~CKBoxAssetDefinition>}
318
+ function prepareAssets( { assets, origin, token, isImageAllowed, isLinkAllowed } ) {
319
+ return assets
320
+ .map( asset => ( {
321
+ id: asset.data.id,
322
+ type: isImage( asset ) ? 'image' : 'link',
323
+ attributes: prepareAssetAttributes( asset, token, origin )
324
+ } ) )
325
+ .filter( asset => asset.type === 'image' ? isImageAllowed : isLinkAllowed );
326
+ }
327
+
328
+ // Parses the assets attributes into the internal data format.
329
+ //
330
+ // @private
331
+ // @param {module:ckbox/ckbox~CKBoxRawAssetDefinition} asset
332
+ // @param {module:cloud-services/token~Token} token
333
+ // @param {String} origin The base URL for assets inserted into the editor.
334
+ // @returns {module:ckbox/ckbox~CKBoxAssetImageAttributesDefinition|module:ckbox/ckbox~CKBoxAssetLinkAttributesDefinition}
335
+ function prepareAssetAttributes( asset, token, origin ) {
336
+ if ( isImage( asset ) ) {
337
+ const { imageFallbackUrl, imageSources } = getImageUrls( {
338
+ token,
339
+ origin,
340
+ id: asset.data.id,
341
+ width: asset.data.metadata.width,
342
+ extension: asset.data.extension
343
+ } );
344
+
345
+ return {
346
+ imageFallbackUrl,
347
+ imageSources,
348
+ imageTextAlternative: asset.data.metadata.description || ''
349
+ };
350
+ }
351
+
352
+ return {
353
+ linkName: asset.data.name,
354
+ linkHref: getAssetUrl( asset, token, origin )
355
+ };
356
+ }
357
+
358
+ // Checks whether the asset is an image.
359
+ //
360
+ // @private
361
+ // @param {module:ckbox/ckbox~CKBoxRawAssetDefinition} asset
362
+ // @returns {Boolean}
363
+ function isImage( asset ) {
364
+ const metadata = asset.data.metadata;
365
+
366
+ if ( !metadata ) {
367
+ return false;
368
+ }
369
+
370
+ return metadata.width && metadata.height;
371
+ }
372
+
373
+ // Creates the URL for the asset.
374
+ //
375
+ // @private
376
+ // @param {module:ckbox/ckbox~CKBoxRawAssetDefinition} asset
377
+ // @param {module:cloud-services/token~Token} token
378
+ // @param {String} origin The base URL for assets inserted into the editor.
379
+ // @returns {String}
380
+ function getAssetUrl( asset, token, origin ) {
381
+ const environmentId = getEnvironmentId( token );
382
+ const url = new URL( `${ environmentId }/assets/${ asset.data.id }/file`, origin );
383
+
384
+ url.searchParams.set( 'download', 'true' );
385
+
386
+ return url.toString();
387
+ }
388
+
389
+ /**
390
+ * Fired when the command is executed.
391
+ *
392
+ * @event ckbox:open
393
+ */
394
+
395
+ /**
396
+ * Fired when the CKBox dialog is closed.
397
+ *
398
+ * @event ckbox:close
399
+ */
400
+
401
+ /**
402
+ * Fired after the assets are chosen.
403
+ *
404
+ * @event ckbox:choose
405
+ * @param {Array.<module:ckbox/ckbox~CKBoxRawAssetDefinition>} assets Chosen assets.
406
+ */