Textpattern | PHP Cross Reference | Content Management Systems |
Description: Collection of client-side tools.
1 /* 2 * Textpattern Content Management System 3 * http://textpattern.com 4 * 5 * Copyright (C) 2016 The Textpattern Development Team 6 * 7 * This file is part of Textpattern. 8 * 9 * Textpattern is free software; you can redistribute it and/or 10 * modify it under the terms of the GNU General Public License 11 * as published by the Free Software Foundation, version 2. 12 * 13 * Textpattern is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 * GNU General Public License for more details. 17 * 18 * You should have received a copy of the GNU General Public License 19 * along with Textpattern. If not, see <http://www.gnu.org/licenses/>. 20 */ 21 22 /** 23 * Collection of client-side tools. 24 */ 25 26 /** 27 * Ascertain the page direction (LTR or RTL) as a variable. 28 */ 29 30 var langdir = document.documentElement.dir; 31 32 /** 33 * Checks if HTTP cookies are enabled. 34 * 35 * @return {boolean} 36 */ 37 38 function checkCookies() 39 { 40 var date = new Date(); 41 42 date.setTime(date.getTime() + (60 * 1000)); 43 44 document.cookie = 'testcookie=enabled; expired=' + date.toGMTString() + '; path=/'; 45 46 cookieEnabled = (document.cookie.length > 2) ? true : false; 47 48 date.setTime(date.getTime() - (60 * 1000)); 49 50 document.cookie = 'testcookie=; expires=' + date.toGMTString() + '; path=/'; 51 52 return cookieEnabled; 53 } 54 55 /** 56 * Spawns a centred popup window. 57 * 58 * @param {string} url The location 59 * @param {integer} width The width 60 * @param {integer} height The height 61 * @param {string} options A list of options 62 */ 63 64 function popWin(url, width, height, options) 65 { 66 var w = (width) ? width : 400; 67 var h = (height) ? height : 400; 68 69 var t = (screen.height) ? (screen.height - h) / 2 : 0; 70 var l = (screen.width) ? (screen.width - w) / 2 : 0; 71 72 var opt = (options) ? options : 'toolbar = no, location = no, directories = no, ' + 73 'status = yes, menubar = no, scrollbars = yes, copyhistory = no, resizable = yes'; 74 75 var popped = window.open(url, 'popupwindow', 76 'top = ' + t + ', left = ' + l + ', width = ' + w + ', height = ' + h + ',' + opt); 77 78 popped.focus(); 79 } 80 81 /** 82 * Legacy multi-edit tool. 83 * 84 * @param {object} elm 85 * @deprecated in 4.6.0 86 */ 87 88 function poweredit(elm) 89 { 90 var something = elm.options[elm.selectedIndex].value; 91 92 // Add another chunk of HTML 93 var pjs = document.getElementById('js'); 94 95 if (pjs == null) { 96 var br = document.createElement('br'); 97 elm.parentNode.appendChild(br); 98 99 pjs = document.createElement('P'); 100 pjs.setAttribute('id', 'js'); 101 elm.parentNode.appendChild(pjs); 102 } 103 104 if (pjs.style.display == 'none' || pjs.style.display == '') { 105 pjs.style.display = 'block'; 106 } 107 108 if (something != '') { 109 switch (something) { 110 default: 111 pjs.style.display = 'none'; 112 break; 113 } 114 } 115 116 return false; 117 } 118 119 /** 120 * Basic confirmation for potentially powerful choices (like deletion, 121 * for example). 122 * 123 * @param {string} msg The message 124 * @return {boolean} TRUE if user confirmed the action 125 */ 126 127 function verify(msg) 128 { 129 return confirm(msg); 130 } 131 132 /** 133 * Selects all multi-edit checkboxes. 134 * 135 * @deprecated in 4.5.0 136 */ 137 138 function selectall() 139 { 140 $('form[name=longform] input[type=checkbox][name="selected[]"]').prop('checked', true); 141 } 142 143 /** 144 * De-selects all multi-edit checkboxes. 145 * 146 * @deprecated in 4.5.0 147 */ 148 149 function deselectall() 150 { 151 $('form[name=longform] input[type=checkbox][name="selected[]"]').prop('checked', false); 152 } 153 154 /** 155 * Selects a range of multi-edit checkboxes. 156 * 157 * @deprecated in 4.5.0 158 */ 159 160 function selectrange() 161 { 162 var inrange = false; 163 164 $('form[name=longform] input[type=checkbox][name="selected[]"]').each(function () 165 { 166 var $this = $(this); 167 168 if ($this.is(':checked')) { 169 inrange = (!inrange) ? true : false; 170 } 171 172 if (inrange) { 173 $this.prop('checked', true); 174 } 175 }); 176 } 177 178 /** 179 * ? 180 * 181 * @deprecated in 4.5.0 182 */ 183 184 function cleanSelects() 185 { 186 var withsel = document.getElementById('withselected'); 187 188 if (withsel && withsel.options[withsel.selectedIndex].value != '') { 189 return (withsel.selectedIndex = 0); 190 } 191 } 192 193 /** 194 * Multi-edit functions. 195 * 196 * @param {string|object} method Called method, or options 197 * @param {object} opt Options if method is a method 198 * @return {object} this 199 * @since 4.5.0 200 */ 201 202 jQuery.fn.txpMultiEditForm = function (method, opt) 203 { 204 var args = {}; 205 206 var defaults = { 207 'checkbox' : 'input[name="selected[]"][type=checkbox]', 208 'row' : 'tbody td', 209 'highlighted' : 'tr', 210 'selectedClass' : 'selected', 211 'actions' : 'select[name=edit_method]', 212 'submitButton' : '.multi-edit input[type=submit]', 213 'selectAll' : 'input[name=select_all][type=checkbox]', 214 'rowClick' : true, 215 'altClick' : true, 216 'confirmation' : textpattern.gTxt('are_you_sure') 217 }; 218 219 if ($.type(method) !== 'string') { 220 opt = method; 221 method = null; 222 } else { 223 args = opt; 224 } 225 226 this.closest('form').each(function () 227 { 228 var $this = $(this), form = {}, methods = {}, lib = {}; 229 230 if ($this.data('_txpMultiEdit')) { 231 form = $this.data('_txpMultiEdit'); 232 opt = $.extend(form.opt, opt); 233 } else { 234 opt = $.extend(defaults, opt); 235 form.boxes = opt.checkbox; 236 form.editMethod = $this.find(opt.actions); 237 form.lastCheck = null; 238 form.opt = opt; 239 form.selectAll = $this.find(opt.selectAll); 240 form.button = $this.find(opt.submitButton); 241 } 242 243 /** 244 * Registers a multi-edit option. 245 * 246 * @param {object} options 247 * @param {string} options.label The option's label 248 * @param {string} options.value The option's value 249 * @param {string} options.html The second step HTML 250 * @return {object} methods 251 */ 252 253 methods.addOption = function (options) 254 { 255 var settings = $.extend({ 256 'label' : null, 257 'value' : null, 258 'html' : null 259 }, options); 260 261 if (!settings.value) { 262 return methods; 263 } 264 265 var option = form.editMethod.find('option').filter(function () 266 { 267 return $(this).val() === settings.value; 268 }); 269 270 var exists = (option.length > 0); 271 form.editMethod.val(''); 272 273 if (!exists) { 274 option = $('<option />'); 275 } 276 277 if (!option.data('_txpMultiMethod')) { 278 if (!option.val()) { 279 option.val(settings.value); 280 } 281 282 if (!option.text() && settings.label) { 283 option.text(settings.label); 284 } 285 286 option.data('_txpMultiMethod', settings.html); 287 } 288 289 if (!exists) { 290 form.editMethod.append(option); 291 } 292 293 return methods; 294 }; 295 296 /** 297 * Selects rows based on supplied arguments. 298 * 299 * Only one of the filters applies at a time. 300 * 301 * @param {object} options 302 * @param {array} options.index Indexes to select 303 * @param {array} options.range Select index range, takes [min, max] 304 * @param {array} options.value Values to select 305 * @param {boolean} options.checked TRUE to check, FALSE to uncheck 306 * @return {object} methods 307 */ 308 309 methods.select = function (options) 310 { 311 var settings = $.extend({ 312 'index' : null, 313 'range' : null, 314 'value' : null, 315 'checked' : true 316 }, options); 317 318 var obj = $this.find(form.boxes); 319 320 if (settings.value !== null) { 321 obj = obj.filter(function () 322 { 323 return $.inArray($(this).val(), settings.value) !== -1; 324 }); 325 } else if (settings.index !== null) { 326 obj = obj.filter(function (index) 327 { 328 return $.inArray(index, settings.index) !== -1; 329 }); 330 } else if (settings.range !== null) { 331 obj = obj.slice(settings.range[0], settings.range[1]); 332 } 333 334 obj.prop('checked', settings.checked).change(); 335 336 return methods; 337 }; 338 339 /** 340 * Highlights selected rows. 341 * 342 * @return {object} lib 343 */ 344 345 lib.highlight = function () 346 { 347 var element = $this.find(form.boxes); 348 element.filter(':checked').closest(opt.highlighted).addClass(opt.selectedClass); 349 element.filter(':not(:checked)').closest(opt.highlighted).removeClass(opt.selectedClass); 350 return lib; 351 }; 352 353 /** 354 * Extends click region to whole row. 355 * 356 * @return {object} lib 357 */ 358 359 lib.extendedClick = function () 360 { 361 if (opt.rowClick) { 362 var selector = opt.row; 363 } else { 364 var selector = form.boxes; 365 } 366 367 $this.on('click', selector, function (e) 368 { 369 var self = ($(e.target).is(form.boxes) || $(this).is(form.boxes)); 370 371 if (!self && (e.target != this || $(this).is('a, :input') || $(e.target).is('a, :input'))) { 372 return; 373 } 374 375 if (!self && opt.altClick && !e.altKey && !e.ctrlKey) { 376 return; 377 } 378 379 var box = $(this).closest(opt.highlighted).find(form.boxes); 380 381 if (box.length < 1) { 382 return; 383 } 384 385 var checked = box.prop('checked'); 386 387 if (self) { 388 checked = !checked; 389 } 390 391 if (e.shiftKey && form.lastCheck) { 392 var boxes = $this.find(form.boxes); 393 var start = boxes.index(box); 394 var end = boxes.index(form.lastCheck); 395 396 methods.select({ 397 'range' : [Math.min(start, end), Math.max(start, end) + 1], 398 'checked' : !checked 399 }); 400 } else if (!self) { 401 box.prop('checked', !checked).change(); 402 } 403 404 if (checked === false) { 405 form.lastCheck = box; 406 } else { 407 form.lastCheck = null; 408 } 409 }); 410 411 return lib; 412 }; 413 414 /** 415 * Tracks row checks. 416 * 417 * @return {object} lib 418 */ 419 420 lib.checked = function () 421 { 422 $this.on('change', form.boxes, function (e) 423 { 424 var box = $(this); 425 var boxes = $this.find(form.boxes); 426 427 if (box.prop('checked')) { 428 $(this).closest(opt.highlighted).addClass(opt.selectedClass); 429 $this.find(opt.selectAll).prop('checked', boxes.filter(':checked').length === boxes.length); 430 } else { 431 $(this).closest(opt.highlighted).removeClass(opt.selectedClass); 432 $this.find(opt.selectAll).prop('checked', false); 433 } 434 }); 435 436 return lib; 437 }; 438 439 /** 440 * Handles edit method selecting. 441 * 442 * @return {object} lib 443 */ 444 445 lib.changeMethod = function () 446 { 447 form.button.hide(); 448 449 form.editMethod.val('').change(function (e) 450 { 451 var selected = $(this).find('option:selected'); 452 $this.find('.multi-step').remove(); 453 454 if (selected.length < 1 || selected.val() === '') { 455 form.button.hide(); 456 return lib; 457 } 458 459 if (selected.data('_txpMultiMethod')) { 460 $(this).after($('<div />').attr('class', 'multi-step multi-option').html(selected.data('_txpMultiMethod'))); 461 form.button.show(); 462 } else { 463 form.button.hide(); 464 $(this).parents('form').submit(); 465 } 466 }); 467 468 return lib; 469 }; 470 471 /** 472 * Handles sending. 473 * 474 * @return {object} lib 475 */ 476 477 lib.sendForm = function () 478 { 479 $this.submit(function () 480 { 481 if (opt.confirmation !== false && verify(opt.confirmation) === false) { 482 form.editMethod.val('').change(); 483 484 return false; 485 } 486 }); 487 488 return lib; 489 }; 490 491 if (!$this.data('_txpMultiEdit')) { 492 lib.highlight().extendedClick().checked().changeMethod().sendForm(); 493 494 (function () 495 { 496 var multiOptions = $this.find('.multi-option:not(.multi-step)'); 497 498 form.editMethod.find('option[value!=""]').each(function () 499 { 500 var value = $(this).val(); 501 502 var option = multiOptions.filter(function () 503 { 504 return $(this).data('multi-option') === value; 505 }); 506 507 if (option.length > 0) { 508 methods.addOption({ 509 'label' : null, 510 'html' : option.eq(0).contents(), 511 'value' : $(this).val() 512 }); 513 } 514 }); 515 516 multiOptions.remove(); 517 })(); 518 519 $this.on('change', opt.selectAll, function (e) 520 { 521 methods.select({ 522 'checked' : $(this).prop('checked') 523 }); 524 }); 525 } 526 527 if (method && methods[method]) { 528 methods[method].call($this, args); 529 } 530 531 $this.data('_txpMultiEdit', form); 532 }); 533 534 return this; 535 }; 536 537 /** 538 * Adds an event handler. 539 * 540 * See jQuery before trying to use this. 541 * 542 * @author S.Andrew http://www.scottandrew.com/ 543 * @param {object} elm The element to attach to 544 * @param {string} evType The event 545 * @param {object} fn The callback function 546 * @param {boolean} useCapture Initiate capture 547 */ 548 549 function addEvent(elm, evType, fn, useCapture) 550 { 551 if (elm.addEventListener) { 552 elm.addEventListener(evType, fn, useCapture); 553 return true; 554 } else if (elm.attachEvent) { 555 var r = elm.attachEvent('on' + evType, fn); 556 return r; 557 } else { 558 elm['on' + evType] = fn; 559 } 560 } 561 562 /** 563 * Sets a HTTP cookie. 564 * 565 * @param {string} name The name 566 * @param {string} value The value 567 * @param {integer} days Expires in 568 */ 569 570 function setCookie(name, value, days) 571 { 572 if (days) { 573 var date = new Date(); 574 575 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 576 577 var expires = '; expires=' + date.toGMTString(); 578 } else { 579 var expires = ''; 580 } 581 582 document.cookie = name + '=' + value + expires + '; path=/'; 583 } 584 585 /** 586 * Gets a HTTP cookie's value. 587 * 588 * @param {string} name The name 589 * @return {string} The cookie 590 */ 591 592 function getCookie(name) 593 { 594 var nameEQ = name + '='; 595 var ca = document.cookie.split(';'); 596 597 for (var i = 0; i < ca.length; i++) { 598 var c = ca[i]; 599 600 while (c.charAt(0) == ' ') { 601 c = c.substring(1, c.length); 602 } 603 604 if (c.indexOf(nameEQ) == 0) { 605 return c.substring(nameEQ.length, c.length); 606 } 607 } 608 609 return null; 610 } 611 612 /** 613 * Deletes a HTTP cookie. 614 * 615 * @param {string} name The cookie 616 */ 617 618 function deleteCookie(name) 619 { 620 setCookie(name, '', -1); 621 } 622 623 /** 624 * Gets element by class. 625 * 626 * See jQuery before trying to use this. 627 * 628 * @param {string} classname The HTML class 629 * @param {object} node The node, defaults to the document 630 * @return {object} Matching nodes 631 * @see http://www.snook.ca/archives/javascript/your_favourite_1/ 632 */ 633 634 function getElementsByClass(classname, node) 635 { 636 var a = []; 637 var re = new RegExp('(^|\\s)' + classname + '(\\s|$)'); 638 639 if (node == null) { 640 node = document; 641 } 642 643 var els = node.getElementsByTagName("*"); 644 645 for (var i = 0, j = els.length; i < j; i++) { 646 if (re.test(els[i].className)) { 647 a.push(els[i]); 648 } 649 } 650 651 return a; 652 } 653 654 /** 655 * Toggles panel's visibility and saves the state to the server. 656 * 657 * @param {string} id The element ID 658 * @return {boolean} Returns FALSE 659 */ 660 661 function toggleDisplay(id) 662 { 663 var obj = $('#' + id); 664 665 if (obj.length) { 666 obj.toggle(); 667 668 // Send state of toggle pane to localStorage or server. 669 if ($(this).data('txp-pane')) { 670 var pane = $(this).data('txp-pane'); 671 672 if (!window.localStorage && $(this).data('txp-token')) { 673 sendAsyncEvent({ 674 event : 'pane', 675 step : 'visible', 676 pane : $(this).data('txp-pane'), 677 visible : obj.is(':visible'), 678 origin : textpattern.event, 679 token : $(this).data('txp-token') 680 }); 681 } 682 } else { 683 var pane = obj.attr('id'); 684 685 if (!window.localStorage) { 686 sendAsyncEvent({ 687 event : textpattern.event, 688 step : 'save_pane_state', 689 pane : obj.attr('id'), 690 visible : obj.is(':visible') 691 }); 692 } 693 } 694 695 var data = new Object; 696 data[pane] = obj.is(':visible'); 697 textpattern.storage.update(data); 698 } 699 700 return false; 701 } 702 703 /** 704 * Direct show/hide referred #segment; decorate parent lever. 705 */ 706 707 function toggleDisplayHref() 708 { 709 var $this = $(this); 710 var href = $this.attr('href'); 711 var lever = $this.parent('.txp-summary'); 712 713 if (href) { 714 toggleDisplay.call(this, href.substr(1)); 715 } 716 717 if (lever.length) { 718 var vis = $(href).is(':visible'); 719 lever.toggleClass('expanded', vis); 720 $this.attr('aria-pressed', vis.toString()); 721 $(href).attr('aria-expanded', vis.toString()); 722 } 723 724 return false; 725 } 726 727 /** 728 * Shows/hides matching elements. 729 * 730 * @param {string} className Targeted element's class 731 * @param {boolean} show TRUE to display 732 */ 733 734 function setClassDisplay(className, show) 735 { 736 $('.' + className).toggle(show); 737 } 738 739 /** 740 * Toggles panel's visibility and saves the state to a HTTP cookie. 741 * 742 * @param {string} classname The HTML class 743 */ 744 745 function toggleClassRemember(className) 746 { 747 var v = getCookie('toggle_' + className); 748 v = (v == 1 ? 0 : 1); 749 750 setCookie('toggle_' + className, v, 365); 751 setClassDisplay(className, v); 752 setClassDisplay(className + '_neg', 1 - v); 753 } 754 755 /** 756 * Toggle visibility of matching elements based on a cookie value. 757 * 758 * @param {string} className The HTML class 759 * @param {string} force The value 760 */ 761 762 function setClassRemember(className, force) 763 { 764 if (typeof(force) != 'undefined') { 765 setCookie('toggle_' + className, force, 365); 766 } 767 768 var v = getCookie('toggle_' + className); 769 setClassDisplay(className, v); 770 setClassDisplay(className + '_neg', 1 - v); 771 } 772 773 /** 774 * Load data from the server using a HTTP POST request. 775 * 776 * @param {object} data POST payload 777 * @param {object} fn Success handler 778 * @param {string} format Response data format, defaults to 'xml' 779 * @return {object} this 780 * @see http://api.jquery.com/jQuery.post/ 781 */ 782 783 function sendAsyncEvent (data, fn, format) 784 { 785 var formdata = false; 786 if ($.type(data) === 'string' && data.length > 0) { 787 // Got serialized data. 788 data = data + '&app_mode=async&_txp_token=' + textpattern._txp_token; 789 } else if (data instanceof FormData) { 790 formdata = true; 791 data.append("app_mode", 'async'); 792 data.append("_txp_token", textpattern._txp_token); 793 } else { 794 data.app_mode = 'async'; 795 data._txp_token = textpattern._txp_token; 796 } 797 798 format = format || 'xml'; 799 800 return formdata ? 801 $.ajax({ 802 type: "POST", 803 url: 'index.php', 804 data: data, 805 success: fn, 806 dataType: format, 807 processData: false, 808 contentType: false 809 }) : 810 $.post('index.php', data, fn, format); 811 } 812 813 /** 814 * A pub/sub hub for client-side events. 815 * 816 * @since 4.5.0 817 */ 818 819 textpattern.Relay = 820 { 821 /** 822 * Publishes an event to all registered subscribers. 823 * 824 * @param {string} event The event 825 * @param {object} data The data passed to registered subscribers 826 * @return {object} The Relay object 827 * @example 828 * textpattern.Relay.callback('newEvent', {'name1' : 'value1', 'name2' : 'value2'}); 829 */ 830 831 callback: function (event, data) 832 { 833 return $(this).trigger(event, data); 834 }, 835 836 /** 837 * Subscribes to an event. 838 * 839 * @param {string} The event 840 * @param {object} fn The callback function 841 * @return {object} The Relay object 842 * @example 843 * textpattern.Relay.register('event', 844 * function (event, data) 845 * { 846 * alert(data); 847 * } 848 * ); 849 */ 850 851 register: function (event, fn) 852 { 853 $(this).on(event, fn); 854 return this; 855 } 856 }; 857 858 /** 859 * Textpattern localStorage. 860 * 861 * @since 4.6.0 862 */ 863 864 textpattern.storage = 865 { 866 /** 867 * Textpattern localStorage data. 868 */ 869 870 data : (window.localStorage ? JSON.parse(window.localStorage.getItem("textpattern")) : null) || {}, 871 872 /** 873 * Updates data. 874 * 875 * @param data The message 876 * @example 877 * textpattern.update({prefs : "site"}); 878 */ 879 880 update : function (data) { 881 882 if (!window.localStorage) { 883 return; 884 } 885 886 if (data) { 887 $.extend(textpattern.storage.data, data); 888 window.localStorage.setItem("textpattern", JSON.stringify(textpattern.storage.data)); 889 } 890 } 891 }; 892 893 /** 894 * Logs debugging messages. 895 * 896 * @since 4.6.0 897 */ 898 899 textpattern.Console = 900 { 901 /** 902 * Stores an array of invoked messages. 903 */ 904 905 history : [], 906 907 /** 908 * Logs a message. 909 * 910 * @param message The message 911 * @return textpattern.Console 912 * @example 913 * textpattern.Console.log('Some message'); 914 */ 915 916 log : function (message) 917 { 918 if (textpattern.production_status === 'debug') { 919 textpattern.Console.history.push(message); 920 921 textpattern.Relay.callback('txpConsoleLog', { 922 'message' : message 923 }); 924 } 925 926 return this; 927 } 928 }; 929 930 /** 931 * Console API module for textpattern.Console. 932 * 933 * Passes invoked messages to Web/JavaScript Console 934 * using console.log(). 935 * 936 * Uses a namespaced 'txpConsoleLog.ConsoleAPI' event. 937 */ 938 939 textpattern.Relay.register('txpConsoleLog.ConsoleAPI', function (event, data) 940 { 941 if ($.type(console) === 'object' && $.type(console.log) === 'function') { 942 console.log(data.message); 943 } 944 }); 945 946 /** 947 * Script routing. 948 * 949 * @since 4.6.0 950 */ 951 952 textpattern.Route = 953 { 954 /** 955 * An array of attached listeners. 956 */ 957 958 attached : [], 959 960 /** 961 * Attaches a listener. 962 * 963 * @param {string} pages The page 964 * @param {object} fn The callback 965 */ 966 967 add : function (pages, fn) 968 { 969 $.each(pages.split(','), function (index, page) 970 { 971 textpattern.Route.attached.push({ 972 'page' : $.trim(page), 973 'fn' : fn 974 }); 975 }); 976 }, 977 978 /** 979 * Initializes attached listeners. 980 * 981 * @param {object} options Options 982 * @param {string} options.event The event 983 * @param {string} options.step The step 984 */ 985 986 init : function (options) 987 { 988 var options = $.extend({ 989 'event' : textpattern.event, 990 'step' : textpattern.step 991 }, options); 992 993 $.each(textpattern.Route.attached, function (index, data) 994 { 995 if (data.page === '' || data.page === options.event || data.page === options.event + '.' + options.step) { 996 data.fn({ 997 'event' : options.event, 998 'step' : options.step, 999 'route' : data.page 1000 }); 1001 } 1002 }); 1003 } 1004 }; 1005 1006 /** 1007 * Sends a form using AJAX and processes the response. 1008 * 1009 * @param {object} options Options 1010 * @param {string} options.dataType The response data type 1011 * @param {object} options.success The success callback 1012 * @param {object} options.error The error callback 1013 * @return {object} this 1014 * @since 4.5.0 1015 */ 1016 1017 jQuery.fn.txpAsyncForm = function (options) 1018 { 1019 options = $.extend({ 1020 dataType : 'script', 1021 success : null, 1022 error : null 1023 }, options); 1024 1025 // Send form data to application, process response as script. 1026 this.on('submit.txpAsyncForm', function (event) 1027 { 1028 event.preventDefault(); 1029 1030 var $this = $(this); 1031 var form = 1032 { 1033 button : $this.find('input[type="submit"]:focus').eq(0), 1034 data : ( window.FormData === undefined ? $this.serialize() : new FormData(this) ), 1035 spinner : $('<span />').addClass('spinner') 1036 }; 1037 1038 // Show feedback while processing. 1039 $this.addClass('busy'); 1040 $('body').addClass('busy'); 1041 1042 // WebKit does not set :focus on button-click: use first submit input as a fallback. 1043 if (!form.button.length) { 1044 form.button = $this.find('input[type="submit"]').eq(0); 1045 } 1046 1047 form.button.attr('disabled', true).after(form.spinner); 1048 1049 if (form.data) 1050 if ( form.data instanceof FormData ) { 1051 form.data.append(form.button.attr('name') || '_txp_submit' , form.button.val() || '_txp_submit'); 1052 } else { 1053 form.data += '&' + (form.button.attr('name') || '_txp_submit') + '=' + (form.button.val() || '_txp_submit'); 1054 } 1055 1056 sendAsyncEvent(form.data, function () {}, options.dataType) 1057 .done(function (data, textStatus, jqXHR) 1058 { 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 { 1073 if (options.error) { 1074 options.error($this, event, jqXHR, $.ajaxSetup(), errorThrown); 1075 } 1076 1077 textpattern.Relay.callback('txpAsyncForm.error', { 1078 'this' : $this, 1079 'event' : event, 1080 'jqXHR' : jqXHR, 1081 'ajaxSettings' : $.ajaxSetup(), 1082 'thrownError' : errorThrown 1083 }); 1084 }) 1085 .always(function () 1086 { 1087 $this.removeClass('busy'); 1088 form.button.removeAttr('disabled'); 1089 form.spinner.remove(); 1090 $('body').removeClass('busy'); 1091 }); 1092 }); 1093 1094 return this; 1095 }; 1096 1097 /** 1098 * Sends a link using AJAX and processes the plain text response. 1099 * 1100 * @param {object} options Options 1101 * @param {string} options.dataType The response data type 1102 * @param {object} options.success The success callback 1103 * @param {object} options.error The error callback 1104 * @return {object} this 1105 * @since 4.5.0 1106 */ 1107 1108 jQuery.fn.txpAsyncHref = function (options) 1109 { 1110 options = $.extend({ 1111 dataType : 'text', 1112 success : null, 1113 error : null 1114 }, options); 1115 1116 this.on('click.txpAsyncHref', function (event) 1117 { 1118 event.preventDefault(); 1119 var $this = $(this); 1120 var url = this.search.replace('?', '') + '&' + $.param({value : $this.text()}); 1121 1122 // Show feedback while processing. 1123 $this.addClass('busy'); 1124 $('body').addClass('busy'); 1125 1126 sendAsyncEvent(url, function () {}, options.dataType) 1127 .done(function (data, textStatus, jqXHR) 1128 { 1129 if (options.dataType === 'text') { 1130 $this.html(data); 1131 } 1132 1133 if (options.success) { 1134 options.success($this, event, data, textStatus, jqXHR); 1135 } 1136 1137 textpattern.Relay.callback('txpAsyncHref.success', { 1138 'this' : $this, 1139 'event' : event, 1140 'data' : data, 1141 'textStatus' : textStatus, 1142 'jqXHR' : jqXHR 1143 }); 1144 }) 1145 .fail(function (jqXHR, textStatus, errorThrown) 1146 { 1147 if (options.error) { 1148 options.error($this, event, jqXHR, $.ajaxSetup(), errorThrown); 1149 } 1150 1151 textpattern.Relay.callback('txpAsyncHref.error', { 1152 'this' : $this, 1153 'event' : event, 1154 'jqXHR' : jqXHR, 1155 'ajaxSettings' : $.ajaxSetup(), 1156 'thrownError' : errorThrown 1157 }); 1158 }) 1159 .always(function () 1160 { 1161 $this.removeClass('busy'); 1162 $('body').removeClass('busy'); 1163 }); 1164 }); 1165 1166 return this; 1167 }; 1168 1169 /** 1170 * Sends a link using AJAX and processes the HTML response. 1171 * 1172 * @param {object} options Options 1173 * @param {string} options.dataType The response data type 1174 * @param {object} options.success The success callback 1175 * @param {object} options.error The error callback 1176 * @return {object} this 1177 * @since 4.6.0 1178 */ 1179 1180 function txpAsyncLink(event) 1181 { 1182 event.preventDefault(); 1183 var $this = $(event.target); 1184 var url = $this.attr('href').replace('?', ''); 1185 1186 // Show feedback while processing. 1187 $this.addClass('busy'); 1188 $('body').addClass('busy'); 1189 1190 sendAsyncEvent(url, function () {}, 'html') 1191 .done(function (data, textStatus, jqXHR) 1192 { 1193 textpattern.Relay.callback('txpAsyncLink.success', { 1194 'this' : $this, 1195 'event' : event, 1196 'data' : data, 1197 'textStatus' : textStatus, 1198 'jqXHR' : jqXHR 1199 }); 1200 }) 1201 .fail(function (jqXHR, textStatus, errorThrown) 1202 { 1203 textpattern.Relay.callback('txpAsyncLink.error', { 1204 'this' : $this, 1205 'event' : event, 1206 'jqXHR' : jqXHR, 1207 'ajaxSettings' : $.ajaxSetup(), 1208 'thrownError' : errorThrown 1209 }); 1210 }) 1211 .always(function () 1212 { 1213 $this.removeClass('busy'); 1214 $('body').removeClass('busy'); 1215 }); 1216 1217 return this; 1218 }; 1219 1220 /** 1221 * Creates a UI dialog. 1222 * 1223 * @param {object} options Options 1224 * @return {object} this 1225 * @since 4.6.0 1226 */ 1227 1228 jQuery.fn.txpDialog = function (options) 1229 { 1230 options = $.extend({ 1231 autoOpen : false, 1232 buttons : [ 1233 { 1234 text : textpattern.gTxt('ok'), 1235 click : function () 1236 { 1237 // callbacks? 1238 1239 if ($(this).is('form')) { 1240 $(this).submit(); 1241 } 1242 1243 $(this).dialog('close'); 1244 } 1245 } 1246 ] 1247 }, options); 1248 1249 this.dialog(options); 1250 1251 return this; 1252 }; 1253 1254 /** 1255 * Creates a date picker. 1256 * 1257 * @param {object} options Options 1258 * @return {object} this 1259 * @since 4.6.0 1260 */ 1261 1262 jQuery.fn.txpDatepicker = function (options) 1263 { 1264 // TODO $.datepicker.regional[ "en" ]; 1265 // TODO support from RTL languages 1266 this.datepicker(options); 1267 1268 return this; 1269 }; 1270 1271 /** 1272 * Creates a sortable element. 1273 * 1274 * This method creates a sortable widget, allowing to 1275 * reorder elements in a list and synchronizes the updated 1276 * order with the server. 1277 * 1278 * @param {object} options 1279 * @param {string} options.dataType The response datatype 1280 * @param {object} options.success The sync success callback 1281 * @param {object} options.error The sync error callback 1282 * @param {string} options.event The event 1283 * @param {string} options.step The step 1284 * @param {string} options.cancel Prevents sorting if you start on elements matching the selector 1285 * @param {integer} options.delay Sorting delay 1286 * @param {integer} options.distance Tolerance, in pixels, for when sorting should start 1287 * @return this 1288 * @since 4.6.0 1289 */ 1290 1291 jQuery.fn.txpSortable = function (options) 1292 { 1293 options = $.extend({ 1294 dataType : 'script', 1295 success : null, 1296 error : null, 1297 event : textpattern.event, 1298 step : 'sortable_save', 1299 cancel : ':input, button', 1300 delay : 0, 1301 distance : 15, 1302 items : '[data-txp-sortable-id]' 1303 }, options); 1304 1305 var methods = 1306 { 1307 /** 1308 * Sends updated order to the server. 1309 */ 1310 1311 update : function () 1312 { 1313 var ids = [], $this = $(this); 1314 1315 $this.children('[data-txp-sortable-id]').each(function () 1316 { 1317 ids.push($(this).data('txp-sortable-id')); 1318 }); 1319 1320 if (ids) { 1321 sendAsyncEvent({ 1322 event : options.event, 1323 step : options.step, 1324 order : ids 1325 }, function () {}, options.dataType) 1326 .done(function (data, textStatus, jqXHR) 1327 { 1328 if (options.success) { 1329 options.success.call($this, data, textStatus, jqXHR); 1330 } 1331 1332 textpattern.Relay.callback('txpSortable.success', { 1333 'this' : $this, 1334 'data' : data, 1335 'textStatus' : textStatus, 1336 'jqXHR' : jqXHR 1337 }); 1338 }) 1339 .fail(function (jqXHR, textStatus, errorThrown) 1340 { 1341 if (options.error) { 1342 options.error.call($this, jqXHR, $.ajaxSetup(), errorThrown); 1343 } 1344 1345 textpattern.Relay.callback('txpSortable.error', { 1346 'this' : $this, 1347 'jqXHR' : jqXHR, 1348 'ajaxSettings' : $.ajaxSetup(), 1349 'thrownError' : errorThrown 1350 }); 1351 }); 1352 } 1353 } 1354 }; 1355 1356 return this.sortable({ 1357 cancel : options.cancel, 1358 delay : options.delay, 1359 distance : options.distance, 1360 update : methods.update, 1361 items : options.items 1362 }); 1363 }; 1364 1365 1366 /** 1367 * Password strength meter. 1368 * 1369 * @since 4.6.0 1370 * @param {object} options 1371 * @param {array} options.gtxt_prefix gTxt() string prefix 1372 * @todo Pass in name/email via 'options' to be injected in user_inputs[] 1373 */ 1374 1375 textpattern.passwordStrength = function (options) 1376 { 1377 jQuery('form').on('keyup', 'input.txp-strength-hint', function() { 1378 var settings = $.extend({ 1379 'gtxt_prefix' : '' 1380 }, options); 1381 1382 var me = jQuery(this); 1383 var pass = me.val(); 1384 var passResult = zxcvbn(pass, user_inputs=[]); 1385 var strengthMap = { 1386 "0": { 1387 "width": "5" 1388 }, 1389 "1": { 1390 "width": "28" 1391 }, 1392 "2": { 1393 "width": "50" 1394 }, 1395 "3": { 1396 "width": "75" 1397 }, 1398 "4": { 1399 "width": "100" 1400 } 1401 }; 1402 1403 var offset = strengthMap[passResult.score]; 1404 var meter = me.siblings('.strength-meter'); 1405 meter.empty(); 1406 1407 if (pass.length > 0) { 1408 meter.append('<div class="bar"></div><div class="indicator">' + textpattern.gTxt(settings.gtxt_prefix+'password_strength_'+passResult.score) + '</div>'); 1409 } 1410 1411 meter 1412 .find('.bar') 1413 .attr('class', 'bar password-strength-'+passResult.score) 1414 .css('width', offset.width+'%'); 1415 }); 1416 } 1417 1418 /** 1419 * Mask/unmask password input field. 1420 * 1421 * @since 4.6.0 1422 */ 1423 1424 textpattern.passwordMask = function() 1425 { 1426 $('form').on('click', '#show_password', function() { 1427 var inputBox = $(this).closest('form').find('input.txp-maskable'); 1428 var newType = (inputBox.attr('type') === 'password') ? 'text' : 'password'; 1429 textpattern.changeType(inputBox, newType); 1430 }); 1431 } 1432 1433 /** 1434 * Change the type of an input element. 1435 * 1436 * @param {object} elem The <input/> element 1437 * @param {string} type The desired type 1438 * 1439 * @see https://gist.github.com/3559343 for original 1440 * @since 4.6.0 1441 */ 1442 1443 textpattern.changeType = function(elem, type) 1444 { 1445 if (elem.prop('type') === type) { 1446 // Already the correct type. 1447 return elem; 1448 } 1449 1450 try { 1451 // May fail if browser prevents it. 1452 return elem.prop('type', type); 1453 } catch(e) { 1454 // Create the element by hand. 1455 // Clone it via a div (jQuery has no html() method for an element). 1456 var html = $("<div>").append(elem.clone()).html(); 1457 1458 // Match existing attributes of type=text or type="text". 1459 var regex = /type=(\")?([^\"\s]+)(\")?/; 1460 1461 // If no match, add the type attribute to the end; otherwise, replace it. 1462 var tmp = $(html.match(regex) == null ? 1463 html.replace(">", ' type="' + type + '">') : 1464 html.replace(regex, 'type="' + type + '"')); 1465 1466 // Copy data from old element. 1467 tmp.data('type', elem.data('type')); 1468 var events = elem.data('events'); 1469 var cb = function(events) { 1470 return function() { 1471 // Re-bind all prior events. 1472 for(var idx in events) { 1473 var ydx = events[idx]; 1474 1475 for(var jdx in ydx) { 1476 tmp.bind(idx, ydx[jdx].handler); 1477 } 1478 } 1479 } 1480 }(events); 1481 1482 elem.replaceWith(tmp); 1483 1484 // Wait a smidge before firing callback. 1485 setTimeout(cb, 10); 1486 1487 return tmp; 1488 } 1489 } 1490 1491 /** 1492 * Encodes a string for a use in HTML. 1493 * 1494 * @param {string} string The string 1495 * @return {string} Encoded string 1496 * @since 4.6.0 1497 */ 1498 1499 textpattern.encodeHTML = function (string) 1500 { 1501 return $('<div/>').text(string).html(); 1502 }; 1503 1504 /** 1505 * Translates given substrings. 1506 * 1507 * @param {string} string The string being translated 1508 * @param {object} replacements Translated substrings 1509 * @return string Translated string 1510 * @since 4.6.0 1511 * @example 1512 * textpattern.tr('hello world, and bye!', {'hello' : 'bye', 'bye' : 'hello'}); 1513 */ 1514 1515 textpattern.tr = function (string, replacements) 1516 { 1517 var match, position, output = '', replacement; 1518 1519 for (position = 0; position < string.length; position++) { 1520 match = false; 1521 1522 $.each(replacements, function (from, to) 1523 { 1524 if (string.substr(position, from.length) === from) { 1525 match = true; 1526 replacement = to; 1527 position = (position + from.length) - 1; 1528 1529 return; 1530 } 1531 }); 1532 1533 if (match) { 1534 output += replacement; 1535 } else { 1536 output += string.charAt(position); 1537 } 1538 } 1539 1540 return output; 1541 }; 1542 1543 /** 1544 * Returns an i18n string. 1545 * 1546 * @param {string} i18n The i18n string 1547 * @param {object} atts Replacement map 1548 * @param {boolean} escape TRUE to escape HTML in atts 1549 * @return {string} The string 1550 * @example 1551 * textpattern.gTxt('string', {'{name}' : 'example'}, true); 1552 */ 1553 1554 textpattern.gTxt = function (i18n, atts, escape) 1555 { 1556 var tags = atts || {}; 1557 var string = i18n; 1558 var name = string.toLowerCase(); 1559 1560 if ($.type(textpattern.textarray[name]) !== 'undefined') { 1561 string = textpattern.textarray[name]; 1562 } 1563 1564 if (escape !== false) { 1565 string = textpattern.encodeHTML(string); 1566 1567 $.each(tags, function (key, value) 1568 { 1569 tags[key] = textpattern.encodeHTML(value); 1570 }); 1571 } 1572 1573 string = textpattern.tr(string, tags); 1574 1575 return string; 1576 }; 1577 1578 /** 1579 * Replaces HTML contents of each matched with i18n string. 1580 * 1581 * This is a jQuery plugin for textpattern.gTxt(). 1582 * 1583 * @param {object|string} options Options or the i18n string 1584 * @param {string} options.string The i18n string 1585 * @param {object} options.tags Replacement map 1586 * @param {boolean} options.escape TRUE to escape HTML in tags 1587 * @param {object} tags Replacement map 1588 * @param {boolean} escape TRUE to escape HTML in tags 1589 * @return {object} this 1590 * @see textpattern.gTxt() 1591 * @example 1592 * $('p').gTxt('string').class('alert-block warning'); 1593 */ 1594 1595 jQuery.fn.gTxt = function (opts, tags, escape) 1596 { 1597 var options = $.extend({ 1598 'string' : opts, 1599 'tags' : tags, 1600 'escape' : escape 1601 }, opts); 1602 1603 this.html(textpattern.gTxt(options.string, options.tags, options.escape)); 1604 1605 return this; 1606 }; 1607 1608 /** 1609 * ESC button closes alert messages. 1610 * 1611 * @since 4.5.0 1612 */ 1613 1614 $(document).keyup(function (e) 1615 { 1616 if (e.keyCode == 27) { 1617 $('.close').parent().remove(); 1618 } 1619 }); 1620 1621 /** 1622 * Search tool. 1623 * 1624 * @since 4.6.0 1625 */ 1626 1627 function txp_search() 1628 { 1629 var $ui = $('.txp-search'); 1630 1631 $ui.find('.txp-search-button').button({ 1632 showLabel: false, 1633 icon: 'ui-icon-search' 1634 }).click(function () 1635 { 1636 $ui.submit(); 1637 }); 1638 1639 $ui.find('.txp-search-options').button({ 1640 showLabel: false, 1641 icon: 'ui-icon-triangle-1-s' 1642 }).on('click', function (e) 1643 { 1644 if (langdir === 'rtl') { 1645 var menu = $ui.find('.txp-dropdown').toggle().position( 1646 { 1647 my: "left top", 1648 at: "left bottom", 1649 of: this 1650 }); 1651 } else { 1652 var menu = $ui.find('.txp-dropdown').toggle().position( 1653 { 1654 my: "right top", 1655 at: "right bottom", 1656 of: this 1657 }); 1658 }; 1659 1660 $(document).one('click blur', function () 1661 { 1662 menu.hide(); 1663 }); 1664 1665 return false; 1666 }); 1667 1668 $ui.find('.txp-search-buttons').controlgroup(); 1669 $ui.find('.txp-dropdown').hide().menu().click(function (e) { 1670 e.stopPropagation(); 1671 }); 1672 1673 $ui.txpMultiEditForm({ 1674 'checkbox' : 'input[name="search_method[]"][type=checkbox]', 1675 'row' : '.txp-dropdown li', 1676 'highlighted' : '.txp-dropdown li', 1677 'confirmation': false 1678 }); 1679 } 1680 1681 /** 1682 * Set expanded/collapsed nature of all twisty boxes in a panel. 1683 * 1684 * The direction can either be 'expand' or 'collapse', passed 1685 * in as an argument to the handler. 1686 * 1687 * @param {event} ev Event that triggered the function 1688 * @since 4.6.0 1689 */ 1690 1691 function txp_expand_collapse_all(ev) { 1692 ev.preventDefault(); 1693 1694 var direction = ev.data.direction, 1695 container = ev.data.container || (ev.delegateTarget == ev.target ? 'body' : ev.delegateTarget); 1696 1697 $(container).find('.txp-summary a').each(function (i, elm) { 1698 var $elm = $(elm); 1699 1700 if (direction === 'collapse') { 1701 if ($elm.parent(".txp-summary").hasClass("expanded")) { 1702 $elm.click(); 1703 } 1704 } else { 1705 if (!$elm.parent(".txp-summary").hasClass("expanded")) { 1706 $elm.click(); 1707 } 1708 } 1709 }); 1710 } 1711 1712 /** 1713 * Restore sub-panel twistys to their as-stored state. 1714 * 1715 * @return {[type]} [description] 1716 */ 1717 jQuery.fn.restorePanes = function () 1718 { 1719 // Initialize dynamic WAI-ARIA attributes. 1720 $(this).find('.txp-summary a').each(function (i, elm) 1721 { 1722 // Get id of toggled <section> region. 1723 var $elm = $(elm), region = $elm.attr('href'); 1724 1725 if (region) { 1726 1727 var $region = $(region); 1728 region = region.substr(1); 1729 1730 var pane = $elm.data("txp-pane"); 1731 1732 if (pane === undefined) { 1733 pane = region; 1734 } 1735 1736 if (textpattern.storage.data[pane] !== undefined) { 1737 if (textpattern.storage.data[pane]) { 1738 $elm.parent(".txp-summary").addClass("expanded"); 1739 $region.show(); 1740 } else { 1741 $elm.parent(".txp-summary").removeClass("expanded"); 1742 $region.hide(); 1743 } 1744 } 1745 1746 var vis = $region.is(':visible').toString(); 1747 $elm.attr('aria-controls', region).attr('aria-pressed', vis); 1748 $region.attr('aria-expanded', vis); 1749 } 1750 }); 1751 } 1752 1753 /** 1754 * Cookie status. 1755 * 1756 * @deprecated in 4.6.0 1757 */ 1758 1759 var cookieEnabled = true; 1760 1761 // Setup panel. 1762 1763 textpattern.Route.add('setup', function () 1764 { 1765 textpattern.passwordMask(); 1766 textpattern.passwordStrength({ 1767 'gtxt_prefix' : 'setup_' 1768 }); 1769 }); 1770 1771 // Login panel. 1772 1773 textpattern.Route.add('login', function () 1774 { 1775 // Check cookies. 1776 if (!checkCookies()) { 1777 cookieEnabled = false; 1778 $('main').prepend($('<p class="alert-block warning" />').text(textpattern.gTxt('cookies_must_be_enabled'))); 1779 } 1780 1781 // Focus on either username or password when empty. 1782 $('#login_form input').each(function() { 1783 if (this.value === '') { 1784 this.focus(); 1785 return false; 1786 } 1787 }); 1788 1789 textpattern.passwordMask(); 1790 textpattern.passwordStrength(); 1791 }); 1792 1793 // Write panel. 1794 1795 textpattern.Route.add('article', function () 1796 { 1797 // Assume users would not change the timestamp if they wanted to 1798 // 'publish now'/'reset time'. 1799 $(document).on('change', 1800 '#write-timestamp input.year,' + 1801 '#write-timestamp input.month,' + 1802 '#write-timestamp input.day,' + 1803 '#write-timestamp input.hour,' + 1804 '#write-timestamp input.minute,' + 1805 '#write-timestamp input.second', 1806 function () 1807 { 1808 $('#publish_now').prop('checked', false); 1809 $('#reset_time').prop('checked', false); 1810 } 1811 ); 1812 1813 var status = $('select[name=Status]'), form = status.parents('form'), submitButton = form.find('input[type=submit]'); 1814 1815 status.change(function () 1816 { 1817 if (!form.hasClass('published')) { 1818 if ($(this).val() < 4) { 1819 submitButton.val(textpattern.gTxt('save')); 1820 } else { 1821 submitButton.val(textpattern.gTxt('publish')); 1822 } 1823 } 1824 }); 1825 1826 $('.txp-actions').on('click', '.txp-clone', function (e) 1827 { 1828 e.preventDefault(); 1829 form.append('<input type="hidden" name="copy" value="1" />'+ 1830 '<input type="hidden" name="publish" value="1" />'); 1831 form.off('submit.txpAsyncForm').trigger('submit'); 1832 }); 1833 1834 // Switch to Text/HTML/Preview mode. 1835 $(document).on('click', 1836 '[data-view-mode]', 1837 function (e) 1838 { 1839 e.preventDefault(); 1840 $('input[name="view"]').val($(this).data('view-mode')); 1841 document.article_form.submit(); 1842 } 1843 ); 1844 }); 1845 1846 // Uncheck reset on timestamp change. 1847 1848 textpattern.Route.add('article, file', function () 1849 { 1850 $(document).on('change', '.posted input', function (e) 1851 { 1852 $('#publish_now, #reset_time').prop('checked', false); 1853 }); 1854 }); 1855 1856 // 'Clone' button on Pages, Forms, Styles panels. 1857 1858 textpattern.Route.add('css, page, form', function () 1859 { 1860 $('.txp-clone').click(function (e) 1861 { 1862 e.preventDefault(); 1863 var target = $(this).data('form'); 1864 if (target) { 1865 $('#'+target).append('<input type="hidden" name="copy" value="1" />'); 1866 $('.txp-save input').click(); 1867 } 1868 }); 1869 }); 1870 1871 // Tagbuilder. 1872 1873 textpattern.Route.add('page, form, file, image', function () 1874 { 1875 // Set up asynchronous tag builder links. 1876 textpattern.Relay.register('txpAsyncLink.success', function (event, data) 1877 { 1878 $('#tagbuild_links').dialog('close').html($(data['data'])).dialog('open').restorePanes(); 1879 $('#txp-tagbuilder-output').select(); 1880 }); 1881 1882 textpattern.Relay.register('txpAsyncForm.success', function (event, data) 1883 { 1884 $('#tagbuild_links').html($(data['data'])); 1885 $('#txp-tagbuilder-output').select(); 1886 }); 1887 1888 $('#tagbuild_links, .files_detail, .images_detail').on('click', '.txp-tagbuilder-link', function(ev) { 1889 txpAsyncLink(ev); 1890 }); 1891 1892 $('#tagbuild_links').dialog({ 1893 dialogClass: 'txp-tagbuilder-container', 1894 autoOpen: false, 1895 focus: function(ev, ui) { 1896 $(ev.target).closest('.ui-dialog').focus(); 1897 } 1898 }); 1899 1900 $('.txp-tagbuilder-dialog').on('click', function(ev) { 1901 ev.preventDefault(); 1902 if ($("#tagbuild_links").dialog('isOpen')) { 1903 $("#tagbuild_links").dialog('close'); 1904 } else { 1905 $("#tagbuild_links").dialog('open'); 1906 } 1907 }); 1908 1909 // Set up delegated asynchronous tagbuilder form submission. 1910 $('#tagbuild_links').on('click', 'form.asynchtml input[type="submit"]', function(ev) { 1911 $(this).closest('form.asynchtml').txpAsyncForm({ 1912 dataType: 'html', 1913 error: function () 1914 { 1915 window.alert(textpattern.gTxt('form_submission_error')); 1916 }, 1917 success: function() 1918 { 1919 } 1920 }); 1921 }); 1922 }); 1923 1924 // Forms panel. 1925 1926 textpattern.Route.add('form', function () 1927 { 1928 $('#allforms_form').txpMultiEditForm({ 1929 'checkbox' : 'input[name="selected_forms[]"][type=checkbox]', 1930 'row' : '.switcher-list li, .form-list-name', 1931 'highlighted' : '.switcher-list li' 1932 }); 1933 }); 1934 1935 // Admin panel. 1936 1937 textpattern.Route.add('admin', function () 1938 { 1939 textpattern.passwordMask(); 1940 textpattern.passwordStrength(); 1941 }); 1942 1943 // Plugins panel. 1944 1945 textpattern.Route.add('plugin', function () 1946 { 1947 textpattern.Relay.register('txpAsyncHref.success', function (event, data) 1948 { 1949 $(data['this']).closest('tr').toggleClass('active'); 1950 }); 1951 }); 1952 1953 // All panels? 1954 1955 textpattern.Route.add('', function () 1956 { 1957 // Collapse/Expand all support. 1958 $('#supporting_content, #tagbuild_links, #content_switcher').on('click', '.txp-collapse-all', {direction: 'collapse'}, txp_expand_collapse_all) 1959 .on('click', '.txp-expand-all', {direction: 'expand'}, txp_expand_collapse_all); 1960 1961 // Pane states 1962 var prefsGroup = $('form:has(.switcher-list li a[data-txp-pane])'); 1963 1964 if (prefsGroup.length == 0) { 1965 return; 1966 } 1967 1968 var prefTabs = prefsGroup.find('.switcher-list li'); 1969 var $switchers = prefTabs.children('a[data-txp-pane]'); 1970 var $section = window.location.hash ? prefsGroup.find($(window.location.hash).closest('section')) : []; 1971 1972 if ($section.length) { 1973 selectedTab = $section.index(); 1974 } 1975 else if (textpattern.storage.data[textpattern.event] !== undefined) { 1976 $switchers.each(function (i, elm) { 1977 if ($(elm).data('txp-pane') == textpattern.storage.data[textpattern.event]) { 1978 selectedTab = i; 1979 $(elm).parent().addClass('ui-tabs-active ui-state-active'); 1980 } else { 1981 $(elm).parent().removeClass('ui-tabs-active ui-state-active'); 1982 } 1983 }); 1984 } 1985 1986 if (selectedTab === undefined) { 1987 selectedTab = 0; 1988 } 1989 1990 prefsGroup.tabs({active: selectedTab}).removeClass('ui-widget ui-widget-content ui-corner-all').addClass('ui-tabs-vertical'); 1991 prefsGroup.find('.switcher-list').removeClass('ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all'); 1992 prefTabs.removeClass('ui-state-default ui-corner-top'); 1993 prefsGroup.find('.txp-prefs-group').removeClass('ui-widget-content ui-corner-bottom'); 1994 1995 prefTabs.on('click focus', function(ev) 1996 { 1997 var me = $(this).children('a[data-txp-pane]'); 1998 1999 if (!window.localStorage) sendAsyncEvent({ 2000 event : 'pane', 2001 step : 'tabVisible', 2002 pane : me.data('txp-pane'), 2003 origin : textpattern.event, 2004 token : me.data('txp-token') 2005 }); 2006 2007 var data = new Object; 2008 data[textpattern.event] = me.data('txp-pane'); 2009 textpattern.storage.update(data); 2010 }); 2011 }); 2012 2013 // Initialize JavaScript. 2014 2015 $(document).ready(function () 2016 { 2017 // Confirmation dialogs. 2018 $(document).on('click.txpVerify', 'a[data-verify]', function (e) 2019 { 2020 return verify($(this).data('verify')); 2021 }); 2022 2023 $(document).on('submit.txpVerify', 'form[data-verify]', function (e) 2024 { 2025 return verify($(this).data('verify')); 2026 }); 2027 2028 // Disable spellchecking on all elements of class "code" in capable browsers. 2029 var c = $(".code")[0]; 2030 2031 if (c && "spellcheck" in c) { 2032 $(".code").prop("spellcheck", false); 2033 } 2034 2035 // Enable spellcheck for all elements mentioned in textpattern.do_spellcheck. 2036 c = $(textpattern.do_spellcheck)[0]; 2037 2038 if (c && "spellcheck" in c) { 2039 $(textpattern.do_spellcheck).prop("spellcheck", true); 2040 } 2041 2042 // Attach toggle behaviours. 2043 $(document).on('click', '.txp-summary a[class!=pophelp]', toggleDisplayHref); 2044 2045 // Attach multi-edit form. 2046 $('.multi_edit_form').txpMultiEditForm(); 2047 2048 // Establish AJAX timeout from prefs. 2049 if ($.ajaxSetup().timeout === undefined) { 2050 $.ajaxSetup({timeout : textpattern.ajax_timeout}); 2051 } 2052 2053 // Set up asynchronous forms. 2054 $('form.async').txpAsyncForm({ 2055 error: function () 2056 { 2057 window.alert(textpattern.gTxt('form_submission_error')); 2058 } 2059 }); 2060 2061 // Set up asynchronous links. 2062 $('a.async:not(.script)').txpAsyncHref({ 2063 error: function () 2064 { 2065 window.alert(textpattern.gTxt('form_submission_error')); 2066 } 2067 }); 2068 2069 $('a.async.script').txpAsyncHref({ 2070 dataType : 'script', 2071 error : function () 2072 { 2073 window.alert(textpattern.gTxt('form_submission_error')); 2074 } 2075 }); 2076 2077 // Close button on the announce pane. 2078 $(document).on('click', '.close', function (e) 2079 { 2080 e.preventDefault(); 2081 $(this).parent().remove(); 2082 }); 2083 2084 $('body').restorePanes(); 2085 2086 // Hide popup elements. 2087 $('.txp-dropdown').hide(); 2088 2089 // Event handling and automation. 2090 $(document).on('change.txpAutoSubmit', 'form [data-submit-on="change"]', function (e) 2091 { 2092 $(this).parents('form').submit(); 2093 }); 2094 2095 // Polyfills. 2096 // Add support for form attribute in submit buttons. 2097 if ($('html').hasClass('no-formattribute')) { 2098 $('.txp-save input[form]').click(function(e) { 2099 var targetForm = $(this).attr('form'); 2100 $('form[id='+targetForm+']').submit(); 2101 }); 2102 } 2103 2104 // Establish UI defaults. 2105 $('.txp-dialog').txpDialog(); 2106 $('.txp-dialog.modal').dialog('option', 'modal', true); 2107 $('.txp-datepicker').txpDatepicker(); 2108 $('.txp-sortable').txpSortable(); 2109 2110 2111 2112 // TODO: integrate jQuery UI stuff properly -------------------------------- 2113 2114 2115 // Selectmenu 2116 $('.jquery-ui-selectmenu').selectmenu(); 2117 2118 // Button 2119 $('.jquery-ui-button').button(); 2120 2121 // Button set 2122 $('.jquery-ui-controlgroup').controlgroup(); 2123 2124 2125 // TODO: end integrate jQuery UI stuff properly ---------------------------- 2126 2127 2128 2129 // Find and open associated dialogs. 2130 $(document).on('click.txpDialog', '[data-txp-dialog]', function (e) 2131 { 2132 $($(this).data('txp-dialog')).dialog('open'); 2133 e.preventDefault(); 2134 }); 2135 2136 // Initialize panel specific JavaScript. 2137 textpattern.Route.init(); 2138 2139 // Arm UI. 2140 $('body').removeClass('not-ready'); 2141 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
title