Skip to content

Commit a43bc03

Browse files
authored
Merge pull request #222 from iceljc/features/refine-chat-window
add multiselect, add chat event
2 parents 3439199 + 788e9ff commit a43bc03

File tree

14 files changed

+537
-32
lines changed

14 files changed

+537
-32
lines changed

src/lib/common/LiveChatEntry.svelte

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,58 @@
33
import { onMount } from 'svelte';
44
import { PUBLIC_LIVECHAT_HOST, PUBLIC_LIVECHAT_ENTRY_ICON } from '$env/static/public';
55
import { getSettingDetail } from '$lib/services/setting-service';
6+
import { chatBotStore } from '$lib/helpers/store';
7+
import { CHAT_FRAME_ID } from '$lib/helpers/constants';
68
7-
let showChatIcon = false;
8-
let showChatBox = false;
99
let chatUrl = PUBLIC_LIVECHAT_HOST;
1010
1111
onMount(async () => {
1212
const agentSettings = await getSettingDetail("Agent");
1313
chatUrl = `${PUBLIC_LIVECHAT_HOST}chat/${agentSettings.hostAgentId}?isFrame=true`;
14-
showChatIcon = true;
1514
});
1615
1716
// Handle event from iframe
1817
window.onmessage = async function(e) {
1918
if (e.data.action == 'close') {
20-
showChatIcon = true;
21-
showChatBox = false;
19+
chatBotStore.set({
20+
showChatBox: false
21+
});
2222
}
2323
};
2424
25-
function handleChatBox() {
26-
showChatIcon = false;
27-
showChatBox = true;
25+
function openChatBox() {
26+
chatBotStore.set({
27+
showChatBox: true
28+
});
2829
}
2930
</script>
3031

3132
<div class="fixed-bottom float-bottom-right">
32-
{#if showChatBox}
33+
{#if $chatBotStore.showChatBox}
3334
<div transition:fade={{ delay: 250, duration: 300 }}>
3435
<iframe
3536
src={chatUrl}
3637
width="380px"
3738
height="650px"
38-
class="border border-2 rounded-3 m-3 float-end chat-iframe"
39+
class={`border border-2 rounded-3 m-3 float-end chat-iframe`}
3940
title="live chat"
40-
id="chat-frame"
41-
>
42-
</iframe>
41+
id={CHAT_FRAME_ID}
42+
/>
4343
</div>
4444
{/if}
4545

46-
{#if showChatIcon}
46+
{#if !$chatBotStore.showChatBox}
4747
<div class="mb-3 float-end wave-effect" transition:fade={{ delay: 100, duration: 500 }}>
48-
<button class="btn btn-transparent" on:click={() => handleChatBox()}>
48+
<button class="btn btn-transparent" on:click={() => openChatBox()}>
4949
<img alt="live chat" class="avatar-md rounded-circle" src={PUBLIC_LIVECHAT_ENTRY_ICON} />
50+
<iframe
51+
src={chatUrl}
52+
width="0px"
53+
height="0px"
54+
class={`border border-2 rounded-3 m-3 float-end chat-iframe`}
55+
title="live chat"
56+
id={CHAT_FRAME_ID}
57+
/>
5058
</button>
5159
</div>
5260
{/if}

src/lib/common/MultiSelect.svelte

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
<script>
2+
import { onMount, createEventDispatcher, tick, afterUpdate } from "svelte";
3+
import { Input } from "@sveltestrap/sveltestrap";
4+
import { clickoutsideDirective } from "$lib/helpers/directives";
5+
6+
const svelteDispatch = createEventDispatcher();
7+
8+
/** @type {string} */
9+
export let tag;
10+
11+
/** @type {any[]} */
12+
export let options = [];
13+
14+
/** @type {boolean} */
15+
export let selectAll = true;
16+
17+
/** @type {string} */
18+
export let searchPlaceholder = '';
19+
20+
/** @type {string} */
21+
export let containerClasses = "";
22+
23+
/** @type {string} */
24+
export let containerStyles = "";
25+
26+
/** @type {boolean} */
27+
export let disableDefaultStyles = false;
28+
29+
/** @type {null | undefined | (() => Promise<any>)} */
30+
export let onScrollMoreOptions = null;
31+
32+
/** @type {string} */
33+
let searchValue = '';
34+
35+
/** @type {boolean} */
36+
let selectAllChecked = false;
37+
38+
/** @type {boolean} */
39+
let showOptionList = false;
40+
41+
/** @type {any[]} */
42+
let innerOptions = [];
43+
44+
/** @type {any[]} */
45+
let refOptions = [];
46+
47+
/** @type {string} */
48+
let displayText = '';
49+
50+
/** @type {boolean} */
51+
let loading = false;
52+
53+
onMount(() => {
54+
innerOptions = options.map(x => {
55+
return {
56+
id: x.id,
57+
name: x.name,
58+
checked: false
59+
}
60+
});
61+
62+
refOptions = options.map(x => {
63+
return {
64+
id: x.id,
65+
name: x.name,
66+
checked: false
67+
}
68+
});
69+
});
70+
71+
72+
async function toggleOptionList() {
73+
showOptionList = !showOptionList;
74+
if (showOptionList) {
75+
await tick();
76+
adjustDropdownPosition();
77+
}
78+
}
79+
80+
81+
/** @param {any} e */
82+
function changeSearchValue(e) {
83+
searchValue = e.target.value || '';
84+
if (searchValue) {
85+
innerOptions = refOptions.filter(x => x.name.includes(searchValue));
86+
} else {
87+
innerOptions = refOptions;
88+
}
89+
90+
verifySelectAll();
91+
}
92+
93+
94+
/**
95+
* @param {any} e
96+
* @param {any} option
97+
*/
98+
function checkOption(e, option) {
99+
const found = innerOptions.find(x => x.id == option.id);
100+
found.checked = e.target.checked;
101+
102+
const refFound = refOptions.find(x => x.id == option.id);
103+
refFound.checked = e.target.checked;
104+
changeDisplayText();
105+
sendEvent();
106+
}
107+
108+
/** @param {any} e */
109+
function checkSelectAll(e) {
110+
selectAllChecked = e.target.checked;
111+
innerOptions = innerOptions.map(x => {
112+
return { ...x, checked: selectAllChecked }
113+
});
114+
115+
syncChangesToRef(selectAllChecked);
116+
changeDisplayText();
117+
sendEvent();
118+
}
119+
120+
/** @param {boolean} checked */
121+
function syncChangesToRef(checked) {
122+
const ids = innerOptions.map(x => x.id);
123+
refOptions = refOptions.map(x => {
124+
if (ids.includes(x.id)) {
125+
return {
126+
...x,
127+
checked: checked
128+
};
129+
}
130+
131+
return { ...x };
132+
});
133+
}
134+
135+
function changeDisplayText() {
136+
const count = refOptions.filter(x => x.checked).length;
137+
if (count === 0) {
138+
displayText = '';
139+
} else if (count === options.length) {
140+
displayText = `All selected (${count})`;
141+
} else {
142+
displayText = `Selected (${count})`;
143+
}
144+
145+
verifySelectAll();
146+
}
147+
148+
function verifySelectAll() {
149+
if (!selectAll) return;
150+
151+
const innerCount = innerOptions.filter(x => x.checked).length;
152+
if (innerCount < innerOptions.length) {
153+
selectAllChecked = false;
154+
} else if (innerCount === innerOptions.length) {
155+
selectAllChecked = true;
156+
}
157+
}
158+
159+
/** @param {any} e */
160+
function handleClickOutside(e) {
161+
e.preventDefault();
162+
163+
const curNode = e.detail.currentNode;
164+
const targetNode = e.detail.targetNode;
165+
166+
if (!curNode?.contains(targetNode)) {
167+
showOptionList = false;
168+
}
169+
}
170+
171+
function sendEvent() {
172+
svelteDispatch("select", {
173+
selecteds: refOptions.filter(x => !!x.checked)
174+
});
175+
}
176+
177+
function adjustDropdownPosition() {
178+
const btn = document.getElementById(`multiselect-btn-${tag}`);
179+
const optionList = document.getElementById(`multiselect-list-${tag}`);
180+
181+
if (!btn || !optionList) return;
182+
183+
const btnRec = btn.getBoundingClientRect();
184+
const windowHeight = window.innerHeight;
185+
const spaceBelow = windowHeight - btnRec.bottom;
186+
const spaceAbove = btnRec.top;
187+
const listHeight = optionList.offsetHeight;
188+
189+
if (spaceBelow < listHeight && spaceAbove > listHeight) {
190+
optionList.style.top = `-${listHeight}px`;
191+
optionList.style.bottom = 'auto';
192+
}
193+
}
194+
195+
function innerScroll() {
196+
if (onScrollMoreOptions != null && onScrollMoreOptions != undefined) {
197+
const dropdown = document.getElementById(`multiselect-list-${tag}`);
198+
if (!dropdown || loading) return;
199+
200+
if (dropdown.scrollHeight - dropdown.scrollTop - dropdown.clientHeight <= 1) {
201+
loading = true;
202+
onScrollMoreOptions().then(res => {
203+
loading = false;
204+
}).catch(err => {
205+
loading = false;
206+
});
207+
}
208+
}
209+
}
210+
211+
$: {
212+
if (options.length > refOptions.length) {
213+
const curIds = refOptions.map(x => x.id);
214+
const newOptions = options.filter(x => !curIds.includes(x.id)).map(x => {
215+
return {
216+
id: x.id,
217+
name: x.name,
218+
checked: false
219+
};
220+
});
221+
222+
innerOptions = [
223+
...innerOptions,
224+
...newOptions
225+
];
226+
227+
refOptions = [
228+
...refOptions,
229+
...newOptions
230+
];
231+
232+
changeDisplayText();
233+
}
234+
}
235+
</script>
236+
237+
238+
<div
239+
class="{disableDefaultStyles ? '' : 'multiselect-container'} {containerClasses}"
240+
style={`${containerStyles}`}
241+
use:clickoutsideDirective
242+
on:clickoutside={(/** @type {any} */ e) => handleClickOutside(e)}
243+
>
244+
<!-- svelte-ignore a11y-click-events-have-key-events -->
245+
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
246+
<ul
247+
class="display-container"
248+
id={`multiselect-btn-${tag}`}
249+
on:click={() => toggleOptionList()}
250+
>
251+
<Input
252+
type="text"
253+
class='clickable'
254+
value={displayText}
255+
readonly
256+
/>
257+
<div class={`display-suffix ${showOptionList ? 'show-list' : ''}`}>
258+
<i class="bx bx-chevron-down" />
259+
</div>
260+
</ul>
261+
{#if showOptionList}
262+
<ul class="option-list" id={`multiselect-list-${tag}`} on:scroll={() => innerScroll()}>
263+
<div class="search-box">
264+
<div class="search-prefix">
265+
<i class="bx bx-search-alt" />
266+
</div>
267+
<Input
268+
type="text"
269+
value={searchValue}
270+
placeholder={searchPlaceholder}
271+
on:input={e => changeSearchValue(e)}
272+
/>
273+
</div>
274+
{#if innerOptions.length > 0}
275+
{#if selectAll}
276+
<li class="option-item">
277+
<div class="line-align-center select-box">
278+
<Input
279+
type="checkbox"
280+
checked={selectAllChecked}
281+
on:change={e => checkSelectAll(e)}
282+
/>
283+
</div>
284+
<div class="line-align-center select-name fw-bold">
285+
{'Select all'}
286+
</div>
287+
</li>
288+
{/if}
289+
{#each innerOptions as option, idx (idx)}
290+
<li class="option-item">
291+
<div class="line-align-center select-box">
292+
<Input
293+
type="checkbox"
294+
checked={option.checked}
295+
on:change={e => checkOption(e, option)}
296+
/>
297+
</div>
298+
<div class="line-align-center select-name">
299+
{option.name}
300+
</div>
301+
</li>
302+
{/each}
303+
{:else}
304+
<li class="option-item">
305+
<div class='nothing'>Nothing...</div>
306+
</li>
307+
{/if}
308+
</ul>
309+
{/if}
310+
</div>

src/lib/helpers/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { EditorType, UserRole } from "./enums";
22

3+
export const CHAT_FRAME_ID = "chatbox-frame";
4+
35
export const USER_SENDERS = [
46
UserRole.Admin,
57
UserRole.User,

0 commit comments

Comments
 (0)