Add survey question drag-sort and backend assets
- Frontend editor: drag-to-sort questions with dedicated sort assets/JS - Backend list/search refinements for tl_survey and tl_survey_content - Repository helpers for survey/question/editor queries - Translations and AI handover notes update
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(git add *)",
|
||||||
|
"Bash(git commit -q -m ' *)",
|
||||||
|
"Bash(git push *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ Diese Entscheidung ist absichtlich so getroffen und kein Bug.
|
|||||||
|
|
||||||
- `tl_survey`
|
- `tl_survey`
|
||||||
- Umfrage-Stammdaten und Statusfelder
|
- Umfrage-Stammdaten und Statusfelder
|
||||||
|
- Enthält zusätzlich die backend-only gepflegte `internalNote`
|
||||||
- `tl_survey_content`
|
- `tl_survey_content`
|
||||||
- Fragen der Umfrage
|
- Fragen der Umfrage
|
||||||
- `tl_survey_condition`
|
- `tl_survey_condition`
|
||||||
@@ -115,10 +116,19 @@ Aktuelle Button- und Sichtbarkeitslogik pro Umfrage:
|
|||||||
- `Umfrage veröffentlichen`
|
- `Umfrage veröffentlichen`
|
||||||
- Eigene Aktion in der Liste
|
- Eigene Aktion in der Liste
|
||||||
- Nach Veröffentlichung disabled und textlich als veröffentlicht markiert
|
- Nach Veröffentlichung disabled und textlich als veröffentlicht markiert
|
||||||
|
- `Duplizieren`
|
||||||
|
- Legt eine Kopie der Umfrage für den Redakteur an
|
||||||
|
- Titelvergabe ist fortlaufend: `Titel - Kopie`, danach `Titel - Kopie 2`, `Titel - Kopie 3` usw.
|
||||||
|
- Bestehende `- Kopie`-Suffixe werden fachlich normalisiert, damit keine Titel wie `... - Kopie - Kopie` mehr neu entstehen
|
||||||
- Kreis grau/grün
|
- Kreis grau/grün
|
||||||
- Schaltet `isActive`
|
- Schaltet `isActive`
|
||||||
- Ist bewusst kein Auge mehr
|
- Ist bewusst kein Auge mehr
|
||||||
|
|
||||||
|
Wichtiger technischer Punkt:
|
||||||
|
|
||||||
|
- Die Frontend-Listenaktionen arbeiten nur noch mit dem Contao-`REQUEST_TOKEN` plus Berechtigungsprüfung.
|
||||||
|
- Die zusätzliche Symfony-Aktions-CSRF-Prüfung wurde bewusst entfernt, weil sie in diesem Frontend-Flow zu ungültigen Tokens führte.
|
||||||
|
|
||||||
### 2. Frontend-Editor
|
### 2. Frontend-Editor
|
||||||
|
|
||||||
Controller:
|
Controller:
|
||||||
@@ -154,6 +164,9 @@ Wichtige Entscheidung:
|
|||||||
- Typ, Pflicht usw. sollen nicht schon im Header gezeigt werden.
|
- Typ, Pflicht usw. sollen nicht schon im Header gezeigt werden.
|
||||||
- Die Details sieht man erst nach dem Aufklappen.
|
- Die Details sieht man erst nach dem Aufklappen.
|
||||||
|
|
||||||
|
- Die Sprungziel-Dropdowns im Frontend zeigen Positionsnummern der Umfrage (`1. ...`, `2. ...`) statt globaler Datenbank-IDs.
|
||||||
|
- Die aktuell bearbeitete Frage wird dort nicht als eigenes Sprungziel angeboten.
|
||||||
|
|
||||||
### 3. Öffentliche Umfrageansicht
|
### 3. Öffentliche Umfrageansicht
|
||||||
|
|
||||||
Controller:
|
Controller:
|
||||||
@@ -232,6 +245,13 @@ Reihenfolge der Navigation:
|
|||||||
Wichtiger Detailpunkt:
|
Wichtiger Detailpunkt:
|
||||||
|
|
||||||
- In der Sprungregelübersicht im Frontend-Editor werden bewusst die Positionsnummern innerhalb der Umfrage angezeigt, nicht die globalen Datenbank-IDs der Fragen.
|
- In der Sprungregelübersicht im Frontend-Editor werden bewusst die Positionsnummern innerhalb der Umfrage angezeigt, nicht die globalen Datenbank-IDs der Fragen.
|
||||||
|
- Gleiches gilt für die Sprungziel-Dropdowns in Backend und Frontend.
|
||||||
|
|
||||||
|
Wichtige technische Eigenschaft:
|
||||||
|
|
||||||
|
- `jumpOnYes`, `jumpOnNo` und generische Bedingungen referenzieren Ziel-Fragen intern immer über die Frage-ID, nicht über die Position.
|
||||||
|
- Dadurch bleiben Sprünge bei einer reinen Umordnung der Fragen fachlich stabil an dieselbe Frage gebunden.
|
||||||
|
- Wenn eine Ziel-Frage an eine andere Position verschoben wird, zeigt die Oberfläche danach automatisch die neue Positionsnummer an.
|
||||||
|
|
||||||
## Excel-Export
|
## Excel-Export
|
||||||
|
|
||||||
@@ -315,6 +335,26 @@ Wichtige Listenoperationen:
|
|||||||
- `Link kopieren`
|
- `Link kopieren`
|
||||||
- `Ergebnisse anzeigen`
|
- `Ergebnisse anzeigen`
|
||||||
|
|
||||||
|
Aktuelle Listen- und Filterlogik:
|
||||||
|
|
||||||
|
- Die mehrspaltige Umfragenliste zeigt aktuell:
|
||||||
|
- `Titel`
|
||||||
|
- `Zugewiesene Benutzer`
|
||||||
|
- `Vorlage`
|
||||||
|
- `Interne Notiz`
|
||||||
|
- `Kategorien` wurden bewusst aus der mehrspaltigen Liste und aus der rechten Filterspalte entfernt.
|
||||||
|
- Die Spalte `Zugewiesene Benutzer` wird in der Listenmetadatenlogik gekürzt:
|
||||||
|
- maximal 3 Namen direkt sichtbar
|
||||||
|
- danach Zusatz wie `... und X weitere`
|
||||||
|
- `internalNote`
|
||||||
|
- backend-only Feld in `tl_survey`
|
||||||
|
- nur im Backend bearbeitbar
|
||||||
|
- in der mehrspaltigen Liste sichtbar
|
||||||
|
- bei Duplikaten wird der Wert mitkopiert
|
||||||
|
- In der rechten Suchspalte ist `Interne Notiz` suchbar.
|
||||||
|
- `ID` wurde für die Umfragenliste bewusst aus dem Suchfeld entfernt.
|
||||||
|
- Das geschieht über einen kleinen backend-spezifischen JS-Workaround, weil Contao `id` im Core für `DC_Table` hart ergänzt.
|
||||||
|
|
||||||
Aktuelle Backend-Entscheidung:
|
Aktuelle Backend-Entscheidung:
|
||||||
|
|
||||||
- Das Warnsymbol genügt als Hinweis bei veröffentlichten Umfragen
|
- Das Warnsymbol genügt als Hinweis bei veröffentlichten Umfragen
|
||||||
@@ -329,6 +369,21 @@ Aktuelle Backend-Entscheidung:
|
|||||||
Hinweis:
|
Hinweis:
|
||||||
|
|
||||||
- `isLocked` wird in den Child-Headerfeldern nicht mehr angezeigt, weil der Backend-Lock bewusst keine führende Benutzerfunktion mehr ist.
|
- `isLocked` wird in den Child-Headerfeldern nicht mehr angezeigt, weil der Backend-Lock bewusst keine führende Benutzerfunktion mehr ist.
|
||||||
|
- In `tl_survey_content` ist der Fragenlisten-Header zusätzlich reduziert:
|
||||||
|
- nur noch `Titel` und `Veröffentlicht`
|
||||||
|
- kein öffentlicher Link-Schlüssel mehr
|
||||||
|
- die rechte Header-Leiste mit Aktionen ist im Normalzustand ausgeblendet
|
||||||
|
- im aktiven Verschiebe-/Clipboard-Modus wird sie wieder eingeblendet, damit `Neu oben` bzw. die Einfügepositionen erreichbar bleiben
|
||||||
|
|
||||||
|
Fragenliste `tl_survey_content`:
|
||||||
|
|
||||||
|
- Direkt sichtbare Primäraktion pro Frage:
|
||||||
|
- `Bearbeiten`-Stift
|
||||||
|
- Verschieben:
|
||||||
|
- erfolgt im Backend über den normalen Contao-Paste-/Move-Flow
|
||||||
|
- die Aktion `Verschieben` liegt bewusst nur unter den drei Punkten
|
||||||
|
- dort wird der Contao-Pfeil `mover.svg` verwendet, nicht die blaue Schere
|
||||||
|
- nach Aktivierung des Clipboard-Zustands erscheinen die üblichen Einfügeziele wie `Nach dem Element ... einfügen`
|
||||||
|
|
||||||
### Systemeinstellungen
|
### Systemeinstellungen
|
||||||
|
|
||||||
@@ -356,6 +411,7 @@ Wichtige Settings:
|
|||||||
- `published` reversibel erlaubt
|
- `published` reversibel erlaubt
|
||||||
- `isActive` per Standard-Contao-Toggle
|
- `isActive` per Standard-Contao-Toggle
|
||||||
- nur Warnsymbol statt Backend-Sperre
|
- nur Warnsymbol statt Backend-Sperre
|
||||||
|
- Admin darf Fragen auch nach Veröffentlichung oder nach vorhandenen Antworten umsortieren
|
||||||
|
|
||||||
## Wichtige Dateien nach Themengebiet
|
## Wichtige Dateien nach Themengebiet
|
||||||
|
|
||||||
@@ -398,12 +454,35 @@ Backend:
|
|||||||
- `ddev exec php bin/console lint:yaml ...`
|
- `ddev exec php bin/console lint:yaml ...`
|
||||||
- `ddev exec php bin/console cache:clear`
|
- `ddev exec php bin/console cache:clear`
|
||||||
|
|
||||||
|
## Offene technische Punkte / Deployment-Hinweise
|
||||||
|
|
||||||
|
- Neue DB-Spalte in `tl_survey`:
|
||||||
|
- `internalNote`
|
||||||
|
- Typ aktuell: `varchar(255) NOT NULL default ''`
|
||||||
|
- Für andere Umgebungen muss diese Spalte per Migration/SQL vorhanden sein, sonst fehlen Backend-Feld, Listenanzeige und Suchfunktion fachlich bzw. führen zu Inkonsistenzen.
|
||||||
|
|
||||||
|
- Nach Änderungen an DCA, Backend-CSS oder Backend-JS ist in dieser Installation typischerweise nötig:
|
||||||
|
- `php vendor/bin/contao-console assets:install public --symlink --relative`
|
||||||
|
- `php vendor/bin/contao-console cache:clear --env=prod`
|
||||||
|
|
||||||
|
- Es gibt aktuell zwei bewusst eingebaute backend-spezifische UI-Workarounds über Bundle-Assets:
|
||||||
|
- In der Umfragenliste wird `ID` aus dem Suchfeld per JS entfernt, weil Contao das Feld im Core von `DC_Table` hart ergänzt.
|
||||||
|
- In der Fragenliste (`table=tl_survey_content`) wird die rechte Parent-Header-Leiste per CSS/JS im Normalzustand ausgeblendet und nur im aktiven Clipboard-/Verschiebe-Modus wieder eingeblendet.
|
||||||
|
|
||||||
|
- Lokale DDEV-Besonderheit, die bei Fehleranalyse relevant sein kann:
|
||||||
|
- Das Bundle läuft hier als Path-Repository/Symlink.
|
||||||
|
- Wenn Backend-Übersetzungen oder Assets trotz korrektem Code unerwartet fehlen, zuerst Bundle-Pfad, veröffentlichte Assets und Prod-Cache prüfen.
|
||||||
|
|
||||||
## Bekannte bewusste Trade-offs
|
## Bekannte bewusste Trade-offs
|
||||||
|
|
||||||
- Backend kann veröffentlichte Umfragen technisch weiterhin verändern.
|
- Backend kann veröffentlichte Umfragen technisch weiterhin verändern.
|
||||||
- Das ist bewusst akzeptiert, weil nur ein Admin arbeitet.
|
- Das ist bewusst akzeptiert, weil nur ein Admin arbeitet.
|
||||||
- `isLocked` bleibt als internes Logikfeld bestehen, obwohl der Backend-Lock nicht mehr durchgesetzt wird.
|
- `isLocked` bleibt als internes Logikfeld bestehen, obwohl der Backend-Lock nicht mehr durchgesetzt wird.
|
||||||
- PDF-Export hängt von Gotenberg ab und ist absichtlich optional.
|
- PDF-Export hängt von Gotenberg ab und ist absichtlich optional.
|
||||||
|
- Eine reine Umordnung von Fragen ist für Sprungziele logisch unkritisch, weil Sprünge an Frage-IDs hängen.
|
||||||
|
- Ergebnisse vorhandener, weiterhin existierender Fragen folgen bei der Anzeige jedoch der aktuellen Sortierung der Frage.
|
||||||
|
- Historische Antwort-Snapshots speichern zwar `questionSorting`, die Ergebnisansicht sortiert vorhandene Fragen aber primär nach der aktuellen Frage-Sortierung.
|
||||||
|
- Das ist im toleranten Backend-Modell ein bewusst akzeptierter Side-Effect.
|
||||||
|
|
||||||
## Empfohlene Einstiegspunkte für künftige Änderungen
|
## Empfohlene Einstiegspunkte für künftige Änderungen
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
$GLOBALS['TL_CSS'][] = 'assets/survey-backend.css|static';
|
$GLOBALS['TL_CSS'][] = 'bundles/survey/css/survey-backend.css|static';
|
||||||
$GLOBALS['TL_CSS'][] = 'bundles/survey/css/auth-module.css|static';
|
$GLOBALS['TL_CSS'][] = 'bundles/survey/css/auth-module.css|static';
|
||||||
|
$GLOBALS['TL_JAVASCRIPT'][] = 'bundles/survey/js/survey-backend.js|static';
|
||||||
|
|
||||||
$GLOBALS['TL_MODELS']['tl_survey'] = Mummert\SurveyBundle\Model\SurveyModel::class;
|
$GLOBALS['TL_MODELS']['tl_survey'] = Mummert\SurveyBundle\Model\SurveyModel::class;
|
||||||
$GLOBALS['TL_MODELS']['tl_survey_category'] = Mummert\SurveyBundle\Model\SurveyCategoryModel::class;
|
$GLOBALS['TL_MODELS']['tl_survey_category'] = Mummert\SurveyBundle\Model\SurveyCategoryModel::class;
|
||||||
|
|||||||
+21
-11
@@ -36,7 +36,7 @@ $GLOBALS['TL_DCA']['tl_survey'] = [
|
|||||||
'panelLayout' => 'filter;sort,search,limit',
|
'panelLayout' => 'filter;sort,search,limit',
|
||||||
],
|
],
|
||||||
'label' => [
|
'label' => [
|
||||||
'fields' => ['title', 'categorySummary', 'assignedMembersSummary', 'templateSummary'],
|
'fields' => ['title', 'assignedMembersSummary', 'templateSummary', 'internalNote'],
|
||||||
'showColumns' => true,
|
'showColumns' => true,
|
||||||
],
|
],
|
||||||
'global_operations' => [
|
'global_operations' => [
|
||||||
@@ -47,22 +47,26 @@ $GLOBALS['TL_DCA']['tl_survey'] = [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
'operations' => [
|
'operations' => [
|
||||||
|
'editheader' => [
|
||||||
|
'label' => ['Umfrage bearbeiten', 'Umfrage bearbeiten'],
|
||||||
|
'href' => 'act=edit',
|
||||||
|
'icon' => 'edit.svg',
|
||||||
|
'primary' => true,
|
||||||
|
'showInHeader' => false,
|
||||||
|
],
|
||||||
'edit' => [
|
'edit' => [
|
||||||
'label' => ['Fragen bearbeiten', 'Fragen der Umfrage bearbeiten'],
|
'label' => ['Fragen bearbeiten', 'Fragen der Umfrage bearbeiten'],
|
||||||
'href' => 'table=tl_survey_content',
|
'href' => 'table=tl_survey_content',
|
||||||
'icon' => 'children.svg',
|
'icon' => 'children.svg',
|
||||||
],
|
'primary' => true,
|
||||||
'editheader' => [
|
'showInHeader' => false,
|
||||||
'label' => ['Umfrage bearbeiten', 'Umfrage bearbeiten'],
|
|
||||||
'href' => 'act=edit',
|
|
||||||
'icon' => 'header.svg',
|
|
||||||
],
|
],
|
||||||
'toggle' => [
|
'toggle' => [
|
||||||
'label' => &$GLOBALS['TL_LANG']['tl_survey']['toggleActive'],
|
'label' => &$GLOBALS['TL_LANG']['tl_survey']['toggleActive'],
|
||||||
'href' => 'act=toggle&field=isActive',
|
'href' => 'act=toggle&field=isActive',
|
||||||
'icon' => 'visible.svg',
|
'icon' => 'visible.svg',
|
||||||
'primary' => true,
|
'primary' => true,
|
||||||
'showInHeader' => true,
|
'showInHeader' => false,
|
||||||
],
|
],
|
||||||
'publishedWarning' => [
|
'publishedWarning' => [
|
||||||
'label' => &$GLOBALS['TL_LANG']['tl_survey']['publishedWarning'],
|
'label' => &$GLOBALS['TL_LANG']['tl_survey']['publishedWarning'],
|
||||||
@@ -100,7 +104,7 @@ $GLOBALS['TL_DCA']['tl_survey'] = [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
'palettes' => [
|
'palettes' => [
|
||||||
'default' => '{title_legend},title,category,description;{assignment_legend},assignedMembers;{publishing_legend},published,isActive,isTemplate;{meta_legend},createdAt,updatedAt',
|
'default' => '{title_legend},title,category,description,internalNote;{assignment_legend},assignedMembers;{publishing_legend},published,isActive,isTemplate;{meta_legend},createdAt,updatedAt',
|
||||||
],
|
],
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'id' => [
|
'id' => [
|
||||||
@@ -129,9 +133,15 @@ $GLOBALS['TL_DCA']['tl_survey'] = [
|
|||||||
'eval' => ['rte' => 'tinyMCE', 'tl_class' => 'clr'],
|
'eval' => ['rte' => 'tinyMCE', 'tl_class' => 'clr'],
|
||||||
'sql' => 'text NULL',
|
'sql' => 'text NULL',
|
||||||
],
|
],
|
||||||
|
'internalNote' => [
|
||||||
|
'label' => &$GLOBALS['TL_LANG']['tl_survey']['internalNote'],
|
||||||
|
'inputType' => 'text',
|
||||||
|
'search' => true,
|
||||||
|
'eval' => ['maxlength' => 255, 'tl_class' => 'clr w50'],
|
||||||
|
'sql' => "varchar(255) NOT NULL default ''",
|
||||||
|
],
|
||||||
'category' => [
|
'category' => [
|
||||||
'label' => &$GLOBALS['TL_LANG']['tl_survey']['category'],
|
'label' => &$GLOBALS['TL_LANG']['tl_survey']['category'],
|
||||||
'filter' => true,
|
|
||||||
'inputType' => 'select',
|
'inputType' => 'select',
|
||||||
'foreignKey' => 'tl_survey_category.title',
|
'foreignKey' => 'tl_survey_category.title',
|
||||||
'eval' => ['multiple' => true, 'chosen' => true, 'tl_class' => 'clr'],
|
'eval' => ['multiple' => true, 'chosen' => true, 'tl_class' => 'clr'],
|
||||||
@@ -188,9 +198,9 @@ $GLOBALS['TL_DCA']['tl_survey'] = [
|
|||||||
],
|
],
|
||||||
'assignedMembers' => [
|
'assignedMembers' => [
|
||||||
'label' => &$GLOBALS['TL_LANG']['tl_survey']['assignedMembers'],
|
'label' => &$GLOBALS['TL_LANG']['tl_survey']['assignedMembers'],
|
||||||
'inputType' => 'select',
|
'inputType' => 'checkboxWizard',
|
||||||
'options_callback' => [Mummert\SurveyBundle\EventListener\SurveyDcaListener::class, 'getMemberOptions'],
|
'options_callback' => [Mummert\SurveyBundle\EventListener\SurveyDcaListener::class, 'getMemberOptions'],
|
||||||
'eval' => ['multiple' => true, 'chosen' => true, 'tl_class' => 'clr'],
|
'eval' => ['multiple' => true, 'class' => 'survey-assigned-members-checkbox-wizard', 'tl_class' => 'clr'],
|
||||||
'load_callback' => [[Mummert\SurveyBundle\EventListener\SurveyDcaListener::class, 'loadSurveyAssignedMembers']],
|
'load_callback' => [[Mummert\SurveyBundle\EventListener\SurveyDcaListener::class, 'loadSurveyAssignedMembers']],
|
||||||
'save_callback' => [[Mummert\SurveyBundle\EventListener\SurveyDcaListener::class, 'saveSurveyAssignedMembers']],
|
'save_callback' => [[Mummert\SurveyBundle\EventListener\SurveyDcaListener::class, 'saveSurveyAssignedMembers']],
|
||||||
'sql' => 'blob NULL',
|
'sql' => 'blob NULL',
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ $GLOBALS['TL_DCA']['tl_survey_content'] = [
|
|||||||
'sorting' => [
|
'sorting' => [
|
||||||
'mode' => 4,
|
'mode' => 4,
|
||||||
'fields' => ['sorting'],
|
'fields' => ['sorting'],
|
||||||
'headerFields' => ['title', 'alias', 'published'],
|
'headerFields' => ['title', 'published'],
|
||||||
'child_record_class' => 'survey-question-record',
|
'child_record_class' => 'survey-question-record',
|
||||||
'panelLayout' => 'sort,search,limit',
|
'panelLayout' => 'sort,search,limit',
|
||||||
],
|
],
|
||||||
@@ -44,6 +44,13 @@ $GLOBALS['TL_DCA']['tl_survey_content'] = [
|
|||||||
'edit' => [
|
'edit' => [
|
||||||
'href' => 'act=edit',
|
'href' => 'act=edit',
|
||||||
'icon' => 'edit.svg',
|
'icon' => 'edit.svg',
|
||||||
|
'primary' => true,
|
||||||
|
],
|
||||||
|
'cut' => [
|
||||||
|
'href' => 'act=paste&mode=cut',
|
||||||
|
'method' => 'POST',
|
||||||
|
'icon' => 'mover.svg',
|
||||||
|
'attributes' => 'data-action="contao--scroll-offset#store"',
|
||||||
],
|
],
|
||||||
'copy' => [
|
'copy' => [
|
||||||
'href' => 'act=copy',
|
'href' => 'act=copy',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ $GLOBALS['TL_LANG']['tl_survey']['templateSummary'] = ['Vorlage', 'Zeigt an, ob
|
|||||||
$GLOBALS['TL_LANG']['tl_survey']['template_yes'] = 'Ja';
|
$GLOBALS['TL_LANG']['tl_survey']['template_yes'] = 'Ja';
|
||||||
$GLOBALS['TL_LANG']['tl_survey']['template_no'] = 'Nein';
|
$GLOBALS['TL_LANG']['tl_survey']['template_no'] = 'Nein';
|
||||||
$GLOBALS['TL_LANG']['tl_survey']['description'] = ['Beschreibung', 'Einleitung für den Editor und das Frontend.'];
|
$GLOBALS['TL_LANG']['tl_survey']['description'] = ['Beschreibung', 'Einleitung für den Editor und das Frontend.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_survey']['internalNote'] = ['Interne Notiz', 'Nur im Backend sichtbare interne Notiz zur Umfrage.'];
|
||||||
$GLOBALS['TL_LANG']['tl_survey']['title_legend'] = 'Grunddaten';
|
$GLOBALS['TL_LANG']['tl_survey']['title_legend'] = 'Grunddaten';
|
||||||
$GLOBALS['TL_LANG']['tl_survey']['assignment_legend'] = 'Bearbeitende Mitglieder';
|
$GLOBALS['TL_LANG']['tl_survey']['assignment_legend'] = 'Bearbeitende Mitglieder';
|
||||||
$GLOBALS['TL_LANG']['tl_survey']['publishing_legend'] = 'Sichtbarkeit';
|
$GLOBALS['TL_LANG']['tl_survey']['publishing_legend'] = 'Sichtbarkeit';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ $GLOBALS['TL_LANG']['tl_survey']['templateSummary'] = ['Template', 'Shows whethe
|
|||||||
$GLOBALS['TL_LANG']['tl_survey']['template_yes'] = 'Yes';
|
$GLOBALS['TL_LANG']['tl_survey']['template_yes'] = 'Yes';
|
||||||
$GLOBALS['TL_LANG']['tl_survey']['template_no'] = 'No';
|
$GLOBALS['TL_LANG']['tl_survey']['template_no'] = 'No';
|
||||||
$GLOBALS['TL_LANG']['tl_survey']['description'] = ['Description', 'Introduction for the editor and the frontend.'];
|
$GLOBALS['TL_LANG']['tl_survey']['description'] = ['Description', 'Introduction for the editor and the frontend.'];
|
||||||
|
$GLOBALS['TL_LANG']['tl_survey']['internalNote'] = ['Internal note', 'Internal note for the survey, visible in the backend only.'];
|
||||||
$GLOBALS['TL_LANG']['tl_survey']['title_legend'] = 'Basic data';
|
$GLOBALS['TL_LANG']['tl_survey']['title_legend'] = 'Basic data';
|
||||||
$GLOBALS['TL_LANG']['tl_survey']['assignment_legend'] = 'Editing members';
|
$GLOBALS['TL_LANG']['tl_survey']['assignment_legend'] = 'Editing members';
|
||||||
$GLOBALS['TL_LANG']['tl_survey']['publishing_legend'] = 'Visibility';
|
$GLOBALS['TL_LANG']['tl_survey']['publishing_legend'] = 'Visibility';
|
||||||
|
|||||||
@@ -708,6 +708,11 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-accordion-header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
.survey-accordion-toggle {
|
.survey-accordion-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -720,6 +725,82 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-accordion-sort-handle {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 3.5rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
border: 0;
|
||||||
|
border-left: 1px solid rgba(162, 168, 180, 0.18);
|
||||||
|
background: transparent;
|
||||||
|
cursor: grab;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: stretch;
|
||||||
|
transition: background 0.2s ease, color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-sort-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-item .handorgel__header__button:hover + .survey-accordion-sort-handle,
|
||||||
|
.survey-accordion-item .handorgel__header__button:focus + .survey-accordion-sort-handle,
|
||||||
|
.survey-accordion-item .handorgel__header--opened .survey-accordion-sort-handle,
|
||||||
|
.survey-accordion-sort-handle:hover,
|
||||||
|
.survey-accordion-sort-handle:focus {
|
||||||
|
background: var(--survey-blue);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-sort-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 0.95rem;
|
||||||
|
height: 1.28rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #58a6da;
|
||||||
|
line-height: 0;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-sort-icon svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-order-save-shell {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 40;
|
||||||
|
transform: translate(120%, -50%);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: transform 0.26s ease, opacity 0.26s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-order-save-shell.is-visible {
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-order-save {
|
||||||
|
box-shadow: 0 18px 40px rgba(0, 59, 102, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-item.is-reordering {
|
||||||
|
box-shadow: 0 20px 44px rgba(0, 59, 102, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-item.sortable-ghost {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
.survey-accordion-item .handorgel__header__button,
|
.survey-accordion-item .handorgel__header__button,
|
||||||
.survey-accordion-item .handorgel__header__button:hover,
|
.survey-accordion-item .handorgel__header__button:hover,
|
||||||
.survey-accordion-item .handorgel__header__button:focus {
|
.survey-accordion-item .handorgel__header__button:focus {
|
||||||
@@ -994,6 +1075,22 @@
|
|||||||
padding: 0.95rem 1rem;
|
padding: 0.95rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.survey-accordion-sort-handle {
|
||||||
|
min-width: 3.1rem;
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-order-save-shell {
|
||||||
|
top: auto;
|
||||||
|
right: 0.85rem;
|
||||||
|
bottom: 0.85rem;
|
||||||
|
transform: translate(0, 130%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-order-save-shell.is-visible {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
@@ -1152,6 +1249,133 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var initializeQuestionSorting = function () {
|
||||||
|
var styleId = 'survey-question-sort-icon-runtime';
|
||||||
|
|
||||||
|
if (!document.getElementById(styleId)) {
|
||||||
|
var runtimeStyle = document.createElement('style');
|
||||||
|
runtimeStyle.id = styleId;
|
||||||
|
runtimeStyle.textContent = '' +
|
||||||
|
'.survey-sort-icon{' +
|
||||||
|
'width:1.14rem !important;' +
|
||||||
|
'height:1.52rem !important;' +
|
||||||
|
'color:#58a6da !important;' +
|
||||||
|
'background:none !important;' +
|
||||||
|
'transform:translateY(1px) !important;' +
|
||||||
|
'}' +
|
||||||
|
'.survey-sort-icon::before,.survey-sort-icon::after{' +
|
||||||
|
'content:none !important;' +
|
||||||
|
'}' +
|
||||||
|
'.survey-sort-icon svg{' +
|
||||||
|
'display:block !important;' +
|
||||||
|
'width:100% !important;' +
|
||||||
|
'height:100% !important;' +
|
||||||
|
'transform:translateY(4px);' +
|
||||||
|
'}' +
|
||||||
|
'.survey-sort-icon svg path:first-child{' +
|
||||||
|
'transform:translateY(1.4px);' +
|
||||||
|
'transform-box:fill-box;' +
|
||||||
|
'transform-origin:center;' +
|
||||||
|
'}' +
|
||||||
|
'.survey-sort-icon svg path:nth-child(2){' +
|
||||||
|
'transform:translateY(-1.4px);' +
|
||||||
|
'transform-box:fill-box;' +
|
||||||
|
'transform-origin:center;' +
|
||||||
|
'}' +
|
||||||
|
'.survey-sort-icon svg path[stroke]{' +
|
||||||
|
'stroke-width:2px;' +
|
||||||
|
'}';
|
||||||
|
|
||||||
|
document.head.appendChild(runtimeStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Sortable !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortableList = document.querySelector('[data-survey-question-sortable]');
|
||||||
|
var orderForm = document.querySelector('[data-survey-question-order-shell]');
|
||||||
|
var orderInput = document.querySelector('[data-survey-question-order-input]');
|
||||||
|
|
||||||
|
if (!sortableList || !orderForm || !orderInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortableList._surveyQuestionSortable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var getOrderedIds = function () {
|
||||||
|
return Array.from(sortableList.querySelectorAll('[data-survey-question-item]')).map(function (item) {
|
||||||
|
return item.getAttribute('data-question-id') || '';
|
||||||
|
}).filter(function (value) {
|
||||||
|
return value !== '';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var initialOrder = getOrderedIds().join(',');
|
||||||
|
|
||||||
|
var updateOrderField = function () {
|
||||||
|
orderInput.value = getOrderedIds().join(',');
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateQuestionIndices = function () {
|
||||||
|
sortableList.querySelectorAll('[data-survey-question-item]').forEach(function (item, index) {
|
||||||
|
var indexNode = item.querySelector('.survey-accordion-index');
|
||||||
|
|
||||||
|
if (indexNode) {
|
||||||
|
indexNode.textContent = (index + 1) + '.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateSaveVisibility = function () {
|
||||||
|
var currentOrder = getOrderedIds().join(',');
|
||||||
|
var isDirty = currentOrder !== initialOrder;
|
||||||
|
|
||||||
|
orderForm.hidden = !isDirty;
|
||||||
|
orderForm.classList.toggle('is-visible', isDirty);
|
||||||
|
updateOrderField();
|
||||||
|
};
|
||||||
|
|
||||||
|
sortableList.querySelectorAll('[data-survey-question-sort-handle]').forEach(function (handle) {
|
||||||
|
handle.addEventListener('click', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateQuestionIndices();
|
||||||
|
updateOrderField();
|
||||||
|
updateSaveVisibility();
|
||||||
|
|
||||||
|
var sortableInstance = Sortable.create(sortableList, {
|
||||||
|
animation: 180,
|
||||||
|
draggable: '[data-survey-question-item]',
|
||||||
|
handle: '[data-survey-question-sort-handle]',
|
||||||
|
ghostClass: 'sortable-ghost',
|
||||||
|
forceFallback: true,
|
||||||
|
fallbackTolerance: 3,
|
||||||
|
onStart: function (event) {
|
||||||
|
if (event.item) {
|
||||||
|
event.item.classList.add('is-reordering');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEnd: function (event) {
|
||||||
|
if (event.item) {
|
||||||
|
event.item.classList.remove('is-reordering');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQuestionIndices();
|
||||||
|
updateSaveVisibility();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sortableList._surveyQuestionSortable = sortableInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.initializeSurveyQuestionSorting = initializeQuestionSorting;
|
||||||
|
|
||||||
if (typeof handorgel === 'function') {
|
if (typeof handorgel === 'function') {
|
||||||
document.querySelectorAll('[data-survey-handorgel]').forEach(function (element) {
|
document.querySelectorAll('[data-survey-handorgel]').forEach(function (element) {
|
||||||
var accordion = new handorgel(element, {
|
var accordion = new handorgel(element, {
|
||||||
@@ -1171,6 +1395,8 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeQuestionSorting();
|
||||||
|
|
||||||
resetEditorViewport();
|
resetEditorViewport();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<style>
|
||||||
|
.survey-accordion-header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-sort-handle {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 3.5rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
border: 0;
|
||||||
|
border-left: 1px solid rgba(162, 168, 180, 0.18);
|
||||||
|
background: transparent;
|
||||||
|
cursor: grab;
|
||||||
|
transition: background 0.2s ease, color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-sort-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-item .handorgel__header__button:hover + .survey-accordion-sort-handle,
|
||||||
|
.survey-accordion-item .handorgel__header__button:focus + .survey-accordion-sort-handle,
|
||||||
|
.survey-accordion-item .handorgel__header--opened .survey-accordion-sort-handle,
|
||||||
|
.survey-accordion-sort-handle:hover,
|
||||||
|
.survey-accordion-sort-handle:focus {
|
||||||
|
background: var(--survey-blue);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-sort-icon {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 0.9rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #58a6da;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(currentColor, currentColor),
|
||||||
|
linear-gradient(currentColor, currentColor),
|
||||||
|
linear-gradient(currentColor, currentColor);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center 0.36rem, center center, center calc(100% - 0.36rem);
|
||||||
|
background-size: 0.78rem 2px, 0.78rem 2px, 0.78rem 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-sort-icon::before,
|
||||||
|
.survey-sort-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 0.24rem solid transparent;
|
||||||
|
border-right: 0.24rem solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-sort-icon::before {
|
||||||
|
top: 0;
|
||||||
|
border-bottom: 0.24rem solid currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-sort-icon::after {
|
||||||
|
bottom: 0;
|
||||||
|
border-top: 0.24rem solid currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-order-save-shell {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 40;
|
||||||
|
transform: translate(120%, -50%);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: transform 0.26s ease, opacity 0.26s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-order-save-shell.is-visible {
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-order-save {
|
||||||
|
box-shadow: 0 18px 40px rgba(0, 59, 102, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-item.is-reordering {
|
||||||
|
box-shadow: 0 20px 44px rgba(0, 59, 102, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-accordion-item.sortable-ghost {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.survey-accordion-sort-handle {
|
||||||
|
min-width: 3.1rem;
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-order-save-shell {
|
||||||
|
top: auto;
|
||||||
|
right: 0.85rem;
|
||||||
|
bottom: 0.85rem;
|
||||||
|
transform: translate(0, 130%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-order-save-shell.is-visible {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
|
||||||
|
<script src="{{ asset('bundles/survey/js/survey-frontend-editor-sort.js') }}"></script>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{% trans_default_domain 'messages' %}
|
{% trans_default_domain 'messages' %}
|
||||||
|
|
||||||
{% include '@Survey/frontend/_survey_assets.html.twig' %}
|
{% include '@Survey/frontend/_survey_assets.html.twig' %}
|
||||||
|
{% include '@Survey/frontend/_survey_question_sort_assets.html.twig' %}
|
||||||
<link rel="stylesheet" href="{{ asset('assets/handorgel/css/handorgel.min.css') }}">
|
<link rel="stylesheet" href="{{ asset('assets/handorgel/css/handorgel.min.css') }}">
|
||||||
{% include '@Survey/frontend/_survey_branding.html.twig' %}
|
{% include '@Survey/frontend/_survey_branding.html.twig' %}
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% set surveyMetaFormId = 'survey-meta-form' %}
|
{% set surveyMetaFormId = 'survey-meta-form' %}
|
||||||
{% set newQuestionFormId = 'survey-question-form-new' %}
|
{% set newQuestionFormId = 'survey-question-form-new' %}
|
||||||
|
{% set canQuestionReorder = canQuestionReorder|default(false) %}
|
||||||
|
|
||||||
<div class="survey-button-row survey-button-row--editor-actions">
|
<div class="survey-button-row survey-button-row--editor-actions">
|
||||||
{% if backUrl %}<a class="survey-button secondary" href="{{ backUrl }}">{{ 'survey.edit.back_to_list'|trans }}</a>{% endif %}
|
{% if backUrl %}<a class="survey-button secondary" href="{{ backUrl }}">{{ 'survey.edit.back_to_list'|trans }}</a>{% endif %}
|
||||||
@@ -74,14 +76,25 @@
|
|||||||
|
|
||||||
<div class="survey-grid-columns survey-editor-layout">
|
<div class="survey-grid-columns survey-editor-layout">
|
||||||
<div class="survey-question-editor-shell" data-survey-question-editor>
|
<div class="survey-question-editor-shell" data-survey-question-editor>
|
||||||
<div class="survey-accordion-list handorgel" data-survey-handorgel>
|
{% if canQuestionReorder %}
|
||||||
|
<form method="post" action="{{ metadataFormActionUrl }}" class="survey-question-order-save-shell" data-survey-question-order-shell hidden>
|
||||||
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
|
<input type="hidden" name="_survey_action" value="reorder-questions">
|
||||||
|
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('reorder-questions-' ~ survey.id) }}">
|
||||||
|
<input type="hidden" name="question_order" value="" data-survey-question-order-input>
|
||||||
|
<button class="survey-button primary survey-question-order-save">{{ 'survey.edit.save_question_order'|trans }}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<div class="survey-accordion-list handorgel" data-survey-handorgel{% if canQuestionReorder %} data-survey-question-sortable="1"{% endif %}>
|
||||||
{% for question in questions %}
|
{% for question in questions %}
|
||||||
{% set questionAnchor = 'survey-question-' ~ question.id %}
|
{% set questionAnchor = 'survey-question-' ~ question.id %}
|
||||||
{% set isOpen = activeQuestionId == question.id %}
|
{% set isOpen = activeQuestionId == question.id %}
|
||||||
{% set questionItemForm = questionForms[question.id]|default(null) %}
|
{% set questionItemForm = questionForms[question.id]|default(null) %}
|
||||||
{% set questionFormId = 'survey-question-form-' ~ question.id %}
|
{% set questionFormId = 'survey-question-form-' ~ question.id %}
|
||||||
<article id="{{ questionAnchor }}" class="survey-accordion-item">
|
<article id="{{ questionAnchor }}" class="survey-accordion-item"{% if canQuestionReorder %} data-survey-question-item data-question-id="{{ question.id }}"{% endif %}>
|
||||||
<h4 class="survey-accordion-header handorgel__header">
|
<h4 class="survey-accordion-header handorgel__header">
|
||||||
|
<div class="survey-accordion-header-row">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="survey-accordion-toggle handorgel__header__button"
|
class="survey-accordion-toggle handorgel__header__button"
|
||||||
@@ -93,6 +106,18 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
{% if canQuestionReorder %}
|
||||||
|
<button type="button" class="survey-accordion-sort-handle" data-survey-question-sort-handle aria-label="{{ 'survey.edit.reorder_question'|trans }}" title="{{ 'survey.edit.reorder_question'|trans }}">
|
||||||
|
<span class="survey-sort-icon" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 18 24" role="presentation" focusable="false">
|
||||||
|
<path d="M9 3 11.6 5.8H6.4z" fill="currentColor"></path>
|
||||||
|
<path d="M9 21 6.4 18.2h5.2z" fill="currentColor"></path>
|
||||||
|
<path d="M4.5 12h9" stroke="currentColor" stroke-linecap="round" stroke-width="1.8"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="survey-accordion-panel handorgel__content">
|
<div class="survey-accordion-panel handorgel__content">
|
||||||
<div class="handorgel__content__inner" data-survey-question-editor>
|
<div class="handorgel__content__inner" data-survey-question-editor>
|
||||||
|
|||||||
@@ -53,7 +53,6 @@
|
|||||||
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
<input type="hidden" name="_survey_action" value="duplicate-survey">
|
<input type="hidden" name="_survey_action" value="duplicate-survey">
|
||||||
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||||
<input type="hidden" name="_token" value="{{ csrf_token('duplicate-survey-' ~ survey.id) }}">
|
|
||||||
<button class="survey-button secondary" onclick="return confirm('{{ 'survey.list.duplicate_confirm'|trans({'%title%': survey.title|striptags|trim})|e('js') }}');">{{ 'survey.list.duplicate'|trans }}</button>
|
<button class="survey-button secondary" onclick="return confirm('{{ 'survey.list.duplicate_confirm'|trans({'%title%': survey.title|striptags|trim})|e('js') }}');">{{ 'survey.list.duplicate'|trans }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,14 +81,12 @@
|
|||||||
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
<input type="hidden" name="_survey_action" value="duplicate-survey">
|
<input type="hidden" name="_survey_action" value="duplicate-survey">
|
||||||
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||||
<input type="hidden" name="_token" value="{{ csrf_token('duplicate-survey-' ~ survey.id) }}">
|
|
||||||
<button class="survey-button secondary" onclick="return confirm('{{ 'survey.list.duplicate_confirm'|trans({'%title%': survey.title|striptags|trim})|e('js') }}');">{{ 'survey.list.duplicate'|trans }}</button>
|
<button class="survey-button secondary" onclick="return confirm('{{ 'survey.list.duplicate_confirm'|trans({'%title%': survey.title|striptags|trim})|e('js') }}');">{{ 'survey.list.duplicate'|trans }}</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" class="survey-question-action-form">
|
<form method="post" class="survey-question-action-form">
|
||||||
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
<input type="hidden" name="_survey_action" value="delete-survey">
|
<input type="hidden" name="_survey_action" value="delete-survey">
|
||||||
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||||
<input type="hidden" name="_token" value="{{ csrf_token('delete-survey-' ~ survey.id) }}">
|
|
||||||
<button class="survey-button danger" onclick="return confirm('{{ 'survey.list.delete_confirm'|trans|e('js') }}') && confirm('{{ 'survey.list.delete_confirm_second'|trans|e('js') }}');">{{ 'survey.list.delete'|trans }}</button>
|
<button class="survey-button danger" onclick="return confirm('{{ 'survey.list.delete_confirm'|trans|e('js') }}') && confirm('{{ 'survey.list.delete_confirm_second'|trans|e('js') }}');">{{ 'survey.list.delete'|trans }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,7 +96,6 @@
|
|||||||
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
<input type="hidden" name="_survey_action" value="publish-survey">
|
<input type="hidden" name="_survey_action" value="publish-survey">
|
||||||
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||||
<input type="hidden" name="_token" value="{{ csrf_token('publish-survey-' ~ survey.id) }}">
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="survey-button primary survey-list-publish-button"
|
class="survey-button primary survey-list-publish-button"
|
||||||
@@ -117,7 +113,6 @@
|
|||||||
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
<input type="hidden" name="REQUEST_TOKEN" value="{{ contao.request_token }}">
|
||||||
<input type="hidden" name="_survey_action" value="toggle-active">
|
<input type="hidden" name="_survey_action" value="toggle-active">
|
||||||
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
<input type="hidden" name="item_id" value="{{ survey.id }}">
|
||||||
<input type="hidden" name="_token" value="{{ csrf_token('toggle-active-' ~ survey.id) }}">
|
|
||||||
<button type="submit" class="survey-visibility-dot{{ survey.isActive ? ' is-on' : '' }}" title="{{ survey.isActive ? 'survey.edit.deactivate_survey'|trans : 'survey.edit.activate_survey'|trans }}" aria-label="{{ survey.isActive ? 'survey.edit.deactivate_survey'|trans : 'survey.edit.activate_survey'|trans }}"></button>
|
<button type="submit" class="survey-visibility-dot{{ survey.isActive ? ' is-on' : '' }}" title="{{ survey.isActive ? 'survey.edit.deactivate_survey'|trans : 'survey.edit.activate_survey'|trans }}" aria-label="{{ survey.isActive ? 'survey.edit.deactivate_survey'|trans : 'survey.edit.activate_survey'|trans }}"></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
.tl_checkbox_wizard.survey-assigned-members-checkbox-wizard {
|
||||||
|
column-count: 4;
|
||||||
|
column-gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl_checkbox_wizard.survey-assigned-members-checkbox-wizard > span {
|
||||||
|
display: block;
|
||||||
|
break-inside: avoid;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl_checkbox_wizard.survey-assigned-members-checkbox-wizard .drag-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl_checkbox_wizard.survey-assigned-members-checkbox-wizard label {
|
||||||
|
display: inline;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.survey-question-list-page .parent_view > .tl_header .tl_content_right {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.survey-question-list-page.survey-question-list-clipboard-active .parent_view > .tl_header .tl_content_right {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
.tl_checkbox_wizard.survey-assigned-members-checkbox-wizard {
|
||||||
|
column-count: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.tl_checkbox_wizard.survey-assigned-members-checkbox-wizard {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.tl_checkbox_wizard.survey-assigned-members-checkbox-wizard {
|
||||||
|
column-count: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
(function () {
|
||||||
|
function isSurveyListPage() {
|
||||||
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
return params.get('do') === 'survey_bundle_surveys';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSurveyQuestionListPage() {
|
||||||
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
return params.get('do') === 'survey_bundle_surveys' && params.get('table') === 'tl_survey_content' && !params.get('act');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleQuestionListClass() {
|
||||||
|
document.body.classList.toggle('survey-question-list-page', isSurveyQuestionListPage());
|
||||||
|
document.body.classList.toggle('survey-question-list-clipboard-active', isSurveyQuestionListPage() && hasQuestionListClipboardState());
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasQuestionListClipboardState() {
|
||||||
|
if (!isSurveyQuestionListPage()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.querySelector('#paste_hint')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(document.querySelectorAll('button')).some(function (button) {
|
||||||
|
return (button.textContent || '').trim() === 'Ablage leeren';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchSelect() {
|
||||||
|
return document.querySelector('select[name="tl_search"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSearchForm(select) {
|
||||||
|
return select ? select.closest('form') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeIdOption(select) {
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idOption = select.querySelector('option[value="id"]');
|
||||||
|
|
||||||
|
if (!idOption) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var wasSelected = idOption.selected;
|
||||||
|
idOption.remove();
|
||||||
|
|
||||||
|
if (wasSelected) {
|
||||||
|
var fallback = select.querySelector('option[value="title"]') || select.options[0];
|
||||||
|
|
||||||
|
if (fallback) {
|
||||||
|
fallback.selected = true;
|
||||||
|
select.value = fallback.value;
|
||||||
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeIdChoicesUi() {
|
||||||
|
document.querySelectorAll('.choices__item[data-value="id"]').forEach(function (node) {
|
||||||
|
node.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.choices__item--choice[data-value="id"]').forEach(function (node) {
|
||||||
|
node.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSearchField() {
|
||||||
|
toggleQuestionListClass();
|
||||||
|
|
||||||
|
if (!isSurveyListPage()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var select = getSearchSelect();
|
||||||
|
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeIdOption(select);
|
||||||
|
removeIdChoicesUi();
|
||||||
|
|
||||||
|
var form = getSearchForm(select);
|
||||||
|
|
||||||
|
if (form && !form.dataset.surveySearchNormalized) {
|
||||||
|
form.dataset.surveySearchNormalized = '1';
|
||||||
|
form.addEventListener('submit', function () {
|
||||||
|
if (select.value === 'id') {
|
||||||
|
var fallback = select.querySelector('option[value="title"]') || select.options[0];
|
||||||
|
|
||||||
|
if (fallback) {
|
||||||
|
select.value = fallback.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', normalizeSearchField);
|
||||||
|
document.addEventListener('turbo:load', normalizeSearchField);
|
||||||
|
|
||||||
|
var observer = new MutationObserver(function () {
|
||||||
|
normalizeSearchField();
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
})();
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
(function () {
|
||||||
|
function ensureSortIconStyles() {
|
||||||
|
if (document.getElementById('survey-question-sort-icon-theme')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.id = 'survey-question-sort-icon-theme';
|
||||||
|
style.textContent = [
|
||||||
|
'.survey-sort-icon{width:1.14rem !important;height:1.52rem !important;color:#58a6da !important;background:none !important;transform:translateY(1px) !important;}',
|
||||||
|
'.survey-sort-icon::before,.survey-sort-icon::after{content:none !important;}',
|
||||||
|
'.survey-sort-icon svg{display:block !important;width:100% !important;height:100% !important;transform:translateY(4px);}',
|
||||||
|
'.survey-sort-icon svg path:first-child{transform:translateY(1.4px);transform-box:fill-box;transform-origin:center;}',
|
||||||
|
'.survey-sort-icon svg path:nth-child(2){transform:translateY(-1.4px);transform-box:fill-box;transform-origin:center;}',
|
||||||
|
'.survey-sort-icon svg path[stroke]{stroke-width:2px;}'
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSurveyQuestionSorting() {
|
||||||
|
ensureSortIconStyles();
|
||||||
|
|
||||||
|
if (typeof Sortable !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortableList = document.querySelector('[data-survey-question-sortable]');
|
||||||
|
var orderForm = document.querySelector('[data-survey-question-order-shell]');
|
||||||
|
var orderInput = document.querySelector('[data-survey-question-order-input]');
|
||||||
|
|
||||||
|
if (!sortableList || !orderForm || !orderInput || sortableList._surveyQuestionSortable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var getOrderedIds = function () {
|
||||||
|
return Array.from(sortableList.querySelectorAll('[data-survey-question-item]')).map(function (item) {
|
||||||
|
return item.getAttribute('data-question-id') || '';
|
||||||
|
}).filter(function (value) {
|
||||||
|
return value !== '';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var initialOrder = getOrderedIds().join(',');
|
||||||
|
|
||||||
|
var updateOrderField = function () {
|
||||||
|
orderInput.value = getOrderedIds().join(',');
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateQuestionIndices = function () {
|
||||||
|
sortableList.querySelectorAll('[data-survey-question-item]').forEach(function (item, index) {
|
||||||
|
var indexNode = item.querySelector('.survey-accordion-index');
|
||||||
|
|
||||||
|
if (indexNode) {
|
||||||
|
indexNode.textContent = (index + 1) + '.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateSaveVisibility = function () {
|
||||||
|
var currentOrder = getOrderedIds().join(',');
|
||||||
|
var isDirty = currentOrder !== initialOrder;
|
||||||
|
|
||||||
|
orderForm.hidden = !isDirty;
|
||||||
|
orderForm.classList.toggle('is-visible', isDirty);
|
||||||
|
updateOrderField();
|
||||||
|
};
|
||||||
|
|
||||||
|
sortableList.querySelectorAll('[data-survey-question-sort-handle]').forEach(function (handle) {
|
||||||
|
handle.addEventListener('click', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateQuestionIndices();
|
||||||
|
updateOrderField();
|
||||||
|
updateSaveVisibility();
|
||||||
|
|
||||||
|
sortableList._surveyQuestionSortable = Sortable.create(sortableList, {
|
||||||
|
animation: 180,
|
||||||
|
draggable: '[data-survey-question-item]',
|
||||||
|
handle: '[data-survey-question-sort-handle]',
|
||||||
|
ghostClass: 'sortable-ghost',
|
||||||
|
forceFallback: true,
|
||||||
|
fallbackTolerance: 3,
|
||||||
|
onStart: function (event) {
|
||||||
|
if (event.item) {
|
||||||
|
event.item.classList.add('is-reordering');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEnd: function (event) {
|
||||||
|
if (event.item) {
|
||||||
|
event.item.classList.remove('is-reordering');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQuestionIndices();
|
||||||
|
updateSaveVisibility();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initSurveyQuestionSorting, { once: true });
|
||||||
|
} else {
|
||||||
|
initSurveyQuestionSorting();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', initSurveyQuestionSorting, { once: true });
|
||||||
|
})();
|
||||||
@@ -188,6 +188,7 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
$template->set('questionForms', $questionFormViews);
|
$template->set('questionForms', $questionFormViews);
|
||||||
$template->set('newQuestionForm', $newQuestionFormView);
|
$template->set('newQuestionForm', $newQuestionFormView);
|
||||||
$template->set('questions', $questions);
|
$template->set('questions', $questions);
|
||||||
|
$template->set('canQuestionReorder', !$structureLocked && count($questions) > 1);
|
||||||
$conditionOverview = $this->surveyConditionRepository->findOverviewBySurvey((int) $survey->id);
|
$conditionOverview = $this->surveyConditionRepository->findOverviewBySurvey((int) $survey->id);
|
||||||
|
|
||||||
$template->set('conditions', $this->buildJumpRuleOverview($questions, $conditionOverview));
|
$template->set('conditions', $this->buildJumpRuleOverview($questions, $conditionOverview));
|
||||||
@@ -279,6 +280,11 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
$this->addFlash('success', 'Die Frage wurde gelöscht.');
|
$this->addFlash('success', 'Die Frage wurde gelöscht.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('reorder-questions' === $action) {
|
||||||
|
$this->surveyEditorService->reorderQuestions($survey, $this->parseQuestionOrder($request));
|
||||||
|
$this->addFlash('success', 'Die neue Reihenfolge der Fragen wurde gespeichert.');
|
||||||
|
}
|
||||||
|
|
||||||
if ('delete-condition' === $action) {
|
if ('delete-condition' === $action) {
|
||||||
$this->surveyEditorService->deleteCondition($survey, $itemId);
|
$this->surveyEditorService->deleteCondition($survey, $itemId);
|
||||||
$this->addFlash('success', 'Die Bedingung wurde gelöscht.');
|
$this->addFlash('success', 'Die Bedingung wurde gelöscht.');
|
||||||
@@ -290,6 +296,23 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
private function parseQuestionOrder(Request $request): array
|
||||||
|
{
|
||||||
|
$rawOrder = trim((string) $request->request->get('question_order', ''));
|
||||||
|
|
||||||
|
if ('' === $rawOrder) {
|
||||||
|
throw new \RuntimeException('Die neue Reihenfolge der Fragen fehlt.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter(
|
||||||
|
array_map('intval', explode(',', $rawOrder)),
|
||||||
|
static fn (int $questionId): bool => $questionId > 0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
private function resolveActiveQuestion(Request $request, int $surveyId): mixed
|
private function resolveActiveQuestion(Request $request, int $surveyId): mixed
|
||||||
{
|
{
|
||||||
$questionId = (int) $request->query->get('question', 0);
|
$questionId = (int) $request->query->get('question', 0);
|
||||||
@@ -317,13 +340,15 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|||||||
private function buildQuestionChoices(int $surveyId, int $excludeQuestionId = 0): array
|
private function buildQuestionChoices(int $surveyId, int $excludeQuestionId = 0): array
|
||||||
{
|
{
|
||||||
$choices = [];
|
$choices = [];
|
||||||
|
$position = 1;
|
||||||
|
|
||||||
foreach ($this->surveyQuestionRepository->findAllBySurvey($surveyId) as $question) {
|
foreach ($this->surveyQuestionRepository->findAllBySurvey($surveyId) as $question) {
|
||||||
if ($excludeQuestionId > 0 && (int) $question->id === $excludeQuestionId) {
|
if ($excludeQuestionId > 0 && (int) $question->id === $excludeQuestionId) {
|
||||||
|
++$position;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$choices[(int) $question->id] = sprintf('#%d %s', (int) $question->id, (string) $question->question);
|
$choices[(int) $question->id] = sprintf('%d. %s', $position++, (string) $question->question);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $choices;
|
return $choices;
|
||||||
|
|||||||
@@ -90,9 +90,8 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
|
|||||||
{
|
{
|
||||||
$surveyId = (int) $request->request->get('item_id', 0);
|
$surveyId = (int) $request->request->get('item_id', 0);
|
||||||
$action = (string) $request->request->get('_survey_action');
|
$action = (string) $request->request->get('_survey_action');
|
||||||
$token = (string) $request->request->get('_token');
|
|
||||||
|
|
||||||
if ($surveyId <= 0 || !$this->isCsrfTokenValid($action.'-'.$surveyId, $token)) {
|
if ($surveyId <= 0 || '' === $action) {
|
||||||
throw new \RuntimeException('Ungültiger CSRF-Token.');
|
throw new \RuntimeException('Ungültiger CSRF-Token.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,9 +196,10 @@ final class SurveyDcaListener
|
|||||||
private function buildQuestionOptions(int $surveyId): array
|
private function buildQuestionOptions(int $surveyId): array
|
||||||
{
|
{
|
||||||
$choices = [];
|
$choices = [];
|
||||||
|
$position = 1;
|
||||||
|
|
||||||
foreach ($this->surveyQuestionRepository->findAllBySurvey($surveyId) as $question) {
|
foreach ($this->surveyQuestionRepository->findAllBySurvey($surveyId) as $question) {
|
||||||
$choices[(int) $question->id] = sprintf('#%d %s', (int) $question->id, (string) $question->question);
|
$choices[(int) $question->id] = sprintf('%d. %s', $position++, (string) $question->question);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $choices;
|
return $choices;
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ final class SurveyEditorRepository
|
|||||||
|
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$name = trim(sprintf('%s %s', (string) $row['firstname'], (string) $row['lastname']));
|
$name = trim(sprintf('%s %s', (string) $row['firstname'], (string) $row['lastname']));
|
||||||
$choices[(int) $row['id']] = '' !== $name ? sprintf('%s <%s>', $name, (string) $row['email']) : (string) $row['email'];
|
$choices[(int) $row['id']] = '' !== $name ? $name : (string) $row['email'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $choices;
|
return $choices;
|
||||||
|
|||||||
@@ -165,6 +165,46 @@ final class SurveyQuestionRepository
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int> $orderedQuestionIds
|
||||||
|
*/
|
||||||
|
public function reorderQuestions(int $surveyId, array $orderedQuestionIds): void
|
||||||
|
{
|
||||||
|
$orderedQuestionIds = array_values(array_unique(array_filter(
|
||||||
|
array_map('intval', $orderedQuestionIds),
|
||||||
|
static fn (int $questionId): bool => $questionId > 0,
|
||||||
|
)));
|
||||||
|
$currentQuestionIds = array_map(static fn (SurveyContentModel $question): int => (int) $question->id, $this->findAllBySurvey($surveyId));
|
||||||
|
|
||||||
|
if ($orderedQuestionIds === $currentQuestionIds || count($orderedQuestionIds) < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortedCurrentIds = $currentQuestionIds;
|
||||||
|
$sortedOrderedIds = $orderedQuestionIds;
|
||||||
|
sort($sortedCurrentIds);
|
||||||
|
sort($sortedOrderedIds);
|
||||||
|
|
||||||
|
if ($sortedCurrentIds !== $sortedOrderedIds) {
|
||||||
|
throw new \RuntimeException('Die neue Reihenfolge der Fragen ist ungültig.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->transactional(function () use ($surveyId, $orderedQuestionIds): void {
|
||||||
|
$sorting = 128;
|
||||||
|
|
||||||
|
foreach ($orderedQuestionIds as $questionId) {
|
||||||
|
$this->connection->update('tl_survey_content', [
|
||||||
|
'sorting' => $sorting,
|
||||||
|
], [
|
||||||
|
'id' => $questionId,
|
||||||
|
'pid' => $surveyId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sorting += 128;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private function hydrate(array $ids): array
|
private function hydrate(array $ids): array
|
||||||
{
|
{
|
||||||
$models = [];
|
$models = [];
|
||||||
|
|||||||
@@ -53,6 +53,28 @@ final class SurveyRepository
|
|||||||
return $this->findById((int) $id);
|
return $this->findById((int) $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function titleExists(string $title, int $excludeId = 0): bool
|
||||||
|
{
|
||||||
|
$qb = $this->connection->createQueryBuilder()
|
||||||
|
->select('id')
|
||||||
|
->from('tl_survey')
|
||||||
|
->where('title = :title')
|
||||||
|
->setParameter('title', trim($title))
|
||||||
|
->setMaxResults(1)
|
||||||
|
;
|
||||||
|
|
||||||
|
if ($excludeId > 0) {
|
||||||
|
$qb
|
||||||
|
->andWhere('id != :excludeId')
|
||||||
|
->setParameter('excludeId', $excludeId)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $qb->executeQuery()->fetchOne();
|
||||||
|
|
||||||
|
return false !== $id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<array<string, mixed>>
|
* @return list<array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
@@ -95,6 +117,7 @@ final class SurveyRepository
|
|||||||
'alias' => $this->ensurePublicAlias(null),
|
'alias' => $this->ensurePublicAlias(null),
|
||||||
'category' => serialize($this->normalizeCategoryIds($data['category'] ?? [])),
|
'category' => serialize($this->normalizeCategoryIds($data['category'] ?? [])),
|
||||||
'description' => (string) ($data['description'] ?? ''),
|
'description' => (string) ($data['description'] ?? ''),
|
||||||
|
'internalNote' => trim((string) ($data['internalNote'] ?? '')),
|
||||||
'published' => '',
|
'published' => '',
|
||||||
'isActive' => '1',
|
'isActive' => '1',
|
||||||
'isLocked' => '',
|
'isLocked' => '',
|
||||||
@@ -121,6 +144,7 @@ final class SurveyRepository
|
|||||||
'alias' => $this->ensurePublicAlias((string) $survey->alias, (int) $survey->id),
|
'alias' => $this->ensurePublicAlias((string) $survey->alias, (int) $survey->id),
|
||||||
'category' => serialize($this->normalizeCategoryIds($data['category'] ?? StringUtil::deserialize($survey->category, true))),
|
'category' => serialize($this->normalizeCategoryIds($data['category'] ?? StringUtil::deserialize($survey->category, true))),
|
||||||
'description' => (string) ($data['description'] ?? $survey->description),
|
'description' => (string) ($data['description'] ?? $survey->description),
|
||||||
|
'internalNote' => trim((string) ($data['internalNote'] ?? $survey->internalNote)),
|
||||||
'published' => array_key_exists('published', $data) ? (!empty($data['published']) ? '1' : '') : (string) $survey->published,
|
'published' => array_key_exists('published', $data) ? (!empty($data['published']) ? '1' : '') : (string) $survey->published,
|
||||||
'isActive' => array_key_exists('isActive', $data) ? (!empty($data['isActive']) ? '1' : '') : (string) ($survey->isActive ?? ''),
|
'isActive' => array_key_exists('isActive', $data) ? (!empty($data['isActive']) ? '1' : '') : (string) ($survey->isActive ?? ''),
|
||||||
'updatedBy' => $memberId,
|
'updatedBy' => $memberId,
|
||||||
@@ -192,7 +216,7 @@ final class SurveyRepository
|
|||||||
'templateFilter' => '1' === (string) $survey->isTemplate ? '1' : '0',
|
'templateFilter' => '1' === (string) $survey->isTemplate ? '1' : '0',
|
||||||
'templateSummary' => '1' === (string) $survey->isTemplate ? 'Ja' : 'Nein',
|
'templateSummary' => '1' === (string) $survey->isTemplate ? 'Ja' : 'Nein',
|
||||||
'assignedMembersFilter' => implode(',', $memberIds),
|
'assignedMembersFilter' => implode(',', $memberIds),
|
||||||
'assignedMembersSummary' => implode(', ', $memberNames),
|
'assignedMembersSummary' => $this->buildAssignedMembersSummary($memberNames),
|
||||||
], [
|
], [
|
||||||
'id' => $surveyId,
|
'id' => $surveyId,
|
||||||
]);
|
]);
|
||||||
@@ -391,6 +415,26 @@ final class SurveyRepository
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $memberNames
|
||||||
|
*/
|
||||||
|
private function buildAssignedMembersSummary(array $memberNames): string
|
||||||
|
{
|
||||||
|
if ([] === $memberNames) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$visibleNames = array_slice($memberNames, 0, 3);
|
||||||
|
$remainingCount = count($memberNames) - count($visibleNames);
|
||||||
|
$summary = implode(', ', $visibleNames);
|
||||||
|
|
||||||
|
if ($remainingCount <= 0) {
|
||||||
|
return $summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('%s, ... und %d weitere', $summary, $remainingCount);
|
||||||
|
}
|
||||||
|
|
||||||
private function isValidPublicAlias(string $alias): bool
|
private function isValidPublicAlias(string $alias): bool
|
||||||
{
|
{
|
||||||
return 1 === preg_match('/^[a-z0-9]{20}$/', $alias);
|
return 1 === preg_match('/^[a-z0-9]{20}$/', $alias);
|
||||||
|
|||||||
@@ -97,6 +97,16 @@ final class SurveyEditorService
|
|||||||
$this->surveyRepository->touch((int) $survey->id);
|
$this->surveyRepository->touch((int) $survey->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int> $orderedQuestionIds
|
||||||
|
*/
|
||||||
|
public function reorderQuestions(SurveyModel $survey, array $orderedQuestionIds): void
|
||||||
|
{
|
||||||
|
$this->assertUnlocked($survey);
|
||||||
|
$this->surveyQuestionRepository->reorderQuestions((int) $survey->id, $orderedQuestionIds);
|
||||||
|
$this->surveyRepository->touch((int) $survey->id);
|
||||||
|
}
|
||||||
|
|
||||||
public function deleteSurvey(SurveyModel $survey): void
|
public function deleteSurvey(SurveyModel $survey): void
|
||||||
{
|
{
|
||||||
$this->surveyRepository->deleteCascade((int) $survey->id);
|
$this->surveyRepository->deleteCascade((int) $survey->id);
|
||||||
@@ -108,8 +118,9 @@ final class SurveyEditorService
|
|||||||
$sourceSurveyId = (int) $survey->id;
|
$sourceSurveyId = (int) $survey->id;
|
||||||
$memberIds = $this->surveyEditorRepository->findMemberIdsBySurvey($sourceSurveyId);
|
$memberIds = $this->surveyEditorRepository->findMemberIdsBySurvey($sourceSurveyId);
|
||||||
$duplicate = $this->surveyRepository->create($actorMemberId ?? (int) ($survey->createdBy ?? 0), [
|
$duplicate = $this->surveyRepository->create($actorMemberId ?? (int) ($survey->createdBy ?? 0), [
|
||||||
'title' => $this->buildDuplicateTitle((string) $survey->title),
|
'title' => $this->buildDuplicateTitle($survey),
|
||||||
'description' => (string) ($survey->description ?? ''),
|
'description' => (string) ($survey->description ?? ''),
|
||||||
|
'internalNote' => (string) ($survey->internalNote ?? ''),
|
||||||
'category' => $survey->category,
|
'category' => $survey->category,
|
||||||
'isTemplate' => false,
|
'isTemplate' => false,
|
||||||
]);
|
]);
|
||||||
@@ -244,14 +255,34 @@ final class SurveyEditorService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildDuplicateTitle(string $title): string
|
private function buildDuplicateTitle(SurveyModel $survey): string
|
||||||
{
|
{
|
||||||
$title = trim($title);
|
$title = trim((string) $survey->title);
|
||||||
|
|
||||||
if ('' === $title) {
|
if ('' === $title) {
|
||||||
$title = 'Neue Umfrage';
|
$title = 'Neue Umfrage';
|
||||||
}
|
}
|
||||||
|
|
||||||
return mb_substr($title.' - Kopie', 0, 255);
|
$baseTitle = trim((string) preg_replace('/ - Kopie(?: \d+)?$/u', '', $title));
|
||||||
|
|
||||||
|
if ('' === $baseTitle) {
|
||||||
|
$baseTitle = 'Neue Umfrage';
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($copyNumber = 1; ; ++$copyNumber) {
|
||||||
|
$suffix = 1 === $copyNumber ? ' - Kopie' : sprintf(' - Kopie %d', $copyNumber);
|
||||||
|
$candidate = $this->buildDuplicateTitleCandidate($baseTitle, $suffix);
|
||||||
|
|
||||||
|
if (!$this->surveyRepository->titleExists($candidate, (int) $survey->id)) {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildDuplicateTitleCandidate(string $baseTitle, string $suffix): string
|
||||||
|
{
|
||||||
|
$maxBaseLength = max(0, 255 - mb_strlen($suffix));
|
||||||
|
|
||||||
|
return mb_substr($baseTitle, 0, $maxBaseLength).$suffix;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +80,8 @@ survey:
|
|||||||
delete: "Löschen"
|
delete: "Löschen"
|
||||||
no_questions: "Noch keine Fragen angelegt."
|
no_questions: "Noch keine Fragen angelegt."
|
||||||
new_question: "Neue Frage anlegen"
|
new_question: "Neue Frage anlegen"
|
||||||
|
reorder_question: "Frage verschieben"
|
||||||
|
save_question_order: "Neue Reihenfolge speichern"
|
||||||
open_question: "Akkordeon öffnen"
|
open_question: "Akkordeon öffnen"
|
||||||
close_question: "Akkordeon schließen"
|
close_question: "Akkordeon schließen"
|
||||||
conditions_title: "Sprungregeln"
|
conditions_title: "Sprungregeln"
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ survey:
|
|||||||
delete: "Delete"
|
delete: "Delete"
|
||||||
no_questions: "No questions created yet."
|
no_questions: "No questions created yet."
|
||||||
new_question: "Create new question"
|
new_question: "Create new question"
|
||||||
|
reorder_question: "Reorder question"
|
||||||
|
save_question_order: "Save new order"
|
||||||
open_question: "Open accordion"
|
open_question: "Open accordion"
|
||||||
close_question: "Close accordion"
|
close_question: "Close accordion"
|
||||||
conditions_title: "Jump rules"
|
conditions_title: "Jump rules"
|
||||||
|
|||||||
Reference in New Issue
Block a user