elastic.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /*
  2. * angular-elastic v2.4.2
  3. * (c) 2014 Monospaced http://monospaced.com
  4. * License: MIT
  5. */
  6. angular.module('monospaced.elastic', [])
  7. .constant('msdElasticConfig', {
  8. append: ''
  9. })
  10. .directive('msdElastic', [
  11. '$timeout', '$window', 'msdElasticConfig',
  12. function($timeout, $window, config) {
  13. 'use strict';
  14. return {
  15. require: 'ngModel',
  16. restrict: 'A, C',
  17. link: function(scope, element, attrs, ngModel) {
  18. // cache a reference to the DOM element
  19. var ta = element[0],
  20. $ta = element;
  21. // ensure the element is a textarea, and browser is capable
  22. if (ta.nodeName !== 'TEXTAREA' || !$window.getComputedStyle) {
  23. return;
  24. }
  25. // set these properties before measuring dimensions
  26. $ta.css({
  27. 'overflow': 'hidden',
  28. 'overflow-y': 'hidden',
  29. 'word-wrap': 'break-word'
  30. });
  31. // force text reflow
  32. var text = ta.value;
  33. ta.value = '';
  34. ta.value = text;
  35. var append = attrs.msdElastic ? attrs.msdElastic.replace(/\\n/g, '\n') : config.append,
  36. $win = angular.element($window),
  37. mirrorInitStyle = 'position: absolute; top: -999px; right: auto; bottom: auto;' +
  38. 'left: 0; overflow: hidden; -webkit-box-sizing: content-box;' +
  39. '-moz-box-sizing: content-box; box-sizing: content-box;' +
  40. 'min-height: 0 !important; height: 0 !important; padding: 0;' +
  41. 'word-wrap: break-word; border: 0;',
  42. $mirror = angular.element('<textarea tabindex="-1" ' +
  43. 'style="' + mirrorInitStyle + '"/>').data('elastic', true),
  44. mirror = $mirror[0],
  45. taStyle = getComputedStyle(ta),
  46. resize = taStyle.getPropertyValue('resize'),
  47. borderBox = taStyle.getPropertyValue('box-sizing') === 'border-box' ||
  48. taStyle.getPropertyValue('-moz-box-sizing') === 'border-box' ||
  49. taStyle.getPropertyValue('-webkit-box-sizing') === 'border-box',
  50. boxOuter = !borderBox ? {width: 0, height: 0} : {
  51. width: parseInt(taStyle.getPropertyValue('border-right-width'), 10) +
  52. parseInt(taStyle.getPropertyValue('padding-right'), 10) +
  53. parseInt(taStyle.getPropertyValue('padding-left'), 10) +
  54. parseInt(taStyle.getPropertyValue('border-left-width'), 10),
  55. height: parseInt(taStyle.getPropertyValue('border-top-width'), 10) +
  56. parseInt(taStyle.getPropertyValue('padding-top'), 10) +
  57. parseInt(taStyle.getPropertyValue('padding-bottom'), 10) +
  58. parseInt(taStyle.getPropertyValue('border-bottom-width'), 10)
  59. },
  60. minHeightValue = parseInt(taStyle.getPropertyValue('min-height'), 10),
  61. heightValue = parseInt(taStyle.getPropertyValue('height'), 10),
  62. minHeight = Math.max(minHeightValue, heightValue) - boxOuter.height,
  63. maxHeight = parseInt(taStyle.getPropertyValue('max-height'), 10),
  64. mirrored,
  65. active,
  66. copyStyle = ['font-family',
  67. 'font-size',
  68. 'font-weight',
  69. 'font-style',
  70. 'letter-spacing',
  71. 'line-height',
  72. 'text-transform',
  73. 'word-spacing',
  74. 'text-indent'];
  75. // exit if elastic already applied (or is the mirror element)
  76. if ($ta.data('elastic')) {
  77. return;
  78. }
  79. // Opera returns max-height of -1 if not set
  80. maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4;
  81. // append mirror to the DOM
  82. if (mirror.parentNode !== document.body) {
  83. angular.element(document.body).append(mirror);
  84. }
  85. // set resize and apply elastic
  86. $ta.css({
  87. 'resize': (resize === 'none' || resize === 'vertical') ? 'none' : 'horizontal'
  88. }).data('elastic', true);
  89. /*
  90. * methods
  91. */
  92. function initMirror() {
  93. var mirrorStyle = mirrorInitStyle;
  94. mirrored = ta;
  95. // copy the essential styles from the textarea to the mirror
  96. taStyle = getComputedStyle(ta);
  97. angular.forEach(copyStyle, function(val) {
  98. mirrorStyle += val + ':' + taStyle.getPropertyValue(val) + ';';
  99. });
  100. mirror.setAttribute('style', mirrorStyle);
  101. }
  102. function adjust() {
  103. var taHeight,
  104. taComputedStyleWidth,
  105. mirrorHeight,
  106. width,
  107. overflow;
  108. if (mirrored !== ta) {
  109. initMirror();
  110. }
  111. // active flag prevents actions in function from calling adjust again
  112. if (!active) {
  113. active = true;
  114. mirror.value = ta.value + append; // optional whitespace to improve animation
  115. mirror.style.overflowY = ta.style.overflowY;
  116. taHeight = ta.style.height === '' ? 'auto' : parseInt(ta.style.height, 10);
  117. taComputedStyleWidth = getComputedStyle(ta).getPropertyValue('width');
  118. // ensure getComputedStyle has returned a readable 'used value' pixel width
  119. if (taComputedStyleWidth.substr(taComputedStyleWidth.length - 2, 2) === 'px') {
  120. // update mirror width in case the textarea width has changed
  121. width = parseInt(taComputedStyleWidth, 10) - boxOuter.width;
  122. mirror.style.width = width + 'px';
  123. }
  124. mirrorHeight = mirror.scrollHeight;
  125. if (mirrorHeight > maxHeight) {
  126. mirrorHeight = maxHeight;
  127. overflow = 'scroll';
  128. } else if (mirrorHeight < minHeight) {
  129. mirrorHeight = minHeight;
  130. }
  131. mirrorHeight += boxOuter.height;
  132. ta.style.overflowY = overflow || 'hidden';
  133. if (taHeight !== mirrorHeight) {
  134. ta.style.height = mirrorHeight + 'px';
  135. scope.$emit('elastic:resize', $ta);
  136. }
  137. // small delay to prevent an infinite loop
  138. $timeout(function() {
  139. active = false;
  140. }, 1);
  141. }
  142. }
  143. function forceAdjust() {
  144. active = false;
  145. adjust();
  146. }
  147. /*
  148. * initialise
  149. */
  150. // listen
  151. if ('onpropertychange' in ta && 'oninput' in ta) {
  152. // IE9
  153. ta['oninput'] = ta.onkeyup = adjust;
  154. } else {
  155. ta['oninput'] = adjust;
  156. }
  157. $win.bind('resize', forceAdjust);
  158. scope.$watch(function() {
  159. return ngModel.$modelValue;
  160. }, function(newValue) {
  161. forceAdjust();
  162. });
  163. scope.$on('elastic:adjust', function() {
  164. initMirror();
  165. forceAdjust();
  166. });
  167. $timeout(adjust);
  168. /*
  169. * destroy
  170. */
  171. scope.$on('$destroy', function() {
  172. $mirror.remove();
  173. $win.unbind('resize', forceAdjust);
  174. });
  175. }
  176. };
  177. }
  178. ]);