diff --git a/VERSION b/VERSION index 5f1dd0a..8347ddf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.15.1 +v0.16.0-dev diff --git a/asset/css/action-link-and-button-link.less b/asset/css/action-link-and-button-link.less new file mode 100644 index 0000000..d654f66 --- /dev/null +++ b/asset/css/action-link-and-button-link.less @@ -0,0 +1,26 @@ +.action-link { + color: var(--control-color, @control-color); +} + +.button-link { + .action-link(); + .rounded-corners(3px); + + background: var(--default-input-bg, @default-input-bg); + display: inline-block; + padding: 0.25em 0.5em; + + &:hover { + background: var(--default-input-hover-bg, @default-input-hover-bg); + text-decoration: none; + } + + &[aria-disabled="true"] { + background: none; + border: 1px solid var(--control-disabled-color, @control-disabled-color); + color: var(--control-disabled-color, @control-disabled-color); + cursor: not-allowed; + + .user-select(none); + } +} diff --git a/asset/css/balls.less b/asset/css/balls.less index f3afae3..70ef391 100644 --- a/asset/css/balls.less +++ b/asset/css/balls.less @@ -2,18 +2,27 @@ .ball { border-radius: 50%; - display: inline-block; - text-align: center; + display: inline-flex; + align-items: center; + justify-content: center; } .ball-size-xs { height: 1/3em; width: 1/3em; + + i.icon, span { + display: none; + } } .ball-size-s { height: 0.5em; width: 0.5em; + + i.icon, span { + display: none; + } } .ball-size-m { @@ -21,10 +30,14 @@ width: 0.75em; line-height: 0; - i.icon:before { + i.icon::before { font-size: .75 - @ball-pad * 2; line-height: 1em; } + + span { + display: none; + } } .ball-size-ml { @@ -35,11 +48,15 @@ i.icon { line-height: 0.3; - &:before { + &::before { font-size: 0.8 - @ball-pad * 2; line-height: 1 - @ball-pad * 2; } } + + span { + display: none; + } } .ball-size-l { @@ -47,7 +64,7 @@ width: 1.5em; line-height: 1em; - i.icon:before { + i.icon::before, span { font-size: 1 - @ball-pad * 2; line-height: 1.5 - @ball-pad * 2; } @@ -57,7 +74,7 @@ width: 2em; height: 2em; - i.icon:before { + i.icon::before, span { line-height: 2 - @ball-pad * 2; } } @@ -75,7 +92,7 @@ .state-ball { .ball(); - &.state-pending:not(.ball-size-l):not(.ball-size-xl) { + &.state-pending:not(.ball-size-l, .ball-size-xl) { .ball-solid(var(--state-pending, @state-pending)); } @@ -84,7 +101,7 @@ .ball-outline(var(--state-pending, @state-pending)); } - &.state-up:not(.ball-size-l):not(.ball-size-xl) { + &.state-up:not(.ball-size-l, .ball-size-xl) { .ball-solid(var(--state-up, @state-up)); } @@ -97,7 +114,7 @@ .ball-solid(var(--state-down, @state-down)); } - &.state-ok:not(.ball-size-l):not(.ball-size-xl) { + &.state-ok:not(.ball-size-l, .ball-size-xl) { .ball-solid(var(--state-ok, @state-ok)); } @@ -124,7 +141,6 @@ i.icon { text-align: center; - display: block; &::before { margin-right: 0; @@ -133,13 +149,13 @@ // Specific icon styles &.ball-size-l i { - &.fa-sitemap:before { + &.fa-sitemap::before { font-size: 8px; // px to ignore browser min font-size } } &.ball-size-xl i { - &.fa-sitemap:before { + &.fa-sitemap::before { font-size: .857em; line-height: (2 - @ball-pad * 2) / .857; } diff --git a/asset/css/compat.less b/asset/css/compat.less index 1188c7e..c4772ac 100644 --- a/asset/css/compat.less +++ b/asset/css/compat.less @@ -2,10 +2,15 @@ .icinga-controls { .uploaded-files { - background-color: @default-input-bg; + background-color: var(--default-input-bg, @default-input-bg); } } +// Icinga Web < 2.13 +.button-link[aria-disabled="true"]:hover { + background: none; +} + form.icinga-form { .uploaded-files { flex: 1 1 auto; @@ -20,6 +25,14 @@ form.icinga-form { } } +.icinga-controls { + .required-hint { + font-weight: bold; + color: var(--default-text-color-light, @default-text-color-light); + } +} + + // Button styles // The `form` selector is only required to overrule the hover effect applied by Icinga Web. @@ -86,3 +99,51 @@ form.icinga-form .control-group { } } } + +// suggestion-element style +form.icinga-form .suggestion-element-group { + flex: 1 1 auto; + + .suggestion-element { + border-radius: 0 0.25em 0.25em 0; + } +} + +.module-icingadb { + // Icinga DB Web (legacy) table header layout (e.g. in group details) + > .controls { + > .table-row { + display: flex; + gap: .5em; + + > .col.title { + margin-right: auto; + } + } + } + + // Icinga DB Web (legacy) object grid layout + > .content > .item-table.group-grid:has(.col.title) { + grid-template-columns: repeat(auto-fit, 15em) !important; + + > .group-grid-cell { + display: revert; + + &::before, &::after { + display: none !important; + } + + > .col.title { + border: none; + + > .column-content { + overflow: hidden; + + > * { + .text-ellipsis(); + } + } + } + } + } +} diff --git a/asset/css/controls.less b/asset/css/controls.less index 1bccbd8..ed27794 100644 --- a/asset/css/controls.less +++ b/asset/css/controls.less @@ -16,7 +16,7 @@ display: block; } - i:before { + i::before { margin: 0; } } @@ -52,7 +52,7 @@ } } - i.icon:before { + i.icon::before { color: inherit; } } @@ -69,7 +69,7 @@ height: 100%; } - i.icon:before { + i.icon::before { margin-right: 0; } } @@ -154,11 +154,6 @@ } > .search-controls > .search-bar .filter-input-area { - label { - &::after, - input { - padding: 0 .5em; - } - } + --term-padding-v: 0px; } } diff --git a/asset/css/empty-state.less b/asset/css/empty-state.less index 6291055..75879d8 100644 --- a/asset/css/empty-state.less +++ b/asset/css/empty-state.less @@ -1,5 +1,5 @@ .empty-state { - color: @empty-state-color; + color: var(--empty-state-color, @empty-state-color); } .empty-state-bar { @@ -7,5 +7,6 @@ text-align: center; .rounded-corners(); - background-color: @empty-state-bar-bg; + background-color: var(--empty-state-bar-bg, @empty-state-bar-bg); + color: var(--default-text-color, @default-text-color); } diff --git a/asset/css/icinga-icons.less b/asset/css/icinga-icons.less index 5b631f9..5888217 100644 --- a/asset/css/icinga-icons.less +++ b/asset/css/icinga-icons.less @@ -1,5 +1,5 @@ @font-face { - font-family: 'Icinga-Icons'; + font-family: Icinga-Icons; src: url('@{iplWebAssets}/font/icinga-icons/fonts/Icinga-Icons.ttf') format('truetype'), url('@{iplWebAssets}/font/icinga-icons/fonts/Icinga-Icons.woff') format('woff'), url('@{iplWebAssets}/font/icinga-icons/fonts/Icinga-Icons.svg') format('svg'); @@ -8,9 +8,9 @@ font-display: block; } -[class^="iicon-"]:before, [class*=" iicon-"]:before { +[class^="iicon-"]::before, [class*=" iicon-"]::before { /* use !important to prevent issues with browser extensions that change fonts */ - font-family: 'Icinga-Icons'; + font-family: Icinga-Icons; speak: none; font-style: normal; font-weight: normal; @@ -23,42 +23,42 @@ -moz-osx-font-smoothing: grayscale; } -.iicon-certificate:before { +.iicon-certificate::before { content: "\e906"; } -.iicon-filter-check-circle:before { +.iicon-filter-check-circle::before { content: "\e90b"; } -.iicon-ca-check-circle:before { +.iicon-ca-check-circle::before { content: "\e908"; } -.iicon-refresh-cert:before { +.iicon-refresh-cert::before { content: "\e909"; } -.iicon-th-list:before { +.iicon-th-list::before { content: "\e90a"; } -.iicon-icinga:before { +.iicon-icinga::before { content: "\e907"; } -.iicon-minimal:before, -.iicon-list-view-minimal:before { +.iicon-minimal::before, +.iicon-list-view-minimal::before { content: "\e900"; } -.iicon-detailed:before, -.iicon-list-view-detailed:before { +.iicon-detailed::before, +.iicon-list-view-detailed::before { content: "\e901"; } -.iicon-default:before, -.iicon-list-view-default:before { +.iicon-default::before, +.iicon-list-view-default::before { content: "\e902"; } -.iicon-grid:before { +.iicon-grid::before { content: "\e903"; } -.iicon-bracket-open:before { +.iicon-bracket-open::before { content: "\e904"; } -.iicon-bracket-close:before { +.iicon-bracket-close::before { content: "\e905"; } diff --git a/asset/css/item-layout.less b/asset/css/item-layout.less new file mode 100644 index 0000000..0d5ef68 --- /dev/null +++ b/asset/css/item-layout.less @@ -0,0 +1,177 @@ +// Layout + +.item-layout { + // Note that mode specific rules are as strict as possible to avoid conflicts with nested layouts. + // Consider an item which contains another item in a different layout mode. Coincidentally, this is + // already the case in Icinga DB Web with the last comment flyout. + + .flowing-content(@layout) when (@layout = "default") { + display: inline-flex; + align-items: baseline; + white-space: nowrap; + min-width: 0; + column-gap: .28125em; // calculated width + + > .ellipsize, > .subject { // .subject is compat only, Icinga DB Web used it thoroughly + .text-ellipsis(); + } + } + .flowing-content(@layout) when (@layout = "detailed") { + display: inline-flex; + align-items: baseline; + flex-wrap: wrap; + column-gap: .28125em; // calculated width + } + + display: flex; + + .visual { + display: flex; + flex-direction: column; + + width: auto; + padding: .25em 0; + margin-right: 1em; + + > i.icon { + font-size: 1.5em; + + &::before { + margin-right: 0; + } + } + } + + .main { + flex: 1 1 auto; + padding: .25em 0; + width: 0; + } + + header { + display: flex; + align-items: baseline; + justify-content: space-between; + + > .extended-info { + flex-shrink: 0; + } + } + + .caption { + p { + display: inline-block; + } + + img { + max-height: 1em; + } + } + + footer { + display: flex; + justify-content: space-between; + + padding-top: .5em; + } + + .title { + margin-right: 1em; + } + + &.minimal-item-layout > .main > header { + max-width: 100%; + height: 1.5em; + + > .caption { + flex: 1 1 auto; + height: 1.5em; + width: 0; + + &:not(:empty) { + margin-right: 1em; + } + + .text-ellipsis(); + } + } + + &.default-item-layout > .main { + > header { + height: 1.5em; + + > .title { + .flowing-content("default"); + } + + > .extended-info { + .flowing-content("default"); + } + } + + > .caption { + height: 1.5em; + + .text-ellipsis(); + } + } + + &.detailed-item-layout > .main { + > header { + > .title { + .flowing-content("detailed"); + + word-break: break-word; + hyphens: auto; + } + + > .extended-info { + .flowing-content("detailed"); + } + } + + > .caption { + display: block; + overflow: hidden; + position: relative; + + .line-clamp(5); + } + } +} + +// Style + +.item-layout { + color: var(--default-text-color-light, @default-text-color-light); + + .caption { + i { + opacity: 0.8; + } + + a { + color: var(--default-text-color, @default-text-color); + } + } + + .title { + .subject { + color: var(--default-text-color, @default-text-color); + } + + a { + color: var(--default-text-color, @default-text-color); + font-weight: bold; + + &:hover { + color: var(--link-hover-color, @link-hover-color); + text-decoration: none; + + .subject { + color: var(--link-hover-color, @link-hover-color); + } + } + } + } +} diff --git a/asset/css/list/item-list.less b/asset/css/list/item-list.less index c5c0bd2..56aba0d 100644 --- a/asset/css/list/item-list.less +++ b/asset/css/list/item-list.less @@ -4,82 +4,35 @@ list-style-type: none; } +.content:has(> .item-list) > .item-list > .empty-state { + .empty-state-bar(); +} + // Layout .item-list { margin: 0; padding: 0; - .list-item { - display: flex; - - .main { - flex: 1 1 auto; - padding: .5em 0; - width: 0; - margin-left: .5em; - } - - .visual { - display: flex; - align-items: center; - flex-direction: column; - } - - .caption { - height: 3em; - text-overflow: ellipsis; - overflow: hidden; - - .line-clamp(); - - img { - max-height: 1em; - } - } - - header { - display: flex; - align-items: flex-start; - justify-content: space-between; - } - - footer { - display: flex; - justify-content: space-between; - } - } - > .empty-state-bar { margin: 0 1em; } } -.item-list.default-layout .list-item { - .title { - display: inline-flex; - align-items: baseline; - white-space: nowrap; - min-width: 0; - - > * { - margin: 0 .28125em; // 0 calculated width - - &:first-child { - margin-left: 0; - } - - &:last-child { - margin-right: 0; - } - } - - .subject { - .text-ellipsis(); - } - } -} - .controls .list-item:not(:last-child) { margin-bottom: .5em; } + +:not(.dashboard) > .container > .content:has(> .item-list), // compat only, for Icinga Web (See #286) +.content:has(> .item-list) { + padding-left: 0; + padding-right: 0; + + > .item-list > .list-item { + padding-left: 1em; + padding-right: 1em; + } + > .item-list > .empty-state { + margin: 0 1em; + } +} diff --git a/asset/css/list/item-table.less b/asset/css/list/item-table.less index bd87503..d075702 100644 --- a/asset/css/list/item-table.less +++ b/asset/css/list/item-table.less @@ -4,79 +4,116 @@ ul.item-table { list-style-type: none; } -.table-row { - color: @default-text-color-light; - - .title { - .subject { - color: @default-text-color; - } - - a { - font-weight: bold; - - &:hover { - color: @list-item-title-hover-color; - text-decoration: none; - } - } - } -} - -@media print { - .item-table li.page-break-follows:not(:last-of-type) { - .col { - border-bottom: none; - } - - .visual { - margin-bottom: 0; - } - } +.content:has(> .item-table) > .item-table > .empty-state { + .empty-state-bar(); } // Layout -.table-row { - .title { - display: flex; +ul.item-table { + // Grid specific rules + display: grid; + grid-template-columns: minmax(0, 1fr) repeat(var(--columns), auto); + &:has(> li > .visual) { + grid-template-columns: auto minmax(0, 1fr) repeat(var(--columns), auto); + } - .visual { - width: 2.5em; - padding: .5em 0; - margin-top: -.5em; - margin-bottom: -.5em; + > li { + display: contents; + + &.item-layout .main { + // Usually, the parent is flex, but here it's contents. .main is still stretched though, + // because it's a grid item with a width of 1fr. But the default-item-layout sets a width + // which needs to be overridden. + width: auto; } - .column-content { - flex: 1 1 auto; - width: 0; + .col, &::before, &::after { + // The li might get a background on hover. Though, this won't be visible + // as it has no box model since we apply display:contents to it. + background-color: inherit; + } + } +} - > * { - .text-ellipsis(); +:not(.dashboard) > .container > .content:has(> .item-table), // compat only, for Icinga Web (See #286) +.content:has(> .item-table) { + padding-left: 0; + padding-right: 0; + + > .item-table > .empty-state { + margin: 0 1em; + } + + > ul.item-table { + // Again, since the li has no box model, it cannot have padding. So the first + // and last child need to get the left and right padding respectively. + // But we don't want to have a border that spans to the very right or left, + // so pseudo elements are required. We could add empty cells instead, but + // that would require hard coding the width here, which I'd like to avoid. + + grid-template-columns: ~"auto minmax(0, 1fr) repeat(var(--columns), auto) auto"; + &:has(> li > .visual) { + grid-template-columns: ~"auto auto minmax(0, 1fr) repeat(var(--columns), auto) auto"; + } + + > li.table-row { + &::before, &::after { + display: inline-block; + content: '\00a0'; + width: 0; + margin-bottom: 1px; + } + + &::before { + padding-left: 1em; + } + + &::after { + padding-right: 1em; } } } - - .col { - white-space: nowrap; - } -} - -ul.item-table { - display: grid; - - > .table-row { - .col:not(.title) { - display: grid; - align-items: center; - } - } } ul.item-table { + // General rules padding: 0; margin: 0; + + .table-row { + .col { + margin-right: 0; // Otherwise background has gaps + padding: .5em 1em .5em 0; + &:last-child { + padding-right: 0; + } + + .title { + margin-right: 0; + } + } + + // This is for the legacy layout only + // TODO: Drop this together with BaseTableRowItem + .col.title:has(> .visual) { + display: flex; + + > .visual { + padding-right: .5em; + } + } + + &:not(:last-of-type) { + .col { + border-bottom: 1px solid @list-item-separation-bg; + + &.visual { + border-color: @default-bg; + } + } + } + } } div.item-table { @@ -85,11 +122,10 @@ div.item-table { } } -div.table-row { - display: flex; - column-gap: 1em; - - .title { - flex: 1 1 auto; +@media print { + .item-table li.page-break-follows:not(:last-of-type) { + .col { + border-bottom: none; + } } } diff --git a/asset/css/list/list-item.less b/asset/css/list/list-item.less index 41b66ff..2b878f4 100644 --- a/asset/css/list/list-item.less +++ b/asset/css/list/list-item.less @@ -1,8 +1,6 @@ // Style .list-item { - color: @default-text-color-light; - &:not(:first-child) > .main { border-top: 1px solid @list-item-separation-bg; } @@ -10,40 +8,6 @@ &:not(:first-child) .visual { margin-top: 1px; } - - .caption { - i { - opacity: 0.8; - } - - a { - color: @default-text-color; - } - } - - .title { - .subject { - color: @default-text-color; - } - - a { - color: @default-text-color; - font-weight: bold; - - &:hover { - color: @list-item-title-hover-color; - text-decoration: none; - - .subject { - color: @list-item-title-hover-color; - } - } - } - } - - footer { - padding-top: .5em; - } } @media print { @@ -56,34 +20,10 @@ // Layout -.list-item { +.list-item.item-layout { + .main, .visual { - padding: .5em 0; - width: 2.5em; - } - - .caption { - p { - display: inline-block; - } - } - - .title { - margin-right: 1em; - - p { - margin: 0; - } - } - - time { - white-space: nowrap; - } - - footer { - > * { - font-size: .857em; - line-height: 1.5*.857em; - } + padding-top: .5em; + padding-bottom: .5em; } } diff --git a/asset/css/mixin/mixins.less b/asset/css/mixin/mixins.less index 58b9567..e82fb37 100644 --- a/asset/css/mixin/mixins.less +++ b/asset/css/mixin/mixins.less @@ -39,3 +39,8 @@ .monospace-font() { font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; } + +.user-select(@user-select) { + -webkit-user-select: @user-select; + user-select: @user-select; +} diff --git a/asset/css/schedule-element.less b/asset/css/schedule-element.less index 1905a05..807b87a 100644 --- a/asset/css/schedule-element.less +++ b/asset/css/schedule-element.less @@ -19,7 +19,7 @@ } &:disabled { - color: @schedule-element-fields-disabled-color; + color: var(--schedule-element-fields-disabled-color, @schedule-element-fields-disabled-color); } } } @@ -39,7 +39,7 @@ .monthly, .ordinal:not(.annually) { padding: .5em; margin-left: -.5em; - border: 1px solid @schedule-element-fields-border-color; + border: 1px solid var(--schedule-element-fields-border-color, @schedule-element-fields-border-color); .rounded-corners(.75em); } @@ -56,13 +56,13 @@ pointer-events: none; label { - color: @schedule-element-fields-disabled-color; - background-color: @schedule-element-fields-disabled-bg; + color: var(--schedule-element-fields-disabled-color, @schedule-element-fields-disabled-color); + background-color: var(--schedule-element-fields-disabled-bg, @schedule-element-fields-disabled-bg); } input:checked + label { - background: @schedule-element-fields-disabled-selected-bg; - color: @schedule-element-fields-disabled-color; + background: var(--schedule-element-fields-disabled-selected-bg, @schedule-element-fields-disabled-selected-bg); + color: var(--schedule-element-fields-disabled-color, @schedule-element-fields-disabled-color); } } @@ -75,11 +75,11 @@ cursor: pointer; text-align: center; padding: .75em 0; - background: @schedule-element-fields-bg; - color: @schedule-element-fields-color; + background: var(--schedule-element-fields-bg, @schedule-element-fields-bg); + color: var(--schedule-element-fields-color, @schedule-element-fields-color); &:hover { - background-color: @schedule-element-fields-hover-bg; + background-color: var(--schedule-element-fields-hover-bg, @schedule-element-fields-hover-bg); } &:focus { @@ -88,27 +88,27 @@ } input:checked + label { - background-color: @schedule-element-fields-selected-bg; - color: @schedule-element-fields-selected-color; + background-color: var(--schedule-element-fields-selected-bg, @schedule-element-fields-selected-bg); + color: var(--schedule-element-fields-selected-color, @schedule-element-fields-selected-color); } input:checked + label:hover { - background-color: @schedule-element-fields-selected-hover-bg; - border-color: @schedule-element-fields-selected-hover-bg; + background-color: var(--schedule-element-fields-selected-hover-bg, @schedule-element-fields-selected-hover-bg); + border-color: var(--schedule-element-fields-selected-hover-bg, @schedule-element-fields-selected-hover-bg); } } &.multiple-fields { li:not(:last-child) label { - border-right: 1px solid @schedule-element-fields-border-color; + border-right: 1px solid var(--schedule-element-fields-border-color, @schedule-element-fields-border-color); } input:focus + label { - box-shadow: inset 0 0 0 3px @schedule-element-fields-outline-color; + box-shadow: inset 0 0 0 3px var(--schedule-element-fields-outline-color, @schedule-element-fields-outline-color); } input:checked:focus + label { - box-shadow: inset 0 0 0 3px @schedule-element-fields-selected-outline-color; + box-shadow: inset 0 0 0 3px var(--schedule-element-fields-selected-outline-color, @schedule-element-fields-selected-outline-color); } } @@ -128,7 +128,7 @@ } &:focus-within { - outline: 3px solid @schedule-element-fields-outline-color; + outline: 3px solid var(--schedule-element-fields-outline-color, @schedule-element-fields-outline-color); outline-offset: 2px; } @@ -137,7 +137,7 @@ } input:checked + label:hover { - background-color: @schedule-element-fields-selected-bg; + background-color: var(--schedule-element-fields-selected-bg, @schedule-element-fields-selected-bg); } } } @@ -145,20 +145,17 @@ .note { display: none; padding: .5em; - background: @schedule-element-keyboard-note-bg; + background: var(--schedule-element-keyboard-note-bg, @schedule-element-keyboard-note-bg); .rounded-corners(.25em); text-align: center; margin-top: 1em; line-height: 1.25; } - /* .weekly */ - .weekly { } - /* .monthly styles */ .monthly { li label { - border-top: 1px solid @schedule-element-fields-border-color; + border-top: 1px solid var(--schedule-element-fields-border-color, @schedule-element-fields-border-color); } li:first-child, @@ -189,14 +186,13 @@ li:nth-child(4n) label { margin-right: 0; } + } - .toggle-slider-controls { - display: flex; - column-gap: 1em; - align-items: center; - margin-top: 1em; - margin-bottom: -.6em; - } + .toggle-slider-controls { + display: flex; + column-gap: 1em; + align-items: center; + margin-bottom: -.6em; } } @@ -205,6 +201,6 @@ padding-top: 0.5625em; p { - color: @schedule-element-fields-disabled-color; + color: var(--schedule-element-fields-disabled-color, @schedule-element-fields-disabled-color); } } diff --git a/asset/css/search-bar.less b/asset/css/search-bar.less index 6ad1aec..deb5f9e 100644 --- a/asset/css/search-bar.less +++ b/asset/css/search-bar.less @@ -11,8 +11,8 @@ } // Submit button styles - input[type=submit], - button[type=submit], + input[type="submit"], + button[type="submit"], button:not([type]) { background: var(--primary-button-bg, @primary-button-bg); color: var(--primary-button-color, @primary-button-color); @@ -26,20 +26,20 @@ } // Hide the submit button, it must exist, but shouldn't be shown to the user - input[type=submit][value="hidden"] { + input[type="submit"][value="hidden"] { display: none; } // Left-most search dropdown style button.search-options { - i.icon:before { + i.icon::before { font-size: 1.2em; margin-right: 0; color: var(--control-color, @control-color); } &:disabled { - i.icon:before { + i.icon::before { color: var(--control-disabled-color, @control-disabled-color); } } @@ -52,7 +52,7 @@ background-color: var(--search-condition-remove-bg, @search-condition-remove-bg); color: var(--search-condition-remove-color, @search-condition-remove-color); - &:after { + &::after { content: ""; position: absolute; width: .4em; @@ -77,7 +77,7 @@ .terms > .filter-condition:first-child button { border-radius: 0 .4em .4em 0; - &:before { + &::before { content: ""; position: absolute; width: .4em; @@ -92,14 +92,14 @@ border-bottom-right-radius: .4em; } - &:after { + &::after { content: none; } } - .logical_operator, - .grouping_operator_open, - .grouping_operator_close { + .logical-operator, + .grouping-operator-open, + .grouping-operator-close { input { .rounded-corners(); background-color: var(--search-logical-operator-bg, @search-logical-operator-bg); @@ -108,9 +108,9 @@ } .operator, - .logical_operator, - .grouping_operator_open, - .grouping_operator_close { + .logical-operator, + .grouping-operator-open, + .grouping-operator-close { input { text-align: center; } @@ -153,7 +153,7 @@ li { display: inline; - &:not(:first-of-type):before { + &:not(:first-of-type)::before { display: inline; content: ', '; } @@ -193,12 +193,12 @@ left: ~"calc(-2em - 2px)"; // That's min-width + margin-right of an operator line-height: 16/12; // 16 (px) desired / default font size (px) - i:before { + i::before { margin-right: 0; } } - &:not(._hover_delay):hover button { + &:not([data-hover-delay]):hover button { display: inline; } } @@ -210,9 +210,9 @@ } label { - &.logical_operator, - &.grouping_operator_open, - &.grouping_operator_close { + &.logical-operator, + &.grouping-operator-open, + &.grouping-operator-close { margin-left: 1px; // adds up to 2px with the previous term margin-right: 2px; } diff --git a/asset/css/search-base.less b/asset/css/search-base.less index 416ee31..50e7fc7 100644 --- a/asset/css/search-base.less +++ b/asset/css/search-base.less @@ -58,20 +58,29 @@ fieldset:disabled .term-input-area [data-drag-initiator] { cursor: not-allowed; } -.invalid-reason { - padding: .25em; - .rounded-corners(.25em); - border: 1px solid black; - font-weight: bold; - background: var(--search-term-invalid-reason-bg, @search-term-invalid-reason-bg); +.term-input-area { + .invalid-reason { + padding: .25em; + .rounded-corners(.25em); + border: 1px solid black; + font-weight: bold; + background: var(--search-term-invalid-reason-bg, @search-term-invalid-reason-bg); - opacity: 0; - visibility: hidden; - transition: opacity 2s, visibility 2s; - &.visible { - opacity: 1; - visibility: visible; - transition: none; + opacity: 0; + visibility: hidden; + transition: opacity 2s, visibility 2s; + + &.visible { + opacity: 1; + visibility: visible; + transition: none; + } + } + + .remove-action { + background: var(--search-term-remove-action-bg, @search-term-remove-action-bg); + color: var(--search-term-remove-action-color, @search-term-remove-action-color); + .rounded-corners(0.25em); } } @@ -153,12 +162,15 @@ fieldset:disabled .term-input-area [data-drag-initiator] { // Layout .search-bar .filter-input-area, .term-input-area:not(.vertical) { + --term-padding-v: .25em; + --term-padding-h: .5em; + overflow: auto hidden; - overflow-x: overlay; // Not invalid, but proprietary feature by chrome/webkit display: flex; flex-wrap: nowrap; width: 100%; - height: ~"calc(2em + 10px)"; // Search bar height + approximate scrollbar height + // input line-height + (input vertical padding * 2) + approximate scrollbar height + height: ~"calc(20px + calc(var(--term-padding-v) * 2) + 10px)"; // Lets inputs grow based on their contents, Inspired by https://css-tricks.com/auto-growing-inputs-textareas/ label { @@ -170,7 +182,7 @@ fieldset:disabled .term-input-area [data-drag-initiator] { &::after, input { width: auto; - padding: .25em .5em; + padding: var(--term-padding-v) var(--term-padding-h); resize: none; } @@ -208,6 +220,11 @@ fieldset:disabled .term-input-area [data-drag-initiator] { margin-right: 1px; } } + + &.read-only [data-index] .remove-action { + line-height: 20/12; + padding: var(--term-padding-v) var(--term-padding-h); + } } .term-input-area.vertical { @@ -278,7 +295,6 @@ fieldset:disabled .term-input-area [data-drag-initiator] { position: relative; input { - padding-left: 1.5em; text-align: center; cursor: pointer; @@ -305,6 +321,36 @@ fieldset:disabled .term-input-area [data-drag-initiator] { top: 85%; left: .5em; } + + .remove-action { + display: flex; + align-items: center; + visibility: visible; + position: absolute; + width: 100%; + top: 0; + line-height: normal; + padding: 0.5em; + cursor: pointer; + + i.icon { + margin-left: auto; + } + + .remove-action-label { + margin-right: auto; + .text-ellipsis(); + } + } + + input:invalid ~ .remove-action, + input.invalid ~ .remove-action { + pointer-events: none; + } + + &:not(:hover) .remove-action { + visibility: hidden; + } } } } @@ -324,7 +370,7 @@ fieldset:disabled .term-input-area [data-drag-initiator] { padding: 0; li.suggestion-title { - padding: 1.25em .625em 0 .625em; + padding: 1.25em .625em 0; } li.failure-message { diff --git a/asset/css/search-editor.less b/asset/css/search-editor.less index d32d31c..81803da 100644 --- a/asset/css/search-editor.less +++ b/asset/css/search-editor.less @@ -28,7 +28,7 @@ .rounded-corners(0); } - i.icon:before { + i.icon::before { color: var(--search-editor-control-color, @search-editor-control-color); } @@ -110,7 +110,7 @@ border-top-right-radius: 0; } - &:before { + &::before { // The left pointing arrow border-bottom: 1px solid var(--search-editor-context-menu-border-color, @search-editor-context-menu-border-color); border-left: 1px solid var(--search-editor-context-menu-border-color, @search-editor-context-menu-border-color); @@ -118,7 +118,7 @@ } } - &:hover i.icon:before { + &:hover i.icon::before { .rounded-corners(); background: var(--primary-button-bg, @primary-button-bg); color: var(--primary-button-color, @primary-button-color); @@ -202,7 +202,7 @@ margin-left: .5em; } - i.icon:before { + i.icon::before { margin: 0; font-size: 1.5em; line-height: 1.5; @@ -227,7 +227,7 @@ white-space: nowrap; } - &:before { + &::before { // The left pointing arrow content: ""; display: block; @@ -246,7 +246,7 @@ display: block; } - i.icon:before { + i.icon::before { padding: ((28/18)-1)/2em; // (Container pixels / default font size) - line height / (padding-top,padding-bottom) line-height: 1; } diff --git a/asset/css/suggestion-element.less b/asset/css/suggestion-element.less new file mode 100644 index 0000000..db6b89f --- /dev/null +++ b/asset/css/suggestion-element.less @@ -0,0 +1,29 @@ +.suggestion-element-group { + display: inline-flex; + + .suggestion-element, + .suggestion-element-icon { + line-height: normal; + height: 2.25em; + padding: 0.5em; + background-color: var(--default-input-bg, @default-input-bg); + color: var(--default-text-color, @default-text-color); + } + + .suggestion-element { + border: none; + outline: none; + border-radius: 0 0.25em 0.25em 0; + } + + .suggestion-element-icon { + padding-right: 0; + border-radius: 0.25em 0 0 0.25em; + } + + &:focus-within { + border-radius: 0.25em; + outline: 3px solid var(--default-input-outline-color, @default-input-outline-color); + outline-offset: 1px; + } +} diff --git a/asset/css/variables.less b/asset/css/variables.less index 322049c..01c2e68 100644 --- a/asset/css/variables.less +++ b/asset/css/variables.less @@ -41,6 +41,12 @@ @default-text-color-light: fade(@default-text-color, 75%); @default-text-color-inverted: @default-bg; @default-input-bg: #404d72; +@default-input-hover-bg: #434374; +@default-input-outline-color: @base-primary-light; +@default-remove-bg: @state-critical; +@default-remove-color: @default-text-color-inverted; +@default-delete-bg: @state-critical; +@default-delete-color: @default-text-color-inverted; @state-ok: #44bb77; @state-up: @state-ok; @@ -54,6 +60,7 @@ @primary-button-color: @default-text-color-inverted; @primary-button-bg: @base-primary-bg; @primary-button-hover-bg: @base-primary-dark; +@link-hover-color: @base-primary-color; @search-term-bg: @base-gray; @search-term-color: @default-text-color-inverted; @@ -66,6 +73,8 @@ @search-term-highlighted-bg: @base-primary-bg; @search-term-highlighted-color: @default-text-color-inverted; @search-term-drag-border-color: @base-gray; +@search-term-remove-action-bg: @default-remove-bg; +@search-term-remove-action-color: @default-remove-color; @search-condition-remove-bg: @state-critical; @search-condition-remove-color: @default-text-color-inverted; @@ -115,7 +124,7 @@ @schedule-element-fields-selected-bg: @primary-button-bg; @schedule-element-fields-selected-color: @default-text-color-inverted; @schedule-element-fields-hover-bg: @base-primary-light; -@schedule-element-fields-outline-color: fade(@base-primary-bg, 50%); +@schedule-element-fields-outline-color: @default-input-outline-color; @schedule-element-fields-selected-outline-color: fade(#fff, 50%); @schedule-element-fields-selected-hover-bg: @primary-button-hover-bg; @schedule-element-fields-disabled-color: @base-gray; @@ -126,7 +135,7 @@ @empty-state-color: @base-gray-semilight; @empty-state-bar-bg: @base-gray-lighter; -@list-item-title-hover-color: @base-primary-color; +@list-item-title-hover-color: @link-hover-color; @list-item-separation-bg: @base-gray-light; @iplWebLightRules: { @@ -143,10 +152,17 @@ --default-text-color-light: fade(#535353, 75%); // --default-text-color --default-text-color-inverted: #F5F9FA; --default-input-bg: #DEECF1; + --default-input-hover-bg: #C0CCCD; + --default-input-outline-color: @base-primary-light; + --default-remove-bg: var(--base-remove-bg); + --default-remove-color: var(--default-text-color-inverted); + --default-delete-bg: var(--base-remove-bg); + --default-delete-color: var(--default-text-color-inverted); --primary-button-color: var(--default-text-color-inverted); --primary-button-bg: @primary-button-bg; --primary-button-hover-bg: @primary-button-hover-bg; + --link-hover-color: var(--base-primary-color); --searchbar-bg: var(--default-input-bg); --searchbar-scrollbar-bg: var(--base-gray-light); @@ -162,6 +178,8 @@ --search-term-highlighted-bg: var(--primary-button-bg); --search-term-highlighted-color: var(--default-text-color-inverted); --search-term-drag-border-color: var(--base-gray); + --search-term-remove-action-bg: var(--default-remove-bg); + --search-term-remove-action-color: var(--default-remove-color); --search-condition-remove-bg: var(--base-remove-bg); --search-condition-remove-color: var(--default-text-color-inverted); @@ -203,7 +221,7 @@ --schedule-element-fields-selected-bg: var(--primary-button-bg); --schedule-element-fields-selected-color: var(--default-text-color-inverted); --schedule-element-fields-hover-bg: @base-primary-light; - --schedule-element-fields-outline-color: fade(@base-primary-bg, 50%); + --schedule-element-fields-outline-color: @default-input-outline-color; --schedule-element-fields-selected-outline-color: fade(#fff, 50%); --schedule-element-fields-selected-hover-bg: var(--primary-button-hover-bg); --schedule-element-fields-disabled-color: var(--base-gray); @@ -214,7 +232,7 @@ --empty-state-color: var(--base-gray-semilight); --empty-state-bar-bg: var(--base-gray-lighter); - --list-item-title-hover-color: var(--base-primary-color); + --list-item-title-hover-color: var(--link-hover-color); --list-item-separation-bg: var(--base-gray-light); } }; diff --git a/asset/js/iterator.js b/asset/js/iterator.js new file mode 100644 index 0000000..2c78d9f --- /dev/null +++ b/asset/js/iterator.js @@ -0,0 +1,96 @@ +(function (root, factory) { + "use strict"; + + if (typeof define === "function" && define.icinga) { + define(["exports"], factory); + } else { + factory(root.icingaIteratorPolyfill = root.icingaIteratorPolyfill || {}); + } +}(self, function (exports) { + /** + * Polyfill for `Iterator.filter` + * + * @param {Symbol.iterator} iterator + * @param {function} callback + * @returns {Generator<*, void, *>} + */ + function* filter(iterator, callback) { + if (typeof iterator.filter === "function") { + yield* iterator.filter(callback); + } + + for (const item of iterator) { + if (callback(item)) { + yield item; + } + } + } + + /** + * Polyfill for `Iterator.find` + * + * @param {Symbol.iterator} iterator + * @param {function} callback + * @returns {*} + */ + function find(iterator, callback) { + if (typeof iterator.find === "function") { + return iterator.find(callback); + } + + for (const item of iterator) { + if (callback(item)) { + return item; + } + } + } + + /** + * Polyfill for `Iterator.map` + * + * @param {Symbol.iterator} iterator + * @param {function} callback + * @returns {Generator<*, void, *>} + */ + function* map(iterator, callback) { + if (typeof iterator.map === "function") { + yield* iterator.map(callback); + } + + for (const item of iterator) { + yield callback(item); + } + } + + /** + * Find the first key in the map whose value satisfies the provided testing function. + * @param {Map} map + * @param {function} callback Passed arguments are: value, key, map + * @returns {*} Returns undefined if no key satisfies the testing function. + */ + function findKey(map, callback) { + for (const key of findKeys(map, callback)) { + return key; + } + } + + /** + * Find all keys in the map whose value satisfies the provided testing function. + * @param {Map} map + * @param {function} callback Passed arguments are: value, key, map + * @returns {Generator<*, void, *>} + */ + function* findKeys(map, callback) { + for (const [ key, value ] of map) { + if (callback(value, key, map)) { + yield key; + } + } + } + + exports.findKeys = findKeys; + exports.findKey = findKey; + exports.filter = filter; + exports.find = find; + exports.map = map; +})); diff --git a/asset/js/widget/BaseInput.js b/asset/js/widget/BaseInput.js index eca7371..360d502 100644 --- a/asset/js/widget/BaseInput.js +++ b/asset/js/widget/BaseInput.js @@ -632,7 +632,11 @@ define(["../notjQuery", "Completer"], function ($, Completer) { let eventData = { submittedBy: input }; if (changeType === 'paste') { // Ensure that what's pasted is also transmitted as value - eventData['terms'] = this.termsToQueryString(data['terms']) + this.separator + data['input']; + if (data['terms'].length === 0) { + eventData['terms'] = data['input']; + } else { + eventData['terms'] = this.termsToQueryString(data['terms']) + this.separator + data['input']; + } } $(this.input.form).trigger('submit', eventData); @@ -713,7 +717,14 @@ define(["../notjQuery", "Completer"], function ($, Completer) { this.input.name = ''; // Set the hidden input's value, it's what's sent - if (event.detail && 'terms' in event.detail) { + if ( + event.detail + && 'terms' in event.detail + && ( + ! ('submittedBy' in event.detail) + || event.detail.submittedBy === this.input + ) + ) { this.termInput.value = event.detail.terms; } else { let renderedTerms = this.termsToQueryString(this.usedTerms); diff --git a/asset/js/widget/FilterInput.js b/asset/js/widget/FilterInput.js index fad3da0..3f1d947 100644 --- a/asset/js/widget/FilterInput.js +++ b/asset/js/widget/FilterInput.js @@ -13,7 +13,7 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) { * * @type {{}} */ - this.negationOperator = { label: '!', search: '!', class: 'logical_operator', type: 'negation_operator' }; + this.negationOperator = { label: '!', search: '!', class: 'logical-operator', type: 'negation_operator' }; /** * Supported grouping operators @@ -21,8 +21,8 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) { * @type {{close: {}, open: {}}} */ this.grouping_operators = { - open: { label: '(', search: '(', class: 'grouping_operator_open', type: 'grouping_operator' }, - close: { label: ')', search: ')', class: 'grouping_operator_close', type: 'grouping_operator' } + open: { label: '(', search: '(', class: 'grouping-operator-open', type: 'grouping_operator' }, + close: { label: ')', search: ')', class: 'grouping-operator-close', type: 'grouping_operator' } }; /** @@ -33,8 +33,8 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) { * @type {{}[]} */ this.logical_operators = [ - { label: '&', search: '&', class: 'logical_operator', type: 'logical_operator', default: true }, - { label: '|', search: '|', class: 'logical_operator', type: 'logical_operator' }, + { label: '&', search: '&', class: 'logical-operator', type: 'logical_operator', default: true }, + { label: '|', search: '|', class: 'logical-operator', type: 'logical_operator' }, ]; /** @@ -958,7 +958,7 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) { label.dataset.type = termData.type; if (! termData.class) { - label.classList.add(termData.type); + label.classList.add(termData.type.replace('_', '-')); } if (termData.counterpart >= 0) { @@ -1256,11 +1256,11 @@ define(["../notjQuery", "BaseInput"], function ($, BaseInput) { let label = event.currentTarget; if (['column', 'operator', 'value'].includes(label.dataset.type)) { - // This adds a class to delay the remove button. If it's shown instantly upon hover + // This adds an attr to delay the remove button. If it's shown instantly upon hover // it's too easy to accidentally click it instead of the desired grouping operator. - label.parentNode.classList.add('_hover_delay'); + label.parentNode.dataset.hoverDelay = ""; setTimeout(function () { - label.parentNode.classList.remove('_hover_delay'); + delete label.parentNode.dataset.hoverDelay; }, 500); } diff --git a/asset/js/widget/TermInput.js b/asset/js/widget/TermInput.js index 9e2d9de..c015469 100644 --- a/asset/js/widget/TermInput.js +++ b/asset/js/widget/TermInput.js @@ -187,8 +187,16 @@ define(["../notjQuery", "../vendor/Sortable", "BaseInput"], function ($, Sortabl const label = super.renderTerm(termData, termIndex); if (this.readOnly) { + const removeLabel = this.termContainer.dataset.removeActionLabel; label.firstChild.readOnly = true; - label.appendChild($.render('')); + label.appendChild( + $.render( + `
- *
- * @internal
- */
-final class Php80
-{
- public static function fdiv(float $dividend, float $divisor): float
- {
- return @($dividend / $divisor);
- }
-
- public static function get_debug_type($value): string
- {
- switch (true) {
- case null === $value: return 'null';
- case \is_bool($value): return 'bool';
- case \is_string($value): return 'string';
- case \is_array($value): return 'array';
- case \is_int($value): return 'int';
- case \is_float($value): return 'float';
- case \is_object($value): break;
- case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class';
- default:
- if (null === $type = @get_resource_type($value)) {
- return 'unknown';
- }
-
- if ('Unknown' === $type) {
- $type = 'closed';
- }
-
- return "resource ($type)";
- }
-
- $class = \get_class($value);
-
- if (false === strpos($class, '@')) {
- return $class;
- }
-
- return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous';
- }
-
- public static function get_resource_id($res): int
- {
- if (!\is_resource($res) && null === @get_resource_type($res)) {
- throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res)));
- }
-
- return (int) $res;
- }
-
- public static function preg_last_error_msg(): string
- {
- switch (preg_last_error()) {
- case \PREG_INTERNAL_ERROR:
- return 'Internal error';
- case \PREG_BAD_UTF8_ERROR:
- return 'Malformed UTF-8 characters, possibly incorrectly encoded';
- case \PREG_BAD_UTF8_OFFSET_ERROR:
- return 'The offset did not correspond to the beginning of a valid UTF-8 code point';
- case \PREG_BACKTRACK_LIMIT_ERROR:
- return 'Backtrack limit exhausted';
- case \PREG_RECURSION_LIMIT_ERROR:
- return 'Recursion limit exhausted';
- case \PREG_JIT_STACKLIMIT_ERROR:
- return 'JIT stack limit exhausted';
- case \PREG_NO_ERROR:
- return 'No error';
- default:
- return 'Unknown error';
- }
- }
-
- public static function str_contains(string $haystack, string $needle): bool
- {
- return '' === $needle || false !== strpos($haystack, $needle);
- }
-
- public static function str_starts_with(string $haystack, string $needle): bool
- {
- return 0 === strncmp($haystack, $needle, \strlen($needle));
- }
-
- public static function str_ends_with(string $haystack, string $needle): bool
- {
- if ('' === $needle || $needle === $haystack) {
- return true;
- }
-
- if ('' === $haystack) {
- return false;
- }
-
- $needleLength = \strlen($needle);
-
- return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength);
- }
-}
diff --git a/vendor/symfony/polyfill-php80/PhpToken.php b/vendor/symfony/polyfill-php80/PhpToken.php
deleted file mode 100644
index cd78c4c..0000000
--- a/vendor/symfony/polyfill-php80/PhpToken.php
+++ /dev/null
@@ -1,106 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Symfony\Polyfill\Php80;
-
-/**
- * @author Fedonyuk Anton