1(function ($) {
2    "use strict";
3
4    var Filelisting = function(element, options) {
5        this.$capiton = $(element).find('.plugin__filelisting_capiton');
6        this.$collapsible = $(element).find('.plugin__filelisting_collapsible');
7        this.$content = $(element).find('.plugin__filelisting_content');
8
9        this.$headertable = $(element).find('.plugin__filelisting_headertable');
10        this.$bodytable = $(element).find('.plugin__filelisting_bodytable');
11
12        this.$footer = $(element).find('.plugin__filelisting_footer');
13
14        this.options = $.extend({}, $.fn.dokuwiki_plugin_filelisting.defaults, options);
15
16        this.storageKey = 'plugin_filelisting';
17        if (this.options.remember_state_per_page) {
18            this.storageKey += '/' + this.options.pageId;
19        }
20
21        this.initToggleButton();
22        this.initAjaxDirectoryExpand();
23        this.initFilter();
24        this.initSorting();
25        this.initDelete();
26    };
27
28    Filelisting.prototype.getToggleStatus = function () {
29        if (!localStorage.getItem(this.storageKey)) {
30            return this.options.defaultToggle;
31        }
32        return localStorage.getItem(this.storageKey);
33    };
34
35    Filelisting.prototype.setToggleStatus = function (status) {
36        if (status !== 'visible' && status !== 'hidden') {
37            throw 'status must be "visible" or "hidden"';
38        }
39        localStorage.setItem(this.storageKey, status);
40    };
41
42    Filelisting.prototype.initToggleButton = function() {
43        //toggle button
44        var $toggleButton = $('<div>').text(this.options.toggleVisible)
45            .css({
46                float: 'right',
47                cursor: 'pointer'
48            }).addClass('plugin__filelisting_toggle').appendTo(this.$capiton);
49
50        //by default filelisting is visible
51        if (this.getToggleStatus() === 'hidden') {
52            this.$collapsible.hide();
53            $toggleButton.text(this.options.toggleHidden);
54        }
55
56        $toggleButton.click($.proxy(function () {
57            if (this.$collapsible.is(':hidden')) {
58                this.$collapsible.slideDown();
59                $toggleButton.text(this.options.toggleVisible);
60                this.setToggleStatus('visible');
61            } else {
62                this.$collapsible.slideUp();
63                $toggleButton.text(this.options.toggleHidden);
64                this.setToggleStatus('hidden');
65            }
66        }, this));
67    };
68
69    Filelisting.prototype.initAjaxDirectoryExpand = function() {
70        //allow click on link
71        this.$content.find('tbody').on('click', 'tr[data-namespace] a', $.proxy(function (event) {
72            event.preventDefault();
73
74            //row and namespace are used in $.post
75            var $row = $(event.target).closest('tr'),
76                namespace = $row.data('namespace');
77
78            //get all siblings and subsiblings
79            var $children = $row.nextAll('[data-childOf="' + namespace + '"]'),
80                $descendants = $row.nextAll('[data-childOf^="' + namespace + '"]');
81
82            //namespace is expanded - hide it
83            if ($row.data('isExpanded')) {
84                //set icon
85                $row.children('.plugin__filelisting_cell_icon').html(this.options.dirClosedIcon);
86                //save the state of all expanded sub namespaces to restore it as it was
87                $descendants.each(function () {
88                     if ($(this).is(':visible')) {
89                         $(this).data('reopenAs', 'visible');
90                     } else {
91                         $(this).data('reopenAs', 'hidden');
92                     }
93                 }).hide();
94                $row.data('isExpanded', false);
95
96            //namespace is hidden and is loaded
97            } else if ($row.data('isLoaded')) {
98                $row.children('.plugin__filelisting_cell_icon').html(this.options.dirOpenedIcon);
99                //always open children
100                $children.show();
101                //check if we should open any descendents
102                $descendants.each(function() {
103                    if ($(this).data('reopenAs') === 'visible') {
104                        $(this).show();
105                    }
106                });
107
108                $row.data('isExpanded', true);
109                this.$content.trigger('expand', namespace);
110
111            //namespace isn't loaded
112            } else {
113                //loading
114                $row.children('.plugin__filelisting_cell_icon').html(this.options.loadingIcon);
115
116                var data = {};
117
118                data['call'] = 'plugin_filelisting';
119                data['namespace'] = namespace;
120                data['baseNamespace'] = this.options.baseNamespace;
121
122                $.post(DOKU_BASE + 'lib/exe/ajax.php', data,
123                    $.proxy(function(html) {
124                        $row.children('.plugin__filelisting_cell_icon').html(this.options.dirOpenedIcon);
125                        $row.after(html);
126
127                        $row.data('isLoaded', true);
128                        this.$content.trigger('nsload', namespace);
129
130                        $row.data('isExpanded', true);
131                        this.$content.trigger('expand', namespace);
132
133                    }, this), 'html')
134                    .fail($.proxy(function () {
135                        $row.children('.plugin__filelisting_cell_icon').html(this.options.dirClosedIcon);
136                    }, this));
137            }
138        }, this));
139
140        this.$content.on('namespaceFilesChanged', $.proxy(function (event, namespace) {
141            var $row = $('tr[data-namespace="'+namespace+'"]');
142            if ($row.length && !$row.data('isLoaded')) {
143                return;
144            }
145
146            var data = {};
147
148            data['call'] = 'plugin_filelisting';
149            data['namespace'] = namespace;
150            data['baseNamespace'] = this.options.baseNamespace;
151            data['filesOnly'] = true;
152
153            $.post(DOKU_BASE + 'lib/exe/ajax.php', data,
154                $.proxy(function(html) {
155                    var fileRows = $(html);
156                    if ($row.length && !$row.data('isExpanded')) {
157                        fileRows.hide();
158                    }
159                    var $filesInNamespace = $('tr[data-childOf="'+namespace+'"]').not('[data-namespace]');
160                    if ($filesInNamespace.length) {
161                        // there are already files in the namespace: replace them
162                        $filesInNamespace.first().replaceWith(fileRows);
163                        $filesInNamespace.remove();
164                    } else if ($('tr[data-childOf="'+namespace+'"]').length) {
165                        // there are no files, but other folders in the namespace: insert after them
166                        $('tr[data-childOf="'+namespace+'"]').last().after(fileRows);
167                    } else {
168                        // tbody is currently empty: append to it
169                        this.$content.find('tbody').append(fileRows);
170                    }
171
172                    this.$content.trigger('nsload', namespace);
173                }, this), 'html');
174        }, this))
175    };
176
177    Filelisting.prototype.initFilter = function() {
178        this.$filter = $('<label>' + this.options.filterLabel + ': <input></label>').appendTo(this.$footer);
179        var $input = this.$filter.find('input');
180
181        //filter has changed, update content
182        $input.on('keyup', $.proxy(this.applyFilter, this));
183
184        //prevent deleting files on pressing enter
185        $input.on('keydown', function (event) {
186            if(event.keyCode === 13) {
187                event.preventDefault();
188                return false;
189            }
190        });
191
192        //bind filtering to content update event
193        this.$content.on('expand', $.proxy(this.applyFilter, this));
194    };
195
196    Filelisting.prototype.applyFilter = function() {
197        var filter = this.$filter.find('input').val(),
198            //escape regex
199            //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_Special_Characters
200            escaped = filter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), // $& means the whole matched string
201            globbing = escaped.replace(/\\\*/g, '.*').replace(/\\\?/g, '.'),
202            regex = new RegExp(globbing),
203            $rows = this.$content.find('tbody tr'),
204            $files = $rows.not('[data-namespace]'),
205            $dirs = $rows.not($files),
206            filterCallback = function() {
207                //text in second column
208                var $row = $(this),
209                    text = $row.find('td.plugin__filelisting_cell_name a').text();
210                if (text.match(regex)) {
211                    $row.show();
212                } else {
213                    $row.hide();
214                }
215            };
216
217        //files in base namespace are always visible
218        $files.filter('[data-childOf="' + this.options.baseNamespace + '"]').each(filterCallback);
219
220        //get namespaces
221        $dirs.filter(function() {
222            //only expanded
223            return $(this).data('isExpanded');
224        }).each(function () {
225            var namespace = $(this).data('namespace');
226            $files.filter('[data-childOf="' + namespace + '"]').each(filterCallback);
227        });
228    };
229
230    Filelisting.prototype.initSorting = function() {
231        //global: current sort header
232        //not defined by default
233        this.$sortHeader = [];
234
235        //create sort links (for styling purposes)
236        this.$content.find('thead th').wrapInner('<a href="#">');
237        //sorting indicator
238        this.$content.find('thead th a').prepend('<span>');
239
240        //options for click
241        this.$content.find('thead th a').on('click', $.proxy(function(event) {
242
243            //prevent from scrolling to top
244            event.preventDefault();
245
246            this.$sortHeader = $(event.target).closest('th');
247
248            var $order = this.$sortHeader.find('span');
249            //clear other sorting indicators
250            this.$content.find('thead th').not(this.$sortHeader).find('span').text('');
251
252            //toggle sort ordering
253            if ($order.text() === '' || $order.text() === this.options.sortDesc) {
254                $order.text(this.options.sortAsc);
255            } else {
256                $order.text(this.options.sortDesc);
257            }
258            //perform sorting
259            this.sortBy();
260        }, this));
261
262        //bind sorting to content update event
263        this.$content.on('nsload', $.proxy(function(event, namespace) {
264            this.sortBy(namespace);
265        }, this));
266    };
267
268    Filelisting.prototype.sortBy = function(namespace) {
269        //don't sort where sortHeader not defined
270        if (this.$sortHeader.length === 0) return;
271        //by default sort starts from the base namespace
272        if (namespace === undefined) {
273            namespace = this.options.baseNamespace;
274        }
275
276        var $root = this.$content.find('tbody tr[data-namespace="' + namespace + '"]'),
277            $rows = this.$content.find('tbody tr[data-childOf="' + namespace + '"]'),
278            $files = $rows.not('[data-namespace]'),
279            $dirs = $rows.not($files),
280            sortCallback = $.proxy(function (a, b) {
281                //remember about first th colspan
282                var colspan = this.$headertable.find('th').first().attr('colspan'),
283                    index = this.$sortHeader.index() + (colspan - 1),
284                    order = 1; //1 ascending order, -1 descending order
285
286                //check for desc sorting
287                if (this.$sortHeader.find('span').text() === this.options.sortDesc) {
288                    order = -1;
289                }
290
291                var dataA = $(a).find('td').eq(index).data('sort'),
292                    dataB = $(b).find('td').eq(index).data('sort');
293                //$.data automatically converts string to integer when possible
294                if (dataA < dataB) {
295                    return -order;
296                } else if (dataA > dataB) {
297                    return order;
298                }
299                return 0;
300            }, this);
301
302        //sort dirs
303        $dirs.sort(sortCallback);
304        //sort files
305        $files.sort(sortCallback);
306
307        //we are on top level
308        if ($root.length === 0) {
309            this.$content.find('tbody').append($dirs, $files);
310        } else {
311            $root.after($dirs, $files);
312        }
313
314        //attach files to corresponding dirs
315        $dirs.each($.proxy(function(index, element) {
316            var namespace = $(element).data('namespace'),
317                $descendants = $(element).siblings('[data-childOf^="' + namespace + '"]');
318            $descendants.insertAfter(element);
319
320            //sort sub namespaces
321            this.sortBy(namespace);
322        }, this));
323    };
324
325    Filelisting.prototype.initDelete = function() {
326        var $deleteButton = this.$collapsible.find('button[name="do[plugin_filelisting_delete]"]');
327
328        $deleteButton.on('click', $.proxy(function (event) {
329            var deleteFiles = window.confirm(this.options.deleteConfirm);
330            if (!deleteFiles) {
331                event.preventDefault();
332            }
333        }, this));
334
335        this.toggleDeleteButton();
336        //show/hide delete button
337        this.$content.on('change', 'input[type=checkbox]', $.proxy(this.toggleDeleteButton, this));
338    };
339
340    Filelisting.prototype.toggleDeleteButton = function() {
341        var $deleteButton = this.$collapsible.find('button[name="do[plugin_filelisting_delete]"]');
342
343        if (this.$content.find('input[type=checkbox]:checked').length === 0) {
344            $deleteButton.hide();
345        } else {
346            $deleteButton.show();
347        }
348    };
349
350
351    $.fn.dokuwiki_plugin_filelisting = function (options) {
352        //return jquery object
353        return this.each(function() {
354            new Filelisting(this, options);
355        });
356    };
357
358    $.fn.dokuwiki_plugin_filelisting.defaults = {
359        //label for visible list
360        toggleVisible: '▼',
361        //label for hidden list
362        toggleHidden: '▲',
363        //id of the current wiki page
364        pageId: '',
365        defaultToggle: 'visible',
366        //html used as dir open icon
367        dirOpenedIcon: '',
368        //html used as dir close icon
369        dirClosedIcon: '',
370        //html used as loading icon for ajax call
371        loadingIcon: '',
372        //namespace of the current wiki page
373        baseNamespace: '',
374        //label of filter input
375        filterLabel: 'Filter',
376        //sort ascending label
377        sortAsc: '↓',
378        //sort descending label
379        sortDesc: '↑',
380        //confirm file deletion
381        deleteConfirm: LANG.plugins.filelisting.delete_confirm
382    };
383
384}(window.jQuery));
385
386jQuery(function() {
387
388    //read JSINFO and LANG
389    if (JSINFO === undefined || LANG === undefined) {
390        console.log('filelisting: JSINFO or LANG undefined');
391        return;
392    }
393    var options = {};
394
395    options.pageId = JSINFO.id;
396
397    var defaulttoggle = JSINFO.plugin.filelisting.defaulttoggle;
398    if (defaulttoggle === '1') {
399        options.defaultToggle = 'visible';
400    } else {
401        options.defaultToggle = 'hidden';
402    }
403    options.remember_state_per_page = JSINFO.plugin.filelisting.remember_state_per_page;
404    options.dirOpenedIcon = JSINFO.plugin.filelisting.dirOpenedIcon;
405    options.dirClosedIcon = JSINFO.plugin.filelisting.dirClosedIcon;
406    options.loadingIcon = JSINFO.plugin.filelisting.loadingIcon;
407
408    var $plugin__filelisting = jQuery('.plugin__filelisting');
409    // if base namespace is not properly set, sorting and filtering wont work
410    var ns = $plugin__filelisting.data("namespace");
411    if (ns !== undefined) {
412        options.baseNamespace = ns;
413    } else {
414        options.baseNamespace = JSINFO.namespace;
415    }
416
417    options.filterLabel = LANG.plugins.filelisting.filter_label;
418    options.deleteConfirm = LANG.plugins.filelisting.delete_confirm;
419
420    $plugin__filelisting.dokuwiki_plugin_filelisting(options);
421
422    /**
423     * Fixes the tablewidths so that the table columns in header and body are exactly aligned
424     *
425     * This is necessary, because browsers have different widths for scrollbars
426     */
427    $plugin__filelisting.each(function adjustTableWidthForScrollbar(index, container) {
428        var $bodyTable = jQuery(container).find('.plugin__filelisting_bodytable table');
429        var $headerWrapper = jQuery(container).find('.plugin__filelisting_headertable');
430        var tablediff = $bodyTable.width() - $headerWrapper.find('table').width();
431        var originalPaddingRight = parseInt($headerWrapper.css('padding-right'), 10);
432        var newPaddingRight = originalPaddingRight - tablediff;
433        $headerWrapper.css('padding-right', newPaddingRight + 'px');
434    });
435});
436