From 11152b8073efd4429c2abdc28fc80310ff49f047 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Mon, 31 Jul 2023 11:17:01 +0100 Subject: [PATCH 01/17] WIP on additional menu in the toolbar --- src/base/Element.php | 19 ++++ src/base/ElementInterface.php | 8 ++ src/controllers/ElementsController.php | 101 ++++++++++++++++------ src/elements/Asset.php | 14 ++- src/events/DefineMenuComponentEvent.php | 24 +++++ src/templates/_layouts/cp.twig | 4 +- src/templates/users/_edit.twig | 83 +++++++++--------- src/web/CpScreenResponseBehavior.php | 19 ++++ src/web/CpScreenResponseFormatter.php | 2 + src/web/assets/cp/src/css/_main.scss | 24 +++++ src/web/assets/cp/src/js/ElementEditor.js | 10 ++- 11 files changed, 231 insertions(+), 77 deletions(-) create mode 100644 src/events/DefineMenuComponentEvent.php diff --git a/src/base/Element.php b/src/base/Element.php index 0a37a3a4684..7fcbdafee6c 100644 --- a/src/base/Element.php +++ b/src/base/Element.php @@ -33,6 +33,7 @@ use craft\events\DefineAttributeKeywordsEvent; use craft\events\DefineEagerLoadingMapEvent; use craft\events\DefineHtmlEvent; +use craft\events\DefineMenuComponentEvent; use craft\events\DefineMetadataEvent; use craft\events\DefineUrlEvent; use craft\events\DefineValueEvent; @@ -337,6 +338,13 @@ abstract class Element extends Component implements ElementInterface */ public const EVENT_DEFINE_ADDITIONAL_BUTTONS = 'defineAdditionalButtons'; + /** + * @event DefineMenuComponentEvent The event that is triggered when defining items for the additional menu that shows at the top of the element’s edit page. + * @see getAdditionalMenuItems() + * @since 5.0.0 + */ + public const EVENT_DEFINE_ADDITIONAL_MENU_ITEMS = 'defineAdditionalMenuItems'; + /** * @event DefineHtmlEvent The event that is triggered when defining the HTML for the editor sidebar. * @see getSidebarHtml() @@ -3229,6 +3237,17 @@ public function getAdditionalButtons(): string return $event->html; } + /** + * @inheritdoc + */ + public function getAdditionalMenuItems(): array + { + // Fire a defineAdditionalMenuItems event + $event = new DefineMenuComponentEvent(); + $this->trigger(self::EVENT_DEFINE_ADDITIONAL_MENU_ITEMS, $event); + return $event->components; + } + /** * @inheritdoc */ diff --git a/src/base/ElementInterface.php b/src/base/ElementInterface.php index 668dbbd4b13..547ef48121a 100644 --- a/src/base/ElementInterface.php +++ b/src/base/ElementInterface.php @@ -926,6 +926,14 @@ public function getCpRevisionsUrl(): ?string; */ public function getAdditionalButtons(): string; + /** + * Returns additional items for the disclosure menu that shows at the top of the element’s edit page. + * + * @return array + * @since 5.0.0 + */ + public function getAdditionalMenuItems(): array; + /** * Returns the additional locations that should be available for previewing the element, besides its primary [[getUrl()|URL]]. * diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index 9e58549d4aa..4e51835ee0c 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -387,12 +387,16 @@ public function actionEdit(?ElementInterface $element, ?int $elementId = null): $canSave, $canSaveCanonical, $canCreateDrafts, - $previewTargets, - $enablePreview, $isCurrent, $isUnpublishedDraft, $isDraft )) + ->additionalMenu(fn() => $this->_additionalMenu( + $element, + $canSave, + $previewTargets, + $enablePreview + )) ->notice($element->isProvisionalDraft ? fn() => $this->_draftNotice() : null) ->prepareScreen( fn(Response $response, string $containerId) => $this->_prepareEditor( @@ -688,35 +692,12 @@ private function _additionalButtons( bool $canSave, bool $canSaveCanonical, bool $canCreateDrafts, - ?array $previewTargets, - bool $enablePreview, bool $isCurrent, bool $isUnpublishedDraft, bool $isDraft, ): string { $components = []; - // Preview (View will be added later by JS) - if ($canSave && $previewTargets) { - $components[] = - Html::beginTag('div', [ - 'class' => ['preview-btn-container', 'btngroup'], - ]) . - ($enablePreview - ? Html::beginTag('button', [ - 'type' => 'button', - 'class' => ['preview-btn', 'btn'], - 'aria' => [ - 'label' => Craft::t('app', 'Preview'), - ], - ]) . - Html::tag('span', Craft::t('app', 'Preview'), ['class' => 'label']) . - Html::tag('span', options: ['class' => ['spinner', 'spinner-absolute']]) . - Html::endTag('button') - : '') . - Html::endTag('div'); - } - // Create a draft if ($isCurrent && !$isUnpublishedDraft && $canCreateDrafts) { if ($canSave) { @@ -773,6 +754,76 @@ private function _additionalButtons( return implode("\n", array_filter($components)); } + private function _additionalMenu( + ElementInterface $element, + bool $canSave, + ?array $previewTargets, + bool $enablePreview, + ): string { + $components = []; + + // Preview (View will be added later by JS) + if ($canSave && $previewTargets) { + $components[] = + Html::beginTag('div', [ + 'class' => ['preview-btn-container'], + ]) . + ($enablePreview + ? Html::beginTag('button', [ + 'type' => 'button', + 'class' => ['preview-btn', 'btn'], + 'aria' => [ + 'label' => Craft::t('app', 'Preview'), + ], + ]) . + Html::tag('span', Craft::t('app', 'Preview'), ['class' => 'label']) . + Html::tag('span', options: ['class' => ['spinner', 'spinner-absolute']]) . + Html::endTag('button') + : '') . + Html::endTag('div'); + } + + $components = array_merge($components, $element->getAdditionalMenuItems()); + + if (!empty($components)) { + $additionalMenuId = 'menu' . random_int(100000,999999); + $menuBtn = Html::button('', [ + 'class' => 'btn', + 'id' => 'additional-menu-btn', + 'title' => Craft::t('app', 'Additional Menu'), + 'aria' => [ + 'label' => Craft::t('app', 'Additional Menu'), + 'controls' => $additionalMenuId, + ], + 'data' => [ + 'icon' => 'settings', + 'disclosure-trigger' => true, + ], + 'role' => 'combobox', + ]); + + $menuStart = Html::beginTag('div', [ + 'id' => $additionalMenuId, + 'class' => ['menu menu--disclosure', 'additional-menu'], + ]) . + Html::beginTag('ul'); + + $menuItems = []; + foreach ($components as $component) { + $menuItems[] = Html::beginTag('li') . $component . Html::endTag('li'); + } + + $menuEnd = Html::endTag('ul') . + Html::endTag('div'); + + + return $menuBtn . "\n" . $menuStart . implode("\n", array_filter($menuItems)) . "\n" . $menuEnd; + } + + + return ''; + } + private function _prepareEditor( ElementInterface $element, bool $canSave, diff --git a/src/elements/Asset.php b/src/elements/Asset.php index 6784c404b46..cba1565afe8 100644 --- a/src/elements/Asset.php +++ b/src/elements/Asset.php @@ -1422,16 +1422,16 @@ public function prepareEditScreen(Response $response, string $containerId): void /** * @inheritdoc */ - public function getAdditionalButtons(): string + public function getAdditionalMenuItems(): array { $volume = $this->getVolume(); $user = Craft::$app->getUser()->getIdentity(); $view = Craft::$app->getView(); - $html = Html::beginTag('div', ['class' => 'btngroup']); + $html = []; if (($url = $this->getUrl()) !== null) { - $html .= Html::a(Craft::t('app', 'View'), $url, [ + $html[] = Html::a(Craft::t('app', 'View'), $url, [ 'class' => 'btn', 'target' => '_blank', 'data' => [ @@ -1440,7 +1440,7 @@ public function getAdditionalButtons(): string ]); } - $html .= Html::button(Craft::t('app', 'Download'), [ + $html[] = Html::button(Craft::t('app', 'Download'), [ 'id' => 'download-btn', 'class' => 'btn', 'data' => [ @@ -1464,13 +1464,11 @@ public function getAdditionalButtons(): string JS; $view->registerJs($js); - $html .= Html::endTag('div'); - if ( $user->can("replaceFiles:$volume->uid") && ($user->id === $this->uploaderId || $user->can("replacePeerFiles:$volume->uid")) ) { - $html .= Html::button(Craft::t('app', 'Replace file'), [ + $html[] = Html::button(Craft::t('app', 'Replace file'), [ 'id' => 'replace-btn', 'class' => 'btn', 'data' => [ @@ -1531,7 +1529,7 @@ public function getAdditionalButtons(): string $view->registerJs($js); } - return $html . parent::getAdditionalButtons(); + return $html + parent::getAdditionalMenuItems(); } /** diff --git a/src/events/DefineMenuComponentEvent.php b/src/events/DefineMenuComponentEvent.php new file mode 100644 index 00000000000..6a5bf06f852 --- /dev/null +++ b/src/events/DefineMenuComponentEvent.php @@ -0,0 +1,24 @@ + + * @since 5.0.0 + */ +class DefineMenuComponentEvent extends Event +{ + /** + * @var array The array of UI components HTML + */ + public array $components = []; +} diff --git a/src/templates/_layouts/cp.twig b/src/templates/_layouts/cp.twig index 748b456d0f5..d5011d7e2d7 100644 --- a/src/templates/_layouts/cp.twig +++ b/src/templates/_layouts/cp.twig @@ -71,6 +71,7 @@ {% set toolbar = (toolbar ?? block('toolbar') ?? '')|trim %} {% set actionButton = (block('actionButton') ?? '')|trim %} {% set additionalButtons = additionalButtons ?? null %} +{% set additionalMenu = additionalMenu ?? null %} {% set details = (details ?? block('details') ?? '')|trim %} {% set footer = (footer ?? block('footer') ?? '')|trim %} {% set crumbs = crumbs ?? null %} @@ -217,9 +218,10 @@ {{ toolbar|raw }} {% endif %} - {% if actionButton or additionalButtons %} + {% if actionButton or additionalButtons or additionalMenu %}
{{ additionalButtons|raw }} + {{ additionalMenu|raw }} {{ actionButton|raw }}
{% endif %} diff --git a/src/templates/users/_edit.twig b/src/templates/users/_edit.twig index 17c46df57ea..50242c66509 100644 --- a/src/templates/users/_edit.twig +++ b/src/templates/users/_edit.twig @@ -30,6 +30,48 @@ {% hook "cp.users.edit" %} {% block actionButton %} + {% if actions|length %} +
+ {{ forms.button({ + class: 'menubtn', + attributes: { + id: 'additional-menu-btn', + title: 'Actions'|t('app'), + aria: { + label: 'Actions'|t('app'), + }, + data: { + icon: 'settings', + }, + }, + spinner: true, + }) }} + +
+ {% endif %} + {% if not currentUser.can('registerUsers') or CraftEdition != CraftPro %} {% else %} @@ -349,47 +391,6 @@
{{ "Status"|t('app') }}
{{ statusLabel }}
- {% if actions|length %} -
- {{ forms.button({ - class: 'menubtn', - attributes: { - id: 'action-menubtn', - title: 'Actions'|t('app'), - aria: { - label: 'Actions'|t('app'), - }, - data: { - icon: 'settings', - }, - }, - spinner: true, - }) }} - -
- {% endif %}
diff --git a/src/web/CpScreenResponseBehavior.php b/src/web/CpScreenResponseBehavior.php index b911d193dce..3b969094f64 100644 --- a/src/web/CpScreenResponseBehavior.php +++ b/src/web/CpScreenResponseBehavior.php @@ -161,6 +161,13 @@ class CpScreenResponseBehavior extends Behavior */ public $additionalButtons = null; + /** + * @var string|callable|null Additional menu items’ HTML. + * + * @see additionalMenu() + */ + public $additionalMenu = null; + /** * @var string|callable|null The content HTML. * @see content() @@ -517,6 +524,18 @@ public function additionalButtonsTemplate(string $template, array $variables = [ ); } + /** + * Sets the additional menu's HTML. + * + * @param callable|string|null $value + * @return Response + */ + public function additionalMenu(callable|string|null $value): Response + { + $this->additionalMenu = $value; + return $this->owner; + } + /** * Sets the content HTML. * diff --git a/src/web/CpScreenResponseFormatter.php b/src/web/CpScreenResponseFormatter.php index e5b46add927..68ce2dcdae4 100644 --- a/src/web/CpScreenResponseFormatter.php +++ b/src/web/CpScreenResponseFormatter.php @@ -121,6 +121,7 @@ private function _formatTemplate(YiiResponse $response, CpScreenResponseBehavior $contextMenu = is_callable($behavior->contextMenu) ? call_user_func($behavior->contextMenu) : $behavior->contextMenu; $addlButtons = is_callable($behavior->additionalButtons) ? call_user_func($behavior->additionalButtons) : $behavior->additionalButtons; $altActions = is_callable($behavior->altActions) ? call_user_func($behavior->altActions) : $behavior->altActions; + $addlMenu = is_callable($behavior->additionalMenu) ? call_user_func($behavior->additionalMenu) : $behavior->additionalMenu; $notice = is_callable($behavior->notice) ? call_user_func($behavior->notice) : $behavior->notice; $content = is_callable($behavior->content) ? call_user_func($behavior->content) : ($behavior->content ?? ''); $sidebar = is_callable($behavior->sidebar) ? call_user_func($behavior->sidebar) : $behavior->sidebar; @@ -149,6 +150,7 @@ private function _formatTemplate(YiiResponse $response, CpScreenResponseBehavior 'contextMenu' => $contextMenu, 'submitButtonLabel' => $behavior->submitButtonLabel, 'additionalButtons' => $addlButtons, + 'additionalMenu' => $addlMenu, 'tabs' => $behavior->tabs, 'fullPageForm' => (bool)$behavior->action, 'mainAttributes' => $behavior->mainAttributes, diff --git a/src/web/assets/cp/src/css/_main.scss b/src/web/assets/cp/src/css/_main.scss index 08876119781..30013469603 100644 --- a/src/web/assets/cp/src/css/_main.scss +++ b/src/web/assets/cp/src/css/_main.scss @@ -5576,6 +5576,30 @@ $min2ColWidth: 400px; display: none; position: absolute; + &.additional-menu { + padding: 0; + + li { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + .btn, + a { + background-color: transparent; + border-radius: 0; + max-width: 100%; + text-align: center; + margin: 0; + justify-content: start; + + &:hover { + @include menu-option-active-styles; + } + } + } + h6 { &:first-child { margin-top: 14px !important; diff --git a/src/web/assets/cp/src/js/ElementEditor.js b/src/web/assets/cp/src/js/ElementEditor.js index 7e30741cd1d..e2d732eea03 100644 --- a/src/web/assets/cp/src/js/ElementEditor.js +++ b/src/web/assets/cp/src/js/ElementEditor.js @@ -16,6 +16,8 @@ Craft.ElementEditor = Garnish.Base.extend( $expandSiteStatusesBtn: null, $statusIcon: null, $previewBtn: null, + $additionalMenuBtn: null, + additionalMenu: null, $editMetaBtn: null, metaHud: null, @@ -102,7 +104,11 @@ Craft.ElementEditor = Garnish.Base.extend( this.$revisionBtn = this.$container.find('.context-btn'); this.$revisionLabel = this.$container.find('.revision-label'); - this.$previewBtn = this.$container.find('.preview-btn'); + this.$additionalMenuBtn = this.$container.find('#additional-menu-btn'); + let additionalMenuId = this.$additionalMenuBtn.attr('aria-controls'); + this.$additionalMenuBtn.disclosureMenu(); + this.additionalMenu = $('#' + additionalMenuId); + this.$previewBtn = this.additionalMenu.find('.preview-btn'); const $spinnerContainer = this.isFullPage ? $('#page-title') @@ -134,7 +140,7 @@ Craft.ElementEditor = Garnish.Base.extend( this.addListener(this.$previewBtn, 'click', 'openPreview'); } - const $previewBtnContainer = this.$container.find( + const $previewBtnContainer = this.additionalMenu.find( '.preview-btn-container' ); From f61f17c72ebd7572d52f6e39c7ee91f4cefbed59 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 2 Aug 2023 13:43:29 +0100 Subject: [PATCH 02/17] refactor additional menu WIP --- src/base/Element.php | 4 +- src/base/ElementInterface.php | 2 +- src/controllers/ElementsController.php | 245 ++++++++++-------- src/elements/Asset.php | 190 +++++++------- .../_layouts/components/additional-menu.twig | 74 ++++++ src/templates/_layouts/cp.twig | 6 +- src/templates/users/_edit.twig | 41 ++- src/web/CpScreenResponseBehavior.php | 14 +- src/web/CpScreenResponseFormatter.php | 12 +- src/web/assets/cp/src/css/_main.scss | 5 + src/web/assets/cp/src/js/ElementEditor.js | 5 +- 11 files changed, 359 insertions(+), 239 deletions(-) create mode 100644 src/templates/_layouts/components/additional-menu.twig diff --git a/src/base/Element.php b/src/base/Element.php index 7fcbdafee6c..a93de7ffced 100644 --- a/src/base/Element.php +++ b/src/base/Element.php @@ -340,7 +340,7 @@ abstract class Element extends Component implements ElementInterface /** * @event DefineMenuComponentEvent The event that is triggered when defining items for the additional menu that shows at the top of the element’s edit page. - * @see getAdditionalMenuItems() + * @see getAdditionalMenuComponents() * @since 5.0.0 */ public const EVENT_DEFINE_ADDITIONAL_MENU_ITEMS = 'defineAdditionalMenuItems'; @@ -3240,7 +3240,7 @@ public function getAdditionalButtons(): string /** * @inheritdoc */ - public function getAdditionalMenuItems(): array + public function getAdditionalMenuComponents(): array { // Fire a defineAdditionalMenuItems event $event = new DefineMenuComponentEvent(); diff --git a/src/base/ElementInterface.php b/src/base/ElementInterface.php index 547ef48121a..9fadfa98b87 100644 --- a/src/base/ElementInterface.php +++ b/src/base/ElementInterface.php @@ -932,7 +932,7 @@ public function getAdditionalButtons(): string; * @return array * @since 5.0.0 */ - public function getAdditionalMenuItems(): array; + public function getAdditionalMenuComponents(): array; /** * Returns the additional locations that should be available for previewing the element, besides its primary [[getUrl()|URL]]. diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index 4e51835ee0c..431c5a30e23 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -387,15 +387,23 @@ public function actionEdit(?ElementInterface $element, ?int $elementId = null): $canSave, $canSaveCanonical, $canCreateDrafts, + $previewTargets, + $enablePreview, $isCurrent, $isUnpublishedDraft, $isDraft )) - ->additionalMenu(fn() => $this->_additionalMenu( + ->additionalMenuComponents(fn() => $this->_additionalMenuComponents( $element, - $canSave, - $previewTargets, - $enablePreview + $canonical, + $type, + $redirectUrl, + $isCurrent, + $isUnpublishedDraft, + $isDraft, + $canDeleteForSite, + $canDeleteCanonical, + $canDeleteDraft )) ->notice($element->isProvisionalDraft ? fn() => $this->_draftNotice() : null) ->prepareScreen( @@ -509,54 +517,6 @@ public function actionEdit(?ElementInterface $element, ?int $elementId = null): ]); } } - - if ($canDeleteForSite) { - $response->addAltAction(Craft::t('app', 'Delete {type} for this site', [ - 'type' => $isUnpublishedDraft ? Craft::t('app', 'draft') : $type, - ]), [ - 'destructive' => true, - 'action' => 'elements/delete-for-site', - 'redirect' => "$redirectUrl#", - 'confirm' => Craft::t('app', 'Are you sure you want to delete the {type} for this site?', [ - 'type' => $isUnpublishedDraft ? Craft::t('app', 'draft') : $type, - ]), - ]); - } - - if ($canDeleteCanonical) { - $response->addAltAction(Craft::t('app', 'Delete {type}', [ - 'type' => $isUnpublishedDraft ? Craft::t('app', 'draft') : $type, - ]), [ - 'destructive' => true, - 'action' => $isUnpublishedDraft ? 'elements/delete-draft' : 'elements/delete', - 'redirect' => "$redirectUrl#", - 'confirm' => Craft::t('app', 'Are you sure you want to delete this {type}?', [ - 'type' => $isUnpublishedDraft ? Craft::t('app', 'draft') : $type, - ]), - ]); - } - } elseif ($isDraft && $canDeleteDraft) { - if ($canDeleteForSite) { - $response->addAltAction(Craft::t('app', 'Delete {type} for this site', [ - 'type' => Craft::t('app', 'draft'), - ]), [ - 'destructive' => true, - 'action' => 'elements/delete-for-site', - 'redirect' => "$redirectUrl#", - 'confirm' => Craft::t('app', 'Are you sure you want to delete the {type} for this site?', compact('type')), - ]); - } - - $response->addAltAction(Craft::t('app', 'Delete {type}', [ - 'type' => Craft::t('app', 'draft'), - ]), [ - 'destructive' => true, - 'action' => 'elements/delete-draft', - 'redirect' => $canonical->getCpEditUrl(), - 'confirm' => Craft::t('app', 'Are you sure you want to delete this {type}?', [ - 'type' => Craft::t('app', 'draft'), - ]), - ]); } } @@ -692,12 +652,35 @@ private function _additionalButtons( bool $canSave, bool $canSaveCanonical, bool $canCreateDrafts, + ?array $previewTargets, + bool $enablePreview, bool $isCurrent, bool $isUnpublishedDraft, bool $isDraft, ): string { $components = []; + // Preview (View will be added later by JS) + if ($canSave && $previewTargets) { + $components[] = + Html::beginTag('div', [ + 'class' => ['preview-btn-container', 'btngroup'], + ]) . + ($enablePreview + ? Html::beginTag('button', [ + 'type' => 'button', + 'class' => ['preview-btn', 'btn'], + 'aria' => [ + 'label' => Craft::t('app', 'Preview'), + ], + ]) . + Html::tag('span', Craft::t('app', 'Preview'), ['class' => 'label']) . + Html::tag('span', options: ['class' => ['spinner', 'spinner-absolute']]) . + Html::endTag('button') + : '') . + Html::endTag('div'); + } + // Create a draft if ($isCurrent && !$isUnpublishedDraft && $canCreateDrafts) { if ($canSave) { @@ -754,74 +737,114 @@ private function _additionalButtons( return implode("\n", array_filter($components)); } - private function _additionalMenu( + /** + * Returns a list of components for the additional menu + * + * Component array should consist of: + * 'tag' => a|button + * 'label' => {text to show in the a or button element; can be an empty string} + * 'data' => {data attributes; optional} + * 'aria' => {aria attributes; optional} + * 'options' => {any other attributes, e.g. class; optional} + * + * @param ElementInterface $element + * @param ElementInterface $canonical + * @param string $type + * @param string $redirectUrl + * @param bool $isCurrent + * @param bool $isUnpublishedDraft + * @param bool $isDraft + * @param bool $canDeleteForSite + * @param bool $canDeleteCanonical + * @param bool $canDeleteDraft + * @return array + */ + private function _additionalMenuComponents( ElementInterface $element, - bool $canSave, - ?array $previewTargets, - bool $enablePreview, - ): string { + ElementInterface $canonical, + string $type, + string $redirectUrl, + bool $isCurrent, + bool $isUnpublishedDraft, + bool $isDraft, + bool $canDeleteForSite, + bool $canDeleteCanonical, + bool $canDeleteDraft, + ): array { $components = []; - // Preview (View will be added later by JS) - if ($canSave && $previewTargets) { - $components[] = - Html::beginTag('div', [ - 'class' => ['preview-btn-container'], - ]) . - ($enablePreview - ? Html::beginTag('button', [ - 'type' => 'button', - 'class' => ['preview-btn', 'btn'], - 'aria' => [ - 'label' => Craft::t('app', 'Preview'), - ], - ]) . - Html::tag('span', Craft::t('app', 'Preview'), ['class' => 'label']) . - Html::tag('span', options: ['class' => ['spinner', 'spinner-absolute']]) . - Html::endTag('button') - : '') . - Html::endTag('div'); - } + if ($isCurrent) { + if ($canDeleteForSite) { + $components[] = [ + 'tag' => 'a', + 'label' => Craft::t('app', 'Delete {type} for this site', [ + 'type' => $isUnpublishedDraft ? Craft::t('app', 'draft') : $type, + ]), + 'data' => [ + 'destructive' => true, + 'action' => 'elements/delete-for-site', + 'redirect' => "$redirectUrl#", + 'confirm' => Craft::t('app', 'Are you sure you want to delete the {type} for this site?', [ + 'type' => $isUnpublishedDraft ? Craft::t('app', 'draft') : $type, + ]), + ], + ]; + } - $components = array_merge($components, $element->getAdditionalMenuItems()); + if ($canDeleteCanonical) { + $components[] = [ + 'tag' => 'a', + 'label' => Craft::t('app', 'Delete {type}', [ + 'type' => $isUnpublishedDraft ? Craft::t('app', 'draft') : $type, + ]), + 'options' => [ + 'class' => ['myTestClass'], + ], + 'data' => [ + 'destructive' => true, + 'action' => $isUnpublishedDraft ? 'elements/delete-draft' : 'elements/delete', + 'redirect' => "$redirectUrl#", + 'confirm' => Craft::t('app', 'Are you sure you want to delete this {type}?', [ + 'type' => $isUnpublishedDraft ? Craft::t('app', 'draft') : $type, + ]), + ], + ]; + } + } elseif ($isDraft && $canDeleteDraft) { + if ($canDeleteForSite) { + $components[] = [ + 'tag' => 'a', + 'label' => Craft::t('app', 'Delete {type} for this site', [ + 'type' => Craft::t('app', 'draft'), + ]), + 'data' => [ + 'destructive' => true, + 'action' => 'elements/delete-for-site', + 'redirect' => "$redirectUrl#", + 'confirm' => Craft::t('app', 'Are you sure you want to delete the {type} for this site?', + compact('type') + ), + ], + ]; + } - if (!empty($components)) { - $additionalMenuId = 'menu' . random_int(100000,999999); - $menuBtn = Html::button('', [ - 'class' => 'btn', - 'id' => 'additional-menu-btn', - 'title' => Craft::t('app', 'Additional Menu'), - 'aria' => [ - 'label' => Craft::t('app', 'Additional Menu'), - 'controls' => $additionalMenuId, - ], + $components[] = [ + 'tag' => 'a', + 'label' => Craft::t('app', 'Delete {type}', [ + 'type' => Craft::t('app', 'draft'), + ]), 'data' => [ - 'icon' => 'settings', - 'disclosure-trigger' => true, + 'destructive' => true, + 'action' => 'elements/delete-draft', + 'redirect' => $canonical->getCpEditUrl(), + 'confirm' => Craft::t('app', 'Are you sure you want to delete this {type}?', [ + 'type' => Craft::t('app', 'draft'), + ]), ], - 'role' => 'combobox', - ]); - - $menuStart = Html::beginTag('div', [ - 'id' => $additionalMenuId, - 'class' => ['menu menu--disclosure', 'additional-menu'], - ]) . - Html::beginTag('ul'); - - $menuItems = []; - foreach ($components as $component) { - $menuItems[] = Html::beginTag('li') . $component . Html::endTag('li'); - } - - $menuEnd = Html::endTag('ul') . - Html::endTag('div'); - - - return $menuBtn . "\n" . $menuStart . implode("\n", array_filter($menuItems)) . "\n" . $menuEnd; + ]; } - - return ''; + return array_merge($components, $element->getAdditionalMenuComponents()); } private function _prepareEditor( diff --git a/src/elements/Asset.php b/src/elements/Asset.php index cba1565afe8..ccb1de2238d 100644 --- a/src/elements/Asset.php +++ b/src/elements/Asset.php @@ -1422,34 +1422,52 @@ public function prepareEditScreen(Response $response, string $containerId): void /** * @inheritdoc */ - public function getAdditionalMenuItems(): array + public function getAdditionalMenuComponents(): array { $volume = $this->getVolume(); - $user = Craft::$app->getUser()->getIdentity(); + $userSession = Craft::$app->getUser(); + $user = $userSession->getIdentity(); $view = Craft::$app->getView(); - $html = []; + $previewable = Craft::$app->getAssets()->getAssetPreviewHandler($this) !== null; + $editable = ( + $this->getSupportsImageEditor() && + $userSession->checkPermission("editImages:$volume->uid") && + ($userSession->getId() == $this->uploaderId || $userSession->checkPermission("editPeerImages:$volume->uid")) + ); + $isMobile = Craft::$app->getRequest()->isMobileBrowser(true); + + $components = []; if (($url = $this->getUrl()) !== null) { - $html[] = Html::a(Craft::t('app', 'View'), $url, [ - 'class' => 'btn', - 'target' => '_blank', + $components[] = [ + 'tag' => 'a', + 'label' => Craft::t('app', 'View'), + 'options' => [ + 'href' => $url, + 'class' => 'btn', + 'target' => '_blank', + ], 'data' => [ 'icon' => 'preview', ], - ]); + ]; } - $html[] = Html::button(Craft::t('app', 'Download'), [ - 'id' => 'download-btn', - 'class' => 'btn', + $components[] = [ + 'tag' => 'button', + 'label' => Craft::t('app', 'Download'), + 'options' => [ + 'id' => 'download-btn', + 'class' => 'btn', + ], 'data' => [ 'icon' => 'download', ], 'aria' => [ 'label' => Craft::t('app', 'Download'), ], - ]); + ]; $js = << { @@ -1468,13 +1486,17 @@ public function getAdditionalMenuItems(): array $user->can("replaceFiles:$volume->uid") && ($user->id === $this->uploaderId || $user->can("replacePeerFiles:$volume->uid")) ) { - $html[] = Html::button(Craft::t('app', 'Replace file'), [ - 'id' => 'replace-btn', - 'class' => 'btn', + $components[] = [ + 'tag' => 'button', + 'label' => Craft::t('app', 'Replace file'), + 'options' => [ + 'id' => 'replace-btn', + 'class' => 'btn', + ], 'data' => [ 'icon' => 'upload', ], - ]); + ]; $dimensionsLabel = Html::encode(Craft::t('app', 'Dimensions')); $updatePreviewThumbJs = $this->_updatePreviewThumbJs(); @@ -1529,7 +1551,69 @@ public function getAdditionalMenuItems(): array $view->registerJs($js); } - return $html + parent::getAdditionalMenuItems(); + if ($previewable) { + $components[] = [ + 'tag' => 'button', + 'label' => Craft::t('app', 'Preview'), + 'options' => [ + 'id' => 'preview-btn', + 'class' => ['btn', 'preview-btn'], + ], + ]; + + $previewBtnId = $view->namespaceInputId('preview-btn'); + $settings = []; + $width = $this->getWidth(); + $height = $this->getHeight(); + if ($width && $height) { + $settings['startingWidth'] = $width; + $settings['startingHeight'] = $height; + } + $jsSettings = Json::encode($settings); + $js = << { + new Craft.PreviewFileModal($this->id, null, $jsSettings); +}); +JS; + $view->registerJs($js); + } + + if ($editable) { + $components[] = [ + 'tag' => 'button', + 'label' => Craft::t('app', 'Edit Image'), + 'options' => [ + 'id' => 'edit-btn', + 'class' => ['btn', 'edit-btn'], + ], + ]; + + $editBtnId = $view->namespaceInputId('edit-btn'); + $updatePreviewThumbJs = $this->_updatePreviewThumbJs(); + $js = << { + new Craft.AssetImageEditor($this->id, { + allowDegreeFractions: Craft.isImagick, + onSave: data => { + if (data.newAssetId) { + // If this is within an Assets field’s editor slideout, replace the selected asset + const slideout = $('#$editBtnId').closest('[data-slideout]').data('slideout'); + if (slideout && slideout.settings.elementSelectInput) { + slideout.settings.elementSelectInput.replaceElement(slideout.\$element.data('id'), data.newAssetId) + .catch(() => {}); + } + return; + } + + $updatePreviewThumbJs + }, + }); +}); +JS; + $view->registerJs($js); + } + + return array_merge($components, parent::getAdditionalMenuComponents()); } /** @@ -2485,80 +2569,6 @@ public function getPreviewHtml(): string ]) . Html::endTag('div'); // .preview-thumb-container; - if ($previewable || $editable) { - $isMobile = Craft::$app->getRequest()->isMobileBrowser(true); - $imageButtonHtml = Html::beginTag('div', [ - 'class' => array_filter([ - 'image-actions', - 'buttons', - ($isMobile ? 'is-mobile' : null), - ]), - ]); - $view = Craft::$app->getView(); - - if ($previewable) { - $imageButtonHtml .= Html::button(Craft::t('app', 'Preview'), [ - 'id' => 'preview-btn', - 'class' => ['btn', 'preview-btn'], - ]); - - $previewBtnId = $view->namespaceInputId('preview-btn'); - $settings = []; - $width = $this->getWidth(); - $height = $this->getHeight(); - if ($width && $height) { - $settings['startingWidth'] = $width; - $settings['startingHeight'] = $height; - } - $jsSettings = Json::encode($settings); - $js = << { - new Craft.PreviewFileModal($this->id, null, $jsSettings); -}); -JS; - $view->registerJs($js); - } - - if ($editable) { - $imageButtonHtml .= Html::button(Craft::t('app', 'Edit Image'), [ - 'id' => 'edit-btn', - 'class' => ['btn', 'edit-btn'], - ]); - - $editBtnId = $view->namespaceInputId('edit-btn'); - $updatePreviewThumbJs = $this->_updatePreviewThumbJs(); - $js = << { - new Craft.AssetImageEditor($this->id, { - allowDegreeFractions: Craft.isImagick, - onSave: data => { - if (data.newAssetId) { - // If this is within an Assets field’s editor slideout, replace the selected asset - const slideout = $('#$editBtnId').closest('[data-slideout]').data('slideout'); - if (slideout && slideout.settings.elementSelectInput) { - slideout.settings.elementSelectInput.replaceElement(slideout.\$element.data('id'), data.newAssetId) - .catch(() => {}); - } - return; - } - - $updatePreviewThumbJs - }, - }); -}); -JS; - $view->registerJs($js); - } - - $imageButtonHtml .= Html::endTag('div'); // .image-actions - - if (Craft::$app->getRequest()->isMobileBrowser(true)) { - $previewThumbHtml .= $imageButtonHtml; - } else { - $previewThumbHtml = Html::appendToTag($previewThumbHtml, $imageButtonHtml); - } - } - $html .= $previewThumbHtml; } catch (NotSupportedException) { // NBD diff --git a/src/templates/_layouts/components/additional-menu.twig b/src/templates/_layouts/components/additional-menu.twig new file mode 100644 index 00000000000..52381b86e1d --- /dev/null +++ b/src/templates/_layouts/components/additional-menu.twig @@ -0,0 +1,74 @@ +{% set menuId = 'menu' ~ random(100000,999999) %} + +{% set safeComponents = additionalMenuComponents|filter(a => not (a.data.destructive ?? false)) %} +{% set destructiveComponents = additionalMenuComponents|filter(a => a.data.destructive ?? false) %} + +{% macro renderComponent(component, destructive) %} + {% set dataAttrs = [] %} + {% if component.data is defined and component.data is not empty %} + {% for key,value in component.data %} + {% set dataAttrs = dataAttrs|merge({(key): (value)}) %} + {% endfor %} + {% endif %} + + {% set ariaAttrs = [] %} + {% if component.aria is defined and component.aria is not empty %} + {% for key,value in component.aria %} + {% set ariaAttrs = ariaAttrs|merge({(key): (value)}) %} + {% endfor %} + {% endif %} + + {% set otherAttrs = [] %} + {% set additionalClasses = [] %} + {% if component.options is defined and component.options is not empty %} + {% for key,value in component.options %} + {% if key == 'class' %} + {% if component.options.class is iterable %} + {% set additionalClasses = value %} + {% else %} + {% set additionalClasses = value|split(',') %} + {% endif %} + {% else %} + {% set otherAttrs = otherAttrs|merge({(key): (value)}) %} + {% endif %} + {% endfor %} + {% endif %} + + {% set componentAttributes = { + class: [ + 'formsubmit', + (destructive ?? false) ? 'error' + ]|merge(additionalClasses), + data: dataAttrs, + aria: ariaAttrs, + }|merge(otherAttrs) %} + + <{{ component.tag }} {{ attr(componentAttributes) }}> + {{ component.label }} + + +{% endmacro %} + +{% if safeComponents is not empty or destructiveComponents is not empty %} + + + +{% endif %} \ No newline at end of file diff --git a/src/templates/_layouts/cp.twig b/src/templates/_layouts/cp.twig index d5011d7e2d7..88442dd7980 100644 --- a/src/templates/_layouts/cp.twig +++ b/src/templates/_layouts/cp.twig @@ -71,7 +71,7 @@ {% set toolbar = (toolbar ?? block('toolbar') ?? '')|trim %} {% set actionButton = (block('actionButton') ?? '')|trim %} {% set additionalButtons = additionalButtons ?? null %} -{% set additionalMenu = additionalMenu ?? null %} +{% set additionalMenuComponents = additionalMenuComponents ?? [] %} {% set details = (details ?? block('details') ?? '')|trim %} {% set footer = (footer ?? block('footer') ?? '')|trim %} {% set crumbs = crumbs ?? null %} @@ -218,11 +218,11 @@ {{ toolbar|raw }} {% endif %} - {% if actionButton or additionalButtons or additionalMenu %} + {% if actionButton or additionalButtons or additionalMenuComponents %}
{{ additionalButtons|raw }} - {{ additionalMenu|raw }} {{ actionButton|raw }} + {% include '_layouts/components/additional-menu' with additionalMenuComponents %}
{% endif %} {% endblock %} diff --git a/src/templates/users/_edit.twig b/src/templates/users/_edit.twig index 50242c66509..02dc2e23271 100644 --- a/src/templates/users/_edit.twig +++ b/src/templates/users/_edit.twig @@ -30,6 +30,26 @@ {% hook "cp.users.edit" %} {% block actionButton %} + {% if not currentUser.can('registerUsers') or CraftEdition != CraftPro %} + + {% else %} + + {% endif %} + {% if actions|length %}
{{ forms.button({ @@ -71,27 +91,6 @@
{% endif %} - - {% if not currentUser.can('registerUsers') or CraftEdition != CraftPro %} - - {% else %} - - {% endif %} - {% endblock %} {% block content %} diff --git a/src/web/CpScreenResponseBehavior.php b/src/web/CpScreenResponseBehavior.php index 3b969094f64..036cabfdadf 100644 --- a/src/web/CpScreenResponseBehavior.php +++ b/src/web/CpScreenResponseBehavior.php @@ -162,11 +162,11 @@ class CpScreenResponseBehavior extends Behavior public $additionalButtons = null; /** - * @var string|callable|null Additional menu items’ HTML. + * @var array|callable|null Additional menu items. * - * @see additionalMenu() + * @see additionalMenuComponents() */ - public $additionalMenu = null; + public $additionalMenuComponents = null; /** * @var string|callable|null The content HTML. @@ -525,14 +525,14 @@ public function additionalButtonsTemplate(string $template, array $variables = [ } /** - * Sets the additional menu's HTML. + * Sets the components to show in the additional menu. * - * @param callable|string|null $value + * @param callable|array|null $value * @return Response */ - public function additionalMenu(callable|string|null $value): Response + public function additionalMenuComponents(callable|array|null $value): Response { - $this->additionalMenu = $value; + $this->additionalMenuComponents = $value; return $this->owner; } diff --git a/src/web/CpScreenResponseFormatter.php b/src/web/CpScreenResponseFormatter.php index 68ce2dcdae4..a545ec1eb67 100644 --- a/src/web/CpScreenResponseFormatter.php +++ b/src/web/CpScreenResponseFormatter.php @@ -121,7 +121,7 @@ private function _formatTemplate(YiiResponse $response, CpScreenResponseBehavior $contextMenu = is_callable($behavior->contextMenu) ? call_user_func($behavior->contextMenu) : $behavior->contextMenu; $addlButtons = is_callable($behavior->additionalButtons) ? call_user_func($behavior->additionalButtons) : $behavior->additionalButtons; $altActions = is_callable($behavior->altActions) ? call_user_func($behavior->altActions) : $behavior->altActions; - $addlMenu = is_callable($behavior->additionalMenu) ? call_user_func($behavior->additionalMenu) : $behavior->additionalMenu; + $addlMenuComponents = is_callable($behavior->additionalMenuComponents) ? call_user_func($behavior->additionalMenuComponents) : $behavior->additionalMenuComponents; $notice = is_callable($behavior->notice) ? call_user_func($behavior->notice) : $behavior->notice; $content = is_callable($behavior->content) ? call_user_func($behavior->content) : ($behavior->content ?? ''); $sidebar = is_callable($behavior->sidebar) ? call_user_func($behavior->sidebar) : $behavior->sidebar; @@ -150,7 +150,15 @@ private function _formatTemplate(YiiResponse $response, CpScreenResponseBehavior 'contextMenu' => $contextMenu, 'submitButtonLabel' => $behavior->submitButtonLabel, 'additionalButtons' => $addlButtons, - 'additionalMenu' => $addlMenu, + 'additionalMenuComponents' => array_map(function(array $component) use ($security): array { + if (isset($component['options']['redirect'])) { + $component['options']['redirect'] = $security->hashData($component['options']['redirect']); + } + if (isset($component['data']['redirect'])) { + $component['data']['redirect'] = $security->hashData($component['data']['redirect']); + } + return $component; + }, $addlMenuComponents ?? []), 'tabs' => $behavior->tabs, 'fullPageForm' => (bool)$behavior->action, 'mainAttributes' => $behavior->mainAttributes, diff --git a/src/web/assets/cp/src/css/_main.scss b/src/web/assets/cp/src/css/_main.scss index 30013469603..7bdd8116c1d 100644 --- a/src/web/assets/cp/src/css/_main.scss +++ b/src/web/assets/cp/src/css/_main.scss @@ -5598,6 +5598,11 @@ $min2ColWidth: 400px; @include menu-option-active-styles; } } + + hr { + max-width: 100%; + margin: 5px 0; + } } h6 { diff --git a/src/web/assets/cp/src/js/ElementEditor.js b/src/web/assets/cp/src/js/ElementEditor.js index e2d732eea03..0399ce6f8ff 100644 --- a/src/web/assets/cp/src/js/ElementEditor.js +++ b/src/web/assets/cp/src/js/ElementEditor.js @@ -104,11 +104,12 @@ Craft.ElementEditor = Garnish.Base.extend( this.$revisionBtn = this.$container.find('.context-btn'); this.$revisionLabel = this.$container.find('.revision-label'); + this.$previewBtn = this.$container.find('.preview-btn'); + this.$additionalMenuBtn = this.$container.find('#additional-menu-btn'); let additionalMenuId = this.$additionalMenuBtn.attr('aria-controls'); this.$additionalMenuBtn.disclosureMenu(); this.additionalMenu = $('#' + additionalMenuId); - this.$previewBtn = this.additionalMenu.find('.preview-btn'); const $spinnerContainer = this.isFullPage ? $('#page-title') @@ -140,7 +141,7 @@ Craft.ElementEditor = Garnish.Base.extend( this.addListener(this.$previewBtn, 'click', 'openPreview'); } - const $previewBtnContainer = this.additionalMenu.find( + const $previewBtnContainer = this.$container.find( '.preview-btn-container' ); From 93994f1aded22073f83fd8e0cf583591b6068c94 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 2 Aug 2023 14:31:40 +0100 Subject: [PATCH 03/17] use anchor tag as default --- src/controllers/ElementsController.php | 4 -- src/elements/Asset.php | 7 ++- .../_layouts/components/additional-menu.twig | 58 ++++++++++--------- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index 431c5a30e23..032304cea1b 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -776,7 +776,6 @@ private function _additionalMenuComponents( if ($isCurrent) { if ($canDeleteForSite) { $components[] = [ - 'tag' => 'a', 'label' => Craft::t('app', 'Delete {type} for this site', [ 'type' => $isUnpublishedDraft ? Craft::t('app', 'draft') : $type, ]), @@ -793,7 +792,6 @@ private function _additionalMenuComponents( if ($canDeleteCanonical) { $components[] = [ - 'tag' => 'a', 'label' => Craft::t('app', 'Delete {type}', [ 'type' => $isUnpublishedDraft ? Craft::t('app', 'draft') : $type, ]), @@ -813,7 +811,6 @@ private function _additionalMenuComponents( } elseif ($isDraft && $canDeleteDraft) { if ($canDeleteForSite) { $components[] = [ - 'tag' => 'a', 'label' => Craft::t('app', 'Delete {type} for this site', [ 'type' => Craft::t('app', 'draft'), ]), @@ -829,7 +826,6 @@ private function _additionalMenuComponents( } $components[] = [ - 'tag' => 'a', 'label' => Craft::t('app', 'Delete {type}', [ 'type' => Craft::t('app', 'draft'), ]), diff --git a/src/elements/Asset.php b/src/elements/Asset.php index ccb1de2238d..696fa04e86f 100644 --- a/src/elements/Asset.php +++ b/src/elements/Asset.php @@ -1435,13 +1435,11 @@ public function getAdditionalMenuComponents(): array $userSession->checkPermission("editImages:$volume->uid") && ($userSession->getId() == $this->uploaderId || $userSession->checkPermission("editPeerImages:$volume->uid")) ); - $isMobile = Craft::$app->getRequest()->isMobileBrowser(true); $components = []; if (($url = $this->getUrl()) !== null) { $components[] = [ - 'tag' => 'a', 'label' => Craft::t('app', 'View'), 'options' => [ 'href' => $url, @@ -1557,7 +1555,10 @@ public function getAdditionalMenuComponents(): array 'label' => Craft::t('app', 'Preview'), 'options' => [ 'id' => 'preview-btn', - 'class' => ['btn', 'preview-btn'], + 'class' => ['btn'], + ], + 'data' => [ + 'icon' => 'view', ], ]; diff --git a/src/templates/_layouts/components/additional-menu.twig b/src/templates/_layouts/components/additional-menu.twig index 52381b86e1d..3206c098515 100644 --- a/src/templates/_layouts/components/additional-menu.twig +++ b/src/templates/_layouts/components/additional-menu.twig @@ -3,7 +3,33 @@ {% set safeComponents = additionalMenuComponents|filter(a => not (a.data.destructive ?? false)) %} {% set destructiveComponents = additionalMenuComponents|filter(a => a.data.destructive ?? false) %} +{% if safeComponents is not empty or destructiveComponents is not empty %} + + + +{% endif %} + {% macro renderComponent(component, destructive) %} + {% set tag = component.tag ?? 'a' %} + {% set dataAttrs = [] %} {% if component.data is defined and component.data is not empty %} {% for key,value in component.data %} @@ -43,32 +69,8 @@ aria: ariaAttrs, }|merge(otherAttrs) %} - <{{ component.tag }} {{ attr(componentAttributes) }}> - {{ component.label }} - + <{{ tag }} {{ attr(componentAttributes) }}> + {{ component.label }} + -{% endmacro %} - -{% if safeComponents is not empty or destructiveComponents is not empty %} - - - -{% endif %} \ No newline at end of file +{% endmacro %} \ No newline at end of file From 6d5924b31abf17a9211cdefe0874f0ada87cd422 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 2 Aug 2023 14:32:25 +0100 Subject: [PATCH 04/17] edit user to use the additional menu, not its own version --- src/controllers/UsersController.php | 62 ++++++++++++++++++++--------- src/templates/users/_edit.twig | 42 ------------------- 2 files changed, 44 insertions(+), 60 deletions(-) diff --git a/src/controllers/UsersController.php b/src/controllers/UsersController.php index 8650511d328..215da26a2a9 100644 --- a/src/controllers/UsersController.php +++ b/src/controllers/UsersController.php @@ -871,7 +871,9 @@ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array $statusLabel = $user->archived ? Craft::t('app', 'Archived') : Craft::t('app', 'Disabled'); if (Craft::$app->getElements()->canSave($user)) { $statusActions[] = [ - 'action' => 'users/enable-user', + 'data' => [ + 'action' => 'users/enable-user', + ], 'label' => Craft::t('app', 'Enable'), ]; } @@ -883,7 +885,9 @@ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array if ($user->email) { if ($user->pending || $canAdministrateUsers) { $statusActions[] = [ - 'action' => 'users/send-activation-email', + 'data' => [ + 'action' => 'users/send-activation-email', + ], 'label' => Craft::t('app', 'Send activation email'), ]; } @@ -891,12 +895,16 @@ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array // Only need to show the "Copy activation URL" option if they don't have a password if (!$user->password) { $statusActions[] = [ - 'id' => 'copy-passwordreset-url', + 'options' => [ + 'id' => 'copy-passwordreset-url', + ], 'label' => Craft::t('app', 'Copy activation URL…'), ]; } $statusActions[] = [ - 'action' => 'users/activate-user', + 'data' => [ + 'action' => 'users/activate-user', + ], 'label' => Craft::t('app', 'Activate account'), ]; } @@ -906,7 +914,9 @@ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array $statusLabel = Craft::t('app', 'Suspended'); if (Craft::$app->getUsers()->canSuspend($currentUser, $user)) { $statusActions[] = [ - 'action' => 'users/unsuspend-user', + 'data' => [ + 'action' => 'users/unsuspend-user', + ], 'label' => Craft::t('app', 'Unsuspend'), ]; } @@ -924,7 +934,9 @@ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array ) ) { $statusActions[] = [ - 'action' => 'users/unlock-user', + 'data' => [ + 'action' => 'users/unlock-user', + ], 'label' => Craft::t('app', 'Unlock'), ]; } @@ -934,12 +946,16 @@ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array if (!$isCurrentUser) { $statusActions[] = [ - 'action' => 'users/send-password-reset-email', + 'data' => [ + 'action' => 'users/send-password-reset-email', + ], 'label' => Craft::t('app', 'Send password reset email'), ]; if ($canAdministrateUsers) { $statusActions[] = [ - 'id' => 'copy-passwordreset-url', + 'options' => [ + 'id' => 'copy-passwordreset-url', + ], 'label' => Craft::t('app', 'Copy password reset URL…'), ]; } @@ -950,20 +966,26 @@ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array if (!$isCurrentUser) { if (Craft::$app->getUsers()->canImpersonate($currentUser, $user)) { $sessionActions[] = [ - 'action' => 'users/impersonate', + 'data' => [ + 'action' => 'users/impersonate', + ], 'label' => $name ? Craft::t('app', 'Sign in as {user}', ['user' => $user->getName()]) : Craft::t('app', 'Sign in as user'), ]; $sessionActions[] = [ - 'id' => 'copy-impersonation-url', + 'options' => [ + 'id' => 'copy-impersonation-url', + ], 'label' => Craft::t('app', 'Copy impersonation URL…'), ]; } if (Craft::$app->getUsers()->canSuspend($currentUser, $user) && $user->active && !$user->suspended) { $destructiveActions[] = [ - 'action' => 'users/suspend-user', + 'data' => [ + 'action' => 'users/suspend-user', + ], 'label' => Craft::t('app', 'Suspend'), ]; } @@ -973,15 +995,19 @@ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array if (!$user->admin || $currentUser->admin) { if (($isCurrentUser || $canAdministrateUsers) && ($user->active || $user->pending)) { $destructiveActions[] = [ - 'action' => 'users/deactivate-user', + 'data' => [ + 'action' => 'users/deactivate-user', + 'confirm' => Craft::t('app', 'Deactivating a user revokes their ability to sign in. Are you sure you want to continue?'), + ], 'label' => Craft::t('app', 'Deactivate…'), - 'confirm' => Craft::t('app', 'Deactivating a user revokes their ability to sign in. Are you sure you want to continue?'), ]; } if ($isCurrentUser || $currentUser->can('deleteUsers')) { $destructiveActions[] = [ - 'id' => 'delete-btn', + 'options' => [ + 'id' => 'delete-btn', + ], 'label' => Craft::t('app', 'Delete…'), ]; } @@ -998,15 +1024,15 @@ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array ]); $this->trigger(self::EVENT_REGISTER_USER_ACTIONS, $event); - $actions = array_filter([ + $additionalMenuComponents = array_filter(array_merge( $event->statusActions, $event->miscActions, $event->sessionActions, array_map(function(array $action): array { - $action['destructive'] = true; + $action['data']['destructive'] = true; return $action; }, $event->destructiveActions), - ]); + )); // Set the appropriate page title // --------------------------------------------------------------------- @@ -1186,7 +1212,7 @@ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array 'user', 'isNewUser', 'statusLabel', - 'actions', + 'additionalMenuComponents', 'languageOptions', 'localeOptions', 'userLanguage', diff --git a/src/templates/users/_edit.twig b/src/templates/users/_edit.twig index 02dc2e23271..56f19aa3618 100644 --- a/src/templates/users/_edit.twig +++ b/src/templates/users/_edit.twig @@ -49,48 +49,6 @@ {% endif %} - - {% if actions|length %} -
- {{ forms.button({ - class: 'menubtn', - attributes: { - id: 'additional-menu-btn', - title: 'Actions'|t('app'), - aria: { - label: 'Actions'|t('app'), - }, - data: { - icon: 'settings', - }, - }, - spinner: true, - }) }} - -
- {% endif %} {% endblock %} {% block content %} From c832d97edebd0124c60443d934447511a0f4414d Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 2 Aug 2023 15:10:36 +0100 Subject: [PATCH 05/17] tweaks to support grouping - tbc --- src/controllers/ElementsController.php | 2 +- src/controllers/UsersController.php | 3 +++ src/elements/Asset.php | 5 +++- src/events/DefineMenuComponentEvent.php | 4 +-- .../_layouts/components/additional-menu.twig | 25 +++++++++++-------- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index 032304cea1b..a3b747b0449 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -741,8 +741,8 @@ private function _additionalButtons( * Returns a list of components for the additional menu * * Component array should consist of: - * 'tag' => a|button * 'label' => {text to show in the a or button element; can be an empty string} + * 'tag' => {optional, defaults to "a"} * 'data' => {data attributes; optional} * 'aria' => {aria attributes; optional} * 'options' => {any other attributes, e.g. class; optional} diff --git a/src/controllers/UsersController.php b/src/controllers/UsersController.php index 215da26a2a9..4e43287a970 100644 --- a/src/controllers/UsersController.php +++ b/src/controllers/UsersController.php @@ -1026,8 +1026,11 @@ public function actionEditUser(mixed $userId = null, ?User $user = null, ?array $additionalMenuComponents = array_filter(array_merge( $event->statusActions, + (!empty($event->statusActions) ? [['tag' => 'hr']] : []), $event->miscActions, + (!empty($event->miscActions) ? [['tag' => 'hr']] : []), $event->sessionActions, + (!empty($event->sessionActions) ? [['tag' => 'hr']] : []), array_map(function(array $action): array { $action['data']['destructive'] = true; return $action; diff --git a/src/elements/Asset.php b/src/elements/Asset.php index 696fa04e86f..dc7a120047b 100644 --- a/src/elements/Asset.php +++ b/src/elements/Asset.php @@ -1447,7 +1447,7 @@ public function getAdditionalMenuComponents(): array 'target' => '_blank', ], 'data' => [ - 'icon' => 'preview', + 'icon' => 'share', ], ]; } @@ -1587,6 +1587,9 @@ public function getAdditionalMenuComponents(): array 'id' => 'edit-btn', 'class' => ['btn', 'edit-btn'], ], + 'data' => [ + 'icon' => 'edit', + ], ]; $editBtnId = $view->namespaceInputId('edit-btn'); diff --git a/src/events/DefineMenuComponentEvent.php b/src/events/DefineMenuComponentEvent.php index 6a5bf06f852..6ac0277f6f3 100644 --- a/src/events/DefineMenuComponentEvent.php +++ b/src/events/DefineMenuComponentEvent.php @@ -10,7 +10,7 @@ use yii\base\Event; /** - * DefineMenuComponentEvent is used to define the HTML for components that are added to the additional menu. + * DefineMenuComponentEvent is used to define the list of components that are added to the additional menu. * * @author Pixel & Tonic, Inc. * @since 5.0.0 @@ -18,7 +18,7 @@ class DefineMenuComponentEvent extends Event { /** - * @var array The array of UI components HTML + * @var array The array of components to be added to the additional menu */ public array $components = []; } diff --git a/src/templates/_layouts/components/additional-menu.twig b/src/templates/_layouts/components/additional-menu.twig index 3206c098515..59a9f32032b 100644 --- a/src/templates/_layouts/components/additional-menu.twig +++ b/src/templates/_layouts/components/additional-menu.twig @@ -11,17 +11,16 @@ @@ -69,8 +68,14 @@ aria: ariaAttrs, }|merge(otherAttrs) %} - <{{ tag }} {{ attr(componentAttributes) }}> - {{ component.label }} - + {% if tag == 'hr' %} +
+ {% else %} +
  • + <{{ tag }} {{ attr(componentAttributes) }}> + {{ component.label }} + +
  • + {% endif %} {% endmacro %} \ No newline at end of file From b7c6c6ef95453336eec9fdae2f6ea8f77db60576 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 16 Aug 2023 09:36:30 +0100 Subject: [PATCH 06/17] asset additional components + cleanup --- src/controllers/ElementsController.php | 3 - src/elements/Asset.php | 121 +++++++++++++------------ src/templates/_layouts/cp.twig | 2 +- 3 files changed, 64 insertions(+), 62 deletions(-) diff --git a/src/controllers/ElementsController.php b/src/controllers/ElementsController.php index a3b747b0449..87e02770ed6 100644 --- a/src/controllers/ElementsController.php +++ b/src/controllers/ElementsController.php @@ -795,9 +795,6 @@ private function _additionalMenuComponents( 'label' => Craft::t('app', 'Delete {type}', [ 'type' => $isUnpublishedDraft ? Craft::t('app', 'draft') : $type, ]), - 'options' => [ - 'class' => ['myTestClass'], - ], 'data' => [ 'destructive' => true, 'action' => $isUnpublishedDraft ? 'elements/delete-draft' : 'elements/delete', diff --git a/src/elements/Asset.php b/src/elements/Asset.php index dc7a120047b..f134b09b16a 100644 --- a/src/elements/Asset.php +++ b/src/elements/Asset.php @@ -1549,72 +1549,77 @@ public function getAdditionalMenuComponents(): array $view->registerJs($js); } - if ($previewable) { + if ($previewable || $editable) { $components[] = [ - 'tag' => 'button', - 'label' => Craft::t('app', 'Preview'), - 'options' => [ - 'id' => 'preview-btn', - 'class' => ['btn'], - ], - 'data' => [ - 'icon' => 'view', - ], + 'tag' => 'hr', ]; + if ($previewable) { + $components[] = [ + 'tag' => 'button', + 'label' => Craft::t('app', 'Preview'), + 'options' => [ + 'id' => 'preview-btn', + 'class' => ['btn'], + ], + 'data' => [ + 'icon' => 'view', + ], + ]; - $previewBtnId = $view->namespaceInputId('preview-btn'); - $settings = []; - $width = $this->getWidth(); - $height = $this->getHeight(); - if ($width && $height) { - $settings['startingWidth'] = $width; - $settings['startingHeight'] = $height; + $previewBtnId = $view->namespaceInputId('preview-btn'); + $settings = []; + $width = $this->getWidth(); + $height = $this->getHeight(); + if ($width && $height) { + $settings['startingWidth'] = $width; + $settings['startingHeight'] = $height; + } + $jsSettings = Json::encode($settings); + $js = << { + new Craft.PreviewFileModal($this->id, null, $jsSettings); + }); + JS; + $view->registerJs($js); } - $jsSettings = Json::encode($settings); - $js = << { - new Craft.PreviewFileModal($this->id, null, $jsSettings); -}); -JS; - $view->registerJs($js); - } - if ($editable) { - $components[] = [ - 'tag' => 'button', - 'label' => Craft::t('app', 'Edit Image'), - 'options' => [ - 'id' => 'edit-btn', - 'class' => ['btn', 'edit-btn'], - ], - 'data' => [ - 'icon' => 'edit', - ], - ]; + if ($editable) { + $components[] = [ + 'tag' => 'button', + 'label' => Craft::t('app', 'Edit Image'), + 'options' => [ + 'id' => 'edit-btn', + 'class' => ['btn', 'edit-btn'], + ], + 'data' => [ + 'icon' => 'edit', + ], + ]; - $editBtnId = $view->namespaceInputId('edit-btn'); - $updatePreviewThumbJs = $this->_updatePreviewThumbJs(); - $js = << { - new Craft.AssetImageEditor($this->id, { - allowDegreeFractions: Craft.isImagick, - onSave: data => { - if (data.newAssetId) { - // If this is within an Assets field’s editor slideout, replace the selected asset - const slideout = $('#$editBtnId').closest('[data-slideout]').data('slideout'); - if (slideout && slideout.settings.elementSelectInput) { - slideout.settings.elementSelectInput.replaceElement(slideout.\$element.data('id'), data.newAssetId) - .catch(() => {}); + $editBtnId = $view->namespaceInputId('edit-btn'); + $updatePreviewThumbJs = $this->_updatePreviewThumbJs(); + $js = << { + new Craft.AssetImageEditor($this->id, { + allowDegreeFractions: Craft.isImagick, + onSave: data => { + if (data.newAssetId) { + // If this is within an Assets field’s editor slideout, replace the selected asset + const slideout = $('#$editBtnId').closest('[data-slideout]').data('slideout'); + if (slideout && slideout.settings.elementSelectInput) { + slideout.settings.elementSelectInput.replaceElement(slideout.\$element.data('id'), data.newAssetId) + .catch(() => {}); + } + return; } - return; - } - - $updatePreviewThumbJs - }, + + $updatePreviewThumbJs + }, + }); }); -}); -JS; - $view->registerJs($js); + JS; + $view->registerJs($js); + } } return array_merge($components, parent::getAdditionalMenuComponents()); diff --git a/src/templates/_layouts/cp.twig b/src/templates/_layouts/cp.twig index 88442dd7980..0a7b144e53d 100644 --- a/src/templates/_layouts/cp.twig +++ b/src/templates/_layouts/cp.twig @@ -222,7 +222,7 @@
    {{ additionalButtons|raw }} {{ actionButton|raw }} - {% include '_layouts/components/additional-menu' with additionalMenuComponents %} + {% include '_layouts/components/additional-menu.twig' with additionalMenuComponents %}
    {% endif %} {% endblock %} From 28e97775c4f39a6b4d5a2396faad8e1c215ab259 Mon Sep 17 00:00:00 2001 From: Iwona Just Date: Wed, 16 Aug 2023 09:37:48 +0100 Subject: [PATCH 07/17] include additional menu btn in a slideout --- src/elements/Asset.php | 6 ++++-- src/templates/_layouts/components/additional-menu.twig | 10 +++++++++- src/web/CpScreenResponseFormatter.php | 6 ++++++ src/web/assets/cp/src/js/CpScreenSlideout.js | 4 ++++ src/web/assets/cp/src/js/ElementEditor.js | 4 +++- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/elements/Asset.php b/src/elements/Asset.php index f134b09b16a..01dacd221ab 100644 --- a/src/elements/Asset.php +++ b/src/elements/Asset.php @@ -1467,8 +1467,9 @@ public function getAdditionalMenuComponents(): array ], ]; + $downloadBtnId = $view->namespaceInputId('download-btn'); $js = << { +$('#$downloadBtnId').on('click', () => { const \$form = Craft.createForm().appendTo(Garnish.\$bod); \$form.append(Craft.getCsrfInput()); $('', {type: 'hidden', name: 'action', value: 'assets/download-asset'}).appendTo(\$form); @@ -1498,8 +1499,9 @@ public function getAdditionalMenuComponents(): array $dimensionsLabel = Html::encode(Craft::t('app', 'Dimensions')); $updatePreviewThumbJs = $this->_updatePreviewThumbJs(); + $replaceBtnId = $view->namespaceInputId('replace-btn'); $js = << { +$('#$replaceBtnId').on('click', () => { const \$fileInput = $('', {type: 'file', name: 'replaceFile', class: 'replaceFile hidden'}).appendTo(Garnish.\$bod); const uploader = new Craft.Uploader(\$fileInput, { url: Craft.getActionUrl('assets/replace-file'), diff --git a/src/templates/_layouts/components/additional-menu.twig b/src/templates/_layouts/components/additional-menu.twig index 59a9f32032b..7ed704a63ba 100644 --- a/src/templates/_layouts/components/additional-menu.twig +++ b/src/templates/_layouts/components/additional-menu.twig @@ -1,7 +1,15 @@ {% set menuId = 'menu' ~ random(100000,999999) %} +{% if fullPage is not defined %} + {% set fullPage = true %} +{% endif %} {% set safeComponents = additionalMenuComponents|filter(a => not (a.data.destructive ?? false)) %} -{% set destructiveComponents = additionalMenuComponents|filter(a => a.data.destructive ?? false) %} +{# we're not showing destructive actions in the slideout #} +{% if fullPage %} + {% set destructiveComponents = additionalMenuComponents|filter(a => a.data.destructive ?? false) %} +{% else %} + {% set destructiveComponents = [] %} +{% endif %} {% if safeComponents is not empty or destructiveComponents is not empty %} + data-icon="ellipsis" data-disclosure-trigger="true" role="combobox" type="button"> -