diff --git a/lib/app/communications/comments.js b/lib/app/communications/comments.js
index 387936d..c55c19b 100644
--- a/lib/app/communications/comments.js
+++ b/lib/app/communications/comments.js
@@ -51,6 +51,11 @@ export default class CustomerRecordComment {
this._var.enter.disabled = flag;
this._var.container.querySelector('.button-send-message').disabled = flag;
}
+ get replyMsgId() { return this._var.replymsgid || -1 }
+ set replyMsgId(v) {
+ this._var.replymsgid = null;
+ this._var.replymsgctrl.style.display = 'none'
+ }
/**
* @param {boolean} flag
@@ -111,6 +116,24 @@ export default class CustomerRecordComment {
container.appendChild(
createElement('div', 'message-bar',
enter,
+ createElement('div', div => {
+ div.className = 'customer-reply';
+ div.style.display = 'none';
+ this._var.replymsgctrl = div;
+ },
+ createElement('span', span => {
+ span.className = 'reply-user';
+ }),
+ createElement('span', span => {
+ span.className = 'reply-msg';
+ }),
+ createElement('layer', layer => {
+ layer.appendChild(createIcon('fa-light', 'times'));
+ layer.addEventListener('click', () => {
+ this.replyMsgId = null;
+ });
+ })
+ ),
createElement('div', div => div.style.textAlign = 'right',
createElement('div', 'prompt-count'),
createElement('button', button => {
@@ -124,7 +147,8 @@ export default class CustomerRecordComment {
setTooltip(button, r('FLTL_02301', 'Post Note'));
button.addEventListener('click', () => {
if (typeof this._var.option.onAddComment === 'function') {
- this._var.option.onAddComment(this.text);
+ this._var.option.onAddComment(this.text, this._var.replymsgid);
+ this.replyMsgId = null;
}
})
})
@@ -158,6 +182,13 @@ export default class CustomerRecordComment {
div.innerText = comment.UserName;
}));
const content = createElement('div', 'item-content');
+ if (comment.ReplyMessage) {
+ const reply = createElement('div', div => {
+ div.className = 'reply';
+ div.innerHTML = comment.ReplyMessage.Comment;
+ });
+ content.appendChild(reply);
+ }
const mmsParts = createElement('div', div => div.style.display = 'none');
content.appendChild(createElement('span', span => span.innerHTML = escapeHtml(escapeEmoji(comment.Comment)), mmsParts));
if (comment.MMSParts?.length > 0) {
diff --git a/lib/app/communications/customer.js b/lib/app/communications/customer.js
index 21e24dd..ac590f3 100644
--- a/lib/app/communications/customer.js
+++ b/lib/app/communications/customer.js
@@ -188,10 +188,7 @@ export default class CustomerCommunication {
this._var.contacts.replaceChildren();
if (contacts?.length > 0) {
var cs = contacts.sort(function (a, b) {
- if (a.Name == b.Name) {
- return 0;
- }
- return a.Name > b.Name ? 1 : -1;
+ return String(a.Name).localeCompare(String(b.Name));
});
const messages = this._var.data.messages;
if (this._var.contactsUpdated !== true && messages?.length > 0) {
@@ -308,6 +305,8 @@ export default class CustomerCommunication {
*/
set companyName(name) {
this._var.option.companyName = name;
+ this._var.container.querySelector('.button-edit-contacts').style.display =
+ this._var.option.recordReadonly && nullOrEmpty(name) ? 'none' : '';
const div = this._var.container.querySelector('.title-company');
const companyCode = div.querySelector('.title-company-code');
if (companyCode != null) {
@@ -674,7 +673,7 @@ export default class CustomerCommunication {
createElement('button', button => {
button.className = 'roundbtn button-edit-contacts';
button.style.backgroundColor = 'rgb(1, 199, 172)';
- if (readonly === true) {
+ if (readonly === true || (recordReadonly && nullOrEmpty(option.companyName))) {
button.style.display = 'none';
}
button.appendChild(createIcon('fa-solid', 'user-edit'));
diff --git a/lib/app/communications/internal.js b/lib/app/communications/internal.js
index 3315350..e062fac 100644
--- a/lib/app/communications/internal.js
+++ b/lib/app/communications/internal.js
@@ -106,6 +106,11 @@ export default class InternalComment {
}
}
}
+ get replyMsgId() { return this._var.replymsgid || -1 }
+ set replyMsgId(v) {
+ this._var.replymsgid = null;
+ this._var.replymsgctrl.style.display = 'none'
+ }
/**
* @param {boolean} flag
@@ -192,6 +197,24 @@ export default class InternalComment {
});
},
enter,
+ createElement('div', div => {
+ div.className = 'customer-reply';
+ div.style.display = 'none';
+ this._var.replymsgctrl = div;
+ },
+ createElement('span', span => {
+ span.className = 'reply-user';
+ }),
+ createElement('span', span => {
+ span.className = 'reply-msg';
+ }),
+ createElement('layer', layer => {
+ layer.appendChild(createIcon('fa-light', 'times'));
+ layer.addEventListener('click', () => {
+ this.replyMsgId = null;
+ });
+ })
+ ),
createElement('div', div => div.style.textAlign = 'right',
createElement('div', 'customer-left',
createElement('div', 'file-selector',
@@ -280,7 +303,7 @@ export default class InternalComment {
}
if (typeof option.onAddComment === 'function') {
this.loading = true;
- option.onAddComment(this.text, this.file);
+ option.onAddComment(this.text, this.file, this.replyMsgId);
}
})
})
@@ -323,6 +346,15 @@ export default class InternalComment {
}
}));
const content = createElement('div', 'item-content');
+ if (comment.ReplyMessage) {
+ const reply = createElement('div', div => {
+ div.className = 'reply';
+ div.innerHTML = comment.ReplyMessage.Message;
+ if (comment.ReplyMessage.MessageType !== 2)
+ div.title = comment.ReplyMessage.Sender + " " + comment.ReplyMessage.TimeStr + "\r\n" + comment.ReplyMessage.Message;
+ });
+ content.appendChild(reply);
+ }
const mmsParts = createElement('div', div => div.style.display = 'none');
content.appendChild(createElement('span', span => {
if (comment.MessageType === 2) {
diff --git a/lib/app/communications/lib.js b/lib/app/communications/lib.js
index 6f31bb4..d9adac2 100644
--- a/lib/app/communications/lib.js
+++ b/lib/app/communications/lib.js
@@ -511,6 +511,33 @@ export function createHideMessageCommentTail(This, optionName, comment, commentT
span.appendChild(icon);
span.addEventListener('click', () => hisFunc(comment.Id));
}),
+ createElement('span', span => {
+ span.className = 'sbutton iconreply';
+ span.style.padding = '0';
+ span.style.fontSize = '12px';
+ setTooltip(span, option?.getText('FLTL_03432', 'Reply'));
+ span.style.display = comment.AllowReply ? '' : 'none';
+ if (comment.AllowReply) {
+ span.addEventListener('click', function () {
+ This._var.replymsgid = comment.Id;
+ This._var.replymsgctrl.querySelector('.reply-user').innerText = (comment.Sender || comment.UserName) + ":";
+ var msg = comment.Message || comment.Comment;
+ if (comment.MessageType == 2)
+ msg = option?.getText('FLTL_00491', 'Call Log');
+ This._var.replymsgctrl.querySelector('.reply-msg').textContent = msg;
+ This._var.replymsgctrl.style.display = '';
+ });
+ }
+ }),
+ createElement('span', span => {
+ span.style.margin = '0 5px 0 0';
+ span.style.color = '#2594fd';
+ span.style.display = comment.ReplyCount > 0 ? '' : 'none';
+ if (comment.ReplyCount > 1)
+ span.innerText = comment.ReplyCount + ' ' + option?.getText('FLTL_03433', 'Replies');
+ else
+ span.innerText = comment.ReplyCount + ' ' + option?.getText('FLTL_03432', 'Reply');
+ }),
createElement('span', span => {
span.innerText = comment[commentTime];
})
diff --git a/lib/app/communications/style.scss b/lib/app/communications/style.scss
index d058c73..362e559 100644
--- a/lib/app/communications/style.scss
+++ b/lib/app/communications/style.scss
@@ -55,7 +55,7 @@
&:hover {
background-color: var(--dark-fore-opacity-color);
- >svg {
+ > svg {
opacity: .6;
}
}
@@ -65,12 +65,12 @@
fill: lightgray;
background-color: transparent !important;
- &:hover>svg {
+ &:hover > svg {
opacity: unset;
}
}
- >svg {
+ > svg {
width: 13px;
height: 14px;
display: block;
@@ -88,19 +88,19 @@
align-items: center;
font-size: var(--font-larger-size);
- >div {
+ > div {
flex: 1 1 auto;
- >.title-company {
+ > .title-company {
line-height: 1rem;
padding: 2px 10px;
// background-color: rgba(0, 0, 0, .15);
- >.title-company-name {
+ > .title-company-name {
font-weight: bold;
}
- >.title-company-selector {
+ > .title-company-selector {
cursor: pointer;
vertical-align: middle;
@@ -108,7 +108,7 @@
background-color: #ccc;
}
- >svg {
+ > svg {
width: 14px;
height: 14px;
fill: rgb(123, 28, 33);
@@ -119,12 +119,12 @@
}
}
- >.title-functions {
+ > .title-functions {
flex: 0 0 auto;
display: flex;
padding: 0 4px;
- >label {
+ > label {
margin: 0 4px;
box-sizing: border-box;
cursor: pointer;
@@ -137,7 +137,7 @@
justify-content: center;
transition: background-color .2s;
- >svg {
+ > svg {
fill: var(--strong-color);
width: 14px;
height: 14px;
@@ -147,7 +147,7 @@
&:hover {
background-color: var(--dark-fore-opacity-color);
- >svg {
+ > svg {
opacity: .6;
}
}
@@ -159,7 +159,7 @@
&:hover {
background-color: var(--dark-fore-color);
- >svg {
+ > svg {
opacity: unset;
}
}
@@ -175,17 +175,17 @@
border-bottom: 1px solid var(--title-ctrlbg-color);
position: relative;
- >.bar-icon {
+ > .bar-icon {
flex: 0 0 auto;
- >svg {
+ > svg {
width: 30px;
height: 30px;
margin: 0 8px;
}
}
- >.bar-list {
+ > .bar-list {
flex: 1 1 auto;
width: calc(100% - 46px);
@@ -202,7 +202,7 @@
align-items: center;
line-height: 22px;
- >svg {
+ > svg {
flex: 0 0 auto;
width: 16px;
height: 16px;
@@ -210,7 +210,7 @@
fill: var(--strong-color);
}
- >span {
+ > span {
// flex: 1 1 auto;
color: var(--strong-color);
font-size: var(--font-size);
@@ -222,14 +222,14 @@
}
}
- >.bar-info {
+ > .bar-info {
display: none;
flex: 1 1 auto;
text-align: right;
margin-right: 50px;
}
- >.bar-collapser {
+ > .bar-collapser {
position: absolute;
top: 3px;
right: 18px;
@@ -245,7 +245,7 @@
background-color: var(--light-color);
}
- >span {
+ > span {
width: 6px;
height: 6px;
position: absolute;
@@ -255,7 +255,7 @@
transform: rotate(135deg);
}
- &.collapsed>span {
+ &.collapsed > span {
top: 9px;
left: 8px;
transform: rotate(45deg);
@@ -266,7 +266,7 @@
float: right;
margin: 4px 10px 10px;
- >svg {
+ > svg {
width: 16px;
}
}
@@ -276,12 +276,12 @@
.contact-bar {
border-bottom-color: transparent;
- >.bar-icon,
- >.bar-list {
+ > .bar-icon,
+ > .bar-list {
display: none;
}
- >.bar-info {
+ > .bar-info {
display: block;
}
}
@@ -297,7 +297,7 @@
display: flex;
flex-direction: column;
- >textarea {
+ > textarea {
padding: 10px 10px 0;
border: 1px solid var(--title-ctrlbg-color);
border-radius: 5px;
@@ -311,19 +311,46 @@
@include outline();
}
- >div {
+ > .customer-reply {
+ background-color: #d3d3d3;
+ padding: 5px 10px 0 10px;
+ border-radius: 5px;
+ margin: 0 6px 3px 6px;
+ display: flex;
+ white-space: nowrap;
+ line-height: 22px;
+
+ > .reply-msg {
+ flex: 1 1 auto;
+ display: inline;
+ margin-left: 4px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ > layer {
+ > .ui-icon {
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+ fill: var(--secondary-link-color);
+ }
+ }
+ }
+
+ > div {
padding: 0 10px 10px;
- >.customer-left {
+ > .customer-left {
float: left;
text-align: left;
- >.customer-name {
- >span {
+ > .customer-name {
+ > span {
font-size: var(--font-smaller-size);
}
- >.ui-input {
+ > .ui-input {
margin-left: 4px;
width: 150px;
border-top: none;
@@ -332,39 +359,39 @@
}
}
- >.file-selector {
+ > .file-selector {
display: inline-flex;
align-items: center;
height: 30px;
- >.selector-link {
+ > .selector-link {
cursor: pointer;
display: flex;
- >svg {
+ > svg {
width: 16px;
height: 16px;
fill: var(--secondary-link-color);
}
- >input {
+ > input {
display: none;
}
}
- >.selector-name {
+ > .selector-name {
max-width: 130px;
padding: 0 20px 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- +layer {
+ + layer {
display: none;
margin-left: -20px;
cursor: pointer;
- >svg {
+ > svg {
width: 16px;
height: 16px;
fill: var(--red-color);
@@ -375,11 +402,11 @@
}
}
- &:hover+layer {
+ &:hover + layer {
display: flex;
}
- >.ui-tooltip-wrapper img {
+ > .ui-tooltip-wrapper img {
max-width: 120px;
max-height: 80px;
}
@@ -387,7 +414,7 @@
}
}
- >.prompt-count {
+ > .prompt-count {
display: inline-block;
color: var(--light-color);
font-size: var(--font-smaller-size);
@@ -423,7 +450,7 @@
font-size: var(--font-size);
align-self: flex-start;
- .ui-tooltip-wrapper>.ui-tooltip-content {
+ .ui-tooltip-wrapper > .ui-tooltip-content {
font-weight: normal;
}
}
@@ -435,13 +462,14 @@
white-space: pre-wrap;
word-break: break-word;
max-width: 240px;
- background-color: rgb(244, 244, 244);
+ /*background-color: rgb(244, 244, 244);*/
+ background-color: #f5f5f5;
audio[controls] {
width: 220px;
}
- a>svg {
+ a > svg {
width: 13px;
height: 13px;
fill: #2140fb;
@@ -451,7 +479,7 @@
}
}
- >span::after {
+ > span::after {
content: '';
display: block;
}
@@ -467,7 +495,7 @@
.ui-tooltip-content .tip-function-button {
text-align: right;
- >svg {
+ > svg {
width: 20px;
height: 20px;
cursor: pointer;
@@ -483,6 +511,21 @@
}
}
}
+
+ .reply {
+ background-color: white;
+ padding: 5px;
+ border: solid 2px #f2f2f2;
+ border-left: solid 2px lightgray;
+ border-radius: 5px;
+ margin-bottom: 2px;
+ max-height: 36px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ }
}
.item-time {
@@ -522,7 +565,7 @@
height: 100%;
min-height: 120px;
- >.ui-grid-wrapper>.ui-grid-table>tbody>.ui-grid-row>td {
+ > .ui-grid-wrapper > .ui-grid-table > tbody > .ui-grid-row > td {
vertical-align: top;
.col-icon {
@@ -532,11 +575,11 @@
.icon-contact-type {
cursor: unset;
- >svg {
+ > svg {
fill: #333;
}
- &:hover>svg {
+ &:hover > svg {
opacity: unset;
}
}
@@ -566,11 +609,11 @@
flex-direction: column;
width: 600px;
- >.follower-search {
+ > .follower-search {
margin-bottom: 6px;
}
- >.follower-grid {
+ > .follower-grid {
height: 380px;
}
}
@@ -580,7 +623,7 @@
flex-direction: column;
width: 780px;
- >.selcontact-grid {
+ > .selcontact-grid {
height: 200px;
}
}
diff --git a/lib/element/addworkorder.js b/lib/element/addworkorder.js
index 6c967bc..5b0a386 100644
--- a/lib/element/addworkorder.js
+++ b/lib/element/addworkorder.js
@@ -1,4 +1,4 @@
-import { createElement, Dropdown, Popup, showAlert, createIcon, DateSelector, showConfirm, Grid, OptionBase } from "../ui";
+import { createElement, Dropdown, Popup, showAlert, createIcon, DateSelector, showConfirm, Grid, OptionBase, createCheckbox } from "../ui";
import { nullOrEmpty } from "../utility";
import AssetSelector from "./assetSelector";
@@ -131,6 +131,11 @@ export default class AddWorkOrder extends OptionBase {
* @private
* @type {Dropdown}
*/
+ dropAssetStatus: null,
+ /**
+ * @private
+ * @type {DateSelector}
+ */
dropAssignedTo: null,
/**
* @private
@@ -221,6 +226,18 @@ export default class AddWorkOrder extends OptionBase {
Status: -1
};
}
+ let assetstatus = el.dropAssetStatus.selected;
+ if (assetstatus != null) {
+ assetstatus = {
+ AssetCustomStatus: assetstatus.Id,
+ AssetCustomStatusType: assetstatus.Type,
+ AssetCustomStatusName: assetstatus.Name
+ };
+ } else {
+ assetstatus = {
+ AssetCustomStatus: -1
+ };
+ }
let machine = this._var.asset;
if (machine == null) {
showAlert(title, this.r('FLTL_00311', 'Asset cannot be empty.')).then(() => this._var.container.querySelector('.wo-asset>svg')?.focus());
@@ -238,7 +255,7 @@ export default class AddWorkOrder extends OptionBase {
AssignedTo: el.dropAssignedTo.selected?.IID ?? '',
AdvisorId: el.dropAdvisor.selected?.Key ?? '',
LocationId: el.dropLocation.selected?.ID || -1,
- DepartmentId: el.dropDepartment.selected?.Id || -1,
+ DepartmentId: el.dropDepartment.checked?.Id || -1,
AssetID: machine.Id,
VIN: machine.VIN,
AssetName: machine.DisplayName,
@@ -250,7 +267,8 @@ export default class AddWorkOrder extends OptionBase {
LaborCost: -1,
HourlyRate: -1,
InspectionTemplateId: -1,
- ...status
+ ...status,
+ ...assetstatus
};
item.CustomerId = this._var.customer?.Id ?? -1;
item.Contacts = this._var.contacts ?? [];
@@ -259,19 +277,18 @@ export default class AddWorkOrder extends OptionBase {
showAlert(title, this.r('FLTL_00602', 'Complaint is required.')).then(() => el.textComplaint.focus());
return null;
}
+ item.MeterType = machine.OnRoad ? 'Odometer' : 'HourMeter';
if (el.dropStatus.selected?.Completed) {
if (!el.dateCompleted.element.validity.valid) {
showAlert(title, this.r('FLTL_00613', 'Completed Date cannot be empty.')).then(() => el.dateCompleted.element.focus());
return null;
}
if (machine.OnRoad) {
- item.MeterType = 'Odometer';
if (nullOrEmpty(item.Odometer) || isNaN(item.Odometer) || item.Odometer < 0) {
showAlert(title, this.r('FLTL_02044', 'Odometer format error.')).then(() => el.inputOdometer.focus());
return null;
}
} else {
- item.MeterType = 'HourMeter';
if (nullOrEmpty(item.HourMeter) || isNaN(item.HourMeter) || item.HourMeter < 0) {
showAlert(title, this.r('FLTL_01516', 'Hour Meter format error.')).then(() => el.inputHours.focus());
return null;
@@ -284,6 +301,9 @@ export default class AddWorkOrder extends OptionBase {
async show() {
const option = this._option;
const allowCustomer = option.allowCustomer === true;
+ const allowCommunicate = option.allowCommunicate === true;
+ const commReadOnly = option.commReadOnly === true;
+ const msgVariables = option.msgVariables || [];
const title = this.r('FLTL_02078', 'Open Work Order');
const tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)) + 3;
const textComplaint = createElement('textarea', textarea => {
@@ -333,6 +353,16 @@ export default class AddWorkOrder extends OptionBase {
{ value: 'Mile', text: this.r('FLTL_01922', 'Mile') },
{ value: 'Kilometre', text: this.r('FLTL_01694', 'Kilometer') }
]
+ const dropAssetStatus = new Dropdown({
+ tabIndex: tabIndex + 5,
+ htmlTemplate: it => createElement('span', 'wo-color-line',
+ createElement('em', em => em.style.backgroundColor = it.Color),
+ createElement('label', label => label.innerText = it.Name)
+ ),
+ search: true,
+ textKey: 'Name',
+ valueKey: 'Id'
+ });
const dropAssignedTo = new Dropdown({
tabIndex: tabIndex + 10,
selected: '',
@@ -360,6 +390,18 @@ export default class AddWorkOrder extends OptionBase {
valueKey: 'Id',
textKey: 'Name'
});
+ const dropMSGVariables = new Dropdown({
+ selected: -1,
+ search: true,
+ valueKey: 'Id',
+ textKey: 'Name'
+ });
+ dropMSGVariables.source = msgVariables.map(v => { return { Id: v.Name, Name: v.Name }; });
+ const textMessage = createElement('textarea', textarea => {
+ textarea.className = 'ui-text wo-message';
+ });
+ const dropMSGVariablesElement = dropMSGVariables.create();
+ dropMSGVariablesElement.style.width = '200px';
// save variables
this._var.el = {
@@ -370,6 +412,7 @@ export default class AddWorkOrder extends OptionBase {
inputHours,
inputOdometer,
dropOdometerUnit,
+ dropAssetStatus,
dropAssignedTo,
dropAdvisor,
dropLocation,
@@ -415,6 +458,7 @@ export default class AddWorkOrder extends OptionBase {
el.inputOdometer.value = '';
el.dropOdometerUnit.select('Mile');
}
+ el.dropAssetStatus.select(it.CustomStatus);
}
}
this._var.asset = it;
@@ -537,6 +581,11 @@ export default class AddWorkOrder extends OptionBase {
patternValidation(inputOdometer, '\\d+\\.?\\d*'),
dropOdometerUnit.create()
),
+ createElement('span', span => {
+ span.className = 'wo-title';
+ span.innerText = this.r('FLTL_03590', 'Asset Availability:');
+ }),
+ dropAssetStatus.create(),
createElement('span', span => {
span.className = 'wo-title';
span.innerText = this.r('FLTL_00382', 'Assigned Tech:');
@@ -606,7 +655,7 @@ export default class AddWorkOrder extends OptionBase {
buttons: [
{
key: 'open',
- text: title,
+ text: this.r('FLTL_00700', 'Create Work Order'),
trigger: async () => {
popup.loading = true;
try {
@@ -620,7 +669,7 @@ export default class AddWorkOrder extends OptionBase {
return false;
}
const next = await showConfirm(title, this.r('FLTL_02992', 'The selected asset is hidden and a work order cannot be created.') + '\n\n' + this.r('FLTL_00999', 'Do you wish to "Un-Hide" the asset?'), [
- { key: 'unhide', text: this.r('FLTL_03136', 'Unhide') },
+ { key: 'unhide', text: this.r('FLTL_03378', 'Yes') },
{ key: 'cancel', text: this.r('FLTL_00502', 'Cancel Work Order') }
]);
if (next.result !== 'unhide') {
@@ -672,10 +721,142 @@ export default class AddWorkOrder extends OptionBase {
if (next !== 'create') {
return false;
}
+
+ if ((item.Status == 100 || item.StatusType == 100)
+ && (item.AssetCustomStatus == 10 || item.AssetCustomStatusType == 10)) {
+ let msgdiv = createElement('div', div => {
+ div.style.minWidth = "300px";
+ div.style.marginLeft = "50px";
+ div.style.fontSize = "14px";
+ let msghtml = this.r('FLTL_03591', 'Asset Availability is set to');
+ msghtml += ' ' + item.AssetCustomStatusName + '';
+ msghtml += '
' + this.r('FLTL_03592', 'Would you like to update?');
+ div.innerHTML = msghtml;
+ }
+ );
+ const nextassetstatus = await showConfirm(title, msgdiv, [
+ { key: 'yes', text: this.r('FLTL_03378', 'Yes') },
+ { key: 'no', text: this.r('FLTL_01978', 'No') }
+ ]);
+ if (nextassetstatus.result !== 'no') {
+ return false;
+ }
+ }
+ }
+ }
+ const el = this._var.el;
+ if (item.Status > 0 && item.CustomerId > 0 && item.StatusMessage && allowCommunicate && !commReadOnly) {
+ const next = await new Promise(resolve => {
+ let addButton;
+ const chkSend = createCheckbox({
+ label: this.r('FLTL_02700', 'Send Update To Customer'),
+ onchange: () => {
+ const chk = chkSend.querySelector('input');
+ el.chkLink.querySelector('input').disabled = !chk.checked;
+ el.txtMessage.disabled = !chk.checked;
+ el.txtPhoneNumber.disabled = !chk.checked;
+ dropMSGVariables.disabled = !chk.checked;
+ addButton.disabled = !chk.checked;
+ }
+ });
+ el.chkSend = chkSend;
+ chkSend.style.paddingLeft = '0px';
+
+ const chkLink = createCheckbox({
+ label: this.r('FLTL_01580', 'Include Status Link')
+ });
+ el.chkLink = chkLink;
+ chkLink.style.paddingLeft = '0px';
+ el.txtMessage = textMessage;
+
+ const iconmobile = createIcon('fa-solid', 'mobile-alt');
+ const sPopup = new Popup({
+ title: this.r('FLTL_02824', 'Status Change') + " - " + item.StatusName,
+ content: createElement('div', 'wo-send-status-msg',
+ createElement('label'),
+ chkSend,
+ createElement('div', div => {
+ div.style.textAlign = 'right';
+ div.style.paddingRight = '5px';
+ div.appendChild(iconmobile);
+ }),
+ createElement('input', input => {
+ input.type = 'text';
+ el.txtPhoneNumber = input;
+ }),
+ createElement('label'),
+ chkLink,
+ createElement('label', label => {
+ label.innerText = this.r('FLTL_01912', 'Message:');
+ label.style.textAlign = 'right';
+ label.style.paddingRight = '5px';
+ }),
+ createElement('div', div => {
+ div.style.display = 'flex';
+ div.style.alignItems = 'center';
+ div.appendChild(dropMSGVariablesElement);
+ addButton = createElement('input', input => {
+ input.type = 'button';
+ input.style.marginLeft = "10px";
+ input.style.height = "24px";
+ input.value = this.r('FLTL_00083', 'Add');
+ })
+ addButton.addEventListener('click', () => {
+ const text = dropMSGVariables.selected?.Name;
+ if (!text) return;
+ textMessage.focus();
+ const startPos = textMessage.selectionStart;
+ const endPos = textMessage.selectionEnd;
+ textMessage.value = textMessage.value.substring(0, startPos) + text + textMessage.value.substring(endPos, textMessage.value.length);
+ textMessage.selectionStart = textMessage.selectionEnd = startPos + text.length;
+ });
+ div.appendChild(addButton);
+ }),
+ createElement('label'),
+ textMessage
+ ),
+ resolve,
+ buttons: [
+ { key: 'ok', text: this.r('FLTL_02582', 'Save Work Order and Send'), trigger: () => resolve('ok') },
+ { key: 'cancel', text: this.r('FLTL_00499', 'Cancel'), trigger: () => resolve('cancel') }
+ ]
+ });
+
+ var names = "";
+ for (var i = 0; i < item.Contacts.length; i++) {
+ var c = item.Contacts[i];
+ if (c.OptOut || c.OptOut_BC) continue;
+ var mp = $.trim(c.MobilePhone);
+ var email = $.trim(c.Email);
+ if ((c.ContactPreference == "0" && (checkPhoneNumber(mp))
+ || (c.ContactPreference == "1" && isEmail(email)))) {
+ if (names == "")
+ names = c.Name;
+ else
+ names += ";" + c.Name;
+ }
+ }
+ el.txtPhoneNumber.value = names;
+ el.chkSend.querySelector('input').checked = item.StatusAutoText;
+ el.chkSend.querySelector('input').dispatchEvent(new Event('change'));
+ el.txtMessage.value = item.StatusMessage;
+ dropMSGVariables.select(dropMSGVariables.source[0].Name);
+
+ sPopup.show().then(mask => {
+ });
+ });
+ if (next !== 'ok') {
+ return false;
}
}
if (typeof this.onSave === 'function') {
- this.onSave(item, this._var.el.dropStatus.selected);
+ const sendStatusInfo = (item.Status > 0 && item.CustomerId > 0 && item.StatusMessage && el.chkSend) ? {
+ SendUpdateToCustomer: el.chkSend.querySelector('input').checked,
+ IncludeStatusLink: el.chkLink.querySelector('input').checked,
+ PhoneEmails: el.txtPhoneNumber.value,
+ Comment: el.txtMessage.value,
+ } : null;
+ this.onSave(item, sendStatusInfo);
}
} finally {
popup.loading = false;
@@ -692,6 +873,7 @@ export default class AddWorkOrder extends OptionBase {
if (typeof option.requestWorkOrderParams === 'function') {
popup.loading = true;
const data = await option.requestWorkOrderParams()
+ dropAssetStatus.source = data.AssetAvailabilities;
if (!isNaN(data.AssetId) && data.AssetId > 0 && typeof option.requestAssetInfo === 'function') {
const it = await option.requestAssetInfo(data.AssetId);
if (it != null) {
@@ -712,6 +894,7 @@ export default class AddWorkOrder extends OptionBase {
dropAssignedTo.source = [{ IID: '', DisplayName: '' }, ...data];
// dropStatus.onSelected(dropStatus.selected);
}
+ dropAssetStatus.select(it.CustomStatus);
}
}
popup.loading = false;
diff --git a/lib/element/inspectionWizard.js b/lib/element/inspectionWizard.js
index a574cc8..6b50345 100644
--- a/lib/element/inspectionWizard.js
+++ b/lib/element/inspectionWizard.js
@@ -69,9 +69,16 @@ export default class InspectionWizard extends OptionBase {
requestTemplates: option.requestTemplates
});
this._var.templateSelector = templateSelector;
- assetSelector.onSelected = asset => {
+ assetSelector.onSelected = async asset => {
this._var.asset = asset;
this._var.template = null;
+ if (typeof option.getAssetUncompletedInspection === 'function') {
+ const report = await option.getAssetUncompletedInspection(asset.Id);
+ if (report) {
+ showAlert(assetSelector.title, this.r('FLTL_03601', 'The following inspection needs to be certified: ') + report.Value);
+ return false;
+ }
+ }
this._changePage(1);
templateSelector.assetId = asset.Id;
};
@@ -98,12 +105,19 @@ export default class InspectionWizard extends OptionBase {
},
{
text: this.r('FLTL_01973', 'Next'),
- trigger: () => {
+ trigger: async () => {
const asset = assetSelector.currentAsset;
if (asset == null) {
showAlert(assetSelector.title, this.r('FLTL_02269', 'Please select an Asset.'));
return false;
}
+ if (typeof option.getAssetUncompletedInspection === 'function') {
+ const report = await option.getAssetUncompletedInspection(asset.Id);
+ if (report) {
+ showAlert(assetSelector.title, this.r('FLTL_03601', 'The following inspection needs to be certified: ') + report.Value);
+ return false;
+ }
+ }
this._var.asset = asset;
this._var.template = null;
this._changePage(1);
diff --git a/lib/element/style.scss b/lib/element/style.scss
index eb4a1d8..6e4ba41 100644
--- a/lib/element/style.scss
+++ b/lib/element/style.scss
@@ -319,4 +319,32 @@
margin: 0 10px;
height: 400px;
}
+}
+
+.wo-send-status-msg {
+ width: 460px;
+ display: grid;
+ grid-template-columns: minmax(80px, auto) 1fr;
+ grid-auto-rows: minmax(32px, auto);
+
+ .ui-text {
+ width: 100%;
+ height: 80px;
+ box-sizing: border-box;
+ }
+
+ .ui-icon {
+ width: 14px;
+ height: 14px;
+ fill: rgb(123, 28, 33);
+ cursor: pointer;
+ transition: opacity .12s ease;
+
+ &:focus,
+ &:active,
+ &:hover {
+ outline: none;
+ opacity: .4;
+ }
+ }
}
\ No newline at end of file
diff --git a/lib/ui.js b/lib/ui.js
index 1826b51..8134c5f 100644
--- a/lib/ui.js
+++ b/lib/ui.js
@@ -8,7 +8,7 @@ import { createTab } from "./ui/tab";
import { Dropdown } from "./ui/dropdown";
import { Grid } from "./ui/grid/grid";
import { GridColumn, GridInputColumn, GridDropdownColumn, GridCheckboxColumn, GridIconColumn, GridTextColumn, GridDateColumn } from './ui/grid/column';
-import { Popup, createPopup, resolvePopup, showAlert, showConfirm } from "./ui/popup";
+import { Popup, createPopup, resolvePopup, showAlert, showInput, showConfirm } from "./ui/popup";
import { createPicture, createAudio, createVideo, createFile, createVideoList } from './ui/media';
import { validation, convertCssStyle } from './ui/extension';
import { createDateInput, toDateValue, getFormatter, formatDate, setDateValue, getDateValue, DateSelector } from './ui/date';
@@ -64,6 +64,96 @@ class OptionBase {
}
}
+/**
+ * 通知选项
+ * @typedef NotifyOption
+ * @property {string | HTMLElement} message - 内容,支持自定义元素
+ * @property {HTMLElement} [parent] - 目标父元素
+ * @property {string} [title] - 标题
+ * @property {"success" | "warning" | "error"} [type] - 提示类型
+ * @property {boolean} [persistent] - 是否持续显示,默认为 `false`,3 秒后自动关闭
+ * @property {number} [timeout] - 超时时间,默认 3 秒
+ * @property {(auto: boolean) => void} [onDismissed] - 关闭时触发的函数
+ */
+
+/**
+ * @private
+ * @param {HTMLDivElement} wrapper
+ * @param {(auto: boolean) => void} [onDismissed]
+ * @param {boolean} [auto]
+ */
+function closeNotify(wrapper, onDismissed, auto) {
+ wrapper.classList.remove('active');
+ setTimeout(() => {
+ wrapper.remove();
+ if (typeof onDismissed === 'function') {
+ onDismissed(auto);
+ }
+ }, 120);
+}
+
+/**
+ *
+ * @param {NotifyOption} opts
+ */
+function notify(opts) {
+ opts ||= {};
+ let timer;
+ const close = createIcon('fa-light', 'times');
+ close.classList.add('ui-notify-close');
+ close.addEventListener('click', () => {
+ timer && clearTimeout(timer);
+ closeNotify(wrapper, opts.onDismissed);
+ });
+ let type;
+ let typeClass;
+ opts.type ||= 'success';
+ switch (opts.type) {
+ case 'warning':
+ type = 'exclamation-circle';
+ typeClass = 'warning';
+ break;
+ case 'error':
+ type = 'times-circle';
+ typeClass = 'error';
+ break;
+ case 'success':
+ type = 'check-circle';
+ typeClass = 'success';
+ break;
+ default:
+ type = opts.type;
+ typeClass = 'success';
+ break;
+ }
+ const icon = createIcon('fa-solid', type);
+ icon.classList.add('ui-notify-type', typeClass);
+ const wrapper = createElement('div', 'ui-notify',
+ utility.nullOrEmpty(opts.title) ?
+ createElement('div', 'ui-notify-single',
+ icon,
+ createElement('span', 'ui-notify-message', opts.message),
+ close
+ ) :
+ createElement('div', 'ui-notify-content',
+ createElement('div', 'ui-notify-header',
+ icon,
+ createElement('h2', 'ui-notify-title', opts.title),
+ close
+ ),
+ createElement('span', 'ui-notify-message', opts.message)
+ )
+ );
+ if (!opts.persistent) {
+ timer = setTimeout(() => {
+ closeNotify(wrapper, opts.onDismissed, true);
+ }, opts.timeout || 3000);
+ }
+ (opts.parent ?? document.body).appendChild(wrapper);
+ setTimeout(() => wrapper.classList.add('active'), 10);
+ return wrapper;
+}
+
export {
createElement,
// icon
@@ -95,6 +185,7 @@ export {
createPopup,
resolvePopup,
showAlert,
+ showInput,
showConfirm,
// dateSelector
createDateInput,
@@ -119,5 +210,7 @@ export {
requestAnimationFrame,
offset,
// base classes
- OptionBase
+ OptionBase,
+ // notify
+ notify
}
diff --git a/lib/ui/checkbox.js b/lib/ui/checkbox.js
index ccb1e9c..e6123e8 100644
--- a/lib/ui/checkbox.js
+++ b/lib/ui/checkbox.js
@@ -5,7 +5,7 @@ import { createIcon } from "./icon";
function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, charactor = 'check', title) {
const checkIcon = createIcon(type, charactor);
checkIcon.classList.add('ui-check-icon');
- const indeterminateIcon = createIcon(type, 'grip-lines');
+ const indeterminateIcon = createIcon(type, 'minus');
indeterminateIcon.classList.add('ui-indeterminate-icon')
container.appendChild(
createElement('layer', layer => {
diff --git a/lib/ui/css/common.scss b/lib/ui/css/common.scss
index f07df0a..44b5d47 100644
--- a/lib/ui/css/common.scss
+++ b/lib/ui/css/common.scss
@@ -21,4 +21,107 @@
.ui-input {
text-indent: var(--text-indent);
line-height: var(--line-height);
+}
+
+.ui-loading {
+ position: absolute;
+ @include inset(0, 0, 0, 0);
+ visibility: hidden;
+ opacity: 0;
+ transition: visibility 0s linear .12s, opacity .12s ease;
+ background-color: var(--loading-bg-color);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 2;
+
+ >div {
+ width: 15px;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ animation: loading-dot 1s infinite linear alternate;
+ /*background-color: var(--loading-fore-color);
+ border-radius: var(--loading-border-radius);
+
+ >svg {
+ width: var(--loading-size);
+ height: var(--loading-size);
+ padding: 20px;
+ animation: loading-spinner 1.2s infinite linear;
+ }*/
+ }
+}
+
+.ui-notify {
+ padding: 10px 10px 12px;
+ position: fixed;
+ top: 16px;
+ right: 16px;
+ min-width: 300px;
+ max-width: 600px;
+ box-shadow: 0 3.2px 7.2px 0 rgba(0, 0, 0, .13), 0 0.6px 1.8px 0 rgba(0, 0, 0, .11);
+ transition: transform .12s ease, opacity .12s ease;
+ transform-origin: right;
+ transform: translateX(100%);
+ opacity: 0;
+
+ &.active {
+ transform: translateX(0);
+ opacity: 1;
+ }
+
+ .ui-icon {
+ width: 30px;
+ height: 30px;
+ box-sizing: border-box;
+ }
+
+ >.ui-notify-single {
+ display: flex;
+ align-items: center;
+
+ >.ui-notify-message {
+ margin: 0 6px;
+ }
+ }
+
+ .ui-notify-header {
+ display: flex;
+ align-items: center;
+ }
+
+ .ui-notify-type {
+ padding: 3px;
+ fill: var(--green-color);
+
+ &.warning {
+ fill: var(--orange-color);
+ }
+
+ &.error {
+ fill: var(--red-color);
+ }
+ }
+
+ .ui-notify-title {
+ flex: 1 1 auto;
+ font-size: 1rem;
+ margin: 0 6px;
+ }
+
+ .ui-notify-close {
+ cursor: pointer;
+ padding: 6px;
+ transition: background-color .12s ease, fill .12s ease;
+ fill: var(--red-color);
+
+ &:hover {
+ background-color: var(--red-color);
+ fill: #fff;
+ }
+ }
+
+ .ui-notify-message {
+ flex: 1 1 auto;
+ }
}
\ No newline at end of file
diff --git a/lib/ui/css/dropdown.scss b/lib/ui/css/dropdown.scss
index bd47f93..cab7f43 100644
--- a/lib/ui/css/dropdown.scss
+++ b/lib/ui/css/dropdown.scss
@@ -19,14 +19,14 @@ $listMaxHeight: 210px;
font-size: var(--font-size);
font-family: var(--font-family);
- >.ui-drop-header {
+ > .ui-drop-header {
background-color: var(--bg-color);
display: flex;
height: $headerHeight;
@include outborder();
- >.ui-drop-text {
+ > .ui-drop-text {
flex: 1 1 auto;
cursor: pointer;
font-size: var(--font-size);
@@ -41,7 +41,12 @@ $listMaxHeight: 210px;
@include outline();
}
- >input.ui-drop-text {
+ > .ui-drop-text::before {
+ content: '';
+ display: inline-block;
+ }
+
+ > input.ui-drop-text {
cursor: initial;
&::placeholder {
@@ -50,7 +55,7 @@ $listMaxHeight: 210px;
}
}
- >.ui-drop-caret {
+ > .ui-drop-caret {
flex: 0 0 auto;
width: $caretWidth;
display: flex;
@@ -79,8 +84,8 @@ $listMaxHeight: 210px;
// box-shadow: none;
}
- >.ui-drop-text,
- >.ui-drop-caret {
+ > .ui-drop-text,
+ > .ui-drop-caret {
cursor: default;
}
}
@@ -116,7 +121,7 @@ $listMaxHeight: 210px;
transform: scaleY(1);
}
- >.ui-drop-search {
+ > .ui-drop-search {
box-sizing: border-box;
height: $searchBarHeight;
line-height: $searchBarHeight;
@@ -125,7 +130,7 @@ $listMaxHeight: 210px;
display: flex;
align-items: center;
- >input[type="text"] {
+ > input[type="text"] {
box-sizing: border-box;
width: 100%;
height: $searchInputHeight;
@@ -133,7 +138,6 @@ $listMaxHeight: 210px;
color: var(--color);
@include outborder();
-
// &:focus {
// box-shadow: 0 0 3px 1px rgba(0, 0, 0, .2);
// }
@@ -143,7 +147,7 @@ $listMaxHeight: 210px;
}
}
- >svg {
+ > svg {
position: absolute;
left: 14px;
width: $searchIconSize;
@@ -152,18 +156,18 @@ $listMaxHeight: 210px;
}
}
- >.ui-drop-list {
+ > .ui-drop-list {
max-height: $listMaxHeight;
overflow-y: auto;
position: relative;
font-size: var(--font-size);
@include scrollbar();
- &.filtered>.drop-content>.li:first-child {
+ &.filtered > .drop-content > .li:first-child {
background-color: var(--hover-bg-color);
}
- >.drop-content {
+ > .drop-content {
position: absolute;
width: 100%;
}
@@ -185,21 +189,26 @@ $listMaxHeight: 210px;
background-color: var(--hover-bg-color);
}
- >.li-wrapper {
+ > .li-wrapper {
display: flex;
align-items: center;
- >.ui-expandor {
+ > .ui-expandor {
width: 12px;
height: 12px;
display: flex;
+ opacity: 0;
+
+ &.active {
+ opacity: 1;
+ }
}
- >.ui-check-wrapper {
+ > .ui-check-wrapper {
height: $dropItemHeight;
display: flex;
}
}
}
}
-}
\ No newline at end of file
+}
diff --git a/lib/ui/css/grid.scss b/lib/ui/css/grid.scss
index 0e7dc5e..25ed965 100644
--- a/lib/ui/css/grid.scss
+++ b/lib/ui/css/grid.scss
@@ -1,432 +1,303 @@
@import "./functions/func.scss";
-.ui-grid {
+.ui-grid-container {
position: relative;
- box-sizing: border-box;
- overflow: auto;
- & {
- --cell-hover-bg-color: lightyellow;
- --header-border-color: #adaba9;
- --header-bg-color: #fafafa;
- --header-fore-color: #000;
- --cell-border-color: #f0f0f0;
- --cell-fore-color: #333;
- --dark-border-color: #666;
- --split-border-color: #b3b3b3;
- --dragger-bg-color: #fff;
- --dragger-cursor-color: #333;
- --row-bg-color: #fff;
- --row-active-bg-color: #fafafa;
- --row-selected-bg-color: #e6f2fb;
- --total-row-bg-color: #b3b3b3;
- --text-disabled-color: gray;
-
- --filter-shadow: 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05);
- --filter-transition: transform .12s ease, opacity .24s ease;
-
- --row-height: 36px;
- --header-line-height: 20px;
- --text-indent: 8px;
-
- --loading-size: 40px;
- --loading-border-radius: 20px;
-
- --arrow-size: 4px;
- --filter-size: 10px;
- --split-width: 10px;
- --dragger-size: 20px;
- --dragger-opacity: .6;
- --dragger-cursor-size: 4px;
- --dragger-cursor-pos: -4px;
- --dragger-cursor-opacity: .3;
-
- --header-padding: 4px 12px 4px 8px;
- --header-filter-padding: 4px 26px 4px 8px;
- --spacing-s: 4px;
- --spacing-cell: 9px 4px 9px 8px;
- --spacing-drop-cell: 5px 4px 5px 8px;
- --filter-line-height: 30px;
- --filter-item-padding: 0 4px;
- }
-
- @include outline();
- @include scrollbar();
-
- &,
- input[type="text"],
- input[type="date"],
- textarea {
- font-size: var(--font-size);
- font-family: var(--font-family);
- }
-
- >.ui-grid-sizer {
- position: absolute;
- white-space: nowrap;
- font-weight: bold;
- visibility: hidden;
- }
-
- >.ui-grid-wrapper {
+ .ui-grid {
position: relative;
+ box-sizing: border-box;
+ overflow: auto;
- >.ui-grid-table {
+ & {
+ --cell-hover-bg-color: lightyellow;
+ --header-border-color: #adaba9;
+ --header-bg-color: #fafafa;
+ --header-fore-color: #000;
+ --cell-border-color: #f0f0f0;
+ --cell-fore-color: #333;
+ --dark-border-color: #666;
+ --split-border-color: #b3b3b3;
+ --dragger-bg-color: #fff;
+ --dragger-cursor-color: #333;
+ --row-bg-color: #fff;
+ --row-active-bg-color: #fafafa;
+ --row-selected-bg-color: #e6f2fb;
+ --total-row-bg-color: #b3b3b3;
+ --text-disabled-color: gray;
+
+ --filter-shadow: 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05);
+ --filter-transition: transform .12s ease, opacity .24s ease;
+
+ --row-height: 36px;
+ --header-line-height: 20px;
+ --text-indent: 8px;
+
+ --loading-size: 40px;
+ --loading-border-radius: 20px;
+
+ --arrow-size: 4px;
+ --filter-size: 10px;
+ --split-width: 10px;
+ --dragger-size: 20px;
+ --dragger-opacity: .6;
+ --dragger-cursor-size: 4px;
+ --dragger-cursor-pos: -4px;
+ --dragger-cursor-opacity: .3;
+
+ --header-padding: 4px 12px 4px 8px;
+ --header-filter-padding: 4px 26px 4px 8px;
+ --spacing-s: 4px;
+ --spacing-cell: 9px 4px 9px 8px;
+ --spacing-drop-cell: 5px 4px 5px 8px;
+ --filter-line-height: 30px;
+ --filter-item-padding: 0 4px;
+ }
+
+ @include outline();
+ @include scrollbar();
+
+ &,
+ input[type="text"],
+ input[type="date"],
+ textarea {
+ font-size: var(--font-size);
+ font-family: var(--font-family);
+ }
+
+ >.ui-grid-sizer {
position: absolute;
- width: 100%;
- min-width: 100%;
- margin: 0;
- border-collapse: collapse;
- border-spacing: 0;
- table-layout: fixed;
+ white-space: nowrap;
+ font-weight: bold;
+ visibility: hidden;
+ }
- >thead {
- color: var(--header-fore-color);
+ >.ui-grid-wrapper {
+ position: relative;
- tr {
-
- >th {
- background-color: var(--header-bg-color);
- user-select: none;
- padding: 0;
- margin: 0;
- word-wrap: break-word;
- white-space: normal;
- // position: relative;
- top: 0;
- position: sticky;
- z-index: 1;
-
- &.sticky {
- position: sticky;
- z-index: 2;
- }
-
- >div {
- line-height: var(--header-line-height);
- min-height: var(--row-height);
- display: flex;
- align-items: center;
- padding: var(--header-padding);
- box-sizing: border-box;
- // overflow-x: hidden;
- // border-right: 1px solid transparent;
- // transition: border-color .12s ease;
-
- >span {
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
-
- &.wrap {
- @include wrap();
- }
- }
-
- >.ui-check-wrapper,
- >.ui-switch {
- height: 20px;
- padding: 0 4px 0 0;
- }
-
- >svg {
- width: 12px;
- min-width: 12px;
- height: 12px;
- margin-left: 4px;
- fill: var(--split-border-color);
- }
- }
-
- >.arrow {
- width: 0;
- height: 0;
- top: 50%;
- margin-top: calc(0px - var(--arrow-size) / 2);
- right: calc(var(--arrow-size) / 2);
- position: absolute;
-
- &.asc {
- border-bottom: var(--arrow-size) solid var(--dark-border-color);
- }
-
- &.desc {
- border-top: var(--arrow-size) solid var(--dark-border-color);
- }
-
- &.asc,
- &.desc {
- border-left: var(--arrow-size) solid transparent;
- border-right: var(--arrow-size) solid transparent;
- }
- }
-
- >.filter {
- width: var(--filter-size);
- height: var(--filter-size);
- top: 50%;
- margin-top: calc(0px - var(--filter-size) / 2);
- right: calc(var(--arrow-size) * 2 + 4px);
- position: absolute;
- display: flex;
-
- >svg {
- width: 100%;
- height: 100%;
- fill: var(--color);
- opacity: .2;
- transition: opacity .12s ease;
-
- &:hover {
- opacity: .8;
- }
- }
-
- &.hover>svg {
- opacity: .8;
- }
-
- &.active>svg {
- opacity: 1;
- }
- }
-
- >.spliter {
- position: absolute;
- height: 100%;
- top: 0;
- right: calc(1px - var(--split-width) /2);
- width: var(--split-width);
- cursor: ew-resize;
- z-index: 2;
-
- &::after {
- content: '';
- height: 100%;
- width: 1px;
- display: block;
- margin: 0 auto;
- transition: background-color .12s ease;
- }
-
- // &:hover::after {
- // background-color: var(--split-border-color);
- // }
- }
-
- >.bottom-border {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 1px;
- z-index: 2;
- background-color: var(--header-border-color);
- }
-
- >.dragger {
- position: absolute;
- left: 0;
- top: 0;
- min-width: var(--dragger-size);
- height: 100%;
- background-color: var(--dragger-bg-color);
- opacity: var(--dragger-opacity);
- display: none;
- }
-
- >.dragger-cursor {
- position: absolute;
- top: 0;
- height: 100%;
- border: 1px solid var(--dragger-cursor-color);
- box-sizing: border-box;
- margin-left: 0;
- opacity: var(--dragger-cursor-opacity);
- display: none;
- transition: left .12s ease;
-
- &::before {
- top: -1px;
- border-top: var(--dragger-cursor-size) solid;
- }
-
- &::after {
- bottom: -1px;
- border-bottom: var(--dragger-cursor-size) solid;
- }
-
- &::before,
- &::after {
- content: '';
- position: absolute;
- left: var(--dragger-cursor-pos);
- border-left: var(--dragger-cursor-size) solid transparent;
- border-right: var(--dragger-cursor-size) solid transparent;
- }
- }
-
- &.header-filter>div {
- padding: var(--header-filter-padding);
- }
- }
-
- // &:hover>th>div {
- // border-color: var(--split-border-color);
- // }
- &:hover>th>.spliter::after {
- background-color: var(--split-border-color);
- }
- }
- }
-
- >tbody,
- >tfoot {
-
- >.ui-grid-row {
- line-height: var(--line-height);
- white-space: nowrap;
- box-sizing: border-box;
-
- >td {
- padding: 0;
-
- &.sticky {
- position: sticky;
- z-index: 1;
- }
-
- >span {
- margin: var(--spacing-cell);
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: pre;
-
- &.wrap {
- @include wrap();
- }
- }
- }
- }
- }
-
- >tfoot {
- color: var(--header-fore-color);
+ >.ui-grid-table {
position: absolute;
width: 100%;
- background-color: var(--total-row-bg-color);
+ min-width: 100%;
+ margin: 0;
+ border-collapse: collapse;
+ border-spacing: 0;
+ table-layout: fixed;
- >.ui-grid-row>td {
- font-weight: bold;
+ >thead {
+ color: var(--header-fore-color);
- &.sticky {
- background-color: var(--total-row-bg-color);
+ tr {
+
+ >th {
+ background-color: var(--header-bg-color);
+ user-select: none;
+ padding: 0;
+ margin: 0;
+ word-wrap: break-word;
+ white-space: normal;
+ // position: relative;
+ top: 0;
+ position: sticky;
+ z-index: 1;
+
+ &.sticky {
+ position: sticky;
+ z-index: 2;
+ }
+
+ >div {
+ line-height: var(--header-line-height);
+ min-height: var(--row-height);
+ display: flex;
+ align-items: center;
+ padding: var(--header-padding);
+ box-sizing: border-box;
+ // overflow-x: hidden;
+ // border-right: 1px solid transparent;
+ // transition: border-color .12s ease;
+
+ >span {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ &.wrap {
+ @include wrap();
+ }
+ }
+
+ >.ui-check-wrapper,
+ >.ui-switch {
+ height: 20px;
+ padding: 0 4px 0 0;
+ }
+
+ >svg {
+ width: 12px;
+ min-width: 12px;
+ height: 12px;
+ margin-left: 4px;
+ fill: var(--split-border-color);
+ }
+ }
+
+ >.arrow {
+ width: 0;
+ height: 0;
+ top: 50%;
+ margin-top: calc(0px - var(--arrow-size) / 2);
+ right: calc(var(--arrow-size) / 2);
+ position: absolute;
+
+ &.asc {
+ border-bottom: var(--arrow-size) solid var(--dark-border-color);
+ }
+
+ &.desc {
+ border-top: var(--arrow-size) solid var(--dark-border-color);
+ }
+
+ &.asc,
+ &.desc {
+ border-left: var(--arrow-size) solid transparent;
+ border-right: var(--arrow-size) solid transparent;
+ }
+ }
+
+ >.filter {
+ width: var(--filter-size);
+ height: var(--filter-size);
+ top: 50%;
+ margin-top: calc(0px - var(--filter-size) / 2);
+ right: calc(var(--arrow-size) * 2 + 4px);
+ position: absolute;
+ display: flex;
+
+ >svg {
+ width: 100%;
+ height: 100%;
+ fill: var(--color);
+ opacity: .2;
+ transition: opacity .12s ease;
+
+ &:hover {
+ opacity: .8;
+ }
+ }
+
+ &.hover>svg {
+ opacity: .8;
+ }
+
+ &.active>svg {
+ opacity: 1;
+ }
+ }
+
+ >.spliter {
+ position: absolute;
+ height: 100%;
+ top: 0;
+ right: calc(1px - var(--split-width) /2);
+ width: var(--split-width);
+ cursor: ew-resize;
+ z-index: 2;
+
+ &::after {
+ content: '';
+ height: 100%;
+ width: 1px;
+ display: block;
+ margin: 0 auto;
+ transition: background-color .12s ease;
+ }
+
+ // &:hover::after {
+ // background-color: var(--split-border-color);
+ // }
+ }
+
+ >.bottom-border {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ z-index: 2;
+ background-color: var(--header-border-color);
+ }
+
+ >.dragger {
+ position: absolute;
+ left: 0;
+ top: 0;
+ min-width: var(--dragger-size);
+ height: 100%;
+ background-color: var(--dragger-bg-color);
+ opacity: var(--dragger-opacity);
+ display: none;
+ }
+
+ >.dragger-cursor {
+ position: absolute;
+ top: 0;
+ height: 100%;
+ border: 1px solid var(--dragger-cursor-color);
+ box-sizing: border-box;
+ margin-left: 0;
+ opacity: var(--dragger-cursor-opacity);
+ display: none;
+ transition: left .12s ease;
+
+ &::before {
+ top: -1px;
+ border-top: var(--dragger-cursor-size) solid;
+ }
+
+ &::after {
+ bottom: -1px;
+ border-bottom: var(--dragger-cursor-size) solid;
+ }
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ left: var(--dragger-cursor-pos);
+ border-left: var(--dragger-cursor-size) solid transparent;
+ border-right: var(--dragger-cursor-size) solid transparent;
+ }
+ }
+
+ &.header-filter>div {
+ padding: var(--header-filter-padding);
+ }
+ }
+
+ // &:hover>th>div {
+ // border-color: var(--split-border-color);
+ // }
+ &:hover>th>.spliter::after {
+ background-color: var(--split-border-color);
+ }
}
}
- }
- >tbody {
- color: var(--cell-fore-color);
+ >tbody,
+ >tfoot {
- >.ui-grid-row {
- background-color: var(--row-bg-color);
- border-bottom: 1px solid var(--cell-border-color);
+ >.ui-grid-row {
+ line-height: var(--line-height);
+ white-space: nowrap;
+ box-sizing: border-box;
- &:hover {
- background-color: var(--row-active-bg-color);
-
- >td.sticky {
- background-color: var(--row-active-bg-color);
- }
- }
-
- &.selected {
- background-color: var(--row-selected-bg-color);
-
- >td.sticky {
- background-color: var(--row-selected-bg-color);
- }
- }
-
- >td {
-
- &.sticky {
- background-color: var(--row-bg-color);
- }
-
- &.ui-expandable {
- &>svg {
- width: 24px;
- height: 34px;
- padding: 8px 3px;
- box-sizing: border-box;
- display: block;
- cursor: pointer;
- transition: opacity .12s ease;
-
- &:hover {
- opacity: .4;
- }
- }
- }
-
- >input[type="text"],
- >input[type="date"],
- >textarea {
- border: none;
- box-sizing: border-box;
- width: 100%;
+ >td {
padding: 0;
- max-height: none !important;
- @include outline();
-
- &:disabled {
- color: var(--text-disabled-color);
+ &.sticky {
+ position: sticky;
+ z-index: 1;
}
- }
-
- >input[type="text"] {
- height: var(--row-height);
- text-indent: var(--text-indent);
- }
-
- >textarea {
- resize: none;
- line-height: var(--line-height);
- display: block;
- padding: var(--spacing-cell);
- white-space: nowrap;
- @include scrollbar();
- }
-
- .ui-check-wrapper,
- .ui-switch {
- display: inline-flex;
- justify-content: center;
- height: var(--row-height);
- padding: 0 8px;
-
- .ui-check-inner {
-
- &,
- >svg {
- transition: none;
- }
- }
-
- >span:first-of-type {
-
- &:before,
- &:after {
- transition: none;
- }
- }
- }
-
- .ui-drop-span {
- margin: 0;
>span {
margin: var(--spacing-cell);
@@ -434,287 +305,456 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
- }
- &.wrap>span {
- @include wrap();
- }
- }
-
- .ui-drop-wrapper {
- height: var(--row-height);
- width: 100%;
- display: flex;
- flex-direction: column;
-
- >.ui-drop-header {
- border: none;
- height: 100%;
-
- >.ui-drop-text {
- padding: var(--spacing-drop-cell);
+ &.wrap {
+ @include wrap();
}
}
}
- .ui-date-cell {
- height: var(--row-height);
- text-indent: 4px;
+ &.ui-grid-row-level.level-1>td:first-child {
+ padding-left: 20px;
+ }
+ }
+ }
- &:invalid {
- color: rgba(0, 0, 0, .3);
+ >tfoot {
+ color: var(--header-fore-color);
+ position: absolute;
+ width: 100%;
+ background-color: var(--total-row-bg-color);
+
+ >.ui-grid-row>td {
+ font-weight: bold;
+
+ &.sticky {
+ background-color: var(--total-row-bg-color);
+ }
+ }
+ }
+
+ >tbody {
+ color: var(--cell-fore-color);
+
+ >.ui-grid-row {
+ background-color: var(--row-bg-color);
+ border-bottom: 1px solid var(--cell-border-color);
+
+ &:hover {
+ background-color: var(--row-active-bg-color);
+
+ >td {
+ &.sticky {
+ background-color: var(--row-active-bg-color);
+ }
+
+ .col-hover {
+ display: block;
+ }
}
}
- .col-icon {
- display: flex;
- cursor: pointer;
- justify-content: center;
- align-items: center;
- position: relative;
- // padding: var(--spacing-s);
+ &.selected {
+ background-color: var(--row-selected-bg-color);
- >svg {
- width: 16px;
- height: 16px;
- fill: var(--primary-color);
- transition: opacity .12s ease;
+ >td.sticky {
+ background-color: var(--row-selected-bg-color);
+ }
+ }
+
+ >td {
+
+ &.sticky {
+ background-color: var(--row-bg-color);
}
- &:hover>svg {
- opacity: .4;
+ &.ui-expandable {
+ &>svg {
+ width: 24px;
+ height: 34px;
+ padding: 8px 3px;
+ box-sizing: border-box;
+ display: block;
+ cursor: pointer;
+ transition: opacity .12s ease;
+
+ &:hover {
+ opacity: .4;
+ }
+ }
}
- &.disabled {
- cursor: unset;
+ >input[type="text"],
+ >input[type="date"],
+ >textarea {
+ border: none;
+ box-sizing: border-box;
+ width: 100%;
+ padding: 0;
+ max-height: none !important;
+
+ @include outline();
+
+ &:disabled {
+ color: var(--text-disabled-color);
+ }
+ }
+
+ >input[type="text"] {
+ height: var(--row-height);
+ text-indent: var(--text-indent);
+ }
+
+ >textarea {
+ resize: none;
+ line-height: var(--line-height);
+ display: block;
+ padding: var(--spacing-cell);
+ white-space: nowrap;
+ @include scrollbar();
+ }
+
+ .ui-check-wrapper,
+ .ui-switch {
+ display: inline-flex;
+ justify-content: center;
+ height: var(--row-height);
+ padding: 0 8px;
+
+ .ui-check-inner {
+
+ &,
+ >svg {
+ transition: none;
+ }
+ }
+
+ >span:first-of-type {
+
+ &:before,
+ &:after {
+ transition: none;
+ }
+ }
+ }
+
+ .ui-drop-span {
+ margin: 0;
+
+ >span {
+ margin: var(--spacing-cell);
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: pre;
+ }
+
+ &.wrap>span {
+ @include wrap();
+ }
+ }
+
+ .ui-drop-wrapper {
+ height: var(--row-height);
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+
+ >.ui-drop-header {
+ border: none;
+ height: 100%;
+
+ >.ui-drop-text {
+ padding: var(--spacing-drop-cell);
+ }
+ }
+ }
+
+ .ui-date-cell {
+ height: var(--row-height);
+ text-indent: 4px;
+
+ &:invalid {
+ color: rgba(0, 0, 0, .3);
+ }
+ }
+
+ .col-icon {
+ display: flex;
+ cursor: pointer;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ // padding: var(--spacing-s);
>svg {
- fill: var(--header-border-color);
- opacity: unset;
+ width: 16px;
+ height: 16px;
+ fill: var(--primary-color);
+ transition: opacity .12s ease;
}
+
+ &:hover>svg {
+ opacity: .4;
+ }
+
+ &.disabled {
+ cursor: unset;
+
+ >svg {
+ fill: var(--header-border-color);
+ opacity: unset;
+ }
+ }
+ }
+
+ .col-hover {
+ display: none;
}
}
}
}
}
+
+ .ui-grid-hover-holder {
+ box-sizing: border-box;
+ position: absolute;
+ // line-height: var(--line-height);
+ // padding: var(--spacing-cell);
+ // background-color: var(--cell-hover-bg-color);
+ // white-space: pre;
+ display: flex;
+ align-items: center;
+ // overflow: hidden;
+ visibility: hidden;
+ opacity: 0;
+ transition: visibility 0s linear .12s, opacity .12s ease;
+ z-index: 2;
+ border-radius: 2px;
+ box-shadow: 0 3.2px 7.2px 0 rgba(0, 0, 0, .13), 0 0.6px 1.8px 0 rgba(0, 0, 0, .11);
+ margin-top: -40px;
+
+ &.active {
+ visibility: visible;
+ opacity: 1;
+ }
+
+ >.ui-grid-hover-pointer {
+ box-sizing: border-box;
+ box-shadow: 0 5px 15px 2px rgba(0, 0, 0, .3);
+ border: 1px solid #fff;
+ z-index: -1;
+ width: 16px;
+ height: 16px;
+ position: absolute;
+ left: var(--pointer-left, calc(50% - 8px));
+ bottom: -8px;
+ transform: rotate(-45deg);
+ transform-origin: center;
+ }
+
+ >.ui-grid-hover-curtain {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ z-index: -1;
+ }
+
+ >.ui-grid-hover-content {
+ font-size: var(--font-smaller-size);
+ line-height: 1rem;
+ white-space: normal;
+ overflow: hidden;
+ margin: 8px;
+ height: calc(100% - 16px);
+ user-select: none;
+ }
+
+ &.ui-grid-hover-down {
+ margin-top: 40px;
+
+ >.ui-grid-hover-pointer {
+ bottom: unset;
+ top: -8px;
+ }
+ }
+
+ &.ui-grid-hover-no>.ui-grid-hover-pointer {
+ display: none;
+ }
+ }
}
- .ui-grid-hover-holder {
- box-sizing: border-box;
+ ~.ui-drop-box {
+ max-width: 300px;
+ }
+
+ >.filter-panel {
position: absolute;
- line-height: var(--line-height);
- padding: var(--spacing-cell);
- background-color: var(--cell-hover-bg-color);
- white-space: pre;
- display: flex;
- align-items: center;
- overflow: hidden;
- visibility: hidden;
+ width: 200px;
+ height: 300px;
+ box-shadow: var(--filter-shadow);
+ transition: var(--filter-transition);
+ background-color: var(--bg-color);
+ transform: scaleY(0);
+ transform-origin: top;
opacity: 0;
- transition: visibility 0s linear .12s, opacity .12s ease;
+ display: flex;
+ flex-direction: column;
z-index: 2;
&.active {
- visibility: visible;
+ transform: scaleY(1);
opacity: 1;
}
- }
- }
- >.ui-grid-loading {
- position: absolute;
- @include inset(0, 0, 0, 0);
- visibility: hidden;
- opacity: 0;
- transition: visibility 0s linear .12s, opacity .12s ease;
- background-color: var(--loading-bg-color);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 2;
+ >.filter-search-holder {
+ position: relative;
+ margin: 8px 8px 4px;
- >div {
- background-color: var(--loading-fore-color);
- border-radius: var(--loading-border-radius);
-
- >svg {
- width: var(--loading-size);
- height: var(--loading-size);
- padding: 20px;
- animation: loading-spinner 1.2s infinite linear;
- }
- }
- }
-
- ~.ui-drop-box {
- max-width: 300px;
- }
-
- >.filter-panel {
- position: absolute;
- width: 200px;
- height: 300px;
- box-shadow: var(--filter-shadow);
- transition: var(--filter-transition);
- background-color: var(--bg-color);
- transform: scaleY(0);
- transform-origin: top;
- opacity: 0;
- display: flex;
- flex-direction: column;
- z-index: 2;
-
- &.active {
- transform: scaleY(1);
- opacity: 1;
- }
-
- >.filter-search-holder {
- position: relative;
- margin: 8px 8px 4px;
-
- >.filter-search-box {
- box-sizing: border-box;
- text-indent: 16px;
- width: 100%;
- font-size: var(--font-smaller-size);
- height: var(--line-height);
- line-height: var(--line-height);
- }
-
- >svg {
- position: absolute;
- width: 12px;
- height: 12px;
- top: calc(50% - 6px);
- left: 4px;
- fill: var(--color);
- cursor: text;
- }
- }
-
- >.filter-item-list {
- flex: 1 1 auto;
- overflow-y: auto;
- overflow-x: hidden;
- position: relative;
- user-select: none;
- @include scrollbar();
-
- >.filter-content {
- position: absolute;
- width: 100%;
- }
-
- .filter-item {
- width: 100%;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- box-sizing: border-box;
- padding: var(--filter-item-padding);
-
- &:hover {
- background-color: var(--hover-bg-color);
+ >.filter-search-box {
+ box-sizing: border-box;
+ text-indent: 16px;
+ width: 100%;
+ font-size: var(--font-smaller-size);
+ height: var(--line-height);
+ line-height: var(--line-height);
}
- .ui-check-wrapper {
+ >svg {
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ top: calc(50% - 6px);
+ left: 4px;
+ fill: var(--color);
+ cursor: text;
+ }
+ }
+
+ >.filter-item-list {
+ flex: 1 1 auto;
+ overflow-y: auto;
+ overflow-x: hidden;
+ position: relative;
+ user-select: none;
+ @include scrollbar();
+
+ >.filter-content {
+ position: absolute;
+ width: 100%;
+ }
+
+ .filter-item {
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ box-sizing: border-box;
+ padding: var(--filter-item-padding);
+
+ &:hover {
+ background-color: var(--hover-bg-color);
+ }
+
+ .ui-check-wrapper {
+ height: var(--filter-line-height);
+ line-height: var(--filter-line-height);
+ display: flex;
+
+ .ui-check-inner+* {
+ font-size: var(--font-smaller-size);
+ }
+ }
+ }
+ }
+
+ >.filter-function {
+ display: flex;
+ justify-content: flex-end;
+ padding: 4px;
+
+ >.button {
+ box-sizing: border-box;
+ padding-inline: 6px;
+ text-align: center;
+ margin-right: 10px;
+ min-width: 40px;
height: var(--filter-line-height);
line-height: var(--filter-line-height);
- display: flex;
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
+ border-radius: 0;
+ transition: background-color .12s ease;
- .ui-check-inner+* {
- font-size: var(--font-smaller-size);
+ @include outline();
+
+ &:hover {
+ background-color: var(--hover-bg-color);
}
}
}
}
- >.filter-function {
+ .ui-sort-panel-content {
+ height: 100%;
display: flex;
- justify-content: flex-end;
- padding: 4px;
+ flex-direction: column;
- >.button {
- box-sizing: border-box;
- padding-inline: 6px;
- text-align: center;
- margin-right: 10px;
- min-width: 40px;
- height: var(--filter-line-height);
- line-height: var(--filter-line-height);
- border: none;
- background-color: transparent;
- cursor: pointer;
- border-radius: 0;
- transition: background-color .12s ease;
+ >.ui-sort-panel-buttons {
+ flex: 0 0 auto;
+ white-space: nowrap;
+ overflow: hidden;
- @include outline();
+ >.button {
+ margin-right: 6px;
+ padding-inline: 6px;
+ text-align: center;
+ border: none;
+ line-height: 28px;
+ color: var(--title-color);
+ border-radius: var(--corner-radius);
+ padding: 0 10px;
+ box-sizing: border-box;
+ height: 28px;
+ line-height: 28px;
+ cursor: pointer;
+ user-select: none;
+ background-color: var(--title-bg-color);
+ transition: opacity .12s ease;
+ display: inline-flex;
+ align-items: center;
- &:hover {
- background-color: var(--hover-bg-color);
+ &:hover {
+ opacity: .8;
+ }
+
+ &:disabled {
+ opacity: .6;
+ cursor: default;
+ }
+
+ >svg {
+ flex: 0 0 auto;
+ width: 16px;
+ height: 16px;
+ fill: var(--title-color);
+ }
+
+ >span {
+ flex: 1 1 auto;
+ margin-left: 4px;
+ }
}
}
- }
- }
- .ui-sort-panel-content {
- height: 100%;
- display: flex;
- flex-direction: column;
-
- >.ui-sort-panel-buttons {
- flex: 0 0 auto;
- white-space: nowrap;
- overflow: hidden;
-
- >.button {
- margin-right: 6px;
- padding-inline: 6px;
- text-align: center;
- border: none;
- line-height: 28px;
- color: var(--title-color);
- border-radius: var(--corner-radius);
- padding: 0 10px;
- box-sizing: border-box;
- height: 28px;
- line-height: 28px;
- cursor: pointer;
- user-select: none;
- background-color: var(--title-bg-color);
- transition: opacity .12s ease;
- display: inline-flex;
- align-items: center;
-
- &:hover {
- opacity: .8;
- }
-
- &:disabled {
- opacity: .6;
- cursor: default;
- }
-
- >svg {
- flex: 0 0 auto;
- width: 16px;
- height: 16px;
- fill: var(--title-color);
- }
-
- >span {
- flex: 1 1 auto;
- margin-left: 4px;
- }
+ >.ui-sort-panel-grid {
+ flex: 1 1 auto;
+ position: relative;
+ height: calc(100% - 30px);
}
}
-
- >.ui-sort-panel-grid {
- flex: 1 1 auto;
- position: relative;
- height: calc(100% - 30px);
- }
}
}
diff --git a/lib/ui/css/popup.scss b/lib/ui/css/popup.scss
index 21451f0..0a0c7f5 100644
--- a/lib/ui/css/popup.scss
+++ b/lib/ui/css/popup.scss
@@ -58,19 +58,20 @@ $buttonHeight: 28px;
font-size: 1rem;
}
- >.ui-popup-header-title {
+ .ui-popup-header-title {
+ flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 5px 0 3px 12px;
}
- >.ui-popup-header-title,
+ .ui-popup-header-title,
.ui-popup-move {
cursor: move;
}
- >.ui-popup-header-title.no-move {
+ .ui-popup-header-title.no-move {
cursor: default;
}
@@ -109,7 +110,7 @@ $buttonHeight: 28px;
flex: 1 1 auto;
line-height: $headerLineHeight;
position: relative;
- min-height: 100px;
+ min-height: 80px;
>.ui-popup-loading {
position: absolute;
@@ -144,6 +145,16 @@ $buttonHeight: 28px;
display: flex;
margin: 10px;
+ &.message-wrapper-input {
+ flex-direction: column;
+ gap: 6px;
+
+ >.ui-text {
+ min-height: 50px;
+ resize: vertical;
+ }
+ }
+
>svg {
width: 30px;
height: 30px;
@@ -204,7 +215,7 @@ $buttonHeight: 28px;
display: flex;
align-items: center;
justify-content: flex-end;
- padding: 4px 10px 16px 2px;
+ padding: 4px 26px 26px 20px;
}
.ui-popup-body,
diff --git a/lib/ui/css/tooltip.scss b/lib/ui/css/tooltip.scss
index 65d69a8..9bb3de4 100644
--- a/lib/ui/css/tooltip.scss
+++ b/lib/ui/css/tooltip.scss
@@ -29,7 +29,7 @@
width: 16px;
height: 16px;
position: absolute;
- left: calc(50% - 8px);
+ left: var(--pointer-left, calc(50% - 8px));
bottom: -8px;
transform: rotate(-45deg);
transform-origin: center;
diff --git a/lib/ui/css/variables/definition.scss b/lib/ui/css/variables/definition.scss
index d27e804..3393302 100644
--- a/lib/ui/css/variables/definition.scss
+++ b/lib/ui/css/variables/definition.scss
@@ -9,6 +9,28 @@
}
}
+@keyframes loading-dot {
+ 0% {
+ box-shadow: 20px 0 #0067c0, -20px 0 #0067c022;
+ background: #0067c0;
+ }
+
+ 33% {
+ box-shadow: 20px 0 #0067c0, -20px 0 #0067c022;
+ background: #0067c022;
+ }
+
+ 66% {
+ box-shadow: 20px 0 #0067c022, -20px 0 #0067c0;
+ background: #0067c022;
+ }
+
+ 100% {
+ box-shadow: 20px 0 #0067c022, -20px 0 #0067c0;
+ background: #0067c0;
+ }
+}
+
:root {
/*color-scheme: light dark;*/
@@ -25,6 +47,8 @@
--switch-active-bg-color: #33c559;
--red-color: red;
+ --orange-color: orange;
+ --green-color: #33c559;
--title-color: #fff;
--title-bg-color: rgb(68, 114, 196);
--title-ctrlbg-color: rgb(68, 114, 196);
diff --git a/lib/ui/dropdown.js b/lib/ui/dropdown.js
index 645ea98..ccb8b96 100644
--- a/lib/ui/dropdown.js
+++ b/lib/ui/dropdown.js
@@ -405,6 +405,8 @@ export class Dropdown {
}
return item;
});
+ this._var.allChecked = false;
+ source.forEach(it => delete it.__checked);
if (itemlist.length === 0) {
this._var.selectedList = null;
this._var.label.innerText = r('none', '( None )');
@@ -608,7 +610,7 @@ export class Dropdown {
}
_contains(it, item, valuekey, textkey) {
- if (item.children?.length > 0) {
+ if (Array.isArray(item.children)) {
for (let t of item.children) {
if (it === getValue(t, valuekey, textkey)) {
return true;
@@ -644,8 +646,19 @@ export class Dropdown {
createElement('span', span => {
// events
span.className = 'ui-expandor';
+ if (Array.isArray(item.children) && item.children.length > 0) {
+ span.classList.add('active');
+ function hideChildren(children) {
+ for (let c of children) {
+ c.__visible = false;
+ if (Array.isArray(c.children)) {
+ hideChildren(c.children);
+ }
+ }
+ }
+ }
},
- createIcon('fa-light', 'caret-down')
+ createIcon('fa-solid', 'caret-down')
)
);
li.appendChild(wrapper);
@@ -700,16 +713,17 @@ export class Dropdown {
const textkey = this._var.options.textKey;
const template = this._var.options.htmlTemplate;
const htmlkey = this._var.options.htmlKey;
+ const source = this.source;
if (checkbox.getAttribute('isall') === '1') {
const allchecked = this._var.allChecked = checkbox.checked;
const boxes = this._var.container.querySelectorAll('input.dataitem');
boxes.forEach(box => box.checked = allchecked);
+ source.forEach(it => it.__checked = allchecked ? 1 : 0);
list = [];
} else {
item.__checked = checkbox.indeterminate ? 2 : checkbox.checked ? 1 : 0;
const all = this._var.container.querySelector('input[isall="1"]');
if (checkbox.checked) {
- const source = this.source;
if (source.some(it => it.__checked) == null) {
this._var.allChecked = true;
if (all != null) {
@@ -726,7 +740,7 @@ export class Dropdown {
if (all != null) {
all.checked = false;
}
- list = this.source.filter(it => String(getValue(it, valuekey, textkey)) !== val);
+ list = source.filter(it => String(getValue(it, valuekey, textkey)) !== val);
} else {
list = this.selectedList.filter(it => String(getValue(it, valuekey, textkey)) !== val);
}
@@ -739,7 +753,7 @@ export class Dropdown {
}
this._var.selectedList = list;
if (typeof this.onSelectedList === 'function') {
- this.onSelectedList(itemlist);
+ this.onSelectedList(list);
}
}
diff --git a/lib/ui/grid/column.d.ts b/lib/ui/grid/column.d.ts
new file mode 100644
index 0000000..a90f3af
--- /dev/null
+++ b/lib/ui/grid/column.d.ts
@@ -0,0 +1,391 @@
+import { Grid, GridItem, GridItemWrapper, GridSourceItem } from "./grid";
+import { Dropdown, DropdownOptions } from "../dropdown";
+
+/** 列类型枚举 */
+declare enum GridColumnType {
+ /** 通用列 */
+ Common = 0,
+ /** 单行文本框列 */
+ Input = 1,
+ /** 下拉选择列 */
+ Dropdown = 2,
+ /** 复选框列 */
+ Checkbox = 3,
+ /** 图标列 */
+ Icon = 4,
+ /** 多行文本列 */
+ Text = 5,
+ /** 日期选择列 */
+ Date = 6
+}
+
+/** 列定义接口 */
+export interface GridColumnDefinition {
+ /** 列关键字,默认以该关键字从行数据中提取单元格值,行数据的关键字属性值里包含 DisplayValue 则优先显示此值 */
+ key?: string;
+ /** 列的类型,可以为 {@linkcode GridColumn} 的子类,或者内置类型 {@linkcode GridColumnType} */
+ type?: GridColumnType | typeof GridColumn;
+ /** 列标题文本 */
+ caption?: string;
+ /** 列标题的元素样式 */
+ captionStyle?: { [key: string]: string };
+ /** 大于 0 则设置为该宽度,否则根据列内容自动调整列宽 */
+ width?: number;
+ /** 列对齐方式 */
+ align?: "left" | "center" | "right";
+ /**
+ * 列是否可用(可编辑),允许以下类型
+ * `boolean` 则直接使用该值
+ * `string` 则以该值为关键字从行数据中取值作为判断条件
+ * `(item: GridItem) => boolean` 则调用该函数(上下文为列定义对象),以返回值作为判断条件
+ */
+ enabled?: boolean | string | ((item: GridItem) => boolean);
+ /**
+ * 单元格取值采用该方法返回的值
+ * @param item 行数据对象
+ * @param editing 是否处于编辑状态
+ * @param body Grid 控件的 `<tbody>` 部分
+ */
+ filter?: (item: GridItem, editing: boolean, body?: HTMLElement) => any;
+ /** 单元格以该值填充内容,忽略filter与关键字属性 */
+ text?: string;
+ /** 列是否可见 */
+ visible?: boolean;
+ /** 列是否允许调整宽度 */
+ resizable?: boolean;
+ /** 列是否允许排序 */
+ sortable?: boolean;
+ /** 列是否允许重排顺序 */
+ orderable?: boolean;
+ /** 列为复选框类型时是否在列头增加全选复选框 */
+ allcheck?: boolean;
+ /** 单元格css样式对象(仅在重建行元素时读取) */
+ css?: { [key: string]: string };
+ /** 根据返回值填充单元格样式(填充行列数据时读取) */
+ styleFilter?: (item: GridItem) => { [key: string]: string };
+ /** 根据返回值设置单元格背景色 */
+ bgFilter?: (item: GridItem) => string;
+ /** 给单元格元素附加事件(事件函数上下文为数据行对象) */
+ events?: { [event: string]: Function };
+ /** 根据返回值设置单元格元素的附加属性,允许直接设置对象也支持函数返回对象 */
+ attrs?: { [key: string]: string } | ((item: GridItem) => { [key: string]: string });
+ /** 是否允许进行列头过滤 */
+ allowFilter?: boolean;
+ /** 自定义列过滤器的数据源(函数上下文为Grid) */
+ filterSource?: Array | ((col: GridColumnDefinition) => Array);
+ /** 自定义列排序函数 */
+ sortFilter?: (a: GridItem, b: GridItem) => -1 | 0 | 1;
+ /** 列为下拉列表类型时以该值设置下拉框的参数 */
+ dropOptions?: DropdownOptions;
+ /** 列为下拉列表类型时以该值设置下拉列表数据源,支持函数返回,也支持返回异步对象 */
+ source?: Array | ((item: GridItem) => Array | Promise>);
+ /** 下拉列表数据源是否缓存结果(即行数据未发生变化时仅从source属性获取一次值) */
+ sourceCache?: boolean;
+ /** 列为图标类型时以该值设置图标样式(函数上下文为列定义对象),默认值 `fa-light` */
+ iconType?: "fa-light" | "fa-regular" | "fa-solid";
+ /** 列为图标类型时以该值作为单元格元素的额外样式类型(函数上下文为列定义对象) */
+ iconClassName?: string | ((item: GridItem) => string);
+ /** 列为日期类型时以该值作为最小可选日期值 */
+ dateMin?: string;
+ /** 列为日期类型时以该值作为最大可选日期值 */
+ dateMax?: string;
+ /** 列为日期类型时自定义日期转字符串函数 */
+ dateValueFormatter?: string | ((date: Date) => string);
+ /** 以返回值额外设置单元格的tooltip(函数上下文为列定义对象) */
+ tooltip?: string | ((item: GridItem) => string);
+
+ /**
+ * 列头复选框改变时触发
+ * @param this 上下文为 Grid 对象
+ * @param col 列定义对象
+ * @param flag 是否选中
+ * @eventProperty
+ */
+ onAllChecked?: (this: Grid, col: GridColumnDefinition, flag: boolean) => void;
+ /**
+ * 单元格发生变化时触发
+ * @param this 上下文为 Grid 对象
+ * @param item 数据行对象
+ * @param value 修改后的值
+ * @param oldValue 修改前的值
+ * @param e 列修改事件传递过来的任意对象
+ * @eventProperty
+ */
+ onChanged?: (this: Grid, item: GridItem, value: boolean | string | number, oldValue: boolean | string | number, e?: any) => void;
+ /**
+ * 文本单元格在输入完成时触发的事件
+ * @param this 上下文为 Grid 对象
+ * @param item 数据行对象
+ * @param value 修改后的文本框值
+ * @eventProperty
+ */
+ onInputEnded?: (this: Grid, item: GridItem, value: string) => void;
+ /**
+ * 列过滤点击OK时触发的事件
+ * @param this 上下文为 Grid 对象
+ * @param col 列定义对象
+ * @param selected 选中的过滤项
+ * @eventProperty
+ */
+ onFilterOk?: (this: Grid, col: GridColumnDefinition, selected: Array) => void;
+ /**
+ * 列过滤后触发的事件
+ * @param this 上下文为 Grid 对象
+ * @param col 列定义对象
+ * @eventProperty
+ */
+ onFiltered?: (this: Grid, col: GridColumnDefinition) => void;
+ /**
+ * 列为下拉框类型时在下拉列表展开时触发的事件
+ * @param this 上下文为列定义对象
+ * @param item 数据行对象
+ * @param drop 下拉框对象
+ * @eventProperty
+ */
+ onDropExpanded?: (this: GridColumnDefinition, item: GridItem, drop: Dropdown) => void;
+ /**
+ * 列为下拉框类型时在下拉列表关闭时触发的事件
+ * @param this 上下文为列定义对象
+ * @param item 数据行对象
+ * @param drop 下拉框对象
+ * @eventProperty
+ */
+ onDropCollapsed?: (this: GridColumnDefinition, item: GridItem, drop: Dropdown) => void;
+}
+
+/** 列定义基类 */
+export class GridColumn {
+ /** @ignore */
+ constructor();
+ /**
+ * 标记该类型是否支持列头批量操作
+ */
+ static get headerEditing(): boolean;
+ /**
+ * 创建显示单元格时调用的方法
+ * @param col 列定义对象
+ * @returns 返回创建的单元格元素
+ * @virtual
+ */
+ static create(col: GridColumnDefinition): HTMLElement;
+ /**
+ * 创建编辑单元格时调用的方法
+ * 元素修改后设置行包装对象的 `__editing` 后,支持在离开编辑状态时及时触发 {@linkcode leaveEdit} 方法
+ * 更多例子参考代码中 {@linkcode GridDropdownColumn} 的实现。
+ * @param trigger 编辑事件回调函数,e 参数会传递给 {@linkcode getValue} 方法
+ * @param col 列定义对象
+ * @param container 父容器元素
+ * @param vals 行包装对象,其 `values` 属性为行数据对象
+ * @returns 返回创建的编辑状态的单元格元素
+ * @virtual
+ */
+ static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, container: HTMLElement, vals: GridItemWrapper): HTMLElement;
+ /**
+ * 创建列头时调用的方法
+ * @param col 列定义对象
+ * @returns 返回创建的列头元素
+ * @virtual
+ */
+ static createCaption?(col: GridColumnDefinition): HTMLElement;
+ /**
+ * 设置单元格值时调用的方法
+ * @param element 单元格元素
+ * @param val 待设置的单元格值
+ * @param vals 行包装对象
+ * @param col 列定义对象
+ * @param grid {@linkcode Grid} 对象
+ * @virtual
+ */
+ static setValue(element: HTMLElement, val: string | boolean | number, vals: GridItemWrapper, col: GridColumnDefinition, grid: Grid): void;
+ /**
+ * 获取编辑状态单元格值时调用的方法
+ * @param e 由 {@linkcode createEdit} 方法中 `trigger` 函数传递来的对象
+ * @param col 列定义对象
+ * @returns 返回单元格的值
+ * @virtual
+ */
+ static getValue(e: any, col: GridColumnDefinition): string | boolean | number;
+ /**
+ * 设置单元格样式时调用的方法
+ * @param element 单元格元素
+ * @param style 样式对象
+ * @virtual
+ */
+ static setStyle(element: HTMLElement, style: { [key: string]: string }): void;
+ /**
+ * 设置单元格可用性时调用的方法
+ * @param element 单元格元素
+ * @param enabled 启用值,为false时代表禁用
+ * @virtual
+ */
+ static setEnabled(element: HTMLElement, enabled?: boolean): void;
+ /**
+ * 单元格离开编辑元素时触发,需要由行包装对象的 `__editing` 来确定是否触发。
+ * @param element 单元格元素
+ * @param container 父容器元素
+ * @virtual
+ */
+ static leaveEdit?(element: HTMLElement, container: HTMLElement): void;
+}
+
+/** 单行文本列 */
+export class GridInputColumn extends GridColumn {
+ /**
+ * 设置该类型是否支持触发 {@linkcode GridColumnDefinition.onInputEnded} 方法
+ * 该属性返回 `true` 后,在任意事件中修改行包装对象的 `__editing` 值,则会在行列元素变动时及时触发 {@linkcode GridColumnDefinition.onInputEnded} 方法,避免例如文本框还未触发 `onchange` 事件就被移除元素而导致的问题
+ * 更多例子参考代码中 {@linkcode GridInputColumn} 的实现
+ */
+ static get editing(): boolean;
+ /**
+ * @inheritdoc GridColumn.createEdit
+ * @override
+ */
+ static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, container: HTMLElement, vals: GridItemWrapper): HTMLElement;
+ /**
+ * @inheritdoc GridColumn.setValue
+ * @override
+ */
+ static setValue(element: HTMLElement, val: string, vals: GridItemWrapper, col: GridColumnDefinition, grid: Grid): void;
+ /**
+ * @inheritdoc GridColumn.getValue
+ * @override
+ */
+ static getValue(e: any): string;
+ /**
+ * @inheritdoc GridColumn.setEnabled
+ * @override
+ */
+ static setEnabled(element: HTMLElement, enabled?: boolean): void;
+}
+
+/** 多行文本列 */
+export class GridTextColumn extends GridInputColumn {
+ /**
+ * @inheritdoc GridInputColumn.createEdit
+ * @override
+ */
+ static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, container: HTMLElement, vals: GridItemWrapper): HTMLElement;
+ /**
+ * @inheritdoc GridInputColumn.setValue
+ * @override
+ */
+ static setValue(element: HTMLElement, val: string, vals: GridItemWrapper, col: GridColumnDefinition, grid: Grid): void;
+}
+
+/** 下拉选择列 */
+export class GridDropdownColumn extends GridColumn {
+ /**
+ * @inheritdoc GridColumn.createEdit
+ * @override
+ */
+ static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, container: HTMLElement, vals: GridItemWrapper): HTMLElement;
+ /**
+ * @inheritdoc GridColumn.setValue
+ * @override
+ */
+ static setValue(element: HTMLElement, val: string, vals: GridItemWrapper, col: GridColumnDefinition): void;
+ /**
+ * @inheritdoc GridColumn.getValue
+ * @override
+ */
+ static getValue(e: any, col: GridColumnDefinition): string;
+ /**
+ * @inheritdoc GridColumn.setEnabled
+ * @override
+ */
+ static setEnabled(element: HTMLElement, enabled?: boolean): void;
+ /**
+ * @inheritdoc GridColumn.leaveEdit
+ * @override
+ */
+ static leaveEdit?(element: HTMLElement, container: HTMLElement): void;
+}
+
+/** 复选框列 */
+export class GridCheckboxColumn extends GridColumn {
+ /**
+ * @inheritdoc GridColumn.createEdit
+ * @override
+ */
+ static createEdit(trigger: (e: any) => void): HTMLElement;
+ /**
+ * @inheritdoc GridColumn.setValue
+ * @override
+ */
+ static setValue(element: HTMLElement, val: boolean): void;
+ /**
+ * @inheritdoc GridColumn.getValue
+ * @override
+ */
+ static getValue(e: any): boolean;
+ /**
+ * @inheritdoc GridColumn.setEnabled
+ * @override
+ */
+ static setEnabled(element: HTMLElement, enabled?: boolean): void;
+}
+
+/** 单选框列 */
+export class GridRadioboxColumn extends GridCheckboxColumn {
+ /**
+ * @inheritdoc GridCheckboxColumn.createEdit
+ * @override
+ */
+ static createEdit(trigger: (e: any) => void): HTMLElement;
+}
+
+/** 图标列 */
+export class GridIconColumn extends GridColumn {
+ /**
+ * @inheritdoc GridColumn.create
+ * @override
+ */
+ static create(): HTMLElement;
+ /**
+ * @inheritdoc GridColumn.setValue
+ * @override
+ */
+ static setValue(element: HTMLElement, val: string, vals: GridItemWrapper, col: GridColumnDefinition): void;
+ /**
+ * @inheritdoc GridColumn.setEnabled
+ * @override
+ */
+ static setEnabled(element: HTMLElement, enabled?: boolean): void;
+}
+
+/** 日期选择列 */
+export class GridDateColumn extends GridColumn {
+ /**
+ * @inheritdoc GridColumn.createEdit
+ * @override
+ */
+ static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, container: HTMLElement, vals: GridItemWrapper): HTMLElement;
+ /**
+ * 设置单元格值时调用的方法
+ * 支持以下几种数据类型
+ * `"2024-01-26"`
+ * `"1/26/2024"`
+ * `"638418240000000000"`
+ * `new Date('2024-01-26')`
+ * @param element 单元格元素
+ * @param val 待设置的单元格值
+ * @override
+ */
+ static setValue(element: HTMLElement, val: string | number): void;
+ /**
+ * @inheritdoc GridColumn.getValue
+ * @override
+ */
+ static getValue(e: any): string | number;
+ /**
+ * @inheritdoc GridColumn.setEnabled
+ * @override
+ */
+ static setEnabled(element: HTMLElement, enabled?: boolean): void;
+ /**
+ * 格式化日期对象为 M/d/yyyy 格式的字符串
+ * @param date 日期对象
+ * @returns 返回格式化后的字符串
+ */
+ static formatDate(date: Date): string;
+}
\ No newline at end of file
diff --git a/lib/ui/grid/column.js b/lib/ui/grid/column.js
index 634779e..232a608 100644
--- a/lib/ui/grid/column.js
+++ b/lib/ui/grid/column.js
@@ -368,6 +368,11 @@ export class GridDropdownColumn extends GridColumn {
col.onDropExpanded.call(col, wrapper.values, drop);
}
};
+ drop.onCollapsed = () => {
+ if (typeof col.onDropCollapsed === 'function') {
+ col.onDropCollapsed.call(col, wrapper.values, drop);
+ }
+ };
return drop.create();
}
diff --git a/lib/ui/grid/grid.d.ts b/lib/ui/grid/grid.d.ts
new file mode 100644
index 0000000..64ead89
--- /dev/null
+++ b/lib/ui/grid/grid.d.ts
@@ -0,0 +1,325 @@
+import { GridColumnDefinition } from "./column"
+/**
+ * 单元格点击回调函数
+ *
+ * @param {number} index - 点击的行索引
+ * @param {number} colIndex - 点击的列索引
+ * @returns {boolean} 返回 `false` 则取消事件冒泡
+ */
+declare function cellClickedCallback(index: number, colIndex: number): boolean;
+
+/** 列数据接口 */
+interface GridItem {
+ /** 值 */
+ Value: any;
+ /** 显示值 */
+ DisplayValue: string;
+}
+
+/** 列数据行包装接口 */
+interface GridItemWrapper {
+ /** 真实数据对象 */
+ values: { [key: string]: GridItem | any };
+ /** 下拉数据源缓存对象 */
+ source: { [key: string]: Array };
+}
+
+/** 下拉框列数据源接口 */
+interface GridSourceItem {
+ /** 值 */
+ value: string;
+ /** 显示文本 */
+ text: string;
+}
+
+/** Grid 语言资源接口 */
+interface GridLanguages {
+ /**
+ * “所有”文本,默认值 `( All )` */
+ all: string;
+ /** “确定”文本,默认值 `OK` */
+ ok: string;
+ /** “重置”文本,默认值 `Reset` */
+ reset: string;
+ cancel: string;
+ /** “空”文本,默认值 `( Null )` */
+ null: string;
+ addLevel: string;
+ deleteLevel: string;
+ copyLevel: string;
+ asc: string;
+ desc: string;
+ column: string;
+ order: string;
+ sort: string;
+ requirePrompt: string;
+ duplicatePrompt: string;
+}
+
+/** Grid 列排序定义接口 */
+interface GridColumnSortDefinition {
+ /** 排序列的关键字 */
+ column: string;
+ /** 升序或降序 */
+ order: "asc" | "desc";
+}
+
+/** 列排序枚举 */
+declare enum GridColumnDirection {
+ /** 倒序 */
+ Descending = -1,
+ /** 升序 */
+ Ascending = 1
+}
+
+/** 列事件枚举 */
+declare enum GridColumnEvent {
+ /** 重排事件 */
+ Reorder = "reorder",
+ /** 宽调整事件 */
+ Resize = "resize",
+ /** 排序事件 */
+ Sort = "sort"
+}
+
+/** Grid 控件基础类 */
+export class Grid {
+ /** 列类型枚举 */
+ static ColumnTypes: {
+ /** 通用列(只读) */
+ Common: 0,
+ /** 单行文本列 */
+ Input: 1,
+ /** 下拉选择列 */
+ Dropdown: 2,
+ /** 复选框列 */
+ Checkbox: 3,
+ /** 图标列 */
+ Icon: 4,
+ /** 多行文本列 */
+ Text: 5,
+ /** 日期选择列 */
+ Date: 6,
+ /**
+ * 判断列是否为复选框列
+ * @param type 列类型
+ */
+ isCheckbox(type: number): boolean;
+ };
+
+ /** 列定义的数组 */
+ columns: Array;
+ /** 多语言资源对象 */
+ langs?: GridLanguages;
+ /** 行数大于等于该值则启用虚模式,默认值 `100` */
+ virtualCount?: number;
+ /** 表格行高,默认值 `36` */
+ rowHeight?: number;
+ /** 文本行高,默认值 `24` */
+ lineHeight?: number;
+ /** 列表底部留出额外行的空白,默认值 `0` */
+ extraRows?: number;
+ /** 过滤条件列表的行高,默认值 `30` */
+ filterRowHeight?: number;
+ /** 列表高度值,为 0 时列表始终显示全部内容(自增高),为非数字或者小于 0 则根据容器高度来确定虚模式的渲染行数,默认值 `null` */
+ height?: number;
+ /** 是否允许多选,默认值 `false` */
+ multiSelect?: boolean;
+ /** 为 false 时只有点击在单元格内才会选中行,默认值 `true` */
+ fullrowClick?: boolean;
+ /** 单元格 tooltip 是否禁用,默认值 `false` */
+ tooltipDisabled?: boolean;
+ /** 列头是否显示,默认值 `true` */
+ headerVisible?: boolean;
+ /** 监听事件的窗口载体,默认值 `window` */
+ window?: Window
+ /** 排序列的索引,默认值 `-1` */
+ sortIndex?: number;
+ /** 排序方式,正数升序,负数倒序,默认值 `1` */
+ sortDirection?: GridColumnDirection;
+ /** 排序列 */
+ sortArray?: Array;
+
+ /**
+ * Grid 控件构造函数
+ * @param container Grid 控件所在的父容器,可以是 string 表示选择器,也可以是 HTMLElement 对象
+ * 构造时可以不进行赋值,但是调用 init 函数时则必须进行赋值
+ * @param getText (可选参数)获取多语言文本的函数代理
+ */
+ constructor(container: string | HTMLElement, getText?: (id: string, def?: string) => string);
+
+ /**
+ * 即将选中行时触发,返回 false、null、undefined、0 等则取消选中动作
+ * @param index 即将选中的行索引
+ * @param colIndex 即将选中的列索引
+ * @eventProperty
+ */
+ willSelect?: (index: number, colIndex: number) => boolean;
+ /**
+ * 单元格单击时触发,colIndex 为 -1 则表示点击的是行的空白处,返回 false 则取消事件冒泡
+ * @eventProperty
+ */
+ cellClicked?: typeof cellClickedCallback;
+
+ /**
+ * 选中行发生变化时触发的事件
+ * @param index 选中的行索引
+ * @eventProperty
+ */
+ onSelectedRowChanged?: (index?: number) => void;
+ /**
+ * 单元格双击时触发的事件,colIndex 为 -1 则表示点击的是行的空白处
+ * @param index 双击的行索引
+ * @param colIndex 双击的列索引
+ * @eventProperty
+ */
+ onCellDblClicked?: (index: number, colIndex: number) => void;
+ /**
+ * 行双击时触发的事件
+ * @param index 双击的行索引
+ * @eventProperty
+ */
+ onRowDblClicked?: (index: number) => void;
+ /**
+ * 列发生变化时触发的事件
+ * @param type 事件类型
+ * "reorder" 为发生列重排事件,此时 value 为目标列索引
+ * "resize" 为发生列宽调整事件,此时 value 为列宽度值
+ * "sort" 为发生列排序事件,此时 value 为 1(升序)或 -1(倒序)
+ * @param colIndex 发生变化事件的列索引
+ * @param value 变化的值
+ * @eventProperty
+ */
+ onColumnChanged?: (type: GridColumnEvent, colIndex: number, value: number | GridColumnDirection) => void;
+ /**
+ * 列滚动时触发的事件
+ * @param e 滚动事件对象
+ * @eventProperty
+ */
+ onBodyScrolled?: (e: Event) => void;
+ /**
+ * 多列排序后触发的事件
+ * @param array 排序列定义数组
+ * @eventProperty
+ */
+ onSorted?: (array?: Array) => void;
+
+ /** 返回所有数据的数据(未过滤) */
+ get allSource(): Array;
+ /** 获取数据数组(已过滤) */
+ get source(): Array;
+ /** 设置数据,并刷新列表 */
+ set source(list: Array);
+ /** 获取列表是否为只读,默认值 `false` */
+ get readonly(): boolean;
+ /** 设置列表是否为只读 */
+ set readonly(flag: boolean);
+ /** 获取当前选中的行索引的数组 */
+ get selectedIndexes(): Array;
+ /** 设置当前选中的行索引的数组,并刷新列表 */
+ set selectedIndexes(indexes: Array);
+ /** 获取 Grid 当前是否处于加载状态 */
+ get loading(): boolean;
+ /** 使 Grid 进入加载状态 */
+ set loading(flag: boolean);
+ /** 获取 Grid 当前滚动的偏移量 */
+ get scrollTop(): number;
+ /** 设置 Grid 滚动偏移量 */
+ set scrollTop(top: number);
+
+ /** 获取 Grid 的页面元素 */
+ get element(): HTMLElement;
+ /** 获取当前 Grid 是否已发生改变 */
+ get changed(): boolean;
+ /** 获取当前是否为虚模式状态 */
+ get virtual(): boolean;
+ /** 获取当前排序的列关键字,为 null 则当前无排序列 */
+ get sortKey(): string | undefined;
+ /** 获取当前选中行的索引,为 -1 则当前没有选中行 */
+ get selectedIndex(): number | -1;
+
+ /**
+ * 初始化Grid控件
+ * @param container 父容器元素,若未传值则采用构造方法中传入的父容器元素
+ */
+ init(container?: HTMLElement): void;
+ /**
+ * 设置数据列表,该方法为 set source 属性的语法糖
+ * @param source 待设置的数据列表
+ */
+ setData(source: Array): void;
+ /**
+ * 设置单行数据
+ * @param index 行索引
+ * @param item 待设置的行数据值
+ */
+ setItem(index: number, item: GridItem): void;
+ /**
+ * 添加行数据
+ * @param item 待添加的行数据值
+ * @param index 待添加的行索引
+ * @returns 返回已添加的行数据
+ */
+ addItem(item: GridItem, index?: number): GridItem;
+ /**
+ * 批量添加行数据
+ * @param array 待添加的行数据数组
+ * @param index 待添加的行索引
+ * @returns 返回已添加的行数据数组
+ */
+ addItems(array: Array, index?: number): Array
+ /**
+ * 删除行数据
+ * @param index 待删除的行索引
+ * @returns 返回已删除的行数据
+ */
+ removeItem(index: number): GridItem;
+ /**
+ * 批量删除行数据
+ * @param indexes 待删除的行索引数组,未传值时删除所有行
+ * @returns 返回已删除的行数据数组
+ */
+ removeItems(indexes?: Array): Array;
+ /**
+ * 滚动到指定行的位置
+ * @param index 待滚动至的行索引
+ */
+ scrollToIndex(index: number): void;
+ /**
+ * 调整 Grid 元素的大小,一般需要在宽度变化时(如页面大小发生变化时)调用
+ * @param force 是否强制 {@linkcode reload},默认只有待渲染的行数发生变化时才会调用
+ * @param keep 是否保持当前滚动位置
+ */
+ resize(force?: boolean, keep?: boolean): void;
+ /**
+ * 重新计算需要渲染的行,并载入元素,一般需要在高度变化时调用
+ * @param keep 是否保持当前滚动位置
+ */
+ reload(keep?: boolean): void;
+ /**
+ * 重新填充Grid单元格数据
+ */
+ refresh(): void;
+ /**
+ * 把所有行重置为未修改的状态
+ */
+ resetChange(): void;
+ /**
+ * 根据当前排序字段进行列排序
+ * @param reload 为 true 则在列排序后调用 {@linkcode Grid.reload} 方法
+ */
+ sortColumn(reload?: boolean): void;
+ /**
+ * 根据当前排序列数组进行多列排序
+ * @param reload 为 true 则在多列排序后调用 {@linkcode Grid.reload} 方法
+ */
+ sort(reload?: boolean): void;
+ /**
+ * 清除列头复选框的选中状态
+ */
+ clearHeaderCheckbox(): void;
+ /**
+ * 显示多列排序设置面板
+ */
+ showSortPanel(): void;
+}
\ No newline at end of file
diff --git a/lib/ui/grid/grid.js b/lib/ui/grid/grid.js
index 39e7775..15a030f 100644
--- a/lib/ui/grid/grid.js
+++ b/lib/ui/grid/grid.js
@@ -285,6 +285,7 @@ let r = lang;
* @property {Function} [onFilterOk] - 列过滤点击 `OK` 时触发的事件
* @property {Function} [onFiltered] - 列过滤后触发的事件
* @property {Function} [onDropExpanded] - 列为下拉框类型时在下拉列表展开时触发的事件
+ * @property {Function} [onDropCollapsed] - 列为下拉框类型时在下拉列表关闭时触发的事件
* @interface
* @example
* [
@@ -410,6 +411,15 @@ let r = lang;
* @this GridColumnDefinition
* @memberof GridColumnDefinition
*/
+/**
+ * 列为下拉框类型时在下拉列表关闭时触发的事件
+ * @name onDropCollapsed
+ * @event
+ * @param {GridRowItem} item - 行数据对象
+ * @param {Dropdown} drop - 下拉框对象
+ * @this GridColumnDefinition
+ * @memberof GridColumnDefinition
+ */
/**
* 判断列是否始终编辑的回调函数
@@ -613,6 +623,12 @@ export class Grid {
* @private
*/
parent: null,
+ /**
+ * Grid 包裹元素
+ * @type {HTMLDivElement}
+ * @private
+ */
+ container: null,
/**
* Grid 元素 - `div.ui-grid`
* @type {HTMLDivElement}
@@ -818,7 +834,7 @@ export class Grid {
*/
footer: null,
/**
- * 加载状态元素引用 - div.ui-grid-loading
+ * 加载状态元素引用 - div.ui-loading
* @type {HTMLDivElement}
* @private
*/
@@ -862,6 +878,13 @@ export class Grid {
* @ignore
*/
langs = {};
+ /**
+ * 区域字符串
+ * @type {string}
+ * @default "en"
+ * @ignore
+ */
+ lgid = 'en';
/**
* 行数大于等于该值则启用虚模式
* @type {number}
@@ -1124,6 +1147,7 @@ export class Grid {
* @param {string} getText.{returns} 返回的多语言
* @property {GridColumnDefinition[]} columns - 列定义的数组
* @property {GridLanguages} [langs] - 多语言资源对象
+ * @property {string} [lgid=en] - 区域字符串
* @property {number} [virtualCount=100] - 行数大于等于该值则启用虚模式
* @property {boolean} [autoResize=true] - 未设置宽度的列自动调整列宽
* @property {number} [rowHeight=36] - 表格行高,修改后同时需要在 `.ui-grid` 所在父容器重写 `--line-height` 的值以配合显示
@@ -1274,10 +1298,23 @@ export class Grid {
if (!Array.isArray(list)) {
throw new Error('source is not an Array.')
}
+ list = list.reduce((array, item) => {
+ array.push({
+ __level: 0,
+ values: item
+ });
+ if (Array.isArray(item.__children)) {
+ array.push(...item.__children.map(c => ({
+ __level: 1,
+ values: c
+ })));
+ }
+ return array;
+ }, []);
list = list.map((it, index) => {
return {
__index: index,
- values: it
+ ...it
};
});
this._var.source = list;
@@ -1335,9 +1372,11 @@ export class Grid {
if (flag === false) {
this._var.refs.loading.style.visibility = 'hidden';
this._var.refs.loading.style.opacity = 0;
+ this._var.el.style.overflow = '';
} else {
this._var.refs.loading.style.visibility = 'visible';
this._var.refs.loading.style.opacity = 1;
+ this._var.el.style.overflow = 'hidden';
}
}
@@ -1402,6 +1441,8 @@ export class Grid {
this._var.parent = container;
this._var.isFirefox = /Firefox\//i.test(navigator.userAgent);
this._var.enabledDict = {};
+ const c = createElement('div', 'ui-grid-container');
+ this._var.container = c;
const grid = createElement('div', 'ui-grid');
grid.setAttribute('tabindex', 0);
grid.addEventListener('keydown', e => {
@@ -1506,7 +1547,8 @@ export class Grid {
}
});
}
- container.replaceChildren(grid);
+ c.appendChild(grid);
+ container.replaceChildren(c);
const sizer = createElement('span', 'ui-grid-sizer');
grid.appendChild(sizer);
this._var.refs.sizer = sizer;
@@ -1525,7 +1567,11 @@ export class Grid {
wrapper.appendChild(table);
// tooltip
if (!this.tooltipDisabled) {
- const holder = createElement('div', 'ui-grid-hover-holder');
+ const holder = createElement('div', 'ui-grid-hover-holder ui-grid-hover ui-tooltip-color',
+ // createElement('div', 'ui-grid-hover-pointer ui-grid-hover ui-tooltip-color'),
+ createElement('div', 'ui-grid-hover-curtain ui-grid-hover ui-tooltip-color'),
+ createElement('div', 'ui-grid-hover-content ui-grid-hover')
+ );
holder.addEventListener('mousedown', e => {
const holder = e.currentTarget;
const row = Number(holder.dataset.row);
@@ -1542,8 +1588,8 @@ export class Grid {
}
// loading
- const loading = createElement('div', 'ui-grid-loading',
- createElement('div', null, createIcon('fa-regular', 'spinner-third'))
+ const loading = createElement('div', 'ui-loading',
+ createElement('div')
);
this._var.refs.loading = loading;
grid.appendChild(loading);
@@ -1958,7 +2004,8 @@ export class Grid {
const gridWrapper = createElement('div', 'ui-sort-panel-grid');
content.append(buttonWrapper, gridWrapper);
const columnSource = this.columns.filter(c => c.sortable !== false); // ticket 56389, && c.visible !== false
- columnSource.sort((a, b) => a.caption > b.caption ? 1 : -1);
+ const lgid = this.lgid;
+ columnSource.sort((a, b) => String(a.caption).localeCompare(b.caption, lgid));
grid.columns = [
{
width: 80,
@@ -2421,8 +2468,12 @@ export class Grid {
return new Promise(resolve => {
let working;
let url;
+ let path = ScriptPath;
+ if (nullOrEmpty(path) && typeof consts !== 'undefined') {
+ path = consts.modulePath;
+ }
if (typeof module === 'string') {
- url = `${ScriptPath}${module}`;
+ url = `${path}${module}`;
} else {
url = URL.createObjectURL(new Blob([`let wasm,WASM_VECTOR_LEN=0,cachegetUint8Memory0=null;function getUint8Memory0(){return null!==cachegetUint8Memory0&&cachegetUint8Memory0.buffer===wasm.memory.buffer||(cachegetUint8Memory0=new Uint8Array(wasm.memory.buffer)),cachegetUint8Memory0}let cachegetInt32Memory0=null;function getInt32Memory0(){return null!==cachegetInt32Memory0&&cachegetInt32Memory0.buffer===wasm.memory.buffer||(cachegetInt32Memory0=new Int32Array(wasm.memory.buffer)),cachegetInt32Memory0}function passArray8ToWasm0(e,t){const a=t(1*e.length);return getUint8Memory0().set(e,a/1),WASM_VECTOR_LEN=e.length,a}function getArrayU8FromWasm0(e,t){return getUint8Memory0().subarray(e/1,e/1+t)}function encode_raw(e,t){var a=passArray8ToWasm0(t,wasm.__wbindgen_malloc),r=WASM_VECTOR_LEN;wasm[e+"_encode_raw"](8,a,r);var s=getInt32Memory0()[2],n=getInt32Memory0()[3],m=getArrayU8FromWasm0(s,n).slice();return wasm.__wbindgen_free(s,1*n),m}self.addEventListener("message",e=>{const t=e.data.type;if("init"===t)if("function"==typeof WebAssembly.instantiateStreaming){const t={},a=fetch(e.data.path+"wasm_flate_bg.wasm");WebAssembly.instantiateStreaming(a,t).then(({instance:e})=>{wasm=e.exports,self.postMessage({type:"init",result:0})}).catch(e=>a.then(t=>{"application/wasm"!==t.headers.get("Content-Type")?self.postMessage({type:"init",error:"\`WebAssembly.instantiateStreaming\` failed because your server does not serve wasm with \`application/wasm\` MIME type. Original error: "+e.message}):self.postMessage({type:"init",error:e.message})}))}else self.postMessage({type:"init",error:"no \`WebAssembly.instantiateStreaming\`"});else if("compress"===t)if(null==wasm)self.postMessage({error:"no \`wasm\` instance"});else{let t=encode_raw("${compressed ?? 'deflate'}",e.data.data);self.postMessage(t,[t.buffer])}});`]));
}
@@ -2469,7 +2520,7 @@ export class Grid {
}
})
working = true;
- worker.postMessage({ type: 'init', path: ScriptPath });
+ worker.postMessage({ type: 'init', path });
});
}
@@ -2515,6 +2566,7 @@ export class Grid {
direction = 1;
}
const editing = col.sortAsText !== true;
+ const lgid = this.lgid;
const comparer = (a, b) => {
a = this._getItemSortProp(a, editing, col);
b = this._getItemSortProp(b, editing, col);
@@ -2541,8 +2593,9 @@ export class Grid {
b = b.join(', ');
}
if (typeof a === 'string' && typeof b === 'string') {
- a = a.toLowerCase();
- b = b.toLowerCase();
+ // a = a.toLowerCase();
+ // b = b.toLowerCase();
+ return a.localeCompare(b, lgid);
}
} else {
if (a == null && b != null) {
@@ -2558,8 +2611,9 @@ export class Grid {
b = b.join(', ');
}
if (typeof a === 'string' && typeof b === 'string') {
- a = a.toLowerCase();
- b = b.toLowerCase();
+ // a = a.toLowerCase();
+ // b = b.toLowerCase();
+ return a.localeCompare(b, lgid);
}
}
return a === b ? 0 : (a > b ? 1 : -1);
@@ -2727,10 +2781,15 @@ export class Grid {
}
th.appendChild(wrapper);
if (!readonly && col.enabled !== false && col.allcheck && alwaysEditing) {
- const check = createCheckbox({
- switch: col.switch,
- onchange: e => this._onColumnAllChecked(col, e.target.checked)
- });
+ let check;
+ if (typeof type.createEdit === 'function') {
+ check = type.createEdit(e => this._onColumnAllChecked(col, e.target.checked), col);
+ } else {
+ check = createCheckbox({
+ switch: col.switch,
+ onchange: e => this._onColumnAllChecked(col, e.target.checked)
+ });
+ }
wrapper.appendChild(check);
}
let caption;
@@ -2752,7 +2811,7 @@ export class Grid {
if (col.captionTooltip != null) {
const help = createIcon('fa-solid', 'question-circle');
wrapper.appendChild(help);
- setTooltip(help, col.captionTooltip, false, this._var.parent);
+ setTooltip(help, col.captionTooltip, false, this._var.container);
}
// order arrow
if (col.sortable) {
@@ -3041,6 +3100,12 @@ export class Grid {
} else if (row.classList.contains('selected')) {
row.classList.remove('selected');
}
+ if (vals.__level !== 0) {
+ row.classList.add('ui-grid-row-level');
+ row.classList.add(`level-${vals.__level}`);
+ } else {
+ row.classList.remove('ui-grid-row-level');
+ }
const stateChanged = virtualRow.editing !== selected;
virtualRow.editing = selected;
// data
@@ -3105,7 +3170,7 @@ export class Grid {
if (col.text != null) {
val = col.text;
} else if (typeof col.filter === 'function') {
- val = col.filter(item, selected, this._var.refs.body, startIndex + i);
+ val = col.filter(item, !this.readonly && selected, this._var.refs.body, startIndex + i);
} else {
val = item[col.key];
if (val != null) {
@@ -3604,7 +3669,7 @@ export class Grid {
/**
* @private
* @param {string} key
- * @param {("autoResize" | "style" | "resizing" | "dragging" | "filterSource" | "filterHeight" | "filterTop")} name
+ * @param {("autoResize" | "style" | "resizing" | "dragging" | "filterSource" | "filterHeight" | "filterTop" | "filterSearched")} name
* @returns {any}
*/
_get(key, name) {
@@ -3618,7 +3683,7 @@ export class Grid {
/**
* @private
* @param {string} key
- * @param {("autoResize" | "style" | "filterSource" | "filterHeight" | "filterTop")} name
+ * @param {("autoResize" | "style" | "filterSource" | "filterHeight" | "filterTop" | "filterSearched")} name
* @param {any} value
*/
_set(key, name, value) {
@@ -3703,7 +3768,7 @@ export class Grid {
if (e.parentElement.classList.contains('ui-switch')) {
return true;
}
- return /^(input|label|layer|svg|use)$/i.test(e.tagName);
+ return /^(i|input|label|layer|svg|use)$/i.test(e.tagName);
}
/**
@@ -3815,6 +3880,7 @@ export class Grid {
panel.style.height = '';
}
+ this._set(col.key, 'filterSearched', false);
// search
let searchbox;
if (col.allowSearch !== false) {
@@ -3884,6 +3950,7 @@ export class Grid {
const type = this._var.colTypes[col.key];
const isDateColumn = type === GridDateColumn || type instanceof GridDateColumn;
const filterAsValue = col.filterAsValue;
+ const lgid = this.lgid;
array.sort((itemA, itemB) => {
let a = itemA.Value;
let b = itemB.Value;
@@ -3901,8 +3968,9 @@ export class Grid {
b = itemB.DisplayValue;
}
if (typeof a === 'string' && typeof b === 'string') {
- a = a.toLowerCase();
- b = b.toLowerCase();
+ // a = a.toLowerCase();
+ // b = b.toLowerCase();
+ return a.localeCompare(b, lgid);
}
}
return a > b ? 1 : (a < b ? -1 : 0);
@@ -3920,7 +3988,7 @@ export class Grid {
};
});
this._fillFilterList(col, itemlist, array, itemall);
- itemall.querySelector('input').checked = ![...itemlist.querySelectorAll('.filter-content input')].some(i => !i.checked);
+ itemall.querySelector('input').checked = array.find(i => !i.__checked) == null;
panel.appendChild(itemlist);
if (searchbox != null) {
searchbox.addEventListener('input', e => {
@@ -3937,6 +4005,7 @@ export class Grid {
}
return String(displayValue).toLowerCase().includes(key);
});
+ this._set(col.key, 'filterSearched', items.length !== array.length);
this._fillFilterList(col, itemlist, items, itemall);
this._set(col.key, 'filterTop', -1);
itemlist.dispatchEvent(new Event('scroll'));
@@ -3949,24 +4018,34 @@ export class Grid {
ok.className = 'button';
ok.innerText = this.langs.ok;
ok.addEventListener('click', () => {
- const array = this._get(col.key, 'filterSource').filter(i => i.__checked !== false);
- if (typeof col.onFilterOk === 'function') {
- col.onFilterOk.call(this, col, array);
+ const filterSource = this._get(col.key, 'filterSource');
+ const filterSearched = this._get(col.key, 'filterSearched');
+ if (!filterSearched && filterSource.find(i => i.__checked === false) == null) {
+ // all checked, equals to 'Reset'
+ delete col.filterValues;
+ this._var.colAttrs.__filtered = this.columns.some(c => c.filterValues != null);
+ filter.replaceChildren(createIcon('fa-solid', this.filterIcon));
+ filter.classList.remove('active');
} else {
- if (GridColumnTypeEnum.isAlwaysEditing(col.type)) {
- col.filterValues = array.map(a => a.Value);
+ const array = filterSource.filter(i => i.__checked !== false);
+ if (typeof col.onFilterOk === 'function') {
+ col.onFilterOk.call(this, col, array);
} else {
- const nullValue = col.filterAllowNull ? null : '';
- col.filterValues = array.map(a => a.Value == null ? nullValue : a.DisplayValue);
+ if (GridColumnTypeEnum.isAlwaysEditing(col.type)) {
+ col.filterValues = array.map(a => a.Value);
+ } else {
+ const nullValue = col.filterAllowNull ? null : '';
+ col.filterValues = array.map(a => a.Value == null ? nullValue : a.DisplayValue);
+ }
}
+ this._var.colAttrs.__filtered = true;
+ filter.replaceChildren(createIcon('fa-solid', this.filteredIcon));
+ filter.classList.add('active');
}
- this._var.colAttrs.__filtered = true;
this._refreshSource();
if (typeof col.onFiltered === 'function') {
col.onFiltered.call(this, col);
}
- filter.replaceChildren(createIcon('fa-solid', this.filteredIcon));
- filter.classList.add('active');
this._onCloseFilter();
});
}),
@@ -3975,7 +4054,7 @@ export class Grid {
reset.innerText = this.langs.reset;
reset.addEventListener('click', () => {
delete col.filterValues;
- this._var.colAttrs.__filtered = this.columns.some(c => c.filterValues != null)
+ this._var.colAttrs.__filtered = this.columns.some(c => c.filterValues != null);
this._refreshSource();
if (typeof col.onFiltered === 'function') {
col.onFiltered.call(this, col);
@@ -3998,34 +4077,39 @@ export class Grid {
* @private
* @param {GridColumnDefinition} col
* @param {HTMLDivElement} list
- * @param {ValueItem[]} array
+ * @param {ValueItem[]} source
* @param {HTMLDivElement} all
*/
- _fillFilterList(col, list, array, all) {
+ _fillFilterList(col, list, source, all) {
list.querySelector('.filter-holder')?.remove();
list.querySelector('.filter-content')?.remove();
const rowHeight = this.filterRowHeight;
- const height = array.length * rowHeight;
+ const height = source.length * rowHeight;
this._set(col.key, 'filterHeight', height);
const holder = createElement('div', 'filter-holder');
holder.style.height = `${height}px`;
const content = createElement('div', 'filter-content');
content.style.top = `${rowHeight}px`;
- this._set(col.key, 'filterSource', array);
+ this._set(col.key, 'filterSource', source);
const propKey = GridColumnTypeEnum.isAlwaysEditing(col.type) ? 'Value' : 'DisplayValue';
const nullValue = col.filterAllowNull ? null : '';
const allSelected = !Array.isArray(col.filterValues);
- for (let item of array) {
- let v = item.Value ?? nullValue;
- if (v != null) {
- v = Object.prototype.hasOwnProperty.call(item, propKey) ? item[propKey] : item;
+ for (let item of source) {
+ if (item.__checked == null) {
+ let v = item.Value ?? nullValue;
+ if (v != null) {
+ v = Object.prototype.hasOwnProperty.call(item, propKey) ? item[propKey] : item;
+ }
+ item.__checked = allSelected || col.filterValues.some(it => Array.isArray(it) ? it.includes(v) : it === v);
}
- item.__checked = allSelected || col.filterValues.some(it => Array.isArray(it) ? it.includes(v) : it === v);
}
- if (array.length > 12) {
- array = array.slice(0, 12);
+ let array;
+ if (source.length > 12) {
+ array = source.slice(0, 12);
+ } else {
+ array = source;
}
- this._doFillFilterList(col, content, array, all);
+ this._doFillFilterList(col, content, array, source, all);
list.append(holder, content);
}
@@ -4034,9 +4118,10 @@ export class Grid {
* @param {GridColumnDefinition} col
* @param {HTMLDivElement} content
* @param {ValueItem[]} array
+ * @param {ValueItem[]} source
* @param {HTMLDivElement} all
*/
- _doFillFilterList(col, content, array, all) {
+ _doFillFilterList(col, content, array, source, all) {
for (let item of array) {
const div = createElement('div', 'filter-item');
const title = Object.prototype.hasOwnProperty.call(item, 'DisplayValue') ? item.DisplayValue : item;
@@ -4053,7 +4138,7 @@ export class Grid {
title,
onchange: e => {
item.__checked = e.target.checked;
- all.querySelector('input').checked = ![...content.querySelectorAll('input')].some(i => !i.checked);
+ all.querySelector('input').checked = source.find(i => !i.__checked) == null;
}
}));
content.appendChild(div);
@@ -4083,15 +4168,16 @@ export class Grid {
if (this._get(col.key, 'filterTop') !== top) {
this._set(col.key, 'filterTop', top);
const startIndex = top / rowHeight;
- let array = this._get(col.key, 'filterSource');
- if (startIndex + 12 < array.length) {
- array = array.slice(startIndex, startIndex + 12);
+ let source = this._get(col.key, 'filterSource');
+ let array;
+ if (startIndex + 12 < source.length) {
+ array = source.slice(startIndex, startIndex + 12);
} else {
- array = array.slice(-12);
+ array = source.slice(-12);
}
const content = list.querySelector('.filter-content');
content.replaceChildren();
- this._doFillFilterList(col, content, array, list.querySelector('.filter-all'));
+ this._doFillFilterList(col, content, array, source, list.querySelector('.filter-all'));
content.style.top = `${top + rowHeight}px`;
}
}
@@ -4341,7 +4427,7 @@ export class Grid {
*/
_onGridMouseMove(e, holder) {
e.stopPropagation();
- if (e.target.classList.contains('ui-grid-hover-holder')) {
+ if (e.target.classList.contains('ui-grid-hover')) {
return;
}
let [parent, target] = this._getRowTarget(e.target);
@@ -4364,8 +4450,10 @@ export class Grid {
holder.dataset.col === col) {
return;
}
- const type = this._var.colTypes[this.columns[col]?.key];
- if (type?.canEdit && this._var.virtualRows[row]?.editing) {
+ const key = this.columns[col]?.key ?? col;
+ const type = this._var.colTypes[key];
+ const virtualRow = this._var.virtualRows[row];
+ if (type?.canEdit && virtualRow?.editing) {
delete holder.dataset.row;
delete holder.dataset.col;
if (holder.classList.contains('active')) {
@@ -4391,7 +4479,7 @@ export class Grid {
element.scrollHeight > element.offsetHeight) {
holder.dataset.row = row;
holder.dataset.col = col;
- holder.innerText = element.innerText;
+ holder.querySelector('.ui-grid-hover-content').innerText = element.innerText;
const top = (parent.classList.contains('ui-grid-total-row') ? this._var.refs.footer.parentElement.offsetTop + 1 : target.offsetTop) + this._var.refs.table.offsetTop;
let left = target.offsetLeft;
let width = holder.offsetWidth;
@@ -4402,8 +4490,13 @@ export class Grid {
if (left > maxleft) {
left = maxleft;
}
- const height = target.offsetHeight;
- holder.style.cssText = `top: ${top}px; left: ${left}px; max-width: ${this._var.wrapClientWidth}px; min-height: ${height - 2}px`;
+ // const height = target.offsetHeight;
+ holder.style.cssText = `top: ${top}px; left: ${left}px; max-width: ${this._var.wrapClientWidth}px`; // ; min-height: ${height - 2}px
+ if (top > this.rowHeight * 2) {
+ holder.classList.remove('ui-grid-hover-down');
+ } else {
+ holder.classList.add('ui-grid-hover-down');
+ }
holder.classList.add('active');
} else if (holder.classList.contains('active')) {
delete holder.dataset.row;
diff --git a/lib/ui/popup.js b/lib/ui/popup.js
index 81dc242..b71ad22 100644
--- a/lib/ui/popup.js
+++ b/lib/ui/popup.js
@@ -17,6 +17,8 @@ const ResizeMods = {
topLeft: 8 | 4
}
+const IconStyle = 'fa-light';
+
// const Cursors = {
// [ResizeMods.right]: 'ew-resize',
// [ResizeMods.bottom]: 'ns-resize',
@@ -110,7 +112,7 @@ export class Popup {
this._var.bounds = r;
container.classList.add('ui-popup-collapse');
if (collapse != null) {
- changeIcon(collapse, 'fa-regular', 'expand-alt');
+ changeIcon(collapse, IconStyle, 'expand-alt');
}
} else {
if (!isNaN(r.width) && r.width > 0) {
@@ -122,7 +124,7 @@ export class Popup {
container.classList.remove('ui-popup-collapse');
this._var.bounds = null;
if (collapse != null) {
- changeIcon(collapse, 'fa-regular', 'compress-alt');
+ changeIcon(collapse, IconStyle, 'compress-alt');
}
}
if (css.length > 0) {
@@ -259,7 +261,7 @@ export class Popup {
const icons = createElement('div', icons => {
icons.className = 'ui-popup-header-icons';
if (option.collapsable === true) {
- const collapse = createIcon('fa-regular', 'compress-alt');
+ const collapse = createIcon(IconStyle, 'compress-alt');
collapse.tabIndex = tabIndex + 2;
collapse.classList.add('icon-expand');
collapse.addEventListener('keypress', e => {
@@ -275,13 +277,13 @@ export class Popup {
this._var.bounds = null;
}
container.classList.remove('ui-popup-collapse');
- changeIcon(collapse, 'fa-regular', 'compress-alt');
+ changeIcon(collapse, IconStyle, 'compress-alt');
} else {
const rect = this.rect;
this._var.bounds = rect;
container.style.cssText += `width: 160px; height: 40px`;
container.classList.add('ui-popup-collapse');
- changeIcon(collapse, 'fa-regular', 'expand-alt');
+ changeIcon(collapse, IconStyle, 'expand-alt');
}
if (typeof option.onResizeEnded === 'function') {
option.onResizeEnded.call(this);
@@ -290,7 +292,7 @@ export class Popup {
icons.appendChild(collapse);
}
if (option.closable !== false) {
- const cancel = createIcon('fa-regular', 'times');
+ const cancel = createIcon(IconStyle, 'times');
cancel.tabIndex = tabIndex + 3;
cancel.addEventListener('keypress', e => {
if (e.key === ' ' || e.key === 'Enter') {
@@ -304,7 +306,7 @@ export class Popup {
header.appendChild(icons);
}),
createElement('div', 'ui-popup-body', content, createElement('div', 'ui-popup-loading',
- createElement('div', null, createIcon('fa-regular', 'spinner-third'))
+ createElement('div', null, createIcon(IconStyle, 'spinner-third'))
))
);
if (Array.isArray(option.buttons) && option.buttons.length > 0) {
@@ -315,6 +317,9 @@ export class Popup {
if (b.className != null) {
button.classList.add(b.className);
}
+ if (b.isPrimary) {
+ button.classList.add('primary');
+ }
if (b.tabIndex > 0) {
button.tabIndex = b.tabIndex;
} else {
@@ -560,6 +565,7 @@ export function resolvePopup(wrapper, callback, removable, zIndex) {
const buttons = [...wrapper.querySelectorAll('.dialog-func>input[type="button"]')].reverse().map(b => ({
tabIndex: b.tabIndex,
text: b.value,
+ isPrimary: b.dataset.primary != null,
trigger: b.onclick == null ? null : (popup => (b.onclick.call(popup), false))
}));
const popup = new Popup({
@@ -594,16 +600,54 @@ export function showAlert(title, message, iconType = 'info', parent = document.b
),
resolve,
buttons: [
- { text: r('ok', 'OK') }
+ { text: r('ok', 'OK'), isPrimary: true }
]
});
popup.show(parent).then(mask => {
- const button = mask.querySelector('.ui-popup-container .ui-popup-footer .ui-popup-button:last-child');
+ const button = mask.querySelector('.ui-popup-container .ui-popup-footer .ui-popup-button');
button?.focus();
});
});
}
+export function showInput(title, multiline, message, def, placeholder, buttons, parent = document.body) {
+ const r = typeof GetTextByKey === 'function' ? GetTextByKey : lang;
+ return new Promise(resolve => {
+ let tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0));
+ if (tabIndex < 0) {
+ tabIndex = 0;
+ }
+ const input = multiline ?
+ createElement('textarea', text => {
+ text.className = 'ui-text';
+ }) :
+ createElement('input', input => {
+ input.type = 'text';
+ input.className = 'ui-input';
+ });
+ input.tabIndex = tabIndex + 4;
+ if (!nullOrEmpty(placeholder)) {
+ input.placeholder = placeholder;
+ }
+ if (!nullOrEmpty(def)) {
+ input.value = def;
+ }
+ const popup = new Popup({
+ title,
+ content: createElement('div', 'message-wrapper message-wrapper-input',
+ nullOrEmpty(message) ? '' : createElement('span', span => span.innerText = message),
+ input
+ ),
+ resolve: r => resolve(r.result === 'yes' ? input.value : null),
+ buttons: buttons ?? [
+ { key: 'yes', text: r('yes', 'Yes'), isPrimary: true },
+ { key: 'no', text: r('no', 'No') }
+ ]
+ });
+ popup.show(parent).then(() => input.focus());
+ });
+}
+
export function showConfirm(title, content, buttons, iconType = 'question', parent = document.body) {
const r = typeof GetTextByKey === 'function' ? GetTextByKey : lang;
return new Promise(resolve => {
@@ -621,6 +665,7 @@ export function showConfirm(title, content, buttons, iconType = 'question', pare
buttons: buttons?.map((b, i) => {
return {
text: b.text,
+ isPrimary: b.isPrimary,
trigger: p => {
let result;
if (typeof b.trigger === 'function') {
@@ -633,12 +678,12 @@ export function showConfirm(title, content, buttons, iconType = 'question', pare
};
}) ??
[
- { key: 'yes', text: r('yes', 'Yes') },
+ { key: 'yes', text: r('yes', 'Yes'), isPrimary: true },
{ key: 'no', text: r('no', 'No') }
]
});
popup.show(parent).then(mask => {
- const button = mask.querySelector('.ui-popup-container .ui-popup-footer .ui-popup-button:last-child');
+ const button = mask.querySelector('.ui-popup-container .ui-popup-footer .ui-popup-button:first-child');
button?.focus();
});
});
diff --git a/lib/ui/tooltip.js b/lib/ui/tooltip.js
index b27d550..a666818 100644
--- a/lib/ui/tooltip.js
+++ b/lib/ui/tooltip.js
@@ -4,7 +4,7 @@ import { global } from '../utility';
const pointerHeight = 12;
-export function setTooltip(container, content, flag = false, parent = null) {
+export function setTooltip(container, content, flag = false, parent = null, maxLeft = null) {
const isParent = parent instanceof HTMLElement;
if (isParent) {
const tipid = container.dataset.tipId;
@@ -54,6 +54,7 @@ export function setTooltip(container, content, flag = false, parent = null) {
return;
}
if (!flag || c.scrollWidth > c.offsetWidth) {
+ wrapper.style.cssText += 'left: -9999px; top: -9999px; display: block';
tid = setTimeout(() => {
let p;
let left;
@@ -75,7 +76,6 @@ export function setTooltip(container, content, flag = false, parent = null) {
top -= p.scrollTop;
p = p.parentElement;
}
- wrapper.style.display = '';
const offsetHeight = wrapper.offsetHeight;
const offsetWidth = wrapper.offsetWidth;
if (isParent) {
@@ -150,6 +150,15 @@ export function setTooltip(container, content, flag = false, parent = null) {
left = lastWidth - offsetWidth - 1;
}
}
+ if (typeof maxLeft === 'function') {
+ const max = maxLeft(offsetWidth);
+ if (left > max) {
+ wrapper.style.setProperty('--pointer-left', 'calc(100% - 20px)');
+ left = max;
+ } else {
+ wrapper.style.setProperty('--pointer-left', null);
+ }
+ }
// wrapper.style.left = `${left}px`;
// wrapper.style.top = `${top}px`;
// wrapper.style.visibility = 'visible';
diff --git a/lib/utility/lgres.js b/lib/utility/lgres.js
index 47d27eb..d04237d 100644
--- a/lib/utility/lgres.js
+++ b/lib/utility/lgres.js
@@ -63,8 +63,9 @@ async function refreshLgres(template, lgres) {
lgres = await doRefreshLgres(template);
}
const ver = Number(consts.resver);
- if (isNaN(lgres.ver) || isNaN(ver) || ver > lgres.ver) {
- console.log(`found new language res version: ${lgres.ver} => ${ver}`);
+ const currentVer = Number(lgres.ver);
+ if (isNaN(currentVer) || isNaN(ver) || ver > currentVer) {
+ console.log(`found new language res version: ${lgres.ver} => ${consts.resver}`);
lgres = await doRefreshLgres(template);
}
Object.defineProperty(lgres, 'r', {
diff --git a/lib/utility/request.js b/lib/utility/request.js
index 7796ba0..ef9a81c 100644
--- a/lib/utility/request.js
+++ b/lib/utility/request.js
@@ -37,13 +37,25 @@ export function post(url, data, options = {}) {
options.customHeaders['Content-Type'] = 'application/json';
}
}
- return fetch(combineUrl(url), {
+ const opts = {
method: options.method || 'POST',
headers: options.customHeaders,
body: data,
signal: options.signal,
cache: 'no-cache'
- });
+ };
+ if (options.diagnostic) {
+ const started = new Date().getTime();
+ return new Promise((resolve, reject) => {
+ fetch(combineUrl(url), opts).then(response => {
+ resolve({
+ time: new Date().getTime() - started,
+ response
+ });
+ }).catch(reject);
+ });
+ }
+ return fetch(combineUrl(url), opts);
}
export function upload(url, data, options = {}) {