]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blob - sources/plugins/div/dialogs/div.js
Update to 4.7.3
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / plugins / div / dialogs / div.js
1 /*
2 * Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 ( function() {
7
8 // Add to collection with DUP examination.
9 // @param {Object} collection
10 // @param {Object} element
11 // @param {Object} database
12 function addSafely( collection, element, database ) {
13 // 1. IE doesn't support customData on text nodes;
14 // 2. Text nodes never get chance to appear twice;
15 if ( !element.is || !element.getCustomData( 'block_processed' ) ) {
16 element.is && CKEDITOR.dom.element.setMarker( database, element, 'block_processed', true );
17 collection.push( element );
18 }
19 }
20
21 // Dialog reused by both 'creatediv' and 'editdiv' commands.
22 // @param {Object} editor
23 // @param {String} command The command name which indicate what the current command is.
24 function divDialog( editor, command ) {
25 // Definition of elements at which div operation should stopped.
26 var divLimitDefinition = ( function() {
27
28 // Customzie from specialize blockLimit elements
29 var definition = CKEDITOR.tools.extend( {}, CKEDITOR.dtd.$blockLimit );
30
31 if ( editor.config.div_wrapTable ) {
32 delete definition.td;
33 delete definition.th;
34 }
35 return definition;
36 } )();
37
38 // DTD of 'div' element
39 var dtd = CKEDITOR.dtd.div;
40
41 // Get the first div limit element on the element's path.
42 // @param {Object} element
43 function getDivContainer( element ) {
44 var container = editor.elementPath( element ).blockLimit;
45
46 // Never consider read-only (i.e. contenteditable=false) element as
47 // a first div limit (http://dev.ckeditor.com/ticket/11083).
48 if ( container.isReadOnly() )
49 container = container.getParent();
50
51 // Dont stop at 'td' and 'th' when div should wrap entire table.
52 if ( editor.config.div_wrapTable && container.is( [ 'td', 'th' ] ) ) {
53 var parentPath = editor.elementPath( container.getParent() );
54 container = parentPath.blockLimit;
55 }
56
57 return container;
58 }
59
60 // Init all fields' setup/commit function.
61 // @memberof divDialog
62 function setupFields() {
63 this.foreach( function( field ) {
64 // Exclude layout container elements
65 if ( /^(?!vbox|hbox)/.test( field.type ) ) {
66 if ( !field.setup ) {
67 // Read the dialog fields values from the specified
68 // element attributes.
69 field.setup = function( element ) {
70 field.setValue( element.getAttribute( field.id ) || '', 1 );
71 };
72 }
73 if ( !field.commit ) {
74 // Set element attributes assigned by the dialog
75 // fields.
76 field.commit = function( element ) {
77 var fieldValue = this.getValue();
78 // ignore default element attribute values
79 if ( field.id == 'dir' && element.getComputedStyle( 'direction' ) == fieldValue ) {
80 return;
81 }
82
83 if ( fieldValue )
84 element.setAttribute( field.id, fieldValue );
85 else
86 element.removeAttribute( field.id );
87 };
88 }
89 }
90 } );
91 }
92
93 // Wrapping 'div' element around appropriate blocks among the selected ranges.
94 // @param {Object} editor
95 function createDiv( editor ) {
96 // new adding containers OR detected pre-existed containers.
97 var containers = [];
98 // node markers store.
99 var database = {};
100 // All block level elements which contained by the ranges.
101 var containedBlocks = [],
102 block;
103
104 // Get all ranges from the selection.
105 var selection = editor.getSelection(),
106 ranges = selection.getRanges();
107 var bookmarks = selection.createBookmarks();
108 var i, iterator;
109
110 // collect all included elements from dom-iterator
111 for ( i = 0; i < ranges.length; i++ ) {
112 iterator = ranges[ i ].createIterator();
113 while ( ( block = iterator.getNextParagraph() ) ) {
114 // include contents of blockLimit elements.
115 if ( block.getName() in divLimitDefinition && !block.isReadOnly() ) {
116 var j,
117 childNodes = block.getChildren();
118 for ( j = 0; j < childNodes.count(); j++ )
119 addSafely( containedBlocks, childNodes.getItem( j ), database );
120 } else {
121 while ( !dtd[ block.getName() ] && !block.equals( ranges[ i ].root ) )
122 block = block.getParent();
123 addSafely( containedBlocks, block, database );
124 }
125 }
126 }
127
128 CKEDITOR.dom.element.clearAllMarkers( database );
129
130 var blockGroups = groupByDivLimit( containedBlocks );
131 var ancestor, divElement;
132
133 for ( i = 0; i < blockGroups.length; i++ ) {
134 // Sometimes we could get empty block group if all elements inside it
135 // don't have parent's nodes (http://dev.ckeditor.com/ticket/13585).
136 if ( !blockGroups[ i ].length ) {
137 continue;
138 }
139
140 var currentNode = blockGroups[ i ][ 0 ];
141
142 // Calculate the common parent node of all contained elements.
143 ancestor = currentNode.getParent();
144 for ( j = 1; j < blockGroups[ i ].length; j++ ) {
145 ancestor = ancestor.getCommonAncestor( blockGroups[ i ][ j ] );
146 }
147
148 // If there is no ancestor, mark editable as one (http://dev.ckeditor.com/ticket/13585).
149 if ( !ancestor ) {
150 ancestor = editor.editable();
151 }
152
153 divElement = new CKEDITOR.dom.element( 'div', editor.document );
154
155 // Normalize the blocks in each group to a common parent.
156 for ( j = 0; j < blockGroups[ i ].length; j++ ) {
157 currentNode = blockGroups[ i ][ j ];
158
159 // Check if the currentNode has a parent before attempting to operate on it (http://dev.ckeditor.com/ticket/13585).
160 while ( currentNode.getParent() && !currentNode.getParent().equals( ancestor ) ) {
161 currentNode = currentNode.getParent();
162 }
163
164 // This could introduce some duplicated elements in array.
165 blockGroups[ i ][ j ] = currentNode;
166 }
167
168 // Wrapped blocks counting
169 for ( j = 0; j < blockGroups[ i ].length; j++ ) {
170 currentNode = blockGroups[ i ][ j ];
171
172 // Avoid DUP elements introduced by grouping.
173 if ( !( currentNode.getCustomData && currentNode.getCustomData( 'block_processed' ) ) ) {
174 currentNode.is && CKEDITOR.dom.element.setMarker( database, currentNode, 'block_processed', true );
175
176 // Establish new container, wrapping all elements in this group.
177 if ( !j ) {
178 divElement.insertBefore( currentNode );
179 }
180
181 divElement.append( currentNode );
182 }
183 }
184
185 CKEDITOR.dom.element.clearAllMarkers( database );
186 containers.push( divElement );
187 }
188
189 selection.selectBookmarks( bookmarks );
190 return containers;
191 }
192
193 // Divide a set of nodes to different groups by their path's blocklimit element.
194 // Note: the specified nodes should be in source order naturally, which mean they are supposed to producea by following class:
195 // * CKEDITOR.dom.range.Iterator
196 // * CKEDITOR.dom.domWalker
197 // @returns {Array[]} the grouped nodes
198 function groupByDivLimit( nodes ) {
199 var groups = [],
200 lastDivLimit = null,
201 block;
202
203 for ( var i = 0; i < nodes.length; i++ ) {
204 block = nodes[ i ];
205 var limit = getDivContainer( block );
206 if ( !limit.equals( lastDivLimit ) ) {
207 lastDivLimit = limit;
208 groups.push( [] );
209 }
210
211 // Sometimes we got nodes that are not inside the DOM, which causes error (http://dev.ckeditor.com/ticket/13585).
212 if ( block.getParent() ) {
213 groups[ groups.length - 1 ].push( block );
214 }
215 }
216
217 return groups;
218 }
219
220 // Synchronous field values to other impacted fields is required, e.g. div styles
221 // change should also alter inline-style text.
222 function commitInternally( targetFields ) {
223 var dialog = this.getDialog(),
224 element = dialog._element && dialog._element.clone() || new CKEDITOR.dom.element( 'div', editor.document );
225
226 // Commit this field and broadcast to target fields.
227 this.commit( element, true );
228
229 targetFields = [].concat( targetFields );
230 var length = targetFields.length,
231 field;
232 for ( var i = 0; i < length; i++ ) {
233 field = dialog.getContentElement.apply( dialog, targetFields[ i ].split( ':' ) );
234 field && field.setup && field.setup( element, true );
235 }
236 }
237
238
239 // Registered 'CKEDITOR.style' instances.
240 var styles = {};
241
242 // Hold a collection of created block container elements.
243 var containers = [];
244
245 // @type divDialog
246 return {
247 title: editor.lang.div.title,
248 minWidth: 400,
249 minHeight: 165,
250 contents: [ {
251 id: 'info',
252 label: editor.lang.common.generalTab,
253 title: editor.lang.common.generalTab,
254 elements: [ {
255 type: 'hbox',
256 widths: [ '50%', '50%' ],
257 children: [ {
258 id: 'elementStyle',
259 type: 'select',
260 style: 'width: 100%;',
261 label: editor.lang.div.styleSelectLabel,
262 'default': '',
263 // Options are loaded dynamically.
264 items: [
265 [ editor.lang.common.notSet, '' ]
266 ],
267 onChange: function() {
268 commitInternally.call( this, [ 'info:elementStyle', 'info:class', 'advanced:dir', 'advanced:style' ] );
269 },
270 setup: function( element ) {
271 for ( var name in styles )
272 styles[ name ].checkElementRemovable( element, true, editor ) && this.setValue( name, 1 );
273 },
274 commit: function( element ) {
275 var styleName;
276 if ( ( styleName = this.getValue() ) ) {
277 var style = styles[ styleName ];
278 style.applyToObject( element, editor );
279 }
280 else {
281 element.removeAttribute( 'style' );
282 }
283 }
284 },
285 {
286 id: 'class',
287 type: 'text',
288 requiredContent: 'div(cke-xyz)', // Random text like 'xyz' will check if all are allowed.
289 label: editor.lang.common.cssClass,
290 'default': ''
291 } ]
292 } ]
293 },
294 {
295 id: 'advanced',
296 label: editor.lang.common.advancedTab,
297 title: editor.lang.common.advancedTab,
298 elements: [ {
299 type: 'vbox',
300 padding: 1,
301 children: [ {
302 type: 'hbox',
303 widths: [ '50%', '50%' ],
304 children: [ {
305 type: 'text',
306 id: 'id',
307 requiredContent: 'div[id]',
308 label: editor.lang.common.id,
309 'default': ''
310 },
311 {
312 type: 'text',
313 id: 'lang',
314 requiredContent: 'div[lang]',
315 label: editor.lang.common.langCode,
316 'default': ''
317 } ]
318 },
319 {
320 type: 'hbox',
321 children: [ {
322 type: 'text',
323 id: 'style',
324 requiredContent: 'div{cke-xyz}', // Random text like 'xyz' will check if all are allowed.
325 style: 'width: 100%;',
326 label: editor.lang.common.cssStyle,
327 'default': '',
328 commit: function( element ) {
329 element.setAttribute( 'style', this.getValue() );
330 }
331 } ]
332 },
333 {
334 type: 'hbox',
335 children: [ {
336 type: 'text',
337 id: 'title',
338 requiredContent: 'div[title]',
339 style: 'width: 100%;',
340 label: editor.lang.common.advisoryTitle,
341 'default': ''
342 } ]
343 },
344 {
345 type: 'select',
346 id: 'dir',
347 requiredContent: 'div[dir]',
348 style: 'width: 100%;',
349 label: editor.lang.common.langDir,
350 'default': '',
351 items: [
352 [ editor.lang.common.notSet, '' ],
353 [ editor.lang.common.langDirLtr, 'ltr' ],
354 [ editor.lang.common.langDirRtl, 'rtl' ]
355 ]
356 } ] }
357 ]
358 } ],
359 onLoad: function() {
360 setupFields.call( this );
361
362 // Preparing for the 'elementStyle' field.
363 var dialog = this,
364 stylesField = this.getContentElement( 'info', 'elementStyle' );
365
366 // Reuse the 'stylescombo' plugin's styles definition.
367 editor.getStylesSet( function( stylesDefinitions ) {
368 var styleName, style;
369
370 if ( stylesDefinitions ) {
371 // Digg only those styles that apply to 'div'.
372 for ( var i = 0; i < stylesDefinitions.length; i++ ) {
373 var styleDefinition = stylesDefinitions[ i ];
374 if ( styleDefinition.element && styleDefinition.element == 'div' ) {
375 styleName = styleDefinition.name;
376 styles[ styleName ] = style = new CKEDITOR.style( styleDefinition );
377
378 if ( editor.filter.check( style ) ) {
379 // Populate the styles field options with style name.
380 stylesField.items.push( [ styleName, styleName ] );
381 stylesField.add( styleName, styleName );
382 }
383 }
384 }
385 }
386
387 // We should disable the content element
388 // it if no options are available at all.
389 stylesField[ stylesField.items.length > 1 ? 'enable' : 'disable' ]();
390
391 // Now setup the field value manually if dialog was opened on element. (http://dev.ckeditor.com/ticket/9689)
392 setTimeout( function() {
393 dialog._element && stylesField.setup( dialog._element );
394 }, 0 );
395 } );
396 },
397 onShow: function() {
398 // Whether always create new container regardless of existed
399 // ones.
400 if ( command == 'editdiv' ) {
401 // Try to discover the containers that already existed in
402 // ranges
403 // update dialog field values
404 this.setupContent( this._element = CKEDITOR.plugins.div.getSurroundDiv( editor ) );
405 }
406 },
407 onOk: function() {
408 if ( command == 'editdiv' )
409 containers = [ this._element ];
410 else
411 containers = createDiv( editor, true );
412
413 // Update elements attributes
414 var size = containers.length;
415 for ( var i = 0; i < size; i++ ) {
416 this.commitContent( containers[ i ] );
417
418 // Remove empty 'style' attribute.
419 !containers[ i ].getAttribute( 'style' ) && containers[ i ].removeAttribute( 'style' );
420 }
421
422 this.hide();
423 },
424 onHide: function() {
425 // Remove style only when editing existing DIV. (http://dev.ckeditor.com/ticket/6315)
426 if ( command == 'editdiv' )
427 this._element.removeCustomData( 'elementStyle' );
428 delete this._element;
429 }
430 };
431 }
432
433 CKEDITOR.dialog.add( 'creatediv', function( editor ) {
434 return divDialog( editor, 'creatediv' );
435 } );
436
437 CKEDITOR.dialog.add( 'editdiv', function( editor ) {
438 return divDialog( editor, 'editdiv' );
439 } );
440
441 } )();
442
443 /**
444 * Whether to wrap the entire table instead of individual cells when creating a `<div>` in a table cell.
445 *
446 * config.div_wrapTable = true;
447 *
448 * @cfg {Boolean} [div_wrapTable=false]
449 * @member CKEDITOR.config
450 */