Compare commits

...

110 Commits

Author SHA1 Message Date
eec9d6045c sync 2024-08-30 17:36:21 +08:00
a3f0288c92 sync 2024-07-22 10:42:32 +08:00
6b1e74790b . 2024-07-12 14:57:06 +08:00
adbf4750cc sync 2024-07-10 12:19:52 +08:00
5baf00de64 add: getText compatibility.
add: `AssetSelector` and `TemplateSelector`.
add: `popup-selector` style class.
add: `ui.resolvePopup` function.
add: `switch` in checkbox.
add: `GridColumn.filterTemplate` supports.
add: add `action` callback in `createIcon`.
change: replace `setTimeout(..., 0)` with `requestAnimationFrame`.
change: Popup result structure adjustment ({ result: any, popup: Popup }).
change: complete add work order flow.
change: reduce Popup title height.
fix: Grid column sort in number.
2024-06-21 17:28:11 +08:00
1a7aa1ab66 add ui-icon className. 2024-06-11 14:52:26 +08:00
d296dd01fd add cjs-dist format 2024-06-07 16:32:50 +08:00
f5b6ce360e . 2024-06-07 16:16:10 +08:00
614a983aa8 sync 2024-06-07 16:15:42 +08:00
ea7f4f538a add: ui-switch style
add: virtual mode in Dropdown
2024-05-27 17:06:00 +08:00
190e43c814 feature: tiered sort 2024-05-22 09:27:22 +08:00
a946012a33 feature: drag support in sort panel. 2024-05-13 16:46:55 +08:00
f676ec76db remove dependency of jQuery plugins.
Grid.expandAll
add total field of exported data.
2024-04-24 14:10:28 +08:00
4e8be83625 feature: seek buffer bars. 2024-04-16 10:39:51 +08:00
d91630212f compatible with Chrome 109.x 2024-04-15 16:30:47 +08:00
5cbbcf8d81 feature: combined-video player. 2024-04-11 17:29:26 +08:00
b6fe3e34f5 issue: customer communication contacts grid height.
change: replace schedule date control.
change: date validation from 1753-01-01 to 9999-12-31.
2024-04-07 08:39:44 +08:00
0ff48a0ac4 change: replace date controller of Scheduler.
feature: `ui.formatDate` supports formatter.
fix: issue about boolean sort.
2024-03-28 16:37:22 +08:00
03e3b4a70f add: column.filterAsValue
fix: case-sensitive order of Array value
2024-03-27 13:36:31 +08:00
c78e445a24 add: comments 2024-03-27 10:09:25 +08:00
6458427c75 fix: issue when input 'MM/dd/yyyy' date format 2024-03-27 10:08:54 +08:00
87e3d6c81b fix: date UTC conversion issue
fix: `type.create` index offset issue
2024-03-27 02:30:04 +08:00
fd8899c597 package updates 2024-03-25 14:52:15 +08:00
f46e25952c fix: issue while using ui.min.js with requireJs
fix: clear `enabledDict` after `init`
fix: set `display: none` after tooltip holder disappeared
2024-03-21 15:30:22 +08:00
18f8cc322d fix: cell style issue while there is no style definition.
optimize: Grid.export
2024-03-13 17:02:04 +08:00
e07342a257 fix: tooltip issue about customer follower
change: Grid.export optimized (compression with `deflate`)
feature: ui-tab
feature: Popup.closable
2024-03-12 13:36:54 +08:00
f54eb3ac24 fix: wrap style in drop column.
fix: also change `DisplayValue` after changed.
feature: support uncompressed export.
change: add `editing` parameter in GridColumn.setEnabled.
2024-03-08 17:26:38 +08:00
70ab06190f change: structure of GridDropdownColumn 2024-03-08 11:02:04 +08:00
39378e9963 feature: caption help text
fix: editing style when turning column content wrap to `true`
2024-03-08 10:51:06 +08:00
3ea7ce62bb fix: reset startIndex after reload. 2024-03-08 08:27:30 +08:00
e75e763938 change: sample structure
fix: tooltip of some readonly column in editing mode
feature: adapt filter panel's height
2024-03-07 17:16:37 +08:00
168cae3ce1 feature: tooltip in the checkbox
feature: add GridColumn.getElement
feature: column property of `contentWrap` and `maxLines`
change: line height from 24px to 18px
fix: adapt enabled after cell changed
fix: wrap issue of filter panel
2024-03-07 12:54:00 +08:00
6fb7c3c769 move sample files 2024-03-05 15:03:53 +08:00
baec3a3959 clean code 2024-03-05 14:17:16 +08:00
6de4987360 fix: change vCell value after changed. 2024-03-05 08:43:07 +08:00
4096979d5d fix: vCell condition issue 2024-03-04 15:38:04 +08:00
1ed5b39308 optimize: change granularity is more fine-grained, only the changed cell elements are modified. 2024-03-04 15:17:31 +08:00
975d54fee1 . 2024-03-04 11:20:03 +08:00
a28b56b191 optimize: introduce vCell, render time while scrolling. 2024-03-04 11:18:08 +08:00
20a8fbae02 fix: body scrollTop issue when column filtered. 2024-03-01 08:54:54 +08:00
5e53d04174 fix: do not change scroll position when refreshing grid.
fix: change `button` to `span`, avoiding conflict styles.
2024-02-28 10:29:19 +08:00
38bad864e0 fix: header z-index issue.
fix: incorrect `onChanged` event trigger on `GridInputColumn`, `GridTextColumn` and `GridDateColumn`.
2024-02-26 16:11:42 +08:00
7940edbea2 fix: grid column border-bottom issue. 2024-02-23 11:01:43 +08:00
c21f62b7b8 fix: grid hover position issue. 2024-02-23 10:52:30 +08:00
dc2b67a33c optimized 2024-02-23 09:54:13 +08:00
63a50e6bcb feature: default wrap headers. 2024-02-23 09:29:51 +08:00
c153b5a4c1 feature: supports different icons in unfiltered/filtered status. 2024-02-22 15:51:44 +08:00
cad5a99eff fix: onChanged issue on Firefox. (GridDateColumn)
feature: supports filterValues on Array field.
2024-02-22 14:53:58 +08:00
fdeefb4e1a feature: make total become a property. 2024-02-21 15:39:50 +08:00
0cb4802e1f optimize comments 2024-02-21 10:38:59 +08:00
5797c2a9cf fix: error rowIndex when there were expanded rows.
optimize: load
2024-02-20 16:53:38 +08:00
d17abdf4b2 fix issue 2024-02-20 15:44:28 +08:00
0e0b46830e optimize GridRowItem comment. 2024-02-20 15:39:21 +08:00
45205cd63d . 2024-02-20 14:31:24 +08:00
3d2c0e7a97 add more comments 2024-02-20 10:11:59 +08:00
7770aa10a0 sync issue fixes, and add more comments. 2024-02-19 17:32:11 +08:00
3fd1b5982a add: grid.filterIcon 2024-02-18 14:17:15 +08:00
6dc9df8b17 optimized 2024-02-05 16:24:54 +08:00
09bdef19d5 add: Grid.readonly 2024-02-05 16:18:27 +08:00
8712554f12 add grid flowchart 2024-02-05 14:25:28 +08:00
8a9ba4d4fb fix: Date Column editing issue.
docs: add some examples.
2024-02-05 09:52:26 +08:00
12680cc662 GridColumnDefinition examples 2024-02-04 16:20:58 +08:00
3fb191ee13 fix: column splitter appearance 2024-02-04 13:58:37 +08:00
a74436ac1f feature: add col.filterAllowNull 2024-02-04 13:15:42 +08:00
bed5c1aca1 add more comments 2024-02-02 16:02:34 +08:00
5d199a2bfb feature: expandable rows
add: comments
2024-02-01 17:29:30 +08:00
24b5a22595 add sample screenshot 2024-01-31 09:33:07 +08:00
a38af7577b change to jsdoc 2024-01-30 17:28:18 +08:00
0b897cae8a export DateSelector, fix issue about column.align and filter panel position of the last header cell. 2024-01-30 11:16:57 +08:00
df42221b52 add: dateSelector 2024-01-29 13:51:58 +08:00
0696ecaff0 fix some issues 2024-01-29 12:44:03 +08:00
56262d6766 feature: addItems and removeItems 2024-01-29 12:14:35 +08:00
b5fd20aa2c Grid.removeItem issue 2024-01-29 09:29:37 +08:00
fc61731c7d resolve issue of addItem, removeItem, update definition of get source() 2024-01-27 15:47:15 +08:00
ac605895c5 add internal sort panel. 2024-01-26 17:27:24 +08:00
984496e08e fixed: column drag issue.
optimized: documentation.
2024-01-26 14:09:52 +08:00
0b9b322b80 add theme @mxssfd/typedoc-theme, upgrade packages. 2024-01-25 17:05:00 +08:00
724840019b add icon dictionary 2024-01-25 16:19:05 +08:00
b50440a5b7 grid multi-select issue 2024-01-25 16:00:29 +08:00
41cb7b0142 npm run doc 2024-01-25 15:02:52 +08:00
9ab9edb44d sync 2024-01-25 14:56:31 +08:00
07f87bfb9d . 2024-01-23 15:58:56 +08:00
09e62c4304 leave edit event 2024-01-23 10:40:49 +08:00
27b2f1052f rename some events. 2024-01-23 09:22:08 +08:00
10e4e52b51 add docs 2024-01-23 09:07:15 +08:00
3d9c5bd3f2 adjustment 2024-01-19 15:47:42 +08:00
feec8b59a7 issue fix 2024-01-18 17:25:44 +08:00
fb9e920c15 sync 2024-01-17 17:31:41 +08:00
84190ed9f1 sync 2023-08-28 15:04:23 +08:00
29209a3a00 adjustment 2023-07-27 10:03:53 +08:00
3e9ee59178 sync working code, and import type-doc 2023-07-17 17:24:49 +08:00
7ab7a7094a communication followers: mobile phone display text 2023-06-06 14:50:51 +08:00
ca22a60b83 . 2023-06-06 14:40:45 +08:00
603c92b37d tooltip position optimization 2023-06-06 14:24:02 +08:00
c045aac4f9 contact tooltip issue 2023-06-06 13:50:05 +08:00
99d4aa59a8 tooltip position issue (for tiny space) 2023-06-05 17:16:12 +08:00
2366b378d2 UI style adjustment 2023-06-05 15:21:56 +08:00
93d5be462e sync 2023-06-05 10:09:25 +08:00
a930a2bf5b sync from server 2023-05-30 17:34:14 +08:00
98a45a6f19 sync 2023-05-30 09:13:02 +08:00
8dafc1c5f4 sync 2023-05-05 15:35:58 +08:00
9bf9e7da27 sync 2023-04-28 15:06:47 +08:00
58de68a878 resolve dropdown tabIndex issue 2023-04-28 14:25:10 +08:00
25ff61fe47 optimize 2023-04-26 10:02:21 +08:00
7419062049 vDOM optimized 2023-04-26 09:51:00 +08:00
54d4c4c4c0 start vtable mode 2023-04-25 17:33:27 +08:00
0be2078c45 virtual mode issue fix. 2023-04-25 15:51:43 +08:00
d7728ebfd6 sync 2023-04-25 14:40:03 +08:00
08e58b3abc Merge remote-tracking branch 'remotes/origin/master' 2023-04-24 17:39:22 +08:00
811467bc7a grid filter 2023-04-24 17:38:48 +08:00
103 changed files with 31571 additions and 12758 deletions

4
.gitignore vendored
View File

@ -23,3 +23,7 @@ dist-ssr
*.sln
*.sw?
desktop.ini
# User definition
docs
jsdoc

View File

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

95
count.cjs Normal file
View File

@ -0,0 +1,95 @@
/**
* @author HoPGoldy
* @source https://www.jianshu.com/p/333795b9893f
* @date 2020-1-7
*
* 统计指定目录下代码行数及注释率
*
* 用法: node count.js <路径> [后缀名]...
* 后缀名不填的话默认为统计 .js 和 .ts 文件
*
* 示例 [统计 ./src 下的 js 文件]: node count.js ./src
* 示例 [统计 ./dist 下的 java 文件]: node count.js ./src .java
*/
const fs = require('fs')
let path = require('path')
// 获取命令行参数
const parm = process.argv.splice(2)
// 第一个参数是路径
const rootPath = parm[0]
// 后面的所有参数都是文件后缀
let types = parm.splice(1)
if (types.length === 0) types = [ '.js', '.ts' ]
// 需要过滤的文件夹
const filter = [ './node_modules', './.git', './.tscache' ]
// 总计
let total = {
path: 'total',
length: 0,
comment: 0,
commentRatio: 1
}
// 统计结果
let result = []
/**
* 对指定文件进行统计
* 包括获取文件行数、注释及计算注释率
*
* @param {string} path 文件路径
*/
async function count(path) {
const rep = await fs.readFileSync(path).toString()
const lines = rep.split('\n')
// 匹配出注释的行数
const commentNum = lines.filter(line => new RegExp('^(//|/\\*|\\*|\\*/)', 'g').test(line.trimStart())).length
result.push({
path,
length: lines.length,
comment: commentNum,
commentRatio: (Math.round(commentNum/lines.length * 10000) / 100) + '%'
})
updateTotal(lines.length, commentNum)
}
/**
* 更新总计信息
*
* @param {number} length 新增行数
* @param {number} comment 新增注释
*/
function updateTotal(length, comment) {
total.length += length
total.comment += comment
total.commentRatio = (Math.round(total.comment/total.length * 10000) / 100) + '%'
}
/**
* 递归所有文件夹统计
*
* @param {string} pt 根目录
*/
async function start(pt) {
fs.readdirSync(pt).map(file => `${pt}/${file}`)
.forEach(file => {
const stat = fs.statSync(file)
// 是文件夹就递归
if (stat.isDirectory()) {
if (filter.indexOf(pt) != -1) return
return start(file)
}
// 是文件并且后缀名符合就执行统计
if (types.indexOf(path.extname(file)) != -1) count(file)
})
}
;(async () => {
await start(rootPath)
result.push(total)
console.table(result)
})()

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 651 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 600 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 510 KiB

View File

@ -1,42 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UI Lib</title>
<script type="module" src="/main.js"></script>
<script src="/dist/ui.min.js"></script>
<script src="/dist/utility.min.js"></script>
</head>
<body>
<div id="directory">
<ul>
<li class="title">lib-ui</li>
<li>
<ol>
<li data-page="lib/ui/icon.html">icon</li>
<li data-page="lib/ui/checkbox.html">checkbox</li>
<li data-page="lib/ui/tooltip.html">tooltip</li>
<li data-page="lib/ui/dropdown.html">dropdown</li>
<li data-page="lib/ui/grid/grid.html">grid</li>
<li data-page="lib/ui/popup.html">popup</li>
</ol>
</li>
<li class="title">lib-utility</li>
<li>
<ol>
<li data-page="lib/utility/cookie.html">cookie</li>
<li data-page="lib/utility/lgres.html">lgres</li>
<li data-page="lib/utility/request.html">request</li>
<li data-page="lib/utility/strings.html">strings</li>
</ol>
</li>
</ul>
</div>
<div id="container"></div>
</body>
</html>

22
jsdoc-date.json Normal file
View File

@ -0,0 +1,22 @@
{
"source": {
"include": ["lib/ui/date.js"],
"includePattern": "\\.js$",
"exclude": []
},
"plugins": ["plugins/markdown"],
"opts": {
"encoding": "utf8",
"destination": "./jsdoc/date",
"recurse": true,
"verbose": false,
"template": "node_modules/docdash",
"theme_opts": {
"default_theme": "light"
}
},
"markdown": {
"hardwrap": false,
"idInHeadings": true
}
}

23
jsdoc.json Normal file
View File

@ -0,0 +1,23 @@
{
"source": {
"include": ["lib/ui/grid", "README.md"],
"includePattern": "\\.js$",
"exclude": []
},
"plugins": ["plugins/markdown"],
"opts": {
"encoding": "utf8",
"readme": "./README.md",
"destination": "./jsdoc/grid",
"recurse": true,
"verbose": false,
"template": "node_modules/docdash",
"theme_opts": {
"default_theme": "light"
}
},
"markdown": {
"hardwrap": false,
"idInHeadings": true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
jsdoc/grid/assets/grid.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -1,8 +1,12 @@
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
InternalComment,
CustomerRecordComment,
createHideMessageTitleButton
}

View File

@ -0,0 +1,186 @@
import { createElement, setTooltip, createIcon, requestAnimationFrame } from "../../ui";
import { r as lang, nullOrEmpty, escapeHtml, escapeEmoji } from "../../utility";
import { createBox, appendMedia, createHideMessageTitleButton, createHideMessageCommentTail } from "./lib";
let r = lang;
export default class CustomerRecordComment {
_var = {};
constructor(opt) {
this._var.option = opt ?? {};
const getText = opt?.getText;
if (typeof getText === 'function') {
r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
}
}
get text() { return this._var.enter?.value }
set text(s) {
const element = this._var.enter;
if (element != null) {
element.value = s
s = String(nullOrEmpty(s) ? 0 : val.length) + '/' + String(this._var.option.maxLength);
this._var.container.querySelector('.message-bar .prompt-count').innerText = s;
}
}
/**
* @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
*/
set loading(flag) {
if (this._var.container == null) {
return;
}
this._var.enter.disabled = flag;
this._var.container.querySelector('.button-send-message').disabled = flag;
}
/**
* @param {boolean} flag
*/
set readonly(flag) {
this._var.option.readonly = flag;
if (this._var.container == null) {
return;
}
this._var.enter.disabled = flag === true;
this._var.container.querySelector('.button-send-message').style.display = flag === true ? 'none' : '';
this._var.container.querySelector('.message-bar .prompt-count').style.display = flag === true ? 'none' : '';
}
create() {
const readonly = this._var.option.readonly;
const container = createBox(
createElement('div', null,
createElement('div', div => {
div.className = 'title-module';
div.innerText = r('FLTL_00584', 'Comments');
},
createHideMessageTitleButton(this, 'showCommentHidden')
)
),
[
createElement('button', button => {
button.className = 'roundbtn button-close';
button.style.backgroundColor = 'transparent';
if (this._var.option.hasClose !== true) {
button.style.display = 'none';
return;
}
button.appendChild(createIcon('fa-solid', 'times', {
fill: '#000'
}));
button.addEventListener('click', () => {
if (typeof this._var.option.onClose === 'function') {
this._var.option.onClose();
}
})
})
]
);
// enter box
const enter = createElement('textarea', 'ui-text');
enter.placeholder = r('FLTL_01154', 'Enter Comment Here');
enter.maxLength = this._var.option.maxLength ??= 3000;
enter.addEventListener('input', () => {
const val = this.text;
const s = String(nullOrEmpty(val) ? 0 : val.length) + '/' + String(this._var.option.maxLength);
this._var.container.querySelector('.message-bar .prompt-count').innerText = s;
});
if (readonly === true) {
enter.disabled = true;
}
this._var.enter = enter;
container.appendChild(
createElement('div', 'message-bar',
enter,
createElement('div', div => div.style.textAlign = 'right',
createElement('div', 'prompt-count'),
createElement('button', button => {
button.className = 'roundbtn button-send-message';
button.style.backgroundColor = 'rgb(19, 150, 204)';
if (readonly === true) {
button.style.display = 'none';
}
button.appendChild(createIcon('fa-solid', 'paper-plane'));
// 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);
}
})
})
)
)
);
const message = createElement('div', 'list-bar');
this._var.message = message;
container.appendChild(message);
return this._var.container = container;
}
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('FLTL_02716', 'Sent To :') + `\n${sendto}`;
// }
div.appendChild(createElement('div', div => {
div.className = 'item-poster';
div.innerText = comment.UserName;
}));
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.Comment)), mmsParts));
if (comment.MMSParts?.length > 0) {
mmsParts.style.display = '';
for (let kv of comment.MMSParts) {
appendMedia(mmsParts, kv.Key, kv.Value);
}
}
div.append(
content,
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);
requestAnimationFrame(() => this._var.message.scrollTop = keep ? this._var.lastTop : this._var.message.scrollHeight);
}
}

View File

@ -1,18 +1,25 @@
import { Dropdown, createElement, createCheckbox, createPopup, showAlert } from "../../ui";
import { isEmail, nullOrEmpty, r } from "../../utility";
import { Grid, Dropdown, createElement, createCheckbox, Popup, showAlert, requestAnimationFrame } from "../../ui";
import { isEmail, nullOrEmpty, r as lang } from "../../utility";
class Contact {
#option;
#refs;
let r = lang;
export class Contact {
_var = {};
constructor(option = {}) {
this.#option = option;
this._var.option = option;
const getText = option?.getText;
if (typeof getText === 'function') {
r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
}
}
async show(parent = document.body) {
const tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)) + 3;
const c = this.#option.contact;
const c = this._var.option.contact;
const contactName = createElement('input', input => {
input.type = 'text';
input.className = 'ui-input';
@ -22,9 +29,9 @@ class Contact {
});
const preferences = new Dropdown({ tabIndex: tabIndex + 2 });
preferences.source = [
{ value: '0', text: r('text', 'Text') },
{ value: '1', text: r('email', 'Email') },
{ value: '2', text: r('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';
@ -48,9 +55,9 @@ class Contact {
txt.style.height = '100px';
});
const buttons = [];
if (this.#option.company) {
if (this._var.option.company) {
buttons.push({
text: c == null ? r('addContactRecord', 'Add Contact Record') : r('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();
@ -58,66 +65,67 @@ class Contact {
return false;
}
item.SaveToCustomer = 1;
if (typeof this.#option.onSave === 'function') {
return this.#option.onSave.call(this, item, 'customerrecord');
if (typeof this._var.option.onSave === 'function') {
return this._var.option.onSave.call(this, item, 'customerrecord');
}
}
});
}
buttons.push(
{
text: r('workOrderOnly', 'Work Order Only'),
text: r('FLTL_03348', 'Work Order Only'),
// tabIndex: tabIndex + 8,
trigger: () => {
const item = this.prepare();
if (item == null) {
return false;
}
item.Id = -1;
//item.Id = -1;
item.SaveToCustomer = 0;
if (typeof this.#option.onSave === 'function') {
return this.#option.onSave.call(this, item, 'workorder');
if (typeof this._var.option.onSave === 'function') {
return this._var.option.onSave.call(this, item, 'workorder');
}
}
},
{
text: r('cancel', 'Cancel'),
text: r('FLTL_00499', 'Cancel'),
// tabIndex: tabIndex + 9
}
);
const popup = createPopup(
c == null ? r('addContact', 'Add Contact') : r('editContact', 'Edit Contact'),
createElement('div', wrapper => {
const popup = new Popup({
onMasking: this._var.option.onMasking,
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('contactNameColon', 'Contact Name:')),
createElement('span', 'setting-label setting-required', r('FLTL_00640', 'Contact Name:')),
contactName
),
createElement('div', 'setting-item',
createElement('span', 'setting-label', r('contactPreferencesColon', 'Contact Preferences:')),
createElement('span', 'setting-label', r('FLTL_00643', 'Contact Preferences:')),
preferences.create()
),
createElement('div', 'setting-item',
createElement('span', 'setting-label', r('contactEmailColon', 'Email Address:')),
createElement('span', 'setting-label', r('FLTL_01092', 'Email Address:')),
contactEmail
),
createElement('div', 'setting-item',
createElement('span', 'setting-label', r('contactMobileColon', 'Mobile:')),
createElement('span', 'setting-label', r('FLTL_01932', 'Mobile:')),
contactMobile
),
createElement('div', 'setting-item',
createElement('span', 'setting-label', r('contactOptColon', 'Opt Out:')),
createElement('span', 'setting-label', r('FLTL_02089', 'Opt Out:')),
checkOpt
),
createElement('div', 'setting-item',
createElement('span', 'setting-label', r('contactNotesColon', 'Notes:')),
createElement('span', 'setting-label', r('FLTL_02017', 'Notes:')),
contactNotes
)
),
...buttons
)
buttons
})
if (c != null) {
contactName.value = c.Name;
preferences.select(String(c.ContactPreference));
@ -128,7 +136,7 @@ class Contact {
} else {
preferences.select('0');
}
this.#refs = {
this._var.refs = {
contactName,
preferences,
contactEmail,
@ -137,37 +145,40 @@ class Contact {
contactNotes
};
const result = await popup.show(parent);
setTimeout(() => contactName.focus());
requestAnimationFrame(() => contactName.focus());
return result;
}
prepare() {
const name = this.#refs.contactName.value;
const pref = this.#refs.preferences.selected.value;
const email = this.#refs.contactEmail.value;
const phone = this.#refs.contactMobile.value;
const opt = this.#refs.checkOpt.querySelector('input').checked;
const notes = this.#refs.contactNotes.value;
const title = this.#option.contact == null ? r('addContact', 'Add Contact') : r('editContact', 'Edit Contact');
const name = this._var.refs.contactName.value;
const pref = this._var.refs.preferences.selected.value;
const email = this._var.refs.contactEmail.value;
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('FLTL_00099', 'Add Contact') : r('FLTL_01041', 'Edit Contact');
if (nullOrEmpty(name)) {
showAlert(title, r('contactNameRequired', 'Contact Name cannot be empty.'), 'warn')
.then(() => this.#refs.contactName.focus());
showAlert(title, r('FLTL_00639', 'Contact Name cannot be empty.'), 'warn')
.then(() => this._var.refs.contactName.focus());
return null;
}
if (nullOrEmpty(email) && nullOrEmpty(phone)) {
showAlert(title, r('contactEmailPhoneRequired', 'Email and Mobile Phone cannot both be empty.'), 'warn')
.then(() => nullOrEmpty(email) ?
this.#refs.contactEmail.focus() :
this.#refs.contactMobile.focus());
if ((pref == 0 || pref == 2) && nullOrEmpty(phone)) {
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('FLTL_01094', 'Email cannot be empty.'), 'warn')
.then(() => this._var.refs.contactEmail.focus());
return null;
}
if (!nullOrEmpty(email) && !isEmail(email)) {
showAlert(title, r('contactEmailInvalid', 'The email address is invalid.'), 'warn')
.then(() => this.#refs.contactEmail.focus());
showAlert(title, r('FLTL_02952', 'The email address is invalid.'), 'warn')
.then(() => this._var.refs.contactEmail.focus());
return null;
}
let contact = this.#option.contact;
let contact = this._var.option.contact;
if (contact == null) {
contact = {};
} else if (contact.OptOut !== opt) {
@ -175,6 +186,8 @@ class Contact {
contact.selected = !opt;
}
}
contact.OldName = contact.Name;
contact.OldMobilePhone = contact.MobilePhone;
contact.Name = name;
contact.ContactPreference = pref;
contact.Email = email;
@ -185,4 +198,70 @@ class Contact {
}
}
export default Contact;
export class CustomerRecordContact {
_var = {};
constructor(option = {}) {
this._var.option = option;
const getText = option?.getText;
if (typeof getText === 'function') {
r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
}
}
async show(title, parent = document.body) {
// const tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)) + 3;
const gridContainer = createElement('div', 'selcontact-grid');
const popup = new Popup({
onMasking: this._var.option.onMasking,
title,
content: createElement('div', 'selcontact-wrapper',
gridContainer
),
buttons: [
{
text: r('FLTL_02057', 'OK'),
key: 'ok',
trigger: () => {
if (typeof this._var.option.onOk === 'function') {
return this._var.option.onOk.call(this, this._var.grid.source.filter(f => f.selected));
}
}
},
{ text: r('FLTL_00499', 'Cancel'), key: 'cancel' }
]
});
const result = await popup.show(parent);
// grid
const grid = new Grid(gridContainer);
grid.columns = [
{
key: 'selected',
type: Grid.ColumnTypes.Checkbox,
width: 40,
// enabled: item => !nullOrEmpty(item.ID)
},
{ 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) });
this._var.grid = grid;
return result;
}
set source(contacts) {
this._var.option.contacts = contacts;
const grid = this._var.grid;
if (grid != null) {
this._var.grid.source = contacts;
}
}
}

View File

@ -9,7 +9,9 @@ interface InitConfig {
readonly?: boolean;
}
export class CustomerCommunication {
export default class CustomerCommunication {
constructor (opt: InitConfig);
get autoUpdatesEnabled(): boolean;
set autoUpdatesEnabled(enabled: boolean);
get autoUpdates(): boolean;
@ -21,8 +23,4 @@ export class CustomerCommunication {
set statusLink(checked: boolean);
create(): HTMLElement;
}
declare var CustomerCommunication: {
new(opt: InitConfig): CustomerCommunication
}

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,30 @@
import { Grid, createElement, createPopup } from "../../ui";
import { nullOrEmpty, r, contains } from "../../utility";
import { Grid, createElement, Popup } from "../../ui";
import { nullOrEmpty, r as lang, contains } from "../../utility";
class Follower {
#option;
#grid;
let r = lang;
export default class Follower {
_var = {};
constructor(option = {}) {
this.#option = option;
this._var.option = option;
const getText = option?.getText;
if (typeof getText === 'function') {
r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
}
}
async show(parent = document.body) {
async show(title, parent = document.body) {
const tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)) + 3;
const gridContainer = createElement('div', 'follower-grid');
const popup = createPopup(
r('addFollowers', 'Add Followers'),
createElement('div', 'follower-wrapper',
createElement('div', div => div.innerText = r('whoWantReceiveCustomerNotification', 'Who do you want to receive customer notifications?')),
const popup = new Popup({
onMasking: this._var.option.onMasking,
title,
content: createElement('div', 'follower-wrapper',
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;
@ -24,52 +32,53 @@ class Follower {
search.addEventListener('input', () => {
const key = search.value;
if (nullOrEmpty(key)) {
this.#grid.source = this.#option.followers;
this._var.grid.source = this._var.option.followers.sort(function (a, b) { return ((b.Text || b.Email) ? 1 : 0) - ((a.Text || a.Email) ? 1 : 0) });
} else {
this.#grid.source = this.#option.followers.filter(f => f.Text || f.Email || contains(f.DisplayName, key, true));
this._var.grid.source = this._var.option.followers.filter(f => f.Text || f.Email || contains(f.DisplayName, key, true))
.sort(function (a, b) { return ((b.Text || b.Email) ? 1 : 0) - ((a.Text || a.Email) ? 1 : 0) });
}
});
}),
gridContainer
),
{
text: r('ok', 'OK'),
key: 'ok',
trigger: () => {
if (typeof this.#option.onOk === 'function') {
return this.#option.onOk.call(this, this.#grid.source.filter(f => f.Email || f.Text));
buttons: [
{
text: r('FLTL_02057', 'OK'),
key: 'ok',
trigger: () => {
if (typeof this._var.option.onOk === 'function') {
return this._var.option.onOk.call(this, this._var.grid.source.filter(f => f.Email || f.Text));
}
}
}
},
{ text: r('cancel', 'Cancel'), key: 'cancel' }
);
},
{ text: r('FLTL_00499', 'Cancel'), key: 'cancel' }
]
});
const result = await popup.show(parent);
result.querySelector('.follower-search').focus();
// grid
const grid = new Grid(gridContainer);
grid.columns = [
{ key: 'DisplayName', caption: r('contactName', 'Contact Name'), width: 240 },
{ key: 'ContactTypeName', caption: r('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('text', 'Text'),
caption: r('FLTL_02915', 'Text'),
type: Grid.ColumnTypes.Checkbox,
width: 60,
enabled: item => !nullOrEmpty(item.Mobile)
},
{
key: 'Email',
caption: r('email', 'Email'),
caption: r('FLTL_01089', 'Email'),
type: Grid.ColumnTypes.Checkbox,
width: 70,
// enabled: item => !nullOrEmpty(item.ID)
}
];
grid.init();
grid.source = this.#option.followers;
this.#grid = grid;
grid.source = this._var.option.followers.sort(function (a, b) { return ((b.Text || b.Email) ? 1 : 0) - ((a.Text || a.Email) ? 1 : 0) });
this._var.grid = grid;
return result;
}
}
export default Follower;
}

View File

@ -1,24 +1,109 @@
import { createElement, setTooltip, createIcon } from "../../ui";
import { r, nullOrEmpty, escapeHtml } from "../../utility";
import { createBox } from "./lib";
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, createHideMessageTitleButton, createHideMessageCommentTail } from "./lib";
class InternalComment {
#container;
#option;
#enter;
#message;
let r = lang;
export default class InternalComment {
_var = {};
// _var.container;
// _var.option;
// _var.enter;
// _var.fileControl;
// _var.file;
// _var.message;
constructor(opt) {
this.#option = opt ?? {};
this._var.option = opt ?? {};
const getText = opt?.getText;
if (typeof getText === 'function') {
r = getText;
} else if (typeof GetTextByKey === 'function') {
r = GetTextByKey;
}
}
get text() { return this.#enter?.value }
get text() { return this._var.enter?.value }
set text(s) {
const element = this.#enter;
const element = this._var.enter;
if (element != null) {
element.value = s
s = String(nullOrEmpty(s) ? 0 : val.length) + '/' + String(this.#option.maxLength);
this.#container.querySelector('.message-bar .prompt-count').innerText = s;
s = String(nullOrEmpty(s) ? 0 : val.length) + '/' + String(this._var.option.maxLength);
this._var.container.querySelector('.message-bar .prompt-count').innerText = s;
}
}
/**
* @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
*/
set contacts(contacts) {
this._var.contacts = contacts;
if (this._var.contactsUpdated !== true && contacts?.length > 0) {
const comments = this._var.comments;
if (comments?.length > 0) {
updateCustomerName(this._var.comments, contacts);
this._var.contactsUpdated = true;
}
}
}
/**
* @param {boolean} flag
*/
set loading(flag) {
if (this._var.container == null) {
return;
}
this._var.enter.disabled = flag;
this._var.container.querySelector('.button-send-message').disabled = flag;
this._var.container.querySelector('.button-post-note').disabled = flag;
}
get file() { return this._var.file || this._var.fileControl?.files?.[0] }
set file(f) {
this._var.fileControl?.remove();
const label = this._var.container.querySelector('.file-selector>.selector-name');
if (f == null) {
this._var.fileControl = null;
this._var.file = null;
if (label != null) {
label.style.display = 'none';
label.innerText = '';
label.querySelector('.ui-tooltip-wrapper')?.remove();
}
} else {
if (f instanceof HTMLInputElement) {
this._var.fileControl = f;
this._var.file = f.files[0];
const link = this._var.container.querySelector('.file-selector>.selector-link');
if (link != null) {
link.appendChild(f);
}
} else {
this._var.fileControl = null;
this._var.file = f;
}
if (label != null) {
label.style.display = '';
label.innerText = f.name;
setTooltip(label, f.name);
}
}
}
@ -26,54 +111,156 @@ class InternalComment {
* @param {boolean} flag
*/
set readonly(flag) {
this.#option.readonly = flag;
if (this.#container == null) {
this._var.option.readonly = flag;
if (this._var.container == null) {
return;
}
this.#enter.disabled = flag === true;
this.#container.querySelector('.button-send-message').style.display = flag === true ? 'none' : '';
this.#container.querySelector('.button-post-note').style.display = flag === true ? 'none' : '';
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('internalComments', 'Internal Comments');
})
div.innerText = r('FLTL_01613', 'Internal Comments');
},
createHideMessageTitleButton(this, 'showMessageHidden')
)
), []
);
const readonly = this.#option.readonly;
const readonly = option.readonly;
// enter box
const enter = createElement('textarea', 'ui-text');
enter.placeholder = r('typeComment', 'Enter Comment Here');
enter.maxLength = this.#option.maxLength ??= 3000;
enter.placeholder = r('FLTL_01154', 'Enter Comment Here');
enter.maxLength = option.maxLength ??= 3000;
enter.addEventListener('input', () => {
const val = this.#enter.value;
const s = String(nullOrEmpty(val) ? 0 : val.length) + '/' + String(this.#option.maxLength);
this.#container.querySelector('.message-bar .prompt-count').innerText = s;
const val = this.text;
const s = String(nullOrEmpty(val) ? 0 : val.length) + '/' + String(option.maxLength);
this._var.container.querySelector('.message-bar .prompt-count').innerText = s;
});
if (readonly === true) {
enter.disabled = true;
}
this.#enter = enter;
enter.addEventListener('paste', e => {
const file = e.clipboardData.files[0];
if (file != null) {
e.preventDefault();
this.file = insertFile(container, file, r);
}
});
this._var.enter = enter;
container.appendChild(
createElement('div', 'message-bar',
createElement('div', div => {
div.className = 'message-bar';
div.addEventListener('dragover', e => {
if (option.readonly !== true) {
const item = e.dataTransfer.items[0];
if (item?.kind === 'file') {
e.preventDefault();
if (item.type.length > 0 && fileSupported.indexOf(item.type) < 0) {
e.dataTransfer.dropEffect = 'none';
} else {
e.dataTransfer.dropEffect = 'link';
}
}
}
});
div.addEventListener('drop', e => {
if (option.readonly !== true) {
const file = e.dataTransfer.files[0];
if (file != null) {
e.preventDefault();
this.file = insertFile(container, file, r);
}
}
});
},
enter,
createElement('div', div => div.style.textAlign = 'right',
createElement('div', 'customer-left',
createElement('div', 'file-selector',
createElement('div', div => {
div.className = 'selector-link';
// div.style.display = 'none';
div.addEventListener('click', () => {
this._var.fileControl?.remove();
const file = createElement('input', input => {
input.type = 'file';
input.accept = fileSupported.join(',');
input.addEventListener('change', () => {
const file = insertFile(container, input.files?.[0], r);
if (file != null) {
this.file = file;
}
});
});
div.appendChild(this._var.fileControl = file);
file.dispatchEvent(new MouseEvent('click'));
});
},
createIcon('fa-regular', 'link')
),
createElement('span', span => {
span.className = 'selector-name';
span.style.display = 'none';
}),
createElement('layer', layer => {
layer.appendChild(createIcon('fa-regular', 'times'));
layer.addEventListener('click', () => this.file = null);
})
)
),
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)';
if (readonly === true || this.#option.noMessage === true) {
if (readonly === true || option.noMessage === true) {
button.style.display = 'none';
}
button.appendChild(createIcon('fa-solid', 'paper-plane'));
setTooltip(button, r('sendMessage', 'Send Message'));
setTooltip(button, r('FLTL_02692', 'Send Message'));
button.addEventListener('click', () => {
if (typeof this.#option.onAddMessage === 'function') {
this.#option.onAddMessage(this.#enter.value);
const val = this.text;
if (nullOrEmpty(val?.trim())) {
return;
}
if (typeof option.onAddMessage === 'function') {
this.loading = true;
option.onAddMessage(this.text);
}
})
}),
@ -85,10 +272,15 @@ class InternalComment {
button.style.display = 'none';
}
button.appendChild(createIcon('fa-solid', 'comment-alt-lines'));
setTooltip(button, r('postNote', 'Post Note'));
setTooltip(button, r('FLTL_02301', 'Post Note'));
button.addEventListener('click', () => {
if (typeof this.#option.onAddComment === 'function') {
this.#option.onAddComment(this.#enter.value);
const val = this.text;
if (nullOrEmpty(val?.trim())) {
return;
}
if (typeof option.onAddComment === 'function') {
this.loading = true;
option.onAddComment(this.text, this.file);
}
})
})
@ -97,49 +289,92 @@ class InternalComment {
);
const message = createElement('div', 'list-bar');
this.#message = message;
this._var.message = message;
container.appendChild(message);
return this.#container = container;
return this._var.container = container;
}
load(data) {
load(data, func, hisFunc, keep) {
const children = [];
if (data?.length > 0) {
this._var.comments = data;
if (this._var.contactsUpdated !== true) {
const contacts = this._var.contacts;
if (contacts?.length > 0) {
updateCustomerName(data, contacts);
this._var.contactsUpdated = true;
}
}
const lastVisible = this._var.option.showMessageHidden;
for (let comment of data) {
const div = createElement('div', 'item-div');
// if (sendto !== '') {
// sendto = r('sendToColon', 'Send To :') + `\n${sendto}`;
// }
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';
div.innerText = comment.UserName;
div.innerText = comment.Sender;
if (sendto?.length > 0) {
setTooltip(div, sendto);
}
}));
const content = createElement('div', 'item-content');
content.appendChild(createElement('span', span => span.innerHTML = escapeHtml(comment.Comment)));
if (comment.FollowUp?.length > 0) {
div.classList.add('item-sent');
const sendto = r('sendToColon', 'Send To :') + '\r\n' + comment.FollowUp.split(';').join('\r\n');
content.appendChild(createElement('div', div => {
const mmsParts = createElement('div', div => div.style.display = 'none');
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) {
appendMedia(mmsParts, kv.Key, kv.Value);
}
}
// if (comment.FollowUp?.length > 0) {
// div.classList.add('item-sent');
// 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('FLTL_02711', 'Sent');
// setTooltip(div, sendto);
// }));
// }
const [status, text, color, tips] = getMessageStatus(comment, r, this._var);
if (status !== -100) {
if (color != null) {
content.style.backgroundColor = color;
}
const divstatus = createElement('div', div => {
div.className = 'item-status';
div.innerText = r('sent', 'Sent');
setTooltip(div, sendto);
}));
div.innerText = text;
if (tips != null) {
setTooltip(div, tips);
}
});
content.appendChild(divstatus);
}
div.append(
content,
createElement('div', div => {
div.className = 'item-time';
div.innerText = comment.SubmitDateStr;
})
createHideMessageCommentTail(
this, 'showMessageHidden',
comment, 'TimeStr',
func, hisFunc)
);
children.push(div);
}
children[0].style.marginTop = '0';
}
this.#message.replaceChildren(...children);
this.#message.scrollTop = this.#message.scrollHeight
// setTimeout(() => this.#message.scrollTop = this.#message.scrollHeight, 0);
if (this._var.message.children.length > 0) {
this._var.lastTop = this._var.message.scrollTop;
}
this._var.message.replaceChildren(...children);
requestAnimationFrame(() => this._var.message.scrollTop = keep ? this._var.lastTop : this._var.message.scrollHeight);
}
}
export default InternalComment;
}

View File

@ -1 +1,3 @@
export function createBox(title: HTMLElement, functions: HTMLElement[]): HTMLElement
export function createBox(title: HTMLElement, functions: HTMLElement[]): HTMLElement
export function appendMedia(container: HTMLElement, mimeType: string, url: string): HTMLElement

View File

@ -1,6 +1,7 @@
import { createElement } from "../../ui";
import { createElement, setTooltip, showAlert, createPicture, createAudio, createVideo, createFile, createIcon, Popup, Grid, Dropdown } from "../../ui";
import { global, isEmail } from "../../utility";
function createBox(title, functions) {
export function createBox(title, functions) {
const container = createElement('div', 'comm');
const header = createElement('div', 'title-bar',
title,
@ -8,8 +9,510 @@ function createBox(title, functions) {
);
container.appendChild(header);
return container;
};
export function appendMedia(container, mimeType, url) {
switch (mimeType) {
case 'application/pdf':
case '.pdf':
container.appendChild(createFile(url, 'file-pdf'));
break;
case 'application/msword':
case 'application/vnd.ms-word':
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
case '.doc':
case '.docx':
container.appendChild(createFile(url, 'file-word'));
break;
case 'application/vnd.ms-excel':
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
case '.xls':
case '.xlsx':
container.appendChild(createFile(url, 'file-excel'));
break;
case 'application/vnd.ms-powerpoint':
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
case '.ppt':
case '.pptx':
container.appendChild(createFile(url, 'file-powerpoint'));
break;
case 'application/smil':
case '.smil':
// TODO: ignore smil files
// container.appendChild(createFile(url, 'smile'));
break;
case 'audio/aac':
case 'audio/amr':
case 'audio/mp3':
case 'audio/mpeg':
case 'audio/x-mpeg':
case 'audio/ogg':
case 'audio/opus':
case 'audio/wav':
case 'audio/webm':
case '.aac':
case '.amr':
case '.mp3':
case '.ogg':
case '.opus':
case '.wav':
container.appendChild(createAudio(mimeType, url));
break;
case 'text/plain':
case 'text/x-vcard':
case '.txt':
case '.vcard':
container.appendChild(createFile(url, 'id-card'));
break;
case 'video/3gpp':
case 'video/mp2t':
case 'video/mp4':
case 'video/mpeg':
case 'video/x-mpeg':
case 'video/quicktime':
case 'video/webm':
case '.3gp':
case '.3gpp':
case '.mp4':
case '.mpg':
case '.mov':
case '.webm':
container.appendChild(createVideo(url));
break;
case '.jpg':
case '.jpeg':
case '.jfif':
case '.png':
case '.gif':
case '.bmp':
container.appendChild(createPicture(url));
break;
default:
if (/^image\//.test(mimeType)) {
container.appendChild(createPicture(url));
} else if (/^audio\//.test(mimeType)) {
container.appendChild(createFile(url, 'music'));
} else if (/^video\//.test(mimeType)) {
container.appendChild(createFile(url, 'video'));
} else {
container.appendChild(createFile(url));
}
break;
}
return container;
};
const MaxAttachmentSize = {
limit: 1_258_291,
text: '1.2MB'
};
export const fileSupported = [
'.amr',
'.ogv',
'application/msword',
'application/vnd.ms-word',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/pdf',
'audio/aac',
'audio/amr',
'audio/mp3',
'audio/mpeg',
'audio/x-mpeg',
'audio/ogg',
'audio/opus',
'audio/wav',
'audio/webm',
'image/bmp',
'image/gif',
'image/jpeg',
'image/jfif',
'image/png',
'image/tiff',
'image/webp',
'text/plain',
'text/vcard',
'text/x-vcard',
'video/3gpp',
'video/mp2t',
'video/mp4',
'video/mpeg',
'video/x-mpeg',
'video/quicktime',
'video/webm',
];
export function insertFile(container, file, r) {
const label = container.querySelector('.file-selector>.selector-name');
if (label != null && file != null) {
let type = file.type;
if (type == null || type.length === 0) {
type = file.name;
type = type.substring(type.lastIndexOf('.'));
}
if (fileSupported.indexOf(type) < 0) {
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('FLTL_01165', 'Error'), r('FLTL_00407', `Attachment size exceeds the maximum allowed to be sent (${MaxAttachmentSize.text})`), 'warn');
return;
}
const fn = file.name;
label.style.display = '';
label.innerText = fn;
if (isImage) {
const img = new Image();
const reader = new FileReader();
reader.onload = e => {
img.src = e.target.result;
setTooltip(label, img);
};
reader.onerror = () => setTooltip(label, fn);
reader.readAsDataURL(file);
// img.src = URL.createObjectURL(file);
// setTooltip(label, img);
} else {
setTooltip(label, fn);
}
return file;
}
};
function getStatusText(status, dict) {
switch (status) {
case 0:
case 1:
case 5:
case 6:
return dict[status];
case 9:
case 10:
case 412:
return dict[9];
default:
return dict[9999];
}
}
export {
createBox
export function getMessageStatus(comm, r, _var) {
const messageStatus = {
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];
const failedStatus = [9, 10, 412];
let status = -100; // 没有状态,页面上不显示
const ls = [];
const msgs = [];
if (!comm.StatusIncorrect && comm.Participator?.length > 0) {
// if (comm.Id === 433339) {
// comm.Participator[4].Status = 6;
// }
for (let p of comm.Participator) {
if (!isEmail(p.CustomerNumber)) {
if (ls.indexOf(p.Status) < 0) {
ls.push(p.Status);
}
p.statusText = getStatusText(p.Status, messageStatus);
msgs.push(p);
}
}
}
if (ls.length === 1) {
status = ls[0];
} else if (ls.length > 1) {
// status = -10; // 多种状态
status = ls
.filter(s => okStatus.indexOf(s) < 0) // ok status
.sort((a, b) => b - a)[0] ?? 1;
}
const statusText = messageStatus[failedStatus.includes(status) ? 9 : status] ?? messageStatus[9999];
const statusColor = okStatus.includes(status) ? null : '#ffc107';
const statusUpdatable = _var.option.statusUpdatable;
let statusTips;
if (statusUpdatable !== false || ls.length > 1) {
statusTips = createElement('div', tip => {
for (let i = 0; i < msgs.length; ++i) {
tip.appendChild(createElement('div', t => {
const p = msgs[i];
if (statusUpdatable !== false && p.StatusChanged) {
t.append(
createElement('span', s => s.innerText = `${p.CustomerNumber}: `),
createElement('span', s => {
s.style.color = '#2140fb';
s.style.cursor = 'pointer';
s.innerText = p.statusText;
s.addEventListener('click', () => {
if (typeof _var.option.onMessageStatusClicked === 'function') {
_var.option.onMessageStatusClicked(p);
}
});
})
)
} else {
t.innerText = `${p.CustomerNumber}: ${p.statusText}`;
}
}));
}
if (statusUpdatable !== false) {
tip.appendChild(createElement('div', b => {
b.className = 'tip-function-button';
// setTooltip(b, r('FLTL_03174', 'Update Status'));
b.addEventListener('click', async () => {
for (let p of comm.Participator) {
switch (p.Status) {
case 0:
case 1:
case 5:
case 6:
p.statusChanged = String(p.Status);
break;
case 9:
case 10:
case 412:
p.statusChanged = '9';
break;
default:
p.statusChanged = '-1';
break;
}
}
const gridContainer = createElement('div', 'status-grid');
const popup = new Popup({
onMasking: _var.option.onMasking,
title: r('FLTL_03174', 'Update Status'),
content: createElement('div', wrapper => {
wrapper.className = 'update-status-wrapper';
wrapper.style.width = '500px';
},
gridContainer
),
buttons: [
{
text: r('FLTL_02057', 'OK'),
key: 'ok',
trigger: () => {
const changed = msgs.filter(m => {
switch (m.statusChanged) {
case '-1':
return knownStatus.includes(m.Status);
case '9':
return failedStatus.indexOf(m.Status) < 0;
default:
return String(m.Status) !== m.statusChanged;
}
}).map(m => {
let status = Number(m.statusChanged);
if (isNaN(status) || status < 0) {
status = 9999;
}
return {
Id: m.Id,
Status: status
};
});
if (typeof _var.option.onUpdateMessageStatus === 'function') {
_var.option.onUpdateMessageStatus(changed);
}
}
},
{
text: r('FLTL_00499', 'Cancel'),
key: 'cancel'
}
]
});
await popup.show();
const grid = new Grid(gridContainer);
// grid.headerVisible = false;
grid.allowHtml = true;
grid.columns = [
{
key: 'CustomerNumber',
caption: r('FLTL_02026', 'Number'),
width: 150
},
/*{
key: 'customerName',
caption: r('FLTL_00742', 'Customer Name'),
width: 120
},*/
{
key: 'statusText',
caption: r('FLTL_00725', 'Current Status'),
width: 155
},
{
key: 'statusChanged',
caption: r('FLTL_02511', 'Revised Status'),
width: 155,
type: Grid.ColumnTypes.Dropdown,
source: [
{ value: '-1', text: messageStatus[9999] },
{ value: '0', text: messageStatus[0] },
{ value: '1', text: messageStatus[1] },
{ value: '5', text: messageStatus[5] },
{ value: '6', text: messageStatus[6] },
{ value: '9', text: messageStatus[9] }
]
}
];
grid.init();
grid.source = msgs;
});
}, createIcon('fa-light', 'wave-sine')));
}
});
}
return [status, statusText, statusColor, statusTips];
};
export function getMessageSendTo(comm, contacts, followers, r) {
let sendto = '';
if (!comm.IsReply && comm.OriPhoneNumbers?.length > 0) {
for (let oriph of comm.OriPhoneNumbers) {
let cname;
const email = isEmail(oriph);
if (contacts?.length > 0) {
let c = email ?
contacts.find(c => c.Email === oriph) :
contacts.find(c => c.MobilePhone === oriph);
if (c != null) {
cname = `${email ? c.Email : c.MobilePhoneDisplayText} - ${c.Name}`;
} else if (followers?.length > 0) {
c = email ?
followers.find(f => f.Email === oriph) :
followers.find(f => f.MobilePhone === oriph);
if (c != null) {
cname = `${email ? c.Email : c.MobilePhoneDisplayText} - ${c.Name}`;
}
}
}
sendto += (cname ?? oriph) + '\n';
}
}
if (sendto !== '') {
sendto = r('FLTL_02716', 'Sent to :') + `\n${sendto}`;
}
return sendto;
}
export function updateCustomerName(messages, contacts) {
if (messages?.length > 0 && contacts?.length > 0) {
for (let m of messages) {
if (m.Participator?.length > 0) {
for (let p of m.Participator) {
const contact = contacts.filter(c => c.MobilePhoneDisplayText === p.CustomerNumber)[0];
p.customerName = contact?.Name;
}
}
}
}
}
export function createHideMessageTitleButton(This, optionName) {
const option = This._var.option;
return createElement('span', span => {
if (option.userIsAdmin) {
if (option[optionName]) {
span.className = 'msgadminsetting sbutton iconview';
} else {
span.className = 'msgadminsetting sbutton iconnotview';
}
span.style.padding = '0px 0px 0px 5px';
setTooltip(span, option?.getText('FLTL_01860', 'Manage Messages'));
span.addEventListener('click', function () {
const container = This._var.container;
if (!option[optionName]) {
this.classList.remove('iconnotview');
this.classList.add('iconview');
option[optionName] = true;
container.querySelectorAll('.msgsetting').forEach(x => x.style.display = '');
container.querySelectorAll('.msgHistory').forEach(h => h.style.display = h.getAttribute('ModifyCount') > 0 ? '' : 'none');
container.querySelectorAll('.hidden-content').forEach(c => c.style.display = '');
} else {
this.classList.remove('iconview');
this.classList.add('iconnotview');
option[optionName] = false;
container.querySelectorAll('.msgsetting').forEach(x => x.style.display = 'none');
container.querySelectorAll('.msgHistory').forEach(h => h.style.display = 'none');
container.querySelectorAll('.hidden-content').forEach(c => c.style.display = 'none');
}
});
}
});
}
export function createHideMessageCommentTail(This, optionName, comment, commentTime, func, hisFunc) {
const option = This._var.option;
const showTooltip = option?.getText('FLTL_03267', 'Visible');
const notShowTooltip = option?.getText('FLTL_02006', 'Not Visible');
return createElement('div', div => {
div.className = 'item-time';
div.style.display = 'flex';
div.style.alignItems = 'center';
},
createElement('span', span => {
span.className = 'msgsetting sbutton ' + (comment.Hidden ? 'iconnotview' : 'iconview');
span.style.padding = '0';
span.style.fontSize = '12px';
setTooltip(span, comment.Hidden ? notShowTooltip : showTooltip);
span.style.display = option[optionName] ? '' : 'none';
span.addEventListener('click', function () {
if (this.classList.contains('iconview')) {
func(comment.Id, true);
this.classList.remove('iconview');
this.classList.add('iconnotview');
setTooltip(this, notShowTooltip);
} else {
func(comment.Id, false);
this.classList.remove('iconnotview');
this.classList.add('iconview');
setTooltip(this, showTooltip);
}
if (isNaN(comment.ModifyCount)) {
comment.ModifyCount = 1;
} else {
comment.ModifyCount += 1;
}
const x = This._var.container.querySelector('.history-span-' + comment.Id);
if (x != null) {
x.setAttribute('ModifyCount', comment.ModifyCount);
x.style.display = (option[optionName] && comment.ModifyCount > 0) ? '' : 'none';
}
});
}),
createElement('span', span => {
span.className = 'msgHistory history-span-' + comment.Id;
span.setAttribute('ModifyCount', comment.ModifyCount ?? 0);
span.style.display = (option[optionName] && comment.ModifyCount > 0) ? '' : 'none';
setTooltip(span, option?.getText('FLTL_01508', 'Hidden History'));
const icon = createIcon('fa-light', 'wave-sine');
icon.style.height = '12px';
icon.style.width = '12px';
icon.style.margin = '0 5px 0 0';
icon.style.cursor = 'pointer';
icon.style.border = '1px solid';
icon.style.borderRadius = '6px';
icon.style.borderColor = '#000';
icon.style.display = 'block';
span.appendChild(icon);
span.addEventListener('click', () => hisFunc(comment.Id));
}),
createElement('span', span => {
span.innerText = comment[commentTime];
})
);
}

View File

@ -1,10 +1,23 @@
@import "../../ui/css/functions/func.scss";
.ui-popup-mask .wrapper-edit-method {
width: 100%;
.ui-popup-mask {
.wrapper-edit-method {
width: 100%;
.ui-check-wrapper {
padding: 0 28px;
.ui-check-wrapper {
padding: 0 28px;
}
}
.status-grid,
.contacts-record,
.contacts-wo {
position: relative;
>.ui-grid {
overflow-x: visible;
max-height: 200px;
}
}
}
@ -13,7 +26,7 @@
flex-direction: column;
width: 320px;
background-color: var(--dark-fore-color);
border: 1px solid var(--title-bg-color);
border: 1px solid var(--title-ctrlbg-color);
margin-left: 12px;
& {
@ -69,7 +82,7 @@
flex: 0 0 auto;
padding: 5px 0 5px 10px;
color: var(--title-color);
background-color: var(--title-bg-color);
background-color: var(--title-ctrlbg-color);
line-height: 24px;
display: flex;
align-items: center;
@ -77,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;
@ -132,7 +172,8 @@
flex: 0 0 auto;
padding: 4px 0;
display: flex;
border-bottom: 1px solid var(--title-bg-color);
border-bottom: 1px solid var(--title-ctrlbg-color);
position: relative;
>.bar-icon {
flex: 0 0 auto;
@ -148,6 +189,14 @@
flex: 1 1 auto;
width: calc(100% - 46px);
.bar-list-container {
min-height: 26px;
max-height: 120px;
overflow-x: hidden;
overflow-y: auto;
position: relative;
}
.contact-item {
display: flex;
align-items: center;
@ -173,6 +222,46 @@
}
}
>.bar-info {
display: none;
flex: 1 1 auto;
text-align: right;
margin-right: 50px;
}
>.bar-collapser {
position: absolute;
top: 3px;
right: 18px;
width: 26px;
height: 26px;
box-sizing: border-box;
padding: 10px;
cursor: pointer;
border-radius: 13px;
transition: background-color .12s ease;
&:hover {
background-color: var(--light-color);
}
>span {
width: 6px;
height: 6px;
position: absolute;
border-top: 1px solid var(--strong-color);
border-right: 1px solid var(--strong-color);
top: 8px;
transform: rotate(135deg);
}
&.collapsed>span {
top: 9px;
left: 8px;
transform: rotate(45deg);
}
}
.roundbtn {
float: right;
margin: 4px 10px 10px;
@ -183,19 +272,41 @@
}
}
&.collapsed {
.contact-bar {
border-bottom-color: transparent;
>.bar-icon,
>.bar-list {
display: none;
}
>.bar-info {
display: block;
}
}
.follower-bar {
display: none !important;
}
}
.message-bar {
flex: 0 0 auto;
border-bottom: 1px solid var(--title-bg-color);
border-bottom: 1px solid var(--title-ctrlbg-color);
display: flex;
flex-direction: column;
>textarea {
padding: 10px 10px 0;
border: none;
border: 1px solid var(--title-ctrlbg-color);
border-radius: 5px;
height: 70px;
resize: none;
font-size: var(--font-smaller-size);
font-family: var(--font-family);
margin: 3px 5px;
background: #f0f0f0;
@include outline();
}
@ -203,19 +314,76 @@
>div {
padding: 0 10px 10px;
>.customer-name {
>.customer-left {
float: left;
text-align: left;
>span {
font-size: var(--font-smaller-size);
>.customer-name {
>span {
font-size: var(--font-smaller-size);
}
>.ui-input {
margin-left: 4px;
width: 150px;
border-top: none;
border-left: none;
border-right: none;
}
}
>.ui-input {
margin-left: 4px;
width: 150px;
border-top: none;
border-left: none;
border-right: none;
>.file-selector {
display: inline-flex;
align-items: center;
height: 30px;
>.selector-link {
cursor: pointer;
display: flex;
>svg {
width: 16px;
height: 16px;
fill: var(--secondary-link-color);
}
>input {
display: none;
}
}
>.selector-name {
max-width: 130px;
padding: 0 20px 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+layer {
display: none;
margin-left: -20px;
cursor: pointer;
>svg {
width: 16px;
height: 16px;
fill: var(--red-color);
}
&:hover {
display: flex;
}
}
&:hover+layer {
display: flex;
}
>.ui-tooltip-wrapper img {
max-width: 120px;
max-height: 80px;
}
}
}
}
@ -231,6 +399,7 @@
flex: 1 1 auto;
overflow: auto;
margin-top: 8px;
min-height: 100px;
.item-div {
margin-top: 5px;
@ -268,6 +437,10 @@
max-width: 240px;
background-color: rgb(244, 244, 244);
audio[controls] {
width: 220px;
}
a>svg {
width: 13px;
height: 13px;
@ -290,6 +463,25 @@
margin-right: -12px;
font-size: .625rem;
float: right;
.ui-tooltip-content .tip-function-button {
text-align: right;
>svg {
width: 20px;
height: 20px;
cursor: pointer;
border: 1px solid;
border-radius: 10px;
padding: 2px;
box-sizing: border-box;
transition: opacity .12s;
&:hover {
opacity: .5;
}
}
}
}
}
@ -330,7 +522,7 @@
height: 100%;
min-height: 120px;
>.ui-grid-body .ui-grid-body-content>.ui-grid-row>td {
>.ui-grid-wrapper>.ui-grid-table>tbody>.ui-grid-row>td {
vertical-align: top;
.col-icon {
@ -356,10 +548,14 @@
.contact-name {
overflow: hidden;
text-overflow: ellipsis;
line-height: 30px;
}
.contact-note {
color: #999;
overflow: hidden;
text-overflow: ellipsis;
margin: 3px 0 3px;
}
}
}
@ -378,14 +574,24 @@
height: 380px;
}
}
.selcontact-wrapper {
display: flex;
flex-direction: column;
width: 780px;
>.selcontact-grid {
height: 200px;
}
}
}
}
@media (prefers-color-scheme: dark) {
/*@media (prefers-color-scheme: dark) {
.comm {
--dark-fore-color: #000;
--dark-fore-opacity-color: rgba(0, 0, 0, .4);
--strong-color: #ccc;
--light-color: #333;
}
}
}*/

12
lib/element.js Normal file
View File

@ -0,0 +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,
AddWorkOrder,
InspectionWizard,
Signature
}

734
lib/element/addworkorder.js Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

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

247
lib/element/schedule.js Normal file
View File

@ -0,0 +1,247 @@
import { createElement, createCheckbox, createRadiobox, Dropdown, validation, toDateValue, OptionBase } from "../ui";
export default class ScheduleItem extends OptionBase {
_var = {};
constructor(opt = {}) {
super(opt);
}
get checkOccurOnce() { return this._var.container.querySelector('.schedule-id-box-occur-once>input'); }
get checkOccurEvery() { return this._var.container.querySelector('.schedule-id-box-occur-every>input'); }
get inputOccurOnce() { return this._var.container.querySelector('.schedule-id-occur-once'); }
get inputOccurEvery() { return this._var.container.querySelector('.schedule-id-occur-every'); }
get inputOccurStarting() { return this._var.container.querySelector('.schedule-id-occur-starting') }
get inputOccurEnding() { return this._var.container.querySelector('.schedule-id-occur-ending') }
changeDailyFrequency(once) {
this.inputOccurOnce.disabled = !once;
this.inputOccurEvery.disabled = once;
this.inputOccurStarting.disabled = once;
this.inputOccurEnding.disabled = once;
}
getParameters() {
return {
Enabled: this._var.container.querySelector('.schedule-id-enabled>input').checked,
Schedule: {
Frequency: Number(this.dropFrequency.selected.value),
Daily: {
OcurrsOnce: this.checkOccurOnce.checked,
OcurrsOnceAt: this.inputOccurOnce.value,
OcurrsInterval: Number(this.inputOccurEvery.value),
StartingAt: this.inputOccurStarting.value,
EndingAt: this.inputOccurEnding.value
},
Monday: this._var.container.querySelector('.schedule-id-1>input').checked,
Tuesday: this._var.container.querySelector('.schedule-id-2>input').checked,
Wednesday: this._var.container.querySelector('.schedule-id-3>input').checked,
Thursday: this._var.container.querySelector('.schedule-id-4>input').checked,
Friday: this._var.container.querySelector('.schedule-id-5>input').checked,
Saturday: this._var.container.querySelector('.schedule-id-6>input').checked,
Sunday: this._var.container.querySelector('.schedule-id-7>input').checked,
DayOfMonth: Number(this._var.container.querySelector('.schedule-id-dayofmonth').value),
StartDate: this._var.container.querySelector('.schedule-id-duration-start').value,
EndDate: this._var.container.querySelector('.schedule-id-duration-end').value
}
};
}
getDateTime(s) {
if (typeof s === 'string') {
const d = new Date(s);
return isNaN(d.getTime()) ? new Date() : d;
}
return s;
}
getTimeString(s) {
const d = this.getDateTime(s);
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
}
getDateString(s) {
const d = this.getDateTime(s);
// return String(d.getMonth() + 1).padStart(2, '0') + '/' + String(d.getDate()).padStart(2, '0') + '/' + String(d.getFullYear());
return toDateValue(d, true);
}
setParameters(p) {
this._var.container.querySelector('.schedule-id-enabled>input').checked = p.Enabled;
const schedule = p.Schedule || {};
this.dropFrequency.select(String(schedule.Frequency));
let checker = schedule.Daily.OcurrsOnce ? this.checkOccurOnce : this.checkOccurEvery;
checker.checked = true;
checker.dispatchEvent(new Event('change'));
this.inputOccurOnce.value = this.getTimeString(schedule.Daily.OcurrsOnceAt);
this.inputOccurEvery.value = String(schedule.Daily.OcurrsInterval);
this.inputOccurStarting.value = this.getTimeString(schedule.Daily.StartingAt);
this.inputOccurEnding.value = this.getTimeString(schedule.Daily.EndingAt);
this._var.container.querySelector('.schedule-id-1>input').checked = schedule.Monday;
this._var.container.querySelector('.schedule-id-2>input').checked = schedule.Tuesday;
this._var.container.querySelector('.schedule-id-3>input').checked = schedule.Wednesday;
this._var.container.querySelector('.schedule-id-4>input').checked = schedule.Thursday;
this._var.container.querySelector('.schedule-id-5>input').checked = schedule.Friday;
this._var.container.querySelector('.schedule-id-6>input').checked = schedule.Saturday;
this._var.container.querySelector('.schedule-id-7>input').checked = schedule.Sunday;
this._var.container.querySelector('.schedule-id-dayofmonth').value = String(schedule.DayOfMonth);
this._var.container.querySelector('.schedule-id-duration-start').value = this.getDateString(schedule.StartDate);
this._var.container.querySelector('.schedule-id-duration-end').value = this.getDateString(schedule.EndDate);
}
create() {
const option = this._option;
const drop = new Dropdown({ selected: '0' });
this.dropFrequency = drop;
drop.source = [
{ value: '0', text: 'Daily' },
{ value: '1', text: 'Weekly' },
{ value: '2', text: 'Monthly' }
];
drop.onSelected = item => {
container.querySelector('.schedule-item-weekly').style.display = item.value === '1' ? '' : 'none';
const monthly = item.value === '2';
container.querySelector('.schedule-item-monthly').style.display = monthly ? '' : 'none';
if (!monthly) {
const dayofmonth = this._var.container.querySelector('.schedule-id-dayofmonth');
if (dayofmonth.classList.contains('validation-error')) {
dayofmonth.value = '1';
}
}
};
const container = createElement('div', 'schedule-item-container',
createElement('fieldset', 'schedule-item-frequency',
createElement('legend', legend => legend.innerText = 'Frequency'),
createElement('div', 'schedule-item-line',
createElement('span', span => span.innerText = 'Occurs'),
drop.create()
),
createElement('div', div => {
div.className = 'schedule-item-panel schedule-item-weekly';
div.style.display = 'none';
},
createElement('table', 'schedule-item-table',
createElement('tr', 'schedule-item-tr',
createElement('td', null, createCheckbox({ className: 'schedule-id-1', label: 'Monday' })),
createElement('td', null, createCheckbox({ className: 'schedule-id-3', label: 'Wednesday' })),
createElement('td', null, createCheckbox({ className: 'schedule-id-5', label: 'Friday' })),
createElement('td', null, createCheckbox({ className: 'schedule-id-6', label: 'Saturday' }))
),
createElement('tr', 'schedule-item-tr',
createElement('td', null, createCheckbox({ className: 'schedule-id-2', label: 'Tuesday' })),
createElement('td', null, createCheckbox({ className: 'schedule-id-4', label: 'Thursday' })),
createElement('td'),
createElement('td', null, createCheckbox({ className: 'schedule-id-7', label: 'Sunday' }))
)
)
),
createElement('div', div => {
div.className = 'schedule-item-panel schedule-item-monthly';
div.style.display = 'none';
},
createElement('div', 'schedule-item-line',
createElement('span', span => span.innerText = 'On day'),
validation(
createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-dayofmonth', i.maxLength = 2 }),
/^([0]?[1-9]|[12][0-9]|[3][01])$/
),
createElement('span', span => span.innerText = 'of the month')
)
)
),
createElement('fieldset', 'schedule-item-daily-frequency',
createElement('legend', legend => legend.innerText = 'Daily frequency'),
createElement('div', 'schedule-item-line',
createRadiobox({
name: 'schedule-daily-occurs',
checked: true,
className: 'schedule-id-box-occur-once',
label: 'Occurs once at',
onchange: e => this.changeDailyFrequency(e.target.checked)
}),
validation(
createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-occur-once', i.maxLength = 5 }),
/^([1-9]|[01][0-9]|[2][0-3]):([1-9]|[0-5][0-9])$/
)
),
createElement('div', 'schedule-item-line schedule-item-line-occur-every',
createRadiobox({
name: 'schedule-daily-occurs',
className: 'schedule-id-box-occur-every',
label: 'Occurs every',
onchange: e => this.changeDailyFrequency(!e.target.checked)
}),
validation(
createElement('input', i => { i.type = 'text', i.className = 'ui-input schedule-id-occur-every', i.maxLength = 5 }),
/^([0][1-9]+|[1-9][0-9]*)$/
),
createElement('span', span => span.innerText = 'minute(s)'),
createElement('div', 'schedule-item-placeholder'),
createElement('div', 'schedule-item-block',
createElement('div', 'scheldule-item-line',
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 }),
/^([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 }),
/^([1-9]|[01][0-9]|[2][0-3]):([1-9]|[0-5][0-9])$/
)
)
)
)
),
createElement('fieldset', 'schedule-item-duration',
createElement('legend', legend => legend.innerText = 'Duration'),
createElement('div', 'schedule-item-line schedule-item-line-duration',
createElement('span', span => span.innerText = 'Start date'),
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'),
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' })
)
)
);
this._var.container = container;
if (option.parameter == null) {
option.parameter = {
Enabled: true,
Schedule: {
Frequency: 0,
Daily: {
OcurrsOnce: true,
OcurrsOnceAt: '00:00',
OcurrsInterval: 60,
StartingAt: '00:00',
EndingAt: '23:00'
},
Monday: true,
Tuesday: false,
Wednesday: false,
Thursday: false,
Friday: false,
Saturday: false,
Sunday: false,
DayOfMonth: 1,
StartDate: '',
EndDate: ''
}
};
}
this.setParameters(option.parameter);
return container;
}
}

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

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

322
lib/element/style.scss Normal file
View File

@ -0,0 +1,322 @@
@import "../ui/css/functions/func.scss";
.schedule-item-container {
fieldset {
margin-top: 10px;
border-width: 1px;
border-radius: 4px;
border-color: var(--border-color);
legend,
span {
font-weight: 400;
font-size: var(--font-size);
padding-left: 8px;
padding-right: 6px;
color: var(--color);
}
.ui-input {
line-height: 20px;
height: 20px;
text-indent: 0;
&.validation-error,
&:invalid {
color: #0000004d;
}
}
.schedule-item-monthly {
margin-top: 5px;
.ui-input {
width: 40px;
}
}
&.schedule-item-daily-frequency .ui-input {
vertical-align: top;
margin-top: 5px;
}
.schedule-item-table {
width: 100%;
}
.schedule-item-line-occur-every {
display: flex;
align-items: flex-start;
>.schedule-item-block>.scheldule-item-line {
display: flex;
align-items: center;
margin-top: 5px;
>span {
flex: 1 1 auto;
}
>.ui-input {
margin-top: 0;
}
}
>span {
line-height: 36px;
}
.ui-input {
width: 70px;
}
}
.schedule-item-line-duration {
display: flex;
align-items: center;
height: 36px;
}
.schedule-item-line {
>.schedule-item-placeholder {
flex: 1 1 auto;
}
}
}
.schedule-item-frequency {
margin-top: 0;
>.schedule-item-line {
line-height: 24px;
}
}
.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;
}
}

View File

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

8
lib/functions.d.ts vendored
View File

@ -1 +1,7 @@
export function createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, init?: string | ((element: HTMLElementTagNameMap[K]) => void), ...children?: (Node | string)[]): HTMLElementTagNameMap[K];
/**
* 创建一个 html 元素
* @param tagName 标签名
* @param init 样式类名,或者为有一个 html 元素作为参数的函数
* @param children 子元素
*/
export function createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, init?: string | ((element: HTMLElementTagNameMap[K]) => void), ...children: (Node | string)[]): HTMLElementTagNameMap[K];

View File

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

38
lib/ui/checkbox.d.ts vendored
View File

@ -1,18 +1,54 @@
/**
* 复选框选项
*/
interface CheckboxOptions {
/** 样式类名 */
className?: string;
/** 是否可用 */
enabled?: boolean;
/** html 名称 */
name?: string;
/** 焦点索引 */
tabIndex?: Number;
/** 是否为 switch 样式 */
switch?: boolean;
/** 样式分类,可以是 ['`fa-light`', '`fa-regular`', '`fa-solid`'] 其中之一 */
type?: string;
/** 标签 */
label?: string | HTMLElement;
/** 标签提示文本 */
title?: string;
/** 是否已选中 */
checked?: boolean;
/** 图片高度 */
imageHeight?: Number;
/** 选中时显示的元素 */
checkedNode?: HTMLElement;
/** 未选中时显示的元素 */
uncheckedNode?: HTMLElement;
customerAttributes?: { [key: string]: string };
/** 自定义 html 属性 */
customAttributes?: { [key: string]: string };
/**
* 复选框选择状态改变时触发
* @param this 当前复选框对应的 HtmlInput 元素
* @param ev 选择事件对象
*/
onchange?: (this: HTMLInputElement, ev: Event) => any;
}
/**
* 创建一个单选框
* @param opts 单选框参数
*/
export function createRadiobox(opts?: CheckboxOptions): HTMLElement
/**
* 创建一个复选框
* @param opts 复选框参数
*/
export function createCheckbox(opts?: CheckboxOptions): HTMLElement
/**
* 解析容器元素内符合条件的子元素为复选框元素
* @param container 容器元素
* @param legacy 是否使用传统模式
*/
export function resolveCheckbox(container?: HTMLElement, legacy?: boolean): HTMLElement

View File

@ -1,138 +0,0 @@
<div>
<h1>checkbox</h1>
<hr />
<p>
创建一个统一样式的复选框元素,或者解析转换页面上特定类型的 label
标签为复选框元素。
</p>
<h2>createCheckbox</h2>
<code>function createCheckbox(opts?: CheckboxOptions): HTMLElement</code>
<h3>opts?: CheckboxOptions</h3>
<p>
复选框初始参数,结构为
<pre>interface CheckboxOptions {
className?: string;
enabled?: boolean;
name?: string;
tabIndex?: Number;
type?: string;
label?: string;
checked?: boolean;
imageHeight?: Number;
checkedNode?: HTMLElement;
uncheckedNode?: HTMLElement;
customerAttributes?: { [key: string]: string };
onchange?: (this: HTMLInputElement, ev: Event) => any;
}</pre>
</p>
<h4>className?: string</h4>
<p>
复选框的自定义 className
</p>
<h4>enabled?: boolean</h4>
<p>
复选框默认是否可用
</p>
<h4>name?: string</h4>
<p>
复选框或单选框的 name
</p>
<h4>tabIndex?: Number</h4>
<p>
复选框的 tabindex
</p>
<h4>type?: string</h4>
<p>
复选框图标的样式,可选值目前有 <code>fa-regular</code><code>fa-light</code><code>fa-solid</code>
</p>
<h4>label?: string | HTMLElement</h4>
<p>
复选框的标签文本,或者想要呈现的元素
</p>
<h4>checked?: boolean</h4>
<p>
初始是否选中
</p>
<h4>imageHeight?: Number</h4>
<p>
为图片复选框时的图片限制高度
</p>
<h4>checkedNode?: HTMLElement</h4>
<p>
为图片复选框时的选中时显示的元素
</p>
<h4>uncheckedNode?: HTMLElement</h4>
<p>
为图片复选框时的未选中时显示的元素
</p>
<h4>customerAttributes?: { [key: string]: string }</h4>
<p>
自定义属性,例如
<pre>{
'data-id': 'xxxxxx',
'disabled': ''
}</pre>
</p>
<h4>onchange?: (this: HTMLInputElement, ev: Event) => any</h4>
<p>
复选框改变时触发的事件
</p>
<h2>createRadiobox</h2>
<code>function createRadiobox(opts?: CheckboxOptions): HTMLElement</code>
<h3>opts?: CheckboxOptions</h3>
<p>
单选框初始参数,结构如上
</p>
<h2>resolveCheckbox</h2>
<code>function resolveCheckbox(container?: HTMLElement, legacy?: boolean): HTMLElement</code>
<h3>container?: HTMLElement</h3>
<p>
将把此 HTML 元素,为 null 则把 document.body 下的所有 <code>label[data-checkbox]</code> 元素解析为复选框,<code>[data-id]</code> 为复选框元素的
id包含
<code>[data-checked]</code> 时复选框默认选中。
</p>
<p>当该元素无子元素时,<code>[data-type]</code> 同上述参数中的 <code>type?: string</code><code>[data-label]</code> 同上述参数中的
<code>label?: string</code>
</p>
<p>当该元素有子元素时解析为图片复选框class 为 <code>checked</code><code>unchecked</code> 的子元素将分别在选中与未选中时显示。</p>
<h3>legacy?: boolean</h3>
<p>
是否开启兼容模式,启用兼容模式时将试图匹配 <code>input[type="checkbox"]</code> 标签,与其周围的 label将其转换为统一样式的复选框。
</p>
<hr />
<h2>示例</h2>
<pre>&lt;div id="checkbox-sample"&gt;
&lt;!-- 1 --&gt;
&lt;label data-checkbox data-type="fa-light" data-label="Checkbox Light"&gt;&lt;/label&gt;
&lt;!-- 2 --&gt;
&lt;label data-checkbox data-checked data-label="Checkbox Regular"&gt;&lt;/label&gt;
&lt;!-- 3 --&gt;
&lt;label data-checkbox data-type="fa-solid" data-label="Checkbox Solid"&gt;&lt;/label&gt;
&lt;!-- 4 --&gt;
&lt;label data-checkbox&gt;
&lt;code class="checked"&gt;Checked&lt;/code&gt;
&lt;code class="unchecked"&gt;Unchecked&lt;/code&gt;
&lt;/label&gt;
&lt;!-- 5 --&gt;
&lt;input id="check-status" type="checkbox"/&gt;
&lt;label for="check-status"&gt;Label for Status&lt;/label&gt;
&lt;/div&gt;
&lt;script type="text/javascript"&gt;
window["lib-ui"].resolveCheckbox(document.querySelector("#checkbox-sample"), true);
&lt;/script&gt;</pre>
<div id="checkbox-sample">
<label data-checkbox data-type="fa-light" data-label="Checkbox Light"></label>
<label data-checkbox data-checked data-label="Checkbox Regular"></label>
<label data-checkbox data-type="fa-solid" data-label="Checkbox Solid"></label>
<label data-checkbox>
<code class="checked">Checked</code>
<code class="unchecked">Unchecked</code>
</label>
<input id="check-status" type="checkbox" />
<label for="check-status">Label for Status</label>
</div>
<script type="text/javascript">
window["lib-ui"].resolveCheckbox(document.querySelector("#checkbox-sample"), true);
</script>
</div>

View File

@ -2,7 +2,11 @@ import './css/checkbox.scss';
import { createElement } from "../functions";
import { createIcon } from "./icon";
function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, charactor = 'check') {
function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, charactor = 'check', title) {
const checkIcon = createIcon(type, charactor);
checkIcon.classList.add('ui-check-icon');
const indeterminateIcon = createIcon(type, 'grip-lines');
indeterminateIcon.classList.add('ui-indeterminate-icon')
container.appendChild(
createElement('layer', layer => {
layer.className = 'ui-check-inner';
@ -18,18 +22,23 @@ function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, char
if (tabindex >= 0) {
layer.tabIndex = tabindex;
}
}, createIcon(type, charactor))
}, checkIcon, indeterminateIcon)
);
if (label instanceof Element) {
container.appendChild(label);
} else if (label?.length > 0) {
} else if (label != null && String(label).length > 0) {
container.appendChild(
createElement('span', span => span.innerText = label)
createElement('span', span => {
span.innerText = label;
if (title != null) {
span.title = title;
}
})
);
}
}
function createRadiobox(opts = {}) {
export function createRadiobox(opts = {}) {
const container = createElement('label', 'ui-check-wrapper ui-radio-wrapper',
createElement('input', input => {
input.setAttribute('type', 'radio');
@ -40,8 +49,8 @@ function createRadiobox(opts = {}) {
if (opts.enabled === false) {
input.disabled = true;
}
if (opts.customerAttributes != null) {
for (let entry of Object.entries(opts.customerAttributes)) {
if (opts.customAttributes != null) {
for (let entry of Object.entries(opts.customAttributes)) {
input.setAttribute(entry[0], entry[1]);
}
}
@ -52,22 +61,25 @@ function createRadiobox(opts = {}) {
if (opts.className) {
container.classList.add(opts.className);
}
fillCheckbox(container, opts.type, opts.label, opts.tabIndex, 'circle');
fillCheckbox(container, opts.type, opts.label, opts.tabIndex, 'circle', opts.title);
return container;
}
function createCheckbox(opts = {}) {
const container = createElement('label', 'ui-check-wrapper',
export function createCheckbox(opts = {}) {
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;
}
if (opts.customerAttributes != null) {
for (let entry of Object.entries(opts.customerAttributes)) {
if (opts.customAttributes != null) {
for (let entry of Object.entries(opts.customAttributes)) {
input.setAttribute(entry[0], entry[1]);
}
}
@ -81,7 +93,23 @@ function createCheckbox(opts = {}) {
if (opts.enabled === false) {
container.classList.add('disabled');
}
if (opts.checkedNode != null && opts.uncheckedNode != null) {
if (opts.switch) {
const label = opts.label;
if (label instanceof Element) {
container.appendChild(label);
} else {
container.appendChild(
createElement('span', span => {
if (label != null && String(label).length > 0) {
span.innerText = label;
}
if (opts.title != null) {
span.title = opts.title;
}
})
);
}
} else if (opts.checkedNode != null && opts.uncheckedNode != null) {
container.classList.add('ui-check-image-wrapper');
let height = opts.imageHeight;
if (isNaN(height) || height <= 0) {
@ -92,12 +120,12 @@ function createCheckbox(opts = {}) {
opts.uncheckedNode.classList.add('unchecked');
container.appendChild(opts.uncheckedNode);
} else {
fillCheckbox(container, opts.type, opts.label, opts.tabIndex);
fillCheckbox(container, opts.type, opts.label, opts.tabIndex, undefined, opts.title);
}
return container;
}
function resolveCheckbox(container = document.body, legacy) {
export function resolveCheckbox(container = document.body, legacy) {
if (legacy) {
const checks = container.querySelectorAll('input[type="checkbox"]');
for (let chk of checks) {
@ -144,7 +172,7 @@ function resolveCheckbox(container = document.body, legacy) {
label.className = 'ui-check-wrapper';
}
label.replaceChildren();
fillCheckbox(label, 'fa-regular', text, chk.tabIndex);
fillCheckbox(label, 'fa-regular', text, chk.tabIndex, undefined, label.title);
label.insertBefore(chk, label.firstChild);
}
}
@ -161,7 +189,10 @@ function resolveCheckbox(container = document.body, legacy) {
fillCheckbox(box,
box.dataset.type,
box.dataset.label,
box.dataset.tabIndex)
box.dataset.tabIndex,
undefined,
box.title);
box.removeAttribute('title');
box.removeAttribute('data-type');
box.removeAttribute('data-label');
}
@ -177,10 +208,4 @@ function resolveCheckbox(container = document.body, legacy) {
box.insertBefore(input, box.firstChild);
}
return container;
}
export {
createCheckbox,
resolveCheckbox,
createRadiobox
}

View File

@ -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;
}
}
}

View File

@ -6,6 +6,16 @@
font-family: var(--font-family);
@include outborder();
&.validation-error,
&:invalid {
border-color: var(--red-color);
&:focus,
&:hover {
border-color: var(--red-color);
}
}
}
.ui-input {

View File

@ -30,7 +30,8 @@ $listMaxHeight: 210px;
flex: 1 1 auto;
cursor: pointer;
font-size: var(--font-size);
// line-height: $headerHeight;
line-height: $headerHeight;
min-height: $headerHeight;
padding: 0 6px;
overflow: hidden;
text-overflow: ellipsis;
@ -84,99 +85,114 @@ $listMaxHeight: 210px;
}
}
}
}
>.ui-drop-box {
position: absolute;
visibility: hidden;
opacity: 0;
transform: scaleY(0);
transform-origin: top;
background-color: var(--bg-color);
top: calc($headerHeight + 2px);
z-index: 2;
transition: transform 120ms ease, opacity 120ms ease, visibility 120ms ease;
width: calc(100% + 2px);
.ui-drop-box {
position: absolute;
visibility: hidden;
opacity: 0;
transform: scaleY(0);
transform-origin: top;
background-color: var(--bg-color);
top: calc($headerHeight + 2px);
z-index: 2;
transition: transform 120ms ease, opacity 120ms ease, visibility 120ms ease;
min-width: calc(100% + 2px);
box-sizing: border-box;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05);
left: -1px;
&.slide-up {
/*border-top-width: 1px;
border-bottom-width: 0;*/
transform-origin: bottom;
top: unset;
bottom: calc($headerHeight + 2px);
}
&.active {
visibility: visible;
opacity: 1;
transform: scaleY(1);
}
>.ui-drop-search {
box-sizing: border-box;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05);
left: -1px;
height: $searchBarHeight;
line-height: $searchBarHeight;
padding: 0 8px;
position: relative;
display: flex;
align-items: center;
&.slide-up {
/*border-top-width: 1px;
border-bottom-width: 0;*/
transform-origin: bottom;
top: unset;
bottom: calc($headerHeight + 2px);
}
&.active {
visibility: visible;
opacity: 1;
transform: scaleY(1);
}
>.ui-drop-search {
>input[type="text"] {
box-sizing: border-box;
height: $searchBarHeight;
line-height: $searchBarHeight;
padding: 0 8px;
position: relative;
display: flex;
align-items: center;
width: 100%;
height: $searchInputHeight;
padding: 0 6px 0 22px;
color: var(--color);
>input[type="text"] {
box-sizing: border-box;
width: 100%;
height: $searchInputHeight;
padding: 0 6px 0 22px;
color: var(--color);
@include outborder();
@include outborder();
// &:focus {
// box-shadow: 0 0 3px 1px rgba(0, 0, 0, .2);
// }
// &:focus {
// box-shadow: 0 0 3px 1px rgba(0, 0, 0, .2);
// }
&::placeholder {
font-style: italic;
}
}
>svg {
position: absolute;
left: 14px;
width: $searchIconSize;
height: 100%;
cursor: text;
&::placeholder {
font-style: italic;
}
}
>.ui-drop-list {
margin: 0;
padding: 0;
list-style: none;
max-height: $listMaxHeight;
overflow-y: auto;
font-size: var(--font-size);
@include scrollbar();
>svg {
position: absolute;
left: 14px;
width: $searchIconSize;
height: 100%;
cursor: text;
}
}
&.filtered>li:first-child {
>.ui-drop-list {
max-height: $listMaxHeight;
overflow-y: auto;
position: relative;
font-size: var(--font-size);
@include scrollbar();
&.filtered>.drop-content>.li:first-child {
background-color: var(--hover-bg-color);
}
>.drop-content {
position: absolute;
width: 100%;
}
li {
// display: flex;
// align-items: center;
list-style: none;
line-height: $dropItemHeight;
height: $dropItemHeight;
padding: 0 10px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover,
&.selected {
background-color: var(--hover-bg-color);
}
>li {
// display: flex;
// align-items: center;
line-height: $dropItemHeight;
height: $dropItemHeight;
padding: 0 10px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
>.li-wrapper {
display: flex;
align-items: center;
&:hover,
&.selected {
background-color: var(--hover-bg-color);
>.ui-expandor {
width: 12px;
height: 12px;
display: flex;
}
>.ui-check-wrapper {

View File

@ -44,6 +44,11 @@
}
}
@mixin wrap() {
white-space: normal;
word-break: break-word;
}
@mixin ui-check() {
.ui-check-inner {
position: relative;
@ -87,7 +92,22 @@
border-color: var(--link-color);
background-color: var(--link-color);
>svg {
>.ui-check-icon {
transform: scale(1);
opacity: 1;
}
}
&:indeterminate+.ui-check-inner {
border-color: var(--secondary-color);
background-color: var(--secondary-color);
>.ui-check-icon {
transform: scale(0);
opacity: 0;
}
>.ui-indeterminate-icon {
transform: scale(1);
opacity: 1;
}

View File

@ -3,12 +3,10 @@
.ui-grid {
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: visible;
overflow: auto;
& {
--hover-bg-color: lightyellow;
--cell-hover-bg-color: lightyellow;
--header-border-color: #adaba9;
--header-bg-color: #fafafa;
--header-fore-color: #000;
@ -21,13 +19,14 @@
--row-bg-color: #fff;
--row-active-bg-color: #fafafa;
--row-selected-bg-color: #e6f2fb;
--total-row-bg-color: #b3b3b3;
--text-disabled-color: gray;
--filter-shadow: 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05);
--filter-transition: transform .12s ease, opacity .24s ease;
--row-height: 36px;
--header-line-height: 26px;
--header-line-height: 20px;
--text-indent: 8px;
--loading-size: 40px;
@ -35,7 +34,7 @@
--arrow-size: 4px;
--filter-size: 10px;
--split-width: 8px;
--split-width: 10px;
--dragger-size: 20px;
--dragger-opacity: .6;
--dragger-cursor-size: 4px;
@ -45,13 +44,18 @@
--header-padding: 4px 12px 4px 8px;
--header-filter-padding: 4px 26px 4px 8px;
--spacing-s: 4px;
--spacing-cell: 6px 4px 6px 8px;
--spacing-cell: 9px 4px 9px 8px;
--spacing-drop-cell: 5px 4px 5px 8px;
--filter-line-height: 30px;
--filter-item-padding: 0 4px;
}
@include outline();
@include scrollbar();
&,
input[type="text"],
input[type="date"],
textarea {
font-size: var(--font-size);
font-family: var(--font-family);
@ -64,299 +68,430 @@
visibility: hidden;
}
>.ui-grid-header {
width: 100%;
min-width: 100%;
margin: 0;
border-bottom: 1px solid var(--header-border-color);
background-color: var(--header-bg-color);
color: var(--header-fore-color);
user-select: none;
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
>.ui-grid-wrapper {
position: relative;
tr {
position: relative;
>.ui-grid-table {
position: absolute;
width: 100%;
min-width: 100%;
margin: 0;
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
>th {
padding: 0;
margin: 0;
word-wrap: break-word;
white-space: normal;
position: relative;
>thead {
color: var(--header-fore-color);
>div {
line-height: var(--header-line-height);
min-height: var(--row-height);
display: flex;
align-items: center;
padding: var(--header-padding);
box-sizing: border-box;
// overflow-x: hidden;
tr {
>span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
>th {
background-color: var(--header-bg-color);
user-select: none;
padding: 0;
margin: 0;
word-wrap: break-word;
white-space: normal;
// position: relative;
top: 0;
position: sticky;
z-index: 1;
>.arrow {
width: 0;
height: 0;
top: 50%;
margin-top: calc(0px - var(--arrow-size) / 2);
right: calc(var(--arrow-size) / 2);
position: absolute;
&.sticky {
position: sticky;
z-index: 2;
}
&.asc {
border-bottom: var(--arrow-size) solid var(--dark-border-color);
}
>div {
line-height: var(--header-line-height);
min-height: var(--row-height);
display: flex;
align-items: center;
padding: var(--header-padding);
box-sizing: border-box;
// overflow-x: hidden;
// border-right: 1px solid transparent;
// transition: border-color .12s ease;
&.desc {
border-top: var(--arrow-size) solid var(--dark-border-color);
}
>span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&.asc,
&.desc {
border-left: var(--arrow-size) solid transparent;
border-right: var(--arrow-size) solid transparent;
}
}
&.wrap {
@include wrap();
}
}
>.filter {
width: var(--filter-size);
height: var(--filter-size);
top: 50%;
margin-top: calc(0px - var(--filter-size) / 2);
right: calc(var(--arrow-size) * 2 + 4px);
position: absolute;
display: flex;
>.ui-check-wrapper,
>.ui-switch {
height: 20px;
padding: 0 4px 0 0;
}
>svg {
width: 100%;
height: 100%;
fill: var(--color);
opacity: .2;
transition: opacity .12s ease;
>svg {
width: 12px;
min-width: 12px;
height: 12px;
margin-left: 4px;
fill: var(--split-border-color);
}
}
&:hover {
opacity: .8;
>.arrow {
width: 0;
height: 0;
top: 50%;
margin-top: calc(0px - var(--arrow-size) / 2);
right: calc(var(--arrow-size) / 2);
position: absolute;
&.asc {
border-bottom: var(--arrow-size) solid var(--dark-border-color);
}
&.desc {
border-top: var(--arrow-size) solid var(--dark-border-color);
}
&.asc,
&.desc {
border-left: var(--arrow-size) solid transparent;
border-right: var(--arrow-size) solid transparent;
}
}
>.filter {
width: var(--filter-size);
height: var(--filter-size);
top: 50%;
margin-top: calc(0px - var(--filter-size) / 2);
right: calc(var(--arrow-size) * 2 + 4px);
position: absolute;
display: flex;
>svg {
width: 100%;
height: 100%;
fill: var(--color);
opacity: .2;
transition: opacity .12s ease;
&:hover {
opacity: .8;
}
}
&.hover>svg {
opacity: .8;
}
&.active>svg {
opacity: 1;
}
}
>.spliter {
position: absolute;
height: 100%;
top: 0;
right: calc(1px - var(--split-width) /2);
width: var(--split-width);
cursor: ew-resize;
z-index: 2;
&::after {
content: '';
height: 100%;
width: 1px;
display: block;
margin: 0 auto;
transition: background-color .12s ease;
}
// &:hover::after {
// background-color: var(--split-border-color);
// }
}
>.bottom-border {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
z-index: 2;
background-color: var(--header-border-color);
}
>.dragger {
position: absolute;
left: 0;
top: 0;
min-width: var(--dragger-size);
height: 100%;
background-color: var(--dragger-bg-color);
opacity: var(--dragger-opacity);
display: none;
}
>.dragger-cursor {
position: absolute;
top: 0;
height: 100%;
border: 1px solid var(--dragger-cursor-color);
box-sizing: border-box;
margin-left: 0;
opacity: var(--dragger-cursor-opacity);
display: none;
transition: left .12s ease;
&::before {
top: -1px;
border-top: var(--dragger-cursor-size) solid;
}
&::after {
bottom: -1px;
border-bottom: var(--dragger-cursor-size) solid;
}
&::before,
&::after {
content: '';
position: absolute;
left: var(--dragger-cursor-pos);
border-left: var(--dragger-cursor-size) solid transparent;
border-right: var(--dragger-cursor-size) solid transparent;
}
}
&.header-filter>div {
padding: var(--header-filter-padding);
}
}
&.hover>svg {
opacity: .8;
}
&.active>svg {
opacity: 1;
}
}
>.spliter {
position: absolute;
height: 100%;
top: 0;
right: calc(0px - var(--split-width) /2);
width: var(--split-width);
cursor: ew-resize;
z-index: 1;
&::after {
content: '';
height: 100%;
width: 1px;
display: block;
margin: 0 auto;
transition: background-color .12s ease;
}
&:hover::after {
// &:hover>th>div {
// border-color: var(--split-border-color);
// }
&:hover>th>.spliter::after {
background-color: var(--split-border-color);
}
}
>.dragger {
position: absolute;
left: 0;
top: 0;
min-width: var(--dragger-size);
height: 100%;
background-color: var(--dragger-bg-color);
opacity: var(--dragger-opacity);
display: none;
}
>.dragger-cursor {
position: absolute;
top: 0;
height: 100%;
border: 1px solid var(--dragger-cursor-color);
box-sizing: border-box;
margin-left: 0;
opacity: var(--dragger-cursor-opacity);
display: none;
transition: left .12s ease;
&::before {
top: -1px;
border-top: var(--dragger-cursor-size) solid;
}
&::after {
bottom: -1px;
border-bottom: var(--dragger-cursor-size) solid;
}
&::before,
&::after {
content: '';
position: absolute;
left: var(--dragger-cursor-pos);
border-left: var(--dragger-cursor-size) solid transparent;
border-right: var(--dragger-cursor-size) solid transparent;
}
}
&.header-filter>div {
padding: var(--header-filter-padding);
}
}
}
}
>.ui-grid-body {
flex: 1 1 auto;
overflow: auto;
color: var(--cell-fore-color);
@include scrollbar();
>tbody,
>tfoot {
.ui-grid-body-content {
position: absolute;
min-width: 100%;
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
>.ui-grid-row {
line-height: var(--line-height);
white-space: nowrap;
box-sizing: border-box;
>.ui-grid-row {
line-height: var(--line-height);
white-space: nowrap;
background-color: var(--row-bg-color);
border-bottom: 1px solid var(--cell-border-color);
box-sizing: border-box;
&:hover {
background-color: var(--row-active-bg-color);
}
&.selected {
background-color: var(--row-selected-bg-color);
}
>td {
padding: 0;
>span {
padding: var(--spacing-cell);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
}
>input[type="text"],
>textarea {
border: none;
box-sizing: border-box;
width: 100%;
>td {
padding: 0;
@include outline();
&:disabled {
color: var(--text-disabled-color);
&.sticky {
position: sticky;
z-index: 1;
}
}
>input[type="text"] {
height: var(--row-height);
text-indent: var(--text-indent);
}
>span {
margin: var(--spacing-cell);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
>textarea {
resize: none;
line-height: var(--line-height);
display: block;
padding: var(--spacing-cell);
white-space: nowrap;
@include scrollbar();
}
.ui-check-wrapper {
display: flex;
justify-content: center;
.ui-check-inner {
&,
>svg {
transition: none;
&.wrap {
@include wrap();
}
}
}
}
}
.ui-drop-wrapper {
height: var(--row-height);
width: 100%;
display: flex;
flex-direction: column;
>tfoot {
color: var(--header-fore-color);
position: absolute;
width: 100%;
background-color: var(--total-row-bg-color);
>.ui-drop-header {
>.ui-grid-row>td {
font-weight: bold;
&.sticky {
background-color: var(--total-row-bg-color);
}
}
}
>tbody {
color: var(--cell-fore-color);
>.ui-grid-row {
background-color: var(--row-bg-color);
border-bottom: 1px solid var(--cell-border-color);
&:hover {
background-color: var(--row-active-bg-color);
>td.sticky {
background-color: var(--row-active-bg-color);
}
}
&.selected {
background-color: var(--row-selected-bg-color);
>td.sticky {
background-color: var(--row-selected-bg-color);
}
}
>td {
&.sticky {
background-color: var(--row-bg-color);
}
&.ui-expandable {
&>svg {
width: 24px;
height: 34px;
padding: 8px 3px;
box-sizing: border-box;
display: block;
cursor: pointer;
transition: opacity .12s ease;
&:hover {
opacity: .4;
}
}
}
>input[type="text"],
>input[type="date"],
>textarea {
border: none;
height: 100%;
box-sizing: border-box;
width: 100%;
padding: 0;
max-height: none !important;
>.ui-drop-text {
padding: var(--spacing-cell);
@include outline();
&:disabled {
color: var(--text-disabled-color);
}
}
>.ui-drop-box {
top: calc(var(--row-height) + 2px);
>input[type="text"] {
height: var(--row-height);
text-indent: var(--text-indent);
}
&.slide-up {
top: unset;
bottom: calc(var(--row-height) + 2px);
>textarea {
resize: none;
line-height: var(--line-height);
display: block;
padding: var(--spacing-cell);
white-space: nowrap;
@include scrollbar();
}
.ui-check-wrapper,
.ui-switch {
display: inline-flex;
justify-content: center;
height: var(--row-height);
padding: 0 8px;
.ui-check-inner {
&,
>svg {
transition: none;
}
}
>span:first-of-type {
&:before,
&:after {
transition: none;
}
}
}
}
.col-icon {
display: flex;
cursor: pointer;
justify-content: center;
align-items: center;
position: relative;
.ui-drop-span {
margin: 0;
>svg {
width: 16px;
height: 16px;
fill: var(--primary-color);
transition: opacity .12s ease;
>span {
margin: var(--spacing-cell);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
}
&.wrap>span {
@include wrap();
}
}
&:hover>svg {
opacity: .4;
.ui-drop-wrapper {
height: var(--row-height);
width: 100%;
display: flex;
flex-direction: column;
>.ui-drop-header {
border: none;
height: 100%;
>.ui-drop-text {
padding: var(--spacing-drop-cell);
}
}
}
&.disabled {
cursor: unset;
.ui-date-cell {
height: var(--row-height);
text-indent: 4px;
&:invalid {
color: rgba(0, 0, 0, .3);
}
}
.col-icon {
display: flex;
cursor: pointer;
justify-content: center;
align-items: center;
position: relative;
// padding: var(--spacing-s);
>svg {
fill: var(--header-border-color);
opacity: unset;
width: 16px;
height: 16px;
fill: var(--primary-color);
transition: opacity .12s ease;
}
&:hover>svg {
opacity: .4;
}
&.disabled {
cursor: unset;
>svg {
fill: var(--header-border-color);
opacity: unset;
}
}
}
}
@ -369,13 +504,15 @@
position: absolute;
line-height: var(--line-height);
padding: var(--spacing-cell);
background-color: var(--hover-bg-color);
background-color: var(--cell-hover-bg-color);
white-space: pre;
display: flex;
align-items: center;
overflow: hidden;
visibility: hidden;
opacity: 0;
transition: visibility 0s linear .12s, opacity .12s ease;
z-index: 2;
&.active {
visibility: visible;
@ -394,7 +531,7 @@
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
z-index: 2;
>div {
background-color: var(--loading-fore-color);
@ -409,6 +546,10 @@
}
}
~.ui-drop-box {
max-width: 300px;
}
>.filter-panel {
position: absolute;
width: 200px;
@ -421,6 +562,7 @@
opacity: 0;
display: flex;
flex-direction: column;
z-index: 2;
&.active {
transform: scaleY(1);
@ -436,6 +578,7 @@
text-indent: 16px;
width: 100%;
font-size: var(--font-smaller-size);
height: var(--line-height);
line-height: var(--line-height);
}
@ -449,12 +592,142 @@
cursor: text;
}
}
>.filter-item-list {
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
position: relative;
user-select: none;
@include scrollbar();
>.filter-content {
position: absolute;
width: 100%;
}
.filter-item {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
padding: var(--filter-item-padding);
&:hover {
background-color: var(--hover-bg-color);
}
.ui-check-wrapper {
height: var(--filter-line-height);
line-height: var(--filter-line-height);
display: flex;
.ui-check-inner+* {
font-size: var(--font-smaller-size);
}
}
}
}
>.filter-function {
display: flex;
justify-content: flex-end;
padding: 4px;
>.button {
box-sizing: border-box;
padding-inline: 6px;
text-align: center;
margin-right: 10px;
min-width: 40px;
height: var(--filter-line-height);
line-height: var(--filter-line-height);
border: none;
background-color: transparent;
cursor: pointer;
border-radius: 0;
transition: background-color .12s ease;
@include outline();
&:hover {
background-color: var(--hover-bg-color);
}
}
}
}
.ui-sort-panel-content {
height: 100%;
display: flex;
flex-direction: column;
>.ui-sort-panel-buttons {
flex: 0 0 auto;
white-space: nowrap;
overflow: hidden;
>.button {
margin-right: 6px;
padding-inline: 6px;
text-align: center;
border: none;
line-height: 28px;
color: var(--title-color);
border-radius: var(--corner-radius);
padding: 0 10px;
box-sizing: border-box;
height: 28px;
line-height: 28px;
cursor: pointer;
user-select: none;
background-color: var(--title-bg-color);
transition: opacity .12s ease;
display: inline-flex;
align-items: center;
&:hover {
opacity: .8;
}
&:disabled {
opacity: .6;
cursor: default;
}
>svg {
flex: 0 0 auto;
width: 16px;
height: 16px;
fill: var(--title-color);
}
>span {
flex: 1 1 auto;
margin-left: 4px;
}
}
}
>.ui-sort-panel-grid {
flex: 1 1 auto;
position: relative;
height: calc(100% - 30px);
}
}
}
@media (prefers-color-scheme: dark) {
.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 {
--hover-bg-color: yellow;
--cell-hover-bg-color: yellow;
--header-border-color: #525456;
--header-bg-color: #050505;
--header-fore-color: #fff;
@ -469,4 +742,4 @@
--row-selected-bg-color: #190d04;
--text-disabled-color: gray;
}
}
}*/

299
lib/ui/css/media.scss Normal file
View File

@ -0,0 +1,299 @@
.ui-media-picture>img {
max-width: 100px;
max-height: 100px;
}
.ui-media-file {
display: inline-flex;
align-items: center;
&::before {
display: none;
}
>svg {
width: 20px;
height: 20px;
fill: var(--secondary-link-color);
}
>a {
margin-left: 6px;
color: var(--link-color);
font-size: var(--font-size);
}
}
.ui-media-audio {
display: inline-flex;
align-items: center;
background-color: #eee;
border-radius: 20px;
overflow: hidden;
padding-right: 16px;
>button {
background-color: transparent;
border: none;
width: 40px;
height: 40px;
padding: 14px;
display: flex;
outline: none;
&:hover {
border: none;
}
&::after {
display: none;
}
>svg {
width: 100%;
height: 100%;
}
}
>.ui-media-timestamp {
color: var(--secondary-color);
font-size: var(--font-size);
user-select: none;
}
}
.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;
}
}
}

View File

@ -32,6 +32,7 @@ $buttonHeight: 28px;
.ui-popup-container {
min-width: 400px;
max-width: 800px;
max-height: 90vh;
background-color: var(--bg-color);
border-radius: var(--corner-radius);
box-shadow: 0 2px 8px var(--shadow-color);
@ -44,11 +45,10 @@ $buttonHeight: 28px;
.ui-popup-header {
flex: 0 0 auto;
padding: 10px 12px 6px;
border-radius: var(--corner-radius) var(--corner-radius) 0 0;
line-height: $headerLineHeight;
user-select: none;
background-color: var(--title-bg-color);
background-color: var(--title-ctrlbg-color);
color: var(--title-color);
display: flex;
align-items: center;
@ -62,28 +62,44 @@ $buttonHeight: 28px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 5px 0 3px 12px;
}
>svg {
>.ui-popup-header-title,
.ui-popup-move {
cursor: move;
}
>.ui-popup-header-title.no-move {
cursor: default;
}
>.ui-popup-header-icons {
flex: 0 0 auto;
width: $headerLineHeight;
height: $headerLineHeight;
fill: var(--title-color);
padding: 4px;
cursor: pointer;
box-sizing: border-box;
transition: opacity .12s ease;
padding: 5px 12px 3px 0;
display: flex;
&:hover {
opacity: .8;
}
>svg {
flex: 0 0 auto;
width: $headerLineHeight;
height: $headerLineHeight;
fill: var(--title-color);
padding: 4px;
cursor: pointer;
box-sizing: border-box;
transition: opacity .12s ease;
&:focus,
&:focus-visible {
outline: none;
opacity: .8;
background-color: rgb(0 0 0/10%);
border-radius: var(--corner-radius);
&:hover {
opacity: .8;
}
&:focus,
&:focus-visible {
outline: none;
opacity: .8;
background-color: rgb(0 0 0/10%);
border-radius: var(--corner-radius);
}
}
}
}
@ -118,6 +134,10 @@ $buttonHeight: 28px;
animation: loading-spinner 1.2s infinite linear;
}
}
&.ui-popup-loading-content {
bottom: 0;
}
}
>.message-wrapper {
@ -125,8 +145,9 @@ $buttonHeight: 28px;
margin: 10px;
>svg {
width: 40px;
height: 40px;
width: 30px;
height: 30px;
fill: unset;
+span {
padding-left: 16px;
@ -184,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;
@ -192,13 +217,13 @@ $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;
cursor: pointer;
user-select: none;
background-color: var(--title-bg-color);
background-color: var(--title-ctrlbg-color);
transition: opacity .12s ease;
&:focus,
@ -207,6 +232,11 @@ $buttonHeight: 28px;
}
@include outline();
&:disabled {
opacity: .4;
cursor: default;
}
}
}

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

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

View File

@ -51,4 +51,13 @@
height: calc(100% - 16px);
user-select: none;
}
&.ui-tooltip-down>.ui-tooltip-pointer {
bottom: unset;
top: -8px;
}
&.ui-tooltip-no>.ui-tooltip-pointer {
display: none;
}
}

View File

@ -10,9 +10,10 @@
}
:root {
color-scheme: light dark;
/*color-scheme: light dark;*/
--color: #201f1e;
--secondary-color: #777;
--bg-color: #fff;
--border-color: #b9b9b9;
--focus-border-color: #666;
@ -20,27 +21,34 @@
--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;
--title-bg-color: rgb(68, 114, 196);
--title-ctrlbg-color: rgb(68, 114, 196);
--hover-bg-color: #eee;
--link-color: #1890ff;
--secondary-link-color: #1d9ac0;
--primary-color: rgb(123, 28, 33);
--loading-bg-color: hsla(0, 0%, 100%, .4);
--loading-fore-color: rgba(0, 0, 0, .2);
--border-radius: 2px;
--text-indent: 4px;
--line-height: 24px;
--line-height: 18px;
--settings-line-height: 32px;
--font-size: .8125rem; // 13px
--font-smaller-size: .75rem; // 12px
--font-larger-size: .875rem; // 14px
--font-header-size: 1.5rem; // 24px
--font-family: "Franklin Gothic Book", "San Francisco", "Segoe UI", "Open Sans", "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei UI", sans-serif;
--header-font-family: Arial, sans-serif;
}
@media (prefers-color-scheme: dark) {
/*@media (prefers-color-scheme: dark) {
:root {
--color: rgb(255 255 255/87%);
--bg-color: #141414;
@ -60,4 +68,4 @@
--loading-bg-color: rgb(20 20 20/40%);
--loading-fore-color: rgb(255 255 255/20%);
}
}
}*/

138
lib/ui/date.d.ts vendored Normal file
View File

@ -0,0 +1,138 @@
/**
* 创建或转换日期选择框
* @param min 最小可选日期
* @param max 最大可选日期
* @param element 转换该元素为日期选择框
* @returns 返回创建或转换的日期选择框
*/
export function createDateInput(min?: string, max?: string, element?: HTMLInputElement): HTMLInputElement;
/**
* 将日期转换成 yyyy-MM-dd 格式的字符串
* @param dt 日期值
* @param local 是否视日期为本地时间
*/
export function toDateValue(dt: Date, local?: boolean): string;
/**
* 格式化日期字符串
* @param date 要格式化的日期值<br/><br/>
* 支持以下几种数据类型<br/><br/>
* `"2024-01-26"`<br/>
* `"1/26/2024"`<br/>
* `"638418240000000000"`<br/>
* `new Date('2024-01-26')`<br/>
* @returns 格式化为 M/d/yyyy 的日期字符串
*/
export function formatDate(date: Date | number | string, formatter?: string): string;
/**
* 设置显示日期
* @param element 要设置显示日期的元素
* @param val 日期值,支持格式参见 {@linkcode formatDate}
* @param formatter 日期格式化字符串(仅设置显示元素时调用)
*/
export function setDateValue(element: HTMLElement, val: Date | number | string, formatter?: string): void;
/**
* 从日期选择框获取日期值
* @param element 要获取的日期选择框
* @param formatter 自定义格式化函数,传入参数为 `Date` 类型
* @returns 默认返回日期 `ticks` 的字符串
*/
export function getDateValue(element: HTMLInputElement, formatter?: string | ((date: Date) => string)): string;
/** 日期选择框类 */
export class DateSelector {
/**
* 把父容器下所有匹配 `input[data-type="date"]` 的元素修改为统一的日期选择框<br/><br/>
* 解析的属性为 `id`, `class`, `data-min`, `data-max`, `disabled`
* @param dom 父元素
* @param trigger 日期设置事件触发函数(上下文为触发设置日期的 `DateSelector` 实例)
* @example
* HTML:
* ```html
* <input id="dateFrom" data-type="date" data-min="1980-01-01"/>
* ```
* js:
* ```js
* const libUI = window['lib-ui'];
* const DateSelector = libUI.DateSelector;
* const formatDate = libUI.formatDate;
*
*
* // 解析 document.body 下所有符合条件的元素,转换为日期选择框,第二个参数可选
* DateSelector.resolve(document.body, function (date) {
* console.log(`element(#${this.element.id}), date changed to: ${formatDate(date)}`);
* // 当日期选择改变时控制台将会输出element(#dateFrom), date changed to: 1/30/2024
* });
*
*
* // 在其他地方调用时
* const value = document.querySelector('#dateFrom').value;
* console.log(`dateFrom.value = '${value}', formatted: '${formatDate(value)}'`);
* // 控制台会输出dateFrom.value = '2024-01-30', formatted: '1/30/2024'
* ```
*/
static resolve(dom?: HTMLElement, trigger?: (this: DateSelector, date: Date) => void): HTMLElement;
/**
* 日期选择框构造函数
* @param opts 日期选项参数
*/
constructor(opts: {
/** 最小可选择日期 */
minDate?: string,
/** 最大可选择日期 */
maxDate?: string,
/** 是否启用 */
enabled?: boolean,
/** 焦点索引 */
tabIndex?: number,
/** 类名 */
className?: string,
/**
* 自定义格式化函数,用于获取日期值时调用
* @param date 日期值
* @returns 返回格式化的值
*/
valueFormatter?: (date: Date) => any
});
/**
* 创建或转换日期选择框元素
* @param element 转换该元素为日期选择框
* @returns 返回创建或转换的日期选择元素
*/
create(element?: HTMLInputElement): HTMLInputElement;
/** 获取日期选择框元素 */
get element(): HTMLInputElement;
/** 获取日期选择框是否启用 */
get enabled(): boolean;
/** 设置日期选择框启用状态 */
set enabled(flag: boolean);
/** 获取设置的日期值,或经过格式化函数返回的值 */
get value(): Date | any;
/** 设置日期值,支持的格式参见 {@linkcode formatDate} */
set value(val: Date | number | string);
/** 获取最小可选择日期 */
get minDate(): string;
/** 设置最小可选择日期 */
set minDate(date: string);
/** 获取最大可选择日期 */
get maxDate(): string;
/** 设置最大可选择日期 */
set maxDate(date: string);
/**
* 日期发生变化时触发的事件
* @param date 日期值,或者经自定义参数中格式化函数格式后的值
* @eventProperty
*/
onDateChanged?: (this: DateSelector, date: Date | any) => void;
}

537
lib/ui/date.js Normal file
View File

@ -0,0 +1,537 @@
import { createElement } from "../functions";
import { isPositive, nullOrEmpty } from "../utility";
/**
* 创建或转换日期选择框
* @param {string} [min] - 最小可选日期
* @param {string} [max] - 最大可选日期
* @param {HTMLInputElement | string} [element] - 转换该元素为日期选择框
* @returns {HTMLInputElement} 返回创建或转换的日期选择框
*/
export function createDateInput(min, max, element) {
let date;
if (typeof element === 'string') {
element = document.querySelector(element);
}
if (element instanceof HTMLInputElement) {
date = element;
date.classList.add('ui-date-cell');
} else {
date = createElement('input', 'ui-date-cell');
}
date.required = true;
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;
}
/**
* 将日期转换为 `yyyy-MM-dd` 格式的字符串
* @param {Date} dt 要转换的日期值
* @param {boolean} [local] 是否视日期为本地时间
* @returns 返回 `yyyy-MM-dd` 格式的字符串
*/
export function toDateValue(dt, local) {
if (isNaN(dt)) {
return '';
}
const year = local ? dt.getFullYear() : dt.getUTCFullYear();
const month = String((local ? dt.getMonth() : dt.getUTCMonth()) + 1).padStart(2, '0');
const date = String(local ? dt.getDate() : dt.getUTCDate()).padStart(2, '0');
return `${year}-${month}-${date}`;
}
/**
* 获取日期格式器
* @param {Date} date - 待格式的日期
* @param {boolean} [utc=true] - 是否按 UTC 时间格式化
* @returns {any} 返回格式化工具对象
*/
export function getFormatter(date, utc) {
const prefix = utc !== false ? 'getUTC' : 'get';
const r = {
/**
* @private
* @returns {number} 返回日数字
*/
j: () => date[`${prefix}Date`](),
/**
* @private
* @returns {string} 返回格式化成两位的日字符串
*/
d: () => String(r.j()).padStart(2, '0'),
/**
* @private
* @returns {number} 返回月数字
*/
n: () => date[`${prefix}Month`]() + 1,
/**
* @private
* @returns {string} 返回格式化成两位的月字符串
*/
m: () => String(r.n()).padStart(2, '0'),
/**
* @private
* @returns {number} 返回年数字
*/
Y: () => date[`${prefix}FullYear`](),
/**
* @private
* @returns {string} 返回年后两位
*/
y: () => String(r.Y()).slice(-2),
/**
* @private
* @returns {number} 返回日期当月总天数
*/
t: () => new Date(r.Y(), r.n(), 0).getDate(),
/**
* @private
* @returns {('1' | '0')} 返回是否为闰年
*/
L() {
const y = r.Y();
return t % 4 === 0 && t % 100 !== 0 || t % 400 === 0 ? 1 : 0;
},
/**
* @private
* @returns {number} 返回小时数字
*/
G: () => date[`${prefix}Hours`](),
/**
* @private
* @returns {number} 返回 12 小时制的数字
*/
g: () => r.G() % 12 || 12,
/**
* @private
* @returns {string} 返回格式化成两位的小时字符串
*/
H: () => String(r.G()).padStart(2, '0'),
/**
* @private
* @returns {string} 返回格式化成两位的 12 小时制的字符串
*/
h: () => String(r.g()).padStart(2, '0'),
/**
* @private
* @returns {string} 返回上下午
*/
A: () => ['AM', 'PM'][r.G() < 12 ? 0 : 1],
/**
* @private
* @returns {string} 返回小写的上下午
*/
a: () => r.A().toLowerCase(),
/**
* @private
* @returns {string} 返回格式化成两位的分钟字符串
*/
i: () => String(date[`${prefix}Minutes`]()).padStart(2, '0'),
/**
* @private
* @returns {string} 返回格式化成两位的秒字符串
*/
s: () => String(date[`${prefix}Seconds`]()).padStart(2, '0'),
/**
* @private
* @returns {string} 返回格式化成六位的毫秒字符串
*/
u: () => String(1e3 * date[`${prefix}Milliseconds`]()).padStart(6, '0'),
/**
* @private
* @returns {string} 返回时区描述字符串
*/
e: () => /\((.*)\)/.exec(String(date))[1] || 'Coordinated Universal Time',
/**
* @private
* @returns {string} 返回时区偏移字符串
*/
O() {
const t = date.getTimezoneOffset();
const r = Math.abs(t);
return (t > 0 ? '-' : '+') + String(100 * Math.floor(r / 60) + r % 60).padStart(4, '0');
},
/**
* @private
* @returns {string} 返回 +08:00 这种格式的时区偏移字符串
*/
P() {
const t = r.O();
return t.substring(0, 3) + ':' + t.substring(3, 5);
}
};
return r;
}
/**
* 格式化日期为 M/d/yyyy 格式的字符串
* @param {Date | number | string} date - 需要格式化的日期值,支持的格式如下:
*
* * `"2024-01-26"`
* * `"2024/1/26"`
* * `"2024-01-26T00:00:00"`
* * `"1/26/2024"`
* * `"638418240000000000"`
* * `new Date('2024-01-26')`
* @param {string} [formatter] - 格式化格式,默认为 `"m/d/Y"`
*
* * Y - 年,例如 `2024`
* * y - 年的后两位,例如 `"24"`
* * n - 月,例如 `2`
* * m - 格式化成两位的月,例如 `"02"`
* * j - 日,例如 `4`
* * d - 格式化成两位的日,例如 `"04"`
* * t - 日期当月总天数,例如 `29`
* * L - 是否为闰年,例如 `1`
* * G - 小时,例如 `15`
* * g - 12 小时制的小时,例如 `3`
* * H - 格式化成两位的小时,例如 `"15"`
* * h - 格式化成两位的 12 小时制的小时,例如 `"03"`
* * A - 上下午,例如 `"PM"`
* * a - 小写的上下午,例如 `"pm"`
* * i - 格式化成两位的分钟,例如 `"39"`
* * s - 格式化成两位的秒,例如 `"20"`
* * u - 格式化成六位的毫秒,例如 `"023040"`
* * e - 时区描述字符串,例如 `"China Standard Time"`
* * O - 时区偏移字符串,例如 `"+0800"`
* * P - `"+08:00"` 这种格式的时区偏移字符串
* @returns {string} 返回格式化后的日期字符串
*/
export function formatDate(date, formatter) {
formatter ??= 'm/d/Y';
if (date === '') {
return '';
}
if (isNaN(date)) {
let e = /^(\d{4})-(\d{2})-(\d{2})/.exec(date);
if (e == null) {
e = /^(\d{4})\/(\d{1,2})\/(\d{1,2})/.exec(date);
}
if (e != null) {
date = new Date(e[1], parseInt(e[2]) - 1, e[3]);
} else {
e = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(date);
if (e != null) {
date = new Date(e[3], parseInt(e[1]) - 1, e[2]);
} else {
return date;
}
}
}
if (date instanceof Date) {
const f = getFormatter(date, false);
return formatter.replace(/\\?(.?)/gi, (k, v) => f[k] ? f[k]() : v);
}
const ticks = Number(date);
if (!isNaN(ticks) && ticks > 0) {
date = new Date((ticks - 621355968e9) / 1e4);
const f = getFormatter(date);
return formatter.replace(/\\?(.?)/gi, (k, v) => f[k] ? f[k]() : v);
}
return date;
}
/**
* 设置显示日期
* @param {HTMLElement} element - 要设置显示日期的元素
* @param {Date | number | string} val - 日期值,支持格式参见 {@linkcode formatDate}
* @param {string} [formatter] - 日期格式化字符串(仅设置显示元素时调用)
*/
export function setDateValue(element, val, 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) {
const month = e[1].padStart(2, '0');
const day = e[2].padStart(2, '0');
const year = e[3];
element.value = `${year}-${month}-${day}`;
} else {
element.value = '';
}
}
} else {
if (val instanceof Date) {
element.value = toDateValue(val, true);
} else {
const ticks = Number(val);
if (!isNaN(ticks) && ticks > 0) {
element.value = toDateValue(new Date((ticks - 621355968e9) / 1e4));
} else {
element.value = '';
}
}
}
} else {
element.innerText = formatDate(val, formatter);
}
}
/**
* 自定义日期格式化回调函数
* @callback DateFormatterCallback
* @param {Date} date - 日期值
* @returns {any} 返回格式化后的结果
*/
/**
* 从日期选择框获取日期值
* @param {HTMLInputElement} element - 要获取的日期选择框
* @param {string | DateFormatterCallback} [formatter] - 自定义格式化字符串或函数,传入参数为 `Date` 类型
* @returns {string | any} 默认返回日期 `ticks` 的字符串
*/
export function getDateValue(element, formatter) {
const date = element?.valueAsDate;
if (date instanceof Date && !isNaN(date)) {
const year = date.getUTCFullYear();
if (year < 1900 || year > 9999) {
return '';
}
if (formatter != null) {
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
// 使外部 formatter 不需要再处理 `getUTCDate` 亦或是 `getDate`
const localDate = new Date(`${year}-${month}-${day}T00:00:00`);
if (typeof formatter === 'function') {
return formatter(localDate);
}
return formatDate(localDate, formatter);
}
return String(date.getTime() * 1e4 + 621355968e9);
}
return '';
}
/**
* 日期选择框类
* @class
*/
export class DateSelector {
_var = {
/**
* @type {HTMLInputElement}
* @private
*/
el: null,
options: {
/**
* @type {boolean?}
* @private
*/
enabled: true,
/**
* @type {string?}
* @private
*/
minDate: null,
/**
* @type {string?}
* @private
*/
maxDate: null,
/**
* @type {number?}
* @private
*/
tabIndex: null,
/**
* @type {string?}
* @private
*/
className: null,
/**
* @type {DateFormatterCallback?}
* @private
*/
valueFormatter: null
}
};
/**
* 日期发生变化时触发的事件
* @event
* @param {Date} date - 修改后的日期值
* @this DateSelector
*/
onDateChanged;
/**
* 日期选择框构造函数
* @constructor
* @param {object} [opts] - 日期选择框初始化参数
* @param {boolean} [opts.enabled] - 是否可用
* @param {string} [opts.minDate] - 最小可选择日期
* @param {string} [opts.maxDate] - 最大可选择日期
* @param {DateFormatterCallback} [opts.valueFormatter] - 自定义格式化函数
* @example <caption>创建一个 DateSelector 并插入到元素中</caption>
* <div id="dateFrom"></div>
* @example
* // 构造参数
* const opts = {
* minDate: '2020-01-01',
* maxDate: '2024-02-01',
* valueFormatter: (date) => {
* return `${date.getUTCFullYear()}/${date.getUTCMonth() + 1}/${date.getUTCDate()}`; // 2024/1/31
* }
* };
* const dateSelectorFrom = new DateSelector(opts);
* document.querySelector('#dateFrom').appendChild(dateSelectorFrom.create());
* @example <caption>将一个元素转化为 DateSelector</caption>
* <input id="dateTo"></div>
* @example
* const dateSelectorTo = new DateSelector();
* dateSelectorTo.create('#dateTo');
*/
constructor(opts) {
this._var.options = opts ?? {};
}
/**
* 创建或转换日期选择框元素
* @param {HTMLInputElement | string} [element] - 转换该元素为日期选择框
* @returns {HTMLInputElement} 返回创建或转换的日期选择元素
*/
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;
}
el.addEventListener('blur', e => {
const date = this._getDate(e.target.valueAsDate);
if (date == null) {
e.target.value = '';
}
if (typeof this.onDateChanged === 'function') {
this.onDateChanged(date);
}
});
this._var.el = el;
return el;
}
/**
* 获取日期选择框元素
* @readonly
* @type {HTMLInputElement}
*/
get element() { return this._var.el }
/**
* 获取或设置日期选择框是否启用
* @type {boolean}
*/
get enabled() { return !this._var.el.disabled }
set enabled(flag) {
this._var.el.disabled = flag === false;
}
/**
* 获取格式化的日期值,或设置日期值,支持的格式参见 {@linkcode formatDate}
* @type {string | any}
*/
get value() { return this._getDate(this._var.el.valueAsDate) }
set value(val) {
setDateValue(this._var.el, val);
}
/**
* 获取或设置最小可选择日期
* @type {string}
*/
get minDate() { return this._var.el.min }
set minDate(date) {
this._var.el.min = date;
this._var.options.minDate = date;
}
/**
* 获取或设置最大可选择日期
* @type {string}
*/
get maxDate() { return this._var.el.max }
set maxDate(date) {
this._var.el.max = date;
this._var.options.maxDate = date;
}
/**
* @private
* @param {Date} date
* @returns {Date | any}
*/
_getDate(date) {
if (date instanceof Date && !isNaN(date)) {
const year = date.getUTCFullYear();
if (year < 1900 || year > 9999) {
return null;
}
if (typeof this._var.options.valueFormatter === 'function') {
return this._var.options.valueFormatter(date);
}
return date;
}
return null;
}
/**
* 把父容器下所有匹配 `input[data-type="date"]` 的元素修改为统一的日期选择框<br/><br/>
* 解析的属性为 `id`, `class`, `data-min`, `data-max`, `disabled`
* @static
* @param {HTMLElement} [dom=document.body] 父元素
* @param {Function} [trigger] 日期设置事件触发函数
* @param {Date} trigger.{this} - 上下文为触发设置日期的 `DateSelector` 实例
* @param {Date} trigger.date - 修改后的日期值
* @example <caption>HTML</caption>
* <input id="dateFrom" data-type="date" data-min="1980-01-01"/>
* @example <caption>解析父容器</caption>
* // 解析 document.body 下所有符合条件的元素,转换为日期选择框,第二个参数可选
* DateSelector.resolve(document.body, function (date) {
* console.log(`element(#${this.element.id}), date changed to: ${formatDate(date)}`);
* // 当日期选择改变时控制台将会输出element(#dateFrom), date changed to: 1/30/2024
* });
* @example <caption>其他地方调用时</caption>
* const value = document.querySelector('#dateFrom').value;
* console.log(`dateFrom.value = '${value}', formatted: '${formatDate(value)}'`);
* // 控制台会输出dateFrom.value = '2024-01-30', formatted: '1/30/2024'
*/
static resolve(dom = document.body, trigger) {
const dates = dom.querySelectorAll('input[data-type="date"]');
for (let dat of dates) {
const val = dat.value;
const dateSelector = new DateSelector({
minDate: dat.getAttribute('data-min'),
maxDate: dat.getAttribute('data-max')
});
if (typeof trigger === 'function') {
dateSelector.onDateChanged = date => trigger.call(dateSelector, date);
}
dat.removeAttribute('data-type');
dat.removeAttribute('data-min');
dat.removeAttribute('data-max');
dateSelector.create(dat);
dateSelector.value = val;
}
}
}

147
lib/ui/dropdown.d.ts vendored
View File

@ -1,51 +1,132 @@
interface DropdownItem {
/** 下拉项接口 */
export interface DropdownItem {
/** 值 */
value: string;
/** 显示文本 */
text: string;
html?: HTMLElement
/** 源码显示内容 */
html?: HTMLElement | string;
}
/** 下拉框选项接口 */
export interface DropdownOptions {
textkey?: string;
valuekey?: string;
htmlkey?: string;
maxlength?: Number;
multiselect?: boolean;
/** 文本关键字,默认值 `text` */
textKey?: string;
/** 值关键字,默认值 `value` */
valueKey?: string;
/** 源码显示的关键字,默认值 `html` */
htmlKey?: string;
/** 源码显示的模板函数 */
htmlTemplate?: (item: DropdownItem) => HTMLElement;
/** 最大输入长度,默认值 `500` */
maxLength?: number;
/** 是否允许多选 */
multiSelect?: boolean;
/** 选中值 */
selected?: string;
selectedlist?: Array<string>;
/** 选中的数组 */
selectedList?: Array<string>;
/** 是否禁用 */
disabled?: boolean;
/** 是否支持输入 */
input?: boolean;
/** 是否支持搜索 */
search?: boolean;
searchkeys?: Array<string>;
searchplaceholder?: string;
tabindex?: Number;
/** 搜索的关键字数组 */
searchKeys?: Array<string>;
/** 搜索提示文本,默认值取语言资源 `searchHolder` "Search..." */
searchPlaceholder?: string;
/** 焦点索引 */
tabIndex?: number;
/** 输入框的提示文本 */
placeholder?: string;
slidefixed?: boolean;
parent?: HTMLElement;
/** 是否固定为向下展开 */
slideFixed?: boolean;
/** 父元素,默认添加到头元素之后 */
wrapper?: HTMLElement;
}
interface Dropdown {
sourceFilter: () => Array<DropdownItem | any>;
onselected: (item: DropdownItem | any) => void;
onselectedlist: (list: Array<DropdownItem | any>) => void;
onexpanded: () => void;
/** 下拉框类 */
export class Dropdown {
/**
* 把父元素下的所有 `select` 元素修改为统一下拉框组件<br/><br/>
* 解析的属性为 `value`, `disabled`, `tabIndex`
* @param dom 父元素
* @param trigger 选中事件触发函数(上下文为触发选中的 `Dropdown` 实例)
* @returns 返回该父元素
*/
static resolve(dom?: HTMLElement, trigger?: (item: DropdownItem) => void): HTMLElement;
/**
* 下拉框的构造函数
* @param options 下拉框选项
*/
constructor(options?: DropdownOptions);
/** 根据该函数返回数据源 */
sourceFilter: () => Array<DropdownItem>;
/**
* 选中时触发
* @param item 选中的条目
* @eventProperty
*/
onSelected: (item: DropdownItem) => void;
/**
* 选中多个时触发
* @param list 选中的条目数组
* @eventProperty
*/
onSelectedList: (list: Array<DropdownItem>) => void;
/**
* 下拉框展开时触发
* @eventProperty
*/
onExpanded: () => void;
/**
* 下拉框收缩时触发
* @eventProperty
*/
onCollapsed: () => void;
/** 获取下拉框是否禁用 */
get disabled(): boolean;
/**
* 设置下拉框禁用状态
* @param flag 禁用状态
*/
set disabled(flag: boolean);
get source(): Array<DropdownItem | any>;
set source(list: Array<DropdownItem | any>);
readonly multiselect: boolean;
readonly selected: DropdownItem | any;
readonly selectedlist: Array<DropdownItem | any>;
/** 获取数据源 */
get source(): Array<DropdownItem>;
/**
* 设置数据源
* @param list 数据源
*/
set source(list: Array<DropdownItem>);
/** 获取是否允许多选 */
get multiSelect(): boolean;
/** 获取选中的条目 */
get selected(): DropdownItem;
/** 获取选中的条目列表 */
get selectedList(): Array<DropdownItem>;
/**
* 创建下拉框元素
* @returns 返回创建的下拉框元素
*/
create(): HTMLElement;
select(selected: string, silence?: boolean): void;
/**
* 选中某个条目
* @param selected 选中的值
* @param silence 是否静默选中,即不触发 {@linkcode onSelected} 事件
* @param ignoreCase 是否不区分大小写
* @returns 是否选中
*/
select(selected: string, silence?: boolean, ignoreCase?: boolean): boolean | undefined;
/**
* 选中条目列表
* @param selectedlist 选中的值的列表
* @param silence 是否静默选中,即不触发 {@linkcode onSelected} 事件
*/
selectlist(selectedlist: Array<string>, silence?: boolean): void;
}
declare var Dropdown: {
prototype: Dropdown;
new(options?: DropdownOptions): Dropdown;
resolve(dom?: HTMLElement): HTMLElement;
}
export default Dropdown;

View File

@ -1,304 +0,0 @@
<div>
<h1>dropdown</h1>
<hr />
<p>
创建一个统一样式的下拉输入、选择框元素,或者解析转换页面上的 select 标签为该元素。
</p>
<h2>constructor</h2>
<code>new(options?: DropdownOptions): Dropdown</code>
<h3>options?: DropdownOptions</h3>
<p>
下拉输入、选择框的初始参数,结构为
<pre>interface DropdownOptions {
textkey?: string;
valuekey?: string;
htmlkey?: string;
maxlength?: Number;
multiselect?: boolean;
selected?: string;
selectedlist?: Array&lt;DropdownItem | any&gt;;
disabled?: boolean;
input?: boolean;
search?: boolean;
searchkeys?: Array&lt;string&gt;;
searchplaceholder?: string;
tabindex?: Number;
placeholder?: string;
slidefixed?: boolean;
parent?: HTMLElement;
}</pre>
</p>
<h4>textkey?: string</h4>
<p>
数据源中用以显示的属性,默认为 <code>text</code>
</p>
<h4>valuekey?: string</h4>
<p>
数据源中作为值的属性,默认为 <code>value</code>
</p>
<h4>htmlkey?: string</h4>
<p>
数据源中用来以 html 方式呈现的属性,默认为 <code>html</code>
</p>
<h4>maxlength?: Number</h4>
<p>
作为输入类型时的最大允许字符数,默认为 500
</p>
<h4>multiselect?: boolean</h4>
<p>
是否允许多选,仅在选择类型下有效
</p>
<h4>selected?: string</h4>
<p>
默认选中的项目的值
</p>
<h4>selectedlist?: Array&lt;DropdownItem | any&gt;</h4>
<p>
多选时默认选中的项目的值的列表
</p>
<h4>disabled?: boolean</h4>
<p>
初始化时下拉框是否禁用
</p>
<h4>input?: boolean</h4>
<p>
是否为输入类型
</p>
<h4>search?: boolean</h4>
<p>
是否允许搜索
</p>
<h4>searchkeys?: Array&lt;string&gt;</h4>
<p>
搜索时搜索的数据源属性的列表,默认为 <code>[valuekey]</code>
</p>
<h4>searchplaceholder?: string</h4>
<p>
搜索输入框的占位字符串
</p>
<h4>tabindex?: Number</h4>
<p>
下拉框的焦点顺序
</p>
<h4>placeholder?: string</h4>
<p>
作为输入类型时,输入框的占位字符串
</p>
<h4>slidefixed?: boolean</h4>
<p>
下拉方向是否固定为下
</p>
<h4>parent?: HTMLElement</h4>
<p>
下拉列表呈现的父容器,默认为 <code>document.body</code>
</p>
<h2>Dropdown.resolve</h2>
<code>static resolve(dom?: HTMLElement): HTMLElement</code>
<h3>dom?: HTMLElement</h3>
<p>
将把此 HTML 元素,为 null 则把 document.body 下的所有 <code>select</code> 元素解析为统一样式的下拉框
</p>
<hr />
<h2>sourceFilter</h2>
<code>sourceFilter: () => Array&lt;DropdownItem | any&gt;</code>
<p>
数据源代理,返回用以呈现在下拉列表中的数据源
</p>
<h2>onselected</h2>
<code>onselected: (item: DropdownItem | any) => void</code>
<h3>item: DropdownItem | any</h3>
<p>
选中项发生改变时触发该事件,参数为选中的项
</p>
<h2>onselectedlist</h2>
<code>onselectedlist: (list: Array&lt;DropdownItem | any&gt;) => void</code>
<h3>list: Array&lt;DropdownItem | any&gt;</h3>
<p>
多选时选中列表发生改变时触发该事件,参数为选中的列表
</p>
<h2>onexpanded</h2>
<code>onexpanded: () => void</code>
<p>
下拉列表展开时触发该事件,一般用来异步获取服务器数据,填充至数据源
</p>
<hr />
<h2>disabled</h2>
<code>property disabled(): boolean</code>
<p>
获取或设置下拉框是否禁用
</p>
<h2>source</h2>
<code>property source(): Array&lt;DropdownItem | any&gt;</code>
<p>
获取或设置下拉框数据源
</p>
<h2>multiselect</h2>
<code>readonly multiselect: boolean</code>
<p>
获取下拉框是否支持多选
</p>
<h2>selected</h2>
<code>readonly selected: DropdownItem | any</code>
<p>
获取下拉框当前选中项
</p>
<h2>selectedlist</h2>
<code>readonly selectedlist: Array&lt;DropdownItem | any&gt;</code>
<p>
获取下拉框当前选中的列表
</p>
<hr />
<h2>create</h2>
<code>create(): HTMLElement</code>
<p>
创建下拉列表,返回一个 HTML 元素
</p>
<h2>select</h2>
<code>select(selected: string, silence?: boolean): void</code>
<h3>selected: string</h3>
<p>
修改下拉框的选中项为参数值对应的项
</p>
<h3>silence?: boolean</h3>
<p>
是否静默修改,为 true 时不触发 <code>onselected</code> 事件
</p>
<h2>selectlist</h2>
<code>selectlist(selectedlist: Array&lt;string&gt;, silence?: boolean): void</code>
<h3>selectedlist: Array&lt;string&gt;</h3>
<p>
修改下拉框的选中列表为参数值列表对应的项的列表
</p>
<h3>silence?: boolean</h3>
<p>
是否静默修改,为 true 时不触发 <code>onselectedlist</code> 事件
</p>
<hr />
<h2>示例</h2>
<pre>&lt;div id="dropdown-sample"&gt;
&lt;select&gt;
&lt;option value="cs1"&gt;Case 1&lt;/option&gt;
&lt;option value="cs2" selected&gt;Case 2&lt;/option&gt;
&lt;option value="cs3"&gt;Case 3&lt;/option&gt;
&lt;/select&gt;
&lt;/div&gt;
&lt;script type="text/javascript"&gt;
const Dropdown = window["lib-ui"].Dropdown;
const sample = document.querySelector('#dropdown-sample');
// 解析 select 元素
Dropdown.resolve(sample);
// 创建简易输入类型下拉框
let drop = new Dropdown({
input: true,
// selected: 'standby',
placeholder: 'asset status',
slidefixed: true
});
drop.source = ['off', 'running', 'standby', 'broken']
.map(it => { return { value: it, text: it } });
sample.appendChild(drop.create());
// 创建自定义显示元素的下拉框
drop = new Dropdown({
selected: '#ff0',
search: true,
// multiselect: true
});
drop.source = [
{ value: '#fff', text: 'White' },
{ value: '#f00', text: 'Red' },
{ value: '#0f0', text: 'Green' },
{ value: '#00f', text: 'Blue' },
{ value: '#ff0', text: 'Yellow' },
{ value: '#0ff', text: 'Cyan' },
{ value: '#f0f', text: 'Magenta' },
];
drop.source.forEach(it => {
const span = document.createElement('span');
span.className = 'ui-drop-item';
span.style.setProperty('--color', it.value);
span.innerText = it.text;
it.html = span;
});
sample.appendChild(drop.create());
&lt;/script&gt;</pre>
<div id="dropdown-sample">
<select>
<option value="cs1">Case 1</option>
<option value="cs2" selected>Case 2</option>
<option value="cs3">Case 3</option>
</select>
</div>
<div style="height: 80px"></div>
<script type="text/javascript">
!function () {
const Dropdown = window["lib-ui"].Dropdown;
const sample = document.querySelector('#dropdown-sample');
Dropdown.resolve(sample);
let drop = new Dropdown({
input: true,
// selected: 'standby',
placeholder: 'asset status',
slidefixed: true
});
drop.source = ['off', 'running', 'standby', 'broken']
.map(it => { return { value: it, text: it } });
sample.appendChild(drop.create());
drop = new Dropdown({
selected: '#ff0',
search: true,
// multiselect: true
});
drop.source = [
{ value: '#fff', text: 'White' },
{ value: '#f00', text: 'Red' },
{ value: '#0f0', text: 'Green' },
{ value: '#00f', text: 'Blue' },
{ value: '#ff0', text: 'Yellow' },
{ value: '#0ff', text: 'Cyan' },
{ value: '#f0f', text: 'Magenta' },
];
drop.source.forEach(it => {
const span = document.createElement('span');
span.className = 'ui-drop-item';
span.style.setProperty('--color', it.value);
span.innerText = it.text;
it.html = span;
});
sample.appendChild(drop.create());
}();
</script>
<style type="text/css">
#dropdown-sample {
display: flex;
}
#dropdown-sample>.ui-drop-wrapper {
width: 200px;
margin-right: 10px;
}
.ui-drop-item {
font-size: .75rem !important;
padding: 0 0 0 22px !important;
position: relative;
}
.ui-drop-item::before {
content: '';
position: absolute;
width: 12px;
height: 12px;
border-radius: 6px;
top: calc(50% - 6px);
left: 6px;
background-color: var(--color);
}
</style>
</div>

View File

@ -1,16 +1,15 @@
// 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"
const SymbolDropdown = Symbol.for('ui-dropdown');
const DropdownTitleHeight = 26;
const DropdownItemHeight = 30;
let r = lang;
let dropdownGlobal = global[SymbolDropdown];
if (dropdownGlobal == null) {
@ -21,18 +20,20 @@ if (dropdownGlobal == null) {
configurable: false,
enumerable: false,
value: function () {
const panel = document.querySelector('.ui-drop-wrapper .ui-drop-box.active');
if (panel == null) {
return;
}
panel.classList.remove('active');
const dropId = panel.parentElement.dataset.dropId;
if (dropId == null) {
return;
}
const dropdown = this[dropId];
if (dropdown?.multiselect && typeof dropdown.oncollapsed === 'function') {
dropdown.oncollapsed();
const panels = document.querySelectorAll('.ui-drop-box.active');
for (let panel of [...panels]) {
if (panel == null) {
continue;
}
panel.classList.remove('active');
const dropId = panel.parentElement.dataset.dropId;
if (dropId == null) {
continue;
}
const dropdown = this[dropId];
if (dropdown?.multiSelect && typeof dropdown.onCollapsed === 'function') {
dropdown.onCollapsed();
}
}
}
})
@ -51,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 {
@ -81,42 +87,86 @@ function filterSource(searchkeys, textkey, key, source) {
return source;
}
class Dropdown {
#options;
function getValue(it, valuekey, textkey) {
if (it == null) {
return null;
}
const value = it[valuekey];
if (value == null || value === '') {
return it[textkey];
}
return value;
}
#wrapper;
#container;
#label;
/**
* 下拉列表参数对象
* @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] - 选中值
* @property {string[]} [selectedList] - 选中的数组
* @property {boolean} [disabled] - 是否禁用
* @property {boolean} [input] - 是否支持输入
* @property {boolean} [search] - 是否支持搜索
* @property {string[]} [searchKeys] - 搜索的关键字数组
* @property {string} [searchPlaceholder] - 搜索提示文本,默认值取语言资源 `searchHolder` "Search..."
* @property {number} [tabIndex] - 焦点索引
* @property {string} [placeholder] - 输入框的提示文本
* @property {boolean} [slideFixed] - 是否固定为向下展开
* @property {HTMLElement} [wrapper] - 父元素,默认添加到头元素之后
*/
#allChecked;
#source;
#lastSelected;
#selected;
#selectedList;
export class Dropdown {
_var = {};
// _var.options;
// _var.wrapper;
// _var.container;
// _var.label;
// _var.allChecked;
// _var.source;
// _var.lastSelected;
// _var.selected;
// _var.selectedList;
sourceFilter;
onselectedlist;
onselected;
onexpanded;
onSelectedList;
onSelected;
onExpanded;
onCollapsed;
constructor(options = {}) {
options.searchplaceholder ??= r('searchHolder', 'Search...');
options.textkey ??= 'text';
options.valuekey ??= 'value';
options.htmlkey ??= 'html';
options.maxlength ??= 500;
this.#options = options;
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() {
const options = this.#options;
const options = this._var.options;
// 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.#wrapper = wrapper;
this._var.wrapper = wrapper;
// header
const header = createElement('div', 'ui-drop-header');
@ -131,8 +181,9 @@ class Dropdown {
if (up || down) {
const source = this.source;
const count = source.length;
const valuekey = this.#options.valuekey;
let index = source?.indexOf(this.#selected);
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;
} else if (index >= count) {
@ -153,26 +204,26 @@ class Dropdown {
index = count - 1;
}
}
const target = source[index]?.[valuekey];
const target = getValue(source[index], valuekey, textkey);
if (target != null) {
this.select(target);
}
} else if (e.key === 'Tab') {
this.#dropdown(false);
this._dropdown(false);
}
});
header.addEventListener('click', () => {
if (this.disabled) {
return;
}
const active = this.#expanded;
const label = this.#label;
const active = this._expanded;
const label = this._var.label;
if (active && label.ownerDocument.activeElement === label) {
return;
}
this.#dropdown(!active);
if (!active && typeof this.onexpanded === 'function') {
setTimeout(() => this.onexpanded(), 120);
this._dropdown(!active);
if (!active && typeof this.onExpanded === 'function') {
setTimeout(() => this.onExpanded(), 120);
}
});
@ -180,28 +231,30 @@ 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);
isPositive(options.maxLength) && label.setAttribute('maxlength', options.maxLength);
isPositive(options.tabIndex) && label.setAttribute('tabindex', options.tabIndex);
label.addEventListener('input', e => {
const key = e.target.value.toLowerCase();
const source = filterSource(options.searchkeys, options.textkey, key, this.source);
this.#filllist(source);
this.#container.classList.add('active');
const source = filterSource(options.searchKeys, options.textKey, key, this.source);
this._filllist(source);
this._var.container.classList.add('active');
});
label.addEventListener('blur', e => this.select(e.target.value));
label.addEventListener('mousedown', e => this.#expanded && e.stopPropagation());
label.addEventListener('mousedown', e => this._expanded && e.stopPropagation());
} else {
isPositive(options.tabindex) && header.setAttribute('tabindex', options.tabindex);
isPositive(options.tabIndex) && header.setAttribute('tabindex', options.tabIndex);
label = createElement('label', 'ui-drop-text');
}
this.#label = label;
if (options.multiselect) {
if (Array.isArray(options.selectedlist)) {
this.selectlist(options.selectedlist, true);
this._var.label = label;
if (options.multiSelect) {
if (Array.isArray(options.selectedList)) {
this.selectlist(options.selectedList, true);
} else {
this.#allChecked = true;
this._var.allChecked = true;
label.innerText = r('allItem', '( All )');
}
} else if (options.selected != null) {
@ -214,23 +267,25 @@ class Dropdown {
return wrapper;
}
get multiselect() { return this.#options.multiselect }
get multiSelect() { return this._var.options.multiSelect }
get disabled() { return this.#wrapper == null || this.#wrapper.querySelector('.ui-drop-header.disabled') != null }
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) {
if (this.#wrapper == null) {
if (this._var.wrapper == null) {
return;
}
if (flag) {
this.#wrapper.querySelector('.ui-drop-header').classList.add('disabled');
this._var.wrapper.querySelector('.ui-drop-header').classList.add('disabled');
} else {
this.#wrapper.querySelector('.ui-drop-header').classList.remove('disabled');
this._var.wrapper.querySelector('.ui-drop-header').classList.remove('disabled');
}
}
get source() {
let source = this.#source;
let source = this._var.source;
if (source == null || !Array.isArray(source)) {
if (typeof this.sourceFilter === 'function') {
source = this.sourceFilter();
@ -238,7 +293,7 @@ class Dropdown {
if (!Array.isArray(source)) {
source = [];
}
this.#source = source;
this._var.source = source;
}
return source;
}
@ -247,113 +302,148 @@ class Dropdown {
if (!Array.isArray(list)) {
return;
}
this.#source = list;
if (this.#expanded) {
setTimeout(() => this.#dropdown(), 120);
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);
}
}
get selected() { return this.#selected }
get selected() { return this._var.selected }
get selectedlist() { return this.#selectedList || [] }
get selectedList() { return this._var.selectedList || [] }
select(selected, silence) {
if (this.#lastSelected === selected) {
return false;
select(selected, silence, ignoreCase) {
if (typeof selected !== 'string') {
selected = String(selected);
}
this.#lastSelected = selected;
const valuekey = this.#options.valuekey;
const textkey = this.#options.textkey;
const htmlkey = this.#options.htmlkey;
let item = this.source.find(it => it[valuekey] === selected);
if (this.#options.input) {
if (ignoreCase) {
selected = selected.toLowerCase();
}
if (this._var.lastSelected === selected) {
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 => (ignoreCase ? String(getValue(it, valuekey, textkey)).toLowerCase() : String(getValue(it, valuekey, textkey))) === selected);
if (this._var.options.input) {
if (item == null) {
item = { [valuekey]: selected };
}
this.#label.value = selected;
this._var.label.value = selected;
} else {
const expanded = this.#expanded;
const expanded = this._expanded;
if (expanded) {
this.#container.querySelectorAll('li[data-value].selected').forEach(li => li.classList.remove('selected'));
this._var.container.querySelectorAll('li[data-value].selected').forEach(li => li.classList.remove('selected'));
}
if (item == null) {
this.#selected = null;
this.#label.innerText = ' ';
this._var.selected = null;
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.#label.replaceChildren(html.cloneNode(true));
this._var.label.replaceChildren(html.cloneNode(true));
} else if (typeof html === 'string') {
this._var.label.innerHTML = html;
} else {
let text = item[textkey];
if (nullOrEmpty(text)) {
text = ' ';
}
this.#label.innerText = text;
this._var.label.innerText = text;
}
if (expanded) {
const val = selected.replace(/"/g, '\\"');
const li = this.#container.querySelector(`li[data-value="${val}"]`);
if (li != null) {
li.classList.add('selected');
for (let li of this._var.container.querySelectorAll('li[data-value]')) {
if ((ignoreCase ? li.dataset.value.toLowerCase() : li.dataset.value) === selected) {
li.classList.add('selected');
break;
}
}
// const val = selected.replace(/"/g, '\\"');
// const li = this._var.container.querySelector(`li[data-value="${val}"]`);
// if (li != null) {
// li.classList.add('selected');
// }
}
}
this.#selected = item;
if (!silence && typeof this.onselected === 'function') {
this.onselected(item);
this._var.selected = item;
if (!silence && typeof this.onSelected === 'function') {
this.onSelected(item);
}
return true;
}
selectlist(selectedlist, silence) {
const source = this.source;
const valuekey = this.#options.valuekey;
const textkey = this.#options.textkey;
const htmlkey = this.#options.htmlkey;
const itemlist = selectedlist.map(v => {
let item = source.find(it => it[valuekey] === v);
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(getValue(it, valuekey, textkey)) === v);
if (item == null) {
item = { [valuekey]: v, [textkey]: v };
item = {
[valuekey]: v,
[textkey]: v
};
}
return item;
});
if (itemlist.length === 0) {
this.#selectedList = null;
this.#label.innerText = none;
this._var.selectedList = null;
this._var.label.innerText = r('none', '( None )');
return false;
}
selectItems(this.#label, itemlist, htmlkey, textkey);
this.#selectedList = itemlist;
if (!silence && typeof this.onselectedlist === 'function') {
this.onselectedlist(itemlist);
selectItems(this._var.label, itemlist, template, htmlkey, textkey);
this._var.selectedList = itemlist;
if (!silence && typeof this.onSelectedList === 'function') {
this.onSelectedList(itemlist);
}
}
get #expanded() { return this.#container?.classList?.contains('active') }
get _expanded() { return this._var.container?.classList?.contains('active') }
#dropdown(flag = true) {
const options = this.#options;
let panel = this.#container;
_dropdown(flag = true) {
const options = this._var.options;
let panel = this._var.container;
if (panel == null) {
panel = createElement('div', 'ui-drop-box');
// search box
if (!options.input && options.search) {
const search = createElement('div', 'ui-drop-search');
const input = createElement('input');
input.setAttribute('type', 'text');
isPositive(options.tabindex) && input.setAttribute('tabindex', options.tabindex);
!nullOrEmpty(options.searchplaceholder) && input.setAttribute('placeholder', options.searchplaceholder);
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 => {
const key = e.target.value.toLowerCase();
const source = filterSource(options.searchkeys, options.textkey, key, this.source);
this.#filllist(source);
const source = filterSource(options.searchKeys, options.textKey, key, this.source);
this._filllist(source);
})
search.append(input, createIcon('fa-light', 'search'));
panel.appendChild(search);
}
// list
const list = createElement('ul', 'ui-drop-list');
if (!this.multiselect) {
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;
while (li.tagName !== 'LI') {
@ -369,149 +459,291 @@ class Dropdown {
});
}
panel.appendChild(list);
this.#container = panel;
this.#wrapper.appendChild(panel);
this._var.container = panel;
if (options.wrapper instanceof HTMLElement) {
options.wrapper.appendChild(panel);
} else {
this._var.wrapper.appendChild(panel);
}
}
if (flag) {
let source = this.source;
if (!options.input && options.search) {
const search = panel.querySelector('.ui-drop-search > input');
if (!nullOrEmpty(search?.value)) {
source = filterSource(options.searchkeys, options.textkey, search.value, source);
source = filterSource(options.searchKeys, options.textKey, search.value, source);
}
}
this.#filllist(source);
this._filllist(source);
// slide direction
if (!options.slidefixed) {
let parent = options.parent ?? document.body;
let p = this.#wrapper;
let top = p.offsetTop;
while ((p = p.parentElement) != null && p !== parent) {
top -= p.scrollTop;
if (!options.slideFixed) {
const parent = options.wrapper ?? document.body;
let p = this._var.wrapper;
panel.style.minWidth = `${p.offsetWidth}px`;
const headerHeight = p.offsetHeight;
let top = p.offsetTop + headerHeight;
let left = p.offsetLeft;
if (p !== parent) {
while ((p = p.parentElement) != null && p !== parent) {
top -= p.scrollTop;
left -= p.scrollLeft;
}
}
if (top - parent.offsetTop + DropdownTitleHeight + panel.offsetHeight >= parent.offsetHeight) {
p = this._var.wrapper;
if (p !== parent) {
while ((p = p.offsetParent) != null && p !== parent) {
top += p.offsetTop;
left += p.offsetLeft;
}
}
const slideUp = top - parent.scrollTop + panel.offsetHeight >= parent.offsetHeight;
if (options.wrapper instanceof HTMLElement) {
if (slideUp) {
panel.style.top = '';
panel.style.bottom = `${parent.offsetHeight - top + headerHeight - 4}px`;
} else {
panel.style.top = `${top}px`;
panel.style.bottom = '';
}
panel.style.left = `${left}px`;
}
if (slideUp) {
panel.classList.add('slide-up');
} else {
panel.classList.remove('slide-up');
}
}
panel.classList.add('active');
this._var.dropTop = 0;
panel.querySelector('.ui-drop-list').dispatchEvent(new Event('scroll'));
} else {
panel.classList.remove('active');
}
}
#filllist(source) {
const list = this.#container.querySelector('.ui-drop-list');
_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.#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,
customerAttributes: { 'isall': '1' },
onchange: e => this.#triggerselect(e.target)
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 valuekey = this.#options.valuekey;
const textkey = this.#options.textkey;
const htmlkey = this.#options.htmlkey;
const selected = this.selected;
const selectedlist = this.selectedlist;
let scrolled;
source.slice(0, 200).forEach((item, i) => {
const val = item[valuekey];
const li = createElement('li');
li.dataset.value = val;
li.setAttribute('title', item[textkey]);
let label;
const html = item[htmlkey];
if (html instanceof HTMLElement) {
label = html;
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;
let scrolled;
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.title = item[textkey];
if (item.__level > 0) {
li.style.marginLeft = `${item.__level * 24}px`;
}
const wrapper = createElement('span', 'li-wrapper',
createElement('span', span => {
// events
span.className = 'ui-expandor';
},
createIcon('fa-light', 'caret-down')
)
);
li.appendChild(wrapper);
let label;
let 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') {
label = createElement('span');
label.innerHTML = html;
}
if (multiselect) {
const selected = selectedlist.some(s => s[valuekey] === val);
if (label == null) {
label = createElement('span');
label.innerText = item[textkey];
}
const box = createCheckbox({
label,
checked: allchecked || selected,
customerAttributes: {
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];
}
if (selected != null && selected[valuekey] === val) {
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.#options.valuekey;
const textkey = this.#options.textkey;
const htmlkey = this.#options.htmlkey;
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.#allChecked = checkbox.checked;
const boxes = this.#container.querySelectorAll('input.dataitem');
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.#container.querySelectorAll('input.dataitem:not(:checked)').length === 0) {
this.#allChecked = true;
this.#container.querySelector('input[isall="1"]').checked = true;
list = [];
} else {
} else {
item.__checked = checkbox.indeterminate ? 2 : checkbox.checked ? 1 : 0;
const all = this._var.container.querySelector('input[isall="1"]');
if (checkbox.checked) {
const source = this.source;
list = [...this.#container.querySelectorAll('input.dataitem:checked')]
.map(c => source.find(it => it[valuekey] === c.dataset.value))
.filter(it => it != null);
}
} else {
const val = checkbox.dataset.value;
if (this.#allChecked) {
this.#allChecked = false;
this.#container.querySelector('input[isall="1"]').checked = false;
list = this.source.filter(it => it[valuekey] !== val);
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 => 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.#allChecked) {
this.#label.innerText = r('allItem', '( All )');
if (this._var.allChecked) {
this._var.label.innerText = r('allItem', '( All )');
} else {
selectItems(this.#label, list, htmlkey, textkey);
selectItems(this._var.label, list, template, htmlkey, textkey);
}
this.#selectedList = list;
if (typeof this.onselectedlist === 'function') {
this.onselectedlist(itemlist);
this._var.selectedList = list;
if (typeof this.onSelectedList === 'function') {
this.onSelectedList(itemlist);
}
}
static resolve(dom = document.body) {
static resolve(dom = document.body, trigger) {
const selects = dom.querySelectorAll('select');
for (let sel of selects) {
const source = [...sel.children].map(it => {
@ -520,13 +752,14 @@ class Dropdown {
const drop = new Dropdown({
selected: sel.value,
disabled: sel.disabled,
tabindex: sel.tabIndex
tabIndex: sel.tabIndex
});
drop.source = source;
if (typeof trigger === 'function') {
drop.onSelected = item => trigger.call(drop, item);
}
sel.parentElement.replaceChild(drop.create(), sel);
}
return dom;
}
}
export default Dropdown;
}

2
lib/ui/extension.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
export function validation(element: HTMLElement, regex: RegExp): HTMLElement
export function convertCssStyle(style: Object): string

16
lib/ui/extension.js Normal file
View File

@ -0,0 +1,16 @@
export function validation(element, regex) {
if (element instanceof HTMLElement && regex instanceof RegExp) {
element.addEventListener('change', e => {
if (regex.test(e.target.value)) {
e.target.classList.remove('validation-error');
} else {
e.target.classList.add('validation-error');
}
})
}
return element;
}
export function convertCssStyle(style) {
return Object.entries(style).map(s => `${s[0]}: ${s[1]}`).join('; ');
}

View File

@ -1,44 +1,245 @@
import { global } from "../../utility";
import { nullOrEmpty } from "../../utility/strings";
import { createElement } from "../../functions";
import { createIcon } from "../icon";
import { createCheckbox } from "../checkbox";
import { setTooltip } from "../tooltip";
import Dropdown from "../dropdown";
import { createCheckbox, createRadiobox } from "../checkbox";
// import { setTooltip } from "../tooltip";
import { Dropdown } from "../dropdown";
import { convertCssStyle } from "../extension";
import { createDateInput, formatDate, setDateValue, getDateValue } from "../date";
// definition
import { GridRowItem, DropdownOptions, GridColumnDefinition, GridSourceItem, GridItemWrapper, Grid } from "./grid";
class GridColumn {
static create() { return createElement('span') }
/**
* @ignore
* @callback DropExpandedCallback
* @param {GridRowItem} item - 行数据对象
* @param {Dropdown} drop - 下拉框对象
* @this GridColumnDefinition
*/
static setValue(element, val) { element.innerText = val }
/**
* 列定义基类
*
* _函数调用流程图示_<br/>
* <img src="./assets/column-refresh.jpg" alt="Column Refresh"/>
* @class
* @static
* @hideconstructor
*/
export class GridColumn {
/**
* 该属性返回 `true` 后,在任意事件中修改行包装对象的 `__editing` 值,则会在行列元素变动时及时触发 [onChanged]{@linkcode GridColumnDefinition#onChanged} 方法,避免例如文本框和日期框还未触发事件就被移除元素而导致的问题
* @member
* @name GridColumn.editing
* @readonly
* @type {boolean}
* @see 更多例子参考 {@linkcode GridInputColumn} {@linkcode GridDateColumn} 中的代码实现
*/
/**
* 标记该类型是否可编辑
* @member
* @name GridColumn.canEdit
* @readonly
* @type {boolean}
*/
/**
* 标记该类型是否支持列头批量操作
* @member
* @name GridColumn.headerEditing
* @readonly
* @type {boolean}
*/
/**
* 创建显示单元格时调用的方法
* @param {GridColumnDefinition} col - 列定义对象
* @param {number} index - 行元素索引(需要配合 [startIndex]{@linkcode Grid#startIndex} 相加得到真实数据索引)
* @param {Grid} grid - Grid 实例
* @returns {HTMLElement} 返回创建的单元格元素
* @virtual
*/
static create() {
return createElement('span');
}
/**
* 创建编辑单元格时调用的方法
*
* 元素修改后设置行包装对象的 `__editing` 后,支持在离开编辑状态时及时触发 [leaveEdit]{@linkcode GridColumn.leaveEdit} 方法<br/>
* 更多例子参考代码中 {@linkcode GridDropdownColumn} 的实现。
* @method
* @name GridColumn.createEdit
* @param {Function} trigger - 编辑事件回调函数
* @param {any} trigger.e - 该参数会传递给 [getValue]{@linkcode GridColumn.getValue} 方法
* @param {GridColumnDefinition} col - 列定义对象
* @param {HTMLElement} [container] - 父容器元素
* @param {GridItemWrapper} [wrapper] - 行包装对象,其 `values` 属性为行数据对象
* @returns {HTMLElement} 返回创建的编辑状态的单元格元素
* @virtual
*/
/**
* 创建列头时调用的方法
* @method
* @name GridColumn.createCaption
* @param {GridColumnDefinition} col - 列定义对象
* @returns {HTMLElement} 返回创建的列头元素
* @virtual
*/
/**
* 获取用于判断文本大小的元素
* @method
* @name GridColumn.getElement
* @param {HTMLElement} element - 单元格主内容元素
* @returns {HTMLElement} 返回文本元素
* @virtual
*/
/**
* 获取编辑状态单元格值时调用的方法
* @method
* @name GridColumn.getValue
* @param {any} e - 由 [createEdit]{@linkcode GridColumn.createEdit} 方法中 `trigger` 函数传递来的对象
* @param {GridColumnDefinition} col - 列定义对象
* @returns {(string | boolean | number)} 返回单元格的值
* @virtual
*/
/**
* 设置单元格值时调用的方法
* @param {HTMLElement} element - 单元格元素
* @param {(string | boolean | number)} val - 待设置的单元格值
* @param {GridItemWrapper} wrapper - 行包装对象
* @param {GridColumnDefinition} col - 列定义对象
* @param {Grid} grid - Grid 对象
* @virtual
*/
static setValue(element, val) {
element.innerText = val;
}
/**
* 设置单元格样式时调用的方法
* @param {HTMLElement} element - 单元格元素
* @param {object} style - 样式对象
* @virtual
*/
static setStyle(element, style) {
for (let css of Object.entries(style)) {
element.style.setProperty(css[0], css[1]);
// for (let css of Object.entries(style)) {
// element.style.setProperty(css[0], css[1]);
// }
element.style.cssText = convertCssStyle(style);
}
/**
* 设置单元格类名时调用的方法
* @param {HTMLElement} element - 单元格元素
* @param {string} name - 要设置的类名
* @virtual
*/
static setClass(element, name) {
element.className = name ?? '';
}
/**
* 设置单元格可用性时调用的方法
* @param {HTMLElement} element - 单元格元素
* @param {boolean} enabled - 启用值,为 `false` 时代表禁用
* @param {boolean} editing - 是否处于编辑状态
* @virtual
*/
static setEnabled(element, enabled) {
const tooltip = element.querySelector('.ui-tooltip-wrapper');
if (tooltip != null) {
tooltip.style.display = enabled === false ? 'none' : '';
}
}
/**
* 单元格编辑状态发生改变时调用的方法
* @method
* @name GridColumn.setEditing
* @param {HTMLElement} element - 单元格元素
* @param {boolean} editing - 是否处于编辑状态
* @virtual
*/
/**
* 单元格离开编辑元素时调用的方法,需要由行包装对象的 `__editing` 来确定是否触发。
* @method
* @name GridColumn.leaveEdit
* @param {HTMLElement} element - 单元格元素
* @param {HTMLElement} container - 父容器元素
* @virtual
*/
/**
* @ignore
*/
static toString() { return 'GridCommon' }
/**
* @ignore
* @param {string} key
* @param {GridItemWrapper} wrapper
* @param {any} value
*/
static _changeValue(key, wrapper, value) {
const val = wrapper.values[key] ?? null;
const hasValue = val != null && Object.prototype.hasOwnProperty.call(val, 'Value');
if (wrapper.__editing == null) {
wrapper.__editing = {
[key]: hasValue ? val.Value : val
}
} else if (!Object.prototype.hasOwnProperty.call(wrapper.__editing, key)) {
wrapper.__editing[key] = hasValue ? val.Value : val;
}
if (hasValue) {
val.Value = value;
if (Object.prototype.hasOwnProperty.call(val, 'DisplayValue')) {
val.DisplayValue = value;
}
} else {
wrapper.values[key] = value;
}
}
}
class GridInputColumn extends GridColumn {
/**
* 单行文本输入列
* @class
* @static
* @extends GridColumn
* @hideconstructor
*/
export class GridInputColumn extends GridColumn {
static get editing() { return true };
static get canEdit() { return true };
static createEdit(trigger, col, _parent, vals) {
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @param {HTMLElement} _container
* @param {GridItemWrapper} wrapper
* @returns {HTMLElement}
*/
static createEdit(trigger, col, _container, wrapper) {
const input = createElement('input');
input.setAttribute('type', 'text');
if (typeof trigger === 'function') {
input.addEventListener('change', trigger);
}
input.addEventListener('input', () => {
if (vals.__editing == null) {
vals.__editing = {
[col.key]: true
}
} else {
vals.__editing[col.key] = true;
}
});
input.addEventListener('input', () => super._changeValue(col.key, wrapper, input.value));
input.addEventListener('change', trigger);
return input;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {string} val
*/
static setValue(element, val) {
if (element.tagName !== 'INPUT') {
super.setValue(element, val);
@ -47,30 +248,62 @@ class GridInputColumn extends GridColumn {
}
}
/**
* @ignore
* @param {Event} e
* @returns {string}
*/
static getValue(e) { return e.target.value }
static setEnabled(element, enabled) { element.disabled = enabled === false }
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} enabled
*/
static setEnabled(element, enabled) {
super.setEnabled(element, enabled);
element.disabled = enabled === false;
}
/**
* @ignore
*/
static toString() { return 'GridInput' }
}
class GridTextColumn extends GridInputColumn {
static createEdit(trigger, col, _parent, vals) {
/**
* 多行文本输入列
* @class
* @static
* @extends GridInputColumn
* @hideconstructor
* @ignore
*/
export class GridTextColumn extends GridInputColumn {
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @param {HTMLElement} _container
* @param {GridItemWrapper} wrapper
* @returns {HTMLElement}
*/
static createEdit(trigger, col, _container, wrapper) {
const input = createElement('textarea');
if (typeof trigger === 'function') {
input.addEventListener('change', trigger);
}
input.addEventListener('input', () => {
if (vals.__editing == null) {
vals.__editing = {
[col.key]: true
}
} else {
vals.__editing[col.key] = true;
}
});
input.addEventListener('input', () => super._changeValue(col.key, wrapper, input.value));
input.addEventListener('change', trigger);
return input;
}
static setValue(element, val, _item, _col, grid) {
/**
* @ignore
* @param {HTMLElement} element
* @param {string} val
* @param {GridItemWrapper} _wrapper
* @param {GridColumnDefinition} _col
* @param {Grid} grid
*/
static setValue(element, val, _wrapper, _col, grid) {
if (element.tagName !== 'TEXTAREA') {
super.setValue(element, val);
} else {
@ -79,20 +312,86 @@ class GridTextColumn extends GridInputColumn {
const lines = String(val).split('\n').length;
element.style.height = `${lines * grid.lineHeight + 12}px`;
}
// TODO: bad performance
}
}
/**
* @ignore
*/
static toString() { return 'GridText' }
}
const SymbolDropdown = Symbol.for('ui-dropdown');
class GridDropdownColumn extends GridColumn {
static createEdit(trigger, col, parent) {
const drop = new Dropdown({ ...col.dropOptions, parent });
drop.onselected = trigger;
/**
* 下拉选择列
* @class
* @static
* @extends GridColumn
* @hideconstructor
*/
export class GridDropdownColumn extends GridColumn {
static get canEdit() { return true };
/**
* @ignore
* @returns {HTMLElement}
*/
static create() {
return createElement('span', 'ui-drop-span', createElement('span'));
}
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @param {HTMLElement} container
* @param {GridItemWrapper} wrapper
* @returns {HTMLElement}
*/
static createEdit(trigger, col, container, wrapper) {
const drop = new Dropdown({
...col.dropOptions,
wrapper: container.parentElement
});
drop.onSelected = trigger;
drop.onExpanded = () => {
if (wrapper.__editing == null) {
wrapper.__editing = {
[col.key]: true
}
} else {
wrapper.__editing[col.key] = true;
}
if (typeof col.onDropExpanded === 'function') {
col.onDropExpanded.call(col, wrapper.values, drop);
}
};
return drop.create();
}
static #getDrop(element) {
/**
* @ignore
* @param {HTMLElement} element
* @returns {HTMLElement}
*/
static getElement(element) {
if (element.tagName === 'DIV') {
return element.children[0].children[0];
}
return element.children[0];
}
/**
* @private
* @param {HTMLElement} element
* @returns {Dropdown}
*/
static _getDrop(element) {
/**
* @type {Map<string, Dropdown>}
*/
const dropGlobal = global[SymbolDropdown];
if (dropGlobal == null) {
return null;
@ -105,125 +404,476 @@ class GridDropdownColumn extends GridColumn {
return drop;
}
static #getSource(item, col) {
let source = col.source;
/**
* @private
* @param {GridItemWrapper} wrapper
* @param {GridColumnDefinition} col
* @returns {GridSourceItem[]}
*/
static _getSource(wrapper, col) {
let source;
if (col.sourceCache !== false) {
source = wrapper.source?.[col.key];
if (source != null) {
return source;
}
}
source = col.source;
if (typeof source === 'function') {
source = source(item);
source = source(wrapper.values);
}
if (col.sourceCache !== false) {
if (wrapper.source == null) {
wrapper.source = { [col.key]: source };
} else {
wrapper.source[col.key] = source;
}
}
return source;
}
static #setValue(source, element, val) {
const data = source?.find(v => v.value === val);
/**
* @private
* @param {GridSourceItem[]} source
* @param {HTMLElement} element
* @param {any} val
* @param {DropdownOptions} opts
*/
static _setValue(source, element, val, opts) {
const data = source?.find(v => v[opts?.valueKey ?? 'value'] === val);
if (data != null) {
val = data.text;
val = data[opts?.textKey ?? 'text'];
}
super.setValue(element, val);
element.children[0].innerText = val;
}
static setValue(element, val, item, col) {
/**
* @ignore
* @param {HTMLElement} element
* @param {any} val
* @param {GridItemWrapper} wrapper
* @param {GridColumnDefinition} col
* @param {Grid} grid
*/
static setValue(element, val, wrapper, col, grid) {
if (element.tagName !== 'DIV') {
let source = this.#getSource(item, col);
let source = this._getSource(wrapper, col);
if (source instanceof Promise) {
source.then(s => this.#setValue(s, element, val));
source.then(s => this._setValue(s, element, val, col.dropOptions));
} else {
this.#setValue(source, element, val);
this._setValue(source, element, val, col.dropOptions);
}
return;
}
const drop = this.#getDrop(element);
const drop = this._getDrop(element);
if (drop == null) {
return;
}
const ignoreCase = col.dropRestrictCase !== true;
if (drop.source == null || drop.source.length === 0) {
let source = this.#getSource(item, col);
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);
}
static getValue(e) {
return e.value;
/**
* @ignore
* @param {GridSourceItem} e
* @param {GridColumnDefinition} col
* @returns {any}
*/
static getValue(e, col) {
return {
value: e[col.dropOptions?.valueKey ?? 'value'],
text: e[col.dropOptions?.textKey ?? 'text']
};
}
/**
* @ignore
* @param {HTMLElement} element
* @param {string} name
*/
static setClass(element, name) {
if (element.tagName === 'DIV') {
element.className = `ui-drop-wrapper ${name ?? ''}`;
} else {
super.setClass(element, name);
}
}
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} enabled
*/
static setEnabled(element, enabled) {
const drop = this.#getDrop(element);
super.setEnabled(element, enabled);
const drop = this._getDrop(element);
if (drop == null) {
return;
}
drop.disabled = enabled === false;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {HTMLElement} container
*/
static leaveEdit(element, container) {
container.parentElement.querySelectorAll('.ui-drop-box.active').forEach(e => {
if (e != null) {
e.classList.remove('active');
}
});
const drop = this._getDrop(element);
if (drop == null) {
return;
}
if (drop?.multiSelect && typeof drop.onCollapsed === 'function') {
drop.onCollapsed();
}
}
/**
* @ignore
*/
static toString() { return 'GridDropdown' }
}
class GridCheckboxColumn extends GridColumn {
static createEdit(trigger) {
/**
* 复选框列
* @class
* @static
* @extends GridColumn
* @hideconstructor
* @ignore
*/
export class GridCheckboxColumn extends GridColumn {
static get canEdit() { return true };
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @returns {HTMLElement}
*/
static createEdit(trigger, col) {
const check = createCheckbox({
onchange: typeof trigger === 'function' ? trigger : null
switch: col.switch,
onchange: trigger
});
return check;
}
static setValue(element, val) { element.querySelector('input').checked = val }
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} val
*/
static setValue(element, val) {
// element.querySelector('input').checked = val;
element.children[0].checked = val;
}
/**
* @ignore
* @param {Event} e
* @returns {boolean}
*/
static getValue(e) { return e.target.checked }
static setEnabled(element, enabled) { element.querySelector('input').disabled = enabled === false }
/**
* @ignore
* @param {HTMLElement} element
* @param {string} name
*/
static setClass(element, name) {
if (element.tagName === 'LABEL') {
element.className = `ui-check-wrapper ${name ?? ''}`;
} else {
super.setClass(element, name);
}
}
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} enabled
*/
static setEnabled(element, enabled) {
super.setEnabled(element, enabled);
// element.querySelector('input').disabled = enabled === false;
element.children[0].disabled = enabled === false;
}
/**
* @ignore
*/
static toString() { return 'GridCheckbox' }
}
class GridIconColumn extends GridColumn {
/**
* 单选框列
* @class
* @static
* @extends GridCheckboxColumn
* @hideconstructor
* @ignore
*/
export class GridRadioboxColumn extends GridCheckboxColumn {
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} _col
* @param {number} index
* @returns {HTMLElement}
*/
static createEdit(trigger, _col, index) {
const check = createRadiobox({
name: `r_${index}`,
onchange: trigger
});
return check;
}
/**
* @ignore
*/
static toString() { return 'GridRadiobox' }
}
/**
* 图标列
* @class
* @static
* @extends GridColumn
* @hideconstructor
* @ignore
*/
export class GridIconColumn extends GridColumn {
/**
* @ignore
* @returns {HTMLElement}
*/
static create() { return createElement('span', 'col-icon') }
static setValue(element, val, item, col, grid) {
let className = col.className;
if (typeof className === 'function') {
className = className.call(col, item);
}
if (className == null) {
element.className = 'col-icon';
} else {
element.className = `col-icon ${className}`;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {string} val
* @param {GridItemWrapper} wrapper
* @param {GridColumnDefinition} col
*/
static setValue(element, val, wrapper, col) {
// let className = col.iconClassName;
// if (typeof className === 'function') {
// className = className.call(col, wrapper.values);
// }
// if (className == null) {
// element.className = 'col-icon';
// } else {
// element.className = `col-icon ${className}`;
// }
let type = col.iconType;
if (typeof type === 'function') {
type = type.call(col, item);
type = type.call(col, wrapper.values);
}
type ??= 'fa-regular';
type ??= 'fa-light';
if (element.dataset.type !== type || element.dataset.icon !== val) {
const icon = createIcon(type, val);
// const layer = element.children[0];
element.replaceChildren(icon);
!nullOrEmpty(col.tooltip) && setTooltip(element, col.tooltip, false, grid.element);
// !nullOrEmpty(col.tooltip) && setTooltip(element, col.tooltip, false, grid.element);
element.dataset.type = type;
element.dataset.icon = val;
}
}
/**
* @ignore
* @param {HTMLElement} element
* @param {string} name
*/
static setClass(element, name) {
element.className = `col-icon ${name ?? ''}`;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} enabled
*/
static setEnabled(element, enabled) {
super.setEnabled(element, enabled);
if (enabled === false) {
element.classList.add('disabled');
} else {
element.classList.remove('disabled');
}
const tooltip = element.querySelector('.ui-tooltip-wrapper');
if (tooltip != null) {
tooltip.style.display = enabled === false ? 'none' : '';
}
}
/**
* @ignore
*/
static toString() { return 'GridIcon' }
}
export {
GridColumn,
GridInputColumn,
GridTextColumn,
GridDropdownColumn,
GridCheckboxColumn,
GridIconColumn
/**
* 日期选择列
* @class
* @static
* @extends GridColumn
* @hideconstructor
*/
export class GridDateColumn extends GridColumn {
static get editing() { return true };
static get canEdit() { return true };
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @param {HTMLElement} _container
* @param {GridItemWrapper} wrapper
* @returns {HTMLElement}
*/
static createEdit(trigger, col, _container, wrapper) {
let enabled = col.enabled;
if (typeof enabled === 'string') {
enabled = wrapper.values[enabled];
} else if (typeof enabled === 'function') {
enabled = col.enabled(wrapper.values);
}
if (enabled === false) {
return super.create();
}
const date = createDateInput(col.dateMin, col.dateMax);
date.addEventListener('change', () => super._changeValue(col.key, wrapper, date.value));
date.addEventListener('blur', trigger);
return date;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {(string | number | Date)} val
* @param {GridItemWrapper} _wrapper
* @param {GridColumnDefinition} col
*/
static setValue(element, val, _wrapper, col) {
setDateValue(element, val, col.dateDisplayFormatter);
}
/**
* @ignore
* @param {Event} e
* @param {GridColumnDefinition} col
* @returns {string}
*/
static getValue(e, col) {
if (e.target.tagName === 'INPUT') {
return {
value: getDateValue(e.target, col.dateValueFormatter),
text: getDateValue(e.target, col.dateDisplayFormatter)
};
}
return e.target.innerText;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {string} name
*/
static setClass(element, name) {
if (element.tagName === 'INPUT') {
element.className = `ui-date-cell ${name ?? ''}`;
} else {
super.setClass(element, name);
}
}
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} enabled
*/
static setEnabled(element, enabled) {
element.disabled = enabled === false;
}
/**
* 格式化日期字符串
* @param {(string | number | Date)} date - 要格式化的日期值
*
* 支持以下几种数据类型
* * `"2024-01-26"`
* * `"1/26/2024"`
* * `"638418240000000000"`
* * `new Date('2024-01-26')`
* @param {string} [formatter] - 格式化格式,默认为 `"m/d/Y"`
*
* * Y - 年,例如 `2024`
* * y - 年的后两位,例如 `"24"`
* * n - 月,例如 `2`
* * m - 格式化成两位的月,例如 `"02"`
* * j - 日,例如 `4`
* * d - 格式化成两位的日,例如 `"04"`
* * t - 日期当月总天数,例如 `29`
* * L - 是否为闰年,例如 `1`
* * G - 小时,例如 `15`
* * g - 12 小时制的小时,例如 `3`
* * H - 格式化成两位的小时,例如 `"15"`
* * h - 格式化成两位的 12 小时制的小时,例如 `"03"`
* * A - 上下午,例如 `"PM"`
* * a - 小写的上下午,例如 `"pm"`
* * i - 格式化成两位的分钟,例如 `"39"`
* * s - 格式化成两位的秒,例如 `"20"`
* * u - 格式化成六位的毫秒,例如 `"023040"`
* * e - 时区描述字符串,例如 `"China Standard Time"`
* * O - 时区偏移字符串,例如 `"+0800"`
* * P - `"+08:00"` 这种格式的时区偏移字符串
* @returns {string} 格式化为 M/d/yyyy 的日期字符串
*/
static formatDate(date, formatter) {
return formatDate(date, formatter);
}
/**
* @ignore
*/
static toString() { return 'GridDate' }
}

134
lib/ui/grid/grid.d.ts vendored
View File

@ -1,134 +0,0 @@
import { DropdownOptions } from "../dropdown";
interface GridItem {
value: any;
displayValue: string;
}
interface GridSourceItem {
value: string;
text: string;
}
declare var GridColumn: {
create(): HTMLElement;
createEdit(trigger: (e: any) => void, col: GridColumnDefinition, body: HTMLElement): HTMLElement;
setValue(element: HTMLElement, val: any, item: GridItem | any, col: GridColumnDefinition): void;
getValue(e: any): any;
setStyle(element: HTMLElement, style: { [key: string]: string }): void;
setEnabled(element: HTMLElement, enabled?: boolean): void;
}
interface GridColumnType {
0: "Common";
1: "Input";
2: "Dropdown";
3: "Checkbox";
4: "Icon";
5: "Text";
}
interface GridColumnDefinition {
key?: string;
type?: keyof GridColumnType | typeof GridColumn;
caption?: string;
width?: Number;
align?: "left" | "center" | "right";
enabled?: boolean | string | ((item: GridItem | any) => boolean);
css?: { [key: string]: string };
styleFilter?: (item: GridItem | any) => { [key: string]: string };
textStyle?: { [key: string]: string };
visible?: boolean;
resizable?: boolean;
sortable?: boolean;
orderable?: boolean;
allcheck?: boolean;
events?: { [event: string]: any };
attrs?: { [key: string]: string } | ((item: GridItem | any) => { [key: string]: string });
// TODO: allowFilter?: boolean;
filter?: (item: GridItem | any) => any;
sortFilter?: (a: GridItem | any, b: GridItem | any) => -1 | 0 | 1;
bgFilter?: (item: GridItem | any) => string;
dropOptions?: DropdownOptions;
source?: Array<any> | ((item: GridItem | any) => Array<any> | Promise<Array<GridSourceItem>>);
iconType?: string;
className?: string | ((item: GridItem | any) => string);
text?: string;
tooltip?: string;
onallchecked?: (this: Grid, col: GridColumnDefinition, flag: boolean) => void;
onchanged?: (this: Grid, item: GridItem | any, value: boolean | any) => void;
}
interface GridColumnDirection {
[-1]: Number,
1: Number
}
interface GridColumnColumnEventMap {
"reorder": string,
"resize": string,
"sort": string
}
interface Grid {
columns: Array<GridColumnDefinition>;
langs?: { all: string, ok: string, reset: string };
virtualCount?: Number;
rowHeight?: Number;
extraRows?: Number;
filterRowHeight?: Number;
height?: Number;
readonly?: boolean;
multiSelect?: boolean;
fullrowClick?: boolean;
allowHtml?: boolean;
holderDisabled?: boolean;
headerVisible?: boolean;
window?: Window
sortIndex?: Number;
sortDirection?: keyof GridColumnDirection;
willSelect?: (index: Number, colIndex: Number) => boolean;
selectedRowChanged?: (index?: Number) => void;
cellDblClicked?: (index: Number, colIndex: Number) => void;
cellClicked?: (index: Number, colIndex: Number) => boolean;
rowDblClicked?: (index: Number) => void;
columnChanged?: <K extends keyof GridColumnColumnEventMap>(type: K, index: Number, value: Number | keyof GridColumnDirection) => void;
get source(): Array<GridItem | any>;
set source(list: Array<GridItem | any>);
get selectedIndexes(): Array<Number>;
set selectedIndexes(indexes: Array<Number>);
get loading(): boolean;
set loading(flag: boolean);
get scrollTop(): Number;
set scrollTop(top: Number);
readonly virtual: boolean;
readonly sortKey: string | undefined;
readonly selectedIndex: Number | -1;
init(container?: HTMLElement): void;
scrollToIndex(index: Number): void;
resize(force?: boolean): void;
reload(): void;
refresh(): void;
resetChange(): void;
sortColumn(reload?: boolean): void;
}
declare var Grid: {
ColumnTypes: {
Common: 0,
Input: 1,
Dropdown: 2,
Checkbox: 3,
Icon: 4,
Text: 5,
isCheckbox(type: Number): boolean;
};
GridColumn: typeof GridColumn;
new(container: HTMLElement): Grid;
}
export default Grid;

View File

@ -1,277 +0,0 @@
<div>
<h1>grid</h1>
<hr />
<p>
创建一个统一样式的滚动列表元素。
</p>
<h2>constructor</h2>
<code>new(container: HTMLElement): Grid</code>
<h3>container: HTMLElement</h3>
<p>
父容器元素Grid 将创建在此元素之内
</p>
<h2>init</h2>
<code>init(container?: HTMLElement): void</code>
<h3>container?: HTMLElement</h3>
<p>
父容器元素,默认使用构造函数中传递的值
</p>
<h2>scrollToIndex</h2>
<code>scrollToIndex(index: Number): void</code>
<h3>index: Number</h3>
<p>
将滚动至此行
</p>
<h2>resize</h2>
<code>resize(force?: boolean): void</code>
<h3>force?: boolean</h3>
<p>
重新计算大小,参数表示是否强制重载
</p>
<h2>reload</h2>
<code>reload(): void</code>
<p>
重载表格元素
</p>
<h2>refresh</h2>
<code>refresh(): void</code>
<p>
刷新表格单元格值
</p>
<h2>resetChange</h2>
<code>resetChange(): void</code>
<p>
还原表格修改状态,置为未修改
</p>
<h2>sortColumn</h2>
<code>sortColumn(reload?: boolean): void</code>
<h3>reload?: boolean</h3>
<p>
根据当前设定的排序列排序,参数表示是否重载表格
</p>
<hr />
<h2>sortIndex</h2>
<code>sortIndex?: Number</code>
<p>
排序的列序号
</p>
<h2>sortDirection</h2>
<code>sortDirection?: keyof GridColumnDirection</code>
<p>
排序的方向,可选值为 <code>-1: desc</code><code>1: asc</code>
</p>
<h2>columns</h2>
<code>columns: Array&lt;GridColumnDefinition&gt;</code>
<p>
表格列的定义,结构为
<pre>interface GridColumnDefinition {
key?: string;
type?: keyof GridColumnType | typeof GridColumn;
caption?: string;
width?: Number;
align?: "left" | "center" | "right";
enabled?: boolean | string;
css?: { [key: string]: string };
styleFilter?: (item: GridItem | any) => { [key: string]: string };
textStyle?: { [key: string]: string };
visible?: boolean;
resizable?: boolean;
sortable?: boolean;
orderable?: boolean;
allcheck?: boolean;
events?: { [event: string]: any };
attrs?: { [key: string]: string } | ((item: GridItem | any) => { [key: string]: string });
filter?: (item: GridItem | any) => any;
sortFilter?: (a: GridItem | any, b: GridItem | any) => -1 | 0 | 1;
bgFilter?: (item: GridItem | any) => string;
dropOptions?: DropdownOptions;
source?: Array&lt;any&gt; | ((item: GridItem | any) => Array&lt;any&gt; | Promise&lt;Array&lt;GridSourceItem&gt;&gt;);
iconType?: string;
text?: string;
tooltip?: string;
onallchecked?: (this: Grid, col: GridColumnDefinition, flag: boolean) => void;
onchanged?: (this: Grid, item: GridItem | any, value: boolean | any) => void;
}</pre>
</p>
<h3>key</h3>
<code>key?: string</code>
<p>关键字</p>
<h3>type</h3>
<code>type?: keyof GridColumnType | typeof GridColumn</code>
<p>列类型,可以为 <code>Grid.ColumnTypes</code> 枚举值表示内置的通用、输入、下拉、复选框、图标等类型
<pre>declare var ColumnTypes: {
Common: 0,
Input: 1,
Dropdown: 2,
Checkbox: 3,
Icon: 4,
isCheckbox(type: Number): boolean;
}</pre>也可以为符合 <code>GridColumn</code> 接口的类或对象表示自定义类型,接口定义如下
<pre>interface GridColumn {
static create(): HTMLElement;
static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, body: HTMLElement): HTMLElement;
static setValue(element: HTMLElement, val: any): void;
static getValue(e: any): any;
static setStyle(element: HTMLElement, style: { [key: string]: string }): void;
static setEnabled(element: HTMLElement, enabled?: boolean): void;
}</pre>
</p>
<h4>create</h4>
<code>static create(): HTMLElement</code>
<p>创建只读状态的单元格元素</p>
<h4>createEdit</h4>
<code>static createEdit(trigger: (e: any) => void, col: GridColumnDefinition, body: HTMLElement): HTMLElement</code>
<p>创建编辑状态的单元格元素</p>
<samp>trigger: (e: any) => void</samp>
<p>用以触发 Grid 的列修改事件 <code>columnChanged</code> 的函数代理</p>
<samp>col: GridColumnDefinition</samp>
<p>当前列的定义对象</p>
<samp>body: HTMLElement</samp>
<p>Grid 正文表格元素</p>
<h4>setValue</h4>
<code>static setValue(element: HTMLElement, val: any, item: GridItem | any, col: GridColumnDefinition): void</code>
<p>设置单元格值时触发的函数</p>
<samp>element: HTMLElement</samp>
<p>单元格元素</p>
<samp>val: any</samp>
<p>单元格的值</p>
<samp>item: GridItem | any</samp>
<p>单元格所在行的数据项</p>
<samp>col: GridColumnDefinition</samp>
<p>单元格所在列的定义对象</p>
<hr />
<h2>示例</h2>
<pre></pre>
<div id="grid-sample"></div>
<!-- <div style="height: 80px"></div> -->
<script type="text/javascript">
!function () {
const Grid = window["lib-ui"].Grid;
const grid = new Grid(document.querySelector('#grid-sample'));
const statusCol = {
...Grid.GridColumn,
create() {
const container = document.createElement('div');
return container;
},
// createEdit() { },
setValue(element, val) {
element.innerHTML = val;
},
getValue(element) {
return element.innerHTML;
},
setEnabled(element, enabled) {
element.className = enabled === false ? 'disabled' : '';
}
};
const iconCol = {
create() {
const a = document.createElement('a');
a.target = '_blank';
a.className = 'icon-col';
return a;
},
setValue(element, val, _item, col) {
if (col.tooltip) {
element.setAttribute('title', col.tooltip);
}
element.innerText = val;
},
setEnabled(element, enabled) {
element.style.display = enabled === false ? 'none' : '';
},
setStyle: Grid.GridColumn.setStyle
};
// grid.readonly = true;
grid.allowHtml = true;
grid.height = 0;
grid.columns = [
{ key: 'c1', caption: 'column 122222222311111111111111111' },
{ key: 'c2', caption: '选择', allcheck: true, type: Grid.ColumnTypes.Checkbox, enabled: 'enabled' },
{
key: 'c2a',
caption: '下拉',
type: Grid.ColumnTypes.Dropdown,
allowFilter: true,
source: item => {
if (item.source == null) {
return new Promise((resolve, reject) => {
setTimeout(() => {
item.source = [
{ value: 'off', text: 'Off' },
{ value: 'pending', text: 'Pending' },
{ value: 'broken', text: 'Broken' },
{ value: 'running', text: 'Running' }
];
resolve(item.source);
}, 2000);
});
}
return item.source;
},
enabled: 'enabled',
onchanged: (item, value) => console.log('dropdown changed', item, value)
},
{
key: 'c2b',
caption: '自定义',
type: statusCol,
enabled: 'enabled'
},
{ key: 'c2c', caption: '多行编辑', type: Grid.ColumnTypes.Text, enabled: 'enabled' },
{ key: 'c3', caption: 'column 3', width: 90 },
{ key: 'c4', caption: 'Note', type: Grid.ColumnTypes.Input },
{
key: 'c5',
type: Grid.ColumnTypes.Icon,
enabled: 'enabled',
text: 'times',
tooltip: 'Delete',
events: {
onclick() { console.log('deleted', this) }
}
},
{
key: 'c6',
type: iconCol,
enabled: 'enabled',
text: '\uf044',
tooltip: 'Edit',
events: {
onclick() { alert(`Edit, ${this.c1}`) }
}
}
];
grid.columnChanged = (type, index, value) => console.log(`column (${index}), ${type}, ${value}`);
grid.cellClicked = (rId, cId) => console.log(`row (${rId}), column (${cId}) clicked.`);
grid.rowDblClicked = (rId) => console.log(`row (${rId}) double clicked.`);
grid.cellDblClicked = (rId, cId) => console.log(`row (${rId}), column (${cId}) double clicked.`);
grid.init();
grid.source = [
{ c1: 'abc', c2: true, c2a: 'off', c2b: '<font style="color: red; margin-left: 8px">red</font>', c2c: 'multiple lines\nline2\nline3\n\nline5', c3: 12345, c4: 'another note' },
{ c1: 'abc2bbbbaaaaa', c2: false, c2a: 'pending', c2b: '<b style="margin-left: 8px">bold</b>', c3: 1225, c4: 'Note note this is note' },
{ c1: 'type', c2: false, c2a: 'broken', c2c: 'multiple lines\nline2\nline3\n\nline5', c3: 121111 },
{ c1: 'diff', c2: true, c2a: 'running', c3: 124445555555555555 },
{ c1: 'diff', c2: true, c2a: 'running', c3: 12499, enabled: false },
{ c1: 'diff', c2: true, c2a: 'off', c3: 1244445 }
];
window.grid = grid;
}();
</script>
<style type="text/css">
#grid-sample {
height: 500px;
}
#grid-sample>.ui-grid {
height: 100%;
}
</style>
</div>

File diff suppressed because it is too large Load Diff

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

@ -1,3 +1,26 @@
export function createIcon(type: string, id: string, style?: { [key: string]: string }): SVGSVGElement
/**
* 创建矢量图标元素
* @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 } | ((icon: SVGSVGElement) => { [key: string]: string }), action?: (this: SVGSVGElement, ev: MouseEvent) => any): SVGSVGElement
/**
* 修改矢量图标
* @param svg 矢量图标元素
* @param type 样式分类,见 { @link createIcon } 第一个参数
* @param id 图标 id见 { @link createIcon } 第二个参数
*/
export function changeIcon(svg: SVGSVGElement, type: string, id: string): SVGSVGElement
/**
* 解析容器元素内符合条件的子元素为矢量图标元素
* @param container 容器元素
* @example
* ```
* <svg data-id="user" data-type="fa-solid"></svg>
* ```
* - `data-id`: 见 { @link createIcon } 第二个参数
* - `data-type`: 见 { @link createIcon } 第一个参数
*/
export function resolveIcon(container: HTMLElement): HTMLElement

View File

@ -1,50 +0,0 @@
<div>
<h1>icon</h1>
<hr />
<p>
创建一个 svg 矢量图标元素,或者解析转换页面上特定类型的 svg
标签到指定的图标元素。
</p>
<h2>createIcon</h2>
<code>function createIcon(type: string, id: string, style?: { [key: string]: string }): SVGElement</code>
<h3>type: string</h3>
<p>
图标类型,可选值目前有 <code>fa-regular</code><code>fa-light</code><code>fa-solid</code>
</p>
<h3>id: string</h3>
<p>
图形 id例如
<code>user-edit</code><code>address-card</code><code>frog</code>……
</p>
<h3>style?: { [key: string]: string }</h3>
<p>
自定义样式的对象
</p>
<h2>resolveIcon</h2>
<code>function resolveIcon(container: HTMLElement): HTMLElement</code>
<h3>container: HTMLElement</h3>
<p>
将把此 HTML 元素下的所有 <code>svg[data-id]</code> 元素解析为图标,<code>[data-id]</code>
同上述 <code>id: string</code><code>[data-type]</code> 同上述
<code>type: string</code>
</p>
<hr />
<h2>示例</h2>
<pre>&lt;div id="icon-sample"&gt;
&lt;svg data-id="address-card" data-type="fa-regular"&gt;&lt;/svg&gt;
&lt;svg data-id="user-edit" data-type="fa-light"&gt;&lt;/svg&gt;
&lt;svg data-id="frog" data-type="fa-solid"&gt;&lt;/svg&gt;
&lt;/div&gt;
&lt;script type="text/javascript"&gt;
window["lib-ui"].resolveIcon(document.querySelector("#icon-sample"));
&lt;/script&gt;</pre>
<div id="icon-sample">
<svg data-id="address-card" data-type="fa-regular"></svg>
<svg data-id="user-edit" data-type="fa-light"></svg>
<svg data-id="frog" data-type="fa-solid"></svg>
</div>
<script type="text/javascript">
window["lib-ui"].resolveIcon(document.querySelector("#icon-sample"));
</script>
</div>

File diff suppressed because one or more lines are too long

32
lib/ui/media.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
/**
* 创建媒体图片元素
* @param url 图片链接地址
* @returns 返回一个 A 标签元素
*/
export function createPicture(url: string): HTMLAnchorElement
/**
* 创建一个音频播放元素
* @param mime 音频类型,如 `audio/amr`、`audio/ogg`
* @param url 音频 url
*/
export function createAudio(mime: string, url: string): HTMLAudioElement | HTMLDivElement
/**
* 创建一个视频播放元素
* @param url 视频 url
*/
export function createVideo(url: string): HTMLVideoElement
/**
* 创建一个文件元素
* @param url 文件 url
* @param icon 图标,默认为 `file-alt`
*/
export function createFile(url: string, icon?: string): HTMLDivElement
/**
* 创建联动视频元素
* @param urls 视频 url 数组
*/
export function createVideoList(urls: string[]): HTMLDivElement

451
lib/ui/media.js Normal file
View File

@ -0,0 +1,451 @@
import "./css/media.scss";
import { createElement } from "../functions";
import { createIcon } from "./icon";
import { get } from "../utility/request";
export function createPicture(url) {
return createElement('a', a => {
a.className = 'ui-media-picture';
a.target = '_blank';
a.href = url;
},
createElement('img', img => {
img.src = url;
})
);
}
function readBlob(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => {
const data = new Uint8Array(e.target.result);
resolve(data);
};
reader.onerror = reject;
reader.readAsArrayBuffer(blob);
});
}
function playAmrArray(array) {
return new Promise((resolve, reject) => {
const samples = AMR.decode(array);
if (samples != null) {
resolve(samples);
} else {
reject();
}
});
}
function playPcm(samples, ended) {
return new Promise(resolve => {
const ctx = new AudioContext();
ctx.addEventListener('statechange', () => resolve(ctx));
const source = ctx.createBufferSource();
if (typeof ended === 'function') {
source.addEventListener('ended', () => ended(ctx));
}
const buffer = ctx.createBuffer(1, samples.length, 8000);
if (typeof buffer.copyToChannel === 'function') {
buffer.copyToChannel(samples, 0, 0);
} else {
const channelBuffer = buffer.getChannelData(0);
channelBuffer.set(samples);
}
source.buffer = buffer;
source.connect(ctx.destination);
ctx.duration = buffer.duration;
source.start();
// resolve(ctx);
});
}
function getTimeLabel(time) {
// 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 = '0:00 / 0:00';
let context;
let timer;
return createElement('div', 'ui-media-audio',
createElement('button', button => {
button.className = 'play';
button.addEventListener('click', () => {
if (context != null) {
clearInterval(timer);
context.close();
context = null;
timestamp.textContent = '0:00 / 0:00';
button.className = 'play';
button.replaceChildren(createIcon('fa-solid', 'play'));
return;
}
get(url, { accept: mime })
.then(r => r.blob())
.then(r => readBlob(r))
.then(r => playAmrArray(r))
.then(r => playPcm(r, ctx => {
context = null;
clearInterval(timer);
timestamp.textContent = '0:00 / ' + getTimeLabel(ctx.duration);
button.className = 'play';
button.replaceChildren(createIcon('fa-solid', 'play'));
}))
.then(ctx => {
context = ctx;
button.className = 'stop';
button.replaceChildren(createIcon('fa-solid', 'stop'));
const total = getTimeLabel(ctx.duration);
const refresh = () => timestamp.textContent = getTimeLabel(ctx.currentTime) + ' / ' + total;
refresh();
timer = setInterval(refresh, 500);
})
.catch(e => {
clearInterval(timer);
console.error(e);
});
});
},
createIcon('fa-solid', 'play')
),
timestamp
);
}
return createElement('audio', audio => {
audio.src = url;
audio.controls = true;
});
}
export function createVideo(url) {
return createElement('video', video => {
video.className = 'ui-media-video';
video.src = url;
video.controls = true;
});
}
export function createFile(url, icon = 'file-alt') {
return createElement('div', `ui-media-file ${icon}`,
createIcon('fa-solid', icon),
createElement('a', a => {
a.target = '_blank';
a.href = url;
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;
}

116
lib/ui/popup.d.ts vendored Normal file
View File

@ -0,0 +1,116 @@
/**
* 弹出框选项
*/
interface PopupOptions {
/** 弹出框内容,可以是文本或者 html 元素 */
content: string | HTMLElement;
/** 弹出框标题,可以是文本或者 html 元素 */
title: string | HTMLElement;
/** 是否持久化显示 */
persistent?: boolean;
/** 是否包含遮罩层,默认为 `true` */
mask?: boolean;
/** 遮罩层 z-index */
zIndex?: number;
/** 是否在获取焦点时修改 z-index */
changeZIndex?: boolean;
/** 是否允许关闭 */
closable?: boolean;
/** 是否允许移动 */
movable?: boolean;
/** 是否允许修改大小 */
resizable?: boolean;
/** 最小宽度,默认 200 */
minWidth?: number;
/** 最小高度,默认 200 */
minHeight?: number;
/** 是否允许弹出框收折 */
collapsable?: boolean;
/** 弹出框的按钮定义集合 */
buttons?: PopupButton[];
/**
* 遮罩层显示或者隐藏时的回调函数
* @param this 当前 popup 对象
* @param masking 显示或隐藏遮罩层
*/
onMasking?: (this: Popup, masking: boolean) => void;
/**
* 移动结束时的回调函数
* @param this 当前 popup 对象
*/
onMoveEnded?: (this: Popup) => void;
/**
* 修改大小开始时的回调函数
* @param this 当前 popup 对象
*/
onResizeStarted?: (this: Popup) => void;
/**
* 修改大小中的回调函数
* @param this 当前 popup 对象
* @param x 坐标 x
* @param y 坐标 y
* @param width 当前宽度
* @param height 当前高度
*/
onResizing?: (this: Popup, x: number, y: number, width: number, height: number) => void;
/**
* 修改大小结束时的回调函数
* @param this 当前 popup 对象
*/
onResizeEnded?: (this: Popup) => 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 {
className?: string;
tabIndex?: number;
key: string;
text: string;
trigger: (this: Popup) => boolean | Promise<boolean>;
}
interface PopupIconTypes {
'info': 'info-circle';
'information': 'info-circle';
'warn': 'exclamation-triangle';
'warning': 'exclamation-triangle';
'question': 'question-circle';
'error': 'times-circle';
}
interface PopupButtonResult {
result: string;
popup: Popup;
}
export function createPopup(title: string | HTMLElement, content: string | HTMLElement, ...buttons: PopupButton[]): Popup
export function resolvePopup(wrapper: string | HTMLElement, callback?: Function, removable?: boolean, zIndex?: number): Popup
export function showAlert(title: string | HTMLElement, message: string, iconType?: keyof PopupIconTypes, parent?: HTMLElement): Promise<void>
export function showConfirm(title: string | HTMLElement, content: string | HTMLElement, buttons: PopupButton[], iconType?: keyof PopupIconTypes, parent?: HTMLElement): Promise<PopupButtonResult>

View File

@ -1,51 +0,0 @@
<div>
<h1>popup</h1>
<hr />
<p>
创建弹出窗口。
</p>
<hr />
<h2>示例</h2>
<div id="popup-sample">
<button id="button-popup">Popup</button>
</div>
<script type="text/javascript">
const ui = window['lib-ui'];
document.querySelector('#button-popup').addEventListener('click', () => {
const popup = new ui.Popup({
title: 'title long title, looooooong title, looooooooooooooooooooooooong ~',
content: ui.createElement('div', 'class-content',
ui.createElement('span', span => span.innerText = 'Text content.')
),
mask: false,
// movable: false,
resizable: true,
collapsable: true,
minWidth: 210,
minHeight: 200,
buttons: [
{
text: 'Loading', trigger: p => {
p.loading = true;
setTimeout(() => p.loading = false, 1000);
return false;
}
},
{ text: 'OK' }
],
onMoveEnded: () => console.log('move ended.', popup.rect),
onResizeEnded: () => console.log('resize ended.', popup.rect)
});
popup.show();
window.popup = popup;
});
</script>
<style type="text/css">
.ui-popup-mask .ui-popup-container {
min-width: 210px;
min-height: 200px;
max-width: unset;
}
</style>
</div>

View File

@ -1,9 +1,10 @@
import "./css/popup.scss";
import { r } from "../utility/lgres";
import { r as lang } from "../utility/lgres";
import { nullOrEmpty } from "../utility/strings";
import { global } from "../utility";
import { createElement } from "../functions";
import { createIcon, changeIcon } from "./icon";
import { requestAnimationFrame } from "../ui";
const ResizeMods = {
right: 1,
@ -38,17 +39,42 @@ function trimPx(px) {
return px;
}
class Popup {
#mask;
#option;
#bounds;
// #cursor;
export class Popup {
_var = {};
// _var.mask;
// _var.option;
// _var.bounds;
// _var.cursor;
constructor(opts = {}) {
this.#option = opts;
this._var.option = opts;
}
get container() { return this.#mask.querySelector('.ui-popup-container') }
get container() { return this._var.mask.querySelector('.ui-popup-container') }
get title() { return this._var.option.title }
set title(title) {
const element = this._var.mask?.querySelector('.ui-popup-container .ui-popup-header .ui-popup-header-title');
if (element != null) {
element.innerText = title;
}
this._var.option.title = title;
}
get loading() { return this._var.mask?.querySelector('.ui-popup-body>.ui-popup-loading')?.style?.visibility === 'visible' }
set loading(flag) {
let loading = this._var.mask?.querySelector('.ui-popup-body>.ui-popup-loading');
if (loading == null) {
return;
}
if (flag === false) {
loading.style.visibility = 'hidden';
loading.style.opacity = 0;
} else {
loading.style.visibility = 'visible';
loading.style.opacity = 1;
}
}
get rect() {
const container = this.container;
@ -57,7 +83,7 @@ class Popup {
}
const style = global.getComputedStyle(container);
const collapsed = container.classList.contains('ui-popup-collapse');
const bounds = this.#bounds;
const bounds = this._var.bounds;
return {
collapsed,
left: trimPx(style.left),
@ -78,10 +104,10 @@ class Popup {
if (!isNaN(r.top)) {
css.push(`top: ${r.top}px`);
}
const collapse = container.querySelector('.ui-popup-header>.icon-expand');
const collapse = container.querySelector('.ui-popup-header-icons>.icon-expand');
if (r.collapsed === true) {
css.push('width: 160px', 'height: 40px');
this.#bounds = r;
this._var.bounds = r;
container.classList.add('ui-popup-collapse');
if (collapse != null) {
changeIcon(collapse, 'fa-regular', 'expand-alt');
@ -94,7 +120,7 @@ class Popup {
css.push(`height: ${r.height}px`);
}
container.classList.remove('ui-popup-collapse');
this.#bounds = null;
this._var.bounds = null;
if (collapse != null) {
changeIcon(collapse, 'fa-regular', 'compress-alt');
}
@ -104,107 +130,191 @@ class Popup {
}
}
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(() => { doClose(); }, 120);
} else {
doClose();
}
if (typeof option.onMasking === 'function') {
option.onMasking.call(this, false);
}
if (typeof option.resolve === 'function') {
option.resolve.call(this, {
result,
popup: this
});
}
}
/**
* 创建 Popup 面板
* @returns {HTMLDivElement} 返回遮罩元素(顶层元素)
*/
create() {
const mask = createElement('div', 'ui-popup-mask');
if (this.#option.mask === false) {
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');
} else if (typeof option.onMasking === 'function') {
option.onMasking.call(this, true);
}
if (!isNaN(option.zIndex)) {
mask.style.zIndex = String(option.zIndex);
}
const container = createElement('div', 'ui-popup-container');
if (option.changeZIndex === true) {
container.addEventListener('mousedown', () => {
const masks = [...this._var.mask.parentElement.children].filter(e => e.classList.contains('ui-popup-mask'));
let max = 200;
masks.forEach(m => {
let index;
if (m.dataset.zindex != null) {
index = parseInt(m.dataset.zindex);
m.style.zIndex = isNaN(index) ? '' : String(index);
delete m.dataset.zindex;
} else {
index = parseInt(m.style.zIndex);
}
if (index > max) {
max = index;
}
});
mask.dataset.zindex = mask.style.zIndex;
mask.style.zIndex = max + 1;
});
} else {
}
let tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0));
if (tabIndex < 0) {
tabIndex = 0;
}
container.tabIndex = tabIndex + 1;
const close = () => {
mask.classList.add('ui-popup-active');
mask.style.opacity = 0;
setTimeout(() => mask.remove(), 120);
};
let content = this.#option.content;
let content = option.content;
if (!(content instanceof HTMLElement)) {
content = createElement('div', d => d.innerText = content);
}
container.append(
createElement('div', header => {
header.className = 'ui-popup-header';
let title = this.#option.title;
let title = option.title;
if (!(title instanceof HTMLElement)) {
title = createElement('div', t => {
t.className = 'ui-popup-header-title';
if (option.movable === false) {
t.className = 'ui-popup-header-title no-move';
} else {
t.className = 'ui-popup-header-title';
}
t.innerText = title;
});
}
header.appendChild(title);
if (this.#option.movable !== false) {
const move = title.querySelector('.ui-popup-move') ?? title;
if (option.movable !== false) {
const move = header; // title.querySelector('.ui-popup-move') ?? title;
move.addEventListener('mousedown', e => {
if (['svg', 'use'].includes(e.target?.tagName)) {
return;
}
if (e.buttons !== 1) {
return;
}
const parent = option.mask === false ? mask.parentElement : mask;
const x = e.clientX - container.offsetLeft;
const y = e.clientY - container.offsetTop;
let moved;
const move = e => {
container.style.left = `${e.clientX - x}px`;
container.style.top = `${e.clientY - y}px`;
moved = true;
if (e.buttons === 1) {
container.style.left = `${e.clientX - x}px`;
container.style.top = `${e.clientY - y}px`;
moved = true;
} else {
parent.dispatchEvent(new MouseEvent('mouseup'));
}
};
mask.addEventListener('mousemove', move, { passive: false });
parent.addEventListener('mousemove', move, { passive: false });
const up = () => {
mask.removeEventListener('mousemove', move, { passive: false });
mask.removeEventListener('mouseup', up);
if (moved === true && typeof this.#option.onMoveEnded === 'function') {
this.#option.onMoveEnded.call(this);
parent.removeEventListener('mousemove', move, { passive: false });
parent.removeEventListener('mouseup', up);
if (moved === true && typeof option.onMoveEnded === 'function') {
option.onMoveEnded.call(this);
}
moved = false;
};
mask.addEventListener('mouseup', up);
parent.addEventListener('mouseup', up);
});
}
if (this.#option.collapsable === true) {
const collapse = createIcon('fa-regular', 'compress-alt');
collapse.tabIndex = tabIndex + 2;
collapse.classList.add('icon-expand');
collapse.addEventListener('keypress', e => {
if (e.key === ' ' || e.key === 'Enter') {
collapse.dispatchEvent(new MouseEvent('click'));
}
});
collapse.addEventListener('click', () => {
if (container.classList.contains('ui-popup-collapse')) {
const bounds = this.#bounds;
if (bounds != null) {
container.style.cssText += `width: ${bounds.width}px; height: ${bounds.height}px`;
this.#bounds = null;
const icons = createElement('div', icons => {
icons.className = 'ui-popup-header-icons';
if (option.collapsable === true) {
const collapse = createIcon('fa-regular', 'compress-alt');
collapse.tabIndex = tabIndex + 2;
collapse.classList.add('icon-expand');
collapse.addEventListener('keypress', e => {
if (e.key === ' ' || e.key === 'Enter') {
collapse.dispatchEvent(new MouseEvent('click'));
}
container.classList.remove('ui-popup-collapse');
changeIcon(collapse, 'fa-regular', 'compress-alt');
} else {
const rect = this.rect;
this.#bounds = rect;
container.style.cssText += `width: 160px; height: 40px`;
container.classList.add('ui-popup-collapse');
changeIcon(collapse, 'fa-regular', 'expand-alt');
}
});
header.appendChild(collapse);
}
const cancel = createIcon('fa-regular', 'times');
cancel.tabIndex = tabIndex + 3;
cancel.addEventListener('keypress', e => {
if (e.key === ' ' || e.key === 'Enter') {
close();
});
collapse.addEventListener('click', () => {
if (container.classList.contains('ui-popup-collapse')) {
const bounds = this._var.bounds;
if (bounds != null) {
container.style.cssText += `width: ${bounds.width}px; height: ${bounds.height}px`;
this._var.bounds = null;
}
container.classList.remove('ui-popup-collapse');
changeIcon(collapse, 'fa-regular', 'compress-alt');
} else {
const rect = this.rect;
this._var.bounds = rect;
container.style.cssText += `width: 160px; height: 40px`;
container.classList.add('ui-popup-collapse');
changeIcon(collapse, 'fa-regular', 'expand-alt');
}
if (typeof option.onResizeEnded === 'function') {
option.onResizeEnded.call(this);
}
});
icons.appendChild(collapse);
}
if (option.closable !== false) {
const cancel = createIcon('fa-regular', 'times');
cancel.tabIndex = tabIndex + 3;
cancel.addEventListener('keypress', e => {
if (e.key === ' ' || e.key === 'Enter') {
this.close();
}
});
cancel.addEventListener('click', () => this.close());
icons.appendChild(cancel);
}
});
cancel.addEventListener('click', () => close());
header.appendChild(cancel);
header.appendChild(icons);
}),
createElement('div', 'ui-popup-body', content, createElement('div', 'ui-popup-loading',
createElement('div', null, createIcon('fa-regular', 'spinner-third'))
))
);
if (Array.isArray(this.#option.buttons)) {
if (Array.isArray(option.buttons) && option.buttons.length > 0) {
tabIndex = Math.max.apply(null, [...container.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0));
container.appendChild(
createElement('div', 'ui-popup-footer', ...this.#option.buttons.map((b, i) => {
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 {
@ -217,14 +327,14 @@ class Popup {
if (typeof result?.then === 'function') {
result.then(r => {
if (r !== false) {
close();
this.close(r);
}
}).catch(() => { });
}).catch(reason => console.warn(reason));
} else if (result !== false) {
close();
this.close(result);
}
} else {
close();
this.close(b.key ?? i);
}
});
return button;
@ -243,93 +353,106 @@ class Popup {
}
});
}
} else {
container.querySelector('.ui-popup-body>.ui-popup-loading').classList.add('ui-popup-loading-content');
}
// resizable
if (this.#option.resizable === true) {
if (option.resizable === true) {
container.append(
createElement('layer', layer => {
layer.className = 'ui-popup-border ui-popup-border-right';
layer.addEventListener('mousedown', e => this.#resize(ResizeMods.right, e));
layer.addEventListener('mousedown', e => this._resize(ResizeMods.right, e));
}),
createElement('layer', layer => {
layer.className = 'ui-popup-border ui-popup-border-bottom';
layer.addEventListener('mousedown', e => this.#resize(ResizeMods.bottom, e));
layer.addEventListener('mousedown', e => this._resize(ResizeMods.bottom, e));
}),
createElement('layer', layer => {
layer.className = 'ui-popup-border ui-popup-border-left';
layer.addEventListener('mousedown', e => this.#resize(ResizeMods.left, e));
layer.addEventListener('mousedown', e => this._resize(ResizeMods.left, e));
}),
createElement('layer', layer => {
layer.className = 'ui-popup-border ui-popup-border-top';
layer.addEventListener('mousedown', e => this.#resize(ResizeMods.top, e));
layer.addEventListener('mousedown', e => this._resize(ResizeMods.top, e));
}),
createElement('layer', layer => {
layer.className = 'ui-popup-border ui-popup-border-bottom-right';
layer.addEventListener('mousedown', e => this.#resize(ResizeMods.bottomRight, e));
layer.addEventListener('mousedown', e => this._resize(ResizeMods.bottomRight, e));
}),
createElement('layer', layer => {
layer.className = 'ui-popup-border ui-popup-border-bottom-left';
layer.addEventListener('mousedown', e => this.#resize(ResizeMods.bottomLeft, e));
layer.addEventListener('mousedown', e => this._resize(ResizeMods.bottomLeft, e));
}),
createElement('layer', layer => {
layer.className = 'ui-popup-border ui-popup-border-top-left';
layer.addEventListener('mousedown', e => this.#resize(ResizeMods.topLeft, e));
layer.addEventListener('mousedown', e => this._resize(ResizeMods.topLeft, e));
}),
createElement('layer', layer => {
layer.className = 'ui-popup-border ui-popup-border-top-right';
layer.addEventListener('mousedown', e => this.#resize(ResizeMods.topRight, e));
layer.addEventListener('mousedown', e => this._resize(ResizeMods.topRight, e));
})
)
}
mask.appendChild(container);
this.#mask = mask;
this._var.mask = mask;
return mask;
}
show(parent = document.body) {
show(parent = document.body, hidden = false) {
if (parent == null) {
return;
}
let mask = this.#mask ?? this.create();
parent.appendChild(mask);
if (this.#option.mask === false) {
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 (this._var.option.mask === false) {
// calculator position
const container = this.container;
container.style.left = String((parent.offsetWidth - container.offsetWidth) / 2) + 'px';
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.#mask?.querySelector('.ui-popup-body>.ui-popup-loading')?.style?.visibility === 'visible' }
set loading(flag) {
let loading = this.#mask?.querySelector('.ui-popup-body>.ui-popup-loading');
if (loading == null) {
_resize(mod, e) {
if (e.buttons !== 1) {
return;
}
if (flag === false) {
loading.style.visibility = 'hidden';
loading.style.opacity = 0;
} else {
loading.style.visibility = 'visible';
loading.style.opacity = 1;
}
}
#resize(mod, e) {
const container = this.container;
const option = this.#option;
const option = this._var.option;
if (typeof option.onResizeStarted === 'function') {
option.onResizeStarted.call(this);
}
const mask = this.#mask;
// this.#cursor = mask.style.cursor;
const mask = this._var.mask;
// this._var.cursor = mask.style.cursor;
// mask.style.cursor = Cursors[mod];
const originalX = e.clientX;
const originalY = e.clientY;
@ -342,7 +465,12 @@ class Popup {
const minWidth = option.minWidth ?? 200;
const minHeight = option.minHeight ?? 200;
let resized;
const parent = option.mask === false ? mask.parentElement : mask;
const move = e => {
if (e.buttons !== 1) {
parent.dispatchEvent(new MouseEvent('mouseup'));
return;
}
const offsetX = e.clientX - originalX;
const offsetY = e.clientY - originalY;
let width = original.width;
@ -386,12 +514,11 @@ class Popup {
}
resized = true;
}
const parent = option.mask === false ? mask.parentElement : mask;
parent.addEventListener('mousemove', move, { passive: false });
const up = () => {
parent.removeEventListener('mousemove', move, { passive: false });
parent.removeEventListener('mouseup', up);
// mask.style.cursor = this.#cursor;
// mask.style.cursor = this._var.cursor;
if (resized === true && typeof option.onResizeEnded === 'function') {
option.onResizeEnded.call(this);
}
@ -401,8 +528,6 @@ class Popup {
}
}
export default Popup;
export function createPopup(title, content, ...buttons) {
const popup = new Popup({
title,
@ -412,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',
@ -422,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,
@ -429,8 +592,9 @@ export function showAlert(title, message, iconType = 'info', parent = document.b
createIcon('fa-solid', iconTypes[iconType] ?? 'info-circle'),
createElement('span', span => span.innerText = message)
),
resolve,
buttons: [
{ text: r('ok', 'OK'), trigger: resolve }
{ text: r('ok', 'OK') }
]
});
popup.show(parent).then(mask => {
@ -441,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)) {
@ -452,34 +617,24 @@ export function showConfirm(title, content, buttons, iconType = 'question', pare
const popup = new Popup({
title,
content: wrapper,
buttons: buttons?.map(b => {
resolve,
buttons: buttons?.map((b, i) => {
return {
text: b.text,
trigger: p => {
let result;
if (typeof b.trigger === 'function') {
result = b.trigger(p, b);
if (typeof result?.then === 'function') {
return result.then(r => {
r !== false && resolve(r);
return r;
});
}
result !== false && resolve(result);
} else {
result = {
key: b.key,
popup: p
};
resolve(result);
result = b.key ?? i;
}
return result;
}
};
}) ??
[
{ text: r('yes', 'Yes'), trigger: p => resolve({ key: 'yes', popup: p }) },
{ text: r('no', 'No'), trigger: p => resolve({ key: 'no', popup: p }) }
{ key: 'yes', text: r('yes', 'Yes') },
{ key: 'no', text: r('no', 'No') }
]
});
popup.show(parent).then(mask => {

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

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

16
lib/ui/tooltip.d.ts vendored
View File

@ -1,2 +1,18 @@
/**
* 为元素设置一个 tooltip
* @param container 要设置 tooltip 的容器元素
* @param content 提示内容,可以是字符串也可以是 html 元素
* @param [flag=true] 是否仅在元素内容没有呈现完全时显示 tooltip
* @param [parent=null] 呈现在哪个父元素上,默认添加在 `container` 末端
*/
export function setTooltip(container: HTMLElement, content: string | HTMLElement, flag?: boolean, parent?: HTMLElement): HTMLElement
/**
* 解析容器元素内符合条件的子元素,为其添加 tooltip
* @param container 容器元素
* @example
* ```
* <span title="tooltip text"></span>
* ```
*/
export function resolveTooltip(container?: HTMLElement): HTMLElement

View File

@ -1,54 +0,0 @@
<div>
<h1>tooltip</h1>
<hr />
<p>
给某个元素或者页面上含有 title 属性的元素设置一个统一样式的 tooltip。
</p>
<h2>setTooltip</h2>
<code>function setTooltip(container: HTMLElement, content: string | HTMLElement, flag?: boolean, parent?: HTMLElement): void</code>
<h3>container: HTMLElement</h3>
<p>
要设置 tooltip 的元素
</p>
<h3>content: string | HTMLElement</h3>
<p>
要设置的 tooltip 内容,允许为字符串或者 HTML 元素
</p>
<h3>flag?: boolean</h3>
<p>
是否启用严格模式,只有显示不完整时才显示 tooltip
</p>
<h3>parent?: HTMLElement</h3>
<p>
创建在哪个元素内,默认创建在目标元素之内
</p>
<h2>resolveTooltip</h2>
<code>function resolveTooltip(container?: HTMLElement): HTMLElement</code>
<h3>container?: HTMLElement</h3>
<p>
给此元素,为 null 则把 document.body 下的所有含有 title 属性的子元素设置成统一样式的 tooltip
</p>
<hr />
<h2>示例</h2>
<pre>&lt;div id="tooltip-sample"&gt;
&lt;!-- 1 --&gt;
&lt;blockquote title="From MDN Website"&gt;To send an HTTP request, create an XMLHttpRequest object, open a URL, and
send the request. After the transaction completes, the object will contain useful information such as the
response body and the HTTP status of the result.&lt;/blockquote&gt;
&lt;!-- 2 --&gt;
&lt;button title="Test to send request through XMLHttpRequest."&gt;Test&lt;/button&gt;
&lt;/div&gt;
&lt;script type="text/javascript"&gt;
window["lib-ui"].resolveCheckbox(document.querySelector("#checkbox-sample"));
&lt;/script&gt;</pre>
<div id="tooltip-sample">
<blockquote title="From MDN Website">To send an HTTP request, create an XMLHttpRequest object, open a URL, and
send the request. After the transaction completes, the object will contain useful information such as the
response body and the HTTP status of the result.</blockquote>
<button title="Test to send request through XMLHttpRequest.">Test</button>
</div>
<script type="text/javascript">
window["lib-ui"].resolveTooltip(document.querySelector("#tooltip-sample"));
</script>
</div>

View File

@ -1,8 +1,10 @@
import './css/tooltip.scss';
import { createElement } from "../functions";
// import { global } from "../utility";
import { global } from '../utility';
function setTooltip(container, content, flag = false, parent = null) {
const pointerHeight = 12;
export function setTooltip(container, content, flag = false, parent = null) {
const isParent = parent instanceof HTMLElement;
if (isParent) {
const tipid = container.dataset.tipId;
@ -14,10 +16,11 @@ function setTooltip(container, content, flag = false, parent = null) {
}
const wrapper = createElement('div', wrapper => {
wrapper.className = 'ui-tooltip-wrapper ui-tooltip-color';
wrapper.style.visibility = 'hidden';
wrapper.style.opacity = 0;
wrapper.style.top = '0';
wrapper.style.left = '0';
// wrapper.style.visibility = 'hidden';
// wrapper.style.opacity = 0;
// wrapper.style.top = '0';
// wrapper.style.left = '0';
wrapper.style.cssText += 'display: none; visibility: hidden; opacity: 0; top: 0; left: 0';
},
createElement('div', 'ui-tooltip-pointer ui-tooltip-color'),
createElement('div', 'ui-tooltip-curtain ui-tooltip-color'),
@ -66,18 +69,92 @@ function setTooltip(container, content, flag = false, parent = null) {
}
}
p = c.parentElement;
const offsetParent = c.offsetParent;
while (p != null && p !== (isParent ? parent : offsetParent)) {
const offsetParent = isParent ? parent : c.offsetParent;
while (p != null && p !== offsetParent) {
left -= p.scrollLeft;
top -= p.scrollTop;
p = p.parentElement;
}
left += (c.offsetWidth - wrapper.offsetWidth) / 2;
top -= wrapper.offsetHeight + 14;
wrapper.style.left = `${left}px`;
wrapper.style.top = `${top}px`;
wrapper.style.visibility = 'visible';
wrapper.style.opacity = 1;
wrapper.style.display = '';
const offsetHeight = wrapper.offsetHeight;
const offsetWidth = wrapper.offsetWidth;
if (isParent) {
top -= offsetHeight + pointerHeight;
if (top < 0) {
top += c.offsetHeight + offsetHeight + pointerHeight * 2;
wrapper.classList.add('ui-tooltip-down');
}
left += (c.offsetWidth - offsetWidth) / 2;
if (left < 1) {
left = 1;
}
} else {
// check overflow
let t = c.offsetTop;
let l = c.offsetLeft;
p = c.offsetParent;
if (p == null) {
return;
}
let lastWidth = p.clientWidth;
let lastHeight = p.clientHeight;
while (p != null) {
const overflow = global.getComputedStyle(p).overflow;
if (overflow !== 'visible') {
break;
}
t += p.offsetTop;
l += p.offsetLeft;
const parent = p.offsetParent;
while (p != null) {
const w = p.clientWidth;
if (w < lastWidth) {
lastWidth += l;
} else {
lastWidth = p.clientWidth;
}
const h = p.clientHeight;
if (h < lastHeight) {
lastHeight += t;
} else {
lastHeight = p.clientHeight;
}
t -= p.scrollTop;
l -= p.scrollLeft;
if (p === parent) {
break;
}
p = p.parentElement;
}
}
if (t - offsetHeight - pointerHeight < 0) {
const containerOffsetHeight = c.offsetHeight;
if (t + containerOffsetHeight + offsetHeight + pointerHeight > lastHeight) {
top = t + (containerOffsetHeight - offsetHeight) / 2;
if (top + offsetHeight + 1 > lastHeight) {
top = lastHeight - offsetHeight - 1;
}
wrapper.classList.add('ui-tooltip-no');
} else {
top += containerOffsetHeight + pointerHeight;
wrapper.classList.add('ui-tooltip-down');
}
} else {
top -= offsetHeight + pointerHeight;
wrapper.classList.remove('ui-tooltip-down');
}
left += (c.offsetWidth - offsetWidth) / 2;
if (l - offsetWidth < 0) {
left = 1;
} else if (left + offsetWidth + 1 > lastWidth) {
left = lastWidth - offsetWidth - 1;
}
}
// wrapper.style.left = `${left}px`;
// wrapper.style.top = `${top}px`;
// wrapper.style.visibility = 'visible';
// wrapper.style.opacity = 1;
wrapper.style.cssText += `left: ${left}px; top: ${top}px; visibility: visible; opacity: 1`;
}, 100);
}
});
@ -86,12 +163,13 @@ function setTooltip(container, content, flag = false, parent = null) {
tid = setTimeout(() => {
wrapper.style.visibility = 'hidden';
wrapper.style.opacity = 0;
tid = setTimeout(() => wrapper.style.display = 'none', 120);
}, 300);
});
return container;
}
function resolveTooltip(container = document.body) {
export function resolveTooltip(container = document.body) {
const tips = container.querySelectorAll('[title]');
for (let tip of tips) {
const title = tip.getAttribute('title');
@ -101,9 +179,4 @@ function resolveTooltip(container = document.body) {
}
}
return container;
}
export {
setTooltip,
resolveTooltip
}

View File

@ -1,7 +1,7 @@
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 } from "./utility/strings";
import { nullOrEmpty, contains, endsWith, padStart, formatUrl, escapeHtml, escapeEmoji } from "./utility/strings";
let g = typeof globalThis !== 'undefined' ? globalThis : self;
@ -39,24 +39,39 @@ function truncate(v) {
return (v > 0 ? Math.floor : Math.ceil)(v);
}
function distinct(array, key, filter) {
const dict = Object.create(null);
for (let item of array) {
const v = typeof filter === 'function' ? filter(item) : item[key];
const val = v?.value ?? v;
if (!Object.prototype.hasOwnProperty.call(dict, val)) {
dict[val] = v;
}
}
return Object.values(dict);
}
function isEmail(text) {
return /^\w[-\w.+]*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(text);
}
function isPhone(text) {
return /^[1-9]\d{9,}$/.test(text);
if (/^[1-9]\d{9,}$/.test(text)) {
return true;
}
if (/^\+?[1-9][\d-]{9,}\d$/.test(text) && /^[1-9]\d{9,}$/.test(text.replace('+', '').replace(new RegExp('-', 'g'), ''))) {
return true;
}
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 {
@ -79,6 +94,7 @@ export {
padStart,
formatUrl,
escapeHtml,
escapeEmoji,
// variables
g as global,
isPositive,
@ -87,7 +103,9 @@ export {
throttle,
debounce,
truncate,
distinct,
isEmail,
isPhone
isPhone,
getPasswordStrength,
verifyPassword,
domLoad
}

View File

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

View File

@ -1,40 +0,0 @@
<div>
<h1>cookie</h1>
<hr />
<p>
cookie 操作工具类:获取、设置、删除。
</p>
<h2>getCookie</h2>
<code>function getCookie(name: string): string</code>
<h3>name: string</h3>
<p>
待获取的 cookie 关键字
</p>
<h2>setCookie</h2>
<code>function setCookie(name: string, value: string, expireDays?: Number): void</code>
<h3>name: string</h3>
<p>
待设置的 cookie 关键字
</p>
<h3>value: string</h3>
<p>
待设置的值
</p>
<h3>expireDays?: Number</h3>
<p>
有效期天数
</p>
<h2>deleteCookie</h2>
<code>function deleteCookie(name: string): void</code>
<h3>name: string</h3>
<p>
待删除的 cookie 关键字
</p>
<hr />
<h2>用法</h2>
<pre>const cookie = window["lib-utility"];
cookie.getCookie('user_id');
cookie.setCookie('user_id', 'guest');
cookie.deleteCookie('user_id');</pre>
</div>

View File

@ -1,8 +1,8 @@
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,10 +11,13 @@ 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}`;
}
function getCookie(name) {
export function getCookie(name) {
if (name == null) {
return null;
}
@ -22,19 +25,13 @@ function getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.indexOf(name) === 0) {
if (cookie.startsWith(name)) {
return decodeURIComponent(cookie.substring(name.length));
}
}
return null;
}
function deleteCookie(name) {
export function deleteCookie(name) {
setCookie(name, '', -1);
}
export {
getCookie,
setCookie,
deleteCookie
}

View File

@ -1,74 +0,0 @@
<div>
<h1>lgres</h1>
<hr />
<p>
语言资源工具类,用以设置页面以及脚本中的多语言。
</p>
<h2>r</h2>
<code>function r(key: string, defaultValue?: any): any</code>
<h3>key: string</h3>
<p>
语言资源的关键字
</p>
<h3>defaultValue?: any</h3>
<p>
资源的默认值,如无法获取该语言资源,则返回该值
</p>
<h2>lang</h2>
<code>const lang : {}</code>
<h3>get current(): string</h3>
<p>
返回当前语言 id
</p>
<h3>get unknownError(): string</h3>
<p>
未知错误的语言资源,默认为 <code>'An unknown error occurred, please contact the administrator.'</code>
</p>
<h3>get savedSuccessfully(): string</h3>
<p>
保存成功的语言资源,默认为 <code>'Saved successfully.'</code>
</p>
<h2>init</h2>
<code>function init(dom?: HTMLElement, options?: LgresOptions): Promise&lt;LanguageResource&gt;</code>
<h3>dom?: HTMLElement</h3>
<p>
待处理的元素,为空时处理整个页面
</p>
<h3>options?: LgresOptions</h3>
<p>
初始化参数,结构为
<pre>interface LgresOptions {
template?: string,
callback?: (result: any) => void
}</pre>
</p>
<h4>template?: string</h4>
<p>
语言资源文件的后缀,资源文件 url 为 <code>`language/${lgid}${template}`</code>
</p>
<h4>callback?: (result: any) => void</h4>
<p>
资源初始化后的回调函数,可能在 DOM 加载完成之前触发。
</p>
<h3>return: Promise&lt;LanguageResource&gt;</h3>
<p>
返回一个包含资源结果的 Promise将在 DOM 加载完成之后触发。
</p>
<p><code>LanguageResource</code> 结构为
<pre>interface LanguageResource {
r(key: string, defaultValue?: any): any
}</pre>
</p>
<hr />
<h2>用法</h2>
<pre>const lgres = window["lib-utility"];
lgres.init(document.body, {
template: '/res.json',
callback: res => document.title = res.r('title', 'Default Title')
}).then(res => {
document.querySelector('#header').innerText = res.r('header', 'My Header');
const msg = lgres.lang.unknownError;
document.querySelector('#message').innerText = lgres.lang.unknownError;
});</pre>
</div>

View File

@ -23,17 +23,21 @@ function getCurrentLgId() {
lgid = 'en';
}
switch (lgid) {
case 'en':
case 'en_au':
case 'fr':
case 'en_ca':
case 'fr_ca':
case 'zh_cn':
return lgid;
}
const lang = lgid.split('_')[0];
switch (lang) {
case 'en':
case 'es':
case 'fr':
case 'pt':
return lang;
case 'zh':
return 'zh_cn';
}
return 'en';
}
@ -47,7 +51,8 @@ function getStorageKey(lgid) {
async function doRefreshLgres(template = '') {
const lgid = getCurrentLgId();
const r = await get(`language/${lgid}${template}`);
const url = template.length > 0 ? template.replace('{lgid}', lgid) : `language/${lgid}`;
const r = await get(url);
const dict = await r.json();
localStorage.setItem(getStorageKey(lgid), JSON.stringify(dict));
return dict;
@ -87,18 +92,38 @@ 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 = '';
}
}
async function init(dom = document.body, options = {}) {
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));
let result;
@ -115,20 +140,7 @@ 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);
}
@ -139,14 +151,14 @@ async function init(dom = document.body, options = {}) {
}
}
function r(key, defaultValue) {
export function r(key, defaultValue) {
if (cache != null) {
return getLanguage(cache, key, defaultValue);
}
return defaultValue;
}
const lang = {
export const lang = {
get current() {
return getCurrentLgId();
},
@ -156,10 +168,4 @@ const lang = {
get savedSuccessfully() {
return r('savedSuccessfully', 'Saved successfully.');
}
}
export {
init,
r,
lang
}

View File

@ -2,7 +2,8 @@ interface RequestOptions {
method?: string;
accept?: string;
contentType?: string;
customerHeaders?: { [key: string]: string };
customHeaders?: { [key: string]: string };
mode?: RequestMode | undefined;
signal?: AbortSignal | null;
progress?: (this: XMLHttpRequestUpload, ev: ProgressEvent<XMLHttpRequestEventTarget>) => any
}

View File

@ -1,111 +0,0 @@
<div>
<h1>request</h1>
<hr />
<p>
网络请求工具类,可以实现比较常见的一些请求操作。
</p>
<h2>get</h2>
<code>function get(url: string, options?: RequestOptions): Promise&lt;Response&gt;</code>
<h3>url: string</h3>
<p>
url 地址
</p>
<h3>options?: RequestOptions</h3>
<p>
请求的配置参数,结构为
<pre>interface RequestOptions {
method?: string;
accept?: string;
contentType?: string;
customerHeaders?: { [key: string]: string };
signal?: AbortSignal | null;
progress?: (this: XMLHttpRequestUpload, ev: ProgressEvent&lt;XMLHttpRequestEventTarget&gt;) => any
}</pre>
</p>
<h4>method?: string</h4>
<p>
请求类型,默认为 GET 或 POST
</p>
<h4>accept?: string</h4>
<p>
Accept 请求头的值
</p>
<h4>contentType?: string</h4>
<p>
Content-Type 请求头的值
</p>
<h4>customerHeaders?: { [key: string]: string }</h4>
<p>
自定义请求头,例如
<pre>{
'X-Auth-Id': 'xxxxxx',
'X-Auth-Token': 'xxxxxx'
}</pre>
</p>
<h4>signal?: AbortSignal | null</h4>
<p>
终止器的信号,用来控制终止请求任务
</p>
<h4>progress?: (this: XMLHttpRequestUpload, ev: ProgressEvent&lt;XMLHttpRequestEventTarget&gt;) => any</h4>
<p>
调用 upload 方法时在上传过程中触发的事件,返回进度参数
</p>
<h3>return: Promise&lt;Response&gt;</h3>
<p>
返回一个结果为 Response 对象的 Promise
</p>
<h2>post</h2>
<code>function post(url: string, data?: BodyInit | null, options?: RequestOptions): Promise&lt;Response&gt;</code>
<h3>url: string</h3>
<p>
url 地址
</p>
<h3>data?: BodyInit | null</h3>
<p>
post 请求传递的请求正文,可以是 FormData 或者任意对象,后者会经 JSON 序列化后发送
</p>
<h3>options?: RequestOptions</h3>
<p>
请求的配置参数,结构如上述 get
</p>
<h3>return: Promise&lt;Response&gt;</h3>
<p>
返回一个结果为 Response 对象的 Promise
</p>
<h2>upload</h2>
<code>upload(url: string, data: FormData, options?: RequestOptions): Promise&lt;XMLHttpRequest&gt;</code>
<h3>url: string</h3>
<p>
url 地址
</p>
<h3>data: FormData</h3>
<p>
upload 请求传递的请求正文,仅支持 FormData
</p>
<h3>options?: RequestOptions</h3>
<p>
请求的配置参数,结构如上述 get仅使用其中 <code>progress</code><code>customerHeaders</code>
</p>
<h3>return: Promise&lt;XMLHttpRequest&gt;</h3>
<p>
返回一个结果为 XMLHttpRequest 对象的 Promise
</p>
<hr />
<h2>用法</h2>
<pre>const request = window["lib-utility"];
request.get('https://www.baidu.com')
.then(r => r.text())
.then(text => console.log(text));
request.post('api/query', { id: 101 })
.then(r => r.json())
.then(result => console.log(result.data));
request.upload('api/upload', data, {
progress: ev => {
console.log(`loaded: ${ev.loaded}, total: ${ev.total}`);
}
})
.then(() => console.log('done.'));</pre>
</div>

View File

@ -8,19 +8,20 @@ function combineUrl(url) {
return (consts.path || '') + url;
}
function get(url, options = {}) {
export function get(url, options = {}) {
return fetch(combineUrl(url), {
method: options.method || 'GET',
headers: {
...options.customerHeaders,
'Accept': options.accept || 'application/json'
...options.customHeaders,
'Accept': options.accept ?? 'application/json'
},
mode: options.mode,
signal: options.signal,
cache: 'default'
});
}
function post(url, data, options = {}) {
export function post(url, data, options = {}) {
// let contentType;
if (data instanceof FormData) {
// contentType = 'multipart/form-data';
@ -29,23 +30,23 @@ function post(url, data, options = {}) {
data = JSON.stringify(data);
}
// contentType = 'application/json';
if (options.customerHeaders == null) {
options.customerHeaders = {};
if (options.customHeaders == null) {
options.customHeaders = {};
}
if (options.customerHeaders['Content-Type'] == null) {
options.customerHeaders['Content-Type'] = 'application/json';
if (options.customHeaders['Content-Type'] == null) {
options.customHeaders['Content-Type'] = 'application/json';
}
}
return fetch(combineUrl(url), {
method: options.method || 'POST',
headers: options.customerHeaders,
headers: options.customHeaders,
body: data,
signal: options.signal,
cache: 'no-cache'
});
}
function upload(url, data, options = {}) {
export function upload(url, data, options = {}) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.onreadystatechange = function () {
@ -65,17 +66,11 @@ function upload(url, data, options = {}) {
}, false);
}
request.open('POST', combineUrl(url));
if (options.customerHeaders != null) {
for (let header of Object.entries(options.customerHeaders)) {
if (options.customHeaders != null) {
for (let header of Object.entries(options.customHeaders)) {
request.setRequestHeader(header[0], header[1]);
}
}
request.send(data);
});
}
export {
get,
post,
upload
}

View File

@ -3,4 +3,5 @@ export function contains(s: string, key: string | any, ignoreCase?: boolean): bo
export function endsWith(s: string, suffix: string): boolean
export function padStart(s: string, num: Number, char: string): boolean
export function formatUrl(msg: string): string
export function escapeHtml(text: string): string
export function escapeHtml(text: string): string
export function escapeEmoji(text: string): string

View File

@ -1,73 +0,0 @@
<div>
<h1>strings</h1>
<hr />
<p>
字符串操作工具类。
</p>
<h2>nullOrEmpty</h2>
<code>function nullOrEmpty(s?: string | any | null): boolean</code>
<h3>s?: string | any | null</h3>
<p>
待判断的对象,非字符串或者长度为 0 则返回 true
</p>
<h2>contains</h2>
<code>function contains(s: string, key: string | any, ignoreCase?: boolean): boolean</code>
<h3>s: string</h3>
<p>
待判断的字符串
</p>
<h3>key: string | any</h3>
<p>
检查字符串中是否含有该值,非字符串会先转换为字符串后再进行判断
</p>
<h3>ignoreCase?: boolean</h3>
<p>
判断时是否忽略大小写
</p>
<h2>endsWith</h2>
<code>function endsWith(s: string, suffix: string): boolean</code>
<h3>s: string</h3>
<p>
待判断的字符串
</p>
<h3>suffix: string</h3>
<p>
检查字符串是否以该字符串结尾
</p>
<h2>padStart</h2>
<code>function padStart(s: string, num: Number, char: string): boolean</code>
<h3>s: string</h3>
<p>
基础字符串
</p>
<h3>num: Number</h3>
<p>
对齐字符个数
</p>
<h3>char: string</h3>
<p>
用此字符串填充,使得字符串对齐,默认为 ' '
</p>
<h2>formatUrl</h2>
<code>function formatUrl(msg: string): string</code>
<h3>msg: string</h3>
<p>
把超链接解析替换为图标
</p>
<h2>escapeHtml</h2>
<code>function escapeHtml(text: string): string</code>
<h3>text: string</h3>
<p>
解析转换 html 代码为显示内容
</p>
<hr />
<h2>用法</h2>
<pre>const util = window["lib-utility"];
let s = 'test string';
console.log(util.nullOrEmpty(s)); // false
console.log(util.contains(s, 'abc')); // true
console.log(util.endsWith(s, 'string')); // true
s = util.padStart(s, 16, '0');
// '00000test string'</pre>
</div>

View File

@ -1,8 +1,8 @@
function nullOrEmpty(s) {
export function nullOrEmpty(s) {
return s == null || typeof s !== 'string' || s.length === 0;
}
function contains(s, key, ignoreCase) {
export function contains(s, key, ignoreCase) {
if (nullOrEmpty(s) || key == null) {
return false;
}
@ -10,26 +10,26 @@ function contains(s, key, ignoreCase) {
key = String(key);
}
if (ignoreCase) {
return s.toLowerCase().indexOf(key.toLowerCase()) >= 0;
return s.toLowerCase().includes(key.toLowerCase());
}
return s.indexOf(key) >= 0;
return s.includes(key);
}
function endsWith(s, suffix) {
export function endsWith(s, suffix) {
if (nullOrEmpty(s) || nullOrEmpty(suffix)) {
return false;
}
return s.indexOf(suffix) === s.length - suffix.length;
return s.endsWith(suffix); // === s.length - suffix.length;
}
function padStart(s, num, char) {
export function padStart(s, num, char) {
if (nullOrEmpty(s) || isNaN(num) || num <= s.length) {
return s;
}
return (char ?? ' ').repeat(num - s.length);
}
function formatUrl(msg) {
export function formatUrl(msg) {
//const urlReg = /(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?/ig;
//const urlArrray = str.match(urlReg);
const p = /(http|ftp|https):\/\/.+?(\s|\r\n|\r|\n|\"|\'|\*|$)/g;
@ -45,15 +45,23 @@ function formatUrl(msg) {
}
}
let path;
if (typeof consts !== 'undefined') {
path = consts.path;
} else if (typeof _network !== 'undefined') {
path = _network.root;
} else if (typeof _net !== 'undefined') {
path = _net.root;
}
for (let r of rs) {
msg = msg.replaceAll(r, '<a target="_blank" href="' + r + '"><svg><use xlink:href="' + ((typeof consts !== 'undefined' && consts.path) || '') + 'fonts/fa-regular.svg#link"></use></svg></a>');
msg = msg.replaceAll(r, `<a target="_blank" href="${r}"><svg><use xlink:href="${path || ''}fonts/fa-regular.svg#link"></use></svg></a>`);
}
}
return msg;
}
function escapeHtml(text) {
export function escapeHtml(text) {
if (text == null) {
return '';
}
@ -66,11 +74,14 @@ function escapeHtml(text) {
.replaceAll(' ', '&nbsp;');
}
export {
nullOrEmpty,
contains,
endsWith,
padStart,
formatUrl,
escapeHtml
export function escapeEmoji(text) {
if (text == null) {
return '';
}
if (typeof text !== 'string') {
text = String(text);
}
return text
.replace(/(=[A-Fa-f0-9]{2}){4}/g, s => decodeURIComponent(s.replaceAll('=', '%')))
.replace(/&#x([0-9a-fA-F]{2,6});/g, (_, h) => String.fromCodePoint(parseInt(h, 16)));
}

67
main.js
View File

@ -1,67 +0,0 @@
import './style.scss'
// import javascriptLogo from './javascript.svg'
import { get } from './lib/utility'
// document.querySelector('#js-logo').src = javascriptLogo
window.consts = {
path: '/',
resver: 20230329
}
function navigate(page) {
get(page, {
accept: 'text/html'
})
.then(r => r.text())
.then(html => {
const range = document.createRange();
range.selectNode(document.body);
const doc = range.createContextualFragment(html);
const container = document.querySelector('#container');
container.replaceChildren(doc);
container.scrollTop = 0;
});
}
document.querySelector('#directory').addEventListener('click', ev => {
const page = ev.target.dataset.page;
if (typeof page === 'string') {
location.hash = page;
navigate(page);
}
});
let page = location.hash;
if (page.length > 1) {
page = page.substring(1);
navigate(page);
}
/*
init(null, {
template: '/res.json',
callback: result => console.log(result)
}).then(() => {
// document.querySelector('#create-icon').appendChild(createIcon('fa-solid', 'user-edit'))
resolveIcon(document.querySelector('#create-icon'))
// document.querySelector('#create-checkbox').appendChild(createCheckbox({
// label: 'Switch 1'
// }))
resolveCheckbox(document.querySelector('#create-checkbox'))
resolveTooltip(document.querySelector('#buttons'))
document.querySelector('#button-fetch').addEventListener('click', () => {
get('javascript.svg', {
// contentType: '',
customerHeaders: {
'X-Auth': 'test/authentication'
}
})
.then(r => r.blob())
.then(blob => document.querySelector('#js-logo').src = URL.createObjectURL(blob));
});
});
*/

3029
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "ui-lib",
"private": true,
"version": "0.0.1",
"version": "1.0.7",
"type": "module",
"files": [
"dist"
@ -22,12 +22,20 @@
],
"scripts": {
"dev": "vite",
"build": "node ./vite.build.js"
"build": "node ./vite.build.js",
"doc": "typedoc",
"jsdoc": "jsdoc -c jsdoc.json",
"jsdoc-date": "jsdoc -c jsdoc-date.json"
},
"devDependencies": {
"postcss-preset-env": "^8.2.0",
"sass": "^1.60.0",
"vite": "^4.0.4",
"@mxssfd/typedoc-theme": "^1.1.6",
"clean-jsdoc-theme": "^4.3.0",
"docdash": "^2.0.2",
"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"
}
}

Binary file not shown.

20
sample/amrnb.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 430 KiB

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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.4 MiB

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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.2 MiB

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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.0 MiB

33
sample/index.html Normal file
View File

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

View File

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 995 B

View File

@ -0,0 +1,211 @@
{
"ver": "#{$X-Res-Ver}",
"title": "Fleet Intelligence\u2122 Internal",
"logout": "Logout",
"workOrder": "Work Orders You Are Following",
"periodCol": "Period:",
"openedDateCol": "Opened Date:",
"closedDateCol": "Closed Date:",
"to": "to",
"threeMonths": "Within three months",
"thisYear": "This year",
"lastYear": "Last Year",
"all": "All",
"allOpened": "All Opened",
"thisMonth": "This Month",
"lastMonth": "Last Month",
"thisQuarter": "This Quarter",
"lastQuarter": "Last Quarter",
"yearToDate": "Year To Date",
"custom": "Custom",
"refresh": "Refresh",
"exportExcel": "Export to Excel",
"report": "Report",
"allItem": "( All )",
"ok": "OK",
"reset": "Reset",
"yes": "Yes",
"no": "No",
"unknownError": "An unknown error occurred, please contact the administrator.",
"searchHolder": "Search...",
"workOrderNumber": "Work Order Number",
"workOrderNumberWidth": 170,
"workOrderType": "Work Order Type",
"workOrderTypeWidth": 190,
"assignedTo": "Assigned To",
"assignedToWidth": 190,
"dealerName": "Dealer Name",
"dealerNameWidth": 270,
"status": "Status",
"statusWidth": 180,
"createDate": "Created Date",
"createDateWidth": 130,
"completeDate": "Completed Date",
"completeDateWidth": 130,
"completed": "Completed",
"totalCost": "Total Cost",
"totalCostWidth": 100,
"vin": "VIN",
"vinWidth": 150,
"make": "Make",
"makeColon": "Make: ",
"makeWidth": 150,
"model": "Model",
"modelColon": "Model:",
"modelWidth": 150,
"type": "Type",
"typeWidth": 150,
"customerCode": "Customer Code",
"customerWidth": 180,
"userProfile": "User Profile",
"save": "Save",
"cancel": "Cancel",
"userName": "Display Name",
"mobilePhone": "Mobile Phone",
"timeZone": "Time Zone",
"language": "Language",
"companyName": "Company Name",
"companyNameWidth": 270,
"advisor": "Advisor",
"advisorWidth": 120,
"lastCommunicationText": "Last Communication Text",
"lastCommunicationTextWidth": 320,
"lastCommunicationBy": "Last Communication By",
"lastCommunicationByWidth": 190,
"lastCommunicationDate": "Last Communication Date",
"lastCommunicationDateWidth": 190,
"displayClosed": "Display Closed (90 days)",
"companyNameColon": "Company Name:",
"woNumberColon": "WO#:",
"savedSuccessfully": "Saved successfully.",
"viewWorkOrder": "Work Order - {0}",
"informationonAssetColon": "Information on Asset: ",
"workOrderColon": "Work Order: ",
"statusColon": "Status:",
"billingInformation": "Billing Information",
"estimateTotalAmountColon": "Estimate Total Amount: ",
"notesColon": "Notes: ",
"issuesIdentifiedinInspection": "Issues Identified in Inspection",
"identifiedIssues": "IDENTIFIED ISSUES",
"inspectionDetail": "INSPECTION DETAIL",
"noIdentifieds": "No Issues Identified in Inspection",
"severityLevelColon": "Severity Level: ",
"low": "Low",
"medium": "Medium",
"high": "High",
"noQuestions": "No Questions",
"workOrderAttachments": "Work Order Attachments",
"messages": "Customer Communication",
"internalComments": "Internal Comments",
"sentTo": "Sent To :",
"sent": "Sent",
"undelivered": "Undelivered",
"delivered": "Delivered",
"failed": "Failed",
"review": "Review",
"pending": "Pending",
"optOut": "Opt-Out",
"landline": "Landline",
"confirmSendMessage": "Message will be sent to:",
"confirmContinue": "Do you want to continue?",
"send": "Send",
"viewWorkOrderInspection": "View Work Order Inspection - {0}",
"viewFullInspection": "View Full Inspection",
"summary": "Summary",
"welcomeTitle": "Thank you for your business!",
"workOrderNumberColon": "Work Order Number:",
"openDateColon": "Open Date:",
"completedDateColon": "Completed Date:",
"approveRejectEstimate": "Approve/Reject Estimate",
"partsCostColon": "Parts Cost:",
"laborCostColon": "Labor Cost:",
"travelTimeCostColon": "Travel Time Cost:",
"otherCostColon": "Other Cost:",
"assetDetail": "Asset Detail",
"assetColon": "Asset:",
"hourMeterColon": "Hour Meter:",
"vinColon": "VIN:",
"typeColon": "Type:",
"odometerColon": "Odometer:",
"workOrderDetail": "Work Order Detail",
"viewApproveEstimate": "View/Approve Estimate",
"workOrderTypeColon": "Work Order Type:",
"customerReportedIssuesColon": "Issues:",
"workOrderTotalCostColon": "Work Order Total Cost:",
"estimateTotalCostColon": "Estimate Total Cost:",
"timeToCompleteColon": "Time To Complete(Hrs):",
"estimateDetail": "Estimate Detail",
"notesFromCustomerColon": "Notes from Customer:",
"approve": "Approve",
"reject": "Reject",
"approveEstimate": "Approve Estimate",
"rejectEstimate": "Reject Estimate",
"confirmApproveEstimate": "Are you sure you want to approve the estimate?",
"confirmRejectEstimate": "Are you sure you want to reject the estimate?",
"estimateStatusColon": "Estimate Status:",
"technicianNotesColon": "Technician Notes:",
"draft": "Draft",
"awaitingResponse": "Awaiting Response",
"closedByDealer": "Closed By Dealer",
"customerRejected": "Customer Rejected",
"customerApproved": "Customer Approved",
"estimateAttachments": "Estimate Attachments",
"photoDocuments": "Photos and Documentation",
"estimateNotPrepared": "Estimate Not Prepared",
"notAvailable": "N/A",
"signatureColon": "Signature:",
"signature": "Signature",
"requireSignature": "Please make a signature first.",
"clear": "Clear",
"approvedByColon": "Approved By:",
"phoneNumberColon": "Phone Number:",
"requireApprovedBy": "Please input the approver.",
"requirePhoneNumber": "Please input the phone number.",
"willWorkOurselves": "Will perform work ourselves",
"willWorkElsewhere": "Will have work done elsewhere",
"cost": "Cost",
"willNotWork": "Will not have work done",
"willWorkLater": "Will have work done at a later date",
"other": "Other",
"preferNotAnswer": "Prefer Not to Answer",
"reasonColon": "Reason:",
"termOfUse": "Terms of Use",
"requireReason": "Please select or input a reason.",
"submit": "Submit",
"survey": "Survey",
"thanksSurvey": "Your survey has been submitted successfully. Thank you for taking the survey.",
"surveyPrompt": "Please Rate the services that you received on work order {wonum} for your {make}/{model}",
"invoiceDetail": "Invoice Detail",
"awaitingPayment": "Awaiting Payment",
"paymentFailure": "Payment Failure",
"customerPaid": "Paid",
"invoiceStatusColon": "Invoice Status:",
"invoiceTotalCostColon": "Invoice Total Cost:",
"taxesColon": "Taxes:",
"invoiceNotPrepared": "Invoice Not Prepared",
"makePayment": "Make Payment",
"typeMessage": "Type your message here",
"typeComment": "Enter Message Here",
"poNumberColon": "PO Number:",
"requirePONumber": "Please Enter a PO Number.",
"void": "Void",
"nameColon": "Name:",
"requireCustomer": "Please select one customer contact at least.",
"openInApp": "Open In App",
"layout": "Layout",
"resetLayout": "Reset Layout",
"resetPivots": "Reset Pivots",
"confirmResetLayout": "Are you sure you want to reset the layout?",
"selectColumns": "Select Columns",
"column": "Column",
"caption": "Caption",
"postNote": "Post Note",
"postedByColon": "Posted by:",
"emailedToColon": "Emailed to:",
"sendMessage": "Send Message",
"editContacts": "Edit Contacts",
"autoUpdateEnabled": "Auto Updates Enabled",
"autoUpdateDisabled": "Auto Updates Disabled",
"statusLinkIncluded": "Status Link Included",
"statusLinkExcluded": "Status Link Excluded"
}

211
sample/language/en/res.json Normal file
View File

@ -0,0 +1,211 @@
{
"ver": "#{$X-Res-Ver}",
"title": "Fleet Intelligence\u2122 Internal",
"logout": "Logout",
"workOrder": "Work Orders You Are Following",
"periodCol": "Period:",
"openedDateCol": "Opened Date:",
"closedDateCol": "Closed Date:",
"to": "to",
"threeMonths": "Within three months",
"thisYear": "This year",
"lastYear": "Last Year",
"all": "All",
"allOpened": "All Opened",
"thisMonth": "This Month",
"lastMonth": "Last Month",
"thisQuarter": "This Quarter",
"lastQuarter": "Last Quarter",
"yearToDate": "Year To Date",
"custom": "Custom",
"refresh": "Refresh",
"exportExcel": "Export to Excel",
"report": "Report",
"allItem": "( All )",
"ok": "OK",
"reset": "Reset",
"yes": "Yes",
"no": "No",
"unknownError": "An unknown error occurred, please contact the administrator.",
"searchHolder": "Search...",
"workOrderNumber": "Work Order Number",
"workOrderNumberWidth": 170,
"workOrderType": "Work Order Type",
"workOrderTypeWidth": 190,
"assignedTo": "Assigned To",
"assignedToWidth": 190,
"dealerName": "Dealer Name",
"dealerNameWidth": 270,
"status": "Status",
"statusWidth": 180,
"createDate": "Created Date",
"createDateWidth": 130,
"completeDate": "Completed Date",
"completeDateWidth": 130,
"completed": "Completed",
"totalCost": "Total Cost",
"totalCostWidth": 100,
"vin": "VIN",
"vinWidth": 150,
"make": "Make",
"makeColon": "Make: ",
"makeWidth": 150,
"model": "Model",
"modelColon": "Model: ",
"modelWidth": 150,
"type": "Type",
"typeWidth": 150,
"customerCode": "Customer Code",
"customerWidth": 180,
"userProfile": "User Profile",
"save": "Save",
"cancel": "Cancel",
"userName": "Display Name",
"mobilePhone": "Mobile Phone",
"timeZone": "Time Zone",
"language": "Language",
"companyName": "Company Name",
"companyNameWidth": 270,
"advisor": "Advisor",
"advisorWidth": 120,
"lastCommunicationText": "Last Communication Text",
"lastCommunicationTextWidth": 320,
"lastCommunicationBy": "Last Communication By",
"lastCommunicationByWidth": 190,
"lastCommunicationDate": "Last Communication Date",
"lastCommunicationDateWidth": 190,
"displayClosed": "Display Closed (90 days)",
"companyNameColon": "Company Name:",
"woNumberColon": "WO#:",
"savedSuccessfully": "Saved successfully.",
"viewWorkOrder": "Work Order - {0}",
"informationonAssetColon": "Information on Asset: ",
"workOrderColon": "Work Order: ",
"statusColon": "Status:",
"billingInformation": "Billing Information",
"estimateTotalAmountColon": "Estimate Total Amount: ",
"notesColon": "Notes: ",
"issuesIdentifiedinInspection": "Issues Identified in Inspection",
"identifiedIssues": "IDENTIFIED ISSUES",
"inspectionDetail": "INSPECTION DETAIL",
"noIdentifieds": "No Issues Identified in Inspection",
"severityLevelColon": "Severity Level: ",
"low": "Low",
"medium": "Medium",
"high": "High",
"noQuestions": "No Questions",
"workOrderAttachments": "Work Order Attachments",
"messages": "Customer Communication",
"internalComments": "Internal Comments",
"sentTo": "Sent To :",
"sent": "Sent",
"undelivered": "Undelivered",
"delivered": "Delivered",
"failed": "Failed",
"review": "Review",
"pending": "Pending",
"optOut": "Opt-Out",
"landline": "Landline",
"confirmSendMessage": "Message will be sent to:",
"confirmContinue": "Do you want to continue?",
"send": "Send",
"viewWorkOrderInspection": "View Work Order Inspection - {0}",
"viewFullInspection": "View Full Inspection",
"summary": "Summary",
"welcomeTitle": "Thank you for your business!",
"workOrderNumberColon": "Work Order Number:",
"openDateColon": "Open Date:",
"completedDateColon": "Completed Date:",
"approveRejectEstimate": "Approve/Reject Estimate",
"partsCostColon": "Parts Cost:",
"laborCostColon": "Labor Cost:",
"travelTimeCostColon": "Travel Time Cost:",
"otherCostColon": "Other Cost:",
"assetDetail": "Asset Detail",
"assetColon": "Asset:",
"hourMeterColon": "Hour Meter:",
"vinColon": "VIN:",
"typeColon": "Type:",
"odometerColon": "Odometer:",
"workOrderDetail": "Work Order Detail",
"viewApproveEstimate": "View/Approve Estimate",
"workOrderTypeColon": "Work Order Type:",
"customerReportedIssuesColon": "Issues:",
"workOrderTotalCostColon": "Work Order Total Cost:",
"estimateTotalCostColon": "Estimate Total Cost:",
"timeToCompleteColon": "Time To Complete(Hrs):",
"estimateDetail": "Estimate Detail",
"notesFromCustomerColon": "Notes from Customer:",
"approve": "Approve",
"reject": "Reject",
"approveEstimate": "Approve Estimate",
"rejectEstimate": "Reject Estimate",
"confirmApproveEstimate": "Are you sure you want to approve the estimate?",
"confirmRejectEstimate": "Are you sure you want to reject the estimate?",
"estimateStatusColon": "Estimate Status:",
"technicianNotesColon": "Technician Notes:",
"draft": "Draft",
"awaitingResponse": "Awaiting Response",
"closedByDealer": "Closed By Dealer",
"customerRejected": "Customer Rejected",
"customerApproved": "Customer Approved",
"estimateAttachments": "Estimate Attachments",
"photoDocuments": "Photos and Documentation",
"estimateNotPrepared": "Estimate Not Prepared",
"notAvailable": "N/A",
"signatureColon": "Signature:",
"signature": "Signature",
"requireSignature": "Please make a signature first.",
"clear": "Clear",
"approvedByColon": "Approved By:",
"phoneNumberColon": "Phone Number:",
"requireApprovedBy": "Please input the approver.",
"requirePhoneNumber": "Please input the phone number.",
"willWorkOurselves": "Will perform work ourselves",
"willWorkElsewhere": "Will have work done elsewhere",
"cost": "Cost",
"willNotWork": "Will not have work done",
"willWorkLater": "Will have work done at a later date",
"other": "Other",
"preferNotAnswer": "Prefer Not to Answer",
"reasonColon": "Reason:",
"termOfUse": "Terms of Use",
"requireReason": "Please select or input a reason.",
"submit": "Submit",
"survey": "Survey",
"thanksSurvey": "Your survey has been submitted successfully. Thank you for taking the survey.",
"surveyPrompt": "Please Rate the services that you received on work order {wonum} for your {make}/{model}",
"invoiceDetail": "Invoice Detail",
"awaitingPayment": "Awaiting Payment",
"paymentFailure": "Payment Failure",
"customerPaid": "Paid",
"invoiceStatusColon": "Invoice Status:",
"invoiceTotalCostColon": "Invoice Total Cost:",
"taxesColon": "Taxes:",
"invoiceNotPrepared": "Invoice Not Prepared",
"makePayment": "Make Payment",
"typeMessage": "Type your message here",
"typeComment": "Enter Message Here",
"poNumberColon": "PO Number:",
"requirePONumber": "Please Enter a PO Number.",
"void": "Void",
"nameColon": "Name:",
"requireCustomer": "Please select one customer contact at least.",
"openInApp": "Open In App",
"layout": "Layout",
"resetLayout": "Reset Layout",
"resetPivots": "Reset Pivots",
"confirmResetLayout": "Are you sure you want to reset the layout?",
"selectColumns": "Select Columns",
"column": "Column",
"caption": "Caption",
"postNote": "Post Note",
"postedByColon": "Posted by:",
"emailedToColon": "Emailed to:",
"sendMessage": "Send Message",
"editContacts": "Edit Contacts",
"autoUpdateEnabled": "Auto Updates Enabled",
"autoUpdateDisabled": "Auto Updates Disabled",
"statusLinkIncluded": "Status Link Included",
"statusLinkExcluded": "Status Link Excluded"
}

211
sample/language/fr/res.json Normal file
View File

@ -0,0 +1,211 @@
{
"ver": "#{$X-Res-Ver}",
"title": "Fleet Intelligence\u2122 Internal",
"logout": "Se déconnecter",
"workOrder": "Ordres de travail que vous suivez",
"periodCol": "Période:",
"openedDateCol": "Date d'ouverture:",
"closedDateCol": "Date de fermeture:",
"to": "à",
"threeMonths": "Dans les trois mois",
"thisYear": "Cette année",
"lastYear": "L'année dernière",
"all": "Tout",
"allOpened": "Tout ouvert",
"thisMonth": "Ce mois-ci",
"lastMonth": "Le mois dernier",
"thisQuarter": "Ce trimestre",
"lastQuarter": "Dernier quart",
"yearToDate": "Année à ce jour",
"custom": "Personnalisé",
"refresh": "Rafraîchir",
"exportExcel": "Exporter vers Excel",
"report": "Signaler",
"allItem": "( Tout )",
"ok": "d'accord",
"reset": "Réinitialiser",
"yes": "Oui",
"no": "Non",
"unknownError": "Une erreur inconnue s'est produite, veuillez contacter l'administrateur.",
"searchHolder": "Chercher...",
"workOrderNumber": "Numéro de l'ordre de travail",
"workOrderNumberWidth": 215,
"workOrderType": "Type d'ordre de travail",
"workOrderTypeWidth": 200,
"assignedTo": "Assigné à",
"assignedToWidth": 190,
"dealerName": "Le nom du revendeur",
"dealerNameWidth": 270,
"status": "Statut",
"statusWidth": 180,
"createDate": "Date de création",
"createDateWidth": 150,
"completeDate": "Date de fin",
"completeDateWidth": 130,
"completed": "Complété",
"totalCost": "Coût total",
"totalCostWidth": 100,
"vin": "VIN",
"vinWidth": 150,
"make": "Faire",
"makeColon": "Faire: ",
"makeWidth": 150,
"model": "Modèle",
"modelColon": "Modèle: ",
"modelWidth": 150,
"type": "Taper",
"typeWidth": 150,
"customerCode": "Code client",
"customerWidth": 180,
"userProfile": "Profil de l'utilisateur",
"save": "Sauvegarder",
"cancel": "Annuler",
"userName": "Afficher un nom",
"mobilePhone": "Téléphone portable",
"timeZone": "Fuseau horaire",
"language": "Langue",
"companyName": "Nom de la compagnie",
"companyNameWidth": 270,
"advisor": "Conseiller",
"advisorWidth": 120,
"lastCommunicationText": "Dernier texte de communication",
"lastCommunicationTextWidth": 320,
"lastCommunicationBy": "Dernière communication par",
"lastCommunicationByWidth": 215,
"lastCommunicationDate": "Date de la dernière communication",
"lastCommunicationDateWidth": 240,
"displayClosed": "Affichage fermé (90 jours)",
"companyNameColon": "Nom de la compagnie:",
"woNumberColon": "WO#:",
"savedSuccessfully": "Enregistré avec succès.",
"viewWorkOrder": "Ordre de travail - {0}",
"informationonAssetColon": "Informations sur lactif : ",
"workOrderColon": "Ordre de travail: ",
"statusColon": "Statut:",
"billingInformation": "Informations de facturation",
"estimateTotalAmountColon": "Montant total estimatif: ",
"notesColon": "Notes: ",
"issuesIdentifiedinInspection": "Problèmes relevés lors de linspection",
"identifiedIssues": "PROBLÈMES IDENTIFIÉS",
"inspectionDetail": "DÉTAIL DE L'INSPECTION",
"noIdentifieds": "Aucun problème identifié lors de l'inspection",
"severityLevelColon": "Degré de gravité: ",
"low": "faible",
"medium": "dans",
"high": "haute",
"noQuestions": "Pas de problème.",
"workOrderAttachments": "Pièces jointes aux ordres de travail",
"messages": "Communication client",
"internalComments": "Commentaires internes",
"sentTo": "Envoyé à :",
"sent": "Expédié",
"undelivered": "Non livré",
"delivered": "Livré",
"failed": "Manqué",
"review": "La revue",
"pending": "En attente",
"optOut": "Se désengager",
"landline": "Téléphone fixe",
"confirmSendMessage": "Le message sera envoyé à :",
"confirmContinue": "Voulez-vous continuer ?",
"send": "Envoyer",
"viewWorkOrderInspection": "Afficher linspection des ordres de travail - {0}",
"viewFullInspection": "Voir linspection complète",
"summary": "Résumé",
"welcomeTitle": "Merci pour votre entreprise !",
"workOrderNumberColon": "Numéro de commande de travail :",
"openDateColon": "Date d'ouverture :",
"completedDateColon": "Date d'achèvement :",
"approveRejectEstimate": "Approuver/Rejeter l'estimation",
"partsCostColon": "Coût des pièces :",
"laborCostColon": "Coût du travail:",
"travelTimeCostColon": "Coût du temps de trajet :",
"otherCostColon": "Autre coût :",
"assetDetail": "Détail de l'actif",
"assetColon": "Actif:",
"hourMeterColon": "Compteur horaire :",
"vinColon": "VIN:",
"typeColon": "Taper:",
"odometerColon": "Odomètre:",
"workOrderDetail": "Détail de l'ordre de travail",
"viewApproveEstimate": "Voir/approuver l'estimation",
"workOrderTypeColon": "Type d'ordre de travail :",
"customerReportedIssuesColon": "Problèmes:",
"workOrderTotalCostColon": "Coût total de l'ordre de travail :",
"estimateTotalCostColon": "Estimer le coût total :",
"timeToCompleteColon": "Temps pour terminer (heures) :",
"estimateDetail": "Détail de l'estimation",
"notesFromCustomerColon": "Remarques du client :",
"approve": "Approuver",
"reject": "Rejeter",
"approveEstimate": "Approuver l'estimation",
"rejectEstimate": "Rejeter l'estimation",
"confirmApproveEstimate": "Êtes-vous sûr de vouloir approuver le devis ?",
"confirmRejectEstimate": "Êtes-vous sûr de vouloir rejeter le devis ?",
"estimateStatusColon": "État de l'estimation :",
"technicianNotesColon": "Notes du technicien :",
"draft": "Brouillon",
"awaitingResponse": "En attente de réponse",
"closedByDealer": "Fermé par le concessionnaire",
"customerRejected": "Client rejeté",
"customerApproved": "Approuvé par le client",
"estimateAttachments": "Estimer les pièces jointes",
"photoDocuments": "Photos et documentation",
"estimateNotPrepared": "Estimation non préparée",
"notAvailable": "N / A",
"signatureColon": "Signature:",
"signature": "Signature",
"requireSignature": "Veuillez d'abord faire une signature.",
"clear": "Dégager",
"approvedByColon": "Approuvé par:",
"phoneNumberColon": "Numéro de téléphone:",
"requireApprovedBy": "Veuillez saisir l'approbateur.",
"requirePhoneNumber": "Veuillez saisir le numéro de téléphone.",
"willWorkOurselves": "Effectuerons nous-mêmes le travail",
"willWorkElsewhere": "Aura le travail fait ailleurs",
"cost": "Coût",
"willNotWork": "N'aura pas de travail fait",
"willWorkLater": "Les travaux seront effectués à une date ultérieure",
"other": "Autre",
"preferNotAnswer": "Préfère ne pas répondre",
"reasonColon": "Raison:",
"termOfUse": "Conditions d'utilisation",
"requireReason": "Veuillez sélectionner ou saisir une raison.",
"submit": "Soumettre",
"survey": "Sondage",
"thanksSurvey": "Votre enquête a été soumise avec succès. Merci d'avoir répondu à l'enquête.",
"surveyPrompt": "Veuillez évaluer les services que vous avez reçus sur le bon de travail {wonum} pour votre {make}/{model}",
"invoiceDetail": "Détail de la facture",
"awaitingPayment": "En attente de paiement",
"paymentFailure": "Échec de paiement",
"customerPaid": "Payé",
"invoiceStatusColon": "État de la facture:",
"invoiceTotalCostColon": "Coût total de la facture:",
"taxesColon": "Impôts:",
"invoiceNotPrepared": "Facture non préparée",
"makePayment": "Effectuer le paiement",
"typeMessage": "Tapez votre message ici",
"typeComment": "Entrez le message ici",
"poNumberColon": "Numéro de bon de commande:",
"requirePONumber": "Veuillez saisir un numéro de bon de commande.",
"void": "Vide",
"nameColon": "Nom:",
"requireCustomer": "Veuillez sélectionner au moins un contact client.",
"openInApp": "Ouvrir dans l'application",
"layout": "Disposition",
"resetLayout": "Réinitialiser la mise en page",
"resetPivots": "Réinitialiser les pivots",
"confirmResetLayout": "Voulez-vous vraiment réinitialiser la mise en page?",
"selectColumns": "Sélectionnez les colonnes",
"column": "Colonne",
"caption": "Légende",
"postNote": "Note de poste",
"postedByColon": "Posté par:",
"emailedToColon": "Envoyé par e-mail à:",
"sendMessage": "Envoyer le message",
"editContacts": "Modifier les contacts",
"autoUpdateEnabled": "Mises à jour automatiques activées",
"autoUpdateDisabled": "Mises à jour automatiques désactivées",
"statusLinkIncluded": "Lien d'état inclus",
"statusLinkExcluded": "Lien de statut exclu"
}

View File

@ -0,0 +1,211 @@
{
"ver": "#{$X-Res-Ver}",
"title": "Fleet Intelligence\u2122 Internal",
"logout": "注销",
"workOrder": "您关注的工单",
"periodCol": "时期:",
"openedDateCol": "开启日期:",
"closedDateCol": "关闭日期:",
"to": "至",
"threeMonths": "三个月内",
"thisYear": "今年",
"lastYear": "去年",
"all": "全部",
"allOpened": "所有已打开",
"thisMonth": "这个月",
"lastMonth": "上个月",
"thisQuarter": "这个季度",
"lastQuarter": "上个季度",
"yearToDate": "今年迄今为止",
"custom": "自定义",
"refresh": "刷新",
"exportExcel": "导出到 Excel",
"report": "报告",
"allItem": "( 全部 )",
"ok": "确定",
"reset": "重置",
"yes": "是",
"no": "否",
"unknownError": "发生未知错误,请联系管理员。",
"searchHolder": "搜索……",
"workOrderNumber": "工单号",
"workOrderNumberWidth": 110,
"workOrderType": "工单类型",
"workOrderTypeWidth": 190,
"assignedTo": "分配给",
"assignedToWidth": 190,
"dealerName": "设施点",
"dealerNameWidth": 270,
"status": "状态",
"statusWidth": 180,
"createDate": "创建日期",
"createDateWidth": 130,
"completeDate": "完成日期",
"completeDateWidth": 130,
"completed": "已完成",
"totalCost": "总开销",
"totalCostWidth": 100,
"vin": "VIN",
"vinWidth": 150,
"make": "制造商",
"makeColon": "制造商:",
"makeWidth": 150,
"model": "型号",
"modelColon": "型号:",
"modelWidth": 150,
"type": "类型",
"typeWidth": 150,
"customerCode": "客户代码",
"customerWidth": 180,
"userProfile": "用户资料",
"save": "保存",
"cancel": "取消",
"userName": "显示名称",
"mobilePhone": "手机",
"timeZone": "时区",
"language": "语言",
"companyName": "公司名称",
"companyNameWidth": 270,
"advisor": "顾问",
"advisorWidth": 120,
"lastCommunicationText": "最后通讯文本",
"lastCommunicationTextWidth": 320,
"lastCommunicationBy": "最后沟通者",
"lastCommunicationByWidth": 190,
"lastCommunicationDate": "最后通讯日期",
"lastCommunicationDateWidth": 130,
"displayClosed": "显示已关闭90 天)",
"companyNameColon": "公司名称:",
"woNumberColon": "WO#",
"savedSuccessfully": "保存成功。",
"viewWorkOrder": "工单 - {0}",
"informationonAssetColon": "机器信息:",
"workOrderColon": "工单:",
"statusColon": "状态:",
"billingInformation": "计费信息",
"estimateTotalAmountColon": "评估总金额:",
"notesColon": "备注:",
"issuesIdentifiedinInspection": "检查中发现的问题",
"identifiedIssues": "严重问题",
"inspectionDetail": "检查细节",
"noIdentifieds": "检查未发现问题",
"severityLevelColon": "严重性等级:",
"low": "低",
"medium": "中",
"high": "高",
"noQuestions": "无问题",
"workOrderAttachments": "工单附件",
"messages": "客户沟通",
"internalComments": "内部评论",
"sentTo": "已发给:",
"sent": "已发送",
"undelivered": "未送达",
"delivered": "已送达",
"failed": "失败",
"review": "审查",
"pending": "待定",
"optOut": "离开",
"landline": "座机",
"confirmSendMessage": "消息将发送至:",
"confirmContinue": "您要继续吗?",
"send": "发送",
"viewWorkOrderInspection": "工单检查 - {0}",
"viewFullInspection": "查看完整检查",
"summary": "摘要",
"welcomeTitle": "感谢您的业务!",
"workOrderNumberColon": "工单编号:",
"openDateColon": "开放日期:",
"completedDateColon": "完成日期:",
"approveRejectEstimate": "批准/拒绝评估",
"partsCostColon": "零件成本:",
"laborCostColon": "劳动力成本:",
"travelTimeCostColon": "旅行时间成本:",
"otherCostColon": "其他费用:",
"assetDetail": "资产详情",
"assetColon": "资产:",
"hourMeterColon": "计时器:",
"vinColon": "VIN",
"typeColon": "类型:",
"odometerColon": "里程表:",
"workOrderDetail": "工单详情",
"viewApproveEstimate": "查看/批准评估",
"workOrderTypeColon": "工单类型:",
"customerReportedIssuesColon": "问题:",
"workOrderTotalCostColon": "工单总成本:",
"estimateTotalCostColon": "评估总成本:",
"timeToCompleteColon": "完成时间(小时):",
"estimateDetail": "评估详情",
"notesFromCustomerColon": "客户备注:",
"approve": "批准",
"reject": "拒绝",
"approveEstimate": "批准评估",
"rejectEstimate": "拒绝评估",
"confirmApproveEstimate": "您确定要批准估算吗?",
"confirmRejectEstimate": "您确定要拒绝估算吗?",
"estimateStatusColon": "评估状态:",
"technicianNotesColon": "技术员注意事项:",
"draft": "草稿",
"awaitingResponse": "等待答复",
"closedByDealer": "由经销商关闭",
"customerRejected": "顾客拒绝",
"customerApproved": "顾客同意",
"estimateAttachments": "评估附件",
"photoDocuments": "照片和文档",
"estimateNotPrepared": "评估未准备",
"notAvailable": "无",
"signatureColon": "签名:",
"signature": "签名",
"requireSignature": "请先签名。",
"clear": "清除",
"approvedByColon": "批准人:",
"phoneNumberColon": "电话号码:",
"requireApprovedBy": "请输入审批人。",
"requirePhoneNumber": "请输入电话号码。",
"willWorkOurselves": "将自己执行工作",
"willWorkElsewhere": "将在其他地方完成工作",
"cost": "成本",
"willNotWork": "将不会完成工作",
"willWorkLater": "将在以后完成工作",
"other": "其他",
"preferNotAnswer": "不想回答",
"reasonColon": "原因:",
"termOfUse": "使用条款",
"requireReason": "请选择或输入原因。",
"submit": "提交",
"survey": "调查问卷",
"thanksSurvey": "您的调查已成功提交。 感谢您参加调查。",
"surveyPrompt": "请为您的 {make}/{model} 的工单 {wonum} 获得的服务评分",
"invoiceDetail": "发票明细",
"awaitingPayment": "等待支付",
"paymentFailure": "支付失败",
"customerPaid": "已支付",
"invoiceStatusColon": "发票状态:",
"invoiceTotalCostColon": "发票总费用:",
"taxesColon": "税费:",
"invoiceNotPrepared": "未准备发票",
"makePayment": "付款",
"typeMessage": "在这里输入您的信息",
"typeComment": "在这里输入信息",
"poNumberColon": "订单号:",
"requirePONumber": "请输入采购订单号。",
"void": "无效",
"nameColon": "姓名:",
"requireCustomer": "请至少选择一位客户联系人。",
"openInApp": "在应用程序中打开",
"layout": "布局",
"resetLayout": "重置布局",
"resetPivots": "重置选择条件",
"confirmResetLayout": "您确定要重置布局吗?",
"selectColumns": "选择列",
"column": "列",
"caption": "标题",
"postNote": "发表评论",
"postedByColon": "发表者:",
"emailedToColon": "电邮至:",
"sendMessage": "发送信息",
"editContacts": "编辑联系人",
"autoUpdateEnabled": "自动更新已启用",
"autoUpdateDisabled": "自动更新已禁用",
"statusLinkIncluded": "已包含状态链接",
"statusLinkExcluded": "已排除状态链接"
}

350
sample/main.js Normal file
View File

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

1
sample/style.css Normal file

File diff suppressed because one or more lines are too long

View File

@ -163,11 +163,13 @@ h1 {
}
#container {
flex: 1 1 auto;
// flex: 1 1 auto;
width: 600px;
overflow: auto;
padding: 20px;
>div {
padding: 20px;
margin-right: 20px;
}
}

13
sample/web.config Normal file
View File

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

Some files were not shown because too many files have changed in this diff Show More