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