superdesk/superdesk-client-core

View on GitHub
scripts/core/ui/autoheight-directive.ts

Summary

Maintainability
C
1 day
Test Coverage
/**
 * Gives auto height while typing on textarea elements
 *
 * Usage:
 * <div sd-auto-height></div>
 *
 * Source:
 * https://github.com/monospaced/angular-elastic/blob/master/elastic.js
 *
 */
angular.module('superdesk.core.ui.autoheight', []).directive('sdAutoHeight', ['$window', function($window) {
    return {
        require: 'ngModel',
        restrict: 'A, C',
        link: function(scope, element, attrs, ngModel) {
            // cache a reference to the DOM element
            var ta = element[0],
                $ta = element;

            // ensure the element is a textarea, and browser is capable
            if (ta.nodeName !== 'TEXTAREA' || !$window.getComputedStyle) {
                return;
            }

            // set these properties before measuring dimensions
            $ta.css({
                overflow: 'hidden',
                'overflow-y': 'hidden',
                'word-wrap': 'break-word',
            });

            // force text reflow
            var text = ta.value;

            ta.value = '';
            ta.value = text;

            var $win = angular.element($window),
                mirrorStyle,
                mirrorStyleBasic =
                    'position: absolute; insetBlockStart: -999px;' +
                    'insetInlineEnd: auto; insetBlockEnd: auto; insetInlineStart: 0 ;' +
                    'overflow: hidden; -webkit-box-sizing: content-box;' +
                    '-moz-box-sizing: content-box; box-sizing: content-box;' +
                    'min-height: 0 !important; height: 0 !important; padding: 0;' +
                    'word-wrap: break-word; border: 0;',
                $mirror = angular.element('<textarea tabindex="-1" ' +
                                          'style="' + mirrorStyleBasic + '"/>').attr('elastic', true),
                mirror = $mirror[0],
                taStyle, borderBox, boxOuter, minHeightValue, heightValue,
                minHeight, maxHeight, mirrored, active,
                copyStyle = ['font-family',
                    'font-size',
                    'font-weight',
                    'font-style',
                    'letter-spacing',
                    'line-height',
                    'text-transform',
                    'word-spacing',
                    'text-indent',
                ];

            // exit if elastic already applied (or is the mirror element)
            if ($ta.attr('elastic')) {
                return;
            }

            // append mirror to the DOM
            if (mirror.parentNode !== document.body) {
                angular.element(document.body).append(mirror);
            }

            // set resize and apply elastic
            $ta.css({
                resize: 'none',
            });
            $ta.attr('elastic', true);

            /*
             * methods
             */
            function initMirror() {
                mirrored = ta;
                taStyle = $window.getComputedStyle(ta);
                borderBox = taStyle.getPropertyValue('box-sizing') === 'border-box' ||
                          taStyle.getPropertyValue('-moz-box-sizing') === 'border-box' ||
                          taStyle.getPropertyValue('-webkit-box-sizing') === 'border-box';
                boxOuter = !borderBox ? {width: 0, height: 0} : {
                    width: parseInt(taStyle.getPropertyValue('border-right-width'), 10) +
                            parseInt(taStyle.getPropertyValue('padding-right'), 10) +
                            parseInt(taStyle.getPropertyValue('padding-left'), 10) +
                            parseInt(taStyle.getPropertyValue('border-left-width'), 10),
                    height: parseInt(taStyle.getPropertyValue('border-top-width'), 10) +
                            parseInt(taStyle.getPropertyValue('padding-top'), 10) +
                            parseInt(taStyle.getPropertyValue('padding-bottom'), 10) +
                            parseInt(taStyle.getPropertyValue('border-bottom-width'), 10),
                };
                minHeightValue = parseInt(taStyle.getPropertyValue('min-height'), 10);
                heightValue = parseInt(taStyle.getPropertyValue('height'), 10);
                minHeight = Math.max(minHeightValue, heightValue) - boxOuter.height;
                maxHeight = parseInt(taStyle.getPropertyValue('max-height'), 10);
            }

            function setMirrorStyle() {
                taStyle = $window.getComputedStyle(ta);
                mirrorStyle = mirrorStyleBasic;
                angular.forEach(copyStyle, (val) => {
                    mirrorStyle += val + ':' + taStyle[val] + ';';
                });

                mirror.setAttribute('style', mirrorStyle);
            }

            function adjust() {
                var taHeight,
                    taComputedStyleWidth,
                    mirrorHeight,
                    width,
                    overflow;

                if (mirrored !== ta) {
                    initMirror();
                }

                setMirrorStyle();

                // active flag prevents actions in function from calling adjust again
                if (!active) {
                    active = true;

                    mirror.value = ta.value; // optional whitespace to improve animation
                    mirror.style.overflowY = ta.style.overflowY;

                    taHeight = ta.style.height === '' ? 'auto' : parseInt(ta.style.height, 10);

                    taComputedStyleWidth = $window.getComputedStyle(ta).getPropertyValue('width');

                    // ensure getComputedStyle has returned a readable 'used value' pixel width
                    if (taComputedStyleWidth.substr(taComputedStyleWidth.length - 2, 2) === 'px') {
                        // update mirror width in case the textarea width has changed
                        width = parseInt(taComputedStyleWidth, 10) - boxOuter.width;
                        mirror.style.width = width + 'px';
                    }

                    mirrorHeight = mirror.scrollHeight;

                    if (mirrorHeight > maxHeight) {
                        mirrorHeight = maxHeight;
                        overflow = 'scroll';
                    } else if (mirrorHeight < minHeight) {
                        mirrorHeight = minHeight;
                    }
                    mirrorHeight += boxOuter.height;

                    ta.style.overflowY = overflow || 'hidden';

                    if (taHeight !== mirrorHeight) {
                        ta.style.height = mirrorHeight + 'px';
                    }

                    // small delay to prevent an infinite loop
                    _.delay(() => {
                        active = false;
                    }, 1);
                }
            }

            function forceAdjust() {
                active = false;
                adjust();
            }

            /*
             * initialise
             */

            // listen
            if ('onpropertychange' in ta && 'oninput' in ta) {
                // IE9
                ta.oninput = ta.onkeyup = adjust;
            } else {
                ta.oninput = adjust;
            }

            $win.bind('resize', forceAdjust);

            scope.$watch(() => ngModel.$modelValue, (newval, oldval) => {
                if (newval !== oldval) {
                    forceAdjust();
                }
            });

            _.defer(adjust);

            /*
             * destroy
             */

            scope.$on('$destroy', () => {
                $mirror.remove();
                $win.unbind('resize', forceAdjust);
            });
        },
    };
}]);