/**
* @fileOverview
* Wysiwyg 영역의 컨텐츠를 조작하기 위해 사용되는 공통되는 Processor 정의
*/
Trex.I.Processor = {};
Trex.I.Processor.Standard = /** @lends Trex.Canvas.Processor.prototype */{
txSelection: _NULL,
isRangeInsideWysiwyg: _FALSE,
lastRange: _NULL,
initialize: function(win, doc) {
this.win = win;
this.doc = doc;
this.txSelection = new Trex.Canvas.Selection(this);
this.bookmark = new Trex.Canvas.Bookmark(this);
},
/**
* Trex.Canvas.Selection 객체를 리턴한다.
* @returns {Object} - Trex.Canvas.Selection 객체
* @example
* processor.getTxSel();
*/
getTxSel: function() {
return this.txSelection;
},
/**
* native selection object를 리턴한다.
* @returns {Object} - native selection 객체
* @example
* processor.getSel();
*/
getSel: function(){
return this.txSelection.getSel();
},
/**
* native range object를 리턴한다.
* @returns {Object} - native range 객체
* @example
* processor.getRange();
*/
getRange: function() {
return this.txSelection.getRange();
},
/**
* Trex.Canvas.Bookmark 객체를 리턴한다.
* @returns {Object} - Trex.Canvas.Bookmark 객체
* @example
* processor.getBookmark();
*/
getBookmark: function() {
return this.bookmark;
},
/**
* 선택된 영역의 collapse 여부(선택된 영역이 있는지 여부)를 리턴한다.
* @returns {Boolean} - collapse 여부
* @see Trex.Canvas.Selection#isCollapsed
* @example
* processor.isCollapsed();
*/
isCollapsed: function() {
return this.txSelection.isCollapsed();
},
/**
* 선택된 영역의 노드를 리턴한다.
* @returns {Element} - 선택된 영역의 노드
* @see Trex.Canvas.Selection#getNode
* @example
* processor.getNode();
*/
getNode: function() {
return this.txSelection.getNode();
},
/**
* 선택된 영역의 컨트롤 노드(img,object,hr,table,button)를 리턴한다.
* @returns {Element} - 선택된 영역의 노드
* @see Trex.Canvas.Selection#getControl
* @example
* processor.getControl();
*/
getControl: function(){
return this.txSelection.getControl();
},
/**
* 선택된 영역이 컨트롤 노드인지 여부를 리턴한다.
* @returns {Boolean} - 컨트롤 노드인지 여부
* @see Trex.Canvas.Selection#hasControl
* @example
* processor.hasControl();
*/
hasControl: function(){
return this.txSelection.hasControl();
},
/**
* 컨트롤 노드를 선택한다.
* @param {Element} node - 컨트롤 노트
* @example
* txSelection.selectControl(node);
*/
selectControl: function(node){
return this.txSelection.selectControl(node);
},
/**
* 선택된 영역의 텍스트 데이터를 리턴한다.
* @returns {String} - 선택된 영역의 텍스트 데이터
* @see Trex.Canvas.Selection#getText
* @example
* processor.getText();
*/
getText: function(){
return this.txSelection.getText();
},
/**
* 선택된 영역이 텍스트 데이터 영역의 어떤 위치인지를 리턴한다.
* @returns {Number} - 텍스트 데이터 영역의 어떤 위치인지
* 텍스트의 처음 : $tom.__POSITION.__START_OF_TEXT : -1
* 텍스트의 중간 : $tom.__POSITION.__MIDDLE_OF_TEXT : 0
* 텍스트의 마지막 : $tom.__POSITION.__END_OF_TEXT : 1
* @see Trex.Canvas.Selection#compareTextPos
* @example
* processor.compareTextPos();
*/
compareTextPos: function() {
return this.txSelection.compareTextPos();
},
/**
* 현재 선택된 영역에서 조건에 맞는 노드를 리턴한다.
* @param {Function, String} filter - 조건을 나타내는 함수 또는 문자열
* @returns {Element} - 조건에 맞는 노드
* @example
* processor.findNode(function() { return 'p,div'; });
* processor.findNode('%paragraph');
*/
findNode: function(filter) {
try {
return $tom.find(this.getNode(), filter);
} catch(e) {
return _NULL;
}
},
/*-------- processor - query style start --------*/
/**
* 특정한 노드의 특정한 스타일 값을 얻어온다.
* @param {Element} node - 특정 노드
* @param {String} styleName - 스타일 명
* @returns {String} - 스타일 값
* @example
* processor.queryStyle(node, 'textAlign');
*/
queryStyle: function(node, styleName) {
if(!node) {
return _NULL;
}
styleName = ((styleName == 'float')? ((node.style.styleFloat === _UNDEFINED)? 'cssFloat': 'styleFloat'): styleName);
if(node.style && node.style[styleName]) {
return node.style[styleName];
} else if(node.currentStyle && node.currentStyle[styleName]) {
return node.currentStyle[styleName];
} else if(_WIN.getComputedStyle) {
var targetNode = node;
while($tom.isText(targetNode)){
targetNode = targetNode.parentNode;
}
var _cssStyle = this.doc.defaultView.getComputedStyle(targetNode, _NULL);
return ((_cssStyle)? _cssStyle[styleName] : _NULL);
}
return _NULL;
},
/**
* 특정한 노드의 특정한 속성 값을 얻어온다.
* @param {Element} node - 특정 노드
* @param {String} attrName - 속성 명
* @returns {String} - 속성 값
* @example
* processor.queryAttr(node, 'align');
*/
queryAttr: function(node, attrName) {
if(!node) {
return _NULL;
}
return $tom.getAttribute(node, attrName);
},
/**
* 선택된 영역의 native queryCommandState 값을 얻어온다.
* @param {String} command - 커맨드 명
* @returns {Boolean} - 해당 영역이 커맨드 상태인지 여부
* @example
* processor.queryCommandState('bold');
*/
queryCommandState: function(command) {
try {
return this.doc.queryCommandState(command);
} catch(e) { return _FALSE; }
},
/**
* 선택된 영역의 native queryCommandValue 값을 얻어온다.
*/
queryCommandValue: function(command) {
try {
return this.doc.queryCommandValue(command);
} catch(e) {
return "";
}
},
/*-------- processor - query style end --------*/
/**
* 선택된 영역에 native execCommand를 실행시킨다.
* @param {String} command - 커맨드 명
* @param {String} data - 데이터 값
* @example
* processor.execCommand('forecolor', '#333');
*/
execCommand: function(command, data) {
if ($tx.gecko) {
// Firefox는 styleWithCSS 기본값이 true
try {
this.doc.execCommand('styleWithCSS', _FALSE, _FALSE);
} catch(e) {}
}
try {
this.doc.execCommand(command, _FALSE, data);
} catch(e) {}
},
/*-------- processor - marker start --------*/
/**
* 선택된 영역에 주어진 handler를 실행시킨다.
* 주로 외부에서 processor를 이용해 DOM조작을 할 경우에 사용된다.
* @param {Funtion} handler - 해당 영역에 실행할 함수
* @example
* processor.execWithMarker(function(marker) {
* $tom.insertAt(node, marker.endMarker);
* });
*/
execWithMarker: function(handler) {
var _marker = new Trex.Canvas.Marker(this);
this.bookmarkTo();
try {
_marker.paste();
_marker.backup();
handler(_marker);
} catch(e) {
console.log(e, e.stack)
} finally {
_marker.remove();
}
},
/*-------- processor - marker end --------*/
/*--------------------- focus, movecaret ----------------------*/
/**
* wysiwyg 영역에 포커스를 준다.
* @example
* processor.focus();
*/
focus: function() {
this.doc.body.focus();
},
/**
* wysiwyg 영역에 포커스를 뺀다.
* @example
* processor.blur();
*/
blur: function() {
_WIN.focus(); //NOTE: by focused on parent window, editor will be blured
},
/**
* 본문의 처음으로 캐럿을 옮긴다.
* @example
* processor.focusOnTop();
*/
focusOnTop: function() {
this.focus();
this.selectFirstText(this.doc.body);
this.doc.body.scrollTop = 0; //NOTE: only html, not xhtml
},
selectFirstText: function(node) {
var firstNode = $tom.top(node);
var range = this.createGoogRangeFromNodes(firstNode, 0, firstNode, 0);
range.select();
},
/**
* 본문의 마지막으로 캐럿을 옮긴다.
* @example
* processor.focusOnBottom();
*/
focusOnBottom: function() {
this.focus();
this.moveCaretTo(this.doc.body, _FALSE);
this.doc.body.scrollTop = this.doc.body.scrollHeight; //NOTE: only html, not xhtml
},
/**
* 특정 노드로 캐럿을 옮긴다.
* @param {Element} node - 특정 노드
* @param {Boolean} toStart - 위치, 시작 = true
* @example
* processor.moveCaretTo(node, true);
*/
moveCaretTo: function(node, toStart) {
if(!node) {
return;
}
this.focus();
this.bookmarkInto(node, toStart);
this.bookmark.select(this.txSelection);
},
/**
* 특정 노드의 바깥으로 캐럿을 옮긴다.
* @param {String} scope - 특정 노드 패턴
* @example
* processor.moveCaretWith(scope);
*/
moveCaretWith: function(scope) {
if(!scope) { return; }
var _elOuter = this.findNode(scope);
if(_elOuter) {
this.focus();
this.bookmark.saveNextTo(_elOuter);
this.bookmark.select(this.txSelection);
}
},
/**
* 특정 노드를 감싸 선택한다.
* @param {Element} node - 특정 노드
* @example
* processor.selectAround(node);
*/
selectAround: function(node) {
if(!node) {
return;
}
this.focus();
this.bookmark.saveAroundNode(node);
this.bookmark.select(this.txSelection);
},
/**
* 특정 노드의 안으로 북마크를 수정한다.
* @param {Element} node - 특정 노드
* @example
* processor.bookmarkInto(node);
*/
bookmarkInto: function(node, toStart) {
if(!node) {
return;
}
toStart = (toStart == _NULL)? _TRUE: toStart;
if(toStart) {
this.bookmark.saveIntoFirst(node);
} else {
this.bookmark.saveIntoLast(node);
}
},
/**
* 특정 노드의 이전으로 북마크를 수정한다.
* @param {Element} node - 특정 노드
* @example
* processor.bookmarkToPrevious(node);
*/
bookmarkToPrevious: function(node) {
if(!node) {
return;
}
this.bookmark.savePreviousTo(node);
},
/**
* 특정 노드의 다음으로 북마크를 수정한다.
* @param {Element} node - 특정 노드
* @example
* processor.bookmarkToNext(node);
*/
bookmarkToNext: function(node) {
if(!node) {
return;
}
this.bookmark.saveNextTo(node);
},
/**
* execute하기 전 range를 북마크한다.
* @example
* processor.bookmark();
*/
bookmarkTo: function(rng) {
rng = rng || this.txSelection.getRange();
this.bookmark.save({
startContainer: rng.startContainer,
startOffset: rng.startOffset,
endContainer: rng.endContainer,
endOffset: rng.endOffset
});
},
/**
* marker에 따라 북마크를 수정한다.
* @example
* processor.bookmarkWithMarker(marker);
*/
bookmarkWithMarker: function(marker) {
this.bookmark.saveWithMarker(marker);
},
/**
* 저장한 range를 선택한다.
* @example
* processor.restore();
*/
restore: function() {
this.bookmark.select(this.txSelection);
},
/*------------ execute action ------------*/
/**
* 인자에 따라 노드를 생성한다.
* @param {String, Object, Element} argument - 가변 arguments
* {String} name : 1st String은 노드명
* {Object} attributes : 적용할 속성들
* {Element, String, Number} children : 자식 노드
* @example
* processor.create('div', { 'className': 'txc-textbox' });
*/
create: function() {
var name = arguments[0];
var _node = this.newNode(name);
for (var i = 1; i < arguments.length; i++) {
var arg = arguments[i];
if (arg.nodeType) {
$tom.append(_node, arg);
} else if (typeof(arg) == 'string' || typeof(arg) == 'number') {
_node.innerHTML += arg;
} else if (typeof(arg) == 'array') {
for (var j = 0; j < arg.length; j++) {
$tom.append(_node, arg[j]);
}
} else {
$tom.applyAttributes(_node, arg);
}
}
return _node;
},
/**
* 선택한 영역에 노드를 삽입한다.
* @param {Array,Element} nodes - 삽입하고자 하는 노드 배열 또는 노드
* @param {Boolean} newline - 현재 영역에서 한줄을 띄운 후 삽입할지 여부
* @param {Object} wrapStyle - wrapper 노드에 적용할 스타일,
* newline이 true 일 경우에만 의미를 갖는다.
* @example
* processor.pasteNode([node, node], true, { 'style': { 'textAlign': 'center' } });
*/
pasteNode: function(nodes, newline, wrapStyle) {
if(!nodes) {
return;
}
if(!nodes.length) {
nodes = [].concat(nodes);
}
this.txSelection.collapse(_FALSE);
if(newline) {
/* order
* (curNode) > wpNode > dvNode
*/
var _curNode, _wpNode, _dvNode;
var _processor = this;
this.execWithMarker(function(marker) {
_dvNode = $tom.divideParagraph(marker.endMarker);
var _detected = $tom.kindOf(_dvNode, 'p,li,dd,dt,h1,h2,h3,h4,h5,h6');
if(_detected) {
_curNode = $tom.previous(_dvNode);
_wpNode = $tom.clone(_dvNode);
} else {
_dvNode = _processor.newNode('p');
$tom.insertAt(_dvNode, marker.endMarker);
_wpNode = _processor.newNode('p');
}
$tom.insertAt(_wpNode, _dvNode);
nodes.each(function(node) {
$tom.append(_wpNode, node);
});
if(wrapStyle) {
$tom.applyAttributes(_wpNode, wrapStyle);
}
// #FTDUEDTR-1442
if (nodes.length == 1) {
var firstChildNode = nodes[0];
var disableBlockTag = $tom.kindOf(firstChildNode, 'table,hr,blockquote,pre,h1,h2,h3,h4,h5,h6,div');
var isParagraphTag = $tom.isTagName(_wpNode, 'p');
if (disableBlockTag && isParagraphTag) {
$tom.unwrap(_wpNode);
}
}
});
if(_curNode) {
if(!$tom.hasData(_curNode)) {
this.stuffNode(_curNode);
}
}
this.stuffNode(_dvNode);
this.bookmark.saveIntoFirst(_dvNode);
} else {
var self = this;
this.executeUsingCaret(function(range, savedCaret) {
var startCaret = savedCaret.getCaret(_FALSE),
endCaret = savedCaret.getCaret(_FALSE);
var targetNode = $tx.msie_nonstd ? startCaret : _NULL;
nodes.each(function(node) {
range.insertNode(node, targetNode);
});
if (endCaret && endCaret.nextSibling) {
self.moveCaretTo(endCaret.nextSibling, 0);
}
});
}
return nodes[0];
},
/**
* 선택한 영역에 HTML 컨텐츠를 삽입한다.
* @param {String} html - 삽입하고자 하는 HTML 컨텐츠
* @param {Boolean} newline - 현재 영역에서 한줄을 띄운 후 삽입할지 여부 true/false
* @param {Object} wrapStyle - wrapper 노드에 적용할 스타일,
* newline이 true 일 경우에만 의미를 갖는다.
* @example
* processor.pasteNode('
', true, { 'style': { 'textAlign': 'center' } });
*/
pasteContent: function(html, newline, wrapStyle) {
var _tmpNode = this.create('div');
_tmpNode.innerHTML = html;
var _dataNodes = $tom.children(_tmpNode);
return this.pasteNode(_dataNodes, newline, wrapStyle);
},
/**
* 주어진 노드를 새로운 노드로 교체한다.
* @param {Element} node - 기존 노드
* @param {String} tag - 새로운 노드 명
* @param {Object} attributes - 새로운 노드에 적용할 속성들
* @returns {Element} - 생성한 노드
* @example
* processor.replace(p, 'li');
*/
replace: function(node, tag, attributes) {
this.bookmark.saveAroundNode(node);
return $tom.replace(node, this.create(tag, attributes));
},
/**
* 선택한 영역안에 있는 노드 중에 패턴을 만족하는 블럭 노드들을 리턴한다.
* @param {Array} filter - 수집할 노드 패턴 조건
* @returns {Array} - 선택한 영역안에 있는 노드 중에 패턴을 만족하는 노드들
* @example
* processor.blocks(function() {
return '%paragraph';
});
*/
blocks: function(filter) {
var _nodes = [];
var _patterns = filter();
if (this.hasControl()) {
var _control = this.getControl();
if ($tom.kindOf(_control.parentNode, _patterns)) {
_nodes.push(_control.parentNode);
}
} else {
var _processor = this;
this.execWithMarker(function(marker) {
var _itr = _processor.getBlockRangeIterator(_patterns, marker.startMarker, marker.endMarker);
var _node;
while (_itr.hasNext()) {
_node = _itr.next();
if ($tom.kindOf(_node, '#tx_start_marker,#tx_end_marker')) {
//ignore
} else {
_nodes.push(_node);
}
}
});
}
return _nodes;
},
/**
* 선택한 영역안에 있는 노드 중에 패턴을 만족하는 인라인 노드들을 리턴한다.
* @param {Array} filter - 수집할 노드 패턴 조건
* @returns {Array} - 선택한 영역안에 있는 노드 중에 패턴을 만족하는 노드들
* @example
* processor.inlines(function(type) {
if(type === 'control') {
return 'hr,table';
}
return '%inline';
});
*/
inlines: function(filter) {
var _nodes = [];
var _patterns = filter();
var _processor = this;
var _createInline = function() {
return _processor.create($tom.inlineOf());
};
if (this.hasControl()) {
var _control = this.getControl();
if ($tom.kindOf(_control, _patterns)) {
_nodes.push(_control);
} else {
var _iNode = _createInline();
$tom.insertNext(_iNode, _control);
$tom.append(_iNode, _control);
}
} else {
this.execWithMarker(function(marker) {
if (marker.checkCollapsed()) { //collapsed
var _iNode = _createInline();
$tom.append(_iNode, _processor.newDummy());
$tom.insertNext(_iNode, marker.startMarker);
_processor.bookmarkTo({
startContainer: _iNode,
startOffset: 1,
endContainer: _iNode,
endOffset: 1
});
_nodes.push(_iNode);
} else {
var _itr = _processor.getInlineRangeIterator(_patterns, marker.startMarker, marker.endMarker);
var _node;
while (_itr.hasNext()) {
_node = _itr.next();
if ($tom.kindOf(_node, '#tx_start_marker,#tx_end_marker')) {
//ignore
} else if ($tom.kindOf(_node, 'br')) {
//ignore
} else {
_nodes.push(_node);
}
}
}
});
}
return _nodes;
},
/**
* 선택한 영역안에 있는 노드 중에 패턴을 만족하는 컨트롤 노드(img,object,hr,table,button)들을 리턴한다.
* @param {Array} filter - 수집할 노드 패턴 조건
* @returns {Array} - 선택한 영역안에 있는 노드 중에 패턴을 만족하는 노드들
* @example
* processor.controls(function() {
return 'hr,table';
});
*/
controls: function(filter) {
var _nodes = [];
if (this.hasControl()) {
if ($tom.kindOf(this.getControl(), filter())) {
_nodes.push(this.getControl());
}
}
return _nodes;
},
/**
* 더미용 nbsp를 넣는 함수.
* webkit 용에 구현되어있습니다.
* Safari 에서 apply 시
* 폰트에 대한 속성을 잃어버리기 때문에 필요.
*/
addDummyNbsp: function () {},
/**
* 배열 내의 모든 노드에게 지정한 속성을 적용한다.
* @param {Array} nodes - 속성을 적용할 노드 배열
* @param {Object} attributes - 노드에 적용할 속성들
* @returns {Array} - 입력 노드들
* @example
* processor.apply([p,p,p], { style: { textAlign: 'center'}});
*/
apply: function(nodes, attributes) {
if(!nodes) {
return _NULL;
}
if(!nodes.length) {
nodes = [].concat(nodes);
}
nodes.each(function(node) {
$tom.applyAttributes(node, attributes);
});
return nodes;
},
/**
* 배열 내의 모든 노드를 주어진 블럭으로 감싼다.
* @param {Array} nodes - 블럭으로 감쌀 노드 배열
* @param {String} tag - 블럭 노드 명
* @param {Object} attributes - 블럭에 적용할 속성
* @returns {Element} - 생성한 블럭노드
* @example
* processor.wrap([p,p,p], 'div', { style: { backgroundColor: 'black'}});
*/
wrap: function(nodes, tag, attributes) {
if(!nodes) {
return _NULL;
}
if(!nodes.length) {
nodes = [].concat(nodes);
}
attributes = attributes || {};
var res = $tom.wrap(this.create(tag, attributes), nodes);
if($tx.msie && !$tom.nextContent(res)){
var e = this.doc.createElement('p');
e.innerHTML = $tom.EMPTY_BOGUS;
this.doc.body.appendChild(e);
}
return res;
},
/**
* 블럭으로 감싸진 노드들을 빼내고 블럭을 삭제한다.
* @param {Element} node - 블럭 노드
* @returns {Element} - 블럭의 첫번째 노드 또는 블럭의 다음 노드
* @example
* processor.unwrap(node);
*/
unwrap: function(node) {
if (!node) {
return _NULL;
}
this.bookmark.saveAroundNode(node);
return $tom.unwrap(node);
},
createGoogRange: function() {
return goog.dom.Range.createFromWindow(this.win)
},
createGoogRangeFromNodes: function(startNode, startOffset, endNode, endOffset) {
return goog.dom.Range.createFromNodes(startNode, startOffset, endNode, endOffset);
},
executeUsingCaret: function(handler) {
try {
var range = this.createGoogRange();
var savedCaret = range.saveUsingCarets();
return handler(range, savedCaret);
} finally {
if (!savedCaret.isDisposed()) {
savedCaret.restore();
}
}
}
};
Trex.module("observe that @when control elements are focused at",
function(editor, toolbar, sidebar, canvas) {
if($tx.webkit || $tx.presto) {
canvas.observeJob(Trex.Ev.__CANVAS_PANEL_MOUSEDOWN, function(ev) {
var _processor = canvas.getProcessor();
var _node = $tx.element(ev);
if ($tom.kindOf(_node, "img,hr,iframe,table")) {
var _button = $tom.find(_node, 'button');
if(_button) {
_processor.selectControl(_button);
} else {
_processor.selectControl(_node);
}
} else if ($tom.kindOf(_node, "button")) {
_processor.selectControl(_node);
}
});
}
}
);
Trex.module("bind iframe activate or deactivate event",
function(editor, toolbar, sidebar, canvas) {
// if ($tx.msie_nonstd) {
canvas.observeJob(Trex.Ev.__IFRAME_LOAD_COMPLETE, function(panelDoc) {
var _processor = canvas.getProcessor(Trex.Canvas.__WYSIWYG_MODE);
$tx.observe(panelDoc, 'beforedeactivate', function(ev) {
_processor.isRangeInsideWysiwyg = true;
_processor.lastRange = _processor.getRange();
});
$tx.observe(panelDoc, 'deactivate', function (ev) {
if (_processor.hasControl()) {
return;
}
_processor.isRangeInsideWysiwyg = false;
});
$tx.observe(panelDoc, 'activate', function() {
_processor.isRangeInsideWysiwyg = true;
_processor.lastRange = _NULL;
});
});
// }
}
);