mirror of
https://github.com/LDAPAccountManager/lam.git
synced 2025-10-03 09:49:16 +02:00
Merge pull request #178 from LDAPAccountManager/feature/sortable
Feature/sortable
This commit is contained in:
commit
42dad06f6d
10 changed files with 3960 additions and 54 deletions
|
@ -1,3 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
~/.local/bin/codespell --skip '*3rdParty*,*/ckeditor/*,*/po/*,*/locale/*,tmp,sess,config,graphics,*/style/images/*,*/style/*.gif,*/style/*.png,*/docs/manual-onePage/*,*/docs/manual-sources/images/*,*/templates/lib/*jquery*,*/templates/lib/*flatpickr*,*~,*/docs/phpdoc/*,*/docs/manual/*,*/docs/devel/images/*,*/docs/manual-pdf/*,*.sh,*/cropper.js,*/lib/extra/*,lam/.phpdoc' --ignore-words-list "tim,te,pres,files'" lam
|
||||
~/.local/bin/codespell --skip '*3rdParty*,*/ckeditor/*,*/po/*,*/locale/*,tmp,sess,config,graphics,*/style/images/*,*/style/*.gif,*/style/*.png,*/docs/manual-onePage/*,*/docs/manual-sources/images/*,*/templates/lib/*jquery*,*/templates/lib/*flatpickr*,*/templates/lib/*Sortable*,*~,*/docs/phpdoc/*,*/docs/manual/*,*/docs/devel/images/*,*/docs/manual-pdf/*,*.sh,*/cropper.js,*/lib/extra/*,lam/.phpdoc' --ignore-words-list "tim,te,pres,files'" lam
|
||||
|
|
|
@ -1197,6 +1197,7 @@ templates/lib/extra/duo/*.js E 2019 Du
|
|||
lib/3rdParty/duo/*.php E 2019 Duo Security
|
||||
templates/lib/extra/friendlyCaptcha B
|
||||
graphics/webauthn.svg F 2017 Duo Security, Inc.
|
||||
templates/lib/400_Sortable*.js B RubaXa, owenm
|
||||
templates/lib/600_jquery.magnific-popup.js B 2016 Dmitry Semenov
|
||||
style/610_magnific-popup.css B 2016 Dmitry Semenov
|
||||
style/010_normalize.css B Nicolas Gallagher and Jonathan Neal
|
||||
|
|
|
@ -5,7 +5,7 @@ June 2022 8.0
|
|||
- 389ds: added hints why login failed if account is locked/deactivated/expired
|
||||
- Removed Zarafa support (please switch to Kopano)
|
||||
- Tree view: display binary data as base64 encoded text
|
||||
- Tree view: better support for move operations
|
||||
- Tree view: better support for move operations and ordered attributes
|
||||
- LAM Pro:
|
||||
-> New captcha providers: hCaptcha and Friendly Captcha
|
||||
- Fixed bugs:
|
||||
|
|
|
@ -1196,6 +1196,7 @@ templates/lib/extra/duo/*.js E 2019 Du
|
|||
templates/lib/extra/friendlyCaptcha B
|
||||
lib/3rdParty/duo/*.php E 2019 Duo Security
|
||||
graphics/webauthn.svg F 2017 Duo Security, Inc.
|
||||
templates/lib/400_Sortable*.js B RubaXa, owenm
|
||||
templates/lib/600_jquery.magnific-popup.js B 2016 Dmitry Semenov
|
||||
style/610_magnific-popup.css B 2016 Dmitry Semenov
|
||||
style/010_normalize.css B Nicolas Gallagher and Jonathan Neal
|
||||
|
|
|
@ -4099,8 +4099,6 @@ class htmlSortableList extends htmlElement {
|
|||
private $elements = array();
|
||||
/** HTML ID */
|
||||
private $id = '';
|
||||
/** element width */
|
||||
private $elementWidth = '';
|
||||
/** on update event */
|
||||
private $onUpdate = null;
|
||||
|
||||
|
@ -4109,12 +4107,10 @@ class htmlSortableList extends htmlElement {
|
|||
*
|
||||
* @param array $elements list of elements as text (HTML special chars must be escaped already) or htmlElement
|
||||
* @param String HTML ID
|
||||
* @param String $elementWidth width of elements (default 250px)
|
||||
*/
|
||||
function __construct($elements, $id, $elementWidth='250px') {
|
||||
function __construct($elements, $id) {
|
||||
$this->elements = $elements;
|
||||
$this->id = htmlspecialchars($id);
|
||||
$this->elementWidth = $elementWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4133,9 +4129,15 @@ class htmlSortableList extends htmlElement {
|
|||
return array();
|
||||
}
|
||||
$return = array();
|
||||
echo '<ul style="width:' . $this->elementWidth . ';" class="sortableList" id="' . $this->id . '">';
|
||||
foreach ($this->elements as $element) {
|
||||
echo '<li class="ui-state-default"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span>';
|
||||
$classesValue = '';
|
||||
if (!empty($this->cssClasses)) {
|
||||
$classesValue = ' class="' . implode(' ', $this->cssClasses) . '"';
|
||||
}
|
||||
echo '<ul' . $classesValue . ' id="' . $this->id . '">';
|
||||
for ($i = 0; $i < sizeof($this->elements); $i++) {
|
||||
$element = $this->elements[$i];
|
||||
echo '<li data-position-orig="' . $i . '">';
|
||||
echo '<span class="ui-icon ui-icon-arrowthick-2-n-s"></span>';
|
||||
if ($element instanceof htmlElement) {
|
||||
parseHtml($module, $element, $values, $restricted, $tabindex, $scope);
|
||||
}
|
||||
|
@ -4147,19 +4149,15 @@ class htmlSortableList extends htmlElement {
|
|||
echo '</ul>';
|
||||
$onUpdate = '';
|
||||
if ($this->onUpdate != null) {
|
||||
$onUpdate = '{
|
||||
update: function(event, ui) {' . $this->onUpdate . '},
|
||||
start: function(event, ui) {
|
||||
var posOrig = ui.item.index();
|
||||
ui.item.data(\'posOrig\', posOrig);
|
||||
}
|
||||
}';
|
||||
$onUpdate = 'onUpdate: ' . $this->onUpdate;
|
||||
}
|
||||
$scriptContent = '
|
||||
jQuery(function() {
|
||||
$("#' . $this->id . '").sortable(' . $onUpdate . ');
|
||||
$("#' . $this->id . '").disableSelection();
|
||||
});';
|
||||
Sortable.create(
|
||||
document.getElementById("' . $this->id . '"),
|
||||
{
|
||||
' . $onUpdate . '
|
||||
}
|
||||
);';
|
||||
$script = new htmlJavaScript($scriptContent);
|
||||
$script->generateHTML($module, $input, $values, $restricted, $tabindex, $scope);
|
||||
return $return;
|
||||
|
@ -5233,3 +5231,50 @@ class htmlForm extends htmlElement {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a (un)ordered list.
|
||||
*/
|
||||
class htmlList extends htmlElement {
|
||||
|
||||
private $elements = array();
|
||||
|
||||
private $isOrdered = false;
|
||||
|
||||
private $id = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $elements elements to render
|
||||
* @param string|null $id HTML unique id
|
||||
* @param bool|null $isOrdered is an ordered list
|
||||
*/
|
||||
function __construct(array $elements, ?string $id = null, ?bool $isOrdered = false) {
|
||||
$this->elements = $elements;
|
||||
$this->id = $id;
|
||||
$this->isOrdered = $isOrdered;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
function generateHTML($module, $input, $values, $restricted, &$tabindex, $scope) {
|
||||
$parentTag = $this->isOrdered ? 'ol' : 'ul';
|
||||
$idValue = empty($this->id) ? '' : ' id="' . $this->id . '"';
|
||||
$classesValue = '';
|
||||
if (!empty($this->cssClasses)) {
|
||||
$classesValue = ' class="' . implode(' ', $this->cssClasses) . '"';
|
||||
}
|
||||
echo '<' . $parentTag . $idValue . $classesValue . '>';
|
||||
$return = array();
|
||||
foreach ($this->elements as $element) {
|
||||
echo '<li>';
|
||||
$return = array_merge($return, $element->generateHTML($module, $input, $values, $restricted, $tabindex, $scope));
|
||||
echo '</li>';
|
||||
}
|
||||
echo '</' . $parentTag . '>';
|
||||
return $return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -11,12 +11,14 @@ use htmlInputField;
|
|||
use htmlInputFileUpload;
|
||||
use htmlInputTextarea;
|
||||
use htmlLink;
|
||||
use htmlList;
|
||||
use htmlOutputText;
|
||||
use htmlResponsiveInputField;
|
||||
use htmlResponsiveRow;
|
||||
use htmlResponsiveSelect;
|
||||
use htmlResponsiveTable;
|
||||
use htmlSelect;
|
||||
use htmlSortableList;
|
||||
use htmlStatusMessage;
|
||||
use htmlSubTitle;
|
||||
use htmlTable;
|
||||
|
@ -518,11 +520,11 @@ class TreeView {
|
|||
$field = $this->getAttributeInputField($attributeName, $values[0], $required, $isMultiLine, true, $schemaAttribute, 0);
|
||||
return $this->addExtraAttributeContent($field, $attributeName, $schemaAttribute);
|
||||
}
|
||||
$valueTable = new htmlTable();
|
||||
$valueTable->setCSSClasses(array('fullwidth'));
|
||||
$valueListContents = array();
|
||||
$autoCompleteValues = array();
|
||||
$onInput = null;
|
||||
if (strtolower($attributeName) === 'objectclass') {
|
||||
$safeAttributeName = htmlspecialchars(strtolower($attributeName));
|
||||
if ($safeAttributeName === 'objectclass') {
|
||||
$schemaObjectClasses = $this->getSchemaObjectClasses();
|
||||
foreach ($schemaObjectClasses as $schemaObjectClass) {
|
||||
if (!in_array_ignore_case($schemaObjectClass->getName(), $values)) {
|
||||
|
@ -534,7 +536,7 @@ class TreeView {
|
|||
foreach ($values as $index => $value) {
|
||||
$inputField = $this->getAttributeInputField($attributeName, $value, $required, $isMultiLine, false, $schemaAttribute, $index);
|
||||
$cssClasses = $inputField->getCSSClasses();
|
||||
$cssClasses[] = 'lam-attr-' . htmlspecialchars(strtolower($attributeName));
|
||||
$cssClasses[] = 'lam-attr-' . $safeAttributeName;
|
||||
$inputField->setCSSClasses($cssClasses);
|
||||
if (!empty($autoCompleteValues) && ($inputField instanceof htmlInputField)) {
|
||||
$inputField->enableAutocompletion($autoCompleteValues);
|
||||
|
@ -543,24 +545,56 @@ class TreeView {
|
|||
if ($onInput !== null) {
|
||||
$inputField->setOnInput($onInput);
|
||||
}
|
||||
$valueTable->addElement($inputField);
|
||||
$valueLine = new htmlTable();
|
||||
$valueLine->setCSSClasses(array('fullwidth'));
|
||||
$valueLine->addElement($inputField);
|
||||
if (checkIfWriteAccessIsAllowed()) {
|
||||
if (!$this->isJpegAttribute($attributeName, $schemaAttribute)) {
|
||||
$addButton = new htmlLink(null, '#', '../../graphics/add.svg');
|
||||
$addButton->setCSSClasses(array('margin2'));
|
||||
$addButton->setOnClick('window.lam.treeview.addValue(event, this);');
|
||||
$valueTable->addElement($addButton);
|
||||
$valueLine->addElement($addButton);
|
||||
}
|
||||
if (!$this->isJpegAttribute($attributeName, $schemaAttribute) || ($value !== '')) {
|
||||
$clearButton = new htmlLink(null, '#', '../../graphics/del.svg');
|
||||
$clearButton->setCSSClasses(array('margin2'));
|
||||
$clearButton->setOnClick('window.lam.treeview.clearValue(event, this);');
|
||||
$valueTable->addElement($clearButton);
|
||||
$valueLine->addElement($clearButton);
|
||||
}
|
||||
}
|
||||
$valueTable->addNewLine();
|
||||
$valueListContents[] = $valueLine;
|
||||
}
|
||||
return $this->addExtraAttributeContent($valueTable, $attributeName, $schemaAttribute);
|
||||
$listClasses = array('nowrap unstyled-list');
|
||||
if ($this->isOrderedAttribute($values)) {
|
||||
$listId = 'attributeList_' . $safeAttributeName;
|
||||
$valueList = new htmlSortableList($valueListContents, $listId);
|
||||
$listClasses[] = 'tree-attribute-sorted-list';
|
||||
$valueList->setOnUpdate('function() {window.lam.treeview.updateAttributePositionData(\'' . $listId . '\');}');
|
||||
}
|
||||
else {
|
||||
$valueList = new htmlList($valueListContents, 'attributeList_' . $safeAttributeName);
|
||||
}
|
||||
$valueList->setCSSClasses($listClasses);
|
||||
return $this->addExtraAttributeContent($valueList, $attributeName, $schemaAttribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the attribute has ordered values.
|
||||
*
|
||||
* @param array $values values
|
||||
* @return bool is ordered
|
||||
*/
|
||||
private function isOrderedAttribute(array $values): bool {
|
||||
if (empty($values)) {
|
||||
return false;
|
||||
}
|
||||
$regex = '/^\\{[0-9]+\\}/';
|
||||
foreach ($values as $value) {
|
||||
if (!preg_match($regex, $value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -604,12 +604,30 @@ input.markPass {
|
|||
background: white url('../graphics/pass.svg') right no-repeat;
|
||||
}
|
||||
|
||||
.sortableList { list-style-type: none; margin: 0; padding: 0; }
|
||||
.sortableList li { margin: 0 3px 3px 3px; padding: 0.4em; padding-left: 1.5em; }
|
||||
.sortableList li span {
|
||||
.module-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.module-list li {
|
||||
margin: 0 3px 3px 3px;
|
||||
padding: 0.4em;
|
||||
padding-left: 1.5em;
|
||||
border: 1px solid #c5c5c5;
|
||||
background: #f6f6f6;
|
||||
font-weight: normal;
|
||||
color: #454545;
|
||||
}
|
||||
.module-list li span {
|
||||
position: absolute;
|
||||
margin-left: -1.3em;
|
||||
margin-top: 0.4rem;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.unstyled-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.strike-through {
|
||||
|
@ -864,3 +882,10 @@ div.tree-highlight select {
|
|||
div.tree-highlight {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tree-attribute-sorted-list li span {
|
||||
position: absolute;
|
||||
margin-left: -1.3em;
|
||||
margin-top: 0.4rem;
|
||||
cursor: grab;
|
||||
}
|
||||
|
|
|
@ -234,9 +234,10 @@ function config_showAccountModules($type, &$container): void {
|
|||
$el->addElement($delButton);
|
||||
$listElements[] = $el;
|
||||
}
|
||||
$selSortable = new htmlSortableList($listElements, $type->getId() . '_selected', null);
|
||||
$selSortable = new htmlSortableList($listElements, $type->getId() . '_selected');
|
||||
$selSortable->alignment = htmlElement::ALIGN_TOP;
|
||||
$selSortable->setOnUpdate('updateModulePositions(\'positions_' . $type->getId() . '\', ui.item.data(\'posOrig\'), ui.item.index());');
|
||||
$selSortable->setCSSClasses(array('module-list'));
|
||||
$selSortable->setOnUpdate('function() {updateModulePositions(\'positions_' . $type->getId() . '\', \'' . $type->getId() . '_selected' . '\');}');
|
||||
$container->add($selSortable, 12, 6);
|
||||
}
|
||||
else {
|
||||
|
|
3794
lam/templates/lib/400_Sortable-1.15.0.js
Normal file
3794
lam/templates/lib/400_Sortable-1.15.0.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -643,27 +643,18 @@ function checkPasswordStrengthHandleReply(data, fieldID) {
|
|||
* Updates the positions of a htmlSortable list in a hidden input field.
|
||||
* The positions must be separated by comma (e.g. "0,1,2,3").
|
||||
*
|
||||
* @param id HTML ID of hidden input field
|
||||
* @param oldPos old position
|
||||
* @param newPos new position
|
||||
* @param inputId HTML ID of hidden input field
|
||||
* @param containerId HTML ID of ul-container
|
||||
*/
|
||||
function updateModulePositions(id, oldPos, newPos) {
|
||||
var positions = jQuery('#' + id).val().split(',');
|
||||
if (newPos > oldPos) {
|
||||
var save = positions[oldPos];
|
||||
for (var i = oldPos; i < newPos; i++) {
|
||||
positions[i] = positions[i + 1];
|
||||
}
|
||||
positions[newPos] = save;
|
||||
function updateModulePositions(inputId, containerId) {
|
||||
const input = document.getElementById(inputId);
|
||||
let positions = [];
|
||||
const container = document.getElementById(containerId);
|
||||
const childLiElements = container.children;
|
||||
for (let i = 0; i < childLiElements.length; i++) {
|
||||
positions[i] = childLiElements[i].getAttribute('data-position-orig');
|
||||
}
|
||||
if (newPos < oldPos) {
|
||||
var oldPosition = positions[oldPos];
|
||||
for (var position = oldPos; position > newPos; position--) {
|
||||
positions[position] = positions[position - 1];
|
||||
}
|
||||
positions[newPos] = oldPosition;
|
||||
}
|
||||
jQuery('#' + id).val(positions.join(','));
|
||||
input.value = positions.join(',');
|
||||
}
|
||||
|
||||
window.lam.filterSelect = window.lam.filterSelect || {};
|
||||
|
@ -2810,6 +2801,20 @@ window.lam.treeview.checkPassword = function(event, element, tokenName, tokenVal
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the positions of a sorted list of LDAP values.
|
||||
*
|
||||
* @param containerId HTML ID of ul-container
|
||||
*/
|
||||
window.lam.treeview.updateAttributePositionData = function(containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
const childLiElements = container.children;
|
||||
for (let i = 0; i < childLiElements.length; i++) {
|
||||
const inputField = childLiElements[i].querySelector('input');
|
||||
inputField.value = '{' + i + '}' + inputField.value.replace(/^\{[0-9]+\}/, '');
|
||||
}
|
||||
}
|
||||
|
||||
window.lam.topmenu = window.lam.topmenu || {};
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue