From 752bb2357101588a577d15d55f73f5ef574511fc Mon Sep 17 00:00:00 2001 From: LChen Date: Wed, 24 Dec 2025 10:55:40 +0800 Subject: [PATCH] sync --- lib/app/communications/comments.js | 33 +- lib/app/communications/customer.js | 9 +- lib/app/communications/internal.js | 34 +- lib/app/communications/lib.js | 27 + lib/app/communications/style.scss | 149 +-- lib/element/addworkorder.js | 199 +++- lib/element/inspectionWizard.js | 18 +- lib/element/style.scss | 28 + lib/ui.js | 97 +- lib/ui/checkbox.js | 2 +- lib/ui/css/common.scss | 103 ++ lib/ui/css/dropdown.scss | 43 +- lib/ui/css/grid.scss | 1314 +++++++++++++------------- lib/ui/css/popup.scss | 21 +- lib/ui/css/tooltip.scss | 2 +- lib/ui/css/variables/definition.scss | 24 + lib/ui/dropdown.js | 24 +- lib/ui/grid/column.d.ts | 391 ++++++++ lib/ui/grid/column.js | 5 + lib/ui/grid/grid.d.ts | 325 +++++++ lib/ui/grid/grid.js | 215 +++-- lib/ui/popup.js | 67 +- lib/ui/tooltip.js | 13 +- lib/utility/lgres.js | 5 +- lib/utility/request.js | 16 +- 25 files changed, 2348 insertions(+), 816 deletions(-) create mode 100644 lib/ui/grid/column.d.ts create mode 100644 lib/ui/grid/grid.d.ts 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 = {}) {