Global Boomarks
This commit is contained in:
124
web/css/components/app-bookmark-form.css
Normal file
124
web/css/components/app-bookmark-form.css
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* app-bookmark-form.css
|
||||
* Styles for bookmark and bookmark-group dialogs.
|
||||
*/
|
||||
@layer components {
|
||||
dialog[data-app-bookmark-dialog] > article.app-bookmark-dialog,
|
||||
dialog[data-app-bookmark-group-dialog] > article.app-bookmark-dialog {
|
||||
max-width: 24rem;
|
||||
}
|
||||
|
||||
dialog[data-app-bookmark-dialog] > article.app-bookmark-dialog > header,
|
||||
dialog[data-app-bookmark-group-dialog] > article.app-bookmark-dialog > header {
|
||||
position: relative;
|
||||
padding-inline-end: 2rem;
|
||||
}
|
||||
|
||||
dialog[data-app-bookmark-dialog] > article.app-bookmark-dialog > header h2,
|
||||
dialog[data-app-bookmark-group-dialog] > article.app-bookmark-dialog > header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dialog[data-app-bookmark-dialog] [data-bookmark-dialog-close].close,
|
||||
dialog[data-app-bookmark-group-dialog] [data-bookmark-group-dialog-close].close {
|
||||
position: absolute;
|
||||
inset-inline-end: 1rem;
|
||||
inset-block-start: 1rem;
|
||||
float: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dialog[data-app-bookmark-dialog] form,
|
||||
dialog[data-app-bookmark-group-dialog] form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dialog[data-app-bookmark-dialog] form label,
|
||||
dialog[data-app-bookmark-group-dialog] form label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dialog[data-app-bookmark-dialog] form input[type="text"],
|
||||
dialog[data-app-bookmark-dialog] form select,
|
||||
dialog[data-app-bookmark-group-dialog] form input[type="text"],
|
||||
dialog[data-app-bookmark-group-dialog] form select {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dialog[data-app-bookmark-dialog] form footer,
|
||||
dialog[data-app-bookmark-group-dialog] form footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
padding-top: var(--app-spacing);
|
||||
}
|
||||
|
||||
dialog[data-app-bookmark-dialog] form footer button,
|
||||
dialog[data-app-bookmark-group-dialog] form footer button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-bookmark-delete-btn {
|
||||
color: var(--app-del-color, #c62828);
|
||||
border-color: var(--app-del-color, #c62828);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-bookmark-delete-btn:hover {
|
||||
background: var(--app-del-color, #c62828);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.app-bookmark-new-group-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-bookmark-group-icon-picker {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.app-bookmark-group-icon-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
min-height: 1.9rem;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: var(--app-border-radius);
|
||||
background: transparent;
|
||||
color: var(--app-muted-color);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
transition: border-color 120ms ease, color 120ms ease;
|
||||
}
|
||||
|
||||
.app-bookmark-group-icon-option:hover {
|
||||
border-color: var(--app-primary);
|
||||
color: var(--app-primary);
|
||||
}
|
||||
|
||||
.app-bookmark-group-icon-option.active {
|
||||
border-color: var(--app-primary);
|
||||
background: var(--app-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.app-bookmark-group-icon-option > i {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.app-bookmark-delete-separator {
|
||||
margin-block: 0.2rem 0.35rem;
|
||||
}
|
||||
}
|
||||
@@ -73,26 +73,24 @@
|
||||
}
|
||||
|
||||
.app-details-titlebar details.dropdown li {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.app-details-titlebar details.dropdown li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.app-details-titlebar details.dropdown ul {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-details-titlebar details.dropdown li button,
|
||||
.app-details-titlebar details.dropdown li [role="button"] {
|
||||
margin: 0 !important;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0.375rem 0.5rem;
|
||||
}
|
||||
|
||||
.app-details-titlebar details.dropdown li button:hover,
|
||||
.app-details-titlebar details.dropdown li button:focus-visible,
|
||||
.app-details-titlebar details.dropdown li [role="button"]:hover,
|
||||
.app-details-titlebar details.dropdown li [role="button"]:focus-visible {
|
||||
background: color-mix(in srgb, var(--app-contrast) 8%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,26 +59,24 @@
|
||||
}
|
||||
|
||||
.app-list-titlebar details.dropdown li {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.app-list-titlebar details.dropdown li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.app-list-titlebar details.dropdown ul {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-list-titlebar details.dropdown li button,
|
||||
.app-list-titlebar details.dropdown li [role="button"] {
|
||||
margin: 0 !important;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0.375rem 0.5rem;
|
||||
}
|
||||
|
||||
.app-list-titlebar details.dropdown li button:hover,
|
||||
.app-list-titlebar details.dropdown li button:focus-visible,
|
||||
.app-list-titlebar details.dropdown li [role="button"]:hover,
|
||||
.app-list-titlebar details.dropdown li [role="button"]:focus-visible {
|
||||
background: color-mix(in srgb, var(--app-contrast) 8%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +323,7 @@
|
||||
|
||||
footer.site-footer {
|
||||
padding-inline: calc(var(--app-spacing) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
footer.site-footer a {
|
||||
color: inherit;
|
||||
@@ -1486,7 +1486,8 @@
|
||||
--app-background-color: var(--app-form-element-readonly-background-color);
|
||||
--app-border-color: var(--app-form-element-readonly-border-color);
|
||||
--app-color: var(--app-form-element-readonly-color);
|
||||
--app-box-shadow: inset 0 0 0 0.0625rem var(--app-form-element-readonly-border-color);
|
||||
--app-box-shadow: inset 0 0 0 0.0625rem
|
||||
var(--app-form-element-readonly-border-color);
|
||||
}
|
||||
|
||||
input:not(
|
||||
@@ -2396,8 +2397,8 @@
|
||||
min-width: fit-content;
|
||||
margin: 0;
|
||||
margin-top: var(--app-outline-width);
|
||||
padding: 0;
|
||||
border: var(--app-border-width) solid var(--app-dropdown-border-color);
|
||||
padding: 0.25rem;
|
||||
border: 1px solid var(--app-dropdown-border-color);
|
||||
border-radius: var(--app-border-radius);
|
||||
background-color: var(--app-dropdown-background-color);
|
||||
box-shadow: var(--app-dropdown-box-shadow);
|
||||
@@ -2417,30 +2418,18 @@
|
||||
details.dropdown > summary + ul li {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
padding: calc(var(--app-form-element-spacing-vertical) * 0.5)
|
||||
var(--app-form-element-spacing-horizontal);
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--app-accordion-border-color);
|
||||
}
|
||||
|
||||
details.dropdown > summary + ul li:first-of-type {
|
||||
margin-top: calc(var(--app-form-element-spacing-vertical) * 0.5);
|
||||
}
|
||||
|
||||
details.dropdown > summary + ul li:last-of-type {
|
||||
margin-bottom: calc(var(--app-form-element-spacing-vertical) * 0.5);
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
details.dropdown > summary + ul li a {
|
||||
display: block;
|
||||
margin: calc(var(--app-form-element-spacing-vertical) * -0.5)
|
||||
calc(var(--app-form-element-spacing-horizontal) * -1);
|
||||
padding: calc(var(--app-form-element-spacing-vertical) * 0.5)
|
||||
var(--app-form-element-spacing-horizontal);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0.375rem 0.5rem;
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
border-radius: 3px;
|
||||
color: var(--app-dropdown-color);
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
@@ -2455,7 +2444,7 @@
|
||||
+ ul
|
||||
li
|
||||
a[aria-current]:not([aria-current="false"]) {
|
||||
background-color: var(--app-dropdown-hover-background-color);
|
||||
background: color-mix(in srgb, var(--app-contrast) 8%, transparent);
|
||||
}
|
||||
|
||||
details.dropdown > summary + ul li label {
|
||||
@@ -2463,7 +2452,8 @@
|
||||
}
|
||||
|
||||
details.dropdown > summary + ul li:has(label):hover {
|
||||
background-color: var(--app-dropdown-hover-background-color);
|
||||
background: color-mix(in srgb, var(--app-contrast) 8%, transparent);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
details.dropdown[open] > summary {
|
||||
@@ -2779,6 +2769,10 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
dialog > article > footer button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bi::before,
|
||||
[class^="bi-"]::before,
|
||||
[class*=" bi-"]::before {
|
||||
|
||||
@@ -283,68 +283,267 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-saved-filters {
|
||||
margin-bottom: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-saved-filter-item:before {
|
||||
content: "";
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group.app-sidebar-saved-filters > small {
|
||||
padding: 5px var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-saved-filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
justify-content: space-between;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-saved-filter-item > a {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-saved-filter-delete {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-saved-filter-delete button {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--app-muted-color);
|
||||
padding: 4px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-saved-filter-delete button:hover {
|
||||
color: var(--app-contrast);
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
.app-sidebar-saved-filter-item:hover
|
||||
.app-sidebar-saved-filter-delete
|
||||
button,
|
||||
.app-sidebar
|
||||
.app-sidebar-saved-filter-item:focus-within
|
||||
.app-sidebar-saved-filter-delete
|
||||
button {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
aside.app-sidebar li.app-sidebar-group {
|
||||
margin-bottom: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-bookmark-item > a > i {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-sidebar #aside-panel-bookmarks .app-sidebar-bookmark-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-sidebar #aside-panel-bookmarks .app-sidebar-bookmark-group-shell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-sidebar #aside-panel-bookmarks .app-sidebar-bookmark-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-sidebar #aside-panel-bookmarks .app-sidebar-bookmark-group-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: var(--app-muted-color);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.app-sidebar #aside-panel-bookmarks .app-sidebar-bookmark-group-label > span {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-sidebar #aside-panel-bookmarks .app-sidebar-bookmark-group-label > i {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-sidebar #aside-panel-bookmarks .app-sidebar-bookmark-action-menu {
|
||||
margin: 0;
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu
|
||||
summary:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary {
|
||||
list-style: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--app-muted-color);
|
||||
padding: 0.25rem;
|
||||
min-width: 1.5rem;
|
||||
min-height: 1.5rem;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
color 120ms ease,
|
||||
background 120ms ease,
|
||||
opacity 120ms ease;
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-item:hover
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary,
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-item:focus-within
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary,
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-group-header:hover
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary,
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-group-header:focus-within
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary,
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu[open]
|
||||
> summary,
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary:focus-visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary
|
||||
i {
|
||||
font-size: 0.95rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu[open]
|
||||
> summary,
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary:hover,
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary:focus-visible {
|
||||
color: var(--app-contrast);
|
||||
background: color-mix(in srgb, var(--app-contrast) 10%, transparent);
|
||||
}
|
||||
|
||||
@media (hover: none), (pointer: coarse) {
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu
|
||||
> summary {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.app-sidebar #aside-panel-bookmarks .app-sidebar-bookmark-action-menu-list {
|
||||
position: absolute;
|
||||
inset-inline-end: 0;
|
||||
inset-block-start: calc(100% + 4px);
|
||||
min-width: 10rem;
|
||||
max-width: calc(220px - 2 * var(--app-spacing));
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: var(--app-border-radius);
|
||||
background: var(--app-card-bg, var(--app-background-color));
|
||||
box-shadow:
|
||||
0 4px 16px rgba(0, 0, 0, 0.14),
|
||||
0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
padding: 0.25rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu-list
|
||||
> li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu-list
|
||||
button {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
text-align: start;
|
||||
padding: 0.375rem 0.5rem;
|
||||
margin: 0;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu-list
|
||||
button:hover,
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu-list
|
||||
button:focus-visible {
|
||||
background: color-mix(in srgb, var(--app-contrast) 8%, transparent);
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu-list
|
||||
button[disabled] {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu-list
|
||||
li:has(> .app-sidebar-bookmark-action-danger) {
|
||||
margin-top: 0.2rem;
|
||||
padding-top: 0.2rem;
|
||||
border-top: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu-list
|
||||
.app-sidebar-bookmark-action-danger {
|
||||
color: var(--app-del-color, #c62828);
|
||||
}
|
||||
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu-list
|
||||
.app-sidebar-bookmark-action-danger:hover,
|
||||
.app-sidebar
|
||||
#aside-panel-bookmarks
|
||||
.app-sidebar-bookmark-action-menu-list
|
||||
.app-sidebar-bookmark-action-danger:focus-visible {
|
||||
background: rgba(198, 40, 40, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,13 +187,12 @@ main:has(#app-details-aside-section) .app-topbar-menu-item-detail-sidebar {
|
||||
.app-topbar-menu-list li > a,
|
||||
.app-topbar-menu-list li > button {
|
||||
display: block;
|
||||
margin: calc(var(--app-form-element-spacing-vertical) * -0.5)
|
||||
calc(var(--app-form-element-spacing-horizontal) * -1);
|
||||
padding: calc(var(--app-form-element-spacing-vertical) * 0.5)
|
||||
var(--app-form-element-spacing-horizontal);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0.375rem 0.5rem;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: var(--app-dropdown-color);
|
||||
font: inherit;
|
||||
@@ -213,7 +212,7 @@ main:has(#app-details-aside-section) .app-topbar-menu-item-detail-sidebar {
|
||||
.app-topbar-menu-list li > button:hover,
|
||||
.app-topbar-menu-list li > button:focus,
|
||||
.app-topbar-menu-list li > button:focus-visible {
|
||||
background-color: var(--app-dropdown-hover-background-color);
|
||||
background: color-mix(in srgb, var(--app-contrast) 8%, transparent);
|
||||
}
|
||||
|
||||
.app-topbar-menu-list li > button.app-topbar-menu-with-hotkey {
|
||||
@@ -230,9 +229,7 @@ main:has(#app-details-aside-section) .app-topbar-menu-item-detail-sidebar {
|
||||
}
|
||||
|
||||
.app-topbar-menu-list li.app-topbar-menu-heading {
|
||||
padding-top: calc(var(--app-spacing) * 0.5);
|
||||
padding-bottom: calc(var(--app-spacing) * 0.35);
|
||||
border-bottom: 0;
|
||||
padding: calc(var(--app-spacing) * 0.5) 0.5rem calc(var(--app-spacing) * 0.35);
|
||||
color: var(--app-muted-color);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.03em;
|
||||
@@ -240,14 +237,9 @@ main:has(#app-details-aside-section) .app-topbar-menu-item-detail-sidebar {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.app-topbar-menu-list.app-topbar-menu-list--right li {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.app-topbar-menu-list li.app-topbar-menu-divider {
|
||||
margin: calc(var(--app-spacing) * 0.25) 0;
|
||||
padding: 0;
|
||||
border-bottom: 0;
|
||||
height: 1px;
|
||||
background: var(--app-border);
|
||||
list-style: none;
|
||||
|
||||
10
web/js/components/app-bookmark-init.js
Normal file
10
web/js/components/app-bookmark-init.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* app-bookmark-init.js
|
||||
* Initializes bookmark save form and sidebar panel.
|
||||
*/
|
||||
|
||||
import { init as initBookmarkSave } from './app-bookmark-save.js';
|
||||
import { init as initBookmarkPanel } from './app-bookmark-panel.js';
|
||||
|
||||
initBookmarkSave();
|
||||
initBookmarkPanel();
|
||||
508
web/js/components/app-bookmark-panel.js
Normal file
508
web/js/components/app-bookmark-panel.js
Normal file
@@ -0,0 +1,508 @@
|
||||
/**
|
||||
* Sidebar bookmarks panel actions:
|
||||
* - Reorder root navigation (groups + ungrouped bookmarks) via up/down
|
||||
* - Reorder bookmarks inside groups (up/down)
|
||||
* - Edit/Delete groups and bookmarks with confirm dialog
|
||||
*/
|
||||
import { showAsyncFlash } from './app-async-flash.js';
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
import { confirmDialog } from '../core/app-confirm-dialog.js';
|
||||
|
||||
export function init() {
|
||||
const panel = document.querySelector('[data-aside-panel="bookmarks"]');
|
||||
if (!(panel instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderUrl = String(panel.dataset.bookmarkReorderUrl || '').trim();
|
||||
const groupDeleteUrl = String(panel.dataset.bookmarkGroupDeleteUrl || '').trim();
|
||||
const bookmarkDeleteUrl = String(panel.dataset.bookmarkDeleteUrl || '').trim();
|
||||
if (!reorderUrl || !groupDeleteUrl || !bookmarkDeleteUrl) {
|
||||
warnOnce('UI_DATA_MISSING', 'Missing bookmark panel endpoint metadata', { module: 'bookmark-panel' });
|
||||
return;
|
||||
}
|
||||
|
||||
const root = document.documentElement;
|
||||
const csrfKey = String(root.dataset.csrfKey || '').trim();
|
||||
const csrfToken = String(root.dataset.csrfToken || '').trim();
|
||||
if (!csrfKey || !csrfToken) {
|
||||
warnOnce('UI_DATA_MISSING', 'Missing CSRF metadata for bookmark panel', { module: 'bookmark-panel' });
|
||||
return;
|
||||
}
|
||||
|
||||
const messageDeleteConfirm = panel.dataset.bookmarkMsgGroupDeleteConfirm || 'Delete group and keep bookmarks?';
|
||||
const messageGroupDeleted = panel.dataset.bookmarkMsgGroupDeleted || 'Group deleted';
|
||||
const messageGroupDeleteFailed = panel.dataset.bookmarkMsgGroupDeleteFailed || 'Group delete failed';
|
||||
const messageBookmarkDeleteConfirm = panel.dataset.bookmarkMsgBookmarkDeleteConfirm || 'Delete this bookmark?';
|
||||
const messageBookmarkDeleted = panel.dataset.bookmarkMsgBookmarkDeleted || 'Bookmark deleted';
|
||||
const messageBookmarkDeleteFailed = panel.dataset.bookmarkMsgBookmarkDeleteFailed || 'Bookmark action failed';
|
||||
const messageGroupActionFailed = panel.dataset.bookmarkMsgGroupActionFailed || messageGroupDeleteFailed;
|
||||
const messageBookmarkActionFailed = panel.dataset.bookmarkMsgBookmarkActionFailed || messageBookmarkDeleteFailed;
|
||||
const messageReorderFailed = panel.dataset.bookmarkMsgReorderFailed || 'Reorder failed';
|
||||
const labelDelete = panel.dataset.bookmarkLabelDelete || 'Delete';
|
||||
|
||||
const moduleContext = { module: 'bookmark-panel' };
|
||||
|
||||
panel.querySelectorAll('[data-bookmark-item-action], [data-bookmark-group-action]').forEach((button) => {
|
||||
if (!(button instanceof HTMLButtonElement)) {
|
||||
return;
|
||||
}
|
||||
button.dataset.baseDisabled = button.disabled ? '1' : '0';
|
||||
});
|
||||
|
||||
let pending = false;
|
||||
|
||||
const actionMenus = () => Array.from(panel.querySelectorAll('[data-bookmark-action-menu]'))
|
||||
.filter((menu) => menu instanceof HTMLDetailsElement);
|
||||
|
||||
const closeActionMenus = (except = null) => {
|
||||
actionMenus().forEach((menu) => {
|
||||
if (menu === except) {
|
||||
return;
|
||||
}
|
||||
menu.open = false;
|
||||
});
|
||||
};
|
||||
|
||||
const panelActionButtons = () => Array.from(
|
||||
panel.querySelectorAll('[data-bookmark-item-action], [data-bookmark-group-action]')
|
||||
).filter((button) => button instanceof HTMLButtonElement);
|
||||
|
||||
const setPending = (nextPending) => {
|
||||
pending = nextPending;
|
||||
panelActionButtons().forEach((button) => {
|
||||
const baselineDisabled = button.dataset.baseDisabled === '1';
|
||||
button.disabled = nextPending || baselineDisabled;
|
||||
});
|
||||
};
|
||||
|
||||
const parseJsonSafe = async (response) => {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const reportError = (code, message, details = {}) => {
|
||||
warnOnce(code, message, { ...moduleContext, ...details });
|
||||
showAsyncFlash('error', message);
|
||||
};
|
||||
|
||||
const runReload = (successMessage = '') => {
|
||||
if (String(successMessage).trim() !== '') {
|
||||
showAsyncFlash('success', successMessage, 2500);
|
||||
}
|
||||
window.setTimeout(() => window.location.reload(), 450);
|
||||
};
|
||||
|
||||
const directChildren = (container, className) => {
|
||||
if (!(container instanceof HTMLElement)) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(container.children).filter(
|
||||
(child) => child instanceof HTMLElement && child.classList.contains(className)
|
||||
);
|
||||
};
|
||||
|
||||
const swapInList = (items, currentItem, direction) => {
|
||||
const currentIndex = items.indexOf(currentItem);
|
||||
if (currentIndex < 0) {
|
||||
return [];
|
||||
}
|
||||
const nextIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
||||
if (nextIndex < 0 || nextIndex >= items.length) {
|
||||
return [];
|
||||
}
|
||||
const reordered = items.slice();
|
||||
const temp = reordered[currentIndex];
|
||||
reordered[currentIndex] = reordered[nextIndex];
|
||||
reordered[nextIndex] = temp;
|
||||
return reordered;
|
||||
};
|
||||
|
||||
const buildPairs = (items, dataKey) => {
|
||||
const pairs = [];
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
const item = items[index];
|
||||
const id = Number.parseInt(item.dataset[dataKey] || '', 10);
|
||||
if (id <= 0) {
|
||||
return [];
|
||||
}
|
||||
pairs.push({
|
||||
id,
|
||||
sort_order: index + 1
|
||||
});
|
||||
}
|
||||
return pairs;
|
||||
};
|
||||
|
||||
const buildRootPairs = (items) => {
|
||||
const pairs = [];
|
||||
for (let index = 0; index < items.length; index += 1) {
|
||||
const item = items[index];
|
||||
const kind = String(item.dataset.bookmarkRootKind || '').trim();
|
||||
const id = Number.parseInt(item.dataset.bookmarkRootId || '', 10);
|
||||
if ((kind !== 'group' && kind !== 'bookmark') || id <= 0) {
|
||||
return [];
|
||||
}
|
||||
pairs.push({
|
||||
kind,
|
||||
id,
|
||||
sort_order: index + 1
|
||||
});
|
||||
}
|
||||
return pairs;
|
||||
};
|
||||
|
||||
const submitReorder = async (type, pairs) => {
|
||||
if (pairs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = new FormData();
|
||||
body.set(csrfKey, csrfToken);
|
||||
body.set('type', type);
|
||||
body.set('items', JSON.stringify(pairs));
|
||||
|
||||
setPending(true);
|
||||
try {
|
||||
const response = await fetch(reorderUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body
|
||||
});
|
||||
const json = await parseJsonSafe(response);
|
||||
if (!response.ok || !json) {
|
||||
reportError('FETCH_FAILED', messageReorderFailed, { status: response.status, type });
|
||||
return;
|
||||
}
|
||||
if (!json.ok) {
|
||||
reportError('FETCH_FAILED', messageReorderFailed, { error: json.error, type });
|
||||
return;
|
||||
}
|
||||
|
||||
runReload();
|
||||
} catch (error) {
|
||||
reportError('FETCH_ERROR', messageReorderFailed, { error, type });
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const reorderGroup = async (button) => {
|
||||
const groupItem = button.closest('.app-sidebar-bookmark-root-item.app-sidebar-bookmark-group');
|
||||
if (!(groupItem instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const container = groupItem.parentElement;
|
||||
const rootItems = directChildren(container, 'app-sidebar-bookmark-root-item');
|
||||
if (rootItems.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = String(button.dataset.bookmarkGroupAction || '').trim();
|
||||
const direction = action === 'move-up' ? 'up' : action === 'move-down' ? 'down' : '';
|
||||
if (!direction) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedRootItems = swapInList(rootItems, groupItem, direction);
|
||||
if (reorderedRootItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pairs = buildRootPairs(reorderedRootItems);
|
||||
if (pairs.length !== reorderedRootItems.length) {
|
||||
reportError('UI_INVALID_STATE', messageReorderFailed, { action, target: 'root' });
|
||||
return;
|
||||
}
|
||||
|
||||
await submitReorder('root', pairs);
|
||||
};
|
||||
|
||||
const reorderBookmark = async (button) => {
|
||||
const bookmarkItem = button.closest('.app-sidebar-bookmark-item');
|
||||
if (!(bookmarkItem instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = String(button.dataset.bookmarkItemAction || '').trim();
|
||||
const direction = action === 'move-up' ? 'up' : action === 'move-down' ? 'down' : '';
|
||||
if (!direction) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = String(button.dataset.bookmarkItemActionScope || '').trim();
|
||||
if (scope === 'root') {
|
||||
const rootItem = bookmarkItem.closest('.app-sidebar-bookmark-root-item');
|
||||
if (!(rootItem instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const container = rootItem.parentElement;
|
||||
const rootItems = directChildren(container, 'app-sidebar-bookmark-root-item');
|
||||
if (rootItems.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedRootItems = swapInList(rootItems, rootItem, direction);
|
||||
if (reorderedRootItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootPairs = buildRootPairs(reorderedRootItems);
|
||||
if (rootPairs.length !== reorderedRootItems.length) {
|
||||
reportError('UI_INVALID_STATE', messageReorderFailed, { action, target: 'root' });
|
||||
return;
|
||||
}
|
||||
|
||||
await submitReorder('root', rootPairs);
|
||||
return;
|
||||
}
|
||||
|
||||
const container = bookmarkItem.parentElement;
|
||||
const bookmarks = directChildren(container, 'app-sidebar-bookmark-item');
|
||||
if (bookmarks.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedBookmarks = swapInList(bookmarks, bookmarkItem, direction);
|
||||
if (reorderedBookmarks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pairs = buildPairs(reorderedBookmarks, 'bookmarkId');
|
||||
if (pairs.length !== reorderedBookmarks.length) {
|
||||
reportError('UI_INVALID_STATE', messageReorderFailed, { action, target: 'bookmarks' });
|
||||
return;
|
||||
}
|
||||
|
||||
await submitReorder('bookmarks', pairs);
|
||||
};
|
||||
|
||||
const deleteGroup = async (button) => {
|
||||
const groupItem = button.closest('.app-sidebar-bookmark-group');
|
||||
if (!(groupItem instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const groupId = Number.parseInt(groupItem.dataset.bookmarkGroupId || '', 10);
|
||||
if (groupId <= 0) {
|
||||
reportError('UI_INVALID_STATE', messageGroupDeleteFailed, { reason: 'invalid_group_id' });
|
||||
return;
|
||||
}
|
||||
|
||||
const approved = await confirmDialog.confirm({
|
||||
message: messageDeleteConfirm,
|
||||
confirmLabel: labelDelete,
|
||||
actionKind: 'delete',
|
||||
variant: 'danger',
|
||||
focus: 'cancel'
|
||||
});
|
||||
if (!approved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = new FormData();
|
||||
body.set(csrfKey, csrfToken);
|
||||
body.set('id', String(groupId));
|
||||
|
||||
setPending(true);
|
||||
try {
|
||||
const response = await fetch(groupDeleteUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body
|
||||
});
|
||||
const json = await parseJsonSafe(response);
|
||||
if (!response.ok || !json) {
|
||||
reportError('FETCH_FAILED', messageGroupDeleteFailed, { status: response.status, groupId });
|
||||
return;
|
||||
}
|
||||
if (!json.ok) {
|
||||
reportError('FETCH_FAILED', messageGroupDeleteFailed, { error: json.error, groupId });
|
||||
return;
|
||||
}
|
||||
|
||||
runReload(messageGroupDeleted);
|
||||
} catch (error) {
|
||||
reportError('FETCH_ERROR', messageGroupDeleteFailed, { error, groupId });
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteBookmark = async (button) => {
|
||||
const bookmarkItem = button.closest('.app-sidebar-bookmark-item');
|
||||
if (!(bookmarkItem instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const bookmarkId = Number.parseInt(bookmarkItem.dataset.bookmarkId || '', 10);
|
||||
if (bookmarkId <= 0) {
|
||||
reportError('UI_INVALID_STATE', messageBookmarkDeleteFailed, { reason: 'invalid_bookmark_id' });
|
||||
return;
|
||||
}
|
||||
|
||||
const approved = await confirmDialog.confirm({
|
||||
message: messageBookmarkDeleteConfirm,
|
||||
confirmLabel: labelDelete,
|
||||
actionKind: 'delete',
|
||||
variant: 'danger',
|
||||
focus: 'cancel'
|
||||
});
|
||||
if (!approved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = new FormData();
|
||||
body.set(csrfKey, csrfToken);
|
||||
body.set('id', String(bookmarkId));
|
||||
|
||||
setPending(true);
|
||||
try {
|
||||
const response = await fetch(bookmarkDeleteUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body
|
||||
});
|
||||
const json = await parseJsonSafe(response);
|
||||
if (!response.ok || !json) {
|
||||
reportError('FETCH_FAILED', messageBookmarkDeleteFailed, { status: response.status, bookmarkId });
|
||||
return;
|
||||
}
|
||||
if (!json.ok) {
|
||||
reportError('FETCH_FAILED', messageBookmarkDeleteFailed, { error: json.error, bookmarkId });
|
||||
return;
|
||||
}
|
||||
|
||||
runReload(messageBookmarkDeleted);
|
||||
} catch (error) {
|
||||
reportError('FETCH_ERROR', messageBookmarkDeleteFailed, { error, bookmarkId });
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const editGroup = (button) => {
|
||||
const groupItem = button.closest('.app-sidebar-bookmark-group');
|
||||
if (!(groupItem instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const groupId = Number.parseInt(groupItem.dataset.bookmarkGroupId || '', 10);
|
||||
const groupName = String(groupItem.dataset.bookmarkGroupName || '').trim();
|
||||
const groupIcon = String(groupItem.dataset.bookmarkGroupIcon || 'bi-folder').trim() || 'bi-folder';
|
||||
if (groupId <= 0 || !groupName) {
|
||||
reportError('UI_INVALID_STATE', messageGroupActionFailed, { reason: 'invalid_group_payload' });
|
||||
return;
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('app:bookmark-group-edit', {
|
||||
detail: {
|
||||
id: groupId,
|
||||
name: groupName,
|
||||
icon: groupIcon
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const editBookmark = (button) => {
|
||||
const bookmarkItem = button.closest('.app-sidebar-bookmark-item');
|
||||
if (!(bookmarkItem instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bookmarkId = Number.parseInt(bookmarkItem.dataset.bookmarkId || '', 10);
|
||||
const bookmarkName = String(bookmarkItem.dataset.bookmarkName || '').trim();
|
||||
const groupId = Number.parseInt(bookmarkItem.dataset.bookmarkGroupId || '', 10);
|
||||
if (bookmarkId <= 0 || !bookmarkName) {
|
||||
reportError('UI_INVALID_STATE', messageBookmarkActionFailed, { reason: 'invalid_bookmark_payload' });
|
||||
return;
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('app:bookmark-edit', {
|
||||
detail: {
|
||||
id: bookmarkId,
|
||||
name: bookmarkName,
|
||||
groupId: groupId > 0 ? String(groupId) : ''
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
panel.addEventListener('click', (event) => {
|
||||
const target = event.target instanceof HTMLElement ? event.target : null;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuSummary = target.closest('[data-bookmark-action-menu] > summary');
|
||||
if (menuSummary instanceof HTMLElement) {
|
||||
if (pending) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
const menu = menuSummary.parentElement;
|
||||
if (menu instanceof HTMLDetailsElement) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const shouldOpen = !menu.open;
|
||||
closeActionMenus(menu);
|
||||
menu.open = shouldOpen;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupActionButton = target.closest('[data-bookmark-group-action]');
|
||||
if (groupActionButton instanceof HTMLButtonElement) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeActionMenus();
|
||||
|
||||
const action = String(groupActionButton.dataset.bookmarkGroupAction || '').trim();
|
||||
if (action === 'edit') {
|
||||
editGroup(groupActionButton);
|
||||
return;
|
||||
}
|
||||
if (action === 'delete') {
|
||||
void deleteGroup(groupActionButton);
|
||||
return;
|
||||
}
|
||||
if (action === 'move-up' || action === 'move-down') {
|
||||
void reorderGroup(groupActionButton);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const itemActionButton = target.closest('[data-bookmark-item-action]');
|
||||
if (itemActionButton instanceof HTMLButtonElement) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeActionMenus();
|
||||
const action = String(itemActionButton.dataset.bookmarkItemAction || '').trim();
|
||||
if (action === 'edit') {
|
||||
editBookmark(itemActionButton);
|
||||
return;
|
||||
}
|
||||
if (action === 'delete') {
|
||||
void deleteBookmark(itemActionButton);
|
||||
return;
|
||||
}
|
||||
if (action === 'move-up' || action === 'move-down') {
|
||||
void reorderBookmark(itemActionButton);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!(event.target instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
const clickedInsideMenu = actionMenus().some((menu) => menu.contains(event.target));
|
||||
if (!clickedInsideMenu) {
|
||||
closeActionMenus();
|
||||
}
|
||||
});
|
||||
}
|
||||
593
web/js/components/app-bookmark-save.js
Normal file
593
web/js/components/app-bookmark-save.js
Normal file
@@ -0,0 +1,593 @@
|
||||
/**
|
||||
* app-bookmark-save.js
|
||||
* Bookmark dialog (create/update) + group dialog (create/update).
|
||||
*/
|
||||
|
||||
import { showAsyncFlash } from './app-async-flash.js';
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
export function init() {
|
||||
const openBtn = document.querySelector('[data-bookmark-open-dialog]');
|
||||
const bookmarkDialog = document.querySelector('[data-app-bookmark-dialog]');
|
||||
if (!(bookmarkDialog instanceof HTMLDialogElement) || !(openBtn instanceof HTMLButtonElement)) return;
|
||||
|
||||
const bookmarkForm = bookmarkDialog.querySelector('[data-bookmark-save-form]');
|
||||
if (!(bookmarkForm instanceof HTMLFormElement)) return;
|
||||
|
||||
const groupDialog = document.querySelector('[data-app-bookmark-group-dialog]');
|
||||
const groupForm = groupDialog instanceof HTMLDialogElement
|
||||
? groupDialog.querySelector('[data-bookmark-group-save-form]')
|
||||
: null;
|
||||
const hasGroupDialog = groupDialog instanceof HTMLDialogElement && groupForm instanceof HTMLFormElement;
|
||||
|
||||
const root = document.documentElement;
|
||||
const csrfKey = String(root.dataset.csrfKey || '').trim();
|
||||
const csrfToken = String(root.dataset.csrfToken || '').trim();
|
||||
const moduleContext = { module: 'bookmark-save' };
|
||||
|
||||
if (!csrfKey || !csrfToken) {
|
||||
warnOnce('UI_DATA_MISSING', 'Missing CSRF metadata for bookmark dialog', moduleContext);
|
||||
return;
|
||||
}
|
||||
|
||||
const nameInput = bookmarkForm.querySelector('input[name="name"]');
|
||||
const groupSelect = bookmarkForm.querySelector('[data-bookmark-group-select]');
|
||||
const newGroupBtn = bookmarkForm.querySelector('[data-bookmark-new-group]');
|
||||
const closeBookmarkBtns = bookmarkDialog.querySelectorAll('[data-bookmark-dialog-close]');
|
||||
const submitBtn = bookmarkDialog.querySelector('[data-bookmark-submit-label]');
|
||||
const titleEl = bookmarkDialog.querySelector('#app-bookmark-dialog-title');
|
||||
|
||||
const maxBookmarkMessage = bookmarkDialog.dataset.maxMessage || '';
|
||||
const bookmarkMessageSaved = bookmarkDialog.dataset.msgSaved || 'Bookmark saved';
|
||||
const bookmarkMessageUpdated = bookmarkDialog.dataset.msgUpdated || 'Bookmark updated';
|
||||
const bookmarkMessageErrorGeneric = bookmarkDialog.dataset.msgErrorGeneric || 'Bookmark action failed';
|
||||
const bookmarkMessageNameRequired = bookmarkDialog.dataset.msgNameRequired || 'Bookmark name is required';
|
||||
const bookmarkApiErrorMessages = {
|
||||
name_required: bookmarkDialog.dataset.msgNameRequired || bookmarkMessageErrorGeneric,
|
||||
url_required: bookmarkDialog.dataset.msgUrlRequired || bookmarkMessageErrorGeneric,
|
||||
group_not_found: bookmarkDialog.dataset.msgGroupNotFound || bookmarkMessageErrorGeneric,
|
||||
not_found: bookmarkMessageErrorGeneric,
|
||||
create_failed: bookmarkMessageErrorGeneric,
|
||||
update_failed: bookmarkMessageErrorGeneric
|
||||
};
|
||||
|
||||
let editingBookmarkId = null;
|
||||
let bookmarkReturnState = null;
|
||||
|
||||
let groupEditingId = null;
|
||||
let groupMode = 'create';
|
||||
let groupSource = 'sidebar';
|
||||
|
||||
const syncModalBodyClass = () => {
|
||||
const isOpen = bookmarkDialog.open || (hasGroupDialog && groupDialog.open);
|
||||
document.body.classList.toggle('modal-is-open', isOpen);
|
||||
};
|
||||
|
||||
const reportError = (code, message, details = {}) => {
|
||||
warnOnce(code, message, { ...moduleContext, ...details });
|
||||
showAsyncFlash('error', message);
|
||||
};
|
||||
|
||||
const parseJsonSafe = async (response) => {
|
||||
try {
|
||||
return await response.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveBookmarkApiErrorMessage = (errorCode) => {
|
||||
if (!errorCode) return bookmarkMessageErrorGeneric;
|
||||
return bookmarkApiErrorMessages[errorCode] || bookmarkMessageErrorGeneric;
|
||||
};
|
||||
|
||||
const runReloadWithSuccess = (message) => {
|
||||
showAsyncFlash('success', message, 2500);
|
||||
window.setTimeout(() => window.location.reload(), 450);
|
||||
};
|
||||
|
||||
const closeBookmarkDialog = () => {
|
||||
try {
|
||||
if (bookmarkDialog.open) bookmarkDialog.close();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
bookmarkForm.removeAttribute('aria-busy');
|
||||
syncModalBodyClass();
|
||||
};
|
||||
|
||||
const openBookmarkDialog = () => {
|
||||
try {
|
||||
bookmarkDialog.showModal();
|
||||
syncModalBodyClass();
|
||||
return true;
|
||||
} catch (error) {
|
||||
reportError('UI_DIALOG_OPEN_FAILED', bookmarkMessageErrorGeneric, { error });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const setBookmarkSubmitPending = (pending) => {
|
||||
if (submitBtn instanceof HTMLButtonElement) {
|
||||
submitBtn.disabled = pending;
|
||||
}
|
||||
bookmarkForm.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||
};
|
||||
|
||||
const setBookmarkSubmitLabel = (label) => {
|
||||
if (!(submitBtn instanceof HTMLElement)) return;
|
||||
const text = String(label || '').trim();
|
||||
if (!text) return;
|
||||
submitBtn.textContent = text;
|
||||
};
|
||||
|
||||
const applyBookmarkModeUi = (isEditMode) => {
|
||||
if (titleEl) {
|
||||
titleEl.textContent = isEditMode
|
||||
? (bookmarkDialog.dataset.titleEdit || 'Bookmarks')
|
||||
: (bookmarkDialog.dataset.titleCreate || 'Bookmarks');
|
||||
}
|
||||
setBookmarkSubmitLabel(isEditMode
|
||||
? (bookmarkDialog.dataset.labelUpdate || 'Save')
|
||||
: (bookmarkDialog.dataset.labelSave || 'Save'));
|
||||
};
|
||||
|
||||
const configureBookmarkModeFromTopbar = () => {
|
||||
const isEditMode = openBtn.hasAttribute('data-bookmark-existing-id');
|
||||
applyBookmarkModeUi(isEditMode);
|
||||
|
||||
if (isEditMode) {
|
||||
editingBookmarkId = openBtn.dataset.bookmarkExistingId || null;
|
||||
if (nameInput) nameInput.value = openBtn.dataset.bookmarkExistingName || '';
|
||||
if (groupSelect) groupSelect.value = openBtn.dataset.bookmarkExistingGroup || '';
|
||||
return;
|
||||
}
|
||||
|
||||
editingBookmarkId = null;
|
||||
bookmarkForm.reset();
|
||||
if (nameInput) {
|
||||
const title = document.title.replace(/\s*[|–—].*$/, '').trim();
|
||||
nameInput.value = title || '';
|
||||
}
|
||||
};
|
||||
|
||||
const openBookmarkEditFromSidebar = (detail) => {
|
||||
const bookmarkId = Number.parseInt(String(detail?.id ?? ''), 10);
|
||||
const bookmarkName = String(detail?.name ?? '').trim();
|
||||
const groupId = String(detail?.groupId ?? '').trim();
|
||||
if (bookmarkId <= 0 || !bookmarkName) {
|
||||
reportError('UI_INVALID_STATE', bookmarkMessageErrorGeneric, { detail });
|
||||
return;
|
||||
}
|
||||
|
||||
editingBookmarkId = String(bookmarkId);
|
||||
applyBookmarkModeUi(true);
|
||||
if (nameInput instanceof HTMLInputElement) {
|
||||
nameInput.value = bookmarkName;
|
||||
}
|
||||
if (groupSelect instanceof HTMLSelectElement) {
|
||||
groupSelect.value = groupId;
|
||||
}
|
||||
|
||||
if (openBookmarkDialog() && nameInput instanceof HTMLInputElement) {
|
||||
nameInput.focus();
|
||||
nameInput.select();
|
||||
}
|
||||
};
|
||||
|
||||
const captureBookmarkState = () => ({
|
||||
isEditMode: editingBookmarkId !== null && editingBookmarkId !== '',
|
||||
bookmarkId: editingBookmarkId,
|
||||
name: nameInput instanceof HTMLInputElement ? nameInput.value : '',
|
||||
groupId: groupSelect instanceof HTMLSelectElement ? groupSelect.value : ''
|
||||
});
|
||||
|
||||
const restoreBookmarkFromState = (selectedGroupId = null) => {
|
||||
const state = bookmarkReturnState;
|
||||
bookmarkReturnState = null;
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyBookmarkModeUi(Boolean(state.isEditMode));
|
||||
editingBookmarkId = state.isEditMode ? state.bookmarkId : null;
|
||||
if (nameInput instanceof HTMLInputElement) {
|
||||
nameInput.value = state.name || '';
|
||||
}
|
||||
if (groupSelect instanceof HTMLSelectElement) {
|
||||
groupSelect.value = selectedGroupId ?? state.groupId ?? '';
|
||||
}
|
||||
|
||||
if (openBookmarkDialog() && nameInput instanceof HTMLInputElement) {
|
||||
nameInput.focus();
|
||||
nameInput.select();
|
||||
}
|
||||
};
|
||||
|
||||
// Bookmark dialog open
|
||||
openBtn.addEventListener('click', () => {
|
||||
configureBookmarkModeFromTopbar();
|
||||
if (openBookmarkDialog() && nameInput instanceof HTMLInputElement) {
|
||||
nameInput.select();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('app:bookmark-edit', (event) => {
|
||||
const detail = event instanceof CustomEvent ? event.detail : null;
|
||||
openBookmarkEditFromSidebar(detail);
|
||||
});
|
||||
|
||||
closeBookmarkBtns.forEach((btn) => {
|
||||
btn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
closeBookmarkDialog();
|
||||
});
|
||||
});
|
||||
|
||||
bookmarkDialog.addEventListener('click', (event) => {
|
||||
if (event.target === bookmarkDialog) {
|
||||
closeBookmarkDialog();
|
||||
}
|
||||
});
|
||||
bookmarkDialog.addEventListener('close', () => {
|
||||
bookmarkForm.removeAttribute('aria-busy');
|
||||
syncModalBodyClass();
|
||||
});
|
||||
bookmarkDialog.addEventListener('cancel', () => {
|
||||
bookmarkForm.removeAttribute('aria-busy');
|
||||
syncModalBodyClass();
|
||||
});
|
||||
|
||||
// Save bookmark
|
||||
bookmarkForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const name = nameInput instanceof HTMLInputElement ? nameInput.value.trim() : '';
|
||||
if (!name) {
|
||||
showAsyncFlash('warning', bookmarkMessageNameRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
const body = new FormData();
|
||||
body.set(csrfKey, csrfToken);
|
||||
body.set('name', name);
|
||||
body.set('group_id', groupSelect instanceof HTMLSelectElement ? groupSelect.value : '');
|
||||
|
||||
setBookmarkSubmitPending(true);
|
||||
if (editingBookmarkId) {
|
||||
body.set('id', editingBookmarkId);
|
||||
try {
|
||||
const response = await fetch(getAppBase() + 'bookmarks/update-data', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body
|
||||
});
|
||||
const json = await parseJsonSafe(response);
|
||||
if (!response.ok || !json) {
|
||||
reportError('FETCH_FAILED', bookmarkMessageErrorGeneric, { status: response.status });
|
||||
return;
|
||||
}
|
||||
if (!json.ok) {
|
||||
reportError('FETCH_FAILED', resolveBookmarkApiErrorMessage(json.error), { error: json.error });
|
||||
return;
|
||||
}
|
||||
|
||||
closeBookmarkDialog();
|
||||
runReloadWithSuccess(bookmarkMessageUpdated);
|
||||
} catch (error) {
|
||||
reportError('FETCH_ERROR', bookmarkMessageErrorGeneric, { error });
|
||||
} finally {
|
||||
setBookmarkSubmitPending(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
body.set('url', currentRelativeUrl());
|
||||
try {
|
||||
const response = await fetch(getAppBase() + 'bookmarks/save-data', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body
|
||||
});
|
||||
const json = await parseJsonSafe(response);
|
||||
if (!response.ok || !json) {
|
||||
reportError('FETCH_FAILED', bookmarkMessageErrorGeneric, { status: response.status });
|
||||
return;
|
||||
}
|
||||
if (!json.ok) {
|
||||
if (json.error === 'max_reached') {
|
||||
showAsyncFlash('warning', maxBookmarkMessage || 'Maximum bookmarks reached');
|
||||
return;
|
||||
}
|
||||
reportError('FETCH_FAILED', resolveBookmarkApiErrorMessage(json.error), { error: json.error });
|
||||
return;
|
||||
}
|
||||
|
||||
closeBookmarkDialog();
|
||||
runReloadWithSuccess(json.mode === 'updated' ? bookmarkMessageUpdated : bookmarkMessageSaved);
|
||||
} catch (error) {
|
||||
reportError('FETCH_ERROR', bookmarkMessageErrorGeneric, { error });
|
||||
} finally {
|
||||
setBookmarkSubmitPending(false);
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasGroupDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupNameInput = groupForm.querySelector('input[name="name"]');
|
||||
const groupIconInput = groupForm.querySelector('input[name="icon"]');
|
||||
const groupIconPicker = groupForm.querySelector('[data-bookmark-group-icon-picker]');
|
||||
const groupCloseBtns = groupDialog.querySelectorAll('[data-bookmark-group-dialog-close]');
|
||||
const groupTitleEl = groupDialog.querySelector('#app-bookmark-group-dialog-title');
|
||||
const groupSubmitBtn = groupDialog.querySelector('[data-bookmark-group-submit-label]');
|
||||
|
||||
const groupMessageCreated = groupDialog.dataset.msgCreated || 'Group created';
|
||||
const groupMessageUpdated = groupDialog.dataset.msgUpdated || 'Group updated';
|
||||
const groupMessageErrorGeneric = groupDialog.dataset.msgErrorGeneric || 'Group action failed';
|
||||
const groupMessageNameRequired = groupDialog.dataset.msgNameRequired || 'Group name is required';
|
||||
const maxGroupMessage = groupDialog.dataset.maxMessage || 'Maximum groups reached';
|
||||
const groupApiErrorMessages = {
|
||||
name_required: groupMessageNameRequired,
|
||||
not_found: groupMessageErrorGeneric,
|
||||
create_failed: groupMessageErrorGeneric,
|
||||
update_failed: groupMessageErrorGeneric
|
||||
};
|
||||
|
||||
const resolveGroupApiErrorMessage = (errorCode) => {
|
||||
if (!errorCode) return groupMessageErrorGeneric;
|
||||
return groupApiErrorMessages[errorCode] || groupMessageErrorGeneric;
|
||||
};
|
||||
|
||||
const closeGroupDialog = () => {
|
||||
try {
|
||||
if (groupDialog.open) groupDialog.close();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
groupForm.removeAttribute('aria-busy');
|
||||
syncModalBodyClass();
|
||||
};
|
||||
|
||||
const openGroupDialog = () => {
|
||||
try {
|
||||
groupDialog.showModal();
|
||||
syncModalBodyClass();
|
||||
return true;
|
||||
} catch (error) {
|
||||
reportError('UI_DIALOG_OPEN_FAILED', groupMessageErrorGeneric, { error });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const setGroupSubmitPending = (pending) => {
|
||||
if (groupSubmitBtn instanceof HTMLButtonElement) {
|
||||
groupSubmitBtn.disabled = pending;
|
||||
}
|
||||
groupForm.setAttribute('aria-busy', pending ? 'true' : 'false');
|
||||
};
|
||||
|
||||
const setGroupSubmitLabel = (label) => {
|
||||
if (!(groupSubmitBtn instanceof HTMLElement)) return;
|
||||
const text = String(label || '').trim();
|
||||
if (!text) return;
|
||||
groupSubmitBtn.textContent = text;
|
||||
};
|
||||
|
||||
const setGroupIcon = (icon) => {
|
||||
const value = String(icon || '').trim() || 'bi-folder';
|
||||
if (groupIconInput instanceof HTMLInputElement) {
|
||||
groupIconInput.value = value;
|
||||
}
|
||||
if (groupIconPicker instanceof HTMLElement) {
|
||||
groupIconPicker.querySelectorAll('.active').forEach((item) => item.classList.remove('active'));
|
||||
const selected = groupIconPicker.querySelector(`[data-icon="${value}"]`);
|
||||
if (selected instanceof HTMLElement) {
|
||||
selected.classList.add('active');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const upsertGroupOption = (group) => {
|
||||
if (!(groupSelect instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
const id = String(group?.id ?? '').trim();
|
||||
const name = String(group?.name ?? '').trim();
|
||||
if (!id || !name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = Array.from(groupSelect.options).find((option) => option.value === id);
|
||||
if (existing) {
|
||||
existing.textContent = name;
|
||||
} else {
|
||||
const option = document.createElement('option');
|
||||
option.value = id;
|
||||
option.textContent = name;
|
||||
groupSelect.appendChild(option);
|
||||
}
|
||||
};
|
||||
|
||||
const configureGroupDialog = ({ mode, source, id = null, name = '', icon = 'bi-folder', restoreState = null }) => {
|
||||
groupMode = mode === 'edit' ? 'edit' : 'create';
|
||||
groupSource = source === 'bookmark' ? 'bookmark' : 'sidebar';
|
||||
groupEditingId = groupMode === 'edit' ? id : null;
|
||||
bookmarkReturnState = groupSource === 'bookmark' ? restoreState : null;
|
||||
|
||||
groupForm.reset();
|
||||
setGroupIcon(icon);
|
||||
if (groupNameInput instanceof HTMLInputElement) {
|
||||
groupNameInput.value = String(name || '');
|
||||
}
|
||||
|
||||
if (groupTitleEl instanceof HTMLElement) {
|
||||
groupTitleEl.textContent = groupMode === 'edit'
|
||||
? (groupDialog.dataset.titleEdit || 'Edit group')
|
||||
: (groupDialog.dataset.titleCreate || 'New group');
|
||||
}
|
||||
setGroupSubmitLabel(groupMode === 'edit'
|
||||
? (groupDialog.dataset.labelUpdate || 'Save')
|
||||
: (groupDialog.dataset.labelSave || 'Save'));
|
||||
};
|
||||
|
||||
const handleGroupCancel = () => {
|
||||
closeGroupDialog();
|
||||
if (groupSource === 'bookmark' && bookmarkReturnState) {
|
||||
restoreBookmarkFromState();
|
||||
} else {
|
||||
bookmarkReturnState = null;
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateGroupFromBookmarkDialog = () => {
|
||||
const state = captureBookmarkState();
|
||||
closeBookmarkDialog();
|
||||
configureGroupDialog({
|
||||
mode: 'create',
|
||||
source: 'bookmark',
|
||||
restoreState: state
|
||||
});
|
||||
if (openGroupDialog() && groupNameInput instanceof HTMLInputElement) {
|
||||
groupNameInput.focus();
|
||||
groupNameInput.select();
|
||||
}
|
||||
};
|
||||
|
||||
if (newGroupBtn instanceof HTMLButtonElement) {
|
||||
newGroupBtn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
openCreateGroupFromBookmarkDialog();
|
||||
});
|
||||
}
|
||||
|
||||
if (groupIconPicker instanceof HTMLElement) {
|
||||
groupIconPicker.addEventListener('click', (event) => {
|
||||
const target = event.target instanceof HTMLElement ? event.target.closest('[data-icon]') : null;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
event.preventDefault();
|
||||
setGroupIcon(target.dataset.icon || 'bi-folder');
|
||||
});
|
||||
}
|
||||
|
||||
groupCloseBtns.forEach((btn) => {
|
||||
btn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
handleGroupCancel();
|
||||
});
|
||||
});
|
||||
|
||||
groupDialog.addEventListener('click', (event) => {
|
||||
if (event.target === groupDialog) {
|
||||
handleGroupCancel();
|
||||
}
|
||||
});
|
||||
groupDialog.addEventListener('cancel', (event) => {
|
||||
event.preventDefault();
|
||||
handleGroupCancel();
|
||||
});
|
||||
groupDialog.addEventListener('close', () => {
|
||||
groupForm.removeAttribute('aria-busy');
|
||||
syncModalBodyClass();
|
||||
});
|
||||
|
||||
document.addEventListener('app:bookmark-group-edit', (event) => {
|
||||
const detail = event instanceof CustomEvent ? event.detail : null;
|
||||
const groupId = Number.parseInt(String(detail?.id ?? ''), 10);
|
||||
const groupName = String(detail?.name ?? '').trim();
|
||||
const groupIcon = String(detail?.icon ?? 'bi-folder').trim() || 'bi-folder';
|
||||
if (groupId <= 0 || !groupName) {
|
||||
reportError('UI_INVALID_STATE', groupMessageErrorGeneric, { detail });
|
||||
return;
|
||||
}
|
||||
|
||||
configureGroupDialog({
|
||||
mode: 'edit',
|
||||
source: 'sidebar',
|
||||
id: groupId,
|
||||
name: groupName,
|
||||
icon: groupIcon
|
||||
});
|
||||
if (openGroupDialog() && groupNameInput instanceof HTMLInputElement) {
|
||||
groupNameInput.focus();
|
||||
groupNameInput.select();
|
||||
}
|
||||
});
|
||||
|
||||
groupForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const name = groupNameInput instanceof HTMLInputElement ? groupNameInput.value.trim() : '';
|
||||
if (!name) {
|
||||
showAsyncFlash('warning', groupMessageNameRequired);
|
||||
return;
|
||||
}
|
||||
const icon = groupIconInput instanceof HTMLInputElement ? groupIconInput.value : 'bi-folder';
|
||||
|
||||
const body = new FormData();
|
||||
body.set(csrfKey, csrfToken);
|
||||
body.set('name', name);
|
||||
body.set('icon', icon);
|
||||
if (groupMode === 'edit' && groupEditingId) {
|
||||
body.set('id', String(groupEditingId));
|
||||
}
|
||||
|
||||
setGroupSubmitPending(true);
|
||||
try {
|
||||
const response = await fetch(getAppBase() + 'bookmarks/group-save-data', {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body
|
||||
});
|
||||
const json = await parseJsonSafe(response);
|
||||
if (!response.ok || !json) {
|
||||
reportError('FETCH_FAILED', groupMessageErrorGeneric, { status: response.status });
|
||||
return;
|
||||
}
|
||||
if (!json.ok) {
|
||||
if (json.error === 'max_reached') {
|
||||
showAsyncFlash('warning', maxGroupMessage);
|
||||
return;
|
||||
}
|
||||
reportError('FETCH_FAILED', resolveGroupApiErrorMessage(json.error), { error: json.error });
|
||||
return;
|
||||
}
|
||||
if (!json.group) {
|
||||
reportError('UI_INVALID_STATE', groupMessageErrorGeneric, { reason: 'missing_group' });
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = json.mode === 'updated' ? 'updated' : 'created';
|
||||
const successMessage = mode === 'updated' ? groupMessageUpdated : groupMessageCreated;
|
||||
|
||||
closeGroupDialog();
|
||||
if (groupSource === 'bookmark') {
|
||||
upsertGroupOption(json.group);
|
||||
restoreBookmarkFromState(String(json.group.id));
|
||||
showAsyncFlash('success', successMessage, 2000);
|
||||
} else {
|
||||
runReloadWithSuccess(successMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
reportError('FETCH_ERROR', groupMessageErrorGeneric, { error });
|
||||
} finally {
|
||||
setGroupSubmitPending(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getAppBase() {
|
||||
const base = document.querySelector('base');
|
||||
return base ? base.href : '/';
|
||||
}
|
||||
|
||||
function currentRelativeUrl() {
|
||||
const baseEl = document.querySelector('base');
|
||||
const basePath = baseEl ? new URL(baseEl.href).pathname : '/';
|
||||
const full = window.location.pathname + window.location.search;
|
||||
if (full.startsWith(basePath)) {
|
||||
return full.slice(basePath.length);
|
||||
}
|
||||
return full.replace(/^\//, '');
|
||||
}
|
||||
@@ -14,21 +14,12 @@ if (config) {
|
||||
const labels = config.labels ?? {};
|
||||
|
||||
const toolbar = document.querySelector('#address-book-drawer-toolbar');
|
||||
const saveFilterButton = document.querySelector('[data-address-book-save-filter]');
|
||||
const saveFilterForm = document.querySelector('#address-book-save-filter-form');
|
||||
const customFilterInputs = Array.from(document.querySelectorAll('[data-address-book-custom-filter]'));
|
||||
const tenantDepartmentMap = toolbar?.dataset?.tenantDeptMap
|
||||
? JSON.parse(toolbar.dataset.tenantDeptMap)
|
||||
: {};
|
||||
|
||||
const uuidIndex = 0;
|
||||
const normalizeCommaSeparated = (value) => {
|
||||
if (Array.isArray(value)) {return value.filter(Boolean);}
|
||||
return String(value || '')
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const initialsForName = (name) => {
|
||||
const parts = String(name || '')
|
||||
@@ -139,71 +130,4 @@ if (config) {
|
||||
});
|
||||
});
|
||||
|
||||
const normalizeJoined = (value) => normalizeCommaSeparated(value).join(',');
|
||||
|
||||
if (saveFilterButton && saveFilterForm) {
|
||||
saveFilterButton.addEventListener('click', () => {
|
||||
if (!gridConfig) {
|
||||
return;
|
||||
}
|
||||
const promptText = saveFilterButton.dataset.saveFilterPrompt || '';
|
||||
const emptyText = saveFilterButton.dataset.saveFilterEmpty || '';
|
||||
const enteredName = window.prompt(promptText, '');
|
||||
if (enteredName === null) {
|
||||
return;
|
||||
}
|
||||
const name = String(enteredName).trim();
|
||||
if (!name) {
|
||||
if (emptyText) {
|
||||
window.alert(emptyText);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const appliedQuery = new URL(gridConfig.baseUrl()).searchParams;
|
||||
const state = {
|
||||
search: String(appliedQuery.get('search') || '').trim(),
|
||||
tenants: normalizeJoined(appliedQuery.get('tenants') || ''),
|
||||
departments: normalizeJoined(appliedQuery.get('departments') || ''),
|
||||
roles: normalizeJoined(appliedQuery.get('roles') || ''),
|
||||
custom: {},
|
||||
};
|
||||
appliedQuery.forEach((value, key) => {
|
||||
if (!/^(cf_|cfm_|cfd_)/i.test(key)) {
|
||||
return;
|
||||
}
|
||||
const cleanedValue = String(value || '').trim();
|
||||
if (cleanedValue === '') {
|
||||
return;
|
||||
}
|
||||
state.custom[key] = cleanedValue;
|
||||
});
|
||||
const nameInput = saveFilterForm.querySelector('input[name="name"]');
|
||||
const searchInput = saveFilterForm.querySelector('input[name="search"]');
|
||||
const tenantsInput = saveFilterForm.querySelector('input[name="tenants"]');
|
||||
const departmentsInput = saveFilterForm.querySelector('input[name="departments"]');
|
||||
const rolesInput = saveFilterForm.querySelector('input[name="roles"]');
|
||||
const extrasContainer = saveFilterForm.querySelector('[data-address-book-save-filter-extra]');
|
||||
if (!nameInput || !searchInput || !tenantsInput || !departmentsInput || !rolesInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
nameInput.value = name;
|
||||
searchInput.value = state.search;
|
||||
tenantsInput.value = state.tenants;
|
||||
departmentsInput.value = state.departments;
|
||||
rolesInput.value = state.roles;
|
||||
if (extrasContainer) {
|
||||
extrasContainer.innerHTML = '';
|
||||
Object.entries(state.custom || {}).forEach(([key, value]) => {
|
||||
const hidden = document.createElement('input');
|
||||
hidden.type = 'hidden';
|
||||
hidden.name = key;
|
||||
hidden.value = String(value ?? '');
|
||||
extrasContainer.appendChild(hidden);
|
||||
});
|
||||
}
|
||||
saveFilterForm.requestSubmit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user