diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/README.md b/README.md index 3cf797b..6aaf795 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,401 @@ # ghost-search -A simple but powerful search library for Ghost Blogging Platform. + +A simple but powerful search library for [Ghost Blogging Platform](https://ghost.org/). + +## Setup + +Open your theme directory and navigate to `assets` subdirectory. \ +Create a directory called `js`, if there isn't already one in there, and add the minified version of `ghost-search` in it. \ +Open `default.hbs` that is located at the root of your theme. \ +At the bottom of this file you should see `{{ghost_foot}}`. \ +Add the following code above it and save it: +``` + +``` + +Add the following code, in a `.hbs` file, where you want to show the search input: + +```html + +``` + +Add the following code, in a `.hbs` file, where you want to show the search results: + +``` +
+``` + +You will need to initialize `ghost-search` to make the search functional. \ +Add the following js code after you included `ghost-search.min.js`: + +```html + +``` + +## Live Examples + +[Set a basic search](https://www.hauntedthemes.com/ghost-search-our-first-open-source-library-for-ghost/#set-a-basic-search) \ +[Display a message if there are no posts found](https://www.hauntedthemes.com/ghost-search-our-first-open-source-library-for-ghost/#display-a-message-if-there-are-no-posts-found) \ +[Search only through tags](https://www.hauntedthemes.com/ghost-search-our-first-open-source-library-for-ghost/#search-only-through-tags) \ +[Search posts from a custom collection](https://www.hauntedthemes.com/ghost-search-our-first-open-source-library-for-ghost/#search-posts-from-a-custom-collection) \ +[Search posts that are published after 01 Jan 2018](https://www.hauntedthemes.com/ghost-search-our-first-open-source-library-for-ghost/#search-posts-that-are-published-after-01-jan-2018) \ +[Search through the title and content of posts](https://www.hauntedthemes.com/ghost-search-our-first-open-source-library-for-ghost/#search-through-the-title-and-content-of-posts) \ +[Get the results when a button is clicked](https://www.hauntedthemes.com/ghost-search-our-first-open-source-library-for-ghost/#get-the-results-when-a-button-is-clicked) \ +[Get proper results when your Ghost is on a sub-path](https://www.hauntedthemes.com/ghost-search-our-first-open-source-library-for-ghost/#get-proper-results-when-your-ghost-is-on-a-sub-path) \ +[Set multiple instances of ghost-search on the same page](https://www.hauntedthemes.com/ghost-search-our-first-open-source-library-for-ghost/#set-multiple-instances-of-ghost-search-on-the-same-page) \ +[Add a loading icon when you have a lot of posts](https://www.hauntedthemes.com/ghost-search-our-first-open-source-library-for-ghost/#add-a-loading-icon-when-you-have-a-lot-of-posts) \ +[Limit the results displayed](https://www.hauntedthemes.com/ghost-search-our-first-open-source-library-for-ghost/#limit-the-results-displayed) + +## Default Options + +``` +{ + input: '#ghost-search-field', + results: '#ghost-search-results', + button: '', + development: false, + template: function(result) { + let url = [location.protocol, '//', location.host].join(''); + return '' + result.title + ''; + }, + trigger: 'focus', + options: { + keys: [ + 'title' + ], + limit: 100, + threshold: -3500, + allowTypo: false + }, + api: { + resource: 'posts', + parameters: { + limit: 'all', + fields: ['title', 'slug'], + filter: '', + include: '', + order: '', + formats: '', + }, + }, + on: { + beforeDisplay: function(){}, + afterDisplay: function(results){}, + beforeFetch: function(){}, + afterFetch: function(){} + } +} +``` + +## Options + +### input + +The ID of the input field that will be transformed into search filter. \ +You can set your own id if you want like this: + +```html + +``` + +```html + +``` + +Default value: `'#ghost-search-field'` + +### results + +The ID of the element that will be transformed into search results. \ +You can set your own id if you want like this: + +```html +
+``` + +```html + +``` + +Default value: `'#ghost-search-results'` + +### button + +The ID of the element that will trigger the search results after it's clicked. \ +By default, the button parameter is empty because `ghost-search` displays the results when you write in the input. \ +To make this work you need to add the input and the button in a `form` element: + +```html +
+ + +
+
+``` + +```html + +``` + +Default value: `''` + +### development + +A parameter that will do some validation to your current instance. Might be useful if you use the `api` parameter. \ +By default it is set to `false` but you can set it like this to `true`: + +```html + +``` + +Default value: `false` + +### template + +The template that will be used to render individual items in the search result. \ +The method has a parameter `result` that stores all the data that you can use inside the method. + +#### Examples: + +Make the results a list and wrap each result with `
  • `: + +```html +
    + +``` + +```html + +``` + +Set url with sub-path: + +```html + +``` + +Default value: + +```html +function(result) { + let url = [location.protocol, '//', location.host].join(''); + return '' + result.title + ''; +} +``` + +### trigger + +Tells the script when to fetch the collection of data. The default value is `focus`, that means when a user clicks the input, all the data is fetched. \ +You can also use `load`. This will fetch the data when the page loads. + +`load` might **create a DDOS effect** because it loads all the data every time a page loads. Use carefully. + +Default value: `'focus'` + +### options + +`ghost-search` is using `fuzzysort` as an algorithm for search. The `option` parameter supports all the options from [fuzzysort](https://github.com/farzher/fuzzysort#options). +By default, `ghost-search` is showing the first 10 results and searches only based on title. + +Let's try another example that will show the first 3 results and searches both title and the content of a collection: + +```html + +``` + +Default value: +``` +{ + keys: [ + 'title' + ], + limit: 10, + threshold: -3500, + allowTypo: false +} +``` + +### api + +The api parameter is an object that supports most of the [resources](https://api.ghost.org/docs/post) and [parameters](https://api.ghost.org/docs/limit) by [Ghost API](https://api.ghost.org). + +Resources: [posts](https://api.ghost.org/docs/post), [tags](https://api.ghost.org/docs/tag), [users](https://api.ghost.org/docs/user) \ +Parameters: [fields](https://api.ghost.org/docs/fields), [filter](https://api.ghost.org/docs/filter), [include](https://api.ghost.org/docs/include), [order](https://api.ghost.org/docs/order), [formats](https://api.ghost.org/docs/formats), [limit](https://api.ghost.org/docs/limit) + +Examples: + +Search through tags: + +```html + +``` + +Search through a custom collection: + +Let's say we have a `routes.yaml` like this: + +``` +routes: + +collections: + /themes/: + permalink: /themes/{slug}/ + filter: tag:themes + data: tag.themes + /: + permalink: /{slug}/ + filter: tag:-themes + template: + - index + +taxonomies: + tag: /tag/{slug}/ + author: /author/{slug}/ +``` + +`/themes/` is a collection that will show posts with tag `themes`. A post like this will have the url `example.com/themes/post-slug`. + +Our `ghost-search` will become: + +``` +let ghostSearch = new GhostSearch({ + options: { + keys: [ + 'title', + ], + }, + api: { + resource: 'posts', + parameters: { + fields: ['title', 'slug'], + filter: 'tags:[themes]', + include: 'tags' + }, + }, + template: function(result) { + let collection = 'themes'; + let url = [location.protocol, '//', location.host].join('') + '/' + collection; + return '' + result.title + ''; + }, +}) +``` + +### on + +This parameter has 4 methods in it: `beforeDisplay`, `afterDisplay`, `beforeFetch`, `afterFetch`. \ +`afterDisplay` and `afterFetch` have a parameter `results` that contains the results fetched. \ +They are usefull to do things before results are visible to users. + +Example: + +``` +let ghostSearch = new GhostSearch({ + on: { + beforeFetch: function(){ + // Create a div that has a spinning icon + console.log('Loading appers'); + }, + afterFetch: function(results){ + // Remove the spinning icon + console.log('Loading disappears'); + } + } +}) +``` + +## Contributing + +All changes should be committed to `src/` files only. + +## Known Issues +* DDOS effect when `trigger` is set to `load` +If you have a lot of posts and set `trigger` to `load` you might get a DDOS effect because you are loading all the post everything a page loads. It would be better to just set `trigger` to `focus`. +* [Avoid using `fields` if you are filtering on a relationship](https://github.com/TryGhost/Ghost/issues/8649). +If you include `tags` or `authors`, the library removes everything you have in `fields` parameter. + +## Changelog + +### 0.1.0 - 17 Sep 2018 +* Initial release + +## Thank You +ghost-search is using as a search algorithm [fuzzysort](https://github.com/farzher/fuzzysort). \ +Thank you [farzher](https://github.com/farzher/) for creating fuzzysort, a simple and usable search library. + +## Copyright & License + +Copyright (c) 2018 Haunted Themes - Released under the [MIT license](LICENSE). +[Ghost is a trademark of The Ghost Foundation](https://ghost.org/) \ No newline at end of file diff --git a/dist/ghost-search.js b/dist/ghost-search.js new file mode 100644 index 0000000..78f2b08 --- /dev/null +++ b/dist/ghost-search.js @@ -0,0 +1,935 @@ +/** + * ghost-search 0.1.0 (https://github.com/HauntedThemes/ghost-search) + * A simple but powerful search library for Ghost Blogging Platform. + * Copyright 2018 Haunted Themes (https://www.hauntedthemes.com) + * Released under MIT License + * Released on: 17 Sep 2018 + */ + +/* +WHAT: SublimeText-like Fuzzy Search + +USAGE: + fuzzysort.single('fs', 'Fuzzy Search') // {score: -16} + fuzzysort.single('test', 'test') // {score: 0} + fuzzysort.single('doesnt exist', 'target') // null + + fuzzysort.go('mr', ['Monitor.cpp', 'MeshRenderer.cpp']) + // [{score: -18, target: "MeshRenderer.cpp"}, {score: -6009, target: "Monitor.cpp"}] + + fuzzysort.highlight(fuzzysort.single('fs', 'Fuzzy Search'), '', '') + // Fuzzy Search +*/ + +// UMD (Universal Module Definition) for fuzzysort +;(function(root, UMD) { + if(typeof define === 'function' && define.amd) define([], UMD) + else if(typeof module === 'object' && module.exports) module.exports = UMD() + else root.fuzzysort = UMD() +})(this, function UMD() { function fuzzysortNew(instanceOptions) { + + var fuzzysort = { + + single: function(search, target, options) { + if(!search) return null + if(!isObj(search)) search = fuzzysort.getPreparedSearch(search) + + if(!target) return null + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo + : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo + : true + var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo + return algorithm(search, target, search[0]) + // var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991 + // var result = algorithm(search, target, search[0]) + // if(result === null) return null + // if(result.score < threshold) return null + // return result + }, + + go: function(search, targets, options) { + if(!search) return noResults + search = fuzzysort.prepareSearch(search) + var searchLowerCode = search[0] + + var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991 + var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991 + var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo + : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo + : true + var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo + var resultsLen = 0; var limitedCount = 0 + var targetsLen = targets.length + + // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys] + + // options.keys + if(options && options.keys) { + var scoreFn = options.scoreFn || defaultScoreFn + var keys = options.keys + var keysLen = keys.length + for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i] + var objResults = new Array(keysLen) + for (var keyI = keysLen - 1; keyI >= 0; --keyI) { + var key = keys[keyI] + var target = getValue(obj, key) + if(!target) { objResults[keyI] = null; continue } + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + objResults[keyI] = algorithm(search, target, searchLowerCode) + } + objResults.obj = obj // before scoreFn so scoreFn can use it + var score = scoreFn(objResults) + if(score === null) continue + if(score < threshold) continue + objResults.score = score + if(resultsLen < limit) { q.add(objResults); ++resultsLen } + else { + ++limitedCount + if(score > q.peek().score) q.replaceTop(objResults) + } + } + + // options.key + } else if(options && options.key) { + var key = options.key + for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i] + var target = getValue(obj, key) + if(!target) continue + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + var result = algorithm(search, target, searchLowerCode) + if(result === null) continue + if(result.score < threshold) continue + + // have to clone result so duplicate targets from different obj can each reference the correct obj + result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden + + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result.score > q.peek().score) q.replaceTop(result) + } + } + + // no keys + } else { + for(var i = targetsLen - 1; i >= 0; --i) { var target = targets[i] + if(!target) continue + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + var result = algorithm(search, target, searchLowerCode) + if(result === null) continue + if(result.score < threshold) continue + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result.score > q.peek().score) q.replaceTop(result) + } + } + } + + if(resultsLen === 0) return noResults + var results = new Array(resultsLen) + for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll() + results.total = resultsLen + limitedCount + return results + }, + + goAsync: function(search, targets, options) { + var canceled = false + var p = new Promise(function(resolve, reject) { + if(!search) return resolve(noResults) + search = fuzzysort.prepareSearch(search) + var searchLowerCode = search[0] + + var q = fastpriorityqueue() + var iCurrent = targets.length - 1 + var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991 + var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991 + var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo + : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo + : true + var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo + var resultsLen = 0; var limitedCount = 0 + function step() { + if(canceled) return reject('canceled') + + var startMs = Date.now() + + // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys] + + // options.keys + if(options && options.keys) { + var scoreFn = options.scoreFn || defaultScoreFn + var keys = options.keys + var keysLen = keys.length + for(; iCurrent >= 0; --iCurrent) { var obj = targets[iCurrent] + var objResults = new Array(keysLen) + for (var keyI = keysLen - 1; keyI >= 0; --keyI) { + var key = keys[keyI] + var target = getValue(obj, key) + if(!target) { objResults[keyI] = null; continue } + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + objResults[keyI] = algorithm(search, target, searchLowerCode) + } + objResults.obj = obj // before scoreFn so scoreFn can use it + var score = scoreFn(objResults) + if(score === null) continue + if(score < threshold) continue + objResults.score = score + if(resultsLen < limit) { q.add(objResults); ++resultsLen } + else { + ++limitedCount + if(score > q.peek().score) q.replaceTop(objResults) + } + + if(iCurrent%1000/*itemsPerCheck*/ === 0) { + if(Date.now() - startMs >= 10/*asyncInterval*/) { + isNode?setImmediate(step):setTimeout(step) + return + } + } + } + + // options.key + } else if(options && options.key) { + var key = options.key + for(; iCurrent >= 0; --iCurrent) { var obj = targets[iCurrent] + var target = getValue(obj, key) + if(!target) continue + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + var result = algorithm(search, target, searchLowerCode) + if(result === null) continue + if(result.score < threshold) continue + + // have to clone result so duplicate targets from different obj can each reference the correct obj + result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden + + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result.score > q.peek().score) q.replaceTop(result) + } + + if(iCurrent%1000/*itemsPerCheck*/ === 0) { + if(Date.now() - startMs >= 10/*asyncInterval*/) { + isNode?setImmediate(step):setTimeout(step) + return + } + } + } + + // no keys + } else { + for(; iCurrent >= 0; --iCurrent) { var target = targets[iCurrent] + if(!target) continue + if(!isObj(target)) target = fuzzysort.getPrepared(target) + + var result = algorithm(search, target, searchLowerCode) + if(result === null) continue + if(result.score < threshold) continue + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result.score > q.peek().score) q.replaceTop(result) + } + + if(iCurrent%1000/*itemsPerCheck*/ === 0) { + if(Date.now() - startMs >= 10/*asyncInterval*/) { + isNode?setImmediate(step):setTimeout(step) + return + } + } + } + } + + if(resultsLen === 0) return resolve(noResults) + var results = new Array(resultsLen) + for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll() + results.total = resultsLen + limitedCount + resolve(results) + } + + isNode?setImmediate(step):step() + }) + p.cancel = function() { canceled = true } + return p + }, + + highlight: function(result, hOpen, hClose) { + if(result === null) return null + if(hOpen === undefined) hOpen = '' + if(hClose === undefined) hClose = '' + var highlighted = '' + var matchesIndex = 0 + var opened = false + var target = result.target + var targetLen = target.length + var matchesBest = result.indexes + for(var i = 0; i < targetLen; ++i) { var char = target[i] + if(matchesBest[matchesIndex] === i) { + ++matchesIndex + if(!opened) { opened = true + highlighted += hOpen + } + + if(matchesIndex === matchesBest.length) { + highlighted += char + hClose + target.substr(i+1) + break + } + } else { + if(opened) { opened = false + highlighted += hClose + } + } + highlighted += char + } + + return highlighted + }, + + prepare: function(target) { + if(!target) return + return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:null, score:null, indexes:null, obj:null} // hidden + }, + prepareSlow: function(target) { + if(!target) return + return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:fuzzysort.prepareNextBeginningIndexes(target), score:null, indexes:null, obj:null} // hidden + }, + prepareSearch: function(search) { + if(!search) return + return fuzzysort.prepareLowerCodes(search) + }, + + + + // Below this point is only internal code + // Below this point is only internal code + // Below this point is only internal code + // Below this point is only internal code + + + + getPrepared: function(target) { + if(target.length > 999) return fuzzysort.prepare(target) // don't cache huge targets + var targetPrepared = preparedCache.get(target) + if(targetPrepared !== undefined) return targetPrepared + targetPrepared = fuzzysort.prepare(target) + preparedCache.set(target, targetPrepared) + return targetPrepared + }, + getPreparedSearch: function(search) { + if(search.length > 999) return fuzzysort.prepareSearch(search) // don't cache huge searches + var searchPrepared = preparedSearchCache.get(search) + if(searchPrepared !== undefined) return searchPrepared + searchPrepared = fuzzysort.prepareSearch(search) + preparedSearchCache.set(search, searchPrepared) + return searchPrepared + }, + + algorithm: function(searchLowerCodes, prepared, searchLowerCode) { + var targetLowerCodes = prepared._targetLowerCodes + var searchLen = searchLowerCodes.length + var targetLen = targetLowerCodes.length + var searchI = 0 // where we at + var targetI = 0 // where you at + var typoSimpleI = 0 + var matchesSimpleLen = 0 + + // very basic fuzzy match; to remove non-matching targets ASAP! + // walk through target. find sequential matches. + // if all chars aren't found then exit + for(;;) { + var isMatch = searchLowerCode === targetLowerCodes[targetI] + if(isMatch) { + matchesSimple[matchesSimpleLen++] = targetI + ++searchI; if(searchI === searchLen) break + searchLowerCode = searchLowerCodes[typoSimpleI===0?searchI : (typoSimpleI===searchI?searchI+1 : (typoSimpleI===searchI-1?searchI-1 : searchI))] + } + + ++targetI; if(targetI >= targetLen) { // Failed to find searchI + // Check for typo or exit + // we go as far as possible before trying to transpose + // then we transpose backwards until we reach the beginning + for(;;) { + if(searchI <= 1) return null // not allowed to transpose first char + if(typoSimpleI === 0) { // we haven't tried to transpose yet + --searchI + var searchLowerCodeNew = searchLowerCodes[searchI] + if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char + typoSimpleI = searchI + } else { + if(typoSimpleI === 1) return null // reached the end of the line for transposing + --typoSimpleI + searchI = typoSimpleI + searchLowerCode = searchLowerCodes[searchI + 1] + var searchLowerCodeNew = searchLowerCodes[searchI] + if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char + } + matchesSimpleLen = searchI + targetI = matchesSimple[matchesSimpleLen - 1] + 1 + break + } + } + } + + var searchI = 0 + var typoStrictI = 0 + var successStrict = false + var matchesStrictLen = 0 + + var nextBeginningIndexes = prepared._nextBeginningIndexes + if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target) + var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1] + + // Our target string successfully matched all characters in sequence! + // Let's try a more advanced and strict test to improve the score + // only count it as a match if it's consecutive or a beginning character! + if(targetI !== targetLen) for(;;) { + if(targetI >= targetLen) { + // We failed to find a good spot for this search char, go back to the previous search char and force it forward + if(searchI <= 0) { // We failed to push chars forward for a better match + // transpose, starting from the beginning + ++typoStrictI; if(typoStrictI > searchLen-2) break + if(searchLowerCodes[typoStrictI] === searchLowerCodes[typoStrictI+1]) continue // doesn't make sense to transpose a repeat char + targetI = firstPossibleI + continue + } + + --searchI + var lastMatch = matchesStrict[--matchesStrictLen] + targetI = nextBeginningIndexes[lastMatch] + + } else { + var isMatch = searchLowerCodes[typoStrictI===0?searchI : (typoStrictI===searchI?searchI+1 : (typoStrictI===searchI-1?searchI-1 : searchI))] === targetLowerCodes[targetI] + if(isMatch) { + matchesStrict[matchesStrictLen++] = targetI + ++searchI; if(searchI === searchLen) { successStrict = true; break } + ++targetI + } else { + targetI = nextBeginningIndexes[targetI] + } + } + } + + { // tally up the score & keep track of matches for highlighting later + if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen } + else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen } + var score = 0 + var lastTargetI = -1 + for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i] + // score only goes down if they're not consecutive + if(lastTargetI !== targetI - 1) score -= targetI + lastTargetI = targetI + } + if(!successStrict) { + score *= 1000 + if(typoSimpleI !== 0) score += -20/*typoPenalty*/ + } else { + if(typoStrictI !== 0) score += -20/*typoPenalty*/ + } + score -= targetLen - searchLen + prepared.score = score + prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i] + + return prepared + } + }, + + algorithmNoTypo: function(searchLowerCodes, prepared, searchLowerCode) { + var targetLowerCodes = prepared._targetLowerCodes + var searchLen = searchLowerCodes.length + var targetLen = targetLowerCodes.length + var searchI = 0 // where we at + var targetI = 0 // where you at + var matchesSimpleLen = 0 + + // very basic fuzzy match; to remove non-matching targets ASAP! + // walk through target. find sequential matches. + // if all chars aren't found then exit + for(;;) { + var isMatch = searchLowerCode === targetLowerCodes[targetI] + if(isMatch) { + matchesSimple[matchesSimpleLen++] = targetI + ++searchI; if(searchI === searchLen) break + searchLowerCode = searchLowerCodes[searchI] + } + ++targetI; if(targetI >= targetLen) return null // Failed to find searchI + } + + var searchI = 0 + var successStrict = false + var matchesStrictLen = 0 + + var nextBeginningIndexes = prepared._nextBeginningIndexes + if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target) + var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1] + + // Our target string successfully matched all characters in sequence! + // Let's try a more advanced and strict test to improve the score + // only count it as a match if it's consecutive or a beginning character! + if(targetI !== targetLen) for(;;) { + if(targetI >= targetLen) { + // We failed to find a good spot for this search char, go back to the previous search char and force it forward + if(searchI <= 0) break // We failed to push chars forward for a better match + + --searchI + var lastMatch = matchesStrict[--matchesStrictLen] + targetI = nextBeginningIndexes[lastMatch] + + } else { + var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI] + if(isMatch) { + matchesStrict[matchesStrictLen++] = targetI + ++searchI; if(searchI === searchLen) { successStrict = true; break } + ++targetI + } else { + targetI = nextBeginningIndexes[targetI] + } + } + } + + { // tally up the score & keep track of matches for highlighting later + if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen } + else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen } + var score = 0 + var lastTargetI = -1 + for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i] + // score only goes down if they're not consecutive + if(lastTargetI !== targetI - 1) score -= targetI + lastTargetI = targetI + } + if(!successStrict) score *= 1000 + score -= targetLen - searchLen + prepared.score = score + prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i] + + return prepared + } + }, + + prepareLowerCodes: function(str) { + var strLen = str.length + var lowerCodes = [] // new Array(strLen) sparse array is too slow + var lower = str.toLowerCase() + for(var i = 0; i < strLen; ++i) lowerCodes[i] = lower.charCodeAt(i) + return lowerCodes + }, + prepareBeginningIndexes: function(target) { + var targetLen = target.length + var beginningIndexes = []; var beginningIndexesLen = 0 + var wasUpper = false + var wasAlphanum = false + for(var i = 0; i < targetLen; ++i) { + var targetCode = target.charCodeAt(i) + var isUpper = targetCode>=65&&targetCode<=90 + var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57 + var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum + wasUpper = isUpper + wasAlphanum = isAlphanum + if(isBeginning) beginningIndexes[beginningIndexesLen++] = i + } + return beginningIndexes + }, + prepareNextBeginningIndexes: function(target) { + var targetLen = target.length + var beginningIndexes = fuzzysort.prepareBeginningIndexes(target) + var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow + var lastIsBeginning = beginningIndexes[0] + var lastIsBeginningI = 0 + for(var i = 0; i < targetLen; ++i) { + if(lastIsBeginning > i) { + nextBeginningIndexes[i] = lastIsBeginning + } else { + lastIsBeginning = beginningIndexes[++lastIsBeginningI] + nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning + } + } + return nextBeginningIndexes + }, + + cleanup: cleanup, + new: fuzzysortNew, + } + return fuzzysort +} // fuzzysortNew + +// This stuff is outside fuzzysortNew, because it's shared with instances of fuzzysort.new() +var isNode = typeof require !== 'undefined' && typeof window === 'undefined' +// var MAX_INT = Number.MAX_SAFE_INTEGER +// var MIN_INT = Number.MIN_VALUE +var preparedCache = new Map() +var preparedSearchCache = new Map() +var noResults = []; noResults.total = 0 +var matchesSimple = []; var matchesStrict = [] +function cleanup() { preparedCache.clear(); preparedSearchCache.clear(); matchesSimple = []; matchesStrict = [] } +function defaultScoreFn(a) { + var max = -9007199254740991 + for (var i = a.length - 1; i >= 0; --i) { + var result = a[i]; if(result === null) continue + var score = result.score + if(score > max) max = score + } + if(max === -9007199254740991) return null + return max +} + +// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop] +// prop = 'key1.key2' 10ms +// prop = ['key1', 'key2'] 27ms +function getValue(obj, prop) { + var tmp = obj[prop]; if(tmp !== undefined) return tmp + var segs = prop + if(!Array.isArray(prop)) segs = prop.split('.') + var len = segs.length + var i = -1 + while (obj && (++i < len)) obj = obj[segs[i]] + return obj +} + +function isObj(x) { return typeof x === 'object' } // faster as a function + +// Hacked version of https://github.com/lemire/FastPriorityQueue.js +var fastpriorityqueue=function(){var r=[],o=0,e={};function n(){for(var e=0,n=r[e],c=1;c>1]=r[e],c=1+(e<<1)}for(var a=e-1>>1;e>0&&n.score>1)r[e]=r[a];r[e]=n}return e.add=function(e){var n=o;r[o++]=e;for(var c=n-1>>1;n>0&&e.score>1)r[n]=r[c];r[n]=e},e.poll=function(){if(0!==o){var e=r[0];return r[0]=r[--o],n(),e}},e.peek=function(e){if(0!==o)return r[0]},e.replaceTop=function(o){r[0]=o,n()},e}; +var q = fastpriorityqueue() // reuse this, except for async, it needs to make its own + +return fuzzysortNew() +}) // UMD + +// TODO: (performance) wasm version!? + +// TODO: (performance) layout memory in an optimal way to go fast by avoiding cache misses + +// TODO: (performance) preparedCache is a memory leak + +// TODO: (like sublime) backslash === forwardslash + +// TODO: (performance) i have no idea how well optizmied the allowing typos algorithm is + +'use strict'; +/** + * @requires ../node_modules/fuzzysort/fuzzysort.js + */ + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } + +var GhostSearch = +/*#__PURE__*/ +function () { + function GhostSearch(args) { + _classCallCheck(this, GhostSearch); + + this.check = false; + var defaults = { + input: '#ghost-search-field', + results: '#ghost-search-results', + button: '', + development: false, + template: function template(result) { + var url = [location.protocol, '//', location.host].join(''); + return '' + result.title + ''; + }, + trigger: 'focus', + options: { + keys: ['title'], + limit: 10, + threshold: -3500, + allowTypo: false + }, + api: { + resource: 'posts', + parameters: { + limit: 'all', + fields: ['title', 'slug'], + filter: '', + include: '', + order: '', + formats: '' + } + }, + on: { + beforeDisplay: function beforeDisplay() {}, + afterDisplay: function afterDisplay(results) {}, + beforeFetch: function beforeFetch() {}, + afterFetch: function afterFetch(results) {} + } + }; + var merged = this.mergeDeep(defaults, args); + Object.assign(this, merged); + this.init(); + } + + _createClass(GhostSearch, [{ + key: "mergeDeep", + value: function mergeDeep(target, source) { + var _this = this; + + if (target && _typeof(target) === 'object' && !Array.isArray(target) && target !== null && source && _typeof(source) === 'object' && !Array.isArray(source) && source !== null) { + Object.keys(source).forEach(function (key) { + if (source[key] && _typeof(source[key]) === 'object' && !Array.isArray(source[key]) && source[key] !== null) { + if (!target[key]) Object.assign(target, _defineProperty({}, key, {})); + + _this.mergeDeep(target[key], source[key]); + } else { + Object.assign(target, _defineProperty({}, key, source[key])); + } + }); + } + + return target; + } + }, { + key: "url", + value: function url() { + if (this.api.resource == 'posts' && this.api.parameters.include.match(/(tags|authors)/)) { + delete this.api.parameters.fields; + } + + ; + var url = ghost.url.api(this.api.resource, this.api.parameters); + return url; + } + }, { + key: "fetch", + value: function (_fetch) { + function fetch() { + return _fetch.apply(this, arguments); + } + + fetch.toString = function () { + return _fetch.toString(); + }; + + return fetch; + }(function () { + var _this2 = this; + + var url = this.url(); + this.on.beforeFetch(); + fetch(url).then(function (response) { + return response.json(); + }).then(function (resource) { + return _this2.search(resource); + }).catch(function (error) { + return console.error("Fetch Error =\n", error); + }); + }) + }, { + key: "createElementFromHTML", + value: function createElementFromHTML(htmlString) { + var div = document.createElement('div'); + div.innerHTML = htmlString.trim(); + return div.firstChild; + } + }, { + key: "cleanup", + value: function cleanup(input) { + return input.toLowerCase().replace(/[^a-zA-Z0-9]+/g, "-"); + } + }, { + key: "displayResults", + value: function displayResults(data) { + if (document.querySelectorAll(this.results)[0].firstChild !== null && document.querySelectorAll(this.results)[0].firstChild !== '') { + while (document.querySelectorAll(this.results)[0].firstChild) { + document.querySelectorAll(this.results)[0].removeChild(document.querySelectorAll(this.results)[0].firstChild); + } + } + + ; + var inputValue = document.querySelectorAll(this.input)[0].value; + var results = fuzzysort.go(inputValue, data, { + keys: this.options.keys, + limit: this.options.limit, + allowTypo: this.options.allowTypo, + threshold: this.options.threshold + }); + + for (var key in results) { + if (key < results.length) { + document.querySelectorAll(this.results)[0].appendChild(this.createElementFromHTML(this.template(results[key].obj))); + } + + ; + } + + this.on.afterDisplay(results); + } + }, { + key: "search", + value: function search(resource) { + var _this3 = this; + + var data = resource[this.api.resource]; + this.on.afterFetch(data); + this.check = true; + + if (this.button != '') { + var button = document.querySelectorAll(this.button)[0]; + + if (button.tagName == 'INPUT' && button.type == 'submit') { + button.closest('form').addEventListener("submit", function (e) { + e.preventDefault(); + }); + } + + ; + button.addEventListener('click', function (e) { + e.preventDefault(); + + _this3.on.beforeDisplay(); + + _this3.displayResults(data); + }); + } else { + document.querySelectorAll(this.input)[0].addEventListener('keyup', function (e) { + _this3.on.beforeDisplay(); + + _this3.displayResults(data); + }); + } + + ; + } + }, { + key: "checkGhostAPI", + value: function checkGhostAPI() { + if (typeof ghost === 'undefined') { + console.log('Ghost API is not enabled'); + return false; + } + + ; + return true; + } + }, { + key: "checkElements", + value: function checkElements() { + if (!document.querySelectorAll(this.input).length) { + console.log('Input not found.'); + return false; + } + + if (!document.querySelectorAll(this.results).length) { + console.log('Results not found.'); + return false; + } + + ; + + if (this.button != '') { + if (!document.querySelectorAll(this.button).length) { + console.log('Button not found.'); + return false; + } + + ; + } + + return true; + } + }, { + key: "checkFields", + value: function checkFields() { + var validFields = []; + + if (this.api.resource == 'posts') { + validFields = ['amp', 'authors', 'codeinjection_foot', 'codeinjection_head', 'comment_id', 'created_at', 'created_by', 'custom_excerpt', 'custom_template', 'feature_image', 'featured', 'html', 'id', 'locale', 'meta_description', 'meta_title', 'mobiledoc', 'og_description', 'og_image', 'og_title', 'page', 'plaintext', 'primary_author', 'primary_tag', 'published_at', 'published_by', 'slug', 'status', 'tags', 'title', 'twitter_description', 'twitter_image', 'twitter_title', 'updated_at', 'updated_by', 'url', 'uuid', 'visibility']; + } else if (this.api.resource == 'tags') { + validFields = ['count', 'created_at', 'created_by', 'description', 'feature_image', 'id', 'meta_description', 'meta_title', 'name', 'parent', 'slug', 'updated_at', 'updated_by', 'visibility']; + } else if (this.api.resource == 'users') { + validFields = ['accessibility', 'bio', 'count', 'cover_image', 'facebook', 'id', 'locale', 'location', 'meta_description', 'meta_title', 'name', 'profile_image', 'slug', 'tour', 'twitter', 'visibility', 'website']; + } + + for (var i = 0; i < this.api.parameters.fields.length; i++) { + if (!validFields.includes(this.api.parameters.fields[i])) { + console.log('\'' + this.api.parameters.fields[i] + '\' is not a valid field for ' + this.api.resource + '. Valid fields for ' + this.api.resource + ': [\'' + validFields.join('\', \'') + '\']'); + } + } + } + }, { + key: "checkFormats", + value: function checkFormats() { + if (this.api.resource == 'posts' && this.api.parameters.fields && _typeof(this.api.parameters.fields) === 'object' && this.api.parameters.fields.constructor === Array) { + for (var i = 0; i < this.api.parameters.fields.length; i++) { + if (!this.api.parameters.formats.includes(this.api.parameters.fields[i]) && this.api.parameters.fields[i].match(/(plaintext|mobiledoc|amp)/) || this.api.parameters.fields[i] == 'html' && this.api.parameters.formats.length > 0 && !this.api.parameters.formats.includes('html')) { + console.log(this.api.parameters.fields[i] + ' is not included in the formats parameter.'); + } + } + } + } + }, { + key: "checkKeys", + value: function checkKeys() { + var _this4 = this; + + if (!this.options.keys.every(function (elem) { + return _this4.api.parameters.fields.indexOf(elem) > -1; + })) { + console.log('Not all keys are in fields. Please add them.'); + } + + ; + } + }, { + key: "validate", + value: function validate() { + if (!this.checkGhostAPI() || !this.checkElements()) { + return false; + } + + ; + + if (this.development) { + this.checkFields(); + this.checkFormats(); + this.checkKeys(); + } + + ; + return true; + } + }, { + key: "init", + value: function init() { + var _this5 = this; + + if (!this.validate()) { + return; + } + + if (this.trigger == 'focus') { + document.querySelectorAll(this.input)[0].addEventListener('focus', function (e) { + if (!_this5.check) { + _this5.fetch(); + } + + ; + }); + } else if (this.trigger == 'load') { + window.onload = function () { + if (!_this5.check) { + _this5.fetch(); + } + + ; + }; + } + } + }]); + + return GhostSearch; +}(); +//# sourceMappingURL=ghost-search.js.map diff --git a/dist/ghost-search.js.map b/dist/ghost-search.js.map new file mode 100644 index 0000000..6d5715d --- /dev/null +++ b/dist/ghost-search.js.map @@ -0,0 +1 @@ +{"version":3,"names":[],"mappings":"","sources":["ghost-search.js"],"sourcesContent":["'use strict';\n/**\r\n * @requires ../node_modules/fuzzysort/fuzzysort.js\r\n */\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nfunction _typeof(obj) { if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nvar GhostSearch =\n/*#__PURE__*/\nfunction () {\n function GhostSearch(args) {\n _classCallCheck(this, GhostSearch);\n\n this.check = false;\n var defaults = {\n input: '#ghost-search-field',\n results: '#ghost-search-results',\n button: '',\n development: false,\n template: function template(result) {\n var url = [location.protocol, '//', location.host].join('');\n return '' + result.title + '';\n },\n trigger: 'focus',\n options: {\n keys: ['title'],\n limit: 10,\n threshold: -3500,\n allowTypo: false\n },\n api: {\n resource: 'posts',\n parameters: {\n limit: 'all',\n fields: ['title', 'slug'],\n filter: '',\n include: '',\n order: '',\n formats: ''\n }\n },\n on: {\n beforeDisplay: function beforeDisplay() {},\n afterDisplay: function afterDisplay(results) {},\n beforeFetch: function beforeFetch() {},\n afterFetch: function afterFetch(results) {}\n }\n };\n var merged = this.mergeDeep(defaults, args);\n Object.assign(this, merged);\n this.init();\n }\n\n _createClass(GhostSearch, [{\n key: \"mergeDeep\",\n value: function mergeDeep(target, source) {\n var _this = this;\n\n if (target && _typeof(target) === 'object' && !Array.isArray(target) && target !== null && source && _typeof(source) === 'object' && !Array.isArray(source) && source !== null) {\n Object.keys(source).forEach(function (key) {\n if (source[key] && _typeof(source[key]) === 'object' && !Array.isArray(source[key]) && source[key] !== null) {\n if (!target[key]) Object.assign(target, _defineProperty({}, key, {}));\n\n _this.mergeDeep(target[key], source[key]);\n } else {\n Object.assign(target, _defineProperty({}, key, source[key]));\n }\n });\n }\n\n return target;\n }\n }, {\n key: \"url\",\n value: function url() {\n if (this.api.resource == 'posts' && this.api.parameters.include.match(/(tags|authors)/)) {\n delete this.api.parameters.fields;\n }\n\n ;\n var url = ghost.url.api(this.api.resource, this.api.parameters);\n return url;\n }\n }, {\n key: \"fetch\",\n value: function (_fetch) {\n function fetch() {\n return _fetch.apply(this, arguments);\n }\n\n fetch.toString = function () {\n return _fetch.toString();\n };\n\n return fetch;\n }(function () {\n var _this2 = this;\n\n var url = this.url();\n this.on.beforeFetch();\n fetch(url).then(function (response) {\n return response.json();\n }).then(function (resource) {\n return _this2.search(resource);\n }).catch(function (error) {\n return console.error(\"Fetch Error =\\n\", error);\n });\n })\n }, {\n key: \"createElementFromHTML\",\n value: function createElementFromHTML(htmlString) {\n var div = document.createElement('div');\n div.innerHTML = htmlString.trim();\n return div.firstChild;\n }\n }, {\n key: \"cleanup\",\n value: function cleanup(input) {\n return input.toLowerCase().replace(/[^a-zA-Z0-9]+/g, \"-\");\n }\n }, {\n key: \"displayResults\",\n value: function displayResults(data) {\n if (document.querySelectorAll(this.results)[0].firstChild !== null && document.querySelectorAll(this.results)[0].firstChild !== '') {\n while (document.querySelectorAll(this.results)[0].firstChild) {\n document.querySelectorAll(this.results)[0].removeChild(document.querySelectorAll(this.results)[0].firstChild);\n }\n }\n\n ;\n var inputValue = document.querySelectorAll(this.input)[0].value;\n var results = fuzzysort.go(inputValue, data, {\n keys: this.options.keys,\n limit: this.options.limit,\n allowTypo: this.options.allowTypo,\n threshold: this.options.threshold\n });\n\n for (var key in results) {\n if (key < results.length) {\n document.querySelectorAll(this.results)[0].appendChild(this.createElementFromHTML(this.template(results[key].obj)));\n }\n\n ;\n }\n\n this.on.afterDisplay(results);\n }\n }, {\n key: \"search\",\n value: function search(resource) {\n var _this3 = this;\n\n var data = resource[this.api.resource];\n this.on.afterFetch(data);\n this.check = true;\n\n if (this.button != '') {\n var button = document.querySelectorAll(this.button)[0];\n\n if (button.tagName == 'INPUT' && button.type == 'submit') {\n button.closest('form').addEventListener(\"submit\", function (e) {\n e.preventDefault();\n });\n }\n\n ;\n button.addEventListener('click', function (e) {\n e.preventDefault();\n\n _this3.on.beforeDisplay();\n\n _this3.displayResults(data);\n });\n } else {\n document.querySelectorAll(this.input)[0].addEventListener('keyup', function (e) {\n _this3.on.beforeDisplay();\n\n _this3.displayResults(data);\n });\n }\n\n ;\n }\n }, {\n key: \"checkGhostAPI\",\n value: function checkGhostAPI() {\n if (typeof ghost === 'undefined') {\n console.log('Ghost API is not enabled');\n return false;\n }\n\n ;\n return true;\n }\n }, {\n key: \"checkElements\",\n value: function checkElements() {\n if (!document.querySelectorAll(this.input).length) {\n console.log('Input not found.');\n return false;\n }\n\n if (!document.querySelectorAll(this.results).length) {\n console.log('Results not found.');\n return false;\n }\n\n ;\n\n if (this.button != '') {\n if (!document.querySelectorAll(this.button).length) {\n console.log('Button not found.');\n return false;\n }\n\n ;\n }\n\n return true;\n }\n }, {\n key: \"checkFields\",\n value: function checkFields() {\n var validFields = [];\n\n if (this.api.resource == 'posts') {\n validFields = ['amp', 'authors', 'codeinjection_foot', 'codeinjection_head', 'comment_id', 'created_at', 'created_by', 'custom_excerpt', 'custom_template', 'feature_image', 'featured', 'html', 'id', 'locale', 'meta_description', 'meta_title', 'mobiledoc', 'og_description', 'og_image', 'og_title', 'page', 'plaintext', 'primary_author', 'primary_tag', 'published_at', 'published_by', 'slug', 'status', 'tags', 'title', 'twitter_description', 'twitter_image', 'twitter_title', 'updated_at', 'updated_by', 'url', 'uuid', 'visibility'];\n } else if (this.api.resource == 'tags') {\n validFields = ['count', 'created_at', 'created_by', 'description', 'feature_image', 'id', 'meta_description', 'meta_title', 'name', 'parent', 'slug', 'updated_at', 'updated_by', 'visibility'];\n } else if (this.api.resource == 'users') {\n validFields = ['accessibility', 'bio', 'count', 'cover_image', 'facebook', 'id', 'locale', 'location', 'meta_description', 'meta_title', 'name', 'profile_image', 'slug', 'tour', 'twitter', 'visibility', 'website'];\n }\n\n for (var i = 0; i < this.api.parameters.fields.length; i++) {\n if (!validFields.includes(this.api.parameters.fields[i])) {\n console.log('\\'' + this.api.parameters.fields[i] + '\\' is not a valid field for ' + this.api.resource + '. Valid fields for ' + this.api.resource + ': [\\'' + validFields.join('\\', \\'') + '\\']');\n }\n }\n }\n }, {\n key: \"checkFormats\",\n value: function checkFormats() {\n if (this.api.resource == 'posts' && this.api.parameters.fields && _typeof(this.api.parameters.fields) === 'object' && this.api.parameters.fields.constructor === Array) {\n for (var i = 0; i < this.api.parameters.fields.length; i++) {\n if (!this.api.parameters.formats.includes(this.api.parameters.fields[i]) && this.api.parameters.fields[i].match(/(plaintext|mobiledoc|amp)/) || this.api.parameters.fields[i] == 'html' && this.api.parameters.formats.length > 0 && !this.api.parameters.formats.includes('html')) {\n console.log(this.api.parameters.fields[i] + ' is not included in the formats parameter.');\n }\n }\n }\n }\n }, {\n key: \"checkKeys\",\n value: function checkKeys() {\n var _this4 = this;\n\n if (!this.options.keys.every(function (elem) {\n return _this4.api.parameters.fields.indexOf(elem) > -1;\n })) {\n console.log('Not all keys are in fields. Please add them.');\n }\n\n ;\n }\n }, {\n key: \"validate\",\n value: function validate() {\n if (!this.checkGhostAPI() || !this.checkElements()) {\n return false;\n }\n\n ;\n\n if (this.development) {\n this.checkFields();\n this.checkFormats();\n this.checkKeys();\n }\n\n ;\n return true;\n }\n }, {\n key: \"init\",\n value: function init() {\n var _this5 = this;\n\n if (!this.validate()) {\n return;\n }\n\n if (this.trigger == 'focus') {\n document.querySelectorAll(this.input)[0].addEventListener('focus', function (e) {\n if (!_this5.check) {\n _this5.fetch();\n }\n\n ;\n });\n } else if (this.trigger == 'load') {\n window.onload = function () {\n if (!_this5.check) {\n _this5.fetch();\n }\n\n ;\n };\n }\n }\n }]);\n\n return GhostSearch;\n}();"],"file":"ghost-search.js"} \ No newline at end of file diff --git a/dist/ghost-search.min.js b/dist/ghost-search.min.js new file mode 100644 index 0000000..ac26b27 --- /dev/null +++ b/dist/ghost-search.min.js @@ -0,0 +1,10 @@ +/** + * ghost-search 0.1.0 (https://github.com/HauntedThemes/ghost-search) + * A simple but powerful search library for Ghost Blogging Platform. + * Copyright 2018 Haunted Themes (https://www.hauntedthemes.com) + * Released under MIT License + * Released on: 17 Sep 2018 + */ + +function _defineProperty(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function _typeof(e){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _defineProperties(e,t){for(var r=0;r=0;--r){var n=e[r];if(null!==n){var o=n.score;o>t&&(t=o)}}return-9007199254740991===t?null:t}function s(e,t){var r=e[t];if(void 0!==r)return r;var n=t;Array.isArray(t)||(n=t.split("."));for(var o=n.length,i=-1;e&&++i>1]=e[r],o=1+(r<<1)}for(var a=r-1>>1;r>0&&n.score>1)e[r]=e[a];e[r]=n}return r.add=function(r){var n=t;e[t++]=r;for(var o=n-1>>1;n>0&&r.score>1)e[n]=e[o];e[n]=r},r.poll=function(){if(0!==t){var r=e[0];return e[0]=e[--t],n(),r}},r.peek=function(r){if(0!==t)return e[0]},r.replaceTop=function(t){e[0]=t,n()},r},f=c();return function p(d){var h={single:function(e,t,r){return e?(u(e)||(e=h.getPreparedSearch(e)),t?(u(t)||(t=h.getPrepared(t)),((r&&void 0!==r.allowTypo?r.allowTypo:!d||void 0===d.allowTypo||d.allowTypo)?h.algorithm:h.algorithmNoTypo)(e,t,e[0])):null):null},go:function(e,t,r){if(!e)return n;var o=(e=h.prepareSearch(e))[0],i=r&&r.threshold||d&&d.threshold||-9007199254740991,a=r&&r.limit||d&&d.limit||9007199254740991,c=(r&&void 0!==r.allowTypo?r.allowTypo:!d||void 0===d.allowTypo||d.allowTypo)?h.algorithm:h.algorithmNoTypo,p=0,g=0,y=t.length;if(r&&r.keys)for(var m=r.scoreFn||l,v=r.keys,b=v.length,k=y-1;k>=0;--k){for(var _=t[k],w=new Array(b),x=b-1;x>=0;--x)(S=s(_,T=v[x]))?(u(S)||(S=h.getPrepared(S)),w[x]=c(e,S,o)):w[x]=null;w.obj=_;var A=m(w);null!==A&&(Af.peek().score&&f.replaceTop(w))))}else if(r&&r.key){var T=r.key;for(k=y-1;k>=0;--k)(S=s(_=t[k],T))&&(u(S)||(S=h.getPrepared(S)),null!==(C=c(e,S,o))&&(C.scoref.peek().score&&f.replaceTop(C)))))}else for(k=y-1;k>=0;--k){var S,C;(S=t[k])&&(u(S)||(S=h.getPrepared(S)),null!==(C=c(e,S,o))&&(C.scoref.peek().score&&f.replaceTop(C)))))}if(0===p)return n;var j=new Array(p);for(k=p-1;k>=0;--k)j[k]=f.poll();return j.total=p+g,j},goAsync:function(t,r,o){var i=!1,a=new Promise(function(a,f){if(!t)return a(n);var p=(t=h.prepareSearch(t))[0],g=c(),y=r.length-1,m=o&&o.threshold||d&&d.threshold||-9007199254740991,v=o&&o.limit||d&&d.limit||9007199254740991,b=(o&&void 0!==o.allowTypo?o.allowTypo:!d||void 0===d.allowTypo||d.allowTypo)?h.algorithm:h.algorithmNoTypo,k=0,_=0;function w(){if(i)return f("canceled");var c=Date.now();if(o&&o.keys)for(var d=o.scoreFn||l,x=o.keys,A=x.length;y>=0;--y){for(var T=r[y],S=new Array(A),C=A-1;C>=0;--C)(I=s(T,P=x[C]))?(u(I)||(I=h.getPrepared(I)),S[C]=b(t,I,p)):S[C]=null;S.obj=T;var j=d(S);if(null!==j&&!(jg.peek().score&&g.replaceTop(S)),y%1e3==0&&Date.now()-c>=10))return void(e?setImmediate(w):setTimeout(w))}else if(o&&o.key){for(var P=o.key;y>=0;--y)if((I=s(T=r[y],P))&&(u(I)||(I=h.getPrepared(I)),null!==(L=b(t,I,p))&&!(L.scoreg.peek().score&&g.replaceTop(L)),y%1e3==0&&Date.now()-c>=10)))return void(e?setImmediate(w):setTimeout(w))}else for(;y>=0;--y){var I,L;if((I=r[y])&&(u(I)||(I=h.getPrepared(I)),null!==(L=b(t,I,p))&&!(L.scoreg.peek().score&&g.replaceTop(L)),y%1e3==0&&Date.now()-c>=10)))return void(e?setImmediate(w):setTimeout(w))}if(0===k)return a(n);for(var B=new Array(k),q=k-1;q>=0;--q)B[q]=g.poll();B.total=k+_,a(B)}e?setImmediate(w):w()});return a.cancel=function(){i=!0},a},highlight:function(e,t,r){if(null===e)return null;void 0===t&&(t=""),void 0===r&&(r="");for(var n="",o=0,i=!1,a=e.target,l=a.length,s=e.indexes,u=0;u999)return h.prepare(e);var r=t.get(e);return void 0!==r?r:(r=h.prepare(e),t.set(e,r),r)},getPreparedSearch:function(e){if(e.length>999)return h.prepareSearch(e);var t=r.get(e);return void 0!==t?t:(t=h.prepareSearch(e),r.set(e,t),t)},algorithm:function(e,t,r){for(var n=t._targetLowerCodes,a=e.length,l=n.length,s=0,u=0,c=0,f=0;;){if(r===n[u]){if(o[f++]=u,++s===a)break;r=e[0===c?s:c===s?s+1:c===s-1?s-1:s]}if(++u>=l)for(;;){if(s<=1)return null;if(0===c){if(r===e[--s])continue;c=s}else{if(1===c)return null;if((r=e[1+(s=--c)])===e[s])continue}u=o[(f=s)-1]+1;break}}s=0;var p=0,d=!1,g=0,y=t._nextBeginningIndexes;null===y&&(y=t._nextBeginningIndexes=h.prepareNextBeginningIndexes(t.target));var m=u=0===o[0]?0:y[o[0]-1];if(u!==l)for(;;)if(u>=l){if(s<=0){if(++p>a-2)break;if(e[p]===e[p+1])continue;u=m;continue}--s,u=y[i[--g]]}else if(e[0===p?s:p===s?s+1:p===s-1?s-1:s]===n[u]){if(i[g++]=u,++s===a){d=!0;break}++u}else u=y[u];if(d)var v=i,b=g;else v=o,b=f;for(var k=0,_=-1,w=0;w=0;--w)t.indexes[w]=v[w];return t},algorithmNoTypo:function(e,t,r){for(var n=t._targetLowerCodes,a=e.length,l=n.length,s=0,u=0,c=0;;){if(r===n[u]){if(o[c++]=u,++s===a)break;r=e[s]}if(++u>=l)return null}s=0;var f=!1,p=0,d=t._nextBeginningIndexes;if(null===d&&(d=t._nextBeginningIndexes=h.prepareNextBeginningIndexes(t.target)),(u=0===o[0]?0:d[o[0]-1])!==l)for(;;)if(u>=l){if(s<=0)break;--s,u=d[i[--p]]}else if(e[s]===n[u]){if(i[p++]=u,++s===a){f=!0;break}++u}else u=d[u];if(f)var g=i,y=p;else g=o,y=c;for(var m=0,v=-1,b=0;b=0;--b)t.indexes[b]=g[b];return t},prepareLowerCodes:function(e){for(var t=e.length,r=[],n=e.toLowerCase(),o=0;o=65&&l<=90,u=s||l>=97&&l<=122||l>=48&&l<=57,c=s&&!o||!i||!u;o=s,i=u,c&&(r[n++]=a)}return r},prepareNextBeginningIndexes:function(e){for(var t=e.length,r=h.prepareBeginningIndexes(e),n=[],o=r[0],i=0,a=0;aa?n[a]=o:(o=r[++i],n[a]=void 0===o?t:o);return n},cleanup:a,new:p};return h}()});var GhostSearch=function(){function e(t){_classCallCheck(this,e),this.check=!1;var r={input:"#ghost-search-field",results:"#ghost-search-results",button:"",development:!1,template:function(e){return''+e.title+""},trigger:"focus",options:{keys:["title"],limit:10,threshold:-3500,allowTypo:!1},api:{resource:"posts",parameters:{limit:"all",fields:["title","slug"],filter:"",include:"",order:"",formats:""}},on:{beforeDisplay:function(){},afterDisplay:function(e){},beforeFetch:function(){},afterFetch:function(e){}}},n=this.mergeDeep(r,t);Object.assign(this,n),this.init()}return _createClass(e,[{key:"mergeDeep",value:function(e,t){var r=this;return e&&"object"===_typeof(e)&&!Array.isArray(e)&&null!==e&&t&&"object"===_typeof(t)&&!Array.isArray(t)&&null!==t&&Object.keys(t).forEach(function(n){t[n]&&"object"===_typeof(t[n])&&!Array.isArray(t[n])&&null!==t[n]?(e[n]||Object.assign(e,_defineProperty({},n,{})),r.mergeDeep(e[n],t[n])):Object.assign(e,_defineProperty({},n,t[n]))}),e}},{key:"url",value:function(){"posts"==this.api.resource&&this.api.parameters.include.match(/(tags|authors)/)&&delete this.api.parameters.fields;var e=ghost.url.api(this.api.resource,this.api.parameters);return e}},{key:"fetch",value:function(e){function t(){return e.apply(this,arguments)}return t.toString=function(){return e.toString()},t}(function(){var e=this,t=this.url();this.on.beforeFetch(),fetch(t).then(function(e){return e.json()}).then(function(t){return e.search(t)}).catch(function(e){return console.error("Fetch Error =\n",e)})})},{key:"createElementFromHTML",value:function(e){var t=document.createElement("div");return t.innerHTML=e.trim(),t.firstChild}},{key:"cleanup",value:function(e){return e.toLowerCase().replace(/[^a-zA-Z0-9]+/g,"-")}},{key:"displayResults",value:function(e){if(null!==document.querySelectorAll(this.results)[0].firstChild&&""!==document.querySelectorAll(this.results)[0].firstChild)for(;document.querySelectorAll(this.results)[0].firstChild;)document.querySelectorAll(this.results)[0].removeChild(document.querySelectorAll(this.results)[0].firstChild);var t=document.querySelectorAll(this.input)[0].value,r=fuzzysort.go(t,e,{keys:this.options.keys,limit:this.options.limit,allowTypo:this.options.allowTypo,threshold:this.options.threshold});for(var n in r)n0&&!this.api.parameters.formats.includes("html"))&&console.log(this.api.parameters.fields[e]+" is not included in the formats parameter.")}},{key:"checkKeys",value:function(){var e=this;this.options.keys.every(function(t){return e.api.parameters.fields.indexOf(t)>-1})||console.log("Not all keys are in fields. Please add them.")}},{key:"validate",value:function(){return!(!this.checkGhostAPI()||!this.checkElements())&&(this.development&&(this.checkFields(),this.checkFormats(),this.checkKeys()),!0)}},{key:"init",value:function(){var e=this;this.validate()&&("focus"==this.trigger?document.querySelectorAll(this.input)[0].addEventListener("focus",function(t){e.check||e.fetch()}):"load"==this.trigger&&(window.onload=function(){e.check||e.fetch()}))}}]),e}(); +//# sourceMappingURL=ghost-search.min.js.map diff --git a/dist/ghost-search.min.js.map b/dist/ghost-search.min.js.map new file mode 100644 index 0000000..da36069 --- /dev/null +++ b/dist/ghost-search.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["ghost-search.js"],"names":["_defineProperty","obj","key","value","Object","defineProperty","enumerable","configurable","writable","_typeof","Symbol","iterator","constructor","prototype","_classCallCheck","instance","Constructor","TypeError","_defineProperties","target","props","i","length","descriptor","_createClass","protoProps","staticProps","root","UMD","define","amd","module","exports","fuzzysort","this","isNode","require","window","preparedCache","Map","preparedSearchCache","noResults","total","matchesSimple","matchesStrict","cleanup","clear","defaultScoreFn","a","max","result","score","getValue","prop","tmp","undefined","segs","Array","isArray","split","len","isObj","x","fastpriorityqueue","r","o","e","n","c","f","add","poll","peek","replaceTop","q","fuzzysortNew","instanceOptions","single","search","options","getPreparedSearch","getPrepared","allowTypo","algorithm","algorithmNoTypo","go","targets","searchLowerCode","prepareSearch","threshold","limit","resultsLen","limitedCount","targetsLen","keys","scoreFn","keysLen","objResults","keyI","_targetLowerCodes","_nextBeginningIndexes","indexes","results","goAsync","canceled","p","Promise","resolve","reject","iCurrent","step","startMs","Date","now","setImmediate","setTimeout","cancel","highlight","hOpen","hClose","highlighted","matchesIndex","opened","targetLen","matchesBest","char","substr","prepare","prepareLowerCodes","prepareSlow","prepareNextBeginningIndexes","targetPrepared","get","set","searchPrepared","searchLowerCodes","prepared","targetLowerCodes","searchLen","searchI","targetI","typoSimpleI","matchesSimpleLen","typoStrictI","successStrict","matchesStrictLen","nextBeginningIndexes","firstPossibleI","matchesBestLen","lastTargetI","str","strLen","lowerCodes","lower","toLowerCase","charCodeAt","prepareBeginningIndexes","beginningIndexes","beginningIndexesLen","wasUpper","wasAlphanum","targetCode","isUpper","isAlphanum","isBeginning","lastIsBeginning","lastIsBeginningI","new","GhostSearch","args","check","defaults","input","button","development","template","location","protocol","host","join","slug","title","trigger","api","resource","parameters","fields","filter","include","order","formats","on","beforeDisplay","afterDisplay","beforeFetch","afterFetch","merged","mergeDeep","assign","init","source","_this","forEach","match","url","ghost","_fetch","fetch","apply","arguments","toString","_this2","then","response","json","catch","error","console","htmlString","div","document","createElement","innerHTML","trim","firstChild","replace","data","querySelectorAll","removeChild","inputValue","appendChild","createElementFromHTML","_this3","tagName","type","closest","addEventListener","preventDefault","displayResults","log","validFields","includes","_this4","every","elem","indexOf","checkGhostAPI","checkElements","checkFields","checkFormats","checkKeys","_this5","validate","onload"],"mappings":"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","sourcesContent":["'use strict';\n/**\r\n * @requires ../node_modules/fuzzysort/fuzzysort.js\r\n */\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nfunction _typeof(obj) { if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nvar GhostSearch =\n/*#__PURE__*/\nfunction () {\n function GhostSearch(args) {\n _classCallCheck(this, GhostSearch);\n\n this.check = false;\n var defaults = {\n input: '#ghost-search-field',\n results: '#ghost-search-results',\n button: '',\n development: false,\n template: function template(result) {\n var url = [location.protocol, '//', location.host].join('');\n return '' + result.title + '';\n },\n trigger: 'focus',\n options: {\n keys: ['title'],\n limit: 10,\n threshold: -3500,\n allowTypo: false\n },\n api: {\n resource: 'posts',\n parameters: {\n limit: 'all',\n fields: ['title', 'slug'],\n filter: '',\n include: '',\n order: '',\n formats: ''\n }\n },\n on: {\n beforeDisplay: function beforeDisplay() {},\n afterDisplay: function afterDisplay(results) {},\n beforeFetch: function beforeFetch() {},\n afterFetch: function afterFetch(results) {}\n }\n };\n var merged = this.mergeDeep(defaults, args);\n Object.assign(this, merged);\n this.init();\n }\n\n _createClass(GhostSearch, [{\n key: \"mergeDeep\",\n value: function mergeDeep(target, source) {\n var _this = this;\n\n if (target && _typeof(target) === 'object' && !Array.isArray(target) && target !== null && source && _typeof(source) === 'object' && !Array.isArray(source) && source !== null) {\n Object.keys(source).forEach(function (key) {\n if (source[key] && _typeof(source[key]) === 'object' && !Array.isArray(source[key]) && source[key] !== null) {\n if (!target[key]) Object.assign(target, _defineProperty({}, key, {}));\n\n _this.mergeDeep(target[key], source[key]);\n } else {\n Object.assign(target, _defineProperty({}, key, source[key]));\n }\n });\n }\n\n return target;\n }\n }, {\n key: \"url\",\n value: function url() {\n if (this.api.resource == 'posts' && this.api.parameters.include.match(/(tags|authors)/)) {\n delete this.api.parameters.fields;\n }\n\n ;\n var url = ghost.url.api(this.api.resource, this.api.parameters);\n return url;\n }\n }, {\n key: \"fetch\",\n value: function (_fetch) {\n function fetch() {\n return _fetch.apply(this, arguments);\n }\n\n fetch.toString = function () {\n return _fetch.toString();\n };\n\n return fetch;\n }(function () {\n var _this2 = this;\n\n var url = this.url();\n this.on.beforeFetch();\n fetch(url).then(function (response) {\n return response.json();\n }).then(function (resource) {\n return _this2.search(resource);\n }).catch(function (error) {\n return console.error(\"Fetch Error =\\n\", error);\n });\n })\n }, {\n key: \"createElementFromHTML\",\n value: function createElementFromHTML(htmlString) {\n var div = document.createElement('div');\n div.innerHTML = htmlString.trim();\n return div.firstChild;\n }\n }, {\n key: \"cleanup\",\n value: function cleanup(input) {\n return input.toLowerCase().replace(/[^a-zA-Z0-9]+/g, \"-\");\n }\n }, {\n key: \"displayResults\",\n value: function displayResults(data) {\n if (document.querySelectorAll(this.results)[0].firstChild !== null && document.querySelectorAll(this.results)[0].firstChild !== '') {\n while (document.querySelectorAll(this.results)[0].firstChild) {\n document.querySelectorAll(this.results)[0].removeChild(document.querySelectorAll(this.results)[0].firstChild);\n }\n }\n\n ;\n var inputValue = document.querySelectorAll(this.input)[0].value;\n var results = fuzzysort.go(inputValue, data, {\n keys: this.options.keys,\n limit: this.options.limit,\n allowTypo: this.options.allowTypo,\n threshold: this.options.threshold\n });\n\n for (var key in results) {\n if (key < results.length) {\n document.querySelectorAll(this.results)[0].appendChild(this.createElementFromHTML(this.template(results[key].obj)));\n }\n\n ;\n }\n\n this.on.afterDisplay(results);\n }\n }, {\n key: \"search\",\n value: function search(resource) {\n var _this3 = this;\n\n var data = resource[this.api.resource];\n this.on.afterFetch(data);\n this.check = true;\n\n if (this.button != '') {\n var button = document.querySelectorAll(this.button)[0];\n\n if (button.tagName == 'INPUT' && button.type == 'submit') {\n button.closest('form').addEventListener(\"submit\", function (e) {\n e.preventDefault();\n });\n }\n\n ;\n button.addEventListener('click', function (e) {\n e.preventDefault();\n\n _this3.on.beforeDisplay();\n\n _this3.displayResults(data);\n });\n } else {\n document.querySelectorAll(this.input)[0].addEventListener('keyup', function (e) {\n _this3.on.beforeDisplay();\n\n _this3.displayResults(data);\n });\n }\n\n ;\n }\n }, {\n key: \"checkGhostAPI\",\n value: function checkGhostAPI() {\n if (typeof ghost === 'undefined') {\n console.log('Ghost API is not enabled');\n return false;\n }\n\n ;\n return true;\n }\n }, {\n key: \"checkElements\",\n value: function checkElements() {\n if (!document.querySelectorAll(this.input).length) {\n console.log('Input not found.');\n return false;\n }\n\n if (!document.querySelectorAll(this.results).length) {\n console.log('Results not found.');\n return false;\n }\n\n ;\n\n if (this.button != '') {\n if (!document.querySelectorAll(this.button).length) {\n console.log('Button not found.');\n return false;\n }\n\n ;\n }\n\n return true;\n }\n }, {\n key: \"checkFields\",\n value: function checkFields() {\n var validFields = [];\n\n if (this.api.resource == 'posts') {\n validFields = ['amp', 'authors', 'codeinjection_foot', 'codeinjection_head', 'comment_id', 'created_at', 'created_by', 'custom_excerpt', 'custom_template', 'feature_image', 'featured', 'html', 'id', 'locale', 'meta_description', 'meta_title', 'mobiledoc', 'og_description', 'og_image', 'og_title', 'page', 'plaintext', 'primary_author', 'primary_tag', 'published_at', 'published_by', 'slug', 'status', 'tags', 'title', 'twitter_description', 'twitter_image', 'twitter_title', 'updated_at', 'updated_by', 'url', 'uuid', 'visibility'];\n } else if (this.api.resource == 'tags') {\n validFields = ['count', 'created_at', 'created_by', 'description', 'feature_image', 'id', 'meta_description', 'meta_title', 'name', 'parent', 'slug', 'updated_at', 'updated_by', 'visibility'];\n } else if (this.api.resource == 'users') {\n validFields = ['accessibility', 'bio', 'count', 'cover_image', 'facebook', 'id', 'locale', 'location', 'meta_description', 'meta_title', 'name', 'profile_image', 'slug', 'tour', 'twitter', 'visibility', 'website'];\n }\n\n for (var i = 0; i < this.api.parameters.fields.length; i++) {\n if (!validFields.includes(this.api.parameters.fields[i])) {\n console.log('\\'' + this.api.parameters.fields[i] + '\\' is not a valid field for ' + this.api.resource + '. Valid fields for ' + this.api.resource + ': [\\'' + validFields.join('\\', \\'') + '\\']');\n }\n }\n }\n }, {\n key: \"checkFormats\",\n value: function checkFormats() {\n if (this.api.resource == 'posts' && this.api.parameters.fields && _typeof(this.api.parameters.fields) === 'object' && this.api.parameters.fields.constructor === Array) {\n for (var i = 0; i < this.api.parameters.fields.length; i++) {\n if (!this.api.parameters.formats.includes(this.api.parameters.fields[i]) && this.api.parameters.fields[i].match(/(plaintext|mobiledoc|amp)/) || this.api.parameters.fields[i] == 'html' && this.api.parameters.formats.length > 0 && !this.api.parameters.formats.includes('html')) {\n console.log(this.api.parameters.fields[i] + ' is not included in the formats parameter.');\n }\n }\n }\n }\n }, {\n key: \"checkKeys\",\n value: function checkKeys() {\n var _this4 = this;\n\n if (!this.options.keys.every(function (elem) {\n return _this4.api.parameters.fields.indexOf(elem) > -1;\n })) {\n console.log('Not all keys are in fields. Please add them.');\n }\n\n ;\n }\n }, {\n key: \"validate\",\n value: function validate() {\n if (!this.checkGhostAPI() || !this.checkElements()) {\n return false;\n }\n\n ;\n\n if (this.development) {\n this.checkFields();\n this.checkFormats();\n this.checkKeys();\n }\n\n ;\n return true;\n }\n }, {\n key: \"init\",\n value: function init() {\n var _this5 = this;\n\n if (!this.validate()) {\n return;\n }\n\n if (this.trigger == 'focus') {\n document.querySelectorAll(this.input)[0].addEventListener('focus', function (e) {\n if (!_this5.check) {\n _this5.fetch();\n }\n\n ;\n });\n } else if (this.trigger == 'load') {\n window.onload = function () {\n if (!_this5.check) {\n _this5.fetch();\n }\n\n ;\n };\n }\n }\n }]);\n\n return GhostSearch;\n}();"],"file":"ghost-search.min.js"} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..0e234af --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,40 @@ +const gulp = require('gulp'); +const minify = require('gulp-minify'); +const babel = require('gulp-babel'); +const sourcemaps = require('gulp-sourcemaps'); +const resolveDependencies = require('gulp-resolve-dependencies'); +const concat = require('gulp-concat'); +const headerComment = require('gulp-header-comment'); + +gulp.task('scripts', () => + gulp.src('src/ghost-search.js') + .pipe(babel({ + presets: ['@babel/env'] + })) + .pipe(sourcemaps.init()) + .pipe(resolveDependencies({ + pattern: /\* @requires [\s-]*(.*\.js)/g + })) + .pipe(concat('ghost-search.js')) + .pipe(minify({ + ext: { + min: '.min.js' + }, + preserveComments: 'some' + })) + .pipe(headerComment(` + <%= pkg.name %> <%= pkg.version %> (<%= pkg.homepage %>) + <%= pkg.description %> + Copyright <%= moment().format('YYYY') %> Haunted Themes (<%= pkg.author.url %>) + Released under <%= pkg.license %> License + Released on: <%= moment().format('D MMM YYYY') %> + `)) + .pipe(sourcemaps.write('/')) + .pipe(gulp.dest('dist')) +); + +gulp.task('watch', () => + gulp.watch('src/*.js', ['scripts']) +); + +gulp.task('default', ['scripts', 'watch']); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..858d32a --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "ghost-search", + "version": "0.1.0", + "description": "A simple but powerful search library for Ghost Blogging Platform.", + "main": "ghost-search.js", + "scripts": { + "dev": "gulp" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/HauntedThemes/ghost-search.git" + }, + "keywords": [ + "ghost", + "search" + ], + "author": { + "name": "Haunted Themes", + "email": "contact@hauntedthemes.com", + "url": "https://www.hauntedthemes.com" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/HauntedThemes/ghost-search/issues" + }, + "homepage": "https://github.com/HauntedThemes/ghost-search", + "dependencies": { + "fuzzysort": "^1.1.4" + }, + "devDependencies": { + "@babel/core": "^7.0.1", + "@babel/preset-env": "^7.0.0", + "gulp": "^3.9.1", + "gulp-babel": "^8.0.0", + "gulp-concat": "^2.6.1", + "gulp-header-comment": "^0.4.0", + "gulp-minify": "^3.1.0", + "gulp-resolve-dependencies": "^2.2.0", + "gulp-sourcemaps": "^2.6.4" + } +} diff --git a/src/ghost-search.js b/src/ghost-search.js new file mode 100644 index 0000000..a322ec9 --- /dev/null +++ b/src/ghost-search.js @@ -0,0 +1,260 @@ +'use strict'; + +/** + * @requires ../node_modules/fuzzysort/fuzzysort.js + */ + +class GhostSearch { + constructor(args) { + + this.check = false; + + const defaults = { + input: '#ghost-search-field', + results: '#ghost-search-results', + button: '', + development: false, + template: function(result) { + let url = [location.protocol, '//', location.host].join(''); + return '' + result.title + ''; + }, + trigger: 'focus', + options: { + keys: [ + 'title' + ], + limit: 10, + threshold: -3500, + allowTypo: false + }, + api: { + resource: 'posts', + parameters: { + limit: 'all', + fields: ['title', 'slug'], + filter: '', + include: '', + order: '', + formats: '', + }, + }, + on: { + beforeDisplay: function(){}, + afterDisplay: function(results){}, + beforeFetch: function(){}, + afterFetch: function(results){} + } + } + + const merged = this.mergeDeep(defaults, args); + Object.assign(this, merged); + this.init(); + + } + + mergeDeep(target, source) { + if ((target && typeof target === 'object' && !Array.isArray(target) && target !== null) && (source && typeof source === 'object' && !Array.isArray(source) && source !== null)) { + Object.keys(source).forEach(key => { + if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) && source[key] !== null) { + if (!target[key]) Object.assign(target, { [key]: {} }); + this.mergeDeep(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + }); + } + return target; + } + + url(){ + + if (this.api.resource == 'posts' && this.api.parameters.include.match( /(tags|authors)/ )) { + delete this.api.parameters.fields; + }; + + let url = ghost.url.api(this.api.resource, this.api.parameters); + + return url; + + } + + fetch(){ + + let url = this.url(); + + this.on.beforeFetch(); + + fetch(url) + .then(response => response.json()) + .then(resource => this.search(resource)) + .catch(error => console.error(`Fetch Error =\n`, error)); + + } + + createElementFromHTML(htmlString) { + var div = document.createElement('div'); + div.innerHTML = htmlString.trim(); + return div.firstChild; + } + + cleanup(input) { + return input.toLowerCase().replace(/[^a-zA-Z0-9]+/g, "-"); + } + + displayResults(data){ + + if (document.querySelectorAll(this.results)[0].firstChild !== null && document.querySelectorAll(this.results)[0].firstChild !== '') { + while (document.querySelectorAll(this.results)[0].firstChild) { + document.querySelectorAll(this.results)[0].removeChild(document.querySelectorAll(this.results)[0].firstChild); + } + }; + + let inputValue = document.querySelectorAll(this.input)[0].value; + const results = fuzzysort.go(inputValue, data, { + keys: this.options.keys, + limit: this.options.limit, + allowTypo: this.options.allowTypo, + threshold: this.options.threshold + }); + for (let key in results){ + if (key < results.length) { + document.querySelectorAll(this.results)[0].appendChild(this.createElementFromHTML(this.template(results[key].obj))); + }; + } + + this.on.afterDisplay(results) + + } + + search(resource){ + + let data = resource[this.api.resource]; + + this.on.afterFetch(data); + this.check = true; + + if (this.button != '') { + let button = document.querySelectorAll(this.button)[0]; + if (button.tagName == 'INPUT' && button.type == 'submit') { + button.closest('form').addEventListener("submit", e => { + e.preventDefault() + }); + }; + button.addEventListener('click', e => { + e.preventDefault() + this.on.beforeDisplay() + this.displayResults(data) + }) + }else{ + document.querySelectorAll(this.input)[0].addEventListener('keyup', e => { + this.on.beforeDisplay() + this.displayResults(data) + }) + }; + + } + + checkGhostAPI(){ + if (typeof ghost === 'undefined') { + console.log('Ghost API is not enabled'); + return false; + }; + return true; + } + + checkElements(){ + if(!document.querySelectorAll(this.input).length){ + console.log('Input not found.'); + return false; + } + if(!document.querySelectorAll(this.results).length){ + console.log('Results not found.'); + return false; + }; + if(this.button != ''){ + if (!document.querySelectorAll(this.button).length) { + console.log('Button not found.'); + return false; + }; + } + return true; + } + + checkFields(){ + + let validFields = []; + + if (this.api.resource == 'posts') { + validFields = ['amp', 'authors', 'codeinjection_foot', 'codeinjection_head', 'comment_id', 'created_at', 'created_by', 'custom_excerpt', 'custom_template', 'feature_image', 'featured', 'html', 'id', 'locale', 'meta_description', 'meta_title', 'mobiledoc', 'og_description', 'og_image', 'og_title', 'page', 'plaintext', 'primary_author', 'primary_tag', 'published_at', 'published_by', 'slug', 'status', 'tags', 'title', 'twitter_description', 'twitter_image', 'twitter_title', 'updated_at', 'updated_by', 'url', 'uuid', 'visibility']; + }else if(this.api.resource == 'tags'){ + validFields = ['count', 'created_at', 'created_by', 'description', 'feature_image', 'id', 'meta_description', 'meta_title', 'name', 'parent', 'slug', 'updated_at', 'updated_by', 'visibility'] + }else if(this.api.resource == 'users'){ + validFields = ['accessibility', 'bio', 'count', 'cover_image', 'facebook', 'id', 'locale', 'location', 'meta_description', 'meta_title', 'name', 'profile_image', 'slug', 'tour', 'twitter', 'visibility', 'website'] + } + + for (let i = 0; i < this.api.parameters.fields.length; i++) { + if (!validFields.includes(this.api.parameters.fields[i])) { + console.log('\'' + this.api.parameters.fields[i] + '\' is not a valid field for ' + this.api.resource + '. Valid fields for ' + this.api.resource + ': [\'' + validFields.join('\', \'') + '\']'); + } + } + + } + + checkFormats(){ + if (this.api.resource == 'posts' && (this.api.parameters.fields && typeof this.api.parameters.fields === 'object' && this.api.parameters.fields.constructor === Array)) { + for (let i = 0; i < this.api.parameters.fields.length; i++) { + if ( + !this.api.parameters.formats.includes(this.api.parameters.fields[i]) && this.api.parameters.fields[i].match( /(plaintext|mobiledoc|amp)/ ) || + (this.api.parameters.fields[i] == 'html' && this.api.parameters.formats.length > 0 && !this.api.parameters.formats.includes('html')) + ) { + console.log(this.api.parameters.fields[i] + ' is not included in the formats parameter.'); + } + } + } + } + + checkKeys(){ + if (!this.options.keys.every(elem => this.api.parameters.fields.indexOf(elem) > -1)) { + console.log('Not all keys are in fields. Please add them.'); + }; + } + + validate(){ + + if (!this.checkGhostAPI() || !this.checkElements()) { + return false; + }; + + if (this.development) { + this.checkFields(); + this.checkFormats(); + this.checkKeys(); + }; + + return true; + + } + + init(){ + + if (!this.validate()) { + return; + } + + if (this.trigger == 'focus') { + document.querySelectorAll(this.input)[0].addEventListener('focus', e => { + if (!this.check) { + this.fetch() + }; + }) + }else if(this.trigger == 'load'){ + window.onload = () => { + if (!this.check) { + this.fetch() + }; + } + } + + } + +} \ No newline at end of file