Skip to content

Commit 27cfa29

Browse files
authored
Merge pull request #2048 from vidartf/tags-bar
Simple implementation of cell tags
2 parents 39b756b + 946a4ac commit 27cfa29

File tree

4 files changed

+295
-0
lines changed

4 files changed

+295
-0
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
define([
5+
'notebook/js/celltoolbar',
6+
'base/js/dialog',
7+
], function(celltoolbar, dialog) {
8+
"use strict";
9+
10+
var CellToolbar = celltoolbar.CellToolbar;
11+
12+
var array_difference = function(a, b) {
13+
return a.filter(function(n) {
14+
return b.indexOf(n) === -1;
15+
});
16+
}
17+
18+
var write_tag = function(cell, name, add) {
19+
if (add) {
20+
// Add to metadata
21+
if (cell.metadata.tags === undefined) {
22+
cell.metadata.tags = [];
23+
} else if (cell.metadata.tags.indexOf(name) !== -1) {
24+
// Tag already exists
25+
return false;
26+
}
27+
cell.metadata.tags.push(name);
28+
} else {
29+
// Remove from metadata
30+
if (!cell.metadata || !cell.metadata.tags) {
31+
// No tags to remove
32+
return false;
33+
}
34+
// Remove tag from tags list
35+
var index = cell.metadata.tags.indexOf(name);
36+
if (index !== -1) {
37+
cell.metadata.tags.splice(index, 1);
38+
}
39+
// If tags list is empty, remove it
40+
if (cell.metadata.tags.length === 0) {
41+
delete cell.metadata.tags;
42+
}
43+
}
44+
cell.events.trigger('set_dirty.Notebook', {value: true});
45+
return true;
46+
}
47+
48+
var preprocess_input = function(input) {
49+
// Split on whitespace:
50+
return input.split(/\s/);
51+
}
52+
53+
var add_tag = function(cell, tag_container, on_remove) {
54+
return function(name) {
55+
if (name === '') {
56+
// Skip empty strings
57+
return;
58+
}
59+
// Write tag to metadata
60+
var changed = write_tag(cell, name, true);
61+
62+
if (changed) {
63+
// Make tag UI
64+
var tag = make_tag(name, on_remove);
65+
tag_container.append(tag);
66+
var tag_map = jQuery.data(tag_container, "tag_map") || {};
67+
tag_map[name] = tag;
68+
jQuery.data(tag_container, 'tag_map', tag_map);
69+
}
70+
};
71+
};
72+
73+
var remove_tag = function(cell, tag_container) {
74+
return function(name) {
75+
var changed = write_tag(cell, name, false);
76+
if (changed) {
77+
// Remove tag UI
78+
var tag_map = jQuery.data(tag_container, "tag_map") || {};
79+
var tag_UI = tag_map[name];
80+
delete tag_map[name];
81+
tag_UI.remove();
82+
}
83+
};
84+
};
85+
86+
var init_tag_container = function(cell, tag_container, on_remove) {
87+
var tag_list = cell.metadata.tags || [];
88+
if (!Array.isArray(tag_list)) {
89+
// We cannot make tags UI for this cell!
90+
// Maybe someone else used "tags" for something?
91+
return false; // Fail gracefully
92+
}
93+
94+
var tag_map = {};
95+
for (var i=0; i < tag_list.length; ++i) {
96+
var tag_name = tag_list[i];
97+
if (typeof tag_name !== 'string') {
98+
// Unexpected type, disable toolbar for safety
99+
return false;
100+
}
101+
var tag = make_tag(tag_name, on_remove);
102+
tag_container.append(tag);
103+
tag_map[tag_name] = tag;
104+
}
105+
jQuery.data(tag_container, 'tag_map', tag_map);
106+
return true;
107+
};
108+
109+
var make_tag = function(name, on_remove) {
110+
var tag_UI = $('<span/>')
111+
.addClass('cell-tag')
112+
.text(name);
113+
114+
var remove_button = $('<a/>')
115+
.addClass('remove-tag-btn')
116+
.text('X')
117+
.click( function(ev) {
118+
if (ev.button === 0) {
119+
on_remove(name);
120+
return false;
121+
}
122+
});
123+
tag_UI.append(remove_button);
124+
return tag_UI;
125+
};
126+
127+
// Single edit with button to add tags
128+
var add_tag_edit = function(div, cell, on_add, on_remove) {
129+
var button_container = $(div);
130+
131+
var text = $('<input/>').attr('type', 'text');
132+
var button = $('<button />')
133+
.addClass('btn btn-default btn-xs')
134+
.text('Add tag')
135+
.click(function() {
136+
var tags = preprocess_input(text[0].value);
137+
for (var i=0; i < tags.length; ++i) {
138+
on_add(tags[i]);
139+
}
140+
// Clear input after adding:
141+
text[0].value = '';
142+
return false;
143+
});
144+
// Wire enter in input to button click
145+
text.keyup(function(event){
146+
if(event.keyCode == 13){
147+
button.click();
148+
}
149+
});
150+
var input_container = $('<span/>')
151+
.addClass('tags-input')
152+
add_dialog_button(input_container, cell, on_add, on_remove);
153+
button_container.append(input_container
154+
.append(text)
155+
.append(button)
156+
);
157+
IPython.keyboard_manager.register_events(text);
158+
};
159+
160+
var tag_dialog = function(cell, on_add, on_remove) {
161+
var tag_list = cell.metadata.tags || [];
162+
163+
var message =
164+
"Edit the list of tags below. All whitespace " +
165+
"is treated as tag separators.";
166+
167+
var textarea = $('<textarea/>')
168+
.attr('rows', '13')
169+
.attr('cols', '80')
170+
.attr('name', 'tags')
171+
.text(tag_list.join('\n'));
172+
173+
var dialogform = $('<div/>').attr('title', 'Edit the tags')
174+
.append(
175+
$('<form/>').append(
176+
$('<fieldset/>').append(
177+
$('<label/>')
178+
.attr('for','tags')
179+
.text(message)
180+
)
181+
.append($('<br/>'))
182+
.append(textarea)
183+
)
184+
);
185+
186+
var modal_obj = dialog.modal({
187+
title: "Edit Tags",
188+
body: dialogform,
189+
default_button: "Cancel",
190+
buttons: {
191+
Cancel: {},
192+
Edit: { class : "btn-primary",
193+
click: function() {
194+
var old_tags = cell.metadata.tags || [];
195+
var new_tags = preprocess_input(textarea[0].value);
196+
var added_tags = array_difference(new_tags, old_tags);
197+
var removed_tags = array_difference(old_tags, new_tags);
198+
for (var i=0; i < added_tags.length; ++i) {
199+
on_add(added_tags[i]);
200+
}
201+
for (var i=0; i < removed_tags.length; ++i) {
202+
on_remove(removed_tags[i]);
203+
}
204+
}
205+
}
206+
},
207+
notebook: cell.notebook,
208+
keyboard_manager: cell.keyboard_manager,
209+
});
210+
};
211+
212+
var add_dialog_button = function(container, cell, on_add, on_remove) {
213+
var button = $('<button />')
214+
.addClass('btn btn-default btn-xs tags-dialog-btn')
215+
.text('...')
216+
.click( function() {
217+
tag_dialog(cell, on_add, on_remove);
218+
return false;
219+
});
220+
container.append(button);
221+
};
222+
223+
var add_tags_cellbar = function(div, cell) {
224+
var button_container = $(div);
225+
226+
button_container.addClass('tags_button_container');
227+
228+
var tag_container = $('<span/>').
229+
addClass('tag-container');
230+
var on_remove = remove_tag(cell, tag_container);
231+
var ok = init_tag_container(cell, tag_container, on_remove);
232+
if (!ok) {
233+
return;
234+
}
235+
button_container.append(tag_container);
236+
237+
var on_add = add_tag(cell, tag_container, on_remove);
238+
add_tag_edit(div, cell, on_add, on_remove);
239+
};
240+
241+
var register = function(notebook) {
242+
CellToolbar.register_callback('tags.edit', add_tags_cellbar);
243+
244+
var tags_preset = [];
245+
tags_preset.push('tags.edit');
246+
247+
CellToolbar.register_preset('Tags', tags_preset, notebook);
248+
249+
};
250+
return {'register' : register};
251+
});

notebook/static/notebook/js/notebook.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ define([
2828
'./celltoolbarpresets/rawcell',
2929
'./celltoolbarpresets/slideshow',
3030
'./celltoolbarpresets/attachments',
31+
'./celltoolbarpresets/tags',
3132
'./scrollmanager',
3233
'./commandpalette',
3334
'./shortcuteditor',
@@ -54,6 +55,7 @@ define([
5455
rawcell_celltoolbar,
5556
slideshow_celltoolbar,
5657
attachments_celltoolbar,
58+
tags_celltoolbar,
5759
scrollmanager,
5860
commandpalette,
5961
shortcuteditor
@@ -190,6 +192,7 @@ define([
190192
rawcell_celltoolbar.register(this);
191193
slideshow_celltoolbar.register(this);
192194
attachments_celltoolbar.register(this);
195+
tags_celltoolbar.register(this);
193196

194197
var that = this;
195198

notebook/static/notebook/less/style.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
@import "notebook.less";
99
@import "celltoolbar.less";
10+
@import "tagbar.less";
1011
@import "completer.less";
1112
@import "kernelselector.less";
1213
@import "menubar.less";
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
3+
.tags_button_container {
4+
width: 100%;
5+
display: flex;
6+
}
7+
8+
.tag-container {
9+
display: flex;
10+
flex-direction: row;
11+
flex-grow: 1;
12+
overflow: hidden;
13+
position: relative;
14+
}
15+
16+
.tag-container > * {
17+
margin: 0 4px;
18+
}
19+
20+
.remove-tag-btn {
21+
margin-left: 4px;
22+
}
23+
24+
.tags-input {
25+
display: flex;
26+
}
27+
28+
.cell-tag:last-child:after {
29+
content: "";
30+
position: absolute;
31+
right: 0;
32+
width: 40px;
33+
height: 100%;
34+
/* Fade to background color of cell toolbar */
35+
background: linear-gradient(to right, rgba(0,0,0,0), #EEE);
36+
}
37+
38+
.tags-dialog-btn {
39+
margin-right: 4px;
40+
}

0 commit comments

Comments
 (0)