Просмотр исходного кода

Merge branch 'master' into scratchy

Rashid Khan 12 лет назад
Родитель
Сommit
2f171c7f51

+ 4 - 2
src/app/app.js

@@ -11,7 +11,8 @@ define([
   'angular-sanitize',
   'angular-strap',
   'angular-dragdrop',
-  'extend-jquery'
+  'extend-jquery',
+  'bindonce'
 ],
 function (angular, $, _, appLevelRequire) {
   "use strict";
@@ -84,7 +85,8 @@ function (angular, $, _, appLevelRequire) {
     '$strap.directives',
     'ngSanitize',
     'ngDragDrop',
-    'kibana'
+    'kibana',
+    'pasvaz.bindonce'
   ];
 
   _.each('controllers directives factories services filters'.split(' '),

+ 20 - 0
src/app/components/kbn.js

@@ -479,6 +479,26 @@ function($, _, moment) {
     });
   };
 
+  // Find the smallest missing number in an array
+  kbn.smallestMissing = function(arr,start,end) {
+    start = start || 0;
+    end = end || arr.length-1;
+
+    if(start > end) {
+      return end + 1;
+    }
+    if(start !== arr[start]) {
+      return start;
+    }
+    var middle = Math.floor((start + end) / 2);
+
+    if (arr[middle] > middle) {
+      return kbn.smallestMissing(arr, start, middle);
+    } else {
+      return kbn.smallestMissing(arr, middle + 1, end);
+    }
+  };
+
   kbn.byteFormat = function(size, decimals) {
     var ext, steps = 0;
     decimals = decimals || 2;

+ 2 - 1
src/app/components/require.config.js

@@ -20,6 +20,7 @@ require.config({
     'angular-sanitize':       '../vendor/angular/angular-sanitize',
     timepicker:               '../vendor/angular/timepicker',
     datepicker:               '../vendor/angular/datepicker',
+    bindonce:                 '../vendor/angular/bindonce',
 
     underscore:               'components/underscore.extended',
     'underscore-src':         '../vendor/underscore',
@@ -84,7 +85,7 @@ require.config({
     'angular-resource':     ['angular'],
     'angular-route':        ['angular'],
     'angular-touch':        ['angular'],
-
+    'bindonce':             ['angular'],
     'angular-strap':        ['angular', 'bootstrap','timepicker', 'datepicker'],
 
     timepicker:             ['jquery', 'bootstrap'],

+ 0 - 11
src/app/dashboards/blank.json

@@ -2,12 +2,6 @@
   "title": "New Dashboard",
   "services": {
     "query": {
-      "idQueue": [
-        1,
-        2,
-        3,
-        4
-      ],
       "list": {
         "0": {
           "query": "*",
@@ -21,11 +15,6 @@
       ]
     },
     "filter": {
-      "idQueue": [
-        0,
-        1,
-        2
-      ],
       "list": {},
       "ids": []
     }

+ 0 - 11
src/app/dashboards/default.json

@@ -2,12 +2,6 @@
   "title": "Introduction",
   "services": {
     "query": {
-      "idQueue": [
-        1,
-        2,
-        3,
-        4
-      ],
       "list": {
         "0": {
           "query": "*",
@@ -23,11 +17,6 @@
       ]
     },
     "filter": {
-      "idQueue": [
-        0,
-        1,
-        2
-      ],
       "list": {},
       "ids": []
     }

+ 0 - 11
src/app/dashboards/guided.json

@@ -2,12 +2,6 @@
   "title": "Your Basic Dashboard",
   "services": {
     "query": {
-      "idQueue": [
-        1,
-        2,
-        3,
-        4
-      ],
       "list": {
         "0": {
           "query": "*",
@@ -23,11 +17,6 @@
       ]
     },
     "filter": {
-      "idQueue": [
-        0,
-        1,
-        2
-      ],
       "list": {},
       "ids": []
     }

+ 0 - 6
src/app/dashboards/logstash.json

@@ -2,9 +2,6 @@
   "title": "Logstash Search",
   "services": {
     "query": {
-      "idQueue": [
-        1
-      ],
       "list": {
         "0": {
           "query": "{{ARGS.query || '*'}}",
@@ -20,9 +17,6 @@
       ]
     },
     "filter": {
-      "idQueue": [
-        1
-      ],
       "list": {
         "0": {
           "type": "time",

+ 0 - 11
src/app/dashboards/noted.json

@@ -2,12 +2,6 @@
   "title": "A few notes",
   "services": {
     "query": {
-      "idQueue": [
-        1,
-        2,
-        3,
-        4
-      ],
       "list": {
         "0": {
           "query": "*",
@@ -23,11 +17,6 @@
       ]
     },
     "filter": {
-      "idQueue": [
-        0,
-        1,
-        2
-      ],
       "list": {},
       "ids": []
     }

+ 2 - 2
src/app/panels/table/editor.html

@@ -42,7 +42,7 @@
       <select class="input-small" ng-model="panel.style['font-size']" ng-options="f for f in ['7pt','8pt','9pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select></span>
     </div>
     <div class="span2">
-      <h6>Trim Factor <tip>Trim fields to this long divided by # of rows</tip></h6>
-      <input type="number" class="input-small" ng-model="panel.trimFactor">
+      <h6>Trim Factor <tip>Trim fields to this long divided by # of rows. Requires data refresh.</tip></h6>
+      <input type="number" class="input-small" ng-model="panel.trimFactor" ng-change="set_refresh(true)">
     </div>
   </div>

+ 24 - 19
src/app/panels/table/module.html

@@ -8,24 +8,29 @@
   </style>
 
   <div class="row-fluid">
-    <div ng-class="{'span3':panel.field_list}" ng-show="panel.field_list">
+    <div bindonce ng-class="{'span3':panel.field_list}" ng-if="panel.field_list">
       <div class="sidebar-nav">
         <strong>Fields <i class=" icon-chevron-sign-left pointer " ng-click="panel.field_list = !panel.field_list" bs-tooltip="'Hide field list'" ng-show="panel.field_list"></i></strong><p>
         <div class="small">
-          <span class="link" ng-click="panel.all_fields = true;" ng-class="{strong:panel.all_fields}">All</span> /
-           <span class="link" ng-click="panel.all_fields = false;" ng-class="{strong:!panel.all_fields}">Current</span>
+          <span class="link" ng-click="panel.all_fields = true;" ng-class="{strong:panel.all_fields}">
+            All ({{fields.list.length}})</span><br>
+          <span class="link" ng-click="panel.all_fields = false;" ng-class="{strong:!panel.all_fields}">
+            Current ({{current_fields.length || 0}})</span>
+
         </div>
         <div><input type="text" class="input-medium" placeholder="Type to filter..." ng-model="fieldFilter"></div>
 
-
-        <ul class="unstyled" style="{{panel.overflow}}:{{panel.height || row.height}};overflow-y:auto;overflow-x:hidden;">
-          <li ng-style="panel.style" ng-repeat="field in fields.list|filter:fieldFilter|orderBy:identity" ng-show="panel.all_fields">
-            <i class="pointer" ng-class="{'icon-check': _.contains(panel.fields,field),'icon-check-empty': !_.contains(panel.fields,field)}" ng-click="toggle_field(field)"></i>
-            <a class="pointer" data-unique="1" bs-popover="'app/panels/table/micropanel.html'" data-placement="rightTop" ng-click="toggle_micropanel(field,true)" ng-class="{label: _.contains(panel.fields,field)}">{{field}}</a>
+        <ul class="unstyled" style="{{panel.overflow}}:{{panel.height || row.height}};overflow-y:auto;overflow-x:hidden;" ng-if="panel.all_fields">
+          <li ng-style="panel.style" ng-repeat="field in fields.list|filter:fieldFilter|orderBy:identity">
+            <i class="pointer" ng-class="{'icon-check': columns[field],'icon-check-empty': _.isUndefined(columns[field])}" ng-click="toggle_field(field)"></i>
+            <a class="pointer" data-unique="1" bs-popover="'app/panels/table/micropanel.html'" data-placement="rightTop" ng-click="toggle_micropanel(field,true)" ng-class="{label: columns[field]}" bo-text="field"></a>
           </li>
-          <li ng-style="panel.style" ng-repeat="field in current_fields|filter:fieldFilter|orderBy:identity" ng-hide="panel.all_fields">
-            <i class="pointer" ng-class="{'icon-check': _.contains(panel.fields,field),'icon-check-empty': !_.contains(panel.fields,field)}" ng-click="toggle_field(field)"></i>
-            <a class="pointer" data-unique="1" bs-popover="'app/panels/table/micropanel.html'" data-placement="rightTop" ng-click="toggle_micropanel(field,true)" ng-class="{label: _.contains(panel.fields,field)}">{{field}}</a>
+        </ul>
+
+        <ul class="unstyled" style="{{panel.overflow}}:{{panel.height || row.height}};overflow-y:auto;overflow-x:hidden;" ng-if="!panel.all_fields">
+          <li ng-style="panel.style" ng-repeat="field in current_fields|filter:fieldFilter|orderBy:identity">
+            <i class="pointer" ng-class="{'icon-check': columns[field],'icon-check-empty': _.isUndefined(columns[field])}" ng-click="toggle_field(field)"></i>
+            <a class="pointer" data-unique="1" bs-popover="'app/panels/table/micropanel.html'" data-placement="rightTop" ng-click="toggle_micropanel(field,true)" ng-class="{label: columns[field]}" bo-text="field"></a>
           </li>
         </ul>
 
@@ -61,12 +66,12 @@
           </th>
 
         </thead>
-        <tbody ng-repeat="event in data| slice:panel.offset:panel.offset+panel.size" ng-class-odd="'odd'">
+        <tbody bindonce ng-repeat="event in data| slice:panel.offset:panel.offset+panel.size" ng-class-odd="'odd'">
           <tr ng-click="toggle_details(event)" class="pointer">
-            <td ng-show="panel.fields.length<1">{{event._source|stringify|tableTruncate:panel.trimFactor:1}}</td>
-            <td ng-show="panel.fields.length>0" ng-repeat="field in panel.fields" ng-bind-html-unsafe="(event.kibana.highlight[field]||event.kibana._source[field]) |tableHighlight | tableTruncate:panel.trimFactor:panel.fields.length"></td>
+            <td ng-if="panel.fields.length<1" bo-text="event._source|stringify|tableTruncate:panel.trimFactor:1"></td>
+            <td ng-show="panel.fields.length>0" ng-repeat="field in panel.fields" bo-html="(event.kibana.highlight[field]||event.kibana._source[field]) |tableHighlight | tableTruncate:panel.trimFactor:panel.fields.length"></td>
           </tr>
-          <tr ng-show="event.kibana.details">
+          <tr ng-if="event.kibana.details">
             <td colspan={{panel.fields.length}} ng-switch="event.kibana.view">
               <span>
                 View:
@@ -82,18 +87,18 @@
                   <th>Value</th>
                 </thead>
                 <tr ng-repeat="(key,value) in event.kibana._source track by $index" ng-class-odd="'odd'">
-                  <td>{{key}}</td>
+                  <td bo-text="key"></td>
                   <td style="white-space:nowrap">
                     <i class='icon-search pointer' ng-click="build_search(key,value)" bs-tooltip="'Add filter to match this value'"></i>
                     <i class='icon-ban-circle pointer' ng-click="build_search(key,value,true)" bs-tooltip="'Add filter to NOT match this value'"></i>
                     <i class="pointer icon-th" ng-click="toggle_field(key)" bs-tooltip="'Toggle table column'"></i>
                   </td>
                   <!-- At some point we need to create a more efficient way of applying the filter pipeline -->
-                  <td style="white-space:pre-wrap" ng-bind-html-unsafe="value|noXml|urlLink|stringify"></td>
+                  <td style="white-space:pre-wrap" bo-html="value|noXml|urlLink|stringify"></td>
                 </tr>
               </table>
-              <pre style="white-space:pre-wrap"  ng-bind-html="without_kibana(event)|tableJson:2" ng-switch-when="json"></pre>
-              <pre ng-bind-html="without_kibana(event)|tableJson:1" ng-switch-when="raw"></pre>
+              <pre style="white-space:pre-wrap"  bo-html="without_kibana(event)|tableJson:2" ng-switch-when="json"></pre>
+              <pre bo-html="without_kibana(event)|tableJson:1" ng-switch-when="raw"></pre>
             </td>
           </tr>
         </tbody>

+ 7 - 0
src/app/panels/table/module.js

@@ -129,6 +129,11 @@ function (angular, app, _, kbn, moment) {
     _.defaults($scope.panel,_d);
 
     $scope.init = function () {
+      $scope.columns = {};
+      _.each($scope.panel.fields,function(field) {
+        $scope.columns[field] = true;
+      });
+
       $scope.Math = Math;
       $scope.identity = angular.identity;
       $scope.$on('refresh',function(){$scope.get_data();});
@@ -203,8 +208,10 @@ function (angular, app, _, kbn, moment) {
     $scope.toggle_field = function(field) {
       if (_.indexOf($scope.panel.fields,field) > -1) {
         $scope.panel.fields = _.without($scope.panel.fields,field);
+        delete $scope.columns[field];
       } else {
         $scope.panel.fields.push(field);
+        $scope.columns[field] = true;
       }
     };
 

+ 13 - 11
src/app/services/dashboard.js

@@ -184,17 +184,19 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
       // Set the current dashboard
       self.current = _.clone(dashboard);
 
-      // Ok, now that we've setup the current dashboard, we can inject our services
-      querySrv = $injector.get('querySrv');
-      filterSrv = $injector.get('filterSrv');
-
-      // Make sure these re-init
-      querySrv.init();
-      filterSrv.init();
-
-      // If there's an interval set, the indices have not been calculated yet,
-      // so there is no data. Call refresh to calculate the indices and notify the panels.
-      self.refresh();
+      // Delay this until we're sure that querySrv and filterSrv are ready
+      $timeout(function() {
+        // Ok, now that we've setup the current dashboard, we can inject our services
+        querySrv = $injector.get('querySrv');
+        filterSrv = $injector.get('filterSrv');
+
+        // Make sure these re-init
+        querySrv.init();
+        filterSrv.init();
+      },0).then(function() {
+        // Call refresh to calculate the indices and notify the panels that we're ready to roll
+        self.refresh();
+      });
 
       if(dashboard.refresh) {
         self.set_interval(dashboard.refresh);

+ 11 - 8
src/app/services/fields.js

@@ -14,20 +14,22 @@ function (angular, _, config) {
 
     this.list = ['_type'];
     this.mapping = {};
+    this.fullMapping = {};
 
     $rootScope.$watch(function(){return dashboard.indices;},function(n) {
       if(!_.isUndefined(n) && n.length) {
         // Only get the mapping for indices we don't know it for
-        var indices = _.difference(n,_.keys(self.mapping));
+        var indices = _.difference(n,_.keys(self.fullMapping));
         // Only get the mapping if there are new indices
         if(indices.length > 0) {
           self.map(indices).then(function(result) {
-            self.mapping = _.extend(self.mapping,result);
-            self.list = mapFields(self.mapping);
+            self.fullMapping = _.extend(self.fullMapping,result);
+            self.list = mapFields(self.fullMapping);
           });
         // Otherwise just use the cached mapping
         } else {
-          self.list = mapFields(_.pick(self.mapping,n));
+          // This is inefficient, should not need to reprocess?
+          self.list = mapFields(_.pick(self.fullMapping,n));
         }
       }
     });
@@ -57,12 +59,13 @@ function (angular, _, config) {
         }
       });
 
+      // Flatten the mapping of each index into dot notated keys.
       return request.then(function(p) {
         var mapping = {};
-        _.each(p.data, function(v,k) {
-          mapping[k] = {};
-          _.each(v, function (v,f) {
-            mapping[k][f] = flatten(v);
+        _.each(p.data, function(type,index) {
+          mapping[index] = {};
+          _.each(type, function (fields,typename) {
+            mapping[index][typename] = flatten(fields);
           });
         });
         return mapping;

+ 7 - 8
src/app/services/filterSrv.js

@@ -14,14 +14,12 @@ define([
 
     // Defaults for it
     var _d = {
-      idQueue : [],
       list : {},
       ids : []
     };
 
     // For convenience
     var ejs = ejsResource(config.elasticsearch);
-    var _f = dashboard.current.services.filter;
 
     // Save a reference to this
     var self = this;
@@ -34,7 +32,6 @@ define([
       // Accessors
       self.list = dashboard.current.services.filter.list;
       self.ids = dashboard.current.services.filter.ids;
-      _f = dashboard.current.services.filter;
 
       _.each(self.list,function(f) {
         self.set(f,f.id,true);
@@ -97,8 +94,6 @@ define([
         delete self.list[id];
         // This must happen on the full path also since _.without returns a copy
         self.ids = dashboard.current.services.filter.ids = _.without(self.ids,id);
-        _f.idQueue.unshift(id);
-        _f.idQueue.sort(function(v,k){return v-k;});
         _r = true;
       } else {
         _r = false;
@@ -230,10 +225,14 @@ define([
     };
 
     var nextId = function() {
-      if(_f.idQueue.length > 0) {
-        return _f.idQueue.shift();
+      var idCount = dashboard.current.services.filter.ids.length;
+      if(idCount > 0) {
+        // Make a sorted copy of the ids array
+        var ids = _.clone(dashboard.current.services.filter.ids).sort();
+        return kbn.smallestMissing(ids);
       } else {
-        return self.ids.length;
+        // No ids currently in list
+        return 0;
       }
     };
 

+ 7 - 9
src/app/services/querySrv.js

@@ -14,7 +14,6 @@ function (angular, _, config, kbn) {
     // Create an object to hold our service state on the dashboard
     dashboard.current.services.query = dashboard.current.services.query || {};
     _.defaults(dashboard.current.services.query,{
-      idQueue : [],
       list : {},
       ids : [],
     });
@@ -31,7 +30,6 @@ function (angular, _, config, kbn) {
 
     // For convenience
     var ejs = ejsResource(config.elasticsearch);
-    var _q = dashboard.current.services.query;
 
     // Holds all actual queries, including all resolved abstract queries
     var resolvedQueries = [];
@@ -176,10 +174,6 @@ function (angular, _, config, kbn) {
         delete self.list[id];
         // This must happen on the full path also since _.without returns a copy
         self.ids = dashboard.current.services.query.ids = _.without(self.ids,id);
-        _q.idQueue.unshift(id);
-        _q.idQueue.sort(function(v,k){
-          return v-k;
-        });
         return true;
       } else {
         return false;
@@ -246,10 +240,14 @@ function (angular, _, config, kbn) {
     };
 
     var nextId = function() {
-      if(_q.idQueue.length > 0) {
-        return _q.idQueue.shift();
+      var idCount = dashboard.current.services.query.ids.length;
+      if(idCount > 0) {
+        // Make a sorted copy of the ids array
+        var ids = _.clone(dashboard.current.services.query.ids).sort();
+        return kbn.smallestMissing(ids);
       } else {
-        return self.ids.length;
+        // No ids currently in list
+        return 0;
       }
     };
 

+ 7 - 3
src/vendor/angular/angular-strap.js

@@ -712,11 +712,15 @@ angular.module('$strap.directives').directive('bsTabs', [
             scope.$watch(iAttrs.ngModel, function (newValue, oldValue) {
               if (angular.isUndefined(newValue))
                 return;
+              console.log(oldValue +" -> "+ newValue);
               activeTab = newValue;
               setTimeout(function () {
-                var $next = $($tabs[0].querySelectorAll('li')[newValue * 1]);
-                if (!$next.hasClass('active')) {
-                  $next.children('a').tab('show');
+                // Check if we're still on the same tab before making the switch
+                if(activeTab === newValue) {
+                  var $next = $($tabs[0].querySelectorAll('li')[newValue * 1]);
+                  if (!$next.hasClass('active')) {
+                    $next.children('a').tab('show');
+                  }
                 }
               });
             });

+ 269 - 0
src/vendor/angular/bindonce.js

@@ -0,0 +1,269 @@
+'use strict';
+/**
+ * Bindonce - Zero watches binding for AngularJs
+ * @version v0.2.1 - 2013-05-07
+ * @link https://github.com/Pasvaz/bindonce
+ * @author Pasquale Vazzana <pasqualevazzana@gmail.com>
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */
+
+ angular.module('pasvaz.bindonce', [])
+
+ .directive('bindonce', function()
+ {
+  var toBoolean = function(value)
+  {
+    if (value && value.length !== 0)
+    {
+      var v = angular.lowercase("" + value);
+      value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]');
+    }
+    else
+    {
+      value = false;
+    }
+    return value;
+  }
+
+  var msie = parseInt((/msie (\d+)/.exec(angular.lowercase(navigator.userAgent)) || [])[1], 10);
+  if (isNaN(msie))
+  {
+    msie = parseInt((/trident\/.*; rv:(\d+)/.exec(angular.lowercase(navigator.userAgent)) || [])[1], 10);
+  }
+
+  var bindonceDirective =
+  {
+    restrict: "AM",
+    controller: ['$scope', '$element', '$attrs', '$interpolate', function($scope, $element, $attrs, $interpolate)
+    {
+      var showHideBinder = function(elm, attr, value)
+      {
+        var show = (attr == 'show') ? '' : 'none';
+        var hide = (attr == 'hide') ? '' : 'none';
+        elm.css('display', toBoolean(value) ? show : hide);
+      }
+      var classBinder = function(elm, value)
+      {
+        if (angular.isObject(value) && !angular.isArray(value))
+        {
+          var results = [];
+          angular.forEach(value, function(value, index)
+          {
+            if (value) results.push(index);
+          });
+          value = results;
+        }
+        if (value)
+        {
+          elm.addClass(angular.isArray(value) ? value.join(' ') : value);
+        }
+      }
+
+      var ctrl =
+      {
+        watcherRemover : undefined,
+        binders : [],
+        group : $attrs.boName,
+        element : $element,
+        ran : false,
+
+        addBinder : function(binder)
+        {
+          this.binders.push(binder);
+
+          // In case of late binding (when using the directive bo-name/bo-parent)
+          // it happens only when you use nested bindonce, if the bo-children
+          // are not dom children the linking can follow another order
+          if (this.ran)
+          {
+            this.runBinders();
+          }
+        },
+
+        setupWatcher : function(bindonceValue)
+        {
+          var that = this;
+          this.watcherRemover = $scope.$watch(bindonceValue, function(newValue)
+          {
+            if (newValue == undefined) return;
+            that.removeWatcher();
+            that.runBinders();
+          }, true);
+        },
+
+        removeWatcher : function()
+        {
+          if (this.watcherRemover != undefined)
+          {
+            this.watcherRemover();
+            this.watcherRemover = undefined;
+          }
+        },
+
+        runBinders : function()
+        {
+          var i, max;
+          for (i = 0, max = this.binders.length; i < max; i ++)
+          {
+            var binder = this.binders[i];
+            if (this.group && this.group != binder.group ) continue;
+            var value = binder.scope.$eval((binder.interpolate) ? $interpolate(binder.value) : binder.value);
+            switch(binder.attr)
+            {
+              case 'if':
+                if (toBoolean(value))
+                {
+                  binder.transclude(binder.scope.$new(), function (clone)
+                  {
+                    var parent = binder.element.parent();
+                    var afterNode = binder.element && binder.element[binder.element.length - 1];
+                    var parentNode = parent && parent[0] || afterNode && afterNode.parentNode;
+                    var afterNextSibling = (afterNode && afterNode.nextSibling) || null;
+                    angular.forEach(clone, function(node)
+                    {
+                      parentNode.insertBefore(node, afterNextSibling);
+                    });
+                  });
+                }
+                break;
+              case 'hide':
+              case 'show':
+                showHideBinder(binder.element, binder.attr, value);
+                break;
+              case 'class':
+                classBinder(binder.element, value);
+                break;
+              case 'text':
+                binder.element.text(value);
+                break;
+              case 'html':
+                binder.element.html(value);
+                break;
+              case 'style':
+                binder.element.css(value);
+                break;
+              case 'src':
+                binder.element.attr(binder.attr, value);
+                if (msie) binder.element.prop('src', value);
+              case 'attr':
+                angular.forEach(binder.attrs, function(attrValue, attrKey)
+                {
+                  var newAttr, newValue;
+                  if (attrKey.match(/^boAttr./) && binder.attrs[attrKey])
+                  {
+                    newAttr = attrKey.replace(/^boAttr/, '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+                    newValue = binder.scope.$eval(binder.attrs[attrKey]);
+                    binder.element.attr(newAttr, newValue);
+                  }
+                });
+                break;
+              case 'href':
+              case 'alt':
+              case 'title':
+              case 'id':
+              case 'value':
+                binder.element.attr(binder.attr, value);
+                break;
+            }
+          }
+          this.ran = true;
+          this.binders = [];
+        }
+      }
+
+      return ctrl;
+    }],
+
+    link: function(scope, elm, attrs, bindonceController)
+    {
+      var value = (attrs.bindonce) ? scope.$eval(attrs.bindonce) : true;
+      if (value != undefined)
+      {
+        bindonceController.runBinders();
+      }
+      else
+      {
+        bindonceController.setupWatcher(attrs.bindonce);
+        elm.bind("$destroy", bindonceController.removeWatcher);
+      }
+    }
+  };
+
+  return bindonceDirective;
+});
+
+angular.forEach(
+[
+  {directiveName:'boShow', attribute: 'show'},
+  {directiveName:'boIf', attribute: 'if', transclude: 'element', terminal: true, priority:1000},
+  {directiveName:'boHide', attribute:'hide'},
+  {directiveName:'boClass', attribute:'class'},
+  {directiveName:'boText', attribute:'text'},
+  {directiveName:'boHtml', attribute:'html'},
+  {directiveName:'boSrcI', attribute:'src', interpolate:true},
+  {directiveName:'boSrc', attribute:'src'},
+  {directiveName:'boHrefI', attribute:'href', interpolate:true},
+  {directiveName:'boHref', attribute:'href'},
+  {directiveName:'boAlt', attribute:'alt'},
+  {directiveName:'boTitle', attribute:'title'},
+  {directiveName:'boId', attribute:'id'},
+  {directiveName:'boStyle', attribute:'style'},
+  {directiveName:'boValue', attribute:'value'},
+  {directiveName:'boAttr', attribute:'attr'}
+],
+function(boDirective)
+{
+  var childPriority = 200;
+  return angular.module('pasvaz.bindonce').directive(boDirective.directiveName, function()
+  {
+    var bindonceDirective =
+    {
+      priority: boDirective.priority || childPriority,
+      transclude: boDirective.transclude || false,
+      terminal: boDirective.terminal || false,
+      require: '^bindonce',
+      compile: function (tElement, tAttrs, transclude)
+      {
+        return function(scope, elm, attrs, bindonceController)
+        {
+          var name = attrs.boParent;
+          if (name && bindonceController.group != name)
+          {
+            var element = bindonceController.element.parent();
+            bindonceController = undefined;
+            var parentValue;
+
+            while (element[0].nodeType != 9 && element.length)
+            {
+              if ((parentValue = element.data('$bindonceController'))
+                && parentValue.group == name)
+              {
+                bindonceController = parentValue
+                break;
+              }
+              element = element.parent();
+            }
+            if (!bindonceController)
+            {
+              throw Error("No bindonce controller: " + name);
+            }
+          }
+
+          bindonceController.addBinder(
+          {
+            element   :   elm,
+            attr    :   boDirective.attribute,
+            attrs     :   attrs,
+            value   :   attrs[boDirective.directiveName],
+            interpolate :   boDirective.interpolate,
+            group   :   name,
+            transclude  :   transclude,
+            scope   :   scope
+          });
+        }
+      }
+    }
+
+    return bindonceDirective;
+  });
+});