milo

TextSelection

function
 TextSelection() 

Option name Type Description
win Window

window in which text selection is processed

Text selection class.
Serves as a helper to manage current selection
The object cannot be reused, if the selection changes some of its properties may contain information related to previous selection

function TextSelection(win) {
    if (! this instanceof TextSelection)
        return new TextSelection(win);
    this.window = win || window;
    this.init();
}

TextSelection$startElement

declaration
 TextSelection$startElement 

TextSelection instance method
Returns selection start element

var TextSelection$startElement = 
    _.partial(_getElement, '_startElement', 'startContainer');

TextSelection$endElement

declaration
 TextSelection$endElement 

TextSelection instance method
Returns selection end element

var TextSelection$endElement = 
    _.partial(_getElement, '_endElement', 'endContainer');

TextSelection$containingElement

declaration
 TextSelection$containingElement 

TextSelection instance method
Returns selection end element

var TextSelection$containingElement = 
    _.partial(_getElement, '_containingElement', 'commonAncestorContainer');

TextSelection$startComponent

declaration
 TextSelection$startComponent 

TextSelection instance method
Returns selection start Component

var TextSelection$startComponent = 
    _.partial(_getComponent, '_startComponent', 'startElement');

TextSelection$endComponent

declaration
 TextSelection$endComponent 

TextSelection instance method
Returns selection end Component

var TextSelection$endComponent = 
    _.partial(_getComponent, '_endComponent', 'endElement');

TextSelection$containingComponent

declaration
 TextSelection$containingComponent 

TextSelection instance method
Returns selection end Component

var TextSelection$containingComponent = 
    _.partial(_getComponent, '_containingComponent', 'containingElement');


_.extendProto(TextSelection, {
    init: TextSelection$init,
    text: TextSelection$text,
    textNodes: TextSelection$textNodes,
    clear: TextSelection$clear,

    startElement: TextSelection$startElement,
    endElement: TextSelection$endElement,
    containingElement: TextSelection$containingElement,

    startComponent: TextSelection$startComponent,
    endComponent: TextSelection$endComponent,
    containingComponent: TextSelection$containingComponent,

    containedComponents: TextSelection$containedComponents,
    eachContainedComponent: TextSelection$eachContainedComponent,
    del: TextSelection$del,
    _getPostDeleteSelectionPoint: _getPostDeleteSelectionPoint,
    _selectAfterDelete: _selectAfterDelete,

    getRange: TextSelection$getRange,
    getState: TextSelection$getState,
    getNormalizedRange: TextSelection$$getNormalizedRange,
    getDirection: TextSelection$$getDirection
});


_.extend(TextSelection, {
    createFromRange: TextSelection$$createFromRange,
    createFromState: TextSelection$$createFromState,
    createStateObject: TextSelection$$createStateObject
});

TextSelection$init

function
 TextSelection$init() 

TextSelection instance method
Initializes TextSelection from the current selection

function TextSelection$init() {
    this.selection = this.window.getSelection();
    if (this.selection.rangeCount)
        this.range = this.selection.getRangeAt(0);
    this.isCollapsed = this.selection.isCollapsed;
}

TextSelection$text

function
 TextSelection$text() 

TextSelection instance method
Retrieves and returns selection text

function TextSelection$text() {
    if (! this.range) return undefined;

    if (! this._text)
        this._text = this.range.toString();

    return this._text;
}

TextSelection$textNodes

function
 TextSelection$textNodes() 

TextSelection instance method
Retrieves and returns selection text nodes

function TextSelection$textNodes() {
    if (! this.range) return undefined;

    if (! this._textNodes)
        this._textNodes = _getTextNodes.call(this);
    return this._textNodes;
}


function TextSelection$clear() {
    this.selection.removeAllRanges();
}

TextSelection$del

function
 TextSelection$del() 

Option name Type Description
selectEndContainer Boolean

set to true if the end container should be selected after deletion

TextSelection instance method
Deletes the current selection and all components in it

function TextSelection$del(selectEndContainer) {
    if (this.isCollapsed || ! this.range) return;

    var selPoint = this._getPostDeleteSelectionPoint(selectEndContainer);

    deleteRangeWithComponents(this.range);

    this._selectAfterDelete(selPoint);
    selPoint.node.parentNode.normalize();
}


function _getPostDeleteSelectionPoint(selectEndContainer) {
    var selNode = this.range.startContainer;
    var selOffset = this.range.startOffset;
    if (selectEndContainer && this.range.startContainer != this.range.endContainer) {
        selNode = this.range.endContainer;
        selOffset = 0;
    }
    return { node: selNode, offset: selOffset };
}


function _selectAfterDelete(selPoint) {
    var selNode = selPoint.node
        , selOffset = selPoint.offset;

    if (!selNode) return;
    if (selNode.nodeType == Node.TEXT_NODE)
        selNode.textContent = selNode.textContent.trimRight();
    if (!selNode.nodeValue)
        selNode.nodeValue = '\u00A0'; //non-breaking space, \u200B for zero width space;

    var position = selOffset > selNode.length ? selNode.length : selOffset;
    setCaretPosition(selNode, position);
}

TextSelection$getRange

function
 TextSelection$getRange() 

Returns selection range

function TextSelection$getRange() {
    return this.range;
}

TextSelection$getState

function
 TextSelection$getState() 

Stores selection window, nodes and offsets in object

function TextSelection$getState(rootEl) {
    var r = this.range;
    var doc = rootEl.ownerDocument
        , win = doc.defaultView || doc.parentWindow;
    if (!r) return { window: win };
    return TextSelection.createStateObject(rootEl, r.startContainer, r.startOffset, r.endContainer, r.endOffset);
}


function TextSelection$$createStateObject(rootEl, startContainer, startOffset, endContainer, endOffset) {
    endContainer = endContainer || startContainer;
    endOffset = endOffset || startOffset;
    var doc = rootEl.ownerDocument
        , win = doc.defaultView || doc.parentWindow;
    return {
        window: win,
        rootEl: rootEl,
        start: _getSelectionPointState(rootEl, startContainer, startOffset),
        end: _getSelectionPointState(rootEl, endContainer, endOffset)
    };
}


function _getSelectionPointState(rootEl, node, offset) {
    var treePath = domUtils.treePathOf(rootEl, node);
    if (! treePath) logger.error('Selection point is outside of root element');
    return {
        treePath: treePath,
        offset: offset
    };
}

TextSelection$$createFromState

function
 TextSelection$$createFromState() 

Restores actual selection to the stored range

function TextSelection$$createFromState(state) {
    var domUtils = state.window.milo.util.dom;

    if (state.rootEl && state.start && state.end) {
        var startNode = _selectionNodeFromState(state.rootEl, state.start)
            , endNode = _selectionNodeFromState(state.rootEl, state.end);

        try {
            domUtils.setSelection(startNode, state.start.offset, endNode, state.end.offset);
            return new TextSelection(state.window);
        } catch(e) {
            logger.error('Text selection: can\'t create selection', e, e.message);
        }
    } else {
        domUtils.clearSelection(state.window);
        return new TextSelection(state.window);
    }
}


function _selectionNodeFromState(rootEl, pointState) {
    var node = domUtils.getNodeAtTreePath(rootEl, pointState.treePath);
    if (! node) logger.error('TextSelection createFromState: no node at treePath');
    return node;
}


/**
 * Creates selection from passed range
 * 
 * @param {Range} range
 * @param {Boolean} backward
 *
 * @return {TextSelection}
 */
function TextSelection$$createFromRange(range, backward) {
    var win = range.startContainer.ownerDocument.defaultView
        , sel = win.getSelection()
        , endRange;

    sel.removeAllRanges();

    if (backward){
        endRange = range.cloneRange();
        endRange.collapse(false);

        sel.addRange(endRange);
        sel.extend(range.startContainer, range.startOffset);
    }
    else {
        sel.addRange(range);
    }

    return new TextSelection(win);
}

/**
 * Returns a normalized copy of the range
 * If you triple click an item, the end of the range is positioned at the beginning of the NEXT node.
 * this function returns a range with the end positioned at the end of the last textnode contained 
 * inside a component with the "editable" facet
 * 
 * @return {range}
 */
function TextSelection$$getNormalizedRange(){
    var doc = this.range.commonAncestorContainer.ownerDocument
        , tw, previousNode
        , newRange = this.range.cloneRange();

    if (newRange.endContainer.nodeType !== Node.TEXT_NODE) {
        tw = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
        tw.currentNode = newRange.endContainer;
        previousNode = tw.previousNode();
        newRange.setEnd(previousNode, previousNode.length);
    }

    return newRange;
}

/**
 * get the direction of a selection
 *
 * 1 forward, -1 backward, 0 no direction, undefined one of the node is detached or in a different frame
 *
 * @return {Integer} can be -1, 0, 1 or undefined
 */
function TextSelection$$getDirection(){
    return domUtils.getSelectionDirection(this.selection);    
}