/[drupal]/contributions/modules/faceted_search/faceted_search_ui.module
ViewVC logotype

Contents of /contributions/modules/faceted_search/faceted_search_ui.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.87 - (show annotations) (download) (as text)
Thu Apr 16 19:50:49 2009 UTC (7 months, 1 week ago) by davidlesieur
Branch: MAIN
CVS Tags: HEAD
Changes since 1.86: +2 -2 lines
File MIME type: text/x-php
#410378 by gustav: Fixed error if search environment cannot be loaded.
1 <?php
2 // $Id: faceted_search_ui.module,v 1.86 2009/03/11 21:31:19 davidlesieur Exp $
3
4 /**
5 * @file
6 * A user interface for searching and browsing through multiple facets.
7 */
8
9 require_once('./'. drupal_get_path('module', 'faceted_search') .'/faceted_search.inc');
10 require_once('./'. drupal_get_path('module', 'faceted_search') .'/faceted_search_ui.inc');
11
12 /**
13 * Implementation of hook_perm().
14 */
15 function faceted_search_ui_perm() {
16 return array('use faceted search');
17 }
18
19 /**
20 * Implementation of hook_forms().
21 */
22 function faceted_search_ui_forms() {
23 $forms = array();
24 foreach (faceted_search_get_env_ids() as $env_id) {
25 $forms['faceted_search_ui_form_'. $env_id]['callback'] = 'faceted_search_ui_form';
26 }
27 return $forms;
28 }
29
30 /**
31 * Implementation of hook_menu().
32 */
33 function faceted_search_ui_menu() {
34 // TODO: hook_menu() is invoked when uninstalling a module, and in this case
35 // faceted_search_get_env_ids() tries to access the {faceted_search_env}
36 // table, which no longer exists since faceted_search.module gets uninstalled
37 // first. This causes an error message. The real issue is the module
38 // dependency tree being ignored when uninstalling
39 // (http://drupal.org/node/151452).
40
41 $items = array();
42 foreach (faceted_search_get_env_ids() as $env_id) {
43 $env = faceted_search_env_load($env_id);
44 $base_path = $env->settings['base_path'];
45 $items[$base_path] = array(
46 'title callback' => 'faceted_search_ui_menu_title',
47 'title arguments' => array((string)$env_id),
48 'page callback' => 'faceted_search_ui_stage_select',
49 'page arguments' => array((string)$env_id),
50 'access arguments' => array('use faceted search'),
51 'type' => MENU_CALLBACK,
52 );
53 $items[$base_path .'/results'] = array(
54 'page callback' => 'faceted_search_ui_stage_results',
55 'page arguments' => array((string)$env_id),
56 'access arguments' => array('use faceted search'),
57 'type' => MENU_CALLBACK,
58 );
59 $items[$base_path .'/facet'] = array(
60 'page callback' => 'faceted_search_ui_stage_facet',
61 'page arguments' => array((string)$env_id),
62 'access arguments' => array('use faceted search'),
63 'type' => MENU_CALLBACK,
64 );
65 $items[$base_path .'/categories'] = array(
66 'page callback' => 'faceted_search_ui_stage_categories',
67 'page arguments' => array((string)$env_id),
68 'access arguments' => array('use faceted search'),
69 'type' => MENU_CALLBACK,
70 );
71 }
72
73 // TODO: Per-environment access control setting (a la Views). Then, will no
74 // longer need the 'use faceted search' permission.
75
76 return $items;
77 }
78
79 /**
80 * Implementation of hook_block().
81 */
82 function faceted_search_ui_block($op = 'list', $delta = 0, $edit = array()) {
83 if ($op == 'list') {
84 $blocks = array();
85 foreach (faceted_search_get_env_ids() as $env_id) {
86 $env = faceted_search_env_load($env_id);
87 if ($env->settings['current_block']) {
88 $blocks[$env_id .'_current'] = array(
89 'info' => t('@env - Current search', array('@env' => $env->name)),
90 'cache' => BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_USER,
91 );
92 }
93 if ($env->settings['keyword_block']) {
94 $blocks[$env_id .'_keyword'] = array(
95 'info' => t('@env - Keyword search', array('@env' => $env->name)),
96 'cache' => BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE,
97 );
98 }
99 if ($env->settings['guided_block']) {
100 $blocks[$env_id .'_guided'] = array(
101 'info' => t('@env - Guided search', array('@env' => $env->name)),
102 'cache' => BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_USER,
103 );
104 }
105 if ($env->settings['related_block']) {
106 $blocks[$env_id .'_related'] = array(
107 'info' => t('@env - Related categories', array('@env' => $env->name)),
108 'cache' => BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_USER,
109 );
110 }
111 if ($env->settings['sort_block']) {
112 $blocks[$env_id .'_sort'] = array(
113 'info' => t('@env - Sort options', array('@env' => $env->name)),
114 'cache' => BLOCK_CACHE_PER_PAGE,
115 );
116 }
117 }
118 return $blocks;
119 }
120 elseif ($op == 'view' && user_access('use faceted search')) {
121 // Determine the environment id and requested block.
122 list($env_id, $delta) = explode('_', $delta, 2);
123
124 $env = faceted_search_env_load($env_id);
125 if (!$env || $env->ui_state['stage'] == 'select' || ($env->ui_state['stage'] == 'facet' && $delta != 'sort')) {
126 // We don't show blocks in this context.
127 return;
128 }
129
130 // Perform the search if has not been done previously. This should happen
131 // only when not on an actual search page.
132 if (!$env->ready()) {
133 $env->prepare();
134 $env->execute();
135 }
136
137 faceted_search_ui_add_css();
138
139 switch ($delta) {
140 case 'current':
141 $block['subject'] = t('Current search');
142 $block['content'] = faceted_search_ui_current_block($env);
143 break;
144
145 case 'keyword':
146 if ($env->ui_state['stage'] == 'results') {
147 $block['subject'] = t('Keyword search');
148 $block['content'] = faceted_search_ui_keyword_block($env);
149 }
150 break;
151
152 case 'guided':
153 if ($env->ui_state['stage'] == 'results') {
154 $block['subject'] = t('Guided search');
155 $block['content'] = faceted_search_ui_guided_block($env);
156 }
157 break;
158
159 case 'related':
160 if (arg(0) == 'node' && is_numeric(arg(1)) && !arg(2) && $node = node_load(arg(1))) {
161 $block['subject'] = t('Related categories');
162 $block['content'] = faceted_search_ui_related_block($env, $node);
163 }
164 break;
165
166 case 'sort':
167 $block['content'] = faceted_search_ui_sort_block($env);
168 break;
169 }
170 return $block;
171 }
172 }
173
174 /**
175 * Implementation of hook_form_alter().
176 */
177 function faceted_search_ui_form_faceted_search_edit_form_alter(&$form, &$form_state) {
178 $env = $form['env']['#value'];
179
180 // Basic information section.
181 $form['info']['base_path'] = array( // TODO: Proper validation of the path.
182 '#type' => 'textfield',
183 '#title' => t('Base path'),
184 '#default_value' => $env->settings['base_path'],
185 '#required' => TRUE,
186 '#description' => t('The base path under which this faceted search environment will be accessed. All faceted search links will be derived from that base path. Do not begin or end the base path with a /. Be careful to assign each faceted search environment its own distinct path.'),
187 );
188 $form['info']['start_page'] = array( // TODO: Proper validation of the path.
189 '#type' => 'textfield',
190 '#title' => t('Start page'),
191 '#required' => TRUE,
192 '#default_value' => $env->settings['start_page'],
193 '#description' => t("Path to go to when the current search is cleared. Popular options are the base path as entered above (shows a full search page), the base path followed by <code>/results</code> (shows all content using the same display style as search results), and <code>&lt;front&gt;</code> (goes to your site's front page). Do not begin or end the start page's path with a /."),
194 );
195
196 // Results page section.
197 $form['results'] = array(
198 '#type' => 'fieldset',
199 '#title' => t('Results page'),
200 '#collapsible' => TRUE,
201 '#collapsed' => FALSE,
202 '#weight' => 1,
203 );
204 $styles = faceted_search_ui_style_list();
205 $options = array();
206 foreach ($styles as $key => $style) {
207 $options[$key] = $style->get_label();
208 }
209 $form['results']['results_style'] = array(
210 '#type' => 'select',
211 '#title' => t('Display style'),
212 '#options' => $options,
213 '#default_value' => $env->settings['results_style'],
214 '#description' => t('When the <em>Extracts</em> option is selected, search results show extracts relevant to the search keywords. With the <em>Teasers</em> option, search results use standard teasers. Other modules may provide additional display styles.'),
215 );
216 $form['results']['results_style_selective_extracts'] = array(
217 '#type' => 'checkbox',
218 '#title' => t('Use the <em>Extracts</em> display style selectively.'),
219 '#default_value' => $env->settings['results_style_selective_extracts'],
220 '#description' => t('If this is enabled, search results will be shown in the <em>Extracts</em> display style when the current search uses keywords, or use the above display style otherwise.'),
221 );
222
223 // Current search block section.
224 $form['current'] = array(
225 '#type' => 'fieldset',
226 '#title' => t('Current search'),
227 '#collapsible' => TRUE,
228 '#collapsed' => TRUE,
229 '#weight' => 2,
230 );
231 $form['current']['current_block'] = array(
232 '#type' => 'checkbox',
233 '#title' => t('Provide Current search block'),
234 '#default_value' => $env->settings['current_block'],
235 '#description' => t('When enabled, this block appears when search terms have been entered. This block can only appear on Faceted Search pages. Block visibility settings may define additional conditions for this block to appear.'),
236 );
237
238 // Keyword search section.
239 $form['keyword']['#weight'] = 3;
240 $form['keyword']['keyword_block'] = array(
241 '#type' => 'checkbox',
242 '#title' => t('Provide Keyword search block'),
243 '#default_value' => $env->settings['keyword_block'],
244 '#description' => t('When enabled, this block provides a form where a search text can be entered. Block visibility settings may define additional conditions for this block to appear.'),
245 '#weight' => -15
246 );
247 $form['keyword']['keyword_mode'] = array(
248 '#type' => 'radios',
249 '#title' => t('Default mode'),
250 '#description' => t("Choose the mode to select by default in keyword search. <em>New search</em> might be more intuitive to users not familiar with Faceted Search's interface since it mimics most search engines."),
251 '#default_value' => $env->settings['keyword_mode'],
252 '#options' => array('new' => t('New search'), 'refine' => t('Search within results')),
253 '#weight' => -10,
254 );
255 $form['keyword']['keyword_field_selector'] = array(
256 '#type' => 'checkbox',
257 '#title' => t('Provide field selector in the Keyword search block'),
258 '#default_value' => $env->settings['keyword_field_selector'],
259 '#description' => t('When enabled, the field selector will appear in the Keyword search block if there is at least one field enabled for keyword search. Note that the field selector is always shown on the <em>More options</em> page.'),
260 '#weight' => -5,
261 );
262
263 // Guided search block section.
264 $form['guided'] = array(
265 '#type' => 'fieldset',
266 '#title' => t('Guided search'),
267 '#collapsible' => TRUE,
268 '#collapsed' => TRUE,
269 '#weight' => 4,
270 );
271 $form['guided']['guided_block'] = array(
272 '#type' => 'checkbox',
273 '#title' => t('Provide Guided search block'),
274 '#default_value' => $env->settings['guided_block'],
275 '#description' => t('When enabled, this block shows categories that may be selected to start or refine a search. Block visibility settings may define additional conditions for this block to appear.'),
276 );
277 $form['guided']['sort_block'] = array(
278 '#type' => 'checkbox',
279 '#title' => t('Provide Sort options block'),
280 '#default_value' => $env->settings['sort_block'],
281 '#description' => t('When enabled, this block appears when navigating a full-page list of categories (after clicking the <em>More</em> link of a facet in the Guided search). Block visibility settings may define additional conditions for this block to appear.'),
282 );
283 $form['guided']['tooltips'] = array(
284 '#type' => 'checkbox',
285 '#title' => t('Show tooltips on categories'),
286 '#description' => t('Check this option to have tooltips displayed with subcategories when hovering over a category in the Guided search. This feature works only with clients that have JavaScript enabled, but is unobtrusive to clients that lack JavaScript.'),
287 '#default_value' => $env->settings['tooltips'],
288 );
289
290 // Related categories block section.
291 $form['related'] = array(
292 '#type' => 'fieldset',
293 '#title' => t('Related categories'),
294 '#collapsible' => TRUE,
295 '#collapsed' => TRUE,
296 '#weight' => 5,
297 );
298 $form['related']['related_block'] = array(
299 '#type' => 'checkbox',
300 '#title' => t('Provide Related categories block'),
301 '#default_value' => $env->settings['related_block'],
302 '#description' => t('When enabled, this block appears on a node\'s full page, showing categories related to that node. Categories may be selected to start a search. This block only appears for node types allowed in the faceted search environment (see the <em>Basic information</em> section). Block visibility settings may define additional conditions for this block to appear.'),
303 );
304 $form['related']['related_style'] = array(
305 '#type' => 'select',
306 '#title' => t('Display style'),
307 '#options' => array(
308 'list_ungrouped' => t('List - Ungrouped'),
309 'list_grouped' => t('List - Grouped by facet'),
310 'table' => t('Table - Grouped by facet'),
311 ),
312 '#default_value' => $env->settings['related_style'],
313 '#description' => t('Related categories may be grouped by facet (useful when a node uses more than one category per facet), or ungrouped (flat list of categories).'),
314 );
315 }
316
317 /**
318 * Implementation of hook_faceted_search_init().
319 */
320 function faceted_search_ui_faceted_search_init(&$env) {
321 $env->settings['base_path'] = '';
322 $env->settings['start_page'] = '';
323 $env->settings['results_style'] = 'faceted_search_ui:teasers';
324 $env->settings['results_style_selective_extracts'] = TRUE;
325 $env->settings['current_block'] = TRUE;
326 $env->settings['keyword_block'] = TRUE;
327 $env->settings['keyword_mode'] = 'new';
328 $env->settings['keyword_field_selector'] = 'keyword_field_selector';
329 $env->settings['guided_block'] = TRUE;
330 $env->settings['sort_block'] = TRUE;
331 $env->settings['tooltips'] = FALSE;
332 $env->settings['related_block'] = TRUE;
333 $env->settings['related_style'] = 'list_ungrouped';
334
335 // Default user interface state.
336 $env->ui_state = _faceted_search_ui_default_ui_state();
337 }
338
339 /**
340 * Implementation of hook_faceted_search_query_alter().
341 *
342 * Give the display style object an opportunity at altering the search
343 * query. The style might, for example, require additional filtering or extra
344 * fields.
345 */
346 function faceted_search_ui_faceted_search_query_alter($search, &$query) {
347 $style = faceted_search_ui_get_style($search);
348 if (method_exists($style, 'query_alter')) {
349 $style->query_alter($query, $search);
350 }
351 }
352
353 /**
354 * Menu callback to show the current search results.
355 *
356 * @param $env_id
357 * The id of the environment into which the search is taking place.
358 * @param $text
359 * The search text.
360 */
361 function faceted_search_ui_stage_results($env_id, $text = '') {
362 // If the menu system has splitted the search text because of slashes, glue it back.
363 if (func_num_args() > 2) {
364 $args = func_get_args();
365 $text .= '/'. implode('/', array_slice($args, 2));
366 }
367
368 $env = faceted_search_env_load($env_id);
369
370 // Initialize the current search.
371 $env->prepare($text);
372 $env->ui_state['stage'] = 'results';
373
374 if ($text) {
375 // Log the search text.
376 $path = faceted_search_ui_build_path($env, $env->ui_state, $text);
377 watchdog('faceted_search', '%text.', array('%text' => $text), WATCHDOG_NOTICE, l(t('results'), $path));
378 }
379
380 faceted_search_ui_add_robots_directive($env);
381 faceted_search_ui_add_css();
382
383 // Collect the search results.
384 $env->execute();
385 $style = faceted_search_ui_get_style($env);
386 if (method_exists($style, 'format_results')) {
387 $results = $style->format_results($env);
388 }
389
390 faceted_search_ui_set_title($env);
391 $content = theme('faceted_search_ui_stage_results', $results, $style);
392 return theme('faceted_search_ui_page', $env, $content);
393 }
394
395 /**
396 * Implementation of hook_faceted_search_ui_style_info().
397 */
398 function faceted_search_ui_faceted_search_ui_style_info() {
399 return array(
400 'extracts' => new faceted_search_ui_extract_style,
401 'teasers' => new faceted_search_ui_teaser_style,
402 );
403 }
404
405 /**
406 * Menu callback to display the search page.
407 */
408 function faceted_search_ui_stage_select($env_id, $text = '') {
409 // If the menu system has splitted the search text because of slashes, glue it back.
410 if (func_num_args() > 2) {
411 $args = func_get_args();
412 $text .= '/'. implode('/', array_slice($args, 2));
413 }
414
415 $env = faceted_search_env_load($env_id);
416
417 // Initialize the current search.
418 $env->prepare($text);
419 $env->ui_state['stage'] = 'select';
420
421 faceted_search_ui_add_robots_directive($env);
422 faceted_search_ui_add_css();
423
424 // Build the search results, which are required to count nodes per category
425 $env->execute();
426
427 faceted_search_ui_set_title($env);
428
429 $keyword_block_content = faceted_search_ui_keyword_block($env);
430 $guided_block_content = faceted_search_ui_guided_block($env);
431 if ($output = theme('faceted_search_ui_stage_select', $env, $keyword_block_content, $guided_block_content)) {
432 return theme('faceted_search_ui_page', $env, $output);
433 }
434 }
435
436 /**
437 * Menu callback to display a facet's categories.
438 */
439 function faceted_search_ui_stage_facet($env_id, $facet_key_id = '', $facet_sort = '', $text = '') {
440 if (!$facet_key_id || !$facet_sort) {
441 drupal_not_found();
442 return;
443 }
444
445 // If the menu system has splitted the search text because of slashes, glue it back.
446 if (func_num_args() > 4) {
447 $args = func_get_args();
448 $text .= '/'. implode('/', array_slice($args, 4));
449 }
450
451 $env = faceted_search_env_load($env_id);
452
453 // Initialize the current search.
454 $env->prepare($text);
455 $env->ui_state['stage'] = 'facet';
456 list($facet_key, $facet_id) = explode(':', $facet_key_id, 2);
457 $env->ui_state['facet-key'] = $facet_key;
458 $env->ui_state['facet-id'] = $facet_id;
459 $env->ui_state['facet-sort'] = $facet_sort;
460
461 faceted_search_ui_add_robots_directive($env);
462 faceted_search_ui_add_css();
463 faceted_search_ui_add_tooltips($env_id);
464
465 // Build the search results, which are required to count nodes per category
466 $env->execute();
467
468 // Find what facet to show
469 list($index, $facet) = $env->get_filter_by_id($facet_key, $facet_id);
470 if (!isset($index) || !isset($facet) || !$facet->is_browsable()) {
471 drupal_not_found();
472 return;
473 }
474
475 faceted_search_ui_set_title($env);
476 $categories = faceted_search_ui_build_categories($env, $index); // TODO: paging
477 return theme('faceted_search_ui_stage_facet', $env, $index, $facet, $categories);
478 }
479
480 /**
481 * Menu callback to display an HTML chunk with categories related to a given
482 * facet (using AJAX).
483 */
484 function faceted_search_ui_stage_categories($env_id, $facet_key_id = '', $facet_sort = '', $text = '') {
485 if (!$facet_key_id || !$facet_sort) {
486 exit();
487 }
488
489 // If the menu system has splitted the search text because of slashes, glue it back.
490 if (func_num_args() > 4) {
491 $args = func_get_args();
492 $text .= '/'. implode('/', array_slice($args, 4));
493 }
494
495 $env = faceted_search_env_load($env_id);
496
497 // Initialize the current search.
498 $env->prepare($text);
499 $env->ui_state['stage'] = 'categories';
500 list($facet_key, $facet_id) = explode(':', $facet_key_id, 2);
501 $env->ui_state['facet-key'] = $facet_key;
502 $env->ui_state['facet-id'] = $facet_id;
503 $env->ui_state['facet-sort'] = $facet_sort;
504
505 // We are returning JavaScript, so tell the browser.
506 drupal_set_header('Content-Type: text/javascript; charset=utf-8');
507
508 // Build the search results, which are required to count nodes per category
509 $env->execute();
510
511 // Find what facet to show
512 $found = FALSE;
513 $facets = $env->get_filters();
514 foreach ($facets as $index => $facet) {
515 if ($facet->is_browsable() && $facet->get_key() == $facet_key && $facet->get_id() == $facet_id) {
516 $found = TRUE;
517 break; // Found
518 }
519 }
520 if (!$found) {
521 exit();
522 }
523
524 if ($facet->get_max_categories() > 0) {
525 $max_count = $facet->get_max_categories();
526 }
527 else {
528 $max_count = NULL; // No limit.
529 }
530
531 // Prepare the HTML chunk
532 $categories = faceted_search_ui_build_categories($env, $index, 0, $max_count, FALSE);
533 if (count($categories)) {
534 $content = '<p>'. t('Subcategories:') .'</p>';
535 $content .= theme('faceted_search_ui_categories', $facet, $categories, 'categories');
536 }
537 else {
538 $content = '<p>'. t('No subcategories.') .'</p>';
539 }
540 $content = theme('faceted_search_ui_facet_wrapper', $env, $facet, 'categories', $content);
541
542 // Output JSON data, with id to help client to cache the content.
543 print drupal_to_js(
544 array(
545 'id' => $env->get_text(),
546 'content' => $content,
547 )
548 );
549 exit();
550 }
551
552 /**
553 * Display the current search arguments.
554 */
555 function faceted_search_ui_current_block($search) {
556 if (!$search->get_text()) {
557 return; // No current search
558 }
559
560 foreach ($search->get_filters() as $index => $filter) {
561 if ($filter->is_active()) {
562 $content = theme('faceted_search_ui_facet_heading', $search, $search->ui_state, $search->get_filters(), $index, 'current');
563 $output .= theme('faceted_search_ui_facet_wrapper', $search, $filter, 'current', $content);
564 }
565 }
566
567 return $output;
568 }
569
570 /**
571 * Display the keyword search form.
572 */
573 function faceted_search_ui_keyword_block($search) {
574 return drupal_get_form('faceted_search_ui_form_'. $search->env_id, $search);
575 }
576
577 /**
578 * Display the faceted browser.
579 */
580 function faceted_search_ui_guided_block($search) {
581 faceted_search_ui_add_tooltips($search);
582
583 $facet_content = array();
584 foreach ($search->get_filters() as $index => $facet) {
585 if (!$facet->is_browsable()) {
586 continue; // Not a facet.
587 }
588
589 if ($facet->get_max_categories() > 0) {
590 $max_count = $facet->get_max_categories();
591 }
592 else {
593 $max_count = NULL; // No limit.
594 }
595
596 $categories = faceted_search_ui_build_categories($search, $index, 0, $max_count);
597 if (count($categories) > 0 || $facet->is_active()) {
598 $content = theme('faceted_search_ui_facet_heading', $search, $search->ui_state, $search->get_filters(), $index, 'guided');
599 $content .= theme('faceted_search_ui_categories', $facet, $categories, $search->ui_state['stage']);
600 $facet_content[] = theme('faceted_search_ui_facet_wrapper', $search, $facet, 'guided', $content);
601 }
602 }
603 return theme('faceted_search_ui_guided_search', $search, $facet_content);
604 }
605
606 /**
607 * Display the facets related to the specified node.
608 *
609 * Facets with the same key and id are grouped under the same label (instead of
610 * repeating the label).
611 */
612 function faceted_search_ui_related_block($env, $node) {
613 $types = faceted_search_types($env);
614 if (!empty($types) && !isset($types[$node->type])) {
615 return; // This node type is not used in the current environment.
616 }
617
618 // Load settings for all enabled facets in this search environment.
619 $all_filter_settings = faceted_search_load_filter_settings($env);
620
621 // Make a selection with all enabled facets.
622 $selection = faceted_search_get_filter_selection($all_filter_settings);
623
624 // Collect all facets relevant to this node.
625 $facets = array();
626 foreach (module_implements('faceted_search_collect') as $module) {
627 $hook = $module .'_faceted_search_collect';
628 $hook($facets, 'node', $env, $selection, $node);
629 }
630
631 // Prepare facets for use, assigning them their settings are sorting them.
632 faceted_search_prepare_filters($facets, $all_filter_settings);
633
634 // Organize facets so that those that have a common label are grouped together.
635 $groups = array(); // All groups of facets.
636 $index = -1; // Index of the current group of facets.
637 $last_facet = '';
638 foreach ($facets as $facet) {
639 $current_facet = $facet->get_key() .'-'. $facet->get_id();
640 if ($current_facet != $last_facet) {
641 $index++; // Start a new group.
642 }
643 $groups[$index][] = $facet;
644 $last_facet = $current_facet;
645 }
646
647 return theme('faceted_search_ui_related_'. $env->settings['related_style'], $env, $node, $groups);
648 }
649
650 /**
651 * Discover the available display styles by invoking
652 * hook_faceted_search_ui_style_info().
653 *
654 * @return
655 * An associative array keyed on style id. The id contains the module's name
656 * to ensure it is globally unique. The value of each key is an array
657 * containing information about the style: 'label', 'callback', 'args'.
658 *
659 * @see faceted_search_ui_views_faceted_search_ui_style_info()
660 */
661 function faceted_search_ui_style_list() {
662 static $styles;
663 if (!isset($styles)) {
664 $styles = array();
665 foreach (module_implements('faceted_search_ui_style_info') as $module) {
666 $function = $module .'_faceted_search_ui_style_info';
667 if ($module_styles = $function()) {
668 foreach ($module_styles as $id => $style) {
669 // Add each of the styles provided by the module to the global array,
670 // prepending the style id with the module name.
671 $styles[$module .':'. $id] = $style;
672 }
673 }
674 }
675 }
676 return $styles;
677 }
678
679 /**
680 * Return the style object to use for the specified search.
681 */
682 function faceted_search_ui_get_style($env) {
683 if ($env->settings['results_style_selective_extracts'] && $env->get_keywords()) {
684 $style = 'faceted_search_ui:extracts';
685 }
686 else {
687 $style = $env->settings['results_style'];
688 }
689 $styles = faceted_search_ui_style_list();
690 return $styles[$style];
691 }
692
693 /**
694 * Display sort options.
695 */
696 function faceted_search_ui_sort_block($search) {
697 $ui_state = $search->ui_state;
698 if ($ui_state['stage'] == 'facet') {
699 list($index, $facet) = $search->get_filter_by_id($search->ui_state['facet-key'], $search->ui_state['facet-id']);
700 $sort_options = $facet->get_sort_options();
701 if (count($sort_options) > 1) {
702 $sort_links = array();
703 foreach ($sort_options as $option => $name) {
704 $attributes = $search->ui_state['facet-sort'] == $option ? array('class' => 'active') : array();
705 $ui_state['facet-sort'] = $option;
706 $path = faceted_search_ui_build_path($search, $ui_state, $search->get_text());
707 $sort_links[] = l($name, $path, $attributes);
708 }
709 return theme('faceted_search_ui_sort_options', $sort_links);
710 }
711 }
712 }
713
714 /**
715 * Callback for the menu title.
716 */
717 function faceted_search_ui_menu_title($env_id) {
718 $env = faceted_search_env_load($env_id);
719 $title = $env->settings['title'];
720 if (module_exists('i18nstrings')) {
721 $title = tt("faceted_search:$env_id:title", $title);
722 }
723 return check_plain($title);
724 }
725
726 /**
727 * Sets the page title.
728 */
729 function faceted_search_ui_set_title($env) {
730 $labels = array();
731 if ($env->get_text()) {
732 foreach ($env->get_filters() as $filter) {
733 if ($filter->is_active()) {
734 $category = $filter->get_active_category();
735 // Note: get_label() is responsible for filtering its returned string.
736 $labels[] = $category->get_label(FALSE);
737 }
738 }
739 }
740 $title = $env->settings['title'];
741 if (module_exists('i18nstrings')) {
742 $title = tt('faceted_search:'. $env->env_id .':title', $title);
743 }
744 if (count($labels)) {
745 drupal_set_title(t('@title: !terms', array('@title' => $title, '!terms' => implode(', ', $labels))));
746 }
747 else {
748 drupal_set_title(check_plain($title));
749 }
750 }
751
752 /**
753 * Render the base search form.
754 */
755 function faceted_search_ui_form($form_state, $search) {
756 $ui_state = $search->ui_state;
757 $current_stage = $ui_state['stage'];
758
759 // Add the keywords to search.
760 $form['keywords'] = array(
761 '#type' => 'textfield',
762 '#title' => '',
763 '#default_value' => '',
764 '#size' => $current_stage == 'select' ? 35 : 20,
765 '#maxlength' => 255,
766 );
767
768 // TODO: This ought to be cached and greatly simplified... {
769
770 // Load settings for all enabled filters in this search environment.
771 $all_filter_settings = faceted_search_load_filter_settings($search);
772
773 // Make a selection with all enabled filters.
774 $selection = faceted_search_get_filter_selection($all_filter_settings);
775
776 // Disallow filters that are already used by the search.
777 // TODO: Remove this limitation; see http://drupal.org/node/261111#comment-859279
778 foreach ($search->get_filters() as $filter) {
779 if ($filter->is_active()) {
780 unset($selection[$filter->get_key()][$filter->get_id()]);
781 }
782 }
783
784 // Gather keyword filters.
785 $keyword_filters = array();
786 foreach (module_implements('faceted_search_collect') as $module) {
787 $hook = $module .'_faceted_search_collect';
788 $hook($keyword_filters, 'keyword filters', $search, $selection);
789 }
790 // Gather the node keyword filter. This is the default, always-enabled keyword
791 // filter that allows searching in the full node index.
792 faceted_search_collect_node_keyword_filters($keyword_filters, 'keyword filters', $search);
793 // Prepare facets for use, assigning them their settings and sorting them.
794 faceted_search_prepare_filters($keyword_filters, $all_filter_settings);
795
796 // TODO }
797
798 // Add the field selector.
799 $options = array();
800 if ($current_stage == 'select' || $search->settings['keyword_field_selector']) {
801 foreach ($keyword_filters as $filter) {
802 if ($filter->get_key() == 'node') {
803 $options[$filter->get_key()] = $filter->get_label();
804 }
805 else {
806 $options[$filter->get_key()] = t('in @field', array('@field' => $filter->get_label()));
807 }
808 }
809 }
810 if (count($options) > 1) {
811 $form['field'] = array(
812 '#type' => 'select',
813 '#title' => '',
814 '#options' => $options,
815 '#default_value' => 'node'
816 );
817 }
818 else {
819 $form['field'] = array(
820 '#type' => 'value',
821 '#value' => 'node'
822 );
823 }
824
825 // Add the keyword operator.
826 if ($current_stage == 'select') {
827 $form['operator'] = array(
828 '#type' => 'select',
829 '#title' => '',
830 '#options' => array(
831 'and' => t('With all of the words'),
832 'phrase' => t('With the exact phrase'),
833 'or' => t('With at least one of the words'),
834 'not' => t('Without the words'),
835 ),
836 '#default_value' => 'and',
837 );
838 }
839 else {
840 $form['operator'] = array(
841 '#type' => 'value',
842 '#value' => 'and',
843 );
844 }
845
846 // Various search states.
847 if ($search->get_text() != '') {
848 $form['text'] = array(
849 '#type' => 'hidden',
850 '#value' => $search->get_text(),
851 );
852 $form['refine'] = array(
853 '#type' => 'checkbox',
854 '#title' => t('Search within results'),
855 '#default_value' => $search->settings['keyword_mode'] == 'new' ? 0 : 1,
856 );
857 }
858 $form['env'] = array(
859 '#type' => 'value',
860 '#value' => $search,
861 );
862 $form['stage'] = array(
863 '#type' => 'hidden',
864 '#value' => $current_stage == 'select' ? 'results' : $current_stage,
865 );
866 $form['facet-key'] = array(
867 '#type' => 'hidden',
868 '#value' => $search->ui_state['facet-key'],
869 );
870 $form['facet-id'] = array(
871 '#type' => 'hidden',
872 '#value' => $search->ui_state['facet-id'],
873 );
874 $form['facet-sort'] = array(
875 '#type' => 'hidden',
876 '#value' => $search->ui_state['facet-sort'],
877 );
878 $form['submit'] = array(
879 '#type' => 'submit',
880 '#value' => t('Search'),
881 );
882 if ($current_stage == 'select') {
883 if ($search->get_text()) {
884 // Add a Back to results button if we're in faceted search's select page.
885 $ui_state['stage'] = 'results';
886 $path = faceted_search_ui_build_path($search, $ui_state, $search->get_text());
887 $form['go-results']['#value'] = l(t('Back to results'), $path);
888 }
889 elseif ($search->settings['start_page'] != $_GET['q']) {
890 // Add a Cancel button if there's a 'start page' other than faceted search's select page.
891 $ui_state['stage'] = 'results';
892 $path = faceted_search_ui_build_path($search, $ui_state, $search->get_text());
893 $form['go-results']['#value'] = l(t('Cancel'), $path);
894 }
895 }
896 else {
897 $ui_state['stage'] = 'select';
898 $path = faceted_search_ui_build_path($search, $ui_state, $search->get_text());
899 $form['go-select']['#value'] = l(t('More options'), $path, array('attributes' => array('class' => 'faceted-search-more')));
900 }
901 $form['#submit'] = array('faceted_search_ui_form_submit');
902 return $form;
903 }
904
905 /**
906 * Process a search form submission.
907 */
908 function faceted_search_ui_form_submit($form, &$form_state) {
909 $new_text = '';
910 switch ($form_state['values']['operator']) {
911 // The following cases are based on node_search_validate().
912 case 'and':
913 if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $form_state['values']['keywords'], $matches)) {
914 $new_text = implode(' ', $matches[1]);
915 }
916 break;
917
918 case 'or':
919 if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $form_state['values']['keywords'], $matches)) {
920 $new_text = implode(' OR ', $matches[1]);
921 }
922 break;
923
924 case 'not':
925 if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $form_state['values']['keywords'], $matches)) {
926 $new_text = '-'. implode(' -', $matches[1]);
927 }
928 break;
929
930 case 'phrase':
931 $new_text .= ' "'. str_replace('"', ' ', $form_state['values']['keywords']) .'"';
932 break;
933 }
934
935 if ($new_text != '' && $form_state['values']['field'] != 'node') {
936 $escape_char = variable_get('faceted_search_escape_char', '\\');
937 $new_text = $form_state['values']['field'] .':"'. faceted_search_quoted_query_escape($new_text) .'"';
938 }
939
940 if ($form_state['values']['refine']) {
941 // Combine pre-exiting text with new one
942 $text = trim($form_state['values']['text'] .' '. $new_text);
943 }
944 else {
945 // Search with new text.
946 $text = $new_text;
947 }
948
949 // Define resulting path. Note: $form_state['values'] has the same keys as the usual
950 // $ui_state argument.
951 $form_state['redirect'] = faceted_search_ui_build_path($form_state['values']['env'], $form_state['values'], $text);
952 }
953
954 /**
955 * Build a search path with the specified components. If $text is not
956 * specified, the search text path component is generated from $facets.
957 */
958 function faceted_search_ui_build_path($env, $ui_state, $text, $facets = array()) {
959 if (!$text && $facets) {
960 $text = faceted_search_build_text($facets);
961 }
962
963 if (!$text && $ui_state['stage'] == 'results') {
964 // No search text, go to the start page.
965 return $env->settings['start_page'];
966 }
967
968 if ($ui_state['stage'] == 'select') {
969 $stage = '';
970 }
971 else {
972 $stage = '/'. $ui_state['stage'];
973 }
974
975 $facet = '';
976 $facet_sort = '';
977 if (($ui_state['stage'] == 'facet' || $ui_state['stage'] == 'categories') && $ui_state['facet-key'] && $ui_state['facet-id']) {
978 $facet = "/{$ui_state['facet-key']}:{$ui_state['facet-id']}";
979
980 if ($ui_state['stage'] == 'facet' && $ui_state['facet-sort']) {
981 $facet_sort = "/${ui_state['facet-sort']}";
982 }
983 else {
984 $facet_sort = '/-';
985 }
986 }
987
988 $base_path = $env->settings['base_path'];
989 return "{$base_path}{$stage}{$facet}{$facet_sort}/{$text}";
990 }
991
992 /**
993 * Build the remover path for a given facet.
994 *
995 * @param $ui_state
996 * The current state of the user interface.
997 * @param $env
998 * The search environment to use.
999 * @param $facets
1000 * The facets to take into account in the remover's links.
1001 * @param $index
1002 * Index of the facet whose remover shall be built.
1003 */
1004 function faceted_search_ui_build_remover_path($env, $ui_state, $facets, $index) {
1005 // Remove current facet for remover.
1006 unset($facets[$index]);
1007 // Switch to results stage.
1008 if ($ui_state['stage'] == 'select' || $ui_state['stage'] == 'facet') {
1009 $ui_state['stage'] = 'results';
1010 }
1011 return faceted_search_ui_build_path($env, $ui_state, '', $facets);
1012 }
1013
1014 /**
1015 * Build the breadcrumb of a facet according to its active path.
1016 *
1017 * @param $ui_state
1018 * The current state of the user interface.
1019 * @param $env
1020 * The search environment to use.
1021 * @param $facets
1022 * The facets to take into account in the breadcrumb's links.
1023 * @param $index
1024 * Index of the facet whose breadcrumb shall be built.
1025 * @param $breadcrumb
1026 * Array of initial components (or prefix components) of the breadcrumb, if
1027 * any is desired.
1028 * @param $context
1029 * The caller's context (either 'guided', 'current', or 'related').
1030 * @return
1031 * Array of breadcrumb components.
1032 */
1033 function faceted_search_ui_build_breadcrumb($env, $ui_state, $facets, $index, $breadcrumb = array(), $context = 'guided') {
1034 $facet = $facets[$index];
1035 if ($facet->is_active()) {
1036 // Replace the current facet with a clone for active category manipulations
1037 // (to avoid manipulating the original facet).
1038 $facets[$index] = drupal_clone($facet);
1039
1040 $path = array();
1041 foreach ($facet->get_active_path() as $category_index => $category) {
1042 $path[] = $category;
1043 // Replace active category within the facet
1044 $facets[$index]->set_active_path($path);
1045 // Switch to results stage.
1046 if ($ui_state['stage'] == 'select') {
1047 $ui_state['stage'] = 'results';
1048 }
1049 $link = faceted_search_ui_build_path($env, $ui_state, '', $facets);
1050 // Note: get_label() is responsible for filtering its returned string.
1051 $breadcrumb[] = l($category->get_label(TRUE), $link, array('html' => TRUE));
1052 }
1053 if ($category && $context != 'related') {
1054 // The last category needs not be a link to itself since it is already in
1055 // the current search.
1056 // Note: get_label() is responsible for filtering its returned string.
1057 $breadcrumb[count($breadcrumb) - 1] = $category->get_label(TRUE);
1058 }
1059 }
1060
1061 return $breadcrumb;
1062 }
1063
1064 /**
1065 * Build the subcategory links of a facet.
1066 *
1067 * @param $search
1068 * The search context.
1069 * @param $index
1070 * Index of the facet within the search context.
1071 * @param $from
1072 * Ordinal number of the first category to load. Numbering starts at 0.
1073 * @param $max_count
1074 * Number of categories to load.
1075 * @param $links
1076 * TRUE to generate category links, FALSE to generate text.
1077 */
1078 function faceted_search_ui_build_categories($search, $index, $from = NULL, $max_count = NULL, $links = TRUE) {
1079 $facets = $search->get_filters();
1080 $facet = $facets[$index];
1081 $facet->set_sort($search->ui_state['facet-sort']);
1082
1083 // Load the categories. If a limit is used, we load one extra category in
1084 // order to determine whether a 'more' link needs to be displayed.
1085 $categories = $search->load_categories($facet, $from, (isset($max_count) ? $max_count + 1 : $max_count));
1086
1087 // Replace the facet with a clone to allow active category manipulations (to
1088 // avoid manipulating the original facet).
1089 $facets[$index] = drupal_clone($facet);
1090
1091 $ui_state = $search->ui_state;
1092 $ui_state['stage'] = 'results';
1093 $ui_state['facet-key'] = '';
1094 $ui_state['facet-id'] = '';
1095 $ui_state['facet-sort'] = '';
1096
1097 // Build links to categories.
1098 // TODO: Separate into alphabetical groups (with first letter of each category label), if configured to do so
1099 $items = array();
1100 foreach ($categories as $category_index => $category) {
1101 if (isset($max_count) && $category_index == $max_count) {
1102 break; // Do not theme the extra category.
1103 }
1104 if ($links) {
1105 $active_path = $facet->get_active_path();
1106 $active_path[] = $category;
1107 // Replace active path in the facet
1108 $facets[$index]->set_active_path($active_path);
1109 $path = faceted_search_ui_build_path($search, $ui_state, '', $facets);
1110 }
1111 $items[] = theme('faceted_search_ui_category', $category, $path);
1112 }
1113
1114 $facets[$index] = $facet; // Restore the altered facet at $index.
1115
1116 // Add the 'more...' link.
1117 if ($search->ui_state['stage'] != 'facet' && isset($max_count) && count($categories) > $max_count) {
1118 if ($links) {
1119 $ui_state['stage'] = 'facet';
1120 $ui_state['facet-key'] = $facet->get_key</