PluginProbe
Customify / trunk
Customify vtrunk
2.10.9 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.7.1 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.6.0 1.6.0.1 1.6.5 1.7.0 1.7.1 All 77 releases
customify / js / customizer / search.js

search.js in Customify trunk, at js/customizer/search.js

300 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * The logic for the Customizer controls search.
3 *
4 * Based on the logic from this WordPress plugin: https://wordpress.org/plugins/customizer-search/
5 */
6
7 /** @namespace customify */
8 window.customify = window.customify || parent.customify || {};
9
10 (function ($, customify, wp) {
11
12 /**
13 * Expose the API publicly on window.customify.search
14 *
15 * @namespace customify.search
16 */
17 if (typeof customify.search === 'undefined') {
18 customify.search = {}
19 }
20
21 _.extend(customify.search, function () {
22 const api = wp.customize
23
24 const searchWrapperSelector = '#accordion-section-customify-customizer-search'
25 const searchInputSelector = '#customify-customizer-search-input'
26
27 let customizePanelsParent = null
28
29 let fuse = null
30
31 const init = function () {
32
33 const searchableControls = _.map(api.settings.controls, function (control, controlId) {
34 if ( typeof controlId !== 'string') {
35 controlId = String(controlId)
36 }
37 // Determine if the control should be excluded from search results.
38 const excluded = _.find( customify.search.excludedControls, function (partial) {
39 return controlId.indexOf(partial) !== -1;
40 })
41 if (excluded !== undefined) {
42 return
43 }
44
45 const searchableControl = {
46 "label": (typeof control.label !== 'undefined' && !_.isEmpty(control.label)) ? control.label : '',
47 "description": (typeof control.description !== 'undefined' && !_.isEmpty(control.description)) ? control.description : '',
48 "panelName": '',
49 "sectionName": '',
50 "panel": null,
51 "section": control.section, // This is to know what section to expand when clicking on this result.
52 }
53
54 _.map(api.settings.sections, function (section, index) {
55 if (control.section === section.id) {
56 _.map(_wpCustomizeSettings.panels, function (panel, index) {
57 if ('' === section.panel) {
58 searchableControl.panelName = section.title
59 }
60
61 if (section.panel === panel.id) {
62 searchableControl.sectionName = section.title
63 searchableControl.panel = section.panel
64 searchableControl.panelName = panel.title
65 }
66 })
67 }
68 })
69
70 return searchableControl
71 }).filter(function(item){ // Make sure that we remove excluded controls entries.
72 return item !== undefined;
73 })
74
75 // Initialize the FuseJS search
76 const fuseOptions = {
77 includeScore: true,
78 includeMatches: true,
79 shouldSort: true,
80 minMatchCharLength: 2,
81 threshold: 0.3, // The fussy search threshold. Lower for closer matches (less fuzzy).
82 keys: [
83 {
84 name: 'label',
85 weight: 1
86 },
87 {
88 name: 'description',
89 weight: 0.8
90 },
91 {
92 name: 'panelName',
93 weight: 0.4
94 },
95 {
96 name: 'sectionName',
97 weight: 0.4
98 }
99 ]
100 }
101
102 // Create a new instance of Fuse
103 fuse = new Fuse(searchableControls, fuseOptions)
104
105 const $customizeInfo = $('#customize-info')
106
107 customizePanelsParent = $('#customize-theme-controls')
108 customizePanelsParent.after('<div id="customify-search-results"></div>')
109
110 $customizeInfo.on('keyup', searchInputSelector, function (event) {
111 event.preventDefault()
112
113 const searchString = $(searchInputSelector).val()
114
115 // At least 3 characters required for search.
116 if (searchString.length > 2) {
117 displayResults(searchString)
118 } else if (searchString.length === 0) {
119 clearSearch()
120 }
121 })
122
123 $customizeInfo.on('click', '.clear-search', function (event) {
124 clearSearch()
125 })
126
127 $customizeInfo.on('click', '.close-search', function (event) {
128 toggleDisplaySearchForm()
129 })
130
131 $customizeInfo.on('click', '.customize-search-toggle', function (event) {
132 toggleDisplaySearchForm()
133 })
134
135 api.previewer.targetWindow.bind(showSearchButtonToggle)
136
137 // Handle showing the current search results when returning to the top pane.
138 api.state( 'expandedSection' ).bind(showSearchResultsWhenTopPaneVisible)
139 api.state( 'expandedPanel' ).bind(showSearchResultsWhenTopPaneVisible)
140 }
141
142 const showSearchResultsWhenTopPaneVisible = function() {
143 if (!api.state( 'expandedSection' ).get() && !api.state( 'expandedPanel' ).get()) {
144 const searchString = $(searchInputSelector).val()
145 if (searchString.length > 2) {
146 setTimeout( function() {
147 displayResults(searchString)
148 }, 400)
149 }
150 }
151 }
152
153 const displayResults = function (stringToSearch) {
154 const resultsArray = fuse.search(stringToSearch)
155
156 // Bail if no results.
157 if (0 === resultsArray.length) {
158 customizePanelsParent.removeClass('search-found')
159 return
160 }
161
162 const html = resultsArray.map(function (result, index) {
163
164 // Bail if no matches or empty label.
165 if (_.isEmpty(result.matches) || '' === result.item.label) {
166 return
167 }
168
169 // Make a copy for highlight.
170 const highlightedResult =$.extend(true, {}, result);
171
172 // Highlight everything there is to highlight.
173 _.each(result.matches, function(match) {
174 if (typeof match.indices === 'undefined' || _.isEmpty(match.indices)) {
175 // No highlighting to do.
176 return
177 }
178
179 // The key is like `label` or `sectionName`.
180 highlightedResult.item[match.key] = generateHighlightedText(match.value, match.indices)
181 })
182
183 // Construct the control trail with panel > section.
184 let controlTrail = highlightedResult.item.panelName
185 if ('' !== highlightedResult.item.sectionName) {
186 controlTrail = `${controlTrail} â–¸ ${highlightedResult.item.sectionName}`
187 }
188
189 return `
190 <li id="accordion-section-${result.item.section}" class="accordion-section control-section control-section-default customizer-search-results" aria-owns="sub-accordion-section-${result.item.section}" data-section="${result.item.section}">
191 <h3 class="accordion-section-title" tabindex="0">
192 ${highlightedResult.item.label}
193 <span class="screen-reader-text">${customify.l10n.search.resultsSectionScreenReaderText}</span>
194 </h3>
195 <span class="search-setting-path">${controlTrail}</i></span>
196 </li>
197 `
198 }).join('')
199
200 customizePanelsParent.addClass('search-found')
201 document.getElementById('customify-search-results').innerHTML = `<ul>${html}</ul>`
202
203 const searchSettings = document.querySelectorAll('#customify-search-results .accordion-section')
204 searchSettings.forEach(setting => setting.addEventListener('click', expandSection))
205 }
206
207 // Does not account for overlapping highlighted regions, if that exists at all O_o..
208 const generateHighlightedText = function (text, regions) {
209 if(!regions) {
210 return text;
211 }
212
213 const highlightedText = []
214 let pair = regions.shift()
215 // Build the formatted string
216 for (let i = 0; i < text.length; i++) {
217 const char = text.charAt(i)
218 if (pair && i == pair[0]) {
219 highlightedText.push('<span class="hl">')
220 }
221 highlightedText.push(char)
222 if (pair && i == pair[1]) {
223 highlightedText.push('</span>')
224 pair = regions.shift()
225 }
226 }
227
228 return highlightedText.join('')
229 }
230
231 /**
232 * Shows the message that is shown for when a header
233 * or footer is already set for this page.
234 */
235 const showSearchButtonToggle = function () {
236 let template = wp.template('customify-search-button')
237 if ($('#customize-info .accordion-section-title .customize-search-toggle').length === 0) {
238 $('#customize-info .accordion-section-title').append(template())
239 }
240
241 template = wp.template('customify-search-form')
242 if ($('#customize-info '+searchWrapperSelector).length === 0) {
243 $('#customize-info .customize-panel-description').after(template())
244 }
245 }
246
247 const toggleDisplaySearchForm = function () {
248 const $wrapper = $(searchWrapperSelector)
249
250 if ($wrapper.hasClass('open')) {
251 // Close it
252 $wrapper.removeClass('open')
253 $wrapper.slideUp('fast')
254
255 // Also clear the search.
256 clearSearch()
257 } else {
258 // Open it
259 $('.customize-panel-description').removeClass('open')
260 $('.customize-panel-description').slideUp('fast')
261
262 $wrapper.addClass('open')
263 $wrapper.slideDown('fast')
264
265 $(searchInputSelector).focus()
266 }
267 }
268
269 const expandSection = function (event) {
270 const sectionName = this.getAttribute('data-section')
271 const section = api.section(sectionName)
272
273 customizePanelsParent.removeClass('search-found')
274 document.getElementById('customify-search-results').innerHTML = ''
275 $(searchInputSelector).focus()
276
277 section.expand()
278 }
279
280 /**
281 * Clear Search input and display all the options.
282 */
283 const clearSearch = function () {
284 customizePanelsParent.removeClass('search-found')
285 document.getElementById('customify-search-results').innerHTML = ''
286 document.getElementById('customify-customizer-search-input').value = ''
287
288 $(searchInputSelector).focus()
289 }
290
291 // When the customizer is ready prepare the search logic.
292 api.bind('ready', init)
293
294 return {
295 init: init,
296 }
297 }())
298
299 })(jQuery, customify, wp)
300