├── .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 | [](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 |
--------------------------------------------------------------------------------