You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

526 lines
15 KiB
JavaScript

define([
"dojo/_base/array",
"dojo/dom", // dom.byId
"dojo/_base/lang",
"dojo/sniff", // has("ie") has("opera")
"dojo/_base/window",
"dijit/focus"
], function(array, dom, lang, has, baseWindow, focus){
// module:
// dijit/selection
// Note that this class is using feature detection, but doesn't use has() because sometimes on IE the outer window
// may be running in standards mode (ie, IE9 mode) but an iframe may be in compatibility mode. So the code path
// used will vary based on the window.
var SelectionManager = function(win){
// summary:
// Class for monitoring / changing the selection (typically highlighted text) in a given window
// win: Window
// The window to monitor/adjust the selection on.
var doc = win.document;
this.getType = function(){
// summary:
// Get the selection type (like doc.select.type in IE).
if(doc.getSelection){
// W3C path
var stype = "text";
// Check if the actual selection is a CONTROL (IMG, TABLE, HR, etc...).
var oSel;
try{
oSel = win.getSelection();
}catch(e){ /*squelch*/ }
if(oSel && oSel.rangeCount == 1){
var oRange = oSel.getRangeAt(0);
if( (oRange.startContainer == oRange.endContainer) &&
((oRange.endOffset - oRange.startOffset) == 1) &&
(oRange.startContainer.nodeType != 3 /* text node*/)
){
stype = "control";
}
}
return stype; //String
}else{
// IE6-8
return doc.selection.type.toLowerCase();
}
};
this.getSelectedText = function(){
// summary:
// Return the text (no html tags) included in the current selection or null if no text is selected
if(doc.getSelection){
// W3C path
var selection = win.getSelection();
return selection ? selection.toString() : ""; //String
}else{
// IE6-8
if(this.getType() == 'control'){
return null;
}
return doc.selection.createRange().text;
}
};
this.getSelectedHtml = function(){
// summary:
// Return the html text of the current selection or null if unavailable
if(doc.getSelection){
// W3C path
var selection = win.getSelection();
if(selection && selection.rangeCount){
var i;
var html = "";
for(i = 0; i < selection.rangeCount; i++){
//Handle selections spanning ranges, such as Opera
var frag = selection.getRangeAt(i).cloneContents();
var div = doc.createElement("div");
div.appendChild(frag);
html += div.innerHTML;
}
return html; //String
}
return null;
}else{
// IE6-8
if(this.getType() == 'control'){
return null;
}
return doc.selection.createRange().htmlText;
}
};
this.getSelectedElement = function(){
// summary:
// Retrieves the selected element (if any), just in the case that
// a single element (object like and image or a table) is
// selected.
if(this.getType() == "control"){
if(doc.getSelection){
// W3C path
var selection = win.getSelection();
return selection.anchorNode.childNodes[ selection.anchorOffset ];
}else{
// IE6-8
var range = doc.selection.createRange();
if(range && range.item){
return doc.selection.createRange().item(0);
}
}
}
return null;
};
this.getParentElement = function(){
// summary:
// Get the parent element of the current selection
if(this.getType() == "control"){
var p = this.getSelectedElement();
if(p){ return p.parentNode; }
}else{
if(doc.getSelection){
var selection = doc.getSelection();
if(selection){
var node = selection.anchorNode;
while(node && (node.nodeType != 1)){ // not an element
node = node.parentNode;
}
return node;
}
}else{
var r = doc.selection.createRange();
r.collapse(true);
return r.parentElement();
}
}
return null;
};
this.hasAncestorElement = function(/*String*/ tagName /* ... */){
// summary:
// Check whether current selection has a parent element which is
// of type tagName (or one of the other specified tagName)
// tagName: String
// The tag name to determine if it has an ancestor of.
return this.getAncestorElement.apply(this, arguments) != null; //Boolean
};
this.getAncestorElement = function(/*String*/ tagName /* ... */){
// summary:
// Return the parent element of the current selection which is of
// type tagName (or one of the other specified tagName)
// tagName: String
// The tag name to determine if it has an ancestor of.
var node = this.getSelectedElement() || this.getParentElement();
return this.getParentOfType(node, arguments); //DOMNode
};
this.isTag = function(/*DomNode*/ node, /*String[]*/ tags){
// summary:
// Function to determine if a node is one of an array of tags.
// node:
// The node to inspect.
// tags:
// An array of tag name strings to check to see if the node matches.
if(node && node.tagName){
var _nlc = node.tagName.toLowerCase();
for(var i=0; i<tags.length; i++){
var _tlc = String(tags[i]).toLowerCase();
if(_nlc == _tlc){
return _tlc; // String
}
}
}
return "";
};
this.getParentOfType = function(/*DomNode*/ node, /*String[]*/ tags){
// summary:
// Function to locate a parent node that matches one of a set of tags
// node:
// The node to inspect.
// tags:
// An array of tag name strings to check to see if the node matches.
while(node){
if(this.isTag(node, tags).length){
return node; // DOMNode
}
node = node.parentNode;
}
return null;
};
this.collapse = function(/*Boolean*/ beginning){
// summary:
// Function to collapse (clear), the current selection
// beginning: Boolean
// Indicates whether to collapse the cursor to the beginning of the selection or end.
if(doc.getSelection){
// W3C path
var selection = win.getSelection();
if(selection.removeAllRanges){ // Mozilla
if(beginning){
selection.collapseToStart();
}else{
selection.collapseToEnd();
}
}else{ // Safari
// pulled from WebCore/ecma/kjs_window.cpp, line 2536
selection.collapse(beginning);
}
}else{
// IE6-8
var range = doc.selection.createRange();
range.collapse(beginning);
range.select();
}
};
this.remove = function(){
// summary:
// Function to delete the currently selected content from the document.
var sel = doc.selection;
if(doc.getSelection){
// W3C path
sel = win.getSelection();
sel.deleteFromDocument();
return sel; //Selection
}else{
// IE6-8
if(sel.type.toLowerCase() != "none"){
sel.clear();
}
return sel; //Selection
}
};
this.selectElementChildren = function(/*DomNode*/ element, /*Boolean?*/ nochangefocus){
// summary:
// clear previous selection and select the content of the node
// (excluding the node itself)
// element: DOMNode
// The element you wish to select the children content of.
// nochangefocus: Boolean
// Indicates if the focus should change or not.
var range;
element = dom.byId(element);
if(doc.getSelection){
// W3C
var selection = win.getSelection();
if(has("opera")){
//Opera's selectAllChildren doesn't seem to work right
//against <body> nodes and possibly others ... so
//we use the W3C range API
if(selection.rangeCount){
range = selection.getRangeAt(0);
}else{
range = doc.createRange();
}
range.setStart(element, 0);
range.setEnd(element,(element.nodeType == 3) ? element.length : element.childNodes.length);
selection.addRange(range);
}else{
selection.selectAllChildren(element);
}
}else{
// IE6-8
range = element.ownerDocument.body.createTextRange();
range.moveToElementText(element);
if(!nochangefocus){
try{
range.select(); // IE throws an exception here if the widget is hidden. See #5439
}catch(e){ /* squelch */}
}
}
};
this.selectElement = function(/*DomNode*/ element, /*Boolean?*/ nochangefocus){
// summary:
// clear previous selection and select element (including all its children)
// element: DOMNode
// The element to select.
// nochangefocus: Boolean
// Boolean indicating if the focus should be changed. IE only.
var range;
element = dom.byId(element); // TODO: remove for 2.0 or sooner, spec listed above doesn't allow for string
if(doc.getSelection){
// W3C path
var selection = doc.getSelection();
range = doc.createRange();
if(selection.removeAllRanges){ // Mozilla
// FIXME: does this work on Safari?
if(has("opera")){
//Opera works if you use the current range on
//the selection if present.
if(selection.getRangeAt(0)){
range = selection.getRangeAt(0);
}
}
range.selectNode(element);
selection.removeAllRanges();
selection.addRange(range);
}
}else{
// IE6-8
try{
var tg = element.tagName ? element.tagName.toLowerCase() : "";
if(tg === "img" || tg === "table"){
range = baseWindow.body(doc).createControlRange();
}else{
range = baseWindow.body(doc).createRange();
}
range.addElement(element);
if(!nochangefocus){
range.select();
}
}catch(e){
this.selectElementChildren(element, nochangefocus);
}
}
};
this.inSelection = function(node){
// summary:
// This function determines if 'node' is
// in the current selection.
// tags:
// public
if(node){
var newRange;
var range;
if(doc.getSelection){
// WC3
var sel = win.getSelection();
if(sel && sel.rangeCount > 0){
range = sel.getRangeAt(0);
}
if(range && range.compareBoundaryPoints && doc.createRange){
try{
newRange = doc.createRange();
newRange.setStart(node, 0);
if(range.compareBoundaryPoints(range.START_TO_END, newRange) === 1){
return true;
}
}catch(e){ /* squelch */}
}
}else{
// IE6-8, so we can't use the range object as the pseudo
// range doesn't implement the boundary checking, we have to
// use IE specific crud.
range = doc.selection.createRange();
try{
newRange = node.ownerDocument.body.createTextRange();
newRange.moveToElementText(node);
}catch(e2){/* squelch */}
if(range && newRange){
// We can finally compare similar to W3C
if(range.compareEndPoints("EndToStart", newRange) === 1){
return true;
}
}
}
}
return false; // Boolean
};
this.getBookmark = function(){
// summary:
// Retrieves a bookmark that can be used with moveToBookmark to reselect the currently selected range.
// TODO: merge additional code from Editor._getBookmark into this method
var bm, rg, tg, sel = doc.selection, cf = focus.curNode;
if(doc.getSelection){
// W3C Range API for selections.
sel = win.getSelection();
if(sel){
if(sel.isCollapsed){
tg = cf? cf.tagName : "";
if(tg){
// Create a fake rangelike item to restore selections.
tg = tg.toLowerCase();
if(tg == "textarea" ||
(tg == "input" && (!cf.type || cf.type.toLowerCase() == "text"))){
sel = {
start: cf.selectionStart,
end: cf.selectionEnd,
node: cf,
pRange: true
};
return {isCollapsed: (sel.end <= sel.start), mark: sel}; //Object.
}
}
bm = {isCollapsed:true};
if(sel.rangeCount){
bm.mark = sel.getRangeAt(0).cloneRange();
}
}else{
rg = sel.getRangeAt(0);
bm = {isCollapsed: false, mark: rg.cloneRange()};
}
}
}else if(sel){
// If the current focus was a input of some sort and no selection, don't bother saving
// a native bookmark. This is because it causes issues with dialog/page selection restore.
// So, we need to create pseudo bookmarks to work with.
tg = cf ? cf.tagName : "";
tg = tg.toLowerCase();
if(cf && tg && (tg == "button" || tg == "textarea" || tg == "input")){
if(sel.type && sel.type.toLowerCase() == "none"){
return {
isCollapsed: true,
mark: null
}
}else{
rg = sel.createRange();
return {
isCollapsed: rg.text && rg.text.length?false:true,
mark: {
range: rg,
pRange: true
}
};
}
}
bm = {};
//'IE' way for selections.
try{
// createRange() throws exception when dojo in iframe
// and nothing selected, see #9632
rg = sel.createRange();
bm.isCollapsed = !(sel.type == 'Text' ? rg.htmlText.length : rg.length);
}catch(e){
bm.isCollapsed = true;
return bm;
}
if(sel.type.toUpperCase() == 'CONTROL'){
if(rg.length){
bm.mark=[];
var i=0,len=rg.length;
while(i<len){
bm.mark.push(rg.item(i++));
}
}else{
bm.isCollapsed = true;
bm.mark = null;
}
}else{
bm.mark = rg.getBookmark();
}
}else{
console.warn("No idea how to store the current selection for this browser!");
}
return bm; // Object
};
this.moveToBookmark = function(/*Object*/ bookmark){
// summary:
// Moves current selection to a bookmark.
// bookmark:
// This should be a returned object from getBookmark().
// TODO: merge additional code from Editor._moveToBookmark into this method
var mark = bookmark.mark;
if(mark){
if(doc.getSelection){
// W3C Range API (FF, WebKit, Opera, etc)
var sel = win.getSelection();
if(sel && sel.removeAllRanges){
if(mark.pRange){
var n = mark.node;
n.selectionStart = mark.start;
n.selectionEnd = mark.end;
}else{
sel.removeAllRanges();
sel.addRange(mark);
}
}else{
console.warn("No idea how to restore selection for this browser!");
}
}else if(doc.selection && mark){
//'IE' way.
var rg;
if(mark.pRange){
rg = mark.range;
}else if(lang.isArray(mark)){
rg = doc.body.createControlRange();
//rg.addElement does not have call/apply method, so can not call it directly
//rg is not available in "range.addElement(item)", so can't use that either
array.forEach(mark, function(n){
rg.addElement(n);
});
}else{
rg = doc.body.createTextRange();
rg.moveToBookmark(mark);
}
rg.select();
}
}
};
this.isCollapsed = function(){
// summary:
// Returns true if there is no text selected
return this.getBookmark().isCollapsed;
};
};
// singleton on the main window
var selection = new SelectionManager(window);
// hook for editor to use class
selection.SelectionManager = SelectionManager;
return selection;
});