##// END OF EJS Templates
Removed require.js scheme since it forces async event driven model,...
Jonathan Frederic -
Show More
@@ -15,243 +15,239 b''
15 * @submodule widget
15 * @submodule widget
16 */
16 */
17
17
18
19 "use strict";
18 "use strict";
20
19
21 // Only run once on a notebook.
20 // Only run once on a notebook.
22 require(["components/underscore/underscore-min",
21 if (IPython.notebook.widget_manager == undefined) {
23 "components/backbone/backbone-min"], function () {
22
24 if (IPython.notebook.widget_manager == undefined) {
23 //-----------------------------------------------------------------------
25
24 // WidgetModel class
26 //-----------------------------------------------------------------------
25 //-----------------------------------------------------------------------
27 // WidgetModel class
26 var WidgetModel = Backbone.Model.extend({
28 //-----------------------------------------------------------------------
27 apply: function(sender) {
29 var WidgetModel = Backbone.Model.extend({
28 this.save();
30 apply: function(sender) {
29
31 this.save();
30 for (var index in this.views) {
32
31 var view = this.views[index];
33 for (var index in this.views) {
32 if (view !== sender) {
34 var view = this.views[index];
33 view.refresh();
35 if (view !== sender) {
36 view.refresh();
37 }
38 }
34 }
39 }
35 }
40 });
36 }
41
37 });
42
38
43 //-----------------------------------------------------------------------
39
44 // WidgetView class
40 //-----------------------------------------------------------------------
45 //-----------------------------------------------------------------------
41 // WidgetView class
46 var WidgetView = Backbone.View.extend({
42 //-----------------------------------------------------------------------
47
43 var WidgetView = Backbone.View.extend({
48 initialize: function() {
44
49 this.model.on('change',this.refresh,this);
45 initialize: function() {
50 },
46 this.model.on('change',this.refresh,this);
47 },
48
49 refresh: function() {
50 this.update();
51
51
52 refresh: function() {
52 if (this.model.css != undefined) {
53 this.update();
53 for (var selector in this.model.css) {
54
54 if (this.model.css.hasOwnProperty(selector)) {
55 if (this.model.css != undefined) {
55
56 for (var selector in this.model.css) {
56 // Get the elements via the css selector. If the selector is
57 if (this.model.css.hasOwnProperty(selector)) {
57 // blank, assume the current element is the target.
58
58 var elements = this.$el.find(selector);
59 // Get the elements via the css selector. If the selector is
59 if (selector=='') {
60 // blank, assume the current element is the target.
60 elements = this.$el;
61 var elements = this.$el.find(selector);
61 }
62 if (selector=='') {
62
63 elements = this.$el;
63 // Apply the css traits to all elements that match the selector.
64 }
64 if (elements.length>0){
65
65 var css_traits = this.model.css[selector];
66 // Apply the css traits to all elements that match the selector.
66 for (var css_key in css_traits) {
67 if (elements.length>0){
67 if (css_traits.hasOwnProperty(css_key)) {
68 var css_traits = this.model.css[selector];
68 elements.css(css_key, css_traits[css_key]);
69 for (var css_key in css_traits) {
70 if (css_traits.hasOwnProperty(css_key)) {
71 elements.css(css_key, css_traits[css_key]);
72 }
73 }
69 }
74 }
70 }
75 }
71 }
76 }
72 }
77 }
73 }
78 },
74 }
79 });
75 },
76 });
77
78
79 //-----------------------------------------------------------------------
80 // WidgetManager class
81 //-----------------------------------------------------------------------
82 // Public constructor
83 var WidgetManager = function(comm_manager){
84 this.comm_manager = comm_manager;
85 this.widget_model_types = {};
86 this.widget_view_types = {};
87 this.model_widget_views = {};
88
89 var that = this;
90 Backbone.sync = function(method, model, options, error) {
91 var result = that.send_sync(method, model);
92 if (options.success) {
93 options.success(result);
94 }
95 };
96 }
80
97
98 // Register a widget model type.
99 WidgetManager.prototype.register_widget_model = function (widget_model_name, widget_model_type) {
100
101 // Register the widget with the comm manager. Make sure to pass this object's context
102 // in so `this` works in the call back.
103 this.comm_manager.register_target(widget_model_name, $.proxy(this.handle_com_open, this));
104
105 // Register the types of the model and view correspong to this widget type. Later
106 // the widget manager will initialize these when the comm is opened.
107 this.widget_model_types[widget_model_name] = widget_model_type;
108 }
81
109
82 //-----------------------------------------------------------------------
110 // Register a widget view type.
83 // WidgetManager class
111 WidgetManager.prototype.register_widget_view = function (widget_view_name, widget_view_type) {
84 //-----------------------------------------------------------------------
112 this.widget_view_types[widget_view_name] = widget_view_type;
85 // Public constructor
113 }
86 var WidgetManager = function(comm_manager){
87 this.comm_manager = comm_manager;
88 this.widget_model_types = {};
89 this.widget_view_types = {};
90 this.model_widget_views = {};
91
92 var that = this;
93 Backbone.sync = function(method, model, options, error) {
94 var result = that.send_sync(method, model);
95 if (options.success) {
96 options.success(result);
97 }
98 };
99 }
100
114
101 // Register a widget model type.
115 // Handle when a comm is opened.
102 WidgetManager.prototype.register_widget_model = function (widget_model_name, widget_model_type) {
116 WidgetManager.prototype.handle_com_open = function (comm, msg) {
103
117 var widget_type_name = msg.content.target_name;
104 // Register the widget with the comm manager. Make sure to pass this object's context
118
105 // in so `this` works in the call back.
119 // Create the corresponding widget model.
106 this.comm_manager.register_target(widget_model_name, $.proxy(this.handle_com_open, this));
120 var widget_model = new this.widget_model_types[widget_type_name];
107
108 // Register the types of the model and view correspong to this widget type. Later
109 // the widget manager will initialize these when the comm is opened.
110 this.widget_model_types[widget_model_name] = widget_model_type;
111 }
112
121
113 // Register a widget view type.
122 // Remember comm associated with the model.
114 WidgetManager.prototype.register_widget_view = function (widget_view_name, widget_view_type) {
123 widget_model.comm = comm;
115 this.widget_view_types[widget_view_name] = widget_view_type;
124 comm.model = widget_model;
116 }
117
125
118 // Handle when a comm is opened.
126 // Create an array to remember the views associated with the model.
119 WidgetManager.prototype.handle_com_open = function (comm, msg) {
127 widget_model.views = [];
120 var widget_type_name = msg.content.target_name;
121
122 // Create the corresponding widget model.
123 var widget_model = new this.widget_model_types[widget_type_name];
124
128
125 // Remember comm associated with the model.
129 // Add a handle to delete the control when the comm is closed.
126 widget_model.comm = comm;
130 var that = this;
127 comm.model = widget_model;
131 var handle_close = function(msg) {
132 that.handle_comm_closed(comm, msg);
133 }
134 comm.on_close(handle_close);
128
135
129 // Create an array to remember the views associated with the model.
136 // Handle incomming messages.
130 widget_model.views = [];
137 var handle_msg = function(msg) {
138 that.handle_comm_msg(comm, msg);
139 }
140 comm.on_msg(handle_msg);
141 }
131
142
132 // Add a handle to delete the control when the comm is closed.
143 // Create view that represents the model.
133 var that = this;
144 WidgetManager.prototype.show_view = function (widget_area, widget_model, widget_view_name) {
134 var handle_close = function(msg) {
145 var widget_view = new this.widget_view_types[widget_view_name]({model: widget_model});
135 that.handle_comm_closed(comm, msg);
146 widget_view.render();
147 widget_model.views.push(widget_view);
148
149 // Handle when the view element is remove from the page.
150 widget_view.$el.on("remove", function(){
151 var index = widget_model.views.indexOf(widget_view);
152 if (index > -1) {
153 widget_model.views.splice(index, 1);
136 }
154 }
137 comm.on_close(handle_close);
155 widget_view.remove(); // Clean-up view
138
156
139 // Handle incomming messages.
157 // Close the comm if there are no views left.
140 var handle_msg = function(msg) {
158 if (widget_model.views.length()==0) {
141 that.handle_comm_msg(comm, msg);
159 widget_model.comm.close();
142 }
160 }
143 comm.on_msg(handle_msg);
161 });
144 }
145
146 // Create view that represents the model.
147 WidgetManager.prototype.show_view = function (widget_area, widget_model, widget_view_name) {
148 var widget_view = new this.widget_view_types[widget_view_name]({model: widget_model});
149 widget_view.render();
150 widget_model.views.push(widget_view);
151
152 // Handle when the view element is remove from the page.
153 widget_view.$el.on("remove", function(){
154 var index = widget_model.views.indexOf(widget_view);
155 if (index > -1) {
156 widget_model.views.splice(index, 1);
157 }
158 widget_view.remove(); // Clean-up view
159
160 // Close the comm if there are no views left.
161 if (widget_model.views.length()==0) {
162 widget_model.comm.close();
163 }
164 });
165
162
166 // Add the view's element to cell's widget div.
163 // Add the view's element to cell's widget div.
167 widget_area
164 widget_area
168 .append($("<div />").append(widget_view.$el))
165 .append($("<div />").append(widget_view.$el))
169 .parent().show(); // Show the widget_area (parent of widget_subarea)
166 .parent().show(); // Show the widget_area (parent of widget_subarea)
170
167
171 // Update the view based on the model contents.
168 // Update the view based on the model contents.
172 widget_view.refresh();
169 widget_view.refresh();
173 }
170 }
174
171
175 // Handle incomming comm msg.
172 // Handle incomming comm msg.
176 WidgetManager.prototype.handle_comm_msg = function (comm, msg) {
173 WidgetManager.prototype.handle_comm_msg = function (comm, msg) {
177 // Different logic for different methods.
174 // Different logic for different methods.
178 var method = msg.content.data.method;
175 var method = msg.content.data.method;
179 switch (method){
176 switch (method){
180 case 'show':
177 case 'show':
181
178
182 // TODO: Get cell from registered output handler.
179 // TODO: Get cell from registered output handler.
183 var cell = IPython.notebook.get_cell(IPython.notebook.get_selected_index()-1);
180 var cell = IPython.notebook.get_cell(IPython.notebook.get_selected_index()-1);
184 var widget_subarea = cell.element.find('.widget_area').find('.widget_subarea');
181 var widget_subarea = cell.element.find('.widget_area').find('.widget_subarea');
185
182
186 if (msg.content.data.parent != undefined) {
183 if (msg.content.data.parent != undefined) {
187 var find_results = widget_subarea.find("." + msg.content.data.parent);
184 var find_results = widget_subarea.find("." + msg.content.data.parent);
188 if (find_results.length > 0) {
185 if (find_results.length > 0) {
189 widget_subarea = find_results;
186 widget_subarea = find_results;
190 }
191 }
187 }
188 }
192
189
193 this.show_view(widget_subarea, comm.model, msg.content.data.view_name);
190 this.show_view(widget_subarea, comm.model, msg.content.data.view_name);
194 break;
191 break;
195 case 'update':
192 case 'update':
196 this.handle_update(comm, msg.content.data.state);
193 this.handle_update(comm, msg.content.data.state);
197 break;
194 break;
198 }
199 }
195 }
196 }
200
197
201 // Handle when a widget is updated via the python side.
198 // Handle when a widget is updated via the python side.
202 WidgetManager.prototype.handle_update = function (comm, state) {
199 WidgetManager.prototype.handle_update = function (comm, state) {
203 for (var key in state) {
200 for (var key in state) {
204 if (state.hasOwnProperty(key)) {
201 if (state.hasOwnProperty(key)) {
205 if (key=="_css"){
202 if (key=="_css"){
206 comm.model.css = state[key];
203 comm.model.css = state[key];
207 } else {
204 } else {
208 comm.model.set(key, state[key]);
205 comm.model.set(key, state[key]);
209 }
210 }
206 }
211 }
207 }
212 comm.model.save();
213 }
208 }
209 comm.model.save();
210 }
214
211
215 // Handle when a widget is closed.
212 // Handle when a widget is closed.
216 WidgetManager.prototype.handle_comm_closed = function (comm, msg) {
213 WidgetManager.prototype.handle_comm_closed = function (comm, msg) {
217 for (var view_index in comm.model.views) {
214 for (var view_index in comm.model.views) {
218 var view = comm.model.views[view_index];
215 var view = comm.model.views[view_index];
219 view.remove();
216 view.remove();
220 }
221 }
217 }
218 }
222
219
223 // Get the cell output area corresponding to the comm.
220 // Get the cell output area corresponding to the comm.
224 WidgetManager.prototype._get_comm_outputarea = function (comm) {
221 WidgetManager.prototype._get_comm_outputarea = function (comm) {
225 // TODO: get element from comm instead of guessing
222 // TODO: get element from comm instead of guessing
226 var cell = IPython.notebook.get_cell(IPython.notebook.get_selected_index())
223 var cell = IPython.notebook.get_cell(IPython.notebook.get_selected_index())
227 return cell.output_area;
224 return cell.output_area;
228 }
225 }
229
226
230 // Send widget state to python backend.
227 // Send widget state to python backend.
231 WidgetManager.prototype.send_sync = function (method, model) {
228 WidgetManager.prototype.send_sync = function (method, model) {
232
229
233 // Create a callback for the output if the widget has an output area associate with it.
230 // Create a callback for the output if the widget has an output area associate with it.
234 var callbacks = {};
231 var callbacks = {};
235 var comm = model.comm;
232 var comm = model.comm;
236 var outputarea = this._get_comm_outputarea(comm);
233 var outputarea = this._get_comm_outputarea(comm);
237 if (outputarea != null) {
234 if (outputarea != null) {
238 callbacks = {
235 callbacks = {
239 iopub : {
236 iopub : {
240 output : $.proxy(outputarea.handle_output, outputarea),
237 output : $.proxy(outputarea.handle_output, outputarea),
241 clear_output : $.proxy(outputarea.handle_clear_output, outputarea)}
238 clear_output : $.proxy(outputarea.handle_clear_output, outputarea)}
242 };
243 };
239 };
244 var model_json = model.toJSON();
240 };
245 var data = {sync_method: method, sync_data: model_json};
241 var model_json = model.toJSON();
246 comm.send(data, callbacks);
242 var data = {sync_method: method, sync_data: model_json};
247 return model_json;
243 comm.send(data, callbacks);
248 }
244 return model_json;
245 }
249
246
250 IPython.WidgetManager = WidgetManager;
247 IPython.WidgetManager = WidgetManager;
251 IPython.WidgetModel = WidgetModel;
248 IPython.WidgetModel = WidgetModel;
252 IPython.WidgetView = WidgetView;
249 IPython.WidgetView = WidgetView;
253
250
254 IPython.notebook.widget_manager = new WidgetManager(IPython.notebook.kernel.comm_manager);
251 IPython.notebook.widget_manager = new WidgetManager(IPython.notebook.kernel.comm_manager);
255
252
256 }
253 };
257 });
@@ -1,19 +1,17 b''
1 require(["notebook/js/widget"], function () {
1 var ContainerModel = IPython.WidgetModel.extend({});
2 var ContainerModel = IPython.WidgetModel.extend({});
2 IPython.notebook.widget_manager.register_widget_model('container_widget', ContainerModel);
3 IPython.notebook.widget_manager.register_widget_model('container_widget', ContainerModel);
4
3
5 var ContainerView = IPython.WidgetView.extend({
4 var ContainerView = IPython.WidgetView.extend({
6
5
7 render : function(){
6 render : function(){
8 this.$el.html('');
7 this.$el.html('');
9 this.$container = $('<div />')
8 this.$container = $('<div />')
10 .addClass('container')
9 .addClass('container')
11 .addClass(this.model.comm.comm_id);
10 .addClass(this.model.comm.comm_id);
12 this.$el.append(this.$container);
11 this.$el.append(this.$container);
13 },
12 },
14
13
15 update : function(){},
14 update : function(){},
16 });
15 });
17
16
18 IPython.notebook.widget_manager.register_widget_view('ContainerView', ContainerView);
17 IPython.notebook.widget_manager.register_widget_view('ContainerView', ContainerView);
19 }); No newline at end of file
@@ -1,121 +1,119 b''
1 require(["notebook/js/widget"], function () {
1 var FloatRangeWidgetModel = IPython.WidgetModel.extend({});
2 var FloatRangeWidgetModel = IPython.WidgetModel.extend({});
2 IPython.notebook.widget_manager.register_widget_model('FloatRangeWidgetModel', FloatRangeWidgetModel);
3 IPython.notebook.widget_manager.register_widget_model('FloatRangeWidgetModel', FloatRangeWidgetModel);
4
3
5 var FloatSliderView = IPython.WidgetView.extend({
4 var FloatSliderView = IPython.WidgetView.extend({
5
6 // Called when view is rendered.
7 render : function(){
8 this.$el
9 .html('')
10 .addClass(this.model.comm.comm_id);
11 this.$slider = $('<div />')
12 .slider({})
13 .addClass('slider');
6
14
7 // Called when view is rendered.
15 // Put the slider in a container
8 render : function(){
16 this.$slider_container = $('<div />')
9 this.$el
17 .css('padding-top', '4px')
10 .html('')
18 .css('padding-bottom', '4px')
11 .addClass(this.model.comm.comm_id);
19 .append(this.$slider);
12 this.$slider = $('<div />')
20 this.$el.append(this.$slider_container);
13 .slider({})
14 .addClass('slider');
15
16 // Put the slider in a container
17 this.$slider_container = $('<div />')
18 .css('padding-top', '4px')
19 .css('padding-bottom', '4px')
20 .append(this.$slider);
21 this.$el.append(this.$slider_container);
22
23 // Set defaults.
24 this.update();
25 },
26
21
27 // Handles: Backend -> Frontend Sync
22 // Set defaults.
28 // Frontent -> Frontend Sync
23 this.update();
29 update : function(){
24 },
30 // Slider related keys.
25
31 var _keys = ['value', 'step', 'max', 'min', 'disabled', 'orientation'];
26 // Handles: Backend -> Frontend Sync
32 for (var index in _keys) {
27 // Frontent -> Frontend Sync
33 var key = _keys[index];
28 update : function(){
34 if (this.model.get(key) != undefined) {
29 // Slider related keys.
35 this.$slider.slider("option", key, this.model.get(key));
30 var _keys = ['value', 'step', 'max', 'min', 'disabled', 'orientation'];
36 }
31 for (var index in _keys) {
32 var key = _keys[index];
33 if (this.model.get(key) != undefined) {
34 this.$slider.slider("option", key, this.model.get(key));
37 }
35 }
38 },
36 }
39
37 },
40 // Handles: User input
38
41 events: { "slide" : "handleSliderChange" },
39 // Handles: User input
42 handleSliderChange: function(e, ui) {
40 events: { "slide" : "handleSliderChange" },
43 this.model.set('value', ui.value);
41 handleSliderChange: function(e, ui) {
44 this.model.apply(this);
42 this.model.set('value', ui.value);
45 },
43 this.model.apply(this);
46 });
44 },
45 });
47
46
48 IPython.notebook.widget_manager.register_widget_view('FloatSliderView', FloatSliderView);
47 IPython.notebook.widget_manager.register_widget_view('FloatSliderView', FloatSliderView);
49
48
50
49
51 var FloatTextView = IPython.WidgetView.extend({
50 var FloatTextView = IPython.WidgetView.extend({
52
51
53 // Called when view is rendered.
52 // Called when view is rendered.
54 render : function(){
53 render : function(){
55 this.$el
54 this.$el
56 .html('')
55 .html('')
57 .addClass(this.model.comm.comm_id);
56 .addClass(this.model.comm.comm_id);
58 this.$textbox = $('<input type="text" />')
57 this.$textbox = $('<input type="text" />')
59 .addClass('input')
58 .addClass('input')
60 .appendTo(this.$el);
59 .appendTo(this.$el);
61 this.update(); // Set defaults.
60 this.update(); // Set defaults.
62 },
61 },
63
62
64 // Handles: Backend -> Frontend Sync
63 // Handles: Backend -> Frontend Sync
65 // Frontent -> Frontend Sync
64 // Frontent -> Frontend Sync
66 update : function(){
65 update : function(){
67 var value = this.model.get('value');
66 var value = this.model.get('value');
68 if (!this.changing && parseFloat(this.$textbox.val()) != value) {
67 if (!this.changing && parseFloat(this.$textbox.val()) != value) {
69 this.$textbox.val(value);
68 this.$textbox.val(value);
70 }
69 }
71
72 if (this.model.get('disabled')) {
73 this.$textbox.attr('disabled','disabled');
74 } else {
75 this.$textbox.removeAttr('disabled');
76 }
77 },
78
70
71 if (this.model.get('disabled')) {
72 this.$textbox.attr('disabled','disabled');
73 } else {
74 this.$textbox.removeAttr('disabled');
75 }
76 },
77
78
79 events: {"keyup input" : "handleChanging",
80 "paste input" : "handleChanging",
81 "cut input" : "handleChanging",
82 "change input" : "handleChanged"}, // Fires only when control is validated or looses focus.
83
84 // Handles and validates user input.
85 handleChanging: function(e) {
79
86
80 events: {"keyup input" : "handleChanging",
87 // Try to parse value as a float.
81 "paste input" : "handleChanging",
88 var numericalValue = 0.0;
82 "cut input" : "handleChanging",
89 if (e.target.value != '') {
83 "change input" : "handleChanged"}, // Fires only when control is validated or looses focus.
90 numericalValue = parseFloat(e.target.value);
91 }
84
92
85 // Handles and validates user input.
93 // If parse failed, reset value to value stored in model.
86 handleChanging: function(e) {
94 if (isNaN(numericalValue)) {
87
95 e.target.value = this.model.get('value');
88 // Try to parse value as a float.
96 } else if (!isNaN(numericalValue)) {
89 var numericalValue = 0.0;
97 numericalValue = Math.min(this.model.get('max'), numericalValue);
90 if (e.target.value != '') {
98 numericalValue = Math.max(this.model.get('min'), numericalValue);
91 numericalValue = parseFloat(e.target.value);
92 }
93
99
94 // If parse failed, reset value to value stored in model.
100 // Apply the value if it has changed.
95 if (isNaN(numericalValue)) {
101 if (numericalValue != this.model.get('value')) {
96 e.target.value = this.model.get('value');
102 this.changing = true;
97 } else if (!isNaN(numericalValue)) {
103 this.model.set('value', numericalValue);
98 numericalValue = Math.min(this.model.get('max'), numericalValue);
104 this.model.apply(this);
99 numericalValue = Math.max(this.model.get('min'), numericalValue);
105 this.changing = false;
100
101 // Apply the value if it has changed.
102 if (numericalValue != this.model.get('value')) {
103 this.changing = true;
104 this.model.set('value', numericalValue);
105 this.model.apply(this);
106 this.changing = false;
107 }
108 }
109 },
110
111 // Applies validated input.
112 handleChanged: function(e) {
113 // Update the textbox
114 if (this.model.get('value') != e.target.value) {
115 e.target.value = this.model.get('value');
116 }
106 }
117 }
107 }
118 });
108 },
109
110 // Applies validated input.
111 handleChanged: function(e) {
112 // Update the textbox
113 if (this.model.get('value') != e.target.value) {
114 e.target.value = this.model.get('value');
115 }
116 }
117 });
119
118
120 IPython.notebook.widget_manager.register_widget_view('FloatTextView', FloatTextView);
119 IPython.notebook.widget_manager.register_widget_view('FloatTextView', FloatTextView);
121 }); No newline at end of file
@@ -1,119 +1,117 b''
1 require(["notebook/js/widget"], function () {
1 var IntRangeWidgetModel = IPython.WidgetModel.extend({});
2 var IntRangeWidgetModel = IPython.WidgetModel.extend({});
2 IPython.notebook.widget_manager.register_widget_model('IntRangeWidgetModel', IntRangeWidgetModel);
3 IPython.notebook.widget_manager.register_widget_model('IntRangeWidgetModel', IntRangeWidgetModel);
4
3
5 var IntSliderView = IPython.WidgetView.extend({
4 var IntSliderView = IPython.WidgetView.extend({
5
6 // Called when view is rendered.
7 render : function(){
8 this.$el.html('');
9 this.$slider = $('<div />')
10 .slider({})
11 .addClass('slider');
6
12
7 // Called when view is rendered.
13 // Put the slider in a container
8 render : function(){
14 this.$slider_container = $('<div />')
9 this.$el.html('');
15 .css('padding-top', '4px')
10 this.$slider = $('<div />')
16 .css('padding-bottom', '4px')
11 .slider({})
17 .addClass(this.model.comm.comm_id)
12 .addClass('slider');
18 .append(this.$slider);
13
19 this.$el.append(this.$slider_container);
14 // Put the slider in a container
15 this.$slider_container = $('<div />')
16 .css('padding-top', '4px')
17 .css('padding-bottom', '4px')
18 .addClass(this.model.comm.comm_id)
19 .append(this.$slider);
20 this.$el.append(this.$slider_container);
21
22 // Set defaults.
23 this.update();
24 },
25
20
26 // Handles: Backend -> Frontend Sync
21 // Set defaults.
27 // Frontent -> Frontend Sync
22 this.update();
28 update : function(){
23 },
29 // Slider related keys.
24
30 var _keys = ['value', 'step', 'max', 'min', 'disabled', 'orientation'];
25 // Handles: Backend -> Frontend Sync
31 for (var index in _keys) {
26 // Frontent -> Frontend Sync
32 var key = _keys[index];
27 update : function(){
33 if (this.model.get(key) != undefined) {
28 // Slider related keys.
34 this.$slider.slider("option", key, this.model.get(key));
29 var _keys = ['value', 'step', 'max', 'min', 'disabled', 'orientation'];
35 }
30 for (var index in _keys) {
31 var key = _keys[index];
32 if (this.model.get(key) != undefined) {
33 this.$slider.slider("option", key, this.model.get(key));
36 }
34 }
37 },
35 }
38
36 },
39 // Handles: User input
37
40 events: { "slide" : "handleSliderChange" },
38 // Handles: User input
41 handleSliderChange: function(e, ui) {
39 events: { "slide" : "handleSliderChange" },
42 this.model.set('value', ~~ui.value); // Double bit-wise not to truncate decimel
40 handleSliderChange: function(e, ui) {
43 this.model.apply(this);
41 this.model.set('value', ~~ui.value); // Double bit-wise not to truncate decimel
44 },
42 this.model.apply(this);
45 });
43 },
44 });
46
45
47 IPython.notebook.widget_manager.register_widget_view('IntSliderView', IntSliderView);
46 IPython.notebook.widget_manager.register_widget_view('IntSliderView', IntSliderView);
48
47
49 var IntTextView = IPython.WidgetView.extend({
48 var IntTextView = IPython.WidgetView.extend({
50
49
51 // Called when view is rendered.
50 // Called when view is rendered.
52 render : function(){
51 render : function(){
53 this.$el
52 this.$el
54 .html('')
53 .html('')
55 .addClass(this.model.comm.comm_id);
54 .addClass(this.model.comm.comm_id);
56 this.$textbox = $('<input type="text" />')
55 this.$textbox = $('<input type="text" />')
57 .addClass('input')
56 .addClass('input')
58 .appendTo(this.$el);
57 .appendTo(this.$el);
59 this.update(); // Set defaults.
58 this.update(); // Set defaults.
60 },
59 },
61
60
62 // Handles: Backend -> Frontend Sync
61 // Handles: Backend -> Frontend Sync
63 // Frontent -> Frontend Sync
62 // Frontent -> Frontend Sync
64 update : function(){
63 update : function(){
65 var value = this.model.get('value');
64 var value = this.model.get('value');
66 if (!this.changing && parseInt(this.$textbox.val()) != value) {
65 if (!this.changing && parseInt(this.$textbox.val()) != value) {
67 this.$textbox.val(value);
66 this.$textbox.val(value);
68 }
67 }
69
70 if (this.model.get('disabled')) {
71 this.$textbox.attr('disabled','disabled');
72 } else {
73 this.$textbox.removeAttr('disabled');
74 }
75 },
76
68
69 if (this.model.get('disabled')) {
70 this.$textbox.attr('disabled','disabled');
71 } else {
72 this.$textbox.removeAttr('disabled');
73 }
74 },
75
76
77 events: {"keyup input" : "handleChanging",
78 "paste input" : "handleChanging",
79 "cut input" : "handleChanging",
80 "change input" : "handleChanged"}, // Fires only when control is validated or looses focus.
81
82 // Handles and validates user input.
83 handleChanging: function(e) {
77
84
78 events: {"keyup input" : "handleChanging",
85 // Try to parse value as a float.
79 "paste input" : "handleChanging",
86 var numericalValue = 0;
80 "cut input" : "handleChanging",
87 if (e.target.value != '') {
81 "change input" : "handleChanged"}, // Fires only when control is validated or looses focus.
88 numericalValue = parseInt(e.target.value);
89 }
82
90
83 // Handles and validates user input.
91 // If parse failed, reset value to value stored in model.
84 handleChanging: function(e) {
92 if (isNaN(numericalValue)) {
85
93 e.target.value = this.model.get('value');
86 // Try to parse value as a float.
94 } else if (!isNaN(numericalValue)) {
87 var numericalValue = 0;
95 numericalValue = Math.min(this.model.get('max'), numericalValue);
88 if (e.target.value != '') {
96 numericalValue = Math.max(this.model.get('min'), numericalValue);
89 numericalValue = parseInt(e.target.value);
90 }
91
97
92 // If parse failed, reset value to value stored in model.
98 // Apply the value if it has changed.
93 if (isNaN(numericalValue)) {
99 if (numericalValue != this.model.get('value')) {
94 e.target.value = this.model.get('value');
100 this.changing = true;
95 } else if (!isNaN(numericalValue)) {
101 this.model.set('value', numericalValue);
96 numericalValue = Math.min(this.model.get('max'), numericalValue);
102 this.model.apply(this);
97 numericalValue = Math.max(this.model.get('min'), numericalValue);
103 this.changing = false;
98
99 // Apply the value if it has changed.
100 if (numericalValue != this.model.get('value')) {
101 this.changing = true;
102 this.model.set('value', numericalValue);
103 this.model.apply(this);
104 this.changing = false;
105 }
106 }
107 },
108
109 // Applies validated input.
110 handleChanged: function(e) {
111 // Update the textbox
112 if (this.model.get('value') != e.target.value) {
113 e.target.value = this.model.get('value');
114 }
104 }
115 }
105 }
116 });
106 },
107
108 // Applies validated input.
109 handleChanged: function(e) {
110 // Update the textbox
111 if (this.model.get('value') != e.target.value) {
112 e.target.value = this.model.get('value');
113 }
114 }
115 });
117
116
118 IPython.notebook.widget_manager.register_widget_view('IntTextView', IntTextView);
117 IPython.notebook.widget_manager.register_widget_view('IntTextView', IntTextView);
119 }); No newline at end of file
@@ -1,130 +1,128 b''
1 require(["notebook/js/widget"], function () {
1 var SelectionWidgetModel = IPython.WidgetModel.extend({});
2 var SelectionWidgetModel = IPython.WidgetModel.extend({});
2 IPython.notebook.widget_manager.register_widget_model('SelectionWidgetModel', SelectionWidgetModel);
3 IPython.notebook.widget_manager.register_widget_model('SelectionWidgetModel', SelectionWidgetModel);
4
3
5 var DropdownView = IPython.WidgetView.extend({
4 var DropdownView = IPython.WidgetView.extend({
5
6 // Called when view is rendered.
7 render : function(){
6
8
7 // Called when view is rendered.
9 this.$el
8 render : function(){
10 .html('')
9
11 .addClass(this.model.comm.comm_id);
10 this.$el
12 this.$buttongroup = $('<div />')
11 .html('')
13 .addClass('btn-group')
12 .addClass(this.model.comm.comm_id);
14 .appendTo(this.$el);
13 this.$buttongroup = $('<div />')
15 this.$droplabel = $('<button />')
14 .addClass('btn-group')
16 .addClass('btn')
15 .appendTo(this.$el);
17 .appendTo(this.$buttongroup);
16 this.$droplabel = $('<button />')
18 this.$dropbutton = $('<button />')
17 .addClass('btn')
19 .addClass('btn')
18 .appendTo(this.$buttongroup);
20 .addClass('dropdown-toggle')
19 this.$dropbutton = $('<button />')
21 .attr('data-toggle', 'dropdown')
20 .addClass('btn')
22 .html('<span class="caret"></span>')
21 .addClass('dropdown-toggle')
23 .appendTo(this.$buttongroup);
22 .attr('data-toggle', 'dropdown')
24 this.$droplist = $('<ul />')
23 .html('<span class="caret"></span>')
25 .addClass('dropdown-menu')
24 .appendTo(this.$buttongroup);
26 .appendTo(this.$buttongroup);
25 this.$droplist = $('<ul />')
27
26 .addClass('dropdown-menu')
28 // Set defaults.
27 .appendTo(this.$buttongroup);
29 this.update();
28
30 },
29 // Set defaults.
31
30 this.update();
32 // Handles: Backend -> Frontend Sync
31 },
33 // Frontent -> Frontend Sync
34 update : function(){
35 this.$droplabel.html(this.model.get('value'));
32
36
33 // Handles: Backend -> Frontend Sync
37 var items = this.model.get('values');
34 // Frontent -> Frontend Sync
38 this.$droplist.html('');
35 update : function(){
39 for (var index in items) {
36 this.$droplabel.html(this.model.get('value'));
40 var that = this;
41 var item_button = $('<a href="#"/>')
42 .html(items[index])
43 .on('click', function(e){
44 that.model.set('value', $(e.target).html(), this );
45 })
37
46
38 var items = this.model.get('values');
47 this.$droplist.append($('<li />').append(item_button))
39 this.$droplist.html('');
48 }
40 for (var index in items) {
49
41 var that = this;
50 if (this.model.get('disabled')) {
42 var item_button = $('<a href="#"/>')
51 this.$buttongroup.attr('disabled','disabled');
52 this.$droplabel.attr('disabled','disabled');
53 this.$dropbutton.attr('disabled','disabled');
54 this.$droplist.attr('disabled','disabled');
55 } else {
56 this.$buttongroup.removeAttr('disabled');
57 this.$droplabel.removeAttr('disabled');
58 this.$dropbutton.removeAttr('disabled');
59 this.$droplist.removeAttr('disabled');
60 }
61 },
62
63 });
64
65 IPython.notebook.widget_manager.register_widget_view('DropdownView', DropdownView);
66 var RadioButtonView = IPython.WidgetView.extend({
67
68 // Called when view is rendered.
69 render : function(){
70 this.$el
71 .html('')
72 .addClass(this.model.comm.comm_id);
73 this.update();
74 },
75
76 // Handles: Backend -> Frontend Sync
77 // Frontent -> Frontend Sync
78 update : function(){
79
80 // Add missing items to the DOM.
81 var items = this.model.get('values');
82 for (var index in items) {
83 var item_query = ' :input[value="' + items[index] + '"]';
84 if (this.$el.find(item_query).length == 0) {
85 var $label = $('<label />')
86 .addClass('radio')
43 .html(items[index])
87 .html(items[index])
44 .on('click', function(e){
88 .appendTo(this.$el);
45 that.model.set('value', $(e.target).html(), this );
46 })
47
89
48 this.$droplist.append($('<li />').append(item_button))
90 var that = this;
91 $('<input />')
92 .attr('type', 'radio')
93 .addClass(this.model)
94 .val(items[index])
95 .prependTo($label)
96 .on('click', function(e){
97 that.model.set('value', $(e.target).val(), this);
98 that.model.apply();
99 });
49 }
100 }
50
101
51 if (this.model.get('disabled')) {
102 if (this.model.get('value') == items[index]) {
52 this.$buttongroup.attr('disabled','disabled');
103 this.$el.find(item_query).prop('checked', true);
53 this.$droplabel.attr('disabled','disabled');
54 this.$dropbutton.attr('disabled','disabled');
55 this.$droplist.attr('disabled','disabled');
56 } else {
104 } else {
57 this.$buttongroup.removeAttr('disabled');
105 this.$el.find(item_query).prop('checked', false);
58 this.$droplabel.removeAttr('disabled');
59 this.$dropbutton.removeAttr('disabled');
60 this.$droplist.removeAttr('disabled');
61 }
106 }
62 },
107 }
63
108
64 });
109 // Remove items that no longer exist.
65
110 this.$el.find('input').each(function(i, obj) {
66 IPython.notebook.widget_manager.register_widget_view('DropdownView', DropdownView);
111 var value = $(obj).val();
67 var RadioButtonView = IPython.WidgetView.extend({
112 var found = false;
68
69 // Called when view is rendered.
70 render : function(){
71 this.$el
72 .html('')
73 .addClass(this.model.comm.comm_id);
74 this.update();
75 },
76
77 // Handles: Backend -> Frontend Sync
78 // Frontent -> Frontend Sync
79 update : function(){
80
81 // Add missing items to the DOM.
82 var items = this.model.get('values');
83 for (var index in items) {
113 for (var index in items) {
84 var item_query = ' :input[value="' + items[index] + '"]';
114 if (items[index] == value) {
85 if (this.$el.find(item_query).length == 0) {
115 found = true;
86 var $label = $('<label />')
116 break;
87 .addClass('radio')
88 .html(items[index])
89 .appendTo(this.$el);
90
91 var that = this;
92 $('<input />')
93 .attr('type', 'radio')
94 .addClass(this.model)
95 .val(items[index])
96 .prependTo($label)
97 .on('click', function(e){
98 that.model.set('value', $(e.target).val(), this);
99 that.model.apply();
100 });
101 }
102
103 if (this.model.get('value') == items[index]) {
104 this.$el.find(item_query).prop('checked', true);
105 } else {
106 this.$el.find(item_query).prop('checked', false);
107 }
117 }
108 }
118 }
109
119
110 // Remove items that no longer exist.
120 if (!found) {
111 this.$el.find('input').each(function(i, obj) {
121 $(obj).parent().remove();
112 var value = $(obj).val();
122 }
113 var found = false;
123 });
114 for (var index in items) {
124 },
115 if (items[index] == value) {
125
116 found = true;
126 });
117 break;
118 }
119 }
120
121 if (!found) {
122 $(obj).parent().remove();
123 }
124 });
125 },
126
127 });
128
127
129 IPython.notebook.widget_manager.register_widget_view('RadioButtonView', RadioButtonView);
128 IPython.notebook.widget_manager.register_widget_view('RadioButtonView', RadioButtonView);
130 }); No newline at end of file
@@ -1,77 +1,74 b''
1 require(["notebook/js/widget"], function () {
1 var StringWidgetModel = IPython.WidgetModel.extend({});
2 var StringWidgetModel = IPython.WidgetModel.extend({});
2 IPython.notebook.widget_manager.register_widget_model('StringWidgetModel', StringWidgetModel);
3 IPython.notebook.widget_manager.register_widget_model('StringWidgetModel', StringWidgetModel);
4
3
5 var TextareaView = IPython.WidgetView.extend({
4 var TextareaView = IPython.WidgetView.extend({
6
5
7 // Called when view is rendered.
6 // Called when view is rendered.
8 render : function(){
7 render : function(){
9 this.$el
8 this.$el
10 .html('')
9 .html('')
11 .addClass(this.model.comm.comm_id);
10 .addClass(this.model.comm.comm_id);
12 this.$textbox = $('<textarea />')
11 this.$textbox = $('<textarea />')
13 .attr('rows', 5)
12 .attr('rows', 5)
14 .appendTo(this.$el);
13 .appendTo(this.$el);
15 this.update(); // Set defaults.
14 this.update(); // Set defaults.
16 },
15 },
17
16
18 // Handles: Backend -> Frontend Sync
17 // Handles: Backend -> Frontend Sync
19 // Frontent -> Frontend Sync
18 // Frontent -> Frontend Sync
20 update : function(){
19 update : function(){
21 if (!this.user_invoked_update) {
20 if (!this.user_invoked_update) {
22 this.$textbox.val(this.model.get('value'));
21 this.$textbox.val(this.model.get('value'));
23 }
22 }
24 },
23 },
25
24
26 events: {"keyup textarea" : "handleChanging",
25 events: {"keyup textarea" : "handleChanging",
27 "paste textarea" : "handleChanging",
26 "paste textarea" : "handleChanging",
28 "cut textarea" : "handleChanging"},
27 "cut textarea" : "handleChanging"},
29
28
30 // Handles and validates user input.
29 // Handles and validates user input.
31 handleChanging: function(e) {
30 handleChanging: function(e) {
32 this.user_invoked_update = true;
31 this.user_invoked_update = true;
33 this.model.set('value', e.target.value);
32 this.model.set('value', e.target.value);
34 this.model.apply(this);
33 this.model.apply(this);
35 this.user_invoked_update = false;
34 this.user_invoked_update = false;
36 },
35 },
37 });
36 });
38
37
39 IPython.notebook.widget_manager.register_widget_view('TextareaView', TextareaView);
38 IPython.notebook.widget_manager.register_widget_view('TextareaView', TextareaView);
40
39
41 var TextboxView = IPython.WidgetView.extend({
40 var TextboxView = IPython.WidgetView.extend({
42
41
43 // Called when view is rendered.
42 // Called when view is rendered.
44 render : function(){
43 render : function(){
45 this.$el
44 this.$el
46 .html('')
45 .html('')
47 .addClass(this.model.comm.comm_id);
46 .addClass(this.model.comm.comm_id);
48 this.$textbox = $('<input type="text" />')
47 this.$textbox = $('<input type="text" />')
49 .addClass('input')
48 .addClass('input')
50 .appendTo(this.$el);
49 .appendTo(this.$el);
51 this.update(); // Set defaults.
50 this.update(); // Set defaults.
52 },
51 },
53
52
54 // Handles: Backend -> Frontend Sync
53 // Handles: Backend -> Frontend Sync
55 // Frontent -> Frontend Sync
54 // Frontent -> Frontend Sync
56 update : function(){
55 update : function(){
57 if (!this.user_invoked_update) {
56 if (!this.user_invoked_update) {
58 this.$textbox.val(this.model.get('value'));
57 this.$textbox.val(this.model.get('value'));
59 }
58 }
60 },
59 },
61
60
62 events: {"keyup input" : "handleChanging",
61 events: {"keyup input" : "handleChanging",
63 "paste input" : "handleChanging",
62 "paste input" : "handleChanging",
64 "cut input" : "handleChanging"},
63 "cut input" : "handleChanging"},
65
64
66 // Handles and validates user input.
65 // Handles and validates user input.
67 handleChanging: function(e) {
66 handleChanging: function(e) {
68 this.user_invoked_update = true;
67 this.user_invoked_update = true;
69 this.model.set('value', e.target.value);
68 this.model.set('value', e.target.value);
70 this.model.apply(this);
69 this.model.apply(this);
71 this.user_invoked_update = false;
70 this.user_invoked_update = false;
72 },
71 },
73 });
72 });
74
73
75 IPython.notebook.widget_manager.register_widget_view('TextboxView', TextboxView);
74 IPython.notebook.widget_manager.register_widget_view('TextboxView', TextboxView);
76
77 }); No newline at end of file
@@ -71,6 +71,8 b''
71 <script src="{{static_url("components/jquery/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
71 <script src="{{static_url("components/jquery/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
72 <script src="{{static_url("components/jquery-ui/ui/minified/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
72 <script src="{{static_url("components/jquery-ui/ui/minified/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script>
73 <script src="{{static_url("components/bootstrap/bootstrap/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
73 <script src="{{static_url("components/bootstrap/bootstrap/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
74 <script src="{{static_url("components/underscore/underscore-min.js") }}" type="text/javascript" charset="utf-8"></script>
75 <script src="{{static_url("components/backbone/backbone-min.js") }}" type="text/javascript" charset="utf-8"></script>
74 <script src="{{static_url("base/js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
76 <script src="{{static_url("base/js/namespace.js") }}" type="text/javascript" charset="utf-8"></script>
75 <script src="{{static_url("base/js/page.js") }}" type="text/javascript" charset="utf-8"></script>
77 <script src="{{static_url("base/js/page.js") }}" type="text/javascript" charset="utf-8"></script>
76 <script src="{{static_url("auth/js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
78 <script src="{{static_url("auth/js/loginwidget.js") }}" type="text/javascript" charset="utf-8"></script>
@@ -1,4 +1,4 b''
1 from base import Widget, init_widget_js
1 from base import Widget
2
2
3 from container import ContainerWidget
3 from container import ContainerWidget
4 from float_range import FloatRangeWidget
4 from float_range import FloatRangeWidget
@@ -1,4 +1,3 b''
1
2 from copy import copy
1 from copy import copy
3 from glob import glob
2 from glob import glob
4 import uuid
3 import uuid
@@ -124,6 +123,7 b' class Widget(LoggingConfigurable):'
124 view_name = self.default_view_name
123 view_name = self.default_view_name
125
124
126 # Require traitlet specified widget js
125 # Require traitlet specified widget js
126 self._require_js('static/notebook/js/widget.js')
127 for requirement in self.js_requirements:
127 for requirement in self.js_requirements:
128 self._require_js(requirement)
128 self._require_js(requirement)
129
129
@@ -165,11 +165,4 b' class Widget(LoggingConfigurable):'
165 def _require_js(self, js_path):
165 def _require_js(self, js_path):
166 # Since we are loading requirements that must be loaded before this call
166 # Since we are loading requirements that must be loaded before this call
167 # returns, preform async js load.
167 # returns, preform async js load.
168 display(Javascript(data="""
168 display(Javascript(data='$.ajax({url: "%s", async: false, dataType: "script", timeout: 1000});' % js_path))
169 $.ajax({
170 url: '{0}',
171 async: false,
172 dataType: "script",
173 timeout: 1000, // Wait a maximum of one second for the script to load.
174 });
175 """.format(js_path))) No newline at end of file
@@ -7,6 +7,6 b' from IPython.utils.javascript import display_all_js'
7 class ContainerWidget(Widget):
7 class ContainerWidget(Widget):
8 target_name = Unicode('container_widget')
8 target_name = Unicode('container_widget')
9 default_view_name = Unicode('ContainerView')
9 default_view_name = Unicode('ContainerView')
10 js_requirements = List(["notebook/js/widgets/container.js"])
10 js_requirements = List(["static/notebook/js/widgets/container.js"])
11
11
12 _keys = []
12 _keys = []
@@ -7,7 +7,7 b' from IPython.utils.javascript import display_all_js'
7 class FloatRangeWidget(Widget):
7 class FloatRangeWidget(Widget):
8 target_name = Unicode('FloatRangeWidgetModel')
8 target_name = Unicode('FloatRangeWidgetModel')
9 default_view_name = Unicode('FloatSliderView')
9 default_view_name = Unicode('FloatSliderView')
10 js_requirements = List(["notebook/js/widgets/float_range.js"])
10 js_requirements = List(["static/notebook/js/widgets/float_range.js"])
11 _keys = ['value', 'step', 'max', 'min', 'disabled', 'orientation']
11 _keys = ['value', 'step', 'max', 'min', 'disabled', 'orientation']
12
12
13 value = Float(0.0)
13 value = Float(0.0)
@@ -7,7 +7,7 b' from IPython.utils.javascript import display_all_js'
7 class IntRangeWidget(Widget):
7 class IntRangeWidget(Widget):
8 target_name = Unicode('IntRangeWidgetModel')
8 target_name = Unicode('IntRangeWidgetModel')
9 default_view_name = Unicode('IntSliderView')
9 default_view_name = Unicode('IntSliderView')
10 js_requirements = List(["notebook/js/widgets/int_range.js"])
10 js_requirements = List(["static/notebook/js/widgets/int_range.js"])
11 _keys = ['value', 'step', 'max', 'min', 'disabled', 'orientation']
11 _keys = ['value', 'step', 'max', 'min', 'disabled', 'orientation']
12
12
13 value = Int(0)
13 value = Int(0)
@@ -7,7 +7,7 b' from IPython.utils.javascript import display_all_js'
7 class SelectionWidget(Widget):
7 class SelectionWidget(Widget):
8 target_name = Unicode('SelectionWidgetModel')
8 target_name = Unicode('SelectionWidgetModel')
9 default_view_name = Unicode('DropdownView')
9 default_view_name = Unicode('DropdownView')
10 js_requirements = List(["notebook/js/widgets/selection.js"])
10 js_requirements = List(["static/notebook/js/widgets/selection.js"])
11 _keys = ['value', 'values', 'disabled']
11 _keys = ['value', 'values', 'disabled']
12
12
13 value = Unicode()
13 value = Unicode()
@@ -7,7 +7,7 b' from IPython.utils.javascript import display_all_js'
7 class StringWidget(Widget):
7 class StringWidget(Widget):
8 target_name = Unicode('StringWidgetModel')
8 target_name = Unicode('StringWidgetModel')
9 default_view_name = Unicode('TextboxView')
9 default_view_name = Unicode('TextboxView')
10 js_requirements = List(["notebook/js/widgets/string.js"])
10 js_requirements = List(["static/notebook/js/widgets/string.js"])
11 _keys = ['value', 'row_count', 'disabled']
11 _keys = ['value', 'row_count', 'disabled']
12
12
13 value = Unicode()
13 value = Unicode()
General Comments 0
You need to be logged in to leave comments. Login now