Repository URL to install this package:
|
Version:
0.0.15 ▾
|
import * as tslib_1 from "tslib";
import { oneStorage } from '@skava/persistence';
import debounce from 'lodash/debounce';
import uniq from 'lodash/uniq';
import serialize from 'serialize-javascript';
import { isArray, isObj, isSafe, isString, isNonEmptyString } from 'exotic';
import { observable, action, computed } from 'xmobx/mobx';
import { oneRouter } from '@skava/router';
// --- container
import { ObservableContainer } from '@skava/packages/libraries/observable-container';
import { searchBaseUrl } from 'src/bootstrap/api/config';
// import { stateForSearch } from 'src/views/organisms/ConsentPopup'
import { searchSchema } from '@skava/packages/core/schemas';
import { defaultSuggestionList, fixture, wording } from './fixture';
import flattenSnap from './transform';
import { searchSuggestionBindings } from './bindings';
const SEARCH_SUGGESTIONS_RECENT_MAX = 5;
// for compat
const fromChildParentToPipes = (recommendation) => {
const { child, parent } = recommendation;
// currently always is required, or failure
if (child !== '' && parent !== '') {
return recommendation.parent + '|||' + recommendation.child;
}
if (parent) {
return recommendation.parent;
}
return '';
};
const transformSearchRecommendations = (response) => {
// coerce? gql always should always default
const gql = response.data.suggestion;
const list = gql.suggestions.map(fromChildParentToPipes);
// @todo optimize & fix on gql side?
const uniqList = uniq(list);
return uniqList;
};
// @todo this makes no sense logically... isEmpty = isNonEmpty ?
// is already in exotic... @todo @@perf
const isNonEmptyArray = (x) => isArray(x) && x.length > 0;
const isEmptySearchResponse = (response) => {
return isObj(response) && !isNonEmptyArray(response.data.suggestion.suggestions);
};
// @@perf dedupe
const isAlphanumeric = (x) => isString(x) && /[^A-Za-z0-9]+/.test(x);
const toSafeSuggestions = (search) => {
const safeSearch = serialize(search);
const suggestions = [wording.noSearchSuggestion];
return suggestions;
};
/**
* @tutorial https://jira.skava.net/confluence/display/TWL/Setting+Up+the+Search+Feature
*
* @example https://demostage.skavaone.com/skavastream/core/v5/skavastore/searchsuggestion?campaignId=2495&search=&offset=0&limit=10&locale=en_US&storeId=0
*
* @description
* this is just a simple async connection on an observable class for all our requests
* probably will get rid of it later & merge anything required into requests, easy to swap out
* it's useful here to help us define use our dynamic params to do the calls
* but it's pretty generic just from routing and stores
* could all be extracted from localStorage
*/
class SearchApi extends ObservableContainer {
constructor() {
super(...arguments);
/**
* @example search: 'shoe'
* @example {"type":"suggestion","properties":{"suggestion":[{"value":["empty something"]}]}}
*/
this.getSearchSuggestions = async (search) => {
if (search !== '') {
const response = await searchSuggestionBindings(search);
console.log('response of searchList', response);
// should be auto saved by requests...?
// @note serialize to disallow xss
const list = isEmptySearchResponse(response)
? toSafeSuggestions(search)
: transformSearchRecommendations(response);
return {
value: list,
response: response.data.suggestion.suggestions,
};
}
// this.oneStorage.set('search_suggestion_list', list)
// @todo why would someone return response here? wrong @invalid
// @todo - for tree model compat
return { value: defaultSuggestionList };
};
}
}
SearchApi.debugName = 'SearchApi';
const searchApi = new SearchApi();
/**
* === @todo move to deps ===
*/
const serializeSearchTerm = (value) => {
if (isAlphanumeric(value) === false) {
return serialize(value);
}
else {
return value.replace(/\%/gim, '');
}
};
const isSearchNameQuoted = (searchText) => isString(searchText) && searchText.startsWith('"') && searchText.endsWith('"');
const removeSearchTextQuote = (searchText) => searchText.slice(1, searchText.length - 1);
const searchNameUnquote = (searchName) => isSearchNameQuoted(searchName) ? removeSearchTextQuote(searchName) : searchName;
class SearchContainer extends ObservableContainer {
constructor() {
super();
/**
* @todo @@perf EMPTY
*/
this.inputReference = {};
this.schemaData = searchSchema(searchBaseUrl);
this.placeholder = wording.placeholder;
this.isVisible = false;
this.isActive = false;
this.suggestionList = [];
this.recentList = [];
this.label = 'keyword';
this.value = undefined;
/**
* @type { Action }
* @todo should us mobx type for array...
* @param text text on the item in recent results
* @api https://mobx.js.org/refguide/array.html
*/
this.handleRemoveRecentItem = (text, event) => {
event.preventDefault();
event.stopPropagation();
if (this.recentList.includes(text)) {
// const index = this.recentList.indexOf(text)
const success = this.recentList.remove(text);
oneStorage.set('search_recent_list', this.recentList);
console.debug('REMOVING', text, success);
}
else {
console.debug('TRIED_TO_REMOVE_NOT_AVAILABLE', text, this.recentList);
}
};
/**
* @event click
* @type {Action}
* @param text text on the item in recent results
*/
this.handleOnClickFor = (text) => (event) => {
this.value = text;
this.inputReference.observableState.value = text;
this.isVisible = false;
console.debug('[SearchContainer] handleOnClickFor: ' + text);
this.handleFromSearchSuggestionOrSubmit(text);
};
/**
* @todo weird to re-assign actions, thought this is not allowed
*/
this.updateSearchSuggestions = debounce(this.updateSearchSuggestions.bind(this), 600);
this.afterCreate();
}
afterCreate() {
/**
* @todo @fixme should change transform in @@graphql @@haircut
*/
const initialSuggestion = fixture;
this.recentList = oneStorage.get('search_recent_list') || [];
this.setLabelValue(initialSuggestion);
}
setLabelValue(labelValue) {
const { label, value } = flattenSnap(labelValue);
this.label = label;
this.value = value;
}
/**
* @type {Action}
* @param {String} value from search input
* @return {Promise} api call
*
* @todo broken api https://jira.skava.net/browse/SKREACT-639?filter=26436
*
* @example {
* "type":"suggestion",
* "properties":{
* "suggestion":[{"value":
* ["men in electronics|||%2528%252bcampaignid%253a2495%2b%252bavailable%253atrue%2b%252bcategorylevel2%253a%2522men%2522%2b%252bcategorylevel1%253a%2522electronics%2522%2529","men in shoes|||%2528%252bcampaignid%253a2495%2b%252bavailable%253atrue%2b%252bcategorylevel2%253a%2522men%2522%2b%252bcategorylevel1%253a%2522shoes%2522%2529","men in clothing|||%2528%252bcampaignid%253a2495%2b%252bavailable%253atrue%2b%252bcategorylevel2%253a%2522men%2522%2b%252bcategorylevel1%253a%2522clothing%2522%2529"]}]}}
*
* @see http://54.161.84.34:3006/api/searchsuggestion?campaignId=212&search=men&offset=0&limit=20&locale=en_US
*/
updateSearchSuggestions(value) {
return searchApi
.getSearchSuggestions(value)
.then(suggestions => {
this.suggestionList = suggestions.value || [];
this.suggestionList = uniq(this.suggestionList);
this.setLabelValue(suggestions);
})
.catch(typeError => {
console.error(typeError);
throw typeError;
});
}
updateSearchToDefaultIfEmpty(value) {
if (value === '' || value === '""') {
this.afterCreate();
}
}
// =================== ui ===================
/**
* @todo @james evaluate a better way to deal with this
* rather than 2-way coupling
*
* @see atoms/SearchInput
* @note dom.observableState is on search input
*/
setInputReference(dom) {
this.inputReference = dom;
}
/**
* @event clear
*/
handleClearRecent(event) {
console.debug('[SearchContainer] handleClearRecent');
oneStorage.remove('search_recent_list');
this.recentList = [];
}
/**
* @tutorial https://reactjs.org/docs/events.html#keyboard-events
*/
handleKeyboard(event) {
console.debug('[SearchContainer] handleKeyboard');
this.show();
const target = event.target;
const value = target.value;
// ignore unsafe & empty
if (isSafe(value) === false || value === '') {
console.debug('empty');
this.updateSearchToDefaultIfEmpty('');
return;
}
else {
this.updateSearchSuggestions(value);
}
// @TODO
if (event.eventName === 'Enter') {
this.goToSearch(value);
}
}
show() {
if (this.isVisible === false) {
this.isVisible = true;
}
}
hide() {
if (this.isVisible === true) {
this.isVisible = false;
}
}
/**
* @description changes url to go to search page
*/
goToSearch(value) {
/**
* if it is not just string or number...
*/
const searchTerm = serializeSearchTerm(value).trim();
const isValidSearchTerm = isNonEmptyString(searchTerm);
const safeSearch = encodeURIComponent(searchNameUnquote(searchTerm));
if (isValidSearchTerm) {
oneRouter.update('/search/' + safeSearch);
}
}
/**
* @param value value of dom state
*/
updateSuggestionsAndSave(value) {
console.debug('[SearchContainer] updateSuggestionsAndSave: ' + value);
// we cannot lose event persistance here, one of the reasons to add event store
if (value === '') {
this.updateSearchToDefaultIfEmpty('');
}
else {
this.updateSearchSuggestions(value);
this.saveSearch(value);
}
}
/**
* @param value value to save in recent search
* @see https://jira.skava.net/browse/SKREACT-4485
*/
saveSearch(value) {
if (this.recentList.length >= SEARCH_SUGGESTIONS_RECENT_MAX) {
this.recentList.shift();
}
if (this.recentList.includes(value) === false) {
this.recentList.push(value);
}
oneStorage.set('search_recent_list', this.recentList);
}
/**
* @event submit ui
*/
handleSubmit(event) {
event.preventDefault();
const value = this.inputReference.observableState.value;
if (value) {
this.handleFromSearchSuggestionOrSubmit(value);
}
}
/**
* @param value search value
*/
handleFromSearchSuggestionOrSubmit(value) {
this.updateSuggestionsAndSave(value);
this.goToSearch(value);
this.handleClickBoundary();
}
/**
* @event focus
* @description Show search suggestions on focus.
* @todo typing here on target
*/
handleSearchBarFocus(props) {
console.debug('[SearchContainer] handleSearchBarFocus');
const target = props.event.target;
const value = target.value;
const isFocus = props.type === 'focus' || props.type === 'boundary';
const searchBarFocus = () => {
this.updateSearchSuggestions(value);
// && !stateForSearch.isModalVisible
if (isFocus) {
this.isVisible = true;
}
else {
this.isVisible = false;
this.isActive = !this.isActive;
}
};
/**
* @description on tap event not working if provided less than 1000 ms for setInterval
*/
const intervalTime = isFocus ? 10 : 1000;
setTimeout(searchBarFocus, intervalTime);
return searchBarFocus;
}
/**
* @event boundaryClick
*/
handleClickBoundary(event) {
this.hide();
}
/**
* @event clear
*/
handleSearchBarClear(component) {
console.debug('[SearchContainer] handleSearchBarClear');
component.observableState.value = '';
this.show();
}
/**
* @event cancel
*/
handleOnCancel(component) {
this.hide();
}
// ========= computed ======
get searchTerm() {
return this.value || oneRouter.get('searchTerm') || '';
}
}
SearchContainer.debugName = 'Search';
tslib_1.__decorate([
observable
], SearchContainer.prototype, "placeholder", void 0);
tslib_1.__decorate([
observable
], SearchContainer.prototype, "isVisible", void 0);
tslib_1.__decorate([
observable
], SearchContainer.prototype, "isActive", void 0);
tslib_1.__decorate([
observable
], SearchContainer.prototype, "suggestionList", void 0);
tslib_1.__decorate([
observable
], SearchContainer.prototype, "recentList", void 0);
tslib_1.__decorate([
observable
], SearchContainer.prototype, "label", void 0);
tslib_1.__decorate([
observable
], SearchContainer.prototype, "value", void 0);
tslib_1.__decorate([
action
], SearchContainer.prototype, "afterCreate", null);
tslib_1.__decorate([
action
], SearchContainer.prototype, "setLabelValue", null);
tslib_1.__decorate([
action
], SearchContainer.prototype, "updateSearchSuggestions", null);
tslib_1.__decorate([
action
], SearchContainer.prototype, "updateSearchToDefaultIfEmpty", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "setInputReference", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "handleClearRecent", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "handleKeyboard", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "show", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "hide", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "goToSearch", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "updateSuggestionsAndSave", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "saveSearch", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "handleSubmit", null);
tslib_1.__decorate([
action
], SearchContainer.prototype, "handleFromSearchSuggestionOrSubmit", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "handleSearchBarFocus", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "handleClickBoundary", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "handleSearchBarClear", null);
tslib_1.__decorate([
action.bound
], SearchContainer.prototype, "handleOnCancel", null);
tslib_1.__decorate([
computed
], SearchContainer.prototype, "searchTerm", null);
const searchContainer = new SearchContainer();
export { searchContainer, SearchContainer, defaultSuggestionList };
export default searchContainer;
//# sourceMappingURL=container.js.map