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