Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add multiselect, add chat event #222

Merged
merged 2 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions src/lib/common/LiveChatEntry.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,58 @@
import { onMount } from 'svelte';
import { PUBLIC_LIVECHAT_HOST, PUBLIC_LIVECHAT_ENTRY_ICON } from '$env/static/public';
import { getSettingDetail } from '$lib/services/setting-service';
import { chatBotStore } from '$lib/helpers/store';
import { CHAT_FRAME_ID } from '$lib/helpers/constants';

let showChatIcon = false;
let showChatBox = false;
let chatUrl = PUBLIC_LIVECHAT_HOST;

onMount(async () => {
const agentSettings = await getSettingDetail("Agent");
chatUrl = `${PUBLIC_LIVECHAT_HOST}chat/${agentSettings.hostAgentId}?isFrame=true`;
showChatIcon = true;
});

// Handle event from iframe
window.onmessage = async function(e) {
if (e.data.action == 'close') {
showChatIcon = true;
showChatBox = false;
chatBotStore.set({
showChatBox: false
});
}
};

function handleChatBox() {
showChatIcon = false;
showChatBox = true;
function openChatBox() {
chatBotStore.set({
showChatBox: true
});
}
</script>

<div class="fixed-bottom float-bottom-right">
{#if showChatBox}
{#if $chatBotStore.showChatBox}
<div transition:fade={{ delay: 250, duration: 300 }}>
<iframe
src={chatUrl}
width="380px"
height="650px"
class="border border-2 rounded-3 m-3 float-end chat-iframe"
class={`border border-2 rounded-3 m-3 float-end chat-iframe`}
title="live chat"
id="chat-frame"
>
</iframe>
id={CHAT_FRAME_ID}
/>
</div>
{/if}

{#if showChatIcon}
{#if !$chatBotStore.showChatBox}
<div class="mb-3 float-end wave-effect" transition:fade={{ delay: 100, duration: 500 }}>
<button class="btn btn-transparent" on:click={() => handleChatBox()}>
<button class="btn btn-transparent" on:click={() => openChatBox()}>
<img alt="live chat" class="avatar-md rounded-circle" src={PUBLIC_LIVECHAT_ENTRY_ICON} />
<iframe
src={chatUrl}
width="0px"
height="0px"
class={`border border-2 rounded-3 m-3 float-end chat-iframe`}
title="live chat"
id={CHAT_FRAME_ID}
/>
</button>
</div>
{/if}
Expand Down
310 changes: 310 additions & 0 deletions src/lib/common/MultiSelect.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
<script>
import { onMount, createEventDispatcher, tick, afterUpdate } from "svelte";
import { Input } from "@sveltestrap/sveltestrap";
import { clickoutsideDirective } from "$lib/helpers/directives";

const svelteDispatch = createEventDispatcher();

/** @type {string} */
export let tag;

/** @type {any[]} */
export let options = [];

/** @type {boolean} */
export let selectAll = true;

/** @type {string} */
export let searchPlaceholder = '';

/** @type {string} */
export let containerClasses = "";

/** @type {string} */
export let containerStyles = "";

/** @type {boolean} */
export let disableDefaultStyles = false;

/** @type {null | undefined | (() => Promise<any>)} */
export let onScrollMoreOptions = null;

/** @type {string} */
let searchValue = '';

/** @type {boolean} */
let selectAllChecked = false;

/** @type {boolean} */
let showOptionList = false;

/** @type {any[]} */
let innerOptions = [];

/** @type {any[]} */
let refOptions = [];

/** @type {string} */
let displayText = '';

/** @type {boolean} */
let loading = false;

onMount(() => {
innerOptions = options.map(x => {
return {
id: x.id,
name: x.name,
checked: false
}
});

refOptions = options.map(x => {
return {
id: x.id,
name: x.name,
checked: false
}
});
});


async function toggleOptionList() {
showOptionList = !showOptionList;
if (showOptionList) {
await tick();
adjustDropdownPosition();
}
}


/** @param {any} e */
function changeSearchValue(e) {
searchValue = e.target.value || '';
if (searchValue) {
innerOptions = refOptions.filter(x => x.name.includes(searchValue));
} else {
innerOptions = refOptions;
}

verifySelectAll();
}


/**
* @param {any} e
* @param {any} option
*/
function checkOption(e, option) {
const found = innerOptions.find(x => x.id == option.id);
found.checked = e.target.checked;

const refFound = refOptions.find(x => x.id == option.id);
refFound.checked = e.target.checked;
changeDisplayText();
sendEvent();
}

/** @param {any} e */
function checkSelectAll(e) {
selectAllChecked = e.target.checked;
innerOptions = innerOptions.map(x => {
return { ...x, checked: selectAllChecked }
});

syncChangesToRef(selectAllChecked);
changeDisplayText();
sendEvent();
}

/** @param {boolean} checked */
function syncChangesToRef(checked) {
const ids = innerOptions.map(x => x.id);
refOptions = refOptions.map(x => {
if (ids.includes(x.id)) {
return {
...x,
checked: checked
};
}

return { ...x };
});
}

function changeDisplayText() {
const count = refOptions.filter(x => x.checked).length;
if (count === 0) {
displayText = '';
} else if (count === options.length) {
displayText = `All selected (${count})`;
} else {
displayText = `Selected (${count})`;
}

verifySelectAll();
}

function verifySelectAll() {
if (!selectAll) return;

const innerCount = innerOptions.filter(x => x.checked).length;
if (innerCount < innerOptions.length) {
selectAllChecked = false;
} else if (innerCount === innerOptions.length) {
selectAllChecked = true;
}
}

/** @param {any} e */
function handleClickOutside(e) {
e.preventDefault();

const curNode = e.detail.currentNode;
const targetNode = e.detail.targetNode;

if (!curNode?.contains(targetNode)) {
showOptionList = false;
}
}

function sendEvent() {
svelteDispatch("select", {
selecteds: refOptions.filter(x => !!x.checked)
});
}

function adjustDropdownPosition() {
const btn = document.getElementById(`multiselect-btn-${tag}`);
const optionList = document.getElementById(`multiselect-list-${tag}`);

if (!btn || !optionList) return;

const btnRec = btn.getBoundingClientRect();
const windowHeight = window.innerHeight;
const spaceBelow = windowHeight - btnRec.bottom;
const spaceAbove = btnRec.top;
const listHeight = optionList.offsetHeight;

if (spaceBelow < listHeight && spaceAbove > listHeight) {
optionList.style.top = `-${listHeight}px`;
optionList.style.bottom = 'auto';
}
}

function innerScroll() {
if (onScrollMoreOptions != null && onScrollMoreOptions != undefined) {
const dropdown = document.getElementById(`multiselect-list-${tag}`);
if (!dropdown || loading) return;

if (dropdown.scrollHeight - dropdown.scrollTop - dropdown.clientHeight <= 1) {
loading = true;
onScrollMoreOptions().then(res => {
loading = false;
}).catch(err => {
loading = false;
});
}
}
}

$: {
if (options.length > refOptions.length) {
const curIds = refOptions.map(x => x.id);
const newOptions = options.filter(x => !curIds.includes(x.id)).map(x => {
return {
id: x.id,
name: x.name,
checked: false
};
});

innerOptions = [
...innerOptions,
...newOptions
];

refOptions = [
...refOptions,
...newOptions
];

changeDisplayText();
}
}
</script>


<div
class="{disableDefaultStyles ? '' : 'multiselect-container'} {containerClasses}"
style={`${containerStyles}`}
use:clickoutsideDirective
on:clickoutside={(/** @type {any} */ e) => handleClickOutside(e)}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<ul
class="display-container"
id={`multiselect-btn-${tag}`}
on:click={() => toggleOptionList()}
>
<Input
type="text"
class='clickable'
value={displayText}
readonly
/>
<div class={`display-suffix ${showOptionList ? 'show-list' : ''}`}>
<i class="bx bx-chevron-down" />
</div>
</ul>
{#if showOptionList}
<ul class="option-list" id={`multiselect-list-${tag}`} on:scroll={() => innerScroll()}>
<div class="search-box">
<div class="search-prefix">
<i class="bx bx-search-alt" />
</div>
<Input
type="text"
value={searchValue}
placeholder={searchPlaceholder}
on:input={e => changeSearchValue(e)}
/>
</div>
{#if innerOptions.length > 0}
{#if selectAll}
<li class="option-item">
<div class="line-align-center select-box">
<Input
type="checkbox"
checked={selectAllChecked}
on:change={e => checkSelectAll(e)}
/>
</div>
<div class="line-align-center select-name fw-bold">
{'Select all'}
</div>
</li>
{/if}
{#each innerOptions as option, idx (idx)}
<li class="option-item">
<div class="line-align-center select-box">
<Input
type="checkbox"
checked={option.checked}
on:change={e => checkOption(e, option)}
/>
</div>
<div class="line-align-center select-name">
{option.name}
</div>
</li>
{/each}
{:else}
<li class="option-item">
<div class='nothing'>Nothing...</div>
</li>
{/if}
</ul>
{/if}
</div>
2 changes: 2 additions & 0 deletions src/lib/helpers/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { EditorType, UserRole } from "./enums";

export const CHAT_FRAME_ID = "chatbox-frame";

export const USER_SENDERS = [
UserRole.Admin,
UserRole.User,
Expand Down
Loading
Loading