/*
The MIT License (MIT)

Copyright (c) 2014 Jeff Collins

Changes:
1. 2017-05-10 Edmond Meng
Add if condition in getTextPrecedingCurrentSelection function to support
non-contenteditable elements in contenteditable element;
Add space button to trigger selection;
2. 2017-05-17 Edmond Meng
Remove operation to add \xA0 after replaced text. We do not need the character.
3. 2017-06-02 Edmond Meng
Modify $watch('isVisible()') to make sure mentio-menu is hidden once mentio input init.
4. 2017-06-06 Edmond Meng
Modify $log.error to $log.debug to avoid error output while changing mention-input
from editable to non-editable.
5. 2017-07-13 Edmond Meng
Modify completely, to support new feature. Maybe refactor is required if time is enough.
*/

'use strict';

angular.module('mentio', [])
    .directive('mentio', ['mentioUtil', '$document', '$compile', '$log', '$timeout',
        function (mentioUtil, $document, $compile, $log, $timeout) {
        return {
            restrict: 'A',
            scope: {
                macros: '=mentioMacros',
                search: '&mentioSearch',
                select: '&mentioSelect',
                items: '=mentioItems',
                autoSearch: '&mentioAutoSearch',
                autoSelect: '&mentioAutoSelect',
                autoItems: '=mentioAutoItems',
                typedTerm: '=mentioTypedTerm',
                rangeInfo: '=mentioRangeInfo',
                altId: '=mentioId',
                iframeElement: '=mentioIframeElement',
                requireLeadingSpace: '=mentioRequireLeadingSpace',
                selectNotFound: '=mentioSelectNotFound',
                trimTerm: '=mentioTrimTerm',
                ngModel: '='
            },
            controller: ["$scope", "$timeout", "$attrs", "enums", function ($scope, $timeout, $attrs, enums) {

                $scope.query = function (triggerChar, triggerText) {
                    var remoteScope = $scope.triggerCharMap[triggerChar];

                    if ($scope.trimTerm === undefined || $scope.trimTerm) {
                        triggerText = triggerText.trim();
                    }

                    remoteScope.showMenu();

                    remoteScope.search({
                        term: triggerText
                    });

                    $scope.typedTerm = triggerText;
                };

                $scope.autoTriggerQuery = function (triggerText) {
                    var remoteScope = $scope.autoTriggerMenu;

                    if ($scope.trimTerm === undefined || $scope.trimTerm) {
                        triggerText = triggerText.trim();
                    }

                    remoteScope.showMenu();

                    var query = remoteScope.search({
                        term: triggerText
                    });
                    
                    if (typeof query.then === 'function') {
                        /* query is a promise, at least our best guess */
                        query.then(function (result) {
                            if (!result || result.length === 0) {
                                $scope.typedTerm = '';
                            }
                            else {
                                $scope.typedTerm = triggerText;
                            }
                        });
                    } else {
                        $scope.typedTerm = triggerText;
                    }
                };

                $scope.defaultSearch = function(locals) {
                    var results = [];
                    angular.forEach($scope.items, function(item) {
                        if (item.label.toUpperCase().indexOf(locals.term.toUpperCase()) >= 0) {
                            results.push(item);
                        }
                    });
                    $scope.localItems = results;
                };

                $scope.bridgeSearch = function(termString) {
                    var searchFn = $attrs.mentioSearch ? $scope.search : $scope.defaultSearch;
                    return searchFn({
                        term: termString
                    });
                };

                $scope.bridgeAutoTriggerSearch = function (termString) {
                    return $scope.autoSearch({
                        term: termString
                    });
                };

                $scope.defaultSelect = function(locals) {
                    return $scope.defaultTriggerChar + locals.item.label;
                };

                $scope.bridgeSelect = function(itemVar) {
                    var selectFn = $attrs.mentioSelect ? $scope.select : $scope.defaultSelect;
                    return selectFn({
                        item: itemVar
                    });
                };

                $scope.bridgeAutoTriggerSelect = function (itemVar) {
                    return $scope.autoSelect({
                        item: itemVar
                    });
                };

                $scope.setTriggerText = function(text) {
                    if ($scope.syncTriggerText) {
                        $scope.typedTerm = ($scope.trimTerm === undefined || $scope.trimTerm) ? text.trim() : text;
                    }
                };

                $scope.context = function() {
                    if ($scope.iframeElement) {
                        return {iframe: $scope.iframeElement};
                    }
                };

                $scope.replaceText = function (text, hasTrailingSpace) {
                    $scope.hideAll();

                    if ($scope.currentMentionTriggerChar) {
                        mentioUtil.replaceTriggerText($scope.context(), $scope.targetElement, $scope.targetElementPath,
                            $scope.targetElementSelectedOffset, $scope.triggerCharSet, text, $scope.requireLeadingSpace,
                            hasTrailingSpace);

                        if (!hasTrailingSpace) {
                            $scope.setTriggerText('');
                            angular.element($scope.targetElement).triggerHandler('change');
                            if ($scope.isContentEditable()) {
                                $scope.contentEditableMenuPasted = true;
                                var timer = $timeout(function () {
                                    $scope.contentEditableMenuPasted = false;
                                }, 200);
                                $scope.$on('$destroy', function () {
                                    $timeout.cancel(timer);
                                });
                            }
                        }
                    }
                    else {
                        mentioUtil.replaceAutoTriggerText($scope.targetElement, $scope.targetElementPath,
                            $scope.targetElementSelectedOffset, $scope.triggerCharSet, text);
                    }

                    mentioUtil.clearEmptyTextNode($scope.targetElement);
                    $scope.rangeInfo = mentioUtil.getContentEditableSelectedPath($scope.context());
                };

                $scope.hideAll = function () {
                    for (var key in $scope.triggerCharMap) {
                        if ($scope.triggerCharMap.hasOwnProperty(key)) {
                            $scope.triggerCharMap[key].hideMenu();
                        }
                    }
                    if ($scope.autoTriggerMenu) {
                        $scope.autoTriggerMenu.hideMenu();
                    }
                };

                $scope.getActiveMenuScope = function () {
                    for (var key in $scope.triggerCharMap) {
                        if ($scope.triggerCharMap.hasOwnProperty(key)) {
                            if ($scope.triggerCharMap[key].visible) {
                                return $scope.triggerCharMap[key];
                            }
                        }
                    }
                    if ($scope.autoTriggerMenu && $scope.autoTriggerMenu.visible) {
                        return $scope.autoTriggerMenu;
                    }
                    return null;
                };

                $scope.selectActive = function () {
                    for (var key in $scope.triggerCharMap) {
                        if ($scope.triggerCharMap.hasOwnProperty(key)) {
                            if ($scope.triggerCharMap[key].visible) {
                                $scope.triggerCharMap[key].selectActive();
                            }
                        }
                    }
                    if ($scope.autoTriggerMenu && $scope.autoTriggerMenu.visible) {
                        $scope.autoTriggerMenu.selectActive();
                    }
                };

                $scope.isActive = function () {
                    for (var key in $scope.triggerCharMap) {
                        if ($scope.triggerCharMap.hasOwnProperty(key)) {
                            if ($scope.triggerCharMap[key].visible) {
                                return true;
                            }
                        }
                    }
                    if ($scope.autoTriggerMenu && $scope.autoTriggerMenu.visible) {
                        return true;
                    }
                    return false;
                };

                $scope.isContentEditable = function() {
                    return ($scope.targetElement.nodeName !== 'INPUT' && $scope.targetElement.nodeName !== 'TEXTAREA');
                };

                $scope.replaceMacro = function(macro, hasTrailingSpace) {
                    if (!hasTrailingSpace) {
                        $scope.replacingMacro = true;
                        $scope.timer = $timeout(function() {
                            mentioUtil.replaceMacroText($scope.context(), $scope.targetElement,
                                $scope.targetElementPath, $scope.targetElementSelectedOffset,
                                $scope.macros, $scope.macros[macro]);
                            angular.element($scope.targetElement).triggerHandler('change');
                            $scope.replacingMacro = false;
                        }, 300);
                        $scope.$on('$destroy', function() {
                            $timeout.cancel($scope.timer);
                        });
                    } else {
                        mentioUtil.replaceMacroText($scope.context(), $scope.targetElement, $scope.targetElementPath,
                            $scope.targetElementSelectedOffset, $scope.macros, $scope.macros[macro]);
                    }

                    mentioUtil.clearEmptyTextNode($scope.targetElement);
                    $scope.rangeInfo = mentioUtil.getContentEditableSelectedPath($scope.context());
                };

                $scope.addMenu = function(menuScope) {
                    if (menuScope.parentScope
                        && (menuScope.triggerChar && $scope.triggerCharMap.hasOwnProperty(menuScope.triggerChar)
                        || !menuScope.triggerChar && $scope.autoTriggerMenu)) {
                        return;
                    }
                    if (menuScope.triggerChar) {
                        $scope.triggerCharMap[menuScope.triggerChar] = menuScope;
                        if ($scope.triggerCharSet === undefined) {
                            $scope.triggerCharSet = [];
                        }
                        $scope.triggerCharSet.push(menuScope.triggerChar);
                    }
                    else {
                        $scope.autoTriggerMenu = menuScope;
                    }

                    menuScope.setParent($scope);
                };

                $scope.$on(
                    'menuCreated', function (event, data) {
                        if (
                            $attrs.id !== undefined ||
                            $attrs.mentioId !== undefined
                        )
                        {
                            if (
                                $attrs.id === data.targetElement ||
                                (
                                    $attrs.mentioId !== undefined &&
                                    $scope.altId === data.targetElement
                                )
                            )
                            {
                                $scope.addMenu(data.scope);
                            }
                        }
                    }
                );

                $document.on(
                    'click', function () {
                        if ($scope.isActive()) {
                            $scope.$apply(function () {
                                $scope.hideAll();
                            });
                        }
                    }
                );

                $document.on(
                    'keydown keypress paste', function (event) {
                        var activeMenuScope = $scope.getActiveMenuScope();
                        if (activeMenuScope) {
                            // Edmond Meng: Add Space button to trigger selection
                            if (event.which === enums.keyCode.tab
                                || event.which === enums.keyCode.enter
                                || event.which === enums.keyCode.space) {
                                event.preventDefault();
                                activeMenuScope.selectActive();
                            }

                            if (event.which === enums.keyCode.esc) {
                                event.preventDefault();
                                activeMenuScope.$apply(function () {
                                    activeMenuScope.hideMenu();
                                });
                            }

                            if (event.which === enums.keyCode.down) {
                                // Since both key down and '(' are in code 40, handle this scenario
                                if (!event.originalEvent || event.originalEvent.key !== '(') {
                                    event.preventDefault();
                                    activeMenuScope.$apply(function () {
                                        activeMenuScope.activateNextItem();
                                    });
                                    activeMenuScope.adjustScroll(1);
                                }
                                else {
                                    event.preventDefault();
                                    activeMenuScope.selectActive();
                                }
                            }

                            if (event.which === enums.keyCode.select) {
                                if (event.originalEvent && event.originalEvent.key === ')') {
                                    event.preventDefault();
                                    activeMenuScope.selectActive();
                                }
                            }

                            if (event.which === enums.keyCode.up) {
                                // Since both key up and '&' are in code 38, handle this scenario
                                if (!event.originalEvent || event.originalEvent.key !== '&') {
                                    event.preventDefault();
                                    activeMenuScope.$apply(function () {
                                        activeMenuScope.activatePreviousItem();
                                    });
                                    activeMenuScope.adjustScroll(-1);
                                }
                            }

                            if (event.which === enums.keyCode.left || event.which === enums.keyCode.right) {
                                // Both key left and '%' are in code 37; Both key right and '\'' are in code 39
                                if (!event.originalEvent || event.originalEvent.key !== '%' && event.originalEvent.key !== '\'') {
                                    event.preventDefault();
                                }
                            }
                        }
                    }
                );
            }],
            link: function (scope, element, attrs) {
                scope.triggerCharMap = {};
                scope.autoTriggerMenu;
                scope.rangeInfo;

                scope.targetElement = element;
                attrs.$set('autocomplete', 'off');

                if (attrs.mentioItems) {
                    scope.localItems = [];
                    scope.parentScope = scope;
                    var itemsRef = attrs.mentioSearch ? ' mentio-items="items"' : ' mentio-items="localItems"';

                    scope.defaultTriggerChar = attrs.mentioTriggerChar ? scope.$eval(attrs.mentioTriggerChar) : '@';

                    var htmlComm = '<mentio-menu' +
                        ' mentio-search="bridgeSearch(term)"' +
                        ' mentio-select="bridgeSelect(item)"' +
                        itemsRef;

                    if (attrs.mentioTemplateUrl) {
                        htmlComm = htmlComm + ' mentio-template-url="' + attrs.mentioTemplateUrl + '"';
                    }
                    htmlComm = htmlComm + ' mentio-trigger-char="\'' + scope.defaultTriggerChar + '\'"' +
                        ' mentio-parent-scope="parentScope"' +
                        '/>';
                    var linkFnComm = $compile(htmlComm);
                    var elComm = linkFnComm(scope);

                    var htmlAuto = '<mentio-menu' +
                        ' mentio-search="bridgeAutoTriggerSearch(term)"' +
                        ' mentio-select="bridgeAutoTriggerSelect(item)"' +
                        ' mentio-items="autoItems"';

                    if (attrs.mentioTemplateUrl) {
                        htmlAuto = htmlAuto + ' mentio-template-url="' + attrs.mentioTemplateUrl + '"';
                    }
                    htmlAuto = htmlAuto + ' mentio-avoid-trigger-char="\'' + scope.defaultTriggerChar + '\'"' +
                        ' mentio-parent-scope="parentScope"' + '/>';
                    var linkFnAuto = $compile(htmlAuto);
                    var elAuto = linkFnAuto(scope);

                    element.parent().append(elComm);
                    element.parent().append(elAuto);

                    scope.$on('$destroy', function() {
                        elComm.remove();
                        elAuto.remove();
                    });
                }

                if (attrs.mentioTypedTerm) {
                    scope.syncTriggerText = true;
                }

                function keyHandler(event) {
                    function stopEvent(event) {
                        event.preventDefault();
                        event.stopPropagation();
                        event.stopImmediatePropagation();
                    }
                    var activeMenuScope = scope.getActiveMenuScope();
                    if (activeMenuScope) {
                        if (event.which === enums.keyCode.tab
                            || event.which === enums.keyCode.enter
                            || event.which === enums.keyCode.space) {
                            stopEvent(event);
                            activeMenuScope.selectActive();
                            return false;
                        }

                        if (event.which === enums.keyCode.esc) {
                            stopEvent(event);
                            activeMenuScope.$apply(function () {
                                activeMenuScope.hideMenu();
                            });
                            return false;
                        }

                        if (event.which === enums.keyCode.down) {
                            // Since both key down and '(' are in code 40, handle this scenario
                            if (!event.originalEvent || event.originalEvent.key !== '(') {
                                stopEvent(event);
                                activeMenuScope.$apply(function () {
                                    activeMenuScope.activateNextItem();
                                });
                                activeMenuScope.adjustScroll(1);
                            }
                            else {
                                stopEvent(event);
                                activeMenuScope.selectActive();
                            }

                            return false;
                        }

                        if (event.which === enums.keyCode.select) {
                            if (event.originalEvent && event.originalEvent.key === ')') {
                                stopEvent(event);
                                activeMenuScope.selectActive();
                                return false;
                            }
                        }

                        if (event.which === enums.keyCode.up) {
                            // Since both key up and '&' are in code 38, handle this scenario
                            if (!event.originalEvent || event.originalEvent.key !== '&') {
                                stopEvent(event);
                                activeMenuScope.$apply(function () {
                                    activeMenuScope.activatePreviousItem();
                                });
                                activeMenuScope.adjustScroll(-1);
                                return false;
                            }
                        }

                        if (event.which === enums.keyCode.left || event.which === enums.keyCode.right) {
                            // Both key left and '%' are in code 37; Both key right and '\'' are in code 39
                            if (!event.originalEvent || event.originalEvent.key !== '%' && event.originalEvent.key !== '\'') {
                                stopEvent(event);
                                return false;
                            }
                        }
                    }
                }

                scope.$watch(
                    'iframeElement', function(newValue) {
                        if (newValue) {
                            var iframeDocument = newValue.contentWindow.document;
                            iframeDocument.addEventListener('click',
                                function () {
                                    if (scope.isActive()) {
                                        scope.$apply(function () {
                                            scope.hideAll();
                                        });
                                    }
                                }
                            );


                            iframeDocument.addEventListener('keydown', keyHandler, true /*capture*/);

                            scope.$on ( '$destroy', function() {
                                iframeDocument.removeEventListener ( 'keydown', keyHandler );
                            });
                        }
                    }
                );

                scope.$watch(
                    'ngModel',
                    function (newValue) {
                        /*jshint maxcomplexity:14 */
                        /*jshint maxstatements:39 */
                        // yes this function needs refactoring
                        if ((!newValue || newValue === '') && !scope.isActive()) {
                            // ignore while setting up
                            return;
                        }

                        if (scope.triggerCharSet === undefined) {
                            $log.debug('No mentio-items attribute was provided, ' +
                                'and no separate mentio-menus were specified.  Nothing to do.');
                            return;
                        }

                        if (scope.contentEditableMenuPasted) {
                            // don't respond to changes from insertion of the menu content
                            scope.contentEditableMenuPasted = false;
                            return;
                        }

                        if (scope.replacingMacro) {
                            $timeout.cancel(scope.timer);
                            scope.replacingMacro = false;
                        }

                        var isActive = scope.isActive();
                        var isContentEditable = scope.isContentEditable();

                        var mentionInfo = mentioUtil.getTriggerInfo(scope.context(), scope.triggerCharSet,
                            scope.requireLeadingSpace, isActive);
                        var autoMentionInfo = mentioUtil.getAutoTriggerInfo(scope.triggerCharSet, isActive);

                        if (mentionInfo !== undefined
                            && mentionInfo.mentionSelectedPath.length <= 1
                            &&  (
                                    !isActive ||
                                    (isActive &&
                                        (
                                            /* content editable selection changes to local nodes which
                                            modifies the start position of the selection over time,
                                            just consider triggerchar changes which
                                            will have the odd effect that deleting a trigger char pops
                                            the menu for a previous
                                            trigger char sequence if one exists in a content editable */
                                            (isContentEditable && mentionInfo.mentionTriggerChar ===
                                                scope.currentMentionTriggerChar) ||
                                            (!isContentEditable && mentionInfo.mentionPosition ===
                                                scope.currentMentionPosition)
                                        )
                                    )
                                )
                            )
                        {
                            /** save selection info about the target control for later re-selection */
                            if (mentionInfo.mentionSelectedElement) {
                                scope.targetElement = mentionInfo.mentionSelectedElement;
                                scope.targetElementPath = mentionInfo.mentionSelectedPath;
                                scope.targetElementSelectedOffset = mentionInfo.mentionSelectedOffset;
                            }

                            /* publish to external ngModel */
                            scope.setTriggerText(mentionInfo.mentionText);
                            /* remember current position */
                            scope.currentMentionPosition = mentionInfo.mentionPosition;
                            scope.currentMentionTriggerChar = mentionInfo.mentionTriggerChar;
                            /* perform query */
                            scope.query(mentionInfo.mentionTriggerChar, mentionInfo.mentionText);
                        } else if (autoMentionInfo !== undefined
                                   && autoMentionInfo.mentionSelectedPath.length <= 1
                                   && isContentEditable
                                      //(
                                      //    !isActive ||
                                      //    (isActive &&
                                      //        (
                                      //            autoTriggerChars.indexOf(autoMentionInfo.mentionTriggerChar) >= 0
                                      //        )
                                      //    )
                                      //)
                                  )
                        {
                            /** save selection info about the target control for later re-selection */
                            if (autoMentionInfo.mentionSelectedElement) {
                                scope.targetElement = autoMentionInfo.mentionSelectedElement;
                                scope.targetElementPath = autoMentionInfo.mentionSelectedPath;
                                scope.targetElementSelectedOffset = autoMentionInfo.mentionSelectedOffset;
                            }

                            /* publish to external ngModel */
                            scope.setTriggerText(autoMentionInfo.mentionText);
                            /* remember current position */
                            scope.currentMentionPosition = autoMentionInfo.mentionPosition;
                            scope.currentMentionTriggerChar = autoMentionInfo.mentionTriggerChar;
                            /* perform query */
                            scope.autoTriggerQuery(autoMentionInfo.mentionText);
                        } else {
                            var currentTypedTerm = scope.typedTerm;
                            scope.setTriggerText('');
                            scope.hideAll();

                            var macroMatchInfo = mentioUtil.getMacroMatch(scope.context(), scope.macros);

                            if (macroMatchInfo !== undefined) {
                                scope.targetElement = macroMatchInfo.macroSelectedElement;
                                scope.targetElementPath = macroMatchInfo.macroSelectedPath;
                                scope.targetElementSelectedOffset = macroMatchInfo.macroSelectedOffset;
                                scope.replaceMacro(macroMatchInfo.macroText, macroMatchInfo.macroHasTrailingSpace);
                            } else if (scope.selectNotFound && currentTypedTerm && currentTypedTerm !== '') {
                                var lastScope;
                                if (scope.currentMentionTriggerChar === undefined || scope.currentMentionTriggerChar === null) {
                                    lastScope = scope.autoTriggerMenu;
                                }
                                else {
                                    lastScope = scope.triggerCharMap[scope.currentMentionTriggerChar];
                                }

                                if (lastScope) {
                                    // just came out of typeahead state
                                    var text = lastScope.select({
                                        item: {label: currentTypedTerm}
                                    });
                                    //if (typeof text.then === 'function') {
                                    //    /* text is a promise, at least our best guess */
                                    //    text.then(scope.replaceText);
                                    //} else {
                                    //    scope.replaceText(text, true);
                                    //}
                                }
                            }
                        }
                    }
                );
            }
        };
    }])

    .directive('mentioMenu', ['mentioUtil', '$rootScope', '$log', '$window', '$document',
        function (mentioUtil, $rootScope, $log, $window, $document) {
        return {
            restrict: 'E',
            scope: {
                search: '&mentioSearch',
                select: '&mentioSelect',
                items: '=mentioItems',
                triggerChar: '=mentioTriggerChar',
                avoidTriggerChar: '=mentioAvoidTriggerChar',
                forElem: '=mentioFor',
                parentScope: '=mentioParentScope'
            },
            templateUrl: function(tElement, tAttrs) {
                return tAttrs.mentioTemplateUrl !== undefined ? tAttrs.mentioTemplateUrl : 'mentio-menu.tpl.html';
            },
            controller: ["$scope", function ($scope) {
                $scope.visible = false;

                // callable both with controller (menuItem) and without controller (local)
                this.activate = $scope.activate = function (item) {
                    $scope.activeItem = item;
                };

                // callable both with controller (menuItem) and without controller (local)
                this.isActive = $scope.isActive = function (item) {
                    return $scope.activeItem === item;
                };

                // callable both with controller (menuItem) and without controller (local)
                this.selectItem = $scope.selectItem = function (item) {
                    var text = $scope.select({
                        item: item
                    });
                    if (typeof text.then === 'function') {
                        /* text is a promise, at least our best guess */
                        text.then($scope.parentMentio.replaceText);
                    } else {
                        $scope.parentMentio.replaceText(text);
                    }
                };

                $scope.activateNextItem = function () {
                    var length = $scope.items.length > 10 ? 10 : $scope.items.length;
                    var index = $scope.items.indexOf($scope.activeItem);
                    this.activate($scope.items[(index + 1) % length]);
                    //this.activate($scope.items[(index + 1) % $scope.items.length]);
                };

                $scope.activatePreviousItem = function () {
                    var length = $scope.items.length > 10 ? 10 : $scope.items.length;
                    var index = $scope.items.indexOf($scope.activeItem);
                    this.activate($scope.items[index === 0 ? length - 1 : index - 1]);
                    //this.activate($scope.items[index === 0 ? $scope.items.length - 1 : index - 1]);
                };

                $scope.isFirstItemActive = function () {
                    var index = $scope.items.indexOf($scope.activeItem);

                    return index === 0;
                };

                $scope.isLastItemActive = function () {
                    var index = $scope.items.indexOf($scope.activeItem);

                    return index === ($scope.items.length - 1);
                };

                $scope.selectActive = function () {
                    $scope.selectItem($scope.activeItem);
                };

                $scope.isVisible = function () {
                    return $scope.visible;
                };

                $scope.showMenu = function () {
                    if (!$scope.visible) {
                        $scope.requestVisiblePendingSearch = true;
                    }
                };

                $scope.setParent = function (scope) {
                    $scope.parentMentio = scope;
                    $scope.targetElement = scope.targetElement;
                };
            }],

            link: function (scope, element) {
                element[0].parentNode.removeChild(element[0]);
                $document[0].body.appendChild(element[0]);
                scope.menuElement = element; // for testing

                if (scope.parentScope) {
                    scope.parentScope.addMenu(scope);
                } else {
                    if (!scope.forElem) {
                        $log.error('mentio-menu requires a target element in tbe mentio-for attribute');
                        return;
                    }
                    if (!scope.triggerChar) {
                        $log.debug('Auto trigger menu.');
                    }
                    else {
                        $log.debug(scope.triggerChar + ' trigger menu.');
                    }
                    // send own scope to mentio directive so that the menu
                    // becomes attached
                    $rootScope.$broadcast('menuCreated',
                        {
                            targetElement : scope.forElem,
                            scope : scope
                        });
                }

                angular.element($window).bind(
                    'resize', function () {
                        if (scope.isVisible()) {
                            if (!scope.triggerChar) {
                                var avoidTriggerCharSet = [];
                                if (scope.avoidTriggerChar !== undefined && scope.avoidTriggerChar !== null) {
                                    avoidTriggerCharSet.push(scope.avoidTriggerChar);
                                }
                                mentioUtil.popAutoTriggerUnderMention(avoidTriggerCharSet, element);
                            }
                            else {
                                var triggerCharSet = [];
                                triggerCharSet.push(scope.triggerChar);
                                mentioUtil.popUnderMention(scope.parentMentio.context(),
                                    triggerCharSet, element, scope.requireLeadingSpace);
                            }
                        }
                    }
                );

                scope.$watch('items', function (items) {
                    if (items && items.length > 0) {
                        scope.activate(items[0]);
                        if (!scope.visible && scope.requestVisiblePendingSearch) {
                            scope.visible = true;
                            scope.requestVisiblePendingSearch = false;
                        }
                    } else {
                        scope.hideMenu();
                    }
                });

                scope.$watch('isVisible()', function (visible) {
                    // wait for the watch notification to show the menu
                    if (visible) {
                        if (!scope.triggerChar) {
                            var avoidTriggerCharSet = [];
                            if (scope.avoidTriggerChar !== undefined && scope.avoidTriggerChar !== null) {
                                avoidTriggerCharSet.push(scope.avoidTriggerChar);
                            }
                            mentioUtil.popAutoTriggerUnderMention(avoidTriggerCharSet, element);
                        }
                        else {
                            var triggerCharSet = [];
                            triggerCharSet.push(scope.triggerChar);
                            mentioUtil.popUnderMention(scope.parentMentio.context(),
                                triggerCharSet, element, scope.requireLeadingSpace);
                        }
                    }
                    // Edmond Meng: Add to make sure mentio-menu is hidden once mentio input init.
                    else if (element.css('display') !== 'none') {
                        element.css('display', 'none');
                    }
                });

                scope.parentMentio.$on('$destroy', function () {
                    element.remove();
                });

                scope.hideMenu = function () {
                    scope.visible = false;
                    scope.$parent.typedTerm = '';
                    element.css('display', 'none');
                };

                scope.adjustScroll = function (direction) {
                    var menuEl = element[0];
                    var menuItemsList = menuEl.querySelector('ul');
                    var menuItem = (menuEl.querySelector('[mentio-menu-item].active') || 
                        menuEl.querySelector('[data-mentio-menu-item].active'));

                    if (scope.isFirstItemActive()) {
                        return menuItemsList.scrollTop = 0;
                    } else if(scope.isLastItemActive()) {
                        return menuItemsList.scrollTop = menuItemsList.scrollHeight;
                    }

                    if (menuItem) {
                        if (direction === 1) {
                            menuItemsList.scrollTop += menuItem.offsetHeight;
                        } else {
                            menuItemsList.scrollTop -= menuItem.offsetHeight;
                        }
                    }
                };

            }
        };
    }])

    .directive('mentioMenuItem', function () {
        return {
            restrict: 'A',
            scope: {
                item: '=mentioMenuItem'
            },
            require: '^mentioMenu',
            link: function (scope, element, attrs, controller) {

                scope.$watch(function () {
                    return controller.isActive(scope.item);
                }, function (active) {
                    if (active) {
                        element.addClass('active');
                    } else {
                        element.removeClass('active');
                    }
                });

                element.bind('mouseenter', function () {
                    scope.$apply(function () {
                        controller.activate(scope.item);
                    });
                });

                element.bind('click', function () {
                    controller.selectItem(scope.item);
                    return false;
                });
            }
        };
    })
    .filter('unsafe', ["$sce", function($sce) {
        return function (val) {
            return $sce.trustAsHtml(val);
        };
    }])
    .filter('mentioHighlight', function() {
        function escapeRegexp (queryToEscape) {
            return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
        }

        return function (matchItem, query, hightlightClass) {
            if (query) {
                var replaceText = hightlightClass ?
                                 '<span class="' + hightlightClass + '">$&</span>' :
                                 '<strong>$&</strong>';
                return ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), replaceText);
            } else {
                return matchItem;
            }
        };
    });

'use strict';

angular.module('mentio')
    .factory('mentioUtil', ["$window", "$location", "$anchorScroll", "$timeout", "enums",
    function ($window, $location, $anchorScroll, $timeout, enums) {

        // public
        function popUnderMention (ctx, triggerCharSet, selectionEl, requireLeadingSpace) {
            var coordinates;
            var mentionInfo = getTriggerInfo(ctx, triggerCharSet, requireLeadingSpace, false);

            if (mentionInfo !== undefined) {

                if (selectedElementIsTextAreaOrInput(ctx)) {
                    coordinates = getTextAreaOrInputUnderlinePosition(ctx, getDocument(ctx).activeElement,
                        mentionInfo.mentionPosition);
                } else {
                    coordinates = getContentEditableCaretPosition(ctx, mentionInfo.mentionPosition);
                }

                // Move the button into place.
                selectionEl.css({
                    top: coordinates.top + 'px',
                    left: coordinates.left + 'px',
                    position: 'absolute',
                    zIndex: 10000,
                    display: 'block'
                });

                $timeout(function(){
                    scrollIntoView(ctx, selectionEl);
                },0);
            } else {
                selectionEl.css({
                    display: 'none'
                });
            }
        }

        // Auto trigger popup function for contenteditable
        function popAutoTriggerUnderMention (triggerCharSet, selectionEl) {
            var coordinates;
            var mentionInfo = getAutoTriggerInfo(triggerCharSet, false);

            if (mentionInfo !== undefined) {
                coordinates = getContentEditableCaretPosition(undefined, mentionInfo.mentionPosition);

                // Move the button into place.
                selectionEl.css({
                    top: coordinates.top + 'px',
                    left: coordinates.left + 'px',
                    position: 'absolute',
                    zIndex: 10000,
                    display: 'block'
                });

                $timeout(function () {
                    scrollIntoView(undefined, selectionEl);
                }, 0);
            } else {
                selectionEl.css({
                    display: 'none'
                });
            }
        }

        function scrollIntoView(ctx, elem)
        {
            // cheap hack in px - need to check styles relative to the element
            var reasonableBuffer = 20;
            var maxScrollDisplacement = 100;
            var clientRect;
            var e = elem[0];
            while (clientRect === undefined || clientRect.height === 0) {
                clientRect = e.getBoundingClientRect();
                if (clientRect.height === 0) {
                    e = e.childNodes[0];
                    if (e === undefined || !e.getBoundingClientRect) {
                        return;
                    }
                }
            }
            var elemTop = clientRect.top;
            var elemBottom = elemTop + clientRect.height;
            if(elemTop < 0) {
                $window.scrollTo(0, $window.pageYOffset + clientRect.top - reasonableBuffer);
            } else if (elemBottom > $window.innerHeight) {
                var maxY = $window.pageYOffset + clientRect.top - reasonableBuffer;
                if (maxY - $window.pageYOffset > maxScrollDisplacement) {
                    maxY = $window.pageYOffset + maxScrollDisplacement;
                }
                var targetY = $window.pageYOffset - ($window.innerHeight - elemBottom);
                if (targetY > maxY) {
                    targetY = maxY;
                }
                $window.scrollTo(0, targetY);
            }
        }

        function selectedElementIsTextAreaOrInput (ctx) {
            var element = getDocument(ctx).activeElement;
            if (element !== null) {
                var nodeName = element.nodeName;
                var type = element.getAttribute('type');
                return (nodeName === 'INPUT' && type === 'text') || nodeName === 'TEXTAREA';
            }
            return false;
        }

        function selectElement (ctx, targetElement, path, offset) {
            var elem = targetElement;
            if (path) {
                for (var i = 0; i < path.length; i++) {
                    elem = elem.childNodes[path[i]];
                    if (elem === undefined) {
                        return;
                    }
                    while (elem.length < offset) {
                        offset -= elem.length;
                        elem = elem.nextSibling;
                        if (!elem) {
                            return;
                        }
                    }
                    if (elem.childNodes.length === 0 && !elem.length) {
                        elem = elem.previousSibling;
                        if (!elem) {
                            return;
                        }
                    }
                }
            }
            moveCaretTo(ctx, elem, offset);
            targetElement.focus();
        }

        function moveCaretTo(ctx, elem, offset) {
            var sel = getWindowSelection(ctx);

            var range = getDocument(ctx).createRange();
            range.setStart(elem, offset);
            range.setEnd(elem, offset);
            range.collapse(true);
            try { sel.removeAllRanges(); } catch (error) { }
            sel.addRange(range);

        }

        function selectAutoTriggerElement(targetElement, path, offset) {
            var elem = targetElement;
            if (path) {
                for (var i = 0; i < path.length; i++) {
                    elem = elem.childNodes[path[i]];
                    if (elem === undefined) {
                        return;
                    }
                    while (elem.length < offset) {
                        offset -= elem.length;
                        elem = elem.nextSibling;
                        if (!elem) {
                            return;
                        }
                    }
                    if (elem.childNodes.length === 0 && !elem.length) {
                        elem = elem.previousSibling;
                        if (!elem) {
                            return;
                        }
                    }
                }
            }
            if (elem.nodeType !== enums.domNodeType.text) {
                offset = elem.childNodes.length;
            }

            moveCaretTo(undefined, elem, offset);
            targetElement.focus();
        }

        function pasteHtml (ctx, html, startPos, endPos) {
            var range, sel;
            sel = getWindowSelection(ctx);
            range = getDocument(ctx).createRange();
            range.setStart(sel.anchorNode, startPos);
            range.setEnd(sel.anchorNode, endPos);
            range.deleteContents();

            var el = getDocument(ctx).createElement('div');
            el.innerHTML = html;
            var frag = getDocument(ctx).createDocumentFragment(),
                node, lastNode;
            while ((node = el.firstChild)) {
                lastNode = frag.appendChild(node);
            }
            range.insertNode(frag);

            // Preserve the selection
            if (lastNode) {
                range = range.cloneRange();
                range.setStartAfter(lastNode);
                range.collapse(true);
                sel.removeAllRanges();
                sel.addRange(range);

                return lastNode;
            }

            return null;
        }

        function resetSelection (ctx, targetElement, path, offset) {
            var nodeName = targetElement.nodeName;
            if (nodeName === 'INPUT' || nodeName === 'TEXTAREA') {
                if (targetElement !== getDocument(ctx).activeElement) {
                    targetElement.focus();
                }
            } else {
                selectElement(ctx, targetElement, path, offset);
            }
        }

        // public
        function replaceMacroText (ctx, targetElement, path, offset, macros, text) {
            resetSelection(ctx, targetElement, path, offset);

            var macroMatchInfo = getMacroMatch(ctx, macros);

            if (macroMatchInfo.macroHasTrailingSpace) {
                macroMatchInfo.macroText = macroMatchInfo.macroText + '\xA0';
                text = text + '\xA0';
            }

            if (macroMatchInfo !== undefined) {
                var element = getDocument(ctx).activeElement;
                if (selectedElementIsTextAreaOrInput(ctx)) {
                    var startPos = macroMatchInfo.macroPosition;
                    var endPos = macroMatchInfo.macroPosition + macroMatchInfo.macroText.length;
                    element.value = element.value.substring(0, startPos) + text +
                        element.value.substring(endPos, element.value.length);
                    element.selectionStart = startPos + text.length;
                    element.selectionEnd = startPos + text.length;
                } else {
                    pasteHtml(ctx, text, macroMatchInfo.macroPosition,
                            macroMatchInfo.macroPosition + macroMatchInfo.macroText.length);
                }
            }
        }

        // public
        function replaceTriggerText (ctx, targetElement, path, offset, triggerCharSet, text, requireLeadingSpace, hasTrailingSpace) {
            resetSelection(ctx, targetElement, path, offset);

            var mentionInfo = getTriggerInfo(ctx, triggerCharSet, requireLeadingSpace, true, hasTrailingSpace);

            if (mentionInfo !== undefined) {
                if (selectedElementIsTextAreaOrInput()) {
                    var myField = getDocument(ctx).activeElement;
                    text = text + ' ';
                    var startPos = mentionInfo.mentionPosition;
                    var endPos = mentionInfo.mentionPosition + mentionInfo.mentionText.length + 1;
                    myField.value = myField.value.substring(0, startPos) + text +
                        myField.value.substring(endPos, myField.value.length);
                    myField.selectionStart = startPos + text.length;
                    myField.selectionEnd = startPos + text.length;
                } else {
                    // Remove operation to add \xA0. We do not need the character.
                    //// add a space to the end of the pasted text
                    //text = text + '\xA0';
                    pasteHtml(ctx, text, mentionInfo.mentionPosition,
                            mentionInfo.mentionPosition + mentionInfo.mentionText.length + 1);
                }
            }
        }

        // public
        function replaceAutoTriggerText(targetElement, path, offset, triggerCharSet, text) {
            selectAutoTriggerElement(targetElement, path, offset);
            //resetSelection(undefined, targetElement, path, offset);

            var mentionInfo = getAutoTriggerInfo(triggerCharSet, true);

            if (mentionInfo !== undefined) {
                // Set caret between () added after the formula name
                var lastNode = pasteHtml(undefined, text, mentionInfo.mentionPosition,
                    mentionInfo.mentionPosition + mentionInfo.mentionText.length);
                while (lastNode.nodeType !== enums.domNodeType.text) {
                    lastNode = lastNode.childNodes[lastNode.childNodes.length - 1];
                }
                moveCaretTo(undefined, lastNode, lastNode.textContent.length - 1);
            }
        }

        function getNodePositionInParent (ctx, elem) {
            if (elem.parentNode === null) {
                return 0;
            }
            for (var i = 0; i < elem.parentNode.childNodes.length; i++) {
                var node = elem.parentNode.childNodes[i];
                if (node === elem) {
                    return i;
                }
            }
        }

        // public
        function getMacroMatch (ctx, macros) {
            var selected, path = [], offset;

            if (selectedElementIsTextAreaOrInput(ctx)) {
                selected = getDocument(ctx).activeElement;
            } else {
                // content editable
                var selectionInfo = getContentEditableSelectedPath(ctx);
                if (selectionInfo) {
                    selected = selectionInfo.selected;
                    path = selectionInfo.path;
                    offset = selectionInfo.offset;
                }
            }
            var effectiveRange = getTextPrecedingCurrentSelection(ctx);
            if (effectiveRange !== undefined && effectiveRange !== null) {

                var matchInfo;

                var hasTrailingSpace = false;

                if (effectiveRange.length > 0 &&
                    (effectiveRange.charAt(effectiveRange.length - 1) === '\xA0' ||
                        effectiveRange.charAt(effectiveRange.length - 1) === ' ')) {
                    hasTrailingSpace = true;
                    // strip space
                    effectiveRange = effectiveRange.substring(0, effectiveRange.length-1);
                }

                angular.forEach(macros, function (macro, c) {
                    var idx = effectiveRange.toUpperCase().lastIndexOf(c.toUpperCase());

                    if (idx >= 0 && c.length + idx === effectiveRange.length) {
                        var prevCharPos = idx - 1;
                        if (idx === 0 || effectiveRange.charAt(prevCharPos) === '\xA0' ||
                            effectiveRange.charAt(prevCharPos) === ' ' ) {

                            matchInfo = {
                                macroPosition: idx,
                                macroText: c,
                                macroSelectedElement: selected,
                                macroSelectedPath: path,
                                macroSelectedOffset: offset,
                                macroHasTrailingSpace: hasTrailingSpace
                            };
                        }
                    }
                });
                if (matchInfo) {
                    return matchInfo;
                }
            }
        }

        function getContentEditableSelectedPath(ctx) {
            // content editable
            var sel = getWindowSelection(ctx);
            var selected = sel.anchorNode;
            var path = [];
            var offset;
            if (selected != null) {
                var i;
                var ce = selected.contentEditable;
                while (selected !== null && ce !== 'true') {
                    i = getNodePositionInParent(ctx, selected);
                    path.push(i);
                    selected = selected.parentNode;
                    if (selected !== null) {
                        ce = selected.contentEditable;
                    }
                }
                path.reverse();
                // getRangeAt may not exist, need alternative
                offset = sel.getRangeAt(0).startOffset;
                return {
                    selected: selected,
                    path: path,
                    offset: offset
                };
            }
        }

        // public
        function getTriggerInfo (ctx, triggerCharSet, requireLeadingSpace, menuAlreadyActive, hasTrailingSpace) {
            /*jshint maxcomplexity:11 */
            // yes this function needs refactoring 
            var selected, path, offset;
            if (selectedElementIsTextAreaOrInput(ctx)) {
                selected = getDocument(ctx).activeElement;
            } else {
                // content editable
                var selectionInfo = getContentEditableSelectedPath(ctx);
                if (selectionInfo) {
                    selected = selectionInfo.selected;
                    path = selectionInfo.path;
                    offset = selectionInfo.offset;
                }
            }
            var effectiveRange = getTextPrecedingCurrentSelection(ctx);

            if (effectiveRange !== undefined && effectiveRange !== null) {
                var mostRecentTriggerCharPos = -1;
                var triggerChar;
                triggerCharSet.forEach(function(c) {
                    var idx = effectiveRange.lastIndexOf(c);
                    if (idx > mostRecentTriggerCharPos) {
                        mostRecentTriggerCharPos = idx;
                        triggerChar = c;
                    }
                });
                if (mostRecentTriggerCharPos >= 0 &&
                        (
                            mostRecentTriggerCharPos === 0 ||
                            !requireLeadingSpace ||
                            /[\xA0\s]/g.test
                            (
                                effectiveRange.substring(
                                    mostRecentTriggerCharPos - 1,
                                    mostRecentTriggerCharPos)
                            )
                        )
                    )
                {
                    var currentTriggerSnippet = effectiveRange.substring(mostRecentTriggerCharPos + 1,
                        effectiveRange.length);

                    triggerChar = effectiveRange.substring(mostRecentTriggerCharPos, mostRecentTriggerCharPos+1);
                    var firstSnippetChar = currentTriggerSnippet.substring(0,1);
                    var leadingSpace = currentTriggerSnippet.length > 0 &&
                        (
                            firstSnippetChar === ' ' ||
                            firstSnippetChar === '\xA0'
                        );
                    if (hasTrailingSpace) {
                        currentTriggerSnippet = currentTriggerSnippet.trim();
                    }
                    if (!leadingSpace && (menuAlreadyActive || !(/[\xA0\s]/g.test(currentTriggerSnippet)))) {
                        return {
                            mentionPosition: mostRecentTriggerCharPos,
                            mentionText: currentTriggerSnippet,
                            mentionSelectedElement: selected,
                            mentionSelectedPath: path,
                            mentionSelectedOffset: offset,
                            mentionTriggerChar: triggerChar
                        };
                    }
                }
            }
        }

        // public
        // Auto trigger function for contenteditable
        function getAutoTriggerInfo (triggerCharSet, menuAlreadyActive) {
            var selected, path, offset;
            var autoTriggerCharSet = ['+', '-', '*', '/', '\xA0', ' '];
            var selectionInfo = getContentEditableSelectedPath();
            if (selectionInfo) {
                selected = selectionInfo.selected;
                path = selectionInfo.path;
                offset = selectionInfo.offset;
            }
            var effectiveRange = getTextPrecedingCurrentSelection();
            if (effectiveRange !== undefined && effectiveRange !== null) {
                var mostRecentTriggerCharPos = -1;
                var triggerChar;
                autoTriggerCharSet.forEach(function (c) {
                    var idx = effectiveRange.lastIndexOf(c);
                    if (idx > mostRecentTriggerCharPos) {
                        mostRecentTriggerCharPos = idx;
                        triggerChar = c;
                    }
                });

                // If only trigger char exists without snippet, do not show menu
                if (mostRecentTriggerCharPos + 1 >= effectiveRange.length) {
                    return;
                }

                var currentTriggerSnippet = effectiveRange.substring(mostRecentTriggerCharPos + 1,
                    effectiveRange.length);

                triggerChar = mostRecentTriggerCharPos >= 0
                    ? effectiveRange.substring(mostRecentTriggerCharPos, mostRecentTriggerCharPos + 1)
                    : null;
                var invalidSnippet = false;
                triggerCharSet.forEach(function (c) {
                    if (currentTriggerSnippet.indexOf(c) > 0) {
                        invalidSnippet = true;
                    }
                });
                if (menuAlreadyActive || !(/[\xA0\s]/g.test(currentTriggerSnippet)) && !invalidSnippet) {
                    return {
                        mentionPosition: mostRecentTriggerCharPos + 1,
                        mentionText: currentTriggerSnippet,
                        mentionSelectedElement: selected,
                        mentionSelectedPath: path,
                        mentionSelectedOffset: offset,
                        mentionTriggerChar: triggerChar
                    };
                }
            }
        }

        // public
        // Clear empty text node in contenteditable.
        function clearEmptyTextNode(elem) {
            var toDeleteNodes = [];
            for (var i = 0; i < elem.childNodes.length; i++) {
                if (elem.childNodes[i].nodeType === enums.domNodeType.text
                    && elem.childNodes[i].nodeValue === '') {
                    toDeleteNodes.push(elem.childNodes[i]);
                }
                else {
                    clearEmptyTextNode(elem.childNodes[i]);
                }
            }

            toDeleteNodes.forEach(function (n) {
                elem.removeChild(n);
            });
        }

        function getWindowSelection(ctx) {
            if (!ctx) {
                return window.getSelection();
            } else {
                return ctx.iframe.contentWindow.getSelection();
            }
        }

        function getDocument(ctx) {
            if (!ctx) {
                return document;
            } else {
                return ctx.iframe.contentWindow.document;
            }
        }

        function getTextPrecedingCurrentSelection (ctx) {
            var text;
            if (selectedElementIsTextAreaOrInput(ctx)) {
                var textComponent = getDocument(ctx).activeElement;
                var startPos = textComponent.selectionStart;
                text = textComponent.value.substring(0, startPos);

            } else {
                var selectedElem = getWindowSelection(ctx).anchorNode;

                // Edmond Meng: Add if condition to support non-contenteditable elements
                // in contenteditable element. Only text nodes can trigger mentio.
                if (selectedElem != null && selectedElem.nodeType === enums.domNodeType.text) {
                    var workingNodeContent = selectedElem.textContent;
                    var selectStartOffset = getWindowSelection(ctx).getRangeAt(0).startOffset;
                    if (selectStartOffset >= 0) {
                        text = workingNodeContent.substring(0, selectStartOffset);
                    }
                }
            }
            return text;
        }

        function getContentEditableCaretPosition (ctx, selectedNodePosition) {
            var markerTextChar = '\ufeff';
            var markerEl, markerId = 'sel_' + new Date().getTime() + '_' + Math.random().toString().substr(2);

            var range;
            var sel = getWindowSelection(ctx);
            var prevRange = sel.getRangeAt(0);
            range = getDocument(ctx).createRange();

            range.setStart(sel.anchorNode, selectedNodePosition);
            range.setEnd(sel.anchorNode, selectedNodePosition);

            range.collapse(false);

            // Create the marker element containing a single invisible character using DOM methods and insert it
            markerEl = getDocument(ctx).createElement('span');
            markerEl.id = markerId;
            markerEl.appendChild(getDocument(ctx).createTextNode(markerTextChar));
            range.insertNode(markerEl);
            sel.removeAllRanges();
            sel.addRange(prevRange);

            var coordinates = {
                left: 0,
                top: markerEl.offsetHeight
            };

            localToGlobalCoordinates(ctx, markerEl, coordinates);

            markerEl.parentNode.removeChild(markerEl);
            return coordinates;
        }

        function localToGlobalCoordinates(ctx, element, coordinates) {
            var obj = element;
            var iframe = ctx ? ctx.iframe : null;
            while(obj) {
                coordinates.left += obj.offsetLeft + obj.clientLeft;
                coordinates.top += obj.offsetTop + obj.clientTop;
                obj = obj.offsetParent;
                if (!obj && iframe) {
                    obj = iframe;
                    iframe = null;
                }
            }            
            obj = element;
            iframe = ctx ? ctx.iframe : null;
            while(obj !== getDocument().body) {
                if (obj.scrollTop && obj.scrollTop > 0) {
                    coordinates.top -= obj.scrollTop;
                }
                if (obj.scrollLeft && obj.scrollLeft > 0) {
                    coordinates.left -= obj.scrollLeft;
                }
                obj = obj.parentNode;
                if (!obj && iframe) {
                    obj = iframe;
                    iframe = null;
                }
            }            
         }

        function getTextAreaOrInputUnderlinePosition (ctx, element, position) {
            var properties = [
                'direction',
                'boxSizing',
                'width',
                'height',
                'overflowX',
                'overflowY',
                'borderTopWidth',
                'borderRightWidth',
                'borderBottomWidth',
                'borderLeftWidth',
                'paddingTop',
                'paddingRight',
                'paddingBottom',
                'paddingLeft',
                'fontStyle',
                'fontVariant',
                'fontWeight',
                'fontStretch',
                'fontSize',
                'fontSizeAdjust',
                'lineHeight',
                'fontFamily',
                'textAlign',
                'textTransform',
                'textIndent',
                'textDecoration',
                'letterSpacing',
                'wordSpacing'
            ];

            var isFirefox = (window.mozInnerScreenX !== null);

            var div = getDocument(ctx).createElement('div');
            div.id = 'input-textarea-caret-position-mirror-div';
            getDocument(ctx).body.appendChild(div);

            var style = div.style;
            var computed = window.getComputedStyle ? getComputedStyle(element) : element.currentStyle;

            style.whiteSpace = 'pre-wrap';
            if (element.nodeName !== 'INPUT') {
                style.wordWrap = 'break-word';
            }

            // position off-screen
            style.position = 'absolute';
            style.visibility = 'hidden';

            // transfer the element's properties to the div
            properties.forEach(function (prop) {
                style[prop] = computed[prop];
            });

            if (isFirefox) {
                style.width = (parseInt(computed.width) - 2) + 'px';
                if (element.scrollHeight > parseInt(computed.height))
                    style.overflowY = 'scroll';
            } else {
                style.overflow = 'hidden';
            }

            div.textContent = element.value.substring(0, position);

            if (element.nodeName === 'INPUT') {
                div.textContent = div.textContent.replace(/\s/g, '\u00a0');
            }

            var span = getDocument(ctx).createElement('span');
            span.textContent = element.value.substring(position) || '.';
            div.appendChild(span);

            var coordinates = {
                top: span.offsetTop + parseInt(computed.borderTopWidth) + parseInt(computed.fontSize),
                left: span.offsetLeft + parseInt(computed.borderLeftWidth)
            };

            localToGlobalCoordinates(ctx, element, coordinates);

            getDocument(ctx).body.removeChild(div);

            return coordinates;
        }

        return {
            // public
            popUnderMention: popUnderMention,
            popAutoTriggerUnderMention: popAutoTriggerUnderMention,
            replaceMacroText: replaceMacroText,
            replaceTriggerText: replaceTriggerText,
            replaceAutoTriggerText: replaceAutoTriggerText,
            getMacroMatch: getMacroMatch,
            getTriggerInfo: getTriggerInfo,
            getAutoTriggerInfo: getAutoTriggerInfo,
            selectElement: selectElement,
            clearEmptyTextNode: clearEmptyTextNode,



            // private: for unit testing only
            getTextAreaOrInputUnderlinePosition: getTextAreaOrInputUnderlinePosition,
            getTextPrecedingCurrentSelection: getTextPrecedingCurrentSelection,
            getContentEditableSelectedPath: getContentEditableSelectedPath,
            getNodePositionInParent: getNodePositionInParent,
            getContentEditableCaretPosition: getContentEditableCaretPosition,
            pasteHtml: pasteHtml,
            resetSelection: resetSelection,
            scrollIntoView: scrollIntoView
        };
    }]);

angular.module("mentio").run(["$templateCache", function($templateCache) {$templateCache.put("mentio-menu.tpl.html","<style>\n.scrollable-menu {\n    height: auto;\n    max-height: 300px;\n    overflow: auto;\n}\n\n.menu-highlighted {\n    font-weight: bold;\n}\n</style>\n<ul class=\"dropdown-menu scrollable-menu\" style=\"display:block\">\n    <li mentio-menu-item=\"item\" ng-repeat=\"item in items track by $index\">\n        <a class=\"text-primary\" ng-bind-html=\"item.label | mentioHighlight:$parent.typedTerm:\'menu-highlighted\' | unsafe\"></a>\n    </li>\n</ul>");}]);