(
        ( factory ) ->

            if typeof define is 'function' and define.amd then define factory

            else @Selection = do factory

    ).call @, ->

        _global = @

        _EVENTS = [ 'mouseup', 'keyup' ]

        _EMPTY_STRING = ''

        Selection = ( global ) ->

            global or global = _global

            if typeof global.getSelection isnt 'function' then return false

            @_selection = do global.getSelection

            @_global = global

            @_listener = => SelectionPrototype._listener.apply @, arguments

            @events = _EVENTS

            @

        SelectionPrototype = Selection:: = {}

        SelectionPrototype._listener = ( _event ) ->

            setTimeout =>

                    value = _EMPTY_STRING + @_selection

                    if value is @_previousValue then return

                    if value is _EMPTY_STRING
                        type = 'deselection'
                        data = {}

                    else
                        type = 'selection'
                        data = do @get

                    data.originalEvent = _event

                    event = @_global.document.createEvent 'CustomEvent'
                    event.initCustomEvent type, false, false, data

                    @_global.dispatchEvent event

                    @_previousValue = value

                , 0

            @

        SelectionPrototype.listen = ->

            @_previousValue = _EMPTY_STRING

            iterator = -1
            events = @events
            length = events.length
            document = @_global.document

            while ++iterator < length

                event = events[ iterator ]

                document.addEventListener event, @_listener, false

            @

        SelectionPrototype.ignore = ->

            iterator = -1
            events = @events
            length = events.length
            document = @_global.document

            while ++iterator < length

                event = events[ iterator ]

                document.removeEventListener event, @_listener, false

            @

        SelectionPrototype.has = ->

            selection = @_selection

            selection and selection.focusNode? and selection.anchorNode?

        SelectionPrototype.set = ( $top, topOffset, $bottom, bottomOffset ) ->

            selection = @_selection

            unless selection? then return false

            unless $top? then return false

            unless topOffset? then topOffset = 0

            if $bottom is $top or not $bottom?

                if $top.firstChild? and $top.lastChild?
                    $bottom = $top.lastChild
                    $top = $top.firstChild

                else
                    $bottom = $top

            unless bottomOffset?

                if @_isText $bottom
                    bottomOffset = $bottom.textContent.length

                else if $bottom.childNodes?
                    bottomOffset = $bottom.childNodes.length

                else
                    bottomOffset = 0

            if selection.collapse and selection.extend

                selection.collapse $top, topOffset

                selection.extend $bottom, bottomOffset

            else

                range = do @_global.document.createRange

                range.setStart $top, topOffset

                range.setEnd $bottom, bottomOffset

                try ( do selection.removeAllRanges ) catch

                selection.addRange range

            @

        SelectionPrototype.get = ->

            unless do @has then return false

            positions = do @_get

            positions.value = _EMPTY_STRING + @_selection

            positions

        SelectionPrototype.clear = ->

            try ( do @_selection.removeAllRanges ) catch

            @

        SelectionPrototype.getStart = ->

            selection = @_selection

            unless selection or not selection.anchorNode? then return false

            '$node' : selection.anchorNode
            'offset' : selection.anchorOffset

        SelectionPrototype.getEnd = ->

            selection = @_selection

            unless selection or not selection.focusNode? then return false

            '$node' : selection.focusNode
            'offset' : selection.focusOffset

        SelectionPrototype.getTop = ->

            positions = do @_get

            '$node' : positions.$top
            'offset' : positions.topOffset

        SelectionPrototype.getBottom = ->

            positions = do @_get

            '$node' : positions.$bottom
            'offset' : positions.bottomOffset

        SelectionPrototype._isPreceding = ( $firstNode, $secondNode ) ->
            $secondNode.compareDocumentPosition( $firstNode ) & 2

        SelectionPrototype._contains = ( $firstNode, $secondNode ) ->
            $firstNode.compareDocumentPosition( $secondNode ) & 16

        SelectionPrototype._isText = ( $node ) ->
            $node and $node.nodeType is 3

        SelectionPrototype._precedes = ( $top, top, $bottom, bottom ) ->

            if $top is $bottom then return top <= bottom

            if @_isText( $top ) and @_isText $bottom
                return @_isPreceding $top, $bottom

            if @_isText( $top ) and not @_isText $bottom
                return not @_precedes $bottom, bottom, $top, top

            unless @_contains $top, $bottom
                return @_isPreceding $top, $bottom

            if $top.childNodes.length <= top then return false

            if $top.childNodes[ top ] is $bottom then return 0 <= bottom

            @_isPreceding $top.childNodes[ top ], $bottom

        SelectionPrototype._get = ->

            unless do @has then return false

            start = do @getStart
            end = do @getEnd

            if @_precedes start.$node, start.offset, end.$node, end.offset
                top = start
                bottom = end

            else
                top = end
                bottom = start

            '$start' :  start.$node
            'startOffset' : start.offset
            '$end' : end.$node
            'endOffset' : end.offset
            '$top' : top.$node
            'topOffset' : top.offset
            '$bottom' : bottom.$node
            'bottomOffset' : bottom.offset

        return Selection
			

Selection

by @wooorm

Table of Contents

Selection.js is a JavaScript module wrapping around the DOM text selection API. The module was created with the intention to be used on articles for the purpose of allowing readers to notify the writer about editorial errors (style, grammar, &c.), and not intended to manipulate the selection (although) selection.js could be used for those means.

The module is pretty small (~7kb uncompressed, ~3kb compressed, ~1kb gzipped) and supports most modern browsers (Internet Explorer 9 and higher).

The module relies on the following methods:

Overall the module should work in Chrome, Firefox, IE 9, Opera 7, and Safari. Note that several well-implemented DOM level 2 methods are used (things like firstChild, and childNodes).

Instantiation

Authors can instantiate the module by calling Selection as a constructor with an optional window object. If the result of calling Selection equals false, getSelection was not available in the window object and the module can't work.

selection = new Selection

Here Selection is instantiated with the global object of the first frame.

selection = new Selection frames[ 0 ]

In the following example Selection is instantiated. If supported, we'll listen to selection and deselection events.

selection = new Selection

if selection
    do selection.listen
    # Some more code...

Events

The main feature of selection.js is it's ability to fire events for selections. Selection.js fires the selection event when the user selects text (by double clicking on a word for example, or when using the the arrow keys to extend a selection), and fires deselection when the user deselects text (e.g. when a click outside a selection occurs). These events are dispatched on the global space provided to the Selection constructor.

You can start listening to events by calling listen, and stop listening by calling ignore.

In the following example, we listen to selection events. When the first selection happens, we call console.log with the value of the selection. Following that, we stop listening to selection events.

do selection.listen

window.addEventListener 'selection', ( event ) ->
    console.log event.detail.value
    do selection.ignore

selection (Event)

The detail property on the event object passed to selection listeners contains returns a selection object (see "Get"), extended with:

deselection (Event)

The detail property on the event object passed to deselection listeners contains the following properties:

Methods

get (Method)

The get method on an instance of Selection returns an object containing the following properties:

getTop, getBottom, getStart, getEnd (Method)

The getTop, getBottom, getStart, and getEnd methods on an instance of Selection returns an object containing the following properties:

clear (Method)

The clear method removes a selection, and returns the instance of Selection.

has (Method)

The has method returns true when the global space provided to the Selection constructor has a selection, and false otherwise.

set (Method)

The set method allows the developer to programmatically set a selection, and returns the instance of Selection. Set takes the following arguments:

[*]: If $top has a first- and last child, $bottom will default to $top.lastChild and $top will be set to $top.firstChild, otherwise, $bottom will default to $top.

[**]: If $bottom is not a text node then bottomOffset will default to $bottom.childNodes.length, otherwise bottomOffset will default to $bottom.textContent.length.

Annotated Source

Table of Contents

Wrap the module in a method that either passes the constructor to define if AMD is available, or registers it on the current global space.

(
    ( factory ) ->

If define is of type "function", and amd of define is truthy, call define with factory.

        if typeof define is 'function' and define.amd then define factory

Otherwise, let @Selection be the result of calling factory.

        else @Selection = do factory

).call @, ->

Explicit Private variables

Let _global reference the current context.

    _global = @

Let _EVENTS be a list of possible selection change events.

    _EVENTS = [ 'mouseup', 'keyup' ]

Let _EMPTY_STRING be an empty string.

    _EMPTY_STRING = ''

Constructor

Let Selection be a constructor function, taking an optional context.

    Selection = ( global ) ->

If global is not passed, fall back to _global.

        global or global = _global

If the type of getSelection is not "function", selection is not supported, so we return false.

        if typeof global.getSelection isnt 'function' then return false

Let @_selection be the result of calling getSelection on global.

        @_selection = do global.getSelection

Let @global reference global.

        @_global = global

Bind context self to @_listener.

        @_listener = => SelectionPrototype._listener.apply @, arguments

Let @events reference EVENTS (by default).

        @events = _EVENTS

Return self.

        @

Prototype

    SelectionPrototype = Selection:: = {}

_listener [implicitly private]

_listener is a method used as a event listener, dispatching selection events when the selection of user changes.

    SelectionPrototype._listener = ( _event ) ->

The body of _listener is wrapped in setTimeout with timeout=0 (moving it to the end of the call-stack), because the actual selection changes only when bubbling is not prevented on the original event.

        setTimeout =>

Let value be the result of (implicitly) calling toString on the actual selection.

                value = _EMPTY_STRING + @_selection

If value equals @_previousValue, nothing has changed, so we return.

                if value is @_previousValue then return

If value equals an empty string, the user deselected a previous selection. Let type be "deselection" and data reference an empty object.

                if value is _EMPTY_STRING
                    type = 'deselection'
                    data = {}

Otherwise, Let type be "selection" and data reference the result of calling @get.

                else
                    type = 'selection'
                    data = do @get

Let originalEvent of data reference event_ (the original event).

                data.originalEvent = _event

let event be the result of calling createEvent with "CustomEvent", call initCustomEvent on event with type type, canBubble="false", cancelable=false, and data.

                event = @_global.document.createEvent 'CustomEvent'
                event.initCustomEvent type, false, false, data

Call dispatchEvent on @_global with event.

                @_global.dispatchEvent event

Let @_previousValue be value.

                @_previousValue = value

            , 0

Return self.

        @

listen [public]

A method attaching _listener to each event in @events on document.

    SelectionPrototype.listen = ->

Let @_previousValue be an empty string.

        @_previousValue = _EMPTY_STRING

Let iterator be -1, events reference @events, length be length of events, and document reference document of @_global.

        iterator = -1
        events = @events
        length = events.length
        document = @_global.document

While adding one to iterator is less than length...

        while ++iterator < length

Let event reference iterator of events.

            event = events[ iterator ]

Call addEventListener on document with event, @_listener, and false.

            document.addEventListener event, @_listener, false

Return self.

        @

ignore [public]

A method detaching _listener to each event in @events on document.

    SelectionPrototype.ignore = ->

Let iterator be -1, events reference @events, length be length of events, and document be document of @_global.

        iterator = -1
        events = @events
        length = events.length
        document = @_global.document

While adding one to iterator is less than length...

        while ++iterator < length

Let event reference iterator of events.

            event = events[ iterator ]

Call removeEventListener on document with event, @_listener, and false.

            document.removeEventListener event, @_listener, false

Return self.

        @

has [public]

A method detecting whether or not an actual selection is present.

    SelectionPrototype.has = ->

Let selection reference @_selection.

        selection = @_selection

Return whether selection is truthy, focusNode of selection is neither null nor undefined, and anchorNode of selection is neither null nor undefined.

        selection and selection.focusNode? and selection.anchorNode?

set [public]

A method setting an actual selection, taking the following arguments:

Let selection reference @_selection.

        selection = @_selection

If selection is either null or undefined, return false.

        unless selection? then return false

If $top is either null or undefined, return false.

        unless $top? then return false

If topOffset is either null or undefined, let topOffset be 0.

        unless topOffset? then topOffset = 0

If $bottom is $top, or if $bottom is either null or undefined...

        if $bottom is $top or not $bottom?

If $top.firstChild is neither null nor undefined and $top.firstChild is neither null nor undefined, let $bottom reference the first child of $top, and $top the last.

            if $top.firstChild? and $top.lastChild?
                $bottom = $top.lastChild
                $top = $top.firstChild

Else, let $bottom reference $top.

            else
                $bottom = $top

If bottomOffset is either null or undefined...

        unless bottomOffset?

If $bottom is a textNode, let bottomOffset be length of textContent of $bottom.

            if @_isText $bottom
                bottomOffset = $bottom.textContent.length

Otherwise, if childNodes of $bottom is neither null nor undefined, let bottomOffset be length of $bottom.childNodes.

            else if $bottom.childNodes?
                bottomOffset = $bottom.childNodes.length

Otherwise, let bottomOffset be 0.

            else
                bottomOffset = 0

If collapse of selection, and extend of selection are both truthy...

        if selection.collapse and selection.extend

Call collapse on selection with $top and topOffset to move both anchor and focus points of the selectionRange to the specified point.

            selection.collapse $top, topOffset

Call extend on selection with $bottom and bottomOffset to move the focus point of the current selectionRange to the specified point.

            selection.extend $bottom, bottomOffset

Else (for browsers like IE9, &c)...

        else

Let range be the result of calling createRange of document of @_global.

            range = do @_global.document.createRange

Call setStart on range with $top and topOffset.

            range.setStart $top, topOffset

Call setEnd on range with $bottom and bottomOffset.

            range.setEnd $bottom, bottomOffset

Try (as IE9 sometimes might throw) to call removeAllRanges on selection.

            try ( do selection.removeAllRanges ) catch

Call addRange on selection with range.

            selection.addRange range

Return self.

        @

get [public]

A method returning an object similar to an actual selection object.

    SelectionPrototype.get = ->

If the result of calling @has is falsey, return false.

        unless do @has then return false

Let positions be the result of calling @_get.

        positions = do @_get

Let value of positions be the result of (implicitly) calling toString on the actual selection.

        positions.value = _EMPTY_STRING + @_selection

Return the positions object.

        positions

clear [public]

Removes an actual selection.

    SelectionPrototype.clear = ->

Try (as IE9 sometimes might throw) to call removeAllRanges on @_selection.

        try ( do @_selection.removeAllRanges ) catch

Return self.

        @

getStart [public]

Returns the anchor of the actual selection.

    SelectionPrototype.getStart = ->

Let selection reference @_selection.

        selection = @_selection

If selection is falsey, or anchorNode of selection is either null or undefined, return false.

        unless selection or not selection.anchorNode? then return false

Return an object with properties:

getEnd [public]

Returns the end of the actual selection.

    SelectionPrototype.getEnd = ->

Let selection reference @_selection.

        selection = @_selection

If selection is falsey, or focusNode of selection is either null or undefined, return false.

        unless selection or not selection.focusNode? then return false

Return an object with properties:

getTop [public]

Return either focus or anchor of the actual selection, depending on which position comes first.

    SelectionPrototype.getTop = ->

Let positions be the result of calling @_get.

        positions = do @_get

Return an object containing:

getBottom [public]

Return either focus or anchor of the actual selection, depending on which position comes last.

    SelectionPrototype.getBottom = ->

Let positions be the result of calling @_get.

        positions = do @_get

Return an object containing:

_isPreceding [implicitly private]

Returns whether $firstNode precedes $secondNode.

    SelectionPrototype._isPreceding = ( $firstNode, $secondNode ) ->
        $secondNode.compareDocumentPosition( $firstNode ) & 2

_contains [implicitly private]

Returns whether $secondNode is positioned inside $secondNode.

    SelectionPrototype._contains = ( $firstNode, $secondNode ) ->
        $firstNode.compareDocumentPosition( $secondNode ) & 16

_isText [implicitly private]

Returns whether $node is truthy, and nodeType of $node is 3.

    SelectionPrototype._isText = ( $node ) ->
        $node and $node.nodeType is 3

_precedes [implicitly private]

Returns true if start is before end (with a bias to start if they are both the same).

    SelectionPrototype._precedes = ( $top, top, $bottom, bottom ) ->

If $top equals $bottom, return whether top is smaller than or equal to bottom.

        if $top is $bottom then return top <= bottom

If the results of calling both @_isText with $top, and @_isText with $bottom are both truthy, return whether $top is preceding $bottom.

        if @_isText( $top ) and @_isText $bottom
            return @_isPreceding $top, $bottom

If the result of calling @_isText with $top is truthy, return the inverse of calling @_precedes with $bottom, bottom, $top, and top.

        if @_isText( $top ) and not @_isText $bottom
            return not @_precedes $bottom, bottom, $top, top

If calling @_contains with $top and $bottom return truthy, return the result of calling @_isPreceding with $top and $bottom.

        unless @_contains $top, $bottom
            return @_isPreceding $top, $bottom

If length of childNodes of $top is smaller than or equal to top, return false.

        if $top.childNodes.length <= top then return false

If length of childNodes of $top is smaller than or equal to top, return false.

        if $top.childNodes[ top ] is $bottom then return 0 <= bottom

Return the result of calling @_isPreceding with top of childNodes of $top, and $bottom.

        @_isPreceding $top.childNodes[ top ], $bottom

_get [implicitly private]

Returns an object with:

If the result of calling @has is falsey, return false.

        unless do @has then return false

Let start be the result of calling @getStart, and end be the result of calling @getEnd.

        start = do @getStart
        end = do @getEnd

If calling @_precedes with $node of start, offset of start, $node of end, and offset of end is truthy, let top be start, and bottom be end.

        if @_precedes start.$node, start.offset, end.$node, end.offset
            top = start
            bottom = end

Otherwise, let top be end and bottom be start.

        else
            top = end
            bottom = start

Return an flattened object of start, end, top, and bottom.

        '$start' :  start.$node
        'startOffset' : start.offset
        '$end' : end.$node
        'endOffset' : end.offset
        '$top' : top.$node
        'topOffset' : top.offset
        '$bottom' : bottom.$node
        'bottomOffset' : bottom.offset

Return value

Return the constructor function Selection.

    return Selection