From d005d0ee998b0637bda77e6e1fe6d939aa54b45f Mon Sep 17 00:00:00 2001 From: Dariusz Szut Date: Fri, 6 Dec 2024 08:47:25 +0100 Subject: [PATCH] IBX-8436: Added modal helper (#1320) --- .../Resources/encore/ibexa.js.config.js | 2 + .../js/scripts/core/suggestion.taggify.js | 130 ++++++++++++++++++ .../public/js/scripts/core/taggify.js | 67 +++++++++ .../public/js/scripts/edit.header.js | 6 +- .../js/scripts/helpers/config.loader.js | 2 + .../public/js/scripts/helpers/modal.helper.js | 12 ++ .../Resources/public/scss/_taggify.scss | 61 ++++++++ src/bundle/Resources/public/scss/ibexa.scss | 1 + .../suggestion_taggify.html.twig | 12 ++ .../suggestion_taggify_item.html.twig | 1 + .../ui/component/taggify/taggify.html.twig | 13 ++ .../component/taggify/taggify_tag.html.twig | 8 ++ 12 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 src/bundle/Resources/public/js/scripts/core/suggestion.taggify.js create mode 100644 src/bundle/Resources/public/js/scripts/core/taggify.js create mode 100644 src/bundle/Resources/public/js/scripts/helpers/modal.helper.js create mode 100644 src/bundle/Resources/public/scss/_taggify.scss create mode 100644 src/bundle/Resources/views/themes/admin/ui/component/suggestion_taggify/suggestion_taggify.html.twig create mode 100644 src/bundle/Resources/views/themes/admin/ui/component/suggestion_taggify/suggestion_taggify_item.html.twig create mode 100644 src/bundle/Resources/views/themes/admin/ui/component/taggify/taggify.html.twig create mode 100644 src/bundle/Resources/views/themes/admin/ui/component/taggify/taggify_tag.html.twig diff --git a/src/bundle/Resources/encore/ibexa.js.config.js b/src/bundle/Resources/encore/ibexa.js.config.js index 0e715c8ce7..c63cfcd0d0 100644 --- a/src/bundle/Resources/encore/ibexa.js.config.js +++ b/src/bundle/Resources/encore/ibexa.js.config.js @@ -22,6 +22,8 @@ const layout = [ path.resolve(__dirname, '../public/js/scripts/core/toggle.button.js'), path.resolve(__dirname, '../public/js/scripts/core/slug.value.input.autogenerator.js'), path.resolve(__dirname, '../public/js/scripts/core/date.time.picker.js'), + path.resolve(__dirname, '../public/js/scripts/core/taggify.js'), + path.resolve(__dirname, '../public/js/scripts/core/suggestion.taggify.js'), path.resolve(__dirname, '../public/js/scripts/adaptive.filters.js'), path.resolve(__dirname, '../public/js/scripts/admin.notifications.js'), path.resolve(__dirname, '../public/js/scripts/button.trigger.js'), diff --git a/src/bundle/Resources/public/js/scripts/core/suggestion.taggify.js b/src/bundle/Resources/public/js/scripts/core/suggestion.taggify.js new file mode 100644 index 0000000000..c9c8703f0e --- /dev/null +++ b/src/bundle/Resources/public/js/scripts/core/suggestion.taggify.js @@ -0,0 +1,130 @@ +import { getRestInfo } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; + +(function (global, doc, ibexa) { + const MIN_QUERY_LENGTH = 3; + + class SuggestionTaggify extends ibexa.core.Taggify { + constructor(config) { + super(config); + + const { siteaccess, token } = getRestInfo(); + + this.suggestionsListNode = config.suggestionsListNode ?? this.container.querySelector('.ibexa-taggify__suggestions'); + this.token = config.token ?? token; + this.siteaccess = config.siteaccess ?? siteaccess; + + this.renderSuggestionsList = this.renderSuggestionsList.bind(this); + this.getItemsFromResponse = this.getItemsFromResponse.bind(this); + } + + hideSuggestionsList() { + this.suggestionsListNode.classList.add('ibexa-taggify__suggestions--hidden'); + } + + showSuggestionsList() { + this.suggestionsListNode.classList.remove('ibexa-taggify__suggestions--hidden'); + } + + createSuggestionsRequestBody(query) { + return JSON.stringify({ + ViewInput: { + identifier: `find-suggestions-${query}`, + public: false, + ContentQuery: { + FacetBuilders: {}, + SortClauses: {}, + Query: { + FullTextCriterion: `${query}*`, + ContentTypeIdentifierCriterion: ibexa.adminUiConfig.userContentTypes, + }, + limit: 10, + offset: 0, + }, + }, + }); + } + + createSuggestionsRequest(body) { + return new Request('/api/ibexa/v2/views', { + method: 'POST', + headers: { + Accept: 'application/vnd.ibexa.api.View+json; version=1.1', + 'Content-Type': 'application/vnd.ibexa.api.ViewInput+json; version=1.1', + 'X-Siteaccess': this.siteaccess, + 'X-CSRF-Token': this.token, + }, + body, + mode: 'same-origin', + credentials: 'same-origin', + }); + } + + getSuggestions(query) { + const body = this.createSuggestionsRequestBody(query); + const request = this.createSuggestionsRequest(body); + + fetch(request) + .then(ibexa.helpers.request.getJsonFromResponse) + .then(this.getItemsFromResponse) + .then(this.renderSuggestionsList) + .catch(ibexa.helpers.notification.showErrorNotification); + } + + getItemsFromResponse(response) { + return response.View.Result.searchHits.searchHit.map((hit) => hit.value.Content); + } + + renderSuggestionsList(items) { + const fragment = doc.createDocumentFragment(); + + items.forEach((item) => { + const listItemNode = this.renderSuggestionItem(item); + + listItemNode.addEventListener( + 'click', + ({ currentTarget }) => { + this.addTag(currentTarget.innerHTML, item); + this.hideSuggestionsList(); + + this.inputNode.value = ''; + }, + false, + ); + + fragment.append(listItemNode); + }); + + this.suggestionsListNode.innerHTML = ''; + this.suggestionsListNode.append(fragment); + + this.showSuggestionsList(); + } + + renderSuggestionItem(item) { + const itemTemplate = this.suggestionsListNode.dataset.template; + const renderedTemplate = itemTemplate.replace('{{ name }}', item.TranslatedName); + const container = doc.createElement('div'); + + container.innerHTML = ''; + container.insertAdjacentHTML('beforeend', renderedTemplate); + + return container.querySelector('div'); + } + + handleInputKeyUp(event) { + super.handleInputKeyUp(event); + + if (this.isAcceptKeyPressed(event.key)) { + this.hideSuggestionsList(); + + return; + } + + if (this.inputNode.value.length > MIN_QUERY_LENGTH) { + this.getSuggestions(this.inputNode.value); + } + } + } + + ibexa.addConfig('core.SuggestionTaggify', SuggestionTaggify); +})(window, window.document, window.ibexa); diff --git a/src/bundle/Resources/public/js/scripts/core/taggify.js b/src/bundle/Resources/public/js/scripts/core/taggify.js new file mode 100644 index 0000000000..e9e90232c2 --- /dev/null +++ b/src/bundle/Resources/public/js/scripts/core/taggify.js @@ -0,0 +1,67 @@ +(function (global, doc, ibexa) { + class Taggify { + constructor(config) { + this.container = config.container; + this.acceptKeys = config.acceptKeys ?? ['Enter']; + this.inputNode = config.inputNode ?? this.container.querySelector('.ibexa-taggify__input'); + this.listNode = config.listNode ?? this.container.querySelector('.ibexa-taggify__list'); + this.tagsPattern = config.tagsPattern ?? null; + this.tags = config.tags ?? new Set(); + + this.attachEventsToTag = this.attachEventsToTag.bind(this); + this.handleInputKeyUp = this.handleInputKeyUp.bind(this); + } + + afterTagsUpdate() {} + + isAcceptKeyPressed(key) { + return this.acceptKeys.includes(key); + } + + addTag(name, value) { + const tagTemplate = this.listNode.dataset.template; + const renderedTemplate = tagTemplate.replace('{{ name }}', name).replace('{{ value }}', value); + const div = doc.createElement('div'); + + div.insertAdjacentHTML('beforeend', renderedTemplate); + + const tag = div.querySelector('.ibexa-taggify__list-tag'); + + this.attachEventsToTag(tag, value); + this.listNode.insertBefore(tag, this.inputNode); + this.tags.add(value); + this.afterTagsUpdate(); + } + + removeTag(tag, value) { + this.tags.delete(value); + tag.remove(); + + this.afterTagsUpdate(); + } + + attachEventsToTag(tag, value) { + const removeBtn = tag.querySelector('.ibexa-taggify__btn--remove'); + + removeBtn.addEventListener('click', () => this.removeTag(tag, value), false); + } + + handleInputKeyUp(event) { + if (this.tagsPattern && !this.tagsPattern.test(this.inputNode.value)) { + return; + } + + if (this.isAcceptKeyPressed(event.key)) { + this.addTag(this.inputNode.value, this.inputNode.value); + + this.inputNode.value = ''; + } + } + + init() { + this.inputNode.addEventListener('keyup', this.handleInputKeyUp, false); + } + } + + ibexa.addConfig('core.Taggify', Taggify); +})(window, window.document, window.ibexa); diff --git a/src/bundle/Resources/public/js/scripts/edit.header.js b/src/bundle/Resources/public/js/scripts/edit.header.js index 4d69dc167b..ece171cfa5 100644 --- a/src/bundle/Resources/public/js/scripts/edit.header.js +++ b/src/bundle/Resources/public/js/scripts/edit.header.js @@ -1,4 +1,4 @@ -(function (global, doc) { +(function (global, doc, ibexa) { const SCROLL_POSITION_TO_FIT = 50; const HEADER_RIGHT_MARGIN = 50; const MIN_HEIGHT_DIFF_FOR_FITTING_HEADER = 150; @@ -11,6 +11,7 @@ const { height: expandedHeaderHeight } = headerNode.getBoundingClientRect(); const scrolledContent = doc.querySelector('.ibexa-edit-content > :first-child'); + const { controlZIndex } = ibexa.helpers.modal; const fitEllipsizedTitle = () => { const headerBottomRowNode = headerNode.querySelector('.ibexa-edit-header__row--bottom'); const titleNode = headerBottomRowNode.querySelector('.ibexa-edit-header__name--ellipsized'); @@ -46,4 +47,5 @@ }; contentNode.addEventListener('scroll', fitHeader, false); -})(window, window.document); + controlZIndex(headerNode); +})(window, window.document, window.ibexa); diff --git a/src/bundle/Resources/public/js/scripts/helpers/config.loader.js b/src/bundle/Resources/public/js/scripts/helpers/config.loader.js index 7fbfb1a3f5..2c2f3ff9b2 100644 --- a/src/bundle/Resources/public/js/scripts/helpers/config.loader.js +++ b/src/bundle/Resources/public/js/scripts/helpers/config.loader.js @@ -8,6 +8,7 @@ import * as icon from './icon.helper'; import * as content from './content.helper'; import * as location from './location.helper'; import * as middleEllipsis from './middle.ellipsis'; +import * as modal from './modal.helper'; import * as notification from './notification.helper'; import * as objectInstances from './object.instances'; import * as pagination from './pagination.helper'; @@ -31,6 +32,7 @@ import * as user from './user.helper'; ibexa.addConfig('helpers.content', content); ibexa.addConfig('helpers.location', location); ibexa.addConfig('helpers.ellipsis.middle', middleEllipsis); + ibexa.addConfig('helpers.modal', modal); ibexa.addConfig('helpers.notification', notification); ibexa.addConfig('helpers.objectInstances', objectInstances); ibexa.addConfig('helpers.pagination', pagination); diff --git a/src/bundle/Resources/public/js/scripts/helpers/modal.helper.js b/src/bundle/Resources/public/js/scripts/helpers/modal.helper.js new file mode 100644 index 0000000000..0976bc1891 --- /dev/null +++ b/src/bundle/Resources/public/js/scripts/helpers/modal.helper.js @@ -0,0 +1,12 @@ +const controlZIndex = (container) => { + const initialZIndex = container.style.zIndex; + + container.addEventListener('show.bs.modal', () => { + container.style.zIndex = 'initial'; + }); + container.addEventListener('hide.bs.modal', () => { + container.style.zIndex = initialZIndex; + }); +}; + +export { controlZIndex }; diff --git a/src/bundle/Resources/public/scss/_taggify.scss b/src/bundle/Resources/public/scss/_taggify.scss new file mode 100644 index 0000000000..683939b1d0 --- /dev/null +++ b/src/bundle/Resources/public/scss/_taggify.scss @@ -0,0 +1,61 @@ +.ibexa-taggify { + position: relative; + + &__list { + display: flex; + align-items: center; + flex-wrap: wrap; + min-height: calculateRem(48px); + border: calculateRem(1px) solid $ibexa-color-light; + } + + &__list-tag { + padding-left: calculateRem(8px); + border: calculateRem(1px) solid $ibexa-color-light; + border-radius: $ibexa-border-radius; + margin-right: calculateRem(8px); + display: flex; + align-items: center; + + .ibexa-btn { + height: auto; + margin-left: calculateRem(8px); + } + + & + .ibexa-taggify__input { + &::placeholder { + opacity: 0; + } + } + } + + &__input { + border: none; + flex-grow: 1; + + &:focus-visible { + outline: none; + } + } + + &__suggestions { + position: absolute; + width: 100%; + bottom: 0; + transform: translate(0, calc(100% + calculateRem(4px))); + border: calculateRem(1px) solid $ibexa-color-light; + border-radius: $ibexa-border-radius; + background-color: $ibexa-color-white; + padding: calculateRem(4px); + box-shadow: $ibexa-edit-content-box-shadow; + + &--hidden { + display: none; + } + } + + &__suggestion-item { + padding: calculateRem(8px) calculateRem(12px); + cursor: pointer; + } +} diff --git a/src/bundle/Resources/public/scss/ibexa.scss b/src/bundle/Resources/public/scss/ibexa.scss index e318699fe5..7a5e84f112 100644 --- a/src/bundle/Resources/public/scss/ibexa.scss +++ b/src/bundle/Resources/public/scss/ibexa.scss @@ -130,3 +130,4 @@ @import 'user-profile'; @import 'additional-actions'; @import 'user-mode-badge'; +@import 'taggify'; diff --git a/src/bundle/Resources/views/themes/admin/ui/component/suggestion_taggify/suggestion_taggify.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/suggestion_taggify/suggestion_taggify.html.twig new file mode 100644 index 0000000000..1bf10ab5a7 --- /dev/null +++ b/src/bundle/Resources/views/themes/admin/ui/component/suggestion_taggify/suggestion_taggify.html.twig @@ -0,0 +1,12 @@ +{% extends '@ibexadesign/ui/component/taggify/taggify.html.twig' %} + +{% block main_class %}ibexa-taggify--suggestion{% endblock %} + +{% block additional_tools_wrapper %} +
+{% endblock %} diff --git a/src/bundle/Resources/views/themes/admin/ui/component/suggestion_taggify/suggestion_taggify_item.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/suggestion_taggify/suggestion_taggify_item.html.twig new file mode 100644 index 0000000000..42842bf167 --- /dev/null +++ b/src/bundle/Resources/views/themes/admin/ui/component/suggestion_taggify/suggestion_taggify_item.html.twig @@ -0,0 +1 @@ +
{{ name }}
diff --git a/src/bundle/Resources/views/themes/admin/ui/component/taggify/taggify.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/taggify/taggify.html.twig new file mode 100644 index 0000000000..64f8b81d77 --- /dev/null +++ b/src/bundle/Resources/views/themes/admin/ui/component/taggify/taggify.html.twig @@ -0,0 +1,13 @@ +
+ {% block list_wrapper %} +
+ +
+ {% endblock %} + {% block additional_tools_wrapper %}{% endblock %} +
diff --git a/src/bundle/Resources/views/themes/admin/ui/component/taggify/taggify_tag.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/taggify/taggify_tag.html.twig new file mode 100644 index 0000000000..2d4d6448d3 --- /dev/null +++ b/src/bundle/Resources/views/themes/admin/ui/component/taggify/taggify_tag.html.twig @@ -0,0 +1,8 @@ +
+ {{ name }} + +