123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- import {
- clear as domClear,
- delegate as domDelegate,
- query as domQuery,
- classes as domClasses,
- attr as domAttr,
- domify as domify
- } from 'min-dom';
- import {
- getBBox as getBoundingBox
- } from '../../util/Elements';
- import {
- escapeHTML
- } from '../../util/EscapeUtil';
- /**
- * Provides searching infrastructure
- */
- export default function SearchPad(canvas, eventBus, overlays, selection) {
- this._open = false;
- this._results = [];
- this._eventMaps = [];
- this._canvas = canvas;
- this._eventBus = eventBus;
- this._overlays = overlays;
- this._selection = selection;
- // setup elements
- this._container = domify(SearchPad.BOX_HTML);
- this._searchInput = domQuery(SearchPad.INPUT_SELECTOR, this._container);
- this._resultsContainer = domQuery(SearchPad.RESULTS_CONTAINER_SELECTOR, this._container);
- // attach search pad
- this._canvas.getContainer().appendChild(this._container);
- // cleanup on destroy
- eventBus.on([ 'canvas.destroy', 'diagram.destroy' ], this.close, this);
- }
- SearchPad.$inject = [
- 'canvas',
- 'eventBus',
- 'overlays',
- 'selection'
- ];
- /**
- * Binds and keeps track of all event listereners
- */
- SearchPad.prototype._bindEvents = function() {
- var self = this;
- function listen(el, selector, type, fn) {
- self._eventMaps.push({
- el: el,
- type: type,
- listener: domDelegate.bind(el, selector, type, fn)
- });
- }
- // close search on clicking anywhere outside
- listen(document, 'html', 'click', function(e) {
- self.close();
- }, true);
- // stop event from propagating and closing search
- // focus on input
- listen(this._container, SearchPad.INPUT_SELECTOR, 'click', function(e) {
- e.stopPropagation();
- e.delegateTarget.focus();
- });
- // preselect result on hover
- listen(this._container, SearchPad.RESULT_SELECTOR, 'mouseover', function(e) {
- e.stopPropagation();
- self._scrollToNode(e.delegateTarget);
- self._preselect(e.delegateTarget);
- });
- // selects desired result on mouse click
- listen(this._container, SearchPad.RESULT_SELECTOR, 'click', function(e) {
- e.stopPropagation();
- self._select(e.delegateTarget);
- });
- // prevent cursor in input from going left and right when using up/down to
- // navigate results
- listen(this._container, SearchPad.INPUT_SELECTOR, 'keydown', function(e) {
- // up
- if (e.keyCode === 38) {
- e.preventDefault();
- }
- // down
- if (e.keyCode === 40) {
- e.preventDefault();
- }
- });
- // handle keyboard input
- listen(this._container, SearchPad.INPUT_SELECTOR, 'keyup', function(e) {
- // escape
- if (e.keyCode === 27) {
- return self.close();
- }
- // enter
- if (e.keyCode === 13) {
- var selected = self._getCurrentResult();
- return selected ? self._select(selected) : self.close();
- }
- // up
- if (e.keyCode === 38) {
- return self._scrollToDirection(true);
- }
- // down
- if (e.keyCode === 40) {
- return self._scrollToDirection();
- }
- // left && right
- // do not search while navigating text input
- if (e.keyCode === 37 || e.keyCode === 39) {
- return;
- }
- // anything else
- self._search(e.delegateTarget.value);
- });
- };
- /**
- * Unbinds all previously established listeners
- */
- SearchPad.prototype._unbindEvents = function() {
- this._eventMaps.forEach(function(m) {
- domDelegate.unbind(m.el, m.type, m.listener);
- });
- };
- /**
- * Performs a search for the given pattern.
- *
- * @param {String} pattern
- */
- SearchPad.prototype._search = function(pattern) {
- var self = this;
- this._clearResults();
- // do not search on empty query
- if (!pattern || pattern === '') {
- return;
- }
- var searchResults = this._searchProvider.find(pattern);
- if (!searchResults.length) {
- return;
- }
- // append new results
- searchResults.forEach(function(result) {
- var id = result.element.id;
- var node = self._createResultNode(result, id);
- self._results[id] = {
- element: result.element,
- node: node
- };
- });
- // preselect first result
- var node = domQuery(SearchPad.RESULT_SELECTOR, this._resultsContainer);
- this._scrollToNode(node);
- this._preselect(node);
- };
- /**
- * Navigate to the previous/next result. Defaults to next result.
- * @param {Boolean} previous
- */
- SearchPad.prototype._scrollToDirection = function(previous) {
- var selected = this._getCurrentResult();
- if (!selected) {
- return;
- }
- var node = previous ? selected.previousElementSibling : selected.nextElementSibling;
- if (node) {
- this._scrollToNode(node);
- this._preselect(node);
- }
- };
- /**
- * Scroll to the node if it is not visible.
- *
- * @param {Element} node
- */
- SearchPad.prototype._scrollToNode = function(node) {
- if (!node || node === this._getCurrentResult()) {
- return;
- }
- var nodeOffset = node.offsetTop;
- var containerScroll = this._resultsContainer.scrollTop;
- var bottomScroll = nodeOffset - this._resultsContainer.clientHeight + node.clientHeight;
- if (nodeOffset < containerScroll) {
- this._resultsContainer.scrollTop = nodeOffset;
- } else if (containerScroll < bottomScroll) {
- this._resultsContainer.scrollTop = bottomScroll;
- }
- };
- /**
- * Clears all results data.
- */
- SearchPad.prototype._clearResults = function() {
- domClear(this._resultsContainer);
- this._results = [];
- this._resetOverlay();
- this._eventBus.fire('searchPad.cleared');
- };
- /**
- * Get currently selected result.
- *
- * @return {Element}
- */
- SearchPad.prototype._getCurrentResult = function() {
- return domQuery(SearchPad.RESULT_SELECTED_SELECTOR, this._resultsContainer);
- };
- /**
- * Create result DOM element within results container
- * that corresponds to a search result.
- *
- * 'result' : one of the elements returned by SearchProvider
- * 'id' : id attribute value to assign to the new DOM node
- * return : created DOM element
- *
- * @param {SearchResult} result
- * @param {String} id
- * @return {Element}
- */
- SearchPad.prototype._createResultNode = function(result, id) {
- var node = domify(SearchPad.RESULT_HTML);
- // create only if available
- if (result.primaryTokens.length > 0) {
- createInnerTextNode(node, result.primaryTokens, SearchPad.RESULT_PRIMARY_HTML);
- }
- // secondary tokens (represent element ID) are allways available
- createInnerTextNode(node, result.secondaryTokens, SearchPad.RESULT_SECONDARY_HTML);
- domAttr(node, SearchPad.RESULT_ID_ATTRIBUTE, id);
- this._resultsContainer.appendChild(node);
- return node;
- };
- /**
- * Register search element provider.
- *
- * SearchProvider.find - provides search function over own elements
- * (pattern) => [{ text: <String>, element: <Element>}, ...]
- *
- * @param {SearchProvider} provider
- */
- SearchPad.prototype.registerProvider = function(provider) {
- this._searchProvider = provider;
- };
- /**
- * Open search pad.
- */
- SearchPad.prototype.open = function() {
- if (!this._searchProvider) {
- throw new Error('no search provider registered');
- }
- if (this.isOpen()) {
- return;
- }
- this._bindEvents();
- this._open = true;
- domClasses(this._container).add('open');
- this._searchInput.focus();
- this._eventBus.fire('searchPad.opened');
- };
- /**
- * Close search pad.
- */
- SearchPad.prototype.close = function() {
- if (!this.isOpen()) {
- return;
- }
- this._unbindEvents();
- this._open = false;
- domClasses(this._container).remove('open');
- this._clearResults();
- this._searchInput.value = '';
- this._searchInput.blur();
- this._resetOverlay();
- this._eventBus.fire('searchPad.closed');
- };
- /**
- * Toggles search pad on/off.
- */
- SearchPad.prototype.toggle = function() {
- this.isOpen() ? this.close() : this.open();
- };
- /**
- * Report state of search pad.
- */
- SearchPad.prototype.isOpen = function() {
- return this._open;
- };
- /**
- * Preselect result entry.
- *
- * @param {Element} element
- */
- SearchPad.prototype._preselect = function(node) {
- var selectedNode = this._getCurrentResult();
- // already selected
- if (node === selectedNode) {
- return;
- }
- // removing preselection from current node
- if (selectedNode) {
- domClasses(selectedNode).remove(SearchPad.RESULT_SELECTED_CLASS);
- }
- var id = domAttr(node, SearchPad.RESULT_ID_ATTRIBUTE);
- var element = this._results[id].element;
- domClasses(node).add(SearchPad.RESULT_SELECTED_CLASS);
- this._resetOverlay(element);
- this._centerViewbox(element);
- this._selection.select(element);
- this._eventBus.fire('searchPad.preselected', element);
- };
- /**
- * Select result node.
- *
- * @param {Element} element
- */
- SearchPad.prototype._select = function(node) {
- var id = domAttr(node, SearchPad.RESULT_ID_ATTRIBUTE);
- var element = this._results[id].element;
- this.close();
- this._resetOverlay();
- this._centerViewbox(element);
- this._selection.select(element);
- this._eventBus.fire('searchPad.selected', element);
- };
- /**
- * Center viewbox on the element middle point.
- *
- * @param {Element} element
- */
- SearchPad.prototype._centerViewbox = function(element) {
- var viewbox = this._canvas.viewbox();
- var box = getBoundingBox(element);
- var newViewbox = {
- x: (box.x + box.width/2) - viewbox.outer.width/2,
- y: (box.y + box.height/2) - viewbox.outer.height/2,
- width: viewbox.outer.width,
- height: viewbox.outer.height
- };
- this._canvas.viewbox(newViewbox);
- this._canvas.zoom(viewbox.scale);
- };
- /**
- * Reset overlay removes and, optionally, set
- * overlay to a new element.
- *
- * @param {Element} element
- */
- SearchPad.prototype._resetOverlay = function(element) {
- if (this._overlayId) {
- this._overlays.remove(this._overlayId);
- }
- if (element) {
- var box = getBoundingBox(element);
- var overlay = constructOverlay(box);
- this._overlayId = this._overlays.add(element, overlay);
- }
- };
- /**
- * Construct overlay object for the given bounding box.
- *
- * @param {BoundingBox} box
- * @return {Object}
- */
- function constructOverlay(box) {
- var offset = 6;
- var w = box.width + offset * 2;
- var h = box.height + offset * 2;
- var styles = [
- 'width: '+ w +'px',
- 'height: '+ h + 'px'
- ].join('; ');
- return {
- position: {
- bottom: h - offset,
- right: w - offset
- },
- show: true,
- html: '<div style="' + styles + '" class="' + SearchPad.OVERLAY_CLASS + '"></div>'
- };
- }
- /**
- * Creates and appends child node from result tokens and HTML template.
- *
- * @param {Element} node
- * @param {Array<Object>} tokens
- * @param {String} template
- */
- function createInnerTextNode(parentNode, tokens, template) {
- var text = createHtmlText(tokens);
- var childNode = domify(template);
- childNode.innerHTML = text;
- parentNode.appendChild(childNode);
- }
- /**
- * Create internal HTML markup from result tokens.
- * Caters for highlighting pattern matched tokens.
- *
- * @param {Array<Object>} tokens
- * @return {String}
- */
- function createHtmlText(tokens) {
- var htmlText = '';
- tokens.forEach(function(t) {
- if (t.matched) {
- htmlText += '<strong class="' + SearchPad.RESULT_HIGHLIGHT_CLASS + '">' + escapeHTML(t.matched) + '</strong>';
- } else {
- htmlText += escapeHTML(t.normal);
- }
- });
- return htmlText !== '' ? htmlText : null;
- }
- /**
- * CONSTANTS
- */
- SearchPad.CONTAINER_SELECTOR = '.djs-search-container';
- SearchPad.INPUT_SELECTOR = '.djs-search-input input';
- SearchPad.RESULTS_CONTAINER_SELECTOR = '.djs-search-results';
- SearchPad.RESULT_SELECTOR = '.djs-search-result';
- SearchPad.RESULT_SELECTED_CLASS = 'djs-search-result-selected';
- SearchPad.RESULT_SELECTED_SELECTOR = '.' + SearchPad.RESULT_SELECTED_CLASS;
- SearchPad.RESULT_ID_ATTRIBUTE = 'data-result-id';
- SearchPad.RESULT_HIGHLIGHT_CLASS = 'djs-search-highlight';
- SearchPad.OVERLAY_CLASS = 'djs-search-overlay';
- SearchPad.BOX_HTML =
- '<div class="djs-search-container djs-draggable djs-scrollable">' +
- '<div class="djs-search-input">' +
- '<input type="text"/>' +
- '</div>' +
- '<div class="djs-search-results"></div>' +
- '</div>';
- SearchPad.RESULT_HTML =
- '<div class="djs-search-result"></div>';
- SearchPad.RESULT_PRIMARY_HTML =
- '<div class="djs-search-result-primary"></div>';
- SearchPad.RESULT_SECONDARY_HTML =
- '<p class="djs-search-result-secondary"></p>';
|