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 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;
}
}