HiddenTagsWindow = function(editorUi, x, y, w, h)
	{
		var graph = editorUi.editor.graph;
		var propertyName = 'tags';

		var div = document.createElement('div');
		div.style.overflow = 'hidden';
		div.style.padding = '12px 8px 12px 8px';
		div.style.height = 'auto';
		
		var searchInput = document.createElement('input');
		searchInput.setAttribute('placeholder', 'Type in the tags and press Enter to add them');
		searchInput.setAttribute('type', 'text');
		searchInput.style.width = '100%';
		searchInput.style.boxSizing = 'border-box';
		searchInput.style.fontSize = '12px';
		searchInput.style.borderRadius = '4px';
		searchInput.style.padding = '4px';
		searchInput.style.marginBottom = '8px';
		div.appendChild(searchInput);

		var filterInput = searchInput.cloneNode(true);
		filterInput.setAttribute('placeholder', 'Filter tags');
		div.appendChild(filterInput);

		var tagCloud = document.createElement('div');
		tagCloud.style.position = 'relative';
		tagCloud.style.fontSize = '12px';
		tagCloud.style.height = 'auto';
		div.appendChild(tagCloud);

		var graph = editorUi.editor.graph;
		var lastValue = null;
		
		function getTagsForCell(cell)
		{
			return graph.getAttributeForCell(cell, propertyName, '');
		};

		function getAllTagsForCells(cells)
		{
			var tokens = [];
			var temp = {};
			
			for (var i = 0; i < cells.length; i++)
			{
				var tags = getTagsForCell(cells[i]);

				if (tags.length > 0)
				{
					var t = tags.toLowerCase().split(' ');
					
					for (var j = 0; j < t.length; j++)
					{
						if (temp[t[j]] == null)
						{
							temp[t[j]] = true;
							tokens.push(t[j]);
						}
					}
				}
			}
			
			tokens.sort();
			
			return tokens;
		};
		
		function getCommonTagsForCells(cells)
		{
			var commonTokens = null;
			var validTags = [];
			
			for (var i = 0; i < cells.length; i++)
			{
				var tags = getTagsForCell(cells[i]);
				validTags = [];

				if (tags.length > 0)
				{
					var tokens = tags.toLowerCase().split(' ');
					var temp = {};
					
					for (var j = 0; j < tokens.length; j++)
					{
						if (commonTokens == null || commonTokens[tokens[j]] != null)
						{
							temp[tokens[j]] = true;
							validTags.push(tokens[j]);
						}
					}
					
					commonTokens = temp;
				}
				else
				{
					return [];
				}
			}
		
			return validTags;
		};
		
		function getLookup(tagList)
		{
			var lookup = {};
			
			for (var i = 0; i < tagList.length; i++)
			{
				lookup[tagList[i].toLowerCase()] = true;
			}
			
			return lookup;
		};
		
		function getAllTags()
		{
			return getAllTagsForCells(graph.model.getDescendants(
				graph.model.getRoot()));
		};

		/**
		 * Returns true if tags exist and are all in lookup.
		 */
		function matchTags(tags, lookup, tagCount)
		{
			if (tags.length > 0)
			{
				var tmp = tags.toLowerCase().split(' ');
				
				if (tmp.length > tagCount)
				{
					return false;
				}
				else
				{
					for (var i = 0; i < tmp.length; i++)
					{
						if (lookup[tmp[i]] == null)
						{
							return false;
						}
					}
					
					return true;
				}
			}
			else
			{
				return false;
			}
		};
		
		var hiddenTags = {};
		var hiddenTagCount = 0;
		var graphIsCellVisible = graph.isCellVisible;

		graph.isCellVisible = function(cell)
		{
			return graphIsCellVisible.apply(this, arguments) &&
				(hiddenTagCount == 0 ||
				!matchTags(getTagsForCell(cell), hiddenTags, hiddenTagCount));
		};
		
		function setCellsVisibleForTag(tag, visible)
		{
			var cells = graph.getCellsForTags([tag], null, propertyName, true);
			
			// Ignores layers for selection
			var temp = [];
			
			for (var i = 0; i < cells.length; i++)
			{
				if (graph.model.isVertex(cells[i]) || graph.model.isEdge(cells[i]))
				{
					temp.push(cells[i]);
				}
			}
			
			graph.setCellsVisible(cells, visible);
		};

		function updateSelectedTags(tags, selected, selectedColor, filter)
		{
			tagCloud.innerHTML = '';
			
			var title = document.createElement('div');
			title.style.marginBottom = '8px';
			mxUtils.write(title, (filter != null) ? 'Select hidden tags:' : 'Or add/remove existing tags for cell(s):');
			tagCloud.appendChild(title);
			
			var found = 0;
			
			for (var i = 0; i < tags.length; i++)
			{
				if (filter == null || tags[i].substring(0, filter.length) == filter)
				{
					var span = document.createElement('span');
					span.style.display = 'inline-block';
					span.style.padding = '6px 8px';
					span.style.borderRadius = '6px';
					span.style.marginBottom = '8px';
					span.style.maxWidth = '80px';
					span.style.overflow = 'hidden';
					span.style.textOverflow = 'ellipsis';
					span.style.cursor = 'pointer';
					span.setAttribute('title', tags[i]);
					span.style.border = '1px solid #808080';
					mxUtils.write(span, tags[i]);
					
					if (selected[tags[i]])
					{
						span.style.background = selectedColor;
						span.style.color = '#ffffff';
					}
					else
					{
						span.style.background = (uiTheme == 'dark') ? 'transparent' : '#ffffff';
					}
					
					mxEvent.addListener(span, 'click', (function(tag)
					{
						return function()
						{
							if (!selected[tag])
							{
								if (!graph.isSelectionEmpty())
								{
									addTagsToCells(graph.getSelectionCells(), [tag])
								}
								else
								{
									hiddenTags[tag] = true;
									hiddenTagCount++;
									refreshUi();
									
									window.setTimeout(function()
									{
										graph.refresh();
									}, 0);
								}
							}
							else
							{
								if (!graph.isSelectionEmpty())
								{
									removeTagsFromCells(graph.getSelectionCells(), [tag])
								}
								else
								{
									delete hiddenTags[tag];
									hiddenTagCount--;
									refreshUi();
									
									window.setTimeout(function()
									{
										graph.refresh();
									}, 0);
								}
							}
						};
					})(tags[i]));
					
					tagCloud.appendChild(span);
					mxUtils.write(tagCloud, ' ');
					found++;
				}
			}

			if (found == 0)
			{
				mxUtils.write(tagCloud, 'No tags found');
			}
		};
		
		function updateTagCloud(tags)
		{
			updateSelectedTags(tags, hiddenTags, '#bb0000', filterInput.value);
		};
		
		function refreshUi()
		{
			if (graph.isSelectionEmpty())
			{
				updateTagCloud(getAllTags(), hiddenTags);
				searchInput.style.display = 'none';
				filterInput.style.display = '';
			}
			else
			{
				updateSelectedTags(getAllTags(), getLookup(getCommonTagsForCells(graph.getSelectionCells())), '#2873e1');
				searchInput.style.display = '';
				filterInput.style.display = 'none';
			}
		}
		
		refreshUi();
		
		function addTagsToCells(cells, tagList)
		{
			if (cells.length > 0 && tagList.length > 0)
			{
				graph.model.beginUpdate();
				
				try
				{
					for (var i = 0; i < cells.length; i++)
					{
						var temp = getTagsForCell(cells[i]);
						var tags = temp.toLowerCase().split(' ');
						
						for (var j = 0; j < tagList.length; j++)
						{
							var tag = tagList[j];
							var changed = false;
		
							if (tags.length == 0 || mxUtils.indexOf(tags, tag) < 0)
							{
								temp = (temp.length > 0) ? temp + ' ' + tag : tag;
								changed = true;
							}
						}
						
						if (changed)
						{
							graph.setAttributeForCell(cells[i], 'tags', temp);
						}
					}
				}
				finally
				{
					graph.model.endUpdate();
				}
			}
		};

		function removeTagsFromCells(cells, tagList)
		{
			if (cells.length > 0 && tagList.length > 0)
			{
				graph.model.beginUpdate();
				
				try
				{
					for (var i = 0; i < cells.length; i++)
					{
						var tags = getTagsForCell(cells[i]);
						
						if (tags.length > 0)
						{
							var tokens = tags.split(' ');
							var changed = false;
							
							for (var j = 0; j < tagList.length; j++)
							{
								var idx = mxUtils.indexOf(tokens, tagList[j]);
								
								if (idx >= 0)
								{
									tokens.splice(idx, 1);
									changed = true;
								}
							}

							if (changed)
							{
								graph.setAttributeForCell(cells[i], 'tags', tokens.join(' '));
							}
						}
					}
				}
				finally
				{
					graph.model.endUpdate();
				}
			}
		};
		
		graph.selectionModel.addListener(mxEvent.EVENT_CHANGE, function(sender, evt)
		{
			refreshUi();
		});
		
		graph.model.addListener(mxEvent.EVENT_CHANGE, function(sender, evt)
		{
			refreshUi();
		});

		mxEvent.addListener(filterInput, 'keyup', function()
		{
			updateTagCloud(getAllTags());
		});
		
		mxEvent.addListener(searchInput, 'keyup', function(evt)
		{
			// Ctrl or Cmd keys
			if (evt.keyCode == 13)
			{
				addTagsToCells(graph.getSelectionCells(), searchInput.value.toLowerCase().split(' '));
				searchInput.value = '';
			}
		});

		this.window = new mxWindow(mxResources.get('hiddenTags'), div, x, y, w, null, true, true);
		this.window.destroyOnClose = false;
		this.window.setMaximizable(false);
		this.window.setResizable(true);
		this.window.setScrollable(true);
		this.window.setClosable(true);
		this.window.contentWrapper.style.overflowY = 'scroll';
		
		this.window.addListener('show', mxUtils.bind(this, function()
		{
			this.window.fit();
			
			if (this.window.isVisible())
			{
				searchInput.focus();
				
				if (mxClient.IS_GC || mxClient.IS_FF || document.documentMode >= 5 || mxClient.IS_QUIRKS)
				{
					searchInput.select();
				}
				else
				{
					document.execCommand('selectAll', false, null);
				}
			}
			else
			{
				graph.container.focus();
			}
		}));
		
		this.window.setLocation = function(x, y)
		{
			var iw = window.innerWidth || document.body.clientWidth || document.documentElement.clientWidth;
			var ih = window.innerHeight || document.body.clientHeight || document.documentElement.clientHeight;
			
			x = Math.max(0, Math.min(x, iw - this.table.clientWidth));
			y = Math.max(0, Math.min(y, ih - this.table.clientHeight - 48));

			if (this.getX() != x || this.getY() != y)
			{
				mxWindow.prototype.setLocation.apply(this, arguments);
			}
		};
		
		var resizeListener = mxUtils.bind(this, function()
		{
			var x = this.window.getX();
			var y = this.window.getY();
			
			this.window.setLocation(x, y);
		});
		
		mxEvent.addListener(window, 'resize', resizeListener);

		this.destroy = function()
		{
			mxEvent.removeListener(window, 'resize', resizeListener);
			this.window.destroy();
		}
	}
Tags plugin.

- Set tags via dialog
- Toggle hidden tags
- Stateless filter

TODO:

- Add hiddenTags to viewState of page
- Export to PDF ignores current tags
- Sync hiddenTags with removed tags