Skip to content

Commit d4019e4

Browse files
authored
Merge pull request #10304 from craftcms/feature/dev-170-native-alt-text-field-for-assets
Native alt text field for assets
2 parents cb7694d + de47b1f commit d4019e4

File tree

19 files changed

+362
-7
lines changed

19 files changed

+362
-7
lines changed

CHANGELOG.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
- Added the “Inactive” user status, which can be used by users which can’t be signed into. ([#8963](https://github.com/craftcms/cms/discussions/8963))
1010
- Added “Credentialed” and “Inactive” user sources.
1111
- Added the “Deactivate…” user action for pending and active users.
12+
- Craft now provides a native “Alternative Text” (`alt`) field for assets. ([#10302](https://github.com/craftcms/cms/discussions/10302))
13+
- Asset thumbnails in the control panel now have `alt` attributes, for assets with a filled-in Alternative Text value.
1214
- Added the `index-assets/cleanup` command.
1315
- Added the “Deactivate users by default” user registration setting, which replaces “Suspend users by default”. ([#5830](https://github.com/craftcms/cms/issues/5830))
1416
- Element source settings are now stored in the project config. ([#8616](https://github.com/craftcms/cms/discussions/8616))
1517
- Added support for `JSON` columns. ([#9089](https://github.com/craftcms/cms/pull/9089))
1618
- It’s now possible to edit images’ focal points from their preview modals. ([#8489](https://github.com/craftcms/cms/discussions/8489))
1719
- Added the `assetUploaders` user query param.
1820
- Added the `authors` user query param.
21+
- Added the `hasAlt` asset query param.
1922
- Added support for setting custom config settings from `config/custom.php`, which are accessible via `Craft::$app->config->custom`. ([#10012](https://github.com/craftcms/cms/issues/10012))
2023
- Added `craft\base\ApplicationTrait::getConditions()`.
2124
- Added `craft\base\ApplicationTrait::getElementSources()`, which replaces `getElementIndexes()`.
@@ -31,6 +34,7 @@
3134
- Added `craft\base\conditions\ConditionInterface`.
3235
- Added `craft\base\conditions\ConditionRuleInterface`.
3336
- Added `craft\base\ElementInterface::createCondition()`.
37+
- Added `craft\base\ElementInterface::getThumbAlt()`.
3438
- Added `craft\base\FieldInterface::getElementConditionRuleType()`.
3539
- Added `craft\base\FieldLayoutComponent`.
3640
- Added `craft\base\Volume::CONFIG_MIMETYPE`.
@@ -48,12 +52,14 @@
4852
- Added `craft\db\Migration::renameTable()`.
4953
- Added `craft\db\Query::collect()`, which returns the query results as an `Illuminate\Support\Collection` object rather than an array. ([#8513](https://github.com/craftcms/cms/discussions/8513))
5054
- Added `craft\db\Table::ASSETINDEXINGSESSIONS`.
55+
- Added `craft\elements\Asset::$alt`.
5156
- Added `craft\elements\Asset::setFilename()`.
5257
- Added `craft\elements\conditions\assets\AssetCondition`.
5358
- Added `craft\elements\conditions\assets\DateModifiedConditionRule`.
5459
- Added `craft\elements\conditions\assets\FilenameConditionRule`.
5560
- Added `craft\elements\conditions\assets\FileSizeConditionRule`.
5661
- Added `craft\elements\conditions\assets\FileTypeConditionRule`.
62+
- Added `craft\elements\conditions\assets\HasAltConditionRule`.
5763
- Added `craft\elements\conditions\assets\HeightConditionRule`.
5864
- Added `craft\elements\conditions\assets\UploaderConditionRule`.
5965
- Added `craft\elements\conditions\assets\VolumeConditionRule`.
@@ -93,7 +99,9 @@
9399
- Added `craft\elements\User::STATUS_INACTIVE`.
94100
- Added `craft\errors\MissingVolumeFolderException`.
95101
- Added `craft\events\RegisterConditionRuleTypesEvent`.
102+
- Added `craft\fieldlayoutelements\AssetAltField`.
96103
- Added `craft\fieldlayoutelements\BaseNativeField`, which replaces `craft\fieldlayoutelements\StandardField`.
104+
- Added `craft\fieldlayoutelements\TextareaField`.
97105
- Added `craft\fieldlayoutelements\TextField`, which replaces `craft\fieldlayoutelements\StandardTextField`.
98106
- Added `craft\fields\Assets::$allowSubfolders`.
99107
- Added `craft\fields\Assets::$restrictedDefaulUploadSubpath`.
@@ -221,6 +229,7 @@
221229
- Filtering users by `active`, `pending`, and `locked` statuses no longer excludes suspended users.
222230
- Assets fields that are restricted to a single location can now be configured to allow selection within subfolders of that location. ([#9070](https://github.com/craftcms/cms/discussions/9070))
223231
- When an image is saved as a new asset from the Image Editor via an Assets field, the Assets field will now automatically replace the selected asset with the new one. ([#8974](https://github.com/craftcms/cms/discussions/8974))
232+
- `alt` is now a reserved field handle for volume field layouts.
224233
- Entry post dates are no longer set automatically until the entry is validated with the `live` scenario. ([#10093](https://github.com/craftcms/cms/pull/10093))
225234
- Entry queries’ `authorGroup()` param method now accepts an array of `craft\models\UserGroup` objects.
226235
- Relational fields now load elements in the current site rather than the primary site, if the source element isn’t localizable. ([#7048](https://github.com/craftcms/cms/issues/7048))
@@ -318,15 +327,16 @@
318327
- Widgets’ `icon()` methods must now have a `?string` return type declaration.
319328
- Widgets’ `maxColspan()` methods must now have an `?int` return type declaration.
320329
- `craft\base\AssetPreviewHandlerInterface::getPreviewHtml()` now accepts an optional array of variable to pass on to the template.
321-
- `craft\base\ElementInterface::getEagerLoadedElements()` now returns an `Illuminate\Support\Collection` object instead of an array. ([#8513](https://github.com/craftcms/cms/discussions/8513))
322330
- `craft\base\Element::__get()` now clones custom field values before returning them. ([#8781](https://github.com/craftcms/cms/discussions/8781))
323331
- `craft\base\Element::fieldLayoutFields()` now has a `visibleOnly` argument.
324332
- `craft\base\Element::getFieldValue()` now returns eager-loaded element values for the field, when they exist. ([#10047](https://github.com/craftcms/cms/issues/10047))
333+
- `craft\base\ElementInterface::getEagerLoadedElements()` now returns an `Illuminate\Support\Collection` object instead of an array. ([#8513](https://github.com/craftcms/cms/discussions/8513))
325334
- `craft\base\MemoizableArray` no longer extends `ArrayObject`, and now implements `IteratorAggregate` and `Countable` directly.
326335
- `craft\base\Model::datetimeAttributes()` is now called from the constructor, instead of the `init()` method.
327336
- `craft\base\Model::setAttributes()` now normalizes date attributes into `DateTime` objects.
328-
- `craft\db\Command` methods with `$includeAuditColumns` arguments now ensure the table actually has audit columns before modifying the query.
329337
- `craft\db\Command::upsert()` no longer merges the `$updateColumns` array into `$insertColumns`. The full array of `INSERT` column values should be passed to `$insertColumns` now.
338+
- `craft\db\Command` methods with `$includeAuditColumns` arguments now ensure the table actually has audit columns before modifying the query.
339+
- `craft\elements\Asset::getImg()` now sets the `alt` attribute to the native Alternative Text field value, if set.
330340
- `craft\elements\db\ElementQuery::ids()` no longer accepts an array of criteria params.
331341
- `craft\events\DraftEvent::$source` has been renamed to `$canonical`.
332342
- `craft\events\GetAssetThumbUrlEvent` has been renamed to `DefineAssetThumbUrlEvent`.
@@ -344,10 +354,10 @@
344354
- `craft\services\Announcements::push()` no longer accepts callables to be passed to the `$heading` and `$body` arguments. `craft\i18n\Translation::prep()` should be used to prepare the messages to be lazy-translated instead.
345355
- `craft\services\AssetIndexer::storeIndexList()` now expects the first argument to be a generator that returns `craft\models\VolumeListing` objects.
346356
- `craft\services\Assets::ensureFolderByFullPathAndVolume()` now returns a `craft\models\VolumeFolder` object rather than a folder ID.
357+
- `craft\services\Assets::ensureTopFolder()` now returns a `craft\models\VolumeFolder` object rather than a folder ID.
347358
- `craft\services\Assets::EVENT_GET_ASSET_THUMB_URL` has been renamed to `EVENT_DEFINE_THUMB_URL`.
348359
- `craft\services\Assets::EVENT_GET_ASSET_URL` has been renamed to `EVENT_DEFINE_ASSET_URL`.
349360
- `craft\services\Assets::EVENT_GET_THUMB_PATH` has been renamed to `EVENT_DEFINE_THUMB_PATH`.
350-
- `craft\services\Assets::ensureTopFolder()` now returns a `craft\models\VolumeFolder` object rather than a folder ID.
351361
- `craft\services\Plugins::doesPluginRequireDatabaseUpdate()` has been renamed to `isPluginUpdatePending()`.
352362
- `craft\services\Updates::getIsCraftDbMigrationNeeded()` has been renamed to `getIsCraftUpdatePending()`.
353363
- `craft\services\Updates::getIsPluginDbUpdateNeeded()` has been renamed to `getIsPluginUpdatePending()`.

src/base/ApplicationTrait.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use craft\events\DeleteSiteEvent;
2727
use craft\events\EditionChangeEvent;
2828
use craft\events\FieldEvent;
29+
use craft\fieldlayoutelements\AssetAltField;
2930
use craft\fieldlayoutelements\AssetTitleField;
3031
use craft\fieldlayoutelements\EntryTitleField;
3132
use craft\fieldlayoutelements\TitleField;
@@ -1508,6 +1509,7 @@ private function _registerFieldLayoutListener(): void
15081509
break;
15091510
case Asset::class:
15101511
$event->fields[] = AssetTitleField::class;
1512+
$event->fields[] = AssetAltField::class;
15111513
break;
15121514
case Entry::class:
15131515
$event->fields[] = EntryTitleField::class;

src/base/Element.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2713,6 +2713,14 @@ public function getThumbUrl(int $size): ?string
27132713
return null;
27142714
}
27152715

2716+
/**
2717+
* @inheritdoc
2718+
*/
2719+
public function getThumbAlt(): ?string
2720+
{
2721+
return null;
2722+
}
2723+
27162724
/**
27172725
* @inheritdoc
27182726
*/

src/base/ElementInterface.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,14 @@ public function getPreviewTargets(): array;
790790
*/
791791
public function getThumbUrl(int $size): ?string;
792792

793+
/**
794+
* Returns alt text for the element’s thumbnail.
795+
*
796+
* @return string|null
797+
* @since 4.0.0
798+
*/
799+
public function getThumbAlt(): ?string;
800+
793801
/**
794802
* Returns whether the element’s thumbnail should have a checkered background.
795803
*

src/base/Volume.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public function validateFieldLayout(): void
103103
{
104104
$fieldLayout = $this->getFieldLayout();
105105
$fieldLayout->reservedFieldHandles = [
106+
'alt',
106107
'folder',
107108
'volume',
108109
];

src/elements/Asset.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,12 @@ private static function _assembleSourceInfoForFolder(VolumeFolder $folder, bool
589589
*/
590590
public ?string $kind = null;
591591

592+
/**
593+
* @var string|null Alternative text
594+
* @since 4.0.0
595+
*/
596+
public ?string $alt = null;
597+
592598
/**
593599
* @var int|null Size
594600
*/
@@ -780,6 +786,11 @@ public function __get($name)
780786
public function init(): void
781787
{
782788
parent::init();
789+
790+
if ($this->alt === '') {
791+
$this->alt = null;
792+
}
793+
783794
$this->_oldVolumeId = $this->_volumeId;
784795
}
785796

@@ -967,7 +978,7 @@ public function getImg($transform = null, ?array $sizes = null): ?Markup
967978
'width' => $this->getWidth(),
968979
'height' => $this->getHeight(),
969980
'srcset' => $sizes ? $this->getSrcset($sizes) : false,
970-
'alt' => $this->title,
981+
'alt' => $this->alt ?? $this->title,
971982
]);
972983

973984
if (isset($oldTransform)) {
@@ -1305,6 +1316,14 @@ public function getThumbUrl(int $size): ?string
13051316
return Craft::$app->getAssets()->getThumbUrl($this, $width, $height, false);
13061317
}
13071318

1319+
/**
1320+
* @inheritdoc
1321+
*/
1322+
public function getThumbAlt(): ?string
1323+
{
1324+
return $this->alt;
1325+
}
1326+
13081327
/**
13091328
* @inheritdoc
13101329
*/
@@ -1995,6 +2014,7 @@ public function afterSave(bool $isNew): void
19952014
$record->folderId = (int)$this->folderId;
19962015
$record->uploaderId = (int)$this->uploaderId ?: null;
19972016
$record->kind = $this->kind;
2017+
$record->alt = $this->alt;
19982018
$record->size = (int)$this->size ?: null;
19992019
$record->width = (int)$this->_width ?: null;
20002020
$record->height = (int)$this->_height ?: null;

src/elements/conditions/assets/AssetCondition.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ protected function conditionRuleTypes(): array
2222
FileSizeConditionRule::class,
2323
FileTypeConditionRule::class,
2424
FilenameConditionRule::class,
25+
HasAltConditionRule::class,
2526
HeightConditionRule::class,
2627
UploaderConditionRule::class,
2728
VolumeConditionRule::class,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace craft\elements\conditions\assets;
4+
5+
use Craft;
6+
use craft\base\conditions\BaseLightswitchConditionRule;
7+
use craft\base\ElementInterface;
8+
use craft\elements\Asset;
9+
use craft\elements\conditions\ElementConditionRuleInterface;
10+
use craft\elements\db\AssetQuery;
11+
use craft\elements\db\ElementQueryInterface;
12+
13+
/**
14+
* “Has alternative text” condition rule for assets.
15+
*
16+
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
17+
* @since 4.0.0
18+
*/
19+
class HasAltConditionRule extends BaseLightswitchConditionRule implements ElementConditionRuleInterface
20+
{
21+
/**
22+
* @inheritdoc
23+
*/
24+
public function getLabel(): string
25+
{
26+
return Craft::t('app', 'Has alternative text');
27+
}
28+
29+
/**
30+
* @inheritdoc
31+
*/
32+
public function getExclusiveQueryParams(): array
33+
{
34+
return ['hasAlt'];
35+
}
36+
37+
/**
38+
* @inheritdoc
39+
*/
40+
public function modifyQuery(ElementQueryInterface $query): void
41+
{
42+
/** @var AssetQuery $query */
43+
$query->hasAlt($this->value);
44+
}
45+
46+
/**
47+
* @inheritdoc
48+
*/
49+
public function matchElement(ElementInterface $element): bool
50+
{
51+
/** @var Asset $element */
52+
return $this->matchValue($element->alt !== null);
53+
}
54+
}

src/elements/db/AssetQuery.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ class AssetQuery extends ElementQuery
125125
*/
126126
public $kind;
127127

128+
/**
129+
* @var bool|null Whether the query should filter assets depending on whether they have alternative text.
130+
* @used-by hasAlt()
131+
* @since 4.0.0
132+
*/
133+
public ?bool $hasAlt = null;
134+
128135
/**
129136
* @var mixed The width (in pixels) that the resulting assets must have.
130137
* ---
@@ -495,6 +502,19 @@ public function kind($value): self
495502
return $this;
496503
}
497504

505+
/**
506+
* Narrows the query results based on whether the assets have alternative text.
507+
*
508+
* @param bool|null $value The property value
509+
* @return self self reference
510+
* @uses $hasAlt
511+
*/
512+
public function hasAlt(?bool $value = true): self
513+
{
514+
$this->hasAlt = $value;
515+
return $this;
516+
}
517+
498518
/**
499519
* Narrows the query results based on the assets’ image widths.
500520
*
@@ -783,6 +803,7 @@ protected function beforePrepare(): bool
783803
'assets.uploaderId',
784804
'assets.filename',
785805
'assets.kind',
806+
'assets.alt',
786807
'assets.width',
787808
'assets.height',
788809
'assets.size',
@@ -831,6 +852,10 @@ protected function beforePrepare(): bool
831852
$this->subQuery->andWhere($kindCondition);
832853
}
833854

855+
if ($this->hasAlt !== null) {
856+
$this->subQuery->andWhere($this->hasAlt ? ['not', ['assets.alt' => null]] : ['assets.alt' => null]);
857+
}
858+
834859
if ($this->width) {
835860
$this->subQuery->andWhere(Db::parseNumericParam('assets.width', $this->width));
836861
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
/**
3+
* @link https://craftcms.com/
4+
* @copyright Copyright (c) Pixel & Tonic, Inc.
5+
* @license https://craftcms.github.io/license/
6+
*/
7+
8+
namespace craft\fieldlayoutelements;
9+
10+
use Craft;
11+
use craft\base\ElementInterface;
12+
use craft\base\Field;
13+
use craft\elements\Asset;
14+
use craft\helpers\ElementHelper;
15+
use yii\base\InvalidArgumentException;
16+
17+
/**
18+
* AssetAltField represents an Alternative Text field that can be included within a volume’s field layout designer.
19+
*
20+
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
21+
* @since 4.0.0
22+
*/
23+
class AssetAltField extends TextareaField
24+
{
25+
/**
26+
* @inheritdoc
27+
*/
28+
public string $attribute = 'alt';
29+
30+
/**
31+
* @inheritdoc
32+
*/
33+
public bool $translatable = true;
34+
35+
/**
36+
* @inheritdoc
37+
*/
38+
public function __construct($config = [])
39+
{
40+
// We didn't start removing autofocus from fields() until 3.5.6
41+
unset(
42+
$config['mandatory'],
43+
$config['attribute'],
44+
$config['translatable'],
45+
$config['maxlength'],
46+
$config['autofocus']
47+
);
48+
49+
parent::__construct($config);
50+
}
51+
52+
/**
53+
* @inheritdoc
54+
*/
55+
public function fields(): array
56+
{
57+
$fields = parent::fields();
58+
unset(
59+
$fields['mandatory'],
60+
$fields['attribute'],
61+
$fields['translatable'],
62+
$fields['maxlength'],
63+
$fields['autofocus']
64+
);
65+
return $fields;
66+
}
67+
68+
/**
69+
* @inheritdoc
70+
*/
71+
public function defaultLabel(?ElementInterface $element = null, bool $static = false): ?string
72+
{
73+
return Craft::t('app', 'Alternative Text');
74+
}
75+
76+
/**
77+
* @inheritdoc
78+
*/
79+
protected function translationDescription(?ElementInterface $element = null, bool $static = false): ?string
80+
{
81+
return Field::TRANSLATION_METHOD_SITE;
82+
}
83+
}

0 commit comments

Comments
 (0)