From 994062c052bc6c4c13e9ac4e9581c7f619d20dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A0=95=ED=99=98/FE=EA=B0=9C=EB=B0=9C=EB=9E=A9/?= =?UTF-8?q?NE?= Date: Fri, 15 Jul 2016 16:11:10 +0900 Subject: [PATCH] enhance table paste process & add T/C - fixed #493 (#618) * change condition statement priority * add condition for non collapsed range check * change condition for multi TD paste & add removeTableContents() * change isInTable() to public & extract pasting branch logic from WwPasteContentHelper * modify condition for table removal * solve TD paste & wrap TDs into TR and TRs into TBODY * extract _completeIncompleteTable() method related * refactor: _getTableDataFromTable * change table completion stage --- apps/core/src/js/domUtils.js | 56 +++- apps/core/src/js/wwClipboardManager.js | 8 +- apps/core/src/js/wwPasteContentHelper.js | 39 ++- apps/core/src/js/wwTableManager.js | 308 +++++++++++++++++- apps/core/test/domUtils.spec.js | 49 +++ apps/core/test/wwClipboardManager.spec.js | 20 +- apps/core/test/wwPasteContentHelper.spec.js | 39 +++ apps/core/test/wwTableManager.spec.js | 338 +++++++++++++++++++- 8 files changed, 837 insertions(+), 20 deletions(-) diff --git a/apps/core/src/js/domUtils.js b/apps/core/src/js/domUtils.js index ee4ed4f4b6..b9bce3a5c1 100644 --- a/apps/core/src/js/domUtils.js +++ b/apps/core/src/js/domUtils.js @@ -383,6 +383,58 @@ var getPath = function(node, root) { return paths; }; +/** + * Find next TR's TD element by given TD and it's offset + * @param {HTMLElement} node TD element + * @param {boolean} [needFirstTd] Boolean value for find first TD in next line + * @returns {HTMLElement|null} + */ +var nextLineTableCell = function(node, needFirstTd) { + var index = 0; + var nextLineTrElement, nextLineTdElement, theadElement; + + if (node) { + if (!needFirstTd) { + while (node.previousElementSibling) { + node = node.previousElementSibling; + index += 1; + } + } + + nextLineTrElement = node.parentNode.nextSibling; + theadElement = $(node).parents('thead')[0]; + + if (nextLineTrElement) { + nextLineTdElement = nextLineTrElement.childNodes[index]; + } else if (theadElement && theadElement.nextElementSibling.tagName === 'TBODY') { + nextLineTdElement = $(theadElement.nextElementSibling).find('td')[index]; + } + + if (nextLineTdElement && nextLineTdElement.tagName === 'TD') { + return nextLineTdElement; + } + } + + return null; +}; + +/** + * Find next TD or TH element by given TE element + * @param {HTMLElement} node TD element + * @returns {HTMLElement|null} + */ +var nextTableCell = function(node) { + var nextElement; + + nextElement = node.nextElementSibling; + + if (nextElement && (nextElement.nodeName === 'TD' || nextElement.nodeName === 'TH')) { + return nextElement; + } + + return null; +}; + module.exports = { getNodeName: getNodeName, isTextNode: isTextNode, @@ -399,5 +451,7 @@ module.exports = { getPrevTextNode: getPrevTextNode, findOffsetNode: findOffsetNode, getPath: getPath, - getNodInfo: getNodeInfo + getNodInfo: getNodeInfo, + nextLineTableCell: nextLineTableCell, + nextTableCell: nextTableCell }; diff --git a/apps/core/src/js/wwClipboardManager.js b/apps/core/src/js/wwClipboardManager.js index 82fd121437..73feb2525d 100644 --- a/apps/core/src/js/wwClipboardManager.js +++ b/apps/core/src/js/wwClipboardManager.js @@ -74,7 +74,7 @@ WwClipboardManager.prototype._initSquireEvent = function() { self._pch.preparePaste(pasteData); self.wwe.eventManager.emit('pasteBefore', {source: 'wysiwyg', data: pasteData}); - self._refineCursorWithPasteContents(pasteData.fragment); + self._refineCursorWithPasteContentsIfNeed(pasteData.fragment); self.wwe.postProcessForChange(); }); }; @@ -84,10 +84,14 @@ WwClipboardManager.prototype._initSquireEvent = function() { * @param {DocumentFragment} fragment Copied contents * @private */ -WwClipboardManager.prototype._refineCursorWithPasteContents = function(fragment) { +WwClipboardManager.prototype._refineCursorWithPasteContentsIfNeed = function(fragment) { var node = fragment; var range = this.wwe.getEditor().getSelection().cloneRange(); + if (fragment.childNodes.length === 0) { + return; + } + while (node.lastChild) { node = node.lastChild; } diff --git a/apps/core/src/js/wwPasteContentHelper.js b/apps/core/src/js/wwPasteContentHelper.js index 734ff17dbf..35b9d9890e 100644 --- a/apps/core/src/js/wwPasteContentHelper.js +++ b/apps/core/src/js/wwPasteContentHelper.js @@ -31,7 +31,9 @@ WwPasteContentHelper.prototype.preparePaste = function(pasteData) { var range = this.wwe.getEditor().getSelection().cloneRange(); var newFragment = this.wwe.getEditor().getDocument().createDocumentFragment(); var firstBlockIsTaken = false; - var nodeName, node, childNodes; + var codeblockManager = this.wwe.getManager('codeblock'); + var tableManager = this.wwe.getManager('table'); + var nodeName, node, childNodes, isPastingList; pasteData.fragment = this._pasteFirstAid(pasteData.fragment); @@ -47,10 +49,14 @@ WwPasteContentHelper.prototype.preparePaste = function(pasteData) { while (childNodes.length) { node = childNodes[0]; nodeName = domUtils.getNodeName(node); - - if (this.wwe.getManager('codeblock').isInCodeBlock(range)) { - newFragment.appendChild(this.wwe.getManager('codeblock').prepareToPasteOnCodeblock(childNodes)); - } else if (nodeName === 'LI' || nodeName === 'UL' || nodeName === 'OL') { + isPastingList = nodeName === 'LI' || nodeName === 'UL' || nodeName === 'OL'; + + if (codeblockManager.isInCodeBlock(range)) { + newFragment.appendChild(codeblockManager.prepareToPasteOnCodeblock(childNodes)); + } else if (tableManager.isInTable(range)) { + newFragment = tableManager.prepareToPasteOnTable(pasteData, node); + childNodes.shift(); + } else if (isPastingList) { newFragment.appendChild(this._prepareToPasteList(childNodes, pasteData.rangeInfo, firstBlockIsTaken)); //첫번째 현재위치와 병합될 가능성이있는 컨텐츠가 만들어진경우는 이후 위치에 대한 정보가 필요없다 firstBlockIsTaken = true; @@ -104,6 +110,8 @@ WwPasteContentHelper.prototype._pasteFirstAid = function(fragment) { this._preElementAid(fragment); + this._tableElementAid(fragment); + //br은 preElemnetAid에서 필요해서 처리후 불필요한 br은 삭제한다. $(fragment).find('br').remove(); @@ -333,4 +341,25 @@ WwPasteContentHelper.prototype._makeNodeAndAppend = function(pathInfo, content) return node[0]; }; +/** + * Pasting table element pre-process + * @param {DocumentFragment} fragment pasteData's fragment + * @memberOf WwPasteContentHelper + * @private + */ +WwPasteContentHelper.prototype._tableElementAid = function(fragment) { + var tableManager = this.wwe.getManager('table'); + var wrapperTable = tableManager.wrapTheadAndTbodyIntoTableIfNeed(fragment); + var wrapperTr = tableManager.wrapDanglingTableCellsIntoTrIfNeed(fragment); + var wrapperTbody = tableManager.wrapTrsIntoTbodyIfNeed(fragment); + + if (wrapperTr) { + $(fragment).append(wrapperTr); + } else if (wrapperTbody) { + $(fragment).append(wrapperTbody); + } else if (wrapperTable) { + $(fragment).append(wrapperTable); + } +}; + module.exports = WwPasteContentHelper; diff --git a/apps/core/src/js/wwTableManager.js b/apps/core/src/js/wwTableManager.js index a56a309301..5668348d70 100644 --- a/apps/core/src/js/wwTableManager.js +++ b/apps/core/src/js/wwTableManager.js @@ -52,6 +52,7 @@ WwTableManager.prototype._initEvent = function() { this.eventManager.listen('wysiwygRangeChangeAfter', function() { self._unwrapBlockInTable(); + self.wwe.defer(self._completeTableIfNeed.bind(self)); }); this.eventManager.listen('wysiwygSetValueAfter', function() { @@ -62,6 +63,13 @@ WwTableManager.prototype._initEvent = function() { //remove last br in td or th return html.replace(/
(<\/td>|<\/th>)/g, '$1'); }); + + this.wwe.getEditor().addEventListener('paste', function(ev) { + var range = self.wwe.getEditor().getSelection(); + if (self.isInTable(range) && !range.collapsed) { + ev.preventDefault(); + } + }); }; /** @@ -74,7 +82,7 @@ WwTableManager.prototype._initKeyHandler = function() { var self = this; this.wwe.addKeyEventHandler(function(ev, range) { - if (self._isInTable(range)) { + if (self.isInTable(range)) { self._recordUndoStateIfNeed(range); } else if (self._lastCellNode) { self._recordUndoStateAndResetCellNode(range); @@ -93,7 +101,7 @@ WwTableManager.prototype._initKeyHandler = function() { ev.preventDefault(); self.wwe.breakToNewDefaultBlock(range, 'before'); isNeedNext = false; - } else if (self._isInTable(range)) { + } else if (self.isInTable(range)) { self._appendBrIfTdOrThNotHaveAsLastChild(range); isNeedNext = false; } @@ -105,7 +113,7 @@ WwTableManager.prototype._initKeyHandler = function() { var isNeedNext; if (range.collapsed) { - if (self._isInTable(range)) { + if (self.isInTable(range)) { self._tableHandlerOnBackspace(range, ev); isNeedNext = false; } else if (self._isAfterTable(range)) { @@ -113,6 +121,12 @@ WwTableManager.prototype._initKeyHandler = function() { self._removeTableOnBackspace(range); isNeedNext = false; } + } else if (self.isInTable(range)) { + if (range.commonAncestorContainer.nodeType !== 3) { + ev.preventDefault(); + self._removeTableContents(range); + isNeedNext = false; + } } return isNeedNext; @@ -120,23 +134,25 @@ WwTableManager.prototype._initKeyHandler = function() { }; /** - * _isInTable + * isInTable * Check whether passed range is in table or not * @param {Range} range range * @returns {boolean} result * @memberOf WwTableManager - * @private + * @api */ -WwTableManager.prototype._isInTable = function(range) { - var target; +WwTableManager.prototype.isInTable = function(range) { + var target, result; if (range.collapsed) { target = range.startContainer; + result = !!$(target).closest('table').length; } else { target = range.commonAncestorContainer; + result = !!$(target).closest('table').length || !!$(range.cloneContents()).find('table').length; } - return !!$(target).closest('table').length; + return result; }; /** @@ -268,4 +284,280 @@ WwTableManager.prototype._recordUndoStateAndResetCellNode = function(range) { this._lastCellNode = null; }; +/** + * Paste table data into table element + * @param {DocumentFragment} fragment Fragment of table element within + * @memberOf WwTableManager + * @api + */ +WwTableManager.prototype.pasteDataIntoTable = function(fragment) { + var range = this.wwe.getEditor().getSelection(); + var tableData = this._getTableDataFromTable(fragment); + var startContainer = range.startContainer; + var parentNode = startContainer.parentNode; + var isTextInTableCell = parentNode.tagName === 'TD' || parentNode.tagName === 'TH'; + var isTableCell = startContainer.tagName === 'TD' || startContainer.tagName === 'TH'; + var isTextNode = startContainer.nodeType === 3; + var anchorElement, td, tr; + + if (isTextNode && isTextInTableCell) { + anchorElement = parentNode; + } else if (isTableCell) { + anchorElement = startContainer; + } else { + anchorElement = $(startContainer).find('th,td')[0]; + } + + td = anchorElement; + + while (tableData.length) { + tr = tableData.shift(); + + while (td && tr.length) { + td.textContent = tr.shift(); + + td = domUtils.nextTableCell(td); + } + + td = domUtils.nextLineTableCell(anchorElement); + anchorElement = td; + } +}; + +/** + * Get array data from table element + * @param {DocumentFragment} fragment table element + * @returns {Array} + * @private + */ +WwTableManager.prototype._getTableDataFromTable = function(fragment) { + var $fragment = $(fragment); + var tableData = []; + var trs = $fragment.find('tr'); + + trs.each(function(i, tr) { + var trData = []; + var tds = $(tr).children(); + + tds.each(function(index, cell) { + trData.push(cell.textContent); + }); + + tableData.push(trData); + }); + + return tableData; +}; +/** + * Remove selected table contents + * @param {Range} range Range object + * @private + */ +WwTableManager.prototype._removeTableContents = function(range) { + var anchorCell = range.startContainer; + var cellLength = $(range.cloneContents()).find('th,td').length; + var index = 0; + var cell, nextCell, nextLineFirstCell; + if (domUtils.isTextNode(anchorCell)) { + anchorCell = anchorCell.parentNode; + } + cell = anchorCell; + + this.wwe.getEditor().saveUndoState(); + for (;index < cellLength; index += 1) { + cell.innerHTML = '
'; + + nextCell = domUtils.nextTableCell(cell); + nextLineFirstCell = domUtils.nextLineTableCell(cell, true); + + if (nextCell) { + cell = nextCell; + } else if (nextLineFirstCell) { + cell = nextLineFirstCell; + } else { + cell = null; + } + } +}; + +/** + * Wrap dangling table cells with new TR + * @param {DocumentFragment} fragment Pasting data + * @returns {HTMLElement|null} + */ +WwTableManager.prototype.wrapDanglingTableCellsIntoTrIfNeed = function(fragment) { + var danglingTableCells = $(fragment).children('td,th'); + var $wrapperTr, tr; + + if (danglingTableCells.length) { + $wrapperTr = $(''); + + danglingTableCells.each(function(i, cell) { + $wrapperTr.append(cell); + }); + + tr = $wrapperTr[0]; + } + + return tr; +}; + +/** + * Wrap TRs with new TBODY + * @param {DocumentFragment} fragment Pasting data + * @returns {HTMLElement|null} + */ +WwTableManager.prototype.wrapTrsIntoTbodyIfNeed = function(fragment) { + var danglingTrs = $(fragment).children('tr'); + var ths = danglingTrs.find('th'); + var $wrapperTableBody, tbody; + + if (ths.length) { + ths.each(function(i, node) { + var $node = $(node); + var td = $(''); + + td.html($node.html()); + td.insertBefore(node); + + $node.detach(); + }); + } + + if (danglingTrs.length) { + $wrapperTableBody = $(''); + + danglingTrs.each(function(i, tr) { + $wrapperTableBody.append(tr); + }); + + tbody = $wrapperTableBody[0]; + } + + return tbody; +}; + +/** + * Wrap THEAD followed by TBODY both into Table + * @param {DocumentFragment} fragment Pasting data + * @returns {HTMLElement|null} + */ +WwTableManager.prototype.wrapTheadAndTbodyIntoTableIfNeed = function(fragment) { + var danglingThead = $(fragment).children('thead'); + var danglingTbody = $(fragment).children('tbody'); + var $wrapperTable, table; + + if (danglingTbody.length && danglingThead.length) { + $wrapperTable = $('
'); + $wrapperTable.append(danglingThead); + $wrapperTable.append(danglingTbody); + table = $wrapperTable[0]; + } + + return table; +}; +/** + * Prepare to paste data on table + * @param {object} pasteData Pasting data + * @param {HTMLElement} node Current pasting element + * @returns {DocumentFragment} + * @memberOf WwTableManager + * @api + */ +WwTableManager.prototype.prepareToPasteOnTable = function(pasteData, node) { + var newFragment = document.createDocumentFragment(); + if (this.isTablePaste(node.nodeName)) { + this.pasteDataIntoTable(pasteData.fragment); + pasteData.fragment = newFragment; + } else { + newFragment.textContent = newFragment.textContent + pasteData.fragment.textContent; + } + + return newFragment; +}; + +/** + * Whether pasting element is table element + * @param {string} pastingNodeName Pasting node name + * @returns {boolean} + * @memberOf WwTableManager + * @api + */ +WwTableManager.prototype.isTablePaste = function(pastingNodeName) { + return pastingNodeName === 'TABLE' || pastingNodeName === 'TBODY' + || pastingNodeName === 'THEAD' || pastingNodeName === 'TR' || pastingNodeName === 'TD'; +}; + +/** + * Generate table cell HTML text + * @param {number} amount Amount of cells + * @param {string} tagName Tag name of cell 'td' or 'th' + * @private + * @returns {string} + */ +function tableCellGenerator(amount, tagName) { + var i; + var tdString = ''; + for (i = 0; i < amount; i += 1) { + tdString = tdString + '<' + tagName + '>
'; + } + + return tdString; +} +/** + * Complete passed table + * @param {HTMLElement} node Table inner element + * @private + */ +WwTableManager.prototype._compleIncompleteTable = function(node) { + var table = $('
'); + var thead = $(''); + var tbody = $(''); + var tr = $(''); + var $node = $(node); + var nodeName = node.tagName; + var theadContents, tbodyContents; + + table.insertAfter(node); + + if (nodeName === 'TBODY') { + tr.append(tableCellGenerator($node.find('tr').eq(0).find('td').length, 'th')); + thead.append(tr); + + tbody = node; + } else if (nodeName === 'THEAD') { + thead = node; + + tr.append(tableCellGenerator($node.find('th').length, 'td')); + tbody.append(tr); + } else if (nodeName === 'TR') { + if ($node.children()[0].tagName === 'TH') { + theadContents = node; + tbodyContents = tableCellGenerator($node.find('th').length, 'td'); + } else { + theadContents = tableCellGenerator($node.find('td').length, 'th'); + tbodyContents = node; + } + thead.append(theadContents); + tbody.append(tbodyContents); + } + table.append(thead); + table.append(tbody); +}; + +/** + * Whole editor body searching incomplete table completion + * @private + */ +WwTableManager.prototype._completeTableIfNeed = function() { + var $body = this.wwe.getEditor().get$Body(); + var self = this; + + $body.children().each(function(index, node) { + if (!self.isTablePaste(node.nodeName) || node.nodeName === 'TABLE') { + return; + } + self._compleIncompleteTable(node); + }); +}; module.exports = WwTableManager; diff --git a/apps/core/test/domUtils.spec.js b/apps/core/test/domUtils.spec.js index d26841d0eb..64d13a0f43 100644 --- a/apps/core/test/domUtils.spec.js +++ b/apps/core/test/domUtils.spec.js @@ -263,4 +263,53 @@ describe('domUtils', function() { expect(domUtils.getPath($dom.find('span')[0], $dom[0])).toEqual(expected); }); }); + describe('table traversal', function() { + it('nextTableCell should get next TH if exist', function() { + var $dom = $('' + + '' + + '' + + '
12
34
56
'); + var result = domUtils.nextTableCell($dom.find('th')[0]); + var result2 = domUtils.nextTableCell($dom.find('th')[1]); + expect(result.textContent).toBe('2'); + expect(result2).toBeNull(); + }); + + it('nextTableCell should get next TD if exist', function() { + var $dom = $('' + + '' + + '' + + '
12
34
56
'); + var result = domUtils.nextTableCell($dom.find('td')[0]); + var result2 = domUtils.nextTableCell($dom.find('td')[3]); + expect(result.textContent).toBe('4'); + expect(result2).toBeNull(); + }); + + it('nextLineTableCell should get next TD if exist at thead', function() { + var $dom = $('' + + '' + + '' + + '
12
34
56
'); + var result = domUtils.nextLineTableCell($dom.find('th')[0]); + var result2 = domUtils.nextLineTableCell($dom.find('th')[1]); + var result3 = domUtils.nextLineTableCell($dom.find('th')[3]); + expect(result.textContent).toBe('3'); + expect(result2.textContent).toBe('4'); + expect(result3).toBeNull(); + }); + + it('nextLineTableCell should get next TD if exist at tbody', function() { + var $dom = $('' + + '' + + '' + + '
12
34
56
'); + var result = domUtils.nextLineTableCell($dom.find('td')[0]); + var result2 = domUtils.nextLineTableCell($dom.find('td')[1]); + var result3 = domUtils.nextLineTableCell($dom.find('td')[3]); + expect(result.textContent).toBe('5'); + expect(result2.textContent).toBe('6'); + expect(result3).toBeNull(); + }); + }) }); diff --git a/apps/core/test/wwClipboardManager.spec.js b/apps/core/test/wwClipboardManager.spec.js index 310a423989..95e50f6f82 100644 --- a/apps/core/test/wwClipboardManager.spec.js +++ b/apps/core/test/wwClipboardManager.spec.js @@ -22,13 +22,13 @@ describe('WwClipboardManager', function() { $('body').empty(); }); - describe('_refineCursorWithPasteContents', function() { + describe('_refineCursorWithPasteContentsIfNeed', function() { it('set selection to last element of contents', function(done) { var fragment = wwe.getEditor().getDocument().createDocumentFragment(); var range; $(fragment).append('