How do I capture table td elements using mousedown.dragselect event? - angularjs

I have a directive which renders a HTML table where each td element has an id
What I want to accomplish is to use the mousedown.dragselect/mouseup.dragselect to determine which elements have been selected, and then highlight those selected elements. What I have so far is something like this:
var $ele = $(this);
scope.bindMultipleSelection = function() {
element.bind('mousedown.dragselect', function() {
$document.bind('mousemove.dragselect', scope.mousemove);
$document.bind('mouseup.dragselect', scope.mouseup);
});
};
scope.bindMultipleSelection();
scope.mousemove = function(e) {
scope.selectElement($(this));
};
scope.mouseup = function(e) {
};
scope.selectElement = function($ele) {
if (!$ele.hasClass('eng-selected-item'))
$ele.addClass('eng-selected-item'); //apply selection or de-selection to current element
};
How can I get every td element selected by mousedown.dragselect, and be able to get their ids and then highlight them?

I suspect using anything relating to dragging won't give you what you want. Dragging is actually used when moving elements about (e.g. dragging files in My Computer / Finder), when what you're after is multiple selection.
So there a number of things the directive needs:
Listen to mousedown, mouseenter and mouseup, events.
mousedown should listen on the cells of the table, and set a "dragging" mode.
mouseenter should listen on the cells as well, and if the directive is in dragging mode, select the "appropriate cells"
mouseup should disable dragging mode, and actually be on the whole body, in case the mouse is lifted up while the cursor is not over the table.
jQuery delegation is useful here, as it can nicely delegate the above events to the table, so the code is much more friendly to cells that are added after this directive is initialised. (I wouldn't include or use jQuery in an Angular project unless you have a clear reason like this).
Although you've not mentioned it, the "appropriate cells" I suspect all the cells "between" where the mouse was clicked, and the current cell, chosen in a rectangle, and not just the cells that have been entered while the mouse was held down. To find these, cellIndex and rowIndex can be used, together with filtering all the cells from the table.
All the listeners should be wrapped $scope.$apply to make sure Angular runs a digest cycle after they fire.
For the directive to communicate the ids of the selected elements to the surrounding scope, the directive can use bi-directional binding using the scope property, and the = symbol, as explained in the Angular docs
Putting all this together gives:
app.directive('dragSelect', function($window, $document) {
return {
scope: {
dragSelectIds: '='
},
controller: function($scope, $element) {
var cls = 'eng-selected-item';
var startCell = null;
var dragging = false;
function mouseUp(el) {
dragging = false;
}
function mouseDown(el) {
dragging = true;
setStartCell(el);
setEndCell(el);
}
function mouseEnter(el) {
if (!dragging) return;
setEndCell(el);
}
function setStartCell(el) {
startCell = el;
}
function setEndCell(el) {
$scope.dragSelectIds = [];
$element.find('td').removeClass(cls);
cellsBetween(startCell, el).each(function() {
var el = angular.element(this);
el.addClass(cls);
$scope.dragSelectIds.push(el.attr('id'));
});
}
function cellsBetween(start, end) {
var coordsStart = getCoords(start);
var coordsEnd = getCoords(end);
var topLeft = {
column: $window.Math.min(coordsStart.column, coordsEnd.column),
row: $window.Math.min(coordsStart.row, coordsEnd.row),
};
var bottomRight = {
column: $window.Math.max(coordsStart.column, coordsEnd.column),
row: $window.Math.max(coordsStart.row, coordsEnd.row),
};
return $element.find('td').filter(function() {
var el = angular.element(this);
var coords = getCoords(el);
return coords.column >= topLeft.column
&& coords.column <= bottomRight.column
&& coords.row >= topLeft.row
&& coords.row <= bottomRight.row;
});
}
function getCoords(cell) {
var row = cell.parents('row');
return {
column: cell[0].cellIndex,
row: cell.parent()[0].rowIndex
};
}
function wrap(fn) {
return function() {
var el = angular.element(this);
$scope.$apply(function() {
fn(el);
});
}
}
$element.delegate('td', 'mousedown', wrap(mouseDown));
$element.delegate('td', 'mouseenter', wrap(mouseEnter));
$document.delegate('body', 'mouseup', wrap(mouseUp));
}
}
});
Another thing that will make the experience a bit nicer, is to set the cursor to a pointer, and disable text selection
[drag-select] {
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
You can also see this in action in this working demo

Related

Combining <ons-sliding-menu> and <ons-carousel>

I have an app with <ons-sliding-menu> and a page with <ons-toolbar> and a horizontal <ons-carousel> covering the remaining space.
For the <ons-sliding-menu> the parameter swipe-target-width="50px" is set.
Is there a way to tell the <ons-carousel> to ignore events originating from the most left 50px and let these go to the menu?
Currently there is no option to make the carousel ignore events on one side, but perhaps you can make a trick. You can put a div at the same level than the carousel and let it take the clicks instead of the carousel in the area you need:
<div class="cover"></div>
<ons-carousel>
...
</ons-carousel>
You can change these values to fit your case:
.cover {
position: absolute;
left: 0;
height: 100%;
width: 200px;
z-index: 1;
}
Check it out here: http://codepen.io/frankdiox/pen/YqKOJE
Hope it helps!
After some experimentation, I came to the solution to inject the necessary functionality directly in the drag event handlers of the OnsCarouselElement. For this purpose I have introduced the attribute swipe-ignore-left for the <ons-carousel>. The other sites could easily be added when needed.In order to inject the functionality, load this JS-Code after loading onsenui.js:
(function () {
'use strict';
/****************************************************************
Checks the current event against the attribute swipe-ignore-left.
****************************************************************/
window.OnsCarouselElement.prototype._ignoreDrag = function (event) {
var attr = this.getAttribute('swipe-ignore-left');
if (attr === undefined) return false;
var left = parseInt(attr, 10);
if (left === undefined || left < 1) return false;
var startX = event.gesture.center.clientX - event.gesture.deltaX;
return startX < left;
};
/****************************************************************
Save the original drag-event-handlers
****************************************************************/
var originalCarouselOnDrag = window.OnsCarouselElement.prototype._onDrag;
var originalCarouselOnDragEnd = window.OnsCarouselElement.prototype._onDragEnd;
/****************************************************************
Override: OnsCarouselElement.prototype._onDrag
****************************************************************/
window.OnsCarouselElement.prototype._onDrag = function (event) {
if (this._ignoreDrag(event)) return;
originalCarouselOnDrag.apply(this, arguments);
};
/****************************************************************
Override: OnsCarouselElement.prototype._onDragEnd
****************************************************************/
window.OnsCarouselElement.prototype._onDragEnd = function (event) {
if (this._ignoreDrag(event)) return;
originalCarouselOnDragEnd.apply(this, arguments);
};
})();
To preserve for example the left 20 pixel for the <ons-sliding-menu>, this HTML is to provide:
<ons-sliding-menu ... side="left" swipeable swipe-target-width="20px" />
...
<ons-carousel ... swipeable swipe-ignore-left="20px" />

how can I click the spinner on a Kendo NumericTextBox in a protractor test

Does anyone know how I can programmatically select an element in a protractor test that has the attribute unselectable="on"?
I need to write a protractor test that checks whether my ng-model is being incremented correctly but I can't select the arrow to click because of this attribute.
So this doesn't work in my test:
var arrow = element(by.css('k-icon k-i-arrow-n')); // this should select the up, increment arrow
arrow.click(); // this doesn't work because nothing has been selected.
Your CSS selector would not match the element, use dots for class names:
element(by.css('.k-icon.k-i-arrow-n')).click();
You may also locate the arrow up by title:
element(by.css('span[title="Increase value"]')).click();
Working demo test (using this Kendo UI demo page):
describe("Kendo UI numeric field", function () {
beforeEach(function () {
browser.ignoreSynchronization = true;
browser.get("http://demos.telerik.com/kendo-ui/numerictextbox/index");
});
it("should increase numeric currency value", function () {
var container = element.all(by.css(".k-numerictextbox")).first();
var input = container.element(by.id("currency"));
var arrowUp = container.element(by.css('span[title="Increase value"]'));
var times = 5;
input.getAttribute("value").then(function (oldValue) {
for (var i=0; i < times; i++) {
arrowUp.click();
}
expect(input.getAttribute("value")).toEqual((parseInt(oldValue) + times).toString());
});
});
});

Force validation of entire form in AngularJS upon editing any portion of the form?

I have a form in which the validity depends upon the relationship between multiple textboxes. For example, if there are three textboxes, then the form is valid only if each textbox's integer value is greater than the previous textbox's integer value.
I'd like to set up this form so that if the user edits any of the textboxes, the entire form revalidates.
I've tried setting up ng-change=revalidate() on all the textboxes, with the following:
$scope.revalidate = function() {
var formData = $parse('signals');
var dataCopy = angular.copy(formData($scope));
formData.assign($scope, dataCopy);
};
I hoped that copying and reassigning the form's data would trigger revalidation, but it doesn't seem to work. How would I achieve this?
I solved this by creating a directive. In that directive, I set up a $watch on the concatenated values of all the textboxes. Then when that $watch sees a change in any of the textboxes, it revalidates the element. Since this directive is applied to all my textboxes, the entire form revalidates when any one of the textboxes is edited.
If someone has a more elegant solution than this, let me know.
link: function(scope, elm, attrs, ctrl) {
// when any of the intervals for this signal change, revalidate this interval
scope.$watch(
// loop through all the intervals for this signal, concatenate their values into one string
function() {
var intervals = [],
child = scope.$parent.$$childHead;
while (child !== null) {
console.log(child);
intervals.push(child.interval.end);
child = child.$$nextSibling;
}
return intervals.join();
},
function() {
validate(ctrl.$viewValue);
}
);
function validate(intervalDateTimeFromView) {
var valid = false;
// if this interval ends before or at the same time as the previous interval
if (scope.$$prevSibling && Number(intervalDateTimeFromView) <= Number(scope.$$prevSibling.interval.end))
{
ctrl.$setValidity('overlappingInterval', false);
return undefined;
} else {
ctrl.$setValidity('overlappingInterval', true);
return intervalDateTimeFromView;
}
}
ctrl.$parsers.unshift(validate);
ctrl.$formatters.unshift(validate);
}
It's not perfect, but it's what I'm working on at the moment:
$element.bind('blur', function() {
formCtrl[inputName].$dirty = true;
$scope.$emit('validate-refresh');
});
$scope.$on('validate-refresh', function() {
var control = formCtrl[inputName];
if (control.$dirty) {
control.$setViewValue(control.$viewValue);
}
}

Looking for a better way to loop through and display one element at a time in AngularJS

I'm building an app that has animations in it and I need it to work better. I want to be able to go through a series of divs at a specified animation interval and display one at a time. And I want each series to have its own speed and its own divs.
Here's what I have so far (also copied below): http://jsfiddle.net/ollerac/shkq7/
Basically, I'm looking for a way to put the setInterval on a property of of the animatedBox so I can create a new animatedBox with custom properties. But every time I try to do this it breaks.
HTML
<div ng-app ng-controller="BoxController">
<div class="layer" ng-repeat="layer in animatedBox.layers" ng-style="{ 'backgroundColor': layer.color}" ng-show="layer == animatedBox.selectedLayer"></div>
</div>
JAVASCRIPT
function buildBox () {
return {
color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
}
}
function BoxController ($scope) {
$scope.animatedBox = {
layers: [],
selectedLayer: null
};
for (var i = 0; i < 5; i++) {
$scope.animatedBox.layers.push(buildBox());
}
var i = -1;
setInterval(function () {
$scope.$apply(function() {
i++;
if (i < $scope.animatedBox.layers.length) {
$scope.animatedBox.displayThisLayer = $scope.animatedBox.layers[i];
} else {
i = 0;
$scope.animatedBox.selectedLayer = $scope.animatedBox.layers[i];
}
});
}, 500);
}
CSS
.layer {
width: 30px;
height: 30px;
position: absolute;
}
*Update*
Here's more along the lines of what I want to do:
updated jsFiddle: http://jsfiddle.net/ollerac/shkq7/2/
function buildBox () {
return {
color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
}
}
function BoxController ($scope) {
$scope.animatedBox = {
layers: [],
selectedLayer: null,
selectedLayerIndex: -1,
updateSelectedLayer: function () {
var self = this;
if (self.layers.length) {
$scope.$apply(function() {
self.selectedLayerIndex++;
if (self.selectedLayerIndex < self.layers.length) {
self.selectedLayer = self.layers[self.selectedLayerIndex];
} else {
self.selectedLayerIndex = 0;
self.selectedLayer = self.layers[self.selectedLayerIndex];
}
});
}
}
};
for (var i = 0; i < 5; i++) {
$scope.animatedBox.layers.push(buildBox());
}
setInterval(function () {
$scope.animatedBox.updateSelectedLayer();
}, 500);
}
So now the object updates its own selectedLayer property. But I still need to call the setInterval that calls the update separately in order to get it to update. But I'd like the object to update itself and be completely independent. Can you think of a good way to do this because I'm really stuggling with it...
I guess this is more of a general javascript question, but I thought there might be an Angular way to handle this type of situation, like maybe using a directive or something would be appropriate.
Any suggestions would be much appreciated.
You are correct, I believe a directive is the right solution here. (This one was a fun one to work on, by the way. :)
When approaching a problem like this, I usually start by writing the HTML and controller that I'd wish I could write, if everything already worked. For this example, here's what I ended up with.
<div ng-controller="BoxController">
<div animated-boxes="colors"></div>
</div>
app.value('randomColor', function() {
var red = Math.floor(Math.random() * 255);
var green = Math.floor(Math.random() * 255);
var blue = Math.floor(Math.random() * 255);
return "rgb(" + red + "," + green + "," + blue + ")";
});
app.controller('BoxController', function($scope, randomColor) {
$scope.colors = [ randomColor(), randomColor() ];
});
Here, the controller is only responsible for setting some basic data on the scope--an array of colors; the DOM is very simple, only passing in that array to something called animated-boxes. randomColor has been moved into a service so it can be reused and tested more easily. (I also changed it a bit so it doesn't result in bad hex values.)
Now, the only part that doesn't already work is this thing called animated-boxes. Any time you want to interact with the DOM, or to trigger some behavior when an HTML attribute is used, we move to a directive.
We'll define our directive, injecting the $timeout service since we know we want to do timer-based stuff. The result of the directive will just be an object.
app.directive('animatedBoxes', function($timeout) {
return {
};
});
Since we want the directive to be self-contained and not mess up the outer scope in which it's contained, we'll give it an isolate scope (see the directive docs for more information, but basically this just means we have a scope that's not attached to the scope in which the directive lives except through variables we specify.)
Since we want to have access to the value passed in to the directive via the HTML attribute, we'll set up a bi-directional scope binding on that value; we'll call it colors.
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
}
};
});
We'll give it a simple template that loops over colors and outputs one of our divs per each color. Our ng-show indicates that the div should only be shown if the scope value selected is equal to $index, which is the array index of the current iteration of the ng-repeat loop.
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
},
template: "<div><div class='layer' ng-repeat='color in colors' " +
"ng-style='{backgroundColor: color}' ng-show='selected == $index'>" +
"</div></div>"
};
});
Now for the link function--the function that will handle the directive's logic. First, we want keep track of which box we're showing; in our ng-show, we used selected for this. We also want to keep track of how many boxes we have; we'll use $watch on our directive's scope to keep up with this.
link: function(scope, elem, attrs) {
scope.selected = 0;
var count = 0;
scope.$watch('colors', function(value) {
// whenever the value of `colors`, which is the array
// of colors passed into the directive, changes, update
// our internal count of colors
if (value) count = value.length;
else count = 0; // if `colors` is falsy, set count to 0
}, true); // `true` ensures we watch the values in the array,
// not just the object reference
}
Finally, we need to cycle through each box every so often. We'll do this with $timeout, which is a version of setTimeout that includes a scope $apply call (it does some other stuff, but we don't care about that now).
var nextBox = function() {
if (scope.selected >= count - 1) scope.selected = 0;
else scope.selected++;
// recursively use `$timeout` instead of `setInterval`
$timeout(nextBox, 500);
};
// kick off the directive by launching the first `nextBox`
nextBox();
If you put the entire directive so far together, you'll end up with this code (comments removed):
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
},
template: "<div><div class='layer' ng-repeat='color in colors' " +
"ng-style='{backgroundColor: color}' ng-show='selected == $index'>" +
"</div></div>",
link: function(scope, elem, attrs) {
scope.selected = 0;
var count = 0;
scope.$watch('colors', function(value) {
if (value) count = value.length;
else count = 0;
}, true);
var nextBox = function() {
if (scope.selected >= count - 1) scope.selected = 0;
else scope.selected++;
$timeout(nextBox, 500);
};
nextBox();
}
};
});
A full working example, including comments and a little debugging area where you can see the value of colors and interact with it (so you can see how the directive responds to changes in the controller) can be found here: http://jsfiddle.net/BinaryMuse/g6A6Y/
Now that you have this, consider trying to apply this knowledge to allow the directive to have a variable speed by passing it in via the DOM, like we did with colors. Here's my result: http://jsfiddle.net/BinaryMuse/cHHKn/

Sencha Touch 2.0 - How to set scrolling inside a textarea for Mobile Safari?

In my mobile safari project, i need to create a message posting feature. it is requires scrolling inside a textarea when lines of texts exceed the max rows of the text area. i couldn't find 'scrollable' property in Ext.field.textarea, any idea how?
Cheers!
There is a bug in touch 2.0.x such that the framework explicitly prevents the scroll action. Supposedly a fix will be in 2.1, though I didn't see that officially, just from a guy on a forum.
Until then, there is kind of a solution for touch1 here http://www.sencha.com/forum/showthread.php?180207-TextArea-scroll-on-iOS-not-working that you can port to V2. It basically involves adding an eventlistener to the actual textarea field (not the sencha object) and then calling preventdefault if it's a valid scrollevent.
The full code is at that link, but the salient bits are here.
Grab the <textarea> field (not the Sencha Touch object) directly and use addListener to apply
'handleTouch' on touchstart and 'handleMove' on touchmove
handleTouch: function(e) {
this.lastY = e.pageY;
},
handleMove: function(e) {
var textArea = e.target;
var top = textArea.scrollTop <= 0;
var bottom = textArea.scrollTop + textArea.clientHeight >= textArea.scrollHeight;
var up = e.pageY > this.lastY;
var down = e.pageY < this.lastY;
this.lastY = e.pageY;
// default (mobile safari) action when dragging past the top or bottom of a scrollable
// textarea is to scroll the containing div, so prevent that.
if((top && up) || (bottom && down)) {
e.preventDefault();
e.stopPropagation(); // this tops scroll going to parent
}
// Sencha disables textarea scrolling on iOS by default,
// so stop propagating the event to delegate to iOS.
if(!(top && bottom)) {
e.stopPropagation(); // this tops scroll going to parent
}
}
Ext.define('Aspen.util.TextArea', {
override: 'Ext.form.TextArea',
adjustHeight: Ext.Function.createBuffered(function (textarea) {
var textAreaEl = textarea.getComponent().input;
if (textAreaEl) {
textAreaEl.dom.style.height = 'auto';
textAreaEl.dom.style.height = textAreaEl.dom.scrollHeight + "px";
}
}, 200, this),
constructor: function () {
this.callParent(arguments);
this.on({
scope: this,
keyup: function (textarea) {
textarea.adjustHeight(textarea);
},
change: function (textarea, newValue) {
textarea.adjustHeight(textarea);
}
});
}
});

Resources