All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.dirigible.dev-tools.ui.SearchableView.js Maven / Gradle / Ivy

There is a newer version: 10.6.27
Show newest version
/*
 * Copyright (C) 2006, 2007, 2008 Apple Inc.  All rights reserved.
 * Copyright (C) 2007 Matt Lilek ([email protected]).
 * Copyright (C) 2009 Joseph Pecoraro
 * Copyright (C) 2011 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import * as Common from '../common/common.js';

import {HistoryInput} from './HistoryInput.js';
import {Toolbar, ToolbarButton, ToolbarToggle} from './Toolbar.js';
import {createTextButton} from './UIUtils.js';
import {VBox} from './Widget.js';

/**
 * @unrestricted
 */
export class SearchableView extends VBox {
  /**
   * @param {!Searchable} searchable
   * @param {string=} settingName
   */
  constructor(searchable, settingName) {
    super(true);
    this.registerRequiredCSS('ui/searchableView.css');
    this.element[_symbol] = this;

    this._searchProvider = searchable;
    // Note: go via self.Common for globally-namespaced singletons.
    this._setting = settingName ? Common.Settings.Settings.instance().createSetting(settingName, {}) : null;
    this._replaceable = false;

    this.contentElement.createChild('slot');
    this._footerElementContainer = this.contentElement.createChild('div', 'search-bar hidden');
    this._footerElementContainer.style.order = 100;
    this._footerElement = this._footerElementContainer.createChild('div', 'toolbar-search');

    const replaceToggleToolbar = new Toolbar('replace-toggle-toolbar', this._footerElement);
    this._replaceToggleButton = new ToolbarToggle(Common.UIString.UIString('Replace'), 'mediumicon-replace');
    this._replaceToggleButton.addEventListener(ToolbarButton.Events.Click, this._toggleReplace, this);
    replaceToggleToolbar.appendToolbarItem(this._replaceToggleButton);

    const searchInputElements = this._footerElement.createChild('div', 'toolbar-search-inputs');
    const searchControlElement = searchInputElements.createChild('div', 'toolbar-search-control');

    this._searchInputElement = HistoryInput.create();
    this._searchInputElement.classList.add('search-replace');
    this._searchInputElement.id = 'search-input-field';
    this._searchInputElement.placeholder = Common.UIString.UIString('Find');
    searchControlElement.appendChild(this._searchInputElement);

    this._matchesElement = searchControlElement.createChild('label', 'search-results-matches');
    this._matchesElement.setAttribute('for', 'search-input-field');

    const searchNavigationElement = searchControlElement.createChild('div', 'toolbar-search-navigation-controls');

    this._searchNavigationPrevElement =
        searchNavigationElement.createChild('div', 'toolbar-search-navigation toolbar-search-navigation-prev');
    this._searchNavigationPrevElement.addEventListener('click', this._onPrevButtonSearch.bind(this), false);
    this._searchNavigationPrevElement.title = Common.UIString.UIString('Search previous');

    this._searchNavigationNextElement =
        searchNavigationElement.createChild('div', 'toolbar-search-navigation toolbar-search-navigation-next');
    this._searchNavigationNextElement.addEventListener('click', this._onNextButtonSearch.bind(this), false);
    this._searchNavigationNextElement.title = Common.UIString.UIString('Search next');

    this._searchInputElement.addEventListener('keydown', this._onSearchKeyDown.bind(this), true);
    this._searchInputElement.addEventListener('input', this._onInput.bind(this), false);

    this._replaceInputElement =
        searchInputElements.createChild('input', 'search-replace toolbar-replace-control hidden');
    this._replaceInputElement.addEventListener('keydown', this._onReplaceKeyDown.bind(this), true);
    this._replaceInputElement.placeholder = Common.UIString.UIString('Replace');

    this._buttonsContainer = this._footerElement.createChild('div', 'toolbar-search-buttons');
    const firstRowButtons = this._buttonsContainer.createChild('div', 'first-row-buttons');

    const toolbar = new Toolbar('toolbar-search-options', firstRowButtons);

    if (this._searchProvider.supportsCaseSensitiveSearch()) {
      this._caseSensitiveButton = new ToolbarToggle(Common.UIString.UIString('Match Case'));
      this._caseSensitiveButton.setText('Aa');
      this._caseSensitiveButton.addEventListener(ToolbarButton.Events.Click, this._toggleCaseSensitiveSearch, this);
      toolbar.appendToolbarItem(this._caseSensitiveButton);
    }

    if (this._searchProvider.supportsRegexSearch()) {
      this._regexButton = new ToolbarToggle(Common.UIString.UIString('Use Regular Expression'));
      this._regexButton.setText('.*');
      this._regexButton.addEventListener(ToolbarButton.Events.Click, this._toggleRegexSearch, this);
      toolbar.appendToolbarItem(this._regexButton);
    }

    const cancelButtonElement =
        createTextButton(Common.UIString.UIString('Cancel'), this.closeSearch.bind(this), 'search-action-button');
    firstRowButtons.appendChild(cancelButtonElement);

    this._secondRowButtons = this._buttonsContainer.createChild('div', 'second-row-buttons hidden');

    this._replaceButtonElement =
        createTextButton(Common.UIString.UIString('Replace'), this._replace.bind(this), 'search-action-button');
    this._replaceButtonElement.disabled = true;
    this._secondRowButtons.appendChild(this._replaceButtonElement);

    this._replaceAllButtonElement =
        createTextButton(Common.UIString.UIString('Replace all'), this._replaceAll.bind(this), 'search-action-button');
    this._secondRowButtons.appendChild(this._replaceAllButtonElement);
    this._replaceAllButtonElement.disabled = true;

    this._minimalSearchQuerySize = 3;
    this._loadSetting();
  }

  /**
   * @param {?Element} element
   * @return {?SearchableView}
   */
  static fromElement(element) {
    let view = null;
    while (element && !view) {
      view = element[_symbol];
      element = element.parentElementOrShadowHost();
    }
    return view;
  }

  _toggleCaseSensitiveSearch() {
    this._caseSensitiveButton.setToggled(!this._caseSensitiveButton.toggled());
    this._saveSetting();
    this._performSearch(false, true);
  }

  _toggleRegexSearch() {
    this._regexButton.setToggled(!this._regexButton.toggled());
    this._saveSetting();
    this._performSearch(false, true);
  }

  _toggleReplace() {
    this._replaceToggleButton.setToggled(!this._replaceToggleButton.toggled());
    this._updateSecondRowVisibility();
  }

  _saveSetting() {
    if (!this._setting) {
      return;
    }
    const settingValue = this._setting.get() || {};
    settingValue.caseSensitive = this._caseSensitiveButton.toggled();
    settingValue.isRegex = this._regexButton.toggled();
    this._setting.set(settingValue);
  }

  _loadSetting() {
    const settingValue = this._setting ? (this._setting.get() || {}) : {};
    if (this._searchProvider.supportsCaseSensitiveSearch()) {
      this._caseSensitiveButton.setToggled(!!settingValue.caseSensitive);
    }
    if (this._searchProvider.supportsRegexSearch()) {
      this._regexButton.setToggled(!!settingValue.isRegex);
    }
  }

  /**
   * @param {number} minimalSearchQuerySize
   */
  setMinimalSearchQuerySize(minimalSearchQuerySize) {
    this._minimalSearchQuerySize = minimalSearchQuerySize;
  }

  /**
   * @param {string} placeholder
   */
  setPlaceholder(placeholder) {
    this._searchInputElement.placeholder = placeholder;
  }

  /**
   * @param {boolean} replaceable
   */
  setReplaceable(replaceable) {
    this._replaceable = replaceable;
  }

  /**
   * @param {number} matches
   * @suppress {checkTypes}
   */
  updateSearchMatchesCount(matches) {
    if (this._searchProvider.currentSearchMatches === matches) {
      return;
    }
    this._searchProvider.currentSearchMatches = matches;
    this._updateSearchMatchesCountAndCurrentMatchIndex(this._searchProvider.currentQuery ? matches : 0, -1);
  }

  /**
   * @param {number} currentMatchIndex
   * @suppress {checkTypes}
   */
  updateCurrentMatchIndex(currentMatchIndex) {
    this._updateSearchMatchesCountAndCurrentMatchIndex(this._searchProvider.currentSearchMatches, currentMatchIndex);
  }

  /**
   * @return {boolean}
   */
  isSearchVisible() {
    return this._searchIsVisible;
  }

  closeSearch() {
    this.cancelSearch();
    if (this._footerElementContainer.hasFocus()) {
      this.focus();
    }
  }

  _toggleSearchBar(toggled) {
    this._footerElementContainer.classList.toggle('hidden', !toggled);
    this.doResize();
  }

  cancelSearch() {
    if (!this._searchIsVisible) {
      return;
    }
    this.resetSearch();
    delete this._searchIsVisible;
    this._toggleSearchBar(false);
  }

  resetSearch() {
    this._clearSearch();
    this._updateReplaceVisibility();
    this._matchesElement.textContent = '';
  }

  refreshSearch() {
    if (!this._searchIsVisible) {
      return;
    }
    this.resetSearch();
    this._performSearch(false, false);
  }

  /**
   * @return {boolean}
   */
  handleFindNextShortcut() {
    if (!this._searchIsVisible) {
      return false;
    }
    this._searchProvider.jumpToNextSearchResult();
    return true;
  }

  /**
   * @return {boolean}
   */
  handleFindPreviousShortcut() {
    if (!this._searchIsVisible) {
      return false;
    }
    this._searchProvider.jumpToPreviousSearchResult();
    return true;
  }

  /**
   * @return {boolean}
   */
  handleFindShortcut() {
    this.showSearchField();
    return true;
  }

  /**
   * @return {boolean}
   */
  handleCancelSearchShortcut() {
    if (!this._searchIsVisible) {
      return false;
    }
    this.closeSearch();
    return true;
  }

  /**
   * @param {boolean} enabled
   */
  _updateSearchNavigationButtonState(enabled) {
    this._replaceButtonElement.disabled = !enabled;
    this._replaceAllButtonElement.disabled = !enabled;
    this._searchNavigationPrevElement.classList.toggle('enabled', enabled);
    this._searchNavigationNextElement.classList.toggle('enabled', enabled);
  }

  /**
   * @param {number} matches
   * @param {number} currentMatchIndex
   */
  _updateSearchMatchesCountAndCurrentMatchIndex(matches, currentMatchIndex) {
    if (!this._currentQuery) {
      this._matchesElement.textContent = '';
    } else if (matches === 0 || currentMatchIndex >= 0) {
      this._matchesElement.textContent = Common.UIString.UIString('%d of %d', currentMatchIndex + 1, matches);
    } else if (matches === 1) {
      this._matchesElement.textContent = Common.UIString.UIString('1 match');
    } else {
      this._matchesElement.textContent = Common.UIString.UIString('%d matches', matches);
    }
    this._updateSearchNavigationButtonState(matches > 0);
  }

  showSearchField() {
    if (this._searchIsVisible) {
      this.cancelSearch();
    }

    let queryCandidate;
    if (!this._searchInputElement.hasFocus()) {
      const selection = self.UI.inspectorView.element.window().getSelection();
      if (selection.rangeCount) {
        queryCandidate = selection.toString().replace(/\r?\n.*/, '');
      }
    }

    this._toggleSearchBar(true);
    this._updateReplaceVisibility();
    if (queryCandidate) {
      this._searchInputElement.value = queryCandidate;
    }
    this._performSearch(false, false);
    this._searchInputElement.focus();
    this._searchInputElement.select();
    this._searchIsVisible = true;
  }

  _updateReplaceVisibility() {
    this._replaceToggleButton.setVisible(this._replaceable);
    if (!this._replaceable) {
      this._replaceToggleButton.setToggled(false);
      this._updateSecondRowVisibility();
    }
  }

  /**
   * @param {!Event} event
   */
  _onSearchKeyDown(event) {
    if (isEscKey(event)) {
      this.closeSearch();
      event.consume(true);
      return;
    }
    if (!isEnterKey(event)) {
      return;
    }

    if (!this._currentQuery) {
      this._performSearch(true, true, event.shiftKey);
    } else {
      this._jumpToNextSearchResult(event.shiftKey);
    }
  }

  /**
   * @param {!Event} event
   */
  _onReplaceKeyDown(event) {
    if (isEnterKey(event)) {
      this._replace();
    }
  }

  /**
   * @param {boolean=} isBackwardSearch
   */
  _jumpToNextSearchResult(isBackwardSearch) {
    if (!this._currentQuery) {
      return;
    }

    if (isBackwardSearch) {
      this._searchProvider.jumpToPreviousSearchResult();
    } else {
      this._searchProvider.jumpToNextSearchResult();
    }
  }

  _onNextButtonSearch(event) {
    if (!this._searchNavigationNextElement.classList.contains('enabled')) {
      return;
    }
    this._jumpToNextSearchResult();
    this._searchInputElement.focus();
  }

  _onPrevButtonSearch(event) {
    if (!this._searchNavigationPrevElement.classList.contains('enabled')) {
      return;
    }
    this._jumpToNextSearchResult(true);
    this._searchInputElement.focus();
  }

  _onFindClick(event) {
    if (!this._currentQuery) {
      this._performSearch(true, true);
    } else {
      this._jumpToNextSearchResult();
    }
    this._searchInputElement.focus();
  }

  _onPreviousClick(event) {
    if (!this._currentQuery) {
      this._performSearch(true, true, true);
    } else {
      this._jumpToNextSearchResult(true);
    }
    this._searchInputElement.focus();
  }

  /** @suppress {checkTypes} */
  _clearSearch() {
    delete this._currentQuery;
    if (!!this._searchProvider.currentQuery) {
      delete this._searchProvider.currentQuery;
      this._searchProvider.searchCanceled();
    }
    this._updateSearchMatchesCountAndCurrentMatchIndex(0, -1);
  }

  /**
   * @param {boolean} forceSearch
   * @param {boolean} shouldJump
   * @param {boolean=} jumpBackwards
   * @suppress {checkTypes}
   */
  _performSearch(forceSearch, shouldJump, jumpBackwards) {
    const query = this._searchInputElement.value;
    if (!query || (!forceSearch && query.length < this._minimalSearchQuerySize && !this._currentQuery)) {
      this._clearSearch();
      return;
    }

    this._currentQuery = query;
    this._searchProvider.currentQuery = query;

    const searchConfig = this._currentSearchConfig();
    this._searchProvider.performSearch(searchConfig, shouldJump, jumpBackwards);
  }

  /**
   * @return {!SearchConfig}
   */
  _currentSearchConfig() {
    const query = this._searchInputElement.value;
    const caseSensitive = this._caseSensitiveButton ? this._caseSensitiveButton.toggled() : false;
    const isRegex = this._regexButton ? this._regexButton.toggled() : false;
    return new SearchConfig(query, caseSensitive, isRegex);
  }

  _updateSecondRowVisibility() {
    const secondRowVisible = this._replaceToggleButton.toggled();
    this._footerElementContainer.classList.toggle('replaceable', secondRowVisible);
    this._secondRowButtons.classList.toggle('hidden', !secondRowVisible);
    this._replaceInputElement.classList.toggle('hidden', !secondRowVisible);

    if (secondRowVisible) {
      this._replaceInputElement.focus();
    } else {
      this._searchInputElement.focus();
    }
    this.doResize();
  }

  _replace() {
    const searchConfig = this._currentSearchConfig();
    /** @type {!Replaceable} */ (this._searchProvider)
        .replaceSelectionWith(searchConfig, this._replaceInputElement.value);
    delete this._currentQuery;
    this._performSearch(true, true);
  }

  _replaceAll() {
    const searchConfig = this._currentSearchConfig();
    /** @type {!Replaceable} */ (this._searchProvider).replaceAllWith(searchConfig, this._replaceInputElement.value);
  }

  /**
   * @param {!Event} event
   */
  _onInput(event) {
    if (this._valueChangedTimeoutId) {
      clearTimeout(this._valueChangedTimeoutId);
    }
    const timeout = this._searchInputElement.value.length < 3 ? 200 : 0;
    this._valueChangedTimeoutId = setTimeout(this._onValueChanged.bind(this), timeout);
  }

  _onValueChanged() {
    if (!this._searchIsVisible) {
      return;
    }
    delete this._valueChangedTimeoutId;
    this._performSearch(false, true);
  }
}

export const _symbol = Symbol('searchableView');


/**
 * @interface
 */
export class Searchable {
  searchCanceled() {
  }

  /**
   * @param {!SearchConfig} searchConfig
   * @param {boolean} shouldJump
   * @param {boolean=} jumpBackwards
   */
  performSearch(searchConfig, shouldJump, jumpBackwards) {
  }

  jumpToNextSearchResult() {
  }

  jumpToPreviousSearchResult() {
  }

  /**
   * @return {boolean}
   */
  supportsCaseSensitiveSearch() {
  }

  /**
   * @return {boolean}
   */
  supportsRegexSearch() {}
}

/**
 * @interface
 */
export class Replaceable {
  /**
   * @param {!SearchConfig} searchConfig
   * @param {string} replacement
   */
  replaceSelectionWith(searchConfig, replacement) {
  }

  /**
   * @param {!SearchConfig} searchConfig
   * @param {string} replacement
   */
  replaceAllWith(searchConfig, replacement) {}
}

/**
 * @unrestricted
 */
export class SearchConfig {
  /**
   * @param {string} query
   * @param {boolean} caseSensitive
   * @param {boolean} isRegex
   */
  constructor(query, caseSensitive, isRegex) {
    this.query = query;
    this.caseSensitive = caseSensitive;
    this.isRegex = isRegex;
  }

  /**
   * @param {boolean=} global
   * @return {!RegExp}
   */
  toSearchRegex(global) {
    let modifiers = this.caseSensitive ? '' : 'i';
    if (global) {
      modifiers += 'g';
    }
    const query = this.isRegex ? '/' + this.query + '/' : this.query;

    let regex;

    // First try creating regex if user knows the / / hint.
    try {
      if (/^\/.+\/$/.test(query)) {
        regex = new RegExp(query.substring(1, query.length - 1), modifiers);
        regex.__fromRegExpQuery = true;
      }
    } catch (e) {
      // Silent catch.
    }

    // Otherwise just do a plain text search.
    if (!regex) {
      regex = createPlainTextSearchRegex(query, modifiers);
    }

    return regex;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy