mirror of
https://github.com/Lastorder-DC/rhymix.git
synced 2026-01-18 10:49:54 +09:00
git-svn-id: http://xe-core.googlecode.com/svn/sandbox@7803 201d5d3c-b55e-5fd7-737f-ddc643e51545
4696 lines
118 KiB
JavaScript
4696 lines
118 KiB
JavaScript
/*
|
|
* XEED - XpressEngine WYSIWYG Editor
|
|
* @author nhn (developers@xpressengine.com)
|
|
*/
|
|
(function($){
|
|
|
|
var d = document, fn, dp, dc,
|
|
rx_command = /(?:^| )@(\w+)(?: |$)/,
|
|
rx_block = /^(H[1-6R]|P|DIV|ADDRESS|PRE|FORM|T(ABLE|BODY|HEAD|FOOT|H|R|D)|LI|OL|UL|CAPTION|BLOCKQUOTE|CENTER|DL|DT|DD|DIR|FIELDSET|NOSCRIPT|MENU|ISINDEX|SAMP)$/i,
|
|
invisibleCh = '\uFEFF',
|
|
_nt_ = 'nodeType',
|
|
_nn_ = 'nodeName',
|
|
_ps_ = 'previousSibling',
|
|
_ns_ = 'nextSibling',
|
|
_pn_ = 'parentNode',
|
|
_cn_ = 'childNodes',
|
|
_ca_ = 'commonAncestorContainer',
|
|
_sc_ = 'startContainer',
|
|
_so_ = 'startOffset',
|
|
_ec_ = 'endContainer',
|
|
_eo_ = 'endOffset',
|
|
_osc_ = 'oStartContainer',
|
|
_iso_ = 'iStartOffset',
|
|
_oec_ = 'oEndContainer',
|
|
_ieo_ = 'iEndOffset',
|
|
_xr_ = '_xeed_root',
|
|
rx_root = new RegExp('(?:^|\\s)'+_xr_+'(?:\\s|$)'),
|
|
Xeed, XHTMLT, Simple, Block, Font, Filter, EditMode, LineBreak, Resize, UndoRedo, SChar, Table, URL, AutoSave, FindReplace, Clear, DOMFix;
|
|
|
|
Xeed = xe.createApp('Xeed', {
|
|
$textarea : null,
|
|
$richedit : null,
|
|
$toolbar : null,
|
|
$root : null,
|
|
_options : null,
|
|
last_node : null,
|
|
|
|
/**
|
|
* @brief constructor
|
|
*/
|
|
init : function(obj, options) {
|
|
var self=this, $obj=$(obj), $text, $edit, opts, content, plugins, i, c, tmpId;
|
|
|
|
// Options
|
|
opts = $.extend({
|
|
minHeight : 400
|
|
}, options);
|
|
this._options = opts;
|
|
|
|
//
|
|
if ($obj.is('textarea')) {
|
|
$text = $obj;
|
|
content = $obj.val();
|
|
} else {
|
|
$text = $obj.before('<textarea>').hide().prev();
|
|
content = $obj.html();
|
|
}
|
|
|
|
// Convert to wysiwyg editor
|
|
this.$textarea = $text;
|
|
this.$root = $text.parent();
|
|
this.$richedit = this.$root.find('div.edit>div.xdcs:first');
|
|
this.$richedit
|
|
.attr('contentEditable', true)
|
|
.attr('id', tmpId = this.getOnetimeId())
|
|
.addClass(_xr_)
|
|
.after(this.$textarea)
|
|
.focus(function(event){ self.cast('ON_FOCUS', [event]) })
|
|
.mousedown(function(event){ self.cast('ON_MOUSEDOWN', [event]) })
|
|
.keydown(function(event){ self.cast('ON_KEYDOWN', [event]) });
|
|
this.$root.find('>div.xd').show();
|
|
this.$toolbar = this.$root.find('>div.xd>div.tool');
|
|
this.$toolbar.find('>a:first').attr('href', '#'+tmpId);
|
|
|
|
// legacy mode for firefox
|
|
try { d.execCommand('styleWithCSS', false, false); } catch(e){};
|
|
|
|
// hide all layer
|
|
$(document).mousedown(function(event) {
|
|
var $target = $(event.target);
|
|
|
|
if ($target.is('input,button')) return;
|
|
|
|
self.cast('HIDE_ALL_LAYER');
|
|
|
|
if ($target.is('div.edit')) {
|
|
self.cast('SET_FOCUS');
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// button hover event
|
|
this.$toolbar
|
|
.delegate('li.ti>button', 'mouseover', function(){ $(this[_pn_]).addClass('hover') })
|
|
.delegate('li.ti>button', 'mouseout', function(){ $(this[_pn_]).removeClass('hover') });
|
|
|
|
// register plugins
|
|
this.registerPlugin(new Hotkey); // Hotkey must be the first
|
|
this.registerPlugin(new EditMode);
|
|
this.registerPlugin(new Simple);
|
|
this.registerPlugin(new Block);
|
|
this.registerPlugin(new Font);
|
|
this.registerPlugin(new Filter);
|
|
this.registerPlugin(new LineBreak);
|
|
this.registerPlugin(new Resize);
|
|
this.registerPlugin(new UndoRedo);
|
|
this.registerPlugin(new SChar);
|
|
this.registerPlugin(new Table);
|
|
this.registerPlugin(new URL);
|
|
this.registerPlugin(new AutoSave);
|
|
this.registerPlugin(new FileUpload);
|
|
this.registerPlugin(new Clear);
|
|
|
|
// set content
|
|
setTimeout(function(){ self.cast('SET_CONTENT', [content]) }, 0);
|
|
},
|
|
|
|
/**
|
|
* @brief Get option
|
|
* @param key String option key
|
|
* @syntax opt = oApp.getOption('keyName')
|
|
* @return Variant
|
|
*/
|
|
getOption : function(key) {
|
|
var v = this._options[key];
|
|
if (is_def(v)) return v;
|
|
},
|
|
|
|
/**
|
|
* @brief Set default option
|
|
* @param key String option key
|
|
* @param val Default value
|
|
* @syntax oApp.setDefault('key', 'defaultValue');
|
|
* oApp.setDefault({'key1':'defaultValue1', 'key2':'defaultValue2', ...});
|
|
*/
|
|
setDefault : function(key, val) {
|
|
var self = this;
|
|
|
|
if ($.isPlainObject(key)) {
|
|
$.each(key, function(k,v){ self.setDefault(k,v) });
|
|
} else if (is_str(key)) {
|
|
if (!is_def(this._options[key])) this._options[key] = val;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @brief Get current selection
|
|
* @syntax oSelection = oApp.getSelection()
|
|
* @return HuskyRange
|
|
* @see getEmptySelection
|
|
*/
|
|
getSelection : function() {
|
|
var range = this.getEmptySelection(), xr = '.'+_xr_, $p;
|
|
|
|
// this may throw an exception if the selected is area is not yet shown
|
|
//try{ range.setFromSelection(); }catch(e){};
|
|
range.setFromSelection();
|
|
|
|
$p = $(range[_ca_]);
|
|
if ( $p.is(xr) || $p.parents(xr+':first').length ) return range;
|
|
},
|
|
|
|
/**
|
|
* @brief Get empty selection
|
|
* @syntax oSelection = oApp.getEmptySelection()
|
|
* @return HuskyRange
|
|
* @see getSelection
|
|
*/
|
|
getEmptySelection : function() {
|
|
return new HuskyRange();
|
|
},
|
|
|
|
checkCurrentNode : function(sel) {
|
|
var _sel = sel || this.getSelection(), sc;
|
|
|
|
if (!_sel) {
|
|
this.last_node = null;
|
|
return false;
|
|
}
|
|
|
|
sc = _sel[_sc_];
|
|
if (sc !== this.last_node) {
|
|
this.last_node = sc;
|
|
this.cast('ON_CHANGE_NODE', [sc]);
|
|
}
|
|
},
|
|
|
|
getOnetimeId : function() {
|
|
return 'xeed' + Math.ceil(Math.random() * 1000) + (''+(new Date).getTime()).substr(8);
|
|
},
|
|
|
|
/**
|
|
* @brief Get an HTML code of the content
|
|
* @param Reserved slot for html code
|
|
* @param Force getting content from rich editor
|
|
* @return html string
|
|
*/
|
|
API_GET_CONTENT : function(sender, params) {
|
|
return params[0];
|
|
},
|
|
|
|
API_BEFORE_GET_CONTENT : function(sender, params) {
|
|
var force = params[1];
|
|
|
|
if (force || this.$richedit.is(':visible')) {
|
|
params[0] = this.$richedit.html();
|
|
} else {
|
|
params[0] = this.$textarea.val();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @brief Set rich content from HTML code
|
|
* @param html The HTML code string
|
|
*
|
|
* Since WebKit has a bug related to content-editable attribute,
|
|
* toggle action doesn't work for some inline style such as bold, itailc and so on.
|
|
* As a workaround, I used manually appending DOM objects instead of setting html.
|
|
*/
|
|
API_SET_CONTENT : function(sender, params) {
|
|
var $rich, $cur;
|
|
|
|
$rich = this.$richedit.empty();
|
|
|
|
$('<div>'+params[0]+'</div>')
|
|
.contents()
|
|
.each(function(){
|
|
if (is_block(this)) {
|
|
$rich.append(this);
|
|
$cur = null;
|
|
|
|
return true;
|
|
}
|
|
|
|
if (this[_nt_] == 3 && /^\s*$/.test(this.nodeValue)) return true;
|
|
|
|
if (!$cur) {
|
|
$cur = $(this).wrap('<p />').parent().appendTo($rich);
|
|
} else {
|
|
$cur.append(this);
|
|
}
|
|
});
|
|
|
|
// If the rich editor is hidden, put the content into the textarea too.
|
|
if ($rich.is(':hidden')) this.$textarea.val( this.cast('GET_CONTENT', [0, true]) );
|
|
},
|
|
|
|
/**
|
|
* @brief Execute a command
|
|
* @param command String that specifies the command to execute.
|
|
* @param ui When this value is true, display a user interface if the command supports one. (Default : false)
|
|
* @param value Optional. Variant that specifies the string, number, or other value to assign.
|
|
*/
|
|
API_EXEC_COMMAND : function(sender, params) {
|
|
var command = params[0], ui = params[1], value = params[2], sel = this.getSelection();
|
|
|
|
if (sel) {
|
|
d.execCommand(command, ui, value);
|
|
this.cast('SAVE_UNDO_POINT');
|
|
this.checkCurrentNode(sel);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @brief Register a command
|
|
* @param selector A selector to find the component or a DOM object
|
|
* @param hotkey Hotkey string
|
|
* @param command Command string
|
|
* @param arg Array for arguments
|
|
*/
|
|
API_REGISTER_COMMAND : function(sender, params) {
|
|
var self = this, $btn, selector = params[0], hotkey = params[1], cmd = params[2], args = params[3], _sel;
|
|
|
|
function fn(){
|
|
if (_sel) {
|
|
try { _sel.select(); } catch(e){ };
|
|
}
|
|
|
|
self.cast('HIDE_ALL_LAYER');
|
|
self.cast(cmd, args);
|
|
return false;
|
|
}
|
|
|
|
// register hotkey
|
|
if (hotkey) this.cast('REGISTER_HOTKEY', [hotkey, fn]);
|
|
|
|
// ui event
|
|
if (selector) {
|
|
($btn = is_str(selector)?this.$toolbar.find(selector):$(selector)).click(fn);
|
|
|
|
if ($btn[0] && $.browser.msie) {
|
|
$btn.mousedown(function(){ _sel = self.getSelection(); return false; });
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @brief Unregister a command
|
|
* @param selector A selector to find the component or a DOM object
|
|
* @param hotkey Hotkey string
|
|
*/
|
|
API_UNREGISTER_COMMAND : function(sender, params) {
|
|
var selector = params[0], hotkey = params[1];
|
|
|
|
// unregister hotkey
|
|
if (hotkey) this.cast('UNREGISTER_HOTKEY', [hotkey]);
|
|
|
|
// unregister ui event
|
|
if (selector) {
|
|
($btn = is_str(selector)?this.$toolbar.find(selector):$(selector)).unbind('click');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @brief Set state of a command button
|
|
* @param selector A selector to find the component or a DOM object
|
|
* @param state State string such as 'disable', 'active' or 'normal'
|
|
*/
|
|
API_SET_COMMAND_STATE : function(sender, params) {
|
|
var $btn, $li, selector = params[0], state = params[1];
|
|
|
|
$btn = is_str(selector)?this.$toolbar.find(selector):$(selector);
|
|
|
|
if (!$btn[0]) return;
|
|
if ($.inArray(state, ['disable', 'active']) == -1) state = 'normal';
|
|
|
|
$li = $btn.parent('li').removeClass('disable active');
|
|
|
|
if (state != 'normal') $li.addClass(state);
|
|
},
|
|
|
|
/**
|
|
* @brief Set focus
|
|
*/
|
|
API_SET_FOCUS : function(sender, params) {
|
|
var self = this, sel = this.getSelection();
|
|
|
|
if (sel) return;
|
|
|
|
this.$richedit.focus();
|
|
},
|
|
|
|
/**
|
|
* @brief Fire event on keydown
|
|
* @param event object
|
|
*/
|
|
API_ON_KEYDOWN : function(sender, params) {
|
|
var timer = arguments.callee.timer;
|
|
|
|
if (timer) clearTimeout(timer);
|
|
arguments.callee.timer = setTimeout(bind(this, this.checkCurrentNode), 100);
|
|
},
|
|
|
|
/**
|
|
* @brief Fire event on mousedown
|
|
* @param event object
|
|
*/
|
|
API_ON_MOUSEDOWN : function(sender, params) {
|
|
setTimeout(bind(this, this.checkCurrentNode), 0);
|
|
},
|
|
|
|
/**
|
|
* @brief Fire event on focus
|
|
* @param event object
|
|
*/
|
|
API_ON_FOCUS : function(sender, params) {
|
|
setTimeout(bind(this, this.checkCurrentNode), 0);
|
|
},
|
|
|
|
/**
|
|
* @brief Hide all layer
|
|
* @param Layer to skip hiding
|
|
*/
|
|
// API_HIDE_ALL_LAYER : function(sender, params) { },
|
|
|
|
/**
|
|
* @brief Fire event on change current node
|
|
* @oaram Current selection
|
|
*/
|
|
// API_ON_CHANGE_NODE : function(sender, params) { },
|
|
|
|
end : 0 // just mark end point of the definition
|
|
});
|
|
|
|
/**
|
|
* {{{ Simple Command plugin
|
|
* This plugin handle simple commands such as bold, italic and underline
|
|
*/
|
|
Simple = xe.createPlugin('SimpleCommand', {
|
|
$btns : {},
|
|
cmd : {
|
|
bold : ['bd', 'ctrl+b'],
|
|
underline : ['ue', 'ctrl+u'],
|
|
italic : ['ic', 'ctrl+i'],
|
|
strikethrough : ['se'],
|
|
superscript : ['sp'],
|
|
subscript : ['sb'],
|
|
justifyleft : ['al'],
|
|
justifycenter : ['ac'],
|
|
justifyright : ['ar'],
|
|
justifyfull : ['aj'],
|
|
insertorderedlist : ['ol'],
|
|
insertunorderedlist : ['ul']
|
|
},
|
|
|
|
// constructor
|
|
init : function() {
|
|
this.$btns = {};
|
|
},
|
|
|
|
// on activate
|
|
activate : function() {
|
|
var self = this, app, $tb, classes = [];
|
|
|
|
// skip this code if there is no toolbar
|
|
if (!(app=this.oApp) || !($tb=app.$toolbar) || !$tb.length) return;
|
|
|
|
// register commands
|
|
$.each(this.cmd, function(cmd) {
|
|
var $btn = $tb.find('button.'+this[0]);
|
|
|
|
if ($btn[0]) {
|
|
self.$btns[cmd] = $btn;
|
|
self.cast('REGISTER_COMMAND', [$btn[0], this[1], 'EXEC_COMMAND', [cmd, false, false]]);
|
|
}
|
|
});
|
|
},
|
|
|
|
// on deactivate
|
|
deactivate : function() {
|
|
var self = this;
|
|
|
|
$.each(this.cmd, function(cmd) {
|
|
if (!self.$btns[cmd]) return;
|
|
self.cast('UNREGISTER_COMMAND', [self.$btns[this[0]], this[1]]);
|
|
});
|
|
|
|
// empty button list
|
|
self.$btns = {};
|
|
},
|
|
|
|
API_ON_CHANGE_NODE : function(sender, params) {
|
|
var self = this, node = params[0];
|
|
|
|
if (!node) {
|
|
$.each(this.$btns, function(cmd, $btn){
|
|
self.cast('SET_COMMAND_STATE', [$btn[0], 'disable']);
|
|
});
|
|
return;
|
|
}
|
|
|
|
$.each(this.$btns, function(cmd, $btn){
|
|
var state = 'disable';
|
|
|
|
if (node && d.queryCommandEnabled(cmd)) {
|
|
state = d.queryCommandState(cmd)?'active':'normal';
|
|
}
|
|
|
|
self.cast('SET_COMMAND_STATE', [$btn[0], state]);
|
|
});
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ Block Command Plugin
|
|
* @brief This plugin handles some block commands such as Heading, Quote, Indent, Outdent, Box and LineHeight.
|
|
*/
|
|
Block = xe.createPlugin('BlockCommand', {
|
|
$head_btn : null,
|
|
$line_btn : null,
|
|
$head_layer : null,
|
|
$line_layer : null,
|
|
$btns : {},
|
|
cmd : {indent:'id',outdent:'od',quote:'qm',box:'bx'},
|
|
init : function() {
|
|
this.$btns = {};
|
|
},
|
|
activate : function() {
|
|
var self = this, app = this.oApp, $tb = app.$toolbar, np = navigator.platform;
|
|
|
|
if (!$tb) return;
|
|
|
|
// heading
|
|
this.$head_btn = $tb.find('li.hx:first>button').mousedown(function(){ self.cast('TOGGLE_HEADING_LAYER'); return false; })
|
|
this.$head_layer = this.$head_btn.next('ul.lr')
|
|
.find('>li>button')
|
|
.hover(
|
|
function(){ $(this[_pn_]).addClass('hover') },
|
|
function(){ $(this[_pn_]).removeClass('hover') }
|
|
)
|
|
.each(function(){
|
|
var $this = $(this);
|
|
if (!/(?:^|\s)h([1-7])(?:\s|$)/i.test($this.parent().attr('class'))) return true;
|
|
if (np && !np.indexOf('Mac')) $this.attr('title', 'CTRL+COMMAND+'+RegExp.$1);
|
|
|
|
self.cast('REGISTER_COMMAND', [this, 'ctrl+'+RegExp.$1, 'EXEC_HEADING', [RegExp.$1]]);
|
|
})
|
|
.end();
|
|
|
|
// line-height
|
|
this.$line_btn = $tb.find('li.lh:first>button').mousedown(function(){ self.cast('TOGGLE_LINEHEIGHT_LAYER'); return false; });
|
|
this.$line_layer = this.$line_btn.next('ul.lr')
|
|
.find('>li>button')
|
|
.hover(
|
|
function(){ $(this[_pn_]).addClass('hover') },
|
|
function(){ $(this[_pn_]).removeClass('hover') }
|
|
)
|
|
.click(function(){
|
|
self.cast('EXEC_LINEHEIGHT', [$(this).text()]);
|
|
self.cast('HIDE_LINEHEIGHT_LAYER');
|
|
return false;
|
|
})
|
|
.end();
|
|
|
|
// quote, box, indent, outdent
|
|
$.each(this.cmd, function(key,val){
|
|
var $btn = $tb.find('button.'+val);
|
|
|
|
if (!$btn[0]) return;
|
|
|
|
self.$btns[val] = $btn;
|
|
self.cast('REGISTER_COMMAND', [$btn[0], '', 'EXEC_'+key.toUpperCase()]);
|
|
});
|
|
},
|
|
deactivate : function() {
|
|
var self = this, $tb = this.oApp.$toolbar, $layer = this.$head_layer, sels = [];
|
|
|
|
if (!$tb) return;
|
|
|
|
// headings
|
|
$tb.find('li.hx:first>button:first').unbind('mousedown');
|
|
if ($layer && $layer.length) {
|
|
$layer.find('>li').unbind();
|
|
this.$head_layer = null;
|
|
}
|
|
|
|
// quote, box, indent, outdent
|
|
$.each(this.$btns, function(key){
|
|
self.cast('UNREGISTER_COMMAND', [this[0]]);
|
|
});
|
|
},
|
|
getBlockParents : function() {
|
|
var sel = this.oApp.getSelection(), nodes, ret = [], $ret, _xs_ = '_xeed_tmp_selection';
|
|
|
|
if (!sel) return ret;
|
|
|
|
nodes = sel.collapsed?[sel.getStartNode()]:sel.getNodes();
|
|
|
|
$(nodes).each(function(i,node){
|
|
var name;
|
|
|
|
while(node[_pn_]) {
|
|
name = node[_nn_].toLowerCase();
|
|
|
|
if (/^t(able|body|foot|head|r)$/i.test(name)) {
|
|
node = node[_pn_];
|
|
continue;
|
|
}
|
|
|
|
if (is_block(node)) {
|
|
if (node[_nt_] == 1) node.className += ' '+_xs_;
|
|
ret.push(node);
|
|
break;
|
|
}
|
|
|
|
if (rx_root.test(node[_pn_].className || '')) {
|
|
if (name != 'br') {
|
|
if (node[_nt_] == 3) node = $(node).wrap('<p>')[0][_pn_];
|
|
if (node[_nt_] == 1) node.className += ' '+_xs_;
|
|
|
|
ret.push(node);
|
|
}
|
|
break;
|
|
}
|
|
|
|
node = node[_pn_];
|
|
}
|
|
});
|
|
|
|
$ret = $(ret);
|
|
|
|
ret = $ret.filter(function(){
|
|
var $this = $(this);
|
|
return !$this.is('.'+_xr_) && !$this.parentsUntil('.'+_xr_).filter('.'+_xs_).length;
|
|
});
|
|
|
|
$ret.removeClass(_xs_).each(function(){ if (!this.className) $(this).removeAttr('class') });
|
|
|
|
return $.makeArray($.unique(ret));
|
|
},
|
|
match : function(node, selector) {
|
|
var $node = $(node);
|
|
|
|
if ($node.is(selector)) {
|
|
return $node[0];
|
|
} else {
|
|
$node = $node.parentsUntil('.'+_xr_).filter(selector);
|
|
if ($node.length) return $node[0];
|
|
}
|
|
},
|
|
unwrap : function(node) {
|
|
var $node = $(node), $wrapper;
|
|
|
|
$node.contents().each(function(){
|
|
if(rx_block.test(this[_nn_])) {
|
|
$wrapper = 0;
|
|
} else {
|
|
if (!$wrapper) $wrapper = $(this).wrap('<p>').parent();
|
|
else $wrapper.append(this);
|
|
}
|
|
});
|
|
$node.children().unwrap();
|
|
},
|
|
getBlockParent : function(node) {
|
|
var name;
|
|
|
|
while(!rx_block.test(node[_nn_])) {
|
|
if (rx_root.test(node[_pn_].className)) break;
|
|
node = node[_pn_];
|
|
}
|
|
|
|
if (!rx_block.test(node[_nn_])) {
|
|
node = $(node).wrap('<p />').parent().get(0);
|
|
}
|
|
|
|
return node;
|
|
},
|
|
getChild : function(node, container) {
|
|
if (node == container) return node;
|
|
|
|
while(node) {
|
|
if (node[_pn_] == container) return node;
|
|
node = node[_pn_];
|
|
}
|
|
},
|
|
/**
|
|
* @brief Get a valid parent (evaluate with XHTML)
|
|
* @param par Element initial parent node
|
|
* @param childName String child node name
|
|
*/
|
|
getValidParent : function(par, childName) {
|
|
while(!XHTMLT[par[_pn_][_nn_].toLowerCase()][childName]) {
|
|
par = par[_pn_];
|
|
}
|
|
return par;
|
|
},
|
|
|
|
fireChangeNode : function(sel) {
|
|
var self = this, _sel = sel || this.oApp.getSelection();
|
|
|
|
setTimeout(function(){ self.cast('ON_CHANGE_NODE', [_sel[_sc_]]) }, 0);
|
|
},
|
|
/**
|
|
* @brief Quotes selection
|
|
*/
|
|
API_EXEC_QUOTE : function(sender, params) {
|
|
var self = this, sel = this.oApp.getSelection(), start, end, ancestor, match, $bq, _bq_ = 'blockquote.bq';
|
|
|
|
if (!sel) return false;
|
|
|
|
start = sel.getStartNode();
|
|
match = this.match(start, _bq_);
|
|
|
|
if (match) {
|
|
this.unwrap(match);
|
|
sel.select();
|
|
this.fireChangeNode(sel);
|
|
return;
|
|
}
|
|
|
|
// get block-level common ancestor
|
|
ancestor = sel.commonAncestorContainer;
|
|
start = this.getValidParent(this.getBlockParent(this.getChild(start, ancestor)), 'blockquote');
|
|
end = sel.collapsed?start:this.getChild(sel.getEndNode(), start[_pn_]);
|
|
|
|
// remove quote blocks in a selection
|
|
$(sel.getNodes()).filter(_bq_).each(function(){ self.unwrap(this) });
|
|
|
|
// wrap nodes with new <blockquote>
|
|
if (start == end && /^(div|p)$/i.test(start[_nn_])) {
|
|
$bq = $(start).wrapInner('<blockquote class="bq" />').children().unwrap();
|
|
} else {
|
|
$bq = $(start).wrap('<blockquote class="bq" />').parent();
|
|
if (start != end) {
|
|
$bq.append($(end).prevUntil(_bq_).toArray().reverse()).append(end);
|
|
}
|
|
}
|
|
|
|
sel.select();
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
|
|
this.fireChangeNode(sel);
|
|
},
|
|
/**
|
|
* @brief
|
|
*/
|
|
API_EXEC_BOX : function(sender, params) {
|
|
var self = this, sel = this.oApp.getSelection(), start, end, ancestor, match, $bx, _bx_ = 'div.bx';
|
|
|
|
if (!sel) return false;
|
|
|
|
start = sel.getStartNode();
|
|
match = this.match(start, _bx_);
|
|
|
|
if (match) {
|
|
this.unwrap(match);
|
|
sel.select();
|
|
this.fireChangeNode(sel);
|
|
return;
|
|
}
|
|
|
|
// get block-level common ancestor
|
|
ancestor = sel.commonAncestorContainer;
|
|
start = this.getValidParent(this.getBlockParent(this.getChild(start, ancestor)), 'div');
|
|
end = sel.collapsed?start:this.getChild(sel.getEndNode(), start[_pn_]);
|
|
|
|
// remove box in a selection
|
|
$(sel.getNodes()).filter(_bx_).each(function(){ self.unwrap(this) });
|
|
|
|
// wrap nodes with new <div>
|
|
if (start == end && /^(div|p)$/i.test(start[_nn_])) {
|
|
$bq = $(start).wrapInner('<div class="bx" />').children().unwrap();
|
|
} else {
|
|
$bq = $(start).wrap('<div class="bx" />').parent();
|
|
if (start != end) $bq.append($(end).prevUntil(_bx_).toArray().reverse()).append(end);
|
|
}
|
|
|
|
sel.select();
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
|
|
this.fireChangeNode(sel);
|
|
},
|
|
API_EXEC_INDENT : function(sender, params) {
|
|
var parents = this.getBlockParents();
|
|
|
|
$(parents).each(function(){
|
|
var $this = $(this), left = parseInt($this.css('margin-left'), 10);
|
|
|
|
left = isNaN(left)?30:left+30;
|
|
|
|
$this.css('margin-left', left+'px');
|
|
});
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
|
|
this.fireChangeNode();
|
|
},
|
|
API_EXEC_OUTDENT : function(sender, params) {
|
|
var parents = this.getBlockParents();
|
|
|
|
$(parents).each(function(){
|
|
var $this = $(this), left = parseInt($this.css('margin-left'), 10);
|
|
|
|
left = Math.max(isNaN(left)?0:left-30, 0);
|
|
left = left?left+'px':'';
|
|
|
|
$this.css('margin-left', left);
|
|
|
|
if (!$this.attr('style')) $this.removeAttr('style');
|
|
});
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
|
|
this.fireChangeNode();
|
|
},
|
|
/**
|
|
* @brief
|
|
* @param Number indicates heading level.
|
|
*/
|
|
API_EXEC_HEADING : function(sender, params) {
|
|
var sel = this.oApp.getSelection(), nodes, n, i, c, $node, first, end;
|
|
|
|
if (!sel) return false;
|
|
|
|
nodes = this.getBlockParents();
|
|
n = parseInt(params[0], 10);
|
|
|
|
for(i=0,c=nodes.length; i<c; i++) {
|
|
$node = $(nodes[i]).wrapInner((n&&n<7)?'<h'+n+'>':'<p>');
|
|
|
|
if (!$node.is('td,th,li')) $node = $node.children(0).unwrap();
|
|
if (!first) first = $node[0];
|
|
}
|
|
|
|
end = $node[0];
|
|
if (first == end) {
|
|
sel.selectNode(first);
|
|
} else {
|
|
sel.setStartBefore(first);
|
|
sel.setEndAfter(end);
|
|
}
|
|
|
|
sel.select();
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
|
|
this.fireChangeNode(sel);
|
|
},
|
|
/**
|
|
* @brief Shows the heading options layer
|
|
*/
|
|
API_SHOW_HEADING_LAYER : function(sender, params) {
|
|
var $layer = this.$head_layer;
|
|
|
|
if (!$layer || $layer.hasClass('open')) return;
|
|
|
|
$layer.addClass('open').parent('li').addClass('active');
|
|
|
|
this.cast('HIDE_ALL_LAYER', [$layer[0]]);
|
|
},
|
|
/**
|
|
* @brief Hides the heading options layer
|
|
*/
|
|
API_HIDE_HEADING_LAYER : function(sender, params) {
|
|
var $layer = this.$head_layer;
|
|
|
|
if (!$layer || !$layer.hasClass('open')) return;
|
|
|
|
$layer.removeClass('open').parent('li').removeClass('active');
|
|
},
|
|
/**
|
|
* @brief Toggle the haeding options layer
|
|
*/
|
|
API_TOGGLE_HEADING_LAYER : function(sender, params) {
|
|
if (!this.$head_layer) return;
|
|
if (this.$head_layer.hasClass('open')) {
|
|
this.cast('HIDE_HEADING_LAYER');
|
|
} else {
|
|
this.cast('SHOW_HEADING_LAYER');
|
|
}
|
|
},
|
|
API_EXEC_LINEHEIGHT : function(sender, params) {
|
|
var sel = this.oApp.getSelection(), nodes;
|
|
|
|
if (!sel) return;
|
|
|
|
nodes = this.getBlockParents();
|
|
$(nodes).css('line-height', params[0]);
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
|
|
this.fireChangeNode(sel);
|
|
},
|
|
API_SHOW_LINEHEIGHT_LAYER : function(sender, params) {
|
|
var $layer = this.$line_layer;
|
|
|
|
if (!$layer || $layer.hasClass('open')) return;
|
|
|
|
$layer.addClass('open').parent('li').addClass('active');
|
|
|
|
this.cast('HIDE_ALL_LAYER', [$layer[0]]);
|
|
},
|
|
API_HIDE_LINEHEIGHT_LAYER : function(sender, params) {
|
|
var $layer = this.$line_layer;
|
|
|
|
if (!$layer || !$layer.hasClass('open')) return;
|
|
|
|
$layer.removeClass('open').parent('li').removeClass('active');
|
|
},
|
|
API_TOGGLE_LINEHEIGHT_LAYER : function(sender, params) {
|
|
if (!this.$line_layer) return;
|
|
if (this.$line_layer.hasClass('open')) {
|
|
this.cast('HIDE_LINEHEIGHT_LAYER');
|
|
} else {
|
|
this.cast('SHOW_LINEHEIGHT_LAYER');
|
|
}
|
|
},
|
|
/**
|
|
* @brief Hides All layer
|
|
*/
|
|
API_HIDE_ALL_LAYER : function(sender, params) {
|
|
if (this.$head_layer && this.$head_layer[0] != params[0]) this.cast('HIDE_HEADING_LAYER');
|
|
if (this.$line_layer && this.$line_layer[0] != params[0]) this.cast('HIDE_LINEHEIGHT_LAYER');
|
|
},
|
|
API_ON_CHANGE_NODE : function(sender, params) {
|
|
var self=this, node = params[0], state = {}, $nodes, $node, i, ml;
|
|
|
|
if (!node) {
|
|
$.each(this.$btns, function(key){
|
|
self.cast('SET_COMMAND_STATE', [this[0], 'disable']);
|
|
});
|
|
return;
|
|
}
|
|
|
|
state.qm = state.bx = state.id = 'normal';
|
|
state.od = 'disable';
|
|
|
|
$nodes = $(node).parentsUntil('.'+_xr_).andSelf();
|
|
|
|
for(i = $nodes.length-1; i > -1 ; i--) {
|
|
if (!is_block($nodes[i])) continue;
|
|
|
|
$node = $nodes.eq(i);
|
|
|
|
if ($node.is('blockquote.bq')) state.qm = 'active';
|
|
else if ($node.is('div.bx')) state.bx = 'active';
|
|
|
|
if (state.od == 'disable' && (ml = $node.css('margin-left')) && !isNaN(ml=parseInt(ml)) && ml > 0) state.od = 'normal';
|
|
}
|
|
|
|
$.each(this.$btns, function(key) {
|
|
self.cast('SET_COMMAND_STATE', [this[0], state[key]]);
|
|
});
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ Font Plugin
|
|
* @brief Set font name, size and color
|
|
*/
|
|
Font = xe.createPlugin('Font', {
|
|
_fn : {},
|
|
rx_color : /^(#([0-9a-f]{3}|[0-9a-f]{6})|rgb\( *\d{1,3} *, *\d{1,3} *, *\d{1,3} *\))$/i,
|
|
selection : null, // preserved selection
|
|
$ff_layer : null, // font famliy layer
|
|
$fs_layer : null, // font size layer
|
|
$cr_layer : null, // color layer
|
|
$ff_btn : null,
|
|
$fs_btn : null,
|
|
$cr_btn : null,
|
|
|
|
init : function() {
|
|
var self = this;
|
|
|
|
this._fn = {
|
|
ff : function(){ self.cast('TOGGLE_FONTFAMILY_LAYER'); return false; },
|
|
fs : function(){ self.cast('TOGGLE_FONTSIZE_LAYER'); return false; },
|
|
cr : function(){ self.cast('TOGGLE_COLOR_LAYER'); return false; },
|
|
hover : function(){ $(this).parent().addClass('hover'); return false; },
|
|
out : function(){ $(this).parent().removeClass('hover'); return false; }
|
|
};
|
|
},
|
|
activate : function() {
|
|
var self = this, $tb = this.oApp.$toolbar, $pv, $col, $bgcol, col, bgcol, _bc_ = 'background-color';
|
|
|
|
// buttons
|
|
this.$ff_btn = $tb.find('li.ff > button:first').mousedown(this._fn.ff);
|
|
this.$fs_btn = $tb.find('li.fs > button:first').mousedown(this._fn.fs);
|
|
this.$cr_btn = $tb.find('li.cr > button:first').mousedown(this._fn.cr);
|
|
|
|
// layers
|
|
this.$ff_layer = this.$ff_btn.next('.lr');
|
|
this.$fs_layer = this.$fs_btn.next('.lr');
|
|
this.$cr_layer = this.$cr_btn.next('.lr').mousedown(function(event){ event.stopPropagation() });
|
|
this.$cr_preview = $pv = this.$cr_layer.find('>.pv span');
|
|
|
|
// items
|
|
this.$ff_layer.find('button')
|
|
.hover(this._fn.hover, this._fn.out)
|
|
.click(function(){
|
|
self.cast('EXEC_FONTFAMILY', [$(this).css('font-family')]);
|
|
self.cast('HIDE_FONTFAMILY_LAYER');
|
|
return false;
|
|
});
|
|
|
|
this.$fs_layer.find('button')
|
|
.hover(this._fn.hover, this._fn.out)
|
|
.click(function(){
|
|
self.cast('EXEC_FONTSIZE', [$(this).css('font-size')]);
|
|
self.cast('HIDE_FONTSIZE_LAYER');
|
|
return false;
|
|
});
|
|
|
|
$col = this.$cr_layer.find('>.fc input[type=text]')
|
|
.focus(function(){ })
|
|
.blur(function(){ })
|
|
.keypress(function(){ });
|
|
|
|
$bgcol = this.$cr_layer.find('>.bc input[type=text]')
|
|
.focus(function(){ })
|
|
.blur(function(){ })
|
|
.keypress(function(){ });
|
|
|
|
this.$cr_layer
|
|
.find('>.fc button')
|
|
.hover(
|
|
function(){ $pv.css('color', $(this).css(_bc_)); },
|
|
function(){ $pv.css('color', $pv.data('col')); }
|
|
)
|
|
.click(function(){
|
|
var val = $(this).css(_bc_);
|
|
|
|
if (col && val == col) {
|
|
self.cast('EXEC_FONTCOLOR', [val]);
|
|
self.cast('HIDE_COLOR_LAYER');
|
|
} else {
|
|
$pv.data('col', col=val).css('color', val);
|
|
$col.val(self.toHex(val)).prev('label').hide();
|
|
setTimeout(function(){ col = '' }, 500);
|
|
}
|
|
return false;
|
|
})
|
|
.end()
|
|
.find('>.bc button')
|
|
.hover(
|
|
function(){ $pv.css(_bc, $(this).css(_bc_)); },
|
|
function(){ $pv.css(_bc_, $pv.data('bgcol')); }
|
|
)
|
|
.click(function(){
|
|
var $this = $(this), val = $this.css(_bc_);
|
|
|
|
if (bgcol && val == bgcol) {
|
|
self.cast('EXEC_FONTBGCOLOR', [val]);
|
|
self.cast('HIDE_COLOR_LAYER');
|
|
} else {
|
|
$pv.data('bgcol', bgcol=val).css(_bc_, val);
|
|
$bgcol.val(self.toHex(val)).prev('label').hide();
|
|
setTimeout(function(){ bgcol = '' }, 500);
|
|
}
|
|
return false;
|
|
})
|
|
.end()
|
|
.find('>button')
|
|
.click(function(){
|
|
var col = $pv.data('col'), bgcol = $pv.data('bgcol');
|
|
|
|
if (col) self.cast('EXEC_FONTCOLOR', [col]);
|
|
if (bgcol) self.cast('EXEC_FONTBGCOLOR', [bgcol]);
|
|
self.cast('HIDE_COLOR_LAYER');
|
|
|
|
return false;
|
|
});
|
|
},
|
|
deactivate : function() {
|
|
this.$ff_btn.unbind('mousedown');
|
|
this.$fs_btn.unbind('mousedown');
|
|
this.$cr_btn.unbind('mousedown');
|
|
|
|
this.$ff_layer.find('button').unbind();
|
|
this.$fs_layer.find('button').unbind();
|
|
this.$cr_layer.find('button,input').unbind();
|
|
},
|
|
showLayer : function($layer) {
|
|
if (!$layer || $layer.hasClass('open')) return;
|
|
|
|
$layer.addClass('open').parent('li').addClass('active');
|
|
|
|
this.cast('HIDE_ALL_LAYER', [$layer[0]]);
|
|
},
|
|
hideLayer : function($layer) {
|
|
if (!$layer || !$layer.hasClass('open')) return;
|
|
|
|
$layer.removeClass('open').parent('li').removeClass('active');
|
|
},
|
|
toggleLayer : function($layer, api) {
|
|
if (!$layer) return;
|
|
if ($layer.hasClass('open')) {
|
|
this.cast('HIDE_'+api+'_LAYER');
|
|
} else {
|
|
this.cast('SHOW_'+api+'_LAYER');
|
|
}
|
|
},
|
|
toHex : function(col) {
|
|
var regNoSharp, regRGB;
|
|
|
|
regNoSharp = /^([0-9A-F]{3}|[0-9A-F]{6})$/i;
|
|
regRGB = /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i;
|
|
|
|
function fixed(num, count) {
|
|
var str = num + '';
|
|
|
|
while(str.length < count) str = '0' + str;
|
|
|
|
return str;
|
|
};
|
|
|
|
if (regNoSharp.test(col)) {
|
|
col = '#'+col;
|
|
} else if (regRGB.test(col)) {
|
|
col = '#'+fixed((+RegExp.$1).toString(16),2)+fixed((+RegExp.$2).toString(16),2)+fixed((+RegExp.$3).toString(16),2);
|
|
}
|
|
|
|
return col;
|
|
},
|
|
/**
|
|
* @brief Set font size
|
|
* @param Number indicates font size in pixel
|
|
*/
|
|
API_EXEC_FONTSIZE : function(sender, params) {
|
|
if(!params[0]) return;
|
|
|
|
this.cast('EXEC_FONTSTYLE', [{fontSize:params[0]}]);
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
},
|
|
/**
|
|
* @brief Set font family
|
|
* @param String indicates font family
|
|
*/
|
|
API_EXEC_FONTFAMILY : function(sender, params) {
|
|
if(!params[0]) return;
|
|
|
|
this.cast('EXEC_FONTSTYLE', [{fontFamily:params[0]}]);
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
},
|
|
/**
|
|
* @brief Set font color
|
|
* @param String indicates color
|
|
*/
|
|
API_EXEC_FONTCOLOR : function(sender, params) {
|
|
if(!params[0]) return;
|
|
|
|
this.cast('EXEC_FONTSTYLE', [{color:params[0]}]);
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
},
|
|
/**
|
|
* @breif Set font background color
|
|
* @param String indicates background color
|
|
*/
|
|
API_EXEC_FONTBGCOLOR : function(sender, params) {
|
|
if(!params[0]) return;
|
|
|
|
this.cast('EXEC_FONTSTYLE', [{backgroundColor:params[0]}]);
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
},
|
|
/**
|
|
* @brief Set font style
|
|
* @param styles Styling information
|
|
*/
|
|
API_EXEC_FONTSTYLE : function(sender, params) {
|
|
var sel = this.oApp.getSelection(), styles = params[0], span, val;
|
|
|
|
if (sel.collapsed) {
|
|
}
|
|
|
|
sel.styleRange(styles);
|
|
this.oApp.$richedit.focus();
|
|
sel.select();
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
},
|
|
/**
|
|
* @brief Show fontfamily layer
|
|
*/
|
|
API_SHOW_FONTFAMILY_LAYER : function(sender, params) {
|
|
this.showLayer(this.$ff_layer);
|
|
},
|
|
/**
|
|
* @brief Hide fontfamily layer
|
|
*/
|
|
API_HIDE_FONTFAMILY_LAYER : function(sender, params) {
|
|
this.hideLayer(this.$ff_layer);
|
|
},
|
|
/**
|
|
* @brief Toggle fontfamily layer
|
|
*/
|
|
API_TOGGLE_FONTFAMILY_LAYER : function(sender, params) {
|
|
this.toggleLayer(this.$ff_layer, 'FONTFAMILY');
|
|
},
|
|
/**
|
|
* @brief Show fontsize layer
|
|
*/
|
|
API_SHOW_FONTSIZE_LAYER : function(sender, params) {
|
|
this.showLayer(this.$fs_layer);
|
|
},
|
|
/**
|
|
* @brief Hide fontsize layer
|
|
*/
|
|
API_HIDE_FONTSIZE_LAYER : function(sender, params) {
|
|
this.hideLayer(this.$fs_layer);
|
|
},
|
|
/**
|
|
* @brief Toggle fontsize layer
|
|
*/
|
|
API_TOGGLE_FONTSIZE_LAYER : function(sender, params) {
|
|
this.toggleLayer(this.$fs_layer, 'FONTSIZE');
|
|
},
|
|
/**
|
|
* @brief Show color layer
|
|
*/
|
|
API_SHOW_COLOR_LAYER : function(sender, params) {
|
|
this.showLayer(this.$cr_layer);
|
|
},
|
|
/**
|
|
* @brief Hide color layer
|
|
*/
|
|
API_HIDE_COLOR_LAYER : function(sender, params) {
|
|
this.hideLayer(this.$cr_layer);
|
|
},
|
|
/**
|
|
* @brief Toggle color layer
|
|
*/
|
|
API_TOGGLE_COLOR_LAYER : function(sender, params) {
|
|
this.toggleLayer(this.$cr_layer, 'COLOR');
|
|
},
|
|
API_HIDE_ALL_LAYER : function(sender, params) {
|
|
var $ff = this.$ff_layer, $fs = this.$fs_layer, $cr = this.$cr_layer, except = params[0];
|
|
|
|
if ($ff && $ff[0] != except) this.cast('HIDE_FONTFAMILY_LAYER');
|
|
if ($fs && $fs[0] != except) this.cast('HIDE_FONTSIZE_LAYER');
|
|
if ($cr && $cr[0] != except) this.cast('HIDE_COLOR_LAYER');
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ LineBreak plugin
|
|
* @brief Insert <br> or wrap the block with <p> when return key is pressed.
|
|
*/
|
|
LineBreak = xe.createPlugin('LineBreak', {
|
|
_fn : null,
|
|
_br_timer : null,
|
|
_in_br : false,
|
|
|
|
init : function(){
|
|
this._fn = bind(this, this.keydown);
|
|
},
|
|
activate : function() {
|
|
this.oApp.$richedit.keydown(this._fn);
|
|
|
|
// If you pres Enter key, <br> will be inserted by default.
|
|
this.oApp.setDefault('force_br', true);
|
|
},
|
|
deactivate : function() {
|
|
this.oApp.$richedit.unbind('keydown', this._fn);
|
|
},
|
|
keydown : function(event) {
|
|
var sel, sc, ec;
|
|
|
|
if (event.keyCode != 13 || event.ctrlKey || event.altKey || event.metaKey) {
|
|
if (this._in_br) {
|
|
clearTimeout(this._br_timer);
|
|
this._br_timer = null;
|
|
this._in_br = false;
|
|
}
|
|
return;
|
|
}
|
|
if (!this.oApp.getOption('force_br')) return;
|
|
if (!(sel = this.oApp.getSelection())) return;
|
|
|
|
event.shiftKey ? this.wrapBlock(sel) : this.insertBR(sel);
|
|
|
|
return false;
|
|
},
|
|
wrapBlock : function(sel) {
|
|
var self = this, sc, so, eo, $node, $clone, $bookmark, last, _xb_ = '_xeed_tmp_bookmark';
|
|
|
|
// collect all sibling
|
|
function sibling(node, prev) {
|
|
var s, ret = [];
|
|
|
|
while(s=node[prev?_ps_:_ns_]) {
|
|
if (s[_nt_] == 3 || (s[_nt_] == 1 && !rx_block.test(s[_nn_]))) ret.push(s);
|
|
node = s;
|
|
}
|
|
|
|
if (prev) ret = ret.reverse();
|
|
|
|
return ret;
|
|
}
|
|
|
|
// delete contents
|
|
sel.deleteContents();
|
|
|
|
// is this node in a list?
|
|
sc = sel[_sc_];
|
|
so = sel[_so_];
|
|
eo = sel[_eo_];
|
|
|
|
if (sc[_nt_] == 1 && so > -1 && (sc=sc[_cn_][so]) && sc[_nt_] == 1) {
|
|
$node = $(sc);
|
|
} else {
|
|
if (sc[_nt_] == 3) sc = sc[_pn_];
|
|
$node = $(sc=sel[_sc_]);
|
|
}
|
|
|
|
// find block parent
|
|
$node = $node.parentsUntil('.'+_xr_).filter(function(){ return rx_block.test(this[_nn_]) });
|
|
$node = $node.length?$node.eq(0):$($.merge(sibling(sc,1), [sc], sibling(sc))).wrap('<p>').parent();
|
|
|
|
// wrap with '<p>' in a table cell
|
|
if ($node.is('td,th')) $node = $node.wrapInner('<p>').children(0);
|
|
|
|
// create clone
|
|
if (/^h[1-6]$/i.test($node[0].nodeName)) {
|
|
$clone = $('<p />');
|
|
} else {
|
|
$clone = $node.clone().empty();
|
|
}
|
|
|
|
// append clone of this node
|
|
$node.after($clone).append($bookmark=$('<span>').attr('id',_xb_));
|
|
|
|
sel.setEndAfter($bookmark[0]);
|
|
$clone.append(sel.extractContents());
|
|
$bookmark.remove();
|
|
$('#'+_xb_).remove();
|
|
|
|
if (!$.browser.msie && !$clone[0][_cn_].length) {
|
|
$clone.append(d.createTextNode(invisibleCh));
|
|
}
|
|
|
|
sel.setStart($clone[0], 0);
|
|
sel.collapseToStart();
|
|
sel.select();
|
|
},
|
|
insertBR : function(sel) {
|
|
var self = this, $br = $('<br>'), st, $par, $p;
|
|
|
|
// insert Node
|
|
sel.insertNode($br[0]);
|
|
sel.selectNode($br[0]);
|
|
sel.collapseToEnd();
|
|
|
|
// Opera web browser can't prevent default keydown event
|
|
// So, you need to workaround it with blur and focus tricks.
|
|
if ($.browser.opera) {
|
|
st = d.documentElement.scrollTop;
|
|
this.oApp.$richedit[0].blur();
|
|
|
|
setTimeout(function(){
|
|
var _sel = self.oApp.getEmptySelection();
|
|
|
|
self.oApp.$richedit[0].focus();
|
|
|
|
if (st != d.documentElement.scrollTop) d.documentElement.scrollTop = st;
|
|
if (!$br[0][_ns_]) $br.after(d.createTextNode(invisibleCh));
|
|
|
|
_sel.selectNode($br[0][_ns_]);
|
|
_sel.collapseToStart();
|
|
_sel.select();
|
|
}, 0);
|
|
return;
|
|
}
|
|
|
|
if (!$.browser.msie) {
|
|
if (!$br[0][_ns_]) $br.after(d.createTextNode(invisibleCh));
|
|
if ($.browser.safari) {
|
|
// TODO : remove broken character which is displayed only in Safari.
|
|
}
|
|
}
|
|
|
|
sel.select();
|
|
|
|
// <br> timer
|
|
if (!this._in_br) {
|
|
this._in_br = true;
|
|
this._br_timer = setTimeout(function(){ self._in_br = false; }, 500);
|
|
return;
|
|
}
|
|
|
|
if ($br && $br.prev().is('br')) {
|
|
$p = $('<p>'+invisibleCh+'</p>');
|
|
$par = $br.parentsUntil('.'+_xr_);
|
|
|
|
$par.length ? $par.eq(-1).after($p) : $br.after($p);
|
|
|
|
$br.prev('br').remove().end().remove();
|
|
|
|
sel.setStart($p[0], 0);
|
|
sel.collapseToStart();
|
|
sel.select();
|
|
}
|
|
|
|
this._in_br = false;
|
|
clearTimeout(this._br_timer);
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ Hotkey plugin
|
|
*/
|
|
Hotkey = xe.createPlugin('Hotkey', {
|
|
_fn : null,
|
|
_key : {},
|
|
map : {
|
|
'BACKSPACE BKSP':8,
|
|
'TAB':9,
|
|
'ENTER RETURN':13,
|
|
'ESC ESCAPE':27,
|
|
'SPACE':32,
|
|
// arrows
|
|
'UP':38,
|
|
'DOWN':40,
|
|
'LEFT':37,
|
|
'RIGHT':39,
|
|
|
|
'HOME':36,
|
|
'PAGEUP PGUP':33,
|
|
'PAGEDOWN PGDN':34,
|
|
'END':35,
|
|
'INSERT INS':45,
|
|
'DELETE DEL':46,
|
|
|
|
// special chars
|
|
'=':187, ', <':188, '- _':189, '. >':190, '/ ?':191, '` ~':192, '{ [':219, '\\ |':220, '} ]':221, '\'\"':222
|
|
},
|
|
|
|
// constructor
|
|
init : function() {
|
|
var self = this;
|
|
|
|
this._fn = function(event){ return self.hotkey(event) };
|
|
this._key = {};
|
|
|
|
// build key map
|
|
if (!this.map.A) {
|
|
// split multiple name
|
|
$.each(this.map, function(k,v){
|
|
if(k.indexOf(' ')<0) return true;
|
|
|
|
var keys = k.split(' '), i, c;
|
|
|
|
for(i=0,c=keys.length; i < c; i++)
|
|
self.map[keys[i]] = v;
|
|
|
|
delete(self.map[k]);
|
|
});
|
|
|
|
function build(from, to) {
|
|
for(var c=from; c<=to; c++) self.map[String.fromCharCode(c)] = c;
|
|
};
|
|
|
|
// A~Z
|
|
build(65, 90);
|
|
|
|
// 0~9
|
|
build(48, 57);
|
|
}
|
|
},
|
|
|
|
// on activate
|
|
activate : function() {
|
|
this.oApp.$richedit.keydown(this._fn);
|
|
},
|
|
|
|
// on deactivate
|
|
deactivate : function() {
|
|
this.oApp.$richedit.unbind('keydown',this._fn);
|
|
},
|
|
|
|
/**
|
|
* @brief hotkey event handler
|
|
* @param e jQuery event object
|
|
*/
|
|
hotkey : function(e) {
|
|
var _k = this._key, kc = e.keyCode, k;
|
|
|
|
// always skip - shift, alt, ctrl, windows, kor/eng
|
|
if ((15 < kc && kc < 19) || kc == 229) return true;
|
|
if (e.metaKey && e.ctrlKey) e.metaKey = false;
|
|
|
|
// make hotkey string
|
|
k = this.key2str(e);
|
|
|
|
if (_k[k]) {
|
|
_k[k](e);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* @brief return normalize hotkey string
|
|
*/
|
|
normalize : function(str) {
|
|
var keys = (str||'').replace(/ \t\r\n/g, '').toUpperCase().split('+'), obj={}, i, c;
|
|
|
|
for(i=0,c=keys.length; i < c; i++) {
|
|
switch(keys[i]) {
|
|
case 'ALT': obj.altKey = 1; break;
|
|
case 'CTRL': obj.ctrlKey = 1; break;
|
|
case 'META': obj.metaKey = 1; break;
|
|
case 'SHIFT': obj.shiftKey = 1; break;
|
|
default:
|
|
if (this.map[keys[i]]) obj.keyCode = this.map[keys[i]];
|
|
}
|
|
}
|
|
|
|
// if there is no valid keyCode, return undefined object.
|
|
if (!obj.keyCode) return;
|
|
|
|
return this.key2str(obj);
|
|
},
|
|
|
|
/**
|
|
* @brief make hotkey string from the key event object.
|
|
* @return hotkey string
|
|
*/
|
|
key2str : function(e) {
|
|
var ret = [];
|
|
|
|
if (e.altKey) ret.push('ALT');
|
|
if (e.ctrlKey) ret.push('CTRL');
|
|
if (e.metaKey) ret.push('META');
|
|
if (e.shiftKey) ret.push('SHIFT');
|
|
ret.push(e.keyCode);
|
|
|
|
return ret.join('+');
|
|
},
|
|
|
|
/**
|
|
* @brief register a hotkey
|
|
* @param str Hotkey string
|
|
* @param fn Hotkey function
|
|
*/
|
|
API_REGISTER_HOTKEY : function(sender, params) {
|
|
var str = this.normalize(params[0]), fn = params[1];
|
|
|
|
if (str) this._key[str] = fn;
|
|
},
|
|
|
|
/**
|
|
* @brief unregister a hotkey
|
|
* @param str Hotkey string
|
|
*/
|
|
API_UNREGISTER_HOTKEY : function(sender, params) {
|
|
var str = this.normalize(params[0]);
|
|
|
|
if (str && this._key[str]) delete this._key[str];
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ Content Filter plugin
|
|
*/
|
|
Filter = xe.createPlugin('ContentFilter', {
|
|
_in : [], // input filters
|
|
_out : [], // output filters
|
|
init : function() {
|
|
this._in = [];
|
|
this._out = [];
|
|
},
|
|
|
|
/**
|
|
* @brief Register a filter
|
|
* @params type Filter type string. 'in', 'out',
|
|
* @params func Filter function
|
|
*/
|
|
API_REGISTER_FILTER : function(sender, params) {
|
|
var type = params[0], func = params[1];
|
|
|
|
if (type != 'in' || type != 'out') return;
|
|
this['_'+type].push(func);
|
|
},
|
|
|
|
/**
|
|
* @brief Unregister a filter
|
|
* @params type Filter type string.
|
|
* @params func Filter function
|
|
*/
|
|
API_UNREGISTER_FILTER : function(sender, params) {
|
|
var type = params[0], func = params[1], pool, newPool=[], i, c;
|
|
|
|
if (type != 'in' || type != 'out') return;
|
|
for(i=0,pool=this['_'+type],c=pool.length; i < c; i++) {
|
|
if (pool[i] !== func) newPool.push(pool[i]);
|
|
}
|
|
this['_'+type] = newPool;
|
|
},
|
|
|
|
/**
|
|
* @brief Run input filters before SET_CONTENT
|
|
*/
|
|
API_BEFORE_SET_CONTENT : function(sender, params) {
|
|
for(var i=0,c=this._in.length; i < c; i++) params[0] = this._in[i](params[0]);
|
|
},
|
|
|
|
/**
|
|
* @brief Run output filters before GET_CONTENT
|
|
*/
|
|
API_BEFORE_GET_CONTENT : function(sender, params) {
|
|
for(var i=0,c=this._out.length; i < c; i++) params[0] = this._out[i](params[0]);
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ Edit Mode plugin
|
|
* @brief Switch edit mode
|
|
*/
|
|
EditMode = xe.createPlugin('EditMode', {
|
|
// constructor
|
|
init : function() { },
|
|
activate : function() {
|
|
var self = this, app = this.oApp, $r = this.oApp.$root;
|
|
|
|
this.$btn_wysiwyg = $r.find('button.wysiwyg').mousedown(function(){ self.cast('MODE_WYSIWYG'); return false; });
|
|
this.$btn_html = $r.find('button.html').mousedown(function(){ self.cast('MODE_HTML'); return false; });
|
|
|
|
this.$btn_wysiwyg_p = this.$btn_wysiwyg.parent();
|
|
this.$btn_html_p = this.$btn_html.parent();
|
|
|
|
app.$textarea.hide();
|
|
app.$richedit.show();
|
|
},
|
|
deactivate : function() {
|
|
this.$btn_wysiwyg.unbind('mousedown');
|
|
this.$btn_html.unbind('mousedown');
|
|
},
|
|
API_MODE_WYSIWYG : function(sender, params) {
|
|
var app = this.oApp;
|
|
|
|
if (app.$richedit.is(':visible')) return true;
|
|
|
|
app.$richedit.show().parent().css('overflow','');
|
|
app.$textarea.hide();
|
|
|
|
// set active button
|
|
this.$btn_wysiwyg_p.addClass('active');
|
|
this.$btn_html_p.removeClass('active');
|
|
|
|
// set content
|
|
this.cast('SET_CONTENT', [app.$textarea.val()]);
|
|
},
|
|
API_MODE_HTML : function(sender, params) {
|
|
var app = this.oApp, h;
|
|
if (app.$richedit.is(':hidden')) return true;
|
|
|
|
app.$textarea.show().css('height', '100%').css('width', '100%').css('border',0);
|
|
app.$richedit.hide().parent().css('overflow','hidden');
|
|
|
|
// set active button
|
|
this.$btn_wysiwyg_p.removeClass('active');
|
|
this.$btn_html_p.addClass('active');
|
|
|
|
// set html code
|
|
this.cast('SET_CONTENT_HTML', [app.$richedit.html()]);
|
|
},
|
|
// If html editor is activated, cancel default SET_CONTENT action
|
|
// and call SET_CONTENT_HTML instead.
|
|
API_BEFORE_SET_CONTENT : function(sender, params) { },
|
|
API_BEFORE_BEFORE_SET_CONTENT : function(sender, params) {
|
|
if (this.$btn_html_p.hasClass('active')) {
|
|
this.cast('SET_CONTENT_HTML', params);
|
|
return false;
|
|
}
|
|
},
|
|
/**
|
|
* @brief Put html code into the textarea
|
|
* @param html String html code
|
|
*/
|
|
API_SET_CONTENT_HTML : function(sender, params) {
|
|
this.oApp.$textarea.val( params[0]||'' );
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ Resize Plugin
|
|
*/
|
|
Resize = xe.createPlugin('Resize', {
|
|
_fn : null,
|
|
prev_height : 400,
|
|
$resize_bar : null,
|
|
$auto_check : null,
|
|
$container : null,
|
|
|
|
init : function() {
|
|
var self = this, startY, startH, resizing;
|
|
|
|
function isLeftButton(event) {
|
|
return (event.which == 1);
|
|
}
|
|
|
|
this._fn = {
|
|
down : function(event) {
|
|
var oChk = self.$auto_check.get(0);
|
|
|
|
if (event.target != this || !isLeftButton(event)) return true;
|
|
if (oChk.checked || oChk.disabled) return true;
|
|
|
|
startY = event.pageY;
|
|
startH = parseInt(self.$container.css('height'));
|
|
$(document).mousemove(self._fn.move).one('mouseup', self._fn.up);
|
|
|
|
resizing = true;
|
|
|
|
self.cast('RESIZE_START');
|
|
|
|
return false;
|
|
},
|
|
up : function(event) {
|
|
if (!resizing) return true;
|
|
|
|
$(document).unbind({mousemove:self._fn.move, mouseup:self._fn.up});
|
|
|
|
resizing = false;
|
|
|
|
self.cast('RESIZE_END');
|
|
|
|
return false;
|
|
},
|
|
move : function(event) {
|
|
var diff_y, new_h;
|
|
|
|
if (!resizing || !isLeftButton(event)) return true;
|
|
|
|
diff_y = event.pageY - startY;
|
|
new_h = startH + diff_y;
|
|
|
|
self.$container.css('height', new_h);
|
|
self.cast('RESIZE');
|
|
|
|
return false;
|
|
},
|
|
check : function(event) {
|
|
self.cast('SET_AUTO_RESIZE', [this.checked]);
|
|
}
|
|
};
|
|
},
|
|
activate : function() {
|
|
var $root = this.oApp.$root, chk;
|
|
|
|
this.$container = this.oApp.$richedit.parent();
|
|
this.$resize_bar = $root.find('button.resize').mousedown(this._fn.down);
|
|
this.$auto_check = $root.find('div.autoResize > :checkbox').click(this._fn.check);
|
|
|
|
chk = this.$auto_check[0].checked;
|
|
this.cast('SET_AUTO_RESIZE', [chk]);
|
|
},
|
|
deactivate : function() {
|
|
this.$resize_bar.unbind({dragstart:this._fn.dragstart, drag:this._fn.drag});
|
|
this.$auto_check.unbind('click', this._fn.check);
|
|
},
|
|
/**
|
|
* @brief on resizing
|
|
* @param new height
|
|
* @param new mouse y position
|
|
*/
|
|
// API_RESIZE : function(sender, params) { },
|
|
/**
|
|
* @brief Start resizing
|
|
* @param current height
|
|
* @param current mouse y position
|
|
*/
|
|
// API_RESIZE_START : function(sender, params) { },
|
|
/**
|
|
* @brief End resizing
|
|
* @param new height
|
|
* @param new mouse y position
|
|
*/
|
|
// API_RESIZE_END : function(sender, params) { },
|
|
/**
|
|
* @brief Set auto resize
|
|
* @param Boolean indicates whether or not to resize automatically.
|
|
*/
|
|
API_SET_AUTO_RESIZE : function(sender, params) {
|
|
var $ctn = this.$container, $rich = this.oApp.$richedit, h;
|
|
|
|
if (params[0]) {
|
|
h = parseInt($ctn.css('height'), 10);
|
|
if (!isNaN(h)) this.prev_height = h;
|
|
|
|
$ctn.css('height', 'auto');
|
|
} else {
|
|
$ctn.css('height', this.prev_height);
|
|
}
|
|
},
|
|
API_BEFORE_MODE_HTML : function(sender, params) {
|
|
var chk = this.$auto_check.get(0).checked;
|
|
|
|
if (chk) {
|
|
this.prev_height = this.$container.height();
|
|
this.cast('SET_AUTO_RESIZE', [false]);
|
|
}
|
|
},
|
|
API_AFTER_MODE_HTML : function(sender, params) {
|
|
this.$auto_check.attr('disabled', true);
|
|
},
|
|
API_BEFORE_MODE_WYSIWYG : function(sender, params) {
|
|
var chk = this.$auto_check.get(0).checked;
|
|
|
|
if (chk) this.cast('SET_AUTO_RESIZE', [true]);
|
|
},
|
|
API_AFTER_MODE_WYSIWYG : function(sender, params) {
|
|
this.$auto_check.removeAttr('disabled');
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ UndoRedo
|
|
* @brief Manages undo and redo actions, save undo point.
|
|
*/
|
|
UndoRedo = xe.createPlugin('UndoRedo', {
|
|
_history : [],
|
|
_index : -1,
|
|
_timer : null,
|
|
_keypress : null,
|
|
$undo_btn : null,
|
|
$redo_btn : null,
|
|
|
|
init : function() {
|
|
this._history = [];
|
|
},
|
|
activate : function() {
|
|
var self = this, $tb = this.oApp.$toolbar;
|
|
|
|
if (!$tb) return;
|
|
|
|
this.$undo_btn = $tb.find('button.ud');
|
|
this.$redo_btn = $tb.find('button.rd');
|
|
|
|
this.cast('REGISTER_COMMAND', [this.$undo_btn[0], 'ctrl+z', 'EXEC_UNDO']);
|
|
this.cast('REGISTER_COMMAND', [this.$redo_btn[0], 'ctrl+shift+z', 'EXEC_REDO']);
|
|
|
|
// keypress
|
|
this._keypress = function( ) {
|
|
if (self._timer) clearTimeout(self._timer);
|
|
self._timer = setTimeout(function(){ self.cast('SAVE_UNDO_POINT') }, 500);
|
|
};
|
|
this.oApp.$richedit.keypress(this._keypress);
|
|
},
|
|
deactivate : function() {
|
|
this.oApp.$richedit.unbind('keypress', this._keypress);
|
|
|
|
this.cast('UNREGISTER_COMMAND', [this.$undo_btn[0]]);
|
|
this.cast('UNREGISTER_COMMAND', [this.$redo_btn[0]]);
|
|
|
|
this.$undo_btn = null;
|
|
this.$redo_btn = null;
|
|
|
|
},
|
|
// redraw buttons
|
|
redraw : function(index) {
|
|
var $undo = this.$undo_btn, $redo = this.$redo_btn, fn, index = this._index, len = this._history.length;
|
|
|
|
if ($undo && $undo[0]) {
|
|
fn = (index > 0 && len)?'removeClass':'addClass';
|
|
$undo.parent()[fn]('disable');
|
|
}
|
|
|
|
if ($redo && $redo[0]) {
|
|
fn = (index+1 < len)?'removeClass':'addClass';
|
|
$redo.parent()[fn]('disable');
|
|
}
|
|
},
|
|
API_EXEC_UNDO : function(sender, params) {
|
|
this.cast('RESTORE_UNDO_POINT', [this._index-1]);
|
|
},
|
|
API_EXEC_REDO : function(sender, params) {
|
|
this.cast('RESTORE_UNDO_POINT', [this._index+1]);
|
|
},
|
|
/**
|
|
* Save undo point
|
|
* @return Number indicates current undo point
|
|
*/
|
|
API_SAVE_UNDO_POINT : function(sender, params) {
|
|
var sel = this.oApp.getSelection(), history = this._history, index = this._index, item = {}, last_item, $rich = this.oApp.$richedit;
|
|
|
|
// if richedit is not shown, don't execute this command.
|
|
if ($rich.is(':hidden')) return -1;
|
|
|
|
// when undo history is saved, clear saving timer
|
|
if (this._timer) {
|
|
clearTimeout(this._timer);
|
|
this._timer = null;
|
|
}
|
|
|
|
// delete redo history
|
|
if (index+1 < history.length) history = history.slice(0, index+1);
|
|
|
|
item.content = this.cast('GET_CONTENT');
|
|
if (sel) {
|
|
item.bookmark = sel.getXPathBookmark();
|
|
} else {
|
|
item.bookmark = null;
|
|
}
|
|
|
|
// if the content isn't changed, don't save this history.
|
|
if (history.length) {
|
|
last_item = history[history.length-1];
|
|
if (item.content == last_item.content) return this._index;
|
|
}
|
|
|
|
history.push(item);
|
|
|
|
this._history = history;
|
|
this._index = history.length - 1;
|
|
|
|
this.redraw();
|
|
|
|
return this._index;
|
|
},
|
|
/**
|
|
* Restore to saved undo point
|
|
* @param Number indicates saved undo point
|
|
* @return Number indicates current undo point
|
|
*/
|
|
API_RESTORE_UNDO_POINT : function(sender, params) {
|
|
var idx = params[0], item, sel, $rich = this.oApp.$richedit;
|
|
|
|
// error : invalid index
|
|
if (idx < 0 || !(item=this._history[idx])) return -1;
|
|
|
|
// if richedit is not shown, don't execute this command.
|
|
if ($rich.is(':hidden')) return -1;
|
|
|
|
// when undo history is restored, clear saving timer
|
|
if (this._timer) {
|
|
clearTimeout(this._timer);
|
|
this._timer = null;
|
|
}
|
|
|
|
// restore content
|
|
this.cast('SET_CONTENT', [item.content]);
|
|
|
|
// restore selection
|
|
sel = this.oApp.getEmptySelection();
|
|
if (item.bookmark) {
|
|
sel.moveToXPathBookmark(item.bookmark);
|
|
sel.select();
|
|
} else {
|
|
$rich.focus().get(0);
|
|
if ($rich[0][_cn_][0]) {
|
|
sel.selectNode($rich[0][_cn_][0]);
|
|
sel.collapseToStart();
|
|
sel.select();
|
|
}
|
|
}
|
|
|
|
// next undo point
|
|
this._index = idx;
|
|
|
|
this.redraw();
|
|
|
|
return idx;
|
|
},
|
|
API_AFTER_SET_CONTENT : function(sender, params) {
|
|
if (sender != this && this.oApp.$richedit.is(':visible')) {
|
|
this.cast('SAVE_UNDO_POINT');
|
|
}
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ SChar
|
|
*/
|
|
SChar = xe.createPlugin('SChar', {
|
|
$btn : null,
|
|
$layer : null,
|
|
$text : null,
|
|
$btns : null,
|
|
|
|
init : function(){
|
|
},
|
|
activate : function() {
|
|
var self=this, app = this.oApp, $tb = app.$toolbar;
|
|
|
|
if (!$tb) return;
|
|
|
|
this.$btn = $tb.find('button.sc').mousedown(function(){ self.cast('TOGGLE_SCHAR_LAYER'); return false; });
|
|
this.$layer = this.$btn.next('div.lr')
|
|
.mousedown(function(event){ event.stopPropagation(); })
|
|
.find('button.tab')
|
|
.mousedown(function(){
|
|
self.$layer.find('li.li').removeClass('active');
|
|
$(this[_pn_]).addClass('active');
|
|
})
|
|
.click(function(){ $(this).mousedown() })
|
|
.end()
|
|
.find('li>button:not(.tab)')
|
|
.click(function(event){ self.$text[0].value += $(this).text(); })
|
|
.end();
|
|
|
|
this.$text = this.$layer.find('input:text')
|
|
.keypress(function(event){
|
|
if (event.keyCode == 13) {
|
|
self.$btns.eq(0).click();
|
|
return false;
|
|
}
|
|
});
|
|
|
|
this.$btns = this.$layer.find('button.btn')
|
|
.each(function(i){
|
|
var $this = $(this);
|
|
|
|
if (i == 0) {
|
|
$this.click(function(){
|
|
var sel = self.sel, dt, rt;
|
|
|
|
dt = d.documentElement.scrollTop;
|
|
rt = app.$richedit[0][_pn_].scrollTop;
|
|
|
|
sel.pasteHTML(self.$text.val());
|
|
|
|
self.sel = null;
|
|
self.cast('HIDE_SCHAR_LAYER');
|
|
|
|
app.$richedit.focus();
|
|
sel.select();
|
|
|
|
if (dt != d.documentElement.scrollTop) d.documentElement.scrollTop = dt;
|
|
if (rt != app.$richedit[0][_pn_].scrollTop) app.$richedit[0][_pn_].scrollTop = rt;
|
|
});
|
|
} else {
|
|
$this.click(function(){
|
|
self.cast('HIDE_SCHAR_LAYER');
|
|
});
|
|
}
|
|
});
|
|
},
|
|
deactivate : function() {
|
|
if (this.$btn) this.$btn.unbind('mousedown');
|
|
if (this.$layer) this.$layer.unbind('mousedown').find('button,input').unbind();
|
|
if (this.$text) this.$text.unbind();
|
|
},
|
|
API_SHOW_SCHAR_LAYER : function() {
|
|
var sel;
|
|
|
|
if (!this.$layer || this.$layer.hasClass('open')) return;
|
|
if (!(sel=this.oApp.getSelection())) return;
|
|
|
|
this.sel = sel; // save selection
|
|
this.$btn.parent().addClass('active');
|
|
this.$layer.addClass('open');
|
|
},
|
|
API_HIDE_SCHAR_LAYER : function() {
|
|
if (!this.$layer || !this.$layer.hasClass('open')) return;
|
|
|
|
this.$btn.parent().removeClass('active');
|
|
this.$layer.removeClass('open');
|
|
},
|
|
/**
|
|
* @brief Toggle special chars layer
|
|
*/
|
|
API_TOGGLE_SCHAR_LAYER : function() {
|
|
this.cast( (this.$layer.hasClass('open')?'HIDE':'SHOW')+'_SCHAR_LAYER' );
|
|
},
|
|
API_HIDE_ALL_LAYER : function(sender, params) {
|
|
if (sender != this) this.cast('HIDE_SCHAR_LAYER');
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ FileUpload
|
|
*/
|
|
FileUpload = xe.createPlugin('FileUpload', {
|
|
$btns : null,
|
|
$modal_box : null,
|
|
$template : null,
|
|
$file_list : null,
|
|
esc_fn : null,
|
|
selection : null,
|
|
_index : 0,
|
|
|
|
init : function(){
|
|
var self = this;
|
|
|
|
this.$btns = [];
|
|
this.esc_fn = function(event){ if(event.keyCode == 27) self.cast('HIDE_FILE_MODAL'); };
|
|
},
|
|
activate : function() {
|
|
var self = this, app = this.oApp, $tb = app.$toolbar;
|
|
|
|
this.$modal_box = app.$root.find('div.xdmw');
|
|
|
|
if (this.$modal_box.length) {
|
|
this.$attach_list = this.$modal_box.find('div.al');
|
|
this.$attach_list
|
|
.mousedown(function(event){ event.stopPropagation(); })
|
|
.find('button.btn.cs')
|
|
.click(function(){ self.cast('HIDE_FILE_MODAL'); return false; });
|
|
|
|
// show and hide function button on hover
|
|
this.$file_list = this.$attach_list.find('div.sn')
|
|
.delegate('button.ctr', 'mouseover',
|
|
function(){
|
|
var $par = $(this).parent();
|
|
|
|
if ($par.is('.uploading')) return;
|
|
$par.find('button.ctr').addClass('show');
|
|
}
|
|
)
|
|
.delegate('button.ctr', 'mouseout',
|
|
function(){ $(this).parent().find('button.ctr').removeClass('show'); }
|
|
)
|
|
.delegate('button.ctr.ins', 'click',
|
|
function(){
|
|
var $this = $(this), $item = $this.parent(), file_url = $this.parent().data('url');
|
|
|
|
self.cast('INSERT_FILE_INTO', [$item.attr('_type'), file_url, $item.find('label').text()]);
|
|
return false;
|
|
}
|
|
)
|
|
.delegate('button.ctr.del', 'click',
|
|
function(){
|
|
var $this = $(this), $item = $this.parent(), file_srl = $this.parent().attr('file_srl');
|
|
|
|
self.cast('DELETE_FILE', [file_srl]);
|
|
return false;
|
|
}
|
|
);
|
|
|
|
this.$template = this.$file_list.eq(0).find('>ul:first>li:first').remove();
|
|
|
|
// select all
|
|
this.$attach_list.find('p.task button.all')
|
|
.click(function(){
|
|
$(this).parents('div.sn:first').find('li > input:checkbox:not([disabled])').attr('checked', 'checked');
|
|
});
|
|
|
|
// insert selected files
|
|
this.$attach_list.find('p.task button.insert')
|
|
.click(function(){
|
|
var $list = $(this).parents('div.sn:first').find('li > input:checked:not([disabled])');
|
|
});
|
|
}
|
|
|
|
if ($tb) {
|
|
$.each(['al','img','mov','file'], function(){
|
|
self.$btns[this] = $tb.find('>div.t1 li.'+this).children('a,button');
|
|
});
|
|
|
|
this.$btns.al.mousedown(function(){
|
|
self.selection = self.oApp.getSelection();
|
|
});
|
|
|
|
this.$btns.al.click(function(){
|
|
self.cast('SHOW_FILE_MODAL');
|
|
return false;
|
|
});
|
|
}
|
|
},
|
|
deactivate : function() {
|
|
this.$attach_list.unbind()
|
|
.find('div.sn').undelegate()
|
|
.end()
|
|
.find('button,input').unbind();
|
|
|
|
// buttons
|
|
$.each(this.$btns, function(key){ this.unbind('click'); });
|
|
this.$btns = [];
|
|
},
|
|
createItem : function(file) {
|
|
var $item, ext, id = 'xeed-id-'+(this._index++), type;
|
|
|
|
ext = file.name.match(/\.([a-z0-9]+)$/i)[1] || '';
|
|
ext = ext.toLowerCase();
|
|
|
|
// get file type
|
|
if ($.inArray(ext, ['gif','jpg','jpeg','png']) > -1) type = 'img';
|
|
else if ($.inArray(ext, ['avi','mov','mpg','wmv','flv']) > -1) type = 'mov';
|
|
else type = 'file';
|
|
|
|
if ($.inArray(ext, ['pptx','xlsx','docx']) > -1) ext = ext.substr(0,3);
|
|
|
|
$item = this.$template.clone()
|
|
.find('button.ob > img').attr('alt', file.name).end()
|
|
.find('label').text(file.name).attr('for', id).end()
|
|
.find('input:checkbox').attr('id', id).end()
|
|
.attr('ext', ext).attr('_type', type);
|
|
|
|
if (type == 'file') {
|
|
$item.find('>button:first').addClass(ext).empty().text(file.name);
|
|
}
|
|
|
|
return $item;
|
|
},
|
|
getKey : function(file) {
|
|
return file.name.toLowerCase()+'-'+file.size;
|
|
},
|
|
updateCount : function() {
|
|
var $items = this.$file_list.find('li[type]'), $tb = this.oApp.$toolbar, $area, types = ['img','mov','file'], i, c;
|
|
|
|
this.$modal_box.find('h2 strong').text($items.length);
|
|
$tb.find('li.ti.al em > strong').text($items.length);
|
|
|
|
for(i=0,c=types.length; i<c; i++) {
|
|
$area = this.$file_list.filter('.'+types[i]);
|
|
$items = $area.find('li[_type]');
|
|
$items.length?$area.removeClass('none'):$area.addClass('none');
|
|
|
|
$tb.find('li.ti.'+types[i]).find('em > strong').text($items.length);
|
|
}
|
|
},
|
|
API_SHOW_FILE_MODAL : function() {
|
|
var self = this, uploader, file_group = [], $form, params, seq;
|
|
|
|
this.$modal_box.show();
|
|
this.$attach_list.show();
|
|
|
|
// register ESC hotkey
|
|
$(document).keydown(this.esc_fn);
|
|
|
|
this.selection = this.oApp.getSelection();
|
|
|
|
// get form
|
|
$form = this.oApp.$textarea.parents('form:first');
|
|
seq = $form.attr('editor_sequence');
|
|
|
|
// create an uploader
|
|
if (!this.$modal_box.data('uploader')) {
|
|
// additional parameter
|
|
params = {
|
|
mid : current_mid,
|
|
act : 'procFileUpload',
|
|
editor_sequence : seq,
|
|
upload_target_srl : $form.find('input[name=document_srl]').val()
|
|
};
|
|
|
|
// file onselect event
|
|
function file_onselect(files, old_len) {
|
|
var html, $ob, i, c, $list, $item, type;
|
|
|
|
for(i=old_len,c=files.length; i < c; i++) {
|
|
$item = self.createItem(files[i]).addClass('uploading').attr('_key', self.getKey(files[i]));
|
|
type = $item.attr('_type');
|
|
$list = self.$file_list.filter('.'+type).append($item);
|
|
|
|
($ob = $item.find('>button.ob'))
|
|
.data('html', $ob.html())
|
|
.html('<span class="progress"><span class="bar" style="width:0%"></span></span>');
|
|
}
|
|
|
|
self.updateCount();
|
|
|
|
setTimeout(function(){ uploader.cast('START'); }, 10);
|
|
}
|
|
|
|
function upload_onprogress(file) {
|
|
var $item = self.$file_list.find('li[_key='+self.getKey(file)+']');
|
|
|
|
$item.find('>button.ob span.bar').css('width', parseInt(file.loaded*100/file.size)+'%');
|
|
}
|
|
|
|
function upload_onfinishone(file) {
|
|
var $item = self.$file_list.find('li[_key='+self.getKey(file)+']'),
|
|
$ob = $item.find('>button.ob');
|
|
|
|
if ($item.attr('type') == 'img') {
|
|
}
|
|
|
|
$ob.html($ob.data('html')).parent().removeClass('uploading');
|
|
}
|
|
|
|
function upload_onfinish() {
|
|
var params = {}, primary, target_srl;
|
|
|
|
// document serial number
|
|
primary = editorRelKeys[seq].primary;
|
|
|
|
params = {
|
|
editor_sequence : $form.attr('editor_sequence'),
|
|
upload_target_srl : primary.value,
|
|
mid : current_mid
|
|
};
|
|
|
|
function callback(ret) {
|
|
if (!ret.files || !ret.files.item) return;
|
|
if (!$.isArray(ret.files.item)) ret.files.item = [ret.files.item];
|
|
if (!primary.value && ret.upload_target_srl) primary.value = ret.upload_target_srl;
|
|
|
|
var i, c, f, k, $item;
|
|
for(i=0,c=ret.files.item.length; i < c; i++) {
|
|
f = ret.files.item[i];
|
|
k = f.source_filename.toLowerCase()+'-'+f.file_size;
|
|
|
|
$item = self.$file_list.find('li[_key='+k+']');
|
|
|
|
if ($item.attr('_type') == 'img') {
|
|
$item.find('button.ob > img')
|
|
.load(function(){
|
|
if(this.width > this.height){
|
|
$(this).css('width', '100%');
|
|
} else {
|
|
$(this).css('height', '100%');
|
|
}
|
|
})
|
|
.attr('src', f.download_url);
|
|
}
|
|
$item.attr('file_srl', f.file_srl).data('url', f.download_url);
|
|
}
|
|
}
|
|
|
|
$.exec_xml('file', 'getFileList', params, callback, ['error', 'message', 'files', 'left_size', 'editor_sequence', 'upload_target_srl', 'upload_status']);
|
|
}
|
|
|
|
uploader = xe.createUploader(
|
|
this.$modal_box.find('button.at'),
|
|
{
|
|
url : request_uri+'index.php',
|
|
dropzone : this.$modal_box.find('div.iBody'),
|
|
params : params,
|
|
onselect : file_onselect,
|
|
onprogress : upload_onprogress,
|
|
onfinishone : upload_onfinishone,
|
|
onfinish : upload_onfinish
|
|
}
|
|
);
|
|
|
|
this.$modal_box.data('uploader', uploader);
|
|
}
|
|
},
|
|
API_HIDE_FILE_MODAL : function() {
|
|
this.$modal_box.hide();
|
|
this.$attach_list.hide();
|
|
|
|
// unregister ESC hotkey
|
|
$(document).unbind('keydown', this.esc_fn);
|
|
|
|
if (this.selection) {
|
|
try { this.selection.select(); } catch(e){};
|
|
}
|
|
},
|
|
/**
|
|
* @brief Add file to the list
|
|
* @params Array of file info. file info = { id : 0, name : '', path : '', size : 0 };
|
|
*/
|
|
API_ADD_FILE_LIST : function(sender, params) {
|
|
},
|
|
/**
|
|
* @brief Insert a file into the rich editor
|
|
*/
|
|
API_INSERT_FILE_INTO : function(sender, params) {
|
|
var type = params[0], url = params[1], name = params[2], code, sel;
|
|
|
|
if (type == 'img') {
|
|
code = '<img src="'+url+'" alt="'+name+'" />';
|
|
} else {
|
|
code = '<a href="'+url+'">'+name+'</a>';
|
|
}
|
|
|
|
sel = this.selection || this.oApp.getSelection();
|
|
|
|
if (sel) {
|
|
sel.pasteHTML(code);
|
|
} else if(this.oApp.$richedit.is(':visible')){
|
|
this.oApp.$richedit.append(code);
|
|
|
|
sel = this.oApp.getEmptySelection();
|
|
sel.selectNode(this.oApp.$richedit[0].lastChild);
|
|
}
|
|
sel.collapseToEnd();
|
|
sel.select();
|
|
},
|
|
/**
|
|
* @brief Delete a file
|
|
*/
|
|
API_DELETE_FILE : function(sender, params) {
|
|
var self = this, file_srl = params[0], $item = this.$file_list.find('li[file_srl='+file_srl+']'), i, c;
|
|
|
|
function callback(ret){
|
|
if (ret && ret.error && ret.error == 0) {
|
|
$item.remove();
|
|
|
|
self.updateCount();
|
|
}
|
|
}
|
|
|
|
$.exec_xml('file', 'procFileDelete', {file_srls:file_srl, editor_sequence:1}, callback);
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ URL
|
|
*/
|
|
URL = xe.createPlugin('URL', {
|
|
sel : null,
|
|
$btn : null,
|
|
$layer : null,
|
|
$text : null,
|
|
$btns : null,
|
|
$chk : null,
|
|
|
|
init : function(){},
|
|
activate : function() {
|
|
var self = this, app = this.oApp, $tb = app.$toolbar;
|
|
|
|
if (!$tb) return;
|
|
|
|
this.$btn = $tb.find('button.ur')
|
|
.mousedown(function(){ self.cast('TOGGLE_URL_LAYER'); return false; });
|
|
|
|
this.$layer = this.$btn.next('.lr')
|
|
.mousedown(function(event){ event.stopPropagation() });
|
|
|
|
this.$text = this.$layer.find('input:text')
|
|
.keypress(function(event){ if(event.keyCode == 13) self.$btns.eq(0).click() })
|
|
.focus(function(){ this.select(); });
|
|
|
|
this.$chk = this.$layer.find('input:checkbox');
|
|
|
|
this.$btns = this.$layer.find('button.btn')
|
|
.each(function(i){
|
|
var $this = $(this);
|
|
|
|
if (i) {
|
|
$this.click(function(){ self.cast('HIDE_URL_LAYER'); });
|
|
} else {
|
|
$this.click(function(){
|
|
var url = self.$text.val(), newWin = self.$chk[0].checked;
|
|
|
|
self.cast('EXEC_URL', [url, newWin]);
|
|
self.cast('HIDE_URL_LAYER');
|
|
});
|
|
}
|
|
});
|
|
},
|
|
deactivate : function() {
|
|
if (this.$btn) this.$btn.unbind('mousedown');
|
|
if (this.$layer) this.$layer.unbind('mousedown').find('input,button').unbind();
|
|
},
|
|
/**
|
|
* @brief Insert an url
|
|
* @param url String url
|
|
* @param newWin Boolean indicates new window link
|
|
*/
|
|
API_EXEC_URL : function(sender, params) {
|
|
var sel = this.sel || this.oApp.getSelection(), url = params[0], newWin = params[1], newUrl;
|
|
|
|
if (!sel) return;
|
|
|
|
if (sel.collapsed && url) {
|
|
|
|
} else {
|
|
sel.select();
|
|
this.cast('EXEC_COMMAND', [url?'createlink':'unlink', false, (newWin&&url?'xeed://':'')+url]);
|
|
|
|
if (newWin && url) {
|
|
this.oApp.$richedit.find('a[href^=xeed://]').attr('href', url).attr('target', '_blank');
|
|
}
|
|
}
|
|
this.cast('HIDE_URL_LAYER');
|
|
},
|
|
API_SHOW_URL_LAYER : function() {
|
|
var sel, $node;
|
|
|
|
if (!this.$layer || this.$layer.hasClass('open')) return;
|
|
if (!(sel = this.oApp.getSelection())) return;
|
|
|
|
this.sel = sel; // save selection
|
|
this.$btn.parent().addClass('active');
|
|
this.$layer.addClass('open');
|
|
|
|
$node = $(sel.commonAncestorContainer);
|
|
if (!$node.is('a')) $node = $node.parentsUntil('.'+_xr_).filter('a');
|
|
if ($node.length) {
|
|
this.$text.val( $node.attr('href') );
|
|
if ($node.attr('target') == '_blank') this.$chk[0].checked = true;
|
|
} else {
|
|
this.$text.val('http://');
|
|
this.$chk[0].checked = false;
|
|
}
|
|
|
|
this.$text.focus().select();
|
|
},
|
|
API_HIDE_URL_LAYER : function() {
|
|
if (!this.$layer || !this.$layer.hasClass('open')) return;
|
|
|
|
this.$btn.parent().removeClass('active');
|
|
this.$layer.removeClass('open');
|
|
},
|
|
API_TOGGLE_URL_LAYER : function() {
|
|
if (!this.$layer) return;
|
|
|
|
this.cast( (this.$layer.hasClass('open')?'HIDE':'SHOW')+'_URL_LAYER' );
|
|
},
|
|
API_HIDE_ALL_LAYER : function() {
|
|
if (this.$layer && this.$layer.hasClass('open')) this.cast('HIDE_URL_LAYER');
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ Table
|
|
*/
|
|
Table = xe.createPlugin('Table', {
|
|
$btns : null,
|
|
$layer : null,
|
|
$table : null, // preview table
|
|
$th_btns : null,
|
|
$unit_btns : null,
|
|
selection : null,
|
|
|
|
selector : '.xeed_selected_cell',
|
|
cell_selector : '',
|
|
cmd : {
|
|
cm : 'MERGE_CELLS',
|
|
cs : 'SPLIT_CELLS',
|
|
rs : 'SPLIT_ROWS'
|
|
},
|
|
|
|
init : function(){
|
|
this.$btns = {};
|
|
this.cell_selector = 'td'+this.selector+',th'+this.selector;
|
|
},
|
|
activate : function() {
|
|
var self = this, $tb = this.oApp.$toolbar, $layer, $fieldset;
|
|
|
|
if (!$tb) return;
|
|
|
|
// tool buttons
|
|
this.$btns.te = $tb.find('button.te').mousedown(function(){ self.cast('TOGGLE_TABLE_LAYER'); return false; });
|
|
$.each(this.cmd, function(key) {
|
|
var $btn = $tb.find('button.'+key);
|
|
|
|
self.$btns[key] = $btn;
|
|
self.cast('REGISTER_COMMAND', [$btn[0], '', this]);
|
|
});
|
|
|
|
// layer, preview table
|
|
this.$layer = $layer = this.$btns.te.next('.lr').mousedown(function(event){ event.stopPropagation() });
|
|
this.$table = $layer.find('fieldset.pv table:first');
|
|
|
|
// caption setting
|
|
$fieldset = $layer.find('fieldset.cn');
|
|
this.$caption_txt = $fieldset.find('input:text')
|
|
.keydown(function(){ self.$table.find('>caption').text( this.value ); })
|
|
.focus(function(){ $(this).prev('label').hide(); })
|
|
.blur(function(){
|
|
if(!this.value) $(this).prev('label').show();
|
|
$(this).keydown();
|
|
});
|
|
this.$caption_pos = $fieldset.find('button')
|
|
.click(function(){
|
|
var $li = $(this[_pn_]), pos, align, $table, $caption;
|
|
|
|
$table = self.$table;
|
|
$caption = $table.find('>caption');
|
|
|
|
$li.parent().children('li').removeClass('selected').end().end().addClass('selected');
|
|
|
|
pos = self.parseCaptionPos($li.attr('class'));
|
|
|
|
(pos.vert == 'top')?$table.prepend($caption):$table.append($caption);
|
|
$table.css('caption-side', pos.vert);
|
|
$caption.css('text-align', pos.align);
|
|
|
|
return false;
|
|
});
|
|
|
|
// header setting
|
|
this.$th_btns = $layer.find('fieldset.th button').click(function(){ self.setHeader(this); return false; });
|
|
|
|
// px or percent
|
|
this.$unit_btns = $layer.find('fieldset.wh button').click(function(){
|
|
self.$unit_btns.removeClass('selected');
|
|
$(this).addClass('selected');
|
|
return false;
|
|
});
|
|
|
|
// apply, cancel
|
|
$btns = $layer.find('div.btnArea button');
|
|
$btns.eq(0).click(function(){
|
|
var cfg = {rows:3, cols:3, width:'100%', caption_text:'', caption_pos:'tc', header:'no'}, $fieldset, $input;
|
|
|
|
// get table properties
|
|
$fieldset = $layer.find('fieldset.cn');
|
|
cfg.caption_pos = $fieldset.find('li.selected').attr('class').replace(/ ?selected ?/, '');
|
|
cfg.caption_text = $fieldset.find('input:text').val();
|
|
|
|
cfg.header = $layer.find('fieldset.th li.selected').attr('class').replace(/ ?selected ?/, '');
|
|
|
|
$fieldset = $layer.find('fieldset.wh');
|
|
$input = $fieldset.find('input:text');
|
|
cfg.cols = $input.eq(0).val() - 0;
|
|
cfg.rows = $input.eq(1).val() - 0;
|
|
cfg.width = $input.eq(2).val() - 0 + $fieldset.find('button.selected').text();
|
|
|
|
self.cast('EXEC_TABLE', [cfg]);
|
|
self.cast('HIDE_TABLE_LAYER');
|
|
|
|
return false;
|
|
});
|
|
$btns.eq(1).click(function(){ self.cast('HIDE_TABLE_LAYER'); return false; });
|
|
},
|
|
deactivate : function() {
|
|
var self = this;
|
|
|
|
// buttons
|
|
if(this.$btns.te) this.$btns.te.unbind('mousedown');
|
|
$.each(this.$btns, function(key) {
|
|
self.cast('UNREGISTER_COMMAND', [this]);
|
|
});
|
|
this.$btns = {};
|
|
|
|
if (this.$layer) this.$layer.unbind().find('input,button').unbind();
|
|
|
|
},
|
|
parseCaptionPos : function(str) {
|
|
var ret = {vert:'', align:''};
|
|
|
|
str = $.trim(str.replace(/selected/g, ''));
|
|
|
|
ret.vert = (str.indexOf('b') < 0)?'top':'bottom';
|
|
|
|
if (str.indexOf('c') >= 0) ret.align = 'center';
|
|
else if (str.indexOf('r') >= 0) ret.align = 'right';
|
|
else ret.align = 'left';
|
|
|
|
return ret;
|
|
},
|
|
setHeader : function(btn) {
|
|
var $btn = $(btn), $li = $btn.parent('li'), $table = this.$table, selector;
|
|
|
|
if (!$btn.is('button') || !$li.length) return;
|
|
|
|
switch($li.attr('class')) {
|
|
case 'no': selector = ''; break;
|
|
case 'lt': selector = 'tr > td:nth-child(1)'; break;
|
|
case 'tp': selector = 'tr:first td'; break;
|
|
case 'bh': selector = 'tr:first td, tr > td:nth-child(1)'; break;
|
|
}
|
|
|
|
// conver td to th
|
|
$table.find('th').replaceWith('<td>TD</td>');
|
|
if (selector) $table.find(selector).replaceWith('<th>TH</th>');
|
|
|
|
// select this button
|
|
this.$th_btns.parent('li').removeClass('selected');
|
|
$li.addClass('selected');
|
|
},
|
|
/**
|
|
* @brief Insert a table with a configuration
|
|
* @param Object contains table properties
|
|
*/
|
|
API_EXEC_TABLE : function(sender, params) {
|
|
var sel = this.selection, cfg, html, caption, $rich = this.oApp.$richedit;
|
|
|
|
if (!sel) return;
|
|
|
|
cfg = $.extend({
|
|
caption_pos : 'tc',
|
|
caption_text : '',
|
|
cols : 3,
|
|
rows : 3,
|
|
header : 'no',
|
|
width : '100%'
|
|
}, params[0]);
|
|
|
|
cfg.cp_side = (cfg.caption_pos.indexOf('t') > -1)?'top':'bottom';
|
|
|
|
if (cfg.caption_pos.indexOf('c') > -1) cfg.cp_align = 'center';
|
|
else if (cfg.caption_pos.indexOf('r') > -1) cfg.cp_align = 'right';
|
|
else cfg.cp_align = 'left';
|
|
|
|
if (cfg.caption_text) {
|
|
caption = '<caption style="text-align:'+cfg.cp_align+'">'+cfg.caption_text+'</caption>';
|
|
} else {
|
|
caption = '';
|
|
}
|
|
|
|
// create table code
|
|
html = '<table border="1" cellpadding="1" cellspacing="0" class="ts" style="width:'+cfg.width+';caption-side:'+cfg.cp_side+';">';
|
|
// top caption
|
|
if (cfg.cp_side == 'top') html += caption;
|
|
|
|
for(i=0; i < cfg.rows; i++) {
|
|
|
|
html += '<tr>';
|
|
for(j=0; j < cfg.cols; j++) {
|
|
if ( (cfg.header == 'bh' && !(i*j)) || (cfg.header == 'tp' && !i) || (cfg.header == 'lt' && !j)) {
|
|
html += '<th scope="'+(i?'row':'col')+'">TH</th>';
|
|
} else {
|
|
html += '<td>TD</td>';
|
|
}
|
|
}
|
|
html += '</tr>';
|
|
}
|
|
|
|
// bottom caption
|
|
if (cfg.cp_side == 'bottom') html += caption;
|
|
|
|
html += '</table>';
|
|
|
|
// insert table
|
|
$rich.focus();
|
|
sel.pasteHTML(html);
|
|
sel.collapseToEnd();
|
|
},
|
|
API_SHOW_TABLE_LAYER : function(sender, params) {
|
|
var $layer = this.$layer;
|
|
|
|
if (!$layer || $layer.hasClass('open')) return;
|
|
|
|
this.selection = this.oApp.getSelection();
|
|
$layer.addClass('open').parent('li').addClass('active');
|
|
},
|
|
API_HIDE_TABLE_LAYER : function(sender, params) {
|
|
var $layer = this.$layer;
|
|
|
|
if (!$layer || !$layer.hasClass('open')) return;
|
|
if (this.selection) this.selection.select();
|
|
|
|
this.selection = null;
|
|
$layer.removeClass('open').parent('li').removeClass('active');
|
|
},
|
|
API_TOGGLE_TABLE_LAYER : function(sender, params) {
|
|
if (!this.$layer) return;
|
|
this.cast( (this.$layer.hasClass('open')?'HIDE':'SHOW') + '_TABLE_LAYER' );
|
|
},
|
|
API_HIDE_ALL_LAYER : function(sender, params) {
|
|
if (sender != this) this.cast('HIDE_TABLE_LAYER');
|
|
},
|
|
API_MERGE_CELLS : function(sender, params) {
|
|
var self = this, html = '', $cell = this.oApp.$richedit.find(this.cell_selector);
|
|
|
|
// if no selected cell then quit
|
|
if (!cell.length) return;
|
|
|
|
// merge content of all cells
|
|
$cell.each(function(){ html += this.innerHTML; }).eq(0).html(html);
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
},
|
|
API_SPLIT_BY_COL : function(sender, params) {
|
|
var cell = this.oApp.$richedit.find(this.cell_selector);
|
|
|
|
if (!$cell.length) $cell = this.$current_cell;
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
},
|
|
API_SPLIT_BY_ROW : function(sender, params) {
|
|
var $cell = this.oApp.$richedit.find(this.cell_selector);
|
|
|
|
if (!$cell.length) $cell = this.$current_cell;
|
|
|
|
// save undo point
|
|
this.cast('SAVE_UNDO_POINT');
|
|
}/*,
|
|
API_ON_CHANGE_NODE : function(sender, params) {
|
|
var self = this, node = params[0], $node, state;
|
|
|
|
state = {
|
|
cs : 'disable',
|
|
rs : 'disable',
|
|
cm : 'disable'
|
|
};
|
|
|
|
if (node) {
|
|
sel = this.oApp.getSelection();
|
|
|
|
if (sel.collapsed) {
|
|
$node = $(node).parentsUntil('.'+_xr_).filter('td,th');
|
|
|
|
if ($node.length) {
|
|
state.cs = state.rs = 'normal';
|
|
if ($node.is('.selected')) state.cm = 'normal';
|
|
}
|
|
}
|
|
}
|
|
|
|
$.each(this.$btns, function(key){
|
|
self.cast('SET_COMMAND_STATE', [this[0], state[key]]);
|
|
});
|
|
}
|
|
*/
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ AutoSave
|
|
*/
|
|
AutoSave = xe.createPlugin('AutoSave', {
|
|
_enable : false,
|
|
$bar : null,
|
|
$text : null,
|
|
|
|
init : function(){ },
|
|
activate : function() {
|
|
var app = this.oApp;
|
|
|
|
// set default option
|
|
app.setDefault('use_autosave', false);
|
|
this._enable = app.getOption('use_autosave');
|
|
|
|
this.$bar = this.oApp.$root.find('div.time').hide();
|
|
},
|
|
deactivate : function() {
|
|
},
|
|
API_EXEC_AUTOSAVE : function() {
|
|
},
|
|
API_ENABLE_AUTOSAVE : function(sender, params) {
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ Clear
|
|
*/
|
|
Clear = xe.createPlugin('Clear', {
|
|
$btn : null,
|
|
_keypress : null,
|
|
_mousemove : null,
|
|
init : function(){ },
|
|
activate : function() {
|
|
var self = this, app = this.oApp;
|
|
|
|
this.$btn = this.oApp.$toolbar.find('button.er');
|
|
if (this.$btn.length) {
|
|
this.cast('REGISTER_COMMAND', [this.$btn[0], '', 'EXEC_CLEAR']);
|
|
}
|
|
},
|
|
deactivate : function() {
|
|
if (this.$btn.length) {
|
|
this.cast('UNREGISTER_COMMAND', [this.$btn[0], '']);
|
|
}
|
|
},
|
|
API_EXEC_CLEAR : function() {
|
|
var sel = this.oApp.getSelection(), $div, node, par, content, after;
|
|
|
|
if (!sel || sel.collapsed) return;
|
|
|
|
// get content
|
|
content = sel.extractContents();
|
|
sel.select();
|
|
|
|
// TODO : get nearest parent
|
|
node = sel.startContainer;
|
|
par = null;
|
|
while(par = node[_pn_]) {
|
|
if (rx_block.test(par.nodeName) || rx_root.test(par.className)) {
|
|
break;
|
|
}
|
|
|
|
node = par;
|
|
}
|
|
|
|
$div = $('<div />').appendTo(document).append(content);
|
|
content = $div.remove().text();
|
|
|
|
sel.setEndAfter(par.lastChild);
|
|
after = sel.extractContents();
|
|
|
|
content = document.createTextNode(content);
|
|
par.appendChild(content);
|
|
par.appendChild(after);
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ Find
|
|
*/
|
|
FindReplace = xe.createPlugin('FindReplace', {
|
|
init : function() {
|
|
},
|
|
activate : function() {
|
|
},
|
|
deactivate : function() {
|
|
},
|
|
API_EXEC_FIND : function() {
|
|
},
|
|
API_SHOW_FINDREPLACE_LAYER : function() {
|
|
},
|
|
API_SHOW_FINDREPLACE_LAYER : function() {
|
|
},
|
|
API_SHOW_FINDREPLACE_LAYER : function() {
|
|
},
|
|
API_HIDE_ALL_LAYER : function() {
|
|
}
|
|
});
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ XHTML Transitional scheme
|
|
* This code is from the tinyMCE project.
|
|
*/
|
|
var XHTMLT = {};
|
|
(function(){
|
|
function unpack(lookup, data) {
|
|
function replace(value) {
|
|
return value.replace(/[A-Z]+/g, function(key) {
|
|
return replace(lookup[key]);
|
|
});
|
|
};
|
|
|
|
// Unpack lookup
|
|
$.each(lookup, function(key){ lookup[key] = replace(this) });
|
|
|
|
// Unpack and parse data into object map
|
|
replace(data).replace(/#/g, '#text').replace(/(\w+)\[([^\]]+)\]/g, function(str, name, children) {
|
|
var i, map = {};
|
|
|
|
children = children.split(/\|/);
|
|
|
|
for (i = children.length - 1; i >= 0; i--)
|
|
map[children[i]] = 1;
|
|
|
|
XHTMLT[name] = map;
|
|
});
|
|
};
|
|
|
|
// This is the XHTML 1.0 transitional elements with it's children packed to reduce it's size
|
|
// we will later include the attributes here and use it as a default for valid elements but it
|
|
// requires us to rewrite the serializer engine
|
|
unpack({
|
|
Z : '#|H|K|N|O|P',
|
|
Y : '#|X|form|R|Q',
|
|
X : 'p|T|div|U|W|isindex|fieldset|table',
|
|
W : 'pre|hr|blockquote|address|center|noframes',
|
|
U : 'ul|ol|dl|menu|dir',
|
|
ZC : '#|p|Y|div|U|W|table|br|span|bdo|object|applet|img|map|K|N|Q',
|
|
T : 'h1|h2|h3|h4|h5|h6',
|
|
ZB : '#|X|S|Q',
|
|
S : 'R|P',
|
|
ZA : '#|a|G|J|M|O|P',
|
|
R : '#|a|H|K|N|O',
|
|
Q : 'noscript|P',
|
|
P : 'ins|del|script',
|
|
O : 'input|select|textarea|label|button',
|
|
N : 'M|L',
|
|
M : 'em|strong|dfn|code|q|samp|kbd|var|cite|abbr|acronym',
|
|
L : 'sub|sup',
|
|
K : 'J|I',
|
|
J : 'tt|i|b|u|s|strike',
|
|
I : 'big|small|font|basefont',
|
|
H : 'G|F',
|
|
G : 'br|span|bdo',
|
|
F : 'object|applet|img|map|iframe'
|
|
}, 'script[]' +
|
|
'style[]' +
|
|
'object[#|param|X|form|a|H|K|N|O|Q]' +
|
|
'param[]' +
|
|
'p[S]' +
|
|
'a[Z]' +
|
|
'br[]' +
|
|
'span[S]' +
|
|
'bdo[S]' +
|
|
'applet[#|param|X|form|a|H|K|N|O|Q]' +
|
|
'h1[S]' +
|
|
'img[]' +
|
|
'map[X|form|Q|area]' +
|
|
'h2[S]' +
|
|
'iframe[#|X|form|a|H|K|N|O|Q]' +
|
|
'h3[S]' +
|
|
'tt[S]' +
|
|
'i[S]' +
|
|
'b[S]' +
|
|
'u[S]' +
|
|
's[S]' +
|
|
'strike[S]' +
|
|
'big[S]' +
|
|
'small[S]' +
|
|
'font[S]' +
|
|
'basefont[]' +
|
|
'em[S]' +
|
|
'strong[S]' +
|
|
'dfn[S]' +
|
|
'code[S]' +
|
|
'q[S]' +
|
|
'samp[S]' +
|
|
'kbd[S]' +
|
|
'var[S]' +
|
|
'cite[S]' +
|
|
'abbr[S]' +
|
|
'acronym[S]' +
|
|
'sub[S]' +
|
|
'sup[S]' +
|
|
'input[]' +
|
|
'select[optgroup|option]' +
|
|
'optgroup[option]' +
|
|
'option[]' +
|
|
'textarea[]' +
|
|
'label[S]' +
|
|
'button[#|p|T|div|U|W|table|G|object|applet|img|map|K|N|Q]' +
|
|
'h4[S]' +
|
|
'ins[#|X|form|a|H|K|N|O|Q]' +
|
|
'h5[S]' +
|
|
'del[#|X|form|a|H|K|N|O|Q]' +
|
|
'h6[S]' +
|
|
'div[#|X|form|a|H|K|N|O|Q]' +
|
|
'ul[li]' +
|
|
'li[#|X|form|a|H|K|N|O|Q]' +
|
|
'ol[li]' +
|
|
'dl[dt|dd]' +
|
|
'dt[S]' +
|
|
'dd[#|X|form|a|H|K|N|O|Q]' +
|
|
'menu[li]' +
|
|
'dir[li]' +
|
|
'pre[ZA]' +
|
|
'hr[]' +
|
|
'blockquote[#|X|form|a|H|K|N|O|Q]' +
|
|
'address[S|p]' +
|
|
'center[#|X|form|a|H|K|N|O|Q]' +
|
|
'noframes[#|X|form|a|H|K|N|O|Q]' +
|
|
'isindex[]' +
|
|
'fieldset[#|legend|X|form|a|H|K|N|O|Q]' +
|
|
'legend[S]' +
|
|
'table[caption|col|colgroup|thead|tfoot|tbody|tr]' +
|
|
'caption[S]' +
|
|
'col[]' +
|
|
'colgroup[col]' +
|
|
'thead[tr]' +
|
|
'tr[th|td]' +
|
|
'th[#|X|form|a|H|K|N|O|Q]' +
|
|
'form[#|X|a|H|K|N|O|Q]' +
|
|
'noscript[#|X|form|a|H|K|N|O|Q]' +
|
|
'td[#|X|form|a|H|K|N|O|Q]' +
|
|
'tfoot[tr]' +
|
|
'tbody[tr]' +
|
|
'area[]' +
|
|
'base[]' +
|
|
'body[#|X|form|a|H|K|N|O|Q]'
|
|
);
|
|
})();
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
/**
|
|
* {{{ Selection and Range Control class
|
|
* This code is a modified version of HuskyRange, the SmartEditor project.
|
|
*/
|
|
|
|
/**
|
|
* {{{ @class W3CDOMRange
|
|
* @brief A cross-browser implementation of W3C's DOM Range
|
|
*/
|
|
function W3CDOMRange(){ this.init(); };
|
|
$.extend(W3CDOMRange.prototype, {
|
|
init : function() {
|
|
this.collapsed = true;
|
|
this[_ca_] = d.body;
|
|
this[_ec_] = d.body;
|
|
this[_eo_] = 0;
|
|
this[_sc_] = d.body;
|
|
this[_so_] = 0;
|
|
},
|
|
cloneContents : function(){
|
|
var oClonedContents = d.createDocumentFragment();
|
|
var oTmpContainer = d.createDocumentFragment();
|
|
|
|
var aNodes = this._getNodesInRange();
|
|
|
|
if(aNodes.length < 1) return oClonedContents;
|
|
|
|
var oClonedContainers = this._constructClonedTree(aNodes, oTmpContainer);
|
|
|
|
// oTopContainer = aNodes[aNodes.length-1].parentNode and this is not part of the initial array and only those child nodes should be cloned
|
|
var oTopContainer = oTmpContainer.firstChild;
|
|
|
|
if(oTopContainer){
|
|
var elCurNode = oTopContainer.firstChild, elNextNode;
|
|
|
|
while(elCurNode){
|
|
elNextNode = elCurNode[_ns_];
|
|
oClonedContents.appendChild(elCurNode);
|
|
elCurNode = elNextNode;
|
|
}
|
|
}
|
|
|
|
oClonedContainers = this._splitTextEndNodes({oStartContainer: oClonedContainers[_osc_], iStartOffset: this[_so_],
|
|
oEndContainer: oClonedContainers[_oec_], iEndOffset: this[_eo_]});
|
|
|
|
if(oClonedContainers[_osc_] && oClonedContainers[_osc_][_ps_])
|
|
dp(oClonedContainers[_osc_]).removeChild(oClonedContainers[_osc_][_ps_]);
|
|
|
|
if(oClonedContainers[_oec_] && oClonedContainers[_oec_][_ns_])
|
|
dp(oClonedContainers[_oec_]).removeChild(oClonedContainers[_oec_][_ns_]);
|
|
|
|
return oClonedContents;
|
|
},
|
|
|
|
_constructClonedTree : function(aNodes, oClonedParentNode){
|
|
var oClonedStartContainer = null;
|
|
var oClonedEndContainer = null;
|
|
|
|
var oStartContainer = this[_sc_];
|
|
var oEndContainer = this[_ec_];
|
|
|
|
_recurConstructClonedTree = function(aAllNodes, iCurIdx, oParentNode, oClonedParentNode){
|
|
|
|
if(iCurIdx < 0) return iCurIdx;
|
|
|
|
var iChildIdx = iCurIdx-1;
|
|
|
|
var oCurNodeCloneWithChildren = aAllNodes[iCurIdx].cloneNode(false);
|
|
|
|
if(aAllNodes[iCurIdx] == oStartContainer) oClonedStartContainer = oCurNodeCloneWithChildren;
|
|
if(aAllNodes[iCurIdx] == oEndContainer) oClonedEndContainer = oCurNodeCloneWithChildren;
|
|
|
|
while(iChildIdx >= 0 && dp(aAllNodes[iChildIdx]) == aAllNodes[iCurIdx]){
|
|
iChildIdx = this._recurConstructClonedTree(aAllNodes, iChildIdx, aAllNodes[iCurIdx], oCurNodeCloneWithChildren, oClonedStartContainer, oClonedEndContainer);
|
|
}
|
|
|
|
// this may trigger an error message in IE when an erroneous script is inserted
|
|
oClonedParentNode.insertBefore(oCurNodeCloneWithChildren, oClonedParentNode.firstChild);
|
|
|
|
return iChildIdx;
|
|
};
|
|
|
|
aNodes[aNodes.length] = dp(aNodes[aNodes.length-1]);
|
|
_recurConstructClonedTree(aNodes, aNodes.length-1, aNodes[aNodes.length-1], oClonedParentNode);
|
|
|
|
return {oStartContainer: oClonedStartContainer, oEndContainer: oClonedEndContainer};
|
|
},
|
|
|
|
cloneRange : function(){
|
|
return this._copyRange(new W3CDOMRange(d));
|
|
},
|
|
|
|
_copyRange : function(oClonedRange){
|
|
oClonedRange.collapsed = this.collapsed;
|
|
oClonedRange[_ca_] = this[_ca_];
|
|
oClonedRange[_ec_] = this[_ec_];
|
|
oClonedRange[_eo_] = this[_eo_];
|
|
oClonedRange[_sc_] = this[_sc_];
|
|
oClonedRange[_so_] = this[_so_];
|
|
oClonedRange._document = d;
|
|
|
|
return oClonedRange;
|
|
},
|
|
|
|
collapse : function(toStart){
|
|
if(toStart){
|
|
this[_ec_] = this[_sc_];
|
|
this[_eo_] = this[_so_];
|
|
}else{
|
|
this[_sc_] = this[_ec_];
|
|
this[_so_] = this[_eo_];
|
|
}
|
|
|
|
this._updateRangeInfo();
|
|
},
|
|
|
|
compareBoundaryPoints : function(how, sourceRange){
|
|
switch(how){
|
|
case W3CDOMRange.START_TO_START:
|
|
return this._compareEndPoint(this[_sc_], this[_so_], sourceRange[_sc_], sourceRange[_so_]);
|
|
case W3CDOMRange.START_TO_END:
|
|
return this._compareEndPoint(this[_ec_], this[_eo_], sourceRange[_sc_], sourceRange[_so_]);
|
|
case W3CDOMRange.END_TO_END:
|
|
return this._compareEndPoint(this[_ec_], this[_eo_], sourceRange[_ec_], sourceRange[_eo_]);
|
|
case W3CDOMRange.END_TO_START:
|
|
return this._compareEndPoint(this[_sc_], this[_so_], sourceRange[_ec_], sourceRange[_eo_]);
|
|
}
|
|
},
|
|
|
|
_findBody : function(oNode){
|
|
if(!oNode) return null;
|
|
while(oNode){
|
|
if(oNode[_nn_].toUpperCase() == "BODY") return oNode;
|
|
oNode = dp(oNode);
|
|
}
|
|
return null;
|
|
},
|
|
|
|
_compareEndPoint : function(oContainerA, iOffsetA, oContainerB, iOffsetB){
|
|
var iIdxA, iIdxB;
|
|
|
|
if(!oContainerA || this._findBody(oContainerA) != d.body){
|
|
oContainerA = d.body;
|
|
iOffsetA = 0;
|
|
}
|
|
|
|
if(!oContainerB || this._findBody(oContainerB) != d.body){
|
|
oContainerB = d.body;
|
|
iOffsetB = 0;
|
|
}
|
|
|
|
var compareIdx = function(iIdxA, iIdxB){
|
|
// iIdxX == -1 when the node is the commonAncestorNode
|
|
// if iIdxA == -1
|
|
// -> [[<nodeA>...<nodeB></nodeB>]]...</nodeA>
|
|
// if iIdxB == -1
|
|
// -> <nodeB>...[[<nodeA></nodeA>...</nodeB>]]
|
|
if(iIdxB == -1) iIdxB = iIdxA+1;
|
|
if(iIdxA < iIdxB) return -1;
|
|
if(iIdxA == iIdxB) return 0;
|
|
return 1;
|
|
};
|
|
|
|
var oCommonAncestor = this._getCommonAncestorContainer(oContainerA, oContainerB);
|
|
|
|
// ================================================================================================================================================
|
|
// Move up both containers so that both containers are direct child nodes of the common ancestor node. From there, just compare the offset
|
|
// Add 0.5 for each contaienrs that has "moved up" since the actual node is wrapped by 1 or more parent nodes and therefore its position is somewhere between idx & idx+1
|
|
// <COMMON_ANCESTOR>NODE1<P>NODE2</P>NODE3</COMMON_ANCESTOR>
|
|
// The position of NODE2 in COMMON_ANCESTOR is somewhere between after NODE1(idx1) and before NODE3(idx2), so we let that be 1.5
|
|
|
|
// container node A in common ancestor container
|
|
var oNodeA = oContainerA;
|
|
if(oNodeA != oCommonAncestor){
|
|
while((oTmpNode = dp(oNodeA)) != oCommonAncestor){oNodeA = oTmpNode;}
|
|
|
|
iIdxA = this._getPosIdx(oNodeA)+0.5;
|
|
}else iIdxA = iOffsetA;
|
|
|
|
// container node B in common ancestor container
|
|
var oNodeB = oContainerB;
|
|
if(oNodeB != oCommonAncestor){
|
|
while((oTmpNode = dp(oNodeB)) != oCommonAncestor){oNodeB = oTmpNode;}
|
|
|
|
iIdxB = this._getPosIdx(oNodeB)+0.5;
|
|
}else iIdxB = iOffsetB;
|
|
|
|
return compareIdx(iIdxA, iIdxB);
|
|
},
|
|
|
|
_getCommonAncestorContainer : function(oNode1, oNode2){
|
|
var oComparingNode = oNode2;
|
|
|
|
while(oNode1){
|
|
while(oComparingNode){
|
|
if(oNode1 == oComparingNode) return oNode1;
|
|
oComparingNode = dp(oComparingNode);
|
|
}
|
|
oComparingNode = oNode2;
|
|
oNode1 = dp(oNode1);
|
|
}
|
|
|
|
return d.body;
|
|
},
|
|
|
|
deleteContents : function(){
|
|
if(this.collapsed) return;
|
|
|
|
this._splitTextEndNodesOfTheRange();
|
|
|
|
var aNodes = this._getNodesInRange();
|
|
|
|
if(aNodes.length < 1) return;
|
|
|
|
var oPrevNode = aNodes[0][_ps_];
|
|
while(oPrevNode && this._isBlankTextNode(oPrevNode)) oPrevNode = oPrevNode[_ps_];
|
|
|
|
var oNewStartContainer, iNewOffset;
|
|
if(!oPrevNode){
|
|
oNewStartContainer = dp(aNodes[0]);
|
|
iNewOffset = 0;
|
|
}
|
|
|
|
for(var i=0; i<aNodes.length; i++){
|
|
var oNode = aNodes[i];
|
|
if(!oNode.firstChild){
|
|
if(oNewStartContainer == oNode){
|
|
iNewOffset = this._getPosIdx(oNewStartContainer);
|
|
oNewStartContainer = dp(oNode);
|
|
}
|
|
dp(oNode).removeChild(oNode);
|
|
}
|
|
}
|
|
|
|
if(!oPrevNode){
|
|
this.setStart(oNewStartContainer, iNewOffset);
|
|
}else{
|
|
if(oPrevNode.tagName == "BODY")
|
|
this.setStartBefore(oPrevNode);
|
|
else
|
|
this.setStartAfter(oPrevNode);
|
|
}
|
|
|
|
this.collapse(true);
|
|
},
|
|
|
|
extractContents : function(){
|
|
var oClonedContents = this.cloneContents();
|
|
this.deleteContents();
|
|
return oClonedContents;
|
|
},
|
|
|
|
insertNode : function(newNode){
|
|
var oFirstNode = null, oParentContainer;
|
|
|
|
if(this[_sc_][_nt_] == 3) {
|
|
oParentContainer = dp(this[_sc_]);
|
|
|
|
// Fix Opera bug : How can a text node is a child node of the other text node?
|
|
while(oParentContainer[_nt_] == 3) oParentContainer = oParentContainer[_pn_];
|
|
|
|
if(this[_sc_].nodeValue.length <= this[_so_])
|
|
oFirstNode = this[_sc_][_ns_];
|
|
else
|
|
oFirstNode = this[_sc_].splitText(this[_so_]);
|
|
}else{
|
|
oParentContainer = this[_sc_];
|
|
oFirstNode = dc(this[_sc_])[this[_so_]];
|
|
}
|
|
|
|
if(!oFirstNode || !dp(oFirstNode)) oFirstNode = null;
|
|
|
|
oParentContainer.insertBefore(newNode, oFirstNode);
|
|
|
|
this.setStartBefore(newNode);
|
|
},
|
|
|
|
selectNode : function(refNode){
|
|
this.setStartBefore(refNode);
|
|
this.setEndAfter(refNode);
|
|
},
|
|
|
|
selectNodeContents : function(refNode){
|
|
this.setStart(refNode, 0);
|
|
this.setEnd(refNode, dc(refNode).length);
|
|
},
|
|
|
|
_endsNodeValidation : function(oNode, iOffset){
|
|
if(!oNode || this._findBody(oNode) != d.body) {
|
|
throw new Error("INVALID_NODE_TYPE_ERR oNode is not part of current document");
|
|
}
|
|
|
|
if(oNode[_nt_] == 3){
|
|
if(iOffset > oNode.nodeValue.length) iOffset = oNode.nodeValue.length;
|
|
}else{
|
|
if(iOffset > dc(oNode).length) iOffset = dc(oNode).length;
|
|
}
|
|
|
|
return iOffset;
|
|
},
|
|
|
|
|
|
setEnd : function(refNode, offset){
|
|
offset = this._endsNodeValidation(refNode, offset);
|
|
|
|
this[_ec_] = refNode;
|
|
this[_eo_] = offset;
|
|
if(!this[_sc_] || this._compareEndPoint(this[_sc_], this[_so_], this[_ec_], this[_eo_]) != -1) this.collapse(false);
|
|
|
|
this._updateRangeInfo();
|
|
},
|
|
|
|
setEndAfter : function(refNode){
|
|
if(!refNode) throw new Error("INVALID_NODE_TYPE_ERR in setEndAfter");
|
|
|
|
if(refNode.tagName == "BODY"){
|
|
this.setEnd(refNode, dc(refNode).length);
|
|
return;
|
|
}
|
|
this.setEnd(dp(refNode), this._getPosIdx(refNode)+1);
|
|
},
|
|
|
|
setEndBefore : function(refNode){
|
|
if(!refNode) throw new Error("INVALID_NODE_TYPE_ERR in setEndBefore");
|
|
|
|
if(refNode.tagName == "BODY"){
|
|
this.setEnd(refNode, 0);
|
|
return;
|
|
}
|
|
|
|
this.setEnd(dp(refNode), this._getPosIdx(refNode));
|
|
},
|
|
|
|
setStart : function(refNode, offset){
|
|
offset = this._endsNodeValidation(refNode, offset);
|
|
|
|
this[_sc_] = refNode;
|
|
this[_so_] = offset;
|
|
|
|
if(!this[_ec_] || this._compareEndPoint(this[_sc_], this[_so_], this[_ec_], this[_eo_]) != -1) this.collapse(true);
|
|
this._updateRangeInfo();
|
|
},
|
|
|
|
setStartAfter : function(refNode){
|
|
if(!refNode) throw new Error("INVALID_NODE_TYPE_ERR in setStartAfter");
|
|
|
|
if(refNode.tagName == "BODY"){
|
|
this.setStart(refNode, dc(refNode).length);
|
|
return;
|
|
}
|
|
|
|
this.setStart(dp(refNode), this._getPosIdx(refNode)+1);
|
|
},
|
|
|
|
setStartBefore : function(refNode){
|
|
if(!refNode) throw new Error("INVALID_NODE_TYPE_ERR in setStartBefore");
|
|
|
|
if(refNode.tagName == "BODY"){
|
|
this.setStart(refNode, 0);
|
|
return;
|
|
}
|
|
this.setStart(dp(refNode), this._getPosIdx(refNode));
|
|
},
|
|
|
|
surroundContents : function(newParent){
|
|
newParent.appendChild(this.extractContents());
|
|
this.insertNode(newParent);
|
|
this.selectNode(newParent);
|
|
},
|
|
|
|
toString : function(){
|
|
return $('<div />').append(this.cloneContents()).text();
|
|
},
|
|
|
|
_isBlankTextNode : function(oNode){
|
|
if(oNode[_nt_] == 3 && oNode.nodeValue == "") return true;
|
|
return false;
|
|
},
|
|
|
|
_getPosIdx : function(refNode){
|
|
var idx = 0;
|
|
for(var node = refNode[_ps_]; node; node = node[_ps_]) idx++;
|
|
|
|
return idx;
|
|
},
|
|
|
|
_updateRangeInfo : function(){
|
|
if(!this[_sc_]){
|
|
this.init();
|
|
return;
|
|
}
|
|
|
|
this.collapsed = this._isCollapsed(this[_sc_], this[_so_], this[_ec_], this[_eo_]);
|
|
|
|
this[_ca_] = this._getCommonAncestorContainer(this[_sc_], this[_ec_]);
|
|
},
|
|
|
|
_isCollapsed : function(oStartContainer, iStartOffset, oEndContainer, iEndOffset){
|
|
var bCollapsed = false;
|
|
|
|
if(oStartContainer == oEndContainer && iStartOffset == iEndOffset){
|
|
bCollapsed = true;
|
|
}else{
|
|
var oActualStartNode = this._getActualStartNode(oStartContainer, iStartOffset);
|
|
var oActualEndNode = this._getActualEndNode(oEndContainer, iEndOffset);
|
|
|
|
// Take the parent nodes on the same level for easier comparison when they're next to each other
|
|
// eg) From
|
|
// <A>
|
|
// <B>
|
|
// <C>
|
|
// </C>
|
|
// </B>
|
|
// <D>
|
|
// <E>
|
|
// <F>
|
|
// </F>
|
|
// </E>
|
|
// </D>
|
|
// </A>
|
|
// , it's easier to compare the position of B and D rather than C and F because they are siblings
|
|
//
|
|
// If the range were collapsed, oActualEndNode will precede oActualStartNode by doing this
|
|
oActualStartNode = this._getNextNode(this._getPrevNode(oActualStartNode));
|
|
oActualEndNode = this._getPrevNode(this._getNextNode(oActualEndNode));
|
|
|
|
if(oActualStartNode && oActualEndNode && oActualEndNode.tagName != "BODY" &&
|
|
(this._getNextNode(oActualEndNode) == oActualStartNode || (oActualEndNode == oActualStartNode && this._isBlankTextNode(oActualEndNode)))
|
|
)
|
|
bCollapsed = true;
|
|
}
|
|
|
|
return bCollapsed;
|
|
},
|
|
|
|
_splitTextEndNodesOfTheRange : function(){
|
|
var oEndPoints = this._splitTextEndNodes({oStartContainer: this[_sc_], iStartOffset: this[_so_],
|
|
oEndContainer: this[_ec_], iEndOffset: this[_eo_]});
|
|
|
|
this[_sc_] = oEndPoints[_osc_];
|
|
this[_so_] = oEndPoints[_iso_];
|
|
|
|
this[_ec_] = oEndPoints[_oec_];
|
|
this[_eo_] = oEndPoints[_ieo_];
|
|
},
|
|
|
|
_splitTextEndNodes : function(oEndPoints){
|
|
oEndPoints = this._splitStartTextNode(oEndPoints);
|
|
oEndPoints = this._splitEndTextNode(oEndPoints);
|
|
|
|
return oEndPoints;
|
|
},
|
|
|
|
_splitStartTextNode : function(oEndPoints){
|
|
var oStartContainer = oEndPoints[_osc_];
|
|
var iStartOffset = oEndPoints[_iso_];
|
|
|
|
var oEndContainer = oEndPoints[_oec_];
|
|
var iEndOffset = oEndPoints[_ieo_];
|
|
|
|
if(!oStartContainer) return oEndPoints;
|
|
if(oStartContainer[_nt_] != 3) return oEndPoints;
|
|
if(iStartOffset == 0) return oEndPoints;
|
|
|
|
if(oStartContainer.nodeValue.length <= iStartOffset) return oEndPoints;
|
|
|
|
var oLastPart = oStartContainer.splitText(iStartOffset);
|
|
|
|
if(oStartContainer == oEndContainer){
|
|
iEndOffset -= iStartOffset;
|
|
oEndContainer = oLastPart;
|
|
}
|
|
oStartContainer = oLastPart;
|
|
iStartOffset = 0;
|
|
|
|
return {oStartContainer: oStartContainer, iStartOffset: iStartOffset, oEndContainer: oEndContainer, iEndOffset: iEndOffset};
|
|
},
|
|
|
|
_splitEndTextNode : function(oEndPoints){
|
|
var oStartContainer = oEndPoints[_osc_];
|
|
var iStartOffset = oEndPoints[_iso_];
|
|
|
|
var oEndContainer = oEndPoints[_oec_];
|
|
var iEndOffset = oEndPoints[_ieo_];
|
|
|
|
if(!oEndContainer) return oEndPoints;
|
|
if(oEndContainer[_nt_] != 3) return oEndPoints;
|
|
|
|
if(iEndOffset >= oEndContainer.nodeValue.length) return oEndPoints;
|
|
if(iEndOffset == 0) return oEndPoints;
|
|
|
|
oEndContainer.splitText(iEndOffset);
|
|
|
|
return {oStartContainer: oStartContainer, iStartOffset: iStartOffset, oEndContainer: oEndContainer, iEndOffset: iEndOffset};
|
|
},
|
|
|
|
_getNodesInRange : function(){
|
|
if(this.collapsed) return [];
|
|
|
|
var oStartNode = this._getActualStartNode(this[_sc_], this[_so_]);
|
|
var oEndNode = this._getActualEndNode(this[_ec_], this[_eo_]);
|
|
|
|
return this._getNodesBetween(oStartNode, oEndNode);
|
|
},
|
|
|
|
_getActualStartNode : function(oStartContainer, iStartOffset){
|
|
var oStartNode = oStartContainer;;
|
|
|
|
if(oStartContainer[_nt_] == 3){
|
|
if(iStartOffset >= oStartContainer.nodeValue.length){
|
|
oStartNode = this._getNextNode(oStartContainer);
|
|
if(oStartNode.tagName == "BODY") oStartNode = null;
|
|
}else{
|
|
oStartNode = oStartContainer;
|
|
}
|
|
}else{
|
|
if(iStartOffset < dc(oStartContainer).length){
|
|
oStartNode = dc(oStartContainer)[iStartOffset];
|
|
}else{
|
|
oStartNode = this._getNextNode(oStartContainer);
|
|
if(oStartNode.tagName == "BODY") oStartNode = null;
|
|
}
|
|
}
|
|
|
|
return oStartNode;
|
|
},
|
|
|
|
_getActualEndNode : function(oEndContainer, iEndOffset){
|
|
var oEndNode = oEndContainer;
|
|
|
|
if(iEndOffset == 0){
|
|
oEndNode = this._getPrevNode(oEndContainer);
|
|
if(oEndNode.tagName == "BODY") oEndNode = null;
|
|
}else if(oEndContainer[_nt_] == 3){
|
|
oEndNode = oEndContainer;
|
|
}else{
|
|
oEndNode = dc(oEndContainer)[iEndOffset-1];
|
|
}
|
|
|
|
return oEndNode;
|
|
},
|
|
|
|
_getNextNode : function(oNode){
|
|
if(!oNode || oNode.tagName == "BODY") return d.body;
|
|
|
|
if(oNode[_ns_]) return oNode[_ns_];
|
|
|
|
return this._getNextNode(dp(oNode));
|
|
},
|
|
|
|
_getPrevNode : function(oNode){
|
|
if(!oNode || oNode.tagName == "BODY") return d.body;
|
|
|
|
if(oNode[_ps_]) return oNode[_ps_];
|
|
|
|
return this._getPrevNode(dp(oNode));
|
|
},
|
|
|
|
// includes partially selected
|
|
// for <div id="a"><div id="b"></div></div><div id="c"></div>, _getNodesBetween(b, c) will yield to b, "a" and c
|
|
_getNodesBetween : function(oStartNode, oEndNode){
|
|
var aNodesBetween = [];
|
|
this._nNodesBetweenLen = 0;
|
|
|
|
if(!oStartNode || !oEndNode) return aNodesBetween;
|
|
|
|
this._recurGetNextNodesUntil(oStartNode, oEndNode, aNodesBetween);
|
|
return aNodesBetween;
|
|
},
|
|
|
|
_recurGetNextNodesUntil : function(oNode, oEndNode, aNodesBetween){
|
|
if(!oNode) return false;
|
|
|
|
if(!this._recurGetChildNodesUntil(oNode, oEndNode, aNodesBetween)) return false;
|
|
|
|
var oNextToChk = oNode[_ns_];
|
|
|
|
while(!oNextToChk){
|
|
if(!(oNode = dp(oNode))) return false;
|
|
|
|
aNodesBetween[this._nNodesBetweenLen++] = oNode;
|
|
|
|
if(oNode == oEndNode) return false;
|
|
|
|
oNextToChk = oNode[_ns_];
|
|
}
|
|
|
|
return this._recurGetNextNodesUntil(oNextToChk, oEndNode, aNodesBetween);
|
|
},
|
|
|
|
_recurGetChildNodesUntil : function(oNode, oEndNode, aNodesBetween){
|
|
if(!oNode) return false;
|
|
|
|
var bEndFound = false;
|
|
var oCurNode = oNode;
|
|
if(oCurNode.firstChild){
|
|
oCurNode = oCurNode.firstChild;
|
|
while(oCurNode){
|
|
if(!this._recurGetChildNodesUntil(oCurNode, oEndNode, aNodesBetween)){
|
|
bEndFound = true;
|
|
break;
|
|
}
|
|
oCurNode = oCurNode[_ns_];
|
|
}
|
|
}
|
|
|
|
aNodesBetween[this._nNodesBetweenLen++] = oNode;
|
|
|
|
if(bEndFound) return false;
|
|
if(oNode == oEndNode) return false;
|
|
|
|
return true;
|
|
}
|
|
});
|
|
|
|
W3CDOMRange.START_TO_START = 0;
|
|
W3CDOMRange.START_TO_END = 1;
|
|
W3CDOMRange.END_TO_END = 2;
|
|
W3CDOMRange.END_TO_START = 3;
|
|
/**
|
|
* }}} W3CDOMRange
|
|
*/
|
|
|
|
/**
|
|
* {{{ @class HuskyRange
|
|
* @brief A cross-browser function that implements all of the W3C's DOM Range specification and some more
|
|
*/
|
|
var
|
|
HUSKY_BOOKMARK_START_ID_PREFIX = "husky_bookmark_start_",
|
|
HUSKY_BOOKMARK_END_ID_PREFIX = "husky_bookmark_end_",
|
|
rxLineBreaker = new RegExp("^("+this.sBlockElement+"|"+this.sBlockContainer+")$");
|
|
|
|
function HuskyRange(){ this.init(); }
|
|
$.extend(HuskyRange.prototype, W3CDOMRange.prototype, {
|
|
init : function(){
|
|
this.oSimpleSelection = new SimpleSelection();
|
|
this.selectionLoaded = this.oSimpleSelection.selectionLoaded;
|
|
|
|
W3CDOMRange.prototype.init.apply(this);
|
|
},
|
|
|
|
select : function(){
|
|
this.oSimpleSelection.selectRange(this);
|
|
},
|
|
|
|
setFromSelection : function(iNum){
|
|
this.setRange(this.oSimpleSelection.getRangeAt(iNum));
|
|
},
|
|
|
|
setRange : function(oW3CRange){
|
|
this.setStart(oW3CRange[_sc_], oW3CRange[_so_]);
|
|
this.setEnd(oW3CRange[_ec_], oW3CRange[_eo_]);
|
|
},
|
|
|
|
setEndNodes : function(oSNode, oENode){
|
|
this.setEndAfter(oENode);
|
|
this.setStartBefore(oSNode);
|
|
},
|
|
|
|
splitTextAtBothEnds : function(){
|
|
this._splitTextEndNodesOfTheRange();
|
|
},
|
|
|
|
getStartNode : function(){
|
|
if(this.collapsed){
|
|
if(this[_sc_][_nt_] == 3){
|
|
if(this[_so_] == 0) return this[_sc_];
|
|
if(this[_sc_].nodeValue.length <= this[_so_]) return null;
|
|
return this[_sc_];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if(this[_sc_][_nt_] == 3){
|
|
if(this[_so_] >= this[_sc_].nodeValue.length) return this._getNextNode(this[_sc_]);
|
|
return this[_sc_];
|
|
}else{
|
|
if(this[_so_] >= dc(this[_sc_]).length) return this._getNextNode(this[_sc_]);
|
|
return dc(this[_sc_])[this[_so_]];
|
|
}
|
|
},
|
|
|
|
getEndNode : function(){
|
|
if(this.collapsed) return this.getStartNode();
|
|
|
|
if(this[_ec_][_nt_] == 3){
|
|
if(this[_eo_] == 0) return this._getPrevNode(this[_ec_]);
|
|
return this[_ec_];
|
|
}else{
|
|
if(this[_eo_] == 0) return this._getPrevNode(this[_ec_]);
|
|
return dc(this[_ec_])[this[_eo_]-1];
|
|
}
|
|
},
|
|
|
|
getNodeAroundRange : function(bBefore, bStrict){
|
|
if(this.collapsed && this[_sc_] && this[_sc_][_nt_] == 3) return this[_sc_];
|
|
if(!this.collapsed || (this[_sc_] && this[_sc_][_nt_] == 3)) return this.getStartNode();
|
|
|
|
var oBeforeRange, oAfterRange, oResult;
|
|
|
|
if(this[_so_] >= dc(this[_sc_]).length)
|
|
oAfterRange = this._getNextNode(this[_sc_]);
|
|
else
|
|
oAfterRange = dc(this[_sc_])[this[_so_]];
|
|
|
|
if(this[_eo_] == 0)
|
|
oBeforeRange = this._getPrevNode(this[_ec_]);
|
|
else
|
|
oBeforeRange = dc(this[_ec_])[this[_eo_]-1];
|
|
|
|
if(bBefore){
|
|
oResult = oBeforeRange;
|
|
if(!oResult && !bStrict) oResult = oAfterRange;
|
|
}else{
|
|
oResult = oAfterRange;
|
|
if(!oResult && !bStrict) oResult = oBeforeRange;
|
|
}
|
|
|
|
return oResult;
|
|
},
|
|
|
|
_getXPath : function(elNode){
|
|
var sXPath = "";
|
|
|
|
while(elNode && elNode[_nt_] == 1){
|
|
sXPath = "/" + elNode.tagName+"["+this._getPosIdx4XPath(elNode)+"]" + sXPath;
|
|
elNode = dp(elNode);
|
|
}
|
|
|
|
return sXPath;
|
|
},
|
|
|
|
_getPosIdx4XPath : function(refNode){
|
|
var idx = 0;
|
|
for(var node = refNode[_ps_]; node; node = node[_ps_])
|
|
if(node.tagName == refNode.tagName) idx++;
|
|
|
|
return idx;
|
|
},
|
|
|
|
// this was written specifically for XPath Bookmark and it may not perform correctly for general purposes
|
|
_evaluateXPath : function(sXPath, oDoc){
|
|
sXPath = sXPath.substring(1, sXPath.length-1);
|
|
var aXPath = sXPath.split(/\//);
|
|
var elNode = oDoc.body;
|
|
|
|
for(var i=2; i<aXPath.length && elNode; i++){
|
|
aXPath[i].match(/([^\[]+)\[(\d+)/i);
|
|
var sTagName = RegExp.$1;
|
|
var nIdx = RegExp.$2;
|
|
|
|
var aAllNodes = dc(elNode);
|
|
var aNodes = [];
|
|
var nLength = aAllNodes.length;
|
|
var nCount = 0;
|
|
for(var ii=0; ii<nLength; ii++){
|
|
if(aAllNodes[ii].tagName == sTagName) aNodes[nCount++] = aAllNodes[ii];
|
|
}
|
|
|
|
if(aNodes.length < nIdx)
|
|
elNode = null;
|
|
else
|
|
elNode = aNodes[nIdx];
|
|
}
|
|
|
|
return elNode;
|
|
},
|
|
|
|
_evaluateXPathBookmark : function(oBookmark){
|
|
var sXPath = oBookmark["sXPath"];
|
|
var nTextNodeIdx = oBookmark["nTextNodeIdx"];
|
|
var nOffset = oBookmark["nOffset"];
|
|
|
|
var elContainer = this._evaluateXPath(sXPath, d);
|
|
|
|
if(nTextNodeIdx > -1 && elContainer){
|
|
var aChildNodes = dc(elContainer);
|
|
var elNode = null;
|
|
|
|
var nIdx = nTextNodeIdx;
|
|
var nOffsetLeft = nOffset;
|
|
|
|
while((elNode = aChildNodes[nIdx]) && elNode[_nt_] == 3 && elNode.nodeValue.length < nOffsetLeft){
|
|
nOffsetLeft -= elNode.nodeValue.length;
|
|
nIdx++;
|
|
}
|
|
|
|
elContainer = dc(elContainer)[nIdx];
|
|
nOffset = nOffsetLeft;
|
|
}
|
|
|
|
if(!elContainer){
|
|
elContainer = d.body;
|
|
nOffset = 0;
|
|
}
|
|
return {elContainer: elContainer, nOffset: nOffset};
|
|
},
|
|
|
|
// this was written specifically for XPath Bookmark and it may not perform correctly for general purposes
|
|
getXPathBookmark : function(){
|
|
var nTextNodeIdx1 = -1;
|
|
var htEndPt1 = {elContainer: this[_sc_], nOffset: this[_so_]};
|
|
var elNode1 = this[_sc_];
|
|
if(elNode1[_nt_] == 3){
|
|
htEndPt1 = this._getFixedStartTextNode();
|
|
nTextNodeIdx1 = this._getPosIdx(htEndPt1.elContainer);
|
|
elNode1 = dp(elNode1);
|
|
}
|
|
var sXPathNode1 = this._getXPath(elNode1);
|
|
var oBookmark1 = {sXPath:sXPathNode1, nTextNodeIdx:nTextNodeIdx1, nOffset: htEndPt1.nOffset};
|
|
|
|
var nTextNodeIdx2 = -1;
|
|
var htEndPt2 = {elContainer: this[_ec_], nOffset: this[_eo_]};
|
|
var elNode2 = this[_ec_];
|
|
if(elNode2[_nt_] == 3){
|
|
htEndPt2 = this._getFixedEndTextNode();
|
|
nTextNodeIdx2 = this._getPosIdx(htEndPt2.elContainer);
|
|
elNode2 = dp(elNode2);
|
|
}
|
|
var sXPathNode2 = this._getXPath(elNode2);
|
|
var oBookmark2 = {sXPath:sXPathNode2, nTextNodeIdx:nTextNodeIdx2, nOffset: htEndPt2.nOffset};
|
|
|
|
return [oBookmark1, oBookmark2];
|
|
},
|
|
|
|
moveToXPathBookmark : function(aBookmark){
|
|
if(!aBookmark) return;
|
|
|
|
var oBookmarkInfo1 = this._evaluateXPathBookmark(aBookmark[0]);
|
|
var oBookmarkInfo2 = this._evaluateXPathBookmark(aBookmark[1]);
|
|
|
|
if(!oBookmarkInfo1["elContainer"] || !oBookmarkInfo2["elContainer"]) return;
|
|
|
|
this[_sc_] = oBookmarkInfo1["elContainer"];
|
|
this[_so_] = oBookmarkInfo1["nOffset"];
|
|
|
|
this[_ec_] = oBookmarkInfo2["elContainer"];
|
|
this[_eo_] = oBookmarkInfo2["nOffset"];
|
|
},
|
|
|
|
_getFixedTextContainer : function(elNode, nOffset){
|
|
while(elNode && elNode[_nt_] == 3 && elNode[_ps_] && elNode[_ps_][_nt_] == 3){
|
|
nOffset += elNode[_ps_].nodeValue.length;
|
|
elNode = elNode[_ps_];
|
|
}
|
|
|
|
return {elContainer:elNode, nOffset:nOffset};
|
|
},
|
|
|
|
_getFixedStartTextNode : function(){
|
|
return this._getFixedTextContainer(this[_sc_], this[_so_]);
|
|
},
|
|
|
|
_getFixedEndTextNode : function(){
|
|
return this._getFixedTextContainer(this[_ec_], this[_eo_]);
|
|
},
|
|
|
|
placeStringBookmark : function(){
|
|
var sTmpId = (new Date()).getTime(), oInsertionPoint, oEndMarker, oStartMarker;
|
|
|
|
(oInsertionPoint = this.cloneRange()).collapseToEnd();
|
|
oInsertionPoint.insertNode( $('<a>').attr('id', HUSKY_BOOKMARK_END_ID_PREFIX+sTmpId)[0] );
|
|
|
|
(oInsertionPoint = this.cloneRange()).collapseToStart();
|
|
oInsertionPoint.insertNode( $('<a>').attr('id', HUSKY_BOOKMARK_START_ID_PREFIX+sTmpId)[0] );
|
|
|
|
this.moveToBookmark(sTmpId);
|
|
|
|
return sTmpId;
|
|
},
|
|
|
|
cloneRange : function(){
|
|
return this._copyRange(new HuskyRange());
|
|
},
|
|
|
|
moveToBookmark : function(vBookmark){
|
|
if(typeof(vBookmark) != "object")
|
|
this.moveToStringBookmark(vBookmark);
|
|
else
|
|
this.moveToXPathBookmark(vBookmark);
|
|
},
|
|
|
|
moveToStringBookmark : function(sBookmarkID){
|
|
var oStartMarker = $('#'+HUSKY_BOOKMARK_START_ID_PREFIX+sBookmarkID)[0];
|
|
var oEndMarker = $('#'+HUSKY_BOOKMARK_END_ID_PREFIX+sBookmarkID)[0];
|
|
|
|
if(!oStartMarker || !oEndMarker) return;
|
|
|
|
this.setEndBefore(oEndMarker);
|
|
this.setStartAfter(oStartMarker);
|
|
},
|
|
|
|
removeStringBookmark : function(sBookmarkID){
|
|
$(
|
|
'#' + HUSKY_BOOKMARK_START_ID_PREFIX + sBookmarkID + ',' +
|
|
'#' + HUSKY_BOOKMARK_END_ID_PREFIX + sBookmarkID
|
|
).remove();
|
|
},
|
|
|
|
collapseToStart : function(){
|
|
this.collapse(true);
|
|
},
|
|
|
|
collapseToEnd : function(){
|
|
this.collapse(false);
|
|
},
|
|
|
|
createAndInsertNode : function(sTagName){
|
|
tmpNode = d.createElement(tagName);
|
|
this.insertNode(tmpNode);
|
|
return tmpNode;
|
|
},
|
|
|
|
getNodes : function(bSplitTextEndNodes, fnFilter){
|
|
if(bSplitTextEndNodes) this._splitTextEndNodesOfTheRange();
|
|
|
|
var aAllNodes = this._getNodesInRange();
|
|
var aFilteredNodes = [];
|
|
|
|
if(!fnFilter) return aAllNodes;
|
|
|
|
for(var i=0; i<aAllNodes.length; i++)
|
|
if(fnFilter(aAllNodes[i])) aFilteredNodes[aFilteredNodes.length] = aAllNodes[i];
|
|
|
|
return aFilteredNodes;
|
|
},
|
|
|
|
getTextNodes : function(bSplitTextEndNodes){
|
|
var txtFilter = function(oNode){
|
|
if (oNode[_nt_] == 3 && oNode.nodeValue != "\n" && oNode.nodeValue != "")
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
return this.getNodes(bSplitTextEndNodes, txtFilter);
|
|
},
|
|
|
|
surroundContentsWithNewNode : function(sTagName){
|
|
var oNewParent = d.createElement(sTagName);
|
|
this.surroundContents(oNewParent);
|
|
return oNewParent;
|
|
},
|
|
|
|
isRangeInRange : function(oAnoterRange, bIncludePartlySelected){
|
|
var startToStart = this.compareBoundaryPoints(this.START_TO_START, oAnoterRange);
|
|
var startToEnd = this.compareBoundaryPoints(this.START_TO_END, oAnoterRange);
|
|
var endToStart = this.compareBoundaryPoints(this.END_TO_START, oAnoterRange);
|
|
var endToEnd = this.compareBoundaryPoints(this.END_TO_END, oAnoterRange);
|
|
|
|
if(startToStart <= 0 && endToEnd >= 0) return true;
|
|
|
|
if(bIncludePartlySelected){
|
|
if(startToEnd == 1) return false;
|
|
if(endToStart == -1) return false;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
isNodeInRange : function(oNode, bIncludePartlySelected, bContentOnly){
|
|
var oTmpRange = new HuskyRange();
|
|
|
|
if(bContentOnly && oNode.firstChild){
|
|
oTmpRange.setStartBefore(oNode.firstChild);
|
|
oTmpRange.setEndAfter(oNode.lastChild);
|
|
}else{
|
|
oTmpRange.selectNode(oNode);
|
|
}
|
|
|
|
return this.isRangeInRange(oTmpRange, !!bIncludePartlySelected);
|
|
},
|
|
|
|
pasteHTML : function(sHTML){
|
|
if(sHTML == ""){
|
|
this.deleteContents();
|
|
return;
|
|
}
|
|
|
|
var oTmpDiv = $('<div>').html(sHTML)[0];
|
|
var oFirstNode = oTmpDiv.firstChild;
|
|
var oLastNode = oTmpDiv.lastChild;
|
|
|
|
var clone = this.cloneRange();
|
|
var sBM = clone.placeStringBookmark();
|
|
|
|
while(oTmpDiv.lastChild) this.insertNode(oTmpDiv.lastChild);
|
|
|
|
this.setEndNodes(oFirstNode, oLastNode);
|
|
|
|
// delete the content later as deleting it first may mass up the insertion point
|
|
// eg) <p>[A]BCD</p> ---paste O---> O<p>BCD</p>
|
|
clone.moveToBookmark(sBM);
|
|
clone.deleteContents();
|
|
clone.removeStringBookmark(sBM);
|
|
},
|
|
|
|
toString : function(){
|
|
this.toString = W3CDOMRange.prototype.toString;
|
|
return this.toString();
|
|
},
|
|
|
|
toHTMLString : function(){
|
|
return $('<div>').append(this.cloneContents()).html();
|
|
},
|
|
|
|
findAncestorByTagName : function(sTagName){
|
|
var oNode = this[_ca_];
|
|
while(oNode && oNode.tagName != sTagName) oNode = dp(oNode);
|
|
|
|
return oNode;
|
|
},
|
|
|
|
selectNodeContents : function(oNode){
|
|
if(!oNode) return;
|
|
|
|
var oFirstNode = oNode.firstChild?oNode.firstChild:oNode;
|
|
var oLastNode = oNode.lastChild?oNode.lastChild:oNode;
|
|
|
|
if(oFirstNode[_nt_] == 3)
|
|
this.setStart(oFirstNode, 0);
|
|
else
|
|
this.setStartBefore(oFirstNode);
|
|
|
|
if(oLastNode[_nt_] == 3)
|
|
this.setEnd(oLastNode, oLastNode.nodeValue.length);
|
|
else
|
|
this.setEndAfter(oLastNode);
|
|
},
|
|
|
|
styleRange : function(oStyle, oAttribute, sNewSpanMarker){
|
|
var aStyleParents = this._getStyleParentNodes(sNewSpanMarker), c = aStyleParents.length, i;
|
|
if(c < 1) return;
|
|
|
|
for(i=0; i < c; i++) {
|
|
if (oStyle) $(aStyleParents[i]).css(oStyle);
|
|
if (oAttribute) $(aStyleParents[i]).attr(oAttribute);
|
|
}
|
|
|
|
this.setStartBefore(aStyleParents[0]);
|
|
this.setEndAfter(aStyleParents[aStyleParents.length-1]);
|
|
},
|
|
|
|
_getStyleParentNodes : function(sNewSpanMarker){
|
|
this._splitTextEndNodesOfTheRange();
|
|
|
|
var oSNode = this.getStartNode();
|
|
var oENode = this.getEndNode();
|
|
|
|
var aAllNodes = this._getNodesInRange();
|
|
var aResult = [];
|
|
var nResult = 0;
|
|
|
|
var oNode, oTmpNode, iStartRelPos, iEndRelPos, $span, iSIdx, iEIdx;
|
|
var nInitialLength = aAllNodes.length;
|
|
var arAllBottmNodes = array_filter(aAllNodes, function(v){return (!v.firstChild);});
|
|
|
|
for(var i=0; i<nInitialLength; i++){
|
|
oNode = aAllNodes[i];
|
|
|
|
if(!oNode) continue;
|
|
if(oNode[_nt_] != 3) continue;
|
|
if(oNode.nodeValue == "") continue;
|
|
|
|
var oParentNode = dp(oNode);
|
|
|
|
if(oParentNode.tagName == "SPAN"){
|
|
// check if the SPAN element is fully contained
|
|
// do quick checks before trying indexOf() because indexOf() function is very slow
|
|
oTmpNode = this._getVeryFirstRealChild(oParentNode);
|
|
if(oTmpNode == oNode) iSIdx = 1;
|
|
else iSIdx = arAllBottmNodes.indexOf(oTmpNode);
|
|
|
|
if(iSIdx != -1){
|
|
oTmpNode = this._getVeryLastRealChild(oParentNode);
|
|
if(oTmpNode == oNode) iEIdx = 1;
|
|
else iEIdx = arAllBottmNodes.indexOf(oTmpNode);
|
|
}
|
|
|
|
if(iSIdx != -1 && iEIdx != -1){
|
|
aResult[nResult++] = oParentNode;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
aResult[nResult++] = $span = $(oNode).wrap('<span />').parent()[0];
|
|
|
|
if(sNewSpanMarker) $span.attr(sNewSpanMarker, "true");
|
|
}
|
|
|
|
this.setStartBefore(oSNode);
|
|
this.setEndAfter(oENode);
|
|
|
|
return aResult;
|
|
},
|
|
|
|
_getVeryFirstChild : function(oNode){
|
|
if(oNode.firstChild) return this._getVeryFirstChild(oNode.firstChild);
|
|
return oNode;
|
|
},
|
|
|
|
_getVeryLastChild : function(oNode){
|
|
if(oNode.lastChild) return this._getVeryLastChild(oNode.lastChild);
|
|
return oNode;
|
|
},
|
|
|
|
_getFirstRealChild : function(oNode){
|
|
var oFirstNode = oNode.firstChild;
|
|
while(oFirstNode && oFirstNode[_nt_] == 3 && oFirstNode.nodeValue == "") oFirstNode = oFirstNode[_ns_];
|
|
|
|
return oFirstNode;
|
|
},
|
|
|
|
_getLastRealChild : function(oNode){
|
|
var oLastNode = oNode.lastChild;
|
|
while(oLastNode && oLastNode[_nt_] == 3 && oLastNode.nodeValue == "") oLastNode = oLastNode[_ps_];
|
|
|
|
return oLastNode;
|
|
},
|
|
|
|
_getVeryFirstRealChild : function(oNode){
|
|
var oFirstNode = this._getFirstRealChild(oNode);
|
|
if(oFirstNode) return this._getVeryFirstRealChild(oFirstNode);
|
|
return oNode;
|
|
},
|
|
_getVeryLastRealChild : function(oNode){
|
|
var oLastNode = this._getLastRealChild(oNode);
|
|
if(oLastNode) return this._getVeryLastChild(oLastNode);
|
|
return oNode;
|
|
},
|
|
|
|
_getLineStartInfo : function(node){
|
|
var frontEndFinal = null;
|
|
var frontEnd = node;
|
|
var lineBreaker = node;
|
|
var bParentBreak = true;
|
|
|
|
// vertical(parent) search
|
|
function getLineStart(node){
|
|
if(!node) return;
|
|
if(frontEndFinal) return;
|
|
|
|
if(rxLineBreaker.test(node.tagName)){
|
|
lineBreaker = node;
|
|
frontEndFinal = frontEnd;
|
|
|
|
bParentBreak = true;
|
|
|
|
return;
|
|
}else{
|
|
frontEnd = node;
|
|
}
|
|
|
|
getFrontEnd(node[_ps_]);
|
|
|
|
if(frontEndFinal) return;
|
|
getLineStart(dp(node));
|
|
}
|
|
|
|
// horizontal(sibling) search
|
|
function getFrontEnd(node){
|
|
if(!node) return;
|
|
if(frontEndFinal) return;
|
|
|
|
if(rxLineBreaker.test(node.tagName)){
|
|
lineBreaker = node;
|
|
frontEndFinal = frontEnd;
|
|
|
|
bParentBreak = false;
|
|
return;
|
|
}
|
|
|
|
if(node.firstChild && node.tagName != "TABLE"){
|
|
var curNode = node.lastChild;
|
|
while(curNode && !frontEndFinal){
|
|
getFrontEnd(curNode);
|
|
|
|
curNode = curNode[_ps_];
|
|
}
|
|
}else{
|
|
frontEnd = node;
|
|
}
|
|
|
|
if(!frontEndFinal){
|
|
getFrontEnd(node[_ps_]);
|
|
}
|
|
}
|
|
|
|
getLineStart(node);
|
|
|
|
return {oNode: frontEndFinal, oLineBreaker: lineBreaker, bParentBreak: bParentBreak};
|
|
},
|
|
|
|
_getLineEndInfo : function(node){
|
|
var backEndFinal = null;
|
|
var backEnd = node;
|
|
var lineBreaker = node;
|
|
var bParentBreak = true;
|
|
|
|
// vertical(parent) search
|
|
function getLineEnd(node){
|
|
if(!node) return;
|
|
if(backEndFinal) return;
|
|
|
|
if(rxLineBreaker.test(node.tagName)){
|
|
lineBreaker = node;
|
|
backEndFinal = backEnd;
|
|
|
|
bParentBreak = true;
|
|
|
|
return;
|
|
}else{
|
|
backEnd = node;
|
|
}
|
|
|
|
getBackEnd(node[_ns_]);
|
|
if(backEndFinal) return;
|
|
|
|
getLineEnd(dp(node));
|
|
}
|
|
|
|
// horizontal(sibling) search
|
|
function getBackEnd(node){
|
|
if(!node) return;
|
|
if(backEndFinal) return;
|
|
|
|
if(rxLineBreaker.test(node.tagName)){
|
|
lineBreaker = node;
|
|
backEndFinal = backEnd;
|
|
|
|
bParentBreak = false;
|
|
|
|
return;
|
|
}
|
|
|
|
if(node.firstChild && node.tagName != "TABLE"){
|
|
var curNode = node.firstChild;
|
|
while(curNode && !backEndFinal){
|
|
getBackEnd(curNode);
|
|
|
|
curNode = curNode[_ns_];
|
|
}
|
|
}else{
|
|
backEnd = node;
|
|
}
|
|
|
|
if(!backEndFinal){
|
|
getBackEnd(node[_ns_]);
|
|
}
|
|
}
|
|
|
|
getLineEnd(node);
|
|
|
|
return {oNode: backEndFinal, oLineBreaker: lineBreaker, bParentBreak: bParentBreak};
|
|
},
|
|
|
|
getLineInfo : function(){
|
|
var oSNode = this.getStartNode();
|
|
var oENode = this.getEndNode();
|
|
|
|
// the range is currently collapsed
|
|
if(!oSNode) oSNode = this.getNodeAroundRange(true, true);
|
|
if(!oENode) oENode = this.getNodeAroundRange(true, true);
|
|
|
|
var oStart = this._getLineStartInfo(oSNode);
|
|
var oStartNode = oStart.oNode;
|
|
var oEnd = this._getLineEndInfo(oENode);
|
|
var oEndNode = oEnd.oNode;
|
|
|
|
var iRelativeStartPos = this._compareEndPoint(dp(oStartNode), this._getPosIdx(oStartNode), this[_ec_], this[_eo_]);
|
|
var iRelativeEndPos = this._compareEndPoint(dp(oEndNode), this._getPosIdx(oEndNode)+1, this[_sc_], this[_so_]);
|
|
|
|
if(!(iRelativeStartPos <= 0 && iRelativeEndPos >= 0)){
|
|
oSNode = this.getNodeAroundRange(false, true);
|
|
oENode = this.getNodeAroundRange(false, true);
|
|
oStart = this._getLineStartInfo(oSNode);
|
|
oEnd = this._getLineEndInfo(oENode);
|
|
}
|
|
|
|
return {oStart: oStart, oEnd: oEnd};
|
|
}
|
|
});
|
|
/**
|
|
* }}} HuskyRange
|
|
*/
|
|
|
|
/**
|
|
* {{{ @class SimpleSelection
|
|
* @brief Cross-browser selection function
|
|
*/
|
|
function SimpleSelection(win){
|
|
this.init();
|
|
|
|
if(!this._oSelection) this.selectionLoaded = false;
|
|
};
|
|
$.extend(SimpleSelection.prototype, {
|
|
selectionLoaded : true,
|
|
selectRange : function(oRng) {
|
|
this.selectNone();
|
|
this.addRange(oRng);
|
|
}
|
|
});
|
|
|
|
function SimpleSelectionImpl_FF() { };
|
|
$.extend(SimpleSelectionImpl_FF.prototype, {
|
|
init : function() {
|
|
this._oSelection = window.getSelection();
|
|
},
|
|
getRangeAt : function(iNum) {
|
|
iNum = iNum || 0;
|
|
|
|
try{
|
|
var oFFRange = this._oSelection.getRangeAt(iNum);
|
|
}catch(e){return new W3CDOMRange();}
|
|
|
|
return this._FFRange2W3CRange(oFFRange);
|
|
},
|
|
addRange : function(oW3CRange){
|
|
var oFFRange = this._W3CRange2FFRange(oW3CRange);
|
|
this._oSelection.addRange(oFFRange);
|
|
},
|
|
selectNone : function() {
|
|
this._oSelection.removeAllRanges();
|
|
},
|
|
_FFRange2W3CRange : function(oFFRange){
|
|
var oW3CRange = new W3CDOMRange();
|
|
oW3CRange.setStart(oFFRange[_sc_], oFFRange[_so_]);
|
|
oW3CRange.setEnd(oFFRange[_ec_], oFFRange[_eo_]);
|
|
return oW3CRange;
|
|
},
|
|
_W3CRange2FFRange : function(oW3CRange){
|
|
var oFFRange = d.createRange();
|
|
oFFRange.setStart(oW3CRange[_sc_], oW3CRange[_so_]);
|
|
oFFRange.setEnd(oW3CRange[_ec_], oW3CRange[_eo_]);
|
|
|
|
return oFFRange;
|
|
}
|
|
});
|
|
|
|
function SimpleSelectionImpl_IE(){ };
|
|
$.extend(SimpleSelectionImpl_IE.prototype, {
|
|
init : function() {
|
|
this._oSelection = d.selection;
|
|
},
|
|
getRangeAt : function(iNum) {
|
|
iNum = iNum || 0;
|
|
|
|
if(this._oSelection.type == "Control"){
|
|
var oW3CRange = new W3CDOMRange();
|
|
var oSelectedNode = this._oSelection.createRange().item(iNum);
|
|
|
|
// if the selction occurs in a different document, ignore
|
|
if(!oSelectedNode || oSelectedNode.ownerDocument != d) return oW3CRange;
|
|
|
|
oW3CRange.selectNode(oSelectedNode);
|
|
|
|
return oW3CRange;
|
|
}else{
|
|
var oSelectedNode = this._oSelection.createRangeCollection().item(iNum).parentElement();
|
|
|
|
// if the selction occurs in a different document, ignore
|
|
if(!oSelectedNode || oSelectedNode.ownerDocument != d){
|
|
var oW3CRange = new W3CDOMRange();
|
|
return oW3CRange;
|
|
}
|
|
return this._IERange2W3CRange(this._oSelection.createRangeCollection().item(iNum));
|
|
}
|
|
},
|
|
addRange : function(oW3CRange){
|
|
var oIERange = this._W3CRange2IERange(oW3CRange);
|
|
oIERange.select();
|
|
},
|
|
selectNone : function(){
|
|
this._oSelection.empty();
|
|
},
|
|
_W3CRange2IERange : function(oW3CRange){
|
|
var oStartIERange = this._getIERangeAt(oW3CRange[_sc_], oW3CRange[_so_]);
|
|
var oEndIERange = this._getIERangeAt(oW3CRange[_ec_], oW3CRange[_eo_]);
|
|
oStartIERange.setEndPoint("EndToEnd", oEndIERange);
|
|
|
|
return oStartIERange;
|
|
},
|
|
_getIERangeAt : function(oW3CContainer, iW3COffset){
|
|
var oIERange = d.body.createTextRange();
|
|
|
|
var oEndPointInfoForIERange = this._getSelectableNodeAndOffsetForIE(oW3CContainer, iW3COffset);
|
|
|
|
var oSelectableNode = oEndPointInfoForIERange.oSelectableNodeForIE;
|
|
var iIEOffset = oEndPointInfoForIERange.iOffsetForIE;
|
|
|
|
oIERange.moveToElementText(oSelectableNode);
|
|
oIERange.collapse(oEndPointInfoForIERange.bCollapseToStart);
|
|
oIERange.moveStart("character", iIEOffset);
|
|
|
|
return oIERange;
|
|
},
|
|
_getSelectableNodeAndOffsetForIE : function(oW3CContainer, iW3COffset){
|
|
var oIERange = d.body.createTextRange();
|
|
|
|
var oNonTextNode = null;
|
|
var aChildNodes = null;
|
|
var iNumOfLeftNodesToCount = 0;
|
|
|
|
if(oW3CContainer[_nt_] == 3){
|
|
oNonTextNode = dp(oW3CContainer);
|
|
aChildNodes = dc(oNonTextNode);
|
|
iNumOfLeftNodesToCount = aChildNodes.length;
|
|
}else{
|
|
oNonTextNode = oW3CContainer;
|
|
aChildNodes = dc(oNonTextNode);
|
|
iNumOfLeftNodesToCount = iW3COffset;
|
|
}
|
|
|
|
var oNodeTester = null;
|
|
|
|
var iResultOffset = 0;
|
|
|
|
var bCollapseToStart = true;
|
|
|
|
for(var i=0; i<iNumOfLeftNodesToCount; i++){
|
|
oNodeTester = aChildNodes[i];
|
|
|
|
if(oNodeTester[_nt_] == 3){
|
|
if(oNodeTester == oW3CContainer) break;
|
|
|
|
iResultOffset += oNodeTester.nodeValue.length;
|
|
}else{
|
|
oIERange.moveToElementText(oNodeTester);
|
|
oNonTextNode = oNodeTester;
|
|
iResultOffset = 0;
|
|
|
|
bCollapseToStart = false;
|
|
}
|
|
}
|
|
|
|
if(oW3CContainer[_nt_] == 3) iResultOffset += iW3COffset;
|
|
|
|
return {oSelectableNodeForIE:oNonTextNode, iOffsetForIE: iResultOffset, bCollapseToStart: bCollapseToStart};
|
|
},
|
|
_IERange2W3CRange : function(oIERange){
|
|
var oW3CRange = new W3CDOMRange();
|
|
|
|
var oIEPointRange = null;
|
|
var oPosition = null;
|
|
|
|
oIEPointRange = oIERange.duplicate();
|
|
oIEPointRange.collapse(true);
|
|
|
|
oPosition = this._getW3CContainerAndOffset(oIEPointRange, true);
|
|
|
|
oW3CRange.setStart(oPosition.oContainer, oPosition.iOffset);
|
|
|
|
var oCollapsedChecker = oIERange.duplicate();
|
|
oCollapsedChecker.collapse(true);
|
|
if(oCollapsedChecker.isEqual(oIERange)){
|
|
oW3CRange.collapse(true);
|
|
}else{
|
|
oIEPointRange = oIERange.duplicate();
|
|
oIEPointRange.collapse(false);
|
|
oPosition = this._getW3CContainerAndOffset(oIEPointRange);
|
|
oW3CRange.setEnd(oPosition.oContainer, oPosition.iOffset);
|
|
}
|
|
|
|
return oW3CRange;
|
|
},
|
|
_getW3CContainerAndOffset : function(oIEPointRange, bStartPt){
|
|
var oRgOrigPoint = oIEPointRange;
|
|
|
|
var oContainer = oRgOrigPoint.parentElement();
|
|
var offset = -1;
|
|
|
|
var oRgTester = d.body.createTextRange();
|
|
var aChildNodes = dc(oContainer);
|
|
var oPrevNonTextNode = null;
|
|
var pointRangeIdx = 0;
|
|
|
|
for(var i=0;i<aChildNodes.length;i++){
|
|
if(aChildNodes[i][_nt_] == 3) continue;
|
|
|
|
oRgTester.moveToElementText(aChildNodes[i]);
|
|
|
|
if(oRgTester.compareEndPoints("StartToStart", oIEPointRange)>=0) break;
|
|
|
|
oPrevNonTextNode = aChildNodes[i];
|
|
}
|
|
|
|
var pointRangeIdx = i;
|
|
|
|
if(pointRangeIdx != 0 && aChildNodes[pointRangeIdx-1][_nt_] == 3){
|
|
var oRgTextStart = d.body.createTextRange();
|
|
var oCurTextNode = null;
|
|
if(oPrevNonTextNode){
|
|
oRgTextStart.moveToElementText(oPrevNonTextNode);
|
|
oRgTextStart.collapse(false);
|
|
oCurTextNode = oPrevNonTextNode.nextSibling;
|
|
}else{
|
|
oRgTextStart.moveToElementText(oContainer);
|
|
oRgTextStart.collapse(true);
|
|
oCurTextNode = oContainer.firstChild;
|
|
}
|
|
|
|
var oRgTextsUpToThePoint = oRgOrigPoint.duplicate();
|
|
oRgTextsUpToThePoint.setEndPoint("StartToStart", oRgTextStart);
|
|
|
|
var textCount = oRgTextsUpToThePoint.text.length
|
|
|
|
while(textCount > oCurTextNode.nodeValue.length && oCurTextNode.nextSibling){
|
|
textCount -= oCurTextNode.nodeValue.length;
|
|
oCurTextNode = oCurTextNode.nextSibling;
|
|
}
|
|
|
|
// this will enforce IE to re-reference oCurTextNode
|
|
var oTmp = oCurTextNode.nodeValue;
|
|
|
|
if(bStartPt && oCurTextNode.nextSibling && oCurTextNode.nextSibling[_nt_] == 3 && textCount == oCurTextNode.nodeValue.length){
|
|
textCount -= oCurTextNode.nodeValue.length;
|
|
oCurTextNode = oCurTextNode.nextSibling;
|
|
}
|
|
|
|
oContainer = oCurTextNode;
|
|
offset = textCount;
|
|
}else{
|
|
oContainer = oRgOrigPoint.parentElement();
|
|
offset = pointRangeIdx;
|
|
}
|
|
|
|
return {"oContainer" : oContainer, "iOffset" : offset};
|
|
}
|
|
});
|
|
$.extend(SimpleSelection.prototype, ($.browser.msie?SimpleSelectionImpl_IE:SimpleSelectionImpl_FF).prototype);
|
|
/**
|
|
* }}} SimpleSelection
|
|
*/
|
|
|
|
// {{{ DOMFix
|
|
DOMFix = {
|
|
init : function(){
|
|
if ($.browser.msie || $.browser.opera) {
|
|
this[_cn_] = this._childNodes_Fix;
|
|
this[_pn_] = this._parentNode_Fix;
|
|
} else {
|
|
this[_cn_] = this._childNodes_Native;
|
|
this[_pn_] = this._parentNode_Native;
|
|
}
|
|
},
|
|
|
|
_parentNode_Native : function(elNode){
|
|
return elNode[_pn_];
|
|
},
|
|
|
|
_parentNode_Fix : function(elNode){
|
|
if(!elNode) return elNode;
|
|
|
|
while(elNode[_ps_]){ elNode = elNode[_ps_]; }
|
|
|
|
return elNode[_pn_];
|
|
},
|
|
|
|
_childNodes_Native : function(elNode){
|
|
return elNode[_cn_];
|
|
},
|
|
|
|
_childNodes_Fix : function(elNode){
|
|
var aResult = null;
|
|
var nCount = 0;
|
|
|
|
if(elNode){
|
|
var aResult = [];
|
|
elNode = elNode.firstChild;
|
|
while(elNode){
|
|
aResult[nCount++] = elNode;
|
|
elNode=elNode[_ns_];
|
|
}
|
|
}
|
|
|
|
return aResult;
|
|
}
|
|
};
|
|
DOMFix.init();
|
|
dp = DOMFix[_pn_];
|
|
dc = DOMFix[_cn_];
|
|
// }}}
|
|
|
|
/**
|
|
* }}} Selection
|
|
*/
|
|
|
|
/**
|
|
* {{{ Utility functions
|
|
*/
|
|
// bind a function to the host object
|
|
function bind(obj, func) {
|
|
return function(){ return func.apply(obj, arguments); };
|
|
};
|
|
|
|
// check block element
|
|
function is_block(el) {
|
|
return (el && el[_nt_] == 1 && rx_block.test(el[_nn_]));
|
|
};
|
|
|
|
// is defined?
|
|
function is_def(v){ return typeof(v)!='undefined'; };
|
|
|
|
// is string?
|
|
function is_str(v){ return typeof(v)=='string'; };
|
|
|
|
// filter
|
|
function array_filter(arr, fn) {
|
|
var ret=[], i, c;
|
|
|
|
for(i=0,c=arr.length; i < c; i++) {
|
|
if (fn(arr[i])) ret.push(arr[i]);
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
// node index
|
|
function node_index(node) {
|
|
var cs = node[_pn_][_cn_], c, i;
|
|
|
|
for(i=0,c=cs.length; i < c; i++) {
|
|
if (cs[i] === node) return i;
|
|
}
|
|
|
|
return -1;
|
|
};
|
|
|
|
/**
|
|
* }}}
|
|
*/
|
|
|
|
// global context
|
|
if (xe.Xeed) xe.Xeed = $.extend(Xeed, xe.Xeed);
|
|
else xe.Xeed = Xeed;
|
|
|
|
xe.W3CDOMRange = W3CDOMRange;
|
|
xe.HuskyRange = HuskyRange;
|
|
|
|
// run callback functions
|
|
if ($.isArray(xe.Xeed.callbacks) && xe.Xeed.callbacks.length) {
|
|
while(fn = xe.Xeed.callbacks.shift()) fn();
|
|
}
|
|
|
|
})(jQuery);
|