1/**
2 * Tags plugin.
3 *
4 * - Set tags via dialog
5 * - Toggle hidden tags
6 * - Stateless filter
7 *
8 * TODO:
9 *
10 * - Add hiddenTags to viewState of page
11 * - Export to PDF ignores current tags
12 * - Sync hiddenTags with removed tags
13 */
14Draw.loadPlugin(function(editorUi)
15{
16	var div = document.createElement('div');
17
18	// Adds resource for action
19	mxResources.parse('hiddenTags=Hidden Tags');
20
21	// Adds action
22	editorUi.actions.addAction('hiddenTags...', function()
23	{
24		if (editorUi.hiddenTagsWindow == null)
25		{
26			editorUi.hiddenTagsWindow = new HiddenTagsWindow(editorUi, document.body.offsetWidth - 380, 120, 300, 240);
27			editorUi.hiddenTagsWindow.window.addListener('show', function()
28			{
29				editorUi.fireEvent(new mxEventObject('hiddenTags'));
30			});
31			editorUi.hiddenTagsWindow.window.addListener('hide', function()
32			{
33				editorUi.fireEvent(new mxEventObject('hiddenTags'));
34			});
35			editorUi.hiddenTagsWindow.window.setVisible(true);
36			editorUi.fireEvent(new mxEventObject('hiddenTags'));
37		}
38		else
39		{
40			editorUi.hiddenTagsWindow.window.setVisible(!editorUi.hiddenTagsWindow.window.isVisible());
41		}
42	});
43
44	var menu = editorUi.menus.get('extras');
45	var oldFunct = menu.funct;
46
47	menu.funct = function(menu, parent)
48	{
49		oldFunct.apply(this, arguments);
50
51		editorUi.menus.addMenuItems(menu, ['-', 'hiddenTags'], parent);
52	};
53
54	var HiddenTagsWindow = function(editorUi, x, y, w, h)
55	{
56		var graph = editorUi.editor.graph;
57
58		var div = document.createElement('div');
59		div.style.overflow = 'hidden';
60		div.style.padding = '12px 8px 12px 8px';
61		div.style.height = 'auto';
62
63		var searchInput = document.createElement('input');
64		searchInput.setAttribute('placeholder', 'Type in the tags and press Enter to add them');
65		searchInput.setAttribute('type', 'text');
66		searchInput.style.width = '100%';
67		searchInput.style.boxSizing = 'border-box';
68		searchInput.style.fontSize = '12px';
69		searchInput.style.borderRadius = '4px';
70		searchInput.style.padding = '4px';
71		searchInput.style.marginBottom = '8px';
72		div.appendChild(searchInput);
73
74		var filterInput = searchInput.cloneNode(true);
75		filterInput.setAttribute('placeholder', 'Filter tags');
76		div.appendChild(filterInput);
77
78		var tagCloud = document.createElement('div');
79		tagCloud.style.position = 'relative';
80		tagCloud.style.fontSize = '12px';
81		tagCloud.style.height = 'auto';
82		div.appendChild(tagCloud);
83
84		var graph = editorUi.editor.graph;
85		var lastValue = null;
86
87		function getLookup(tagList)
88		{
89			var lookup = {};
90
91			for (var i = 0; i < tagList.length; i++)
92			{
93				lookup[tagList[i].toLowerCase()] = true;
94			}
95
96			return lookup;
97		};
98
99		function getAllTags()
100		{
101			return graph.getTagsForCells(graph.model.getDescendants(
102				graph.model.getRoot()));
103		};
104
105		/**
106		 * Returns true if tags exist and are all in lookup.
107		 */
108		function matchTags(tags, lookup, tagCount)
109		{
110			if (tags.length > 0)
111			{
112				var tmp = tags.toLowerCase().split(' ');
113
114				if (tmp.length > tagCount)
115				{
116					return false;
117				}
118				else
119				{
120					for (var i = 0; i < tmp.length; i++)
121					{
122						if (lookup[tmp[i]] == null)
123						{
124							return false;
125						}
126					}
127
128					return true;
129				}
130			}
131			else
132			{
133				return false;
134			}
135		};
136
137		var hiddenTags = {};
138		var hiddenTagCount = 0;
139		var graphIsCellVisible = graph.isCellVisible;
140
141		graph.isCellVisible = function(cell)
142		{
143			return graphIsCellVisible.apply(this, arguments) &&
144				(hiddenTagCount == 0 ||
145				!matchTags(graph.getTagsForCell(cell), hiddenTags, hiddenTagCount));
146		};
147
148		function setCellsVisibleForTag(tag, visible)
149		{
150			var cells = graph.getCellsForTags([tag], null, true);
151
152			// Ignores layers for selection
153			var temp = [];
154
155			for (var i = 0; i < cells.length; i++)
156			{
157				if (graph.model.isVertex(cells[i]) || graph.model.isEdge(cells[i]))
158				{
159					temp.push(cells[i]);
160				}
161			}
162
163			graph.setCellsVisible(cells, visible);
164		};
165
166		function updateSelectedTags(tags, selected, selectedColor, filter)
167		{
168			tagCloud.innerHTML = '';
169
170			var title = document.createElement('div');
171			title.style.marginBottom = '8px';
172			mxUtils.write(title, (filter != null) ? 'Select hidden tags:' : 'Or add/remove existing tags for cell(s):');
173			tagCloud.appendChild(title);
174
175			var found = 0;
176
177			for (var i = 0; i < tags.length; i++)
178			{
179				if (filter == null || tags[i].substring(0, filter.length) == filter)
180				{
181					var span = document.createElement('span');
182					span.style.display = 'inline-block';
183					span.style.padding = '6px 8px';
184					span.style.borderRadius = '6px';
185					span.style.marginBottom = '8px';
186					span.style.maxWidth = '80px';
187					span.style.overflow = 'hidden';
188					span.style.textOverflow = 'ellipsis';
189					span.style.cursor = 'pointer';
190					span.setAttribute('title', tags[i]);
191					span.style.border = '1px solid #808080';
192					mxUtils.write(span, tags[i]);
193
194					if (selected[tags[i]])
195					{
196						span.style.background = selectedColor;
197						span.style.color = '#ffffff';
198					}
199					else
200					{
201						span.style.background = (Editor.isDarkMode()) ? 'transparent' : '#ffffff';
202					}
203
204					mxEvent.addListener(span, 'click', (function(tag)
205					{
206						return function()
207						{
208							if (!selected[tag])
209							{
210								if (!graph.isSelectionEmpty())
211								{
212									graph.addTagsForCells(graph.getSelectionCells(), [tag])
213								}
214								else
215								{
216									hiddenTags[tag] = true;
217									hiddenTagCount++;
218									refreshUi();
219
220									window.setTimeout(function()
221									{
222										graph.refresh();
223									}, 0);
224								}
225							}
226							else
227							{
228								if (!graph.isSelectionEmpty())
229								{
230									graph.removeTagsForCells(graph.getSelectionCells(), [tag])
231								}
232								else
233								{
234									delete hiddenTags[tag];
235									hiddenTagCount--;
236									refreshUi();
237
238									window.setTimeout(function()
239									{
240										graph.refresh();
241									}, 0);
242								}
243							}
244						};
245					})(tags[i]));
246
247					tagCloud.appendChild(span);
248					mxUtils.write(tagCloud, ' ');
249					found++;
250				}
251			}
252
253			if (found == 0)
254			{
255				mxUtils.write(tagCloud, 'No tags found');
256			}
257		};
258
259		function updateTagCloud(tags)
260		{
261			updateSelectedTags(tags, hiddenTags, '#bb0000', filterInput.value);
262		};
263
264		function refreshUi()
265		{
266			if (graph.isSelectionEmpty())
267			{
268				updateTagCloud(getAllTags(), hiddenTags);
269				searchInput.style.display = 'none';
270				filterInput.style.display = '';
271			}
272			else
273			{
274				updateSelectedTags(getAllTags(), getLookup(graph.getCommonTagsForCells(graph.getSelectionCells())), '#2873e1');
275				searchInput.style.display = '';
276				filterInput.style.display = 'none';
277			}
278		}
279
280		refreshUi();
281
282		graph.selectionModel.addListener(mxEvent.CHANGE, function(sender, evt)
283		{
284			refreshUi();
285		});
286
287		graph.model.addListener(mxEvent.CHANGE, function(sender, evt)
288		{
289			refreshUi();
290		});
291
292		mxEvent.addListener(filterInput, 'keyup', function()
293		{
294			updateTagCloud(getAllTags());
295		});
296
297		mxEvent.addListener(searchInput, 'keyup', function(evt)
298		{
299			// Ctrl or Cmd keys
300			if (evt.keyCode == 13)
301			{
302				graph.addTagsForCells(graph.getSelectionCells(), searchInput.value.toLowerCase().split(' '));
303				searchInput.value = '';
304			}
305		});
306
307		this.window = new mxWindow(mxResources.get('hiddenTags'), div, x, y, w, null, true, true);
308		this.window.destroyOnClose = false;
309		this.window.setMaximizable(false);
310		this.window.setResizable(true);
311		this.window.setScrollable(true);
312		this.window.setClosable(true);
313		this.window.contentWrapper.style.overflowY = 'scroll';
314
315		this.window.addListener('show', mxUtils.bind(this, function()
316		{
317			this.window.fit();
318
319			if (this.window.isVisible())
320			{
321				searchInput.focus();
322
323				if (mxClient.IS_GC || mxClient.IS_FF || document.documentMode >= 5)
324				{
325					searchInput.select();
326				}
327				else
328				{
329					document.execCommand('selectAll', false, null);
330				}
331			}
332			else
333			{
334				graph.container.focus();
335			}
336		}));
337
338		this.window.setLocation = function(x, y)
339		{
340			var iw = window.innerWidth || document.body.clientWidth || document.documentElement.clientWidth;
341			var ih = window.innerHeight || document.body.clientHeight || document.documentElement.clientHeight;
342
343			x = Math.max(0, Math.min(x, iw - this.table.clientWidth));
344			y = Math.max(0, Math.min(y, ih - this.table.clientHeight - 48));
345
346			if (this.getX() != x || this.getY() != y)
347			{
348				mxWindow.prototype.setLocation.apply(this, arguments);
349			}
350		};
351
352		var resizeListener = mxUtils.bind(this, function()
353		{
354			var x = this.window.getX();
355			var y = this.window.getY();
356
357			this.window.setLocation(x, y);
358		});
359
360		mxEvent.addListener(window, 'resize', resizeListener);
361
362		this.destroy = function()
363		{
364			mxEvent.removeListener(window, 'resize', resizeListener);
365			this.window.destroy();
366		}
367	};
368
369});
370