mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 21:27:40 +00:00
Merge pull request #25 from glyptodon/guac-input
GUAC-810: Implement guacTextInput directive.
This commit is contained in:
115
guacamole/src/main/webapp/app/textInput/directives/guacKey.js
Normal file
115
guacamole/src/main/webapp/app/textInput/directives/guacKey.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Glyptodon LLC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which displays a button that controls the pressed state of a
|
||||
* single keyboard key.
|
||||
*/
|
||||
angular.module('textInput').directive('guacKey', [function guacKey() {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
|
||||
/**
|
||||
* The text to display within the key. This will be run through the
|
||||
* translation filter prior to display.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
text : '=',
|
||||
|
||||
/**
|
||||
* The keysym to send within keyup and keydown events when this key
|
||||
* is pressed or released.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
keysym : '=',
|
||||
|
||||
/**
|
||||
* Whether this key is sticky. Sticky keys toggle their pressed
|
||||
* state with each click.
|
||||
*
|
||||
* @type Boolean
|
||||
* @default false
|
||||
*/
|
||||
sticky : '=?',
|
||||
|
||||
/**
|
||||
* Whether this key is currently pressed.
|
||||
*
|
||||
* @type Boolean
|
||||
* @default false
|
||||
*/
|
||||
pressed : '=?'
|
||||
|
||||
},
|
||||
|
||||
templateUrl: 'app/textInput/templates/guacKey.html',
|
||||
controller: ['$scope', '$rootScope',
|
||||
function guacKey($scope, $rootScope) {
|
||||
|
||||
// Not sticky by default
|
||||
$scope.sticky = $scope.sticky || false;
|
||||
|
||||
// Unpressed by default
|
||||
$scope.pressed = $scope.pressed || false;
|
||||
|
||||
/**
|
||||
* Presses and releases this key, sending the corresponding keydown
|
||||
* and keyup events. In the case of sticky keys, the pressed state
|
||||
* is toggled, and only a single keydown/keyup event will be sent,
|
||||
* depending on the current state.
|
||||
*/
|
||||
$scope.updateKey = function updateKey() {
|
||||
|
||||
// If sticky, toggle pressed state
|
||||
if ($scope.sticky)
|
||||
$scope.pressed = !$scope.pressed;
|
||||
|
||||
// For all non-sticky keys, press and release key immediately
|
||||
else {
|
||||
$rootScope.$broadcast('guacKeydown', $scope.keysym);
|
||||
$rootScope.$broadcast('guacKeyup', $scope.keysym);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Send keyup/keydown when pressed state is altered
|
||||
$scope.$watch('pressed', function updatePressedState(isPressed, wasPressed) {
|
||||
|
||||
// If the key is pressed now, send keydown
|
||||
if (isPressed)
|
||||
$rootScope.$broadcast('guacKeydown', $scope.keysym);
|
||||
|
||||
// If the key was pressed, but is not pressed any longer, send keyup
|
||||
else if (wasPressed)
|
||||
$rootScope.$broadcast('guacKeyup', $scope.keysym);
|
||||
|
||||
});
|
||||
|
||||
}]
|
||||
|
||||
};
|
||||
}]);
|
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Glyptodon LLC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A directive which displays the Guacamole text input method.
|
||||
*/
|
||||
angular.module('textInput').directive('guacTextInput', [function guacTextInput() {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {},
|
||||
|
||||
templateUrl: 'app/textInput/templates/guacTextInput.html',
|
||||
controller: ['$scope', '$rootScope', '$element', '$timeout',
|
||||
function guacTextInput($scope, $rootScope, $element, $timeout) {
|
||||
|
||||
/**
|
||||
* The number of characters to include on either side of text input
|
||||
* content, to allow the user room to use backspace and delete.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var TEXT_INPUT_PADDING = 128;
|
||||
|
||||
/**
|
||||
* The Unicode codepoint of the character to use for padding on
|
||||
* either side of text input content.
|
||||
*
|
||||
* @type Number
|
||||
*/
|
||||
var TEXT_INPUT_PADDING_CODEPOINT = 0x200B;
|
||||
|
||||
/**
|
||||
* Recently-sent text, ordered from oldest to most recent.
|
||||
*
|
||||
* @type String[]
|
||||
*/
|
||||
$scope.sentText = [];
|
||||
|
||||
/**
|
||||
* Whether the "Alt" key is currently pressed within the text input
|
||||
* interface.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
$scope.altPressed = false;
|
||||
|
||||
/**
|
||||
* Whether the "Ctrl" key is currently pressed within the text
|
||||
* input interface.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
$scope.ctrlPressed = false;
|
||||
|
||||
/**
|
||||
* The text area input target.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var target = $element.find('.target')[0];
|
||||
|
||||
/**
|
||||
* Whether the text input target currently has focus. Setting this
|
||||
* attribute has no effect, but any bound property will be updated
|
||||
* as focus is gained or lost.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
var hasFocus = false;
|
||||
|
||||
target.onfocus = function targetFocusGained() {
|
||||
hasFocus = true;
|
||||
resetTextInputTarget(TEXT_INPUT_PADDING);
|
||||
};
|
||||
|
||||
target.onblur = function targetFocusLost() {
|
||||
hasFocus = false;
|
||||
target.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether composition is currently active within the text input
|
||||
* target element, such as when an IME is in use.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
var composingText = false;
|
||||
|
||||
target.addEventListener("compositionstart", function targetComposeStart(e) {
|
||||
composingText = true;
|
||||
}, false);
|
||||
|
||||
target.addEventListener("compositionend", function targetComposeEnd(e) {
|
||||
composingText = false;
|
||||
}, false);
|
||||
|
||||
/**
|
||||
* Translates a given Unicode codepoint into the corresponding X11
|
||||
* keysym.
|
||||
*
|
||||
* @param {Number} codepoint
|
||||
* The Unicode codepoint to translate.
|
||||
*
|
||||
* @returns {Number}
|
||||
* The X11 keysym that corresponds to the given Unicode
|
||||
* codepoint, or null if no such keysym exists.
|
||||
*/
|
||||
var keysymFromCodepoint = function keysymFromCodepoint(codepoint) {
|
||||
|
||||
// Keysyms for control characters
|
||||
if (codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F))
|
||||
return 0xFF00 | codepoint;
|
||||
|
||||
// Keysyms for ASCII chars
|
||||
if (codepoint >= 0x0000 && codepoint <= 0x00FF)
|
||||
return codepoint;
|
||||
|
||||
// Keysyms for Unicode
|
||||
if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
|
||||
return 0x01000000 | codepoint;
|
||||
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Presses and releases the key corresponding to the given keysym,
|
||||
* as if typed by the user.
|
||||
*
|
||||
* @param {Number} keysym The keysym of the key to send.
|
||||
*/
|
||||
var sendKeysym = function sendKeysym(keysym) {
|
||||
$rootScope.$broadcast('guacKeydown', keysym);
|
||||
$rootScope.$broadcast('guacKeyup', keysym);
|
||||
};
|
||||
|
||||
/**
|
||||
* Presses and releases the key having the keysym corresponding to
|
||||
* the Unicode codepoint given, as if typed by the user.
|
||||
*
|
||||
* @param {Number} codepoint
|
||||
* The Unicode codepoint of the key to send.
|
||||
*/
|
||||
var sendCodepoint = function sendCodepoint(codepoint) {
|
||||
|
||||
if (codepoint === 10) {
|
||||
sendKeysym(0xFF0D);
|
||||
releaseStickyKeys();
|
||||
return;
|
||||
}
|
||||
|
||||
var keysym = keysymFromCodepoint(codepoint);
|
||||
if (keysym) {
|
||||
sendKeysym(keysym);
|
||||
releaseStickyKeys();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates each character within the given string to keysyms and
|
||||
* sends each, in order, as if typed by the user.
|
||||
*
|
||||
* @param {String} content
|
||||
* The string to send.
|
||||
*/
|
||||
var sendString = function sendString(content) {
|
||||
|
||||
var sentText = "";
|
||||
|
||||
// Send each codepoint within the string
|
||||
for (var i=0; i<content.length; i++) {
|
||||
var codepoint = content.charCodeAt(i);
|
||||
if (codepoint !== TEXT_INPUT_PADDING_CODEPOINT) {
|
||||
sentText += String.fromCharCode(codepoint);
|
||||
sendCodepoint(codepoint);
|
||||
}
|
||||
}
|
||||
|
||||
// Display the text that was sent
|
||||
$scope.$apply(function addSentText() {
|
||||
$scope.sentText.push(sentText);
|
||||
});
|
||||
|
||||
// Remove text after one second
|
||||
$timeout(function removeSentText() {
|
||||
$scope.sentText.shift();
|
||||
}, 1000);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Releases all currently-held sticky keys within the text input UI.
|
||||
*/
|
||||
var releaseStickyKeys = function releaseStickyKeys() {
|
||||
|
||||
// Reset all sticky keys
|
||||
$scope.$apply(function clearAllStickyKeys() {
|
||||
$scope.altPressed = false;
|
||||
$scope.ctrlPressed = false;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all content from the text input target, replacing it
|
||||
* with the given number of padding characters. Padding of the
|
||||
* requested size is added on both sides of the cursor, thus the
|
||||
* overall number of characters added will be twice the number
|
||||
* specified.
|
||||
*
|
||||
* @param {Number} padding
|
||||
* The number of characters to pad the text area with.
|
||||
*/
|
||||
var resetTextInputTarget = function resetTextInputTarget(padding) {
|
||||
|
||||
var paddingChar = String.fromCharCode(TEXT_INPUT_PADDING_CODEPOINT);
|
||||
|
||||
// Pad text area with an arbitrary, non-typable character (so there is something
|
||||
// to delete with backspace or del), and position cursor in middle.
|
||||
target.value = new Array(padding*2 + 1).join(paddingChar);
|
||||
target.setSelectionRange(padding, padding);
|
||||
|
||||
};
|
||||
|
||||
target.addEventListener("input", function(e) {
|
||||
|
||||
// Ignore input events during text composition
|
||||
if (composingText)
|
||||
return;
|
||||
|
||||
var i;
|
||||
var content = target.value;
|
||||
var expectedLength = TEXT_INPUT_PADDING*2;
|
||||
|
||||
// If content removed, update
|
||||
if (content.length < expectedLength) {
|
||||
|
||||
// Calculate number of backspaces and send
|
||||
var backspaceCount = TEXT_INPUT_PADDING - target.selectionStart;
|
||||
for (i = 0; i < backspaceCount; i++)
|
||||
sendKeysym(0xFF08);
|
||||
|
||||
// Calculate number of deletes and send
|
||||
var deleteCount = expectedLength - content.length - backspaceCount;
|
||||
for (i = 0; i < deleteCount; i++)
|
||||
sendKeysym(0xFFFF);
|
||||
|
||||
}
|
||||
|
||||
else
|
||||
sendString(content);
|
||||
|
||||
// Reset content
|
||||
resetTextInputTarget(TEXT_INPUT_PADDING);
|
||||
e.preventDefault();
|
||||
|
||||
}, false);
|
||||
|
||||
// Do not allow event target contents to be selected during input
|
||||
target.addEventListener("selectstart", function(e) {
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
|
||||
}]
|
||||
|
||||
};
|
||||
}]);
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Glyptodon LLC
|
||||
* Copyright (C) 2014 Glyptodon LLC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -20,33 +20,30 @@
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#text-input {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
||||
.text-input {
|
||||
width: 100%;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.5);
|
||||
background: #CDA;
|
||||
|
||||
}
|
||||
|
||||
#text-input #text-input-field,
|
||||
#text-input #text-input-buttons {
|
||||
.text-input .text-input-field,
|
||||
.text-input .text-input-buttons {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#text-input #text-input-field {
|
||||
.text-input .text-input-field {
|
||||
width: 30%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#text-input #text-input-buttons {
|
||||
.text-input .text-input-buttons {
|
||||
width: 70%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#text-input #target {
|
||||
.text-input .target {
|
||||
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
@@ -67,18 +64,18 @@
|
||||
|
||||
}
|
||||
|
||||
#text-input.open {
|
||||
.text-input.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#text-input #sent-history {
|
||||
.text-input .sent-history {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.25em;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
#text-input #sent-history .sent-text {
|
||||
.text-input .sent-history .sent-text {
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
white-space: pre;
|
||||
@@ -89,7 +86,7 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#text-input #text-input-buttons button {
|
||||
.text-input .text-input-buttons button {
|
||||
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||
background: none;
|
||||
color: black;
|
||||
@@ -101,8 +98,8 @@
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
#text-input #text-input-buttons button:active,
|
||||
#text-input #text-input-buttons button.pressed {
|
||||
.text-input .text-input-buttons button:active,
|
||||
.text-input .text-input-buttons button.pressed {
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
color: white;
|
@@ -0,0 +1,25 @@
|
||||
<button class="key" ng-click="updateKey()" ng-class="{pressed: pressed, sticky: sticky}">
|
||||
<!--
|
||||
Copyright (C) 2014 Glyptodon LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
-->
|
||||
|
||||
{{text | translate}}
|
||||
</button>
|
@@ -0,0 +1,27 @@
|
||||
<div class="text-input">
|
||||
<!--
|
||||
Copyright (C) 2014 Glyptodon LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
-->
|
||||
|
||||
<!-- Text input target -->
|
||||
<div class="text-input-field"><div class="sent-history"><div class="sent-text" ng-repeat="text in sentText track by $index">{{text}}</div></div><textarea rows="1" class="target"></textarea></div><div class="text-input-buttons"><guac-key keysym="65507" sticky="true" text="'client.ctrl'" pressed="ctrlPressed"></guac-key><guac-key keysym="65513" sticky="true" text="'client.alt'" pressed="altPressed"></guac-key><guac-key keysym="65307" text="'client.esc'"></guac-key><guac-key keysym="65289" text="'client.tab'"></guac-key></div>
|
||||
|
||||
</div>
|
26
guacamole/src/main/webapp/app/textInput/textInputModule.js
Normal file
26
guacamole/src/main/webapp/app/textInput/textInputModule.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Glyptodon LLC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module for displaying the Guacamole text input method.
|
||||
*/
|
||||
angular.module('textInput', []);
|
Reference in New Issue
Block a user