##// END OF EJS Templates
Fix view rendering order.
Jonathan Frederic -
Show More
@@ -1,246 +1,246 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 "underscore",
5 "underscore",
6 "backbone",
6 "backbone",
7 "jquery",
7 "jquery",
8 "base/js/utils",
8 "base/js/utils",
9 "base/js/namespace"
9 "base/js/namespace"
10 ], function (_, Backbone, $, utils, IPython) {
10 ], function (_, Backbone, $, utils, IPython) {
11 "use strict";
11 "use strict";
12 //--------------------------------------------------------------------
12 //--------------------------------------------------------------------
13 // WidgetManager class
13 // WidgetManager class
14 //--------------------------------------------------------------------
14 //--------------------------------------------------------------------
15 var WidgetManager = function (comm_manager, notebook) {
15 var WidgetManager = function (comm_manager, notebook) {
16 // Public constructor
16 // Public constructor
17 WidgetManager._managers.push(this);
17 WidgetManager._managers.push(this);
18
18
19 // Attach a comm manager to the
19 // Attach a comm manager to the
20 this.keyboard_manager = notebook.keyboard_manager;
20 this.keyboard_manager = notebook.keyboard_manager;
21 this.notebook = notebook;
21 this.notebook = notebook;
22 this.comm_manager = comm_manager;
22 this.comm_manager = comm_manager;
23 this._models = {}; /* Dictionary of model ids and model instances */
23 this._models = {}; /* Dictionary of model ids and model instances */
24
24
25 // Register with the comm manager.
25 // Register with the comm manager.
26 this.comm_manager.register_target('ipython.widget', $.proxy(this._handle_comm_open, this));
26 this.comm_manager.register_target('ipython.widget', $.proxy(this._handle_comm_open, this));
27 };
27 };
28
28
29 //--------------------------------------------------------------------
29 //--------------------------------------------------------------------
30 // Class level
30 // Class level
31 //--------------------------------------------------------------------
31 //--------------------------------------------------------------------
32 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
32 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
33 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
33 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
34 WidgetManager._managers = []; /* List of widget managers */
34 WidgetManager._managers = []; /* List of widget managers */
35
35
36 WidgetManager.register_widget_model = function (model_name, model_type) {
36 WidgetManager.register_widget_model = function (model_name, model_type) {
37 // Registers a widget model by name.
37 // Registers a widget model by name.
38 WidgetManager._model_types[model_name] = model_type;
38 WidgetManager._model_types[model_name] = model_type;
39 };
39 };
40
40
41 WidgetManager.register_widget_view = function (view_name, view_type) {
41 WidgetManager.register_widget_view = function (view_name, view_type) {
42 // Registers a widget view by name.
42 // Registers a widget view by name.
43 WidgetManager._view_types[view_name] = view_type;
43 WidgetManager._view_types[view_name] = view_type;
44 };
44 };
45
45
46 //--------------------------------------------------------------------
46 //--------------------------------------------------------------------
47 // Instance level
47 // Instance level
48 //--------------------------------------------------------------------
48 //--------------------------------------------------------------------
49 WidgetManager.prototype.display_view = function(msg, model) {
49 WidgetManager.prototype.display_view = function(msg, model) {
50 // Displays a view for a particular model.
50 // Displays a view for a particular model.
51 var cell = this.get_msg_cell(msg.parent_header.msg_id);
51 var cell = this.get_msg_cell(msg.parent_header.msg_id);
52 if (cell === null) {
52 if (cell === null) {
53 console.log("Could not determine where the display" +
53 console.log("Could not determine where the display" +
54 " message was from. Widget will not be displayed");
54 " message was from. Widget will not be displayed");
55 } else {
55 } else {
56 var dummy = null;
56 var dummy = null;
57 if (cell.widget_subarea) {
57 if (cell.widget_subarea) {
58 dummy = $('<div />');
58 dummy = $('<div />');
59 cell.widget_subarea.append(dummy);
59 cell.widget_subarea.append(dummy);
60 }
60 }
61
61
62 var that = this;
62 var that = this;
63 this.create_view(model, {cell: cell}).then(function(view) {
63 this.create_view(model, {cell: cell}).then(function(view) {
64 that._handle_display_view(view);
64 that._handle_display_view(view);
65 if (dummy) {
65 if (dummy) {
66 dummy.replaceWith(view.$el);
66 dummy.replaceWith(view.$el);
67 }
67 }
68 view.trigger('displayed');
68 view.trigger('displayed');
69 }, function(error) { console.error(error); });
69 }, console.error);
70 }
70 }
71 };
71 };
72
72
73 WidgetManager.prototype._handle_display_view = function (view) {
73 WidgetManager.prototype._handle_display_view = function (view) {
74 // Have the IPython keyboard manager disable its event
74 // Have the IPython keyboard manager disable its event
75 // handling so the widget can capture keyboard input.
75 // handling so the widget can capture keyboard input.
76 // Note, this is only done on the outer most widgets.
76 // Note, this is only done on the outer most widgets.
77 if (this.keyboard_manager) {
77 if (this.keyboard_manager) {
78 this.keyboard_manager.register_events(view.$el);
78 this.keyboard_manager.register_events(view.$el);
79
79
80 if (view.additional_elements) {
80 if (view.additional_elements) {
81 for (var i = 0; i < view.additional_elements.length; i++) {
81 for (var i = 0; i < view.additional_elements.length; i++) {
82 this.keyboard_manager.register_events(view.additional_elements[i]);
82 this.keyboard_manager.register_events(view.additional_elements[i]);
83 }
83 }
84 }
84 }
85 }
85 }
86 };
86 };
87
87
88
88
89 WidgetManager.prototype.create_view = function(model, options) {
89 WidgetManager.prototype.create_view = function(model, options) {
90 // Creates a view for a particular model.
90 // Creates a view for a particular model.
91 return new Promise(function(resolve, reject) {
91 return new Promise(function(resolve, reject) {
92 var view_name = model.get('_view_name');
92 var view_name = model.get('_view_name');
93 var view_module = model.get('_view_module');
93 var view_module = model.get('_view_module');
94 utils.try_load(view_name, view_module, WidgetManager._view_types).then(function(ViewType){
94 utils.try_load(view_name, view_module, WidgetManager._view_types).then(function(ViewType){
95
95
96 // If a view is passed into the method, use that view's cell as
96 // If a view is passed into the method, use that view's cell as
97 // the cell for the view that is created.
97 // the cell for the view that is created.
98 options = options || {};
98 options = options || {};
99 if (options.parent !== undefined) {
99 if (options.parent !== undefined) {
100 options.cell = options.parent.options.cell;
100 options.cell = options.parent.options.cell;
101 }
101 }
102
102
103 // Create and render the view...
103 // Create and render the view...
104 var parameters = {model: model, options: options};
104 var parameters = {model: model, options: options};
105 var view = new ViewType(parameters);
105 var view = new ViewType(parameters);
106 view.render();
106 view.render();
107 model.on('destroy', view.remove, view);
107 model.on('destroy', view.remove, view);
108 resolve(view);
108 resolve(view);
109 }, reject);
109 }, reject);
110 });
110 });
111 };
111 };
112
112
113 WidgetManager.prototype.get_msg_cell = function (msg_id) {
113 WidgetManager.prototype.get_msg_cell = function (msg_id) {
114 var cell = null;
114 var cell = null;
115 // First, check to see if the msg was triggered by cell execution.
115 // First, check to see if the msg was triggered by cell execution.
116 if (this.notebook) {
116 if (this.notebook) {
117 cell = this.notebook.get_msg_cell(msg_id);
117 cell = this.notebook.get_msg_cell(msg_id);
118 }
118 }
119 if (cell !== null) {
119 if (cell !== null) {
120 return cell;
120 return cell;
121 }
121 }
122 // Second, check to see if a get_cell callback was defined
122 // Second, check to see if a get_cell callback was defined
123 // for the message. get_cell callbacks are registered for
123 // for the message. get_cell callbacks are registered for
124 // widget messages, so this block is actually checking to see if the
124 // widget messages, so this block is actually checking to see if the
125 // message was triggered by a widget.
125 // message was triggered by a widget.
126 var kernel = this.comm_manager.kernel;
126 var kernel = this.comm_manager.kernel;
127 if (kernel) {
127 if (kernel) {
128 var callbacks = kernel.get_callbacks_for_msg(msg_id);
128 var callbacks = kernel.get_callbacks_for_msg(msg_id);
129 if (callbacks && callbacks.iopub &&
129 if (callbacks && callbacks.iopub &&
130 callbacks.iopub.get_cell !== undefined) {
130 callbacks.iopub.get_cell !== undefined) {
131 return callbacks.iopub.get_cell();
131 return callbacks.iopub.get_cell();
132 }
132 }
133 }
133 }
134
134
135 // Not triggered by a cell or widget (no get_cell callback
135 // Not triggered by a cell or widget (no get_cell callback
136 // exists).
136 // exists).
137 return null;
137 return null;
138 };
138 };
139
139
140 WidgetManager.prototype.callbacks = function (view) {
140 WidgetManager.prototype.callbacks = function (view) {
141 // callback handlers specific a view
141 // callback handlers specific a view
142 var callbacks = {};
142 var callbacks = {};
143 if (view && view.options.cell) {
143 if (view && view.options.cell) {
144
144
145 // Try to get output handlers
145 // Try to get output handlers
146 var cell = view.options.cell;
146 var cell = view.options.cell;
147 var handle_output = null;
147 var handle_output = null;
148 var handle_clear_output = null;
148 var handle_clear_output = null;
149 if (cell.output_area) {
149 if (cell.output_area) {
150 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
150 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
151 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
151 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
152 }
152 }
153
153
154 // Create callback dictionary using what is known
154 // Create callback dictionary using what is known
155 var that = this;
155 var that = this;
156 callbacks = {
156 callbacks = {
157 iopub : {
157 iopub : {
158 output : handle_output,
158 output : handle_output,
159 clear_output : handle_clear_output,
159 clear_output : handle_clear_output,
160
160
161 // Special function only registered by widget messages.
161 // Special function only registered by widget messages.
162 // Allows us to get the cell for a message so we know
162 // Allows us to get the cell for a message so we know
163 // where to add widgets if the code requires it.
163 // where to add widgets if the code requires it.
164 get_cell : function () {
164 get_cell : function () {
165 return cell;
165 return cell;
166 },
166 },
167 },
167 },
168 };
168 };
169 }
169 }
170 return callbacks;
170 return callbacks;
171 };
171 };
172
172
173 WidgetManager.prototype.get_model = function (model_id) {
173 WidgetManager.prototype.get_model = function (model_id) {
174 // Look-up a model instance by its id.
174 // Look-up a model instance by its id.
175 var model = this._models[model_id];
175 var model = this._models[model_id];
176 if (model !== undefined && model.id == model_id) {
176 if (model !== undefined && model.id == model_id) {
177 return model;
177 return model;
178 }
178 }
179 return null;
179 return null;
180 };
180 };
181
181
182 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
182 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
183 // Handle when a comm is opened.
183 // Handle when a comm is opened.
184 this.create_model({
184 this.create_model({
185 model_name: msg.content.data.model_name,
185 model_name: msg.content.data.model_name,
186 model_module: msg.content.data.model_module,
186 model_module: msg.content.data.model_module,
187 comm: comm});
187 comm: comm});
188 };
188 };
189
189
190 WidgetManager.prototype.create_model = function (options) {
190 WidgetManager.prototype.create_model = function (options) {
191 // Create and return a promise to create a new widget model.
191 // Create and return a promise to create a new widget model.
192 //
192 //
193 // Minimally, one must provide the model_name and widget_class
193 // Minimally, one must provide the model_name and widget_class
194 // parameters to create a model from Javascript.
194 // parameters to create a model from Javascript.
195 //
195 //
196 // Example
196 // Example
197 // --------
197 // --------
198 // JS:
198 // JS:
199 // IPython.notebook.kernel.widget_manager.create_model({
199 // IPython.notebook.kernel.widget_manager.create_model({
200 // model_name: 'WidgetModel',
200 // model_name: 'WidgetModel',
201 // widget_class: 'IPython.html.widgets.widget_int.IntSlider'})
201 // widget_class: 'IPython.html.widgets.widget_int.IntSlider'})
202 // .then(function(model) { console.log('Create success!', model); },
202 // .then(function(model) { console.log('Create success!', model); },
203 // function(error) { console.error(error); });
203 // console.error);
204 //
204 //
205 // Parameters
205 // Parameters
206 // ----------
206 // ----------
207 // options: dictionary
207 // options: dictionary
208 // Dictionary of options with the following contents:
208 // Dictionary of options with the following contents:
209 // model_name: string
209 // model_name: string
210 // Target name of the widget model to create.
210 // Target name of the widget model to create.
211 // model_module: (optional) string
211 // model_module: (optional) string
212 // Module name of the widget model to create.
212 // Module name of the widget model to create.
213 // widget_class: (optional) string
213 // widget_class: (optional) string
214 // Target name of the widget in the back-end.
214 // Target name of the widget in the back-end.
215 // comm: (optional) Comm
215 // comm: (optional) Comm
216 return new Promise(function(resolve, reject) {
216 return new Promise(function(resolve, reject) {
217
217
218 // Get the model type using require or through the registry.
218 // Get the model type using require or through the registry.
219 var widget_type_name = options.model_name;
219 var widget_type_name = options.model_name;
220 var widget_module = options.model_module;
220 var widget_module = options.model_module;
221 var that = this;
221 var that = this;
222 utils.try_load(widget_type_name, widget_module, WidgetManager._model_types)
222 utils.try_load(widget_type_name, widget_module, WidgetManager._model_types)
223 .then(function(ModelType) {
223 .then(function(ModelType) {
224
224
225 // Create a comm if it wasn't provided.
225 // Create a comm if it wasn't provided.
226 var comm = options.comm;
226 var comm = options.comm;
227 if (!comm) {
227 if (!comm) {
228 comm = that.comm_manager.new_comm('ipython.widget', {'widget_class': options.widget_class});
228 comm = that.comm_manager.new_comm('ipython.widget', {'widget_class': options.widget_class});
229 }
229 }
230
230
231 var model_id = comm.comm_id;
231 var model_id = comm.comm_id;
232 var widget_model = new ModelType(that, model_id, comm);
232 var widget_model = new ModelType(that, model_id, comm);
233 widget_model.on('comm:close', function () {
233 widget_model.on('comm:close', function () {
234 delete that._models[model_id];
234 delete that._models[model_id];
235 });
235 });
236 that._models[model_id] = widget_model;
236 that._models[model_id] = widget_model;
237 reolve(widget_model);
237 reolve(widget_model);
238 }, reject);
238 }, reject);
239 });
239 });
240 };
240 };
241
241
242 // Backwards compatibility.
242 // Backwards compatibility.
243 IPython.WidgetManager = WidgetManager;
243 IPython.WidgetManager = WidgetManager;
244
244
245 return {'WidgetManager': WidgetManager};
245 return {'WidgetManager': WidgetManager};
246 });
246 });
@@ -1,622 +1,622 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define(["widgets/js/manager",
4 define(["widgets/js/manager",
5 "underscore",
5 "underscore",
6 "backbone",
6 "backbone",
7 "jquery",
7 "jquery",
8 "base/js/namespace",
8 "base/js/namespace",
9 ], function(widgetmanager, _, Backbone, $, IPython){
9 ], function(widgetmanager, _, Backbone, $, IPython){
10
10
11 var WidgetModel = Backbone.Model.extend({
11 var WidgetModel = Backbone.Model.extend({
12 constructor: function (widget_manager, model_id, comm, init_state_callback) {
12 constructor: function (widget_manager, model_id, comm, init_state_callback) {
13 // Constructor
13 // Constructor
14 //
14 //
15 // Creates a WidgetModel instance.
15 // Creates a WidgetModel instance.
16 //
16 //
17 // Parameters
17 // Parameters
18 // ----------
18 // ----------
19 // widget_manager : WidgetManager instance
19 // widget_manager : WidgetManager instance
20 // model_id : string
20 // model_id : string
21 // An ID unique to this model.
21 // An ID unique to this model.
22 // comm : Comm instance (optional)
22 // comm : Comm instance (optional)
23 // init_state_callback : callback (optional)
23 // init_state_callback : callback (optional)
24 // Called once when the first state message is recieved from
24 // Called once when the first state message is recieved from
25 // the back-end.
25 // the back-end.
26 this.widget_manager = widget_manager;
26 this.widget_manager = widget_manager;
27 this.init_state_callback = init_state_callback;
27 this.init_state_callback = init_state_callback;
28 this._buffered_state_diff = {};
28 this._buffered_state_diff = {};
29 this.pending_msgs = 0;
29 this.pending_msgs = 0;
30 this.msg_buffer = null;
30 this.msg_buffer = null;
31 this.state_lock = null;
31 this.state_lock = null;
32 this.id = model_id;
32 this.id = model_id;
33 this.views = {};
33 this.views = {};
34
34
35 if (comm !== undefined) {
35 if (comm !== undefined) {
36 // Remember comm associated with the model.
36 // Remember comm associated with the model.
37 this.comm = comm;
37 this.comm = comm;
38 comm.model = this;
38 comm.model = this;
39
39
40 // Hook comm messages up to model.
40 // Hook comm messages up to model.
41 comm.on_close($.proxy(this._handle_comm_closed, this));
41 comm.on_close($.proxy(this._handle_comm_closed, this));
42 comm.on_msg($.proxy(this._handle_comm_msg, this));
42 comm.on_msg($.proxy(this._handle_comm_msg, this));
43 }
43 }
44 return Backbone.Model.apply(this);
44 return Backbone.Model.apply(this);
45 },
45 },
46
46
47 send: function (content, callbacks) {
47 send: function (content, callbacks) {
48 // Send a custom msg over the comm.
48 // Send a custom msg over the comm.
49 if (this.comm !== undefined) {
49 if (this.comm !== undefined) {
50 var data = {method: 'custom', content: content};
50 var data = {method: 'custom', content: content};
51 this.comm.send(data, callbacks);
51 this.comm.send(data, callbacks);
52 this.pending_msgs++;
52 this.pending_msgs++;
53 }
53 }
54 },
54 },
55
55
56 _handle_comm_closed: function (msg) {
56 _handle_comm_closed: function (msg) {
57 // Handle when a widget is closed.
57 // Handle when a widget is closed.
58 this.trigger('comm:close');
58 this.trigger('comm:close');
59 this.stopListening();
59 this.stopListening();
60 this.trigger('destroy', this);
60 this.trigger('destroy', this);
61 delete this.comm.model; // Delete ref so GC will collect widget model.
61 delete this.comm.model; // Delete ref so GC will collect widget model.
62 delete this.comm;
62 delete this.comm;
63 delete this.model_id; // Delete id from model so widget manager cleans up.
63 delete this.model_id; // Delete id from model so widget manager cleans up.
64 for (var id in this.views) {
64 for (var id in this.views) {
65 if (this.views.hasOwnProperty(id)) {
65 if (this.views.hasOwnProperty(id)) {
66 this.views[id].remove();
66 this.views[id].remove();
67 }
67 }
68 }
68 }
69 },
69 },
70
70
71 _handle_comm_msg: function (msg) {
71 _handle_comm_msg: function (msg) {
72 // Handle incoming comm msg.
72 // Handle incoming comm msg.
73 var method = msg.content.data.method;
73 var method = msg.content.data.method;
74 switch (method) {
74 switch (method) {
75 case 'update':
75 case 'update':
76 this.set_state(msg.content.data.state);
76 this.set_state(msg.content.data.state);
77 if (this.init_state_callback) {
77 if (this.init_state_callback) {
78 this.init_state_callback.apply(this, [this]);
78 this.init_state_callback.apply(this, [this]);
79 delete this.init_state_callback;
79 delete this.init_state_callback;
80 }
80 }
81 break;
81 break;
82 case 'custom':
82 case 'custom':
83 this.trigger('msg:custom', msg.content.data.content);
83 this.trigger('msg:custom', msg.content.data.content);
84 break;
84 break;
85 case 'display':
85 case 'display':
86 this.widget_manager.display_view(msg, this);
86 this.widget_manager.display_view(msg, this);
87 break;
87 break;
88 }
88 }
89 },
89 },
90
90
91 set_state: function (state) {
91 set_state: function (state) {
92 // Handle when a widget is updated via the python side.
92 // Handle when a widget is updated via the python side.
93 this.state_lock = state;
93 this.state_lock = state;
94 try {
94 try {
95 var that = this;
95 var that = this;
96 WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
96 WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
97 obj[key] = that._unpack_models(state[key]);
97 obj[key] = that._unpack_models(state[key]);
98 return obj;
98 return obj;
99 }, {})]);
99 }, {})]);
100 } finally {
100 } finally {
101 this.state_lock = null;
101 this.state_lock = null;
102 }
102 }
103 },
103 },
104
104
105 _handle_status: function (msg, callbacks) {
105 _handle_status: function (msg, callbacks) {
106 // Handle status msgs.
106 // Handle status msgs.
107
107
108 // execution_state : ('busy', 'idle', 'starting')
108 // execution_state : ('busy', 'idle', 'starting')
109 if (this.comm !== undefined) {
109 if (this.comm !== undefined) {
110 if (msg.content.execution_state ==='idle') {
110 if (msg.content.execution_state ==='idle') {
111 // Send buffer if this message caused another message to be
111 // Send buffer if this message caused another message to be
112 // throttled.
112 // throttled.
113 if (this.msg_buffer !== null &&
113 if (this.msg_buffer !== null &&
114 (this.get('msg_throttle') || 3) === this.pending_msgs) {
114 (this.get('msg_throttle') || 3) === this.pending_msgs) {
115 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
115 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
116 this.comm.send(data, callbacks);
116 this.comm.send(data, callbacks);
117 this.msg_buffer = null;
117 this.msg_buffer = null;
118 } else {
118 } else {
119 --this.pending_msgs;
119 --this.pending_msgs;
120 }
120 }
121 }
121 }
122 }
122 }
123 },
123 },
124
124
125 callbacks: function(view) {
125 callbacks: function(view) {
126 // Create msg callbacks for a comm msg.
126 // Create msg callbacks for a comm msg.
127 var callbacks = this.widget_manager.callbacks(view);
127 var callbacks = this.widget_manager.callbacks(view);
128
128
129 if (callbacks.iopub === undefined) {
129 if (callbacks.iopub === undefined) {
130 callbacks.iopub = {};
130 callbacks.iopub = {};
131 }
131 }
132
132
133 var that = this;
133 var that = this;
134 callbacks.iopub.status = function (msg) {
134 callbacks.iopub.status = function (msg) {
135 that._handle_status(msg, callbacks);
135 that._handle_status(msg, callbacks);
136 };
136 };
137 return callbacks;
137 return callbacks;
138 },
138 },
139
139
140 set: function(key, val, options) {
140 set: function(key, val, options) {
141 // Set a value.
141 // Set a value.
142 var return_value = WidgetModel.__super__.set.apply(this, arguments);
142 var return_value = WidgetModel.__super__.set.apply(this, arguments);
143
143
144 // Backbone only remembers the diff of the most recent set()
144 // Backbone only remembers the diff of the most recent set()
145 // operation. Calling set multiple times in a row results in a
145 // operation. Calling set multiple times in a row results in a
146 // loss of diff information. Here we keep our own running diff.
146 // loss of diff information. Here we keep our own running diff.
147 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
147 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
148 return return_value;
148 return return_value;
149 },
149 },
150
150
151 sync: function (method, model, options) {
151 sync: function (method, model, options) {
152 // Handle sync to the back-end. Called when a model.save() is called.
152 // Handle sync to the back-end. Called when a model.save() is called.
153
153
154 // Make sure a comm exists.
154 // Make sure a comm exists.
155 var error = options.error || function() {
155 var error = options.error || function() {
156 console.error('Backbone sync error:', arguments);
156 console.error('Backbone sync error:', arguments);
157 };
157 };
158 if (this.comm === undefined) {
158 if (this.comm === undefined) {
159 error();
159 error();
160 return false;
160 return false;
161 }
161 }
162
162
163 // Delete any key value pairs that the back-end already knows about.
163 // Delete any key value pairs that the back-end already knows about.
164 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
164 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
165 if (this.state_lock !== null) {
165 if (this.state_lock !== null) {
166 var keys = Object.keys(this.state_lock);
166 var keys = Object.keys(this.state_lock);
167 for (var i=0; i<keys.length; i++) {
167 for (var i=0; i<keys.length; i++) {
168 var key = keys[i];
168 var key = keys[i];
169 if (attrs[key] === this.state_lock[key]) {
169 if (attrs[key] === this.state_lock[key]) {
170 delete attrs[key];
170 delete attrs[key];
171 }
171 }
172 }
172 }
173 }
173 }
174
174
175 // Only sync if there are attributes to send to the back-end.
175 // Only sync if there are attributes to send to the back-end.
176 attrs = this._pack_models(attrs);
176 attrs = this._pack_models(attrs);
177 if (_.size(attrs) > 0) {
177 if (_.size(attrs) > 0) {
178
178
179 // If this message was sent via backbone itself, it will not
179 // If this message was sent via backbone itself, it will not
180 // have any callbacks. It's important that we create callbacks
180 // have any callbacks. It's important that we create callbacks
181 // so we can listen for status messages, etc...
181 // so we can listen for status messages, etc...
182 var callbacks = options.callbacks || this.callbacks();
182 var callbacks = options.callbacks || this.callbacks();
183
183
184 // Check throttle.
184 // Check throttle.
185 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
185 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
186 // The throttle has been exceeded, buffer the current msg so
186 // The throttle has been exceeded, buffer the current msg so
187 // it can be sent once the kernel has finished processing
187 // it can be sent once the kernel has finished processing
188 // some of the existing messages.
188 // some of the existing messages.
189
189
190 // Combine updates if it is a 'patch' sync, otherwise replace updates
190 // Combine updates if it is a 'patch' sync, otherwise replace updates
191 switch (method) {
191 switch (method) {
192 case 'patch':
192 case 'patch':
193 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
193 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
194 break;
194 break;
195 case 'update':
195 case 'update':
196 case 'create':
196 case 'create':
197 this.msg_buffer = attrs;
197 this.msg_buffer = attrs;
198 break;
198 break;
199 default:
199 default:
200 error();
200 error();
201 return false;
201 return false;
202 }
202 }
203 this.msg_buffer_callbacks = callbacks;
203 this.msg_buffer_callbacks = callbacks;
204
204
205 } else {
205 } else {
206 // We haven't exceeded the throttle, send the message like
206 // We haven't exceeded the throttle, send the message like
207 // normal.
207 // normal.
208 var data = {method: 'backbone', sync_data: attrs};
208 var data = {method: 'backbone', sync_data: attrs};
209 this.comm.send(data, callbacks);
209 this.comm.send(data, callbacks);
210 this.pending_msgs++;
210 this.pending_msgs++;
211 }
211 }
212 }
212 }
213 // Since the comm is a one-way communication, assume the message
213 // Since the comm is a one-way communication, assume the message
214 // arrived. Don't call success since we don't have a model back from the server
214 // arrived. Don't call success since we don't have a model back from the server
215 // this means we miss out on the 'sync' event.
215 // this means we miss out on the 'sync' event.
216 this._buffered_state_diff = {};
216 this._buffered_state_diff = {};
217 },
217 },
218
218
219 save_changes: function(callbacks) {
219 save_changes: function(callbacks) {
220 // Push this model's state to the back-end
220 // Push this model's state to the back-end
221 //
221 //
222 // This invokes a Backbone.Sync.
222 // This invokes a Backbone.Sync.
223 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
223 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
224 },
224 },
225
225
226 _pack_models: function(value) {
226 _pack_models: function(value) {
227 // Replace models with model ids recursively.
227 // Replace models with model ids recursively.
228 var that = this;
228 var that = this;
229 var packed;
229 var packed;
230 if (value instanceof Backbone.Model) {
230 if (value instanceof Backbone.Model) {
231 return "IPY_MODEL_" + value.id;
231 return "IPY_MODEL_" + value.id;
232
232
233 } else if ($.isArray(value)) {
233 } else if ($.isArray(value)) {
234 packed = [];
234 packed = [];
235 _.each(value, function(sub_value, key) {
235 _.each(value, function(sub_value, key) {
236 packed.push(that._pack_models(sub_value));
236 packed.push(that._pack_models(sub_value));
237 });
237 });
238 return packed;
238 return packed;
239 } else if (value instanceof Date || value instanceof String) {
239 } else if (value instanceof Date || value instanceof String) {
240 return value;
240 return value;
241 } else if (value instanceof Object) {
241 } else if (value instanceof Object) {
242 packed = {};
242 packed = {};
243 _.each(value, function(sub_value, key) {
243 _.each(value, function(sub_value, key) {
244 packed[key] = that._pack_models(sub_value);
244 packed[key] = that._pack_models(sub_value);
245 });
245 });
246 return packed;
246 return packed;
247
247
248 } else {
248 } else {
249 return value;
249 return value;
250 }
250 }
251 },
251 },
252
252
253 _unpack_models: function(value) {
253 _unpack_models: function(value) {
254 // Replace model ids with models recursively.
254 // Replace model ids with models recursively.
255 var that = this;
255 var that = this;
256 var unpacked;
256 var unpacked;
257 if ($.isArray(value)) {
257 if ($.isArray(value)) {
258 unpacked = [];
258 unpacked = [];
259 _.each(value, function(sub_value, key) {
259 _.each(value, function(sub_value, key) {
260 unpacked.push(that._unpack_models(sub_value));
260 unpacked.push(that._unpack_models(sub_value));
261 });
261 });
262 return unpacked;
262 return unpacked;
263
263
264 } else if (value instanceof Object) {
264 } else if (value instanceof Object) {
265 unpacked = {};
265 unpacked = {};
266 _.each(value, function(sub_value, key) {
266 _.each(value, function(sub_value, key) {
267 unpacked[key] = that._unpack_models(sub_value);
267 unpacked[key] = that._unpack_models(sub_value);
268 });
268 });
269 return unpacked;
269 return unpacked;
270
270
271 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
271 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
272 var model = this.widget_manager.get_model(value.slice(10, value.length));
272 var model = this.widget_manager.get_model(value.slice(10, value.length));
273 if (model) {
273 if (model) {
274 return model;
274 return model;
275 } else {
275 } else {
276 return value;
276 return value;
277 }
277 }
278 } else {
278 } else {
279 return value;
279 return value;
280 }
280 }
281 },
281 },
282
282
283 on_some_change: function(keys, callback, context) {
283 on_some_change: function(keys, callback, context) {
284 // on_some_change(["key1", "key2"], foo, context) differs from
284 // on_some_change(["key1", "key2"], foo, context) differs from
285 // on("change:key1 change:key2", foo, context).
285 // on("change:key1 change:key2", foo, context).
286 // If the widget attributes key1 and key2 are both modified,
286 // If the widget attributes key1 and key2 are both modified,
287 // the second form will result in foo being called twice
287 // the second form will result in foo being called twice
288 // while the first will call foo only once.
288 // while the first will call foo only once.
289 this.on('change', function() {
289 this.on('change', function() {
290 if (keys.some(this.hasChanged, this)) {
290 if (keys.some(this.hasChanged, this)) {
291 callback.apply(context);
291 callback.apply(context);
292 }
292 }
293 }, this);
293 }, this);
294
294
295 },
295 },
296 });
296 });
297 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
297 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
298
298
299
299
300 var WidgetView = Backbone.View.extend({
300 var WidgetView = Backbone.View.extend({
301 initialize: function(parameters) {
301 initialize: function(parameters) {
302 // Public constructor.
302 // Public constructor.
303 this.model.on('change',this.update,this);
303 this.model.on('change',this.update,this);
304 this.options = parameters.options;
304 this.options = parameters.options;
305 this.child_model_views = {};
305 this.child_model_views = {};
306 this.child_views = {};
306 this.child_views = {};
307 this.id = this.id || IPython.utils.uuid();
307 this.id = this.id || IPython.utils.uuid();
308 this.model.views[this.id] = this;
308 this.model.views[this.id] = this;
309 this.on('displayed', function() {
309 this.on('displayed', function() {
310 this.is_displayed = true;
310 this.is_displayed = true;
311 }, this);
311 }, this);
312 },
312 },
313
313
314 update: function(){
314 update: function(){
315 // Triggered on model change.
315 // Triggered on model change.
316 //
316 //
317 // Update view to be consistent with this.model
317 // Update view to be consistent with this.model
318 },
318 },
319
319
320 create_child_view: function(child_model, options) {
320 create_child_view: function(child_model, options) {
321 // Create and return a child view.
321 // Create and return a child view.
322 //
322 //
323 // -given a model and (optionally) a view name if the view name is
323 // -given a model and (optionally) a view name if the view name is
324 // not given, it defaults to the model's default view attribute.
324 // not given, it defaults to the model's default view attribute.
325
325 return new Promise(function(resolve, reject) {
326 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
326 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
327 // it would be great to have the widget manager add the cell metadata
327 // it would be great to have the widget manager add the cell metadata
328 // to the subview without having to add it here.
328 // to the subview without having to add it here.
329 var that = this;
329 var that = this;
330 var old_callback = options.callback || function(view) {};
330 options = $.extend({ parent: this }, options || {});
331 options = $.extend({ parent: this }, options || {});
331
332
332 this.model.widget_manager.create_view(child_model, options).then(function(child_view) {
333 this.model.widget_manager.create_view(child_model, options).then(function(child_view) {
333 // Associate the view id with the model id.
334 // Associate the view id with the model id.
334 if (that.child_model_views[child_model.id] === undefined) {
335 if (that.child_model_views[child_model.id] === undefined) {
335 that.child_model_views[child_model.id] = [];
336 that.child_model_views[child_model.id] = [];
336 }
337 }
337 that.child_model_views[child_model.id].push(child_view.id);
338 that.child_model_views[child_model.id].push(child_view.id);
339
338
340 // Remember the view by id.
339 // Remember the view by id.
341 that.child_views[child_view.id] = child_view;
340 that.child_views[child_view.id] = child_view;
342 old_callback(child_view);
341 resolve(child_view);
343 }, function(error) { console.error(error); });
342 }, reject);
343 });
344 },
344 },
345
345
346 pop_child_view: function(child_model) {
346 pop_child_view: function(child_model) {
347 // Delete a child view that was previously created using create_child_view.
347 // Delete a child view that was previously created using create_child_view.
348 var view_ids = this.child_model_views[child_model.id];
348 var view_ids = this.child_model_views[child_model.id];
349 if (view_ids !== undefined) {
349 if (view_ids !== undefined) {
350
350
351 // Only delete the first view in the list.
351 // Only delete the first view in the list.
352 var view_id = view_ids[0];
352 var view_id = view_ids[0];
353 var view = this.child_views[view_id];
353 var view = this.child_views[view_id];
354 delete this.child_views[view_id];
354 delete this.child_views[view_id];
355 view_ids.splice(0,1);
355 view_ids.splice(0,1);
356 delete child_model.views[view_id];
356 delete child_model.views[view_id];
357
357
358 // Remove the view list specific to this model if it is empty.
358 // Remove the view list specific to this model if it is empty.
359 if (view_ids.length === 0) {
359 if (view_ids.length === 0) {
360 delete this.child_model_views[child_model.id];
360 delete this.child_model_views[child_model.id];
361 }
361 }
362 return view;
362 return view;
363 }
363 }
364 return null;
364 return null;
365 },
365 },
366
366
367 do_diff: function(old_list, new_list, removed_callback, added_callback) {
367 do_diff: function(old_list, new_list, removed_callback, added_callback) {
368 // Difference a changed list and call remove and add callbacks for
368 // Difference a changed list and call remove and add callbacks for
369 // each removed and added item in the new list.
369 // each removed and added item in the new list.
370 //
370 //
371 // Parameters
371 // Parameters
372 // ----------
372 // ----------
373 // old_list : array
373 // old_list : array
374 // new_list : array
374 // new_list : array
375 // removed_callback : Callback(item)
375 // removed_callback : Callback(item)
376 // Callback that is called for each item removed.
376 // Callback that is called for each item removed.
377 // added_callback : Callback(item)
377 // added_callback : Callback(item)
378 // Callback that is called for each item added.
378 // Callback that is called for each item added.
379
379
380 // Walk the lists until an unequal entry is found.
380 // Walk the lists until an unequal entry is found.
381 var i;
381 var i;
382 for (i = 0; i < new_list.length; i++) {
382 for (i = 0; i < new_list.length; i++) {
383 if (i >= old_list.length || new_list[i] !== old_list[i]) {
383 if (i >= old_list.length || new_list[i] !== old_list[i]) {
384 break;
384 break;
385 }
385 }
386 }
386 }
387
387
388 // Remove the non-matching items from the old list.
388 // Remove the non-matching items from the old list.
389 for (var j = i; j < old_list.length; j++) {
389 for (var j = i; j < old_list.length; j++) {
390 removed_callback(old_list[j]);
390 removed_callback(old_list[j]);
391 }
391 }
392
392
393 // Add the rest of the new list items.
393 // Add the rest of the new list items.
394 for (; i < new_list.length; i++) {
394 for (; i < new_list.length; i++) {
395 added_callback(new_list[i]);
395 added_callback(new_list[i]);
396 }
396 }
397 },
397 },
398
398
399 callbacks: function(){
399 callbacks: function(){
400 // Create msg callbacks for a comm msg.
400 // Create msg callbacks for a comm msg.
401 return this.model.callbacks(this);
401 return this.model.callbacks(this);
402 },
402 },
403
403
404 render: function(){
404 render: function(){
405 // Render the view.
405 // Render the view.
406 //
406 //
407 // By default, this is only called the first time the view is created
407 // By default, this is only called the first time the view is created
408 },
408 },
409
409
410 show: function(){
410 show: function(){
411 // Show the widget-area
411 // Show the widget-area
412 if (this.options && this.options.cell &&
412 if (this.options && this.options.cell &&
413 this.options.cell.widget_area !== undefined) {
413 this.options.cell.widget_area !== undefined) {
414 this.options.cell.widget_area.show();
414 this.options.cell.widget_area.show();
415 }
415 }
416 },
416 },
417
417
418 send: function (content) {
418 send: function (content) {
419 // Send a custom msg associated with this view.
419 // Send a custom msg associated with this view.
420 this.model.send(content, this.callbacks());
420 this.model.send(content, this.callbacks());
421 },
421 },
422
422
423 touch: function () {
423 touch: function () {
424 this.model.save_changes(this.callbacks());
424 this.model.save_changes(this.callbacks());
425 },
425 },
426
426
427 after_displayed: function (callback, context) {
427 after_displayed: function (callback, context) {
428 // Calls the callback right away is the view is already displayed
428 // Calls the callback right away is the view is already displayed
429 // otherwise, register the callback to the 'displayed' event.
429 // otherwise, register the callback to the 'displayed' event.
430 if (this.is_displayed) {
430 if (this.is_displayed) {
431 callback.apply(context);
431 callback.apply(context);
432 } else {
432 } else {
433 this.on('displayed', callback, context);
433 this.on('displayed', callback, context);
434 }
434 }
435 },
435 },
436 });
436 });
437
437
438
438
439 var DOMWidgetView = WidgetView.extend({
439 var DOMWidgetView = WidgetView.extend({
440 initialize: function (parameters) {
440 initialize: function (parameters) {
441 // Public constructor
441 // Public constructor
442 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
442 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
443 this.on('displayed', this.show, this);
443 this.on('displayed', this.show, this);
444 this.model.on('change:visible', this.update_visible, this);
444 this.model.on('change:visible', this.update_visible, this);
445 this.model.on('change:_css', this.update_css, this);
445 this.model.on('change:_css', this.update_css, this);
446
446
447 this.model.on('change:_dom_classes', function(model, new_classes) {
447 this.model.on('change:_dom_classes', function(model, new_classes) {
448 var old_classes = model.previous('_dom_classes');
448 var old_classes = model.previous('_dom_classes');
449 this.update_classes(old_classes, new_classes);
449 this.update_classes(old_classes, new_classes);
450 }, this);
450 }, this);
451
451
452 this.model.on('change:color', function (model, value) {
452 this.model.on('change:color', function (model, value) {
453 this.update_attr('color', value); }, this);
453 this.update_attr('color', value); }, this);
454
454
455 this.model.on('change:background_color', function (model, value) {
455 this.model.on('change:background_color', function (model, value) {
456 this.update_attr('background', value); }, this);
456 this.update_attr('background', value); }, this);
457
457
458 this.model.on('change:width', function (model, value) {
458 this.model.on('change:width', function (model, value) {
459 this.update_attr('width', value); }, this);
459 this.update_attr('width', value); }, this);
460
460
461 this.model.on('change:height', function (model, value) {
461 this.model.on('change:height', function (model, value) {
462 this.update_attr('height', value); }, this);
462 this.update_attr('height', value); }, this);
463
463
464 this.model.on('change:border_color', function (model, value) {
464 this.model.on('change:border_color', function (model, value) {
465 this.update_attr('border-color', value); }, this);
465 this.update_attr('border-color', value); }, this);
466
466
467 this.model.on('change:border_width', function (model, value) {
467 this.model.on('change:border_width', function (model, value) {
468 this.update_attr('border-width', value); }, this);
468 this.update_attr('border-width', value); }, this);
469
469
470 this.model.on('change:border_style', function (model, value) {
470 this.model.on('change:border_style', function (model, value) {
471 this.update_attr('border-style', value); }, this);
471 this.update_attr('border-style', value); }, this);
472
472
473 this.model.on('change:font_style', function (model, value) {
473 this.model.on('change:font_style', function (model, value) {
474 this.update_attr('font-style', value); }, this);
474 this.update_attr('font-style', value); }, this);
475
475
476 this.model.on('change:font_weight', function (model, value) {
476 this.model.on('change:font_weight', function (model, value) {
477 this.update_attr('font-weight', value); }, this);
477 this.update_attr('font-weight', value); }, this);
478
478
479 this.model.on('change:font_size', function (model, value) {
479 this.model.on('change:font_size', function (model, value) {
480 this.update_attr('font-size', this._default_px(value)); }, this);
480 this.update_attr('font-size', this._default_px(value)); }, this);
481
481
482 this.model.on('change:font_family', function (model, value) {
482 this.model.on('change:font_family', function (model, value) {
483 this.update_attr('font-family', value); }, this);
483 this.update_attr('font-family', value); }, this);
484
484
485 this.model.on('change:padding', function (model, value) {
485 this.model.on('change:padding', function (model, value) {
486 this.update_attr('padding', value); }, this);
486 this.update_attr('padding', value); }, this);
487
487
488 this.model.on('change:margin', function (model, value) {
488 this.model.on('change:margin', function (model, value) {
489 this.update_attr('margin', this._default_px(value)); }, this);
489 this.update_attr('margin', this._default_px(value)); }, this);
490
490
491 this.model.on('change:border_radius', function (model, value) {
491 this.model.on('change:border_radius', function (model, value) {
492 this.update_attr('border-radius', this._default_px(value)); }, this);
492 this.update_attr('border-radius', this._default_px(value)); }, this);
493
493
494 this.after_displayed(function() {
494 this.after_displayed(function() {
495 this.update_visible(this.model, this.model.get("visible"));
495 this.update_visible(this.model, this.model.get("visible"));
496 this.update_classes([], this.model.get('_dom_classes'));
496 this.update_classes([], this.model.get('_dom_classes'));
497
497
498 this.update_attr('color', this.model.get('color'));
498 this.update_attr('color', this.model.get('color'));
499 this.update_attr('background', this.model.get('background_color'));
499 this.update_attr('background', this.model.get('background_color'));
500 this.update_attr('width', this.model.get('width'));
500 this.update_attr('width', this.model.get('width'));
501 this.update_attr('height', this.model.get('height'));
501 this.update_attr('height', this.model.get('height'));
502 this.update_attr('border-color', this.model.get('border_color'));
502 this.update_attr('border-color', this.model.get('border_color'));
503 this.update_attr('border-width', this.model.get('border_width'));
503 this.update_attr('border-width', this.model.get('border_width'));
504 this.update_attr('border-style', this.model.get('border_style'));
504 this.update_attr('border-style', this.model.get('border_style'));
505 this.update_attr('font-style', this.model.get('font_style'));
505 this.update_attr('font-style', this.model.get('font_style'));
506 this.update_attr('font-weight', this.model.get('font_weight'));
506 this.update_attr('font-weight', this.model.get('font_weight'));
507 this.update_attr('font-size', this.model.get('font_size'));
507 this.update_attr('font-size', this.model.get('font_size'));
508 this.update_attr('font-family', this.model.get('font_family'));
508 this.update_attr('font-family', this.model.get('font_family'));
509 this.update_attr('padding', this.model.get('padding'));
509 this.update_attr('padding', this.model.get('padding'));
510 this.update_attr('margin', this.model.get('margin'));
510 this.update_attr('margin', this.model.get('margin'));
511 this.update_attr('border-radius', this.model.get('border_radius'));
511 this.update_attr('border-radius', this.model.get('border_radius'));
512
512
513 this.update_css(this.model, this.model.get("_css"));
513 this.update_css(this.model, this.model.get("_css"));
514 }, this);
514 }, this);
515 },
515 },
516
516
517 _default_px: function(value) {
517 _default_px: function(value) {
518 // Makes browser interpret a numerical string as a pixel value.
518 // Makes browser interpret a numerical string as a pixel value.
519 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
519 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
520 return value.trim() + 'px';
520 return value.trim() + 'px';
521 }
521 }
522 return value;
522 return value;
523 },
523 },
524
524
525 update_attr: function(name, value) {
525 update_attr: function(name, value) {
526 // Set a css attr of the widget view.
526 // Set a css attr of the widget view.
527 this.$el.css(name, value);
527 this.$el.css(name, value);
528 },
528 },
529
529
530 update_visible: function(model, value) {
530 update_visible: function(model, value) {
531 // Update visibility
531 // Update visibility
532 this.$el.toggle(value);
532 this.$el.toggle(value);
533 },
533 },
534
534
535 update_css: function (model, css) {
535 update_css: function (model, css) {
536 // Update the css styling of this view.
536 // Update the css styling of this view.
537 var e = this.$el;
537 var e = this.$el;
538 if (css === undefined) {return;}
538 if (css === undefined) {return;}
539 for (var i = 0; i < css.length; i++) {
539 for (var i = 0; i < css.length; i++) {
540 // Apply the css traits to all elements that match the selector.
540 // Apply the css traits to all elements that match the selector.
541 var selector = css[i][0];
541 var selector = css[i][0];
542 var elements = this._get_selector_element(selector);
542 var elements = this._get_selector_element(selector);
543 if (elements.length > 0) {
543 if (elements.length > 0) {
544 var trait_key = css[i][1];
544 var trait_key = css[i][1];
545 var trait_value = css[i][2];
545 var trait_value = css[i][2];
546 elements.css(trait_key ,trait_value);
546 elements.css(trait_key ,trait_value);
547 }
547 }
548 }
548 }
549 },
549 },
550
550
551 update_classes: function (old_classes, new_classes, $el) {
551 update_classes: function (old_classes, new_classes, $el) {
552 // Update the DOM classes applied to an element, default to this.$el.
552 // Update the DOM classes applied to an element, default to this.$el.
553 if ($el===undefined) {
553 if ($el===undefined) {
554 $el = this.$el;
554 $el = this.$el;
555 }
555 }
556 this.do_diff(old_classes, new_classes, function(removed) {
556 this.do_diff(old_classes, new_classes, function(removed) {
557 $el.removeClass(removed);
557 $el.removeClass(removed);
558 }, function(added) {
558 }, function(added) {
559 $el.addClass(added);
559 $el.addClass(added);
560 });
560 });
561 },
561 },
562
562
563 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
563 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
564 // Update the DOM classes applied to the widget based on a single
564 // Update the DOM classes applied to the widget based on a single
565 // trait's value.
565 // trait's value.
566 //
566 //
567 // Given a trait value classes map, this function automatically
567 // Given a trait value classes map, this function automatically
568 // handles applying the appropriate classes to the widget element
568 // handles applying the appropriate classes to the widget element
569 // and removing classes that are no longer valid.
569 // and removing classes that are no longer valid.
570 //
570 //
571 // Parameters
571 // Parameters
572 // ----------
572 // ----------
573 // class_map: dictionary
573 // class_map: dictionary
574 // Dictionary of trait values to class lists.
574 // Dictionary of trait values to class lists.
575 // Example:
575 // Example:
576 // {
576 // {
577 // success: ['alert', 'alert-success'],
577 // success: ['alert', 'alert-success'],
578 // info: ['alert', 'alert-info'],
578 // info: ['alert', 'alert-info'],
579 // warning: ['alert', 'alert-warning'],
579 // warning: ['alert', 'alert-warning'],
580 // danger: ['alert', 'alert-danger']
580 // danger: ['alert', 'alert-danger']
581 // };
581 // };
582 // trait_name: string
582 // trait_name: string
583 // Name of the trait to check the value of.
583 // Name of the trait to check the value of.
584 // previous_trait_value: optional string, default ''
584 // previous_trait_value: optional string, default ''
585 // Last trait value
585 // Last trait value
586 // $el: optional jQuery element handle, defaults to this.$el
586 // $el: optional jQuery element handle, defaults to this.$el
587 // Element that the classes are applied to.
587 // Element that the classes are applied to.
588 var key = previous_trait_value;
588 var key = previous_trait_value;
589 if (key === undefined) {
589 if (key === undefined) {
590 key = this.model.previous(trait_name);
590 key = this.model.previous(trait_name);
591 }
591 }
592 var old_classes = class_map[key] ? class_map[key] : [];
592 var old_classes = class_map[key] ? class_map[key] : [];
593 key = this.model.get(trait_name);
593 key = this.model.get(trait_name);
594 var new_classes = class_map[key] ? class_map[key] : [];
594 var new_classes = class_map[key] ? class_map[key] : [];
595
595
596 this.update_classes(old_classes, new_classes, $el || this.$el);
596 this.update_classes(old_classes, new_classes, $el || this.$el);
597 },
597 },
598
598
599 _get_selector_element: function (selector) {
599 _get_selector_element: function (selector) {
600 // Get the elements via the css selector.
600 // Get the elements via the css selector.
601 var elements;
601 var elements;
602 if (!selector) {
602 if (!selector) {
603 elements = this.$el;
603 elements = this.$el;
604 } else {
604 } else {
605 elements = this.$el.find(selector).addBack(selector);
605 elements = this.$el.find(selector).addBack(selector);
606 }
606 }
607 return elements;
607 return elements;
608 },
608 },
609 });
609 });
610
610
611
611
612 var widget = {
612 var widget = {
613 'WidgetModel': WidgetModel,
613 'WidgetModel': WidgetModel,
614 'WidgetView': WidgetView,
614 'WidgetView': WidgetView,
615 'DOMWidgetView': DOMWidgetView,
615 'DOMWidgetView': DOMWidgetView,
616 };
616 };
617
617
618 // For backwards compatability.
618 // For backwards compatability.
619 $.extend(IPython, widget);
619 $.extend(IPython, widget);
620
620
621 return widget;
621 return widget;
622 });
622 });
@@ -1,346 +1,348 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 "widgets/js/widget",
5 "widgets/js/widget",
6 "jqueryui",
6 "jqueryui",
7 "bootstrap",
7 "bootstrap",
8 ], function(widget, $){
8 ], function(widget, $){
9
9
10 var BoxView = widget.DOMWidgetView.extend({
10 var BoxView = widget.DOMWidgetView.extend({
11 initialize: function(){
11 initialize: function(){
12 // Public constructor
12 // Public constructor
13 BoxView.__super__.initialize.apply(this, arguments);
13 BoxView.__super__.initialize.apply(this, arguments);
14 this.model.on('change:children', function(model, value) {
14 this.model.on('change:children', function(model, value) {
15 this.update_children(model.previous('children'), value);
15 this.update_children(model.previous('children'), value);
16 }, this);
16 }, this);
17 this.model.on('change:overflow_x', function(model, value) {
17 this.model.on('change:overflow_x', function(model, value) {
18 this.update_overflow_x();
18 this.update_overflow_x();
19 }, this);
19 }, this);
20 this.model.on('change:overflow_y', function(model, value) {
20 this.model.on('change:overflow_y', function(model, value) {
21 this.update_overflow_y();
21 this.update_overflow_y();
22 }, this);
22 }, this);
23 this.model.on('change:box_style', function(model, value) {
23 this.model.on('change:box_style', function(model, value) {
24 this.update_box_style();
24 this.update_box_style();
25 }, this);
25 }, this);
26 },
26 },
27
27
28 update_attr: function(name, value) {
28 update_attr: function(name, value) {
29 // Set a css attr of the widget view.
29 // Set a css attr of the widget view.
30 this.$box.css(name, value);
30 this.$box.css(name, value);
31 },
31 },
32
32
33 render: function(){
33 render: function(){
34 // Called when view is rendered.
34 // Called when view is rendered.
35 this.$box = this.$el;
35 this.$box = this.$el;
36 this.$box.addClass('widget-box');
36 this.$box.addClass('widget-box');
37 this.update_children([], this.model.get('children'));
37 this.update_children([], this.model.get('children'));
38 this.update_overflow_x();
38 this.update_overflow_x();
39 this.update_overflow_y();
39 this.update_overflow_y();
40 this.update_box_style('');
40 this.update_box_style('');
41 },
41 },
42
42
43 update_overflow_x: function() {
43 update_overflow_x: function() {
44 // Called when the x-axis overflow setting is changed.
44 // Called when the x-axis overflow setting is changed.
45 this.$box.css('overflow-x', this.model.get('overflow_x'));
45 this.$box.css('overflow-x', this.model.get('overflow_x'));
46 },
46 },
47
47
48 update_overflow_y: function() {
48 update_overflow_y: function() {
49 // Called when the y-axis overflow setting is changed.
49 // Called when the y-axis overflow setting is changed.
50 this.$box.css('overflow-y', this.model.get('overflow_y'));
50 this.$box.css('overflow-y', this.model.get('overflow_y'));
51 },
51 },
52
52
53 update_box_style: function(previous_trait_value) {
53 update_box_style: function(previous_trait_value) {
54 var class_map = {
54 var class_map = {
55 success: ['alert', 'alert-success'],
55 success: ['alert', 'alert-success'],
56 info: ['alert', 'alert-info'],
56 info: ['alert', 'alert-info'],
57 warning: ['alert', 'alert-warning'],
57 warning: ['alert', 'alert-warning'],
58 danger: ['alert', 'alert-danger']
58 danger: ['alert', 'alert-danger']
59 };
59 };
60 this.update_mapped_classes(class_map, 'box_style', previous_trait_value, this.$box);
60 this.update_mapped_classes(class_map, 'box_style', previous_trait_value, this.$box);
61 },
61 },
62
62
63 update_children: function(old_list, new_list) {
63 update_children: function(old_list, new_list) {
64 // Called when the children list changes.
64 // Called when the children list changes.
65 this.do_diff(old_list, new_list,
65 this.do_diff(old_list, new_list,
66 $.proxy(this.remove_child_model, this),
66 $.proxy(this.remove_child_model, this),
67 $.proxy(this.add_child_model, this));
67 $.proxy(this.add_child_model, this));
68 },
68 },
69
69
70 remove_child_model: function(model) {
70 remove_child_model: function(model) {
71 // Called when a model is removed from the children list.
71 // Called when a model is removed from the children list.
72 this.pop_child_view(model).remove();
72 this.pop_child_view(model).remove();
73 },
73 },
74
74
75 add_child_model: function(model) {
75 add_child_model: function(model) {
76 // Called when a model is added to the children list.
76 // Called when a model is added to the children list.
77 var that = this;
77 var that = this;
78 this.create_child_view(model, {callback: function(view) {
78 var dummy = $('<div/>');
79 that.$box.append(view.$el);
79 that.$box.append(dummy);
80 this.create_child_view(model).then(function(view) {
81 dummy.replaceWith(view.$el);
80
82
81 // Trigger the displayed event of the child view.
83 // Trigger the displayed event of the child view.
82 that.after_displayed(function() {
84 that.after_displayed(function() {
83 view.trigger('displayed');
85 view.trigger('displayed');
84 });
86 });
85 }});
87 }, console.error);
86 },
88 },
87 });
89 });
88
90
89
91
90 var FlexBoxView = BoxView.extend({
92 var FlexBoxView = BoxView.extend({
91 render: function(){
93 render: function(){
92 FlexBoxView.__super__.render.apply(this);
94 FlexBoxView.__super__.render.apply(this);
93 this.model.on('change:orientation', this.update_orientation, this);
95 this.model.on('change:orientation', this.update_orientation, this);
94 this.model.on('change:flex', this._flex_changed, this);
96 this.model.on('change:flex', this._flex_changed, this);
95 this.model.on('change:pack', this._pack_changed, this);
97 this.model.on('change:pack', this._pack_changed, this);
96 this.model.on('change:align', this._align_changed, this);
98 this.model.on('change:align', this._align_changed, this);
97 this._flex_changed();
99 this._flex_changed();
98 this._pack_changed();
100 this._pack_changed();
99 this._align_changed();
101 this._align_changed();
100 this.update_orientation();
102 this.update_orientation();
101 },
103 },
102
104
103 update_orientation: function(){
105 update_orientation: function(){
104 var orientation = this.model.get("orientation");
106 var orientation = this.model.get("orientation");
105 if (orientation == "vertical") {
107 if (orientation == "vertical") {
106 this.$box.removeClass("hbox").addClass("vbox");
108 this.$box.removeClass("hbox").addClass("vbox");
107 } else {
109 } else {
108 this.$box.removeClass("vbox").addClass("hbox");
110 this.$box.removeClass("vbox").addClass("hbox");
109 }
111 }
110 },
112 },
111
113
112 _flex_changed: function(){
114 _flex_changed: function(){
113 if (this.model.previous('flex')) {
115 if (this.model.previous('flex')) {
114 this.$box.removeClass('box-flex' + this.model.previous('flex'));
116 this.$box.removeClass('box-flex' + this.model.previous('flex'));
115 }
117 }
116 this.$box.addClass('box-flex' + this.model.get('flex'));
118 this.$box.addClass('box-flex' + this.model.get('flex'));
117 },
119 },
118
120
119 _pack_changed: function(){
121 _pack_changed: function(){
120 if (this.model.previous('pack')) {
122 if (this.model.previous('pack')) {
121 this.$box.removeClass(this.model.previous('pack'));
123 this.$box.removeClass(this.model.previous('pack'));
122 }
124 }
123 this.$box.addClass(this.model.get('pack'));
125 this.$box.addClass(this.model.get('pack'));
124 },
126 },
125
127
126 _align_changed: function(){
128 _align_changed: function(){
127 if (this.model.previous('align')) {
129 if (this.model.previous('align')) {
128 this.$box.removeClass('align-' + this.model.previous('align'));
130 this.$box.removeClass('align-' + this.model.previous('align'));
129 }
131 }
130 this.$box.addClass('align-' + this.model.get('align'));
132 this.$box.addClass('align-' + this.model.get('align'));
131 },
133 },
132 });
134 });
133
135
134 var PopupView = BoxView.extend({
136 var PopupView = BoxView.extend({
135
137
136 render: function(){
138 render: function(){
137 // Called when view is rendered.
139 // Called when view is rendered.
138 var that = this;
140 var that = this;
139
141
140 this.$el.on("remove", function(){
142 this.$el.on("remove", function(){
141 that.$backdrop.remove();
143 that.$backdrop.remove();
142 });
144 });
143 this.$backdrop = $('<div />')
145 this.$backdrop = $('<div />')
144 .appendTo($('#notebook-container'))
146 .appendTo($('#notebook-container'))
145 .addClass('modal-dialog')
147 .addClass('modal-dialog')
146 .css('position', 'absolute')
148 .css('position', 'absolute')
147 .css('left', '0px')
149 .css('left', '0px')
148 .css('top', '0px');
150 .css('top', '0px');
149 this.$window = $('<div />')
151 this.$window = $('<div />')
150 .appendTo(this.$backdrop)
152 .appendTo(this.$backdrop)
151 .addClass('modal-content widget-modal')
153 .addClass('modal-content widget-modal')
152 .mousedown(function(){
154 .mousedown(function(){
153 that.bring_to_front();
155 that.bring_to_front();
154 });
156 });
155
157
156 // Set the elements array since the this.$window element is not child
158 // Set the elements array since the this.$window element is not child
157 // of this.$el and the parent widget manager or other widgets may
159 // of this.$el and the parent widget manager or other widgets may
158 // need to know about all of the top-level widgets. The IPython
160 // need to know about all of the top-level widgets. The IPython
159 // widget manager uses this to register the elements with the
161 // widget manager uses this to register the elements with the
160 // keyboard manager.
162 // keyboard manager.
161 this.additional_elements = [this.$window];
163 this.additional_elements = [this.$window];
162
164
163 this.$title_bar = $('<div />')
165 this.$title_bar = $('<div />')
164 .addClass('popover-title')
166 .addClass('popover-title')
165 .appendTo(this.$window)
167 .appendTo(this.$window)
166 .mousedown(function(){
168 .mousedown(function(){
167 that.bring_to_front();
169 that.bring_to_front();
168 });
170 });
169 this.$close = $('<button />')
171 this.$close = $('<button />')
170 .addClass('close fa fa-remove')
172 .addClass('close fa fa-remove')
171 .css('margin-left', '5px')
173 .css('margin-left', '5px')
172 .appendTo(this.$title_bar)
174 .appendTo(this.$title_bar)
173 .click(function(){
175 .click(function(){
174 that.hide();
176 that.hide();
175 event.stopPropagation();
177 event.stopPropagation();
176 });
178 });
177 this.$minimize = $('<button />')
179 this.$minimize = $('<button />')
178 .addClass('close fa fa-arrow-down')
180 .addClass('close fa fa-arrow-down')
179 .appendTo(this.$title_bar)
181 .appendTo(this.$title_bar)
180 .click(function(){
182 .click(function(){
181 that.popped_out = !that.popped_out;
183 that.popped_out = !that.popped_out;
182 if (!that.popped_out) {
184 if (!that.popped_out) {
183 that.$minimize
185 that.$minimize
184 .removeClass('fa-arrow-down')
186 .removeClass('fa-arrow-down')
185 .addClass('fa-arrow-up');
187 .addClass('fa-arrow-up');
186
188
187 that.$window
189 that.$window
188 .draggable('destroy')
190 .draggable('destroy')
189 .resizable('destroy')
191 .resizable('destroy')
190 .removeClass('widget-modal modal-content')
192 .removeClass('widget-modal modal-content')
191 .addClass('docked-widget-modal')
193 .addClass('docked-widget-modal')
192 .detach()
194 .detach()
193 .insertBefore(that.$show_button);
195 .insertBefore(that.$show_button);
194 that.$show_button.hide();
196 that.$show_button.hide();
195 that.$close.hide();
197 that.$close.hide();
196 } else {
198 } else {
197 that.$minimize
199 that.$minimize
198 .addClass('fa-arrow-down')
200 .addClass('fa-arrow-down')
199 .removeClass('fa-arrow-up');
201 .removeClass('fa-arrow-up');
200
202
201 that.$window
203 that.$window
202 .removeClass('docked-widget-modal')
204 .removeClass('docked-widget-modal')
203 .addClass('widget-modal modal-content')
205 .addClass('widget-modal modal-content')
204 .detach()
206 .detach()
205 .appendTo(that.$backdrop)
207 .appendTo(that.$backdrop)
206 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
208 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
207 .resizable()
209 .resizable()
208 .children('.ui-resizable-handle').show();
210 .children('.ui-resizable-handle').show();
209 that.show();
211 that.show();
210 that.$show_button.show();
212 that.$show_button.show();
211 that.$close.show();
213 that.$close.show();
212 }
214 }
213 event.stopPropagation();
215 event.stopPropagation();
214 });
216 });
215 this.$title = $('<div />')
217 this.$title = $('<div />')
216 .addClass('widget-modal-title')
218 .addClass('widget-modal-title')
217 .html("&nbsp;")
219 .html("&nbsp;")
218 .appendTo(this.$title_bar);
220 .appendTo(this.$title_bar);
219 this.$box = $('<div />')
221 this.$box = $('<div />')
220 .addClass('modal-body')
222 .addClass('modal-body')
221 .addClass('widget-modal-body')
223 .addClass('widget-modal-body')
222 .addClass('widget-box')
224 .addClass('widget-box')
223 .addClass('vbox')
225 .addClass('vbox')
224 .appendTo(this.$window);
226 .appendTo(this.$window);
225
227
226 this.$show_button = $('<button />')
228 this.$show_button = $('<button />')
227 .html("&nbsp;")
229 .html("&nbsp;")
228 .addClass('btn btn-info widget-modal-show')
230 .addClass('btn btn-info widget-modal-show')
229 .appendTo(this.$el)
231 .appendTo(this.$el)
230 .click(function(){
232 .click(function(){
231 that.show();
233 that.show();
232 });
234 });
233
235
234 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
236 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
235 this.$window.resizable();
237 this.$window.resizable();
236 this.$window.on('resize', function(){
238 this.$window.on('resize', function(){
237 that.$box.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
239 that.$box.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
238 });
240 });
239
241
240 this._shown_once = false;
242 this._shown_once = false;
241 this.popped_out = true;
243 this.popped_out = true;
242
244
243 this.update_children([], this.model.get('children'));
245 this.update_children([], this.model.get('children'));
244 this.model.on('change:children', function(model, value) {
246 this.model.on('change:children', function(model, value) {
245 this.update_children(model.previous('children'), value);
247 this.update_children(model.previous('children'), value);
246 }, this);
248 }, this);
247 },
249 },
248
250
249 hide: function() {
251 hide: function() {
250 // Called when the modal hide button is clicked.
252 // Called when the modal hide button is clicked.
251 this.$window.hide();
253 this.$window.hide();
252 this.$show_button.removeClass('btn-info');
254 this.$show_button.removeClass('btn-info');
253 },
255 },
254
256
255 show: function() {
257 show: function() {
256 // Called when the modal show button is clicked.
258 // Called when the modal show button is clicked.
257 this.$show_button.addClass('btn-info');
259 this.$show_button.addClass('btn-info');
258 this.$window.show();
260 this.$window.show();
259 if (this.popped_out) {
261 if (this.popped_out) {
260 this.$window.css("positon", "absolute");
262 this.$window.css("positon", "absolute");
261 this.$window.css("top", "0px");
263 this.$window.css("top", "0px");
262 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
264 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
263 $(window).scrollLeft()) + "px");
265 $(window).scrollLeft()) + "px");
264 this.bring_to_front();
266 this.bring_to_front();
265 }
267 }
266 },
268 },
267
269
268 bring_to_front: function() {
270 bring_to_front: function() {
269 // Make the modal top-most, z-ordered about the other modals.
271 // Make the modal top-most, z-ordered about the other modals.
270 var $widget_modals = $(".widget-modal");
272 var $widget_modals = $(".widget-modal");
271 var max_zindex = 0;
273 var max_zindex = 0;
272 $widget_modals.each(function (index, el){
274 $widget_modals.each(function (index, el){
273 var zindex = parseInt($(el).css('z-index'));
275 var zindex = parseInt($(el).css('z-index'));
274 if (!isNaN(zindex)) {
276 if (!isNaN(zindex)) {
275 max_zindex = Math.max(max_zindex, zindex);
277 max_zindex = Math.max(max_zindex, zindex);
276 }
278 }
277 });
279 });
278
280
279 // Start z-index of widget modals at 2000
281 // Start z-index of widget modals at 2000
280 max_zindex = Math.max(max_zindex, 2000);
282 max_zindex = Math.max(max_zindex, 2000);
281
283
282 $widget_modals.each(function (index, el){
284 $widget_modals.each(function (index, el){
283 $el = $(el);
285 $el = $(el);
284 if (max_zindex == parseInt($el.css('z-index'))) {
286 if (max_zindex == parseInt($el.css('z-index'))) {
285 $el.css('z-index', max_zindex - 1);
287 $el.css('z-index', max_zindex - 1);
286 }
288 }
287 });
289 });
288 this.$window.css('z-index', max_zindex);
290 this.$window.css('z-index', max_zindex);
289 },
291 },
290
292
291 update: function(){
293 update: function(){
292 // Update the contents of this view
294 // Update the contents of this view
293 //
295 //
294 // Called when the model is changed. The model may have been
296 // Called when the model is changed. The model may have been
295 // changed by another view or by a state update from the back-end.
297 // changed by another view or by a state update from the back-end.
296 var description = this.model.get('description');
298 var description = this.model.get('description');
297 if (description.trim().length === 0) {
299 if (description.trim().length === 0) {
298 this.$title.html("&nbsp;"); // Preserve title height
300 this.$title.html("&nbsp;"); // Preserve title height
299 } else {
301 } else {
300 this.$title.text(description);
302 this.$title.text(description);
301 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
303 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
302 }
304 }
303
305
304 var button_text = this.model.get('button_text');
306 var button_text = this.model.get('button_text');
305 if (button_text.trim().length === 0) {
307 if (button_text.trim().length === 0) {
306 this.$show_button.html("&nbsp;"); // Preserve button height
308 this.$show_button.html("&nbsp;"); // Preserve button height
307 } else {
309 } else {
308 this.$show_button.text(button_text);
310 this.$show_button.text(button_text);
309 }
311 }
310
312
311 if (!this._shown_once) {
313 if (!this._shown_once) {
312 this._shown_once = true;
314 this._shown_once = true;
313 this.show();
315 this.show();
314 }
316 }
315
317
316 return PopupView.__super__.update.apply(this);
318 return PopupView.__super__.update.apply(this);
317 },
319 },
318
320
319 _get_selector_element: function(selector) {
321 _get_selector_element: function(selector) {
320 // Get an element view a 'special' jquery selector. (see widget.js)
322 // Get an element view a 'special' jquery selector. (see widget.js)
321 //
323 //
322 // Since the modal actually isn't within the $el in the DOM, we need to extend
324 // Since the modal actually isn't within the $el in the DOM, we need to extend
323 // the selector logic to allow the user to set css on the modal if need be.
325 // the selector logic to allow the user to set css on the modal if need be.
324 // The convention used is:
326 // The convention used is:
325 // "modal" - select the modal div
327 // "modal" - select the modal div
326 // "modal [selector]" - select element(s) within the modal div.
328 // "modal [selector]" - select element(s) within the modal div.
327 // "[selector]" - select elements within $el
329 // "[selector]" - select elements within $el
328 // "" - select the $el
330 // "" - select the $el
329 if (selector.substring(0, 5) == 'modal') {
331 if (selector.substring(0, 5) == 'modal') {
330 if (selector == 'modal') {
332 if (selector == 'modal') {
331 return this.$window;
333 return this.$window;
332 } else {
334 } else {
333 return this.$window.find(selector.substring(6));
335 return this.$window.find(selector.substring(6));
334 }
336 }
335 } else {
337 } else {
336 return PopupView.__super__._get_selector_element.apply(this, [selector]);
338 return PopupView.__super__._get_selector_element.apply(this, [selector]);
337 }
339 }
338 },
340 },
339 });
341 });
340
342
341 return {
343 return {
342 'BoxView': BoxView,
344 'BoxView': BoxView,
343 'PopupView': PopupView,
345 'PopupView': PopupView,
344 'FlexBoxView': FlexBoxView,
346 'FlexBoxView': FlexBoxView,
345 };
347 };
346 });
348 });
@@ -1,257 +1,261 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 "widgets/js/widget",
5 "widgets/js/widget",
6 "base/js/utils",
6 "base/js/utils",
7 "jquery",
7 "jquery",
8 "bootstrap",
8 "bootstrap",
9 ], function(widget, utils, $){
9 ], function(widget, utils, $){
10
10
11 var AccordionView = widget.DOMWidgetView.extend({
11 var AccordionView = widget.DOMWidgetView.extend({
12 render: function(){
12 render: function(){
13 // Called when view is rendered.
13 // Called when view is rendered.
14 var guid = 'panel-group' + utils.uuid();
14 var guid = 'panel-group' + utils.uuid();
15 this.$el
15 this.$el
16 .attr('id', guid)
16 .attr('id', guid)
17 .addClass('panel-group');
17 .addClass('panel-group');
18 this.containers = [];
18 this.containers = [];
19 this.model_containers = {};
19 this.model_containers = {};
20 this.update_children([], this.model.get('children'));
20 this.update_children([], this.model.get('children'));
21 this.model.on('change:children', function(model, value, options) {
21 this.model.on('change:children', function(model, value, options) {
22 this.update_children(model.previous('children'), value);
22 this.update_children(model.previous('children'), value);
23 }, this);
23 }, this);
24 this.model.on('change:selected_index', function(model, value, options) {
24 this.model.on('change:selected_index', function(model, value, options) {
25 this.update_selected_index(model.previous('selected_index'), value, options);
25 this.update_selected_index(model.previous('selected_index'), value, options);
26 }, this);
26 }, this);
27 this.model.on('change:_titles', function(model, value, options) {
27 this.model.on('change:_titles', function(model, value, options) {
28 this.update_titles(value);
28 this.update_titles(value);
29 }, this);
29 }, this);
30 var that = this;
30 var that = this;
31 this.on('displayed', function() {
31 this.on('displayed', function() {
32 this.update_titles();
32 this.update_titles();
33 }, this);
33 }, this);
34 },
34 },
35
35
36 update_titles: function(titles) {
36 update_titles: function(titles) {
37 // Set tab titles
37 // Set tab titles
38 if (!titles) {
38 if (!titles) {
39 titles = this.model.get('_titles');
39 titles = this.model.get('_titles');
40 }
40 }
41
41
42 var that = this;
42 var that = this;
43 _.each(titles, function(title, page_index) {
43 _.each(titles, function(title, page_index) {
44 var accordian = that.containers[page_index];
44 var accordian = that.containers[page_index];
45 if (accordian !== undefined) {
45 if (accordian !== undefined) {
46 accordian
46 accordian
47 .find('.panel-heading')
47 .find('.panel-heading')
48 .find('.accordion-toggle')
48 .find('.accordion-toggle')
49 .text(title);
49 .text(title);
50 }
50 }
51 });
51 });
52 },
52 },
53
53
54 update_selected_index: function(old_index, new_index, options) {
54 update_selected_index: function(old_index, new_index, options) {
55 // Only update the selection if the selection wasn't triggered
55 // Only update the selection if the selection wasn't triggered
56 // by the front-end. It must be triggered by the back-end.
56 // by the front-end. It must be triggered by the back-end.
57 if (options === undefined || options.updated_view != this) {
57 if (options === undefined || options.updated_view != this) {
58 this.containers[old_index].find('.panel-collapse').collapse('hide');
58 this.containers[old_index].find('.panel-collapse').collapse('hide');
59 if (0 <= new_index && new_index < this.containers.length) {
59 if (0 <= new_index && new_index < this.containers.length) {
60 this.containers[new_index].find('.panel-collapse').collapse('show');
60 this.containers[new_index].find('.panel-collapse').collapse('show');
61 }
61 }
62 }
62 }
63 },
63 },
64
64
65 update_children: function(old_list, new_list) {
65 update_children: function(old_list, new_list) {
66 // Called when the children list is modified.
66 // Called when the children list is modified.
67 this.do_diff(old_list,
67 this.do_diff(old_list,
68 new_list,
68 new_list,
69 $.proxy(this.remove_child_model, this),
69 $.proxy(this.remove_child_model, this),
70 $.proxy(this.add_child_model, this));
70 $.proxy(this.add_child_model, this));
71 },
71 },
72
72
73 remove_child_model: function(model) {
73 remove_child_model: function(model) {
74 // Called when a child is removed from children list.
74 // Called when a child is removed from children list.
75 var accordion_group = this.model_containers[model.id];
75 var accordion_group = this.model_containers[model.id];
76 this.containers.splice(accordion_group.container_index, 1);
76 this.containers.splice(accordion_group.container_index, 1);
77 delete this.model_containers[model.id];
77 delete this.model_containers[model.id];
78 accordion_group.remove();
78 accordion_group.remove();
79 this.pop_child_view(model);
79 this.pop_child_view(model);
80 },
80 },
81
81
82 add_child_model: function(model) {
82 add_child_model: function(model) {
83 // Called when a child is added to children list.
83 // Called when a child is added to children list.
84 var index = this.containers.length;
84 var index = this.containers.length;
85 var uuid = utils.uuid();
85 var uuid = utils.uuid();
86 var accordion_group = $('<div />')
86 var accordion_group = $('<div />')
87 .addClass('panel panel-default')
87 .addClass('panel panel-default')
88 .appendTo(this.$el);
88 .appendTo(this.$el);
89 var accordion_heading = $('<div />')
89 var accordion_heading = $('<div />')
90 .addClass('panel-heading')
90 .addClass('panel-heading')
91 .appendTo(accordion_group);
91 .appendTo(accordion_group);
92 var that = this;
92 var that = this;
93 var accordion_toggle = $('<a />')
93 var accordion_toggle = $('<a />')
94 .addClass('accordion-toggle')
94 .addClass('accordion-toggle')
95 .attr('data-toggle', 'collapse')
95 .attr('data-toggle', 'collapse')
96 .attr('data-parent', '#' + this.$el.attr('id'))
96 .attr('data-parent', '#' + this.$el.attr('id'))
97 .attr('href', '#' + uuid)
97 .attr('href', '#' + uuid)
98 .click(function(evt){
98 .click(function(evt){
99
99
100 // Calling model.set will trigger all of the other views of the
100 // Calling model.set will trigger all of the other views of the
101 // model to update.
101 // model to update.
102 that.model.set("selected_index", index, {updated_view: that});
102 that.model.set("selected_index", index, {updated_view: that});
103 that.touch();
103 that.touch();
104 })
104 })
105 .text('Page ' + index)
105 .text('Page ' + index)
106 .appendTo(accordion_heading);
106 .appendTo(accordion_heading);
107 var accordion_body = $('<div />', {id: uuid})
107 var accordion_body = $('<div />', {id: uuid})
108 .addClass('panel-collapse collapse')
108 .addClass('panel-collapse collapse')
109 .appendTo(accordion_group);
109 .appendTo(accordion_group);
110 var accordion_inner = $('<div />')
110 var accordion_inner = $('<div />')
111 .addClass('panel-body')
111 .addClass('panel-body')
112 .appendTo(accordion_body);
112 .appendTo(accordion_body);
113 var container_index = this.containers.push(accordion_group) - 1;
113 var container_index = this.containers.push(accordion_group) - 1;
114 accordion_group.container_index = container_index;
114 accordion_group.container_index = container_index;
115 this.model_containers[model.id] = accordion_group;
115 this.model_containers[model.id] = accordion_group;
116
116
117 this.create_child_view(model, {callback: function(view) {
117 var dummy = $('<div/>');
118 accordion_inner.append(view.$el);
118 accordion_inner.append(dummy);
119
119 this.create_child_view(model).then(function(view) {
120 dummy.replaceWith(view.$el);
120 that.update();
121 that.update();
121 that.update_titles();
122 that.update_titles();
122
123
123 // Trigger the displayed event of the child view.
124 // Trigger the displayed event of the child view.
124 that.after_displayed(function() {
125 that.after_displayed(function() {
125 view.trigger('displayed');
126 view.trigger('displayed');
126 });
127 });
127 }});
128 }, console.error);
128 },
129 },
129 });
130 });
130
131
131
132
132 var TabView = widget.DOMWidgetView.extend({
133 var TabView = widget.DOMWidgetView.extend({
133 initialize: function() {
134 initialize: function() {
134 // Public constructor.
135 // Public constructor.
135 this.containers = [];
136 this.containers = [];
136 TabView.__super__.initialize.apply(this, arguments);
137 TabView.__super__.initialize.apply(this, arguments);
137 },
138 },
138
139
139 render: function(){
140 render: function(){
140 // Called when view is rendered.
141 // Called when view is rendered.
141 var uuid = 'tabs'+utils.uuid();
142 var uuid = 'tabs'+utils.uuid();
142 var that = this;
143 var that = this;
143 this.$tabs = $('<div />', {id: uuid})
144 this.$tabs = $('<div />', {id: uuid})
144 .addClass('nav')
145 .addClass('nav')
145 .addClass('nav-tabs')
146 .addClass('nav-tabs')
146 .appendTo(this.$el);
147 .appendTo(this.$el);
147 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
148 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
148 .addClass('tab-content')
149 .addClass('tab-content')
149 .appendTo(this.$el);
150 .appendTo(this.$el);
150 this.containers = [];
151 this.containers = [];
151 this.update_children([], this.model.get('children'));
152 this.update_children([], this.model.get('children'));
152 this.model.on('change:children', function(model, value, options) {
153 this.model.on('change:children', function(model, value, options) {
153 this.update_children(model.previous('children'), value);
154 this.update_children(model.previous('children'), value);
154 }, this);
155 }, this);
155 },
156 },
156
157
157 update_attr: function(name, value) {
158 update_attr: function(name, value) {
158 // Set a css attr of the widget view.
159 // Set a css attr of the widget view.
159 this.$tabs.css(name, value);
160 this.$tabs.css(name, value);
160 },
161 },
161
162
162 update_children: function(old_list, new_list) {
163 update_children: function(old_list, new_list) {
163 // Called when the children list is modified.
164 // Called when the children list is modified.
164 this.do_diff(old_list,
165 this.do_diff(old_list,
165 new_list,
166 new_list,
166 $.proxy(this.remove_child_model, this),
167 $.proxy(this.remove_child_model, this),
167 $.proxy(this.add_child_model, this));
168 $.proxy(this.add_child_model, this));
168 },
169 },
169
170
170 remove_child_model: function(model) {
171 remove_child_model: function(model) {
171 // Called when a child is removed from children list.
172 // Called when a child is removed from children list.
172 var view = this.pop_child_view(model);
173 var view = this.pop_child_view(model);
173 this.containers.splice(view.parent_tab.tab_text_index, 1);
174 this.containers.splice(view.parent_tab.tab_text_index, 1);
174 view.parent_tab.remove();
175 view.parent_tab.remove();
175 view.parent_container.remove();
176 view.parent_container.remove();
176 view.remove();
177 view.remove();
177 },
178 },
178
179
179 add_child_model: function(model) {
180 add_child_model: function(model) {
180 // Called when a child is added to children list.
181 // Called when a child is added to children list.
181 var index = this.containers.length;
182 var index = this.containers.length;
182 var uuid = utils.uuid();
183 var uuid = utils.uuid();
183
184
184 var that = this;
185 var that = this;
185 var tab = $('<li />')
186 var tab = $('<li />')
186 .css('list-style-type', 'none')
187 .css('list-style-type', 'none')
187 .appendTo(this.$tabs);
188 .appendTo(this.$tabs);
188
189
189 this.create_child_view(model, {callback: function(view) {
190 view.parent_tab = tab;
191
190
192 var tab_text = $('<a />')
191 var tab_text = $('<a />')
193 .attr('href', '#' + uuid)
192 .attr('href', '#' + uuid)
194 .attr('data-toggle', 'tab')
193 .attr('data-toggle', 'tab')
195 .text('Page ' + index)
194 .text('Page ' + index)
196 .appendTo(tab)
195 .appendTo(tab)
197 .click(function (e) {
196 .click(function (e) {
198
197
199 // Calling model.set will trigger all of the other views of the
198 // Calling model.set will trigger all of the other views of the
200 // model to update.
199 // model to update.
201 that.model.set("selected_index", index, {updated_view: that});
200 that.model.set("selected_index", index, {updated_view: that});
202 that.touch();
201 that.touch();
203 that.select_page(index);
202 that.select_page(index);
204 });
203 });
205 tab.tab_text_index = that.containers.push(tab_text) - 1;
204 tab.tab_text_index = that.containers.push(tab_text) - 1;
205
206 var dummy = $('<div />');
207 var contents_div = $('<div />', {id: uuid})
208 .addClass('tab-pane')
209 .addClass('fade')
210 .append(dummy)
211 .appendTo(that.$tab_contents);
206
212
207 var contents_div = $('<div />', {id: uuid})
213 this.create_child_view(model).then(function(view) {
208 .addClass('tab-pane')
214 dummy.replaceWith(view.$el);
209 .addClass('fade')
215 view.parent_tab = tab;
210 .append(view.$el)
211 .appendTo(that.$tab_contents);
212 view.parent_container = contents_div;
216 view.parent_container = contents_div;
213
217
214 // Trigger the displayed event of the child view.
218 // Trigger the displayed event of the child view.
215 that.after_displayed(function() {
219 that.after_displayed(function() {
216 view.trigger('displayed');
220 view.trigger('displayed');
217 });
221 });
218 }});
222 }, console.error);
219 },
223 },
220
224
221 update: function(options) {
225 update: function(options) {
222 // Update the contents of this view
226 // Update the contents of this view
223 //
227 //
224 // Called when the model is changed. The model may have been
228 // Called when the model is changed. The model may have been
225 // changed by another view or by a state update from the back-end.
229 // changed by another view or by a state update from the back-end.
226 if (options === undefined || options.updated_view != this) {
230 if (options === undefined || options.updated_view != this) {
227 // Set tab titles
231 // Set tab titles
228 var titles = this.model.get('_titles');
232 var titles = this.model.get('_titles');
229 var that = this;
233 var that = this;
230 _.each(titles, function(title, page_index) {
234 _.each(titles, function(title, page_index) {
231 var tab_text = that.containers[page_index];
235 var tab_text = that.containers[page_index];
232 if (tab_text !== undefined) {
236 if (tab_text !== undefined) {
233 tab_text.text(title);
237 tab_text.text(title);
234 }
238 }
235 });
239 });
236
240
237 var selected_index = this.model.get('selected_index');
241 var selected_index = this.model.get('selected_index');
238 if (0 <= selected_index && selected_index < this.containers.length) {
242 if (0 <= selected_index && selected_index < this.containers.length) {
239 this.select_page(selected_index);
243 this.select_page(selected_index);
240 }
244 }
241 }
245 }
242 return TabView.__super__.update.apply(this);
246 return TabView.__super__.update.apply(this);
243 },
247 },
244
248
245 select_page: function(index) {
249 select_page: function(index) {
246 // Select a page.
250 // Select a page.
247 this.$tabs.find('li')
251 this.$tabs.find('li')
248 .removeClass('active');
252 .removeClass('active');
249 this.containers[index].tab('show');
253 this.containers[index].tab('show');
250 },
254 },
251 });
255 });
252
256
253 return {
257 return {
254 'AccordionView': AccordionView,
258 'AccordionView': AccordionView,
255 'TabView': TabView,
259 'TabView': TabView,
256 };
260 };
257 });
261 });
General Comments 0
You need to be logged in to leave comments. Login now