/* * 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); }); } }; } ]);