<!doctype html>
<html>
<head>
	<meta charset="utf-8" />

	<title>
		Trying To Enable Tabbing In Textareas In Angular 2 Beta 17
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>

	<h1>
		Trying To Enable Tabbing In Textareas In Angular 2 Beta 17
	</h1>

	<my-app>
		Loading...
	</my-app>

	<!-- Load demo scripts. -->
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/es6-shim.min.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/Rx.umd.min.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/angular2-polyfills.min.js"></script>
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/angular2-all.umd.js"></script>
	<!-- AlmondJS - minimal implementation of RequireJS. -->
	<script type="text/javascript" src="../../vendor/angularjs-2-beta/17/almond.js"></script>
	<script type="text/javascript">

		// Defer bootstrapping until all of the components have been declared.
		requirejs(
			[ /* Using require() for better readability. */ ],
			function run() {

				ng.platform.browser.bootstrap( require( "App" ) );

			}
		);


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I provide the root application component.
		define(
			"App",
			function registerApp() {

				// Configure the App component definition.
				ng.core
					.Component({
						selector: "my-app",
						directives: [ require( "TabEnabled" ) ],

						// In our textarea, we are using the tabEnabled directive to add
						// adding custom key-press behavior; and, we're doing so in a way
						// that needs to play nicely with the events that drive ngModel.
						// That's one of the hardest parts of feature augmentation - not
						// just the base implementation; but, implementing it in a way
						// that doesn't break other "expected" behavior.
						template:
						`
							<textarea
								[(ngModel)]="content"
								(ngModelChange)="logCurrentValue()"
								tabEnabled
								autofocus>
							</textarea>
						`
					})
					.Class({
						constructor: AppController
					})
				;

				return( AppController );


				// I control the App component.
				function AppController() {

					var vm = this;

					// I hold the content of the textarea. We're using the ngModel
					// directive to facilitate two-way data binding. As such, the user's
					// edits will automatically be pushed back into this property.
					vm.content = "Hello world.";

					// Expose the public methods.
					vm.logCurrentValue = logCurrentValue;


					// ---
					// PUBLIC METHODS.
					// ---


					// I log the current value (that has been synchronized by ngModel).
					function logCurrentValue() {

						console.log( "(ngModelChange):", vm.content );

					}

				}

			}
		);


		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //


		// I provide a directive that enables tabbing and shift-tabbing in a textarea.
		define(
			"TabEnabled",
			function registerTabEnabled() {

				// Configure the TabEnabled directive definition.
				ng.core
					.Directive({
						selector: "textarea[tabEnabled]",
						host: {
							"(keydown.tab)": "handleTab( $event )",
							"(keydown.shift.tab)": "handleShiftTab( $event )",
							"(keydown.enter)": "handleEnter( $event )"
						}
					})
					.Class({
						constructor: TabEnabledController
					})
				;

				TabEnabledController.parameters = [
					new ng.core.Inject( ng.core.ElementRef )
				];

				return( TabEnabledController );


				// I control the TabEnabled directive.
				function TabEnabledController( elementRef ) {

					var vm = this;

					// I hold the tab and newline implementations.
					// --
					// TODO: Wire "tab" into input binding so it can be dynamically
					// defined (such as if someone went CRAZY and wanted spaces).
					var newline = getNewlineImplementation();
					var tab = "\t";

					// Expose the public methods.
					vm.handleEnter = handleEnter;
					vm.handleShiftTab = handleShiftTab;
					vm.handleTab = handleTab;


					// ---
					// PUBLIC METHODS.
					// ---


					// I handle the Enter key combination.
					function handleEnter( event ) {

						event.preventDefault();

						// If we end up changing the textarea value, we need to dispatch
						// a custom (input) event so that we play nicely with other
						// directives (like ngModel) and event handlers.
						if ( setConfig( insertEnterAtSelection( getConfig() ) ) ) {

							dispatchInputEvent();

						}

					}


					// I handle the Shift+Tab key combination.
					function handleShiftTab( event ) {

						event.preventDefault();

						// If we end up changing the textarea value, we need to dispatch
						// a custom (input) event so that we play nicely with other
						// directives (like ngModel) and event handlers.
						if ( setConfig( removeTabAtSelection( getConfig() ) ) ) {

							dispatchInputEvent();

						}

					}


					// I handle the Tab key combination.
					function handleTab( event ) {

						event.preventDefault();

						// If we end up changing the textarea value, we need to dispatch
						// a custom (input) event so that we play nicely with other
						// directives (like ngModel) and event handlers.
						if ( setConfig( insertTabAtSelection( getConfig() ) ) ) {

							dispatchInputEvent();

						}

					}


					// ---
					// PRIVATE METHODS.
					// ---


					// I dispatch a custom (input) event.
					function dispatchInputEvent() {

						var bubbles = true;
						var cancelable = false;

						// IE (shakes fist) uses some other kind of event initialization.
						// As such, we'll default to trying the "normal" event generation
						// and then fallback to using the IE version.
						try {

							var inputEvent = new CustomEvent(
								"input",
								{
									bubbles: bubbles,
									cancelable: cancelable
								}
							);

						} catch ( error ) {

							var inputEvent = document.createEvent( "CustomEvent" );

							inputEvent.initCustomEvent( "input", bubbles, cancelable );

						}

						elementRef.nativeElement.dispatchEvent( inputEvent );

					}


					// I find the index of the line-start that contains the given offset.
					function findStartOfLine( value, offset ) {

						var delimiter = /[\r\n]/i;

						for ( var i = ( offset - 1 ) ; i >= 0 ; i-- ) {

							if ( delimiter.test( value.charAt( i ) ) ) {

								return( i + 1 );

							}

						}

						return( 0 );

					}


					// I get the current selection and value configuration for the
					// textarea element.
					function getConfig() {

						var element = elementRef.nativeElement;

						return({
							value: element.value,
							start: element.selectionStart,
							end: element.selectionEnd
						});

					}


					// I calculate and return the newline implementation. Different
					// operating systems and browsers implement a "newline" with different
					// character combinations.
					function getNewlineImplementation() {

						var fragment = document.createElement( "textarea" );
						fragment.value = "\r\n";

						return( fragment.value );

					}


					// I apply the Enter key combination to the given configuration.
					function insertEnterAtSelection( config ) {

						var value = config.value;
						var start = config.start;
						var end = config.end;

						var leadingTabs = value
							.slice( findStartOfLine( value, start ), start )
							.match( new RegExp( ( "^(?:" + tab + ")+" ), "i" ) )
						;

						var tabCount = leadingTabs
							? leadingTabs[ 0 ].length
							: 0
						;

						var preDelta = value.slice( 0, start );
						var postDelta = value.slice( start );
						var delta = ( newline + repeat( tab, tabCount ) );

						return({
							value: ( preDelta + delta + postDelta ),
							start: ( start + delta.length ),
							end: ( end + delta.length )
						});

					}


					// I apply the Tab key combination to the given configuration.
					function insertTabAtSelection( config ) {

						var value = config.value;
						var start = config.start;
						var end = config.end;

						var deltaStart = ( start === end )
							? start
							: findStartOfLine( value, start )
						;
						var deltaEnd = end;
						var deltaValue = value.slice( deltaStart, deltaEnd );

						var preDelta = value.slice( 0, deltaStart );
						var postDelta = value.slice( deltaEnd );

						var replacement = deltaValue.replace( new RegExp( ( "(^|" + newline + ")" ), "g" ), ( "$1" + tab ) );

						var newValue = ( preDelta + replacement + postDelta );
						var newStart = ( start + tab.length );
						var newEnd = ( end + ( replacement.length - deltaValue.length ) );

						return({
							value: newValue,
							start: newStart,
							end: newEnd
						});

					}


					// I apply the Shift+Tab key combination to the given configuration.
					function removeTabAtSelection( config ) {

						var value = config.value;
						var start = config.start;
						var end = config.end;

						var deltaStart = findStartOfLine( value, start )
						var deltaEnd = end;
						var deltaValue = value.slice( deltaStart, deltaEnd );
						var deltaHasLeadingTab = ( deltaValue.indexOf( tab ) === 0 );

						var preDelta = value.slice( 0, deltaStart );
						var postDelta = value.slice( deltaEnd );

						var replacement = deltaValue.replace( new RegExp( ( "^" + tab ), "gm" ), "" );

						var newValue = ( preDelta + replacement + postDelta );
						var newStart = deltaHasLeadingTab
							? ( start - tab.length )
							: start
						;
						var newEnd = ( end - ( deltaValue.length - replacement.length ) );

						return({
							value: newValue,
							start: newStart,
							end: newEnd
						});

					}


					// I repeat the given string the given number of times.
					function repeat( value, count ) {

						return( new Array( count + 1 ).join( value ) );

					}


					// I apply the given config to the textarea and return a flag
					// indicating as to whether or not any changes were precipitated.
					function setConfig( config ) {

						var element = elementRef.nativeElement;

						// If the value hasn't actually changed, just return out. There's
						// no need to set the selection if nothing changed.
						if ( config.value === element.value ) {

							return( false );

						}

						element.value = config.value;
						element.selectionStart = config.start;
						element.selectionEnd = config.end;

						return( true );

					}

				}

			}
		);

	</script>

</body>
</html>