├── Docs ├── icon.png ├── badge.png ├── button.png ├── lists.png ├── modal.png ├── headings.png ├── modalform.png ├── table_01.png ├── table_02.png ├── table_03.png ├── pagination.png └── flashmessages.png ├── Resources ├── Private │ └── Fusion │ │ ├── Root.fusion │ │ └── Components │ │ ├── Icon.fusion │ │ ├── FlashMessages.fusion │ │ ├── Translate.fusion │ │ ├── ModuleLink.fusion │ │ ├── Badge.fusion │ │ ├── Pagination.fusion │ │ ├── Module.fusion │ │ ├── Modal.fusion │ │ ├── RelativeTime.fusion │ │ ├── ModalForm.fusion │ │ ├── ToggleButton.fusion │ │ ├── Button.fusion │ │ └── Table.fusion └── Public │ ├── Styles │ └── module.css │ └── Scripts │ └── module.js ├── Configuration └── Settings.Fusion.yaml ├── composer.json ├── Classes └── Eel │ ├── ModuleLinkHelper.php │ ├── I18nHelper.php │ ├── FlashMessageHelper.php │ └── PaginationHelper.php ├── README.md └── LICENSE /Docs/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/icon.png -------------------------------------------------------------------------------- /Docs/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/badge.png -------------------------------------------------------------------------------- /Docs/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/button.png -------------------------------------------------------------------------------- /Docs/lists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/lists.png -------------------------------------------------------------------------------- /Docs/modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/modal.png -------------------------------------------------------------------------------- /Docs/headings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/headings.png -------------------------------------------------------------------------------- /Docs/modalform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/modalform.png -------------------------------------------------------------------------------- /Docs/table_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/table_01.png -------------------------------------------------------------------------------- /Docs/table_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/table_02.png -------------------------------------------------------------------------------- /Docs/table_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/table_03.png -------------------------------------------------------------------------------- /Docs/pagination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/pagination.png -------------------------------------------------------------------------------- /Docs/flashmessages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bwaidelich/Wwwision.Neos.ModuleComponents/HEAD/Docs/flashmessages.png -------------------------------------------------------------------------------- /Resources/Private/Fusion/Root.fusion: -------------------------------------------------------------------------------- 1 | include: resource://Neos.Fusion/Private/Fusion/Root.fusion 2 | include: resource://Neos.Fusion.Form/Private/Fusion/Root.fusion 3 | include: **/* 4 | -------------------------------------------------------------------------------- /Configuration/Settings.Fusion.yaml: -------------------------------------------------------------------------------- 1 | Neos: 2 | Fusion: 3 | defaultContext: 4 | NeosBE.FlashMessage: Wwwision\Neos\ModuleComponents\Eel\FlashMessageHelper 5 | NeosBE.I18n: Wwwision\Neos\ModuleComponents\Eel\I18nHelper 6 | NeosBE.ModuleLink: Wwwision\Neos\ModuleComponents\Eel\ModuleLinkHelper 7 | NeosBE.Pagination: Wwwision\Neos\ModuleComponents\Eel\PaginationHelper 8 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Components/Icon.fusion: -------------------------------------------------------------------------------- 1 | /* 2 | Renders a single icon (from fontawesome) 3 | 4 | See https://fontawesome.com/v5/search?m=free 5 | 6 | # Usage 7 | 8 | ``` 9 | 10 | 11 | 12 | ``` 13 | */ 14 | prototype(NeosBE:Icon) < prototype(Neos.Fusion:Component) { 15 | 16 | # name of the icon to render (see https://fontawesome.com/v5/search?m=free) 17 | icon = '' 18 | 19 | renderer = afx`` 20 | } 21 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Components/FlashMessages.fusion: -------------------------------------------------------------------------------- 1 | /* 2 | Renders and removes flash messages as unordered list 3 | 4 | *Note:* This component is used by `NeosBE:Module` and usually does not have to be added manually 5 | */ 6 | prototype(NeosBE:FlashMessages) < prototype(Neos.Fusion:Tag) { 7 | tagName = 'ul' 8 | content = Neos.Fusion:Loop { 9 | items = ${NeosBE.FlashMessage.getMessagesAndFlush(request)} 10 | itemName = 'flashMessage' 11 | itemRenderer = afx`
  • {flashMessage}
  • ` 12 | } 13 | @if.hasMessages = ${NeosBE.FlashMessage.hasMessages(request)} 14 | } 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Flow package with utilities and components to create Fusion based backend modules with the common Neos look and feel", 3 | "type": "neos-package", 4 | "name": "wwwision/neos-modulecomponents", 5 | "license": "GPL-3.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "bwaidelich", 9 | "email": "b.waidelich@wwwision.de" 10 | } 11 | ], 12 | "funding": [ 13 | { 14 | "type": "github", 15 | "url": "https://github.com/sponsors/bwaidelich" 16 | }, 17 | { 18 | "type": "paypal", 19 | "url": "https://www.paypal.me/bwaidelich" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=8.1", 24 | "neos/neos": "^7.3 || ^8.0 || ^9.0", 25 | "neos/fusion-form": "^2" 26 | }, 27 | "require-dev": { 28 | "roave/security-advisories": "dev-latest" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Wwwision\\Neos\\ModuleComponents\\": "Classes" 33 | } 34 | }, 35 | "extra": { 36 | "neos": { 37 | "package-key": "Wwwision.Neos.ModuleComponents" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Classes/Eel/ModuleLinkHelper.php: -------------------------------------------------------------------------------- 1 | setRequest($request->getMainRequest()); 30 | 31 | $arguments = [ 32 | 'module' => $module, 33 | 'moduleArguments' => array_merge($arguments, ['@action' => $action]), 34 | ]; 35 | return $uriBuilder->uriFor('index', $arguments); 36 | } 37 | 38 | public function allowsCallOfMethod($methodName): bool 39 | { 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Components/Translate.fusion: -------------------------------------------------------------------------------- 1 | /* 2 | A component to translate strings using XLIFF 3 | 4 | **Note:** By default, corresponding translation files are expected in `/Resources/Private/Translations//.xlf` 5 | 6 | # Usage 7 | 8 | ``` 9 | 10 | 11 | 12 | 13 | ``` 14 | */ 15 | prototype(NeosBE:Translate) < prototype(Neos.Fusion:Component) { 16 | # Id of the XLIF `trans-unit` 17 | id = '' 18 | 19 | # Optional arguments to be replaced (using "{0}", "{1}", ... in the translated strings) 20 | arguments = Neos.Fusion:DataStructure 21 | 22 | # Original label to fallback to 23 | originalLabel = null 24 | 25 | # Translation files will be found in `/Resources/Private/Translations//.xlf` by default 26 | source = 'Modules' 27 | 28 | # Optional quantity in order to use plural forms (see https://docs.oasis-open.org/xliff/v1.2/xliff-profile-po/xliff-profile-po-1.2-cd02.html 5.2.2) 29 | quantity = null 30 | 31 | renderer = ${NeosBE.I18n.translateById(request, props.id, props.arguments, props.quantity, props.source)} 32 | } 33 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Components/ModuleLink.fusion: -------------------------------------------------------------------------------- 1 | /* 2 | A link pointing to a different Neos Backend Module 3 | 4 | # Usage 5 | 6 | ``` 7 | Add new user 8 | List documents 9 | ``` 10 | */ 11 | prototype(NeosBE:ModuleLink) < prototype(Neos.Fusion:Component) { 12 | 13 | # Name of the target MVC action 14 | action = 'index' 15 | 16 | # Path of the BE module, as configured via `Neos.Neos.modules` settings, e.g. "administration/users" 17 | module = '' 18 | 19 | # Arguments to be passed to the target MVC action 20 | arguments = Neos.Fusion:DataStructure 21 | 22 | # Whether to keep current query arguments in the resulting URL 23 | addQueryString = false 24 | 25 | # Additional HTML attributes for the resulting `` tag 26 | attributes = Neos.Fusion:DataStructure 27 | 28 | # Contents of the link 29 | content = '' 30 | 31 | renderer = Neos.Fusion:Tag { 32 | tagName = 'a' 33 | attributes { 34 | href = ${NeosBE.ModuleLink.create(request, props.module, props.action || 'index', props.arguments, props.addQueryString)} 35 | @apply.attributes = ${props.attributes} 36 | } 37 | content = ${props.content} 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Resources/Public/Styles/module.css: -------------------------------------------------------------------------------- 1 | .neos .neos-module-content h1 { 2 | font-size: 150%; 3 | padding: 0.5em 0; 4 | } 5 | 6 | .neos .neos-module-content h2 { 7 | font-size: 130%; 8 | padding: 1.5em 0 1em 0; 9 | } 10 | 11 | .neos .neos-module-content h3 { 12 | font-size: 120%; 13 | } 14 | 15 | .neos .neos-module-content h4 { 16 | font-size: 110%; 17 | } 18 | 19 | .neos .neos-module-content .neos-label, 20 | .neos .neos-module-content .neos-label-important, 21 | .neos .neos-module-content .neos-label-warning, 22 | .neos .neos-module-content .neos-label-success { 23 | padding: 0 0.2em; 24 | border-radius: 0.2em; 25 | } 26 | 27 | .neos .neos-module-content ol, .neos .neos-module-content ul { 28 | margin: .5em 0 1em 0; 29 | } 30 | 31 | .neos .neos-module-content ol li, .neos .neos-module-content ul li { 32 | list-style: disc; 33 | margin-left: 2em; 34 | line-height: 1.7em; 35 | } 36 | 37 | .neos .neos-module-content ol li { 38 | list-style: decimal; 39 | } 40 | 41 | .neos .neos-module-content tr:hover td { 42 | background-color: #3f3f3f; 43 | } 44 | 45 | .neos .neos-module-content .neos-modal-footer { 46 | clear: both; 47 | } 48 | 49 | .neos .neos-module-content tr.neos-folder td div.neos-pull-right .neos-button, .neos .neos-module-content tr.neos-folder td div.neos-pull-right .neos-button:hover { 50 | background: none; 51 | padding: 0; 52 | line-height: 1em; 53 | } 54 | 55 | .neos .neos-module-content .page-navigation ul, .neos .neos-module-content .page-navigation ul li { 56 | margin: 0; 57 | } 58 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Components/Badge.fusion: -------------------------------------------------------------------------------- 1 | /* 2 | A simple badge aka label, potentially with custom error level 3 | 4 | # Usage 5 | 6 | ``` 7 | Some default badge 8 | Success 9 | Warning 10 | 11 | ``` 12 | 13 | *Note:* It can be useful to wrap this component to reflect the state of some domain concept. 14 | 15 | ## Example 16 | 17 | prototype(Some.Package:SomeCustomStateBadge) < prototype(Neos.Fusion:Component) { 18 | state = null 19 | renderer = NeosBE:Badge { 20 | errorLevel = Neos.Fusion:Match { 21 | @subject = ${props.state} 22 | @default = 0 23 | 'active' = 1 24 | 'disabled' = 2 25 | 'error' = 3 26 | } 27 | content = ${props.state} 28 | } 29 | } 30 | */ 31 | prototype(NeosBE:Badge) < prototype(Neos.Fusion:Component) { 32 | 33 | # Optional error level for this badge. 0: no level (grey), 1: success, 2: warning, 3: error 34 | errorLevel = 0 35 | 36 | # Content of the badge 37 | content = '' 38 | 39 | renderer.@context { 40 | class = Neos.Fusion:Match { 41 | @subject = ${props.errorLevel} 42 | @default = 'neos-label' 43 | 1 = 'neos-label-success' 44 | 2 = 'neos-label-warning' 45 | 3 = 'neos-label-important' 46 | } 47 | } 48 | renderer = afx` 49 | {props.content} 50 | ` 51 | } 52 | -------------------------------------------------------------------------------- /Classes/Eel/I18nHelper.php: -------------------------------------------------------------------------------- 1 | translator->translateById($id, $arguments, $quantity, null, $source, $actionRequest->getControllerPackageKey()); 26 | } 27 | 28 | public function translateContent(ActionRequest $actionRequest, string $content, string $source = 'Modules'): string 29 | { 30 | return preg_replace_callback('/LLL:(?{[^}]+}|(?[a-z][a-zA-Z0-9\.\-\_]+))/', fn (array $match) => $this->translateById($actionRequest, $match['id'], isset($match['quantity']) ? [$match['quantity']] : [], isset($match['quantity']) ? (int)$match['quantity'] : null, $source) ?? '[' . $match['id'] . ']', $content); 31 | } 32 | 33 | public function locale(): string 34 | { 35 | return (string)$this->i18nService->getConfiguration()->getCurrentLocale(); 36 | } 37 | 38 | public function allowsCallOfMethod($methodName): bool 39 | { 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Components/Pagination.fusion: -------------------------------------------------------------------------------- 1 | /* 2 | Page navigation usually combined with a `NeosBE:Table` component 3 | 4 | # Usage 5 | 6 | ``` 7 | 8 | ``` 9 | */ 10 | prototype(NeosBE:Pagination) < prototype(Neos.Fusion:Component) { 11 | # Total number of results (required to determine the number of page links to display) 12 | numberOfResults = 0 13 | 14 | # Maximum number of elements on one page 15 | resultsPerPage = 10 16 | 17 | # Name of the URI query parameter that will be passed to the pagination links 18 | queryParameter = 'page' 19 | 20 | renderer.@context.paginationItems = ${NeosBE.Pagination.create({numberOfResults: props.numberOfResults, resultsPerPage: props.resultsPerPage, queryParameter: props.queryParameter}, request.httpRequest)} 21 | renderer = afx` 22 | 29 | ` 30 | } 31 | 32 | prototype(NeosBE:Pagination.Link) < prototype(Neos.Fusion:Component) { 33 | label = '' 34 | href = '' 35 | type = '' 36 | 37 | renderer = Neos.Fusion:Match { 38 | @subject = ${props.type} 39 | @default = afx`
  • {props.label}
  • ` 40 | 41 | previous = afx`` 42 | current = afx`
  • {props.label}
  • ` 43 | ellipsis = afx`
  • ...
  • ` 44 | next = afx`` 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Components/Module.fusion: -------------------------------------------------------------------------------- 1 | /* 2 | The root component used to render the backend module content for one MVC action 3 | 4 | # Usage 5 | 6 | ``` 7 | Some.Package.SomeController.index = NeosBE:Module { 8 | content = 'main content' 9 | } 10 | ``` 11 | 12 | With the `translateContent` prop set to `true`, "LLL:" strings in flash messages, main and footer content will be replaced using the \Wwwision\Neos\ModuleComponents\Eel\I18nHelper::translateContent() Eel helper: 13 | 14 | ``` 15 | Some.Package.SomeController.someOtherAction = NeosBE:Module { 16 | translateContent = true 17 | content = afx` 18 | This will be LLL:translated 19 | ` 20 | footer = afx` 21 | LLL:alsoTranslated 22 | ` 23 | } 24 | ``` 25 | */ 26 | prototype(NeosBE:Module) < prototype(Neos.Fusion:Component) { 27 | 28 | # Main content of the module 29 | content = '' 30 | 31 | # Optional footer content of the module 32 | footer = '' 33 | 34 | # If true, all "LLL:" strings in flash messages, main and footer content will be replaced using the \Wwwision\Neos\ModuleComponents\Eel\I18nHelper::translateContent() Eel helper 35 | translateContent = false 36 | 37 | renderer = Neos.Fusion:Join { 38 | flashMessages = NeosBE:FlashMessages { 39 | attributes.id = "neos-notifications-inline" 40 | content.itemRenderer = afx`
  • {flashMessage}
  • ` 41 | } 42 | mainContent = Neos.Fusion:Tag { 43 | tagName = 'div' 44 | attributes.class = "neos-module-content neos-row-fluid" 45 | content = ${props.content} 46 | } 47 | footerContent = Neos.Fusion:Tag { 48 | tagName = 'div' 49 | attributes.class = "neos-footer" 50 | content = ${props.footer} 51 | } 52 | @process.translate = ${NeosBE.I18n.translateContent(request, value)} 53 | @process.translate.@if.enabled = ${props.translateContent} 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Components/Modal.fusion: -------------------------------------------------------------------------------- 1 | /* 2 | Modal dialog that is hidden by default and can be shown via the `Button` component 3 | 4 | ## Usage 5 | 6 | ``` 7 | 8 | 9 | Some modal content 10 | 11 | 12 | 13 | // ... 14 | Show modal 15 | ``` 16 | */ 17 | prototype(NeosBE:Modal) < prototype(Neos.Fusion:Component) { 18 | 19 | # ID attribute of the modal element – this is required in order to target the modal from Button components: `` 20 | id = '' 21 | 22 | # additional HTML attributes of the modal 23 | attributes = Neos.Fusion:DataStructure 24 | 25 | # Main modal content 26 | content = '' 27 | 28 | # Modals are hidden by default – with this flag they can be made visible programatically (e.g. for deep links that should show the modal by default) 29 | hidden = true 30 | 31 | renderer = afx` 32 |
    33 |
    34 |
    35 | {props.content} 36 |
    37 |
    38 |
    39 |
    40 | ` 41 | } 42 | prototype(NeosBE:Modal.Header) < prototype(Neos.Fusion:Component) { 43 | header = '' 44 | content = '' 45 | renderer = afx` 46 |
    47 | 48 |
    {props.header}
    49 |
    50 | {props.content} 51 |
    52 |
    53 | ` 54 | } 55 | prototype(NeosBE:Modal.Footer) < prototype(Neos.Fusion:Component) { 56 | cancelButtonText = '' 57 | content = '' 58 | renderer = afx` 59 | 63 | ` 64 | } 65 | -------------------------------------------------------------------------------- /Resources/Public/Scripts/module.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("DOMContentLoaded", () => { 2 | 3 | function toggleClasses(element) { 4 | const targetElement = document.getElementById(element.dataset.toggle); 5 | if (!targetElement) { 6 | console.error("Target element " + element.dataset.target + " not found"); 7 | } 8 | const hidden = targetElement.classList.toggle("neos-hide"); 9 | const iconElement = element.querySelector('i.fas'); 10 | if (iconElement && element.dataset.icontoggled) { 11 | if (hidden) { 12 | iconElement.classList.replace(element.dataset.icontoggled, element.dataset.icon); 13 | } else { 14 | iconElement.classList.replace(element.dataset.icon, element.dataset.icontoggled); 15 | } 16 | } 17 | } 18 | 19 | /** 20 | * @param date {Date} 21 | * @param locale {string | undefined} 22 | * @param options {object | undefined} 23 | * @returns {string} 24 | */ 25 | function getRelativeTimeString(date, locale, options) { 26 | const deltaSeconds = Math.round((date.getTime() - Date.now()) / 1000); 27 | 28 | // Array reprsenting one minute, hour, day, week, month, etc in seconds 29 | const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity]; 30 | 31 | // Array equivalent to the above but in the string representation of the units 32 | const units = ["second", "minute", "hour", "day", "week", "month", "year"]; 33 | 34 | // Grab the ideal cutoff unit 35 | const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(deltaSeconds)); 36 | 37 | // Get the divisor to divide from the seconds. E.g. if our unit is "day" our divisor 38 | // is one day in seconds, so we can divide our seconds by this to get the # of days 39 | const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1; 40 | 41 | // Intl.RelativeTimeFormat do its magic 42 | const rtf = new Intl.RelativeTimeFormat(locale, options); 43 | return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]); 44 | } 45 | 46 | document.addEventListener("click", function (event) { 47 | let toggleElement; 48 | if (toggleElement = event.target.closest('a[data-toggle]')) { 49 | event.preventDefault(); 50 | toggleClasses(toggleElement); 51 | } 52 | }) 53 | 54 | document.querySelectorAll('time[datetime][data-relativetime]').forEach((element) => { 55 | const locale = element.dataset.locale || undefined; 56 | const options = element.dataset.options ? JSON.parse(element.dataset.options) : undefined; 57 | element.innerText = getRelativeTimeString(new Date(element.getAttribute('datetime')), locale, options); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Components/RelativeTime.fusion: -------------------------------------------------------------------------------- 1 | /* 2 | A DateTime value that is rendered as relative time using `Intl.RelativeTimeFormat()` on the client side 3 | See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat 4 | 5 | # Usage 6 | 7 | ``` 8 | 9 | 10 | 11 | ``` 12 | */ 13 | prototype(NeosBE:RelativeTime) < prototype(Neos.Fusion:Component) { 14 | 15 | # A PHP DateTime instance 16 | dateTime = null 17 | 18 | # PHP format string for the title attribute of the resulting `