Problems related to a resizable component - reactjs

I am writing a component that allows the width of it's child element to change if you click on its right border and drag it.
I have a few problems however. First off, it's awkward to drag the div element, because if the mouse enters the other element to the right while dragging, the dragging state is lost and bugs out.
Also, I currently show the resize cursor when the point is within 5 pixels of the right border, which works fine when inside the resizable div. However, if you approach the border from the right (mouse inside other div), you cannot select it, even though you're within 5 pixels.
Another problem is that when I drag the mouse and resize the div, the mouse selects the text it drags over.
Lastly, because the element has to rerender each time it's width is changed, I've noticed that the performance is not always smooth.
Any advice on how to mitigate these problems?
Resizable = React.createClass({
propTypes: {
id : React.PropTypes.string,
class : React.PropTypes.string,
width : React.PropTypes.number,
onResize : React.PropTypes.func,
onAction : React.PropTypes.func,
},
getInitialState: function() {
return {
showResizeCursor : false,
canResize : false,
};
},
getDefaultProps: function() {
return {
};
},
_handleMouseMove: function(event) {
var node = React.findDOMNode(this);
var offsets = node.getBoundingClientRect();
var divLeft = offsets.left;
var divRight = offsets.right;
var mouseX = event.clientX;
var maxWidth = this.props.maxWidth || this.props.width;
var minWidth = this.props.minWidth || this.props.width;
var newWidth = mouseX - divLeft + 200;
var isWithinBounds = newWidth <= maxWidth && newWidth >= minWidth;
if (this.state.canResize && isWithinBounds) {
this.props.onResize(newWidth);
}
var difference = Math.abs(divRight - mouseX);
if (difference < 4) {
return this.setState({ showResizeCursor: true });
}
if (this.state.showResizeCursor) {
this.setState({ showResizeCursor: false });
}
},
_handleMouseUp: function() {
this.setState({ canResize: false });
},
_handleMouseDown: function() {
if (this.state.showResizeCursor) {
this.setState({ canResize: true });
}
},
render: function() {
var style = {
width : this.state.width,
};
if (this.state.showResizeCursor) { style.cursor = 'col-resize'; }
return (
<div id={this.props.id}
style ={style}
className ={this.props.class}
onMouseDown ={this._handleMouseDown}
onMouseUp ={this._handleMouseUp}
onMouseMove ={this._handleMouseMove}
onMouseLeave={this._handleMouseUp}
>
{this.props.children}
</div>
);
}
});
Example usage:
render: function() {
...
return (
<Wrapper>
<Resizable
id = {'list-view'}
width = {this.state.listViewWidth}
maxWidth = {this.state.listViewMaxWidth}
minWidth = {this.state.listViewMinWidth}
onResize = {this._handleListViewResize}
>
{first_column_that_should_be_resizable}
</Resizable>
{second_column_not_resizeable}

There are many different concerns here...
First off, it's awkward to drag the div element, because if the mouse enters the other element to the right while dragging, the dragging state is lost and bugs out.
This is a very common issue when you start coding your first drag&drop alike behavior. You should not listen the mousedown, mousemove and mouseup events on the same element, you should only listen the mousedown event and in that handler start listening the other two but on the body of the document. This way, you have a global handler and you will not have problems with the mouse getting over other elements.
Also, I currently show the resize cursor when the point is within 5 pixels of the right border, which works fine when inside the resizable div. However, if you approach the border from the right (mouse inside other div), you cannot select it, even though you're within 5 pixels.
I would suggest you to use only CSS for this. Is what it is for :)
Another problem is that when I drag the mouse and resize the div, the mouse selects the text it drags over.
Yep, just CSS. Once your mousedown handler is executed add a special CSS class to your element and put something like this in your CSS.
.disable-select {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
Lastly, because the element has to rerender each time it's width is changed, I've noticed that the performance is not always smooth.
I don't think React is your best option in here. I would just add this behavior using jQuery and the lifecycle methods like componentDidMount. This way, you can resize the div using plain jQuery (on every mouse move) and then just apply the final state (that is, the final size) to your component on the mouseup handler.

Related

Animating a height value for text input

So i am using react-native-autogrow-textinput in order to have an editable document viewable in my application. I am currently trying to work around the keyboard in order to adjust the height of textinput box, so that all text is visible. I have found the following code to do so
componentWillMount () {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide.bind(this));
}
componentWillUnmount () {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
keyboardDidShow(e){
let newSize = Dimensions.get('window').height- e.endCoordinates.height - 150;
console.log(e.endCoordinates);
this.setState({docViewHeight: newSize});
}
keyboardDidHide(e){
let newSize = Dimensions.get('window').height - 170;
this.setState({docViewHeight: newSize})
}
However, the result i am getting is: When the keyboard is animating off screen, the height of the textinput remains the same, let newSize = Dimensions.get('window').height- e.endCoordinates.height - 150, untill the keyboard has finished sliding off screen.
The height then adjusts to fill the whole screen again, except it sort of 'pops' into the new height. How do i get the value of this height to gradually grow, so it looks like it is simply extending to fit the whole screen? Ill post my Autogrow TextInput code below also. Any help would be much appreciated.
<AutoGrowingTextInput
ref="editText"
editable = {this.state.editting}
style = {{fontSize: fontProperties.fontSize+3, marginLeft: 18, marginRight: 18, marginTop: 15}}
/*animate this*/ minHeight = {this.state.docViewHeight}
animation = {{animated: true, duration: 300}}
//has some other confidential props here for onChange etc
</AutoGrowingTextInput>
Found the answer myself, after digging through some library files.
The solution is to use a keyboardWillHide event listener instead of keyboardDidHide.
This will fire before the keyboard begins its outro animation. Ive put the code below.
componentWillMount () {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
this.keyboardWillHideListener = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide.bind(this));
}
componentWillUnmount () {
this.keyboardDidShowListener.remove();
this.keyboardWillHideListener.remove();
}
keyboardWillHide(e){
let newSize = Dimensions.get('window').height - 170;
this.setState({docViewHeight: newSize})
}

AngularJS animate dynamic margin

I have an element that appears when the user clicks a button elsewhere on the screen. The element appears to come out of the top of the screen. The element by default needs to be tucked out of view above the screen, so I will have a margin-top style that is based on the height of the element (and will be a negative value). This cannot be hardcoded in css because the element height may vary. When I click the button, I want the element margin-top to change to 0 and I want a transition animation.
The sample shown on angularJS documentation is for adding a removing a class. This would work fine if I knew the values to be set and could code them in CSS, however I cannot. What is the correct way to solve this?
The code below works for displaying and hiding my element using a margin but there is no animation. How do I trigger an animation here when the margin changes?
https://docs.angularjs.org/guide/animations
Quote Total: {{salesPriceTotal + taxesTotal - tradeInsTotal | currency}}
<div class="totals" ng-style="setTopMargin()">
// totals stuff here.
</div>
$scope.setTopMargin = function() {
return {
marginTop: $scope.marginTop
}
};
$scope.$watch('showTotals', function() {
var margin = $scope.showTotals ? 10 : -160 + $scope.modelTotals.length * -200;
$scope.marginTop = margin.toString() + 'px';
});
I added the following code per a suggested solution, but this code is never hit.
myApp.animation('.totals', function () {
return {
move: function (element, done) {
element.css('opacity', 0);
jQuery(element).animate({
opacity: 1
}, done);
// optional onDone or onCancel callback
// function to handle any post-animation
// cleanup operations
return function (isCancelled) {
if (isCancelled) {
jQuery(element).stop();
}
}
},
}
});
As the documentation explains: "The same approach to animation can be used using JavaScript code (jQuery is used within to perform animations)".
So you basically needs to use animate() from jQuery to do what you want.

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

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

Combo box mouse wheel scrolling also scrolling on page

I have a dojo comboBox element which I fill with a lot of entries so the combo-box becomes scrollable. When I scroll inside the combobox with the mouse wheel also the page is being scrolled. Is there a way to prevent this?
dojo.connect(combo, 'onChange', function() {
var dropDownMenu = combo.dropDown.domNode;
if (dropDownMenu !== null) {
dropDownMenu.addEventListener('mousewheel', function(event) {
document.body.style.overflow = "hidden";
},false);
}
});

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