Skip to content
Snippets Groups Projects
elastic.js 7.57 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
     * angular-elastic v2.4.0
     * (c) 2014 Monospaced http://monospaced.com
     * License: MIT
     */
    
    angular.module('monospaced.elastic', [])
    
      .constant('msdElasticConfig', {
        append: ''
      })
    
      .directive('msdElastic', [
        '$timeout', '$window', 'msdElasticConfig',
        function($timeout, $window, config) {
          'use strict';
    
          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 append = attrs.msdElastic ? attrs.msdElastic.replace(/\\n/g, '\n') : config.append,
                  $win = angular.element($window),
                  mirrorInitStyle = 'position: absolute; top: -999px; right: auto; bottom: auto;' +
                                    'left: 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="' + mirrorInitStyle + '"/>').data('elastic', true),
                  mirror = $mirror[0],
                  taStyle = getComputedStyle(ta),
                  resize = taStyle.getPropertyValue('resize'),
                  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),
                  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.data('elastic')) {
                return;
              }
    
              // Opera returns max-height of -1 if not set
              maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4;
    
              // append mirror to the DOM
              if (mirror.parentNode !== document.body) {
                angular.element(document.body).append(mirror);
              }
    
              // set resize and apply elastic
              $ta.css({
                'resize': (resize === 'none' || resize === 'vertical') ? 'none' : 'horizontal'
              }).data('elastic', true);
    
              /*
               * methods
               */
    
              function initMirror() {
                var mirrorStyle = mirrorInitStyle;
    
                mirrored = ta;
                // copy the essential styles from the textarea to the mirror
                taStyle = getComputedStyle(ta);
                angular.forEach(copyStyle, function(val) {
                  mirrorStyle += val + ':' + taStyle.getPropertyValue(val) + ';';
                });
                mirror.setAttribute('style', mirrorStyle);
              }
    
              function adjust() {
                var taHeight,
                    taComputedStyleWidth,
                    mirrorHeight,
                    width,
                    overflow;
    
                if (mirrored !== ta) {
                  initMirror();
                }
    
                // active flag prevents actions in function from calling adjust again
                if (!active) {
                  active = true;
    
                  mirror.value = ta.value + append; // optional whitespace to improve animation
                  mirror.style.overflowY = ta.style.overflowY;
    
                  taHeight = ta.style.height === '' ? 'auto' : parseInt(ta.style.height, 10);
    
                  taComputedStyleWidth = 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';
                    scope.$emit('elastic:resize', $ta);
                  }
    
                  // small delay to prevent an infinite loop
                  $timeout(function() {
                    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(function() {
                return ngModel.$modelValue;
              }, function(newValue) {
                forceAdjust();
              });
    
              scope.$on('elastic:adjust', function() {
                initMirror();
                forceAdjust();
              });
    
              $timeout(adjust);
    
              /*
               * destroy
               */
    
              scope.$on('$destroy', function() {
                $mirror.remove();
                $win.unbind('resize', forceAdjust);
              });
            }
          };
        }
      ]);