Browse code

Add new GridPosition widget

Benjamin Roth authored on28/02/2025 10:55:44
Showing10 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,15 @@
1
+<?php
2
+
3
+declare(strict_types=1);
4
+
5
+/*
6
+ * This file is part of vonRotenberg Coretools Bundle.
7
+ *
8
+ * (c) vonRotenberg
9
+ *
10
+ * @license proprietary
11
+ */
12
+
13
+use vonRotenberg\CoretoolsBundle\Widget\GridPosition;
14
+
15
+$GLOBALS['BE_FFL']['gridPosition'] = GridPosition::class;
0 16
new file mode 100644
... ...
@@ -0,0 +1,3 @@
1
+<div class="grid-container" data-rows="<?= $this->intRows ?>" data-cols="<?= $this->intCols ?>" data-field-id="<?= $this->id ?>">
2
+</div>
3
+<input type="hidden" name="<?= $this->id ?>" id="<?= $this->id ?>" value="<?= $this->value ?>">
0 4
\ No newline at end of file
1 5
new file mode 100644
... ...
@@ -0,0 +1,61 @@
1
+:root {
2
+  --gp-primary: #a80556;
3
+  --gp-primary-rgb: 168, 5, 86;
4
+  --contao: var(--gp-primary);
5
+  --contao-rgb: var(--gp-primary-rgb);
6
+  --header-bg: var(--gp-primary);
7
+}
8
+
9
+.grid-container {
10
+  display: grid;
11
+  grid-template-columns: repeat(6, 1fr);
12
+  grid-gap: 5px;
13
+}
14
+.grid-container[data-cols="1"] {
15
+  grid-template-columns: repeat(1, 1fr);
16
+}
17
+.grid-container[data-cols="2"] {
18
+  grid-template-columns: repeat(2, 1fr);
19
+}
20
+.grid-container[data-cols="3"] {
21
+  grid-template-columns: repeat(3, 1fr);
22
+}
23
+.grid-container[data-cols="4"] {
24
+  grid-template-columns: repeat(4, 1fr);
25
+}
26
+.grid-container[data-cols="5"] {
27
+  grid-template-columns: repeat(5, 1fr);
28
+}
29
+.grid-container[data-cols="6"] {
30
+  grid-template-columns: repeat(6, 1fr);
31
+}
32
+.grid-container[data-cols="7"] {
33
+  grid-template-columns: repeat(7, 1fr);
34
+}
35
+.grid-container[data-cols="8"] {
36
+  grid-template-columns: repeat(8, 1fr);
37
+}
38
+.grid-container[data-cols="9"] {
39
+  grid-template-columns: repeat(9, 1fr);
40
+}
41
+.grid-container[data-cols="10"] {
42
+  grid-template-columns: repeat(10, 1fr);
43
+}
44
+.grid-container[data-cols="11"] {
45
+  grid-template-columns: repeat(11, 1fr);
46
+}
47
+.grid-container[data-cols="12"] {
48
+  grid-template-columns: repeat(12, 1fr);
49
+}
50
+
51
+.grid-cell {
52
+  height: 30px;
53
+  border: 1px solid #ccc;
54
+  cursor: pointer;
55
+}
56
+.grid-cell:hover {
57
+  background-color: rgba(var(--contao-rgb), 0.3);
58
+}
59
+.grid-cell.selected {
60
+  background-color: var(--contao);
61
+}
0 62
new file mode 100644
... ...
@@ -0,0 +1 @@
1
+:root{--gp-primary:#a80556;--gp-primary-rgb:168, 5, 86;--contao:var(--gp-primary);--contao-rgb:var(--gp-primary-rgb);--header-bg:var(--gp-primary)}.grid-container{display:grid;grid-template-columns:repeat(6,1fr);grid-gap:5px}.grid-container[data-cols="1"]{grid-template-columns:repeat(1,1fr)}.grid-container[data-cols="2"]{grid-template-columns:repeat(2,1fr)}.grid-container[data-cols="3"]{grid-template-columns:repeat(3,1fr)}.grid-container[data-cols="4"]{grid-template-columns:repeat(4,1fr)}.grid-container[data-cols="5"]{grid-template-columns:repeat(5,1fr)}.grid-container[data-cols="6"]{grid-template-columns:repeat(6,1fr)}.grid-container[data-cols="7"]{grid-template-columns:repeat(7,1fr)}.grid-container[data-cols="8"]{grid-template-columns:repeat(8,1fr)}.grid-container[data-cols="9"]{grid-template-columns:repeat(9,1fr)}.grid-container[data-cols="10"]{grid-template-columns:repeat(10,1fr)}.grid-container[data-cols="11"]{grid-template-columns:repeat(11,1fr)}.grid-container[data-cols="12"]{grid-template-columns:repeat(12,1fr)}.grid-cell{height:30px;border:1px solid #ccc;cursor:pointer}.grid-cell:hover{background-color:rgba(var(--contao-rgb),.3)}.grid-cell.selected{background-color:var(--contao)}
0 2
\ No newline at end of file
1 3
new file mode 100644
... ...
@@ -0,0 +1,33 @@
1
+:root {
2
+  --gp-primary: #a80556;
3
+  --gp-primary-rgb: 168, 5, 86;
4
+  --contao: var(--gp-primary);
5
+  --contao-rgb: var(--gp-primary-rgb);
6
+  --header-bg: var(--gp-primary);
7
+}
8
+
9
+.grid-container {
10
+  display: grid;
11
+  grid-template-columns: repeat(6, 1fr);
12
+  grid-gap: 5px;
13
+
14
+  @for $i from 1 through 12 {
15
+    &[data-cols="#{$i}"] {
16
+      grid-template-columns: repeat($i, 1fr);
17
+    }
18
+  }
19
+}
20
+
21
+.grid-cell {
22
+  height: 30px;
23
+  border: 1px solid #ccc;
24
+  cursor: pointer;
25
+
26
+  &:hover {
27
+    background-color: rgba(var(--contao-rgb),.3);
28
+  }
29
+
30
+  &.selected {
31
+    background-color: var(--contao);
32
+  }
33
+}
0 34
new file mode 100644
... ...
@@ -0,0 +1,141 @@
1
+document.addEventListener('DOMContentLoaded', () => {
2
+  const gridContainer = document.querySelector('.grid-container');
3
+
4
+  if (!gridContainer) return;
5
+
6
+  // Werte aus den data-Attributen holen oder Standardwerte setzen
7
+  const gridRows = parseInt(gridContainer.dataset.rows) || 1; // Standardwert für Reihen: 5
8
+  const gridCols = parseInt(gridContainer.dataset.cols) || 6; // Standardwert für Spalten: 5
9
+  let selectedIndices = [];
10
+
11
+  // Grid erstellen
12
+  const fragment = document.createDocumentFragment();
13
+  // Erstelle die Zellen basierend auf Reihen und Spalten (gridRows * gridCols)
14
+  for (let i = 0; i < gridRows * gridCols; i++) {
15
+    const gridItem = document.createElement('div');
16
+    gridItem.classList.add('grid-cell');
17
+    gridItem.dataset.index = i; // Weise jeder Zelle ihren Index zu
18
+    gridItem.addEventListener('click', () => toggleCell(i)); // Event für Klick hinzufügen
19
+    fragment.appendChild(gridItem);
20
+  }
21
+  gridContainer.appendChild(fragment);
22
+
23
+  /**
24
+   * Zelle ein-/auswählen
25
+   * @param {number} index - Der Index der Zelle
26
+   */
27
+  function toggleCell(index) {
28
+    if (selectedIndices.includes(index)) {
29
+      // Zelle abwählen
30
+      removeRowOrColumn(index);
31
+    } else {
32
+      // Zelle hinzufügen
33
+      selectedIndices.push(index);
34
+      fillSelection(); // Rechteck auffüllen
35
+    }
36
+
37
+    updateGrid(); // Visuelle Aktualisierung
38
+    saveSelection(); // Speicherung der Auswahl
39
+  }
40
+
41
+  /**
42
+   * Entfernt eine Zeile oder eine Spalte basierend auf dem Index
43
+   * @param {number} index - Der Index der Zelle
44
+   */
45
+  function removeRowOrColumn(index) {
46
+    const row = Math.floor(index / gridCols); // Berechnet die aktuelle Reihe
47
+    const col = index % gridCols; // Berechnet die aktuelle Spalte
48
+
49
+    if (col === 0) {
50
+      // Entferne die gesamte Reihe, wenn das Feld in der ersten Spalte ist
51
+      // UND entferne alle darunterliegenden Reihen
52
+      selectedIndices = selectedIndices.filter(cell => {
53
+        const cellRow = Math.floor(cell / gridCols);
54
+        return cellRow < row; // Entferne alle Reihen >= der aktuellen
55
+      });
56
+    } else {
57
+      // Entferne die gesamte Spalte, wenn das Feld NICHT in der ersten Spalte ist
58
+      selectedIndices = selectedIndices.filter(cell => {
59
+        const cellCol = cell % gridCols;
60
+        return cellCol !== col;
61
+      });
62
+
63
+      // Prüfe, ob Spalten rechts entfernt werden müssen, um Lücken zu vermeiden
64
+      adjustRectangleAfterColumnRemoval();
65
+    }
66
+  }
67
+
68
+  /**
69
+   * Passt das Rechteck an, indem leere Spalten rechts entfernt werden
70
+   */
71
+  function adjustRectangleAfterColumnRemoval() {
72
+    if (selectedIndices.length === 0) return;
73
+
74
+    // Ermittle die minimalen und maximalen Spalten des bestehenden Rechtecks
75
+    const firstCol = Math.min(...selectedIndices.map(cell => cell % gridCols));
76
+    const lastCol = Math.max(...selectedIndices.map(cell => cell % gridCols));
77
+
78
+    // Prüfe jede Spalte von links nach rechts
79
+    for (let col = firstCol; col <= lastCol; col++) {
80
+      // Überprüfen, ob diese Spalte komplett leer ist
81
+      const isColumnEmpty = !selectedIndices.some(cell => cell % gridCols === col);
82
+
83
+      if (isColumnEmpty) {
84
+        // Entferne alle Zellen in Spalten rechts von der leeren Spalte
85
+        selectedIndices = selectedIndices.filter(cell => cell % gridCols < col);
86
+        break; // Stoppe, wenn wir eine leere Spalte finden
87
+      }
88
+    }
89
+  }
90
+
91
+  /**
92
+   * Füllt die Auswahl als Rechteck aus
93
+   */
94
+  function fillSelection() {
95
+    if (selectedIndices.length === 0) return;
96
+
97
+    // Ermittle die minimalen und maximalen Indizes
98
+    const firstRow = Math.min(...selectedIndices.map(cell => Math.floor(cell / gridCols)));
99
+    const lastRow = Math.max(...selectedIndices.map(cell => Math.floor(cell / gridCols)));
100
+    const firstCol = Math.min(...selectedIndices.map(cell => cell % gridCols));
101
+    const lastCol = Math.max(...selectedIndices.map(cell => cell % gridCols));
102
+
103
+    // Rechteck auffüllen: Alle Zellen zwischen den Ecken hinzufügen
104
+    const newSelection = [];
105
+    for (let row = firstRow; row <= lastRow; row++) {
106
+      for (let col = firstCol; col <= lastCol; col++) {
107
+        const cellIndex = row * gridCols + col;
108
+        newSelection.push(cellIndex);
109
+      }
110
+    }
111
+
112
+    selectedIndices = newSelection; // Aktualisiere die Auswahl
113
+  }
114
+
115
+  /**
116
+   * Aktualisiert die visuelle Darstellung des Grids
117
+   */
118
+  function updateGrid() {
119
+    // Entferne die "selected"-Klasse von allen Zellen
120
+    gridContainer.querySelectorAll('.grid-cell').forEach(cell => {
121
+      cell.classList.remove('selected');
122
+    });
123
+
124
+    // Füge die "selected"-Klasse zu den ausgewählten Zellen hinzu
125
+    selectedIndices.forEach(index => {
126
+      gridContainer
127
+        .querySelector(`.grid-cell[data-index="${index}"]`)
128
+        ?.classList.add('selected');
129
+    });
130
+  }
131
+
132
+  /**
133
+   * Speichert die Auswahl in einem versteckten Eingabeelement
134
+   */
135
+  function saveSelection() {
136
+    const gridDataInput = document.getElementById('grid-data');
137
+    if (gridDataInput) {
138
+      gridDataInput.value = JSON.stringify(selectedIndices);
139
+    }
140
+  }
141
+});
0 142
new file mode 100644
... ...
@@ -0,0 +1 @@
1
+document.addEventListener("DOMContentLoaded",()=>{const gridContainer=document.querySelector(".grid-container");if(!gridContainer)return;const gridRows=parseInt(gridContainer.dataset.rows)||1;const gridCols=parseInt(gridContainer.dataset.cols)||6;let selectedIndices=[];const fragment=document.createDocumentFragment();for(let i=0;i<gridRows*gridCols;i++){const gridItem=document.createElement("div");gridItem.classList.add("grid-cell");gridItem.dataset.index=i;gridItem.addEventListener("click",()=>toggleCell(i));fragment.appendChild(gridItem)}gridContainer.appendChild(fragment);function toggleCell(index){if(selectedIndices.includes(index)){removeRowOrColumn(index)}else{selectedIndices.push(index);fillSelection()}updateGrid();saveSelection()}function removeRowOrColumn(index){const row=Math.floor(index/gridCols);const col=index%gridCols;if(col===0){selectedIndices=selectedIndices.filter(cell=>{const cellRow=Math.floor(cell/gridCols);return cellRow<row})}else{selectedIndices=selectedIndices.filter(cell=>{const cellCol=cell%gridCols;return cellCol!==col});adjustRectangleAfterColumnRemoval()}}function adjustRectangleAfterColumnRemoval(){if(selectedIndices.length===0)return;const firstCol=Math.min(...selectedIndices.map(cell=>cell%gridCols));const lastCol=Math.max(...selectedIndices.map(cell=>cell%gridCols));for(let col=firstCol;col<=lastCol;col++){const isColumnEmpty=!selectedIndices.some(cell=>cell%gridCols===col);if(isColumnEmpty){selectedIndices=selectedIndices.filter(cell=>cell%gridCols<col);break}}}function fillSelection(){if(selectedIndices.length===0)return;const firstRow=Math.min(...selectedIndices.map(cell=>Math.floor(cell/gridCols)));const lastRow=Math.max(...selectedIndices.map(cell=>Math.floor(cell/gridCols)));const firstCol=Math.min(...selectedIndices.map(cell=>cell%gridCols));const lastCol=Math.max(...selectedIndices.map(cell=>cell%gridCols));const newSelection=[];for(let row=firstRow;row<=lastRow;row++){for(let col=firstCol;col<=lastCol;col++){const cellIndex=row*gridCols+col;newSelection.push(cellIndex)}}selectedIndices=newSelection}function updateGrid(){gridContainer.querySelectorAll(".grid-cell").forEach(cell=>{cell.classList.remove("selected")});selectedIndices.forEach(index=>{gridContainer.querySelector(`.grid-cell[data-index="${index}"]`)?.classList.add("selected")})}function saveSelection(){const gridDataInput=document.getElementById("grid-data");if(gridDataInput){gridDataInput.value=JSON.stringify(selectedIndices)}}});
0 2
\ No newline at end of file
1 3
new file mode 100644
... ...
@@ -0,0 +1,101 @@
1
+document.addEventListener('DOMContentLoaded', () => {
2
+  const gridContainer = document.querySelector('.grid-container');
3
+  const gridSize = 5;
4
+  let selectedIndices = [];
5
+
6
+  if (!gridContainer) return;
7
+
8
+  // Grid dynamisch erstellen
9
+  const fragment = document.createDocumentFragment();
10
+  for (let i = 0; i < gridSize * gridSize; i++) {
11
+    const gridItem = document.createElement('div');
12
+    gridItem.classList.add('grid-cell');
13
+    gridItem.dataset.index = i;
14
+    gridItem.addEventListener('click', () => toggleCell(i));
15
+    fragment.appendChild(gridItem);
16
+  }
17
+  gridContainer.appendChild(fragment);
18
+
19
+  function toggleCell(index) {
20
+    const gridItem = gridContainer.querySelector(`.grid-cell[data-index="${index}"]`);
21
+    if (!gridItem) return;
22
+
23
+    if (selectedIndices.includes(index)) {
24
+      // Entfernen, falls bereits ausgewählt
25
+      selectedIndices = selectedIndices.filter(cell => cell !== index);
26
+      gridItem.classList.remove('selected');
27
+    } else if (isSelectable(index)) {
28
+      // Wählen, wenn erste oder angrenzende Zelle
29
+      selectedIndices.push(index);
30
+      gridItem.classList.add('selected');
31
+    } else {
32
+      // Invalid, Auswahl zurücksetzen
33
+      resetSelection(index);
34
+    }
35
+    validateSelection();
36
+    saveSelection();
37
+  }
38
+
39
+  function isSelectable(index) {
40
+    return selectedIndices.length === 0 || selectedIndices.some(selected => isAdjacent(index, selected));
41
+  }
42
+
43
+  function resetSelection(index) {
44
+    selectedIndices = [index];
45
+    resetGrid();
46
+    gridContainer.querySelector(`.grid-cell[data-index="${index}"]`)?.classList.add('selected');
47
+  }
48
+
49
+  function isAdjacent(index1, index2) {
50
+    const rowDiff = Math.abs(Math.floor(index1 / gridSize) - Math.floor(index2 / gridSize));
51
+    const colDiff = Math.abs(index1 % gridSize - index2 % gridSize);
52
+    return (rowDiff === 1 && colDiff === 0) || (rowDiff === 0 && colDiff === 1);
53
+  }
54
+
55
+  function validateSelection() {
56
+    if (selectedIndices.length <= 1) return;
57
+
58
+    const validSelection = [selectedIndices[0]];
59
+    const remainingCells = selectedIndices.slice(1);
60
+
61
+    while (remainingCells.length > 0) {
62
+      let foundAdjacent = false;
63
+
64
+      for (let i = 0; i < remainingCells.length; i++) {
65
+        if (validSelection.some(selected => isAdjacent(remainingCells[i], selected))) {
66
+          validSelection.push(remainingCells[i]);
67
+          remainingCells.splice(i, 1);
68
+          foundAdjacent = true;
69
+          break;
70
+        }
71
+      }
72
+
73
+      if (!foundAdjacent) {
74
+        selectedIndices = validSelection;
75
+        resetGrid();
76
+        highlightSelection();
77
+        return;
78
+      }
79
+    }
80
+
81
+    selectedIndices = validSelection;
82
+    highlightSelection();
83
+  }
84
+
85
+  function resetGrid() {
86
+    gridContainer.querySelectorAll('.grid-cell').forEach(cell => cell.classList.remove('selected'));
87
+  }
88
+
89
+  function highlightSelection() {
90
+    selectedIndices.forEach(index => {
91
+      gridContainer.querySelector(`.grid-cell[data-index="${index}"]`)?.classList.add('selected');
92
+    });
93
+  }
94
+
95
+  function saveSelection() {
96
+    const gridDataInput = document.getElementById(gridContainer.dataset.fieldId);
97
+    if (gridDataInput) {
98
+      gridDataInput.value = JSON.stringify(selectedIndices);
99
+    }
100
+  }
101
+});
0 102
new file mode 100644
... ...
@@ -0,0 +1,35 @@
1
+<?php
2
+
3
+declare(strict_types=1);
4
+
5
+namespace vonRotenberg\CoretoolsBundle\EventListener;
6
+
7
+use Contao\CoreBundle\Routing\ScopeMatcher;
8
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
9
+use Symfony\Component\HttpKernel\Event\RequestEvent;
10
+use Symfony\Component\HttpKernel\KernelEvents;
11
+
12
+class KernelRequestSubscriber implements EventSubscriberInterface
13
+{
14
+    protected $scopeMatcher;
15
+
16
+    public function __construct(ScopeMatcher $scopeMatcher)
17
+    {
18
+        $this->scopeMatcher = $scopeMatcher;
19
+    }
20
+
21
+    public static function getSubscribedEvents()
22
+    {
23
+        return [KernelEvents::REQUEST => 'onKernelRequest'];
24
+    }
25
+
26
+    public function onKernelRequest(RequestEvent $e): void
27
+    {
28
+        $request = $e->getRequest();
29
+
30
+        if ($this->scopeMatcher->isBackendRequest($request)) {
31
+            $GLOBALS['TL_CSS'][] = 'bundles/vonrotenbergcoretools/css/grid_position_widget.min.css|static';
32
+            $GLOBALS['TL_JAVASCRIPT'][] = 'bundles/vonrotenbergcoretools/js/GridSelectionHandler.js|async';
33
+        }
34
+    }
35
+}
0 36
new file mode 100644
... ...
@@ -0,0 +1,51 @@
1
+<?php
2
+
3
+declare(strict_types=1);
4
+
5
+namespace vonRotenberg\CoretoolsBundle\Widget;
6
+
7
+use Contao\Widget;
8
+
9
+class GridPosition extends Widget
10
+{
11
+    /**
12
+     * @inheritdoc
13
+     */
14
+    protected $strTemplate = 'be_grid_position';
15
+
16
+    protected $parent;
17
+
18
+    protected $intCols = 6;
19
+
20
+    protected $intRows = 1;
21
+
22
+    public function __construct($arrAttributes = null)
23
+    {
24
+        parent::__construct($arrAttributes);
25
+
26
+        $this->parent = $this->activeRecord;
27
+    }
28
+
29
+    public function __set($strKey, $varValue)
30
+    {
31
+        switch ($strKey)
32
+        {
33
+            case 'rows':
34
+                $this->intRows = $varValue;
35
+                break;
36
+
37
+            case 'cols':
38
+                $this->intCols = $varValue;
39
+                break;
40
+            default:
41
+                parent::__set($strKey, $varValue);
42
+                break;
43
+        }
44
+    }
45
+
46
+
47
+    public function generate()
48
+    {
49
+        return $this->parse();
50
+    }
51
+}