Jquery plugin with prototype - javascript

I was trying on the basics of Jquery plugin and the prototype concept, but ended up in an unusual behavior.
HTML :
<div>
<span>
<textarea>Text Area with 500 characters. Adding Some text.</textarea>
<span class="cl"></span>
</span>
<span>
<textarea>Text Area with 100 characters</textarea>
<span class="cl"></span>
</span>
</div>
JQuery :
(function ($) {
var tisCharsLeftCntxt = null;
function fnCharsLeft(ele, genStngs) {
this.jqe = $(ele);
this.maxChars = genStngs.maxChars;
tisCharsLeftCntxt = this;
this.fnInit();
}
fnCharsLeft.prototype = {
fnInit: function () {
tisCharsLeftCntxt.fnUpdateRemainingChars();
tisCharsLeftCntxt.jqe.keyup(function (event) {
key = event.keyCode ? event.keyCode : event.which;
if ((37 != key) && (38 != key) && (39 != key) && (40 != key)) {
tisCharsLeftCntxt.fnUpdateRemainingChars();
}
});
},
fnUpdateRemainingChars: function () {
var charsLft = tisCharsLeftCntxt.maxChars - tisCharsLeftCntxt.jqe.val().length,
jqeDestToUpdt = tisCharsLeftCntxt.jqe.siblings('.cl');
charsLft = (charsLft < 0) ? 0 : charsLft;
if (charsLft) {
jqeDestToUpdt.text(charsLft + ' more of ' + tisCharsLeftCntxt.maxChars + ' characters');
} else {
tisCharsLeftCntxt.jqe.val(tisCharsLeftCntxt.jqe.val()
.substring(0, tisCharsLeftCntxt.maxChars));
tisCharsLeftCntxt.jqe.scrollTop(tisCharsLeftCntxt.jqe[0].scrollHeight);
jqeDestToUpdt.text("Maximum limit of " + tisCharsLeftCntxt.maxChars + " characters reached");
return false;
}
}
};
$.fn.fnCharsLeftPlgn = function (genStngs) {
return $(this).data("charsleft", new fnCharsLeft(this, genStngs));
};
})(window.jQuery);
$('div span:nth-child(1) textarea').fnCharsLeftPlgn({maxChars: 500});
$('div span:nth-child(2) textarea').fnCharsLeftPlgn({maxChars: 100});
Fiddle :
http://jsfiddle.net/5UQ4D/ & http://jsfiddle.net/5UQ4D/1/
Requirement is, the plugin should show the number of characters that can be added in a text-area. If there is only one text-area in a page this is working good. But if there are more than one, only the text-area which is last associated with the plugin is working properly.
With respect to code here, In both the text-area number of characters left is updated correctly during initialization (only for the first time). But later when the text area content is changed, only the second with 100 chars (or the most recent text-area associated with the plugin) is working properly.
Seems like, I'm failing to restrict the plugin context independently to a text-area. Please Advice,..

Problem 1:
As mentioned in the comments, you're creating a variable named tisCharsLeftCntxt outside of the other contexts, then assigning this to it in your constructor. Every time you run your plugin you stomp on tisCharsLeftCntxt with a new this.
There is no reason to use a reference to this in the wholesale fashion in which you have. There is only one place in your code where the scope changes such that this is no longer your instance. That place is inside of the keyup event handling function. You should localize your aliasing of this to just the method which contains that event handler.
Problem 2:
I believe another part of your problem (this would be seen if you ran the plugin against a selector which matched more than one element) is inside of the plugin function (the one which lives off of $.fn).
$.fn.fnCharsLeftPlgn = function (genStngs) {
return $(this).data("charsleft", new fnCharsLeft(this, genStngs));
};
It should be:
$.fn.fnCharsLeftPlgn = function (genStngs) {
return this.each(function () {
$(this).data("charsleft", new fnCharsLeft(this, genStngs));
});
};
When directly inside of a method which has been added to the jQuery prototype ($.fn), this refers to the entirety of the current collection, not an element. A plugin should each itself in order to run element specific logic against its individual members.
Without using .each() you are calling .data() against an entire collection, setting all of their charsleft data properties to the one instance of fnCharsLeft. By using .each() you create a new instance of fnCharsLeft for each of the elements in the collection.
Since the .each() then returns the collection, and a plugin should be chainable, you simply return it.
A rule of thumb is that if you're passing this into the jQuery factory ($()) directly inside of a plugin, function then you're doing something wrong since it is already the collection. As a second rule of thumb, almost all plugin definitions except those which are intended to return info about an element (such as .val(), .html(), or .text() when not given a param) should start with return this.each(function() {...
Solutions:
Bringing those changes together results in this fiddle: http://jsfiddle.net/5UQ4D/4/
And this code:
(function ($) {
var fnCharsLeft = function (ele, genStngs) {
this.jqe = $(ele);
this.maxChars = genStngs.maxChars;
this.fnInit();
};
fnCharsLeft.prototype = {
fnInit: function () {
var instance = this;
this.fnUpdateRemainingChars();
this.jqe.on('keyup', function (e) {
key = e.keyCode ? e.keyCode : e.which;
if (37 != key && 38 != key && 39 != key && 40 != key) {
instance.fnUpdateRemainingChars();
}
});
},
fnUpdateRemainingChars: function () {
var charsLft = this.maxChars - this.jqe.val().length,
jqeDestToUpdt = this.jqe.siblings('.cl');
charsLft = charsLft < 0 ? 0 : charsLft;
if (charsLft) {
jqeDestToUpdt.text(charsLft + ' more of ' + this.maxChars + ' characters');
} else {
this.jqe
.val(this.jqe.val().substring(0, this.maxChars))
.scrollTop(this.jqe[0].scrollHeight);
jqeDestToUpdt.text("Maximum limit of " + this.maxChars + " characters reached");
return false;
}
}
};
$.fn.fnCharsLeftPlgn = function (genStngs) {
return this.each(function () {
$(this).data('charsleft', new fnCharsLeft(this, genStngs));
});
};
}(window.jQuery));

Related

Can't update variable value inside function angularjs

Hello developers please help, updating value dynamically working perfect inside angularjs controller which is passing in html tag. But when I use scope variable inside function (within controller).
Working perfect when updating value outside function:
var app = angular.module('previewApp', []);
app.controller('previewCtrl', function ($scope) {
var $ = jQuery;
var targetValue = 0;
$scope.w = targetValue;
});
passing to html span tag:
<span style="width:{{w}}px;height:{{h}}px; background-color: white" class="item-inner">
But I am getting value using jQuery whenever input event trigger value is not updating inside function dynamically, in console value is updating perfectly (console.log('Width ' + targetValue);) but not in html tag:
function onChangeWidth(event) {
if (event.which != 8 && event.which != 0 && (event.which < 48 || event.which > 57)) {
isProcess = false;
}
const targetValue = $(this).closest('.gfield').find('input').val();
// $scope.width = ;
$scope.w = function () {
return targetValue;
};
$scope.docPPIWidth = ppi * +targetValue;
console.log('Width ' + targetValue);
$scope.w = targetValue; //updating w didn't work inside function
}
function onChangeHeight(event) {
const targetValueH = $(this).closest('.gfield').find('input').val();
if (event.which != 8 && event.which != 0 && (event.which < 48 || event.which > 57)) {
var isProcess = false;
}
$scope.docPPIHeight = ppi * targetValueH;
console.log('Height ' + targetValueH);
}
$('.width_check input').on('change click', onChangeWidth);
$('.height_check input').on('change click', onChangeHeight);
My first recommendation for you is to start thinking about how to prevent mixing JQuery and AngularJS, because eventually you will end up with "bug apocalypse".
I think what happens in your case is that Angular is not aware about JQuery triggered an event, so the digest loop is not triggered automatically.
Again, this solution can help you in that particular situation, but it is really not a good practice:
$scope.w = targetValue;
$scope.$digest();
After you change scope variable, let Angular know that it has to trigger digest loop to refresh view data by adding $scope.$digest(). This function will check only this component and its children scope data. In case if you need to check entire app, use $rootScope.$digest() (very poor decision from the performance standpoint).

AngularJS Show typeahead on button click

I am using the typeahead directive in AngularJS and it works fine. However, I would like to have a button outside of the input that when clicked would show the typeahead dropdown. Here is a snippet of what I am after...
<li class="input">
<input focus-me="click" ng-model="something"
typeahead="state for state in Suggestions | filter:$viewValue:stateComparator" typeahead-focus typeahead-focus-first="false" typeahead-on-select="updateTagInput(newTagName)">
Open
</li>
Ok, I am having an absolutely terrible time trying to create a JSFiddle or even a Plunkr for this, so I will just give you the code for this directive.
This directive originally comes from..
This epic Bootstrap library!
..and I stole it and played with it. If you would like to use it, you will need the "Bootstrap" (its really a subset of angular directives) library that I linked to. You can make your own subset of this library, but I am not entirely sure of all of the dependencies my directive has as I am using the entire library in my project. Basically, you need any directive that starts with "typeahead".
As you can see, I have named the directive wwTypeahead (that "ww" is for WebWanderer!). It is a very easy to use directive and it works just like the original.
<input
class="form-control"
type="text"
spellcheck="false"
ng-model="selection"
ng-trim="false"
placeholder="Search Here"
ww-typeahead="key as key.label for key in list"
typeahead-on-select="selectionMade($item, $model, $label)"
typeahead-min-length="0"
/>
The really important part to note is the attribute typeahead-min-length="0" which has really been the heart of many discussions online. I managed to make that work.
This directive is meant to take the place of the typeahead directive in the library I linked to. Your typeahead list will be shown on focus of your input box. No, the list does not show on the click of a button, but hopefully getting there will be baby-steps from here. If you need help implementing that, I will be happy to help.
/*
NOTE:
The following directive is a modification of the
Angular typeahead directive. The normal directives,
unfortunately, do not allow matching on 0 length values
and the user may want a returned list of all values during
the lack of input.
This directives was taken from ...
http://angular-ui.github.io/bootstrap/
..and modified.
*/
angular.module('ui.directives', []).directive('wwTypeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser',
function($compile, $parse, $q, $timeout, $document, $position, typeaheadParser)
{
var HOT_KEYS = [9, 13, 27, 38, 40];
return {
require:'ngModel',
link:function(originalScope, element, attrs, modelCtrl)
{
//SUPPORTED ATTRIBUTES (OPTIONS)
//minimal no of characters that needs to be entered before typeahead kicks-in
//var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
var testEval = originalScope.$eval(attrs.typeaheadMinLength);
var minSearch = !isNaN(parseFloat(testEval)) && isFinite(testEval) || 1;
//minimal wait time after last character typed before typehead kicks-in
var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
//should it restrict model values to the ones selected from the popup only?
var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
//binding to a variable that indicates if matches are being retrieved asynchronously
var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
//a callback executed when a match is selected
var onSelectCallback = $parse(attrs.typeaheadOnSelect);
var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
//INTERNAL VARIABLES
//model setter executed upon match selection
var $setModelValue = $parse(attrs.ngModel).assign;
//expressions used by typeahead
var parserResult = typeaheadParser.parse(attrs.cmcTypeahead);
//pop-up element used to display matches
var popUpEl = angular.element('<typeahead-popup></typeahead-popup>');
popUpEl.attr({
matches: 'matches',
active: 'activeIdx',
select: 'select(activeIdx)',
query: 'query',
position: 'position'
});
//custom item template
if(angular.isDefined(attrs.typeaheadTemplateUrl))
{
popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
}
//create a child scope for the typeahead directive so we are not polluting original scope
//with typeahead-specific data (matches, query etc.)
var scope = originalScope.$new();
originalScope.$on('$destroy', function()
{
scope.$destroy();
});
var resetMatches = function()
{
scope.matches = [];
scope.activeIdx = -1;
};
var getMatchesAsync = function(inputValue)
{
var matchParsePrefix = originalScope.$eval(attrs.typeaheadParsePrefix);
var locals = {
$viewValue: inputValue.indexOf(matchParsePrefix) === 0 ? inputValue.substring(matchParsePrefix.length, (inputValue.length + 1)) : inputValue
};
isLoadingSetter(originalScope, true);
$q.when(parserResult.source(scope, locals)).then(function(matches)
{
//it might happen that several async queries were in progress if a user were typing fast
//but we are interested only in responses that correspond to the current view value
//if(matches && inputValue === modelCtrl.$viewValue)
/*
Ehh.. that didn't seem to work when I "cleared" the input box
*/
if(matches)
{
if(matches.length > 0)
{
scope.activeIdx = 0;
scope.matches.length = 0;
//transform labels
for(var i = 0; i < matches.length; i++)
{
locals[parserResult.itemName] = matches[i];
scope.matches.push({
label: parserResult.viewMapper(scope, locals),
model: matches[i]
});
}
scope.query = inputValue;
//position pop-up with matches - we need to re-calculate its position each time we are opening a window
//with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
//due to other elements being rendered
scope.position = $position.position(element);
scope.position.top = scope.position.top + element.prop('offsetHeight');
}
else if(minSearch === 0)
{
resetMatches();//temp
}
else
{
resetMatches();
}
isLoadingSetter(originalScope, false);
}
}, function()
{
resetMatches();
isLoadingSetter(originalScope, false);
});
};
resetMatches();
/*
Can't figure out how to make this work...*/
if(attrs.hasOwnProperty('typeaheadBindMatchReloader'))
{
$parse(attrs.typeaheadBindMatchReloader).assign(scope, function()
{
getMatchesAsync(element[0].value);
});
}
//we need to propagate user's query so we can higlight matches
scope.query = undefined;
//Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
var timeoutPromise;
//plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
//$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
modelCtrl.$parsers.unshift(function(inputValue)
{
resetMatches();
if((inputValue && inputValue.length >= minSearch)
|| minSearch === 0)
{
if(waitTime > 0)
{
if(timeoutPromise)
{
$timeout.cancel(timeoutPromise);//cancel previous timeout
}
timeoutPromise = $timeout(function()
{
getMatchesAsync(inputValue);
}, waitTime);
}
else
{
getMatchesAsync(inputValue);
}
}
if(isEditable)
{
return inputValue;
}
else
{
modelCtrl.$setValidity('editable', false);
return undefined;
}
});
modelCtrl.$formatters.push(function(modelValue)
{
var candidateViewValue, emptyViewValue;
var locals = {};
if(inputFormatter)
{
locals['$model'] = modelValue;
return inputFormatter(originalScope, locals);
}
else
{
//it might happen that we don't have enough info to properly render input value
//we need to check for this situation and simply return model value if we can't apply custom formatting
locals[parserResult.itemName] = modelValue;
candidateViewValue = parserResult.viewMapper(originalScope, locals);
locals[parserResult.itemName] = undefined;
emptyViewValue = parserResult.viewMapper(originalScope, locals);
return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue;
}
});
scope.select = function(activeIdx)
{
//called from within the $digest() cycle
var locals = {};
var model, item;
locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
model = parserResult.modelMapper(originalScope, locals);
$setModelValue(originalScope, model);
modelCtrl.$setValidity('editable', true);
onSelectCallback(originalScope, {
$item: item,
$model: model,
$label: parserResult.viewMapper(originalScope, locals)
});
resetMatches();
//return focus to the input element if a mach was selected via a mouse click event
element[0].focus();
};
//bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
element.bind('keydown', function(evt)
{
//typeahead is open and an "interesting" key was pressed
if(scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1)
return;
evt.preventDefault();
if(evt.which === 40)
{
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
scope.$digest();
}
else if(evt.which === 38)
{
scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1;
scope.$digest();
}
else if(evt.which === 13 || evt.which === 9)
{
scope.$apply(function()
{
scope.select(scope.activeIdx);
});
}
else if(evt.which === 27)
{
evt.stopPropagation();
resetMatches();
scope.$digest();
}
});
// Keep reference to click handler to unbind it.
var dismissClickHandler = function(evt)
{
if(element[0] !== evt.target)
{
resetMatches();
scope.$digest();
}
else
{
getMatchesAsync(element[0].value);
}
};
$document.bind('click', dismissClickHandler);
originalScope.$on('$destroy', function()
{
$document.unbind('click', dismissClickHandler);
});
element.after($compile(popUpEl)(scope));
}
};
}]);
Call To Action:
Somebody PLEASE make a working example of this typeahead directive! I would forever be in debt to you! (well, not really but it would make me very happy)
DISCLAIMER:
I understand that this answer is in no way orthodox. I did not provide the askee (askee?) with a direct answer to the question, yet I did provide the tools that I believe are needed to get to his/her answer. I understand that I should spend the time to make a working example, but I am a very busy man and simply wished to share my work with the community, as I have seen this question asked too many times while I sit back and hold the answer. Please let me know if you have any issues, questions, or complications. I am happy to help.
Thanks!
<input
class="form-control"
spellcheck="false"
focus-me="click" ng-model="something"
ng-trim="false"
placeholder="Search Here"
uib-typeahead="key as key.label for key in list | filter:{label:$viewValue}"
typeahead-on-select="openTypeAhead($item, $model, $label)"
typeahead-min-length="0"
/>
in controller angularjs
$scope.openTypeAhead = ($item, $model, $label) =>{ console.log('arg =>',$item, $model, $label);}

1 line of code (console.log), two emplacement in the code = two results differents

The problem I encountered is with the jQuery Selector that returns me two different answers for the same line of code :
console.log($('.page_container')[3]);
In my navigation fonction using arrows of the keybord, this line would return me the content of this div including the parent '.page_container'.
This is exactly what I don't want since I need it as an object.
function checkKey(e) {
var actualScroll = $('.main').scrollTop();
var scrollTo = $('.page_container').height();
console.log($('.page_container')[3]);
if (e.keyCode == '38' && working == false) {
working = true;
// up arrow
$(".main").animate({
scrollTop: actualScroll - scrollTo,
}, 1000, 'easeInOutExpo', function () {
working = false;
});
} else if (e.keyCode == '40' && working == false) {
working = true;
// down arrow
$(".main").animate({
scrollTop: actualScroll + scrollTo,
}, 1000, 'easeInOutExpo', function () {
working = false;
});
}
}
$(document).ready(function () {
//ArrowsNavigations
document.onkeydown = testArrows;
});
But anywhere else in the code, it would return me the normal thing, the object version with all its original properties.
Edit :
Here is the fiddle but I don't get why I don't have the same results, anyways on this fiddle whenever I select my entire array of '.page_container' I get the object sample but if I select a particular index of this array I'll get the html of this occurence.
On my local version, the selector returns me the html content only when it's called in the checkKey function.
I think you do get an object. It's just the browser's way of displaying things.
Try this:
console.log($('.page_container')[3].id);

using a function on textbox focus?

I want to add a autocomplete function to a site and found this guide which uses some js code which works really nice for one textbox: http://www.sks.com.np/article/9/ajax-autocomplete-using-php-mysql.html
However when trying to add multiple autocompletes only the last tetbox will work since it is the last one set.
Here is the function that sets the variables for the js script
function setAutoComplete(field_id, results_id, get_url)
{
// initialize vars
acSearchId = "#" + field_id;
acResultsId = "#" + results_id;
acURL = get_url;
// create the results div
$("#auto").append('<div id="' + results_id + '"></div>');
// register mostly used vars
acSearchField = $(acSearchId);
acResultsDiv = $(acResultsId);
// reposition div
repositionResultsDiv();
// on blur listener
acSearchField.blur(function(){ setTimeout("clearAutoComplete()", 200) });
// on key up listener
acSearchField.keyup(function (e) {
// get keyCode (window.event is for IE)
var keyCode = e.keyCode || window.event.keyCode;
var lastVal = acSearchField.val();
// check an treat up and down arrows
if(updownArrow(keyCode)){
return;
}
// check for an ENTER or ESC
if(keyCode == 13 || keyCode == 27){
clearAutoComplete();
return;
}
// if is text, call with delay
setTimeout(function () {autoComplete(lastVal)}, acDelay);
});
}
For one textbox I can call the function like this
$(function(){
setAutoComplete("field", "fieldSuggest", "/functions/autocomplete.php?part=");
});
However when using multiple textboxes I am unsure how I should go about doing this, here is something I did try but it did not work
$('#f1').focus(function (e) {
setAutoComplete("f1", "fSuggest1", "/functions/autocomplete.php?q1=");
}
$('#f2').focus(function (e) {
setAutoComplete("f2", "fSuggest2", "/functions/autocomplete.php?q2=");
}
Thanks for your help.
You should be using classes to make your function work in more than one element on the same page. Just drop the fixed ID's and do a forEach to target every single element with that class.

Check if browser supports load method on iFrame

I'm trying to code a simple form with file upload which targets the iFrame. I've got it pretty much covered, but Internet Explorer does not support load method on dynamically generated iFrames so I need to do an alternative approach for it. My question is - what's the best and most accurate (plus light) way of identifying the browser type using Javascript?
I know that Modernizr can check for specific methods / properties, but not quite sure if it could help in this specific scenario. It has Modernizr.hasEvent(), but I can't use it to check the dynamically created elements.
The easiest way to check this, to my mind would be:
if ('onload' in iFrameVar)
{
console.log('your code here');
}
Where iFrameVar is a reference to an iframe, of course:
function elemSupportsEvent(elem,e)
{
var f = document.createElement(elem);
if (e in f)
{
console.log(elem + ' supports the '+ e + ' event');
return true;
}
console.log(elem + ' doesn\'t support the '+ e + ' event');
return false;
}
elemSupportsEvent('iframe','onload');//logs "iframe supports the onload event" in chrome and IE8
Just a quick fiddle by means of example of how you can use a function to check event support on various elements.
In response to your comment: if you want to check for dynamic content -as in ajax replies- you could simply use the readystatechange event:
xhr.onreadystatechange = function()
{
if (this.readyState === 4 && this.status === 200)
{
var parent = document.getElementById('targetContainerId');//suppose you're injecting the html here:
parent.innerHTML += this.responseText;//append HTML
onloadCallback.apply(parent,[this]);//call handler, with parent element as context, pass xhr object as argument
}
};
function onloadCallback(xhr)
{
//this === parent, so this.id === 'targetContainerId'
//xhr.responseText === appended HTML, xhr.status === 200 etc...
alert('additional content was loaded in the '+ this.tagName.toLowerCase+'#'+this.id);
//alerts "additional content was loaded in the div/iframe/td/whatever#targetContainerID"
}
if you want to check support for particular event, you can try something like this :
var isEventSupported = (function(){
var TAGNAMES = {
'select':'input','change':'input',
'submit':'form','reset':'form',
'error':'img','load':'img','abort':'img'
}
function isEventSupported(eventName) {
var el = document.createElement(TAGNAMES[eventName] || 'div');
eventName = 'on' + eventName;
var isSupported = (eventName in el);
if (!isSupported) {
el.setAttribute(eventName, 'return;');
isSupported = typeof el[eventName] == 'function';
}
el = null;
return isSupported;
}
return isEventSupported;
})();
here is a live demo for the above :
http://kangax.github.com/iseventsupported/
Use navigator.userAgent. It contains browser user agent
if (navigator.userAgent.search("MSIE 6") == -1){
// We've got IE.
}

Categories