Compare commits
17 Commits
0ff48a0ac4
...
master
Author | SHA1 | Date | |
---|---|---|---|
eec9d6045c
|
|||
a3f0288c92
|
|||
6b1e74790b
|
|||
adbf4750cc
|
|||
5baf00de64
|
|||
1a7aa1ab66
|
|||
d296dd01fd
|
|||
f5b6ce360e | |||
614a983aa8 | |||
ea7f4f538a | |||
190e43c814 | |||
a946012a33 | |||
f676ec76db | |||
4e8be83625 | |||
d91630212f | |||
5cbbcf8d81 | |||
b6fe3e34f5 |
35
README.md
@ -1,3 +1,36 @@
|
||||
# [ui-lib].Grid
|
||||
|
||||
UI Mordern Gridview Library
|
||||
UI Mordern Gridview Library
|
||||
|
||||
## 1.0.7
|
||||
* 调整: [(event) onBodyScrolled(e: Event, index: number, count: number)](Grid.html#event:onBodyScrolled) - 列滚动时触发的事件,增加显示行起始索引与一屏呈现行数
|
||||
* 新增: [`currentItem: GridRowItem | null`](Grid.html#currentItem) - 获取当前选中的行对象
|
||||
|
||||
## 1.0.6
|
||||
* 新增: [`col.switch`](GridColumnDefinition.html) - 列复选框样式改为 `ui-switch`
|
||||
|
||||
## 1.0.5
|
||||
* 新增: [`col`.filterTemplate: (this: GridColumnDefinition, item: ValueItem) => HTMLElement](GridColumnDefinition.html) - 列头过滤项的模板函数
|
||||
|
||||
## 1.0.4
|
||||
* 调整: `Dropdown` 组件支持虚模式
|
||||
* 新增: `ui-switch` 样式(iOS 切换组件)
|
||||
* 新增: `Dropdown` 组件增加 `ignoreAll` 参数
|
||||
|
||||
## 1.0.3
|
||||
* 调整: [showSortPanel(callback?: Function, layout?: boolean)](Grid.html#showSortPanel) 现支持输入搜索列,已添加的列不会重复显示在下拉数据源中,增加回调函数与 layout 更新复选框。
|
||||
* 新增: [onRowChanged(action: "update" | "add" | "remove", items: GridRowItem[], indexes: number | number[])](Grid.html#onRowChanged) - 行发生变化时触发的事件
|
||||
### bugs
|
||||
* 修复: 清空多列排序后列头箭头没有清除的异常。
|
||||
|
||||
## 1.0.2
|
||||
* 新增: [export(compressed: string | boolean, module?: string) : Promise<GridExportData>](Grid.html#export) - 导出已压缩的数据源
|
||||
|
||||
## 1.0.1
|
||||
* 新增: [total: GridRowItem](Grid.html#total) - 获取或设置合计行数据
|
||||
* 新增: [showSortPanel()](Grid.html#showSortPanel) - 显示多列排序设置面板
|
||||
* 新增: [setItem(index: number, item: GridRowItem)](Grid.html#setItem) - 设置单行数据
|
||||
* 新增: [addItem(item: GridRowItem, index?: number): GridRowItem](Grid.html#addItem) - 添加行数据
|
||||
* 新增: [addItems(array: GridRowItem[], index?: number) : GridRowItem[]](Grid.html#addItems) - 批量添加行数据
|
||||
* 新增: [removeItem(index: number) : GridRowItem](Grid.html#removeItem) - 删除行数据
|
||||
* 新增: [removeItems(indexes?: number[]): GridRowItem[]](Grid.html#removeItems) - 批量删除行数据
|
||||
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) });
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
@ -420,4 +420,99 @@ 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];
|
||||
})
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -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
260
lib/element/assetSelector.js
Normal 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();
|
||||
}
|
||||
}
|
189
lib/element/inspectionWizard.js
Normal 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' : '';
|
||||
}
|
||||
}
|
@ -1,17 +1,10 @@
|
||||
import { createElement, createCheckbox, createRadiobox, Dropdown, validation, toDateValue } from "../ui";
|
||||
import { r as lang } from "../utility";
|
||||
import { createElement, createCheckbox, createRadiobox, Dropdown, validation, toDateValue, OptionBase } from "../ui";
|
||||
|
||||
let r = lang;
|
||||
|
||||
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'); }
|
||||
@ -97,7 +90,7 @@ export default class ScheduleItem {
|
||||
}
|
||||
|
||||
create() {
|
||||
const option = this._var.option;
|
||||
const option = this._option;
|
||||
const drop = new Dropdown({ selected: '0' });
|
||||
this.dropFrequency = drop;
|
||||
drop.source = [
|
||||
@ -168,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',
|
||||
@ -189,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])$/
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -206,10 +199,16 @@ 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'),
|
||||
createElement('input', i => { i.type = 'date', i.className = 'ui-input schedule-id-duration-start', i.maxLength = 10 }),
|
||||
validation(
|
||||
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'),
|
||||
createElement('input', i => { i.type = 'date', i.className = 'ui-input schedule-id-duration-end', i.maxLength = 10 })
|
||||
validation(
|
||||
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',
|
||||
createCheckbox({ className: 'schedule-id-enabled', checked: true, label: 'Enabled' })
|
||||
|
151
lib/element/signature.js
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
@ -88,4 +96,227 @@
|
||||
.ui-drop-wrapper>.ui-drop-header {
|
||||
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;
|
||||
}
|
||||
}
|
142
lib/element/templateSelector.js
Normal 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;
|
||||
}
|
||||
}
|
69
lib/ui.js
@ -8,10 +8,61 @@ 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,
|
||||
@ -42,11 +93,13 @@ export {
|
||||
// popup
|
||||
Popup,
|
||||
createPopup,
|
||||
resolvePopup,
|
||||
showAlert,
|
||||
showConfirm,
|
||||
// dateSelector
|
||||
createDateInput,
|
||||
toDateValue,
|
||||
getFormatter,
|
||||
formatDate,
|
||||
setDateValue,
|
||||
getDateValue,
|
||||
@ -56,7 +109,15 @@ export {
|
||||
createAudio,
|
||||
createVideo,
|
||||
createFile,
|
||||
createVideoList,
|
||||
// extension
|
||||
validation,
|
||||
convertCssStyle
|
||||
convertCssStyle,
|
||||
// utility
|
||||
utility,
|
||||
// functions
|
||||
requestAnimationFrame,
|
||||
offset,
|
||||
// base classes
|
||||
OptionBase
|
||||
}
|
||||
|
2
lib/ui/checkbox.d.ts
vendored
@ -10,6 +10,8 @@ interface CheckboxOptions {
|
||||
name?: string;
|
||||
/** 焦点索引 */
|
||||
tabIndex?: Number;
|
||||
/** 是否为 switch 样式 */
|
||||
switch?: boolean;
|
||||
/** 样式分类,可以是 ['`fa-light`', '`fa-regular`', '`fa-solid`'] 其中之一 */
|
||||
type?: string;
|
||||
/** 标签 */
|
||||
|
@ -3,6 +3,10 @@ import { createElement } from "../functions";
|
||||
import { createIcon } from "./icon";
|
||||
|
||||
function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, charactor = 'check', title) {
|
||||
const checkIcon = createIcon(type, charactor);
|
||||
checkIcon.classList.add('ui-check-icon');
|
||||
const indeterminateIcon = createIcon(type, 'grip-lines');
|
||||
indeterminateIcon.classList.add('ui-indeterminate-icon')
|
||||
container.appendChild(
|
||||
createElement('layer', layer => {
|
||||
layer.className = 'ui-check-inner';
|
||||
@ -18,7 +22,7 @@ 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);
|
||||
@ -26,7 +30,9 @@ function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, char
|
||||
container.appendChild(
|
||||
createElement('span', span => {
|
||||
span.innerText = label;
|
||||
span.title = title;
|
||||
if (title != null) {
|
||||
span.title = title;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -60,12 +66,15 @@ export function createRadiobox(opts = {}) {
|
||||
}
|
||||
|
||||
export function createCheckbox(opts = {}) {
|
||||
const container = createElement('label', 'ui-check-wrapper',
|
||||
const container = createElement('label', opts.switch ? 'ui-switch' : 'ui-check-wrapper',
|
||||
createElement('input', input => {
|
||||
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;
|
||||
}
|
||||
@ -84,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) {
|
||||
|
@ -48,4 +48,101 @@
|
||||
color: var(--color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,8 @@
|
||||
|
||||
@include outborder();
|
||||
|
||||
&.validation-error {
|
||||
&.validation-error,
|
||||
&:invalid {
|
||||
border-color: var(--red-color);
|
||||
|
||||
&:focus,
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
>.ui-check-wrapper {
|
||||
height: $dropItemHeight;
|
||||
>.li-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
>.ui-expandor {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
>.ui-check-wrapper {
|
||||
height: $dropItemHeight;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -92,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;
|
||||
}
|
||||
|
@ -45,6 +45,7 @@
|
||||
--header-filter-padding: 4px 26px 4px 8px;
|
||||
--spacing-s: 4px;
|
||||
--spacing-cell: 9px 4px 9px 8px;
|
||||
--spacing-drop-cell: 5px 4px 5px 8px;
|
||||
--filter-line-height: 30px;
|
||||
--filter-item-padding: 0 4px;
|
||||
}
|
||||
@ -122,8 +123,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
>.ui-check-wrapper {
|
||||
>.ui-check-wrapper,
|
||||
>.ui-switch {
|
||||
height: 20px;
|
||||
padding: 0 4px 0 0;
|
||||
}
|
||||
|
||||
>svg {
|
||||
@ -398,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 {
|
||||
|
||||
@ -410,6 +415,14 @@
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
>span:first-of-type {
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-drop-span {
|
||||
@ -439,7 +452,7 @@
|
||||
height: 100%;
|
||||
|
||||
>.ui-drop-text {
|
||||
padding: var(--spacing-cell);
|
||||
padding: var(--spacing-drop-cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -705,6 +718,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ui-popup-mask .ui-popup-container .ui-popup-footer {
|
||||
>.ui-sort-layout {
|
||||
flex: 1 1 auto;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/*@media (prefers-color-scheme: dark) {
|
||||
.ui-grid {
|
||||
--cell-hover-bg-color: yellow;
|
||||
|
@ -65,4 +65,235 @@
|
||||
.ui-media-video {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
@ -76,7 +76,7 @@ $buttonHeight: 28px;
|
||||
|
||||
>.ui-popup-header-icons {
|
||||
flex: 0 0 auto;
|
||||
padding: 10px 12px 6px 0;
|
||||
padding: 5px 12px 3px 0;
|
||||
display: flex;
|
||||
|
||||
>svg {
|
||||
@ -145,8 +145,9 @@ $buttonHeight: 28px;
|
||||
margin: 10px;
|
||||
|
||||
>svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
fill: unset;
|
||||
|
||||
+span {
|
||||
padding-left: 16px;
|
||||
@ -204,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;
|
||||
@ -212,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;
|
||||
@ -227,6 +232,11 @@ $buttonHeight: 28px;
|
||||
}
|
||||
|
||||
@include outline();
|
||||
|
||||
&:disabled {
|
||||
opacity: .4;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
@ -36,11 +38,14 @@
|
||||
--border-radius: 2px;
|
||||
--text-indent: 4px;
|
||||
--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) {
|
||||
|
9
lib/ui/date.d.ts
vendored
@ -30,8 +30,9 @@ export function formatDate(date: Date | number | string, formatter?: string): st
|
||||
* 设置显示日期
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 从日期选择框获取日期值
|
||||
@ -39,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 {
|
||||
@ -86,6 +87,10 @@ export class DateSelector {
|
||||
maxDate?: string,
|
||||
/** 是否启用 */
|
||||
enabled?: boolean,
|
||||
/** 焦点索引 */
|
||||
tabIndex?: number,
|
||||
/** 类名 */
|
||||
className?: string,
|
||||
/**
|
||||
* 自定义格式化函数,用于获取日期值时调用
|
||||
* @param date 日期值
|
||||
|
@ -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;
|
||||
}
|
||||
@ -46,12 +51,12 @@ export function toDateValue(dt, local) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Date} date
|
||||
* @param {boolean} [utc=true]
|
||||
* @returns {string}
|
||||
* 获取日期格式器
|
||||
* @param {Date} date - 待格式的日期
|
||||
* @param {boolean} [utc=true] - 是否按 UTC 时间格式化
|
||||
* @returns {any} 返回格式化工具对象
|
||||
*/
|
||||
function getFormatter(date, utc) {
|
||||
export function getFormatter(date, utc) {
|
||||
const prefix = utc !== false ? 'getUTC' : 'get';
|
||||
const r = {
|
||||
/**
|
||||
@ -173,6 +178,7 @@ function getFormatter(date, utc) {
|
||||
* @param {Date | number | string} date - 需要格式化的日期值,支持的格式如下:
|
||||
*
|
||||
* * `"2024-01-26"`
|
||||
* * `"2024/1/26"`
|
||||
* * `"2024-01-26T00:00:00"`
|
||||
* * `"1/26/2024"`
|
||||
* * `"638418240000000000"`
|
||||
@ -203,6 +209,25 @@ function getFormatter(date, utc) {
|
||||
*/
|
||||
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);
|
||||
@ -220,14 +245,17 @@ export function formatDate(date, formatter) {
|
||||
* 设置显示日期
|
||||
* @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{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) {
|
||||
@ -241,7 +269,7 @@ export function setDateValue(element, val) {
|
||||
}
|
||||
} else {
|
||||
if (val instanceof Date) {
|
||||
element.value = toDateValue(val);
|
||||
element.value = toDateValue(val, true);
|
||||
} else {
|
||||
const ticks = Number(val);
|
||||
if (!isNaN(ticks) && ticks > 0) {
|
||||
@ -252,7 +280,7 @@ export function setDateValue(element, val) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
element.innerText = formatDate(val);
|
||||
element.innerText = formatDate(val, formatter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,7 +294,7 @@ 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) {
|
||||
@ -276,11 +304,15 @@ export function getDateValue(element, formatter) {
|
||||
if (year < 1900 || year > 9999) {
|
||||
return '';
|
||||
}
|
||||
if (typeof formatter === 'function') {
|
||||
if (formatter != null) {
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
// 使外部 formatter 不需要再处理 `getUTCDate` 亦或是 `getDate`
|
||||
return formatter(new Date(`${year}-${month}-${day}T00:00:00`));
|
||||
const localDate = new Date(`${year}-${month}-${day}T00:00:00`);
|
||||
if (typeof formatter === 'function') {
|
||||
return formatter(localDate);
|
||||
}
|
||||
return formatDate(localDate, formatter);
|
||||
}
|
||||
return String(date.getTime() * 1e4 + 621355968e9);
|
||||
}
|
||||
@ -314,6 +346,16 @@ export class DateSelector {
|
||||
* @private
|
||||
*/
|
||||
maxDate: null,
|
||||
/**
|
||||
* @type {number?}
|
||||
* @private
|
||||
*/
|
||||
tabIndex: null,
|
||||
/**
|
||||
* @type {string?}
|
||||
* @private
|
||||
*/
|
||||
className: null,
|
||||
/**
|
||||
* @type {DateFormatterCallback?}
|
||||
* @private
|
||||
@ -369,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;
|
||||
}
|
||||
|
6
lib/ui/dropdown.d.ts
vendored
@ -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 选中的值的列表
|
||||
|
@ -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') {
|
||||
@ -322,7 +369,7 @@ export class Dropdown {
|
||||
}
|
||||
if (expanded) {
|
||||
for (let li of this._var.container.querySelectorAll('li[data-value]')) {
|
||||
if (li.dataset.value === selected) {
|
||||
if ((ignoreCase ? li.dataset.value.toLowerCase() : li.dataset.value) === selected) {
|
||||
li.classList.add('selected');
|
||||
break;
|
||||
}
|
||||
@ -338,16 +385,18 @@ export class Dropdown {
|
||||
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,
|
||||
@ -358,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);
|
||||
@ -379,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 => {
|
||||
@ -391,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;
|
||||
@ -463,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') {
|
||||
@ -509,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) {
|
||||
this._var.allChecked = true;
|
||||
this._var.container.querySelector('input[isall="1"]').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);
|
||||
}
|
||||
} 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);
|
||||
item.__checked = checkbox.indeterminate ? 2 : checkbox.checked ? 1 : 0;
|
||||
const all = this._var.container.querySelector('input[isall="1"]');
|
||||
if (checkbox.checked) {
|
||||
const source = this.source;
|
||||
if (source.some(it => it.__checked) == null) {
|
||||
this._var.allChecked = true;
|
||||
if (all != null) {
|
||||
all.checked = true;
|
||||
}
|
||||
list = [];
|
||||
} else {
|
||||
list = source.filter(it => it.__checked);
|
||||
}
|
||||
} else {
|
||||
list = this.selectedList.filter(it => String(it[valuekey]) !== val);
|
||||
const val = checkbox.dataset.value;
|
||||
if (this._var.allChecked) {
|
||||
this._var.allChecked = false;
|
||||
if (all != null) {
|
||||
all.checked = false;
|
||||
}
|
||||
list = this.source.filter(it => String(getValue(it, valuekey, textkey)) !== val);
|
||||
} else {
|
||||
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') {
|
||||
|
@ -44,6 +44,14 @@ export class GridColumn {
|
||||
* @type {boolean}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 标记该类型是否支持列头批量操作
|
||||
* @member
|
||||
* @name GridColumn.headerEditing
|
||||
* @readonly
|
||||
* @type {boolean}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 创建显示单元格时调用的方法
|
||||
* @param {GridColumnDefinition} col - 列定义对象
|
||||
@ -445,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) {
|
||||
@ -460,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -555,10 +587,12 @@ export class GridCheckboxColumn extends GridColumn {
|
||||
/**
|
||||
* @ignore
|
||||
* @param {Function} trigger
|
||||
* @param {GridColumnDefinition} col
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
static createEdit(trigger) {
|
||||
static createEdit(trigger, col) {
|
||||
const check = createCheckbox({
|
||||
switch: col.switch,
|
||||
onchange: trigger
|
||||
});
|
||||
return check;
|
||||
@ -756,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -769,7 +805,10 @@ export class GridDateColumn extends GridColumn {
|
||||
*/
|
||||
static getValue(e, col) {
|
||||
if (e.target.tagName === 'INPUT') {
|
||||
return getDateValue(e.target, col.dateValueFormatter);
|
||||
return {
|
||||
value: getDateValue(e.target, col.dateValueFormatter),
|
||||
text: getDateValue(e.target, col.dateDisplayFormatter)
|
||||
};
|
||||
}
|
||||
return e.target.innerText;
|
||||
}
|
||||
|
3
lib/ui/icon.d.ts
vendored
@ -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 矢量图标元素
|
||||
|
@ -24,20 +24,27 @@ export function changeIcon(svg, type, id) {
|
||||
return svg;
|
||||
}
|
||||
|
||||
export function createIcon(type, id, style) {
|
||||
export function createIcon(type, id, style, action) {
|
||||
const svg = document.createElementNS(svgns, 'svg');
|
||||
svg.classList.add('ui-icon');
|
||||
svg.appendChild(createUse(type, id));
|
||||
if (style != null) {
|
||||
if (typeof style === 'function') {
|
||||
style(svg);
|
||||
} else if (style != null) {
|
||||
for (let css of Object.entries(style)) {
|
||||
svg.style.setProperty(css[0], css[1]);
|
||||
}
|
||||
}
|
||||
if (typeof action === 'function') {
|
||||
svg.addEventListener('click', action);
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
export function resolveIcon(container) {
|
||||
const svgs = container.querySelectorAll('svg[data-id]');
|
||||
for (let icon of svgs) {
|
||||
icon.classList.add('ui-icon');
|
||||
const type = icon.dataset.type;
|
||||
const id = icon.dataset.id;
|
||||
icon.replaceChildren(createUse(type, id));
|
||||
|
8
lib/ui/media.d.ts
vendored
@ -23,4 +23,10 @@ export function createVideo(url: string): HTMLVideoElement
|
||||
* @param url 文件 url
|
||||
* @param icon 图标,默认为 `file-alt`
|
||||
*/
|
||||
export function createFile(url: string, icon?: string): HTMLDivElement
|
||||
export function createFile(url: string, icon?: string): HTMLDivElement
|
||||
|
||||
/**
|
||||
* 创建联动视频元素
|
||||
* @param urls 视频 url 数组
|
||||
*/
|
||||
export function createVideoList(urls: string[]): HTMLDivElement
|
321
lib/ui/media.js
@ -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'));
|
||||
}))
|
||||
@ -139,4 +149,303 @@ export function createFile(url, icon = 'file-alt') {
|
||||
a.innerText = 'Click here to view the file';
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建联动视频元素
|
||||
* @param {string[]} urls - 视频 url 数组
|
||||
* @param {any} [options] - 播放参数
|
||||
* @param {boolean} options.[autoPlay] - 是否自动播放
|
||||
* @param {boolean} options.[autoFullScreen] - 是否自动全屏
|
||||
* @param {boolean} options.[autoLoop] - 是否循环播放
|
||||
* @param {Function} options.[onLoaded] - 视频加载完成回调
|
||||
* @param {Function} [callback] - 视频元素处理回调函数
|
||||
* @returns {HTMLDivElement} 返回联动视频元素
|
||||
*/
|
||||
export function createVideoList(urls, options, callback) {
|
||||
if (!Array.isArray(urls)) {
|
||||
urls = [urls];
|
||||
}
|
||||
const length = urls.length;
|
||||
const container = createElement('div', 'ui-media-video-container');
|
||||
let seekBufferBar;
|
||||
let seekProgressBar;
|
||||
let playIcon;
|
||||
let timeLabel;
|
||||
let volumeProgressBar;
|
||||
let volumeIcon;
|
||||
|
||||
const videos = Array(length);
|
||||
const waiting = Array(length);
|
||||
let prepared = 0;
|
||||
let ended = 0;
|
||||
|
||||
let playing;
|
||||
let duration = -1;
|
||||
let durationLabel;
|
||||
let seekBarWidth;
|
||||
let mousing;
|
||||
const controller = createElement('div', 'ui-video-control active',
|
||||
createElement('div', seek => {
|
||||
seek.className = 'ui-video-bar seek-bar';
|
||||
seek.addEventListener('mousedown', e => {
|
||||
if (prepared < length || waiting.find(p => p) != null) {
|
||||
return;
|
||||
}
|
||||
const width = seekBarWidth = seek.offsetWidth;
|
||||
const currentTime = Math.min(Math.max(e.offsetX * duration / width, 0), duration);
|
||||
videos.forEach(video => typeof video.fastSeek === 'function' ?
|
||||
video.fastSeek(currentTime) :
|
||||
(video.currentTime = currentTime));
|
||||
mousing = true;
|
||||
e.stopPropagation();
|
||||
});
|
||||
seek.addEventListener('mousemove', e => {
|
||||
if (mousing) {
|
||||
if (!e.buttons) {
|
||||
mousing = false;
|
||||
} else {
|
||||
const v = Math.min(Math.max(e.offsetX / seekBarWidth, 0), 1);
|
||||
seekProgressBar.style.width = `${(v * 100).toFixed(2)}%`;
|
||||
}
|
||||
}
|
||||
});
|
||||
seek.addEventListener('mouseup', e => {
|
||||
if (mousing) {
|
||||
mousing = false;
|
||||
const currentTime = Math.min(Math.max(e.offsetX * duration / seekBarWidth, 0), duration);
|
||||
videos.forEach(video => typeof video.fastSeek === 'function' ?
|
||||
video.fastSeek(currentTime) :
|
||||
(video.currentTime = currentTime));
|
||||
}
|
||||
});
|
||||
},
|
||||
createElement('div', 'ui-video-duration seek-duration'),
|
||||
seekBufferBar = createElement('div', 'seek-buffers'),
|
||||
seekProgressBar = createElement('div', 'ui-video-progress seek-progress')
|
||||
),
|
||||
playIcon = createElement('div', 'ui-video-icon play-icon',
|
||||
createIcon('fa-solid', 'play'),
|
||||
createIcon('fa-solid', 'pause')
|
||||
),
|
||||
timeLabel = createElement('div', 'ui-video-time-label'),
|
||||
createElement('div', 'ui-video-volume-container',
|
||||
createElement('div', volume => {
|
||||
volume.className = 'ui-video-bar volume-bar';
|
||||
volume.addEventListener('mousedown', e => {
|
||||
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
video.volume = v;
|
||||
}
|
||||
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
|
||||
mousing = true;
|
||||
e.stopPropagation();
|
||||
});
|
||||
volume.addEventListener('mousemove', e => {
|
||||
if (mousing) {
|
||||
if (!e.buttons) {
|
||||
mousing = false;
|
||||
} else {
|
||||
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
|
||||
volumeProgressBar.style.width = `${(v * 100).toFixed(2)}%`;
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
video.volume = v;
|
||||
}
|
||||
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
|
||||
}
|
||||
}
|
||||
});
|
||||
volume.addEventListener('mouseup', e => {
|
||||
if (mousing) {
|
||||
mousing = false;
|
||||
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
video.volume = v;
|
||||
}
|
||||
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
|
||||
}
|
||||
});
|
||||
},
|
||||
createElement('div', 'ui-video-duration video-duration'),
|
||||
volumeProgressBar = createElement('div', 'ui-video-progress video-progress')
|
||||
),
|
||||
volumeIcon = createElement('div', 'ui-video-icon volume-icon',
|
||||
createIcon('fa-solid', 'volume'),
|
||||
createIcon('fa-solid', 'volume-mute')
|
||||
)
|
||||
)
|
||||
);
|
||||
controller.addEventListener('mousedown', () => {
|
||||
if (prepared < length || waiting.find(p => p) != null) {
|
||||
// not prepared.
|
||||
return;
|
||||
}
|
||||
if (playing) {
|
||||
videos.forEach(video => video.pause());
|
||||
} else {
|
||||
ended = 0;
|
||||
videos.forEach(video => video.play().catch(() => { }));
|
||||
}
|
||||
});
|
||||
volumeIcon.addEventListener('mousedown', e => {
|
||||
if (volumeIcon.classList.contains('disabled')) {
|
||||
return;
|
||||
}
|
||||
const video = videos[0];
|
||||
if (video == null) {
|
||||
return;
|
||||
}
|
||||
if (volumeIcon.classList.contains('muted')) {
|
||||
video.volume = 1;
|
||||
volumeIcon.classList.remove('muted');
|
||||
} else {
|
||||
video.volume = 0;
|
||||
volumeIcon.classList.add('muted');
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
if (length === 1) {
|
||||
controller.append(createElement('div', icon => {
|
||||
icon.className = 'ui-video-icon fullscreen-icon';
|
||||
icon.addEventListener('mousedown', e => {
|
||||
if (prepared < length) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
if (document.fullscreenElement == null) {
|
||||
video.requestFullscreen().catch(() => { });
|
||||
} else if (typeof document.exitFullscreen === 'function') {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
createIcon('fa-regular', 'expand')
|
||||
));
|
||||
} else {
|
||||
controller.classList.add('no-fullscreen');
|
||||
}
|
||||
|
||||
const content = createElement('div', 'ui-video-content');
|
||||
container.append(content, controller);
|
||||
|
||||
urls.forEach((url, i) => {
|
||||
const video = createElement('video');
|
||||
videos[i] = video;
|
||||
video.src = url;
|
||||
video.addEventListener('durationchange', () => {
|
||||
const d = video.duration;
|
||||
if (d > duration) {
|
||||
duration = d;
|
||||
durationLabel = getTimeLabel(d);
|
||||
timeLabel.innerText = '0:00 / ' + durationLabel;
|
||||
}
|
||||
});
|
||||
video.addEventListener('loadeddata', () => {
|
||||
if (i === 0) {
|
||||
volumeProgressBar.style.width = `${(video.volume * 100).toFixed(2)}%`;
|
||||
} else {
|
||||
video.volume = 0;
|
||||
}
|
||||
prepared += 1;
|
||||
if (prepared >= length) {
|
||||
if (options?.autoPlay) {
|
||||
// auto play
|
||||
videos.forEach(v => v.play().catch(() => { }));
|
||||
if (options?.autoFullScreen && length === 1 && document.fullscreenElement == null) {
|
||||
video.requestFullscreen().catch(() => { });
|
||||
}
|
||||
}
|
||||
if (typeof options?.onLoaded === 'function') {
|
||||
options.onLoaded();
|
||||
}
|
||||
}
|
||||
});
|
||||
video.addEventListener('progress', () => {
|
||||
const buffered = video.buffered;
|
||||
for (let i = 0; i < buffered.length; ++i) {
|
||||
let buffer = seekBufferBar.children[i];
|
||||
if (buffer == null) {
|
||||
seekBufferBar.append(buffer = createElement('div', 'ui-video-buffer'));
|
||||
}
|
||||
const start = buffered.start(i) * 100 / duration;
|
||||
const end = buffered.end(i) * 100 / duration;
|
||||
buffer.style.left = `${start.toFixed(2)}%`;
|
||||
buffer.style.width = `${(end - start).toFixed(2)}%`;
|
||||
}
|
||||
for (let i = seekBufferBar.children.length - 1; i >= buffered.length; i -= 1) {
|
||||
seekBufferBar.children[i].remove();
|
||||
}
|
||||
});
|
||||
video.addEventListener('canplaythrough', () => {
|
||||
const w = video.parentElement.querySelector('.ui-video-waiting');
|
||||
if (w != null) {
|
||||
w.style.opacity = 0;
|
||||
}
|
||||
if (waiting[i]) {
|
||||
waiting[i] = false;
|
||||
if (waiting.find(p => p) == null) {
|
||||
videos.forEach(v => v.play().catch(() => { }));
|
||||
}
|
||||
}
|
||||
});
|
||||
video.addEventListener('pause', () => {
|
||||
playing = false;
|
||||
controller.classList.add('active');
|
||||
playIcon.classList.remove('pause');
|
||||
});
|
||||
video.addEventListener('playing', () => {
|
||||
playing = true;
|
||||
controller.classList.remove('active');
|
||||
playIcon.classList.add('pause');
|
||||
});
|
||||
video.addEventListener('waiting', () => {
|
||||
waiting[i] = true;
|
||||
const w = video.parentElement.querySelector('.ui-video-waiting');
|
||||
if (w != null) {
|
||||
w.style.opacity = 1;
|
||||
}
|
||||
videos.forEach(v => v.pause());
|
||||
});
|
||||
video.addEventListener('ended', () => {
|
||||
ended += 1;
|
||||
if (ended >= length) {
|
||||
if (options?.autoLoop) {
|
||||
videos.forEach(v => v.play().catch(() => { }));
|
||||
} else {
|
||||
playing = false;
|
||||
controller.classList.add('active');
|
||||
seekProgressBar.style.width = '0';
|
||||
playIcon.classList.remove('pause');
|
||||
timeLabel.innerText = '0:00 / ' + durationLabel;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (i === 0) {
|
||||
video.addEventListener('timeupdate', () => {
|
||||
seekProgressBar.style.width = `${(video.currentTime * 100 / duration).toFixed(2)}%`;
|
||||
timeLabel.innerText = getTimeLabel(video.currentTime) + ' / ' + durationLabel;
|
||||
});
|
||||
video.addEventListener('volumechange', () => {
|
||||
volumeProgressBar.style.width = `${(video.volume * 100).toFixed(2)}%`;
|
||||
});
|
||||
}
|
||||
const wrapper = createElement('div', 'ui-video-wrapper',
|
||||
video,
|
||||
createElement('div', 'ui-video-waiting',
|
||||
createIcon('fa-regular', 'spinner-third')
|
||||
)
|
||||
);
|
||||
if (typeof callback === 'function') {
|
||||
callback(wrapper);
|
||||
}
|
||||
content.append(wrapper);
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
26
lib/ui/popup.d.ts
vendored
@ -7,6 +7,8 @@ interface PopupOptions {
|
||||
/** 弹出框标题,可以是文本或者 html 元素 */
|
||||
title: string | HTMLElement;
|
||||
|
||||
/** 是否持久化显示 */
|
||||
persistent?: boolean;
|
||||
/** 是否包含遮罩层,默认为 `true` */
|
||||
mask?: boolean;
|
||||
/** 遮罩层 z-index */
|
||||
@ -61,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>;
|
||||
@ -85,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>
|
186
lib/ui/popup.js
@ -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');
|
||||
@ -271,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 {
|
||||
@ -283,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;
|
||||
@ -354,24 +398,33 @@ 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();
|
||||
// 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);
|
||||
if (!isNaN(z) && z > zindex) {
|
||||
zindex = z;
|
||||
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(global.getComputedStyle(ex).zIndex);
|
||||
if (!isNaN(z) && z > zindex) {
|
||||
zindex = z;
|
||||
}
|
||||
}
|
||||
if (zindex > 0) {
|
||||
mask.style.zIndex = String(zindex + 1);
|
||||
}
|
||||
parent.appendChild(mask);
|
||||
if (hidden === true) {
|
||||
mask.style.display = 'none';
|
||||
return Promise.resolve(mask);
|
||||
}
|
||||
}
|
||||
if (zindex > 0) {
|
||||
mask.style.zIndex = String(zindex + 1);
|
||||
}
|
||||
parent.appendChild(mask);
|
||||
if (this._var.option.mask === false) {
|
||||
// calculator position
|
||||
const container = this.container;
|
||||
@ -379,29 +432,16 @@ 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) {
|
||||
if (e.buttons !== 1) {
|
||||
return;
|
||||
@ -497,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',
|
||||
@ -507,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,
|
||||
@ -516,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 => {
|
||||
@ -527,6 +605,7 @@ export function showAlert(title, message, iconType = 'info', parent = document.b
|
||||
}
|
||||
|
||||
export function showConfirm(title, content, buttons, iconType = 'question', parent = document.body) {
|
||||
const r = typeof GetTextByKey === 'function' ? GetTextByKey : lang;
|
||||
return new Promise(resolve => {
|
||||
const wrapper = createElement('div', 'message-wrapper');
|
||||
if (!nullOrEmpty(iconType)) {
|
||||
@ -539,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 => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
}
|
2
lib/utility/cookie.d.ts
vendored
@ -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
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
1193
package-lock.json
generated
16
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ui-lib",
|
||||
"private": true,
|
||||
"version": "1.0.2",
|
||||
"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.18",
|
||||
"@mxssfd/typedoc-theme": "^1.1.6",
|
||||
"clean-jsdoc-theme": "^4.3.0",
|
||||
"docdash": "^2.0.2",
|
||||
"jsdoc": "^4.0.2",
|
||||
"postcss-preset-env": "^9.5.2",
|
||||
"sass": "^1.72.0",
|
||||
"typedoc": "^0.25.12",
|
||||
"vite": "^5.2.6",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 350 KiB After Width: | Height: | Size: 430 KiB |
Before Width: | Height: | Size: 651 KiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 600 KiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 510 KiB After Width: | Height: | Size: 1.0 MiB |
@ -6,11 +6,11 @@
|
||||
<!-- <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>
|
||||
<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;
|
||||
|
@ -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: {
|
||||
|