diff --git a/docs/classes/_minisearch_.minisearch.html b/docs/classes/_minisearch_.minisearch.html index d2b88a76..e09f5a16 100644 --- a/docs/classes/_minisearch_.minisearch.html +++ b/docs/classes/_minisearch_.minisearch.html @@ -199,7 +199,7 @@

constructor

  • @@ -287,7 +287,7 @@

    dirtCount

  • @@ -309,7 +309,7 @@

    dirtFactor

  • @@ -335,7 +335,7 @@

    documentCount

  • @@ -357,7 +357,7 @@

    isVacuuming

  • @@ -379,7 +379,7 @@

    termCount

  • @@ -404,7 +404,7 @@

    add

  • @@ -435,7 +435,7 @@

    addAll

  • @@ -466,7 +466,7 @@

    addAllAsync

  • @@ -512,7 +512,7 @@

    autoSuggest

  • @@ -598,7 +598,7 @@

    discard

  • @@ -669,7 +669,7 @@

    discardAll

  • @@ -704,7 +704,7 @@

    getStoredFields

  • @@ -737,7 +737,7 @@

    has

  • @@ -769,7 +769,7 @@

    remove

  • @@ -807,7 +807,7 @@

    removeAll

  • @@ -842,7 +842,7 @@

    replace

  • @@ -881,7 +881,7 @@

    search

  • @@ -1033,7 +1033,7 @@

    toJSON

  • @@ -1073,7 +1073,7 @@

    vacuum

  • @@ -1134,7 +1134,7 @@

    Static getDefault

  • @@ -1180,7 +1180,7 @@

    Static loadJSON

  • diff --git a/docs/classes/_searchablemap_searchablemap_.searchablemap.html b/docs/classes/_searchablemap_searchablemap_.searchablemap.html index ec06d9b5..e4ce7ff9 100644 --- a/docs/classes/_searchablemap_searchablemap_.searchablemap.html +++ b/docs/classes/_searchablemap_searchablemap_.searchablemap.html @@ -153,7 +153,7 @@

    constructor

  • @@ -191,7 +191,7 @@

    size

  • @@ -218,7 +218,7 @@

    [Symbol.iterator]

  • @@ -242,7 +242,7 @@

    atPrefix

  • @@ -295,7 +295,7 @@

    clear

  • @@ -319,7 +319,7 @@

    delete

  • @@ -352,7 +352,7 @@

    entries

  • @@ -377,7 +377,7 @@

    fetch

  • @@ -432,7 +432,7 @@

    forEach

  • @@ -489,7 +489,7 @@

    fuzzyGet

  • @@ -547,7 +547,7 @@

    get

  • @@ -582,7 +582,7 @@

    has

  • @@ -616,7 +616,7 @@

    keys

  • @@ -641,7 +641,7 @@

    set

  • @@ -681,7 +681,7 @@

    update

  • @@ -748,7 +748,7 @@

    values

  • @@ -773,7 +773,7 @@

    Static from

  • @@ -811,7 +811,7 @@

    Static fromObject

  • diff --git a/docs/demo/plain_js/README.md b/docs/demo/plain_js/README.md new file mode 100644 index 00000000..7dc3f9c9 --- /dev/null +++ b/docs/demo/plain_js/README.md @@ -0,0 +1,13 @@ +# MiniSearch example in Plain JavaScript + +This is an example client-side application, written in plain JavaScript (no +framework), to showcase `MiniSearch`. It demonstrates search, auto-completion, +and some advanced options. + +## Start the application + +In order to start this example application on your machine: + + 1. Open a terminal and `cd` to this directory + 2. Start an HTTP server, for example by running `python3 -m http.server` if you have Python installed, or `npx http-server -p 8000` if you have NodeJS installed + 3. Visit the server URL on your browser (e.g. http://localhost:8000) diff --git a/docs/demo/plain_js/app.css b/docs/demo/plain_js/app.css new file mode 100644 index 00000000..841e8cbd --- /dev/null +++ b/docs/demo/plain_js/app.css @@ -0,0 +1,225 @@ +* { + box-sizing: border-box; +} +body { + font-family: 'Open Sans', Helvetica, Arial, sans-serif; + text-rendering: geometricPrecision; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: #555; + margin: 0; + padding: 0; + background: #fff; +} +a { + color: #0099cc; +} +h1, h2, h3, h4 { + margin: 1em 0 0.5em 0; + color: #333; +} +dl { + margin: 0; +} +dt, dd { + display: inline; + margin: 0; +} +dt { + font-weight: bold; + color: #333; +} +dd:after { + content: ''; + display: block; +} +details, summary { + outline: none; +} +.App .main { + padding: 1em; + display: flex; + flex-flow: column; + max-height: 100vh; + max-width: 900px; + margin: 0 auto; +} +.Header h1 { + font-size: 2em; + margin-top: 0; +} +.SearchBox { + position: relative; +} +.Search { + position: relative; +} +.Search button.clear { + position: absolute; + top: 0; + bottom: 0.2em; + right: 0.5em; + font-size: 1.5em; + line-height: 1; + z-index: 20; + border: none; + background: none; + outline: none; + margin: 0; + padding: 0; +} +.Search input { + width: 100%; + padding: 0.5em; + font-size: 16px; + border: 1px solid #ccc; + border-radius: 3px; + outline: none; + color: #555; + box-shadow: none; +} +.hasResults .Explanation { + display: none; +} +.AdvancedOptions { + font-size: 0.9em; +} +.AdvancedOptions summary { + text-decoration: underline; +} +.AdvancedOptions .options { + margin-top: 1em; + font-size: 0.85em; +} +.AdvancedOptions .options label { + display: inline; + margin-left: 0.7em; +} +.SongList { + margin: 1em 0 0 0; + padding: 0; + list-style: none; + flex-grow: 1; + position: relative; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; +} +.SongList:before { + content: ''; + display: block; + position: sticky; + z-index: 10; + left: 0; + right: 0; + top: -1px; + width: 100%; + height: 0.7em; + margin-bottom: -0.7em; + background: linear-gradient(white, rgba(255, 255, 255, 0)); +} +.SongList:after { + content: ''; + display: block; + position: sticky; + z-index: 10; + left: 0; + right: 0; + bottom: -1px; + width: 100%; + height: 0.7em; + margin-bottom: -0.7em; + background: linear-gradient(rgba(255, 255, 255, 0), white); +} +.Song { + border-bottom: 1px solid #ccc; + padding: 0.7em 0 1em 0; +} +.Song:last-child { + border-bottom: none; +} +.Song h3 { + margin-top: 0; + margin-bottom: 0.15em; +} +.SuggestionList { + display: none; + list-style: none; + padding: 0; + border: 1px solid #ccc; + border-top: 0; + margin: 0 0 0.2em 0; + border-radius: 3px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + background: rgba(255, 255, 255, 0.93); + position: absolute; + z-index: 20; + left: 0; + right: 0; +} +.hasSuggestions .SuggestionList { + display: block; +} +.Suggestion { + padding: 0.5em 1em; + border-bottom: 1px solid #eee; +} +.Suggestion:last-child { + border: none; +} +.Suggestion.selected { + background: rgba(240, 240, 240, 0.95); +} +.Suggestion:hover:not(.selected) { + background: rgba(250, 250, 250, 0.95); +} +.Loader { + display: none; +} +.loading .Loader { + display: block; +} +.loading .main { + display: none; +} +.Loader, +.Loader:after { + border-radius: 50%; + width: 10em; + height: 10em; +} +.Loader { + margin: 5px auto; + font-size: 5px; + position: relative; + text-indent: -9999em; + border-top: 1.1em solid rgba(255, 255, 255, 0.2); + border-right: 1.1em solid rgba(255, 255, 255, 0.2); + border-bottom: 1.1em solid rgba(255, 255, 255, 0.2); + border-left: 1.1em solid #0099cc; + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); + -webkit-animation: load8 1.1s infinite linear; + animation: load8 1.1s infinite linear; +} +@-webkit-keyframes load8 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes load8 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + diff --git a/docs/demo/plain_js/app.js b/docs/demo/plain_js/app.js new file mode 100644 index 00000000..783f6db3 --- /dev/null +++ b/docs/demo/plain_js/app.js @@ -0,0 +1,184 @@ +// Setup MiniSearch +const miniSearch = new MiniSearch({ + fields: ['artist', 'title'], + storeFields: ['year'] +}) + +// Select DOM elements +const $app = document.querySelector('.App') +const $search = document.querySelector('.Search') +const $searchInput = document.querySelector('.Search input') +const $clearButton = document.querySelector('.Search button.clear') +const $songList = document.querySelector('.SongList') +const $explanation = document.querySelector('.Explanation') +const $suggestionList = document.querySelector('.SuggestionList') +const $options = document.querySelector('.AdvancedOptions form') + +// Fetch and index data +$app.classList.add('loading') +let songsById = {} + +fetch('billboard_1965-2015.json') + .then(response => response.json()) + .then((allSongs) => { + songsById = allSongs.reduce((byId, song) => { + byId[song.id] = song + return byId + }, {}) + return miniSearch.addAll(allSongs) + }).then(() => { + $app.classList.remove('loading') + }) + +// Bind event listeners: + +// Typing into search bar updates search results and suggestions +$searchInput.addEventListener('input', (event) => { + const query = $searchInput.value + + const results = (query.length > 1) ? getSearchResults(query) : [] + renderSearchResults(results) + + const suggestions = (query.length > 1) ? getSuggestions(query) : [] + renderSuggestions(suggestions) +}) + +// Clicking on clear button clears search and suggestions +$clearButton.addEventListener('click', () => { + $searchInput.value = '' + $searchInput.focus() + + renderSearchResults([]) + renderSuggestions([]) +}) + +// Clicking on a suggestion selects it +$suggestionList.addEventListener('click', (event) => { + const $suggestion = event.target + + if ($suggestion.classList.contains('Suggestion')) { + const query = $suggestion.innerText.trim() + $searchInput.value = query + $searchInput.focus() + + const results = getSearchResults(query) + renderSearchResults(results) + renderSuggestions([]) + } +}) + +// Pressing up/down/enter key while on search bar navigates through suggestions +$search.addEventListener('keydown', (event) => { + const key = event.key + + if (key === 'ArrowDown') { + selectSuggestion(+1) + } else if (key === 'ArrowUp') { + selectSuggestion(-1) + } else if (key === 'Enter' || key === 'Escape') { + $searchInput.blur() + renderSuggestions([]) + } else { + return + } + const query = $searchInput.value + const results = getSearchResults(query) + renderSearchResults(results) +}) + +// Clicking outside of search bar clears suggestions +$app.addEventListener('click', (event) => { + renderSuggestions([]) +}) + +// Changing any advanced option recomputes the search option +$options.addEventListener('change', (event) => { + recomputeSearchOptions() + + const query = $searchInput.value + const results = getSearchResults(query) + renderSearchResults(results) +}) + +// Define functions and support variables +const searchOptions = { + fuzzy: 0.2, + prefix: true, + fields: ['title', 'artist'], + combineWith: 'OR', + filter: null +} + +const getSearchResults = (query) => { + return miniSearch.search(query, searchOptions).map(({ id }) => songsById[id]) +} + +const getSuggestions = (query) => { + return miniSearch.autoSuggest(query, { boost: { artist: 5 } }) + .filter(({ suggestion, score }, _, [first]) => score > first.score / 4) + .slice(0, 5) +} + +const renderSearchResults = (results) => { + $songList.innerHTML = results.map(({ artist, title, year, rank }) => { + return `
  • +

    ${capitalize(title)}

    +
    +
    Artist:
    ${capitalize(artist)}
    +
    Year:
    ${year}
    +
    Billbord Position:
    ${rank}
    +
    +
  • ` + }).join('\n') + + if (results.length > 0) { + $app.classList.add('hasResults') + } else { + $app.classList.remove('hasResults') + } +} + +const renderSuggestions = (suggestions) => { + $suggestionList.innerHTML = suggestions.map(({ suggestion }) => { + return `
  • ${suggestion}
  • ` + }).join('\n') + + if (suggestions.length > 0) { + $app.classList.add('hasSuggestions') + } else { + $app.classList.remove('hasSuggestions') + } +} + +const selectSuggestion = (direction) => { + const $suggestions = document.querySelectorAll('.Suggestion') + const $selected = document.querySelector('.Suggestion.selected') + const index = Array.from($suggestions).indexOf($selected) + + if (index > -1) { + $suggestions[index].classList.remove('selected') + } + + const nextIndex = Math.max(Math.min(index + direction, $suggestions.length - 1), 0) + $suggestions[nextIndex].classList.add('selected') + $searchInput.value = $suggestions[nextIndex].innerText +} + +const recomputeSearchOptions = () => { + const formData = new FormData($options) + + searchOptions.fuzzy = formData.has('fuzzy') ? 0.2 : false + searchOptions.prefix = formData.has('prefix') + searchOptions.fields = formData.getAll('fields') + searchOptions.combineWith = formData.get('combineWith') + + const fromYear = parseInt(formData.get('fromYear'), 10) + const toYear = parseInt(formData.get('toYear'), 10) + + searchOptions.filter = ({ year }) => { + year = parseInt(year, 10) + return year >= fromYear && year <= toYear + } +} + +const capitalize = (string) => string.replace(/(\b\w)/gi, (char) => char.toUpperCase()) diff --git a/docs/examples/billboard_1965-2015.json b/docs/demo/plain_js/billboard_1965-2015.json similarity index 100% rename from docs/examples/billboard_1965-2015.json rename to docs/demo/plain_js/billboard_1965-2015.json diff --git a/docs/demo/plain_js/index.html b/docs/demo/plain_js/index.html new file mode 100644 index 00000000..254d5bb6 --- /dev/null +++ b/docs/demo/plain_js/index.html @@ -0,0 +1,95 @@ + + + + MiniSearch Example + + + + + +
    +
    +
    loading...
    +
    +
    +

    Song Search

    + +
    +

    + This is a demo of the MiniSearch JavaScript + library: try searching through more than 5000 top songs and artists + in Billboard Hot 100 from year 1965 to 2015. This example + demonstrates search (with prefix and fuzzy match) and auto-completion. +

    +
      +
    +
    +
    +
    + + + + + diff --git a/docs/examples/app.js b/docs/examples/app.js deleted file mode 100644 index 009df626..00000000 --- a/docs/examples/app.js +++ /dev/null @@ -1,30 +0,0 @@ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var a=t[r]={i:r,l:!1,exports:{}};return e[r].call(a.exports,a,a.exports,n),a.l=!0,a.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)n.d(r,a,function(t){return e[t]}.bind(null,a));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=7)}([function(e,t,n){"use strict";e.exports=n(4)},function(e,t,n){"use strict"; -/* -object-assign -(c) Sindre Sorhus -@license MIT -*/var r=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,l=Object.prototype.propertyIsEnumerable;function o(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,i,u=o(e),s=1;s