tag, what will be wrapped? it could be useful if the manipulator would actually * wrap everything inside the div except the
is allowed to wrap
* textNodes,
and and . therefore this tricky method should answer the question for those 3 elements
* with true, but for for the
it should return false. and since the method does not know this, there is a configuration
* for this
*
* @param selectionTree rangeObject selection tree element (only one, not an array of)
* @param markupObject lowercase string of the tag to be verified (e.g. "b")
* @return true if the markup is allowed to wrap the selection tree element, false otherwise
* @hide
*/
isMarkupAllowedToStealSelectionTreeElement: function(selectionTreeElement, markupObject) {
if (!selectionTreeElement.domobj) {
return false;
}
var nodeName = selectionTreeElement.domobj.nodeName.toLowerCase(),
markupName;
nodeName = (nodeName == '#text') ? 'textNode' : nodeName;
markupName = markupObject[0].nodeName.toLowerCase();
// if nothing is defined for the markup, it's now allowed
if (!this.allowedToStealElements[ markupName ]) {
return false;
}
// if something is defined, but the specifig tag is not in the list
if (this.allowedToStealElements[ markupName ].indexOf(nodeName) == -1) {
return false;
}
return true;
},
/**
* checks if a selection can be completey wrapped by a certain html tags (helper method for this.optimizeSelectionTree4Markup
* @param selectionTree rangeObject selection tree
* @param markupObject lowercase string of the tag to be verified (e.g. "b")
* @return true if selection can be applied as whole, false otherwise
* @hide
*/
canMarkupBeApplied2ElementAsWhole: function(selectionTree, markupObject) {
var htmlTag, i, el, returnVal;
if (markupObject.jquery) {
htmlTag = markupObject[0].tagName;
}
if (markupObject.tagName) {
htmlTag = markupObject.tagName;
}
returnVal = true;
for ( i = 0; i < selectionTree.length; i++) {
el = selectionTree[i];
if (el.domobj && (el.selection != "none" || markupObject.isReplacingElement)) {
// Aloha.Log.debug(this, 'Checking, if <' + htmlTag + '> can be applied to ' + el.domobj.nodeName);
if (!this.canTag1WrapTag2(htmlTag, el.domobj.nodeName)) {
return false;
}
if (el.children.length > 0 && !this.canMarkupBeApplied2ElementAsWhole(el.children, markupObject)) {
return false;
}
}
}
return returnVal;
},
/**
* checks if a tag 1 (first parameter) can wrap tag 2 (second parameter).
* IMPORTANT: the method does not verify, if there have to be other tags in between
* Example: this.canTag1WrapTag2("table", "td") will return true, because the method does not take into account, that there has to be a "tr" in between
* @param t1 string: tagname of outer tag to verify, e.g. "b"
* @param t2 string: tagname of inner tag to verify, e.g. "b"
* @return true if tag 1 can wrap tag 2, false otherwise
* @hide
*/
canTag1WrapTag2: function(t1, t2) {
t1 = (t1 == '#text')?'textNode':t1.toLowerCase();
t2 = (t2 == '#text')?'textNode':t2.toLowerCase();
if (!this.tagHierarchy[ t1 ]) {
// Aloha.Log.warn(this, t1 + ' is an unknown tag to the method canTag1WrapTag2 (paramter 1). Sadfully allowing the wrapping...');
return true;
}
if (!this.tagHierarchy[ t2 ]) {
// Aloha.Log.warn(this, t2 + ' is an unknown tag to the method canTag1WrapTag2 (paramter 2). Sadfully allowing the wrapping...');
return true;
}
var t1Array = this.tagHierarchy[ t1 ],
returnVal = (t1Array.indexOf( t2 ) != -1) ? true : false;
return returnVal;
},
/**
* Check whether it is allowed to insert the given tag at the start of the
* current selection. This method will check whether the markup effective for
* the start and outside of the editable part (starting with the editable tag
* itself) may wrap the given tag.
* @param tagName {String} name of the tag which shall be inserted
* @return true when it is allowed to insert that tag, false if not
* @hide
*/
mayInsertTag: function (tagName) {
if (typeof this.rangeObject.unmodifiableMarkupAtStart == 'object') {
// iterate over all DOM elements outside of the editable part
for (var i = 0; i < this.rangeObject.unmodifiableMarkupAtStart.length; ++i) {
// check whether an element may not wrap the given
if (!this.canTag1WrapTag2(this.rangeObject.unmodifiableMarkupAtStart[i].nodeName, tagName)) {
// found a DOM element which forbids to insert the given tag, we are done
return false;
}
}
// all of the found DOM elements allow inserting the given tag
return true;
} else {
Aloha.Log.warn(this, 'Unable to determine whether tag ' + tagName + ' may be inserted');
return true;
}
},
/**
* String representation
* @return "Aloha.Selection"
* @hide
*/
toString: function() {
return 'Aloha.Selection';
},
/**
* @namespace Aloha.Selection
* @class SelectionRange
* @extends GENTICS.Utils.RangeObject
* Constructor for a range object.
* Optionally you can pass in a range object that's properties will be assigned to the new range object.
* @param rangeObject A range object thats properties will be assigned to the new range object.
* @constructor
*/
SelectionRange: GENTICS.Utils.RangeObject.extend({
_constructor: function(rangeObject){
this._super(rangeObject);
// If a range object was passed in we apply the values to the new range object
if (rangeObject) {
if (rangeObject.commonAncestorContainer) {
this.commonAncestorContainer = rangeObject.commonAncestorContainer;
}
if (rangeObject.selectionTree) {
this.selectionTree = rangeObject.selectionTree;
}
if (rangeObject.limitObject) {
this.limitObject = rangeObject.limitObject;
}
if (rangeObject.markupEffectiveAtStart) {
this.markupEffectiveAtStart = rangeObject.markupEffectiveAtStart;
}
if (rangeObject.unmodifiableMarkupAtStart) {
this.unmodifiableMarkupAtStart = rangeObject.unmodifiableMarkupAtStart;
}
if (rangeObject.splitObject) {
this.splitObject = rangeObject.splitObject;
}
}
},
/**
* DOM object of the common ancestor from startContainer and endContainer
* @hide
*/
commonAncestorContainer: undefined,
/**
* The selection tree
* @hide
*/
selectionTree: undefined,
/**
* Array of DOM objects effective for the start container and inside the
* editable part (inside the limit object). relevant for the button status
* @hide
*/
markupEffectiveAtStart: [],
/**
* Array of DOM objects effective for the start container, which lies
* outside of the editable portion (starting with the limit object)
* @hide
*/
unmodifiableMarkupAtStart: [],
/**
* DOM object being the limit for all markup relevant activities
* @hide
*/
limitObject: undefined,
/**
* DOM object being split when enter key gets hit
* @hide
*/
splitObject: undefined,
/**
* Sets the visible selection in the Browser based on the range object.
* If the selection is collapsed, this will result in a blinking cursor,
* otherwise in a text selection.
* @method
*/
select: function() {
// Call Utils' select()
this._super();
// update the selection
Aloha.Selection.updateSelection();
},
/**
* Method to update a range object internally
* @param commonAncestorContainer (DOM Object); optional Parameter; if set, the parameter
* will be used instead of the automatically calculated CAC
* @return void
* @hide
*/
update: function(commonAncestorContainer) {
this.updatelimitObject();
this.updateMarkupEffectiveAtStart();
this.updateCommonAncestorContainer(commonAncestorContainer);
// reset the selectiontree (must be recalculated)
this.selectionTree = undefined;
},
/**
* Get the selection tree for this range
* TODO: remove this (was moved to range.js)
* @return selection tree
* @hide
*/
getSelectionTree: function () {
// if not yet calculated, do this now
if (!this.selectionTree) {
this.selectionTree = Aloha.Selection.getSelectionTree(this);
}
return this.selectionTree;
},
/**
* TODO: move this to range.js
* Get an array of domobj (in dom tree order) of siblings of the given domobj, which are contained in the selection
* @param domobj dom object to start with
* @return array of siblings of the given domobj, which are also selected
* @hide
*/
getSelectedSiblings: function (domobj) {
var selectionTree = this.getSelectionTree();
return this.recursionGetSelectedSiblings(domobj, selectionTree);
},
/**
* TODO: move this to range.js
* Recursive method to find the selected siblings of the given domobj (which should be selected as well)
* @param domobj dom object for which the selected siblings shall be found
* @param selectionTree current level of the selection tree
* @return array of selected siblings of dom objects or false if none found
* @hide
*/
recursionGetSelectedSiblings: function (domobj, selectionTree) {
var selectedSiblings = false,
foundObj = false,
i;
for ( i = 0; i < selectionTree.length; ++i) {
if (selectionTree[i].domobj === domobj) {
foundObj = true;
selectedSiblings = [];
} else if (!foundObj && selectionTree[i].children) {
// do the recursion
selectedSiblings = this.recursionGetSelectedSiblings(domobj, selectionTree[i].children);
if (selectedSiblings !== false) {
break;
}
} else if (foundObj && selectionTree[i].domobj && selectionTree[i].selection != 'collapsed' && selectionTree[i].selection != 'none') {
selectedSiblings.push(selectionTree[i].domobj);
} else if (foundObj && selectionTree[i].selection == 'none') {
break;
}
}
return selectedSiblings;
},
/**
* TODO: move this to range.js
* Method updates member var markupEffectiveAtStart and splitObject, which is relevant primarily for button status and enter key behaviour
* @return void
* @hide
*/
updateMarkupEffectiveAtStart: function() {
// reset the current markup
this.markupEffectiveAtStart = [];
this.unmodifiableMarkupAtStart = [];
var
parents = this.getStartContainerParents(),
limitFound = false,
splitObjectWasSet,
i, el;
for ( i = 0; i < parents.length; i++) {
el = parents[i];
if (!limitFound && (el !== this.limitObject)) {
this.markupEffectiveAtStart[ i ] = el;
if (!splitObjectWasSet && GENTICS.Utils.Dom.isSplitObject(el)) {
splitObjectWasSet = true;
this.splitObject = el;
}
} else {
limitFound = true;
this.unmodifiableMarkupAtStart.push(el);
}
}
if (!splitObjectWasSet) {
this.splitObject = false;
}
return;
},
/**
* TODO: remove this
* Method updates member var markupEffectiveAtStart, which is relevant primarily for button status
* @return void
* @hide
*/
updatelimitObject: function() {
if (Aloha.editables && Aloha.editables.length > 0) {
var parents = this.getStartContainerParents(),
editables = Aloha.editables,
i, el, j, editable;
for ( i = 0; i < parents.length; i++) {
el = parents[i];
for ( j = 0; j < editables.length; j++) {
editable = editables[j].obj[0];
if (el === editable) {
this.limitObject = el;
return true;
}
}
}
}
this.limitObject = jQuery('body');
return true;
},
/**
* string representation of the range object
* @param verbose set to true for verbose output
* @return string representation of the range object
* @hide
*/
toString: function(verbose) {
if (!verbose) {
return 'Aloha.Selection.SelectionRange';
}
return 'Aloha.Selection.SelectionRange {start [' + this.startContainer.nodeValue + '] offset '
+ this.startOffset + ', end [' + this.endContainer.nodeValue + '] offset ' + this.endOffset + '}';
}
}) // SelectionRange
}); // Selection
/**
* This method implements an ugly workaround for a selection problem in ie:
* when the cursor shall be placed at the end of a text node in a li element, that is followed by a nested list,
* the selection would always snap into the first li of the nested list
* therefore, we make sure that the text node ends with a space and place the cursor right before it
*/
function nestedListInIEWorkaround ( range ) {
if (jQuery.browser.msie
&& range.startContainer === range.endContainer
&& range.startOffset === range.endOffset
&& range.startContainer.nodeType == 3
&& range.startOffset == range.startContainer.data.length
&& range.startContainer.nextSibling
&& ["OL", "UL"].indexOf(range.startContainer.nextSibling.nodeName) !== -1) {
if (range.startContainer.data[range.startContainer.data.length-1] == ' ') {
range.startOffset = range.endOffset = range.startOffset-1;
} else {
range.startContainer.data = range.startContainer.data + ' ';
}
}
}
function correctRange ( range ) {
nestedListInIEWorkaround(range);
return range;
}
/**
* Implements Selection http://html5.org/specs/dom-range.html#selection
* @namespace Aloha
* @class Selection This singleton class always represents the
* current user selection
* @singleton
*/
var AlohaSelection = Class.extend({
_constructor : function( nativeSelection ) {
this._nativeSelection = nativeSelection;
this.ranges = [];
// will remember if urged to not change the selection
this.preventChange = false;
},
/**
* Returns the element that contains the start of the selection. Returns null if there's no selection.
* @readonly
* @type Node
*/
anchorNode: null,
/**
* Returns the offset of the start of the selection relative to the element that contains the start
* of the selection. Returns 0 if there's no selection.
* @readonly
* @type int
*/
anchorOffset: 0,
/**
* Returns the element that contains the end of the selection.
* Returns null if there's no selection.
* @readonly
* @type Node
*/
focusNode: null,
/**
* Returns the offset of the end of the selection relative to the element that contains the end
* of the selection. Returns 0 if there's no selection.
* @readonly
* @type int
*/
focusOffset: 0,
/**
* Returns true if there's no selection or if the selection is empty. Otherwise, returns false.
* @readonly
* @type boolean
*/
isCollapsed: false,
/**
* Returns the number of ranges in the selection.
* @readonly
* @type int
*/
rangeCount: 0,
/**
* Replaces the selection with an empty one at the given position.
* @throws a WRONG_DOCUMENT_ERR exception if the given node is in a different document.
* @param parentNode Node of new selection
* @param offest offest of new Selection in parentNode
* @void
*/
collapse: function ( parentNode, offset ) {
this._nativeSelection.collapse( parentNode, offset );
},
/**
* Replaces the selection with an empty one at the position of the start of the current selection.
* @throws an INVALID_STATE_ERR exception if there is no selection.
* @void
*/
collapseToStart: function() {
throw "NOT_IMPLEMENTED";
},
/**
* @void
*/
extend: function ( parentNode, offset) {
},
/**
* @param alter DOMString
* @param direction DOMString
* @param granularity DOMString
* @void
*/
modify: function ( alter, direction, granularity ) {
},
/**
* Replaces the selection with an empty one at the position of the end of the current selection.
* @throws an INVALID_STATE_ERR exception if there is no selection.
* @void
*/
collapseToEnd: function() {
throw "NOT_IMPLEMENTED";
},
/**
* Replaces the selection with one that contains all the contents of the given element.
* @throws a WRONG_DOCUMENT_ERR exception if the given node is in a different document.
* @param parentNode Node the Node fully select
* @void
*/
selectAllChildren: function( parentNode ) {
throw "NOT_IMPLEMENTED";
},
/**
* Deletes the contents of the selection
*/
deleteFromDocument: function() {
throw "NOT_IMPLEMENTED";
},
/**
* NB!
* We have serious problem in IE.
* The range that we get in IE is not the same as the range we had set,
* so even if we normalize it during getRangeAt, in IE, we will be
* correcting the range to the "correct" place, but still not the place
* where it was originally set.
*
* Returns the given range.
* The getRangeAt(index) method returns the indexth range in the list.
* NOTE: Aloha Editor only support 1 range! index can only be 0
* @throws INDEX_SIZE_ERR DOM exception if index is less than zero or
* greater or equal to the value returned by the rangeCount.
* @param index int
* @return Range return the selected range from index
*/
getRangeAt: function ( index ) {
return correctRange( this._nativeSelection.getRangeAt( index ) );
//if ( index < 0 || this.rangeCount ) {
// throw "INDEX_SIZE_ERR DOM";
//}
//return this._ranges[index];
},
/**
* Adds the given range to the selection.
* The addRange(range) method adds the given range Range object to the list of
* selections, at the end (so the newly added range is the new last range).
* NOTE: Aloha Editor only support 1 range! The added range will replace the
* range at index 0
* see http://html5.org/specs/dom-range.html#selection note about addRange
* @throws an INVALID_NODE_TYPE_ERR exception if the given Range has a boundary point
* node that's not a Text or Element node, and an INVALID_MODIFICATION_ERR exception
* if it has a boundary point node that doesn't descend from a Document.
* @param range Range adds the range to the selection
* @void
*/
addRange: function( range ) {
// set readonly attributes
this._nativeSelection.addRange( range );
// We will correct the range after rangy has processed the native
// selection range, so that our correction will be the final fix on
// the range according to the guarentee's that Aloha wants to make
this._nativeSelection._ranges[ 0 ] = correctRange( range );
// make sure, the old Aloha selection will be updated (until all implementations use the new AlohaSelection)
Aloha.Selection.updateSelection();
},
/**
* Removes the given range from the selection, if the range was one of the ones in the selection.
* NOTE: Aloha Editor only support 1 range! The added range will replace the
* range at with index 0
* @param range Range removes the range from the selection
* @void
*/
removeRange: function( range ) {
this._nativeSelection.removeRange();
},
/**
* Removes all the ranges in the selection.
* @viod
*/
removeAllRanges: function() {
this._nativeSelection.removeAllRanges();
},
/**
* prevents the next aloha-selection-changed event from
* being triggered
* @param flag boolean defines weather to update the selection on change or not
*/
preventedChange: function( flag ) {
// this.preventChange = typeof flag === 'undefined' ? false : flag;
},
/**
* will return wheter selection change event was prevented or not, and reset the
* preventSelectionChangedFlag
* @return boolean true if aloha-selection-change event
* was prevented
*/
isChangedPrevented: function() {
// return this.preventSelectionChangedFlag;
},
/**
* INFO: Method is used for integration with Gentics
* Aloha, has no use otherwise Updates the rangeObject
* according to the current user selection Method is
* always called on selection change
*
* @param event
* jQuery browser event object
* @return true when rangeObject was modified, false
* otherwise
* @hide
*/
refresh: function(event) {
},
/**
* String representation
*
* @return "Aloha.Selection"
* @hide
*/
toString: function() {
return 'Aloha.Selection';
},
getRangeCount: function() {
return this._nativeSelection.rangeCount;
}
});
/**
* A wrapper for the function of the same name in the rangy core-depdency.
* This function should be preferred as it hides the global rangy object.
* For more information look at the following sites:
* http://html5.org/specs/dom-range.html
* @param window optional - specifices the window to get the selection of
*/
Aloha.getSelection = function( target ) {
var target = ( target !== document || target !== window ) ? window : target;
// Aloha.Selection.refresh()
// implement Aloha Selection
// TODO cache
return new AlohaSelection( window.rangy.getSelection( target ) );
};
/**
* A wrapper for the function of the same name in the rangy core-depdency.
* This function should be preferred as it hides the global rangy object.
* Please note: when the range object is not needed anymore,
* invoke the detach method on it. It is currently unknown to me why
* this is required, but that's what it says in the rangy specification.
* For more information look at the following sites:
* http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
* @param document optional - specifies which document to create the range for
*/
Aloha.createRange = function(givenWindow) {
return window.rangy.createRange(givenWindow);
};
var selection = new Selection();
Aloha.Selection = selection;
return selection;
});