Commit 1d70b760 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Merge branch 'F8348-DropdownMenuViaAS_link' into 'master'

F8348 dropdown menu via as link

See merge request !141
parents 98fe89cb ac25758c
Pipeline #1859 passed with stage
in 2 minutes and 18 seconds
......@@ -5603,6 +5603,8 @@ Column: _link
|x | |Copy to |y:[some content] |y:this will be copied |Click on it copies the value of 'y:' to the clipboard. Optional a file ('F:...') might be specified as source. |
| | |clipboard | | |See `copyToClipboard`_. |
+---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| | |Dropdown menu |z |z||p:home|t:Home |Creates a dropdown menu. SEe `dropdownMenu`_. |
+---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| | |Text |t:<text> |t:Firstname Lastname | |
+---+---+--------------+-----------------------------------+---------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| | |Render |r:<mode> |r:3 |See: `render-mode`_, Default: 0 |
......@@ -6849,6 +6851,65 @@ Excel export samples::
# With parameter (via SIP) - get the Parameter on page 'exceldata' with '{{arg1:S}}' and '{{arg2:S}}'
SELECT CONCAT('d:final.xlsx|t:Excel (parameter)|uid:32&arg1=hello&arg2=world') AS _excel
.. _dropdownMenu:
Dropdown Menu
-------------
Creates a menu with custom links. The same notation and options are used as with regular QFQ links.
Format String::
<dropdown menu symbol options>||<menu entry 1>||<menu entry 2>||...
Each menu entry is separated by two bars! A menu entry itself might contain multiple single bars.
Example 1::
SELECT 'z||p:home|t:Home|o:Jump to home||p:person&form=person&r=123|t:Edit: John Doe|s' AS _link
This defines a menu (three vertical buttons) - a click on it shows two menu entries: 'Home' and 'Edit: John Doe'
Format the dropdown menu symbol:
* *Glyph*: Via `G:<glyphicon name>` any glyphicon can be defined. To hide the default glyph, specify: `G:0`.
* *Text*: Via `t:Menu` an additional text will be displayed for the menu symbol.
* *Tooltip*: Via `o:Detail menu` a tooltip is defined.
* *Render mode*: Via `r:3` the menu is disabled. No menu entries / links / sip are rendered.
* *Button*: Via `b` the dropdown meny symbol will be rendered with a button. Also `b:<style>` might set the BS color.
Format a menu entry:
* *qfq link*: All options as with a regular QFQ link.
* *header*: If a text starts with '===', it becomes a header in the dropdown menu. Multiple headers are possible. Headers can't be a link. An additional `r:1` is necessary.
* *separator*: If a text is exactly '---', it becomes a separator line between two menu entries. An additional `r:1` is necessary.
* *disabled menu entry*: If a text starts with '---' (like separator), the following text becomes a disable menu entry. An additional `r:1` is necessary.
Example 2::
SELECT CONCAT('z|t:Menu|G:0|o:Please select an option',
'||p:home|t:Home|o:Jump to home|G:glyphicon-home|b:0',
'||r:1|t:---',
'||p:person&form=person&r=123|t:Edit: John Doe|s|q:Really edit?|G:glyphicon-user|b:0',
'||t:===Header|r:1',
'||d|p:form&form=person&r=',p.id,'|s|t:Download Person|b:0',
'||r:1|t:---Disabled entry') AS _link
Line 1: The dropdown menu symbol definition, with a text 'Menu' `t:Menu`, but without the three vertical bullets `G:0`
and a tooltip for the menu `o:Please select an option`.
Line 2: First menu entry. Directs to T3 page 'home' `p:home`. `t:Home` sets the menu entry text. `G:glyphicon-home` set's
glyphicon symbol in the menu entry. `b:0` switches off the button, which has been implicit activated by the use of `G:...`.
Line 3: A separator line.
Line 4: A SIP encoded edit record link, with a question dialog.
Line 5: A header line.
Line 6: A PDF download.
Line 7: A disabled menu entry.
.. _drag_and_drop:
......
......@@ -1595,6 +1595,7 @@ const TOKEN_PAGE = 'p';
const TOKEN_UID = 'uid';
const TOKEN_DOWNLOAD = 'd';
const TOKEN_COPY_TO_CLIPBOARD = 'y';
const TOKEN_DROPDOWN = 'z';
const TOKEN_TEXT = 't';
const TOKEN_ALT_TEXT = 'a';
......
......@@ -43,19 +43,30 @@ require_once(__DIR__ . '/Thumbnail.php');
* D:delete
* e:encryption 0|1
* E:edit
* f:
* F:File
* g:target
* G:Glyph
* h:
* H:Help
* i:
* I:information
* j:
* J:
* k:
* K:
* l:
* L:
* m:mailto
* M:Mode
* n:
* N:new
* o:ToolTip
* O:Monitor
* p:page
* P:picture [file]
* q:question <text>
* Q:
* r:render
* R:right
* s:sip
......@@ -64,18 +75,25 @@ require_once(__DIR__ . '/Thumbnail.php');
* T:Thumbnail
* u:url
* U:URL Param
* v:
* V:
* w:
* W:Dimension
* x:Delete
* X:
* y:Copy to clipboard
* Y:
* z:DropDown Menu
* Z:
*
*/
const NAME_URL = 'url';
const NAME_MAIL = 'mail';
const NAME_PAGE = 'page';
const NAME_UID = 'uid';
const NAME_TEXT = 'text';
const NAME_DROPDOWN = 'dropdown';
const NAME_DOWNLOAD = DOWNLOAD_EXPORT_FILENAME;
const NAME_COLLECT_ELEMENTS = 'downloadElements'; // array with element sources
const NAME_DOWNLOAD_MODE = 'mode';
......@@ -171,6 +189,7 @@ class Link {
TOKEN_PAGE => 'buildPage',
TOKEN_COPY_TO_CLIPBOARD => 'buildCopyToClipboard',
TOKEN_DOWNLOAD => 'buildDownload',
TOKEN_DROPDOWN => 'buildDropdown',
TOKEN_TOOL_TIP => 'buildToolTip',
TOKEN_PICTURE => 'buildPicture',
TOKEN_BULLET => 'buildBullet',
......@@ -193,6 +212,7 @@ class Link {
TOKEN_MAIL => NAME_MAIL,
TOKEN_PAGE => NAME_PAGE,
TOKEN_UID => NAME_UID,
TOKEN_DROPDOWN => NAME_DROPDOWN,
TOKEN_DOWNLOAD => NAME_DOWNLOAD,
TOKEN_DOWNLOAD_MODE => NAME_DOWNLOAD_MODE,
TOKEN_TEXT => NAME_TEXT,
......@@ -371,6 +391,194 @@ class Link {
}
/**
* @param string $str
*
* @return string
* @throws CodeException
* @throws UserFormException
* @throws UserReportException
*/
private function processDropdown($str) {
$menuEntryStrArr = array();
$menuEntryLinkArr = array();
$tokenCollect = array();
static $count = 0;
$time = microtime(false);
if (defined('PHPUNIT_QFQ')) {
$time = '0.123 1234';
}
$htmlId = $time . ' ' . $count++;
$paramArr = KeyValueStringParser::explodeEscape(PARAM_DELIMITER, $str);
// Iterate over token. Find delimiter to separate dropdown definition and all menu entries.
foreach ($paramArr as $tokenStr) {
$tokenArr = explode(PARAM_TOKEN_DELIMITER, $tokenStr, 2);
switch ($tokenArr[0] ?? '') {
case TOKEN_DROPDOWN:
$tokenCollect[] = ''; // In case there are no further options given, it's necessary to have at least one
break;
// Indicator to start menu entry: force a flush of existing token and start a new round.
case '':
// New menu entry.
if (!empty($tokenCollect)) {
$menuEntryStrArr[] = implode(PARAM_DELIMITER, $tokenCollect);
}
$tokenCollect = array();
// $tokenCollect[] = $tokenStr;
break;
default:
$tokenCollect[] = $tokenStr;
}
}
// Flush remaining element.
$menuEntryStrArr[] = implode(PARAM_DELIMITER, $tokenCollect);
// Remove first array element - that's the menu, not a menu entry. Render the menu.
$flagMenuEnabled = false;
$dropdownSymbol = $this->renderDropdownSymbol(array_shift($menuEntryStrArr), $htmlId, $flagMenuEnabled);
if ($flagMenuEnabled) {
// For each menu entry get the link
foreach ($menuEntryStrArr as $str) {
$menuEntryLinkArr[] = $this->renderLink($str);
}
}
return $this->renderDropdown($dropdownSymbol, $menuEntryLinkArr, $htmlId, $flagMenuEnabled);
}
/**
* Start
* <span class="dropdown">
* <span class="glyphicon glyphicon-option-vertical dropdown-toggle" id="dropdownMenu11" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
* </span>
* <ul class="dropdown-menu" aria-labelledby="dropdownMenu11">
* <li><a href="#">Action</a></li>
* <li><a href="#">Another action</a></li>
* <li><a href="#">Something else here</a></li>
* <li role="separator" class="divider"></li>
* <li><a href="#">Separated link</a></li>
* </ul>
* </span>
* End
*
* @param string $dropdownSymbol
* @param array $menuEntryLinkArr
* @param string $htmlId
* @param bool $flagMenuEnabled
* @return string
*/
private function renderDropdown(string $dropdownSymbol, array $menuEntryLinkArr, $htmlId, $flagMenuEnabled) {
$ul = '';
$li = '';
$attribute = '';
if ($flagMenuEnabled) {
$tmp = '';
foreach ($menuEntryLinkArr as $link) {
$attribute = '';
switch (substr($link, 0, 3)) {
case '---':
$link = substr($link, 3);
if ($link == '') {
# Separator
$attribute = ' role="separator" class="divider"';
} else {
# Disabled
$attribute = ' class="disabled"';
if (false === strstr($link, '<a ')) {
# If there is no '<a>'-tag, the 'disabled' class is broken - set a fake one.
$link = Support::wrapTag('<a href="#">', $link);
}
}
break;
case '===':
// Header
$link = substr($link, 3);
$attribute = ' class="dropdown-header"';
break;
default:
break;
}
$li .= '<li' . $attribute . '>' . $link . '</li>';
}
$ul = Support::wrapTag('<ul class="dropdown-menu" aria-labelledby="' . $htmlId . '">', $li);
}
return Support::wrapTag('<span class="dropdown">', $dropdownSymbol . $ul);
}
/**
* @param string $dropdownStr
* @param string $htmlId
* @return string
* @throws CodeException
* @throws UserFormException
* @throws UserReportException
*/
private function renderDropdownSymbol($dropdownStr, $htmlId, &$flagMenuEnabled) {
$class = '';
$paramArr = KeyValueStringParser::parse($dropdownStr, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER);
foreach ([TOKEN_URL, TOKEN_MAIL, TOKEN_PAGE, TOKEN_DOWNLOAD, TOKEN_COPY_TO_CLIPBOARD] as $token) {
if (isset($paramArr[$token])) {
throw new UserReportException("Dropdown / broken definition: Token '$token' can't be part of " . TOKEN_DROPDOWN, ERROR_INVALID_VALUE);
}
}
// All render mode <3 means: enabled
$flagMenuEnabled = ($paramArr[TOKEN_RENDER] ?? 0) < 3;
// Misuse the link class to render the menu symbol: this gives tooltip, glyph, button, text ... - set render mode to '3' = 'no link'
$paramArr[TOKEN_RENDER] = '3';
if (!isset($paramArr[TOKEN_GLYPH])) {
$paramArr[TOKEN_GLYPH] = 'glyphicon-option-vertical';
}
if (isset($paramArr[TOKEN_BOOTSTRAP_BUTTON])) {
$vars = $this->buildBootstrapButton(array(), $paramArr[TOKEN_BOOTSTRAP_BUTTON]);
$class = ' ' . ($vars[NAME_BOOTSTRAP_BUTTON] ?? '');
}
if (!isset($paramArr[TOKEN_ATTRIBUTE])) {
$paramArr[TOKEN_ATTRIBUTE] = 'data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"';
}
if (!$flagMenuEnabled) {
$class .= ' disabled';
// In case there is no button: set the text an glyphicon to 'muted'
if (($vars[NAME_BOOTSTRAP_BUTTON] ?? '') === '') {
if (($paramArr[TOKEN_TEXT] ?? '') != '') {
$paramArr[TOKEN_TEXT] = Support::wrapTag('<span class="text-muted">', $paramArr[TOKEN_TEXT]);
}
if (($paramArr[TOKEN_GLYPH] ?? '') != '') {
$paramArr[TOKEN_GLYPH] .= ' text-muted';
}
}
}
$paramArr[TOKEN_ATTRIBUTE] .= ' class="dropdown-toggle' . $class . '"';
$paramArr[TOKEN_ATTRIBUTE] .= ' id="' . $htmlId . '"';
return $this->renderLink(KeyValueStringParser::unparse($paramArr, PARAM_TOKEN_DELIMITER, PARAM_DELIMITER));
}
/**
* Build the whole link.
*
......@@ -391,6 +599,11 @@ class Link {
return '';
}
// Check for dropdown menu
if (($str[0] ?? '') == TOKEN_DROPDOWN) {
return $this->processDropdown($str);
}
$vars = $this->fillParameter($str, $tokenGiven);
$vars = $this->processParameter($vars, $tokenGiven);
$mode = $this->getModeRender($vars, $tokenGiven);
......@@ -489,10 +702,10 @@ class Link {
$key = substr($value, 0, 2);
if (strlen($key) == 1) {
$key .= ':';
$key .= PARAM_TOKEN_DELIMITER;
}
if ($key == TOKEN_DOWNLOAD . ':') {
if ($key == TOKEN_DOWNLOAD . PARAM_TOKEN_DELIMITER) {
$prio[] = $value;
} else {
$regular[] = $value;
......@@ -569,7 +782,7 @@ class Link {
unset($rcTokenGiven[$key]); // Skip Bookkeeping for TOKEN_URL_PARAM | TOKEN_FILE | TOKEN_URL.
continue;
} else {
// TOKEN_GLYPH should not treated as an regular image. Same applies to the other Glyph symbols, but those don't have a value, and therefore do not fill $vars['image'].
// TOKEN_GLYPH should not be treated as an regular image. Same applies to the other Glyph symbols, but those don't have a value, and therefore do not fill $vars['image'].
if ($key != TOKEN_GLYPH) {
$vars[$keyName] = $value;
}
......@@ -1029,7 +1242,7 @@ class Link {
$html .= '<img ' . $tags . '>';
}
if ($vars[NAME_GLYPH] !== '') {
if ($vars[NAME_GLYPH] !== '' && $vars[NAME_GLYPH] !== '0') {
$tags = Support::doAttribute('class', 'glyphicon ' . $vars[NAME_GLYPH]);
$html .= Support::wrapTag('<span ' . $tags . '>', '', false);
}
......@@ -1094,7 +1307,7 @@ class Link {
$level = ($arr[QUESTION_INDEX_LEVEL] === '') ? DEFAULT_QUESTION_LEVEL : $arr[QUESTION_INDEX_LEVEL];
$ok = ($arr[QUESTION_INDEX_BUTTON_OK] === '') ? 'Ok' : $arr[QUESTION_INDEX_BUTTON_OK];
$cancel = ($arr[QUESTION_INDEX_BUTTON_FALSE] === '') ? 'Cancel' : $arr[QUESTION_INDEX_BUTTON_FALSE];
$cancel = ($cancel=='-') ? '': ", { label: '$cancel',eventName: 'cancel'}";
$cancel = ($cancel == '-') ? '' : ", { label: '$cancel',eventName: 'cancel'}";
$timeout = ($arr[QUESTION_INDEX_TIMEOUT] === '') ? '0' : $arr[QUESTION_INDEX_TIMEOUT] * 1000;
$flagModalStatus = ($arr[QUESTION_INDEX_FLAG_MODAL] === '') ? '1' : $arr[QUESTION_INDEX_FLAG_MODAL];
......@@ -1137,7 +1350,7 @@ EOF;
$attributes .= Support::doAttribute('class', $vars[FINAL_CLASS]);
$attributes .= Support::doAttribute('target', $vars[NAME_TARGET]);
$attributes .= Support::doAttribute('title', $vars[FINAL_TOOL_TIP]);
if ( $vars[NAME_ATTRIBUTE] != '') {
if ($vars[NAME_ATTRIBUTE] != '') {
$attributes .= $vars[NAME_ATTRIBUTE] . ' ';
}
$attributes .= $vars[FINAL_QUESTION];
......
......@@ -1458,6 +1458,49 @@ EOF;
$this->assertEquals('', $result);
}
/**
* @throws CodeException
* @throws UserFormException
* @throws UserReportException
*/
public function testLinkDropdown() {
$link = new Link($this->sip, DB_INDEX_DEFAULT, true);
// Empty definition
$expect = '<span class="dropdown"><span title="Details" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true" class="dropdown-toggle" id="0.123 1234 0" ><span class="glyphicon glyphicon-option-vertical" ></span></span><ul class="dropdown-menu" aria-labelledby="0.123 1234 0"></ul></span>';
$result = $link->renderLink('z');
$this->assertEquals($expect, $result);
// Empty definition.
$expect = '<span class="dropdown"><span title="Details" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true" class="dropdown-toggle" id="0.123 1234 1" ><span class="glyphicon glyphicon-option-vertical" ></span></span><ul class="dropdown-menu" aria-labelledby="0.123 1234 1"></ul></span>';
$result = $link->renderLink('z:');
$this->assertEquals($expect, $result);
// Dropdown menu with text, custom Glyph and a tooltip.
$expect = '<span class="dropdown"><span title="Open menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true" class="dropdown-toggle" id="0.123 1234 2" ><span class="glyphicon glyph-icon-left" ></span> Menu</span><ul class="dropdown-menu" aria-labelledby="0.123 1234 2"></ul></span>';
$result = $link->renderLink('z|t:Menu|G:glyph-icon-left|o:Open menu');
$this->assertEquals($expect, $result);
// Dropdown menu with menu entry.
$expect = '<span class="dropdown"><span title="Open menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true" class="dropdown-toggle" id="0.123 1234 3" ><span class="glyphicon glyph-icon-left" ></span> Menu</span><ul class="dropdown-menu" aria-labelledby="0.123 1234 3"><li><a href="?id=home" >Home</a></li></ul></span>';
$result = $link->renderLink('z|t:Menu|G:glyph-icon-left|o:Open menu||p:home|t:Home');
$this->assertEquals($expect, $result);
// Dropdown menu disabled.
$expect = '<span class="dropdown"><span title="Open menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true" class="dropdown-toggle disabled" id="0.123 1234 4" ><span class="glyphicon glyph-icon-left text-muted" ></span> <span class="text-muted">Menu</span></span></span>';
$result = $link->renderLink('z|t:Menu|G:glyph-icon-left|o:Open menu|r:3||p:home|t:Home');
$this->assertEquals($expect, $result);
// Dropdown menu with two entries, one is SIP encoded.
$expect = '<span class="dropdown"><span title="Open menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true" class="dropdown-toggle" id="0.123 1234 5" ><span class="glyphicon glyph-icon-left" ></span> Menu</span><ul class="dropdown-menu" aria-labelledby="0.123 1234 5"><li><a href="index.php?id=home&s=badcaffee1234" >Home</a></li><li><a href="?id=back" title="Navigate back" >Back</a></li></ul></span>';
$result = $link->renderLink('z|t:Menu|G:glyph-icon-left|o:Open menu||p:home|t:Home|s||p:back|t:Back|o:Navigate back');
$this->assertEquals($expect, $result);
// Dropdown menu header, separator and disabled entry.
$expect = '<span class="dropdown"><span title="Open menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true" class="dropdown-toggle" id="0.123 1234 6" ><span class="glyphicon glyph-icon-left" ></span> Menu</span><ul class="dropdown-menu" aria-labelledby="0.123 1234 6"><li><a href="index.php?id=home&s=badcaffee1234" >Home</a></li><li><a href="?id=back" title="Navigate back" >Back</a></li><li role="separator" class="divider"></li><li class="dropdown-header">Header</li><li class="disabled"><a href="#">Disabled entry</a></li></ul></span>';
$result = $link->renderLink('z|t:Menu|G:glyph-icon-left|o:Open menu||p:home|t:Home|s||p:back|t:Back|o:Navigate back||r:1|t:---||t:===Header|r:1||r:1|t:---Disabled entry');
$this->assertEquals($expect, $result);
}
/**
* @expectedException \qfq\UserReportException
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment