@moonitoring/nidamjs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/nidam.cjs +1 -0
- package/dist/nidam.css +1 -0
- package/dist/nidam.es.js +1096 -0
- package/dist/nidam.umd.js +1 -0
- package/package.json +46 -0
- package/readme.md +175 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(f,y){typeof exports=="object"&&typeof module<"u"?y(exports):typeof define=="function"&&define.amd?define(["exports"],y):(f=typeof globalThis<"u"?globalThis:f||self,y(f.Nidam={}))})(this,(function(f){"use strict";class y{static#t=new WeakSet;static initialize(t,e=document,s=null,i=[]){!Array.isArray(i)||i.length===0||i.forEach(({selector:o,init:n,name:a})=>{if(!o||typeof n!="function")return;e.querySelectorAll(o).forEach(l=>{if(!this.#t.has(l))try{const c=n(l,t,s);this.#t.add(l),a&&c&&s&&typeof s.set=="function"&&s.set(a,c)}catch(c){console.warn(`Failed to initialize ${o}:`,c)}})})}}class b{#t=new Map;#e;#s=new Map;constructor(t=document){this.#e=t,this.#i()}#i(){["click","input","change","focusin","focusout","keydown","mousedown","desktop:toggle-matrix","desktop:theme-changed"].forEach(e=>{const s=i=>this.#o(e,i);this.#e.addEventListener(e,s),this.#s.set(e,s)})}#o(t,e){const s=this.#t.get(t);if(!(!s||s.length===0))for(const{selector:i,handler:o}of s){if(!i){o.call(this.#e,e,this.#e);continue}const n=e.target.closest(i);n&&o.call(n,e,n)}}on(t,e,s,i={}){const o=i.group;this.#t.has(t)||this.#t.set(t,[]);const n={selector:e,handler:s,group:o};return this.#t.get(t).push(n),n}off(t,e){const s=this.#t.get(t);if(!s)return;const i=s.indexOf(e);i!==-1&&s.splice(i,1),s.length||this.#t.delete(t)}destroy(){for(const[t,e]of this.#s)this.#e.removeEventListener(t,e);this.#s.clear(),this.#t.clear()}}const R={moveCursorToEnd(r){const t=document.createRange();t.selectNodeContents(r),t.collapse(!1);const e=window.getSelection();e.removeAllRanges(),e.addRange(t)},onEscape(r){const t=e=>{e.key==="Escape"&&(r(),document.removeEventListener("keydown",t))};return document.addEventListener("keydown",t),t},hide(...r){r.forEach(t=>t?.classList.add("hidden"))},show(...r){r.forEach(t=>t?.classList.remove("hidden"))},toggle(...r){r.forEach(t=>t?.classList.toggle("hidden"))}};class W{_root;_elements={};_state={};_utils=R;_delegator=null;constructor(t,e){if(this._root=typeof t=="string"?document.querySelector(t):t,this._delegator=e,!this._root)throw new Error(`Root element not found: ${t}`);this._initialize()}_initialize(){this._elements=this._cacheElements(),this._bindEvents()}_cacheElements(){return{}}_bindEvents(){}_query(t){return this._root.querySelector(t)}_queryAll(t){return this._root.querySelectorAll(t)}_on(t,e,s){this._delegator.on(t,e,(i,o)=>{this._root.contains(o)&&s.call(this,i,o)})}}const T=new WeakMap;function p(r){return!r||r==="auto"||r==="normal"?"":r}function S(r){return Number.isFinite(r)?`${Math.round(r)}px`:""}function C(r,t={}){const e=t.includePosition===!0;let s=null;const i=()=>(s||(s=window.getComputedStyle(r)),s),o=(r.offsetWidth>0?S(r.offsetWidth):"")||p(r.style.width)||p(i().width),n=(r.offsetHeight>0?S(r.offsetHeight):"")||p(r.style.height)||p(i().height);let a="",d="";return e&&(a=S(r.offsetLeft)||p(r.style.left)||p(i().left),d=S(r.offsetTop)||p(r.style.top)||p(i().top)),{width:o||"",height:n||"",left:a,top:d}}function w(r){let t=T.get(r);return t||(t=new Map,T.set(r,t)),t}function D(r,t="prevState",e={}){const s=C(r,e),i=JSON.stringify(s);return r.dataset[t]=i,w(r).set(t,{raw:i,parsed:s}),s}function v(r,t="prevState"){const e=r.dataset[t];if(!e)return null;const s=w(r).get(t);if(s&&s.raw===e)return s.parsed;try{const i=JSON.parse(e);if(!i||typeof i!="object")return w(r).set(t,{raw:e,parsed:null}),null;const o={width:p(i.width),height:p(i.height),left:p(i.left),top:p(i.top)};return w(r).set(t,{raw:e,parsed:o}),o}catch{return w(r).set(t,{raw:e,parsed:null}),null}}function L(r,t,e={}){return!t||typeof t!="object"?!1:(t.width&&r.style.width!==t.width&&(r.style.width=t.width),t.height&&r.style.height!==t.height&&(r.style.height=t.height),e.includePosition&&(t.left&&r.style.left!==t.left&&(r.style.left=t.left),t.top&&r.style.top!==t.top&&(r.style.top=t.top)),!0)}class I extends W{_config={zIndexBase:40,layoutStabilizationMs:450,cascadeOffset:30,cooldownMs:500,maxWindows:10,snapGap:6,taskbarHeight:64,snapThreshold:30,dragThreshold:10,resizeDebounceMs:6,animationDurationMs:400,defaultWidth:800,defaultHeight:600,minMargin:10,edgeDetectionRatio:.4,scrollRestoreTimeoutMs:2e3};_windows=new Map;_zIndexCounter=this._config.zIndexBase;_getModules=null;_notify=null;_fetchWindowContent=null;_initializeContent=null;_resolveEndpoint=null;_lastOpenTimestamps=new Map;_pendingRequests=new Map;_snapIndicator=null;constructor(t,e,s={}){super(t,e);const{getModules:i=null,config:o=null,notify:n=null,fetchWindowContent:a=null,initializeContent:d=null,resolveEndpoint:l=null}=s||{};this._getModules=i,this._notify=n||this._defaultNotify.bind(this),this._fetchWindowContent=a||this._defaultFetchWindowContent.bind(this),this._initializeContent=d||(()=>{}),this._resolveEndpoint=l||this._defaultResolveEndpoint,o&&typeof o=="object"&&(this._config={...this._config,...o}),this._zIndexCounter=this._config.zIndexBase,this._initSnapIndicator()}_initSnapIndicator(){this._snapIndicator=document.createElement("div"),this._snapIndicator.className="snap-indicator",document.body.appendChild(this._snapIndicator)}_cacheElements(){return{}}_bindEvents(){this._delegator.on("click","[data-modal]",this._handleModalTrigger.bind(this)),this._delegator.on("click","[data-maximize]",this._handleMaximizeTrigger.bind(this)),this._delegator.on("click","[data-close]",this._handleCloseTrigger.bind(this)),this._delegator.on("mousedown",".window",this._handleWindowFocus.bind(this)),this._delegator.on("mousedown","[data-bar]",this._handleWindowDragStart.bind(this)),document.addEventListener("keydown",this._handleGlobalKeydown.bind(this));let t;window.addEventListener("resize",()=>{clearTimeout(t),t=setTimeout(()=>this._handleResize(),this._config.resizeDebounceMs)})}_handleModalTrigger(t,e){t.preventDefault(),this.open(e.dataset.modal).catch(s=>{console.debug("Modal trigger failed:",s)})}_handleCloseTrigger(t,e){t.preventDefault();const s=e.closest(".window");s&&this.close(s)}_handleWindowFocus(t,e){if(t.target.closest("[data-close]")||t.target.closest("[data-modal]"))return;const s=e.closest(".window");s&&this._focusWindow(s)}_handleMaximizeTrigger(t,e){t.preventDefault();const s=e.closest(".window");s&&this.toggleMaximize(s)}_handleWindowDragStart(t,e){if(t.target.closest("[data-close]")||t.target.closest("[data-maximize]"))return;t.preventDefault();const s=e.closest(".window");s&&(this._focusWindow(s),this.drag(t,s))}_handleGlobalKeydown(t){t.key==="Escape"&&!t.repeat&&this._closeTopmostWindow()}toggleMaximize(t){const e=t.classList.contains("maximized"),s=t.classList.contains("tiled")&&typeof t.dataset.snapType=="string"&&t.dataset.snapType.length>0;t.classList.add("window-toggling"),!e&&!t.classList.contains("tiled")&&this._ensureRestoreState(t);const i=t.classList.toggle("maximized");let o=!1;if(this._updateMaximizeIcon(t,i),!i)if(s){const n=this._getSnapLayout(t.dataset.snapType,window.innerWidth,window.innerHeight-this._config.taskbarHeight);Object.assign(t.style,n)}else{const n=v(t);L(t,n);const a=this._parseCssPixelValue(n?.width)||this._parseCssPixelValue(t.style.width)||t.offsetWidth,d=this._parseCssPixelValue(n?.height)||this._parseCssPixelValue(t.style.height)||t.offsetHeight;this._repositionWindowFromRatios(t,window.innerWidth,window.innerHeight,{widthPx:a,heightPx:d}),o=!0}setTimeout(()=>{t.classList.remove("window-toggling"),o&&this._savePositionRatios(t)},this._config.animationDurationMs)}async open(t,e=!1,s=null,i=!0){if(this._windows.size>=this._config.maxWindows&&!this._windows.has(t)){const a=document.body.dataset.errorMaxWindows||`Maximum of ${this._config.maxWindows} windows allowed.`;return this._notify("error",a.replace("%s",String(this._config.maxWindows))),Promise.reject(new Error("Max windows reached"))}if(this._windows.has(t)&&!e){const a=this._windows.get(t);return i&&this._focusWindow(a),Promise.resolve(a)}if(this._pendingRequests.has(t))return this._pendingRequests.get(t);const o=Date.now();if(!e&&o-(this._lastOpenTimestamps.get(t)||0)<this._config.cooldownMs)return Promise.resolve();this._lastOpenTimestamps.set(t,o);const n=(async()=>{try{const a=await this._fetchWindowContent(t,{force:e,focusSelector:s,activate:i,manager:this});if(typeof a!="string")throw new TypeError("fetchWindowContent must return an HTML string");if(this._windows.has(t)&&e){const l=this._windows.get(t);return!i&&this._isWindowBusy(l)||(this._refreshWindowContent(l,a),i&&this._focusWindow(l),s&&this._handleFocusSelector(l,s)),l}const d=this._createWindowElement(a,t);if(!d){console.warn(`No .window element found for ${t}`);return}return this._setupNewWindow(d,t,s,i),d}catch(a){console.error("Error opening window:",a);const d=document.body.dataset.errorOpenFailed||"Failed to open window.";throw this._notify("error",d),a}finally{this._pendingRequests.delete(t)}})();return this._pendingRequests.set(t,n),n}close(t){const e=t.dataset.endpoint;this._windows.get(e)===t&&this._windows.delete(e),t.classList.add("animate-disappearance"),t.classList.remove("animate-appearance"),t.addEventListener("animationend",()=>{t.isConnected&&t.remove()},{once:!0})}_focusWindow(t){this._zIndexCounter++,t.style.zIndex=this._zIndexCounter,t.classList.add("focused"),this._windows.forEach(e=>{e!==t&&e.classList.remove("focused")})}_isWindowBusy(t){return t.dataset.isBusy==="true"?!0:t.querySelector('[data-is-busy="true"]')!==null}_refreshWindowContent(t,e){const o=new DOMParser().parseFromString(e,"text/html").querySelector(".window");if(!o)return;const n=t.dataset.snapType,a=t.dataset.prevState,d=t.dataset.xRatio,l=t.dataset.yRatio,c=t.classList.contains("focused"),h=t.classList.contains("maximized"),g=t.classList.contains("tiled"),u=this._captureScrollState(t);t.innerHTML=o.innerHTML,t.className=o.className,n&&(t.dataset.snapType=n),a&&(t.dataset.prevState=a),d&&(t.dataset.xRatio=d),l&&(t.dataset.yRatio=l),c&&t.classList.add("focused"),g&&t.classList.add("tiled"),h&&(t.classList.add("maximized"),this._updateMaximizeIcon(t,!0)),!g&&!h&&(o.style.width&&(t.style.width=o.style.width),o.style.height&&(t.style.height=o.style.height)),t.style.margin="0",t.style.transform="none",this._restoreScrollState(t,u),this._initializeModalContent(t)}_updateMaximizeIcon(t,e){const s=t.querySelector("[data-maximize] i");s&&(s.classList.toggle("fa-expand",!e),s.classList.toggle("fa-compress",e))}_createWindowElement(t,e){const o=new DOMParser().parseFromString(t,"text/html").querySelector(".window");return o&&(o.dataset.endpoint=e),o}_setupNewWindow(t,e,s,i=!0){Object.assign(t.style,{position:"absolute",pointerEvents:"auto",margin:"0",transform:"none",visibility:"hidden"}),this._root.appendChild(t);const o=this._windows.size,n=t.dataset.defaultSnap;if(n){const a=window.innerWidth,d=window.innerHeight-this._config.taskbarHeight;this._snapWindow(t,n,a,d)}else this._positionWindow(t,o),this._ensureRestoreState(t);this._windows.set(e,t),this._initializeModalContent(t),i&&this._focusWindow(t),t.style.visibility="",n||this._stabilizeInitialPlacement(t,o),s&&this._handleFocusSelector(t,s)}_positionWindow(t,e=null){const s=t.offsetWidth||parseInt(t.style.width)||this._config.defaultWidth,i=t.offsetHeight||parseInt(t.style.height)||this._config.defaultHeight,o=window.innerWidth,n=window.innerHeight,a=Number.isFinite(e)&&e>=0?e:this._windows.size,d=a*this._config.cascadeOffset,l=a*this._config.cascadeOffset;let c=(o-s)/2+d,h=(n-i)/2+l;const g=this._config.minMargin;c+s>o&&(c=Math.max(g,o-s-g)),h+i>n&&(h=Math.max(g,n-i-g)),t.style.left=`${Math.round(c)}px`,t.style.top=`${Math.round(h)}px`,this._savePositionRatios(t)}_stabilizeInitialPlacement(t,e){if(!t?.isConnected)return;const s=Number.isFinite(this._config.layoutStabilizationMs)&&this._config.layoutStabilizationMs>0?this._config.layoutStabilizationMs:450,i=typeof performance<"u"?()=>performance.now():()=>Date.now(),o=i();let n=!0,a=null,d=t.offsetWidth,l=t.offsetHeight;const c=()=>{n&&(n=!1,a&&(a.disconnect(),a=null))},h=()=>{if(!n||!t.isConnected){c();return}if(t.classList.contains("tiled")||t.classList.contains("maximized"))return;const u=t.offsetWidth,m=t.offsetHeight;(u!==d||m!==l)&&(d=u,l=m,this._positionWindow(t,e))},g=()=>{if(n){if(h(),i()-o<s){requestAnimationFrame(g);return}c()}};requestAnimationFrame(g),typeof ResizeObserver=="function"&&(a=new ResizeObserver(()=>h()),a.observe(t)),setTimeout(()=>c(),s)}_savePositionRatios(t){if(t.classList.contains("tiled")||t.classList.contains("maximized"))return;const e=t.offsetLeft+t.offsetWidth/2,s=t.offsetTop+t.offsetHeight/2;t.dataset.xRatio=String(e/window.innerWidth),t.dataset.yRatio=String(s/window.innerHeight)}_ensureRestoreState(t){const e=v(t);return e?.width&&e?.height?e:D(t,"prevState",{includePosition:!1})}_parseCssPixelValue(t){if(!t)return null;const e=parseFloat(t);return Number.isFinite(e)?e:null}_repositionWindowFromRatios(t,e,s,i=null){const o=parseFloat(t.dataset.xRatio),n=parseFloat(t.dataset.yRatio);if(isNaN(o)||isNaN(n))return!1;const a=(i&&Number.isFinite(i.widthPx)&&i.widthPx>0?i.widthPx:null)||t.offsetWidth,d=(i&&Number.isFinite(i.heightPx)&&i.heightPx>0?i.heightPx:null)||t.offsetHeight,l=o*e,c=n*s;return t.style.left=`${Math.round(l-a/2)}px`,t.style.top=`${Math.round(c-d/2)}px`,!0}_handleFocusSelector(t,e){const s=t.querySelector(e);s&&((s.type==="radio"||s.type==="checkbox")&&(s.checked=!0),s.focus())}_closeTopmostWindow(){let t=null,e=0;this._windows.forEach(s=>{if(s.classList.contains("animate-disappearance"))return;const i=parseInt(s.style.zIndex||0,10);i>e&&(e=i,t=s)}),t&&this.close(t)}drag(t,e){this._dragState?.active||(this._dragState={active:!0,winElement:e,startX:t.clientX,startY:t.clientY,currentX:t.clientX,currentY:t.clientY,startWinLeft:e.offsetLeft,startWinTop:e.offsetTop,isRestored:!1,restoreXRatio:null,initialState:{tiled:e.classList.contains("tiled"),maximized:e.classList.contains("maximized")},view:{w:window.innerWidth,h:window.innerHeight-this._config.taskbarHeight},snap:null,inhibitSnap:!1,isDragging:!1},this._dragHandlers={move:s=>{this._dragState.currentX=s.clientX,this._dragState.currentY=s.clientY},stop:()=>this._handleDragStop()},document.addEventListener("mousemove",this._dragHandlers.move,{passive:!0}),document.addEventListener("mouseup",this._dragHandlers.stop),requestAnimationFrame(()=>this._dragLoop()))}_dragLoop(){this._dragState?.active&&(this._updateDragPosition(),requestAnimationFrame(()=>this._dragLoop()))}_updateDragPosition(){const t=this._dragState,{winElement:e,currentX:s,currentY:i,startX:o,startY:n}=t,a=s-o,d=i-n;if(!t.isDragging&&(Math.abs(a)>this._config.dragThreshold||Math.abs(d)>this._config.dragThreshold)&&(t.isDragging=!0),!t.isDragging)return;(t.initialState.tiled||t.initialState.maximized)&&!t.isRestored&&t.isDragging&&(t.initialState.maximized?(t.restoreXRatio=o/window.innerWidth,this._restoreWindowInternal(e,t.restoreXRatio),t.startWinTop=0):(t.restoreXRatio=(o-e.offsetLeft)/e.offsetWidth,this._restoreWindowInternal(e,null),t.startWinTop=e.offsetTop),t.startX=s,t.startY=i,t.isRestored=!0);let l,c;if(t.isRestored&&t.restoreXRatio!==null){const h=e.offsetWidth;l=s-t.restoreXRatio*h,c=Math.max(0,t.startWinTop+(i-n))}else l=t.startWinLeft+(t.isRestored?s-t.startX:a),c=Math.max(0,t.startWinTop+(t.isRestored?i-t.startY:d));e.style.left=`${l}px`,e.style.top=`${c}px`,(t.isRestored||!t.initialState.tiled&&!t.initialState.maximized)&&(e.classList.contains("tiled")&&e.classList.remove("tiled"),e.classList.contains("maximized")&&(e.classList.remove("maximized"),this._updateMaximizeIcon(e,!1))),t.isDragging&&this._detectSnapZone(s,i)}_detectSnapZone(t,e){const{view:s}=this._dragState,i=this._config.snapThreshold,o=s.w*this._config.edgeDetectionRatio,n=s.h*this._config.edgeDetectionRatio;let a=null;e<i?t<o?a="tl":t>s.w-o?a="tr":a="maximize":t<i?e<n?a="tl":e>s.h-n?a="bl":a="left":t>s.w-i?e<n?a="tr":e>s.h-n?a="br":a="right":e>s.h-i&&(a=t<s.w/2?"bl":"br"),this._dragState.snap!==a&&(this._dragState.snap=a,this._updateSnapIndicator(a,s.w,s.h))}_handleDragStop(){if(!this._dragState?.active)return;const{winElement:t,snap:e,view:s}=this._dragState;document.removeEventListener("mousemove",this._dragHandlers.move),document.removeEventListener("mouseup",this._dragHandlers.stop),this._snapIndicator.classList.remove("visible"),e?e==="maximize"?this.toggleMaximize(t):this._snapWindow(t,e,s.w,s.h):this._savePositionRatios(t),this._dragState.active=!1,this._dragState=null,this._dragHandlers=null}_restoreWindowInternal(t,e){let s,i;const o=v(t);e===null?o&&(s=o.width,i=o.height):(s=o?.width||t.style.width,i=o?.height||t.style.height),(!s||s==="100%")&&(s=this._config.defaultWidth+"px"),(!i||i==="100%")&&(i=this._config.defaultHeight+"px"),t.classList.remove("maximized","tiled"),this._updateMaximizeIcon(t,!1),t.classList.add("window-toggling","dragging-restore"),L(t,{width:s,height:i}),setTimeout(()=>{t.classList.remove("window-toggling","dragging-restore"),this._savePositionRatios(t)},this._config.animationDurationMs)}_updateSnapIndicator(t,e,s){if(!t){this._snapIndicator.classList.remove("visible");return}let i;t==="maximize"?i={top:"0px",left:"0px",width:`${e}px`,height:`${s}px`}:i=this._getSnapLayout(t,e,s),Object.assign(this._snapIndicator.style,i),this._snapIndicator.classList.add("visible")}_snapWindow(t,e,s,i){t.classList.contains("tiled")||this._ensureRestoreState(t),t.classList.add("window-toggling","tiled"),t.dataset.snapType=e;const o=this._getSnapLayout(e,s,i);Object.assign(t.style,o),setTimeout(()=>t.classList.remove("window-toggling"),this._config.animationDurationMs)}_getSnapLayout(t,e,s){const i=this._config.snapGap,o=(e-i*3)/2,n=(s-i*3)/2,a=s-i*2,d=i,l=o+i*2,c=i,h=n+i*2,u={tl:{top:c,left:d,width:o,height:n},tr:{top:c,left:l,width:o,height:n},bl:{top:h,left:d,width:o,height:n},br:{top:h,left:l,width:o,height:n},left:{top:c,left:d,width:o,height:a},right:{top:c,left:l,width:o,height:a}}[t];return{width:`${u.width}px`,height:`${u.height}px`,top:`${u.top}px`,left:`${u.left}px`}}_handleResize(){const t=window.innerWidth,e=window.innerHeight,s=e-this._config.taskbarHeight;this._windows.forEach(i=>{if(i.classList.contains("tiled")&&i.dataset.snapType){const o=i.dataset.snapType,n=this._getSnapLayout(o,t,s);Object.assign(i.style,n)}else i.classList.contains("maximized")||this._repositionWindowFromRatios(i,t,e)})}_captureScrollState(t){const e=new Map;return(t.scrollTop>0||t.scrollLeft>0)&&e.set("root",{top:t.scrollTop,left:t.scrollLeft}),t.querySelectorAll("*").forEach(s=>{(s.scrollTop>0||s.scrollLeft>0)&&e.set(this._getElementPath(t,s),{top:s.scrollTop,left:s.scrollLeft})}),e}_restoreScrollState(t,e){const s=()=>{e.forEach((n,a)=>{let d;if(a==="root")d=t;else try{d=t.querySelector(`:scope > ${a}`)}catch{d=t.querySelector(a)}d&&(d.scrollTop=n.top,d.scrollLeft=n.left)})};s();const i=t.querySelector(".window-content-scrollable > div")||t,o=new ResizeObserver(()=>s());o.observe(i),setTimeout(()=>o.disconnect(),this._config.scrollRestoreTimeoutMs)}_getElementPath(t,e){let s=[],i=e;for(;i&&i!==t;){let o=Array.prototype.indexOf.call(i.parentNode.children,i);s.unshift(`${i.tagName}:nth-child(${o+1})`),i=i.parentNode}return s.join(" > ")}async _defaultFetchWindowContent(t){return(await fetch(this._resolveEndpoint(t),{headers:{"X-Modal-Request":"1"},cache:"no-cache"})).text()}_defaultResolveEndpoint(t){return`/${String(t||"").replace(/^\/+/,"")}`}_defaultNotify(t,e){(t==="error"?console.error:console.log)(`[nidamjs:${t}]`,e)}_initializeModalContent(t){const e=this._getModules?this._getModules():null;this._initializeContent(t,{delegator:this._delegator,modules:e,manager:this})}}class H{#t;#e;#s=200;constructor(t,{refreshMap:e=null,refreshTimeout:s=200}={}){this.#t=t,this.#e=e||window.window_refresh_map||{},this.#s=s}setRefreshMap(t={}){this.#e=t||{}}handleEvent(t,e){const s=this.#e[t],[i,o]=t.split(":"),n=o==="deleted",a=e?.id||null;this.#t&&Array.from(this.#t._windows.entries()).forEach(([d,l])=>{const c=d.startsWith("/")?d.slice(1):d;if(n&&a&&l.dataset.dependsOn&&l.dataset.dependsOn.split("|").some(u=>{const[m,x]=u.split(":");return i===m&&String(x)===String(a)})){setTimeout(()=>{this.#t.close(l)},this.#s);return}s&&s.forEach(h=>{this.#i(h,c,n?null:a)&&setTimeout(()=>{this.#t.open(d,!0,null,!1)},this.#s)})})}#i(t,e,s=null){if(t===e)return!0;const i=t.split("/"),o=e.split("/");return!(i[i.length-1]==="*")&&i.length!==o.length?!1:i.every((a,d)=>a==="*"?!0:a.startsWith("{")&&a.endsWith("}")?s!==null?String(o[d])===String(s):!0:a===o[d])}}const $=(r,t)=>{(r==="error"?console.error:console.log)(`[nidamjs:${r}]`,t)};class P{#t;#e=new Map;#s=null;constructor(t={}){this.#t={root:document,modalContainer:"#target",pendingModalDatasetKey:"pendingModal",registry:[],refreshMap:null,refreshTimeout:200,notify:$,windowManager:{},...t}}initialize(){return this.#i(),this.#o(),this.#r(),this}getModule(t){return this.#e.get(t)}getModules(){return this.#e}#i(){this.#s=new b(this.#t.root),this.#e.set("delegator",this.#s)}#o(){const t=this.#t.root.querySelector(this.#t.modalContainer);if(!t)return;const e=new I(t,this.#s,{getModules:()=>this.#e,initializeContent:(i,o)=>y.initialize(o.delegator,i,o.modules,this.#t.registry),notify:this.#t.notify,...this.#t.windowManager||{}});this.#e.set("window",e),this.#a(t,e);const s=new H(e,{refreshMap:this.#t.refreshMap,refreshTimeout:this.#t.refreshTimeout});this.#e.set("refresher",s)}#a(t,e){const s=this.#t.pendingModalDatasetKey,i=(t?.dataset?.[s]||"").trim();if(!i)return;const o=i.startsWith("/")?i.slice(1):i;e.open(o)}#r(){y.initialize(this.#s,this.#t.root,this.#e,this.#t.registry)}}const N=(r={})=>new P(r),M={set(r,t){try{const e=JSON.stringify(t);localStorage.setItem(r,e)}catch(e){console.error(`Error saving to localStorage (key: ${r}):`,e)}},get(r,t=null){try{const e=localStorage.getItem(r);return e===null?t:JSON.parse(e)}catch(e){return console.error(`Error reading from localStorage (key: ${r}):`,e),t}},remove(r){try{localStorage.removeItem(r)}catch(t){console.error(`Error removing from localStorage (key: ${r}):`,t)}},clear(){try{localStorage.clear()}catch(r){console.error("Error clearing localStorage:",r)}},has(r){try{return localStorage.getItem(r)!==null}catch{return!1}}};class A extends W{_dragState=null;_storageKey="desktop_grid_layout";_storageNamespace="";_storage=M;constructor(t,e,s={}){super(t,e),this._storageKey=s.storageKey||this._storageKey,this._storageNamespace=s.storageNamespace||"",this._storage=s.storage||M,this._loadLayout()}_getStorageKey(){return this._storageNamespace?`${this._storageNamespace}_${this._storageKey}`:this._storageKey}_loadLayout(){try{const t=this._getStorageKey(),e=this._storage.get(t,[]);if(!e||!Array.isArray(e))return;this._root.querySelectorAll(".desktop-icon").forEach(i=>{const o=i.dataset.modal,n=e.find(a=>a.id===o);if(o&&n&&Array.isArray(n.classes)){const a=Array.from(i.classList).filter(d=>d.startsWith("col-start-")||d.startsWith("row-start-")||d.startsWith("col-end-"));i.classList.remove(...a),i.classList.add(...n.classes)}})}catch(t){console.error("Failed to load desktop layout",t)}}_bindEvents(){this._delegator.on("mousedown",".desktop-icon",this._handleDragStart.bind(this))}_handleDragStart(t,e){if(t.button!==0)return;t.preventDefault();const s=e.closest(".desktop-icon"),i=s.getBoundingClientRect(),o=this._root.getBoundingClientRect(),n=Array.from(s.classList).filter(a=>a.startsWith("col-start-")||a.startsWith("row-start-")||a.startsWith("col-end-"));this._dragState={element:s,startX:t.clientX,startY:t.clientY,initialLeft:i.left-o.left,initialTop:i.top-o.top,containerRect:o,originalClasses:n},s.style.left=`${this._dragState.initialLeft}px`,s.style.top=`${this._dragState.initialTop}px`,s.style.width=`${i.width}px`,s.style.position="absolute",s.classList.remove(...n),s.classList.add("dragging"),this._dragHandlers={move:this._handleDragMove.bind(this),stop:this._handleDragStop.bind(this)},document.addEventListener("mousemove",this._dragHandlers.move),document.addEventListener("mouseup",this._dragHandlers.stop)}_handleDragMove(t){if(!this._dragState)return;const{element:e,startX:s,startY:i,initialLeft:o,initialTop:n,containerRect:a}=this._dragState,d=t.clientX-s,l=t.clientY-i;let c=o+d,h=n+l;const g=a.width-e.offsetWidth,u=a.height-e.offsetHeight;c=Math.max(0,Math.min(c,g)),h=Math.max(0,Math.min(h,u)),e.style.left=`${c}px`,e.style.top=`${h}px`}_handleDragStop(t){if(!this._dragState)return;const{element:e,startX:s,startY:i,originalClasses:o}=this._dragState,n=Math.abs(t.clientX-s),a=Math.abs(t.clientY-i),d=n>5||a>5;if(e.classList.remove("dragging"),e.style.zIndex="",d){const l=c=>{c.preventDefault(),c.stopPropagation()};e.addEventListener("click",l,{capture:!0,once:!0}),this._snapToGrid(e)}else e.style.position="",e.style.left="",e.style.top="",e.style.width="",e.classList.add(...o);this._saveLayout&&this._saveLayout(),document.removeEventListener("mousemove",this._dragHandlers.move),document.removeEventListener("mouseup",this._dragHandlers.stop),this._dragState=null}_snapToGrid(t){const e=this._root.getBoundingClientRect(),s=t.getBoundingClientRect();let i=2;window.innerWidth>=1280?i=10:window.innerWidth>=1024?i=8:window.innerWidth>=768&&(i=6);const o=3,n=e.width/i,a=e.height/o,d=s.left-e.left+s.width/2,l=s.top-e.top+s.height/2;let c=Math.floor(d/n)+1,h=Math.floor(l/a)+1;c=Math.max(1,Math.min(c,i)),h=Math.max(1,Math.min(h,o));const g=`col-start-${c}`,u=`row-start-${h}`;if(Array.from(this._root.querySelectorAll(".desktop-icon")).find(_=>_!==t&&_.classList.contains(g)&&_.classList.contains(u))){if(t.style.position="",t.style.left="",t.style.top="",t.style.width="",this._dragState&&this._dragState.originalClasses){const _=Array.from(t.classList).filter(z=>z.startsWith("col-start-")||z.startsWith("row-start-")||z.startsWith("col-end-"));t.classList.remove(..._),t.classList.add(...this._dragState.originalClasses)}return}t.style.position="",t.style.left="",t.style.top="",t.style.width="";const x=Array.from(t.classList).filter(_=>_.startsWith("col-start-")||_.startsWith("row-start-")||_.startsWith("col-end-"));t.classList.remove(...x),t.classList.add(g,u)}_saveLayout(){const t=[];this._root.querySelectorAll(".desktop-icon").forEach(i=>{const o=i.dataset.modal;if(o){const n=Array.from(i.classList).filter(a=>a.startsWith("col-start-")||a.startsWith("row-start-"));t.push({id:o,classes:n})}});const s=this._getStorageKey();this._storage.set(s,t)}}function X(r,t){if(!r||!t||t.type!=="success"||!t.emit)return;let e=t.emit,s=t.id||null;const i=e.split(":");i.length>=3&&(s=i.pop(),e=i.join(":")),r.handleEvent(e,{...t,emit:e,id:s})}N().initialize(),f.BaseManager=W,f.ContentInitializer=y,f.DOMUtils=R,f.DesktopIconManager=A,f.EventDelegator=b,f.NidamApp=P,f.WindowManager=I,f.WindowRefresher=H,f.applyWindowState=L,f.captureWindowState=C,f.createNidamApp=N,f.handleRefreshEvent=X,f.readWindowState=v,f.saveWindowState=D,f.storageUtil=M,Object.defineProperty(f,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@moonitoring/nidamjs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/nidam.cjs",
|
|
6
|
+
"module": "./dist/nidam.es.js",
|
|
7
|
+
"style": "./dist/nidam.css",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/nidam.es.js",
|
|
11
|
+
"require": "./dist/nidam.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./cjs": "./dist/nidam.cjs",
|
|
14
|
+
"./umd": "./dist/nidam.umd.js",
|
|
15
|
+
"./nidam.css": "./dist/nidam.css"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"readme.md"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"tag": "beta"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"imports": "bun -e \"await import('./src/index.js')\"",
|
|
27
|
+
"test": "vitest run --coverage",
|
|
28
|
+
"test:watch": "vitest --coverage",
|
|
29
|
+
"lint": "tsc --noEmit",
|
|
30
|
+
"format": "prettier -w .",
|
|
31
|
+
"quality": "bun run imports && bun run lint && bun run test && bun run format",
|
|
32
|
+
"build": "vite build --mode lib && vite build --mode css",
|
|
33
|
+
"csr": "bun run build && vite --open /examples/csr/index.html",
|
|
34
|
+
"ssr": "bun run build && bun examples/ssr/main.js"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.10.13",
|
|
38
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
39
|
+
"express": "^5.2.1",
|
|
40
|
+
"jsdom": "^27.4.0",
|
|
41
|
+
"prettier": "^3.8.1",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"vite": "^7.3.1",
|
|
44
|
+
"vitest": "^3.2.4"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# NidamJS
|
|
2
|
+
|
|
3
|
+
NidamJS is a framework-agnostic JavaScript library for desktop-like window components in web applications.
|
|
4
|
+
|
|
5
|
+
## Official API
|
|
6
|
+
|
|
7
|
+
Use the package root export for manual initialization:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { createNidamApp, WindowManager, WindowRefresher } from "nidamjs";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Internal paths (`src/*`) are implementation details and not public API.
|
|
14
|
+
|
|
15
|
+
## Bundle Strategy
|
|
16
|
+
|
|
17
|
+
- `nidamjs` (`dist/nidam.es.js`, `dist/nidam.umd.js`): core bundle.
|
|
18
|
+
- Initialization is explicit: call `createNidamApp(...).initialize()`.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { createNidamApp } from "nidamjs";
|
|
30
|
+
|
|
31
|
+
const app = createNidamApp({
|
|
32
|
+
modalContainer: "#target",
|
|
33
|
+
registry: [],
|
|
34
|
+
windowManager: {
|
|
35
|
+
config: {
|
|
36
|
+
layoutStabilizationMs: 450,
|
|
37
|
+
},
|
|
38
|
+
notify: (level, message) => console.log(level, message),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
app.initialize();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`layoutStabilizationMs` controls how long the first window can auto-recenter if late CSS changes its rendered size after
|
|
46
|
+
open.
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
- Porting plan and Code Arena differences: [porting_plan.md](porting_plan.md)
|
|
51
|
+
- Additional docs index: [docs/readme.md](docs/readme.md)
|
|
52
|
+
- CSR example: [examples/csr/readme.md](examples/csr/readme.md)
|
|
53
|
+
- SSR example: [examples/ssr/readme.md](examples/ssr/readme.md)
|
|
54
|
+
|
|
55
|
+
## Naming Convention
|
|
56
|
+
|
|
57
|
+
- All documentation filenames use lowercase.
|
|
58
|
+
- Standard file name: `readme.md` (not `README.md`).
|
|
59
|
+
|
|
60
|
+
## Files
|
|
61
|
+
|
|
62
|
+
- Porting plan: [porting_plan.md](porting_plan.md)
|
|
63
|
+
- Project root documentation: [readme.md](readme.md)
|
|
64
|
+
|
|
65
|
+
## Scripts (Bun)
|
|
66
|
+
|
|
67
|
+
- `bun run imports`: verifies the public entrypoint can be imported.
|
|
68
|
+
- `bun run test`: runs Vitest with coverage enabled.
|
|
69
|
+
- `bun run test:watch`: watch mode with coverage enabled.
|
|
70
|
+
- `bun run lint`: type-lints JS with TypeScript (`checkJs`).
|
|
71
|
+
- `bun run format`: rewrites formatting with Prettier.
|
|
72
|
+
- `bun run quality`: aggregate quality command.
|
|
73
|
+
- `bun run csr`: builds bundles then opens the CSR demo.
|
|
74
|
+
- `bun run ssr`: builds bundles then starts the SSR demo server.
|
|
75
|
+
|
|
76
|
+
## Quality Stack
|
|
77
|
+
|
|
78
|
+
- Test framework: `Vitest` + `jsdom`
|
|
79
|
+
- Coverage: `@vitest/coverage-v8` (V8 provider)
|
|
80
|
+
- Type linter: `TypeScript` in `checkJs` mode
|
|
81
|
+
- Formatter: `Prettier`
|
|
82
|
+
|
|
83
|
+
## Coverage Report
|
|
84
|
+
|
|
85
|
+
- Run `bun run test`.
|
|
86
|
+
- Terminal summary is printed after tests.
|
|
87
|
+
- HTML report: `coverage/index.html`.
|
|
88
|
+
- LCOV report: `coverage/lcov.info`.
|
|
89
|
+
|
|
90
|
+
## Runtime DOM Contract
|
|
91
|
+
|
|
92
|
+
The window engine expects these selectors/attributes in your modal HTML:
|
|
93
|
+
|
|
94
|
+
- `.window`
|
|
95
|
+
- `[data-modal]`
|
|
96
|
+
- `[data-close]`
|
|
97
|
+
- `[data-maximize]`
|
|
98
|
+
- `[data-bar]`
|
|
99
|
+
- `.window-content-scrollable` (optional but recommended)
|
|
100
|
+
|
|
101
|
+
## Project Tree
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
.
|
|
105
|
+
├── .gitignore
|
|
106
|
+
├── porting_plan.md
|
|
107
|
+
├── readme.md
|
|
108
|
+
├── bun.lock
|
|
109
|
+
├── docs
|
|
110
|
+
│ └── readme.md
|
|
111
|
+
├── examples
|
|
112
|
+
│ ├── csr
|
|
113
|
+
│ │ ├── index.html
|
|
114
|
+
│ │ ├── main.js
|
|
115
|
+
│ │ └── readme.md
|
|
116
|
+
│ ├── ssr
|
|
117
|
+
│ │ ├── main.js
|
|
118
|
+
│ │ ├── public
|
|
119
|
+
│ │ │ └── client.js
|
|
120
|
+
│ │ ├── readme.md
|
|
121
|
+
│ │ └── server
|
|
122
|
+
│ │ ├── routes.js
|
|
123
|
+
│ │ └── templates
|
|
124
|
+
│ │ └── layout.js
|
|
125
|
+
│ ├── shared
|
|
126
|
+
│ ├── demo.css
|
|
127
|
+
│ ├── page-one.html
|
|
128
|
+
│ └── page-two.html
|
|
129
|
+
├── package.json
|
|
130
|
+
├── src
|
|
131
|
+
│ ├── bootstrap
|
|
132
|
+
│ │ └── NidamApp.js
|
|
133
|
+
│ ├── core
|
|
134
|
+
│ │ ├── BaseManager.js
|
|
135
|
+
│ │ ├── ContentInitializer.js
|
|
136
|
+
│ │ └── EventDelegator.js
|
|
137
|
+
│ ├── features
|
|
138
|
+
│ │ ├── desktop
|
|
139
|
+
│ │ │ └── DesktopIconManager.js
|
|
140
|
+
│ │ └── window
|
|
141
|
+
│ │ ├── WindowManager.js
|
|
142
|
+
│ │ └── WindowRefresher.js
|
|
143
|
+
│ ├── index.js
|
|
144
|
+
│ └── utils
|
|
145
|
+
│ ├── dom.js
|
|
146
|
+
│ └── eventUtils.js
|
|
147
|
+
├── tests
|
|
148
|
+
│ ├── readme.md
|
|
149
|
+
│ └── unit
|
|
150
|
+
│ ├── contentInitializer.test.js
|
|
151
|
+
│ ├── windowManager.test.js
|
|
152
|
+
│ └── windowRefresher.test.js
|
|
153
|
+
├── tsconfig.json
|
|
154
|
+
└── vitest.config.js
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Tree Explanation
|
|
158
|
+
|
|
159
|
+
- `src/index.js`: side-effect-free public API entrypoint.
|
|
160
|
+
- `src/bootstrap/`: app bootstrap composition (`NidamApp`).
|
|
161
|
+
- `src/core/`: generic infrastructure primitives (base manager, event delegation, dynamic init).
|
|
162
|
+
- `src/features/window/`: core window system (open/close/focus/drag/snap/refresh).
|
|
163
|
+
- `src/features/desktop/`: desktop icon drag-and-drop behavior.
|
|
164
|
+
- `src/utils/`: shared utility helpers.
|
|
165
|
+
- `examples/csr/`: ES module client-side example.
|
|
166
|
+
- `examples/ssr/`: Express-based server-side rendering example.
|
|
167
|
+
- `examples/shared/`: shared pages and demo styling reused by CSR/SSR.
|
|
168
|
+
- `tests/unit/`: focused unit tests for core and window features.
|
|
169
|
+
- `docs/porting_plan.md`: migration decisions and boundaries.
|
|
170
|
+
- `tsconfig.json`: type-lint config for JS (`checkJs`).
|
|
171
|
+
- `vitest.config.js`: test runner config.
|
|
172
|
+
|
|
173
|
+
## Notes
|
|
174
|
+
|
|
175
|
+
Detailed migration strategy and design rationale are documented in [docs/porting_plan.md](porting_plan.md).
|