diff --git a/css/dropdown.scss b/css/dropdown.scss index 1ef27a0..1f3baaa 100644 --- a/css/dropdown.scss +++ b/css/dropdown.scss @@ -13,14 +13,14 @@ $searchInputHeight: 26px; $searchIconSize: 13px; $listMaxHeight: 210px; -.dropdown-wrapper { +.drop-wrapper { display: inline-block; border: none; border-radius: unset; user-select: none; position: relative; - >.dropdown-header { + >.drop-header { border: 1px solid $borderColor; border-radius: $borderRadius; background-color: $bgColor; @@ -39,7 +39,7 @@ $listMaxHeight: 210px; outline: none; } - /*>.dropdown-select-container { + /*>.drop-select-container { flex: 1 1 auto; overflow-x: auto; white-space: nowrap; @@ -104,7 +104,7 @@ $listMaxHeight: 210px; } }*/ - >.dropdown-text { + >.drop-text { flex: 1 1 auto; cursor: pointer; font-size: $mediumSize; @@ -117,7 +117,7 @@ $listMaxHeight: 210px; white-space: nowrap; } - >input.dropdown-text { + >input.drop-text { cursor: initial; &::placeholder { @@ -126,7 +126,7 @@ $listMaxHeight: 210px; } } - >.dropdown-caret { + >.drop-caret { flex: 0 0 auto; width: $caretWidth; display: flex; @@ -154,14 +154,14 @@ $listMaxHeight: 210px; // box-shadow: none; } - >.dropdown-text, - >.dropdown-caret { + >.drop-text, + >.drop-caret { cursor: default; } } } - >.dropdown-box { + >.drop-box { position: absolute; visibility: hidden; opacity: 0; @@ -192,7 +192,7 @@ $listMaxHeight: 210px; transform: scaleY(1); } - >.dropdown-search { + >.drop-search { box-sizing: border-box; height: $searchBarHeight; line-height: $searchBarHeight; @@ -235,7 +235,7 @@ $listMaxHeight: 210px; } } - >.dropdown-list { + >.drop-list { margin: 0; padding: 0; list-style: none; diff --git a/css/functions/checkbox.scss b/css/functions/checkbox.scss index 99250c8..fb6bb31 100644 --- a/css/functions/checkbox.scss +++ b/css/functions/checkbox.scss @@ -59,23 +59,4 @@ $boxDisabledColor: #d9d9d9; } } } - - /* - &.disabled { - .check-box-inner { - border-color: $boxDisabledColor; - cursor: default; - } - - >input:checked+.check-box-inner { - border-color: $boxDisabledColor; - background-color: $boxDisabledColor; - } - - >span { - color: $boxDisabledColor; - cursor: default; - } - } - */ } \ No newline at end of file diff --git a/css/grid.scss b/css/grid.scss index 8e0e83a..af4dcde 100644 --- a/css/grid.scss +++ b/css/grid.scss @@ -63,8 +63,8 @@ &, input[type="text"], - .dropdown-wrapper>.dropdown-header>.dropdown-text, - .dropdown-wrapper>.dropdown-box>.dropdown-list { + .drop-wrapper>.drop-header>.drop-text, + .drop-wrapper>.drop-box>.drop-list { font-size: var(--font-size); } @@ -268,22 +268,22 @@ } } - .dropdown-wrapper { + .drop-wrapper { height: calc(var(--line-height) + 2px); width: 100%; display: flex; flex-direction: column; - >.dropdown-header { + >.drop-header { border: none; height: 100%; - >.dropdown-text { + >.drop-text { padding: var(--spacing-cell); } } - >.dropdown-box { + >.drop-box { top: calc(var(--line-height) + 2px); &.slide-up { diff --git a/css/popup.scss b/css/popup.scss new file mode 100644 index 0000000..869e26c --- /dev/null +++ b/css/popup.scss @@ -0,0 +1,94 @@ +@import './functions/func.scss'; +@import './variables/definition.scss'; + +$headerLineHeight: 24px; +$buttonHeight: 36px; + +.popup-mask { + position: fixed; + @include inset(0, 0, 0, 0); + background-color: rgba(0 0 0 /20%); + display: flex; + justify-content: center; + align-items: center; + opacity: 0; + z-index: 200; + transition: opacity .12s ease; + + &.active .popup-container { + transform: scale(1.1); + } + + .popup-container { + min-width: 400px; + max-width: 800px; + background-color: $bgColor; + border-radius: 2px; + box-shadow: 0 2px 8px rgba(0 0 0 /11%); + transition: all .12s ease; + position: absolute; + display: flex; + flex-direction: column; + + .popup-header { + flex: 0 0 auto; + padding: 10px 12px 6px; + line-height: $headerLineHeight; + user-select: none; + background-color: var(--title-bg-color); + color: var(--title-color); + display: flex; + + >div { + flex: 1 1 auto; + font-size: 1rem; + } + + >svg { + flex: 0 0 auto; + width: $headerLineHeight; + height: $headerLineHeight; + padding: 4px; + cursor: pointer; + box-sizing: border-box; + + &:hover { + opacity: .8; + } + } + } + + .popup-body { + margin: 6px 10px; + flex: 1 1 auto; + line-height: $headerLineHeight; + } + + .popup-footer { + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: flex-end; + padding: 4px 10px 16px 2px; + + .popup-button { + margin-left: 8px; + border: none; + height: $buttonHeight; + padding: 0 8px; + min-width: 60px; + cursor: pointer; + background-color: var(--title-bg-color); + + &:hover { + opacity: .8; + } + + &:focus, + &:focus-visible { + outline: none; + } + } + } + } +} \ No newline at end of file diff --git a/lib/app.js b/lib/app.js index 4969f8e..a0ac1cc 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,5 +1,7 @@ import CustomerCommunication from "./app/communications/customer"; +import InternalComment from "./app/communications/internal"; export { - CustomerCommunication + CustomerCommunication, + InternalComment } \ No newline at end of file diff --git a/lib/app/communications/customer.js b/lib/app/communications/customer.js index 11920ca..336d4c7 100644 --- a/lib/app/communications/customer.js +++ b/lib/app/communications/customer.js @@ -1,11 +1,13 @@ import "./style.scss"; -import { createElement, createElementInit } from "../../functions"; +import { createElement } from "../../functions"; import { r } from "../../utility/lgres"; +import { nullOrEmpty } from "../../utility/strings"; import { formatUrl, isEmail, isPhone } from "../../utility"; import { setTooltip } from "../../ui/tooltip"; import { createIcon } from "../../ui/icon"; import { createCheckbox } from "../../ui/checkbox"; import { createBox } from "./lib"; +import { createPopup } from "../../ui/popup"; class CustomerCommunication { #container; @@ -23,123 +25,223 @@ class CustomerCommunication { get autoUpdatesEnabled() { return this.#autoUpdates?.disabled !== true } set autoUpdatesEnabled(flag) { const element = this.#autoUpdates; - element != null && (element.disabled = flag === false); + if (element == null) { + return; + } + if (flag === false) { + element.disabled = true; + element.parentElement?.classList?.add('disabled'); + } else { + element.disabled = false; + element.parentElement?.classList?.remove('disabled'); + } } get autoUpdates() { return this.#autoUpdates?.checked } set autoUpdates(flag) { const element = this.#autoUpdates; - element != null && (element.checked = flag); + if (element == null) { + return; + } + element.checked = flag; + element.dispatchEvent(new Event('change')); } get #statusLink() { return this.#container.querySelector('.check-status-link') } get statusLinkEnabled() { return this.#statusLink?.disabled !== true } set statusLinkEnabled(flag) { const element = this.#statusLink; - element != null && (element.disabled = flag === false); + if (element == null) { + return; + } + if (flag === false) { + element.disabled = true; + element.parentElement?.classList?.add('disabled'); + } else { + element.disabled = false; + element.parentElement?.classList?.remove('disabled'); + } } get statusLink() { return this.#statusLink?.checked } set statusLink(flag) { const element = this.#statusLink; - element != null && (element.checked = flag); + if (element == null) { + return; + } + element.checked = flag; + element.dispatchEvent(new Event('change')); } get text() { return this.#enter?.value } set text(s) { const element = this.#enter; - element != null && (element.value = s); + if (element != null) { + element.value = s + s = String(nullOrEmpty(s) ? 0 : val.length) + '/3000'; + this.#container.querySelector('.message-bar .prompt-count').innerText = s; + } + } + + #createContactItem(c) { + if (c.OptOut || c.OptOut_BC) { + return null; + } + const mp = String(c.MobilePhone).trim(); + const email = String(c.Email).trim(); + if (c.ContactPreference === '0' && !isPhone(mp) || + c.ContactPreference === '1' && !isEmail(email)) { + return null; + } + const to = c.ContactPreference === '0' ? mp : email; + return createElement('div', 'contact-item', + createIcon('fa-light', c.ContactPreference === '0' ? 'comment-lines' : 'envelope'), + setTooltip(createElement('span', span => { + span.dataset.to = to; + span.dataset.name = c.Name; + span.innerText = to; + }), to, true) + ); } get contacts() { return [...this.#contacts.children].map(el => { const span = el.querySelector('span'); - return { 'Key': span.innerText, 'Value': span.dataset.name }; + return { 'Key': span.dataset.to, 'Value': span.dataset.name }; }); } set contacts(contacts) { this.#contacts.replaceChildren(); if (contacts?.length > 0) { for (let c of contacts) { - if (c.OptOut || c.OptOut_BC) { - continue; + const item = this.#createContactItem(c); + if (item != null) { + this.#contacts.appendChild(item); } - const mp = String(c.MobilePhone).trim(); - const email = String(c.Email).trim(); - if (c.ContactPreference === '0' && !isPhone(mp) || - c.ContactPreference === '1' && !isEmail(email)) { - continue; - } - const to = c.ContactPreference === '0' ? mp : email; - this.#contacts.appendChild( - createElement('div', 'contact-item', - createIcon('fa-light', c.ContactPreference === '0' ? 'comment-lines' : 'envelope'), - createElementInit('span', span => { - span.dataset.name = c.Name; - span.innerText = to; - }) - ) - ) } + this.#message.scrollTop = this.#message.scrollHeight } } + /** + * @param {boolean} flag + */ + set readonly(flag) { + this.#option.readonly = flag; + if (this.#container == null) { + return; + } + this.#container.querySelector('.button-edit-contacts').style.display = flag === true ? 'none' : ''; + this.#container.querySelector('.button-edit-followers').style.display = flag === true ? 'none' : ''; + this.#enter.disabled = flag === true; + } + + get followers() { + return [...this.#followers.children].map(el => { + const span = el.querySelector('span'); + return { 'Key': span.dataset.to, 'Value': span.dataset.name }; + }); + } + set followers(followers) { + this.#followers.replaceChildren(); + if (followers?.length > 0) { + this.#container.querySelector('.follower-bar').style.display = ''; + for (let f of followers) { + const item = this.#createContactItem(f); + if (item != null) { + this.#followers.appendChild(item); + } + } + } else { + this.#container.querySelector('.follower-bar').style.display = 'none'; + } + this.#message.scrollTop = this.#message.scrollHeight + } + create() { // functions const checkAutoUpdate = createCheckbox({ className: 'check-auto-update', - checked: this.#option.autoUpdates?.checked, - enabled: this.#option.autoUpdates?.enabled, + checked: this.#option.autoUpdates, checkedNode: createIcon('fa-regular', 'redo-alt'), uncheckedNode: createIcon('fa-regular', 'ban'), - onchange: () => { + onchange: function () { setTooltip(checkAutoUpdate, this.checked ? r('autoUpdateEnabled', 'Auto Updates Enabled') : r('autoUpdateDisabled', 'Auto Updates Disabled')); - if (typeof this.#option.autoUpdates?.onchanged === 'function') { - this.#option.autoUpdates.onchanged(this.checked); - } } }); const checkLink = createCheckbox({ className: 'check-status-link', - checked: this.#option.statusLink?.checked, - enabled: this.#option.statusLink?.enabled, + checked: this.#option.statusLink, checkedNode: createIcon('fa-regular', 'link'), uncheckedNode: createIcon('fa-regular', 'unlink'), - onchange: () => { + onchange: function () { setTooltip(checkLink, this.checked ? r('statusLinkIncluded', 'Status Link Included') : r('statusLinkExcluded', 'Status Link Excluded')); - if (typeof this.#option.statusLink?.onchanged === 'function') { - this.#option.statusLink.onchanged(this.checked); - } } }); const container = createBox( createElement('div', null, - createElementInit('div', div => div.innerText = r('messages', 'Customer Communication')), - createElementInit('div', div => div.innerText = consts.user?.companyName)), + createElement('div', div => div.innerText = r('messages', 'Customer Communication')), + createElement('div', div => { + div.className = 'title-company'; + if (nullOrEmpty(this.#option.companyName)) { + div.style.display = 'none'; + } else { + div.innerText = this.#option.companyName; + } + }) + ), [ - setTooltip(checkAutoUpdate, r('autoUpdateDisabled', 'Auto Updates Disabled')), + setTooltip(checkAutoUpdate, r('autoUpdateEnabled', 'Auto Updates Enabled')), setTooltip(checkLink, r('statusLinkExcluded', 'Status Link Excluded')) ] ); // contacts + const readonly = this.#option.readonly; const contacts = createElement('div'); container.append( createElement('div', 'contact-bar', - createIcon('fa-solid', 'user-circle', { - 'fill': 'lightgray', - 'flex': '0 0 auto' - }), - createElementInit('div', div => div.style.flex = '1 1 auto', + createElement('div', 'bar-icon', + createIcon('fa-solid', 'user-circle', { + 'fill': 'lightgray' + }) + ), + createElement('div', 'bar-list', contacts, - createElementInit('button', button => { - button.className = 'roundbtn'; + createElement('button', button => { + button.className = 'roundbtn button-edit-contacts'; button.style.backgroundColor = 'rgb(1, 199, 172)'; + if (readonly === true) { + button.style.display = 'none'; + } button.appendChild(createIcon('fa-solid', 'user-edit')); setTooltip(button, r('editContacts', 'Edit Contacts')); button.addEventListener('click', () => { // TODO: + const pop = createPopup( + createElement('div', div => { + div.style.display = 'flex'; + div.append( + createElement('span', span => { + span.style.flex = '1 1 auto'; + span.innerText = r('editContacts', 'Edit Contacts'); + }), + createElement('button', button => { + button.style.flex = '0 0 auto'; + button.className = 'roundbtn button-add-contact'; + button.backgroundColor = 'rgb(1, 199, 172)'; + button.appendChild(createIcon('fa-regular', 'user')); + setTooltip(button, r('addContact', 'Add Contact')) + }) + ) + }), + createElement('div', div => { + div.style.height = '300px'; + div.innerText = 'Contacts from Customer Record'; + })); + container.append(pop); + setTimeout(() => pop.style.opacity = 1, 0); }); }) ) @@ -149,20 +251,27 @@ class CustomerCommunication { // followers const followers = createElement('div'); container.append( - createElement('div', 'contact-bar follower-bar', - createIcon('fa-solid', 'user-tag', { - 'fill': '#fff', - 'background-color': 'lightgray', - 'box-sizing': 'border-box', - 'border-radius': '15px', - 'padding': '4px', - 'flex': '0 0 auto' - }), - createElementInit('div', div => div.style.flex = '1 1 auto', + createElement('div', div => { + div.className = 'contact-bar follower-bar'; + div.style.display = 'none'; + }, + setTooltip(createElement('div', 'bar-icon', + createIcon('fa-solid', 'user-tag', { + 'fill': '#fff', + 'background-color': 'lightgray', + 'box-sizing': 'border-box', + 'border-radius': '15px', + 'padding': '4px' + }) + ), r('copied', 'Copied')), + createElement('div', 'bar-list', followers, - createElementInit('button', button => { - button.className = 'roundbtn'; + createElement('button', button => { + button.className = 'roundbtn button-edit-followers'; button.style.backgroundColor = 'rgb(48, 107, 255)'; + if (readonly === true) { + button.style.display = 'none'; + } button.appendChild(createIcon('fa-solid', 'pen')); setTooltip(button, r('editFollower', 'Edit Followers')); button.addEventListener('click', () => { @@ -175,20 +284,31 @@ class CustomerCommunication { this.#followers = followers; // enter box const enter = createElement('textarea'); - enter.placeholder = r('typeComment', 'Enter Message Here'); + enter.placeholder = r('typeMessage', 'Enter Message Here'); enter.maxLength = 3000; + if (readonly === true) { + enter.disabled = true; + } + enter.addEventListener('input', () => { + const val = this.#enter.value; + const s = String(nullOrEmpty(val) ? 0 : val.length) + '/3000'; + this.#container.querySelector('.message-bar .prompt-count').innerText = s; + }); this.#enter = enter; container.appendChild( createElement('div', 'message-bar', enter, - createElementInit('div', div => div.style.textAlign = 'right', - createElementInit('button', button => { - button.className = 'roundbtn'; + createElement('div', div => div.style.textAlign = 'right', + createElement('div', 'prompt-count'), + createElement('button', button => { + button.className = 'roundbtn button-send-message'; button.style.backgroundColor = 'rgb(19, 150, 204)'; button.appendChild(createIcon('fa-solid', 'paper-plane')); setTooltip(button, r('sendMessage', 'Send Message')); button.addEventListener('click', () => { - // TODO: Add text + if (typeof this.#option.onAddMessage === 'function') { + this.#option.onAddMessage(this.#enter.value); + } }) }) ) @@ -240,7 +360,7 @@ class CustomerCommunication { if (sendto !== '') { sendto = r('sendToColon', 'Send To :') + `\n${sendto}`; } - div.appendChild(createElementInit('div', div => { + div.appendChild(createElement('div', div => { div.className = 'item-poster'; div.innerText = name; if (!comm.IsReply && sendto?.length > 0) { @@ -248,14 +368,17 @@ class CustomerCommunication { } })); const content = createElement('div', 'item-content'); - if (/https?:\/\//i.test(comm.Message)) { - content.innerHTML = formatUrl(comm.Message); - } else { - content.innerText = comm.Message; - } + content.appendChild(createElement('span', span => { + if (/https?:\/\//i.test(comm.Message)) { + span.innerHTML = formatUrl(comm.Message); + } else { + span.innerText = comm.Message; + } + })); if (comm.IsReply) { div.classList.add('item-other'); } else { + div.classList.add('item-self'); const [status, statusmsg] = this.#getMessageStatus(comm); if (status !== -100) { let statustext; @@ -284,7 +407,7 @@ class CustomerCommunication { content.style.backgroundColor = '#ffc107'; break; } - const divstatus = createElementInit('div', div => { + const divstatus = createElement('div', div => { div.className = 'item-status'; div.innerText = statustext; if (status == -10) { @@ -296,7 +419,7 @@ class CustomerCommunication { } div.append( content, - createElementInit('div', div => { + createElement('div', div => { div.className = 'item-time'; div.innerText = comm.TimeStr; }) @@ -304,10 +427,10 @@ class CustomerCommunication { children.push(div); } children[0].style.marginTop = '0'; - this.#message.append(...children); - this.#message.scrollTop = this.#message.scrollHeight - // setTimeout(() => this.#message.scrollTop = this.#message.scrollHeight, 0); } + this.#message.replaceChildren(...children); + this.#message.scrollTop = this.#message.scrollHeight + // setTimeout(() => this.#message.scrollTop = this.#message.scrollHeight, 0); } #getMessageStatus(comm) { diff --git a/lib/app/communications/internal.js b/lib/app/communications/internal.js new file mode 100644 index 0000000..940bcda --- /dev/null +++ b/lib/app/communications/internal.js @@ -0,0 +1,124 @@ +import "./style.scss"; +import { createElement } from "../../functions"; +import { r } from "../../utility/lgres"; +import { nullOrEmpty } from "../../utility/strings"; +import { escapeHtml } from "../../utility"; +import { setTooltip } from "../../ui/tooltip"; +import { createIcon } from "../../ui/icon"; +import { createBox } from "./lib"; + +class InternalComment { + #container; + #option; + #enter; + #message; + + constructor(opt) { + this.#option = opt ?? {}; + } + + get text() { return this.#enter?.value } + set text(s) { + const element = this.#enter; + if (element != null) { + element.value = s + s = String(nullOrEmpty(s) ? 0 : val.length) + '/3000'; + this.#container.querySelector('.message-bar .prompt-count').innerText = s; + } + } + + create() { + const container = createBox( + createElement('div', null, + createElement('div', div => div.innerText = r('internalComments', 'Internal Comments')) + ), [] + ); + // enter box + const enter = createElement('textarea'); + enter.placeholder = r('typeComment', 'Enter Comment Here'); + enter.maxLength = 3000; + enter.addEventListener('input', () => { + const val = this.#enter.value; + const s = String(nullOrEmpty(val) ? 0 : val.length) + '/3000'; + this.#container.querySelector('.message-bar .prompt-count').innerText = s; + }); + this.#enter = enter; + container.appendChild( + createElement('div', 'message-bar', + enter, + createElement('div', div => div.style.textAlign = 'right', + createElement('div', 'prompt-count'), + createElement('button', button => { + button.className = 'roundbtn button-send-message'; + button.style.backgroundColor = 'rgb(19, 150, 204)'; + button.appendChild(createIcon('fa-solid', 'paper-plane')); + setTooltip(button, r('sendMessage', 'Send Message')); + button.addEventListener('click', () => { + if (typeof this.#option.onAddMessage === 'function') { + this.#option.onAddMessage(this.#enter.value); + } + }) + }), + createElement('button', button => { + button.className = 'roundbtn button-post-note'; + button.style.border = '1px solid rgb(19, 150, 204)'; + button.style.fill = 'rgb(19, 150, 204)'; + button.appendChild(createIcon('fa-solid', 'comment-alt-lines')); + setTooltip(button, r('postNote', 'Post Note')); + button.addEventListener('click', () => { + if (typeof this.#option.onAddComment === 'function') { + this.#option.onAddComment(this.#enter.value); + } + }) + }) + ) + ) + ); + + const message = createElement('div', 'list-bar'); + this.#message = message; + container.appendChild(message); + return this.#container = container; + } + + load(data) { + const children = []; + if (data?.length > 0) { + for (let comment of data) { + const div = createElement('div', 'item-div'); + // if (sendto !== '') { + // sendto = r('sendToColon', 'Send To :') + `\n${sendto}`; + // } + div.appendChild(createElement('div', div => { + div.className = 'item-poster'; + div.innerText = comment.UserName; + })); + const content = createElement('div', 'item-content'); + content.appendChild(createElement('span', span => span.innerText = escapeHtml(comment.Comment))); + if (comment.FollowUp?.length > 0) { + div.classList.add('item-sent'); + const sendto = r('sendToColon', 'Send To :') + '\r\n' + comment.FollowUp.split(';').join('\r\n'); + content.appendChild(createElement('div', div => { + div.className = 'item-status'; + div.innerText = r('sent', 'Sent'); + setTooltip(div, sendto); + })); + } + div.append( + content, + createElement('div', div => { + div.className = 'item-time'; + div.innerText = comment.SubmitDateStr; + }) + ); + children.push(div); + } + children[0].style.marginTop = '0'; + } + this.#message.replaceChildren(...children); + this.#message.scrollTop = this.#message.scrollHeight + // setTimeout(() => this.#message.scrollTop = this.#message.scrollHeight, 0); + } +} + +export default InternalComment; \ No newline at end of file diff --git a/lib/app/communications/lib.js b/lib/app/communications/lib.js index 41fedfb..95467b1 100644 --- a/lib/app/communications/lib.js +++ b/lib/app/communications/lib.js @@ -1,4 +1,4 @@ -import { createElement, createElementInit } from "../../functions"; +import { createElement } from "../../functions"; function createBox(title, functions) { const container = createElement('div', 'comm'); diff --git a/lib/app/communications/style.scss b/lib/app/communications/style.scss index b19967e..d7adc14 100644 --- a/lib/app/communications/style.scss +++ b/lib/app/communications/style.scss @@ -1,10 +1,6 @@ :root { - --dark-fore-color: #fff; - --dark-fore-opacity-color: rgba(255, 255, 255, .6); --title-color: #fff; --title-bg-color: rgb(68, 114, 196); - --strong-color: #333; - --medium-font-size: .875rem; } .comm { @@ -13,6 +9,15 @@ width: 320px; background-color: var(--dark-fore-color); border: 1px solid var(--title-bg-color); + margin-left: 12px; + + & { + --dark-fore-color: #fff; + --dark-fore-opacity-color: rgba(255, 255, 255, .6); + --strong-color: #333; + --light-color: #ccc; + --medium-font-size: .875rem; + } .roundbtn { width: 30px; @@ -86,6 +91,7 @@ line-height: 24px; display: flex; align-items: center; + font-size: 1.2em; >div { flex: 1 1 auto; @@ -94,6 +100,7 @@ >.title-functions { flex: 0 0 auto; display: flex; + padding: 4px; >label { margin: 0 4px; @@ -122,6 +129,19 @@ opacity: .6; } } + + &.disabled { + cursor: default; + opacity: .6; + + &:hover { + background-color: var(--dark-fore-color); + + >svg { + opacity: unset; + } + } + } } } } @@ -132,29 +152,41 @@ display: flex; border-bottom: 1px solid var(--title-bg-color); - >svg { - width: 30px; - height: 30px; - margin: 0 8px; - } - - .contact-item { - display: flex; - align-items: center; - line-height: 22px; + >.bar-icon { + flex: 0 0 auto; >svg { - flex: 0 0 auto; - width: 16px; - height: 16px; - margin-right: 6px; - fill: var(--strong-color); + width: 30px; + height: 30px; + margin: 0 8px; } + } - >span { - flex: 1 1 auto; - color: var(--strong-color); - font-size: var(--medium-font-size); + >.bar-list { + flex: 1 1 auto; + width: calc(100% - 46px); + + .contact-item { + display: flex; + align-items: center; + line-height: 22px; + + >svg { + flex: 0 0 auto; + width: 16px; + height: 16px; + margin-right: 6px; + fill: var(--strong-color); + } + + >span { + flex: 1 1 auto; + color: var(--strong-color); + font-size: var(--medium-font-size); + overflow: hidden; + text-overflow: ellipsis; + padding-right: 10px; + } } } @@ -182,6 +214,11 @@ >div { padding: 0 10px 10px; + + >.prompt-count { + display: inline-block; + color: var(--light-color); + } } } @@ -206,64 +243,78 @@ &:last-child { border-bottom: none; } - } - .item-poster { - font-weight: bold; - align-self: flex-end; + .item-poster { + font-weight: bold; + align-self: flex-start; - .tooltip-wrapper>.tooltip-content { - font-weight: normal; - } - } - - .item-content { - line-height: 1.2rem; - padding: 8px 20px; - border-radius: 5px; - white-space: pre-wrap; - word-break: break-word; - max-width: 240px; - margin-right: 10px; - background-color: #9eea6a; - align-self: flex-end; - - a>svg { - width: 13px; - height: 13px; - fill: #2140fb; - - &:hover { - border-bottom: 1px solid; + .tooltip-wrapper>.tooltip-content { + font-weight: normal; } } - .item-status { - text-align: right; - margin-top: 3px; - font-weight: bold; - margin-right: -12px; - font-size: 10px; - } - } - - .item-time { - align-self: flex-end; - color: #aaa; - font-size: .7rem; - } - - &.item-other { - - .item-poster, - .item-content { - align-self: flex-start; - } - .item-content { + line-height: 1.2rem; + padding: 8px 20px; + border-radius: 5px; + white-space: pre-wrap; + word-break: break-word; + max-width: 240px; background-color: rgb(244, 244, 244); - margin-right: unset; - margin-left: 10px; + + a>svg { + width: 13px; + height: 13px; + fill: #2140fb; + + &:hover { + border-bottom: 1px solid; + } + } + + >span::after { + content: ''; + display: block; + } + + .item-status { + text-align: right; + margin-top: 3px; + font-weight: bold; + margin-right: -12px; + font-size: 10px; + float: right; + } + } + + .item-time { + align-self: flex-end; + color: #aaa; + font-size: .7rem; + } + + &.item-other { + .item-content { + margin-left: 10px; + align-self: flex-start; + } + } + + &.item-self { + + .item-poster, + .item-content { + align-self: flex-end; + } + + .item-content { + margin-right: 10px; + background-color: #9eea6a; + } + } + + &.item-sent .item-content { + background-color: rgb(164, 226, 251); } } } diff --git a/lib/functions.d.ts b/lib/functions.d.ts index 8b5b3d4..3205237 100644 --- a/lib/functions.d.ts +++ b/lib/functions.d.ts @@ -1,2 +1 @@ -export function createElement(tagName: K, className?: string, ...children?: (Node | string)[]): HTMLElementTagNameMap[K]; -export function createElementInit(tagName: K, init?: (element: HTMLElementTagNameMap[K]) => void, ...children?: (Node | string)[]): HTMLElementTagNameMap[K]; \ No newline at end of file +export function createElement(tagName: K, init?: string | ((element: HTMLElementTagNameMap[K]) => void), ...children?: (Node | string)[]): HTMLElementTagNameMap[K]; \ No newline at end of file diff --git a/lib/functions.js b/lib/functions.js index 717ef7b..d52765e 100644 --- a/lib/functions.js +++ b/lib/functions.js @@ -1,19 +1,12 @@ -function createElement(tagName, className, ...children) { - return createElementInit(tagName, className && (element => element.className = className), ...children); -} - -function createElementInit(tagName, init, ...children) { +export function createElement(tagName, init, ...children) { const element = document.createElement(tagName); if (typeof init === 'function') { init(element); + } else if (init != null) { + element.className = init; } if (children.length > 0) { element.append(...children); } return element; } - -export { - createElement, - createElementInit -} \ No newline at end of file diff --git a/lib/ui/checkbox.js b/lib/ui/checkbox.js index 8663a95..583f4d6 100644 --- a/lib/ui/checkbox.js +++ b/lib/ui/checkbox.js @@ -1,4 +1,4 @@ -import { createElement, createElementInit } from "../functions"; +import { createElement } from "../functions"; import { createIcon } from "./icon"; function fillCheckbox(container, type, label) { @@ -9,14 +9,14 @@ function fillCheckbox(container, type, label) { container.appendChild(label); } else if (label?.length > 0) { container.appendChild( - createElementInit('span', span => span.innerText = label) + createElement('span', span => span.innerText = label) ); } } function createCheckbox(opts = {}) { const container = createElement('label', 'checkbox-wrapper', - createElementInit('input', input => { + createElement('input', input => { input.setAttribute('type', 'checkbox'); if (opts.checked === true) { input.checked = true; diff --git a/lib/ui/dropdown.html b/lib/ui/dropdown.html index 902fcd9..219f570 100644 --- a/lib/ui/dropdown.html +++ b/lib/ui/dropdown.html @@ -279,7 +279,7 @@ display: flex; } - #dropdown-sample>.dropdown-wrapper { + #dropdown-sample>.drop-wrapper { width: 200px; margin-right: 10px; } diff --git a/lib/ui/dropdown.js b/lib/ui/dropdown.js index b436734..f783696 100644 --- a/lib/ui/dropdown.js +++ b/lib/ui/dropdown.js @@ -20,7 +20,7 @@ if (dropdownGlobal == null) { configurable: false, enumerable: false, value: function () { - const panel = document.querySelector('.dropdown-wrapper .dropdown-box.active'); + const panel = document.querySelector('.drop-wrapper .drop-box.active'); if (panel == null) { return; } @@ -40,7 +40,7 @@ if (dropdownGlobal == null) { document.addEventListener('mousedown', e => { let parent = e.target; while (parent != null) { - if (parent.classList.contains('dropdown-box')) { + if (parent.classList.contains('drop-box')) { e.stopPropagation(); return; } @@ -111,14 +111,14 @@ class Dropdown { const options = this.#options; // wrapper - const wrapper = createElement('div', 'dropdown-wrapper'); + const wrapper = createElement('div', 'drop-wrapper'); const dropId = String(Math.random()).substring(2); wrapper.dataset.dropId = dropId; dropdownGlobal[dropId] = this; this.#wrapper = wrapper; // header - const header = createElement('div', 'dropdown-header'); + const header = createElement('div', 'drop-header'); header.addEventListener('click', () => { if (this.disabled) { return; @@ -136,7 +136,7 @@ class Dropdown { // label or input let label; if (options.input) { - label = createElement('input', 'dropdown-text'); + label = createElement('input', 'drop-text'); label.setAttribute('type', 'text'); options.placeholder && label.setAttribute('placeholder', options.placeholder); isPositive(options.maxlength) && label.setAttribute('maxlength', options.maxlength); @@ -151,7 +151,7 @@ class Dropdown { label.addEventListener('mousedown', e => this.#expanded && e.stopPropagation()); } else { isPositive(options.tabindex) && header.setAttribute('tabindex', options.tabindex); - label = createElement('label', 'dropdown-text'); + label = createElement('label', 'drop-text'); } this.#label = label; if (options.multiselect) { @@ -164,7 +164,7 @@ class Dropdown { } else if (options.selected != null) { this.select(options.selected, true); } - header.append(label, createElement('label', 'dropdown-caret')); + header.append(label, createElement('label', 'drop-caret')); wrapper.appendChild(header); this.disabled = options.disabled || false; @@ -173,16 +173,16 @@ class Dropdown { get multiselect() { return this.#options.multiselect } - get disabled() { return this.#wrapper == null || this.#wrapper.querySelector('.dropdown-header.disabled') != null } + get disabled() { return this.#wrapper == null || this.#wrapper.querySelector('.drop-header.disabled') != null } set disabled(flag) { if (this.#wrapper == null) { return; } if (flag) { - this.#wrapper.querySelector('.dropdown-header').classList.add('disabled'); + this.#wrapper.querySelector('.drop-header').classList.add('disabled'); } else { - this.#wrapper.querySelector('.dropdown-header').classList.remove('disabled'); + this.#wrapper.querySelector('.drop-header').classList.remove('disabled'); } } @@ -275,16 +275,16 @@ class Dropdown { } } - get #expanded() { return this.#container?.style.visibility === 'visible' } + get #expanded() { return this.#container?.style?.visibility === 'visible' } #dropdown(flag = true) { const options = this.#options; let panel = this.#container; if (panel == null) { - panel = createElement('div', 'dropdown-box'); + panel = createElement('div', 'drop-box'); // search box if (!options.input && options.search) { - const search = createElement('div', 'dropdown-search'); + const search = createElement('div', 'drop-search'); const input = createElement('input'); input.setAttribute('type', 'text'); isPositive(options.tabindex) && input.setAttribute('tabindex', options.tabindex); @@ -298,7 +298,7 @@ class Dropdown { panel.appendChild(search); } // list - const list = createElement('ul', 'dropdown-list'); + const list = createElement('ul', 'drop-list'); if (!this.multiselect) { list.addEventListener('click', e => { let li = e.target; @@ -321,7 +321,7 @@ class Dropdown { if (flag) { let source = this.source; if (!options.input && options.search) { - const search = panel.querySelector('.dropdown-search > input'); + const search = panel.querySelector('.drop-search > input'); if (!nullOrEmpty(search?.value)) { source = filterSource(options.searchkeys, options.textkey, search.value, source); } @@ -348,7 +348,7 @@ class Dropdown { } #filllist(source) { - const list = this.#container.querySelector('.dropdown-list'); + const list = this.#container.querySelector('.drop-list'); list.replaceChildren(); const multiselect = this.multiselect; const allchecked = this.#allChecked; diff --git a/lib/ui/grid.d.ts b/lib/ui/grid.d.ts index 28a1c72..bfa9669 100644 --- a/lib/ui/grid.d.ts +++ b/lib/ui/grid.d.ts @@ -5,6 +5,11 @@ interface GridItem { displayValue: string; } +interface GridSourceItem { + value: string; + text: string; +} + declare var GridColumn: { create(): HTMLElement; createEdit(trigger: (e: any) => void, col: GridColumnDefinition, body: HTMLElement): HTMLElement; @@ -44,7 +49,7 @@ interface GridColumnDefinition { sortFilter?: (a: GridItem | any, b: GridItem | any) => -1 | 0 | 1; bgFilter?: (item: GridItem | any) => string; dropOptions?: DropdownOptions; - source?: Array | ((item: GridItem | any) => Array); + source?: Array | ((item: GridItem | any) => Array | Promise>); iconType?: string; text?: string; tooltip?: string; diff --git a/lib/ui/grid.html b/lib/ui/grid.html index 949e160..2bdfa62 100644 --- a/lib/ui/grid.html +++ b/lib/ui/grid.html @@ -85,7 +85,7 @@ sortFilter?: (a: GridItem | any, b: GridItem | any) => -1 | 0 | 1; bgFilter?: (item: GridItem | any) => string; dropOptions?: DropdownOptions; - source?: Array<any> | ((item: GridItem | any) => Array<any>); + source?: Array<any> | ((item: GridItem | any) => Array<any> | Promise<Array<GridSourceItem>>); iconType?: string; text?: string; tooltip?: string; @@ -136,8 +136,8 @@ val: any

单元格的值

item: GridItem | any -

单元格所在行的数据项/p> - col: GridColumnDefinition +

单元格所在行的数据项

+ col: GridColumnDefinition

单元格所在列的定义对象


示例

@@ -198,12 +198,22 @@ key: 'c2a', caption: '下拉', type: Grid.ColumnTypes.Dropdown, - source: [ - { value: 'off', text: 'Off' }, - { value: 'pending', text: 'Pending' }, - { value: 'broken', text: 'Broken' }, - { value: 'running', text: 'Running' } - ], + source: item => { + if (item.source == null) { + return new Promise((resolve, reject) => { + setTimeout(() => { + item.source = [ + { value: 'off', text: 'Off' }, + { value: 'pending', text: 'Pending' }, + { value: 'broken', text: 'Broken' }, + { value: 'running', text: 'Running' } + ]; + resolve(item.source); + }, 2000); + }); + } + return item.source; + }, enabled: 'enabled', onchanged: (item, value) => console.log('dropdown changed', item, value) }, diff --git a/lib/ui/grid.js b/lib/ui/grid.js index df8609c..c641a35 100644 --- a/lib/ui/grid.js +++ b/lib/ui/grid.js @@ -1,7 +1,7 @@ import { global, isPositive, isMobile, throttle, truncate } from "../utility"; import { nullOrEmpty } from "../utility/strings"; import { r } from "../utility/lgres"; -import { createElement, createElementInit } from "../functions"; +import { createElement } from "../functions"; import { createIcon } from "./icon"; import { createCheckbox } from "./checkbox"; import { setTooltip } from "./tooltip"; @@ -85,7 +85,7 @@ class GridDropdownColumn extends GridColumn { return drop.create(); } - static getDrop(element) { + static #getDrop(element) { const dropGlobal = global[SymbolDropdown]; if (dropGlobal == null) { return null; @@ -98,7 +98,7 @@ class GridDropdownColumn extends GridColumn { return drop; } - static getSource(item, col) { + static #getSource(item, col) { let source = col.source; if (typeof source === 'function') { source = source(item); @@ -106,23 +106,37 @@ class GridDropdownColumn extends GridColumn { return source; } + static #setValue(source, element, val) { + const data = source?.find(v => v.value === val); + if (data != null) { + val = data.text; + } + super.setValue(element, val); + } + static setValue(element, val, item, col) { if (element.tagName !== 'DIV') { - let source = this.getSource(item, col); - const data = source?.find(v => v.value === val); - if (data != null) { - val = data.text; + let source = this.#getSource(item, col); + if (source instanceof Promise) { + source.then(s => this.#setValue(s, element, val)); + } else { + this.#setValue(source, element, val); } - super.setValue(element, val); return; } - const drop = this.getDrop(element); + const drop = this.#getDrop(element); if (drop == null) { return; } if (drop.source == null || drop.source.length === 0) { - let source = this.getSource(item, col); - if (source != null) { + let source = this.#getSource(item, col); + if (source instanceof Promise) { + source.then(s => { + drop.source = s; + drop.select(val, true); + }) + return; + } else if (source != null) { drop.source = source; } } @@ -134,7 +148,7 @@ class GridDropdownColumn extends GridColumn { } static setEnabled(element, enabled) { - const drop = this.getDrop(element); + const drop = this.#getDrop(element); if (drop == null) { return; } @@ -316,7 +330,7 @@ class Grid { get selectedIndex() { return (this.#selectedIndexes && this.#selectedIndexes[0]) ?? -1 } - get loading() { return this.#refs.loading?.style.visibility === 'visible' } + get loading() { return this.#refs.loading?.style?.visibility === 'visible' } set loading(flag) { if (this.#refs.loading == null) { return; diff --git a/lib/ui/popup.js b/lib/ui/popup.js new file mode 100644 index 0000000..1124a9d --- /dev/null +++ b/lib/ui/popup.js @@ -0,0 +1,45 @@ +import "../../css/popup.scss"; +import { createElement } from "../functions"; +import { createIcon } from "./icon"; + +function createPopup(title, content, ...buttons) { + const mask = createElement('div', 'popup-mask'); + const container = createElement('div', 'popup-container'); + const close = () => { + mask.classList.add('popup-active'); + mask.style.opacity = 0; + setTimeout(() => mask.remove(), 120); + }; + container.append( + createElement('div', header => { + header.className = 'popup-header'; + if (title instanceof HTMLElement) { + header.appendChild(title); + } else { + header.appendChild(createElement('div', t => t.innerText = title)); + } + const cancel = createIcon('fa-regular', 'times'); + cancel.addEventListener('click', () => close()); + header.appendChild(cancel); + }), + createElement('div', 'popup-body', content), + createElement('div', 'popup-footer', ...buttons.map(b => { + const button = createElement('div', 'popup-button'); + button.innerText = b.text; + if (typeof b.trigger === 'function') { + button.addEventListener('click', () => { + if (b.trigger(container) === false) { + return; + } + close(); + }); + } + })) + ); + mask.appendChild(container); + return mask; +} + +export { + createPopup +} \ No newline at end of file diff --git a/lib/ui/tooltip.d.ts b/lib/ui/tooltip.d.ts index de17fd8..1e0a69d 100644 --- a/lib/ui/tooltip.d.ts +++ b/lib/ui/tooltip.d.ts @@ -1,2 +1,2 @@ -export function setTooltip(container: HTMLElement, content: string | HTMLElement): HTMLElement +export function setTooltip(container: HTMLElement, content: string | HTMLElement, flag?: boolean): HTMLElement export function resolveTooltip(container?: HTMLElement): HTMLElement \ No newline at end of file diff --git a/lib/ui/tooltip.html b/lib/ui/tooltip.html index c33feae..ecb66c0 100644 --- a/lib/ui/tooltip.html +++ b/lib/ui/tooltip.html @@ -5,7 +5,7 @@ 给某个元素或者页面上含有 title 属性的元素设置一个统一样式的 tooltip。

setTooltip

- function setTooltip(container: HTMLElement, content: string | HTMLElement): void + function setTooltip(container: HTMLElement, content: string | HTMLElement, flag?: boolean): void

container: HTMLElement

要设置 tooltip 的元素 @@ -14,6 +14,10 @@

要设置的 tooltip 内容,允许为字符串或者 HTML 元素

+

flag?: boolean

+

+ 是否启用严格模式,只有显示不完整时才显示 tooltip +

resolveTooltip

function resolveTooltip(container?: HTMLElement): HTMLElement

container?: HTMLElement

diff --git a/lib/ui/tooltip.js b/lib/ui/tooltip.js index 312bf46..c57d5c4 100644 --- a/lib/ui/tooltip.js +++ b/lib/ui/tooltip.js @@ -1,18 +1,18 @@ -import { createElement, createElementInit } from "../functions"; +import { createElement } from "../functions"; -function setTooltip(container, content) { +function setTooltip(container, content, flag = false) { const tip = container.querySelector('.tooltip-wrapper'); if (tip != null) { tip.remove(); } - const wrapper = createElementInit('div', wrapper => { + const wrapper = createElement('div', wrapper => { wrapper.className = 'tooltip-wrapper tooltip-color'; wrapper.style.visibility = 'hidden'; wrapper.style.opacity = 0; }, createElement('div', 'tooltip-pointer tooltip-color'), createElement('div', 'tooltip-curtain tooltip-color'), - createElementInit('div', cnt => { + createElement('div', cnt => { cnt.className = 'tooltip-content'; if (content instanceof HTMLElement) { cnt.appendChild(content); @@ -27,32 +27,35 @@ function setTooltip(container, content) { let tid; container.addEventListener('mouseenter', () => { tid && clearTimeout(tid); - tid = setTimeout(() => { - while (container?.offsetWidth == null) { - container = container.parentElement; - } - if (container == null) { - return; - } - let parent = container; - let left = container.offsetLeft; - let top = container.offsetTop; - while ((parent = parent.offsetParent) != null) { - left += parent.offsetLeft; - top += parent.offsetTop; - } - parent = container; - while ((parent = parent.parentElement) != null) { - left -= parent.scrollLeft; - top -= parent.scrollTop; - } - left -= wrapper.offsetWidth / 2 - container.offsetWidth / 2; - top -= wrapper.offsetHeight + 14; - wrapper.style.left = `${left}px`; - wrapper.style.top = `${top}px`; - wrapper.style.visibility = 'visible'; - wrapper.style.opacity = 1; - }, 100); + let c = container; + while (c?.offsetWidth == null) { + c = c.parentElement; + } + if (c == null) { + return; + } + if (!flag || c.scrollWidth > c.offsetWidth) { + tid = setTimeout(() => { + let parent = c; + let left = c.offsetLeft; + let top = c.offsetTop; + while ((parent = parent.offsetParent) != null) { + left += parent.offsetLeft; + top += parent.offsetTop; + } + parent = c; + while ((parent = parent.parentElement) != null) { + left -= parent.scrollLeft; + top -= parent.scrollTop; + } + left -= wrapper.offsetWidth / 2 - c.offsetWidth / 2; + top -= wrapper.offsetHeight + 14; + wrapper.style.left = `${left}px`; + wrapper.style.top = `${top}px`; + wrapper.style.visibility = 'visible'; + wrapper.style.opacity = 1; + }, 100); + } }); container.addEventListener('mouseleave', () => { tid && clearTimeout(tid); diff --git a/lib/utility.js b/lib/utility.js index 55fafb0..343c536 100644 --- a/lib/utility.js +++ b/lib/utility.js @@ -1,7 +1,7 @@ import { getCookie, setCookie, deleteCookie } from "./utility/cookie"; import { init, r, lang } from "./utility/lgres"; import { get, post, upload } from "./utility/request"; -import { nullOrEmpty, contains, endsWith, padStart, formatUrl } from "./utility/strings"; +import { nullOrEmpty, contains, endsWith, padStart, formatUrl, escapeHtml } from "./utility/strings"; let g = typeof globalThis !== 'undefined' ? globalThis : self; @@ -58,6 +58,7 @@ export { endsWith, padStart, formatUrl, + escapeHtml, // variables g as global, isPositive, diff --git a/lib/utility/strings.d.ts b/lib/utility/strings.d.ts index c318863..c2b30ea 100644 --- a/lib/utility/strings.d.ts +++ b/lib/utility/strings.d.ts @@ -2,4 +2,5 @@ export function nullOrEmpty(s?: string | any | null): boolean export function contains(s: string, key: string | any, ignoreCase?: boolean): boolean export function endsWith(s: string, suffix: string): boolean export function padStart(s: string, num: Number, char: string): boolean -export function formatUrl(msg: string): string \ No newline at end of file +export function formatUrl(msg: string): string +export function escapeHtml(text: string): string \ No newline at end of file diff --git a/lib/utility/strings.html b/lib/utility/strings.html index 5f524e9..416c06c 100644 --- a/lib/utility/strings.html +++ b/lib/utility/strings.html @@ -54,6 +54,12 @@

把超链接解析替换为图标

+

escapeHtml

+ function escapeHtml(text: string): string +

text: string

+

+ 解析转换 html 代码为显示内容 +


用法

const util = window["lib-utility"];
diff --git a/lib/utility/strings.js b/lib/utility/strings.js
index 0c3c235..8cb83ab 100644
--- a/lib/utility/strings.js
+++ b/lib/utility/strings.js
@@ -34,7 +34,7 @@ function formatUrl(msg) {
     //const urlArrray = str.match(urlReg);
     const p = /(http|ftp|https):\/\/.+?(\s|\r\n|\r|\n|\"|\'|\*|$)/g;
     const r = msg.match(p);
-    msg = htmlencode(msg);
+    msg = escapeHtml(msg);
 
     if (r?.length > 0) {
         const rs = [];
@@ -50,7 +50,17 @@ function formatUrl(msg) {
         }
     }
 
-    return msg
+    return msg;
+}
+
+function escapeHtml(text) {
+    if (text == null) {
+        return '';
+    }
+    return String(text)
+        .replaceAll('&', '&')
+        .replaceAll('<', '<')
+        .replaceAll('>', '>')
         .replaceAll('\r\n', '
') .replaceAll('\n', '
') .replaceAll(' ', ' '); @@ -61,5 +71,6 @@ export { contains, endsWith, padStart, - formatUrl + formatUrl, + escapeHtml } \ No newline at end of file