diff --git a/appinfo/routes.php b/appinfo/routes.php
index ffdb4771..a534ebe8 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -60,6 +60,8 @@ $application->registerRoutes($this, array('routes' => array(
array('name' => 'tasks#setReminderDate','url' => '/tasks/{taskID}/reminder', 'verb' => 'POST'),
array('name' => 'tasks#addComment', 'url' => '/tasks/{taskID}/comment', 'verb' => 'POST'),
array('name' => 'tasks#deleteComment', 'url' => '/tasks/{taskID}/comment/{commentID}/delete', 'verb' => 'POST'),
+ array('name' => 'tasks#addCategory', 'url' => '/tasks/{taskID}/category/add', 'verb' => 'POST'),
+ array('name' => 'tasks#removeCategory', 'url' => '/tasks/{taskID}/category/remove', 'verb' => 'POST'),
// settings
array('name' => 'settings#get', 'url' => '/settings', 'verb' => 'GET'),
diff --git a/controller/pagecontroller.php b/controller/pagecontroller.php
index 016aa0ee..2bbfd795 100644
--- a/controller/pagecontroller.php
+++ b/controller/pagecontroller.php
@@ -27,11 +27,15 @@ class PageController extends Controller {
\OCP\Util::addScript('tasks', 'vendor/angularjs/angular');
\OCP\Util::addScript('tasks', 'vendor/angularjs/angular-route');
\OCP\Util::addScript('tasks', 'vendor/angularjs/angular-animate');
+ \OCP\Util::addScript('tasks', 'vendor/angularjs/angular-sanitize');
+ \OCP\Util::addScript('tasks', 'vendor/angularui/ui-select/select');
\OCP\Util::addScript('tasks', 'vendor/bootstrap/ui-bootstrap-custom-tpls-0.10.0');
} else {
\OCP\Util::addScript('tasks', 'vendor/angularjs/angular.min');
\OCP\Util::addScript('tasks', 'vendor/angularjs/angular-route.min');
\OCP\Util::addScript('tasks', 'vendor/angularjs/angular-animate.min');
+ \OCP\Util::addScript('tasks', 'vendor/angularjs/angular-sanitize.min');
+ \OCP\Util::addScript('tasks', 'vendor/angularui/ui-select/select.min');
\OCP\Util::addScript('tasks', 'vendor/bootstrap/ui-bootstrap-custom-tpls-0.10.0.min');
}
\OCP\Util::addScript('tasks', 'public/app');
@@ -39,6 +43,7 @@ class PageController extends Controller {
\OCP\Util::addScript('tasks', 'vendor/timepicker/jquery.ui.timepicker');
\OCP\Util::addStyle('tasks', 'style');
\OCP\Util::addStyle('tasks', 'vendor/bootstrap/bootstrap');
+ \OCP\Util::addStyle('tasks', 'vendor/angularui/ui-select/select2');
$date = new \DateTimeZone(\OC_Calendar_App::getTimezone());
$day = new \DateTime('today', $date);
diff --git a/controller/taskscontroller.php b/controller/taskscontroller.php
index 0e797571..c09ff9e1 100644
--- a/controller/taskscontroller.php
+++ b/controller/taskscontroller.php
@@ -514,14 +514,53 @@ class TasksController extends Controller {
/**
* @NoAdminRequired
*/
- public function setCategories(){
+ public function addCategory(){
$taskId = $this->params('taskID');
- $categories = $this->params('categories');
+ $category = $this->params('category');
$response = new JSONResponse();
try {
$vcalendar = \OC_Calendar_App::getVCalendar($taskId);
$vtodo = $vcalendar->VTODO;
- $vtodo->CATEGORIES = $categories;
+ // fetch categories from TODO
+ $categories = $vtodo->CATEGORIES;
+ if ($categories){
+ $taskcategories = $categories->getParts();
+ }
+ // add category
+ if (!in_array($category, $taskcategories)){
+ $taskcategories[] = $category;
+ } else {
+ }
+ $vtodo->CATEGORIES = $taskcategories;
+ \OC_Calendar_Object::edit($taskId, $vcalendar->serialize());
+ } catch(\Exception $e) {
+ // throw new BusinessLayerException($e->getMessage());
+ }
+ return $response;
+ }
+
+ /**
+ * @NoAdminRequired
+ */
+ public function removeCategory(){
+ $taskId = $this->params('taskID');
+ $category = $this->params('category');
+ $response = new JSONResponse();
+ try {
+ $vcalendar = \OC_Calendar_App::getVCalendar($taskId);
+ $vtodo = $vcalendar->VTODO;
+ // fetch categories from TODO
+ $categories = $vtodo->CATEGORIES;
+ if ($categories){
+ $taskcategories = $categories->getParts();
+ }
+ // remove category
+ $key = array_search($category, $taskcategories);
+ if ($key !== null && $key !== false){
+ unset($taskcategories[$key]);
+ } else {
+ }
+ $vtodo->CATEGORIES = $taskcategories;
\OC_Calendar_Object::edit($taskId, $vcalendar->serialize());
} catch(\Exception $e) {
// throw new BusinessLayerException($e->getMessage());
diff --git a/css/style.css b/css/style.css
index 4b7c6fdf..509ad381 100644
--- a/css/style.css
+++ b/css/style.css
@@ -903,7 +903,6 @@
font-weight: 500;
outline: medium none;
padding: 4px;
- transition: all 0.2s ease-in-out 0s;
}
#task-details div.content-wrapper .body .section input.focus {
background-color: #F8F8F8;
@@ -1024,6 +1023,9 @@
#task-details div.content-wrapper .body .section.detail-reminder.date .section-description {
display: block;
}
+#task-details div.content-wrapper .body .section.detail-categories {
+ height: auto;
+}
#task-details div.content-wrapper .body .section.detail-note {
padding: 20px 0;
height: auto;
diff --git a/css/style.less b/css/style.less
index c94c4a9d..a695ce92 100644
--- a/css/style.less
+++ b/css/style.less
@@ -953,7 +953,6 @@
font-weight: 500;
outline: medium none;
padding: 4px;
- transition: all 0.2s ease-in-out 0s;
&.focus{
background-color: #F8F8F8;
border: 1px solid rgba(43, 136, 217, 0.65);
@@ -1083,6 +1082,9 @@
}
}
}
+ &.detail-categories{
+ height: auto;
+ }
&.detail-note{
padding: 20px 0;
height:auto;
diff --git a/css/vendor/angularui/ui-select/select.css b/css/vendor/angularui/ui-select/select.css
new file mode 100644
index 00000000..d397c975
--- /dev/null
+++ b/css/vendor/angularui/ui-select/select.css
@@ -0,0 +1,221 @@
+/*!
+ * ui-select
+ * http://github.com/angular-ui/ui-select
+ * Version: 0.11.2 - 2015-03-17T04:08:46.478Z
+ * License: MIT
+ */
+
+
+/* Style when highlighting a search. */
+.ui-select-highlight {
+ font-weight: bold;
+}
+
+.ui-select-offscreen {
+ clip: rect(0 0 0 0) !important;
+ width: 1px !important;
+ height: 1px !important;
+ border: 0 !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ overflow: hidden !important;
+ position: absolute !important;
+ outline: 0 !important;
+ left: 0px !important;
+ top: 0px !important;
+}
+
+/* Select2 theme */
+
+/* Mark invalid Select2 */
+.ng-dirty.ng-invalid > a.select2-choice {
+ border-color: #D44950;
+}
+
+.select2-result-single {
+ padding-left: 0;
+}
+
+.select2-locked > .select2-search-choice-close{
+ display:none;
+}
+
+.select-locked > .ui-select-match-close{
+ display:none;
+}
+
+body > .select2-container.open {
+ z-index: 9999; /* The z-index Select2 applies to the select2-drop */
+}
+
+/* Selectize theme */
+
+/* Helper class to show styles when focus */
+.selectize-input.selectize-focus{
+ border-color: #007FBB !important;
+}
+
+/* Fix input width for Selectize theme */
+.selectize-control > .selectize-input > input {
+ width: 100%;
+}
+
+/* Fix dropdown width for Selectize theme */
+.selectize-control > .selectize-dropdown {
+ width: 100%;
+}
+
+/* Mark invalid Selectize */
+.ng-dirty.ng-invalid > div.selectize-input {
+ border-color: #D44950;
+}
+
+
+/* Bootstrap theme */
+
+/* Helper class to show styles when focus */
+.btn-default-focus {
+ color: #333;
+ background-color: #EBEBEB;
+ border-color: #ADADAD;
+ text-decoration: none;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+
+.ui-select-bootstrap .ui-select-toggle {
+ position: relative;
+}
+
+.ui-select-bootstrap .ui-select-toggle > .caret {
+ position: absolute;
+ height: 10px;
+ top: 50%;
+ right: 10px;
+ margin-top: -2px;
+}
+
+/* Fix Bootstrap dropdown position when inside a input-group */
+.input-group > .ui-select-bootstrap.dropdown {
+ /* Instead of relative */
+ position: static;
+}
+
+.input-group > .ui-select-bootstrap > input.ui-select-search.form-control {
+ border-radius: 4px; /* FIXME hardcoded value :-/ */
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.ui-select-bootstrap > .ui-select-match > .btn{
+ /* Instead of center because of .btn */
+ text-align: left !important;
+}
+
+.ui-select-bootstrap > .ui-select-match > .caret {
+ position: absolute;
+ top: 45%;
+ right: 15px;
+}
+
+/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */
+.ui-select-bootstrap > .ui-select-choices {
+ width: 100%;
+ height: auto;
+ max-height: 200px;
+ overflow-x: hidden;
+ margin-top: -1px;
+}
+
+body > .ui-select-bootstrap.open {
+ z-index: 1000; /* Standard Bootstrap dropdown z-index */
+}
+
+.ui-select-multiple.ui-select-bootstrap {
+ height: auto;
+ padding: 3px 3px 0 3px;
+}
+
+.ui-select-multiple.ui-select-bootstrap input.ui-select-search {
+ background-color: transparent !important; /* To prevent double background when disabled */
+ border: none;
+ outline: none;
+ height: 1.666666em;
+ margin-bottom: 3px;
+}
+
+.ui-select-multiple.ui-select-bootstrap .ui-select-match .close {
+ font-size: 1.6em;
+ line-height: 0.75;
+}
+
+.ui-select-multiple.ui-select-bootstrap .ui-select-match-item {
+ outline: 0;
+ margin: 0 3px 3px 0;
+}
+
+.ui-select-multiple .ui-select-match-item {
+ position: relative;
+}
+
+.ui-select-multiple .ui-select-match-item.dropping-before:before {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: 100%;
+ height: 100%;
+ margin-right: 2px;
+ border-left: 1px solid #428bca;
+}
+
+.ui-select-multiple .ui-select-match-item.dropping-after:after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 100%;
+ height: 100%;
+ margin-left: 2px;
+ border-right: 1px solid #428bca;
+}
+
+.ui-select-bootstrap .ui-select-choices-row>a {
+ display: block;
+ padding: 3px 20px;
+ clear: both;
+ font-weight: 400;
+ line-height: 1.42857143;
+ color: #333;
+ white-space: nowrap;
+}
+
+.ui-select-bootstrap .ui-select-choices-row>a:hover, .ui-select-bootstrap .ui-select-choices-row>a:focus {
+ text-decoration: none;
+ color: #262626;
+ background-color: #f5f5f5;
+}
+
+.ui-select-bootstrap .ui-select-choices-row.active>a {
+ color: #fff;
+ text-decoration: none;
+ outline: 0;
+ background-color: #428bca;
+}
+
+.ui-select-bootstrap .ui-select-choices-row.disabled>a,
+.ui-select-bootstrap .ui-select-choices-row.active.disabled>a {
+ color: #777;
+ cursor: not-allowed;
+ background-color: #fff;
+}
+
+/* fix hide/show angular animation */
+.ui-select-match.ng-hide-add,
+.ui-select-search.ng-hide-add {
+ display: none !important;
+}
+
+/* Mark invalid Bootstrap */
+.ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match {
+ border-color: #D44950;
+}
\ No newline at end of file
diff --git a/css/vendor/angularui/ui-select/select.min.css b/css/vendor/angularui/ui-select/select.min.css
new file mode 100644
index 00000000..0e460473
--- /dev/null
+++ b/css/vendor/angularui/ui-select/select.min.css
@@ -0,0 +1,6 @@
+/*!
+ * ui-select
+ * http://github.com/angular-ui/ui-select
+ * Version: 0.11.2 - 2015-03-17T04:08:46.478Z
+ * License: MIT
+ */.ui-select-highlight{font-weight:700}.ui-select-offscreen{clip:rect(0 0 0 0)!important;width:1px!important;height:1px!important;border:0!important;margin:0!important;padding:0!important;overflow:hidden!important;position:absolute!important;outline:0!important;left:0!important;top:0!important}.ng-dirty.ng-invalid>a.select2-choice{border-color:#D44950}.select2-result-single{padding-left:0}.select-locked>.ui-select-match-close,.select2-locked>.select2-search-choice-close{display:none}body>.select2-container.open{z-index:9999}.selectize-input.selectize-focus{border-color:#007FBB!important}.selectize-control>.selectize-dropdown,.selectize-control>.selectize-input>input{width:100%}.ng-dirty.ng-invalid>div.selectize-input{border-color:#D44950}.btn-default-focus{color:#333;background-color:#EBEBEB;border-color:#ADADAD;text-decoration:none;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.ui-select-bootstrap .ui-select-toggle{position:relative}.ui-select-bootstrap .ui-select-toggle>.caret{position:absolute;height:10px;top:50%;right:10px;margin-top:-2px}.input-group>.ui-select-bootstrap.dropdown{position:static}.input-group>.ui-select-bootstrap>input.ui-select-search.form-control{border-radius:4px 0 0 4px}.ui-select-bootstrap>.ui-select-match>.btn{text-align:left!important}.ui-select-bootstrap>.ui-select-match>.caret{position:absolute;top:45%;right:15px}.ui-select-bootstrap>.ui-select-choices{width:100%;height:auto;max-height:200px;overflow-x:hidden;margin-top:-1px}body>.ui-select-bootstrap.open{z-index:1000}.ui-select-multiple.ui-select-bootstrap{height:auto;padding:3px 3px 0}.ui-select-multiple.ui-select-bootstrap input.ui-select-search{background-color:transparent!important;border:none;outline:0;height:1.666666em;margin-bottom:3px}.ui-select-multiple.ui-select-bootstrap .ui-select-match .close{font-size:1.6em;line-height:.75}.ui-select-multiple.ui-select-bootstrap .ui-select-match-item{outline:0;margin:0 3px 3px 0}.ui-select-multiple .ui-select-match-item{position:relative}.ui-select-multiple .ui-select-match-item.dropping-before:before{content:"";position:absolute;top:0;right:100%;height:100%;margin-right:2px;border-left:1px solid #428bca}.ui-select-multiple .ui-select-match-item.dropping-after:after{content:"";position:absolute;top:0;left:100%;height:100%;margin-left:2px;border-right:1px solid #428bca}.ui-select-bootstrap .ui-select-choices-row>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.ui-select-bootstrap .ui-select-choices-row>a:focus,.ui-select-bootstrap .ui-select-choices-row>a:hover{text-decoration:none;color:#262626;background-color:#f5f5f5}.ui-select-bootstrap .ui-select-choices-row.active>a{color:#fff;text-decoration:none;outline:0;background-color:#428bca}.ui-select-bootstrap .ui-select-choices-row.active.disabled>a,.ui-select-bootstrap .ui-select-choices-row.disabled>a{color:#777;cursor:not-allowed;background-color:#fff}.ui-select-match.ng-hide-add,.ui-select-search.ng-hide-add{display:none!important}.ui-select-bootstrap.ng-dirty.ng-invalid>button.btn.ui-select-match{border-color:#D44950}
\ No newline at end of file
diff --git a/css/vendor/angularui/ui-select/select2.css b/css/vendor/angularui/ui-select/select2.css
new file mode 100644
index 00000000..2b32ed6f
--- /dev/null
+++ b/css/vendor/angularui/ui-select/select2.css
@@ -0,0 +1,615 @@
+/*
+Version: 3.4.5 Timestamp: Mon Nov 4 08:22:42 PST 2013
+*/
+.select2-container {
+ margin: 0;
+ position: relative;
+ display: inline-block;
+ /* inline-block for ie7 */
+ zoom: 1;
+ *display: inline;
+ vertical-align: middle;
+}
+
+.select2-container,
+.select2-drop,
+.select2-search,
+.select2-search input {
+ /*
+ Force border-box so that % widths fit the parent
+ container without overlap because of margin/padding.
+
+ More Info : http://www.quirksmode.org/css/box.html
+ */
+ -webkit-box-sizing: border-box; /* webkit */
+ -moz-box-sizing: border-box; /* firefox */
+ box-sizing: border-box; /* css3 */
+}
+
+.select2-container .select2-choice {
+ display: block;
+ height: 26px;
+ padding: 0 0 0 8px;
+ overflow: hidden;
+ position: relative;
+
+ border: 1px solid #aaa;
+ white-space: nowrap;
+ line-height: 26px;
+ color: #444;
+ text-decoration: none;
+
+ border-radius: 4px;
+
+ background-clip: padding-box;
+
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+ background-color: #fff;
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff));
+ background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 50%);
+ background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 50%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0);
+ background-image: linear-gradient(top, #fff 0%, #eee 50%);
+}
+
+.select2-container.select2-drop-above .select2-choice {
+ border-bottom-color: #aaa;
+
+ border-radius: 0 0 4px 4px;
+
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.9, #fff));
+ background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 90%);
+ background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 90%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);
+ background-image: linear-gradient(top, #eee 0%, #fff 90%);
+}
+
+.select2-container.select2-allowclear .select2-choice .select2-chosen {
+ margin-right: 42px;
+}
+
+.select2-container .select2-choice > .select2-chosen {
+ margin-right: 26px;
+ display: block;
+ overflow: hidden;
+
+ white-space: nowrap;
+
+ text-overflow: ellipsis;
+}
+
+.select2-container .select2-choice abbr {
+ display: none;
+ width: 12px;
+ height: 12px;
+ position: absolute;
+ right: 24px;
+ top: 8px;
+
+ font-size: 1px;
+ text-decoration: none;
+
+ border: 0;
+ background: url('select2.png') right top no-repeat;
+ cursor: pointer;
+ outline: 0;
+}
+
+.select2-container.select2-allowclear .select2-choice abbr {
+ display: inline-block;
+}
+
+.select2-container .select2-choice abbr:hover {
+ background-position: right -11px;
+ cursor: pointer;
+}
+
+.select2-drop-mask {
+ border: 0;
+ margin: 0;
+ padding: 0;
+ position: fixed;
+ left: 0;
+ top: 0;
+ min-height: 100%;
+ min-width: 100%;
+ height: auto;
+ width: auto;
+ opacity: 0;
+ z-index: 9998;
+ /* styles required for IE to work */
+ background-color: #fff;
+ filter: alpha(opacity=0);
+}
+
+.select2-drop {
+ width: 100%;
+ margin-top: -1px;
+ position: absolute;
+ z-index: 9999;
+ top: 100%;
+
+ background: #fff;
+ color: #000;
+ border: 1px solid #aaa;
+ border-top: 0;
+
+ border-radius: 0 0 4px 4px;
+
+ -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+ box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+}
+
+.select2-drop-auto-width {
+ border-top: 1px solid #aaa;
+ width: auto;
+}
+
+.select2-drop-auto-width .select2-search {
+ padding-top: 4px;
+}
+
+.select2-drop.select2-drop-above {
+ margin-top: 1px;
+ border-top: 1px solid #aaa;
+ border-bottom: 0;
+
+ border-radius: 4px 4px 0 0;
+
+ -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+ box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+}
+
+.select2-drop-active {
+ border: 1px solid #5897fb;
+ border-top: none;
+}
+
+.select2-drop.select2-drop-above.select2-drop-active {
+ border-top: 1px solid #5897fb;
+}
+
+.select2-container .select2-choice .select2-arrow {
+ display: inline-block;
+ width: 18px;
+ height: 100%;
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ border-left: 1px solid #aaa;
+ border-radius: 0 4px 4px 0;
+
+ background-clip: padding-box;
+
+ background: #ccc;
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee));
+ background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%);
+ background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0);
+ background-image: linear-gradient(top, #ccc 0%, #eee 60%);
+}
+
+.select2-container .select2-choice .select2-arrow b {
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: url('select2.png') no-repeat 0 1px;
+}
+
+.select2-search {
+ display: inline-block;
+ width: 100%;
+ min-height: 26px;
+ margin: 0;
+ padding-left: 4px;
+ padding-right: 4px;
+
+ position: relative;
+ z-index: 10000;
+
+ white-space: nowrap;
+}
+
+.select2-search input {
+ width: 100%;
+ height: auto !important;
+ min-height: 26px;
+ padding: 4px 20px 4px 5px;
+ margin: 0;
+
+ outline: 0;
+ font-family: sans-serif;
+ font-size: 1em;
+
+ border: 1px solid #aaa;
+ border-radius: 0;
+
+ -webkit-box-shadow: none;
+ box-shadow: none;
+
+ background: #fff url('select2.png') no-repeat 100% -22px;
+ background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee));
+ background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%);
+ background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%);
+ background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #fff 85%, #eee 99%);
+}
+
+.select2-drop.select2-drop-above .select2-search input {
+ margin-top: 4px;
+}
+
+.select2-search input.select2-active {
+ background: #fff url('select2-spinner.gif') no-repeat 100%;
+ background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee));
+ background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%);
+ background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%);
+ background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(top, #fff 85%, #eee 99%);
+}
+
+.select2-container-active .select2-choice,
+.select2-container-active .select2-choices {
+ border: 1px solid #5897fb;
+ outline: none;
+
+ -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3);
+ box-shadow: 0 0 5px rgba(0, 0, 0, .3);
+}
+
+.select2-dropdown-open .select2-choice {
+ border-bottom-color: transparent;
+ -webkit-box-shadow: 0 1px 0 #fff inset;
+ box-shadow: 0 1px 0 #fff inset;
+
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+
+ background-color: #eee;
+ background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #fff), color-stop(0.5, #eee));
+ background-image: -webkit-linear-gradient(center bottom, #fff 0%, #eee 50%);
+ background-image: -moz-linear-gradient(center bottom, #fff 0%, #eee 50%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);
+ background-image: linear-gradient(top, #fff 0%, #eee 50%);
+}
+
+.select2-dropdown-open.select2-drop-above .select2-choice,
+.select2-dropdown-open.select2-drop-above .select2-choices {
+ border: 1px solid #5897fb;
+ border-top-color: transparent;
+
+ background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(0.5, #eee));
+ background-image: -webkit-linear-gradient(center top, #fff 0%, #eee 50%);
+ background-image: -moz-linear-gradient(center top, #fff 0%, #eee 50%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);
+ background-image: linear-gradient(bottom, #fff 0%, #eee 50%);
+}
+
+.select2-dropdown-open .select2-choice .select2-arrow {
+ background: transparent;
+ border-left: none;
+ filter: none;
+}
+.select2-dropdown-open .select2-choice .select2-arrow b {
+ background-position: -18px 1px;
+}
+
+/* results */
+.select2-results {
+ max-height: 200px;
+ padding: 0 0 0 4px;
+ margin: 4px 4px 4px 0;
+ position: relative;
+ overflow-x: hidden;
+ overflow-y: auto;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+.select2-results ul.select2-result-sub {
+ margin: 0;
+ padding-left: 0;
+}
+
+.select2-results ul.select2-result-sub > li .select2-result-label { padding-left: 20px }
+.select2-results ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 40px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 60px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 80px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 100px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 110px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 120px }
+
+.select2-results li {
+ list-style: none;
+ display: list-item;
+ background-image: none;
+}
+
+.select2-results li.select2-result-with-children > .select2-result-label {
+ font-weight: bold;
+}
+
+.select2-results .select2-result-label {
+ padding: 3px 7px 4px;
+ margin: 0;
+ cursor: pointer;
+
+ min-height: 1em;
+
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.select2-results .select2-highlighted {
+ background: #3875d7;
+ color: #fff;
+}
+
+.select2-results li em {
+ background: #feffde;
+ font-style: normal;
+}
+
+.select2-results .select2-highlighted em {
+ background: transparent;
+}
+
+.select2-results .select2-highlighted ul {
+ background: #fff;
+ color: #000;
+}
+
+
+.select2-results .select2-no-results,
+.select2-results .select2-searching,
+.select2-results .select2-selection-limit {
+ background: #f4f4f4;
+ display: list-item;
+}
+
+/*
+disabled look for disabled choices in the results dropdown
+*/
+.select2-results .select2-disabled.select2-highlighted {
+ color: #666;
+ background: #f4f4f4;
+ display: list-item;
+ cursor: default;
+}
+.select2-results .select2-disabled {
+ background: #f4f4f4;
+ display: list-item;
+ cursor: default;
+}
+
+.select2-results .select2-selected {
+ display: none;
+}
+
+.select2-more-results.select2-active {
+ background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%;
+}
+
+.select2-more-results {
+ background: #f4f4f4;
+ display: list-item;
+}
+
+/* disabled styles */
+
+.select2-container.select2-container-disabled .select2-choice {
+ background-color: #f4f4f4;
+ background-image: none;
+ border: 1px solid #ddd;
+ cursor: default;
+}
+
+.select2-container.select2-container-disabled .select2-choice .select2-arrow {
+ background-color: #f4f4f4;
+ background-image: none;
+ border-left: 0;
+}
+
+.select2-container.select2-container-disabled .select2-choice abbr {
+ display: none;
+}
+
+
+/* multiselect */
+
+.select2-container-multi .select2-choices {
+ height: auto !important;
+ height: 1%;
+ margin: 0;
+ padding: 0;
+ position: relative;
+
+ border: 1px solid #aaa;
+ cursor: text;
+ overflow: hidden;
+
+ background-color: #fff;
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eee), color-stop(15%, #fff));
+ background-image: -webkit-linear-gradient(top, #eee 1%, #fff 15%);
+ background-image: -moz-linear-gradient(top, #eee 1%, #fff 15%);
+ background-image: linear-gradient(top, #eee 1%, #fff 15%);
+}
+
+.select2-locked {
+ padding: 3px 5px 3px 5px !important;
+}
+
+.select2-container-multi .select2-choices {
+ min-height: 26px;
+}
+
+.select2-container-multi.select2-container-active .select2-choices {
+ border: 1px solid #5897fb;
+ outline: none;
+
+ -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3);
+ box-shadow: 0 0 5px rgba(0, 0, 0, .3);
+}
+.select2-container-multi .select2-choices li {
+ float: left;
+ list-style: none;
+}
+.select2-container-multi .select2-choices .select2-search-field {
+ margin: 0;
+ padding: 0;
+ white-space: nowrap;
+}
+
+.select2-container-multi .select2-choices .select2-search-field input {
+ padding: 5px;
+ margin: 1px 0;
+
+ font-family: sans-serif;
+ font-size: 100%;
+ color: #666;
+ outline: 0;
+ border: 0;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+ background: transparent !important;
+}
+
+.select2-container-multi .select2-choices .select2-search-field input.select2-active {
+ background: #fff url('select2-spinner.gif') no-repeat 100% !important;
+}
+
+.select2-default {
+ color: #999 !important;
+}
+
+.select2-container-multi .select2-choices .select2-search-choice {
+ padding: 3px 5px 3px 18px;
+ margin: 3px 0 3px 5px;
+ position: relative;
+
+ line-height: 13px;
+ color: #333;
+ cursor: default;
+ border: 1px solid #aaaaaa;
+
+ border-radius: 3px;
+
+ -webkit-box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05);
+ box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05);
+
+ background-clip: padding-box;
+
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
+ background-color: #e4e4e4;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0);
+ background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eee));
+ background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
+ background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
+ background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
+}
+.select2-container-multi .select2-choices .select2-search-choice .select2-chosen {
+ cursor: default;
+}
+.select2-container-multi .select2-choices .select2-search-choice-focus {
+ background: #d4d4d4;
+}
+
+.select2-search-choice-close {
+ display: block;
+ width: 12px;
+ height: 13px;
+ position: absolute;
+ right: 3px;
+ top: 4px;
+
+ font-size: 1px;
+ outline: none;
+ background: url('select2.png') right top no-repeat;
+}
+
+.select2-container-multi .select2-search-choice-close {
+ left: 3px;
+}
+
+.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover {
+ background-position: right -11px;
+}
+.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close {
+ background-position: right -11px;
+}
+
+/* disabled styles */
+.select2-container-multi.select2-container-disabled .select2-choices {
+ background-color: #f4f4f4;
+ background-image: none;
+ border: 1px solid #ddd;
+ cursor: default;
+}
+
+.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice {
+ padding: 3px 5px 3px 5px;
+ border: 1px solid #ddd;
+ background-image: none;
+ background-color: #f4f4f4;
+}
+
+.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none;
+ background: none;
+}
+/* end multiselect */
+
+
+.select2-result-selectable .select2-match,
+.select2-result-unselectable .select2-match {
+ text-decoration: underline;
+}
+
+.select2-offscreen, .select2-offscreen:focus {
+ clip: rect(0 0 0 0) !important;
+ width: 1px !important;
+ height: 1px !important;
+ border: 0 !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ overflow: hidden !important;
+ position: absolute !important;
+ outline: 0 !important;
+ left: 0px !important;
+ top: 0px !important;
+}
+
+.select2-display-none {
+ display: none;
+}
+
+.select2-measure-scrollbar {
+ position: absolute;
+ top: -10000px;
+ left: -10000px;
+ width: 100px;
+ height: 100px;
+ overflow: scroll;
+}
+/* Retina-ize icons */
+
+@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) {
+ .select2-search input, .select2-search-choice-close, .select2-container .select2-choice abbr, .select2-container .select2-choice .select2-arrow b {
+ background-image: url('select2x2.png') !important;
+ background-repeat: no-repeat !important;
+ background-size: 60px 40px !important;
+ }
+ .select2-search input {
+ background-position: 100% -21px !important;
+ }
+}
diff --git a/js/app/app.coffee b/js/app/app.coffee
index fa304f44..823eea1d 100644
--- a/js/app/app.coffee
+++ b/js/app/app.coffee
@@ -19,7 +19,8 @@ You should have received a copy of the GNU Affero General Public
License along with this library. If not, see .
###
-angular.module('Tasks',['OC','ngRoute','ngAnimate','ui.bootstrap'])
+angular.module('Tasks',['OC','ngRoute','ngAnimate','ui.bootstrap','ui.select',
+ 'ngSanitize'])
.config ['$provide','$routeProvider', '$interpolateProvider',
($provide, $routeProvider, $interpolateProvider) ->
$provide.value 'Config', config =
diff --git a/js/app/controllers/detailscontroller.coffee b/js/app/controllers/detailscontroller.coffee
index 617e6ec5..2548ab6c 100644
--- a/js/app/controllers/detailscontroller.coffee
+++ b/js/app/controllers/detailscontroller.coffee
@@ -319,6 +319,13 @@ $timeout, $routeParams, SettingsModel, Loading) ->
input: t('tasks','Add a comment')
}
+ @_$scope.availableCategories = []
+
+ @_$scope.addCategory = (category, model) ->
+ _tasksbusinesslayer.addCategory(_$scope.route.taskID, category)
+
+ @_$scope.removeCategory = (category, model) ->
+ _tasksbusinesslayer.removeCategory(_$scope.route.taskID, category)
return new DetailsController($scope, $window, TasksModel,
TasksBusinessLayer, $route, $location, $timeout, $routeParams,
diff --git a/js/app/services/businesslayer/tasksbusinesslayer.coffee b/js/app/services/businesslayer/tasksbusinesslayer.coffee
index ac3a4dd1..824f934f 100644
--- a/js/app/services/businesslayer/tasksbusinesslayer.coffee
+++ b/js/app/services/businesslayer/tasksbusinesslayer.coffee
@@ -355,6 +355,12 @@ angular.module('Tasks').factory 'TasksBusinessLayer',
getCompletedTasks: (listID) ->
@_persistence.getTasks('completed', listID)
+ addCategory: (taskID, category) ->
+ @_persistence.addCategory(taskID, category)
+
+ removeCategory: (taskID, category) ->
+ @_persistence.removeCategory(taskID, category)
+
return new TasksBusinessLayer(TasksModel, Persistence)
]
\ No newline at end of file
diff --git a/js/app/services/persistence.coffee b/js/app/services/persistence.coffee
index 78c6b444..b6966702 100644
--- a/js/app/services/persistence.coffee
+++ b/js/app/services/persistence.coffee
@@ -356,6 +356,24 @@ angular.module('Tasks').factory 'Persistence',
@_request.post '/apps/tasks/tasks/{taskID}/comment/
{commentID}/delete', params
+ addCategory: (taskID, category) ->
+ params =
+ routeParams:
+ taskID: taskID
+ data:
+ category: category
+
+ @_request.post '/apps/tasks/tasks/{taskID}/category/add', params
+
+ removeCategory: (taskID, category) ->
+ params =
+ routeParams:
+ taskID: taskID
+ data:
+ category: category
+
+ @_request.post '/apps/tasks/tasks/{taskID}/category/remove', params
+
return new Persistence(Request, Loading, $rootScope)
]
\ No newline at end of file
diff --git a/js/public/app.js b/js/public/app.js
index 3d7ea6f8..6de7e348 100644
--- a/js/public/app.js
+++ b/js/public/app.js
@@ -12,7 +12,7 @@
(function() {
- angular.module('Tasks', ['OC', 'ngRoute', 'ngAnimate', 'ui.bootstrap']).config([
+ angular.module('Tasks', ['OC', 'ngRoute', 'ngAnimate', 'ui.bootstrap', 'ui.select', 'ngSanitize']).config([
'$provide', '$routeProvider', '$interpolateProvider', function($provide, $routeProvider, $interpolateProvider) {
var config;
$provide.value('Config', config = {
@@ -769,6 +769,13 @@
input: t('tasks', 'Add a comment')
};
};
+ this._$scope.availableCategories = [];
+ this._$scope.addCategory = function(category, model) {
+ return _tasksbusinesslayer.addCategory(_$scope.route.taskID, category);
+ };
+ this._$scope.removeCategory = function(category, model) {
+ return _tasksbusinesslayer.removeCategory(_$scope.route.taskID, category);
+ };
}
return DetailsController;
@@ -1850,6 +1857,14 @@
return this._persistence.getTasks('completed', listID);
};
+ TasksBusinessLayer.prototype.addCategory = function(taskID, category) {
+ return this._persistence.addCategory(taskID, category);
+ };
+
+ TasksBusinessLayer.prototype.removeCategory = function(taskID, category) {
+ return this._persistence.removeCategory(taskID, category);
+ };
+
return TasksBusinessLayer;
})();
@@ -2943,6 +2958,32 @@
{commentID}/delete', params);
};
+ Persistence.prototype.addCategory = function(taskID, category) {
+ var params;
+ params = {
+ routeParams: {
+ taskID: taskID
+ },
+ data: {
+ category: category
+ }
+ };
+ return this._request.post('/apps/tasks/tasks/{taskID}/category/add', params);
+ };
+
+ Persistence.prototype.removeCategory = function(taskID, category) {
+ var params;
+ params = {
+ routeParams: {
+ taskID: taskID
+ },
+ data: {
+ category: category
+ }
+ };
+ return this._request.post('/apps/tasks/tasks/{taskID}/category/remove', params);
+ };
+
return Persistence;
})();
diff --git a/js/vendor/angularjs/angular-sanitize.js b/js/vendor/angularjs/angular-sanitize.js
new file mode 100644
index 00000000..61c50899
--- /dev/null
+++ b/js/vendor/angularjs/angular-sanitize.js
@@ -0,0 +1,679 @@
+/**
+ * @license AngularJS v1.3.15
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
+ * License: MIT
+ */
+(function(window, angular, undefined) {'use strict';
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Any commits to this file should be reviewed with security in mind. *
+ * Changes to this file can potentially create security vulnerabilities. *
+ * An approval from 2 Core members with history of modifying *
+ * this file is required. *
+ * *
+ * Does the change somehow allow for arbitrary javascript to be executed? *
+ * Or allows for someone to change the prototype of built-in objects? *
+ * Or gives undesired access to variables likes document or window? *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+var $sanitizeMinErr = angular.$$minErr('$sanitize');
+
+/**
+ * @ngdoc module
+ * @name ngSanitize
+ * @description
+ *
+ * # ngSanitize
+ *
+ * The `ngSanitize` module provides functionality to sanitize HTML.
+ *
+ *
+ *
+ *
+ * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
+ */
+
+/*
+ * HTML Parser By Misko Hevery (misko@hevery.com)
+ * based on: HTML Parser By John Resig (ejohn.org)
+ * Original code by Erik Arvidsson, Mozilla Public License
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
+ *
+ * // Use like so:
+ * htmlParser(htmlString, {
+ * start: function(tag, attrs, unary) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * });
+ *
+ */
+
+
+/**
+ * @ngdoc service
+ * @name $sanitize
+ * @kind function
+ *
+ * @description
+ * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
+ * then serialized back to properly escaped html string. This means that no unsafe input can make
+ * it into the returned string, however, since our parser is more strict than a typical browser
+ * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
+ * browser, won't make it through the sanitizer. The input may also contain SVG markup.
+ * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
+ * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
+ *
+ * @param {string} html HTML input.
+ * @returns {string} Sanitized HTML.
+ *
+ * @example
+
+
+
+
+ Snippet:
+
+
+ Directive
+ How
+ Source
+ Rendered
+
+
+ ng-bind-html
+ Automatically uses $sanitize
+ <div ng-bind-html="snippet"> </div>
+
+
+
+ ng-bind-html
+ Bypass $sanitize by explicitly trusting the dangerous value
+
+ <div ng-bind-html="deliberatelyTrustDangerousSnippet()">
+</div>
+
+
+
+
+ ng-bind
+ Automatically escapes
+ <div ng-bind="snippet"> </div>
+
+
+
+
+
+
+ it('should sanitize the html snippet by default', function() {
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
+ toBe('an html\nclick here \nsnippet
');
+ });
+
+ it('should inline raw snippet if bound to a trusted value', function() {
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
+ toBe("an html\n" +
+ "click here \n" +
+ "snippet
");
+ });
+
+ it('should escape snippet without any filter', function() {
+ expect(element(by.css('#bind-default div')).getInnerHtml()).
+ toBe("<p style=\"color:blue\">an html\n" +
+ "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
+ "snippet</p>");
+ });
+
+ it('should update', function() {
+ element(by.model('snippet')).clear();
+ element(by.model('snippet')).sendKeys('new text ');
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
+ toBe('new text ');
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
+ 'new text ');
+ expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
+ "new <b onclick=\"alert(1)\">text</b>");
+ });
+
+
+ */
+function $SanitizeProvider() {
+ this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
+ return function(html) {
+ var buf = [];
+ htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
+ return !/^unsafe/.test($$sanitizeUri(uri, isImage));
+ }));
+ return buf.join('');
+ };
+ }];
+}
+
+function sanitizeText(chars) {
+ var buf = [];
+ var writer = htmlSanitizeWriter(buf, angular.noop);
+ writer.chars(chars);
+ return buf.join('');
+}
+
+
+// Regular Expressions for parsing tags and attributes
+var START_TAG_REGEXP =
+ /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
+ END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
+ ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
+ BEGIN_TAG_REGEXP = /^,
+ BEGING_END_TAGE_REGEXP = /^<\//,
+ COMMENT_REGEXP = //g,
+ DOCTYPE_REGEXP = /]*?)>/i,
+ CDATA_REGEXP = //g,
+ SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
+ // Match everything outside of normal chars and " (quote character)
+ NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
+
+
+// Good source of info about elements and attributes
+// http://dev.w3.org/html5/spec/Overview.html#semantics
+// http://simon.html5.org/html-elements
+
+// Safe Void Elements - HTML5
+// http://dev.w3.org/html5/spec/Overview.html#void-elements
+var voidElements = makeMap("area,br,col,hr,img,wbr");
+
+// Elements that you can, intentionally, leave open (and which close themselves)
+// http://dev.w3.org/html5/spec/Overview.html#optional-tags
+var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
+ optionalEndTagInlineElements = makeMap("rp,rt"),
+ optionalEndTagElements = angular.extend({},
+ optionalEndTagInlineElements,
+ optionalEndTagBlockElements);
+
+// Safe Block Elements - HTML5
+var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
+ "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
+ "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
+
+// Inline Elements - HTML5
+var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
+ "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
+ "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
+
+// SVG Elements
+// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
+var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," +
+ "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," +
+ "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," +
+ "stop,svg,switch,text,title,tspan,use");
+
+// Special Elements (can contain anything)
+var specialElements = makeMap("script,style");
+
+var validElements = angular.extend({},
+ voidElements,
+ blockElements,
+ inlineElements,
+ optionalEndTagElements,
+ svgElements);
+
+//Attributes that have href and hence need to be sanitized
+var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href");
+
+var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
+ 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
+ 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
+ 'scope,scrolling,shape,size,span,start,summary,target,title,type,' +
+ 'valign,value,vspace,width');
+
+// SVG attributes (without "id" and "name" attributes)
+// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
+var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
+ 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' +
+ 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' +
+ 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' +
+ 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' +
+ 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' +
+ 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' +
+ 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' +
+ 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' +
+ 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' +
+ 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' +
+ 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' +
+ 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' +
+ 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' +
+ 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' +
+ 'zoomAndPan');
+
+var validAttrs = angular.extend({},
+ uriAttrs,
+ svgAttrs,
+ htmlAttrs);
+
+function makeMap(str) {
+ var obj = {}, items = str.split(','), i;
+ for (i = 0; i < items.length; i++) obj[items[i]] = true;
+ return obj;
+}
+
+
+/**
+ * @example
+ * htmlParser(htmlString, {
+ * start: function(tag, attrs, unary) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * });
+ *
+ * @param {string} html string
+ * @param {object} handler
+ */
+function htmlParser(html, handler) {
+ if (typeof html !== 'string') {
+ if (html === null || typeof html === 'undefined') {
+ html = '';
+ } else {
+ html = '' + html;
+ }
+ }
+ var index, chars, match, stack = [], last = html, text;
+ stack.last = function() { return stack[stack.length - 1]; };
+
+ while (html) {
+ text = '';
+ chars = true;
+
+ // Make sure we're not in a script or style element
+ if (!stack.last() || !specialElements[stack.last()]) {
+
+ // Comment
+ if (html.indexOf("", index) === index) {
+ if (handler.comment) handler.comment(html.substring(4, index));
+ html = html.substring(index + 3);
+ chars = false;
+ }
+ // DOCTYPE
+ } else if (DOCTYPE_REGEXP.test(html)) {
+ match = html.match(DOCTYPE_REGEXP);
+
+ if (match) {
+ html = html.replace(match[0], '');
+ chars = false;
+ }
+ // end tag
+ } else if (BEGING_END_TAGE_REGEXP.test(html)) {
+ match = html.match(END_TAG_REGEXP);
+
+ if (match) {
+ html = html.substring(match[0].length);
+ match[0].replace(END_TAG_REGEXP, parseEndTag);
+ chars = false;
+ }
+
+ // start tag
+ } else if (BEGIN_TAG_REGEXP.test(html)) {
+ match = html.match(START_TAG_REGEXP);
+
+ if (match) {
+ // We only have a valid start-tag if there is a '>'.
+ if (match[4]) {
+ html = html.substring(match[0].length);
+ match[0].replace(START_TAG_REGEXP, parseStartTag);
+ }
+ chars = false;
+ } else {
+ // no ending tag found --- this piece should be encoded as an entity.
+ text += '<';
+ html = html.substring(1);
+ }
+ }
+
+ if (chars) {
+ index = html.indexOf("<");
+
+ text += index < 0 ? html : html.substring(0, index);
+ html = index < 0 ? "" : html.substring(index);
+
+ if (handler.chars) handler.chars(decodeEntities(text));
+ }
+
+ } else {
+ // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w].
+ html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
+ function(all, text) {
+ text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
+
+ if (handler.chars) handler.chars(decodeEntities(text));
+
+ return "";
+ });
+
+ parseEndTag("", stack.last());
+ }
+
+ if (html == last) {
+ throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
+ "of html: {0}", html);
+ }
+ last = html;
+ }
+
+ // Clean up any remaining tags
+ parseEndTag();
+
+ function parseStartTag(tag, tagName, rest, unary) {
+ tagName = angular.lowercase(tagName);
+ if (blockElements[tagName]) {
+ while (stack.last() && inlineElements[stack.last()]) {
+ parseEndTag("", stack.last());
+ }
+ }
+
+ if (optionalEndTagElements[tagName] && stack.last() == tagName) {
+ parseEndTag("", tagName);
+ }
+
+ unary = voidElements[tagName] || !!unary;
+
+ if (!unary)
+ stack.push(tagName);
+
+ var attrs = {};
+
+ rest.replace(ATTR_REGEXP,
+ function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
+ var value = doubleQuotedValue
+ || singleQuotedValue
+ || unquotedValue
+ || '';
+
+ attrs[name] = decodeEntities(value);
+ });
+ if (handler.start) handler.start(tagName, attrs, unary);
+ }
+
+ function parseEndTag(tag, tagName) {
+ var pos = 0, i;
+ tagName = angular.lowercase(tagName);
+ if (tagName)
+ // Find the closest opened tag of the same type
+ for (pos = stack.length - 1; pos >= 0; pos--)
+ if (stack[pos] == tagName)
+ break;
+
+ if (pos >= 0) {
+ // Close all the open elements, up the stack
+ for (i = stack.length - 1; i >= pos; i--)
+ if (handler.end) handler.end(stack[i]);
+
+ // Remove the open elements from the stack
+ stack.length = pos;
+ }
+ }
+}
+
+var hiddenPre=document.createElement("pre");
+/**
+ * decodes all entities into regular string
+ * @param value
+ * @returns {string} A string with decoded entities.
+ */
+function decodeEntities(value) {
+ if (!value) { return ''; }
+
+ hiddenPre.innerHTML = value.replace(//g, '>');
+}
+
+/**
+ * create an HTML/XML writer which writes to buffer
+ * @param {Array} buf use buf.jain('') to get out sanitized html string
+ * @returns {object} in the form of {
+ * start: function(tag, attrs, unary) {},
+ * end: function(tag) {},
+ * chars: function(text) {},
+ * comment: function(text) {}
+ * }
+ */
+function htmlSanitizeWriter(buf, uriValidator) {
+ var ignore = false;
+ var out = angular.bind(buf, buf.push);
+ return {
+ start: function(tag, attrs, unary) {
+ tag = angular.lowercase(tag);
+ if (!ignore && specialElements[tag]) {
+ ignore = tag;
+ }
+ if (!ignore && validElements[tag] === true) {
+ out('<');
+ out(tag);
+ angular.forEach(attrs, function(value, key) {
+ var lkey=angular.lowercase(key);
+ var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
+ if (validAttrs[lkey] === true &&
+ (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
+ out(' ');
+ out(key);
+ out('="');
+ out(encodeEntities(value));
+ out('"');
+ }
+ });
+ out(unary ? '/>' : '>');
+ }
+ },
+ end: function(tag) {
+ tag = angular.lowercase(tag);
+ if (!ignore && validElements[tag] === true) {
+ out('');
+ out(tag);
+ out('>');
+ }
+ if (tag == ignore) {
+ ignore = false;
+ }
+ },
+ chars: function(chars) {
+ if (!ignore) {
+ out(encodeEntities(chars));
+ }
+ }
+ };
+}
+
+
+// define ngSanitize module and register $sanitize service
+angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
+
+/* global sanitizeText: false */
+
+/**
+ * @ngdoc filter
+ * @name linky
+ * @kind function
+ *
+ * @description
+ * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
+ * plain email address links.
+ *
+ * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
+ *
+ * @param {string} text Input text.
+ * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
+ * @returns {string} Html-linkified text.
+ *
+ * @usage
+
+ *
+ * @example
+
+
+
+
+ Snippet:
+
+
+ Filter
+ Source
+ Rendered
+
+
+ linky filter
+
+ <div ng-bind-html="snippet | linky"> </div>
+
+
+
+
+
+
+ linky target
+
+ <div ng-bind-html="snippetWithTarget | linky:'_blank'"> </div>
+
+
+
+
+
+
+ no filter
+ <div ng-bind="snippet"> </div>
+
+
+
+
+
+ it('should linkify the snippet with urls', function() {
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
+ toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
+ });
+
+ it('should not linkify snippet without the linky filter', function() {
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
+ toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
+ expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
+ });
+
+ it('should update', function() {
+ element(by.model('snippet')).clear();
+ element(by.model('snippet')).sendKeys('new http://link.');
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
+ toBe('new http://link.');
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
+ .toBe('new http://link.');
+ });
+
+ it('should work with the target property', function() {
+ expect(element(by.id('linky-target')).
+ element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
+ toBe('http://angularjs.org/');
+ expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
+ });
+
+
+ */
+angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
+ var LINKY_URL_REGEXP =
+ /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/,
+ MAILTO_REGEXP = /^mailto:/;
+
+ return function(text, target) {
+ if (!text) return text;
+ var match;
+ var raw = text;
+ var html = [];
+ var url;
+ var i;
+ while ((match = raw.match(LINKY_URL_REGEXP))) {
+ // We can not end in these as they are sometimes found at the end of the sentence
+ url = match[0];
+ // if we did not match ftp/http/www/mailto then assume mailto
+ if (!match[2] && !match[4]) {
+ url = (match[3] ? 'http://' : 'mailto:') + url;
+ }
+ i = match.index;
+ addText(raw.substr(0, i));
+ addLink(url, match[0].replace(MAILTO_REGEXP, ''));
+ raw = raw.substring(i + match[0].length);
+ }
+ addText(raw);
+ return $sanitize(html.join(''));
+
+ function addText(text) {
+ if (!text) {
+ return;
+ }
+ html.push(sanitizeText(text));
+ }
+
+ function addLink(url, text) {
+ html.push('
');
+ addText(text);
+ html.push(' ');
+ }
+ };
+}]);
+
+
+})(window, window.angular);
diff --git a/js/vendor/angularjs/angular-sanitize.min.js b/js/vendor/angularjs/angular-sanitize.min.js
new file mode 100644
index 00000000..f6879953
--- /dev/null
+++ b/js/vendor/angularjs/angular-sanitize.min.js
@@ -0,0 +1,16 @@
+/*
+ AngularJS v1.3.15
+ (c) 2010-2014 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(n,h,p){'use strict';function E(a){var e=[];r(e,h.noop).chars(a);return e.join("")}function g(a){var e={};a=a.split(",");var d;for(d=0;d
=c;d--)e.end&&e.end(f[d]);f.length=c}}"string"!==typeof a&&(a=null===a||"undefined"===typeof a?"":""+a);var b,k,f=[],m=a,l;for(f.last=function(){return f[f.length-1]};a;){l="";k=!0;if(f.last()&&w[f.last()])a=a.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*"+f.last()+"[^>]*>","i"),function(a,b){b=b.replace(H,"$1").replace(I,"$1");e.chars&&e.chars(q(b));return""}),c("",f.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",b)===b&&(e.comment&&
+e.comment(a.substring(4,b)),a=a.substring(b+3),k=!1);else if(x.test(a)){if(b=a.match(x))a=a.replace(b[0],""),k=!1}else if(J.test(a)){if(b=a.match(y))a=a.substring(b[0].length),b[0].replace(y,c),k=!1}else K.test(a)&&((b=a.match(z))?(b[4]&&(a=a.substring(b[0].length),b[0].replace(z,d)),k=!1):(l+="<",a=a.substring(1)));k&&(b=a.indexOf("<"),l+=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),e.chars&&e.chars(q(l)))}if(a==m)throw L("badparse",a);m=a}c()}function q(a){if(!a)return"";A.innerHTML=a.replace(//g,">")}function r(a,e){var d=!1,c=h.bind(a,a.push);return{start:function(a,k,f){a=h.lowercase(a);!d&&w[a]&&(d=a);d||!0!==C[a]||(c("<"),c(a),h.forEach(k,function(d,f){var k=h.lowercase(f),g="img"===a&&"src"===k||"background"===
+k;!0!==O[k]||!0===D[k]&&!e(d,g)||(c(" "),c(f),c('="'),c(B(d)),c('"'))}),c(f?"/>":">"))},end:function(a){a=h.lowercase(a);d||!0!==C[a]||(c(""),c(a),c(">"));a==d&&(d=!1)},chars:function(a){d||c(B(a))}}}var L=h.$$minErr("$sanitize"),z=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,y=/^<\/\s*([\w:-]+)[^>]*>/,G=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,K=/^,J=/^<\//,H=/\x3c!--(.*?)--\x3e/g,x=/]*?)>/i,
+I=/"\u201d\u2019]/,d=/^mailto:/;return function(c,b){function k(a){a&&g.push(E(a))}function f(a,c){g.push("');k(c);g.push(" ")}if(!c)return c;for(var m,l=c,g=[],n,p;m=l.match(e);)n=m[0],m[2]||m[4]||(n=(m[3]?"http://":"mailto:")+n),p=m.index,k(l.substr(0,p)),f(n,m[0].replace(d,"")),l=l.substring(p+m[0].length);k(l);return a(g.join(""))}}])})(window,window.angular);
+//# sourceMappingURL=angular-sanitize.min.js.map
diff --git a/js/vendor/angularui/ui-select/select.js b/js/vendor/angularui/ui-select/select.js
new file mode 100644
index 00000000..aa0e5ae5
--- /dev/null
+++ b/js/vendor/angularui/ui-select/select.js
@@ -0,0 +1,1746 @@
+/*!
+ * ui-select
+ * http://github.com/angular-ui/ui-select
+ * Version: 0.11.2 - 2015-03-17T04:08:46.474Z
+ * License: MIT
+ */
+
+
+(function () {
+"use strict";
+
+var KEY = {
+ TAB: 9,
+ ENTER: 13,
+ ESC: 27,
+ SPACE: 32,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ SHIFT: 16,
+ CTRL: 17,
+ ALT: 18,
+ PAGE_UP: 33,
+ PAGE_DOWN: 34,
+ HOME: 36,
+ END: 35,
+ BACKSPACE: 8,
+ DELETE: 46,
+ COMMAND: 91,
+
+ MAP: { 91 : "COMMAND", 8 : "BACKSPACE" , 9 : "TAB" , 13 : "ENTER" , 16 : "SHIFT" , 17 : "CTRL" , 18 : "ALT" , 19 : "PAUSEBREAK" , 20 : "CAPSLOCK" , 27 : "ESC" , 32 : "SPACE" , 33 : "PAGE_UP", 34 : "PAGE_DOWN" , 35 : "END" , 36 : "HOME" , 37 : "LEFT" , 38 : "UP" , 39 : "RIGHT" , 40 : "DOWN" , 43 : "+" , 44 : "PRINTSCREEN" , 45 : "INSERT" , 46 : "DELETE", 48 : "0" , 49 : "1" , 50 : "2" , 51 : "3" , 52 : "4" , 53 : "5" , 54 : "6" , 55 : "7" , 56 : "8" , 57 : "9" , 59 : ";", 61 : "=" , 65 : "A" , 66 : "B" , 67 : "C" , 68 : "D" , 69 : "E" , 70 : "F" , 71 : "G" , 72 : "H" , 73 : "I" , 74 : "J" , 75 : "K" , 76 : "L", 77 : "M" , 78 : "N" , 79 : "O" , 80 : "P" , 81 : "Q" , 82 : "R" , 83 : "S" , 84 : "T" , 85 : "U" , 86 : "V" , 87 : "W" , 88 : "X" , 89 : "Y" , 90 : "Z", 96 : "0" , 97 : "1" , 98 : "2" , 99 : "3" , 100 : "4" , 101 : "5" , 102 : "6" , 103 : "7" , 104 : "8" , 105 : "9", 106 : "*" , 107 : "+" , 109 : "-" , 110 : "." , 111 : "/", 112 : "F1" , 113 : "F2" , 114 : "F3" , 115 : "F4" , 116 : "F5" , 117 : "F6" , 118 : "F7" , 119 : "F8" , 120 : "F9" , 121 : "F10" , 122 : "F11" , 123 : "F12", 144 : "NUMLOCK" , 145 : "SCROLLLOCK" , 186 : ";" , 187 : "=" , 188 : "," , 189 : "-" , 190 : "." , 191 : "/" , 192 : "`" , 219 : "[" , 220 : "\\" , 221 : "]" , 222 : "'"
+ },
+
+ isControl: function (e) {
+ var k = e.which;
+ switch (k) {
+ case KEY.COMMAND:
+ case KEY.SHIFT:
+ case KEY.CTRL:
+ case KEY.ALT:
+ return true;
+ }
+
+ if (e.metaKey) return true;
+
+ return false;
+ },
+ isFunctionKey: function (k) {
+ k = k.which ? k.which : k;
+ return k >= 112 && k <= 123;
+ },
+ isVerticalMovement: function (k){
+ return ~[KEY.UP, KEY.DOWN].indexOf(k);
+ },
+ isHorizontalMovement: function (k){
+ return ~[KEY.LEFT,KEY.RIGHT,KEY.BACKSPACE,KEY.DELETE].indexOf(k);
+ }
+ };
+
+/**
+ * Add querySelectorAll() to jqLite.
+ *
+ * jqLite find() is limited to lookups by tag name.
+ * TODO This will change with future versions of AngularJS, to be removed when this happens
+ *
+ * See jqLite.find - why not use querySelectorAll? https://github.com/angular/angular.js/issues/3586
+ * See feat(jqLite): use querySelectorAll instead of getElementsByTagName in jqLite.find https://github.com/angular/angular.js/pull/3598
+ */
+if (angular.element.prototype.querySelectorAll === undefined) {
+ angular.element.prototype.querySelectorAll = function(selector) {
+ return angular.element(this[0].querySelectorAll(selector));
+ };
+}
+
+/**
+ * Add closest() to jqLite.
+ */
+if (angular.element.prototype.closest === undefined) {
+ angular.element.prototype.closest = function( selector) {
+ var elem = this[0];
+ var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector;
+
+ while (elem) {
+ if (matchesSelector.bind(elem)(selector)) {
+ return elem;
+ } else {
+ elem = elem.parentElement;
+ }
+ }
+ return false;
+ };
+}
+
+var latestId = 0;
+
+var uis = angular.module('ui.select', [])
+
+.constant('uiSelectConfig', {
+ theme: 'bootstrap',
+ searchEnabled: true,
+ sortable: false,
+ placeholder: '', // Empty by default, like HTML tag
+ refreshDelay: 1000, // In milliseconds
+ closeOnSelect: true,
+ generateId: function() {
+ return latestId++;
+ },
+ appendToBody: false
+})
+
+// See Rename minErr and make it accessible from outside https://github.com/angular/angular.js/issues/6913
+.service('uiSelectMinErr', function() {
+ var minErr = angular.$$minErr('ui.select');
+ return function() {
+ var error = minErr.apply(this, arguments);
+ var message = error.message.replace(new RegExp('\nhttp://errors.angularjs.org/.*'), '');
+ return new Error(message);
+ };
+})
+
+// Recreates old behavior of ng-transclude. Used internally.
+.directive('uisTranscludeAppend', function () {
+ return {
+ link: function (scope, element, attrs, ctrl, transclude) {
+ transclude(scope, function (clone) {
+ element.append(clone);
+ });
+ }
+ };
+})
+
+/**
+ * Highlights text that matches $select.search.
+ *
+ * Taken from AngularUI Bootstrap Typeahead
+ * See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L340
+ */
+.filter('highlight', function() {
+ function escapeRegexp(queryToEscape) {
+ return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
+ }
+
+ return function(matchItem, query) {
+ return query && matchItem ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$& ') : matchItem;
+ };
+})
+
+/**
+ * A read-only equivalent of jQuery's offset function: http://api.jquery.com/offset/
+ *
+ * Taken from AngularUI Bootstrap Position:
+ * See https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js#L70
+ */
+.factory('uisOffset',
+ ['$document', '$window',
+ function ($document, $window) {
+
+ return function(element) {
+ var boundingClientRect = element[0].getBoundingClientRect();
+ return {
+ width: boundingClientRect.width || element.prop('offsetWidth'),
+ height: boundingClientRect.height || element.prop('offsetHeight'),
+ top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
+ left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
+ };
+ };
+}]);
+
+uis.directive('uiSelectChoices',
+ ['uiSelectConfig', 'uisRepeatParser', 'uiSelectMinErr', '$compile',
+ function(uiSelectConfig, RepeatParser, uiSelectMinErr, $compile) {
+
+ return {
+ restrict: 'EA',
+ require: '^uiSelect',
+ replace: true,
+ transclude: true,
+ templateUrl: function(tElement) {
+ // Gets theme attribute from parent (ui-select)
+ var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
+ return theme + '/choices.tpl.html';
+ },
+
+ compile: function(tElement, tAttrs) {
+
+ if (!tAttrs.repeat) throw uiSelectMinErr('repeat', "Expected 'repeat' expression.");
+
+ return function link(scope, element, attrs, $select, transcludeFn) {
+
+ // var repeat = RepeatParser.parse(attrs.repeat);
+ var groupByExp = attrs.groupBy;
+
+ $select.parseRepeatAttr(attrs.repeat, groupByExp); //Result ready at $select.parserResult
+
+ $select.disableChoiceExpression = attrs.uiDisableChoice;
+ $select.onHighlightCallback = attrs.onHighlight;
+
+ if(groupByExp) {
+ var groups = element.querySelectorAll('.ui-select-choices-group');
+ if (groups.length !== 1) throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-group but got '{0}'.", groups.length);
+ groups.attr('ng-repeat', RepeatParser.getGroupNgRepeatExpression());
+ }
+
+ var choices = element.querySelectorAll('.ui-select-choices-row');
+ if (choices.length !== 1) {
+ throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", choices.length);
+ }
+
+ choices.attr('ng-repeat', RepeatParser.getNgRepeatExpression($select.parserResult.itemName, '$select.items', $select.parserResult.trackByExp, groupByExp))
+ .attr('ng-if', '$select.open') //Prevent unnecessary watches when dropdown is closed
+ .attr('ng-mouseenter', '$select.setActiveItem('+$select.parserResult.itemName +')')
+ .attr('ng-click', '$select.select(' + $select.parserResult.itemName + ',false,$event)');
+
+ var rowsInner = element.querySelectorAll('.ui-select-choices-row-inner');
+ if (rowsInner.length !== 1) throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row-inner but got '{0}'.", rowsInner.length);
+ rowsInner.attr('uis-transclude-append', ''); //Adding uisTranscludeAppend directive to row element after choices element has ngRepeat
+
+ $compile(element, transcludeFn)(scope); //Passing current transcludeFn to be able to append elements correctly from uisTranscludeAppend
+
+ scope.$watch('$select.search', function(newValue) {
+ if(newValue && !$select.open && $select.multiple) $select.activate(false, true);
+ $select.activeIndex = $select.tagging.isActivated ? -1 : 0;
+ $select.refresh(attrs.refresh);
+ });
+
+ attrs.$observe('refreshDelay', function() {
+ // $eval() is needed otherwise we get a string instead of a number
+ var refreshDelay = scope.$eval(attrs.refreshDelay);
+ $select.refreshDelay = refreshDelay !== undefined ? refreshDelay : uiSelectConfig.refreshDelay;
+ });
+ };
+ }
+ };
+}]);
+
+/**
+ * Contains ui-select "intelligence".
+ *
+ * The goal is to limit dependency on the DOM whenever possible and
+ * put as much logic in the controller (instead of the link functions) as possible so it can be easily tested.
+ */
+uis.controller('uiSelectCtrl',
+ ['$scope', '$element', '$timeout', '$filter', 'uisRepeatParser', 'uiSelectMinErr', 'uiSelectConfig',
+ function($scope, $element, $timeout, $filter, RepeatParser, uiSelectMinErr, uiSelectConfig) {
+
+ var ctrl = this;
+
+ var EMPTY_SEARCH = '';
+
+ ctrl.placeholder = uiSelectConfig.placeholder;
+ ctrl.searchEnabled = uiSelectConfig.searchEnabled;
+ ctrl.sortable = uiSelectConfig.sortable;
+ ctrl.refreshDelay = uiSelectConfig.refreshDelay;
+
+ ctrl.removeSelected = false; //If selected item(s) should be removed from dropdown list
+ ctrl.closeOnSelect = true; //Initialized inside uiSelect directive link function
+ ctrl.search = EMPTY_SEARCH;
+
+ ctrl.activeIndex = 0; //Dropdown of choices
+ ctrl.items = []; //All available choices
+
+ ctrl.open = false;
+ ctrl.focus = false;
+ ctrl.disabled = false;
+ ctrl.selected = undefined;
+
+ ctrl.focusser = undefined; //Reference to input element used to handle focus events
+ ctrl.resetSearchInput = true;
+ ctrl.multiple = undefined; // Initialized inside uiSelect directive link function
+ ctrl.disableChoiceExpression = undefined; // Initialized inside uiSelectChoices directive link function
+ ctrl.tagging = {isActivated: false, fct: undefined};
+ ctrl.taggingTokens = {isActivated: false, tokens: undefined};
+ ctrl.lockChoiceExpression = undefined; // Initialized inside uiSelectMatch directive link function
+ ctrl.clickTriggeredSelect = false;
+ ctrl.$filter = $filter;
+
+ ctrl.searchInput = $element.querySelectorAll('input.ui-select-search');
+ if (ctrl.searchInput.length !== 1) {
+ throw uiSelectMinErr('searchInput', "Expected 1 input.ui-select-search but got '{0}'.", ctrl.searchInput.length);
+ }
+
+ ctrl.isEmpty = function() {
+ return angular.isUndefined(ctrl.selected) || ctrl.selected === null || ctrl.selected === '';
+ };
+
+ // Most of the time the user does not want to empty the search input when in typeahead mode
+ function _resetSearchInput() {
+ if (ctrl.resetSearchInput || (ctrl.resetSearchInput === undefined && uiSelectConfig.resetSearchInput)) {
+ ctrl.search = EMPTY_SEARCH;
+ //reset activeIndex
+ if (ctrl.selected && ctrl.items.length && !ctrl.multiple) {
+ ctrl.activeIndex = ctrl.items.indexOf(ctrl.selected);
+ }
+ }
+ }
+
+ // When the user clicks on ui-select, displays the dropdown list
+ ctrl.activate = function(initSearchValue, avoidReset) {
+ if (!ctrl.disabled && !ctrl.open) {
+ if(!avoidReset) _resetSearchInput();
+
+ $scope.$broadcast('uis:activate');
+
+ ctrl.open = true;
+
+ ctrl.activeIndex = ctrl.activeIndex >= ctrl.items.length ? 0 : ctrl.activeIndex;
+
+ // ensure that the index is set to zero for tagging variants
+ // that where first option is auto-selected
+ if ( ctrl.activeIndex === -1 && ctrl.taggingLabel !== false ) {
+ ctrl.activeIndex = 0;
+ }
+
+ // Give it time to appear before focus
+ $timeout(function() {
+ ctrl.search = initSearchValue || ctrl.search;
+ ctrl.searchInput[0].focus();
+ });
+ }
+ };
+
+ ctrl.findGroupByName = function(name) {
+ return ctrl.groups && ctrl.groups.filter(function(group) {
+ return group.name === name;
+ })[0];
+ };
+
+ ctrl.parseRepeatAttr = function(repeatAttr, groupByExp) {
+ function updateGroups(items) {
+ ctrl.groups = [];
+ angular.forEach(items, function(item) {
+ var groupFn = $scope.$eval(groupByExp);
+ var groupName = angular.isFunction(groupFn) ? groupFn(item) : item[groupFn];
+ var group = ctrl.findGroupByName(groupName);
+ if(group) {
+ group.items.push(item);
+ }
+ else {
+ ctrl.groups.push({name: groupName, items: [item]});
+ }
+ });
+ ctrl.items = [];
+ ctrl.groups.forEach(function(group) {
+ ctrl.items = ctrl.items.concat(group.items);
+ });
+ }
+
+ function setPlainItems(items) {
+ ctrl.items = items;
+ }
+
+ ctrl.setItemsFn = groupByExp ? updateGroups : setPlainItems;
+
+ ctrl.parserResult = RepeatParser.parse(repeatAttr);
+
+ ctrl.isGrouped = !!groupByExp;
+ ctrl.itemProperty = ctrl.parserResult.itemName;
+
+ ctrl.refreshItems = function (data){
+ data = data || ctrl.parserResult.source($scope);
+ var selectedItems = ctrl.selected;
+ //TODO should implement for single mode removeSelected
+ if ((angular.isArray(selectedItems) && !selectedItems.length) || !ctrl.removeSelected) {
+ ctrl.setItemsFn(data);
+ }else{
+ if ( data !== undefined ) {
+ var filteredItems = data.filter(function(i) {return selectedItems.indexOf(i) < 0;});
+ ctrl.setItemsFn(filteredItems);
+ }
+ }
+ };
+
+ // See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L259
+ $scope.$watchCollection(ctrl.parserResult.source, function(items) {
+ if (items === undefined || items === null) {
+ // If the user specifies undefined or null => reset the collection
+ // Special case: items can be undefined if the user did not initialized the collection on the scope
+ // i.e $scope.addresses = [] is missing
+ ctrl.items = [];
+ } else {
+ if (!angular.isArray(items)) {
+ throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items);
+ } else {
+ //Remove already selected items (ex: while searching)
+ //TODO Should add a test
+ ctrl.refreshItems(items);
+ ctrl.ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
+ }
+ }
+ });
+
+ };
+
+ var _refreshDelayPromise;
+
+ /**
+ * Typeahead mode: lets the user refresh the collection using his own function.
+ *
+ * See Expose $select.search for external / remote filtering https://github.com/angular-ui/ui-select/pull/31
+ */
+ ctrl.refresh = function(refreshAttr) {
+ if (refreshAttr !== undefined) {
+
+ // Debounce
+ // See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L155
+ // FYI AngularStrap typeahead does not have debouncing: https://github.com/mgcrea/angular-strap/blob/v2.0.0-rc.4/src/typeahead/typeahead.js#L177
+ if (_refreshDelayPromise) {
+ $timeout.cancel(_refreshDelayPromise);
+ }
+ _refreshDelayPromise = $timeout(function() {
+ $scope.$eval(refreshAttr);
+ }, ctrl.refreshDelay);
+ }
+ };
+
+ ctrl.setActiveItem = function(item) {
+ ctrl.activeIndex = ctrl.items.indexOf(item);
+ };
+
+ ctrl.isActive = function(itemScope) {
+ if ( !ctrl.open ) {
+ return false;
+ }
+ var itemIndex = ctrl.items.indexOf(itemScope[ctrl.itemProperty]);
+ var isActive = itemIndex === ctrl.activeIndex;
+
+ if ( !isActive || ( itemIndex < 0 && ctrl.taggingLabel !== false ) ||( itemIndex < 0 && ctrl.taggingLabel === false) ) {
+ return false;
+ }
+
+ if (isActive && !angular.isUndefined(ctrl.onHighlightCallback)) {
+ itemScope.$eval(ctrl.onHighlightCallback);
+ }
+
+ return isActive;
+ };
+
+ ctrl.isDisabled = function(itemScope) {
+
+ if (!ctrl.open) return;
+
+ var itemIndex = ctrl.items.indexOf(itemScope[ctrl.itemProperty]);
+ var isDisabled = false;
+ var item;
+
+ if (itemIndex >= 0 && !angular.isUndefined(ctrl.disableChoiceExpression)) {
+ item = ctrl.items[itemIndex];
+ isDisabled = !!(itemScope.$eval(ctrl.disableChoiceExpression)); // force the boolean value
+ item._uiSelectChoiceDisabled = isDisabled; // store this for later reference
+ }
+
+ return isDisabled;
+ };
+
+
+ // When the user selects an item with ENTER or clicks the dropdown
+ ctrl.select = function(item, skipFocusser, $event) {
+ if (item === undefined || !item._uiSelectChoiceDisabled) {
+
+ if ( ! ctrl.items && ! ctrl.search ) return;
+
+ if (!item || !item._uiSelectChoiceDisabled) {
+ if(ctrl.tagging.isActivated) {
+ // if taggingLabel is disabled, we pull from ctrl.search val
+ if ( ctrl.taggingLabel === false ) {
+ if ( ctrl.activeIndex < 0 ) {
+ item = ctrl.tagging.fct !== undefined ? ctrl.tagging.fct(ctrl.search) : ctrl.search;
+ if (!item || angular.equals( ctrl.items[0], item ) ) {
+ return;
+ }
+ } else {
+ // keyboard nav happened first, user selected from dropdown
+ item = ctrl.items[ctrl.activeIndex];
+ }
+ } else {
+ // tagging always operates at index zero, taggingLabel === false pushes
+ // the ctrl.search value without having it injected
+ if ( ctrl.activeIndex === 0 ) {
+ // ctrl.tagging pushes items to ctrl.items, so we only have empty val
+ // for `item` if it is a detected duplicate
+ if ( item === undefined ) return;
+
+ // create new item on the fly if we don't already have one;
+ // use tagging function if we have one
+ if ( ctrl.tagging.fct !== undefined && typeof item === 'string' ) {
+ item = ctrl.tagging.fct(ctrl.search);
+ if (!item) return;
+ // if item type is 'string', apply the tagging label
+ } else if ( typeof item === 'string' ) {
+ // trim the trailing space
+ item = item.replace(ctrl.taggingLabel,'').trim();
+ }
+ }
+ }
+ // search ctrl.selected for dupes potentially caused by tagging and return early if found
+ if ( ctrl.selected && angular.isArray(ctrl.selected) && ctrl.selected.filter( function (selection) { return angular.equals(selection, item); }).length > 0 ) {
+ ctrl.close(skipFocusser);
+ return;
+ }
+ }
+
+ $scope.$broadcast('uis:select', item);
+
+ var locals = {};
+ locals[ctrl.parserResult.itemName] = item;
+
+ $timeout(function(){
+ ctrl.onSelectCallback($scope, {
+ $item: item,
+ $model: ctrl.parserResult.modelMapper($scope, locals)
+ });
+ });
+
+ if (ctrl.closeOnSelect) {
+ ctrl.close(skipFocusser);
+ }
+ if ($event && $event.type === 'click') {
+ ctrl.clickTriggeredSelect = true;
+ }
+ }
+ }
+ };
+
+ // Closes the dropdown
+ ctrl.close = function(skipFocusser) {
+ if (!ctrl.open) return;
+ if (ctrl.ngModel && ctrl.ngModel.$setTouched) ctrl.ngModel.$setTouched();
+ _resetSearchInput();
+ ctrl.open = false;
+
+ $scope.$broadcast('uis:close', skipFocusser);
+
+ };
+
+ ctrl.setFocus = function(){
+ if (!ctrl.focus) ctrl.focusInput[0].focus();
+ };
+
+ ctrl.clear = function($event) {
+ ctrl.select(undefined);
+ $event.stopPropagation();
+ ctrl.focusser[0].focus();
+ };
+
+ // Toggle dropdown
+ ctrl.toggle = function(e) {
+ if (ctrl.open) {
+ ctrl.close();
+ e.preventDefault();
+ e.stopPropagation();
+ } else {
+ ctrl.activate();
+ }
+ };
+
+ ctrl.isLocked = function(itemScope, itemIndex) {
+ var isLocked, item = ctrl.selected[itemIndex];
+
+ if (item && !angular.isUndefined(ctrl.lockChoiceExpression)) {
+ isLocked = !!(itemScope.$eval(ctrl.lockChoiceExpression)); // force the boolean value
+ item._uiSelectChoiceLocked = isLocked; // store this for later reference
+ }
+
+ return isLocked;
+ };
+
+ var sizeWatch = null;
+ ctrl.sizeSearchInput = function() {
+
+ var input = ctrl.searchInput[0],
+ container = ctrl.searchInput.parent().parent()[0],
+ calculateContainerWidth = function() {
+ // Return the container width only if the search input is visible
+ return container.clientWidth * !!input.offsetParent;
+ },
+ updateIfVisible = function(containerWidth) {
+ if (containerWidth === 0) {
+ return false;
+ }
+ var inputWidth = containerWidth - input.offsetLeft - 10;
+ if (inputWidth < 50) inputWidth = containerWidth;
+ ctrl.searchInput.css('width', inputWidth+'px');
+ return true;
+ };
+
+ ctrl.searchInput.css('width', '10px');
+ $timeout(function() { //Give tags time to render correctly
+ if (sizeWatch === null && !updateIfVisible(calculateContainerWidth())) {
+ sizeWatch = $scope.$watch(calculateContainerWidth, function(containerWidth) {
+ if (updateIfVisible(containerWidth)) {
+ sizeWatch();
+ sizeWatch = null;
+ }
+ });
+ }
+ });
+ };
+
+ function _handleDropDownSelection(key) {
+ var processed = true;
+ switch (key) {
+ case KEY.DOWN:
+ if (!ctrl.open && ctrl.multiple) ctrl.activate(false, true); //In case its the search input in 'multiple' mode
+ else if (ctrl.activeIndex < ctrl.items.length - 1) { ctrl.activeIndex++; }
+ break;
+ case KEY.UP:
+ if (!ctrl.open && ctrl.multiple) ctrl.activate(false, true); //In case its the search input in 'multiple' mode
+ else if (ctrl.activeIndex > 0 || (ctrl.search.length === 0 && ctrl.tagging.isActivated && ctrl.activeIndex > -1)) { ctrl.activeIndex--; }
+ break;
+ case KEY.TAB:
+ if (!ctrl.multiple || ctrl.open) ctrl.select(ctrl.items[ctrl.activeIndex], true);
+ break;
+ case KEY.ENTER:
+ if(ctrl.open && ctrl.activeIndex >= 0){
+ ctrl.select(ctrl.items[ctrl.activeIndex]); // Make sure at least one dropdown item is highlighted before adding.
+ } else {
+ ctrl.activate(false, true); //In case its the search input in 'multiple' mode
+ }
+ break;
+ case KEY.ESC:
+ ctrl.close();
+ break;
+ default:
+ processed = false;
+ }
+ return processed;
+ }
+
+ // Bind to keyboard shortcuts
+ ctrl.searchInput.on('keydown', function(e) {
+
+ var key = e.which;
+
+ // if(~[KEY.ESC,KEY.TAB].indexOf(key)){
+ // //TODO: SEGURO?
+ // ctrl.close();
+ // }
+
+ $scope.$apply(function() {
+
+ var tagged = false;
+
+ if (ctrl.items.length > 0 || ctrl.tagging.isActivated) {
+ _handleDropDownSelection(key);
+ if ( ctrl.taggingTokens.isActivated ) {
+ for (var i = 0; i < ctrl.taggingTokens.tokens.length; i++) {
+ if ( ctrl.taggingTokens.tokens[i] === KEY.MAP[e.keyCode] ) {
+ // make sure there is a new value to push via tagging
+ if ( ctrl.search.length > 0 ) {
+ tagged = true;
+ }
+ }
+ }
+ if ( tagged ) {
+ $timeout(function() {
+ ctrl.searchInput.triggerHandler('tagged');
+ var newItem = ctrl.search.replace(KEY.MAP[e.keyCode],'').trim();
+ if ( ctrl.tagging.fct ) {
+ newItem = ctrl.tagging.fct( newItem );
+ }
+ if (newItem) ctrl.select(newItem, true);
+ });
+ }
+ }
+ }
+
+ });
+
+ if(KEY.isVerticalMovement(key) && ctrl.items.length > 0){
+ _ensureHighlightVisible();
+ }
+
+ });
+
+ // If tagging try to split by tokens and add items
+ ctrl.searchInput.on('paste', function (e) {
+ var data = e.originalEvent.clipboardData.getData('text/plain');
+ if (data && data.length > 0 && ctrl.taggingTokens.isActivated && ctrl.tagging.fct) {
+ var items = data.split(ctrl.taggingTokens.tokens[0]); // split by first token only
+ if (items && items.length > 0) {
+ angular.forEach(items, function (item) {
+ var newItem = ctrl.tagging.fct(item);
+ if (newItem) {
+ ctrl.select(newItem, true);
+ }
+ });
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+ });
+
+ ctrl.searchInput.on('tagged', function() {
+ $timeout(function() {
+ _resetSearchInput();
+ });
+ });
+
+ // See https://github.com/ivaynberg/select2/blob/3.4.6/select2.js#L1431
+ function _ensureHighlightVisible() {
+ var container = $element.querySelectorAll('.ui-select-choices-content');
+ var choices = container.querySelectorAll('.ui-select-choices-row');
+ if (choices.length < 1) {
+ throw uiSelectMinErr('choices', "Expected multiple .ui-select-choices-row but got '{0}'.", choices.length);
+ }
+
+ if (ctrl.activeIndex < 0) {
+ return;
+ }
+
+ var highlighted = choices[ctrl.activeIndex];
+ var posY = highlighted.offsetTop + highlighted.clientHeight - container[0].scrollTop;
+ var height = container[0].offsetHeight;
+
+ if (posY > height) {
+ container[0].scrollTop += posY - height;
+ } else if (posY < highlighted.clientHeight) {
+ if (ctrl.isGrouped && ctrl.activeIndex === 0)
+ container[0].scrollTop = 0; //To make group header visible when going all the way up
+ else
+ container[0].scrollTop -= highlighted.clientHeight - posY;
+ }
+ }
+
+ $scope.$on('$destroy', function() {
+ ctrl.searchInput.off('keyup keydown tagged blur paste');
+ });
+
+}]);
+
+uis.directive('uiSelect',
+ ['$document', 'uiSelectConfig', 'uiSelectMinErr', 'uisOffset', '$compile', '$parse', '$timeout',
+ function($document, uiSelectConfig, uiSelectMinErr, uisOffset, $compile, $parse, $timeout) {
+
+ return {
+ restrict: 'EA',
+ templateUrl: function(tElement, tAttrs) {
+ var theme = tAttrs.theme || uiSelectConfig.theme;
+ return theme + (angular.isDefined(tAttrs.multiple) ? '/select-multiple.tpl.html' : '/select.tpl.html');
+ },
+ replace: true,
+ transclude: true,
+ require: ['uiSelect', '^ngModel'],
+ scope: true,
+
+ controller: 'uiSelectCtrl',
+ controllerAs: '$select',
+ compile: function(tElement, tAttrs) {
+
+ //Multiple or Single depending if multiple attribute presence
+ if (angular.isDefined(tAttrs.multiple))
+ tElement.append(" ").removeAttr('multiple');
+ else
+ tElement.append(" ");
+
+ return function(scope, element, attrs, ctrls, transcludeFn) {
+
+ var $select = ctrls[0];
+ var ngModel = ctrls[1];
+
+ $select.generatedId = uiSelectConfig.generateId();
+ $select.baseTitle = attrs.title || 'Select box';
+ $select.focusserTitle = $select.baseTitle + ' focus';
+ $select.focusserId = 'focusser-' + $select.generatedId;
+
+ $select.closeOnSelect = function() {
+ if (angular.isDefined(attrs.closeOnSelect)) {
+ return $parse(attrs.closeOnSelect)();
+ } else {
+ return uiSelectConfig.closeOnSelect;
+ }
+ }();
+
+ $select.onSelectCallback = $parse(attrs.onSelect);
+ $select.onRemoveCallback = $parse(attrs.onRemove);
+
+ //Set reference to ngModel from uiSelectCtrl
+ $select.ngModel = ngModel;
+
+ $select.choiceGrouped = function(group){
+ return $select.isGrouped && group && group.name;
+ };
+
+ if(attrs.tabindex){
+ attrs.$observe('tabindex', function(value) {
+ $select.focusInput.attr("tabindex", value);
+ element.removeAttr("tabindex");
+ });
+ }
+
+ scope.$watch('searchEnabled', function() {
+ var searchEnabled = scope.$eval(attrs.searchEnabled);
+ $select.searchEnabled = searchEnabled !== undefined ? searchEnabled : uiSelectConfig.searchEnabled;
+ });
+
+ scope.$watch('sortable', function() {
+ var sortable = scope.$eval(attrs.sortable);
+ $select.sortable = sortable !== undefined ? sortable : uiSelectConfig.sortable;
+ });
+
+ attrs.$observe('disabled', function() {
+ // No need to use $eval() (thanks to ng-disabled) since we already get a boolean instead of a string
+ $select.disabled = attrs.disabled !== undefined ? attrs.disabled : false;
+ });
+
+ attrs.$observe('resetSearchInput', function() {
+ // $eval() is needed otherwise we get a string instead of a boolean
+ var resetSearchInput = scope.$eval(attrs.resetSearchInput);
+ $select.resetSearchInput = resetSearchInput !== undefined ? resetSearchInput : true;
+ });
+
+ attrs.$observe('tagging', function() {
+ if(attrs.tagging !== undefined)
+ {
+ // $eval() is needed otherwise we get a string instead of a boolean
+ var taggingEval = scope.$eval(attrs.tagging);
+ $select.tagging = {isActivated: true, fct: taggingEval !== true ? taggingEval : undefined};
+ }
+ else
+ {
+ $select.tagging = {isActivated: false, fct: undefined};
+ }
+ });
+
+ attrs.$observe('taggingLabel', function() {
+ if(attrs.tagging !== undefined )
+ {
+ // check eval for FALSE, in this case, we disable the labels
+ // associated with tagging
+ if ( attrs.taggingLabel === 'false' ) {
+ $select.taggingLabel = false;
+ }
+ else
+ {
+ $select.taggingLabel = attrs.taggingLabel !== undefined ? attrs.taggingLabel : '(new)';
+ }
+ }
+ });
+
+ attrs.$observe('taggingTokens', function() {
+ if (attrs.tagging !== undefined) {
+ var tokens = attrs.taggingTokens !== undefined ? attrs.taggingTokens.split('|') : [',','ENTER'];
+ $select.taggingTokens = {isActivated: true, tokens: tokens };
+ }
+ });
+
+ //Automatically gets focus when loaded
+ if (angular.isDefined(attrs.autofocus)){
+ $timeout(function(){
+ $select.setFocus();
+ });
+ }
+
+ //Gets focus based on scope event name (e.g. focus-on='SomeEventName')
+ if (angular.isDefined(attrs.focusOn)){
+ scope.$on(attrs.focusOn, function() {
+ $timeout(function(){
+ $select.setFocus();
+ });
+ });
+ }
+
+ function onDocumentClick(e) {
+ if (!$select.open) return; //Skip it if dropdown is close
+
+ var contains = false;
+
+ if (window.jQuery) {
+ // Firefox 3.6 does not support element.contains()
+ // See Node.contains https://developer.mozilla.org/en-US/docs/Web/API/Node.contains
+ contains = window.jQuery.contains(element[0], e.target);
+ } else {
+ contains = element[0].contains(e.target);
+ }
+
+ if (!contains && !$select.clickTriggeredSelect) {
+ //Will lose focus only with certain targets
+ var focusableControls = ['input','button','textarea'];
+ var targetScope = angular.element(e.target).scope(); //To check if target is other ui-select
+ var skipFocusser = targetScope && targetScope.$select && targetScope.$select !== $select; //To check if target is other ui-select
+ if (!skipFocusser) skipFocusser = ~focusableControls.indexOf(e.target.tagName.toLowerCase()); //Check if target is input, button or textarea
+ $select.close(skipFocusser);
+ scope.$digest();
+ }
+ $select.clickTriggeredSelect = false;
+ }
+
+ // See Click everywhere but here event http://stackoverflow.com/questions/12931369
+ $document.on('click', onDocumentClick);
+
+ scope.$on('$destroy', function() {
+ $document.off('click', onDocumentClick);
+ });
+
+ // Move transcluded elements to their correct position in main template
+ transcludeFn(scope, function(clone) {
+ // See Transclude in AngularJS http://blog.omkarpatil.com/2012/11/transclude-in-angularjs.html
+
+ // One day jqLite will be replaced by jQuery and we will be able to write:
+ // var transcludedElement = clone.filter('.my-class')
+ // instead of creating a hackish DOM element:
+ var transcluded = angular.element('').append(clone);
+
+ var transcludedMatch = transcluded.querySelectorAll('.ui-select-match');
+ transcludedMatch.removeAttr('ui-select-match'); //To avoid loop in case directive as attr
+ transcludedMatch.removeAttr('data-ui-select-match'); // Properly handle HTML5 data-attributes
+ if (transcludedMatch.length !== 1) {
+ throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-match but got '{0}'.", transcludedMatch.length);
+ }
+ element.querySelectorAll('.ui-select-match').replaceWith(transcludedMatch);
+
+ var transcludedChoices = transcluded.querySelectorAll('.ui-select-choices');
+ transcludedChoices.removeAttr('ui-select-choices'); //To avoid loop in case directive as attr
+ transcludedChoices.removeAttr('data-ui-select-choices'); // Properly handle HTML5 data-attributes
+ if (transcludedChoices.length !== 1) {
+ throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-choices but got '{0}'.", transcludedChoices.length);
+ }
+ element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices);
+ });
+
+ // Support for appending the select field to the body when its open
+ var appendToBody = scope.$eval(attrs.appendToBody);
+ if (appendToBody !== undefined ? appendToBody : uiSelectConfig.appendToBody) {
+ scope.$watch('$select.open', function(isOpen) {
+ if (isOpen) {
+ positionDropdown();
+ } else {
+ resetDropdown();
+ }
+ });
+
+ // Move the dropdown back to its original location when the scope is destroyed. Otherwise
+ // it might stick around when the user routes away or the select field is otherwise removed
+ scope.$on('$destroy', function() {
+ resetDropdown();
+ });
+ }
+
+ // Hold on to a reference to the .ui-select-container element for appendToBody support
+ var placeholder = null,
+ originalWidth = '';
+
+ function positionDropdown() {
+ // Remember the absolute position of the element
+ var offset = uisOffset(element);
+
+ // Clone the element into a placeholder element to take its original place in the DOM
+ placeholder = angular.element('
');
+ placeholder[0].style.width = offset.width + 'px';
+ placeholder[0].style.height = offset.height + 'px';
+ element.after(placeholder);
+
+ // Remember the original value of the element width inline style, so it can be restored
+ // when the dropdown is closed
+ originalWidth = element[0].style.width;
+
+ // Now move the actual dropdown element to the end of the body
+ $document.find('body').append(element);
+
+ element[0].style.position = 'absolute';
+ element[0].style.left = offset.left + 'px';
+ element[0].style.top = offset.top + 'px';
+ element[0].style.width = offset.width + 'px';
+ }
+
+ function resetDropdown() {
+ if (placeholder === null) {
+ // The dropdown has not actually been display yet, so there's nothing to reset
+ return;
+ }
+
+ // Move the dropdown element back to its original location in the DOM
+ placeholder.replaceWith(element);
+ placeholder = null;
+
+ element[0].style.position = '';
+ element[0].style.left = '';
+ element[0].style.top = '';
+ element[0].style.width = originalWidth;
+ }
+ };
+ }
+ };
+}]);
+
+uis.directive('uiSelectMatch', ['uiSelectConfig', function(uiSelectConfig) {
+ return {
+ restrict: 'EA',
+ require: '^uiSelect',
+ replace: true,
+ transclude: true,
+ templateUrl: function(tElement) {
+ // Gets theme attribute from parent (ui-select)
+ var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
+ var multi = tElement.parent().attr('multiple');
+ return theme + (multi ? '/match-multiple.tpl.html' : '/match.tpl.html');
+ },
+ link: function(scope, element, attrs, $select) {
+ $select.lockChoiceExpression = attrs.uiLockChoice;
+ attrs.$observe('placeholder', function(placeholder) {
+ $select.placeholder = placeholder !== undefined ? placeholder : uiSelectConfig.placeholder;
+ });
+
+ function setAllowClear(allow) {
+ $select.allowClear = (angular.isDefined(allow)) ? (allow === '') ? true : (allow.toLowerCase() === 'true') : false;
+ }
+
+ attrs.$observe('allowClear', setAllowClear);
+ setAllowClear(attrs.allowClear);
+
+ if($select.multiple){
+ $select.sizeSearchInput();
+ }
+
+ }
+ };
+}]);
+
+uis.directive('uiSelectMultiple', ['uiSelectMinErr','$timeout', function(uiSelectMinErr, $timeout) {
+ return {
+ restrict: 'EA',
+ require: ['^uiSelect', '^ngModel'],
+
+ controller: ['$scope','$timeout', function($scope, $timeout){
+
+ var ctrl = this,
+ $select = $scope.$select,
+ ngModel;
+
+ //Wait for link fn to inject it
+ $scope.$evalAsync(function(){ ngModel = $scope.ngModel; });
+
+ ctrl.activeMatchIndex = -1;
+
+ ctrl.updateModel = function(){
+ ngModel.$setViewValue(Date.now()); //Set timestamp as a unique string to force changes
+ ctrl.refreshComponent();
+ };
+
+ ctrl.refreshComponent = function(){
+ //Remove already selected items
+ //e.g. When user clicks on a selection, the selected array changes and
+ //the dropdown should remove that item
+ $select.refreshItems();
+ $select.sizeSearchInput();
+ };
+
+ // Remove item from multiple select
+ ctrl.removeChoice = function(index){
+
+ var removedChoice = $select.selected[index];
+
+ // if the choice is locked, can't remove it
+ if(removedChoice._uiSelectChoiceLocked) return;
+
+ var locals = {};
+ locals[$select.parserResult.itemName] = removedChoice;
+
+ $select.selected.splice(index, 1);
+ ctrl.activeMatchIndex = -1;
+ $select.sizeSearchInput();
+
+ // Give some time for scope propagation.
+ $timeout(function(){
+ $select.onRemoveCallback($scope, {
+ $item: removedChoice,
+ $model: $select.parserResult.modelMapper($scope, locals)
+ });
+ });
+
+ ctrl.updateModel();
+
+ };
+
+ ctrl.getPlaceholder = function(){
+ //Refactor single?
+ if($select.selected.length) return;
+ return $select.placeholder;
+ };
+
+
+ }],
+ controllerAs: '$selectMultiple',
+
+ link: function(scope, element, attrs, ctrls) {
+
+ var $select = ctrls[0];
+ var ngModel = scope.ngModel = ctrls[1];
+ var $selectMultiple = scope.$selectMultiple;
+
+ //$select.selected = raw selected objects (ignoring any property binding)
+
+ $select.multiple = true;
+ $select.removeSelected = true;
+
+ //Input that will handle focus
+ $select.focusInput = $select.searchInput;
+
+ //From view --> model
+ ngModel.$parsers.unshift(function () {
+ var locals = {},
+ result,
+ resultMultiple = [];
+ for (var j = $select.selected.length - 1; j >= 0; j--) {
+ locals = {};
+ locals[$select.parserResult.itemName] = $select.selected[j];
+ result = $select.parserResult.modelMapper(scope, locals);
+ resultMultiple.unshift(result);
+ }
+ return resultMultiple;
+ });
+
+ // From model --> view
+ ngModel.$formatters.unshift(function (inputValue) {
+ var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
+ locals = {},
+ result;
+ if (!data) return inputValue;
+ var resultMultiple = [];
+ var checkFnMultiple = function(list, value){
+ if (!list || !list.length) return;
+ for (var p = list.length - 1; p >= 0; p--) {
+ locals[$select.parserResult.itemName] = list[p];
+ result = $select.parserResult.modelMapper(scope, locals);
+ if($select.parserResult.trackByExp){
+ var matches = /\.(.+)/.exec($select.parserResult.trackByExp);
+ if(matches.length>0 && result[matches[1]] == value[matches[1]]){
+ resultMultiple.unshift(list[p]);
+ return true;
+ }
+ }
+ if (angular.equals(result,value)){
+ resultMultiple.unshift(list[p]);
+ return true;
+ }
+ }
+ return false;
+ };
+ if (!inputValue) return resultMultiple; //If ngModel was undefined
+ for (var k = inputValue.length - 1; k >= 0; k--) {
+ //Check model array of currently selected items
+ if (!checkFnMultiple($select.selected, inputValue[k])){
+ //Check model array of all items available
+ if (!checkFnMultiple(data, inputValue[k])){
+ //If not found on previous lists, just add it directly to resultMultiple
+ resultMultiple.unshift(inputValue[k]);
+ }
+ }
+ }
+ return resultMultiple;
+ });
+
+ //Watch for external model changes
+ scope.$watchCollection(function(){ return ngModel.$modelValue; }, function(newValue, oldValue) {
+ if (oldValue != newValue){
+ ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
+ $selectMultiple.refreshComponent();
+ }
+ });
+
+ ngModel.$render = function() {
+ // Make sure that model value is array
+ if(!angular.isArray(ngModel.$viewValue)){
+ // Have tolerance for null or undefined values
+ if(angular.isUndefined(ngModel.$viewValue) || ngModel.$viewValue === null){
+ $select.selected = [];
+ } else {
+ throw uiSelectMinErr('multiarr', "Expected model value to be array but got '{0}'", ngModel.$viewValue);
+ }
+ }
+ $select.selected = ngModel.$viewValue;
+ scope.$evalAsync(); //To force $digest
+ };
+
+ scope.$on('uis:select', function (event, item) {
+ $select.selected.push(item);
+ $selectMultiple.updateModel();
+ });
+
+ scope.$on('uis:activate', function () {
+ $selectMultiple.activeMatchIndex = -1;
+ });
+
+ scope.$watch('$select.disabled', function(newValue, oldValue) {
+ // As the search input field may now become visible, it may be necessary to recompute its size
+ if (oldValue && !newValue) $select.sizeSearchInput();
+ });
+
+ $select.searchInput.on('keydown', function(e) {
+ var key = e.which;
+ scope.$apply(function() {
+ var processed = false;
+ // var tagged = false; //Checkme
+ if(KEY.isHorizontalMovement(key)){
+ processed = _handleMatchSelection(key);
+ }
+ if (processed && key != KEY.TAB) {
+ //TODO Check si el tab selecciona aun correctamente
+ //Crear test
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ });
+ });
+ function _getCaretPosition(el) {
+ if(angular.isNumber(el.selectionStart)) return el.selectionStart;
+ // selectionStart is not supported in IE8 and we don't want hacky workarounds so we compromise
+ else return el.value.length;
+ }
+ // Handles selected options in "multiple" mode
+ function _handleMatchSelection(key){
+ var caretPosition = _getCaretPosition($select.searchInput[0]),
+ length = $select.selected.length,
+ // none = -1,
+ first = 0,
+ last = length-1,
+ curr = $selectMultiple.activeMatchIndex,
+ next = $selectMultiple.activeMatchIndex+1,
+ prev = $selectMultiple.activeMatchIndex-1,
+ newIndex = curr;
+
+ if(caretPosition > 0 || ($select.search.length && key == KEY.RIGHT)) return false;
+
+ $select.close();
+
+ function getNewActiveMatchIndex(){
+ switch(key){
+ case KEY.LEFT:
+ // Select previous/first item
+ if(~$selectMultiple.activeMatchIndex) return prev;
+ // Select last item
+ else return last;
+ break;
+ case KEY.RIGHT:
+ // Open drop-down
+ if(!~$selectMultiple.activeMatchIndex || curr === last){
+ $select.activate();
+ return false;
+ }
+ // Select next/last item
+ else return next;
+ break;
+ case KEY.BACKSPACE:
+ // Remove selected item and select previous/first
+ if(~$selectMultiple.activeMatchIndex){
+ $selectMultiple.removeChoice(curr);
+ return prev;
+ }
+ // Select last item
+ else return last;
+ break;
+ case KEY.DELETE:
+ // Remove selected item and select next item
+ if(~$selectMultiple.activeMatchIndex){
+ $selectMultiple.removeChoice($selectMultiple.activeMatchIndex);
+ return curr;
+ }
+ else return false;
+ }
+ }
+
+ newIndex = getNewActiveMatchIndex();
+
+ if(!$select.selected.length || newIndex === false) $selectMultiple.activeMatchIndex = -1;
+ else $selectMultiple.activeMatchIndex = Math.min(last,Math.max(first,newIndex));
+
+ return true;
+ }
+
+ $select.searchInput.on('keyup', function(e) {
+
+ if ( ! KEY.isVerticalMovement(e.which) ) {
+ scope.$evalAsync( function () {
+ $select.activeIndex = $select.taggingLabel === false ? -1 : 0;
+ });
+ }
+ // Push a "create new" item into array if there is a search string
+ if ( $select.tagging.isActivated && $select.search.length > 0 ) {
+
+ // return early with these keys
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || KEY.isVerticalMovement(e.which) ) {
+ return;
+ }
+ // always reset the activeIndex to the first item when tagging
+ $select.activeIndex = $select.taggingLabel === false ? -1 : 0;
+ // taggingLabel === false bypasses all of this
+ if ($select.taggingLabel === false) return;
+
+ var items = angular.copy( $select.items );
+ var stashArr = angular.copy( $select.items );
+ var newItem;
+ var item;
+ var hasTag = false;
+ var dupeIndex = -1;
+ var tagItems;
+ var tagItem;
+
+ // case for object tagging via transform `$select.tagging.fct` function
+ if ( $select.tagging.fct !== undefined) {
+ tagItems = $select.$filter('filter')(items,{'isTag': true});
+ if ( tagItems.length > 0 ) {
+ tagItem = tagItems[0];
+ }
+ // remove the first element, if it has the `isTag` prop we generate a new one with each keyup, shaving the previous
+ if ( items.length > 0 && tagItem ) {
+ hasTag = true;
+ items = items.slice(1,items.length);
+ stashArr = stashArr.slice(1,stashArr.length);
+ }
+ newItem = $select.tagging.fct($select.search);
+ newItem.isTag = true;
+ // verify the the tag doesn't match the value of an existing item
+ if ( stashArr.filter( function (origItem) { return angular.equals( origItem, $select.tagging.fct($select.search) ); } ).length > 0 ) {
+ return;
+ }
+ newItem.isTag = true;
+ // handle newItem string and stripping dupes in tagging string context
+ } else {
+ // find any tagging items already in the $select.items array and store them
+ tagItems = $select.$filter('filter')(items,function (item) {
+ return item.match($select.taggingLabel);
+ });
+ if ( tagItems.length > 0 ) {
+ tagItem = tagItems[0];
+ }
+ item = items[0];
+ // remove existing tag item if found (should only ever be one tag item)
+ if ( item !== undefined && items.length > 0 && tagItem ) {
+ hasTag = true;
+ items = items.slice(1,items.length);
+ stashArr = stashArr.slice(1,stashArr.length);
+ }
+ newItem = $select.search+' '+$select.taggingLabel;
+ if ( _findApproxDupe($select.selected, $select.search) > -1 ) {
+ return;
+ }
+ // verify the the tag doesn't match the value of an existing item from
+ // the searched data set or the items already selected
+ if ( _findCaseInsensitiveDupe(stashArr.concat($select.selected)) ) {
+ // if there is a tag from prev iteration, strip it / queue the change
+ // and return early
+ if ( hasTag ) {
+ items = stashArr;
+ scope.$evalAsync( function () {
+ $select.activeIndex = 0;
+ $select.items = items;
+ });
+ }
+ return;
+ }
+ if ( _findCaseInsensitiveDupe(stashArr) ) {
+ // if there is a tag from prev iteration, strip it
+ if ( hasTag ) {
+ $select.items = stashArr.slice(1,stashArr.length);
+ }
+ return;
+ }
+ }
+ if ( hasTag ) dupeIndex = _findApproxDupe($select.selected, newItem);
+ // dupe found, shave the first item
+ if ( dupeIndex > -1 ) {
+ items = items.slice(dupeIndex+1,items.length-1);
+ } else {
+ items = [];
+ items.push(newItem);
+ items = items.concat(stashArr);
+ }
+ scope.$evalAsync( function () {
+ $select.activeIndex = 0;
+ $select.items = items;
+ });
+ }
+ });
+ function _findCaseInsensitiveDupe(arr) {
+ if ( arr === undefined || $select.search === undefined ) {
+ return false;
+ }
+ var hasDupe = arr.filter( function (origItem) {
+ if ( $select.search.toUpperCase() === undefined || origItem === undefined ) {
+ return false;
+ }
+ return origItem.toUpperCase() === $select.search.toUpperCase();
+ }).length > 0;
+
+ return hasDupe;
+ }
+ function _findApproxDupe(haystack, needle) {
+ var dupeIndex = -1;
+ if(angular.isArray(haystack)) {
+ var tempArr = angular.copy(haystack);
+ for (var i = 0; i
model
+ ngModel.$parsers.unshift(function (inputValue) {
+ var locals = {},
+ result;
+ locals[$select.parserResult.itemName] = inputValue;
+ result = $select.parserResult.modelMapper(scope, locals);
+ return result;
+ });
+
+ //From model --> view
+ ngModel.$formatters.unshift(function (inputValue) {
+ var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
+ locals = {},
+ result;
+ if (data){
+ var checkFnSingle = function(d){
+ locals[$select.parserResult.itemName] = d;
+ result = $select.parserResult.modelMapper(scope, locals);
+ return result == inputValue;
+ };
+ //If possible pass same object stored in $select.selected
+ if ($select.selected && checkFnSingle($select.selected)) {
+ return $select.selected;
+ }
+ for (var i = data.length - 1; i >= 0; i--) {
+ if (checkFnSingle(data[i])) return data[i];
+ }
+ }
+ return inputValue;
+ });
+
+ //Update viewValue if model change
+ scope.$watch('$select.selected', function(newValue) {
+ if (ngModel.$viewValue !== newValue) {
+ ngModel.$setViewValue(newValue);
+ }
+ });
+
+ ngModel.$render = function() {
+ $select.selected = ngModel.$viewValue;
+ };
+
+ scope.$on('uis:select', function (event, item) {
+ $select.selected = item;
+ });
+
+ scope.$on('uis:close', function (event, skipFocusser) {
+ $timeout(function(){
+ $select.focusser.prop('disabled', false);
+ if (!skipFocusser) $select.focusser[0].focus();
+ },0,false);
+ });
+
+ scope.$on('uis:activate', function () {
+ focusser.prop('disabled', true); //Will reactivate it on .close()
+ });
+
+ //Idea from: https://github.com/ivaynberg/select2/blob/79b5bf6db918d7560bdd959109b7bcfb47edaf43/select2.js#L1954
+ var focusser = angular.element(" ");
+ $compile(focusser)(scope);
+ $select.focusser = focusser;
+
+ //Input that will handle focus
+ $select.focusInput = focusser;
+
+ element.parent().append(focusser);
+ focusser.bind("focus", function(){
+ scope.$evalAsync(function(){
+ $select.focus = true;
+ });
+ });
+ focusser.bind("blur", function(){
+ scope.$evalAsync(function(){
+ $select.focus = false;
+ });
+ });
+ focusser.bind("keydown", function(e){
+
+ if (e.which === KEY.BACKSPACE) {
+ e.preventDefault();
+ e.stopPropagation();
+ $select.select(undefined);
+ scope.$apply();
+ return;
+ }
+
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
+ return;
+ }
+
+ if (e.which == KEY.DOWN || e.which == KEY.UP || e.which == KEY.ENTER || e.which == KEY.SPACE){
+ e.preventDefault();
+ e.stopPropagation();
+ $select.activate();
+ }
+
+ scope.$digest();
+ });
+
+ focusser.bind("keyup input", function(e){
+
+ if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || e.which == KEY.ENTER || e.which === KEY.BACKSPACE) {
+ return;
+ }
+
+ $select.activate(focusser.val()); //User pressed some regular key, so we pass it to the search input
+ focusser.val('');
+ scope.$digest();
+
+ });
+
+
+ }
+ };
+}]);
+// Make multiple matches sortable
+uis.directive('uiSelectSort', ['$timeout', 'uiSelectConfig', 'uiSelectMinErr', function($timeout, uiSelectConfig, uiSelectMinErr) {
+ return {
+ require: '^uiSelect',
+ link: function(scope, element, attrs, $select) {
+ if (scope[attrs.uiSelectSort] === null) {
+ throw uiSelectMinErr('sort', "Expected a list to sort");
+ }
+
+ var options = angular.extend({
+ axis: 'horizontal'
+ },
+ scope.$eval(attrs.uiSelectSortOptions));
+
+ var axis = options.axis,
+ draggingClassName = 'dragging',
+ droppingClassName = 'dropping',
+ droppingBeforeClassName = 'dropping-before',
+ droppingAfterClassName = 'dropping-after';
+
+ scope.$watch(function(){
+ return $select.sortable;
+ }, function(n){
+ if (n) {
+ element.attr('draggable', true);
+ } else {
+ element.removeAttr('draggable');
+ }
+ });
+
+ element.on('dragstart', function(e) {
+ element.addClass(draggingClassName);
+
+ (e.dataTransfer || e.originalEvent.dataTransfer).setData('text/plain', scope.$index);
+ });
+
+ element.on('dragend', function() {
+ element.removeClass(draggingClassName);
+ });
+
+ var move = function(from, to) {
+ /*jshint validthis: true */
+ this.splice(to, 0, this.splice(from, 1)[0]);
+ };
+
+ var dragOverHandler = function(e) {
+ e.preventDefault();
+
+ var offset = axis === 'vertical' ? e.offsetY || e.layerY || (e.originalEvent ? e.originalEvent.offsetY : 0) : e.offsetX || e.layerX || (e.originalEvent ? e.originalEvent.offsetX : 0);
+
+ if (offset < (this[axis === 'vertical' ? 'offsetHeight' : 'offsetWidth'] / 2)) {
+ element.removeClass(droppingAfterClassName);
+ element.addClass(droppingBeforeClassName);
+
+ } else {
+ element.removeClass(droppingBeforeClassName);
+ element.addClass(droppingAfterClassName);
+ }
+ };
+
+ var dropTimeout;
+
+ var dropHandler = function(e) {
+ e.preventDefault();
+
+ var droppedItemIndex = parseInt((e.dataTransfer || e.originalEvent.dataTransfer).getData('text/plain'), 10);
+
+ // prevent event firing multiple times in firefox
+ $timeout.cancel(dropTimeout);
+ dropTimeout = $timeout(function() {
+ _dropHandler(droppedItemIndex);
+ }, 20);
+ };
+
+ var _dropHandler = function(droppedItemIndex) {
+ var theList = scope.$eval(attrs.uiSelectSort),
+ itemToMove = theList[droppedItemIndex],
+ newIndex = null;
+
+ if (element.hasClass(droppingBeforeClassName)) {
+ if (droppedItemIndex < scope.$index) {
+ newIndex = scope.$index - 1;
+ } else {
+ newIndex = scope.$index;
+ }
+ } else {
+ if (droppedItemIndex < scope.$index) {
+ newIndex = scope.$index;
+ } else {
+ newIndex = scope.$index + 1;
+ }
+ }
+
+ move.apply(theList, [droppedItemIndex, newIndex]);
+
+ scope.$apply(function() {
+ scope.$emit('uiSelectSort:change', {
+ array: theList,
+ item: itemToMove,
+ from: droppedItemIndex,
+ to: newIndex
+ });
+ });
+
+ element.removeClass(droppingClassName);
+ element.removeClass(droppingBeforeClassName);
+ element.removeClass(droppingAfterClassName);
+
+ element.off('drop', dropHandler);
+ };
+
+ element.on('dragenter', function() {
+ if (element.hasClass(draggingClassName)) {
+ return;
+ }
+
+ element.addClass(droppingClassName);
+
+ element.on('dragover', dragOverHandler);
+ element.on('drop', dropHandler);
+ });
+
+ element.on('dragleave', function(e) {
+ if (e.target != element) {
+ return;
+ }
+ element.removeClass(droppingClassName);
+ element.removeClass(droppingBeforeClassName);
+ element.removeClass(droppingAfterClassName);
+
+ element.off('dragover', dragOverHandler);
+ element.off('drop', dropHandler);
+ });
+ }
+ };
+}]);
+
+/**
+ * Parses "repeat" attribute.
+ *
+ * Taken from AngularJS ngRepeat source code
+ * See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L211
+ *
+ * Original discussion about parsing "repeat" attribute instead of fully relying on ng-repeat:
+ * https://github.com/angular-ui/ui-select/commit/5dd63ad#commitcomment-5504697
+ */
+
+uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinErr, $parse) {
+ var self = this;
+
+ /**
+ * Example:
+ * expression = "address in addresses | filter: {street: $select.search} track by $index"
+ * itemName = "address",
+ * source = "addresses | filter: {street: $select.search}",
+ * trackByExp = "$index",
+ */
+ self.parse = function(expression) {
+
+ var match = expression.match(/^\s*(?:([\s\S]+?)\s+as\s+)?([\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
+
+ if (!match) {
+ throw uiSelectMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
+ expression);
+ }
+
+ return {
+ itemName: match[2], // (lhs) Left-hand side,
+ source: $parse(match[3]),
+ trackByExp: match[4],
+ modelMapper: $parse(match[1] || match[2])
+ };
+
+ };
+
+ self.getGroupNgRepeatExpression = function() {
+ return '$group in $select.groups';
+ };
+
+ self.getNgRepeatExpression = function(itemName, source, trackByExp, grouped) {
+ var expression = itemName + ' in ' + (grouped ? '$group.items' : source);
+ if (trackByExp) {
+ expression += ' track by ' + trackByExp;
+ }
+ return expression;
+ };
+}]);
+
+}());
+angular.module("ui.select").run(["$templateCache", function($templateCache) {$templateCache.put("bootstrap/choices.tpl.html","");
+$templateCache.put("bootstrap/match-multiple.tpl.html"," × ");
+$templateCache.put("bootstrap/match.tpl.html","");
+$templateCache.put("bootstrap/select-multiple.tpl.html","");
+$templateCache.put("bootstrap/select.tpl.html","");
+$templateCache.put("select2/choices.tpl.html","");
+$templateCache.put("select2/match-multiple.tpl.html"," ");
+$templateCache.put("select2/match.tpl.html","{{$select.placeholder}} ");
+$templateCache.put("select2/select-multiple.tpl.html","");
+$templateCache.put("select2/select.tpl.html","");
+$templateCache.put("selectize/choices.tpl.html","");
+$templateCache.put("selectize/match.tpl.html","
");
+$templateCache.put("selectize/select.tpl.html","");}]);
\ No newline at end of file
diff --git a/js/vendor/angularui/ui-select/select.min.js b/js/vendor/angularui/ui-select/select.min.js
new file mode 100644
index 00000000..7e50d794
--- /dev/null
+++ b/js/vendor/angularui/ui-select/select.min.js
@@ -0,0 +1,8 @@
+/*!
+ * ui-select
+ * http://github.com/angular-ui/ui-select
+ * Version: 0.11.2 - 2015-03-17T04:08:46.474Z
+ * License: MIT
+ */
+!function(){"use strict";var e={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,COMMAND:91,MAP:{91:"COMMAND",8:"BACKSPACE",9:"TAB",13:"ENTER",16:"SHIFT",17:"CTRL",18:"ALT",19:"PAUSEBREAK",20:"CAPSLOCK",27:"ESC",32:"SPACE",33:"PAGE_UP",34:"PAGE_DOWN",35:"END",36:"HOME",37:"LEFT",38:"UP",39:"RIGHT",40:"DOWN",43:"+",44:"PRINTSCREEN",45:"INSERT",46:"DELETE",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",61:"=",65:"A",66:"B",67:"C",68:"D",69:"E",70:"F",71:"G",72:"H",73:"I",74:"J",75:"K",76:"L",77:"M",78:"N",79:"O",80:"P",81:"Q",82:"R",83:"S",84:"T",85:"U",86:"V",87:"W",88:"X",89:"Y",90:"Z",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9",106:"*",107:"+",109:"-",110:".",111:"/",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NUMLOCK",145:"SCROLLLOCK",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},isControl:function(t){var c=t.which;switch(c){case e.COMMAND:case e.SHIFT:case e.CTRL:case e.ALT:return!0}return t.metaKey?!0:!1},isFunctionKey:function(e){return e=e.which?e.which:e,e>=112&&123>=e},isVerticalMovement:function(t){return~[e.UP,e.DOWN].indexOf(t)},isHorizontalMovement:function(t){return~[e.LEFT,e.RIGHT,e.BACKSPACE,e.DELETE].indexOf(t)}};void 0===angular.element.prototype.querySelectorAll&&(angular.element.prototype.querySelectorAll=function(e){return angular.element(this[0].querySelectorAll(e))}),void 0===angular.element.prototype.closest&&(angular.element.prototype.closest=function(e){for(var t=this[0],c=t.matches||t.webkitMatchesSelector||t.mozMatchesSelector||t.msMatchesSelector;t;){if(c.bind(t)(e))return t;t=t.parentElement}return!1});var t=0,c=angular.module("ui.select",[]).constant("uiSelectConfig",{theme:"bootstrap",searchEnabled:!0,sortable:!1,placeholder:"",refreshDelay:1e3,closeOnSelect:!0,generateId:function(){return t++},appendToBody:!1}).service("uiSelectMinErr",function(){var e=angular.$$minErr("ui.select");return function(){var t=e.apply(this,arguments),c=t.message.replace(new RegExp("\nhttp://errors.angularjs.org/.*"),"");return new Error(c)}}).directive("uisTranscludeAppend",function(){return{link:function(e,t,c,i,s){s(e,function(e){t.append(e)})}}}).filter("highlight",function(){function e(e){return e.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(t,c){return c&&t?t.replace(new RegExp(e(c),"gi"),'$& '):t}}).factory("uisOffset",["$document","$window",function(e,t){return function(c){var i=c[0].getBoundingClientRect();return{width:i.width||c.prop("offsetWidth"),height:i.height||c.prop("offsetHeight"),top:i.top+(t.pageYOffset||e[0].documentElement.scrollTop),left:i.left+(t.pageXOffset||e[0].documentElement.scrollLeft)}}}]);c.directive("uiSelectChoices",["uiSelectConfig","uisRepeatParser","uiSelectMinErr","$compile",function(e,t,c,i){return{restrict:"EA",require:"^uiSelect",replace:!0,transclude:!0,templateUrl:function(t){var c=t.parent().attr("theme")||e.theme;return c+"/choices.tpl.html"},compile:function(s,l){if(!l.repeat)throw c("repeat","Expected 'repeat' expression.");return function(s,l,n,a,r){var o=n.groupBy;if(a.parseRepeatAttr(n.repeat,o),a.disableChoiceExpression=n.uiDisableChoice,a.onHighlightCallback=n.onHighlight,o){var u=l.querySelectorAll(".ui-select-choices-group");if(1!==u.length)throw c("rows","Expected 1 .ui-select-choices-group but got '{0}'.",u.length);u.attr("ng-repeat",t.getGroupNgRepeatExpression())}var d=l.querySelectorAll(".ui-select-choices-row");if(1!==d.length)throw c("rows","Expected 1 .ui-select-choices-row but got '{0}'.",d.length);d.attr("ng-repeat",t.getNgRepeatExpression(a.parserResult.itemName,"$select.items",a.parserResult.trackByExp,o)).attr("ng-if","$select.open").attr("ng-mouseenter","$select.setActiveItem("+a.parserResult.itemName+")").attr("ng-click","$select.select("+a.parserResult.itemName+",false,$event)");var p=l.querySelectorAll(".ui-select-choices-row-inner");if(1!==p.length)throw c("rows","Expected 1 .ui-select-choices-row-inner but got '{0}'.",p.length);p.attr("uis-transclude-append",""),i(l,r)(s),s.$watch("$select.search",function(e){e&&!a.open&&a.multiple&&a.activate(!1,!0),a.activeIndex=a.tagging.isActivated?-1:0,a.refresh(n.refresh)}),n.$observe("refreshDelay",function(){var t=s.$eval(n.refreshDelay);a.refreshDelay=void 0!==t?t:e.refreshDelay})}}}}]),c.controller("uiSelectCtrl",["$scope","$element","$timeout","$filter","uisRepeatParser","uiSelectMinErr","uiSelectConfig",function(t,c,i,s,l,n,a){function r(){(d.resetSearchInput||void 0===d.resetSearchInput&&a.resetSearchInput)&&(d.search=p,d.selected&&d.items.length&&!d.multiple&&(d.activeIndex=d.items.indexOf(d.selected)))}function o(t){var c=!0;switch(t){case e.DOWN:!d.open&&d.multiple?d.activate(!1,!0):d.activeIndex0||0===d.search.length&&d.tagging.isActivated&&d.activeIndex>-1)&&d.activeIndex--;break;case e.TAB:(!d.multiple||d.open)&&d.select(d.items[d.activeIndex],!0);break;case e.ENTER:d.open&&d.activeIndex>=0?d.select(d.items[d.activeIndex]):d.activate(!1,!0);break;case e.ESC:d.close();break;default:c=!1}return c}function u(){var e=c.querySelectorAll(".ui-select-choices-content"),t=e.querySelectorAll(".ui-select-choices-row");if(t.length<1)throw n("choices","Expected multiple .ui-select-choices-row but got '{0}'.",t.length);if(!(d.activeIndex<0)){var i=t[d.activeIndex],s=i.offsetTop+i.clientHeight-e[0].scrollTop,l=e[0].offsetHeight;s>l?e[0].scrollTop+=s-l:s=d.items.length?0:d.activeIndex,-1===d.activeIndex&&d.taggingLabel!==!1&&(d.activeIndex=0),i(function(){d.search=e||d.search,d.searchInput[0].focus()}))},d.findGroupByName=function(e){return d.groups&&d.groups.filter(function(t){return t.name===e})[0]},d.parseRepeatAttr=function(e,c){function i(e){d.groups=[],angular.forEach(e,function(e){var i=t.$eval(c),s=angular.isFunction(i)?i(e):e[i],l=d.findGroupByName(s);l?l.items.push(e):d.groups.push({name:s,items:[e]})}),d.items=[],d.groups.forEach(function(e){d.items=d.items.concat(e.items)})}function s(e){d.items=e}d.setItemsFn=c?i:s,d.parserResult=l.parse(e),d.isGrouped=!!c,d.itemProperty=d.parserResult.itemName,d.refreshItems=function(e){e=e||d.parserResult.source(t);var c=d.selected;if(angular.isArray(c)&&!c.length||!d.removeSelected)d.setItemsFn(e);else if(void 0!==e){var i=e.filter(function(e){return c.indexOf(e)<0});d.setItemsFn(i)}},t.$watchCollection(d.parserResult.source,function(e){if(void 0===e||null===e)d.items=[];else{if(!angular.isArray(e))throw n("items","Expected an array but got '{0}'.",e);d.refreshItems(e),d.ngModel.$modelValue=null}})};var h;d.refresh=function(e){void 0!==e&&(h&&i.cancel(h),h=i(function(){t.$eval(e)},d.refreshDelay))},d.setActiveItem=function(e){d.activeIndex=d.items.indexOf(e)},d.isActive=function(e){if(!d.open)return!1;var t=d.items.indexOf(e[d.itemProperty]),c=t===d.activeIndex;return!c||0>t&&d.taggingLabel!==!1||0>t&&d.taggingLabel===!1?!1:(c&&!angular.isUndefined(d.onHighlightCallback)&&e.$eval(d.onHighlightCallback),c)},d.isDisabled=function(e){if(d.open){var t,c=d.items.indexOf(e[d.itemProperty]),i=!1;return c>=0&&!angular.isUndefined(d.disableChoiceExpression)&&(t=d.items[c],i=!!e.$eval(d.disableChoiceExpression),t._uiSelectChoiceDisabled=i),i}},d.select=function(e,c,s){if(void 0===e||!e._uiSelectChoiceDisabled){if(!d.items&&!d.search)return;if(!e||!e._uiSelectChoiceDisabled){if(d.tagging.isActivated){if(d.taggingLabel===!1)if(d.activeIndex<0){if(e=void 0!==d.tagging.fct?d.tagging.fct(d.search):d.search,!e||angular.equals(d.items[0],e))return}else e=d.items[d.activeIndex];else if(0===d.activeIndex){if(void 0===e)return;if(void 0!==d.tagging.fct&&"string"==typeof e){if(e=d.tagging.fct(d.search),!e)return}else"string"==typeof e&&(e=e.replace(d.taggingLabel,"").trim())}if(d.selected&&angular.isArray(d.selected)&&d.selected.filter(function(t){return angular.equals(t,e)}).length>0)return d.close(c),void 0}t.$broadcast("uis:select",e);var l={};l[d.parserResult.itemName]=e,i(function(){d.onSelectCallback(t,{$item:e,$model:d.parserResult.modelMapper(t,l)})}),d.closeOnSelect&&d.close(c),s&&"click"===s.type&&(d.clickTriggeredSelect=!0)}}},d.close=function(e){d.open&&(d.ngModel&&d.ngModel.$setTouched&&d.ngModel.$setTouched(),r(),d.open=!1,t.$broadcast("uis:close",e))},d.setFocus=function(){d.focus||d.focusInput[0].focus()},d.clear=function(e){d.select(void 0),e.stopPropagation(),d.focusser[0].focus()},d.toggle=function(e){d.open?(d.close(),e.preventDefault(),e.stopPropagation()):d.activate()},d.isLocked=function(e,t){var c,i=d.selected[t];return i&&!angular.isUndefined(d.lockChoiceExpression)&&(c=!!e.$eval(d.lockChoiceExpression),i._uiSelectChoiceLocked=c),c};var g=null;d.sizeSearchInput=function(){var e=d.searchInput[0],c=d.searchInput.parent().parent()[0],s=function(){return c.clientWidth*!!e.offsetParent},l=function(t){if(0===t)return!1;var c=t-e.offsetLeft-10;return 50>c&&(c=t),d.searchInput.css("width",c+"px"),!0};d.searchInput.css("width","10px"),i(function(){null!==g||l(s())||(g=t.$watch(s,function(e){l(e)&&(g(),g=null)}))})},d.searchInput.on("keydown",function(c){var s=c.which;t.$apply(function(){var t=!1;if((d.items.length>0||d.tagging.isActivated)&&(o(s),d.taggingTokens.isActivated)){for(var l=0;l0&&(t=!0);t&&i(function(){d.searchInput.triggerHandler("tagged");var t=d.search.replace(e.MAP[c.keyCode],"").trim();d.tagging.fct&&(t=d.tagging.fct(t)),t&&d.select(t,!0)})}}),e.isVerticalMovement(s)&&d.items.length>0&&u()}),d.searchInput.on("paste",function(e){var t=e.originalEvent.clipboardData.getData("text/plain");if(t&&t.length>0&&d.taggingTokens.isActivated&&d.tagging.fct){var c=t.split(d.taggingTokens.tokens[0]);c&&c.length>0&&(angular.forEach(c,function(e){var t=d.tagging.fct(e);t&&d.select(t,!0)}),e.preventDefault(),e.stopPropagation())}}),d.searchInput.on("tagged",function(){i(function(){r()})}),t.$on("$destroy",function(){d.searchInput.off("keyup keydown tagged blur paste")})}]),c.directive("uiSelect",["$document","uiSelectConfig","uiSelectMinErr","uisOffset","$compile","$parse","$timeout",function(e,t,c,i,s,l,n){return{restrict:"EA",templateUrl:function(e,c){var i=c.theme||t.theme;return i+(angular.isDefined(c.multiple)?"/select-multiple.tpl.html":"/select.tpl.html")},replace:!0,transclude:!0,require:["uiSelect","^ngModel"],scope:!0,controller:"uiSelectCtrl",controllerAs:"$select",compile:function(s,a){return angular.isDefined(a.multiple)?s.append(" ").removeAttr("multiple"):s.append(" "),function(s,a,r,o,u){function d(e){if(g.open){var t=!1;if(t=window.jQuery?window.jQuery.contains(a[0],e.target):a[0].contains(e.target),!t&&!g.clickTriggeredSelect){var c=["input","button","textarea"],i=angular.element(e.target).scope(),l=i&&i.$select&&i.$select!==g;l||(l=~c.indexOf(e.target.tagName.toLowerCase())),g.close(l),s.$digest()}g.clickTriggeredSelect=!1}}function p(){var t=i(a);m=angular.element('
'),m[0].style.width=t.width+"px",m[0].style.height=t.height+"px",a.after(m),$=a[0].style.width,e.find("body").append(a),a[0].style.position="absolute",a[0].style.left=t.left+"px",a[0].style.top=t.top+"px",a[0].style.width=t.width+"px"}function h(){null!==m&&(m.replaceWith(a),m=null,a[0].style.position="",a[0].style.left="",a[0].style.top="",a[0].style.width=$)}var g=o[0],f=o[1];g.generatedId=t.generateId(),g.baseTitle=r.title||"Select box",g.focusserTitle=g.baseTitle+" focus",g.focusserId="focusser-"+g.generatedId,g.closeOnSelect=function(){return angular.isDefined(r.closeOnSelect)?l(r.closeOnSelect)():t.closeOnSelect}(),g.onSelectCallback=l(r.onSelect),g.onRemoveCallback=l(r.onRemove),g.ngModel=f,g.choiceGrouped=function(e){return g.isGrouped&&e&&e.name},r.tabindex&&r.$observe("tabindex",function(e){g.focusInput.attr("tabindex",e),a.removeAttr("tabindex")}),s.$watch("searchEnabled",function(){var e=s.$eval(r.searchEnabled);g.searchEnabled=void 0!==e?e:t.searchEnabled}),s.$watch("sortable",function(){var e=s.$eval(r.sortable);g.sortable=void 0!==e?e:t.sortable}),r.$observe("disabled",function(){g.disabled=void 0!==r.disabled?r.disabled:!1}),r.$observe("resetSearchInput",function(){var e=s.$eval(r.resetSearchInput);g.resetSearchInput=void 0!==e?e:!0}),r.$observe("tagging",function(){if(void 0!==r.tagging){var e=s.$eval(r.tagging);g.tagging={isActivated:!0,fct:e!==!0?e:void 0}}else g.tagging={isActivated:!1,fct:void 0}}),r.$observe("taggingLabel",function(){void 0!==r.tagging&&(g.taggingLabel="false"===r.taggingLabel?!1:void 0!==r.taggingLabel?r.taggingLabel:"(new)")}),r.$observe("taggingTokens",function(){if(void 0!==r.tagging){var e=void 0!==r.taggingTokens?r.taggingTokens.split("|"):[",","ENTER"];g.taggingTokens={isActivated:!0,tokens:e}}}),angular.isDefined(r.autofocus)&&n(function(){g.setFocus()}),angular.isDefined(r.focusOn)&&s.$on(r.focusOn,function(){n(function(){g.setFocus()})}),e.on("click",d),s.$on("$destroy",function(){e.off("click",d)}),u(s,function(e){var t=angular.element("").append(e),i=t.querySelectorAll(".ui-select-match");if(i.removeAttr("ui-select-match"),i.removeAttr("data-ui-select-match"),1!==i.length)throw c("transcluded","Expected 1 .ui-select-match but got '{0}'.",i.length);a.querySelectorAll(".ui-select-match").replaceWith(i);var s=t.querySelectorAll(".ui-select-choices");if(s.removeAttr("ui-select-choices"),s.removeAttr("data-ui-select-choices"),1!==s.length)throw c("transcluded","Expected 1 .ui-select-choices but got '{0}'.",s.length);a.querySelectorAll(".ui-select-choices").replaceWith(s)});var v=s.$eval(r.appendToBody);(void 0!==v?v:t.appendToBody)&&(s.$watch("$select.open",function(e){e?p():h()}),s.$on("$destroy",function(){h()}));var m=null,$=""}}}}]),c.directive("uiSelectMatch",["uiSelectConfig",function(e){return{restrict:"EA",require:"^uiSelect",replace:!0,transclude:!0,templateUrl:function(t){var c=t.parent().attr("theme")||e.theme,i=t.parent().attr("multiple");return c+(i?"/match-multiple.tpl.html":"/match.tpl.html")},link:function(t,c,i,s){function l(e){s.allowClear=angular.isDefined(e)?""===e?!0:"true"===e.toLowerCase():!1}s.lockChoiceExpression=i.uiLockChoice,i.$observe("placeholder",function(t){s.placeholder=void 0!==t?t:e.placeholder}),i.$observe("allowClear",l),l(i.allowClear),s.multiple&&s.sizeSearchInput()}}}]),c.directive("uiSelectMultiple",["uiSelectMinErr","$timeout",function(t,c){return{restrict:"EA",require:["^uiSelect","^ngModel"],controller:["$scope","$timeout",function(e,t){var c,i=this,s=e.$select;e.$evalAsync(function(){c=e.ngModel}),i.activeMatchIndex=-1,i.updateModel=function(){c.$setViewValue(Date.now()),i.refreshComponent()},i.refreshComponent=function(){s.refreshItems(),s.sizeSearchInput()},i.removeChoice=function(c){var l=s.selected[c];if(!l._uiSelectChoiceLocked){var n={};n[s.parserResult.itemName]=l,s.selected.splice(c,1),i.activeMatchIndex=-1,s.sizeSearchInput(),t(function(){s.onRemoveCallback(e,{$item:l,$model:s.parserResult.modelMapper(e,n)})}),i.updateModel()}},i.getPlaceholder=function(){return s.selected.length?void 0:s.placeholder}}],controllerAs:"$selectMultiple",link:function(i,s,l,n){function a(e){return angular.isNumber(e.selectionStart)?e.selectionStart:e.value.length}function r(t){function c(){switch(t){case e.LEFT:return~h.activeMatchIndex?u:n;case e.RIGHT:return~h.activeMatchIndex&&r!==n?o:(d.activate(),!1);case e.BACKSPACE:return~h.activeMatchIndex?(h.removeChoice(r),u):n;case e.DELETE:return~h.activeMatchIndex?(h.removeChoice(h.activeMatchIndex),r):!1}}var i=a(d.searchInput[0]),s=d.selected.length,l=0,n=s-1,r=h.activeMatchIndex,o=h.activeMatchIndex+1,u=h.activeMatchIndex-1,p=r;return i>0||d.search.length&&t==e.RIGHT?!1:(d.close(),p=c(),h.activeMatchIndex=d.selected.length&&p!==!1?Math.min(n,Math.max(l,p)):-1,!0)}function o(e){if(void 0===e||void 0===d.search)return!1;var t=e.filter(function(e){return void 0===d.search.toUpperCase()||void 0===e?!1:e.toUpperCase()===d.search.toUpperCase()}).length>0;return t}function u(e,t){var c=-1;if(angular.isArray(e))for(var i=angular.copy(e),s=0;s=0;s--)t={},t[d.parserResult.itemName]=d.selected[s],e=d.parserResult.modelMapper(i,t),c.unshift(e);return c}),p.$formatters.unshift(function(e){var t,c=d.parserResult.source(i,{$select:{search:""}}),s={};if(!c)return e;var l=[],n=function(e,c){if(e&&e.length){for(var n=e.length-1;n>=0;n--){if(s[d.parserResult.itemName]=e[n],t=d.parserResult.modelMapper(i,s),d.parserResult.trackByExp){var a=/\.(.+)/.exec(d.parserResult.trackByExp);if(a.length>0&&t[a[1]]==c[a[1]])return l.unshift(e[n]),!0}if(angular.equals(t,c))return l.unshift(e[n]),!0}return!1}};if(!e)return l;for(var a=e.length-1;a>=0;a--)n(d.selected,e[a])||n(c,e[a])||l.unshift(e[a]);return l}),i.$watchCollection(function(){return p.$modelValue},function(e,t){t!=e&&(p.$modelValue=null,h.refreshComponent())}),p.$render=function(){if(!angular.isArray(p.$viewValue)){if(!angular.isUndefined(p.$viewValue)&&null!==p.$viewValue)throw t("multiarr","Expected model value to be array but got '{0}'",p.$viewValue);d.selected=[]}d.selected=p.$viewValue,i.$evalAsync()},i.$on("uis:select",function(e,t){d.selected.push(t),h.updateModel()}),i.$on("uis:activate",function(){h.activeMatchIndex=-1}),i.$watch("$select.disabled",function(e,t){t&&!e&&d.sizeSearchInput()}),d.searchInput.on("keydown",function(t){var c=t.which;i.$apply(function(){var i=!1;e.isHorizontalMovement(c)&&(i=r(c)),i&&c!=e.TAB&&(t.preventDefault(),t.stopPropagation())})}),d.searchInput.on("keyup",function(t){if(e.isVerticalMovement(t.which)||i.$evalAsync(function(){d.activeIndex=d.taggingLabel===!1?-1:0}),d.tagging.isActivated&&d.search.length>0){if(t.which===e.TAB||e.isControl(t)||e.isFunctionKey(t)||t.which===e.ESC||e.isVerticalMovement(t.which))return;if(d.activeIndex=d.taggingLabel===!1?-1:0,d.taggingLabel===!1)return;var c,s,l,n,a=angular.copy(d.items),r=angular.copy(d.items),p=!1,h=-1;if(void 0!==d.tagging.fct){if(l=d.$filter("filter")(a,{isTag:!0}),l.length>0&&(n=l[0]),a.length>0&&n&&(p=!0,a=a.slice(1,a.length),r=r.slice(1,r.length)),c=d.tagging.fct(d.search),c.isTag=!0,r.filter(function(e){return angular.equals(e,d.tagging.fct(d.search))}).length>0)return;c.isTag=!0}else{if(l=d.$filter("filter")(a,function(e){return e.match(d.taggingLabel)}),l.length>0&&(n=l[0]),s=a[0],void 0!==s&&a.length>0&&n&&(p=!0,a=a.slice(1,a.length),r=r.slice(1,r.length)),c=d.search+" "+d.taggingLabel,u(d.selected,d.search)>-1)return;if(o(r.concat(d.selected)))return p&&(a=r,i.$evalAsync(function(){d.activeIndex=0,d.items=a})),void 0;if(o(r))return p&&(d.items=r.slice(1,r.length)),void 0}p&&(h=u(d.selected,c)),h>-1?a=a.slice(h+1,a.length-1):(a=[],a.push(c),a=a.concat(r)),i.$evalAsync(function(){d.activeIndex=0,d.items=a})}}),d.searchInput.on("blur",function(){c(function(){h.activeMatchIndex=-1})})}}}]),c.directive("uiSelectSingle",["$timeout","$compile",function(t,c){return{restrict:"EA",require:["^uiSelect","^ngModel"],link:function(i,s,l,n){var a=n[0],r=n[1];r.$parsers.unshift(function(e){var t,c={};return c[a.parserResult.itemName]=e,t=a.parserResult.modelMapper(i,c)}),r.$formatters.unshift(function(e){var t,c=a.parserResult.source(i,{$select:{search:""}}),s={};if(c){var l=function(c){return s[a.parserResult.itemName]=c,t=a.parserResult.modelMapper(i,s),t==e};if(a.selected&&l(a.selected))return a.selected;for(var n=c.length-1;n>=0;n--)if(l(c[n]))return c[n]}return e}),i.$watch("$select.selected",function(e){r.$viewValue!==e&&r.$setViewValue(e)}),r.$render=function(){a.selected=r.$viewValue},i.$on("uis:select",function(e,t){a.selected=t}),i.$on("uis:close",function(e,c){t(function(){a.focusser.prop("disabled",!1),c||a.focusser[0].focus()},0,!1)}),i.$on("uis:activate",function(){o.prop("disabled",!0)});var o=angular.element(" ");c(o)(i),a.focusser=o,a.focusInput=o,s.parent().append(o),o.bind("focus",function(){i.$evalAsync(function(){a.focus=!0})}),o.bind("blur",function(){i.$evalAsync(function(){a.focus=!1})}),o.bind("keydown",function(t){return t.which===e.BACKSPACE?(t.preventDefault(),t.stopPropagation(),a.select(void 0),i.$apply(),void 0):(t.which===e.TAB||e.isControl(t)||e.isFunctionKey(t)||t.which===e.ESC||((t.which==e.DOWN||t.which==e.UP||t.which==e.ENTER||t.which==e.SPACE)&&(t.preventDefault(),t.stopPropagation(),a.activate()),i.$digest()),void 0)}),o.bind("keyup input",function(t){t.which===e.TAB||e.isControl(t)||e.isFunctionKey(t)||t.which===e.ESC||t.which==e.ENTER||t.which===e.BACKSPACE||(a.activate(o.val()),o.val(""),i.$digest())})}}}]),c.directive("uiSelectSort",["$timeout","uiSelectConfig","uiSelectMinErr",function(e,t,c){return{require:"^uiSelect",link:function(t,i,s,l){if(null===t[s.uiSelectSort])throw c("sort","Expected a list to sort");var n=angular.extend({axis:"horizontal"},t.$eval(s.uiSelectSortOptions)),a=n.axis,r="dragging",o="dropping",u="dropping-before",d="dropping-after";t.$watch(function(){return l.sortable},function(e){e?i.attr("draggable",!0):i.removeAttr("draggable")}),i.on("dragstart",function(e){i.addClass(r),(e.dataTransfer||e.originalEvent.dataTransfer).setData("text/plain",t.$index)}),i.on("dragend",function(){i.removeClass(r)});var p,h=function(e,t){this.splice(t,0,this.splice(e,1)[0])},g=function(e){e.preventDefault();var t="vertical"===a?e.offsetY||e.layerY||(e.originalEvent?e.originalEvent.offsetY:0):e.offsetX||e.layerX||(e.originalEvent?e.originalEvent.offsetX:0);t
+
+
+
+ {{$item}}
+
+ {{category}}
+
+
+