Source: view.js

var DOM = require("./utilities/dom"),
    extend = require("./utilities/extend"),
    replaceAll = require("./utilities/string-replace").all,
    escapeRegExp = require("./utilities/regexp").escapeRegExp;

module.exports = View;


/**
 * @constructor
 * @param {HTMLElement} container
 * @param {Object} templates - a set of templates into which we render munged data
 * @param {Object} options
 */
function View(container, templates, options) {
    this.container = container;
    this.templates = extend({}, templates);
    this.marginTop = options.marginTop;
    this.errMessage = options.errMessage;
}

/**
 * Creates view from data and calls View~_renderSuccess. If there are no data, calls View~_renderError.
 * @method
 * @param {array} data
 */
View.prototype.render = function(data) {
    var templates = this.templates,
        items,
        err,
        toRender;
    if (!data || !data.length) {
        err = templates.error.replace("{{message}}", this.errMessage);
        this.container.innerHTML = err;
    }
    else {
        items = data.map(this._renderLI, this).join("");
        this.container.innerHTML = templates.list.replace("{{choices}}", items);
    }
    return this;
};

/**
 * Renders listItem template with a datum as the context
 * @method
 * @private
 * @param {object} datum - A piece of data 
 */
View.prototype._renderLI = function(datum) {
    var template = this.templates.listItem;
    return this._renderWithContext(template, datum);
};

/**
 * Renders a template given the context of an object
 * @method
 * @private
 * @param {string} template
 * @param {object} o - Context for a template string.
 */
View.prototype._renderWithContext = function(template, o) {
    var prop,
        result = template;

    for (prop in o) {
        if (o.hasOwnProperty(prop)) {
            result = replaceAll(result, "{{"+prop+"}}", o[prop]);
        }
    }

    return result;
};

/**
 * Makes the popover disappear
 * @method
 * @param {Quill} quill
 * @param {Object} range
 */
View.prototype.hide = function hide(quill, range) {
    DOM.removeClass(this.container, "ql-is-mentioning");
    this.container.style.marginTop = "0";
    if (range) quill.setSelection(range);
    return this;
};

/**
 * @method
 * @returns {HTMLElement[]}
 */
View.prototype.getMatches = function getMatches() {
    return this.container.querySelectorAll("li");
};

/**
 * @method
 * @returns {HTMLElement[]}
 */
View.prototype.hasMatches = function hasMatches() {
    return this.getMatches().length > 0;
};

/**
 * Returns whether the popover is in view. I had bad feels about this method but it's coming in hand re: keyboard events right now.
 * @method
 * @returns {boolean}
 */
View.prototype.isHidden = function isHidden() {
    return !DOM.hasClass(this.container, "ql-is-mentioning");
};

/**
 * Adds an active class to the mentions popover and sits it beneath the cursor.
 * [TODO - add active class to config]
 * @method
 * @param {Quill} quill
 */
View.prototype.show = function show(quill) {

    this.container.style.marginTop = this._getTopMargin(quill);
    this.container.style.marginLeft = this._getLeftMargin(quill);
    DOM.addClass(this.container, "ql-is-mentioning"); // TODO - config active class
    this.container.focus(); // Does this even do anything? It would if we were using form elements instead of LIs prob

    return this;
};

/**
 * Return an array of dom nodes corresponding to all lines at or before the line corresponding to the current range.
 * @method
 * @private
 * @param {Range} range
 * @return {Node[]}
 */
View.prototype._findOffsetLines = function(quill) {
    var node = this._findMentionContainerNode(quill);
    return DOM.getOlderSiblingsInclusive(node);
};

/**
 * Return the DOM node that encloses the line on which current mention is being typed.
 * @method
 * @private
 * @param {Range} range
 * @return {Node|null}
 */
View.prototype._findMentionContainerNode = function _findMentionContainerNode(quill) {
    var range = quill.getSelection(),
        leafAndOffset,
        leaf,
        offset,
        node;
       
                
    leafAndOffset = quill.editor.doc.findLeafAt(range.start, true);
    leaf = leafAndOffset[0];
    offset = leafAndOffset[1]; // how many chars in front of current range
    if (leaf) node = leaf.node;
    while (node) {
        if (node.tagName === "DIV") break;
        node = node.parentNode;
    }
    if (!node) return null;
    return node;
};

/**
 * Return the (hopefully inline-positioned) DOM node that encloses the mention itself.
 * @method
 * @private
 * @param {Range} range
 * @return {Node|null}
 */
View.prototype._findMentionNode = function _findMentionNode(quill) {
    var range = quill.getSelection(),
        leafAndOffset,
        leaf,
        offset,
        node;
       
                
    leafAndOffset = quill.editor.doc.findLeafAt(range.start, true);
    leaf = leafAndOffset[0];
    offset = leafAndOffset[1]; // how many chars in front of current range
    if (leaf) node = leaf.node;
    while (node) {
        if (node.tagName === "SPAN") break;
        node = node.parentNode;
    }
    if (!node) return null;
    return node;
};

View.prototype._getLeftMargin = function(quill) {
    var mentionNode = this._findMentionNode(quill),
        mentionParent = this._findMentionContainerNode(quill);

    var mentionRect = mentionNode.getBoundingClientRect(),
        parentRect = mentionParent.getBoundingClientRect(),
        editorRect = quill.container.getBoundingClientRect();

    var marginLeft = mentionRect.left - parentRect.left;

    var overflow = marginLeft + mentionRect.width - editorRect.width;

    if (overflow > 0) {
        marginLeft -= (overflow);
    }

    return marginLeft + "px";
};

/**
 * @method
 * @private
 */
View.prototype._getTopMargin = function(quill) {
    var qlEditor = quill.editor.root,
        qlLines,
        marginTop = this.marginTop,
        negMargin = -marginTop,
        range;

    qlLines = this._findOffsetLines(quill);

    negMargin += this._nodeHeight(qlEditor);
    negMargin -= qlLines.reduce(function(total, line) {
        return total + this._nodeHeight(line);
    }.bind(this), 0);

    return "-" + negMargin + "px";
};

/**
 * @method
 * @private
 */
View.prototype._nodeHeight = function(node) {
    return node.getBoundingClientRect().height;
};


// TODO - write QuillEditor View
function QuillEditorView() {
    throw new Error("NYI");
}