bootstrap-x-editable-rails 1.4.0 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # X-editable for Rails asset pipeline
1
+ # X-editable 1.4.1 for Rails
2
2
 
3
3
  [X-editable](https://github.com/vitalets/x-editable) is an in-place editing plugin with support for Twitter Bootstrap, jQuery UI or pure jQuery.
4
4
 
5
- The `bootstrap-x-editable-rails` gem integrates `X-editable` with the Rails asset pipeline. This gem only supports the Bootstrap part of X-editable.
5
+ The `bootstrap-x-editable-rails` gem integrates `X-editable` with Rails asset pipeline. This gem only supports the Bootstrap part of X-editable.
6
6
 
7
7
  ## Usage
8
8
 
@@ -31,3 +31,14 @@ Add to your `app/assets/javascripts/application.js`
31
31
  Add to your `app/assets/stylesheets/application.css`
32
32
 
33
33
  *= require bootstrap-editable
34
+
35
+ ## Updating the gem
36
+ There are two rake tasks designed to ease the maintenance of this gem.
37
+
38
+ The `update` task pulls the latest X-editable code from github and places images, stylesheets and javascripts in the correct gem paths. It also changes background-image properties in the stylesheet to asset pipeline equivalents.
39
+
40
+ rake update
41
+
42
+ The `build` task is a simple wrapper for `gem build`
43
+
44
+ rake build
@@ -0,0 +1,38 @@
1
+ desc "Update"
2
+ task :update do
3
+ src_path = "x-editable-src"
4
+ dist_path = "#{src_path}/dist/bootstrap-editable"
5
+
6
+ system("rm -rf #{src_path}")
7
+
8
+ system("git clone https://github.com/vitalets/x-editable #{src_path}")
9
+ system("cd #{src_path} && npm install")
10
+ system("cd #{src_path} && grunt build")
11
+
12
+ system("cp #{dist_path}/img/clear.png vendor/assets/images/")
13
+ system("cp #{dist_path}/img/loading.gif vendor/assets/images/")
14
+ system("cp #{dist_path}/css/bootstrap-editable.css vendor/assets/stylesheets/bootstrap-editable.scss")
15
+ system("cp #{dist_path}/js/bootstrap-editable.js vendor/assets/javascripts/")
16
+
17
+ fixes
18
+
19
+ system("rm -rf x-editable-src")
20
+ end
21
+
22
+ def fixes
23
+ replace_string_in_file("vendor/assets/stylesheets/bootstrap-editable.scss", "url('../img/loading.gif')", "image-url('loading.gif')")
24
+ replace_string_in_file("vendor/assets/stylesheets/bootstrap-editable.scss", "url('../img/clear.png')", "image-url('clear.png')")
25
+ end
26
+
27
+ def replace_string_in_file(file, find, replace)
28
+ file_content = File.read(file)
29
+
30
+ File.open(file, "w") do |f|
31
+ f.puts file_content.gsub!(find, replace)
32
+ end
33
+ end
34
+
35
+ desc "Build"
36
+ task "build" do
37
+ system("gem build bootstrap-x-editable-rails.gemspec")
38
+ end
@@ -1,5 +1,5 @@
1
1
  module BootstrapXEditableRails
2
2
  module Rails
3
- VERSION = "1.4.0"
3
+ VERSION = "1.4.1"
4
4
  end
5
5
  end
@@ -1,4 +1,4 @@
1
- /*! X-editable - v1.4.0
1
+ /*! X-editable - v1.4.1
2
2
  * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
3
3
  * http://github.com/vitalets/x-editable
4
4
  * Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
@@ -16,7 +16,7 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
16
16
 
17
17
  var EditableForm = function (div, options) {
18
18
  this.options = $.extend({}, $.fn.editableform.defaults, options);
19
- this.$div = $(div); //div, containing form. Not form tag! Not editable-element.
19
+ this.$div = $(div); //div, containing form. Not form tag. Not editable-element.
20
20
  if(!this.options.scope) {
21
21
  this.options.scope = this;
22
22
  }
@@ -30,6 +30,7 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
30
30
  this.input = this.options.input;
31
31
 
32
32
  //set initial value
33
+ //todo: may be add check: typeof str === 'string' ?
33
34
  this.value = this.input.str2value(this.options.value);
34
35
  },
35
36
  initTemplate: function() {
@@ -232,6 +233,7 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
232
233
  }
233
234
 
234
235
  //if success callback returns object like {newValue: <something>} --> use that value instead of submitted
236
+ //it is usefull if you want to chnage value in url-function
235
237
  if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) {
236
238
  newValue = res.newValue;
237
239
  }
@@ -387,18 +389,25 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
387
389
  type: 'text',
388
390
  /**
389
391
  Url for submit, e.g. <code>'/post'</code>
390
- If function - it will be called instead of ajax. Function can return deferred object to run fail/done callbacks.
392
+ If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks.
391
393
 
392
394
  @property url
393
395
  @type string|function
394
396
  @default null
395
397
  @example
396
398
  url: function(params) {
399
+ var d = new $.Deferred;
397
400
  if(params.value === 'abc') {
398
- var d = new $.Deferred;
399
401
  return d.reject('error message'); //returning error via deferred object
400
402
  } else {
401
- someModel.set(params.name, params.value); //save data in some js model
403
+ //async saving data in js model
404
+ someModel.asyncSaveMethod({
405
+ ...,
406
+ success: function(){
407
+ d.resolve();
408
+ }
409
+ });
410
+ return d.promise();
402
411
  }
403
412
  }
404
413
  **/
@@ -689,21 +698,34 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
689
698
  /*
690
699
  returns array items from sourceData having value property equal or inArray of 'value'
691
700
  */
692
- itemsByValue: function(value, sourceData) {
701
+ itemsByValue: function(value, sourceData, valueProp) {
693
702
  if(!sourceData || value === null) {
694
703
  return [];
695
704
  }
696
705
 
697
- //convert to array
698
- if(!$.isArray(value)) {
699
- value = [value];
700
- }
706
+ valueProp = valueProp || 'value';
701
707
 
702
- /*jslint eqeq: true*/
703
- var result = $.grep(sourceData, function(o){
704
- return $.grep(value, function(v){ return v == o.value; }).length;
708
+ var isValArray = $.isArray(value),
709
+ result = [],
710
+ that = this;
711
+
712
+ $.each(sourceData, function(i, o) {
713
+ if(o.children) {
714
+ result = result.concat(that.itemsByValue(value, o.children));
715
+ } else {
716
+ /*jslint eqeq: true*/
717
+ if(isValArray) {
718
+ if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? o[valueProp] : o); }).length) {
719
+ result.push(o);
720
+ }
721
+ } else {
722
+ if(value == (o && typeof o === 'object' ? o[valueProp] : o)) {
723
+ result.push(o);
724
+ }
725
+ }
726
+ /*jslint eqeq: false*/
727
+ }
705
728
  });
706
- /*jslint eqeq: false*/
707
729
 
708
730
  return result;
709
731
  },
@@ -785,8 +807,8 @@ Applied as jQuery method.
785
807
  innerCss: null, //tbd in child class
786
808
  init: function(element, options) {
787
809
  this.$element = $(element);
788
- //todo: what is in priority: data or js?
789
- this.options = $.extend({}, $.fn.editableContainer.defaults, $.fn.editableutils.getConfigData(this.$element), options);
810
+ //since 1.4.1 container do not use data-* directly as they already merged into options.
811
+ this.options = $.extend({}, $.fn.editableContainer.defaults, options);
790
812
  this.splitOptions();
791
813
 
792
814
  //set scope of form callbacks to element
@@ -878,6 +900,7 @@ Applied as jQuery method.
878
900
  cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button
879
901
  show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state)
880
902
  rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown
903
+ resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed
881
904
  rendered: $.proxy(function(){
882
905
  /**
883
906
  Fired when container is shown and form is rendered (for select will wait for loading dropdown options)
@@ -1004,7 +1027,6 @@ Applied as jQuery method.
1004
1027
  },
1005
1028
 
1006
1029
  save: function(e, params) {
1007
- this.hide('save');
1008
1030
  /**
1009
1031
  Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance
1010
1032
 
@@ -1025,6 +1047,9 @@ Applied as jQuery method.
1025
1047
  });
1026
1048
  **/
1027
1049
  this.$element.triggerHandler('save', params);
1050
+
1051
+ //hide must be after trigger, as saving value may require methods od plugin, applied to input
1052
+ this.hide('save');
1028
1053
  },
1029
1054
 
1030
1055
  /**
@@ -1255,12 +1280,14 @@ Applied as jQuery method.
1255
1280
  innerHide: function () {
1256
1281
  this.$tip.hide(this.options.anim, $.proxy(function() {
1257
1282
  this.$element.show();
1258
- this.tip().empty().remove();
1283
+ this.innerDestroy();
1259
1284
  }, this));
1260
1285
  },
1261
1286
 
1262
1287
  innerDestroy: function() {
1263
- this.tip().remove();
1288
+ if(this.tip()) {
1289
+ this.tip().empty().remove();
1290
+ }
1264
1291
  }
1265
1292
  });
1266
1293
 
@@ -1275,8 +1302,13 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1275
1302
 
1276
1303
  var Editable = function (element, options) {
1277
1304
  this.$element = $(element);
1278
- this.options = $.extend({}, $.fn.editable.defaults, $.fn.editableutils.getConfigData(this.$element), options);
1279
- this.init();
1305
+ //data-* has more priority over js options: because dynamically created elements may change data-*
1306
+ this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element));
1307
+ if(this.options.selector) {
1308
+ this.initLive();
1309
+ } else {
1310
+ this.init();
1311
+ }
1280
1312
  };
1281
1313
 
1282
1314
  Editable.prototype = {
@@ -1319,8 +1351,10 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1319
1351
  if(this.options.toggle !== 'manual') {
1320
1352
  this.$element.addClass('editable-click');
1321
1353
  this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){
1354
+ //prevent following link
1322
1355
  e.preventDefault();
1323
- //stop propagation not required anymore because in document click handler it checks event target
1356
+
1357
+ //stop propagation not required because in document click handler it checks event target
1324
1358
  //e.stopPropagation();
1325
1359
 
1326
1360
  if(this.options.toggle === 'mouseenter') {
@@ -1362,6 +1396,24 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1362
1396
  }, this));
1363
1397
  },
1364
1398
 
1399
+ /*
1400
+ Initializes parent element for live editables
1401
+ */
1402
+ initLive: function() {
1403
+ //store selector
1404
+ var selector = this.options.selector;
1405
+ //modify options for child elements
1406
+ this.options.selector = false;
1407
+ this.options.autotext = 'never';
1408
+ //listen toggle events
1409
+ this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){
1410
+ var $target = $(e.target);
1411
+ if(!$target.data('editable')) {
1412
+ $target.editable(this.options).trigger(e);
1413
+ }
1414
+ }, this));
1415
+ },
1416
+
1365
1417
  /*
1366
1418
  Renders value into element's text.
1367
1419
  Can call custom display method from options.
@@ -1375,8 +1427,8 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1375
1427
  return;
1376
1428
  }
1377
1429
 
1378
- //if it is input with source, we pass callback in third param to be called when source is loaded
1379
- if(this.input.options.hasOwnProperty('source')) {
1430
+ //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded
1431
+ if(this.input.value2htmlFinal) {
1380
1432
  return this.input.value2html(this.value, this.$element[0], this.options.display, response);
1381
1433
  //if display method defined --> use it
1382
1434
  } else if(typeof this.options.display === 'function') {
@@ -1394,7 +1446,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1394
1446
  enable: function() {
1395
1447
  this.options.disabled = false;
1396
1448
  this.$element.removeClass('editable-disabled');
1397
- this.handleEmpty();
1449
+ this.handleEmpty(this.isEmpty);
1398
1450
  if(this.options.toggle !== 'manual') {
1399
1451
  if(this.$element.attr('tabindex') === '-1') {
1400
1452
  this.$element.removeAttr('tabindex');
@@ -1410,7 +1462,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1410
1462
  this.options.disabled = true;
1411
1463
  this.hide();
1412
1464
  this.$element.addClass('editable-disabled');
1413
- this.handleEmpty();
1465
+ this.handleEmpty(this.isEmpty);
1414
1466
  //do not stop focus on this element
1415
1467
  this.$element.attr('tabindex', -1);
1416
1468
  },
@@ -1471,27 +1523,33 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1471
1523
  },
1472
1524
 
1473
1525
  /*
1474
- * set emptytext if element is empty (reverse: remove emptytext if needed)
1526
+ * set emptytext if element is empty
1475
1527
  */
1476
- handleEmpty: function () {
1528
+ handleEmpty: function (isEmpty) {
1477
1529
  //do not handle empty if we do not display anything
1478
1530
  if(this.options.display === false) {
1479
1531
  return;
1480
1532
  }
1481
1533
 
1482
- var emptyClass = 'editable-empty';
1534
+ this.isEmpty = isEmpty !== undefined ? isEmpty : $.trim(this.$element.text()) === '';
1535
+
1483
1536
  //emptytext shown only for enabled
1484
1537
  if(!this.options.disabled) {
1485
- if ($.trim(this.$element.text()) === '') {
1486
- this.$element.addClass(emptyClass).text(this.options.emptytext);
1487
- } else {
1488
- this.$element.removeClass(emptyClass);
1538
+ if (this.isEmpty) {
1539
+ this.$element.text(this.options.emptytext);
1540
+ if(this.options.emptyclass) {
1541
+ this.$element.addClass(this.options.emptyclass);
1542
+ }
1543
+ } else if(this.options.emptyclass) {
1544
+ this.$element.removeClass(this.options.emptyclass);
1489
1545
  }
1490
1546
  } else {
1491
1547
  //below required if element disable property was changed
1492
- if(this.$element.hasClass(emptyClass)) {
1548
+ if(this.isEmpty) {
1493
1549
  this.$element.empty();
1494
- this.$element.removeClass(emptyClass);
1550
+ if(this.options.emptyclass) {
1551
+ this.$element.removeClass(this.options.emptyclass);
1552
+ }
1495
1553
  }
1496
1554
  }
1497
1555
  },
@@ -1513,6 +1571,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1513
1571
  input: this.input //pass input to form (as it is already created)
1514
1572
  });
1515
1573
  this.$element.editableContainer(containerOptions);
1574
+ //listen `save` event
1516
1575
  this.$element.on("save.internal", $.proxy(this.save, this));
1517
1576
  this.container = this.$element.data('editableContainer');
1518
1577
  } else if(this.container.tip().is(':visible')) {
@@ -1550,13 +1609,29 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1550
1609
  * called when form was submitted
1551
1610
  */
1552
1611
  save: function(e, params) {
1553
- //if url is not user's function and value was not sent to server and value changed --> mark element with unsaved css.
1554
- if(typeof this.options.url !== 'function' && this.options.display !== false && params.response === undefined && this.input.value2str(this.value) !== this.input.value2str(params.newValue)) {
1555
- this.$element.addClass('editable-unsaved');
1556
- } else {
1557
- this.$element.removeClass('editable-unsaved');
1612
+ //mark element with unsaved class if needed
1613
+ if(this.options.unsavedclass) {
1614
+ /*
1615
+ Add unsaved css to element if:
1616
+ - url is not user's function
1617
+ - value was not sent to server
1618
+ - params.response === undefined, that means data was not sent
1619
+ - value changed
1620
+ */
1621
+ var sent = false;
1622
+ sent = sent || typeof this.options.url === 'function';
1623
+ sent = sent || this.options.display === false;
1624
+ sent = sent || params.response !== undefined;
1625
+ sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue));
1626
+
1627
+ if(sent) {
1628
+ this.$element.removeClass(this.options.unsavedclass);
1629
+ } else {
1630
+ this.$element.addClass(this.options.unsavedclass);
1631
+ }
1558
1632
  }
1559
1633
 
1634
+ //set new value
1560
1635
  this.setValue(params.newValue, false, params.response);
1561
1636
 
1562
1637
  /**
@@ -1648,7 +1723,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1648
1723
  url: '/post',
1649
1724
  pk: 1
1650
1725
  });
1651
- **/
1726
+ **/
1652
1727
  $.fn.editable = function (option) {
1653
1728
  //special API methods returning non-jquery object
1654
1729
  var result = {}, args = arguments, datakey = 'editable';
@@ -1665,7 +1740,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1665
1740
  username: "username is required",
1666
1741
  fullname: "fullname should be minimum 3 letters length"
1667
1742
  }
1668
- **/
1743
+ **/
1669
1744
  case 'validate':
1670
1745
  this.each(function () {
1671
1746
  var $this = $(this), data = $this.data(datakey), error;
@@ -1686,7 +1761,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1686
1761
  username: "superuser",
1687
1762
  fullname: "John"
1688
1763
  }
1689
- **/
1764
+ **/
1690
1765
  case 'getValue':
1691
1766
  this.each(function () {
1692
1767
  var $this = $(this), data = $this.data(datakey);
@@ -1705,11 +1780,11 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1705
1780
  @param {object} options
1706
1781
  @param {object} options.url url to submit data
1707
1782
  @param {object} options.data additional data to submit
1708
- @param {object} options.ajaxOptions additional ajax options
1783
+ @param {object} options.ajaxOptions additional ajax options
1709
1784
  @param {function} options.error(obj) error handler
1710
1785
  @param {function} options.success(obj,config) success handler
1711
1786
  @returns {Object} jQuery object
1712
- **/
1787
+ **/
1713
1788
  case 'submit': //collects value, validate and submit to server for creating new record
1714
1789
  var config = arguments[1] || {},
1715
1790
  $elems = this,
@@ -1860,7 +1935,51 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1860
1935
  }
1861
1936
  }
1862
1937
  **/
1863
- display: null
1938
+ display: null,
1939
+ /**
1940
+ Css class applied when editable text is empty.
1941
+
1942
+ @property emptyclass
1943
+ @type string
1944
+ @since 1.4.1
1945
+ @default editable-empty
1946
+ **/
1947
+ emptyclass: 'editable-empty',
1948
+ /**
1949
+ Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`).
1950
+ You may set it to `null` if you work with editables locally and submit them together.
1951
+
1952
+ @property unsavedclass
1953
+ @type string
1954
+ @since 1.4.1
1955
+ @default editable-unsaved
1956
+ **/
1957
+ unsavedclass: 'editable-unsaved',
1958
+ /**
1959
+ If a css selector is provided, editable will be delegated to the specified targets.
1960
+ Usefull for dynamically generated DOM elements.
1961
+ **Please note**, that delegated targets can't use `emptytext` and `autotext` options,
1962
+ as they are initialized after first click.
1963
+
1964
+ @property selector
1965
+ @type string
1966
+ @since 1.4.1
1967
+ @default null
1968
+ @example
1969
+ <div id="user">
1970
+ <a href="#" data-name="username" data-type="text" title="Username">awesome</a>
1971
+ <a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" title="Group">Operator</a>
1972
+ </div>
1973
+
1974
+ <script>
1975
+ $('#user').editable({
1976
+ selector: 'a',
1977
+ url: '/post',
1978
+ pk: 1
1979
+ });
1980
+ </script>
1981
+ **/
1982
+ selector: null
1864
1983
  };
1865
1984
 
1866
1985
  }(window.jQuery));
@@ -2053,15 +2172,7 @@ To create your own input you can inherit from this class.
2053
2172
  @type string
2054
2173
  @default input-medium
2055
2174
  **/
2056
- inputclass: 'input-medium',
2057
- /**
2058
- Name attribute of input
2059
-
2060
- @property name
2061
- @type string
2062
- @default null
2063
- **/
2064
- name: null
2175
+ inputclass: 'input-medium'
2065
2176
  };
2066
2177
 
2067
2178
  $.extend($.fn.editabletypes, {abstractinput: AbstractInput});
@@ -2214,7 +2325,7 @@ List - abstract class for inputs that have source option loaded from js array or
2214
2325
  }, this)
2215
2326
  });
2216
2327
  } else { //options as json/array/function
2217
- if (typeof this.options.source === 'function') {
2328
+ if ($.isFunction(this.options.source)) {
2218
2329
  this.sourceData = this.makeArray(this.options.source());
2219
2330
  } else {
2220
2331
  this.sourceData = this.makeArray(this.options.source);
@@ -2270,35 +2381,45 @@ List - abstract class for inputs that have source option loaded from js array or
2270
2381
  * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
2271
2382
  */
2272
2383
  makeArray: function(data) {
2273
- var count, obj, result = [], iterateEl;
2384
+ var count, obj, result = [], item, iterateItem;
2274
2385
  if(!data || typeof data === 'string') {
2275
2386
  return null;
2276
2387
  }
2277
2388
 
2278
2389
  if($.isArray(data)) { //array
2279
- iterateEl = function (k, v) {
2390
+ /*
2391
+ function to iterate inside item of array if item is object.
2392
+ Caclulates count of keys in item and store in obj.
2393
+ */
2394
+ iterateItem = function (k, v) {
2280
2395
  obj = {value: k, text: v};
2281
2396
  if(count++ >= 2) {
2282
- return false;// exit each if object has more than one value
2397
+ return false;// exit from `each` if item has more than one key.
2283
2398
  }
2284
2399
  };
2285
2400
 
2286
2401
  for(var i = 0; i < data.length; i++) {
2287
- if(typeof data[i] === 'object') {
2288
- count = 0;
2289
- $.each(data[i], iterateEl);
2290
- if(count === 1) {
2402
+ item = data[i];
2403
+ if(typeof item === 'object') {
2404
+ count = 0; //count of keys inside item
2405
+ $.each(item, iterateItem);
2406
+ //case: [{val1: 'text1'}, {val2: 'text2} ...]
2407
+ if(count === 1) {
2291
2408
  result.push(obj);
2292
- } else if(count > 1 && data[i].hasOwnProperty('value') && data[i].hasOwnProperty('text')) {
2293
- result.push(data[i]);
2294
- } else {
2295
- //data contains incorrect objects
2409
+ //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...]
2410
+ } else if(count > 1) {
2411
+ //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text')
2412
+ if(item.children) {
2413
+ item.children = this.makeArray(item.children);
2414
+ }
2415
+ result.push(item);
2296
2416
  }
2297
2417
  } else {
2298
- result.push({value: data[i], text: data[i]});
2418
+ //case: ['text1', 'text2' ...]
2419
+ result.push({value: item, text: item});
2299
2420
  }
2300
2421
  }
2301
- } else { //object
2422
+ } else { //case: {val1: 'text1', val2: 'text2, ...}
2302
2423
  $.each(data, function (k, v) {
2303
2424
  result.push({value: k, text: v});
2304
2425
  });
@@ -2321,12 +2442,16 @@ List - abstract class for inputs that have source option loaded from js array or
2321
2442
  List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2322
2443
  /**
2323
2444
  Source data for list.
2324
- If **array** - it should be in format: `[{value: 1, text: "text1"}, {...}]`
2445
+ If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`
2325
2446
  For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order.
2326
2447
 
2327
2448
  If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option.
2328
2449
 
2329
2450
  If **function**, it should return data in format above (since 1.4.0).
2451
+
2452
+ Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only).
2453
+ `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]`
2454
+
2330
2455
 
2331
2456
  @property source
2332
2457
  @type string | array | object | function
@@ -2350,8 +2475,8 @@ List - abstract class for inputs that have source option loaded from js array or
2350
2475
  **/
2351
2476
  sourceError: 'Error when loading list',
2352
2477
  /**
2353
- if <code>true</code> and source is **string url** - results will be cached for fields with the same source and name.
2354
- Usefull for editable grids.
2478
+ if <code>true</code> and source is **string url** - results will be cached for fields with the same source.
2479
+ Usefull for editable column in grid to prevent extra requests.
2355
2480
 
2356
2481
  @property sourceCache
2357
2482
  @type boolean
@@ -2415,10 +2540,7 @@ $(function(){
2415
2540
  .keyup($.proxy(this.toggleClear, this))
2416
2541
  .parent().css('position', 'relative');
2417
2542
 
2418
- this.$clear.click($.proxy(function(){
2419
- this.$clear.hide();
2420
- this.$input.val('').focus();
2421
- }, this));
2543
+ this.$clear.click($.proxy(this.clear, this));
2422
2544
  }
2423
2545
  },
2424
2546
 
@@ -2443,12 +2565,17 @@ $(function(){
2443
2565
  return;
2444
2566
  }
2445
2567
 
2446
- if(this.$input.val()) {
2568
+ if(this.$input.val().length) {
2447
2569
  this.$clear.show();
2448
2570
  } else {
2449
2571
  this.$clear.hide();
2450
2572
  }
2451
- }
2573
+ },
2574
+
2575
+ clear: function() {
2576
+ this.$clear.hide();
2577
+ this.$input.val('').focus();
2578
+ }
2452
2579
  });
2453
2580
 
2454
2581
  Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
@@ -2622,14 +2749,21 @@ $(function(){
2622
2749
  $.extend(Select.prototype, {
2623
2750
  renderList: function() {
2624
2751
  this.$input.empty();
2625
-
2626
- if(!$.isArray(this.sourceData)) {
2627
- return;
2628
- }
2629
2752
 
2630
- for(var i=0; i<this.sourceData.length; i++) {
2631
- this.$input.append($('<option>', {value: this.sourceData[i].value}).text(this.sourceData[i].text));
2632
- }
2753
+ var fillItems = function($el, data) {
2754
+ if($.isArray(data)) {
2755
+ for(var i=0; i<data.length; i++) {
2756
+ if(data[i].children) {
2757
+ $el.append(fillItems($('<optgroup>', {label: data[i].text}), data[i].children));
2758
+ } else {
2759
+ $el.append($('<option>', {value: data[i].value}).text(data[i].text));
2760
+ }
2761
+ }
2762
+ }
2763
+ return $el;
2764
+ };
2765
+
2766
+ fillItems(this.$input, this.sourceData);
2633
2767
 
2634
2768
  this.setClass();
2635
2769
 
@@ -2714,8 +2848,7 @@ $(function(){
2714
2848
  for(var i=0; i<this.sourceData.length; i++) {
2715
2849
  $label = $('<label>').append($('<input>', {
2716
2850
  type: 'checkbox',
2717
- value: this.sourceData[i].value,
2718
- name: this.options.name
2851
+ value: this.sourceData[i].value
2719
2852
  }))
2720
2853
  .append($('<span>').text(' '+this.sourceData[i].text));
2721
2854
 
@@ -2744,7 +2877,7 @@ $(function(){
2744
2877
 
2745
2878
  //set checked on required checkboxes
2746
2879
  value2input: function(value) {
2747
- this.$input.removeAttr('checked');
2880
+ this.$input.prop('checked', false);
2748
2881
  if($.isArray(value) && value.length) {
2749
2882
  this.$input.each(function(i, el) {
2750
2883
  var $el = $(el);
@@ -2753,7 +2886,7 @@ $(function(){
2753
2886
  /*jslint eqeq: true*/
2754
2887
  if($el.val() == val) {
2755
2888
  /*jslint eqeq: false*/
2756
- $el.attr('checked', 'checked');
2889
+ $el.prop('checked', true);
2757
2890
  }
2758
2891
  });
2759
2892
  });
@@ -2992,6 +3125,199 @@ Range (inherit from number)
2992
3125
  $.fn.editabletypes.range = Range;
2993
3126
  }(window.jQuery));
2994
3127
  /**
3128
+ Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2.
3129
+ Please see [original docs](http://ivaynberg.github.com/select2) for detailed description and options.
3130
+ You should manually include select2 distributive:
3131
+
3132
+ <link href="select2/select2.css" rel="stylesheet" type="text/css"></link>
3133
+ <script src="select2/select2.js"></script>
3134
+
3135
+ @class select2
3136
+ @extends abstractinput
3137
+ @since 1.4.1
3138
+ @final
3139
+ @example
3140
+ <a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-original-title="Select country"></a>
3141
+ <script>
3142
+ $(function(){
3143
+ $('#country').editable({
3144
+ source: [
3145
+ {id: 'gb', text: 'Great Britain'},
3146
+ {id: 'us', text: 'United States'},
3147
+ {id: 'ru', text: 'Russia'}
3148
+ ],
3149
+ select2: {
3150
+ multiple: true
3151
+ }
3152
+ });
3153
+ });
3154
+ </script>
3155
+ **/
3156
+ (function ($) {
3157
+
3158
+ var Constructor = function (options) {
3159
+ this.init('select2', options, Constructor.defaults);
3160
+
3161
+ options.select2 = options.select2 || {};
3162
+
3163
+ var that = this,
3164
+ mixin = {
3165
+ placeholder: options.placeholder
3166
+ };
3167
+
3168
+ //detect whether it is multi-valued
3169
+ this.isMultiple = options.select2.tags || options.select2.multiple;
3170
+
3171
+ //if not `tags` mode, we need define init set data from source
3172
+ if(!options.select2.tags) {
3173
+ if(options.source) {
3174
+ mixin.data = options.source;
3175
+ }
3176
+
3177
+ //this function can be defaulted in seletc2. See https://github.com/ivaynberg/select2/issues/710
3178
+ mixin.initSelection = function (element, callback) {
3179
+ var val = that.str2value(element.val()),
3180
+ data = $.fn.editableutils.itemsByValue(val, mixin.data, 'id');
3181
+
3182
+ //for single-valued mode should not use array. Take first element instead.
3183
+ if($.isArray(data) && data.length && !that.isMultiple) {
3184
+ data = data[0];
3185
+ }
3186
+
3187
+ callback(data);
3188
+ };
3189
+ }
3190
+
3191
+ //overriding objects in config (as by default jQuery extend() is not recursive)
3192
+ this.options.select2 = $.extend({}, Constructor.defaults.select2, mixin, options.select2);
3193
+ };
3194
+
3195
+ $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
3196
+
3197
+ $.extend(Constructor.prototype, {
3198
+ render: function() {
3199
+ this.setClass();
3200
+ //apply select2
3201
+ this.$input.select2(this.options.select2);
3202
+
3203
+ //trigger resize of editableform to re-position container in multi-valued mode
3204
+ if(this.isMultiple) {
3205
+ this.$input.on('change', function() {
3206
+ $(this).closest('form').parent().triggerHandler('resize');
3207
+ });
3208
+ }
3209
+
3210
+ },
3211
+
3212
+ value2html: function(value, element) {
3213
+ var text = '', data;
3214
+ if(this.$input) { //when submitting form
3215
+ data = this.$input.select2('data');
3216
+ } else { //on init (autotext)
3217
+ //here select2 instance not created yet and data may be even not loaded.
3218
+ //we can check data/tags property of select config and if exist lookup text
3219
+ if(this.options.select2.tags) {
3220
+ data = value;
3221
+ } else if(this.options.select2.data) {
3222
+ data = $.fn.editableutils.itemsByValue(value, this.options.select2.data, 'id');
3223
+ }
3224
+ }
3225
+
3226
+ if($.isArray(data)) {
3227
+ //collect selected data and show with separator
3228
+ text = [];
3229
+ $.each(data, function(k, v){
3230
+ text.push(v && typeof v === 'object' ? v.text : v);
3231
+ });
3232
+ } else if(data) {
3233
+ text = data.text;
3234
+ }
3235
+
3236
+ text = $.isArray(text) ? text.join(this.options.viewseparator) : text;
3237
+
3238
+ $(element).text(text);
3239
+ },
3240
+
3241
+ html2value: function(html) {
3242
+ return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null;
3243
+ },
3244
+
3245
+ value2input: function(value) {
3246
+ this.$input.val(value).trigger('change');
3247
+ },
3248
+
3249
+ input2value: function() {
3250
+ return this.$input.select2('val');
3251
+ },
3252
+
3253
+ str2value: function(str, separator) {
3254
+ if(typeof str !== 'string' || !this.isMultiple) {
3255
+ return str;
3256
+ }
3257
+
3258
+ separator = separator || this.options.select2.separator || $.fn.select2.defaults.separator;
3259
+
3260
+ var val, i, l;
3261
+
3262
+ if (str === null || str.length < 1) {
3263
+ return null;
3264
+ }
3265
+ val = str.split(separator);
3266
+ for (i = 0, l = val.length; i < l; i = i + 1) {
3267
+ val[i] = $.trim(val[i]);
3268
+ }
3269
+
3270
+ return val;
3271
+ }
3272
+
3273
+ });
3274
+
3275
+ Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
3276
+ /**
3277
+ @property tpl
3278
+ @default <input type="hidden">
3279
+ **/
3280
+ tpl:'<input type="hidden">',
3281
+ /**
3282
+ Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2).
3283
+
3284
+ @property select2
3285
+ @type object
3286
+ @default null
3287
+ **/
3288
+ select2: null,
3289
+ /**
3290
+ Placeholder attribute of select
3291
+
3292
+ @property placeholder
3293
+ @type string
3294
+ @default null
3295
+ **/
3296
+ placeholder: null,
3297
+ /**
3298
+ Source data for select. It will be assigned to select2 `data` property and kept here just for convenience.
3299
+ Please note, that format is different from simple `select` input: use 'id' instead of 'value'.
3300
+ E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`.
3301
+
3302
+ @property source
3303
+ @type array
3304
+ @default null
3305
+ **/
3306
+ source: null,
3307
+ /**
3308
+ Separator used to display tags.
3309
+
3310
+ @property viewseparator
3311
+ @type string
3312
+ @default ', '
3313
+ **/
3314
+ viewseparator: ', '
3315
+ });
3316
+
3317
+ $.fn.editabletypes.select2 = Constructor;
3318
+
3319
+ }(window.jQuery));
3320
+ /**
2995
3321
  * Combodate - 1.0.1
2996
3322
  * Dropdown date and time picker.
2997
3323
  * Converts text input into dropdowns to pick day, month, year, hour, minute and second.
@@ -4934,3 +5260,225 @@ Automatically shown in inline mode.
4934
5260
  $.fn.datepicker.DPGlobal = DPGlobal;
4935
5261
 
4936
5262
  }( window.jQuery );
5263
+
5264
+ /**
5265
+ Typeahead input (bootstrap only). Based on Twitter Bootstrap [typeahead](http://twitter.github.com/bootstrap/javascript.html#typeahead).
5266
+ Depending on `source` format typeahead operates in two modes:
5267
+
5268
+ * **strings**:
5269
+ When `source` defined as array of strings, e.g. `['text1', 'text2', 'text3' ...]`.
5270
+ User can submit one of these strings or any text entered in input (even if it is not matching source).
5271
+
5272
+ * **objects**:
5273
+ When `source` defined as array of objects, e.g. `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`.
5274
+ User can submit only values that are in source (otherwise `null` is submitted). This is more like *dropdown* behavior.
5275
+
5276
+ @class typeahead
5277
+ @extends list
5278
+ @since 1.4.1
5279
+ @final
5280
+ @example
5281
+ <a href="#" id="country" data-type="typeahead" data-pk="1" data-url="/post" data-original-title="Input country"></a>
5282
+ <script>
5283
+ $(function(){
5284
+ $('#country').editable({
5285
+ value: 'ru',
5286
+ source: [
5287
+ {value: 'gb', text: 'Great Britain'},
5288
+ {value: 'us', text: 'United States'},
5289
+ {value: 'ru', text: 'Russia'}
5290
+ ]
5291
+ }
5292
+ });
5293
+ });
5294
+ </script>
5295
+ **/
5296
+ (function ($) {
5297
+
5298
+ var Constructor = function (options) {
5299
+ this.init('typeahead', options, Constructor.defaults);
5300
+
5301
+ //overriding objects in config (as by default jQuery extend() is not recursive)
5302
+ this.options.typeahead = $.extend({}, Constructor.defaults.typeahead, {
5303
+ //set default methods for typeahead to work with objects
5304
+ matcher: this.matcher,
5305
+ sorter: this.sorter,
5306
+ highlighter: this.highlighter,
5307
+ updater: this.updater
5308
+ }, options.typeahead);
5309
+ };
5310
+
5311
+ $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.list);
5312
+
5313
+ $.extend(Constructor.prototype, {
5314
+ renderList: function() {
5315
+ this.$input = this.$tpl.is('input') ? this.$tpl : this.$tpl.find('input[type="text"]');
5316
+
5317
+ //set source of typeahead
5318
+ this.options.typeahead.source = this.sourceData;
5319
+
5320
+ //apply typeahead
5321
+ this.$input.typeahead(this.options.typeahead);
5322
+
5323
+ //attach own render method
5324
+ this.$input.data('typeahead').render = $.proxy(this.typeaheadRender, this.$input.data('typeahead'));
5325
+
5326
+ this.renderClear();
5327
+ this.setClass();
5328
+ this.setAttr('placeholder');
5329
+ },
5330
+
5331
+ value2htmlFinal: function(value, element) {
5332
+ if(this.getIsObjects()) {
5333
+ var items = $.fn.editableutils.itemsByValue(value, this.sourceData);
5334
+ $(element).text(items.length ? items[0].text : '');
5335
+ } else {
5336
+ $(element).text(value);
5337
+ }
5338
+ },
5339
+
5340
+ html2value: function (html) {
5341
+ return html ? html : null;
5342
+ },
5343
+
5344
+ value2input: function(value) {
5345
+ if(this.getIsObjects()) {
5346
+ var items = $.fn.editableutils.itemsByValue(value, this.sourceData);
5347
+ this.$input.data('value', value).val(items.length ? items[0].text : '');
5348
+ } else {
5349
+ this.$input.val(value);
5350
+ }
5351
+ },
5352
+
5353
+ input2value: function() {
5354
+ if(this.getIsObjects()) {
5355
+ var value = this.$input.data('value'),
5356
+ items = $.fn.editableutils.itemsByValue(value, this.sourceData);
5357
+
5358
+ if(items.length && items[0].text.toLowerCase() === this.$input.val().toLowerCase()) {
5359
+ return value;
5360
+ } else {
5361
+ return null; //entered string not found in source
5362
+ }
5363
+ } else {
5364
+ return this.$input.val();
5365
+ }
5366
+ },
5367
+
5368
+ /*
5369
+ if in sourceData values <> texts, typeahead in "objects" mode:
5370
+ user must pick some value from list, otherwise `null` returned.
5371
+ if all values == texts put typeahead in "strings" mode:
5372
+ anything what entered is submited.
5373
+ */
5374
+ getIsObjects: function() {
5375
+ if(this.isObjects === undefined) {
5376
+ this.isObjects = false;
5377
+ for(var i=0; i<this.sourceData.length; i++) {
5378
+ if(this.sourceData[i].value !== this.sourceData[i].text) {
5379
+ this.isObjects = true;
5380
+ break;
5381
+ }
5382
+ }
5383
+ }
5384
+ return this.isObjects;
5385
+ },
5386
+
5387
+ /*
5388
+ Methods borrowed from text input
5389
+ */
5390
+ activate: $.fn.editabletypes.text.prototype.activate,
5391
+ renderClear: $.fn.editabletypes.text.prototype.renderClear,
5392
+ postrender: $.fn.editabletypes.text.prototype.postrender,
5393
+ toggleClear: $.fn.editabletypes.text.prototype.toggleClear,
5394
+ clear: function() {
5395
+ $.fn.editabletypes.text.prototype.clear.call(this);
5396
+ this.$input.data('value', '');
5397
+ },
5398
+
5399
+
5400
+ /*
5401
+ Typeahead option methods used as defaults
5402
+ */
5403
+ /*jshint eqeqeq:false, curly: false, laxcomma: true*/
5404
+ matcher: function (item) {
5405
+ return $.fn.typeahead.Constructor.prototype.matcher.call(this, item.text);
5406
+ },
5407
+ sorter: function (items) {
5408
+ var beginswith = []
5409
+ , caseSensitive = []
5410
+ , caseInsensitive = []
5411
+ , item
5412
+ , text;
5413
+
5414
+ while (item = items.shift()) {
5415
+ text = item.text;
5416
+ if (!text.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item);
5417
+ else if (~text.indexOf(this.query)) caseSensitive.push(item);
5418
+ else caseInsensitive.push(item);
5419
+ }
5420
+
5421
+ return beginswith.concat(caseSensitive, caseInsensitive);
5422
+ },
5423
+ highlighter: function (item) {
5424
+ return $.fn.typeahead.Constructor.prototype.highlighter.call(this, item.text);
5425
+ },
5426
+ updater: function (item) {
5427
+ item = this.$menu.find('.active').data('item');
5428
+ this.$element.data('value', item.value);
5429
+ return item.text;
5430
+ },
5431
+
5432
+
5433
+ /*
5434
+ Overwrite typeahead's render method to store objects.
5435
+ There are a lot of disscussion in bootstrap repo on this point and still no result.
5436
+ See https://github.com/twitter/bootstrap/issues/5967
5437
+
5438
+ This function just store item in via jQuery data() method instead of attr('data-value')
5439
+ */
5440
+ typeaheadRender: function (items) {
5441
+ var that = this;
5442
+
5443
+ items = $(items).map(function (i, item) {
5444
+ // i = $(that.options.item).attr('data-value', item)
5445
+ i = $(that.options.item).data('item', item);
5446
+ i.find('a').html(that.highlighter(item));
5447
+ return i[0];
5448
+ });
5449
+
5450
+ items.first().addClass('active');
5451
+ this.$menu.html(items);
5452
+ return this;
5453
+ }
5454
+ /*jshint eqeqeq: true, curly: true, laxcomma: false*/
5455
+
5456
+ });
5457
+
5458
+ Constructor.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
5459
+ /**
5460
+ @property tpl
5461
+ @default <input type="text">
5462
+ **/
5463
+ tpl:'<input type="text">',
5464
+ /**
5465
+ Configuration of typeahead. [Full list of options](http://twitter.github.com/bootstrap/javascript.html#typeahead).
5466
+
5467
+ @property typeahead
5468
+ @type object
5469
+ @default null
5470
+ **/
5471
+ typeahead: null,
5472
+ /**
5473
+ Whether to show `clear` button
5474
+
5475
+ @property clear
5476
+ @type boolean
5477
+ @default true
5478
+ **/
5479
+ clear: true
5480
+ });
5481
+
5482
+ $.fn.editabletypes.typeahead = Constructor;
5483
+
5484
+ }(window.jQuery));
@@ -1,4 +1,4 @@
1
- /*! X-editable - v1.4.0
1
+ /*! X-editable - v1.4.1
2
2
  * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
3
3
  * http://github.com/vitalets/x-editable
4
4
  * Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
@@ -116,21 +116,6 @@
116
116
  .editable-clear-x:hover {
117
117
  opacity: 1;
118
118
  }
119
-
120
- /*
121
- .editable-clear-x1 {
122
- background: image-url('clear.png') center center no-repeat;
123
- display: inline-block;
124
- zoom: 1;
125
- *display: inline;
126
- width: 13px;
127
- height: 13px;
128
- vertical-align: middle;
129
- position: relative;
130
- margin-left: -20px;
131
- }
132
- */
133
-
134
119
  .editable-container {
135
120
  max-width: none !important; /* without this rule poshytip/tooltip does not stretch */
136
121
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bootstrap-x-editable-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-12 00:00:00.000000000 Z
12
+ date: 2013-01-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: railties
@@ -69,6 +69,7 @@ files:
69
69
  - .gitignore
70
70
  - Gemfile
71
71
  - README.md
72
+ - Rakefile
72
73
  - X-EDITABLE-LICENSE-MIT
73
74
  - bootstrap-x-editable-rails.gemspec
74
75
  - bootstrap-x-editable-rails.sublime-project
@@ -76,6 +77,7 @@ files:
76
77
  - lib/bootstrap-x-editable-rails/engine.rb
77
78
  - lib/bootstrap-x-editable-rails/railtie.rb
78
79
  - lib/bootstrap-x-editable-rails/version.rb
80
+ - vendor/assets/images/clear.png
79
81
  - vendor/assets/images/loading.gif
80
82
  - vendor/assets/javascripts/bootstrap-editable-inline.js
81
83
  - vendor/assets/javascripts/bootstrap-editable.js
@@ -94,7 +96,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
96
  version: '0'
95
97
  segments:
96
98
  - 0
97
- hash: 3701022052104564382
99
+ hash: 1222445192061243716
98
100
  required_rubygems_version: !ruby/object:Gem::Requirement
99
101
  none: false
100
102
  requirements:
@@ -103,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
105
  version: '0'
104
106
  segments:
105
107
  - 0
106
- hash: 3701022052104564382
108
+ hash: 1222445192061243716
107
109
  requirements: []
108
110
  rubyforge_project:
109
111
  rubygems_version: 1.8.24