Mobile optimized web development (モバイル向けウェブ開発)

#Contents
1. Touch effect(タッチ効果)
2. Touch up inside(iOSぽいタッチ処理)
3. JS Load more(JSで無限スクロール読み込む)
4. Multiple background images
5. CSS image preloading
6. CSS interactive animations
7. Modals and popups
8. Rails AJAX integration

##1. Touch effect(タッチ効果)
We want to add some animations on buttons on user interaction.
ボタンを押したときアニメーション効果をいれたいです。
###1.1 Basic approach(基本実装)
```html:index.html
<a href="#" id="button">
  Button
</a>
```
```js:javascript.js
$(function() {

  $("#button")
  .css({
      "transition": "all 0.15s linear",
  })
  .on("mousedown",function() {
    $(this).css({
      "background-color": "red",
    });
  })
  .on("mouseup",function() {
    $(this).css({
      "background-color": "transparent",
    });
  })
});
```

###1.2 Better approach(改善)
```html:index.html
<a href="#" cam-touch-effect>
  Button
</a>
<a href="#" cam-touch-effect>
  Button 2
</a>
```
```js:javascript.js
$(function() {
  $("[cam-touch-effect]").each(function(){
    $(this)
    .css({
        "transition": "all 0.15s linear",
    })
    .on("mousedown",function() {
      $(this).css({
        "background-color": "red",
      });
    })
    .on("mouseup",function() {
      $(this).css({
        "background-color": "transparent",
      });
    })
  });
});
```
Now we can reuse the effect on other buttons.

###1.3 Better than better approach(改善の改善)
```html:index.html
<a href="#" class=btn cam-touch-effect>
  Button
</a>
<a href="#" class="btn btn-custom" cam-touch-effect>
  Button 2
</a>
```
```js:javascript.js
$(function() {
  $("[cam-touch-effect]").each(function(){
    $(this)
    .on("touchstart",function() {
      $(this).addClass("touch");
    })
    .on("touchend touchcancel",function() {
      $(this).removeClass("touch");
    })
  });
});
```
```css:style.scss
.btn {
  transition: -webkit-transform 0.15s linear;
  & > .touch {
    -webkit-transform: scale(1.05);
  }
}

.btn-custom {
  transition: background-color 0.15s linear;
  & > .touch {
    background-color: rgba(0,0,0,0.2);
  }
}
```
In 1.1 and 1.2, view and controller logic are mixed together.
1.1と1.2にはビューとコントローラーがまぜています。

1.3 separates view from controller.
1.3にビューとコントローラーが完全に分かれています。

**1.3 is Very reusasble! (再利用可能性が高い)**

##2. Touch up inside

In iOS, when you tap a button, onTouchupInside is used to process touch.
It is not onClick, onTouchBegan or onTouchEnd.

### Why not use default onclick event?

onclick event has a slower UI response. When you click an element with onclick callback, it takes a few hundred milliseconds before the browser finally recognizes the event and make changes accordingly. It also comes with inconveniences such as when you leave the button, it is still considered a click and follows the link.

### onTouchupInside in Javascript

Javascript in modern browsers comes equipped with touch events. touchbegin, touchmove, touchend, touchcancel are available. None of them translates into onTouchupInside individually.

onTouchupInside is in a simple gesture composed of touch began and touch end.
Just checking touch began and touch end is not enough unfortunately.
To make a good touch up inside event, it is necessary to determine if touch end happened inside the button area.

Further optimizations include allowing a larger area (about 10*10px larger than the button area) for touch end to provide a better UX.
Also, having the gesture cancel on page scroll is also a good idea so that when user touches the button and scrolls the page, it won't be turned into a touchup event.

```js:javascript.js
$(function() {
  var shared = shared || {debug:false};

  /** Detect touch device */
  shared.isTouchDevice = (typeof window.ontouchstart) !== 'undefined';
  shared.screenWidth = $(window).width();;
  shared.touchSensitivity = shared.screenWidth * 0.2;

  jQuery.fn.extend({
    /* on touchup inside event which is faster than onclick
     * ================*/
    onTouchupInside: function (callback) {
      $(this).each(function () {
        var $this = $(this);

        var clearCallbacks = function () {
          var touchStart = $this.data("touchstart");
          var touchMove = $this.data("touchmove");
          var touchEnd = $this.data("touchend");
          if (touchStart) {
            $this.off("touchstart mousedown", touchStart);
          }
          if (touchMove) {
            $this.off("touchmove mousemove", touchMove);
          }
          if (touchEnd) {
            $this.off("touchend mouseup", touchEnd);
          }
        };

        if (!callback) {
          clearCallbacks();
        } else {
          var startPos = {x:0,y:0};
          var totalMovement = {x:0,y:0};

          var getEvent = function(e) {
            var event = e.originalEvent;
            if (e.originalEvent instanceof MouseEvent) {
            } else {
              if (event.changedTouches) {
                event = event.changedTouches[0];
              } else {
                event = event.touches[0];
              }
            }
            return event;
          };

          // Touchstart event
          var touchStart = function (e) {
            totalMovement = {x:0,y:0};
            var event = getEvent(e);
            startPos.x = event.clientX;
            startPos.y = event.clientY;
          };

          var touchMove = function (e) {
            var event = getEvent(e);
            totalMovement.x+=Math.abs(event.clientX - startPos.x);
            totalMovement.y+=Math.abs(event.clientY - startPos.y);
          };

          // Touchend event
          var touchEnd = function (e) {
            var $this = $(this);

            var event = getEvent(e);
            var elem = document.elementFromPoint(event.pageX, event.pageY);
            var $pos = $this.offset();
            var $size = {width:$this.width(), height: $this.height()};
            var bounds = {minX: $pos.left-10, minY: $pos.top-10, maxX: $pos.left+$this.outerWidth()+10, maxY: $pos.top+$this.outerHeight()+10};
            var tPos = {x: event.pageX, y: event.pageY};

            if (tPos.x >= bounds.minX && tPos.x <= bounds.maxX && tPos.y >= bounds.minY && tPos.y <= bounds.maxY) {
              if (totalMovement.x < shared.touchSensitivity && totalMovement.y < shared.touchSensitivity) {
                callback.apply(this, [e]);
              }
            }
            e.stopPropagation();
            return false;
          };

          // Event binding
          clearCallbacks();
          if (shared.isTouchDevice) {
            $this.on("touchstart", touchStart);
            $this.on("touchmove", touchMove);
            $this.on("touchend", touchEnd);
          } else {
            $this.on("mousedown", touchStart);
            $this.on("mousemove", touchMove);
            $this.on("mouseup", touchEnd);
          }
          $this.data("touchstart", touchStart);
          $this.data("touchmove", touchMove);
          $this.data("touchend", touchEnd);
        } 
      });
    },
  });
});
```
### Usage

```index.html
<div id="mybutton" class="btn btn-lg">
  Click me!
</div>
```

```app.js
$(function() {
  // Setup
  $("#mybutton").onTouchupInside(function(){
    alert("I'm clicked!");
  });
  // Un-setup
  $("#mybutton").onTouchupInside(null);
});
```

##3. JS Load more(JSで無限スクロール読み込む)
![loading-wheel-black.png](https://qiita-image-store.s3.amazonaws.com/0/73843/a8f934e0-c439-8767-85c1-f413be111fab.png)
![loading-wheel-white.png](https://qiita-image-store.s3.amazonaws.com/0/73843/0dde38e7-a5e8-5d96-3e19-fbadf54582a8.png)

We want to make an infinite scroll pagination system with loading wheel at the end of the page. When the loading wheel appears into the view, a new AJAX request is sent to server to load next page.

###Controller
In Rails controller, we define index method.

```controller.rb
def index
  @page = params[:page] || 1
  @items = Item.paginate({
  	page: @page,
  	per_page: 10,
  }).all
end
```

###Index
"index" method has two views for "html" and "js" formats.

```index.html.erb
<div class=items id=items>
  <% if @items.any? %>
    <%= render partial: "items" %>
  <% else %>
    <center>
      表示する案件がまだありません。
    </center>
  <% end %>
</div>

<% if @items.any? %>
  <! load-more button >
  <div cam-load-more></div>
<% end %>
```
```index.js.erb
<% if @items.any?%>
  <% if @page == 1 %>
    $("#items").html("");
  <% end %>
  $("#items").append("<%=escape_javascript(render(partial: "items"))%>");
<% end %>
```

Implement _items partial.

```_items.html.erb
<% @items.each do |o| %>
  <div class=item>
  	o.name
  </div>
<% end %>
```


###Javascript
Implementation of cam-load-more

```directives.js
/* Check if scrolling has reached the target element */
var isScrolledIntoView = function(elem) {
  var docViewTop = $(window).scrollTop();
  var docViewBottom = docViewTop + $(window).height();
  var elemTop = $(elem).offset().top;
  return ((elemTop <= docViewBottom) && (elemTop >= docViewTop));
};
  
  
/* cam-load-more
 * =============
 * Load more button directive for web pagination.
 * Load page from the current url location.href
 * Attaches page parameter to the url.
 **/
$("[cam-load-more]").each(function() {
  var page = 1;
  var self = this;
  var $this = $(this);
  var updating = false;
  var params = {};
  var url = $this.attr("cam-load-more") || location.href;
  // Attrを外す。
  $this.attr("cam-load-more", null);
  
  var updatePage = function () {
    // Load items only when the loading spinner is visible
    if (!isScrolledIntoView(self)) return;

    if (updating) return;

    updating = true;

    page++;

    params["page"] = page

    $.get(url + ".js")
    .success(function (res) {
      if (res.trim().length <= 0) {
        // duration, onComplete
        $this.fadeOut(150,function(){
          $this.remove();
        });
      }
      updating = false;
    })
    .error(function (res) {
      updating = false;
    })
  };

  // Call once at the beginning to check if it is already in screen
  updatePage();

  // Add scroll event listener
  $(window).on("scroll",updatePage);
});
```

#To be continued