app = angular.module "plunker", [
  "gg.directives.borderLayout"
]

app.controller 'MainCtrl', ($scope) ->
  $scope.name = 'World'
<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <link data-require="bootstrap-css@*" data-semver="2.3.2" rel="stylesheet" href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" />
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="borderLayout.css" />
    <link rel="stylesheet" href="style.css" />
    <script data-require="angular.js@1.1.x" src="http://code.angularjs.org/1.1.5/angular.min.js" data-semver="1.1.5"></script>
    <script data-require="ui-bootstrap@0.4.0" data-semver="0.4.0" src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.4.0.min.js"></script>
    <script src="borderLayout.js"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="MainCtrl">
    <border-layout>
      <pane anchor="north" min="41px" max="41px">
        <div class="navbar navbar-fixed-top navbar-inverse">
          <div class="navbar-inner">
            <div class="container">
              <a class="brand" href="">border-layout</a>
              <p>North pane</p>
            </div>
          </div>
        </div>
      </pane>
      
      <pane anchor="west" target="20%" min="10%" max="30%">
        <div layout-handle class="right"></div>
        <p>I am the west pane. I have a minimum size of 20% and a maximum size of 40%.</p>
        <p>I have a handle that is absolutely positioned as the right-most 4px.</p>
        <p>For fun, the label below is also a drag handle.</p>
        <span class="label" layout-handle>DRAG ME</span>
      </pane>
      
      <center>
        <border-layout>
          <pane anchor="south" min="20%" max="40%">
            <div layout-handle class="top"></div>
            <p>I am the south pane of the layout nested in the main layout'c center pane.
            I have a minimum size of 20% and a maximum size of 40%.</p>
            <p>For fun, the label below is also a drag handle.</p>
            <span class="label" layout-handle>DRAG ME</span>
          </pane>
          
          <center ng-include="'README.html'"></center>
        </border-layout>

      </center>
    </border-layout>
  </body>

</html>

.border-layout-pane.north {
  @media (max-width: 767px) {
    padding: 0 20px;
  }
  
  .navbar-fixed-top {
    margin-bottom: 0;
  }
}

.border-layout-pane.west {
  padding: 10px;
  
  padding-right: 14px;
  
  
  .border-layout-handle.right {
    position: absolute;
    top: 0; right: 0; bottom: 0;
    width: 4px;
    background-color: #333;
    
    &.handle-moving {
      background-color: #393;
    }
    &.handle-constrained {
      background-color: #933;
    }
    
  }
  .border-layout-handle {
    cursor: ew-resize;
  }
}

.border-layout-pane.center {
  padding: 20px;
}
.border-layout-pane.south {
  border-top: 4px solid blue;
  padding: 20px;
}
module = angular.module "gg.directives.borderLayout", [
]

calculateSize = (target, total) ->
  target ||= 0
  
  if angular.isNumber(target)
    if target >= 1 then return Math.round(target)
    if target >= 0 then return Math.round(target * total)
    
    return 0
    
  if matches = target.match /^(\d+)px$/ then return parseInt(matches[1], 10)
  if matches = target.match /^(\d+)%$/ then return Math.round(total * parseInt(matches[1], 10) / 100)
  
  throw new Error("Unsupported size: #{target}")


throttle = (delay, fn) ->
  throttled = false
  ->
    return if throttled
    
    throttled = true
    setTimeout ->
      throttled = false
    , delay
    
    fn.call(@, arguments...)




module.directive "pane", ->
  restrict: "E"
  replace: true
  require: ["pane", "^borderLayout"]
  transclude: true
  scope:
    anchor: "@"
    target: "@"
    min: "@"
    max: "@"
  template: """
    <div class="border-layout-pane" ng-class="anchor" ng-style="style" ng-transclude>
      <div class="border-layout-overlay"></div>
    </div>
  """
  controller: class Pane
    @inject = ["$scope", "$element"]
    constructor: ($scope, $element) ->
      @scope = $scope
      @scope.style = {}
      
      @el = $element[0]
    
    getElementSize: ->
      if @scope.anchor in ["north", "south"] then return @el.offsetHeight
      else if @scope.anchor in ["west", "east"] then return @el.offsetWidth
      else throw new Error("Unsupported anchor: #{@scope.anchor}")
      
    setSize: (size, style = {}) ->
      @scope.size = size
      
      if @scope.anchor in ["north", "south"] then style.height = "#{size}px"
      else if @scope.anchor in ["west", "east"] then style.width = "#{size}px"
      else throw new Error("Unsupported anchor: #{@scope.anchor}")
      
      angular.copy style, @scope.style
      
      # Return the size being set
      size
      
    calculateSize: (avail, total, style = {}) ->
      target = @scope.size || @scope.target || @getElementSize() || 0
      max = @scope.max || Number.MAX_VALUE
      min = @scope.min || 0
      
      size = calculateSize(target, total)
      size = Math.min(size, calculateSize(max, total))
      size = Math.max(size, calculateSize(min, total))
      size = Math.min(size, avail)
      
      @setSize(size, style)
      
  link: ($scope, $el, attrs, [ctrl, layout]) ->
    layout.setBorder $scope.anchor, ctrl





module.directive "center", ->
  restrict: "E"
  replace: true
  require: ["center", "^borderLayout"]
  transclude: true
  template: """
    <div class="border-layout-pane center" ng-style="style" ng-transclude>
    </div>
  """
  controller: class Center
    @inject = ["$scope"]
    constructor: ($scope) ->
      @scope = $scope
    
    setRect: (rect) ->
      @scope.style =
        left: "#{rect.left || 0}px"
        top: "#{rect.top || 0}px"
        right: "#{rect.right || 0}px"
        bottom: "#{rect.bottom || 0}px"
      
  link: ($scope, $el, attrs, [ctrl, layout]) ->
    layout.setCenter ctrl




module.directive "layoutHandle", [ "$window", ($window) ->
  restrict: "A"
  require: ["^pane", "^borderLayout"]
  link: ($scope, $el, attrs, [border, layout]) ->
    $el.addClass("border-layout-handle")
    
    layout.registerHandle($el, border)
]

module.directive "borderLayout", [ "$window", ($window) ->
  stylesAdded = false
  stylesheet = """
  """
  
  restrict: "E"
  replace: true
  require: "borderLayout"
  transclude: true
  template: """
    <div class="border-layout" ng-class="{masked: masked, moving: moving, constrained: constrained}" ng-transclude>
    </div>
  """
  
  controller: class BorderLayout
    @inject = ["$scope", "$element"]
    constructor: ($scope, $element) ->
      @el = $element[0]
      @scope = $scope
    
    setCenter: (center) ->
      throw new Error("Center already assigned") if @center
      
      @center = center
      @
    
    setBorder: (border, pane) ->
      throw new Error("Border already assigned: #{border}") if @[border]
      
      @[border] = pane
      @
    
    registerHandle: ($el, pane) ->
      layout = @
      el = $el[0]
      
      el.addEventListener "mousedown", (e) ->
        if pane.scope.anchor in ["north", "south"] then coord = "y"
        else if pane.scope.anchor in ["west", "east"] then coord = "x"
  
        if pane.scope.anchor in ["north", "west"] then scale = 1
        else if pane.scope.anchor in ["south", "east"] then scale = -1
      
        startPos = {x: e.x, y: e.y}
        startCoord = e[coord]
        startSize = pane.scope.size
        startTime = Date.now()

        $el.addClass("handle-active")
        
        # Not sure if this really adds value, but added for compatibility
        el.unselectable = "on"
        el.onselectstart = -> false
        el.style.userSelect = el.style.MozUserSelect = "none"
        
        # Null out the event to re-use e and prevent memory leaks
        #e.setCapture()
        e.preventDefault()
        e.defaultPrevented = true
        e = null
        
        layout.scope.$apply ->
          layout.scope.masked = true
        
        handleMouseMove = (e) ->
          $el.addClass("handle-moving")
        
          # Inside Angular's digest, determine the ideal size of the element
          # according to movements then determine if those movements have been
          # constrained by boundaries, other panes or min/max clauses
          layout.scope.$apply ->
            pane.scope.size = targetSize = startSize + scale * (e[coord] - startCoord)

            layout.reflow()
            
            layout.scope.constrained = targetSize != pane.scope.size
            layout.scope.moving = true
            
            if layout.scope.constrained then $el.addClass("handle-constrained")
            else $el.removeClass("handle-constrained")


          # Null out the event in case of memory leaks
          #e.setCapture()
          e.preventDefault()
          e.defaultPrevented = true
          e = null
          
        handleMouseUp = (e) ->
          # In case the mouse is released at the end of a throttle period
          handleMouseMove(e)
          
          layout.scope.$apply ->
            layout.scope.masked = false
            layout.scope.moving = false
            layout.scope.constrained = false
        
          
          $el.removeClass("handle-active")
          $el.removeClass("handle-moving")
          $el.removeClass("handle-constrained")
          
          $window.removeEventListener "mousemove", handleMouseMoveThrottled, true
          $window.removeEventListener "mouseup", handleMouseUp, true

          # Null out the event in case of memory leaks
          #e.releaseCapture()
          e.preventDefault()
          e.defaultPrevented = true
          e = null
        
        # Prevent the reflow logic from happening too often
        handleMouseMoveThrottled = throttle(10, handleMouseMove)
      
        $window.addEventListener "mousemove", handleMouseMoveThrottled, true
        $window.addEventListener "mouseup", handleMouseUp, true

    reflow: ->
      
      width = @el.offsetWidth
      height = @el.offsetHeight
      
      rect =
        left: 0
        top: 0
        bottom: 0
        right: 0
      
      
      if @north then rect.top += @north.calculateSize(height - rect.top - rect.bottom, height)
      if @south then rect.bottom += @south.calculateSize(height - rect.top - rect.bottom, height)
      
      if @west then rect.left += @west.calculateSize(width - rect.left - rect.right, width, { top: "#{rect.top}px", bottom: "#{rect.bottom}px" })
      if @east then rect.right += @east.calculateSize(width - rect.left - rect.right, width, { top: "#{rect.top}px", bottom: "#{rect.bottom}px" })
      
      @center.setRect(rect)
      
      @scope.$broadcast "reflow"
    
  link: ($scope, $el, attr, ctrl) ->

    ctrl.reflow()
    
    $window.addEventListener "resize", ->
      #console.log "Resize event", arguments...
      $scope.$apply(ctrl.reflow.bind(ctrl))
]
# Border layout directive

The border layout directive allows its users to create dynamic layouts by
attaching elements to the borders of an element.

### Understanding the border layout

In the border layout world, there are four possible border panes (north, east, south and west) and a center pane.

![The five panes of the border layout](http://upload.wikimedia.org/wikipedia/commons/thumb/a/a3/Java-LayoutManager-BorderLayout.svg/400px-Java-LayoutManager-BorderLayout.svg.png)

The north and south border panes take priority and will extend the full width of the layout's container.

The center pane is required and will occupy the space remaining after determining the size of all of the border panes.

### Nesting

The border layout directive can be nested as many times as you want.

### Usage

```html
<border-layout>
  <pane anchor="north" min="10px" target="10%" max="40px">
    Anything can go here and will be transcluded into the pane
    
    You can also add elements that have a <code>layout-handle</code> attribute
    that will allow the pane to be resized within the bounds of the pane's
    optional min and max constraints.
  </pane>
  
  <center>
    The center pane is required and will occupy the space remaining after
    allocating the available space to all of the borders.
  </center>
</border-layout>
```

#### Styles

Some css styles are required for the border-layout to function properly. See
`borderLayout.less`.
.border-layout {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  
  .border-layout-pane {
    position: absolute;
    box-sizing: border-box;
    overflow: auto;
    
  }

  .border-layout-overlay {
    z-index: 999;
    display: none;
    position: absolute;
    top: 0; left: 0; right: 0; bottom: 0;
  }
  
  &.masked .border-layout-overlay {
    display: block;
    background-color: transparent;
  }

  .border-layout-pane.north {
    top: 0; left: 0; right: 0;
  }
  .border-layout-pane.south {
    left: 0; right: 0; bottom: 0;
  }
  
  .border-layout-pane.west {
    top: 0; left: 0; bottom: 0;
  }
  .border-layout-pane.east {
    top: 0; right: 0; bottom: 0;
  }
  
  .border-layout-pane.center {
    top: 0; left: 0; right: 0; bottom: 0;
  }
}