Profile Stack
The profile stack shows the list of users currently accessing the Document. Try adding and deleting users to see how the profile stack changes.
main.js
1import yorkie from '@yorkie-js/sdk';2import { getRandomName, getRandomColor } from './util.js';34const client = new yorkie.Client({5 rpcAddr: import.meta.env.VITE_YORKIE_API_ADDR,6 apiKey: import.meta.env.VITE_YORKIE_API_KEY,7});89const doc = new yorkie.Document('profile-stack', {10 enableDevtools: true,11});1213const myRandomPresence = {14 name: getRandomName(),15 color: getRandomColor(),16};1718const MAX_PEER_VIEW = 3;19const SPEECH_BUBBLE_INDEX = {20 me: 0,21 peer: (index) => index + 1,22 more: 4,23};2425let activeSpeechBubbleIndex = null;26let myPresence = null;27let peerList = [];2829async function main() {30 await client.activate();31 doc.subscribe('presence', (e) => {32 initUserPresences(doc.getPresences());33 renderPeerList();34 initEditProfileModal();35 renderSpeechBubble(activeSpeechBubbleIndex);36 });37 await client.attach(doc, {38 initialPresence: {39 name: myRandomPresence.name,40 color: myRandomPresence.color,41 },42 });43 bindGlobalClickDismiss();44}4546const initUserPresences = (peers) => {47 peerList = peers.filter(({ clientID: id }) => id !== client.getID());48 myPresence = peers.find(49 ({ clientID: id }) => id === client.getID(),50 )?.presence;51};5253const bindGlobalClickDismiss = () => {54 window.addEventListener('click', (event) => {55 const $target = event.target;56 const $profile = $target.closest('.profile');57 const $speechBubble = $target.closest('.speech-bubble');58 const $editProfileModal = $target.closest('.modal');59 if ($profile || $speechBubble || $editProfileModal) {60 return;61 }62 removeAllSpeechBubbles();63 });64};6566// user profile67const createUserIcon = (color) => {68 const $peer = document.createElement('div');69 $peer.className = 'peer';70 $peer.innerHTML = `71 <div class="profile">72 <img src="./images/profile-${color}.svg" alt="profile" class="profile-img"/>73 </div>74 `;75 return $peer;76};7778const createSmallUserProfile = (color, name) => {79 const $peer = document.createElement('div');80 $peer.className = 'small-peer';81 const $userIcon = createUserIcon(color);82 $userIcon.className = 'user-icon';83 $peer.appendChild($userIcon);84 $peer.appendChild(document.createElement('span'));85 $peer.querySelector('span').className = 'name';86 $peer.querySelector('span').innerHTML = name;87 return $peer;88};8990export const renderPeerList = () => {91 const $peerList = document.getElementById('peerList');92 $peerList.innerHTML = '';93 const $me = createUserIcon(myPresence.color);94 const $profile = $me.querySelector('.profile');95 $profile.addEventListener('click', () => {96 activeSpeechBubbleIndex = SPEECH_BUBBLE_INDEX.me;97 renderSpeechBubble(activeSpeechBubbleIndex);98 });99 $me.classList.add('me');100 $peerList.appendChild($me);101102 peerList.forEach((peer, i) => {103 const { color } = peer.presence;104 if (i < MAX_PEER_VIEW) {105 const $peer = createUserIcon(color);106 const $profile = $peer.querySelector('.profile');107 $profile.addEventListener('click', () => {108 activeSpeechBubbleIndex = SPEECH_BUBBLE_INDEX.peer(i);109 renderSpeechBubble(activeSpeechBubbleIndex);110 });111 $peerList.appendChild($peer);112 return;113 }114 });115116 const hasMorePeers = peerList.length > MAX_PEER_VIEW;117118 if (hasMorePeers) {119 const $viewMore = document.createElement('div');120 $viewMore.className = 'peer more';121 $viewMore.innerHTML = `122 <div class="profile">123 +${peerList.length - MAX_PEER_VIEW}124 </div>125 `;126 $peerList.appendChild($viewMore);127 const $profile = $viewMore.querySelector('.profile');128 $profile.addEventListener('click', () => {129 activeSpeechBubbleIndex = SPEECH_BUBBLE_INDEX.more;130 renderSpeechBubble(activeSpeechBubbleIndex);131 });132 }133};134135// speech bubble136const createSpeechBubbleContainer = () => {137 const $speechBubbleContainer = document.createElement('div');138 $speechBubbleContainer.className = 'speech-bubble';139 return $speechBubbleContainer;140};141142const createUserNameSpeechBubble = (name, isMe) => {143 const $speechBubbleContainer = createSpeechBubbleContainer();144 const $editProfileBtn = document.createElement('button');145 $editProfileBtn.className = 'edit-profile-btn';146 $editProfileBtn.innerHTML = 'Edit Profile';147 $editProfileBtn.addEventListener('click', openEditModal);148 $speechBubbleContainer.innerHTML = `<span class="name">${name}${149 isMe ? ' (me)' : ''150 }</span>`;151 if (isMe) {152 $speechBubbleContainer.classList.add('me');153 $speechBubbleContainer.appendChild($editProfileBtn);154 }155 return $speechBubbleContainer;156};157158const createPeerListSpeechBubble = (moreUserProfiles) => {159 const $speechBubbleContainer = createSpeechBubbleContainer();160 moreUserProfiles.forEach((profile) => {161 $speechBubbleContainer.appendChild(162 createSmallUserProfile(profile.color, profile.name),163 );164 });165 return $speechBubbleContainer;166};167168const removeAllSpeechBubbles = () => {169 const $speechBubble = document.querySelectorAll(`.speech-bubble`);170 $speechBubble.forEach((bubble) => {171 bubble.remove();172 });173};174175export const renderSpeechBubble = (speechBubbleIndex) => {176 removeAllSpeechBubbles();177 let $speechBubble;178 if (speechBubbleIndex === null) return;179 if (speechBubbleIndex === SPEECH_BUBBLE_INDEX.me) {180 $speechBubble = createUserNameSpeechBubble(myPresence?.name, true);181 } else if (speechBubbleIndex === SPEECH_BUBBLE_INDEX.more) {182 const moreUserProfiles = peerList183 .filter((_, i) => i >= MAX_PEER_VIEW)184 .map(({ presence }) => ({185 color: presence.color,186 name: presence.name,187 }));188 $speechBubble = createPeerListSpeechBubble(moreUserProfiles);189 $speechBubble.classList.add('peer-more-list');190 } else {191 const peerName = peerList[speechBubbleIndex - 1].presence.name;192 $speechBubble = createUserNameSpeechBubble(peerName, false);193 }194 const $targetPeer =195 document.querySelectorAll(`#peerList .peer`)[speechBubbleIndex];196 $targetPeer.appendChild($speechBubble);197};198199// modal200const initEditProfileModal = () => {201 const $editProfileModal = document.getElementById('editProfileModal');202 const $editProfileModalCloseBtn = $editProfileModal.querySelector('.close');203 $editProfileModalCloseBtn.addEventListener('click', closeEditModal);204 const $editProfileModalInput = $editProfileModal.querySelector('input');205 $editProfileModalInput.defaultValue = myPresence?.name;206 const $editProfileModalSaveBtn = $editProfileModal.querySelector('.save');207 $editProfileModalSaveBtn.addEventListener('click', saveEditProfile);208 const $editProfileModalImg = $editProfileModal.querySelector('.profile-img');209 $editProfileModalImg.src = `./images/profile-${myPresence?.color}.svg`;210};211212const openEditModal = () => {213 const $editProfileModal = document.getElementById('editProfileModal');214 $editProfileModal.style.display = 'block';215};216217const closeEditModal = () => {218 const $editProfileModal = document.getElementById('editProfileModal');219 $editProfileModal.style.display = 'none';220};221222const saveEditProfile = async () => {223 const $editProfileModal = document.getElementById('editProfileModal');224 const $editProfileModalInput = $editProfileModal.querySelector('input');225 const newName = $editProfileModalInput.value;226 doc.update((_, presence) => {227 presence.set({228 name: newName,229 });230 });231 closeEditModal();232};233234main();
- User 1
- User 2
- User 1
- User 2