├── .gitignore ├── assets └── video-image.png ├── dist ├── templates │ ├── result.php │ └── finder.php ├── readme.md ├── assets │ ├── css │ │ └── finder.css │ └── js │ │ └── finder.js └── fuzzy-finder-wp.php ├── src ├── templates │ ├── result.php │ └── finder.php ├── assets │ ├── less │ │ ├── variables.less │ │ ├── components.less │ │ ├── utils.less │ │ └── finder.less │ └── js │ │ └── finder.js ├── readme.md └── fuzzy-finder-wp.php ├── package.json ├── Readme.md └── Gruntfile.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /assets/video-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NateWr/fuzzy-finder-wp/HEAD/assets/video-image.png -------------------------------------------------------------------------------- /dist/templates/result.php: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | {string} 4 | 5 |
  • 6 | -------------------------------------------------------------------------------- /src/templates/result.php: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | {string} 4 | 5 |
  • 6 | -------------------------------------------------------------------------------- /src/assets/less/variables.less: -------------------------------------------------------------------------------- 1 | // 2 | // Variables for the fuzzy finder less stylesheet 3 | // 4 | 5 | // Colors 6 | @primary: #0073aa; 7 | @bg: #ddd; 8 | @light-shade: #eee; 9 | @lift: #fff; 10 | @border-color: #ddd; 11 | @text-light: #777; 12 | @text-light-rgba: rgba(0, 0, 0, 0.54); 13 | 14 | // Fonts 15 | @font-sml: 11px; 16 | 17 | // Borders 18 | @border: 1px solid @border-color; 19 | @radius: 0.5em; 20 | -------------------------------------------------------------------------------- /src/assets/less/components.less: -------------------------------------------------------------------------------- 1 | // 2 | // Re-usable components 3 | // 4 | 5 | .ffwp-spinner { 6 | 7 | &:after { 8 | display: inline-block; 9 | position: relative; 10 | width: 20px; 11 | height: 20px; 12 | .animation( cffrtbrotate .6s linear infinite ); 13 | border-radius: 100%; 14 | border-top: 1px solid #888; 15 | border-bottom: 1px solid #bbb; 16 | border-left: 1px solid #888; 17 | border-right: 1px solid #bbb; 18 | vertical-align: middle; 19 | content: ''; 20 | opacity: 0.5; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fuzzy-finder-wp", 3 | "description": "A fuzzy finder for your WordPress admin. Quickly search for Posts, Pages, Categories, Tags and Users.", 4 | "version": "0.2.0", 5 | "author": { 6 | "name": "Nate Wright", 7 | "url": "https://github.com/NateWr/" 8 | }, 9 | "devDependencies": { 10 | "grunt": "~0.4.2", 11 | "grunt-contrib-compress": "~1.3.0", 12 | "grunt-contrib-concat": "~0.3.0", 13 | "grunt-contrib-copy": "~0.8.0", 14 | "grunt-contrib-jshint": "~0.6.0", 15 | "grunt-contrib-less": "~0.9.0", 16 | "grunt-contrib-nodeunit": "~0.2.0", 17 | "grunt-contrib-uglify": "~0.2.2", 18 | "grunt-contrib-watch": "~0.4.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/less/utils.less: -------------------------------------------------------------------------------- 1 | // 2 | // Mix-ins, animations, utilities and helper functions 3 | // 4 | 5 | // Mix-ins 6 | .transform (@transform) { 7 | transform: @transform; 8 | -webkit-transform: @transform; 9 | -moz-transform: @transform; 10 | -o-transform: @transform; 11 | } 12 | 13 | .animation(@animation) { 14 | -webkit-animation: @animation; 15 | -moz-animation: @animation; 16 | -ms-animation: @animation; 17 | -o-animation: @animation; 18 | animation: @animation; 19 | } 20 | 21 | // Animations 22 | @keyframes cffrtbrotate { 23 | 0% { 24 | .transform(rotateZ(-360deg)); 25 | } 26 | 100% { 27 | .transform(rotateZ(0deg)); 28 | } 29 | } 30 | 31 | @-webkit-keyframes cffrtbrotate { 32 | 0% { 33 | .transform(rotateZ(-360deg)); 34 | } 35 | 100% { 36 | .transform(rotateZ(0deg)); 37 | } 38 | } 39 | 40 | @-moz-keyframes cffrtbrotate { 41 | 0% { 42 | .transform(rotateZ(-360deg)); 43 | } 44 | 100% { 45 | .transform(rotateZ(0deg)); 46 | } 47 | } 48 | 49 | @-o-keyframes cffrtbrotate { 50 | 0% { 51 | .transform(rotateZ(-360deg)); 52 | } 53 | 100% { 54 | .transform(rotateZ(0deg)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Fuzzy Finder 2 | 3 | [![Video demonstrating the fuzzy finder](https://github.com/NateWr/fuzzy-finder-wp/blob/master/assets/video-image.png)](https://www.youtube.com/watch?v=d75mT2fkQUc) 4 | 5 | A fuzzy finder for your WordPress admin. ctrl-shift-f from any WordPress admin area to quickly search for Posts, Pages, Categories, Tags and Users. 6 | 7 | Search results are cached in your browser's local storage. Previous results that match your current search will be shown immediately. 8 | 9 | It will also search custom post types that have been registered with the `show_ui` and `show_in_rest` arguments set to `true`. 10 | 11 | Inspired by the [Atom package](https://github.com/atom/fuzzy-finder). Leans heavily on [client-js](https://github.com/WP-API/client-js) for handling REST API content endpoints. 12 | 13 | ## Installation 14 | 15 | Download the latest [release package](https://github.com/NateWr/fuzzy-finder-wp/releases), upload to your WordPress site from Plugins > Add New > Upload Plugin, and activate the Fuzzy Finder from the Plugins list. 16 | 17 | Or install with [WP-CLI](http://wp-cli.org/): `wp plugin install --activate`. 18 | -------------------------------------------------------------------------------- /dist/templates/finder.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 7 | 8 |
    9 |
      10 |
      11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
      29 |
      30 |
      31 | -------------------------------------------------------------------------------- /src/templates/finder.php: -------------------------------------------------------------------------------- 1 |
      2 |
      3 |
      4 | 7 | 8 |
      9 |
        10 |
        11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
        29 |
        30 |
        31 | -------------------------------------------------------------------------------- /dist/readme.md: -------------------------------------------------------------------------------- 1 | # Restaurant Reservations 2 | Contributors: NateWr 3 |
        4 | Author URI: https://github.com/NateWr 5 |
        6 | Plugin URL: https://github.com/NateWr/fuzzy-finder-wp 7 |
        8 | Requires at Least: 4.7 9 |
        10 | Tested Up To: 4.7 11 |
        12 | Tags: 13 |
        14 | Stable tag: 0.2 15 |
        16 | License: GPLv2 or later 17 |
        18 | Donate link: https://github.com/NateWr 19 | 20 | A fuzzy finder for your WordPress admin. ctrl-shift-f from any WordPress admin area to quickly search Posts, Pages, Categories, Tags and Users. 21 | 22 | ## Description 23 | 24 | A fuzzy finder for your WordPress admin. ctrl-shift-f from any WordPress admin area to quickly search for Posts, Pages, Categories, Tags and Users. 25 | 26 | Search results are cached in your browser's local storage. Previous results that match your current search will be shown immediately. 27 | 28 | It will also search custom post types that have been registered with the `show_ui` and `show_in_rest` arguments set to `true`. 29 | 30 | Inspired by the [Atom package](https://github.com/atom/fuzzy-finder). Leans heavily on [client-js](https://github.com/WP-API/client-js) for handling REST API content endpoints. 31 | 32 | ## Changelog 33 | 34 | ### 0.2 (2016-12-11) 35 | * Search Posts, Pages, Categories, Tags and Users 36 | * Store past searches in browser's Local Storage 37 | 38 | ### 0.1 (2015-07-17) 39 | * Hello World! 40 | -------------------------------------------------------------------------------- /src/readme.md: -------------------------------------------------------------------------------- 1 | # Restaurant Reservations 2 | Contributors: NateWr 3 |
        4 | Author URI: https://github.com/NateWr 5 |
        6 | Plugin URL: https://github.com/NateWr/fuzzy-finder-wp 7 |
        8 | Requires at Least: 4.7 9 |
        10 | Tested Up To: 4.7 11 |
        12 | Tags: 13 |
        14 | Stable tag: 0.2 15 |
        16 | License: GPLv2 or later 17 |
        18 | Donate link: https://github.com/NateWr 19 | 20 | A fuzzy finder for your WordPress admin. ctrl-shift-f from any WordPress admin area to quickly search Posts, Pages, Categories, Tags and Users. 21 | 22 | ## Description 23 | 24 | A fuzzy finder for your WordPress admin. ctrl-shift-f from any WordPress admin area to quickly search for Posts, Pages, Categories, Tags and Users. 25 | 26 | Search results are cached in your browser's local storage. Previous results that match your current search will be shown immediately. 27 | 28 | It will also search custom post types that have been registered with the `show_ui` and `show_in_rest` arguments set to `true`. 29 | 30 | Inspired by the [Atom package](https://github.com/atom/fuzzy-finder). Leans heavily on [client-js](https://github.com/WP-API/client-js) for handling REST API content endpoints. 31 | 32 | ## Changelog 33 | 34 | ### 0.2 (2016-12-11) 35 | * Search Posts, Pages, Categories, Tags and Users 36 | * Store past searches in browser's Local Storage 37 | 38 | ### 0.1 (2015-07-17) 39 | * Hello World! 40 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | 8 | // Load grunt project configuration 9 | pkg: grunt.file.readJSON('package.json'), 10 | 11 | // Configure less CSS compiler 12 | less: { 13 | build: { 14 | options: { 15 | compress: true, 16 | cleancss: true, 17 | ieCompat: true 18 | }, 19 | files: { 20 | 'dist/assets/css/finder.css': [ 21 | 'src/assets/less/finder.less', 22 | 'src/assets/less/finder-*.less' 23 | ] 24 | } 25 | } 26 | }, 27 | 28 | // Configure JSHint 29 | jshint: { 30 | test: { 31 | src: 'src/assets/js/*.js' 32 | } 33 | }, 34 | 35 | // Concatenate scripts 36 | concat: { 37 | build: { 38 | files: { 39 | 'dist/assets/js/finder.js': [ 40 | 'src/assets/js/finder.js', 41 | 'src/assets/js/finder-*.js' 42 | ] 43 | } 44 | } 45 | }, 46 | 47 | // Minimize scripts 48 | uglify: { 49 | options: { 50 | banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' 51 | }, 52 | build: { 53 | files: { 54 | 'dist/assets/js/finder.js' : 'dist/assets/js/finder.js' 55 | } 56 | } 57 | }, 58 | 59 | // Copy files from /src to /dist that aren't compiled 60 | copy: { 61 | main: { 62 | files: [{ 63 | expand: true, 64 | dot: true, 65 | cwd: 'src', 66 | dest: 'dist', 67 | src: [ 68 | '**/*.{php,txt,md}', 69 | ] 70 | }] 71 | } 72 | }, 73 | 74 | // Watch for changes on some files and auto-compile them 75 | watch: { 76 | less: { 77 | files: ['src/assets/less/*.less'], 78 | tasks: ['less'] 79 | }, 80 | js: { 81 | files: ['src/assets/js/*.js'], 82 | tasks: ['jshint', 'concat', 'uglify'] 83 | }, 84 | copy: { 85 | files: ['src/**/*.{php,txt,md}'], 86 | tasks: ['copy'] 87 | }, 88 | }, 89 | 90 | // Build a package for distribution 91 | compress: { 92 | main: { 93 | options: { 94 | archive: 'fuzzy-finder-wp-<%= pkg.version %>.zip' 95 | }, 96 | files: [ 97 | { 98 | expand: true, 99 | cwd: 'dist/', 100 | src: [ 101 | '*', '**/*', 102 | ], 103 | dest: 'fuzzy-finder-wp/', 104 | } 105 | ] 106 | } 107 | } 108 | 109 | }); 110 | 111 | // Load tasks 112 | grunt.loadNpmTasks('grunt-contrib-compress'); 113 | grunt.loadNpmTasks('grunt-contrib-concat'); 114 | grunt.loadNpmTasks('grunt-contrib-copy'); 115 | grunt.loadNpmTasks('grunt-contrib-jshint'); 116 | grunt.loadNpmTasks('grunt-contrib-less'); 117 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 118 | grunt.loadNpmTasks('grunt-contrib-uglify'); 119 | grunt.loadNpmTasks('grunt-contrib-watch'); 120 | 121 | // Default task(s). 122 | grunt.registerTask('default', ['watch']); 123 | 124 | }; 125 | -------------------------------------------------------------------------------- /dist/assets/css/finder.css: -------------------------------------------------------------------------------- 1 | @keyframes cffrtbrotate{0%{transform:rotateZ(-360deg);-webkit-transform:rotateZ(-360deg);-moz-transform:rotateZ(-360deg);-o-transform:rotateZ(-360deg)}100%{transform:rotateZ(0deg);-webkit-transform:rotateZ(0deg);-moz-transform:rotateZ(0deg);-o-transform:rotateZ(0deg)}}@-webkit-keyframes cffrtbrotate{0%{transform:rotateZ(-360deg);-webkit-transform:rotateZ(-360deg);-moz-transform:rotateZ(-360deg);-o-transform:rotateZ(-360deg)}100%{transform:rotateZ(0deg);-webkit-transform:rotateZ(0deg);-moz-transform:rotateZ(0deg);-o-transform:rotateZ(0deg)}}@-moz-keyframes cffrtbrotate{0%{transform:rotateZ(-360deg);-webkit-transform:rotateZ(-360deg);-moz-transform:rotateZ(-360deg);-o-transform:rotateZ(-360deg)}100%{transform:rotateZ(0deg);-webkit-transform:rotateZ(0deg);-moz-transform:rotateZ(0deg);-o-transform:rotateZ(0deg)}}@-o-keyframes cffrtbrotate{0%{transform:rotateZ(-360deg);-webkit-transform:rotateZ(-360deg);-moz-transform:rotateZ(-360deg);-o-transform:rotateZ(-360deg)}100%{transform:rotateZ(0deg);-webkit-transform:rotateZ(0deg);-moz-transform:rotateZ(0deg);-o-transform:rotateZ(0deg)}}.ffwp-spinner:after{display:inline-block;position:relative;width:20px;height:20px;-webkit-animation:cffrtbrotate .6s linear infinite;-moz-animation:cffrtbrotate .6s linear infinite;-ms-animation:cffrtbrotate .6s linear infinite;-o-animation:cffrtbrotate .6s linear infinite;animation:cffrtbrotate .6s linear infinite;border-radius:100%;border-top:1px solid #888;border-bottom:1px solid #bbb;border-left:1px solid #888;border-right:1px solid #bbb;vertical-align:middle;content:'';opacity:.5}#ffwp-finder{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.8);z-index:161000;overflow-y:auto;cursor:pointer;visibility:hidden;opacity:0;-webkit-transition:opacity .3s,visibility .3s;-moz-transition:opacity .3s,visibility .3s;transition:opacity .3s,visibility .3s}#ffwp-finder.is-visible{visibility:visible;opacity:1}.ffwp-finder-container{position:relative;width:90%;max-width:30em;background:#ddd;margin:32px auto;padding-bottom:44px;cursor:auto;border-radius:.5em;transform:translateY(-32px);-webkit-transform:translateY(-32px);-moz-transform:translateY(-32px);-o-transform:translateY(-32px);-webkit-transition-property:-webkit-transform;-moz-transition-property:-moz-transform;transition-property:transform;-webkit-transition-duration:.3s;-moz-transition-duration:.3s;transition-duration:.3s}@media (min-height:768px){.ffwp-finder-container{margin:64px auto}}.is-visible .ffwp-finder-container{transform:translateY(0);-webkit-transform:translateY(0);-moz-transform:translateY(0);-o-transform:translateY(0)}.ffwp-control{padding:1em;background:#0073aa;border-top-left-radius:.5em;border-top-right-radius:.5em}.ffwp-control input{width:100%;padding:0 .5em;line-height:2em;box-shadow:none;border:0}.ffwp-results{margin:0;padding:0;list-style:none;overflow-y:scroll;background:#fff;max-height:200px}@media (min-height:480px){.ffwp-results{overflow-y:scroll;max-height:302px}}@media (min-height:614px){.ffwp-results{overflow-y:scroll;max-height:436px}}@media (min-height:768px){.ffwp-results{overflow-y:scroll;max-height:590px}}@media (min-height:1024px){.ffwp-results{overflow-y:scroll;max-height:846px}}.ffwp-results li{padding:0;margin:0}.ffwp-results a{display:block;width:100%;padding:.7em 1em;line-height:1.5em;border-bottom:1px solid #ddd;box-sizing:border-box;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;text-decoration:none}.ffwp-results a:hover,.ffwp-results a:focus{background:#eee;outline:0;box-shadow:none}.ffwp-results li:last-child>a{border-bottom:0}.ffwp-results .cache a:before{content:"\f321";display:inline-block;width:20px;height:20px;font-size:20px;line-height:1;font-family:dashicons;text-decoration:inherit;font-weight:400;font-style:normal;vertical-align:top;text-align:center;-webkit-transition:color .1s ease-in 0;transition:color .1s ease-in 0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:rgba(0,0,0,.3)}.ffwp-status{position:absolute;bottom:0;left:0;right:0;padding:1em;min-height:2em;background:#ddd;border-bottom-left-radius:.5em;border-bottom-right-radius:.5em;font-size:11px;color:#777}.ffwp-status>span{display:none;line-height:2em}.ffwp-status .ffwp-progress{float:right}.ffwp-status.waiting .ffwp-waiting{display:inline-block}.ffwp-status.fetching .ffwp-fetching{display:inline-block}.ffwp-status.searching .ffwp-searching{display:inline-block}.ffwp-status.complete .ffwp-complete{display:inline-block}.ffwp-status.searching .ffwp-progress,.ffwp-status.complete .ffwp-progress{display:inline-block}.ffwp-status .dashicons{font-size:18px;width:20px;vertical-align:middle} -------------------------------------------------------------------------------- /src/assets/less/finder.less: -------------------------------------------------------------------------------- 1 | // 2 | // Styles for the fuzzy finder component 3 | // 4 | 5 | @import "variables"; 6 | @import "utils"; 7 | @import "components"; 8 | 9 | #ffwp-finder { 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | width: 100%; 14 | height: 100%; 15 | background: rgba(0, 0, 0, 0.8); 16 | z-index: 161000; 17 | overflow-y: auto; 18 | cursor: pointer; 19 | visibility: hidden; 20 | opacity: 0; 21 | -webkit-transition: opacity 0.3s, visibility 0.3s; 22 | -moz-transition: opacity 0.3s, visibility 0.3s; 23 | transition: opacity 0.3s, visibility 0.3s; 24 | 25 | &.is-visible { 26 | visibility: visible; 27 | opacity: 1; 28 | } 29 | } 30 | 31 | // Modal 32 | .ffwp-finder-container { 33 | position: relative; 34 | width: 90%; 35 | max-width: 30em; 36 | background: @bg; 37 | margin: 32px auto; 38 | padding-bottom: 44px; // status bar 39 | cursor: auto; 40 | border-radius: @radius; // Don't stick out over header/footer corners 41 | .transform(translateY(-32px)); 42 | -webkit-transition-property: -webkit-transform; 43 | -moz-transition-property: -moz-transform; 44 | transition-property: transform; 45 | -webkit-transition-duration: 0.3s; 46 | -moz-transition-duration: 0.3s; 47 | transition-duration: 0.3s; 48 | 49 | @media( min-height: 768px ) { 50 | margin: 64px auto; 51 | } 52 | 53 | } 54 | 55 | // Modal when visible 56 | .is-visible .ffwp-finder-container { 57 | .transform(translateY(0)); 58 | } 59 | 60 | // Header 61 | .ffwp-control { 62 | padding: 1em; 63 | background: @primary; 64 | border-top-left-radius: @radius; 65 | border-top-right-radius: @radius; 66 | 67 | input { 68 | width: 100%; 69 | padding: 0 0.5em; 70 | line-height: 2em; 71 | 72 | // Overwrite some existing styles 73 | box-shadow: none; 74 | border: none; 75 | } 76 | } 77 | 78 | // Results list 79 | .ffwp-results { 80 | margin: 0; 81 | padding: 0; 82 | list-style: none; 83 | overflow-y: scroll; 84 | background: @lift; 85 | // @todo, small screens probably deserve a completely different approach, 86 | // maybe a full-screen display 87 | max-height: 200px; 88 | 89 | // Set a reasonable max height for the results panel based on screen height 90 | // break points. 91 | // unless they scroll. 92 | // results height = max height - 114px (header) - 64 (modal margins) 93 | @media( min-height: 480px ) { 94 | overflow-y: scroll; 95 | max-height: 480px - 178px; 96 | } 97 | 98 | @media( min-height: 614px ) { 99 | overflow-y: scroll; 100 | max-height: 614px - 178px; 101 | } 102 | 103 | @media( min-height: 768px ) { 104 | overflow-y: scroll; 105 | max-height: 768px - 178px; 106 | 107 | } 108 | 109 | @media( min-height: 1024px ) { 110 | overflow-y: scroll; 111 | max-height: 1024px - 178px; 112 | 113 | } 114 | 115 | li { 116 | padding: 0; 117 | margin: 0; 118 | } 119 | 120 | a { 121 | display: block; 122 | width: 100%; 123 | padding: 0.7em 1em; 124 | line-height: 1.5em; 125 | border-bottom: @border; 126 | box-sizing: border-box; 127 | overflow: hidden; 128 | white-space: nowrap; 129 | text-overflow: ellipsis; 130 | text-decoration: none; 131 | 132 | &:hover, 133 | &:focus { 134 | background: @light-shade; 135 | outline: 0; 136 | box-shadow: none; 137 | } 138 | } 139 | 140 | li:last-child > a { 141 | border-bottom: none; 142 | } 143 | 144 | .cache a:before { 145 | content: "\f321"; 146 | display: inline-block; 147 | width: 20px; 148 | height: 20px; 149 | font-size: 20px; 150 | line-height: 1; 151 | font-family: dashicons; 152 | text-decoration: inherit; 153 | font-weight: normal; 154 | font-style: normal; 155 | vertical-align: top; 156 | text-align: center; 157 | -webkit-transition: color .1s ease-in 0; 158 | transition: color .1s ease-in 0; 159 | -webkit-font-smoothing: antialiased; 160 | -moz-osx-font-smoothing: grayscale; 161 | color: rgba( 0, 0, 0, 0.3 ); 162 | } 163 | } 164 | 165 | // Status bar 166 | .ffwp-status { 167 | position: absolute; 168 | bottom: 0; 169 | left: 0; 170 | right: 0; 171 | padding: 1em; 172 | min-height: 2em; 173 | background: @bg; 174 | border-bottom-left-radius: @radius; 175 | border-bottom-right-radius: @radius; 176 | font-size: @font-sml; 177 | color: @text-light; 178 | 179 | > span { 180 | display: none; 181 | line-height: 2em; 182 | } 183 | 184 | .ffwp-progress { 185 | float: right; 186 | } 187 | 188 | // Waiting for input 189 | &.waiting { 190 | 191 | .ffwp-waiting { 192 | display: inline-block; 193 | } 194 | } 195 | 196 | // Fetching symbols 197 | &.fetching .ffwp-fetching { display: inline-block; } 198 | 199 | // Searching for matches 200 | &.searching .ffwp-searching { display: inline-block; } 201 | 202 | // Search complete 203 | &.complete .ffwp-complete { display: inline-block; } 204 | 205 | // Show search progress 206 | &.searching .ffwp-progress, 207 | &.complete .ffwp-progress { display: inline-block; } 208 | 209 | .dashicons { 210 | font-size: 18px; 211 | width: 20px; // match the spinner 212 | vertical-align: middle; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /dist/fuzzy-finder-wp.php: -------------------------------------------------------------------------------- 1 | init(); 88 | } 89 | 90 | return self::$instance; 91 | } 92 | 93 | /** 94 | * Initialize the plugin and register hooks 95 | */ 96 | public function init() { 97 | 98 | if ( !current_user_can( 'edit_posts' ) ) { 99 | return; 100 | } 101 | 102 | // Initialize the plugin 103 | add_action( 'admin_init', array( $this, 'load_textdomain' ) ); 104 | add_action( 'admin_init', array( $this, 'load_config' ) ); 105 | 106 | add_action( 'admin_enqueue_scripts', array( $this, 'register_assets' ) ); 107 | 108 | // Load search data and pass finder to the frontend 109 | add_action( 'admin_footer', array( $this, 'load_finder' ) ); 110 | 111 | } 112 | 113 | /** 114 | * Load the plugin textdomain for localistion 115 | * 116 | * @since 0.1 117 | */ 118 | public function load_textdomain() { 119 | load_plugin_textdomain( 'fuzzy-finder-wp', false, plugin_basename( dirname( __FILE__ ) ) . '/languages/' ); 120 | } 121 | 122 | /** 123 | * Load the plugin's configuration variables 124 | * 125 | * @since 0.1 126 | */ 127 | public function load_config() { 128 | $data = get_plugin_data( __FILE__, false, false ); 129 | self::$plugin_version = $data['Version']; 130 | } 131 | 132 | /** 133 | * Register the script and style assets 134 | * 135 | * @since 0.1 136 | */ 137 | public function register_assets() { 138 | wp_enqueue_style( 'ffwp_finder', self::$plugin_url . '/assets/css/finder.css', '', self::$plugin_version ); 139 | wp_enqueue_script( 'ffwp_finder', self::$plugin_url . '/assets/js/finder.js', array( 'jquery', 'wp-api' ), self::$plugin_version, true ); 140 | } 141 | 142 | /** 143 | * Load search data and pass finder to the frontend 144 | * 145 | * @since 0.1 146 | */ 147 | public function load_finder() { 148 | 149 | $this->get_menu_items(); 150 | 151 | // Uncomment this to test a large set of sample data 152 | // $this->get_sample_strings(); 153 | 154 | // Compile a template for a result 155 | ob_start(); 156 | include( self::$plugin_dir . '/templates/result.php' ); 157 | $result_template = ob_get_clean(); 158 | 159 | // Pass data 160 | wp_localize_script( 161 | 'ffwp_finder', 162 | 'ffwp_finder_settings', 163 | array( 164 | 'strings' => $this->strings, 165 | 'urls' => $this->urls, 166 | 'result_template' => $result_template, 167 | 'post_types' => $this->get_post_types(), 168 | 'taxonomies' => $this->get_taxonomies(), 169 | 'admin_url' => admin_url(), 170 | ) 171 | ); 172 | 173 | // Print modal template markup 174 | include( self::$plugin_dir . '/templates/finder.php' ); 175 | } 176 | 177 | /** 178 | * Retrieve all menu items for the finder 179 | * 180 | * @since 0.1 181 | */ 182 | public function get_menu_items() { 183 | global $menu; 184 | global $submenu; 185 | 186 | foreach( $menu as $item ) { 187 | 188 | // Skip separators 189 | if ( empty( $item[0] ) ) { 190 | continue; 191 | } 192 | 193 | $this->strings[] = $item[0]; 194 | $this->urls[] = $this->get_menu_item_url( $item[2] ); 195 | 196 | if ( !empty( $submenu[ $item[2] ] ) ) { 197 | $separator = apply_filters( 'ffwp_string_separator', ' > ' ); 198 | foreach( $submenu[ $item[2] ] as $subitem ) { 199 | $this->strings[] = $item[0] . $separator . $subitem[0]; 200 | $this->urls[] = $this->get_menu_item_url( $subitem[2] ); 201 | } 202 | } 203 | } 204 | } 205 | 206 | /** 207 | * Retrieve a URL for a menu item 208 | * 209 | * @since 0.1 210 | */ 211 | public function get_menu_item_url( $slug ) { 212 | return strpos( $slug, '.php' ) === false ? menu_page_url( $slug, false ) : get_admin_url( null, $slug ); 213 | } 214 | 215 | /** 216 | * Retrieve post type information 217 | * 218 | * @since 0.1 219 | */ 220 | public function get_post_types() { 221 | 222 | $return = array(); 223 | 224 | $post_types = get_post_types( array( 'show_ui' => true, 'show_in_rest' => true ), 'objects' ); 225 | foreach( $post_types as $post_type => $attributes ) { 226 | $return[] = array( 227 | 'post_type' => $post_type, 228 | 'label' => isset( $attributes->labels ) && !empty( $attributes->labels->singular_name ) ? $attributes->labels->singular_name : $attributes->label, 229 | 'edit_link' => $attributes->_edit_link, 230 | ); 231 | } 232 | 233 | $return = apply_filters( 'ffwp_post_types', $return ); 234 | 235 | return $return; 236 | } 237 | 238 | /** 239 | * Retrieve taxonomy information 240 | * 241 | * @since 0.1 242 | */ 243 | public function get_taxonomies() { 244 | 245 | $return = array(); 246 | 247 | $taxonomies = get_taxonomies( array( 'show_ui' => true, 'show_in_rest' => true ), 'objects' ); 248 | foreach( $taxonomies as $name => $attributes ) { 249 | $return[] = array( 250 | 'taxonomy_name' => $name, 251 | 'label' => isset( $attributes->labels ) && !empty( $attributes->labels->singular_name ) ? $attributes->labels->singular_name : $attributes->label, 252 | ); 253 | } 254 | 255 | $return = apply_filters( 'ffwp_post_types', $return ); 256 | 257 | return $return; 258 | } 259 | 260 | /** 261 | * Retrieve sample strings for load testing very large lists 262 | * 263 | * Around 100,000 will take a while to loop through 264 | * 265 | * @since 0.1 266 | */ 267 | public function get_sample_strings() { 268 | 269 | $c = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 270 | $clen = strlen( $c ); 271 | for( $i = 0; $i < 50000; $i++ ) { 272 | $this->strings[] = $this->generate_random_string( $c, $clen, 50 ); 273 | $this->urls[] = 'edit.php?post=12345'; 274 | } 275 | 276 | } 277 | 278 | /** 279 | * Generate a random string for load testing 280 | * 281 | * @since 0.1 282 | */ 283 | public function generate_random_string( $c, $clen, $length = 10 ) { 284 | $str = ''; 285 | for ($i = 0; $i < $length; $i++) { 286 | $str .= $c[ rand( 0, $clen - 1 ) ]; 287 | } 288 | 289 | return $str; 290 | } 291 | } 292 | } // endif; 293 | 294 | /** 295 | * This function returns one ffwpInit instance everywhere 296 | * and can be used like a global, without needing to declare the global. 297 | * 298 | * Example: $ffwp = ffwpInit(); 299 | */ 300 | if ( !function_exists( 'ffwpInit' ) ) { 301 | function ffwpInit() { 302 | return ffwpInit::instance(); 303 | } 304 | add_action( 'plugins_loaded', 'ffwpInit' ); 305 | } // endif; 306 | -------------------------------------------------------------------------------- /src/fuzzy-finder-wp.php: -------------------------------------------------------------------------------- 1 | init(); 88 | } 89 | 90 | return self::$instance; 91 | } 92 | 93 | /** 94 | * Initialize the plugin and register hooks 95 | */ 96 | public function init() { 97 | 98 | if ( !current_user_can( 'edit_posts' ) ) { 99 | return; 100 | } 101 | 102 | // Initialize the plugin 103 | add_action( 'admin_init', array( $this, 'load_textdomain' ) ); 104 | add_action( 'admin_init', array( $this, 'load_config' ) ); 105 | 106 | add_action( 'admin_enqueue_scripts', array( $this, 'register_assets' ) ); 107 | 108 | // Load search data and pass finder to the frontend 109 | add_action( 'admin_footer', array( $this, 'load_finder' ) ); 110 | 111 | } 112 | 113 | /** 114 | * Load the plugin textdomain for localistion 115 | * 116 | * @since 0.1 117 | */ 118 | public function load_textdomain() { 119 | load_plugin_textdomain( 'fuzzy-finder-wp', false, plugin_basename( dirname( __FILE__ ) ) . '/languages/' ); 120 | } 121 | 122 | /** 123 | * Load the plugin's configuration variables 124 | * 125 | * @since 0.1 126 | */ 127 | public function load_config() { 128 | $data = get_plugin_data( __FILE__, false, false ); 129 | self::$plugin_version = $data['Version']; 130 | } 131 | 132 | /** 133 | * Register the script and style assets 134 | * 135 | * @since 0.1 136 | */ 137 | public function register_assets() { 138 | wp_enqueue_style( 'ffwp_finder', self::$plugin_url . '/assets/css/finder.css', '', self::$plugin_version ); 139 | wp_enqueue_script( 'ffwp_finder', self::$plugin_url . '/assets/js/finder.js', array( 'jquery', 'wp-api' ), self::$plugin_version, true ); 140 | } 141 | 142 | /** 143 | * Load search data and pass finder to the frontend 144 | * 145 | * @since 0.1 146 | */ 147 | public function load_finder() { 148 | 149 | $this->get_menu_items(); 150 | 151 | // Uncomment this to test a large set of sample data 152 | // $this->get_sample_strings(); 153 | 154 | // Compile a template for a result 155 | ob_start(); 156 | include( self::$plugin_dir . '/templates/result.php' ); 157 | $result_template = ob_get_clean(); 158 | 159 | // Pass data 160 | wp_localize_script( 161 | 'ffwp_finder', 162 | 'ffwp_finder_settings', 163 | array( 164 | 'strings' => $this->strings, 165 | 'urls' => $this->urls, 166 | 'result_template' => $result_template, 167 | 'post_types' => $this->get_post_types(), 168 | 'taxonomies' => $this->get_taxonomies(), 169 | 'admin_url' => admin_url(), 170 | ) 171 | ); 172 | 173 | // Print modal template markup 174 | include( self::$plugin_dir . '/templates/finder.php' ); 175 | } 176 | 177 | /** 178 | * Retrieve all menu items for the finder 179 | * 180 | * @since 0.1 181 | */ 182 | public function get_menu_items() { 183 | global $menu; 184 | global $submenu; 185 | 186 | foreach( $menu as $item ) { 187 | 188 | // Skip separators 189 | if ( empty( $item[0] ) ) { 190 | continue; 191 | } 192 | 193 | $this->strings[] = $item[0]; 194 | $this->urls[] = $this->get_menu_item_url( $item[2] ); 195 | 196 | if ( !empty( $submenu[ $item[2] ] ) ) { 197 | $separator = apply_filters( 'ffwp_string_separator', ' > ' ); 198 | foreach( $submenu[ $item[2] ] as $subitem ) { 199 | $this->strings[] = $item[0] . $separator . $subitem[0]; 200 | $this->urls[] = $this->get_menu_item_url( $subitem[2] ); 201 | } 202 | } 203 | } 204 | } 205 | 206 | /** 207 | * Retrieve a URL for a menu item 208 | * 209 | * @since 0.1 210 | */ 211 | public function get_menu_item_url( $slug ) { 212 | return strpos( $slug, '.php' ) === false ? menu_page_url( $slug, false ) : get_admin_url( null, $slug ); 213 | } 214 | 215 | /** 216 | * Retrieve post type information 217 | * 218 | * @since 0.1 219 | */ 220 | public function get_post_types() { 221 | 222 | $return = array(); 223 | 224 | $post_types = get_post_types( array( 'show_ui' => true, 'show_in_rest' => true ), 'objects' ); 225 | foreach( $post_types as $post_type => $attributes ) { 226 | $return[] = array( 227 | 'post_type' => $post_type, 228 | 'label' => isset( $attributes->labels ) && !empty( $attributes->labels->singular_name ) ? $attributes->labels->singular_name : $attributes->label, 229 | 'edit_link' => $attributes->_edit_link, 230 | ); 231 | } 232 | 233 | $return = apply_filters( 'ffwp_post_types', $return ); 234 | 235 | return $return; 236 | } 237 | 238 | /** 239 | * Retrieve taxonomy information 240 | * 241 | * @since 0.1 242 | */ 243 | public function get_taxonomies() { 244 | 245 | $return = array(); 246 | 247 | $taxonomies = get_taxonomies( array( 'show_ui' => true, 'show_in_rest' => true ), 'objects' ); 248 | foreach( $taxonomies as $name => $attributes ) { 249 | $return[] = array( 250 | 'taxonomy_name' => $name, 251 | 'label' => isset( $attributes->labels ) && !empty( $attributes->labels->singular_name ) ? $attributes->labels->singular_name : $attributes->label, 252 | ); 253 | } 254 | 255 | $return = apply_filters( 'ffwp_post_types', $return ); 256 | 257 | return $return; 258 | } 259 | 260 | /** 261 | * Retrieve sample strings for load testing very large lists 262 | * 263 | * Around 100,000 will take a while to loop through 264 | * 265 | * @since 0.1 266 | */ 267 | public function get_sample_strings() { 268 | 269 | $c = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 270 | $clen = strlen( $c ); 271 | for( $i = 0; $i < 50000; $i++ ) { 272 | $this->strings[] = $this->generate_random_string( $c, $clen, 50 ); 273 | $this->urls[] = 'edit.php?post=12345'; 274 | } 275 | 276 | } 277 | 278 | /** 279 | * Generate a random string for load testing 280 | * 281 | * @since 0.1 282 | */ 283 | public function generate_random_string( $c, $clen, $length = 10 ) { 284 | $str = ''; 285 | for ($i = 0; $i < $length; $i++) { 286 | $str .= $c[ rand( 0, $clen - 1 ) ]; 287 | } 288 | 289 | return $str; 290 | } 291 | } 292 | } // endif; 293 | 294 | /** 295 | * This function returns one ffwpInit instance everywhere 296 | * and can be used like a global, without needing to declare the global. 297 | * 298 | * Example: $ffwp = ffwpInit(); 299 | */ 300 | if ( !function_exists( 'ffwpInit' ) ) { 301 | function ffwpInit() { 302 | return ffwpInit::instance(); 303 | } 304 | add_action( 'plugins_loaded', 'ffwpInit' ); 305 | } // endif; 306 | -------------------------------------------------------------------------------- /dist/assets/js/finder.js: -------------------------------------------------------------------------------- 1 | /*! fuzzy-finder-wp 2016-12-10 */ 2 | var ffwp_finder=ffwp_finder||{};jQuery(document).ready(function(a){ffwp_finder.init=function(){this.results=[],this.strings=[],this.urls=[],this.post_ids=[],this.term_ids=[],this.user_ids=[],this.comment_ids=[];var b=[],c=[];try{b=JSON.parse(localStorage.getItem("ffwp_finder_strings"))||[],c=JSON.parse(localStorage.getItem("ffwp_finder_urls"))||[],this.post_ids=JSON.parse(localStorage.getItem("ffwp_finder_post_ids"))||[],this.term_ids=JSON.parse(localStorage.getItem("ffwp_finder_term_ids"))||[],this.user_ids=JSON.parse(localStorage.getItem("ffwp_finder_user_ids"))||[],this.comment_ids=JSON.parse(localStorage.getItem("ffwp_finder_comment_ids"))||[],this.hasLocalStorage=!0}catch(d){this.hasLocalStorage=!1}this.strings=ffwp_finder_settings.strings.concat(b),this.urls=ffwp_finder_settings.urls.concat(c),this.status="waiting",this.searching=[],this.total_count=ffwp_finder.strings.length,this.search_throttle=0,this.cache=this.cache||{},this.cache.body=a("body"),this.cache.finder=a("#ffwp-finder"),this.cache.search=this.cache.finder.find("#ffwp-search"),this.cache.results=this.cache.finder.find(".ffwp-results"),this.cache.status=this.cache.finder.find(".ffwp-status"),this.cache.progress=this.cache.status.find(".ffwp-progress"),this.cache.body.on("keyup",this.handleShortcuts),this.cache.search.on("keyup",this.searchThrottle),this.cache.finder.on("click",this.handleFinderWrapperEvents)},ffwp_finder.open=function(){ffwp_finder.cache.body.addClass("ffwp-finder-is-visible"),ffwp_finder.cache.finder.addClass("is-visible"),setTimeout(function(){ffwp_finder.cache.search.focus()},300)},ffwp_finder.close=function(){ffwp_finder.cache.body.removeClass("ffwp-finder-is-visible"),ffwp_finder.cache.finder.removeClass("is-visible"),setTimeout(function(){ffwp_finder.cache.search.val(""),ffwp_finder.clearResults(),ffwp_finder.setStatusWaiting()},300)},ffwp_finder.handleShortcuts=function(a){return a.ctrlKey&&a.shiftKey&&70==a.which?void ffwp_finder.open():27==a.which&&ffwp_finder.cache.finder.hasClass("is-visible")?void ffwp_finder.close():void 0},ffwp_finder.handleFinderWrapperEvents=function(a){"click"==a.type&&"ffwp-finder"==a.target.id&&ffwp_finder.close()},ffwp_finder.searchThrottle=function(){clearTimeout(ffwp_finder.search_throttle),ffwp_finder.search_throttle=setTimeout(ffwp_finder.search,300)},ffwp_finder.search=function(){var b=ffwp_finder.cache.search.val();if(b!==ffwp_finder.current_term){if(ffwp_finder.current_term=b,b.length<3)return ffwp_finder.clearResults(),void ffwp_finder.setStatusWaiting();for(var c=ffwp_finder.results.length-1;c>=0;c--)new RegExp(b,"i").test(ffwp_finder.strings[ffwp_finder.results[c]])||ffwp_finder.removeResult(ffwp_finder.results[c]);if("undefined"!==wp.api){var d=function(a,c){b===ffwp_finder.current_term&&console.log("Error fetching results. This error should probably be more helpful.")};if(ffwp_finder_settings.post_types.length){var e=function(a,c,d){if(b===ffwp_finder.current_term&&(ffwp_finder.setStatusComplete(a.post_type),a.length)){var e=_.findWhere(ffwp_finder_settings.post_types,{post_type:a.post_type}),f=ffwp_finder_settings.admin_url+e.edit_link+"&action=edit";a.forEach(function(a){var b=ffwp_finder.post_ids.indexOf(a.get("id"));0>b&&(b=ffwp_finder.strings.length),ffwp_finder.strings[b]=e.label+" > "+a.get("title").rendered,ffwp_finder.urls[b]=f.replace("%d",a.get("id")),ffwp_finder.post_ids[b]=a.get("id"),ffwp_finder.addResult(b,"live",!0)}),ffwp_finder.updateProgress(),ffwp_finder.updateLocalStorage()}};for(var f in ffwp_finder_settings.post_types){var g,h,i=ffwp_finder_settings.post_types[f].post_type;if(ffwp_finder.setStatusSearching(i),"post"===i)h=wp.api.collections.Posts.extend({post_type:i});else if("page"===i)h=wp.api.collections.Pages.extend({post_type:i});else if("attachment"===i)h=wp.api.collections.Media.extend({post_type:i});else{var j=wp.api.endpoints.at(1);h=wp.api.collections.Posts.extend({url:j.get("apiRoot")+j.get("versionString")+i,post_type:i})}if(h){g=new h;var k={search:b,posts_per_page:100,context:"edit"},l={};"attachment"===i?_.extendOwn(l,k):_.extendOwn(l,k,{status:"any"}),g.fetch({data:l,error:d,success:e})}}}if(ffwp_finder_settings.taxonomies.length){var m=function(a,c,d){if(b===ffwp_finder.current_term&&(ffwp_finder.setStatusComplete(a.taxonomy),a.length)){var e=_.findWhere(ffwp_finder_settings.taxonomies,{taxonomy_name:a.at(0).get("taxonomy")}),f=ffwp_finder_settings.admin_url+"term.php?taxonomy="+e+"&tag_ID=";a.forEach(function(a){var b=ffwp_finder.term_ids.indexOf(a.get("id"));0>b&&(b=ffwp_finder.strings.length),ffwp_finder.strings[b]=e.label+" > "+a.get("name"),ffwp_finder.urls[b]=f+a.get("id"),ffwp_finder.term_ids[b]=a.get("id"),ffwp_finder.addResult(b,"live",!0)}),ffwp_finder.updateProgress(),ffwp_finder.updateLocalStorage()}};for(var n in ffwp_finder_settings.taxonomies){var o,p,q=ffwp_finder_settings.taxonomies[n].taxonomy_name;ffwp_finder.setStatusSearching(q),"category"===q?p=wp.api.collections.Categories.extend({taxonomy:q}):"post_tag"===q?p=wp.api.collections.Tags.extend({taxonomy:q}):o=null,p&&(o=new p,o.fetch({data:{search:b,posts_per_page:100,context:"edit"},error:d,success:m}))}}ffwp_finder.setStatusSearching("users");var r=new wp.api.collections.Users;r.fetch({data:{search:b,posts_per_page:100,context:"edit"},error:d,success:function(a,c,d){if(b===ffwp_finder.current_term&&(ffwp_finder.setStatusComplete("users"),a.length)){var e=ffwp_finder_settings.admin_url+"user-edit.php?user_id=";a.forEach(function(a){var b=ffwp_finder.user_ids.indexOf(a.get("id"));0>b&&(b=ffwp_finder.strings.length),ffwp_finder.strings[b]="User > "+a.get("name"),ffwp_finder.urls[b]=e+a.get("id"),ffwp_finder.user_ids[b]=a.get("id"),ffwp_finder.addResult(b,"live",!0)}),ffwp_finder.updateProgress(),ffwp_finder.updateLocalStorage()}}}),ffwp_finder.setStatusSearching("comments");var s=new wp.api.collections.Comments;s.fetch({data:{search:b,posts_per_page:100,context:"edit",status:"any",_embed:!0},error:d,success:function(c,d,e){if(b===ffwp_finder.current_term&&(ffwp_finder.setStatusComplete("comments"),c.length)){var f=ffwp_finder_settings.admin_url+"comment.php?action=editcomment&c=";c.forEach(function(b){var c=ffwp_finder.comment_ids.indexOf(b.get("id"));0>c&&(c=ffwp_finder.strings.length),ffwp_finder.strings[c]="Comment > "+b.get("author_name")+" > "+b.get("_embedded").up[0].title.rendered+" > "+a(b.get("content").rendered).text().substring(0,50),ffwp_finder.urls[c]=f+b.get("id"),ffwp_finder.comment_ids[c]=b.get("id"),ffwp_finder.addResult(c,"live",!0)}),ffwp_finder.updateProgress(),ffwp_finder.updateLocalStorage()}}})}ffwp_finder.setStatusSearching("cache");var t=0,u=ffwp_finder.strings.length,v=function(){for(t;u>t;t++)if(new RegExp(b,"i").test(ffwp_finder.strings[t])&&ffwp_finder.addResult(t,"cache"),t+1>=u)t++,ffwp_finder.updateProgress(),ffwp_finder.setStatusComplete("cache");else if(t%100===0){if(b!==ffwp_finder.current_term)break;setTimeout(v,0),t++;break}};v()}},ffwp_finder.addResult=function(a,b,c){if(-1!==ffwp_finder.results.indexOf(a)&&ffwp_finder.removeResult(a),!(ffwp_finder.results.length>100)){ffwp_finder.results.push(a);var d=ffwp_finder_settings.result_template.replace("{url}",ffwp_finder.urls[a]).replace("{string}",ffwp_finder.strings[a]).replace("{index}",a.toString()).replace("{class}",b);c?ffwp_finder.cache.results.prepend(d):ffwp_finder.cache.results.append(d)}},ffwp_finder.removeResult=function(a){var b=ffwp_finder.results.indexOf(a);b>-1&&ffwp_finder.results.splice(b,1),ffwp_finder.cache.results.find("#ffwp-result-"+a.toString()).remove(),ffwp_finder.updateProgress()},ffwp_finder.clearResults=function(){ffwp_finder.results=[],ffwp_finder.cache.results.empty()},ffwp_finder.setStatusFetching=function(){ffwp_finder.cache.status.removeClass("fetching complete waiting").addClass("fetching"),ffwp_finder.status="fetching",ffwp_finder.clearProgress()},ffwp_finder.setStatusSearching=function(a){ffwp_finder.cache.status.removeClass("fetching complete waiting").addClass("searching"),ffwp_finder.searching.push(a),ffwp_finder.status="searching"},ffwp_finder.setStatusComplete=function(a){var b=ffwp_finder.searching.indexOf(a);b>-1&&ffwp_finder.searching.splice(b,1),ffwp_finder.searching.length||(ffwp_finder.cache.status.removeClass("fetching searching waiting").addClass("complete"),ffwp_finder.status="complete")},ffwp_finder.setStatusWaiting=function(){ffwp_finder.cache.status.removeClass("fetching searching complete").addClass("waiting"),ffwp_finder.status="waiting",ffwp_finder.clearProgress()},ffwp_finder.updateProgress=function(){ffwp_finder.cache.progress.html(ffwp_finder.results.length+" matches")},ffwp_finder.clearProgress=function(){ffwp_finder.cache.progress.empty()},ffwp_finder.updateLocalStorage=function(a){if(ffwp_finder.hasLocalStorage){var b=ffwp_finder.strings.slice(0);b.splice(0,ffwp_finder_settings.strings.length);var c=ffwp_finder.urls.slice(0);c.splice(0,ffwp_finder_settings.urls.length),b.length>1e3&&(b.splice(0,b.length-1e3),c.splice(0,c.length-1e3)),localStorage.setItem("ffwp_finder_strings",JSON.stringify(b)),localStorage.setItem("ffwp_finder_urls",JSON.stringify(c)),localStorage.setItem("ffwp_finder_post_ids",JSON.stringify(ffwp_finder.post_ids)),localStorage.setItem("ffwp_finder_term_ids",JSON.stringify(ffwp_finder.term_ids)),localStorage.setItem("ffwp_finder_user_ids",JSON.stringify(ffwp_finder.user_ids)),localStorage.setItem("ffwp_finder_comment_ids",JSON.stringify(ffwp_finder.comment_ids))}},ffwp_finder.init()}); -------------------------------------------------------------------------------- /src/assets/js/finder.js: -------------------------------------------------------------------------------- 1 | // Controller for the fuzzy finder component 2 | var ffwp_finder = ffwp_finder || {}; 3 | 4 | jQuery(document).ready(function ($) { 5 | 6 | /** 7 | * Initialize the component 8 | * 9 | * @since 0.1 10 | */ 11 | ffwp_finder.init = function() { 12 | 13 | // Result index keys currently being shown 14 | this.results = []; 15 | 16 | // Search data 17 | this.strings = []; 18 | this.urls = []; 19 | this.post_ids = []; 20 | this.term_ids = []; 21 | this.user_ids = []; 22 | this.comment_ids = []; 23 | 24 | // Check for local storage support in browser 25 | var stored_strings = []; 26 | var stored_urls = []; 27 | try { 28 | stored_strings = JSON.parse( localStorage.getItem( 'ffwp_finder_strings' ) ) || []; 29 | stored_urls = JSON.parse( localStorage.getItem( 'ffwp_finder_urls' ) ) || []; 30 | this.post_ids = JSON.parse( localStorage.getItem( 'ffwp_finder_post_ids' ) ) || []; 31 | this.term_ids = JSON.parse( localStorage.getItem( 'ffwp_finder_term_ids' ) ) || []; 32 | this.user_ids = JSON.parse( localStorage.getItem( 'ffwp_finder_user_ids' ) ) || []; 33 | this.comment_ids = JSON.parse( localStorage.getItem( 'ffwp_finder_comment_ids' ) ) || []; 34 | this.hasLocalStorage = true; 35 | } catch( e ) { 36 | this.hasLocalStorage = false; 37 | } 38 | 39 | this.strings = ffwp_finder_settings.strings.concat( stored_strings ); 40 | this.urls = ffwp_finder_settings.urls.concat( stored_urls ); 41 | 42 | // Current status 43 | this.status = 'waiting'; 44 | this.searching = []; 45 | this.total_count = ffwp_finder.strings.length; 46 | 47 | // A timer used to throttle the search 48 | this.search_throttle = 0; 49 | 50 | // jQuery object cache 51 | this.cache = this.cache || {}; 52 | this.cache.body = $( 'body' ); 53 | this.cache.finder = $( '#ffwp-finder' ); 54 | this.cache.search = this.cache.finder.find( '#ffwp-search' ); 55 | this.cache.results = this.cache.finder.find( '.ffwp-results' ); 56 | this.cache.status = this.cache.finder.find( '.ffwp-status' ); 57 | this.cache.progress = this.cache.status.find( '.ffwp-progress' ); 58 | 59 | this.cache.body.on( 'keyup', this.handleShortcuts ); 60 | this.cache.search.on( 'keyup', this.searchThrottle ); 61 | this.cache.finder.on( 'click', this.handleFinderWrapperEvents ); 62 | }; 63 | 64 | /** 65 | * Open the finder 66 | * 67 | * @since 0.1 68 | */ 69 | ffwp_finder.open = function() { 70 | ffwp_finder.cache.body.addClass( 'ffwp-finder-is-visible' ); 71 | ffwp_finder.cache.finder.addClass( 'is-visible' ); 72 | 73 | setTimeout( function() { 74 | ffwp_finder.cache.search.focus(); 75 | }, 300 ); 76 | }; 77 | 78 | /** 79 | * Close the finder 80 | * 81 | * @since 0.1 82 | */ 83 | ffwp_finder.close = function() { 84 | ffwp_finder.cache.body.removeClass( 'ffwp-finder-is-visible' ); 85 | ffwp_finder.cache.finder.removeClass( 'is-visible' ); 86 | 87 | setTimeout( function() { 88 | ffwp_finder.cache.search.val( '' ); 89 | ffwp_finder.clearResults(); 90 | ffwp_finder.setStatusWaiting(); 91 | }, 300 ); 92 | }; 93 | 94 | /** 95 | * Process shortcut commands 96 | * 97 | * @since 0.1 98 | */ 99 | ffwp_finder.handleShortcuts = function( e ) { 100 | 101 | // ctrl+shift+f - Open 102 | if ( e.ctrlKey && e.shiftKey && e.which == 70 ) { 103 | ffwp_finder.open(); 104 | return; 105 | 106 | // esc - Close 107 | } else if( e.which == 27 && ffwp_finder.cache.finder.hasClass( 'is-visible' ) ) { 108 | ffwp_finder.close(); 109 | return; 110 | } 111 | }; 112 | 113 | /** 114 | * Process click events on the modal wrapper 115 | * 116 | * @since 0.1 117 | */ 118 | ffwp_finder.handleFinderWrapperEvents = function( e ) { 119 | if ( e.type == 'click' && e.target.id == 'ffwp-finder' ) { 120 | ffwp_finder.close(); 121 | } 122 | }; 123 | 124 | /** 125 | * Wrap the search function with a small utility to throttle requests. 126 | * 127 | * This prevents the search from firing unless its been 300ms since the last 128 | * request. 129 | * 130 | * @since 0.1 131 | */ 132 | ffwp_finder.searchThrottle = function() { 133 | clearTimeout( ffwp_finder.search_throttle ); 134 | ffwp_finder.search_throttle = setTimeout( ffwp_finder.search, 300 ); 135 | }; 136 | 137 | /** 138 | * Search for results 139 | * 140 | * @since 0.1 141 | */ 142 | ffwp_finder.search = function() { 143 | 144 | var term = ffwp_finder.cache.search.val(); 145 | 146 | if ( term === ffwp_finder.current_term ) { 147 | return; 148 | } 149 | 150 | ffwp_finder.current_term = term; 151 | 152 | if ( term.length < 3 ) { 153 | ffwp_finder.clearResults(); 154 | ffwp_finder.setStatusWaiting(); 155 | return; 156 | } 157 | 158 | // Remove existing results that no longer match 159 | // Loop in reverse order so we don't tamper with array keys 160 | for( var r = ffwp_finder.results.length - 1; r >= 0; r-- ) { 161 | if ( !( new RegExp( term, 'i' ) ).test( ffwp_finder.strings[ ffwp_finder.results[r] ] ) ) { 162 | ffwp_finder.removeResult( ffwp_finder.results[r] ); 163 | } 164 | } 165 | 166 | // Search the database 167 | if ( wp.api !== 'undefined' ) { 168 | 169 | var apiError = function( collection, xhr ) { 170 | if ( term !== ffwp_finder.current_term ) { 171 | return; 172 | } 173 | console.log( 'Error fetching results. This error should probably be more helpful.' ); 174 | }; 175 | 176 | if ( ffwp_finder_settings.post_types.length ) { 177 | 178 | var apiSuccessPosts = function( collection, models, xhr ) { 179 | 180 | if ( term !== ffwp_finder.current_term ) { 181 | return; 182 | } 183 | 184 | ffwp_finder.setStatusComplete( collection.post_type ); 185 | 186 | if ( !collection.length ) { 187 | return; 188 | } 189 | 190 | var post_type = _.findWhere( ffwp_finder_settings.post_types, { post_type: collection.post_type } ); 191 | var url = ffwp_finder_settings.admin_url + post_type.edit_link + '&action=edit'; 192 | collection.forEach( function( post ) { 193 | 194 | // Don't add an item twice 195 | var key = ffwp_finder.post_ids.indexOf( post.get( 'id' ) ); 196 | if ( key < 0 ) { 197 | key = ffwp_finder.strings.length; 198 | } 199 | 200 | ffwp_finder.strings[key] = post_type.label + ' > ' + post.get('title').rendered; 201 | ffwp_finder.urls[key] = url.replace( '%d', post.get( 'id' ) ); 202 | ffwp_finder.post_ids[key] = post.get( 'id' ); 203 | ffwp_finder.addResult( key, 'live', true ); 204 | } ); 205 | ffwp_finder.updateProgress(); 206 | ffwp_finder.updateLocalStorage(); 207 | }; 208 | 209 | for ( var p in ffwp_finder_settings.post_types ) { 210 | var post_type = ffwp_finder_settings.post_types[p].post_type; 211 | var posts; 212 | var post_collection; 213 | ffwp_finder.setStatusSearching( post_type ); 214 | if ( post_type === 'post' ) { 215 | post_collection = wp.api.collections.Posts.extend( { 216 | post_type: post_type, 217 | } ); 218 | } else if ( post_type === 'page' ) { 219 | post_collection = wp.api.collections.Pages.extend( { 220 | post_type: post_type, 221 | } ); 222 | } else if ( post_type === 'attachment' ) { 223 | post_collection = wp.api.collections.Media.extend( { 224 | post_type: post_type, 225 | } ); 226 | } else { 227 | var routeModel = wp.api.endpoints.at(1); 228 | post_collection = wp.api.collections.Posts.extend({ 229 | url: routeModel.get( 'apiRoot' ) + routeModel.get( 'versionString' ) + post_type, 230 | post_type: post_type, 231 | }); 232 | } 233 | 234 | if ( post_collection ) { 235 | 236 | posts = new post_collection(); 237 | 238 | var common_request_data = { 239 | search: term, 240 | posts_per_page: 100, 241 | context: 'edit', 242 | }; 243 | 244 | var data = {}; 245 | if ( post_type === 'attachment' ) { 246 | _.extendOwn( data, common_request_data ); 247 | } else { 248 | _.extendOwn( data, common_request_data, { status: 'any' } ); 249 | } 250 | 251 | posts.fetch({ 252 | data: data, 253 | error: apiError, 254 | success: apiSuccessPosts, 255 | }); 256 | } 257 | } 258 | } 259 | 260 | if ( ffwp_finder_settings.taxonomies.length ) { 261 | 262 | var apiSuccessTerms = function( collection, models, xhr ) { 263 | 264 | if ( term !== ffwp_finder.current_term ) { 265 | return; 266 | } 267 | 268 | ffwp_finder.setStatusComplete( collection.taxonomy ); 269 | 270 | if ( !collection.length ) { 271 | return; 272 | } 273 | 274 | var taxonomy = _.findWhere( ffwp_finder_settings.taxonomies, { taxonomy_name: collection.at(0).get( 'taxonomy' ) } ); 275 | var url = ffwp_finder_settings.admin_url + 'term.php?taxonomy=' + taxonomy + '&tag_ID='; 276 | collection.forEach( function( term ) { 277 | 278 | // Don't add an item twice 279 | var key = ffwp_finder.term_ids.indexOf( term.get( 'id' ) ); 280 | if ( key < 0 ) { 281 | key = ffwp_finder.strings.length; 282 | } 283 | 284 | ffwp_finder.strings[key] = taxonomy.label + ' > ' + term.get('name'); 285 | ffwp_finder.urls[key] = url + term.get( 'id' ); 286 | ffwp_finder.term_ids[key] = term.get( 'id' ); 287 | ffwp_finder.addResult( key, 'live', true ); 288 | } ); 289 | ffwp_finder.updateProgress(); 290 | ffwp_finder.updateLocalStorage(); 291 | }; 292 | 293 | for ( var t in ffwp_finder_settings.taxonomies ) { 294 | 295 | var taxonomy_name = ffwp_finder_settings.taxonomies[t].taxonomy_name; 296 | var terms; 297 | var taxonomy_collection; 298 | ffwp_finder.setStatusSearching( taxonomy_name ); 299 | if ( taxonomy_name === 'category' ) { 300 | taxonomy_collection = wp.api.collections.Categories.extend( { 301 | taxonomy: taxonomy_name, 302 | } ); 303 | } else if ( taxonomy_name === 'post_tag' ) { 304 | taxonomy_collection = wp.api.collections.Tags.extend( { 305 | taxonomy: taxonomy_name, 306 | } ); 307 | } else { 308 | terms = null; 309 | } 310 | 311 | if ( taxonomy_collection ) { 312 | terms = new taxonomy_collection(); 313 | terms.fetch({ 314 | data: { 315 | search: term, 316 | posts_per_page: 100, 317 | context: 'edit', 318 | }, 319 | error: apiError, 320 | success: apiSuccessTerms, 321 | }); 322 | } 323 | } 324 | } 325 | 326 | ffwp_finder.setStatusSearching( 'users' ); 327 | var users = new wp.api.collections.Users(); 328 | users.fetch({ 329 | data: { 330 | search: term, 331 | posts_per_page: 100, 332 | context: 'edit', 333 | }, 334 | error: apiError, 335 | success: function( collection, models, xhr ) { 336 | 337 | if ( term !== ffwp_finder.current_term ) { 338 | return; 339 | } 340 | 341 | ffwp_finder.setStatusComplete( 'users' ); 342 | 343 | if ( !collection.length ) { 344 | return; 345 | } 346 | 347 | var url = ffwp_finder_settings.admin_url + 'user-edit.php?user_id='; 348 | collection.forEach( function( user ) { 349 | 350 | // Don't add an item twice 351 | var key = ffwp_finder.user_ids.indexOf( user.get( 'id' ) ); 352 | if ( key < 0 ) { 353 | key = ffwp_finder.strings.length; 354 | } 355 | 356 | ffwp_finder.strings[key] = 'User > ' + user.get('name'); 357 | ffwp_finder.urls[key] = url + user.get( 'id' ); 358 | ffwp_finder.user_ids[key] = user.get( 'id' ); 359 | ffwp_finder.addResult( key, 'live', true ); 360 | } ); 361 | ffwp_finder.updateProgress(); 362 | ffwp_finder.updateLocalStorage(); 363 | }, 364 | }); 365 | 366 | ffwp_finder.setStatusSearching( 'comments' ); 367 | var comments = new wp.api.collections.Comments(); 368 | comments.fetch({ 369 | data: { 370 | search: term, 371 | posts_per_page: 100, 372 | context: 'edit', 373 | status: 'any', 374 | _embed: true, 375 | }, 376 | error: apiError, 377 | success: function( collection, models, xhr ) { 378 | 379 | if ( term !== ffwp_finder.current_term ) { 380 | return; 381 | } 382 | 383 | ffwp_finder.setStatusComplete( 'comments' ); 384 | 385 | if ( !collection.length ) { 386 | return; 387 | } 388 | 389 | var url = ffwp_finder_settings.admin_url + 'comment.php?action=editcomment&c='; 390 | collection.forEach( function( comment ) { 391 | 392 | // Don't add an item twice 393 | var key = ffwp_finder.comment_ids.indexOf( comment.get( 'id' ) ); 394 | if ( key < 0 ) { 395 | key = ffwp_finder.strings.length; 396 | } 397 | 398 | ffwp_finder.strings[key] = 'Comment > ' + comment.get('author_name') + ' > ' + comment.get( '_embedded' ).up[0].title.rendered + ' > ' + $( comment.get( 'content' ).rendered ).text().substring( 0, 50 ); 399 | ffwp_finder.urls[key] = url + comment.get( 'id' ); 400 | ffwp_finder.comment_ids[key] = comment.get( 'id' ); 401 | ffwp_finder.addResult( key, 'live', true ); 402 | } ); 403 | ffwp_finder.updateProgress(); 404 | ffwp_finder.updateLocalStorage(); 405 | }, 406 | }); 407 | } 408 | 409 | // Search list for matches 410 | ffwp_finder.setStatusSearching( 'cache' ); 411 | var i = 0; 412 | var len = ffwp_finder.strings.length; 413 | var processBatch = function() { 414 | for( i; i < len; i++ ) { 415 | 416 | if ( ( new RegExp( term, 'i' ) ).test( ffwp_finder.strings[i] ) ) { 417 | ffwp_finder.addResult( i, 'cache' ); 418 | } 419 | 420 | // Emit an event when we've finished the search 421 | if ( i + 1 >= len ) { 422 | i++; 423 | ffwp_finder.updateProgress(); 424 | ffwp_finder.setStatusComplete( 'cache' ); 425 | 426 | // Take a breath before continuing with the next batch 427 | } else if ( i % 100 === 0 ) { 428 | 429 | // Stop looping if the term has changed 430 | if ( term !== ffwp_finder.current_term ) { 431 | break; 432 | } 433 | 434 | setTimeout( processBatch, 0 ); 435 | i++; 436 | break; 437 | } 438 | } 439 | }; 440 | processBatch(); 441 | }; 442 | 443 | /** 444 | * Add a result to the list 445 | * 446 | * @since 0.1 447 | */ 448 | ffwp_finder.addResult = function( i, resultClass, prepend ) { 449 | 450 | // Already in results. Remove the existing result so it can be updated 451 | // with the new result 452 | if ( ffwp_finder.results.indexOf( i ) !== -1 ) { 453 | ffwp_finder.removeResult(i); 454 | } 455 | 456 | // Don't show too many results in the list. It just makes browsers cry 457 | if ( ffwp_finder.results.length > 100 ) { 458 | // @todo attach a note saying: more results available, refine search query. 459 | return; 460 | } 461 | 462 | // Add to array of visible resuls 463 | ffwp_finder.results.push( i ); 464 | 465 | // Attach to dom 466 | var html = ffwp_finder_settings.result_template.replace( '{url}', ffwp_finder.urls[i] ) 467 | .replace( '{string}', ffwp_finder.strings[i] ) 468 | .replace( '{index}', i.toString() ) 469 | .replace( '{class}', resultClass ); 470 | if ( prepend ) { 471 | ffwp_finder.cache.results.prepend( html ); 472 | } else { 473 | ffwp_finder.cache.results.append( html ); 474 | } 475 | }; 476 | 477 | /** 478 | * Remove a result from teh list 479 | * 480 | * @since 0.1 481 | */ 482 | ffwp_finder.removeResult = function( i ) { 483 | 484 | var index = ffwp_finder.results.indexOf( i ); 485 | if ( index > -1 ) { 486 | ffwp_finder.results.splice( index, 1 ); 487 | } 488 | 489 | ffwp_finder.cache.results.find( '#ffwp-result-' + i.toString() ).remove(); 490 | 491 | ffwp_finder.updateProgress(); 492 | }; 493 | 494 | /** 495 | * Clear out all results from the list 496 | * 497 | * @since 0.1 498 | */ 499 | ffwp_finder.clearResults = function() { 500 | ffwp_finder.results = []; 501 | ffwp_finder.cache.results.empty(); 502 | }; 503 | 504 | /** 505 | * Set the finder's status to fetching 506 | * 507 | * @since 0.1 508 | */ 509 | ffwp_finder.setStatusFetching = function() { 510 | 511 | ffwp_finder.cache.status.removeClass( 'fetching complete waiting' ) 512 | .addClass( 'fetching' ); 513 | 514 | ffwp_finder.status = 'fetching'; 515 | ffwp_finder.clearProgress(); 516 | }; 517 | 518 | /** 519 | * Set the finder's status to searching 520 | * 521 | * @since 0.1 522 | */ 523 | ffwp_finder.setStatusSearching = function( key ) { 524 | 525 | ffwp_finder.cache.status.removeClass( 'fetching complete waiting' ) 526 | .addClass( 'searching' ); 527 | 528 | ffwp_finder.searching.push( key ); 529 | 530 | ffwp_finder.status = 'searching'; 531 | }; 532 | 533 | /** 534 | * Set the finder's status to complete 535 | * 536 | * @since 0.1 537 | */ 538 | ffwp_finder.setStatusComplete = function( key ) { 539 | 540 | var search_completed = ffwp_finder.searching.indexOf( key ); 541 | if ( search_completed > -1 ) { 542 | ffwp_finder.searching.splice( search_completed, 1 ); 543 | } 544 | 545 | if ( ffwp_finder.searching.length ) { 546 | return; 547 | } 548 | 549 | ffwp_finder.cache.status.removeClass( 'fetching searching waiting' ) 550 | .addClass( 'complete' ); 551 | ffwp_finder.status = 'complete'; 552 | }; 553 | 554 | /** 555 | * Set the finder's status to waiting 556 | * 557 | * This status indicates that no search will be processed for current terms 558 | * 559 | * @since 0.1 560 | */ 561 | ffwp_finder.setStatusWaiting = function() { 562 | 563 | ffwp_finder.cache.status.removeClass( 'fetching searching complete' ) 564 | .addClass( 'waiting' ); 565 | 566 | ffwp_finder.status = 'waiting'; 567 | ffwp_finder.clearProgress(); 568 | }; 569 | 570 | /** 571 | * Update the search progress tracker 572 | * 573 | * @since 0.1 574 | */ 575 | ffwp_finder.updateProgress = function() { 576 | ffwp_finder.cache.progress.html( ffwp_finder.results.length + ' matches' ); 577 | }; 578 | 579 | /** 580 | * Clear the search progress tracker 581 | * 582 | * @since 0.1 583 | */ 584 | ffwp_finder.clearProgress = function() { 585 | ffwp_finder.cache.progress.empty(); 586 | }; 587 | 588 | /** 589 | * Update arrays in local storage 590 | * 591 | * @since 0.1 592 | */ 593 | ffwp_finder.updateLocalStorage = function( keys ) { 594 | 595 | if ( !ffwp_finder.hasLocalStorage ) { 596 | return; 597 | } 598 | 599 | var strings = ffwp_finder.strings.slice(0); 600 | strings.splice( 0, ffwp_finder_settings.strings.length ); 601 | var urls = ffwp_finder.urls.slice(0); 602 | urls.splice( 0, ffwp_finder_settings.urls.length ); 603 | 604 | if ( strings.length > 1000 ) { 605 | strings.splice( 0, strings.length - 1000 ); 606 | urls.splice( 0, urls.length - 1000 ); 607 | } 608 | 609 | localStorage.setItem( 'ffwp_finder_strings', JSON.stringify( strings ) ); 610 | localStorage.setItem( 'ffwp_finder_urls', JSON.stringify( urls ) ); 611 | localStorage.setItem( 'ffwp_finder_post_ids', JSON.stringify( ffwp_finder.post_ids ) ); 612 | localStorage.setItem( 'ffwp_finder_term_ids', JSON.stringify( ffwp_finder.term_ids ) ); 613 | localStorage.setItem( 'ffwp_finder_user_ids', JSON.stringify( ffwp_finder.user_ids ) ); 614 | localStorage.setItem( 'ffwp_finder_comment_ids', JSON.stringify( ffwp_finder.comment_ids ) ); 615 | }; 616 | 617 | // Go! 618 | ffwp_finder.init(); 619 | 620 | }); 621 | --------------------------------------------------------------------------------