Robert Marshall преди 8 години
родител
ревизия
15053f367d

+ 23 - 17
Controller/Gallery.php

@@ -4,17 +4,17 @@ class Gallery extends Controller {
 		$albums=$repo->GetAlbums();
 		return new View("Gallery/index.view",array("albums"=>$albums));
 	}
-	
+
 	public function View($url) {
 		$album=new Album($url,true);
 		Breadcrumbs::Add($album->AlbumTitle, "");
 		return new View("Gallery/view.view",array("album"=>$album));
 	}
-	
-	public function Manage(IAlbumRepository $albumRepo, IImageRepository $imageRepo, $errors) {		
+
+	public function Manage(IAlbumRepository $albumRepo, IImageRepository $imageRepo, $errors) {
 		$vars=array();
 		Breadcrumbs::Add("Manage", "");
-		$vars["albums"]=$albumRepo->GetAlbums(true,true);
+		$vars["albums"]=$albumRepo->GetAlbums(true,true,true);
 		if (isset($errors))
 			$vars["errors"]=$errors;
 		$vars['images']=$imageRepo->GetImagesByAlbum(0);
@@ -24,19 +24,25 @@ class Gallery extends Controller {
 			$vars['image']=new Image();
 		return new View("Gallery/manage.view",$vars);
 	}
-	
-	public function Move($moveImages, $selected, $selectAlbumNew){
-		if (!isset($moveImages))
-			return;
-		//return print_r($params,true);
-		foreach ($selected as $imgId){
-			$image=new Image($imgId);
-			$image->AlbumId=$selectAlbumNew;
-			$image->Save();
+
+	public function Move(IAlbumRepository $albumRepo, $selectedImages, $targetAlbumId){
+		if ($selectedImages!=null){
+			foreach ($selectedImages as $imgId){
+				$image=new Image($imgId);
+				$image->AlbumId=$targetAlbumId;
+				$image->Save();
+			}
 		}
-		header("location:/gallery/manage");
+		$albums=$albumRepo->GetAlbums(true,true,true);
+		return json_encode($albums);
+	}
+
+	public function DeleteImages(IAlbumRepository $albumRepo, IImageRepository $imageRepo, $selectedImages){
+		$imageRepo->DeleteImages($selectedImages);
+		$albums=$albumRepo->GetAlbums(true,true,true);
+		return json_encode($albums);
 	}
-	
+
 	public function CreateAlbum($title,$description) {
 		Breadcrumbs::Add("Manage", "");
 		Breadcrumbs::Add("Create Album", "");
@@ -52,7 +58,7 @@ class Gallery extends Controller {
 		}
 		return new View("Gallery/create_album.view",array("album"=>$album));
 	}
-	
+
 	public function Upload($imageTitle,$imageDesc) {
 		if (!isset($_FILES['imageFile'])){
 			header("location: /gallery/manage/");
@@ -81,7 +87,7 @@ class Gallery extends Controller {
 		}
 		return $this->Manage(array("errors"=>$errors,"image"=>$image));
 	}
-	
+
 	public function JsonLoadAlbum($albumId) {
 		$repo=new ImageRepository();
 		$json='[';

+ 23 - 2
Database/AlbumRepository.php

@@ -1,7 +1,28 @@
 <?php
 class AlbumRepository extends BaseRepository implements IAlbumRepository {
-	public function GetAlbums($includeHidden=false, $includeEmpty=false) {
-		$albums=array();
+	/**
+	 * @var IImageRepository
+	 */
+	private $_imageRepo;
+
+	public function __construct() {
+		parent::__construct();
+		$this->_imageRepo=new ImageRepository();
+	}
+
+	public function GetAlbums($includeHidden=false, $includeEmpty=false, $includeOrphanImages=false) {
+		if ($includeOrphanImages){
+			$noAlbum=new Album();
+			$noAlbum->AlbumId=0;
+			$noAlbum->AlbumTitle="No Album";
+			$noAlbum->AlbumDescription="Images without an album";
+			$noAlbum->Images=$this->_imageRepo->GetImagesByAlbum(0);
+
+			$albums=array(
+				$noAlbum
+			);
+		}
+		
 		$hidden=$includeHidden?" OR album_hidden=1":"";
 		$albumIds=self::$PDO->query("SELECT album_id FROM albums WHERE album_deleted=0 AND album_hidden=0".$hidden);
 		while ($albumId=$albumIds->fetchColumn()){

+ 1 - 0
Database/IImageRepository.php

@@ -1,4 +1,5 @@
 <?php
 interface IImageRepository {
 	public static function GetImagesByAlbum($albumId);
+	public static function DeleteImages($imageIds);
 }

+ 6 - 0
Database/ImageRepository.php

@@ -8,4 +8,10 @@ class ImageRepository extends BaseRepository implements IImageRepository {
 			$images[]=new Image($imageId);
 		return $images;
 	}
+
+	public static function DeleteImages($imageIds) {
+		$inQuery = implode(',', array_fill(0, count($imageIds), '?'));
+		$prep=self::$PDO->prepare("UPDATE images SET image_deleted=1 WHERE image_id IN ($inQuery)");
+		$prep->execute($imageIds);
+	}
 }

+ 11 - 5
Model/Album.php

@@ -1,21 +1,27 @@
 <?php
-class Album extends DBObject {
+class Album extends DBObject implements JsonSerializable {
 	public $Images=array();
-	
+
 	public function __construct($id=0,$forceUrl=false) {
 		$repo=new ImageRepository();
-		
+
 		$field="album_id";
 		if ($forceUrl || !is_numeric($id))
 			$field="album_url";
 		parent::__construct("albums", $field, $id);
-		
+
 		$this->Images=$repo->GetImagesByAlbum($this->AlbumId);
 	}
-	
+
 	public function Save() {
 		if (!isset($this->AlbumUrl) || $this->AlbumUrl==null || $this->AlbumUrl=="")
 			$this->AlbumUrl=Utils::MakeUniqueURL("albums", "album_url", $this->AlbumTitle);
 		parent::Save();
 	}
+
+	public function jsonSerialize() {
+		$obj= parent::jsonSerialize();
+		$obj->Images=$this->Images;
+		return $obj;
+	}
 }

+ 10 - 2
Model/DBObject.php

@@ -1,6 +1,6 @@
 <?php
 
-class DBObject implements ISavableObject {
+class DBObject implements ISavableObject, JsonSerializable {
 	protected static $PREPARED_STATEMENTS=array();
 	private static $_classFields=array();
 	protected $_fields=array();
@@ -54,7 +54,7 @@ class DBObject implements ISavableObject {
 			$this->_keys=array($key);
 			$this->_ids=array($id);
 		}
-		
+
 		$this->ReIndexKeyAndIdArrays();
 		$this->Load();
 	}
@@ -157,4 +157,12 @@ class DBObject implements ISavableObject {
 		}
 	}
 
+	public function jsonSerialize() {
+		$obj=new stdClass();
+		foreach ($this->_fields as $key=> $value) {
+			$obj->$key=$value;
+		}
+		return $obj;
+	}
+
 }

+ 3 - 5
Model/Image.php

@@ -80,11 +80,9 @@ class Image extends DBObject implements JsonSerializable {
 	}
 
 	public function jsonSerialize() {
-		$obj=new stdClass();
-		$obj->path=$this->Path;
-		$obj->thumbnailPath=$this->ThumbnailPath;
-		$obj->imageTitle=$this->ImageTitle;
-		$obj->imageDescription=$this->ImageDescription;
+		$obj=parent::jsonSerialize();
+		$obj->ThumbnailPath=$this->ThumbnailPath;
+		$obj->Path=$this->Path;
 		return $obj;
 	}
 

+ 23 - 191
View/Gallery/manage.view

@@ -1,203 +1,35 @@
-@Title{Gallery}@
-@CSS{
-	<?=file_get_contents("css/gallery.css")?>
-
-	#main-header .header{
-		box-shadow:none !important;
-	}
-}@
-@CSSMed{
-	@media(orientation:portrait){
-		.image{
-			width:33.3%;
-		}
-	}
+@Init{
+	$this->RegisterCSSFile("gallery.css");
+	$this->RegisterJSFile("controllers/galleryManage.js");
+	$this->RegisterJSFile("directives/contextMenu.js");
 }@
-@CSSSmall{
-	@media(orientation:portrait){
-		#albums td:nth-child(3), #albums th:nth-child(3){
-			display:none;
-		}
-		
-		.image{
-			width:50%;
-		}
-	}
-	
-	@media(orientation:landscape){
-		.image{
-			width:33.3%;
-		}
-	}
-	
-	#albums td:first-child, #albums th:first-child{
-		display:none;
-	}
-	
-	table{
-		width:100%;
-	}
-	
-	#albums td:last-child, #albums th:last-child, #albums td:nth-child(3), #albums th:nth-child(3){
-		width:1px;
-	}
+@Title{Gallery}@
+@ButtonsLeft{
+	<button onclick="Navigate('/gallery/createalbum/')" title="Upload Images">
+		<img src="/images/upload.svg" alt="Add Album" />
+	</button>
 }@
 @ButtonsRight{
 	<button onclick="Navigate('/gallery/createalbum/')" title="Create Album">
 		<img src="/images/add_album.svg" alt="Add Album" />
 	</button>
 }@
-@JavaScript{
-	$(function(){
-		$("#tabs-content").css("margin-top",$("#tabs").height()+"px");
-	
-		$("#tabs>a").click(function(){
-			$("#tabs-content>*").hide();
-			$($(this).attr("href")).show();
-			$("#tabs>a").removeClass("active");
-			$(this).addClass("active");
-			
-			return false;
-		});
-		
-		$("#tabs a.active").click();
-		
-		$(".image input[type=checkbox]").click(function(){
-			return false;
-		});
-		
-		function SetupSelectedImageClass(elem){
-			var tick=elem.find("input[type=checkbox]");
-			if (tick.prop("checked"))
-				elem.addClass("selected");
-			else
-				elem.removeClass("selected");
-		}
-		
-		function ApplyImageClick(){
-			$(".image").click(function(){
-				var tick=$(this).find("input[type=checkbox]");
-				tick.prop("checked",!tick.prop("checked"));
-
-				SetupSelectedImageClass($(this));
-			});
-		}
-		
-		$(".image").each(function(e){ // refresh hack
-			SetupSelectedImageClass($(this));
-		});
-		
-		$("#selectAlbumOld").change(function(){
-			$.ajax("/gallery/jsonLoadAlbum/"+this.selectedIndex).done(function(data){
-				var images=JSON.parse(data);
-				html="";
-				for (i=0;i<images.length;i++)
-					html+='<div class="image">'+
-						'<div>'+
-							'<img src="/'+images[i].ThumbnailPath+'" alt="'+images[i].ImageTitle+'" />'+
-							'<span>'+images[i].ImageTitle+'</span>'+
-							'<input type="checkbox" value="'+images[i].ImageId+'" name="selected[]" />'+
-						'</div>'+
-					'</div>';
-				if (html=="")
-					html="<p>There are no images in this album</p>";
-				$("#imageSelector").html(html);
-				ApplyImageClick();
-			});
-		});
-		
-		ApplyImageClick();
-	});
-}@
 @Body{
-<div id="tabs">
-	<a href="#images">Images</a><a href="#albums">Albums</a><a class="active" href="#organise">Organise</a>
-</div>
-<div id="tabs-content" style="margin-top:55px"> 
-	<div id="images">
-		<h3>New Image</h3>
-		<form action="/gallery/upload/" method="post" enctype="multipart/form-data">
-			<table>
-				<tr>
-					<td><label for="imageTitle">Image title:</label></td>
-					<td><input type="text" name="imageTitle" id="imageTitle" value="<?=$image->ImageTitle?>" /></td>
-				</tr>
-				<tr>
-					<td><label for="imageDesc">Image description</label></td>
-					<td><textarea name="imageDesc" id="imageDesc"><?=$image->ImageDescription?></textarea></td>
-				</tr>
-				<tr>
-					<td><label for="imageFile">Select image:</label></td>
-					<td><input type="file" name="imageFile" id="imageFile" /></td>
-				</tr>
-				<tr>
-					<td colspan="2" align="right">
-						<button type="submit" title="Upload" name="imageUpload">
-							<img src="/images/send.svg" alt="Upload" />
-						</button>
-					</td>
-				</tr>
-			</table>
-		</form>
-	</div>
-	<div id="albums">
-		<form action="" method="post" id="manageForm">
-			<input type="hidden" name="formSubmitted" value="yes" />
-			<table>
-				<tr>
-					<th>ID</th>
-					<th>Title</th>
-					<th>Timestamp</th>
-					<th>Actions</th>
-				</tr>
-				<?php foreach ($albums as $album){?>
-				<tr>
-					<td><?=$album->AlbumId?></td>
-					<td><a href="/gallery/view/<?=$album->AlbumUrl?>"><?=$album->AlbumTitle?></a></td>
-					<td><?=$album->AlbumTimestamp?></td>
-					<td>
-						<button value="<?=$album->AlbumId?>" name="delete" title="Delete">
-							<img src="/images/delete.svg" alt="Delete" />
-						</button>
-						<button value="/blog/edit/<?=$album->AlbumId?>" name="edit" title="Edit" class="jsNav">
-							<img src="/images/edit.svg" alt="Edit" />
-						</button>
-					</td>
-				</tr>
-				<?php } ?>
-			</table>
-		</form>
-	</div>
-	<div id="organise">
-		<form action="/gallery/move" method="post">
-			<label for="selectAlbumOld">Move from album </label>
-			<select id="selectAlbumOld" name="selectAlbumOld">
-				<option value="0">No album</option>
-				<?php foreach ($albums as $album)
-					echo '<option value="',$album->AlbumId,'">',$album->AlbumTitle,'</option>';
-				?>
-			</select>
-			<label for="selectAlbumNew"> to album </label>
-			<select id="selectAlbumNew" name="selectAlbumNew">
-				<option value="0">No album</option>
-				<?php foreach ($albums as $album)
-					echo '<option value="',$album->AlbumId,'">',$album->AlbumTitle,'</option>';
-				?>
-			</select>
-			<input type="submit" name="moveImages" value="go" />
-			<div id="imageSelector">
-				<?php
-				foreach ($images as $image)
-					echo '<div class="image">',
-							'<div>',
-								'<img src="/',$image->ThumbnailPath,'" alt="',$image->ImageTitle,'" />',
-								'<span>',$image->ImageTitle,'</span>',
-								'<input type="checkbox" value="',$image->ImageId,'" name="selected[]" />',
-							'</div>',
-						'</div>';
-				?>
+<div ng-controller="galleryManage">
+	<select ng-model="selectedAlbum" ng-options="album.AlbumTitle for album in albums"></select>
+	<button ng-click="editAlbum(selectedAlbum)" title="Edit Album">
+		<img src="/images/edit.svg" alt="Edit" />
+	</button>
+	<div class="imageSelector">
+		<context-menu actions="contextMenuActions"></context-menu>
+		<div class="image" ng-repeat="image in selectedAlbum.Images" ng-click="selectImage(image)" ng-class="{selected:image.selected}">
+			<div>
+				<img ng-src="/{{image.ThumbnailPath}}" alt="{{image.ImageTitle}}" />
+				<span>{{image.ImageTitle}}</span>
+				<input type="checkbox" value="{{image.ImageId}}" name="selected[]" />
 			</div>
-		</form>
+		</div>
 	</div>
+	<scope-init value="albums"><?=json_encode($albums)?></scope-init>
 </div>
 }@

+ 24 - 0
css/animations.css

@@ -0,0 +1,24 @@
+.ng-leave {
+  opacity: 1;
+}
+.ng-leave.ng-leave-active {
+  opacity: 0;
+}
+.ng-enter {
+  opacity: 0;
+}
+.ng-enter.ng-enter-active {
+  opacity: 1;
+}
+.ng-hide-add {
+  opacity: 1;
+}
+.ng-hide-add.ng-hide-add-active {
+  opacity: 0;
+}
+.ng-hide-remove {
+  opacity: 0;
+}
+.ng-hide-remove.ng-hide-remove-active {
+  opacity: 1;
+}

+ 31 - 0
css/contextMenu.css

@@ -0,0 +1,31 @@
+.contextMenu {
+  position: absolute;
+  float: left;
+  background: #fafafa;
+  border: 1px solid #9e9e9e;
+  border-radius: 5px;
+  padding: 3px 0;
+  box-shadow: 3px 3px 5px 0px rgba(0, 0, 0, 0.5);
+  -webkit-user-select: none;
+  /* Chrome all / Safari all */
+  -moz-user-select: none;
+  /* Firefox all */
+  -ms-user-select: none;
+  /* IE 10+ */
+  user-select: none;
+  cursor: default;
+}
+.contextMenu .menuItem {
+  padding: 0 5px;
+  position: relative;
+  white-space: nowrap;
+}
+.contextMenu .menuItem:hover {
+  background: #80cbc4;
+}
+.contextMenu .menuItem:not(:last-child) {
+  border-bottom: 1px solid #eeeeee;
+}
+.contextMenu .menuItem .arrow {
+  text-align: right;
+}

+ 0 - 1
css/gallery.css

@@ -22,7 +22,6 @@
 .album {
   width: 33.33%;
   /*float:left;*/
-
   display: inline-block;
   vertical-align: top;
 }

+ 56 - 1
css/style.css

@@ -1,4 +1,59 @@
 @import url(http://fonts.googleapis.com/css?family=Roboto);
+.ng-leave {
+  opacity: 1;
+}
+.ng-leave.ng-leave-active {
+  opacity: 0;
+}
+.ng-enter {
+  opacity: 0;
+}
+.ng-enter.ng-enter-active {
+  opacity: 1;
+}
+.ng-hide-add {
+  opacity: 1;
+}
+.ng-hide-add.ng-hide-add-active {
+  opacity: 0;
+}
+.ng-hide-remove {
+  opacity: 0;
+}
+.ng-hide-remove.ng-hide-remove-active {
+  opacity: 1;
+}
+.contextMenu {
+  position: absolute;
+  float: left;
+  background: #fafafa;
+  border: 1px solid #9e9e9e;
+  border-radius: 5px;
+  padding: 3px 0;
+  box-shadow: 3px 3px 5px 0px rgba(0, 0, 0, 0.5);
+  -webkit-user-select: none;
+  /* Chrome all / Safari all */
+  -moz-user-select: none;
+  /* Firefox all */
+  -ms-user-select: none;
+  /* IE 10+ */
+  user-select: none;
+  cursor: default;
+}
+.contextMenu .menuItem {
+  padding: 0 5px;
+  position: relative;
+  white-space: nowrap;
+}
+.contextMenu .menuItem:hover {
+  background: #80cbc4;
+}
+.contextMenu .menuItem:not(:last-child) {
+  border-bottom: 1px solid #eeeeee;
+}
+.contextMenu .menuItem .arrow {
+  text-align: right;
+}
 * {
   box-sizing: border-box;
   outline: none;
@@ -277,7 +332,7 @@ p:last-child {
   background: rgba(0, 0, 0, 0.5);
   width: 100%;
   height: 100%;
-  position: fixed;
+  position: absolute;
   top: 0;
   left: 0;
   z-index: 100;

+ 9 - 0
images/upload.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<path fill="none" d="M0,0h24v24H0V0z"/>
+<polygon fill="#03A9F4" points="9,16 15,16 15,10 19,10 12,3 5,10 9,10 "/>
+<rect x="5" y="18" fill="#0288D1" width="14" height="2"/>
+</svg>

+ 31 - 0
less/animations.less

@@ -0,0 +1,31 @@
+.ng-leave {
+	opacity:1;
+
+	&.ng-leave-active {
+		opacity:0;
+	}
+}
+
+.ng-enter{
+	opacity:0;
+
+	&.ng-enter-active {
+		opacity:1;
+	}
+}
+
+.ng-hide-add{
+	opacity:1;
+
+	&.ng-hide-add-active{
+		opacity:0;
+	}
+}
+
+.ng-hide-remove{
+	opacity:0;
+
+	&.ng-hide-remove-active{
+		opacity:1;
+	}
+}

+ 34 - 0
less/contextMenu.less

@@ -0,0 +1,34 @@
+@import "colours.less";
+
+.contextMenu{
+	position: absolute;
+	float:left;
+	background:@background;
+	border:1px solid @Grey-500;
+	border-radius:5px;
+	padding: 3px 0;
+	box-shadow: 3px 3px 5px 0px rgba(0, 0, 0, 0.5);
+	-webkit-user-select: none;  /* Chrome all / Safari all */
+	-moz-user-select: none;     /* Firefox all */
+	-ms-user-select: none;      /* IE 10+ */
+	user-select: none;
+	cursor:default;
+
+	.menuItem{
+		padding: 0 5px;
+		position:relative;
+		white-space: nowrap;
+
+		&:hover{
+			background: @accent-200;
+		}
+
+		&:not(:last-child){
+			border-bottom: 1px solid @Grey-200;
+		}
+
+		.arrow{
+			text-align: right;
+		}
+	}
+}

+ 3 - 1
less/style.less

@@ -1,4 +1,6 @@
 @import "colours.less";
+@import "animations.less";
+@import "contextMenu.less";
 
 @import url(http://fonts.googleapis.com/css?family=Roboto);
 
@@ -328,7 +330,7 @@ p{
 	background: rgba(0,0,0,0.5);
 	width: 100%;
 	height:100%;
-	position:fixed;
+	position:absolute;
 	top:0;
 	left:0;
 	z-index:100;

+ 77 - 0
scripts/controllers/galleryManage.js

@@ -0,0 +1,77 @@
+app.controller('galleryManage', function($scope, $http) {
+	$scope.albums=[];
+	var albumActions={};
+
+	var moveIndex="Move selected to album"
+	$scope.contextMenuActions={
+		"Delete selected":deleteSelected
+	};
+
+	function getSelectedImageIds(){
+		var images=[];
+		angular.forEach($scope.selectedAlbum.Images, function(image){
+			if (image.selected)
+				images.push(image.ImageId);
+		});
+		return images;
+	}
+
+	function updateAlbums(data){
+		var selectedId=$scope.selectedAlbum.AlbumId;
+		$scope.albums=data;
+		angular.forEach($scope.albums, function(album){
+			if (album.AlbumId==selectedId)
+				$scope.selectedAlbum=album;
+		});
+	}
+
+	function deleteSelected(){
+		var images=getSelectedImageIds();
+		if (images.length===0 || !confirm("Are you sure you want to delete?"))
+			return;
+
+		var data={
+			selectedImages:images
+		};
+
+		$http.post("/gallery/deleteimages",data).then(function(response){
+			updateAlbums(response.data);
+		});
+	};
+
+	function moveImages(newAlbum){
+		var images=getSelectedImageIds();
+		if (images.length===0)
+			return;
+
+		var data={
+			selectedImages:images,
+			targetAlbumId:newAlbum.AlbumId
+		};
+		$http.post("/gallery/move",data).then(function(response){
+			updateAlbums(response.data);
+		});
+	}
+
+	$scope.$watch("albums", function(){
+		if ($scope.selectedAlbum===undefined)
+			$scope.selectedAlbum=$scope.albums[0];
+
+		albumActions={};
+		angular.forEach($scope.albums, function(album){
+			albumActions[album.AlbumTitle]=function(){
+				moveImages(album);
+			};
+		});
+		$scope.contextMenuActions[moveIndex]=albumActions;
+	});
+
+	$scope.selectImage=function(image){
+		image.selected=!image.selected;
+	};
+
+	$scope.editAlbum=function(album){
+		Navigate("/gallery/editablum/"+album.AlbumId);
+	}
+});
+

+ 61 - 0
scripts/directives/contextMenu.js

@@ -0,0 +1,61 @@
+/* global google, angular */
+
+app.directive('contextMenu', function() {
+	return {
+		restrict: 'E',
+		templateUrl: '/scripts/directives/templates/contextMenu.html',
+		replace:false,
+		scope: {
+			actions: '=',
+			isChild: '=?'
+		},
+		link: function(scope, element) {
+			scope.isFunction = angular.isFunction;
+			scope.show=false;
+			scope.selectedElement=null;
+			scope.subMenuShowKey="";
+
+			scope.changeSubMenuShowKey=function(name){
+				scope.subMenuShowKey=name;
+			}
+
+			function show(x, y, target){
+				scope.changeSubMenuShowKey("");
+				scope.menuX=x+"px";
+				scope.menuY=y+"px";
+				scope.selectedElement=target
+				scope.show=true;
+			}
+
+			function hide(){
+				scope.show=false;
+			}
+
+			scope.performAction=function(func){
+				if (!angular.isFunction(func))
+					return;
+				func(scope.selectedElement);
+				hide();
+			}
+
+			$(document).on("click", function(e){
+				hide();
+				scope.$apply();
+			});
+
+			var parent=element.parent()[0];
+
+			if (scope.isChild){
+				show(parent.clientWidth,parent.clientTop);
+			}else
+				$(parent).css("position","relative");
+				element.parent().on("contextmenu", function(e){
+					var offset=$(element).offset();
+					show(e.pageX-offset.left, e.pageY-offset.top, e.target);
+					scope.$apply();
+					e.preventDefault();
+				});
+		}
+	};
+});
+

+ 6 - 0
scripts/directives/templates/contextMenu.html

@@ -0,0 +1,6 @@
+<div class="contextMenu" ng-if="show" ng-style="{left:menuX,top:menuY}">
+	<div ng-repeat="(name, func) in actions track by name" class="menuItem" ng-click="performAction(func)" ng-mouseover="changeSubMenuShowKey(name)">
+		{{name}}<span class="arrow" ng-if="!isFunction(func)"> &#8250;</span>
+		<context-menu ng-if="!isFunction(func)" actions="func" is-child="true" ng-show="subMenuShowKey==name"></context-menu>
+	</div>
+</div>

+ 2 - 2
scripts/javascript.js

@@ -55,7 +55,7 @@ Number.prototype.oldToString=Number.prototype.toString;
 Number.prototype.toString=function(input, shouldRound){
 	if (!shouldRound)
 		return this.oldToString(input);
-	
+
 	return round(this, input);
 };
 
@@ -92,7 +92,7 @@ $(function() {
 		return false;
 	});
 
-	$("#content").css("padding-bottom", window.innerHeight - $("#buttons").offset().top);
+	$("#content").css("padding-bottom", window.innerHeight - $("#buttons-right").offset().top);
 
 	$("form[ajaxForm]").submit(function(e) {
 		e.preventDefault();