Add subfolder searching to search UI
authorMichael M Slusarz <slusarz@curecanti.org>
Thu, 18 Nov 2010 23:54:41 +0000 (16:54 -0700)
committerMichael M Slusarz <slusarz@curecanti.org>
Fri, 19 Nov 2010 20:10:48 +0000 (13:10 -0700)
For now, remove Tree display of mailboxes in UI.  It might be a bit
easier to add mailboxes using a tree, but it is more difficult
to visualize the current list of search mailboxes (and I can't figure
out a clean way to allow subfolder searches to be defined).

imp/docs/CHANGES
imp/js/search.js
imp/lib/Imap/Tree.php
imp/lib/Search/Query.php
imp/search.php
imp/templates/imp/search/search.html
imp/themes/ie6_or_less.css
imp/themes/ie7.css
imp/themes/screen.css

index cb782c6..fc110e6 100644 (file)
@@ -2,6 +2,7 @@
 v5.0-git
 --------
 
+[mms] Add subfolder searching.
 [mms] Refactor inline message image blocking to operate on all messages, not
       just HTML messages.
 [mms] Add attachment message filter.
index ed6a671..59eac48 100644 (file)
@@ -7,24 +7,28 @@
 
 var ImpSearch = {
     // The following variables are defined in search.php:
-    //   data, i_criteria, recent, selected, text
+    //   data, i_criteria, i_folders, i_recent, text
     criteria: {},
+    folders: $H(),
     saved_searches: {},
 
-    getAll: function()
+    updateRecentSearches: function(searches)
     {
-        return $('search_form').getInputs(null, 'folder_list[]');
-    },
+        var fragment = document.createDocumentFragment(),
+            node = new Element('OPTION');
 
-    selectFolders: function(checked)
-    {
-        this.getAll().each(function(e) {
-            if (!e.disabled) {
-                e.checked = Boolean(checked);
-            }
-        });
+        $('recent_searches_div').show().next().show();
+
+        this.saved_searches = $H(searches);
+        this.saved_searches.each(function(s) {
+            fragment.appendChild($(node.clone(false)).writeAttribute({ value: s.key.escapeHTML() }).update(s.value.l.escapeHTML()));
+        }, this);
+
+        $('recent_searches').appendChild(fragment);
     },
 
+    // Criteria actions
+
     showOr: function(show)
     {
         var or = $('search_criteria_add').down('[value="or"]');
@@ -35,22 +39,7 @@ var ImpSearch = {
         }
     },
 
-    updateRecentSearches: function(searches)
-    {
-        var fragment = document.createDocumentFragment(),
-            node = new Element('OPTION');
-
-        $('recent_searches_div').show();
-
-        $H(searches).each(function(s) {
-            fragment.appendChild($(node.clone(false)).writeAttribute({ value: s.value.v.escapeHTML() }).update(s.value.l.escapeHTML()));
-            this.saved_searches[s.key] = s.value.c;
-        }, this);
-
-        $('recent_searches').appendChild(fragment);
-    },
-
-    updateSearchCriteria: function(criteria)
+    updateCriteria: function(criteria)
     {
         this.resetCriteria();
 
@@ -77,7 +66,7 @@ var ImpSearch = {
                 case 'cc':
                 case 'bcc':
                 case 'subject':
-                    this.insertText(crit.h.capitalize(), crit.t, crit.n);
+                    this.insertText(crit.h, crit.t, crit.n);
                     break;
 
                 default:
@@ -115,82 +104,14 @@ var ImpSearch = {
                 break;
             }
         }, this);
-    },
-
-    updateSelectedFolders: function(folders)
-    {
-        var tmp = $('search_folders_hdr');
 
-        if (!tmp) {
-            return;
-        }
-
-        tmp = tmp.next();
-        this.selectFolders(false);
-        folders.each(function(f) {
-            var i = tmp.down('INPUT[value=' + f + ']');
-            if (i) {
-                i.checked = true;
-            }
-        });
-    },
-
-    changeHandler: function(e)
-    {
-        var elt = e.element(), val = $F(elt);
-
-        switch (elt.readAttribute('id')) {
-        case 'recent_searches':
-            this.updateSearchCriteria(this.saved_searches[$F(elt)]);
-            if (!$('search_criteria').up().visible()) {
-                this.toggleHeader($('search_criteria').up().previous());
-            }
-            elt.clear();
-            break;
-
-        case 'search_criteria_add':
-            if (val == 'or') {
-                this.insertOr();
-                break;
-            }
-
-            switch (this.data.types[val]) {
-            case 'header':
-            case 'text':
-                this.insertText(val);
-                break;
-
-            case 'customhdr':
-                this.insertCustomHdr();
-                break;
-
-            case 'size':
-                this.insertSize(val);
-                break;
-
-            case 'date':
-                this.insertDate(val);
-                break;
-
-            case 'within':
-                this.insertWithin(val);
-                break;
-
-            case 'filter':
-                this.insertFilter(val);
-                break;
-
-            case 'flag':
-                this.insertFlag(val);
-                break;
-            }
-            break;
+        if ($('search_criteria').childElements().size()) {
+            $('no_search_criteria', 'search_criteria').invoke('toggle');
+            this.showOr(true);
         }
-
-        e.stop();
     },
 
-    getLabel: function(id)
+    getCriteriaLabel: function(id)
     {
         return $('search_criteria_add').down('[value="' + RegExp.escape(id) + '"]').getText() + ': ';
     },
@@ -217,26 +138,32 @@ var ImpSearch = {
 
         if (!keys.size()) {
             this.showOr(false);
+            $('no_search_criteria', 'search_criteria').invoke('toggle');
         }
     },
 
     resetCriteria: function()
     {
-        $('search_criteria').childElements().invoke('remove');
-        this.criteria = {};
-        this.showOr(false);
+        var elts = $('search_criteria').childElements();
+        if (elts) {
+            elts.invoke('remove');
+            $('no_search_criteria', 'search_criteria').invoke('toggle');
+            this.criteria = {};
+            this.showOr(false);
+        }
     },
 
     insertCriteria: function(tds)
     {
-        var div = new Element('DIV', { className: 'searchCriteriaId' }),
-            div2 = new Element('DIV', { className: 'searchCriteriaElement' });
+        var div = new Element('DIV', { className: 'searchId' }),
+            div2 = new Element('DIV', { className: 'searchElement' });
 
         if ($('search_criteria').childElements().size()) {
             if (this.criteria[$('search_criteria').childElements().last().readAttribute('id')].t != 'or') {
                 div.insert(new Element('EM', { className: 'join' }).insert(this.text.and));
             }
         } else {
+            $('no_search_criteria', 'search_criteria').invoke('toggle');
             this.showOr(true);
         }
 
@@ -265,7 +192,7 @@ var ImpSearch = {
     insertText: function(id, text, not)
     {
         var tmp = [
-            new Element('EM').insert(this.getLabel(id)),
+            new Element('EM').insert(this.getCriteriaLabel(id)),
             new Element('INPUT', { type: 'text', size: 25 }).setValue(text),
             new Element('SPAN', { className: 'notMatch' }).insert(new Element('INPUT', { checked: Boolean(not), className: 'checkbox', type: 'checkbox' })).insert(this.text.not_match)
         ];
@@ -290,7 +217,7 @@ var ImpSearch = {
     insertSize: function(id, size)
     {
         var tmp = [
-            new Element('EM').insert(this.getLabel(id)),
+            new Element('EM').insert(this.getCriteriaLabel(id)),
             // Convert from bytes to KB
             new Element('INPUT', { type: 'text', size: 10 }).setValue(Object.isNumber(size) ? Math.round(size / 1024) : '')
         ];
@@ -305,7 +232,7 @@ var ImpSearch = {
         }
 
         var tmp = [
-                new Element('EM').insert(this.getLabel(id)),
+                new Element('EM').insert(this.getCriteriaLabel(id)),
                 new Element('SPAN').insert(new Element('SPAN')).insert(new Element('A', { href: '#', className: 'calendarPopup', title: this.text.dateselection }).insert(new Element('SPAN', { className: 'iconImg searchuiImg searchuiCalendar' })))
             ];
         this.replaceDate(this.insertCriteria(tmp), id, data);
@@ -313,7 +240,7 @@ var ImpSearch = {
 
     replaceDate: function(id, type, d)
     {
-        $(id).down('TD SPAN SPAN').update(this.data.months[d.getMonth()] + ' ' + d.getDate() + ', ' + (d.getYear() + 1900));
+        $(id).down('SPAN SPAN').update(this.data.months[d.getMonth()] + ' ' + d.getDate() + ', ' + (d.getYear() + 1900));
         // Need to store date information at all times in criteria, since we
         // have no other way to track this information (there is not form
         // field for this type).
@@ -325,7 +252,7 @@ var ImpSearch = {
         data = data || { l: '', v: '' };
 
         var tmp = [
-            new Element('EM').insert(this.getLabel(id)),
+            new Element('EM').insert(this.getCriteriaLabel(id)),
             new Element('SPAN').insert(new Element('INPUT', { type: 'text', size: 8 }).setValue(data.v)).insert(' ').insert($($('within_criteria').clone(true)).writeAttribute({ id: null }).show().setValue(data.l))
         ];
         this.criteria[this.insertCriteria(tmp)] = { t: id };
@@ -335,7 +262,7 @@ var ImpSearch = {
     insertFilter: function(id, not)
     {
         var tmp = [
-            new Element('EM').insert(this.getLabel(id)),
+            new Element('EM').insert(this.getCriteriaLabel(id)),
             new Element('SPAN').insert(new Element('INPUT', { checked: Boolean(not), className: 'checkbox', type: 'checkbox' })).insert(this.text.not_match)
         ];
         this.criteria[this.insertCriteria(tmp)] = { t: id };
@@ -345,84 +272,195 @@ var ImpSearch = {
     {
         var tmp = [
             new Element('EM').insert(this.text.flag),
-            this.getLabel(id).slice(0, -2),
+            this.getCriteriaLabel(id).slice(0, -2),
             new Element('SPAN').insert(new Element('INPUT', { checked: Boolean(not), className: 'checkbox', type: 'checkbox' })).insert(this.text.not_match)
         ];
         this.criteria[this.insertCriteria(tmp)] = { t: id };
     },
 
+    // Folder actions
+
+    // folders = (object) m: mailboxes, s: subfolders
+    updateFolders: function(folders)
+    {
+        this.resetFolders();
+        folders.m.each(function(f) {
+            this.insertFolder(f, false);
+        }, this);
+        folders.s.each(function(f) {
+            this.insertFolder(f, true);
+        }, this);
+    },
+
+    deleteFolder: function(div)
+    {
+        var first, keys,
+            id = div.identify()
+
+        this.disableFolder(false, this.folders.get(id));
+        this.folders.unset(id);
+        div.remove();
+
+        keys = $('search_folders').childElements().pluck('id');
+
+        if (keys.size()) {
+            first = keys.first();
+            if ($(first).down().hasClassName('join')) {
+                $(first).down().remove();
+            }
+        }
+
+        if (!keys.size()) {
+            $('no_search_folders', 'search_folders').invoke('toggle');
+        }
+    },
+
+    resetFolders: function()
+    {
+        elts = $('search_folders').childElements();
+
+        if (elts.size()) {
+            this.folders.values().each(this.disableFolder.bind(this, false));
+            elts.invoke('remove');
+            $('no_search_folders', 'search_folders').invoke('toggle');
+            this.folders = $H();
+        }
+    },
+
+    insertFolder: function(folder, checked)
+    {
+        var id,
+            div = new Element('DIV', { className: 'searchId' }),
+            div2 = new Element('DIV', { className: 'searchElement' });
+
+        if ($('search_folders').childElements().size()) {
+            div.insert(new Element('EM', { className: 'join' }).insert(this.text.and));
+        } else {
+            $('no_search_folders', 'search_folders').invoke('toggle');
+        }
+
+        div.insert(div2);
+
+        div2.insert(
+            new Element('EM').insert(this.getFolderLabel(folder).escapeHTML())
+        ).insert(
+            new Element('SPAN', { className: 'subfolders' }).insert(new Element('INPUT', { checked: checked, className: 'checkbox', type: 'checkbox' })).insert(this.text.subfolder_search)
+        ).insert(
+            new Element('A', { href: '#', className: 'iconImg searchuiImg searchuiDelete' })
+        );
+
+
+        this.disableFolder(true, folder);
+        $('search_folders_add').clear();
+        $('search_folders').insert(div);
+
+        id = div.identify();
+        this.folders.set(id, folder);
+
+        return id;
+    },
+
+    getFolderLabel: function(folder)
+    {
+        return this.data.folder_list[folder];
+    },
+
+    disableFolder: function(disable, folder)
+    {
+        $('search_folders_add').down('[value="' + escape(folder) + '"]').writeAttribute({ disabled: disable });
+    },
+
+    // Miscellaneous actions
+
     submit: function()
     {
-        var data = [], tmp;
+        var criteria,
+            data = [],
+            f_out = { mbox: [], subfolder: [] },
+            sflist = [];
 
-        if ($('search_folders_hdr') &&
-            !this.getAll().findAll(function(i) { return i.checked; }).size()) {
-            alert(this.text.need_folder);
-        } else if ($F('search_type') && !$('search_label').present()) {
+        if ($F('search_type') && !$('search_label').present()) {
             alert(this.text.need_label);
-        } else {
-            tmp = $('search_criteria').childElements().pluck('id');
-            if (tmp.size()) {
-                tmp.each(function(c) {
-                    var tmp2;
-
-                    if (this.criteria[c].t == 'or') {
-                        data.push(this.criteria[c]);
-                        return;
-                    }
+            return;
+        }
 
-                    switch (this.data.types[this.criteria[c].t]) {
-                    case 'header':
-                    case 'text':
-                        this.criteria[c].n = Number(Boolean($F($(c).down('INPUT[type=checkbox]'))));
-                        this.criteria[c].v = $F($(c).down('INPUT[type=text]'));
-                        data.push(this.criteria[c]);
-                        break;
-
-                    case 'customhdr':
-                        this.criteria[c].v = { h: $F($(c).down('INPUT')), s: $F($(c).down('INPUT', 1)) };
-                        data.push(this.criteria[c]);
-                        break;
-
-                    case 'size':
-                        tmp2 = Number($F($(c).down('INPUT')));
-                        if (!isNaN(tmp2)) {
-                            // Convert KB to bytes
-                            this.criteria[c].v = tmp2 * 1024;
-                            data.push(this.criteria[c]);
-                        }
-                        break;
-
-                    case 'date':
-                        data.push(this.criteria[c]);
-                        break;
-
-                    case 'within':
-                        this.criteria[c].v = { l: $F($(c).down('SELECT')), v: parseInt($F($(c).down('INPUT')), 10) };
-                        data.push(this.criteria[c]);
-                        break;
-
-                    case 'filter':
-                        this.criteria[c].n = Number(Boolean($F($(c).down('INPUT[type=checkbox]'))));
-                        data.push(this.criteria[c]);
-                        break;
-
-                    case 'flag':
-                        this.criteria[c].n = Number(Boolean($F($(c).down('INPUT[type=checkbox]'))));
-                        data.push({
-                            n: this.criteria[c].n,
-                            t: 'flag',
-                            v: this.criteria[c].t
-                        });
-                        break;
-                    }
-                }, this);
-                $('criteria_form').setValue(Object.toJSON(data));
-                $('search_form').submit();
-            } else {
-                alert(this.text.need_criteria);
-            }
+        if (!this.folders.size()) {
+            alert(this.text.need_folder);
+            return;
+        }
+
+        criteria = $('search_criteria').childElements().pluck('id');
+        if (!criteria.size()) {
+            alert(this.text.need_criteria);
+            return;
         }
+
+        criteria.each(function(c) {
+            var tmp;
+
+            if (this.criteria[c].t == 'or') {
+                data.push(this.criteria[c]);
+                return;
+            }
+
+            switch (this.data.types[this.criteria[c].t]) {
+            case 'header':
+            case 'text':
+                this.criteria[c].n = Number(Boolean($F($(c).down('INPUT[type=checkbox]'))));
+                this.criteria[c].v = $F($(c).down('INPUT[type=text]'));
+                data.push(this.criteria[c]);
+                break;
+
+            case 'customhdr':
+                this.criteria[c].v = { h: $F($(c).down('INPUT')), s: $F($(c).down('INPUT', 1)) };
+                data.push(this.criteria[c]);
+                break;
+
+            case 'size':
+                tmp2 = Number($F($(c).down('INPUT')));
+                if (!isNaN(tmp2)) {
+                    // Convert KB to bytes
+                    this.criteria[c].v = tmp2 * 1024;
+                    data.push(this.criteria[c]);
+                }
+                break;
+
+            case 'date':
+                data.push(this.criteria[c]);
+                break;
+
+            case 'within':
+                this.criteria[c].v = { l: $F($(c).down('SELECT')), v: parseInt($F($(c).down('INPUT')), 10) };
+                data.push(this.criteria[c]);
+                break;
+
+            case 'filter':
+                this.criteria[c].n = Number(Boolean($F($(c).down('INPUT[type=checkbox]'))));
+                data.push(this.criteria[c]);
+                break;
+
+            case 'flag':
+                this.criteria[c].n = Number(Boolean($F($(c).down('INPUT[type=checkbox]'))));
+                data.push({
+                    n: this.criteria[c].n,
+                    t: 'flag',
+                    v: this.criteria[c].t
+                });
+                break;
+            }
+        }, this);
+
+        $('criteria_form').setValue(Object.toJSON(data));
+
+        this.folders.each(function(f) {
+            var type = $F($(f.key).down('INPUT[type=checkbox]'))
+                ? 'subfolder'
+                : 'mbox';
+            f_out[type].push(f.value);
+        });
+        $('folders_form').setValue(Object.toJSON(f_out));
+
+        $('search_form').submit();
     },
 
     clickHandler: function(e)
@@ -445,7 +483,7 @@ var ImpSearch = {
 
             case 'search_reset':
                 this.resetCriteria();
-                this.selectFolders(false);
+                this.resetFolders();
                 return;
 
             case 'search_dimp_return':
@@ -453,12 +491,6 @@ var ImpSearch = {
                 window.parent.DimpBase.go('folder:' + this.data.searchmbox);
                 return;
 
-            case 'link_sel_all':
-            case 'link_sel_none':
-                this.selectFolders(id == 'link_sel_all');
-                e.stop();
-                return;
-
             case 'search_edit_query_cancel':
                 e.stop();
                 if (this.data.dimp) {
@@ -469,15 +501,16 @@ var ImpSearch = {
                 return;
 
             default:
-                if (elt.hasClassName('arrowExpanded') ||
-                    elt.hasClassName('arrowCollapsed')) {
-                    this.toggleHeader(elt.up());
-                } else if (elt.hasClassName('searchuiDelete')) {
-                    this.deleteCriteria(elt.up('DIV.searchCriteriaId'));
+                if (elt.hasClassName('searchuiDelete')) {
+                    if (elt.up('#search_criteria')) {
+                        this.deleteCriteria(elt.up('DIV.searchId'));
+                    } else {
+                        this.deleteFolder(elt.up('DIV.searchId'));
+                    }
                     e.stop();
                     return;
                 } else if (elt.hasClassName('searchuiCalendar')) {
-                    Horde_Calendar.open(elt.identify(), this.criteria[elt.up('DIV.searchCriteriaId').identify()].v);
+                    Horde_Calendar.open(elt.identify(), this.criteria[elt.up('DIV.searchId').identify()].v);
                     e.stop();
                     return;
                 }
@@ -488,20 +521,70 @@ var ImpSearch = {
         }
     },
 
-    toggleHeader: function(elt)
+    changeHandler: function(e)
     {
-        elt.down().toggle().next().toggle().up().next().toggle();
-        if (elt.readAttribute('id') == 'search_folders_hdr') {
-            elt.down('SPAN.searchuiFoldersActions').toggle();
-            if (window.imp_search && elt.next().visible()) {
-                window.imp_search.stripe();
+        var tmp,
+            elt = e.element(),
+            val = $F(elt);
+
+        switch (elt.readAttribute('id')) {
+        case 'recent_searches':
+            tmp = this.saved_searches.get($F(elt));
+            this.updateCriteria(tmp.c);
+            this.updateFolders(tmp.f);
+            elt.clear();
+            break;
+
+        case 'search_criteria_add':
+            if (val == 'or') {
+                this.insertOr();
+                break;
+            }
+
+            switch (this.data.types[val]) {
+            case 'header':
+            case 'text':
+                this.insertText(val);
+                break;
+
+            case 'customhdr':
+                this.insertCustomHdr();
+                break;
+
+            case 'size':
+                this.insertSize(val);
+                break;
+
+            case 'date':
+                this.insertDate(val);
+                break;
+
+            case 'within':
+                this.insertWithin(val);
+                break;
+
+            case 'filter':
+                this.insertFilter(val);
+                break;
+
+            case 'flag':
+                this.insertFlag(val);
+                break;
             }
+            break;
+
+        case 'search_folders_add':
+            this.insertFolder(unescape($F('search_folders_add')));
+            break;
         }
+
+        e.stop();
     },
 
+
     calendarSelectHandler: function(e)
     {
-        var id = e.findElement('DIV.searchCriteriaId').identify();
+        var id = e.findElement('DIV.searchId').identify();
         this.replaceDate(id, this.criteria[id].t, e.memo);
     },
 
@@ -515,20 +598,20 @@ var ImpSearch = {
         this.data.constants.date = $H(this.data.constants.date);
         this.data.constants.within = $H(this.data.constants.within);
 
-        if (this.recent) {
-            this.updateRecentSearches(this.recent);
-            this.recent = null;
-        }
-
-        if (this.selected) {
-            this.updateSelectedFolders(this.selected);
-            this.selected = null;
+        if (this.i_recent) {
+            this.updateRecentSearches(this.i_recent);
+            this.i_recent = null;
         }
 
         if (this.i_criteria) {
-            this.updateSearchCriteria(this.i_criteria);
+            this.updateCriteria(this.i_criteria);
             this.i_criteria = null;
         }
+
+        if (this.i_folders) {
+            this.updateFolders(this.i_folders);
+            this.i_folders = null;
+        }
     }
 
 };
index 2be5e56..51a0a2c 100644 (file)
@@ -1464,6 +1464,10 @@ class IMP_Imap_Tree implements ArrayAccess, Iterator, Serializable
      *            DEFAULT: null (add to base level)
      * 'poll_info' - (boolean) Include poll information?
      *               DEFAULT: false
+     * 'render_params' - (array) List of params to pass to renderer if
+     *                   auto-creating.
+     *                   DEFAULT: 'alternate', 'lines', 'lines_base', and
+     *                            'nosession' are passed in with true values
      * 'render_type' - (string) The renderer name.
      *                 DEFAULT: Javascript
      * </pre>
@@ -1473,6 +1477,8 @@ class IMP_Imap_Tree implements ArrayAccess, Iterator, Serializable
     public function createTree($name, array $opts = array())
     {
         $opts = array_merge(array(
+            'parent' => null,
+            'render_params' => array(),
             'render_type' => 'Javascript'
         ), $opts);
 
@@ -1483,12 +1489,12 @@ class IMP_Imap_Tree implements ArrayAccess, Iterator, Serializable
             $tree = $name;
             $parent = $opts['parent'];
         } else {
-            $tree = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Tree')->create($name, $opts['render_type'], array(
+            $tree = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Tree')->create($name, $opts['render_type'], array_merge(array(
                 'alternate' => true,
                 'lines' => true,
                 'lines_base' => true,
                 'nosession' => true
-            ));
+            ), $opts['render_params']));
             $parent = null;
         }
 
index faf1bad..474614f 100644 (file)
@@ -91,7 +91,8 @@ class IMP_Search_Query implements Serializable
      *         DEFAULT: Search Results
      * mboxes - (array) The list of mailboxes to search.
      *          DEFAULT: None
-     * subfolders - (array) The list of mailboxes to do subfolder searces for.
+     * subfolders - (array) The list of mailboxes to do subfolder searches
+     *              for.
      *              DEFAULT: None
      * </pre>
      */
@@ -121,6 +122,8 @@ class IMP_Search_Query implements Serializable
                 $this->_mboxes[] = self::SUBFOLDER . $val;
             }
         }
+
+        natsort($this->_mboxes);
     }
 
     /**
@@ -133,11 +136,15 @@ class IMP_Search_Query implements Serializable
      * 'label' - (string) The query label.
      * 'mboxes' - (array) The list of mailboxes to query. This list
      *            automatically expands subfolder searches.
+     * 'mbox_list' - (array) The list of individual mailboxes to query (no
+     *               subfolder mailboxes).
      * 'mid' - (string) The query ID with the search mailbox prefix.
      * 'query' - (array) The list of IMAP queries that comprise this search.
      *           Keys are mailbox names, values are
      *           Horde_Imap_Client_Search_Query objects.
      * 'querytext' - (string) The textual representation of the query.
+     * 'subfolder_list' - (array) The list of mailboxes to do subfolder
+     *                    queries for. The subfolders are not expanded.
      * </pre>
      *
      * @return mixed  Property value.
index 1d781c9..02126b7 100644 (file)
@@ -11,7 +11,8 @@
  * 'edit_query_filter' - (string) The name of the filter being edited.
  * 'edit_query_vfolder' - (string) The name of the virtual folder being
  *                        edited.
- * 'folder_list' - (array) The list of folders to add to the query.
+ * 'folders_form' - (string) JSON representation of the list of mailboxes for
+ *                  the query. Hash containing 2 keys: mbox & subfolder.
  * 'search_label' - (string) The label to use when saving the search.
  * 'search_mailbox' - (string) Use this mailbox as the default value.
  *                    DEFAULT: INBOX
@@ -286,10 +287,12 @@ if ($vars->criteria_form) {
         break;
 
     case 'vfolder':
+        $folders_form = Horde_Serialize::unserialize($vars->folders_form, Horde_Serialize::JSON);
         $q_ob = $imp_search->createQuery($c_list, array(
             'id' => IMP::formMbox($vars->edit_query_vfolder, false),
             'label' => $vars->search_label,
-            'mboxes' => Horde_Serialize::unserialize($vars->folders_form, Horde_Serialize::JSON),
+            'mboxes' => $folders_form->mbox,
+            'subfolders' => $folders_form->subfolder,
             'type' => IMP_Search::CREATE_VFOLDER
         ));
 
@@ -303,8 +306,10 @@ if ($vars->criteria_form) {
         break;
 
     default:
+        $folders_form = Horde_Serialize::unserialize($vars->folders_form, Horde_Serialize::JSON);
         $q_ob = $imp_search->createQuery($c_list, array(
-            'mboxes' => Horde_Serialize::unserialize($vars->folders_form, Horde_Serialize::JSON)
+            'mboxes' => $folders_form->mbox,
+            'subfolders' => $folders_form->subfolder
         ));
         $redirect_target = 'mailbox';
         break;
@@ -331,9 +336,6 @@ if ($vars->criteria_form) {
     }
 }
 
-/* Preselect mailboxes. */
-$js_vars['ImpSearch.selected'] = array($search_mailbox);
-
 /* Prepare the search template. */
 $t = $injector->createInstance('Horde_Template');
 $t->setOption('gettext', true);
@@ -365,6 +367,10 @@ if ($vars->edit_query && $imp_search->isSearchMbox($vars->edit_query)) {
     }
 
     $js_vars['ImpSearch.i_criteria'] = $q_ob->criteria;
+    $js_vars['ImpSearch.i_folders'] = array(
+        'm' => $q_ob->mbox_list,
+        's' => $q_ob->subfolder_list
+    );
 } else {
     /* Process list of recent searches. */
     $rs = array();
@@ -372,14 +378,22 @@ if ($vars->edit_query && $imp_search->isSearchMbox($vars->edit_query)) {
     foreach ($imp_search as $val) {
         $rs[$val->id] = array(
             'c' => $val->criteria,
-            'l' => Horde_String::truncate($val->querytext),
-            'v' => $val->id
+            'f' => array(
+                'm' => $val->mbox_list,
+                's' => $val->subfolder_list
+            ),
+            'l' => Horde_String::truncate($val->querytext)
         );
     }
 
     if (!empty($rs)) {
-        $js_vars['ImpSearch.recent'] = $rs;
+        $js_vars['ImpSearch.i_recent'] = $rs;
     }
+
+    $js_vars['ImpSearch.i_folders'] = array(
+        'm' => array($search_mailbox),
+        's' => array()
+    );
 }
 
 /* Create the criteria list. */
@@ -416,16 +430,27 @@ foreach ($flist['set'] as $val) {
 $t->set('flist', $flag_set);
 
 /* Generate master folder list. */
+$folder_list = array();
 if (!$t->get('edit_query_filter')) {
-    $tree = $injector->getInstance('IMP_Imap_Tree')->createTree('imp_search', array(
-        'checkbox' => true,
+    $imap_tree = $injector->getInstance('IMP_Imap_Tree');
+    $imap_tree->setIteratorFilter();
+
+    $tree = $imap_tree->createTree('imp_search', array(
+        'render_params' => array(
+            'abbrev' => 0,
+            'heading' => _("Add search folder:")
+        ),
+        'render_type' => 'IMP_Tree_Flist'
     ));
     $t->set('tree', $tree->getTree());
+
+    foreach ($imap_tree as $val) {
+        $folder_list[$val->value] = $val->display;
+    }
 }
 
 Horde_Core_Ui_JsCalendar::init();
 Horde::addScriptFile('horde.js', 'horde');
-Horde::addScriptFile('stripe.js', 'horde');
 Horde::addScriptFile('search.js', 'imp');
 
 Horde::addInlineJsVars(array_merge($js_vars, array(
@@ -433,6 +458,7 @@ Horde::addInlineJsVars(array_merge($js_vars, array(
     'ImpSearch.data' => array(
         'constants' => $constants,
         'dimp' => $dimp_view,
+        'folder_list' => $folder_list,
         'months' => Horde_Core_Ui_JsCalendar::months(),
         'searchmbox' => $search_mailbox,
         'types' => $types
@@ -449,7 +475,8 @@ Horde::addInlineJsVars(array_merge($js_vars, array(
         'need_label' => _("Saved searches require a label."),
         'not_match' => _("Do NOT Match"),
         'or' => _("OR"),
-        'search_term' => _("Search Term:")
+        'search_term' => _("Search Term:"),
+        'subfolder_search' => _("Search all subfolders?")
     )
 )), array('onload' => 'dom'));
 
index 44a8cef..923ec80 100644 (file)
@@ -1,5 +1,6 @@
 <form id="search_form" action="<tag:action />" method="post">
  <input class="hidden" name="criteria_form" id="criteria_form" value="" />
+ <input class="hidden" name="folders_form" id="folders_form" value="" />
 
  <h1 class="header">
   <strong>
@@ -16,8 +17,6 @@
  </h1>
 
  <div id="recent_searches_div" class="smallheader leftAlign" style="display:none">
-  <span class="iconImg searchuiImg arrowExpanded" style="display:none"></span>
-  <span class="iconImg searchuiImg arrowCollapsed"></span>
   <gettext>Recent Searches</gettext>
  </div>
 
  </div>
 
  <div class="smallheader leftAlign">
-  <span class="iconImg searchuiImg arrowExpanded"></span>
-  <span class="iconImg searchuiImg arrowCollapsed" style="display:none"></span>
- <gettext>Search Criteria</gettext>
+  <gettext>Search Criteria</gettext>
  </div>
 
- <div>
-  <div id="search_criteria"></div>
+ <div class="item">
+  <div id="no_search_criteria"><gettext>No Search Criteria</gettext></div>
+  <div id="search_criteria" style="display:none"></div>
 
-  <div class="searchCriteriaElement">
+  <div class="searchAdd">
    <select id="search_criteria_add">
     <option value=""><gettext>Add search criteria:</gettext></option>
     <option value="" disabled="disabled">- - - - - - - - -</option>
  </div>
 
 <if:edit_query_filter><else:edit_query_filter>
- <div class="smallheader leftAlign" id="search_folders_hdr">
-  <span class="iconImg searchuiImg arrowExpanded" style="display:none"></span>
-  <span class="iconImg searchuiImg arrowCollapsed"></span>
+ <div class="smallheader leftAlign">
   <gettext>Search Folders</gettext>
-  <span class="searchuiFoldersActions" style="display:none">
-   [ <a id="link_sel_all" href="#"><gettext>Select all</gettext></a> |
-   <a id="link_sel_none" href="#"><gettext>Select none</gettext></a> ]
-  </span>
  </div>
 
- <div class="item" id="search_folders_tree" style="display:none">
-  <tag:tree />
+ <div class="item">
+  <div id="no_search_folders"><gettext>No Search Folders</gettext></div>
+  <div id="search_folders" style="display:none"></div>
+
+  <div class="searchAdd">
+   <select id="search_folders_add">
+    <tag:tree />
+   </select>
+  </div>
  </div>
 </else:edit_query_filter></if:edit_query_filter>
 
 <if:virtualfolder>
  <div class="smallheader leftAlign">
-  <span class="iconImg searchuiImg arrowExpanded" style="display:none"></span>
-  <span class="iconImg searchuiImg arrowCollapsed"></span>
   <gettext>Saved Searches</gettext>
  </div>
 
- <div style="display:none">
+ <div>
 <if:edit_query_vfolder>
   <input type="hidden" id="search_type" name="search_type" value="vfolder" />
   <input type="hidden" name="edit_query_vfolder" value="<tag:edit_query_vfolder />" />
  </div>
 </form>
 
-<div id="folder_row" style="display:none">
- <input type="checkbox" class="checkbox" name="search_folders_form[]" />
-</div>
-
 <select id="within_criteria" style="display:none">
  <option value="d"><gettext>Days</gettext></option>
  <option value="m"><gettext>Months</gettext></option>
index 1a5ef35..15aa62c 100644 (file)
@@ -5,8 +5,8 @@
 /* Fixes broken inline-block. */
 .mimePartInfo div,
 .mimeStatusMessage,
-.searchCriteriaElement,
-#search_criteria em.joinOr {
+.searchElement,
+#search_form em.joinOr {
     zoom: 1;
     *display: inline;
 }
index 5e91746..11f19c3 100644 (file)
@@ -5,8 +5,8 @@
 /* Fixes broken inline-block. */
 .mimePartInfo div,
 .mimeStatusMessage,
-.searchCriteriaElement,
-#search_criteria em.joinOr {
+.searchElement,
+#search_form em.joinOr {
     zoom: 1;
     *display: inline;
 }
index 2e1e02a..3e8da7f 100644 (file)
@@ -199,51 +199,65 @@ div.msgActions, #fmanager div.folderActions {
 #search_form div.item {
     padding: 1px 0 1px 0;
 }
+#search_form em.join {
+    font-weight: bold;
+    padding-left: 2px;
+    padding-right: 2px;
+}
+#search_form em.joinOr {
+    display: inline-block;
+    padding: 5px;
+}
 
-.searchCriteriaElement {
+.searchAdd, .searchElement {
     display: -moz-inline-stack;
     display: inline-block;
     padding: 3px;
 }
-#search_criteria .searchCriteriaElement {
+
+#no_search_criteria, #no_search_folders {
+    font-weight: bold;
+    padding: 3px;
+}
+
+.searchElement {
     background-color: lightblue;
     border: 1px solid gray;
     margin: 2px;
     -moz-border-radius: 4px;
     -webkit-border-radius: 4px;
 }
-#search_criteria em.join {
+.searchElement em {
     font-style: normal;
-    font-weight: bold;
-    padding-left: 2px;
-    padding-right: 2px;
-}
-#search_criteria em.joinOr {
-    display: inline-block;
-    padding: 5px;
 }
-#search_criteria span.notMatch {
+.searchElement span.notMatch {
     padding-left: 3px;
     padding-right: 3px;
 }
-#search_criteria input.checkbox {
-    margin-right: 2px;
+.searchElement span.subfolders {
+    padding-left: 10px;
 }
-#search_criteria .searchuiDelete {
-    padding-left: 6px;
-    vertical-align: middle;
+.searchElement input.checkbox {
+    margin-right: 3px;
+    vertical-align: text-top;
 }
-
-#search_folders_tree input.checkbox {
-    vertical-align: middle;
+.searchElement .calendarPopup {
+    margin-left: 3px;
+}
+.searchElement .searchuiDelete {
+    padding-left: 6px;
+    vertical-align: text-top;
 }
 
 .searchuiCalendar {
     background-image: url("graphics/calendar.png");
 }
 .searchuiFoldersActions {
-    margin-left: 20px;
     font-size: 90%;
+    margin-left: 10px;
+}
+.searchuiFoldersActions a {
+    color: #366;
 }
 .searchuiButtons {
     padding-top: 5px;