Textpattern | PHP Cross Reference | Content Management Systems |
Description: Collection of client-side tools.
1 /* eslint-env jquery */ 2 3 /* 4 * Textpattern Content Management System 5 * https://textpattern.com/ 6 * 7 * Copyright (C) 2020 The Textpattern Development Team 8 * 9 * This file is part of Textpattern. 10 * 11 * Textpattern is free software; you can redistribute it and/or 12 * modify it under the terms of the GNU General Public License 13 * as published by the Free Software Foundation, version 2. 14 * 15 * Textpattern is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 * GNU General Public License for more details. 19 * 20 * You should have received a copy of the GNU General Public License 21 * along with Textpattern. If not, see <https://www.gnu.org/licenses/>. 22 */ 23 24 'use strict'; 25 26 /** 27 * Collection of client-side tools. 28 */ 29 30 textpattern.version = '4.8.4'; 31 32 /** 33 * Ascertain the page direction (LTR or RTL) as a variable. 34 */ 35 36 var langdir = document.documentElement.dir, 37 dir = langdir === 'rtl' ? 'left' : 'right'; 38 39 /** 40 * Checks if HTTP cookies are enabled. 41 * 42 * @return {boolean} 43 */ 44 45 function checkCookies() 46 { 47 cookieEnabled = navigator.cookieEnabled && (document.cookie.indexOf('txp_test_cookie') >= 0 || document.cookie.indexOf('txp_login') >= 0); 48 49 if (!cookieEnabled) { 50 textpattern.Console.addMessage([textpattern.gTxt('cookies_must_be_enabled'), 1]); 51 } else { 52 document.cookie = 'txp_test_cookie=; Max-Age=0; SameSite=Lax'; 53 } 54 } 55 56 /** 57 * Basic confirmation for potentially powerful choices (like deletion, 58 * for example). 59 * 60 * @param {string} msg The message 61 * @return {boolean} TRUE if user confirmed the action 62 */ 63 64 function verify(msg) 65 { 66 return confirm(msg); 67 } 68 69 /** 70 * Multi-edit functions. 71 * 72 * @param {string|object} method Called method, or options 73 * @param {object} opt Options if method is a method 74 * @return {object} this 75 * @since 4.5.0 76 */ 77 78 jQuery.fn.txpMultiEditForm = function (method, opt) { 79 var args = {}; 80 81 var defaults = { 82 'checkbox' : 'input[name="selected[]"][type=checkbox]', 83 'row' : 'tbody td', 84 'highlighted' : 'tr', 85 'filteredClass': 'filtered', 86 'selectedClass': 'selected', 87 'actions' : 'select[name=edit_method]', 88 'submitButton' : '.multi-edit input[type=submit]', 89 'selectAll' : 'input[name=select_all][type=checkbox]', 90 'rowClick' : true, 91 'altClick' : true, 92 'confirmation' : textpattern.gTxt('are_you_sure') 93 }; 94 95 if ($.type(method) !== 'string') { 96 opt = method; 97 method = null; 98 } else { 99 args = opt; 100 } 101 102 this./*closest('form').*/each(function () { 103 var $this = $(this), form = {}, methods = {}, lib = {}; 104 105 if ($this.data('_txpMultiEdit')) { 106 form = $this.data('_txpMultiEdit'); 107 opt = $.extend(form.opt, opt); 108 } else { 109 opt = $.extend(defaults, opt); 110 form.editMethod = $this.find(opt.actions); 111 form.lastCheck = null; 112 form.opt = opt; 113 form.selectAll = $this.find(opt.selectAll); 114 form.button = $this.find(opt.submitButton); 115 } 116 117 form.boxes = $this.find(opt.checkbox); 118 119 /** 120 * Registers a multi-edit option. 121 * 122 * @param {object} options 123 * @param {string} options.label The option's label 124 * @param {string} options.value The option's value 125 * @param {string} options.html The second step HTML 126 * @return {object} methods 127 */ 128 129 methods.addOption = function (options) { 130 var settings = $.extend({ 131 'label': null, 132 'value': null, 133 'html' : null 134 }, options); 135 136 if (!settings.value) { 137 return methods; 138 } 139 140 var option = form.editMethod.find('option').filter(function () { 141 return $(this).val() === settings.value; 142 }); 143 144 var exists = (option.length > 0); 145 form.editMethod.val(''); 146 147 if (!exists) { 148 option = $('<option />'); 149 } 150 151 if (!option.data('_txpMultiMethod')) { 152 if (!option.val()) { 153 option.val(settings.value); 154 } 155 156 if (!option.text() && settings.label) { 157 option.text(settings.label); 158 } 159 160 option.data('_txpMultiMethod', settings.html); 161 } 162 163 if (!exists) { 164 form.editMethod.append(option); 165 } 166 167 return methods; 168 }; 169 170 /** 171 * Selects rows based on supplied arguments. 172 * 173 * Only one of the filters applies at a time. 174 * 175 * @param {object} options 176 * @param {array} options.index Indexes to select 177 * @param {array} options.range Select index range, takes [min, max] 178 * @param {array} options.value Values to select 179 * @param {boolean} options.checked TRUE to check, FALSE to uncheck 180 * @return {object} methods 181 */ 182 183 methods.select = function (options) { 184 var settings = $.extend({ 185 'index' : null, 186 'range' : null, 187 'value' : null, 188 'checked': true 189 }, options); 190 191 var obj = form.boxes;//$this.find(opt.checkbox); 192 193 if (settings.value !== null) { 194 obj = obj.filter(function () { 195 return $.inArray($(this).val(), settings.value) !== -1; 196 }); 197 } else if (settings.index !== null) { 198 obj = obj.filter(function (index) { 199 return $.inArray(index, settings.index) !== -1; 200 }); 201 } else if (settings.range !== null) { 202 obj = obj.slice(settings.range[0], settings.range[1]); 203 } 204 205 obj.filter(settings.checked ? ':not(:checked)' : ':checked').prop('checked', settings.checked).change(); 206 lib.highlight(); 207 208 return methods; 209 }; 210 211 /** 212 * Highlights selected rows. 213 * 214 * @return {object} lib 215 */ 216 217 lib.highlight = function () { 218 var checked = form.boxes.filter(':checked'), count = checked.length, 219 option = form.editMethod.find('[value=""]'); 220 checked.closest(opt.highlighted).addClass(opt.selectedClass); 221 form.boxes.filter(':not(:checked)').closest(opt.highlighted).removeClass(opt.selectedClass); 222 223 option.gTxt('with_selected_option', { 224 '{count}': count 225 }); 226 form.selectAll.prop('checked', count === form.boxes.length).change(); 227 form.editMethod.prop('disabled', !count); 228 229 if (!count) { 230 form.editMethod.val('').change(); 231 } 232 233 return lib; 234 }; 235 236 /** 237 * Extends click region to whole row. 238 * 239 * @return {object} lib 240 */ 241 242 lib.extendedClick = function () { 243 var selector = opt.rowClick ? opt.row : opt.checkbox; 244 245 $this.on('click', selector, function (e) { 246 var self = ($(e.target).is(opt.checkbox) || $(this).is(opt.checkbox)); 247 248 if (!self && (e.target != this || $(this).is('a, :input') || $(e.target).is('a, :input'))) { 249 return; 250 } 251 252 if (!self && opt.altClick && !e.altKey && !e.ctrlKey) { 253 return; 254 } 255 256 var box = $(this).closest(opt.highlighted).find(opt.checkbox); 257 258 if (box.length < 1) { 259 return; 260 } 261 262 var checked = box.prop('checked'); 263 264 if (self) { 265 checked = !checked; 266 } 267 268 if (e.shiftKey && form.lastCheck) { 269 var boxes = form.boxes; 270 var start = boxes.index(box); 271 var end = boxes.index(form.lastCheck); 272 273 methods.select({ 274 'range' : [Math.min(start, end), Math.max(start, end) + 1], 275 'checked': !checked 276 }); 277 } else if (!self) { 278 box.prop('checked', !checked).change(); 279 } 280 281 form.lastCheck = box; 282 }); 283 284 return lib; 285 }; 286 287 /** 288 * Tracks row checks. 289 * 290 * @return {object} lib 291 */ 292 293 lib.checked = function () { 294 $this.on('change', opt.checkbox, function (e) { 295 var box = $(this); 296 297 if (box.prop('checked')) { 298 if (-1 == $.inArray(box.val(), textpattern.Relay.data.selected)) { 299 textpattern.Relay.data.selected.push(box.val()); 300 } 301 } else { 302 textpattern.Relay.data.selected = $.grep(textpattern.Relay.data.selected, function(value) { 303 return value != box.val(); 304 }); 305 } 306 307 if (typeof(e.originalEvent) != 'undefined') { 308 lib.highlight(); 309 } 310 }); 311 312 return lib; 313 }; 314 315 /** 316 * Handles edit method selecting. 317 * 318 * @return {object} lib 319 */ 320 321 lib.changeMethod = function () { 322 form.button.hide(); 323 324 form.editMethod.val('').change(function (e) { 325 var selected = $(this).find('option:selected'); 326 $this.find('.multi-step').remove(); 327 328 if (selected.length < 1 || selected.val() === '') { 329 form.button.hide(); 330 return lib; 331 } 332 333 if (selected.data('_txpMultiMethod')) { 334 $(this).after($('<div />').attr('class', 'multi-step multi-option').html(selected.data('_txpMultiMethod'))); 335 form.button.show(); 336 } else { 337 form.button.hide(); 338 $(this).parents('form').submit(); 339 } 340 }); 341 342 return lib; 343 }; 344 345 /** 346 * Handles sending. 347 * 348 * @return {object} lib 349 */ 350 351 lib.sendForm = function () { 352 $this.submit(function () { 353 if (opt.confirmation !== false && verify(opt.confirmation) === false) { 354 form.editMethod.val('').change(); 355 356 return false; 357 } 358 }); 359 360 return lib; 361 }; 362 363 if (!$this.data('_txpMultiEdit')) { 364 lib.highlight().extendedClick().checked().changeMethod().sendForm(); 365 366 (function () { 367 var multiOptions = $this.find('.multi-option:not(.multi-step)'); 368 369 form.editMethod.find('option[value!=""]').each(function () { 370 var value = $(this).val(); 371 372 var option = multiOptions.filter(function () { 373 return $(this).data('multi-option') === value; 374 }); 375 376 if (option.length > 0) { 377 methods.addOption({ 378 'label': null, 379 'html' : option.eq(0).contents(), 380 'value': $(this).val() 381 }); 382 } 383 }); 384 385 multiOptions.remove(); 386 })(); 387 388 form.selectAll.on('change', function (e) { 389 if (typeof(e.originalEvent) != 'undefined') { 390 methods.select({ 391 'checked': $(this).prop('checked') 392 }); 393 } 394 395 $this.toggleClass(opt.filteredClass, !$(this).prop('checked')); 396 }).change(); 397 } 398 399 if (method && methods[method]) { 400 methods[method].call($this, args); 401 } 402 403 $this.data('_txpMultiEdit', form); 404 }); 405 406 return this; 407 }; 408 409 /** 410 * Sets a HTTP cookie. 411 * 412 * @param {string} name The name 413 * @param {string} value The value 414 * @param {integer} days Expires in 415 */ 416 417 function setCookie(name, value, days) 418 { 419 var expires = ''; 420 421 if (days) { 422 var date = new Date(); 423 424 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 425 426 expires = '; expires=' + date.toGMTString(); 427 } 428 429 document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax'; 430 } 431 432 /** 433 * Gets a HTTP cookie's value. 434 * 435 * @param {string} name The name 436 * @return {string} The cookie 437 */ 438 439 function getCookie(name) 440 { 441 var nameEQ = name + '='; 442 var ca = document.cookie.split(';'); 443 444 for (var i = 0; i < ca.length; i++) { 445 var c = ca[i]; 446 447 while (c.charAt(0) == ' ') { 448 c = c.substring(1, c.length); 449 } 450 451 if (c.indexOf(nameEQ) == 0) { 452 return c.substring(nameEQ.length, c.length); 453 } 454 } 455 456 return null; 457 } 458 459 /** 460 * Deletes a HTTP cookie. 461 * 462 * @param {string} name The cookie 463 */ 464 465 function deleteCookie(name) 466 { 467 setCookie(name, '', -1); 468 } 469 470 /** 471 * Toggles column's visibility and saves the state. 472 * 473 * @param {string} sel The column selector object 474 * @return {boolean} Returns FALSE 475 * @since 4.7.0 476 */ 477 478 function toggleColumn(sel, $sel, vis) 479 { 480 // $sel = $(sel); 481 if ($sel.length) { 482 $sel.toggle(!!vis); 483 484 // Send state of toggle pane to localStorage. 485 var data = new Object; 486 487 data[textpattern.event] = {'columns':{}}; 488 data[textpattern.event]['columns'][sel] = !!vis ? null : false; 489 textpattern.storage.update(data); 490 } 491 492 return false; 493 } 494 495 /** 496 * Toggles panel's visibility and saves the state. 497 * 498 * @param {string} id The element ID 499 * @return {boolean} Returns FALSE 500 */ 501 502 function toggleDisplay(id) 503 { 504 var obj = $('#' + id); 505 506 if (obj.length) { 507 obj.toggle(); 508 509 // Send state of toggle pane to localStorage. 510 var pane = $(this).data('txp-pane') || obj.attr('id'); 511 var data = new Object; 512 513 data[textpattern.event] = {'panes':{}}; 514 data[textpattern.event]['panes'][pane] = obj.is(':visible') ? true : null; 515 textpattern.storage.update(data); 516 } 517 518 return false; 519 } 520 521 /** 522 * Direct show/hide referred #segment; decorate parent lever. 523 */ 524 525 function toggleDisplayHref() 526 { 527 var $this = $(this); 528 var href = $this.attr('href'); 529 var lever = $this.parent('.txp-summary'); 530 531 if (href) { 532 toggleDisplay.call(this, href.substr(1)); 533 } 534 535 if (lever.length) { 536 var vis = $(href).is(':visible'); 537 lever.toggleClass('expanded', vis); 538 $this.attr('aria-pressed', vis.toString()); 539 $(href).attr('aria-expanded', vis.toString()); 540 } 541 542 return false; 543 } 544 545 /** 546 * Shows/hides matching elements. 547 * 548 * @param {string} className Targeted element's class 549 * @param {boolean} show TRUE to display 550 */ 551 552 function setClassDisplay(className, show) 553 { 554 $('.' + className).toggle(show); 555 } 556 557 /** 558 * Toggles panel's visibility and saves the state to a HTTP cookie. 559 * 560 * @param {string} classname The HTML class 561 */ 562 563 function toggleClassRemember(className) 564 { 565 var v = getCookie('toggle_' + className); 566 v = (v == 1 ? 0 : 1); 567 568 setCookie('toggle_' + className, v, 365); 569 setClassDisplay(className, v); 570 setClassDisplay(className + '_neg', 1 - v); 571 } 572 573 /** 574 * Toggle visibility of matching elements based on a cookie value. 575 * 576 * @param {string} className The HTML class 577 * @param {string} force The value 578 */ 579 580 function setClassRemember(className, force) 581 { 582 if (typeof(force) != 'undefined') { 583 setCookie('toggle_' + className, force, 365); 584 } 585 586 var v = getCookie('toggle_' + className); 587 setClassDisplay(className, v); 588 setClassDisplay(className + '_neg', 1 - v); 589 } 590 591 /** 592 * Load data from the server using a HTTP POST request. 593 * 594 * @param {object} data POST payload 595 * @param {object} fn Success handler 596 * @param {string} format Response data format, defaults to 'xml' 597 * @return {object} this 598 * @see https://api.jquery.com/jQuery.post/ 599 */ 600 601 function sendAsyncEvent(data, fn, format) 602 { 603 var formdata = false; 604 605 if ($.type(data) === 'string' && data.length > 0) { 606 // Got serialized data. 607 data = data + '&app_mode=async&_txp_token=' + textpattern._txp_token; 608 } else if (data instanceof FormData) { 609 formdata = true; 610 data.append('app_mode', 'async'); 611 data.append('_txp_token', textpattern._txp_token); 612 } else { 613 data.app_mode = 'async'; 614 data._txp_token = textpattern._txp_token; 615 } 616 617 format = format || 'xml'; 618 619 return formdata ? 620 $.ajax({ 621 type: 'POST', 622 url: 'index.php', 623 data: data, 624 success: fn, 625 dataType: format, 626 processData: false, 627 contentType: false 628 }) : 629 $.post('index.php', data, fn, format); 630 } 631 632 /** 633 * A pub/sub hub for client-side events. 634 * 635 * @since 4.5.0 636 */ 637 638 textpattern.Relay = 639 { 640 /** 641 * Publishes an event to all registered subscribers. 642 * 643 * @param {string} event The event 644 * @param {object} data The data passed to registered subscribers 645 * @return {object} The Relay object 646 * @example 647 * textpattern.Relay.callback('newEvent', {'name1': 'value1', 'name2': 'value2'}); 648 */ 649 650 callback: function (event, data, timeout) { 651 clearTimeout(textpattern.Relay.timeouts[event]); 652 timeout = !timeout ? 0 : parseInt(timeout, 10); 653 654 if (!timeout || isNaN(timeout)) { 655 return $(this).trigger(event, data); 656 } 657 658 textpattern.Relay.timeouts[event] = setTimeout( 659 $.proxy(function() { 660 return textpattern.Relay.callback(event, data); 661 }, this), 662 parseInt(timeout, 10) 663 ); 664 }, 665 666 /** 667 * Subscribes to an event. 668 * 669 * @param {string} The event 670 * @param {object} fn The callback function 671 * @return {object} The Relay object 672 * @example 673 * textpattern.Relay.register('event', 674 * function (event, data) 675 * { 676 * alert(data); 677 * } 678 * ); 679 */ 680 681 register: function (event, fn) { 682 if (fn) { 683 $(this).on(event, fn); 684 } else { 685 $(this).off(event); 686 } 687 688 return this; 689 }, 690 691 timeouts: {}, 692 data: {selected: []} 693 }; 694 695 /** 696 * Textpattern localStorage. 697 * 698 * @since 4.6.0 699 */ 700 701 textpattern.storage = 702 { 703 /** 704 * Textpattern localStorage data. 705 */ 706 707 data: (!navigator.cookieEnabled || !window.localStorage ? null : JSON.parse(window.localStorage.getItem('textpattern.' + textpattern._txp_uid))) || {}, 708 709 /** 710 * Updates data. 711 * 712 * @param data The message 713 * @example 714 * textpattern.update({prefs: 'site'}); 715 */ 716 717 update: function (data) { 718 $.extend(true, textpattern.storage.data, data); 719 textpattern.storage.clean(textpattern.storage.data); 720 721 if (navigator.cookieEnabled && window.localStorage) { 722 window.localStorage.setItem('textpattern.' + textpattern._txp_uid, JSON.stringify(textpattern.storage.data)); 723 } 724 }, 725 726 clean: function (obj) { 727 Object.keys(obj).forEach(function (key) { 728 if (obj[key] && typeof obj[key] === 'object') { 729 textpattern.storage.clean(obj[key]); 730 } else if (obj[key] === null) { 731 delete obj[key]; 732 } 733 }); 734 } 735 }; 736 737 /** 738 * Logs debugging messages. 739 * 740 * @since 4.6.0 741 */ 742 743 textpattern.Console = 744 { 745 /** 746 * Stores an array of invoked messages. 747 */ 748 749 history: [], 750 751 /** 752 * Stores an array of messages to announce. 753 */ 754 755 messages: {}, 756 757 queue: {}, 758 759 /** 760 * Clear. 761 * 762 * @param {string} The event 763 * @return textpattern.Console 764 */ 765 766 clear: function (event, reset) { 767 event = event || textpattern.event; 768 textpattern.Console.messages[event] = []; 769 770 if (!!reset) { 771 textpattern.Console.queue[event] = false; 772 } 773 774 return this; 775 }, 776 777 /** 778 * Add message to announce. 779 * 780 * @param {string} The event 781 * @param {string} The message 782 * @return textpattern.Console 783 */ 784 785 addMessage: function (message, event) { 786 event = event || textpattern.event; 787 788 if (typeof textpattern.Console.messages[event] === 'undefined') { 789 textpattern.Console.messages[event] = []; 790 } 791 792 textpattern.Console.messages[event].push(message); 793 794 return this; 795 }, 796 797 /** 798 * Announce. 799 * 800 * @param {string} The event 801 * @return textpattern.Console 802 */ 803 804 announce: function (event, options) { 805 event = event || textpattern.event; 806 807 if (!!textpattern.Console.queue[event]) { 808 return this; 809 } else { 810 textpattern.Console.queue[event] = true; 811 } 812 813 $(document).ready(function() { 814 var c = 0, message = [], status = 0; 815 816 if (textpattern.Console.messages[event] && textpattern.Console.messages[event].length) { 817 var container = textpattern.prefs.message || '{message}'; 818 819 textpattern.Console.messages[event].forEach (function(pair) { 820 message.push(textpattern.mustache(container, { 821 status: pair[1] != 1 && pair[1] != 2 ? 'check' : 'alert', 822 message: pair[0] 823 })); 824 c += 2*(pair[1] == 1) + 1*(pair[1] == 2); 825 }); 826 827 status = !c ? 'success' : (c == 2*textpattern.Console.messages[event].length ? 'error' : 'warning'); 828 } 829 830 textpattern.Relay.callback('announce', {event: event, message: message, status: status}); 831 textpattern.Console.clear(event, true); 832 }); 833 834 return this; 835 }, 836 837 /** 838 * Logs a message. 839 * 840 * @param message The message 841 * @return textpattern.Console 842 * @example 843 * textpattern.Console.log('Some message'); 844 */ 845 846 log: function (message) { 847 if (textpattern.prefs.production_status === 'debug') { 848 textpattern.Console.history.push(message); 849 850 textpattern.Relay.callback('txpConsoleLog', { 851 'message': message 852 }); 853 } 854 855 return this; 856 } 857 }; 858 859 /** 860 * Console API module for textpattern.Console. 861 * 862 * Passes invoked messages to Web/JavaScript Console 863 * using console.log(). 864 * 865 * Uses a namespaced 'txpConsoleLog.ConsoleAPI' event. 866 */ 867 868 textpattern.Relay.register('txpConsoleLog.ConsoleAPI', function (event, data) { 869 if ($.type(console) === 'object' && $.type(console.log) === 'function') { 870 console.log(data.message); 871 } 872 }).register('uploadProgress', function (event, data) { 873 $('progress.txp-upload-progress').val(data.loaded / data.total); 874 }).register('uploadStart', function (event, data) { 875 $('progress.txp-upload-progress').val(0).show(); 876 }).register('uploadEnd', function (event, data) { 877 $('progress.txp-upload-progress').hide(); 878 }).register('updateList', function (event, data) { 879 var list = data.list || '#messagepane, .txp-async-update', 880 url = data.url || 'index.php', 881 callback = data.callback || function(event) { 882 textpattern.Console.announce(event); 883 }, 884 handle = function(html) { 885 if (html) { 886 var $html = $(html); 887 $.each(list.split(','), function(index, value) { 888 $(value).each(function() { 889 var id = this.id; 890 if (id) { 891 $(this).replaceWith($html.find('#'+id)).remove(); 892 $('#'+id).trigger('updateList'); 893 } 894 }); 895 }); 896 897 $html.remove(); 898 } 899 900 callback(data.event); 901 }; 902 903 $(list).addClass('disabled'); 904 905 if (typeof data.html == 'undefined') { 906 $('<html />').load(url, data.data, function(responseText, textStatus, jqXHR) { 907 handle(this); 908 }); 909 } else { 910 handle(data.html); 911 } 912 }).register('announce', function(event, data) { 913 var container = textpattern.prefs.messagePane || '', 914 message = container && data.message.length ? textpattern.mustache(container, {message: data.message.join('<br />'), status: data.status, close: textpattern.gTxt('close')}) : ''; 915 916 if (message) { 917 $('#messagepane').html(message); 918 } 919 }); 920 921 /** 922 * Script routing. 923 * 924 * @since 4.6.0 925 */ 926 927 textpattern.Route = 928 { 929 /** 930 * An array of attached listeners. 931 */ 932 933 attached: [], 934 935 /** 936 * Attaches a listener. 937 * 938 * @param {string} pages The page 939 * @param {object} fn The callback 940 */ 941 942 add: function (pages, fn) { 943 $.each(pages.split(','), function (index, page) { 944 textpattern.Route.attached.push({ 945 'page': $.trim(page), 946 'fn' : fn 947 }); 948 }); 949 950 return this; 951 }, 952 953 /** 954 * Initializes attached listeners. 955 * 956 * @param {object} options Options 957 * @param {string} options.event The event 958 * @param {string} options.step The step 959 */ 960 961 init: function (options) { 962 var custom = !!options; 963 var options = $.extend({ 964 'event': textpattern.event, 965 'step' : textpattern.step 966 }, options); 967 968 textpattern.Route.attached = textpattern.Route.attached.filter(function (elt) {return !!elt}); 969 textpattern.Route.attached.forEach(function (data, index) { 970 if (!custom && data.page === '' || data.page === options.event || data.page === '.' + options.step || data.page === options.event + '.' + options.step) { 971 data.fn({ 972 'event': options.event, 973 'step' : options.step, 974 'route': data.page 975 }); 976 977 delete(textpattern.Route.attached[index]); 978 } 979 }); 980 981 return this; 982 } 983 }; 984 985 /** 986 * Sends a form using AJAX and processes the response. 987 * 988 * @param {object} options Options 989 * @param {string} options.dataType The response data type 990 * @param {object} options.success The success callback 991 * @param {object} options.error The error callback 992 * @return {object} this 993 * @since 4.5.0 994 */ 995 996 jQuery.fn.txpAsyncForm = function (options) { 997 options = $.extend({ 998 dataType: 'script', 999 success : null, 1000 error : null 1001 }, options); 1002 1003 // Send form data to application, process response as script. 1004 this.off('submit.txpAsyncForm').on('submit.txpAsyncForm', function (event, extra) { 1005 event.preventDefault(); 1006 1007 if (typeof extra === 'undefined') extra = new Object; 1008 1009 var $this = $(this); 1010 1011 // Safari workaround? 1012 var $inputs = $('input[type="file"]:not([disabled])', $this); 1013 $inputs.each(function(i, input) { 1014 if (input.files.length > 0) return; 1015 $(input).prop('disabled', true); 1016 }); 1017 1018 var form = 1019 { 1020 data : typeof extra.form !== 'undefined' ? extra.form : ( typeof window.FormData === 'undefined' ? $this.serialize() : new FormData(this) ), 1021 extra : new Object, 1022 spinner: typeof extra['_txp_spinner'] !== 'undefined' ? $(extra['_txp_spinner']) : $('<span />').addClass('spinner ui-icon ui-icon-refresh') 1023 }; 1024 1025 $inputs.prop('disabled', false);// Safari workaround. 1026 1027 if (typeof extra['_txp_submit'] !== 'undefined') { 1028 form.button = $this.find(extra['_txp_submit']).eq(0); 1029 } else { 1030 form.button = $this.find('input[type="submit"]:focus').eq(0); 1031 1032 // WebKit does not set :focus on button-click: use first submit input as a fallback. 1033 if (!form.button.length) { 1034 form.button = $this.find('input[type="submit"]').eq(0); 1035 } 1036 } 1037 1038 form.extra[form.button.attr('name') || '_txp_submit'] = form.button.val() || '_txp_submit'; 1039 $.extend(true, form.extra, options.data, extra.data); 1040 // Show feedback while processing. 1041 form.button.attr('disabled', true).after(form.spinner.val(0)); 1042 $this.addClass('busy'); 1043 $('body').addClass('busy'); 1044 1045 if (form.data) { 1046 if ( form.data instanceof FormData ) { 1047 $.each(form.extra, function(key, val) { 1048 form.data.append(key, val); 1049 }); 1050 } else { 1051 $.each(form.extra, function(key, val) { 1052 form.data += '&'+key+'='+val; 1053 }); 1054 } 1055 } 1056 1057 sendAsyncEvent(form.data, function () {}, options.dataType) 1058 .done(function (data, textStatus, jqXHR) { 1059 if (options.success) { 1060 options.success($this, event, data, textStatus, jqXHR); 1061 } 1062 1063 textpattern.Relay.callback('txpAsyncForm.success', { 1064 'this' : $this, 1065 'event' : event, 1066 'data' : data, 1067 'textStatus': textStatus, 1068 'jqXHR' : jqXHR 1069 }); 1070 }) 1071 .fail(function (jqXHR, textStatus, errorThrown) { 1072 if (options.error) { 1073 options.error($this, event, jqXHR, $.ajaxSetup(), errorThrown); 1074 } 1075 1076 textpattern.Relay.callback('txpAsyncForm.error', { 1077 'this' : $this, 1078 'event' : event, 1079 'jqXHR' : jqXHR, 1080 'ajaxSettings': $.ajaxSetup(), 1081 'thrownError' : errorThrown 1082 }); 1083 }) 1084 .always(function () { 1085 $this.removeClass('busy'); 1086 form.button.removeAttr('disabled'); 1087 form.spinner.remove(); 1088 $('body').removeClass('busy'); 1089 textpattern.Console.announce(); 1090 }); 1091 }); 1092 1093 return this; 1094 }; 1095 1096 /** 1097 * Sends a link using AJAX and processes the plain text response. 1098 * 1099 * @param {object} options Options 1100 * @param {string} options.dataType The response data type 1101 * @param {object} options.success The success callback 1102 * @param {object} options.error The error callback 1103 * @return {object} this 1104 * @since 4.5.0 1105 */ 1106 1107 jQuery.fn.txpAsyncHref = function (options, selector) { 1108 options = $.extend({ 1109 dataType: 'text', 1110 success : null, 1111 error : null 1112 }, options); 1113 1114 selector = !!selector ? selector : null; 1115 1116 this.on('click.txpAsyncHref', selector, function (event) { 1117 event.preventDefault(); 1118 var $this = $(this); 1119 var url = this.search.replace('?', '') + '&' + $.param({value: $this.text()}); 1120 1121 // Show feedback while processing. 1122 $this.addClass('busy'); 1123 $('body').addClass('busy'); 1124 1125 sendAsyncEvent(url, function () {}, options.dataType) 1126 .done(function (data, textStatus, jqXHR) { 1127 if (options.dataType === 'text') { 1128 $this.html(data); 1129 } 1130 1131 if (options.success) { 1132 options.success($this, event, data, textStatus, jqXHR); 1133 } 1134 1135 textpattern.Relay.callback('txpAsyncHref.success', { 1136 'this' : $this, 1137 'event' : event, 1138 'data' : data, 1139 'textStatus': textStatus, 1140 'jqXHR' : jqXHR 1141 }); 1142 }) 1143 .fail(function (jqXHR, textStatus, errorThrown) { 1144 if (options.error) { 1145 options.error($this, event, jqXHR, $.ajaxSetup(), errorThrown); 1146 } 1147 1148 textpattern.Relay.callback('txpAsyncHref.error', { 1149 'this' : $this, 1150 'event' : event, 1151 'jqXHR' : jqXHR, 1152 'ajaxSettings': $.ajaxSetup(), 1153 'thrownError' : errorThrown 1154 }); 1155 }) 1156 .always(function () { 1157 $this.removeClass('busy'); 1158 $('body').removeClass('busy'); 1159 }); 1160 }); 1161 1162 return this; 1163 }; 1164 1165 /** 1166 * Sends a link using AJAX and processes the HTML response. 1167 * 1168 * @param {object} options Options 1169 * @param {string} options.dataType The response data type 1170 * @param {object} options.success The success callback 1171 * @param {object} options.error The error callback 1172 * @return {object} this 1173 * @since 4.6.0 1174 */ 1175 1176 function txpAsyncLink(event, txpEvent) 1177 { 1178 event.preventDefault(); 1179 var $this = $(event.target); 1180 if ($this.attr('href') === undefined) { 1181 $this = $this.parent(); 1182 } 1183 var url = $this.attr('href').replace('?', ''); 1184 1185 // Show feedback while processing. 1186 $this.addClass('busy'); 1187 $('body').addClass('busy'); 1188 1189 sendAsyncEvent(url, function () {}, 'html') 1190 .done(function (data, textStatus, jqXHR) { 1191 textpattern.Relay.callback('txpAsyncLink.'+txpEvent+'.success', { 1192 'this' : $this, 1193 'event' : event, 1194 'data' : data, 1195 'textStatus': textStatus, 1196 'jqXHR' : jqXHR 1197 }); 1198 }) 1199 .fail(function (jqXHR, textStatus, errorThrown) { 1200 textpattern.Relay.callback('txpAsyncLink.'+txpEvent+'.error', { 1201 'this' : $this, 1202 'event' : event, 1203 'jqXHR' : jqXHR, 1204 'ajaxSettings': $.ajaxSetup(), 1205 'thrownError' : errorThrown 1206 }); 1207 }) 1208 .always(function () { 1209 $this.removeClass('busy'); 1210 $('body').removeClass('busy'); 1211 }); 1212 1213 return this; 1214 } 1215 1216 /** 1217 * Creates a UI dialog. 1218 * 1219 * @param {object} options Options 1220 * @return {object} this 1221 * @since 4.6.0 1222 */ 1223 1224 jQuery.fn.txpDialog = function (options) { 1225 options = $.extend({ 1226 autoOpen: false, 1227 buttons: [{ 1228 text: textpattern.gTxt('ok'), 1229 click: function () { 1230 // callbacks? 1231 1232 if ($(this).is('form')) { 1233 $(this).submit(); 1234 } 1235 1236 $(this).dialog('close'); 1237 } 1238 }], 1239 width: 440 1240 }, options, $(this).data()); 1241 1242 this.dialog(options); 1243 1244 return this; 1245 }; 1246 1247 /** 1248 * Creates a date picker. 1249 * 1250 * @param {object} options Options 1251 * @return {object} this 1252 * @since 4.6.0 1253 */ 1254 1255 jQuery.fn.txpDatepicker = function (options) { 1256 // TODO $.datepicker.regional[ 'en' ]; 1257 // TODO support from RTL languages 1258 this.datepicker(options); 1259 1260 return this; 1261 }; 1262 1263 /** 1264 * Creates a sortable element. 1265 * 1266 * This method creates a sortable widget, allowing to 1267 * reorder elements in a list and synchronizes the updated 1268 * order with the server. 1269 * 1270 * @param {object} options 1271 * @param {string} options.dataType The response datatype 1272 * @param {object} options.success The sync success callback 1273 * @param {object} options.error The sync error callback 1274 * @param {string} options.event The event 1275 * @param {string} options.step The step 1276 * @param {string} options.cancel Prevents sorting if you start on elements matching the selector 1277 * @param {integer} options.delay Sorting delay 1278 * @param {integer} options.distance Tolerance, in pixels, for when sorting should start 1279 * @return this 1280 * @since 4.6.0 1281 */ 1282 1283 jQuery.fn.txpSortable = function (options) { 1284 options = $.extend({ 1285 dataType: 'script', 1286 success : null, 1287 error : null, 1288 event : textpattern.event, 1289 step : 'sortable_save', 1290 cancel : ':input, button', 1291 delay : 0, 1292 distance: 15, 1293 items : '[data-txp-sortable-id]' 1294 }, options); 1295 1296 var methods = 1297 { 1298 /** 1299 * Sends updated order to the server. 1300 */ 1301 1302 update: function () { 1303 var ids = [], $this = $(this); 1304 1305 $this.children('[data-txp-sortable-id]').each(function () { 1306 ids.push($(this).data('txp-sortable-id')); 1307 }); 1308 1309 if (ids) { 1310 sendAsyncEvent({ 1311 event: options.event, 1312 step : options.step, 1313 order: ids 1314 }, function () {}, options.dataType) 1315 .done(function (data, textStatus, jqXHR) { 1316 if (options.success) { 1317 options.success.call($this, data, textStatus, jqXHR); 1318 } 1319 1320 textpattern.Relay.callback('txpSortable.success', { 1321 'this' : $this, 1322 'data' : data, 1323 'textStatus': textStatus, 1324 'jqXHR' : jqXHR 1325 }); 1326 }) 1327 .fail(function (jqXHR, textStatus, errorThrown) { 1328 if (options.error) { 1329 options.error.call($this, jqXHR, $.ajaxSetup(), errorThrown); 1330 } 1331 1332 textpattern.Relay.callback('txpSortable.error', { 1333 'this' : $this, 1334 'jqXHR' : jqXHR, 1335 'ajaxSettings': $.ajaxSetup(), 1336 'thrownError' : errorThrown 1337 }); 1338 }); 1339 } 1340 } 1341 }; 1342 1343 return this.sortable({ 1344 cancel : options.cancel, 1345 delay : options.delay, 1346 distance: options.distance, 1347 update : methods.update, 1348 items : options.items 1349 }); 1350 }; 1351 1352 /** 1353 * Mask/unmask password input field. 1354 * 1355 * @since 4.6.0 1356 */ 1357 1358 textpattern.passwordMask = function () { 1359 $('form').on('click', '#show_password', function () { 1360 var inputBox = $(this).closest('form').find('input.txp-maskable'); 1361 var newType = (inputBox.attr('type') === 'password') ? 'text' : 'password'; 1362 textpattern.changeType(inputBox, newType); 1363 $(this).attr('checked', newType === 'text' ? 'checked' : null).prop('checked', newType === 'text'); 1364 }).find('#show_password').prop('checked', false); 1365 }; 1366 1367 /** 1368 * Change the type of an input element. 1369 * 1370 * @param {object} elem The <input/> element 1371 * @param {string} type The desired type 1372 * 1373 * @see https://gist.github.com/3559343 for original 1374 * @since 4.6.0 1375 */ 1376 1377 textpattern.changeType = function (elem, type) { 1378 if (elem.prop('type') === type) { 1379 // Already the correct type. 1380 return elem; 1381 } 1382 1383 try { 1384 // May fail if browser prevents it. 1385 return elem.prop('type', type); 1386 } catch (e) { 1387 // Create the element by hand. 1388 // Clone it via a div (jQuery has no html() method for an element). 1389 var html = $('<div>').append(elem.clone()).html(); 1390 1391 // Match existing attributes of type=text or type="text". 1392 var regex = /type=(\")?([^\"\s]+)(\")?/; 1393 1394 // If no match, add the type attribute to the end; otherwise, replace it. 1395 var tmp = $(html.match(regex) == null ? 1396 html.replace('>', ' type="' + type + '">') : 1397 html.replace(regex, 'type="' + type + '"')); 1398 1399 // Copy data from old element. 1400 tmp.data('type', elem.data('type')); 1401 var events = elem.data('events'); 1402 var cb = function (events) { 1403 return function () { 1404 // Re-bind all prior events. 1405 for (var idx in events) { 1406 var ydx = events[idx]; 1407 1408 for (var jdx in ydx) { 1409 tmp.bind(idx, ydx[jdx].handler); 1410 } 1411 } 1412 }; 1413 }(events); 1414 1415 elem.replaceWith(tmp); 1416 1417 // Wait a smidge before firing callback. 1418 setTimeout(cb, 10); 1419 1420 return tmp; 1421 } 1422 }; 1423 1424 /** 1425 * Encodes a string for a use in HTML. 1426 * 1427 * @param {string} string The string 1428 * @return {string} Encoded string 1429 * @since 4.6.0 1430 */ 1431 1432 textpattern.encodeHTML = function (string) { 1433 return $('<div/>').text(string).html(); 1434 }; 1435 1436 /** 1437 * Decodes a string as HTML. 1438 * 1439 * @param {string} string The string 1440 * @return {string} Encoded string 1441 * @since 4.8.0 1442 */ 1443 1444 textpattern.decodeHTML = function (string) { 1445 let div = document.createElement('template'); 1446 div.innerHTML = string.trim(); 1447 1448 return div.content; 1449 } 1450 1451 /** 1452 * Translates given substrings. 1453 * 1454 * @param {string} string The mustached string 1455 * @param {object} replacements Translated substrings 1456 * @return string Translated string 1457 * @since 4.7.0 1458 * @example 1459 * textpattern.mustache('{hello} world, and {bye|thanks}!', {hello: 'bye'}); 1460 */ 1461 1462 textpattern.mustache = function(string, replacements) 1463 { 1464 return string.replace(/\{([^\{\|\}]+)(\|[^\{\}]*)?\}/g, function(match, p1, p2) { 1465 return typeof replacements[p1] != 'undefined' ? replacements[p1] : (typeof p2 == 'undefined' ? match : p2.replace('|', '')); 1466 }); 1467 }; 1468 1469 /** 1470 * Translates given substrings. 1471 * 1472 * @param {string} string The string being translated 1473 * @param {object} replacements Translated substrings 1474 * @return string Translated string 1475 * @since 4.6.0 1476 * @example 1477 * textpattern.tr('hello world, and bye!', {'hello': 'bye', 'bye': 'hello'}); 1478 */ 1479 1480 textpattern.tr = function (string, replacements) { 1481 var match, position, output = '', replacement; 1482 1483 for (position = 0; position < string.length; position++) { 1484 match = false; 1485 1486 $.each(replacements, function (from, to) { 1487 if (string.substr(position, from.length) === from) { 1488 match = true; 1489 replacement = to; 1490 position = (position + from.length) - 1; 1491 1492 return; 1493 } 1494 }); 1495 1496 if (match) { 1497 output += replacement; 1498 } else { 1499 output += string.charAt(position); 1500 } 1501 } 1502 1503 return output; 1504 }; 1505 1506 /** 1507 * Returns an i18n string. 1508 * 1509 * @param {string} i18n The i18n string 1510 * @param {object} atts Replacement map 1511 * @param {boolean} escape TRUE to escape HTML in atts 1512 * @return {string} The string 1513 * @example 1514 * textpattern.gTxt('string', {'{name}': 'example'}, true); 1515 */ 1516 1517 textpattern.gTxt = function (i18n, atts, escape) { 1518 var tags = atts || {}; 1519 var string = i18n; 1520 var name = string.toLowerCase(); 1521 1522 if ($.type(textpattern.textarray[name]) !== 'undefined') { 1523 string = textpattern.textarray[name]; 1524 } 1525 1526 if (escape !== false) { 1527 string = textpattern.encodeHTML(string); 1528 1529 $.each(tags, function (key, value) { 1530 tags[key] = textpattern.encodeHTML(value); 1531 }); 1532 } 1533 1534 string = textpattern.tr(string, tags); 1535 1536 return string; 1537 }; 1538 1539 /** 1540 * Replaces HTML contents of each matched with i18n string. 1541 * 1542 * This is a jQuery plugin for textpattern.gTxt(). 1543 * 1544 * @param {object|string} options Options or the i18n string 1545 * @param {string} options.string The i18n string 1546 * @param {object} options.tags Replacement map 1547 * @param {boolean} options.escape TRUE to escape HTML in tags 1548 * @param {object} tags Replacement map 1549 * @param {boolean} escape TRUE to escape HTML in tags 1550 * @return {object} this 1551 * @see textpattern.gTxt() 1552 * @example 1553 * $('p').gTxt('string').class('alert-block warning'); 1554 */ 1555 1556 jQuery.fn.gTxt = function (opts, tags, escape) { 1557 var options = $.extend({ 1558 'string': opts, 1559 'tags' : tags, 1560 'escape': escape 1561 }, opts); 1562 1563 this.html(textpattern.gTxt(options.string, options.tags, options.escape)); 1564 1565 return this; 1566 }; 1567 1568 /** 1569 * ESC button closes alert messages. 1570 * CTRL+S triggers Save buttons click. 1571 * 1572 * @since 4.7.0 1573 */ 1574 1575 $(document).keydown(function (e) { 1576 var key = e.which; 1577 1578 if (key === 27) { 1579 $('.close').parent().toggle(); 1580 } else if (key === 19 || (!e.altKey && (e.metaKey || e.ctrlKey) && String.fromCharCode(key).toLowerCase() === 's')) 1581 { 1582 var obj = $('input.publish'); 1583 1584 if (obj.length) 1585 { 1586 e.preventDefault(); 1587 obj.eq(0).closest('form').submit(); 1588 } 1589 } 1590 }); 1591 1592 jQuery.fn.txpMenu = function(button) { 1593 var menu = this; 1594 1595 menu.on('click focusin', function (e) { 1596 e.stopPropagation(); 1597 }).menu({ 1598 select: function(e, ui) { 1599 menu.menu('focus', null, ui.item); 1600 if (e.originalEvent.type !== 'click') { 1601 ui.item.find('input[type="checkbox"]').click(); 1602 } 1603 } 1604 }).find('input[type="checkbox"]').keyup(function(e) { 1605 e.preventDefault(); 1606 }); 1607 1608 !button || button.on('click', function (e) { 1609 menu.toggle().position({ 1610 my: dir+' top', 1611 at: dir+' bottom', 1612 of: this 1613 }).focus().menu('focus', null, menu.find('.ui-menu-item:first')); 1614 1615 if (menu.is(':visible')) { 1616 $(document).one('blur click focusin', function (e) { 1617 menu.hide(); 1618 }); 1619 } 1620 1621 return false; 1622 }).on('focusin', function(e) { 1623 e.stopPropagation(); 1624 }); 1625 1626 return this; 1627 }; 1628 1629 /** 1630 * Search tool. 1631 * 1632 * @since 4.6.0 1633 */ 1634 1635 function txp_search() 1636 { 1637 var $ui = $('.txp-search'), 1638 button = $ui.find('.txp-search-options').button({ 1639 showLabel: false, 1640 icon: 'ui-icon-triangle-1-s' 1641 }), 1642 menu = $ui.find('.txp-dropdown'), 1643 crit = $ui.find('input[name="crit"]'); 1644 1645 menu.hide().txpMenu(button); 1646 1647 $ui.find('.txp-search-button').button({ 1648 showLabel: false, 1649 icon: 'ui-icon-search' 1650 }).click(function (e) { 1651 e.stopPropagation(); 1652 e.preventDefault(); 1653 $ui.submit(); 1654 }); 1655 1656 $ui.find('.txp-search-buttons').controlgroup(); 1657 1658 $ui.find('.txp-search-clear').click(function(e) { 1659 e.preventDefault(); 1660 crit.val(''); 1661 $ui.submit(); 1662 }); 1663 1664 $ui.txpMultiEditForm({ 1665 'checkbox' : 'input[name="search_method[]"][type=checkbox]', 1666 'row' : '.txp-dropdown li', 1667 'highlighted': '.txp-dropdown li', 1668 'confirmation': false 1669 }); 1670 1671 $ui.submit(function(e) { 1672 var empty = crit.val() !== ''; 1673 1674 if (empty) { 1675 menu.find('input[name="search_method[]"]').each(function() { 1676 empty = empty && !$(this).is(':checked'); 1677 }); 1678 } 1679 1680 if(empty) { 1681 button.click(); 1682 return false; 1683 } 1684 }); 1685 } 1686 1687 /** 1688 * Column manipulation tool. 1689 * 1690 * @since 4.7.0 1691 */ 1692 1693 var uniqueID = (function() { 1694 var id = 0; 1695 return function() { 1696 return id++; 1697 }; 1698 })(); // Invoke the outer function after defining it. 1699 1700 jQuery.fn.txpColumnize = function () 1701 { 1702 var $table = $(this), items = [], selectAll = true, stored = true, 1703 $headers = $table.find('thead tr>th'); 1704 1705 $headers.each(function (index) { 1706 var $this = $(this), $title = $this.text().trim(), $id = $this.data('col'); 1707 1708 if (!$title) { 1709 return; 1710 } 1711 1712 if ($id == undefined) { 1713 if ($id = this.className.match(/\btxp-list-col-([\w\-]+)\b/)) { 1714 $id = $id[1]; 1715 } else { 1716 return; 1717 } 1718 } 1719 1720 var disabled = $this.hasClass('asc') || $this.hasClass('desc'); 1721 var $li = $('<li />').addClass(disabled ? 'ui-state-disabled' : null); 1722 var $box = $('<input type="checkbox" tabindex="-1" class="checkbox active" data-name="list_options" checked="checked" />') 1723 .attr('value', $id) 1724 .attr('data-index', index) 1725 .prop('disabled', disabled); 1726 1727 $li.html($('<div role="menuitem" />') 1728 .append($('<label />').text($title).prepend($box))); 1729 1730 1731 var $target = $table.find('tr>*:nth-child(' + (index + 1) + ')'); 1732 var me = $li.find('input').on('change', function (ev) { 1733 toggleColumn($id, $target, $(this).prop('checked')); 1734 }); 1735 1736 if (stored) { 1737 try { 1738 if (textpattern.storage.data[textpattern.event]['columns'][$id] == false) { 1739 selectAll = false; 1740 $target.hide(); 1741 me.prop('checked', false); 1742 } 1743 } catch (e) { 1744 stored = false; 1745 } 1746 } 1747 1748 items.push($li); 1749 }); 1750 1751 if (!items.length) { 1752 return this; 1753 } 1754 1755 var $menu = $('<ul class="txp-dropdown" role="menu" />').hide(), 1756 $button = $('<a class="txp-list-options-button" href="#" />').text(textpattern.gTxt('list_options')).prepend('<span class="ui-icon ui-icon-gear"></span>Â '); 1757 1758 var $li = $('<li class="txp-dropdown-toggle-all" />'), 1759 $box = $('<input tabindex="-1" class="checkbox active" data-name="select_all" type="checkbox" />').attr('checked', selectAll); 1760 1761 $li.html($('<div role="menuitem" />') 1762 .append($('<label />').html(textpattern.gTxt('toggle_all_selected')).prepend($box))); 1763 1764 $menu.html($li).append(items); 1765 1766 var $container = $table.closest('.txp-layout-1col'); 1767 var $ui = $container.find('.txp-list-options'); 1768 var $panel = $container.find('.txp-control-panel'); 1769 1770 if (!$ui.length) { 1771 $ui = $('<div class="txp-list-options"></div>'); 1772 } else { 1773 $ui.find('a.txp-list-options-button, ul.txp-dropdown').remove(); 1774 $panel = false; 1775 } 1776 1777 $ui.append($button).append($menu); 1778 $menu.txpMenu($button); 1779 1780 $ui.data('_txpMultiEdit', null).txpMultiEditForm({ 1781 'checkbox' : 'input:not(:disabled)[data-name="list_options"][type=checkbox]', 1782 'selectAll' : 'input[data-name="select_all"][type=checkbox]', 1783 'row' : '.txp-dropdown li', 1784 'highlighted': '.txp-dropdown li', 1785 'confirmation': false 1786 }); 1787 1788 if ($panel.length) { 1789 $panel.after($ui); 1790 } else if ($panel !== false) { 1791 $table.closest('form').prepend($ui); 1792 } 1793 1794 return this; 1795 }; 1796 1797 /** 1798 * Set expanded/collapsed nature of all twisty boxes in a panel. 1799 * 1800 * The direction can either be 'expand' or 'collapse', passed 1801 * in as an argument to the handler. 1802 * 1803 * @param {event} ev Event that triggered the function 1804 * @since 4.6.0 1805 */ 1806 1807 function txp_expand_collapse_all(ev) 1808 { 1809 ev.preventDefault(); 1810 1811 var direction = ev.data.direction, 1812 container = ev.data.container || (ev.delegateTarget == ev.target ? 'body' : ev.delegateTarget); 1813 1814 $(container).find('.txp-summary a').each(function (i, elm) { 1815 var $elm = $(elm); 1816 1817 if (direction === 'collapse') { 1818 if ($elm.parent('.txp-summary').hasClass('expanded')) { 1819 $elm.click(); 1820 } 1821 } else { 1822 if (!$elm.parent('.txp-summary').hasClass('expanded')) { 1823 $elm.click(); 1824 } 1825 } 1826 }); 1827 } 1828 1829 /** 1830 * Restore sub-panel twisties to their as-stored state. 1831 * 1832 * @return {[type]} [description] 1833 */ 1834 1835 jQuery.fn.restorePanes = function () { 1836 var $this = $(this), stored = true; 1837 // Initialize dynamic WAI-ARIA attributes. 1838 $this.find('.txp-summary a').each(function (i, elm) { 1839 // Get id of toggled <section> region. 1840 var $elm = $(elm), region = this.hash; 1841 1842 if (region) { 1843 var $region = $this.find(region); 1844 region = region.substr(1); 1845 1846 var pane = $elm.data('txp-pane'); 1847 1848 if (pane === undefined) { 1849 pane = region; 1850 } 1851 1852 if (stored) { 1853 try { 1854 if (textpattern.storage.data[textpattern.event]['panes'][pane] == true) { 1855 $elm.parent('.txp-summary').addClass('expanded'); 1856 $region.show(); 1857 } 1858 } catch (e) { 1859 stored = false; 1860 } 1861 } 1862 1863 var vis = $region.is(':visible').toString(); 1864 $elm.attr('aria-controls', region).attr('aria-pressed', vis); 1865 $region.attr('aria-expanded', vis); 1866 } 1867 }); 1868 1869 return $this; 1870 }; 1871 1872 /** 1873 * Manage file uploads. 1874 * 1875 * @since 4.7.0 1876 */ 1877 1878 jQuery.fn.txpFileupload = function (options) { 1879 if (!jQuery.fn.fileupload) return this; 1880 1881 var form = this, fileInput = this.find('input[type="file"]'), 1882 maxChunkSize = Math.min(parseFloat(textpattern.prefs.max_upload_size || 1000000), Number.MAX_SAFE_INTEGER), 1883 maxFileSize = Math.min(parseFloat(textpattern.prefs.max_file_size || 1000000), Number.MAX_SAFE_INTEGER); 1884 1885 form.fileupload($.extend({ 1886 paramName: fileInput.attr('name'), 1887 dataType: 'script', 1888 maxFileSize: maxFileSize, 1889 maxChunkSize: maxChunkSize, 1890 singleFileUploads: true, 1891 formData: null, 1892 fileInput: null, 1893 dropZone: null, 1894 replaceFileInput: false,/* 1895 add: function (e, data) { 1896 form.uploadCount++; 1897 data.submit(); 1898 },*/ 1899 progressall: function (e, data) { 1900 textpattern.Relay.callback('uploadProgress', data); 1901 }, 1902 start: function (e) { 1903 textpattern.Relay.callback('uploadStart', e); 1904 }, 1905 stop: function (e) { 1906 textpattern.Relay.callback('uploadEnd', e); 1907 } 1908 }, options)).off('submit').submit(function (e) { 1909 e.preventDefault(); 1910 form.uploadCount = 0; 1911 var files = []; 1912 1913 for (let file of fileInput.prop('files')) { 1914 if (file.size > maxFileSize) { 1915 textpattern.Console.addMessage(['<strong>'+textpattern.encodeHTML(file['name'])+'</strong> - '+textpattern.gTxt('upload_err_form_size'), 1], 'uploadEnd'); 1916 } else { 1917 form.uploadCount++; 1918 file['order'] = form.uploadCount; 1919 files.push(file); 1920 } 1921 } 1922 1923 if (!files.length) { 1924 textpattern.Console.announce('uploadEnd'); 1925 } else { 1926 form.fileupload('add', { 1927 files: files 1928 }); 1929 } 1930 fileInput.val(''); 1931 }).bind('fileuploadsubmit', function (e, data) { 1932 data.formData = $.merge([{ 1933 'name' : 'fileInputOrder', 'value' : data.files[0].order+'/'+form.uploadCount 1934 }], options.formData); 1935 $.merge(data.formData, form.serializeArray()); 1936 1937 // Reduce maxChunkSize by extra data size (?) 1938 var res = typeof data.formData.entries !== 'undefined' 1939 ? Array.from(data.formData.entries(), function(prop) { 1940 return prop[1].name.length + prop[1].value.length; 1941 }).reduce(function(a, b) {return a + b + 2;}, 0) 1942 : 256; 1943 1944 var chunkSize = form.fileupload('option', 'maxChunkSize'); 1945 form.fileupload('option', 'maxChunkSize', Math.min(maxChunkSize - 8*(res + 255), chunkSize)); 1946 }); 1947 /* 1948 fileInput.on('change', function(e) { 1949 var singleFileUploads = false; 1950 1951 $(this.files).each(function () { 1952 if (this.size > maxChunkSize) { 1953 singleFileUploads = true; 1954 } 1955 }); 1956 1957 form.fileupload('option', 'singleFileUploads', singleFileUploads); 1958 })*/ 1959 return this; 1960 }; 1961 1962 jQuery.fn.txpUploadPreview = function(template) { 1963 if (!(template = template || textpattern.prefs.uploadPreview)) { 1964 return this; 1965 } 1966 1967 var form = $(this), last = form.children(':last-child'), maxSize = textpattern.prefs.max_file_size; 1968 var createObjectURL = (window.URL || window.webkitURL || {}).createObjectURL; 1969 1970 form.find('input[type="reset"]').on('click', function (e) { 1971 last.nextAll().remove(); 1972 }); 1973 1974 form.find('input[type="file"]').on('change', function (e) { 1975 last.nextAll().remove(); 1976 1977 $(this.files).each(function (index) { 1978 var preview = '', mime = this.type.split('/'), hash = typeof(md5) == 'function' ? md5(this.name) : index, status = this.size > maxSize ? 'alert' : ''; 1979 1980 if (createObjectURL) { 1981 switch (mime[0]) { 1982 case 'image': 1983 preview = '<img src="' + createObjectURL(this) + '" />'; 1984 break; 1985 // TODO case 'video':? 1986 case 'audio': 1987 preview = '<'+mime[0]+' controls src="' + createObjectURL(this) + '" />'; 1988 break; 1989 } 1990 } 1991 1992 preview = textpattern.mustache(template, $.extend(this, { 1993 hash: hash, 1994 preview: preview, 1995 status: status, 1996 title: textpattern.encodeHTML(this.name.replace(/\.[^\.]*$/, '')) 1997 })); 1998 form.append(preview); 1999 }); 2000 }).change(); 2001 2002 return this; 2003 }; 2004 2005 2006 /** 2007 * Cookie status. 2008 * 2009 * @deprecated in 4.6.0 2010 */ 2011 2012 var cookieEnabled = true; 2013 2014 // Setup panel. 2015 2016 textpattern.Route.add('setup', function () { 2017 textpattern.passwordMask(); 2018 $('#setup_admin_theme').prop('required',true); 2019 $('#setup_public_theme').prop('required',true); 2020 2021 if ($('textarea[name=config]').length) { 2022 $('.txp-config-download').on('click', function (e) { 2023 var text = $('textarea[name=config]').val(); 2024 var text = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text); 2025 var el = e.currentTarget; 2026 el.href = text; 2027 el.download = 'config.php'; 2028 }); 2029 } 2030 }); 2031 2032 // Login panel. 2033 2034 textpattern.Route.add('login', function () { 2035 // Check cookies. 2036 cookieEnabled = checkCookies(); 2037 2038 // Focus on either username or password when empty. 2039 $('#login_form input').filter(function() { 2040 return !this.value; 2041 }).first().focus(); 2042 2043 textpattern.passwordMask(); 2044 }); 2045 2046 // Write panel. 2047 2048 textpattern.Route.add('article', function () { 2049 // Assume users would not change the timestamp if they wanted to 2050 // 'publish now'/'reset time'. 2051 $(document).on('change', 2052 '#write-timestamp input.year,' + 2053 '#write-timestamp input.month,' + 2054 '#write-timestamp input.day,' + 2055 '#write-timestamp input.hour,' + 2056 '#write-timestamp input.minute,' + 2057 '#write-timestamp input.second', 2058 function () { 2059 $('#publish_now').prop('checked', false); 2060 $('#reset_time').prop('checked', false); 2061 } 2062 ); 2063 2064 textpattern.Relay.register('article.section_changed', 2065 function (event, data) { 2066 var $overrideForm = $('#override-form'); 2067 var override_sel = $overrideForm.val(); 2068 2069 $overrideForm.empty().append('<option></option>'); 2070 2071 $.each(data.data, function(key, item) { 2072 var $option = $('<option />'); 2073 $option.text(item).attr('dir', 'auto').prop('selected', item == override_sel); 2074 $overrideForm.append($option); 2075 }); 2076 } 2077 ); 2078 2079 $('#txp-write-sort-group').on('change', '#section', 2080 function () { 2081 if (typeof allForms !== 'undefined') { 2082 textpattern.Relay.callback('article.section_changed', { 2083 data: allForms[$(this).find(':selected').data('skin')] 2084 }); 2085 } 2086 } 2087 ).change(); 2088 2089 var status = 'select[name=Status]', form = $(status).parents('form'), submitButton = form.find('input[type=submit]'); 2090 2091 $('#article_form').on('change', status, function () { 2092 if (!form.hasClass('published')) { 2093 if ($(this).val() < 4) { 2094 submitButton.val(textpattern.gTxt('save')); 2095 } else { 2096 submitButton.val(textpattern.gTxt('publish')); 2097 } 2098 } 2099 }).on('submit.txpAsyncForm', function (e) { 2100 if ($pane.dialog('isOpen') && !$('#live-preview').is(':checked')) { 2101 $viewMode.click(); 2102 } 2103 }).on('click', '.txp-clone', function (e) { 2104 e.preventDefault(); 2105 form.trigger('submit', {data: {copy:1, publish:1}}); 2106 }); 2107 2108 // Switch to Text/HTML/Preview mode. 2109 var $pane = $('#pane-view').closest('.txp-dialog'), 2110 $field = '', 2111 $viewMode = $('#view_modes li.active [data-view-mode]'); 2112 if (!$viewMode.length) $viewMode = $('#view_modes [data-view-mode]').first(); 2113 2114 $pane.dialog({ 2115 dialogClass: 'txp-preview-container', 2116 buttons: [], 2117 closeOnEscape: false, 2118 maxWidth: '100%' 2119 }); 2120 2121 $pane.on( 'dialogopen', function( event, ui ) { 2122 $('#live-preview').trigger('change'); 2123 }).on( 'dialogclose', function( event, ui ) { 2124 $('#body, #excerpt').off('input', txp_article_preview); 2125 }); 2126 2127 $('#live-preview').on('change', function() { 2128 if ($(this).is(':checked')) { 2129 $('#body, #excerpt').on('input', txp_article_preview); 2130 } else { 2131 $('#body, #excerpt').off('input', txp_article_preview); 2132 } 2133 }) 2134 2135 textpattern.Relay.register('article.preview', 2136 function (e) { 2137 var data = form.serializeArray(); 2138 data.push({name: 'app_mode', value: 'async'}); 2139 data.push({name: 'preview', value: $field}); 2140 data.push({name: 'view', value: $viewMode.data('view-mode')}); 2141 textpattern.Relay.callback('updateList', { 2142 url: 'index.php #pane-view', 2143 data: data, 2144 list: '#pane-view', 2145 callback: function () { 2146 $pane.dialog('open'); 2147 } 2148 }); 2149 } 2150 ); 2151 2152 $(document).on('click', '[data-view-mode]', function(e) { 2153 e.preventDefault(); 2154 $viewMode = $(this); 2155 let $view = $viewMode.data('view-mode'); 2156 $viewMode.closest('ul').children('li').removeClass('active').filter('#tab-'+$view).addClass('active'); 2157 textpattern.Relay.callback('article.preview'); 2158 }).on('click', '[data-preview-link]', function(e) { 2159 e.preventDefault(); 2160 $field = $(this).data('preview-link'); 2161 $pane.dialog('option', 'title', $(this).text()); 2162 $viewMode.click(); 2163 }).on('updateList', '#pane-view.html', function() { 2164 Prism.highlightAllUnder(this); 2165 }); 2166 2167 function txp_article_preview() { 2168 $field = this.id; 2169 textpattern.Relay.callback('article.preview', null, 1000); 2170 } 2171 2172 // Handle Textfilter options. 2173 var $listoptions = $('.txp-textfilter-options .jquery-ui-selectmenu'); 2174 2175 $listoptions.on('selectmenuchange', function (e) { 2176 var me = $('option:selected', this); 2177 2178 var wrapper = me.closest('.txp-textfilter-options'); 2179 var thisHelp = me.data('help'); 2180 var renderHelp = (typeof thisHelp === 'undefined') ? '' : thisHelp; 2181 2182 wrapper.find('.textfilter-value').val(me.data('id')).trigger('change'); 2183 wrapper.find('.textfilter-help').html(renderHelp); 2184 2185 if ($pane.dialog('isOpen')) { 2186 wrapper.find('[data-preview-link]').click(); 2187 } 2188 }); 2189 2190 $listoptions.hide().menu(); 2191 }); 2192 2193 2194 textpattern.Route.add('article.init', function () { 2195 $('.txp-textfilter-options .jquery-ui-selectmenu').trigger('selectmenuchange') 2196 }) 2197 2198 textpattern.Route.add('file, image', function () { 2199 if (!$('#txp-list-container').length) return; 2200 2201 textpattern.Relay.register('uploadStart', function(event) { 2202 textpattern.Relay.data.fileid = []; 2203 }).register('uploadEnd', function(event) { 2204 var callback = function() { 2205 textpattern.Console.clear().announce(event.type); 2206 }; 2207 2208 $(document).ready(function() { 2209 $.merge(textpattern.Relay.data.selected, textpattern.Relay.data.fileid); 2210 2211 if (textpattern.Relay.data.fileid.length) { 2212 textpattern.Relay.callback('updateList', { 2213 data: $('nav.prev-next form').serializeArray(), 2214 list: '#txp-list-container', 2215 event: event.type, 2216 callback: callback 2217 }); 2218 } else { 2219 callback(); 2220 } 2221 }); 2222 }); 2223 2224 $('form.upload-form.async').txpUploadPreview() 2225 .txpFileupload({formData: [{name: 'app_mode', value: 'async'}]}); 2226 }); 2227 2228 // Uncheck reset on timestamp change. 2229 2230 textpattern.Route.add('article, file', function () { 2231 $(document).on('change', '.posted input', function (e) { 2232 $('#publish_now, #reset_time').prop('checked', false); 2233 }); 2234 }); 2235 2236 // 'Clone' button on Pages, Forms, Styles panels. 2237 2238 textpattern.Route.add('skin, css, page, form', function () { 2239 $('.txp-clone').click(function (e) { 2240 e.preventDefault(); 2241 var target = $(this).data('form'); 2242 if (target) { 2243 var $target = $('#' + target); 2244 $target.append('<input type="hidden" name="copy" value="1" />'); 2245 $target.off('submit.txpAsyncForm').trigger('submit'); 2246 } 2247 }); 2248 }); 2249 2250 // Tagbuilder. 2251 2252 textpattern.Route.add('page, form, file, image', function () { 2253 // Set up asynchronous tag builder links. 2254 textpattern.Relay.register('txpAsyncLink.tag.success', function (event, data) { 2255 $('#tagbuild_links').dialog('close').html($(data['data'])).dialog('open').restorePanes(); 2256 $('#txp-tagbuilder-output').select(); 2257 }); 2258 2259 $(document).on('click', '.txp-tagbuilder-link', function (ev) { 2260 txpAsyncLink(ev, 'tag'); 2261 }); 2262 2263 // Set up asynchronous tag builder launcher. 2264 textpattern.Relay.register('txpAsyncLink.tagbuilder.success', function (event, data) { 2265 $('#tagbuild_links').dialog('close').html($(data['data'])).dialog('open').restorePanes(); 2266 }); 2267 2268 $(document).on('click', '.txp-tagbuilder-dialog', function (ev) { 2269 txpAsyncLink(ev, 'tagbuilder'); 2270 }); 2271 2272 $('#tagbuild_links').dialog({ 2273 dialogClass: 'txp-tagbuilder-container', 2274 autoOpen: false, 2275 focus: function (ev, ui) { 2276 $(ev.target).closest('.ui-dialog').focus(); 2277 } 2278 }); 2279 2280 // Set up delegated asynchronous tagbuilder form submission. 2281 $('#tagbuild_links').on('click', 'form.asynchtml input[type="submit"]', function (ev) { 2282 $(this).closest('form.asynchtml').txpAsyncForm({ 2283 dataType: 'html', 2284 error: function () { 2285 window.alert(textpattern.gTxt('form_submission_error')); 2286 }, 2287 success: function ($this, event, data) { 2288 $('#tagbuild_links').html(data); 2289 $('#txp-tagbuilder-output').select(); 2290 } 2291 }); 2292 }); 2293 }); 2294 2295 // Pophelp. 2296 2297 textpattern.Route.add('', function () { 2298 textpattern.Relay.register('txpAsyncLink.pophelp.success', function (event, data) { 2299 $(data.event.target).parent().attr('data-item', encodeURIComponent(data.data) ); 2300 $('#pophelp_dialog').dialog('close').html(data.data).dialog('open'); 2301 }); 2302 2303 $('body').on('click','.pophelp', function (ev) { 2304 var $pophelp = $('#pophelp_dialog'); 2305 2306 if ($pophelp.length == 0) { 2307 $pophelp = $('<div id="pophelp_dialog"></div>'); 2308 $('body').append($pophelp); 2309 $pophelp.dialog({ 2310 classes: {'ui-dialog': 'txp-dialog-container'}, 2311 autoOpen: false, 2312 width: 440, 2313 title: textpattern.gTxt('help'), 2314 focus: function (ev, ui) { 2315 $(ev.target).closest('.ui-dialog').focus(); 2316 } 2317 }); 2318 } 2319 2320 var item = $(ev.target).parent().attr('data-item') || $(ev.target).attr('data-item'); 2321 if (typeof item === 'undefined' ) { 2322 txpAsyncLink(ev, 'pophelp'); 2323 } else { 2324 $pophelp.dialog('close').html(decodeURIComponent(item)).dialog('open'); 2325 } 2326 return false; 2327 }); 2328 }); 2329 2330 // Forms panel. 2331 2332 textpattern.Route.add('form', function () { 2333 $('#allforms_form').txpMultiEditForm({ 2334 'checkbox' : 'input[name="selected_forms[]"][type=checkbox]', 2335 'row' : '.switcher-list li, .form-list-name', 2336 'highlighted': '.switcher-list li' 2337 }); 2338 2339 textpattern.Relay.register('txpAsyncForm.success', function () { 2340 $('#allforms_form').txpMultiEditForm('select', {value: textpattern.Relay.data.selected}); 2341 $('#allforms_form_sections').restorePanes(); 2342 }); 2343 }); 2344 2345 // Users panel. 2346 2347 textpattern.Route.add('admin', function () { 2348 textpattern.passwordMask(); 2349 }); 2350 2351 // Plugins panel. 2352 2353 textpattern.Route.add('plugin', function () { 2354 textpattern.Relay.register('txpAsyncHref.success', function (event, data) { 2355 $(data['this']).closest('tr').toggleClass('active'); 2356 }); 2357 }); 2358 2359 // Diagnostics panel. 2360 2361 textpattern.Route.add('diag', function () { 2362 $('#diag_clear_private').change(function () { 2363 var diag_data = $('#diagnostics-data').val(); 2364 if ($('#diag_clear_private').is(':checked')) { 2365 var regex = new RegExp($('#diagnostics-data').attr('data-txproot'), 'g'); 2366 diag_data = diag_data.replace(/^===.*\s/gm, '').replace(regex, '__TXP-ROOT'); 2367 } else { 2368 diag_data = diag_data.replace(/^=== +/gm, ''); 2369 } 2370 $('#diagnostics-detail').val(diag_data); 2371 }); 2372 $('#diag_clear_private').change(); 2373 }); 2374 2375 // Languages panel. 2376 2377 textpattern.Route.add('lang', function () { 2378 $('.txp-grid-lang').on('click', 'button', function (ev) { 2379 ev.preventDefault(); 2380 var $me = $(this), $form = $me.closest('form'); 2381 $form.find('input[name=step]').val($me.attr('name')); 2382 $(ev.delegateTarget).addClass('disabled').find('button').attr('disabled', true); 2383 $form.submit(); 2384 }); 2385 }); 2386 2387 // Images edit panel. 2388 2389 textpattern.Route.add('image', function () { 2390 $('.thumbnail-swap-size').button({ 2391 showLabel: false, 2392 icon: 'ui-icon-transfer-e-w' 2393 }).on('click', function (ev) { 2394 var $w = $('#width'); 2395 var $h = $('#height'); 2396 var width = $w.val(); 2397 var height = $h.val(); 2398 $w.val(height); 2399 $h.val(width); 2400 }); 2401 }); 2402 2403 // Sections panel. Used for edit panel and multiedit change of page+style. 2404 // This can probably be cleaned up / optimised. 2405 2406 textpattern.Route.add('section', function () 2407 { 2408 /** 2409 * Display assets based on the selected theme. 2410 * 2411 * @param string skin The theme name from which to show assets 2412 */ 2413 function section_theme_show(skin) { 2414 $('#section_page, #section_css, #multiedit_page, #multiedit_css, #multiedit_dev_page, #multiedit_dev_css').empty(); 2415 var $pageSelect = $('[name=section_page], #multiedit_dev_page'); 2416 var $styleSelect = $('[name=css], #multiedit_dev_css'); 2417 2418 if (skin in skin_page) { 2419 $pageSelect.append('<option></option>'); 2420 2421 $.each(skin_page[skin], function(key, item) { 2422 var isSelected = (item == page_sel) ? ' selected' : ''; 2423 $pageSelect.append('<option'+isSelected+'>'+item+'</option>'); 2424 }); 2425 2426 if (page_sel === null) { 2427 $pageSelect.append('<option selected>*</option>'); 2428 } 2429 } 2430 2431 if (skin in skin_style) { 2432 $styleSelect.append('<option></option>'); 2433 2434 $.each(skin_style[skin], function(key, item) { 2435 var isSelected = (item == style_sel) ? ' selected' : ''; 2436 $styleSelect.append('<option'+isSelected+'>'+item+'</option>'); 2437 }); 2438 2439 if (style_sel === null) { 2440 $styleSelect.append('<option selected>*</option>'); 2441 } 2442 } 2443 } 2444 2445 $('main').on('change', '#section_skin, #multiedit_skin, #multiedit_dev_skin', function() { 2446 section_theme_show($(this).val()); 2447 }).on('change', 'select[name=edit_method]', function() { 2448 if ($(this).val() === 'changepagestyle') { 2449 $('#multiedit_skin').change(); 2450 } else if ($(this).val() === 'changepagestyledev') { 2451 $('#multiedit_dev_skin').change(); 2452 } 2453 }); 2454 2455 // Invoke the handler now to set things on initial page load. 2456 $('#section_skin').change(); 2457 }); 2458 2459 // Plugin help panel. 2460 2461 textpattern.Route.add('plugin.plugin_help', function () 2462 { 2463 var $helpWrap = $(document.body).children('main'); 2464 var $helpTxt = $helpWrap.children('.txp-layout-textbox'); 2465 var $head = $helpTxt.children(':first'); 2466 var $sectHeads = $helpTxt.children('h2'); 2467 var $intro = $head.nextUntil($sectHeads); 2468 var sectIdPrefix = 'plugin_help_section_'; 2469 2470 if ($head.prop('tagName') != 'H1' 2471 || $intro.length && !$sectHeads.length 2472 || !$intro.length && $sectHeads.length < 2 2473 || $helpTxt.find('h1').length > 1 2474 || $helpTxt.find('script, style, [style], [id^="' + sectIdPrefix + '"], [id*=" ' + sectIdPrefix + '"], [class^="txp-layout"], [class*=" txp-layout"], [class^="txp-grid"], [class*=" txp-grid"]').length 2475 ) { 2476 return; 2477 } 2478 2479 $helpTxt.detach(); 2480 2481 var $sects = $(); 2482 var tabs = ''; 2483 2484 if ($intro.length) { 2485 $intro = $intro.wrapAll('<section class="txp-tabs-vertical-group" id="' + sectIdPrefix + 'intro" aria-labelledby="intro-label" />').parent(); 2486 $sects = $sects.add($intro); 2487 tabs += '<li><a data-txp-pane="intro" href="#' + sectIdPrefix + 'intro">' + textpattern.gTxt('documentation') + '</a></li>'; 2488 } 2489 2490 $sectHeads.each(function(i, sectHead) { 2491 var $sectHead = $(sectHead); 2492 var $tabHead = $sectHead.clone(); 2493 2494 $tabHead.find('a').each(function(i, anchor) { 2495 $(anchor).contents().unwrap(); 2496 }); 2497 2498 // Grab the heading, strip out markup, then sanitize. 2499 var tabTitle = $("<div>").html($tabHead.html()).text(); 2500 var tabName = tabTitle.replace(/[^a-z0-9\s]/gi, '').replace(/[_\s]/g, '_').toLowerCase(); 2501 var sectId = sectIdPrefix + tabName; 2502 2503 $sects = $sects.add($sectHead.nextUntil(sectHead).addBack().wrapAll('<section class="txp-tabs-vertical-group" id="' + sectId + '" aria-labelledby="' + sectId + '-label" />').parent()); 2504 tabs += '<li><a data-txp-pane="' + tabName + '" href="#' + sectId + '">' + tabTitle + '</a></li>'; 2505 }); 2506 2507 $head.addClass('txp-heading').wrap('<div class="txp-layout-1col"></div>'); 2508 $sects.wrapAll('<div class="txp-layout-4col-3span" />'); 2509 $sects.parent().before('<div class="txp-layout-4col-alt"><section class="txp-details" id="all_sections" aria-labelledby="all_sections-label"><h3 id="all_sections-label">' + textpattern.gTxt('plugin_help') + '</h3><div role="group"><ul class="switcher-list">' + tabs + '</ul></div></section></div>'); 2510 $helpTxt.wrap('<div class="txp-layout" />').contents().unwrap().parent().appendTo($helpWrap); 2511 }); 2512 2513 // All panels? 2514 2515 textpattern.Route.add('', function () { 2516 // Pane states 2517 var hasTabs = $('.txp-layout:has(.switcher-list li a[data-txp-pane])'); 2518 2519 if (hasTabs.length == 0) { 2520 return; 2521 } 2522 2523 var tabs = hasTabs.find('.switcher-list li'); 2524 var $switchers = tabs.children('a[data-txp-pane]'); 2525 var $section = window.location.hash ? hasTabs.find($(window.location.hash).closest('section')) : []; 2526 var selectedTab = 1; 2527 2528 if (textpattern.event === 'plugin') { 2529 var nameParam = new RegExp('[\?&]name=([^&#]*)').exec(window.location.href); 2530 var dataItem = nameParam[1]; 2531 } else { 2532 dataItem = textpattern.event; 2533 } 2534 2535 tabs.on('click focus', function (ev) { 2536 var me = $(this).children('a[data-txp-pane]'); 2537 var data = new Object; 2538 2539 data[dataItem] = {'tab':me.data('txp-pane')}; 2540 textpattern.storage.update(data); 2541 }); 2542 2543 hasTabs.find('a:not([data-txp-pane], .pophelp)').click(function() { 2544 $section = hasTabs.find($(this.hash).closest('section')); 2545 2546 if ($section.length) { 2547 selectedTab = $section.index(); 2548 $switchers.eq(selectedTab).click(); 2549 } 2550 }); 2551 2552 if ($section.length) { 2553 selectedTab = $section.index(); 2554 $switchers.eq(selectedTab).click(); 2555 } else if (textpattern.storage.data[dataItem] !== undefined && textpattern.storage.data[dataItem]['tab'] !== undefined) { 2556 $switchers.each(function (i, elm) { 2557 if ($(elm).data('txp-pane') == textpattern.storage.data[dataItem]['tab']) { 2558 selectedTab = i; 2559 } 2560 }); 2561 } else { 2562 selectedTab = 0; 2563 } 2564 2565 if (typeof selectedTab === 'undefined') { 2566 selectedTab = 0; 2567 } 2568 2569 hasTabs.tabs({active: selectedTab}).removeClass('ui-widget ui-widget-content ui-corner-all').addClass('ui-tabs-vertical'); 2570 hasTabs.find('.switcher-list').removeClass('ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all'); 2571 tabs.removeClass('ui-state-default ui-corner-top'); 2572 hasTabs.find('.txp-tabs-vertical-group').removeClass('ui-widget-content ui-corner-bottom'); 2573 }); 2574 2575 // Initialize JavaScript. 2576 $(document).ready(function () { 2577 $('body').restorePanes(); 2578 2579 // Collapse/Expand all support. 2580 $('#supporting_content, #tagbuild_links, #content_switcher').on('click', '.txp-collapse-all', {direction: 'collapse'}, txp_expand_collapse_all) 2581 .on('click', '.txp-expand-all', {direction: 'expand'}, txp_expand_collapse_all); 2582 2583 // Confirmation dialogs. 2584 $(document).on('click.txpVerify', 'a[data-verify]', function (e) { 2585 return verify($(this).data('verify')); 2586 }); 2587 2588 $(document).on('submit.txpVerify', 'form[data-verify]', function (e) { 2589 return verify($(this).data('verify')); 2590 }); 2591 2592 // Disable spellchecking on all elements of class 'code' in capable browsers. 2593 var c = $('.code')[0]; 2594 2595 if (c && 'spellcheck' in c) { 2596 $('.code').prop('spellcheck', false); 2597 } 2598 2599 // Enable spellcheck for all elements mentioned in textpattern.prefs.do_spellcheck. 2600 $(textpattern.prefs.do_spellcheck).each(function(i, c) { 2601 if ('spellcheck' in c) { 2602 $(c).prop('spellcheck', true); 2603 } 2604 }); 2605 2606 // Attach toggle behaviours. 2607 $(document).on('click', '.txp-summary a[class!=pophelp]', toggleDisplayHref); 2608 2609 // Establish AJAX timeout from prefs. 2610 if ($.ajaxSetup().timeout === undefined) { 2611 $.ajaxSetup({timeout: textpattern.ajax_timeout}); 2612 } 2613 2614 // Set up asynchronous forms. 2615 $('form.async').txpAsyncForm({ 2616 error: function () { 2617 window.alert(textpattern.gTxt('form_submission_error')); 2618 } 2619 }); 2620 2621 // Set up asynchronous links. 2622 $('body').txpAsyncHref($.extend({ 2623 error: function () { 2624 window.alert(textpattern.gTxt('form_submission_error')); 2625 } 2626 }, $(this).hasClass('script') ? {dataType: 'script'} : {}), 'a.async'); 2627 2628 // Close button on the announce pane. 2629 $(document).on('click', '.close', function (e) { 2630 e.preventDefault(); 2631 $(this).parent().remove(); 2632 }); 2633 2634 // Event handling and automation. 2635 $(document).on('change.txpAutoSubmit', 'form [data-submit-on="change"]', function (e) { 2636 $(this).parents('form').submit(); 2637 }); 2638 2639 // Polyfills. 2640 // Add support for form attribute in submit buttons. 2641 if ($('html').hasClass('no-formattribute')) { 2642 $('.txp-save input[form]').click(function (e) { 2643 var targetForm = $(this).attr('form'); 2644 $('form[id=' + targetForm + ']').submit(); 2645 }); 2646 } 2647 2648 // Establish UI defaults. 2649 $('.txp-dropdown').hide(); 2650 $('.txp-dialog').txpDialog(); 2651 $('.txp-dialog.modal').dialog('option', 'modal', true); 2652 $('.txp-datepicker').txpDatepicker(); 2653 $('.txp-sortable').txpSortable(); 2654 2655 2656 // TODO: integrate jQuery UI stuff properly -------------------------------- 2657 2658 2659 // Selectmenu 2660 $('.jquery-ui-selectmenu').selectmenu({ 2661 position: { my: dir+' top', at: dir+' bottom' } 2662 }); 2663 2664 // Button 2665 $('.jquery-ui-button').button(); 2666 2667 // Button set 2668 $('.jquery-ui-controlgroup').controlgroup(); 2669 2670 2671 // TODO: end integrate jQuery UI stuff properly ---------------------------- 2672 2673 2674 // Async lists navigation 2675 $('#txp-list-container').closest('main').on('submit', 'nav.prev-next form', function(e) { 2676 e.preventDefault(); 2677 textpattern.Relay.callback('updateList', {data: $(this).serializeArray()}); 2678 }).on('click', '.txp-navigation a', function(e) { 2679 if ($(this).hasClass('pophelp')) return; 2680 e.preventDefault(); 2681 textpattern.Relay.callback('updateList', {url: $(this).attr('href'), data: $('nav.prev-next form').serializeArray()}); 2682 scroll(0, 0); 2683 }).on('click', '.txp-list thead th a', function(e) { 2684 e.preventDefault(); 2685 textpattern.Relay.callback('updateList', {list: '#txp-list-container', url: $(this).attr('href'), data: $('nav.prev-next form').serializeArray()}); 2686 }).on('submit', 'form[name="longform"]', function(e) { 2687 e.preventDefault(); 2688 textpattern.Relay.callback('updateList', {data: $(this).serializeArray()}); 2689 }).on('submit', 'form.txp-search', function(e) { 2690 e.preventDefault(); 2691 if ($(this).find('input[name="crit"]').val()) $(this).find('.txp-search-clear').removeClass('ui-helper-hidden'); 2692 else $(this).find('.txp-search-clear').addClass('ui-helper-hidden'); 2693 textpattern.Relay.callback('updateList', {data: $(this).serializeArray()}); 2694 }).on('updateList', '#txp-list-container', function() { 2695 if ($(this).find('.multi_edit_form').txpMultiEditForm('select', {value: textpattern.Relay.data.selected}).find('table.txp-list').txpColumnize().length == 0) { 2696 $(this).closest('.txp-layout-1col').find('.txp-list-options-button').hide(); 2697 } 2698 }); 2699 2700 // Find and open associated dialogs. 2701 $(document).on('click.txpDialog', '[data-txp-dialog]', function (e) { 2702 $($(this).data('txp-dialog')).dialog('open'); 2703 e.preventDefault(); 2704 }); 2705 2706 // Attach multi-edit form. 2707 $('.multi_edit_form').txpMultiEditForm(); 2708 $('table.txp-list').txpColumnize(); 2709 2710 $('a.txp-logout, .txp-logout a').attr('href', 'index.php?logout=1&lang='+textpattern.prefs.language_ui+'&_txp_token='+textpattern._txp_token); 2711 2712 // Initialize panel specific JavaScript. 2713 textpattern.Route.init(); 2714 // Trigger post init events. 2715 textpattern.Route.init({'step':'init'}); 2716 2717 // Arm UI. 2718 $('.not-ready').removeClass('not-ready'); 2719 textpattern.Console.announce(); 2720 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
title