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 单元格的值 单元格所在行的数据项/p>
- 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。
function setTooltip(container: HTMLElement, content: string | HTMLElement): void
+ function setTooltip(container: HTMLElement, content: string | HTMLElement, flag?: boolean): void
要设置 tooltip 的元素 @@ -14,6 +14,10 @@
要设置的 tooltip 内容,允许为字符串或者 HTML 元素
++ 是否启用严格模式,只有显示不完整时才显示 tooltip +
function resolveTooltip(container?: HTMLElement): HTMLElement
把超链接解析替换为图标
+function escapeHtml(text: string): 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