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 });