Commit 1ec4fcb6 authored by Carsten  Rose's avatar Carsten Rose

F7284tablesorter save sort order

parent 73adfd53
......@@ -563,3 +563,30 @@ Current Page
Redirect
: Issued by the Server. It is a command prompting the Client to
navigate away from the Current Page.
### Tablesorter Save View
To save a table view the client sends a post request with the following data:
{
"name": "<name of view>",
"public": <boolean>,
"tableId": "<id of table to which view belongs>",
"view": {
"columnSelection": <array of chosen column ids>,
"filters": <array of strings>,
"sortList": <array of arrays of length 2>
}
}
Request URL
: api/save.php
Request Method
: POST
URL Parameters
: `s=<SIP>` - to prove permission.
Server Response
: The response contains at least a [Minimal Response].
......@@ -12,11 +12,11 @@ General Concept
* There is one PHP file to handle all REST calls:
typo3conf/ext/qfq/Classes/api/rest.php
typo3conf/ext/qfq/Classes/Api/rest.php
* All further endpoints are appended after rest.php, seperated by '/'. Example:
http://localhost/qfq/typo3conf/ext/qfq/Classes/api/rest.php/restPerson/1/restAddress/123?myEmail=jonni@miller.com
http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/restPerson/1/restAddress/123?myEmail=jonni@miller.com
The argument 'myEmail' is just to show how GET variables will be submitted.
......@@ -39,7 +39,7 @@ GET - export
Example:
curl -X GET "http://localhost/qfq/typo3conf/ext/qfq/Classes/api/rest.php/restPerson"
curl -X GET "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/restPerson"
Details:
......@@ -55,7 +55,7 @@ POST - insert
Example:
curl -X POST "http://localhost/qfq/typo3conf/ext/qfq/Classes/api/rest.php/restPerson" -d '{"name":"Miller","firstname":"Jonni"}'
curl -X POST "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/restPerson" -d '{"name":"Miller","firstname":"Jonni"}'
Details:
......@@ -69,7 +69,7 @@ PUT - update
Example:
curl -X PUT "http://localhost/qfq/typo3conf/ext/qfq/Classes/api/rest.php/restPerson/1" -d '{"name":"Miller","firstname":"Jonni"}'
curl -X PUT "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/restPerson/1" -d '{"name":"Miller","firstname":"Jonni"}'
Details:
......@@ -83,7 +83,7 @@ Delete
Example:
curl -X DELETE "http://localhost/qfq/typo3conf/ext/qfq/Classes/api/rest.php/restPerson/1"
curl -X DELETE "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/restPerson/1"
Details:
......@@ -97,7 +97,7 @@ Header Token Authorization
Example:
curl -X GET -H 'Authorization: Token token="mySuperSecretToken"' "http://localhost/qfq/typo3conf/ext/qfq/Classes/api/rest.php/restPerson/"
curl -X GET -H 'Authorization: Token token="mySuperSecretToken"' "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/restPerson/"
Static token
------------
......@@ -120,6 +120,6 @@ Append the GET variable `?XDEBUG_SESSION_START=1`
Example:
curl -X POST "http://localhost/qfq/typo3conf/ext/qfq/Classes/api/rest.php/restPerson?XDEBUG_SESSION_START=1" -d '{"name":"Miller","firstname":"Jonni"}'
curl -X POST "http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/rest.php/restPerson?XDEBUG_SESSION_START=1" -d '{"name":"Miller","firstname":"Jonni"}'
PhpStorm with activated debugger will stop at any breakpoint and 'stepping' through the code is possible.
===========
Tablesorter
===========
* https://mottie.github.io/tablesorter/docs/index.html
General Concept
===============
Tablesorter supports filter, column on/off and sort.
A QFQ enhancement allows save/delete/activate of custom tablesorter `views`. This feature is described in this document.
QFQ uses the table 'Setting' to store per user, public and readonly settings. At time of writing only 'tablesorter' settings
are supported. Further settings might come in the future.
Show table with tablesorter activate/save/delete
================================================
A page with a HTML table includes the command `{{'<uniqueName>' AS _tablesorter-view-saver}}` inside of the HTML table tag.
QFQ will replace the command::
<table {{ 'allperson' AS _tablesorter-view-saver }} class="tablesorter tablesorter-filter tablesorter-column-selector"> ... </table>
by
<table data-tablesorter-id='allperson' data-tablesorter-sip='5d0e29c4eacc2'
data-tablesorter-view='[{"name":"test","public":false,"tableId":"mytable","view":"eyJjb2x1bW5TZWxlY3Rpb24iOlswLDEsMiwzXSwiZmlsdGVycyI6WyIiLCIiLCIiLCIiXSwic29ydExpc3QiOltdfQ=="}]' class="table tablesorter tablesorter-column-selector tablesorter-filter tablesorter-pager">
data-tablesorter-id:
'<uniqueName>'
data-tablesorter-sip:
tableId: the '<uniqueName>', in the example it's 'allperson'
feUser: the current logged in FE user. If no FE user is logged in, take the QFQ cookie. With this workaround, it's at
least possible to work with 'views' during a browser session.
data-tablesorter-view: JSON encoded array
Array of views.
Each view:
['name'] - Name of the view. Value is html entity encoded.
['public'] - true or false. false mean's 'private'.
['tableId'] - '<uniqueName>' - this is the filter to assign views to a specific HTML table.
['view'] - base64 encoded JSON array. It's base64 encoded for two reasons:
a) we do not have to take care about SQL injection (the user might supply filter strings, incl. regex expressions)
b) the JSON array will contain '}}', which confuses QFQ/evaluate.
QFQ will collect all views:
a) all private views of the current table and FE User,
b) all public views of the current table.
Save tablesorter view
=====================
The user might click on 'Save Private View' or 'Save Public View'.
A Ajax Post request to
http://localhost/qfq/typo3conf/ext/qfq/Classes/Api/setting.php?s=badcaffee1234
The SIP is the one given by `data-tablesorter-sip` and contains `tableId` and `feUser`. The SIP is right to manipulate the views.
The POST contains:
feUser - fe user login name
mode - missing or 'delete' if the given view should be deleted
public - true|false
name - name of the view
view - base64 encoded tablesorter filter/order/column selector.
Response setting.php
====================
setting.php will always answer with a JSON stream (Minimal response):
{
"status": "success"|"error",
"message": "<message>"
}
This diff is collapsed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: BSTabs.js</title>
<script src="scripts/prettify/prettify.js"></script>
<script src="scripts/prettify/lang-css.js"></script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: BSTabs.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Rafael Ostertag &lt;rafael.ostertag@math.uzh.ch>
*/
/* global $ */
/* global console */
/* global EventEmitter */
/* @depend QfqEvents.js */
/**
* Qfq Namespace
*
* @namespace QfqNS
*/
var QfqNS = QfqNS || {};
(function (n) {
'use strict';
/**
* Tab Constructor.
*
* Programatically access Bootstrap nav-tabs.
*
* @param {string} tabId HTML id of the element having `nav` and `nav-tabs` classes
* @constructor
*
* @name QfqNS.BSTabs
*/
n.BSTabs = function (tabId) {
this.tabId = tabId;
this._tabContainerLinkSelector = '#' + this.tabId + ' a[data-toggle="tab"]';
this._tabActiveSelector = '#' + this.tabId + ' .active a[data-toggle="tab"]';
this.tabs = {};
this.currentTab = this.getActiveTabFromDOM();
this.eventEmitter = new EventEmitter();
// Fill this.tabs
this.fillTabInformation();
// Enable update of current tab field
this.installTabHandlers();
};
n.BSTabs.prototype.on = n.EventEmitter.onMixin;
/**
* Get active tab from DOM.
*
* Used upon object creation to fill the currentTab. It gets the ID of the currently shown tab. It does
it, by
* targeting the element in the navigator having the `active` class set.
*
* @private
*/
n.BSTabs.prototype.getActiveTabFromDOM = function () {
var activeTabAnchors = $(this._tabActiveSelector);
if (activeTabAnchors.length &lt; 1) {
// This could be due to the DOM not fully loaded. If that's really the case, then the active tab
// attribute should be set by the show.bs.tab handler
return null;
}
return activeTabAnchors[0].hash.slice(1);
};
/**
* Fill tabs object.
*
* Fill the tabs object using the tab HTML id as attribute name
*
* @private
*/
n.BSTabs.prototype.fillTabInformation = function () {
var tabLinks = $(this._tabContainerLinkSelector);
if ($(tabLinks).length === 0) {
throw new Error("Unable to find a BootStrap container matching: " + this._tabContainerLinkSelector);
}
var that = this;
tabLinks.each(function (index, element) {
if (element.hash !== "") {
var tabId = element.hash.slice(1);
that.tabs[tabId] = {
index: index,
element: element
};
}
}
);
};
/**
* @private
*/
n.BSTabs.prototype.installTabHandlers = function () {
$(this._tabContainerLinkSelector)
.on('show.bs.tab', this.tabShowHandler.bind(this));
};
/**
* Tab Show handler.
*
* Sets this.currentTab to the clicked tab and calls all registered tab click handlers.
*
* @private
* @param event
*/
n.BSTabs.prototype.tabShowHandler = function (event) {
n.Log.debug('Enter: BSTabs.tabShowHandler()');
this.currentTab = event.target.hash.slice(1);
n.Log.debug("BSTabs.tabShowHandler(): invoke user handler(s)");
this.eventEmitter.emitEvent('bootstrap.tab.shown', n.EventEmitter.makePayload(this, null));
n.Log.debug('Exit: BSTabs.tabShowHandler()');
};
/**
* Get all tab IDs.
*
* @returns {Array}
*
* @public
*/
n.BSTabs.prototype.getTabIds = function () {
var tabIds = [];
for (var tabId in this.tabs) {
if (this.tabs.hasOwnProperty(tabId)) {
tabIds.push(tabId);
}
}
return tabIds;
};
/**
*
* @returns {Array}
*
* @public
*/
n.BSTabs.prototype.getTabAnchors = function () {
var tabLinks = [];
for (var tabId in this.tabs) {
if (this.tabs.hasOwnProperty(tabId)) {
tabLinks.push(this.tabs[tabId].element);
}
}
return tabLinks;
};
/**
* Activate a given tab.
*
* @param {string} tabId Id of the tab to activate
*
*/
n.BSTabs.prototype.activateTab = function (tabId) {
if (!this.tabs[tabId]) {
console.error("Unable to find tab with id: " + tabId);
return false;
}
$(this.tabs[tabId].element).tab('show');
return true;
};
n.BSTabs.prototype.getCurrentTab = function () {
return this.currentTab;
};
n.BSTabs.prototype.getTabName = function (tabId) {
if (!this.tabs[tabId]) {
console.error("Unable to find tab with id: " + tabId);
return null;
}
return $(this.tabs[tabId].element).text().trim();
};
n.BSTabs.prototype.getActiveTab = function () {
return this.currentTab;
};
n.BSTabs.prototype.getContainingTabIdForFormControl = function (formControlName) {
var $formControl = $("[name='" + formControlName + "']");
if ($formControl.length === 0) {
n.Log.debug("BSTabs.getContainingTabForFormControl(): unable to find form control with name '" +
formControlName + "'");
return null;
}
var iterator = $formControl[0];
while (iterator !== null) {
if (iterator.hasAttribute('role') &amp;&amp;
iterator.getAttribute('role') === 'tabpanel') {
return iterator.id || null;
}
iterator = iterator.parentElement;
}
return null;
};
})(QfqNS);</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2>
<h3>Classes</h3>
<ul>
<li><a href="n.Alert.html">Alert</a></li>
<li><a href="QfqNS.BSTabs.html">BSTabs</a></li>
<li><a href="QfqNS.Element.Checkbox.html">Checkbox</a></li>
<li><a href="QfqNS.Element.FormGroup.html">FormGroup</a></li>
<li><a href="QfqNS.Element.Radio.html">Radio</a></li>
<li><a href="QfqNS.Element.Select.html">Select</a></li>
<li><a href="QfqNS.Element.Textual.html">Textual</a></li>
<li><a href="QfqNS.FileDelete.html">FileDelete</a></li>
<li><a href="QfqNS.FileUpload.html">FileUpload</a></li>
<li><a href="QfqNS.Form.html">Form</a></li>
<li><a href="QfqNS.PageState.html">PageState</a></li>
<li><a href="QfqNS.QfqForm.html">QfqForm</a></li>
<li><a href="QfqNS.QfqPage.html">QfqPage</a></li>
<li><a href="QfqNS.QfqRecordList.html">QfqRecordList</a></li>
</ul>
<h3>Namespaces</h3>
<ul>
<li><a href="global.html#QfqNS">QfqNS</a></li>
<li><a href="QfqNS.Element.html">Element</a></li>
<li><a href="QfqNS.Helper.html">Helper</a></li>
</ul>
<h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.4.0</a> on Mon May 23 2016 12:28:24
GMT+0200 (CEST)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"></script>
</body>
</html>
<
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: Element/Checkbox.js</title>
<script src="scripts/prettify/prettify.js"></script>
<script src="scripts/prettify/lang-css.js"></script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: Element/Checkbox.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Rafael Ostertag &lt;rafael.ostertag@math.uzh.ch>
*/
/* @depend FormGroup.js */
/**
* Qfq Namespace
*
* @namespace QfqNS
*/
var QfqNS = QfqNS || {};
/**
* Qfq.Element Namespace
*
* @namespace QfqNS.Element
*/
QfqNS.Element = QfqNS.Element || {};
(function (n) {
'use strict';
/**
*
* @param $element
* @constructor
* @name QfqNS.Element.Checkbox
*/
function Checkbox($element) {
n.FormGroup.call(this, $element);
var type = "checkbox";
if (!this.isType(type)) {
throw new Error("$element is not of type 'checkbox'");
}
// We allow one Form Group to have several checkboxes. Therefore, we have to remember which checkbox was
// selected if possible.
if ($element.length === 1 &amp;&amp; $element.attr('type') === type) {
this.$singleElement = $element;
} else {
this.$singleElement = null;
}
}
Checkbox.prototype = Object.create(n.FormGroup.prototype);
Checkbox.prototype.constructor = Checkbox;
Checkbox.prototype.setValue = function (val) {
if (this.$singleElement) {
this.$singleElement.prop('checked', val);
} else {
this.$element.prop('checked', val);
}
};
Checkbox.prototype.getValue = function () {
if (this.$singleElement) {
return this.$singleElement.prop('checked');
} else {
return this.$element.prop('checked');
}
};