Update URL in browser while scrolling a page in gatsby [duplicate] - reactjs

Is it possible to change the URL in the address bar instantly when I scroll to an id? Or have a long document with multiple id and the url changes on address bar, when I hit a new id even i scroll form bottom to top.
i use Change url when manually scrolled to an anchor?
but this is not working when scroll from bottom to top.
$(function () {
var currentHash = "#";
$(document).scroll(function () {
$('.block').each(function () {
var top = window.pageYOffset;
var distance = top - $(this).offset().top;
var hash = $(this).attr('id');
// 30 is an arbitrary padding choice,
// if you want a precise check then use distance===0
if (distance < 30 && distance > -30 && currentHash != hash) {
window.location.hash = (hash);
currentHash = hash;
}
});
});
});
body {
margin: 0px;
}
div {
height: 900px;
text-align: center;
padding: 15px;
background-color: #00f;
}
div:nth-child(even) { background: #ccc; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
<div class="block" id="one">Block 1</div>
<div class="block" id="two">Block 2</div>
<div class="block" id="three">Block 3</div>
<div class="block" id="four">Block 4</div>
<div class="block" id="five">Block 5</div>
Thanks in advance.

After your comment, I understand what you are trying to achieve:
The following code is based on scroll up/ down.
Will store the current block and make you "jump" between blocks:
$(function () {
var blocksArr = $('.block');
var lastScrollTop = 0;
var currentBlock = 0;
$(document).scroll(function () {
var st = $(this).scrollTop();
var hash;
//make sure it is in the boundaries
if (st > lastScrollTop && currentBlock< blocksArr.length -1){
// downscroll code
hash = $(blocksArr[++currentBlock]).attr('id');
window.location.hash = (hash);
}
else
if (st < lastScrollTop && currentBlock > 0){
// scrollup code
hash = $(blocksArr[--currentBlock]).attr('id');
window.location.hash = (hash);
}
lastScrollTop = $(this).scrollTop();
});
});
"working" fiddle (hash wont change on fiddle)
added:
If you only want to see the URL changes:
$(function () {
var currentHash = "#";
var blocksArr = $('.block');
$(document).scroll(function () {
var currentTop = window.pageYOffset/1;
for (var i=0; blocksArr.length; i++){
var currentElementTop = $(blocksArr[i]).offset().top;
var hash = $(blocksArr[i]).attr('id');
if (currentElementTop < currentTop && currentTop < currentElementTop + $(blocksArr[i]).height() && currentHash!=hash){
if(history.pushState) {
history.pushState(null, null, '#'+hash);
}
else {
location.hash = '#'+hash;
}
currentHash = hash;
}
}
});
});

Related

AngularJS: How to initialize a $scope attribute

I wrote the following small angular code snippet:
html file:
<body ng-controller="myCtrl">
<div class="container">
....
</div>
</body>
javascript file:
app.controller('myCtrl', function($scope) {
$scope.white_sld = get_init_white_sld();
$scope.get_soldier_style = function is_soldier(loc) {
var sld_color = get_soldier_color(loc);
if (sld_color == "") {
return ""; // no soldier in this square
} else {
return {
'width': '80%', 'height': '80%', 'border-radius': '80%', 'background-color': sld_color,
'margin': 'auto auto', 'vertical-align': 'middle'
}
}
function get_soldier_color(loc) {
for (var i = 0; i < white_sld.length; i++) {
if ((loc[0] == white_sld[i][0]) && (loc[1] == white_sld[i][1])) {
return "white"
}
}
for (var i = 0; i < black_sld.length; i++) {
if ((loc[0] == black_sld[i][0]) && (loc[1] == black_sld[i][1])) {
return "black"
}
}
return ""; // no soldier found
}
}
});
function get_init_sld_line(x_loc, y_loc, color) {
var line_sld = [];
for (var i = 0; i < 4; i++) {
line_sld.push(new Soldier(x_loc + 2 * i, y_loc, color));
}
return line_sld;
}
function get_init_white_sld() {
var init_white_sld = [];
init_white_sld.push(get_init_sld_line(0, 0, "white"));
init_white_sld.push(get_init_sld_line(1, 1, "white"));
init_white_sld.push(get_init_sld_line(0, 2, "white"));
return init_white_sld;
};
My problem with this code is that after the line "$scope.white_sld = get_init_white_sld();", $scope.white_sld gets the return value of the function get_init_white_sld(). However, when it gets inside the function 'is_soldier', then $scope.white_sld becomes undefined. My question is why and how can I resolve it?
That's because you should use $scope.white_sld within that function, not just white_sld.
I have updated the plunkr please check
Actually you forgot to add return statement in get_init_sld_line function.
https://plnkr.co/edit/kYebs1pFznaDpHlFDodK?p=preview
return line_sld;

Custom Directive is not working with div id

I have a custom directive called packageHeader, When the user scrolls through the list, the header must show at top of the list until the next one is reached. It's working with only window element not with div id, here i'm attaching the html
<div id="list">
<package-header>
<div>Header 1</div>
</package-header>
<div>content Header 1</div>
<package-header>
<div>Header2</div>
</package-header>
<div>content Header2</div>
<package-header>
<div>Header 3</div>
</package-header>
<div>content Header 3</div>
<div>
my custom directive
angular.module('myApp')
.directive('packageHeader',
['$window', function($window) {
var stickies = [],
scroll = function scroll() {
var header= angular.element(document.querySelector("#List"))[0];
console.log("scroll _ scroll");
angular.forEach(stickies, function($sticky, index) {
var sticky = $sticky[0],
pos = $sticky.data('pos');
if (pos <= header.pageYOffset) {
console.log("scroll offset Y");
var $next = stickies[index + 1],
next = $next ? $next[0] : null,
npos = $next.data('pos');
$sticky.addClass("fixed");
if (next && next.offsetTop >= npos - next.clientHeight)
$sticky.addClass("absolute").css("top", npos - sticky.clientHeight + 'px');
} else {
console.log("scroll offset X");
var $prev = stickies[index - 1],
prev = $prev ? $prev[0] : null;
$sticky.removeClass("fixed");
if (prev && header.pageYOffset <= pos - prev.clientHeight)
$prev.removeClass("absolute").removeAttr("style");
}
});
},
link = function($scope, element, attrs) {
var sticky = element.children()[0],
$sticky = angular.element(sticky);
element.css('height', sticky.clientHeight + 'px');
$sticky.data('pos', sticky.offsetTop);
stickies.push($sticky);
};
angular.element(document.querySelector("#List"))
.off('scroll', scroll)
.on('scroll', scroll);
return {
restrict: 'E',
transclude: true,
//sticky - getting from style sheet
template: '<sticky ng-transclude></sticky>',
link: link
};
}]);
CSS
package-header,
sticky {
display: block;
}
package-header{
opacity:.8;
}
package-header>sticky {
background: #9aa2a8;
line-height: 24px;
z-index: 1;
color: #fff;
font-weight: 700;
}
package-header>sticky.fixed {
position: fixed;
top: 0;
width: 100%;
z-index: 0;
}
package-header>sticky.fixed.absolute {
position: absolute;
}
Please help me to resolve my issue

How to swipe from left to right Ionic list item?

I want to swipe Ionic list items to both sides. (i.e left-right AND right-left). It works perfectly for right-left swipe but I am not able to swipe list item to left side.
I used $ionicGesture for left-right swipe, and it also gives me an alert when i use swiperight event: event($ionicGesture.on('swiperight', scope.reportEvent, elem)), but I am not able to let it show the ion-option-button at the left side.
Here is my directive and controller code:
.directive('onSwipeRight', function($ionicGesture) {
return {
restrict : 'A',
link : function(scope, elem, attrs) {
var gestureType = attrs.gestureType;
switch(gestureType) {
case 'swipeRight':
$ionicGesture.on('swiperight', scope.reportEvent, elem);
break;
case 'swipeleft':
$ionicGesture.on('swipeleft', scope.reportEvent, elem);
break;
case 'doubletap':
$ionicGesture.on('doubletap', scope.reportEvent, elem);
break;
case 'tap':
$ionicGesture.on('tap', scope.reportEvent, elem);
break;
}
}
}
})
.controller('ChatsCtrl', function($scope, Chats) {
// With the new view caching in Ionic, Controllers are only called
// when they are recreated or on app start, instead of every page change.
// To listen for when this page is active (for example, to refresh data),
// listen for the $ionicView.enter event:
//
//$scope.$on('$ionicView.enter', function(e) {
//});
$scope.chats = Chats.all();
$scope.remove = function(chat) {
Chats.remove(chat);
}
$scope.reportEvent = function (event) {
alert("hi");
console.log('Reporting : ' + event.type);
event.preventDefault();
};
})
Here is my html code.
<ion-view view-title="Chats">
<ion-content>
<ion-list can-swipe="true">
<ion-item gesture-type="swipeRight" on-swipe-right="swipeRight()" class="item-remove-animate item-avatar item-icon-right" ng-repeat="chat in chats" type="item-text-wrap" href="#/tab/chats/{{chat.id}}">
<img ng-src="{{chat.face}}">
<h2>{{chat.name}}</h2>
<p>{{chat.lastText}}</p>
<i class="icon ion-chevron-right icon-accessory"></i>
<ion-option-button class="button-assertive" ng-click="share(item)" side="left">
Share
</ion-option-button>
<ion-option-button class="button-assertive" ng-click="remove(chat)" side="right">
Delete
</ion-option-button>
</ion-item>
</ion-list>
</ion-content>
</ion-view>
So I want to display share button at left side and delete button at right side.
Can anybody provide me specific solution for it?
I've edited ionic lib to do something like that. But i couldn't do a JSFiddle or a Code Pen i Will give you the link to my modified ionic.css and ionic.bundle.js!
TL;DR
https://gist.githubusercontent.com/booris/847f044d2ef2a05101ce/raw/2274365384f5eed3e4538b269f3a7d7998eb22ed/ionic.css
https://gist.githubusercontent.com/booris/847f044d2ef2a05101ce/raw/2274365384f5eed3e4538b269f3a7d7998eb22ed/ionic.bundle.js
Just replace it with yours, start an ionic project blank. And put this HTML in it:
<body ng-app="starter">
<ion-pane>
<ion-header-bar class="bar-stable">
<h1 class="title">Ionic Blank Starter</h1>
</ion-header-bar>
<ion-content>
<ion-list show-delete="false" can-swipe="true" swipe-direction="both">
<ion-item href="#">
Item 1
<ion-option-button side="right" class="button-light icon ion-heart"></ion-option-button>
<ion-option-button side="right" class="button-light icon ion-email"></ion-option-button>
<ion-option-button side="left" class="button-assertive icon ion-trash-a"></ion-option-button>
</ion-item>
<ion-item href="#">
Item 2
<ion-option-button class="button-light icon ion-heart"></ion-option-button>
<ion-option-button class="button-light icon ion-email"></ion-option-button>
<ion-option-button class="button-assertive icon ion-trash-a"></ion-option-button>
</ion-item>
</ion-list>
</ion-content>
</ion-pane>
</body>
You can specify the wipe direction with left, right or both.
And in the ion-options-button you can give it a side.
Hope it helps, anything you need just ask! I will try to comment my changes in the code later on!
EDIT:
I will try to explain what i did.
First change the ionOptionButton directive to create to div for the button, one left and one right
//added second div with class item-options-left for the left buttons
var ITEM_TPL_OPTION_BUTTONS =
'<div class="item-options invisible">' +
'</div>' + '<div class="item-options-left invisible">' +
'</div>';
IonicModule.directive('ionOptionButton', [function () {
function stopPropagation(e) {
e.stopPropagation();
}
return {
restrict: 'E',
require: '^ionItem',
priority: Number.MAX_VALUE,
compile: function ($element, $attr) {
$attr.$set('class', ($attr['class'] || '') + ' button', true);
return function ($scope, $element, $attr, itemCtrl) {
if (!itemCtrl.optionsContainer) {
itemCtrl.optionsContainer = jqLite(ITEM_TPL_OPTION_BUTTONS);
itemCtrl.$element.append(itemCtrl.optionsContainer);
}
//[NEW] if it as an attribute side = 'left' put the button in the left container
if ($attr.side === 'left') {
angular.element(itemCtrl.optionsContainer[1]).append($element);
itemCtrl.$element.addClass('item-left-editable');
} else{
angular.element(itemCtrl.optionsContainer[0]).append($element);
itemCtrl.$element.addClass('item-right-editable');
}
//Don't bubble click up to main .item
$element.on('click', stopPropagation);
};
}
};
}]);
Add CSS to left buttons in ionic.css file
.item-options-left {
position: absolute;
top: 0;
left: 0;
z-index: 1;
height: 100%; }
.item-options-left .button {
height: 100%;
border: none;
border-radius: 0;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -moz-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
-moz-align-items: center;
align-items: center; }
.item-options .button:before {
margin: 0 auto; }
Now change the ion-list controller to accept swipe directions attribute
.controller('$ionicList', [
'$scope',
'$attrs',
'$ionicListDelegate',
'$ionicHistory',
function ($scope, $attrs, $ionicListDelegate, $ionicHistory) {
var self = this;
//[NEW] object with can-swipe attr and swipe-direction side attr, default direction is left
var swipe = {
isSwipeable: true,
side: 'left'
};
var isReorderShown = false;
var isDeleteShown = false;
var deregisterInstance = $ionicListDelegate._registerInstance(
self, $attrs.delegateHandle,
function () {
return $ionicHistory.isActiveScope($scope);
}
);
$scope.$on('$destroy', deregisterInstance);
self.showReorder = function (show) {
if (arguments.length) {
isReorderShown = !!show;
}
return isReorderShown;
};
self.showDelete = function (show) {
if (arguments.length) {
isDeleteShown = !!show;
}
return isDeleteShown;
};
//[NEW] get swipe direction attribute and store it in a variable to access in other function
self.canSwipeItems = function (can) {
if (arguments.length) {
swipe.isSwipeable = !!can;
swipe.side = $attrs.swipeDirection;
}
return swipe;
};
self.closeOptionButtons = function () {
self.listView && self.listView.clearDragEffects();
};
}]);
To end, you should replace slideDrag function with this one, just search for it in ionic.bundle.js
//[NEW] add this var to the others in the function
var ITEM_OPTIONS_CLASS_RIGHT = 'item-options-left';
var SlideDrag = function (opts) {
this.dragThresholdX = opts.dragThresholdX || 10;
this.el = opts.el;
this.item = opts.item;
this.canSwipe = opts.canSwipe;
};
SlideDrag.prototype = new DragOp();
SlideDrag.prototype.start = function (e) {
var content, buttonsLeft, buttonsRight, offsetX, buttonsLeftWidth, buttonsRightWidth;
if (!this.canSwipe().isSwipeable) {
return;
}
if (e.target.classList.contains(ITEM_CONTENT_CLASS)) {
content = e.target;
} else if (e.target.classList.contains(ITEM_CLASS)) {
content = e.target.querySelector('.' + ITEM_CONTENT_CLASS);
} else {
content = ionic.DomUtil.getParentWithClass(e.target, ITEM_CONTENT_CLASS);
}
// If we don't have a content area as one of our children (or ourselves), skip
if (!content) {
return;
}
// Make sure we aren't animating as we slide
content.classList.remove(ITEM_SLIDING_CLASS);
// Grab the starting X point for the item (for example, so we can tell whether it is open or closed to start)
offsetX = parseFloat(content.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]) || 0;
// Grab the buttons
buttonsLeft = content.parentNode.querySelector('.' + ITEM_OPTIONS_CLASS);
if (!buttonsLeft) {
return;
}
//[NEW] get the Right buttons
buttonsRight = content.parentNode.querySelector('.' + ITEM_OPTIONS_CLASS_RIGHT);
if (!buttonsRight) {
return;
}
// [NEW] added the same functionality to both sides, to make buttons visible when dragged
if(e.gesture.direction === "left")
buttonsLeft.classList.remove('invisible');
else
buttonsRight.classList.remove('invisible');
//[NEW] added buttonRight and buttonLeft properties to currentDrag
buttonsLeftWidth = buttonsLeft.offsetWidth;
buttonsRightWidth = buttonsRight.offsetWidth;
this._currentDrag = {
buttonsLeft: buttonsLeft,
buttonsRight: buttonsRight,
buttonsLeftWidth: buttonsLeftWidth,
buttonsRightWidth: buttonsRightWidth,
content: content,
startOffsetX: offsetX
};
};
/**
* Check if this is the same item that was previously dragged.
*/
SlideDrag.prototype.isSameItem = function (op) {
if (op._lastDrag && this._currentDrag) {
return this._currentDrag.content == op._lastDrag.content;
}
return false;
};
SlideDrag.prototype.clean = function (isInstant) {
var lastDrag = this._lastDrag;
if (!lastDrag || !lastDrag.content) return;
lastDrag.content.style[ionic.CSS.TRANSITION] = '';
lastDrag.content.style[ionic.CSS.TRANSFORM] = '';
if (isInstant) {
lastDrag.content.style[ionic.CSS.TRANSITION] = 'none';
makeInvisible();
ionic.requestAnimationFrame(function () {
lastDrag.content.style[ionic.CSS.TRANSITION] = '';
});
} else {
ionic.requestAnimationFrame(function () {
setTimeout(makeInvisible, 250);
});
}
function makeInvisible() {
lastDrag.buttonsLeft && lastDrag.buttonsLeft.classList.add('invisible');
lastDrag.buttonsRight && lastDrag.buttonsRight.classList.add('invisible');
}
};
SlideDrag.prototype.drag = ionic.animationFrameThrottle(function (e) {
var buttonsLeftWidth;
var buttonsRightWidth;
// We really aren't dragging
if (!this._currentDrag) {
return;
}
// Check if we should start dragging. Check if we've dragged past the threshold,
// or we are starting from the open state.
if (!this._isDragging &&
((Math.abs(e.gesture.deltaX) > this.dragThresholdX) ||
(Math.abs(this._currentDrag.startOffsetX) > 0))) {
this._isDragging = true;
}
if (this._isDragging) {
buttonsLeftWidth = this._currentDrag.buttonsLeftWidth;
buttonsRightWidth = this._currentDrag.buttonsRightWidth;
// Grab the new X point, capping it at zero
//[NEW] added right swipe new position
if (this.canSwipe().side === 'left' || (this.canSwipe().side === 'both' && e.gesture.direction === 'left'))
var newX = Math.min(0, this._currentDrag.startOffsetX + e.gesture.deltaX);
else if (this.canSwipe().side === 'right' || (this.canSwipe().side === 'both' && e.gesture.direction === 'right'))
var newX = Math.max(0, this._currentDrag.startOffsetX + e.gesture.deltaX);
var buttonsWidth = 0;
if (e.gesture.direction === 'right')
buttonsWidth = buttonsRightWidth;
else
buttonsWidth = buttonsLeftWidth;
// If the new X position is past the buttons, we need to slow down the drag (rubber band style)
if (newX < -buttonsWidth) {
// Calculate the new X position, capped at the top of the buttons
newX = Math.min(-buttonsWidth, -buttonsWidth + (((e.gesture.deltaX + buttonsWidth) * 0.4)));
}
this._currentDrag.content.$$ionicOptionsOpen = newX !== 0;
this._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + newX + 'px, 0, 0)';
this._currentDrag.content.style[ionic.CSS.TRANSITION] = 'none';
}
});
SlideDrag.prototype.end = function (e, doneCallback) {
var self = this;
// There is no drag, just end immediately
if (!self._currentDrag) {
doneCallback && doneCallback();
return;
}
// If we are currently dragging, we want to snap back into place
// The final resting point X will be the width of the exposed buttons
var restingPoint;
if (e.gesture.direction === 'left' && (this.canSwipe().side === 'left' || this.canSwipe().side === 'both'))
restingPoint = -self._currentDrag.buttonsLeftWidth;
if (e.gesture.direction === 'right' && (this.canSwipe().side === 'right' || this.canSwipe().side === 'both'))
restingPoint = self._currentDrag.buttonsRightWidth;
// Check if the drag didn't clear the buttons mid-point
// and we aren't moving fast enough to swipe open
var buttonsWidth = 0;
if (e.gesture.direction === 'right')
buttonsWidth = self._currentDrag.buttonsRightWidth;
else
buttonsWidth = self._currentDrag.buttonsLeftWidth;
if (e.gesture.deltaX > -(buttonsWidth / 2)) {
// If we are going left or right but too slow, or going right, go back to resting
if ((e.gesture.direction == "left" || e.gesture.direction == "right") && Math.abs(e.gesture.velocityX) < 0.3) {
restingPoint = 0;
}
}
ionic.requestAnimationFrame(function () {
if (restingPoint === 0) {
self._currentDrag.content.style[ionic.CSS.TRANSFORM] = '';
var buttonsLeft = self._currentDrag.buttonsLeft;
var buttonsRight = self._currentDrag.buttonsRight;
setTimeout(function () {
buttonsLeft && buttonsLeft.classList.add('invisible');
buttonsRight && buttonsRight.classList.add('invisible');
}, 250);
} else {
self._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + restingPoint + 'px,0,0)';
}
self._currentDrag.content.style[ionic.CSS.TRANSITION] = '';
// Kill the current drag
if (!self._lastDrag) {
self._lastDrag = {};
}
ionic.extend(self._lastDrag, self._currentDrag);
if (self._currentDrag) {
self._currentDrag.buttons = null;
self._currentDrag.content = null;
}
self._currentDrag = null;
// We are done, notify caller
doneCallback && doneCallback();
});
};
My solution is not perfect, but it works. and there are others ways of doing this, i did it this way to understand better how Ionic works and how they do Ionic directives.
Any feedback is welcome, and with this you can try to make your own or improve this one.
I made item-swipe-pane directive which creates a container inside a ion-item, which is visible when the item is swiped to the left or to the right.
var ITEM_SWIPE_PANE_TPL = '<div class="item-options invisible item-swipe-pane"></div>';
var DIRECTION_RIGHT_CLASS = 'direction-right';
module.directive( 'itemSwipePane' , function() {
return {
restrict: 'E',
require: '^ionItem',
link: function (scope, $element, attrs, itemCtrl) {
var container;
var direction = 'left';
// Set direction
if (attrs['direction'] && attrs['direction'] === 'right'){
direction = 'right';
} else if (attrs['direction'] && attrs['direction'] === 'left'){
direction = 'left';
}
if (direction === 'left'){
if (!itemCtrl.itemSwipeLeft){
itemCtrl.itemSwipeLeft = angular.element(ITEM_SWIPE_PANE_TPL);
itemCtrl.$element.append(itemCtrl.itemSwipeLeft);
}
container = itemCtrl.itemSwipeLeft;
} else if (direction === 'right'){
if (!itemCtrl.itemSwipeRight){
itemCtrl.itemSwipeRight = angular.element(ITEM_SWIPE_PANE_TPL);
// If direction is right, move position of item options
// to the left - override inherited right:0;
itemCtrl.itemSwipeRight.css({right: 'auto'});
// "direction-right" is container selector.
itemCtrl.itemSwipeRight.addClass(DIRECTION_RIGHT_CLASS);
itemCtrl.$element.append(itemCtrl.itemSwipeRight);
}
container = itemCtrl.itemSwipeRight;
}
container.append($element);
// Animation to slowly close opened item.
itemCtrl.$element.addClass('item-right-editable');
} // link
}; // return
}); // item-swipe-pane
Attribute direction controls swipe direction. Possible values are left or right. Default direction is left.
You can place any content it the directive, button, text, image, icon, avatar, background image, etc.
The container is quite raw in a sense that everything you place in it has to be formatted by CSS or by other means.
item-swipe-pane is compatible with ion-option-button, ion-delete-button and ion-reorder-button directives.
It is possible to combine two item-swipe-panes on the same ion-item. Each one with different swipe direction.
Example with two item-swipe-panes, one is on the left and one on the right:
<ion-item>
Two item-swipe-panes. One on the left, one on the right.
<item-swipe-pane direction="right">
<button class="button button-balanced ion-arrow-right-c"></button>
<button class="button button-positive">Update</button>
<button class="button button-royal">Add</button>
</item-swipe-pane>
<item-swipe-pane class="left-pane">
<button class="button button-assertive">Delete</button>
<button class="button button-calm">Share</button>
<button class="button button-balanced ion-arrow-left-c"></button>
</item-swipe-pane>
</ion-item>
More item-swipe-pane examples are on Codepen.
Important note:
Unfortunately Ionic Framework does not allow right swipe (from left to right) of a list item, so I had to make few modifications to the Ionic library. Here is summary of modifications to Ionic Framework.
Links:
Modified Ionic library download.
item-swipe-pane directive on Github.
here is an sample code using that u can achieve it
swipe-pane.html
<div class="mk-swipe-pane">
<div class="col-xs-4 swipe-actions-padding" ng-repeat="action in swipeActions">
<div ng-click="currentActionClick(action.actionName)"
ng-class="[action.actionCssclass]">
<div class="icon-font-size">
<i ng-class="[action.actionIcon]"></i>
</div>
{{action.actionName}}
</div>
</div>
</div>
swipe-pane.js
angular.module('mk.Directives')
.directive('mkSwipePane', function ($swipe) {
return {
templateUrl: "lib/mobikon/directives/notifications/swipe-pane/swipe-pane.html",
restrict: 'E',
scope: {
swipeActions: "="
},
replace: true,
link: function ($scope, element) {
var MAX_VERTICAL_DISTANCE = 75,
MAX_VERTICAL_RATIO = 0.3,
MIN_HORIZONTAL_DISTANCE = 30,
startCoords,
valid,
elWidth = $(element).width(),
direction = 1,
pointerTypes = ['touch'],
delayForAnimation = 70;
$scope.currentActionClick = function (actionName) {
$scope.$emit('currentActionName', actionName);
};
function validSwipe(coords) {
if (!startCoords) return false;
var deltaY = Math.abs(coords.y - startCoords.y);
var deltaX = (coords.x - startCoords.x) * direction;
return valid && // Short circuit for already-invalidated swipes.
deltaY < MAX_VERTICAL_DISTANCE &&
deltaX > 0 &&
deltaX > MIN_HORIZONTAL_DISTANCE &&
deltaY / deltaX < MAX_VERTICAL_RATIO;
}
$swipe.bind(element, {
'start': function (coords, event) {
startCoords = coords;
valid = true;
},
'move': function (coords, event) {
var diffX = coords.x - startCoords.x;
if (diffX < 0) {
direction = -1; // For left swipe
} else {
direction = 1; // For right swipe
}
if (validSwipe(coords)) {
var marginLeft = parseInt($(element).css("marginLeft"));
if (direction === -1 && Math.abs(diffX) <= elWidth / 2) {
$(element).prev().css({"margin-left": diffX});
} else if (direction === 1 && (marginLeft + diffX) <= 0) {
$(element).prev().css({"margin-left": marginLeft + diffX});
}
}
},
'cancel': function (event) {
valid = false;
},
'end': function (coords, event) {
if (validSwipe(coords)) {
if (direction === -1) {
$(element).prev().animate({"margin-left": "-50%"}, delayForAnimation);
$scope.$emit('isCurrentRowClickable', {isSwiped: false});
} else {
$(element).prev().animate({"margin-left": "0%"}, delayForAnimation);
$scope.$emit('isCurrentRowClickable', {isSwiped: true});
}
}
}
}, pointerTypes);
}
}
});
require("./swipe-pane.html");
require("./swipe-pane.scss");
swipe-pane.scss
#import "../../../../../views/mixins";
[mk-swipe-pane], .mk-swipe-pane {
display: inline-block;
width: 50%;
$icon-outline-color: $mk-pure-white;
$icon-font-size: 35px;
$icon-text-font-size: 16px;
$icon-margin-top:-10px;
$icon-padding-top:35%;
$icon-padding-bottom:5px;
$icon-container-height:120px;
$icon-width:20px;
#media screen and (max-width: 768px) {
.swipe-actions-padding {
padding-left: 0px;
padding-right: 0px;
float: none;
display: inline-block;
}
.icon-font-size {
font-size: $icon-font-size;
}
.email-icon {
text-align: center;
font-size: $icon-text-font-size;
margin-top: $icon-margin-top;
padding-top: $icon-padding-top;
height: $icon-container-height;
vertical-align: middle;
background-color: $mk-swipe-action-1-icon-background-orange;
color: $icon-outline-color;
}
.sms-icon {
text-align: center;
font-size: $icon-text-font-size;
margin-top: $icon-margin-top;
padding-top: $icon-padding-top;
height: $icon-container-height;
vertical-align: middle;
background-color: $mk-swipe-action-2-icon-background-blue;
color: $icon-outline-color;
}
.call-icon {
text-align: center;
font-size: $icon-text-font-size;
margin-top: $icon-margin-top;
padding-top: $icon-padding-top;
height: $icon-container-height;
vertical-align: middle;
background-color: $mk-swipe-action-3-icon-background-green;
color: $icon-outline-color;
}
.disabled {
background-color: $mk-background-gray !important;
}
}
}
Unlike other answers, i've created an angular wrapper for swiper (that seems to be the slider lib used in ionic 2) focused on ionic v1 instead of editing the framework itself.
My wrapper is avaliable here, and there's an demo here.
You can use npm install ionic-swiper to install it, and import like instructed on README.md:
In javascript with webpack (you can import the whole bundle too like a normal js):
import {moduleName as ionicSwiperModule} from 'ionic-swiper';
angular.module('yourModule',[ionicSwiperModule]);
Edit:
I've made some changes since i wrote this answer, so here's a more correct way to use my lib:
<ionic-swiper ng-repeat="i in [1,2,3]"
center-on-disable="{{ true || 'disable default center on disable behavior'}}"
is-swipable="{{ true || 'some prop to watch' }}"
left-swiper="{{:: true || 'or any prop that evaluate to a boolean' }}"
right-swiper="{{:: true || 'or any prop that evaluate to a boolean' }}">
<!-- containerId is available inside this context -->
<!-- Left transclude is optional -->
<left-swiper class="side-item">
Left
</left-swiper>
<!-- Central transclude is required -->
<central-swiper class="central-item">
Central {{:: containerId}}
</central-swiper>
<!-- Right transclude is optional -->
<right-swiper class="side-item">
Right
</right-swiper>
</ionic-swiper>
And here's the original answer usage example:
In HTML (you will need to adjust some css too):
<ionic-list>
<div
swiper-container="true"
class="swiper-container"
ng-repeat="item in [1,2,3,4,5,6]">
<!-- containerId is available inside this context -->
<div class="swiper-wrapper">
<ion-item swiper-slide="center">
This swiper container id is {{:: containerId }}
</ion-item>
<ion-item swiper-slide="right">
Right Button
</ion-item>
<ion-item swiper-slide="left">
Left Button
</ion-item>
</div>
</div>
</ionic-list>
Here's an gif from the demo (i've recorded this in a touchpad, that's why it seems 'sticky')

Having trouble dealing with Moment JS (Angular)

I am having trouble with Moment JS. Basically, I have some metadata for a radio station, and in my php call, I get in return the 'duration' of the song, the 'timestamp' when the song started.
I did some calculation with Moment JS to get the time when the song will be finished, and then I find the difference. However, the difference is returning a negative number, which then breaks the app.
If someone can help me that would be great.
This is my plunk http://plnkr.co/edit/joVLYdTKY5dZBOTNfTOI
Services
angular.module('starter', [])
.run(function(CurrentTrack){
CurrentTrack.refreshTrackData();
})
.controller('radioCtrl', function($scope,CurrentTrack) {
console.log(CurrentTrack);
$scope.CurrentTrack = CurrentTrack;
})
.service('CurrentTrack',function(radioData,$timeout){
var currentTrack = this;
this.setTrackData = function(trackData){
currentTrack.coverUrl = trackData.cover_url;
currentTrack.title = trackData.title;
currentTrack.artist = trackData.artist;
currentTrack.duration = moment.duration(parseInt(trackData.duration));
currentTrack.startedAt = moment.unix(trackData.timestamp);
currentTrack.finishesAt = moment(this.startedAt.add(this.duration));
currentTrack.updateIn = this.finishesAt.diff(moment());
currentTrack.refreshing = false;
return currentTrack.updateIn;
}
this.refreshTrackData = function(){
currentTrack.refreshing = true;
return radioData.refresh()
.then(currentTrack.setTrackData.bind(currentTrack))
.then(currentTrack.scheduleUpdate);
}
this.scheduleUpdate = function(ms){
console.log(ms)
$timeout(function(){
currentTrack.refreshTrackData()
},ms);
return;
}
})
Factory
.factory('radioData', function($http,$timeout) {
var retries = 0;
function parseResponse(response){
retries = 0;
if(!response.data.results){
console.log('no results')
return false;
}
console.log('refreshed...')
return response.data.results[0];
}
function makeRequest(){
console.log('refreshing...')
return $http.get('http://radio-sante-animale.fr/blah11.php? callback=jsonpCallback')
}
function retry(errResponse){
console.error('timed out');
//wait for a sec
retries++;
if(retries > 5){
throw new Error('timed out after 5 attempts!');
}
//oops
return $timeout(makeRequest,1000).then(null,retry);
}
var radioData = {
refresh: function() {
return makeRequest()
.then(null,retry)
.then(parseResponse)
.catch(function(err){
console.log(err);
});
}
};
return radioData;
});
From my side, it worked by adding Math.abs in your $timeout method
this.scheduleUpdate = function(ms) {
console.log( 'update in :' + ms)
$timeout(function() {
currentTrack.refreshTrackData()
}, Math.abs(ms));
return;
}
First I changed:
currentTrack.finishesAt = moment(this.startedAt.add(this.duration));
to:
currentTrack.finishesAt = moment(currentTrack.startedAt).add(currentTrack.duration);
In the original code you are mutating the startedAt time then cloning. I also changed all the this to currentTrack to make it more consistent.
The problem
The bug manifests when there is a difference in the time the server is keeping and the time that the client is keeping, (not because you have anything reversed).
Basically the server sends you trackData.timestamp which you parse and convert into a moment to use as your startedAt time. Then you change the trackData.duration into a moment.duration and add it to the startedAt time to get your finishesAt time.
If the time that the client is keeping is running ahead the server's time, the calculated finishesAt time will be earlier than when the actual song ends. An exaggerated example makes this more clear.
var app = angular.module('app', []);
app.controller('myController', function($scope, $timeout, $interval, Server) {
function updateTime() {
$scope.serverTime = moment().subtract(1,'s');
$scope.clientTime = moment();
}
function refreshInfo() {
Server.getSongInfo().then(parseInfo).then(scheduleRefresh);
};
function parseInfo(trackData) {
$scope.songInfo = trackData;
var startedAt = trackData.timestamp;
$scope.finishesAt = moment(startedAt).add(trackData.duration, 'ms');
$scope.updateIn = $scope.finishesAt.diff(moment());
return $scope.updateIn;
}
function scheduleRefresh(updateIn) {
if (updateIn < 0) {
$scope.bugged = true;
} else {
$scope.bugged = false;
}
$timeout(refreshInfo, updateIn);
}
$scope.songInfo = "loading";
updateTime();
refreshInfo();
$interval(updateTime, 1000);
});
app.service('Server', function($timeout, $interval) {
var song = {
duration: 5000
};
this.getSongInfo = function() {
return $timeout(function() { return this.trackData });
};
function nextSong() {
this.trackData = {
duration: song.duration,
timestamp: moment().subtract(1,'s')
};
}
nextSong();
$interval(nextSong, song.duration);
});
.container {
display: flex;
flex-flow: row wrap;
}
.row {
display: flex;
flex-direction: row;
flex-flow: space-around;
width: 100%;
}
.col {
margin: auto;
text-align: center;
}
.red {
background-color: red;
}
<head>
<script data-require="angular.js#1.4.0-beta.3" data-semver="1.4.0-beta.3" src="https://code.angularjs.org/1.4.0-beta.3/angular.js"></script>
<script data-require="moment.js#2.8.3" data-semver="2.8.3" src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.3/moment.min.js"></script>
</head>
<div ng-app='app' ng-controller='myController'>
<div class="container">
<div class='row'>
<div class='col' ng-class='{red:bugged}'>
<h3>Song Info</h3>
<div>{{ songInfo }}</div>
</div>
</div>
<div class="row">
<div class='col'>
<h3>Server</h3>
<div>{{ serverTime.format("hh:mm:ss") }}</div>
</div>
<div class='col'>
<h3>Finishes At</h3>
<div>{{ finishesAt.format("hh:mm:ss") }}</div>
</div>
<div class='col'>
<h3>Update In</h3>
<div>{{ updateIn }}</div>
</div>
<div class='col'>
<h3>Client</h3>
<div>{{ clientTime.format("hh:mm:ss") }}</div>
</div>
</div>
</div>
</div>
Whenever the song info div is red, the client is in the loop where it is requesting new track info and parsing out a finish time that is earlier than the current moment, which immediately makes it request new track info.
angular.module('starter', [])
.run(function(CurrentTrack) {
CurrentTrack.refreshTrackData();
})
.controller('radioCtrl', function($scope, CurrentTrack) {
$scope.CurrentTrack = CurrentTrack;
})
.service('CurrentTrack', function(radioData, $timeout) {
var currentTrack = this;
this.setTrackData = function(trackData) {
currentTrack.coverUrl = trackData.cover_url;
currentTrack.title = trackData.title;
currentTrack.artist = trackData.artist;
currentTrack.duration = moment.duration(parseInt(trackData.duration));
currentTrack.startedAt = moment.unix(trackData.timestamp);
currentTrack.finishesAt = moment(currentTrack.startedAt).add(currentTrack.duration);
currentTrack.updateIn = currentTrack.finishesAt.diff(moment())
console.log(currentTrack, trackData);
currentTrack.refreshing = false;
return currentTrack.updateIn;
}
this.refreshTrackData = function() {
currentTrack.refreshing = true;
return radioData.refresh()
.then(currentTrack.setTrackData)
.then(currentTrack.scheduleUpdate);
}
this.scheduleUpdate = function(ms) {
console.log("update in " + ms)
$timeout(function() {
currentTrack.refreshTrackData()
}, ms);
return;
}
})
.factory('radioData', function($http, $timeout) {
var retries = 0;
function parseResponse(response) {
retries = 0;
if (!response.data.results) {
console.log('no results')
return false;
}
console.log('response parsed.')
return response.data.results[0];
}
function makeRequest() {
console.log('making request.')
return $http.get('http://radio-sante-animale.fr/blah11.php?callback=jsonpCallback')
}
function retry(errResponse) {
console.error('timed out');
//wait for a sec
retries++;
if (retries > 5) {
throw new Error('timed out after 5 attempts!');
}
//oops
return $timeout(makeRequest, 1000).then(parseResponse, retry);
}
var radioData = {
refresh: function() {
return makeRequest()
.then(parseResponse, retry)
.catch(function(err) {
console.log(err);
});
}
};
return radioData;
});
<!DOCTYPE html>
<html ng-app="starter">
<head>
<script data-require="angular.js#1.4.0-beta.3" data-semver="1.4.0-beta.3" src="https://code.angularjs.org/1.4.0-beta.3/angular.js"></script>
<script data-require="moment.js#2.8.3" data-semver="2.8.3" src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.3/moment.min.js"></script>
</head>
<body ng-controller="radioCtrl">
<h3>Current Music Information</h3>
<p ng-show="CurrentTrack.refreshing">refreshing...</p>
<img style="width:300px;height:300px;" src="{{CurrentTrack.coverUrl}}" alt="">
<p>
Title : {{ CurrentTrack.title }}
<br />Artist : {{ CurrentTrack.artist }}
<br />The song started at {{ CurrentTrack.startedAt }}
<br />Duration of the song {{ CurrentTrack.duration.asSeconds() }} seconds
<br />Finishes At {{ CurrentTrack.finishesAt }}
<br />Update In {{ CurrentTrack.updateIn }}
<br />
</body>
</html>
I'm actually not sure the best approach to fixing this problem but hopefully some one has a better answer.
A hack solution is to add more time (some acceptable amount of error) to the finishesAt to give a little leeway.
currentTrack.finishesAt = moment(currentTrack.startedAt).add(currentTrack.duration).add(1, 's');
From http://momentjs.com/docs/#/displaying/difference/
If the moment is earlier than the moment you are passing to
moment.fn.diff, the return value will be negative.
You need to reverse the dates in your diff.
EDIT: Like this -
currentTrack.updateIn = moment().diff(this.finishesAt);

AngularJS : Two-way binding between a textarea and ng-repeat-ed inputs

I was going to ask this as a question, but I figured out a solution. So at this point, I'm looking for a critique of my solution.
I've got a static textarea, and an input with an ng-repeat directive.
As the user types a sentence into the textarea, a input is rendered for each word in the sentence.
Then if the user updates the text in any input, the corresponding word in the textarea sentence is updated (really the whole sentence is recreated).
Demo: http://plnkr.co/edit/bSjtOK?p=preview
Questions
Keeping in mind that I'm only 2 weeks into my AngularJS learning:
Did I write this in the "angular" way?
Is there something I could have done better?
Am I violating any no-nos?
Abbreviated Code
HTML
<textarea ng-model="sentence" ng-change="parseSentence()" style="width: 100%; height: 15em;"></textarea>
<input type="text" ng-repeat="w in words" ng-model="w.word" ng-change="buildSentance(w)" />
JavaScript
function WordCtrl($scope, debounce) {
$scope.words = [];
$scope.sentence = 'Hello there how are you today?';
// this is called when the textarea is changed
// it splits up the textarea's text and updates $scope.words
$scope.parseSentence = function() {
var words = $scope.sentence.split(/\s+/g);
var wordObjects = [];
for (var i=0;i<words.length;i++) {
wordObjects.push({word: words[i]});
}
if ((words.length == 1) && (words[0] === '')) {
$scope.words = [];
} else {
$scope.words = wordObjects;
}
};
$scope.parseSentenceDebounced = debounce($scope.parseSentence, 1000, false);
$scope.buildSentance = function(w) {
var words = [];
for (var i=0;i<$scope.words.length;i++) {
var word = $scope.words[i].word;
if (word.replace(/\s+/g,'') !== '') {
words.push(word);
}
}
$scope.sentence = words.join(' ');
// if the user puts a space in the input
// call parseSentence() to update $scope.words
if (w.word.indexOf(' ') > -1) {
$scope.parseSentenceDebounced();
}
}
$scope.parseSentence();
}
Interesting issue you are having. I put your code on my page and the first thing I noticed is that you cannot pass debounce in the controller method.
Next Problem I noticed is that you have an ng-change that changes the values on another box with ng-change. I changed the event to Keypress to stop the digest in a digest.
Here it is working in JSFiddle enter link description here
The code:
HTML
<body ng-app="portal">
<div ng-controller="WordCtrl">
<textarea ng-model="sentence" ng-keypress="parseSentence()" style="width: 100%; height: 15em;"></textarea>
<input type="text" ng-repeat="w in words" ng-model="w.word" ng-keypress="buildSentance(w)" />
</div>
</body>
Javascript
angular.module("portal",[]).controller("WordCtrl",function($scope) {
$scope.words = [];
$scope.sentence = 'Hello there how are you today?';
$scope.parseSentence = function () {
var words = $scope.sentence.split(/\s+/g);
var wordObjects = [];
for (var i = 0; i < words.length; i++) {
wordObjects.push({ word: words[i] });
}
if ((words.length == 1) && (words[0] === ''))
{
$scope.words = [];
}
else
{
$scope.words = angular.copy(wordObjects);
}
}
$scope.buildSentance = function (w) {
var words = [];
for (var i = 0; i < $scope.words.length; i++) {
var word = $scope.words[i].word;
if (word.replace(/\s+/g, '') !== '') {
words.push(word);
}
}
$scope.sentence = words.join(' ');
// if the user puts a space in the input
// call parseSentence() to update $scope.words
if (w.word.indexOf(' ') > -1) {
$scope.parseSentenceDebounced();
}
}
$scope.parseSentence();
});
Hope this solves your issue.

Resources