/* eslint-env jquery */
/*
* Textpattern Content Management System
* https://textpattern.com/
*
* Copyright (C) 2020 The Textpattern Development Team
*
* This file is part of Textpattern.
*
* Textpattern is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, version 2.
*
* Textpattern is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Textpattern. If not, see .
*/
'use strict';
/**
* Collection of client-side tools.
*/
textpattern.version = '4.8.4';
/**
* Ascertain the page direction (LTR or RTL) as a variable.
*/
var langdir = document.documentElement.dir,
dir = langdir === 'rtl' ? 'left' : 'right';
/**
* Checks if HTTP cookies are enabled.
*
* @return {boolean}
*/
function checkCookies()
{
cookieEnabled = navigator.cookieEnabled && (document.cookie.indexOf('txp_test_cookie') >= 0 || document.cookie.indexOf('txp_login') >= 0);
if (!cookieEnabled) {
textpattern.Console.addMessage([textpattern.gTxt('cookies_must_be_enabled'), 1]);
} else {
document.cookie = 'txp_test_cookie=; Max-Age=0; SameSite=Lax';
}
}
/**
* Basic confirmation for potentially powerful choices (like deletion,
* for example).
*
* @param {string} msg The message
* @return {boolean} TRUE if user confirmed the action
*/
function verify(msg)
{
return confirm(msg);
}
/**
* Multi-edit functions.
*
* @param {string|object} method Called method, or options
* @param {object} opt Options if method is a method
* @return {object} this
* @since 4.5.0
*/
jQuery.fn.txpMultiEditForm = function (method, opt) {
var args = {};
var defaults = {
'checkbox' : 'input[name="selected[]"][type=checkbox]',
'row' : 'tbody td',
'highlighted' : 'tr',
'filteredClass': 'filtered',
'selectedClass': 'selected',
'actions' : 'select[name=edit_method]',
'submitButton' : '.multi-edit input[type=submit]',
'selectAll' : 'input[name=select_all][type=checkbox]',
'rowClick' : true,
'altClick' : true,
'confirmation' : textpattern.gTxt('are_you_sure')
};
if ($.type(method) !== 'string') {
opt = method;
method = null;
} else {
args = opt;
}
this./*closest('form').*/each(function () {
var $this = $(this), form = {}, methods = {}, lib = {};
if ($this.data('_txpMultiEdit')) {
form = $this.data('_txpMultiEdit');
opt = $.extend(form.opt, opt);
} else {
opt = $.extend(defaults, opt);
form.editMethod = $this.find(opt.actions);
form.lastCheck = null;
form.opt = opt;
form.selectAll = $this.find(opt.selectAll);
form.button = $this.find(opt.submitButton);
}
form.boxes = $this.find(opt.checkbox);
/**
* Registers a multi-edit option.
*
* @param {object} options
* @param {string} options.label The option's label
* @param {string} options.value The option's value
* @param {string} options.html The second step HTML
* @return {object} methods
*/
methods.addOption = function (options) {
var settings = $.extend({
'label': null,
'value': null,
'html' : null
}, options);
if (!settings.value) {
return methods;
}
var option = form.editMethod.find('option').filter(function () {
return $(this).val() === settings.value;
});
var exists = (option.length > 0);
form.editMethod.val('');
if (!exists) {
option = $('');
}
if (!option.data('_txpMultiMethod')) {
if (!option.val()) {
option.val(settings.value);
}
if (!option.text() && settings.label) {
option.text(settings.label);
}
option.data('_txpMultiMethod', settings.html);
}
if (!exists) {
form.editMethod.append(option);
}
return methods;
};
/**
* Selects rows based on supplied arguments.
*
* Only one of the filters applies at a time.
*
* @param {object} options
* @param {array} options.index Indexes to select
* @param {array} options.range Select index range, takes [min, max]
* @param {array} options.value Values to select
* @param {boolean} options.checked TRUE to check, FALSE to uncheck
* @return {object} methods
*/
methods.select = function (options) {
var settings = $.extend({
'index' : null,
'range' : null,
'value' : null,
'checked': true
}, options);
var obj = form.boxes;//$this.find(opt.checkbox);
if (settings.value !== null) {
obj = obj.filter(function () {
return $.inArray($(this).val(), settings.value) !== -1;
});
} else if (settings.index !== null) {
obj = obj.filter(function (index) {
return $.inArray(index, settings.index) !== -1;
});
} else if (settings.range !== null) {
obj = obj.slice(settings.range[0], settings.range[1]);
}
obj.filter(settings.checked ? ':not(:checked)' : ':checked').prop('checked', settings.checked).change();
lib.highlight();
return methods;
};
/**
* Highlights selected rows.
*
* @return {object} lib
*/
lib.highlight = function () {
var checked = form.boxes.filter(':checked'), count = checked.length,
option = form.editMethod.find('[value=""]');
checked.closest(opt.highlighted).addClass(opt.selectedClass);
form.boxes.filter(':not(:checked)').closest(opt.highlighted).removeClass(opt.selectedClass);
option.gTxt('with_selected_option', {
'{count}': count
});
form.selectAll.prop('checked', count === form.boxes.length).change();
form.editMethod.prop('disabled', !count);
if (!count) {
form.editMethod.val('').change();
}
return lib;
};
/**
* Extends click region to whole row.
*
* @return {object} lib
*/
lib.extendedClick = function () {
var selector = opt.rowClick ? opt.row : opt.checkbox;
$this.on('click', selector, function (e) {
var self = ($(e.target).is(opt.checkbox) || $(this).is(opt.checkbox));
if (!self && (e.target != this || $(this).is('a, :input') || $(e.target).is('a, :input'))) {
return;
}
if (!self && opt.altClick && !e.altKey && !e.ctrlKey) {
return;
}
var box = $(this).closest(opt.highlighted).find(opt.checkbox);
if (box.length < 1) {
return;
}
var checked = box.prop('checked');
if (self) {
checked = !checked;
}
if (e.shiftKey && form.lastCheck) {
var boxes = form.boxes;
var start = boxes.index(box);
var end = boxes.index(form.lastCheck);
methods.select({
'range' : [Math.min(start, end), Math.max(start, end) + 1],
'checked': !checked
});
} else if (!self) {
box.prop('checked', !checked).change();
}
form.lastCheck = box;
});
return lib;
};
/**
* Tracks row checks.
*
* @return {object} lib
*/
lib.checked = function () {
$this.on('change', opt.checkbox, function (e) {
var box = $(this);
if (box.prop('checked')) {
if (-1 == $.inArray(box.val(), textpattern.Relay.data.selected)) {
textpattern.Relay.data.selected.push(box.val());
}
} else {
textpattern.Relay.data.selected = $.grep(textpattern.Relay.data.selected, function(value) {
return value != box.val();
});
}
if (typeof(e.originalEvent) != 'undefined') {
lib.highlight();
}
});
return lib;
};
/**
* Handles edit method selecting.
*
* @return {object} lib
*/
lib.changeMethod = function () {
form.button.hide();
form.editMethod.val('').change(function (e) {
var selected = $(this).find('option:selected');
$this.find('.multi-step').remove();
if (selected.length < 1 || selected.val() === '') {
form.button.hide();
return lib;
}
if (selected.data('_txpMultiMethod')) {
$(this).after($('
').attr('class', 'multi-step multi-option').html(selected.data('_txpMultiMethod')));
form.button.show();
} else {
form.button.hide();
$(this).parents('form').submit();
}
});
return lib;
};
/**
* Handles sending.
*
* @return {object} lib
*/
lib.sendForm = function () {
$this.submit(function () {
if (opt.confirmation !== false && verify(opt.confirmation) === false) {
form.editMethod.val('').change();
return false;
}
});
return lib;
};
if (!$this.data('_txpMultiEdit')) {
lib.highlight().extendedClick().checked().changeMethod().sendForm();
(function () {
var multiOptions = $this.find('.multi-option:not(.multi-step)');
form.editMethod.find('option[value!=""]').each(function () {
var value = $(this).val();
var option = multiOptions.filter(function () {
return $(this).data('multi-option') === value;
});
if (option.length > 0) {
methods.addOption({
'label': null,
'html' : option.eq(0).contents(),
'value': $(this).val()
});
}
});
multiOptions.remove();
})();
form.selectAll.on('change', function (e) {
if (typeof(e.originalEvent) != 'undefined') {
methods.select({
'checked': $(this).prop('checked')
});
}
$this.toggleClass(opt.filteredClass, !$(this).prop('checked'));
}).change();
}
if (method && methods[method]) {
methods[method].call($this, args);
}
$this.data('_txpMultiEdit', form);
});
return this;
};
/**
* Sets a HTTP cookie.
*
* @param {string} name The name
* @param {string} value The value
* @param {integer} days Expires in
*/
function setCookie(name, value, days)
{
var expires = '';
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = '; expires=' + date.toGMTString();
}
document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax';
}
/**
* Gets a HTTP cookie's value.
*
* @param {string} name The name
* @return {string} The cookie
*/
function getCookie(name)
{
var nameEQ = name + '=';
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1, c.length);
}
if (c.indexOf(nameEQ) == 0) {
return c.substring(nameEQ.length, c.length);
}
}
return null;
}
/**
* Deletes a HTTP cookie.
*
* @param {string} name The cookie
*/
function deleteCookie(name)
{
setCookie(name, '', -1);
}
/**
* Toggles column's visibility and saves the state.
*
* @param {string} sel The column selector object
* @return {boolean} Returns FALSE
* @since 4.7.0
*/
function toggleColumn(sel, $sel, vis)
{
// $sel = $(sel);
if ($sel.length) {
$sel.toggle(!!vis);
// Send state of toggle pane to localStorage.
var data = new Object;
data[textpattern.event] = {'columns':{}};
data[textpattern.event]['columns'][sel] = !!vis ? null : false;
textpattern.storage.update(data);
}
return false;
}
/**
* Toggles panel's visibility and saves the state.
*
* @param {string} id The element ID
* @return {boolean} Returns FALSE
*/
function toggleDisplay(id)
{
var obj = $('#' + id);
if (obj.length) {
obj.toggle();
// Send state of toggle pane to localStorage.
var pane = $(this).data('txp-pane') || obj.attr('id');
var data = new Object;
data[textpattern.event] = {'panes':{}};
data[textpattern.event]['panes'][pane] = obj.is(':visible') ? true : null;
textpattern.storage.update(data);
}
return false;
}
/**
* Direct show/hide referred #segment; decorate parent lever.
*/
function toggleDisplayHref()
{
var $this = $(this);
var href = $this.attr('href');
var lever = $this.parent('.txp-summary');
if (href) {
toggleDisplay.call(this, href.substr(1));
}
if (lever.length) {
var vis = $(href).is(':visible');
lever.toggleClass('expanded', vis);
$this.attr('aria-pressed', vis.toString());
$(href).attr('aria-expanded', vis.toString());
}
return false;
}
/**
* Shows/hides matching elements.
*
* @param {string} className Targeted element's class
* @param {boolean} show TRUE to display
*/
function setClassDisplay(className, show)
{
$('.' + className).toggle(show);
}
/**
* Toggles panel's visibility and saves the state to a HTTP cookie.
*
* @param {string} classname The HTML class
*/
function toggleClassRemember(className)
{
var v = getCookie('toggle_' + className);
v = (v == 1 ? 0 : 1);
setCookie('toggle_' + className, v, 365);
setClassDisplay(className, v);
setClassDisplay(className + '_neg', 1 - v);
}
/**
* Toggle visibility of matching elements based on a cookie value.
*
* @param {string} className The HTML class
* @param {string} force The value
*/
function setClassRemember(className, force)
{
if (typeof(force) != 'undefined') {
setCookie('toggle_' + className, force, 365);
}
var v = getCookie('toggle_' + className);
setClassDisplay(className, v);
setClassDisplay(className + '_neg', 1 - v);
}
/**
* Load data from the server using a HTTP POST request.
*
* @param {object} data POST payload
* @param {object} fn Success handler
* @param {string} format Response data format, defaults to 'xml'
* @return {object} this
* @see https://api.jquery.com/jQuery.post/
*/
function sendAsyncEvent(data, fn, format)
{
var formdata = false;
if ($.type(data) === 'string' && data.length > 0) {
// Got serialized data.
data = data + '&app_mode=async&_txp_token=' + textpattern._txp_token;
} else if (data instanceof FormData) {
formdata = true;
data.append('app_mode', 'async');
data.append('_txp_token', textpattern._txp_token);
} else {
data.app_mode = 'async';
data._txp_token = textpattern._txp_token;
}
format = format || 'xml';
return formdata ?
$.ajax({
type: 'POST',
url: 'index.php',
data: data,
success: fn,
dataType: format,
processData: false,
contentType: false
}) :
$.post('index.php', data, fn, format);
}
/**
* A pub/sub hub for client-side events.
*
* @since 4.5.0
*/
textpattern.Relay =
{
/**
* Publishes an event to all registered subscribers.
*
* @param {string} event The event
* @param {object} data The data passed to registered subscribers
* @return {object} The Relay object
* @example
* textpattern.Relay.callback('newEvent', {'name1': 'value1', 'name2': 'value2'});
*/
callback: function (event, data, timeout) {
clearTimeout(textpattern.Relay.timeouts[event]);
timeout = !timeout ? 0 : parseInt(timeout, 10);
if (!timeout || isNaN(timeout)) {
return $(this).trigger(event, data);
}
textpattern.Relay.timeouts[event] = setTimeout(
$.proxy(function() {
return textpattern.Relay.callback(event, data);
}, this),
parseInt(timeout, 10)
);
},
/**
* Subscribes to an event.
*
* @param {string} The event
* @param {object} fn The callback function
* @return {object} The Relay object
* @example
* textpattern.Relay.register('event',
* function (event, data)
* {
* alert(data);
* }
* );
*/
register: function (event, fn) {
if (fn) {
$(this).on(event, fn);
} else {
$(this).off(event);
}
return this;
},
timeouts: {},
data: {selected: []}
};
/**
* Textpattern localStorage.
*
* @since 4.6.0
*/
textpattern.storage =
{
/**
* Textpattern localStorage data.
*/
data: (!navigator.cookieEnabled || !window.localStorage ? null : JSON.parse(window.localStorage.getItem('textpattern.' + textpattern._txp_uid))) || {},
/**
* Updates data.
*
* @param data The message
* @example
* textpattern.update({prefs: 'site'});
*/
update: function (data) {
$.extend(true, textpattern.storage.data, data);
textpattern.storage.clean(textpattern.storage.data);
if (navigator.cookieEnabled && window.localStorage) {
window.localStorage.setItem('textpattern.' + textpattern._txp_uid, JSON.stringify(textpattern.storage.data));
}
},
clean: function (obj) {
Object.keys(obj).forEach(function (key) {
if (obj[key] && typeof obj[key] === 'object') {
textpattern.storage.clean(obj[key]);
} else if (obj[key] === null) {
delete obj[key];
}
});
}
};
/**
* Logs debugging messages.
*
* @since 4.6.0
*/
textpattern.Console =
{
/**
* Stores an array of invoked messages.
*/
history: [],
/**
* Stores an array of messages to announce.
*/
messages: {},
queue: {},
/**
* Clear.
*
* @param {string} The event
* @return textpattern.Console
*/
clear: function (event, reset) {
event = event || textpattern.event;
textpattern.Console.messages[event] = [];
if (!!reset) {
textpattern.Console.queue[event] = false;
}
return this;
},
/**
* Add message to announce.
*
* @param {string} The event
* @param {string} The message
* @return textpattern.Console
*/
addMessage: function (message, event) {
event = event || textpattern.event;
if (typeof textpattern.Console.messages[event] === 'undefined') {
textpattern.Console.messages[event] = [];
}
textpattern.Console.messages[event].push(message);
return this;
},
/**
* Announce.
*
* @param {string} The event
* @return textpattern.Console
*/
announce: function (event, options) {
event = event || textpattern.event;
if (!!textpattern.Console.queue[event]) {
return this;
} else {
textpattern.Console.queue[event] = true;
}
$(document).ready(function() {
var c = 0, message = [], status = 0;
if (textpattern.Console.messages[event] && textpattern.Console.messages[event].length) {
var container = textpattern.prefs.message || '{message}';
textpattern.Console.messages[event].forEach (function(pair) {
message.push(textpattern.mustache(container, {
status: pair[1] != 1 && pair[1] != 2 ? 'check' : 'alert',
message: pair[0]
}));
c += 2*(pair[1] == 1) + 1*(pair[1] == 2);
});
status = !c ? 'success' : (c == 2*textpattern.Console.messages[event].length ? 'error' : 'warning');
}
textpattern.Relay.callback('announce', {event: event, message: message, status: status});
textpattern.Console.clear(event, true);
});
return this;
},
/**
* Logs a message.
*
* @param message The message
* @return textpattern.Console
* @example
* textpattern.Console.log('Some message');
*/
log: function (message) {
if (textpattern.prefs.production_status === 'debug') {
textpattern.Console.history.push(message);
textpattern.Relay.callback('txpConsoleLog', {
'message': message
});
}
return this;
}
};
/**
* Console API module for textpattern.Console.
*
* Passes invoked messages to Web/JavaScript Console
* using console.log().
*
* Uses a namespaced 'txpConsoleLog.ConsoleAPI' event.
*/
textpattern.Relay.register('txpConsoleLog.ConsoleAPI', function (event, data) {
if ($.type(console) === 'object' && $.type(console.log) === 'function') {
console.log(data.message);
}
}).register('uploadProgress', function (event, data) {
$('progress.txp-upload-progress').val(data.loaded / data.total);
}).register('uploadStart', function (event, data) {
$('progress.txp-upload-progress').val(0).show();
}).register('uploadEnd', function (event, data) {
$('progress.txp-upload-progress').hide();
}).register('updateList', function (event, data) {
var list = data.list || '#messagepane, .txp-async-update',
url = data.url || 'index.php',
callback = data.callback || function(event) {
textpattern.Console.announce(event);
},
handle = function(html) {
if (html) {
var $html = $(html);
$.each(list.split(','), function(index, value) {
$(value).each(function() {
var id = this.id;
if (id) {
$(this).replaceWith($html.find('#'+id)).remove();
$('#'+id).trigger('updateList');
}
});
});
$html.remove();
}
callback(data.event);
};
$(list).addClass('disabled');
if (typeof data.html == 'undefined') {
$('