]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blob - sources/plugins/widgetselection/plugin.js
Update to 4.7.3
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / plugins / widgetselection / plugin.js
1 /**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 /**
7 * @fileOverview A plugin created to handle ticket http://dev.ckeditor.com/ticket/11064. While the issue is caused by native WebKit/Blink behaviour,
8 * this plugin can be easily detached or modified when the issue is fixed in the browsers without changing the core.
9 * When Ctrl/Cmd + A is pressed to select all content it does not work due to a bug in
10 * Webkit/Blink if a non-editable element is at the beginning or the end of the content.
11 */
12
13 ( function() {
14 'use strict';
15
16 CKEDITOR.plugins.add( 'widgetselection', {
17
18 init: function( editor ) {
19 if ( CKEDITOR.env.webkit ) {
20 var widgetselection = CKEDITOR.plugins.widgetselection;
21
22 editor.on( 'contentDom', function( evt ) {
23
24 var editor = evt.editor,
25 doc = editor.document,
26 editable = editor.editable();
27
28 editable.attachListener( doc, 'keydown', function( evt ) {
29 var data = evt.data.$;
30
31 // Ctrl/Cmd + A
32 if ( evt.data.getKey() == 65 && ( CKEDITOR.env.mac && data.metaKey || !CKEDITOR.env.mac && data.ctrlKey ) ) {
33
34 // Defer the call so the selection is already changed by the pressed keys.
35 CKEDITOR.tools.setTimeout( function() {
36
37 // Manage filler elements on keydown. If there is no need
38 // to add fillers, we need to check and clean previously used once.
39 if ( !widgetselection.addFillers( editable ) ) {
40 widgetselection.removeFillers( editable );
41 }
42 }, 0 );
43 }
44 }, null, null, -1 );
45
46 // Check and clean previously used fillers.
47 editor.on( 'selectionCheck', function( evt ) {
48 widgetselection.removeFillers( evt.editor.editable() );
49 } );
50
51 // Remove fillers on paste before data gets inserted into editor.
52 editor.on( 'paste', function( evt ) {
53 evt.data.dataValue = widgetselection.cleanPasteData( evt.data.dataValue );
54 } );
55
56 if ( 'selectall' in editor.plugins ) {
57 widgetselection.addSelectAllIntegration( editor );
58 }
59 } );
60 }
61 }
62 } );
63
64 /**
65 * A set of helper methods for the Widget Selection plugin.
66 *
67 * @property widgetselection
68 * @member CKEDITOR.plugins
69 * @since 4.6.1
70 */
71 CKEDITOR.plugins.widgetselection = {
72
73 /**
74 * The start filler element reference.
75 *
76 * @property {CKEDITOR.dom.element}
77 * @member CKEDITOR.plugins.widgetselection
78 * @private
79 */
80 startFiller: null,
81
82 /**
83 * The end filler element reference.
84 *
85 * @property {CKEDITOR.dom.element}
86 * @member CKEDITOR.plugins.widgetselection
87 * @private
88 */
89 endFiller: null,
90
91 /**
92 * An attribute which identifies the filler element.
93 *
94 * @property {String}
95 * @member CKEDITOR.plugins.widgetselection
96 * @private
97 */
98 fillerAttribute: 'data-cke-filler-webkit',
99
100 /**
101 * The default content of the filler element. Note: The filler needs to have `visible` content.
102 * Unprintable elements or empty content do not help as a workaround.
103 *
104 * @property {String}
105 * @member CKEDITOR.plugins.widgetselection
106 * @private
107 */
108 fillerContent: ' ',
109
110 /**
111 * Tag name which is used to create fillers.
112 *
113 * @property {String}
114 * @member CKEDITOR.plugins.widgetselection
115 * @private
116 */
117 fillerTagName: 'div',
118
119 /**
120 * Adds a filler before or after a non-editable element at the beginning or the end of the `editable`.
121 *
122 * @param {CKEDITOR.editable} editable
123 * @returns {Boolean}
124 * @member CKEDITOR.plugins.widgetselection
125 */
126 addFillers: function( editable ) {
127 var editor = editable.editor;
128
129 // Whole content should be selected, if not fix the selection manually.
130 if ( !this.isWholeContentSelected( editable ) && editable.getChildCount() > 0 ) {
131
132 var firstChild = editable.getFirst( filterTempElements ),
133 lastChild = editable.getLast( filterTempElements );
134
135 // Check if first element is editable. If not prepend with filler.
136 if ( firstChild && firstChild.type == CKEDITOR.NODE_ELEMENT && !firstChild.isEditable() ) {
137 this.startFiller = this.createFiller();
138 editable.append( this.startFiller, 1 );
139 }
140
141 // Check if last element is editable. If not append filler.
142 if ( lastChild && lastChild.type == CKEDITOR.NODE_ELEMENT && !lastChild.isEditable() ) {
143 this.endFiller = this.createFiller( true );
144 editable.append( this.endFiller, 0 );
145 }
146
147 // Reselect whole content after any filler was added.
148 if ( this.hasFiller( editable ) ) {
149 var rangeAll = editor.createRange();
150 rangeAll.selectNodeContents( editable );
151 rangeAll.select();
152 return true;
153 }
154 }
155 return false;
156 },
157
158 /**
159 * Removes filler elements or updates their references.
160 *
161 * It will **not remove** filler elements if the whole content is selected, as it would break the
162 * selection.
163 *
164 * @param {CKEDITOR.editable} editable
165 * @member CKEDITOR.plugins.widgetselection
166 */
167 removeFillers: function( editable ) {
168 // If startFiller or endFiller exists and not entire content is selected it means the selection
169 // just changed from selected all. We need to remove fillers and set proper selection/content.
170 if ( this.hasFiller( editable ) && !this.isWholeContentSelected( editable ) ) {
171
172 var startFillerContent = editable.findOne( this.fillerTagName + '[' + this.fillerAttribute + '=start]' ),
173 endFillerContent = editable.findOne( this.fillerTagName + '[' + this.fillerAttribute + '=end]' );
174
175 if ( this.startFiller && startFillerContent && this.startFiller.equals( startFillerContent ) ) {
176 this.removeFiller( this.startFiller, editable );
177 } else {
178 // The start filler is still present but it is a different element than previous one. It means the
179 // undo recreating entirely selected content was performed. We need to update filler reference.
180 this.startFiller = startFillerContent;
181 }
182
183 if ( this.endFiller && endFillerContent && this.endFiller.equals( endFillerContent ) ) {
184 this.removeFiller( this.endFiller, editable );
185 } else {
186 // Same as with start filler.
187 this.endFiller = endFillerContent;
188 }
189 }
190 },
191
192 /**
193 * Removes fillers from the paste data.
194 *
195 * @param {String} data
196 * @returns {String}
197 * @member CKEDITOR.plugins.widgetselection
198 * @private
199 */
200 cleanPasteData: function( data ) {
201 if ( data && data.length ) {
202 data = data
203 .replace( this.createFillerRegex(), '' )
204 .replace( this.createFillerRegex( true ), '' );
205 }
206 return data;
207 },
208
209 /**
210 * Checks if the entire content of the given editable is selected.
211 *
212 * @param {CKEDITOR.editable} editable
213 * @returns {Boolean}
214 * @member CKEDITOR.plugins.widgetselection
215 * @private
216 */
217 isWholeContentSelected: function( editable ) {
218
219 var range = editable.editor.getSelection().getRanges()[ 0 ];
220 if ( range ) {
221
222 if ( range && range.collapsed ) {
223 return false;
224
225 } else {
226 var rangeClone = range.clone();
227 rangeClone.enlarge( CKEDITOR.ENLARGE_ELEMENT );
228
229 return !!( rangeClone && editable && rangeClone.startContainer && rangeClone.endContainer &&
230 rangeClone.startOffset === 0 && rangeClone.endOffset === editable.getChildCount() &&
231 rangeClone.startContainer.equals( editable ) && rangeClone.endContainer.equals( editable ) );
232 }
233 }
234 return false;
235 },
236
237 /**
238 * Checks if there is any filler element in the given editable.
239 *
240 * @param {CKEDITOR.editable} editable
241 * @returns {Boolean}
242 * @member CKEDITOR.plugins.widgetselection
243 * @private
244 */
245 hasFiller: function( editable ) {
246 return editable.find( this.fillerTagName + '[' + this.fillerAttribute + ']' ).count() > 0;
247 },
248
249 /**
250 * Creates a filler element.
251 *
252 * @param {Boolean} [onEnd] If filler will be placed on end or beginning of the content.
253 * @returns {CKEDITOR.dom.element}
254 * @member CKEDITOR.plugins.widgetselection
255 * @private
256 */
257 createFiller: function( onEnd ) {
258 var filler = new CKEDITOR.dom.element( this.fillerTagName );
259 filler.setHtml( this.fillerContent );
260 filler.setAttribute( this.fillerAttribute, onEnd ? 'end' : 'start' );
261 filler.setAttribute( 'data-cke-temp', 1 );
262 filler.setStyles( {
263 display: 'block',
264 width: 0,
265 height: 0,
266 padding: 0,
267 border: 0,
268 margin: 0,
269 position: 'absolute',
270 top: 0,
271 left: '-9999px',
272 opacity: 0,
273 overflow: 'hidden'
274 } );
275
276 return filler;
277 },
278
279 /**
280 * Removes the specific filler element from the given editable. If the filler contains any content (typed or pasted),
281 * it replaces the current editable content. If not, the caret is placed before the first or after the last editable
282 * element (depends if the filler was at the beginning or the end).
283 *
284 * @param {CKEDITOR.dom.element} filler
285 * @param {CKEDITOR.editable} editable
286 * @member CKEDITOR.plugins.widgetselection
287 * @private
288 */
289 removeFiller: function( filler, editable ) {
290 if ( filler ) {
291 var editor = editable.editor,
292 currentRange = editable.editor.getSelection().getRanges()[ 0 ],
293 currentPath = currentRange.startPath(),
294 range = editor.createRange(),
295 insertedHtml,
296 fillerOnStart,
297 manuallyHandleCaret;
298
299 if ( currentPath.contains( filler ) ) {
300 insertedHtml = filler.getHtml();
301 manuallyHandleCaret = true;
302 }
303
304 fillerOnStart = filler.getAttribute( this.fillerAttribute ) == 'start';
305 filler.remove();
306 filler = null;
307
308 if ( insertedHtml && insertedHtml.length > 0 && insertedHtml != this.fillerContent ) {
309 editable.insertHtmlIntoRange( insertedHtml, editor.getSelection().getRanges()[ 0 ] );
310 range.setStartAt( editable.getChild( editable.getChildCount() - 1 ), CKEDITOR.POSITION_BEFORE_END );
311 editor.getSelection().selectRanges( [ range ] );
312
313 } else if ( manuallyHandleCaret ) {
314 if ( fillerOnStart ) {
315 range.setStartAt( editable.getFirst().getNext(), CKEDITOR.POSITION_AFTER_START );
316 } else {
317 range.setEndAt( editable.getLast().getPrevious(), CKEDITOR.POSITION_BEFORE_END );
318 }
319 editable.editor.getSelection().selectRanges( [ range ] );
320 }
321 }
322 },
323
324 /**
325 * Creates a regular expression which will match the filler HTML in the text.
326 *
327 * @param {Boolean} [onEnd] Whether a regular expression should be created for the filler at the beginning or
328 * the end of the content.
329 * @returns {RegExp}
330 * @member CKEDITOR.plugins.widgetselection
331 * @private
332 */
333 createFillerRegex: function( onEnd ) {
334 var matcher = this.createFiller( onEnd ).getOuterHtml()
335 .replace( /style="[^"]*"/gi, 'style="[^"]*"' )
336 .replace( />[^<]*</gi, '>[^<]*<' );
337
338 return new RegExp( ( !onEnd ? '^' : '' ) + matcher + ( onEnd ? '$' : '' ) );
339 },
340
341 /**
342 * Adds an integration for the [Select All](http://ckeditor.com/addon/selectall) plugin to the given `editor`.
343 *
344 * @private
345 * @param {CKEDITOR.editor} editor
346 * @member CKEDITOR.plugins.widgetselection
347 */
348 addSelectAllIntegration: function( editor ) {
349 var widgetselection = this;
350
351 editor.editable().attachListener( editor, 'beforeCommandExec', function( evt ) {
352 var editable = editor.editable();
353
354 if ( evt.data.name == 'selectAll' && editable ) {
355 widgetselection.addFillers( editable );
356 }
357 }, null, null, 9999 );
358 }
359 };
360
361
362 function filterTempElements( el ) {
363 return el.getName && !el.hasAttribute( 'data-cke-temp' );
364 }
365
366 } )();