diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-01-25 17:45:33 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-01-25 18:00:33 +0100 |
commit | 7adcb81e4f83f98c468889aaa5a85558ba88c770 (patch) | |
tree | 0d6ede733777b29060b48df4afaa2c64bfbae276 /sources/plugins/tabletools | |
download | connexionswing-ckeditor-component-7adcb81e4f83f98c468889aaa5a85558ba88c770.tar.gz connexionswing-ckeditor-component-7adcb81e4f83f98c468889aaa5a85558ba88c770.tar.zst connexionswing-ckeditor-component-7adcb81e4f83f98c468889aaa5a85558ba88c770.zip |
Initial commit4.5.6
Diffstat (limited to 'sources/plugins/tabletools')
-rw-r--r-- | sources/plugins/tabletools/dialogs/tableCell.js | 474 | ||||
-rw-r--r-- | sources/plugins/tabletools/plugin.js | 1005 |
2 files changed, 1479 insertions, 0 deletions
diff --git a/sources/plugins/tabletools/dialogs/tableCell.js b/sources/plugins/tabletools/dialogs/tableCell.js new file mode 100644 index 00000000..980d3486 --- /dev/null +++ b/sources/plugins/tabletools/dialogs/tableCell.js | |||
@@ -0,0 +1,474 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | CKEDITOR.dialog.add( 'cellProperties', function( editor ) { | ||
7 | var langTable = editor.lang.table, | ||
8 | langCell = langTable.cell, | ||
9 | langCommon = editor.lang.common, | ||
10 | validate = CKEDITOR.dialog.validate, | ||
11 | widthPattern = /^(\d+(?:\.\d+)?)(px|%)$/, | ||
12 | spacer = { type: 'html', html: ' ' }, | ||
13 | rtl = editor.lang.dir == 'rtl', | ||
14 | colorDialog = editor.plugins.colordialog; | ||
15 | |||
16 | // Returns a function, which runs regular "setup" for all selected cells to find out | ||
17 | // whether the initial value of the field would be the same for all cells. If so, | ||
18 | // the value is displayed just as if a regular "setup" was executed. Otherwise, | ||
19 | // i.e. when there are several cells of different value of the property, a field | ||
20 | // gets empty value. | ||
21 | // | ||
22 | // * @param {Function} setup Setup function which returns a value instead of setting it. | ||
23 | // * @returns {Function} A function to be used in dialog definition. | ||
24 | function setupCells( setup ) { | ||
25 | return function( cells ) { | ||
26 | var fieldValue = setup( cells[ 0 ] ); | ||
27 | |||
28 | // If one of the cells would have a different value of the | ||
29 | // property, set the empty value for a field. | ||
30 | for ( var i = 1; i < cells.length; i++ ) { | ||
31 | if ( setup( cells[ i ] ) !== fieldValue ) { | ||
32 | fieldValue = null; | ||
33 | break; | ||
34 | } | ||
35 | } | ||
36 | |||
37 | // Setting meaningful or empty value only makes sense | ||
38 | // when setup returns some value. Otherwise, a *default* value | ||
39 | // is used for that field. | ||
40 | if ( typeof fieldValue != 'undefined' ) { | ||
41 | this.setValue( fieldValue ); | ||
42 | |||
43 | // The only way to have an empty select value in Firefox is | ||
44 | // to set a negative selectedIndex. | ||
45 | if ( CKEDITOR.env.gecko && this.type == 'select' && !fieldValue ) | ||
46 | this.getInputElement().$.selectedIndex = -1; | ||
47 | } | ||
48 | }; | ||
49 | } | ||
50 | |||
51 | // Reads the unit of width property of the table cell. | ||
52 | // | ||
53 | // * @param {CKEDITOR.dom.element} cell An element representing table cell. | ||
54 | // * @returns {String} A unit of width: 'px', '%' or undefined if none. | ||
55 | function getCellWidthType( cell ) { | ||
56 | var match = widthPattern.exec( | ||
57 | cell.getStyle( 'width' ) || cell.getAttribute( 'width' ) ); | ||
58 | |||
59 | if ( match ) | ||
60 | return match[ 2 ]; | ||
61 | } | ||
62 | |||
63 | return { | ||
64 | title: langCell.title, | ||
65 | minWidth: CKEDITOR.env.ie && CKEDITOR.env.quirks ? 450 : 410, | ||
66 | minHeight: CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ? 230 : 220, | ||
67 | contents: [ { | ||
68 | id: 'info', | ||
69 | label: langCell.title, | ||
70 | accessKey: 'I', | ||
71 | elements: [ { | ||
72 | type: 'hbox', | ||
73 | widths: [ '40%', '5%', '40%' ], | ||
74 | children: [ { | ||
75 | type: 'vbox', | ||
76 | padding: 0, | ||
77 | children: [ { | ||
78 | type: 'hbox', | ||
79 | widths: [ '70%', '30%' ], | ||
80 | children: [ { | ||
81 | type: 'text', | ||
82 | id: 'width', | ||
83 | width: '100px', | ||
84 | label: langCommon.width, | ||
85 | validate: validate.number( langCell.invalidWidth ), | ||
86 | |||
87 | // Extra labelling of width unit type. | ||
88 | onLoad: function() { | ||
89 | var widthType = this.getDialog().getContentElement( 'info', 'widthType' ), | ||
90 | labelElement = widthType.getElement(), | ||
91 | inputElement = this.getInputElement(), | ||
92 | ariaLabelledByAttr = inputElement.getAttribute( 'aria-labelledby' ); | ||
93 | |||
94 | inputElement.setAttribute( 'aria-labelledby', [ ariaLabelledByAttr, labelElement.$.id ].join( ' ' ) ); | ||
95 | }, | ||
96 | |||
97 | setup: setupCells( function( element ) { | ||
98 | var widthAttr = parseInt( element.getAttribute( 'width' ), 10 ), | ||
99 | widthStyle = parseInt( element.getStyle( 'width' ), 10 ); | ||
100 | |||
101 | return !isNaN( widthStyle ) ? widthStyle : | ||
102 | !isNaN( widthAttr ) ? widthAttr : ''; | ||
103 | } ), | ||
104 | commit: function( element ) { | ||
105 | var value = parseInt( this.getValue(), 10 ), | ||
106 | |||
107 | // There might be no widthType value, i.e. when multiple cells are | ||
108 | // selected but some of them have width expressed in pixels and some | ||
109 | // of them in percent. Try to re-read the unit from the cell in such | ||
110 | // case (#11439). | ||
111 | unit = this.getDialog().getValueOf( 'info', 'widthType' ) || getCellWidthType( element ); | ||
112 | |||
113 | if ( !isNaN( value ) ) | ||
114 | element.setStyle( 'width', value + unit ); | ||
115 | else | ||
116 | element.removeStyle( 'width' ); | ||
117 | |||
118 | element.removeAttribute( 'width' ); | ||
119 | }, | ||
120 | 'default': '' | ||
121 | }, | ||
122 | { | ||
123 | type: 'select', | ||
124 | id: 'widthType', | ||
125 | label: editor.lang.table.widthUnit, | ||
126 | labelStyle: 'visibility:hidden', | ||
127 | 'default': 'px', | ||
128 | items: [ | ||
129 | [ langTable.widthPx, 'px' ], | ||
130 | [ langTable.widthPc, '%' ] | ||
131 | ], | ||
132 | setup: setupCells( getCellWidthType ) | ||
133 | } ] | ||
134 | }, | ||
135 | { | ||
136 | type: 'hbox', | ||
137 | widths: [ '70%', '30%' ], | ||
138 | children: [ { | ||
139 | type: 'text', | ||
140 | id: 'height', | ||
141 | label: langCommon.height, | ||
142 | width: '100px', | ||
143 | 'default': '', | ||
144 | validate: validate.number( langCell.invalidHeight ), | ||
145 | |||
146 | // Extra labelling of height unit type. | ||
147 | onLoad: function() { | ||
148 | var heightType = this.getDialog().getContentElement( 'info', 'htmlHeightType' ), | ||
149 | labelElement = heightType.getElement(), | ||
150 | inputElement = this.getInputElement(), | ||
151 | ariaLabelledByAttr = inputElement.getAttribute( 'aria-labelledby' ); | ||
152 | |||
153 | inputElement.setAttribute( 'aria-labelledby', [ ariaLabelledByAttr, labelElement.$.id ].join( ' ' ) ); | ||
154 | }, | ||
155 | |||
156 | setup: setupCells( function( element ) { | ||
157 | var heightAttr = parseInt( element.getAttribute( 'height' ), 10 ), | ||
158 | heightStyle = parseInt( element.getStyle( 'height' ), 10 ); | ||
159 | |||
160 | return !isNaN( heightStyle ) ? heightStyle : | ||
161 | !isNaN( heightAttr ) ? heightAttr : ''; | ||
162 | } ), | ||
163 | commit: function( element ) { | ||
164 | var value = parseInt( this.getValue(), 10 ); | ||
165 | |||
166 | if ( !isNaN( value ) ) | ||
167 | element.setStyle( 'height', CKEDITOR.tools.cssLength( value ) ); | ||
168 | else | ||
169 | element.removeStyle( 'height' ); | ||
170 | |||
171 | element.removeAttribute( 'height' ); | ||
172 | } | ||
173 | }, | ||
174 | { | ||
175 | id: 'htmlHeightType', | ||
176 | type: 'html', | ||
177 | html: '<br />' + langTable.widthPx | ||
178 | } ] | ||
179 | }, | ||
180 | spacer, | ||
181 | { | ||
182 | type: 'select', | ||
183 | id: 'wordWrap', | ||
184 | label: langCell.wordWrap, | ||
185 | 'default': 'yes', | ||
186 | items: [ | ||
187 | [ langCell.yes, 'yes' ], | ||
188 | [ langCell.no, 'no' ] | ||
189 | ], | ||
190 | setup: setupCells( function( element ) { | ||
191 | var wordWrapAttr = element.getAttribute( 'noWrap' ), | ||
192 | wordWrapStyle = element.getStyle( 'white-space' ); | ||
193 | |||
194 | if ( wordWrapStyle == 'nowrap' || wordWrapAttr ) | ||
195 | return 'no'; | ||
196 | } ), | ||
197 | commit: function( element ) { | ||
198 | if ( this.getValue() == 'no' ) | ||
199 | element.setStyle( 'white-space', 'nowrap' ); | ||
200 | else | ||
201 | element.removeStyle( 'white-space' ); | ||
202 | |||
203 | element.removeAttribute( 'noWrap' ); | ||
204 | } | ||
205 | }, | ||
206 | spacer, | ||
207 | { | ||
208 | type: 'select', | ||
209 | id: 'hAlign', | ||
210 | label: langCell.hAlign, | ||
211 | 'default': '', | ||
212 | items: [ | ||
213 | [ langCommon.notSet, '' ], | ||
214 | [ langCommon.alignLeft, 'left' ], | ||
215 | [ langCommon.alignCenter, 'center' ], | ||
216 | [ langCommon.alignRight, 'right' ], | ||
217 | [ langCommon.alignJustify, 'justify' ] | ||
218 | ], | ||
219 | setup: setupCells( function( element ) { | ||
220 | var alignAttr = element.getAttribute( 'align' ), | ||
221 | textAlignStyle = element.getStyle( 'text-align' ); | ||
222 | |||
223 | return textAlignStyle || alignAttr || ''; | ||
224 | } ), | ||
225 | commit: function( selectedCell ) { | ||
226 | var value = this.getValue(); | ||
227 | |||
228 | if ( value ) | ||
229 | selectedCell.setStyle( 'text-align', value ); | ||
230 | else | ||
231 | selectedCell.removeStyle( 'text-align' ); | ||
232 | |||
233 | selectedCell.removeAttribute( 'align' ); | ||
234 | } | ||
235 | }, | ||
236 | { | ||
237 | type: 'select', | ||
238 | id: 'vAlign', | ||
239 | label: langCell.vAlign, | ||
240 | 'default': '', | ||
241 | items: [ | ||
242 | [ langCommon.notSet, '' ], | ||
243 | [ langCommon.alignTop, 'top' ], | ||
244 | [ langCommon.alignMiddle, 'middle' ], | ||
245 | [ langCommon.alignBottom, 'bottom' ], | ||
246 | [ langCell.alignBaseline, 'baseline' ] | ||
247 | ], | ||
248 | setup: setupCells( function( element ) { | ||
249 | var vAlignAttr = element.getAttribute( 'vAlign' ), | ||
250 | vAlignStyle = element.getStyle( 'vertical-align' ); | ||
251 | |||
252 | switch ( vAlignStyle ) { | ||
253 | // Ignore all other unrelated style values.. | ||
254 | case 'top': | ||
255 | case 'middle': | ||
256 | case 'bottom': | ||
257 | case 'baseline': | ||
258 | break; | ||
259 | default: | ||
260 | vAlignStyle = ''; | ||
261 | } | ||
262 | |||
263 | return vAlignStyle || vAlignAttr || ''; | ||
264 | } ), | ||
265 | commit: function( element ) { | ||
266 | var value = this.getValue(); | ||
267 | |||
268 | if ( value ) | ||
269 | element.setStyle( 'vertical-align', value ); | ||
270 | else | ||
271 | element.removeStyle( 'vertical-align' ); | ||
272 | |||
273 | element.removeAttribute( 'vAlign' ); | ||
274 | } | ||
275 | } ] | ||
276 | }, | ||
277 | spacer, | ||
278 | { | ||
279 | type: 'vbox', | ||
280 | padding: 0, | ||
281 | children: [ { | ||
282 | type: 'select', | ||
283 | id: 'cellType', | ||
284 | label: langCell.cellType, | ||
285 | 'default': 'td', | ||
286 | items: [ | ||
287 | [ langCell.data, 'td' ], | ||
288 | [ langCell.header, 'th' ] | ||
289 | ], | ||
290 | setup: setupCells( function( selectedCell ) { | ||
291 | return selectedCell.getName(); | ||
292 | } ), | ||
293 | commit: function( selectedCell ) { | ||
294 | selectedCell.renameNode( this.getValue() ); | ||
295 | } | ||
296 | }, | ||
297 | spacer, | ||
298 | { | ||
299 | type: 'text', | ||
300 | id: 'rowSpan', | ||
301 | label: langCell.rowSpan, | ||
302 | 'default': '', | ||
303 | validate: validate.integer( langCell.invalidRowSpan ), | ||
304 | setup: setupCells( function( selectedCell ) { | ||
305 | var attrVal = parseInt( selectedCell.getAttribute( 'rowSpan' ), 10 ); | ||
306 | if ( attrVal && attrVal != 1 ) | ||
307 | return attrVal; | ||
308 | } ), | ||
309 | commit: function( selectedCell ) { | ||
310 | var value = parseInt( this.getValue(), 10 ); | ||
311 | if ( value && value != 1 ) | ||
312 | selectedCell.setAttribute( 'rowSpan', this.getValue() ); | ||
313 | else | ||
314 | selectedCell.removeAttribute( 'rowSpan' ); | ||
315 | } | ||
316 | }, | ||
317 | { | ||
318 | type: 'text', | ||
319 | id: 'colSpan', | ||
320 | label: langCell.colSpan, | ||
321 | 'default': '', | ||
322 | validate: validate.integer( langCell.invalidColSpan ), | ||
323 | setup: setupCells( function( element ) { | ||
324 | var attrVal = parseInt( element.getAttribute( 'colSpan' ), 10 ); | ||
325 | if ( attrVal && attrVal != 1 ) | ||
326 | return attrVal; | ||
327 | } ), | ||
328 | commit: function( selectedCell ) { | ||
329 | var value = parseInt( this.getValue(), 10 ); | ||
330 | if ( value && value != 1 ) | ||
331 | selectedCell.setAttribute( 'colSpan', this.getValue() ); | ||
332 | else | ||
333 | selectedCell.removeAttribute( 'colSpan' ); | ||
334 | } | ||
335 | }, | ||
336 | spacer, | ||
337 | { | ||
338 | type: 'hbox', | ||
339 | padding: 0, | ||
340 | widths: [ '60%', '40%' ], | ||
341 | children: [ { | ||
342 | type: 'text', | ||
343 | id: 'bgColor', | ||
344 | label: langCell.bgColor, | ||
345 | 'default': '', | ||
346 | setup: setupCells( function( element ) { | ||
347 | var bgColorAttr = element.getAttribute( 'bgColor' ), | ||
348 | bgColorStyle = element.getStyle( 'background-color' ); | ||
349 | |||
350 | return bgColorStyle || bgColorAttr; | ||
351 | } ), | ||
352 | commit: function( selectedCell ) { | ||
353 | var value = this.getValue(); | ||
354 | |||
355 | if ( value ) | ||
356 | selectedCell.setStyle( 'background-color', this.getValue() ); | ||
357 | else | ||
358 | selectedCell.removeStyle( 'background-color' ); | ||
359 | |||
360 | selectedCell.removeAttribute( 'bgColor' ); | ||
361 | } | ||
362 | }, | ||
363 | colorDialog ? { | ||
364 | type: 'button', | ||
365 | id: 'bgColorChoose', | ||
366 | 'class': 'colorChooser', // jshint ignore:line | ||
367 | label: langCell.chooseColor, | ||
368 | onLoad: function() { | ||
369 | // Stick the element to the bottom (#5587) | ||
370 | this.getElement().getParent().setStyle( 'vertical-align', 'bottom' ); | ||
371 | }, | ||
372 | onClick: function() { | ||
373 | editor.getColorFromDialog( function( color ) { | ||
374 | if ( color ) | ||
375 | this.getDialog().getContentElement( 'info', 'bgColor' ).setValue( color ); | ||
376 | this.focus(); | ||
377 | }, this ); | ||
378 | } | ||
379 | } : spacer ] | ||
380 | }, | ||
381 | spacer, | ||
382 | { | ||
383 | type: 'hbox', | ||
384 | padding: 0, | ||
385 | widths: [ '60%', '40%' ], | ||
386 | children: [ { | ||
387 | type: 'text', | ||
388 | id: 'borderColor', | ||
389 | label: langCell.borderColor, | ||
390 | 'default': '', | ||
391 | setup: setupCells( function( element ) { | ||
392 | var borderColorAttr = element.getAttribute( 'borderColor' ), | ||
393 | borderColorStyle = element.getStyle( 'border-color' ); | ||
394 | |||
395 | return borderColorStyle || borderColorAttr; | ||
396 | } ), | ||
397 | commit: function( selectedCell ) { | ||
398 | var value = this.getValue(); | ||
399 | if ( value ) | ||
400 | selectedCell.setStyle( 'border-color', this.getValue() ); | ||
401 | else | ||
402 | selectedCell.removeStyle( 'border-color' ); | ||
403 | |||
404 | selectedCell.removeAttribute( 'borderColor' ); | ||
405 | } | ||
406 | }, | ||
407 | |||
408 | colorDialog ? { | ||
409 | type: 'button', | ||
410 | id: 'borderColorChoose', | ||
411 | 'class': 'colorChooser', // jshint ignore:line | ||
412 | label: langCell.chooseColor, | ||
413 | style: ( rtl ? 'margin-right' : 'margin-left' ) + ': 10px', | ||
414 | onLoad: function() { | ||
415 | // Stick the element to the bottom (#5587) | ||
416 | this.getElement().getParent().setStyle( 'vertical-align', 'bottom' ); | ||
417 | }, | ||
418 | onClick: function() { | ||
419 | editor.getColorFromDialog( function( color ) { | ||
420 | if ( color ) | ||
421 | this.getDialog().getContentElement( 'info', 'borderColor' ).setValue( color ); | ||
422 | this.focus(); | ||
423 | }, this ); | ||
424 | } | ||
425 | } : spacer ] | ||
426 | } ] | ||
427 | } ] | ||
428 | } ] | ||
429 | } ], | ||
430 | onShow: function() { | ||
431 | this.cells = CKEDITOR.plugins.tabletools.getSelectedCells( this._.editor.getSelection() ); | ||
432 | this.setupContent( this.cells ); | ||
433 | }, | ||
434 | onOk: function() { | ||
435 | var selection = this._.editor.getSelection(), | ||
436 | bookmarks = selection.createBookmarks(); | ||
437 | |||
438 | var cells = this.cells; | ||
439 | for ( var i = 0; i < cells.length; i++ ) | ||
440 | this.commitContent( cells[ i ] ); | ||
441 | |||
442 | this._.editor.forceNextSelectionCheck(); | ||
443 | selection.selectBookmarks( bookmarks ); | ||
444 | this._.editor.selectionChange(); | ||
445 | }, | ||
446 | onLoad: function() { | ||
447 | var saved = {}; | ||
448 | |||
449 | // Prevent from changing cell properties when the field's value | ||
450 | // remains unaltered, i.e. when selected multiple cells and dialog loaded | ||
451 | // only the properties of the first cell (#11439). | ||
452 | this.foreach( function( field ) { | ||
453 | if ( !field.setup || !field.commit ) | ||
454 | return; | ||
455 | |||
456 | // Save field's value every time after "setup" is called. | ||
457 | field.setup = CKEDITOR.tools.override( field.setup, function( orgSetup ) { | ||
458 | return function() { | ||
459 | orgSetup.apply( this, arguments ); | ||
460 | saved[ field.id ] = field.getValue(); | ||
461 | }; | ||
462 | } ); | ||
463 | |||
464 | // Compare saved value with actual value. Update cell only if value has changed. | ||
465 | field.commit = CKEDITOR.tools.override( field.commit, function( orgCommit ) { | ||
466 | return function() { | ||
467 | if ( saved[ field.id ] !== field.getValue() ) | ||
468 | orgCommit.apply( this, arguments ); | ||
469 | }; | ||
470 | } ); | ||
471 | } ); | ||
472 | } | ||
473 | }; | ||
474 | } ); | ||
diff --git a/sources/plugins/tabletools/plugin.js b/sources/plugins/tabletools/plugin.js new file mode 100644 index 00000000..41b08af0 --- /dev/null +++ b/sources/plugins/tabletools/plugin.js | |||
@@ -0,0 +1,1005 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | ( function() { | ||
7 | var cellNodeRegex = /^(?:td|th)$/; | ||
8 | |||
9 | function getSelectedCells( selection ) { | ||
10 | var ranges = selection.getRanges(); | ||
11 | var retval = []; | ||
12 | var database = {}; | ||
13 | |||
14 | function moveOutOfCellGuard( node ) { | ||
15 | // Apply to the first cell only. | ||
16 | if ( retval.length > 0 ) | ||
17 | return; | ||
18 | |||
19 | // If we are exiting from the first </td>, then the td should definitely be | ||
20 | // included. | ||
21 | if ( node.type == CKEDITOR.NODE_ELEMENT && cellNodeRegex.test( node.getName() ) && !node.getCustomData( 'selected_cell' ) ) { | ||
22 | CKEDITOR.dom.element.setMarker( database, node, 'selected_cell', true ); | ||
23 | retval.push( node ); | ||
24 | } | ||
25 | } | ||
26 | |||
27 | for ( var i = 0; i < ranges.length; i++ ) { | ||
28 | var range = ranges[ i ]; | ||
29 | |||
30 | if ( range.collapsed ) { | ||
31 | // Walker does not handle collapsed ranges yet - fall back to old API. | ||
32 | var startNode = range.getCommonAncestor(); | ||
33 | var nearestCell = startNode.getAscendant( 'td', true ) || startNode.getAscendant( 'th', true ); | ||
34 | if ( nearestCell ) | ||
35 | retval.push( nearestCell ); | ||
36 | } else { | ||
37 | var walker = new CKEDITOR.dom.walker( range ); | ||
38 | var node; | ||
39 | walker.guard = moveOutOfCellGuard; | ||
40 | |||
41 | while ( ( node = walker.next() ) ) { | ||
42 | // If may be possible for us to have a range like this: | ||
43 | // <td>^1</td><td>^2</td> | ||
44 | // The 2nd td shouldn't be included. | ||
45 | // | ||
46 | // So we have to take care to include a td we've entered only when we've | ||
47 | // walked into its children. | ||
48 | |||
49 | if ( node.type != CKEDITOR.NODE_ELEMENT || !node.is( CKEDITOR.dtd.table ) ) { | ||
50 | var parent = node.getAscendant( 'td', true ) || node.getAscendant( 'th', true ); | ||
51 | if ( parent && !parent.getCustomData( 'selected_cell' ) ) { | ||
52 | CKEDITOR.dom.element.setMarker( database, parent, 'selected_cell', true ); | ||
53 | retval.push( parent ); | ||
54 | } | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | } | ||
59 | |||
60 | CKEDITOR.dom.element.clearAllMarkers( database ); | ||
61 | |||
62 | return retval; | ||
63 | } | ||
64 | |||
65 | function getFocusElementAfterDelCells( cellsToDelete ) { | ||
66 | var i = 0, | ||
67 | last = cellsToDelete.length - 1, | ||
68 | database = {}, | ||
69 | cell, focusedCell, tr; | ||
70 | |||
71 | while ( ( cell = cellsToDelete[ i++ ] ) ) | ||
72 | CKEDITOR.dom.element.setMarker( database, cell, 'delete_cell', true ); | ||
73 | |||
74 | // 1.first we check left or right side focusable cell row by row; | ||
75 | i = 0; | ||
76 | while ( ( cell = cellsToDelete[ i++ ] ) ) { | ||
77 | if ( ( focusedCell = cell.getPrevious() ) && !focusedCell.getCustomData( 'delete_cell' ) || ( focusedCell = cell.getNext() ) && !focusedCell.getCustomData( 'delete_cell' ) ) { | ||
78 | CKEDITOR.dom.element.clearAllMarkers( database ); | ||
79 | return focusedCell; | ||
80 | } | ||
81 | } | ||
82 | |||
83 | CKEDITOR.dom.element.clearAllMarkers( database ); | ||
84 | |||
85 | // 2. then we check the toppest row (outside the selection area square) focusable cell | ||
86 | tr = cellsToDelete[ 0 ].getParent(); | ||
87 | if ( ( tr = tr.getPrevious() ) ) | ||
88 | return tr.getLast(); | ||
89 | |||
90 | // 3. last we check the lowerest row focusable cell | ||
91 | tr = cellsToDelete[ last ].getParent(); | ||
92 | if ( ( tr = tr.getNext() ) ) | ||
93 | return tr.getChild( 0 ); | ||
94 | |||
95 | return null; | ||
96 | } | ||
97 | |||
98 | function insertRow( selection, insertBefore ) { | ||
99 | var cells = getSelectedCells( selection ), | ||
100 | firstCell = cells[ 0 ], | ||
101 | table = firstCell.getAscendant( 'table' ), | ||
102 | doc = firstCell.getDocument(), | ||
103 | startRow = cells[ 0 ].getParent(), | ||
104 | startRowIndex = startRow.$.rowIndex, | ||
105 | lastCell = cells[ cells.length - 1 ], | ||
106 | endRowIndex = lastCell.getParent().$.rowIndex + lastCell.$.rowSpan - 1, | ||
107 | endRow = new CKEDITOR.dom.element( table.$.rows[ endRowIndex ] ), | ||
108 | rowIndex = insertBefore ? startRowIndex : endRowIndex, | ||
109 | row = insertBefore ? startRow : endRow; | ||
110 | |||
111 | var map = CKEDITOR.tools.buildTableMap( table ), | ||
112 | cloneRow = map[ rowIndex ], | ||
113 | nextRow = insertBefore ? map[ rowIndex - 1 ] : map[ rowIndex + 1 ], | ||
114 | width = map[ 0 ].length; | ||
115 | |||
116 | var newRow = doc.createElement( 'tr' ); | ||
117 | for ( var i = 0; cloneRow[ i ] && i < width; i++ ) { | ||
118 | var cell; | ||
119 | // Check whether there's a spanning row here, do not break it. | ||
120 | if ( cloneRow[ i ].rowSpan > 1 && nextRow && cloneRow[ i ] == nextRow[ i ] ) { | ||
121 | cell = cloneRow[ i ]; | ||
122 | cell.rowSpan += 1; | ||
123 | } else { | ||
124 | cell = new CKEDITOR.dom.element( cloneRow[ i ] ).clone(); | ||
125 | cell.removeAttribute( 'rowSpan' ); | ||
126 | cell.appendBogus(); | ||
127 | newRow.append( cell ); | ||
128 | cell = cell.$; | ||
129 | } | ||
130 | |||
131 | i += cell.colSpan - 1; | ||
132 | } | ||
133 | |||
134 | insertBefore ? newRow.insertBefore( row ) : newRow.insertAfter( row ); | ||
135 | } | ||
136 | |||
137 | function deleteRows( selectionOrRow ) { | ||
138 | if ( selectionOrRow instanceof CKEDITOR.dom.selection ) { | ||
139 | var cells = getSelectedCells( selectionOrRow ), | ||
140 | firstCell = cells[ 0 ], | ||
141 | table = firstCell.getAscendant( 'table' ), | ||
142 | map = CKEDITOR.tools.buildTableMap( table ), | ||
143 | startRow = cells[ 0 ].getParent(), | ||
144 | startRowIndex = startRow.$.rowIndex, | ||
145 | lastCell = cells[ cells.length - 1 ], | ||
146 | endRowIndex = lastCell.getParent().$.rowIndex + lastCell.$.rowSpan - 1, | ||
147 | rowsToDelete = []; | ||
148 | |||
149 | // Delete cell or reduce cell spans by checking through the table map. | ||
150 | for ( var i = startRowIndex; i <= endRowIndex; i++ ) { | ||
151 | var mapRow = map[ i ], | ||
152 | row = new CKEDITOR.dom.element( table.$.rows[ i ] ); | ||
153 | |||
154 | for ( var j = 0; j < mapRow.length; j++ ) { | ||
155 | var cell = new CKEDITOR.dom.element( mapRow[ j ] ), | ||
156 | cellRowIndex = cell.getParent().$.rowIndex; | ||
157 | |||
158 | if ( cell.$.rowSpan == 1 ) | ||
159 | cell.remove(); | ||
160 | // Row spanned cell. | ||
161 | else { | ||
162 | // Span row of the cell, reduce spanning. | ||
163 | cell.$.rowSpan -= 1; | ||
164 | // Root row of the cell, root cell to next row. | ||
165 | if ( cellRowIndex == i ) { | ||
166 | var nextMapRow = map[ i + 1 ]; | ||
167 | nextMapRow[ j - 1 ] ? cell.insertAfter( new CKEDITOR.dom.element( nextMapRow[ j - 1 ] ) ) : new CKEDITOR.dom.element( table.$.rows[ i + 1 ] ).append( cell, 1 ); | ||
168 | } | ||
169 | } | ||
170 | |||
171 | j += cell.$.colSpan - 1; | ||
172 | } | ||
173 | |||
174 | rowsToDelete.push( row ); | ||
175 | } | ||
176 | |||
177 | var rows = table.$.rows; | ||
178 | |||
179 | // Where to put the cursor after rows been deleted? | ||
180 | // 1. Into next sibling row if any; | ||
181 | // 2. Into previous sibling row if any; | ||
182 | // 3. Into table's parent element if it's the very last row. | ||
183 | var cursorPosition = new CKEDITOR.dom.element( rows[ endRowIndex + 1 ] || ( startRowIndex > 0 ? rows[ startRowIndex - 1 ] : null ) || table.$.parentNode ); | ||
184 | |||
185 | for ( i = rowsToDelete.length; i >= 0; i-- ) | ||
186 | deleteRows( rowsToDelete[ i ] ); | ||
187 | |||
188 | return cursorPosition; | ||
189 | } else if ( selectionOrRow instanceof CKEDITOR.dom.element ) { | ||
190 | table = selectionOrRow.getAscendant( 'table' ); | ||
191 | |||
192 | if ( table.$.rows.length == 1 ) | ||
193 | table.remove(); | ||
194 | else | ||
195 | selectionOrRow.remove(); | ||
196 | } | ||
197 | |||
198 | return null; | ||
199 | } | ||
200 | |||
201 | function getCellColIndex( cell, isStart ) { | ||
202 | var row = cell.getParent(), | ||
203 | rowCells = row.$.cells; | ||
204 | |||
205 | var colIndex = 0; | ||
206 | for ( var i = 0; i < rowCells.length; i++ ) { | ||
207 | var mapCell = rowCells[ i ]; | ||
208 | colIndex += isStart ? 1 : mapCell.colSpan; | ||
209 | if ( mapCell == cell.$ ) | ||
210 | break; | ||
211 | } | ||
212 | |||
213 | return colIndex - 1; | ||
214 | } | ||
215 | |||
216 | function getColumnsIndices( cells, isStart ) { | ||
217 | var retval = isStart ? Infinity : 0; | ||
218 | for ( var i = 0; i < cells.length; i++ ) { | ||
219 | var colIndex = getCellColIndex( cells[ i ], isStart ); | ||
220 | if ( isStart ? colIndex < retval : colIndex > retval ) | ||
221 | retval = colIndex; | ||
222 | } | ||
223 | return retval; | ||
224 | } | ||
225 | |||
226 | function insertColumn( selection, insertBefore ) { | ||
227 | var cells = getSelectedCells( selection ), | ||
228 | firstCell = cells[ 0 ], | ||
229 | table = firstCell.getAscendant( 'table' ), | ||
230 | startCol = getColumnsIndices( cells, 1 ), | ||
231 | lastCol = getColumnsIndices( cells ), | ||
232 | colIndex = insertBefore ? startCol : lastCol; | ||
233 | |||
234 | var map = CKEDITOR.tools.buildTableMap( table ), | ||
235 | cloneCol = [], | ||
236 | nextCol = [], | ||
237 | height = map.length; | ||
238 | |||
239 | for ( var i = 0; i < height; i++ ) { | ||
240 | cloneCol.push( map[ i ][ colIndex ] ); | ||
241 | var nextCell = insertBefore ? map[ i ][ colIndex - 1 ] : map[ i ][ colIndex + 1 ]; | ||
242 | nextCol.push( nextCell ); | ||
243 | } | ||
244 | |||
245 | for ( i = 0; i < height; i++ ) { | ||
246 | var cell; | ||
247 | |||
248 | if ( !cloneCol[ i ] ) | ||
249 | continue; | ||
250 | |||
251 | // Check whether there's a spanning column here, do not break it. | ||
252 | if ( cloneCol[ i ].colSpan > 1 && nextCol[ i ] == cloneCol[ i ] ) { | ||
253 | cell = cloneCol[ i ]; | ||
254 | cell.colSpan += 1; | ||
255 | } else { | ||
256 | cell = new CKEDITOR.dom.element( cloneCol[ i ] ).clone(); | ||
257 | cell.removeAttribute( 'colSpan' ); | ||
258 | cell.appendBogus(); | ||
259 | cell[ insertBefore ? 'insertBefore' : 'insertAfter' ].call( cell, new CKEDITOR.dom.element( cloneCol[ i ] ) ); | ||
260 | cell = cell.$; | ||
261 | } | ||
262 | |||
263 | i += cell.rowSpan - 1; | ||
264 | } | ||
265 | } | ||
266 | |||
267 | function deleteColumns( selectionOrCell ) { | ||
268 | var cells = getSelectedCells( selectionOrCell ), | ||
269 | firstCell = cells[ 0 ], | ||
270 | lastCell = cells[ cells.length - 1 ], | ||
271 | table = firstCell.getAscendant( 'table' ), | ||
272 | map = CKEDITOR.tools.buildTableMap( table ), | ||
273 | startColIndex, endColIndex, | ||
274 | rowsToDelete = []; | ||
275 | |||
276 | // Figure out selected cells' column indices. | ||
277 | for ( var i = 0, rows = map.length; i < rows; i++ ) { | ||
278 | for ( var j = 0, cols = map[ i ].length; j < cols; j++ ) { | ||
279 | if ( map[ i ][ j ] == firstCell.$ ) | ||
280 | startColIndex = j; | ||
281 | if ( map[ i ][ j ] == lastCell.$ ) | ||
282 | endColIndex = j; | ||
283 | } | ||
284 | } | ||
285 | |||
286 | // Delete cell or reduce cell spans by checking through the table map. | ||
287 | for ( i = startColIndex; i <= endColIndex; i++ ) { | ||
288 | for ( j = 0; j < map.length; j++ ) { | ||
289 | var mapRow = map[ j ], | ||
290 | row = new CKEDITOR.dom.element( table.$.rows[ j ] ), | ||
291 | cell = new CKEDITOR.dom.element( mapRow[ i ] ); | ||
292 | |||
293 | if ( cell.$ ) { | ||
294 | if ( cell.$.colSpan == 1 ) | ||
295 | cell.remove(); | ||
296 | // Reduce the col spans. | ||
297 | else | ||
298 | cell.$.colSpan -= 1; | ||
299 | |||
300 | j += cell.$.rowSpan - 1; | ||
301 | |||
302 | if ( !row.$.cells.length ) | ||
303 | rowsToDelete.push( row ); | ||
304 | } | ||
305 | } | ||
306 | } | ||
307 | |||
308 | var firstRowCells = table.$.rows[ 0 ] && table.$.rows[ 0 ].cells; | ||
309 | |||
310 | // Where to put the cursor after columns been deleted? | ||
311 | // 1. Into next cell of the first row if any; | ||
312 | // 2. Into previous cell of the first row if any; | ||
313 | // 3. Into table's parent element; | ||
314 | var cursorPosition = new CKEDITOR.dom.element( firstRowCells[ startColIndex ] || ( startColIndex ? firstRowCells[ startColIndex - 1 ] : table.$.parentNode ) ); | ||
315 | |||
316 | // Delete table rows only if all columns are gone (do not remove empty row). | ||
317 | if ( rowsToDelete.length == rows ) | ||
318 | table.remove(); | ||
319 | |||
320 | return cursorPosition; | ||
321 | } | ||
322 | |||
323 | function insertCell( selection, insertBefore ) { | ||
324 | var startElement = selection.getStartElement(); | ||
325 | var cell = startElement.getAscendant( 'td', 1 ) || startElement.getAscendant( 'th', 1 ); | ||
326 | |||
327 | if ( !cell ) | ||
328 | return; | ||
329 | |||
330 | // Create the new cell element to be added. | ||
331 | var newCell = cell.clone(); | ||
332 | newCell.appendBogus(); | ||
333 | |||
334 | if ( insertBefore ) | ||
335 | newCell.insertBefore( cell ); | ||
336 | else | ||
337 | newCell.insertAfter( cell ); | ||
338 | } | ||
339 | |||
340 | function deleteCells( selectionOrCell ) { | ||
341 | if ( selectionOrCell instanceof CKEDITOR.dom.selection ) { | ||
342 | var cellsToDelete = getSelectedCells( selectionOrCell ); | ||
343 | var table = cellsToDelete[ 0 ] && cellsToDelete[ 0 ].getAscendant( 'table' ); | ||
344 | var cellToFocus = getFocusElementAfterDelCells( cellsToDelete ); | ||
345 | |||
346 | for ( var i = cellsToDelete.length - 1; i >= 0; i-- ) | ||
347 | deleteCells( cellsToDelete[ i ] ); | ||
348 | |||
349 | if ( cellToFocus ) | ||
350 | placeCursorInCell( cellToFocus, true ); | ||
351 | else if ( table ) | ||
352 | table.remove(); | ||
353 | } else if ( selectionOrCell instanceof CKEDITOR.dom.element ) { | ||
354 | var tr = selectionOrCell.getParent(); | ||
355 | if ( tr.getChildCount() == 1 ) | ||
356 | tr.remove(); | ||
357 | else | ||
358 | selectionOrCell.remove(); | ||
359 | } | ||
360 | } | ||
361 | |||
362 | // Remove filler at end and empty spaces around the cell content. | ||
363 | function trimCell( cell ) { | ||
364 | var bogus = cell.getBogus(); | ||
365 | bogus && bogus.remove(); | ||
366 | cell.trim(); | ||
367 | } | ||
368 | |||
369 | function placeCursorInCell( cell, placeAtEnd ) { | ||
370 | var docInner = cell.getDocument(), | ||
371 | docOuter = CKEDITOR.document; | ||
372 | |||
373 | // Fixing "Unspecified error" thrown in IE10 by resetting | ||
374 | // selection the dirty and shameful way (#10308). | ||
375 | // We can not apply this hack to IE8 because | ||
376 | // it causes error (#11058). | ||
377 | if ( CKEDITOR.env.ie && CKEDITOR.env.version == 10 ) { | ||
378 | docOuter.focus(); | ||
379 | docInner.focus(); | ||
380 | } | ||
381 | |||
382 | var range = new CKEDITOR.dom.range( docInner ); | ||
383 | if ( !range[ 'moveToElementEdit' + ( placeAtEnd ? 'End' : 'Start' ) ]( cell ) ) { | ||
384 | range.selectNodeContents( cell ); | ||
385 | range.collapse( placeAtEnd ? false : true ); | ||
386 | } | ||
387 | range.select( true ); | ||
388 | } | ||
389 | |||
390 | function cellInRow( tableMap, rowIndex, cell ) { | ||
391 | var oRow = tableMap[ rowIndex ]; | ||
392 | if ( typeof cell == 'undefined' ) | ||
393 | return oRow; | ||
394 | |||
395 | for ( var c = 0; oRow && c < oRow.length; c++ ) { | ||
396 | if ( cell.is && oRow[ c ] == cell.$ ) | ||
397 | return c; | ||
398 | else if ( c == cell ) | ||
399 | return new CKEDITOR.dom.element( oRow[ c ] ); | ||
400 | } | ||
401 | return cell.is ? -1 : null; | ||
402 | } | ||
403 | |||
404 | function cellInCol( tableMap, colIndex ) { | ||
405 | var oCol = []; | ||
406 | for ( var r = 0; r < tableMap.length; r++ ) { | ||
407 | var row = tableMap[ r ]; | ||
408 | oCol.push( row[ colIndex ] ); | ||
409 | |||
410 | // Avoid adding duplicate cells. | ||
411 | if ( row[ colIndex ].rowSpan > 1 ) | ||
412 | r += row[ colIndex ].rowSpan - 1; | ||
413 | } | ||
414 | return oCol; | ||
415 | } | ||
416 | |||
417 | function mergeCells( selection, mergeDirection, isDetect ) { | ||
418 | var cells = getSelectedCells( selection ); | ||
419 | |||
420 | // Invalid merge request if: | ||
421 | // 1. In batch mode despite that less than two selected. | ||
422 | // 2. In solo mode while not exactly only one selected. | ||
423 | // 3. Cells distributed in different table groups (e.g. from both thead and tbody). | ||
424 | var commonAncestor; | ||
425 | if ( ( mergeDirection ? cells.length != 1 : cells.length < 2 ) || ( commonAncestor = selection.getCommonAncestor() ) && commonAncestor.type == CKEDITOR.NODE_ELEMENT && commonAncestor.is( 'table' ) ) | ||
426 | return false; | ||
427 | |||
428 | var cell, | ||
429 | firstCell = cells[ 0 ], | ||
430 | table = firstCell.getAscendant( 'table' ), | ||
431 | map = CKEDITOR.tools.buildTableMap( table ), | ||
432 | mapHeight = map.length, | ||
433 | mapWidth = map[ 0 ].length, | ||
434 | startRow = firstCell.getParent().$.rowIndex, | ||
435 | startColumn = cellInRow( map, startRow, firstCell ); | ||
436 | |||
437 | if ( mergeDirection ) { | ||
438 | var targetCell; | ||
439 | try { | ||
440 | var rowspan = parseInt( firstCell.getAttribute( 'rowspan' ), 10 ) || 1; | ||
441 | var colspan = parseInt( firstCell.getAttribute( 'colspan' ), 10 ) || 1; | ||
442 | |||
443 | targetCell = map[ mergeDirection == 'up' ? ( startRow - rowspan ) : mergeDirection == 'down' ? ( startRow + rowspan ) : startRow ][ | ||
444 | mergeDirection == 'left' ? | ||
445 | ( startColumn - colspan ) : | ||
446 | mergeDirection == 'right' ? ( startColumn + colspan ) : startColumn ]; | ||
447 | |||
448 | } catch ( er ) { | ||
449 | return false; | ||
450 | } | ||
451 | |||
452 | // 1. No cell could be merged. | ||
453 | // 2. Same cell actually. | ||
454 | if ( !targetCell || firstCell.$ == targetCell ) | ||
455 | return false; | ||
456 | |||
457 | // Sort in map order regardless of the DOM sequence. | ||
458 | cells[ ( mergeDirection == 'up' || mergeDirection == 'left' ) ? 'unshift' : 'push' ]( new CKEDITOR.dom.element( targetCell ) ); | ||
459 | } | ||
460 | |||
461 | // Start from here are merging way ignorance (merge up/right, batch merge). | ||
462 | var doc = firstCell.getDocument(), | ||
463 | lastRowIndex = startRow, | ||
464 | totalRowSpan = 0, | ||
465 | totalColSpan = 0, | ||
466 | // Use a documentFragment as buffer when appending cell contents. | ||
467 | frag = !isDetect && new CKEDITOR.dom.documentFragment( doc ), | ||
468 | dimension = 0; | ||
469 | |||
470 | for ( var i = 0; i < cells.length; i++ ) { | ||
471 | cell = cells[ i ]; | ||
472 | |||
473 | var tr = cell.getParent(), | ||
474 | cellFirstChild = cell.getFirst(), | ||
475 | colSpan = cell.$.colSpan, | ||
476 | rowSpan = cell.$.rowSpan, | ||
477 | rowIndex = tr.$.rowIndex, | ||
478 | colIndex = cellInRow( map, rowIndex, cell ); | ||
479 | |||
480 | // Accumulated the actual places taken by all selected cells. | ||
481 | dimension += colSpan * rowSpan; | ||
482 | // Accumulated the maximum virtual spans from column and row. | ||
483 | totalColSpan = Math.max( totalColSpan, colIndex - startColumn + colSpan ); | ||
484 | totalRowSpan = Math.max( totalRowSpan, rowIndex - startRow + rowSpan ); | ||
485 | |||
486 | if ( !isDetect ) { | ||
487 | // Trim all cell fillers and check to remove empty cells. | ||
488 | if ( trimCell( cell ), cell.getChildren().count() ) { | ||
489 | // Merge vertically cells as two separated paragraphs. | ||
490 | if ( rowIndex != lastRowIndex && cellFirstChild && !( cellFirstChild.isBlockBoundary && cellFirstChild.isBlockBoundary( { br: 1 } ) ) ) { | ||
491 | var last = frag.getLast( CKEDITOR.dom.walker.whitespaces( true ) ); | ||
492 | if ( last && !( last.is && last.is( 'br' ) ) ) | ||
493 | frag.append( 'br' ); | ||
494 | } | ||
495 | |||
496 | cell.moveChildren( frag ); | ||
497 | } | ||
498 | i ? cell.remove() : cell.setHtml( '' ); | ||
499 | } | ||
500 | lastRowIndex = rowIndex; | ||
501 | } | ||
502 | |||
503 | if ( !isDetect ) { | ||
504 | frag.moveChildren( firstCell ); | ||
505 | |||
506 | firstCell.appendBogus(); | ||
507 | |||
508 | if ( totalColSpan >= mapWidth ) | ||
509 | firstCell.removeAttribute( 'rowSpan' ); | ||
510 | else | ||
511 | firstCell.$.rowSpan = totalRowSpan; | ||
512 | |||
513 | if ( totalRowSpan >= mapHeight ) | ||
514 | firstCell.removeAttribute( 'colSpan' ); | ||
515 | else | ||
516 | firstCell.$.colSpan = totalColSpan; | ||
517 | |||
518 | // Swip empty <tr> left at the end of table due to the merging. | ||
519 | var trs = new CKEDITOR.dom.nodeList( table.$.rows ), | ||
520 | count = trs.count(); | ||
521 | |||
522 | for ( i = count - 1; i >= 0; i-- ) { | ||
523 | var tailTr = trs.getItem( i ); | ||
524 | if ( !tailTr.$.cells.length ) { | ||
525 | tailTr.remove(); | ||
526 | count++; | ||
527 | continue; | ||
528 | } | ||
529 | } | ||
530 | |||
531 | return firstCell; | ||
532 | } | ||
533 | // Be able to merge cells only if actual dimension of selected | ||
534 | // cells equals to the caculated rectangle. | ||
535 | else { | ||
536 | return ( totalRowSpan * totalColSpan ) == dimension; | ||
537 | } | ||
538 | } | ||
539 | |||
540 | function horizontalSplitCell( selection, isDetect ) { | ||
541 | var cells = getSelectedCells( selection ); | ||
542 | if ( cells.length > 1 ) | ||
543 | return false; | ||
544 | else if ( isDetect ) | ||
545 | return true; | ||
546 | |||
547 | var cell = cells[ 0 ], | ||
548 | tr = cell.getParent(), | ||
549 | table = tr.getAscendant( 'table' ), | ||
550 | map = CKEDITOR.tools.buildTableMap( table ), | ||
551 | rowIndex = tr.$.rowIndex, | ||
552 | colIndex = cellInRow( map, rowIndex, cell ), | ||
553 | rowSpan = cell.$.rowSpan, | ||
554 | newCell, newRowSpan, newCellRowSpan, newRowIndex; | ||
555 | |||
556 | if ( rowSpan > 1 ) { | ||
557 | newRowSpan = Math.ceil( rowSpan / 2 ); | ||
558 | newCellRowSpan = Math.floor( rowSpan / 2 ); | ||
559 | newRowIndex = rowIndex + newRowSpan; | ||
560 | var newCellTr = new CKEDITOR.dom.element( table.$.rows[ newRowIndex ] ), | ||
561 | newCellRow = cellInRow( map, newRowIndex ), | ||
562 | candidateCell; | ||
563 | |||
564 | newCell = cell.clone(); | ||
565 | |||
566 | // Figure out where to insert the new cell by checking the vitual row. | ||
567 | for ( var c = 0; c < newCellRow.length; c++ ) { | ||
568 | candidateCell = newCellRow[ c ]; | ||
569 | // Catch first cell actually following the column. | ||
570 | if ( candidateCell.parentNode == newCellTr.$ && c > colIndex ) { | ||
571 | newCell.insertBefore( new CKEDITOR.dom.element( candidateCell ) ); | ||
572 | break; | ||
573 | } else { | ||
574 | candidateCell = null; | ||
575 | } | ||
576 | } | ||
577 | |||
578 | // The destination row is empty, append at will. | ||
579 | if ( !candidateCell ) | ||
580 | newCellTr.append( newCell ); | ||
581 | } else { | ||
582 | newCellRowSpan = newRowSpan = 1; | ||
583 | |||
584 | newCellTr = tr.clone(); | ||
585 | newCellTr.insertAfter( tr ); | ||
586 | newCellTr.append( newCell = cell.clone() ); | ||
587 | |||
588 | var cellsInSameRow = cellInRow( map, rowIndex ); | ||
589 | for ( var i = 0; i < cellsInSameRow.length; i++ ) | ||
590 | cellsInSameRow[ i ].rowSpan++; | ||
591 | } | ||
592 | |||
593 | newCell.appendBogus(); | ||
594 | |||
595 | cell.$.rowSpan = newRowSpan; | ||
596 | newCell.$.rowSpan = newCellRowSpan; | ||
597 | if ( newRowSpan == 1 ) | ||
598 | cell.removeAttribute( 'rowSpan' ); | ||
599 | if ( newCellRowSpan == 1 ) | ||
600 | newCell.removeAttribute( 'rowSpan' ); | ||
601 | |||
602 | return newCell; | ||
603 | } | ||
604 | |||
605 | function verticalSplitCell( selection, isDetect ) { | ||
606 | var cells = getSelectedCells( selection ); | ||
607 | if ( cells.length > 1 ) | ||
608 | return false; | ||
609 | else if ( isDetect ) | ||
610 | return true; | ||
611 | |||
612 | var cell = cells[ 0 ], | ||
613 | tr = cell.getParent(), | ||
614 | table = tr.getAscendant( 'table' ), | ||
615 | map = CKEDITOR.tools.buildTableMap( table ), | ||
616 | rowIndex = tr.$.rowIndex, | ||
617 | colIndex = cellInRow( map, rowIndex, cell ), | ||
618 | colSpan = cell.$.colSpan, | ||
619 | newCell, newColSpan, newCellColSpan; | ||
620 | |||
621 | if ( colSpan > 1 ) { | ||
622 | newColSpan = Math.ceil( colSpan / 2 ); | ||
623 | newCellColSpan = Math.floor( colSpan / 2 ); | ||
624 | } else { | ||
625 | newCellColSpan = newColSpan = 1; | ||
626 | var cellsInSameCol = cellInCol( map, colIndex ); | ||
627 | for ( var i = 0; i < cellsInSameCol.length; i++ ) | ||
628 | cellsInSameCol[ i ].colSpan++; | ||
629 | } | ||
630 | newCell = cell.clone(); | ||
631 | newCell.insertAfter( cell ); | ||
632 | newCell.appendBogus(); | ||
633 | |||
634 | cell.$.colSpan = newColSpan; | ||
635 | newCell.$.colSpan = newCellColSpan; | ||
636 | if ( newColSpan == 1 ) | ||
637 | cell.removeAttribute( 'colSpan' ); | ||
638 | if ( newCellColSpan == 1 ) | ||
639 | newCell.removeAttribute( 'colSpan' ); | ||
640 | |||
641 | return newCell; | ||
642 | } | ||
643 | |||
644 | CKEDITOR.plugins.tabletools = { | ||
645 | requires: 'table,dialog,contextmenu', | ||
646 | init: function( editor ) { | ||
647 | var lang = editor.lang.table; | ||
648 | |||
649 | function createDef( def ) { | ||
650 | return CKEDITOR.tools.extend( def || {}, { | ||
651 | contextSensitive: 1, | ||
652 | refresh: function( editor, path ) { | ||
653 | this.setState( path.contains( { td: 1, th: 1 }, 1 ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED ); | ||
654 | } | ||
655 | } ); | ||
656 | } | ||
657 | function addCmd( name, def ) { | ||
658 | var cmd = editor.addCommand( name, def ); | ||
659 | editor.addFeature( cmd ); | ||
660 | } | ||
661 | |||
662 | addCmd( 'cellProperties', new CKEDITOR.dialogCommand( 'cellProperties', createDef( { | ||
663 | allowedContent: 'td th{width,height,border-color,background-color,white-space,vertical-align,text-align}[colspan,rowspan]', | ||
664 | requiredContent: 'table' | ||
665 | } ) ) ); | ||
666 | CKEDITOR.dialog.add( 'cellProperties', this.path + 'dialogs/tableCell.js' ); | ||
667 | |||
668 | addCmd( 'rowDelete', createDef( { | ||
669 | requiredContent: 'table', | ||
670 | exec: function( editor ) { | ||
671 | var selection = editor.getSelection(); | ||
672 | placeCursorInCell( deleteRows( selection ) ); | ||
673 | } | ||
674 | } ) ); | ||
675 | |||
676 | addCmd( 'rowInsertBefore', createDef( { | ||
677 | requiredContent: 'table', | ||
678 | exec: function( editor ) { | ||
679 | var selection = editor.getSelection(); | ||
680 | insertRow( selection, true ); | ||
681 | } | ||
682 | } ) ); | ||
683 | |||
684 | addCmd( 'rowInsertAfter', createDef( { | ||
685 | requiredContent: 'table', | ||
686 | exec: function( editor ) { | ||
687 | var selection = editor.getSelection(); | ||
688 | insertRow( selection ); | ||
689 | } | ||
690 | } ) ); | ||
691 | |||
692 | addCmd( 'columnDelete', createDef( { | ||
693 | requiredContent: 'table', | ||
694 | exec: function( editor ) { | ||
695 | var selection = editor.getSelection(); | ||
696 | var element = deleteColumns( selection ); | ||
697 | element && placeCursorInCell( element, true ); | ||
698 | } | ||
699 | } ) ); | ||
700 | |||
701 | addCmd( 'columnInsertBefore', createDef( { | ||
702 | requiredContent: 'table', | ||
703 | exec: function( editor ) { | ||
704 | var selection = editor.getSelection(); | ||
705 | insertColumn( selection, true ); | ||
706 | } | ||
707 | } ) ); | ||
708 | |||
709 | addCmd( 'columnInsertAfter', createDef( { | ||
710 | requiredContent: 'table', | ||
711 | exec: function( editor ) { | ||
712 | var selection = editor.getSelection(); | ||
713 | insertColumn( selection ); | ||
714 | } | ||
715 | } ) ); | ||
716 | |||
717 | addCmd( 'cellDelete', createDef( { | ||
718 | requiredContent: 'table', | ||
719 | exec: function( editor ) { | ||
720 | var selection = editor.getSelection(); | ||
721 | deleteCells( selection ); | ||
722 | } | ||
723 | } ) ); | ||
724 | |||
725 | addCmd( 'cellMerge', createDef( { | ||
726 | allowedContent: 'td[colspan,rowspan]', | ||
727 | requiredContent: 'td[colspan,rowspan]', | ||
728 | exec: function( editor ) { | ||
729 | placeCursorInCell( mergeCells( editor.getSelection() ), true ); | ||
730 | } | ||
731 | } ) ); | ||
732 | |||
733 | addCmd( 'cellMergeRight', createDef( { | ||
734 | allowedContent: 'td[colspan]', | ||
735 | requiredContent: 'td[colspan]', | ||
736 | exec: function( editor ) { | ||
737 | placeCursorInCell( mergeCells( editor.getSelection(), 'right' ), true ); | ||
738 | } | ||
739 | } ) ); | ||
740 | |||
741 | addCmd( 'cellMergeDown', createDef( { | ||
742 | allowedContent: 'td[rowspan]', | ||
743 | requiredContent: 'td[rowspan]', | ||
744 | exec: function( editor ) { | ||
745 | placeCursorInCell( mergeCells( editor.getSelection(), 'down' ), true ); | ||
746 | } | ||
747 | } ) ); | ||
748 | |||
749 | addCmd( 'cellVerticalSplit', createDef( { | ||
750 | allowedContent: 'td[rowspan]', | ||
751 | requiredContent: 'td[rowspan]', | ||
752 | exec: function( editor ) { | ||
753 | placeCursorInCell( verticalSplitCell( editor.getSelection() ) ); | ||
754 | } | ||
755 | } ) ); | ||
756 | |||
757 | addCmd( 'cellHorizontalSplit', createDef( { | ||
758 | allowedContent: 'td[colspan]', | ||
759 | requiredContent: 'td[colspan]', | ||
760 | exec: function( editor ) { | ||
761 | placeCursorInCell( horizontalSplitCell( editor.getSelection() ) ); | ||
762 | } | ||
763 | } ) ); | ||
764 | |||
765 | addCmd( 'cellInsertBefore', createDef( { | ||
766 | requiredContent: 'table', | ||
767 | exec: function( editor ) { | ||
768 | var selection = editor.getSelection(); | ||
769 | insertCell( selection, true ); | ||
770 | } | ||
771 | } ) ); | ||
772 | |||
773 | addCmd( 'cellInsertAfter', createDef( { | ||
774 | requiredContent: 'table', | ||
775 | exec: function( editor ) { | ||
776 | var selection = editor.getSelection(); | ||
777 | insertCell( selection ); | ||
778 | } | ||
779 | } ) ); | ||
780 | |||
781 | // If the "menu" plugin is loaded, register the menu items. | ||
782 | if ( editor.addMenuItems ) { | ||
783 | editor.addMenuItems( { | ||
784 | tablecell: { | ||
785 | label: lang.cell.menu, | ||
786 | group: 'tablecell', | ||
787 | order: 1, | ||
788 | getItems: function() { | ||
789 | var selection = editor.getSelection(), | ||
790 | cells = getSelectedCells( selection ); | ||
791 | return { | ||
792 | tablecell_insertBefore: CKEDITOR.TRISTATE_OFF, | ||
793 | tablecell_insertAfter: CKEDITOR.TRISTATE_OFF, | ||
794 | tablecell_delete: CKEDITOR.TRISTATE_OFF, | ||
795 | tablecell_merge: mergeCells( selection, null, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED, | ||
796 | tablecell_merge_right: mergeCells( selection, 'right', true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED, | ||
797 | tablecell_merge_down: mergeCells( selection, 'down', true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED, | ||
798 | tablecell_split_vertical: verticalSplitCell( selection, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED, | ||
799 | tablecell_split_horizontal: horizontalSplitCell( selection, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED, | ||
800 | tablecell_properties: cells.length > 0 ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED | ||
801 | }; | ||
802 | } | ||
803 | }, | ||
804 | |||
805 | tablecell_insertBefore: { | ||
806 | label: lang.cell.insertBefore, | ||
807 | group: 'tablecell', | ||
808 | command: 'cellInsertBefore', | ||
809 | order: 5 | ||
810 | }, | ||
811 | |||
812 | tablecell_insertAfter: { | ||
813 | label: lang.cell.insertAfter, | ||
814 | group: 'tablecell', | ||
815 | command: 'cellInsertAfter', | ||
816 | order: 10 | ||
817 | }, | ||
818 | |||
819 | tablecell_delete: { | ||
820 | label: lang.cell.deleteCell, | ||
821 | group: 'tablecell', | ||
822 | command: 'cellDelete', | ||
823 | order: 15 | ||
824 | }, | ||
825 | |||
826 | tablecell_merge: { | ||
827 | label: lang.cell.merge, | ||
828 | group: 'tablecell', | ||
829 | command: 'cellMerge', | ||
830 | order: 16 | ||
831 | }, | ||
832 | |||
833 | tablecell_merge_right: { | ||
834 | label: lang.cell.mergeRight, | ||
835 | group: 'tablecell', | ||
836 | command: 'cellMergeRight', | ||
837 | order: 17 | ||
838 | }, | ||
839 | |||
840 | tablecell_merge_down: { | ||
841 | label: lang.cell.mergeDown, | ||
842 | group: 'tablecell', | ||
843 | command: 'cellMergeDown', | ||
844 | order: 18 | ||
845 | }, | ||
846 | |||
847 | tablecell_split_horizontal: { | ||
848 | label: lang.cell.splitHorizontal, | ||
849 | group: 'tablecell', | ||
850 | command: 'cellHorizontalSplit', | ||
851 | order: 19 | ||
852 | }, | ||
853 | |||
854 | tablecell_split_vertical: { | ||
855 | label: lang.cell.splitVertical, | ||
856 | group: 'tablecell', | ||
857 | command: 'cellVerticalSplit', | ||
858 | order: 20 | ||
859 | }, | ||
860 | |||
861 | tablecell_properties: { | ||
862 | label: lang.cell.title, | ||
863 | group: 'tablecellproperties', | ||
864 | command: 'cellProperties', | ||
865 | order: 21 | ||
866 | }, | ||
867 | |||
868 | tablerow: { | ||
869 | label: lang.row.menu, | ||
870 | group: 'tablerow', | ||
871 | order: 1, | ||
872 | getItems: function() { | ||
873 | return { | ||
874 | tablerow_insertBefore: CKEDITOR.TRISTATE_OFF, | ||
875 | tablerow_insertAfter: CKEDITOR.TRISTATE_OFF, | ||
876 | tablerow_delete: CKEDITOR.TRISTATE_OFF | ||
877 | }; | ||
878 | } | ||
879 | }, | ||
880 | |||
881 | tablerow_insertBefore: { | ||
882 | label: lang.row.insertBefore, | ||
883 | group: 'tablerow', | ||
884 | command: 'rowInsertBefore', | ||
885 | order: 5 | ||
886 | }, | ||
887 | |||
888 | tablerow_insertAfter: { | ||
889 | label: lang.row.insertAfter, | ||
890 | group: 'tablerow', | ||
891 | command: 'rowInsertAfter', | ||
892 | order: 10 | ||
893 | }, | ||
894 | |||
895 | tablerow_delete: { | ||
896 | label: lang.row.deleteRow, | ||
897 | group: 'tablerow', | ||
898 | command: 'rowDelete', | ||
899 | order: 15 | ||
900 | }, | ||
901 | |||
902 | tablecolumn: { | ||
903 | label: lang.column.menu, | ||
904 | group: 'tablecolumn', | ||
905 | order: 1, | ||
906 | getItems: function() { | ||
907 | return { | ||
908 | tablecolumn_insertBefore: CKEDITOR.TRISTATE_OFF, | ||
909 | tablecolumn_insertAfter: CKEDITOR.TRISTATE_OFF, | ||
910 | tablecolumn_delete: CKEDITOR.TRISTATE_OFF | ||
911 | }; | ||
912 | } | ||
913 | }, | ||
914 | |||
915 | tablecolumn_insertBefore: { | ||
916 | label: lang.column.insertBefore, | ||
917 | group: 'tablecolumn', | ||
918 | command: 'columnInsertBefore', | ||
919 | order: 5 | ||
920 | }, | ||
921 | |||
922 | tablecolumn_insertAfter: { | ||
923 | label: lang.column.insertAfter, | ||
924 | group: 'tablecolumn', | ||
925 | command: 'columnInsertAfter', | ||
926 | order: 10 | ||
927 | }, | ||
928 | |||
929 | tablecolumn_delete: { | ||
930 | label: lang.column.deleteColumn, | ||
931 | group: 'tablecolumn', | ||
932 | command: 'columnDelete', | ||
933 | order: 15 | ||
934 | } | ||
935 | } ); | ||
936 | } | ||
937 | |||
938 | // If the "contextmenu" plugin is laoded, register the listeners. | ||
939 | if ( editor.contextMenu ) { | ||
940 | editor.contextMenu.addListener( function( element, selection, path ) { | ||
941 | var cell = path.contains( { 'td': 1, 'th': 1 }, 1 ); | ||
942 | if ( cell && !cell.isReadOnly() ) { | ||
943 | return { | ||
944 | tablecell: CKEDITOR.TRISTATE_OFF, | ||
945 | tablerow: CKEDITOR.TRISTATE_OFF, | ||
946 | tablecolumn: CKEDITOR.TRISTATE_OFF | ||
947 | }; | ||
948 | } | ||
949 | |||
950 | return null; | ||
951 | } ); | ||
952 | } | ||
953 | }, | ||
954 | |||
955 | getSelectedCells: getSelectedCells | ||
956 | |||
957 | }; | ||
958 | CKEDITOR.plugins.add( 'tabletools', CKEDITOR.plugins.tabletools ); | ||
959 | } )(); | ||
960 | |||
961 | /** | ||
962 | * Create a two-dimension array that reflects the actual layout of table cells, | ||
963 | * with cell spans, with mappings to the original td elements. | ||
964 | * | ||
965 | * @param {CKEDITOR.dom.element} table | ||
966 | * @member CKEDITOR.tools | ||
967 | */ | ||
968 | CKEDITOR.tools.buildTableMap = function( table ) { | ||
969 | var aRows = table.$.rows; | ||
970 | |||
971 | // Row and Column counters. | ||
972 | var r = -1; | ||
973 | |||
974 | var aMap = []; | ||
975 | |||
976 | for ( var i = 0; i < aRows.length; i++ ) { | ||
977 | r++; | ||
978 | !aMap[ r ] && ( aMap[ r ] = [] ); | ||
979 | |||
980 | var c = -1; | ||
981 | |||
982 | for ( var j = 0; j < aRows[ i ].cells.length; j++ ) { | ||
983 | var oCell = aRows[ i ].cells[ j ]; | ||
984 | |||
985 | c++; | ||
986 | while ( aMap[ r ][ c ] ) | ||
987 | c++; | ||
988 | |||
989 | var iColSpan = isNaN( oCell.colSpan ) ? 1 : oCell.colSpan; | ||
990 | var iRowSpan = isNaN( oCell.rowSpan ) ? 1 : oCell.rowSpan; | ||
991 | |||
992 | for ( var rs = 0; rs < iRowSpan; rs++ ) { | ||
993 | if ( !aMap[ r + rs ] ) | ||
994 | aMap[ r + rs ] = []; | ||
995 | |||
996 | for ( var cs = 0; cs < iColSpan; cs++ ) { | ||
997 | aMap[ r + rs ][ c + cs ] = aRows[ i ].cells[ j ]; | ||
998 | } | ||
999 | } | ||
1000 | |||
1001 | c += iColSpan - 1; | ||
1002 | } | ||
1003 | } | ||
1004 | return aMap; | ||
1005 | }; | ||