From e9eafef26b6b29ab318fd5daf73d81b736c8ce69 Mon Sep 17 00:00:00 2001 From: Dom Christie Date: Wed, 31 Aug 2016 18:53:20 +0100 Subject: [PATCH 01/29] Replace jQuery code in internal methods with vanilla js/DOM --- expanding.js | 167 ++++++++++++++++++++++++----------------- test/expanding_test.js | 66 ++++------------ 2 files changed, 113 insertions(+), 120 deletions(-) diff --git a/expanding.js b/expanding.js index 9515c4f..d0985f8 100644 --- a/expanding.js +++ b/expanding.js @@ -15,22 +15,28 @@ // Class Definition // ================ - var Expanding = function ($textarea, options) { - this.$textarea = $textarea; - this.$textCopy = $(''); - this.$clone = $('

').prepend(this.$textCopy); - - $textarea - .wrap($('
')) - .after(this.$clone); + var Expanding = function (textarea) { + this.textarea = textarea; + this.textCopy = document.createElement('span'); + this.clone = document.createElement('pre'); + this.clone.className = 'expanding-clone'; + this.clone.appendChild(this.textCopy); + this.clone.appendChild(document.createElement('br')); + this.wrapper = document.createElement('div'); + this.wrapper.className = 'expanding-wrapper'; + this.wrapper.style.position = 'relative'; + + // Wrap + this.textarea.parentNode.insertBefore(this.wrapper, this.textarea); + this.wrapper.appendChild(this.textarea); + this.wrapper.appendChild(this.clone); + + this._eventListeners = {}; + this._oldTextareaStyles = this.textarea.getAttribute('style'); this.attach(); this.setStyles(); this.update(); - - if (typeof options.update === 'function') { - $textarea.bind('update.expanding', options.update); - } }; Expanding.DEFAULTS = { @@ -66,31 +72,35 @@ // Attaches input events // Only attaches `keyup` events if `input` is not fully suported attach: function () { - var events = 'input.expanding change.expanding', - _this = this; - if (!inputSupported) events += ' keyup.expanding'; - this.$textarea.bind(events, function () { _this.update(); }); + var _this = this; + var events = [(inputSupported ? 'input' : 'keyup'), 'change']; + function handler () { _this.update(); } + + for (var i = 0; i < events.length; i++) { + var event = events[i]; + this.textarea.addEventListener(event, handler); + this._eventListeners[event] = handler; + } }, // Updates the clone with the textarea value update: function () { - this.$textCopy.text(this.$textarea.val().replace(/\r\n/g, '\n')); - - // Use `triggerHandler` to prevent conflicts with `update` in Prototype.js - this.$textarea.triggerHandler('update.expanding'); + this.textCopy.textContent = this.textarea.value.replace(/\r\n/g, '\n'); + dispatch('expanding:update', { target: this.textarea }); }, // Tears down the plugin: removes generated elements, applies styles // that were prevously present, removes instance from data, unbinds events destroy: function () { - this.$clone.remove(); - this.$textarea - .unwrap() - .attr('style', this._oldTextareaStyles || '') - .removeData('expanding') - .unbind('input.expanding change.expanding keyup.expanding update.expanding'); - + this.wrapper.removeChild(this.clone); + this.wrapper.parentNode.insertBefore(this.textarea, this.wrapper); + this.wrapper.parentNode.removeChild(this.wrapper); + this.textarea.setAttribute('style', this._oldTextareaStyles || ''); delete this._oldTextareaStyles; + + for (var event in this._eventListeners) { + this.textarea.removeEventListener(event, this._eventListeners[event]); + } }, setStyles: function () { @@ -102,15 +112,16 @@ // Applies reset styles to the textarea and clone // Stores the original textarea styles in case of destroying _resetStyles: function () { - this._oldTextareaStyles = this.$textarea.attr('style'); - - this.$textarea.add(this.$clone).css({ - margin: 0, - webkitBoxSizing: 'border-box', - mozBoxSizing: 'border-box', - boxSizing: 'border-box', - width: '100%' - }); + var elements = [this.textarea, this.clone]; + for (var i = 0; i < elements.length; i++) { + style(elements[i], { + margin: 0, + webkitBoxSizing: 'border-box', + mozBoxSizing: 'border-box', + boxSizing: 'border-box', + width: '100%' + }); + } }, // Sets the basic clone styles and copies styles over from the textarea @@ -119,45 +130,49 @@ display: 'block', border: '0 solid', visibility: 'hidden', - minHeight: this.$textarea.outerHeight() + minHeight: this.textarea.offsetHeight + 'px' }; - if (this.$textarea.attr('wrap') === 'off') css.overflowX = 'scroll'; - else css.whiteSpace = 'pre-wrap'; + if (this.textarea.getAttribute('wrap') === 'off') { + css.overflowX = 'scroll'; + } else css.whiteSpace = 'pre-wrap'; - this.$clone.css(css); + style(this.clone, css); this._copyTextareaStylesToClone(); }, _copyTextareaStylesToClone: function () { - var _this = this, - properties = [ - 'lineHeight', 'textDecoration', 'letterSpacing', - 'fontSize', 'fontFamily', 'fontStyle', - 'fontWeight', 'textTransform', 'textAlign', - 'direction', 'wordSpacing', 'fontSizeAdjust', - 'wordWrap', 'word-break', - 'borderLeftWidth', 'borderRightWidth', - 'borderTopWidth', 'borderBottomWidth', - 'paddingLeft', 'paddingRight', - 'paddingTop', 'paddingBottom', 'maxHeight' - ]; - - $.each(properties, function (i, property) { - var val = _this.$textarea.css(property); + var properties = [ + 'lineHeight', 'textDecoration', 'letterSpacing', + 'fontSize', 'fontFamily', 'fontStyle', + 'fontWeight', 'textTransform', 'textAlign', + 'direction', 'wordSpacing', 'fontSizeAdjust', + 'wordWrap', 'word-break', + 'borderLeftWidth', 'borderRightWidth', + 'borderTopWidth', 'borderBottomWidth', + 'paddingLeft', 'paddingRight', + 'paddingTop', 'paddingBottom', 'maxHeight' + ]; + var computedTextareaStyles = window.getComputedStyle(this.textarea); + var computedCloneStyles = window.getComputedStyle(this.clone); + + for (var i = 0; i < properties.length; i++) { + var property = properties[i]; + var computedTextareaStyle = computedTextareaStyles[property]; + var computedCloneStyle = computedCloneStyles[property]; // Prevent overriding percentage css values. - if (_this.$clone.css(property) !== val) { - _this.$clone.css(property, val); - if (property === 'maxHeight' && val !== 'none') { - _this.$clone.css('overflow', 'hidden'); + if (computedCloneStyle !== computedTextareaStyle) { + this.clone.style[property] = computedTextareaStyle; + if (property === 'maxHeight' && computedTextareaStyle !== 'none') { + this.clone.style.overflow = 'hidden'; } } - }); + } }, _setTextareaStyles: function () { - this.$textarea.css({ + style(this.textarea, { position: 'absolute', top: 0, left: 0, @@ -168,6 +183,22 @@ } }; + function style (element, styles) { + for (var property in styles) element.style[property] = styles[property]; + } + + function dispatch (eventName, options) { + options = options || {}; + var event = document.createEvent('Event'); + event.initEvent(eventName, true, options.cancelable === true); + event.data = options.data != null ? options.data : {}; + var target = options.target != null ? options.target : document; + target.dispatchEvent(event); + } + + function _warn(text) { + if (window.console && console.warn) console.warn(text); + } // Plugin Definition // ================= @@ -180,7 +211,10 @@ var instance = $this.data('expanding'); - if (instance && option === 'destroy') return instance.destroy(); + if (instance && option === 'destroy') { + $this.removeData('expanding'); + return instance.destroy(); + } if (instance && option === 'refresh') return instance.setStyles(); @@ -191,7 +225,7 @@ if (!instance && visible) { var options = $.extend({}, $.expanding, typeof option === 'object' && option); - $this.data('expanding', new Expanding($this, options)); + $this.data('expanding', new Expanding($this[0], options)); } }); return this; @@ -200,14 +234,9 @@ $.fn.expanding = Plugin; $.fn.expanding.Constructor = Expanding; - function _warn(text) { - if (window.console && console.warn) console.warn(text); - } - $(function () { if ($.expanding.autoInitialize) { $($.expanding.initialSelector).expanding(); } }); - -})); \ No newline at end of file +})); diff --git a/test/expanding_test.js b/test/expanding_test.js index 8d06e2c..f2ab529 100644 --- a/test/expanding_test.js +++ b/test/expanding_test.js @@ -4,6 +4,15 @@ module('ExpandingTextareas', { } }); +function dispatch (eventName, options) { + options = options || {}; + var event = document.createEvent('Event'); + event.initEvent(eventName, true, options.cancelable === true); + event.data = options.data != null ? options.data : {}; + var target = options.target != null ? options.target : document; + target.dispatchEvent(event); +} + test('Returns the jQuery object', 1, function () { var $textarea = $(' +``` +npm install expanding-textareas +… +var Expanding = require('expanding-textareas') +``` -*And that's it*. The plugin finds textareas with the `expanding` class on page load and initializes them for you. These textareas will automatically resize now as the user changes the value. +via bower: -If you'd like to change the initial selector to grab ALL textareas on load, you can change this property: +``` +bower install expanding-textareas +``` - $.expanding.initialSelector = "textarea"; +The library is also available as a jQuery plugin (see below). -### Manual +Usage +----- -If you would prefer to initialize the textareas on your own, do something like this: +`Expanding` is a constructor which takes a textarea DOM node as its only argument: - +```js +var textarea = document.querySelector('textarea') +var expanding = new Expanding(textarea) +``` -If you'd like to change the value by code and have it resize manually, you can do: +That's it! The textarea will now expand as the user types. - $('textarea').val('New\nValue!').change() +### `update` +Updates the textarea height. This method is called automatically when the user types, but when setting the textarea content programmatically, it can be used to ensure the height expands as needed. For example: -## Options +```js +var textarea = document.querySelector('textarea') +var expanding = new Expanding(textarea) -There aren't any options needed for this plugin. If your textarea has certain attributes, the plugin will handle them gracefully. +textarea.value = 'Hello\nworld!' // Height is not yet updated +expanding.update() // Height is now updated +``` -* ` +``` -### `refresh` +The plugin will attach the behavior to every `.expanding` textarea when the DOM is ready. -Plugin styles can be refreshed as follows: +### Customizing the Initial Selector - $(".element").expanding('refresh'); +To change the selector used for automatic initialization, modify `$.expanding.initialSelector`. For example: -This should be called after expanding textarea styles are updated, or box model dimensions are changed. +```javascript +$.expanding = { + initialSelector: '[data-behavior=expanding]' +} +``` -### Textareas outside the DOM +### Disabling Automatic Initialization -The plugin creates a textarea clone with identical dimensions to that of the original. It therefore requires that the textarea be in place in the DOM for these dimensions to be correct. Calling `expanding()` on a textarea outside the DOM will have no effect. +To disable auto-initialization, set `$.expanding.autoInitialize` to `false`: -## Styling +``` +$.expanding = { + autoInitialize: false +} +``` -You can style things how you'd like for the textarea, and they will automatically be copied over to the invisible pre tag, **with the exception of margins** (which are reset to 0, to ensure that the clone maintains the correct size and positioning). +### Manual Initialization -**[Flash of unstyled content](http://en.wikipedia.org/wiki/Flash_of_unstyled_content) (FOUC)** can be avoided by adding the following styles to your stylesheet (adjust the selector if necessary): +To manually initialize the plugin call `expanding()` on the jQuery selection. For example to apply the behavior to all textareas: - textarea.expanding { - margin: 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - width: 100%; - } +```javascript +$('textarea').expanding() +``` -By default, the textarea will behave like a block-level element: its width will expand to fill its container. To restrict the textarea width, simply apply a width declaration to a parent element e.g. the textarea wrapper: +### Options - .expanding-wrapper { - width: 50%; - } +#### `destroy` -See the [demo](http://bgrins.github.com/ExpandingTextareas/) to see the plugin in action. +`'destroy'` will remove the behavior: -## Browser Support +```js +$('textarea').expanding('destroy') +``` -This has been checked in Chrome, Safari, Firefox, IE8, and mobile Safari and it works in all of them. +#### `active` -## How it works +`'active'` will check whether it has the expanding behavior applied: -See the [original article](http://www.alistapart.com/articles/expanding-text-areas-made-elegant/) for a great explanation of how this technique works. +```js +$('textarea').expanding('active') // returns true or false +``` -The plugin will automatically find this textarea, and turn it into an expanding one. The final (generated) markup will look something like this: +Note: this behaves like `.hasClass()`: it will return `true` if _any_ of the nodes in the selection have the expanding behaviour. -
- -
-
+#### `refresh` -The way it works is that as the user types, the text content is copied into the div inside the pre (which is actually providing the height of the textarea). So it could look like this: +`'refresh'` will update the styles (see above for more details): -
- -
Some Content - Was Entered
-
+```javascript +$('textarea').expanding('refresh') +``` -## Running Tests Locally +Caveats +------- -**Browser**: open `test/index.html` +Textareas must be visible for the library to function properly. The library creates a textarea clone with identical dimensions to that of the original. It therefore requires that the textarea be in place in the DOM for these dimensions to be correct. -**Command line**: make sure you have installed [node.js](http://nodejs.org/), and [grunt-cli](http://gruntjs.com/getting-started), then run: +Any styling applied to the target textarea will be maintained with the exception of margins and widths. (Margins are reset to 0 to ensure that the textarea maintains the correct size and positioning.) - $ npm install +After the expanding behavior has been applied, the textarea will appear like a block-level element: its width will expand to fill its container. To restrict the textarea width, apply a width declaration to a parent element. The library's wrapper (`.expanding-wrapper`) element may be useful in this case: -Followed by: +```css +.expanding-wrapper { + width: 50%; +} +``` - $ grunt test +[Flash of unstyled content](http://en.wikipedia.org/wiki/Flash_of_unstyled_content) can be avoided by adding the following styles (adjust the selector as necessary): -## Continuous Deployment +```css +textarea.expanding { + margin: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 100%; +} +``` -View tests online at: https://travis-ci.org/bgrins/ExpandingTextareas. +Browser Support +--------------- -[![Build Status](https://travis-ci.org/bgrins/ExpandingTextareas.svg?branch=master)](https://travis-ci.org/bgrins/ExpandingTextareas) +The library aims to support modern versions of the following browsers: Chrome, Firefox, IE (9+), Opera, and Safari (incl. iOS). View [the test suite](http://bgrins.github.io/ExpandingTextareas/test/) to see if check if your browser is fully supported. (If there are no failures then you're good to go!) + +Development & Testing +--------------------- + +This library has been developed with ES2015 modules and bundled with [Rollup](http://rollupjs.org). To get started with development, first clone the project: + +``` +git clone git@github.com:bgrins/ExpandingTextareas.git +``` + +Then navigate to the project and install the dependencies: + +``` +cd ExpandingTextareas +npm install +``` + +To bundle the source files: + +``` +npm run build +``` + +And finally to test: + +``` +npm test +``` + +Run the tests in a browser by opening `test/index.html`. From f88c5a3f3cd22ddb7d5ccdac06e3b82ea9c5cdd8 Mon Sep 17 00:00:00 2001 From: Dom Christie Date: Fri, 30 Sep 2016 17:54:00 +0100 Subject: [PATCH 24/29] Update demo to reflect new API --- index.html | 2 +- libs/homepage.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 095fb26..481341c 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ Expanding Textareas - + diff --git a/libs/homepage.js b/libs/homepage.js index 9385823..587fde7 100644 --- a/libs/homepage.js +++ b/libs/homepage.js @@ -24,11 +24,11 @@ $(function() { typetype("This is just a normal textarea...\n\n", { e:0, // no typing errors! ms:90, // fast typing - keypress: function(){$(this).change()}, + keypress: function(){$(this).data('expanding').update()}, callback: function(){$('#demo').addClass('persp')} }). typetype("except it expands when you type!", { - keypress: function(){$(this).change()}, + keypress: function(){$(this).data('expanding').update()}, callback: function(){introSeq.resolve()} }) @@ -43,4 +43,4 @@ $(function() { introAnimation.done(function(){ $(window).off('DOMMouseScroll mousewheel') }) -}); \ No newline at end of file +}); From 4ff97ce81f9abd5b583a1df5efaf9d9e6210bd7b Mon Sep 17 00:00:00 2001 From: Dom Christie Date: Fri, 30 Sep 2016 17:54:16 +0100 Subject: [PATCH 25/29] Update package.json --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8ffbfae..1b1f660 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "ExpandingTextareas", - "version": "0.2.0", - "description": "jQuery plugin for elegant expanding textareas", + "name": "expanding-textareas", + "version": "1.0.0", + "description": "An elegant approach to making textareas automatically grow.", "homepage": "http://bgrins.github.com/ExpandingTextareas/", "repository": { "type": "git", From 21b5bf9e193336cab7d665eb709543676cff9ea2 Mon Sep 17 00:00:00 2001 From: Dom Christie Date: Wed, 12 Oct 2016 16:45:16 +0100 Subject: [PATCH 26/29] Use native bind to bind inputHandler --- dist/expanding.jquery.js | 5 +---- dist/expanding.js | 5 +---- src/expanding.js | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/dist/expanding.jquery.js b/dist/expanding.jquery.js index 1a12b39..69ece5e 100644 --- a/dist/expanding.jquery.js +++ b/dist/expanding.jquery.js @@ -183,7 +183,6 @@ TextareaClone.prototype = { } function Expanding (textarea) { - var _this = this this.element = createElement() this.textarea = new Textarea(textarea) this.textareaClone = new TextareaClone() @@ -194,9 +193,7 @@ function Expanding (textarea) { wrap(textarea, this.element) this.element.appendChild(this.textareaClone.element) - function inputHandler () { - _this.update.apply(_this, arguments) - } + var inputHandler = this.update.bind(this) this.textarea.on(inputEvent, inputHandler) this.textarea.on('change', inputHandler) diff --git a/dist/expanding.js b/dist/expanding.js index f7872f8..948bd33 100644 --- a/dist/expanding.js +++ b/dist/expanding.js @@ -182,7 +182,6 @@ TextareaClone.prototype = { } function Expanding (textarea) { - var _this = this this.element = createElement() this.textarea = new Textarea(textarea) this.textareaClone = new TextareaClone() @@ -193,9 +192,7 @@ function Expanding (textarea) { wrap(textarea, this.element) this.element.appendChild(this.textareaClone.element) - function inputHandler () { - _this.update.apply(_this, arguments) - } + var inputHandler = this.update.bind(this) this.textarea.on(inputEvent, inputHandler) this.textarea.on('change', inputHandler) diff --git a/src/expanding.js b/src/expanding.js index 6a37629..8d877b6 100644 --- a/src/expanding.js +++ b/src/expanding.js @@ -3,7 +3,6 @@ import Textarea from './textarea' import TextareaClone from './textarea-clone' function Expanding (textarea) { - var _this = this this.element = createElement() this.textarea = new Textarea(textarea) this.textareaClone = new TextareaClone() @@ -14,9 +13,7 @@ function Expanding (textarea) { wrap(textarea, this.element) this.element.appendChild(this.textareaClone.element) - function inputHandler () { - _this.update.apply(_this, arguments) - } + var inputHandler = this.update.bind(this) this.textarea.on(inputEvent, inputHandler) this.textarea.on('change', inputHandler) From 4f9f0868e0becce1cc897be69d6c030d33a87c7c Mon Sep 17 00:00:00 2001 From: Dom Christie Date: Wed, 12 Oct 2016 16:46:46 +0100 Subject: [PATCH 27/29] Use contributors field in package.json --- package.json | 14 ++++++++++---- rollup.config.js | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1b1f660..3035a2d 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,16 @@ }, "bugs": "https://github.com/bgrins/ExpandingTextareas/issues", "license": "MIT", - "author": { - "name": "Brian Grinstead", - "web": "http://briangrinstead.com/" - }, + "contributors": [ + { + "name": "Brian Grinstead", + "url": "http://briangrinstead.com/" + }, + { + "name": "Dom Christie", + "url": "http://www.domchristie.co.uk/" + } + ], "main": "dist/expanding.js", "scripts": { "build": "rollup -c && rollup -c rollup.config.jquery.js", diff --git a/rollup.config.js b/rollup.config.js index c82b3e3..385caaf 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -10,7 +10,7 @@ export default { banner: [ '/*', ' * ' + pkg.name + ' ' + pkg.version, - ' * Copyright © 2011+ ' + pkg.author.name, + ' * Copyright © 2011+ ' + pkg.contributors[0].name, ' * Released under the ' + pkg.license + ' license', ' * ' + pkg.homepage, ' */', From 2aea6e7f96eee86828241944efaa942ae99cf50c Mon Sep 17 00:00:00 2001 From: Dom Christie Date: Wed, 12 Oct 2016 16:47:00 +0100 Subject: [PATCH 28/29] Update banner --- dist/expanding.jquery.js | 2 +- dist/expanding.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/expanding.jquery.js b/dist/expanding.jquery.js index 69ece5e..9bbe6c5 100644 --- a/dist/expanding.jquery.js +++ b/dist/expanding.jquery.js @@ -1,5 +1,5 @@ /* - * ExpandingTextareas 0.2.0 + * expanding-textareas 1.0.0 * Copyright © 2011+ Brian Grinstead * Released under the MIT license * http://bgrins.github.com/ExpandingTextareas/ diff --git a/dist/expanding.js b/dist/expanding.js index 948bd33..af4b03f 100644 --- a/dist/expanding.js +++ b/dist/expanding.js @@ -1,5 +1,5 @@ /* - * ExpandingTextareas 0.2.0 + * expanding-textareas 1.0.0 * Copyright © 2011+ Brian Grinstead * Released under the MIT license * http://bgrins.github.com/ExpandingTextareas/ From 6c2a28633f2decce027b203232d976ac99ff9ce9 Mon Sep 17 00:00:00 2001 From: Dom Christie Date: Tue, 25 Oct 2016 19:09:08 +0100 Subject: [PATCH 29/29] Close inline code markdown --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 495e976..93a2a3f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ An elegant approach to making textareas automatically grow. Based on [Expanding Installation ------------ -`Expanding` can be installed via NPM, Bower, or by downloading the script located at `dist/expanding.js. It can be required via CommonJS, AMD, or as a global (e.g. `window.Expanding`). +`Expanding` can be installed via NPM, Bower, or by downloading the script located at `dist/expanding.js`. It can be required via CommonJS, AMD, or as a global (e.g. `window.Expanding`). via npm: