From d76a9bab712c27f857f9d08c8a38aa0dbd0ced05 Mon Sep 17 00:00:00 2001 From: Sungho Kim Date: Thu, 28 Jan 2016 19:07:13 +0900 Subject: [PATCH] ww: enhance hr control resolved #425, resolved #426 --- apps/core/src/js/domUtils.js | 23 ++-- apps/core/src/js/wwHrManager.js | 102 +++++++++++++----- apps/core/src/js/wwTableManager.js | 2 +- apps/core/src/js/wwTaskManager.js | 6 +- apps/core/src/js/wysiwygCommands/hr.js | 30 +++--- apps/core/src/js/wysiwygEditor.js | 11 +- apps/core/test/domUtils.spec.js | 122 +++++++++++----------- apps/core/test/wwHrManager.spec.js | 75 +++++++++++-- apps/core/test/wysiwygCommands/hr.spec.js | 67 ++++++++++++ 9 files changed, 300 insertions(+), 138 deletions(-) create mode 100644 apps/core/test/wysiwygCommands/hr.spec.js diff --git a/apps/core/src/js/domUtils.js b/apps/core/src/js/domUtils.js index c7a1f69813..72f4489c67 100644 --- a/apps/core/src/js/domUtils.js +++ b/apps/core/src/js/domUtils.js @@ -25,19 +25,6 @@ var isElemNode = function(node) { return node && node.nodeType === Node.ELEMENT_NODE; }; -/** - * getChildNodeAt - * Get child node in given parent and index - * @param {HTMLElement} elem parent element - * @param {number} index node index - * @return {Node} child - */ -var getChildNodeAt = function(elem, index) { - if (elem.childNodes.length && index >= 0) { - return elem.childNodes[index]; - } -}; - /** * getNodeName * Get node name of node @@ -117,7 +104,7 @@ var getChildNodeByOffset = function(node, index) { if (isTextNode(node)) { currentNode = node; - } else { + } else if (node.childNodes.length && index >= 0) { currentNode = node.childNodes[index]; } @@ -249,8 +236,11 @@ var getNextTopBlockNode = function(node) { return getNodeWithDirectionUnderParent('next', node, 'BODY'); }; +var getTopBlockNode = function(node) { + return getParentUntil(node, 'BODY'); +}; + module.exports = { - getChildNodeAt: getChildNodeAt, getNodeName: getNodeName, isTextNode: isTextNode, isElemNode: isElemNode, @@ -261,5 +251,6 @@ module.exports = { getChildNodeByOffset: getChildNodeByOffset, getPrevTopBlockNode: getPrevTopBlockNode, getNextTopBlockNode: getNextTopBlockNode, - getParentUntil: getParentUntil + getParentUntil: getParentUntil, + getTopBlockNode: getTopBlockNode }; diff --git a/apps/core/src/js/wwHrManager.js b/apps/core/src/js/wwHrManager.js index f2bf582812..d28df97779 100644 --- a/apps/core/src/js/wwHrManager.js +++ b/apps/core/src/js/wwHrManager.js @@ -51,14 +51,16 @@ WwHrManager.prototype._initEvent = function() { WwHrManager.prototype._initKeyHandler = function() { var self = this; - this.wwe.addKeyEventHandler('ENTER', function(event, range) { - if (self._isInHr(range) || self._isNearHr(range)) { - return self._removeHrIfNeed(range, event); - } + this.wwe.addKeyEventHandler(function(ev, range, keyMap) { + return self._onTypedInHr(range, keyMap); + }); + + this.wwe.addKeyEventHandler('ENTER', function(ev, range) { + return self._removeHrOnEnter(range, ev); }); - this.wwe.addKeyEventHandler('BACK_SPACE', function(event, range) { - return self._removeHrIfNeed(range, event); + this.wwe.addKeyEventHandler('BACK_SPACE', function(ev, range) { + return self._removeHrOnBackspace(range, ev); }); }; @@ -79,42 +81,90 @@ WwHrManager.prototype._isInHr = function(range) { * @returns {boolean} result */ WwHrManager.prototype._isNearHr = function(range) { - var prevNode = domUtils.getChildNodeAt(range.startContainer, range.startOffset - 1); + var prevNode = domUtils.getChildNodeByOffset(range.startContainer, range.startOffset - 1); return domUtils.getNodeName(prevNode) === 'HR'; }; +WwHrManager.prototype._onTypedInHr = function(range, keyMap) { + //HR위에서 테스트 컨텐츠 입력을 시도한경우 + if ((this._isInHr(range) || this._isNearHr(range)) + && (!keyMap.length || /^[A-Z0-9]$/.test(keyMap)) + ) { + this.wwe.breakToNewDefaultBlock(range, 'before'); + return false; + } +}; + /** - * _removeHrIfNeed - * Remove hr if need + * _removeHrOnEnter + * Remove hr if need on enter * @param {Range} range range - * @param {Event} event event + * @param {Event} ev event * @returns {boolean} return true if hr was removed */ -WwHrManager.prototype._removeHrIfNeed = function(range, event) { - var hrSuspect, cursorTarget; +WwHrManager.prototype._removeHrOnEnter = function(range, ev) { + var hrSuspect, blockPosition; + + if (!range.collapsed) { + return; + } if (this._isInHr(range)) { - hrSuspect = domUtils.getChildNodeAt(range.startContainer, range.startOffset); - } else if (range.startOffset === 0) { - hrSuspect = range.startContainer.previousSibling || range.startContainer.parentNode.previousSibling; + hrSuspect = domUtils.getChildNodeByOffset(range.startContainer, range.startOffset); + } else if (this._isNearHr(range)) { + hrSuspect = domUtils.getChildNodeByOffset(range.startContainer, range.startOffset - 1); + blockPosition = 'before'; + } - if (domUtils.getNodeName(hrSuspect) !== 'HR') { - hrSuspect = null; - } + return this._changeHrToNewDefaultBlock(hrSuspect, range, ev, blockPosition); +}; + +/** + * _removeHrOnBackspace + * Remove hr if need on backspace + * @param {Range} range range + * @param {Event} ev event + * @returns {boolean} return true if hr was removed + */ +WwHrManager.prototype._removeHrOnBackspace = function(range, ev) { + var hrSuspect, blockPosition; + + if (!range.collapsed) { + return; + } + + if (this._isInHr(range)) { + hrSuspect = domUtils.getChildNodeByOffset(range.startContainer, range.startOffset); + } else if (range.startOffset === 0) { + hrSuspect = domUtils.getPrevTopBlockNode(range.startContainer); + blockPosition = 'none'; } else if (this._isNearHr(range)) { - hrSuspect = domUtils.getChildNodeAt(range.startContainer, range.startOffset - 1); + hrSuspect = domUtils.getChildNodeByOffset(range.startContainer, range.startOffset - 1); + blockPosition = 'before'; } - if (hrSuspect) { - event.preventDefault(); + return this._changeHrToNewDefaultBlock(hrSuspect, range, ev, blockPosition); +}; + +/** + * _changeHrToNewDefaultBlock + * Remove hr and add new default block then set range to it + * @param {Node} hrSuspect Node could be hr + * @param {Range} range range + * @param {Event} ev event + * @param {strong} newBlockPosition new default block add position + * @returns {boolean} return true if hr was removed + */ +WwHrManager.prototype._changeHrToNewDefaultBlock = function(hrSuspect, range, ev, newBlockPosition) { + if (hrSuspect && domUtils.getNodeName(hrSuspect) === 'HR') { + ev.preventDefault(); + + if (newBlockPosition !== 'none') { + this.wwe.breakToNewDefaultBlock(range, newBlockPosition); + } - cursorTarget = hrSuspect.nextSibling; $(hrSuspect).remove(); - range.setStartBefore(cursorTarget); - range.collapse(true); - this.wwe.getEditor().setSelection(range); - return false; } }; diff --git a/apps/core/src/js/wwTableManager.js b/apps/core/src/js/wwTableManager.js index e2d745972a..98e6251fd0 100644 --- a/apps/core/src/js/wwTableManager.js +++ b/apps/core/src/js/wwTableManager.js @@ -132,7 +132,7 @@ WwTableManager.prototype._isInTable = function(range) { * @returns {boolean} result */ WwTableManager.prototype._isBeforeTable = function(range) { - return domUtils.getNodeName(domUtils.getChildNodeAt(range.startContainer, range.startOffset)) === 'TABLE'; + return domUtils.getNodeName(domUtils.getChildNodeByOffset(range.startContainer, range.startOffset)) === 'TABLE'; }; /** diff --git a/apps/core/src/js/wwTaskManager.js b/apps/core/src/js/wwTaskManager.js index 06b0bda3a3..a2ae8f57dc 100644 --- a/apps/core/src/js/wwTaskManager.js +++ b/apps/core/src/js/wwTaskManager.js @@ -232,14 +232,14 @@ WwTaskManager.prototype._unformatTaskIfNeedOnBackspace = function(range) { if (domUtils.isElemNode(startContainer)) { //태스크리스트의 제일 첫 오프셋인경우(인풋박스 바로 위) if (startOffset === 0) { - prevEl = domUtils.getChildNodeAt(startContainer, startOffset); + prevEl = domUtils.getChildNodeByOffset(startContainer, startOffset); //inputbox 오른편 어딘가에서 지워지는경우 } else { - prevEl = domUtils.getChildNodeAt(startContainer, startOffset - 1); + prevEl = domUtils.getChildNodeByOffset(startContainer, startOffset - 1); //지워질위치가 인풋스페이스 텍스트 영역으로 의심되는경우 그다음 엘리먼드로 prevEl을 지정해준다.(그다음이 input이면 지워지도록) if (domUtils.isTextNode(prevEl) && prevEl.nodeValue.length === 1 && FIND_TASK_SPACES_RX.test(prevEl.nodeValue)) { - prevEl = domUtils.getChildNodeAt(startContainer, startOffset - 2); + prevEl = domUtils.getChildNodeByOffset(startContainer, startOffset - 2); } } diff --git a/apps/core/src/js/wysiwygCommands/hr.js b/apps/core/src/js/wysiwygCommands/hr.js index 1f9acb20d8..3a793529f9 100644 --- a/apps/core/src/js/wysiwygCommands/hr.js +++ b/apps/core/src/js/wysiwygCommands/hr.js @@ -5,7 +5,8 @@ 'use strict'; -var CommandManager = require('../commandManager'); +var CommandManager = require('../commandManager'), + domUtils = require('../domUtils'); /** * HR @@ -22,29 +23,32 @@ var HR = CommandManager.command('wysiwyg', /** @lends HR */{ * @param {WysiwygEditor} wwe WYsiwygEditor instance */ exec: function(wwe) { - var sq = wwe.getEditor(); + var sq = wwe.getEditor(), + range = sq.getSelection(), + nextBlockNode; - if (!sq.getSelection().collapsed || sq.hasFormat('TABLE')) { + if (!range.collapsed || sq.hasFormat('TABLE')) { sq.focus(); return; } - sq.modifyBlocks(function(frag) { - /* - var block = sq.createElement('DIV'); - var newFrag = sq._doc.createDocumentFragment(); + nextBlockNode = domUtils.getNextTopBlockNode(domUtils.getChildNodeByOffset(range.startContainer, range.startOffset)); - newFrag.appendChild(frag); - newFrag.appendChild(block); + if (!nextBlockNode) { + nextBlockNode = sq.createDefaultBlock(); + wwe.get$Body().append(nextBlockNode); + } - block.appendChild(sq.createElement('BR')); - block.appendChild(sq.createElement('HR')); -*/ + sq.modifyBlocks(function(frag) { frag.appendChild(sq.createElement('HR')); - return frag; }); + range.selectNodeContents(nextBlockNode); + range.collapse(true); + + sq.setSelection(range); + sq.focus(); } }); diff --git a/apps/core/src/js/wysiwygEditor.js b/apps/core/src/js/wysiwygEditor.js index f560b9ec16..7c6bab6772 100644 --- a/apps/core/src/js/wysiwygEditor.js +++ b/apps/core/src/js/wysiwygEditor.js @@ -390,13 +390,6 @@ WysiwygEditor.prototype._onKeyDown = function(keyboardEvent) { WysiwygEditor.prototype._initDefaultKeyEventHandler = function() { var self = this; - this.addKeyEventHandler('ENTER', function(ev, range) { - if (self._isInOrphanText(range)) { - self._wrapDefaultBlockTo(range); - return false; - } - }); - this.addKeyEventHandler('BACK_SPACE', function(ev, range) { if (!range.collapsed) { self.postProcessForChange(); @@ -454,7 +447,7 @@ WysiwygEditor.prototype._wrapDefaultBlockTo = function(range) { block = this.getEditor().createDefaultBlock([range.startContainer]); //range for insert block - insertTargetNode = domUtils.getChildNodeAt(range.startContainer, range.startOffset); + insertTargetNode = domUtils.getChildNodeByOffset(range.startContainer, range.startOffset); if (insertTargetNode) { range.setStartBefore(insertTargetNode); } else { @@ -814,7 +807,7 @@ WysiwygEditor.prototype.hasFormatWithRx = function(rx) { WysiwygEditor.prototype.breakToNewDefaultBlock = function(range, where) { var div, pathToBody, appendBefore, currentNode; - currentNode = domUtils.getChildNodeAt(range.startContainer, range.startOffset) || range.startContainer; + currentNode = domUtils.getChildNodeByOffset(range.startContainer, range.startOffset) || domUtils.getChildNodeByOffset(range.startContainer, range.startOffset - 1); pathToBody = $(currentNode).parentsUntil('body'); diff --git a/apps/core/test/domUtils.spec.js b/apps/core/test/domUtils.spec.js index b90824b923..ee55f7c58b 100644 --- a/apps/core/test/domUtils.spec.js +++ b/apps/core/test/domUtils.spec.js @@ -7,63 +7,6 @@ describe('domUtils', function() { $('body').empty(); }); - describe('getChildNodeAt()', function() { - it('returns childNodes at index', function() { - var result; - - $('body').html([ - '' - ].join('')); - - result = domUtils.getChildNodeAt($('ul')[0], 1); - - expect(result).toEqual($('li')[1]); - }); - - it('returns undefined if theres no result', function() { - var result; - - $('body').html([ - '' - ].join('')); - - result = domUtils.getChildNodeAt($('ul')[0], 2); - - expect(result).toBeUndefined(); - }); - - it('returns undefined if there is no child', function() { - var result; - - $('body').html(''); - - result = domUtils.getChildNodeAt($('ul')[0], 2); - - expect(result).toBeUndefined(); - }); - - it('returns childNodes if index >= 0', function() { - var result; - - $('body').html([ - '' - ].join('')); - - result = domUtils.getChildNodeAt($('ul')[0], -1); - - expect(result).toBeUndefined(); - }); - }); - describe('getNodeName', function() { it('returns tagName if passed Node is ELEMENT_NODE', function() { expect(domUtils.getNodeName($('
')[0])).toEqual('DIV'); @@ -94,7 +37,7 @@ describe('domUtils', function() { }); }); - describe('getOffsetLength', function() { + describe('getTextLength', function() { it('returns node\'s text content length', function() { expect(domUtils.getTextLength($('

hi

')[0])).toBe(2); expect(domUtils.getTextLength($('

hi

')[0].firstChild)).toBe(2); @@ -149,7 +92,7 @@ describe('domUtils', function() { }); }); - describe('getNodeByOffset()', function() { + describe('getChildNodeByOffset()', function() { it('return node\'s childNode with index', function() { var node = $('

textweafwae

'); expect(domUtils.getChildNodeByOffset(node[0], 1)).toBe(node[0].childNodes[1]); @@ -159,6 +102,60 @@ describe('domUtils', function() { var node = $('

text

'); expect(domUtils.getChildNodeByOffset(node[0].childNodes[0], 1)).toBe(node[0].childNodes[0]); }); + it('returns childNodes at index', function() { + var result; + + $('body').html([ + '' + ].join('')); + + result = domUtils.getChildNodeByOffset($('ul')[0], 1); + + expect(result).toEqual($('li')[1]); + }); + + it('returns undefined if theres no result', function() { + var result; + + $('body').html([ + '' + ].join('')); + + result = domUtils.getChildNodeByOffset($('ul')[0], 2); + + expect(result).toBeUndefined(); + }); + + it('returns undefined if there is no child', function() { + var result; + + $('body').html(''); + + result = domUtils.getChildNodeByOffset($('ul')[0], 2); + + expect(result).toBeUndefined(); + }); + + it('returns childNodes if index >= 0', function() { + var result; + + $('body').html([ + '' + ].join('')); + + result = domUtils.getChildNodeByOffset($('ul')[0], -1); + + expect(result).toBeUndefined(); + }); }); describe('getPrevTopBlockNode', function() { @@ -175,6 +172,13 @@ describe('domUtils', function() { }); }); + describe('getTopBlockNode', function() { + it('return top block element of passed node', function() { + $('body').append('
aweftext1

text2

'); + expect(domUtils.getTopBlockNode($('em')[0].firstChild)).toBe($('div')[0]); + }); + }); + describe('getParentUntil', function() { it('return parent node of passed node until passed parent', function() { $('body').append('

aweftext1

'); diff --git a/apps/core/test/wwHrManager.spec.js b/apps/core/test/wwHrManager.spec.js index a5a41e726e..b7f8855f69 100644 --- a/apps/core/test/wwHrManager.spec.js +++ b/apps/core/test/wwHrManager.spec.js @@ -30,9 +30,22 @@ describe('WwHrManager', function() { }); }); + describe('_removeHrOnEnter', function() { + //현재커서가 hr을 가르키는 경우 + it('remove hr current selection is hr', function() { + var range = wwe.getEditor().getSelection().cloneRange(); + + wwe.setValue('
abcd
'); - describe('_removeHrIfNeed()', function() { - //같은 부모의 이전 offset의 엘리먼트가 hr일때 + range.setStart(wwe.getEditor().getDocument().body, 0); + range.collapse(true); + mgr._removeHrOnEnter(range, {preventDefault: function() {}}); + + expect(wwe.get$Body().find('hr').length).toEqual(0); + }); + + //크롬에서 커서 이동시 밑에서 위로 이동했을때 hr위에서 정상적으로 range가 잡히지 않는다 + //그런 상황에서 적용되는 케이스 it('remove hr if current is on first offset and previousSibling elemet is hr', function() { var range = wwe.getEditor().getSelection().cloneRange(); @@ -40,20 +53,59 @@ describe('WwHrManager', function() { range.setStart(wwe.getEditor().getDocument().body, 1); range.collapse(true); - mgr._removeHrIfNeed(range, {preventDefault: function() {}}); + mgr._removeHrOnEnter(range, {preventDefault: function() {}}); + + expect(wwe.get$Body().find('hr').length).toEqual(0); + }); + + //hr이후의 엘리먼트가 없을때 + it('remove hr then set cursor to new block when nextSibling is not exists', function() { + var range = wwe.getEditor().getSelection().cloneRange(), + newRange; + + wwe.setValue('
abcd<

'); + + range.setStart(wwe.get$Body()[0], 1); + range.collapse(true); + mgr._removeHrOnEnter(range, {preventDefault: function() {}}); + + newRange = wwe.getEditor().getSelection(); + + expect(wwe.get$Body().find('hr').length).toEqual(0); + expect(newRange.startContainer.tagName).toEqual('DIV'); + expect(newRange.startOffset).toEqual(0); + }); + + it('remove hr then set cursor to new block if next sibling is exist', function() { + var range = wwe.getEditor().getSelection().cloneRange(), + newRange; + + wwe.setValue('
abcd<
'); + + range.setStart(wwe.get$Body()[0], 0); + range.collapse(true); + mgr._removeHrOnEnter(range, {preventDefault: function() {}}); + + newRange = wwe.getEditor().getSelection(); expect(wwe.get$Body().find('hr').length).toEqual(0); + expect(newRange.startContainer.tagName).toEqual('DIV'); + expect(newRange.startOffset).toEqual(0); }); + }); + + describe('_removeHrOnBackspace()', function() { //현재커서가 hr을 가르키는 경우 it('remove hr current selection is hr', function() { var range = wwe.getEditor().getSelection().cloneRange(); wwe.setValue('
abcd
'); - range.setStart(wwe.getEditor().getDocument().body, 0); + range.selectNode(wwe.get$Body().find('hr')[0]); range.collapse(true); - mgr._removeHrIfNeed(range, {preventDefault: function() {}}); + + mgr._removeHrOnBackspace(range, {preventDefault: function() {}}); expect(wwe.get$Body().find('hr').length).toEqual(0); }); @@ -66,20 +118,21 @@ describe('WwHrManager', function() { range.setStart(wwe.get$Body().find('b')[0], 0); range.collapse(true); - mgr._removeHrIfNeed(range, {preventDefault: function() {}}); + mgr._removeHrOnBackspace(range, {preventDefault: function() {}}); expect(wwe.get$Body().find('hr').length).toEqual(0); }); - //현재 같은 부모에서는 이전 엘리먼트가 더이상 없고 부모래밸의 이전 앨리먼트가 hr일경우 - it('remove hr then set cursor to nextSibling if next sibling is exist', function() { + //크롬에서 커서 이동시 밑에서 위로 이동했을때 hr위에서 정상적으로 range가 잡히지 않는다 + //그런 상황에서 적용되는 케이스 + it('remove hr if current is on first offset and previousSibling elemet is hr', function() { var range = wwe.getEditor().getSelection().cloneRange(); - wwe.setValue('
abcd<
'); + wwe.setValue('
abcd
'); - range.setStart(wwe.get$Body().find('b')[0], 0); + range.setStart(wwe.getEditor().getDocument().body, 1); range.collapse(true); - mgr._removeHrIfNeed(range, {preventDefault: function() {}}); + mgr._removeHrOnBackspace(range, {preventDefault: function() {}}); expect(wwe.get$Body().find('hr').length).toEqual(0); }); diff --git a/apps/core/test/wysiwygCommands/hr.spec.js b/apps/core/test/wysiwygCommands/hr.spec.js new file mode 100644 index 0000000000..7bb16fcf56 --- /dev/null +++ b/apps/core/test/wysiwygCommands/hr.spec.js @@ -0,0 +1,67 @@ +'use strict'; + +var HR = require('../../src/js/wysiwygCommands/hr'), + WysiwygEditor = require('../../src/js/wysiwygEditor'), + WwTaskManager = require('../../src/js/wwTaskManager'), + EventManager = require('../../src/js/eventManager'); + +describe('HR', function() { + var wwe, sq; + + beforeEach(function(done) { + var $container = $('
'); + + $('body').append($container); + + wwe = new WysiwygEditor($container, null, new EventManager()); + + wwe.init(function() { + sq = wwe.getEditor(); + wwe.addManager('task', WwTaskManager); + done(); + }); + }); + + afterEach(function() { + $('body').empty(); + }); + + it('add HR and if there is no next block then append default block', function() { + HR.exec(wwe); + + expect(wwe.get$Body().find('hr').length).toEqual(1); + expect(wwe.get$Body().find('div').length).toEqual(2); + }); + + it('add HR and if there is next block then dont make default block', function() { + var range = sq.getSelection().cloneRange(); + + sq.setHTML('
test

'); + + range.setStart(wwe.get$Body().find('div')[0], 0); + range.collapse(true); + + sq.setSelection(range); + + HR.exec(wwe); + + expect(wwe.get$Body().find('hr').length).toEqual(1); + expect(wwe.get$Body().find('div').length).toEqual(2); + }); + + it('append hr then cursor to next block', function() { + var range = sq.getSelection().cloneRange(); + + sq.setHTML('
test

'); + + range.setStart(wwe.get$Body().find('div')[0], 0); + range.collapse(true); + + sq.setSelection(range); + + HR.exec(wwe); + + expect(wwe.get$Body().find('div').length).toEqual(2); + expect(sq.getSelection().startContainer).toBe(wwe.get$Body().find('div')[1]); + }); +});