Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

### Content Management
- Matrix fields set to the “Cards” or “Blocks” view modes now show an “Add” button per entry type group, when the viewport is wide enough to support it. ([#17731](https://github.com/craftcms/cms/pull/17731))
- Matrix fields set to the “Blocks” view mode now have “Expand/collapse selected blocks” and “Copy selected blocks” field-level actions, if any blocks are selected. ([#18001](https://github.com/craftcms/cms/discussions/18001))
- Matrix fields set to the “Cards” view mode now have “Copy selected entries”, “Duplicate selected entries”, and “Delete selected entries” field-level actions, if any entries are selected. ([#18251](https://github.com/craftcms/cms/pull/18251))
- Matrix fields set to the “Blocks” view mode now have a “Expand/collapse selected blocks”, “Copy selected blocks”, “Duplicate selected blocks”, and “Delete selected blocks” field-level actions, if any entries are selected. ([#18001](https://github.com/craftcms/cms/discussions/18001), [#18251](https://github.com/craftcms/cms/pull/18251))
- Matrix fields set to the “Blocks” view mode now have block action menus with “Expand/Collapse”, “Entry type settings”, and “Copy” actions, even if the field isn’t editable. ([#18013](https://github.com/craftcms/cms/discussions/18013))
- Chips and cards are generally no longer hyperlinked. ([#17591](https://github.com/craftcms/cms/pull/17591))
- Entry revision menus now always include a “View all revisions” link. ([#18050](https://github.com/craftcms/cms/pull/18050))
Expand Down
3 changes: 3 additions & 0 deletions src/elements/NestedElementManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ public function getCardsHtml(?ElementInterface $owner, array $config = []): stri
$config += [
'showInGrid' => false,
'prevalidate' => false,
'selectable' => false,
];

return $this->createView(
Expand All @@ -400,6 +401,7 @@ function(string $id, array $config, $attribute, &$settings) use ($owner) {
'type' => $this->elementType::lowerDisplayName(),
]),
'showInGrid' => $config['showInGrid'],
'selectable' => $config['selectable'],
];

$html = Html::beginTag('div', options: [
Expand Down Expand Up @@ -439,6 +441,7 @@ function(string $id, array $config, $attribute, &$settings) use ($owner) {
fn(ElementInterface $element) => Cp::elementCardHtml($element, [
'context' => 'field',
'showActionMenu' => true,
'selectable' => $config['selectable'],
'sortable' => $config['sortable'],
'showInGrid' => $config['showInGrid'] ?? false,
]),
Expand Down
229 changes: 150 additions & 79 deletions src/fields/Matrix.php
Original file line number Diff line number Diff line change
Expand Up @@ -972,63 +972,95 @@ private function blockViewActionMenuItems(): array
$view->namespaceInputId($this->getInputId()),
]);

// Copy
// Copy, Duplicate, Delete
if ($this->maxEntries !== 1) {
$items[] = ['type' => 'hr'];

$copyAllId = sprintf('action-copy-all-%s', mt_rand());
$items[] = [
'id' => $copyAllId,
'icon' => 'clone-dashed',
'color' => \craft\enums\Color::Fuchsia,
'label' => StringHelper::upperCaseFirst(Craft::t('app', 'Copy all {type}', [
'type' => Entry::pluralLowerDisplayName(),
])),
];
$type = mb_strtolower(Craft::t('app', 'Blocks'));
$entrySelector = ' > .blocks > .matrixblock';

$baseInfo = Json::encode([
'type' => Entry::class,
'fieldId' => $this->id,
]);
$items[] = $this->copyAction($type, $entrySelector);
$items[] = $this->duplicateAction($type, $entrySelector, <<<JS
field.data('matrix').duplicateSelectedEntries();
JS);
$items[] = $this->deleteAction($type, $entrySelector, <<<JS
field.data('matrix').deleteSelectedEntries();
JS);
}

return $items;
}

private function cardViewActionMenuItems(): array
{
$items = [];

// Copy, Duplicate, Delete
if ($this->maxEntries !== 1) {
$type = Entry::pluralLowerDisplayName();
$entrySelector = ' > .nested-element-cards > .elements > li > .element';

$items[] = $this->copyAction($type, $entrySelector);
$items[] = $this->duplicateAction($type, $entrySelector, <<<JS
field.children('.nested-element-cards').data('nestedElementManager').duplicateElements(getEntries());
JS);
$items[] = $this->deleteAction($type, $entrySelector, <<<JS
field.children('.nested-element-cards').data('nestedElementManager').deleteElements(getEntries());
JS);
}

return $items;
}

private function copyAction(string $type, string $entrySelector): array
{
$view = Craft::$app->getView();
$id = sprintf('action-copy-%s', mt_rand());

$baseInfo = Json::encode([
'type' => Entry::class,
'fieldId' => $this->id,
]);

$view->registerJsWithVars(fn($copyAllId, $fieldId, $type) => <<<JS
$view->registerJsWithVars(fn($id, $fieldId, $entrySelector, $type) => <<<JS
(() => {
const copyBtn = $('#' + $copyAllId);
const btn = $('#' + $id);
const field = $('#' + $fieldId);
const menu = copyBtn.closest('.menu');
const getBlocks = () => {
const blocks = field.find(' > .blocks > .matrixblock');
const selectedBlocks = blocks.filter('.sel');
return selectedBlocks.length ? selectedBlocks : blocks;
};

if (field.length) {
copyBtn.on('activate', () => {
const elementInfo = [];
getBlocks().each((i, element) => {
element = $(element);
elementInfo.push(Object.assign({
id: element.data('id'),
draftId: element.data('draftId'),
revisionId: element.data('revisionId'),
ownerId: element.data('ownerId'),
siteId: element.data('siteId'),
}, $baseInfo));
});
Craft.cp.copyElements(elementInfo);
});
} else {
const menu = btn.closest('.menu');

if (!field.length) {
setTimeout(() => {
menu.data('disclosureMenu').removeItem(copyBtn[0]);
menu.data('disclosureMenu').removeItem(btn[0]);
}, 1);
return;
}


const getEntries = () => {
const entries = field.find($entrySelector);
const selectedEntries = entries.filter('.sel');
return (selectedEntries.length ? selectedEntries : entries).toArray();
};

btn.on('activate', () => {
Craft.cp.copyElements(getEntries().map((element) => {
element = $(element);
return {
... $baseInfo,
id: element.data('id'),
draftId: element.data('draftId'),
revisionId: element.data('revisionId'),
ownerId: element.data('ownerId'),
siteId: element.data('siteId'),
};
}));
});

setTimeout(() => {
const disclosureMenu = menu.data('disclosureMenu');
disclosureMenu.on('show', () => {
let blocks = getBlocks();
const entries = getEntries();
let copyLabel;
if (blocks.is('.sel')) {
if ($(entries).is('.sel')) {
copyLabel = Craft.t('app', 'Copy selected {type}', {
type: $type,
});
Expand All @@ -1037,61 +1069,99 @@ private function blockViewActionMenuItems(): array
type: $type,
});
}
copyBtn.find('.menu-item-label').text(copyLabel);
disclosureMenu.toggleItem(copyBtn[0], !!blocks.length);
btn.find('.menu-item-label').text(copyLabel);
disclosureMenu.toggleItem(btn[0], !!entries.length);
});
}, 1);
})();
JS, [
$view->namespaceInputId($copyAllId),
$view->namespaceInputId($this->getInputId()),
Entry::pluralLowerDisplayName(),
]);
}
$view->namespaceInputId($id),
$view->namespaceInputId($this->getInputId()),
$entrySelector,
$type,
]);

return $items;
return [
'id' => $id,
'icon' => 'clone-dashed',
'color' => \craft\enums\Color::Fuchsia,
'label' => StringHelper::upperCaseFirst(Craft::t('app', 'Copy all {type}', [
'type' => $type,
])),
];
}

private function cardViewActionMenuItems(): array
private function duplicateAction(string $type, string $entrySelector, string $activateJs): array
{
$items = [];
$view = Craft::$app->getView();
return $this->bulkAction($entrySelector, $activateJs, [
'icon' => 'clone',
'label' => StringHelper::upperCaseFirst(Craft::t('app', 'Duplicate selected {type}', [
'type' => $type,
])),
]);
}

// Copy all
if ($this->maxEntries !== 1) {
$copyAllId = sprintf('action-copy-all-%s', mt_rand());
$items[] = [
'id' => $copyAllId,
'icon' => 'clone-dashed',
'color' => \craft\enums\Color::Fuchsia,
'label' => StringHelper::upperCaseFirst(Craft::t('app', 'Copy all {type}', [
'type' => Entry::pluralLowerDisplayName(),
])),
];
private function deleteAction(string $type, string $entrySelector, string $activateJs): array
{
$typeJs = Json::encode($type);
$activateJs = <<<JS
if (confirm(Craft.t('app', 'Are you sure you want to delete the selected {type}?', {
type: $typeJs,
}))) {
$activateJs
}
JS;

return $this->bulkAction($entrySelector, $activateJs, [
'icon' => 'trash',
'label' => StringHelper::upperCaseFirst(Craft::t('app', 'Delete selected {type}', [
'type' => $type,
])),
'destructive' => true,
]);
}

$view->registerJsWithVars(fn($copyAllId, $fieldId) => <<<JS
private function bulkAction(string $entrySelector, string $activateJs, array $item): array
{
$view = Craft::$app->getView();
$id = sprintf('action-%s', mt_rand());

$view->registerJsWithVars(fn($id, $fieldId, $entrySelector) => <<<JS
(() => {
const copyBtn = $('#' + $copyAllId);
const btn = $('#' + $id);
const field = $('#' + $fieldId);
if (field.length) {
copyBtn.on('activate', () => {
Craft.cp.copyElements(field.find('> .nested-element-cards > .elements > li > .element'));
});
} else {
const menu = btn.closest('.menu');

if (!field.length) {
setTimeout(() => {
const menu = copyBtn.closest('.menu').data('disclosureMenu');
menu.removeItem(copyBtn[0]);
menu.data('disclosureMenu').removeItem(btn[0]);
}, 1);
return;
}

const getEntries = () => field.find($entrySelector).filter('.sel').toArray();

btn.on('activate', () => {
$activateJs
});

setTimeout(() => {
const disclosureMenu = menu.data('disclosureMenu');
disclosureMenu.on('show', () => {
disclosureMenu.toggleItem(btn[0], !!getEntries().length);
});
}, 1);
})();
JS, [
$view->namespaceInputId($copyAllId),
$view->namespaceInputId($this->getInputId()),
]);
}
$view->namespaceInputId($id),
$view->namespaceInputId($this->getInputId()),
$entrySelector,
]);

return $items;
return [
...$item,
'id' => $id,
];
}

/**
Expand Down Expand Up @@ -1261,6 +1331,7 @@ private function nestedElementManagerHtml(?ElementInterface $owner, bool $static
if (!$static) {
$entryTypeIdsJs = Json::encode(array_map(fn(EntryType $entryType) => $entryType->id, $entryTypes));
$config += [
'selectable' => true,
'sortable' => true,
'canCreate' => true,
'canPaste' => <<<JS
Expand Down
2 changes: 2 additions & 0 deletions src/translations/en/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@
'Delete photo' => 'Delete photo',
'Delete row {index}' => 'Delete row {index}',
'Delete selected group' => 'Delete selected group',
'Delete selected {type}' => 'Delete selected {type}',
'Delete tags from the “{tagGroup}” tag group' => 'Delete tags from the “{tagGroup}” tag group',
'Delete their content' => 'Delete their content',
'Delete them' => 'Delete them',
Expand Down Expand Up @@ -630,6 +631,7 @@
'Dropdown Options' => 'Dropdown Options',
'Dropdown' => 'Dropdown',
'Duplicate (with descendants)' => 'Duplicate (with descendants)',
'Duplicate selected {type}' => 'Duplicate selected {type}',
'Duplicate' => 'Duplicate',
'Edit Category Group' => 'Edit Category Group',
'Edit Entry Type' => 'Edit Entry Type',
Expand Down
1 change: 1 addition & 0 deletions src/web/assets/cp/CpAsset.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ private function _registerTranslations(View $view): void
'Apply',
'Are you sure you want to close the editor? Any changes will be lost.',
'Are you sure you want to close this screen? Any changes will be lost.',
'Are you sure you want to delete the selected {type}?',
'Are you sure you want to delete this image?',
'Are you sure you want to delete this {type}?',
'Are you sure you want to delete “{name}”?',
Expand Down
2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/cp.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/cp.js.map

Large diffs are not rendered by default.

Loading