Initial xentral_oss_20.3.c9ffacf
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\Chart;
|
||||
|
||||
/**
|
||||
* Spezielles Dataset für Balkendiagramme
|
||||
*
|
||||
* Besonderheiten:
|
||||
* * Balkenhintergrund und -rahmen haben keine Transparenz
|
||||
*/
|
||||
class BarDataset extends Dataset
|
||||
{
|
||||
/** @var array $defaultAlphaValues Transparenz-Werte */
|
||||
protected static $defaultAlphaValues = [
|
||||
'borderColor' => 1.0,
|
||||
'backgroundColor' => 1.0,
|
||||
'hoverBorderColor' => 1.0,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\Chart;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
class Chart
|
||||
{
|
||||
/** @var array $validTypes */
|
||||
private static $validTypes = [
|
||||
'line',
|
||||
'bar',
|
||||
'radar',
|
||||
'pie',
|
||||
'doughnut',
|
||||
'polarArea',
|
||||
//'bubble',
|
||||
//'scatter',
|
||||
];
|
||||
|
||||
/** @var array $defaultColors In RGB */
|
||||
private static $defaultColors = [
|
||||
[162, 197, 90],
|
||||
[69, 185, 211],
|
||||
[246, 158, 6],
|
||||
[14, 131, 148],
|
||||
];
|
||||
|
||||
/** @var array $defaultOptions */
|
||||
private static $defaultOptions = [
|
||||
'responsive' => true,
|
||||
'responsiveAnimationDuration' => 0,
|
||||
'maintainAspectRatio' => true,
|
||||
'tooltips' => [
|
||||
'enabled' => true,
|
||||
'mode' => 'nearest',
|
||||
'backgroundColor' => 'rgba(0, 0, 0, 0.5)',
|
||||
],
|
||||
'legend' => [
|
||||
'display' => true,
|
||||
],
|
||||
'animation' => [
|
||||
'duration' => 1000,
|
||||
'animateRotate' => true, // Pie + Doughnut
|
||||
],
|
||||
'scales' => [
|
||||
'xAxes' => [
|
||||
[
|
||||
'display' => true,
|
||||
],
|
||||
],
|
||||
'yAxes' => [
|
||||
[
|
||||
'display' => true,
|
||||
'ticks' => [
|
||||
'beginAtZero' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
protected $type;
|
||||
protected $labels;
|
||||
protected $datasets;
|
||||
protected $options;
|
||||
|
||||
/** @var int $currentColor Zähler für die letzte Default-Farbe die verwendet wurde */
|
||||
private $currentColor;
|
||||
|
||||
/**
|
||||
* @param string $type Chart-Typ
|
||||
* @param array $labels
|
||||
* @param array|Dataset[] $datasets
|
||||
* @param array $options chart.js Optionen
|
||||
*/
|
||||
public function __construct($type = 'line', $labels = [], array $datasets = [], array $options = [])
|
||||
{
|
||||
if (!in_array($type, self::$validTypes, true)) {
|
||||
throw new InvalidArgumentException(sprintf('Chart type "%s" is not valid.', $type));
|
||||
}
|
||||
// Ungültige Werte in leeres Array wandeln
|
||||
if (!is_array($labels)) {
|
||||
$labels = [];
|
||||
}
|
||||
|
||||
$this->type = $type;
|
||||
$this->labels = $labels;
|
||||
$this->datasets = $datasets;
|
||||
$this->options = array_replace_recursive(self::$defaultOptions, $options);
|
||||
|
||||
if ($type === 'line') {
|
||||
$this->options['tooltips']['mode'] = 'index';
|
||||
}
|
||||
if ($type === 'doughnut' || $type === 'pie') {
|
||||
$this->options['scales']['xAxes'][0]['display'] = false;
|
||||
$this->options['scales']['yAxes'][0]['display'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $labels
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addLabels($labels)
|
||||
{
|
||||
// Ungültige Werte in leeres Array wandeln
|
||||
if (!is_array($labels)) {
|
||||
$labels = [];
|
||||
}
|
||||
|
||||
foreach ($labels as $label) {
|
||||
$this->addLabel($label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $label
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addLabel($label)
|
||||
{
|
||||
$this->labels[] = (string)$label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Dataset $dataset
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addDataset(Dataset $dataset)
|
||||
{
|
||||
$this->datasets[] = $dataset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie setYAxis(), nur dass das Dataset zusätzlich noch zum Chart hinzugefügt wird
|
||||
*
|
||||
* @see setYAxis()
|
||||
*
|
||||
* @param Dataset $dataset
|
||||
* @param string $position
|
||||
* @param string $type
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addDatasetAsYAxis(Dataset $dataset, $position = 'left', $type = 'linear')
|
||||
{
|
||||
$this->setYAxis($dataset, $position, $type);
|
||||
$this->addDataset($dataset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Übergebenes Dataset für die Anzeige der Y-Achse verwenden
|
||||
*
|
||||
* * Mehrere Y-Achsen sind möglich
|
||||
* * Dataset wird aber nicht zum Chart hinzugefügt; muss über addDataset() passieren
|
||||
*
|
||||
* @param Dataset $dataset
|
||||
* @param string $position
|
||||
* @param string $type
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setYAxis(Dataset $dataset, $position = 'left', $type = 'linear')
|
||||
{
|
||||
// Falls vorher noch kein Dataset als y-Achse definiert wurde, Default-y-Achsen-Config löschen.
|
||||
// (Ansonsten wird eine y-Achse zu viel angezeigt)
|
||||
$hasMultipleAxesConfig = array_key_exists('id', $this->options['scales']['yAxes'][0]);
|
||||
if (!$hasMultipleAxesConfig) {
|
||||
unset($this->options['scales']['yAxes']);
|
||||
}
|
||||
|
||||
$id = $dataset->generateYAxisId();
|
||||
$this->options['scales']['yAxes'][] = [
|
||||
'id' => $id,
|
||||
'display' => true,
|
||||
'type' => $type,
|
||||
'position' => $position,
|
||||
'ticks' => [
|
||||
'beginAtZero' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function accumulateData()
|
||||
{
|
||||
foreach ($this->datasets as $dataset) {
|
||||
$dataset->accumulateData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
foreach ($this->datasets as $dataset) {
|
||||
/** @var Dataset $dataset */
|
||||
// Default-Farben durchiterieren, wenn noch keine Farbe gesetzt
|
||||
if (!$dataset->hasColorAssigned()) {
|
||||
$color = $this->getNextDefaultColor();
|
||||
$dataset->setColorByRgb(...$color);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'labels' => $this->labels,
|
||||
'datasets' => $this->datasets,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getOptions()
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function toJson()
|
||||
{
|
||||
return json_encode([
|
||||
'type' => $this->getType(),
|
||||
'data' => $this->getData(),
|
||||
'options' => $this->getOptions(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert bei jedem Aufruf die nächste Farbe aus den Standard-Farben
|
||||
*
|
||||
* @return array Array mit drei Farbwerten [R, G, B]
|
||||
*/
|
||||
private function getNextDefaultColor()
|
||||
{
|
||||
// Bei jedem Aufruf Index hochzählen
|
||||
$this->currentColor = $this->currentColor === null ? 0 : $this->currentColor + 1;
|
||||
|
||||
// Wieder vorne anfangen wenn letzte Farbe ausgeliefert wurde
|
||||
if ($this->currentColor >= count(self::$defaultColors)) {
|
||||
$this->currentColor = 0;
|
||||
}
|
||||
|
||||
return self::$defaultColors[$this->currentColor];
|
||||
}
|
||||
|
||||
/**
|
||||
* Beim Klonen die Dataset-Objekte einzeln klonen
|
||||
*
|
||||
* Ansonsten beinhaltet das geklonte Chart-Objekt die Referenzen zum Ursprungsobjekt!
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->datasets as $key => $dataset) {
|
||||
$this->datasets[$key] = clone $dataset;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\Chart;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use JsonSerializable;
|
||||
|
||||
class Color implements JsonSerializable
|
||||
{
|
||||
private $red;
|
||||
private $green;
|
||||
private $blue;
|
||||
private $alpha;
|
||||
|
||||
/**
|
||||
* @param int $red Farbwert von 0 bis 255
|
||||
* @param int $green Farbwert von 0 bis 255
|
||||
* @param int $blue Farbwert von 0 bis 255
|
||||
* @param float $alpha Transparenzwert von 0 bis 1
|
||||
*/
|
||||
public function __construct($red = 0, $green = 0, $blue = 0, $alpha = 0.1)
|
||||
{
|
||||
$this->red = $this->ensureColorValue($red);
|
||||
$this->green = $this->ensureColorValue($green);
|
||||
$this->blue = $this->ensureColorValue($blue);
|
||||
$this->setAlpha($alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hexColor Beispiel: "#112233"
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function createFromHex($hexColor)
|
||||
{
|
||||
$hexColor = str_replace('#', '', $hexColor);
|
||||
if (strlen($hexColor) !== 6) {
|
||||
throw new InvalidArgumentException('Only full length hex values are supported.');
|
||||
}
|
||||
|
||||
$parts = str_split($hexColor, 2);
|
||||
$red = hexdec($parts[0]);
|
||||
$green = hexdec($parts[1]);
|
||||
$blue = hexdec($parts[2]);
|
||||
|
||||
return new self($red, $green, $blue, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $cssRgba Beispiel: rgba(255, 128, 0, 0.5)
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function createFromCssRgba($cssRgba)
|
||||
{
|
||||
$cssRgba = str_replace(' ', '', $cssRgba);
|
||||
preg_match('/^rgba\((\d+),(\d+),(\d+),([\d\.]+)\);?/i', $cssRgba, $colors);
|
||||
|
||||
return new self((int)$colors[1], (int)$colors[2], (int)$colors[3], (float)$colors[4]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $cssRgb Beispiel: "rgb(255, 128, 0)"
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function createFromCssRgb($cssRgb)
|
||||
{
|
||||
$cssRgb = str_replace(' ', '', $cssRgb);
|
||||
preg_match('/^rgb\((\d+),(\d+),(\d+)\);?/i', $cssRgb, $colors);
|
||||
|
||||
return new self((int)$colors[1], (int)$colors[2], (int)$colors[3], 1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $value
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function ensureColorValue($value)
|
||||
{
|
||||
$value = (int)$value;
|
||||
if ($value < 0) {
|
||||
$value = 0;
|
||||
}
|
||||
if ($value > 255) {
|
||||
$value = 255;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $alpha Wert zwischen 0 und 1
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setAlpha($alpha)
|
||||
{
|
||||
$alpha = (float)$alpha;
|
||||
if ($alpha < 0.0) {
|
||||
$alpha = 0.0;
|
||||
}
|
||||
if ($alpha > 1.0) {
|
||||
$alpha = 1.0;
|
||||
}
|
||||
|
||||
$this->alpha = $alpha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Farbwerte per Zufall variieren
|
||||
*
|
||||
* @param int $difference
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function makeVariant($difference)
|
||||
{
|
||||
$difference = (int)$difference;
|
||||
|
||||
$redVariant = $this->red + mt_rand($difference * -1, $difference);
|
||||
$this->red = $this->ensureColorValue($redVariant);
|
||||
|
||||
$greenVariant = $this->green + mt_rand($difference * -1, $difference);
|
||||
$this->green = $this->ensureColorValue($greenVariant);
|
||||
|
||||
$blueVariant = $this->blue + mt_rand($difference * -1, $difference);
|
||||
$this->blue = $this->ensureColorValue($blueVariant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Farbe heller machen; verändert nicht die Transparenz
|
||||
*
|
||||
* @param float $percent
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function makeLighter($percent = 10.0)
|
||||
{
|
||||
$percent = (float)$percent;
|
||||
$difference = (int)($percent * 2.55 / 2);
|
||||
$this->red = $this->ensureColorValue($this->red + $difference);
|
||||
$this->blue = $this->ensureColorValue($this->blue + $difference);
|
||||
$this->green = $this->ensureColorValue($this->green + $difference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Farbe dunkler machen; verändert nicht die Transparenz
|
||||
*
|
||||
* @param float $percent
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function makeDarker($percent = 10.0)
|
||||
{
|
||||
$percent = (float)$percent;
|
||||
$difference = (int)($percent * 2.55 / 2);
|
||||
$this->red = $this->ensureColorValue($this->red - $difference);
|
||||
$this->blue = $this->ensureColorValue($this->blue - $difference);
|
||||
$this->green = $this->ensureColorValue($this->green - $difference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausgabe in CSS rgba() Notation
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toCssRgba()
|
||||
{
|
||||
return sprintf(
|
||||
'rgba(%s, %s, %s, %s)',
|
||||
$this->red,
|
||||
$this->green,
|
||||
$this->blue,
|
||||
number_format($this->alpha, 3, '.', '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausgabe in CSS rgb() Notation; Transparenz geht verloren
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toCssRgb()
|
||||
{
|
||||
return sprintf(
|
||||
'rgb(%s, %s, %s)',
|
||||
$this->red,
|
||||
$this->green,
|
||||
$this->blue
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausgabe als Hex-Farbwert; Transparenz geht verloren
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toHex()
|
||||
{
|
||||
return sprintf(
|
||||
'#%s%s%s',
|
||||
str_pad(dechex($this->red), 2, '0', STR_PAD_LEFT),
|
||||
str_pad(dechex($this->green), 2, '0', STR_PAD_LEFT),
|
||||
str_pad(dechex($this->blue), 2, '0', STR_PAD_LEFT)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->__toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->toCssRgba();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\Chart;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use JsonSerializable;
|
||||
|
||||
class Dataset implements JsonSerializable
|
||||
{
|
||||
const LINE_STYLE_SOLID = 'solid';
|
||||
const LINE_STYLE_DASHED = 'dashed';
|
||||
const LINE_STYLE_DOTTED = 'dotted';
|
||||
|
||||
const COLOR_GREEN = 'green';
|
||||
const COLOR_BLUE = 'blue';
|
||||
const COLOR_ORANGE = 'orange';
|
||||
const COLOR_DARKBLUE = 'darkblue';
|
||||
|
||||
/** @var array $validLineStyles */
|
||||
protected static $validLineStyles = [
|
||||
self::LINE_STYLE_SOLID,
|
||||
self::LINE_STYLE_DOTTED,
|
||||
self::LINE_STYLE_DASHED,
|
||||
];
|
||||
|
||||
/** @var array $colorMap */
|
||||
protected static $colorMap = [
|
||||
self::COLOR_GREEN => [162, 197, 90],
|
||||
self::COLOR_BLUE => [69, 185, 211],
|
||||
self::COLOR_ORANGE => [246, 158, 6],
|
||||
self::COLOR_DARKBLUE => [14, 131, 148],
|
||||
];
|
||||
|
||||
/** @var array $defaultOptions Standardwerte für Dataset-Optionen */
|
||||
protected static $defaultOptions = [
|
||||
'pointBorderWidth' => 1,
|
||||
'pointHoverBorderWidth' => 2,
|
||||
'pointRadius' => 1,
|
||||
'pointHoverRadius' => 5,
|
||||
'pointHitRadius' => 15,
|
||||
'lineTension' => 0.2,
|
||||
'fill' => true,
|
||||
'borderWidth' => 3,
|
||||
'borderDash' => [],
|
||||
'borderCapStyle' => 'butt',
|
||||
'borderColor' => 'rgba(0, 0, 0, 0.1)',
|
||||
'backgroundColor' => 'rgba(0, 0, 0, 0.1)',
|
||||
'pointBackgroundColor' => 'rgba(0, 0, 0, 0.1)',
|
||||
];
|
||||
|
||||
/** @var array $defaultAlphaValues Transparenz-Werte */
|
||||
protected static $defaultAlphaValues = [
|
||||
'borderColor' => 1.0,
|
||||
'backgroundColor' => 0.1,
|
||||
'pointBackgroundColor' => 1.0,
|
||||
];
|
||||
|
||||
protected $data;
|
||||
protected $label;
|
||||
protected $options;
|
||||
|
||||
/** @var bool $isDataAccumulated Gibt an ob Daten bereits kumuliert wurden */
|
||||
protected $isDataAccumulated = false;
|
||||
|
||||
/** @var bool $hasColorSet Gibt an ob bereits eine Farbe manuell gesetzt wurde */
|
||||
protected $hasColorAssigned = false;
|
||||
|
||||
/**
|
||||
* @param string $label
|
||||
* @param array $data
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct($label, $data, array $options = [])
|
||||
{
|
||||
// Ungültige Werte in leeres Array wandeln
|
||||
if (!is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
$this->data = $data;
|
||||
$this->label = $label;
|
||||
$this->options = array_replace(self::$defaultOptions, $options);
|
||||
|
||||
$defaultColor = new Color(0, 0, 0, 0.1);
|
||||
if (!is_object($this->options['borderColor'])) {
|
||||
$this->options['borderColor'] = $defaultColor;
|
||||
}
|
||||
if (!is_object($this->options['backgroundColor'])) {
|
||||
$this->options['backgroundColor'] = $defaultColor;
|
||||
}
|
||||
if (!is_object($this->options['pointBackgroundColor'])) {
|
||||
$this->options['pointBackgroundColor'] = $defaultColor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDataCount()
|
||||
{
|
||||
return count($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Wurde bereits eine Farbe gesetzt
|
||||
*/
|
||||
public function hasColorAssigned()
|
||||
{
|
||||
return $this->hasColorAssigned;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function accumulateData()
|
||||
{
|
||||
if ($this->isDataAccumulated === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sum = 0;
|
||||
foreach ($this->data as $key => $value) {
|
||||
$sum += (float)$value;
|
||||
$this->data[$key] = $sum;
|
||||
}
|
||||
|
||||
$this->isDataAccumulated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $lineStyle
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setLineStyle($lineStyle)
|
||||
{
|
||||
if (!in_array($lineStyle, self::$validLineStyles, true)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Line style "%s" is not valid.', $lineStyle
|
||||
));
|
||||
}
|
||||
|
||||
if ($lineStyle === self::LINE_STYLE_DOTTED) {
|
||||
$this->options['borderDash'] = [1, 15];
|
||||
$this->options['borderCapStyle'] = 'round';
|
||||
}
|
||||
if ($lineStyle === self::LINE_STYLE_DASHED) {
|
||||
$this->options['borderDash'] = [15, 10];
|
||||
$this->options['borderCapStyle'] = 'butt';
|
||||
}
|
||||
if ($lineStyle === self::LINE_STYLE_SOLID) {
|
||||
$this->options['borderDash'] = [];
|
||||
$this->options['borderCapStyle'] = 'butt';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Color $color
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setColor(Color $color)
|
||||
{
|
||||
$borderColor = clone $color;
|
||||
$backgroundColor = clone $color;
|
||||
$pointBackgroundColor = clone $color;
|
||||
|
||||
$borderColor->setAlpha($this->getDefaultAlphaValue('borderColor'));
|
||||
$backgroundColor->setAlpha($this->getDefaultAlphaValue('backgroundColor'));
|
||||
$pointBackgroundColor->setAlpha($this->getDefaultAlphaValue('pointBackgroundColor'));
|
||||
|
||||
$this->options['borderColor'] = $borderColor;
|
||||
$this->options['backgroundColor'] = $backgroundColor;
|
||||
$this->options['pointBackgroundColor'] = $pointBackgroundColor;
|
||||
|
||||
$this->hasColorAssigned = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $red
|
||||
* @param int $green
|
||||
* @param int $blue
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setColorByRgb($red, $green, $blue)
|
||||
{
|
||||
$this->setColor(new Color($red, $green, $blue, 1.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hexColor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setColorByHex($hexColor)
|
||||
{
|
||||
$hexColor = str_replace('#', '', $hexColor);
|
||||
if (strlen($hexColor) !== 6) {
|
||||
throw new InvalidArgumentException('Only full length hex values are supported.');
|
||||
}
|
||||
|
||||
$parts = str_split($hexColor, 2);
|
||||
$red = hexdec($parts[0]);
|
||||
$green = hexdec($parts[1]);
|
||||
$blue = hexdec($parts[2]);
|
||||
|
||||
$this->setColorByRgb($red, $green, $blue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $colorName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setColorByName($colorName)
|
||||
{
|
||||
if (!isset(self::$colorMap[$colorName])) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Color name "%s" is not valid.', $colorName
|
||||
));
|
||||
}
|
||||
|
||||
$rgb = self::$colorMap[$colorName];
|
||||
$this->setColorByRgb(...$rgb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zufällige ID generieren; wird für Multiple Axes Config benötigt
|
||||
*
|
||||
* @see http://www.chartjs.org/docs/latest/axes/cartesian/#axis-id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateYAxisId()
|
||||
{
|
||||
if (!isset($this->options['yAxisID'])) {
|
||||
$this->options['yAxisID'] = uniqid('', false);
|
||||
}
|
||||
|
||||
return $this->options['yAxisID'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$result = $this->options;
|
||||
$result['data'] = $this->data;
|
||||
if (!empty($this->label)) {
|
||||
$result['label'] = $this->label;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $colorType
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function getDefaultAlphaValue($colorType)
|
||||
{
|
||||
if (!isset(static::$defaultAlphaValues[$colorType])) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Wichtig: static nicht self!
|
||||
// Sonst hat Überschreiben von $defaultAlphaValues in abgeleiteten Klassen keine Auswirkung.
|
||||
return static::$defaultAlphaValues[$colorType];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\Chart;
|
||||
|
||||
class HtmlRenderer
|
||||
{
|
||||
/** @var Chart $chart */
|
||||
protected $chart;
|
||||
|
||||
/** @var string $title */
|
||||
protected $title;
|
||||
|
||||
/** @var int $width */
|
||||
protected $width;
|
||||
|
||||
/** @var int $height */
|
||||
protected $height;
|
||||
|
||||
/** @var array $attributes */
|
||||
protected $attributes;
|
||||
|
||||
/**
|
||||
* @param Chart $chart
|
||||
* @param string $title
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param array $attributes Zusätzliche HTML-Attribute als assoziatives Array
|
||||
*/
|
||||
public function __construct(Chart $chart, $title = '', $width = 400, $height = 200, array $attributes = [])
|
||||
{
|
||||
$this->chart = $chart;
|
||||
$this->title = $title;
|
||||
$this->width = (int)$width;
|
||||
$this->height = (int)$height;
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return sprintf(
|
||||
'<div class="chart-wrapper" %s>%s' .
|
||||
' <div class="chart-content">' .
|
||||
' <canvas data-graph-id="%s" width="%s" height="%s"></canvas>' .
|
||||
' <script type="application/json">%s</script>' .
|
||||
' </div>' .
|
||||
'</div>',
|
||||
$this->renderAttributes(),
|
||||
$this->renderTitle(),
|
||||
uniqid(null, false),
|
||||
$this->width,
|
||||
$this->height,
|
||||
$this->chart->toJson()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function renderAttributes()
|
||||
{
|
||||
$result = '';
|
||||
foreach ($this->attributes as $key => $value) {
|
||||
$result .= sprintf(' %s="%s"', $key, $value);
|
||||
}
|
||||
|
||||
return trim($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function renderTitle()
|
||||
{
|
||||
if (empty($this->title)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf('<div class="chart-title">%s</div>', $this->title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\Chart;
|
||||
|
||||
use DateInterval;
|
||||
use DatePeriod;
|
||||
use DateTimeInterface;
|
||||
|
||||
class PeriodMatcher
|
||||
{
|
||||
/** @var DateTimeInterface $start */
|
||||
protected $start;
|
||||
|
||||
/** @var DateTimeInterface $end */
|
||||
protected $end;
|
||||
|
||||
/** @var DateInterval $interval */
|
||||
protected $interval;
|
||||
|
||||
/** @var string $format */
|
||||
protected $format;
|
||||
|
||||
/** @var DatePeriod $range */
|
||||
protected $range;
|
||||
|
||||
/**
|
||||
* @param DateTimeInterface $start
|
||||
* @param DateTimeInterface $end
|
||||
* @param DateInterval $interval
|
||||
* @param string $format Formate der PHP date()-Funktion
|
||||
*/
|
||||
public function __construct(
|
||||
DateTimeInterface $start,
|
||||
DateTimeInterface $end,
|
||||
DateInterval $interval,
|
||||
$format = 'Y.m.d'
|
||||
) {
|
||||
$this->start = $start;
|
||||
$this->end = $end;
|
||||
$this->interval = $interval;
|
||||
$this->format = $format;
|
||||
$this->range = new DatePeriod($start, $interval, $end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param string $dateKey
|
||||
* @param string $valueKey
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function matchData($data, $dateKey, $valueKey)
|
||||
{
|
||||
if ($data === null) {
|
||||
$data = [];
|
||||
}
|
||||
$dates = array_column($data, $dateKey);
|
||||
$values = array_column($data, $valueKey);
|
||||
|
||||
$result = [];
|
||||
foreach ($this->getDates() as $date) {
|
||||
$matchedKey = array_search($date, $dates, true);
|
||||
$result[] = $matchedKey !== false ? (float)$values[$matchedKey] : 0.0;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $format Datumsformat überschreiben
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDates($format = null)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
/** @var DateTimeInterface $date */
|
||||
foreach ($this->range as $date) {
|
||||
$result[] = $date->format($format !== null ? $format : $this->format);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\Chart;
|
||||
|
||||
/**
|
||||
* Spezielles Dataset für Pie-/Doughnut-Charts
|
||||
*
|
||||
* Besonderheiten:
|
||||
* * Farben müssen als Array angegeben werden; pro Wert eine Farbe
|
||||
* * Strichbreiten ('borderWidth') müssen als Array angegeben werden; pro Wert eine Breite
|
||||
*
|
||||
* http://www.chartjs.org/docs/master/charts/doughnut.html#dataset-properties
|
||||
*/
|
||||
class PieDataset extends Dataset
|
||||
{
|
||||
/** @var array $defaultAlphaValues Transparenz-Werte */
|
||||
protected static $defaultAlphaValues = [
|
||||
'borderColor' => 1.0,
|
||||
'backgroundColor' => 1.0,
|
||||
'hoverBorderColor' => 1.0,
|
||||
];
|
||||
|
||||
/** @var int $colorPointer Merkt sich die zuletzt verwendete Default-Color */
|
||||
private $colorPointer = 0;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function __construct($label, $data, array $options = [])
|
||||
{
|
||||
parent::__construct($label, $data, $options);
|
||||
|
||||
$this->options['borderWidth'] = 3;
|
||||
|
||||
unset($this->options['pointBackgroundColor']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Color $color
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setColor(Color $color)
|
||||
{
|
||||
$backgroundColor = clone $color;
|
||||
$borderColor = new Color(255, 255, 255);
|
||||
$hoverBorderColor = new Color(255, 255, 255);
|
||||
|
||||
$borderColor->setAlpha($this->getDefaultAlphaValue('borderColor'));
|
||||
$backgroundColor->setAlpha($this->getDefaultAlphaValue('backgroundColor'));
|
||||
$hoverBorderColor->setAlpha($this->getDefaultAlphaValue('hoverBorderColors'));
|
||||
|
||||
// Hintergrundfarbe als Array setzen
|
||||
// Pro Datensatz Farbe etwas heller machen
|
||||
$backgroundColors = [];
|
||||
$dataCount = $this->getDataCount();
|
||||
foreach ($this->data as $key => $value) {
|
||||
$difference = $key * 75 / $dataCount;
|
||||
$backgroundColorLighter = clone $backgroundColor;
|
||||
$backgroundColorLighter->makeLighter($difference);
|
||||
$backgroundColors[$key] = $backgroundColorLighter;
|
||||
}
|
||||
|
||||
$this->options['borderColor'] = $borderColor;
|
||||
$this->options['backgroundColor'] = $backgroundColors;
|
||||
$this->options['hoverBorderColor'] = $hoverBorderColor;
|
||||
|
||||
$this->hasColorAssigned = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hintergrundfarben pro Wert hinterlegen
|
||||
*
|
||||
* Die Anzahl der Farben sollte der Anzahl der Daten entsprechen
|
||||
*
|
||||
* @param array|string[] $colors Hexadecimal- oder RGB-Schreibweise
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setColors(array $colors)
|
||||
{
|
||||
// Rahmenfarbe auf Weiß setzen
|
||||
$borderColor = new Color(255, 255, 255);
|
||||
$hoverBorderColor = new Color(255, 255, 255);
|
||||
$borderColor->setAlpha($this->getDefaultAlphaValue('borderColor'));
|
||||
$hoverBorderColor->setAlpha($this->getDefaultAlphaValue('hoverBorderColors'));
|
||||
|
||||
// Nur Hintergrundfarbe/Füllfarbe variieren
|
||||
$backgroundColors = [];
|
||||
//foreach ($colors as $color) {
|
||||
foreach ($this->data as $key => $value) {
|
||||
$color = $colors[$key];
|
||||
if (strpos($color, '#', 0) === 0) {
|
||||
$backgroundColor = Color::createFromHex($color);
|
||||
} else {
|
||||
$rgb = $this->getNextDefaultColor();
|
||||
$backgroundColor = new Color($rgb[0], $rgb[1], $rgb[2], 1);
|
||||
}
|
||||
|
||||
$backgroundColor->setAlpha($this->getDefaultAlphaValue('backgroundColor'));
|
||||
$backgroundColors[] = $backgroundColor;
|
||||
}
|
||||
|
||||
$this->options['borderColor'] = $borderColor;
|
||||
$this->options['hoverBorderColor'] = $hoverBorderColor;
|
||||
$this->options['backgroundColor'] = $backgroundColors;
|
||||
|
||||
$this->hasColorAssigned = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array RGB-Array
|
||||
*/
|
||||
private function getNextDefaultColor()
|
||||
{
|
||||
$this->colorPointer++;
|
||||
if ($this->colorPointer >= count(self::$colorMap)) {
|
||||
$this->colorPointer = 0;
|
||||
}
|
||||
|
||||
$colors = array_values(self::$colorMap);
|
||||
|
||||
return $colors[$this->colorPointer];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\ChunkedUpload;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerJavascript()
|
||||
{
|
||||
return [
|
||||
'chunkedupload' => [
|
||||
'./classes/Widgets/ChunkedUpload/www/js/jquery.chunkedUpload.js',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerStylesheets()
|
||||
{
|
||||
return [
|
||||
'chunkedupload' => [
|
||||
'./classes/Widgets/ChunkedUpload/www/css/chunked_upload.css',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices()
|
||||
{
|
||||
return [
|
||||
'ChunkedUploadRequestHandler' => 'onInitChunkedUploadRequestHandler',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ChunkedUploadRequestHandler
|
||||
*/
|
||||
public static function onInitChunkedUploadRequestHandler()
|
||||
{
|
||||
return new ChunkedUploadRequestHandler();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\ChunkedUpload;
|
||||
|
||||
use Xentral\Components\Http\JsonResponse;
|
||||
use Xentral\Components\Http\Request;
|
||||
use Xentral\Components\Util\StringUtil;
|
||||
use Xentral\Widgets\ChunkedUpload\Exception\ChunkedUploadExceptionInterface;
|
||||
use Xentral\Widgets\ChunkedUpload\Exception\DecodingFailedException;
|
||||
use Xentral\Widgets\ChunkedUpload\Exception\FilesystemErrorException;
|
||||
|
||||
final class ChunkedUploadRequestHandler
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canHandleRequest(Request $request)
|
||||
{
|
||||
if ($request->isCli() || !$request->isAjax()) {
|
||||
return false;
|
||||
}
|
||||
if (!$request->post->has('file_id') ||
|
||||
!$request->post->has('file_name') ||
|
||||
!$request->post->has('file_data') ||
|
||||
!$request->post->has('file_size')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return strtolower($request->getHeader('Content-Type')) === 'application/x-www-form-urlencoded; charset=utf-8';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param string $tempDir Absolute path to directory, where the unfinished file will be stored
|
||||
* @param string $saveDir Absolute path to directory, where the final upload will be stored
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function handleRequest(Request $request, $tempDir, $saveDir, $newFilename = null)
|
||||
{
|
||||
try {
|
||||
$bytesWritten = $this->handleUpload($request, $tempDir, $saveDir, $newFilename);
|
||||
} catch (ChunkedUploadExceptionInterface $exception) {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'error' => $exception->getMessage(),
|
||||
], JsonResponse::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Antwort zusammenbauen
|
||||
$responseData = [
|
||||
'success' => true,
|
||||
'file' => [
|
||||
'id' => $request->getPost('file_id'),
|
||||
'bytes' => $bytesWritten,
|
||||
],
|
||||
];
|
||||
|
||||
// Beim ersten Request das Upload-Limit von PHP in der Antwort mitschicken
|
||||
// Erklärung:
|
||||
// Der erste Chunk ist bewusst klein gewählt (100KB), damit auf keinen Fall das Upload-Limit von PHP greift.
|
||||
// Die Antwort nach dem Upload des ersten Chunks enthält das Upload-Limit von PHP.
|
||||
// Der Uploader passt die Chunk-Size an, falls diese über dem Upload-Limit von PHP liegt.
|
||||
$fileOffset = $request->getPost('file_offset', null);
|
||||
if ($fileOffset !== null && (int)$fileOffset === 0) {
|
||||
$responseData['uploadLimit'] = $this->determineMaxUploadSize();
|
||||
}
|
||||
|
||||
return new JsonResponse($responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param string $tempDir Absolute path to directory, where the unfinished file will be stored
|
||||
* @param string $saveDir Absolute path to directory, where the final upload will be stored
|
||||
* @param string|null $newFileName
|
||||
*
|
||||
* @FilesystemErrorException
|
||||
*
|
||||
* @return int Bytes written
|
||||
*/
|
||||
private function handleUpload(Request $request, $tempDir, $saveDir, $newFileName = null)
|
||||
{
|
||||
if (!is_dir($tempDir)) {
|
||||
throw new FilesystemErrorException(sprintf('Temporary upload directory does not exist: %s', $tempDir));
|
||||
}
|
||||
if (!is_dir($saveDir)) {
|
||||
throw new FilesystemErrorException(sprintf('Final storage directory does not exist: %s', $saveDir));
|
||||
}
|
||||
|
||||
$fileId = $request->getPost('file_id');
|
||||
$fileName = $request->getPost('file_name');
|
||||
$fileData = $request->getPost('file_data');
|
||||
$fileSize = (int)$request->getPost('file_size');
|
||||
$fileHash = sha1(json_encode(['id' => $fileId, 'name' => $newFileName ?? $fileName, 'size' => $fileSize]));
|
||||
|
||||
$tempPath = realpath($tempDir) . '/' . $fileHash;
|
||||
$savePath = realpath($saveDir) . '/' . ($newFileName ?? $fileName);
|
||||
$bytesWritten = $this->writeChunkData($tempPath, $fileData, $fileName);
|
||||
$tempSize = (int)@filesize($tempPath);
|
||||
|
||||
if (file_exists($savePath)) {
|
||||
@unlink($tempPath);
|
||||
throw new FilesystemErrorException(sprintf(
|
||||
'Pre check failed. Final file already exists: %s', $savePath
|
||||
));
|
||||
}
|
||||
|
||||
if ($tempSize > $fileSize) {
|
||||
@unlink($tempPath);
|
||||
throw new FilesystemErrorException(sprintf(
|
||||
'Unknown Error: Temporary file is larger than uploaded file: %s', $tempPath
|
||||
));
|
||||
}
|
||||
|
||||
if ($tempSize === $fileSize) {
|
||||
$this->moveFinishedFile($tempPath, $savePath);
|
||||
}
|
||||
|
||||
return $bytesWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tempPath Absolute path to temp file
|
||||
* @param string $savePath Absolute path to final file
|
||||
*
|
||||
* @throws FilesystemErrorException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function moveFinishedFile($tempPath, $savePath)
|
||||
{
|
||||
if (!file_exists($tempPath)) {
|
||||
throw new FilesystemErrorException(sprintf(
|
||||
'Failed to move temporary file to final location. Temp file is missing: %s', $tempPath
|
||||
));
|
||||
}
|
||||
|
||||
if (!@rename($tempPath, $savePath)) {
|
||||
@unlink($tempPath);
|
||||
throw new FilesystemErrorException(sprintf(
|
||||
'Failed to move temporary file to final location: %s', $savePath
|
||||
));
|
||||
}
|
||||
|
||||
@unlink($tempPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tempPath Absolute path to temp file; chunk data will be appended
|
||||
* @param string $fileData Base64 encoded chunk data
|
||||
* @param string $fileName File name; without directory; Only needed for debugging
|
||||
*
|
||||
* @throws FilesystemErrorException
|
||||
*
|
||||
* @return int Bytes written
|
||||
*/
|
||||
private function writeChunkData($tempPath, $fileData, $fileName)
|
||||
{
|
||||
$resource = @fopen($tempPath, 'a+b');
|
||||
if ($resource === false) {
|
||||
throw new FilesystemErrorException(sprintf('Can not open file for writing: %s', $tempPath));
|
||||
}
|
||||
|
||||
$binaryData = $this->decodeChunkData($fileData, $fileName);
|
||||
$bytesWritten = @fwrite($resource, $binaryData);
|
||||
if ($bytesWritten === false) {
|
||||
@unlink($tempPath);
|
||||
throw new FilesystemErrorException(sprintf('Can not write to file: %s', $tempPath));
|
||||
}
|
||||
if (@fclose($resource) === false) {
|
||||
@unlink($tempPath);
|
||||
throw new FilesystemErrorException(sprintf('Could not close file pointer: %s', $tempPath));
|
||||
}
|
||||
|
||||
return (int)$bytesWritten;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $data Example 'data:application/octet-stream;base64,S0cJXKqx01pYOVeXbdtv...'
|
||||
* @param string $fileName File name; without directory; Only needed for debugging
|
||||
*
|
||||
* @throws DecodingFailedException
|
||||
*
|
||||
* @return string Decoded binary data chunk
|
||||
*/
|
||||
private function decodeChunkData($data, $fileName)
|
||||
{
|
||||
$parts = explode(';base64,', $data);
|
||||
if (!is_array($parts) || !isset($parts[1])) {
|
||||
throw new DecodingFailedException(sprintf('Could not decode file upload #1. File name: %s', $fileName));
|
||||
}
|
||||
|
||||
$binaryData = base64_decode($parts[1]);
|
||||
if ($binaryData === false) {
|
||||
throw new DecodingFailedException(sprintf('Could not decode file upload #2. File name: %s', $fileName));
|
||||
}
|
||||
|
||||
return $binaryData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int Max upload size per file in bytes
|
||||
*/
|
||||
private function determineMaxUploadSize()
|
||||
{
|
||||
$fileLimit = StringUtil::parsePhpByteSize(ini_get('upload_max_filesize'));
|
||||
$postLimit = StringUtil::parsePhpByteSize(ini_get('post_max_size'));
|
||||
$memLimit = StringUtil::parsePhpByteSize(ini_get('memory_limit'));
|
||||
|
||||
return min($fileLimit, $postLimit, $memLimit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\ChunkedUpload\Exception;
|
||||
|
||||
use Xentral\Core\Exception\WidgetExceptionInterface;
|
||||
|
||||
interface ChunkedUploadExceptionInterface extends WidgetExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\ChunkedUpload\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class DecodingFailedException extends RuntimeException implements ChunkedUploadExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\ChunkedUpload\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class FilesystemErrorException extends RuntimeException implements ChunkedUploadExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
# ChunkedUpload-Widget
|
||||
|
||||
## Einrichtung
|
||||
|
||||
### Im Modul
|
||||
|
||||
```php
|
||||
/** @var \Xentral\Components\Http\Request $request */
|
||||
$request = $this->app->Container->get('Request');
|
||||
|
||||
/** @var \Xentral\Widgets\ChunkedUpload\ChunkedUploadRequestHandler $handler */
|
||||
$handler = $this->app->Container->get('ChunkedUploadRequestHandler');
|
||||
|
||||
if ($handler->canHandleRequest($request)) {
|
||||
$tempDir = $this->app->erp->GetTMP(); // alternativ sys_get_temp_dir();
|
||||
$saveDir = __DIR__ . '/uploads';
|
||||
$response = $handler->handleRequest($request, $tempDir, $saveDir);
|
||||
$response->send();
|
||||
$this->app->erp->ExitWawi();
|
||||
}
|
||||
```
|
||||
|
||||
Zum Laden des benötigten jQuery-Plugins reicht folgende Zeile im Modul:
|
||||
|
||||
```php
|
||||
$this->app->ModuleScriptCache->IncludeWidgetNew('ChunkedUpload');
|
||||
```
|
||||
|
||||
### Im Template
|
||||
|
||||
```html
|
||||
<input type="file" id="chunkyfile" multiple="multiple">
|
||||
```
|
||||
|
||||
### In Javascript
|
||||
|
||||
```javascript
|
||||
$(document).ready(function () {
|
||||
$('#chunkyfile').chunkedUpload({
|
||||
upload: {
|
||||
url: 'index.php?module=meinmodul&action=meineaction&cmd=upload'
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
.chunked-file-upload-container table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.chunked-file-upload-container table th,
|
||||
.chunked-file-upload-container table td {
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
.chunked-file-upload-container table .button {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
/**
|
||||
* # ChunkedUpload
|
||||
*
|
||||
* ## Initialisierung
|
||||
*
|
||||
* ```html
|
||||
* <input type="file" id="chunky">
|
||||
* <div id="files"></div>
|
||||
*
|
||||
* <script type="application/javascript">
|
||||
* $('#chunky').chunkedUpload({
|
||||
* chunkSize: 6291456, // 6MB
|
||||
* upload: {
|
||||
* url: 'index.php?module=foo&action=bar',
|
||||
* },
|
||||
* filesContainer: '#files'
|
||||
* });
|
||||
* </script>
|
||||
* ```
|
||||
*
|
||||
* ## Callback
|
||||
*
|
||||
* ### `fileComplete`
|
||||
*
|
||||
* ```javascript
|
||||
* $('#chunky').chunkedUpload({
|
||||
* fileComplete: function(fileInfo) {
|
||||
* // Do something when file upload is completed
|
||||
* // fileInfo.id = Unique id; example: 'chunked_upload_2377390993'
|
||||
* // fileInfo.name = Client file name
|
||||
* // fileInfo.type = File mime type
|
||||
* // fileInfo.size = File size in bytes
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
var ChunkedUpload = function ($elem, options) {
|
||||
|
||||
var STATUS = {
|
||||
WAITING: 'waiting', // Datei wurde hinzugefügt; Es wird gewartet dass Benutzer den Upload startet
|
||||
UPLOADING: 'uploading', // Datei wird gerade hochgeladen
|
||||
FINISHED: 'finished', // Upload verarbeitet
|
||||
FAILURE: 'failure' // Fehler beim Upload
|
||||
};
|
||||
|
||||
var me = {
|
||||
|
||||
/** @property {Object} me.options Default configuration */
|
||||
options: {
|
||||
chunkSize: 6291456, // 6291456 = 6MB
|
||||
upload: {
|
||||
url: null,
|
||||
view: 'standard',
|
||||
formData: {}
|
||||
},
|
||||
filesContainer: '#chunked-upload-files',
|
||||
|
||||
/**
|
||||
* Callback wenn Datei erfolgreich hochgeladen wurde
|
||||
*
|
||||
* @param {FileInfo} fileInfo
|
||||
*/
|
||||
fileComplete: function (fileInfo) {}
|
||||
},
|
||||
|
||||
/** @property {Object} me.storage Runtime storage */
|
||||
storage: {
|
||||
$fileInput: null,
|
||||
$filesContainer: null,
|
||||
$filesList: null,
|
||||
uploads: {}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
* @param {Object} options
|
||||
*/
|
||||
init: function (element, options) {
|
||||
|
||||
// Prüfen ob HTML5-File-API verfügbar
|
||||
if (!me.isFileApiAvailable()) {
|
||||
alert(
|
||||
'Die HTML5 File-API wird von ihrem Browser nicht unterstützt. ' +
|
||||
'Bitte verwenden sie einen moderneren Browser.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optionen mit Defaults mergen
|
||||
me.options = $.extend({}, me.options, options);
|
||||
if (typeof me.options.upload.url === 'undefined' || me.options.upload.url === null) {
|
||||
throw 'Initialisierung nicht möglich. Upload-URL fehlt.';
|
||||
}
|
||||
if (typeof me.options.upload.formData === 'undefined' || me.options.upload.formData === null) {
|
||||
me.options.upload.formData = {};
|
||||
}
|
||||
|
||||
var $fileInput = $(element);
|
||||
if ($fileInput.length !== 1) {
|
||||
throw 'File-Input Element wurde nicht gefunden.';
|
||||
}
|
||||
if (!$fileInput.is('input[type=file]')) {
|
||||
alert('Init-Element muss ein "input"-Element vom Typ "file" sein.');
|
||||
return;
|
||||
}
|
||||
|
||||
$fileInput.on('change', me.onSelectFilesEventHandler);
|
||||
me.storage.$fileInput = $fileInput;
|
||||
|
||||
me.createFilesContainer();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
onSelectFilesEventHandler: function (event) {
|
||||
/** @var {FileList} files */
|
||||
var files = event.target.files;
|
||||
$.each(files, function (index, file) {
|
||||
me.addFileUpload(file);
|
||||
});
|
||||
|
||||
// File-Input leeren
|
||||
var $input = $(this);
|
||||
$input.replaceWith($input.val('').clone(true));
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {File} fileObject
|
||||
*/
|
||||
addFileUpload: function (fileObject) {
|
||||
var fileId = me.generateRandomId();
|
||||
var fileSize = me.formatBytes(fileObject.size);
|
||||
|
||||
var uploadObject = {
|
||||
id: fileId,
|
||||
file: fileObject,
|
||||
reader: new FileReader(),
|
||||
status: STATUS.WAITING,
|
||||
elements: {
|
||||
$progressBar: null,
|
||||
$statusInfo: null,
|
||||
$actionCell: null
|
||||
}
|
||||
};
|
||||
|
||||
var $removeFileButton = $('<a>');
|
||||
$removeFileButton.attr('href', '#').text('Entfernen');
|
||||
$removeFileButton.addClass('chuncked-removefile-trigger').addClass('button');
|
||||
$removeFileButton.on('click', function (event) {
|
||||
event.preventDefault();
|
||||
var $link = $(this);
|
||||
var $row = $link.parents('tr');
|
||||
var fileId = $row.data('fileId');
|
||||
|
||||
me.removeFile(fileId);
|
||||
});
|
||||
|
||||
var $uploadFileButton = $('<a>');
|
||||
$uploadFileButton.attr('href', '#').text('Hochladen');
|
||||
$uploadFileButton.addClass('chuncked-startupload-trigger').addClass('button');
|
||||
$uploadFileButton.on('click', function (event) {
|
||||
event.preventDefault();
|
||||
var $link = $(this);
|
||||
var $row = $link.parents('tr');
|
||||
var fileId = $row.data('fileId');
|
||||
|
||||
me.startUpload(fileId);
|
||||
});
|
||||
|
||||
var $progressBar = $('<progress>').attr('min', '0').attr('max', '100').val(0);
|
||||
var $row = $('<tr>').attr('id', fileId).data('fileId', fileId);
|
||||
if(me.options.upload.view === 'sidebar') {
|
||||
var $td = $('<td>').appendTo($row);
|
||||
var $table = $('<table>').appendTo($td);
|
||||
$('<td>').addClass('filename').html(fileObject.name).appendTo($('<tr>').appendTo($table)).before('<td>Dateiname:</td>');
|
||||
$('<td>').addClass('filesize').html(fileSize).appendTo($('<tr>').appendTo($table)).before('<td>Größe:</td>');
|
||||
$('<td>').addClass('fileprogress').html($progressBar).appendTo($('<tr>').appendTo($table)).before('<td>Fortschritt:</td>');
|
||||
var $statusInfo = $('<td>').addClass('filestatus').html('Bereit zum Hochladen').appendTo($('<tr>').appendTo($table)).before('<td>Status:</td>');
|
||||
var $actionsCell = $('<td>').addClass('fileaction').appendTo($('<tr>').data('fileId', fileId).appendTo($table));
|
||||
}
|
||||
else {
|
||||
$('<td>').addClass('filename').html(fileObject.name).appendTo($row);
|
||||
$('<td>').addClass('filesize').html(fileSize).appendTo($row);
|
||||
$('<td>').addClass('fileprogress').html($progressBar).appendTo($row);
|
||||
var $statusInfo = $('<td>').addClass('filestatus').html('Bereit zum Hochladen').appendTo($row);
|
||||
var $actionsCell = $('<td>').addClass('fileaction').appendTo($row);
|
||||
}
|
||||
$actionsCell.append($removeFileButton);
|
||||
$actionsCell.append($uploadFileButton);
|
||||
$row.appendTo(me.storage.$filesList);
|
||||
|
||||
uploadObject.elements.$progressBar = $progressBar;
|
||||
uploadObject.elements.$statusInfo = $statusInfo;
|
||||
uploadObject.elements.$actionCell = $actionsCell;
|
||||
me.storage.uploads[fileId] = uploadObject;
|
||||
|
||||
me.storage.$filesContainer.show();
|
||||
me.storage.$filesContainer.find('table').show();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {String} uploadId
|
||||
*/
|
||||
removeFile: function (uploadId) {
|
||||
var $row = $('#' + uploadId);
|
||||
if ($row.length === 0) {
|
||||
alert('Can not remove file upload. Element "#' + uploadId + '" not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Datei existiert nicht (mehr?)
|
||||
if (!me.storage.uploads.hasOwnProperty(uploadId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload läuft gerade
|
||||
if (me.storage.uploads[uploadId].status === STATUS.UPLOADING) {
|
||||
return; // @todo Laufenden Upload abbrechen
|
||||
}
|
||||
|
||||
// Tabellenzeile entfernen
|
||||
$row.remove();
|
||||
delete me.storage.uploads[uploadId];
|
||||
},
|
||||
|
||||
/**
|
||||
* @ŧodo Upload starten
|
||||
*
|
||||
* @param {String} uploadId
|
||||
*/
|
||||
startUpload: function (uploadId) {
|
||||
if (me.storage.uploads.hasOwnProperty(uploadId)) {
|
||||
var upload = me.storage.uploads[uploadId];
|
||||
me.startSingleUpload(upload);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} upload Einzelner Wert aus me.storage.waiting
|
||||
*/
|
||||
startSingleUpload: function (upload) {
|
||||
if (upload === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var fileId = upload.id;
|
||||
if (typeof fileId === 'undefined') {
|
||||
throw 'Upload fehlgeschlagen. Unique-ID is missing.';
|
||||
}
|
||||
|
||||
var $tableRow = $('#' + fileId);
|
||||
var $statusCell = $tableRow.find('.filestatus');
|
||||
$statusCell.html('Bitte warten...');
|
||||
upload.elements.$progressBar.val(0);
|
||||
|
||||
me.uploadFileChunk(upload, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} upload
|
||||
* @param {number} start
|
||||
*/
|
||||
uploadFileChunk: function (upload, start) {
|
||||
var chunkSize = me.options.chunkSize;
|
||||
|
||||
// ChunkSize kleiner 10KB macht keinen Sinn; Upload verhindern
|
||||
if (chunkSize < 10240) {
|
||||
me.displayUploadError(upload.id, 'ChunkSize ist zu gering (<= 10KB). Upload nicht möglich.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Im allerersten Upload die ChunkSize auf 100KB stellen
|
||||
// Server schickt in seiner Antwort das PHP-Upload-Limit mit
|
||||
if (start === 0) {
|
||||
chunkSize = 102400; // 102400 = 100KB
|
||||
}
|
||||
|
||||
var offset = start + chunkSize + 1;
|
||||
var chunkBlob = upload.file.slice(start, offset);
|
||||
|
||||
var fileId = upload.id;
|
||||
if (typeof fileId === 'undefined') {
|
||||
throw 'Upload fehlgeschlagen. Unique-ID is missing.';
|
||||
}
|
||||
|
||||
upload.status = STATUS.UPLOADING;
|
||||
upload.elements.$statusInfo.html('Hochladen...');
|
||||
upload.elements.$actionCell.html('');
|
||||
|
||||
// Datei-Inhalt fertig eingelesen => Upload starten
|
||||
upload.reader.onloadend = function (event) {
|
||||
if (event.target.readyState !== FileReader.DONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ajaxData = me.options.upload.formData;
|
||||
ajaxData.file_data = event.target.result;
|
||||
ajaxData.file_name = upload.file.name;
|
||||
ajaxData.file_type = upload.file.type;
|
||||
ajaxData.file_size = upload.file.size;
|
||||
ajaxData.file_id = upload.id;
|
||||
ajaxData.file_offset = start;
|
||||
|
||||
upload.xhr = $.ajax({
|
||||
url: me.options.upload.url,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
cache: false,
|
||||
data: ajaxData,
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
var errorMessage = 'Unbekannter Fehler #21: ' + errorThrown;
|
||||
|
||||
// User hat "Abbrechen" geklickt
|
||||
if (textStatus === 'abort') {
|
||||
errorMessage = 'Upload abgebrochen';
|
||||
}
|
||||
|
||||
// PHP-Skript hat Fehler geliefert (z.b. 404)
|
||||
if (textStatus === 'error') {
|
||||
errorMessage = 'Unbekannter Server-Fehler';
|
||||
}
|
||||
|
||||
// PHP-Skript liefer JSON-Error-Response
|
||||
if (jqXHR.hasOwnProperty('responseJSON') && jqXHR.responseJSON.hasOwnProperty('error')) {
|
||||
errorMessage = 'Server-Fehler: ' + jqXHR.responseJSON.error;
|
||||
}
|
||||
|
||||
upload.elements.$statusInfo.html('<strong>' + errorMessage + '</strong>');
|
||||
upload.elements.$progressBar.val(null);
|
||||
},
|
||||
success: function (data) {
|
||||
if (data.hasOwnProperty('success') && data.success === false) {
|
||||
upload.elements.$progressBar.val(null);
|
||||
upload.elements.$statusInfo.html('<strong>Server-Fehler: ' + data.error + '</strong>');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.hasOwnProperty('file') && !data.file.hasOwnProperty('bytes')) {
|
||||
alert('Fehlerhafte Antwort vom Server. #1');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.file.bytes === false) {
|
||||
alert('Fehlerhafte Antwort vom Server. #2');
|
||||
return;
|
||||
}
|
||||
|
||||
var bytesSend = data.file.bytes;
|
||||
var sizeDone = start + bytesSend;
|
||||
var sizeTotal = upload.file.size;
|
||||
var percentDone = Math.floor((sizeDone / sizeTotal) * 100);
|
||||
upload.elements.$progressBar.val(percentDone);
|
||||
|
||||
// Die erste Response vom Server enthält das PHP-Upload-Limit
|
||||
// => ChunkSize anpassen falls diese über dem PHP-Limit liegt
|
||||
if (data.hasOwnProperty('uploadLimit')
|
||||
&& typeof data.uploadLimit === 'number'
|
||||
&& data.uploadLimit > 0
|
||||
) {
|
||||
var uploadLimit = Math.floor(data.uploadLimit / 100 * 95); // 5% als Reserve freihalten
|
||||
var transferSize = me.calculateBase64SizeFromRawSize(me.options.chunkSize);
|
||||
if (uploadLimit < transferSize) {
|
||||
var maxChunkSize = me.calculateRawSizeFromBase64Size(uploadLimit);
|
||||
me.options.chunkSize = maxChunkSize;
|
||||
console.warn('ChunkedUpload: PHP upload limit is ' + data.uploadLimit + ' bytes.');
|
||||
console.warn('ChunkedUpload: Chunk size set to ' + maxChunkSize + ' bytes.');
|
||||
}
|
||||
}
|
||||
|
||||
if (offset < sizeTotal) {
|
||||
me.uploadFileChunk(upload, offset);
|
||||
} else {
|
||||
me.finishUpload(upload.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Datei einlesen starten
|
||||
upload.reader.readAsDataURL(chunkBlob);
|
||||
},
|
||||
|
||||
/**
|
||||
* Erfolgreichen Upload abschließen
|
||||
*
|
||||
* @param {String} uploadId
|
||||
*/
|
||||
finishUpload: function (uploadId) {
|
||||
if (me.storage.uploads.hasOwnProperty(uploadId)) {
|
||||
var upload = me.storage.uploads[uploadId];
|
||||
upload.elements.$statusInfo.html('Upload erfolgreich');
|
||||
upload.elements.$actionCell.html('');
|
||||
upload.status = STATUS.FINISHED;
|
||||
|
||||
// Callback aufrufen
|
||||
var fileInfo = new FileInfo(upload.id, upload.file.name, upload.file.type, upload.file.size);
|
||||
me.options.fileComplete(fileInfo);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload als fehlerhaft markieren
|
||||
*
|
||||
* @param {String} uploadId
|
||||
* @param {String} errorMessage
|
||||
*/
|
||||
displayUploadError: function (uploadId, errorMessage) {
|
||||
if (me.storage.uploads.hasOwnProperty(uploadId)) {
|
||||
var upload = me.storage.uploads[uploadId];
|
||||
upload.elements.$statusInfo.html('Upload-Fehler: ' + errorMessage);
|
||||
upload.elements.$actionCell.html('');
|
||||
upload.status = STATUS.FAILURE;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Datei-Tabelle erzeugen
|
||||
*/
|
||||
createFilesContainer: function () {
|
||||
var template = '<table>';
|
||||
if(me.options.upload.view === 'sidebar') {
|
||||
template +=
|
||||
'<thead><th></th><th></th>' +
|
||||
'<tbody></tbody>' +
|
||||
'</table>';
|
||||
}
|
||||
else {
|
||||
template +=
|
||||
'<thead><th align="left">Dateiname</th><th>Größe</th>' +
|
||||
'<th>Fortschritt</th><th>Status</th><th>Aktionen</th></tr></thead>' +
|
||||
'<tbody></tbody>' +
|
||||
'</table>';
|
||||
}
|
||||
var $list = $(template).hide();
|
||||
|
||||
var $filesContainer = $(me.options.filesContainer);
|
||||
if ($filesContainer.length === 0) {
|
||||
$filesContainer = $('<div>').insertAfter(me.storage.$fileInput);
|
||||
}
|
||||
|
||||
$filesContainer.append($list);
|
||||
$filesContainer.addClass('chunked-file-upload-container');
|
||||
|
||||
me.storage.$filesContainer = $filesContainer;
|
||||
me.storage.$filesList = $list.find('tbody');
|
||||
},
|
||||
|
||||
/**
|
||||
* HTML5 File-API vorhanden? Oder Uralt-Browser?
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isFileApiAvailable: function () {
|
||||
return typeof window.File !== 'undefined' &&
|
||||
typeof window.FileList !== 'undefined' &&
|
||||
typeof window.FileReader !== 'undefined';
|
||||
},
|
||||
|
||||
/**
|
||||
* Zufällige ID generieren
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
generateRandomId: function () {
|
||||
return 'chunked_upload_' + Math.floor(Math.random() * Math.floor(9999999999));
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string|number} value
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
formatBytes: function (value) {
|
||||
var bytes = parseInt(value, 10);
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes';
|
||||
}
|
||||
|
||||
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
var exponent = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
var decimalString = (bytes / Math.pow(1024, exponent)).toFixed(1) + '';
|
||||
|
||||
return decimalString.replace('.', ',') + ' ' + sizes[exponent];
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} rawSize Größe der Binärdaten (in Bytes)
|
||||
*
|
||||
* @return {number} Größe in Bytes wenn Base64-kodiert
|
||||
*/
|
||||
calculateBase64SizeFromRawSize: function (rawSize) {
|
||||
return Math.floor(rawSize / 3 * 4);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} encodedSize Größe eines Base64-kodierten Strings (in Bytes)
|
||||
*
|
||||
* @return {number} Größe in Bytes nach Base64-Dekodierung
|
||||
*/
|
||||
calculateRawSizeFromBase64Size: function (encodedSize) {
|
||||
return Math.floor(encodedSize / 4 * 3);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} id Unique id; example: 'chunked_upload_2377390993'
|
||||
* @param {string} name file name
|
||||
* @param {string} type Mime type
|
||||
* @param {number} size File size in bytes
|
||||
* @constructor
|
||||
*/
|
||||
var FileInfo = function (id, name, type, size) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.size = size;
|
||||
};
|
||||
|
||||
me.init($elem, options);
|
||||
|
||||
/**
|
||||
* Return public api
|
||||
*/
|
||||
return {};
|
||||
};
|
||||
|
||||
// Dokumentation: Siehe Dateianfang
|
||||
$.fn.chunkedUpload = function (options) {
|
||||
return this.each(function () {
|
||||
var $elem = $(this);
|
||||
|
||||
if (!$elem.data('chunkedUpload')) {
|
||||
var api = new ChunkedUpload(this, options);
|
||||
$elem.data('chunkedUpload', api);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
}(jQuery));
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Xentral\Widgets\ClickByClickAssistant;
|
||||
|
||||
|
||||
class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerJavascript()
|
||||
{
|
||||
return [
|
||||
'ClickByClickAssistant' => [
|
||||
'./classes/Widgets/ClickByClickAssistant/www/js/click_by_click_assistant.js',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerStylesheets()
|
||||
{
|
||||
return [
|
||||
'ClickByClickAssistant' => [
|
||||
'./classes/Widgets/ClickByClickAssistant/www/css/click_by_click_assistant.css',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Widgets\ClickByClickAssistant\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ComponentExceptionInterface;
|
||||
|
||||
interface ClickByClickAssistantExceptionInterface extends ComponentExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Widgets\ClickByClickAssistant\Exception;
|
||||
|
||||
use InvalidArgumentException as SplInvalidArgumentException;
|
||||
|
||||
class InvalidArgumentException extends SplInvalidArgumentException implements ClickByClickAssistantExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Widgets\ClickByClickAssistant;
|
||||
|
||||
use Xentral\Widgets\ClickByClickAssistant\Exception\InvalidArgumentException;
|
||||
|
||||
final class VueUtil
|
||||
{
|
||||
/**
|
||||
* @param array $array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function keyValueArrayToVueOptions($array): array
|
||||
{
|
||||
self::ensureScalarKeyValueArray($array);
|
||||
|
||||
$ret = [];
|
||||
foreach ($array as $value => $text) {
|
||||
$ret[] = ['value' => (string)$value, 'text' => (string)$text,];
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array[] $pageArray
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getInputNamesFromVuePages($pageArray): array
|
||||
{
|
||||
self::ensureArray($pageArray);
|
||||
|
||||
$ret = [];
|
||||
|
||||
foreach ($pageArray as $page) {
|
||||
if (empty($page['inputs'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self::ensureArray($page['inputs']);
|
||||
foreach ($page['inputs'] as $input) {
|
||||
if (isset($input['name'])) {
|
||||
$ret[] = $input['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function ensureArray($value): void
|
||||
{
|
||||
$type = gettype($value);
|
||||
if ($type !== 'array') {
|
||||
throw new InvalidArgumentException(sprintf('Wrong type "%s". Only "array" is allowed.', $type));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function ensureScalarKeyValueArray($value): void
|
||||
{
|
||||
self::ensureArray($value);
|
||||
|
||||
foreach ($value as $key => $val) {
|
||||
if ($val !== null && !is_scalar($val)) {
|
||||
$type = gettype($val);
|
||||
throw new InvalidArgumentException(sprintf('Wrong type "%s". Only scalar types ar allowed.', $type));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
:root {
|
||||
--main-icon-size: 65px;
|
||||
}
|
||||
|
||||
/**
|
||||
xentral corp colors:
|
||||
blueish #5a63ee
|
||||
pink #e66dcb
|
||||
green #29e7a1
|
||||
cyan #27e3e5
|
||||
**/
|
||||
|
||||
.click-by-click-assistant {
|
||||
position: fixed;
|
||||
z-index: 9998;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: table;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .header-icon {
|
||||
width: var(--main-icon-size);
|
||||
height: var(--main-icon-size);
|
||||
margin: 0 auto 12px auto;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .survey-icon {
|
||||
background: url('../themes/new/images/question-circle.svg');
|
||||
background-size: var(--main-icon-size);
|
||||
}
|
||||
.click-by-click-assistant .thanks-icon {
|
||||
background: url('../themes/new/images/thanks-smiley.svg');
|
||||
background-size: var(--main-icon-size);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .password-icon {
|
||||
background: url('../themes/new/images/password-icon.svg');
|
||||
background-size: var(--main-icon-size);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .add-person-icon {
|
||||
background: url('../themes/new/images/add-person-icon.svg');
|
||||
background-size: var(--main-icon-size);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .consultant {
|
||||
width: 107px;
|
||||
height: 107px;
|
||||
margin-top: -85px;
|
||||
border-radius: 100px;
|
||||
background: url('../themes/new/images/consultant.jpg') center;
|
||||
background-size: 120px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant h2,
|
||||
.click-by-click-assistant h3 {
|
||||
line-height: 22px;
|
||||
color: #25233A;
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .h2{
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .h3{
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.click-by-click-assistant p {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
opacity: 0.7;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 10px;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.click-by-click-assistant p.page-text{
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .wrapper {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .errorMsg {
|
||||
text-align: center;
|
||||
padding-top: 15px;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .container {
|
||||
width: 600px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-close-button {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: url('../themes/new/images/icon-close.svg') no-repeat center;
|
||||
background-size: 10px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .page {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .media-youtube {
|
||||
width: 100%;
|
||||
min-height: 337px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .media-image {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .page-content {
|
||||
padding: 35px 30px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .container .cta {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
padding: 10px 50px;
|
||||
margin-top: 20px;
|
||||
background-color: var(--button-primary-background);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .container .button.center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .flex-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .survey-button-container {
|
||||
box-sizing: border-box;
|
||||
margin: 10px 5px 0 5px;
|
||||
width: calc(1 / 3 * 100% - 10px);
|
||||
}
|
||||
|
||||
.click-by-click-assistant label.button-secondary {
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
height: 41px;
|
||||
line-height: 41px;
|
||||
padding: 0;
|
||||
color: var(--button-secondary-color);
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
box-shadow: 5px 5px 15px rgba(128, 128, 128, 0.05);
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.click-by-click-assistant label.button-secondary:hover {
|
||||
border: 1px solid var(--button-secondary-border-color);
|
||||
background-color: var(--button-secondary-background);
|
||||
color: var(--button-secondary-color);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked + label.button.button-secondary {
|
||||
background-color: var(--button-secondary-background);
|
||||
color: var(--button-secondary-color);
|
||||
}
|
||||
|
||||
.click-by-click-assistant input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.click-by-click-assistant input::-webkit-textfield-decoration-container {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .next-enter {
|
||||
opacity: 0;
|
||||
transform: translate3d(100px, 0, 0);
|
||||
}
|
||||
.click-by-click-assistant .next-enter-active,
|
||||
.click-by-click-assistant .next-leave-active {
|
||||
transition: 0.2s cubic-bezier(1.0, 0.5, 0.8, 1.0);
|
||||
}
|
||||
.click-by-click-assistant .next-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate3d(-100px, 0, 0);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .prev-enter {
|
||||
opacity: 0;
|
||||
transform: translate3d(-100px, 0, 0);
|
||||
}
|
||||
.click-by-click-assistant .prev-enter-active,
|
||||
.click-by-click-assistant .prev-leave-active {
|
||||
transition: 0.2s cubic-bezier(1.0, 0.5, 0.8, 1.0);
|
||||
}
|
||||
.click-by-click-assistant .prev-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate3d(100px, 0, 0);
|
||||
}
|
||||
|
||||
.click-by-click-assistant .fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .3s;
|
||||
}
|
||||
.click-by-click-assistant .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */
|
||||
{
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Input Styles
|
||||
*/
|
||||
|
||||
.click-by-click-assistant .app-row-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-row-container .add-row,
|
||||
.click-by-click-assistant .link {
|
||||
color: #87B668;
|
||||
text-decoration: underline;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
margin: 30px auto;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-row-container .add-row a,
|
||||
.click-by-click-assistant .link a {
|
||||
color: #87B668;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-row-container .add-row.inactive {
|
||||
cursor: auto;
|
||||
pointer-events: none;
|
||||
color: #b7b7be;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row.reduced-width {
|
||||
width: 60%;
|
||||
min-width: 200px;
|
||||
margin: 0 auto;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row .remove-row {
|
||||
cursor: pointer;
|
||||
align-self: flex-end;
|
||||
flex: 0 0 30px;
|
||||
height: 83px;
|
||||
background: url('../themes/new/images/remove-icon.svg') no-repeat;
|
||||
background-size: 15px 12px;
|
||||
background-position-x: 10px;
|
||||
background-position-y: 47px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row .app-row-valid {
|
||||
position: absolute;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
bottom: 23px;
|
||||
left: -17px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row .app-row-valid.icon-ok {
|
||||
background: url('../themes/new/images/icon-ok.svg') no-repeat center;
|
||||
background-size: 11px 8px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row .app-row-valid.checkbox {
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .container a {
|
||||
color: #929292;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
padding-right: 12px;
|
||||
box-sizing: border-box;
|
||||
margin: 35px auto 10px auto;
|
||||
transition: 0.2s ease width;
|
||||
-moz-transition: 0.2s ease width;
|
||||
-webkit-transition: 0.2s ease width;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input[type="checkbox"] {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input-row .app-input:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
|
||||
.click-by-click-assistant .app-input.input-error *:not(input) {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input.input-error input,
|
||||
.click-by-click-assistant .app-input.input-error select {
|
||||
border-bottom-color: #f44336;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input .input-error {
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
left: 5px;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input .reveal {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: 30px;
|
||||
height: 38px;
|
||||
background: url('../themes/new/images/icon-invisible.svg');
|
||||
background-size: 15px 12px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
opacity: .3;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input,
|
||||
.click-by-click-assistant .app-input select {
|
||||
width: 100%;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
color: #25233A;
|
||||
padding: 10px 10px 10px 5px;
|
||||
display: block;
|
||||
border: none;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
border-radius: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input:focus,
|
||||
.click-by-click-assistant .app-input select:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input label {
|
||||
color: #b7b7be;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
font-weight: normal;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: 5px;
|
||||
top: 10px;
|
||||
transition: 0.2s ease all;
|
||||
-moz-transition: 0.2s ease all;
|
||||
-webkit-transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input[type="checkbox"] ~ label {
|
||||
left: 10%;
|
||||
top: 0;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input.select:after {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
margin-top: -2.5px;
|
||||
pointer-events: none;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input:focus ~ label,
|
||||
.click-by-click-assistant .app-input input:valid ~ label,
|
||||
.click-by-click-assistant .app-input select.hasSelected ~ label,
|
||||
.click-by-click-assistant .app-input input.hasValue ~ label,
|
||||
.click-by-click-assistant .app-input select:valid ~ label {
|
||||
top: -15px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-input input[type="checkbox"]:focus ~ label,
|
||||
.click-by-click-assistant .app-input input[type="checkbox"]:valid ~ label,
|
||||
.click-by-click-assistant .app-input input[type="checkbox"].hasValue ~ label {
|
||||
top: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-pagination {
|
||||
text-align: center;
|
||||
margin: 20px 0 0 0;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-pagination div {
|
||||
display: inline-block;
|
||||
background-color: #d9d9d9;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 5px;
|
||||
margin: 0 2px;
|
||||
vertical-align: middle;
|
||||
transition: 0.2s ease all;
|
||||
-moz-transition: 0.2s ease all;
|
||||
-webkit-transition: 0.2s ease all;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .app-pagination div.active {
|
||||
background-color: #a0a0a0;
|
||||
}
|
||||
|
||||
.click-by-click-assistant .spinner {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
clear: both;
|
||||
right: 15px;
|
||||
top: 9px;
|
||||
}
|
||||
|
||||
/* Spinner Circle Rotation */
|
||||
.click-by-click-assistant .spinner-circle {
|
||||
border: 3px rgba(255, 255, 255, 0.25) solid;
|
||||
border-top: 3px rgba(255, 255, 255, 1) solid;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: spCircRot .6s infinite linear;
|
||||
animation: spCircRot .6s infinite linear;
|
||||
}
|
||||
@-webkit-keyframes spCircRot {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
@keyframes spCircRot {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
Vue.component('click-by-click-assistant', {
|
||||
props: ['pages', 'allowclose', 'pagination'],
|
||||
data: function(){
|
||||
return {
|
||||
activePage: 0,
|
||||
currentTransition:'',
|
||||
dataStorage: []
|
||||
}
|
||||
},
|
||||
template: '<div class="click-by-click-assistant"><div class="wrapper"><div class="container">' +
|
||||
'<div v-if="allowclose" class="app-close-button" @click="$emit(\'close\')"></div>' +
|
||||
|
||||
'<transition :name="currentTransition" mode="out-in">' +
|
||||
|
||||
/** DEFAULT TEXT PAGE **/
|
||||
'<div class="page" v-for="(page, index) in pages" ' +
|
||||
'v-if="page.type === \'defaultPage\' && activePage === index" ' +
|
||||
':data-pageIndex="index" ' +
|
||||
':key="index">' +
|
||||
'<app-media v-if="page.headerMedia" :media="page.headerMedia"></app-media>' +
|
||||
'<div class="page-content">' +
|
||||
'<div v-if="!page.headerMedia && page.icon" class="header-icon" :class="page.icon"></div>' +
|
||||
'<h2 v-if="page.headline" v-html="page.headline"></h2>'+
|
||||
'<h3 v-if="page.subHeadline" v-html="page.subHeadline"></h3>'+
|
||||
'<p class="page-text" v-if="page.text" v-html="page.text"></p>'+
|
||||
'<div class="flex-container" v-if="page.link">'+
|
||||
'<div v-if="page.link" class="link">'+
|
||||
'<a class="link" :href="page.link.link" >{{ page.link.title }}</a>'+
|
||||
'</div>' +
|
||||
'</div>'+
|
||||
'<button v-if="button.action === \'next\'" ' +
|
||||
'v-for="button in page.ctaButtons" ' +
|
||||
'class="button button-primary cta center" ' +
|
||||
'@click="changePage(\'next\')">{{ button.title }}</button>'+
|
||||
|
||||
'<button v-if="!button.link && button.action === \'close\'" ' +
|
||||
'v-for="button in page.ctaButtons" ' +
|
||||
'class="button button-primary cta center" ' +
|
||||
'@click="$emit(\'close\')">{{ button.title }}</button>'+
|
||||
|
||||
'<button v-if="!button.link && button.action === \'completeStep\'" ' +
|
||||
'v-for="button in page.ctaButtons" ' +
|
||||
'class="button button-primary cta center" ' +
|
||||
'@click="$emit(\'completeStep\')">{{ button.title }}</button>'+
|
||||
|
||||
'<button v-if="button.link && button.action === \'close\'" ' +
|
||||
'v-for="button in page.ctaButtons" ' +
|
||||
'class="button button-primary cta center" ' +
|
||||
'@click="link(button.link)">{{ button.title }}</button>'+
|
||||
|
||||
'<app-pagination v-if="pagination" :pages="pages" :index="index"></app-pagination>' +
|
||||
'</div>'+
|
||||
'</div>' +
|
||||
|
||||
/** FORM PAGE **/
|
||||
'<div class="page" v-for="(page, index) in pages" ' +
|
||||
'v-if="(page.type === \'form\' || page.type === \'survey\') && activePage === index" ' +
|
||||
':data-pageIndex="index" :key="index">' +
|
||||
'<app-media v-if="page.headerMedia" :media="page.headerMedia"></app-media>' +
|
||||
'<div class="page-content">' +
|
||||
'<div v-if="!page.headerMedia && page.icon" class="header-icon" :class="page.icon"></div>' +
|
||||
'<h2 v-html="page.headline"></h2>'+
|
||||
'<p v-if="page.subHeadline" v-html="page.subHeadline"></p>'+
|
||||
'<app-form :page="page"></app-form>' +
|
||||
'<app-pagination v-if="pagination" :pages="pages" :index="index"></app-pagination>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</transition>' +
|
||||
|
||||
'</div></div></div>',
|
||||
mounted: function(){
|
||||
var self = this;
|
||||
self.saveDataRequiredForSubmit();
|
||||
|
||||
self.$on('completeStep', function(){
|
||||
|
||||
if(WizardContainer !== undefined){
|
||||
WizardContainer.completeStep();
|
||||
}
|
||||
|
||||
self.$emit('close');
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @param {string} direction
|
||||
*/
|
||||
changePage: function(direction){
|
||||
if(direction !== 'back' && direction !== 'next'){
|
||||
return;
|
||||
}
|
||||
|
||||
this.activePage = direction === 'next' ? this.activePage + 1 : this.activePage - 1;
|
||||
this.currentTransition = direction;
|
||||
},
|
||||
link: function(link)
|
||||
{
|
||||
window.location.href = link;
|
||||
},
|
||||
|
||||
/**
|
||||
* saves all data that was defined on the building JSON file in order to submit it later
|
||||
*/
|
||||
saveDataRequiredForSubmit: function(){
|
||||
var current;
|
||||
|
||||
for(var i = 0; i < this.pages.length; i++){
|
||||
current = this.pages[i].dataRequiredForSubmit;
|
||||
|
||||
if(current === undefined || current.length === 0){
|
||||
return;
|
||||
}
|
||||
this.setToStorage(current);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} object
|
||||
*/
|
||||
setToStorage: function(object){
|
||||
this.dataStorage.push(object);
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getStorage: function(){
|
||||
return this.dataStorage;
|
||||
},
|
||||
|
||||
/**
|
||||
* clear storage, but keeps vue listener on this.dataStorage
|
||||
*/
|
||||
clearStorage: function(){
|
||||
for (var member in this.dataStorage) {
|
||||
delete this.dataStorage[member];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('app-form',{
|
||||
props: ['page'],
|
||||
data: function () {
|
||||
return {
|
||||
rowId: 0,
|
||||
surveyChoice: [],
|
||||
showSurveyError: false,
|
||||
formValid: true,
|
||||
formWasValidated: false,
|
||||
loading: false,
|
||||
errorMsg: 'Bitte überprüfe die Eingabefelder'
|
||||
}
|
||||
},
|
||||
template:
|
||||
'<form @submit.prevent="processForm" novalidate>' +
|
||||
'<div class="flex-container" v-for="(row, rowIndex) in page.form" :key="row.id">' +
|
||||
'<app-input-row v-if="row.inputs !== undefined && row.inputs.length > 0" ' +
|
||||
'ref="row" :row="row" ' +
|
||||
':hasSiblings="page.form.length > 1" ' +
|
||||
'@deleteme="removeInputRow(rowIndex)"></app-input-row>' +
|
||||
|
||||
'<span v-else-if="row.surveyButtons !== undefined && row.surveyButtons.length > 0" ' +
|
||||
'class="survey-button-container" ' +
|
||||
'v-for="button in row.surveyButtons">'+
|
||||
'<input type="checkbox" :id="button.value" name="data" :value="button.value" v-model="surveyChoice"/>'+
|
||||
'<label :for="button.value" class="button button-secondary" > {{ button.title }} </label>' +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'<div class="flex-container" v-if="page.link">'+
|
||||
'<div v-if="page.link" class="add-row">'+
|
||||
'<a class="link" :href="page.link.link" >{{ page.link.title }}</a>'+
|
||||
'</div>' +
|
||||
'</div>'+
|
||||
'<transition name="fade">' +
|
||||
'<div v-if="page.errorMsg && showSurveyError && surveyChoice.length === 0" ' +
|
||||
'class="errorMsg">{{ page.errorMsg }}</div>'+
|
||||
'<div v-if="formWasValidated && !formValid" class="errorMsg"> {{ errorMsg }}</div>'+
|
||||
'</transition>' +
|
||||
'<button v-for="button in page.ctaButtons" ' +
|
||||
':type="button.action" class="button button-primary cta center">{{ button.title }}' +
|
||||
'<app-spinner v-if="loading"></app-spinner></button>' +
|
||||
'</form>',
|
||||
methods:{
|
||||
/**
|
||||
* @param {Object} row
|
||||
*/
|
||||
addInputRow: function(row){
|
||||
this.rowId++
|
||||
row.id = this.rowId;
|
||||
|
||||
for(var k = 0; k < row.inputs.length; k++){
|
||||
row.inputs[k].name += this.rowId;
|
||||
}
|
||||
|
||||
this.page.form.push(row);
|
||||
|
||||
for(var i = 0; i < this.page.form.length -1; i++){
|
||||
this.page.form[i].add.allow = false;
|
||||
}
|
||||
|
||||
if(row.add.maximum === this.page.form.length){
|
||||
this.allowAddOnLastRow(false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
*/
|
||||
removeInputRow: function(index){
|
||||
if(this.page.form.length <= 1){
|
||||
return;
|
||||
}
|
||||
|
||||
this.page.form.splice(index, 1);
|
||||
|
||||
this.allowAddOnLastRow(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} decision
|
||||
*/
|
||||
allowAddOnLastRow: function(decision){
|
||||
var lastIndex = this.page.form.length - 1;
|
||||
this.page.form[lastIndex].add.allow = decision;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} e
|
||||
*/
|
||||
processForm: function(e){
|
||||
this.validateForm();
|
||||
|
||||
if(!this.formValid){
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.page.submitType){
|
||||
throw new Error("Please define submitType in your JSON");
|
||||
}
|
||||
|
||||
if(this.page.submitType === "save"){
|
||||
this.$parent.setToStorage(this.filterDataFromSubmitEvent(e));
|
||||
this.$parent.changePage("next");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitForm(e);
|
||||
},
|
||||
|
||||
validateForm: function(){
|
||||
if(this.page.submitType === 'survey'){
|
||||
this.formValid = this.surveyChoice.length !== 0;
|
||||
this.formWasValidated = true;
|
||||
|
||||
if(!this.formValid){
|
||||
this.showSurveyError = true;
|
||||
}
|
||||
} else {
|
||||
this.formValid = this.requiredRowsValid();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the required rows are valid
|
||||
* @returns {boolean}
|
||||
*/
|
||||
requiredRowsValid: function(){
|
||||
if(this.$refs === undefined){
|
||||
console.error("Please define ref on child component");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(this.$refs.row === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var current;
|
||||
|
||||
for(var i = 0; i < this.$refs.row.length; i++){
|
||||
current = this.$refs.row[i];
|
||||
|
||||
// case 1: if row has not been validated (no user input), form is valid in regard of this row
|
||||
if(!current.rowWasValidated){
|
||||
this.formWasValidated = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// case 2: if row is invalid, form is not valid
|
||||
// rowValid only includes required inputs (filtered out on row component)
|
||||
if(!current.rowValid){
|
||||
this.formWasValidated = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true; // if case1 or case2 didn't match, form valid
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Event} e
|
||||
*/
|
||||
submitForm: function(e){
|
||||
var request = new XMLHttpRequest(),
|
||||
self = this,
|
||||
data,
|
||||
responseJson;
|
||||
|
||||
data = this.prepareSubmitData(e);
|
||||
|
||||
request.open("POST", this.page.submitUrl + '', true);
|
||||
|
||||
request.addEventListener('load', function(event) {
|
||||
if (request.status >= 200 && request.status < 300) {
|
||||
console.log("POST " + request.statusText + " status: " + request.status);
|
||||
responseJson = JSON.parse(request.responseText);
|
||||
if(responseJson.page !== undefined) {
|
||||
self.$parent.pages.push(responseJson.page);
|
||||
}
|
||||
self.$parent.clearStorage();
|
||||
if(responseJson.dataRequiredForSubmit !== undefined){
|
||||
self.$parent.setToStorage(responseJson.dataRequiredForSubmit);
|
||||
}
|
||||
|
||||
self.$parent.changePage("next");
|
||||
|
||||
self.loading = false;
|
||||
} else {
|
||||
console.warn(request.statusText, request.responseText);
|
||||
|
||||
self.loading = false;
|
||||
self.formValid = false;
|
||||
self.formWasValidated = true;
|
||||
|
||||
responseJson = JSON.parse(request.responseText);
|
||||
|
||||
if(responseJson.error !== undefined) {
|
||||
self.errorMsg = responseJson.error;
|
||||
}
|
||||
else {
|
||||
self.errorMsg = 'Ooops, da ist etwas schief gelaufen. Bitte versuche es erneut.';
|
||||
}
|
||||
|
||||
if(responseJson.dataRequiredForSubmit !== undefined){
|
||||
self.$parent.setToStorage(responseJson.dataRequiredForSubmit);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.loading = true;
|
||||
request.send(data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Combines all available data of all not-submitted pages
|
||||
*
|
||||
* @param {Object} e
|
||||
*
|
||||
* @returns {FormData}
|
||||
*/
|
||||
prepareSubmitData: function(e){
|
||||
var submitData = new FormData(),
|
||||
filteredEventData,
|
||||
storageData;
|
||||
|
||||
filteredEventData = this.filterDataFromSubmitEvent(e);
|
||||
storageData = JSON.parse(JSON.stringify(this.$parent.getStorage()));
|
||||
if(storageData !== undefined && storageData.length > 0){
|
||||
for(var i = 0; i < storageData.length; i++){
|
||||
|
||||
for(var key in storageData[i]){
|
||||
submitData.append(key, storageData[i][key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(filteredEventData !== undefined){
|
||||
for(var filteredEventDataKey in filteredEventData){
|
||||
submitData.append(filteredEventDataKey, filteredEventData[filteredEventDataKey]);
|
||||
}
|
||||
}
|
||||
return submitData;
|
||||
},
|
||||
|
||||
/**
|
||||
* Serializes all data from a form submit event
|
||||
*
|
||||
* @param e
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
filterDataFromSubmitEvent: function(e){
|
||||
var data = {},
|
||||
checkedInSurvey = [],
|
||||
current;
|
||||
|
||||
for(var i = 0; i < e.target.length; i++){
|
||||
current = e.target[i];
|
||||
|
||||
if(current.tagName === "button" || current.tagName === "BUTTON"){
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!current.name){
|
||||
throw new Error("Please define names for all inputs");
|
||||
}
|
||||
|
||||
if(this.page.type === "survey" && (current.tagName === "input" || current.tagName === "INPUT")){
|
||||
if(current.checked){
|
||||
checkedInSurvey.push(current.value);
|
||||
}
|
||||
|
||||
data[current.name] = checkedInSurvey;
|
||||
} else {
|
||||
if(current.type === 'checkbox') {
|
||||
if(current.checked){
|
||||
data[current.name] = current.value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
data[current.name] = current.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('app-input-row',{
|
||||
props: ['row', 'hasSiblings'],
|
||||
data: function(){
|
||||
return {
|
||||
rowValid: true,
|
||||
rowWasValidated: false
|
||||
}
|
||||
},
|
||||
template:
|
||||
'<div class="app-row-container">' +
|
||||
'<div class="app-input-row" :class="{\'reduced-width\': row.inputs.length === 1 }">' +
|
||||
'<div class="app-row-valid" :class="{\'icon-ok\': rowValid && rowWasValidated}"></div>' +
|
||||
'<app-input ' +
|
||||
'v-for="(input, inputIndex) in row.inputs" ' +
|
||||
':type="input.type" ' +
|
||||
':validation="input.validation" ' +
|
||||
':customErrorMsg="input.customErrorMsg" ' +
|
||||
':name="input.name" ' +
|
||||
':label="input.label"' +
|
||||
':value="input.value"' +
|
||||
':connectedTo="input.connectedTo"' +
|
||||
':options="input.options"' +
|
||||
'ref="input"'+
|
||||
':key="inputIndex"></app-input>' +
|
||||
'<div v-if="row.removable && hasSiblings" @click="$emit(\'deleteme\')" class="remove-row"></div>' +
|
||||
'</div>' +
|
||||
'<div v-if="row.add && row.add.allow" @click="addRow" class="add-row">{{ row.add.text }}</div>' +
|
||||
'<div v-if="row.link" class="add-row"><a class="link" :href="row.link.link" >{{ row.link.title }}</a></div>' +
|
||||
'</div>',
|
||||
methods:{
|
||||
addRow: function(){
|
||||
var newRow = JSON.parse(JSON.stringify(this.row)); // removes vue observable and makes it possible to change
|
||||
|
||||
this.$parent.addInputRow(newRow);
|
||||
},
|
||||
|
||||
validateRow: function(){
|
||||
this.rowValid = this.requiredInputsValid();
|
||||
|
||||
this.rowWasValidated = true;
|
||||
|
||||
if(this.rowValid){
|
||||
this.$parent.validateForm();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the required Inputs are valid
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
requiredInputsValid: function(){
|
||||
if(this.$refs === undefined){
|
||||
throw new Error("Please define ref on child component");
|
||||
}
|
||||
|
||||
var valid = true,
|
||||
current;
|
||||
|
||||
for(var i = 0; i < this.$refs.input.length; i++){
|
||||
current = this.$refs.input[i];
|
||||
|
||||
if(!current.validation){
|
||||
valid = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
valid = current.validation && current.valid && current.wasValidated;
|
||||
|
||||
// row is invalid after first invalid input
|
||||
if(!valid){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('app-input', {
|
||||
props: ['type', 'validation', 'name', 'label', 'customErrorMsg', 'options', 'value', 'connectedTo'],
|
||||
data: function () {
|
||||
return {
|
||||
inputValue: this.value ? this.value : '',
|
||||
inputType: this.type,
|
||||
inputErrorMsg: undefined,
|
||||
valid: true,
|
||||
wasValidated: false
|
||||
}
|
||||
},
|
||||
template:
|
||||
'<div v-if="type === \'select\'" class="app-input select" :class="{\'input-error\': !valid }">' +
|
||||
'<select :id="name" :name="name" v-model="inputValue" :class="{\'hasSelected\': inputValue.length > 0 }" ' +
|
||||
'@change="validateInput" >' +
|
||||
'<option v-for="(option, index) in options" :value="option.value" :key="index">{{ option.text }}</option>' +
|
||||
'</select>' +
|
||||
'<label :for="name">{{ label }} <span v-if="validation"> (Pflichtfeld)</span></label>' +
|
||||
'</div>'+
|
||||
|
||||
'<div v-else class="app-input" :class="{\'input-error\': !valid}">' +
|
||||
//'<input style="display: none" type="password" />' +
|
||||
'<input :type="inputType" :id="name" :name="name" v-model="inputValue" ' +
|
||||
':class="{\'hasValue\': inputValue.length > 0 }" ' +
|
||||
'@blur="validateInput" autocomplete="off" required />' +
|
||||
'<div v-if="type === \'password\'" class="reveal" @click="togglePasswordVisibility"></div>' +
|
||||
'<label :for="name">{{ label }} <span v-if="validation"> (Pflichtfeld)</span></label>' +
|
||||
'<transition name="fade">' +
|
||||
'<div v-if="!valid && inputErrorMsg" class="input-error"> {{ inputErrorMsg }}</div>' +
|
||||
'</transition>'+
|
||||
'</div>',
|
||||
mounted: function(){
|
||||
var self = this;
|
||||
|
||||
// listens to compare request "broadcast" from other component
|
||||
self.$root.$on('compareConnected', function(data){
|
||||
if(self.name !== data.connectedTo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// "broadcasts" to every component listening
|
||||
self.$root.$emit('comparisonResult', {
|
||||
requestingInput: data.requestingInput.name,
|
||||
valid: self.inputValue === data.requestingInput.inputValue && self.valid
|
||||
})
|
||||
});
|
||||
|
||||
self.$root.$on('comparisonResult', function(result){
|
||||
if(self.name === result.requestingInput){
|
||||
self.valid = result.valid;
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
validateInput: function(){
|
||||
if((this.inputValue.length === 0 && !this.wasValidated) || !this.validation){
|
||||
// input is valid if it has a value and wasn't validated before (inputs do not get validated on page render)
|
||||
// or if it's not necessary to validate
|
||||
this.valid = true;
|
||||
return;
|
||||
}
|
||||
|
||||
switch(this.type){
|
||||
case "email":
|
||||
var regex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
this.valid = regex.test(this.inputValue);
|
||||
this.inputErrorMsg = this.customErrorMsg || "Adresse nicht gültig";
|
||||
break;
|
||||
|
||||
case "text":
|
||||
this.valid = this.inputValue.length >= 2;
|
||||
this.inputErrorMsg = this.customErrorMsg || "Mindestens zwei Zeichen";
|
||||
break;
|
||||
|
||||
case "password":
|
||||
|
||||
// "broadcasting" event to listening components
|
||||
// In this case we compare if passwords match in connected fields -> "connectedTo" option in JSON
|
||||
if(this.connectedTo !== undefined){
|
||||
this.$root.$emit('compareConnected', {
|
||||
connectedTo: this.connectedTo,
|
||||
requestingInput: {
|
||||
name: this.name,
|
||||
inputValue: this.inputValue
|
||||
}
|
||||
});
|
||||
this.inputErrorMsg = this.customErrorMsg || "Bitte wiederholen Sie das Passwort";
|
||||
|
||||
} else {
|
||||
this.valid = this.inputValue.length >= 4;
|
||||
this.inputErrorMsg = this.customErrorMsg || "Mindestens vier Zeichen";
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "select":
|
||||
// it's "selected/changed" (event) so it always has a valid value
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.wasValidated = true;
|
||||
|
||||
this.$parent.validateRow();
|
||||
},
|
||||
|
||||
togglePasswordVisibility: function(){
|
||||
this.inputType = this.inputType === 'password' ? 'text' : 'password';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('app-pagination', {
|
||||
props: ["pages", "index"],
|
||||
template: '' +
|
||||
'<div class="app-pagination">' +
|
||||
'<div v-for="(dot, dotIndex) in pages" :class="{\'active\': index === dotIndex}"></div>' +
|
||||
'</div>'
|
||||
});
|
||||
|
||||
Vue.component('app-spinner',{
|
||||
template: '<div class="spinner spinner-circle"></div>'
|
||||
});
|
||||
|
||||
Vue.component('app-media',{
|
||||
props: ["media"],
|
||||
template:
|
||||
'<div>' +
|
||||
'<iframe v-if="media.type === \'video\'"' +
|
||||
'class="media-youtube" ' +
|
||||
':src="media.link + \'?rel=0\'" ' +
|
||||
'frameborder="0" ' +
|
||||
'allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" ' +
|
||||
'allowfullscreen>' +
|
||||
'</iframe>' +
|
||||
|
||||
'<img ' +
|
||||
'v-if="media.type === \'image\'"' +
|
||||
'class="media-image"' +
|
||||
':src="media.link">' +
|
||||
'<img/>' +
|
||||
'</div>'
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable;
|
||||
|
||||
use Xentral\Core\DependencyInjection\ContainerInterface;
|
||||
use Xentral\Widgets\DataTable\Service\DataTableRequestHandler;
|
||||
use Xentral\Widgets\DataTable\Service\DataTableService;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices()
|
||||
{
|
||||
return [
|
||||
'DataTableService' => 'onInitDataTableService',
|
||||
'DataTableRequestHandler' => 'onInitDataTableRequestHandler',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @return DataTableService
|
||||
*/
|
||||
public static function onInitDataTableService(ContainerInterface $container)
|
||||
{
|
||||
$factory = new DataTableFactory($container);
|
||||
|
||||
return $factory->createDataTableService();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @return DataTableRequestHandler
|
||||
*/
|
||||
public static function onInitDataTableRequestHandler(ContainerInterface $container)
|
||||
{
|
||||
$factory = new DataTableFactory($container);
|
||||
|
||||
return $factory->createDataTableRequestHandler();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Column;
|
||||
|
||||
use JsonSerializable;
|
||||
use Xentral\Widgets\DataTable\Exception\InvalidArgumentException;
|
||||
use Xentral\Widgets\DataTable\Feature\ResponsiveFeature;
|
||||
|
||||
final class Column implements JsonSerializable
|
||||
{
|
||||
/** @var string ALIGN_LEFT */
|
||||
const ALIGN_LEFT = 'left';
|
||||
|
||||
/** @var string ALIGN_RIGHT */
|
||||
const ALIGN_RIGHT = 'right';
|
||||
|
||||
/** @var string ALIGN_CENTER */
|
||||
const ALIGN_CENTER = 'center';
|
||||
|
||||
/** @var string ALIGN_JUSTIFY */
|
||||
const ALIGN_JUSTIFY = 'justify';
|
||||
|
||||
/** @var array $validAlignments */
|
||||
public static $validAlignments = [
|
||||
self::ALIGN_LEFT,
|
||||
self::ALIGN_RIGHT,
|
||||
self::ALIGN_CENTER,
|
||||
self::ALIGN_JUSTIFY,
|
||||
];
|
||||
|
||||
/** @var string $name */
|
||||
private $name;
|
||||
|
||||
/** @var string $title */
|
||||
private $title;
|
||||
|
||||
/** @var bool $visible */
|
||||
private $visible;
|
||||
|
||||
/** @var bool $sortable */
|
||||
private $sortable;
|
||||
|
||||
/** @var bool $searchable */
|
||||
private $searchable;
|
||||
|
||||
/** @var bool $exportable */
|
||||
private $exportable;
|
||||
|
||||
/** @var bool $fixed */
|
||||
private $fixed;
|
||||
|
||||
/** @var string $alignment */
|
||||
private $alignment;
|
||||
|
||||
/** @var string|null $dbColumn */
|
||||
private $dbColumn;
|
||||
|
||||
/** @var string|null $width */
|
||||
private $width;
|
||||
|
||||
/** @var callable|null $formatter */
|
||||
private $formatter;
|
||||
|
||||
/** @var array $properties */
|
||||
private $properties = [];
|
||||
|
||||
/** @var array $cssClasses CSS classes */
|
||||
private $cssClasses = [];
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string $title
|
||||
* @param string $align [left|right|center|justify]
|
||||
* @param string|null $width Column width as CSS value (e.g 20%, 3em, 55px)
|
||||
* @param bool $visible Is column currently visible? Visibility can be changed at runtime
|
||||
* @param bool $sortable
|
||||
* @param bool $searchable
|
||||
* @param bool $exportable
|
||||
* @param bool $fixed If true, column is always visible and visibility can not be changed at runtime
|
||||
* * Fixed columns can't be hidden (ResponsiveFeature, ColumnVisibilityFeature)
|
||||
* * Fixed columns can't be reordered (ColumnReorderFeature)
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(
|
||||
$name,
|
||||
$title,
|
||||
$align = 'left',
|
||||
$width = null,
|
||||
$visible = true,
|
||||
$sortable = false,
|
||||
$searchable = false,
|
||||
$exportable = false,
|
||||
$fixed = false
|
||||
) {
|
||||
if (empty($name)) {
|
||||
throw new InvalidArgumentException('Column name can not be empty.');
|
||||
}
|
||||
$cleanedName = (string)preg_replace('#[^a-z0-9_]#', '', trim($name));
|
||||
if ($cleanedName !== $name) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Name "%s" contains illegal characters. Valid characters are: a-z, 0-9 and underscore.',
|
||||
$name
|
||||
));
|
||||
}
|
||||
|
||||
$this->name = (string)$name;
|
||||
$this->title = (string)$title;
|
||||
$this->visible = (bool)$visible;
|
||||
$this->sortable = (bool)$sortable;
|
||||
$this->searchable = (bool)$searchable;
|
||||
$this->exportable = (bool)$exportable;
|
||||
$this->fixed = (bool)$fixed;
|
||||
$this->alignment = (string)$align;
|
||||
$this->width = $width !== null ? (string)$width : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently hidden column; can be unhidden
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $title
|
||||
* @param string $align [left|right|center|justify]
|
||||
* @param string|null $width Column width as CSS value (e.g 20%, 3em, 55px)
|
||||
*
|
||||
* @return Column
|
||||
*/
|
||||
public static function hidden($name, $title, $align = 'left', $width = null)
|
||||
{
|
||||
return new static($name, $title, $align, $width, false, false, false, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible column; not sortable and not searchable
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $title
|
||||
* @param string $align [left|right|center|justify]
|
||||
* @param string|null $width Column width as CSS value (e.g 20%, 3em, 55px)
|
||||
*
|
||||
* @return Column
|
||||
*/
|
||||
public static function visible($name, $title, $align = 'left', $width = null)
|
||||
{
|
||||
return new static($name, $title, $align, $width, true, false, false, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible and sortable column; not searchable
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $title
|
||||
* @param string $align [left|right|center|justify]
|
||||
* @param string|null $width Column width as CSS value (e.g 20%, 3em, 55px)
|
||||
*
|
||||
* @return Column
|
||||
*/
|
||||
public static function sortable($name, $title, $align = 'left', $width = null)
|
||||
{
|
||||
return new static($name, $title, $align, $width, true, true, false, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible, sortable und searchable column
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $title
|
||||
* @param string $align [left|right|center|justify]
|
||||
* @param string|null $width Column width as CSS value (e.g 20%, 3em, 55px)
|
||||
*
|
||||
* @return Column
|
||||
*/
|
||||
public static function searchable($name, $title, $align = 'left', $width = null)
|
||||
{
|
||||
return new static($name, $title, $align, $width, true, true, true, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Always visible and with fixed position (for Menu and Selection columns)
|
||||
*
|
||||
* - Always visible; Can't be hidden (ColumnVisibilityFeature)
|
||||
* - Fixed position; Can't be reordered (ColumnReorderFeature)
|
||||
* - Not searchable
|
||||
* - Not sortable
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $title
|
||||
*
|
||||
* @return Column
|
||||
*/
|
||||
public static function fixed($name, $title = '', $align = 'center', $width = null)
|
||||
{
|
||||
$fixed = new static($name, $title, $align, $width, true, false, false, false, true);
|
||||
$fixed->set('responsivePriority', ResponsiveFeature::PRIO_HIGHER);
|
||||
|
||||
return $fixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dbColumn
|
||||
*/
|
||||
public function setDbColumn($dbColumn)
|
||||
{
|
||||
$this->dbColumn = (string)$dbColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDbColumn()
|
||||
{
|
||||
return $this->dbColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAlignment()
|
||||
{
|
||||
return $this->alignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see $validAlignments
|
||||
*
|
||||
* @param string $alignment [left|right|center|justify]
|
||||
*/
|
||||
public function setAlignment($alignment)
|
||||
{
|
||||
if (!in_array($alignment, self::$validAlignments, true)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Alignment "%s" is not valid. Valid alignments: %s',
|
||||
$alignment,
|
||||
implode(', ', self::$validAlignments)
|
||||
));
|
||||
}
|
||||
|
||||
$this->alignment = $alignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return callable|null
|
||||
*/
|
||||
public function getFormatter()
|
||||
{
|
||||
return $this->formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $formatter
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setFormatter(callable $formatter)
|
||||
{
|
||||
$this->formatter = $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isFixed()
|
||||
{
|
||||
return $this->fixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isVisible()
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSortable()
|
||||
{
|
||||
return $this->sortable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isExportable()
|
||||
{
|
||||
return $this->exportable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isSearchable()
|
||||
{
|
||||
return $this->searchable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($property)
|
||||
{
|
||||
if (isset($this->{$property})) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isset($this->properties[$property]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function get($property)
|
||||
{
|
||||
if (isset($this->{$property})) {
|
||||
return $this->{$property};
|
||||
}
|
||||
|
||||
if (isset($this->properties[$property])) {
|
||||
return $this->properties[$property];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($property, $value)
|
||||
{
|
||||
if (isset($this->{$property})) {
|
||||
$this->{$property} = $value;
|
||||
}
|
||||
|
||||
$this->properties[$property] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasCssClass($className)
|
||||
{
|
||||
return in_array($className, $this->cssClasses, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addCssClass($className)
|
||||
{
|
||||
$this->cssClasses[] = trim($className);
|
||||
$this->cssClasses = array_unique($this->cssClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeCssClass($className)
|
||||
{
|
||||
$classKey = array_search($className, $this->cssClasses, true);
|
||||
if ($classKey !== false) {
|
||||
unset($this->cssClasses[$classKey]);
|
||||
$this->cssClasses = array_values($this->cssClasses);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$result = $this->properties;
|
||||
$result['data'] = isset($result['data']) ? $result['data'] : $this->name;
|
||||
$result['name'] = $this->name;
|
||||
$result['title'] = $this->title;
|
||||
$result['exportable'] = $this->exportable;
|
||||
$result['searchable'] = $this->searchable;
|
||||
$result['orderable'] = $this->sortable;
|
||||
$result['visible'] = $this->visible;
|
||||
$result['fixed'] = $this->fixed;
|
||||
if ($this->fixed === true) {
|
||||
$result['visible'] = true;
|
||||
}
|
||||
|
||||
// Spalte hat keine Daten; z.B. Menü-Spalte
|
||||
if ($this->dbColumn === null) {
|
||||
//$result['data'] = null;
|
||||
$result['defaultContent'] = isset($result['defaultContent']) ? $result['defaultContent'] : '';
|
||||
$result['orderable'] = false;
|
||||
$result['searchable'] = false;
|
||||
// $result['data'] = $this->name;
|
||||
// $result['searchable'] = $this->searchable;
|
||||
}
|
||||
|
||||
$cssClasses = $this->cssClasses;
|
||||
$cssClasses[] = 'dt-' . $this->alignment;
|
||||
$result['className'] = implode(' ', $cssClasses);
|
||||
|
||||
if ($this->width !== null) {
|
||||
$result['width'] = $this->width;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Column;
|
||||
|
||||
use ArrayIterator;
|
||||
use IteratorAggregate;
|
||||
use JsonSerializable;
|
||||
use Traversable;
|
||||
use Xentral\Widgets\DataTable\Exception\ColumnNameAssignedException;
|
||||
use Xentral\Widgets\DataTable\Exception\InvalidArgumentException;
|
||||
|
||||
class ColumnCollection implements JsonSerializable, IteratorAggregate
|
||||
{
|
||||
/** @var array|Column[] $columns */
|
||||
protected $columns = [];
|
||||
|
||||
/**
|
||||
* @param array|Column[] $columns
|
||||
*/
|
||||
public function __construct(array $columns = [])
|
||||
{
|
||||
foreach ($columns as $column) {
|
||||
$this->add($column);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $columnName
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($columnName)
|
||||
{
|
||||
return $this->getByName($columnName) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Column $column
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add(Column $column)
|
||||
{
|
||||
$this->ensureUniqueColumnName($column->getName());
|
||||
$this->columns[] = $column;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Column $newColumn
|
||||
* @param string $columnNameBefore
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addAfter(Column $newColumn, $columnNameBefore)
|
||||
{
|
||||
$this->ensureUniqueColumnName($newColumn->getName());
|
||||
$offset = $this->getColumnIndexByName($columnNameBefore) + 1;
|
||||
|
||||
$columnsBefore = array_slice($this->columns, 0, $offset, false);
|
||||
$columnsAfter = array_slice($this->columns, $offset, null, false);
|
||||
|
||||
$this->columns = array_merge($columnsBefore, [$newColumn], $columnsAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Column $newColumn
|
||||
* @param string $columnNameAfter
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addBefore(Column $newColumn, $columnNameAfter)
|
||||
{
|
||||
$this->ensureUniqueColumnName($newColumn->getName());
|
||||
$offset = $this->getColumnIndexByName($columnNameAfter);
|
||||
|
||||
$columnsBefore = array_slice($this->columns, 0, $offset, false);
|
||||
$columnsAfter = array_slice($this->columns, $offset, null, false);
|
||||
|
||||
$this->columns = array_merge($columnsBefore, [$newColumn], $columnsAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $columnName
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function remove($columnName)
|
||||
{
|
||||
foreach ($this->columns as $index => $column) {
|
||||
if ($column->getName() !== $columnName) {
|
||||
unset($this->columns[$index]);
|
||||
$this->columns = array_values($this->columns);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return Column|null
|
||||
*/
|
||||
public function getByName($name)
|
||||
{
|
||||
foreach ($this->columns as $column) {
|
||||
if ($column->getName() === $name) {
|
||||
return $column;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $index
|
||||
*
|
||||
* @return Column|null
|
||||
*/
|
||||
public function getByIndex($index)
|
||||
{
|
||||
return isset($this->columns[(int)$index]) ? $this->columns[(int)$index] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $columnName
|
||||
*
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getColumnIndexByName($columnName)
|
||||
{
|
||||
$offset = false;
|
||||
$this->columns = array_values($this->columns);
|
||||
foreach ($this->columns as $index => $column) {
|
||||
if ($column->getName() === $columnName) {
|
||||
$offset = $index;
|
||||
}
|
||||
}
|
||||
|
||||
if ($offset === false) {
|
||||
throw new InvalidArgumentException(sprintf('Column name "%s" does not exists.', $columnName));
|
||||
}
|
||||
|
||||
return $offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Column[]
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getSearchableDbColumns()
|
||||
{
|
||||
$searchable = [];
|
||||
|
||||
foreach ($this->columns as $column) {
|
||||
if ($column->isSearchable()) {
|
||||
$searchable[] = $column->getDbColumn();
|
||||
}
|
||||
}
|
||||
|
||||
return $searchable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|callable[] Array with callables, indexed by column name;
|
||||
* Empty array if there aren't any formatters
|
||||
*/
|
||||
public function getFormatters()
|
||||
{
|
||||
$formatters = [];
|
||||
|
||||
foreach ($this->columns as $column) {
|
||||
$colName = $column->getName();
|
||||
$formatter = $column->getFormatter();
|
||||
if (!empty($colName) && $formatter !== null) {
|
||||
$formatters[$colName] = $formatter;
|
||||
}
|
||||
}
|
||||
|
||||
return $formatters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->columns as $column) {
|
||||
$result[] = $column->toArray();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayIterator|Traversable
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new ArrayIterator($this->columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep copy object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->columns as $index => $column) {
|
||||
$this->columns[$index] = clone $column;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $columnName
|
||||
*
|
||||
* @throws ColumnNameAssignedException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function ensureUniqueColumnName($columnName)
|
||||
{
|
||||
if ($this->has($columnName)) {
|
||||
throw new ColumnNameAssignedException(sprintf('Column name "%s" is already assigend.', $columnName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Column;
|
||||
|
||||
use Closure;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
|
||||
class ColumnFormatter
|
||||
{
|
||||
/**
|
||||
* @param mixed $ifEmpty
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
public static function ifEmpty($ifEmpty)
|
||||
{
|
||||
return static function ($value) use ($ifEmpty) {
|
||||
if (empty($value)) {
|
||||
return $ifEmpty;
|
||||
}
|
||||
|
||||
return $value;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @example Format::sprintf('row_id_%s') %s will be replaced with the current value
|
||||
*
|
||||
* @param mixed $sprintf
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
public static function sprintf($sprintf)
|
||||
{
|
||||
return static function ($value) use ($sprintf) {
|
||||
return sprintf($sprintf, $value);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $template
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
public static function template($template)
|
||||
{
|
||||
return static function ($value, $rowAssoc) use ($template) {
|
||||
$templateVars = [];
|
||||
foreach ($rowAssoc as $assocKey => $assocValue) {
|
||||
$templateVar = '{' . strtoupper($assocKey) . '}';
|
||||
$templateVar = str_replace('-', '_', $templateVar);
|
||||
$templateVars[$templateVar] = $assocValue;
|
||||
}
|
||||
|
||||
return strtr($template, $templateVars);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $decimals
|
||||
* @param string $decimalSeperator
|
||||
* @param string $thousandsSeperator
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
public static function number($decimals = 2, $decimalSeperator = ',', $thousandsSeperator = '.')
|
||||
{
|
||||
return static function ($value) use ($decimals, $decimalSeperator, $thousandsSeperator) {
|
||||
return number_format($value, $decimals, $decimalSeperator, $thousandsSeperator);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $decimals
|
||||
* @param string $decimalSeperator
|
||||
* @param string $thousandsSeperator
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
public static function bytes($decimals = 1, $decimalSeperator = ',', $thousandsSeperator = '.')
|
||||
{
|
||||
return static function ($bytes) use ($decimals, $decimalSeperator, $thousandsSeperator) {
|
||||
$bytes = (float)$bytes;
|
||||
$base = log($bytes, 1024);
|
||||
$suffixes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
$suffixIndex = (int)floor($base);
|
||||
$suffix = $suffixes[$suffixIndex];
|
||||
$number = pow(1024, $base - floor($base));
|
||||
|
||||
return number_format($number, $decimals, $decimalSeperator, $thousandsSeperator) . ' ' . $suffix;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dateFormat https://www.php.net/manual/de/function.date.php
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
public static function date($dateFormat)
|
||||
{
|
||||
return static function ($dateString) use ($dateFormat) {
|
||||
try {
|
||||
$date = new DateTime($dateString);
|
||||
|
||||
return $date->format($dateFormat);
|
||||
} catch (Exception $exception) {
|
||||
return $exception->getMessage();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Closure
|
||||
*/
|
||||
public static function htmlEscape()
|
||||
{
|
||||
return static function ($value) {
|
||||
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Fixen
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
public static function dump()
|
||||
{
|
||||
return static function ($value, $row) {
|
||||
$data = [
|
||||
'value' => $value,
|
||||
'row' => $row,
|
||||
];
|
||||
|
||||
return sprintf(
|
||||
'<pre class="dump">%s</pre>',
|
||||
htmlspecialchars(var_export($data, true))
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable;
|
||||
|
||||
use Xentral\Widgets\DataTable\Exception\InvalidArgumentException;
|
||||
use Xentral\Widgets\DataTable\Type\DataTableTypeInterface;
|
||||
|
||||
final class DataTableBuildConfig
|
||||
{
|
||||
/** @var string $tableName */
|
||||
private $tableName;
|
||||
|
||||
/** @var string $tableClass */
|
||||
private $tableClass;
|
||||
|
||||
/** @var string $ajaxUrl */
|
||||
private $ajaxUrl;
|
||||
|
||||
/** @var string $ajaxMethod */
|
||||
private $ajaxMethod;
|
||||
|
||||
/** @var array $ajaxParams Additional AJAX parameter */
|
||||
private $ajaxParams;
|
||||
|
||||
/** @var bool $autoInit */
|
||||
private $autoInit;
|
||||
|
||||
/**
|
||||
* Available DataTable classes: display, compact, hover, order-column, row-border, cell-border, stripe, nowrap
|
||||
*
|
||||
* display = Short-hand for stripe, hover, row-border and order-column.
|
||||
*
|
||||
* @see https://datatables.net/manual/styling/classes#Table-classes
|
||||
*
|
||||
* @var array $cssClasses
|
||||
*/
|
||||
private $cssClasses = [];
|
||||
|
||||
/**
|
||||
* @param string $tableName Unique table name; Will be used as id-attribute on <table> element
|
||||
* @param string $tableClass FQCN of DataTable class that implements DataTableTypeInterface
|
||||
* @param string $ajaxUrl
|
||||
* @param bool $autoInit
|
||||
*/
|
||||
public function __construct($tableName, $tableClass, $ajaxUrl, $autoInit = true)
|
||||
{
|
||||
if (!class_exists($tableClass, true)) {
|
||||
throw new InvalidArgumentException(sprintf('DataTable class "%s" not found', $tableClass));
|
||||
}
|
||||
$interfaces = class_implements($tableClass, true);
|
||||
if (!in_array(DataTableTypeInterface::class, $interfaces, true)) {
|
||||
throw new InvalidArgumentException('DataTable class does not implement %s', DataTableTypeInterface::class);
|
||||
}
|
||||
$tableNameCleaned = preg_replace('/[^a-z0-9_-]+/', '', $tableName);
|
||||
if ($tableNameCleaned !== $tableName) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Table name "%s" contains illegal characters. ' .
|
||||
'Valid characters are: a-z, 0-9, hyphens and underscores.',
|
||||
$tableName
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
$this->tableName = $tableName;
|
||||
$this->tableClass = $tableClass;
|
||||
$this->ajaxUrl = $ajaxUrl;
|
||||
$this->ajaxMethod = 'GET';
|
||||
$this->ajaxParams = ['tablename' => $tableName];
|
||||
$this->autoInit = $autoInit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTableName()
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTableClass()
|
||||
{
|
||||
return $this->tableClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAjaxUrl()
|
||||
{
|
||||
return $this->ajaxUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAjaxMethod()
|
||||
{
|
||||
return $this->ajaxMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method [GET|POST]
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setAjaxMethod($method)
|
||||
{
|
||||
$method = strtoupper($method);
|
||||
if (!in_array($method, ['GET', 'POST'], true)) {
|
||||
throw new InvalidArgumentException('AJAX method "%s" is invalid.', $method);
|
||||
}
|
||||
|
||||
$this->ajaxMethod = $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getAjaxParams()
|
||||
{
|
||||
return $this->ajaxParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $param
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setAjaxParam($param, $value)
|
||||
{
|
||||
$cleanedName = (string)preg_replace('#[^A-Za-z0-9_-]#', '', trim($param));
|
||||
if ($cleanedName !== $param) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'AJAX parameter name "%s" contains illegal characters. ' .
|
||||
'Valid characters are: a-z, 0-9, hyphens and underscores.',
|
||||
$param
|
||||
));
|
||||
}
|
||||
if ($param === 'tablename') {
|
||||
throw new InvalidArgumentException('AJAX parameter "tablename" is reserved.');
|
||||
}
|
||||
|
||||
$this->ajaxParams[$param] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isAutoInit()
|
||||
{
|
||||
return $this->autoInit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCssClassesString()
|
||||
{
|
||||
return implode(' ', $this->getCssClasses());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getCssClasses()
|
||||
{
|
||||
return $this->cssClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasCssClass($className)
|
||||
{
|
||||
return in_array($className, $this->cssClasses, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addCssClass($className)
|
||||
{
|
||||
$cleanedName = (string)preg_replace('#[^a-z0-9_-]#', '', trim($className));
|
||||
if ($cleanedName !== $className) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'CSS class name "%s" contains illegal characters. ' .
|
||||
'Valid characters are: a-z, 0-9, hyphens and underscores.',
|
||||
$className
|
||||
));
|
||||
}
|
||||
|
||||
$this->cssClasses[] = $cleanedName;
|
||||
$this->cssClasses = array_unique($this->cssClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeCssClass($className)
|
||||
{
|
||||
$classKey = array_search($className, $this->cssClasses, true);
|
||||
if ($classKey !== false) {
|
||||
unset($this->cssClasses[$classKey]);
|
||||
$this->cssClasses = array_values($this->cssClasses);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable;
|
||||
|
||||
use Xentral\Core\DependencyInjection\ContainerInterface;
|
||||
use Xentral\Widgets\DataTable\Request\DataTableRequest;
|
||||
use Xentral\Widgets\DataTable\Service\DataTableBuilder;
|
||||
use Xentral\Widgets\DataTable\Service\DataTableFetcher;
|
||||
use Xentral\Widgets\DataTable\Service\DataTableRenderer;
|
||||
use Xentral\Widgets\DataTable\Service\DataTableRequestHandler;
|
||||
use Xentral\Widgets\DataTable\Service\DataTableService;
|
||||
|
||||
final class DataTableFactory
|
||||
{
|
||||
/** @var ContainerInterface */
|
||||
private $container;
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
*/
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataTableRequestHandler
|
||||
*/
|
||||
public function createDataTableRequestHandler()
|
||||
{
|
||||
return new DataTableRequestHandler($this->createDataTableService(), $this->createDataTableRequest());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataTableService
|
||||
*/
|
||||
public function createDataTableService()
|
||||
{
|
||||
return new DataTableService(
|
||||
$this->createDataTableBuilder(),
|
||||
$this->createDataTableRenderer(),
|
||||
$this->createDataTableFetcher()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataTableBuilder
|
||||
*/
|
||||
private function createDataTableBuilder()
|
||||
{
|
||||
return new DataTableBuilder($this->container->get('Database'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataTableRenderer
|
||||
*/
|
||||
private function createDataTableRenderer()
|
||||
{
|
||||
return new DataTableRenderer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataTableFetcher
|
||||
*/
|
||||
private function createDataTableFetcher()
|
||||
{
|
||||
return new DataTableFetcher($this->container->get('Database'), $this->createDataTableRequest());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataTableRequest
|
||||
*/
|
||||
private function createDataTableRequest()
|
||||
{
|
||||
return DataTableRequest::fromRequest($this->container->get('Request'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable;
|
||||
|
||||
use Closure;
|
||||
use Xentral\Components\Database\SqlQuery\SelectQuery;
|
||||
use Xentral\Widgets\DataTable\Column\ColumnCollection;
|
||||
use Xentral\Widgets\DataTable\Options\DataTableOptions;
|
||||
use Xentral\Widgets\DataTable\Feature\FeatureCollection;
|
||||
use Xentral\Widgets\DataTable\Filter\FilterCollection;
|
||||
|
||||
interface DataTableInterface
|
||||
{
|
||||
/**
|
||||
* @return DataTableBuildConfig
|
||||
*/
|
||||
public function getConfig();
|
||||
|
||||
/**
|
||||
* @return DataTableOptions
|
||||
*/
|
||||
public function getOptions();
|
||||
|
||||
/**
|
||||
* @return ColumnCollection
|
||||
*/
|
||||
public function getColumns();
|
||||
|
||||
/**
|
||||
* @return FeatureCollection
|
||||
*/
|
||||
public function getFeatures();
|
||||
|
||||
/**
|
||||
* @return SelectQuery
|
||||
*/
|
||||
public function getBaseQuery();
|
||||
|
||||
/**
|
||||
* @return Closure|null
|
||||
*/
|
||||
public function getCustomSearch();
|
||||
|
||||
/**
|
||||
* @return FilterCollection
|
||||
*/
|
||||
public function getFilters();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class BuildFailedException extends RuntimeException implements DataTableExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Wenn bereits eine Column mit diesem Name existiert; Namen wüssen einmalig sein
|
||||
*/
|
||||
class ColumnNameAssignedException extends RuntimeException implements DataTableExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class ColumnNotFoundException extends RuntimeException implements DataTableExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class DataTableException extends RuntimeException implements DataTableExceptionInterface
|
||||
{
|
||||
/**
|
||||
* @param string $columnName
|
||||
*
|
||||
* @return DataTableException
|
||||
*/
|
||||
/*public static function columnNotFound($columnName)
|
||||
{
|
||||
return new self(sprintf('Column "%s" not found.', $columnName));
|
||||
}*/
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Exception;
|
||||
|
||||
use Xentral\Core\Exception\WidgetExceptionInterface;
|
||||
|
||||
interface DataTableExceptionInterface extends WidgetExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Exception;
|
||||
|
||||
class FeatureExistsException extends \RuntimeException implements DataTableExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
class FeatureIncompatibleException extends LogicException implements DataTableExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Exception;
|
||||
|
||||
class FeatureNotFoundException extends \InvalidArgumentException implements DataTableExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
class FeatureNotImplementedException extends LogicException implements DataTableExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Exception;
|
||||
|
||||
use InvalidArgumentException as SplInvalidArgumentException;
|
||||
|
||||
class InvalidArgumentException extends SplInvalidArgumentException implements DataTableExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\Options\DataTableOptions;
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Exception\FeatureNotImplementedException;
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*
|
||||
* @example https://datatables.net/reference/api/columns().footer()#Example
|
||||
*/
|
||||
final class ColumnAggregateFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/**
|
||||
* @throws FeatureNotImplementedException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
throw new FeatureNotImplementedException('Feature is not implemented yet.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
$this->modifyOptions($table->getOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableOptions $options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function modifyOptions(DataTableOptions $options)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\Column\Column;
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
|
||||
/**
|
||||
* @todo Fertigstellen
|
||||
*/
|
||||
final class ColumnFilterFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/** @var string TYPE_NONE Filter deactivated */
|
||||
const TYPE_NONE = 'none'; // Filter deactivated
|
||||
|
||||
/** @var string TYPE_TEXT Default filter */
|
||||
const TYPE_TEXT = 'text'; // Default
|
||||
|
||||
/** @var string TYPE_TEXT_MULTI @todo Mehrere Wörter mit ODER suchen */
|
||||
const TYPE_TEXT_MULTI = 'text_multi';
|
||||
|
||||
/** @var string TYPE_SELECT @todo Dropdown */
|
||||
const TYPE_SELECT = 'select';
|
||||
|
||||
/** @var string TYPE_NUMBER */
|
||||
const TYPE_NUMBER = 'number';
|
||||
|
||||
/** @var string TYPE_NUMBER_RANGE */
|
||||
const TYPE_NUMBER_RANGE = 'number_range';
|
||||
|
||||
/** @var string TYPE_DATE @todo */
|
||||
const TYPE_DATE = 'date';
|
||||
|
||||
/** @var string TYPE_DATE_RANGE @todo */
|
||||
const TYPE_DATE_RANGE = 'date_range';
|
||||
|
||||
/** @var array $columnSettings */
|
||||
private $columnSettings = [];
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
$this->modifyOptions($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $columnName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addNumberRangeFilter($columnName)
|
||||
{
|
||||
$this->columnSettings[$columnName] = [
|
||||
'name' => $columnName,
|
||||
'type' => self::TYPE_NUMBER_RANGE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*
|
||||
* @param string $columnName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addMultiWordFilter($columnName)
|
||||
{
|
||||
$this->columnSettings[$columnName] = [
|
||||
'name' => $columnName,
|
||||
'type' => self::TYPE_TEXT,
|
||||
'multi_word' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*
|
||||
* @param string $columnName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
// public function addDropdownFilter($columnName)
|
||||
// {
|
||||
// $this->columnSettings[$columnName] = [
|
||||
// 'name' => $columnName,
|
||||
// 'type' => self::TYPE_SELECT,
|
||||
// ];
|
||||
// }
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function modifyOptions(DataTableInterface $table)
|
||||
{
|
||||
$result = [];
|
||||
/** @var Column $column */
|
||||
foreach ($table->getColumns() as $index => $column) {
|
||||
$columnName = $column->getName();
|
||||
if (isset($this->columnSettings[$columnName])) {
|
||||
$result[$index] = $this->columnSettings[$columnName];
|
||||
} else {
|
||||
// Default
|
||||
$defaultType = $column->isSearchable() ? self::TYPE_TEXT : self::TYPE_NONE;
|
||||
$result[$index] = [
|
||||
'name' => $column->getName(),
|
||||
'type' => $defaultType,
|
||||
];
|
||||
}
|
||||
if ($columnName === 'id' || $columnName === 'menu') {
|
||||
$result[$index] = [
|
||||
'name' => $column->getName(),
|
||||
'type' => self::TYPE_NONE, // Column filtering inactive on menu and id column
|
||||
];
|
||||
}
|
||||
if (strpos($columnName, '_') === 0) {
|
||||
$result[$index] = [
|
||||
'name' => $column->getName(),
|
||||
'type' => self::TYPE_NONE, // @todo Wird benötigt?
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$table->getOptions()->setOption('columnFilter', $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\Options\DataTableOptions;
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Exception\FeatureIncompatibleException;
|
||||
|
||||
/**
|
||||
* @deprecated Nicht verwenden; Momentan inkompatibel mit ColumnFilter! Filter-Eingabefelder werden falsch zugeordnet.
|
||||
*
|
||||
* @see https://datatables.net/extensions/colreorder/
|
||||
*/
|
||||
final class ColumnReorderFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/**
|
||||
* @throws FeatureIncompatibleException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
throw new FeatureIncompatibleException('DataTable feature "ColumnReorder" is incompatible.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
$this->modifyOptions($table->getOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableOptions $options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function modifyOptions(DataTableOptions $options)
|
||||
{
|
||||
/** @see https://datatables.net/reference/option/colReorder */
|
||||
$options->setOption('colReorder', ['enable' => true, 'realtime' => false]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Exception\DataTableExceptionInterface;
|
||||
|
||||
interface DataTableFeatureInterface
|
||||
{
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @throws DataTableExceptionInterface
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
|
||||
final class DebugFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/** @var bool $enabled */
|
||||
private $enabled;
|
||||
|
||||
/**
|
||||
* @param bool $enabled
|
||||
*/
|
||||
public function __construct($enabled = true)
|
||||
{
|
||||
$this->enabled = (bool)$enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
if ($this->enabled === true) {
|
||||
$table->getConfig()->addCssClass('datatable-debug');
|
||||
} else {
|
||||
$table->getConfig()->removeCssClass('datatable-debug');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled()
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function enable()
|
||||
{
|
||||
$this->enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disable()
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use ArrayIterator;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
use Xentral\Widgets\DataTable\Exception\FeatureExistsException;
|
||||
use Xentral\Widgets\DataTable\Exception\FeatureNotFoundException;
|
||||
use Xentral\Widgets\DataTable\Exception\InvalidArgumentException;
|
||||
|
||||
class FeatureCollection implements IteratorAggregate
|
||||
{
|
||||
/** @var array $features */
|
||||
protected $features = [];
|
||||
|
||||
/**
|
||||
* @param array|DataTableFeatureInterface[] $features
|
||||
*/
|
||||
public function __construct(array $features = [])
|
||||
{
|
||||
foreach ($features as $feature) {
|
||||
$this->add($feature);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className Full-qualified class name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($className)
|
||||
{
|
||||
$this->ensureClassNameParameter($className, __METHOD__);
|
||||
|
||||
foreach ($this->features as $feature) {
|
||||
if (get_class($feature) === $className) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className Full-qualified class name
|
||||
*
|
||||
* @throws FeatureNotFoundException
|
||||
*
|
||||
* @return DataTableFeatureInterface
|
||||
*/
|
||||
public function get($className)
|
||||
{
|
||||
$this->ensureClassNameParameter($className, __METHOD__);
|
||||
|
||||
foreach ($this->features as $feature) {
|
||||
if (get_class($feature) === $className) {
|
||||
return $feature;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FeatureNotFoundException(sprintf('Feature class "%s" not found.', $className));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|DataTableFeatureInterface[]
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return $this->features;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new feature
|
||||
*
|
||||
* @param DataTableFeatureInterface $feature
|
||||
*
|
||||
* @throws FeatureExistsException If feature with same type already exists
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add(DataTableFeatureInterface $feature)
|
||||
{
|
||||
if ($this->has(get_class($feature))) {
|
||||
throw new FeatureExistsException(sprintf('Feature class "%s" already exists', get_class($feature)));
|
||||
}
|
||||
|
||||
$this->features[] = $feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a feature; If feature with same type exists, it will be overwritten.
|
||||
*
|
||||
* @param DataTableFeatureInterface $feature
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set(DataTableFeatureInterface $feature)
|
||||
{
|
||||
$this->remove(get_class($feature));
|
||||
$this->features[] = $feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|object $className Full-qualified class name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function remove($className)
|
||||
{
|
||||
$this->ensureClassNameParameter($className, __METHOD__);
|
||||
|
||||
foreach ($this->features as $index => $feature) {
|
||||
if (get_class($feature) === $className) {
|
||||
unset($this->features[$index]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function removeAll()
|
||||
{
|
||||
$this->features = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayIterator|Traversable
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new ArrayIterator($this->features);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep copy object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->features as $index => $column) {
|
||||
$this->features[$index] = clone $column;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $className
|
||||
* @param string $callerName
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function ensureClassNameParameter($className, $callerName)
|
||||
{
|
||||
if (!is_string($className)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Parameter "className" in method "%s" has to be a class name.', $callerName
|
||||
));
|
||||
}
|
||||
|
||||
if (!class_exists($className, true)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'"%s" is not a valid class.', $className
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Options\DataTableOptions;
|
||||
use Xentral\Widgets\DataTable\Exception\FeatureIncompatibleException;
|
||||
|
||||
/**
|
||||
* @see https://datatables.net/extensions/fixedheader/
|
||||
*/
|
||||
final class FixedHeaderFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/**
|
||||
* @throws FeatureIncompatibleException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
throw new FeatureIncompatibleException('Feature "FixedHeaderFeature" does not work currently.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
$this->modifyOptions($table->getOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableOptions $options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function modifyOptions(DataTableOptions $options)
|
||||
{
|
||||
/** @see https://datatables.net/reference/option/fixedHeader */
|
||||
$options->setOption('fixedHeader', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\Column\Column;
|
||||
use Xentral\Widgets\DataTable\Column\ColumnCollection;
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Options\DataTableOptions;
|
||||
|
||||
/**
|
||||
* @see https://datatables.net/extensions/responsive/
|
||||
*/
|
||||
final class ResponsiveFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/** @var int PRIO_HIGHEST */
|
||||
const PRIO_HIGHEST = 1;
|
||||
|
||||
/** @var int PRIO_HIGHER */
|
||||
const PRIO_HIGHER = 10;
|
||||
|
||||
/** @var int PRIO_NORMAL */
|
||||
const PRIO_NORMAL = 100;
|
||||
|
||||
/** @var int PRIO_LOWER */
|
||||
const PRIO_LOWER = 1000;
|
||||
|
||||
/** @var int PRIO_LOWEST */
|
||||
const PRIO_LOWEST = 10000;
|
||||
|
||||
/** @var array $responsiveProperty */
|
||||
private $responsiveProperty = [
|
||||
'details' => false,
|
||||
];
|
||||
|
||||
/** @var array $columnPriorities */
|
||||
private $columnPriorities = [];
|
||||
|
||||
/** @var int $defaultPriority */
|
||||
private $defaultPriority = self::PRIO_NORMAL;
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
$this->modifyOptions($table->getOptions());
|
||||
$this->modifyColumns($table->getColumns());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $columnName
|
||||
* @param int $priority
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPriority($columnName, $priority)
|
||||
{
|
||||
$this->columnPriorities[$columnName] = (int)$priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $priority
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setDefaultPriority($priority)
|
||||
{
|
||||
$this->defaultPriority = (int)$priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableOptions $options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function modifyOptions(DataTableOptions $options)
|
||||
{
|
||||
$options->setOption('responsive', $this->responsiveProperty);
|
||||
$options->removeOption('scrollX');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ColumnCollection $columns
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function modifyColumns(ColumnCollection $columns)
|
||||
{
|
||||
/** @var Column $column */
|
||||
foreach ($columns as $column) {
|
||||
$name = $column->getName();
|
||||
if (isset($this->columnPriorities[$name])) {
|
||||
$column->set('responsivePriority', $this->columnPriorities[$name]);
|
||||
} else {
|
||||
if (!$column->has('responsivePriority')) {
|
||||
$column->set('responsivePriority', $this->defaultPriority);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Closure;
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
|
||||
final class RowClassesFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/** @var array $colors */
|
||||
private static $availableColors = [
|
||||
'lightgray',
|
||||
'lightgreen',
|
||||
'lightteal',
|
||||
'lightcyan',
|
||||
'lightblue',
|
||||
'lightindigo',
|
||||
'lightviolet',
|
||||
'lightfuchsia',
|
||||
'lightpink',
|
||||
'lightred',
|
||||
'lightorange',
|
||||
'lightyellow',
|
||||
'lightlime',
|
||||
];
|
||||
|
||||
/** @var array|string[] $classes */
|
||||
private $classes;
|
||||
|
||||
/** @var array|Closure[] $customFormatter */
|
||||
private $customFormatter = [];
|
||||
|
||||
/**
|
||||
* @param array|string[] $classes
|
||||
* @param array|Closure $customFormatter
|
||||
*/
|
||||
public function __construct(array $classes = [], array $customFormatter = [])
|
||||
{
|
||||
foreach ($classes as $class) {
|
||||
$this->addClass($class);
|
||||
}
|
||||
foreach ($customFormatter as $formatter) {
|
||||
$this->addCustomFormatter($formatter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClass($className)
|
||||
{
|
||||
$this->classes[] = trim($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|string[]
|
||||
*/
|
||||
public function getClasses()
|
||||
{
|
||||
return $this->classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getClassesString()
|
||||
{
|
||||
return implode(' ', $this->classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasCustomFormatter()
|
||||
{
|
||||
return !empty($this->customFormatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Closure[]
|
||||
*/
|
||||
public function getCustomFormatter()
|
||||
{
|
||||
return $this->customFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure $closure
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addCustomFormatter(Closure $closure)
|
||||
{
|
||||
$this->customFormatter[] = $closure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
// @todo Logik steckt momentan in DataTableRenderer; muss aber hier rein
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRandomColor()
|
||||
{
|
||||
$count = count(self::$availableColors);
|
||||
$index = mt_rand() % $count;
|
||||
|
||||
return self::$availableColors[$index];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\Column\Column;
|
||||
use Xentral\Widgets\DataTable\Column\ColumnFormatter;
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Exception\DataTableExceptionInterface;
|
||||
use Xentral\Widgets\DataTable\Exception\InvalidArgumentException;
|
||||
|
||||
final class RowDetailsFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/** @var string $ajaxUrl */
|
||||
private $ajaxUrl;
|
||||
|
||||
/** @var string $ajaxMethod */
|
||||
private $ajaxMethod;
|
||||
|
||||
/** @var array $ajaxParams @todo Additional AJAX parameter */
|
||||
private $ajaxParams = [];
|
||||
|
||||
/**
|
||||
* Der Wert aus der id-Spalte wird als POST-Parameter `id` übergeben
|
||||
*
|
||||
* @param string $ajaxUrl `./index.php?module=foo&action=bar`
|
||||
* @param string $ajaxMethod [GET|POST]
|
||||
* @param callable|null $customFormatter @todo
|
||||
*/
|
||||
public function __construct($ajaxUrl, $ajaxMethod = 'POST', $customFormatter = null)
|
||||
{
|
||||
$ajaxMethod = strtoupper($ajaxMethod);
|
||||
if (!in_array($ajaxMethod, ['GET', 'POST'])) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid method "%s".', $ajaxMethod));
|
||||
}
|
||||
|
||||
$this->ajaxUrl = $ajaxUrl;
|
||||
$this->ajaxMethod = $ajaxMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @throws DataTableExceptionInterface
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
$table->getOptions()->setOption('rowDetails', [
|
||||
'ajax' => [
|
||||
'url' => $this->ajaxUrl,
|
||||
'method' => $this->ajaxMethod,
|
||||
'data' => $this->ajaxParams,
|
||||
],
|
||||
]);
|
||||
|
||||
// Detail-Spalte erzeugen
|
||||
$newCol = Column::fixed('details', '', 'center', '20px');
|
||||
$newCol->setFormatter(ColumnFormatter::template('<span class="details" data-id="{ID}"></span>'));
|
||||
$newCol->addCssClass('dt-details');
|
||||
|
||||
// Detail-Spalte vor erste Spalte einfügen
|
||||
/** @var Column $firstCol */
|
||||
$firstCol = $table->getColumns()->getByIndex(0);
|
||||
$table->getColumns()->addBefore($newCol, $firstCol->getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Exception\ColumnNotFoundException;
|
||||
use Xentral\Widgets\DataTable\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @see https://datatables.net/extensions/rowgroup/
|
||||
*/
|
||||
final class RowGroupFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/** @var array $groupColumns */
|
||||
private $groupColumns;
|
||||
|
||||
/** @var bool $enabled */
|
||||
private $enabled;
|
||||
|
||||
/**
|
||||
* @param array $columnNames
|
||||
*/
|
||||
public function __construct(array $columnNames)
|
||||
{
|
||||
if (count($columnNames) === 0) {
|
||||
throw new InvalidArgumentException('Parameter "columnNames" is can not be empty.');
|
||||
}
|
||||
$this->groupColumns = $columnNames;
|
||||
$this->enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @throws ColumnNotFoundException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
foreach ($this->groupColumns as $columnName) {
|
||||
if (!$table->getColumns()->has($columnName)) {
|
||||
throw new ColumnNotFoundException(sprintf(
|
||||
'RowGroupFeature failed. Column "%s" not found.',
|
||||
$columnName
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($this->enabled === true) {
|
||||
$table->getOptions()->setOption('rowGroup', ['dataSrc' => $this->groupColumns]);
|
||||
}
|
||||
if ($this->enabled === false) {
|
||||
$table->getOptions()->setOption('rowGroup', false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $columnNames
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function groupBy(array $columnNames)
|
||||
{
|
||||
if (count($columnNames) === 0) {
|
||||
throw new InvalidArgumentException('Parameter "columnNames" is can not be empty.');
|
||||
}
|
||||
$this->groupColumns = $columnNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function enable()
|
||||
{
|
||||
$this->enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disable()
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
|
||||
final class StateSaveFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/** @var bool $enabled */
|
||||
private $enabled;
|
||||
|
||||
/** @var int $duration In seconds (0 = Forever) */
|
||||
private $duration;
|
||||
|
||||
/**
|
||||
* @param bool $enabled
|
||||
* @param int $duration
|
||||
*/
|
||||
public function __construct($enabled = true, $duration = 0)
|
||||
{
|
||||
$this->enabled = (bool)$enabled;
|
||||
$this->duration = (int)$duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
// $options = $table->getOptions()->toArray();
|
||||
// $options['columns'] = $table->getColumns()->toArray();
|
||||
// $table->getOptions()->setOption('revision', md5(json_encode($options)));
|
||||
|
||||
$table->getOptions()->setOption('stateSave', $this->enabled);
|
||||
$table->getOptions()->setOption('stateDuration', $this->duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function enable()
|
||||
{
|
||||
$this->enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disable()
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled()
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDuration()
|
||||
{
|
||||
return $this->duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $duration
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setDuration($duration)
|
||||
{
|
||||
$this->duration = (int)$duration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
|
||||
final class TableControlFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/** @var array $buttonConfigCopy */
|
||||
private static $buttonConfigCopy = [
|
||||
'extend' => 'copy',
|
||||
'text' => 'Zwischenablage',
|
||||
];
|
||||
|
||||
/** @var array $buttonConfigCsv */
|
||||
private static $buttonConfigCsv = [
|
||||
'extend' => 'collection',
|
||||
'text' => 'CSV',
|
||||
'collectionTitle' => 'CSV-Export',
|
||||
'autoClose' => true,
|
||||
'buttons' => [
|
||||
[
|
||||
'text' => 'Alle Seiten',
|
||||
'action' => 'export-csv-all',
|
||||
],
|
||||
[
|
||||
'text' => 'Aktuelle Seite',
|
||||
'action' => 'export-csv-page',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/** @var array $buttonConfigExcel */
|
||||
private static $buttonConfigExcel = [
|
||||
'extend' => 'excel',
|
||||
'text' => 'Excel',
|
||||
];
|
||||
|
||||
/** @var array $buttonConfigPdf */
|
||||
private static $buttonConfigPdf = [
|
||||
'extend' => 'pdf',
|
||||
'text' => 'PDF',
|
||||
'orientation' => 'landscape',
|
||||
'pageSize' => 'A4',
|
||||
];
|
||||
|
||||
/** @var array $buttonConfigPrint */
|
||||
private static $buttonConfigPrint = [
|
||||
'extend' => 'print',
|
||||
'text' => 'Drucken',
|
||||
];
|
||||
|
||||
/** @var bool $info */
|
||||
private $info = true;
|
||||
|
||||
/** @var bool $paging */
|
||||
private $paging = true;
|
||||
|
||||
/** @var bool $searching */
|
||||
private $searching = true;
|
||||
|
||||
/** @var bool $lengthChange */
|
||||
private $lengthChange = true;
|
||||
|
||||
/** @var int|null $pageLength */
|
||||
private $pageLength;
|
||||
|
||||
/** @var bool $processing */
|
||||
private $processing = true;
|
||||
|
||||
/** @var bool $sorting */
|
||||
private $sorting = true;
|
||||
|
||||
/** @var array $buttons */
|
||||
private $buttons = [];
|
||||
|
||||
/**
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->setFullMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
$table->getOptions()->setOption('info', $this->info);
|
||||
$table->getOptions()->setOption('paging', $this->paging);
|
||||
$table->getOptions()->setOption('buttons', $this->buttons);
|
||||
$table->getOptions()->setOption('searching', $this->searching);
|
||||
$table->getOptions()->setOption('lengthChange', $this->lengthChange);
|
||||
$table->getOptions()->setOption('processing', $this->processing);
|
||||
$table->getOptions()->setOption('ordering', $this->sorting);
|
||||
if ($this->pageLength !== null && $this->pageLength > 0) {
|
||||
$table->getOptions()->setOption('pageLength', $this->pageLength);
|
||||
$table->getOptions()->setOption('lengthChange', false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setFullMode()
|
||||
{
|
||||
$this->showInfo();
|
||||
$this->showButtons();
|
||||
$this->showLengthChange();
|
||||
$this->enableSearching();
|
||||
$this->enableSorting();
|
||||
$this->enablePaging();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setMinimalMode()
|
||||
{
|
||||
$this->showInfo();
|
||||
$this->enablePaging();
|
||||
$this->enableSorting();
|
||||
|
||||
$this->hideButtons();
|
||||
$this->hideLengthChange();
|
||||
$this->disableSearching();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function showInfo()
|
||||
{
|
||||
$this->info = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function hideInfo()
|
||||
{
|
||||
$this->info = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function showButtons()
|
||||
{
|
||||
$this->buttons = [
|
||||
'buttons' => [
|
||||
self::$buttonConfigCopy,
|
||||
self::$buttonConfigCsv,
|
||||
self::$buttonConfigExcel,
|
||||
self::$buttonConfigPdf,
|
||||
self::$buttonConfigPrint,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function hideButtons()
|
||||
{
|
||||
$this->buttons = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function showLengthChange()
|
||||
{
|
||||
$this->lengthChange = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function hideLengthChange()
|
||||
{
|
||||
$this->lengthChange = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $rowsPerPage
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPageLength($rowsPerPage)
|
||||
{
|
||||
$this->pageLength = (int)$rowsPerPage;
|
||||
$this->hideLengthChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function showProcessingIndicator()
|
||||
{
|
||||
$this->processing = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function hideProcessingIndicator()
|
||||
{
|
||||
$this->processing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function enableSearching()
|
||||
{
|
||||
// @todo ColumnFilter aktivieren
|
||||
$this->searching = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disableSearching()
|
||||
{
|
||||
// @todo ColumnFilter deaktivieren
|
||||
$this->searching = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function enableSorting()
|
||||
{
|
||||
$this->sorting = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disableSorting()
|
||||
{
|
||||
$this->sorting = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function enablePaging()
|
||||
{
|
||||
$this->paging = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disablePaging()
|
||||
{
|
||||
$this->paging = false;
|
||||
$this->lengthChange = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Feature;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
|
||||
final class TableStylingFeature implements DataTableFeatureInterface
|
||||
{
|
||||
/** @var array $cssClasses */
|
||||
private $cssClasses = [];
|
||||
|
||||
/**
|
||||
* @param bool $compact
|
||||
* @param bool $disableLineWrapping
|
||||
*/
|
||||
public function __construct($compact = false, $disableLineWrapping = false)
|
||||
{
|
||||
$this->setDefaultStyle();
|
||||
if ($compact === true) {
|
||||
$this->setCompactStyle();
|
||||
}
|
||||
if ($disableLineWrapping === true) {
|
||||
$this->disableLineWrapping();
|
||||
} else {
|
||||
$this->enableLineWrapping();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modifyTable(DataTableInterface $table)
|
||||
{
|
||||
foreach ($this->cssClasses as $cssClass) {
|
||||
$table->getConfig()->addCssClass($cssClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* display: Short-hand for stripe, hover, row-border and order-column.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setDefaultStyle()
|
||||
{
|
||||
$this->removeCssClass('display');
|
||||
$this->removeCssClass('compact');
|
||||
$this->removeCssClass('order-column');
|
||||
|
||||
$this->enableHover();
|
||||
$this->disableRowBorder();
|
||||
$this->disableStripes();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function setCompactStyle()
|
||||
{
|
||||
$this->addCssClass('compact');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function enableLineWrapping()
|
||||
{
|
||||
$this->removeCssClass('nowrap');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disableLineWrapping()
|
||||
{
|
||||
$this->addCssClass('nowrap');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function enableHover()
|
||||
{
|
||||
$this->addCssClass('hover');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disableHover()
|
||||
{
|
||||
$this->removeCssClass('hover');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function enableStripes()
|
||||
{
|
||||
$this->addCssClass('stripe');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disableStripes()
|
||||
{
|
||||
$this->removeCssClass('stripe');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function enableRowBorder()
|
||||
{
|
||||
$this->addCssClass('row-border');
|
||||
$this->removeCssClass('cell-border');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function disableRowBorder()
|
||||
{
|
||||
$this->removeCssClass('row-border');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function hasCssClass($className)
|
||||
{
|
||||
return in_array($className, $this->cssClasses, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function addCssClass($className)
|
||||
{
|
||||
$this->cssClasses[] = trim($className);
|
||||
$this->cssClasses = array_unique($this->cssClasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function removeCssClass($className)
|
||||
{
|
||||
$classKey = array_search($className, $this->cssClasses, true);
|
||||
if ($classKey !== false) {
|
||||
unset($this->cssClasses[$classKey]);
|
||||
$this->cssClasses = array_values($this->cssClasses);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Filter;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Request\DataTableRequest;
|
||||
|
||||
abstract class AbstractFilter implements FilterInterface
|
||||
{
|
||||
/** @var string $type */
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getType();
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
* @param DataTableRequest $request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function applyFilter(DataTableInterface $table, DataTableRequest $request);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Filter;
|
||||
|
||||
use Closure;
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Request\DataTableRequest;
|
||||
|
||||
final class CustomFilter implements FilterInterface
|
||||
{
|
||||
/** @var Closure $closure */
|
||||
private $closure;
|
||||
|
||||
/**
|
||||
* @param Closure $closure
|
||||
*/
|
||||
public function __construct(Closure $closure)
|
||||
{
|
||||
$this->closure = $closure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return FilterInterface::TYPE_CUSTOM;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
* @param DataTableRequest $request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function applyFilter(DataTableInterface $table, DataTableRequest $request)
|
||||
{
|
||||
$closure = $this->closure;
|
||||
$closure($table->getBaseQuery(), $request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Filter;
|
||||
|
||||
use ArrayIterator;
|
||||
use IteratorAggregate;
|
||||
use JsonSerializable;
|
||||
use Traversable;
|
||||
|
||||
class FilterCollection implements JsonSerializable, IteratorAggregate
|
||||
{
|
||||
/** @var array|FilterInterface[] $filters */
|
||||
protected $filters = [];
|
||||
|
||||
/**
|
||||
* @param array|FilterInterface[] $filters
|
||||
*/
|
||||
public function __construct(array $filters = [])
|
||||
{
|
||||
foreach ($filters as $filter) {
|
||||
$this->add($filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FilterInterface $filter
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add(FilterInterface $filter)
|
||||
{
|
||||
$this->filters[] = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|FilterInterface[]
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->filters as $filter) {
|
||||
$result[] = $filter->toArray();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayIterator|Traversable
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new ArrayIterator($this->filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep copy object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->filters as $index => $filter) {
|
||||
$this->filters[$index] = clone $filter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Filter;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Request\DataTableRequest;
|
||||
|
||||
interface FilterInterface
|
||||
{
|
||||
/** @var string TYPE_TEXT */
|
||||
const TYPE_TEXT = 'text';
|
||||
|
||||
/** @var string TYPE_NUMBER */
|
||||
const TYPE_NUMBER = 'number';
|
||||
|
||||
/** @var string TYPE_NUMBER_RANGE */
|
||||
const TYPE_NUMBER_RANGE = 'number_range';
|
||||
|
||||
/** @var string TYPE_CUSTOM */
|
||||
const TYPE_CUSTOM = 'custom';
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType();
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
* @param DataTableRequest $request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function applyFilter(DataTableInterface $table, DataTableRequest $request);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Filter;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Exception\FeatureNotImplementedException;
|
||||
use Xentral\Widgets\DataTable\Request\DataTableRequest;
|
||||
|
||||
/**
|
||||
* @deprecated Filter ist nocht nicht fertig
|
||||
*/
|
||||
final class NumberRangeFilter extends AbstractFilter
|
||||
{
|
||||
/**
|
||||
* @throws FeatureNotImplementedException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
throw new FeatureNotImplementedException('Filter type not implemented yet.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return FilterInterface::TYPE_NUMBER_RANGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
* @param DataTableRequest $request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function applyFilter(DataTableInterface $table, DataTableRequest $request)
|
||||
{
|
||||
// TODO: Implement applyFilter() method.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Filter;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Exception\ColumnNotFoundException;
|
||||
use Xentral\Widgets\DataTable\Exception\InvalidArgumentException;
|
||||
use Xentral\Widgets\DataTable\Request\DataTableRequest;
|
||||
|
||||
class SingleWordTextFilter implements FilterInterface
|
||||
{
|
||||
/** @var string LIKE_EQUALS */
|
||||
const LIKE_EQUALS = 'equals';
|
||||
|
||||
/** @var string LIKE_STARTS_WITH */
|
||||
const LIKE_STARTS_WITH = 'startswith';
|
||||
|
||||
/** @var string LIKE_ENDS_WITH */
|
||||
const LIKE_ENDS_WITH = 'endswith';
|
||||
|
||||
/** @var string LIKE_ANY */
|
||||
const LIKE_ANY = 'any';
|
||||
|
||||
/** @var string $columnName*/
|
||||
private $columnName;
|
||||
|
||||
/** @var string $filterName */
|
||||
private $filterName;
|
||||
|
||||
/** @var string $likePattern */
|
||||
private $likePattern;
|
||||
|
||||
/**
|
||||
* @param string $columnName
|
||||
* @param string $filterName
|
||||
* @param string|null $likePattern
|
||||
*/
|
||||
public function __construct($columnName, $filterName, $likePattern = self::LIKE_ANY)
|
||||
{
|
||||
if ($likePattern !== null) {
|
||||
$validLikePatterns = [self::LIKE_EQUALS, self::LIKE_STARTS_WITH, self::LIKE_ENDS_WITH, self::LIKE_ANY];
|
||||
if (!in_array($likePattern, $validLikePatterns, true)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Like pattern "%s" is invalid. Valid patterns are: %s', $likePattern,
|
||||
implode(', ', $validLikePatterns)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$this->columnName = $columnName;
|
||||
$this->filterName = $filterName;
|
||||
$this->likePattern = $likePattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFilterName()
|
||||
{
|
||||
return $this->filterName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLikePattern()
|
||||
{
|
||||
return $this->likePattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return FilterInterface::TYPE_TEXT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
* @param DataTableRequest $request
|
||||
*
|
||||
* @throws ColumnNotFoundException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function applyFilter(DataTableInterface $table, DataTableRequest $request)
|
||||
{
|
||||
$column = $table->getColumns()->getByName($this->columnName);
|
||||
if ($column === null || $column->getDbColumn() === null) {
|
||||
throw new ColumnNotFoundException(sprintf(
|
||||
'Can not apply text filter. Column "%s" is missing.',
|
||||
$this->columnName
|
||||
));
|
||||
}
|
||||
|
||||
$filterValues = $request->getParams()->getFilterValues();
|
||||
if (!array_key_exists($this->filterName, $filterValues)) {
|
||||
return; // Filter param is not set
|
||||
}
|
||||
|
||||
$filterValue = (string)$filterValues[$this->filterName];
|
||||
if ($filterValue === '') {
|
||||
return; // Filter value is empty
|
||||
}
|
||||
|
||||
switch ($this->likePattern) {
|
||||
case self::LIKE_EQUALS:
|
||||
$filterCondition = $filterValue;
|
||||
break;
|
||||
case self::LIKE_STARTS_WITH:
|
||||
$filterCondition = $filterValue . '%';
|
||||
break;
|
||||
case self::LIKE_ENDS_WITH:
|
||||
$filterCondition = '%' . $filterValue;
|
||||
break;
|
||||
case self::LIKE_ANY:
|
||||
default:
|
||||
$filterCondition = '%' . $filterValue . '%';
|
||||
break;
|
||||
}
|
||||
|
||||
$table->getBaseQuery()->where($column->getDbColumn() . ' LIKE ?', $filterCondition);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Options;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
final class DataTableOptions implements JsonSerializable
|
||||
{
|
||||
/** @var array $options Datatable initialisation options */
|
||||
private $options;
|
||||
|
||||
/** @var array $defaultSorting */
|
||||
private $defaultSorting = [];
|
||||
|
||||
/** @var array $postSorting */
|
||||
private $postSorting = [];
|
||||
|
||||
/** @var array $preSorting */
|
||||
private $preSorting = [];
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = $this->getDefaults();
|
||||
foreach ($options as $property => $value) {
|
||||
$this->setOption($property, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasOption($property)
|
||||
{
|
||||
return isset($this->options[(string)$property]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param mixed|null $fallbackValue
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function getOption($property, $fallbackValue = null)
|
||||
{
|
||||
if ($this->hasOption($property)) {
|
||||
return $this->options[(string)$property];
|
||||
}
|
||||
|
||||
return $fallbackValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets/overwrites a property
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setOption($property, $value)
|
||||
{
|
||||
$this->options[(string)$property] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets a property
|
||||
*
|
||||
* @param $property
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeOption($property)
|
||||
{
|
||||
if ($this->hasOption($property)) {
|
||||
unset($this->options[(string)$property]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDefaultSorting()
|
||||
{
|
||||
return $this->defaultSorting;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getPreSorting()
|
||||
{
|
||||
return $this->preSorting;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getPostSorting()
|
||||
{
|
||||
return $this->postSorting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default-Sortierung; Benutzer-Sortierung überschreibt Default-Sortierung
|
||||
*
|
||||
* @example ['lagerbestand' => 'DESC', 'bezeichnung' => 'ASC']
|
||||
*
|
||||
* @param array $sorting
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setDefaultSorting(array $sorting = [])
|
||||
{
|
||||
$this->defaultSorting = $sorting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feste Vor-Sortierung; kann vom Benutzer nicht geändert werden
|
||||
*
|
||||
* @example ['lagerbestand' => 'DESC', 'bezeichnung' => 'ASC']
|
||||
*
|
||||
* @param array $sorting
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPreSorting(array $sorting = [])
|
||||
{
|
||||
$this->preSorting = $sorting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feste Nach-Sortierung; kann vom Benutzer nicht geändert werden
|
||||
*
|
||||
* @example ['lagerbestand' => 'DESC', 'bezeichnung' => 'ASC']
|
||||
*
|
||||
* @param array $sorting
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPostSorting(array $sorting = [])
|
||||
{
|
||||
$this->postSorting = $sorting;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function getDefaults()
|
||||
{
|
||||
return [
|
||||
'processing' => true,
|
||||
'serverSide' => true,
|
||||
'ajax' => [
|
||||
'url' => null,
|
||||
'type' => 'GET',
|
||||
'data' => [],
|
||||
],
|
||||
'scrollX' => true,
|
||||
'orderCellsTop' => true, // Handle sorting events only on first header row
|
||||
'orderMulti' => true, // Multiple column ordering ability control
|
||||
'dom' => $this->getDefaultDomTemplate(),
|
||||
'language' => [
|
||||
'emptyTable' => 'Keine Einträge gefunden',
|
||||
'info' => 'Zeige _START_ bis _END_ von _TOTAL_ Einträgen',
|
||||
'infoEmpty' => 'Zeile 0 bis 0 von 0 Einträgen',
|
||||
'infoFiltered' => '(gefiltert aus insgesamt _MAX_ Einträgen)',
|
||||
'infoPostFix' => '',
|
||||
'decimal' => ',',
|
||||
'thousands' => '.',
|
||||
'lengthMenu' => '_MENU_ Einträge pro Seite',
|
||||
'loadingRecords' => 'Lade...',
|
||||
'processing' => 'Verarbeite...',
|
||||
'search' => 'Suche:',
|
||||
'zeroRecords' => 'Keine passenden Einträge gefunden',
|
||||
'paginate' => [
|
||||
'first' => '⇤',
|
||||
'last' => '⇥',
|
||||
'next' => '»',
|
||||
'previous' => '«',
|
||||
],
|
||||
'aria' => [
|
||||
'sortAscending' => ': Anklicken für aufsteigende Sortierung',
|
||||
'sortDescending' => ': Anklicken für absteigende Sortierung',
|
||||
],
|
||||
],
|
||||
|
||||
// Plugins
|
||||
'responsive' => false,
|
||||
|
||||
// Own config options
|
||||
'autoinit' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* - l = Length changing input control ("Einträge pro Seite")
|
||||
* - f = Filtering input (Search)
|
||||
* - t = Table
|
||||
* - i = Information summary ("Zeige 1 bis 10 von 14 Einträgen")
|
||||
* - p = Pagination
|
||||
* - r = Processing display element (Loading overlay)
|
||||
* - B = Buttons
|
||||
* - R = ColReorder (Column visibility)
|
||||
*
|
||||
* @see https://datatables.net/reference/option/dom
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getDefaultDomTemplate()
|
||||
{
|
||||
return
|
||||
"<'datatable-top'<'datatable-length'l><'datatable-search'f>" .
|
||||
'r>t' .
|
||||
"<'datatable-bottom'<'datatable-info'i><'datatable-buttons'B><'datatable-paginate'p>>";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable;
|
||||
|
||||
use Closure;
|
||||
use Xentral\Components\Database\SqlQuery\SelectQuery;
|
||||
use Xentral\Widgets\DataTable\Column\ColumnCollection;
|
||||
use Xentral\Widgets\DataTable\Options\DataTableOptions;
|
||||
use Xentral\Widgets\DataTable\Feature\FeatureCollection;
|
||||
use Xentral\Widgets\DataTable\Filter\FilterCollection;
|
||||
|
||||
final class PreparedDataTable implements DataTableInterface
|
||||
{
|
||||
/** @var DataTableBuildConfig $config */
|
||||
private $config;
|
||||
|
||||
/** @var SelectQuery $query */
|
||||
private $query;
|
||||
|
||||
/** @var DataTableOptions $options */
|
||||
private $options;
|
||||
|
||||
/** @var ColumnCollection $columns */
|
||||
private $columns;
|
||||
|
||||
/** @var FeatureCollection $features */
|
||||
private $features;
|
||||
|
||||
/** @var FilterCollection $filters */
|
||||
private $filters;
|
||||
|
||||
/** @var Closure|null $customSearch @todo */
|
||||
private $customSearch;
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $config
|
||||
* @param DataTableOptions $options
|
||||
* @param SelectQuery $selectQuery
|
||||
* @param ColumnCollection $columns
|
||||
* @param FeatureCollection $features
|
||||
* @param FilterCollection $filters
|
||||
*/
|
||||
public function __construct(
|
||||
DataTableBuildConfig $config,
|
||||
DataTableOptions $options,
|
||||
SelectQuery $selectQuery,
|
||||
ColumnCollection $columns,
|
||||
FeatureCollection $features,
|
||||
FilterCollection $filters
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->options = $options;
|
||||
$this->query = $selectQuery;
|
||||
$this->columns = $columns;
|
||||
$this->features = $features;
|
||||
$this->filters = $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataTableBuildConfig
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataTableOptions
|
||||
*/
|
||||
public function getOptions()
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SelectQuery
|
||||
*/
|
||||
public function getBaseQuery()
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ColumnCollection
|
||||
*/
|
||||
public function getColumns()
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FeatureCollection
|
||||
*/
|
||||
public function getFeatures()
|
||||
{
|
||||
return $this->features;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FilterCollection
|
||||
*/
|
||||
public function getFilters()
|
||||
{
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Closure|null @todo
|
||||
*/
|
||||
public function getCustomSearch()
|
||||
{
|
||||
return $this->customSearch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Request;
|
||||
|
||||
use Xentral\Components\Http\Request;
|
||||
use Xentral\Widgets\DataTable\Exception\InvalidArgumentException;
|
||||
|
||||
final class DataTableRequest
|
||||
{
|
||||
/** @var Request $request */
|
||||
private $request;
|
||||
|
||||
/** @var DataTableRequestParameter $params */
|
||||
private $params;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param DataTableRequestParameter $parameter
|
||||
*/
|
||||
public function __construct(Request $request, DataTableRequestParameter $parameter)
|
||||
{
|
||||
$method = $request->getMethod();
|
||||
if (!in_array($method, ['GET', 'POST'], true)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Can not create DataTableRequest instance. HTTP method "%s" is invalid.', $method
|
||||
));
|
||||
}
|
||||
|
||||
$this->request = $request;
|
||||
$this->params = $parameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function fromRequest(Request $request)
|
||||
{
|
||||
$parameters = DataTableRequestParameter::fromRequest($request);
|
||||
|
||||
return new self($request, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isDataRequest()
|
||||
{
|
||||
if (!$this->isValidDataTableRequest()) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->isAjax()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private function isValidDataTableRequest()
|
||||
{
|
||||
if (empty($this->getParams()->getTableName())) {
|
||||
return false;
|
||||
}
|
||||
if ($this->params->getDraw() < 1) {
|
||||
return false;
|
||||
}
|
||||
if (empty($this->params->getColumnsValues()) ||
|
||||
empty($this->params->getOrderValues()) ||
|
||||
empty($this->params->getSearchValues())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isAjax()
|
||||
{
|
||||
return $this->request->isAjax();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMethod()
|
||||
{
|
||||
return $this->request->getMethod();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataTableRequestParameter
|
||||
*/
|
||||
public function getParams()
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
public function getOriginalRequest()
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isExportRequest()
|
||||
{
|
||||
if (!$this->isValidDataTableRequest()) {
|
||||
return false;
|
||||
}
|
||||
if ($this->isAjax()) {
|
||||
return false;
|
||||
}
|
||||
if (empty($this->params->getExportValues())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Request;
|
||||
|
||||
use Xentral\Components\Http\Request;
|
||||
|
||||
/**
|
||||
* @see https://datatables.net/manual/server-side
|
||||
*/
|
||||
final class DataTableRequestParameter
|
||||
{
|
||||
/** @var string|null $tableName */
|
||||
private $tableName;
|
||||
|
||||
/** @var int $draw Draw counter */
|
||||
private $draw;
|
||||
|
||||
/** @var int $start Paging first record offset */
|
||||
private $start;
|
||||
|
||||
/** @var int $length Number of records returned */
|
||||
private $length;
|
||||
|
||||
/** @var array $columns Column settings and search queries */
|
||||
private $columns;
|
||||
|
||||
/** @var array $search Global search query */
|
||||
private $search;
|
||||
|
||||
/** @var array $order Ordering settings */
|
||||
private $order;
|
||||
|
||||
/** @var array $filter Custom parameter for filter feature */
|
||||
private $filter;
|
||||
|
||||
/** @var array $export Custom parameter for export feature */
|
||||
private $export;
|
||||
|
||||
/**
|
||||
* @param string $tableName
|
||||
* @param int $draw
|
||||
* @param int $start
|
||||
* @param int $length
|
||||
* @param array $columns
|
||||
* @param array $search
|
||||
* @param array $order
|
||||
* @param array $filter
|
||||
* @param array $export
|
||||
*/
|
||||
public function __construct(
|
||||
$tableName = null,
|
||||
$draw = 1,
|
||||
$start = 0,
|
||||
$length = 10,
|
||||
$columns = [],
|
||||
$search = [],
|
||||
$order = [],
|
||||
$filter = [],
|
||||
$export = []
|
||||
) {
|
||||
$this->tableName = $tableName;
|
||||
$this->draw = (int)$draw;
|
||||
$this->start = (int)$start;
|
||||
$this->length = (int)$length;
|
||||
$this->columns = (array)$columns;
|
||||
$this->search = (array)$search;
|
||||
$this->order = (array)$order;
|
||||
$this->filter = (array)$filter;
|
||||
$this->export = (array)$export;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function fromRequest(Request $request)
|
||||
{
|
||||
$params = $request->getMethod() === 'GET' ? $request->get : $request->post;
|
||||
|
||||
$tableName = $params->getAlphaNumWithDashes('tablename', null);
|
||||
$draw = $params->getInt('draw', 1);
|
||||
$start = $params->getInt('start', 0);
|
||||
$length = $params->getInt('length', 10);
|
||||
$columns = (array)$params->get('columns', []);
|
||||
$search = (array)$params->get('search', []);
|
||||
$order = (array)$params->get('order', []);
|
||||
$filter = (array)$params->get('filter', []);
|
||||
$export = (array)$params->get('export', []);
|
||||
|
||||
return new self($tableName, $draw, $start, $length, $columns, $search, $order, $filter, $export);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTableName()
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDraw()
|
||||
{
|
||||
return $this->draw;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getStart()
|
||||
{
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLength()
|
||||
{
|
||||
return $this->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getSearchValues()
|
||||
{
|
||||
return $this->search;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getOrderValues()
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getColumnsValues()
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getFilterValues()
|
||||
{
|
||||
return $this->filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getExportValues()
|
||||
{
|
||||
return $this->export;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Result;
|
||||
|
||||
use JsonSerializable;
|
||||
use Xentral\Widgets\DataTable\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @see https://datatables.net/manual/server-side
|
||||
*/
|
||||
final class DataTableDataResult implements JsonSerializable
|
||||
{
|
||||
/** @var int $drawCounter The draw counter */
|
||||
private $drawCounter;
|
||||
|
||||
/** @var int $recordsTotal Total number of records, before filtering */
|
||||
private $recordsTotal;
|
||||
|
||||
/** @var int $recordsFiltered Total number of records, after filtering */
|
||||
private $recordsFiltered;
|
||||
|
||||
/** @var array $data */
|
||||
private $data;
|
||||
|
||||
/** @var string|null $errorMessage */
|
||||
private $errorMessage;
|
||||
|
||||
/** @var array|null $debugInfo */
|
||||
private $debugInfo;
|
||||
|
||||
/**
|
||||
* @param int $drawCounter
|
||||
* @param int $recordsTotal
|
||||
* @param int $recordsFiltered
|
||||
* @param array $data
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(
|
||||
$drawCounter = 1,
|
||||
$recordsTotal = 0,
|
||||
$recordsFiltered = 0,
|
||||
$data = []
|
||||
) {
|
||||
if (!is_int($drawCounter)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid argument. Draw counter must be an integer. Given type: %s',
|
||||
strtolower(gettype($drawCounter))
|
||||
));
|
||||
}
|
||||
if (!is_int($recordsTotal)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid argument. Total records count must be an integer. Given type: %s',
|
||||
strtolower(gettype($recordsTotal))
|
||||
));
|
||||
}
|
||||
if (!is_int($recordsFiltered)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid argument. Filtered records count be an integer. Given type: %s',
|
||||
strtolower(gettype($recordsFiltered))
|
||||
));
|
||||
}
|
||||
if (!is_array($data)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid argument. Data parameter must be an array. Given type: %s',
|
||||
strtolower(gettype($data))
|
||||
));
|
||||
}
|
||||
|
||||
$this->drawCounter = $drawCounter;
|
||||
$this->recordsTotal = $recordsTotal;
|
||||
$this->recordsFiltered = $recordsFiltered;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getResult()
|
||||
{
|
||||
$result = [
|
||||
'draw' => $this->drawCounter,
|
||||
'recordsTotal' => $this->recordsTotal,
|
||||
'recordsFiltered' => $this->recordsFiltered,
|
||||
'data' => $this->data,
|
||||
];
|
||||
|
||||
if ($this->debugInfo !== null) {
|
||||
$result['debug'] = $this->debugInfo;
|
||||
}
|
||||
if ($this->errorMessage !== null) {
|
||||
$result['error'] = $this->errorMessage;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDrawCounter()
|
||||
{
|
||||
return $this->drawCounter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getRecordsTotal()
|
||||
{
|
||||
return $this->recordsTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getRecordsFiltered()
|
||||
{
|
||||
return $this->recordsFiltered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
*/
|
||||
public function getDebugInfo()
|
||||
{
|
||||
return $this->debugInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $debugInfo
|
||||
*/
|
||||
public function setDebugInfo($debugInfo)
|
||||
{
|
||||
if (!is_array($debugInfo)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid argument. Debug information must be an array. Given type: %s',
|
||||
strtolower(gettype($debugInfo))
|
||||
));
|
||||
}
|
||||
|
||||
$this->debugInfo = $debugInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasError()
|
||||
{
|
||||
return $this->errorMessage !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getErrorMessage()
|
||||
{
|
||||
return $this->errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $errorMessage
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setErrorMessage($errorMessage)
|
||||
{
|
||||
if (!is_string($errorMessage)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid argument. Error message must be a string. Given type: %s',
|
||||
strtolower(gettype($errorMessage))
|
||||
));
|
||||
}
|
||||
|
||||
$this->errorMessage = $errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return $this->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Result;
|
||||
|
||||
use Xentral\Widgets\DataTable\Exception\InvalidArgumentException;
|
||||
|
||||
final class DataTableHtmlResult
|
||||
{
|
||||
/** @var string $tableHtml */
|
||||
private $tableHtml;
|
||||
|
||||
/** @var array $scriptOptions Initialization options for DataTable */
|
||||
private $scriptOptions = [];
|
||||
|
||||
/**
|
||||
* @param string $tableHtml
|
||||
* @param array $scriptOptions
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($tableHtml, array $scriptOptions)
|
||||
{
|
||||
if (empty($tableHtml)) {
|
||||
throw new InvalidArgumentException('Required parameter "tableHtml" is empty.');
|
||||
}
|
||||
if (empty($scriptOptions)) {
|
||||
throw new InvalidArgumentException('Required parameter "scriptOptions" is empty.');
|
||||
}
|
||||
|
||||
$this->tableHtml = $tableHtml;
|
||||
$this->scriptOptions = $scriptOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getResult()
|
||||
{
|
||||
return $this->getHtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHtml()
|
||||
{
|
||||
$html = '<div class="datatable-container">';
|
||||
$html .= $this->getTableHtml();
|
||||
$html .= $this->getScriptHtml();
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTableHtml()
|
||||
{
|
||||
return $this->tableHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getScriptHtml()
|
||||
{
|
||||
$optionsJsonString = json_encode(
|
||||
$this->getScriptOptions(),
|
||||
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT
|
||||
);
|
||||
|
||||
return sprintf('<script type="application/json">%s</script>', $optionsJsonString);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getScriptOptions()
|
||||
{
|
||||
return $this->scriptOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getHtml();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Service;
|
||||
|
||||
use Xentral\Components\Database\Database;
|
||||
use Xentral\Widgets\DataTable\Column\ColumnCollection;
|
||||
use Xentral\Widgets\DataTable\DataTableBuildConfig;
|
||||
use Xentral\Widgets\DataTable\Exception\BuildFailedException;
|
||||
use Xentral\Widgets\DataTable\Options\DataTableOptions;
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Feature\DataTableFeatureInterface;
|
||||
use Xentral\Widgets\DataTable\Feature\FeatureCollection;
|
||||
use Xentral\Widgets\DataTable\Filter\FilterCollection;
|
||||
use Xentral\Widgets\DataTable\PreparedDataTable;
|
||||
use Xentral\Widgets\DataTable\Type\DataTableTypeInterface;
|
||||
|
||||
final class DataTableBuilder
|
||||
{
|
||||
/** @var Database $database */
|
||||
private $database;
|
||||
|
||||
/**
|
||||
* @param Database $database
|
||||
*/
|
||||
public function __construct(Database $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $config
|
||||
*
|
||||
* @throws BuildFailedException
|
||||
*
|
||||
* @return DataTableInterface
|
||||
*/
|
||||
public function buildTable(DataTableBuildConfig $config)
|
||||
{
|
||||
if (empty(trim($config->getTableName()))) {
|
||||
throw new BuildFailedException('Build config is incomplete. Table name is empty.');
|
||||
}
|
||||
if (empty(trim($config->getAjaxUrl()))) {
|
||||
throw new BuildFailedException('Build config is incomplete. Property "ajaxUrl" is missing.');
|
||||
}
|
||||
if (!class_exists($config->getTableClass(), true)) {
|
||||
throw new BuildFailedException(sprintf('DataTable class "%s" not found', $config->getTableClass()));
|
||||
}
|
||||
$interfaces = class_implements($config->getTableClass(), true);
|
||||
if (!in_array(DataTableTypeInterface::class, $interfaces, true)) {
|
||||
throw new BuildFailedException(
|
||||
'Can not build data table. Class does not implement ' . DataTableTypeInterface::class
|
||||
);
|
||||
}
|
||||
|
||||
/** @var DataTableTypeInterface $table */
|
||||
$className = $config->getTableClass();
|
||||
$table = new $className();
|
||||
|
||||
// @todo getParent() verarbeiten
|
||||
|
||||
$options = new DataTableOptions();
|
||||
$table->configureOptions($options);
|
||||
|
||||
$columns = new ColumnCollection();
|
||||
$table->configureColumns($columns);
|
||||
|
||||
$query = $this->database->select();
|
||||
$table->configureQuery($query);
|
||||
|
||||
if ($query->hasOrderBy()) {
|
||||
throw new BuildFailedException(
|
||||
'Sorting in "configureQuery" will be overwritten. ' .
|
||||
'Use "setDefaultSorting" in "configureOptions" instead.'
|
||||
);
|
||||
}
|
||||
|
||||
$features = new FeatureCollection();
|
||||
$table->configureFeatures($features);
|
||||
|
||||
$filters = new FilterCollection();
|
||||
$table->configureFilters($filters);
|
||||
|
||||
$preparedTable = new PreparedDataTable($config, $options, $query, $columns, $features, $filters);
|
||||
$this->prepareTable($preparedTable);
|
||||
|
||||
return $preparedTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function prepareTable(DataTableInterface $table)
|
||||
{
|
||||
$this->prepareColumns($table);
|
||||
$this->applyFeatures($table);
|
||||
$this->prepareSorting($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function applyFeatures(DataTableInterface $table)
|
||||
{
|
||||
/** @var DataTableFeatureInterface $feature */
|
||||
foreach ($table->getFeatures() as $feature) {
|
||||
$feature->modifyTable($table);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function prepareColumns(DataTableInterface $table)
|
||||
{
|
||||
// Spalten aus dem SQL-Query holen
|
||||
$query = $table->getBaseQuery();
|
||||
$columnNames = $query->getCols();
|
||||
|
||||
foreach ($columnNames as $alias => $fullColumnName) {
|
||||
// Spalten mit Spaltenaliasen zuerst behandeln (easy)
|
||||
$column = $table->getColumns()->getByName($alias);
|
||||
if ($column !== null) {
|
||||
$column->setDbColumn($fullColumnName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tabellenalias aus Spaltenname entfernen
|
||||
$shortColumnName = $this->extractNameFromColumn($fullColumnName);
|
||||
$column = $table->getColumns()->getByName($shortColumnName);
|
||||
if ($column !== null) {
|
||||
$column->setDbColumn($fullColumnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function prepareSorting(DataTableInterface $table)
|
||||
{
|
||||
$columnNames = array_column($table->getColumns()->toArray(), 'data');
|
||||
$defaultSorting = $table->getOptions()->getDefaultSorting();
|
||||
$postSorting = $table->getOptions()->getPostSorting();
|
||||
$preSorting = $table->getOptions()->getPreSorting();
|
||||
|
||||
$defaultSorting = $this->translateSortingValues($columnNames, $defaultSorting);
|
||||
$postSorting = $this->translateSortingValues($columnNames, $postSorting);
|
||||
$preSorting = $this->translateSortingValues($columnNames, $preSorting);
|
||||
|
||||
/**
|
||||
* Sortierung, wenn nichts gesetzt ist; Benutzer-Sortierung überschreibt diesen Wert
|
||||
*
|
||||
* @see https://datatables.net/reference/option/order
|
||||
*/
|
||||
if (empty($defaultSorting)) {
|
||||
$defaultSorting = [[0, 'asc']];
|
||||
}
|
||||
$table->getOptions()->setOption('order', $defaultSorting);
|
||||
|
||||
/**
|
||||
* Vor- und Nach-Sortierung; Kann vom Benutzer nicht geändert werden
|
||||
*
|
||||
* @see https://datatables.net/reference/option/orderFixed
|
||||
*/
|
||||
if (!empty($preSorting)) {
|
||||
$orderFixed['pre'] = $preSorting;
|
||||
}
|
||||
if (!empty($postSorting)) {
|
||||
$orderFixed['post'] = $postSorting;
|
||||
}
|
||||
if (!empty($orderFixed)) {
|
||||
$table->getOptions()->setOption('orderFixed', $orderFixed);
|
||||
} else {
|
||||
$table->getOptions()->removeOption('orderFixed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @example ['lagerbestand' => 'DESC', 'bezeichnung' => 'ASC'] wird zu [[3, 'desc'], [1, 'asc']]
|
||||
*
|
||||
* @param array $columnNames
|
||||
* @param array $sortingValues
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function translateSortingValues($columnNames, $sortingValues)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($sortingValues as $columnName => $sortOrder) {
|
||||
$columnIndex = array_search($columnName, $columnNames, true);
|
||||
if ($columnIndex !== false) {
|
||||
$result[] = [$columnIndex, strtolower($sortOrder)];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $column
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function extractNameFromColumn($column)
|
||||
{
|
||||
if ($pos = strrpos($column, '.')) {
|
||||
return substr($column, $pos + 1);
|
||||
}
|
||||
|
||||
return $column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Service;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Xentral\Components\Database\Database;
|
||||
use Xentral\Components\Database\SqlQuery\SelectQuery;
|
||||
use Xentral\Components\Exporter\Csv\CsvConfig;
|
||||
use Xentral\Components\Exporter\Csv\CsvWriter;
|
||||
use Xentral\Components\Exporter\Exception\InvalidResourceException;
|
||||
use Xentral\Widgets\DataTable\Column\Column;
|
||||
use Xentral\Widgets\DataTable\DataTableBuildConfig;
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Exception\InvalidArgumentException;
|
||||
use Xentral\Widgets\DataTable\Feature\DebugFeature;
|
||||
use Xentral\Widgets\DataTable\Feature\RowClassesFeature;
|
||||
use Xentral\Widgets\DataTable\Filter\FilterInterface;
|
||||
use Xentral\Widgets\DataTable\Request\DataTableRequest;
|
||||
use Xentral\Widgets\DataTable\Result\DataTableDataResult;
|
||||
|
||||
final class DataTableFetcher
|
||||
{
|
||||
/** @var Database $db */
|
||||
private $db;
|
||||
|
||||
/** @var DataTableRequest $request */
|
||||
private $request;
|
||||
|
||||
/**
|
||||
* @param Database $db
|
||||
* @param DataTableRequest $request
|
||||
*/
|
||||
public function __construct(Database $db, DataTableRequest $request)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $buildConfig
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canFetchData(DataTableBuildConfig $buildConfig)
|
||||
{
|
||||
if (!$this->request->isAjax()) {
|
||||
return false;
|
||||
}
|
||||
if ($this->request->getMethod() !== $buildConfig->getAjaxMethod()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tableNameDefined = $buildConfig->getTableName();
|
||||
$tableNameRequested = $this->request->getParams()->getTableName();
|
||||
|
||||
return $tableNameDefined === $tableNameRequested;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $buildConfig
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canExportData(DataTableBuildConfig $buildConfig)
|
||||
{
|
||||
$exportParams = (array)$this->request->getParams()->getExportValues();
|
||||
if (empty($exportParams['format']) || empty($exportParams['result'])) {
|
||||
return false;
|
||||
}
|
||||
if ($this->request->isAjax()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tableNameDefined = $buildConfig->getTableName();
|
||||
$tableNameRequested = $this->request->getParams()->getTableName();
|
||||
|
||||
return $tableNameDefined === $tableNameRequested;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return DataTableDataResult
|
||||
*/
|
||||
public function fetchData(DataTableInterface $table)
|
||||
{
|
||||
$startParam = $this->request->getParams()->getStart();
|
||||
$lengthParam = $this->request->getParams()->getLength();
|
||||
|
||||
try {
|
||||
|
||||
$debugging = false;
|
||||
if ($table->getFeatures()->has(DebugFeature::class)) {
|
||||
/** @var DebugFeature $debugFeature */
|
||||
$debugFeature = $table->getFeatures()->get(DebugFeature::class);
|
||||
$debugging = $debugFeature->isEnabled();
|
||||
}
|
||||
|
||||
if ($debugging === true) {
|
||||
$debugData = ['profiler' => ['start' => microtime(true)]];
|
||||
}
|
||||
|
||||
$baseQuery = $table->getBaseQuery();
|
||||
$cols = $baseQuery->getCols();
|
||||
|
||||
// Set up query for total record count
|
||||
$recordsTotalQuery = clone $baseQuery;
|
||||
$recordsTotalQuery
|
||||
->resetCols()
|
||||
->cols([sprintf('COUNT(%s) AS num', $cols[0])]);
|
||||
|
||||
// Apply filters and searches
|
||||
$this->applyFilters($table);
|
||||
$this->applyColumnSearch($table, $baseQuery);
|
||||
$this->applyGlobalSearch($table, $baseQuery);
|
||||
|
||||
// Set up query for data + limit result set
|
||||
$dataQuery = clone $baseQuery;
|
||||
$dataQuery->offset($startParam);
|
||||
$dataQuery->limit($lengthParam);
|
||||
if ($startParam === -1 || $lengthParam === -1) {
|
||||
$dataQuery->offset(0);
|
||||
$dataQuery->limit(0);
|
||||
}
|
||||
|
||||
// Apply ORDER BY + LIMIT
|
||||
$sortingValues = $this->prepareSortingValue($table);
|
||||
$this->applySorting($dataQuery, $sortingValues);
|
||||
$this->applyPaging($dataQuery, $startParam, $lengthParam);
|
||||
|
||||
// Set up query for filtered record count
|
||||
// (= Record count with applied filters and searches)
|
||||
$recordsFilteredQuery = clone $baseQuery;
|
||||
$recordsFilteredQuery->resetCols()->cols([sprintf('COUNT(%s) AS num', $cols[0])]);
|
||||
|
||||
// Ergebnisanzahl; mit Filter
|
||||
$recordsFiltered = $this->db->fetchValue(
|
||||
$recordsFilteredQuery->getStatement(),
|
||||
$recordsFilteredQuery->getBindValues()
|
||||
);
|
||||
|
||||
// Ergebnisanzahl; ohne Filter
|
||||
$recordsTotal = $this->db->fetchValue(
|
||||
$recordsTotalQuery->getStatement(),
|
||||
$recordsTotalQuery->getBindValues()
|
||||
);
|
||||
|
||||
// Fetch data; displayed result
|
||||
$data = $this->db->fetchAll(
|
||||
$dataQuery->getStatement(),
|
||||
$dataQuery->getBindValues()
|
||||
);
|
||||
|
||||
// Column-Formatter anwenden
|
||||
$columnFormatters = $table->getColumns()->getFormatters();
|
||||
$this->applyColumnFormatters($data, $columnFormatters);
|
||||
|
||||
// Row-Formatter anwenden
|
||||
// @todo In RowClassesFeature auslagern
|
||||
if ($table->getFeatures()->has(RowClassesFeature::class)) {
|
||||
/** @var RowClassesFeature $rowStyling */
|
||||
$rowStyling = $table->getFeatures()->get(RowClassesFeature::class);
|
||||
if ($rowStyling->hasCustomFormatter()) {
|
||||
$rowFormatter = $rowStyling->getCustomFormatter();
|
||||
foreach ($data as &$rowValues) {
|
||||
$rowClasses = $rowStyling->getClassesString();
|
||||
foreach ($rowFormatter as $closure) {
|
||||
$rowClasses .= $closure($rowValues);
|
||||
}
|
||||
$rowValues['DT_RowClass'] = $rowClasses;
|
||||
}
|
||||
unset($rowValues);
|
||||
}
|
||||
}
|
||||
|
||||
// ID-Attribut für jede Zeile setzen
|
||||
$tableName = $table->getConfig()->getTableName();
|
||||
$this->appendRowIdAttribute($data, $tableName);
|
||||
|
||||
// Result-Objekt bauen
|
||||
$result = new DataTableDataResult(
|
||||
(int)$this->request->getParams()->getDraw(),
|
||||
(int)$recordsTotal,
|
||||
(int)$recordsFiltered,
|
||||
(array)$data
|
||||
);
|
||||
|
||||
} catch (Exception $exception) {
|
||||
$result = new DataTableDataResult();
|
||||
$result->setErrorMessage(sprintf(
|
||||
'Unhandled exception: (%s) %s',
|
||||
get_class($exception),
|
||||
$exception->getMessage()
|
||||
));
|
||||
}
|
||||
|
||||
if ($debugging === true) {
|
||||
$debugData['profiler']['finish'] = microtime(true);
|
||||
$debugData['profiler']['duration_real'] = $debugData['profiler']['finish'] - $debugData['profiler']['start'];
|
||||
$debugData['profiler']['duration'] = sprintf('%.6f', $debugData['profiler']['duration_real']) . ' seconds';
|
||||
|
||||
if (isset($dataQuery)) {
|
||||
$debugData['query']['statement'] = $dataQuery->getStatement();
|
||||
$debugData['query']['bindings'] = var_export($dataQuery->getBindValues(), true);
|
||||
}
|
||||
$result->setDebugInfo($debugData);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return string Path to export file
|
||||
*/
|
||||
public function exportData(DataTableInterface $table)
|
||||
{
|
||||
$startParam = (int)$this->request->getParams()->getStart();
|
||||
$lengthParam = (int)$this->request->getParams()->getLength();
|
||||
$exportParams = (array)$this->request->getParams()->getExportValues();
|
||||
|
||||
$exportFormat = !empty($exportParams['format']) ? $exportParams['format'] : 'csv';
|
||||
if ($exportFormat !== 'csv') {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid export format "%s". Only "csv" is valid.', $exportFormat
|
||||
));
|
||||
}
|
||||
|
||||
$exportResult = !empty($exportParams['result']) ? $exportParams['result'] : 'page';
|
||||
if (!in_array($exportResult, ['all', 'page'], true)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid export result parameter value "%s". Valid values: %s',
|
||||
$exportResult,
|
||||
implode(', ', ['all', 'page'])
|
||||
));
|
||||
}
|
||||
|
||||
// Alle Ergebnisse exportieren
|
||||
if ($exportResult === 'all') {
|
||||
$startParam = -1;
|
||||
$lengthParam = -1;
|
||||
}
|
||||
|
||||
$fileName = uniqid('export-' . $table->getConfig()->getTableName(), false) . '.csv';
|
||||
$filePath = sys_get_temp_dir() . '/' . $fileName;
|
||||
|
||||
$csv = @fopen($filePath, 'x+b');
|
||||
if ($csv === false) {
|
||||
throw new InvalidResourceException(sprintf('Failed to open resource for file path "%s".', $filePath));
|
||||
}
|
||||
|
||||
$writer = new CsvWriter($csv, new CsvConfig());
|
||||
|
||||
$titles = [];
|
||||
$dbCols = [];
|
||||
foreach ($table->getColumns() as $column) {
|
||||
/** @var Column $column */
|
||||
if ($column->isExportable() && !empty($column->getDbColumn())) {
|
||||
$titles[] = $column->getTitle();
|
||||
$dbCols[] = $column->getDbColumn();
|
||||
}
|
||||
}
|
||||
$writer->writeLine($titles);
|
||||
|
||||
$dataQuery = clone $table->getBaseQuery();
|
||||
$dataQuery->resetCols()->cols($dbCols);
|
||||
|
||||
// Apply filters and searches
|
||||
$this->applyFilters($table);
|
||||
$this->applyColumnSearch($table, $dataQuery);
|
||||
$this->applyGlobalSearch($table, $dataQuery);
|
||||
|
||||
// Apply ORDER BY
|
||||
$sortingValues = $this->prepareSortingValue($table);
|
||||
$this->applySorting($dataQuery, $sortingValues);
|
||||
|
||||
$itemsPerStep = 2500;
|
||||
$currentOffset = 0;
|
||||
$hasResults = true;
|
||||
|
||||
if ($exportResult === 'page') {
|
||||
$itemsPerStep = $lengthParam;
|
||||
$currentOffset = $startParam;
|
||||
}
|
||||
|
||||
do {
|
||||
|
||||
$dataQuery->offset($currentOffset);
|
||||
$dataQuery->limit($itemsPerStep);
|
||||
|
||||
$data = $this->db->yieldAll(
|
||||
$dataQuery->getStatement(),
|
||||
$dataQuery->getBindValues()
|
||||
);
|
||||
|
||||
if (!$data->valid()) {
|
||||
$hasResults = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$writer->writeLines($data);
|
||||
|
||||
$currentOffset += $itemsPerStep;
|
||||
|
||||
// Nach einer Iteration aufhören, wenn nur eine Seite exportiert werden soll
|
||||
if ($exportResult === 'page') {
|
||||
$hasResults = false;
|
||||
}
|
||||
|
||||
} while ($hasResults);
|
||||
|
||||
fclose($csv);
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function applyFilters(DataTableInterface $table)
|
||||
{
|
||||
/** @var FilterInterface $filter */
|
||||
foreach ($table->getFilters() as $filter) {
|
||||
$filter->applyFilter($table, $this->request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suche über das allgemeine Suchfeld verarbeiten (oben rechts)
|
||||
*
|
||||
* @param DataTableInterface $table
|
||||
* @param SelectQuery $query
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function applyGlobalSearch(DataTableInterface $table, SelectQuery $query)
|
||||
{
|
||||
$searchValue = $this->getSearchParam();
|
||||
if (empty($searchValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$searchParts = explode(' ', $searchValue);
|
||||
$searchParts = array_filter($searchParts, 'trim');
|
||||
|
||||
// Custom Search @todo Momentan ohne Funktion; Es gibt keine Möglichkeit zum Setzen der Einstellung
|
||||
// Beispiel-Setter:
|
||||
//$this->setCustomSearch(function (SelectQuery $query) {
|
||||
// return $query
|
||||
// ->cols(['artikel.id'])
|
||||
// ->from('artikel')
|
||||
// ->where('artikel.name_de LIKE :query')
|
||||
// ->orWhere('artikel.name_en LIKE :query');
|
||||
//});
|
||||
//$customSearchClosure = $table->getCustomSearch();
|
||||
//if ($customSearchClosure !== null) {
|
||||
// $matchColumn = $query->getCols()[0];
|
||||
// $customSearchQuery = $customSearchClosure($this->db->select());
|
||||
//
|
||||
// $query->joinSubSelect('inner', $customSearchQuery, 'matches', 'matches.id = ' . $matchColumn);
|
||||
// $query->bindValue('query', '%' . $searchValue . '%');
|
||||
//
|
||||
// return;
|
||||
//}
|
||||
|
||||
// Normale Suche
|
||||
$searchableDbColumns = $table->getColumns()->getSearchableDbColumns();
|
||||
foreach ($searchParts as $searchWord) {
|
||||
$query->where(static function (SelectQuery $select) use ($searchableDbColumns, $searchWord) {
|
||||
foreach ($searchableDbColumns as $searchDbColumn) {
|
||||
$select->orWhere(sprintf('%s LIKE ?', $searchDbColumn), '%' . $searchWord . '%');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo In ColumnFilterFeature auslagern
|
||||
*
|
||||
* @param DataTableInterface $table
|
||||
* @param SelectQuery $query
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function applyColumnSearch(DataTableInterface $table, SelectQuery $query)
|
||||
{
|
||||
//$columnFilter = $table->getFeatures()->get(ColumnFilterFeature::class);
|
||||
|
||||
$params = (array)$this->request->getParams()->getColumnsValues();
|
||||
foreach ($params as $index => $param) {
|
||||
$searchValue = $param['search']['value'];
|
||||
if (empty($searchValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Spaltensuche wurde ausgefüllt
|
||||
$column = $table->getColumns()->getByName($param['name']);
|
||||
if ($column === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Zahlenbereich-Suche
|
||||
if (strpos($searchValue, 'number_range:') === 0) {
|
||||
$searchPattern = str_replace([':null|', '|null'], '|', $searchValue);
|
||||
if ($searchPattern === 'number_range:|') {
|
||||
continue; // Leere Suche
|
||||
}
|
||||
|
||||
$searchPattern = str_replace('number_range:', '', $searchPattern);
|
||||
$searchParts = explode('|', $searchPattern);
|
||||
if (count($searchParts) !== 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$valueFrom = str_replace(',', '.', $searchParts[0]);
|
||||
$valueTo = str_replace(',', '.', $searchParts[1]);
|
||||
if (is_numeric($valueFrom)) {
|
||||
$query->where($column->getDbColumn() . ' >= ?', (float)$valueFrom);
|
||||
}
|
||||
if (is_numeric($valueTo)) {
|
||||
$query->where($column->getDbColumn() . ' <= ?', (float)$valueTo);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normale Textsuche
|
||||
$query->where($column->getDbColumn() . ' LIKE ?', '%' . $searchValue . '%');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://datatables.net/manual/server-side#Sent-parameters Parameters 'start' and 'length'
|
||||
*
|
||||
* @param SelectQuery $dataQuery
|
||||
* @param int $startValue
|
||||
* @param int $lengthValue
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function applyPaging(SelectQuery $dataQuery, $startValue, $lengthValue)
|
||||
{
|
||||
$dataQuery->offset($startValue);
|
||||
$dataQuery->limit($lengthValue);
|
||||
if ($startValue === -1 || $lengthValue === -1) {
|
||||
$dataQuery->offset(0);
|
||||
$dataQuery->limit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SelectQuery $query
|
||||
* @param array $sortingValues
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function applySorting(SelectQuery $query, array $sortingValues)
|
||||
{
|
||||
if (!empty($sortingValues)) {
|
||||
$query->resetOrderBy();
|
||||
foreach ($sortingValues as $sortColumn => $sortDirection) {
|
||||
$query->orderBy([sprintf('%s %s', $sortColumn, strtoupper($sortDirection))]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Column-Formatter anwenden
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $formatters
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function applyColumnFormatters(array &$data, array $formatters = [])
|
||||
{
|
||||
if (empty($formatters)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($formatters as $colName => $formatter) {
|
||||
if (!is_callable($formatter)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($data as &$rowData) {
|
||||
$cellData = $rowData[$colName];
|
||||
$newValue = $this->callColumnFormatter($formatter, $cellData, $rowData);
|
||||
$rowData[$colName] = $newValue;
|
||||
}
|
||||
unset($rowData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure $callback
|
||||
* @param string $value
|
||||
* @param array $rowValues
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function callColumnFormatter(Closure $callback, $value, $rowValues)
|
||||
{
|
||||
return $callback($value, $rowValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function prepareSortingValue(DataTableInterface $table)
|
||||
{
|
||||
$orderValue = $this->getOrderParam();
|
||||
|
||||
$sorting = [];
|
||||
foreach ($orderValue as $orderItem) {
|
||||
$columnIndex = (int)$orderItem['column'];
|
||||
$column = $table->getColumns()->getByIndex($columnIndex);
|
||||
if ($column === null) {
|
||||
break;
|
||||
}
|
||||
$columnName = $column->getDbColumn();
|
||||
$sortDirection = in_array(strtolower($orderItem['dir']), ['asc', 'desc'], true)
|
||||
? strtolower($orderItem['dir'])
|
||||
: null;
|
||||
|
||||
if ($columnName !== null && $sortDirection !== null) {
|
||||
$sorting[$columnName] = strtoupper($sortDirection);
|
||||
}
|
||||
}
|
||||
|
||||
return $sorting;
|
||||
}
|
||||
|
||||
/**
|
||||
* ID-Attribut für jede Zeile setzen
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $tableName
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function appendRowIdAttribute(&$data, $tableName)
|
||||
{
|
||||
// Row-ID hinzufügen
|
||||
foreach ($data as &$rowValues) {
|
||||
foreach ($rowValues as $key => &$value) {
|
||||
if ($key === 'id') {
|
||||
$rowValues['DT_RowId'] = sprintf('%s_row_%s', $tableName, $value);
|
||||
}
|
||||
}
|
||||
unset($value);
|
||||
}
|
||||
unset($rowValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function getSearchParam()
|
||||
{
|
||||
return $this->request->getParams()->getSearchValues()['value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function getOrderParam()
|
||||
{
|
||||
return (array)$this->request->getParams()->getOrderValues();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Service;
|
||||
|
||||
use Xentral\Widgets\DataTable\Column\Column;
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Result\DataTableHtmlResult;
|
||||
|
||||
final class DataTableRenderer
|
||||
{
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return DataTableHtmlResult
|
||||
*/
|
||||
public function createHtmlResult(DataTableInterface $table)
|
||||
{
|
||||
return new DataTableHtmlResult($this->getHtmlTable($table), $this->getDataTableOptions($table));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getDataTableOptions(DataTableInterface $table)
|
||||
{
|
||||
$options = $table->getOptions()->toArray();
|
||||
|
||||
$options['ajax'] = [
|
||||
'url' => $table->getConfig()->getAjaxUrl(),
|
||||
'type' => $table->getConfig()->getAjaxMethod(),
|
||||
'data' => $table->getConfig()->getAjaxParams(),
|
||||
];
|
||||
$options['columns'] = $table->getColumns()->toArray();
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableInterface $table
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getHtmlTable(DataTableInterface $table)
|
||||
{
|
||||
$columns = $table->getColumns();
|
||||
$headerHtml1 = '';
|
||||
$footerHtml = '';
|
||||
|
||||
/** @var Column $column */
|
||||
foreach ($columns as $column) {
|
||||
$headerHtml1 .= sprintf('<th data-name="%s">%s</th>', $column->getName(), $column->getTitle());
|
||||
if ($column->has('footerHtml')) {
|
||||
$footerHtml .= sprintf('<th data-name="%s">%s</th>', $column->getName(), $column->get('footerHtml'));
|
||||
} else {
|
||||
$footerHtml .= sprintf('<th data-name="%s">%s</th>', $column->getName(), $column->getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
$html = "\n";
|
||||
$html .= sprintf(
|
||||
'<table id="%s" class="%s" width="100%%" data-autoinit="%s">',
|
||||
$table->getConfig()->getTableName(),
|
||||
$table->getConfig()->getCssClassesString(),
|
||||
$table->getConfig()->isAutoInit() ? 'true' : 'false'
|
||||
) . "\n";
|
||||
$html .= '<thead>';
|
||||
$html .= '<tr>' . $headerHtml1 . '</tr>';
|
||||
$html .= '</thead>' . "\n";
|
||||
$html .= '<tfoot><tr>' . $footerHtml . '</tr></tfoot>' . "\n";
|
||||
$html .= '</table>' . "\n";
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Service;
|
||||
|
||||
use Xentral\Components\Http\FileResponse;
|
||||
use Xentral\Components\Http\JsonResponse;
|
||||
use Xentral\Components\Http\Response;
|
||||
use Xentral\Widgets\DataTable\DataTableBuildConfig;
|
||||
use Xentral\Widgets\DataTable\Request\DataTableRequest;
|
||||
|
||||
final class DataTableRequestHandler
|
||||
{
|
||||
/** @var DataTableService $service */
|
||||
private $service;
|
||||
|
||||
/** @var DataTableRequest $request */
|
||||
private $request;
|
||||
|
||||
/**
|
||||
* @param DataTableService $service
|
||||
* @param DataTableRequest $request
|
||||
*/
|
||||
public function __construct(DataTableService $service, DataTableRequest $request)
|
||||
{
|
||||
$this->service = $service;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $config
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateHtml(DataTableBuildConfig $config)
|
||||
{
|
||||
return $this->service->renderHtml($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $config
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canHandleRequest(DataTableBuildConfig $config)
|
||||
{
|
||||
if ($this->request->getMethod() !== $config->getAjaxMethod()) {
|
||||
return false;
|
||||
}
|
||||
if ($this->request->isDataRequest()) {
|
||||
return true;
|
||||
}
|
||||
if ($this->request->isExportRequest()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $config
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function handleRequest(DataTableBuildConfig $config)
|
||||
{
|
||||
if ($this->request->isDataRequest()) {
|
||||
return $this->handleDataRequest($config);
|
||||
}
|
||||
if ($this->request->isExportRequest()) {
|
||||
return $this->handleExportRequest($config);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'error' => 'Can not fetch data from datatable. This is not a valid request.',
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $config
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
private function handleExportRequest(DataTableBuildConfig $config)
|
||||
{
|
||||
$filePath = $this->service->exportData($config);
|
||||
|
||||
return FileResponse::createFromFile($filePath, 'export.csv', 'text/csv', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $config
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
private function handleDataRequest(DataTableBuildConfig $config)
|
||||
{
|
||||
if (!$this->service->canFetchData($config)) {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'error' => 'Can not fetch data from datatable. Build config does not match with request parameters.',
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$result = $this->service->fetchData($config);
|
||||
$status = $result->hasError() ? Response::HTTP_INTERNAL_SERVER_ERROR : Response::HTTP_OK;
|
||||
|
||||
return new JsonResponse($result, $status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Service;
|
||||
|
||||
use Xentral\Widgets\DataTable\DataTableBuildConfig;
|
||||
use Xentral\Widgets\DataTable\DataTableInterface;
|
||||
use Xentral\Widgets\DataTable\Result\DataTableDataResult;
|
||||
use Xentral\Widgets\DataTable\Result\DataTableHtmlResult;
|
||||
|
||||
final class DataTableService
|
||||
{
|
||||
/** @var DataTableBuilder $builder */
|
||||
private $builder;
|
||||
|
||||
/** @var DataTableRenderer $renderer */
|
||||
private $renderer;
|
||||
|
||||
/** @var DataTableFetcher $fetcher */
|
||||
private $fetcher;
|
||||
|
||||
/**
|
||||
* @param DataTableBuilder $builder
|
||||
* @param DataTableRenderer $renderer
|
||||
* @param DataTableFetcher $fetcher
|
||||
*/
|
||||
public function __construct(DataTableBuilder $builder, DataTableRenderer $renderer, DataTableFetcher $fetcher)
|
||||
{
|
||||
$this->builder = $builder;
|
||||
$this->renderer = $renderer;
|
||||
$this->fetcher = $fetcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $buildConfig
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canFetchData(DataTableBuildConfig $buildConfig)
|
||||
{
|
||||
return $this->fetcher->canFetchData($buildConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $buildConfig
|
||||
*
|
||||
* @return DataTableDataResult
|
||||
*/
|
||||
public function fetchData(DataTableBuildConfig $buildConfig)
|
||||
{
|
||||
$dataTable = $this->buildTable($buildConfig);
|
||||
|
||||
return $this->fetcher->fetchData($dataTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $buildConfig
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canExportData(DataTableBuildConfig $buildConfig)
|
||||
{
|
||||
return $this->fetcher->canExportData($buildConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $buildConfig
|
||||
*
|
||||
* @return string Path to temporary file
|
||||
*/
|
||||
public function exportData(DataTableBuildConfig $buildConfig)
|
||||
{
|
||||
$dataTable = $this->buildTable($buildConfig);
|
||||
|
||||
return $this->fetcher->exportData($dataTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $buildConfig
|
||||
*
|
||||
* @return DataTableHtmlResult
|
||||
*/
|
||||
public function renderHtml(DataTableBuildConfig $buildConfig)
|
||||
{
|
||||
$dataTable = $this->buildTable($buildConfig);
|
||||
|
||||
return $this->renderer->createHtmlResult($dataTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTableBuildConfig $buildConfig
|
||||
*
|
||||
* @return DataTableInterface
|
||||
*/
|
||||
private function buildTable(DataTableBuildConfig $buildConfig)
|
||||
{
|
||||
return $this->builder->buildTable($buildConfig);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Type;
|
||||
|
||||
use Xentral\Components\Database\SqlQuery\SelectQuery;
|
||||
use Xentral\Widgets\DataTable\Column\ColumnCollection;
|
||||
use Xentral\Widgets\DataTable\Feature\DataTableFeatureInterface;
|
||||
use Xentral\Widgets\DataTable\Feature\FeatureCollection;
|
||||
use Xentral\Widgets\DataTable\Feature\ResponsiveFeature;
|
||||
use Xentral\Widgets\DataTable\Feature\StateSaveFeature;
|
||||
use Xentral\Widgets\DataTable\Feature\TableControlFeature;
|
||||
use Xentral\Widgets\DataTable\Feature\TableStylingFeature;
|
||||
use Xentral\Widgets\DataTable\Filter\FilterCollection;
|
||||
use Xentral\Widgets\DataTable\Options\DataTableOptions;
|
||||
|
||||
abstract class AbstractDataTableType implements DataTableTypeInterface
|
||||
{
|
||||
/**
|
||||
* @param DataTableOptions $options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function configureOptions(DataTableOptions $options)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SelectQuery $query
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function configureQuery(SelectQuery $query)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ColumnCollection $columns
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function configureColumns(ColumnCollection $columns)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FeatureCollection $features
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function configureFeatures(FeatureCollection $features)
|
||||
{
|
||||
$this->addDefaultFeatures($features);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FilterCollection $filters
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function configureFilters(FilterCollection $filters)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FeatureCollection $featureCollection
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addDefaultFeatures(FeatureCollection $featureCollection)
|
||||
{
|
||||
foreach ($this->getDefaultFeatures() as $defaultFeature) {
|
||||
$defaultFeatureClassName = get_class($defaultFeature);
|
||||
if (!$featureCollection->has($defaultFeatureClassName)) {
|
||||
$featureCollection->add($defaultFeature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataTableFeatureInterface[]|array
|
||||
*/
|
||||
private function getDefaultFeatures()
|
||||
{
|
||||
return [
|
||||
new StateSaveFeature($enabled = true, $duration = 0),
|
||||
new TableStylingFeature($compact = false, $noWrap = false),
|
||||
new TableControlFeature(),
|
||||
new ResponsiveFeature(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\DataTable\Type;
|
||||
|
||||
use Xentral\Components\Database\SqlQuery\SelectQuery;
|
||||
use Xentral\Widgets\DataTable\Column\ColumnCollection;
|
||||
use Xentral\Widgets\DataTable\Options\DataTableOptions;
|
||||
use Xentral\Widgets\DataTable\Feature\FeatureCollection;
|
||||
use Xentral\Widgets\DataTable\Filter\FilterCollection;
|
||||
|
||||
interface DataTableTypeInterface
|
||||
{
|
||||
/** @var string|null PARENT_TABLE */
|
||||
const PARENT_TABLE = null;
|
||||
|
||||
/**
|
||||
* @param DataTableOptions $options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function configureOptions(DataTableOptions $options);
|
||||
|
||||
/**
|
||||
* @param SelectQuery $query
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function configureQuery(SelectQuery $query);
|
||||
|
||||
/**
|
||||
* @param ColumnCollection $columns
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function configureColumns(ColumnCollection $columns);
|
||||
|
||||
/**
|
||||
* @param FeatureCollection $features
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function configureFeatures(FeatureCollection $features);
|
||||
|
||||
/**
|
||||
* @param FilterCollection $filters
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function configureFilters(FilterCollection $filters);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# DataTables
|
||||
|
||||
## Annahmen
|
||||
|
||||
* Nur falls Zeilen selektiert werden sollen:
|
||||
* In jeder SQL-Abfrage muss die erste Spalte eine eindeutige ID zurückliefern.
|
||||
* Diese Spalte muss den Namen `id` bekommen.
|
||||
* Diese Spalte muss nicht als `Column` definiert werden.
|
||||
|
||||
* Jede Tabellenspalte benötigt einen eindeutigen Namen; für die Zuordnung von Filtern.
|
||||
* Dieser Name korrespondiert mit einem SQL-Spaltennamen bzw. dem Alias. Beispiel:
|
||||
`SELECT projekt.abkuerzung AS projekt_name ... ` dann muss der Spaltenname `projekt_name` und nicht `abkuerzung` heißen.
|
||||
|
||||
* Jede DataTable benötigt einen eindeutigen Namen.
|
||||
* Für die Zuordnung von Filtern.
|
||||
* Eindeutiger Name wird aus Klassenname generiert, wenn kein Name in der BuildConfig angegeben wird.
|
||||
|
||||
## Spaltenarten
|
||||
|
||||
Siehe `\Xentral\Widgets\DataTable\Column\Column` Klasse.
|
||||
|
||||
##### `Column::visible($name, $title, $align = 'left', $width = null)`
|
||||
* Sichtbar
|
||||
* Nicht sortierbar
|
||||
* Nicht durchsuchbar
|
||||
|
||||
##### `Column::sortable($name, $title, $align = 'left', $width = null)`
|
||||
* Sichtbar
|
||||
* Sortierbar
|
||||
* Nicht durchsuchbar
|
||||
|
||||
##### `Column::searchable($name, $title, $align = 'left', $width = null)`
|
||||
* Sichtbar
|
||||
* Sortierbar
|
||||
* Durchsuchbar
|
||||
|
||||
##### `Column::fixed($name, $title, $align = 'left', $width = null)`
|
||||
* Sichtbar
|
||||
* Nicht sortierbar
|
||||
* Nicht durchsuchbar
|
||||
* Für Menü-Spalten und Zeilen-Selektion
|
||||
|
||||
##### `Column::hidden($name, $title, $align = 'left', $width = null)`
|
||||
* Initial ausgeblendet; kann eingeblendet werden (Feature noch nicht implementiert)
|
||||
* Nicht sortierbar
|
||||
* Nicht durchsuchbar
|
||||
|
||||
|
||||
## Aufbau
|
||||
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Vorlage
|
||||
|
||||
```php
|
||||
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Attachment;
|
||||
|
||||
abstract class AbstractAttachment implements AttachmentInterface
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getType();
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
abstract public function getData();
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return [
|
||||
'type' => $this->getType(),
|
||||
'data' => $this->getData(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Attachment;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
interface AttachmentInterface extends JsonSerializable
|
||||
{
|
||||
/** @var string TYPE_BUTTON_BLOCK */
|
||||
const TYPE_BUTTON_BLOCK = 'button_block';
|
||||
|
||||
/** @var string TYPE_CONTENT_STATIC */
|
||||
const TYPE_CONTENT_STATIC = 'content_static';
|
||||
|
||||
/** @var string TYPE_CONTENT_DYNAMIC */
|
||||
const TYPE_CONTENT_DYNAMIC = 'content_dynamic';
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType();
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getData();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Attachment;
|
||||
|
||||
final class ButtonBlockAttachment extends AbstractAttachment
|
||||
{
|
||||
/** @var array $buttons */
|
||||
private $buttons = [];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return self::TYPE_BUTTON_BLOCK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return $this->buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Attribute validieren
|
||||
*
|
||||
* @param string $title
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addButton($title, array $attributes = [])
|
||||
{
|
||||
$this->buttons[] = [
|
||||
'title' => (string)$title,
|
||||
'attributes' => !empty($attributes) ? $attributes : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Attachment;
|
||||
|
||||
final class DynamicContentAttachment extends AbstractAttachment
|
||||
{
|
||||
/** @var string $ajaxUrl */
|
||||
private $ajaxUrl;
|
||||
|
||||
/** @var array $postParams */
|
||||
private $postParams = [];
|
||||
|
||||
/**
|
||||
* @param string $ajaxUrl
|
||||
* @param array $postParams
|
||||
*/
|
||||
public function __construct($ajaxUrl, array $postParams = [])
|
||||
{
|
||||
$this->ajaxUrl = (string)$ajaxUrl;
|
||||
$this->postParams = $postParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return self::TYPE_CONTENT_DYNAMIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return [
|
||||
'url' => $this->ajaxUrl,
|
||||
'params' => $this->postParams,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Attachment;
|
||||
|
||||
final class StaticContentAttachment extends AbstractAttachment
|
||||
{
|
||||
/** @var string $content */
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
*/
|
||||
public function __construct($content)
|
||||
{
|
||||
$this->content = (string)$content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return self::TYPE_CONTENT_STATIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return [
|
||||
'content' => $this->content,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerJavascript()
|
||||
{
|
||||
return [
|
||||
'supersearch' => [
|
||||
'./classes/Widgets/SuperSearch/www/js/supersearch.js',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerStylesheets()
|
||||
{
|
||||
return [
|
||||
'supersearch' => [
|
||||
'./classes/Widgets/SuperSearch/www/css/supersearch.css',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Exception;
|
||||
|
||||
use InvalidArgumentException as SplInvalidArgumentException;
|
||||
|
||||
class InvalidArgumentException extends SplInvalidArgumentException implements SuperSearchExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Exception;
|
||||
|
||||
use Xentral\Core\Exception\WidgetExceptionInterface;
|
||||
|
||||
interface SuperSearchExceptionInterface extends WidgetExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Query;
|
||||
|
||||
use Xentral\Widgets\SuperSearch\Exception\InvalidArgumentException;
|
||||
|
||||
final class DetailQuery
|
||||
{
|
||||
/** @var string $groupKey */
|
||||
private $groupKey;
|
||||
|
||||
/** @var int|string $itemIdentifier */
|
||||
private $itemIdentifier;
|
||||
|
||||
/**
|
||||
* @param string $groupKey
|
||||
* @param int|string $itemIdentifier
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($groupKey, $itemIdentifier)
|
||||
{
|
||||
$groupKeyCleaned = (string)preg_replace('#[^a-z_]#', '', $groupKey);
|
||||
if ($groupKey !== $groupKeyCleaned) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid characters in $groupKey. Allowed characters: a-z and underscore.'
|
||||
);
|
||||
}
|
||||
if (!is_int($itemIdentifier) && !is_string($itemIdentifier)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Invalid argument type for $itemIdentifier. Only integer or string is allowed. Type given: %s.',
|
||||
gettype($itemIdentifier)
|
||||
));
|
||||
}
|
||||
|
||||
$this->groupKey = (string)$groupKey;
|
||||
$this->itemIdentifier = $itemIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getGroupKey()
|
||||
{
|
||||
return $this->groupKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|string
|
||||
*/
|
||||
public function getItemIdentifier()
|
||||
{
|
||||
return $this->itemIdentifier;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Query;
|
||||
|
||||
use Xentral\Widgets\SuperSearch\Exception\InvalidArgumentException;
|
||||
|
||||
final class SearchQuery
|
||||
{
|
||||
/** @var string $searchTerm */
|
||||
private $searchTerm;
|
||||
|
||||
/** @var array $searchWords */
|
||||
private $searchWords = null;
|
||||
|
||||
/**
|
||||
* @param string $searchTerm
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($searchTerm)
|
||||
{
|
||||
if (empty($searchTerm)) {
|
||||
throw new InvalidArgumentException('Parameter "searchTerm" is empty.');
|
||||
}
|
||||
|
||||
$this->searchTerm = trim($searchTerm);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSearchTerm()
|
||||
{
|
||||
return $this->searchTerm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getSearchWords()
|
||||
{
|
||||
if ($this->searchWords === null) {
|
||||
$this->searchWords = (array)preg_split('/([\s]+)/um', $this->searchTerm, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
|
||||
return $this->searchWords;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Result;
|
||||
|
||||
use DateTimeInterface;
|
||||
use JsonSerializable as JsonSerializableAlias;
|
||||
|
||||
final class ResultCollection implements JsonSerializableAlias
|
||||
{
|
||||
/** @var ResultGroup[]|array $groups */
|
||||
private $groups;
|
||||
|
||||
/** @var DateTimeInterface|null $lastIndexUpdateTime */
|
||||
private $lastIndexUpdateTime;
|
||||
|
||||
/**
|
||||
* @param ResultGroup[]|array $resultGroups
|
||||
* @param DateTimeInterface|null $lastIndexUpdate
|
||||
*/
|
||||
public function __construct(array $resultGroups = [], DateTimeInterface $lastIndexUpdate = null)
|
||||
{
|
||||
foreach ($resultGroups as $resultGroup) {
|
||||
$this->addGroup($resultGroup);
|
||||
}
|
||||
$this->lastIndexUpdateTime = $lastIndexUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmpty()
|
||||
{
|
||||
return count($this->groups) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ResultGroup $group
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addGroup(ResultGroup $group)
|
||||
{
|
||||
$this->groups[] = $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $groupKey
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasGroup($groupKey)
|
||||
{
|
||||
foreach ($this->groups as $group) {
|
||||
if ($groupKey === $group->getKey()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $groupKey
|
||||
*
|
||||
* @return ResultGroup|null
|
||||
*/
|
||||
public function getGroup($groupKey)
|
||||
{
|
||||
foreach ($this->groups as $group) {
|
||||
if ($groupKey === $group->getKey()) {
|
||||
return $group;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DateTimeInterface|null
|
||||
*/
|
||||
public function getLastIndexUpdateTime()
|
||||
{
|
||||
return $this->lastIndexUpdateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$results = [];
|
||||
$itemCount = 0;
|
||||
foreach ($this->groups as $group) {
|
||||
$itemCount += $group->countItems();
|
||||
$index = $group->getKey();
|
||||
$results[$index] = $group;
|
||||
}
|
||||
|
||||
return [
|
||||
'count' => $itemCount,
|
||||
'results' => $results,
|
||||
'last_index_update_rfc2822' =>
|
||||
$this->lastIndexUpdateTime !== null ? $this->lastIndexUpdateTime->format(DATE_RFC2822) : null,
|
||||
'last_index_update_formatted' =>
|
||||
$this->lastIndexUpdateTime !== null ? $this->lastIndexUpdateTime->format('d.m.Y H:i') . ' Uhr' : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Result;
|
||||
|
||||
use JsonSerializable;
|
||||
use Xentral\Widgets\SuperSearch\Attachment\ButtonBlockAttachment;
|
||||
use Xentral\Widgets\SuperSearch\Attachment\DynamicContentAttachment;
|
||||
use Xentral\Widgets\SuperSearch\Attachment\StaticContentAttachment;
|
||||
use Xentral\Widgets\SuperSearch\Exception\InvalidArgumentException;
|
||||
|
||||
final class ResultDetail implements JsonSerializable
|
||||
{
|
||||
/** @var string $title */
|
||||
private $title = '';
|
||||
|
||||
/** @var ButtonBlockAttachment|null $buttons */
|
||||
private $buttons;
|
||||
|
||||
/** @var StaticContentAttachment|null $staticDescription */
|
||||
private $staticDescription;
|
||||
|
||||
/** @var DynamicContentAttachment|null $dynamicDescription */
|
||||
private $dynamicDescription;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTitle()
|
||||
{
|
||||
return !empty($this->title);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
*/
|
||||
public function setTitle($title)
|
||||
{
|
||||
if (empty($title)) {
|
||||
throw new InvalidArgumentException('Required parameter "title" is empty.');
|
||||
}
|
||||
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beschreibung setzen; MiniDetail wird dann geleert!
|
||||
*
|
||||
* @param string $description
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
if (empty($description)) {
|
||||
throw new InvalidArgumentException('Required parameter $description is empty.');
|
||||
}
|
||||
|
||||
$this->staticDescription = new StaticContentAttachment($description);
|
||||
$this->dynamicDescription = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* MiniDetail-URL setzen; Beschreibung wird dann geleert!
|
||||
*
|
||||
* @param string $miniDetailUrl
|
||||
* @param array $postParams
|
||||
*/
|
||||
public function setMiniDetailUrl($miniDetailUrl, array $postParams = [])
|
||||
{
|
||||
if (empty($miniDetailUrl)) {
|
||||
throw new InvalidArgumentException('Required parameter $miniDetailUrl is empty.');
|
||||
}
|
||||
|
||||
$this->dynamicDescription = new DynamicContentAttachment($miniDetailUrl, $postParams);
|
||||
$this->staticDescription = null; // Entweder MiniDetail oder Beschreibungstext
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
* @param string|null $href Hyperlink reference
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addButton($title, $href = null, array $attributes = [])
|
||||
{
|
||||
if ($this->buttons === null) {
|
||||
$this->buttons = new ButtonBlockAttachment();
|
||||
}
|
||||
|
||||
if ($href !== null) {
|
||||
$attributes['href'] = (string)$href;
|
||||
}
|
||||
|
||||
$this->buttons->addButton($title, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid()
|
||||
{
|
||||
return $this->hasTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|false
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
if (!$this->isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$attachments = [];
|
||||
if ($this->buttons !== null) {
|
||||
$attachments[] = $this->buttons;
|
||||
}
|
||||
if ($this->staticDescription !== null) {
|
||||
$attachments[] = $this->staticDescription;
|
||||
}
|
||||
if ($this->dynamicDescription !== null) {
|
||||
$attachments[] = $this->dynamicDescription;
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $this->title,
|
||||
'attachments' => $attachments,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Result;
|
||||
|
||||
final class ResultGroup implements \JsonSerializable
|
||||
{
|
||||
/** @var string $key */
|
||||
private $key;
|
||||
|
||||
/** @var string $title */
|
||||
private $title;
|
||||
|
||||
/** @var ResultItem[]|array $items */
|
||||
private $items = [];
|
||||
|
||||
/**
|
||||
* @param string $groupKey
|
||||
* @param string $groupTitle
|
||||
* @param ResultItem[]|array $resultItems
|
||||
*/
|
||||
public function __construct($groupKey, $groupTitle, array $resultItems = [])
|
||||
{
|
||||
$this->key = $groupKey;
|
||||
$this->title = $groupTitle;
|
||||
|
||||
foreach ($resultItems as $resultItem) {
|
||||
$this->addItem($resultItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ResultItem $item
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addItem(ResultItem $item)
|
||||
{
|
||||
$this->items[] = $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ResultItem[]|array
|
||||
*/
|
||||
public function getItems()
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function countItems()
|
||||
{
|
||||
return count($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return [
|
||||
'key' => $this->key,
|
||||
'title' => $this->title,
|
||||
'count' => count($this->items),
|
||||
'items' => $this->items,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Widgets\SuperSearch\Result;
|
||||
|
||||
use JsonSerializable;
|
||||
use Xentral\Widgets\SuperSearch\Exception\InvalidArgumentException;
|
||||
|
||||
final class ResultItem implements JsonSerializable
|
||||
{
|
||||
/** @var int|string $identifier */
|
||||
private $identifier;
|
||||
|
||||
/** @var string $title */
|
||||
private $title;
|
||||
|
||||
/** @var string $link */
|
||||
private $link;
|
||||
|
||||
/** @var string|null $subTitle */
|
||||
private $subTitle;
|
||||
|
||||
/** @var array|string[] $additionalInfos */
|
||||
private $additionalInfos = [];
|
||||
|
||||
/**
|
||||
* @param int|string $identifier Database-ID or unique keyword
|
||||
* @param string $title
|
||||
* @param string $link
|
||||
* @param string|null $subTitle
|
||||
* @param array|null $additionalInfos
|
||||
*/
|
||||
public function __construct($identifier, $title, $link, $subTitle = null, array $additionalInfos = null)
|
||||
{
|
||||
if (empty($identifier)) {
|
||||
throw new InvalidArgumentException('Parameter "id" is empty.');
|
||||
}
|
||||
if (empty($title)) {
|
||||
throw new InvalidArgumentException('Parameter "title" is empty.');
|
||||
}
|
||||
if (empty($link)) {
|
||||
throw new InvalidArgumentException('Parameter "link" is empty.');
|
||||
}
|
||||
|
||||
$this->identifier = $identifier;
|
||||
$this->title = (string)$title;
|
||||
$this->link = (string)$link;
|
||||
|
||||
$subTitle = trim($subTitle);
|
||||
if ($subTitle !== '') {
|
||||
$this->subTitle = $subTitle;
|
||||
}
|
||||
|
||||
$additionalInfos = (array)$additionalInfos;
|
||||
foreach ($additionalInfos as $additionalInfo) {
|
||||
$additionalInfo = trim($additionalInfo);
|
||||
if ($additionalInfo !== '') {
|
||||
$this->additionalInfos[] = $additionalInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $state
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function fromDbState(array $state)
|
||||
{
|
||||
$additionalInfos = explode(' ## ' , $state['additional_infos']);
|
||||
|
||||
return new self($state['index_id'], $state['title'], $state['link'], $state['subtitle'], $additionalInfos);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getIdentifier()
|
||||
{
|
||||
return $this->identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLink()
|
||||
{
|
||||
return $this->link;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getSubTitle()
|
||||
{
|
||||
return $this->subTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getAdditionalInfos()
|
||||
{
|
||||
return $this->additionalInfos;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return [
|
||||
'type' => 'default',
|
||||
'identifier' => $this->identifier,
|
||||
'title' => $this->title,
|
||||
'link' => $this->link,
|
||||
'subtitle' => $this->subTitle,
|
||||
'additionalInfos' => !empty($this->additionalInfos) ? $this->additionalInfos : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
SuperSearch
|
||||
*/
|
||||
#supersearch-overlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 994;
|
||||
top: 55px;
|
||||
left: 18px;
|
||||
width: 250px;
|
||||
height: 440px;
|
||||
background-color: var(--body-background);
|
||||
box-shadow: 3px 3px 10px rgba(0, 0, 0, .33);
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#supersearch-overlay.has-detail {
|
||||
width: 890px;
|
||||
}
|
||||
|
||||
#supersearch-overlay #supersearch-icon-close {
|
||||
z-index: 981;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-image: url('../themes/new/images/icon-close.svg');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 80%;
|
||||
}
|
||||
|
||||
#supersearch-overlay .result-wrapper {
|
||||
overflow: auto;
|
||||
width: 250px;
|
||||
height: 416px;
|
||||
background-color: var(--body-background);
|
||||
}
|
||||
#supersearch-overlay .detail-wrapper {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 250px;
|
||||
width: 640px;
|
||||
height: 440px;
|
||||
overflow-y: auto;
|
||||
background-color: var(--fieldset);
|
||||
}
|
||||
#supersearch-overlay.has-detail .detail-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#supersearch-overlay .search {
|
||||
display: none;
|
||||
height: 45px;
|
||||
padding: 10px;
|
||||
background-color: var(--body-background);
|
||||
}
|
||||
|
||||
#supersearch-overlay .empty-message,
|
||||
#supersearch-overlay .error-message {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
}
|
||||
#supersearch-overlay .error-message {
|
||||
font-weight: bold;
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
#supersearch-overlay .result {
|
||||
display: block;
|
||||
padding: 0;
|
||||
background-color: var(--body-background);
|
||||
}
|
||||
|
||||
#supersearch-overlay .last-update {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 250px;
|
||||
padding: 6px 12px;
|
||||
font-size: 10px;
|
||||
color: var(--text-color);
|
||||
background-color: var(--fieldset-dark);
|
||||
}
|
||||
|
||||
#supersearch-overlay .detail {
|
||||
padding: 0;
|
||||
margin: 10px 28px;
|
||||
line-height: 1.428;
|
||||
}
|
||||
|
||||
#supersearch-overlay .detail img {
|
||||
max-width: 80%;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
#supersearch-overlay .detail .minidetail {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#supersearch-overlay .result-empty {
|
||||
padding: 4px 6px 3px 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
#supersearch-overlay .result-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
#supersearch-overlay .result-head,
|
||||
#supersearch-overlay .result-foot,
|
||||
#supersearch-overlay .result-item {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#supersearch-overlay .result-head {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#supersearch-overlay .result-foot {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#supersearch-overlay .result-item {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#supersearch-overlay .result-head,
|
||||
#supersearch-overlay .result-foot {
|
||||
padding: 4px 6px 3px 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
#supersearch-overlay .result-head {
|
||||
background-color: var(--fieldset-dark);
|
||||
}
|
||||
|
||||
#supersearch-overlay .result-item a,
|
||||
#supersearch-overlay .result-item a:link,
|
||||
#supersearch-overlay .result-item a:visited,
|
||||
#supersearch-overlay .result-item a:hover,
|
||||
#supersearch-overlay .result-item a:active {
|
||||
display: block;
|
||||
padding: 3px 6px 3px 12px;
|
||||
}
|
||||
#supersearch-overlay .result-item a:hover {
|
||||
color: var(--grey);
|
||||
background-color: rgba(0, 0, 0, .15);
|
||||
}
|
||||
#supersearch-overlay .result-item.active {
|
||||
color: var(--grey);
|
||||
background-color: rgba(0, 0, 0, .15);
|
||||
}
|
||||
#supersearch-overlay .result-item .title,
|
||||
#supersearch-overlay .result-item .caption {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
#supersearch-overlay .result-item .caption {
|
||||
font-size: .9em;
|
||||
color: var(--text-color);
|
||||
margin-top: 2px;
|
||||
}
|
||||
#supersearch-overlay .result-item .title span,
|
||||
#supersearch-overlay .result-item .caption span {
|
||||
flex: 1 33%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 2px;
|
||||
}
|
||||
#supersearch-overlay .result-item .caption span:last-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
#supersearch-overlay .result-item .caption span:first-of-type {
|
||||
text-align: left;
|
||||
}
|
||||
#supersearch-overlay .result-item .title .title-main {
|
||||
flex: 1 33%;
|
||||
}
|
||||
#supersearch-overlay .result-item .title .title-sub {
|
||||
flex: 2 66%;
|
||||
}
|
||||
|
||||
#supersearch-overlay .detail h1,
|
||||
#supersearch-overlay .detail h2,
|
||||
#supersearch-overlay .detail h3,
|
||||
#supersearch-overlay .detail h4,
|
||||
#supersearch-overlay .detail h5,
|
||||
#supersearch-overlay .detail h6,
|
||||
#supersearch-overlay .detail p {
|
||||
padding: 0;
|
||||
margin: 8px 0;
|
||||
}
|
||||
#supersearch-overlay .detail h1 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
#supersearch-overlay .detail h2 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
#supersearch-overlay .detail h3 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
#supersearch-overlay .detail h4 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
#supersearch-overlay .detail ul {
|
||||
padding: 0 0 0 20px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
#supersearch-overlay .detail ul ul {
|
||||
margin: 0;
|
||||
}
|
||||
#supersearch-overlay .detail ul li a {
|
||||
display: inline-block;
|
||||
padding: 3px 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#supersearch-overlay .detail .button .icon {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 12px;
|
||||
margin: 0 4px 0 0;
|
||||
}
|
||||
#supersearch-overlay .detail .button .icon img {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
width: 16px;
|
||||
height: 12px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
/** Overlay ausblenden wenn Sidebar und Suchfeld nicht sichtbar */
|
||||
@media screen and (max-width: 1000px) {
|
||||
#supersearch-overlay {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/** Overlay um 200 Pixel verbreitern, wenn genug Platz vorhanden */
|
||||
@media screen and (min-width: 1200px) {
|
||||
#supersearch-overlay.has-detail {
|
||||
width: 1090px;
|
||||
}
|
||||
#supersearch-overlay .detail-wrapper {
|
||||
width: 840px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,642 @@
|
||||
var SuperSearch = (function ($) {
|
||||
'use strict';
|
||||
|
||||
var me = {
|
||||
|
||||
config: {
|
||||
inputBuffer: 300 // in milliseconds
|
||||
},
|
||||
|
||||
storage: {
|
||||
$input: null,
|
||||
$overlay: null,
|
||||
$details: null,
|
||||
$results: null,
|
||||
$lastUpdate: null,
|
||||
debounceBuffer: null,
|
||||
hasResults: false,
|
||||
isOpen: false
|
||||
},
|
||||
|
||||
init: function () {
|
||||
me.storage.$input = $('#supersearch-input');
|
||||
if (me.storage.$input.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
me.registerEvents();
|
||||
},
|
||||
|
||||
registerEvents: function () {
|
||||
me.storage.$input.on('keyup.SuperSearch', me.onKeyUpSearchInput);
|
||||
|
||||
// Overlay anzeigen bei Focus in das Such-Eingabefeld; nur wenn es schon mal geöffnet war
|
||||
me.storage.$input.on('focus.SuperSearch', me.onFocusSearchInput);
|
||||
|
||||
// Overlay mit ESC schließen
|
||||
$(document).bind('keydown', function(e) {
|
||||
if (me.storage.$overlay === null) {
|
||||
return;
|
||||
}
|
||||
if (me.storage.isOpen !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ESC
|
||||
if (e.keyCode === 27) {
|
||||
me.hideOverlay();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {jQuery}
|
||||
*/
|
||||
getOverlay: function () {
|
||||
if (typeof me.storage.$overlay === 'undefined' || me.storage.$overlay === null) {
|
||||
me.storage.$overlay = me.createOverlay();
|
||||
me.storage.$details = me.storage.$overlay.find('section.detail');
|
||||
me.storage.$results = me.storage.$overlay.find('section.result');
|
||||
me.storage.$lastUpdate = me.storage.$overlay.find('section.last-update');
|
||||
}
|
||||
|
||||
return me.storage.$overlay;
|
||||
},
|
||||
|
||||
showOverlay: function () {
|
||||
var $overlay = me.getOverlay();
|
||||
$overlay.show();
|
||||
me.storage.isOpen = true;
|
||||
me.showDetails();
|
||||
},
|
||||
|
||||
hideOverlay: function () {
|
||||
me.getOverlay().hide();
|
||||
me.storage.isOpen = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {jQuery}
|
||||
*/
|
||||
createOverlay: function () {
|
||||
var overlaySelector = '#supersearch-overlay';
|
||||
if ($(overlaySelector).length > 0) {
|
||||
return $(overlaySelector);
|
||||
}
|
||||
|
||||
var overlayTemplate =
|
||||
'<span id="supersearch-icon-close" class="icon icon-close"></span>' +
|
||||
'<div class="result-wrapper">' +
|
||||
'<section class="empty-message">Keine Suchergebnisse gefunden</section>' +
|
||||
'<section class="error-message"></section>' +
|
||||
'<section class="result"></section>' +
|
||||
'<section class="last-update"></section>' +
|
||||
'</div>' +
|
||||
'<div class="detail-wrapper">' +
|
||||
'<section class="detail"></section>' +
|
||||
'</div>';
|
||||
|
||||
var overlayIdAttr = overlaySelector.substr(1);
|
||||
var $overlay = $('<div>').attr('id', overlayIdAttr).addClass('supersearch-overlay').html(overlayTemplate);
|
||||
|
||||
$overlay.off('click.SuperSearch', '#supersearch-icon-close');
|
||||
$overlay.on('click.SuperSearch', '#supersearch-icon-close', function (event) {
|
||||
event.preventDefault();
|
||||
me.hideOverlay();
|
||||
});
|
||||
|
||||
$overlay.hide();
|
||||
$overlay.appendTo('#header');
|
||||
me.storage.isOpen = false;
|
||||
|
||||
return $overlay;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
onKeyUpSearchInput: function (event) {
|
||||
event.preventDefault();
|
||||
var controlKeyCodes = [
|
||||
9, // Tab
|
||||
13, // Enter
|
||||
16, // Shift
|
||||
17, // Strg
|
||||
18, // Alt
|
||||
20, // Caps lock
|
||||
27, // ESC
|
||||
37, // Cursor Left
|
||||
38, // Cursor Up
|
||||
39, // Cursor Right
|
||||
40 // Cursor Down
|
||||
];
|
||||
if ($.inArray(event.keyCode, controlKeyCodes) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
me.debounce(function () {
|
||||
var searchQuery = $(that).val();
|
||||
me.fetchSearchResults(searchQuery).then(me.renderSearchResults);
|
||||
}, me.config.inputBuffer);
|
||||
},
|
||||
|
||||
/**
|
||||
* Overlay anzeigen bei Focus in das Such-Eingabefeld; nur wenn es schon mal geöffnet war
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
onFocusSearchInput: function (event) {
|
||||
event.preventDefault();
|
||||
if (me.storage.$overlay === null) {
|
||||
return;
|
||||
}
|
||||
if (me.storage.hasResults === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
me.showOverlay();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} searchQuery
|
||||
*
|
||||
* @return {jqXHR}
|
||||
*/
|
||||
fetchSearchResults: function (searchQuery) {
|
||||
if (typeof searchQuery !== 'string') {
|
||||
searchQuery = '';
|
||||
}
|
||||
|
||||
return $.ajax({
|
||||
url: 'index.php?module=supersearch&action=ajax&cmd=search',
|
||||
method: 'post',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
search_query: searchQuery
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
var errorMessage = 'SuperSearch - Unbekannter Fehler #31: ' + errorThrown;
|
||||
|
||||
// PHP-Skript hat Fehler geliefert (z.b. 404)
|
||||
if (textStatus === 'error') {
|
||||
errorMessage = 'SuperSearch - Unbekannter Server-Fehler beim Laden der Such-Ergebnisse: ';
|
||||
errorMessage += errorThrown;
|
||||
}
|
||||
|
||||
// PHP-Skript liefert JSON-Error-Response
|
||||
if (jqXHR.hasOwnProperty('responseJSON') && jqXHR.responseJSON.hasOwnProperty('error')) {
|
||||
errorMessage = 'SuperSearch - Server-Fehler beim Laden der Such-Ergebnisse: ';
|
||||
errorMessage += jqXHR.responseJSON.error;
|
||||
|
||||
if (jqXHR.responseJSON.hasOwnProperty('data') &&
|
||||
jqXHR.responseJSON.data === 'index-empty') {
|
||||
me.showErrorMessage('Fehler: ' + jqXHR.responseJSON.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
alert(errorMessage);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Array} rawResult
|
||||
*/
|
||||
renderSearchResults: function (rawResult) {
|
||||
var $overlay = me.getOverlay();
|
||||
var $resultContainer = $overlay.find('section.result');
|
||||
$resultContainer.html('');
|
||||
|
||||
if (rawResult.length === 0 || !rawResult.hasOwnProperty('data')) {
|
||||
$resultContainer.html('Fehler: Suche hat fehlerhaftes Ergebnis geliefert.');
|
||||
me.storage.hasResults = false;
|
||||
me.hideResults();
|
||||
return;
|
||||
}
|
||||
|
||||
// Overlay ausblenden, wenn Suchbegriff zu kurz
|
||||
if (rawResult.data === null) {
|
||||
me.storage.hasResults = false;
|
||||
me.hideOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
// Anzeigen wann der Such-Index das letzte Mal aktualisiert wurde
|
||||
if (rawResult.data.hasOwnProperty('last_index_update_formatted')) {
|
||||
if (rawResult.data.last_index_update_formatted !== null) {
|
||||
var lastIndexUpdate = rawResult.data.last_index_update_formatted;
|
||||
me.storage.$lastUpdate.text('Such-Index vom ' + lastIndexUpdate).show();
|
||||
} else {
|
||||
me.storage.$lastUpdate.text('').hide();
|
||||
}
|
||||
}
|
||||
|
||||
var resultCount = rawResult.data.count;
|
||||
var searchResults = rawResult.data.results;
|
||||
if (resultCount === 0) {
|
||||
me.storage.hasResults = false;
|
||||
me.showEmptyResults();
|
||||
return;
|
||||
}
|
||||
|
||||
me.storage.$details.html('');
|
||||
Object.keys(searchResults).forEach(function (group) {
|
||||
var groupResult = searchResults[group];
|
||||
var $groupHtml = me.buildGroupResult(groupResult.key, groupResult.title, groupResult.items);
|
||||
$resultContainer.append($groupHtml);
|
||||
});
|
||||
|
||||
me.storage.hasResults = true;
|
||||
me.showResults();
|
||||
me.showOverlay();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} groupKey
|
||||
* @param {string} groupTitle
|
||||
* @param {array} items
|
||||
*
|
||||
* @return {jQuery}
|
||||
*/
|
||||
buildGroupResult: function (groupKey, groupTitle, items) {
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (typeof groupTitle === 'undefined') {
|
||||
groupTitle = 'Ergebnis';
|
||||
}
|
||||
|
||||
var $resultWrapper = $('<div class="result-group">');
|
||||
var $resultList = $('<ul class="result-list">');
|
||||
var $listHead = $('<li class="result-head">').html(groupTitle);
|
||||
|
||||
$resultList.append($listHead);
|
||||
items.forEach(function (item) {
|
||||
item.group = groupKey;
|
||||
var itemType = item.type !== null ? item.type : 'default';
|
||||
var $listItem;
|
||||
|
||||
switch (itemType) {
|
||||
case 'default':
|
||||
default:
|
||||
$listItem = me.buildDefaultItemResult(item);
|
||||
break;
|
||||
}
|
||||
|
||||
$resultList.append($listItem);
|
||||
});
|
||||
$resultWrapper.append($resultList);
|
||||
|
||||
return $resultWrapper;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} item
|
||||
*
|
||||
* @return {jQuery}
|
||||
*/
|
||||
buildDefaultItemResult: function (item) {
|
||||
var hasSubtitle = item.hasOwnProperty('subtitle') && typeof item.subtitle === 'string';
|
||||
var hasAdditionalInfos =
|
||||
item.hasOwnProperty('additionalInfos') &&
|
||||
typeof item.additionalInfos === 'object' &&
|
||||
item.additionalInfos !== null;
|
||||
|
||||
var mainTitle = '<span class="title-main">' + item.title + '</span>';
|
||||
var subTitle = hasSubtitle ? '<span class="title-sub">' + item.subtitle + '</span>' : '';
|
||||
var titleString = '<span class="title">' + mainTitle + subTitle + '</span>';
|
||||
|
||||
if (hasAdditionalInfos) {
|
||||
titleString += '<span class="caption">';
|
||||
$.each(item.additionalInfos, function (index, additionalInfo) {
|
||||
titleString += '<span class="additional">' + additionalInfo + '</span>';
|
||||
});
|
||||
titleString += '</span>';
|
||||
}
|
||||
|
||||
var $listItem = $('<li>').addClass('result-item');
|
||||
var $itemLink = $('<a>').attr('href', item.link).html(titleString);
|
||||
$itemLink.appendTo($listItem);
|
||||
|
||||
$itemLink.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
me.renderItemDetails(item);
|
||||
});
|
||||
|
||||
return $listItem;
|
||||
},
|
||||
|
||||
/**
|
||||
* Rendert Ergebnisdetails
|
||||
*
|
||||
* @param {object} item
|
||||
*/
|
||||
renderItemDetails: function (item) {
|
||||
// Per AJAX ausführliche Inhalte nachladen
|
||||
me.fetchItemDetailsDynamicContent(item).then(
|
||||
function (data) {
|
||||
me.renderItemDetailsDynamicContent(data, item);
|
||||
},
|
||||
function (jqXhr) {
|
||||
var error =
|
||||
typeof jqXhr.responseJSON !== 'undefined' &&
|
||||
typeof jqXhr.responseJSON.error !== 'undefined'
|
||||
? jqXhr.responseJSON.error
|
||||
: 'Unbekannter Fehler';
|
||||
alert('Fehler beim Laden der Detail-Informationen: ' + error);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} detailResult
|
||||
* @param {object} listItem Originales Item-Objekt aus Suchergebnis-Liste
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
renderItemDetailsDynamicContent: function (detailResult, listItem) {
|
||||
if (!detailResult.hasOwnProperty('data') || detailResult.data === false) {
|
||||
// Es wurde kein Detail-Result gefunden
|
||||
// Link aus Suchergebnis-Item aufrufen
|
||||
me.hideDetails();
|
||||
window.location.href = listItem.link;
|
||||
return;
|
||||
}
|
||||
|
||||
var detail = detailResult.data;
|
||||
var $details = me.storage.$details;
|
||||
|
||||
// Überschrift rendern
|
||||
var $headline = $('<h1>').html(detail.title);
|
||||
$details.html('').append($headline);
|
||||
|
||||
// Attachments (z.B. Buttons) rendern
|
||||
if (detail.hasOwnProperty('attachments')) {
|
||||
var $attachments = me.generateDetailAttachments(detail.attachments);
|
||||
$details.append($attachments);
|
||||
}
|
||||
|
||||
me.showDetails();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} item
|
||||
*
|
||||
* @return {jqXHR}
|
||||
*/
|
||||
fetchItemDetailsDynamicContent: function (item) {
|
||||
return $.ajax({
|
||||
url: 'index.php?module=supersearch&action=ajax&cmd=detail',
|
||||
method: 'post',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
detail_group: item.group,
|
||||
detail_identifier: item.identifier
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
var errorMessage = 'SuperSearch - Unbekannter Fehler #32: ' + errorThrown;
|
||||
|
||||
// PHP-Skript hat Fehler geliefert (z.b. 404)
|
||||
if (textStatus === 'error') {
|
||||
errorMessage = 'SuperSearch - Unbekannter Server-Fehler beim Laden des Detail-Ergebnisses: ';
|
||||
errorMessage += errorThrown;
|
||||
}
|
||||
|
||||
// PHP-Skript liefer JSON-Error-Response
|
||||
if (jqXHR.hasOwnProperty('responseJSON') && jqXHR.responseJSON.hasOwnProperty('error')) {
|
||||
errorMessage = 'SuperSearch - Server-Fehler beim Laden des Detail-Ergebnisses: ';
|
||||
errorMessage += jqXHR.responseJSON.error;
|
||||
}
|
||||
|
||||
alert(errorMessage);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Array} attachments
|
||||
*
|
||||
* @return {jQuery} jQuery-Element
|
||||
*/
|
||||
generateDetailAttachments: function (attachments) {
|
||||
var $attachments = $('<div>');
|
||||
|
||||
$.each(attachments, function (index, attachment) {
|
||||
if (!attachment.hasOwnProperty('type')) {
|
||||
console.error('Attachment ungültig. "type"-Property fehlt.');
|
||||
return;
|
||||
}
|
||||
if (!attachment.hasOwnProperty('data')) {
|
||||
console.error('Attachment ungültig. "data"-Property fehlt.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (attachment.type === 'button_block') {
|
||||
var $buttonBlock = me.generateDetailAttachmentTypeButtonBlock(attachment.data);
|
||||
$attachments.append($buttonBlock);
|
||||
}
|
||||
if (attachment.type === 'content_static') {
|
||||
var $contentStatic = me.generateDetailAttachmentTypeStaticContent(attachment.data);
|
||||
$attachments.append($contentStatic);
|
||||
}
|
||||
if (attachment.type === 'content_dynamic') {
|
||||
var $contentDynamic = me.generateDetailAttachmentTypeDynamicContent(attachment.data);
|
||||
$attachments.append($contentDynamic);
|
||||
}
|
||||
});
|
||||
|
||||
return $attachments;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Array} items
|
||||
*
|
||||
* @return {jQuery} jQuery-Element
|
||||
*/
|
||||
generateDetailAttachmentTypeButtonBlock: function (items) {
|
||||
var $buttonBlock = $('<div>');
|
||||
|
||||
$.each(items, function (index, item) {
|
||||
var $button = $('<a>').text(item.title).addClass('button');
|
||||
if (item.hasOwnProperty('attributes')) {
|
||||
|
||||
// Button-Attribute verarbeiten
|
||||
$.each(item.attributes, function (attrName, attrValue) {
|
||||
if (attrName === 'class') {
|
||||
$button.addClass(attrValue);
|
||||
return;
|
||||
}
|
||||
if (attrName === 'data-icon') {
|
||||
var iconUrl = '';
|
||||
switch (attrValue) {
|
||||
case 'help':
|
||||
iconUrl = './themes/new/images/help.svg';
|
||||
break;
|
||||
case 'settings':
|
||||
iconUrl = './themes/new/images/settings.svg';
|
||||
break;
|
||||
}
|
||||
if (iconUrl !== '') {
|
||||
$button.addClass('icon');
|
||||
$button.addClass('icon-' + attrValue);
|
||||
var $iconElem = $('<img alt="Handbuch">').attr('src', iconUrl);
|
||||
var $iconWrapper = $('<span class="icon">').append($iconElem);
|
||||
$button.prepend($iconWrapper);
|
||||
|
||||
}
|
||||
}
|
||||
$button.attr(attrName, attrValue);
|
||||
});
|
||||
}
|
||||
$button.appendTo($buttonBlock);
|
||||
});
|
||||
|
||||
return $buttonBlock;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
*
|
||||
* @return {jQuery} jQuery-Element
|
||||
*/
|
||||
generateDetailAttachmentTypeStaticContent: function (data) {
|
||||
return $('<p>').html(data.content);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
*
|
||||
* @return {jQuery} jQuery-Element
|
||||
*/
|
||||
generateDetailAttachmentTypeDynamicContent: function (data) {
|
||||
var $dynamicContent = $('<div>').addClass('minidetail');
|
||||
|
||||
if (data.hasOwnProperty('url') && data.url !== null) {
|
||||
me.fetchMiniDetailContent(data.url, data.params)
|
||||
.then(
|
||||
function (htmlContent) {
|
||||
$dynamicContent.html(htmlContent);
|
||||
me.storage.$details.append($dynamicContent);
|
||||
},
|
||||
function (jqXhr) {
|
||||
var message = 'Fehler beim Laden der Mini-Details: ';
|
||||
if (jqXhr.hasOwnProperty('responseJSON') && jqXhr.responseJSON.hasOwnProperty('error')) {
|
||||
message += jqXhr.responseJSON.error;
|
||||
} else {
|
||||
message += jqXhr.status + ' ' + jqXhr.statusText;
|
||||
}
|
||||
$('<div class="error"></div>').text(message).appendTo(me.storage.$details);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return $dynamicContent;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} miniDetailUrl
|
||||
* @param {Object} miniDetailParams Zusätzliche POST-Parameter
|
||||
*
|
||||
* @return {jqXHR}
|
||||
*/
|
||||
fetchMiniDetailContent: function (miniDetailUrl, miniDetailParams) {
|
||||
if (miniDetailUrl.substr(0, 10) !== 'index.php?') {
|
||||
alert('Mini-Detail-URL ist ungültig: ' + miniDetailUrl);
|
||||
throw 'Mini-Detail-URL ist ungültig: ' + miniDetailUrl;
|
||||
}
|
||||
if (typeof miniDetailParams !== 'object') {
|
||||
miniDetailParams = {};
|
||||
}
|
||||
|
||||
return $.ajax({
|
||||
url: miniDetailUrl,
|
||||
data: miniDetailParams,
|
||||
method: 'post',
|
||||
dataType: 'html'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Suchergebnisse einblenden
|
||||
*/
|
||||
showResults: function () {
|
||||
me.getOverlay().addClass('has-result');
|
||||
me.getOverlay().find('section.empty-message').hide();
|
||||
me.getOverlay().find('section.error-message').hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Suchergebnisse ausblenden
|
||||
*/
|
||||
hideResults: function () {
|
||||
me.getOverlay().removeClass('has-result');
|
||||
me.getOverlay().find('section.empty-message').hide();
|
||||
me.getOverlay().find('section.error-message').hide();
|
||||
me.getOverlay().find('section.last-update').hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Details einblenden
|
||||
*/
|
||||
showDetails: function () {
|
||||
me.getOverlay().addClass('has-detail');
|
||||
me.getOverlay().find('.detail-wrapper').scrollTop(0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Details einblenden
|
||||
*/
|
||||
hideDetails: function () {
|
||||
me.getOverlay().removeClass('has-detail');
|
||||
},
|
||||
|
||||
/**
|
||||
* Hinweis anzeigen das keine Ergebnisse gefunden wurden
|
||||
*/
|
||||
showEmptyResults: function () {
|
||||
me.showOverlay();
|
||||
me.hideDetails();
|
||||
me.getOverlay().removeClass('has-result');
|
||||
me.getOverlay().find('section.empty-message').show();
|
||||
me.getOverlay().find('section.error-message').hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} errorMessage
|
||||
*/
|
||||
showErrorMessage: function (errorMessage) {
|
||||
me.showOverlay();
|
||||
me.hideDetails();
|
||||
me.getOverlay().find('section.empty-message').hide();
|
||||
me.getOverlay().find('section.error-message').html(errorMessage).show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Puffer-Funktion um Events erst nach einer bestimmten Zeit auszuführen
|
||||
*
|
||||
* @param {function} callback
|
||||
* @param {number} delay
|
||||
* @param {object|null} contextParam
|
||||
*/
|
||||
debounce: function (callback, delay, contextParam) {
|
||||
var context = typeof contextParam !== 'undefined' && contextParam !== null ? contextParam : this;
|
||||
var args = arguments;
|
||||
|
||||
window.clearTimeout(me.storage.debounceBuffer);
|
||||
me.storage.debounceBuffer = window.setTimeout(function () {
|
||||
callback.apply(context, args);
|
||||
}, delay || 250);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
init: me.init
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
|
||||
$(function () {
|
||||
SuperSearch.init();
|
||||
});
|
||||
Reference in New Issue
Block a user