Skip to content

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>