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