sproutcore 0.9.17 → 0.9.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. data/History.txt +45 -2
  2. data/Manifest.txt +10 -0
  3. data/Rakefile +1 -1
  4. data/app_generators/sproutcore/templates/sc-config +8 -2
  5. data/bin/sc-server +4 -0
  6. data/clients/sc_docs/english.lproj/body.css +0 -20
  7. data/clients/sc_docs/english.lproj/body.rhtml +1 -3
  8. data/clients/sc_docs/english.lproj/strings.js +1 -1
  9. data/clients/sc_docs/french.lproj/strings.js +14 -0
  10. data/clients/sc_test_runner/english.lproj/body.css +0 -20
  11. data/clients/sc_test_runner/english.lproj/body.rhtml +1 -3
  12. data/config/hoe.rb +1 -1
  13. data/frameworks/sproutcore/HISTORY +56 -1
  14. data/frameworks/sproutcore/debug/trace.js +81 -0
  15. data/frameworks/sproutcore/debug/unittest.js +2 -1
  16. data/frameworks/sproutcore/english.lproj/buttons.css +5 -2
  17. data/frameworks/sproutcore/english.lproj/core.css +0 -16
  18. data/frameworks/sproutcore/english.lproj/images/sc-theme-sprite.png +0 -0
  19. data/frameworks/sproutcore/english.lproj/splitview.css +83 -0
  20. data/frameworks/sproutcore/english.lproj/theme.css +21 -5
  21. data/frameworks/sproutcore/foundation/object.js +23 -0
  22. data/frameworks/sproutcore/foundation/string.js +4 -3
  23. data/frameworks/sproutcore/lib/core_views.rb +43 -8
  24. data/frameworks/sproutcore/lib/form_views.rb +2 -2
  25. data/frameworks/sproutcore/lib/index.rhtml +1 -1
  26. data/frameworks/sproutcore/mixins/enumerable.js +4 -8
  27. data/frameworks/sproutcore/mixins/selection_support.js +1 -1
  28. data/frameworks/sproutcore/models/collection.js +14 -3
  29. data/frameworks/sproutcore/models/record.js +6 -5
  30. data/frameworks/sproutcore/models/store.js +3 -3
  31. data/frameworks/sproutcore/panes/picker.js +1 -0
  32. data/frameworks/sproutcore/server/rails_server.js +4 -7
  33. data/frameworks/sproutcore/server/server.js +58 -1
  34. data/frameworks/sproutcore/tests/controllers/object.rhtml +1 -1
  35. data/frameworks/sproutcore/tests/models/collection.rhtml +160 -0
  36. data/frameworks/sproutcore/tests/models/model.rhtml +15 -3
  37. data/frameworks/sproutcore/tests/views/collection/base.rhtml +120 -47
  38. data/frameworks/sproutcore/tests/views/collection/source_list_rendering.rhtml +232 -0
  39. data/frameworks/sproutcore/tests/views/view/frame.rhtml +2 -2
  40. data/frameworks/sproutcore/tests/views/view/innerFrame.rhtml +87 -85
  41. data/frameworks/sproutcore/tests/views/view/scrollFrame.rhtml +25 -26
  42. data/frameworks/sproutcore/views/collection/collection.js +5 -1
  43. data/frameworks/sproutcore/views/field/select_field.js +1 -1
  44. data/frameworks/sproutcore/views/radio_group.js +2 -2
  45. data/frameworks/sproutcore/views/split.js +191 -174
  46. data/frameworks/sproutcore/views/split_divider.js +71 -68
  47. data/frameworks/sproutcore/views/view.js +65 -25
  48. data/lib/sproutcore.rb +4 -1
  49. data/lib/sproutcore/build_tools/html_builder.rb +50 -46
  50. data/lib/sproutcore/build_tools/resource_builder.rb +17 -5
  51. data/lib/sproutcore/bundle_installer.rb +3 -1
  52. data/lib/sproutcore/bundle_manifest.rb +4 -3
  53. data/lib/sproutcore/cssmin.rb +195 -0
  54. data/lib/sproutcore/generator_helper.rb +15 -0
  55. data/lib/sproutcore/helpers/capture_helper.rb +2 -22
  56. data/lib/sproutcore/helpers/dom_id_helper.rb +14 -0
  57. data/lib/sproutcore/helpers/static_helper.rb +6 -2
  58. data/lib/sproutcore/helpers/text_helper.rb +1 -1
  59. data/lib/sproutcore/merb.rb +2 -2
  60. data/lib/sproutcore/renderers/erubis.rb +43 -0
  61. data/lib/sproutcore/renderers/haml.rb +28 -0
  62. data/lib/sproutcore/renderers/sass.rb +42 -0
  63. data/lib/sproutcore/version.rb +1 -1
  64. data/lib/sproutcore/view_helpers.rb +40 -46
  65. data/sc_generators/controller/controller_generator.rb +1 -1
  66. data/sc_generators/language/USAGE +5 -7
  67. data/sc_generators/language/language_generator.rb +1 -1
  68. data/sc_generators/language/templates/strings.js +5 -1
  69. data/sc_generators/model/model_generator.rb +1 -1
  70. data/sc_generators/test/test_generator.rb +1 -1
  71. data/sc_generators/view/view_generator.rb +1 -1
  72. metadata +12 -5
@@ -6,104 +6,107 @@
6
6
  require('views/view') ;
7
7
  require('views/split');
8
8
 
9
- /**
9
+ /**
10
10
  @class
11
11
 
12
- A SplitDividerView displays a divider between two split views. Clicking
13
- and dragging the divider will change the thickness of the view either to
14
- the left or right of the divider, depending on which side of the flexible
15
- view the divider is on.
16
-
17
- Double-clicking will try to collapse the same view so it is not visible
18
- unless you have canCollapse disabled on the SplitView.
19
-
20
- This view must be a direct child of the split view it works with.
21
-
12
+ A SplitDividerView displays a divider between two views within a SplitView.
13
+ Clicking and dragging the divider will change the thickness of each view
14
+ either to the top/left or bottom/right of the divider.
15
+
16
+ Double-clicking on the SplitDividerView will try to collapse the first
17
+ view within the SplitView that has property canCollapse set to true,
18
+ so it is not visible, unless you have canCollapse disabled on the SplitView.
19
+
20
+ This view must be a direct child of the split view it works with. It must
21
+ be surrounded by two other views.
22
+
22
23
  @extends SC.View
23
-
24
+
24
25
  @author Charles Jolley
26
+ @author Lawrence Pit
25
27
  */
26
28
  SC.SplitDividerView = SC.View.extend(
27
29
  /** @scope SC.SplitDividerView.prototype */ {
28
-
30
+
29
31
  emptyElement: '<div class="sc-split-divider-view"></div>',
30
32
 
31
- /**
32
- Returns the view to be managed by the divider view.
33
- */
34
- targetView: function() {
35
- var splitView = this.get('parentNode') ;
36
- if (!splitView) return null ;
37
-
38
- var flexibleView = splitView.computeFlexibleView() ;
39
- var views = splitView.get('childNodes') ;
40
- var myIndex = views.indexOf(this) ;
41
- var flexibleIndex = views.indexOf(flexibleView) ;
42
-
43
- if (myIndex < 0) throw "SplitDividerView must belong to the SplitView";
44
-
45
- return (myIndex <= flexibleIndex) ? this.get('previousSibling') : this.get('nextSibling') ;
46
-
47
- }.property(),
48
-
49
33
  mouseDown: function(evt) {
50
-
51
- var splitView = this.get('parentNode') ;
52
- if (!splitView) return ;
53
-
54
34
  // cache some info for later use.
55
35
  this._mouseDownLocation = Event.pointerLocation(evt) ;
36
+ this._splitView = this.get('parentNode') ;
37
+ this._tlView = this.get('previousSibling') ;
38
+ this._brView = this.get('nextSibling') ;
39
+ this._originalTopLeftThickness = this._splitView.getThicknessForView(this._tlView) ;
40
+ this._direction = this._splitView.get('layoutDirection') ;
56
41
 
57
- this._targetView = this.get('targetView') ;
58
-
59
- // determine the view to change.
60
- this._originalThickness = splitView.thicknessForView(this._targetView);
61
-
62
- this._direction = splitView.get('layoutDirection') ;
63
-
64
42
  // return true so we can track mouse dragged.
65
43
  return true ;
66
44
  },
67
-
45
+
68
46
  mouseDragged: function(evt) {
69
-
70
- // calculate new thickness
47
+ // calculate new thickness requested by mouse
71
48
  var loc = Event.pointerLocation(evt) ;
72
-
49
+
73
50
  if (this._direction == SC.HORIZONTAL) {
74
51
  var offset = loc.x - this._mouseDownLocation.x ;
75
52
  } else {
76
53
  var offset = loc.y - this._mouseDownLocation.y ;
77
54
  }
78
55
 
79
- var thickness = this._originalThickness + offset ;
80
- var splitView = this.get('parentNode') ;
81
- splitView.setThicknessForView(this._targetView, thickness) ;
82
-
56
+ var proposedThickness = this._originalTopLeftThickness + offset ;
57
+ this._splitView.setThicknessForView(this._tlView, proposedThickness) ;
58
+ this._setCursorStyle() ;
83
59
  return true ;
84
60
  },
85
-
61
+
86
62
  // clear left overs.
87
63
  mouseUp: function(evt) {
88
- this._targetView = this._originalThickness = this._direction = this._mouseDownLocation = null ;
64
+ this._mouseDownLocation = this._originalTopLeftThickness = null ;
89
65
  },
90
-
66
+
91
67
  doubleClick: function(evt) {
92
- var splitView = this.get('parentNode') ;
93
- if (!splitView) return; // nothing to do.
94
-
95
- // try to collapse or un-collapse.
96
- var targetView = this.get('targetView');
97
- var isCollapsed = targetView.get('isCollapsed') || NO;
98
-
99
- // do not collapse if not allowed.
100
- if (!isCollapsed && !splitView.canCollapseView(targetView)) return;
101
-
102
- // now set the collapsed state and layout.
103
- targetView.set('isCollapsed', !isCollapsed) ;
104
- splitView.layout() ;
105
-
68
+ var view = this._tlView ;
69
+ var isCollapsed = view.get('isCollapsed') || NO ;
70
+ if (!isCollapsed && !this._splitView.canCollapseView(view)) {
71
+ view = this._brView ;
72
+ isCollapsed = view.get('isCollapsed') || NO ;
73
+ if (!isCollapsed && !this._splitView.canCollapseView(view)) return;
74
+ }
75
+
76
+ if (!isCollapsed) {
77
+ // remember thickness in it's uncollapsed state
78
+ view._uncollapsedThickness = this._splitView.getThicknessForView(view) ;
79
+ // and collapse
80
+ this._splitView.setThicknessForView(view, 0) ;
81
+ // if however the splitview decided not to collapse, clear:
82
+ if (!view.get("isCollapsed")) {
83
+ view._uncollapsedThickness = null;
84
+ }
85
+ } else {
86
+ // uncollapse to the last thickness in it's uncollapsed state
87
+ this._splitView.setThicknessForView(view, view._uncollapsedThickness) ;
88
+ view._uncollapsedThickness = null ;
89
+ }
90
+ this._setCursorStyle() ;
106
91
  return true ;
92
+ },
93
+
94
+ _setCursorStyle: function() {
95
+ tlThickness = this._splitView.getThicknessForView(this._tlView) ;
96
+ brThickness = this._splitView.getThicknessForView(this._brView) ;
97
+ if (this._tlView.get('isCollapsed') ||
98
+ tlThickness == this._tlView.get("minThickness") ||
99
+ brThickness == this._brView.get("maxThickness"))
100
+ {
101
+ this.setStyle({cursor: this._direction == SC.HORIZONTAL ? "e-resize" : "s-resize" }) ;
102
+ } else if (this._brView.get('isCollapsed') ||
103
+ tlThickness == this._tlView.get("maxThickness") ||
104
+ brThickness == this._brView.get("minThickness"))
105
+ {
106
+ this.setStyle({cursor: this._direction == SC.HORIZONTAL ? "w-resize" : "n-resize" }) ;
107
+ } else {
108
+ this.setStyle({cursor: this._direction == SC.HORIZONTAL ? "ew-resize" : "ns-resize" }) ;
109
+ }
107
110
  }
108
-
111
+
109
112
  });
@@ -949,7 +949,7 @@ SC.View = SC.Responder.extend(SC.PathModule, SC.DelegateSupport,
949
949
 
950
950
  var f ;
951
951
  if (this._innerFrame == null) {
952
-
952
+
953
953
  // get the base frame
954
954
  // The _collectInnerFrame function is set at the bottom of this file
955
955
  // based on the browser type.
@@ -970,19 +970,34 @@ SC.View = SC.Responder.extend(SC.PathModule, SC.DelegateSupport,
970
970
 
971
971
  // fix the x & y with the clientTop/clientLeft
972
972
  var clientLeft, clientTop ;
973
- if (el.clientLeft == null) {
974
- clientLeft = parseInt(this.getStyle('border-left-width'),0) || 0 ;
975
- } else clientLeft = el.clientLeft ;
973
+
974
+ if (SC.Platform.IE) {
975
+ if (!el.width) {
976
+ clientLeft = parseInt(this.getStyle('border-left-width'),0) || 0 ;
977
+ } else clientLeft = el.clientLeft ;
976
978
 
977
- if (el.clientTop == null) {
978
- clientTop = parseInt(this.getStyle('border-top-width'),0) || 0 ;
979
- } else clientTop = el.clientTop ;
979
+ if (!el.height) {
980
+ clientTop = parseInt(this.getStyle('border-top-width'),0) || 0 ;
981
+ } else clientTop = el.clientTop ;
982
+
983
+ f.x += clientLeft; f.y += clientTop;
984
+ }
985
+ else {
986
+ if (el.clientLeft == null) {
987
+ clientLeft = parseInt(this.getStyle('border-left-width'),0) || 0 ;
988
+ } else clientLeft = el.clientLeft ;
980
989
 
981
- f.x += clientLeft; f.y += clientTop;
990
+ if (el.clientTop == null) {
991
+ clientTop = parseInt(this.getStyle('border-top-width'),0) || 0 ;
992
+ } else clientTop = el.clientTop ;
993
+
994
+ f.x += clientLeft; f.y += clientTop;
995
+ }
982
996
 
983
997
  // cache this frame if using manual layout mode
984
998
  this._innerFrame = SC.cloneRect(f);
985
999
  } else f = SC.cloneRect(this._innerFrame) ;
1000
+ // console.log('returning x:%@ y:%@ w:%@ h:%@'.fmt(f.x, f.y, f.width, f.height));
986
1001
  return f ;
987
1002
  }.property('frame'),
988
1003
 
@@ -1049,7 +1064,8 @@ SC.View = SC.Responder.extend(SC.PathModule, SC.DelegateSupport,
1049
1064
  while(--idx >= 0) {
1050
1065
  padding += parseInt(this.getStyle(SC.View.WIDTH_PADDING_STYLES[idx]), 0) || 0;
1051
1066
  }
1052
- style.width = (Math.floor(f.width) - padding).toString() + 'px' ;
1067
+ var width = Math.floor(f.width) - padding ;
1068
+ if (!isNaN(width)) style.width = width.toString() + 'px' ;
1053
1069
  }
1054
1070
 
1055
1071
  // Resize Height
@@ -1060,7 +1076,8 @@ SC.View = SC.Responder.extend(SC.PathModule, SC.DelegateSupport,
1060
1076
  while(--idx >= 0) {
1061
1077
  padding += parseInt(this.getStyle(SC.View.HEIGHT_PADDING_STYLES[idx]), 0) || 0;
1062
1078
  }
1063
- style.height = (Math.floor(f.height) - padding).toString() + 'px' ;
1079
+ var height = Math.floor(f.height) - padding ;
1080
+ if(!isNaN(height)) style.height = height.toString() + 'px' ;
1064
1081
  }
1065
1082
 
1066
1083
  // now apply style change and clear the cached frame
@@ -1267,14 +1284,40 @@ SC.View = SC.Responder.extend(SC.PathModule, SC.DelegateSupport,
1267
1284
  var f;
1268
1285
  if (this._scrollFrame == null) {
1269
1286
  var el = this.rootElement ;
1270
- f = this._collectFrame(function() {
1271
- return {
1272
- x: 0 - el.scrollLeft,
1273
- y: 0 - el.scrollTop,
1274
- width: el.scrollWidth,
1275
- height: el.scrollHeight
1287
+ var func;
1288
+ if (SC.isIE()) {
1289
+ func = function() {
1290
+ var borderTopWidth = 0;
1291
+ var borderBottomWidth = 0;
1292
+ var borderLeftWidth = 0;
1293
+ var borderRightWidth = 0;
1294
+
1295
+ var overflow = el.currentStyle.overflow;
1296
+ if ( overflow != 'hidden' && overflow != 'auto' ) {
1297
+ borderTopWidth = parseInt(el.currentStyle.borderTopWidth, 0) || 0 ;
1298
+ borderBottomWidth = parseInt(el.currentStyle.borderBottomWidth, 0) || 0 ;
1299
+ borderLeftWidth = parseInt(el.currentStyle.borderLeftWidth, 0) || 0 ;
1300
+ borderRightWidth = parseInt(el.currentStyle.borderRightWidth, 0) || 0 ;
1301
+ }
1302
+ return {
1303
+ x: 0 - el.scrollLeft,
1304
+ y: 0 - el.scrollTop,
1305
+ width: el.scrollWidth + borderLeftWidth + borderRightWidth,
1306
+ height: Math.max(el.scrollHeight, el.clientHeight) + borderTopWidth + borderBottomWidth
1307
+ };
1276
1308
  };
1277
- }) ;
1309
+ }
1310
+ else {
1311
+ func = function() {
1312
+ return {
1313
+ x: 0 - el.scrollLeft,
1314
+ y: 0 - el.scrollTop,
1315
+ width: el.scrollWidth,
1316
+ height: el.scrollHeight
1317
+ };
1318
+ };
1319
+ }
1320
+ f = this._collectFrame(func);
1278
1321
 
1279
1322
  // cache this frame if using manual layout mode
1280
1323
  this._scrollFrame = SC.cloneRect(f);
@@ -2207,18 +2250,15 @@ if (SC.Platform.IE) {
2207
2250
  // case always use the scrollWidth/Height.
2208
2251
  SC.View._collectInnerFrame = function() {
2209
2252
  var el = this.rootElement ;
2210
- var hasLayout = (el.currentStyle) ? el.currentStyle.hasLayout : NO ;
2253
+ var hasLayout = (el.currentStyle) ? el.currentStyle.hasLayout : false ;
2211
2254
  var borderTopWidth = parseInt(el.currentStyle.borderTopWidth, 0) || 0 ;
2212
2255
  var borderBottomWidth = parseInt(el.currentStyle.borderBottomWidth, 0) || 0 ;
2213
- var scrollHeight = el.offsetHeight-borderTopWidth-borderBottomWidth;
2214
- if(el.clientWidth > el.scrollWidth)
2215
- {
2216
- scrollHeight-15;
2217
- }
2256
+ var scrollHeight = el.offsetHeight - borderTopWidth - borderBottomWidth ;
2257
+ if (el.clientWidth > el.scrollWidth) scrollHeight - 15 ;
2218
2258
 
2219
2259
  return {
2220
- x: el.offsetLeft,
2221
- y: el.offsetTop,
2260
+ x: el.offsetLeft,
2261
+ y: el.offsetTop,
2222
2262
  width: (hasLayout) ? Math.min(el.scrollWidth, el.clientWidth) : el.scrollWidth,
2223
2263
  height: (hasLayout) ? Math.min(scrollHeight, el.clientHeight) : scrollHeight
2224
2264
  };
data/lib/sproutcore.rb CHANGED
@@ -21,8 +21,11 @@ module SproutCore
21
21
  end
22
22
 
23
23
  # Force load the code files. Others may be loaded only as required
24
- %w(library bundle bundle_manifest bundle_installer jsdoc jsmin version).each do |fname|
24
+ %w(library bundle bundle_manifest bundle_installer jsdoc jsmin cssmin version).each do |fname|
25
25
  require "sproutcore/#{fname}"
26
26
  end
27
+ %w(erubis haml sass).each do |fname|
28
+ require "sproutcore/renderers/#{fname}"
29
+ end
27
30
 
28
31
  SC= SproutCore
@@ -1,4 +1,3 @@
1
- require 'erubis'
2
1
  require 'sproutcore/helpers'
3
2
  require 'sproutcore/view_helpers'
4
3
 
@@ -17,9 +16,10 @@ module SproutCore
17
16
  include SproutCore::Helpers::TextHelper
18
17
  include SproutCore::Helpers::CaptureHelper
19
18
  include SproutCore::Helpers::StaticHelper
19
+ include SproutCore::Helpers::DomIdHelper
20
20
  include SproutCore::ViewHelpers
21
21
 
22
- attr_reader :entry, :bundle, :entries, :filename, :language, :library
22
+ attr_reader :entry, :bundle, :entries, :filename, :language, :library, :renderer
23
23
 
24
24
  def initialize(entry, bundle, deep=true)
25
25
  @entry = nil
@@ -56,61 +56,65 @@ module SproutCore
56
56
  # Actually builds the HTML file from the entry (actually from any
57
57
  # composite entries)
58
58
  def build
59
-
60
59
  @layout_path = bundle.layout_path
61
60
 
62
- # Render each filename. By default, the output goes to the resources
63
- # string
64
- @content_for_resources = ''
65
- entries.each { |fn| _render_one(fn) }
61
+ # Render each filename. By default, the output goes to the :resources
62
+ # content section
63
+ entries.each do |entry|
64
+ content_for :resources do
65
+ _build_one(entry)
66
+ end
67
+ end
66
68
 
67
69
  # Finally, render the layout. This should produce the final output to
68
70
  # return
69
- input = File.read(@layout_path)
70
-
71
- # render using either erb or haml
72
- case File.extname(@layout_path)
73
- when /\.rhtml$/, /\.html.erb$/
74
- return eval(Erubis::Eruby.new.convert(input))
75
- when /\.haml$/, /\.html.haml$/
76
- require 'haml'
77
- return Haml::Engine.new(input).to_html(self)
78
- end
71
+ _render(@layout_path)
79
72
  end
80
73
 
81
- # render a single entry
82
- def _render_one(entry)
83
- @entry = entry
84
- @filename = @entry.filename
85
-
86
- # avoid double render of layout path
87
- return if @entry.source_path == @layout_path
88
-
89
- # render. Result goes into @content_for_resources
90
- input = File.read(@entry.source_path)
91
-
92
- # render using either erb or haml
93
- case File.extname(@entry.source_path)
94
- when /\.rhtml$/, /\.html.erb$/
95
- @content_for_resources += eval(Erubis::Eruby.new.convert(input))
96
- when /\.haml$/, /\.html.haml$/
97
- require 'haml'
98
- @content_for_resources += Haml::Engine.new(input).to_html(self)
99
- end
100
-
101
- @filename =nil
102
- @entry = nil
103
- end
104
-
105
-
106
74
  # Returns the current bundle name. Often useful for generating titles,
107
75
  # etc.
108
76
  def bundle_name; bundle.bundle_name; end
109
77
 
110
- #### For Rails Compatibility. render() does not do anything useful
111
- # since the new build system is nice about putting things into the right
112
- # place for output.
113
- def render; ''; end
78
+ private
79
+
80
+ # Builds a single entry
81
+ def _build_one(entry)
82
+ # avoid double render of layout path
83
+ return if entry.source_path == @layout_path
84
+
85
+ @entry = entry
86
+ @filename = @entry.filename
87
+ begin
88
+ _render(@entry.source_path)
89
+ ensure
90
+ @filename = nil
91
+ @entry = nil
92
+ end
93
+ end
94
+
95
+ def _render(file_path)
96
+ SC.logger.debug("~ Rendering #{file_path}")
97
+ input = File.read(file_path)
98
+ @renderer = case file_path
99
+ when /\.rhtml$/, /\.html.erb$/
100
+ Sproutcore::Renderers::Erubis.new(self)
101
+ when /\.haml$/
102
+ Sproutcore::Renderers::Haml.new(self)
103
+ end
104
+ _render_compiled_template( @renderer.compile(input) )
105
+ end
106
+
107
+ # Renders a compiled template within this context
108
+ def _render_compiled_template(compiled_template)
109
+ self.instance_eval "def __render(); #{compiled_template}; end"
110
+ begin
111
+ self.send(:__render) do |*names|
112
+ self.instance_variable_get("@content_for_#{names.first}")
113
+ end
114
+ ensure
115
+ class << self; self end.class_eval{ remove_method(:__render) } rescue nil
116
+ end
117
+ end
114
118
 
115
119
  end
116
120