I've created an application which enables the user to double click on an item to edit. I'd like to allow the same functionality on mobile devices, meaning the user would double tap to edit the item.
What is the simplest way to implement this? I'd rather not use any additional library (I have heard of Hammer and AngularTouch but haven't used neither before) nor jQuery (in my app I completely forgone jQuery).
If there the only way to implement this is using a library, what would be lightest and easiest?
Many thanks
EDIT: Adding code
This is my controller for editing the item:
// Double click to edit products
$scope.editItem = function (item) {
item.editing = true;
};
$scope.doneEditing = function (item) {
item.editing = false;
$http.put('/api/users?id' + $scope.User.id, $scope.User);
};
$scope.cancelEditing = function (item) {
item.editing = false;
};
$scope.deleteItem = function (item) {
delete $scope.User.todos[item.id];
$http.put('/api/users?id' + $scope.User.id, $scope.User);
};
And this is the my template (Jade)
p(ng-dblclick="editItem(todo)", ng-hide="todo.editing").todo-title
span {{todo.content}}
form(ng-submit="doneEditing(todo)" ng-show="todo.editing", class="inline-editing-2")
input(type="text", class="form-control", ng-model="todo.content")
div.btn-block
button(class="btn btn-success mr-1", ng-show="todo.editing", ng-click="cancelEditing(todo)")
span(ng-click="doneEditing(todo)").fa.fa-check-circle
button(class="btn btn-warning mr-1", ng-show="todo.editing", ng-click="cancelEditing(todo)")
span(ng-click="cancelEditing(todo)").fa.fa-times-circle
So as you can see I use ng-doubleclick to fire my function. I'd need something like ng-double-tab to fire up the double tap. I've been reading more about Hammer and will use Angular Hammer for double tap but I'm not sure how it works...
You can use ios-dblclick, a directive I wrote to handle double click event on mobile browser (write it for iOS, but works on other browsers). It has no dependency and works like ng-dblclick.
It's available here on Github.
Here's an example
<div ios-dblclick="removePhoto()"></div>
Here is the code of this directive
app.directive('iosDblclick',
function () {
const DblClickInterval = 300; //milliseconds
var firstClickTime;
var waitingSecondClick = false;
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.bind('click', function (e) {
if (!waitingSecondClick) {
firstClickTime = (new Date()).getTime();
waitingSecondClick = true;
setTimeout(function () {
waitingSecondClick = false;
}, DblClickInterval);
}
else {
waitingSecondClick = false;
var time = (new Date()).getTime();
if (time - firstClickTime < DblClickInterval) {
scope.$apply(attrs.iosDblclick);
}
}
});
}
};
});
You could always implement your own double tap directive. Start by looking at touchstart and touchend . Bind to these events, and check for multiple taps within some designated period of time.
As far as libraries, we've used this to handle doubletaps for mobile devices in angular
https://github.com/technoweenie/jquery.doubletap
The solution above is not working on my IOS - But I found an other Solution, which is working fine on my IPhone:
Just sharing for you:
http://jsfiddle.net/9Ymvt/3397/
fessmodule.directive('onDoubleClick', function($timeout) {
return {
restrict: 'A',
link: function($scope, $elm, $attrs) {
var clicks=0;
$elm.bind('click', function(evt) {
console.log('clicked');
clicks++;
if (clicks == 1) {
$timeout(function(){
if(clicks == 1) {
//single_click_callback.call(self, event);
} else {
$scope.$apply(function() {
console.log('clicked');
$scope.$eval($attrs.onDoubleClick)
});
}
clicks = 0;
}, 300);}
});
}
};
});
Related
I have AngularJS directive like this:
(function () {
'use strict';
// better click that ingore drag
angular
.module('module')
.directive('exClick', exClick);
exClick.$inject = [];
function exClick() {
return {
restrict: 'A',
scope: {
exClick: '&'
},
link: function ($scope, $element) {
var isDragging = false;
function mousemove() {
isDragging = true;
$(window).off('mousemove', mousemove);
}
var timer;
$element.mousedown(function() {
isDragging = false;
// there is wierd issue where move is triggerd just
// after mousedown even without moving the cursor
timer = setTimeout(function() {
$(window).mousemove(mousemove);
}, 100);
}).mouseup(function(e) {
var wasDragging = isDragging;
isDragging = false;
clearTimeout(timer);
$(window).off('mousemove', mousemove);
if (!wasDragging) {
$scope.$apply($scope.exClick);
}
});
$scope.$on('$destroy', function() {
$(window).off('mousemove', mousemove);
$element.off('mousedown mouseup');
});
}
}
}
})();
and I want to use like normal ng event I have ng-click on table row and on row controls I have ng-click="$event.stopPropagation()". I've replaced row with ex-click and I want to use ex-click="$event.stopPropagation()". I can use ng-mouseup to prevent the event from happening, but I want to know how to make my custom event to behave the same as native ng event.
I've tried:
$scope.exClick({$event: e});
and
$scope.$event = e;
$scope.$apply();
$scope.exClick();
Found in the source code ngClick is kind of normal AngularJS directive (it's added differently but it have the same compile and link, so it should work the same).
And for reference here is the copy of the code:
forEach(
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
function(eventName) {
var directiveName = directiveNormalize('ng-' + eventName);
ngEventDirectives[directiveName] = ['$parse', '$rootScope', '$exceptionHandler', function($parse, $rootScope, $exceptionHandler) {
return createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsyncEvents[eventName]);
}];
}
);
function createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsync) {
return {
restrict: 'A',
compile: function($element, attr) {
// NOTE:
// We expose the powerful `$event` object on the scope that provides access to the Window,
// etc. This is OK, because expressions are not sandboxed any more (and the expression
// sandbox was never meant to be a security feature anyway).
var fn = $parse(attr[directiveName]);
return function ngEventHandler(scope, element) {
element.on(eventName, function(event) {
var callback = function() {
fn(scope, {$event: event});
};
if (!$rootScope.$$phase) {
scope.$apply(callback);
} else if (forceAsync) {
scope.$evalAsync(callback);
} else {
try {
callback();
} catch (error) {
$exceptionHandler(error);
}
}
});
};
}
};
}
is there any way, how can I globally (in service) disable and enable all ng-click and ng-submit events?
For example when user is offline I want to disable all actions till he gets connection back..
I tried to bind all elements with an onClick event which will call stopImmediatePropagation but it didn't work..
$('*[ng-click]').click(function( event ) {
event.stopImmediatePropagation();
});
Also this question is a little bit different from this one:
Disable ng-click on certain conditions of application for all types of element
I'd like to disable/enable all events in APP globally from service, I'm not able to modify all ng-* calls on all elements in the APP..
Try including a return false too:
$('*[ng-click]').click(function( event ) {
event.stopImmediatePropagation();
return false;
});
Snippet
The below snippet demonstrates that multiple event handlers attached to a single <a> works too.
$(function () {
$("a").click(function () {
alert("Hello!");
return false;
});
$("a").click(function () {
alert("Bye!");
return false;
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
Click Me
So finally I end up with temporarily disabling all events on the page using jquery..
I got inspired from this plugin http://ignitersworld.com/lab/eventPause.html which for some reason did not work (without any error)
So I took main parts and put it to this class which is working now using jquery v2.1.1:
var EventManager = function() {
var self = this;
var nullFun=function(){};
var getIndex = function(array,value){
for(var i=0; i< array.length; i++){
if(array[i]==value){
return i;
}
}
return -1;
};
this.pauseEvent = function(elm,eventAry){
var events = $._data(elm, "events");
if (events) {
$.each(events, function(type, definition) {
if((getIndex(eventAry,type)!=-1)||(eventAry=='')){
$.each(definition, function(index, event) {
if (event.handler.toString() != nullFun.toString()){
if(!$._iwEventPause) $._iwEventPause = {};
$._iwEventPause["iw-event" + event.guid] = event.handler;
event.handler = nullFun;
}
})
}
})
}
};
this.activeEvent = function(elm,eventAry){
var events = $._data(elm, "events");
if (events) {
$.each(events, function(type, definition) {
if((getIndex(eventAry,type)!=-1)||(eventAry=='')){
$.each(definition, function(index, event) {
if (event.handler.toString() == nullFun.toString()){
event.handler = $._iwEventPause["iw-event" + event.guid];
}
})
}
})
}
};
this.disableAll = function(el) {
el = el || $('*');
el.each(function() {
self.pauseEvent($(this)[0], '');
});
self.pauseEvent($(window)[0], '');
};
this.enableAll = function(el) {
el = el || $('*');
el.each(function() {
self.activeEvent($(this)[0], '');
});
self.activeEvent($(window)[0], '');
};
return this;
};
var eManager = new EventManager();
eManager.disableAll();
eManager.enableAll();
This will go through window object and all elements on the page, move their event handlers away to _iwEventPause object and replace handlers with dummy function.. When enabling, it will move handlers back so they get normally called..
This solution does not handle event handlers added after disabling..
I recently have been upgrading the Phonegap to the latest version and now it forces me to follow the Chrome's Content Security Policy which in a way is good. But now I am forced to remove the all the onclick handlers in the HTML code and add them in the jquery handler some$(document).ready(function(evt){
$('#addRecordBtn').on('click', function(){
alert("Adding Record");
AddValueToDB();
});
$('#refreshBtn').on('click', function(){
alert("Refresh Records");
ListDBValues();
});
});
But as per what my app is scaled upto I feel that there will be too many of these handlers. Is there an example which shows maintenance of such handlers and a proper way or proper place of defining such handlers.
Here's an idea. You could make an object that stores all of the functions that also knows how to give up the function
var handlers = {
getHandler: function (str) {
return this[str];
},
'#addRecordBtn': function () {
alert("Adding Record");
AddValueToDB();
},
'#refreshBtn': function () {
alert("Refresh Records");
ListDBValues();
}
};
Then apply all of your handlers using this form.
$('#addRecordBtn').on('click', handlers.getHandler('#addRecordBtn'));
$('#refreshBtn').on('click', handlers.getHandler('#refreshBtn'));
Optimization Time if you want to get really fancy and you assign a unique ID to every button as convention
var handlers = {
defer: function () {
return function (){
handlers[$(this).attr('id')](arguments);
};
},
registerHandlers: function () {
for (var key in this) {
if (this.hasOwnProperty(key) && typeof(key) === "string") {
$('#' + key).on('click', this.defer());
}
}
},
'addRecordBtn': function () {
alert("Adding Record");
AddValueToDB();
},
'refreshBtn': function () {
alert("Refresh Records");
ListDBValues();
}
};
call it with
$('#addRecordBtn').on('click', handlers.defer());
$('#refreshBtn').on('click', handlers.defer());
or register everything automatically
handlers.registerHandlers();
Here is a fiddle of my solution
Do you look for something like this?
$('[data-clickhandler]').on('click', function(e) {
var $btn = $(e.currentTarget);
var handler = $btn.data('clickhandler');
alert('Refresh ' + handler);
window[handler] && window[handler](e);
e.preventDefault();
});
Now your elements can specify their clickhandler like so:
<a data-clickhandler="AddValueToDB" href="">...</a>
Or so:
<span data-clickhandler="ListDBValues">...</span>
Important Edit
The problem doesn't occur with an ng-hide we removed that code for a bootstrap collapse, but it still occurs. My next guess is the following piece of code
<div ng-include="getTemplateUrl()"></div>
This is the whole directive:
stuffModule.directive('stuffDirective', function ($compile) {
var oldId = undefined;
return {
restrict: 'E',
scope: {
model: '='
},
link: function (scope, elem, attrs) {
scope.$watch(function (scope) {
if (oldId !== scope.model.key) {
oldId = scope.model.key;
return true;
}
return false;
}, function (newValue, oldValue) {
if (scope.model.someswitch) {
switch (scope.model.someswitch) {
case 'condition1':
scope.getTemplateUrl = function () {
return 'condition1.html';
}
break;
case 'condition2':
case 'condition3':
scope.getTemplateUrl = function () {
return 'condition23.html';
}
break;
default:
break;
}
} else {
scope.getTemplateUrl = function () {
return 'default.html';
}
}
});
},
template: '<div ng-include="getTemplateUrl()"></div>'
};
});
Just a short clarification, it is literally not possible to scroll with the mouse, but you can easily tab through the fields.
PS: It only happens in Internet Explorer 11, that is the version our customer is using. In Firefox I don't have that problem.
We replaced the code
Because there is an important presentation tomorrow and a missing scrollbar is something like a really big issue, we decided to remove the piece of code and replace it with just normal routing.
Thanks to all commentors :)
Original question with ng-hide
I have a simple page, where I hide a part with ng-hide. When ng-hide turns false the part gets shown, but randomly the page is not scrollable until I reload the whole page.
If it helps, the data which turn ng-hide to false come from an AJAX request.
EDIT 1 - not relevent anymore
Here is the code which does the HTTP requests
this.getCall = function (url) {
var dfd = $q.defer();
$rootScope.loading = true;
$rootScope.loadingError = false;
$rootScope.progressActive = true;
$rootScope.loadingClass = "progress-bar-info";
$http.get('http://localhost/something', {
cache: true
}).success(function (data) {
$rootScope.loadingClass = "progress-bar-success";
$rootScope.progressActive = false;
$timeout(function () {
$rootScope.loading = false;
}, 500);
dfd.resolve(data);
}).error(function (data, status, headers) {
$rootScope.loading = false;
$rootScope.loadingError = true;
$rootScope.progressActive = false;
$rootScope.loadingClass = "progress-bar-danger";
console.error(data);
dfd.reject(JSON.stringify(data));
});
return dfd.promise;
};
The properties on $routescope are there to show a simple progress bar for every HTTP request.
Many things are weird in your code. $scope.watch callback will be executed when the first function will return a result that is different than the last time it was executed. You will certainly not obtain the expected behavior with what you have: instead simply watch for model.key
Another problem is the way you redefine getTemplateUrl: you should not redefine a function, but change what it returns, as pinted out by #New Dev in a comment.
Fixed directive:
link: function (scope, elem, attrs) {
// Or simply bind templateUrl in your ng-include
scope.getTemplateUrl = function() {
return scope.templateUrl;
}
scope.$watch('model.key', function (newValue) {
if (scope.model.someswitch) {
switch (scope.model.someswitch) {
case 'condition1':
scope.templateUrl = 'condition1.html';
break;
case 'condition2':
case 'condition3':
scope.templateUrl = 'condition23.html';
break;
default:
break;
}
} else {
scope.templateUrl = 'default.html';
}
});
}
Now your scrolling issue has probably nothing to do with that. If the template is right but the scrolling wrong, you should investigate as to what is causing that specific issue. For us to help, we need a way to reproduce or understand the issue.
It could have to do with your ng-include being empty until your watch triggers. You can try using an ng-if, as this will only include your element in the DOM when the ng-if expression is true.
template: '<div ng-if="!whatever you had in your ng-hide" ng-include="getTemplateUrl()"></div>'
I found the solution and it didn't had anything to do with ng-include. The problem was, we use a bootstrap modal that we open like this:
$('#modal').modal('show');
But it does not hide properly, the result is that the body keeps the class modal-open that causes that the scrolling doesn't work anymore.
Thanks to everybody who helped and invested time.
This is a follow-up question to this question: AngularJS input with focus kills ng-repeat filter of list
Basically my code is using AngularJS to pop-out a div (a drawer) on the right for filtering a list of things. Most times this is used the UI is blocked so clicking on that blocking div closes the drawer. But in some cases we don't block the UI and need to allow the user to click outside of the drawer to cancel the search or select something else on the page.
My initial thought was to attach a window.onclick handler when the drawer opens and if anything is clicked other than the drawer it should close the drawer. That's how I would do it in a pure JavaScript app. But in Angular it is being a bit more difficult.
Here is a jsfiddle with a sample that I based on #Yoshi's jsBin example: http://jsfiddle.net/tpeiffer/kDmn8/
The relevant piece of code from this sample is below. Basically if the user clicks outside of the drawer I invoke $scope.toggleSearch so that $scope.open is set back to false.
The code works, and even though the $scope.open goes from true to false it doesn't modify the DOM. I'm sure this has something to do with the lifecycle of events or perhaps when I modify $scope (since it is from a separate event) that it is a copy and not the original...
The ultimate goal on this will be for it to be a directive for ultimate reusability. If anyone can point me in the right direction to do that I would be grateful.
$scope.toggleSearch = function () {
$scope.open = !$scope.open;
if ($scope.open) {
$scope.$window.onclick = function (event) {
closeSearchWhenClickingElsewhere(event, $scope.toggleSearch);
};
} else {
$scope.$window.onclick = null;
}
};
and
function closeSearchWhenClickingElsewhere(event, callbackOnClose) {
var clickedElement = event.target;
if (!clickedElement) return;
var elementClasses = clickedElement.classList;
var clickedOnSearchDrawer = elementClasses.contains('handle-right')
|| elementClasses.contains('drawer-right')
|| (clickedElement.parentElement !== null
&& clickedElement.parentElement.classList.contains('drawer-right'));
if (!clickedOnSearchDrawer) {
callbackOnClose();
}
}
The drawer is not closing because the click event occurs outside the digest cycle and Angular doesn't know that $scope.open has changed. To fix it you can call $scope.$apply() after $scope.open is set to false, this will trigger the digest cycle.
$scope.toggleSearch = function () {
$scope.open = !$scope.open;
if ($scope.open) {
$scope.$window.onclick = function (event) {
closeSearchWhenClickingElsewhere(event, $scope.toggleSearch);
};
} else {
$scope.open = false;
$scope.$window.onclick = null;
$scope.$apply(); //--> trigger digest cycle and make angular aware.
}
};
Here is your Fiddle.
I was also trying to create a directive for the search drawer, if it helps (it needs some fixes :)). Here is a JSBin.
I suggest to add $event.stopPropagation(); on the view right after on the ng-click. You don't need to use jQuery.
<button ng-click="toggleSearch();$event.stopPropagation();">toggle</button>
Then, you can use this simplified js.
$scope.toggleSearch = function () {
$window.onclick = null;
$scope.menuOpen = !$scope.menuOpen;
if ($scope.model.menuOpen) {
$window.onclick = function (event) {
$scope.menuOpen = false;
$scope.$apply();
};
}
};
The accepted answer will throw an error if you click on the button to close the drawer/popup, and the button is located outside of it, because $apply() will be executed twice.
This is a simplified version, that doesn't need call the whole toggleSearch() function again and prevents the double $apply().
$scope.toggleSearch = function() {
$scope.open = !$scope.open;
if ($scope.open) {
$window.onclick = function(event) {
var clickedElement = event.target;
if (!clickedElement) return;
var clickedOnSearchDrawer = elementClasses.contains('handle-right') || elementClasses.contains('drawer-right') || (clickedElement.parentElement !== null && clickedElement.parentElement.classList.contains('drawer-right'));
if (!clickedOnSearchDrawer) {
$scope.open = !$scope.open;
$window.onclick = null;
$scope.$apply();
}
}
} else {
$window.onclick = null;
}
};
I couldn't find a solution i was 100% happy with, this is what i used:
<div class="options">
<span ng-click="toggleAccountMenu($event)">{{ email }}</span>
<div ng-show="accountMenu" class="accountMenu">
<a ng-click="go('account')">Account</a>
<a ng-click="go('logout')">Log Out</a>
</div>
</div>
the span with ng-click is used to open the menu, the div.accountMenu is toggled open or closed
$scope.accountMenu = false;
$scope.toggleAccountMenu = function(e){
if(e) e.stopPropagation();
$scope.accountMenu = !$scope.accountMenu;
if ($scope.accountMenu) {
$window.onclick = function(e) {
var target = $(e.target);
if(!target) return;
if(!target.hasClass('accountMenu') && !target.is($('.accountMenu').children())){
$scope.toggleAccountMenu();
}
};
} else if (!e) {
$window.onclick = null;
$scope.$apply();
}
}
This uses jQuery for child checking but you can probably do it without if needed.
I was getting some nasty errors with other peoples version, like trying to call $apply() when its already in a cycle, my version prevents propagation and safe-checks against $apply()