Created
May 11, 2016 11:09
Trying To Enable Tabbing In Textareas In Angular 2 Beta 17
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 ); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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