##// END OF EJS Templates
Merge pull request #6127 from minrk/fix-edit-md...
Matthias Bussonnier -
r17287:c3e6f16b merge
parent child Browse files
Show More
@@ -1,163 +1,163 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 ], function(IPython, $) {
8 8 "use strict";
9 9
10 10 var modal = function (options) {
11 11
12 12 var modal = $("<div/>")
13 13 .addClass("modal")
14 14 .addClass("fade")
15 15 .attr("role", "dialog");
16 16 var dialog = $("<div/>")
17 17 .addClass("modal-dialog")
18 18 .appendTo(modal);
19 19 var dialog_content = $("<div/>")
20 20 .addClass("modal-content")
21 21 .appendTo(dialog);
22 22 dialog_content.append(
23 23 $("<div/>")
24 24 .addClass("modal-header")
25 25 .append($("<button>")
26 26 .attr("type", "button")
27 27 .addClass("close")
28 28 .attr("data-dismiss", "modal")
29 29 .attr("aria-hidden", "true")
30 30 .html("&times;")
31 31 ).append(
32 32 $("<h4/>")
33 33 .addClass('modal-title')
34 34 .text(options.title || "")
35 35 )
36 36 ).append(
37 37 $("<div/>").addClass("modal-body").append(
38 38 options.body || $("<p/>")
39 39 )
40 40 );
41 41
42 42 var footer = $("<div/>").addClass("modal-footer");
43 43
44 44 for (var label in options.buttons) {
45 45 var btn_opts = options.buttons[label];
46 46 var button = $("<button/>")
47 47 .addClass("btn btn-default btn-sm")
48 48 .attr("data-dismiss", "modal")
49 49 .text(label);
50 50 if (btn_opts.click) {
51 51 button.click($.proxy(btn_opts.click, dialog_content));
52 52 }
53 53 if (btn_opts.class) {
54 54 button.addClass(btn_opts.class);
55 55 }
56 56 footer.append(button);
57 57 }
58 58 dialog_content.append(footer);
59 59 // hook up on-open event
60 60 modal.on("shown.bs.modal", function() {
61 61 setTimeout(function() {
62 62 footer.find("button").last().focus();
63 63 if (options.open) {
64 64 $.proxy(options.open, modal)();
65 65 }
66 66 }, 0);
67 67 });
68 68
69 69 // destroy modal on hide, unless explicitly asked not to
70 70 if (options.destroy === undefined || options.destroy) {
71 71 modal.on("hidden.bs.modal", function () {
72 72 modal.remove();
73 73 });
74 74 }
75 75 modal.on("hidden.bs.modal", function () {
76 76 if (options.notebook) {
77 77 var cell = options.notebook.get_selected_cell();
78 78 if (cell) cell.select();
79 79 }
80 80 if (options.keyboard_manager) {
81 81 options.keyboard_manager.enable();
82 82 options.keyboard_manager.command_mode();
83 83 }
84 84 });
85 85
86 86 if (options.keyboard_manager) {
87 87 options.keyboard_manager.disable();
88 88 }
89 89
90 90 return modal.modal(options);
91 91 };
92 92
93 93 var edit_metadata = function (options) {
94 94 options.name = options.name || "Cell";
95 95 var error_div = $('<div/>').css('color', 'red');
96 96 var message =
97 97 "Manually edit the JSON below to manipulate the metadata for this " + options.name + "." +
98 98 " We recommend putting custom metadata attributes in an appropriately named sub-structure," +
99 99 " so they don't conflict with those of others.";
100 100
101 101 var textarea = $('<textarea/>')
102 102 .attr('rows', '13')
103 103 .attr('cols', '80')
104 104 .attr('name', 'metadata')
105 105 .text(JSON.stringify(options.md || {}, null, 2));
106 106
107 107 var dialogform = $('<div/>').attr('title', 'Edit the metadata')
108 108 .append(
109 109 $('<form/>').append(
110 110 $('<fieldset/>').append(
111 111 $('<label/>')
112 112 .attr('for','metadata')
113 113 .text(message)
114 114 )
115 115 .append(error_div)
116 116 .append($('<br/>'))
117 117 .append(textarea)
118 118 )
119 119 );
120 120 var editor = CodeMirror.fromTextArea(textarea[0], {
121 121 lineNumbers: true,
122 122 matchBrackets: true,
123 123 indentUnit: 2,
124 124 autoIndent: true,
125 125 mode: 'application/json',
126 126 });
127 var modal = modal({
127 var modal_obj = modal({
128 128 title: "Edit " + options.name + " Metadata",
129 129 body: dialogform,
130 130 buttons: {
131 131 OK: { class : "btn-primary",
132 132 click: function() {
133 133 // validate json and set it
134 134 var new_md;
135 135 try {
136 136 new_md = JSON.parse(editor.getValue());
137 137 } catch(e) {
138 138 console.log(e);
139 139 error_div.text('WARNING: Could not save invalid JSON.');
140 140 return false;
141 141 }
142 142 options.callback(new_md);
143 143 }
144 144 },
145 145 Cancel: {}
146 146 },
147 147 notebook: options.notebook,
148 148 keyboard_manager: options.keyboard_manager,
149 149 });
150 150
151 modal.on('shown.bs.modal', function(){ editor.refresh(); });
151 modal_obj.on('shown.bs.modal', function(){ editor.refresh(); });
152 152 };
153 153
154 154 var dialog = {
155 155 modal : modal,
156 156 edit_metadata : edit_metadata,
157 157 };
158 158
159 159 // Backwards compatability.
160 160 IPython.dialog = dialog;
161 161
162 162 return dialog;
163 163 });
@@ -1,417 +1,417 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 ], function(IPython, $) {
8 8 "use strict";
9 9
10 10 var CellToolbar = function (options) {
11 11 // Constructor
12 12 //
13 13 // Parameters:
14 14 // options: dictionary
15 15 // Dictionary of keyword arguments.
16 16 // events: $(Events) instance
17 17 // cell: Cell instance
18 18 // notebook: Notebook instance
19 19 CellToolbar._instances.push(this);
20 20 this.notebook = options.notebook;
21 21 this.events = options.events;
22 22 this.cell = options.cell;
23 23 this.create_element();
24 24 this.rebuild();
25 25 return this;
26 26 };
27 27
28 28
29 29 CellToolbar.prototype.create_element = function () {
30 30 this.inner_element = $('<div/>').addClass('celltoolbar');
31 31 this.element = $('<div/>').addClass('ctb_hideshow')
32 32 .append(this.inner_element);
33 33 };
34 34
35 35
36 36 // The default css style for the outer celltoolbar div
37 37 // (ctb_hideshow) is display: none.
38 38 // To show the cell toolbar, *both* of the following conditions must be met:
39 39 // - A parent container has class `ctb_global_show`
40 40 // - The celltoolbar has the class `ctb_show`
41 41 // This allows global show/hide, as well as per-cell show/hide.
42 42
43 43 CellToolbar.global_hide = function () {
44 44 $('body').removeClass('ctb_global_show');
45 45 };
46 46
47 47
48 48 CellToolbar.global_show = function () {
49 49 $('body').addClass('ctb_global_show');
50 50 };
51 51
52 52
53 53 CellToolbar.prototype.hide = function () {
54 54 this.element.removeClass('ctb_show');
55 55 };
56 56
57 57
58 58 CellToolbar.prototype.show = function () {
59 59 this.element.addClass('ctb_show');
60 60 };
61 61
62 62
63 63 /**
64 64 * Class variable that should contain a dict of all available callback
65 65 * we need to think of wether or not we allow nested namespace
66 66 * @property _callback_dict
67 67 * @private
68 68 * @static
69 69 * @type Dict
70 70 */
71 71 CellToolbar._callback_dict = {};
72 72
73 73
74 74 /**
75 75 * Class variable that should contain the reverse order list of the button
76 76 * to add to the toolbar of each cell
77 77 * @property _ui_controls_list
78 78 * @private
79 79 * @static
80 80 * @type List
81 81 */
82 82 CellToolbar._ui_controls_list = [];
83 83
84 84
85 85 /**
86 86 * Class variable that should contain the CellToolbar instances for each
87 87 * cell of the notebook
88 88 *
89 89 * @private
90 90 * @property _instances
91 91 * @static
92 92 * @type List
93 93 */
94 94 CellToolbar._instances = [];
95 95
96 96
97 97 /**
98 98 * keep a list of all the available presets for the toolbar
99 99 * @private
100 100 * @property _presets
101 101 * @static
102 102 * @type Dict
103 103 */
104 104 CellToolbar._presets = {};
105 105
106 106
107 107 // this is by design not a prototype.
108 108 /**
109 109 * Register a callback to create an UI element in a cell toolbar.
110 110 * @method register_callback
111 111 * @param name {String} name to use to refer to the callback. It is advised to use a prefix with the name
112 112 * for easier sorting and avoid collision
113 113 * @param callback {function(div, cell)} callback that will be called to generate the ui element
114 114 * @param [cell_types] {List of String|undefined} optional list of cell types. If present the UI element
115 115 * will be added only to cells of types in the list.
116 116 *
117 117 *
118 118 * The callback will receive the following element :
119 119 *
120 120 * * a div in which to add element.
121 121 * * the cell it is responsible from
122 122 *
123 123 * @example
124 124 *
125 125 * Example that create callback for a button that toggle between `true` and `false` label,
126 126 * with the metadata under the key 'foo' to reflect the status of the button.
127 127 *
128 128 * // first param reference to a DOM div
129 129 * // second param reference to the cell.
130 130 * var toggle = function(div, cell) {
131 131 * var button_container = $(div)
132 132 *
133 133 * // let's create a button that show the current value of the metadata
134 134 * var button = $('<div/>').button({label:String(cell.metadata.foo)});
135 135 *
136 136 * // On click, change the metadata value and update the button label
137 137 * button.click(function(){
138 138 * var v = cell.metadata.foo;
139 139 * cell.metadata.foo = !v;
140 140 * button.button("option", "label", String(!v));
141 141 * })
142 142 *
143 143 * // add the button to the DOM div.
144 144 * button_container.append(button);
145 145 * }
146 146 *
147 147 * // now we register the callback under the name `foo` to give the
148 148 * // user the ability to use it later
149 149 * CellToolbar.register_callback('foo', toggle);
150 150 */
151 151 CellToolbar.register_callback = function(name, callback, cell_types) {
152 152 // Overwrite if it already exists.
153 153 CellToolbar._callback_dict[name] = cell_types ? {callback: callback, cell_types: cell_types} : callback;
154 154 };
155 155
156 156
157 157 /**
158 158 * Register a preset of UI element in a cell toolbar.
159 159 * Not supported Yet.
160 160 * @method register_preset
161 161 * @param name {String} name to use to refer to the preset. It is advised to use a prefix with the name
162 162 * for easier sorting and avoid collision
163 163 * @param preset_list {List of String} reverse order of the button in the toolbar. Each String of the list
164 164 * should correspond to a name of a registerd callback.
165 165 *
166 166 * @private
167 167 * @example
168 168 *
169 169 * CellToolbar.register_callback('foo.c1', function(div, cell){...});
170 170 * CellToolbar.register_callback('foo.c2', function(div, cell){...});
171 171 * CellToolbar.register_callback('foo.c3', function(div, cell){...});
172 172 * CellToolbar.register_callback('foo.c4', function(div, cell){...});
173 173 * CellToolbar.register_callback('foo.c5', function(div, cell){...});
174 174 *
175 175 * CellToolbar.register_preset('foo.foo_preset1', ['foo.c1', 'foo.c2', 'foo.c5'])
176 176 * CellToolbar.register_preset('foo.foo_preset2', ['foo.c4', 'foo.c5'])
177 177 */
178 178 CellToolbar.register_preset = function(name, preset_list, notebook, events) {
179 179 CellToolbar._presets[name] = preset_list;
180 180 events.trigger('preset_added.CellToolbar', {name: name});
181 181 // When "register_callback" is called by a custom extension, it may be executed after notebook is loaded.
182 182 // In that case, activate the preset if needed.
183 183 if (notebook && notebook.metadata && notebook.metadata.celltoolbar === name)
184 184 CellToolbar.activate_preset(name, events);
185 185 };
186 186
187 187
188 188 /**
189 189 * List the names of the presets that are currently registered.
190 190 *
191 191 * @method list_presets
192 192 * @static
193 193 */
194 194 CellToolbar.list_presets = function() {
195 195 var keys = [];
196 196 for (var k in CellToolbar._presets) {
197 197 keys.push(k);
198 198 }
199 199 return keys;
200 200 };
201 201
202 202
203 203 /**
204 204 * Activate an UI preset from `register_preset`
205 205 *
206 206 * This does not update the selection UI.
207 207 *
208 208 * @method activate_preset
209 209 * @param preset_name {String} string corresponding to the preset name
210 210 *
211 211 * @static
212 212 * @private
213 213 * @example
214 214 *
215 215 * CellToolbar.activate_preset('foo.foo_preset1');
216 216 */
217 217 CellToolbar.activate_preset = function(preset_name, events){
218 218 var preset = CellToolbar._presets[preset_name];
219 219
220 220 if(preset !== undefined){
221 221 CellToolbar._ui_controls_list = preset;
222 222 CellToolbar.rebuild_all();
223 223 }
224 224
225 225 if (events) {
226 226 events.trigger('preset_activated.CellToolbar', {name: preset_name});
227 227 }
228 228 };
229 229
230 230
231 231 /**
232 232 * This should be called on the class and not on a instance as it will trigger
233 233 * rebuild of all the instances.
234 234 * @method rebuild_all
235 235 * @static
236 236 *
237 237 */
238 238 CellToolbar.rebuild_all = function(){
239 239 for(var i=0; i < CellToolbar._instances.length; i++){
240 240 CellToolbar._instances[i].rebuild();
241 241 }
242 242 };
243 243
244 244 /**
245 245 * Rebuild all the button on the toolbar to update its state.
246 246 * @method rebuild
247 247 */
248 248 CellToolbar.prototype.rebuild = function(){
249 249 // strip evrything from the div
250 250 // which is probably inner_element
251 251 // or this.element.
252 252 this.inner_element.empty();
253 253 this.ui_controls_list = [];
254 254
255 255 var callbacks = CellToolbar._callback_dict;
256 256 var preset = CellToolbar._ui_controls_list;
257 257 // Yes we iterate on the class variable, not the instance one.
258 258 for (var i=0; i < preset.length; i++) {
259 259 var key = preset[i];
260 260 var callback = callbacks[key];
261 261 if (!callback) continue;
262 262
263 263 if (typeof callback === 'object') {
264 264 if (callback.cell_types.indexOf(this.cell.cell_type) === -1) continue;
265 265 callback = callback.callback;
266 266 }
267 267
268 268 var local_div = $('<div/>').addClass('button_container');
269 269 try {
270 270 callback(local_div, this.cell, this);
271 271 this.ui_controls_list.push(key);
272 272 } catch (e) {
273 273 console.log("Error in cell toolbar callback " + key, e);
274 274 continue;
275 275 }
276 276 // only append if callback succeeded.
277 277 this.inner_element.append(local_div);
278 278 }
279 279
280 280 // If there are no controls or the cell is a rendered TextCell hide the toolbar.
281 if (!this.ui_controls_list.length || (this.cell_type != 'code' && this.cell.rendered)) {
281 if (!this.ui_controls_list.length || (this.cell.cell_type != 'code' && this.cell.rendered)) {
282 282 this.hide();
283 283 } else {
284 284 this.show();
285 285 }
286 286 };
287 287
288 288
289 289 /**
290 290 */
291 291 CellToolbar.utils = {};
292 292
293 293
294 294 /**
295 295 * A utility function to generate bindings between a checkbox and cell/metadata
296 296 * @method utils.checkbox_ui_generator
297 297 * @static
298 298 *
299 299 * @param name {string} Label in front of the checkbox
300 300 * @param setter {function( cell, newValue )}
301 301 * A setter method to set the newValue
302 302 * @param getter {function( cell )}
303 303 * A getter methods which return the current value.
304 304 *
305 305 * @return callback {function( div, cell )} Callback to be passed to `register_callback`
306 306 *
307 307 * @example
308 308 *
309 309 * An exmple that bind the subkey `slideshow.isSectionStart` to a checkbox with a `New Slide` label
310 310 *
311 311 * var newSlide = CellToolbar.utils.checkbox_ui_generator('New Slide',
312 312 * // setter
313 313 * function(cell, value){
314 314 * // we check that the slideshow namespace exist and create it if needed
315 315 * if (cell.metadata.slideshow == undefined){cell.metadata.slideshow = {}}
316 316 * // set the value
317 317 * cell.metadata.slideshow.isSectionStart = value
318 318 * },
319 319 * //geter
320 320 * function(cell){ var ns = cell.metadata.slideshow;
321 321 * // if the slideshow namespace does not exist return `undefined`
322 322 * // (will be interpreted as `false` by checkbox) otherwise
323 323 * // return the value
324 324 * return (ns == undefined)? undefined: ns.isSectionStart
325 325 * }
326 326 * );
327 327 *
328 328 * CellToolbar.register_callback('newSlide', newSlide);
329 329 *
330 330 */
331 331 CellToolbar.utils.checkbox_ui_generator = function(name, setter, getter){
332 332 return function(div, cell, celltoolbar) {
333 333 var button_container = $(div);
334 334
335 335 var chkb = $('<input/>').attr('type', 'checkbox');
336 336 var lbl = $('<label/>').append($('<span/>').text(name));
337 337 lbl.append(chkb);
338 338 chkb.attr("checked", getter(cell));
339 339
340 340 chkb.click(function(){
341 341 var v = getter(cell);
342 342 setter(cell, !v);
343 343 chkb.attr("checked", !v);
344 344 });
345 345 button_container.append($('<div/>').append(lbl));
346 346 };
347 347 };
348 348
349 349
350 350 /**
351 351 * A utility function to generate bindings between a dropdown list cell
352 352 * @method utils.select_ui_generator
353 353 * @static
354 354 *
355 355 * @param list_list {list of sublist} List of sublist of metadata value and name in the dropdown list.
356 356 * subslit shoud contain 2 element each, first a string that woul be displayed in the dropdown list,
357 357 * and second the corresponding value to be passed to setter/return by getter. the corresponding value
358 358 * should not be "undefined" or behavior can be unexpected.
359 359 * @param setter {function( cell, newValue )}
360 360 * A setter method to set the newValue
361 361 * @param getter {function( cell )}
362 362 * A getter methods which return the current value of the metadata.
363 363 * @param [label=""] {String} optionnal label for the dropdown menu
364 364 *
365 365 * @return callback {function( div, cell )} Callback to be passed to `register_callback`
366 366 *
367 367 * @example
368 368 *
369 369 * var select_type = CellToolbar.utils.select_ui_generator([
370 370 * ["<None>" , "None" ],
371 371 * ["Header Slide" , "header_slide" ],
372 372 * ["Slide" , "slide" ],
373 373 * ["Fragment" , "fragment" ],
374 374 * ["Skip" , "skip" ],
375 375 * ],
376 376 * // setter
377 377 * function(cell, value){
378 378 * // we check that the slideshow namespace exist and create it if needed
379 379 * if (cell.metadata.slideshow == undefined){cell.metadata.slideshow = {}}
380 380 * // set the value
381 381 * cell.metadata.slideshow.slide_type = value
382 382 * },
383 383 * //geter
384 384 * function(cell){ var ns = cell.metadata.slideshow;
385 385 * // if the slideshow namespace does not exist return `undefined`
386 386 * // (will be interpreted as `false` by checkbox) otherwise
387 387 * // return the value
388 388 * return (ns == undefined)? undefined: ns.slide_type
389 389 * }
390 390 * CellToolbar.register_callback('slideshow.select', select_type);
391 391 *
392 392 */
393 393 CellToolbar.utils.select_ui_generator = function(list_list, setter, getter, label) {
394 394 label = label || "";
395 395 return function(div, cell, celltoolbar) {
396 396 var button_container = $(div);
397 397 var lbl = $("<label/>").append($('<span/>').text(label));
398 398 var select = $('<select/>').addClass('ui-widget ui-widget-content');
399 399 for(var i=0; i < list_list.length; i++){
400 400 var opt = $('<option/>')
401 401 .attr('value', list_list[i][1])
402 402 .text(list_list[i][0]);
403 403 select.append(opt);
404 404 }
405 405 select.val(getter(cell));
406 406 select.change(function(){
407 407 setter(cell, select.val());
408 408 });
409 409 button_container.append($('<div/>').append(lbl).append(select));
410 410 };
411 411 };
412 412
413 413 // Backwards compatability.
414 414 IPython.CellToolbar = CellToolbar;
415 415
416 416 return {'CellToolbar': CellToolbar};
417 417 });
@@ -1,40 +1,51 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'jquery',
6 6 'notebook/js/celltoolbar',
7 7 'base/js/dialog',
8 8 ], function($, celltoolbar, dialog) {
9 9 "use strict";
10 10
11 11 var CellToolbar = celltoolbar.CellToolbar;
12 12
13 var raw_edit = function(cell){
14 dialog.edit_metadata(cell.metadata, function (md) {
15 cell.metadata = md;
13 var raw_edit = function (cell) {
14 dialog.edit_metadata({
15 md: cell.metadata,
16 callback: function (md) {
17 cell.metadata = md;
18 },
19 name: 'Cell',
20 notebook: this.notebook,
21 keyboard_manager: this.keyboard_manager
16 22 });
17 23 };
18 24
19 25 var add_raw_edit_button = function(div, cell) {
20 26 var button_container = $(div);
21 27 var button = $('<button/>')
22 28 .addClass("btn btn-default btn-xs")
23 29 .text("Edit Metadata")
24 30 .click( function () {
25 31 raw_edit(cell);
26 32 return false;
27 33 });
28 34 button_container.append(button);
29 35 };
30 36
31 37 var register = function (notebook, events) {
32 38 CellToolbar.register_callback('default.rawedit', add_raw_edit_button);
39 raw_edit = $.proxy(raw_edit, {
40 notebook: notebook,
41 keyboard_manager: notebook.keyboard_manager
42 });
43
33 44 var example_preset = [];
34 45 example_preset.push('default.rawedit');
35 46
36 47 CellToolbar.register_preset('Edit Metadata', example_preset, notebook, events);
37 48 console.log('Default extension for cell metadata editing loaded.');
38 49 };
39 50 return {'register': register};
40 51 });
General Comments 0
You need to be logged in to leave comments. Login now