935 lines
23 KiB
JavaScript
935 lines
23 KiB
JavaScript
// Remote AppSidebar.
|
|
// Visuals and interaction shell are self-contained in Shadow DOM.
|
|
// Only React/createRoot are borrowed from the host so React icon nodes can be rendered.
|
|
// Font family is inherited from the host application.
|
|
// Routing, permissions and logout logic stay in the host and arrive as callbacks/flags.
|
|
|
|
const REACT_REMOTE_DEPS_KEY = "__UIKIT_REACT_REMOTE_DEPS__";
|
|
const TAG_NAME = "uikit-app-sidebar";
|
|
|
|
const EXIT_ICON_SVG = `
|
|
<svg fill="currentColor" width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
|
|
<title>ionicons-v5-i</title>
|
|
<path d="M336,376V272H191a16,16,0,0,1,0-32H336V136a56.06,56.06,0,0,0-56-56H88a56.06,56.06,0,0,0-56,56V376a56.06,56.06,0,0,0,56,56H280A56.06,56.06,0,0,0,336,376Z"/>
|
|
<path d="M425.37,272l-52.68,52.69a16,16,0,0,0,22.62,22.62l80-80a16,16,0,0,0,0-22.62l-80-80a16,16,0,0,0-22.62,22.62L425.37,240H336v32Z"/>
|
|
</svg>`;
|
|
|
|
const MORE_ICON_SVG = `
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
|
|
<circle cx="5" cy="12" r="2"/>
|
|
<circle cx="12" cy="12" r="2"/>
|
|
<circle cx="19" cy="12" r="2"/>
|
|
</svg>`;
|
|
|
|
const CLOSE_ICON_SVG = `
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false">
|
|
<path d="M18 6 6 18"/>
|
|
<path d="m6 6 12 12"/>
|
|
</svg>`;
|
|
|
|
const CSS = `
|
|
:host {
|
|
box-sizing: border-box;
|
|
display: block;
|
|
width: 72px;
|
|
min-width: 72px;
|
|
max-width: 72px;
|
|
height: calc(100dvh - 110px);
|
|
min-height: calc(100dvh - 110px);
|
|
max-height: calc(100dvh - 110px);
|
|
flex: 0 0 72px;
|
|
font-family: inherit;
|
|
color: #1a202c;
|
|
direction: rtl;
|
|
}
|
|
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
|
|
.desktop-nav {
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 0;
|
|
padding: 16px 0;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
overscroll-behavior: contain;
|
|
background: #fff;
|
|
border-left: 1px solid #e2e8f0;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #cbd5e0 transparent;
|
|
}
|
|
|
|
.desktop-nav::-webkit-scrollbar { width: 4px; }
|
|
.desktop-nav::-webkit-scrollbar-track { background: transparent; }
|
|
.desktop-nav::-webkit-scrollbar-thumb {
|
|
border-radius: 999px;
|
|
background: #cbd5e0;
|
|
}
|
|
|
|
.items {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 6px;
|
|
width: 100%;
|
|
}
|
|
|
|
.item-wrap {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 56px;
|
|
height: 42px;
|
|
flex: 0 0 42px;
|
|
}
|
|
|
|
.item {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 56px;
|
|
height: 42px;
|
|
min-width: 56px;
|
|
min-height: 42px;
|
|
margin: 0;
|
|
padding: 0;
|
|
border: 0;
|
|
border-radius: 6px;
|
|
outline: 0;
|
|
background: transparent;
|
|
color: #718096;
|
|
text-decoration: none;
|
|
font: inherit;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
transition: background-color 120ms ease, box-shadow 120ms ease, opacity 120ms ease;
|
|
}
|
|
|
|
.item:hover:not([aria-disabled="true"]) { background: #edf2f7; }
|
|
.item[data-active="true"] { background: #e2e8f0; }
|
|
.item[data-active="true"]:hover { background: #e2e8f0; }
|
|
.item:focus-visible { box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.45); }
|
|
.item[aria-disabled="true"] { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
.icon {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 16px;
|
|
height: 16px;
|
|
font-size: 16px;
|
|
color: #314E89 !important;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.icon > *,
|
|
.icon svg {
|
|
display: block;
|
|
width: 16px;
|
|
height: 16px;
|
|
max-width: 16px;
|
|
max-height: 16px;
|
|
color: #314E89 !important;
|
|
}
|
|
|
|
.exit-icon svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
fill: currentColor;
|
|
stroke: none;
|
|
}
|
|
|
|
.floating-tooltip {
|
|
position: fixed;
|
|
z-index: 2147483000;
|
|
display: none;
|
|
min-width: max-content;
|
|
max-width: 280px;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
background: #2d3748;
|
|
color: #fff;
|
|
font-family: inherit;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
line-height: 1.2;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.14);
|
|
}
|
|
|
|
.floating-tooltip[data-open="true"] { display: block; }
|
|
|
|
.floating-tooltip::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 16px;
|
|
left: 100%;
|
|
width: 0;
|
|
height: 0;
|
|
border-top: 6px solid transparent;
|
|
border-bottom: 6px solid transparent;
|
|
border-left: 6px solid #2d3748;
|
|
}
|
|
|
|
.mobile-nav,
|
|
.drawer-layer {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 61.999em) {
|
|
:host {
|
|
display: block;
|
|
width: 0;
|
|
min-width: 0;
|
|
max-width: none;
|
|
height: 0;
|
|
min-height: 0;
|
|
flex: 0 0 0;
|
|
}
|
|
|
|
.desktop-nav,
|
|
.floating-tooltip {
|
|
display: none !important;
|
|
}
|
|
|
|
.mobile-nav {
|
|
position: fixed;
|
|
inset-inline: 0;
|
|
bottom: 0;
|
|
z-index: 1200;
|
|
display: block;
|
|
background: #fff;
|
|
border-top: 1px solid #e2e8f0;
|
|
box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.08);
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
|
|
.mobile-bar {
|
|
display: flex;
|
|
align-items: stretch;
|
|
width: 100%;
|
|
height: 56px;
|
|
padding-inline: 6px;
|
|
gap: 4px;
|
|
}
|
|
|
|
.mobile-slot {
|
|
display: flex;
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
align-items: center;
|
|
}
|
|
|
|
.mobile-control {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
display: flex;
|
|
width: 100%;
|
|
min-width: 0;
|
|
height: 100%;
|
|
margin: 0;
|
|
padding: 0;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 0;
|
|
outline: 0;
|
|
background: transparent;
|
|
color: #718096;
|
|
text-decoration: none;
|
|
font: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.mobile-control[aria-disabled="true"] {
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.mobile-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
min-width: 0;
|
|
gap: 2px;
|
|
padding: 4px;
|
|
border-radius: 12px;
|
|
color: #718096;
|
|
background: transparent;
|
|
opacity: 1;
|
|
transition: background-color 150ms ease, color 150ms ease;
|
|
}
|
|
|
|
.mobile-control[data-active="true"] .mobile-content {
|
|
color: #2b6cb0;
|
|
background: #ebf8ff;
|
|
}
|
|
|
|
.mobile-control[aria-disabled="true"] .mobile-content {
|
|
opacity: 0.4;
|
|
}
|
|
|
|
.mobile-control.mobile-exit .mobile-content {
|
|
color: #c53030;
|
|
}
|
|
|
|
.mobile-icon {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 16px;
|
|
height: 16px;
|
|
flex: 0 0 16px;
|
|
color: #314E89 !important;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.mobile-icon > *,
|
|
.mobile-icon svg {
|
|
display: block;
|
|
width: 16px;
|
|
height: 16px;
|
|
max-width: 16px;
|
|
max-height: 16px;
|
|
color: #314E89 !important;
|
|
}
|
|
|
|
.mobile-label {
|
|
display: block;
|
|
width: 100%;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: currentColor;
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
line-height: 1.375;
|
|
text-align: center;
|
|
}
|
|
|
|
.mobile-control[data-active="true"] .mobile-label {
|
|
font-weight: 700;
|
|
}
|
|
|
|
.drawer-layer {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 1300;
|
|
display: none;
|
|
align-items: flex-end;
|
|
justify-content: stretch;
|
|
}
|
|
|
|
.drawer-layer[data-open="true"] {
|
|
display: flex;
|
|
}
|
|
|
|
.drawer-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.48);
|
|
}
|
|
|
|
.drawer-panel {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
max-height: min(78dvh, 620px);
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
overflow: hidden;
|
|
border-radius: 16px 16px 0 0;
|
|
background: #fff;
|
|
box-shadow: 0 -12px 32px rgba(0, 0, 0, 0.16);
|
|
}
|
|
|
|
.drawer-header {
|
|
position: relative;
|
|
display: flex;
|
|
min-height: 52px;
|
|
padding: 14px 48px 14px 16px;
|
|
align-items: center;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
color: #1a202c;
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.drawer-close {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
position: absolute;
|
|
top: 10px;
|
|
inset-inline-end: 10px;
|
|
display: inline-flex;
|
|
width: 32px;
|
|
height: 32px;
|
|
padding: 0;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 0;
|
|
border-radius: 8px;
|
|
background: transparent;
|
|
color: #4a5568;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.drawer-body {
|
|
padding: 16px;
|
|
overflow-y: auto;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
.drawer-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 8px;
|
|
}
|
|
|
|
.drawer-item-wrap {
|
|
min-width: 0;
|
|
}
|
|
|
|
.drawer-item {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
display: flex;
|
|
width: 100%;
|
|
height: 70px;
|
|
min-width: 0;
|
|
padding: 4px;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
background: #fff;
|
|
color: #4a5568;
|
|
text-decoration: none;
|
|
font: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.drawer-item[data-active="true"] {
|
|
border-color: #63b3ed;
|
|
background: #ebf8ff;
|
|
color: #2c5282;
|
|
}
|
|
|
|
.drawer-item[aria-disabled="true"] {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.drawer-item .mobile-label {
|
|
display: -webkit-box;
|
|
overflow: hidden;
|
|
white-space: normal;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
font-size: 10px;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.drawer-item[data-active="true"] .mobile-label {
|
|
font-weight: 700;
|
|
}
|
|
|
|
.drawer-exit-section {
|
|
margin-top: 16px;
|
|
padding-top: 16px;
|
|
border-top: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.drawer-exit {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
display: flex;
|
|
width: 100%;
|
|
min-height: 44px;
|
|
padding: 12px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
border: 0;
|
|
border-radius: 12px;
|
|
background: #fff5f5;
|
|
color: #c53030;
|
|
font: inherit;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.drawer-exit[aria-disabled="true"] {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 62em) {
|
|
.mobile-nav,
|
|
.drawer-layer {
|
|
display: none !important;
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.item,
|
|
.mobile-content { transition: none; }
|
|
}
|
|
`;
|
|
|
|
function getReactDeps() {
|
|
if (typeof window === "undefined") return {};
|
|
return window[REACT_REMOTE_DEPS_KEY] || {};
|
|
}
|
|
|
|
function createElement(tag, className) {
|
|
const element = document.createElement(tag);
|
|
if (className) element.className = className;
|
|
return element;
|
|
}
|
|
|
|
function setIconMarkup(container, markup) {
|
|
container.innerHTML = markup || "";
|
|
}
|
|
|
|
class AppSidebarElement extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this._props = {};
|
|
this._iconRoots = [];
|
|
this._shadow = this.attachShadow({ mode: "open" });
|
|
this._drawerOpen = false;
|
|
this._moreButton = null;
|
|
|
|
this._shadow.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape" && this._drawerOpen) {
|
|
this.setDrawerOpen(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
this.cleanupIconRoots();
|
|
}
|
|
|
|
set componentProps(value) {
|
|
this._props = value || {};
|
|
if (this.isConnected) this.render();
|
|
}
|
|
|
|
get componentProps() {
|
|
return this._props;
|
|
}
|
|
|
|
cleanupIconRoots() {
|
|
for (const root of this._iconRoots) {
|
|
try {
|
|
root.unmount();
|
|
} catch (_) {}
|
|
}
|
|
this._iconRoots = [];
|
|
}
|
|
|
|
mountReactIcon(container, icon) {
|
|
const { React, createRoot } = getReactDeps();
|
|
if (!React || !createRoot || !React.isValidElement?.(icon)) return false;
|
|
|
|
const root = createRoot(container);
|
|
root.render(icon);
|
|
this._iconRoots.push(root);
|
|
return true;
|
|
}
|
|
|
|
renderIcon(container, item) {
|
|
const icon = item?.icon;
|
|
|
|
if (this.mountReactIcon(container, icon)) return;
|
|
|
|
if (typeof item?.iconSvg === "string") {
|
|
setIconMarkup(container, item.iconSvg);
|
|
return;
|
|
}
|
|
|
|
if (typeof icon === "string" && icon.trim().startsWith("<svg")) {
|
|
setIconMarkup(container, icon);
|
|
}
|
|
}
|
|
|
|
getItemLabel(item) {
|
|
return String(item?.mobileTitle || item?.title || "");
|
|
}
|
|
|
|
hideTooltip() {
|
|
if (!this._tooltip) return;
|
|
this._tooltip.dataset.open = "false";
|
|
}
|
|
|
|
showTooltip(control, label) {
|
|
if (!this._tooltip || !control || !label) return;
|
|
|
|
this._tooltip.textContent = label;
|
|
this._tooltip.dataset.open = "true";
|
|
this._tooltip.style.left = "0px";
|
|
this._tooltip.style.top = "0px";
|
|
|
|
const rect = control.getBoundingClientRect();
|
|
const tooltipRect = this._tooltip.getBoundingClientRect();
|
|
const left = Math.max(8, rect.left - tooltipRect.width - 10);
|
|
const top = Math.max(8, Math.min(rect.top, window.innerHeight - tooltipRect.height - 8));
|
|
|
|
this._tooltip.style.left = `${left}px`;
|
|
this._tooltip.style.top = `${top}px`;
|
|
}
|
|
|
|
bindTooltip(wrap, control, label) {
|
|
const show = () => this.showTooltip(control, label);
|
|
const hide = () => this.hideTooltip();
|
|
|
|
wrap.addEventListener("mouseenter", show);
|
|
wrap.addEventListener("mouseleave", hide);
|
|
control.addEventListener("focus", show);
|
|
control.addEventListener("blur", hide);
|
|
}
|
|
|
|
createItemControl(item, index, className, onActivate) {
|
|
const disabled = Boolean(item?.disabled);
|
|
const active = Boolean(item?.active);
|
|
const hasClick = typeof item?.onClick === "function";
|
|
const hasHref = typeof item?.href === "string" && item.href.length > 0;
|
|
const control = createElement(hasHref && !hasClick ? "a" : "button", className);
|
|
|
|
if (control.tagName === "BUTTON") {
|
|
control.type = "button";
|
|
} else {
|
|
control.href = item.href;
|
|
if (item.target) control.target = item.target;
|
|
if (item.rel) control.rel = item.rel;
|
|
}
|
|
|
|
control.setAttribute("aria-label", String(item?.title || `آیتم ${index + 1}`));
|
|
control.setAttribute("aria-disabled", disabled ? "true" : "false");
|
|
control.dataset.active = active ? "true" : "false";
|
|
|
|
if (active) control.setAttribute("aria-current", "page");
|
|
if (disabled) control.tabIndex = -1;
|
|
|
|
control.addEventListener("click", (event) => {
|
|
if (disabled) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
|
|
if (hasClick) {
|
|
item.onClick(item, event);
|
|
}
|
|
|
|
if (typeof onActivate === "function") {
|
|
onActivate(item, event);
|
|
}
|
|
});
|
|
|
|
return control;
|
|
}
|
|
|
|
createNavItem(item, index) {
|
|
const wrap = createElement("div", "item-wrap");
|
|
const disabled = Boolean(item?.disabled);
|
|
const label = disabled ? "عدم دسترسی" : String(item?.title || "");
|
|
const control = this.createItemControl(item, index, "item");
|
|
|
|
const icon = createElement("span", "icon");
|
|
control.appendChild(icon);
|
|
this.renderIcon(icon, item);
|
|
|
|
wrap.appendChild(control);
|
|
this.bindTooltip(wrap, control, label);
|
|
return wrap;
|
|
}
|
|
|
|
createExitItem() {
|
|
const { exitLabel = "خروج", onExit, exitDisabled = false } = this._props;
|
|
const wrap = createElement("div", "item-wrap");
|
|
const control = createElement("button", "item");
|
|
control.type = "button";
|
|
control.setAttribute("aria-label", exitLabel);
|
|
control.setAttribute("aria-disabled", exitDisabled ? "true" : "false");
|
|
if (exitDisabled) control.tabIndex = -1;
|
|
|
|
control.addEventListener("click", (event) => {
|
|
if (exitDisabled) return;
|
|
if (typeof onExit === "function") onExit(event);
|
|
});
|
|
|
|
const icon = createElement("span", "icon exit-icon");
|
|
icon.innerHTML = EXIT_ICON_SVG;
|
|
control.appendChild(icon);
|
|
|
|
wrap.appendChild(control);
|
|
this.bindTooltip(
|
|
wrap,
|
|
control,
|
|
exitDisabled ? "عدم دسترسی" : exitLabel,
|
|
);
|
|
return wrap;
|
|
}
|
|
|
|
createMobileContent(item, isExit = false) {
|
|
const content = createElement("span", "mobile-content");
|
|
const icon = createElement("span", `mobile-icon${isExit ? " exit-icon" : ""}`);
|
|
|
|
if (isExit) {
|
|
icon.innerHTML = EXIT_ICON_SVG;
|
|
} else {
|
|
this.renderIcon(icon, item);
|
|
}
|
|
|
|
const label = createElement("span", "mobile-label");
|
|
label.textContent = isExit
|
|
? String(this._props.exitLabel || "خروج")
|
|
: this.getItemLabel(item);
|
|
|
|
content.appendChild(icon);
|
|
content.appendChild(label);
|
|
return content;
|
|
}
|
|
|
|
createMobileItem(item, index, { drawer = false } = {}) {
|
|
const wrap = createElement("div", drawer ? "drawer-item-wrap" : "mobile-slot");
|
|
const control = this.createItemControl(
|
|
item,
|
|
index,
|
|
drawer ? "drawer-item" : "mobile-control",
|
|
drawer ? () => this.setDrawerOpen(false) : undefined,
|
|
);
|
|
|
|
control.appendChild(this.createMobileContent(item));
|
|
wrap.appendChild(control);
|
|
return wrap;
|
|
}
|
|
|
|
createMobileExit({ drawer = false } = {}) {
|
|
const { exitLabel = "خروج", onExit, exitDisabled = false } = this._props;
|
|
|
|
if (drawer) {
|
|
const section = createElement("div", "drawer-exit-section");
|
|
const control = createElement("button", "drawer-exit");
|
|
control.type = "button";
|
|
control.setAttribute("aria-label", exitLabel);
|
|
control.setAttribute("aria-disabled", exitDisabled ? "true" : "false");
|
|
if (exitDisabled) control.tabIndex = -1;
|
|
|
|
const icon = createElement("span", "mobile-icon exit-icon");
|
|
icon.innerHTML = EXIT_ICON_SVG;
|
|
const label = createElement("span");
|
|
label.textContent = exitLabel === "خروج" ? "خروج از سامانه" : exitLabel;
|
|
control.appendChild(icon);
|
|
control.appendChild(label);
|
|
|
|
control.addEventListener("click", (event) => {
|
|
if (exitDisabled) return;
|
|
this.setDrawerOpen(false);
|
|
if (typeof onExit === "function") onExit(event);
|
|
});
|
|
|
|
section.appendChild(control);
|
|
return section;
|
|
}
|
|
|
|
const wrap = createElement("div", "mobile-slot");
|
|
const control = createElement("button", "mobile-control mobile-exit");
|
|
control.type = "button";
|
|
control.setAttribute("aria-label", exitLabel);
|
|
control.setAttribute("aria-disabled", exitDisabled ? "true" : "false");
|
|
control.dataset.active = "false";
|
|
if (exitDisabled) control.tabIndex = -1;
|
|
|
|
control.appendChild(this.createMobileContent(null, true));
|
|
control.addEventListener("click", (event) => {
|
|
if (exitDisabled) return;
|
|
if (typeof onExit === "function") onExit(event);
|
|
});
|
|
|
|
wrap.appendChild(control);
|
|
return wrap;
|
|
}
|
|
|
|
createMoreButton(isActive) {
|
|
const wrap = createElement("div", "mobile-slot");
|
|
const control = createElement("button", "mobile-control");
|
|
control.type = "button";
|
|
control.setAttribute("aria-label", "نمایش سایر بخشها");
|
|
control.setAttribute("aria-haspopup", "dialog");
|
|
control.setAttribute("aria-expanded", this._drawerOpen ? "true" : "false");
|
|
control.dataset.active = isActive ? "true" : "false";
|
|
|
|
const content = createElement("span", "mobile-content");
|
|
const icon = createElement("span", "mobile-icon");
|
|
icon.innerHTML = MORE_ICON_SVG;
|
|
const label = createElement("span", "mobile-label");
|
|
label.textContent = "بیشتر";
|
|
content.appendChild(icon);
|
|
content.appendChild(label);
|
|
control.appendChild(content);
|
|
|
|
control.addEventListener("click", () => this.setDrawerOpen(true));
|
|
wrap.appendChild(control);
|
|
this._moreButton = control;
|
|
return wrap;
|
|
}
|
|
|
|
createDrawer(secondaryItems) {
|
|
const layer = createElement("div", "drawer-layer");
|
|
layer.dataset.open = this._drawerOpen ? "true" : "false";
|
|
|
|
const overlay = createElement("div", "drawer-overlay");
|
|
overlay.addEventListener("click", () => this.setDrawerOpen(false));
|
|
|
|
const panel = createElement("section", "drawer-panel");
|
|
panel.setAttribute("role", "dialog");
|
|
panel.setAttribute("aria-modal", "true");
|
|
panel.setAttribute("aria-label", "سایر بخشهای سامانه");
|
|
|
|
const header = createElement("div", "drawer-header");
|
|
header.textContent = "سایر بخشهای سامانه";
|
|
const close = createElement("button", "drawer-close");
|
|
close.type = "button";
|
|
close.setAttribute("aria-label", "بستن");
|
|
close.innerHTML = CLOSE_ICON_SVG;
|
|
close.addEventListener("click", () => this.setDrawerOpen(false));
|
|
header.appendChild(close);
|
|
|
|
const body = createElement("div", "drawer-body");
|
|
const grid = createElement("div", "drawer-grid");
|
|
|
|
secondaryItems.forEach((item, index) => {
|
|
grid.appendChild(this.createMobileItem(item, index + 4, { drawer: true }));
|
|
});
|
|
|
|
body.appendChild(grid);
|
|
|
|
if (this._props.showExit) {
|
|
body.appendChild(this.createMobileExit({ drawer: true }));
|
|
}
|
|
|
|
panel.appendChild(header);
|
|
panel.appendChild(body);
|
|
layer.appendChild(overlay);
|
|
layer.appendChild(panel);
|
|
|
|
this._drawerLayer = layer;
|
|
return layer;
|
|
}
|
|
|
|
setDrawerOpen(open) {
|
|
this._drawerOpen = Boolean(open);
|
|
|
|
if (this._drawerLayer) {
|
|
this._drawerLayer.dataset.open = this._drawerOpen ? "true" : "false";
|
|
}
|
|
|
|
if (this._moreButton) {
|
|
this._moreButton.setAttribute("aria-expanded", this._drawerOpen ? "true" : "false");
|
|
}
|
|
|
|
if (!this._drawerOpen && this._moreButton) {
|
|
this._moreButton.focus({ preventScroll: true });
|
|
}
|
|
}
|
|
|
|
renderDesktop(items) {
|
|
const nav = createElement("nav", "desktop-nav");
|
|
nav.setAttribute("aria-label", this._props.ariaLabel || "ناوبری اصلی");
|
|
nav.dir = this._props.dir === "ltr" ? "ltr" : "rtl";
|
|
|
|
const itemsContainer = createElement("div", "items");
|
|
|
|
items.forEach((item, index) => {
|
|
itemsContainer.appendChild(this.createNavItem(item, index));
|
|
});
|
|
|
|
if (this._props.showExit) {
|
|
itemsContainer.appendChild(this.createExitItem());
|
|
}
|
|
|
|
nav.appendChild(itemsContainer);
|
|
return nav;
|
|
}
|
|
|
|
renderMobile(items) {
|
|
const mobileNav = createElement("nav", "mobile-nav");
|
|
mobileNav.setAttribute("aria-label", `${this._props.ariaLabel || "ناوبری اصلی"} موبایل`);
|
|
mobileNav.dir = this._props.dir === "ltr" ? "ltr" : "rtl";
|
|
|
|
const bar = createElement("div", "mobile-bar");
|
|
const hasOverflow = items.length > 4;
|
|
const primaryItems = hasOverflow ? items.slice(0, 4) : items;
|
|
const secondaryItems = hasOverflow ? items.slice(4) : [];
|
|
|
|
primaryItems.forEach((item, index) => {
|
|
bar.appendChild(this.createMobileItem(item, index));
|
|
});
|
|
|
|
if (hasOverflow) {
|
|
const overflowActive = secondaryItems.some((item) => Boolean(item?.active));
|
|
bar.appendChild(this.createMoreButton(overflowActive));
|
|
} else if (this._props.showExit) {
|
|
bar.appendChild(this.createMobileExit());
|
|
}
|
|
|
|
mobileNav.appendChild(bar);
|
|
|
|
return {
|
|
mobileNav,
|
|
drawer: hasOverflow ? this.createDrawer(secondaryItems) : null,
|
|
};
|
|
}
|
|
|
|
render() {
|
|
this.cleanupIconRoots();
|
|
this._shadow.replaceChildren();
|
|
this._drawerOpen = false;
|
|
this._drawerLayer = null;
|
|
this._moreButton = null;
|
|
|
|
const style = document.createElement("style");
|
|
style.textContent = CSS;
|
|
this._shadow.appendChild(style);
|
|
|
|
const items = Array.isArray(this._props.items)
|
|
? this._props.items.filter((item) => item && !item.hidden)
|
|
: [];
|
|
|
|
this._shadow.appendChild(this.renderDesktop(items));
|
|
|
|
const { mobileNav, drawer } = this.renderMobile(items);
|
|
this._shadow.appendChild(mobileNav);
|
|
if (drawer) this._shadow.appendChild(drawer);
|
|
|
|
this._tooltip = createElement("span", "floating-tooltip");
|
|
this._tooltip.setAttribute("role", "tooltip");
|
|
this._tooltip.dataset.open = "false";
|
|
this._shadow.appendChild(this._tooltip);
|
|
}
|
|
}
|
|
|
|
if (!customElements.get(TAG_NAME)) {
|
|
customElements.define(TAG_NAME, AppSidebarElement);
|
|
}
|
|
|
|
export { AppSidebarElement };
|