contextmenu.ts
ts
export class ContextmenuHelper<T> {
private static instance: ContextmenuHelper<any> | null = null
private contextmenuInstance: T | null = null
private isMounted = false
static create<T>(): ContextmenuHelper<T> {
if (!this.instance) {
this.instance = new ContextmenuHelper<T>()
}
return this.instance
}
public mounted(): void {
if (this.isMounted) {
return
}
this.isMounted = true
document.addEventListener('click', () => {
this.close()
})
}
public open(instance: T): void {
setTimeout(() => {
this.contextmenuInstance = instance
})
}
public close(): void {
requestAnimationFrame(() => {
this.contextmenuInstance = null
})
}
public isOpen(instance: T): boolean {
return this.contextmenuInstance === instance
}
}
contextmenu-composition.ts
ts
const state = {
isMounted: false,
contextmenu: null,
};
const mount = () => {
if (state.isMounted) {
return
}
state.isMounted = true;
document.addEventListener('click', () => {
this.close()
})
};
const open = (symbol: Symbol): void => {
setTimeout(() => {
state.contextmenu = symbol
})
};
const close = (): void => {
requestAnimationFrame(() => {
state.contextmenu = null
})
};
const isOpen = (symbol: Symbol): boolean => {
return state.contextmenu === symbol
};
contextmenu-object.js
js
class Mediator {
static instance;
static create() {
if (!this.instance) {
this.instance = new Mediator()
}
return this.instance
}
contextMenus = [];
constructor() {}
register(contextmenu) {
this.contextMenus.push(contextmenu)
}
unregister(contextmenu) {
this.contextMenus = this.contextMenus.filter((instance) => {
return instance !== contextmenu
})
}
open(contextmenu) {
this.contextMenus.forEach((instance) => {
instance.isOpen = instance === contextmenu;
})
}
closeAll() {
this.contextMenus.forEach((instance) => {
instance.isOpen = false;
})
}
}
class ContextMenu {
static create() {
return new ContextMenu();
}
mediator = Mediator.create();
isOpen = false;
constructor() {
this.mediator.register(this)
}
remove() {
this.mediator.unregister(this)
}
open() {
this.mediator.open(this)
}
}
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.section {height: 300px;}
.section.on .modal {background: #222;}
.modal {width: 500px; max-width: 100%; height: 200px; background: #f8f8f8;}
</style>
</head>
<body>
<div>배경 클릭 시 모두 회색으로 됨</div>
<div class="section">
<button>검정</button>
<div class="modal"></div>
</div>
<div class="section">
<button>검정</button>
<div class="modal"></div>
</div>
<div class="section">
<button>검정</button>
<div class="modal"></div>
</div>
<script src="./contextmenu-object.js"></script>
<script>
const sections = document.querySelectorAll('.section');
const buttons = Array.from(sections)
.map((section) => section.querySelector('button'));
const contextMenus = Array.from({length: sections.length})
.map(() => ContextMenu.create());
const mediator = Mediator.create();
const hookRender = (fn) => () => {
fn();
render();
};
const render = () => {
sections.forEach((section, index) => {
const {isOpen} = contextMenus[index];
const commend = isOpen ? 'add' : 'remove';
section.classList[commend]('on')
});
};
const init = () => {
buttons.forEach((button, index) => {
button.addEventListener('click', hookRender(() => {
contextMenus[index].open();
}));
});
document.addEventListener('click', hookRender(() => {
mediator.closeAll();
}), true);
};
init();
</script>
</body>
</html>