CoolCMS API

ThemePickerTrait

Template and color variant picker: the shared core of both appearance screens, "Website appearance" ({@see \App\UI\Admin\System\Presenters\ThemeSettingPresenter}, front end templates) and "Admin settings" ({@see \App\UI\Admin\System\Presenters\AdminSettingPresenter}, admin templates).

Both branches share the PHP and the template (Templates/@themePicker.latte) and differ only in three setting keys and the templates directory.

IMPORTANT: the user picks a (template, skin) pair with one click. With separate selects, the skin options would belong to the previous template after a switch and would need an AJAX reload. A click on a preview therefore writes both hidden fields at once.

NOTE: there are two fields (theme, skin) instead of one composite value because that is exactly what is stored: each maps to one setting key. An empty skin means the base color.

IMPORTANT: path has no field of its own and must not get one: it is derived from theme. A drifted name x path pair is why both screens exist; on the admin branch it ends in variantExists() on null, an HTTP 500 on every admin page including the login.

Tags
phpstan-import-type

ThemeItem from ThemeCatalog

ThemeVariant from ThemeCatalog

Table of Contents

Constants

PICKER_MODULE  : mixed = 1
Settings of both appearances live in the System module.

Methods

addThemePickerFields()  : void
Adds the hidden field pair that a click on a preview fills.
saveThemePickerSettings()  : bool
Saves the map from {@see themePickerSettings()}; FALSE = nothing was saved.
themePickerDirRel()  : string
Relative (from the project root) directory with the templates of this branch.
themePickerGroups()  : array<int, array{slug: string, labelKey: string, themes: list, variants: list}>}>
Templates grouped for rendering. Each variant carries its preview and an "is currently set" flag.
themePickerKeys()  : array{theme: string, path: string, skin: string}
Setting keys of this branch: `theme` (template name), `path` (derived path) and `skin` (color variant, empty = base).
themePickerSettings()  : array<string, string>|null
Validates the submitted pair and builds a map "setting key => value", or returns NULL and writes the error into the form.
themePickerAssetUrl()  : string|null
Returns the relative path to the preview if the file exists and is not empty, otherwise NULL.
themePickerChoices()  : array<string, array<int, string>>
Offered pairs: template name => list of its skins (`''` = base color).
themePickerCover()  : array{skin: string, label: ?string, color: ?string, preview: ?string, previewFull: ?string, active: bool}
Variant shown by the card in the grid: the selected one, otherwise the base color.
themePickerDir()  : string
Returns the absolute directory with the templates of this branch.
themePickerGallery()  : array<int, array{view: string, url: string}>
Screenshots for the template gallery: large samples opened by a click on the thumbnail.
themePickerNames()  : array<int, string>
Names of the offered templates for the form rule.
themePickerPreviewFullUrl()  : string|null
Returns the URL of the whole-page thumbnail for the (template, skin) pair, or NULL when the file is missing.
themePickerPreviewUrl()  : string|null
Returns the preview URL for the (template, skin) pair, or NULL when the file is missing.
themePickerValue()  : string
Value of a key from module 1 as a string ('' = key missing or not a scalar).

Constants

PICKER_MODULE

Settings of both appearances live in the System module.

private mixed PICKER_MODULE = 1

IMPORTANT: the keys cannot be renumbered even on the admin branch: ThemeRepository::pick() reads them hard-coded from $apiResponse[1].

Methods

addThemePickerFields()

Adds the hidden field pair that a click on a preview fills.

protected addThemePickerFields(Form $form) : void

IMPORTANT: both fields are hidden and JavaScript writes their values, so validation against the catalog is mandatory, not cosmetic: anyone can post anything into a hidden field and an invalid pair would store a nonexistent template (a site without appearance on the front end, a crash on variantExists() on null in the admin).

NOTE: the rule guards only the template. A skin means something only within its template (two templates may have a skin of the same name), so the whole pair is verified in themePickerSettings(), where the catalog and the template name are at hand.

NOTE: an error of a hidden field is shown in the admin only as a form error (nothing to highlight at the field), so the template must print {$form->getErrors()}.

The default value is set directly on the control, not via setDefaults(): Nette treats the second parameter of addHidden() as the default and setDefaults() did not override it for a hidden field, so the field stayed empty and the screen claimed nothing was selected. On submit the request value wins anyway.

Parameters
$form : Form

saveThemePickerSettings()

Saves the map from {@see themePickerSettings()}; FALSE = nothing was saved.

protected saveThemePickerSettings(array<string, string> $settings[, array<int, Setting> $extra = [] ]) : bool

IMPORTANT: the template / path / skin triple is indivisible, so it is saved in one transaction (SettingManager::saveBatch()). Saving key by key would leave a new template with an old path after a failure of the second key. $extra adds further form keys (e.g. paging) to the same transaction.

IMPORTANT: is_public of an existing row is not overwritten by a save, so the theme keys keep the flag that front end rendering and the admin login depend on. A new key would be created with is_public = 0, which is why each of them is seeded by an SQL script and not by the first save.

Parameters
$settings : array<string, string>
$extra : array<int, Setting> = []
Return values
bool

themePickerDirRel()

Relative (from the project root) directory with the templates of this branch.

protected abstract themePickerDirRel() : string

IMPORTANT: the slashes at both ends are part of the format; ThemeMapper builds the absolute path by plain concatenation.

Return values
string

themePickerGroups()

Templates grouped for rendering. Each variant carries its preview and an "is currently set" flag.

protected themePickerGroups() : array<int, array{slug: string, labelKey: string, themes: list, variants: list}>}>

The first variant is always the base color of the template; the others are its skins. The screen builds a grid of cards (one card = one template) and a modal with all variants of that template.

cover is the variant shown by the card in the grid: the selected one, otherwise the base color. After picking another color the card thus shows the appearance that will really be stored. It is computed here, not in the template, where it would be a loop searching for active in the middle of rendering.

Return values
array<int, array{slug: string, labelKey: string, themes: list, variants: list}>}>

themePickerKeys()

Setting keys of this branch: `theme` (template name), `path` (derived path) and `skin` (color variant, empty = base).

protected abstract themePickerKeys() : array{theme: string, path: string, skin: string}
Return values
array{theme: string, path: string, skin: string}

themePickerSettings()

Validates the submitted pair and builds a map "setting key => value", or returns NULL and writes the error into the form.

protected themePickerSettings(ArrayHash<string|int, mixed> $values, Form $form) : array<string, string>|null

IMPORTANT: the whole pair is verified, not only the template. A skin is valid only within its template, so "an existing skin" alone means nothing. The catalog is also re-read from the file system: a template deleted between render and submit must not reach the settings.

IMPORTANT: failure is reported by Form::addError(), not flashMessage(). A failed save is not redirected (the form must stay filled), but a flash renders only after a redirect, so the message would never show and the rejection would look like nothing happened.

Parameters
$values : ArrayHash<string|int, mixed>
$form : Form
Return values
array<string, string>|null

themePickerAssetUrl()

Returns the relative path to the preview if the file exists and is not empty, otherwise NULL.

private themePickerAssetUrl(string $relative) : string|null

IMPORTANT: the size is checked too. An interrupted generation leaves a zero-byte file for which is_file() returns true, and the screen would render a broken image.

NOTE: the relative path is also the public URL (/www/ is part of template asset addresses), so the prefix is not stripped.

Parameters
$relative : string
Return values
string|null

themePickerChoices()

Offered pairs: template name => list of its skins (`''` = base color).

private themePickerChoices() : array<string, array<int, string>>

NOTE: duplicate colors are not dropped here, unlike the visitor switcher where two equal dots side by side would look like a bug. Here every variant has its own screenshot, so two skins of the same color are two different previews.

Return values
array<string, array<int, string>>

themePickerCover()

Variant shown by the card in the grid: the selected one, otherwise the base color.

private themePickerCover(array<int, array{skin: string, label: ?string, color: ?string, preview: ?string, previewFull: ?string, active: bool, duplicateColor: bool}> $variants) : array{skin: string, label: ?string, color: ?string, preview: ?string, previewFull: ?string, active: bool}

NOTE: the list is never empty; ThemeCatalog::variants() puts the base color first even for a template without any skin.

Parameters
$variants : array<int, array{skin: string, label: ?string, color: ?string, preview: ?string, previewFull: ?string, active: bool, duplicateColor: bool}>
Return values
array{skin: string, label: ?string, color: ?string, preview: ?string, previewFull: ?string, active: bool}

themePickerDir()

Returns the absolute directory with the templates of this branch.

private themePickerDir() : string

IMPORTANT: it is not taken from $this->themeResolver: in the admin that holds the theme of the admin branch and its frontendThemesDir() returns NULL there. The catalog therefore receives the path.

Return values
string

themePickerGallery()

Screenshots for the template gallery: large samples opened by a click on the thumbnail.

private themePickerGallery(string $theme) : array<int, array{view: string, url: string}>

The order is fixed and is also the order of the switches in the gallery. A missing file is skipped, so a template without a generated gallery does not break the screen.

NOTE: only the base color of the template is used; the gallery shows the layout, not the shade, and the screen tells the user so in a label.

Parameters
$theme : string
Return values
array<int, array{view: string, url: string}>

themePickerNames()

Names of the offered templates for the form rule.

private themePickerNames() : array<int, string>
Return values
array<int, string>

themePickerPreviewFullUrl()

Returns the URL of the whole-page thumbnail for the (template, skin) pair, or NULL when the file is missing.

private themePickerPreviewFullUrl(string $theme, string $skin) : string|null

NOTE: it is a separate file, not a crop of the large one: the large preview is only a crop of the header and hero, so the rest of the page cannot be taken from it, and that is what the strip on the card shows.

Parameters
$theme : string
$skin : string
Return values
string|null

themePickerPreviewUrl()

Returns the preview URL for the (template, skin) pair, or NULL when the file is missing.

private themePickerPreviewUrl(string $theme, string $skin) : string|null

The base color has preview.webp next to theme.json, skins live in previews/<skin>.webp; bin/theme-previews.php generates them. WebP, not PNG: a website screenshot is photographic and lossless PNG took six times more space.

A missing file is not an error: the variant renders without an image, only with the color and a label, so a new template or skin never breaks the screen.

Parameters
$theme : string
$skin : string
Return values
string|null

themePickerValue()

Value of a key from module 1 as a string ('' = key missing or not a scalar).

private themePickerValue(string $key) : string
Parameters
$key : string
Return values
string
On this page

Search results