diff --git a/.jshintignore b/.jshintignore deleted file mode 100644 index e9ed0f1..0000000 --- a/.jshintignore +++ /dev/null @@ -1,2 +0,0 @@ -libs/ -node_modules \ No newline at end of file diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index a48d640..0000000 --- a/.jshintrc +++ /dev/null @@ -1,21 +0,0 @@ -{ - "globals" : { - "define": true, - "deepEqual": true, - "notEqual": true, - "equal": true, - "ok": true, - "test": true - }, - - "browser": true, - "eqnull": true, - "indent": 2, - "jquery": true, - "latedef": true, - "node": true, - "quotmark": "single", - "undef": true, - "unused": true, - "white": true -} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0ce3be2..4909f83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ language: node_js node_js: - - "0.10" -before_script: - - npm install -g grunt-cli \ No newline at end of file + - "6" diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 6634796..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = function (grunt) { - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-qunit'); - - grunt.initConfig({ - jshint: { - all: ['*.js', 'test/*.js'], - - options: { - jshintrc: true - } - }, - qunit: { - all: ['test/index.html', 'test/document_ready.html'] - } - }); - - grunt.registerTask('test', ['jshint', 'qunit']); - grunt.registerTask('default', ['test']); -}; \ No newline at end of file diff --git a/README.md b/README.md index 4415a12..93a2a3f 100644 --- a/README.md +++ b/README.md @@ -1,140 +1,195 @@ -# Expanding Textareas jQuery Plugin +ExpandingTextareas +================== -Based off of work by [Neil Jenkins](http://nmjenkins.com/) that can be seen here: http://www.alistapart.com/articles/expanding-text-areas-made-elegant/ +[![Build Status](https://travis-ci.org/bgrins/ExpandingTextareas.svg?branch=master)](https://travis-ci.org/bgrins/ExpandingTextareas) -## Usage +An elegant approach to making textareas automatically grow. Based on [Expanding Text Areas Made Elegant](http://www.alistapart.com/articles/expanding-text-areas-made-elegant/) by [Neil Jenkins](http://nmjenkins.com/). -### Automatic +Installation +------------ -Start with markup like this: +`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: -*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. +``` +npm install expanding-textareas +… +var Expanding = require('expanding-textareas') +``` -If you'd like to change the initial selector to grab ALL textareas on load, you can change this property: +via bower: - $.expanding.initialSelector = "textarea"; +``` +bower install expanding-textareas +``` -### Manual +The library is also available as a jQuery plugin (see below). -If you would prefer to initialize the textareas on your own, do something like this: +Usage +----- - +`Expanding` is a constructor which takes a textarea DOM node as its only argument: -If you'd like to change the value by code and have it resize manually, you can do: +```js +var textarea = document.querySelector('textarea') +var expanding = new Expanding(textarea) +``` - $('textarea').val('New\nValue!').change() +That's it! The textarea will now expand as the user types. +### `update` -## Options +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: -There aren't any options needed for this plugin. If your textarea has certain attributes, the plugin will handle them gracefully. +```js +var textarea = document.querySelector('textarea') +var expanding = new Expanding(textarea) -* ` +``` - $("#element").expanding('active'); // -> boolean +The plugin will attach the behavior to every `.expanding` textarea when the DOM is ready. -Note: this behaves like `.hasClass()`: it will return `true` if _any_ of the nodes in the selection have expanding behaviour. +### Customizing the Initial Selector -### `refresh` +To change the selector used for automatic initialization, modify `$.expanding.initialSelector`. For example: -Plugin styles can be refreshed as follows: +```javascript +$.expanding = { + initialSelector: '[data-behavior=expanding]' +} +``` - $(".element").expanding('refresh'); +### Disabling Automatic Initialization -This should be called after expanding textarea styles are updated, or box model dimensions are changed. +To disable auto-initialization, set `$.expanding.autoInitialize` to `false`: -### Textareas outside the DOM +``` +$.expanding = { + autoInitialize: false +} +``` -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. +### Manual Initialization -## Styling +To manually initialize the plugin call `expanding()` on the jQuery selection. For example to apply the behavior to all textareas: -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). +```javascript +$('textarea').expanding() +``` -**[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): +### Options - textarea.expanding { - margin: 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - width: 100%; - } +#### `destroy` -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: +`'destroy'` will remove the behavior: - .expanding-wrapper { - width: 50%; - } +```js +$('textarea').expanding('destroy') +``` -See the [demo](http://bgrins.github.com/ExpandingTextareas/) to see the plugin in action. +#### `active` -## Browser Support +`'active'` will check whether it has the expanding behavior applied: -This has been checked in Chrome, Safari, Firefox, IE8, and mobile Safari and it works in all of them. +```js +$('textarea').expanding('active') // returns true or false +``` -## How it works +Note: this behaves like `.hasClass()`: it will return `true` if _any_ of the nodes in the selection have the expanding behaviour. -See the [original article](http://www.alistapart.com/articles/expanding-text-areas-made-elegant/) for a great explanation of how this technique works. +#### `refresh` -The plugin will automatically find this textarea, and turn it into an expanding one. The final (generated) markup will look something like this: +`'refresh'` will update the styles (see above for more details): -
- -
-
+```javascript +$('textarea').expanding('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: +Caveats +------- -
- -
Some Content - Was Entered
-
+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. -## Running Tests Locally +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.) -**Browser**: open `test/index.html` +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: -**Command line**: make sure you have installed [node.js](http://nodejs.org/), and [grunt-cli](http://gruntjs.com/getting-started), then run: +```css +.expanding-wrapper { + width: 50%; +} +``` - $ npm install +[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): -Followed by: +```css +textarea.expanding { + margin: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 100%; +} +``` - $ grunt test +Browser Support +--------------- -## Continuous Deployment +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!) -View tests online at: https://travis-ci.org/bgrins/ExpandingTextareas. +Development & Testing +--------------------- -[![Build Status](https://travis-ci.org/bgrins/ExpandingTextareas.svg?branch=master)](https://travis-ci.org/bgrins/ExpandingTextareas) +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`. diff --git a/bower.json b/bower.json index 1b44a8e..5366ba8 100644 --- a/bower.json +++ b/bower.json @@ -6,7 +6,7 @@ "Brian Grinstead " ], "description": "jQuery plugin for elegant expanding textareas", - "main": "expanding.js", + "main": "dist/expanding.js", "moduleType": [ "amd", "globals" diff --git a/dist/expanding.jquery.js b/dist/expanding.jquery.js new file mode 100644 index 0000000..9bbe6c5 --- /dev/null +++ b/dist/expanding.jquery.js @@ -0,0 +1,316 @@ +/* + * expanding-textareas 1.0.0 + * Copyright © 2011+ Brian Grinstead + * Released under the MIT license + * http://bgrins.github.com/ExpandingTextareas/ + */ + +(function () { +'use strict'; + +var userAgent = window.navigator.userAgent + +// Returns the version of Internet Explorer or -1 +// (indicating the use of another browser). +// From: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx#ParsingUA +var ieVersion = (function () { + var version = -1 + if (window.navigator.appName === 'Microsoft Internet Explorer') { + var regExp = new RegExp('MSIE ([0-9]{1,}[\\.0-9]{0,})') + if (regExp.exec(userAgent) !== null) version = parseFloat(RegExp.$1) + } + return version +})() + +// Check for oninput support +// IE9 supports oninput, but not when deleting text, so keyup is used. +// onpropertychange _is_ supported by IE8/9, but may not be fired unless +// attached with `attachEvent` +// (see: http://stackoverflow.com/questions/18436424/ie-onpropertychange-event-doesnt-fire), +// and so is avoided altogether. +var inputEventSupported = ( + 'oninput' in document.createElement('input') && ieVersion !== 9 +) + +var inputEvent = inputEventSupported ? 'input' : 'keyup' + +var isIosDevice = /iPad|iPhone|iPod/.test(userAgent) && !window.MSStream + +function wrap (element, wrapper) { + element.parentNode.insertBefore(wrapper, element) + wrapper.appendChild(element) +} + +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) +} + +function Textarea (element) { + this.element = element + this._eventListeners = {} +} + +Textarea.prototype = { + style: function (styles) { + style(this.element, styles) + }, + + styles: function () { + return { + position: 'absolute', + top: 0, + left: 0, + height: '100%', + resize: 'none', + overflow: 'auto' + } + }, + + value: function (value) { + if (arguments.length === 0) { + return this.element.value.replace(/\r\n/g, '\n') + } else { + this.element.value = value + } + }, + + on: function (eventName, handler) { + this.element.addEventListener(eventName, handler) + this._eventListeners[eventName] = handler + }, + + off: function (eventName) { + if (arguments.length === 0) { + for (var event in this._eventListeners) { this.off(event) } + } else { + this.element.removeEventListener( + eventName, + this._eventListeners[eventName] + ) + delete this._eventListeners[eventName] + } + }, + + destroy: function () { + this.element.setAttribute('style', this.oldStyleAttribute || '') + this.off() + } +} + +var styleProperties = { + borderBottomWidth: null, + borderLeftWidth: null, + borderRightWidth: null, + borderTopWidth: null, + direction: null, + fontFamily: null, + fontSize: null, + fontSizeAdjust: null, + fontStyle: null, + fontWeight: null, + letterSpacing: null, + lineHeight: null, + maxHeight: null, + paddingBottom: null, + paddingLeft: paddingHorizontal, + paddingRight: paddingHorizontal, + paddingTop: null, + textAlign: null, + textDecoration: null, + textTransform: null, + wordBreak: null, + wordSpacing: null, + wordWrap: null +} + +function paddingHorizontal (computedStyle) { + return isIosDevice ? (parseFloat(computedStyle) + 3) + 'px' : computedStyle +} + +function TextareaClone () { + this.element = document.createElement('pre') + this.element.className = 'expanding-clone' + this.innerElement = document.createElement('span') + this.element.appendChild(this.innerElement) + this.element.appendChild(document.createElement('br')) +} + +TextareaClone.prototype = { + value: function (value) { + if (arguments.length === 0) return this.innerElement.textContent + else this.innerElement.textContent = value + }, + + style: function (styles) { + style(this.element, styles) + }, + + styles: function (textarea) { + var wrap = textarea.getAttribute('wrap') + var styles = { + display: 'block', + border: '0 solid', + visibility: 'hidden', + overflowX: wrap === 'off' ? 'scroll' : 'hidden', + whiteSpace: wrap === 'off' ? 'pre' : 'pre-wrap' + } + + var computedStyles = window.getComputedStyle(textarea) + + for (var property in styleProperties) { + var valueFunction = styleProperties[property] + var computedStyle = computedStyles[property] + styles[property] = ( + valueFunction ? valueFunction(computedStyle) : computedStyle + ) + } + + return styles + } +} + +function Expanding (textarea) { + this.element = createElement() + this.textarea = new Textarea(textarea) + this.textareaClone = new TextareaClone() + this.textarea.oldStyleAttribute = textarea.getAttribute('style') + resetStyles.call(this) + setStyles.call(this) + + wrap(textarea, this.element) + this.element.appendChild(this.textareaClone.element) + + var inputHandler = this.update.bind(this) + this.textarea.on(inputEvent, inputHandler) + this.textarea.on('change', inputHandler) + + this.update() +} + +Expanding.prototype = { + update: function () { + this.textareaClone.value(this.textarea.value()) + dispatch('expanding:update', { target: this.textarea.element }) + }, + + refresh: function () { + setStyles.call(this) + }, + + destroy: function () { + this.element.removeChild(this.textareaClone.element) + this.element.parentNode.insertBefore(this.textarea.element, this.element) + this.element.parentNode.removeChild(this.element) + this.textarea.destroy() + } +} + +function createElement () { + var element = document.createElement('div') + element.className = 'expanding-wrapper' + element.style.position = 'relative' + return element +} + +function resetStyles () { + var styles = { + margin: 0, + webkitBoxSizing: 'border-box', + mozBoxSizing: 'border-box', + boxSizing: 'border-box', + width: '100%' + } + // Should only be called once i.e. on initialization + this.textareaClone.style({ + minHeight: this.textarea.element.offsetHeight + 'px' + }) + this.textareaClone.style(styles) + this.textarea.style(styles) +} + +function setStyles () { + this.textareaClone.style(this.textareaClone.styles(this.textarea.element)) + this.textarea.style(this.textarea.styles()) +} + +/* global jQuery, define */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory) + } else if (typeof module === 'object' && module.exports) { + module.exports = function (root, jQuery) { + if (jQuery === undefined) { + if (typeof window !== 'undefined') { + jQuery = require('jquery') + } else { + jQuery = require('jquery')(root) + } + } + factory(jQuery) + return jQuery + } + } else { + factory(jQuery) + } +}(function ($) { + function plugin (option) { + if (option === 'active') return !!this.data('expanding') + + this.filter('textarea').each(function () { + var $this = jQuery(this) + var instance = $this.data('expanding') + + if (instance) { + switch (option) { + case 'destroy': + $this.removeData('expanding') + instance.destroy() + return + case 'refresh': + instance.refresh() + return + default: + return + } + } else if (!(this.offsetWidth > 0 || this.offsetHeight > 0)) { + warn( + 'ExpandingTextareas: attempt to initialize an invisible textarea. ' + + 'Call expanding() again once it has been inserted into the page and/or is visible.' + ) + return + } else { + return $this.data('expanding', new Expanding(this)) + } + }) + return this + } + + var defaults = { + autoInitialize: true, + initialSelector: 'textarea.expanding' + } + $.expanding = $.extend({}, defaults, $.expanding || {}) + $.fn.expanding = plugin + $.fn.expanding.Constructor = Expanding + + if ($.expanding.autoInitialize) { + $(document).ready(function () { + $($.expanding.initialSelector).expanding() + }) + } +})) + +}()); \ No newline at end of file diff --git a/dist/expanding.js b/dist/expanding.js new file mode 100644 index 0000000..af4b03f --- /dev/null +++ b/dist/expanding.js @@ -0,0 +1,250 @@ +/* + * expanding-textareas 1.0.0 + * Copyright © 2011+ Brian Grinstead + * Released under the MIT license + * http://bgrins.github.com/ExpandingTextareas/ + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.Expanding = factory()); +}(this, (function () { 'use strict'; + +var userAgent = window.navigator.userAgent + +// Returns the version of Internet Explorer or -1 +// (indicating the use of another browser). +// From: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx#ParsingUA +var ieVersion = (function () { + var version = -1 + if (window.navigator.appName === 'Microsoft Internet Explorer') { + var regExp = new RegExp('MSIE ([0-9]{1,}[\\.0-9]{0,})') + if (regExp.exec(userAgent) !== null) version = parseFloat(RegExp.$1) + } + return version +})() + +// Check for oninput support +// IE9 supports oninput, but not when deleting text, so keyup is used. +// onpropertychange _is_ supported by IE8/9, but may not be fired unless +// attached with `attachEvent` +// (see: http://stackoverflow.com/questions/18436424/ie-onpropertychange-event-doesnt-fire), +// and so is avoided altogether. +var inputEventSupported = ( + 'oninput' in document.createElement('input') && ieVersion !== 9 +) + +var inputEvent = inputEventSupported ? 'input' : 'keyup' + +var isIosDevice = /iPad|iPhone|iPod/.test(userAgent) && !window.MSStream + +function wrap (element, wrapper) { + element.parentNode.insertBefore(wrapper, element) + wrapper.appendChild(element) +} + +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 Textarea (element) { + this.element = element + this._eventListeners = {} +} + +Textarea.prototype = { + style: function (styles) { + style(this.element, styles) + }, + + styles: function () { + return { + position: 'absolute', + top: 0, + left: 0, + height: '100%', + resize: 'none', + overflow: 'auto' + } + }, + + value: function (value) { + if (arguments.length === 0) { + return this.element.value.replace(/\r\n/g, '\n') + } else { + this.element.value = value + } + }, + + on: function (eventName, handler) { + this.element.addEventListener(eventName, handler) + this._eventListeners[eventName] = handler + }, + + off: function (eventName) { + if (arguments.length === 0) { + for (var event in this._eventListeners) { this.off(event) } + } else { + this.element.removeEventListener( + eventName, + this._eventListeners[eventName] + ) + delete this._eventListeners[eventName] + } + }, + + destroy: function () { + this.element.setAttribute('style', this.oldStyleAttribute || '') + this.off() + } +} + +var styleProperties = { + borderBottomWidth: null, + borderLeftWidth: null, + borderRightWidth: null, + borderTopWidth: null, + direction: null, + fontFamily: null, + fontSize: null, + fontSizeAdjust: null, + fontStyle: null, + fontWeight: null, + letterSpacing: null, + lineHeight: null, + maxHeight: null, + paddingBottom: null, + paddingLeft: paddingHorizontal, + paddingRight: paddingHorizontal, + paddingTop: null, + textAlign: null, + textDecoration: null, + textTransform: null, + wordBreak: null, + wordSpacing: null, + wordWrap: null +} + +function paddingHorizontal (computedStyle) { + return isIosDevice ? (parseFloat(computedStyle) + 3) + 'px' : computedStyle +} + +function TextareaClone () { + this.element = document.createElement('pre') + this.element.className = 'expanding-clone' + this.innerElement = document.createElement('span') + this.element.appendChild(this.innerElement) + this.element.appendChild(document.createElement('br')) +} + +TextareaClone.prototype = { + value: function (value) { + if (arguments.length === 0) return this.innerElement.textContent + else this.innerElement.textContent = value + }, + + style: function (styles) { + style(this.element, styles) + }, + + styles: function (textarea) { + var wrap = textarea.getAttribute('wrap') + var styles = { + display: 'block', + border: '0 solid', + visibility: 'hidden', + overflowX: wrap === 'off' ? 'scroll' : 'hidden', + whiteSpace: wrap === 'off' ? 'pre' : 'pre-wrap' + } + + var computedStyles = window.getComputedStyle(textarea) + + for (var property in styleProperties) { + var valueFunction = styleProperties[property] + var computedStyle = computedStyles[property] + styles[property] = ( + valueFunction ? valueFunction(computedStyle) : computedStyle + ) + } + + return styles + } +} + +function Expanding (textarea) { + this.element = createElement() + this.textarea = new Textarea(textarea) + this.textareaClone = new TextareaClone() + this.textarea.oldStyleAttribute = textarea.getAttribute('style') + resetStyles.call(this) + setStyles.call(this) + + wrap(textarea, this.element) + this.element.appendChild(this.textareaClone.element) + + var inputHandler = this.update.bind(this) + this.textarea.on(inputEvent, inputHandler) + this.textarea.on('change', inputHandler) + + this.update() +} + +Expanding.prototype = { + update: function () { + this.textareaClone.value(this.textarea.value()) + dispatch('expanding:update', { target: this.textarea.element }) + }, + + refresh: function () { + setStyles.call(this) + }, + + destroy: function () { + this.element.removeChild(this.textareaClone.element) + this.element.parentNode.insertBefore(this.textarea.element, this.element) + this.element.parentNode.removeChild(this.element) + this.textarea.destroy() + } +} + +function createElement () { + var element = document.createElement('div') + element.className = 'expanding-wrapper' + element.style.position = 'relative' + return element +} + +function resetStyles () { + var styles = { + margin: 0, + webkitBoxSizing: 'border-box', + mozBoxSizing: 'border-box', + boxSizing: 'border-box', + width: '100%' + } + // Should only be called once i.e. on initialization + this.textareaClone.style({ + minHeight: this.textarea.element.offsetHeight + 'px' + }) + this.textareaClone.style(styles) + this.textarea.style(styles) +} + +function setStyles () { + this.textareaClone.style(this.textareaClone.styles(this.textarea.element)) + this.textarea.style(this.textarea.styles()) +} + +return Expanding; + +}))); \ No newline at end of file diff --git a/expanding.js b/expanding.js deleted file mode 100644 index 634b49e..0000000 --- a/expanding.js +++ /dev/null @@ -1,213 +0,0 @@ -// Expanding Textareas v0.3.0 -// MIT License -// https://github.com/bgrins/ExpandingTextareas - -(function (factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['jquery'], factory); - } else { - // Browser globals - factory(jQuery); - } -}(function ($) { - - // Class Definition - // ================ - - var Expanding = function ($textarea, options) { - this.$textarea = $textarea; - this.$textCopy = $(''); - this.$clone = $('

').prepend(this.$textCopy); - - $textarea - .wrap($('
')) - .after(this.$clone); - - this.attach(); - this.setStyles(); - this.update(); - - if (typeof options.update === 'function') { - $textarea.bind('update.expanding', options.update); - } - }; - - Expanding.DEFAULTS = { - autoInitialize: true, - initialSelector: 'textarea.expanding' - }; - - $.expanding = $.extend({}, Expanding.DEFAULTS, $.expanding || {}); - - // Returns the version of Internet Explorer or -1 - // (indicating the use of another browser). - // From: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx#ParsingUA - var ieVersion = (function () { - var v = -1; - if (navigator.appName === 'Microsoft Internet Explorer') { - var ua = navigator.userAgent; - var re = new RegExp('MSIE ([0-9]{1,}[\\.0-9]{0,})'); - if (re.exec(ua) !== null) v = parseFloat(RegExp.$1); - } - return v; - })(); - - // Check for oninput support - // IE9 supports oninput, but not when deleting text, so keyup is used. - // onpropertychange _is_ supported by IE8/9, but may not be fired unless - // attached with `attachEvent` - // (see: http://stackoverflow.com/questions/18436424/ie-onpropertychange-event-doesnt-fire), - // and so is avoided altogether. - var inputSupported = 'oninput' in document.createElement('input') && ieVersion !== 9; - - Expanding.prototype = { - - // 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(); }); - }, - - // 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'); - }, - - // 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'); - - delete this._oldTextareaStyles; - }, - - setStyles: function () { - this._resetStyles(); - this._setCloneStyles(); - this._setTextareaStyles(); - }, - - // 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%' - }); - }, - - // Sets the basic clone styles and copies styles over from the textarea - _setCloneStyles: function () { - var css = { - display: 'block', - border: '0 solid', - visibility: 'hidden', - minHeight: this.$textarea.outerHeight() - }; - - if (this.$textarea.attr('wrap') === 'off') css.overflowX = 'scroll'; - else css.whiteSpace = 'pre-wrap'; - - this.$clone.css(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); - - // 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'); - } - } - }); - }, - - _setTextareaStyles: function () { - this.$textarea.css({ - position: 'absolute', - top: 0, - left: 0, - height: '100%', - resize: 'none', - overflow: 'auto' - }); - } - }; - - - // Plugin Definition - // ================= - - function Plugin(option) { - if (option === 'active') return !!this.data('expanding'); - - this.filter('textarea').each(function () { - var $this = $(this); - - var instance = $this.data('expanding'); - - if (instance && option === 'destroy') return instance.destroy(); - - if (instance && option === 'refresh') return instance.setStyles(); - - var visible = this.offsetWidth > 0 || this.offsetHeight > 0; - - if (!visible) _warn('ExpandingTextareas: attempt to initialize an invisible textarea. ' + - 'Call expanding() again once it has been inserted into the page and/or is visible.'); - - if (!instance && visible) { - var options = $.extend({}, $.expanding, typeof option === 'object' && option); - $this.data('expanding', new Expanding($this, options)); - } - }); - return this; - } - - $.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/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 +}); diff --git a/package.json b/package.json index e264949..3035a2d 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,34 @@ { - "name": "ExpandingTextareas", - "version": "0.3.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", "url": "git://github.com/bgrins/ExpandingTextareas.git" }, "bugs": "https://github.com/bgrins/ExpandingTextareas/issues", - "licenses": [ + "license": "MIT", + "contributors": [ { - "type": "MIT", - "url": "http://mit-license.org/" + "name": "Brian Grinstead", + "url": "http://briangrinstead.com/" + }, + { + "name": "Dom Christie", + "url": "http://www.domchristie.co.uk/" } ], - "author": { - "name": "Brian Grinstead", - "web": "http://briangrinstead.com/" - }, - "main": "./expanding.js", + "main": "dist/expanding.js", "scripts": { - "test": "grunt test" + "build": "rollup -c && rollup -c rollup.config.jquery.js", + "test": "standard src/*.js && node-qunit-phantomjs ./test/index.html" }, "dependencies": {}, "devDependencies": { - "qunit": "~0.5.15", - "grunt-contrib-jshint": "~0.10.0", - "grunt-contrib-qunit": "~0.2.1", - "qunitjs": "~1.11.0" + "node-qunit-phantomjs": "^1.4.0", + "rollup": "^0.34.11", + "rollup-plugin-json": "^2.0.1", + "standard": "^8.0.0" } -} \ No newline at end of file +} diff --git a/rollup.config.jquery.js b/rollup.config.jquery.js new file mode 100644 index 0000000..7cf8489 --- /dev/null +++ b/rollup.config.jquery.js @@ -0,0 +1,7 @@ +import config from './rollup.config' + +export default Object.assign({}, config, { + entry: 'src/expanding.jquery.js', + dest: 'dist/expanding.jquery.js', + format: 'iife' +}) diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..385caaf --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,19 @@ +import json from 'rollup-plugin-json' +var pkg = require('./package.json') + +export default { + entry: 'src/expanding.js', + dest: 'dist/expanding.js', + format: 'umd', + moduleName: 'Expanding', + plugins: [json({ include: ['./package.json'] })], + banner: [ + '/*', + ' * ' + pkg.name + ' ' + pkg.version, + ' * Copyright © 2011+ ' + pkg.contributors[0].name, + ' * Released under the ' + pkg.license + ' license', + ' * ' + pkg.homepage, + ' */', + '' + ].join('\n') +} diff --git a/src/expanding.jquery.js b/src/expanding.jquery.js new file mode 100644 index 0000000..c71f239 --- /dev/null +++ b/src/expanding.jquery.js @@ -0,0 +1,70 @@ +/* global jQuery, define */ +import { warn } from './helpers' +import Expanding from './expanding' + +// UMD jQuery Plugin Template from: https://github.com/umdjs/umd#jquery-plugin +;(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory) + } else if (typeof module === 'object' && module.exports) { + module.exports = function (root, jQuery) { + if (jQuery === undefined) { + if (typeof window !== 'undefined') { + jQuery = require('jquery') + } else { + jQuery = require('jquery')(root) + } + } + factory(jQuery) + return jQuery + } + } else { + factory(jQuery) + } +}(function ($) { + function plugin (option) { + if (option === 'active') return !!this.data('expanding') + + this.filter('textarea').each(function () { + var $this = jQuery(this) + var instance = $this.data('expanding') + + if (instance) { + switch (option) { + case 'destroy': + $this.removeData('expanding') + instance.destroy() + return + case 'refresh': + instance.refresh() + return + default: + return + } + } else if (!(this.offsetWidth > 0 || this.offsetHeight > 0)) { + warn( + 'ExpandingTextareas: attempt to initialize an invisible textarea. ' + + 'Call expanding() again once it has been inserted into the page and/or is visible.' + ) + return + } else { + return $this.data('expanding', new Expanding(this)) + } + }) + return this + } + + var defaults = { + autoInitialize: true, + initialSelector: 'textarea.expanding' + } + $.expanding = $.extend({}, defaults, $.expanding || {}) + $.fn.expanding = plugin + $.fn.expanding.Constructor = Expanding + + if ($.expanding.autoInitialize) { + $(document).ready(function () { + $($.expanding.initialSelector).expanding() + }) + } +})) diff --git a/src/expanding.js b/src/expanding.js new file mode 100644 index 0000000..8d877b6 --- /dev/null +++ b/src/expanding.js @@ -0,0 +1,69 @@ +import { wrap, inputEvent, dispatch } from './helpers' +import Textarea from './textarea' +import TextareaClone from './textarea-clone' + +function Expanding (textarea) { + this.element = createElement() + this.textarea = new Textarea(textarea) + this.textareaClone = new TextareaClone() + this.textarea.oldStyleAttribute = textarea.getAttribute('style') + resetStyles.call(this) + setStyles.call(this) + + wrap(textarea, this.element) + this.element.appendChild(this.textareaClone.element) + + var inputHandler = this.update.bind(this) + this.textarea.on(inputEvent, inputHandler) + this.textarea.on('change', inputHandler) + + this.update() +} + +Expanding.prototype = { + update: function () { + this.textareaClone.value(this.textarea.value()) + dispatch('expanding:update', { target: this.textarea.element }) + }, + + refresh: function () { + setStyles.call(this) + }, + + destroy: function () { + this.element.removeChild(this.textareaClone.element) + this.element.parentNode.insertBefore(this.textarea.element, this.element) + this.element.parentNode.removeChild(this.element) + this.textarea.destroy() + } +} + +function createElement () { + var element = document.createElement('div') + element.className = 'expanding-wrapper' + element.style.position = 'relative' + return element +} + +function resetStyles () { + var styles = { + margin: 0, + webkitBoxSizing: 'border-box', + mozBoxSizing: 'border-box', + boxSizing: 'border-box', + width: '100%' + } + // Should only be called once i.e. on initialization + this.textareaClone.style({ + minHeight: this.textarea.element.offsetHeight + 'px' + }) + this.textareaClone.style(styles) + this.textarea.style(styles) +} + +function setStyles () { + this.textareaClone.style(this.textareaClone.styles(this.textarea.element)) + this.textarea.style(this.textarea.styles()) +} + +export default Expanding diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 0000000..0f6551f --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,49 @@ +var userAgent = window.navigator.userAgent + +// Returns the version of Internet Explorer or -1 +// (indicating the use of another browser). +// From: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx#ParsingUA +var ieVersion = (function () { + var version = -1 + if (window.navigator.appName === 'Microsoft Internet Explorer') { + var regExp = new RegExp('MSIE ([0-9]{1,}[\\.0-9]{0,})') + if (regExp.exec(userAgent) !== null) version = parseFloat(RegExp.$1) + } + return version +})() + +// Check for oninput support +// IE9 supports oninput, but not when deleting text, so keyup is used. +// onpropertychange _is_ supported by IE8/9, but may not be fired unless +// attached with `attachEvent` +// (see: http://stackoverflow.com/questions/18436424/ie-onpropertychange-event-doesnt-fire), +// and so is avoided altogether. +var inputEventSupported = ( + 'oninput' in document.createElement('input') && ieVersion !== 9 +) + +export var inputEvent = inputEventSupported ? 'input' : 'keyup' + +export var isIosDevice = /iPad|iPhone|iPod/.test(userAgent) && !window.MSStream + +export function wrap (element, wrapper) { + element.parentNode.insertBefore(wrapper, element) + wrapper.appendChild(element) +} + +export function style (element, styles) { + for (var property in styles) element.style[property] = styles[property] +} + +export 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) +} + +export function warn (text) { + if (window.console && console.warn) console.warn(text) +} diff --git a/src/textarea-clone.js b/src/textarea-clone.js new file mode 100644 index 0000000..e2a128a --- /dev/null +++ b/src/textarea-clone.js @@ -0,0 +1,73 @@ +import { style, isIosDevice } from './helpers' + +var styleProperties = { + borderBottomWidth: null, + borderLeftWidth: null, + borderRightWidth: null, + borderTopWidth: null, + direction: null, + fontFamily: null, + fontSize: null, + fontSizeAdjust: null, + fontStyle: null, + fontWeight: null, + letterSpacing: null, + lineHeight: null, + maxHeight: null, + paddingBottom: null, + paddingLeft: paddingHorizontal, + paddingRight: paddingHorizontal, + paddingTop: null, + textAlign: null, + textDecoration: null, + textTransform: null, + wordBreak: null, + wordSpacing: null, + wordWrap: null +} + +function paddingHorizontal (computedStyle) { + return isIosDevice ? (parseFloat(computedStyle) + 3) + 'px' : computedStyle +} + +export default function TextareaClone () { + this.element = document.createElement('pre') + this.element.className = 'expanding-clone' + this.innerElement = document.createElement('span') + this.element.appendChild(this.innerElement) + this.element.appendChild(document.createElement('br')) +} + +TextareaClone.prototype = { + value: function (value) { + if (arguments.length === 0) return this.innerElement.textContent + else this.innerElement.textContent = value + }, + + style: function (styles) { + style(this.element, styles) + }, + + styles: function (textarea) { + var wrap = textarea.getAttribute('wrap') + var styles = { + display: 'block', + border: '0 solid', + visibility: 'hidden', + overflowX: wrap === 'off' ? 'scroll' : 'hidden', + whiteSpace: wrap === 'off' ? 'pre' : 'pre-wrap' + } + + var computedStyles = window.getComputedStyle(textarea) + + for (var property in styleProperties) { + var valueFunction = styleProperties[property] + var computedStyle = computedStyles[property] + styles[property] = ( + valueFunction ? valueFunction(computedStyle) : computedStyle + ) + } + + return styles + } +} diff --git a/src/textarea.js b/src/textarea.js new file mode 100644 index 0000000..ab56808 --- /dev/null +++ b/src/textarea.js @@ -0,0 +1,53 @@ +import { style } from './helpers' + +export default function Textarea (element) { + this.element = element + this._eventListeners = {} +} + +Textarea.prototype = { + style: function (styles) { + style(this.element, styles) + }, + + styles: function () { + return { + position: 'absolute', + top: 0, + left: 0, + height: '100%', + resize: 'none', + overflow: 'auto' + } + }, + + value: function (value) { + if (arguments.length === 0) { + return this.element.value.replace(/\r\n/g, '\n') + } else { + this.element.value = value + } + }, + + on: function (eventName, handler) { + this.element.addEventListener(eventName, handler) + this._eventListeners[eventName] = handler + }, + + off: function (eventName) { + if (arguments.length === 0) { + for (var event in this._eventListeners) { this.off(event) } + } else { + this.element.removeEventListener( + eventName, + this._eventListeners[eventName] + ) + delete this._eventListeners[eventName] + } + }, + + destroy: function () { + this.element.setAttribute('style', this.oldStyleAttribute || '') + this.off() + } +} 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 = $('