app = angular.module "ped", [ "ped.session" "ped.editor" "ped.borderLayout" "ped.previewer" ] app.controller 'MainCtrl', ["$scope", "session", ($scope, session) -> $scope.session = session session.reset files: [ filename: "index.html" content: """ <!doctype html> <html ng-app="plunker" > <head> <meta charset="utf-8"> <title>AngularJS Plunker</title> <script>document.write('<base href="' + document.location + '" />');</script> <link rel="stylesheet" href="style.css"> <script data-require="angular.js@1.1.x" src="http://code.angularjs.org/1.1.4/angular.js"></script> <script src="app.js"></script> </head> <body ng-controller="MainCtrl"> <p>Hello {{name}}!</p> </body> </html> """ , filename: "app.js" content: """ var app = angular.module('plunker', []); app.controller('MainCtrl', function($scope) { $scope.name = 'World'; }); """ , filename: "style.css" content: """ p { color: red; } """ ] $scope.addFile = -> if filename = prompt("Filename?") session.addBuffer(filename) ]

<!DOCTYPE html> <html ng-app="ped"> <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.min.css" /> <script>document.write('<base href="' + document.location + '" />');</script> <link rel="stylesheet" href="style.css" /> <script data-require="angular.js@1.1.x" src="http://code.angularjs.org/1.1.5/angular.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 data-require="ui-router@0.0.1" data-semver="0.0.1" src="http://angular-ui.github.io/ui-router/release/angular-ui-router.min.js"></script> <script data-require="ace@0.2.0" data-semver="0.2.0" src="http://ace.ajax.org/build/src/ace.js"></script> <script src="filer.js"></script> <script src="editor.js"></script> <script src="borderLayout.js"></script> <script src="previewer.js"></script> <script src="session.js"></script> <script src="app.js"></script> </head> <body ng-controller="MainCtrl"> <border-layout> <border class="sidebar" anchor="west" target="250px" min="200px" max="300px"> <ul class="nav nav-list"> <li ng-class="{active: buffer==session.active}" ng-repeat="buffer in session.buffers track by buffer.filename"> <a ng-click="session.active=buffer" ng-bind="buffer.filename"></a> </li> <li> <a ng-click="addFile()">Add file...</a> </li> </ul> <handle></handle> </border> <border class="preview" anchor="east" target="30%"> <previewer session="session"></previewer> <handle></handle> </border> <center> <editor session="session"></editor> </center> </border-layout> </body> </html>

/* Put your css in here */ .editor { position: absolute; top: 0; left: 0; right: 0; bottom: 0; .editor-canvas { position: absolute; top: 0; left: 0; right: 0; bottom: 0; } } .border-layout { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: black; .pane { position: absolute; box-sizing: border-box; overflow: auto; background-color: white; .overlay { z-index: 999; display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0; } &.moving .overlay { background-color: transparent; display: block; } } .pane.north { top: 0; left: 0; right: 0; } .pane.south { left: 0; right: 0; bottom: 0; } .pane.west { top: 0; left: 0; bottom: 0; } .pane.east { top: 0; right: 0; bottom: 0; } .pane.center { top: 0; left: 0; right: 0; bottom: 0; } } .pane.sidebar { padding-right: 8px; .nav-list a { cursor: pointer; } .handle { position: absolute; top: 0; right: 0; bottom: 0; width: 8px; background-color: #666; cursor: ew-resize; &.moving { background-color: #AAA; } &.constrained { background-color: #966; } } } .pane.preview { padding-left: 8px; .handle { position: absolute; top: 0; left: 0; bottom: 0; width: 8px; background-color: #666; cursor: ew-resize; &.moving { background-color: #AAA; } &.constrained { background-color: #966; } } } .pane.navbar { margin: 0; } .moving h1 { color: red; }

module = angular.module "ped.session", [ ] module.service "session", class Session class Buffer constructor: (@filename, @content = "") -> #console.log "Buffer::constructor", arguments... rename: (filename) -> @filename = filename @ constructor: -> #console.log "Session::constructor", arguments... @active = new Buffer("index.html") @buffers = [@active] reset: (json = {}) -> @buffers.length = 0 @buffers.push new Buffer(file.filename, file.content) for file in json.files @buffers.push new Buffer("index.html") unless @buffers.length unless @active = @getBufferByFilename("index.html") @active = @buffers[0] getBufferByFilename: (filename) -> return buffer for buffer in @buffers when buffer.filename is filename add: (filename, content = "") -> filename ||= prompt("Filename?") unless filename throw new Error("Invalid filename") if buffer = @getBufferByFilename(filename) throw new Error("Buffer already exists") @buffers.push new Buffer(filename, content) @ remove: (filename) -> unless filename throw new Error("Invalid filename") unless buffer = @getBufferByFilename(filename) throw new Error("Buffer does not exist") @buffers.splice @buffers.indexOf(buffer), 1 @

module = angular.module "ped.editor", [ ] module.directive "editorBuffer", [ "$rootScope", ($rootScope) -> EditSession = require("ace/edit_session").EditSession UndoManager = require("ace/undomanager").UndoManager Range = require("ace/range").Range restrict: "E" replace: true require: ["^editor", "ngModel", "editorBuffer"] scope: buffer: "=" template: """ <div class="editor-buffer"></div> """ controller: class EditorBuffer @inject = ["$scope"] constructor: ($scope) -> #console.log "EditorBuffer::constructor", arguments... @buffer = $scope.buffer @aceSession = new EditSession(@buffer.content or "") @aceSession.setUndoManager(new UndoManager()) @aceSession.setTabSize(2) @aceSession.setUseWorker(true) $scope.$watch "buffer.filename", => @aceSession.setMode(@guessMode()) syncWith: ($scope, model) -> session = @aceSession model.$render = -> session.setValue(model.$modelValue) updateViewValue = (value) -> model.$setViewValue(value) session.on "change", -> if model.$viewValue != (value = session.getValue()) if $rootScope.$$phase then updateViewValue(value) else $scope.$apply -> updateViewValue(value) $scope.$on "$destroy", -> editor.removeListener "change", updateViewValue guessMode: -> "ace/mode/" + if /\.js$/.test(@buffer.filename) then "javascript" else if /\.html$/.test(@buffer.filename) then "html" else if /\.css$/.test(@buffer.filename) then "css" else "text" link: ($scope, $el, attrs, [editor, model, ctrl]) -> #console.log "editor-buffer::link", arguments... editor.attachBuffer(ctrl) ctrl.syncWith($scope, model) ] module.directive "editorCanvas", [ () -> restrict: "E" replace: true require: "^editor" template: """ <div class="editor-canvas"></div> """ link: ($scope, $el, attrs, editor) -> #console.log "editor-canvas::link", arguments... editor.setCanvas($el[0]) ] module.directive "editor", -> AceEditor = require("ace/editor").Editor Renderer = require("ace/virtual_renderer").VirtualRenderer restrict: "E" replace: true require: "editor" scope: session: "=" template: """ <div class="editor"> <editor-buffer buffer="buffer" ng-model="buffer.content" ng-repeat="buffer in session.buffers"></editor-buffer> <editor-canvas></editor-canvas> </div> """ controller: class Editor constructor: () -> #console.log "Editor::constructor", arguments... @controllers = [] setCanvas: (el) -> @ace = new AceEditor(new Renderer(el, "ace/theme/textmate")) attachBuffer: (controller) -> @controllers.push(controller) link: ($scope, $el, attrs, ctrl) -> #console.log "editor::link", arguments... #console.log "Editor", ctrl.ace $scope.$watch "session.active", (active) -> for controller in ctrl.controllers when controller.buffer == active ctrl.ace.setSession(controller.aceSession) #console.log "Activating", controller $scope.$on "reflow", -> ctrl.ace.resize()

module = angular.module "ped.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 "border", -> restrict: "E" replace: true require: ["border", "^borderLayout"] transclude: true scope: anchor: "@" target: "@" min: "@" max: "@" template: """ <div class="pane {{anchor}}" ng-class="{moving: moving, constrained: constrained}" ng-style="style" ng-transclude> <div class="overlay"></div> </div> """ controller: class Border @inject = ["$scope", "$element"] constructor: ($scope, $element) -> #console.log "Border::constructor", arguments... $scope.style = {} @scope = $scope @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 = {}) -> #console.log "Border::setSize", arguments... @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 size calculateSize: (avail, total, style = {}) -> #console.log "calculateSize", arguments... 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]) -> #console.log "border::link", arguments... layout.setBorder $scope.anchor, ctrl module.directive "center", -> restrict: "E" replace: true require: ["center", "^borderLayout"] transclude: true template: """ <div class="pane center" ng-style="style" ng-transclude> </div> """ controller: class Center @inject = ["$scope"] constructor: ($scope) -> #console.log "Center::constructor", arguments... @scope = $scope setRect: (rect) -> #console.log "Center::setRect", arguments... @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]) -> #console.log "center::link" layout.setCenter ctrl module.directive "handle", [ "$window", ($window) -> restrict: "E" replace: true require: ["^border", "^borderLayout"] transclude: true template: """ <div class="handle" ng-class="{moving: moving, constrained: constrained}" ng-mousedown="trackMove($event)" ng-transclude> </div> """ link: ($scope, $el, attrs, [border, layout]) -> $scope.trackMove = ($event) -> #console.log "trackMove", arguments... $event.preventDefault() if border.scope.anchor in ["north", "south"] then coord = "y" else if border.scope.anchor in ["west", "east"] then coord = "x" if border.scope.anchor in ["north", "west"] then scale = 1 else if border.scope.anchor in ["south", "east"] then scale = -1 initialCoord = $event[coord] initialSize = border.scope.size el = $el[0] mouseMove = throttle 10, (e) -> #console.log "Mousemove", arguments... e.preventDefault() el.setCapture?() $scope.$apply -> targetSize = initialSize + scale * (e[coord] - initialCoord) border.scope.size = targetSize layout.reflow() #console.log "Constrained", targetSize, border.scope.size $scope.constrained = border.scope.constrained = targetSize != border.scope.size $scope.moving = border.scope.moving = true mouseUp = (e) -> #console.log "Mouseup", arguments... e.preventDefault() $scope.$apply -> $scope.constrained = border.scope.constrained = false $scope.moving = border.scope.moving = false $window.removeEventListener "mousemove", mouseMove, true $window.removeEventListener "mouseup", mouseUp, true el.releaseCapture?() el.unselectable = "on"; el.onselectstart = -> false el.style.userSelect = el.style.MozUserSelect = "none" $window.addEventListener "mousemove", mouseMove, true $window.addEventListener "mouseup", mouseUp, true ] module.directive "borderLayout", [ "$window", ($window) -> stylesAdded = false stylesheet = """ """ restrict: "E" replace: true require: "borderLayout" transclude: true template: """ <div class="border-layout" ng-transclude> </div> """ controller: class BorderLayout @inject = ["$scope", "$element"] constructor: ($scope, $element) -> #console.log "BorderLayout::constructor", arguments... @handleSize = 8 @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 @ 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) -> #console.log "border-layout::link" ctrl.reflow() $window.addEventListener "resize", -> #console.log "Resize event", arguments... $scope.$apply(ctrl.reflow.bind(ctrl)) ]

module = angular.module "ped.previewer", [ ] module.factory "types", -> types = html: regex: /\.html$/i mime: "text/html" javascript: regex: /\.js$/i mime: "text/javascript" css: regex: /\.css$/i mime: "text/css" text: regex: /\.txt$/ mime: "text/plain" for name, type of types types.name = name types: types getByFilename: (filename) -> for name, mode of @types if mode.regex.test(filename) then return mode return @types.text module.directive "previewer", [ "$q", "$rootScope", "types", ($q, $rootScope, types) -> restrict: "E" replace: true scope: session: "=" template: """ <iframe src="about:blank" width="100%" height="400px" frameborder="0"></iframe> """ link: ($scope, $el, attrs) -> withFiler = do -> dfd = $q.defer() filer = new Filer filer.init {persistent: false, size: 1024 * 1024}, (fs) -> $rootScope.$apply -> if fs then dfd.resolve(filer) else dfd.reject("Failed to create filesystem") (cb) -> dfd.promise.then(cb) $scope.$watch "session.buffers", (buffers) -> withFiler (filer) -> promises = [] index = null for buffer in buffers then do (buffer) -> dfd = $q.defer() filer.write buffer.filename, {data: buffer.content, type: types.getByFilename(buffer.filename).mime, append: false}, (entry) -> $scope.$apply -> #console.log "File written", buffer.filename, arguments... if buffer.filename is "index.html" then index = entry dfd.resolve() promises.push(dfd.promise) $q.all(promises).then -> #console.log "Preview ready", index.toURL() $el.attr "src", if index then index.toURL() else "about:blank" $scope.$broadcast "refresh" , true ]