1 /**
  2  * Add table plugin for KISSY.
  3  * @author yiminghe@gmail.com
  4  */
  5 KISSY.add("editor/plugin/table/index", function (S, Editor, DialogLoader, ContextMenu) {
  6 
  7     var UA = S.UA,
  8         DOM = S.DOM,
  9         Node = S.Node,
 10         tableRules = ["tr", "th", "td", "tbody", "table"],
 11         cellNodeRegex = /^(?:td|th)$/;
 12 
 13     function getSelectedCells(selection) {
 14         // Walker will try to split text nodes, which will make the current selection
 15         // invalid. So save bookmarks before doing anything.
 16         var bookmarks = selection.createBookmarks(),
 17             ranges = selection.getRanges(),
 18             retval = [],
 19             database = {};
 20 
 21         function moveOutOfCellGuard(node) {
 22             // Apply to the first cell only.
 23             if (retval.length > 0) {
 24                 return;
 25             }
 26             // If we are exiting from the first </td>, then the td should definitely be
 27             // included.
 28             if (node[0].nodeType == DOM.ELEMENT_NODE &&
 29                 cellNodeRegex.test(node.nodeName()) &&
 30                 !node.data('selected_cell')) {
 31                 node._4e_setMarker(database, 'selected_cell', true, undefined);
 32                 retval.push(node);
 33             }
 34         }
 35 
 36         for (var i = 0; i < ranges.length; i++) {
 37             var range = ranges[ i ];
 38 
 39             if (range.collapsed) {
 40                 // Walker does not handle collapsed ranges yet - fall back to old API.
 41                 var startNode = range.getCommonAncestor(),
 42                     nearestCell = startNode.closest('td', undefined) ||
 43                         startNode.closest('th', undefined);
 44                 if (nearestCell)
 45                     retval.push(nearestCell);
 46             } else {
 47                 var walker = new Walker(range),
 48                     node;
 49                 walker.guard = moveOutOfCellGuard;
 50 
 51                 while (( node = walker.next() )) {
 52                     // If may be possible for us to have a range like this:
 53                     // <td>^1</td><td>^2</td>
 54                     // The 2nd td shouldn't be included.
 55                     //
 56                     // So we have to take care to include a td we've entered only when we've
 57                     // walked into its children.
 58 
 59                     var parent = node.parent();
 60                     if (parent && cellNodeRegex.test(parent.nodeName()) &&
 61                         !parent.data('selected_cell')) {
 62                         parent._4e_setMarker(database, 'selected_cell', true, undefined);
 63                         retval.push(parent);
 64                     }
 65                 }
 66             }
 67         }
 68 
 69         Editor.Utils.clearAllMarkers(database);
 70         // Restore selection position.
 71         selection.selectBookmarks(bookmarks);
 72 
 73         return retval;
 74     }
 75 
 76     function clearRow($tr) {
 77         // Get the array of row's cells.
 78         var $cells = $tr.cells;
 79         // Empty all cells.
 80         for (var i = 0; i < $cells.length; i++) {
 81             $cells[ i ].innerHTML = '';
 82             if (!UA['ie'])
 83                 ( new Node($cells[ i ]) )._4e_appendBogus(undefined);
 84         }
 85     }
 86 
 87     function insertRow(selection, insertBefore) {
 88         // Get the row where the selection is placed in.
 89         var row = selection.getStartElement().parent('tr');
 90         if (!row)
 91             return;
 92 
 93         // Create a clone of the row.
 94         var newRow = row.clone(true);
 95         // Insert the new row before of it.
 96         newRow.insertBefore(row);
 97         // Clean one of the rows to produce the illusion of
 98         // inserting an empty row
 99         // before or after.
100         clearRow(insertBefore ? newRow[0] : row[0]);
101     }
102 
103     function deleteRows(selectionOrRow) {
104         if (selectionOrRow instanceof Editor.Selection) {
105             var cells = getSelectedCells(selectionOrRow),
106                 cellsCount = cells.length,
107                 rowsToDelete = [],
108                 cursorPosition,
109                 previousRowIndex,
110                 nextRowIndex;
111 
112             // Queue up the rows - it's possible and
113             // likely that we have duplicates.
114             for (var i = 0; i < cellsCount; i++) {
115                 var row = cells[ i ].parent(),
116                     rowIndex = row[0].rowIndex;
117 
118                 !i && ( previousRowIndex = rowIndex - 1 );
119                 rowsToDelete[ rowIndex ] = row;
120                 i == cellsCount - 1 && ( nextRowIndex = rowIndex + 1 );
121             }
122 
123             var table = row.parent('table'),
124                 rows = table[0].rows,
125                 rowCount = rows.length;
126 
127             // Where to put the cursor after rows been deleted?
128             // 1. Into next sibling row if any;
129             // 2. Into previous sibling row if any;
130             // 3. Into table's parent element if it's the very last row.
131             cursorPosition = new Node(
132                 nextRowIndex < rowCount && table[0].rows[ nextRowIndex ] ||
133                     previousRowIndex > 0 && table[0].rows[ previousRowIndex ] ||
134                     table[0].parentNode);
135 
136             for (i = rowsToDelete.length; i >= 0; i--) {
137                 if (rowsToDelete[ i ])
138                     deleteRows(rowsToDelete[ i ]);
139             }
140 
141             return cursorPosition;
142         }
143         else if (selectionOrRow instanceof Node) {
144             table = selectionOrRow.parent('table');
145 
146             if (table[0].rows.length == 1)
147                 table.remove();
148             else
149                 selectionOrRow.remove();
150         }
151 
152         return 0;
153     }
154 
155     function insertColumn(selection, insertBefore) {
156         // Get the cell where the selection is placed in.
157         var startElement = selection.getStartElement(),
158             cell = startElement.closest('td', undefined) ||
159                 startElement.closest('th', undefined);
160 
161         if (!cell) {
162             return;
163         }
164 
165         // Get the cell's table.
166         var table = cell.parent('table'),
167             cellIndex = cell[0].cellIndex;
168         // Loop through all rows available in the table.
169         for (var i = 0; i < table[0].rows.length; i++) {
170             var $row = table[0].rows[ i ];
171             // If the row doesn't have enough cells, ignore it.
172             if ($row.cells.length < ( cellIndex + 1 ))
173                 continue;
174             cell = new Node($row.cells[ cellIndex ].cloneNode(undefined));
175 
176             if (!UA['ie'])
177                 cell._4e_appendBogus(undefined);
178             // Get back the currently selected cell.
179             var baseCell = new Node($row.cells[ cellIndex ]);
180             if (insertBefore)
181                 cell.insertBefore(baseCell);
182             else
183                 cell.insertAfter(baseCell);
184         }
185     }
186 
187     function getFocusElementAfterDelCols(cells) {
188         var cellIndexList = [],
189             table = cells[ 0 ] && cells[ 0 ].parent('table'),
190             i, length,
191             targetIndex, targetCell;
192 
193         // get the cellIndex list of delete cells
194         for (i = 0, length = cells.length; i < length; i++) {
195             cellIndexList.push(cells[i][0].cellIndex);
196         }
197 
198         // get the focusable column index
199         cellIndexList.sort();
200         for (i = 1, length = cellIndexList.length;
201              i < length; i++) {
202             if (cellIndexList[ i ] - cellIndexList[ i - 1 ] > 1) {
203                 targetIndex = cellIndexList[ i - 1 ] + 1;
204                 break;
205             }
206         }
207 
208         if (!targetIndex) {
209             targetIndex = cellIndexList[ 0 ] > 0 ? ( cellIndexList[ 0 ] - 1 )
210                 : ( cellIndexList[ cellIndexList.length - 1 ] + 1 );
211         }
212 
213         // scan row by row to get the target cell
214         var rows = table[0].rows;
215         for (i = 0, length = rows.length;
216              i < length; i++) {
217             targetCell = rows[ i ].cells[ targetIndex ];
218             if (targetCell) {
219                 break;
220             }
221         }
222 
223         return targetCell ? new Node(targetCell) : table.prev();
224     }
225 
226     function deleteColumns(selectionOrCell) {
227         if (selectionOrCell instanceof Editor.Selection) {
228             var colsToDelete = getSelectedCells(selectionOrCell),
229                 elementToFocus = getFocusElementAfterDelCols(colsToDelete);
230 
231             for (var i = colsToDelete.length - 1; i >= 0; i--) {
232                 //某一列已经删除??这一列的cell再做? !table判断处理
233                 if (colsToDelete[ i ]) {
234                     deleteColumns(colsToDelete[i]);
235                 }
236             }
237 
238             return elementToFocus;
239         } else if (selectionOrCell instanceof Node) {
240             // Get the cell's table.
241             var table = selectionOrCell.parent('table');
242 
243             //该单元格所属的列已经被删除了
244             if (!table)
245                 return null;
246 
247             // Get the cell index.
248             var cellIndex = selectionOrCell[0].cellIndex;
249 
250             /*
251              * Loop through all rows from down to up,
252              *  coz it's possible that some rows
253              * will be deleted.
254              */
255             for (i = table[0].rows.length - 1; i >= 0; i--) {
256                 // Get the row.
257                 var row = new Node(table[0].rows[ i ]);
258 
259                 // If the cell to be removed is the first one and
260                 //  the row has just one cell.
261                 if (!cellIndex && row[0].cells.length == 1) {
262                     deleteRows(row);
263                     continue;
264                 }
265 
266                 // Else, just delete the cell.
267                 if (row[0].cells[ cellIndex ])
268                     row[0].removeChild(row[0].cells[ cellIndex ]);
269             }
270         }
271 
272         return null;
273     }
274 
275     function placeCursorInCell(cell, placeAtEnd) {
276         var range = new Editor.Range(cell[0].ownerDocument);
277         if (!range['moveToElementEditablePosition'](cell,
278             placeAtEnd ? true : undefined)) {
279             range.selectNodeContents(cell);
280             range.collapse(placeAtEnd ? false : true);
281         }
282         range.select(true);
283     }
284 
285     function getSel(editor) {
286         var selection = editor.getSelection(),
287             startElement = selection && selection.getStartElement(),
288             table = startElement && startElement.closest('table', undefined);
289         if (!table)
290             return undefined;
291         var td = startElement.closest(function (n) {
292             var name = DOM.nodeName(n);
293             return table.contains(n) && (name == "td" || name == "th");
294         }, undefined);
295         var tr = startElement.closest(function (n) {
296             var name = DOM.nodeName(n);
297             return table.contains(n) && name == "tr";
298         }, undefined);
299         return {
300             table:table,
301             td:td,
302             tr:tr
303         };
304     }
305 
306     function ensureTd(editor) {
307         var info = getSel(editor);
308         return info && info.td;
309 
310     }
311 
312     function ensureTr(editor) {
313         var info = getSel(editor);
314         return info && info.tr;
315     }
316 
317     var statusChecker = {
318         表格属性:getSel,
319         删除表格:ensureTd,
320         删除列:ensureTd,
321         删除行:ensureTr,
322         '在上方插入行':ensureTr,
323         '在下方插入行':ensureTr,
324         '在左侧插入列':ensureTd,
325         '在右侧插入列':ensureTd
326     };
327 
328     /**
329      * table 编辑模式下显示虚线边框便于编辑
330      */
331     var showBorderClassName = 'ke_show_border',
332         cssTemplate =
333             // IE6 don't have child selector support,
334             // where nested table cells could be incorrect.
335             ( UA['ie'] === 6 ?
336                 [
337                     'table.%2,',
338                     'table.%2 td, table.%2 th,',
339                     '{',
340                     'border : #d3d3d3 1px dotted',
341                     '}'
342                 ] :
343                 [
344                     ' table.%2,',
345                     ' table.%2 > tr > td,  table.%2 > tr > th,',
346                     ' table.%2 > tbody > tr > td,  table.%2 > tbody > tr > th,',
347                     ' table.%2 > thead > tr > td,  table.%2 > thead > tr > th,',
348                     ' table.%2 > tfoot > tr > td,  table.%2 > tfoot > tr > th',
349                     '{',
350                     'border : #d3d3d3 1px dotted',
351                     '}'
352                 ] ).join(''),
353 
354         cssStyleText = cssTemplate.replace(/%2/g, showBorderClassName),
355 
356         extraDataFilter = {
357             elements:{
358                 'table':function (element) {
359                     var attributes = element.attributes,
360                         cssClass = attributes[ 'class' ],
361                         border = parseInt(attributes.border, 10);
362 
363                     if (!border || border <= 0)
364                         attributes[ 'class' ] = ( cssClass || '' ) + ' ' +
365                             showBorderClassName;
366                 }
367             }
368         },
369 
370         extraHtmlFilter = {
371             elements:{
372                 'table':function (table) {
373                     var attributes = table.attributes,
374                         cssClass = attributes[ 'class' ];
375 
376                     if (cssClass) {
377                         attributes[ 'class' ] = S.trim(cssClass.replace(showBorderClassName, ""));
378                     }
379                 }
380 
381             }
382         };
383 
384     function TablePlugin(config) {
385         this.config = config || {};
386     }
387 
388     S.augment(TablePlugin, {
389         init:function (editor) {
390             /**
391              * 动态加入显表格border css,便于编辑
392              */
393             editor.addCustomStyle(cssStyleText);
394 
395             var dataProcessor = editor.htmlDataProcessor,
396                 dataFilter = dataProcessor && dataProcessor.dataFilter,
397                 htmlFilter = dataProcessor && dataProcessor.htmlFilter;
398 
399             dataFilter.addRules(extraDataFilter);
400             htmlFilter.addRules(extraHtmlFilter);
401 
402             var self = this,
403                 handlers = {
404 
405                     表格属性:function () {
406                         this.hide();
407                         var info = getSel(editor);
408                         if (info) {
409                             DialogLoader.useDialog(editor, "table",
410                                 self.config,
411                                 {
412                                     selectedTable:info.table,
413                                     selectedTd:info.td
414                                 });
415                         }
416                     },
417 
418                     删除表格:function () {
419                         this.hide();
420                         var selection = editor.getSelection(),
421                             startElement = selection && selection.getStartElement(),
422                             table = startElement && startElement.closest('table', undefined);
423 
424                         if (!table) {
425                             return;
426                         }
427 
428                         // Maintain the selection point at where the table was deleted.
429                         selection.selectElement(table);
430                         var range = selection.getRanges()[0];
431                         range.collapse();
432                         selection.selectRanges([ range ]);
433 
434                         // If the table's parent has only one child,
435                         // remove it,except body,as well.( #5416 )
436                         var parent = table.parent();
437                         if (parent[0].childNodes.length == 1 &&
438                             parent.nodeName() != 'body' &&
439                             parent.nodeName() != 'td') {
440                             parent.remove();
441                         } else {
442                             table.remove();
443                         }
444                     },
445 
446                     '删除行 ':function () {
447                         this.hide();
448                         var selection = editor.getSelection();
449                         placeCursorInCell(deleteRows(selection), undefined);
450                     },
451 
452                     '删除列 ':function () {
453                         this.hide();
454                         var selection = editor.getSelection(),
455                             element = deleteColumns(selection);
456                         element && placeCursorInCell(element, true);
457                     },
458 
459                     '在上方插入行':function () {
460                         this.hide();
461                         var selection = editor.getSelection();
462                         insertRow(selection, true);
463                     },
464 
465                     '在下方插入行':function () {
466                         this.hide();
467                         var selection = editor.getSelection();
468                         insertRow(selection, undefined);
469                     },
470 
471                     '在左侧插入列':function () {
472                         this.hide();
473                         var selection = editor.getSelection();
474                         insertColumn(selection, true);
475                     },
476 
477                     '在右侧插入列':function () {
478                         this.hide();
479                         var selection = editor.getSelection();
480                         insertColumn(selection, undefined);
481                     }
482                 };
483 
484             var children = [];
485             S.each(handlers, function (h, name) {
486                 children.push({
487                     content:name
488                 });
489             });
490 
491             editor.addContextMenu("table", function (node) {
492                 if (S.inArray(DOM.nodeName(node), tableRules)) {
493                     return true;
494                 }
495             }, {
496                 width:"120px",
497                 children:children,
498                 listeners:{
499                     click:function (e) {
500                         var content = e.target.get("content");
501                         if (handlers[content]) {
502                             handlers[content].apply(this);
503                         }
504 
505                     },
506                     beforeVisibleChange:function (e) {
507                         if (e.newVal) {
508                             var self = this, children = self.get("children");
509                             var editor = self.get("editor");
510                             S.each(children, function (c) {
511                                 var content = c.get("content");
512                                 if (!statusChecker[content] ||
513                                     statusChecker[content].call(self, editor)) {
514                                     c.set("disabled", false);
515                                 } else {
516                                     c.set("disabled", true);
517                                 }
518                             });
519 
520                         }
521                     }
522                 }
523             });
524 
525             editor.addButton("table", {
526                 mode:Editor.WYSIWYG_MODE,
527                 listeners:{
528                     click:function () {
529                         DialogLoader.useDialog(editor, "table",
530                             self.config,
531                             {
532                                 selectedTable:0,
533                                 selectedTd:0
534                             });
535 
536                     }
537                 },
538                 tooltip:"插入表格"
539             });
540 
541         }
542     });
543 
544     return TablePlugin;
545 }, {
546     requires:['editor', '../dialogLoader/', '../contextmenu/']
547 });