Compare commits

...

21 Commits

Author SHA1 Message Date
eec9d6045c sync 2024-08-30 17:36:21 +08:00
a3f0288c92 sync 2024-07-22 10:42:32 +08:00
6b1e74790b . 2024-07-12 14:57:06 +08:00
adbf4750cc sync 2024-07-10 12:19:52 +08:00
5baf00de64 add: getText compatibility.
add: `AssetSelector` and `TemplateSelector`.
add: `popup-selector` style class.
add: `ui.resolvePopup` function.
add: `switch` in checkbox.
add: `GridColumn.filterTemplate` supports.
add: add `action` callback in `createIcon`.
change: replace `setTimeout(..., 0)` with `requestAnimationFrame`.
change: Popup result structure adjustment ({ result: any, popup: Popup }).
change: complete add work order flow.
change: reduce Popup title height.
fix: Grid column sort in number.
2024-06-21 17:28:11 +08:00
1a7aa1ab66 add ui-icon className. 2024-06-11 14:52:26 +08:00
d296dd01fd add cjs-dist format 2024-06-07 16:32:50 +08:00
f5b6ce360e . 2024-06-07 16:16:10 +08:00
614a983aa8 sync 2024-06-07 16:15:42 +08:00
ea7f4f538a add: ui-switch style
add: virtual mode in Dropdown
2024-05-27 17:06:00 +08:00
190e43c814 feature: tiered sort 2024-05-22 09:27:22 +08:00
a946012a33 feature: drag support in sort panel. 2024-05-13 16:46:55 +08:00
f676ec76db remove dependency of jQuery plugins.
Grid.expandAll
add total field of exported data.
2024-04-24 14:10:28 +08:00
4e8be83625 feature: seek buffer bars. 2024-04-16 10:39:51 +08:00
d91630212f compatible with Chrome 109.x 2024-04-15 16:30:47 +08:00
5cbbcf8d81 feature: combined-video player. 2024-04-11 17:29:26 +08:00
b6fe3e34f5 issue: customer communication contacts grid height.
change: replace schedule date control.
change: date validation from 1753-01-01 to 9999-12-31.
2024-04-07 08:39:44 +08:00
0ff48a0ac4 change: replace date controller of Scheduler.
feature: `ui.formatDate` supports formatter.
fix: issue about boolean sort.
2024-03-28 16:37:22 +08:00
03e3b4a70f add: column.filterAsValue
fix: case-sensitive order of Array value
2024-03-27 13:36:31 +08:00
c78e445a24 add: comments 2024-03-27 10:09:25 +08:00
6458427c75 fix: issue when input 'MM/dd/yyyy' date format 2024-03-27 10:08:54 +08:00
53 changed files with 13321 additions and 3853 deletions

View File

@ -1,3 +1,36 @@
# [ui-lib].Grid # [ui-lib].Grid
UI Mordern Gridview Library UI Mordern Gridview Library
## 1.0.7
* 调整: [(event) onBodyScrolled(e: Event, index: number, count: number)](Grid.html#event:onBodyScrolled) - 列滚动时触发的事件,增加显示行起始索引与一屏呈现行数
* 新增: [`currentItem: GridRowItem | null`](Grid.html#currentItem) - 获取当前选中的行对象
## 1.0.6
* 新增: [`col.switch`](GridColumnDefinition.html) - 列复选框样式改为 `ui-switch`
## 1.0.5
* 新增: [`col`.filterTemplate: (this: GridColumnDefinition, item: ValueItem) => HTMLElement](GridColumnDefinition.html) - 列头过滤项的模板函数
## 1.0.4
* 调整: `Dropdown` 组件支持虚模式
* 新增: `ui-switch` 样式iOS 切换组件)
* 新增: `Dropdown` 组件增加 `ignoreAll` 参数
## 1.0.3
* 调整: [showSortPanel(callback?: Function, layout?: boolean)](Grid.html#showSortPanel) 现支持输入搜索列,已添加的列不会重复显示在下拉数据源中,增加回调函数与 layout 更新复选框。
* 新增: [onRowChanged(action: "update" | "add" | "remove", items: GridRowItem[], indexes: number | number[])](Grid.html#onRowChanged) - 行发生变化时触发的事件
### bugs
* 修复: 清空多列排序后列头箭头没有清除的异常。
## 1.0.2
* 新增: [export(compressed: string | boolean, module?: string) : Promise<GridExportData>](Grid.html#export) - 导出已压缩的数据源
## 1.0.1
* 新增: [total: GridRowItem](Grid.html#total) - 获取或设置合计行数据
* 新增: [showSortPanel()](Grid.html#showSortPanel) - 显示多列排序设置面板
* 新增: [setItem(index: number, item: GridRowItem)](Grid.html#setItem) - 设置单行数据
* 新增: [addItem(item: GridRowItem, index?: number): GridRowItem](Grid.html#addItem) - 添加行数据
* 新增: [addItems(array: GridRowItem[], index?: number) : GridRowItem[]](Grid.html#addItems) - 批量添加行数据
* 新增: [removeItem(index: number) : GridRowItem](Grid.html#removeItem) - 删除行数据
* 新增: [removeItems(indexes?: number[]): GridRowItem[]](Grid.html#removeItems) - 批量删除行数据

View File

@ -2,9 +2,11 @@ import "./app/communications/style.scss";
import CustomerCommunication from "./app/communications/customer"; import CustomerCommunication from "./app/communications/customer";
import InternalComment from "./app/communications/internal"; import InternalComment from "./app/communications/internal";
import CustomerRecordComment from "./app/communications/comments"; import CustomerRecordComment from "./app/communications/comments";
import { createHideMessageTitleButton } from "./app/communications/lib";
export { export {
CustomerCommunication, CustomerCommunication,
InternalComment, InternalComment,
CustomerRecordComment CustomerRecordComment,
createHideMessageTitleButton
} }

View File

@ -1,6 +1,6 @@
import { createElement, setTooltip, createIcon } from "../../ui"; import { createElement, setTooltip, createIcon, requestAnimationFrame } from "../../ui";
import { r as lang, nullOrEmpty, escapeHtml, escapeEmoji } from "../../utility"; import { r as lang, nullOrEmpty, escapeHtml, escapeEmoji } from "../../utility";
import { createBox, appendMedia } from "./lib"; import { createBox, appendMedia, createHideMessageTitleButton, createHideMessageCommentTail } from "./lib";
let r = lang; let r = lang;
@ -12,6 +12,8 @@ export default class CustomerRecordComment {
const getText = opt?.getText; const getText = opt?.getText;
if (typeof getText === 'function') { if (typeof getText === 'function') {
r = getText; r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
} }
} }
@ -25,6 +27,20 @@ export default class CustomerRecordComment {
} }
} }
/**
* @param {boolean} flag
*/
set messageHidden(flag) {
const el = this._var.container.querySelector('.msgadminsetting');
if (el == null) {
return;
}
this._var.option.showCommentHidden = flag;
// TODO: 是否与参数 flag 无关
el.classList.remove('iconview');
el.classList.add('iconnotview');
}
/** /**
* @param {boolean} flag * @param {boolean} flag
*/ */
@ -55,8 +71,10 @@ export default class CustomerRecordComment {
createElement('div', null, createElement('div', null,
createElement('div', div => { createElement('div', div => {
div.className = 'title-module'; div.className = 'title-module';
div.innerText = r('P_CR_COMMENTS', 'Comments'); div.innerText = r('FLTL_00584', 'Comments');
}) },
createHideMessageTitleButton(this, 'showCommentHidden')
)
), ),
[ [
createElement('button', button => { createElement('button', button => {
@ -79,7 +97,7 @@ export default class CustomerRecordComment {
); );
// enter box // enter box
const enter = createElement('textarea', 'ui-text'); const enter = createElement('textarea', 'ui-text');
enter.placeholder = r('P_CU_ENTERCOMMENTHERE', 'Enter Comment Here'); enter.placeholder = r('FLTL_01154', 'Enter Comment Here');
enter.maxLength = this._var.option.maxLength ??= 3000; enter.maxLength = this._var.option.maxLength ??= 3000;
enter.addEventListener('input', () => { enter.addEventListener('input', () => {
const val = this.text; const val = this.text;
@ -102,8 +120,8 @@ export default class CustomerRecordComment {
button.style.display = 'none'; button.style.display = 'none';
} }
button.appendChild(createIcon('fa-solid', 'paper-plane')); button.appendChild(createIcon('fa-solid', 'paper-plane'));
// setTooltip(button, r('P_M3_SENDMESSAGE', 'Send Message')); // setTooltip(button, r('FLTL_02692', 'Send Message'));
setTooltip(button, r('P_CU_POSTNOTE', 'Post Note')); setTooltip(button, r('FLTL_02301', 'Post Note'));
button.addEventListener('click', () => { button.addEventListener('click', () => {
if (typeof this._var.option.onAddComment === 'function') { if (typeof this._var.option.onAddComment === 'function') {
this._var.option.onAddComment(this.text); this._var.option.onAddComment(this.text);
@ -120,13 +138,20 @@ export default class CustomerRecordComment {
return this._var.container = container; return this._var.container = container;
} }
load(data) { load(data, func, hisFunc, keep) {
const children = []; const children = [];
if (data?.length > 0) { if (data?.length > 0) {
const lastVisible = this._var.option.showCommentHidden;
for (let comment of data) { for (let comment of data) {
const div = createElement('div', 'item-div'); const div = createElement('div', 'item-div');
if (comment.Hidden) {
div.classList.add('hidden-content');
if (!lastVisible) {
div.style.display = 'none';
}
}
// if (sendto !== '') { // if (sendto !== '') {
// sendto = r('P_CU_SENDTO_COLON', 'Sent To :') + `\n${sendto}`; // sendto = r('FLTL_02716', 'Sent To :') + `\n${sendto}`;
// } // }
div.appendChild(createElement('div', div => { div.appendChild(createElement('div', div => {
div.className = 'item-poster'; div.className = 'item-poster';
@ -143,17 +168,19 @@ export default class CustomerRecordComment {
} }
div.append( div.append(
content, content,
createElement('div', div => { createHideMessageCommentTail(
div.className = 'item-time'; this, 'showCommentHidden',
div.innerText = comment.SubmitLocalDateStr; comment, 'SubmitLocalDateStr',
}) func, hisFunc)
); );
children.push(div); children.push(div);
} }
children[0].style.marginTop = '0'; children[0].style.marginTop = '0';
} }
if (this._var.message.children.length > 0) {
this._var.lastTop = this._var.message.scrollTop;
}
this._var.message.replaceChildren(...children); this._var.message.replaceChildren(...children);
this._var.message.scrollTop = this._var.message.scrollHeight requestAnimationFrame(() => this._var.message.scrollTop = keep ? this._var.lastTop : this._var.message.scrollHeight);
// setTimeout(() => this._var.message.scrollTop = this._var.message.scrollHeight, 0);
} }
} }

View File

@ -1,4 +1,4 @@
import { Grid, Dropdown, createElement, createCheckbox, Popup, showAlert } from "../../ui"; import { Grid, Dropdown, createElement, createCheckbox, Popup, showAlert, requestAnimationFrame } from "../../ui";
import { isEmail, nullOrEmpty, r as lang } from "../../utility"; import { isEmail, nullOrEmpty, r as lang } from "../../utility";
let r = lang; let r = lang;
@ -11,6 +11,8 @@ export class Contact {
const getText = option?.getText; const getText = option?.getText;
if (typeof getText === 'function') { if (typeof getText === 'function') {
r = getText; r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
} }
} }
@ -27,9 +29,9 @@ export class Contact {
}); });
const preferences = new Dropdown({ tabIndex: tabIndex + 2 }); const preferences = new Dropdown({ tabIndex: tabIndex + 2 });
preferences.source = [ preferences.source = [
{ value: '0', text: r('P_CR_TEXT', 'Text') }, { value: '0', text: r('FLTL_02915', 'Text') },
{ value: '1', text: r('P_CR_EMAIL', 'Email') }, { value: '1', text: r('FLTL_01089', 'Email') },
{ value: '2', text: r('P_CR_PHONE', 'Phone') } { value: '2', text: r('FLTL_02194', 'Phone') }
]; ];
const contactEmail = createElement('input', input => { const contactEmail = createElement('input', input => {
input.type = 'email'; input.type = 'email';
@ -55,7 +57,7 @@ export class Contact {
const buttons = []; const buttons = [];
if (this._var.option.company) { if (this._var.option.company) {
buttons.push({ buttons.push({
text: c == null ? r('P_WO_ADDCONTACTRECORD', 'Add Contact Record') : r('P_WO_EDITCONTACTRECORD', 'Edit Contact Record'), text: c == null ? r('FLTL_00100', 'Add Contact Record') : r('FLTL_01042', 'Edit Contact Record'),
// tabIndex: tabIndex + 7, // tabIndex: tabIndex + 7,
trigger: () => { trigger: () => {
const item = this.prepare(); const item = this.prepare();
@ -71,7 +73,7 @@ export class Contact {
} }
buttons.push( buttons.push(
{ {
text: r('P_WO_WORKORDERONLY', 'Work Order Only'), text: r('FLTL_03348', 'Work Order Only'),
// tabIndex: tabIndex + 8, // tabIndex: tabIndex + 8,
trigger: () => { trigger: () => {
const item = this.prepare(); const item = this.prepare();
@ -86,39 +88,39 @@ export class Contact {
} }
}, },
{ {
text: r('P_WO_CANCEL', 'Cancel'), text: r('FLTL_00499', 'Cancel'),
// tabIndex: tabIndex + 9 // tabIndex: tabIndex + 9
} }
); );
const popup = new Popup({ const popup = new Popup({
onMasking: this._var.option.onMasking, onMasking: this._var.option.onMasking,
title: c == null ? r('P_CR_ADDCONTACT', 'Add Contact') : r('P_CR_EDITCONTACT', 'Edit Contact'), title: c == null ? r('FLTL_00099', 'Add Contact') : r('FLTL_01041', 'Edit Contact'),
content: createElement('div', wrapper => { content: createElement('div', wrapper => {
wrapper.className = 'setting-wrapper'; wrapper.className = 'setting-wrapper';
wrapper.style.width = '500px'; wrapper.style.width = '500px';
}, },
createElement('div', 'setting-item', createElement('div', 'setting-item',
createElement('span', 'setting-label setting-required', r('P_CR_CONTACTNAME_COLON', 'Contact Name:')), createElement('span', 'setting-label setting-required', r('FLTL_00640', 'Contact Name:')),
contactName contactName
), ),
createElement('div', 'setting-item', createElement('div', 'setting-item',
createElement('span', 'setting-label', r('P_CR_CONTACTPREFERENCES_COLON', 'Contact Preferences:')), createElement('span', 'setting-label', r('FLTL_00643', 'Contact Preferences:')),
preferences.create() preferences.create()
), ),
createElement('div', 'setting-item', createElement('div', 'setting-item',
createElement('span', 'setting-label', r('P_CR_EMAILADDRESS_COLON', 'Email Address:')), createElement('span', 'setting-label', r('FLTL_01092', 'Email Address:')),
contactEmail contactEmail
), ),
createElement('div', 'setting-item', createElement('div', 'setting-item',
createElement('span', 'setting-label', r('P_WO_MOBILE_COLON', 'Mobile:')), createElement('span', 'setting-label', r('FLTL_01932', 'Mobile:')),
contactMobile contactMobile
), ),
createElement('div', 'setting-item', createElement('div', 'setting-item',
createElement('span', 'setting-label', r('P_CR_OPTOUT_COLON', 'Opt Out:')), createElement('span', 'setting-label', r('FLTL_02089', 'Opt Out:')),
checkOpt checkOpt
), ),
createElement('div', 'setting-item', createElement('div', 'setting-item',
createElement('span', 'setting-label', r('P_CR_NOTES_COLON', 'Notes:')), createElement('span', 'setting-label', r('FLTL_02017', 'Notes:')),
contactNotes contactNotes
) )
), ),
@ -143,7 +145,7 @@ export class Contact {
contactNotes contactNotes
}; };
const result = await popup.show(parent); const result = await popup.show(parent);
setTimeout(() => contactName.focus()); requestAnimationFrame(() => contactName.focus());
return result; return result;
} }
@ -154,24 +156,24 @@ export class Contact {
const phone = this._var.refs.contactMobile.value; const phone = this._var.refs.contactMobile.value;
const opt = this._var.refs.checkOpt.querySelector('input').checked; const opt = this._var.refs.checkOpt.querySelector('input').checked;
const notes = this._var.refs.contactNotes.value; const notes = this._var.refs.contactNotes.value;
const title = this._var.option.contact == null ? r('P_CR_ADDCONTACT', 'Add Contact') : r('P_CR_EDITCONTACT', 'Edit Contact'); const title = this._var.option.contact == null ? r('FLTL_00099', 'Add Contact') : r('FLTL_01041', 'Edit Contact');
if (nullOrEmpty(name)) { if (nullOrEmpty(name)) {
showAlert(title, r('P_CR_CONTACTNAMECANNOTBEEMPTY', 'Contact Name cannot be empty.'), 'warn') showAlert(title, r('FLTL_00639', 'Contact Name cannot be empty.'), 'warn')
.then(() => this._var.refs.contactName.focus()); .then(() => this._var.refs.contactName.focus());
return null; return null;
} }
if ((pref == 0 || pref == 2) && nullOrEmpty(phone)) { if ((pref == 0 || pref == 2) && nullOrEmpty(phone)) {
showAlert(title, r('P_CR_MOBILECANNOTBEEMPTY', 'Mobile cannot be empty.'), 'warn') showAlert(title, r('FLTL_01929', 'Mobile cannot be empty.'), 'warn')
.then(() => this._var.refs.contactMobile.focus()); .then(() => this._var.refs.contactMobile.focus());
return null; return null;
} }
if (pref == 1 && nullOrEmpty(email)) { if (pref == 1 && nullOrEmpty(email)) {
showAlert(title, r('P_CU_EMAILCANNOTBEEMPTY', 'Email cannot be empty.'), 'warn') showAlert(title, r('FLTL_01094', 'Email cannot be empty.'), 'warn')
.then(() => this._var.refs.contactEmail.focus()); .then(() => this._var.refs.contactEmail.focus());
return null; return null;
} }
if (!nullOrEmpty(email) && !isEmail(email)) { if (!nullOrEmpty(email) && !isEmail(email)) {
showAlert(title, r('P_CR_EMAILISNOTAVALIDEMAILADDRESS', 'The email address is invalid.'), 'warn') showAlert(title, r('FLTL_02952', 'The email address is invalid.'), 'warn')
.then(() => this._var.refs.contactEmail.focus()); .then(() => this._var.refs.contactEmail.focus());
return null; return null;
} }
@ -204,6 +206,8 @@ export class CustomerRecordContact {
const getText = option?.getText; const getText = option?.getText;
if (typeof getText === 'function') { if (typeof getText === 'function') {
r = getText; r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
} }
} }
@ -219,7 +223,7 @@ export class CustomerRecordContact {
), ),
buttons: [ buttons: [
{ {
text: r('P_WO_OK', 'OK'), text: r('FLTL_02057', 'OK'),
key: 'ok', key: 'ok',
trigger: () => { trigger: () => {
if (typeof this._var.option.onOk === 'function') { if (typeof this._var.option.onOk === 'function') {
@ -227,7 +231,7 @@ export class CustomerRecordContact {
} }
} }
}, },
{ text: r('P_WO_CANCEL', 'Cancel'), key: 'cancel' } { text: r('FLTL_00499', 'Cancel'), key: 'cancel' }
] ]
}); });
const result = await popup.show(parent); const result = await popup.show(parent);
@ -240,12 +244,12 @@ export class CustomerRecordContact {
width: 40, width: 40,
// enabled: item => !nullOrEmpty(item.ID) // enabled: item => !nullOrEmpty(item.ID)
}, },
{ key: 'Name', caption: r("P_CR_CONTACTNAME", "Contact Name"), width: 100 }, { key: 'Name', caption: r('FLTL_00637', 'Contact Name'), width: 100 },
{ key: 'Email', caption: r("P_CR_CONTACTEMAIL", "Contact Email"), css: { 'width': 180, 'text-align': 'left' } }, { key: 'Email', caption: r('FLTL_00633', 'Contact Email'), css: { 'width': 180, 'text-align': 'left' } },
{ key: 'MobilePhoneDisplayText', caption: r("P_CR_CONTACTMOBILE", "Contact Mobile"), width: 130 }, { key: 'MobilePhoneDisplayText', caption: r('FLTL_00636', 'Contact Mobile'), width: 130 },
{ key: 'ContactPreferenceStr', caption: r("P_CR_CONTACTPREFERENCES", "Contact Preferences"), width: 100 }, { key: 'ContactPreferenceStr', caption: r('FLTL_00642', 'Contact Preferences'), width: 100 },
{ key: 'OptOut', caption: r("P_CR_OPTOUT", "Opt Out"), type: Grid.ColumnTypes.Checkbox, width: 70, enabled: false, align: 'center' }, { key: 'OptOut', caption: r('FLTL_02084', 'Opt Out'), type: Grid.ColumnTypes.Checkbox, width: 70, enabled: false, align: 'center' },
{ key: 'Notes', caption: r("P_CR_NOTES", "Notes"), width: 120 } { key: 'Notes', caption: r('FLTL_02012', 'Notes'), width: 120 }
]; ];
grid.init(); grid.init();
grid.source = this._var.option.contacts.sort(function (a, b) { return ((b.Text || b.Email) ? 1 : 0) - ((a.Text || a.Email) ? 1 : 0) }); grid.source = this._var.option.contacts.sort(function (a, b) { return ((b.Text || b.Email) ? 1 : 0) - ((a.Text || a.Email) ? 1 : 0) });

View File

@ -1,6 +1,6 @@
import { Grid, GridColumn, createElement, setTooltip, createIcon, createCheckbox, createRadiobox, showAlert, showConfirm, Popup } from "../../ui"; import { Grid, GridColumn, createElement, setTooltip, createIcon, createCheckbox, createRadiobox, showAlert, showConfirm, Popup, requestAnimationFrame } from "../../ui";
import { r as lang, nullOrEmpty, formatUrl, escapeEmoji, isEmail, isPhone } from "../../utility"; import { r as lang, nullOrEmpty, formatUrl, escapeEmoji, isEmail, isPhone } from "../../utility";
import { createBox, appendMedia, fileSupported, insertFile, getMessageSendTo, getMessageStatus, updateCustomerName } from "./lib"; import { createBox, appendMedia, fileSupported, insertFile, getMessageSendTo, getMessageStatus, updateCustomerName, createHideMessageTitleButton, createHideMessageCommentTail } from "./lib";
import { Contact, CustomerRecordContact } from "./contact"; import { Contact, CustomerRecordContact } from "./contact";
import Follower from "./follower"; import Follower from "./follower";
@ -41,6 +41,8 @@ export default class CustomerCommunication {
const getText = opt?.getText; const getText = opt?.getText;
if (typeof getText === 'function') { if (typeof getText === 'function') {
r = getText; r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
} }
} }
@ -69,6 +71,20 @@ export default class CustomerCommunication {
element.dispatchEvent(new Event('change')); element.dispatchEvent(new Event('change'));
} }
/**
* @param {boolean} flag
*/
set messageHidden(flag) {
const el = this._var.container.querySelector('.msgadminsetting');
if (el == null) {
return;
}
this._var.option.showMessageHidden = flag;
// TODO: 是否与参数 flag 无关
el.classList.remove('iconview');
el.classList.add('iconnotview');
}
get _statusLink() { return this._var.container.querySelector('.check-status-link>input') } get _statusLink() { return this._var.container.querySelector('.check-status-link>input') }
get statusLinkEnabled() { return this._statusLink?.disabled !== true } get statusLinkEnabled() { return this._statusLink?.disabled !== true }
set statusLinkEnabled(flag) { set statusLinkEnabled(flag) {
@ -199,7 +215,7 @@ export default class CustomerCommunication {
let tipstr; let tipstr;
if (c.OptOut || c.OptOut_BC || c.selected === false) { if (c.OptOut || c.OptOut_BC || c.selected === false) {
icon = 'times'; icon = 'times';
tipstr = r('P_CU_OPTEDOUT_PROMPT', 'User has opted out of messages'); tipstr = r('FLTL_03200', 'User has opted out of messages');
} }
else { else {
switch (pref) { switch (pref) {
@ -208,20 +224,20 @@ export default class CustomerCommunication {
icon = 'times'; icon = 'times';
if (c.MobilePhoneStatus === 412) { if (c.MobilePhoneStatus === 412) {
// landline // landline
tipstr = r('P_CU_LANDLINE', 'Landline'); tipstr = r('FLTL_01707', 'Landline');
} }
} else { } else {
icon = 'comment-lines'; icon = 'comment-lines';
} }
method = r('P_CU_TEXTSTO_COLON', 'Texts to:'); method = r('FLTL_02924', 'Texts to:');
break; break;
case '2': case '2':
icon = 'phone'; icon = 'phone';
tipstr = r('P_CU_NOMESSAGE', 'No Messages Sent'); tipstr = r('FLTL_01991', 'No Messages Sent');
break; break;
default: default:
icon = 'envelope'; icon = 'envelope';
method = r('P_CU_EMAILSTO_COLON', 'Emails to:'); method = r('FLTL_01109', 'Emails to:');
break; break;
} }
} }
@ -238,7 +254,7 @@ export default class CustomerCommunication {
this._var.contacts.appendChild(item); this._var.contacts.appendChild(item);
let tip = tipstr || `${method} ${to}`; let tip = tipstr || `${method} ${to}`;
if (span.scrollWidth > span.offsetWidth) { if (span.scrollWidth > span.offsetWidth) {
tip = r('P_WO_NAME_COLON', 'Name:') + ` ${c.Name}\n${tip}`; tip = r('FLTL_01970', 'Name:') + ` ${c.Name}\n${tip}`;
} }
setTooltip(span, tip); setTooltip(span, tip);
} }
@ -262,6 +278,7 @@ export default class CustomerCommunication {
} }
link.querySelector('input').disabled = flag; link.querySelector('input').disabled = flag;
const display = flag === true ? 'none' : ''; const display = flag === true ? 'none' : '';
this._var.container.querySelector('.title-company').style.display = display;
this._var.container.querySelector('.button-edit-contacts').style.display = display; this._var.container.querySelector('.button-edit-contacts').style.display = display;
this._var.container.querySelector('.button-edit-followers').style.display = display; this._var.container.querySelector('.button-edit-followers').style.display = display;
// this._var.enter.disabled = flag === true; // this._var.enter.disabled = flag === true;
@ -277,8 +294,13 @@ export default class CustomerCommunication {
if (this._var.container == null) { if (this._var.container == null) {
return; return;
} }
this._var.container.querySelector('.button-edit-contacts').style.display = flag === true ? 'none' : ''; const display = flag === true ? 'none' : '';
this._var.container.querySelector('.button-edit-followers').style.display = flag === true ? 'none' : ''; const companySelector = this._var.container.querySelector('.title-company .title-company-selector');
if (companySelector != null) {
companySelector.querySelector('.title-company-selector').style.display = display;
}
this._var.container.querySelector('.button-edit-contacts').style.display = display;
this._var.container.querySelector('.button-edit-followers').style.display = display;
} }
/** /**
@ -287,12 +309,23 @@ export default class CustomerCommunication {
set companyName(name) { set companyName(name) {
this._var.option.companyName = name; this._var.option.companyName = name;
const div = this._var.container.querySelector('.title-company'); const div = this._var.container.querySelector('.title-company');
const companyCode = div.querySelector('.title-company-code');
if (companyCode != null) {
if (nullOrEmpty(name)) { if (nullOrEmpty(name)) {
if (this._var.option.customerReadonly === true) {
div.style.display = 'none'; div.style.display = 'none';
} else { } else {
div.innerText = name; div.querySelector('.title-company-name').innerText = r('FLTL_01985', 'No Customer Assigned');
div.style.display = ''; companyCode.innerText = ' /\n' + r('FLTL_02654', 'Select Customer');
companyCode.style.display = '';
} }
} else {
div.style.display = '';
div.querySelector('.title-company-name').innerText = name;
companyCode.style.display = 'none';
}
}
div.style.display = '';
} }
/** /**
* @param {String} code * @param {String} code
@ -301,15 +334,24 @@ export default class CustomerCommunication {
const option = this._var.option; const option = this._var.option;
option.companyCode = code; option.companyCode = code;
const div = this._var.container.querySelector('.title-company'); const div = this._var.container.querySelector('.title-company');
const companyCode = div.querySelector('.title-company-code');
if (companyCode != null) {
if (nullOrEmpty(option.companyName)) { if (nullOrEmpty(option.companyName)) {
div.style.display = 'none'; div.querySelector('.title-company-name').innerText = r('FLTL_01985', 'No Customer Assigned');
companyCode.innerText = ' /\n' + r('FLTL_02654', 'Select Customer');
companyCode.style.display = '';
} else { } else {
div.innerText = option.companyName; div.querySelector('.title-company-name').innerText = option.companyName;
if (!nullOrEmpty(code)) if (nullOrEmpty(code)) {
div.innerText = option.companyName + "/" + code; companyCode.style.display = 'none';
div.style.display = ''; } else {
companyCode.innerText = ' / ' + code;
companyCode.style.display = '';
} }
} }
}
div.style.display = '';
}
get followers() { get followers() {
return [...this._var.followers.children].map(el => { return [...this._var.followers.children].map(el => {
@ -322,7 +364,7 @@ export default class CustomerCommunication {
this._var.followers.replaceChildren(); this._var.followers.replaceChildren();
if (followers?.length > 0) { if (followers?.length > 0) {
this._var.container.querySelector('.follower-bar').style.display = ''; this._var.container.querySelector('.follower-bar').style.display = '';
setTooltip(this._var.buttonFollower, r('P_CU_EDITFOLLOWERS', 'Edit Followers')); setTooltip(this._var.buttonFollower, r('FLTL_01054', 'Edit Followers'));
this._var.container.querySelector('.follower-bar>.bar-list').appendChild(this._var.buttonFollower); this._var.container.querySelector('.follower-bar>.bar-list').appendChild(this._var.buttonFollower);
for (let f of followers) { for (let f of followers) {
if (f.OptOut) { if (f.OptOut) {
@ -333,10 +375,10 @@ export default class CustomerCommunication {
const email = String(f.Email).trim(); const email = String(f.Email).trim();
const tips = []; const tips = [];
if (f.SendEmail) { if (f.SendEmail) {
tips.push(r('P_CU_EMAILSTO_COLON', 'Emails to:') + ` ${email}`); tips.push(r('FLTL_01109', 'Emails to:') + ` ${email}`);
} }
if (f.SendText) { if (f.SendText) {
tips.push(r('P_CU_TEXTSTO_COLON', 'Texts to:') + ` ${mpDisplay}`); tips.push(r('FLTL_02924', 'Texts to:') + ` ${mpDisplay}`);
} }
let icon; let icon;
if (f.SendText && f.SendEmail) { if (f.SendText && f.SendEmail) {
@ -360,13 +402,13 @@ export default class CustomerCommunication {
); );
this._var.followers.appendChild(item); this._var.followers.appendChild(item);
if (span.scrollWidth > span.offsetWidth) { if (span.scrollWidth > span.offsetWidth) {
tips.splice(0, 0, r('P_WO_NAME_COLON', 'Name:') + ` ${f.Name}`); tips.splice(0, 0, r('FLTL_01970', 'Name:') + ` ${f.Name}`);
} }
setTooltip(span, tips.join('\n')); setTooltip(span, tips.join('\n'));
} }
} else { } else {
this._var.container.querySelector('.follower-bar').style.display = 'none'; this._var.container.querySelector('.follower-bar').style.display = 'none';
setTooltip(this._var.buttonFollower, r('P_CR_ADDFOLLOWERS', 'Add Followers')); setTooltip(this._var.buttonFollower, r('FLTL_00116', 'Add Followers'));
this._var.container.querySelector('.button-edit-contacts').insertAdjacentElement('beforebegin', this._var.buttonFollower) this._var.container.querySelector('.button-edit-contacts').insertAdjacentElement('beforebegin', this._var.buttonFollower)
} }
this._var.message.scrollTop = this._var.message.scrollHeight this._var.message.scrollTop = this._var.message.scrollHeight
@ -387,8 +429,8 @@ export default class CustomerCommunication {
uncheckedNode: createIcon('fa-regular', 'ban'), uncheckedNode: createIcon('fa-regular', 'ban'),
onchange: function () { onchange: function () {
setTooltip(checkAutoUpdate, this.checked ? setTooltip(checkAutoUpdate, this.checked ?
r('P_CU_AUTOUPDATESENABLED', 'Auto Updates Enabled') : r('FLTL_00420', 'Auto Updates Enabled') :
r('P_CU_AUTOUPDATESDISABLED', 'Auto Updates Disabled')); r('FLTL_00419', 'Auto Updates Disabled'));
} }
}); });
if (option.autoUpdatesVisible === false) { if (option.autoUpdatesVisible === false) {
@ -403,8 +445,8 @@ export default class CustomerCommunication {
uncheckedNode: createIcon('fa-regular', 'unlink'), uncheckedNode: createIcon('fa-regular', 'unlink'),
onchange: function () { onchange: function () {
setTooltip(checkLink, this.checked ? setTooltip(checkLink, this.checked ?
r('P_WO_STATUSLINKINCLUDED', 'Status Link Included') : r('FLTL_02830', 'Status Link Included') :
r('P_WO_STATUSLINKEXCLUDED', 'Status Link Excluded')); r('FLTL_02829', 'Status Link Excluded'));
if (typeof option.onStatusLinkChanged === 'function') { if (typeof option.onStatusLinkChanged === 'function') {
option.onStatusLinkChanged.call(This, this.checked); option.onStatusLinkChanged.call(This, this.checked);
} }
@ -413,28 +455,57 @@ export default class CustomerCommunication {
if (option.statusLinkVisible === false) { if (option.statusLinkVisible === false) {
checkLink.style.display = 'none'; checkLink.style.display = 'none';
} }
const container = createBox( const container = createBox(
createElement('div', null, createElement('div', null,
createElement('div', div => { createElement('div', div => {
div.className = 'title-module'; div.className = 'title-module';
div.innerText = option.title ?? r('P_WO_CUSTOMERCOMMUNICATION', 'Customer Communication'); div.innerText = option.title ?? r('FLTL_00732', 'Customer Communication');
}), },
createHideMessageTitleButton(this, 'showMessageHidden')
),
// option.allowCustomer === false ? createElement('div', 'title-company') :
createElement('div', div => { createElement('div', div => {
div.className = 'title-company'; div.className = 'title-company';
// div.style.display = 'none';
},
createElement('span', span => {
span.className = 'title-company-name';
if (nullOrEmpty(option.companyName)) { if (nullOrEmpty(option.companyName)) {
div.style.display = 'none'; span.innerText = r('FLTL_01985', 'No Customer Assigned');
} else { } else {
if (nullOrEmpty(option.companyCode)) { span.innerText = option.companyName;
div.innerText = option.companyName; }
}),
createElement('span', span => {
span.className = 'title-company-code';
if (option.customerReadonly === true) {
span.style.display = 'none';
} else if (nullOrEmpty(option.companyName)) {
span.innerText = ' /\n' + r('FLTL_02654', 'Select Customer');
} else if (!nullOrEmpty(option.companyCode)) {
span.innerText = ' / ' + option.companyCode;
} else { } else {
div.innerText = option.companyName + "/" + option.companyCode; span.style.display = 'none';
} }
}),
createElement('span', span => {
span.className = 'title-company-selector';
if (option.recordReadonly) {
span.style.display = 'none';
} }
}) setTooltip(span, r('FLTL_02654', 'Select Customer'));
if (typeof option.onAddCompany === 'function') {
span.addEventListener('click', () => option.onAddCompany.call(this));
}
},
createIcon('fa-light', 'search')
)
)
), ),
[ [
setTooltip(checkAutoUpdate, r('P_CU_AUTOUPDATESENABLED', 'Auto Updates Enabled')), setTooltip(checkAutoUpdate, r('FLTL_00420', 'Auto Updates Enabled')),
setTooltip(checkLink, r('P_WO_STATUSLINKEXCLUDED', 'Status Link Excluded')) setTooltip(checkLink, r('FLTL_02829', 'Status Link Excluded'))
] ]
); );
// contacts // contacts
@ -443,7 +514,7 @@ export default class CustomerCommunication {
this._var.followers = this._createFollowers(container, option); this._var.followers = this._createFollowers(container, option);
// enter box // enter box
const enter = createElement('textarea', 'ui-text'); const enter = createElement('textarea', 'ui-text');
enter.placeholder = r('P_CU_ENTERMESSAGEHERE', 'Enter Message Here'); enter.placeholder = r('FLTL_01157', 'Enter Message Here');
option.maxLength ??= 3000; option.maxLength ??= 3000;
enter.maxLength = option.maxLength; enter.maxLength = option.maxLength;
// if (readonly === true) { // if (readonly === true) {
@ -515,7 +586,7 @@ export default class CustomerCommunication {
div.style.display = 'none'; div.style.display = 'none';
} }
}, },
createElement('span', span => span.innerText = r('P_WO_NAME_COLON', 'Name:')), createElement('span', span => span.innerText = r('FLTL_01970', 'Name:')),
createElement('input', input => { createElement('input', input => {
input.type = 'text'; input.type = 'text';
input.className = 'ui-input'; input.className = 'ui-input';
@ -560,11 +631,11 @@ export default class CustomerCommunication {
// button.style.display = 'none'; // button.style.display = 'none';
// } // }
button.appendChild(createIcon('fa-solid', 'paper-plane')); button.appendChild(createIcon('fa-solid', 'paper-plane'));
setTooltip(button, r('P_M3_SENDMESSAGE', 'Send Message')); setTooltip(button, r('FLTL_02692', 'Send Message'));
button.addEventListener('click', () => { button.addEventListener('click', () => {
const val = this.text; const val = this.text;
if (nullOrEmpty(val?.trim())) { if (nullOrEmpty(val?.trim())) {
const p = showAlert(r('P_WO_ERROR', 'Error'), r('P_WO_PLEASEINPUTTHEMESSAGE', 'Please input the message.'), 'warn'); const p = showAlert(r('FLTL_01165', 'Error'), r('FLTL_02233', 'Please input the message.'), 'warn');
if (typeof option.onMasking === 'function') { if (typeof option.onMasking === 'function') {
option.onMasking(true); option.onMasking(true);
p.then(() => option.onMasking(false)); p.then(() => option.onMasking(false));
@ -607,8 +678,9 @@ export default class CustomerCommunication {
button.style.display = 'none'; button.style.display = 'none';
} }
button.appendChild(createIcon('fa-solid', 'user-edit')); button.appendChild(createIcon('fa-solid', 'user-edit'));
setTooltip(button, r('P_CU_EDITCONTACTS', 'Edit Contacts')); setTooltip(button, r('FLTL_01043', 'Edit Contacts'));
button.addEventListener('click', () => { button.addEventListener('click', () => {
const editContacts = () => {
const pop = new Popup({ const pop = new Popup({
onMasking: option.onMasking, onMasking: option.onMasking,
title: createElement('div', div => { title: createElement('div', div => {
@ -620,27 +692,34 @@ export default class CustomerCommunication {
div.className = 'ui-popup-move'; div.className = 'ui-popup-move';
div.style.flex = '1 1 auto'; div.style.flex = '1 1 auto';
}, },
createElement('div', div => div.innerText = r('P_CU_EDITCONTACTS', 'Edit Contacts')), createElement('div', div => div.innerText = r('FLTL_01043', 'Edit Contacts')),
createElement('div', div => { createElement('div', div => {
div.className = 'title-company'; div.className = 'title-company';
div.style.maxWidth = '540px'; div.style.maxWidth = '540px';
if (nullOrEmpty(option.companyName)) { if (nullOrEmpty(option.companyName)) {
div.style.display = 'none'; div.style.display = 'none';
} else {
if (nullOrEmpty(option.companyCode)) {
div.innerText = option.companyName;
} else {
div.innerText = option.companyName + "/" + option.companyCode;
} }
},
createElement('span', span => {
span.className = 'title-company-name';
if (!nullOrEmpty(option.companyName)) {
span.innerText = option.companyName;
}
}),
createElement('span', span => {
span.className = 'title-company-code';
if (!nullOrEmpty(option.companyCode)) {
span.innerText = '/' + option.companyCode;
} }
}) })
)
), ),
createElement('button', button => { createElement('button', button => {
button.style.flex = '0 0 auto'; button.style.flex = '0 0 auto';
button.style.backgroundColor = 'rgb(1, 199, 172)'; button.style.backgroundColor = 'rgb(1, 199, 172)';
button.style.marginRight = '10px'; button.style.marginRight = '10px';
button.className = 'roundbtn button-from-customer-record'; button.className = 'roundbtn button-from-customer-record';
if (recordReadonly) { if (recordReadonly || nullOrEmpty(option.companyName)) {
button.style.display = 'none'; button.style.display = 'none';
} }
button.appendChild(createIcon('fa-solid', 'handshake', { button.appendChild(createIcon('fa-solid', 'handshake', {
@ -681,7 +760,7 @@ export default class CustomerCommunication {
}); });
} }
}); });
var title = r('P_CU_SELECTFROMCUSTOMERRECORD', 'Select from Customer Record'); var title = r('FLTL_02657', 'Select from Customer Record');
sel.show(title, container); sel.show(title, container);
if (typeof option.onOpenSelectCRContacts === 'function') { if (typeof option.onOpenSelectCRContacts === 'function') {
@ -717,7 +796,7 @@ export default class CustomerCommunication {
return false; return false;
} }
}); });
setTooltip(button, r('P_CU_SELECTFROMCUSTOMERRECORD', 'Select from Customer Record')) setTooltip(button, r('FLTL_02657', 'Select from Customer Record'))
}), }),
createElement('button', button => { createElement('button', button => {
button.style.flex = '0 0 auto'; button.style.flex = '0 0 auto';
@ -739,7 +818,7 @@ export default class CustomerCommunication {
onSave: item => { onSave: item => {
const exists = this._var.gridContact.source.some(s => s.Name === item.Name && s.MobilePhone === item.MobilePhone); const exists = this._var.gridContact.source.some(s => s.Name === item.Name && s.MobilePhone === item.MobilePhone);
if (exists) { if (exists) {
showAlert(r('P_CR_ADDCONTACT', 'Add Contact'), r('P_WO_CONTACTNAMEANDMOBILEUNIQUECOMBINATION', 'Contact name and contact mobile must be a unique combination.'), 'warn'); showAlert(r('FLTL_00099', 'Add Contact'), r('FLTL_00638', 'Contact name and contact mobile must be a unique combination.'), 'warn');
return false; return false;
} }
if (typeof option.onSave === 'function') { if (typeof option.onSave === 'function') {
@ -773,7 +852,7 @@ export default class CustomerCommunication {
}); });
add.show(container); add.show(container);
}); });
setTooltip(button, r('P_CR_ADDCONTACT', 'Add Contact')) setTooltip(button, r('FLTL_00099', 'Add Contact'))
}) })
) )
}), }),
@ -783,26 +862,26 @@ export default class CustomerCommunication {
div.style.display = 'none'; div.style.display = 'none';
} }
div.style.fontWeight = 'bold'; div.style.fontWeight = 'bold';
div.innerText = r('P_CU_CONTACTSFROMCUSTOMERRECORD', 'Contacts from Customer Record'); div.innerText = r('FLTL_00657', 'Contacts from Customer Record');
}), }),
createElement('div', div => { createElement('div', div => {
if (nullOrEmpty(option.companyName)) { if (nullOrEmpty(option.companyName)) {
div.style.display = 'none'; div.style.display = 'none';
} }
div.className = 'contacts-record'; div.className = 'contacts-record';
div.style.maxHeight = '200px'; // div.style.maxHeight = '200px';
div.style.width = '675px'; div.style.width = '675px';
div.style.overflow = 'auto'; // div.style.overflow = 'auto';
}), }),
createElement('div', div => { createElement('div', div => {
div.style.fontWeight = 'bold'; div.style.fontWeight = 'bold';
div.innerText = r('P_CU_CONTACTSNOTCUSTOMERRECORD', 'Contacts not on Customer Record'); div.innerText = r('FLTL_00659', 'Contacts not on Customer Record');
}), }),
createElement('div', div => { createElement('div', div => {
div.className = 'contacts-wo'; div.className = 'contacts-wo';
div.style.maxHeight = '200px'; // div.style.maxHeight = '200px';
div.style.width = '675px'; div.style.width = '675px';
div.style.overflow = 'auto'; // div.style.overflow = 'auto';
}) })
) )
}); });
@ -818,7 +897,7 @@ export default class CustomerCommunication {
option.onChanged([...This._var.gridContact.source, ...This._var.gridWo.source]); option.onChanged([...This._var.gridContact.source, ...This._var.gridWo.source]);
} }
}, },
tooltip: item => item.selected ? r('P_CU_OPTEDIN', 'Opted In') : r('P_CU_OPTEDOUT', 'Opted Out') tooltip: item => item.selected ? r('FLTL_02090', 'Opted In') : r('FLTL_02091', 'Opted Out')
} }
}; };
const iconCol = { const iconCol = {
@ -848,7 +927,7 @@ export default class CustomerCommunication {
key: 'edit', key: 'edit',
...buttonCol, ...buttonCol,
text: 'edit', text: 'edit',
tooltip: r('P_WO_EDIT', 'Edit'), tooltip: r('FLTL_01032', 'Edit'),
events: { events: {
onclick: function () { onclick: function () {
const edit = new Contact({ const edit = new Contact({
@ -861,7 +940,7 @@ export default class CustomerCommunication {
This._var.gridContact.source.some(s => s !== this && s.Name === item.Name && s.MobilePhone === item.MobilePhone) || This._var.gridContact.source.some(s => s !== this && s.Name === item.Name && s.MobilePhone === item.MobilePhone) ||
This._var.gridWo.source.some(s => s !== this && s.Name === item.Name && s.MobilePhone === item.MobilePhone); This._var.gridWo.source.some(s => s !== this && s.Name === item.Name && s.MobilePhone === item.MobilePhone);
if (exists) { if (exists) {
showAlert(r('P_CR_EDITCONTACT', 'Edit Contact'), r('P_WO_CONTACTNAMEANDMOBILEUNIQUECOMBINATION', 'Contact name and contact mobile must be a unique combination.'), 'warn'); showAlert(r('FLTL_01041', 'Edit Contact'), r('FLTL_00638', 'Contact name and contact mobile must be a unique combination.'), 'warn');
return false; return false;
} }
if (typeof option.onSave === 'function') { if (typeof option.onSave === 'function') {
@ -914,15 +993,15 @@ export default class CustomerCommunication {
key: 'delete', key: 'delete',
...buttonCol, ...buttonCol,
text: 'times', text: 'times',
tooltip: r('P_WO_DELETE', 'Delete'), tooltip: r('FLTL_00791', 'Delete'),
events: { events: {
onclick: function () { onclick: function () {
showConfirm( showConfirm(
r('P_CU_REMOVECONTACT', 'Remove Contact'), r('FLTL_02417', 'Remove Contact'),
createElement('div', null, createElement('div', null,
createElement('div', div => { createElement('div', div => {
div.style.paddingLeft = '16px'; div.style.paddingLeft = '16px';
div.innerText = r('P_CU_REMOVEFROM', 'Remove {name} from').replace('{name}', this.Name); div.innerText = r('FLTL_02412', 'Remove {name} from').replace('{name}', this.Name);
}), }),
createElement('div', div => { createElement('div', div => {
div.style.display = 'flex'; div.style.display = 'flex';
@ -931,25 +1010,25 @@ export default class CustomerCommunication {
}, },
createRadiobox({ createRadiobox({
name: 'remove-type', name: 'remove-type',
label: r('P_CUSTOMERRECORD', 'Customer Record'), label: r('FLTL_00747', 'Customer Record'),
checked: true, checked: true,
className: 'radio-customer-record' className: 'radio-customer-record'
}), }),
createRadiobox({ createRadiobox({
name: 'remove-type', name: 'remove-type',
label: r('P_WORKORDER', 'Work Order') label: r('FLTL_03317', 'Work Order')
}) })
) )
), ),
[ [
{ key: 'ok', text: r('P_WO_OK', 'OK') }, { key: 'ok', text: r('FLTL_02057', 'OK') },
{ key: 'cancel', text: r('P_WO_CANCEL', 'Cancel') } { key: 'cancel', text: r('FLTL_00499', 'Cancel') }
] ]
).then(result => { ).then(r => {
if (result?.key === 'ok') { if (r.result === 'ok') {
const isRecord = result.popup.container.querySelector('.radio-customer-record>input').checked; const isRecord = r.popup.container.querySelector('.radio-customer-record>input').checked;
if (typeof option.onDelete === 'function') { if (typeof option.onDelete === 'function') {
option.onDelete(result.key, this, isRecord); option.onDelete(r.result, this, isRecord);
} }
const index = grid.source.indexOf(this); const index = grid.source.indexOf(this);
if (index >= 0) { if (index >= 0) {
@ -999,16 +1078,16 @@ export default class CustomerCommunication {
key: 'delete', key: 'delete',
...buttonCol, ...buttonCol,
text: 'times', text: 'times',
tooltip: r('P_WO_DELETE', 'Delete'), tooltip: r('FLTL_00791', 'Delete'),
events: { events: {
onclick: function () { onclick: function () {
showConfirm(r('P_CU_REMOVECONTACT', 'Remove Contact'), r('P_CU_REMOVEFROMWORKORDER', 'You are removing {name} from work order.\n\nDo you want to Continue?').replace('{name}', this.Name), [ showConfirm(r('FLTL_02417', 'Remove Contact'), r('FLTL_03382', 'You are removing {name} from work order.\n\nDo you want to Continue?').replace('{name}', this.Name), [
{ key: 'continue', text: r('P_JS_CONTINUE', 'Continue') }, { key: 'continue', text: r('FLTL_00661', 'Continue') },
{ key: 'cancel', text: r('P_WO_CANCEL', 'Cancel') } { key: 'cancel', text: r('FLTL_00499', 'Cancel') }
]).then(result => { ]).then(r => {
if (result?.key === 'continue') { if (r.result === 'continue') {
if (typeof option.onDelete === 'function') { if (typeof option.onDelete === 'function') {
option.onDelete(result.key, this); option.onDelete(r.result, this);
} }
const index = gridWo.source.indexOf(this); const index = gridWo.source.indexOf(this);
if (index >= 0) { if (index >= 0) {
@ -1042,12 +1121,33 @@ export default class CustomerCommunication {
}; };
this._var.gridWo = gridWo; this._var.gridWo = gridWo;
}); });
};
if (nullOrEmpty(option.companyName)) {
showConfirm(
r('FLTL_01043', 'Edit Contacts'),
r('FLTL_03008', 'There is no company associated with this work order. Would you like to add one?\n\nIf no company is indicated, contacts must be added as "Work Order Only".'),
[
{ key: 'add', text: r('FLTL_00098', 'Add Company') },
{ key: 'skip', text: r('FLTL_02777', 'Skip This Step') }
]
).then(r => {
if (r.result === 'add') {
if (typeof option.onAddCompany === 'function') {
option.onAddCompany.call(this);
}
} else if (r.result === 'skip') {
editContacts();
}
});
} else {
editContacts();
}
}); });
}) })
), ),
createElement('div', div => { createElement('div', div => {
div.className = 'bar-info'; div.className = 'bar-info';
div.innerText = r('P_CR_CONTACTINFORMATION', 'Contact Information'); div.innerText = r('FLTL_00635', 'Contact Information');
}), }),
createElement('div', div => { createElement('div', div => {
if (option.contactCollapserVisible === false) { if (option.contactCollapserVisible === false) {
@ -1091,12 +1191,12 @@ export default class CustomerCommunication {
button.style.display = 'none'; button.style.display = 'none';
} }
button.appendChild(createIcon('fa-solid', 'pen')); button.appendChild(createIcon('fa-solid', 'pen'));
setTooltip(button, r('P_CU_EDITFOLLOWERS', 'Edit Followers')); setTooltip(button, r('FLTL_01054', 'Edit Followers'));
button.addEventListener('click', () => { button.addEventListener('click', () => {
if (typeof option.onInitFollower === 'function') { if (typeof option.onInitFollower === 'function') {
option.onInitFollower(this._var.data.followers).then(data => { option.onInitFollower(this._var.data.followers).then(data => {
if (typeof data === 'string') { if (typeof data === 'string') {
showAlert(r('P_CUSTOMERRECORD', 'Customer Record'), data, 'warn'); showAlert(r('FLTL_00747', 'Customer Record'), data, 'warn');
return; return;
} }
const add = new Follower({ const add = new Follower({
@ -1116,7 +1216,7 @@ export default class CustomerCommunication {
} }
} }
}); });
var title = this._var.data.followers?.length > 0 ? r('P_CU_EDITFOLLOWERS', 'Edit Followers') : r('P_CR_ADDFOLLOWERS', 'Add Followers'); var title = this._var.data.followers?.length > 0 ? r('FLTL_01054', 'Edit Followers') : r('FLTL_00116', 'Add Followers');
add.show(title, container); add.show(title, container);
}); });
} }
@ -1136,7 +1236,7 @@ export default class CustomerCommunication {
'border-radius': '15px', 'border-radius': '15px',
'padding': '4px' 'padding': '4px'
}) })
), r('P_CU_COPIED', 'Copied')), ), r('FLTL_00667', 'Copied')),
createElement('div', 'bar-list', createElement('div', 'bar-list',
followers, followers,
buttonEditFollower buttonEditFollower
@ -1146,7 +1246,7 @@ export default class CustomerCommunication {
return followers; return followers;
} }
load(data, contacts, followers) { load(data, contacts, followers, func, hisFunc, keep) {
const children = []; const children = [];
if (data?.length > 0) { if (data?.length > 0) {
contacts ??= this._var.data.contacts; contacts ??= this._var.data.contacts;
@ -1159,45 +1259,52 @@ export default class CustomerCommunication {
this._var.contactsUpdated = true; this._var.contactsUpdated = true;
} }
} }
for (let comm of data) { const lastVisible = this._var.option.showMessageHidden;
for (let comment of data) {
const div = createElement('div', 'item-div'); const div = createElement('div', 'item-div');
if (comment.Hidden) {
div.classList.add('hidden-content');
if (!lastVisible) {
div.style.display = 'none';
}
}
let name; let name;
if (comm.IsReply && contacts?.length > 0) { if (comment.IsReply && contacts?.length > 0) {
const c = isEmail(comm.Sender) ? const c = isEmail(comment.Sender) ?
contacts.find(c => c.Email === comm.Sender) : contacts.find(c => c.Email === comment.Sender) :
contacts.find(c => c.MobilePhone === comm.Sender); contacts.find(c => c.MobilePhone === comment.Sender);
name = c?.Name; name = c?.Name;
} }
name ??= comm.IsReply && String(comm.FormatSender) !== '' ? comm.FormatSender : comm.Sender; name ??= comment.IsReply && String(comment.FormatSender) !== '' ? comment.FormatSender : comment.Sender;
const sendto = getMessageSendTo(comm, contacts, followers, r) const sendto = getMessageSendTo(comment, contacts, followers, r)
div.appendChild(createElement('div', div => { div.appendChild(createElement('div', div => {
div.className = 'item-poster'; div.className = 'item-poster';
div.innerText = name; div.innerText = name;
if (!comm.IsReply && sendto?.length > 0) { if (!comment.IsReply && sendto?.length > 0) {
setTooltip(div, sendto); setTooltip(div, sendto);
} }
})); }));
const content = createElement('div', 'item-content'); const content = createElement('div', 'item-content');
const mmsParts = createElement('div', div => div.style.display = 'none'); const mmsParts = createElement('div', div => div.style.display = 'none');
content.appendChild(createElement('span', span => { content.appendChild(createElement('span', span => {
if (/https?:\/\//i.test(comm.Message)) { if (/https?:\/\//i.test(comment.Message)) {
span.innerHTML = formatUrl(escapeEmoji(comm.Message)); span.innerHTML = formatUrl(escapeEmoji(comment.Message));
} else { } else {
span.innerText = escapeEmoji(comm.Message); span.innerText = escapeEmoji(comment.Message);
} }
span.appendChild(mmsParts); span.appendChild(mmsParts);
})); }));
if (comm.MMSParts?.length > 0) { if (comment.MMSParts?.length > 0) {
mmsParts.style.display = ''; mmsParts.style.display = '';
for (let kv of comm.MMSParts) { for (let kv of comment.MMSParts) {
appendMedia(mmsParts, kv.Key, kv.Value); appendMedia(mmsParts, kv.Key, kv.Value);
} }
} }
if (comm.IsReply) { if (comment.IsReply) {
div.classList.add('item-other'); div.classList.add('item-other');
} else { } else {
div.classList.add('item-self'); div.classList.add('item-self');
const [status, text, color, tips] = getMessageStatus(comm, r, this._var); const [status, text, color, tips] = getMessageStatus(comment, r, this._var);
if (status !== -100) { if (status !== -100) {
if (color != null) { if (color != null) {
content.style.backgroundColor = color; content.style.backgroundColor = color;
@ -1214,17 +1321,19 @@ export default class CustomerCommunication {
} }
div.append( div.append(
content, content,
createElement('div', div => { createHideMessageCommentTail(
div.className = 'item-time'; this, 'showMessageHidden',
div.innerText = comm.TimeStr; comment, 'TimeStr',
}) func, hisFunc)
); );
children.push(div); children.push(div);
} }
children[0].style.marginTop = '0'; children[0].style.marginTop = '0';
} }
if (this._var.message.children.length > 0) {
this._var.lastTop = this._var.message.scrollTop;
}
this._var.message.replaceChildren(...children); this._var.message.replaceChildren(...children);
this._var.message.scrollTop = this._var.message.scrollHeight requestAnimationFrame(() => this._var.message.scrollTop = keep ? this._var.lastTop : this._var.message.scrollHeight);
// setTimeout(() => this._var.message.scrollTop = this._var.message.scrollHeight, 0);
} }
} }

View File

@ -11,6 +11,8 @@ export default class Follower {
const getText = option?.getText; const getText = option?.getText;
if (typeof getText === 'function') { if (typeof getText === 'function') {
r = getText; r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
} }
} }
@ -22,7 +24,7 @@ export default class Follower {
onMasking: this._var.option.onMasking, onMasking: this._var.option.onMasking,
title, title,
content: createElement('div', 'follower-wrapper', content: createElement('div', 'follower-wrapper',
createElement('div', div => div.innerText = r('P_CR_WHODOYOUWANTTORECEIVECUSTOMERNOTIFICATIONS', 'Who do you want to receive customer notifications?')), createElement('div', div => div.innerText = r('FLTL_03300', 'Who do you want to receive customer notifications?')),
createElement('input', search => { createElement('input', search => {
search.type = 'text'; search.type = 'text';
search.tabIndex = tabIndex + 3; search.tabIndex = tabIndex + 3;
@ -41,7 +43,7 @@ export default class Follower {
), ),
buttons: [ buttons: [
{ {
text: r('P_WO_OK', 'OK'), text: r('FLTL_02057', 'OK'),
key: 'ok', key: 'ok',
trigger: () => { trigger: () => {
if (typeof this._var.option.onOk === 'function') { if (typeof this._var.option.onOk === 'function') {
@ -49,7 +51,7 @@ export default class Follower {
} }
} }
}, },
{ text: r('P_WO_CANCEL', 'Cancel'), key: 'cancel' } { text: r('FLTL_00499', 'Cancel'), key: 'cancel' }
] ]
}); });
const result = await popup.show(parent); const result = await popup.show(parent);
@ -57,18 +59,18 @@ export default class Follower {
// grid // grid
const grid = new Grid(gridContainer); const grid = new Grid(gridContainer);
grid.columns = [ grid.columns = [
{ key: 'DisplayName', caption: r('P_WO_CONTACTNAME', 'Contact Name'), width: 240 }, { key: 'DisplayName', caption: r('FLTL_00637', 'Contact Name'), width: 240 },
{ key: 'ContactTypeName', caption: r('P_WO_CONTACTTYPE', 'Contact Type'), width: 120 }, { key: 'ContactTypeName', caption: r('FLTL_00644', 'Contact Type'), width: 120 },
{ {
key: 'Text', key: 'Text',
caption: r('P_CR_TEXT', 'Text'), caption: r('FLTL_02915', 'Text'),
type: Grid.ColumnTypes.Checkbox, type: Grid.ColumnTypes.Checkbox,
width: 60, width: 60,
enabled: item => !nullOrEmpty(item.Mobile) enabled: item => !nullOrEmpty(item.Mobile)
}, },
{ {
key: 'Email', key: 'Email',
caption: r('P_CR_EMAIL', 'Email'), caption: r('FLTL_01089', 'Email'),
type: Grid.ColumnTypes.Checkbox, type: Grid.ColumnTypes.Checkbox,
width: 70, width: 70,
// enabled: item => !nullOrEmpty(item.ID) // enabled: item => !nullOrEmpty(item.ID)

View File

@ -1,7 +1,8 @@
import { createElement, setTooltip, createIcon } from "../../ui"; import AddWorkOrder from "../../element/addWorkorder";
import { createElement, setTooltip, createIcon, requestAnimationFrame } from "../../ui";
import { r as lang, nullOrEmpty, escapeHtml, escapeEmoji } from "../../utility"; import { r as lang, nullOrEmpty, escapeHtml, escapeEmoji } from "../../utility";
import { createBox, appendMedia } from "./lib"; import { createBox, appendMedia } from "./lib";
import { fileSupported, insertFile, getMessageSendTo, getMessageStatus, updateCustomerName } from "./lib"; import { fileSupported, insertFile, getMessageSendTo, getMessageStatus, updateCustomerName, createHideMessageTitleButton, createHideMessageCommentTail } from "./lib";
let r = lang; let r = lang;
@ -19,6 +20,8 @@ export default class InternalComment {
const getText = opt?.getText; const getText = opt?.getText;
if (typeof getText === 'function') { if (typeof getText === 'function') {
r = getText; r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
} }
} }
@ -32,6 +35,20 @@ export default class InternalComment {
} }
} }
/**
* @param {boolean} flag
*/
set messageHidden(flag) {
const el = this._var.container.querySelector('.msgadminsetting');
if (el == null) {
return;
}
this._var.option.showMessageHidden = flag;
// TODO: 是否与参数 flag 无关
el.classList.remove('iconview');
el.classList.add('iconnotview');
}
/** /**
* @param {any} contacts * @param {any} contacts
*/ */
@ -99,24 +116,38 @@ export default class InternalComment {
return; return;
} }
this._var.enter.disabled = flag === true; this._var.enter.disabled = flag === true;
this._var.container.querySelector('.button-call-log').style.display = flag === true ? 'none' : '';
this._var.container.querySelector('.button-send-message').style.display = flag === true ? 'none' : ''; this._var.container.querySelector('.button-send-message').style.display = flag === true ? 'none' : '';
this._var.container.querySelector('.button-post-note').style.display = flag === true ? 'none' : ''; this._var.container.querySelector('.button-post-note').style.display = flag === true ? 'none' : '';
} }
/**
* @param {boolean} flag
*/
set noCallLog(flag) {
this._var.option.noCallLog = flag;
if (this._var.container == null) {
return;
}
this._var.container.querySelector('.button-call-log').style.display = flag === true ? 'none' : '';
}
create() { create() {
const option = this._var.option; const option = this._var.option;
const container = createBox( const container = createBox(
createElement('div', null, createElement('div', null,
createElement('div', div => { createElement('div', div => {
div.className = 'title-module'; div.className = 'title-module';
div.innerText = r('P_WO_INTERNALCOMMENTS', 'Internal Comments'); div.innerText = r('FLTL_01613', 'Internal Comments');
}) },
createHideMessageTitleButton(this, 'showMessageHidden')
)
), [] ), []
); );
const readonly = option.readonly; const readonly = option.readonly;
// enter box // enter box
const enter = createElement('textarea', 'ui-text'); const enter = createElement('textarea', 'ui-text');
enter.placeholder = r('P_CU_ENTERCOMMENTHERE', 'Enter Comment Here'); enter.placeholder = r('FLTL_01154', 'Enter Comment Here');
enter.maxLength = option.maxLength ??= 3000; enter.maxLength = option.maxLength ??= 3000;
enter.addEventListener('input', () => { enter.addEventListener('input', () => {
const val = this.text; const val = this.text;
@ -196,6 +227,24 @@ export default class InternalComment {
) )
), ),
createElement('div', 'prompt-count'), createElement('div', 'prompt-count'),
createElement('button', button => {
button.className = 'roundbtn button-call-log';
button.style.backgroundImage = 'url(' + AddWorkOrder.IconWorkOrder + ')';
button.style.backgroundSize = '80% 80%';
button.style.backgroundPosition = 'center';
button.style.backgroundRepeat = 'no-repeat';
button.style.verticalAlign = 'top';//firefox图片需要设置verticalAlign
if (readonly === true || option.noCallLog === true) {
button.style.display = 'none';
}
setTooltip(button, r('FLTL_00491', 'Call Log'));
button.addEventListener('click', () => {
if (typeof option.onAddCallLog === 'function') {
this.loading = true;
option.onAddCallLog();
}
})
}),
createElement('button', button => { createElement('button', button => {
button.className = 'roundbtn button-send-message'; button.className = 'roundbtn button-send-message';
button.style.backgroundColor = 'rgb(19, 150, 204)'; button.style.backgroundColor = 'rgb(19, 150, 204)';
@ -203,7 +252,7 @@ export default class InternalComment {
button.style.display = 'none'; button.style.display = 'none';
} }
button.appendChild(createIcon('fa-solid', 'paper-plane')); button.appendChild(createIcon('fa-solid', 'paper-plane'));
setTooltip(button, r('P_M3_SENDMESSAGE', 'Send Message')); setTooltip(button, r('FLTL_02692', 'Send Message'));
button.addEventListener('click', () => { button.addEventListener('click', () => {
const val = this.text; const val = this.text;
if (nullOrEmpty(val?.trim())) { if (nullOrEmpty(val?.trim())) {
@ -223,7 +272,7 @@ export default class InternalComment {
button.style.display = 'none'; button.style.display = 'none';
} }
button.appendChild(createIcon('fa-solid', 'comment-alt-lines')); button.appendChild(createIcon('fa-solid', 'comment-alt-lines'));
setTooltip(button, r('P_CU_POSTNOTE', 'Post Note')); setTooltip(button, r('FLTL_02301', 'Post Note'));
button.addEventListener('click', () => { button.addEventListener('click', () => {
const val = this.text; const val = this.text;
if (nullOrEmpty(val?.trim())) { if (nullOrEmpty(val?.trim())) {
@ -245,7 +294,7 @@ export default class InternalComment {
return this._var.container = container; return this._var.container = container;
} }
load(data) { load(data, func, hisFunc, keep) {
const children = []; const children = [];
if (data?.length > 0) { if (data?.length > 0) {
this._var.comments = data; this._var.comments = data;
@ -256,8 +305,15 @@ export default class InternalComment {
this._var.contactsUpdated = true; this._var.contactsUpdated = true;
} }
} }
const lastVisible = this._var.option.showMessageHidden;
for (let comment of data) { for (let comment of data) {
const div = createElement('div', 'item-div'); const div = createElement('div', 'item-div');
if (comment.Hidden) {
div.classList.add('hidden-content');
if (!lastVisible) {
div.style.display = 'none';
}
}
const sendto = getMessageSendTo(comment, null, null, r) const sendto = getMessageSendTo(comment, null, null, r)
div.appendChild(createElement('div', div => { div.appendChild(createElement('div', div => {
div.className = 'item-poster'; div.className = 'item-poster';
@ -268,7 +324,13 @@ export default class InternalComment {
})); }));
const content = createElement('div', 'item-content'); const content = createElement('div', 'item-content');
const mmsParts = createElement('div', div => div.style.display = 'none'); const mmsParts = createElement('div', div => div.style.display = 'none');
content.appendChild(createElement('span', span => span.innerHTML = escapeHtml(escapeEmoji(comment.Message)), mmsParts)); content.appendChild(createElement('span', span => {
if (comment.MessageType === 2) {
span.innerHTML = comment.Message;
} else {
span.innerHTML = escapeHtml(escapeEmoji(comment.Message));
}
}, mmsParts));
if (comment.MMSParts?.length > 0) { if (comment.MMSParts?.length > 0) {
mmsParts.style.display = ''; mmsParts.style.display = '';
for (let kv of comment.MMSParts) { for (let kv of comment.MMSParts) {
@ -277,10 +339,10 @@ export default class InternalComment {
} }
// if (comment.FollowUp?.length > 0) { // if (comment.FollowUp?.length > 0) {
// div.classList.add('item-sent'); // div.classList.add('item-sent');
// const sendto = r('P_CU_SENDTO_COLON', 'Sent To :') + '\r\n' + comment.FollowUp.split(';').join('\r\n'); // const sendto = r('FLTL_02716', 'Sent To :') + '\r\n' + comment.FollowUp.split(';').join('\r\n');
// content.appendChild(createElement('div', div => { // content.appendChild(createElement('div', div => {
// div.className = 'item-status'; // div.className = 'item-status';
// div.innerText = r('P_WO_SENT', 'Sent'); // div.innerText = r('FLTL_02711', 'Sent');
// setTooltip(div, sendto); // setTooltip(div, sendto);
// })); // }));
// } // }
@ -300,17 +362,19 @@ export default class InternalComment {
} }
div.append( div.append(
content, content,
createElement('div', div => { createHideMessageCommentTail(
div.className = 'item-time'; this, 'showMessageHidden',
div.innerText = comment.TimeStr; comment, 'TimeStr',
}) func, hisFunc)
); );
children.push(div); children.push(div);
} }
children[0].style.marginTop = '0'; children[0].style.marginTop = '0';
} }
if (this._var.message.children.length > 0) {
this._var.lastTop = this._var.message.scrollTop;
}
this._var.message.replaceChildren(...children); this._var.message.replaceChildren(...children);
this._var.message.scrollTop = this._var.message.scrollHeight requestAnimationFrame(() => this._var.message.scrollTop = keep ? this._var.lastTop : this._var.message.scrollHeight);
// setTimeout(() => this._var.message.scrollTop = this._var.message.scrollHeight, 0);
} }
} }

View File

@ -155,12 +155,12 @@ export function insertFile(container, file, r) {
type = type.substring(type.lastIndexOf('.')); type = type.substring(type.lastIndexOf('.'));
} }
if (fileSupported.indexOf(type) < 0) { if (fileSupported.indexOf(type) < 0) {
showAlert(r('P_WO_ERROR', 'Error'), r('P_CU_TYPENOTSUPPORTED', 'File type "{type}" is now not supported.').replace('{type}', type)); showAlert(r('FLTL_01165', 'Error'), r('FLTL_01385', 'File type "{type}" is now not supported.').replace('{type}', type));
return; return;
} }
const isImage = /^image\//.test(type); const isImage = /^image\//.test(type);
if (!isImage && file.size > MaxAttachmentSize.limit) { if (!isImage && file.size > MaxAttachmentSize.limit) {
showAlert(r('P_WO_ERROR', 'Error'), r('P_WO_ATTACHMENTSIZEEXCEEDSTHEMAXIMUMTIPS', `Attachment size exceeds the maximum allowed to be sent (${MaxAttachmentSize.text})`), 'warn'); showAlert(r('FLTL_01165', 'Error'), r('FLTL_00407', `Attachment size exceeds the maximum allowed to be sent (${MaxAttachmentSize.text})`), 'warn');
return; return;
} }
const fn = file.name; const fn = file.name;
@ -202,12 +202,12 @@ function getStatusText(status, dict) {
export function getMessageStatus(comm, r, _var) { export function getMessageStatus(comm, r, _var) {
const messageStatus = { const messageStatus = {
0: r('P_CU_PENDING', 'Pending'), 0: r('FLTL_02186', 'Pending'),
1: r('P_WO_SENT', 'Sent'), 1: r('FLTL_02711', 'Sent'),
5: r('P_CU_DELIVERYCONFIRMED', 'Delivery Confirmed'), 5: r('FLTL_00864', 'Delivery Confirmed'),
6: r('P_CU_RESENT', 'Resent'), 6: r('FLTL_02478', 'Resent'),
9: r('P_MA_FAILED', 'Failed'), 9: r('FLTL_01224', 'Failed'),
9999: r('P_CU_UNKNOWN', 'Unknown') 9999: r('FLTL_03152', 'Unknown')
}; };
const knownStatus = [0, 1, 5, 6, 9, 10, 412]; const knownStatus = [0, 1, 5, 6, 9, 10, 412];
const okStatus = [1, 5, 6]; const okStatus = [1, 5, 6];
@ -269,7 +269,7 @@ export function getMessageStatus(comm, r, _var) {
if (statusUpdatable !== false) { if (statusUpdatable !== false) {
tip.appendChild(createElement('div', b => { tip.appendChild(createElement('div', b => {
b.className = 'tip-function-button'; b.className = 'tip-function-button';
// setTooltip(b, r('P_CU_UPDATESTATUS', 'Update Status')); // setTooltip(b, r('FLTL_03174', 'Update Status'));
b.addEventListener('click', async () => { b.addEventListener('click', async () => {
for (let p of comm.Participator) { for (let p of comm.Participator) {
switch (p.Status) { switch (p.Status) {
@ -292,7 +292,7 @@ export function getMessageStatus(comm, r, _var) {
const gridContainer = createElement('div', 'status-grid'); const gridContainer = createElement('div', 'status-grid');
const popup = new Popup({ const popup = new Popup({
onMasking: _var.option.onMasking, onMasking: _var.option.onMasking,
title: r('P_CU_UPDATESTATUS', 'Update Status'), title: r('FLTL_03174', 'Update Status'),
content: createElement('div', wrapper => { content: createElement('div', wrapper => {
wrapper.className = 'update-status-wrapper'; wrapper.className = 'update-status-wrapper';
wrapper.style.width = '500px'; wrapper.style.width = '500px';
@ -301,7 +301,7 @@ export function getMessageStatus(comm, r, _var) {
), ),
buttons: [ buttons: [
{ {
text: r('P_WO_OK', 'OK'), text: r('FLTL_02057', 'OK'),
key: 'ok', key: 'ok',
trigger: () => { trigger: () => {
const changed = msgs.filter(m => { const changed = msgs.filter(m => {
@ -329,7 +329,7 @@ export function getMessageStatus(comm, r, _var) {
} }
}, },
{ {
text: r('P_WO_CANCEL', 'Cancel'), text: r('FLTL_00499', 'Cancel'),
key: 'cancel' key: 'cancel'
} }
] ]
@ -341,22 +341,22 @@ export function getMessageStatus(comm, r, _var) {
grid.columns = [ grid.columns = [
{ {
key: 'CustomerNumber', key: 'CustomerNumber',
caption: r('P_JS_NUMBER', 'Number'), caption: r('FLTL_02026', 'Number'),
width: 150 width: 150
}, },
/*{ /*{
key: 'customerName', key: 'customerName',
caption: r('P_WOS_CUSTOMERNAME', 'Customer Name'), caption: r('FLTL_00742', 'Customer Name'),
width: 120 width: 120
},*/ },*/
{ {
key: 'statusText', key: 'statusText',
caption: r('P_CU_CURRENTSTATUS', 'Current Status'), caption: r('FLTL_00725', 'Current Status'),
width: 155 width: 155
}, },
{ {
key: 'statusChanged', key: 'statusChanged',
caption: r('P_CU_REVISEDSTATUS', 'Revised Status'), caption: r('FLTL_02511', 'Revised Status'),
width: 155, width: 155,
type: Grid.ColumnTypes.Dropdown, type: Grid.ColumnTypes.Dropdown,
source: [ source: [
@ -404,7 +404,7 @@ export function getMessageSendTo(comm, contacts, followers, r) {
} }
} }
if (sendto !== '') { if (sendto !== '') {
sendto = r('P_CU_SENDTO_COLON', 'Sent to :') + `\n${sendto}`; sendto = r('FLTL_02716', 'Sent to :') + `\n${sendto}`;
} }
return sendto; return sendto;
} }
@ -421,3 +421,98 @@ export function updateCustomerName(messages, contacts) {
} }
} }
} }
export function createHideMessageTitleButton(This, optionName) {
const option = This._var.option;
return createElement('span', span => {
if (option.userIsAdmin) {
if (option[optionName]) {
span.className = 'msgadminsetting sbutton iconview';
} else {
span.className = 'msgadminsetting sbutton iconnotview';
}
span.style.padding = '0px 0px 0px 5px';
setTooltip(span, option?.getText('FLTL_01860', 'Manage Messages'));
span.addEventListener('click', function () {
const container = This._var.container;
if (!option[optionName]) {
this.classList.remove('iconnotview');
this.classList.add('iconview');
option[optionName] = true;
container.querySelectorAll('.msgsetting').forEach(x => x.style.display = '');
container.querySelectorAll('.msgHistory').forEach(h => h.style.display = h.getAttribute('ModifyCount') > 0 ? '' : 'none');
container.querySelectorAll('.hidden-content').forEach(c => c.style.display = '');
} else {
this.classList.remove('iconview');
this.classList.add('iconnotview');
option[optionName] = false;
container.querySelectorAll('.msgsetting').forEach(x => x.style.display = 'none');
container.querySelectorAll('.msgHistory').forEach(h => h.style.display = 'none');
container.querySelectorAll('.hidden-content').forEach(c => c.style.display = 'none');
}
});
}
});
}
export function createHideMessageCommentTail(This, optionName, comment, commentTime, func, hisFunc) {
const option = This._var.option;
const showTooltip = option?.getText('FLTL_03267', 'Visible');
const notShowTooltip = option?.getText('FLTL_02006', 'Not Visible');
return createElement('div', div => {
div.className = 'item-time';
div.style.display = 'flex';
div.style.alignItems = 'center';
},
createElement('span', span => {
span.className = 'msgsetting sbutton ' + (comment.Hidden ? 'iconnotview' : 'iconview');
span.style.padding = '0';
span.style.fontSize = '12px';
setTooltip(span, comment.Hidden ? notShowTooltip : showTooltip);
span.style.display = option[optionName] ? '' : 'none';
span.addEventListener('click', function () {
if (this.classList.contains('iconview')) {
func(comment.Id, true);
this.classList.remove('iconview');
this.classList.add('iconnotview');
setTooltip(this, notShowTooltip);
} else {
func(comment.Id, false);
this.classList.remove('iconnotview');
this.classList.add('iconview');
setTooltip(this, showTooltip);
}
if (isNaN(comment.ModifyCount)) {
comment.ModifyCount = 1;
} else {
comment.ModifyCount += 1;
}
const x = This._var.container.querySelector('.history-span-' + comment.Id);
if (x != null) {
x.setAttribute('ModifyCount', comment.ModifyCount);
x.style.display = (option[optionName] && comment.ModifyCount > 0) ? '' : 'none';
}
});
}),
createElement('span', span => {
span.className = 'msgHistory history-span-' + comment.Id;
span.setAttribute('ModifyCount', comment.ModifyCount ?? 0);
span.style.display = (option[optionName] && comment.ModifyCount > 0) ? '' : 'none';
setTooltip(span, option?.getText('FLTL_01508', 'Hidden History'));
const icon = createIcon('fa-light', 'wave-sine');
icon.style.height = '12px';
icon.style.width = '12px';
icon.style.margin = '0 5px 0 0';
icon.style.cursor = 'pointer';
icon.style.border = '1px solid';
icon.style.borderRadius = '6px';
icon.style.borderColor = '#000';
icon.style.display = 'block';
span.appendChild(icon);
span.addEventListener('click', () => hisFunc(comment.Id));
}),
createElement('span', span => {
span.innerText = comment[commentTime];
})
);
}

View File

@ -16,6 +16,7 @@
>.ui-grid { >.ui-grid {
overflow-x: visible; overflow-x: visible;
max-height: 200px;
} }
} }
} }
@ -89,12 +90,39 @@
>div { >div {
flex: 1 1 auto; flex: 1 1 auto;
>.title-company {
line-height: 1rem;
padding: 2px 10px;
// background-color: rgba(0, 0, 0, .15);
>.title-company-name {
font-weight: bold;
}
>.title-company-selector {
cursor: pointer;
vertical-align: middle;
&:hover {
background-color: #ccc;
}
>svg {
width: 14px;
height: 14px;
fill: rgb(123, 28, 33);
margin: 0 5px 3px;
vertical-align: middle;
}
}
}
} }
>.title-functions { >.title-functions {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
padding: 4px; padding: 0 4px;
>label { >label {
margin: 0 4px; margin: 0 4px;

View File

@ -1,6 +1,12 @@
import "./element/style.scss"; import "./element/style.scss";
import ScheduleItem from "./element/schedule"; import ScheduleItem from "./element/schedule";
import AddWorkOrder from "./element/addWorkorder";
import InspectionWizard from "./element/inspectionWizard";
import Signature from "./element/signature";
export { export {
ScheduleItem ScheduleItem,
AddWorkOrder,
InspectionWizard,
Signature
} }

734
lib/element/addworkorder.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,260 @@
import { Dropdown, Grid, OptionBase, createCheckbox, createElement, createIcon } from "../ui";
export default class AssetSelector extends OptionBase {
_var = {
/**
* @private
* @type {HTMLElement}
*/
container: null,
el: {
/**
* @private
* @type {HTMLInputElement}
*/
inputSearch: null,
/**
* @private
* @type {HTMLLabelElement}
*/
checkHidden: null,
/**
* @private
* @type {Dropdown}
*/
dropAssetGroup: null,
/**
* @private
* @type {Dropdown}
*/
dropJobsite: null,
/**
* @private
* @type {Dropdown}
*/
dropJobsiteCode: null,
/**
* @private
* @type {Grid}
*/
grid: null
}
}
onSelected;
constructor(opt = {
assetFullcontrol: false,
ignoreHidden: false,
/**
* @private
* @type {(flag: boolean) => void}
*/
loading: null,
/**
* @private
* @type {() => void}
*/
close: null,
/**
* @private
* @type {(hidden: boolean, search?: string, groups?: string[], jobsites?: number[], codes?: string[]) => Promise<any[]>}
*/
requestAssets: null,
/**
* @private
* @type {() => Promise<any>}
*/
requestAddAsset: null,
/**
* @private
* @type {() => Promise<{AssetGroups: any[], Codes: any[], Jobsites: any[]}>}
*/
requestAssetParams: null,
dataSource: {
AssetGroups: [],
Codes: [],
Jobsites: []
}
}) {
super(opt);
}
get title() { return this.r('FLTL_02645', 'Select Asset') }
get currentAsset() { return this._var.el.grid.currentItem }
loading(flag) {
if (typeof this._option.loading === 'function') {
this._option.loading(flag);
}
}
refresh() {
const requestAssets = this._option.requestAssets;
if (typeof requestAssets !== 'function') {
return;
}
const el = this._var.el;
this.loading(true);
requestAssets(
el.checkHidden.querySelector('input[type="checkbox"]')?.checked,
el.inputSearch.value,
el.dropAssetGroup.selected?.Key,
el.dropJobsite.selected?.ID,
el.dropJobsiteCode.selected?.value
).then(data => {
el.grid.source = data;
}).finally(() => this.loading(false));
}
select(item) {
if (typeof this.onSelected !== 'function') {
return false;
}
if (item == null) {
item = this._var.el.grid.currentItem;
}
if (item == null) {
return false;
}
this.onSelected(item);
}
create() {
const option = this._option;
const tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)) + 3;
const inputSearch = createElement('input', input => {
input.type = 'text';
input.placeholder = this.r('FLTL_02606', 'Search');
input.tabIndex = tabIndex + 2;
input.className = 'ui-input';
input.addEventListener('keypress', e => {
if (e.key === 'Enter') {
this.refresh();
}
})
});
const checkHidden = createCheckbox({
tabIndex: tabIndex + 3,
label: this.r('FLTL_02768', 'Show Hidden'),
onchange: () => this.refresh()
});
if (option.ignoreHidden) {
checkHidden.style.display = 'none';
}
const dropAssetGroup = new Dropdown({
tabIndex: tabIndex + 4,
search: true,
valueKey: 'Key',
textKey: 'Value'
});
dropAssetGroup.onSelected = () => this.refresh();
const dropJobsite = new Dropdown({
tabIndex: tabIndex + 5,
search: true,
valueKey: 'ID',
textKey: 'Name'
});
dropJobsite.onSelected = () => this.refresh();
const dropJobsiteCode = new Dropdown({
tabIndex: tabIndex + 6,
search: true
});
dropJobsiteCode.onSelected = () => this.refresh();
const gridContent = createElement('div', div => {
div.className = 'popup-selector-content';
div.style.height = '400px';
});
const grid = new Grid(gridContent, this.r);
grid.columns = [
{ key: 'VIN', caption: this.r('FLTL_03260', 'VIN'), width: 170 },
{ key: 'DisplayName', caption: this.r('FLTL_01966', 'Name'), width: 190 },
{ key: 'MakeName', caption: this.r('FLTL_01832', 'Make'), width: 110, allowFilter: true },
{ key: 'ModelName', caption: this.r('FLTL_01933', 'Model'), width: 110, allowFilter: true },
{ key: 'TypeName', caption: this.r('FLTL_03112', 'Type'), width: 110, allowFilter: true },
{ key: 'AcquisitionType', caption: this.r('FLTL_00072', 'Acquisition Type'), width: 130, allowFilter: true },
{ key: 'AssetGroups', caption: this.r('FLTL_00318', 'Asset Group'), width: 150 },
{ key: 'Jobsites', caption: this.r('FLTL_01666', 'Jobsite'), width: 180, filter: it => it.Jobsites?.map(s => s.Key) }
];
grid.onRowDblClicked = index => {
const item = grid.source[index];
this.select(item);
if (typeof this._option.close === 'function') {
this._option.close();
}
};
grid.init();
grid.element.tabIndex = tabIndex + 7;
this._var.el = {
inputSearch,
checkHidden,
dropAssetGroup,
dropJobsite,
dropJobsiteCode,
grid
}
const container = createElement('div', 'popup-selector',
createElement('div', 'popup-selector-header',
createElement('img', img => img.src = '../img/modules/devices.png'),
createElement('h3', h3 => h3.innerText = this.title),
option.assetFullcontrol ? createElement('button', button => {
button.className = 'ui-popup-button';
button.tabIndex = tabIndex + 1;
button.innerText = this.r('FLTL_00091', 'Add Asset');
button.addEventListener('click', async () => {
if (typeof option.requestAddAsset === 'function') {
this.loading(true);
const asset = await option.requestAddAsset();
this.loading(false);
if (asset != null) {
this.select(asset);
if (typeof this._option.close === 'function') {
this._option.close();
}
}
}
});
}) : ''
),
createElement('div', 'popup-selector-function',
createElement('div', 'search-box',
inputSearch,
createElement('span', span => {
span.addEventListener('click', () => this.refresh());
},
createIcon('fa-light', 'search')
)
),
checkHidden,
createElement('span', span => span.innerText = this.r('FLTL_00318', 'Asset Group')),
dropAssetGroup.create(),
createElement('span', span => span.innerText = this.r('FLTL_01666', 'Jobsite')),
dropJobsite.create(),
createElement('span', span => span.innerText = this.r('FLTL_01669', 'Jobsite Code')),
dropJobsiteCode.create()
),
gridContent
);
this._var.container = container;
return container;
}
async init() {
const option = this._option;
if (option.dataSource == null) {
if (typeof option.requestAssetParams === 'function') {
this.loading(true);
const data = await option.requestAssetParams();
option.dataSource = data;
} else {
option.dataSource = { AssetGroups: [], Jobsites: [], Codes: [] };
}
}
this._var.el.dropAssetGroup.source = [{ Key: '-1', Value: '' }, ...option.dataSource.AssetGroups];
this._var.el.dropJobsite.source = [{ ID: '-1', Name: '' }, ...option.dataSource.Jobsites];
this._var.el.dropJobsiteCode.source = [{ value: '-1', text: '' }, ...option.dataSource.Codes.map(c => ({ value: c, text: c }))];
this.refresh();
}
}

View File

@ -0,0 +1,189 @@
import { OptionBase, Popup, createElement, showAlert } from "../ui";
import AssetSelector from "./assetselector";
import TemplateSelector from "./templateselector";
export default class InspectionWizard extends OptionBase {
_var = {
/**
* @private
* @type {number}
*/
index: 0,
/**
* @private
* @type {HTMLElement[]}
*/
containers: [],
/**
* @private
* @type {AssetSelector}
*/
assetSelector: null,
asset: null,
template: null,
/**
* @private
* @type {Popup}
*/
popup: null
};
/**
* @event
* @type {(this: InspectionWizard, asset: any, template: any) => void}
*/
onSelected;
constructor(opt = {
/**
* @private
* @type {(assetId: number, search: string) => Promise<any[]>}
*/
requestTemplates: null,
/**
* @private
* @type {(hidden: boolean, search?: string, groups?: string[], jobsites?: number[], codes?: string[]) => Promise<any[]>}
*/
requestAssets: null,
/**
* @private
* @type {() => Promise<{AssetGroups: any[], Codes: any[], Jobsites: any[]}>}
*/
requestAssetParams: null
}) {
super(opt);
}
async create(asset) {
const option = this._option;
const loading = flag => this._var.popup.loading = flag;
const assetSelector = new AssetSelector({
ignoreHidden: true,
loading,
requestAssets: option.requestAssets,
requestAssetParams: option.requestAssetParams
});
this._var.assetSelector = assetSelector;
const templateSelector = new TemplateSelector({
loading,
requestTemplates: option.requestTemplates
});
this._var.templateSelector = templateSelector;
assetSelector.onSelected = asset => {
this._var.asset = asset;
this._var.template = null;
this._changePage(1);
templateSelector.assetId = asset.Id;
};
templateSelector.onSelected = template => {
this._var.template = template;
this._select();
this._var.popup.close();
}
this._var.containers = [
assetSelector.create(),
templateSelector.create()
];
const popup = new Popup({
title: this.r('FLTL_00121', 'Add Inspection'),
content: createElement('div', null, ...this._var.containers),
persistent: true,
buttons: [
{
text: this.r('FLTL_00447', 'Back'),
trigger: () => {
this._changePage(0);
return false;
}
},
{
text: this.r('FLTL_01973', 'Next'),
trigger: () => {
const asset = assetSelector.currentAsset;
if (asset == null) {
showAlert(assetSelector.title, this.r('FLTL_02269', 'Please select an Asset.'));
return false;
}
this._var.asset = asset;
this._var.template = null;
this._changePage(1);
templateSelector.assetId = asset.Id;
return false;
}
},
{
text: this.r('FLTL_02057', 'OK'),
trigger: () => {
const template = templateSelector.currentTemplate;
if (template == null) {
showAlert(templateSelector.title, this.r('FLTL_02261', 'Please select a template.'));
return false;
}
this._var.template = template;
this._select();
}
},
{ text: this.r('FLTL_00499', 'Cancel') }
]
});
this._var.popup = popup;
popup.create();
if (asset) {
this._changePage(1);
this._var.asset = asset;
templateSelector.assetId = asset.Id;
const footerButtons = popup.container.querySelectorAll('.ui-popup-footer>.ui-popup-button');
footerButtons[0].style.display = 'none';
}
else
this._changePage(0);
popup.rect = { width: 1220 };
const mask = await popup.show();
assetSelector.init();
return mask;
}
show(asset) {
if (this._var.popup == null) {
return this.create(asset);
}
if (asset) {
this._var.asset = asset;
this._var.template = null;
this._changePage(1);
this._var.templateSelector._var.el.inputSearch.value = '';
this._var.templateSelector.assetId = asset.Id;
const footerButtons = this._var.popup.container.querySelectorAll('.ui-popup-footer>.ui-popup-button');
footerButtons[0].style.display = 'none';
}
else {
this._changePage(0);
const selector = this._var.assetSelector;
selector._var.el.inputSearch.value = '';
selector._var.el.dropAssetGroup.select('-1');
selector._var.el.dropJobsite.select('-1');
selector._var.el.dropJobsiteCode.select('-1');
selector.refresh();
}
return this._var.popup.show();
}
_select() {
if (typeof this.onSelected === 'function') {
this.onSelected(this._var.asset, this._var.template);
}
}
_changePage(index) {
if (isNaN(index) || index < 0 || index >= 2) {
return;
}
this._var.index = index;
this._var.containers[0].style.display = index === 0 ? '' : 'none';
this._var.containers[1].style.display = index === 1 ? '' : 'none';
const footerButtons = this._var.popup.container.querySelectorAll('.ui-popup-footer>.ui-popup-button');
footerButtons[0].style.display = index === 0 ? 'none' : '';
footerButtons[1].style.display = index === 1 ? 'none' : '';
footerButtons[2].style.display = index === 0 ? 'none' : '';
}
}

View File

@ -1,27 +1,10 @@
import { Grid, GridColumn, createElement, setTooltip, createIcon, createCheckbox, createRadiobox, showAlert, showConfirm, Popup, Dropdown, validation } from "../ui"; import { createElement, createCheckbox, createRadiobox, Dropdown, validation, toDateValue, OptionBase } from "../ui";
import { r as lang, nullOrEmpty, formatUrl, escapeEmoji, isEmail, isPhone } from "../utility";
let r = lang; export default class ScheduleItem extends OptionBase {
function datepicker(element) {
if (typeof $?.fn?.datepicker === 'function') {
$(element).datepicker({
autoHide: true,
format: 'm/dd/yyyy'
});
}
return element;
}
export default class ScheduleItem {
_var = {}; _var = {};
constructor(opt) { constructor(opt = {}) {
this._var.option = opt ?? {}; super(opt);
const getText = opt?.getText;
if (typeof getText === 'function') {
r = getText;
}
} }
get checkOccurOnce() { return this._var.container.querySelector('.schedule-id-box-occur-once>input'); } get checkOccurOnce() { return this._var.container.querySelector('.schedule-id-box-occur-once>input'); }
@ -79,7 +62,8 @@ export default class ScheduleItem {
getDateString(s) { getDateString(s) {
const d = this.getDateTime(s); const d = this.getDateTime(s);
return String(d.getMonth() + 1).padStart(2, '0') + '/' + String(d.getDate()).padStart(2, '0') + '/' + String(d.getFullYear()); // return String(d.getMonth() + 1).padStart(2, '0') + '/' + String(d.getDate()).padStart(2, '0') + '/' + String(d.getFullYear());
return toDateValue(d, true);
} }
setParameters(p) { setParameters(p) {
@ -101,19 +85,12 @@ export default class ScheduleItem {
this._var.container.querySelector('.schedule-id-6>input').checked = schedule.Saturday; this._var.container.querySelector('.schedule-id-6>input').checked = schedule.Saturday;
this._var.container.querySelector('.schedule-id-7>input').checked = schedule.Sunday; this._var.container.querySelector('.schedule-id-7>input').checked = schedule.Sunday;
this._var.container.querySelector('.schedule-id-dayofmonth').value = String(schedule.DayOfMonth); this._var.container.querySelector('.schedule-id-dayofmonth').value = String(schedule.DayOfMonth);
const start = this.getDateString(schedule.StartDate); this._var.container.querySelector('.schedule-id-duration-start').value = this.getDateString(schedule.StartDate);
const end = this.getDateString(schedule.EndDate); this._var.container.querySelector('.schedule-id-duration-end').value = this.getDateString(schedule.EndDate);
if (typeof $?.fn?.datepicker === 'function') {
$(this._var.container.querySelector('.schedule-id-duration-start')).datepicker('setDate', new Date(start));
$(this._var.container.querySelector('.schedule-id-duration-end')).datepicker('setDate', new Date(end));
} else {
this._var.container.querySelector('.schedule-id-duration-start').value = start;
this._var.container.querySelector('.schedule-id-duration-end').value = end;
}
} }
create() { create() {
const option = this._var.option; const option = this._option;
const drop = new Dropdown({ selected: '0' }); const drop = new Dropdown({ selected: '0' });
this.dropFrequency = drop; this.dropFrequency = drop;
drop.source = [ drop.source = [
@ -184,7 +161,7 @@ export default class ScheduleItem {
}), }),
validation( validation(
createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-occur-once', i.maxLength = 5 }), createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-occur-once', i.maxLength = 5 }),
/^([01][0-9]|[2][0-3]):[0-5][0-9]$/ /^([1-9]|[01][0-9]|[2][0-3]):([1-9]|[0-5][0-9])$/
) )
), ),
createElement('div', 'schedule-item-line schedule-item-line-occur-every', createElement('div', 'schedule-item-line schedule-item-line-occur-every',
@ -205,14 +182,14 @@ export default class ScheduleItem {
createElement('span', span => span.innerText = 'Starting at'), createElement('span', span => span.innerText = 'Starting at'),
validation( validation(
createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-occur-starting', i.maxLength = 5 }), createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-occur-starting', i.maxLength = 5 }),
/^([01][0-9]|[2][0-3]):[0-5][0-9]$/ /^([1-9]|[01][0-9]|[2][0-3]):([1-9]|[0-5][0-9])$/
) )
), ),
createElement('div', 'scheldule-item-line', createElement('div', 'scheldule-item-line',
createElement('span', span => span.innerText = 'Ending at'), createElement('span', span => span.innerText = 'Ending at'),
validation( validation(
createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-occur-ending', i.maxLength = 5 }), createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-occur-ending', i.maxLength = 5 }),
/^([01][0-9]|[2][0-3]):[0-5][0-9]$/ /^([1-9]|[01][0-9]|[2][0-3]):([1-9]|[0-5][0-9])$/
) )
) )
) )
@ -222,19 +199,15 @@ export default class ScheduleItem {
createElement('legend', legend => legend.innerText = 'Duration'), createElement('legend', legend => legend.innerText = 'Duration'),
createElement('div', 'schedule-item-line schedule-item-line-duration', createElement('div', 'schedule-item-line schedule-item-line-duration',
createElement('span', span => span.innerText = 'Start date'), createElement('span', span => span.innerText = 'Start date'),
datepicker(
validation( validation(
createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-duration-start', i.maxLength = 10 }), createElement('input', i => { i.type = 'date', i.className = 'ui-input schedule-id-duration-start', i.maxLength = 10, i.required = true, i.min = '1753-01-01', i.max = '9999-12-31' }),
/^([0]?[1-9]|[1][0-2])\/([0]?[1-9]|[12][0-9]|[3][01])\/[0-9]{4}$/ /^[1-9][0-9]{3}-(1[0-2]|0[1-9])-(3[0-1]|[1-2][0-9]|0[1-9])$/
)
), ),
createElement('div', 'schedule-item-placeholder'), createElement('div', 'schedule-item-placeholder'),
createElement('span', span => span.innerText = 'End date'), createElement('span', span => span.innerText = 'End date'),
datepicker(
validation( validation(
createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-duration-end', i.maxLength = 10 }), createElement('input', i => { i.type = 'date', i.className = 'ui-input schedule-id-duration-end', i.maxLength = 10, i.required = true, i.min = '1753-01-01', i.max = '9999-12-31' }),
/^([0]?[1-9]|[1][0-2])\/([0]?[1-9]|[12][0-9]|[3][01])\/[0-9]{4}$/ /^[1-9][0-9]{3}-(1[0-2]|0[1-9])-(3[0-1]|[1-2][0-9]|0[1-9])$/
)
) )
), ),
createElement('div', 'schedule-item-line', createElement('div', 'schedule-item-line',

151
lib/element/signature.js Normal file
View File

@ -0,0 +1,151 @@
import { OptionBase, Popup, createElement } from "../ui";
export default class Signature extends OptionBase {
_var = {
/**
* @private
* @type {HTMLCanvasElement}
*/
canvas: null,
/**
* @private
* @type {Popup}
*/
popup: null,
isDrawing: false,
/**
* @private
* @type {{x: number, y: number}[]}
*/
points: [],
/**
* @private
* @type {{x: number, y: number}[][]}
*/
allPoints: []
};
/**
* @event
* @type {(this: Signature, singature: string) => void}
*/
onSignatured;
constructor(opt = {}) {
super(opt);
}
async show() {
const popup = new Popup({
title: this.r('FLTL_02770', 'Signature'),
content: this._var.canvas = createElement('canvas', canvas => {
canvas.style.width = '100%';
canvas.style.height = '100%';
}),
resolve: result => {
if (result.result === 'ok' && this._var.allPoints.length > 0) {
if (typeof this.onSignatured === 'function') {
const data = this._var.canvas.toDataURL('image/png');
this.onSignatured(data);
}
} else {
if (typeof this.onSignatured === 'function') {
this.onSignatured();
}
}
},
buttons: [
{
text: this.r('FLTL_02057', 'OK'),
trigger: () => {
if (this._var.allPoints.length === 0) {
return false;
}
return 'ok';
}
},
{
text: this.r('FLTL_02479', 'Reset'),
trigger: () => {
const ctx = this._var.canvas.getContext('2d');
ctx.clearRect(0, 0, this._var.canvas.width, this._var.canvas.height);
return false
}
},
{ text: this.r('FLTL_00499', 'Cancel') }
]
});
this._var.popup = popup;
popup.create();
popup.container.classList.add('ui-popup-signature');
const mask = await popup.show();
this.init();
return mask;
}
init() {
const canvas = this._var.canvas;
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
const ctx = canvas.getContext('2d');
ctx.lineWidth = 3;
ctx.strokeStyle = '#000';
canvas.addEventListener('mousedown', e => this._down(ctx, e.offsetX, e.offsetY), false);
canvas.addEventListener('mousemove', e => this._move(ctx, e.offsetX, e.offsetY), false);
canvas.addEventListener('mouseup', () => this._up(ctx), false);
}
/**
* @private
* @param {CanvasRenderingContext2D} ctx
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
*/
_draw(ctx, x1, y1, x2, y2) {
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
/**
* @private
* @param {CanvasRenderingContext2D} ctx
* @param {number} x
* @param {number} y
*/
_down(ctx, x, y) {
this._var.isDrawing = true;
this._var.points.push({ x, y });
ctx.beginPath();
}
/**
* @private
* @param {CanvasRenderingContext2D} ctx
* @param {number} x
* @param {number} y
*/
_move(ctx, x, y) {
if (this._var.isDrawing) {
const lastPoint = this._var.points.at(-1);
this._draw(ctx, lastPoint.x, lastPoint.y, x, y);
this._var.points.push({ x, y });
}
}
/**
* @private
* @param {CanvasRenderingContext2D} ctx
*/
_up(ctx) {
if (this._var.isDrawing) {
this._var.isDrawing = false;
ctx.closePath();
this._var.allPoints.push(this._var.points);
this._var.points = [];
}
}
}

View File

@ -1,3 +1,5 @@
@import "../ui/css/functions/func.scss";
.schedule-item-container { .schedule-item-container {
fieldset { fieldset {
@ -18,6 +20,12 @@
.ui-input { .ui-input {
line-height: 20px; line-height: 20px;
height: 20px; height: 20px;
text-indent: 0;
&.validation-error,
&:invalid {
color: #0000004d;
}
} }
.schedule-item-monthly { .schedule-item-monthly {
@ -89,3 +97,226 @@
height: 24px; height: 24px;
} }
} }
.open-wo-container,
.popup-selector {
.ui-icon {
width: 14px;
height: 14px;
fill: rgb(123, 28, 33);
cursor: pointer;
transition: opacity .12s ease;
&:focus,
&:active,
&:hover {
outline: none;
opacity: .4;
}
}
.col-icon.disabled>.ui-icon {
cursor: inherit;
}
}
.open-wo-container {
display: flex;
flex-direction: column;
>.open-wo-header {
flex: 0 0 auto;
display: flex;
align-items: flex-start;
>h3 {
font-size: var(--font-header-size);
font-family: var(--header-font-family);
margin: 10px;
}
}
>.open-wo-content {
flex: 1 1 auto;
margin: 10px;
display: grid;
grid-template-columns: minmax(130px, auto) 1fr;
grid-auto-rows: minmax(32px, auto);
align-items: center;
justify-items: start;
>.wo-line {
grid-column: 1 / 3;
}
.wo-combined {
line-height: var(--settings-line-height);
display: flex;
align-items: center;
>.ui-icon {
margin-left: 4px;
}
}
.wo-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.wo-title-required {
&::after {
content: '*';
color: var(--red-color);
}
}
}
.wo-sub-line {
margin-left: 10px;
}
.wo-asset-name,
.wo-company-name {
margin-left: 6px;
max-width: 400px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ui-input,
.ui-date-cell,
.ui-drop-wrapper {
margin-left: 4px;
}
.ui-input {
height: 28px;
line-height: 28px;
box-sizing: border-box;
}
.ui-input,
.ui-drop-wrapper {
min-width: 180px;
max-width: 300px;
}
.input-odometer+.ui-drop-wrapper {
min-width: 50px;
}
.ui-date-cell {
height: 26px;
padding: 0 4px;
@include outborder();
&:invalid {
color: rgba(0, 0, 0, .3);
}
}
.ui-text {
width: 100%;
height: 80px;
box-sizing: border-box;
}
.wo-color-line {
display: flex;
align-items: center;
>em {
flex: 0 0 auto;
width: 14px;
height: 14px;
border-radius: 7px;
}
>label {
flex: 1 1 auto;
padding-left: 8px;
cursor: pointer;
}
}
}
}
.popup-selector {
display: flex;
flex-direction: column;
>.popup-selector-header {
flex: 0 0 auto;
display: flex;
align-items: flex-start;
>img {
width: 100px;
}
>h3 {
font-size: var(--font-header-size);
font-family: var(--header-font-family);
margin: 10px;
flex: 1 1 auto;
}
}
>.popup-selector-function {
flex: 0 0 auto;
margin: 10px;
display: flex;
align-items: center;
>.search-box {
position: relative;
margin-right: 8px;
>span {
position: absolute;
display: flex;
right: 4px;
top: calc(50% - 7px);
cursor: pointer;
}
}
.ui-input {
width: 200px;
box-sizing: border-box;
height: 28px;
}
.ui-drop-wrapper {
margin: 0 8px 0 4px;
width: 180px;
}
}
>.popup-selector-content {
flex: 1 1 auto;
margin: 10px;
}
}
.wo-opened-workorder {
display: flex;
flex-direction: column;
width: 650px;
>header {
flex: 0 0 auto;
margin: 10px;
}
>.wo-grid-opened {
flex: 1 1 auto;
margin: 0 10px;
height: 400px;
}
}

View File

@ -0,0 +1,142 @@
import { Grid, OptionBase, createElement, createIcon } from "../ui";
import { nullOrEmpty } from "../utility";
export default class TemplateSelector extends OptionBase {
_var = {
/**
* @private
* @type {number}
*/
assetId: -1,
/**
* @private
* @type {HTMLElement}
*/
container: null,
el: {
/**
* @private
* @type {HTMLInputElement}
*/
inputSearch: null,
/**
* @private
* @type {Grid}
*/
grid: null
}
};
/**
* @event
* @type {(this: TemplateSelector, template: any) => void}
*/
onSelected;
constructor(opt = {
/** @type {(flag: boolean) => void} */
loading: null,
/** @type {() => void} */
close: null,
/** @type {(assetId: number, search: string) => Promise<any[]>} */
requestTemplates: null
}) {
super(opt);
}
get assetId() { return this._var.assetId }
set assetId(id) {
this._var.assetId = id;
this.refresh();
}
get title() { return this.r('FLTL_01604', 'Inspection Templates') }
get currentTemplate() { return this._var.el.grid.currentItem }
loading(flag) {
if (typeof this._option.loading === 'function') {
this._option.loading(flag);
}
}
refresh() {
const requestTemplates = this._option.requestTemplates;
if (typeof requestTemplates !== 'function') {
return;
}
const el = this._var.el;
this.loading(true);
requestTemplates(this._var.assetId, el.inputSearch.value)
.then(data => el.grid.source = data)
.finally(() => this.loading(false));
}
select(item) {
if (typeof this.onSelected !== 'function') {
return false;
}
this.onSelected(item);
}
create() {
const tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)) + 3;
const inputSearch = createElement('input', input => {
input.type = 'text';
input.placeholder = this.r('FLTL_02606', 'Search');
input.tabIndex = tabIndex + 1;
input.className = 'ui-input';
input.addEventListener('keypress', e => e.key === 'Enter' && this.refresh());
});
const gridContent = createElement('div', div => {
div.className = 'popup-selector-content';
div.style.height = '400px';
});
const grid = new Grid(gridContent, this.r);
grid.columns = [
{
key: 'IssueId',
type: Grid.ColumnTypes.Icon,
width: 30,
enabled: false,
resizable: false,
filter: it => nullOrEmpty(it.IssueId) ? '' : 'cubes'
},
{ key: 'Name', caption: this.r('FLTL_01966', 'Name'), width: 390 },
{ key: 'Notes', caption: this.r('FLTL_02012', 'Notes'), width: 630 }
];
grid.onRowDblClicked = index => {
const item = grid.source[index];
this.select(item);
if (typeof this._option.close === 'function') {
this._option.close();
}
};
grid.init();
grid.element.tabIndex = tabIndex + 2;
this._var.el = {
inputSearch,
grid
};
// content
const container = createElement('div', 'popup-selector',
createElement('div', 'popup-selector-header',
createElement('h3', h3 => h3.innerText = this.r('FLTL_02261', 'Please select a template.'))
),
createElement('div', 'popup-selector-function',
createElement('div', 'search-box',
inputSearch,
createElement('span', span => {
span.addEventListener('click', () => this.refresh());
},
createIcon('fa-light', 'search')
)
)
),
gridContent
);
this._var.container = container;
return container;
}
}

View File

@ -8,10 +8,61 @@ import { createTab } from "./ui/tab";
import { Dropdown } from "./ui/dropdown"; import { Dropdown } from "./ui/dropdown";
import { Grid } from "./ui/grid/grid"; import { Grid } from "./ui/grid/grid";
import { GridColumn, GridInputColumn, GridDropdownColumn, GridCheckboxColumn, GridIconColumn, GridTextColumn, GridDateColumn } from './ui/grid/column'; import { GridColumn, GridInputColumn, GridDropdownColumn, GridCheckboxColumn, GridIconColumn, GridTextColumn, GridDateColumn } from './ui/grid/column';
import { Popup, createPopup, showAlert, showConfirm } from "./ui/popup"; import { Popup, createPopup, resolvePopup, showAlert, showConfirm } from "./ui/popup";
import { createPicture, createAudio, createVideo, createFile } from './ui/media'; import { createPicture, createAudio, createVideo, createFile, createVideoList } from './ui/media';
import { validation, convertCssStyle } from './ui/extension'; import { validation, convertCssStyle } from './ui/extension';
import { createDateInput, toDateValue, formatDate, setDateValue, getDateValue, DateSelector } from './ui/date'; import { createDateInput, toDateValue, getFormatter, formatDate, setDateValue, getDateValue, DateSelector } from './ui/date';
import * as utility from './utility';
function requestAnimationFrame(callback) {
if (typeof utility.global.requestAnimationFrame === 'function') {
utility.global.requestAnimationFrame(callback);
} else {
setTimeout(callback, 0);
}
}
function scrollLeft() {
const n = document.documentElement;
return (utility.global.scrollX || n.scrollLeft) - (n.clientLeft || 0);
}
function scrollTop() {
const n = document.documentElement;
return (utility.global.scrollY || n.scrollTop) - (n.clientTop || 0);
}
/**
* @private
* @param {HTMLElement} e
*/
function offset(e) {
const rect = e.getBoundingClientRect();
return {
top: rect.top + scrollTop(),
left: rect.left + scrollLeft(),
height: rect.height,
width: rect.width
};
}
class OptionBase {
_option;
r;
constructor(opt) {
this._option = opt;
const getText = opt.getText;
if (typeof getText === 'function') {
this.r = getText;
} else if (typeof GetTextByKey === 'function') {
this.r = GetTextByKey;
} else {
this.r = utility.r;
}
}
}
export { export {
createElement, createElement,
@ -42,11 +93,13 @@ export {
// popup // popup
Popup, Popup,
createPopup, createPopup,
resolvePopup,
showAlert, showAlert,
showConfirm, showConfirm,
// dateSelector // dateSelector
createDateInput, createDateInput,
toDateValue, toDateValue,
getFormatter,
formatDate, formatDate,
setDateValue, setDateValue,
getDateValue, getDateValue,
@ -56,7 +109,15 @@ export {
createAudio, createAudio,
createVideo, createVideo,
createFile, createFile,
createVideoList,
// extension // extension
validation, validation,
convertCssStyle convertCssStyle,
// utility
utility,
// functions
requestAnimationFrame,
offset,
// base classes
OptionBase
} }

View File

@ -10,6 +10,8 @@ interface CheckboxOptions {
name?: string; name?: string;
/** 焦点索引 */ /** 焦点索引 */
tabIndex?: Number; tabIndex?: Number;
/** 是否为 switch 样式 */
switch?: boolean;
/** 样式分类,可以是 ['`fa-light`', '`fa-regular`', '`fa-solid`'] 其中之一 */ /** 样式分类,可以是 ['`fa-light`', '`fa-regular`', '`fa-solid`'] 其中之一 */
type?: string; type?: string;
/** 标签 */ /** 标签 */

View File

@ -3,6 +3,10 @@ import { createElement } from "../functions";
import { createIcon } from "./icon"; import { createIcon } from "./icon";
function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, charactor = 'check', title) { 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');
indeterminateIcon.classList.add('ui-indeterminate-icon')
container.appendChild( container.appendChild(
createElement('layer', layer => { createElement('layer', layer => {
layer.className = 'ui-check-inner'; layer.className = 'ui-check-inner';
@ -18,7 +22,7 @@ function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, char
if (tabindex >= 0) { if (tabindex >= 0) {
layer.tabIndex = tabindex; layer.tabIndex = tabindex;
} }
}, createIcon(type, charactor)) }, checkIcon, indeterminateIcon)
); );
if (label instanceof Element) { if (label instanceof Element) {
container.appendChild(label); container.appendChild(label);
@ -26,7 +30,9 @@ function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, char
container.appendChild( container.appendChild(
createElement('span', span => { createElement('span', span => {
span.innerText = label; span.innerText = label;
if (title != null) {
span.title = title; span.title = title;
}
}) })
); );
} }
@ -60,12 +66,15 @@ export function createRadiobox(opts = {}) {
} }
export function createCheckbox(opts = {}) { export function createCheckbox(opts = {}) {
const container = createElement('label', 'ui-check-wrapper', const container = createElement('label', opts.switch ? 'ui-switch' : 'ui-check-wrapper',
createElement('input', input => { createElement('input', input => {
input.setAttribute('type', 'checkbox'); input.setAttribute('type', 'checkbox');
if (opts.checked === true) { if (opts.checked === true) {
input.checked = true; input.checked = true;
} }
if (opts.indeterminate === true) {
input.indeterminate = true;
}
if (opts.enabled === false) { if (opts.enabled === false) {
input.disabled = true; input.disabled = true;
} }
@ -84,7 +93,23 @@ export function createCheckbox(opts = {}) {
if (opts.enabled === false) { if (opts.enabled === false) {
container.classList.add('disabled'); container.classList.add('disabled');
} }
if (opts.checkedNode != null && opts.uncheckedNode != null) { if (opts.switch) {
const label = opts.label;
if (label instanceof Element) {
container.appendChild(label);
} else {
container.appendChild(
createElement('span', span => {
if (label != null && String(label).length > 0) {
span.innerText = label;
}
if (opts.title != null) {
span.title = opts.title;
}
})
);
}
} else if (opts.checkedNode != null && opts.uncheckedNode != null) {
container.classList.add('ui-check-image-wrapper'); container.classList.add('ui-check-image-wrapper');
let height = opts.imageHeight; let height = opts.imageHeight;
if (isNaN(height) || height <= 0) { if (isNaN(height) || height <= 0) {

View File

@ -49,3 +49,100 @@
} }
} }
} }
.ui-switch {
line-height: 1rem;
user-select: none;
cursor: pointer;
&.disabled {
cursor: default;
}
>span:first-of-type {
display: inline-flex;
align-items: center;
position: relative;
&::before {
content: '';
width: 30px;
min-width: 30px;
max-width: 30px;
height: 16px;
margin-right: 4px;
background-color: var(--switch-bg-color);
border-radius: 8px;
transition: background-color .08s ease;
}
&::after {
content: '';
position: absolute;
left: 1px;
top: calc(50% - 7px);
width: 14px;
height: 14px;
background-color: white;
border-radius: 7px;
box-shadow: 1px 1px 8px rgb(0 0 0 / 20%);
transition: left .08s ease;
}
}
&.ui-switch-red>span:first-of-type::before {
background-color: var(--red-color);
}
>input[type="checkbox"] {
display: none;
&:checked+span:first-of-type {
&::before {
background-color: var(--switch-active-bg-color);
}
&::after {
left: 15px;
}
}
&:disabled+span:first-of-type {
cursor: default;
&::before {
opacity: .3;
}
}
}
}
.ui-switch-group {
line-height: 1rem;
user-select: none;
display: inline-flex;
background-color: var(--switch-bg-color);
border-radius: 10px;
>label {
>span {
display: block;
padding: 4px 10px;
margin: 2px;
border-radius: 8px;
cursor: pointer;
}
}
>label>input[type="radio"] {
display: none;
&:checked+span {
background-color: white;
box-shadow: 1px 1px 8px rgb(0 0 0 / 20%);
transition: background-color .08s ease;
}
}
}

View File

@ -7,7 +7,8 @@
@include outborder(); @include outborder();
&.validation-error { &.validation-error,
&:invalid {
border-color: var(--red-color); border-color: var(--red-color);
&:focus, &:focus,

View File

@ -30,7 +30,8 @@ $listMaxHeight: 210px;
flex: 1 1 auto; flex: 1 1 auto;
cursor: pointer; cursor: pointer;
font-size: var(--font-size); font-size: var(--font-size);
// line-height: $headerHeight; line-height: $headerHeight;
min-height: $headerHeight;
padding: 0 6px; padding: 0 6px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -152,21 +153,25 @@ $listMaxHeight: 210px;
} }
>.ui-drop-list { >.ui-drop-list {
margin: 0;
padding: 0;
list-style: none;
max-height: $listMaxHeight; max-height: $listMaxHeight;
overflow-y: auto; overflow-y: auto;
position: relative;
font-size: var(--font-size); font-size: var(--font-size);
@include scrollbar(); @include scrollbar();
&.filtered>li:first-child { &.filtered>.drop-content>.li:first-child {
background-color: var(--hover-bg-color); background-color: var(--hover-bg-color);
} }
>li { >.drop-content {
position: absolute;
width: 100%;
}
li {
// display: flex; // display: flex;
// align-items: center; // align-items: center;
list-style: none;
line-height: $dropItemHeight; line-height: $dropItemHeight;
height: $dropItemHeight; height: $dropItemHeight;
padding: 0 10px; padding: 0 10px;
@ -180,6 +185,16 @@ $listMaxHeight: 210px;
background-color: var(--hover-bg-color); background-color: var(--hover-bg-color);
} }
>.li-wrapper {
display: flex;
align-items: center;
>.ui-expandor {
width: 12px;
height: 12px;
display: flex;
}
>.ui-check-wrapper { >.ui-check-wrapper {
height: $dropItemHeight; height: $dropItemHeight;
display: flex; display: flex;
@ -187,3 +202,4 @@ $listMaxHeight: 210px;
} }
} }
} }
}

View File

@ -92,7 +92,22 @@
border-color: var(--link-color); border-color: var(--link-color);
background-color: var(--link-color); background-color: var(--link-color);
>svg { >.ui-check-icon {
transform: scale(1);
opacity: 1;
}
}
&:indeterminate+.ui-check-inner {
border-color: var(--secondary-color);
background-color: var(--secondary-color);
>.ui-check-icon {
transform: scale(0);
opacity: 0;
}
>.ui-indeterminate-icon {
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
} }

View File

@ -45,6 +45,7 @@
--header-filter-padding: 4px 26px 4px 8px; --header-filter-padding: 4px 26px 4px 8px;
--spacing-s: 4px; --spacing-s: 4px;
--spacing-cell: 9px 4px 9px 8px; --spacing-cell: 9px 4px 9px 8px;
--spacing-drop-cell: 5px 4px 5px 8px;
--filter-line-height: 30px; --filter-line-height: 30px;
--filter-item-padding: 0 4px; --filter-item-padding: 0 4px;
} }
@ -122,8 +123,10 @@
} }
} }
>.ui-check-wrapper { >.ui-check-wrapper,
>.ui-switch {
height: 20px; height: 20px;
padding: 0 4px 0 0;
} }
>svg { >svg {
@ -398,10 +401,12 @@
@include scrollbar(); @include scrollbar();
} }
.ui-check-wrapper { .ui-check-wrapper,
.ui-switch {
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
height: var(--row-height); height: var(--row-height);
padding: 0 8px;
.ui-check-inner { .ui-check-inner {
@ -410,6 +415,14 @@
transition: none; transition: none;
} }
} }
>span:first-of-type {
&:before,
&:after {
transition: none;
}
}
} }
.ui-drop-span { .ui-drop-span {
@ -439,7 +452,7 @@
height: 100%; height: 100%;
>.ui-drop-text { >.ui-drop-text {
padding: var(--spacing-cell); padding: var(--spacing-drop-cell);
} }
} }
} }
@ -705,6 +718,13 @@
} }
} }
.ui-popup-mask .ui-popup-container .ui-popup-footer {
>.ui-sort-layout {
flex: 1 1 auto;
margin-left: 8px;
}
}
/*@media (prefers-color-scheme: dark) { /*@media (prefers-color-scheme: dark) {
.ui-grid { .ui-grid {
--cell-hover-bg-color: yellow; --cell-hover-bg-color: yellow;

View File

@ -66,3 +66,234 @@
max-width: 200px; max-width: 200px;
max-height: 200px; max-height: 200px;
} }
.ui-media-video-container {
position: relative;
background-color: #000;
min-height: 200px;
height: 100%;
user-select: none;
--icon-size: 16px;
>.ui-video-content {
height: 100%;
>.ui-video-wrapper {
position: relative;
display: inline-block;
>video {
width: 100%;
height: 100%;
}
>.ui-video-waiting {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .3);
display: flex;
justify-content: center;
align-items: center;
transition: opacity .2s;
>svg {
fill: #eee;
width: 30px;
height: 30px;
top: calc(50% - 15px);
left: calc(50% - 15px);
animation: spinner 1.2s infinite linear;
}
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
}
}
}
>.ui-video-control {
position: absolute;
left: 0;
width: 100%;
height: 70px;
bottom: 0;
opacity: 0;
background: linear-gradient(transparent, #000);
// calc(100% - 100px), rgba(50, 50, 50, .7) calc(100% - 65px)
transition: opacity .5s;
&:hover,
&.active {
opacity: 1;
}
.ui-video-icon {
position: absolute;
width: var(--icon-size);
height: var(--icon-size);
cursor: pointer;
>svg {
width: var(--icon-size);
height: var(--icon-size);
display: block;
fill: #eee;
}
&.disabled {
cursor: default;
opacity: .5;
}
}
.ui-video-bar {
position: absolute;
height: 4px;
padding: 4px 0;
cursor: pointer;
>.seek-buffers {
position: absolute;
width: 100%;
>.ui-video-buffer {
background-color: #adadad;
}
}
>.ui-video-duration,
>.seek-buffers>.ui-video-buffer,
>.ui-video-progress {
position: absolute;
border-radius: 2px;
height: 4px;
cursor: pointer;
}
>.ui-video-duration {
width: 100%;
background-color: #4e4e4e;
}
>.ui-video-progress {
width: 0;
background-color: #fff;
&::after {
content: '\a0';
position: absolute;
right: -6px;
top: -5px;
width: 14px;
height: 14px;
border-radius: 7px;
background-color: #fff;
}
}
}
>.play-icon {
left: 20px;
bottom: 41px;
>svg {
position: absolute;
&:last-child {
display: none;
}
}
&.pause {
>svg:first-child {
display: none;
}
>svg:last-child {
display: initial;
}
}
}
>.ui-video-time-label {
position: absolute;
font-family: var(--font-family);
font-size: 14px;
font-weight: 600;
left: 48px;
bottom: 40px;
color: #eee;
}
>.fullscreen-icon {
right: 20px;
bottom: 41px;
}
>.ui-video-volume-container {
position: absolute;
bottom: 40px;
right: 50px;
>.volume-icon {
right: 0;
bottom: 1px;
>svg {
position: absolute;
&:last-child {
display: none;
}
}
&.muted {
>svg:first-child {
display: none;
}
>svg:last-child {
display: initial;
}
}
}
>.volume-bar {
width: 60px;
right: 30px;
bottom: 2px;
}
}
&.no-fullscreen>.ui-video-volume-container {
right: 20px;
}
>.seek-bar {
left: 16px;
right: 16px;
bottom: 16px;
>.seek-progress::after {
opacity: 0;
transition: opacity .2s;
}
}
&.active>.seek-bar>.seek-progress::after,
>.seek-bar:hover>.seek-progress::after {
opacity: 1;
}
}
}

View File

@ -62,7 +62,7 @@ $buttonHeight: 28px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding: 10px 0 6px 12px; padding: 5px 0 3px 12px;
} }
>.ui-popup-header-title, >.ui-popup-header-title,
@ -76,7 +76,7 @@ $buttonHeight: 28px;
>.ui-popup-header-icons { >.ui-popup-header-icons {
flex: 0 0 auto; flex: 0 0 auto;
padding: 10px 12px 6px 0; padding: 5px 12px 3px 0;
display: flex; display: flex;
>svg { >svg {
@ -145,8 +145,9 @@ $buttonHeight: 28px;
margin: 10px; margin: 10px;
>svg { >svg {
width: 40px; width: 30px;
height: 40px; height: 30px;
fill: unset;
+span { +span {
padding-left: 16px; padding-left: 16px;
@ -204,6 +205,10 @@ $buttonHeight: 28px;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
padding: 4px 10px 16px 2px; padding: 4px 10px 16px 2px;
}
.ui-popup-body,
.ui-popup-footer {
.ui-popup-button { .ui-popup-button {
margin-left: 12px; margin-left: 12px;
@ -212,7 +217,7 @@ $buttonHeight: 28px;
line-height: $buttonHeight; line-height: $buttonHeight;
color: var(--title-color); color: var(--title-color);
border-radius: var(--corner-radius); border-radius: var(--corner-radius);
padding: 4px 16px; padding: 1px 16px;
box-sizing: border-box; box-sizing: border-box;
min-width: 70px; min-width: 70px;
text-align: center; text-align: center;
@ -227,6 +232,11 @@ $buttonHeight: 28px;
} }
@include outline(); @include outline();
&:disabled {
opacity: .4;
cursor: default;
}
} }
} }

View File

@ -21,6 +21,8 @@
--disabled-color: #aaa; --disabled-color: #aaa;
--disabled-bg-color: #e9e9e9; --disabled-bg-color: #e9e9e9;
--disabled-border-color: #d9d9d9; --disabled-border-color: #d9d9d9;
--switch-bg-color: #eae9eb;
--switch-active-bg-color: #33c559;
--red-color: red; --red-color: red;
--title-color: #fff; --title-color: #fff;
@ -36,11 +38,14 @@
--border-radius: 2px; --border-radius: 2px;
--text-indent: 4px; --text-indent: 4px;
--line-height: 18px; --line-height: 18px;
--settings-line-height: 32px;
--font-size: .8125rem; // 13px --font-size: .8125rem; // 13px
--font-smaller-size: .75rem; // 12px --font-smaller-size: .75rem; // 12px
--font-larger-size: .875rem; // 14px --font-larger-size: .875rem; // 14px
--font-header-size: 1.5rem; // 24px
--font-family: "Franklin Gothic Book", "San Francisco", "Segoe UI", "Open Sans", "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei UI", sans-serif; --font-family: "Franklin Gothic Book", "San Francisco", "Segoe UI", "Open Sans", "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei UI", sans-serif;
--header-font-family: Arial, sans-serif;
} }
/*@media (prefers-color-scheme: dark) { /*@media (prefers-color-scheme: dark) {

14
lib/ui/date.d.ts vendored
View File

@ -10,8 +10,9 @@ export function createDateInput(min?: string, max?: string, element?: HTMLInputE
/** /**
* 将日期转换成 yyyy-MM-dd 格式的字符串 * 将日期转换成 yyyy-MM-dd 格式的字符串
* @param dt 日期值 * @param dt 日期值
* @param local 是否视日期为本地时间
*/ */
export function toDateValue(dt: Date): string; export function toDateValue(dt: Date, local?: boolean): string;
/** /**
* 格式化日期字符串 * 格式化日期字符串
@ -23,14 +24,15 @@ export function toDateValue(dt: Date): string;
* `new Date('2024-01-26')`<br/> * `new Date('2024-01-26')`<br/>
* @returns 格式化为 M/d/yyyy 的日期字符串 * @returns 格式化为 M/d/yyyy 的日期字符串
*/ */
export function formatDate(date: Date | number | string): string; export function formatDate(date: Date | number | string, formatter?: string): string;
/** /**
* 设置显示日期 * 设置显示日期
* @param element 要设置显示日期的元素 * @param element 要设置显示日期的元素
* @param val 日期值,支持格式参见 {@linkcode formatDate} * @param val 日期值,支持格式参见 {@linkcode formatDate}
* @param formatter 日期格式化字符串(仅设置显示元素时调用)
*/ */
export function setDateValue(element: HTMLElement, val: Date | number | string): void; export function setDateValue(element: HTMLElement, val: Date | number | string, formatter?: string): void;
/** /**
* 从日期选择框获取日期值 * 从日期选择框获取日期值
@ -38,7 +40,7 @@ export function setDateValue(element: HTMLElement, val: Date | number | string):
* @param formatter 自定义格式化函数,传入参数为 `Date` 类型 * @param formatter 自定义格式化函数,传入参数为 `Date` 类型
* @returns 默认返回日期 `ticks` 的字符串 * @returns 默认返回日期 `ticks` 的字符串
*/ */
export function getDateValue(element: HTMLInputElement, formatter?: (date: Date) => string): string; export function getDateValue(element: HTMLInputElement, formatter?: string | ((date: Date) => string)): string;
/** 日期选择框类 */ /** 日期选择框类 */
export class DateSelector { export class DateSelector {
@ -85,6 +87,10 @@ export class DateSelector {
maxDate?: string, maxDate?: string,
/** 是否启用 */ /** 是否启用 */
enabled?: boolean, enabled?: boolean,
/** 焦点索引 */
tabIndex?: number,
/** 类名 */
className?: string,
/** /**
* 自定义格式化函数,用于获取日期值时调用 * 自定义格式化函数,用于获取日期值时调用
* @param date 日期值 * @param date 日期值

View File

@ -1,4 +1,5 @@
import { createElement } from "../functions"; import { createElement } from "../functions";
import { isPositive, nullOrEmpty } from "../utility";
/** /**
* 创建或转换日期选择框 * 创建或转换日期选择框
@ -22,9 +23,13 @@ export function createDateInput(min, max, element) {
date.type = 'date'; date.type = 'date';
if (min != null) { if (min != null) {
date.min = min; date.min = min;
} else {
date.min = '1753-01-01';
} }
if (max != null) { if (max != null) {
date.max = max; date.max = max;
} else {
date.max = '9999-12-31';
} }
return date; return date;
} }
@ -32,38 +37,206 @@ export function createDateInput(min, max, element) {
/** /**
* 将日期转换为 `yyyy-MM-dd` 格式的字符串 * 将日期转换为 `yyyy-MM-dd` 格式的字符串
* @param {Date} dt 要转换的日期值 * @param {Date} dt 要转换的日期值
* @param {boolean} [utc] 是否采用 UTC 值 * @param {boolean} [local] 是否视日期为本地时间
* @returns 返回 `yyyy-MM-dd` 格式的字符串 * @returns 返回 `yyyy-MM-dd` 格式的字符串
*/ */
export function toDateValue(dt, utc) { export function toDateValue(dt, local) {
if (isNaN(dt)) { if (isNaN(dt)) {
return ''; return '';
} }
const year = utc ? dt.getUTCFullYear() : dt.getFullYear(); const year = local ? dt.getFullYear() : dt.getUTCFullYear();
const month = String((utc ? dt.getUTCMonth() : dt.getMonth()) + 1).padStart(2, '0'); const month = String((local ? dt.getMonth() : dt.getUTCMonth()) + 1).padStart(2, '0');
const date = String(utc ? dt.getUTCDate() : dt.getDate()).padStart(2, '0'); const date = String(local ? dt.getDate() : dt.getUTCDate()).padStart(2, '0');
return `${year}-${month}-${date}`; return `${year}-${month}-${date}`;
} }
/**
* 获取日期格式器
* @param {Date} date - 待格式的日期
* @param {boolean} [utc=true] - 是否按 UTC 时间格式化
* @returns {any} 返回格式化工具对象
*/
export function getFormatter(date, utc) {
const prefix = utc !== false ? 'getUTC' : 'get';
const r = {
/**
* @private
* @returns {number} 返回日数字
*/
j: () => date[`${prefix}Date`](),
/**
* @private
* @returns {string} 返回格式化成两位的日字符串
*/
d: () => String(r.j()).padStart(2, '0'),
/**
* @private
* @returns {number} 返回月数字
*/
n: () => date[`${prefix}Month`]() + 1,
/**
* @private
* @returns {string} 返回格式化成两位的月字符串
*/
m: () => String(r.n()).padStart(2, '0'),
/**
* @private
* @returns {number} 返回年数字
*/
Y: () => date[`${prefix}FullYear`](),
/**
* @private
* @returns {string} 返回年后两位
*/
y: () => String(r.Y()).slice(-2),
/**
* @private
* @returns {number} 返回日期当月总天数
*/
t: () => new Date(r.Y(), r.n(), 0).getDate(),
/**
* @private
* @returns {('1' | '0')} 返回是否为闰年
*/
L() {
const y = r.Y();
return t % 4 === 0 && t % 100 !== 0 || t % 400 === 0 ? 1 : 0;
},
/**
* @private
* @returns {number} 返回小时数字
*/
G: () => date[`${prefix}Hours`](),
/**
* @private
* @returns {number} 返回 12 小时制的数字
*/
g: () => r.G() % 12 || 12,
/**
* @private
* @returns {string} 返回格式化成两位的小时字符串
*/
H: () => String(r.G()).padStart(2, '0'),
/**
* @private
* @returns {string} 返回格式化成两位的 12 小时制的字符串
*/
h: () => String(r.g()).padStart(2, '0'),
/**
* @private
* @returns {string} 返回上下午
*/
A: () => ['AM', 'PM'][r.G() < 12 ? 0 : 1],
/**
* @private
* @returns {string} 返回小写的上下午
*/
a: () => r.A().toLowerCase(),
/**
* @private
* @returns {string} 返回格式化成两位的分钟字符串
*/
i: () => String(date[`${prefix}Minutes`]()).padStart(2, '0'),
/**
* @private
* @returns {string} 返回格式化成两位的秒字符串
*/
s: () => String(date[`${prefix}Seconds`]()).padStart(2, '0'),
/**
* @private
* @returns {string} 返回格式化成六位的毫秒字符串
*/
u: () => String(1e3 * date[`${prefix}Milliseconds`]()).padStart(6, '0'),
/**
* @private
* @returns {string} 返回时区描述字符串
*/
e: () => /\((.*)\)/.exec(String(date))[1] || 'Coordinated Universal Time',
/**
* @private
* @returns {string} 返回时区偏移字符串
*/
O() {
const t = date.getTimezoneOffset();
const r = Math.abs(t);
return (t > 0 ? '-' : '+') + String(100 * Math.floor(r / 60) + r % 60).padStart(4, '0');
},
/**
* @private
* @returns {string} 返回 +08:00 这种格式的时区偏移字符串
*/
P() {
const t = r.O();
return t.substring(0, 3) + ':' + t.substring(3, 5);
}
};
return r;
}
/** /**
* 格式化日期为 M/d/yyyy 格式的字符串 * 格式化日期为 M/d/yyyy 格式的字符串
* @param {Date | number | string} date - 需要格式化的日期值,支持的格式如下: * @param {Date | number | string} date - 需要格式化的日期值,支持的格式如下:
* *
* * `"2024-01-26"` * * `"2024-01-26"`
* * `"2024/1/26"`
* * `"2024-01-26T00:00:00"` * * `"2024-01-26T00:00:00"`
* * `"1/26/2024"` * * `"1/26/2024"`
* * `"638418240000000000"` * * `"638418240000000000"`
* * `new Date('2024-01-26')` * * `new Date('2024-01-26')`
* @param {string} [formatter] - 格式化格式,默认为 `"m/d/Y"`
*
* * Y - 年,例如 `2024`
* * y - 年的后两位,例如 `"24"`
* * n - 月,例如 `2`
* * m - 格式化成两位的月,例如 `"02"`
* * j - 日,例如 `4`
* * d - 格式化成两位的日,例如 `"04"`
* * t - 日期当月总天数,例如 `29`
* * L - 是否为闰年,例如 `1`
* * G - 小时,例如 `15`
* * g - 12 小时制的小时,例如 `3`
* * H - 格式化成两位的小时,例如 `"15"`
* * h - 格式化成两位的 12 小时制的小时,例如 `"03"`
* * A - 上下午,例如 `"PM"`
* * a - 小写的上下午,例如 `"pm"`
* * i - 格式化成两位的分钟,例如 `"39"`
* * s - 格式化成两位的秒,例如 `"20"`
* * u - 格式化成六位的毫秒,例如 `"023040"`
* * e - 时区描述字符串,例如 `"China Standard Time"`
* * O - 时区偏移字符串,例如 `"+0800"`
* * P - `"+08:00"` 这种格式的时区偏移字符串
* @returns {string} 返回格式化后的日期字符串 * @returns {string} 返回格式化后的日期字符串
*/ */
export function formatDate(date) { export function formatDate(date, formatter) {
formatter ??= 'm/d/Y';
if (date === '') {
return '';
}
if (isNaN(date)) {
let e = /^(\d{4})-(\d{2})-(\d{2})/.exec(date);
if (e == null) {
e = /^(\d{4})\/(\d{1,2})\/(\d{1,2})/.exec(date);
}
if (e != null) {
date = new Date(e[1], parseInt(e[2]) - 1, e[3]);
} else {
e = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(date);
if (e != null) {
date = new Date(e[3], parseInt(e[1]) - 1, e[2]);
} else {
return date;
}
}
}
if (date instanceof Date) { if (date instanceof Date) {
return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; const f = getFormatter(date, false);
return formatter.replace(/\\?(.?)/gi, (k, v) => f[k] ? f[k]() : v);
} }
const ticks = Number(date); const ticks = Number(date);
if (!isNaN(ticks) && ticks > 0) { if (!isNaN(ticks) && ticks > 0) {
date = new Date((ticks - 621355968e9) / 1e4); date = new Date((ticks - 621355968e9) / 1e4);
return `${date.getUTCMonth() + 1}/${date.getUTCDate()}/${date.getUTCFullYear()}`; const f = getFormatter(date);
return formatter.replace(/\\?(.?)/gi, (k, v) => f[k] ? f[k]() : v);
} }
return date; return date;
} }
@ -72,27 +245,42 @@ export function formatDate(date) {
* 设置显示日期 * 设置显示日期
* @param {HTMLElement} element - 要设置显示日期的元素 * @param {HTMLElement} element - 要设置显示日期的元素
* @param {Date | number | string} val - 日期值,支持格式参见 {@linkcode formatDate} * @param {Date | number | string} val - 日期值,支持格式参见 {@linkcode formatDate}
* @param {string} [formatter] - 日期格式化字符串(仅设置显示元素时调用)
*/ */
export function setDateValue(element, val) { export function setDateValue(element, val, formatter) {
if (element.tagName === 'INPUT') { if (element.tagName === 'INPUT') {
if (val === '') { if (val === '') {
element.value = ''; element.value = '';
} else if (isNaN(val)) { } else if (isNaN(val)) {
if (/^\d{4}-\d{2}-\d{2}/.test(val)) { if (/^\d{4}-\d{2}-\d{2}/.test(val)) {
element.value = String(val).substring(0, 10); element.value = String(val).substring(0, 10);
} else if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(val)) { } else if (/^\d{4}\/\d{2}\/\d{2}/.test(val)) {
element.value = toDateValue(new Date(val)); element.value = String(val).replace('/', '-').substring(0, 10);
} else {
const e = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(val);
if (e != null) {
const month = e[1].padStart(2, '0');
const day = e[2].padStart(2, '0');
const year = e[3];
element.value = `${year}-${month}-${day}`;
} else { } else {
element.value = ''; element.value = '';
} }
} else {
if (!(val instanceof Date)) {
val = new Date((val - 621355968e9) / 1e4);
} }
} else {
if (val instanceof Date) {
element.value = toDateValue(val, true); element.value = toDateValue(val, true);
} else {
const ticks = Number(val);
if (!isNaN(ticks) && ticks > 0) {
element.value = toDateValue(new Date((ticks - 621355968e9) / 1e4));
} else {
element.value = '';
}
}
} }
} else { } else {
element.innerText = formatDate(val); element.innerText = formatDate(val, formatter);
} }
} }
@ -106,7 +294,7 @@ export function setDateValue(element, val) {
/** /**
* 从日期选择框获取日期值 * 从日期选择框获取日期值
* @param {HTMLInputElement} element - 要获取的日期选择框 * @param {HTMLInputElement} element - 要获取的日期选择框
* @param {DateFormatterCallback} [formatter] - 自定义格式化函数,传入参数为 `Date` 类型 * @param {string | DateFormatterCallback} [formatter] - 自定义格式化字符串或函数,传入参数为 `Date` 类型
* @returns {string | any} 默认返回日期 `ticks` 的字符串 * @returns {string | any} 默认返回日期 `ticks` 的字符串
*/ */
export function getDateValue(element, formatter) { export function getDateValue(element, formatter) {
@ -116,8 +304,15 @@ export function getDateValue(element, formatter) {
if (year < 1900 || year > 9999) { if (year < 1900 || year > 9999) {
return ''; return '';
} }
if (formatter != null) {
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
// 使外部 formatter 不需要再处理 `getUTCDate` 亦或是 `getDate`
const localDate = new Date(`${year}-${month}-${day}T00:00:00`);
if (typeof formatter === 'function') { if (typeof formatter === 'function') {
return formatter(date); return formatter(localDate);
}
return formatDate(localDate, formatter);
} }
return String(date.getTime() * 1e4 + 621355968e9); return String(date.getTime() * 1e4 + 621355968e9);
} }
@ -130,7 +325,43 @@ export function getDateValue(element, formatter) {
*/ */
export class DateSelector { export class DateSelector {
_var = { _var = {
options: null /**
* @type {HTMLInputElement}
* @private
*/
el: null,
options: {
/**
* @type {boolean?}
* @private
*/
enabled: true,
/**
* @type {string?}
* @private
*/
minDate: null,
/**
* @type {string?}
* @private
*/
maxDate: null,
/**
* @type {number?}
* @private
*/
tabIndex: null,
/**
* @type {string?}
* @private
*/
className: null,
/**
* @type {DateFormatterCallback?}
* @private
*/
valueFormatter: null
}
}; };
/** /**
@ -180,6 +411,10 @@ export class DateSelector {
create(element) { create(element) {
const opts = this._var.options; const opts = this._var.options;
const el = createDateInput(opts.minDate, opts.maxDate, element); const el = createDateInput(opts.minDate, opts.maxDate, element);
isPositive(opts.tabIndex) && el.setAttribute('tabindex', opts.tabIndex);
if (!nullOrEmpty(opts.className)) {
el.classList.add(opts.className);
}
if (element == null) { if (element == null) {
el.disabled = opts.enabled === false; el.disabled = opts.enabled === false;
} }
@ -241,6 +476,11 @@ export class DateSelector {
this._var.options.maxDate = date; this._var.options.maxDate = date;
} }
/**
* @private
* @param {Date} date
* @returns {Date | any}
*/
_getDate(date) { _getDate(date) {
if (date instanceof Date && !isNaN(date)) { if (date instanceof Date && !isNaN(date)) {
const year = date.getUTCFullYear(); const year = date.getUTCFullYear();

View File

@ -16,6 +16,8 @@ export interface DropdownOptions {
valueKey?: string; valueKey?: string;
/** 源码显示的关键字,默认值 `html` */ /** 源码显示的关键字,默认值 `html` */
htmlKey?: string; htmlKey?: string;
/** 源码显示的模板函数 */
htmlTemplate?: (item: DropdownItem) => HTMLElement;
/** 最大输入长度,默认值 `500` */ /** 最大输入长度,默认值 `500` */
maxLength?: number; maxLength?: number;
/** 是否允许多选 */ /** 是否允许多选 */
@ -117,8 +119,10 @@ export class Dropdown {
* 选中某个条目 * 选中某个条目
* @param selected 选中的值 * @param selected 选中的值
* @param silence 是否静默选中,即不触发 {@linkcode onSelected} 事件 * @param silence 是否静默选中,即不触发 {@linkcode onSelected} 事件
* @param ignoreCase 是否不区分大小写
* @returns 是否选中
*/ */
select(selected: string, silence?: boolean): void; select(selected: string, silence?: boolean, ignoreCase?: boolean): boolean | undefined;
/** /**
* 选中条目列表 * 选中条目列表
* @param selectedlist 选中的值的列表 * @param selectedlist 选中的值的列表

View File

@ -1,8 +1,7 @@
// import { r, global, contains, isPositive, nullOrEmpty } from "../utility";
import './css/dropdown.scss'; import './css/dropdown.scss';
import { r } from "../utility/lgres"; import { r as lang } from "../utility/lgres";
import { contains, nullOrEmpty } from "../utility/strings"; import { contains, nullOrEmpty } from "../utility/strings";
import { global, isPositive } from "../utility"; import { global, isPositive, throttle } from "../utility";
import { createElement } from "../functions"; import { createElement } from "../functions";
import { createCheckbox } from "./checkbox"; import { createCheckbox } from "./checkbox";
import { createIcon } from "./icon" import { createIcon } from "./icon"
@ -10,6 +9,7 @@ import { createIcon } from "./icon"
const SymbolDropdown = Symbol.for('ui-dropdown'); const SymbolDropdown = Symbol.for('ui-dropdown');
const DropdownItemHeight = 30; const DropdownItemHeight = 30;
let r = lang;
let dropdownGlobal = global[SymbolDropdown]; let dropdownGlobal = global[SymbolDropdown];
if (dropdownGlobal == null) { if (dropdownGlobal == null) {
@ -52,8 +52,13 @@ if (dropdownGlobal == null) {
}); });
} }
function selectItems(label, itemlist, htmlkey, textkey) { function selectItems(label, itemlist, template, htmlkey, textkey) {
const htmls = itemlist.map(it => it[htmlkey]); let htmls;
if (typeof template === 'function') {
htmls = itemlist.map(it => template.call(this, it));
} else {
htmls = itemlist.map(it => it[htmlkey]);
}
if (htmls.some(it => it instanceof HTMLElement)) { if (htmls.some(it => it instanceof HTMLElement)) {
label.replaceChildren(...htmls.filter(it => it != null).map(it => it.cloneNode(true))); label.replaceChildren(...htmls.filter(it => it != null).map(it => it.cloneNode(true)));
} else { } else {
@ -82,12 +87,24 @@ function filterSource(searchkeys, textkey, key, source) {
return source; return source;
} }
function getValue(it, valuekey, textkey) {
if (it == null) {
return null;
}
const value = it[valuekey];
if (value == null || value === '') {
return it[textkey];
}
return value;
}
/** /**
* 下拉列表参数对象 * 下拉列表参数对象
* @typedef DropdownOptions * @typedef DropdownOptions
* @property {string} [textKey=text] - 文本关键字 * @property {string} [textKey=text] - 文本关键字
* @property {string} [valueKey=value] - 值关键字 * @property {string} [valueKey=value] - 值关键字
* @property {string} [htmlKey=html] - 源码显示的关键字 * @property {string} [htmlKey=html] - 源码显示的关键字
* @property {Function} [htmlTemplate] - 模板创建函数
* @property {number} [maxLength=500] - 最大输入长度 * @property {number} [maxLength=500] - 最大输入长度
* @property {boolean} [multiSelect] - 是否允许多选 * @property {boolean} [multiSelect] - 是否允许多选
* @property {string} [selected] - 选中值 * @property {string} [selected] - 选中值
@ -124,12 +141,18 @@ export class Dropdown {
onCollapsed; onCollapsed;
constructor(options = {}) { constructor(options = {}) {
options.searchPlaceholder ??= r('searchHolder', 'Search...');
options.textKey ??= 'text'; options.textKey ??= 'text';
options.valueKey ??= 'value'; options.valueKey ??= 'value';
options.htmlKey ??= 'html'; options.htmlKey ??= 'html';
options.maxLength ??= 500; options.maxLength ??= 500;
this._var.options = options; this._var.options = options;
const getText = options.getText;
if (typeof getText === 'function') {
r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
}
options.searchPlaceholder ??= r('searchHolder', 'Search...');
} }
create() { create() {
@ -138,6 +161,9 @@ export class Dropdown {
// wrapper // wrapper
const wrapper = createElement('div', 'ui-drop-wrapper'); const wrapper = createElement('div', 'ui-drop-wrapper');
const dropId = String(Math.random()).substring(2); const dropId = String(Math.random()).substring(2);
if (options.wrapper instanceof HTMLElement) {
options.wrapper.dataset.dropId = dropId;
}
wrapper.dataset.dropId = dropId; wrapper.dataset.dropId = dropId;
dropdownGlobal[dropId] = this; dropdownGlobal[dropId] = this;
this._var.wrapper = wrapper; this._var.wrapper = wrapper;
@ -156,6 +182,7 @@ export class Dropdown {
const source = this.source; const source = this.source;
const count = source.length; const count = source.length;
const valuekey = this._var.options.valueKey; const valuekey = this._var.options.valueKey;
const textkey = this._var.options.textKey;
let index = source?.indexOf(this._var.selected); let index = source?.indexOf(this._var.selected);
if (isNaN(index) || index < -1) { if (isNaN(index) || index < -1) {
index = -1; index = -1;
@ -177,7 +204,7 @@ export class Dropdown {
index = count - 1; index = count - 1;
} }
} }
const target = source[index]?.[valuekey]; const target = getValue(source[index], valuekey, textkey);
if (target != null) { if (target != null) {
this.select(target); this.select(target);
} }
@ -204,7 +231,9 @@ export class Dropdown {
let label; let label;
if (options.input) { if (options.input) {
label = createElement('input', 'ui-drop-text'); label = createElement('input', 'ui-drop-text');
label.setAttribute('type', 'text'); label.type = 'text';
label.autocomplete = 'off';
label.draggable = false;
options.placeholder && label.setAttribute('placeholder', options.placeholder); options.placeholder && label.setAttribute('placeholder', options.placeholder);
isPositive(options.maxLength) && label.setAttribute('maxlength', options.maxLength); isPositive(options.maxLength) && label.setAttribute('maxlength', options.maxLength);
isPositive(options.tabIndex) && label.setAttribute('tabindex', options.tabIndex); isPositive(options.tabIndex) && label.setAttribute('tabindex', options.tabIndex);
@ -240,6 +269,8 @@ export class Dropdown {
get multiSelect() { return this._var.options.multiSelect } get multiSelect() { return this._var.options.multiSelect }
get ignoreAll() { return this._var.options.ignoreAll }
get disabled() { return this._var.wrapper == null || this._var.wrapper.querySelector('.ui-drop-header.disabled') != null } get disabled() { return this._var.wrapper == null || this._var.wrapper.querySelector('.ui-drop-header.disabled') != null }
set disabled(flag) { set disabled(flag) {
@ -271,7 +302,14 @@ export class Dropdown {
if (!Array.isArray(list)) { if (!Array.isArray(list)) {
return; return;
} }
this._var.source = list; const valuekey = this._var.options.valueKey;
function reduceItems(list, id, level = 0) {
if (!Array.isArray(list)) {
return [];
}
return list.reduce((array, item) => [...array, { __p: id, __level: level, ...item }, ...reduceItems(item.children, item[valuekey], level + 1)], []);
}
this._var.source = reduceItems(list);
if (this._expanded) { if (this._expanded) {
setTimeout(() => this._dropdown(), 120); setTimeout(() => this._dropdown(), 120);
} }
@ -281,18 +319,22 @@ export class Dropdown {
get selectedList() { return this._var.selectedList || [] } get selectedList() { return this._var.selectedList || [] }
select(selected, silence) { select(selected, silence, ignoreCase) {
if (typeof selected !== 'string') { if (typeof selected !== 'string') {
selected = String(selected); selected = String(selected);
} }
if (ignoreCase) {
selected = selected.toLowerCase();
}
if (this._var.lastSelected === selected) { if (this._var.lastSelected === selected) {
return false; return;
} }
this._var.lastSelected = selected; this._var.lastSelected = selected;
const valuekey = this._var.options.valueKey; const valuekey = this._var.options.valueKey;
const textkey = this._var.options.textKey; const textkey = this._var.options.textKey;
const template = this._var.options.htmlTemplate;
const htmlkey = this._var.options.htmlKey; const htmlkey = this._var.options.htmlKey;
let item = this.source.find(it => String(it[valuekey]) === selected); let item = this.source.find(it => (ignoreCase ? String(getValue(it, valuekey, textkey)).toLowerCase() : String(getValue(it, valuekey, textkey))) === selected);
if (this._var.options.input) { if (this._var.options.input) {
if (item == null) { if (item == null) {
item = { [valuekey]: selected }; item = { [valuekey]: selected };
@ -308,7 +350,12 @@ export class Dropdown {
this._var.label.innerText = ' '; this._var.label.innerText = ' ';
return false; return false;
} }
const html = item[htmlkey]; let html;
if (typeof template === 'function') {
html = template.call(this, item);
} else {
html = item[htmlkey];
}
if (html instanceof HTMLElement) { if (html instanceof HTMLElement) {
this._var.label.replaceChildren(html.cloneNode(true)); this._var.label.replaceChildren(html.cloneNode(true));
} else if (typeof html === 'string') { } else if (typeof html === 'string') {
@ -322,7 +369,7 @@ export class Dropdown {
} }
if (expanded) { if (expanded) {
for (let li of this._var.container.querySelectorAll('li[data-value]')) { for (let li of this._var.container.querySelectorAll('li[data-value]')) {
if (li.dataset.value === selected) { if ((ignoreCase ? li.dataset.value.toLowerCase() : li.dataset.value) === selected) {
li.classList.add('selected'); li.classList.add('selected');
break; break;
} }
@ -338,16 +385,18 @@ export class Dropdown {
if (!silence && typeof this.onSelected === 'function') { if (!silence && typeof this.onSelected === 'function') {
this.onSelected(item); this.onSelected(item);
} }
return true;
} }
selectlist(selectedlist, silence) { selectlist(selectedlist, silence) {
const source = this.source; const source = this.source;
const valuekey = this._var.options.valueKey; const valuekey = this._var.options.valueKey;
const textkey = this._var.options.textKey; const textkey = this._var.options.textKey;
const template = this._var.options.htmlTemplate;
const htmlkey = this._var.options.htmlKey; const htmlkey = this._var.options.htmlKey;
const itemlist = selectedlist.map(a => { const itemlist = selectedlist.map(a => {
const v = typeof a === 'string' ? a : String(a); const v = typeof a === 'string' ? a : String(a);
let item = source.find(it => String(it[valuekey]) === v); let item = source.find(it => String(getValue(it, valuekey, textkey)) === v);
if (item == null) { if (item == null) {
item = { item = {
[valuekey]: v, [valuekey]: v,
@ -358,10 +407,10 @@ export class Dropdown {
}); });
if (itemlist.length === 0) { if (itemlist.length === 0) {
this._var.selectedList = null; this._var.selectedList = null;
this._var.label.innerText = none; this._var.label.innerText = r('none', '( None )');
return false; return false;
} }
selectItems(this._var.label, itemlist, htmlkey, textkey); selectItems(this._var.label, itemlist, template, htmlkey, textkey);
this._var.selectedList = itemlist; this._var.selectedList = itemlist;
if (!silence && typeof this.onSelectedList === 'function') { if (!silence && typeof this.onSelectedList === 'function') {
this.onSelectedList(itemlist); this.onSelectedList(itemlist);
@ -379,7 +428,8 @@ export class Dropdown {
if (!options.input && options.search) { if (!options.input && options.search) {
const search = createElement('div', 'ui-drop-search'); const search = createElement('div', 'ui-drop-search');
const input = createElement('input'); const input = createElement('input');
input.setAttribute('type', 'text'); input.type = 'text';
input.className = 'ui-input';
isPositive(options.tabIndex) && input.setAttribute('tabindex', options.tabIndex); isPositive(options.tabIndex) && input.setAttribute('tabindex', options.tabIndex);
!nullOrEmpty(options.searchPlaceholder) && input.setAttribute('placeholder', options.searchPlaceholder); !nullOrEmpty(options.searchPlaceholder) && input.setAttribute('placeholder', options.searchPlaceholder);
input.addEventListener('input', e => { input.addEventListener('input', e => {
@ -391,7 +441,8 @@ export class Dropdown {
panel.appendChild(search); panel.appendChild(search);
} }
// list // list
const list = createElement('ul', 'ui-drop-list'); const list = createElement('div', 'ui-drop-list');
list.addEventListener('scroll', e => throttle(this._onlistscroll, 10, this, list, e.target.scrollTop), { passive: true });
if (!this.multiSelect) { if (!this.multiSelect) {
list.addEventListener('click', e => { list.addEventListener('click', e => {
let li = e.target; let li = e.target;
@ -463,45 +514,148 @@ export class Dropdown {
} }
} }
panel.classList.add('active'); panel.classList.add('active');
this._var.dropTop = 0;
panel.querySelector('.ui-drop-list').dispatchEvent(new Event('scroll'));
} else { } else {
panel.classList.remove('active'); panel.classList.remove('active');
} }
} }
_onlistscroll(list, top) {
const offset = (this.multiSelect && !this.ignoreAll) ? DropdownItemHeight : 0;
top -= (top % (DropdownItemHeight * 2)) + offset;
if (top < 0) {
top = 0;
} else {
let bottomTop = this._var.dropHeight - (20 * DropdownItemHeight);
if (bottomTop < 0) {
bottomTop = 0;
}
if (top > bottomTop) {
top = bottomTop;
}
}
if (this._var.dropTop !== top) {
this._var.dropTop = top;
const startIndex = top / DropdownItemHeight;
let array = this._var.currentSource;
if (startIndex + 20 < array.length) {
array = array.slice(startIndex, startIndex + 20);
} else {
array = array.slice(-20);
}
const content = list.querySelector('.drop-content');
content.replaceChildren();
this._dofilllist(content, array);
content.style.top = `${top + offset}px`;
}
}
_filllist(source) { _filllist(source) {
const list = this._var.container.querySelector('.ui-drop-list'); const list = this._var.container.querySelector('.ui-drop-list');
list.replaceChildren(); list.replaceChildren();
const multiselect = this.multiSelect; const height = source.length * DropdownItemHeight;
const allchecked = this._var.allChecked; this._var.dropHeight = height;
if (multiselect) { this._var.currentSource = source;
const holder = createElement('div', 'drop-holder');
holder.style.height = `${height}px`;
const content = createElement('div', 'drop-content');
if (this.multiSelect && !this.ignoreAll) {
list.appendChild( list.appendChild(
createElement('li', null, createElement('li', null,
createCheckbox({ createCheckbox({
label: r('allItem', '( All )'), label: r('allItem', '( All )'),
checked: allchecked, checked: this._var.allChecked,
customAttributes: { 'isall': '1' }, customAttributes: { 'isall': '1' },
onchange: e => this._triggerselect(e.target) onchange: e => this._triggerselect(e.target)
}) })
) )
); );
content.style.top = `${DropdownItemHeight}px`;
} else {
content.style.top = '0px';
} }
// TODO: virtual mode const multiselect = this.multiSelect;
const valuekey = this._var.options.valueKey; const valuekey = this._var.options.valueKey;
const textkey = this._var.options.textKey; const textkey = this._var.options.textKey;
const allchecked = this._var.allChecked;
const selectedlist = this.selectedList;
source.forEach((item, i) => {
let val = getValue(item, valuekey, textkey);
if (typeof val !== 'string') {
val = String(val);
}
if (multiselect) {
const selected = selectedlist.some(s => String(getValue(s, valuekey, textkey)) === val);
if (allchecked || selected) {
item.__checked = 1;
} else {
const indeterminate = selectedlist.some(s => this._contains(String(getValue(s, valuekey, textkey)), item, valuekey, textkey));
if (indeterminate) {
item.__checked = 2;
}
}
}
});
if (source.length > 20) {
source = source.slice(0, 20);
}
const scrolled = this._dofilllist(content, source);
list.append(holder, content);
if (scrolled != null) {
setTimeout(() => list.scrollTop = scrolled, 10);
}
}
_contains(it, item, valuekey, textkey) {
if (item.children?.length > 0) {
for (let t of item.children) {
if (it === getValue(t, valuekey, textkey)) {
return true;
}
if (this._contains(it, t, valuekey, textkey)) {
return true;
}
}
}
return false;
}
_dofilllist(content, array) {
const multiselect = this.multiSelect;
const valuekey = this._var.options.valueKey;
const textkey = this._var.options.textKey;
const template = this._var.options.htmlTemplate;
const htmlkey = this._var.options.htmlKey; const htmlkey = this._var.options.htmlKey;
const selected = this.selected; const selected = this.selected;
const selectedlist = this.selectedList;
let scrolled; let scrolled;
source.slice(0, 200).forEach((item, i) => { array.forEach((item, i) => {
let val = item[valuekey]; let val = getValue(item, valuekey, textkey);
if (typeof val !== 'string') { if (typeof val !== 'string') {
val = String(val); val = String(val);
} }
const li = createElement('li'); const li = createElement('li');
li.dataset.value = val; li.dataset.value = val;
li.setAttribute('title', item[textkey]); li.title = item[textkey];
if (item.__level > 0) {
li.style.marginLeft = `${item.__level * 24}px`;
}
const wrapper = createElement('span', 'li-wrapper',
createElement('span', span => {
// events
span.className = 'ui-expandor';
},
createIcon('fa-light', 'caret-down')
)
);
li.appendChild(wrapper);
let label; let label;
const html = item[htmlkey]; let html;
if (typeof template === 'function') {
html = template.call(this, item);
} else {
html = item[htmlkey];
}
if (html instanceof HTMLElement) { if (html instanceof HTMLElement) {
label = html; label = html;
} else if (typeof html === 'string') { } else if (typeof html === 'string') {
@ -509,77 +663,79 @@ export class Dropdown {
label.innerHTML = html; label.innerHTML = html;
} }
if (multiselect) { if (multiselect) {
const selected = selectedlist.some(s => String(s[valuekey]) === val);
if (label == null) { if (label == null) {
label = createElement('span'); label = createElement('span');
label.innerText = item[textkey]; label.innerText = item[textkey];
} }
const box = createCheckbox({ const box = createCheckbox({
label, label,
checked: allchecked || selected, checked: item.__checked === 1,
indeterminate: item.__checked === 2,
customAttributes: { customAttributes: {
'class': 'dataitem', 'class': 'dataitem',
'data-value': val 'data-value': val
}, },
onchange: e => this._triggerselect(e.target) onchange: e => this._triggerselect(e.target, item)
}); });
li.appendChild(box); wrapper.appendChild(box);
} else { } else {
if (label == null) { if (label == null) {
li.innerText = item[textkey]; label = createElement('span');
} else { label.innerHTML = item[textkey];
li.appendChild(label);
} }
wrapper.appendChild(label);
if (selected != null && String(selected[valuekey]) === val) { if (selected != null && String(selected[valuekey]) === val) {
scrolled = DropdownItemHeight * i; scrolled = DropdownItemHeight * i;
li.classList.add('selected'); li.classList.add('selected');
} }
} }
list.appendChild(li); content.appendChild(li);
}); });
if (scrolled != null) { return scrolled;
setTimeout(() => list.scrollTop = scrolled, 10);
}
} }
_triggerselect(checkbox) { _triggerselect(checkbox, item) {
let list; let list;
const valuekey = this._var.options.valueKey; const valuekey = this._var.options.valueKey;
const textkey = this._var.options.textKey; const textkey = this._var.options.textKey;
const template = this._var.options.htmlTemplate;
const htmlkey = this._var.options.htmlKey; const htmlkey = this._var.options.htmlKey;
if (checkbox.getAttribute('isall') === '1') { if (checkbox.getAttribute('isall') === '1') {
const allchecked = this._var.allChecked = checkbox.checked; const allchecked = this._var.allChecked = checkbox.checked;
const boxes = this._var.container.querySelectorAll('input.dataitem'); const boxes = this._var.container.querySelectorAll('input.dataitem');
boxes.forEach(box => box.checked = allchecked); boxes.forEach(box => box.checked = allchecked);
list = []; list = [];
} else if (checkbox.checked) { } else {
if (this._var.container.querySelectorAll('input.dataitem:not(:checked)').length === 0) { 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; this._var.allChecked = true;
this._var.container.querySelector('input[isall="1"]').checked = true; if (all != null) {
all.checked = true;
}
list = []; list = [];
} else { } else {
const source = this.source; list = source.filter(it => it.__checked);
list = [...this._var.container.querySelectorAll('input.dataitem:checked')]
.map(c => {
const v = c.dataset.value;
return source.find(it => String(it[valuekey]) === v);
})
.filter(it => it != null);
} }
} else { } else {
const val = checkbox.dataset.value; const val = checkbox.dataset.value;
if (this._var.allChecked) { if (this._var.allChecked) {
this._var.allChecked = false; this._var.allChecked = false;
this._var.container.querySelector('input[isall="1"]').checked = false; if (all != null) {
list = this.source.filter(it => String(it[valuekey]) !== val); all.checked = false;
}
list = this.source.filter(it => String(getValue(it, valuekey, textkey)) !== val);
} else { } else {
list = this.selectedList.filter(it => String(it[valuekey]) !== val); list = this.selectedList.filter(it => String(getValue(it, valuekey, textkey)) !== val);
}
} }
} }
if (this._var.allChecked) { if (this._var.allChecked) {
this._var.label.innerText = r('allItem', '( All )'); this._var.label.innerText = r('allItem', '( All )');
} else { } else {
selectItems(this._var.label, list, htmlkey, textkey); selectItems(this._var.label, list, template, htmlkey, textkey);
} }
this._var.selectedList = list; this._var.selectedList = list;
if (typeof this.onSelectedList === 'function') { if (typeof this.onSelectedList === 'function') {

View File

@ -44,6 +44,14 @@ export class GridColumn {
* @type {boolean} * @type {boolean}
*/ */
/**
* 标记该类型是否支持列头批量操作
* @member
* @name GridColumn.headerEditing
* @readonly
* @type {boolean}
*/
/** /**
* 创建显示单元格时调用的方法 * 创建显示单元格时调用的方法
* @param {GridColumnDefinition} col - 列定义对象 * @param {GridColumnDefinition} col - 列定义对象
@ -445,8 +453,9 @@ export class GridDropdownColumn extends GridColumn {
* @param {any} val * @param {any} val
* @param {GridItemWrapper} wrapper * @param {GridItemWrapper} wrapper
* @param {GridColumnDefinition} col * @param {GridColumnDefinition} col
* @param {Grid} grid
*/ */
static setValue(element, val, wrapper, col) { static setValue(element, val, wrapper, col, grid) {
if (element.tagName !== 'DIV') { if (element.tagName !== 'DIV') {
let source = this._getSource(wrapper, col); let source = this._getSource(wrapper, col);
if (source instanceof Promise) { if (source instanceof Promise) {
@ -460,19 +469,42 @@ export class GridDropdownColumn extends GridColumn {
if (drop == null) { if (drop == null) {
return; return;
} }
const ignoreCase = col.dropRestrictCase !== true;
if (drop.source == null || drop.source.length === 0) { if (drop.source == null || drop.source.length === 0) {
let source = this._getSource(wrapper, col); let source = this._getSource(wrapper, col);
if (source instanceof Promise) { if (source instanceof Promise) {
source.then(s => { source.then(s => {
drop.source = s; drop.source = s;
drop.select(val, true); drop.select(val, true, ignoreCase);
}) })
return; return;
} else if (source != null) { } else if (source != null) {
drop.source = source; drop.source = source;
} }
} }
drop.select(val, true); if (typeof val === 'string' && val !== '') {
const lVal = String(val).toLowerCase();
const item = drop.source.find(s => {
let v = s[col.dropOptions?.valueKey ?? 'value'];
if (ignoreCase) {
return String(v).toLowerCase() === lVal;
}
return v === val;
});
if (item == null) {
let text;
if (col.text == null && typeof col.filter === 'function') {
text = col.filter(wrapper.values, false, grid._var.refs.body);
} else {
text = val;
}
drop.source.push({
[col.dropOptions?.textKey ?? 'text']: text,
[col.dropOptions?.valueKey ?? 'value']: val
});
}
}
drop.select(val, true, ignoreCase);
} }
/** /**
@ -555,10 +587,12 @@ export class GridCheckboxColumn extends GridColumn {
/** /**
* @ignore * @ignore
* @param {Function} trigger * @param {Function} trigger
* @param {GridColumnDefinition} col
* @returns {HTMLElement} * @returns {HTMLElement}
*/ */
static createEdit(trigger) { static createEdit(trigger, col) {
const check = createCheckbox({ const check = createCheckbox({
switch: col.switch,
onchange: trigger onchange: trigger
}); });
return check; return check;
@ -756,9 +790,11 @@ export class GridDateColumn extends GridColumn {
* @ignore * @ignore
* @param {HTMLElement} element * @param {HTMLElement} element
* @param {(string | number | Date)} val * @param {(string | number | Date)} val
* @param {GridItemWrapper} _wrapper
* @param {GridColumnDefinition} col
*/ */
static setValue(element, val) { static setValue(element, val, _wrapper, col) {
setDateValue(element, val); setDateValue(element, val, col.dateDisplayFormatter);
} }
/** /**
@ -769,7 +805,10 @@ export class GridDateColumn extends GridColumn {
*/ */
static getValue(e, col) { static getValue(e, col) {
if (e.target.tagName === 'INPUT') { if (e.target.tagName === 'INPUT') {
return getDateValue(e.target, col.dateValueFormatter); return {
value: getDateValue(e.target, col.dateValueFormatter),
text: getDateValue(e.target, col.dateDisplayFormatter)
};
} }
return e.target.innerText; return e.target.innerText;
} }
@ -805,10 +844,32 @@ export class GridDateColumn extends GridColumn {
* * `"1/26/2024"` * * `"1/26/2024"`
* * `"638418240000000000"` * * `"638418240000000000"`
* * `new Date('2024-01-26')` * * `new Date('2024-01-26')`
* @param {string} [formatter] - 格式化格式,默认为 `"m/d/Y"`
*
* * Y - 年,例如 `2024`
* * y - 年的后两位,例如 `"24"`
* * n - 月,例如 `2`
* * m - 格式化成两位的月,例如 `"02"`
* * j - 日,例如 `4`
* * d - 格式化成两位的日,例如 `"04"`
* * t - 日期当月总天数,例如 `29`
* * L - 是否为闰年,例如 `1`
* * G - 小时,例如 `15`
* * g - 12 小时制的小时,例如 `3`
* * H - 格式化成两位的小时,例如 `"15"`
* * h - 格式化成两位的 12 小时制的小时,例如 `"03"`
* * A - 上下午,例如 `"PM"`
* * a - 小写的上下午,例如 `"pm"`
* * i - 格式化成两位的分钟,例如 `"39"`
* * s - 格式化成两位的秒,例如 `"20"`
* * u - 格式化成六位的毫秒,例如 `"023040"`
* * e - 时区描述字符串,例如 `"China Standard Time"`
* * O - 时区偏移字符串,例如 `"+0800"`
* * P - `"+08:00"` 这种格式的时区偏移字符串
* @returns {string} 格式化为 M/d/yyyy 的日期字符串 * @returns {string} 格式化为 M/d/yyyy 的日期字符串
*/ */
static formatDate(date) { static formatDate(date, formatter) {
return formatDate(date); return formatDate(date, formatter);
} }
/** /**

File diff suppressed because it is too large Load Diff

3
lib/ui/icon.d.ts vendored
View File

@ -3,8 +3,9 @@
* @param type 样式分类,可以是 ['`fa-light`', '`fa-regular`', '`fa-solid`'] 其中之一 * @param type 样式分类,可以是 ['`fa-light`', '`fa-regular`', '`fa-solid`'] 其中之一
* @param id 图标 id * @param id 图标 id
* @param style 样式表对象 * @param style 样式表对象
* @param action 点击回调事件
*/ */
export function createIcon(type: string, id: string, style?: { [key: string]: string }): SVGSVGElement export function createIcon(type: string, id: string, style?: { [key: string]: string } | ((icon: SVGSVGElement) => { [key: string]: string }), action?: (this: SVGSVGElement, ev: MouseEvent) => any): SVGSVGElement
/** /**
* 修改矢量图标 * 修改矢量图标
* @param svg 矢量图标元素 * @param svg 矢量图标元素

File diff suppressed because one or more lines are too long

6
lib/ui/media.d.ts vendored
View File

@ -24,3 +24,9 @@ export function createVideo(url: string): HTMLVideoElement
* @param icon 图标,默认为 `file-alt` * @param icon 图标,默认为 `file-alt`
*/ */
export function createFile(url: string, icon?: string): HTMLDivElement export function createFile(url: string, icon?: string): HTMLDivElement
/**
* 创建联动视频元素
* @param urls 视频 url 数组
*/
export function createVideoList(urls: string[]): HTMLDivElement

View File

@ -1,7 +1,7 @@
import "./css/media.scss"; import "./css/media.scss";
import { createElement } from "../functions"; import { createElement } from "../functions";
import { createIcon } from "./icon"; import { createIcon } from "./icon";
import { get } from "../utility"; import { get } from "../utility/request";
export function createPicture(url) { export function createPicture(url) {
return createElement('a', a => { return createElement('a', a => {
@ -62,14 +62,24 @@ function playPcm(samples, ended) {
} }
function getTimeLabel(time) { function getTimeLabel(time) {
time = Math.round(time); // time = Math.round(time);
return String(Math.floor(time / 60)).padStart(2, '0') + ':' + String(time % 60).padStart(2, '0'); // return String(Math.floor(time / 60)).padStart(2, '0') + ':' + String(time % 60).padStart(2, '0');
if (isNaN(time) || time < 0) {
return '0:00';
}
time = Math.floor(time);
const m = Math.floor(time / 60);
const h = Math.floor(m / 60);
if (h > 0) {
return h + ':' + String(m % 60).padStart(2, '0') + ':' + String(time % 60).padStart(2, '0');
}
return m + ':' + String(time % 60).padStart(2, '0');
} }
export function createAudio(mime, url) { export function createAudio(mime, url) {
if ((mime === 'audio/amr' || mime === '.amr') && typeof AMR !== 'undefined') { if ((mime === 'audio/amr' || mime === '.amr') && typeof AMR !== 'undefined') {
const timestamp = createElement('span', 'ui-media-timestamp'); const timestamp = createElement('span', 'ui-media-timestamp');
timestamp.textContent = '00:00 / 00:00'; timestamp.textContent = '0:00 / 0:00';
let context; let context;
let timer; let timer;
return createElement('div', 'ui-media-audio', return createElement('div', 'ui-media-audio',
@ -80,7 +90,7 @@ export function createAudio(mime, url) {
clearInterval(timer); clearInterval(timer);
context.close(); context.close();
context = null; context = null;
timestamp.textContent = '00:00 / 00:00'; timestamp.textContent = '0:00 / 0:00';
button.className = 'play'; button.className = 'play';
button.replaceChildren(createIcon('fa-solid', 'play')); button.replaceChildren(createIcon('fa-solid', 'play'));
return; return;
@ -92,7 +102,7 @@ export function createAudio(mime, url) {
.then(r => playPcm(r, ctx => { .then(r => playPcm(r, ctx => {
context = null; context = null;
clearInterval(timer); clearInterval(timer);
timestamp.textContent = '00:00 / ' + getTimeLabel(ctx.duration); timestamp.textContent = '0:00 / ' + getTimeLabel(ctx.duration);
button.className = 'play'; button.className = 'play';
button.replaceChildren(createIcon('fa-solid', 'play')); button.replaceChildren(createIcon('fa-solid', 'play'));
})) }))
@ -140,3 +150,302 @@ export function createFile(url, icon = 'file-alt') {
}) })
); );
} }
/**
* 创建联动视频元素
* @param {string[]} urls - 视频 url 数组
* @param {any} [options] - 播放参数
* @param {boolean} options.[autoPlay] - 是否自动播放
* @param {boolean} options.[autoFullScreen] - 是否自动全屏
* @param {boolean} options.[autoLoop] - 是否循环播放
* @param {Function} options.[onLoaded] - 视频加载完成回调
* @param {Function} [callback] - 视频元素处理回调函数
* @returns {HTMLDivElement} 返回联动视频元素
*/
export function createVideoList(urls, options, callback) {
if (!Array.isArray(urls)) {
urls = [urls];
}
const length = urls.length;
const container = createElement('div', 'ui-media-video-container');
let seekBufferBar;
let seekProgressBar;
let playIcon;
let timeLabel;
let volumeProgressBar;
let volumeIcon;
const videos = Array(length);
const waiting = Array(length);
let prepared = 0;
let ended = 0;
let playing;
let duration = -1;
let durationLabel;
let seekBarWidth;
let mousing;
const controller = createElement('div', 'ui-video-control active',
createElement('div', seek => {
seek.className = 'ui-video-bar seek-bar';
seek.addEventListener('mousedown', e => {
if (prepared < length || waiting.find(p => p) != null) {
return;
}
const width = seekBarWidth = seek.offsetWidth;
const currentTime = Math.min(Math.max(e.offsetX * duration / width, 0), duration);
videos.forEach(video => typeof video.fastSeek === 'function' ?
video.fastSeek(currentTime) :
(video.currentTime = currentTime));
mousing = true;
e.stopPropagation();
});
seek.addEventListener('mousemove', e => {
if (mousing) {
if (!e.buttons) {
mousing = false;
} else {
const v = Math.min(Math.max(e.offsetX / seekBarWidth, 0), 1);
seekProgressBar.style.width = `${(v * 100).toFixed(2)}%`;
}
}
});
seek.addEventListener('mouseup', e => {
if (mousing) {
mousing = false;
const currentTime = Math.min(Math.max(e.offsetX * duration / seekBarWidth, 0), duration);
videos.forEach(video => typeof video.fastSeek === 'function' ?
video.fastSeek(currentTime) :
(video.currentTime = currentTime));
}
});
},
createElement('div', 'ui-video-duration seek-duration'),
seekBufferBar = createElement('div', 'seek-buffers'),
seekProgressBar = createElement('div', 'ui-video-progress seek-progress')
),
playIcon = createElement('div', 'ui-video-icon play-icon',
createIcon('fa-solid', 'play'),
createIcon('fa-solid', 'pause')
),
timeLabel = createElement('div', 'ui-video-time-label'),
createElement('div', 'ui-video-volume-container',
createElement('div', volume => {
volume.className = 'ui-video-bar volume-bar';
volume.addEventListener('mousedown', e => {
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
const video = videos[0];
if (video != null) {
video.volume = v;
}
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
mousing = true;
e.stopPropagation();
});
volume.addEventListener('mousemove', e => {
if (mousing) {
if (!e.buttons) {
mousing = false;
} else {
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
volumeProgressBar.style.width = `${(v * 100).toFixed(2)}%`;
const video = videos[0];
if (video != null) {
video.volume = v;
}
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
}
}
});
volume.addEventListener('mouseup', e => {
if (mousing) {
mousing = false;
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
const video = videos[0];
if (video != null) {
video.volume = v;
}
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
}
});
},
createElement('div', 'ui-video-duration video-duration'),
volumeProgressBar = createElement('div', 'ui-video-progress video-progress')
),
volumeIcon = createElement('div', 'ui-video-icon volume-icon',
createIcon('fa-solid', 'volume'),
createIcon('fa-solid', 'volume-mute')
)
)
);
controller.addEventListener('mousedown', () => {
if (prepared < length || waiting.find(p => p) != null) {
// not prepared.
return;
}
if (playing) {
videos.forEach(video => video.pause());
} else {
ended = 0;
videos.forEach(video => video.play().catch(() => { }));
}
});
volumeIcon.addEventListener('mousedown', e => {
if (volumeIcon.classList.contains('disabled')) {
return;
}
const video = videos[0];
if (video == null) {
return;
}
if (volumeIcon.classList.contains('muted')) {
video.volume = 1;
volumeIcon.classList.remove('muted');
} else {
video.volume = 0;
volumeIcon.classList.add('muted');
}
e.stopPropagation();
});
if (length === 1) {
controller.append(createElement('div', icon => {
icon.className = 'ui-video-icon fullscreen-icon';
icon.addEventListener('mousedown', e => {
if (prepared < length) {
e.stopPropagation();
return;
}
const video = videos[0];
if (video != null) {
if (document.fullscreenElement == null) {
video.requestFullscreen().catch(() => { });
} else if (typeof document.exitFullscreen === 'function') {
document.exitFullscreen();
}
}
});
},
createIcon('fa-regular', 'expand')
));
} else {
controller.classList.add('no-fullscreen');
}
const content = createElement('div', 'ui-video-content');
container.append(content, controller);
urls.forEach((url, i) => {
const video = createElement('video');
videos[i] = video;
video.src = url;
video.addEventListener('durationchange', () => {
const d = video.duration;
if (d > duration) {
duration = d;
durationLabel = getTimeLabel(d);
timeLabel.innerText = '0:00 / ' + durationLabel;
}
});
video.addEventListener('loadeddata', () => {
if (i === 0) {
volumeProgressBar.style.width = `${(video.volume * 100).toFixed(2)}%`;
} else {
video.volume = 0;
}
prepared += 1;
if (prepared >= length) {
if (options?.autoPlay) {
// auto play
videos.forEach(v => v.play().catch(() => { }));
if (options?.autoFullScreen && length === 1 && document.fullscreenElement == null) {
video.requestFullscreen().catch(() => { });
}
}
if (typeof options?.onLoaded === 'function') {
options.onLoaded();
}
}
});
video.addEventListener('progress', () => {
const buffered = video.buffered;
for (let i = 0; i < buffered.length; ++i) {
let buffer = seekBufferBar.children[i];
if (buffer == null) {
seekBufferBar.append(buffer = createElement('div', 'ui-video-buffer'));
}
const start = buffered.start(i) * 100 / duration;
const end = buffered.end(i) * 100 / duration;
buffer.style.left = `${start.toFixed(2)}%`;
buffer.style.width = `${(end - start).toFixed(2)}%`;
}
for (let i = seekBufferBar.children.length - 1; i >= buffered.length; i -= 1) {
seekBufferBar.children[i].remove();
}
});
video.addEventListener('canplaythrough', () => {
const w = video.parentElement.querySelector('.ui-video-waiting');
if (w != null) {
w.style.opacity = 0;
}
if (waiting[i]) {
waiting[i] = false;
if (waiting.find(p => p) == null) {
videos.forEach(v => v.play().catch(() => { }));
}
}
});
video.addEventListener('pause', () => {
playing = false;
controller.classList.add('active');
playIcon.classList.remove('pause');
});
video.addEventListener('playing', () => {
playing = true;
controller.classList.remove('active');
playIcon.classList.add('pause');
});
video.addEventListener('waiting', () => {
waiting[i] = true;
const w = video.parentElement.querySelector('.ui-video-waiting');
if (w != null) {
w.style.opacity = 1;
}
videos.forEach(v => v.pause());
});
video.addEventListener('ended', () => {
ended += 1;
if (ended >= length) {
if (options?.autoLoop) {
videos.forEach(v => v.play().catch(() => { }));
} else {
playing = false;
controller.classList.add('active');
seekProgressBar.style.width = '0';
playIcon.classList.remove('pause');
timeLabel.innerText = '0:00 / ' + durationLabel;
}
}
});
if (i === 0) {
video.addEventListener('timeupdate', () => {
seekProgressBar.style.width = `${(video.currentTime * 100 / duration).toFixed(2)}%`;
timeLabel.innerText = getTimeLabel(video.currentTime) + ' / ' + durationLabel;
});
video.addEventListener('volumechange', () => {
volumeProgressBar.style.width = `${(video.volume * 100).toFixed(2)}%`;
});
}
const wrapper = createElement('div', 'ui-video-wrapper',
video,
createElement('div', 'ui-video-waiting',
createIcon('fa-regular', 'spinner-third')
)
);
if (typeof callback === 'function') {
callback(wrapper);
}
content.append(wrapper);
});
return container;
}

26
lib/ui/popup.d.ts vendored
View File

@ -7,6 +7,8 @@ interface PopupOptions {
/** 弹出框标题,可以是文本或者 html 元素 */ /** 弹出框标题,可以是文本或者 html 元素 */
title: string | HTMLElement; title: string | HTMLElement;
/** 是否持久化显示 */
persistent?: boolean;
/** 是否包含遮罩层,默认为 `true` */ /** 是否包含遮罩层,默认为 `true` */
mask?: boolean; mask?: boolean;
/** 遮罩层 z-index */ /** 遮罩层 z-index */
@ -61,15 +63,31 @@ interface PopupOptions {
/** /**
* 弹出框关闭时的回调函数 * 弹出框关闭时的回调函数
*/ */
resolve?: () => void; resolve?: (this: Popup, result: { result: any, popup: Popup }) => void;
} }
export class Popup { export class Popup {
constructor(opts?: PopupOptions); constructor(opts?: PopupOptions);
get container(): HTMLElement;
get title(): string;
set title(title: string);
get loading(): boolean;
set loading(flag: boolean);
get rect(): { collapsed: boolean, left: number, top: number, width: number, height: number };
set rect(r: { collapsed?: boolean, left?: number, top?: number, width?: number, height?: number });
close(result?: any, animation?: boolean): void;
create(): HTMLDivElement;
show(parent?: HTMLElement, hidden?: boolean): Promise<HTMLElement> | undefined;
} }
interface PopupButton { interface PopupButton {
tabIndex: number; className?: string;
tabIndex?: number;
key: string; key: string;
text: string; text: string;
trigger: (this: Popup) => boolean | Promise<boolean>; trigger: (this: Popup) => boolean | Promise<boolean>;
@ -85,12 +103,14 @@ interface PopupIconTypes {
} }
interface PopupButtonResult { interface PopupButtonResult {
key: string; result: string;
popup: Popup; popup: Popup;
} }
export function createPopup(title: string | HTMLElement, content: string | HTMLElement, ...buttons: PopupButton[]): Popup export function createPopup(title: string | HTMLElement, content: string | HTMLElement, ...buttons: PopupButton[]): Popup
export function resolvePopup(wrapper: string | HTMLElement, callback?: Function, removable?: boolean, zIndex?: number): Popup
export function showAlert(title: string | HTMLElement, message: string, iconType?: keyof PopupIconTypes, parent?: HTMLElement): Promise<void> export function showAlert(title: string | HTMLElement, message: string, iconType?: keyof PopupIconTypes, parent?: HTMLElement): Promise<void>
export function showConfirm(title: string | HTMLElement, content: string | HTMLElement, buttons: PopupButton[], iconType?: keyof PopupIconTypes, parent?: HTMLElement): Promise<PopupButtonResult> export function showConfirm(title: string | HTMLElement, content: string | HTMLElement, buttons: PopupButton[], iconType?: keyof PopupIconTypes, parent?: HTMLElement): Promise<PopupButtonResult>

View File

@ -1,9 +1,10 @@
import "./css/popup.scss"; import "./css/popup.scss";
import { r } from "../utility/lgres"; import { r as lang } from "../utility/lgres";
import { nullOrEmpty } from "../utility/strings"; import { nullOrEmpty } from "../utility/strings";
import { global } from "../utility"; import { global } from "../utility";
import { createElement } from "../functions"; import { createElement } from "../functions";
import { createIcon, changeIcon } from "./icon"; import { createIcon, changeIcon } from "./icon";
import { requestAnimationFrame } from "../ui";
const ResizeMods = { const ResizeMods = {
right: 1, right: 1,
@ -51,6 +52,30 @@ export class Popup {
get container() { return this._var.mask.querySelector('.ui-popup-container') } get container() { return this._var.mask.querySelector('.ui-popup-container') }
get title() { return this._var.option.title }
set title(title) {
const element = this._var.mask?.querySelector('.ui-popup-container .ui-popup-header .ui-popup-header-title');
if (element != null) {
element.innerText = title;
}
this._var.option.title = title;
}
get loading() { return this._var.mask?.querySelector('.ui-popup-body>.ui-popup-loading')?.style?.visibility === 'visible' }
set loading(flag) {
let loading = this._var.mask?.querySelector('.ui-popup-body>.ui-popup-loading');
if (loading == null) {
return;
}
if (flag === false) {
loading.style.visibility = 'hidden';
loading.style.opacity = 0;
} else {
loading.style.visibility = 'visible';
loading.style.opacity = 1;
}
}
get rect() { get rect() {
const container = this.container; const container = this.container;
if (container == null) { if (container == null) {
@ -105,25 +130,41 @@ export class Popup {
} }
} }
close(animation = true) { close(result = null, animation = true) {
const option = this._var.option;
const mask = this._var.mask; const mask = this._var.mask;
const doClose = () => {
if (option.persistent) {
mask.style.display = 'none';
} else {
mask.remove();
this._var.mask = null;
}
}
if (animation) { if (animation) {
mask.classList.add('ui-popup-active'); mask.classList.add('ui-popup-active');
mask.style.opacity = 0; mask.style.opacity = 0;
setTimeout(() => { mask.remove(); }, 120); setTimeout(() => { doClose(); }, 120);
} else { } else {
mask.remove(); doClose();
} }
if (typeof this._var.option.onMasking === 'function') { if (typeof option.onMasking === 'function') {
this._var.option.onMasking.call(this, false); option.onMasking.call(this, false);
} }
if (typeof this._var.option.resolve === 'function') { if (typeof option.resolve === 'function') {
this._var.option.resolve(); option.resolve.call(this, {
result,
popup: this
});
} }
} }
/**
* 创建 Popup 面板
* @returns {HTMLDivElement} 返回遮罩元素(顶层元素)
*/
create() { create() {
const mask = createElement('div', 'ui-popup-mask'); const mask = createElement('div', 'ui-popup-mask ui-popup-active');
const option = this._var.option; const option = this._var.option;
if (option.mask === false) { if (option.mask === false) {
mask.classList.add('ui-popup-transparent'); mask.classList.add('ui-popup-transparent');
@ -271,6 +312,9 @@ export class Popup {
container.appendChild( container.appendChild(
createElement('div', 'ui-popup-footer', ...option.buttons.map((b, i) => { createElement('div', 'ui-popup-footer', ...option.buttons.map((b, i) => {
const button = createElement('button', 'ui-popup-button'); const button = createElement('button', 'ui-popup-button');
if (b.className != null) {
button.classList.add(b.className);
}
if (b.tabIndex > 0) { if (b.tabIndex > 0) {
button.tabIndex = b.tabIndex; button.tabIndex = b.tabIndex;
} else { } else {
@ -283,14 +327,14 @@ export class Popup {
if (typeof result?.then === 'function') { if (typeof result?.then === 'function') {
result.then(r => { result.then(r => {
if (r !== false) { if (r !== false) {
this.close(); this.close(r);
} }
}).catch(reason => console.warn(reason)); }).catch(reason => console.warn(reason));
} else if (result !== false) { } else if (result !== false) {
this.close(); this.close(result);
} }
} else { } else {
this.close(); this.close(b.key ?? i);
} }
}); });
return button; return button;
@ -354,16 +398,20 @@ export class Popup {
return mask; return mask;
} }
show(parent = document.body) { show(parent = document.body, hidden = false) {
if (parent == null) { if (parent == null) {
return; return;
} }
let mask = this._var.mask ?? this.create(); let mask = this._var.mask;
if (mask == null) {
mask = this._var.mask = this.create();
}
if (mask.parentElement == null) {
// const exists = [...parent.children].filter(e => e.classList.contains('ui-popup-mask')); // const exists = [...parent.children].filter(e => e.classList.contains('ui-popup-mask'));
const exists = parent.querySelectorAll('.ui-popup-mask'); const exists = parent.querySelectorAll('.ui-popup-mask');
let zindex = 0; let zindex = 0;
for (let ex of exists) { for (let ex of exists) {
let z = parseInt(ex.style.zIndex); let z = parseInt(global.getComputedStyle(ex).zIndex);
if (!isNaN(z) && z > zindex) { if (!isNaN(z) && z > zindex) {
zindex = z; zindex = z;
} }
@ -372,6 +420,11 @@ export class Popup {
mask.style.zIndex = String(zindex + 1); mask.style.zIndex = String(zindex + 1);
} }
parent.appendChild(mask); parent.appendChild(mask);
if (hidden === true) {
mask.style.display = 'none';
return Promise.resolve(mask);
}
}
if (this._var.option.mask === false) { if (this._var.option.mask === false) {
// calculator position // calculator position
const container = this.container; const container = this.container;
@ -379,27 +432,14 @@ export class Popup {
container.style.top = String((parent.offsetHeight - container.offsetHeight) / 2) + 'px'; container.style.top = String((parent.offsetHeight - container.offsetHeight) / 2) + 'px';
} }
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { mask.style.display = '';
requestAnimationFrame(() => {
mask.classList.remove('ui-popup-active');
mask.style.opacity = 1; mask.style.opacity = 1;
this.container.focus(); this.container.focus();
resolve(mask); setTimeout(() => resolve(mask), 120);
}, 0); });
}); });
}
get loading() { return this._var.mask?.querySelector('.ui-popup-body>.ui-popup-loading')?.style?.visibility === 'visible' }
set loading(flag) {
let loading = this._var.mask?.querySelector('.ui-popup-body>.ui-popup-loading');
if (loading == null) {
return;
}
if (flag === false) {
loading.style.visibility = 'hidden';
loading.style.opacity = 0;
} else {
loading.style.visibility = 'visible';
loading.style.opacity = 1;
}
} }
_resize(mod, e) { _resize(mod, e) {
@ -497,6 +537,43 @@ export function createPopup(title, content, ...buttons) {
return popup; return popup;
} }
/**
* 解析对话框元素
* @param {HTMLElement | string} wrapper - 解析该 `.dialog` 元素
* @param {Function} [callback] - 关闭对话框时的回调
* @param {boolean} [removable] - 是否可移除
* @param {number} [zIndex] - 对话框默认 `z-index`
* @returns {Popup} 返回弹出框字典
*/
export function resolvePopup(wrapper, callback, removable, zIndex) {
if (typeof wrapper === 'string') {
wrapper = document.querySelector(wrapper);
}
if (wrapper == null) {
return null;
}
if (!wrapper.classList.contains('dialog')) {
return null;
}
const title = wrapper.querySelector('.dialog-title>.title')?.innerText;
const content = wrapper.querySelector('.dialog-title+div');
const buttons = [...wrapper.querySelectorAll('.dialog-func>input[type="button"]')].reverse().map(b => ({
tabIndex: b.tabIndex,
text: b.value,
trigger: b.onclick == null ? null : (popup => (b.onclick.call(popup), false))
}));
const popup = new Popup({
title,
content,
persistent: !removable,
resolve: typeof callback === 'function' ? (result => callback(result)) : null,
zIndex: wrapper.zIndex ?? zIndex,
buttons
});
popup.show(document.body, true);
return popup;
}
const iconTypes = { const iconTypes = {
'info': 'info-circle', 'info': 'info-circle',
'information': 'info-circle', 'information': 'info-circle',
@ -507,6 +584,7 @@ const iconTypes = {
} }
export function showAlert(title, message, iconType = 'info', parent = document.body) { export function showAlert(title, message, iconType = 'info', parent = document.body) {
const r = typeof GetTextByKey === 'function' ? GetTextByKey : lang;
return new Promise(resolve => { return new Promise(resolve => {
const popup = new Popup({ const popup = new Popup({
title, title,
@ -516,7 +594,7 @@ export function showAlert(title, message, iconType = 'info', parent = document.b
), ),
resolve, resolve,
buttons: [ buttons: [
{ text: r('ok', 'OK'), trigger: resolve } { text: r('ok', 'OK') }
] ]
}); });
popup.show(parent).then(mask => { popup.show(parent).then(mask => {
@ -527,6 +605,7 @@ export function showAlert(title, message, iconType = 'info', parent = document.b
} }
export function showConfirm(title, content, buttons, iconType = 'question', parent = document.body) { export function showConfirm(title, content, buttons, iconType = 'question', parent = document.body) {
const r = typeof GetTextByKey === 'function' ? GetTextByKey : lang;
return new Promise(resolve => { return new Promise(resolve => {
const wrapper = createElement('div', 'message-wrapper'); const wrapper = createElement('div', 'message-wrapper');
if (!nullOrEmpty(iconType)) { if (!nullOrEmpty(iconType)) {
@ -539,34 +618,23 @@ export function showConfirm(title, content, buttons, iconType = 'question', pare
title, title,
content: wrapper, content: wrapper,
resolve, resolve,
buttons: buttons?.map(b => { buttons: buttons?.map((b, i) => {
return { return {
text: b.text, text: b.text,
trigger: p => { trigger: p => {
let result; let result;
if (typeof b.trigger === 'function') { if (typeof b.trigger === 'function') {
result = b.trigger(p, b); result = b.trigger(p, b);
if (typeof result?.then === 'function') {
return result.then(r => {
r !== false && resolve(r);
return r;
});
}
result !== false && resolve(result);
} else { } else {
result = { result = b.key ?? i;
key: b.key,
popup: p
};
resolve(result);
} }
return result; return result;
} }
}; };
}) ?? }) ??
[ [
{ text: r('yes', 'Yes'), trigger: p => resolve({ key: 'yes', popup: p }) }, { key: 'yes', text: r('yes', 'Yes') },
{ text: r('no', 'No'), trigger: p => resolve({ key: 'no', popup: p }) } { key: 'no', text: r('no', 'No') }
] ]
}); });
popup.show(parent).then(mask => { popup.show(parent).then(mask => {

View File

@ -1,6 +1,6 @@
import './css/tooltip.scss'; import './css/tooltip.scss';
import { createElement } from "../functions"; import { createElement } from "../functions";
// import { global } from "../utility"; import { global } from '../utility';
const pointerHeight = 12; const pointerHeight = 12;
@ -99,7 +99,7 @@ export function setTooltip(container, content, flag = false, parent = null) {
let lastWidth = p.clientWidth; let lastWidth = p.clientWidth;
let lastHeight = p.clientHeight; let lastHeight = p.clientHeight;
while (p != null) { while (p != null) {
const overflow = window.getComputedStyle(p).overflow; const overflow = global.getComputedStyle(p).overflow;
if (overflow !== 'visible') { if (overflow !== 'visible') {
break; break;
} }

View File

@ -1,5 +1,5 @@
import { getCookie, setCookie, deleteCookie } from "./utility/cookie"; import { getCookie, setCookie, deleteCookie } from "./utility/cookie";
import { init, r, lang } from "./utility/lgres"; import { domLoad, init, r, lang } from "./utility/lgres";
import { get, post, upload } from "./utility/request"; import { get, post, upload } from "./utility/request";
import { nullOrEmpty, contains, endsWith, padStart, formatUrl, escapeHtml, escapeEmoji } from "./utility/strings"; import { nullOrEmpty, contains, endsWith, padStart, formatUrl, escapeHtml, escapeEmoji } from "./utility/strings";
@ -53,6 +53,27 @@ function isPhone(text) {
return false; return false;
} }
function getPasswordStrength(password) {
if (password == null || typeof password !== 'string') {
return 0;
}
if (password.length < 8) {
return 0;
}
let secure = 0;
if (/[0-9]/.test(password)) { secure++ }
if (/[a-z]/.test(password)) { secure++ }
if (/[A-Z]/.test(password)) { secure++ }
if (/[^0-9a-zA-Z]/.test(password)) { secure++ }
return secure;
}
function verifyPassword(password, min) {
min ??= 3;
const secure = getPasswordStrength(password);
return secure >= min;
}
export { export {
// cookie // cookie
getCookie, getCookie,
@ -83,5 +104,8 @@ export {
debounce, debounce,
truncate, truncate,
isEmail, isEmail,
isPhone isPhone,
getPasswordStrength,
verifyPassword,
domLoad
} }

View File

@ -1,3 +1,3 @@
export function getCookie(name: string): string export function getCookie(name: string): string
export function setCookie(name: string, value: string, expireDays?: Number): void export function setCookie(name: string, value: string, expireDays?: Number, host?: string, encode?: boolean): void
export function deleteCookie(name: string): void export function deleteCookie(name: string): void

View File

@ -1,8 +1,8 @@
export function setCookie(name, value, expireDays) { export function setCookie(name, value, expireDays, host, encode) {
if (name == null) { if (name == null) {
return; return;
} }
let extra = `; domain=${location.host}; path=/`; let extra = `; domain=${host ?? location.hostname}; path=/`;
if (expireDays != null) { if (expireDays != null) {
const d = new Date(); const d = new Date();
d.setTime(d.getTime() + (expireDays * 24 * 60 * 60 * 1000)); d.setTime(d.getTime() + (expireDays * 24 * 60 * 60 * 1000));
@ -11,7 +11,10 @@ export function setCookie(name, value, expireDays) {
if (/^(https|wss):$/.test(location.protocol)) { if (/^(https|wss):$/.test(location.protocol)) {
extra += '; secure'; extra += '; secure';
} }
document.cookie = `${name}=${encodeURIComponent(value)}${extra}`; if (encode !== false) {
value = encodeURIComponent(value);
}
document.cookie = `${name}=${value}${extra}`;
} }
export function getCookie(name) { export function getCookie(name) {

View File

@ -23,17 +23,21 @@ function getCurrentLgId() {
lgid = 'en'; lgid = 'en';
} }
switch (lgid) { switch (lgid) {
case 'en':
case 'en_au': case 'en_au':
case 'fr': case 'en_ca':
case 'fr_ca':
case 'zh_cn': case 'zh_cn':
return lgid; return lgid;
} }
const lang = lgid.split('_')[0]; const lang = lgid.split('_')[0];
switch (lang) { switch (lang) {
case 'en': case 'en':
case 'es':
case 'fr': case 'fr':
case 'pt':
return lang; return lang;
case 'zh':
return 'zh_cn';
} }
return 'en'; return 'en';
} }
@ -88,17 +92,37 @@ function applyLanguage(dom, result) {
} else { } else {
text.innerText = getLanguage(result, key, text.innerText); text.innerText = getLanguage(result, key, text.innerText);
} }
// delete text.dataset.lgid;
text.dataset.lgid = '';
} }
for (let title of dom.querySelectorAll('[data-title-lgid]')) { for (let title of dom.querySelectorAll('[data-title-lgid]')) {
const key = title.dataset.titleLgid; const key = title.dataset.titleLgid;
title.setAttribute('title', getLanguage(result, key, title.getAttribute('title'))); title.setAttribute('title', getLanguage(result, key, title.getAttribute('title')));
// delete title.dataset.titleLgid;
title.dataset.titleLgid = '';
} }
for (let holder of dom.querySelectorAll('[data-placeholder-lgid]')) { for (let holder of dom.querySelectorAll('[data-placeholder-lgid]')) {
const key = holder.dataset.placeholderLgid; const key = holder.dataset.placeholderLgid;
holder.setAttribute('placeholder', getLanguage(result, key, holder.getAttribute('placeholder'))); holder.setAttribute('placeholder', getLanguage(result, key, holder.getAttribute('placeholder')));
// delete holder.dataset.placeholderLgid;
holder.dataset.placeholderLgid = '';
} }
} }
export function domLoad() {
if (document.readyState === 'loading') {
return new Promise((resolve, reject) => {
let tid = setTimeout(() => reject('timeout'), 30000);
document.addEventListener('DOMContentLoaded', () => {
clearTimeout(tid);
tid = void 0;
resolve();
});
});
}
return Promise.resolve();
}
export async function init(dom = document.body, options = {}) { export async function init(dom = document.body, options = {}) {
const lgid = getCurrentLgId(); const lgid = getCurrentLgId();
let lgres = localStorage.getItem(getStorageKey(lgid)); let lgres = localStorage.getItem(getStorageKey(lgid));
@ -116,20 +140,7 @@ export async function init(dom = document.body, options = {}) {
} }
try { try {
if (document.readyState === 'loading') { await domLoad();
return await new Promise((resolve, reject) => {
let tid = setTimeout(() => reject('timeout'), 30000);
document.addEventListener('DOMContentLoaded', () => {
clearTimeout(tid);
tid = void 0;
if (typeof options.callback === 'function') {
options.callback(result);
}
applyLanguage(dom, result);
resolve(result);
});
});
}
if (typeof options.callback === 'function') { if (typeof options.callback === 'function') {
options.callback(result); options.callback(result);
} }

1193
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "ui-lib", "name": "ui-lib",
"private": true, "private": true,
"version": "1.0.2", "version": "1.0.7",
"type": "module", "type": "module",
"files": [ "files": [
"dist" "dist"
@ -28,14 +28,14 @@
"jsdoc-date": "jsdoc -c jsdoc-date.json" "jsdoc-date": "jsdoc -c jsdoc-date.json"
}, },
"devDependencies": { "devDependencies": {
"@mxssfd/typedoc-theme": "^1.1.3", "@mxssfd/typedoc-theme": "^1.1.6",
"clean-jsdoc-theme": "^4.2.18", "clean-jsdoc-theme": "^4.3.0",
"docdash": "^2.0.2", "docdash": "^2.0.2",
"jsdoc": "^4.0.2", "jsdoc": "^4.0.3",
"postcss-preset-env": "^9.5.2", "postcss-preset-env": "^9.6.0",
"sass": "^1.72.0", "sass": "^1.77.8",
"typedoc": "^0.25.12", "typedoc": "^0.26.5",
"vite": "^5.2.6", "vite": "^5.3.4",
"vite-plugin-externals": "^0.6.2" "vite-plugin-externals": "^0.6.2"
} }
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 430 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 651 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 510 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -6,11 +6,11 @@
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> --> <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UI Lib</title> <title>UI Lib</title>
<link href="/dist/ui.min.css" rel="stylesheet" /> <link href="dist/ui.min.css" rel="stylesheet" />
<script src="/amrnb.js"></script> <script src="amrnb.js"></script>
<script type="module" src="/main.js"></script> <script type="module" src="main.js"></script>
<script src="/dist/ui.min.js"></script> <script src="dist/ui.min.js"></script>
<script src="/dist/utility.min.js"></script> <script src="dist/utility.min.js"></script>
<style type="text/css"> <style type="text/css">
#container>.ui-grid { #container>.ui-grid {
width: 1000px; width: 1000px;

View File

@ -14,8 +14,8 @@ const libraries = [
lib: { lib: {
entry: './lib/ui.js', entry: './lib/ui.js',
name: 'lib-ui', name: 'lib-ui',
formats: ['umd'], formats: ['cjs', 'umd'],
fileName: (_format, name) => `${name}.min.js` fileName: (format, name) => format === 'cjs' ? `${name}-cjs.min.js` : `${name}.min.js`
}, },
rollupOptions: { rollupOptions: {
output: { output: {