Index: mollom.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.admin.inc,v
retrieving revision 1.1.2.41
diff -u -p -r1.1.2.41 mollom.admin.inc
--- mollom.admin.inc	20 Sep 2010 23:51:30 -0000	1.1.2.41
+++ mollom.admin.inc	21 Sep 2010 03:48:26 -0000
@@ -323,12 +323,16 @@ function mollom_admin_unprotect_form_sub
 function mollom_admin_blacklist_form(&$form_state, $type = 'spam') {
   $form['#tree'] = TRUE;
 
-  // Select options and translation of internal values for rendering.
+  // Translate internal reason values for rendering and select list in form.
   $contexts = array(
     'everything' => t('All fields'),
     'links' => t('Links'),
     'author' => t('Author name'),
   );
+  $matches = array(
+    'contains' => t('contains'),
+    'exact' => t('exact'),
+  );
 
   $form['blacklist'] = array();
   // Do not retrieve the current blacklist when submitting the form.
@@ -338,10 +342,21 @@ function mollom_admin_blacklist_form(&$f
       if ($entry['reason'] != $type) {
         continue;
       }
+      // #class property is internally used by
+      // theme_mollom_admin_blacklist_form().
       $row = array(
-        'context' => array('#value' => check_plain($contexts[$entry['context']])),
-        'text' => array('#value' => check_plain($entry['text'])),
-        'text' => array('#value' => check_plain($entry['text'])),
+        'context' => array(
+          '#value' => check_plain($contexts[$entry['context']]),
+          '#class' => 'mollom-blacklist-context-' . check_plain($entry['context']),
+        ),
+        'match' => array(
+          '#value' => check_plain($matches[$entry['match']]),
+          '#class' => 'mollom-blacklist-match-' . check_plain($entry['match']),
+        ),
+        'text' => array(
+          '#value' => check_plain($entry['text']),
+          '#class' => 'mollom-blacklist-text',
+        ),
       );
       $query = 'text=' . urlencode($entry['text']) . '&';
       $query .= 'context=' . urlencode($entry['context']) . '&';
@@ -355,17 +370,33 @@ function mollom_admin_blacklist_form(&$f
     }
   }
 
+  // The value NULL in the following select lists will intentionally trigger a
+  // form validation error, since the elements are #required.
+  // @see http://drupal.org/node/140783
   $form['entry']['context'] = array(
     '#type' => 'select',
-    '#options' => $contexts,
-    '#default_value' => 'everything',
+    '#title' => t('Context'),
+    '#options' => array(NULL => t('- Any -')) + $contexts,
     '#required' => TRUE,
+    '#id' => 'mollom-blacklist-filter-context',
+  );
+  $form['entry']['match'] = array(
+    '#type' => 'select',
+    '#title' => t('Match'),
+    '#options' => array(NULL => t('- Any -')) + $matches,
+    '#required' => TRUE,
+    '#id' => 'mollom-blacklist-filter-match',
   );
   $form['entry']['text'] = array(
     '#type' => 'textfield',
+    '#title' => t('Text'),
     '#size' => 40,
     '#required' => TRUE,
     '#maxlength' => 64,
+    '#id' => 'mollom-blacklist-filter-text',
+    '#attributes' => array(
+      'autocomplete' => 'off',
+    ),
   );
   $form['entry']['reason'] = array(
     '#type' => 'value',
@@ -389,6 +420,7 @@ function mollom_admin_blacklist_form_sub
   $data = array(
     'text' => $form_state['values']['entry']['text'],
     'context' => $form_state['values']['entry']['context'],
+    'match' => $form_state['values']['entry']['match'],
     'reason' => $form_state['values']['entry']['reason'],
   );
   $result = mollom('mollom.addBlacklistText', $data);
@@ -396,12 +428,13 @@ function mollom_admin_blacklist_form_sub
   $args = array(
     '@text' => $data['text'],
     '@context' => $data['context'],
+    '@match' => $data['match'],
     '@reason' => $data['reason'],
   );
   if ($result === TRUE) {
     drupal_set_message(t('The entry was added to the blacklist.'));
     _mollom_watchdog(array(
-      'Added @text (@context) to @reason blacklist.' => $args,
+      'Added @text (@context, @match) to @reason blacklist.' => $args,
       'Data:<pre>@data</pre>' => array('@data' => $data),
       'Result:<pre>@result</pre>' => array('@result' => $result),
     ));
@@ -409,7 +442,7 @@ function mollom_admin_blacklist_form_sub
   else {
     drupal_set_message(t('An error occurred upon trying to add the text to the blacklist.'), 'error');
     _mollom_watchdog(array(
-      'Failed to add @text (%context) to @reason blacklist.' => $args,
+      'Failed to add @text (@context, @match) to @reason blacklist.' => $args,
       'Data:<pre>@data</pre>' => array('@data' => $data),
       'Result:<pre>@result</pre>' => array('@result' => $result),
     ), WATCHDOG_ERROR);
@@ -422,27 +455,45 @@ function mollom_admin_blacklist_form_sub
 function theme_mollom_admin_blacklist_form($form) {
   $header = array(
     t('Context'),
+    t('Matches'),
     t('Text'),
     '',
   );
   $rows = array();
 
+  // D6 only: There is no #title_display yet, so unset the titles.
+  unset($form['entry']['context']['#title']);
+  unset($form['entry']['match']['#title']);
+  unset($form['entry']['text']['#title']);
   $rows[] = array(
     drupal_render($form['entry']['context']),
+    drupal_render($form['entry']['match']),
     drupal_render($form['entry']['text']),
     drupal_render($form['entry']['actions']),
   );
 
   foreach (element_children($form['blacklist']) as $id) {
     $rows[] = array(
-      drupal_render($form['blacklist'][$id]['context']),
-      drupal_render($form['blacklist'][$id]['text']),
+      array(
+        'data' => drupal_render($form['blacklist'][$id]['context']),
+        'class' => $form['blacklist'][$id]['context']['#class'],
+      ),
+      array(
+        'data' => drupal_render($form['blacklist'][$id]['match']),
+        'class' => $form['blacklist'][$id]['match']['#class'],
+      ),
+      array(
+        'data' => drupal_render($form['blacklist'][$id]['text']),
+        'class' => $form['blacklist'][$id]['text']['#class'],
+      ),
       drupal_render($form['blacklist'][$id]['actions']),
     );
   }
 
   // This table is never empty due to the form.
-  $output  = theme('table', $header, $rows);
+  $output = drupal_render($form['filter']);
+  $output .= theme('table', $header, $rows, array('id' => 'mollom-blacklist'));
+  drupal_add_js(drupal_get_path('module', 'mollom') . '/mollom.admin.js');
   $output .= drupal_render($form);
 
   return $output;
Index: mollom.admin.js
===================================================================
RCS file: mollom.admin.js
diff -N mollom.admin.js
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ mollom.admin.js	21 Sep 2010 05:05:46 -0000
@@ -0,0 +1,63 @@
+// $Id: mollom.js,v 1.9 2010/08/04 03:58:46 dries Exp $
+(function ($) {
+
+/**
+ * Filters blacklist entries.
+ */
+Drupal.behaviors.mollomBlacklistFilter = function (context) {
+  // Intentionally wrong indentation for simplified maintenance.
+    var self = {};
+    $('#mollom-blacklist:not(.mollom-processed)', context).addClass('mollom-processed').each(function () {
+      // Prepare a list of all entries to optimize performance. Each key is a
+      // blacklisted text and each value is an object containing the
+      // corresponding table row, context, and match.
+      self.entries = {};
+      $(this).find('tr:has(.mollom-blacklist-text)').each(function () {
+        var $row = $(this);
+        self.entries[$row.find('.mollom-blacklist-text').text()] = {
+          row: $row.get(0),
+          context: $row.children('td').eq(0).attr('class').substring(25),
+          match: $row.children('td').eq(1).attr('class').substring(23)
+        };
+      });
+
+      // Attach the instant text filtering behavior.
+      var $filterText = $('#mollom-blacklist-filter-text', context);
+      var $filterContext = $('#mollom-blacklist-filter-context', context);
+      var $filterMatch = $('#mollom-blacklist-filter-match', context);
+
+      var filterRows = function () {
+        // @todo Store last known values and immediately cancel processing if
+        //   nothing changed.
+
+        // Prepare static variables and conditions only once.
+        var text, visible;
+        var search = {
+          text: $filterText.val(),
+          context: $filterContext.val(),
+          match: $filterMatch.val()
+        };
+        // Blacklists can contain plenty of entries (thousands), so we use a
+        // simple for...in loop instead of jQuery.each() to save many function
+        // calls. Likewise, we directly apply the 'display' style, since
+        // jQuery.fn.hide() and jQuery.fn.show() call into jQuery.fn.animate(),
+        // which is useless for this purpose.
+        for (text in self.entries) {
+          visible = (search.text.length == 0 || text.indexOf(search.text) != -1);
+          visible = visible && (search.context.length == 0 || self.entries[text].context == search.context);
+          visible = visible && (search.match.length == 0 || self.entries[text].match == search.match);
+          if (visible) {
+            self.entries[text].row.style.display = '';
+          }
+          else {
+            self.entries[text].row.style.display = 'none';
+          }
+        }
+      };
+      $filterText.keyup(filterRows);
+      $filterContext.change(filterRows);
+      $filterMatch.change(filterRows);
+    });
+};
+
+})(jQuery);
