Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created May 11, 2016 11:09
Trying To Enable Tabbing In Textareas In Angular 2 Beta 17
// 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 );
}
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment