/** Datei: plugins/rest/assets/scss/colors/_variables.scss **/ $theme-bg-blur : 5px !default; $theme-color-red : #ee1501 !default; $theme-color-orange : #ee6c01 !default; $theme-color-yellow : #eece01 !default; $theme-color-green : #01ee21 !default; $theme-color-cyan : #01c3ee !default; $theme-color-blue : #014cee !default; $theme-color-pink : #ee01e2 !default; $theme-color-purple : #8301ee !default; $theme-color-white : #ffffff !default; $theme-color-light : #bebebe !default; $theme-color-medium : #818181 !default; $theme-color-dark : #595959 !default; $theme-color-black : #000000 !default; $theme-colors : ( 'background': $theme-color-white, 'color' : $theme-color-black, 'content' : $theme-color-white, 'default' : $theme-color-medium, 'inverted' : $theme-color-light, 'primary' : $theme-color-cyan, 'secondary' : $theme-color-yellow, 'tertiary' : $theme-color-pink, 'success' : $theme-color-green, 'danger' : $theme-color-red, 'warning' : $theme-color-yellow, 'red' : $theme-color-red, 'orange' : $theme-color-orange, 'yellow' : $theme-color-yellow, 'green' : $theme-color-green, 'cyan' : $theme-color-cyan, 'blue' : $theme-color-blue, 'pink' : $theme-color-pink, 'purple' : $theme-color-purple, 'white' : $theme-color-white, 'light' : $theme-color-light, 'medium' : $theme-color-medium, 'dark' : $theme-color-dark, 'black' : $theme-color-black, ) !default; $theme-colorize-alpha : .3 !default; $theme-colorize-bg-alpha : .4 !default; $theme-colorize-border-alpha : .5 !default; $theme-colorize-active-alpha : .1 !default; $theme-colorize-hover-alpha : .2 !default; $theme-colorize-light : $theme-color-white !default; $theme-colorize-dark : $theme-color-black !default; $theme-brightness: ( 50 : 5%, 100 : 0%, 200 : -2.5%, 300 : -5%, 400 : -7.5%, 500 : -10%, ) !default; /** Datei: plugins/rest/assets/scss/colors/_functions.scss **/ @function get_color( $color, $alpha : 1, $brightness : 0 ) { @if type-of( $color ) != color { $color : map-get( $theme-colors, $color ); } @if( $brightness < 0 ) { $color : darken( $color, $brightness * -1); } @if( $brightness > 0 ) { $color : lighten( $color, $brightness ); } @if( $alpha != 1 ) { $color : rgba( red( $color ), green( $color ), blue( $color ), $alpha ); } @return $color } @function get_color_hover( $color, $alpha : 1 ) { @return get_color( $color, $alpha, 5% ); } @function get_color_active( $color, $alpha : 1 ) { @return get_color( $color, $alpha, -5% ); } @function get_color_tone( $color, $level : 100, $tint: null, $tint-strength: 15% ) { @if( $level > 0 ) { $base: get_color( $color, 1, get_brightness( $level ) ); } @else { $base: get_color( $color, 1, get_brightness( $level * -1 ) * -1 ); } @if $tint == null { @return $base; } $accent: get_color( $tint ); @return mix( $accent, $base, $tint-strength ); } @function get_brightness( $level : 100 ) { @return map-get( $theme-brightness, $level ); } @function contrast( $color, $color1, $color2, $contrast-threshold : 55% ) { @if type-of( $color ) == color and type-of( $color1 ) == color and type-of( $color2 ) == color { $light : if( lightness( $color1 ) > lightness( $color2 ), $color1, $color2 ); $dark : if( lightness( $color1 ) > lightness( $color2 ), $color2, $color1 ); @return if( lightness( $color ) < $contrast-threshold, $light, $dark ); } @else { @return $color1; } } /** Datei: plugins/rest/assets/scss/colors/_styles.scss **/ @import "variables.scss"; @import "mixins.scss"; @import "functions.scss"; @mixin colorize_btn_all() { @each $name, $color in $theme-colors { &.colored-#{$name} { @include colorize_btn( $name ); } } } @mixin colorize_btn( $color: inverted ) { @include colorize_this( $color ); &.focus { @include colorize_this( $color, $theme-active-opacity ); } &.active { @include colorize_this( $color, $theme-active-opacity ); } &.current { @include colorize_this( $color, $theme-active-opacity ); } &:focus { @include colorize_this( $color, $theme-hover-opacity ); } &:hover { @include colorize_this( $color, $theme-hover-opacity ); } } @each $name, $color in $theme-colors { .text--colored-#{$name} { color: #{get_color( $color )}; } .theme--colored-#{$name} { @include colorize( $color ); } } /** Datei: plugins/rest/assets/scss/colors/_mixins.scss **/ @mixin colorize( $color, $light: $theme-colorize-light, $dark: $theme-colorize-dark ) { // @debug 'theme-colorize-alpha: #{$theme-colorize-alpha}'; // @debug 'theme-colorize-bg-alpha: #{$theme-colorize-bg-alpha}'; // @debug 'theme-colorize-border-alpha: #{$theme-colorize-border-alpha}'; // @debug 'theme-colorize-hover-alpha: #{$theme-colorize-hover-alpha}'; // @debug 'theme-colorize-active-alpha: #{$theme-colorize-active-alpha}'; $normal : get_colorize_bg( $color ); $hover : get_colorize_bg( $color, $theme-colorize-hover-alpha ); $active : get_colorize_bg( $color, $theme-colorize-active-alpha ); --color-background: #{$normal}; --color-background-hover: #{$hover}; --color-background-active: #{$active}; --color-border: #{get_colorize_border( $color )}; --color-border-hover: #{get_colorize_border( $color, $theme-colorize-hover-alpha )}; --color-border-active: #{get_colorize_border( $color, $theme-colorize-active-alpha )}; --color-text: #{get_colorize_text( $normal, $light, $dark )}; --color-text-hover: #{get_colorize_text( $hover, $light, $dark )}; --color-text-active: #{get_colorize_text( $active, $light, $dark )}; } @function get_colorize_bg( $color, $alpha: 0 ) { @return get_color( $color, $theme-colorize-alpha + $theme-colorize-bg-alpha + $alpha ); } @function get_colorize_border( $color, $alpha: 0 ) { @return get_color( $color, #{$theme-colorize-alpha + $theme-colorize-border-alpha + $alpha } ); } @function get_colorize_text( $color, $light: $theme-colorize-light, $dark: $theme-colorize-dark ) { @return contrast( $color, $light, $dark ); } /** Datei: plugins/rest/assets/scss/_functions.scss **/ /** Datei: plugins/rest/assets/scss/_mixins.scss **/ @mixin transition( $property: all, $duration: 200ms, $timing-function: ease-in-out, $behavior: normal, $delay: 0 ) { transition-property: $property; transition-duration: $duration; transition-timing-function: $timing-function; transition-behavior: $behavior; transition-delay: $delay; } /** Datei: plugins/rest/assets/scss/layout/_table.scss **/ @import "../mixins.scss"; .cursor--pointer, .js--click-bound, .js--click-ownbound { cursor : pointer; }; .table { $table-padding : 5px; $table-gap-small : 10px !default; $table-gap-default : 15px !default; $table-gap-wide : 20px !default; $table-font-family : sans-serif !default; $table-border-radius : 0 !default; $table-border-spacing : 0 !default; $table-header-font-family : $table-font-family !default; $table-header-border-width : 0 1px 1px 0 !default; $table-header-bg-color : get_color_tone( content, 400 ) !default; $table-header-bg-color-hover : get_color_tone( content, 500 ) !default; $table-header-bg-color-active : get_color_tone( content, 400, primary ) !default; $table-header-bg-color-active-hover : get_color_tone( content, 500, primary ) !default; $table-header-border-color : get_color_tone( border, 400 ) !default; $table-header-border-color-hover : $table-header-border-color !default; $table-header-border-color-active : get_color_tone( content, 500, primary ) !default; $table-header-border-color-active-hover : $table-header-border-color-active !default; $table-cell-font-family : $table-header-font-family !default; $table-cell-border-width : $table-header-border-width !default; $table-cell-border-color : get_color( border ) !default; $table-cell-bg-color-hover : $table-header-bg-color-hover !default; $table-cell-border-color-hover : $table-header-border-color !default; $table-row-bg-color : get_color_tone( content, 100 ) !default; $table-row-bg-color-even : get_color_tone( content, 200 ) !default; $table-row-bg-color-hover : get_color_tone( content, 300, primary ) !default; $table-row-bg-color-hover-even : get_color_tone( content, 400, primary ) !default; $table-row-bg-color-active-hover : get_color_tone( content, 300, secondary ) !default; $table-row-bg-color-active-hover-even : get_color_tone( content, 400, secondary ) !default; $table-row-bg-color-success-even : get_color_tone( content, 200, success ) !default; $table-row-bg-color-success-hover-even : get_color_tone( content, 300, success ) !default; $table-row-bg-color-success-active-hover-even : get_color_tone( content, 400, success ) !default; $table-row-bg-color-danger-even : get_color_tone( content, 200, danger ) !default; $table-row-bg-color-danger-hover-even : get_color_tone( content, 300, danger ) !default; $table-row-bg-color-danger-active-hover-even : get_color_tone( content, 400, danger ) !default; width: calc( 100% + var( --gap ) * 2 ); margin: var( --gap-negate ); font-family: #{$table-font-family}; @if( $table-border-spacing == 0 ) { border-collapse: collapse; border: 1px solid #{get_color( border )}; } @else { border-spacing: #{$table-border-spacing}; border-collapse: separate; } &__ { &wrapper { display: flex; flex-direction: column; gap: #{$table-gap-wide}; grid-column: 1/-1; overflow: hidden; } &toolbar { display: grid; grid-template-areas: 'search buttons'; grid-template-rows: 2rem; gap: #{$table-border-spacing}; } &search { flex: 1; display: flex; gap: #{$table-border-spacing}; grid-area: search; &__input { max-width: 250px; padding: 0.35rem; border: 1px solid #{get_color( border )}; border-radius: #{$table-border-radius}; font-size: 0.9rem; min-height: 2rem; outline: none; &:hover { border-color: #{get_color_hover(primary)}; box-shadow: 0 0 5px #{get_color(primary, 0.5)}; outline: none; } &:focus { border-color: #{get_color(primary)}; box-shadow: 0 0 5px #{get_color(primary, 0.5)}; outline: none; } } } &buttons { display: flex; gap: #{$table-border-spacing}; grid-area: buttons; justify-content: end; height: 100%; } &body { &__ { &row { @include transition( 'background-color' ); &:nth-child(odd) { background-color: #{$table-row-bg-color}; &.js--click-bound:hover { background-color: #{$table-row-bg-color-hover}; } .js--click-ownbound:hover { background-color: #{$table-row-bg-color-active-hover}; } } &:nth-child(even) { background-color: #{$table-row-bg-color-even}; &.js--click-bound:hover { background-color: #{$table-row-bg-color-hover-even}; } .js--click-ownbound:hover { background-color: #{$table-row-bg-color-active-hover-even}; } } &--failed { background-color: #{get_color_tone(content, 100, danger)} !important; &.js--click-bound:hover { background-color: #{get_color_tone(content, 200, danger)} !important; } &.js--click-ownbound:hover { background-color: #{get_color_tone(content, 200, danger)} !important; } } &--failed { background-color: #{$table-row-bg-color-danger-even} !important; &.js--click-bound:hover { background-color: #{$table-row-bg-color-danger-hover-even} !important; } .js--click-ownbound:hover { background-color: #{$table-row-bg-color-danger-active-hover-even} !important; } } &--updated { background-color: #{$table-row-bg-color-success-even} !important; &.js--click-bound:hover { background-color: #{$table-row-bg-color-success-hover-even} !important; } .js--click-ownbound:hover { background-color: #{$table-row-bg-color-success-active-hover-even} !important; } } } } } &cell { padding: 0.3rem 0.5rem; height: 42px; // Grundhöhe vertical-align: middle; text-align: left; border-style: solid; border-color: #{$table-cell-border-color}; border-width: #{$table-cell-border-width}; .icon:not(.button__icon) { margin-right: #{$table-gap-small}; } &__ { &inner { $table-cell-inner: & !global; height: 100%; &__ { &text { flex: 1 0 auto; display: flex; align-items: center; } } } } &:is( .table__body__cell ) { padding: 0.3rem; > #{$table-cell-inner} { gap: 0.3rem; display: flex; justify-content: end; } } &--type { &-email { a { color: inherit; } } &-text { } &-amount { $table-cell-type-amount: & !global; text-align: right; } &-actions { text-align: right; } &-icon, &-status { width: 42px; min-width: 42px; text-align: center; padding: 0; &-icon { max-width: 42px; } .icon { display: flex; justify-content: center; align-items: center; width: 28px; height: 28px; font-size: 1.4rem; border-radius: 50%; margin: auto !important; } } } } &header { &__ { &cell { background-color: #{$table-header-bg-color}; border-color: #{$table-header-border-color}; border-width: #{$table-header-border-width}; font-weight: bold; text-transform: uppercase; &.sortable { position: relative; cursor: pointer; padding-right: 1.2rem; &::after { content: "▲▼"; position: absolute; font-size: 0.8rem; opacity: 0.5; right: 0.5rem; } &:hover { background-color: #{$table-header-bg-color-hover}; border-color: #{$table-header-border-color-hover}; } &#{$table-cell-type-amount} { padding-right: 0.5rem; padding-left: 1.2rem; &::after { right: auto; left: 0.5rem; } } &.sorting--asc::after { content: "▲"; } &.sorting--desc::after { content: "▼"; } &.sorting--current { background-color: #{$table-header-bg-color-active}; border-color: #{$table-header-border-color-active}; font-weight: bold; &:hover { background-color: #{$table-header-bg-color-active-hover}; border-color: #{$table-header-border-color-active-hover}; } // @include highlightBorder(); } } } } } } } /** Datei: plugins/rest/assets/scss/layout/_form.scss **/ .form { $form-padding : 5px; $form-font-size-label : 8px; $form-control-padding : $form-padding; $form-checked-size : 16px; $form-checked-inner : 10px; $tabs-gap-tiny : 5px !default;; $form-gap-small : 10px !default; $form-gap-default : 15px !default; $form-gap-wide : 20px !default; $form-icon-font-family : bootstrap-icons !default; $form-border-radius : 0 !default; font-size: 1rem; &__ { &container { display: flex; flex-direction: column; gap: $form-gap-default; > * { margin: 0; } } &fieldset { display: flex; flex-direction: column; gap: #{$form-gap-wide}; padding: 0; border: 0; &.has-legend { flex: 1 1 20%; background-color: #{get_color( white )}; padding: #{$form-gap-wide} 0 0; &.has-border { padding: #{$form-gap-wide}; border: 1px solid #{get_color( border )}; border-radius: #{$form-border-radius}; } } } &legend { font-weight: 600; font-size: 1.17em; color: #{get_color(black)}; } &info { font-size: #{$form-font-size-label}; padding: #{$form-padding}; position: absolute; display: none; flex-direction: row-reverse; align-items: start; gap: #{$form-padding}; top: 0; right: 0; &-- { &required { $form-info-required: & !global; color: get_color( danger ); } &feedback { $form-info-feedback: & !global; &::before { font-size: #{$form-font-size-label *1.5}; font-family: #{$icon-font-family}; translate: 0 -#{$form-font-size-label * 0.5 / 2}; } } } } &control { $form-control: & !global; display: flex; align-items: center; position: relative; &:is( input, select, textarea, .is-fake-control ) { position: relative; font-size: 1em; border-radius: #{$form-border-radius}; border: 0; outline: 0; padding: #{$form-control-padding}; transition: border-color 0.3s, box-shadow 0.3s; background: transparent; width: 100%; font-family: inherit; } &:is( pre ) { margin: 0; } &__{ &group { display: flex; flex-direction: column; gap: #{$form-control-padding}; } &icon { margin-right: #{$form-gap-small}; } &label { display: block; width: 100%; padding: #{$form-control-padding}; padding-left: #{$form-checked-size + $form-control-padding * 3}; position: relative; &:has( input[type=radio] ) { &::after, &::before { border-radius: 50%; } } &::before { content: ""; display: inline-block; aspect-ratio: 1; height: #{$form-checked-size}; border: 1px solid get_color( border ); margin-right: 0.5em; position: absolute; top: #{$form-control-padding}; left: #{$form-control-padding}; } &:has( input:checked ) { &::after { content: ''; aspect-ratio: 1; display: block; top: #{$form-control-padding + $form-checked-size / 2 - $form-checked-inner / 2}; left: #{$form-control-padding + $form-checked-size / 2 - $form-checked-inner / 2}; position: absolute; background-color: #{get_color_hover(primary)}; height: #{$form-checked-inner}; } } &:hover { &::before { border-color: #{get_color_hover(primary)}; box-shadow: 0 0 5px #{get_color(primary, 0.5)}; outline: none; } } } } &-- { @mixin form-control-icon { font-family: #{$form-icon-font-family}; position: absolute; right: #{$form-padding}; top: #{$form-padding + $form-padding + $form-font-size-label}; bottom: #{$form-padding}; aspect-ratio: 1; display: flex; justify-content: center; align-items: center; } &progress { .progress { &__ { &wrapper { position: relative; height: 17px; font-size: 0.8rem; margin: #{$form-control-padding}; overflow: hidden; border-radius: #{$form-border-radius}; background-color: #{get_color( border )}; } &bar, &label { position: absolute; top: 0; left: 0; height: 100%; } &bar { background-color: var( --progress-color, #{get_color( primary )} ); transition: background-color 300ms ease-in-out, width 500ms cubic-bezier(.4,0,.2,1), } &label { color: #{get_color( white )}; width: 100%; display: flex; align-items: center; justify-content: center;; } } } } &radio, &checkbox { // border-color: transparent !important; box-shadow: none !important; flex-direction: column; width: 100%; input { visibility: hidden; position: fixed; top: 100vh; left: 100vw; } } &file { cursor: pointer; input { position: absolute !important; visibility: hidden; } &::after { content: '\F3D1'; @include form-control-icon; } } &textarea { textarea { height: calc( 2em + var( --lines, 2 ) * ( 2.8125em + #{$form-gap-wide} ) ); height: calc( ( var(--lines) * #{$form-gap-wide} ) + ( 45px * var(--lines) ) + 1.625em ) } } &select { select { appearance: none; } &::after { content: '\F229'; @include form-control-icon; } } } } &group { display: flex; flex-direction: column; position: relative; border: 1px solid #{get_color(border)}; > label { font-weight: 500; color: #{get_color(text)}; font-size: #{$form-font-size-label}; padding: #{$form-padding} #{$form-padding} 0; } &:hover { border-color: #{get_color_hover(primary)}; box-shadow: 0 0 5px #{get_color(primary, 0.5)}; outline: none; } &.has-focus { border-color: #{get_color(primary)}; box-shadow: 0 0 5px #{get_color(primary, 0.5)}; outline: none; } &.is-required { #{$form-info-required} { display: flex; } } &.is-valid, &.is-invalid, &.on-validate { #{$form-info-required} { display: none; } #{$form-info-feedback} { display: flex; } } &.is-valid { border-color: #{get_color(success)}; background-color: #{get_color(success, 0.05)}; &:hover { box-shadow: 0 0 5px #{get_color(success, 0.5)}; } #{$form-info-feedback} { color: #{get_color(success)}; &::before { content: '\F26A'; } } } &.is-invalid { border-color: #{get_color(danger)}; background-color: #{get_color(danger, 0.05)}; &:hover { box-shadow: 0 0 5px #{get_color(danger, 0.5)}; } #{$form-info-feedback} { color: #{get_color(danger)}; &::before { content: '\F622'; } } } &.on-validate { border-color: #{get_color(warning)}; background-color: #{get_color(warning, 0.05)}; &:hover { box-shadow: 0 0 5px #{get_color(warning, 0.5)}; } #{$form-info-feedback} { color: #{get_color(warning)}; &::before { content: '\F3E2'; animation-name: animate-rotate; animation-duration: 3s; animation-iteration-count: infinite; animation-timing-function: linear; } } } } &buttons { display: flex; justify-content: end; gap: $form-gap-default; } &actions { display: flex; justify-content: flex-end; gap: 0.75rem; .action-button { padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: #{$form-border-radius}; cursor: pointer; border: none; transition: background-color 0.3s, transform 0.2s; &.primary { background-color: #{get_color(primary)}; color: #{get_color(white)}; &:hover { background-color: #{get_color_hover(primary)}; transform: scale(1.05); } &:active { transform: scale(0.95); } } &.secondary { background-color: #{get_color(secondary)}; color: #{get_color(white)}; &:hover { background-color: #{get_color_hover(secondary)}; transform: scale(1.05); } &:active { transform: scale(0.95); } } } } } } /** Datei: plugins/rest/assets/scss/layout/_tabs.scss **/ .tabs { $tabs-gap-tiny : 5px !default; $tabs-gap-small : 10px !default; $tabs-gap-default : 15px !default; $tabs-gap-wide : 20px !default; $tabs-icon-font-family : bootstrap-icons !default; $tabs-border-radius : 0 !default; display: flex; flex-direction: column; gap: #{$tabs-gap-wide}; width: 100%; .headline, .paragraph { grid-column: 1/-1; } &__ { &list { display: flex; list-style: none; gap: #{$tabs-gap-tiny}; padding: 0; margin: 0; border-bottom: 2px solid #{getCoreColor( gray )}; } &item { // padding: 0.2em 0.6em; font-size: 1rem; gap: #{$tabs-gap-small}; // min-height: 2rem; border-radius: #{$tabs-border-radius}; // border: none; background: transparent; cursor: pointer; transition: background 0.2s ease; display: flex; align-items: center; user-select: none; &:hover { background: rgba(0, 0, 0, 0.05); } &--active { // font-weight: bold; // @include highlightBorder(); } &--action { margin-left: auto; // Action-Tabs immer rechts background: rgba(0, 0, 0, 0.05); border-radius: #{$tabs-border-radius}; & + & { margin-left: 0; } } // @include buttonColors(); } &content { // padding: 15px; // border: 1px solid #{getThemeColor( border )}; // background: #fff; &__item { display: flex; flex-direction: column; gap: #{$tabs-gap-default}; display: grid; grid-template-columns: repeat( 2, 1fr ); > * { margin-block: 0; } > .form__group { grid-column: 1/-1; } } } &icon { font-size: 16px; } } } /** Datei: plugins/rest/assets/scss/layout/_button.scss **/ .button { &__ { &dropdown { $button-dropdown: & !global; display: none; z-index: 10; position: absolute; top: 100%; padding-top: var( --gap ); left: 0; flex-direction: column; gap: var( --gap ); } &wrapper { $button-wrapper: & !global; position: relative; display: flex; flex-direction: column-reverse; gap: var( --gap ); &.is--open { #{$button-dropdown} { display: flex; } } } } } /** Datei: plugins/rest/assets/js/components/button.js **/ /** * /arunox/components/button.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ ARUNOX.Templates.register( 'button', { hooks : { config : function( config ) { if( config.type ) config.attributes.type ??= config.type; if( config.name ) config.attributes.name ??= config.name; if( config.value ) config.attributes.value ??= config.value; if( config.title ) config.attributes.title ??= config.title; if( config.target ) config.attributes.target ??= config.target; return config; }, wrapper : function( root ) { const config = this.instance.config; if( !config.dropdowns ) return root; const wrapper = ARUNOX.Templates.render( 'div', { classes : 'button__wrapper', }) this.instance.nodes.wrapper = wrapper; this.instance.apply('dropdown'); wrapper.root.append( root ); return wrapper.root; }, dropdown : function() { const config = this.instance.config; const wrapper = this.instance.nodes.wrapper; if( !wrapper || !config.dropdowns ) return; const menu = wrapper.render('div', { nodeName : 'menu', classes : 'button__dropdown' }); for( const item of config.dropdowns ) { const itemContainer = menu.render( 'div', { classes : 'button__dropdown__item' }); itemContainer.render( 'button', Object.assign({ color : 'secondary', type : 'button', block : 'button__dropdown' }, item.config ), this.instance.data); } this.instance.root.addEventListener( 'click', ( event ) => { event.stopPropagation(); wrapper.root.classList.toggle('is--open'); }); wrapper.root.addEventListener( 'mouseleave', () => { wrapper.root.classList.remove( 'is--open'); const focusedElement = wrapper.root.querySelector(':focus'); if( focusedElement ) focusedElement.blur(); }); }, }, htmlTag : function( config, data, ctx ) { return config.href ? 'a' : 'button'; }, builder : function( config, data ) { const className = config?.name || 'button'; const block = config.block ? `${className} ${config.block}__${className}` : className; this.root.classList.add( ...block.split(' ') ); if( config.label ) { this.root.classList.add( 'has--label' ); this.render( 'span', { classes : `${block}__label label`, content : config.label, }); } if( config.href ) { this.root.href = ARUNOX.Router.parseUrl( config.href, data ); this.root.addEventListener( 'click', ( event ) => { event.stopPropagation(); }); return; } if( config.type ) { const form = this.root.closest( 'form' ); if( form ) { if( !form.id ) form.id = crypto.randomUUID(); this.root.setAttribute( 'form', form.id ); } } if( config.action && !config.dropdowns ) { this.root.addEventListener( 'click', ( event ) => { event.preventDefault(); event.stopPropagation(); if( this.root.name && this.root.value !== '' && this.root.value !== 'undefined' ) { ARUNOX.Router.handle( config.action, { [`${this.name}`] : this.value }, data, 'rest' ); } else ARUNOX.Router.handle( config.action, {}, data, 'rest' ); }); } }, }); ARUNOX.Templates.register( 'buttons', { htmlTag : 'div', hooks : { config : function( config ) { config.datasets.arx.node = 'buttons'; config.content ??= config.buttons || []; return config; }, }, builder : function( config, data ) {} }); /** Datei: plugins/rest/assets/js/components/tabs.js **/ /** * /arunox/components/tabs.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ ARUNOX.Templates.register( 'tabs', { htmlTag : 'div', hooks : { config : function( config, data ) { config.classes.push( 'tabs' ); return config; }, buildRootAfter : function() { const config = this.instance.config; config.query = window.location.hash.match( /\?(.*)/ ); config.hash = window.location.hash.substring(1).replace( /\?.*/, '' ); config.filter = new URLSearchParams( config.query ? config.query[1] : '' ); config.isNested = this.instance.root.closest( '.dialog, .tabs__content' ) !== null; if( config.isNested ) { let match = ARUNOX.Router.history.match( /.*(?:#([a-zA-Z0-9]+)).*/ ); config.hash = this.instance.root.dataset.activeTab || match ? match[1] : ''; config.filter = new URLSearchParams(); } }, }, builder : function( config, data ) { this.render( 'tabs.buttons', { nodeName : 'buttons', tabs : config.tabs, }); this.render( 'tabs.contents', { nodeName : 'contents', tabs : config.tabs, }); }, }); ARUNOX.Templates.register( 'tabs.buttons', { extends : 'buttons', hooks : { config : function( config, data ) { config.classes.push( 'tabs__list' ); return config; }, content : function( content, config, data ) { for( let [ index, tabConfig ] of config.tabs.entries() ) { tabConfig = Object.assign( { index }, tabConfig ); delete tabConfig.content; content.push( ARUNOX.Templates.config( 'tabs.button', tabConfig, data ) ); } return content; }, }, }); ARUNOX.Templates.register( 'tabs.button', { extends : 'button', hooks : { config : function( config, data ) { config.classes.push( 'tabs__item' ); config.color = 'secondary'; config.arx.tab = config.id; return config; }, select : function() { const tabs = this.instance.closest( 'tabs' ); const tabsConfig = tabs.config; const config = this.instance.config; const buttons = tabs.nodes.buttons; const contents = tabs.nodes.contents; for( const button of buttons.root.childNodes ) { button.classList.remove('tabs__item--active' ); if( button.dataset.arxTab == config.arx.tab ) { this.instance.root.classList.add( 'tabs__item--active' ); } } for( const content of Object.values( contents.root.childNodes ) ) { if( content.dataset.arxTab == config.arx.tab ) { content.classList.add('tabs__content--active' ); content.style.display = ''; content.ARX.apply('show'); } else { content.classList.remove('tabs__content--active' ); content.style.display = 'none'; content.ARX.apply('hide'); } } if( tabsConfig.isNested ) return; if( this.instance.root.dataset.tabFilter ) history.pushState(null, null, `#${config.arx.tab}?${config.arx.filter}` ); else if( config.arx.tab ) history.pushState(null, null, `#${config.arx.tab}` ); else history.pushState(null, null, '' ); }, afterRender : function( config, data ) { const tabs = this.instance.closest( 'tabs' ); const tabsConfig = tabs.config; if( config.action ) { this.instance.root.classList.add( 'tabs__item--action' ); this.instance.root.addEventListener( 'click', () => ARUNOX.Router.handle( config.action, {}, data, 'rest' ) ); return; } if( config.type === 'submit' ) { this.instance.root.classList.add( 'tabs__item--action' ); return; } this.instance.root.addEventListener( 'click', () => this.instance.apply('select') ); if( ( tabsConfig.hash && tabsConfig.hash === config.arx.tab ) || ( !tabsConfig.hash && ( config.active || ( config.index === 0 && !tabsConfig.tabs.some( t => t.active ) ) ) ) ) { queueMicrotask( () => this.instance.apply('select') ); } }, }, }); ARUNOX.Templates.register( 'tabs.contents', { htmlTag : 'div', hooks : { config : function( config, data ) { config.classes.push( 'tabs__content', 'scroll__viewport' ); config.arx.tab = config.id; return config; }, }, builder : function( config, data ) { for( const tabConfig of config.tabs ) { if( tabConfig.action || tabConfig.type === 'submit' ) continue; this.render( 'tabs.content', tabConfig ); } }, }); ARUNOX.Templates.register( 'tabs.content', { htmlTag : 'div', hooks : { config : function( config, data ) { config.classes.push( 'tabs__content__item' ); config.arx.tab = config.id; config.styles.display = 'none'; delete config.icon; delete config.label; return config; }, }, builder : function( config, data ) {}, }); /** Datei: plugins/rest/assets/js/components/table.js **/ /** * /arunox/components/table.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ ARUNOX.Templates.register( 'table', { hooks : { config : function( config, data ) { config.classes.push( 'table' ); return config; }, wrapper : function( root ) { const wrapper = ARUNOX.Templates.render( 'div', { classes : 'table__wrapper', }) this.instance.nodes.wrapper = wrapper; this.instance.apply('toolbar'); wrapper.root.append( root ); return wrapper.root; }, toolbar : function() { const config = this.instance.config; const filters = this.instance.filter( 'filters', config.filter, config.columns ); const wrapper = this.instance.nodes.wrapper; const toolbar = wrapper.render( 'div', { nodeName : 'toolbar', classes : 'table__toolbar', }); if( config.search ) { toolbar.render( 'div', { nodeName : 'searchContainer', classes : 'table__search', content : [ config.search ? ARUNOX.Templates.config( 'input', { nodeName : 'searchInput', type : 'text', classes : 'table__search__input', attributes : { placeholder : 'Suchen...', }, onInput : () => this.instance.apply( 'search', toolbar.nodes.searchInput.root.value ), }) : null, ARUNOX.Templates.config( 'button', { nodeName : 'searchReset', color : 'danger', type : 'button', block : 'table__search', title : 'Suche zurücksetzen', icon : 'bi bi-funnel', apply : [ 'active', false ], onClick : () => this.instance.apply( 'reset', 'search', config ), }), ], }); } if( filters ) { toolbar.render( 'buttons', { nodeName : 'filterContainer', classes : 'table__filter', content : [ ...filters, ARUNOX.Templates.config( 'button', { nodeName : 'filterReset', color : 'danger', type : 'button', block : 'table__filter', label : 'Filter zurücksetzen', icon : 'bi bi-funnel', apply : [ 'active', false ], onClick : () => this.instance.apply( 'reset', 'filter', config ), }) ], }); } if( config.buttons ) { const buttons = toolbar.render( 'buttons', { content : config.buttons, nodeName : 'buttons', classes : 'table__buttons', }); } }, filters : function( filters, columns ) { const buttons = []; if( !Array.isArray( filters ) ) filters = [ filters ]; for( const column of filters ) { const config = columns.find( item => item.key == column ); if( !config?.filterable || !config?.options ) continue; for( const option of config.options ) { buttons.push( ARUNOX.Templates.config( 'button', { nodeName : `filter${config.key.toUpperCaseFirst()}-${option.value}`, color : option?.color, type : 'button', block : 'table__filter', label : option?.label, icon : option?.icon, apply : [ 'active', false ], onClick : () => this.instance.apply( 'filter', config.key, option?.value ), }) ); } } return buttons; }, filter : function( column, filterValue ) { // console.log( 'FILTER', column, filterValue ); const toolbar = this.instance.nodes.toolbar; const tableBody = this.instance.nodes.body; const tableRows = tableBody.root.querySelectorAll('tr'); filterValue = filterValue.toString().toLowerCase(); let visibleRows = []; tableRows.forEach(row => { const filterData = row.dataset?.[`arxFilter${column.toUpperCaseFirst()}`] || ''; const isVisible = filterData == filterValue; row.style.display = isVisible ? null : 'none'; if( isVisible ) { visibleRows.push( row ); } }); toolbar.nodes.filterContainer.root.childNodes.forEach( element => { element.ARX.apply( 'active', element.ARX.config.nodeName == `filter${column.toUpperCaseFirst()}-${filterValue}` ); }); toolbar.nodes.filterReset.apply( 'active', visibleRows.length != tableRows.length ); visibleRows.forEach( row => tableBody.root.append( row ) ); }, search : function( searchValue ) { // console.log( 'SEARCH', searchValue ); const toolbar = this.instance.nodes.toolbar; const tableBody = this.instance.nodes.body; const tableRows = tableBody.root.querySelectorAll('tr'); searchValue = searchValue.toLowerCase(); let visibleRows = []; tableRows.forEach(row => { const searchData = row.dataset?.arxSearch || ''; const isVisible = searchData.includes( searchValue ); row.style.display = isVisible ? null : 'none'; if( isVisible ) { visibleRows.push( row ); } }); toolbar.nodes.searchReset.apply( 'active', visibleRows.length != tableRows.length ); visibleRows.forEach( row => tableBody.root.append( row ) ); }, reset : function( config, type ) { // console.log( 'RESET', type ); const toolbar = this.instance.nodes.toolbar; const tableBody = this.instance.nodes.body; const tableRows = tableBody.root.querySelectorAll('tr'); tableRows.forEach(row => { row.style.display = null; }); const query = window.location.hash.match( /\?(.*)/ ); const hash = window.location.hash.substring(1).replace( /\?.*/, '' ); const filter = new URLSearchParams( query ? query[1] : '' ); if( config?.query ) { for( const [ index, value ] of config.query.entries() ) { if( filter.has( index ) && filter.get( index ) == value ) { filter.delete( index ); } } } if( hash && filter?.size ) history.pushState(null, null, `#${hash}?${filter.toString()}`); else if( filter?.size ) history.pushState(null, null, `?${filter.toString()}`); else if( hash ) history.pushState(null, null, `#${hash}`); else history.pushState(null, null, ); toolbar.nodes.filterContainer.root.childNodes.forEach( element => { element.ARX.apply( 'active', false ); }); if( toolbar.nodes.filterReset ) toolbar.nodes.filterReset.apply( 'active', false ); if( toolbar.nodes.searchReset ) toolbar.nodes.searchReset.apply( 'active', false ); if( toolbar.nodes.searchInput ) toolbar.nodes.searchInput.root.value = ''; }, sort : function( column, order ) { // console.log( 'SORT', column, order ); const config = this.instance.config; const thead = this.instance.nodes.header; const tbody = this.instance.nodes.body; const tabelCell = thead.root.querySelector( `[data-arx-column="${column}"]` ); const tableRows = Array.from( tbody.root.querySelectorAll( 'tr' ) ); if( !tabelCell || !tableRows ) return; if( !order ) order = tabelCell.dataset.arxOrder === 'asc' ? 'desc' : 'asc'; const columns = config.columns || []; const rules = config.order.rules || {}; if( rules[ column ]?.replace ) { for( const [ oldCol, newCol ] of Object.entries( rules[ column ].replace ) ) { this.instance.root.querySelectorAll(`td[data-arx-column="${oldCol}"]`).forEach(td => { const rowData = td.closest('tr').rowData; td.innerHTML = rowData[ newCol ] || ''-''; // td.dataset.arxColumn = newCol; // Key anpassen }); } } if( rules[ column ]?.show ) { for( const showCol of rules[ column ].show ) { this.instance.root.querySelectorAll(`td[data-arx-column="${showCol}"], th[data-arx-column="${showCol}"]`).forEach(el => { el.style.display = ''; // Wieder anzeigen }); } } if( rules[ column ]?.hide ) { for( const hideCol of rules[ column ].hide ) { this.instance.root.querySelectorAll(`td[data-arx-column="${hideCol}"], th[data-arx-column="${hideCol}"]`).forEach(el => { el.style.display = 'none'; // Wieder anzeigen }); } } // Entferne vorherige Sortierklassen this.instance.root.querySelectorAll('.table__header__cell').forEach(cell => { cell.classList.remove('sorting--asc', 'sorting--desc', 'sorting--current'); }); // Setze neue Sortierklasse if( tabelCell ) { tabelCell.dataset.arxOrder = order; tabelCell.classList.add( `sorting--${order}`, 'sorting--current' ); } this.instance.root.dataset.arxSortingColumn = column; this.instance.root.dataset.arxSortingOrder = order; // Reihen sortieren tableRows.sort( (rowA, rowB) => { let a = rowA.ARX.data[ column ]?.trim() || ''; let b = rowB.ARX.data[ column ]?.trim() || ''; // Versuche als Zahl zu interpretieren, falls möglich if (!isNaN(a) && !isNaN(b)) { a = parseFloat(a); b = parseFloat(b); } return order === 'asc' ? String(a).localeCompare(String(b)) : String(b).localeCompare(String(a)); }); // Sortierte Reihen neu einfügen tbody.root.innerHTML = ''; tableRows.forEach( row => tbody.root.append( row ) ); }, loaded : function() { const config = this.instance.config; if( config.order && config.order.column ) { this.instance.apply( 'sort', config.order.column, config.order.order ); } }, }, builder : function( config, data ) { this.render( 'thead', { nodeName : 'header', columns : config.columns, }); const body = this.render( 'tbody', { nodeName : 'body', columns : config.columns, apply : [ 'generate', data, true ], }, data ); if( config.api ) { if( !config.api.resolved ) { config.api = ARUNOX.Router.parseRoute( config.api, data ); } ARUNOX.Router.fetch( config.api ).then( data => body.apply( 'generate', data, false ) ); } }, }); ARUNOX.Templates.register( 'thead', { hooks : { config : function( config, data ) { config.classes.push( 'table__header' ); return config; }, }, builder : function( config, data ) { const headerRow = this.render( 'tr', { classes : [ 'table__header__row' ], columns : config.columns, }); } }); ARUNOX.Templates.register( 'tbody', { hooks : { config : function( config, data ) { config.classes.push( 'table__body' ); return config; }, generate : async function( data, loading ) { const table = this.instance.closest( 'table' ); this.instance.root.innerHTML = ''; if( data && data.length ) { for( const rowData of data ) { this.instance.render( 'tr', { classes : [ 'table__body__row' ], columns : this.instance.config.columns, }, rowData ); } table.apply( 'loaded' ); return; } this.instance.render( 'tr', { classes : [ 'table__body__row' ], columns : [ { key : 'hint', attributes : { colSpan : this.instance.config.columns.length, }, innerText : loading ? 'Daten werden geladen ...' : 'Keine Datensätze gefunden', } ], }); }, }, }); ARUNOX.Templates.register( 'tr', { hooks : { config : function( config, data ) { config.arx.filter ??= {}; config.arx.search ??= []; config.classes.push( 'table_row' ); return config; }, afterRender : function( config, data ) { const table = this.instance.closest( 'table' ); this.instance.setUUID( data?.hashid ); if( table.config.action && data ) { this.instance.root.classList.add('cursor--pointer'); this.instance.root.addEventListener('click', async ( event ) => { if( event.target.tagName.toLowerCase() === 'a' ) return; await ARUNOX.Router.handle( table.config.action, {}, data, 'rest' ); }); } }, afterContent : function( config, data ) { this.instance.config.arx.search = this.instance.config.arx.search.join( ' ' ).toLowerCase(); }, update : function( data, status ) { const updatedRow = this.instance.replace( 'tr', { columns : this.instance.config.columns, classes : status ? 'table__body__row--updated' : 'table__body__row--failed', }, data ); setTimeout(() => updatedRow.root.classList.remove( 'table__body__row--updated', 'table__body__row--failed' ), 200 ); }, }, builder : function( config, data ) { const cellTag = this.parent.template == 'thead' ? 'th' : 'td'; for( const column of config.columns ) { this.render( cellTag, column, data ); } }, }); ARUNOX.Templates.register( 'th', { htmlTag : 'th', hooks : { config : function( config, data ) { config.type ??= 'text'; config.innerText ??= config?.label || null; config.arx.column ??= config?.key || null; config.classes.push( 'table__header__cell' ); config.classes.push( `table__header__cell--type-${config.type}` ); config.classes.push( 'table__cell' ); config.classes.push( `table__cell--type-${config.type}` ); if( config.sortable ) { config.classes.push( 'sortable' ); config.arx.column = config.key; config.arx.order = config.order?.order || 'asc'; config.events.click = () => this.instance.apply( 'sort', null ); } if( !config.visible ) { config.styles.display = 'none'; } return config; }, sort : function( order ) { const table = this.instance.closest( 'table' ); const column = this.instance.root.dataset?.arxColumn || null; table.apply( 'sort', column, order ); }, }, builder : function( config, data ) {} }); ARUNOX.Templates.register( 'td', { hooks : { config : function( config, data ) { config.type ??= 'text'; config.arx.column ??= config?.key || null; config.classes.push( 'table__cell' ); config.classes.push( `table__cell--type-${config.type}` ); if( !config.visible ) { config.styles.display = 'none'; } return config; }, data : function( data, config ) { if( data === undefined ) return data; let value = '-'; if( typeof config.output === 'object' ) { let keys = Array.isArray( config.output.keys ) ? config.output.keys : [ config.output.keys ]; let values = []; for( const key of keys ) { values.push( data[ key ] || null ); } if( config.output.format == 'string' ) { value = values.join( config.output.seperator || ' ' ); } else value = values.join(' '); data[ config.key ] = value; } if( data[ config.key ] !== undefined ) { value = data[ config.key ]; } else if( config.key.indexOf('.') > -1 ) { let current = data; for( const key of config.key.split('.') ) { current = current[ key ]; } if( current !== undefined ) { value = current; } } return value; }, beforeRender : function( config, data ) { this.instance.config.innerText ??= data || '-'; if( data !== null && data !== undefined ) { if( config?.filterable ) this.instance.parent.config.arx.filter[ config.key ] = data.toString().toLowerCase(); if( config?.searchable ) this.instance.parent.config.arx.search.push( data.trim() ); } }, afterRender : function( config, data ) { this.instance.apply( `type.${config.type}` ); }, }, }); ARUNOX.Hooks.register( 'td->type.icon', function() { const data = this.instance.data; const config = this.instance.config; const option = config.options[ data ] || {}; this.instance.root.textContent = null; const icon = this.instance.render( 'icon', { name : option?.icon || 'bi bi-question-diamond', color : { target : 'text', prefix : null, color : option?.color || 'default', }, attributes : { title : option?.label || `${config.title} nicht definiert`, }, }); }); ARUNOX.Hooks.register( 'td->type.status', function() { this.instance.apply( 'type.icon' ); const table = this.instance.closest( 'table' ); const config = this.instance.config; const data = this.instance.parent.data; const action = Object.assign( {}, { query : table.config.api?.query || {}, }, config.action ); this.instance.root.classList.add( 'js--click-ownbound' ); this.instance.root.addEventListener( 'click', async event => { event.preventDefault(); event.stopPropagation(); await ARUNOX.Router.handle( action, {}, data, 'rest' ); }); }); /** Datei: plugins/rest/assets/js/components/headline.js **/ /** * /components/headline.js * * Headline-Component mit Icon-Support * * @author Sparky * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File created */ ARUNOX.Templates.register( 'headline', { htmlTag : function( config, data, ctx ) { return `h${config.size || 2}`; }, builder : function( config, data ) { const className = 'headline'; const block = config.block ? `${className} ${config.block}__${className}` : className; this.root.classList.add( ...block.split(' ') ); // Title als Text-Node if( config.title ) { const textNode = document.createTextNode( config.title ); this.root.appendChild( textNode ); } } }); /** Datei: plugins/rest/assets/js/components/icon.js **/ /** * /arunox/components/icon.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ ARUNOX.Templates.register( 'icon', { htmlTag : 'i', builder : function( config, data ) { const className = 'icon'; const block = config.block ? `${className} ${config.block}__${className}` : className; this.root.classList.add( ...config.name.split(' ') ); this.root.classList.add( ...block.split(' ') ); this.parent.root?.classList.add( 'has--icon' ); }, }); /** Datei: plugins/rest/assets/js/components/form.js **/ /** * rest/arunox/templates/form.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ ARUNOX.Templates.register( 'form', { htmlTag : 'form', hooks : { config : function( config ) { config.attributes.id = crypto.randomUUID(); config.datasets.arx.node = this.instance.htmlTag; config.classes.push( 'form__container' ); return config; }, content : function( content, config, data ) { if( config.buttons ) { content.push( ARUNOX.Templates.config( 'buttons', { classes : [ 'form__buttons' ], content : config.buttons.map( button => { button.config.attributes ??= {}; button.config.attributes.form = this.instance.config.attributes.id; return button; }), }, data ) ); } return content; }, afterRender : function( config, data ) { if( !config.action ) return; let lastSearchParams = ARUNOX.Router.history?.match( /\?(.*)/) | {}; let formSearchParams = config.action.route.match( /\?(.*)/); if( lastSearchParams ) lastSearchParams = new URLSearchParams( lastSearchParams[1] ); if( formSearchParams ) formSearchParams = new URLSearchParams( formSearchParams[1] ); if( lastSearchParams ) { if( lastSearchParams.has('columns') && ( !formSearchParams || !formSearchParams.has('columns') ) ) { if( formSearchParams ) lastSearchParams = '&columns=' + lastSearchParams.get('columns'); else lastSearchParams = '?columns=' + lastSearchParams.get('columns'); config.action.route += lastSearchParams; } } this.instance.noValidate = true; this.instance.actionConfig = ARUNOX.Router.parseRoute( config.action, data, {}, 'rest' ); } }, }); ARUNOX.Templates.register( 'control', { htmlTag : 'div', hooks : { config : function( config ) { config.input = Object.assignRecursive( {}, config || {} ); config.template = `input/${config.type}`; config.datasets.arx.node = 'control'; config.datasets.arx.control = config.type; if( !ARUNOX.Templates.exists( config.template ) ) { if( config.type == 'select' ) config.template = 'select' else if( config.type == 'textarea' ) config.template = 'textarea' else if( config.type == 'datalist' ) config.template = 'datalist' else config.template = 'input'; } config.classes.push( 'form__group', `form__control--${config.type}` ); return config; }, }, builder : function( config, data ) { if( config.label ) { const label = this.render( 'label', { classes : [ 'form__label' ], content : config.label, attributes : { for : `control_${config.key}`, }, }); } const element = this.render( config.template, Object.assignRecursive( { classes : [ 'form__control' ], datasets : { arx : { node : 'input', }, }, }, config.input || {} ), data ); }, }); ARUNOX.Hooks.register( 'input->beforeRender', function( config, data ) { config.attributes = { name : config.key, type : config.type, required : !!config.required, }; }); ARUNOX.Hooks.register( 'select->beforeRender', function( config, data ) { config.content = []; config.attributes = { name : config.key, required : !!config.required, multiple : !!config.multiple, }; // console.log( config, data ); if( config.choose ) { config.content.push( ARUNOX.Templates.config( 'option', { content : config.choose, attributes : { selected : true, }, } ) ); } for( const option of config.options ) { config.content.push( ARUNOX.Templates.config( 'option', { content : option.label, attributes : { value : option.value, selected : data?.[ config.key ] == option.value, }, } ) ); } }); ARUNOX.Hooks.register( 'textarea->beforeRender', function( config, data ) { config.attributes = { name : config.key, required : !!config.required, }; }); if( !window.formSubmitListener ) { document.addEventListener('submit', async event => { if( !event.target.matches('form') ) return; event.preventDefault(); const form = event.target; const inputs = form.querySelectorAll('input, select, textarea'); const promises = []; console.log( inputs ); inputs.forEach(input => { if( typeof input.ARX.validate === 'function' ) { promises.push(new Promise((resolve) => { input.addEventListener('validationComplete', () => { resolve( !!input.closest('.is-valid') ); }, { once: true }); input.dispatchEvent( new Event('validate', { bubbles: true } ) ); })); } }); const results = await Promise.all( promises ); const isValid = results.every( result => result ); if( isValid ) { ARUNOX.Router.handle( form.ARX.actionConfig, new FormData( form ), {}, 'rest' ); } }); window.formSubmitListener = true; } /** Datei: plugins/rest/assets/js/components/notifications.js **/ /** * /arunox/components/notifications.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ ARUNOX.Templates.register( 'notifications', { htmlTag : 'div', hooks : { shouldRebuild : function( shouldRebuild, config, data ) { return document.querySelector('.notification__wrapper'); }, config : function( config, data ) { config.classes.push( 'notification__wrapper', 'notification--show-on-hover', 'notification--stacked' ); return config }, }, addNotification : function( config, data ) { this.render( 'notification', config, data ); } }); ARUNOX.Templates.register( 'notification', { htmlTag : 'div', hooks : { config : function( config, data ) { return { type : config.type, color : config.color || config.type, duration : config.duration, message : config.message || config.title, classes : [ 'notification' ], content : [ config.icon ? ARUNOX.Templates.config( 'icon', { name: config.icon, block: 'notification' }) : null, ARUNOX.Templates.config( 'div', { classes : [ 'notification__inner' ], content : [ config.title ? ARUNOX.Templates.config( 'div', { innerText : config.title, classes : 'notification__title', }) : null, config.message ? ARUNOX.Templates.config( 'div', { innerText : config.message, classes : 'notification__message', }) : null, ], }), ARUNOX.Templates.config( 'button', { classes : [ 'notification__close' ], innerHTML : '×', onClick : () => this.apply( 'close' ), }), ], }; }, close : function() { this.instance.root.classList.add( 'is-closing' ); setTimeout(() => this.instance.root.remove(), 300 ); }, afterRender : function( config, data ) { if( config.duration === 0 ) return; setTimeout(() => this.instance.apply( 'close' ), config.duration || 5000 ); } }, }); /** Datei: plugins/rest/assets/js/hooks/colorize.js **/ /** * rest/arunox/hooks/colorize.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Adaptation to new ctx method * @since 1.0.0 File is created */ ARUNOX.Hooks.register( 'colorize', function( cellColorConfig, optionColorConfig, iconContainer ) { if( typeof cellColorConfig === 'string' ) cellColorConfig = { color: cellColorConfig } if( typeof optionColorConfig === 'string' ) optionColorConfig = { color: optionColorConfig } let colorConfig = Object.assign( { target : 'background', prefix : false, }, optionColorConfig, cellColorConfig ); // console.log( colorConfig ); if( colorConfig.prefix ) { this.instance.root.classList.removeRexEx( /.*--colored-.*/ ); this.instance.root.classList.add( `${colorConfig.prefix}--colored-${colorConfig.color || 'default'}` ); return ; } if( this.instance.root && ['button'].includes( colorConfig.target ) ) { this.instance.root.classList.removeRexEx( /button--colored-.*/ ); this.instance.root.classList.add( `button--colored-${colorConfig.color || 'default'}` ); this.instance.root.style.backgroundColor = colorConfig.value; } if( this.instance.root && ['background'].includes( colorConfig.target ) ) { this.instance.root.classList.removeRexEx( /bg--colored-.*/ ); this.instance.root.classList.add( `bg--colored-${colorConfig.color || 'default'}` ); this.instance.root.style.backgroundColor = colorConfig.value; } if( iconContainer && ['icon','both'].includes( colorConfig.target ) ) { iconContainer.classList.removeRexEx( /text--colored-.*/ ); iconContainer.classList.add( `text--colored-${colorConfig.color || 'default'}` ); iconContainer.style.color = colorConfig.value; } if( this.instance.root && ['text','both'].includes( colorConfig.target ) ) { this.instance.root.classList.removeRexEx( /text--colored-.*/ ); this.instance.root.classList.add( `text--colored-${colorConfig.color || 'default'}` ); this.instance.root.style.color = colorConfig.value; } }); /** Datei: plugins/rest/assets/js/hooks/_general.js **/ /** * /arunox/hooks/_general.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 1.0.0 * @since 1.0.0 File is created */ import '/plugins/rest/assets/js/hooks/colorize.js'; import '/plugins/rest/assets/js/components/icon.js'; ARUNOX.Hooks.register( 'applyConfigAfter', function( element, config ) { if( config.color ) this.instance.apply( 'colorize', typeof config.color === 'object' ? config.color : { color: config.color }, { prefix: 'theme' } ); if( config.icon ) this.instance.render( 'icon', { name: config.icon } ); }); ARUNOX.Hooks.register( 'config', function( config, data ) { config.arx ??= {}; config.events ??= []; config.styles ??= {}; config.classes ??= []; config.datasets ??= {}; config.attributes ??= {}; config.datasets.arx ??= config.arx; if( typeof config.color === 'string' ) config.color = { color: config.color }; if( typeof config.classes === 'string' ) config.classes = config.classes.split(' '); return config; }, 2 ); ARUNOX.Hooks.register( 'config', function( config, data ) { if( config.nodeName ) { config = this.instance.filter( `config.node->${config.nodeName}`, config, data ); } return config; }); /** Datei: plugins/rest/assets/js/core/templates.js **/ /** * /arunox/core/templates.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX ??= {}; ARUNOX.Templates = { _type : 'templates', _componentData : { content : undefined, config : undefined, data : undefined, htmlTag : 'div', template : null, container : null, // DOM-Object root : null, // DOM-Object parent : null, // ARX-Object children : [], // ARX-Object nodes : () => Object.create(null), hooks : () => Object.create(null), keepOnReplace : () => ({ classes : [], // CSS-Klassen die erhalten bleiben sollen attributes : [], // Attribute die erhalten bleiben sollen datasets : [], // Data-Attribute die erhalten bleiben sollen structure : null, // DOM-Struktur die erhalten werden soll }), takeOnReplace : () => ({ classes : [], // CSS-Klassen die übernommen werden sollen attributes : [], // Attribute die übernommen werden sollen datasets : [], // Data-Attribute die übernommen werden sollen }), mounted : false, destroyed : false, }, _componentProto : { apply : function( hook, ...args ) { ARUNOX.Hooks.apply.call( this, hook, ...args ); if( this.extends ) ARUNOX.Hooks.apply.call( this, `${this.extends}->${hook}`, ...args ); ARUNOX.Hooks.apply.call( this, `${this.template}->${hook}`, ...args ); // if( this.nodeName ) console.log( 'apply', `${this.template}->${hook}`, this.nodeName ); }, filter : function( hook, value, ...args ) { value = ARUNOX.Hooks.filter.call( this, hook, value, ...args ); if( this.extends ) value = ARUNOX.Hooks.filter.call( this, `${this.extends}->${hook}`, value, ...args ); // if( this.nodeName ) console.log( 'filter', `${this.template}->${hook}`, this.nodeName ); return ARUNOX.Hooks.filter.call( this, `${this.template}->${hook}`, value, ...args ); }, render : function( templateName, config, data, container ) { return ARUNOX.Templates.render( templateName, config, data, container || this.root, this ); }, replace : function( template, config, data ) { const newCtx = typeof template === 'object' ? template : ARUNOX.Templates.render( template, config, data, null, this.parent ); const newRoot = newCtx && newCtx.root instanceof HTMLElement ? newCtx.root : null; if( !newRoot ) return this.root; const preserveData = this._collectPreservables( this, newCtx ); if( this.mounted && this.root instanceof HTMLElement ) { this.root.replaceWith( newRoot ); } this._applyPreserved( newRoot, preserveData ); this.root = newRoot; this.nodes = newCtx.nodes || Object.create(null); newCtx.mounted = true; return this; }, node : function( nodeName, nodeConfig = {}, placement = 'append', refElement = null ) { if( !this.nodes ) this.nodes = {}; if( !this.nodes[ nodeName ] ) { nodeConfig = typeof nodeConfig === 'object' ? nodeConfig : { textNode: nodeConfig }; nodeConfig = Object.assign( { htmlTag : 'div', datasets : { arx : { node : nodeName, }, }, }, nodeConfig ); nodeConfig = this.filter(`node`, nodeConfig ); nodeConfig = this.filter(`node${nodeName.toUpperCaseFirst()}`, nodeConfig ); refElement = refElement instanceof HTMLElement ? refElement : refElement?.root || null; const nodeCtx = ARUNOX.Templates._createContext( nodeName, nodeConfig, this, this.root, { htmlTag: nodeConfig.htmlTag }); const ctxRender = nodeCtx.render; const ctxReplace = nodeCtx.replace; nodeCtx.render = function( template, config, data, container ) { if( !this.root ) { const tmp = ARUNOX.Templates.render( this.config.htmlTag, this.config, {}, this.parent?.root, this.parent ); this.root = tmp.root; this.nodes = tmp.nodes; this.nodeName = nodeName; this.root.ARX = this; if( placement === 'prepend' ) this.parent.root.prepend( this.root ); else if( placement === 'append' ) this.parent.root.append( this.root ); else if( placement === 'before' && refElement ) refElement.before( this.root ); else if( placement === 'after' && refElement ) refElement.after( this.root ); this.mounted = true; } return ctxRender.call( this, template, config, data, container || this.root ) || this; }; nodeCtx.replace = function( template, config, data ) { if( !this.root ) this.render(); return ctxReplace.call( this, template, config, data ); } nodeCtx.nodeName = nodeName; // console.log(nodeCtx); this.nodes[ nodeName ] = nodeCtx; } return this.nodes[ nodeName ]; }, closest : function( template ) { if( this.template == template || this.extends == template ) { return this; } return this.parent.closest( template ); }, dataset : function( dataset, namespace = 'arx' ) { dataset = typeof dataset === 'string' ? { [dataset] : true } : dataset; for( const [ key, value ] of Object.entries( dataset ) ) { this.root.setAttribute( `data-${namespace}-${key}`, Array.isArray( value ) ? value.join(',') : value ); } }, setUUID : function( hashid, context = '') { this.dataset({ 'uuid': btoa( [ this.template, hashid, context ].filter( Boolean ).join(':') ).replace( /[^a-zA-Z0-9]/g, '' ) }); }, _collectPreservables : function( oldCtx, newCtx ) { const preserveData = { classes : new Set(), attributes : new Map(), datasets : new Map(), structure : null }; if( oldCtx.keepOnReplace ) this._collectPreserveData( oldCtx.root, oldCtx.keepOnReplace, preserveData ); if( newCtx.takeOnReplace ) this._collectPreserveData( oldCtx.root, newCtx.takeOnReplace, preserveData ); return preserveData; }, _collectPreserveData : function( element, rules, preserveData ) { if( !element ) return; // CSS Classes if( rules.classes ) { rules.classes.forEach( pattern => { const matchingClasses = this._matchClasses( element, pattern ); matchingClasses.forEach( cls => preserveData.classes.add( cls ) ); }); } // Attributes if( rules.attributes ) { rules.attributes.forEach( pattern => { const matchingAttrs = this._matchAttributes( element, pattern ); matchingAttrs.forEach( ([name, value]) => { preserveData.attributes.set( name, value ); }); }); } // Datasets if( rules.datasets ) { rules.datasets.forEach( pattern => { const matchingData = this._matchDatasets( element, pattern ); matchingData.forEach( ([name, value]) => { preserveData.datasets.set( name, value ); }); }); } // Structure (komplexerer Fall - erstmal placeholder) if( rules.structure ) { preserveData.structure = rules.structure; } }, _applyPreserved : function( newRoot, preserveData ) { preserveData.classes.forEach( cls => { newRoot.classList.add( cls ); }); preserveData.attributes.forEach( (value, name) => { newRoot.setAttribute( name, value ); }); preserveData.datasets.forEach( (value, name) => { newRoot.setAttribute( `data-${name}`, value ); }); // Structure handling würde hier kommen (komplexer) if( preserveData.structure ) { // TODO: Implementierung für Structure-Preservation console.log( 'Structure preservation not yet implemented:', preserveData.structure ); } }, _matchClasses : function( element, pattern ) { const classes = Array.from( element.classList ); if( pattern.includes('*') ) { // Wildcard matching: "lcars-*" matches "lcars-panel", "lcars-frame", etc. const regex = new RegExp( '^' + pattern.replace(/\*/g, '.*') + '$' ); return classes.filter( cls => regex.test( cls ) ); } else { // Exact match return classes.includes( pattern ) ? [pattern] : []; } }, _matchAttributes : function( element, pattern ) { const attributes = Array.from( element.attributes ); const matching = []; attributes.forEach( attr => { if( pattern.includes('*') ) { const regex = new RegExp( '^' + pattern.replace(/\*/g, '.*') + '$' ); if( regex.test( attr.name ) ) { matching.push( [attr.name, attr.value] ); } } else if( attr.name === pattern ) { matching.push( [attr.name, attr.value] ); } }); return matching; }, _matchDatasets : function( element, pattern ) { const matching = []; // Durchsuche alle data-* Attribute Array.from( element.attributes ).forEach( attr => { if( attr.name.startsWith('data-') ) { const dataName = attr.name.substring(5); // Remove 'data-' prefix if( pattern.includes('*') ) { const regex = new RegExp( '^' + pattern.replace(/\*/g, '.*') + '$' ); if( regex.test( dataName ) ) { matching.push( [dataName, attr.value] ); } } else if( dataName === pattern ) { matching.push( [dataName, attr.value] ); } } }); return matching; }, }, register : function( template, callback, priority ) { return ARUNOX.Registry.register( this._type, template, callback, priority ); }, once : function( template, callback, priority = 10 ) { return ARUNOX.Registry.once( this._type, template, callback, priority ); }, remove : function( template, callback ) { return ARUNOX.Registry.remove( this._type , template, callback ); }, resolve : function( template ) { const mergeHooks = (baseHooks={}, childHooks={}) => { const out = {}; for( const [ key, fn ] of Object.entries( baseHooks ) ) { out[ key ] = Array.isArray( fn ) ? fn : [ fn ]; } for( const [ key, fn ] of Object.entries( childHooks ) ) { const hooks = Array.isArray( fn ) ? fn : [ fn ]; out[ key ] = out[ key ] ? [ ...out[ key ], ...hooks ] : hooks; } return out; }; const extendsSupport = function( childName ) { const base = ARUNOX.Templates.resolve( this.extends ); const merged = { ...base, ...this }; merged.hooks = mergeHooks( base.hooks, this.hooks ); // merged.extends = base; // delete merged.extends; return merged; }; let def = ARUNOX.Registry.resolve( this._type, template, extendsSupport ); if( !def ) { const source = ARUNOX.Loader.findSource(template); if( source ) { if( ARUNOX.Loader.loadSync( source.path ) ) { // Hooks laden (nur registrierte) if( source.source.hooks.has( template ) ) { ARUNOX.Loader.loadSync( source.source.hooks.get( template ) ); } // Plugin-Hooks for( const [ name, plugin ] of ARUNOX.Loader._sources.plugins ) { if( plugin.hooks.has( template ) ) { ARUNOX.Loader.loadSync( plugin.hooks.get( template ) ); } } // Theme-Hooks if( ARUNOX.Loader._sources.theme.hooks.has( template ) ) { ARUNOX.Loader.loadSync( ARUNOX.Loader._sources.theme.hooks.get( template ) ); } def = ARUNOX.Registry.resolve( this._type, template, extendsSupport ); } } } if( !def ) { if( template.match(/\//) ) return false; if( !( document.createElement( template ) instanceof HTMLUnknownElement ) ) { return { htmlTag: template }; } return false } if( typeof def === 'function' ) return { builder: def }; if( typeof def === 'object') { if( !def.htmlTag ) { if( !( document.createElement( template ) instanceof HTMLUnknownElement ) ) { return { ...def, htmlTag: template }; } throw new Error(`[ARUNOX.Templates] Invalid definition for: ${template}`); } if( def.hooks ) { // Normalisiere alle hooks zu Arrays for (const [key, fn] of Object.entries(def.hooks)) { if (!Array.isArray(fn)) { def.hooks[key] = [fn]; } } } return def; } throw new Error(`[Templates] Invalid definition for: ${template}`); }, exists : function( template ) { return ARUNOX.Registry.exists( this._type, template ); }, all : function( template ) { return [ ...ARUNOX.Registry.all( this._type, template ) ].reverse(); }, config : function( template, config, data ) { return { template, config, data }; }, render : function( template, config, data, container, parentCtx ) { if( !template ) return; const definition = this.resolve( template ); if( !definition ) return; config = Object.assign( {}, config ); const ctx = this._createContext( template, config, parentCtx || null, container, definition ); this._registerHooks( template, ctx ); config = ctx.config = ctx.filter( 'config', config, data ); data = ctx.data = ctx.filter( 'data', data, config ); const doRebuild = ctx.filter( 'shouldRebuild', null, config, data ); const doRender = ctx.filter( 'shouldRender', true, config, data ); if( doRebuild ) { const rebuildCtx = doRebuild?.ARX ? doRebuild.ARX : doRebuild; if( rebuildCtx ) { rebuildCtx.apply( 'beforeRebuild', config, data ); this._applyConfig( rebuildCtx, config ); // Config kann sich geändert haben this._renderContent( rebuildCtx, config, data ); // Neuer Content queueMicrotask( ()=> { this._collectNodes( rebuildCtx ); rebuildCtx.apply( 'afterRebuild', config, data ); }); } return rebuildCtx; } if( doRender ) { ctx.apply( 'beforeRender', config, data ); this._buildRoot( definition, ctx, config, data ); this._collectNodes( ctx ); this._renderContent( ctx, config, data ); this._applyConfig( ctx, config ); ctx.root = ctx.filter( 'wrapper', ctx.root ); this._mount( ctx, container ); queueMicrotask( ()=>{ this._collectNodes( ctx ); ctx.apply( 'afterRender', config, data ); }); return ctx; } return ctx; }, _registerHooks : function( template, ctx ) { for( const [ hookKey, hookDef ] of Object.entries( ctx.hooks ) ) { const hookArray = Array.isArray( hookDef ) ? hookDef : [ hookDef ]; hookArray.forEach( def => { // Parse Hook-Definition const hooks = this._parseHookDefinition( hookKey, def, template ); // Registriere alle geparsten Hooks hooks.forEach( hookConfig => { const originalCallback = hookConfig.callback; const wrappedCallback = function( ...args ) { this.owner = ctx; return originalCallback.call( this, ...args ); }; wrappedCallback._originalCallback = originalCallback; if( !ARUNOX.Hooks.exists( `${hookConfig.template}->${hookConfig.hook}`, originalCallback ) ) { if( hookConfig.once ) { ARUNOX.Hooks.once( `${hookConfig.template}->${hookConfig.hook}`, wrappedCallback, hookConfig.priority ); } else { ARUNOX.Hooks.register( `${hookConfig.template}->${hookConfig.hook}`, wrappedCallback, hookConfig.priority ); } } }); }); } }, _parseHookDefinition : function( hookKey, hookDef, currentTemplate ) { // Function → Standard Hook für eigenes Template if( typeof hookDef === 'function' ) { return [{ template : currentTemplate, hook : hookKey, callback : hookDef, priority : 9, once : false }]; } // Object → Vollständige Definition let templates, hookName; // Templates ermitteln if( hookDef.templates ) { templates = hookDef.templates; } else if( hookDef.template ) { templates = [ hookDef.template ]; } else if( hookKey.includes('->') ) { templates = [ hookKey.split('->')[0] ]; } else { templates = [ currentTemplate ]; } // Hook-Name ermitteln if( hookDef.hook ) { hookName = hookDef.hook; } else if( hookKey.includes('->') ) { hookName = hookKey.split('->')[1]; } else { hookName = hookKey; } // Für jedes Template eine Hook-Config erstellen return templates.map( templateName => ({ template : templateName, hook : hookName, callback : hookDef.callback, priority : hookDef.priority || 9, once : hookDef.once || false })); }, _createContext : function( template, config, parent = null, container = null, definition ) { const prototype = Object.create( this._componentProto ); const context = Object.assign( prototype, definition ); for( const key in this._componentData ) { if( typeof context[ key ] !== 'undefined' ) continue; const val = this._componentData[ key ]; context[ key ] = typeof val === 'function' ? val() : val; } if( definition.keepOnReplace || config.keepOnReplace ) { context.keepOnReplace = this._mergeReplaceRules( definition.keepOnReplace || {}, config.keepOnReplace || {} ); } if( definition.takeOnReplace || config.takeOnReplace ) { context.takeOnReplace = this._mergeReplaceRules( definition.takeOnReplace || {}, config.takeOnReplace || {} ); } context.extends = definition.extends; context.template = template; context.parent = parent; context.container = container || null; context.config = config || {}; // console.log( definition ); // console.log( template, context ); return context; }, _mergeReplaceRules : function( templateRules, configRules ) { return { classes: [ ...(templateRules.classes || []), ...(configRules.classes || []) ], attributes: [ ...(templateRules.attributes || []), ...(configRules.attributes || []) ], datasets: [ ...(templateRules.datasets || []), ...(configRules.datasets || []) ], structure: configRules.structure || templateRules.structure || null }; }, _createElement : function( tagResolver, config, data, ctx ) { // Tag ermitteln (String oder Function(config, data, ctx) -> String) const tag = ( typeof tagResolver === 'function' ) ? tagResolver( config, data, ctx ) : ( tagResolver || 'div' ); return document.createElement( String(tag || 'div') ); }, _buildRoot : function( definition, ctx, config, data ) { ctx.apply( 'buildRootBefore' ); // 1) Root vorab erstellen (Definition darf optional `tag` liefern: string | fn(cfg,data,ctx) -> string) if( !ctx.root ) { ctx.root = this._createElement( definition.htmlTag, config, data, ctx ); } // 2) Definition ausführen – nur wenn render existiert (permissive) if( typeof definition.builder === 'function' ) { const out = definition.builder.call( ctx, config, data ); if( out instanceof HTMLElement ) { ctx.root = out; } else if( typeof out === 'string' ) { const tpl = document.createElement('template'); tpl.innerHTML = out.trim(); ctx.root = tpl.content.firstElementChild || ctx.root; } } // 3) Inputs persistieren ctx.config = config; ctx.data = data; ctx.root.ARX = ctx; ctx.apply( 'buildRootAfter' ); return ctx.root; }, _collectNodes : function( ctx ) { ctx.apply( 'collectNodesBefore' ); if( !ctx.root ) return; // console.log( ctx.root.querySelectorAll('[data-arx-node]') ); ctx.root.querySelectorAll('[data-arx-node]').forEach( element => { const nodeName = element.getAttribute('data-arx-node'); if( nodeName && element.ARX && !ctx.nodes[ nodeName ] ) { element.ARX.nodeName = nodeName; ctx.nodes[ nodeName ] = element.ARX; } }); ctx.apply( 'collectNodesAfter' ); }, _applyConfig : function( ctx, config ) { if( !ctx.root || !config ) return; ctx.apply( 'applyConfigBefore', ctx.root, config ); if( config.classes ) { if( typeof config.classes === 'string' ) ctx.root.classList.add( ...config.classes.split(/\s+/).filter( Boolean ) ); else if( Array.isArray( config.classes ) ) ctx.root.classList.add( ...config.classes ); else if( typeof config.classes === 'object' ) { const { add = [], remove = [] } = config.classes; if( add.length ) ctx.root.classList.add( ...add ); if( remove.length ) ctx.root.classList.remove( ...remove ); } } if( typeof config === 'object' ) { for( const key of Object.keys( config ) ) { const match = key.match( /^on([a-z].*)/i ); if( !match ) continue; config.events ??= {}; config.events[ match[1].toLowerCase() ] = config[ key ]; } } if( typeof config.events === 'object' ) { for( const [ name, callback ] of Object.entries( config.events ) ) ctx.root.addEventListener( name, callback ); } if( typeof config.styles === 'object' ) { for (const [k, v] of Object.entries( config.styles )) ctx.root.style[k] = v; } if( typeof config.nodeName === 'string' ) { config.datasets.arx.node = config.nodeName; if( ctx.parent ) { ctx.parent.nodes[ config.nodeName ] = ctx; } } if( typeof config.datasets === 'object' ) { const applyDataset = ( obj, prefix = '' ) => { for( const [ k, v ] of Object.entries(obj)) { const key = prefix ? `${prefix}-${k}` : k; if( v && typeof v === 'object' && !Array.isArray( v ) ) { applyDataset( v, key ); // rekursiv } else { ctx.root.setAttribute( `data-${key}`, String( v ) ); } } }; applyDataset( config.datasets ); } if( typeof config.attributes === 'object' ) { for (const [k, v] of Object.entries( config.attributes )) { if (v === false || v == null) ctx.root.removeAttribute(k); else ctx.root.setAttribute(k, String(v)); } } if( typeof config.apply === 'object' ) ctx.apply( ...config.apply ); if( typeof config.innerHTML === 'string' ) ctx.root.innerHTML = config.innerHTML; if( typeof config.innerText === 'string' ) ctx.root.innerText = config.innerText; ctx.apply( 'applyConfigAfter', ctx.root, config ); }, _renderContent : function( ctx, config, data ) { const content = ctx.filter( 'content', config?.content || [], config, data ); ctx.apply( 'beforeContent', config, data ); if( typeof content === 'string' ) { ctx.root.innerText = content; } else if( Array.isArray( content ) ) { content.filter( Boolean ).forEach( item => { if( item.history ) { if( location.pathname != item.history ) { window.history.pushState({}, "", item.history ); } } ctx.render( item.template || item.generator, item.config, item.data ); }); } else if( typeof content === 'object' ) { if( content.history ) { if( location.pathname != content.history ) { window.history.pushState({}, "", content.history ); } } ctx.render( content.template || content.generator, content.config, content.data ); } ctx.apply( 'afterContent', config, data ); }, _mount : function( ctx, container ) { if( !ctx || !ctx.root ) return ctx; ctx.apply( 'mount' ); // Ziel ermitteln: explizites container-Arg → sonst ctx.container let target = ( container !== undefined ? container : ctx.container ); if( !target ) return ctx; // Selector-String erlauben if( typeof target === 'string' ) target = document.querySelector( target ); // ARX als Container erlauben else if( target && typeof target === 'object' && target.root instanceof HTMLElement ) target = target.root; if( !( target instanceof HTMLElement ) ) return ctx; target.appendChild( ctx.root ); ctx.container = target; ctx.mounted = true; ctx.apply( 'mounted' ); return ctx; }, }; /** Datei: plugins/rest/assets/js/core/utilities.js **/ /** * /arunox/core/utilities.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX ??= {}; ARUNOX.Utils = { device: { get isRotatable() { return window.orientation > -1; }, get isMobile() { return this.isRotatable || screen.availHeight <= 800 || screen.availWidth <= 800; }, get screenSize() { return { width: screen.availWidth, height: screen.availHeight }; } }, // Weitere Utils können hier hinzugefügt werden dom: { ready(callback) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', callback); } else { callback(); } } } }; /** Datei: plugins/rest/assets/js/core/prototypes.js **/ /** * /arunox/core/prototypes.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ Object.defineProperty( DOMTokenList.prototype, 'removeRexEx', { value: function( regex ) { if( !( regex instanceof RegExp ) ) { throw new TypeError('DOMTokenList.removeRegex erwartet ein RegExp-Objekt.'); } const toRemove = [ ...this ].filter( cls => regex.test( cls ) ); if( toRemove.length ) this.remove( ...toRemove ); }, enumerable : false, writable : true, configurable: true }); Object.defineProperty( DOMTokenList.prototype, 'match', { value: function( regex, returnMatchIndex = 1 ) { if( !( regex instanceof RegExp ) ) { throw new TypeError('DOMTokenList.match erwartet ein RegExp-Objekt.'); } return [ ...this ] .map( cls => regex.exec( cls ) ) .filter( Boolean ) .map( m => m[ returnMatchIndex ] ); }, enumerable : false, writable : true, configurable: true }); Object.defineProperty( Object.prototype, 'assignRecursive', { value: function( target, ...sources ) { for( const source of sources ) { for( const [ key, item ] of Object.entries( source ) ) { if( item === null ) { delete target[ key ]; continue; } if( Array.isArray( item ) ) { target[ key ] = [].concat( item, target[ key ] || [] ); continue; } if( typeof item === 'object' && item !== null ) { if( typeof target[ key ] !== 'object' || target[ key ] === null ) { target[ key ] = target[ key ] === undefined ? {} : { [key] : target[ key ] }; } target[ key ] = Object.assignRecursive( target[ key ] || {}, item ); continue; } target[ key ] = item; } } return target; }, enumerable : false, writable : true, configurable: true }); Object.defineProperty( String.prototype, 'toUpperCaseFirst', { value: function() { return String( this ).charAt(0).toUpperCase() + String( this ).slice(1); }, enumerable : false, writable : true, configurable: true }); /** Datei: plugins/rest/assets/js/core/loader.js **/ /** * /arunox/core/loader.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX ??= {}; ARUNOX.Loader = { _loaded : new Set(), _loading : new Map(), _sources : { core : { basePath: '/plugins/rest/assets/js', templates: new Map(), hooks: new Map() }, theme : { basePath: '/themes/lcars/assets/js', templates: new Map(), hooks: new Map() }, plugins : new Map() }, load : async function( path, options = {} ) { const { force = false, cache = true, type = 'module' } = options; if( !force && this._loaded.has( path ) ) return Promise.resolve(); // Already loaded? if( this._loading.has( path ) ) return this._loading.get( path ); // Currently loading? const loadPromise = this._loadModule( path, type, cache ); this._loading.set( path, loadPromise ); try { await loadPromise; this._loaded.add( path ); } catch( error ) { console.error( `Failed to load module: ${path}`, error ); throw error; } finally { this._loading.delete( path ); } return loadPromise; }, loadAll : async function( modules ) { const promises = modules.map( module => { if( typeof module === 'string' ) { return this.load( module ); } return this.load( module.path, module.options || {} ); }); return Promise.all( promises ); }, loadIf : async function( condition, path, options = {} ) { if( typeof condition === 'function' ) { condition = condition(); } if( condition ) return this.load( path, options ); return Promise.resolve(); }, loadSync : function( path ) { if( this._loaded.has( path ) || this._loaded.has(path + ':404' ) ) return this._loaded.has( path ); try { const xhr = new XMLHttpRequest(); xhr.open('GET', path, false); // SYNCHRON! xhr.send(); if( xhr.status === 200 ) { // Extrahiere und entferne import Statements const importRegex = /import\s+['"](.*?)['"]\s*;?/g; const imports = []; let code = xhr.responseText; let match; while ((match = importRegex.exec(code)) !== null) { imports.push(match[1]); } // Entferne imports aus dem Code code = code.replace(importRegex, ''); // Lade alle imports zuerst (rekursiv) imports.forEach(importPath => { this.loadSync(importPath); }); const script = document.createElement('script'); // script.type = 'module'; script.textContent = code; document.head.appendChild( script ); document.head.removeChild( script ); this._loaded.add( path ); return true; } if( xhr.status === 404 ) { // 404 merken, damit wir es nicht nochmal versuchen this._loaded.add( path + ':404' ); return false; } return false; } catch( e ) { return false; } }, isLoaded : function( path ) { return this._loaded.has( path ); }, unload : function( path ) { this._loaded.delete( path ); }, registerSource : function( type, name, config ) { if( type === 'core' ) { this._sources.core.basePath = config.basePath || this._sources.core.basePath; this._sources.core.templates = this._parseTemplates( config.templates, this._sources.core.basePath ); this._sources.core.hooks = this._parseHooks( config.hooks, this._sources.core.basePath ); } else if( type === 'theme' ) { this._sources.theme.basePath = config.basePath || this._sources.theme.basePath; this._sources.theme.templates = this._parseTemplates( config.templates, this._sources.theme.basePath ); this._sources.theme.hooks = this._parseHooks( config.hooks, this._sources.theme.basePath ); } else if( type === 'plugin' ) { this._sources.plugins.set( name, { basePath : config.basePath, templates : this._parseTemplates( config.templates, config.basePath ), hooks : this._parseHooks( config.hooks, config.basePath ) }); } }, findSource : function( template ) { // Theme (höchste Priorität) if( this._sources.theme.templates.has( template ) ) { return { type : 'theme', path : this._sources.theme.templates.get( template ), source : this._sources.theme }; } // Plugins for( const [ name, plugin ] of this._sources.plugins ) { if( plugin.templates.has( template ) ) { return { type : 'plugin', name, path : plugin.templates.get( template ), source : plugin }; } } // Core (Fallback) if( this._sources.core.templates.has( template ) ) { return { type : 'core', path : this._sources.core.templates.get( template), source : this._sources.core }; } return null; }, _loadModule : function( path, type, cache ) { return new Promise(( resolve, reject ) => { const script = document.createElement('script'); script.onload = () => { document.head.removeChild( script ); resolve(); }; script.onerror = () => { document.head.removeChild( script ); reject( new Error( `Failed to load script: ${path}` ) ); }; if( type === 'module' ) { script.type = 'module'; } if( !cache ) { const url = new URL( path, window.location.origin ); url.searchParams.set( '_t', Date.now() ); script.src = url.toString(); } else { script.src = path; } document.head.appendChild( script ); }); }, _parseTemplates : function( templates, basePath ) { if( Array.isArray( templates ) ) { // Einfache Liste → nutzt basePath const map = new Map(); templates.forEach( t => map.set( t, `${basePath}/components/${t}.js` ) ); return map; } else if( typeof templates === 'object' ) { // Explizite Pfade return new Map( Object.entries( templates ) ); } return new Map(); }, _parseHooks : function( hooks, basePath ) { if( Array.isArray( hooks ) ) { const map = new Map(); hooks.forEach( h => map.set( h, `${basePath}/hooks/${h}.js` ) ); return map; } else if( typeof hooks === 'object' ) { return new Map( Object.entries( hooks ) ); } return new Map(); }, }; /** Datei: plugins/rest/assets/js/core/hooks.js **/ /** * /arunox/core/hooks.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX = window.ARUNOX || {}; ARUNOX.Hooks = { _type : 'hooks', register : function( name, callback, priority ) { return ARUNOX.Registry.register( this._type, name, callback, priority ); }, once : function( name, callback, priority = 10 ) { return ARUNOX.Registry.once( this._type, name, callback, priority ); }, remove : function( name, callback ) { return ARUNOX.Registry.remove( this._type , name, callback ); }, exists : function( name, callback ) { return ARUNOX.Registry.exists( this._type, name, callback ); }, resolve : function( name ) { return ARUNOX.Registry.resolve( this._type, name ); }, all : function( name ) { return [ ...ARUNOX.Registry.all( this._type, name ) ].reverse(); }, filter : function( name, value, ...args ) { let hookNames = Array.isArray( name ) ? name : name.split(' '); let callbacks = hookNames.map( n => ARUNOX.Hooks.all( n ) ).flat(2).filter(Boolean); let stopped = false; let context = { stop : function() { stopped = true; }, instance : this }; // console.log( hookNames ); if( this.hooks?.hooks ) { callbacks = callbacks.concat( hookNames.map( n => this.hooks.hooks[ n ] ).filter(Boolean).flat() ); } // if( callbacks.length ) console.log( callbacks, context.instance ); for( const fn of callbacks ) { if( stopped ) break; if( fn.ARX?.once && fn.ARX?.done ) continue; value = fn.call( context, value, ...args ); if( fn.ARX?.once ) fn.ARX.done = true; } return value; }, apply : function( name, ...args ) { let hookNames = Array.isArray( name ) ? name : name.split(' '); let callbacks = hookNames.map( n => ARUNOX.Hooks.all( n ) ).flat(2).filter(Boolean); let stopped = false; let context = { stop : function() { stopped = true; }, instance : this }; // console.log( hookNames ); if( this.hooks?.hooks ) { callbacks = callbacks.concat( hookNames.map( n => this.hooks.hooks[ n ] ).filter(Boolean).flat() ); } for( const fn of callbacks ) { if( stopped ) break; if( fn.ARX?.once && fn.ARX?.done ) continue; fn.call( context, ...args ); if( fn.ARX?.once ) fn.ARX.done = true; } }, Instance : class { constructor( owner, hooks ) { this.owner = owner; this.hooks = hooks || {}; } _hooks( name ) { return [...new Set([ name, this.owner.constructor?.extends ? `${this.owner.constructor.extends.type}->${name}` : null, `${this.owner.type}->${name}`, this.owner.constructor?.extends ? `${this.owner.constructor.extends.hook}->${name}` : null, `${this.owner.hook}->${name}`, ].filter(Boolean))]; } register( name, callback ) { this.hooks[ name ] ??= []; this.hooks[ name ].push( callback ); return this; } exists( name ) { return ARUNOX.Hooks.exists( name ); } filter( name, value, ...args ) { return ARUNOX.Hooks.filter.call( this.owner, this._hooks( name ), value, ...args ); } apply( name, ...args ) { ARUNOX.Hooks.apply.call( this.owner, this._hooks( name ), ...args ); } } }; /** Datei: plugins/rest/assets/js/core/router.js **/ /** * /arunox/core/router.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX = ARUNOX || {}; ARUNOX.Router = new ( class { constructor() { this._routes = {}; this._cache = new Map(); this._lastCacheKey = null; this._isReloaded = false; this._CACHE_TIMEOUT = 500; } async handle( config, sendData = {}, parseData = {}, mode = 'data' ) { const debug = false; if( !config ) { console.warn("⚠️ Ungültige Konfiguration für handleAction()."); return; } if( !config.resolved ) { config = this.parseRoute( config, parseData, true, mode ); } if( debug ) console.groupCollapsed( config.resolved ); // console.log( config ); let response = { click : [], route : [], dialog : [], updater : [], wrapper : [], template : [], generator : [], javascript : [], }; for( const item of await this.fetch( config, sendData, {}, mode ) ) { response[ item.type ].push( item ); } // console.log( config, response ); ARUNOX.Router.history = config.history; if( response.dialog ) { for( const item of response.dialog ) { let dialog = null; if( item.name === '#ALL' ) { document.querySelectorAll('dialog').forEach( dialog => dialog.closeDialog() ); continue; } if( dialog = document.querySelector(`dialog[data-id="${item.name}"]`) ) { dialog.closeDialog(); continue; } if( dialog = document.querySelector('dialog') ) { dialog.closeDialog(); } } } if( response.route.length ) { for( const item of response.route ) { await ARUNOX.Router.handle( item, {}, {}, 'rest' ); } ARUNOX.Router.history = config.history; } if( response.template.length && !response.route.length ) { document.body.innerHTML = ''; ARUNOX.Router.template = ARUNOX.Templates.render( response.template[0].template, response.template[0], Object.assign( {}, config ), document.body ); } // if( response.template.length && !response.route.length ) { // document.body.innerHTML = ''; // template = ARUNOX.Theme.template.render( response.template[0].template, Object.assign( {}, config ), response.template[0], document.body ); // } if( response.wrapper.length && !response.route.length ) { for( const item of response.wrapper ) { ARUNOX.Router.template.nodes.content.render( item.generator, item.config, item.list || item.data || {} ); if( item.history ) { if( location.pathname != item.history ) { window.history.pushState({}, "", item.history ); } } } } // if( response.generator.length && !response.route.length ) { // for( const item of response.generator ) { // ARUNOX.Theme.generator.render( item.generator, config, item, template ); // } // } // if( response.updater.length && !response.route.length ) { for( const item of response.updater ) { document.querySelectorAll(`[data-arx-uuid="${item.updater}"]`).forEach( element => element.ARX.apply( 'update', item.data, item.status ) ); } } // // if( response.click.length ) { // for( const item of response.click ) { // const element = document.querySelector(`[data-uuid="${item.parent}"] ${item.selector ?? ''}` ); // // element.click(); // } // } if( response.javascript.length ) { for( const item of response.javascript ) { eval( item.script ); } } if( debug ) console.groupEnd(); ARUNOX.Router.history = config.history; } async routes() { if( !this._isLoaded() ) { await this._loadList(); } return this._routes; } async fetch(config, params = {}, options = {}, mode = 'data') { try { let cached = this.getCache(!config, {}, mode); if (cached !== false) return cached; cached = this.getCache(config, params, mode); if (cached !== false) return cached; if (!config.resolved) config = this.parseRoute(config, params, true, mode); if (!config.resolved) throw new Error(`❌ Fehlerhafte API-URL: ${config.resloved.data}`); const cacheKey = `${config.method}:${config.cachekey}`; const defaultOptions = { method: config.method, headers: {}, ...options }; if (config.method === "GET" || config.method === "DELETE") { const queryString = new URLSearchParams(params).toString(); config.resolved += queryString ? `?${queryString}` : ""; } else { if (params instanceof FormData) { defaultOptions.body = params; } else { defaultOptions.body = JSON.stringify(params); defaultOptions.headers["Content-Type"] = "application/json"; } } const response = await fetch(config.resolved, defaultOptions); const data = await response.json(); const fullcache = data.caching || false; delete data.caching; this._cache.set(cacheKey, { response: data, fullcache: false, timestamp: Date.now() }); this._lastCacheKey = cacheKey; return data; } catch (error) { console.error('❌ Fehler beim Abrufen der API-Daten:', error); return null; } } async fetchData(config, params = {}, options = {}) { const response = await this.fetch(config, params, options); return response.data; } async fetchList(config, params = {}, options = {}) { const response = await this.fetch(config, params, options); return response.list; } async fetchConfig(config, params = {}, options = {}) { const response = await this.fetch(config, params, options); return response.config; } async fetchGenerator(config, params = {}, options = {}) { const response = await this.fetch(config, params, options); return response.generator; } async fetchStream(config, params = {}, options = {}, onChunk) { try { if (!config.resolved) config = this.parseRoute(config, params, true); if (!config.resolved) throw new Error(`❌ Fehlerhafte API-URL: ${config.resloved.data}`); const defaultOptions = { method: config.method, headers: {}, ...options }; const response = await fetch(config.resolved, defaultOptions); if (!response.body) return; const decoder = new TextDecoder(); const reader = response.body.getReader(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; if (onChunk) { buffer += decoder.decode(value, { stream: true }); let lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (!line.trim()) continue; try { onChunk(JSON.parse(line)); } catch { onChunk(line); } } } } if (onChunk && buffer.trim()) { try { onChunk(JSON.parse(buffer)); } catch { onChunk(buffer); } } } catch (error) { console.error('❌ Fehler beim Abrufen des API-Stream:', error); return null; } } async find(path, includeHidden = false) { await this._loadList(); return this._routes.find(route => { if (!includeHidden && route.visibility === "hidden") return false; const routePattern = new RegExp(`^${route.path.replace(/{\w+}/g, "([^/]+)")}$`); return routePattern.test(path); }); } getCache(config, params = {}, mode = 'data') { if (config === true && this._lastCacheKey) { const cached = this._cache.get(this._lastCacheKey); if (cached && Date.now() - cached.timestamp < this._CACHE_TIMEOUT) { cached.timestamp = Date.now(); return cached.response; } return null; } if (typeof config === 'object' || typeof config === 'string') { if (!config.resolved) config = this.parseRoute(config, params, {}, mode); const cacheKey = `${config.method}:${config.cachekey}`; const cached = this._cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this._CACHE_TIMEOUT) { this._lastCacheKey = cacheKey; return cached.response; } if (cached && cached.fullcache && location.pathname == config.cachekey) { return cached.response; } } return false; } getBreadcrumbSegments(path) { let segments = path.split('/').filter(Boolean); let breadcrumbs = []; let currentPath = ""; if (!segments.length) { segments = ['']; } segments.forEach(segment => { currentPath += `/${segment}`; const route = this._routes.find(r => r.path && new RegExp(`^${r.path.replace(/{\w+}/g, "([^/]+)")}$`).test(currentPath)); const group = this._routes.find(r => r.name && (new RegExp(`^${r.name.replace(/{\w+}/g, "([^/]+)")}$`).test(currentPath) || r.name == segment)); if (route) { breadcrumbs.push({ label: route.breadcrumb || route.label || segment, path: currentPath, }); } else if (group) { breadcrumbs.push({ label: group.breadcrumb || group.label || segment, path: false, }); } }); return breadcrumbs; } getDataRoute(config, params = {}, queryIncludes = true) { config = this.parseRoute(config ?? location.pathname, params, queryIncludes, 'data'); return config.resolved; } parseRoute( config, params = {}, queryIncludes = true, mode = 'data' ) { let route = typeof config === "string" ? config : config.route; let query = new URLSearchParams( config?.query || [] ); let connector = /\?/.test( route ) ? '&' : '?'; let resolved = null; let cachekey = null; if( query.size ) route += connector + query; if( !queryIncludes ) route = route.replace(/\?.*/, ''); switch (mode) { case 'rest': resolved = this._resolveApiUrl(route, params, '/rest'); cachekey = this._resolveApiUrl(route, params, '/rest', false); break; case 'data': resolved = this._resolveApiUrl(route, params, true); cachekey = this._resolveApiUrl(route, params, false, false); break; } return { route : route, resolved: resolved, cachekey: cachekey, history : this._resolveApiUrl(route, params, false), method : config?.method || "GET", query : config?.query || {}, }; } parseUrl(route, params) { const missingKeys = []; const resolvedUrl = route.replace(/{(\w+)}/g, (_, key) => { if (params[key] === 'undefined') { missingKeys.push(key); return `{${key}}`; } return params[key]; }).replace(/\/$/, ''); if (missingKeys.length > 0) { console.error(`Nicht ersetzte Platzhalter: ${missingKeys.join(', ')}`, `URL: ${route}`); return null; } return resolvedUrl; } _isLoaded() { return !!Object.entries( this._routes ).length; } async _loadList() { this._routes = await this.fetchList("/rest/routes"); } _resolveApiUrl(route, params = {}, prefix = true, hash = true) { const API_PREFIX = '/api/v1'; if (typeof prefix === 'string') { route = this._stripApiPrefix(route, API_PREFIX); route = route.startsWith(prefix) ? route : `${prefix}${route}`; } const mergedParams = params; const prefixedUrl = route.startsWith(API_PREFIX) ? route : `${API_PREFIX}${route}`; const resolvedUrl = this.parseUrl(prefixedUrl, params); if (!hash) { const withoutHash = resolvedUrl.replace(/#.*/, ''); return prefix ? withoutHash : this._stripApiPrefix(withoutHash, API_PREFIX); } return prefix ? resolvedUrl : this._stripApiPrefix(resolvedUrl, API_PREFIX); } _stripApiPrefix(route, api_prefix = '/api/v1') { if (route.startsWith(api_prefix)) { return route.replace(api_prefix, ''); } return route; } })(); ARUNOX.Router.history = null; /** Datei: plugins/rest/assets/js/core/registry.js **/ /** * /arunox/core/regestry.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX = window.ARUNOX || {}; ARUNOX.Registry = { _store : {}, register : function( type, name, callback, priority = 10, once = false ) { if( !this._store[ type ] ) this._store[ type ] = {}; if( !this._store[ type ][ name ]) this._store[ type ][ name ] = []; // callback.name = name; callback.once = once; callback.priority = priority; this._store[ type ][ name ].unshift( callback ); this._store[ type ][ name ].sort( ( a, b ) => b.priority - a.priority ); }, once : function( type, name, callback, priority = 10 ) { return this.register( type, name, callback, priority, true ); }, remove : function( type, name, callback ) { const arr = this._store[ type ]?.[ name ]; if (!arr) return; this._store[type][name] = arr.filter(e => e !== callback); }, resolve : function( type, name, extendsSupportCallback ) { const resolved = this._store[ type ]?.[ name ]?.[0]; if( !resolved ) return null; if( resolved?.extends && typeof resolved?.extends === 'string' ) { return extendsSupportCallback.call( resolved, name ); } return resolved; }, exists : function( type, name, callback, originCallback ) { if( this._store[ type ]?.[ name ] ) { return !!( this._store[ type ]?.[ name ]?.filter( item => item == callback || item._originalCallback == callback ).length ); } return false; }, all : function( type, name ) { if( !type ) return this._store; if( !name ) return Object.keys(this._store[ type ] || {} ); return this._store[ type ]?.[ name ] || []; }, template : { _type : 'template', register : function( name, callback, priority ) { return ARUNOX.Registry.register( this._type, name, callback, priority); }, remove : function( name, callback ) { return ARUNOX.Registry.remove( this._type, name, callback ); }, resolve : function( name ) { return ARUNOX.Registry.resolve( this._type, name ); }, exists : function( name ) { return ARUNOX.Registry.exists( this._type, name ); }, all : function( name ) { return ARUNOX.Registry.all( this._type, name ); }, }, wrapper : { _type : 'wrapper', register : function( name, callback, priority ) { return ARUNOX.Registry.register( this._type, name, callback, priority); }, remove : function( name, callback ) { return ARUNOX.Registry.remove( this._type, name, callback ); }, resolve : function( name ) { return ARUNOX.Registry.resolve( this._type, name ); }, exists : function( name ) { return ARUNOX.Registry.exists( this._type, name ); }, all : function( name ) { return ARUNOX.Registry.all( this._type, name ); }, }, generator : { _type : 'generator', register : function( name, callback, priority ) { return ARUNOX.Registry.register( this._type, name, callback, priority); }, remove : function( name, callback ) { return ARUNOX.Registry.remove( this._type, name, callback ); }, resolve : function( name ) { return ARUNOX.Registry.resolve( this._type, name ); }, exists : function( name ) { return ARUNOX.Registry.exists( this._type, name ); }, all : function( name ) { return ARUNOX.Registry.all( this._type, name ); }, }, htmlTag : { _type : 'htmltag', register : function( name, callback, priority ) { return ARUNOX.Registry.register( this._type, name, callback, priority); }, remove : function( name, callback ) { return ARUNOX.Registry.remove( this._type, name, callback ); }, resolve : function( name ) { return ARUNOX.Registry.resolve( this._type, name ); }, exists : function( name ) { return ARUNOX.Registry.exists( this._type, name ); }, all : function( name ) { return ARUNOX.Registry.all( this._type, name ); }, }, _mergeHooks : function( baseHooks = {}, childHooks = {} ) { const merged = {...baseHooks}; for (const [name, hookFn] of Object.entries(childHooks)) { if (name.startsWith('!')) { // explizites Überschreiben: "!config" const realName = name.slice(1); merged[realName] = hookFn; } else { // beide behalten → Liste draus machen if (merged[name]) { merged[name] = [].concat(merged[name], hookFn); } else { merged[name] = hookFn; } } } return merged; } };