feat: add new-tab links and removable media previews (1.7.3)
This commit is contained in:
1 parent
b8991ddd5c
commit
6d21c452bd
28 files changed
+496
-46
No files matched your search
@@ -42,7 +42,7 @@ final class DocumentValidator
|
||||
public function validate(array $document, bool $requireHeading = true): void
|
||||
{
|
||||
$schemaVersion = $document['schemaVersion'] ?? null;
|
||||
$this->require(in_array($schemaVersion, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], true), 'Unsupported document schema.');
|
||||
$this->require(in_array($schemaVersion, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], true), 'Unsupported document schema.');
|
||||
$this->text($document['title'] ?? null, 160);
|
||||
$this->text($document['description'] ?? null, 320);
|
||||
$this->require(is_array($document['blocks'] ?? null) && array_is_list($document['blocks']), 'Blocks must be a list.');
|
||||
@@ -124,6 +124,15 @@ final class DocumentValidator
|
||||
$limit = 'text' === $type ? (RichText::supports($schemaVersion) ? RichText::MAX_LENGTH : 10000) : ('heading' === $type && RichText::headings($schemaVersion) ? 2000 : 300);
|
||||
$this->text($props['text'] ?? null, $limit);
|
||||
}
|
||||
if (!$plugin && array_key_exists('newTab', $props)) {
|
||||
$this->require($schemaVersion >= 20 && in_array($type, ['button', 'image', 'iconcard', 'cta', 'icon'], true), 'Neue Link-Tabs brauchen Schema v20 und einen verlinkbaren Block.');
|
||||
$this->require(is_bool($props['newTab']), 'In neuem Tab öffnen ist an oder aus.');
|
||||
}
|
||||
foreach (($block['style'] ?? []) as $values) {
|
||||
if ('none' === ($values['backgroundImage'] ?? null)) {
|
||||
$this->require($schemaVersion >= 20, 'Ein entferntes Hintergrundbild braucht Schema v20.');
|
||||
}
|
||||
}
|
||||
if ('heading' === $type) {
|
||||
$this->require(is_int($props['level'] ?? null) && $props['level'] >= 1 && $props['level'] <= 6, 'Invalid heading level.');
|
||||
$headings += 1 === $props['level'] ? 1 : 0;
|
||||
|
||||
@@ -132,7 +132,7 @@ final class MenuRepository
|
||||
if ('' === trim($label)) {
|
||||
continue;
|
||||
}
|
||||
$links[] = ['label' => $label, 'href' => $href, 'current' => $current, 'children' => $children];
|
||||
$links[] = ['label' => $label, 'href' => $href, 'current' => $current, 'children' => $children, ...(($item['newTab'] ?? false) ? ['newTab' => true] : [])];
|
||||
}
|
||||
|
||||
return $links;
|
||||
@@ -153,6 +153,9 @@ final class MenuRepository
|
||||
if (!is_array($item) || !is_string($item['label'] ?? null) || '' === trim($item['label']) || mb_strlen($item['label']) > 80) {
|
||||
throw new InvalidMenu('Jeder Eintrag braucht eine Beschriftung mit höchstens 80 Zeichen.');
|
||||
}
|
||||
if (array_key_exists('newTab', $item) && !is_bool($item['newTab'])) {
|
||||
throw new InvalidMenu('In neuem Tab öffnen ist an oder aus.');
|
||||
}
|
||||
if ('page' === ($item['type'] ?? null)) {
|
||||
if (!is_string($item['pageId'] ?? null) || 1 !== preg_match('/^[a-zA-Z0-9_-]{1,64}$/D', $item['pageId'])) {
|
||||
throw new InvalidMenu('Für einen Seiteneintrag fehlt die Seite.');
|
||||
|
||||
@@ -21,7 +21,7 @@ final class RichText
|
||||
'strong' => [], 'b' => [], 'em' => [], 'i' => [], 'u' => [], 's' => [],
|
||||
'code' => [], 'mark' => [], 'sup' => [], 'sub' => [],
|
||||
'ul' => [], 'ol' => [], 'li' => [],
|
||||
'a' => ['href'],
|
||||
'a' => ['href', 'target'],
|
||||
];
|
||||
/** Tags whose content is dropped along with them; unwrapping them would leak code as text. */
|
||||
private const DROP_ENTIRELY = ['script', 'style', 'iframe', 'object', 'embed', 'template', 'noscript', 'svg', 'math'];
|
||||
@@ -121,7 +121,10 @@ final class RichText
|
||||
continue;
|
||||
}
|
||||
$child->setAttribute('href', $href);
|
||||
if (str_starts_with($href, 'https://')) {
|
||||
if ('_blank' !== $child->getAttribute('target')) {
|
||||
$child->removeAttribute('target');
|
||||
}
|
||||
if ('_blank' === $child->getAttribute('target') || str_starts_with($href, 'https://')) {
|
||||
$child->setAttribute('rel', 'noopener');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ final class StyleCompiler
|
||||
sprintf('"%s" is outside its range.', $key),
|
||||
),
|
||||
'font' => $this->require(1 === preg_match('/^[a-z][a-z0-9-]{0,39}$/D', $text), sprintf('"%s" is not a font key.', $key)),
|
||||
'media' => $this->require(1 === preg_match('/^[a-f0-9]{32}$/D', $text), sprintf('"%s" is not a media ID.', $key)),
|
||||
'media' => $this->require('none' === $text || 1 === preg_match('/^[a-f0-9]{32}$/D', $text), sprintf('"%s" is not a media ID.', $key)),
|
||||
default => $this->require(false, 'Unknown style type.'),
|
||||
};
|
||||
}
|
||||
@@ -233,7 +233,7 @@ final class StyleCompiler
|
||||
$this->css(['type' => 'color'], (string) $values['gradientTo']),
|
||||
);
|
||||
}
|
||||
if (isset($values['backgroundImage']) && null !== $this->media) {
|
||||
if (isset($values['backgroundImage']) && 'none' !== $values['backgroundImage'] && null !== $this->media) {
|
||||
$item = $this->media->find((string) $values['backgroundImage']);
|
||||
if (null !== $item && [] !== $item['variants']) {
|
||||
// A phone should not download the 2400 pixel variant of a background. CSS has no
|
||||
@@ -244,7 +244,7 @@ final class StyleCompiler
|
||||
}
|
||||
}
|
||||
|
||||
return [] === $layers ? '' : 'background-image:'.implode(',', $layers);
|
||||
return [] === $layers ? ('none' === ($values['backgroundImage'] ?? null) ? 'background-image:none' : '') : 'background-image:'.implode(',', $layers);
|
||||
}
|
||||
|
||||
private function shadow(array $values): string
|
||||
@@ -348,21 +348,19 @@ final class StyleCompiler
|
||||
*/
|
||||
private function scaleBackground(array $style): array
|
||||
{
|
||||
$image = $style['desktop']['backgroundImage'] ?? null;
|
||||
if (null === $image) {
|
||||
return $style;
|
||||
}
|
||||
$inherited = $style['desktop'] ?? [];
|
||||
foreach (['tablet', 'mobile'] as $device) {
|
||||
if (!isset($style[$device]['backgroundImage'])) {
|
||||
$style[$device]['backgroundImage'] = $image;
|
||||
if (isset($inherited['backgroundImage']) || isset($style[$device]['backgroundImage'])) {
|
||||
$style[$device]['backgroundImage'] ??= $inherited['backgroundImage'];
|
||||
// Verlauf und Überlagerung gehören zur selben Regel: ohne sie stünde auf dem
|
||||
// Telefon plötzlich ein unabgedunkeltes Bild hinter hellem Text.
|
||||
foreach (['gradientFrom', 'gradientTo', 'gradientAngle', 'overlayColor', 'overlayOpacity', 'backgroundSize', 'backgroundPosition', 'backgroundRepeat', 'backgroundAttachment'] as $begleiter) {
|
||||
if (isset($style['desktop'][$begleiter]) && !isset($style[$device][$begleiter])) {
|
||||
$style[$device][$begleiter] = $style['desktop'][$begleiter];
|
||||
if (isset($inherited[$begleiter]) && !isset($style[$device][$begleiter])) {
|
||||
$style[$device][$begleiter] = $inherited[$begleiter];
|
||||
}
|
||||
}
|
||||
}
|
||||
$inherited = array_replace($inherited, $style[$device] ?? []);
|
||||
}
|
||||
|
||||
return $style;
|
||||
|
||||
@@ -111,6 +111,15 @@ final class MediaController extends AbstractController
|
||||
return $this->json(['items' => $this->media->list(), 'csrfToken' => $this->csrf->getToken('media')->getValue()]);
|
||||
}
|
||||
|
||||
#[Route('/%kant.admin_path%/api/media/{id}', requirements: ['id' => '[a-f0-9]{32}'], methods: ['GET'])]
|
||||
public function read(string $id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
||||
$item = $this->media->find($id);
|
||||
|
||||
return null === $item ? $this->json(['error' => 'Bild nicht gefunden.'], 404) : $this->json(['item' => $item]);
|
||||
}
|
||||
|
||||
#[Route('/%kant.admin_path%/api/media/upload', methods: ['POST'])]
|
||||
#[Route('/%kant.admin_path%/api/media/{id}', requirements: ['id' => '[a-f0-9]{32}'], methods: ['POST'])]
|
||||
public function write(Request $request, ?string $id = null): Response
|
||||
|
||||
@@ -76,6 +76,7 @@ final class MenuController extends AbstractController
|
||||
$rows[] = [
|
||||
'label' => $item['label'], 'type' => $item['type'], 'level' => $level,
|
||||
'pageId' => $item['pageId'] ?? '', 'anchor' => $item['anchor'] ?? '', 'url' => $item['url'] ?? '',
|
||||
'newTab' => $item['newTab'] ?? false,
|
||||
];
|
||||
array_push($rows, ...$this->flatten($item['children'] ?? [], $level + 1));
|
||||
}
|
||||
@@ -101,6 +102,7 @@ final class MenuController extends AbstractController
|
||||
'pageId' => (string) ($row['pageId'] ?? ''),
|
||||
'anchor' => ltrim(trim((string) ($row['anchor'] ?? '')), '#'),
|
||||
'url' => trim((string) ($row['url'] ?? '')),
|
||||
'newTab' => '1' === (string) ($row['newTab'] ?? ''),
|
||||
'level' => max(1, min(MenuRepository::MAX_DEPTH, (int) ($row['level'] ?? 1))),
|
||||
'position' => (int) ($row['position'] ?? 0),
|
||||
];
|
||||
@@ -110,6 +112,9 @@ final class MenuController extends AbstractController
|
||||
$items = [];
|
||||
foreach ($flat as $row) {
|
||||
$item = ['label' => $row['label'], 'type' => $row['type'], 'children' => []];
|
||||
if ($row['newTab']) {
|
||||
$item['newTab'] = true;
|
||||
}
|
||||
if ('url' === $row['type']) {
|
||||
$item['url'] = $row['url'];
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user