API Docs for: 3.11.0-git
Show:

File: src/sm-editor/js/extensions/editor-link.js

/*jshint expr:true, onevar:false */

/**
Provides the `Editor.Link` extension.

@module gallery-sm-editor
@submodule gallery-sm-editor-link
**/

/**
Extension for `Editor.Base` that enables inserting links

Provides support for the following commands:

- createLink
- unlink

@class Editor.Link
@extends Base
@extensionfor Editor.Base
**/

(function() {
var EditorLink = Y.Base.create('editorLink', Y.Base, [], {
    // -- Public Properties ----------------------------------------------------

    /**
    Hash of link commands supported by this editor.

    Names should correspond with valid `execCommand()` command names. Values
    are properties in the following format:

    @property {Object} linkCommands
        @param {Function|String} commandFn
        @param {Function|String} [queryFn]
    **/
    linkCommands: {
        createLink: {
            commandFn: '_createLink',
            queryFn:   'isLink'
        },

        unlink: {
            commandFn: '_unlink',
            queryFn:   'isLink'
        }
    },

    /**
    HTML tags supported by this editor. Unsupported tags will be treated
    as text

    @property {String} supportedTags
    **/
    linkTags: 'a',

    /**
    HTML Template for building an anchor node

    @property {Object} linkTemplate
    **/
    linkTemplate: '<a href="{href}" target="{target}"></a>',


    // -- Lifecycle ------------------------------------------------------------

    initializer: function () {
        this.commands = Y.merge(this.commands, this.linkCommands);

        if (this.supportedTags) {
            this.supportedTags += ',' + this.linkTags;
        } else {
            this.supportedTags = this.linkTags;
        }
    },


    // -- Public Methods -------------------------------------------------------

    /**
    Returns whether or not the current range is entirely in an anchor element

    @method isLink
    @return {boolean} `true` if the range is contained in an anchor element,
      `false` otherwise
    **/
    isLink: function () {
        return !!this._getAnchorNode();
    },


    // -- Protected Methods ----------------------------------------------------

    /**
    Returns the nearest ancestor anchor that entirely contains
    the current range

    @method _getAnchorNode
    @return {Node} The containing anchor element
    @protected
    **/
    _getAnchorNode: function() {
        this.focus();

        var parentNode = this.selection.range().shrink().parentNode();

        return parentNode.ancestor(this.linkTags, true);
    },


    /**
    Implementation for the `createLink` command

    Wraps the currently selected range in an anchor `<a>` tag

    @method _createLink
    @param {Object} options
        @param {String} options.href
        @param {String} [options.target=_self]
        @param {String} [options.text]
    @protected
    **/
    _createLink: function(options){
        var range = this.selection.range(),
            anchorNode, styleNodes;

        if (!range) {
            return;
        }

        if (this.isLink()) {
            this._unlink();
            range = this.selection.range();
        }

        options || (options = {});
        options.href = encodeURI(options.href || '');
        options.target = encodeURIComponent(options.target || '_self');

        anchorNode = Y.Node.create(Y.Lang.sub(this.linkTemplate, options));
        styleNodes = this._getStyleNodes(range);

        anchorNode.append(styleNodes);

        range.insertNode(anchorNode);

        if (options.text && options.text !== range.toString()) {
            var firstChild = anchorNode.get('firstChild');

            if (this._isStyleNode(firstChild)) {
                firstChild.set('text', options.text);
                anchorNode.setHTML(firstChild);
            } else {
                anchorNode.set('text', options.text);
            }
        }

        range.selectNode(anchorNode).collapse();

        this.selection.select(range);
    },


    /**
    Removes link by replacing the anchor element with the child nodes
    of the anchor

    The anchor element will be removed from the DOM and destroyed.

    @method _unlink
    @protected
    **/
    _unlink: function() {
        var selection = this.selection,
            anchorNode;

        // we can use the native unlink command once we have bookmarking
        // in place, but firefox selects adjacent text nodes after unlink

        if (anchorNode = this._getAnchorNode()) {
            var firstChild = anchorNode.get('firstChild'),
                lastChild = anchorNode.get('lastChild'),
                range = selection.range();

            // only need to unwrap one of the children to unwrap the
            // whole anchorNode
            firstChild.unwrap();

            anchorNode.destroy();

            range.startNode(firstChild, 0);
            range.endNode(lastChild, 'after');

            selection.select(range.shrink({trim: true}));
        }
    }
});

Y.namespace('Editor').Link = EditorLink;

}());