Compare commits

..

44 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
87e3d6c81b fix: date UTC conversion issue
fix: `type.create` index offset issue
2024-03-27 02:30:04 +08:00
fd8899c597 package updates 2024-03-25 14:52:15 +08:00
f46e25952c fix: issue while using ui.min.js with requireJs
fix: clear `enabledDict` after `init`
fix: set `display: none` after tooltip holder disappeared
2024-03-21 15:30:22 +08:00
18f8cc322d fix: cell style issue while there is no style definition.
optimize: Grid.export
2024-03-13 17:02:04 +08:00
e07342a257 fix: tooltip issue about customer follower
change: Grid.export optimized (compression with `deflate`)
feature: ui-tab
feature: Popup.closable
2024-03-12 13:36:54 +08:00
f54eb3ac24 fix: wrap style in drop column.
fix: also change `DisplayValue` after changed.
feature: support uncompressed export.
change: add `editing` parameter in GridColumn.setEnabled.
2024-03-08 17:26:38 +08:00
70ab06190f change: structure of GridDropdownColumn 2024-03-08 11:02:04 +08:00
39378e9963 feature: caption help text
fix: editing style when turning column content wrap to `true`
2024-03-08 10:51:06 +08:00
3ea7ce62bb fix: reset startIndex after reload. 2024-03-08 08:27:30 +08:00
e75e763938 change: sample structure
fix: tooltip of some readonly column in editing mode
feature: adapt filter panel's height
2024-03-07 17:16:37 +08:00
168cae3ce1 feature: tooltip in the checkbox
feature: add GridColumn.getElement
feature: column property of `contentWrap` and `maxLines`
change: line height from 24px to 18px
fix: adapt enabled after cell changed
fix: wrap issue of filter panel
2024-03-07 12:54:00 +08:00
6fb7c3c769 move sample files 2024-03-05 15:03:53 +08:00
baec3a3959 clean code 2024-03-05 14:17:16 +08:00
6de4987360 fix: change vCell value after changed. 2024-03-05 08:43:07 +08:00
4096979d5d fix: vCell condition issue 2024-03-04 15:38:04 +08:00
1ed5b39308 optimize: change granularity is more fine-grained, only the changed cell elements are modified. 2024-03-04 15:17:31 +08:00
975d54fee1 . 2024-03-04 11:20:03 +08:00
a28b56b191 optimize: introduce vCell, render time while scrolling. 2024-03-04 11:18:08 +08:00
20a8fbae02 fix: body scrollTop issue when column filtered. 2024-03-01 08:54:54 +08:00
5e53d04174 fix: do not change scroll position when refreshing grid.
fix: change `button` to `span`, avoiding conflict styles.
2024-02-28 10:29:19 +08:00
38bad864e0 fix: header z-index issue.
fix: incorrect `onChanged` event trigger on `GridInputColumn`, `GridTextColumn` and `GridDateColumn`.
2024-02-26 16:11:42 +08:00
7940edbea2 fix: grid column border-bottom issue. 2024-02-23 11:01:43 +08:00
c21f62b7b8 fix: grid hover position issue. 2024-02-23 10:52:30 +08:00
71 changed files with 21979 additions and 11517 deletions

View File

@ -1,3 +1,36 @@
# [ui-lib].Grid
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) - 批量删除行数据

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 651 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 600 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 510 KiB

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UI Lib</title>
<script src="/amrnb.js"></script>
<script type="module" src="/main.js"></script>
<script src="/dist/ui.min.js"></script>
<script src="/dist/utility.min.js"></script>
</head>
<body>
<div id="container"></div>
</body>
</html>

View File

@ -2,9 +2,11 @@ import "./app/communications/style.scss";
import CustomerCommunication from "./app/communications/customer";
import InternalComment from "./app/communications/internal";
import CustomerRecordComment from "./app/communications/comments";
import { createHideMessageTitleButton } from "./app/communications/lib";
export {
CustomerCommunication,
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 { createBox, appendMedia } from "./lib";
import { createBox, appendMedia, createHideMessageTitleButton, createHideMessageCommentTail } from "./lib";
let r = lang;
@ -12,6 +12,8 @@ export default class CustomerRecordComment {
const getText = opt?.getText;
if (typeof getText === 'function') {
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
*/
@ -55,8 +71,10 @@ export default class CustomerRecordComment {
createElement('div', null,
createElement('div', div => {
div.className = 'title-module';
div.innerText = r('P_CR_COMMENTS', 'Comments');
})
div.innerText = r('FLTL_00584', 'Comments');
},
createHideMessageTitleButton(this, 'showCommentHidden')
)
),
[
createElement('button', button => {
@ -79,7 +97,7 @@ export default class CustomerRecordComment {
);
// enter box
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.addEventListener('input', () => {
const val = this.text;
@ -102,8 +120,8 @@ export default class CustomerRecordComment {
button.style.display = 'none';
}
button.appendChild(createIcon('fa-solid', 'paper-plane'));
// setTooltip(button, r('P_M3_SENDMESSAGE', 'Send Message'));
setTooltip(button, r('P_CU_POSTNOTE', 'Post Note'));
// setTooltip(button, r('FLTL_02692', 'Send Message'));
setTooltip(button, r('FLTL_02301', 'Post Note'));
button.addEventListener('click', () => {
if (typeof this._var.option.onAddComment === 'function') {
this._var.option.onAddComment(this.text);
@ -120,13 +138,20 @@ export default class CustomerRecordComment {
return this._var.container = container;
}
load(data) {
load(data, func, hisFunc, keep) {
const children = [];
if (data?.length > 0) {
const lastVisible = this._var.option.showCommentHidden;
for (let comment of data) {
const div = createElement('div', 'item-div');
if (comment.Hidden) {
div.classList.add('hidden-content');
if (!lastVisible) {
div.style.display = 'none';
}
}
// 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.className = 'item-poster';
@ -143,17 +168,19 @@ export default class CustomerRecordComment {
}
div.append(
content,
createElement('div', div => {
div.className = 'item-time';
div.innerText = comment.SubmitLocalDateStr;
})
createHideMessageCommentTail(
this, 'showCommentHidden',
comment, 'SubmitLocalDateStr',
func, hisFunc)
);
children.push(div);
}
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.scrollTop = this._var.message.scrollHeight
// setTimeout(() => this._var.message.scrollTop = this._var.message.scrollHeight, 0);
requestAnimationFrame(() => this._var.message.scrollTop = keep ? this._var.lastTop : this._var.message.scrollHeight);
}
}

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";
let r = lang;
@ -11,6 +11,8 @@ export class Contact {
const getText = option?.getText;
if (typeof getText === 'function') {
r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
}
}
@ -27,9 +29,9 @@ export class Contact {
});
const preferences = new Dropdown({ tabIndex: tabIndex + 2 });
preferences.source = [
{ value: '0', text: r('P_CR_TEXT', 'Text') },
{ value: '1', text: r('P_CR_EMAIL', 'Email') },
{ value: '2', text: r('P_CR_PHONE', 'Phone') }
{ value: '0', text: r('FLTL_02915', 'Text') },
{ value: '1', text: r('FLTL_01089', 'Email') },
{ value: '2', text: r('FLTL_02194', 'Phone') }
];
const contactEmail = createElement('input', input => {
input.type = 'email';
@ -55,7 +57,7 @@ export class Contact {
const buttons = [];
if (this._var.option.company) {
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,
trigger: () => {
const item = this.prepare();
@ -71,7 +73,7 @@ export class Contact {
}
buttons.push(
{
text: r('P_WO_WORKORDERONLY', 'Work Order Only'),
text: r('FLTL_03348', 'Work Order Only'),
// tabIndex: tabIndex + 8,
trigger: () => {
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
}
);
const popup = new Popup({
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 => {
wrapper.className = 'setting-wrapper';
wrapper.style.width = '500px';
},
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
),
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()
),
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
),
createElement('div', 'setting-item',
createElement('span', 'setting-label', r('P_WO_MOBILE_COLON', 'Mobile:')),
createElement('span', 'setting-label', r('FLTL_01932', 'Mobile:')),
contactMobile
),
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
),
createElement('div', 'setting-item',
createElement('span', 'setting-label', r('P_CR_NOTES_COLON', 'Notes:')),
createElement('span', 'setting-label', r('FLTL_02017', 'Notes:')),
contactNotes
)
),
@ -143,7 +145,7 @@ export class Contact {
contactNotes
};
const result = await popup.show(parent);
setTimeout(() => contactName.focus());
requestAnimationFrame(() => contactName.focus());
return result;
}
@ -154,24 +156,24 @@ export class Contact {
const phone = this._var.refs.contactMobile.value;
const opt = this._var.refs.checkOpt.querySelector('input').checked;
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)) {
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());
return null;
}
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());
return null;
}
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());
return null;
}
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());
return null;
}
@ -204,6 +206,8 @@ export class CustomerRecordContact {
const getText = option?.getText;
if (typeof getText === 'function') {
r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
}
}
@ -219,7 +223,7 @@ export class CustomerRecordContact {
),
buttons: [
{
text: r('P_WO_OK', 'OK'),
text: r('FLTL_02057', 'OK'),
key: 'ok',
trigger: () => {
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);
@ -240,12 +244,12 @@ export class CustomerRecordContact {
width: 40,
// enabled: item => !nullOrEmpty(item.ID)
},
{ key: 'Name', caption: r("P_CR_CONTACTNAME", "Contact Name"), width: 100 },
{ key: 'Email', caption: r("P_CR_CONTACTEMAIL", "Contact Email"), css: { 'width': 180, 'text-align': 'left' } },
{ key: 'MobilePhoneDisplayText', caption: r("P_CR_CONTACTMOBILE", "Contact Mobile"), width: 130 },
{ key: 'ContactPreferenceStr', caption: r("P_CR_CONTACTPREFERENCES", "Contact Preferences"), width: 100 },
{ key: 'OptOut', caption: r("P_CR_OPTOUT", "Opt Out"), type: Grid.ColumnTypes.Checkbox, width: 70, enabled: false, align: 'center' },
{ key: 'Notes', caption: r("P_CR_NOTES", "Notes"), width: 120 }
{ key: 'Name', caption: r('FLTL_00637', 'Contact Name'), width: 100 },
{ key: 'Email', caption: r('FLTL_00633', 'Contact Email'), css: { 'width': 180, 'text-align': 'left' } },
{ key: 'MobilePhoneDisplayText', caption: r('FLTL_00636', 'Contact Mobile'), width: 130 },
{ key: 'ContactPreferenceStr', caption: r('FLTL_00642', 'Contact Preferences'), width: 100 },
{ key: 'OptOut', caption: r('FLTL_02084', 'Opt Out'), type: Grid.ColumnTypes.Checkbox, width: 70, enabled: false, align: 'center' },
{ key: 'Notes', caption: r('FLTL_02012', 'Notes'), width: 120 }
];
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) });

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 { 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 Follower from "./follower";
@ -41,6 +41,8 @@ export default class CustomerCommunication {
const getText = opt?.getText;
if (typeof getText === 'function') {
r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
}
}
@ -69,6 +71,20 @@ export default class CustomerCommunication {
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 statusLinkEnabled() { return this._statusLink?.disabled !== true }
set statusLinkEnabled(flag) {
@ -199,7 +215,7 @@ export default class CustomerCommunication {
let tipstr;
if (c.OptOut || c.OptOut_BC || c.selected === false) {
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 {
switch (pref) {
@ -208,20 +224,20 @@ export default class CustomerCommunication {
icon = 'times';
if (c.MobilePhoneStatus === 412) {
// landline
tipstr = r('P_CU_LANDLINE', 'Landline');
tipstr = r('FLTL_01707', 'Landline');
}
} else {
icon = 'comment-lines';
}
method = r('P_CU_TEXTSTO_COLON', 'Texts to:');
method = r('FLTL_02924', 'Texts to:');
break;
case '2':
icon = 'phone';
tipstr = r('P_CU_NOMESSAGE', 'No Messages Sent');
tipstr = r('FLTL_01991', 'No Messages Sent');
break;
default:
icon = 'envelope';
method = r('P_CU_EMAILSTO_COLON', 'Emails to:');
method = r('FLTL_01109', 'Emails to:');
break;
}
}
@ -238,7 +254,7 @@ export default class CustomerCommunication {
this._var.contacts.appendChild(item);
let tip = tipstr || `${method} ${to}`;
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);
}
@ -262,6 +278,7 @@ export default class CustomerCommunication {
}
link.querySelector('input').disabled = flag;
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-followers').style.display = display;
// this._var.enter.disabled = flag === true;
@ -277,8 +294,13 @@ export default class CustomerCommunication {
if (this._var.container == null) {
return;
}
this._var.container.querySelector('.button-edit-contacts').style.display = flag === true ? 'none' : '';
this._var.container.querySelector('.button-edit-followers').style.display = flag === true ? 'none' : '';
const 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) {
this._var.option.companyName = name;
const div = this._var.container.querySelector('.title-company');
const companyCode = div.querySelector('.title-company-code');
if (companyCode != null) {
if (nullOrEmpty(name)) {
if (this._var.option.customerReadonly === true) {
div.style.display = 'none';
} else {
div.innerText = name;
div.style.display = '';
div.querySelector('.title-company-name').innerText = r('FLTL_01985', 'No Customer Assigned');
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
@ -301,15 +334,24 @@ export default class CustomerCommunication {
const option = this._var.option;
option.companyCode = code;
const div = this._var.container.querySelector('.title-company');
const companyCode = div.querySelector('.title-company-code');
if (companyCode != null) {
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 {
div.innerText = option.companyName;
if (!nullOrEmpty(code))
div.innerText = option.companyName + "/" + code;
div.style.display = '';
div.querySelector('.title-company-name').innerText = option.companyName;
if (nullOrEmpty(code)) {
companyCode.style.display = 'none';
} else {
companyCode.innerText = ' / ' + code;
companyCode.style.display = '';
}
}
}
div.style.display = '';
}
get followers() {
return [...this._var.followers.children].map(el => {
@ -322,7 +364,7 @@ export default class CustomerCommunication {
this._var.followers.replaceChildren();
if (followers?.length > 0) {
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);
for (let f of followers) {
if (f.OptOut) {
@ -333,10 +375,10 @@ export default class CustomerCommunication {
const email = String(f.Email).trim();
const tips = [];
if (f.SendEmail) {
tips.push(r('P_CU_EMAILSTO_COLON', 'Emails to:') + ` ${email}`);
tips.push(r('FLTL_01109', 'Emails to:') + ` ${email}`);
}
if (f.SendText) {
tips.push(r('P_CU_TEXTSTO_COLON', 'Texts to:') + ` ${mpDisplay}`);
tips.push(r('FLTL_02924', 'Texts to:') + ` ${mpDisplay}`);
}
let icon;
if (f.SendText && f.SendEmail) {
@ -360,13 +402,13 @@ export default class CustomerCommunication {
);
this._var.followers.appendChild(item);
if (span.scrollWidth > span.offsetWidth) {
tips.splice(0, 0, r('P_WO_NAME_COLON', 'Name:') + ` ${c.Name}`);
tips.splice(0, 0, r('FLTL_01970', 'Name:') + ` ${f.Name}`);
}
setTooltip(span, tips.join('\n'));
}
} else {
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.message.scrollTop = this._var.message.scrollHeight
@ -387,8 +429,8 @@ export default class CustomerCommunication {
uncheckedNode: createIcon('fa-regular', 'ban'),
onchange: function () {
setTooltip(checkAutoUpdate, this.checked ?
r('P_CU_AUTOUPDATESENABLED', 'Auto Updates Enabled') :
r('P_CU_AUTOUPDATESDISABLED', 'Auto Updates Disabled'));
r('FLTL_00420', 'Auto Updates Enabled') :
r('FLTL_00419', 'Auto Updates Disabled'));
}
});
if (option.autoUpdatesVisible === false) {
@ -403,8 +445,8 @@ export default class CustomerCommunication {
uncheckedNode: createIcon('fa-regular', 'unlink'),
onchange: function () {
setTooltip(checkLink, this.checked ?
r('P_WO_STATUSLINKINCLUDED', 'Status Link Included') :
r('P_WO_STATUSLINKEXCLUDED', 'Status Link Excluded'));
r('FLTL_02830', 'Status Link Included') :
r('FLTL_02829', 'Status Link Excluded'));
if (typeof option.onStatusLinkChanged === 'function') {
option.onStatusLinkChanged.call(This, this.checked);
}
@ -413,28 +455,57 @@ export default class CustomerCommunication {
if (option.statusLinkVisible === false) {
checkLink.style.display = 'none';
}
const container = createBox(
createElement('div', null,
createElement('div', div => {
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 => {
div.className = 'title-company';
// div.style.display = 'none';
},
createElement('span', span => {
span.className = 'title-company-name';
if (nullOrEmpty(option.companyName)) {
div.style.display = 'none';
span.innerText = r('FLTL_01985', 'No Customer Assigned');
} else {
if (nullOrEmpty(option.companyCode)) {
div.innerText = option.companyName;
span.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 {
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(checkLink, r('P_WO_STATUSLINKEXCLUDED', 'Status Link Excluded'))
setTooltip(checkAutoUpdate, r('FLTL_00420', 'Auto Updates Enabled')),
setTooltip(checkLink, r('FLTL_02829', 'Status Link Excluded'))
]
);
// contacts
@ -443,7 +514,7 @@ export default class CustomerCommunication {
this._var.followers = this._createFollowers(container, option);
// enter box
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;
enter.maxLength = option.maxLength;
// if (readonly === true) {
@ -515,7 +586,7 @@ export default class CustomerCommunication {
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 => {
input.type = 'text';
input.className = 'ui-input';
@ -560,11 +631,11 @@ export default class CustomerCommunication {
// button.style.display = 'none';
// }
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', () => {
const val = this.text;
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') {
option.onMasking(true);
p.then(() => option.onMasking(false));
@ -607,8 +678,9 @@ export default class CustomerCommunication {
button.style.display = 'none';
}
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', () => {
const editContacts = () => {
const pop = new Popup({
onMasking: option.onMasking,
title: createElement('div', div => {
@ -620,27 +692,34 @@ export default class CustomerCommunication {
div.className = 'ui-popup-move';
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 => {
div.className = 'title-company';
div.style.maxWidth = '540px';
if (nullOrEmpty(option.companyName)) {
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 => {
button.style.flex = '0 0 auto';
button.style.backgroundColor = 'rgb(1, 199, 172)';
button.style.marginRight = '10px';
button.className = 'roundbtn button-from-customer-record';
if (recordReadonly) {
if (recordReadonly || nullOrEmpty(option.companyName)) {
button.style.display = 'none';
}
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);
if (typeof option.onOpenSelectCRContacts === 'function') {
@ -717,7 +796,7 @@ export default class CustomerCommunication {
return false;
}
});
setTooltip(button, r('P_CU_SELECTFROMCUSTOMERRECORD', 'Select from Customer Record'))
setTooltip(button, r('FLTL_02657', 'Select from Customer Record'))
}),
createElement('button', button => {
button.style.flex = '0 0 auto';
@ -739,7 +818,7 @@ export default class CustomerCommunication {
onSave: item => {
const exists = this._var.gridContact.source.some(s => s.Name === item.Name && s.MobilePhone === item.MobilePhone);
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;
}
if (typeof option.onSave === 'function') {
@ -773,7 +852,7 @@ export default class CustomerCommunication {
});
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.fontWeight = 'bold';
div.innerText = r('P_CU_CONTACTSFROMCUSTOMERRECORD', 'Contacts from Customer Record');
div.innerText = r('FLTL_00657', 'Contacts from Customer Record');
}),
createElement('div', div => {
if (nullOrEmpty(option.companyName)) {
div.style.display = 'none';
}
div.className = 'contacts-record';
div.style.maxHeight = '200px';
// div.style.maxHeight = '200px';
div.style.width = '675px';
div.style.overflow = 'auto';
// div.style.overflow = 'auto';
}),
createElement('div', div => {
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 => {
div.className = 'contacts-wo';
div.style.maxHeight = '200px';
// div.style.maxHeight = '200px';
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]);
}
},
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 = {
@ -848,7 +927,7 @@ export default class CustomerCommunication {
key: 'edit',
...buttonCol,
text: 'edit',
tooltip: r('P_WO_EDIT', 'Edit'),
tooltip: r('FLTL_01032', 'Edit'),
events: {
onclick: function () {
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.gridWo.source.some(s => s !== this && s.Name === item.Name && s.MobilePhone === item.MobilePhone);
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;
}
if (typeof option.onSave === 'function') {
@ -914,15 +993,15 @@ export default class CustomerCommunication {
key: 'delete',
...buttonCol,
text: 'times',
tooltip: r('P_WO_DELETE', 'Delete'),
tooltip: r('FLTL_00791', 'Delete'),
events: {
onclick: function () {
showConfirm(
r('P_CU_REMOVECONTACT', 'Remove Contact'),
r('FLTL_02417', 'Remove Contact'),
createElement('div', null,
createElement('div', div => {
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 => {
div.style.display = 'flex';
@ -931,25 +1010,25 @@ export default class CustomerCommunication {
},
createRadiobox({
name: 'remove-type',
label: r('P_CUSTOMERRECORD', 'Customer Record'),
label: r('FLTL_00747', 'Customer Record'),
checked: true,
className: 'radio-customer-record'
}),
createRadiobox({
name: 'remove-type',
label: r('P_WORKORDER', 'Work Order')
label: r('FLTL_03317', 'Work Order')
})
)
),
[
{ key: 'ok', text: r('P_WO_OK', 'OK') },
{ key: 'cancel', text: r('P_WO_CANCEL', 'Cancel') }
{ key: 'ok', text: r('FLTL_02057', 'OK') },
{ key: 'cancel', text: r('FLTL_00499', 'Cancel') }
]
).then(result => {
if (result?.key === 'ok') {
const isRecord = result.popup.container.querySelector('.radio-customer-record>input').checked;
).then(r => {
if (r.result === 'ok') {
const isRecord = r.popup.container.querySelector('.radio-customer-record>input').checked;
if (typeof option.onDelete === 'function') {
option.onDelete(result.key, this, isRecord);
option.onDelete(r.result, this, isRecord);
}
const index = grid.source.indexOf(this);
if (index >= 0) {
@ -999,16 +1078,16 @@ export default class CustomerCommunication {
key: 'delete',
...buttonCol,
text: 'times',
tooltip: r('P_WO_DELETE', 'Delete'),
tooltip: r('FLTL_00791', 'Delete'),
events: {
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), [
{ key: 'continue', text: r('P_JS_CONTINUE', 'Continue') },
{ key: 'cancel', text: r('P_WO_CANCEL', 'Cancel') }
]).then(result => {
if (result?.key === 'continue') {
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('FLTL_00661', 'Continue') },
{ key: 'cancel', text: r('FLTL_00499', 'Cancel') }
]).then(r => {
if (r.result === 'continue') {
if (typeof option.onDelete === 'function') {
option.onDelete(result.key, this);
option.onDelete(r.result, this);
}
const index = gridWo.source.indexOf(this);
if (index >= 0) {
@ -1042,12 +1121,33 @@ export default class CustomerCommunication {
};
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 => {
div.className = 'bar-info';
div.innerText = r('P_CR_CONTACTINFORMATION', 'Contact Information');
div.innerText = r('FLTL_00635', 'Contact Information');
}),
createElement('div', div => {
if (option.contactCollapserVisible === false) {
@ -1091,12 +1191,12 @@ export default class CustomerCommunication {
button.style.display = 'none';
}
button.appendChild(createIcon('fa-solid', 'pen'));
setTooltip(button, r('P_CU_EDITFOLLOWERS', 'Edit Followers'));
setTooltip(button, r('FLTL_01054', 'Edit Followers'));
button.addEventListener('click', () => {
if (typeof option.onInitFollower === 'function') {
option.onInitFollower(this._var.data.followers).then(data => {
if (typeof data === 'string') {
showAlert(r('P_CUSTOMERRECORD', 'Customer Record'), data, 'warn');
showAlert(r('FLTL_00747', 'Customer Record'), data, 'warn');
return;
}
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);
});
}
@ -1136,7 +1236,7 @@ export default class CustomerCommunication {
'border-radius': '15px',
'padding': '4px'
})
), r('P_CU_COPIED', 'Copied')),
), r('FLTL_00667', 'Copied')),
createElement('div', 'bar-list',
followers,
buttonEditFollower
@ -1146,7 +1246,7 @@ export default class CustomerCommunication {
return followers;
}
load(data, contacts, followers) {
load(data, contacts, followers, func, hisFunc, keep) {
const children = [];
if (data?.length > 0) {
contacts ??= this._var.data.contacts;
@ -1159,45 +1259,52 @@ export default class CustomerCommunication {
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');
if (comment.Hidden) {
div.classList.add('hidden-content');
if (!lastVisible) {
div.style.display = 'none';
}
}
let name;
if (comm.IsReply && contacts?.length > 0) {
const c = isEmail(comm.Sender) ?
contacts.find(c => c.Email === comm.Sender) :
contacts.find(c => c.MobilePhone === comm.Sender);
if (comment.IsReply && contacts?.length > 0) {
const c = isEmail(comment.Sender) ?
contacts.find(c => c.Email === comment.Sender) :
contacts.find(c => c.MobilePhone === comment.Sender);
name = c?.Name;
}
name ??= comm.IsReply && String(comm.FormatSender) !== '' ? comm.FormatSender : comm.Sender;
const sendto = getMessageSendTo(comm, contacts, followers, r)
name ??= comment.IsReply && String(comment.FormatSender) !== '' ? comment.FormatSender : comment.Sender;
const sendto = getMessageSendTo(comment, contacts, followers, r)
div.appendChild(createElement('div', div => {
div.className = 'item-poster';
div.innerText = name;
if (!comm.IsReply && sendto?.length > 0) {
if (!comment.IsReply && sendto?.length > 0) {
setTooltip(div, sendto);
}
}));
const content = createElement('div', 'item-content');
const mmsParts = createElement('div', div => div.style.display = 'none');
content.appendChild(createElement('span', span => {
if (/https?:\/\//i.test(comm.Message)) {
span.innerHTML = formatUrl(escapeEmoji(comm.Message));
if (/https?:\/\//i.test(comment.Message)) {
span.innerHTML = formatUrl(escapeEmoji(comment.Message));
} else {
span.innerText = escapeEmoji(comm.Message);
span.innerText = escapeEmoji(comment.Message);
}
span.appendChild(mmsParts);
}));
if (comm.MMSParts?.length > 0) {
if (comment.MMSParts?.length > 0) {
mmsParts.style.display = '';
for (let kv of comm.MMSParts) {
for (let kv of comment.MMSParts) {
appendMedia(mmsParts, kv.Key, kv.Value);
}
}
if (comm.IsReply) {
if (comment.IsReply) {
div.classList.add('item-other');
} else {
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 (color != null) {
content.style.backgroundColor = color;
@ -1214,17 +1321,19 @@ export default class CustomerCommunication {
}
div.append(
content,
createElement('div', div => {
div.className = 'item-time';
div.innerText = comm.TimeStr;
})
createHideMessageCommentTail(
this, 'showMessageHidden',
comment, 'TimeStr',
func, hisFunc)
);
children.push(div);
}
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.scrollTop = this._var.message.scrollHeight
// setTimeout(() => this._var.message.scrollTop = this._var.message.scrollHeight, 0);
requestAnimationFrame(() => this._var.message.scrollTop = keep ? this._var.lastTop : this._var.message.scrollHeight);
}
}

View File

@ -11,6 +11,8 @@ export default class Follower {
const getText = option?.getText;
if (typeof getText === 'function') {
r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
}
}
@ -22,7 +24,7 @@ export default class Follower {
onMasking: this._var.option.onMasking,
title,
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 => {
search.type = 'text';
search.tabIndex = tabIndex + 3;
@ -41,7 +43,7 @@ export default class Follower {
),
buttons: [
{
text: r('P_WO_OK', 'OK'),
text: r('FLTL_02057', 'OK'),
key: 'ok',
trigger: () => {
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);
@ -57,18 +59,18 @@ export default class Follower {
// grid
const grid = new Grid(gridContainer);
grid.columns = [
{ key: 'DisplayName', caption: r('P_WO_CONTACTNAME', 'Contact Name'), width: 240 },
{ key: 'ContactTypeName', caption: r('P_WO_CONTACTTYPE', 'Contact Type'), width: 120 },
{ key: 'DisplayName', caption: r('FLTL_00637', 'Contact Name'), width: 240 },
{ key: 'ContactTypeName', caption: r('FLTL_00644', 'Contact Type'), width: 120 },
{
key: 'Text',
caption: r('P_CR_TEXT', 'Text'),
caption: r('FLTL_02915', 'Text'),
type: Grid.ColumnTypes.Checkbox,
width: 60,
enabled: item => !nullOrEmpty(item.Mobile)
},
{
key: 'Email',
caption: r('P_CR_EMAIL', 'Email'),
caption: r('FLTL_01089', 'Email'),
type: Grid.ColumnTypes.Checkbox,
width: 70,
// 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 { 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;
@ -19,6 +20,8 @@ export default class InternalComment {
const getText = opt?.getText;
if (typeof getText === 'function') {
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
*/
@ -99,24 +116,38 @@ export default class InternalComment {
return;
}
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-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() {
const option = this._var.option;
const container = createBox(
createElement('div', null,
createElement('div', div => {
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;
// enter box
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.addEventListener('input', () => {
const val = this.text;
@ -196,6 +227,24 @@ export default class InternalComment {
)
),
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 => {
button.className = 'roundbtn button-send-message';
button.style.backgroundColor = 'rgb(19, 150, 204)';
@ -203,7 +252,7 @@ export default class InternalComment {
button.style.display = 'none';
}
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', () => {
const val = this.text;
if (nullOrEmpty(val?.trim())) {
@ -223,7 +272,7 @@ export default class InternalComment {
button.style.display = 'none';
}
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', () => {
const val = this.text;
if (nullOrEmpty(val?.trim())) {
@ -245,7 +294,7 @@ export default class InternalComment {
return this._var.container = container;
}
load(data) {
load(data, func, hisFunc, keep) {
const children = [];
if (data?.length > 0) {
this._var.comments = data;
@ -256,8 +305,15 @@ export default class InternalComment {
this._var.contactsUpdated = true;
}
}
const lastVisible = this._var.option.showMessageHidden;
for (let comment of data) {
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)
div.appendChild(createElement('div', div => {
div.className = 'item-poster';
@ -268,7 +324,13 @@ export default class InternalComment {
}));
const content = createElement('div', 'item-content');
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) {
mmsParts.style.display = '';
for (let kv of comment.MMSParts) {
@ -277,10 +339,10 @@ export default class InternalComment {
}
// if (comment.FollowUp?.length > 0) {
// 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 => {
// div.className = 'item-status';
// div.innerText = r('P_WO_SENT', 'Sent');
// div.innerText = r('FLTL_02711', 'Sent');
// setTooltip(div, sendto);
// }));
// }
@ -300,17 +362,19 @@ export default class InternalComment {
}
div.append(
content,
createElement('div', div => {
div.className = 'item-time';
div.innerText = comment.TimeStr;
})
createHideMessageCommentTail(
this, 'showMessageHidden',
comment, 'TimeStr',
func, hisFunc)
);
children.push(div);
}
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.scrollTop = this._var.message.scrollHeight
// setTimeout(() => this._var.message.scrollTop = this._var.message.scrollHeight, 0);
requestAnimationFrame(() => this._var.message.scrollTop = keep ? this._var.lastTop : this._var.message.scrollHeight);
}
}

View File

@ -155,12 +155,12 @@ export function insertFile(container, file, r) {
type = type.substring(type.lastIndexOf('.'));
}
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;
}
const isImage = /^image\//.test(type);
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;
}
const fn = file.name;
@ -202,12 +202,12 @@ function getStatusText(status, dict) {
export function getMessageStatus(comm, r, _var) {
const messageStatus = {
0: r('P_CU_PENDING', 'Pending'),
1: r('P_WO_SENT', 'Sent'),
5: r('P_CU_DELIVERYCONFIRMED', 'Delivery Confirmed'),
6: r('P_CU_RESENT', 'Resent'),
9: r('P_MA_FAILED', 'Failed'),
9999: r('P_CU_UNKNOWN', 'Unknown')
0: r('FLTL_02186', 'Pending'),
1: r('FLTL_02711', 'Sent'),
5: r('FLTL_00864', 'Delivery Confirmed'),
6: r('FLTL_02478', 'Resent'),
9: r('FLTL_01224', 'Failed'),
9999: r('FLTL_03152', 'Unknown')
};
const knownStatus = [0, 1, 5, 6, 9, 10, 412];
const okStatus = [1, 5, 6];
@ -269,7 +269,7 @@ export function getMessageStatus(comm, r, _var) {
if (statusUpdatable !== false) {
tip.appendChild(createElement('div', b => {
b.className = 'tip-function-button';
// setTooltip(b, r('P_CU_UPDATESTATUS', 'Update Status'));
// setTooltip(b, r('FLTL_03174', 'Update Status'));
b.addEventListener('click', async () => {
for (let p of comm.Participator) {
switch (p.Status) {
@ -292,7 +292,7 @@ export function getMessageStatus(comm, r, _var) {
const gridContainer = createElement('div', 'status-grid');
const popup = new Popup({
onMasking: _var.option.onMasking,
title: r('P_CU_UPDATESTATUS', 'Update Status'),
title: r('FLTL_03174', 'Update Status'),
content: createElement('div', wrapper => {
wrapper.className = 'update-status-wrapper';
wrapper.style.width = '500px';
@ -301,7 +301,7 @@ export function getMessageStatus(comm, r, _var) {
),
buttons: [
{
text: r('P_WO_OK', 'OK'),
text: r('FLTL_02057', 'OK'),
key: 'ok',
trigger: () => {
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'
}
]
@ -341,22 +341,22 @@ export function getMessageStatus(comm, r, _var) {
grid.columns = [
{
key: 'CustomerNumber',
caption: r('P_JS_NUMBER', 'Number'),
caption: r('FLTL_02026', 'Number'),
width: 150
},
/*{
key: 'customerName',
caption: r('P_WOS_CUSTOMERNAME', 'Customer Name'),
caption: r('FLTL_00742', 'Customer Name'),
width: 120
},*/
{
key: 'statusText',
caption: r('P_CU_CURRENTSTATUS', 'Current Status'),
caption: r('FLTL_00725', 'Current Status'),
width: 155
},
{
key: 'statusChanged',
caption: r('P_CU_REVISEDSTATUS', 'Revised Status'),
caption: r('FLTL_02511', 'Revised Status'),
width: 155,
type: Grid.ColumnTypes.Dropdown,
source: [
@ -404,7 +404,7 @@ export function getMessageSendTo(comm, contacts, followers, r) {
}
}
if (sendto !== '') {
sendto = r('P_CU_SENDTO_COLON', 'Sent to :') + `\n${sendto}`;
sendto = r('FLTL_02716', 'Sent to :') + `\n${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 {
overflow-x: visible;
max-height: 200px;
}
}
}
@ -89,12 +90,39 @@
>div {
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 {
flex: 0 0 auto;
display: flex;
padding: 4px;
padding: 0 4px;
>label {
margin: 0 4px;

View File

@ -1,6 +1,12 @@
import "./element/style.scss";
import ScheduleItem from "./element/schedule";
import AddWorkOrder from "./element/addWorkorder";
import InspectionWizard from "./element/inspectionWizard";
import Signature from "./element/signature";
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 { r as lang, nullOrEmpty, formatUrl, escapeEmoji, isEmail, isPhone } from "../utility";
import { createElement, createCheckbox, createRadiobox, Dropdown, validation, toDateValue, OptionBase } from "../ui";
let r = lang;
function datepicker(element) {
if (typeof $?.fn?.datepicker === 'function') {
$(element).datepicker({
autoHide: true,
format: 'm/dd/yyyy'
});
}
return element;
}
export default class ScheduleItem {
export default class ScheduleItem extends OptionBase {
_var = {};
constructor(opt) {
this._var.option = opt ?? {};
const getText = opt?.getText;
if (typeof getText === 'function') {
r = getText;
}
constructor(opt = {}) {
super(opt);
}
get checkOccurOnce() { return this._var.container.querySelector('.schedule-id-box-occur-once>input'); }
@ -79,7 +62,8 @@ export default class ScheduleItem {
getDateString(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) {
@ -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-7>input').checked = schedule.Sunday;
this._var.container.querySelector('.schedule-id-dayofmonth').value = String(schedule.DayOfMonth);
const start = this.getDateString(schedule.StartDate);
const end = 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;
}
this._var.container.querySelector('.schedule-id-duration-start').value = this.getDateString(schedule.StartDate);
this._var.container.querySelector('.schedule-id-duration-end').value = this.getDateString(schedule.EndDate);
}
create() {
const option = this._var.option;
const option = this._option;
const drop = new Dropdown({ selected: '0' });
this.dropFrequency = drop;
drop.source = [
@ -184,7 +161,7 @@ export default class ScheduleItem {
}),
validation(
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',
@ -205,14 +182,14 @@ export default class ScheduleItem {
createElement('span', span => span.innerText = 'Starting at'),
validation(
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('span', span => span.innerText = 'Ending at'),
validation(
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('div', 'schedule-item-line schedule-item-line-duration',
createElement('span', span => span.innerText = 'Start date'),
datepicker(
validation(
createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-duration-start', i.maxLength = 10 }),
/^([0]?[1-9]|[1][0-2])\/([0]?[1-9]|[12][0-9]|[3][01])\/[0-9]{4}$/
)
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' }),
/^[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('span', span => span.innerText = 'End date'),
datepicker(
validation(
createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-duration-end', i.maxLength = 10 }),
/^([0]?[1-9]|[1][0-2])\/([0]?[1-9]|[12][0-9]|[3][01])\/[0-9]{4}$/
)
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' }),
/^[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',

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 {
fieldset {
@ -18,6 +20,12 @@
.ui-input {
line-height: 20px;
height: 20px;
text-indent: 0;
&.validation-error,
&:invalid {
color: #0000004d;
}
}
.schedule-item-monthly {
@ -89,3 +97,226 @@
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

@ -4,13 +4,65 @@ import { createElement } from "./functions";
import { createIcon, changeIcon, resolveIcon } from "./ui/icon";
import { createCheckbox, createRadiobox, resolveCheckbox } from "./ui/checkbox";
import { setTooltip, resolveTooltip } from "./ui/tooltip";
import { createTab } from "./ui/tab";
import { Dropdown } from "./ui/dropdown";
import { Grid } from "./ui/grid/grid";
import { GridColumn, GridInputColumn, GridDropdownColumn, GridCheckboxColumn, GridIconColumn, GridTextColumn, GridDateColumn } from './ui/grid/column';
import { Popup, createPopup, showAlert, showConfirm } from "./ui/popup";
import { createPicture, createAudio, createVideo, createFile } from './ui/media';
import { Popup, createPopup, resolvePopup, showAlert, showConfirm } from "./ui/popup";
import { createPicture, createAudio, createVideo, createFile, createVideoList } from './ui/media';
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 {
createElement,
@ -25,6 +77,8 @@ export {
// tooltip
setTooltip,
resolveTooltip,
// tab
createTab,
// dropdown
Dropdown,
// grid
@ -39,11 +93,13 @@ export {
// popup
Popup,
createPopup,
resolvePopup,
showAlert,
showConfirm,
// dateSelector
createDateInput,
toDateValue,
getFormatter,
formatDate,
setDateValue,
getDateValue,
@ -53,7 +109,15 @@ export {
createAudio,
createVideo,
createFile,
createVideoList,
// extension
validation,
convertCssStyle
convertCssStyle,
// utility
utility,
// functions
requestAnimationFrame,
offset,
// base classes
OptionBase
}

View File

@ -10,10 +10,14 @@ interface CheckboxOptions {
name?: string;
/** 焦点索引 */
tabIndex?: Number;
/** 是否为 switch 样式 */
switch?: boolean;
/** 样式分类,可以是 ['`fa-light`', '`fa-regular`', '`fa-solid`'] 其中之一 */
type?: string;
/** 标签 */
label?: string | HTMLElement;
/** 标签提示文本 */
title?: string;
/** 是否已选中 */
checked?: boolean;
/** 图片高度 */

View File

@ -2,7 +2,11 @@ import './css/checkbox.scss';
import { createElement } from "../functions";
import { createIcon } from "./icon";
function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, charactor = 'check') {
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(
createElement('layer', layer => {
layer.className = 'ui-check-inner';
@ -18,13 +22,18 @@ function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, char
if (tabindex >= 0) {
layer.tabIndex = tabindex;
}
}, createIcon(type, charactor))
}, checkIcon, indeterminateIcon)
);
if (label instanceof Element) {
container.appendChild(label);
} else if (label != null && String(label).length > 0) {
container.appendChild(
createElement('span', span => span.innerText = label)
createElement('span', span => {
span.innerText = label;
if (title != null) {
span.title = title;
}
})
);
}
}
@ -52,17 +61,20 @@ export function createRadiobox(opts = {}) {
if (opts.className) {
container.classList.add(opts.className);
}
fillCheckbox(container, opts.type, opts.label, opts.tabIndex, 'circle');
fillCheckbox(container, opts.type, opts.label, opts.tabIndex, 'circle', opts.title);
return container;
}
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 => {
input.setAttribute('type', 'checkbox');
if (opts.checked === true) {
input.checked = true;
}
if (opts.indeterminate === true) {
input.indeterminate = true;
}
if (opts.enabled === false) {
input.disabled = true;
}
@ -81,7 +93,23 @@ export function createCheckbox(opts = {}) {
if (opts.enabled === false) {
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');
let height = opts.imageHeight;
if (isNaN(height) || height <= 0) {
@ -92,7 +120,7 @@ export function createCheckbox(opts = {}) {
opts.uncheckedNode.classList.add('unchecked');
container.appendChild(opts.uncheckedNode);
} else {
fillCheckbox(container, opts.type, opts.label, opts.tabIndex);
fillCheckbox(container, opts.type, opts.label, opts.tabIndex, undefined, opts.title);
}
return container;
}
@ -144,7 +172,7 @@ export function resolveCheckbox(container = document.body, legacy) {
label.className = 'ui-check-wrapper';
}
label.replaceChildren();
fillCheckbox(label, 'fa-regular', text, chk.tabIndex);
fillCheckbox(label, 'fa-regular', text, chk.tabIndex, undefined, label.title);
label.insertBefore(chk, label.firstChild);
}
}
@ -161,7 +189,10 @@ export function resolveCheckbox(container = document.body, legacy) {
fillCheckbox(box,
box.dataset.type,
box.dataset.label,
box.dataset.tabIndex)
box.dataset.tabIndex,
undefined,
box.title);
box.removeAttribute('title');
box.removeAttribute('data-type');
box.removeAttribute('data-label');
}

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();
&.validation-error {
&.validation-error,
&:invalid {
border-color: var(--red-color);
&:focus,

View File

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

View File

@ -44,6 +44,11 @@
}
}
@mixin wrap() {
white-space: normal;
word-break: break-word;
}
@mixin ui-check() {
.ui-check-inner {
position: relative;
@ -87,7 +92,22 @@
border-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);
opacity: 1;
}

View File

@ -44,7 +44,8 @@
--header-padding: 4px 12px 4px 8px;
--header-filter-padding: 4px 26px 4px 8px;
--spacing-s: 4px;
--spacing-cell: 6px 4px 6px 8px;
--spacing-cell: 9px 4px 9px 8px;
--spacing-drop-cell: 5px 4px 5px 8px;
--filter-line-height: 30px;
--filter-item-padding: 0 4px;
}
@ -94,6 +95,7 @@
// position: relative;
top: 0;
position: sticky;
z-index: 1;
&.sticky {
position: sticky;
@ -117,12 +119,22 @@
text-overflow: ellipsis;
&.wrap {
white-space: normal;
@include wrap();
}
}
>.ui-check-wrapper {
>.ui-check-wrapper,
>.ui-switch {
height: 20px;
padding: 0 4px 0 0;
}
>svg {
width: 12px;
min-width: 12px;
height: 12px;
margin-left: 4px;
fill: var(--split-border-color);
}
}
@ -202,7 +214,7 @@
// }
}
>em.bottom-border {
>.bottom-border {
position: absolute;
bottom: 0;
left: 0;
@ -285,11 +297,15 @@
}
>span {
padding: var(--spacing-cell);
margin: var(--spacing-cell);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
&.wrap {
@include wrap();
}
}
}
}
@ -356,11 +372,13 @@
}
>input[type="text"],
>input[type="date"],
>textarea {
border: none;
box-sizing: border-box;
width: 100%;
padding: 0;
max-height: none !important;
@include outline();
@ -383,10 +401,12 @@
@include scrollbar();
}
.ui-check-wrapper {
.ui-check-wrapper,
.ui-switch {
display: inline-flex;
justify-content: center;
height: var(--row-height);
padding: 0 8px;
.ui-check-inner {
@ -395,6 +415,30 @@
transition: none;
}
}
>span:first-of-type {
&:before,
&:after {
transition: none;
}
}
}
.ui-drop-span {
margin: 0;
>span {
margin: var(--spacing-cell);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
}
&.wrap>span {
@include wrap();
}
}
.ui-drop-wrapper {
@ -408,18 +452,14 @@
height: 100%;
>.ui-drop-text {
padding: var(--spacing-cell);
padding: var(--spacing-drop-cell);
}
}
}
.ui-date-cell {
line-height: 22px;
box-sizing: border-box;
padding: var(--spacing-cell);
border: none;
font-size: var(--font-size);
width: 100%;
height: var(--row-height);
text-indent: 4px;
&:invalid {
color: rgba(0, 0, 0, .3);
@ -432,7 +472,7 @@
justify-content: center;
align-items: center;
position: relative;
padding: var(--spacing-s);
// padding: var(--spacing-s);
>svg {
width: 16px;
@ -580,6 +620,7 @@
.ui-check-wrapper {
height: var(--filter-line-height);
line-height: var(--filter-line-height);
display: flex;
.ui-check-inner+* {
@ -594,11 +635,14 @@
justify-content: flex-end;
padding: 4px;
>button {
>.button {
box-sizing: border-box;
padding-inline: 6px;
text-align: center;
margin-right: 10px;
min-width: 40px;
height: var(--filter-line-height);
line-height: var(--filter-line-height);
border: none;
background-color: transparent;
cursor: pointer;
@ -624,8 +668,10 @@
white-space: nowrap;
overflow: hidden;
>button {
>.button {
margin-right: 6px;
padding-inline: 6px;
text-align: center;
border: none;
line-height: 28px;
color: var(--title-color);
@ -633,6 +679,7 @@
padding: 0 10px;
box-sizing: border-box;
height: 28px;
line-height: 28px;
cursor: pointer;
user-select: none;
background-color: var(--title-bg-color);
@ -671,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) {
.ui-grid {
--cell-hover-bg-color: yellow;

View File

@ -66,3 +66,234 @@
max-width: 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;
overflow: hidden;
text-overflow: ellipsis;
padding: 10px 0 6px 12px;
padding: 5px 0 3px 12px;
}
>.ui-popup-header-title,
@ -70,9 +70,13 @@ $buttonHeight: 28px;
cursor: move;
}
>.ui-popup-header-title.no-move {
cursor: default;
}
>.ui-popup-header-icons {
flex: 0 0 auto;
padding: 10px 12px 6px 0;
padding: 5px 12px 3px 0;
display: flex;
>svg {
@ -141,8 +145,9 @@ $buttonHeight: 28px;
margin: 10px;
>svg {
width: 40px;
height: 40px;
width: 30px;
height: 30px;
fill: unset;
+span {
padding-left: 16px;
@ -200,6 +205,10 @@ $buttonHeight: 28px;
align-items: center;
justify-content: flex-end;
padding: 4px 10px 16px 2px;
}
.ui-popup-body,
.ui-popup-footer {
.ui-popup-button {
margin-left: 12px;
@ -208,7 +217,7 @@ $buttonHeight: 28px;
line-height: $buttonHeight;
color: var(--title-color);
border-radius: var(--corner-radius);
padding: 4px 16px;
padding: 1px 16px;
box-sizing: border-box;
min-width: 70px;
text-align: center;
@ -223,6 +232,11 @@ $buttonHeight: 28px;
}
@include outline();
&:disabled {
opacity: .4;
cursor: default;
}
}
}

47
lib/ui/css/tab.scss Normal file
View File

@ -0,0 +1,47 @@
.ui-tab-container {
display: flex;
flex-direction: column;
--header-line-height: 24px;
--header-padding: 6px 6px 0;
--header-hover-color: #b9b9b9;
--header-selected-color: rgb(173, 84, 12);
>.ui-tab-header {
flex: 0 0 auto;
display: flex;
flex-wrap: wrap;
line-height: var(--header-line-height);
padding: var(--header-padding);
border-bottom: 1px solid var(--border-color);
user-select: none;
>.ui-tab-title {
border: 1px solid transparent;
background-color: var(--bg-color);
cursor: pointer;
padding: 0 12px;
&:hover {
color: var(--header-hover-color);
}
&.selected {
border-color: var(--border-color);
border-bottom-color: var(--bg-color);
color: var(--header-selected-color);
}
}
}
>.ui-tab-page {
flex: 1 1 auto;
padding: 6px;
overflow: auto;
width: 100%;
height: 100%;
box-sizing: border-box;
position: relative;
display: none;
}
}

View File

@ -21,6 +21,8 @@
--disabled-color: #aaa;
--disabled-bg-color: #e9e9e9;
--disabled-border-color: #d9d9d9;
--switch-bg-color: #eae9eb;
--switch-active-bg-color: #33c559;
--red-color: red;
--title-color: #fff;
@ -35,12 +37,15 @@
--border-radius: 2px;
--text-indent: 4px;
--line-height: 24px;
--line-height: 18px;
--settings-line-height: 32px;
--font-size: .8125rem; // 13px
--font-smaller-size: .75rem; // 12px
--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;
--header-font-family: Arial, sans-serif;
}
/*@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 格式的字符串
* @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/>
* @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 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` 类型
* @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 {
@ -85,6 +87,10 @@ export class DateSelector {
maxDate?: string,
/** 是否启用 */
enabled?: boolean,
/** 焦点索引 */
tabIndex?: number,
/** 类名 */
className?: string,
/**
* 自定义格式化函数,用于获取日期值时调用
* @param date 日期值

View File

@ -1,4 +1,5 @@
import { createElement } from "../functions";
import { isPositive, nullOrEmpty } from "../utility";
/**
* 创建或转换日期选择框
@ -22,9 +23,13 @@ export function createDateInput(min, max, element) {
date.type = 'date';
if (min != null) {
date.min = min;
} else {
date.min = '1753-01-01';
}
if (max != null) {
date.max = max;
} else {
date.max = '9999-12-31';
}
return date;
}
@ -32,26 +37,140 @@ export function createDateInput(min, max, element) {
/**
* 将日期转换为 `yyyy-MM-dd` 格式的字符串
* @param {Date} dt 要转换的日期值
* @param {boolean} [local] 是否视日期为本地时间
* @returns 返回 `yyyy-MM-dd` 格式的字符串
*/
export function toDateValue(dt) {
export function toDateValue(dt, local) {
if (isNaN(dt)) {
return '';
}
const month = String(dt.getMonth() + 1).padStart(2, '0');
const date = String(dt.getDate()).padStart(2, '0');
return `${dt.getFullYear()}-${month}-${date}`;
const year = local ? dt.getFullYear() : dt.getUTCFullYear();
const month = String((local ? dt.getMonth() : dt.getUTCMonth()) + 1).padStart(2, '0');
const date = String(local ? dt.getDate() : dt.getUTCDate()).padStart(2, '0');
return `${year}-${month}-${date}`;
}
function resolveDate(s) {
if (s instanceof Date) {
return s;
/**
* 获取日期格式器
* @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);
}
const ticks = Number(s);
if (!isNaN(ticks) && ticks > 0) {
return new Date((ticks - 621355968e9) / 1e4);
}
return new Date(s);
};
return r;
}
/**
@ -59,45 +178,109 @@ function resolveDate(s) {
* @param {Date | number | string} date - 需要格式化的日期值,支持的格式如下:
*
* * `"2024-01-26"`
* * `"2024/1/26"`
* * `"2024-01-26T00:00:00"`
* * `"1/26/2024"`
* * `"638418240000000000"`
* * `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} 返回格式化后的日期字符串
*/
export function formatDate(date) {
date = resolveDate(date);
if (date instanceof Date && !isNaN(date)) {
return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
}
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) {
const f = getFormatter(date, false);
return formatter.replace(/\\?(.?)/gi, (k, v) => f[k] ? f[k]() : v);
}
const ticks = Number(date);
if (!isNaN(ticks) && ticks > 0) {
date = new Date((ticks - 621355968e9) / 1e4);
const f = getFormatter(date);
return formatter.replace(/\\?(.?)/gi, (k, v) => f[k] ? f[k]() : v);
}
return date;
}
/**
* 设置显示日期
* @param {HTMLElement} element - 要设置显示日期的元素
* @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 (val === '') {
element.value = '';
} else if (isNaN(val)) {
if (/^\d{4}-\d{2}-\d{2}/.test(val)) {
element.value = String(val).substring(0, 10);
} else if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(val)) {
element.value = toDateValue(new Date(val));
} else if (/^\d{4}\/\d{2}\/\d{2}/.test(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 {
element.value = '';
}
} else {
if (!(val instanceof Date)) {
val = new Date((val - 621355968e9) / 1e4);
}
element.value = toDateValue(val);
}
} else {
element.innerText = formatDate(val);
if (val instanceof Date) {
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 {
element.innerText = formatDate(val, formatter);
}
}
@ -111,18 +294,25 @@ export function setDateValue(element, val) {
/**
* 从日期选择框获取日期值
* @param {HTMLInputElement} element - 要获取的日期选择框
* @param {DateFormatterCallback} [formatter] - 自定义格式化函数,传入参数为 `Date` 类型
* @param {string | DateFormatterCallback} [formatter] - 自定义格式化字符串或函数,传入参数为 `Date` 类型
* @returns {string | any} 默认返回日期 `ticks` 的字符串
*/
export function getDateValue(element, formatter) {
const date = element?.valueAsDate;
if (date instanceof Date && !isNaN(date)) {
const year = date.getFullYear();
const year = date.getUTCFullYear();
if (year < 1900 || year > 9999) {
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') {
return formatter(date);
return formatter(localDate);
}
return formatDate(localDate, formatter);
}
return String(date.getTime() * 1e4 + 621355968e9);
}
@ -135,7 +325,43 @@ export function getDateValue(element, formatter) {
*/
export class DateSelector {
_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
}
};
/**
@ -162,7 +388,7 @@ export class DateSelector {
* minDate: '2020-01-01',
* maxDate: '2024-02-01',
* valueFormatter: (date) => {
* return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; // 2024/1/31
* return `${date.getUTCFullYear()}/${date.getUTCMonth() + 1}/${date.getUTCDate()}`; // 2024/1/31
* }
* };
* const dateSelectorFrom = new DateSelector(opts);
@ -185,6 +411,10 @@ export class DateSelector {
create(element) {
const opts = this._var.options;
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) {
el.disabled = opts.enabled === false;
}
@ -246,9 +476,14 @@ export class DateSelector {
this._var.options.maxDate = date;
}
/**
* @private
* @param {Date} date
* @returns {Date | any}
*/
_getDate(date) {
if (date instanceof Date && !isNaN(date)) {
const year = date.getFullYear();
const year = date.getUTCFullYear();
if (year < 1900 || year > 9999) {
return null;
}

View File

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

View File

@ -1,8 +1,7 @@
// import { r, global, contains, isPositive, nullOrEmpty } from "../utility";
import './css/dropdown.scss';
import { r } from "../utility/lgres";
import { r as lang } from "../utility/lgres";
import { contains, nullOrEmpty } from "../utility/strings";
import { global, isPositive } from "../utility";
import { global, isPositive, throttle } from "../utility";
import { createElement } from "../functions";
import { createCheckbox } from "./checkbox";
import { createIcon } from "./icon"
@ -10,6 +9,7 @@ import { createIcon } from "./icon"
const SymbolDropdown = Symbol.for('ui-dropdown');
const DropdownItemHeight = 30;
let r = lang;
let dropdownGlobal = global[SymbolDropdown];
if (dropdownGlobal == null) {
@ -52,8 +52,13 @@ if (dropdownGlobal == null) {
});
}
function selectItems(label, itemlist, htmlkey, textkey) {
const htmls = itemlist.map(it => it[htmlkey]);
function selectItems(label, itemlist, template, htmlkey, textkey) {
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)) {
label.replaceChildren(...htmls.filter(it => it != null).map(it => it.cloneNode(true)));
} else {
@ -82,12 +87,24 @@ function filterSource(searchkeys, textkey, key, 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
* @property {string} [textKey=text] - 文本关键字
* @property {string} [valueKey=value] - 值关键字
* @property {string} [htmlKey=html] - 源码显示的关键字
* @property {Function} [htmlTemplate] - 模板创建函数
* @property {number} [maxLength=500] - 最大输入长度
* @property {boolean} [multiSelect] - 是否允许多选
* @property {string} [selected] - 选中值
@ -124,12 +141,18 @@ export class Dropdown {
onCollapsed;
constructor(options = {}) {
options.searchPlaceholder ??= r('searchHolder', 'Search...');
options.textKey ??= 'text';
options.valueKey ??= 'value';
options.htmlKey ??= 'html';
options.maxLength ??= 500;
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() {
@ -138,6 +161,9 @@ export class Dropdown {
// wrapper
const wrapper = createElement('div', 'ui-drop-wrapper');
const dropId = String(Math.random()).substring(2);
if (options.wrapper instanceof HTMLElement) {
options.wrapper.dataset.dropId = dropId;
}
wrapper.dataset.dropId = dropId;
dropdownGlobal[dropId] = this;
this._var.wrapper = wrapper;
@ -156,6 +182,7 @@ export class Dropdown {
const source = this.source;
const count = source.length;
const valuekey = this._var.options.valueKey;
const textkey = this._var.options.textKey;
let index = source?.indexOf(this._var.selected);
if (isNaN(index) || index < -1) {
index = -1;
@ -177,7 +204,7 @@ export class Dropdown {
index = count - 1;
}
}
const target = source[index]?.[valuekey];
const target = getValue(source[index], valuekey, textkey);
if (target != null) {
this.select(target);
}
@ -204,7 +231,9 @@ export class Dropdown {
let label;
if (options.input) {
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);
isPositive(options.maxLength) && label.setAttribute('maxlength', options.maxLength);
isPositive(options.tabIndex) && label.setAttribute('tabindex', options.tabIndex);
@ -240,6 +269,8 @@ export class Dropdown {
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 }
set disabled(flag) {
@ -271,7 +302,14 @@ export class Dropdown {
if (!Array.isArray(list)) {
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) {
setTimeout(() => this._dropdown(), 120);
}
@ -281,18 +319,22 @@ export class Dropdown {
get selectedList() { return this._var.selectedList || [] }
select(selected, silence) {
select(selected, silence, ignoreCase) {
if (typeof selected !== 'string') {
selected = String(selected);
}
if (ignoreCase) {
selected = selected.toLowerCase();
}
if (this._var.lastSelected === selected) {
return false;
return;
}
this._var.lastSelected = selected;
const valuekey = this._var.options.valueKey;
const textkey = this._var.options.textKey;
const template = this._var.options.htmlTemplate;
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 (item == null) {
item = { [valuekey]: selected };
@ -308,7 +350,12 @@ export class Dropdown {
this._var.label.innerText = ' ';
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) {
this._var.label.replaceChildren(html.cloneNode(true));
} else if (typeof html === 'string') {
@ -321,27 +368,35 @@ export class Dropdown {
this._var.label.innerText = text;
}
if (expanded) {
const val = selected.replace(/"/g, '\\"');
const li = this._var.container.querySelector(`li[data-value="${val}"]`);
if (li != null) {
for (let li of this._var.container.querySelectorAll('li[data-value]')) {
if ((ignoreCase ? li.dataset.value.toLowerCase() : li.dataset.value) === selected) {
li.classList.add('selected');
break;
}
}
// const val = selected.replace(/"/g, '\\"');
// const li = this._var.container.querySelector(`li[data-value="${val}"]`);
// if (li != null) {
// li.classList.add('selected');
// }
}
}
this._var.selected = item;
if (!silence && typeof this.onSelected === 'function') {
this.onSelected(item);
}
return true;
}
selectlist(selectedlist, silence) {
const source = this.source;
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 itemlist = selectedlist.map(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) {
item = {
[valuekey]: v,
@ -352,10 +407,10 @@ export class Dropdown {
});
if (itemlist.length === 0) {
this._var.selectedList = null;
this._var.label.innerText = none;
this._var.label.innerText = r('none', '( None )');
return false;
}
selectItems(this._var.label, itemlist, htmlkey, textkey);
selectItems(this._var.label, itemlist, template, htmlkey, textkey);
this._var.selectedList = itemlist;
if (!silence && typeof this.onSelectedList === 'function') {
this.onSelectedList(itemlist);
@ -373,7 +428,8 @@ export class Dropdown {
if (!options.input && options.search) {
const search = createElement('div', 'ui-drop-search');
const input = createElement('input');
input.setAttribute('type', 'text');
input.type = 'text';
input.className = 'ui-input';
isPositive(options.tabIndex) && input.setAttribute('tabindex', options.tabIndex);
!nullOrEmpty(options.searchPlaceholder) && input.setAttribute('placeholder', options.searchPlaceholder);
input.addEventListener('input', e => {
@ -385,7 +441,8 @@ export class Dropdown {
panel.appendChild(search);
}
// 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) {
list.addEventListener('click', e => {
let li = e.target;
@ -457,45 +514,148 @@ export class Dropdown {
}
}
panel.classList.add('active');
this._var.dropTop = 0;
panel.querySelector('.ui-drop-list').dispatchEvent(new Event('scroll'));
} else {
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) {
const list = this._var.container.querySelector('.ui-drop-list');
list.replaceChildren();
const multiselect = this.multiSelect;
const allchecked = this._var.allChecked;
if (multiselect) {
const height = source.length * DropdownItemHeight;
this._var.dropHeight = height;
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(
createElement('li', null,
createCheckbox({
label: r('allItem', '( All )'),
checked: allchecked,
checked: this._var.allChecked,
customAttributes: { 'isall': '1' },
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 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 selected = this.selected;
const selectedlist = this.selectedList;
let scrolled;
source.slice(0, 200).forEach((item, i) => {
let val = item[valuekey];
array.forEach((item, i) => {
let val = getValue(item, valuekey, textkey);
if (typeof val !== 'string') {
val = String(val);
}
const li = createElement('li');
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;
const html = item[htmlkey];
let html;
if (typeof template === 'function') {
html = template.call(this, item);
} else {
html = item[htmlkey];
}
if (html instanceof HTMLElement) {
label = html;
} else if (typeof html === 'string') {
@ -503,77 +663,79 @@ export class Dropdown {
label.innerHTML = html;
}
if (multiselect) {
const selected = selectedlist.some(s => String(s[valuekey]) === val);
if (label == null) {
label = createElement('span');
label.innerText = item[textkey];
}
const box = createCheckbox({
label,
checked: allchecked || selected,
checked: item.__checked === 1,
indeterminate: item.__checked === 2,
customAttributes: {
'class': 'dataitem',
'data-value': val
},
onchange: e => this._triggerselect(e.target)
onchange: e => this._triggerselect(e.target, item)
});
li.appendChild(box);
wrapper.appendChild(box);
} else {
if (label == null) {
li.innerText = item[textkey];
} else {
li.appendChild(label);
label = createElement('span');
label.innerHTML = item[textkey];
}
wrapper.appendChild(label);
if (selected != null && String(selected[valuekey]) === val) {
scrolled = DropdownItemHeight * i;
li.classList.add('selected');
}
}
list.appendChild(li);
content.appendChild(li);
});
if (scrolled != null) {
setTimeout(() => list.scrollTop = scrolled, 10);
}
return scrolled;
}
_triggerselect(checkbox) {
_triggerselect(checkbox, item) {
let list;
const valuekey = this._var.options.valueKey;
const textkey = this._var.options.textKey;
const template = this._var.options.htmlTemplate;
const htmlkey = this._var.options.htmlKey;
if (checkbox.getAttribute('isall') === '1') {
const allchecked = this._var.allChecked = checkbox.checked;
const boxes = this._var.container.querySelectorAll('input.dataitem');
boxes.forEach(box => box.checked = allchecked);
list = [];
} else if (checkbox.checked) {
if (this._var.container.querySelectorAll('input.dataitem:not(:checked)').length === 0) {
} else {
item.__checked = checkbox.indeterminate ? 2 : checkbox.checked ? 1 : 0;
const all = this._var.container.querySelector('input[isall="1"]');
if (checkbox.checked) {
const source = this.source;
if (source.some(it => it.__checked) == null) {
this._var.allChecked = true;
this._var.container.querySelector('input[isall="1"]').checked = true;
if (all != null) {
all.checked = true;
}
list = [];
} else {
const source = this.source;
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);
list = source.filter(it => it.__checked);
}
} else {
const val = checkbox.dataset.value;
if (this._var.allChecked) {
this._var.allChecked = false;
this._var.container.querySelector('input[isall="1"]').checked = false;
list = this.source.filter(it => String(it[valuekey]) !== val);
if (all != null) {
all.checked = false;
}
list = this.source.filter(it => String(getValue(it, valuekey, textkey)) !== val);
} 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) {
this._var.label.innerText = r('allItem', '( All )');
} else {
selectItems(this._var.label, list, htmlkey, textkey);
selectItems(this._var.label, list, template, htmlkey, textkey);
}
this._var.selectedList = list;
if (typeof this.onSelectedList === 'function') {

View File

@ -1,7 +1,7 @@
import { global } from "../../utility";
import { createElement } from "../../functions";
import { createIcon } from "../icon";
import { createCheckbox } from "../checkbox";
import { createCheckbox, createRadiobox } from "../checkbox";
// import { setTooltip } from "../tooltip";
import { Dropdown } from "../dropdown";
import { convertCssStyle } from "../extension";
@ -36,6 +36,22 @@ export class GridColumn {
* @see 更多例子参考 {@linkcode GridInputColumn} {@linkcode GridDateColumn} 中的代码实现
*/
/**
* 标记该类型是否可编辑
* @member
* @name GridColumn.canEdit
* @readonly
* @type {boolean}
*/
/**
* 标记该类型是否支持列头批量操作
* @member
* @name GridColumn.headerEditing
* @readonly
* @type {boolean}
*/
/**
* 创建显示单元格时调用的方法
* @param {GridColumnDefinition} col - 列定义对象
@ -55,10 +71,11 @@ export class GridColumn {
* 更多例子参考代码中 {@linkcode GridDropdownColumn} 的实现。
* @method
* @name GridColumn.createEdit
* @param {Function} trigger - 编辑事件回调函数`e` 参数会传递给 [getValue]{@linkcode GridColumn.getValue} 方法
* @param {Function} trigger - 编辑事件回调函数
* @param {any} trigger.e - 该参数会传递给 [getValue]{@linkcode GridColumn.getValue} 方法
* @param {GridColumnDefinition} col - 列定义对象
* @param {HTMLElement} container - 父容器元素
* @param {GridItemWrapper} wrapper - 行包装对象,其 `values` 属性为行数据对象
* @param {HTMLElement} [container] - 父容器元素
* @param {GridItemWrapper} [wrapper] - 行包装对象,其 `values` 属性为行数据对象
* @returns {HTMLElement} 返回创建的编辑状态的单元格元素
* @virtual
*/
@ -72,6 +89,15 @@ export class GridColumn {
* @virtual
*/
/**
* 获取用于判断文本大小的元素
* @method
* @name GridColumn.getElement
* @param {HTMLElement} element - 单元格主内容元素
* @returns {HTMLElement} 返回文本元素
* @virtual
*/
/**
* 获取编辑状态单元格值时调用的方法
* @method
@ -122,6 +148,7 @@ export class GridColumn {
* 设置单元格可用性时调用的方法
* @param {HTMLElement} element - 单元格元素
* @param {boolean} enabled - 启用值,为 `false` 时代表禁用
* @param {boolean} editing - 是否处于编辑状态
* @virtual
*/
static setEnabled(element, enabled) {
@ -132,7 +159,16 @@ export class GridColumn {
}
/**
* 单元格离开编辑元素时触发,需要由行包装对象的 `__editing` 来确定是否触发。
* 单元格编辑状态发生改变时调用的方法
* @method
* @name GridColumn.setEditing
* @param {HTMLElement} element - 单元格元素
* @param {boolean} editing - 是否处于编辑状态
* @virtual
*/
/**
* 单元格离开编辑元素时调用的方法,需要由行包装对象的 `__editing` 来确定是否触发。
* @method
* @name GridColumn.leaveEdit
* @param {HTMLElement} element - 单元格元素
@ -143,7 +179,33 @@ export class GridColumn {
/**
* @ignore
*/
static toString() { return '[object Column]' }
static toString() { return 'GridCommon' }
/**
* @ignore
* @param {string} key
* @param {GridItemWrapper} wrapper
* @param {any} value
*/
static _changeValue(key, wrapper, value) {
const val = wrapper.values[key] ?? null;
const hasValue = val != null && Object.prototype.hasOwnProperty.call(val, 'Value');
if (wrapper.__editing == null) {
wrapper.__editing = {
[key]: hasValue ? val.Value : val
}
} else if (!Object.prototype.hasOwnProperty.call(wrapper.__editing, key)) {
wrapper.__editing[key] = hasValue ? val.Value : val;
}
if (hasValue) {
val.Value = value;
if (Object.prototype.hasOwnProperty.call(val, 'DisplayValue')) {
val.DisplayValue = value;
}
} else {
wrapper.values[key] = value;
}
}
}
/**
@ -155,27 +217,21 @@ export class GridColumn {
*/
export class GridInputColumn extends GridColumn {
static get editing() { return true };
static get canEdit() { return true };
/**
* @ignore
* @param {Function} _trigger
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @param {HTMLElement} _container
* @param {GridItemWrapper} wrapper
* @returns {HTMLElement}
*/
static createEdit(_trigger, col, _container, wrapper) {
static createEdit(trigger, col, _container, wrapper) {
const input = createElement('input');
input.setAttribute('type', 'text');
input.addEventListener('input', () => {
if (wrapper.__editing == null) {
wrapper.__editing = {
[col.key]: true
}
} else {
wrapper.__editing[col.key] = true;
}
});
input.addEventListener('input', () => super._changeValue(col.key, wrapper, input.value));
input.addEventListener('change', trigger);
return input;
}
@ -208,6 +264,11 @@ export class GridInputColumn extends GridColumn {
super.setEnabled(element, enabled);
element.disabled = enabled === false;
}
/**
* @ignore
*/
static toString() { return 'GridInput' }
}
/**
@ -221,23 +282,16 @@ export class GridInputColumn extends GridColumn {
export class GridTextColumn extends GridInputColumn {
/**
* @ignore
* @param {Function} _trigger
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @param {HTMLElement} _container
* @param {GridItemWrapper} wrapper
* @returns {HTMLElement}
*/
static createEdit(_trigger, col, _container, wrapper) {
static createEdit(trigger, col, _container, wrapper) {
const input = createElement('textarea');
input.addEventListener('input', () => {
if (wrapper.__editing == null) {
wrapper.__editing = {
[col.key]: true
}
} else {
wrapper.__editing[col.key] = true;
}
});
input.addEventListener('input', () => super._changeValue(col.key, wrapper, input.value));
input.addEventListener('change', trigger);
return input;
}
@ -261,6 +315,11 @@ export class GridTextColumn extends GridInputColumn {
// TODO: bad performance
}
}
/**
* @ignore
*/
static toString() { return 'GridText' }
}
const SymbolDropdown = Symbol.for('ui-dropdown');
@ -273,6 +332,16 @@ const SymbolDropdown = Symbol.for('ui-dropdown');
* @hideconstructor
*/
export class GridDropdownColumn extends GridColumn {
static get canEdit() { return true };
/**
* @ignore
* @returns {HTMLElement}
*/
static create() {
return createElement('span', 'ui-drop-span', createElement('span'));
}
/**
* @ignore
* @param {Function} trigger
@ -302,6 +371,18 @@ export class GridDropdownColumn extends GridColumn {
return drop.create();
}
/**
* @ignore
* @param {HTMLElement} element
* @returns {HTMLElement}
*/
static getElement(element) {
if (element.tagName === 'DIV') {
return element.children[0].children[0];
}
return element.children[0];
}
/**
* @private
* @param {HTMLElement} element
@ -363,7 +444,7 @@ export class GridDropdownColumn extends GridColumn {
if (data != null) {
val = data[opts?.textKey ?? 'text'];
}
super.setValue(element, val);
element.children[0].innerText = val;
}
/**
@ -372,8 +453,9 @@ export class GridDropdownColumn extends GridColumn {
* @param {any} val
* @param {GridItemWrapper} wrapper
* @param {GridColumnDefinition} col
* @param {Grid} grid
*/
static setValue(element, val, wrapper, col) {
static setValue(element, val, wrapper, col, grid) {
if (element.tagName !== 'DIV') {
let source = this._getSource(wrapper, col);
if (source instanceof Promise) {
@ -387,19 +469,42 @@ export class GridDropdownColumn extends GridColumn {
if (drop == null) {
return;
}
const ignoreCase = col.dropRestrictCase !== true;
if (drop.source == null || drop.source.length === 0) {
let source = this._getSource(wrapper, col);
if (source instanceof Promise) {
source.then(s => {
drop.source = s;
drop.select(val, true);
drop.select(val, true, ignoreCase);
})
return;
} else if (source != null) {
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);
}
/**
@ -409,7 +514,10 @@ export class GridDropdownColumn extends GridColumn {
* @returns {any}
*/
static getValue(e, col) {
return e[col.dropOptions?.valueKey ?? 'value'];
return {
value: e[col.dropOptions?.valueKey ?? 'value'],
text: e[col.dropOptions?.textKey ?? 'text']
};
}
/**
@ -458,6 +566,11 @@ export class GridDropdownColumn extends GridColumn {
drop.onCollapsed();
}
}
/**
* @ignore
*/
static toString() { return 'GridDropdown' }
}
/**
@ -469,14 +582,18 @@ export class GridDropdownColumn extends GridColumn {
* @ignore
*/
export class GridCheckboxColumn extends GridColumn {
static get canEdit() { return true };
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @returns {HTMLElement}
*/
static createEdit(trigger) {
static createEdit(trigger, col) {
const check = createCheckbox({
onchange: typeof trigger === 'function' ? trigger : null
switch: col.switch,
onchange: trigger
});
return check;
}
@ -487,7 +604,8 @@ export class GridCheckboxColumn extends GridColumn {
* @param {boolean} val
*/
static setValue(element, val) {
element.querySelector('input').checked = val;
// element.querySelector('input').checked = val;
element.children[0].checked = val;
}
/**
@ -517,8 +635,44 @@ export class GridCheckboxColumn extends GridColumn {
*/
static setEnabled(element, enabled) {
super.setEnabled(element, enabled);
element.querySelector('input').disabled = enabled === false;
// element.querySelector('input').disabled = enabled === false;
element.children[0].disabled = enabled === false;
}
/**
* @ignore
*/
static toString() { return 'GridCheckbox' }
}
/**
* 单选框列
* @class
* @static
* @extends GridCheckboxColumn
* @hideconstructor
* @ignore
*/
export class GridRadioboxColumn extends GridCheckboxColumn {
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} _col
* @param {number} index
* @returns {HTMLElement}
*/
static createEdit(trigger, _col, index) {
const check = createRadiobox({
name: `r_${index}`,
onchange: trigger
});
return check;
}
/**
* @ignore
*/
static toString() { return 'GridRadiobox' }
}
/**
@ -590,6 +744,11 @@ export class GridIconColumn extends GridColumn {
element.classList.remove('disabled');
}
}
/**
* @ignore
*/
static toString() { return 'GridIcon' }
}
/**
@ -601,16 +760,17 @@ export class GridIconColumn extends GridColumn {
*/
export class GridDateColumn extends GridColumn {
static get editing() { return true };
static get canEdit() { return true };
/**
* @ignore
* @param {Function} _trigger
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @param {HTMLElement} _container
* @param {GridItemWrapper} wrapper
* @returns {HTMLElement}
*/
static createEdit(_trigger, col, _container, wrapper) {
static createEdit(trigger, col, _container, wrapper) {
let enabled = col.enabled;
if (typeof enabled === 'string') {
enabled = wrapper.values[enabled];
@ -621,15 +781,8 @@ export class GridDateColumn extends GridColumn {
return super.create();
}
const date = createDateInput(col.dateMin, col.dateMax);
date.addEventListener('change', () => {
if (wrapper.__editing == null) {
wrapper.__editing = {
[col.key]: true
}
} else {
wrapper.__editing[col.key] = true;
}
});
date.addEventListener('change', () => super._changeValue(col.key, wrapper, date.value));
date.addEventListener('blur', trigger);
return date;
}
@ -637,9 +790,11 @@ export class GridDateColumn extends GridColumn {
* @ignore
* @param {HTMLElement} element
* @param {(string | number | Date)} val
* @param {GridItemWrapper} _wrapper
* @param {GridColumnDefinition} col
*/
static setValue(element, val) {
setDateValue(element, val);
static setValue(element, val, _wrapper, col) {
setDateValue(element, val, col.dateDisplayFormatter);
}
/**
@ -649,7 +804,13 @@ export class GridDateColumn extends GridColumn {
* @returns {string}
*/
static getValue(e, col) {
return getDateValue(e.target, col.dateValueFormatter);
if (e.target.tagName === 'INPUT') {
return {
value: getDateValue(e.target, col.dateValueFormatter),
text: getDateValue(e.target, col.dateDisplayFormatter)
};
}
return e.target.innerText;
}
/**
@ -683,9 +844,36 @@ export class GridDateColumn extends GridColumn {
* * `"1/26/2024"`
* * `"638418240000000000"`
* * `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 的日期字符串
*/
static formatDate(date) {
return formatDate(date);
static formatDate(date, formatter) {
return formatDate(date, formatter);
}
/**
* @ignore
*/
static toString() { return 'GridDate' }
}

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 id 图标 id
* @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 矢量图标元素

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`
*/
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 { createElement } from "../functions";
import { createIcon } from "./icon";
import { get } from "../utility";
import { get } from "../utility/request";
export function createPicture(url) {
return createElement('a', a => {
@ -62,14 +62,24 @@ function playPcm(samples, ended) {
}
function getTimeLabel(time) {
time = Math.round(time);
return String(Math.floor(time / 60)).padStart(2, '0') + ':' + String(time % 60).padStart(2, '0');
// time = Math.round(time);
// 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) {
if ((mime === 'audio/amr' || mime === '.amr') && typeof AMR !== 'undefined') {
const timestamp = createElement('span', 'ui-media-timestamp');
timestamp.textContent = '00:00 / 00:00';
timestamp.textContent = '0:00 / 0:00';
let context;
let timer;
return createElement('div', 'ui-media-audio',
@ -80,7 +90,7 @@ export function createAudio(mime, url) {
clearInterval(timer);
context.close();
context = null;
timestamp.textContent = '00:00 / 00:00';
timestamp.textContent = '0:00 / 0:00';
button.className = 'play';
button.replaceChildren(createIcon('fa-solid', 'play'));
return;
@ -92,7 +102,7 @@ export function createAudio(mime, url) {
.then(r => playPcm(r, ctx => {
context = null;
clearInterval(timer);
timestamp.textContent = '00:00 / ' + getTimeLabel(ctx.duration);
timestamp.textContent = '0:00 / ' + getTimeLabel(ctx.duration);
button.className = '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;
}

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

@ -7,12 +7,16 @@ interface PopupOptions {
/** 弹出框标题,可以是文本或者 html 元素 */
title: string | HTMLElement;
/** 是否持久化显示 */
persistent?: boolean;
/** 是否包含遮罩层,默认为 `true` */
mask?: boolean;
/** 遮罩层 z-index */
zIndex?: number;
/** 是否在获取焦点时修改 z-index */
changeZIndex?: boolean;
/** 是否允许关闭 */
closable?: boolean;
/** 是否允许移动 */
movable?: boolean;
/** 是否允许修改大小 */
@ -59,15 +63,31 @@ interface PopupOptions {
/**
* 弹出框关闭时的回调函数
*/
resolve?: () => void;
resolve?: (this: Popup, result: { result: any, popup: Popup }) => void;
}
export class Popup {
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 {
tabIndex: number;
className?: string;
tabIndex?: number;
key: string;
text: string;
trigger: (this: Popup) => boolean | Promise<boolean>;
@ -83,12 +103,14 @@ interface PopupIconTypes {
}
interface PopupButtonResult {
key: string;
result: string;
popup: 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 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 { r } from "../utility/lgres";
import { r as lang } from "../utility/lgres";
import { nullOrEmpty } from "../utility/strings";
import { global } from "../utility";
import { createElement } from "../functions";
import { createIcon, changeIcon } from "./icon";
import { requestAnimationFrame } from "../ui";
const ResizeMods = {
right: 1,
@ -51,6 +52,30 @@ export class Popup {
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() {
const container = this.container;
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 doClose = () => {
if (option.persistent) {
mask.style.display = 'none';
} else {
mask.remove();
this._var.mask = null;
}
}
if (animation) {
mask.classList.add('ui-popup-active');
mask.style.opacity = 0;
setTimeout(() => { mask.remove(); }, 120);
setTimeout(() => { doClose(); }, 120);
} else {
mask.remove();
doClose();
}
if (typeof this._var.option.onMasking === 'function') {
this._var.option.onMasking.call(this, false);
if (typeof option.onMasking === 'function') {
option.onMasking.call(this, false);
}
if (typeof this._var.option.resolve === 'function') {
this._var.option.resolve();
if (typeof option.resolve === 'function') {
option.resolve.call(this, {
result,
popup: this
});
}
}
/**
* 创建 Popup 面板
* @returns {HTMLDivElement} 返回遮罩元素(顶层元素)
*/
create() {
const mask = createElement('div', 'ui-popup-mask');
const mask = createElement('div', 'ui-popup-mask ui-popup-active');
const option = this._var.option;
if (option.mask === false) {
mask.classList.add('ui-popup-transparent');
@ -172,7 +213,11 @@ export class Popup {
let title = option.title;
if (!(title instanceof HTMLElement)) {
title = createElement('div', t => {
if (option.movable === false) {
t.className = 'ui-popup-header-title no-move';
} else {
t.className = 'ui-popup-header-title';
}
t.innerText = title;
});
}
@ -244,6 +289,7 @@ export class Popup {
});
icons.appendChild(collapse);
}
if (option.closable !== false) {
const cancel = createIcon('fa-regular', 'times');
cancel.tabIndex = tabIndex + 3;
cancel.addEventListener('keypress', e => {
@ -253,6 +299,7 @@ export class Popup {
});
cancel.addEventListener('click', () => this.close());
icons.appendChild(cancel);
}
});
header.appendChild(icons);
}),
@ -265,6 +312,9 @@ export class Popup {
container.appendChild(
createElement('div', 'ui-popup-footer', ...option.buttons.map((b, i) => {
const button = createElement('button', 'ui-popup-button');
if (b.className != null) {
button.classList.add(b.className);
}
if (b.tabIndex > 0) {
button.tabIndex = b.tabIndex;
} else {
@ -277,14 +327,14 @@ export class Popup {
if (typeof result?.then === 'function') {
result.then(r => {
if (r !== false) {
this.close();
this.close(r);
}
}).catch(reason => console.warn(reason));
} else if (result !== false) {
this.close();
this.close(result);
}
} else {
this.close();
this.close(b.key ?? i);
}
});
return button;
@ -348,16 +398,20 @@ export class Popup {
return mask;
}
show(parent = document.body) {
show(parent = document.body, hidden = false) {
if (parent == null) {
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.querySelectorAll('.ui-popup-mask');
let zindex = 0;
for (let ex of exists) {
let z = parseInt(ex.style.zIndex);
let z = parseInt(global.getComputedStyle(ex).zIndex);
if (!isNaN(z) && z > zindex) {
zindex = z;
}
@ -366,6 +420,11 @@ export class Popup {
mask.style.zIndex = String(zindex + 1);
}
parent.appendChild(mask);
if (hidden === true) {
mask.style.display = 'none';
return Promise.resolve(mask);
}
}
if (this._var.option.mask === false) {
// calculator position
const container = this.container;
@ -373,27 +432,14 @@ export class Popup {
container.style.top = String((parent.offsetHeight - container.offsetHeight) / 2) + 'px';
}
return new Promise(resolve => {
setTimeout(() => {
mask.style.display = '';
requestAnimationFrame(() => {
mask.classList.remove('ui-popup-active');
mask.style.opacity = 1;
this.container.focus();
resolve(mask);
}, 0);
setTimeout(() => resolve(mask), 120);
});
});
}
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) {
@ -491,6 +537,43 @@ export function createPopup(title, content, ...buttons) {
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 = {
'info': 'info-circle',
'information': 'info-circle',
@ -501,6 +584,7 @@ const iconTypes = {
}
export function showAlert(title, message, iconType = 'info', parent = document.body) {
const r = typeof GetTextByKey === 'function' ? GetTextByKey : lang;
return new Promise(resolve => {
const popup = new Popup({
title,
@ -510,7 +594,7 @@ export function showAlert(title, message, iconType = 'info', parent = document.b
),
resolve,
buttons: [
{ text: r('ok', 'OK'), trigger: resolve }
{ text: r('ok', 'OK') }
]
});
popup.show(parent).then(mask => {
@ -521,6 +605,7 @@ export function showAlert(title, message, iconType = 'info', parent = document.b
}
export function showConfirm(title, content, buttons, iconType = 'question', parent = document.body) {
const r = typeof GetTextByKey === 'function' ? GetTextByKey : lang;
return new Promise(resolve => {
const wrapper = createElement('div', 'message-wrapper');
if (!nullOrEmpty(iconType)) {
@ -533,34 +618,23 @@ export function showConfirm(title, content, buttons, iconType = 'question', pare
title,
content: wrapper,
resolve,
buttons: buttons?.map(b => {
buttons: buttons?.map((b, i) => {
return {
text: b.text,
trigger: p => {
let result;
if (typeof b.trigger === 'function') {
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 {
result = {
key: b.key,
popup: p
};
resolve(result);
result = b.key ?? i;
}
return result;
}
};
}) ??
[
{ text: r('yes', 'Yes'), trigger: p => resolve({ key: 'yes', popup: p }) },
{ text: r('no', 'No'), trigger: p => resolve({ key: 'no', popup: p }) }
{ key: 'yes', text: r('yes', 'Yes') },
{ key: 'no', text: r('no', 'No') }
]
});
popup.show(parent).then(mask => {

43
lib/ui/tab.js Normal file
View File

@ -0,0 +1,43 @@
import "./css/tab.scss";
/**
* Tab 页创建参数类
* @typedef TabOption
* @property {HTMLElement} container - 父容器
*/
import { createElement } from "../functions";
/**
* 创建 Tab 页
* @param {TabOption | HTMLElement} options - 创建选项
*/
export function createTab(options) {
if (options instanceof HTMLElement) {
options = { container: options };
}
let container;
if (options?.container instanceof HTMLElement) {
container = options.container;
if (!container.classList.contains('ui-tab-container')) {
container.classList.add('ui-tab-container');
}
} else {
container = createElement('div', 'ui-tab-container');
}
container.replaceChildren(
createElement('div', header => {
header.className = 'ui-tab-header';
header.addEventListener('click', e => {
const title = e.target;
if (title.classList.contains('ui-tab-title')) {
// title
header.querySelectorAll('.ui-tab-title').forEach(t => t === title ? t.classList.add('selected') : t.classList.remove('selected'));
// pages
const page = title.dataset.for;
container.querySelectorAll('[data-page]').forEach(p => p.style.display = p.dataset.page === page ? 'block' : '');
}
});
})
);
}

View File

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

View File

@ -1,5 +1,5 @@
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 { nullOrEmpty, contains, endsWith, padStart, formatUrl, escapeHtml, escapeEmoji } from "./utility/strings";
@ -53,6 +53,27 @@ function isPhone(text) {
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 {
// cookie
getCookie,
@ -83,5 +104,8 @@ export {
debounce,
truncate,
isEmail,
isPhone
isPhone,
getPasswordStrength,
verifyPassword,
domLoad
}

View File

@ -1,3 +1,3 @@
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

View File

@ -1,8 +1,8 @@
export function setCookie(name, value, expireDays) {
export function setCookie(name, value, expireDays, host, encode) {
if (name == null) {
return;
}
let extra = `; domain=${location.host}; path=/`;
let extra = `; domain=${host ?? location.hostname}; path=/`;
if (expireDays != null) {
const d = new Date();
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)) {
extra += '; secure';
}
document.cookie = `${name}=${encodeURIComponent(value)}${extra}`;
if (encode !== false) {
value = encodeURIComponent(value);
}
document.cookie = `${name}=${value}${extra}`;
}
export function getCookie(name) {

View File

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

View File

@ -50,6 +50,8 @@ export function formatUrl(msg) {
path = consts.path;
} else if (typeof _network !== 'undefined') {
path = _network.root;
} else if (typeof _net !== 'undefined') {
path = _net.root;
}
for (let r of rs) {
msg = msg.replaceAll(r, `<a target="_blank" href="${r}"><svg><use xlink:href="${path || ''}fonts/fa-regular.svg#link"></use></svg></a>`);

59
main.js
View File

@ -1,59 +0,0 @@
import './style.scss'
// import javascriptLogo from './javascript.svg'
// import { get } from './lib/utility'
// import { createPicture, createAudio, createVideo, createPdf } from './lib/ui/media'
import './lib/element/style.scss'
import ScheduleItem from './lib/element/schedule'
import { createElement } from './lib/functions';
// document.querySelector('#js-logo').src = javascriptLogo
window.consts = {
path: '/',
resver: 20231218
}
const schedule = new ScheduleItem();
document.querySelector('#container').replaceChildren(
schedule.create(),
createElement('button', button => {
button.innerText = 'Get';
button.addEventListener('click', () => console.log(schedule.getParameters()));
})
);
// document.querySelector('#container').replaceChildren(
// // createPicture('https://fleet.foresightintelligence.com/doc/mmspart/1740581frZuuFhz5WWCysxs9oGB.jpg'),
// createAudio('audio/amr', 'http://vite.tsanie.org/1055003tb0DisaMu1615PeSXKG.amr'),
// createPdf('AG-PRO COMPANIES', 'https://fleet.foresightintelligence.com/doc/mmspart/1333321JLrYhkGYqsw6QSVMx3d.pdf'),
// // createPicture('https://fleet.foresightintelligence.com/doc/mmspart/138390UGZUMWRmqBsEgPnWuW16.gif'),
// // createVideo('https://fleet.foresightintelligence.com/doc/mmspart/17359338sR5qsG7TvS7eaUdP9PL.mp4'),
// );
/*
init(null, {
template: '/res.json',
callback: result => console.log(result)
}).then(() => {
// document.querySelector('#create-icon').appendChild(createIcon('fa-solid', 'user-edit'))
resolveIcon(document.querySelector('#create-icon'))
// document.querySelector('#create-checkbox').appendChild(createCheckbox({
// label: 'Switch 1'
// }))
resolveCheckbox(document.querySelector('#create-checkbox'))
resolveTooltip(document.querySelector('#buttons'))
document.querySelector('#button-fetch').addEventListener('click', () => {
get('javascript.svg', {
// contentType: '',
customHeaders: {
'X-Auth': 'test/authentication'
}
})
.then(r => r.blob())
.then(blob => document.querySelector('#js-logo').src = URL.createObjectURL(blob));
});
});
*/

1277
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "ui-lib",
"private": true,
"version": "1.0.1",
"version": "1.0.7",
"type": "module",
"files": [
"dist"
@ -28,14 +28,14 @@
"jsdoc-date": "jsdoc -c jsdoc-date.json"
},
"devDependencies": {
"@mxssfd/typedoc-theme": "^1.1.3",
"clean-jsdoc-theme": "^4.2.17",
"@mxssfd/typedoc-theme": "^1.1.6",
"clean-jsdoc-theme": "^4.3.0",
"docdash": "^2.0.2",
"jsdoc": "^4.0.2",
"postcss-preset-env": "^9.4.0",
"sass": "^1.71.0",
"typedoc": "^0.25.8",
"vite": "^5.1.3",
"jsdoc": "^4.0.3",
"postcss-preset-env": "^9.6.0",
"sass": "^1.77.8",
"typedoc": "^0.26.5",
"vite": "^5.3.4",
"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

4948
sample/fonts/fa-light.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.4 MiB

4948
sample/fonts/fa-regular.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.2 MiB

4948
sample/fonts/fa-solid.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.0 MiB

33
sample/index.html Normal file
View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UI Lib</title>
<link href="dist/ui.min.css" rel="stylesheet" />
<script src="amrnb.js"></script>
<script type="module" src="main.js"></script>
<script src="dist/ui.min.js"></script>
<script src="dist/utility.min.js"></script>
<style type="text/css">
#container>.ui-grid {
width: 1000px;
height: 400px;
}
</style>
</head>
<body>
<div style="display: flex; flex-direction: column; align-items: flex-start">
<div id="container"></div>
<!--<button id="setItem">set item</button>
<button id="addItem">add item</button>
<button id="addItems">add items</button>
<button id="removeItem">remove item</button>
<button id="removeItems">remove items</button>-->
</div>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 995 B

350
sample/main.js Normal file
View File

@ -0,0 +1,350 @@
// import './style.scss'
// import javascriptLogo from './javascript.svg'
// import { get } from './lib/utility'
// import { createPicture, createAudio, createVideo, createPdf } from './lib/ui/media'
// import './lib/element/style.scss'
// import ScheduleItem from './lib/element/schedule'
// import { createElement } from './lib/functions';
// import Grid from './lib/ui/grid/grid';
// document.querySelector('#js-logo').src = javascriptLogo
window.consts = {
path: '/',
resver: 20231218
}
// const DateSelector = window['lib-ui'].DateSelector;
// const formatDate = window['lib-ui'].formatDate;
// window.addEventListener('load', () => {
// DateSelector.resolve(document.querySelector('#container'), function (date) {
// console.log(`element(#${this.element.id}), date changed to: ${formatDate(date)}`);
// const value = document.querySelector('#dateFrom').value;
// console.log(`dateFrom.value = '${value}', formatted: '${formatDate(value)}'`);
// });
// });
const Grid = window['lib-ui'].Grid;
const createElement = window['lib-ui'].createElement;
const toDateValue = window['lib-ui'].toDateValue;
const showConfirm = window['lib-ui'].showConfirm;
window.addEventListener('load', () => {
const grid = new Grid('#container');
grid.columns = [
{
key: 'name',
// type: Grid.ColumnTypes.Common,
caption: 'Name',
captionStyle: {
'font-style': 'italic'
},
width: 150,
allowFilter: true,
totalCss: {
'text-align': 'right'
}
},
{
key: 'birthday',
type: Grid.ColumnTypes.Date,
caption: 'Birthday',
width: 120,
dateMin: '1900-01-01',
dateMax: '2025-01-01',
dateValueFormatter: toDateValue
},
{
key: 'age',
type: Grid.ColumnTypes.Input,
caption: 'Age',
enabled: false,
align: 'right',
filter: item => {
const ms = new Date() - new Date(item.birthday);
const age = Math.floor(ms / 1000 / 60 / 60 / 24 / 365);
return String(age);
}
},
{
key: 'sex',
type: Grid.ColumnTypes.Dropdown,
caption: 'Sex',
source: [
{ value: 'male', text: 'Male' },
{ value: 'female', text: 'Female' },
{ value: 'other', text: 'Other' }
]
},
{
key: 'active',
type: Grid.ColumnTypes.Checkbox,
caption: 'Active'
},
{
key: 'remove',
type: Grid.ColumnTypes.Icon,
text: 'times',
resizable: false,
sortable: false,
orderable: false,
tooltip: 'Remove',
events: {
onclick: function () {
showConfirm('Remove', `Are you sure you want to remove "${this.name}"?`, [
{
key: 'yes',
text: 'Yes',
trigger: () => {
console.log('yes');
return true;
}
},
{
key: 'no',
text: 'No'
}
], 'question')
}
}
}
];
// grid.height = 700 - 36;
// grid.autoResize = false;
grid.multiSelect = true;
// grid.expandable = true;
// grid.expandableGenerator = item => ({
// element: createElement('div', div => {
// div.innerText = JSON.stringify(item);
// })
// });
const fnames = '李王张刘陈杨赵黄周吴徐孙胡朱高林何郭马罗梁宋郑谢韩唐冯于董萧程曹袁邓许傅沈曾彭吕苏卢蒋蔡贾丁魏薛叶阎余潘杜戴夏钟汪田任姜范方石姚谭廖邹熊金陆郝孔白崔康毛邱秦江史顾侯邵孟龙万段漕钱汤尹黎易常武乔贺赖龚文';
const names = '先帝创业未半而中道崩殂今天下三分益州疲弊此诚危急存亡之秋也然侍卫之臣不懈于内忠志之士忘身于外者盖追先帝之殊遇欲报之于陛下也诚宜开张圣听以光先帝遗德恢弘志士之气不宜妄自菲薄引喻失义以塞忠谏之路也';
// grid.source = Array.from({ length: 200 }).map(() => {
// const r = Math.random();
// const r2 = Math.random();
// const date = new Date(631152000000 + Math.floor(20 * 365 * 24 * 60 * 60 * 1000 * r));
// return {
// name: `${fnames[Math.floor(r * fnames.length)]}${names[Math.floor(r2 * names.length)]}`,
// birthday: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`,
// sex: r > 0.5 ? 'female' : 'male',
// active: r2 > 0.5 ? true : false
// }
// });
const getCountFilter = key => (it) => {
const count = it[key];
if (isNaN(count)) {
return '';
}
// return count.toLocaleString();
return String(count).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
grid.columns = [
{ key: 'name', caption: '包名称', type: Grid.ColumnTypes.Input },
{ key: 'version', caption: '版本号' },
{ key: 'author', caption: '作者' },
{ key: 'count', caption: '下载量', filter: getCountFilter('count'), align: 'right' },
{ key: 'mauiName', caption: 'Maui 对应包' },
{ key: 'mauiVersion', caption: '版本号' },
{ key: 'mauiNet', caption: '.NET' },
{ key: 'mauiCount', caption: '下载量', filter: getCountFilter('mauiCount'), align: 'right' }
];
grid.source = [
{
name: 'Dynatrace.OneAgent.Xamarin',
version: '8.283.1',
author: 'Dynatrace',
count: 308681,
mauiName: 'Dynatrace.OneAgent.MAUI',
mauiVersion: '1.283.1',
mauiNet: '7.0',
mauiCount: 12351
},
{
name: 'Esri.ArcGISRuntime.Xamarin.Forms',
version: '100.15.4',
author: 'Esri_Inc',
count: 249909,
mauiName: 'Esri.ArcGISRuntime.Maui',
mauiVersion: '200.3.0',
mauiNet: '8.0',
mauiCount: 40913
},
{
name: 'Scandit.DataCapture.Core.Xamarin',
version: '6.22.0',
author: 'Scandit',
count: 527424,
mauiName: 'Scandit.DataCapture.Core.Maui',
mauiVersion: '6.22.0',
mauiNet: '6.0',
mauiCount: 14032
},
{
name: 'Scandit.DataCapture.Barcode.Xamarin',
version: '6.22.0',
author: 'Scandit',
count: 417823,
mauiName: 'Scandit.DataCapture.Barcode.Maui',
mauiVersion: '6.22.0',
mauiNet: '6.0',
mauiCount: 6048
},
{
name: 'SkiaSharp.Views.Forms',
version: '2.88.7',
author: 'Microsoft Xamarin',
count: 7229049,
mauiName: 'SkiaSharp.Views.Maui.Core',
mauiVersion: '2.88.7',
mauiNet: '7.0',
mauiCount: 558468
},
{
mauiName: 'SkiaSharp.Views.Maui.Controls',
mauiVersion: '2.88.7',
mauiNet: '7.0',
mauiCount: 550081
},
{
name: 'ZXing.Net.Mobile',
version: '2.4.1',
author: 'redth',
count: 7187155,
mauiName: 'ZXing.Net.Maui',
mauiVersion: '0.4.0',
mauiNet: '7.0',
mauiCount: 177002
},
{
name: 'ZXing.Net.Mobile.Forms',
version: '2.4.1',
author: 'redth',
count: 5494785,
mauiName: 'ZXing.Net.Maui.Controls',
mauiVersion: '0.4.0',
mauiNet: '7.0',
mauiCount: 104144
}
]
grid.init();
// setTimeout(() => {
// grid.total = { name: '合计', birthday: grid.source.length };
// }, 1000);
window.grid = grid;
});
/*
window.addEventListener('load', () => {
const grid = new Grid('#container');
grid.columns = ['a', 'b'].map(i => ({
key: i,
caption: `column ${i}`,
width: 200,
allowFilter: true
}));
grid.multiSelect = true;
grid.init();
const items = [];
for (let i = 0; i < 10; ++i) {
items.push({ a: i + 1, b: `row ${i + 1}` });
}
grid.source = items;
window.grid = grid;
});
document.querySelector('#setItem').addEventListener('click', () => {
if (window.grid.selectedIndex < 0) {
return;
}
window.grid.setItem(window.grid.selectedIndex, {
a: 'new',
b: 'new item'
});
});
document.querySelector('#addItem').addEventListener('click', () => {
window.grid.addItem({
a: 'add',
b: 'add item'
}, window.grid.selectedIndex);
});
document.querySelector('#addItems').addEventListener('click', () => {
window.grid.addItems([
{
a: 'add1',
b: 'add item 1'
},
{
a: 'add2',
b: 'add item 2'
},
{
a: 'add3',
b: 'add item 3'
}
], window.grid.selectedIndex);
});
document.querySelector('#removeItem').addEventListener('click', () => {
if (window.grid.selectedIndex < 0) {
return;
}
window.grid.removeItem(window.grid.selectedIndex);
});
document.querySelector('#removeItems').addEventListener('click', () => {
window.grid.removeItems(window.grid.selectedIndexes);
});
//*/
// const schedule = new ScheduleItem();
// document.querySelector('#container').replaceChildren(
// schedule.create(),
// createElement('button', button => {
// button.innerText = 'Get';
// button.addEventListener('click', () => console.log(schedule.getParameters()));
// })
// );
// document.querySelector('#container').replaceChildren(
// // createPicture('https://fleet.foresightintelligence.com/doc/mmspart/1740581frZuuFhz5WWCysxs9oGB.jpg'),
// createAudio('audio/amr', 'http://vite.tsanie.org/1055003tb0DisaMu1615PeSXKG.amr'),
// createPdf('AG-PRO COMPANIES', 'https://fleet.foresightintelligence.com/doc/mmspart/1333321JLrYhkGYqsw6QSVMx3d.pdf'),
// // createPicture('https://fleet.foresightintelligence.com/doc/mmspart/138390UGZUMWRmqBsEgPnWuW16.gif'),
// // createVideo('https://fleet.foresightintelligence.com/doc/mmspart/17359338sR5qsG7TvS7eaUdP9PL.mp4'),
// );
/*
init(null, {
template: '/res.json',
callback: result => console.log(result)
}).then(() => {
// document.querySelector('#create-icon').appendChild(createIcon('fa-solid', 'user-edit'))
resolveIcon(document.querySelector('#create-icon'))
// document.querySelector('#create-checkbox').appendChild(createCheckbox({
// label: 'Switch 1'
// }))
resolveCheckbox(document.querySelector('#create-checkbox'))
resolveTooltip(document.querySelector('#buttons'))
document.querySelector('#button-fetch').addEventListener('click', () => {
get('javascript.svg', {
// contentType: '',
customHeaders: {
'X-Auth': 'test/authentication'
}
})
.then(r => r.blob())
.then(blob => document.querySelector('#js-logo').src = URL.createObjectURL(blob));
});
});
*/

13
sample/web.config Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<mimeMap fileExtension=".amr" mimeType="audio/amr" />
</staticContent>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>

View File

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