I have a textarea which allows users to put in 16 lines. I've build an directive for that purpose, and everything works fine, if the user hits enter.
But I also want to prevent more than 16 lines, even if the user does not hit enter, but puts in a very long text, which is displayed into multiple lines (forced line break).
The background of this question is the following: I have a postcard, and users should be able to enter text to this postcard. The postcard has a fixed width/height. The textarea should represent the fixed width/height of the postcard, so users can see how many space they have left to fill out the postcard (not more than 16 lines).
Is this possible with JS?
My code so far:
HTML
<textarea placeholder="Enter text" rows="16" ng-trim="false" id="message-textarea" maxlines="16" maxlines-prevent-enter="true"></textarea>
JS Directive
app.directive('maxlines', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attrs, ngModel) {
var maxLines = 1;
attrs.$observe('maxlines', function(val) {
maxLines = parseInt(val);
});
ngModel.$validators.maxlines = function(modelValue, viewValue) {
var numLines = (modelValue || '').split("\n").length;
var diffLines = maxLines - numLines;
scope.$emit('cliked-from-directive-maxlines', {diffLines});
return numLines <= maxLines;
};
attrs.$observe('maxlinesPreventEnter', function(preventEnter) {
// if attribute value starts with 'f', treat as false. Everything else is true
preventEnter = (preventEnter || '').toLocaleLowerCase().indexOf('f') !== 0;
if (preventEnter) {
addKeypress();
} else {
removeKeypress();
}
});
function addKeypress() {
elem.on('keypress', function(event) {
// test if adding a newline would cause the validator to fail
if (event.keyCode == 13 && !ngModel.$validators.maxlines(ngModel.$modelValue + '\n', ngModel.$viewValue + '\n')) {
event.preventDefault();
}
});
}
function removeKeypress() {
elem.off('.maxlines');
}
scope.$on('$destroy', removeKeypress);
}
};
});
AFAIk there is no way to read or restrict the number of lines of a textarea. Your best chance IMHO is using 16 single-line inputs and focus the next row whenever the user hits the chars-per-line limit.
Try this
function addKeypress() {
var lines = 16;
elem.on('keypress', function(e) {
var newLines = elem.val().split("\n").length;
if(e.keyCode == 13 && newLines >= lines) {
return false;
}
});
});
}
Maybe you can do something like this:
JavaScript
$(".limit").on("input", function(evt) {
var $limit = $(this);
var limit = this;
if($limit.innerHeight() !== limit.scrollHeight) {
$limit.val($limit.data("before"));
evt.preventDefault();
return false;
} else {
$limit.data("before", $limit.val());
}
}).each(function(index, el) {
$(el).data("before", $(el).val());
});
HTML
<textarea class="limit" rows="10"></textarea>
CSS
.limit {
max-width: 300px;
min-width: 300px;
resize: none;
overflow: hidden;
}
Related
Which characters are considered unsafe to allow for users to type in a text input field, to prevent hackers etc to perform SQL injections etc? Which characters should be blocked from the input?
For the record, I'm currently blocking the input with AngularJS using the following HTML:
<input type="text" ng-pattern-restrict='^[^<>#*"]+$'>
And here's the beautiful directive I found:
/* RESTRICT CERTAIN CHARACTERS IN INPUT FIELDS
<input type="text" ng-pattern-restrict="^[A-Za-z0-9]*$">
Alpha numeric chars only ^[A-Za-z0-9]*$
Date format YYYY-MM-DD ^\d{0,4}(-\d{0,2}(-\d{0,2})?)?$
*/
/*jslint browser: true, plusplus: true, indent: 2 */
// This will be removed by uglify, along with the DEBUG code
if (typeof DEBUG === 'undefined') {
DEBUG = true;
}
// Logic and fallbacks based on the following SO answers:
// - Getting caret position cross browser: http://stackoverflow.com/a/9370239/147507
// - Selection API on non input-text fields: http://stackoverflow.com/a/24247942/147507
// - Set cursor position on input text: http://stackoverflow.com/q/5755826/147507
angular.module('ngPatternRestrict', [])
.directive('ngPatternRestrict', ['$log', function ($log) {
'use strict';
function showDebugInfo() {
$log.debug("[ngPatternRestrict] " + Array.prototype.join.call(arguments, ' '));
}
return {
restrict: 'A',
require: "?ngModel",
compile: function uiPatternRestrictCompile() {
DEBUG && showDebugInfo("Loaded");
return function ngPatternRestrictLinking(scope, iElement, iAttrs, ngModelController) {
var regex, // validation regex object
oldValue, // keeping track of the previous value of the element
caretPosition, // keeping track of where the caret is at to avoid jumpiness
// housekeeping
initialized = false, // have we initialized our directive yet?
eventsBound = false, // have we bound our events yet?
// functions
getCaretPosition, // function to get the caret position, set in detectGetCaretPositionMethods
setCaretPosition; // function to set the caret position, set in detectSetCaretPositionMethods
//-------------------------------------------------------------------
// caret position
function getCaretPositionWithInputSelectionStart() {
return iElement[0].selectionStart; // we need to go under jqlite
}
function getCaretPositionWithDocumentSelection() {
// create a selection range from where we are to the beggining
// and measure how much we moved
var range = document.selection.createRange();
range.moveStart('character', -iElement.val().length);
return range.text.length;
}
function getCaretPositionWithWindowSelection() {
var s = window.getSelection(),
originalSelectionLength = String(s).length,
selectionLength,
didReachZero = false,
detectedCaretPosition,
restorePositionCounter;
do {
selectionLength = String(s).length;
s.modify('extend', 'backward', 'character');
// we're undoing a selection, and starting a new one towards the beggining of the string
if (String(s).length === 0) {
didReachZero = true;
}
} while (selectionLength !== String(s).length);
detectedCaretPosition = didReachZero ? selectionLength : selectionLength - originalSelectionLength;
s.collapseToStart();
restorePositionCounter = detectedCaretPosition;
while (restorePositionCounter-- > 0) {
s.modify('move', 'forward', 'character');
}
while (originalSelectionLength-- > 0) {
s.modify('extend', 'forward', 'character');
}
return detectedCaretPosition;
}
function setCaretPositionWithSetSelectionRange(position) {
iElement[0].setSelectionRange(position, position);
}
function setCaretPositionWithCreateTextRange(position) {
var textRange = iElement[0].createTextRange();
textRange.collapse(true);
textRange.moveEnd('character', position);
textRange.moveStart('character', position);
textRange.select();
}
function setCaretPositionWithWindowSelection(position) {
var s = window.getSelection(),
selectionLength;
do {
selectionLength = String(s).length;
s.modify('extend', 'backward', 'line');
} while (selectionLength !== String(s).length);
s.collapseToStart();
while (position--) {
s.modify('move', 'forward', 'character');
}
}
// HACK: Opera 12 won't give us a wrong validity status although the input is invalid
// we can select the whole text and check the selection size
// Congratulations to IE 11 for doing the same but not returning the selection.
function getValueLengthThroughSelection(input) {
// only do this on opera, since it'll mess up the caret position
// and break Firefox functionality
if (!/Opera/i.test(navigator.userAgent)) {
return 0;
}
input.focus();
document.execCommand("selectAll");
var focusNode = window.getSelection().focusNode;
return (focusNode || {}).selectionStart || 0;
}
//-------------------------------------------------------------------
// event handlers
function revertToPreviousValue() {
if (ngModelController) {
scope.$apply(function () {
ngModelController.$setViewValue(oldValue);
});
}
iElement.val(oldValue);
if (!angular.isUndefined(caretPosition)) {
setCaretPosition(caretPosition);
}
}
function updateCurrentValue(newValue) {
oldValue = newValue;
caretPosition = getCaretPosition();
}
function genericEventHandler(evt) {
DEBUG && showDebugInfo("Reacting to event:", evt.type);
//HACK Chrome returns an empty string as value if user inputs a non-numeric string into a number type input
// and this may happen with other non-text inputs soon enough. As such, if getting the string only gives us an
// empty string, we don't have the chance of validating it against a regex. All we can do is assume it's wrong,
// since the browser is rejecting it either way.
var newValue = iElement.val(),
inputValidity = iElement.prop("validity");
if (newValue === "" && iElement.attr("type") !== "text" && inputValidity && inputValidity.badInput) {
DEBUG && showDebugInfo("Value cannot be verified. Should be invalid. Reverting back to:", oldValue);
evt.preventDefault();
revertToPreviousValue();
} else if (newValue === "" && getValueLengthThroughSelection(iElement[0]) !== 0) {
DEBUG && showDebugInfo("Invalid input. Reverting back to:", oldValue);
evt.preventDefault();
revertToPreviousValue();
} else if (regex.test(newValue)) {
DEBUG && showDebugInfo("New value passed validation against", regex, newValue);
updateCurrentValue(newValue);
} else {
DEBUG && showDebugInfo("New value did NOT pass validation against", regex, newValue, "Reverting back to:", oldValue);
evt.preventDefault();
revertToPreviousValue();
}
}
//-------------------------------------------------------------------
// setup based on attributes
function tryParseRegex(regexString) {
try {
regex = new RegExp(regexString);
} catch (e) {
throw "Invalid RegEx string parsed for ngPatternRestrict: " + regexString;
}
}
//-------------------------------------------------------------------
// setup events
function bindListeners() {
if (eventsBound) {
return;
}
iElement.bind('input keyup click', genericEventHandler);
DEBUG && showDebugInfo("Bound events: input, keyup, click");
}
function unbindListeners() {
if (!eventsBound) {
return;
}
iElement.unbind('input', genericEventHandler);
//input: HTML5 spec, changes in content
iElement.unbind('keyup', genericEventHandler);
//keyup: DOM L3 spec, key released (possibly changing content)
iElement.unbind('click', genericEventHandler);
//click: DOM L3 spec, mouse clicked and released (possibly changing content)
DEBUG && showDebugInfo("Unbound events: input, keyup, click");
eventsBound = false;
}
//-------------------------------------------------------------------
// initialization
function readPattern() {
var entryRegex = !!iAttrs.ngPatternRestrict ? iAttrs.ngPatternRestrict : iAttrs.pattern;
DEBUG && showDebugInfo("RegEx to use:", entryRegex);
tryParseRegex(entryRegex);
}
function notThrows(testFn, shouldReturnTruthy) {
try {
return testFn() || !shouldReturnTruthy;
} catch (e) {
return false;
}
}
function detectGetCaretPositionMethods() {
var input = iElement[0];
// Chrome will throw on input.selectionStart of input type=number
// See http://stackoverflow.com/a/21959157/147507
if (notThrows(function () { return input.selectionStart; })) {
getCaretPosition = getCaretPositionWithInputSelectionStart;
} else {
// IE 9- will use document.selection
// TODO support IE 11+ with document.getSelection()
if (notThrows(function () { return document.selection; }, true)) {
getCaretPosition = getCaretPositionWithDocumentSelection;
} else {
getCaretPosition = getCaretPositionWithWindowSelection;
}
}
}
function detectSetCaretPositionMethods() {
var input = iElement[0];
if (typeof input.setSelectionRange === 'function') {
setCaretPosition = setCaretPositionWithSetSelectionRange;
} else if (typeof input.createTextRange === 'function') {
setCaretPosition = setCaretPositionWithCreateTextRange;
} else {
setCaretPosition = setCaretPositionWithWindowSelection;
}
}
function initialize() {
if (initialized) {
return;
}
DEBUG && showDebugInfo("Initializing");
readPattern();
oldValue = iElement.val();
if (!oldValue) {
oldValue = "";
}
DEBUG && showDebugInfo("Original value:", oldValue);
bindListeners();
detectGetCaretPositionMethods();
detectSetCaretPositionMethods();
initialized = true;
}
function uninitialize() {
DEBUG && showDebugInfo("Uninitializing");
unbindListeners();
}
iAttrs.$observe("ngPatternRestrict", readPattern);
iAttrs.$observe("pattern", readPattern);
scope.$on("$destroy", uninitialize);
initialize();
};
}
};
}]);
Please note that it's also necessary to load this directive in the beginning:
var app = angular.module("myApp", ['ngPatternRestrict']);
As others have pointed out, you should solve this on the back-end by using prepared statements. It's the only way as the client can be edited relatively easily.
In Java, for example, you can use this:
PreparedStatement stmt = conn.prepareStatement("INSERT INTO student VALUES(?)");
stmt.setString(1, userInput);
stmt.execute();
This way the input is placed into the query as plain text, so SQL injection will not be an issue and Little Bobby Tables can go to your school.
Consider the following simple md-input-container with textarea inside:
<md-input-container class="md-block">
<textarea aria-label="tt" ng-model="obj.prop" md-maxlength="250" rows="5"></textarea>
</md-input-container>
When i update obj.prop in my controller, the text is changed (therefore i should call $scope.$evalAsync() too). However, the md-char-count is still not updated. In order it to be updated, user will click on text-area and change it. Only then it is being changed.
I think its a known bug but is there any improvements or is there a -at least- a workaround to this problem?
Here you find a codepen
ps(if needed):angular-material version 1.0.1 & angular 1.4.5
I resolved this issue by override - md-maxlength directive.
Try this codepen - http://codepen.io/himvins/pen/JEgyOK?editors=1010
Function for overriding md-maxlength:
function override_mdMaxlength($provide) {
$provide.decorator(
'mdMaxlengthDirective',
function($delegate) {
var mdMaxlength = $delegate[0];
var link = mdMaxlength.link;
mdMaxlength.compile = function() {
//Line 18 to 64: Code of md-maxlength directive. Change in line 62
return function(scope, element, attr, ctrls) {
var maxlength;
var ngModelCtrl = ctrls[0];
var containerCtrl = ctrls[1];
var charCountEl = angular.element('<div class="md-char-counter">');
attr.$set('ngTrim', 'false');
containerCtrl.element.append(charCountEl);
ngModelCtrl.$formatters.push(renderCharCount);
ngModelCtrl.$viewChangeListeners.push(renderCharCount);
element.on(
'input keydown',
function() {
renderCharCount(); //make sure it's called with no args
}
);
scope.$watch(attr.mdMaxlength, function(value) {
maxlength = value;
if (angular.isNumber(value) && value > 0) {
if (!charCountEl.parent().length) {
$animate.enter(
charCountEl,
containerCtrl.element,
angular.element(containerCtrl.element[0].lastElementChild)
);
}
renderCharCount();
} else {
$animate.leave(charCountEl);
}
});
ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
if (!angular.isNumber(maxlength) || maxlength < 0) {
return true;
}
return (modelValue || element.val() || viewValue || '').length <= maxlength;
};
function renderCharCount(value) {
//Original code commented
debugger;
if(ngModelCtrl.$modelValue !== "")
charCountEl.text( ( element.val() || value || '' ).length + '/' + maxlength );
else
charCountEl.text((ngModelCtrl.$modelValue || '').length + '/' + maxlength);
return value;
}
};
};
return $delegate;
}
);
}
Add this function into your module's config like below:
yourModule.config(override_mdMaxlength);
This will correct character-count behavior.
I meet this bug too, after look deeper into the mdMaxlength directive, I found that we could solve this problem easily like this(according to your code):
angular.module('app').controller(function($timeout){
$scope.obj={prop:""};
//after user input some value reset the form
$scope.reset=function(){
$scope.obj.prop=null;
$scope.formName.$setPristine();
$scope.formName.$setValidity();
$scope.formName.$setUntouched();
$timeout(function(){
$scope.obj.prop="";
})
}
})
I have created an application in angularjs with ckeditor plugin, I have created a directive for ckeditor, The application is working fine but the issue is that i need to set a max character length to be 50, so i put maxlength="50", but its not working,
Can anyone please tell me some solution for this
JSFiddle
html
<div data-ng-app="app" data-ng-controller="myCtrl">
<h3>CKEditor 4.2:</h3>
<div ng-repeat="editor in ckEditors">
<textarea data-ng-model="editor.value" maxlength="50" data-ck-editor></textarea>
<br />
</div>
<button ng-click="addEditor()">New Editor</button>
</div>
script
var app = angular.module('app', []);
app.directive('ckEditor', [function () {
return {
require: '?ngModel',
link: function ($scope, elm, attr, ngModel) {
var ck = CKEDITOR.replace(elm[0]);
ck.on('pasteState', function () {
$scope.$apply(function () {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function (value) {
ck.setData(ngModel.$modelValue);
};
}
};
}])
function myCtrl($scope){
$scope.ckEditors = [{value: ''}];
}
You need to add an id to your textarea, like this:
<textarea data-ng-model="editor.value" maxlength="50" id="mytext" data-ck-editor></textarea>
You need to handle the key events for CKEDITOR:
window.onload = function() {
CKEDITOR.instances.mytext.on( 'key', function() {
var str = CKEDITOR.instances.mytext.getData();
if (str.length > 50) {
CKEDITOR.instances.mytext.setData(str.substring(0, 50));
}
} );
};
This works, however, note, that the content contains html tags as well, you might want to keep them,
I came across this same issue. I made this function which works with CKEditor to basically mimic the maxlength function.
window.onload = function() {
CKEDITOR.instances.mytext.on('key',function(event){
var deleteKey = 46;
var backspaceKey = 8;
var keyCode = event.data.keyCode;
if (keyCode === deleteKey || keyCode === backspaceKey)
return true;
else
{
var textLimit = 50;
var str = CKEDITOR.instances.mytext.getData();
if (str.length >= textLimit)
return false;
}
});
};
This function will check to make sure the input does not have more than the allowed characters.
If it does it will return false which simply does not allow any more inputs into the field.
If the user presses backspace or delete then it will return true which still allows the user to change their content if they reach the content limit.
I would like to implement such a directive for AngularJS: if used in input[text], on hitting enter the focus would move to next input. What would be the best way to accomplish this?
I have a very large form and would like to implement a way to go over the fields in a fast way.
Check this FIDDLE
There is ngKeypress directive in AngularJS, you can read more in here.
If your form is static as you mentioned, easiest way to accomplish what you need is passing next input's id (or index in my example). It's up to you how to provide ids, you can pass entire id or an index.
<input type="text" ng-model="field1" id="f_1"
ng-keypress="keypressHandler($event, 2)"/>
<br/>
<input type="text" ng-model="field2" id="f_2"
ng-keypress="keypressHandler($event, 3)"/>
<br/>
Then in your controller, if key is enter, get element by given id, then focus it.
$scope.keypressHandler = function(event, nextIdx){
if(event.keyCode == 13){
angular.element(
document.querySelector('#f_'+nextIdx))[0].focus();
}
}
As you can see, you can use ng-repeat like that by passing $index instead of hardcoded numbers and creating ids dynamically.
Another solution, use this directive:
angular.module('myApp').directive("nextFocus", nextFocus);
/** Usage:
<input next-focus id="field1">
<input next-focus id="field2">
<input id="field3">
Upon pressing ENTER key the directive will switch focus to
the next field id e.g field2
The last field should not have next-focus directive to avoid
focusing on non-existing element.
Works for Web, iOS (Go button) & Android (Next button) browsers,
**/
function nextFocus() {
var directive = {
restrict: 'A',
link: function(scope, elem, attrs) {
elem.bind('keydown', function(e) {
var partsId = attrs.id.match(/field(\d{1})/);
var currentId = parseInt(partsId[1]);
var code = e.keyCode || e.which;
if (code === 13) {
e.preventDefault();
document.querySelector('#field' + (currentId + 1)).focus();
}
});
}
};
return directive;
}
Related: angularjs move focus to next control on enter
Create a custom directive:
.directive('nextOnEnter', function () {
return {
restrict: 'A',
link: function ($scope, selem, attrs) {
selem.bind('keydown', function (e) {
var code = e.keyCode || e.which;
if (code === 13) {
e.preventDefault();
var pageElems = document.querySelectorAll('input, select, textarea'),
elem = e.srcElement
focusNext = false,
len = pageElems.length;
for (var i = 0; i < len; i++) {
var pe = pageElems[i];
if (focusNext) {
if (pe.style.display !== 'none') {
pe.focus();
break;
}
} else if (pe === e.srcElement) {
focusNext = true;
}
}
}
});
}
}
})
Source: https://stackoverflow.com/a/36960376/717267
At the company I work at, we took over a project made with symfony2 and angular.js.
It is a platform containing courses. These courses are actually "books", that are made interactive with videos and places to discuss course content.
A course consists of a number of chapters, each with a set of pages.
The person who developed this in angular, used div with contenteditable attributes to enter text. (see screenshot here : http://imgur.com/kqpelaG ) The divs also have a sk-placeholder attribute.
I know the basics to angular and I presumed the sk-placeholder attribute was a directive, replacing the content of the div with some text. Appearently, the contenteditable is the directive using the attribute sk-placeholder's content, to fill the element. (in this case a div).
When in an editable element, if you press enter, the content is saved. If however, you don't fill in anything, the placeholder text is not removed and it is presumed the content of the specific section of the page you're working on. (I hope I am clear enough, if there are any questions please do ask). It should clear the content of all divs with attribute "contenteditable" by default when saving. And that's the part I can't seem to figure out.
SEK.app.directive('contenteditable', function($location, sekApi, $q){
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, element, attrs, ctrl) {
var richText = attrs.richText || false,
focused = false;
function renderMath () {
if(richText) {
var math = element[0];
MathJax.Hub.Queue(["Typeset",MathJax.Hub,math]);
}
}
function renderElement() {
if(!ctrl.$viewValue && attrs.skPlaceholder) {
element.addClass("sk-placeholding");
element.html(attrs.skPlaceholder);
} else {
element.removeClass("sk-placeholding");
element.html(ctrl.$viewValue);
renderMath();
}
}
ctrl.$render = function() {
renderElement();
};
element[0].onpaste = function(e) {
var pastedText = undefined;
if (window.clipboardData && window.clipboardData.getData) { // IE
pastedText = window.clipboardData.getData('Text');
} else if (e.clipboardData && e.clipboardData.getData) {
pastedText = e.clipboardData.getData('text/plain');
}
SEK.utilities.insertTextAtCursor(pastedText);
// Prevent the default handler from running.
return false;
};
element.bind('focus', function () {
element.html(ctrl.$viewValue || "");
element.removeClass("sk-placeholding");
focused = true;
});
element.bind('blur', function(event) {
var newViewValue = false;
if(element.html().length > 0){
var htmlContent = element.html();
htmlContent = htmlContent.replace(/<div><br><\/div>/g, "<br>");
htmlContent = htmlContent.replace(/<div><br \/><\/div>/g, "<br>");
htmlContent = htmlContent.replace(/<div>/g, "<br>");
htmlContent = htmlContent.replace(/<\/div>/g, "");
newViewValue = htmlContent;
}
if(element.html().length == 0 && attrs.skPlaceholder) {
newViewValue = "";
};
if(typeof newViewValue === "string") {
scope.$apply(function() {
ctrl.$setViewValue(newViewValue);
});
}
renderElement();
focused = false;
});
element.bind('keydown', function(event) {
var esc = event.which == 27,
enter = event.which == 13,
el = event.target;
if(!richText && esc) {
element.html(ctrl.$viewValue);
el.blur();
event.preventDefault();
}
if (esc || (!richText && enter)) {
scope.ngModel = element.html();
el.blur();
event.preventDefault();
}
});
}
}
});
Any questions are more than welcome. Please do note that I'm a novice when it comes to Angular.js
On save check:
attrs.skPlaceholder === element.html() || attrs.skPlaceholder === element.text()
if this expression returns true, the element seems to be empty.
Or you may check:
element.hasClass("sk-placeholding")