commit:升级到vue3,更新最近工作流技术栈,支持sa-token

This commit is contained in:
Jerry
2024-07-05 22:42:33 +08:00
parent bbcc608584
commit 565ecb6371
1751 changed files with 236790 additions and 0 deletions

View File

@@ -0,0 +1,240 @@
<template>
<el-container class="advance-query-form advance-query advance-box">
<el-aside width="300px" style="background-color: white">
<el-card class="base-card" shadow="never" style="height: 100%; border: none">
<template v-slot:header>
<div class="base-card-header">
<span class="header-title">{{ treePanel.title }}</span>
<div class="base-card-operation">
<el-button
class="advance-icon-btn"
@click="refreshGroup()"
style="width: 40px; height: 32px; padding: 0"
>
<img src="@/assets/img/refresh2.png" alt="" style="vertical-align: middle" />
</el-button>
<el-button
class="advance-icon-btn"
v-if="treePanel.supportAdd"
style="width: 40px; height: 32px; padding: 0"
@click="onEditGroupItem(null)"
>
<!-- {{treePanel.addText}} -->
<img src="@/assets/img/add.png" alt="" style="vertical-align: middle" />
</el-button>
</div>
</div>
</template>
<el-scrollbar :style="{ height: height - 130 + 'px' }" class="custom-scroll">
<el-tree
ref="groupTree"
:data="groupDataList"
:node-key="treePanel.keyColumnName"
@node-click="onNodeClick"
:highlight-current="true"
:default-expand-all="true"
:auto-expand-parent="true"
:expand-on-click-node="false"
>
<template v-slot="{ data }">
<el-row
class="tree-node-item module-node-item"
type="flex"
justify="space-between"
align="middle"
>
<span class="node-text" :title="data[treePanel.nameColumnName]">{{
data[treePanel.nameColumnName]
}}</span>
<div class="module-node-menu" style="padding-right: 9px">
<el-button
v-if="treePanel.supportEdit"
link
type="primary"
@click.stop="onEditGroupItem(data)"
:icon="Edit"
></el-button>
<el-button
v-if="treePanel.supportDelete"
link
type="danger"
@click.stop="onDeleteGroupItem(data)"
:icon="Delete"
></el-button>
</div>
</el-row>
</template>
</el-tree>
</el-scrollbar>
</el-card>
</el-aside>
<el-main class="table-panel">
<el-card
class="base-card"
shadow="never"
:body-style="{ padding: '0px' }"
style="border: none"
>
<template v-slot:header>
<div class="base-card-header">
<span class="header-title">{{ tablePanel.title }}</span>
<div class="base-card-operation">
<slot name="tableFilter" />
<el-button
v-if="tablePanel.supportAdd"
type="primary"
style="margin-left: 20px"
:size="layoutStore.defaultFormItemSize"
:icon="Plus"
@click="onAddTableItem()"
>{{ tablePanel.addText }}</el-button
>
</div>
</div>
</template>
<div class="advance-table-box">
<slot name="table" />
</div>
</el-card>
</el-main>
<slot />
</el-container>
</template>
<script setup lang="ts">
import { Edit, Delete, Plus } from '@element-plus/icons-vue';
import { inject, onMounted, ref, watch } from 'vue';
import { ANY_OBJECT } from '@/types/generic';
import { treeDataTranslate, findItemFromList } from '@/common/utils';
const emit = defineEmits<{
editGroupItem: [ANY_OBJECT];
refreshTable: [ANY_OBJECT];
deleteGroupItem: [ANY_OBJECT];
addTableItem: [];
}>();
const props = defineProps<{
height: number;
treePanel: ANY_OBJECT;
tablePanel: ANY_OBJECT;
}>();
import { useLayoutStore } from '@/store';
const layoutStore = useLayoutStore();
const groupTree = ref();
const currentGroup = ref();
const groupDataList = ref<ANY_OBJECT[]>([]);
const loadGroupData = () => {
if (typeof props.treePanel.loadFunction === 'function') {
props.treePanel
.loadFunction()
.then((dataList: ANY_OBJECT[]) => {
groupDataList.value = treeDataTranslate(
dataList.map((item: ANY_OBJECT) => {
return {
...item,
};
}),
props.treePanel.keyColumnName,
);
if (currentGroup.value != null) {
currentGroup.value = findItemFromList(
dataList,
currentGroup.value[props.treePanel.keyColumnName],
props.treePanel.keyColumnName,
);
}
if (currentGroup.value == null) currentGroup.value = dataList[0];
setTimeout(() => {
if (groupTree.value)
groupTree.value.setCurrentKey(currentGroup.value[props.treePanel.keyColumnName]);
}, 50);
})
.catch((e: Error) => {
console.warn(e);
});
}
};
const refreshGroup = () => {
loadGroupData();
};
const onEditGroupItem = (data: ANY_OBJECT) => {
emit('editGroupItem', data);
};
const onNodeClick = (data: ANY_OBJECT) => {
currentGroup.value = data;
};
const onDeleteGroupItem = (data: ANY_OBJECT) => {
emit('deleteGroupItem', data);
};
const onAddTableItem = () => {
emit('addTableItem');
};
defineExpose({ refreshGroup });
watch(
currentGroup,
(newVal, oldVal) => {
if (newVal != oldVal) {
emit('refreshTable', newVal);
}
},
{
deep: true,
},
);
onMounted(() => {
loadGroupData();
});
</script>
<style scoped>
.el-main {
display: flex;
flex-direction: column;
}
.table-panel {
padding: 0;
margin-left: 15px;
background: white;
}
.table-panel .base-card {
display: flex;
flex-direction: column;
flex: 1;
}
.advance-query :deep(.el-card__body) {
display: flex;
flex-direction: column;
flex: 1;
}
.advance-query :deep(.el-tree-node__content) {
height: 35px;
}
.advance-query .tree-node-item {
flex: 1;
height: 35px;
line-height: 35px;
font-size: 12px;
}
.advance-query .module-node-menu {
text-align: right;
}
.advance-query .tree-node-item .node-text {
overflow: hidden;
max-width: 175px;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.advance-table-box {
display: flex;
flex-direction: column;
flex: 1;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<el-button
v-bind="$attrs"
type="primary"
class="right-add-btn"
:size="layoutStore.defaultFormItemSize"
link
:icon="CirclePlusFilled"
>
<slot />
</el-button>
</template>
<script setup lang="ts">
import { CirclePlusFilled } from '@element-plus/icons-vue';
import { EpPropMergeType } from 'element-plus/es/utils';
import { useLayoutStore } from '@/store';
const layoutStore = useLayoutStore();
type BtnSizeType =
| EpPropMergeType<StringConstructor, '' | 'default' | 'small' | 'large', never>
| undefined;
const props = withDefaults(
defineProps<{
size?: BtnSizeType;
}>(),
{ size: 'default' },
);
</script>
<style lang="scss" scoped>
.right-add-btn {
display: flex;
align-items: center;
:deep(.el-icon-circle-plus) {
font-size: 14px !important;
}
:deep(span) {
margin-left: 4px !important;
}
}
</style>

View File

@@ -0,0 +1,271 @@
<template>
<div class="date-range">
<el-select
v-model="dateType"
:size="size"
style="min-width: 100px; max-width: 100px; margin-right: 10px"
v-if="!hideTypeOnlyOne || validTypeList.length > 1"
>
<el-option
v-for="type in validTypeList"
:key="type.value"
:value="type.value"
:label="type.label"
/>
</el-select>
<el-date-picker
style="flex-grow: 1"
v-model="currentDates"
:size="size"
:placeholder="placeholder"
:type="innerDateType"
:disabled="disabled"
:format="innerDateFormat"
:readonly="readonly"
:editable="editable"
:clearable="clearable"
:start-placeholder="startPlaceholder"
:end-placeholder="endPlaceholder"
:align="align"
:range-separator="rangeSeparator"
:value-format="valueFormat"
:prefix-icon="prefixIcon"
:clear-icon="clearIcon"
/>
</div>
</template>
<script setup lang="ts">
import { ref, Component } from 'vue';
import { CircleClose, Calendar } from '@element-plus/icons-vue';
import { ANY_OBJECT } from '@/types/generic';
type ModelValue = string | Date | [Date, Date] | [string, string];
const allTypeList = [
{
value: 'day',
label: '自然日',
},
{
value: 'month',
label: '自然月',
},
{
value: 'year',
label: '自然年',
},
];
const emit = defineEmits<{
'update:value': [ModelValue];
change: [ModelValue];
}>();
const props = withDefaults(
defineProps<{
value?: ModelValue;
// 默认显示的数据选择方式如果不存在与allowTypes中则显示allowTypes中的第一个
defaultDateType?: string;
// small / default / large
size?: '' | 'default' | 'small' | 'large' | undefined;
// 数据选择方式只有一个的时候是否隐藏数据选择方式下拉
hideTypeOnlyOne?: boolean;
// 允许的数据选择方式day, month, year
allowTypes?: string[];
// 是否范围选择
isRange?: boolean;
editable?: boolean;
clearable?: boolean;
// 对齐方式left, center, right
align?: string;
rangeSeparator?: string;
format?: string;
valueFormat?: string;
// 自定义头部图标的类名
prefixIcon?: Component;
// 自定义清空图标的类名
clearIcon?: Component;
// 非范围选择时的占位内容
placeholder?: string;
// 范围选择时开始日期的占位内容
startPlaceholder?: string;
//范围选择时结束日期的占位内容
endPlaceholder?: string;
// 完全只读
readonly?: boolean;
//禁用
disabled?: boolean;
}>(),
{
value: () => '',
defaultDateType: 'day',
size: 'default',
hideTypeOnlyOne: true,
allowTypes: () => ['day', 'month', 'year'],
isRange: true,
editable: true,
clearable: true,
align: 'left',
rangeSeparator: '-',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
prefixIcon: Calendar,
clearIcon: CircleClose,
},
);
const dateType = ref(props.defaultDateType);
const currentDates = computed({
get() {
return props.value;
},
set(val) {
emit('update:value', val);
},
});
const emitChange = () => {
let outputDate = [];
if (currentDates.value != null) {
if (!props.isRange) {
outputDate[0] = new Date(currentDates.value.toString());
outputDate[1] = new Date(currentDates.value.toString());
} else {
if (Array.isArray(currentDates.value) && currentDates.value.length == 2) {
outputDate[0] = new Date(currentDates.value[0]);
outputDate[1] = new Date(currentDates.value[1]);
}
}
if (outputDate[0] != null && outputDate[1] != null) {
outputDate[0].setHours(0, 0, 0, 0);
outputDate[1].setHours(0, 0, 0, 0);
switch (dateType.value) {
case 'day':
outputDate[1].setDate(outputDate[1].getDate() + 1);
break;
case 'month':
outputDate[1].setDate(1);
outputDate[0].setDate(1);
outputDate[1].setMonth(outputDate[1].getMonth() + 1);
break;
case 'year':
outputDate[1].setMonth(0);
outputDate[1].setDate(1);
outputDate[0].setMonth(0);
outputDate[0].setDate(1);
outputDate[1].setFullYear(outputDate[1].getFullYear() + 1);
break;
}
outputDate[1] = new Date(outputDate[1].getTime() - 1);
}
}
emit('update:value', outputDate as ModelValue);
emit('change', outputDate as ModelValue);
};
const validTypeList = computed(() => {
return allTypeList.filter(item => {
return props.allowTypes.indexOf(item.value) != -1;
});
});
/**
* el-date-picker使用的type
*/
const innerDateType = computed(() => {
switch (dateType.value) {
case 'day':
return props.isRange ? 'daterange' : 'date';
case 'month':
return props.isRange ? 'monthrange' : 'month';
case 'year':
return props.isRange ? 'monthrange' : 'year';
default:
return props.isRange ? 'daterange' : 'date';
}
});
/**
* el-date-picker使用的format
*/
const innerDateFormat = computed(() => {
switch (dateType.value) {
case 'day':
return 'YYYY-MM-DD';
case 'month':
return 'YYYY-MM';
case 'year':
return 'YYYY';
default:
return 'YYYY-MM-DD';
}
});
watch(
dateType,
(newValue, oldValue) => {
console.log('daterange dateType changed', newValue, oldValue);
if (props.allowTypes.indexOf(dateType.value) == -1) {
dateType.value = props.allowTypes[0] || 'day';
}
emitChange();
},
{
deep: true,
immediate: true,
},
);
watch(
() => props.value,
(newVal, oldVal) => {
console.log('daterange value change', newVal, oldVal);
if (newVal != oldVal) {
currentDates.value = newVal;
}
},
{
deep: true,
},
);
watch(
() => props.defaultDateType,
(newValue, oldValue) => {
console.log('daterange defaultDateType changed', newValue, oldValue);
if (props.allowTypes.indexOf(newValue) !== -1) {
dateType.value = newValue;
} else {
dateType.value = props.allowTypes[0];
}
},
{
deep: true,
immediate: true,
},
);
watch(
() => props.isRange,
(newValue, oldValue) => {
console.log('daterange isRange changed', newValue, oldValue);
if (newValue) {
if (currentDates.value && !Array.isArray(currentDates.value)) {
currentDates.value = [currentDates.value.toString(), currentDates.value.toString()];
}
} else if (Array.isArray(currentDates.value)) {
currentDates.value = currentDates.value[0] as ModelValue;
}
},
{
deep: true,
immediate: true,
},
);
</script>
<style type="scss" scoped>
.date-range {
display: flex;
}
</style>

View File

@@ -0,0 +1,240 @@
<template>
<div style="width: 100%">
<el-form
ref="formSysDept"
:model="formFilter"
label-width="75px"
size="default"
label-position="right"
@submit.prevent
>
<el-row type="flex" justify="space-between">
<el-form-item label="部门名称">
<el-input
v-model="formFilter.deptName"
style="width: 200px"
:clearable="true"
placeholder="部门名称"
@change="refreshFormSysDept(true)"
/>
</el-form-item>
<el-button type="primary" @click="onSubmit" style="height: 28px" size="default">
确定
</el-button>
</el-row>
</el-form>
<el-row>
<el-col :span="24">
<vxe-table
ref="table"
:row-id="dialogParams.props.value"
:data="formSysDeptWidget.dataList"
height="500"
show-overflow="title"
show-header-overflow="title"
:row-config="{ height: 35, isHover: true }"
:radio-config="{ highlight: true }"
:checkbox-config="getSelectConfig"
:tree-config="{
transform: true,
rowField: 'deptId',
parentField: 'parentId',
expandAll: true,
}"
@radio-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-change="onSelectChange"
>
<vxe-column :type="dialogParams.multiple ? 'checkbox' : 'radio'" :width="50" />
<vxe-column title="部门名称" field="deptName" tree-node />
<template v-slot:empty>
<div class="table-empty unified-font">
<img src="@/assets/img/empty.png" />
<span>暂无数据</span>
</div>
</template>
</vxe-table>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { VxeTable, VxeColumn, VxeTableDefines } from 'vxe-table';
import { reactive, ref, nextTick, computed, onMounted } from 'vue';
import { ANY_OBJECT } from '@/types/generic';
import { DialogProp } from '@/components/Dialog/types';
import { useTable } from '@/common/hooks/useTable';
import { TableOptions } from '@/common/types/pagination';
import { SysCommonBizController } from '@/api/system';
import { findItemFromList } from '@/common/utils';
import { useThirdParty } from '@/components/thirdParty/hooks';
import { ThirdProps } from '@/components/thirdParty/types';
interface IProps extends ThirdProps {
value: Array<ANY_OBJECT>;
props?: ANY_OBJECT;
multiple?: boolean;
// 当使用Dialog.show弹出组件时须定义该prop属性以便对dialog进行回调
dialog?: DialogProp<ANY_OBJECT | ANY_OBJECT[] | undefined>;
}
const props = withDefaults(defineProps<IProps>(), {
props: () => {
return {
label: 'deptName',
value: 'deptId',
};
},
multiple: false,
});
const { thirdParams, onCloseThirdDialog } = useThirdParty(props);
const table = ref();
const formFilter = reactive<{ deptName: string | undefined }>({ deptName: undefined });
const dialogSelectItems = ref<ANY_OBJECT | ANY_OBJECT[]>();
const dialogParams = computed(() => {
return {
value: props.value || thirdParams.value.value,
props:
props.props || thirdParams.value.props == null ? props.props : thirdParams.value.props || {},
multiple:
props.multiple || thirdParams.value.multiple == null
? props.multiple
: thirdParams.value.multiple,
};
});
const getSelectConfig = computed(() => {
let selectRowKeys =
dialogParams.value.multiple || Array.isArray(dialogSelectItems.value)
? (dialogSelectItems.value || []).map((item: ANY_OBJECT) => {
return item[dialogParams.value.props.value];
})
: (dialogSelectItems.value || {})[dialogParams.value.props.value];
return {
highlight: true,
checkStrictly: true,
checkRowKeys: dialogParams.value.multiple ? selectRowKeys : undefined,
checkRowKey: dialogParams.value.multiple ? undefined : selectRowKeys,
showHeader: false,
};
});
const setTableSelectRow = () => {
if (table.value == null || !Array.isArray(formSysDeptWidget.dataList)) return;
setTimeout(() => {
table.value.clearRadioRow();
table.value.clearCheckboxRow();
if (dialogParams.value.multiple || Array.isArray(dialogSelectItems.value)) {
table.value.setCheckboxRow(
formSysDeptWidget.dataList.filter(item => {
return (
findItemFromList(
dialogSelectItems.value as ANY_OBJECT[],
item[dialogParams.value.props.value],
dialogParams.value.props.value,
) != null
);
}),
true,
);
} else {
let selectRow = findItemFromList(
formSysDeptWidget.dataList,
(dialogSelectItems.value || {})[dialogParams.value.props.value],
dialogParams.value.props.value,
);
table.value.setRadioRow(selectRow);
}
}, 50);
};
const loadSysDeptData = (params: ANY_OBJECT) => {
params.widgetType = 'upms_dept';
params.filter = {
deptName: formFilter.deptName,
};
return new Promise((resolve, reject) => {
SysCommonBizController.list(params)
.then(res => {
resolve({
dataList: res.data.dataList,
totalCount: res.data.totalCount,
});
nextTick(() => {
setTableSelectRow();
});
})
.catch((e: Error) => {
reject(e);
});
});
};
const loadSysDeptVerify = () => {
return true;
};
const tableOptions: TableOptions<ANY_OBJECT> = {
loadTableData: loadSysDeptData,
verifyTableParameter: loadSysDeptVerify,
};
const formSysDeptWidget = reactive(useTable(tableOptions));
const onSubmit = () => {
if (props.dialog) {
props.dialog.submit(dialogSelectItems.value);
} else {
onCloseThirdDialog(true, dialogParams.value.value, dialogSelectItems.value);
}
};
const refreshFormSysDept = (reloadData = false) => {
// 重新获取数据组件的数据
if (reloadData) {
formSysDeptWidget.refreshTable(true, 1);
} else {
formSysDeptWidget.refreshTable();
}
};
const onSelectChange = ({
checked,
row,
}: VxeTableDefines.RadioChangeEventParams | VxeTableDefines.CheckboxAllEventParams) => {
if (dialogParams.value.multiple) {
if (row == null) {
dialogSelectItems.value = [];
if (checked) {
dialogSelectItems.value = formSysDeptWidget.dataList;
}
} else {
if (dialogSelectItems.value == null) dialogSelectItems.value = [];
if (checked) {
dialogSelectItems.value.push(row);
} else {
dialogSelectItems.value = dialogSelectItems.value.filter((item: ANY_OBJECT) => {
return item[dialogParams.value.props.value] !== row[dialogParams.value.props.value];
});
}
}
} else {
dialogSelectItems.value = row;
}
};
onMounted(() => {
if (Array.isArray(dialogParams.value.value) && dialogParams.value.value.length > 0) {
if (dialogParams.value.multiple) {
dialogSelectItems.value = dialogParams.value.value.map(item => {
return {
...item,
};
});
} else {
dialogSelectItems.value = {
...dialogParams.value.value[0],
};
}
}
refreshFormSysDept(true);
});
</script>

View File

@@ -0,0 +1,211 @@
<template>
<div class="dept-select">
<el-select
:model-value="value"
style="width: 100%"
:multiple="multiple"
:disabled="disabled"
:size="size"
:clearable="clearable"
:collapse-tags="collapseTags"
:placeholder="placeholder"
:teleported="false"
popper-class="dept-select-popper"
@visible-change="onVisibleChange"
@remove-tag="onRemoveTag"
@clear="onClear"
>
<el-option
v-for="item in selectedItems"
:key="item[pps.value]"
:label="item[pps.label]"
:value="item[pps.value]"
/>
</el-select>
</div>
</template>
<script setup lang="ts">
import { getUUID } from '@/common/utils';
import { ANY_OBJECT } from '@/types/generic';
import { SysCommonBizController } from '@/api/system';
import { Dialog } from '@/components/Dialog';
import DeptSelectDlg from './DeptSelectDlg.vue';
const emit = defineEmits<{ input: [ANY_OBJECT]; change: [ANY_OBJECT] }>();
const props = withDefaults(
defineProps<{
value: string | number | Array<ANY_OBJECT>;
size?: '' | 'default' | 'small' | 'large';
placeholder?: string;
props?: ANY_OBJECT;
multiple?: boolean;
disabled?: boolean;
clearable?: boolean;
collapseTags?: boolean;
}>(),
{
props: () => {
return {
label: 'deptName',
value: 'deptId',
};
},
multiple: false,
disabled: false,
clearable: false,
collapseTags: true,
},
);
const widgetId = ref(getUUID());
const selectedItems = ref<ANY_OBJECT[]>([]);
const pps = computed(() => props.props);
const refreshData = (data: ANY_OBJECT) => {
if (data.path === 'thirdSelectDept/' + widgetId.value && data.isSuccess) {
handlerEditOperate(data.data);
}
};
const handlerEditOperate = (items: Ref<ANY_OBJECT>) => {
console.log('DeptSelect > handlerEditOperate', items);
selectedItems.value = [];
if (props.multiple) {
if (Array.isArray(items.value)) selectedItems.value = items.value;
} else {
if (items.value != null) selectedItems.value.push(items.value);
}
if (!checkSelectChange()) return;
emitChange();
};
const onVisibleChange = (visible: boolean) => {
if (visible) {
Dialog.show<Ref<ANY_OBJECT>>(
'部门选择',
DeptSelectDlg,
{
area: ['900px', '650px'],
offset: '100px',
},
{
value: selectedItems.value,
props: props.props,
path: 'thirdSelectDept/' + widgetId.value,
multiple: props.multiple,
},
{
width: '900px',
height: '650px',
pathName: '/thirdParty/thirdSelectDept',
},
).then(res => {
handlerEditOperate(res);
});
}
};
const onRemoveTag = (val: ANY_OBJECT) => {
selectedItems.value = selectedItems.value.filter(item => {
return item[props.props.value] !== val;
});
emitChange();
};
const onClear = () => {
selectedItems.value = [];
emitChange();
};
const emitChange = () => {
let tempValue;
if (props.multiple) {
tempValue = selectedItems.value.map(item => {
return item[props.props.value];
});
} else {
tempValue = (selectedItems.value[0] || {})[props.props.value];
}
emit('input', tempValue);
emit('change', tempValue);
};
const checkSelectChange = () => {
let valueIdString =
props.multiple && Array.isArray(props.value)
? (props.value || [])
.sort((val1: ANY_OBJECT, val2: ANY_OBJECT) => {
if (val1 === val2) return 0;
return val1 < val2 ? -1 : 1;
})
.join(',')
: props.value || '';
let selectedItemsString = selectedItems.value
.sort((item1, item2) => {
if (item1[props.props.value] === item2[props.props.value]) return 0;
return item1[props.props.value] < item2[props.props.value] ? -1 : 1;
})
.map(item => item[props.props.value])
.join(',');
return valueIdString !== selectedItemsString;
};
const getSelectDeptList = () => {
let params: ANY_OBJECT = {
widgetType: 'upms_dept',
};
if (props.value == null || props.value === '' || props.value.length <= 0)
selectedItems.value = [];
if (props.multiple) {
params.fieldValues = Array.isArray(props.value) ? props.value : [];
} else {
params.fieldValues = Array.isArray(props.value) ? props.value[0] : props.value;
params.fieldValues = params.fieldValues == null ? [] : [params.fieldValues];
}
if (Array.isArray(params.fieldValues) && params.fieldValues.length > 0) {
params.fieldValues = params.fieldValues.join(',');
SysCommonBizController.viewByIds(params, {
showMask: false,
})
.then(res => {
if (Array.isArray(res.data)) {
selectedItems.value = res.data;
}
})
.catch(e => {
console.warn(e);
});
}
};
watch(
() => props.value,
() => {
if (props.value) getSelectDeptList();
},
{
immediate: true,
},
);
defineExpose({
refreshData,
});
</script>
<style scoped>
.dept-select :deep(.dept-select-popper) {
display: none;
}
.dept-select :deep(.el-dialog__header) {
height: 42px;
line-height: 42px;
padding: 0 20px;
background-color: #f8f8f8;
}
.dept-select :deep(.el-dialog__title) {
font-size: 14px;
color: #333;
}
.dept-select :deep(.el-dialog__headerbtn) {
top: 12px;
}
.dept-select :deep(.el-dialog__body) {
padding: 25px;
}
</style>

View File

@@ -0,0 +1,164 @@
import { layer } from '@layui/layui-vue';
import { Component } from 'vue';
import { ANY_OBJECT } from '@/types/generic';
import { getAppId, getToken, getUUID } from '@/common/utils';
import { DialogProp } from './types';
import Layout from './layout.vue';
const LAYER_Z_INDEX = 500;
export class Dialog {
private static index = 0;
static closeAll = () => {
layer.closeAll();
Dialog.index = 0;
};
// 未提供单独关闭某个对话框的方法涉及到z-index的变化规则若需提供须考虑z-index的变化规则
// options可参考http://www.layui-vue.com/zh-CN/components/layer 和 https://layui.dev/docs/2/layer/#options
/**
* 打开弹窗
* @param {*} title 弹窗标题
* @param {*} component 弹窗内容的组件
* @param {*} options 弹窗设置详情请见layui官网 http://www.layui-vue.com/zh-CN/components/layer 和 https://layui.dev/docs/2/layer/#options
* @param {*} params 弹窗组件参数
* @param {*} thirdPartyOptions 第三方接入参数
* @param {*} thirdPartyOptions.pathName 接入路由name
* @param {*} thirdPartyOptions.width 弹窗宽度
* @param {*} thirdPartyOptions.height 弹窗高度
*/
static show = <D>(
title: string,
component: Component | string,
options?: ANY_OBJECT,
params?: ANY_OBJECT,
thirdPartyOptions?: ANY_OBJECT,
) => {
// 调用第三方弹窗方法
if (getAppId() != null && getAppId() !== '') {
if (thirdPartyOptions && window.parent) {
showDialog(title, params, thirdPartyOptions);
return new Promise<D>((resolve, reject) => {
const eventListener = (e: ANY_OBJECT) => {
if (e.data.type === 'refreshData') {
console.log('第三方弹窗关闭后,回传的数据', e);
window.removeEventListener('message', eventListener);
resolve(e.data.data?.data as D);
}
};
window.addEventListener('message', eventListener, false);
});
} else {
console.warn('错误的第三方调用!');
return Promise.reject('错误的第三方调用!');
}
}
return new Promise<D>((resolve, reject) => {
const observer: DialogProp<D> = {
index: '',
cancel: () => {
layer.close(observer.index);
reject({ message: 'canceled' });
},
submit: (data: D) => {
//console.log('dialog index', observer.index);
layer.close(observer.index);
resolve(data);
},
};
let layerOptions = {
title: title,
type: 1,
skin:
'layer-dialog ' + (window.innerWidth <= 1900 ? 'container-default' : 'container-large'),
resize: false,
offset: 'auto',
shadeClose: false,
content: '' as string | Component,
zIndex: LAYER_Z_INDEX + Dialog.index,
end: () => {
//console.log('layer end');
Dialog.index--;
},
};
// end之后要执行index--
if (options && options.end) {
const end = options.end;
layerOptions.end = () => {
Dialog.index--;
if (typeof end == 'function') {
end();
}
};
}
layerOptions = { ...layerOptions, ...options };
params = { ...params };
params.dialog = observer;
console.log('dialog params', params);
//layerOptions.content = h(component, params);
layerOptions.content = h(Layout, () => h(component, params));
const id = layer.open(layerOptions);
observer.index = id;
Dialog.index++;
});
};
}
function showDialog(title: string, params?: ANY_OBJECT, options?: ANY_OBJECT) {
console.log('第三方弹窗', title, params, options);
// 调用第三方弹窗方法
if (options && window.parent) {
const paramsCopy: ANY_OBJECT = {};
if (params) {
for (const key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
const element = params[key];
paramsCopy[key] = unref(element);
}
}
}
const dialogKey = getUUID();
const location = window.location;
let dlgUrl =
location.origin +
location.pathname +
'#' +
options.pathName +
'?appId=' +
getAppId() +
'&token=' +
getToken() +
'&dlgFullScreen=' +
(options.fullscreen ? '1' : '0') +
'&dialogKey=' +
dialogKey;
dlgUrl += '&thirdParamsString=' + encodeURIComponent(JSON.stringify(paramsCopy));
const data = {
title: title,
dlgFullScreen: options.fullscreen,
width: options.width,
height: options.height,
top: options.top,
params: paramsCopy,
url: dlgUrl,
dialogKey: dialogKey,
};
const dlgOption = {
type: 'openDialog',
data: JSON.parse(JSON.stringify(data)),
};
window.parent.postMessage(dlgOption, '*');
}
}

View File

@@ -0,0 +1,7 @@
<template>
<el-config-provider :locale="zhCn"><slot /></el-config-provider>
</template>
<script setup lang="ts">
import zhCn from 'element-plus/dist/locale/zh-cn.mjs';
</script>

View File

@@ -0,0 +1,5 @@
export interface DialogProp<T> {
index: string;
cancel: () => void;
submit: (data: T) => void;
}

View File

@@ -0,0 +1,28 @@
import { EpPropMergeType } from 'element-plus/es/utils/vue/props';
import { Dialog } from '@/components/Dialog';
import { ANY_OBJECT } from '@/types/generic';
export const useDialog = () => {
const defaultFormItemSize = inject<
EpPropMergeType<StringConstructor, '' | 'default' | 'small' | 'large', never> | undefined
>('defaultFormItemSize', 'default');
const show = <D>(
title: string,
component: Component | string,
options?: ANY_OBJECT,
params?: ANY_OBJECT,
thirdPartyOptions?: ANY_OBJECT,
) => {
if (!params) {
params = {};
}
params.defaultFormItemSize = defaultFormItemSize;
return Dialog.show<D>(title, component, options, params, thirdPartyOptions);
};
return {
show,
};
};

View File

@@ -0,0 +1,77 @@
<template>
<el-row class="flex-box" type="flex">
<slot />
<div class="search-box" v-if="hasSearch" :style="{ 'min-width': minMenuWidth + 'px' }">
<el-button class="search-btn" type="default" plain @click="search" :icon="ElIconSearch"
>查询</el-button
>
<el-button v-if="hasReset" type="default" plain @click="reset" style="width: 72px"
>重置</el-button
>
<div style="float: right">
<slot name="operation" />
</div>
</div>
</el-row>
</template>
<script setup lang="ts">
import { Search as ElIconSearch } from '@element-plus/icons-vue';
const emit = defineEmits<{
(e: 'reset'): void;
(e: 'search'): void;
}>();
withDefaults(
defineProps<{
hasSearch?: boolean;
hasReset?: boolean;
minMenuWidth?: number;
}>(),
{
hasSearch: true,
hasReset: true,
// 这个值在size为default时会导致某些页面按钮折行
minMenuWidth: 300,
},
);
const search = () => {
emit('search');
};
const reset = () => {
emit('reset');
};
</script>
<style lang="scss" scoped>
.flex-box {
padding: 16px 24px 0;
margin-bottom: 16px;
background-color: white;
.search-btn {
color: $color-primary;
border-color: $color-primary;
&:hover {
background-color: $color-primary-light-9;
}
}
:deep(.el-form-item) {
margin-right: 8px;
margin-bottom: 16px;
}
.extend-box {
img {
cursor: pointer;
margin-left: 8px;
}
}
.search-box {
flex-shrink: 0;
padding-left: 8px;
margin-bottom: 16px;
flex-grow: 1;
}
}
</style>

View File

@@ -0,0 +1,280 @@
[
"el-icon-delete-solid",
"el-icon-delete",
"el-icon-s-tools",
"el-icon-setting",
"el-icon-user-solid",
"el-icon-user",
"el-icon-phone",
"el-icon-phone-outline",
"el-icon-more",
"el-icon-more-outline",
"el-icon-star-on",
"el-icon-star-off",
"el-icon-s-goods",
"el-icon-goods",
"el-icon-warning",
"el-icon-warning-outline",
"el-icon-question",
"el-icon-info",
"el-icon-remove",
"el-icon-circle-plus",
"el-icon-success",
"el-icon-error",
"el-icon-zoom-in",
"el-icon-zoom-out",
"el-icon-remove-outline",
"el-icon-circle-plus-outline",
"el-icon-circle-check",
"el-icon-circle-close",
"el-icon-s-help",
"el-icon-help",
"el-icon-minus",
"el-icon-plus",
"el-icon-check",
"el-icon-close",
"el-icon-picture",
"el-icon-picture-outline",
"el-icon-picture-outline-round",
"el-icon-upload",
"el-icon-upload2",
"el-icon-download",
"el-icon-camera-solid",
"el-icon-camera",
"el-icon-video-camera-solid",
"el-icon-video-camera",
"el-icon-message-solid",
"el-icon-bell",
"el-icon-s-cooperation",
"el-icon-s-order",
"el-icon-s-platform",
"el-icon-s-fold",
"el-icon-s-unfold",
"el-icon-s-operation",
"el-icon-s-promotion",
"el-icon-s-home",
"el-icon-s-release",
"el-icon-s-ticket",
"el-icon-s-management",
"el-icon-s-open",
"el-icon-s-shop",
"el-icon-s-marketing",
"el-icon-s-flag",
"el-icon-s-comment",
"el-icon-s-finance",
"el-icon-s-claim",
"el-icon-s-custom",
"el-icon-s-opportunity",
"el-icon-s-data",
"el-icon-s-check",
"el-icon-s-grid",
"el-icon-menu",
"el-icon-share",
"el-icon-d-caret",
"el-icon-caret-left",
"el-icon-caret-right",
"el-icon-caret-bottom",
"el-icon-caret-top",
"el-icon-bottom-left",
"el-icon-bottom-right",
"el-icon-back",
"el-icon-right",
"el-icon-bottom",
"el-icon-top",
"el-icon-top-left",
"el-icon-top-right",
"el-icon-arrow-left",
"el-icon-arrow-right",
"el-icon-arrow-down",
"el-icon-arrow-up",
"el-icon-d-arrow-left",
"el-icon-d-arrow-right",
"el-icon-video-pause",
"el-icon-video-play",
"el-icon-refresh",
"el-icon-refresh-right",
"el-icon-refresh-left",
"el-icon-finished",
"el-icon-sort",
"el-icon-sort-up",
"el-icon-sort-down",
"el-icon-rank",
"el-icon-loading",
"el-icon-view",
"el-icon-c-scale-to-original",
"el-icon-date",
"el-icon-edit",
"el-icon-edit-outline",
"el-icon-folder",
"el-icon-folder-opened",
"el-icon-folder-add",
"el-icon-folder-remove",
"el-icon-folder-delete",
"el-icon-folder-checked",
"el-icon-tickets",
"el-icon-document-remove",
"el-icon-document-delete",
"el-icon-document-copy",
"el-icon-document-checked",
"el-icon-document",
"el-icon-document-add",
"el-icon-printer",
"el-icon-paperclip",
"el-icon-takeaway-box",
"el-icon-search",
"el-icon-monitor",
"el-icon-attract",
"el-icon-mobile",
"el-icon-scissors",
"el-icon-umbrella",
"el-icon-headset",
"el-icon-brush",
"el-icon-mouse",
"el-icon-coordinate",
"el-icon-magic-stick",
"el-icon-reading",
"el-icon-data-line",
"el-icon-data-board",
"el-icon-pie-chart",
"el-icon-data-analysis",
"el-icon-collection-tag",
"el-icon-film",
"el-icon-suitcase",
"el-icon-suitcase-1",
"el-icon-receiving",
"el-icon-collection",
"el-icon-files",
"el-icon-notebook-1",
"el-icon-notebook-2",
"el-icon-toilet-paper",
"el-icon-office-building",
"el-icon-school",
"el-icon-table-lamp",
"el-icon-house",
"el-icon-no-smoking",
"el-icon-smoking",
"el-icon-shopping-cart-full",
"el-icon-shopping-cart-1",
"el-icon-shopping-cart-2",
"el-icon-shopping-bag-1",
"el-icon-shopping-bag-2",
"el-icon-sold-out",
"el-icon-sell",
"el-icon-present",
"el-icon-box",
"el-icon-bank-card",
"el-icon-money",
"el-icon-coin",
"el-icon-wallet",
"el-icon-discount",
"el-icon-price-tag",
"el-icon-news",
"el-icon-guide",
"el-icon-male",
"el-icon-female",
"el-icon-thumb",
"el-icon-cpu",
"el-icon-link",
"el-icon-connection",
"el-icon-open",
"el-icon-turn-off",
"el-icon-set-up",
"el-icon-chat-round",
"el-icon-chat-line-round",
"el-icon-chat-square",
"el-icon-chat-dot-round",
"el-icon-chat-dot-square",
"el-icon-chat-line-square",
"el-icon-message",
"el-icon-postcard",
"el-icon-position",
"el-icon-turn-off-microphone",
"el-icon-microphone",
"el-icon-close-notification",
"el-icon-bangzhu",
"el-icon-time",
"el-icon-odometer",
"el-icon-crop",
"el-icon-aim",
"el-icon-switch-button",
"el-icon-full-screen",
"el-icon-copy-document",
"el-icon-mic",
"el-icon-stopwatch",
"el-icon-medal-1",
"el-icon-medal",
"el-icon-trophy",
"el-icon-trophy-1",
"el-icon-first-aid-kit",
"el-icon-discover",
"el-icon-place",
"el-icon-location",
"el-icon-location-outline",
"el-icon-location-information",
"el-icon-add-location",
"el-icon-delete-location",
"el-icon-map-location",
"el-icon-alarm-clock",
"el-icon-timer",
"el-icon-watch-1",
"el-icon-watch",
"el-icon-lock",
"el-icon-unlock",
"el-icon-key",
"el-icon-service",
"el-icon-mobile-phone",
"el-icon-bicycle",
"el-icon-truck",
"el-icon-ship",
"el-icon-basketball",
"el-icon-football",
"el-icon-soccer",
"el-icon-baseball",
"el-icon-wind-power",
"el-icon-light-rain",
"el-icon-lightning",
"el-icon-heavy-rain",
"el-icon-sunrise",
"el-icon-sunrise-1",
"el-icon-sunset",
"el-icon-sunny",
"el-icon-cloudy",
"el-icon-partly-cloudy",
"el-icon-cloudy-and-sunny",
"el-icon-moon",
"el-icon-moon-night",
"el-icon-dish",
"el-icon-dish-1",
"el-icon-food",
"el-icon-chicken",
"el-icon-fork-spoon",
"el-icon-knife-fork",
"el-icon-burger",
"el-icon-tableware",
"el-icon-sugar",
"el-icon-dessert",
"el-icon-ice-cream",
"el-icon-hot-water",
"el-icon-water-cup",
"el-icon-coffee-cup",
"el-icon-cold-drink",
"el-icon-goblet",
"el-icon-goblet-full",
"el-icon-goblet-square",
"el-icon-goblet-square-full",
"el-icon-refrigerator",
"el-icon-grape",
"el-icon-watermelon",
"el-icon-cherry",
"el-icon-apple",
"el-icon-pear",
"el-icon-orange",
"el-icon-coffee",
"el-icon-ice-tea",
"el-icon-ice-drink",
"el-icon-milk-tea",
"el-icon-potato-strips",
"el-icon-lollipop",
"el-icon-ice-cream-square",
"el-icon-ice-cream-round"
]

View File

@@ -0,0 +1,126 @@
<template>
<el-popover width="510" v-model:visible="showDropdown" @show="onDropdownShow" trigger="click">
<div class="icon-select-dropdown">
<el-row type="flex" style="flex-wrap: wrap">
<div
v-for="icon in getIconList"
:key="icon"
class="icon-item"
:class="{ active: value === icon }"
@click="onIconClick(icon)"
:title="icon"
>
<orange-icon :icon="icon" />
</div>
</el-row>
<el-row type="flex" justify="space-between">
<el-button link @click="onClearClick" style="margin-left: 10px">清空</el-button>
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
layout="prev, pager, next"
:total="getIconCount"
>
</el-pagination>
</el-row>
</div>
<template v-slot:reference>
<div
class="icon-select-input"
:style="{
width: height + 'px',
height: height + 'px',
'line-height': height + 'px',
'font-size': height * 0.5 + 'px',
}"
>
<orange-icon v-if="value" :icon="value" />
</div>
</template>
</el-popover>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import OrangeIcon from '../icons/index.vue';
//import iconList from './icon.json';
const iconList: string[] = [];
for (const [key] of Object.entries(ElementPlusIconsVue)) {
//console.log(key);
iconList.push(key);
}
const emit = defineEmits<{
(e: 'update:value', icon?: string): void;
}>();
const props = withDefaults(
defineProps<{
value?: string;
height?: number;
}>(),
{
value: '',
height: 45,
},
);
const showDropdown = ref(false);
const currentPage = ref(1);
const pageSize = ref(32);
const onIconClick = (icon: string) => {
emit('update:value', icon);
showDropdown.value = false;
};
const onClearClick = () => {
emit('update:value', '');
showDropdown.value = false;
};
const onDropdownShow = () => {
currentPage.value = 1;
let pos = iconList.indexOf(props.value);
if (pos >= 0) {
currentPage.value += Math.floor(pos / pageSize.value);
}
};
const getIconCount = computed(() => {
return iconList.length;
});
const getIconList = computed(() => {
let beginPos = (currentPage.value - 1) * pageSize.value;
let endPos = beginPos + pageSize.value;
return iconList.slice(beginPos, endPos);
});
</script>
<style scoped>
.icon-select-input {
text-align: center;
color: #5f6266;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
}
.icon-item {
width: 40px;
height: 40px;
margin: 10px;
font-size: 20px;
text-align: center;
color: #5f6266;
border: 1px solid #dcdfe6;
border-radius: 3px;
line-height: 40px;
cursor: pointer;
}
.active {
color: #ef5e1c;
}
</style>

View File

@@ -0,0 +1,236 @@
<template>
<div
class="el-input el-date-editor el-range-editor el-input__wrapper el-input-number-range"
:class="[
inputSize ? 'el-range-editor--' + inputSize : '',
focused ? 'is-active' : '',
{
'is-disabled': inputDisabled,
'el-input--prefix': prefixIcon,
},
]"
style="height: 100%"
@mouseenter="showClose = true"
@mouseleave="showClose = false"
>
<div class="el-input__icon el-range__icon" :class="prefixIcon">
<slot name="prepend"></slot>
</div>
<input
autocomplete="off"
:placeholder="startPlaceholder"
:value="userInput && userInput[0]"
:disabled="inputDisabled"
:readonly="readonly"
:name="name && name[0]"
@input="handleStartInput"
@change="handleStartChange"
@focus="focused = true"
@blur="focused = false"
class="el-range-input el-input__wrappe"
style="flex-grow: 1"
/>
<slot name="range-separator">
<span>{{ rangeSeparator }}</span>
</slot>
<input
autocomplete="off"
:placeholder="endPlaceholder"
:value="userInput && userInput[1]"
:disabled="inputDisabled"
:readonly="readonly"
:name="name && name[1]"
@input="handleEndInput"
@change="handleEndChange"
@focus="focused = true"
@blur="focused = false"
class="el-range-input el-input__wrappe"
style="flex-grow: 1"
/>
<el-icon
class="el-input__icon el-range__close-icon"
style="margin-right: 5px"
:style="{ visibility: showClear ? 'visible' : 'hidden' }"
@click="handleClickClear"
>
<CircleClose />
</el-icon>
</div>
</template>
<script lang="ts">
export default {
name: 'InputNumberRange',
};
</script>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useFormItem } from 'element-plus';
import { CircleClose } from '@element-plus/icons-vue';
const { form, formItem } = useFormItem();
function isNumber(val: string | null | undefined) {
if (!val) return false;
var regPos = /^\d+(\.\d+)?$/; // 非负浮点数
var regNeg = /^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$/; // 负浮点数
if (regPos.test(val) || regNeg.test(val)) {
return true;
} else {
return false;
}
}
const emit = defineEmits<{
'update:value': [(number | null)[] | null];
change: [(number | null)[] | null];
}>();
const props = withDefaults(
defineProps<{
value?: number[];
// small / default / large
size?: 'small' | 'default' | 'large' | '';
//禁用
disabled?: boolean;
// 完全只读
readonly?: boolean;
// 自定义头部图标的类名
prefixIcon?: string;
/**
* 范围选择时最小值的占位内容
*/
startPlaceholder?: string;
/**
* 范围选择时最大值的占位内容
*/
endPlaceholder?: string;
name?: string;
/**
* 选择范围时的分隔符
*/
rangeSeparator?: string;
validateEvent?: boolean;
clearable?: boolean;
}>(),
{
value: () => [],
size: 'default',
name: '',
rangeSeparator: '-',
validateEvent: true,
},
);
const focused = ref(false);
const userInput = ref<null | (number | null)[]>(props.value);
const showClose = ref(false);
const inputSize = computed(() => {
return props.size || formItem?.size;
});
const inputDisabled = computed(() => {
return props.disabled || form?.disabled;
});
const showClear = computed(() => {
let result =
props.clearable &&
!inputDisabled.value &&
!props.readonly &&
showClose.value &&
userInput.value != null &&
userInput.value.length > 0 &&
(userInput.value[0] != null || userInput.value[1] != null);
return result;
});
const handleStartInput = (event: Event) => {
let value = null;
if (event.target) {
value = parseInt((event.target as HTMLInputElement).value);
}
if (value) {
value = isNumber(value.toString()) ? value : null;
}
if (userInput.value) {
userInput.value = [value, userInput.value[1]];
} else {
userInput.value = [value, null];
}
};
const handleEndInput = (event: Event) => {
let value = null;
if (event.target) {
value = parseInt((event.target as HTMLInputElement).value);
}
if (value) {
value = isNumber(value.toString()) ? value : null;
}
if (userInput.value) {
userInput.value = [userInput.value[0], value];
} else {
userInput.value = [null, value];
}
};
const handleStartChange = () => {
let value = userInput.value && userInput.value[0];
if (userInput.value) {
userInput.value[0] = value;
} else {
userInput.value = [value, null];
}
//event.srcElement.value = value;
emitInput(userInput.value);
};
const handleEndChange = () => {
let value = userInput.value && userInput.value[1];
if (userInput.value) {
userInput.value[1] = value;
} else {
userInput.value = [null, value];
}
//event.srcElement.value = value;
emitInput(userInput.value);
};
const handleClickClear = () => {
userInput.value = null;
emitInput(userInput.value);
};
const emitInput = (values: (number | null)[] | null) => {
emit('update:value', values);
emit('change', values);
};
watch(
() => props.value,
val => {
//console.log('number range changed', val);
userInput.value = val;
// 触发校验
formItem?.validate('change').catch(e => {
console.warn(e);
});
},
{
deep: true,
},
);
</script>
<style lang="scss" scoped>
.el-input__inner {
border: 1px solid #a7b2cb;
border-radius: 4px;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
}
</style>
<style>
/*
.el-input-number-range.is-active .el-range__close-icon {
visibility: visible !important;
}
*/
</style>

View File

@@ -0,0 +1,175 @@
<template>
<div class="multi-item-box">
<!-- 数据列表 -->
<VueDraggable
draggable=".column-item"
v-model="dataList"
:group="dragGroup"
style="overflow: hidden; height: 100%"
:disabled="!supportSort"
>
<el-alert
v-for="item in dataList"
:key="item[prop.value]"
class="column-item"
:type="itemType"
:closable="false"
>
<template v-slot:title>
<el-row type="flex" align="middle" justify="space-between" style="width: 100%">
<el-link :type="itemType" :disabled="disabled" @click="onEditItem(item)">
<slot v-if="hasSlot" :data="item" />
<span v-else>{{ item[prop.label] }}</span>
</el-link>
<div class="right">
<slot name="right" :data="item" />
<el-icon><Close /></el-icon>
</div>
</el-row>
</template>
</el-alert>
</VueDraggable>
<!-- 添加按钮 -->
<el-alert
v-show="maxCount == null || maxCount > (data || []).length"
class="column-item"
:type="addType"
:closable="false"
>
<template v-slot:title>
<el-row type="flex" align="middle" justify="space-between" style="width: 100%">
<el-link :disabled="disabled" :type="addType" @click="onEditItem(null)">
{{ addText }}
</el-link>
</el-row>
</template>
</el-alert>
<slot />
</div>
</template>
<script setup lang="ts">
import { VueDraggable } from 'vue-draggable-plus';
import { getCurrentInstance } from 'vue';
import { Close } from '@element-plus/icons-vue';
import { EpPropMergeType } from 'element-plus/es/utils';
import { ANY_OBJECT } from '@/types/generic';
const emit = defineEmits<{
add: [];
edit: [ANY_OBJECT];
delete: [ANY_OBJECT];
'update:data': [ANY_OBJECT | ANY_OBJECT[]];
}>();
const props = withDefaults(
defineProps<{
/**
* 添加按钮类型
*/
addType?:
| EpPropMergeType<StringConstructor, 'info' | 'success' | 'warning', unknown>
| undefined;
data: ANY_OBJECT | ANY_OBJECT[];
/**
* 数据项类型
*/
itemType?:
| EpPropMergeType<StringConstructor, 'info' | 'success' | 'warning', unknown>
| undefined;
/**
* 添加按钮文本
*/
addText?: string;
disabled?: boolean;
/**
* 最大项数,当达到最大项数,则不能添加数据
*/
maxCount?: number;
/**
* 是否支持拖拽排序默认为false
*/
supportSort?: boolean;
/**
* 是否支持添加
*/
supportAdd?: boolean;
dragGroup?: string;
prop?: ANY_OBJECT;
}>(),
{
addType: 'info',
itemType: 'success',
addText: '请点击添加数据项',
disabled: false,
supportSort: false,
supportAdd: true,
dragGroup: 'componentsGroup',
prop: () => {
return {
// 数据显示字段
label: 'name',
// 数据值字段
value: 'id',
};
},
},
);
const hasSlot = computed(() => {
return getCurrentInstance()?.slots.default;
});
const dataList = computed({
get: () => {
if (props.data == null) return [];
return Array.isArray(props.data) ? props.data : [props.data];
},
set: (val: ANY_OBJECT[]) => {
// 更新数据调用的地方必须使用v-model:data的方式
emit('update:data', val);
},
});
const onEditItem = (item: ANY_OBJECT | null) => {
if (props.disabled) return;
if (item == null) {
emit('add');
} else {
emit('edit', item);
}
};
// const onDeleteItem = (item: ANY_OBJECT) => {
// if (props.disabled) return;
// emit('delete', item);
// };
</script>
<style scoped>
.multi-item-box {
display: inline-block;
width: 100%;
min-height: 30px;
padding: 5px;
border: 1px solid #dcdfe6;
border-radius: 3px;
}
.multi-item-box :deep(.el-alert) {
padding: 0;
}
.multi-item-box :deep(.el-alert__content) {
width: 100%;
padding: 5px 10px;
font-size: 12px;
}
.multi-item-box :deep(.el-alert__content .el-icon-close) {
cursor: pointer;
flex-shrink: 0;
flex-grow: 0;
}
.column-item + .column-item {
margin-top: 5px;
}
.column-item :deep(.el-alert__title, .column-item .el-link) {
font-size: 12px !important;
}
</style>

View File

@@ -0,0 +1,213 @@
<template>
<el-form-item class="multi-item-list" :class="{ empty: (dataList || []).length <= 0 }">
<template v-slot:label>
<el-row justify="space-between" align="middle" style="width: 100%">
<span>{{ label }}</span>
<right-add-btn
class="add-btn"
:disabled="addDisabled"
@click.prevent.stop="onEditItem(null)"
>{{ addText ? addText : '添加' }}</right-add-btn
>
</el-row>
</template>
<VueDraggable
draggable=".list-item"
v-model="dataList"
:group="dragGroup"
style="display: inline-block; overflow: hidden; width: 100%; height: 100%"
:disabled="!supportSort"
>
<el-row
class="list-item"
v-for="item in dataList"
:key="getItemValue(item)"
align="middle"
justify="space-between"
style="width: 100%; flex-wrap: nowrap"
>
<div class="item" style="width: 100%; padding: 0 15px">
<el-row align="middle" justify="space-between" style="min-height: 32px; padding: 6px 0">
<el-link
style="font-size: 12px; color: #333; line-height: 20px"
:disabled="disabled"
@click="onEditItem(item)"
>
<slot v-if="hasSlot" :data="item" />
<span v-else>{{ getItemLabel(item) }}</span>
</el-link>
<div class="right" style="line-height: 20px; font-size: 12px">
<slot name="right" :data="item" />
</div>
</el-row>
</div>
<el-icon
style="margin-left: 8px; color: #333"
class="close-btn"
:class="{ disabled: getItemDisabled(item) }"
@click.stop="onDeleteItem(item)"
><Remove
/></el-icon>
</el-row>
</VueDraggable>
</el-form-item>
</template>
<script setup lang="ts">
import { VueDraggable } from 'vue-draggable-plus';
import { getCurrentInstance } from 'vue';
import { Remove } from '@element-plus/icons-vue';
import { ANY_OBJECT } from '@/types/generic';
import RightAddBtn from '@/components/Btns/RightAddBtn.vue';
const emit = defineEmits<{
add: [];
edit: [ANY_OBJECT];
delete: [ANY_OBJECT];
'update:data': [ANY_OBJECT | ANY_OBJECT[]];
}>();
const props = withDefaults(
defineProps<{
label: string;
data?: ANY_OBJECT | ANY_OBJECT[];
disabled?: boolean;
/**
* 最大项数,当达到最大项数,则不能添加数据
*/
maxCount?: number;
/**
* 是否支持拖拽排序默认为false
*/
supportSort?: boolean;
/**
* 是否支持添加
*/
supportAdd?: boolean;
dragGroup?: string;
prop?: ANY_OBJECT;
addText?: string;
}>(),
{
disabled: false,
supportSort: false,
supportAdd: true,
dragGroup: 'componentsGroup',
prop: () => {
return {
// 数据显示字段
label: 'name',
// 数据值字段
value: 'id',
// 数据disabled字段
disabled: undefined,
};
},
},
);
const slots = useSlots();
console.log('slots', slots);
const addDisabled = computed(() => {
console.log(
'addDisabled',
props.label,
props.disabled,
!props.supportAdd,
props.maxCount,
(dataList.value || []).length,
);
return (
props.disabled ||
!props.supportAdd ||
(props.maxCount && props.maxCount <= (dataList.value || []).length)
);
});
const hasSlot = computed(() => {
console.log('getCurrentInstance.slots', getCurrentInstance()?.slots);
return getCurrentInstance()?.slots.default;
});
const dataList = computed({
get: () => {
console.log('dataList 0>>>', props.data);
if (props.data == null) return [];
return Array.isArray(props.data) ? props.data : [props.data];
},
set: (val: ANY_OBJECT[]) => {
console.log('MultiItemList data change', val);
// 更新数据调用的地方必须使用v-model:data的方式
emit('update:data', val);
},
});
const getItemDisabled = (item: ANY_OBJECT) => {
let itemDisabled = false;
if (item != null && props.prop.disabled != null) {
if (typeof props.prop.disabled === 'function') {
itemDisabled = props.prop.disabled(item);
} else {
itemDisabled = item[props.prop.disabled];
}
}
return props.disabled || item == null || itemDisabled;
};
const onEditItem = (item: ANY_OBJECT | null) => {
if (props.disabled) return;
if (item == null) {
emit('add');
} else {
emit('edit', item);
}
};
const onDeleteItem = (item: ANY_OBJECT) => {
if (getItemDisabled(item)) return;
emit('delete', item);
};
const getItemValue = (item: ANY_OBJECT) => {
console.log('getItemValue >>>', props.label, item[props.prop.value], dataList.value);
if (typeof props.prop.value === 'function') return props.prop.value(item);
return item[props.prop.value];
};
const getItemLabel = (item: ANY_OBJECT) => {
if (typeof props.prop.label === 'function') return props.prop.label(item);
return item[props.prop.label];
};
</script>
<style scoped>
.multi-item-list :deep(.el-form-item__label) {
width: 100%;
}
.multi-item-list .item {
background: white;
border: 1px solid #e8e8e8;
border-radius: 4px;
}
.list-item + .list-item {
margin-top: 8px;
}
.multi-item-list.empty :deep(.el-form-item__content) {
height: 0;
}
</style>
<style lang="scss" scoped>
.add-btn {
position: relative;
z-index: 9;
margin-left: 5px;
font-size: 12px;
color: $color-primary;
}
.close-btn {
cursor: pointer;
}
.close-btn.disabled {
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,19 @@
<template>
<label class="page-close-box" @click="onCancel()">
<img src="@/assets/img/back2.png" alt="" />
</label>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
const emit = defineEmits<{ close: [] }>();
const router = useRouter();
const onCancel = () => {
// TODO 刷新父页面
router.go(-1);
emit('close');
};
</script>

View File

@@ -0,0 +1,36 @@
<template>
<el-progress v-bind="$attrs" :percentage="getPercentage" />
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = withDefaults(
defineProps<{
/**
* 组件最小值
*/
min?: number;
/**
* 组件最大值
*/
max?: number;
/**
* 组件当前值
*/
value?: number;
}>(),
{ min: 0, max: 100, value: 0 },
);
const getPercentage = computed(() => {
let value: number = Math.min(props.max, Math.max(props.min, props.value));
value = value - props.min;
if (props.max - props.min === 0) {
value = 0;
} else {
value = (value * 100) / (props.max - props.min);
}
return value;
});
</script>

View File

@@ -0,0 +1,171 @@
<template>
<div style="border: 1px solid #ccc">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:defaultConfig="toolbarConfig"
:mode="mode"
/>
<Editor
style="overflow-y: hidden"
:style="{ height: height }"
v-model="valueHtml"
:defaultConfig="editorConfig"
:mode="mode"
@onCreated="handleCreated"
/>
</div>
</template>
<script lang="ts">
// TODO 须测试
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
import { IEditorConfig, IToolbarConfig } from '@wangeditor/editor';
const defaultEditorConfig: Partial<IEditorConfig> = {
placeholder: '请输入内容...',
MENU_CONF: {
uploadImage: {
// 小于该值就插入 base64 格式(而不上传),默认为 0
base64LimitSize: 1024 * 1024, // 5kb
},
},
};
const defaultToolbarConfig: Partial<IToolbarConfig> = {
toolbarKeys: [
'headerSelect',
// '|',
// 'header1',
// 'header2',
// 'header3',
// 'header4',
// 'header5',
'bold',
'underline',
'italic',
'through',
// 'code',
// 'sub',
// 'sup',
// 'clearStyle',
'color',
'bgColor',
'fontSize',
'fontFamily',
'indent',
'delIndent',
// 'justifyLeft',
// 'justifyRight',
// 'justifyCenter',
// 'justifyJustify',
'lineHeight',
'insertImage',
'uploadImage',
// 'deleteImage',
// 'editImage',
// 'viewImageLink',
// 'imageWidth30',
// 'imageWidth50',
// 'imageWidth100',
'divider',
// 'emotion',
'insertLink',
'editLink',
'unLink',
'viewLink',
'codeBlock',
'blockquote',
'todo',
'redo',
'undo',
'fullScreen',
// 'enter',
// 'bulletedList',
// 'numberedList',
// 'insertTable',
// 'deleteTable',
// 'insertTableRow',
// 'deleteTableRow',
// 'insertTableCol',
// 'deleteTableCol',
// 'tableHeader',
// 'tableFullWidth',
// 'insertVideo',
// 'uploadVideo',
// 'editVideoSize',
// 'uploadImage',
// 'codeSelectLang',
],
};
</script>
<script setup lang="ts">
import { onBeforeUnmount, shallowRef } from 'vue';
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
import { ANY_OBJECT } from '@/types/generic';
const emit = defineEmits<{ 'update:value': [string | undefined] }>();
const props = withDefaults(
defineProps<{
value?: string;
editorConfig?: ANY_OBJECT;
toolbarConfig?: ANY_OBJECT;
height?: string;
}>(),
{
editorConfig: () => {
return defaultEditorConfig;
},
toolbarConfig: () => {
return defaultToolbarConfig;
},
height: '300px',
},
);
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef();
const mode = ref('default');
// 内容 HTML
const valueHtml = computed({
get() {
return props.value;
},
set(val) {
console.log('richeditor value input', val);
emit('update:value', val);
},
});
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value;
if (editor == null) return;
editor.destroy();
});
const handleCreated = (editor: ANY_OBJECT) => {
editorRef.value = editor; // 记录 editor 实例,重要!
console.log('editor', editor);
console.log(editor.getAllMenuKeys());
};
watch(
() => props.value,
newValue => {
if (editorRef.value) editorRef.value.txt.html(newValue);
},
{
deep: true,
immediate: true,
},
);
const getHtml = () => {
return editorRef.value ? editorRef.value.txt.html() : undefined;
};
defineExpose({ getHtml });
</script>

View File

@@ -0,0 +1,11 @@
/* eslint no-bitwise: "off" */
/*
v: int value
digit: bit len of v
flag: true or false
*/
const bitmap = (v, digit, flag) => {
const b = 1 << digit;
return flag ? v | b : v ^ b;
};
export default bitmap;

View File

@@ -0,0 +1,39 @@
// src: include chars: [0-9], +, -, *, /
// // 9+(3-1)*3+10/2 => 9 3 1-3*+ 10 2/+
const infix2suffix = src => {
const operatorStack = [];
const stack = [];
for (let i = 0; i < src.length; i += 1) {
const c = src.charAt(i);
if (c !== ' ') {
if (c >= '0' && c <= '9') {
stack.push(c);
} else if (c === ')') {
let c1 = operatorStack.pop();
while (c1 !== '(') {
stack.push(c1);
c1 = operatorStack.pop();
}
} else {
// priority: */ > +-
if (operatorStack.length > 0 && (c === '+' || c === '-')) {
const last = operatorStack[operatorStack.length - 1];
if (last === '*' || last === '/') {
while (operatorStack.length > 0) {
stack.push(operatorStack.pop());
}
}
}
operatorStack.push(c);
}
}
}
while (operatorStack.length > 0) {
stack.push(operatorStack.pop());
}
return stack;
};
export default {
infix2suffix,
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -0,0 +1,137 @@
<?xml version='1.0' encoding='UTF-8'?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" width="262px" height="72px" viewBox="0 0 262 72" preserveAspectRatio="none">
<g xmlns="http://www.w3.org/2000/svg" transform="translate(0,0)"><path fill="#000000" fill-rule="evenodd" d="M11.5656391,4.43436088 L9,7 L16,7 L16,0 L13.0418424,2.95815758 C11.5936787,1.73635959 9.72260775,1 7.67955083,1 C4.22126258,1 1.25575599,3.10984908 0,6 L2,7 C2.93658775,4.60974406 5.12943697,3.08011229 7.67955083,3 C9.14881247,3.0528747 10.4994783,3.57862053 11.5656391,4.43436088 Z" transform="matrix(-1 0 0 1 17 5)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(18,0)"><path fill="#000000" fill-rule="evenodd" d="M11.5656391,4.43436088 L9,7 L16,7 L16,0 L13.0418424,2.95815758 C11.5936787,1.73635959 9.72260775,1 7.67955083,1 C4.22126258,1 1.25575599,3.10984908 0,6 L2,7 C2.93658775,4.60974406 5.12943697,3.08011229 7.67955083,3 C9.14881247,3.0528747 10.4994783,3.57862053 11.5656391,4.43436088 Z" transform="translate(1 5)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(36,0)"><path fill="#000000" fill-rule="evenodd" d="M13,14 L3,14 L3,11 L0,11 L0,6.00591905 C0,4.89808055 0.894513756,4 1.99406028,4 L14.0059397,4 C15.1072288,4 16,4.88655484 16,6.00591905 L16,11 L13,11 L13,14 Z M5,9 L11,9 L11,12 L5,12 L5,9 Z M3,0 L13,0 L13,3 L3,3 L3,0 Z M12,6 L14,6 L14,8 L12,8 L12,6 Z" transform="translate(1 2)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(54,0)"><path fill="#000000" fill-rule="evenodd" d="M9,0 L1,0 C0.45,0 0,0.45 0,1 L0,4 C0,4.55 0.45,5 1,5 L9,5 C9.55,5 10,4.55 10,4 L10,3 L11,3 L11,6 L4,6 L4,14 L6,14 L6,8 L13,8 L13,2 L10,2 L10,1 C10,0.45 9.55,0 9,0 Z" transform="translate(3 2)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(72,0)"><path fill="#000000" fill-rule="evenodd" d="M0.27,1.55 L5.43,6.7 L3,12 L5.5,12 L7.14,8.42 L11.73,13 L13,11.73 L1.55,0.27 L0.27,1.55 L0.27,1.55 Z M3.82,0 L5.82,2 L7.58,2 L7.03,3.21 L8.74,4.92 L10.08,2 L14,2 L14,0 L3.82,0 L3.82,0 Z" transform="translate(2 3)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(90,0)"><path fill="#000000" fill-rule="evenodd" d="M9,3.5 C9,1.57 7.43,0 5.5,0 L1.77635684e-15,0 L1.77635684e-15,12 L6.25,12 C8.04,12 9.5,10.54 9.5,8.75 C9.5,7.45 8.73,6.34 7.63,5.82 C8.46,5.24 9,4.38 9,3.5 Z M5,2 C5.82999992,2 6.5,2.67 6.5,3.5 C6.5,4.33 5.82999992,5 5,5 L3,5 L3,2 L5,2 Z M3,10 L3,7 L5.5,7 C6.32999992,7 7,7.67 7,8.5 C7,9.33 6.32999992,10 5.5,10 L3,10 Z" transform="translate(4 3)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(108,0)"><polygon fill="#000000" fill-rule="evenodd" points="4 0 4 2 6.58 2 2.92 10 0 10 0 12 8 12 8 10 5.42 10 9.08 2 12 2 12 0" transform="translate(3 3)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(126,0)"><path fill="#000000" d="M6,12 C8.76,12 11,9.76 11,7 L11,0 L9,0 L9,7 C9,8.75029916 7.49912807,10 6,10 C4.50087193,10 3,8.75837486 3,7 L3,0 L1,0 L1,7 C1,9.76 3.24,12 6,12 Z M0,13 L0,15 L12,15 L12,13 L0,13 Z" transform="translate(3 3)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(144,0)"><path fill="#010101" fill-rule="evenodd" d="M2.8875,3.06 C2.8875,2.6025 2.985,2.18625 3.18375,1.8075 C3.3825,1.42875 3.66,1.10625 4.02,0.84 C4.38,0.57375 4.80375,0.3675 5.29875,0.22125 C5.79375,0.075 6.33375,0 6.92625,0 C7.53375,0 8.085,0.0825 8.58,0.25125 C9.075,0.42 9.49875,0.6525 9.85125,0.95625 C10.20375,1.25625 10.47375,1.6125 10.665,2.02875 C10.85625,2.44125 10.95,2.895 10.95,3.38625 L8.6925,3.38625 C8.6925,3.1575 8.655,2.94375 8.58375,2.74875 C8.5125,2.55 8.4,2.38125 8.25,2.2425 C8.1,2.10375 7.9125,1.99125 7.6875,1.91625 C7.4625,1.8375 7.19625,1.8 6.88875,1.8 C6.5925,1.8 6.3375,1.83375 6.11625,1.8975 C5.89875,1.96125 5.71875,2.05125 5.57625,2.1675 C5.43375,2.28375 5.325,2.41875 5.25375,2.5725 C5.1825,2.72625 5.145,2.895 5.145,3.0675 C5.145,3.4275 5.32875,3.73125 5.69625,3.975 C5.71780203,3.98908066 5.73942012,4.00311728 5.76118357,4.01733315 C6.02342923,4.18863185 6.5,4.5 7,5 L4,5 C4,5 3.21375,4.37625 3.17625,4.30875 C2.985,3.9525 2.8875,3.53625 2.8875,3.06 Z M14,6 L0,6 L0,8 L7.21875,8 C7.35375,8.0525 7.51875,8.105 7.63125,8.15375 C7.90875,8.2775 8.12625,8.40875 8.28375,8.53625 C8.44125,8.6675 8.54625,8.81 8.6025,8.96 C8.65875,9.11375 8.685,9.28625 8.685,9.47375 C8.685,9.65 8.65125,9.815 8.58375,9.965 C8.51625,10.11875 8.41125,10.25 8.2725,10.35875 C8.13375,10.4675 7.95375,10.55375 7.74,10.6175 C7.5225,10.68125 7.27125,10.71125 6.97875,10.71125 C6.6525,10.71125 6.35625,10.6775 6.09,10.61375 C5.82375,10.55 5.59875,10.445 5.41125,10.3025 C5.22375,10.16 5.0775,9.9725 4.9725,9.74375 C4.8675,9.515 4.78125,9.17 4.78125,9 L2.55,9 C2.55,9.2525 2.61,9.6875 2.72625,10.025 C2.8425,10.3625 3.0075,10.66625 3.21375,10.9325 C3.42,11.19875 3.6675,11.4275 3.94875,11.6225 C4.23,11.8175 4.53375,11.9825 4.86375,12.11 C5.19375,12.24125 5.535,12.33875 5.89875,12.39875 C6.25875,12.4625 6.6225,12.4925 6.9825,12.4925 C7.5825,12.4925 8.13,12.425 8.6175,12.28625 C9.105,12.1475 9.525,11.94875 9.87,11.69375 C10.215,11.435 10.48125,11.12 10.6725,10.74125 C10.86375,10.3625 10.95375,9.935 10.95375,9.455 C10.95375,9.005 10.875,8.6 10.72125,8.24375 C10.68375,8.1575 10.6425,8.075 10.59375,7.9925 L14,8 L14,6 Z" transform="translate(2 3)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(162,0)"><path fill="#000000" fill-rule="evenodd" d="M7,0 L5,0 L0.5,12 L2.5,12 L3.62,9 L8.37,9 L9.49,12 L11.49,12 L7,0 L7,0 Z M4.38,7 L6,2.67 L7.62,7 L4.38,7 L4.38,7 Z" transform="translate(3 1)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(180,0)"><g fill="none" fill-rule="evenodd">
<path fill="#000000" d="M14.5,8.87 C14.5,8.87 13,10.49 13,11.49 C13,12.32 13.67,12.99 14.5,12.99 C15.33,12.99 16,12.32 16,11.49 C16,10.5 14.5,8.87 14.5,8.87 L14.5,8.87 Z M12.71,6.79 L5.91,0 L4.85,1.06 L6.44,2.65 L2.29,6.79 C1.9,7.18 1.9,7.81 2.29,8.2 L6.79,12.7 C6.99,12.9 7.24,13 7.5,13 C7.76,13 8.01,12.9 8.21,12.71 L12.71,8.21 C13.1,7.82 13.1,7.18 12.71,6.79 L12.71,6.79 Z M4.21,7 L7.5,3.71 L10.79,7 L4.21,7 L4.21,7 Z"/>
</g>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(198,0)"><path fill="#000000" fill-rule="evenodd" d="M3,6 L1,6 L1,2 L8,2 L8,4 L3,4 L3,6 Z M10,4 L10,2 L17,2 L17,6 L15,6 L15,4 L10,4 Z M10,14 L15,14 L15,12 L17,12 L17,16 L10,16 L10,14 Z M1,12 L3,12 L3,14 L8,14 L8,16 L1,16 L1,12 Z M1,8 L5,8 L5,6 L8,9 L5,12 L5,10 L1,10 L1,8 Z M10,9 L13,6 L13,8 L17,8 L17,10 L13,10 L13,12 L10,9 Z"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(216,0)"><path fill="#000000" fill-rule="evenodd" d="M0,14 L10,14 L10,12 L0,12 L0,14 Z M10,4 L0,4 L0,6 L10,6 L10,4 Z M0,0 L0,2 L14,2 L14,0 L0,0 Z M0,10 L14,10 L14,8 L0,8 L0,10 Z" transform="translate(2 2)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(234,0)"><path fill="#000000" fill-rule="evenodd" d="M2,12 L2,14 L12,14 L12,12 L2,12 Z M2,4 L2,6 L12,6 L12,4 L2,4 Z M0,10 L14,10 L14,8 L0,8 L0,10 Z M0,0 L0,2 L14,2 L14,0 L0,0 Z" transform="translate(2 2)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(0,18)"><path fill="#000000" fill-rule="evenodd" d="M4,14 L14,14 L14,12 L4,12 L4,14 Z M0,10 L14,10 L14,8 L0,8 L0,10 Z M0,0 L0,2 L14,2 L14,0 L0,0 Z M4,6 L14,6 L14,4 L4,4 L4,6 Z" transform="translate(2 2)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(18,18)"><path fill="#000000" fill-rule="evenodd" d="M0,0 L0,2 L12,2 L12,0 L0,0 L0,0 Z M2.5,7 L5,7 L5,14 L7,14 L7,7 L9.5,7 L6,3.5 L2.5,7 L2.5,7 Z" transform="translate(3 2)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(36,18)"><path fill="#000000" fill-rule="evenodd" d="M9.5,3 L7,3 L7,0 L5,0 L5,3 L2.5,3 L6,6.5 L9.5,3 L9.5,3 Z M0,8 L0,10 L12,10 L12,8 L0,8 L0,8 Z M2.5,15 L5,15 L5,18 L7,18 L7,15 L9.5,15 L6,11.5 L2.5,15 L2.5,15 Z" transform="translate(3)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(54,18)"><path fill="#000000" fill-rule="evenodd" d="M9.5,7 L7,7 L7,0 L5,0 L5,7 L2.5,7 L6,10.5 L9.5,7 L9.5,7 Z M0,12 L0,14 L12,14 L12,12 L0,12 L0,12 Z" transform="translate(3 2)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(72,18)"><path fill="#000000" fill-rule="evenodd" d="M14,0 L0,0 L0,2 L14,2 L14,0 Z M0,12 L4,12 L4,10 L0,10 L0,12 Z M11.5,5 L0,5 L0,7 L11.75,7 C12.58,7 13.25,7.67 13.25,8.5 C13.25,9.33 12.58,10 11.75,10 L9,10 L9,8 L6,11 L9,14 L9,12 L11.5,12 C13.43,12 15,10.43 15,8.5 C15,6.57 13.43,5 11.5,5 Z" transform="translate(2 3)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(90,18)"><path fill="#000000" fill-rule="evenodd" d="M0,0 L0,1 L6,7 L6,12 L8,11 L8,7 L14,1 L14,0 L0,0 Z M4,3 L10,3 L7,6 L4,3 Z" transform="translate(2 3)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(108,18)"><polygon fill="#000000" fill-rule="evenodd" points="10 0 0 0 0 1.8 5.5 7 0 12.2 0 14 10 14 10 12 3.1 12 8 7 3.1 2 10 2" transform="translate(4 2)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(126,18)"><polygon fill="#000000" fill-rule="evenodd" points="0 0 4 4 8 0" transform="translate(5 7)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(144,18)"><polygon fill="#000000" fill-rule="evenodd" points="-2 2 2 6 6 2" transform="rotate(-90 8 3)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(162,18)"><path fill="#000000" fill-rule="evenodd" d="M1.9,4 C1.9,2.84 2.84,1.9 4,1.9 L8,1.9 L8,0 L4,0 C1.79,0 0,1.79 0,4 C0,6.21 1.79,8 4,8 L8,8 L8,6.1 L4,6.1 C2.84,6.1 1.9,5.16 1.9,4 L1.9,4 Z M14,0 L10,0 L10,1.9 L14,1.9 C15.16,1.9 16.1,2.84 16.1,4 C16.1,5.16 15.16,6.1 14,6.1 L10,6.1 L10,8 L14,8 C16.21,8 18,6.21 18,4 C18,1.79 16.21,0 14,0 L14,0 Z M6,5 L12,5 L12,3 L6,3 L6,5 L6,5 Z" transform="translate(0 5)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(180,18)"><path fill="#000000" fill-rule="evenodd" d="M15,0 C15.55,0 16,0.45 16,1 L16,15 C16,15.55 15.55,16 15,16 L1,16 C0.45,16 0,15.55 0,15 L0,1 C0,0.45 0.45,0 1,0 L15,0 Z M2,2 L2,14 L14,14 L14,2 L2,2 Z M6,12 L4,12 L4,7 L6,7 L6,12 L6,12 Z M9,12 L7,12 L7,4 L9,4 L9,12 L9,12 Z M12,12 L10,12 L10,8 L12,8 L12,12 L12,12 Z" transform="translate(1 1)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(198,18)"><g fill="none" fill-rule="evenodd">
<path stroke="#000000" d="M1.5 3.5H16.5V15.5H1.5z"/>
<path fill="#000000" d="M6 8H7V15H6z"/>
<path fill="#D8D8D8" d="M2 4H16V7H2z"/>
<path fill="#000000" d="M2 7H16V8H2zM2 11H16V12H2z"/>
</g>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(216,18)"><path fill="#000000" fill-rule="evenodd" d="M2,0.5 C1.17,0.5 0.5,1.17 0.5,2 C0.5,2.83 1.17,3.5 2,3.5 C2.83,3.5 3.5,2.83 3.5,2 C3.5,1.17 2.83,0.5 2,0.5 L2,0.5 Z M12,0.5 C11.17,0.5 10.5,1.17 10.5,2 C10.5,2.83 11.17,3.5 12,3.5 C12.83,3.5 13.5,2.83 13.5,2 C13.5,1.17 12.83,0.5 12,0.5 L12,0.5 Z M7,0.5 C6.17,0.5 5.5,1.17 5.5,2 C5.5,2.83 6.17,3.5 7,3.5 C7.83,3.5 8.5,2.83 8.5,2 C8.5,1.17 7.83,0.5 7,0.5 L7,0.5 Z" transform="translate(2 7)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(234,18)"><path fill="#000000" fill-rule="evenodd" d="M6,4 L6,0 L4,0 L4,4 L0,4 L0,6 L4,6 L4,10 L6,10 L6,6 L10,6 L10,4 L6,4 Z" transform="translate(4 4)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(0,36)"><path fill="#000000" fill-rule="evenodd" d="M0,0 L0,14 L14,14 L14,0 L0,0 L0,0 Z M6,12 L2,12 L2,8 L6,8 L6,12 L6,12 Z M6,6 L2,6 L2,2 L6,2 L6,6 L6,6 Z M12,12 L8,12 L8,8 L12,8 L12,12 L12,12 Z M12,6 L8,6 L8,2 L12,2 L12,6 L12,6 Z" transform="translate(2 2)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(18,36)">
<g fill="#000000" fill-rule="evenodd" transform="translate(2 2)">
<path d="M0,14 L2,14 L2,12 L0,12 L0,14 L0,14 Z M2,3 L0,3 L0,5 L2,5 L2,3 L2,3 Z M3,14 L5,14 L5,12 L3,12 L3,14 L3,14 Z M11,0 L9,0 L9,2 L11,2 L11,0 L11,0 Z M2,0 L0,0 L0,2 L2,2 L2,0 L2,0 Z M5,0 L3,0 L3,2 L5,2 L5,0 L5,0 Z M0,11 L2,11 L2,9 L0,9 L0,11 L0,11 Z M9,14 L11,14 L11,12 L9,12 L9,14 L9,14 Z M12,0 L12,2 L14,2 L14,0 L12,0 L12,0 Z M12,5 L14,5 L14,3 L12,3 L12,5 L12,5 Z M12,14 L14,14 L14,12 L12,12 L12,14 L12,14 Z M12,11 L14,11 L14,9 L12,9 L12,11 L12,11 Z" opacity=".54"/>
<polygon points="8 0 6 0 6 6 0 6 0 8 6 8 6 14 8 14 8 8 14 8 14 6 8 6"/>
</g>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(36,36)">
<g fill="#000000" fill-rule="evenodd" transform="translate(2 2)">
<path d="M6,14 L8,14 L8,12 L6,12 L6,14 L6,14 Z M3,2 L5,2 L5,0 L3,0 L3,2 L3,2 Z M6,11 L8,11 L8,9 L6,9 L6,11 L6,11 Z M3,14 L5,14 L5,12 L3,12 L3,14 L3,14 Z M0,5 L2,5 L2,3 L0,3 L0,5 L0,5 Z M0,14 L2,14 L2,12 L0,12 L0,14 L0,14 Z M0,2 L2,2 L2,0 L0,0 L0,2 L0,2 Z M0,11 L2,11 L2,9 L0,9 L0,11 L0,11 Z M12,11 L14,11 L14,9 L12,9 L12,11 L12,11 Z M12,14 L14,14 L14,12 L12,12 L12,14 L12,14 Z M12,5 L14,5 L14,3 L12,3 L12,5 L12,5 Z M12,0 L12,2 L14,2 L14,0 L12,0 L12,0 Z M6,2 L8,2 L8,0 L6,0 L6,2 L6,2 Z M9,2 L11,2 L11,0 L9,0 L9,2 L9,2 Z M6,5 L8,5 L8,3 L6,3 L6,5 L6,5 Z M9,14 L11,14 L11,12 L9,12 L9,14 L9,14 Z" opacity=".54"/>
<polygon points="0 8 14 8 14 6 0 6"/>
</g>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(54,36)">
<g fill="#000000" fill-rule="evenodd" transform="translate(2 2)">
<path d="M3,14 L5,14 L5,12 L3,12 L3,14 L3,14 Z M0,5 L2,5 L2,3 L0,3 L0,5 L0,5 Z M0,2 L2,2 L2,0 L0,0 L0,2 L0,2 Z M3,8 L5,8 L5,6 L3,6 L3,8 L3,8 Z M3,2 L5,2 L5,0 L3,0 L3,2 L3,2 Z M0,14 L2,14 L2,12 L0,12 L0,14 L0,14 Z M0,8 L2,8 L2,6 L0,6 L0,8 L0,8 Z M0,11 L2,11 L2,9 L0,9 L0,11 L0,11 Z M12,0 L12,2 L14,2 L14,0 L12,0 L12,0 Z M12,8 L14,8 L14,6 L12,6 L12,8 L12,8 Z M12,14 L14,14 L14,12 L12,12 L12,14 L12,14 Z M12,5 L14,5 L14,3 L12,3 L12,5 L12,5 Z M12,11 L14,11 L14,9 L12,9 L12,11 L12,11 Z M9,14 L11,14 L11,12 L9,12 L9,14 L9,14 Z M9,8 L11,8 L11,6 L9,6 L9,8 L9,8 Z M9,2 L11,2 L11,0 L9,0 L9,2 L9,2 Z" opacity=".54"/>
<polygon points="6 14 8 14 8 0 6 0"/>
</g>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(72,36)">
<g fill="#000000" fill-rule="evenodd" transform="translate(2 2)">
<path d="M8,3 L6,3 L6,5 L8,5 L8,3 L8,3 Z M11,6 L9,6 L9,8 L11,8 L11,6 L11,6 Z M8,6 L6,6 L6,8 L8,8 L8,6 L8,6 Z M8,9 L6,9 L6,11 L8,11 L8,9 L8,9 Z M5,6 L3,6 L3,8 L5,8 L5,6 L5,6 Z" opacity=".54"/>
<path d="M0,0 L14,0 L14,14 L0,14 L0,0 Z M12,12 L12,2 L2,2 L2,12 L12,12 Z"/>
</g>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(90,36)">
<g fill="#000000" fill-rule="evenodd" transform="translate(2 2)">
<path d="M6,8 L8,8 L8,6 L6,6 L6,8 L6,8 Z M6,5 L8,5 L8,3 L6,3 L6,5 L6,5 Z M6,11 L8,11 L8,9 L6,9 L6,11 L6,11 Z M6,14 L8,14 L8,12 L6,12 L6,14 L6,14 Z M3,14 L5,14 L5,12 L3,12 L3,14 L3,14 Z M3,2 L5,2 L5,0 L3,0 L3,2 L3,2 Z M3,8 L5,8 L5,6 L3,6 L3,8 L3,8 Z M12,14 L14,14 L14,12 L12,12 L12,14 L12,14 Z M12,8 L14,8 L14,6 L12,6 L12,8 L12,8 Z M12,11 L14,11 L14,9 L12,9 L12,11 L12,11 Z M12,5 L14,5 L14,3 L12,3 L12,5 L12,5 Z M6,2 L8,2 L8,0 L6,0 L6,2 L6,2 Z M12,0 L12,2 L14,2 L14,0 L12,0 L12,0 Z M9,14 L11,14 L11,12 L9,12 L9,14 L9,14 Z M9,8 L11,8 L11,6 L9,6 L9,8 L9,8 Z M9,2 L11,2 L11,0 L9,0 L9,2 L9,2 Z" opacity=".54"/>
<polygon points="0 14 2 14 2 0 0 0"/>
</g>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(108,36)">
<g fill="#000000" fill-rule="evenodd" transform="translate(2 2)">
<path d="M3,8 L5,8 L5,6 L3,6 L3,8 L3,8 Z M0,14 L2,14 L2,12 L0,12 L0,14 L0,14 Z M6,14 L8,14 L8,12 L6,12 L6,14 L6,14 Z M6,11 L8,11 L8,9 L6,9 L6,11 L6,11 Z M3,14 L5,14 L5,12 L3,12 L3,14 L3,14 Z M0,11 L2,11 L2,9 L0,9 L0,11 L0,11 Z M6,8 L8,8 L8,6 L6,6 L6,8 L6,8 Z M0,5 L2,5 L2,3 L0,3 L0,5 L0,5 Z M0,8 L2,8 L2,6 L0,6 L0,8 L0,8 Z M12,8 L14,8 L14,6 L12,6 L12,8 L12,8 Z M12,11 L14,11 L14,9 L12,9 L12,11 L12,11 Z M12,5 L14,5 L14,3 L12,3 L12,5 L12,5 Z M6,5 L8,5 L8,3 L6,3 L6,5 L6,5 Z M9,14 L11,14 L11,12 L9,12 L9,14 L9,14 Z M9,8 L11,8 L11,6 L9,6 L9,8 L9,8 Z M12,14 L14,14 L14,12 L12,12 L12,14 L12,14 Z" opacity=".54"/>
<polygon points="0 0 0 2 14 2 14 0"/>
</g>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(126,36)">
<g fill="#000000" fill-rule="evenodd" transform="translate(2 2)">
<path d="M0,2 L2,2 L2,0 L0,0 L0,2 L0,2 Z M3,2 L5,2 L5,0 L3,0 L3,2 L3,2 Z M3,8 L5,8 L5,6 L3,6 L3,8 L3,8 Z M3,14 L5,14 L5,12 L3,12 L3,14 L3,14 Z M0,5 L2,5 L2,3 L0,3 L0,5 L0,5 Z M0,8 L2,8 L2,6 L0,6 L0,8 L0,8 Z M0,14 L2,14 L2,12 L0,12 L0,14 L0,14 Z M0,11 L2,11 L2,9 L0,9 L0,11 L0,11 Z M9,8 L11,8 L11,6 L9,6 L9,8 L9,8 Z M6,14 L8,14 L8,12 L6,12 L6,14 L6,14 Z M9,14 L11,14 L11,12 L9,12 L9,14 L9,14 Z M6,2 L8,2 L8,0 L6,0 L6,2 L6,2 Z M9,2 L11,2 L11,0 L9,0 L9,2 L9,2 Z M6,11 L8,11 L8,9 L6,9 L6,11 L6,11 Z M6,5 L8,5 L8,3 L6,3 L6,5 L6,5 Z M6,8 L8,8 L8,6 L6,6 L6,8 L6,8 Z" opacity=".54"/>
<polygon points="12 0 12 14 14 14 14 0"/>
</g>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(144,36)">
<g fill="#000000" fill-rule="evenodd" transform="translate(2 2)">
<path d="M5,0 L3,0 L3,2 L5,2 L5,0 L5,0 Z M8,6 L6,6 L6,8 L8,8 L8,6 L8,6 Z M8,9 L6,9 L6,11 L8,11 L8,9 L8,9 Z M11,6 L9,6 L9,8 L11,8 L11,6 L11,6 Z M5,6 L3,6 L3,8 L5,8 L5,6 L5,6 Z M11,0 L9,0 L9,2 L11,2 L11,0 L11,0 Z M8,3 L6,3 L6,5 L8,5 L8,3 L8,3 Z M8,0 L6,0 L6,2 L8,2 L8,0 L8,0 Z M2,9 L0,9 L0,11 L2,11 L2,9 L2,9 Z M12,11 L14,11 L14,9 L12,9 L12,11 L12,11 Z M12,5 L14,5 L14,3 L12,3 L12,5 L12,5 Z M12,8 L14,8 L14,6 L12,6 L12,8 L12,8 Z M12,0 L12,2 L14,2 L14,0 L12,0 L12,0 Z M2,0 L0,0 L0,2 L2,2 L2,0 L2,0 Z M2,3 L0,3 L0,5 L2,5 L2,3 L2,3 Z M2,6 L0,6 L0,8 L2,8 L2,6 L2,6 Z" opacity=".54"/>
<polygon points="0 14 14 14 14 12 0 12"/>
</g>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(162,36)"><path fill="#000000" fill-rule="evenodd" d="M6,14 L8,14 L8,12 L6,12 L6,14 L6,14 Z M3,8 L5,8 L5,6 L3,6 L3,8 L3,8 Z M3,2 L5,2 L5,0 L3,0 L3,2 L3,2 Z M6,11 L8,11 L8,9 L6,9 L6,11 L6,11 Z M3,14 L5,14 L5,12 L3,12 L3,14 L3,14 Z M0,5 L2,5 L2,3 L0,3 L0,5 L0,5 Z M0,14 L2,14 L2,12 L0,12 L0,14 L0,14 Z M0,2 L2,2 L2,0 L0,0 L0,2 L0,2 Z M0,8 L2,8 L2,6 L0,6 L0,8 L0,8 Z M6,8 L8,8 L8,6 L6,6 L6,8 L6,8 Z M0,11 L2,11 L2,9 L0,9 L0,11 L0,11 Z M12,11 L14,11 L14,9 L12,9 L12,11 L12,11 Z M12,14 L14,14 L14,12 L12,12 L12,14 L12,14 Z M12,8 L14,8 L14,6 L12,6 L12,8 L12,8 Z M12,5 L14,5 L14,3 L12,3 L12,5 L12,5 Z M12,0 L12,2 L14,2 L14,0 L12,0 L12,0 Z M6,2 L8,2 L8,0 L6,0 L6,2 L6,2 Z M9,2 L11,2 L11,0 L9,0 L9,2 L9,2 Z M6,5 L8,5 L8,3 L6,3 L6,5 L6,5 Z M9,14 L11,14 L11,12 L9,12 L9,14 L9,14 Z M9,8 L11,8 L11,6 L9,6 L9,8 L9,8 Z" transform="translate(2 2)" opacity=".54"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(180,36)"><path fill="#000000" fill-rule="evenodd" d="M6.5,3.62 L0,10.12 L0,13 L2.88,13 L9.38,6.5 L6.5,3.62 Z M11.85,4.02 C12.05,3.82 12.05,3.51 11.85,3.31 L9.68,1.14 C9.48,0.94 9.17,0.94 8.97,1.14 L7.62,2.5 L10.5,5.38 L11.85,4.02 L11.85,4.02 Z" transform="translate(4)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(198,36)"><path fill="#000000" fill-rule="evenodd" d="M0,0 L14,0 L14,2 L0,2 L0,0 Z M0,4 L6,4 L6,6 L0,6 L0,4 Z M0,8 L2,8 L2,10 L0,10 L0,8 Z M8,4 L14,4 L14,6 L8,6 L8,4 Z M4,8 L6,8 L6,10 L4,10 L4,8 Z M8,8 L10,8 L10,10 L8,10 L8,8 Z M12,8 L14,8 L14,10 L12,10 L12,8 Z" transform="translate(2 4)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(216,36)"><polygon fill="#000000" fill-rule="evenodd" points="-2 2 2 6 6 2" transform="rotate(90 3 10)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(234,36)"><polygon fill="#000000" fill-rule="evenodd" points="7.53 1.53 6.47 .47 4 2.94 1.53 .47 .47 1.53 2.94 4 .47 6.47 1.53 7.53 4 5.06 6.47 7.53 7.53 6.47 5.06 4" transform="translate(5 5)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(0,54)"><polygon fill="#000000" fill-rule="evenodd" points="8.44 .44 5 3.88 1.56 .44 .5 1.5 5 6 9.5 1.5" transform="translate(4 6)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(18,54)"><polygon fill="#000000" fill-rule="evenodd" points="5 0 .5 4.5 1.56 5.56 5 2.12 8.44 5.56 9.5 4.5" transform="translate(4 6)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(36,54)"><polygon fill="#000000" fill-rule="evenodd" points="8.44 .44 5 3.88 1.56 .44 .5 1.5 5 6 9.5 1.5" transform="rotate(90 4 8)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(54,54)"><polygon fill="#000000" fill-rule="evenodd" points="5 0 .5 4.5 1.56 5.56 5 2.12 8.44 5.56 9.5 4.5" transform="rotate(90 4 8)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(72,54)"><polygon fill="#000000" fill-rule="evenodd" points="12 5 3.125 5 7.06 1.06 6 0 0 6 6 12 7.06 10.94 3.125 7 12 7" transform="matrix(-1 0 0 1 15 3)"/>
</g>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(90,54)"><polygon fill="#000000" fill-rule="evenodd" points="12 5 3.125 5 7.06 1.06 6 0 0 6 6 12 7.06 10.94 3.125 7 12 7" transform="translate(3 3)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,507 @@
const imageMap = new Map();
function dpr() {
return window.devicePixelRatio || 1;
}
function thinLineWidth() {
return dpr() - 0.5;
}
function npx(px) {
return parseInt(px * dpr(), 10);
}
function npxLine(px) {
const n = npx(px);
return n > 0 ? n - 0.5 : 0.5;
}
class DrawBox {
constructor(x, y, w, h, padding = 0) {
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.padding = padding;
this.bgcolor = '#ffffff';
// border: [width, style, color]
this.borderTop = null;
this.borderRight = null;
this.borderBottom = null;
this.borderLeft = null;
}
setBorders({ top, bottom, left, right }) {
if (top) this.borderTop = top;
if (right) this.borderRight = right;
if (bottom) this.borderBottom = bottom;
if (left) this.borderLeft = left;
}
innerWidth() {
return this.width - this.padding * 2 - 2;
}
innerHeight() {
return this.height - this.padding * 2 - 2;
}
textx(align) {
const { width, padding } = this;
let { x } = this;
if (align === 'left') {
x += padding;
} else if (align === 'center') {
x += width / 2;
} else if (align === 'right') {
x += width - padding;
}
return x;
}
texty(align, h) {
const { height, padding } = this;
let { y } = this;
if (align === 'top') {
y += padding;
} else if (align === 'middle') {
y += height / 2 - h / 2;
} else if (align === 'bottom') {
y += height - padding - h;
}
return y;
}
topxys() {
const { x, y, width } = this;
return [
[x, y],
[x + width, y],
];
}
rightxys() {
const { x, y, width, height } = this;
return [
[x + width, y],
[x + width, y + height],
];
}
bottomxys() {
const { x, y, width, height } = this;
return [
[x, y + height],
[x + width, y + height],
];
}
leftxys() {
const { x, y, height } = this;
return [
[x, y],
[x, y + height],
];
}
}
function drawFontLine(type, tx, ty, align, valign, blheight, blwidth) {
const floffset = { x: 0, y: 0 };
if (type === 'underline') {
if (valign === 'bottom') {
floffset.y = 0;
} else if (valign === 'top') {
floffset.y = -(blheight + 2);
} else {
floffset.y = -blheight / 2;
}
} else if (type === 'strike') {
if (valign === 'bottom') {
floffset.y = blheight / 2;
} else if (valign === 'top') {
floffset.y = -(blheight / 2 + 2);
}
}
if (align === 'center') {
floffset.x = blwidth / 2;
} else if (align === 'right') {
floffset.x = blwidth;
}
this.line([tx - floffset.x, ty - floffset.y], [tx - floffset.x + blwidth, ty - floffset.y]);
}
class Draw {
constructor(el, width, height) {
this.el = el;
this.ctx = el.getContext('2d');
this.resize(width, height);
this.ctx.scale(dpr(), dpr());
}
resize(width, height) {
// console.log('dpr:', dpr);
this.el.style.width = `${width}px`;
this.el.style.height = `${height}px`;
this.el.width = npx(width);
this.el.height = npx(height);
}
clear() {
const { width, height } = this.el;
this.ctx.clearRect(0, 0, width, height);
return this;
}
attr(options) {
Object.assign(this.ctx, options);
return this;
}
save() {
this.ctx.save();
this.ctx.beginPath();
return this;
}
restore() {
this.ctx.restore();
return this;
}
beginPath() {
this.ctx.beginPath();
return this;
}
translate(x, y) {
this.ctx.translate(npx(x), npx(y));
return this;
}
scale(x, y) {
this.ctx.scale(x, y);
return this;
}
clearRect(x, y, w, h) {
this.ctx.clearRect(x, y, w, h);
return this;
}
fillRect(x, y, w, h) {
this.ctx.fillRect(npx(x) - 0.5, npx(y) - 0.5, npx(w), npx(h));
return this;
}
fillText(text, x, y) {
this.ctx.fillText(text, npx(x), npx(y));
return this;
}
/**
* <20><>ͼƬ<CDBC><C6AC>
* @param {*} box - һ<><D2BB> DrawBox <20><><EFBFBD><EFBFBD>
* @param {string} src - ͼƬ<CDBC><C6AC>·<EFBFBD><C2B7>
* @param {Object} fixedIndexWidth - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
* @param {Object} fixedIndexHeight - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߶<EFBFBD>
*/
fillImage(box, { value: src }, { fixedIndexWidth, fixedIndexHeight }, scroll, celldata) {
if (!celldata.imagewidth) {
return;
}
if (celldata.value == '' || celldata.value == undefined) {
imageMap.forEach((value, key) => {
if (value[0] === celldata.scaledWidth && value[1] === celldata.scaledHeight) {
imageMap.delete(key);
}
});
}
const imageTop = celldata.top;
const imageLeft = celldata.left;
const { x, y, width, height } = box;
// if(!((imageTop ==y && imageLeft == x) || (imageTop ==y-scroll.scroll.y && imageLeft == x-scroll.scroll.x)))
// {return}
const img = new Image();
img.src = src;
img.onload = () => {
this.ctx.save();
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ͻ<EFBFBD>λ<EFBFBD>ã<EFBFBD>Ϊʲôtranslateû<65><C3BB><EFBFBD><EFBFBD>Ч<EFBFBD>أ<EFBFBD><D8A3><EFBFBD>Ϊ<EFBFBD><EFBFBD><ECB2BD><EFBFBD><EFBFBD>
let sx = x + fixedIndexWidth;
let sy = y + fixedIndexHeight;
if (scroll) {
sx = sx - scroll.scroll.x + 0;
sy = sy - scroll.scroll.y + 0;
}
//<2F><><EFBFBD><EFBFBD><E3B3A4><EFBFBD><EFBFBD>
const imageWidth = celldata.imagewidth;
const imageHeight = celldata.imageheight;
const imageH = height - 2; // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD>ʿ<EFBFBD><CABF>߱ȣ<DFB1>ֱ<EFBFBD><D6B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ԫ<EFBFBD><D4AA>
const gridCellWidth = width;
const gridCellHeight = height;
let widthRatio = gridCellWidth / imageWidth;
let heightRatio = gridCellHeight / imageHeight;
let scaleRatio = Math.min(widthRatio, heightRatio);
let scaledWidth = imageWidth * scaleRatio;
let scaledHeight = imageHeight * scaleRatio;
if (imageMap.has(img.src)) {
// scaledWidth = imageMap.get(img.src)[0]
// scaledHeight = imageMap.get(img.src)[1]
// celldata.scaledWidth = scaledWidth
// celldata.scaledHeight = scaledHeight
// celldata.scaleRatio = scaleRatio
//<2F><><EFBFBD><EFBFBD>ͼƬʱ<C6AC>Ѵ<EFBFBD><D1B4><EFBFBD>ƫ<EFBFBD><C6AB>
if (imageMap.get(img.src)[2] != 0 || imageMap.get(img.src)[3] != 0) {
//<2F><><EFBFBD>¼<EFBFBD><C2BC><EFBFBD><EFBFBD>ʼλ<CABC><CEBB>
// if (scroll.scroll.x > imageMap.get(img.src)[2]) {
// sx = sx + (scroll.scroll.x - imageMap.get(img.src)[2])
// } else {
// sx = sx - (imageMap.get(img.src)[2] - scroll.scroll.x)
// }
// if (scroll.scroll.y > imageMap.get(img.src)[3]) {
// sy = sy + (scroll.scroll.y - imageMap.get(img.src)[3])
// } else {
// sy = sy - (imageMap.get(img.src)[3] - scroll.scroll.y)
// }
// sx = sx - (scroll.scroll.x - imageMap.get(img.src)[2]) - 5
// sy = sy - (scroll.scroll.y - imageMap.get(img.src)[3]) - 5
// <20><>ֹ<EFBFBD>ظ<EFBFBD><D8B8><EFBFBD>Ⱦ
if (imageTop == y - scroll.scroll.y && imageLeft == x - scroll.scroll.x) {
this.ctx.drawImage(img, npx(sx), npx(sy), npx(imageWidth), npx(imageH));
this.ctx.restore();
}
} else {
// if (imageTop == y && imageLeft == x) {
this.ctx.drawImage(img, npx(sx), npx(sy), npx(imageWidth), npx(imageH));
this.ctx.restore();
// }
}
} else {
// imageMap.set(img.src, [scaledWidth, scaledHeight, scroll.scroll.x, scroll.scroll.y])
// celldata.scaledWidth = scaledWidth
// celldata.scaledHeight = scaledHeight
// celldata.scaleRatio = scaleRatio
// if (imageTop == y - scroll.scroll.y && imageLeft == x - scroll.scroll.x) {
this.ctx.drawImage(img, npx(sx), npx(sy), npx(imageWidth), npx(imageH));
this.ctx.restore();
// }
}
};
return this;
}
/**
* <20><><EFBFBD><EFBFBD>ͼ<EFBFBD>Ρ<EFBFBD>
* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFB1BE><EFBFBD><EFBFBD><EFBFBD>ο<EFBFBD>text<78><74><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߼<EFBFBD><DFBC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ԫ<EFBFBD><D4AA>Ϊ radio, checkbox, date ʱ<><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧ<EFBFBD><D3A6>ͼ<EFBFBD>Ρ<EFBFBD>
* @param {Object} cell - <20><>Ԫ<EFBFBD><D4AA>
* @param {Object} box - DrawBox
* @param {Object} fixedIndexWidth - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
* @param {Object} fixedIndexHeight - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߶<EFBFBD>
* @returns {Draw} CanvasRenderingContext2D ʵ<><CAB5>
*/
async geometry(cell, box, { fixedIndexWidth, fixedIndexHeight }, style, scroll, celldata) {
const { type } = cell;
switch (type) {
case 'image':
await this.fillImage(box, cell, { fixedIndexWidth, fixedIndexHeight }, scroll, celldata);
break;
default:
}
return this;
}
/*
txt: render text
box: DrawBox
attr: {
align: left | center | right
valign: top | middle | bottom
color: '#333333',
strike: false,
font: {
name: 'Arial',
size: 14,
bold: false,
italic: false,
}
}
textWrap: text wrapping
*/
text(mtxt, box, attr = {}, textWrap = true) {
const { ctx } = this;
const { align, valign, font, color, strike, underline } = attr;
const tx = box.textx(align);
ctx.save();
ctx.beginPath();
this.attr({
textAlign: align,
textBaseline: valign,
font: `${font.italic ? 'italic' : ''} ${font.bold ? 'bold' : ''} ${npx(font.size)}px ${
font.name
}`,
fillStyle: color,
strokeStyle: color,
});
const txts = `${mtxt}`.split('\n');
const biw = box.innerWidth();
const ntxts = [];
txts.forEach(it => {
const txtWidth = ctx.measureText(it).width;
if (textWrap && txtWidth > npx(biw)) {
let textLine = { w: 0, len: 0, start: 0 };
for (let i = 0; i < it.length; i += 1) {
if (textLine.w >= npx(biw)) {
ntxts.push(it.substr(textLine.start, textLine.len));
textLine = { w: 0, len: 0, start: i };
}
textLine.len += 1;
textLine.w += ctx.measureText(it[i]).width + 1;
}
if (textLine.len > 0) {
ntxts.push(it.substr(textLine.start, textLine.len));
}
} else {
ntxts.push(it);
}
});
const txtHeight = (ntxts.length - 1) * (font.size + 2);
let ty = box.texty(valign, txtHeight);
ntxts.forEach(txt => {
const txtWidth = ctx.measureText(txt).width;
this.fillText(txt, tx, ty);
if (strike) {
drawFontLine.call(this, 'strike', tx, ty, align, valign, font.size, txtWidth);
}
if (underline) {
drawFontLine.call(this, 'underline', tx, ty, align, valign, font.size, txtWidth);
}
ty += font.size + 2;
});
ctx.restore();
return this;
}
border(style, color) {
const { ctx } = this;
ctx.lineWidth = thinLineWidth;
ctx.strokeStyle = color;
// console.log('style:', style);
if (style === 'medium') {
ctx.lineWidth = npx(2) - 0.5;
} else if (style === 'thick') {
ctx.lineWidth = npx(3);
} else if (style === 'dashed') {
ctx.setLineDash([npx(3), npx(2)]);
} else if (style === 'dotted') {
ctx.setLineDash([npx(1), npx(1)]);
} else if (style === 'double') {
ctx.setLineDash([npx(2), 0]);
}
return this;
}
line(...xys) {
const { ctx } = this;
if (xys.length > 1) {
ctx.beginPath();
const [x, y] = xys[0];
ctx.moveTo(npxLine(x), npxLine(y));
for (let i = 1; i < xys.length; i += 1) {
const [x1, y1] = xys[i];
ctx.lineTo(npxLine(x1), npxLine(y1));
}
ctx.stroke();
}
return this;
}
strokeBorders(box) {
const { ctx } = this;
ctx.save();
// border
const { borderTop, borderRight, borderBottom, borderLeft } = box;
if (borderTop) {
this.border(...borderTop);
// console.log('box.topxys:', box.topxys());
this.line(...box.topxys());
}
if (borderRight) {
this.border(...borderRight);
this.line(...box.rightxys());
}
if (borderBottom) {
this.border(...borderBottom);
this.line(...box.bottomxys());
}
if (borderLeft) {
this.border(...borderLeft);
this.line(...box.leftxys());
}
ctx.restore();
}
dropdown(box) {
const { ctx } = this;
const { x, y, width, height } = box;
const sx = x + width - 15;
const sy = y + height - 15;
ctx.save();
ctx.beginPath();
ctx.moveTo(npx(sx), npx(sy));
ctx.lineTo(npx(sx + 8), npx(sy));
ctx.lineTo(npx(sx + 4), npx(sy + 6));
ctx.closePath();
ctx.fillStyle = 'rgba(0, 0, 0, .45)';
ctx.fill();
ctx.restore();
}
error(box) {
const { ctx } = this;
const { x, y, width } = box;
const sx = x + width - 1;
ctx.save();
ctx.beginPath();
ctx.moveTo(npx(sx - 8), npx(y - 1));
ctx.lineTo(npx(sx), npx(y - 1));
ctx.lineTo(npx(sx), npx(y + 8));
ctx.closePath();
ctx.fillStyle = 'rgba(255, 0, 0, .65)';
ctx.fill();
ctx.restore();
}
frozen(box) {
const { ctx } = this;
const { x, y, width } = box;
const sx = x + width - 1;
ctx.save();
ctx.beginPath();
ctx.moveTo(npx(sx - 8), npx(y - 1));
ctx.lineTo(npx(sx), npx(y - 1));
ctx.lineTo(npx(sx), npx(y + 8));
ctx.closePath();
ctx.fillStyle = 'rgba(0, 255, 0, .85)';
ctx.fill();
ctx.restore();
}
rect(box, dtextcb) {
const { ctx } = this;
const { x, y, width, height, bgcolor } = box;
ctx.save();
ctx.beginPath();
ctx.fillStyle = bgcolor || '#fff';
ctx.rect(npxLine(x + 1), npxLine(y + 1), npx(width - 2), npx(height - 2));
ctx.clip();
ctx.fill();
dtextcb();
ctx.restore();
}
}
export default {};
export { Draw, DrawBox, thinLineWidth, npx };

View File

@@ -0,0 +1,90 @@
class Draw {
constructor(el) {
this.el = el;
this.ctx = el.getContext('2d');
}
clear() {
const { width, height } = this.el;
this.ctx.clearRect(0, 0, width, height);
return this;
}
attr(m) {
Object.assign(this.ctx, m);
return this;
}
save() {
this.ctx.save();
this.ctx.beginPath();
return this;
}
restore() {
this.ctx.restore();
return this;
}
beginPath() {
this.ctx.beginPath();
return this;
}
closePath() {
this.ctx.closePath();
return this;
}
measureText(text) {
return this.ctx.measureText(text);
}
rect(x, y, width, height) {
this.ctx.rect(x, y, width, height);
return this;
}
scale(x, y) {
this.ctx.scale(x, y);
return this;
}
rotate(angle) {
this.ctx.rotate(angle);
return this;
}
translate(x, y) {
this.ctx.translate(x, y);
return this;
}
transform(a, b, c, d, e) {
this.ctx.transform(a, b, c, d, e);
return this;
}
fillRect(x, y, w, h) {
this.ctx.fillRect(x, y, w, h);
return this;
}
strokeRect(x, y, w, h) {
this.ctx.strokeRect(x, y, w, h);
return this;
}
fillText(text, x, y, maxWidth) {
this.ctx.fillText(text, x, y, maxWidth);
return this;
}
strokeText(text, x, y, maxWidth) {
this.ctx.strokeText(text, x, y, maxWidth);
return this;
}
}
export default {};
export { Draw };

View File

@@ -0,0 +1,62 @@
import { cssPrefix } from '../config.js';
import { h } from './element.js';
import Icon from './icon.js';
import DropdownColor from './dropdown_color.js';
import DropdownLineType from './dropdown_linetype.js';
function buildTable(...trs) {
return h('table', '').child(h('tbody', '').children(...trs));
}
function buildTd(iconName) {
return h('td', '').child(
h('div', `${cssPrefix}-border-palette-cell`)
.child(new Icon(`border-${iconName}`))
.on('click', () => {
this.mode = iconName;
const { mode, style, color } = this;
this.change({ mode, style, color });
}),
);
}
export default class BorderPalette {
constructor() {
this.color = '#000';
this.style = 'thin';
this.mode = 'all';
this.change = () => {
console.log('empty function');
};
this.ddColor = new DropdownColor('line-color', this.color);
this.ddColor.change = color => {
this.color = color;
};
this.ddType = new DropdownLineType(this.style);
this.ddType.change = ([s]) => {
this.style = s;
};
this.el = h('div', `${cssPrefix}-border-palette`);
const table = buildTable(
h('tr', '').children(
h('td', `${cssPrefix}-border-palette-left`).child(
buildTable(
h('tr', '').children(
...['all', 'inside', 'horizontal', 'vertical', 'outside'].map(it =>
buildTd.call(this, it),
),
),
h('tr', '').children(
...['left', 'top', 'right', 'bottom', 'none'].map(it => buildTd.call(this, it)),
),
),
),
h('td', `${cssPrefix}-border-palette-right`).children(
h('div', `${cssPrefix}-toolbar-btn`).child(this.ddColor.el),
h('div', `${cssPrefix}-toolbar-btn`).child(this.ddType.el),
),
),
);
this.el.child(table);
}
}

View File

@@ -0,0 +1,204 @@
import { cssPrefix } from '../config.js';
import { tf } from '../locale/locale.js';
import { h } from './element.js';
import { bindClickoutside, unbindClickoutside } from './event.js';
import Icon from './icon.js';
import FormInput from './form_input.js';
import Dropdown from './dropdown.js';
// Record: temp not used
// import { xtoast } from './message.js';
class DropdownMore extends Dropdown {
constructor(click) {
const icon = new Icon('ellipsis');
super(icon, 'auto', false, 'top-left');
this.contentClick = click;
}
reset(items) {
const eles = items.map((it, i) =>
h('div', `${cssPrefix}-item`)
.css('width', '150px')
.css('font-weight', 'normal')
.on('click', () => {
this.contentClick(i);
this.hide();
})
.child(it),
);
this.setContentChildren(...eles);
}
setTitle() {
console.log('empty function');
}
}
const menuItems = [{ key: 'delete', title: tf('contextmenu.deleteSheet') }];
function buildMenuItem(item) {
return h('div', `${cssPrefix}-item`)
.child(item.title())
.on('click', () => {
this.itemClick(item.key);
this.hide();
});
}
function buildMenu() {
return menuItems.map(it => buildMenuItem.call(this, it));
}
class ContextMenu {
constructor() {
this.el = h('div', `${cssPrefix}-contextmenu`)
.css('width', '160px')
.children(...buildMenu.call(this))
.hide();
this.itemClick = () => {
console.log('empty function');
};
}
hide() {
const { el } = this;
el.hide();
unbindClickoutside(el);
}
setOffset(offset) {
const { el } = this;
el.offset(offset);
el.show();
bindClickoutside(el);
}
}
export default class Bottombar {
constructor(
addFunc = () => {
console.log('empty function');
},
swapFunc = () => {
console.log('empty function');
},
deleteFunc = () => {
console.log('empty function');
},
updateFunc = () => {
console.log('empty function');
},
) {
this.swapFunc = swapFunc;
this.updateFunc = updateFunc;
this.dataNames = [];
this.activeEl = null;
this.deleteEl = null;
this.items = [];
this.moreEl = new DropdownMore(i => {
this.clickSwap2(this.items[i]);
});
this.contextMenu = new ContextMenu();
this.contextMenu.itemClick = deleteFunc;
this.el = h('div', `${cssPrefix}-bottombar`).children(
this.contextMenu.el,
(this.menuEl = h('ul', `${cssPrefix}-menu`).child(
h('li', '').children(
new Icon('add').on('click', () => {
addFunc();
}),
h('span', '').child(this.moreEl),
),
)),
);
}
addItem(name, active, options) {
this.dataNames.push(name);
const item = h('li', active ? 'active' : '').child(name);
item
.on('click', () => {
this.clickSwap2(item);
})
.on('contextmenu', evt => {
if (options.mode === 'read') return;
const { offsetLeft, offsetHeight } = evt.target;
this.contextMenu.setOffset({ left: offsetLeft, bottom: offsetHeight + 1 });
this.deleteEl = item;
})
.on('dblclick', () => {
if (options.mode === 'read') return;
const v = item.html();
const input = new FormInput('auto', '');
input.val(v);
input.input.on('blur', ({ target }) => {
const { value } = target;
const nindex = this.dataNames.findIndex(it => it === v);
this.renameItem(nindex, value);
/*
this.dataNames.splice(nindex, 1, value);
this.moreEl.reset(this.dataNames);
item.html('').child(value);
this.updateFunc(nindex, value);
*/
});
item.html('').child(input.el);
input.focus();
});
if (active) {
this.clickSwap(item);
}
this.items.push(item);
this.menuEl.child(item);
this.moreEl.reset(this.dataNames);
}
renameItem(index, value) {
this.dataNames.splice(index, 1, value);
this.moreEl.reset(this.dataNames);
this.items[index].html('').child(value);
this.updateFunc(index, value);
}
clear() {
this.items.forEach(it => {
this.menuEl.removeChild(it.el);
});
this.items = [];
this.dataNames = [];
this.moreEl.reset(this.dataNames);
}
deleteItem() {
const { activeEl, deleteEl } = this;
if (this.items.length > 1) {
const index = this.items.findIndex(it => it === deleteEl);
this.items.splice(index, 1);
this.dataNames.splice(index, 1);
this.menuEl.removeChild(deleteEl.el);
this.moreEl.reset(this.dataNames);
if (activeEl === deleteEl) {
const [f] = this.items;
this.activeEl = f;
this.activeEl.toggle();
return [index, 0];
}
return [index, -1];
}
return [-1];
}
clickSwap2(item) {
const index = this.items.findIndex(it => it === item);
this.clickSwap(item);
this.activeEl.toggle();
this.swapFunc(index);
}
clickSwap(item) {
if (this.activeEl !== null) {
this.activeEl.toggle();
}
this.activeEl = item;
}
}

View File

@@ -0,0 +1,11 @@
import { cssPrefix } from '../config.js';
import { t } from '../locale/locale.js';
import { Element } from './element.js';
export default class Button extends Element {
// type: primary
constructor(title, type = '') {
super('div', `${cssPrefix}-button ${type}`);
this.child(t(`button.${title}`));
}
}

View File

@@ -0,0 +1,112 @@
import { t } from '../locale/locale.js';
import { h } from './element.js';
import Icon from './icon.js';
function addMonth(date, step) {
date.setMonth(date.getMonth() + step);
}
function weekday(date, index) {
const d = new Date(date);
d.setDate(index - date.getDay() + 1);
return d;
}
function monthDays(year, month, cdate) {
// the first day of month
const startDate = new Date(year, month, 1, 23, 59, 59);
const datess = [[], [], [], [], [], []];
for (let i = 0; i < 6; i += 1) {
for (let j = 0; j < 7; j += 1) {
const index = i * 7 + j;
const d = weekday(startDate, index);
const disabled = d.getMonth() !== month;
// console.log('d:', d, ', cdate:', cdate);
const active = d.getMonth() === cdate.getMonth() && d.getDate() === cdate.getDate();
datess[i][j] = { d, disabled, active };
}
}
return datess;
}
export default class Calendar {
constructor(value) {
this.value = value;
this.cvalue = new Date(value);
this.headerLeftEl = h('div', 'calendar-header-left');
this.bodyEl = h('tbody', '');
this.buildAll();
this.el = h('div', 'x-spreadsheet-calendar').children(
h('div', 'calendar-header').children(
this.headerLeftEl,
h('div', 'calendar-header-right').children(
h('a', 'calendar-prev')
.on('click.stop', () => this.prev())
.child(new Icon('chevron-left')),
h('a', 'calendar-next')
.on('click.stop', () => this.next())
.child(new Icon('chevron-right')),
),
),
h('table', 'calendar-body').children(
h('thead', '').child(
h('tr', '').children(...t('calendar.weeks').map(week => h('th', 'cell').child(week))),
),
this.bodyEl,
),
);
this.selectChange = () => {
console.log('empty function');
};
}
setValue(value) {
this.value = value;
this.cvalue = new Date(value);
this.buildAll();
}
prev() {
const { value } = this;
addMonth(value, -1);
this.buildAll();
}
next() {
const { value } = this;
addMonth(value, 1);
this.buildAll();
}
buildAll() {
this.buildHeaderLeft();
this.buildBody();
}
buildHeaderLeft() {
const { value } = this;
this.headerLeftEl.html(`${t('calendar.months')[value.getMonth()]} ${value.getFullYear()}`);
}
buildBody() {
const { value, cvalue, bodyEl } = this;
const mDays = monthDays(value.getFullYear(), value.getMonth(), cvalue);
const trs = mDays.map(it => {
const tds = it.map(it1 => {
let cls = 'cell';
if (it1.disabled) cls += ' disabled';
if (it1.active) cls += ' active';
return h('td', '').child(
h('div', cls)
.on('click.stop', () => {
this.selectChange(it1.d);
})
.child(it1.d.getDate().toString()),
);
});
return h('tr', '').children(...tds);
});
bodyEl.html('').children(...trs);
}
}

View File

@@ -0,0 +1,124 @@
import { cssPrefix } from '../config.js';
import { h } from './element.js';
const themeColorPlaceHolders = [
'#ffffff',
'#000100',
'#e7e5e6',
'#445569',
'#5b9cd6',
'#ed7d31',
'#a5a5a5',
'#ffc001',
'#4371c6',
'#71ae47',
];
const themeColors = [
[
'#f2f2f2',
'#7f7f7f',
'#d0cecf',
'#d5dce4',
'#deeaf6',
'#fce5d5',
'#ededed',
'#fff2cd',
'#d9e2f3',
'#e3efd9',
],
[
'#d8d8d8',
'#595959',
'#afabac',
'#adb8ca',
'#bdd7ee',
'#f7ccac',
'#dbdbdb',
'#ffe59a',
'#b3c6e7',
'#c5e0b3',
],
[
'#bfbfbf',
'#3f3f3f',
'#756f6f',
'#8596b0',
'#9cc2e6',
'#f4b184',
'#c9c9c9',
'#fed964',
'#8eaada',
'#a7d08c',
],
[
'#a5a5a5',
'#262626',
'#3a3839',
'#333f4f',
'#2e75b5',
'#c45a10',
'#7b7b7b',
'#bf8e01',
'#2f5596',
'#538136',
],
[
'#7f7f7f',
'#0c0c0c',
'#171516',
'#222a35',
'#1f4e7a',
'#843c0a',
'#525252',
'#7e6000',
'#203864',
'#365624',
],
];
const standardColors = [
'#c00000',
'#fe0000',
'#fdc101',
'#ffff01',
'#93d051',
'#00b04e',
'#01b0f1',
'#0170c1',
'#012060',
'#7030a0',
];
function buildTd(bgcolor) {
return h('td', '').child(
h('div', `${cssPrefix}-color-palette-cell`)
.on('click.stop', () => this.change(bgcolor))
.css('background-color', bgcolor),
);
}
export default class ColorPalette {
constructor() {
this.el = h('div', `${cssPrefix}-color-palette`);
this.change = () => {
console.log('empty function');
};
const table = h('table', '').children(
h('tbody', '').children(
h('tr', `${cssPrefix}-theme-color-placeholders`).children(
...themeColorPlaceHolders.map(color => buildTd.call(this, color)),
),
...themeColors.map(it =>
h('tr', `${cssPrefix}-theme-colors`).children(
...it.map(color => buildTd.call(this, color)),
),
),
h('tr', `${cssPrefix}-standard-colors`).children(
...standardColors.map(color => buildTd.call(this, color)),
),
),
);
this.el.child(table);
}
}

View File

@@ -0,0 +1,99 @@
import { cssPrefix } from '../config.js';
import { tf } from '../locale/locale.js';
import { h } from './element.js';
import { bindClickoutside, unbindClickoutside } from './event.js';
const menuItems = [
{ key: 'copy', title: tf('contextmenu.copy'), label: 'Ctrl+C' },
{ key: 'cut', title: tf('contextmenu.cut'), label: 'Ctrl+X' },
{ key: 'paste', title: tf('contextmenu.paste'), label: 'Ctrl+V' },
{ key: 'paste-value', title: tf('contextmenu.pasteValue'), label: 'Ctrl+Shift+V' },
{ key: 'paste-format', title: tf('contextmenu.pasteFormat'), label: 'Ctrl+Alt+V' },
{ key: 'divider' },
{ key: 'insert-row', title: tf('contextmenu.insertRow') },
{ key: 'insert-column', title: tf('contextmenu.insertColumn') },
{ key: 'divider' },
{ key: 'delete-row', title: tf('contextmenu.deleteRow') },
{ key: 'delete-column', title: tf('contextmenu.deleteColumn') },
{ key: 'delete-cell-text', title: tf('contextmenu.deleteCellText') },
{ key: 'hide', title: tf('contextmenu.hide') },
{ key: 'divider' },
{ key: 'validation', title: tf('contextmenu.validation') },
{ key: 'divider' },
{ key: 'cell-printable', title: tf('contextmenu.cellprintable') },
{ key: 'cell-non-printable', title: tf('contextmenu.cellnonprintable') },
{ key: 'divider' },
{ key: 'cell-editable', title: tf('contextmenu.celleditable') },
{ key: 'cell-non-editable', title: tf('contextmenu.cellnoneditable') },
];
function buildMenuItem(item) {
if (item.key === 'divider') {
return h('div', `${cssPrefix}-item divider`);
}
return h('div', `${cssPrefix}-item`)
.on('click', () => {
this.itemClick(item.key);
this.hide();
})
.children(item.title(), h('div', 'label').child(item.label || ''));
}
function buildMenu() {
return menuItems.map(it => buildMenuItem.call(this, it));
}
export default class ContextMenu {
constructor(viewFn, isHide = false) {
this.menuItems = buildMenu.call(this);
this.el = h('div', `${cssPrefix}-contextmenu`)
.children(...this.menuItems)
.hide();
this.viewFn = viewFn;
this.itemClick = () => {
console.log('empty function');
};
this.isHide = isHide;
this.setMode('range');
}
// row-col: the whole rows or the whole cols
// range: select range
setMode(mode) {
const hideEl = this.menuItems[12];
if (mode === 'row-col') {
hideEl.show();
} else {
hideEl.hide();
}
}
hide() {
const { el } = this;
el.hide();
unbindClickoutside(el);
}
setPosition(x, y) {
if (this.isHide) return;
const { el } = this;
const { width } = el.show().offset();
const view = this.viewFn();
const vhf = view.height / 2;
let left = x;
if (view.width - x <= width) {
left -= width;
}
el.css('left', `${left}px`);
if (y > vhf) {
el.css('bottom', `${view.height - y}px`)
.css('max-height', `${y}px`)
.css('top', 'auto');
} else {
el.css('top', `${y}px`)
.css('max-height', `${view.height - y}px`)
.css('bottom', 'auto');
}
bindClickoutside(el);
}
}

View File

@@ -0,0 +1,39 @@
import { cssPrefix } from '../config.js';
import Calendar from './calendar.js';
import { h } from './element.js';
export default class Datepicker {
constructor() {
this.calendar = new Calendar(new Date());
this.el = h('div', `${cssPrefix}-datepicker`).child(this.calendar.el).hide();
}
setValue(date) {
// console.log(':::::::', date, typeof date, date instanceof string);
const { calendar } = this;
if (typeof date === 'string') {
// console.log(/^\d{4}-\d{1,2}-\d{1,2}$/.test(date));
if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(date)) {
calendar.setValue(new Date(date.replace(new RegExp('-', 'g'), '/')));
}
} else if (date instanceof Date) {
calendar.setValue(date);
}
return this;
}
change(cb) {
this.calendar.selectChange = d => {
cb(d);
this.hide();
};
}
show() {
this.el.show();
}
hide() {
this.el.hide();
}
}

View File

@@ -0,0 +1,70 @@
import { cssPrefix } from '../config.js';
import { Element, h } from './element.js';
import { bindClickoutside, unbindClickoutside } from './event.js';
export default class Dropdown extends Element {
constructor(title, width, showArrow, placement, ...children) {
super('div', `${cssPrefix}-dropdown ${placement}`);
this.title = title;
this.change = () => {
console.log('empty function');
};
this.headerClick = () => {
console.log('empty function');
};
if (typeof title === 'string') {
this.title = h('div', `${cssPrefix}-dropdown-title`).child(title);
} else if (showArrow) {
this.title.addClass('arrow-left');
}
this.contentEl = h('div', `${cssPrefix}-dropdown-content`).css('width', width).hide();
this.setContentChildren(...children);
this.headerEl = h('div', `${cssPrefix}-dropdown-header`);
this.headerEl
.on('click', () => {
if (this.contentEl.css('display') !== 'block') {
this.show();
} else {
this.hide();
}
})
.children(
this.title,
showArrow
? h('div', `${cssPrefix}-icon arrow-right`).child(
h('div', `${cssPrefix}-icon-img arrow-down`),
)
: '',
);
this.children(this.headerEl, this.contentEl);
}
setContentChildren(...children) {
this.contentEl.html('');
if (children.length > 0) {
this.contentEl.children(...children);
}
}
setTitle(title) {
this.title.html(title);
this.hide();
}
show() {
const { contentEl } = this;
contentEl.show();
this.parent().active();
bindClickoutside(this.parent(), () => {
this.hide();
});
}
hide() {
this.parent().active(false);
this.contentEl.hide();
unbindClickoutside(this.parent());
}
}

View File

@@ -0,0 +1,26 @@
import { cssPrefix } from '../config.js';
import Dropdown from './dropdown.js';
import { h } from './element.js';
import Icon from './icon.js';
function buildItemWithIcon(iconName) {
return h('div', `${cssPrefix}-item`).child(new Icon(iconName));
}
export default class DropdownAlign extends Dropdown {
constructor(aligns, align) {
const icon = new Icon(`align-${align}`);
const naligns = aligns.map(it =>
buildItemWithIcon(`align-${it}`).on('click', () => {
this.setTitle(it);
this.change(it);
}),
);
super(icon, 'auto', true, 'bottom-left', ...naligns);
}
setTitle(align) {
this.title.setName(`align-${align}`);
this.hide();
}
}

View File

@@ -0,0 +1,15 @@
import Dropdown from './dropdown.js';
import Icon from './icon.js';
import BorderPalette from './border_palette.js';
export default class DropdownBorder extends Dropdown {
constructor() {
const icon = new Icon('border-all');
const borderPalette = new BorderPalette();
borderPalette.change = v => {
this.change(v);
this.hide();
};
super(icon, 'auto', false, 'bottom-left', borderPalette.el);
}
}

View File

@@ -0,0 +1,22 @@
import Dropdown from './dropdown.js';
import Icon from './icon.js';
import ColorPalette from './color_palette.js';
export default class DropdownColor extends Dropdown {
constructor(iconName, color) {
const icon = new Icon(iconName)
.css('height', '16px')
.css('border-bottom', `3px solid ${color}`);
const colorPalette = new ColorPalette();
colorPalette.change = v => {
this.setTitle(v);
this.change(v);
};
super(icon, 'auto', false, 'bottom-left', colorPalette.el);
}
setTitle(color) {
this.title.css('border-color', color);
this.hide();
}
}

View File

@@ -0,0 +1,18 @@
import { baseFonts } from '../core/font.js';
import { cssPrefix } from '../config.js';
import Dropdown from './dropdown.js';
import { h } from './element.js';
export default class DropdownFont extends Dropdown {
constructor() {
const nfonts = baseFonts.map(it =>
h('div', `${cssPrefix}-item`)
.on('click', () => {
this.setTitle(it.title);
this.change(it);
})
.child(it.title),
);
super(baseFonts[0].title, '160px', true, 'bottom-left', ...nfonts);
}
}

View File

@@ -0,0 +1,18 @@
import { fontSizes } from '../core/font.js';
import { cssPrefix } from '../config.js';
import Dropdown from './dropdown.js';
import { h } from './element.js';
export default class DropdownFontSize extends Dropdown {
constructor() {
const nfontSizes = fontSizes.map(it =>
h('div', `${cssPrefix}-item`)
.on('click', () => {
this.setTitle(`${it.pt}`);
this.change(it);
})
.child(`${it.pt}`),
);
super('10', '60px', true, 'bottom-left', ...nfontSizes);
}
}

View File

@@ -0,0 +1,35 @@
import { baseFormats } from '../core/format.js';
import { cssPrefix } from '../config.js';
import Dropdown from './dropdown.js';
import { h } from './element.js';
export default class DropdownFormat extends Dropdown {
constructor() {
let nformats = baseFormats.slice(0);
nformats.splice(2, 0, { key: 'divider' });
nformats.splice(8, 0, { key: 'divider' });
nformats = nformats.map(it => {
const item = h('div', `${cssPrefix}-item`);
if (it.key === 'divider') {
item.addClass('divider');
} else {
item.child(it.title()).on('click', () => {
this.setTitle(it.title());
this.change(it);
});
if (it.label) item.child(h('div', 'label').html(it.label));
}
return item;
});
super('Normal', '220px', true, 'bottom-left', ...nformats);
}
setTitle(key) {
for (let i = 0; i < baseFormats.length; i += 1) {
if (baseFormats[i].key === key) {
this.title.html(baseFormats[i].title());
}
}
this.hide();
}
}

View File

@@ -0,0 +1,19 @@
import { baseFormulas } from '../core/formula.js';
import { cssPrefix } from '../config.js';
import Dropdown from './dropdown.js';
import Icon from './icon.js';
import { h } from './element.js';
export default class DropdownFormula extends Dropdown {
constructor() {
const nformulas = baseFormulas.map(it =>
h('div', `${cssPrefix}-item`)
.on('click', () => {
this.hide();
this.change(it);
})
.child(it.key),
);
super(new Icon('formula'), '180px', true, 'bottom-left', ...nformulas);
}
}

View File

@@ -0,0 +1,48 @@
import { cssPrefix } from '../config.js';
import Dropdown from './dropdown.js';
import { h } from './element.js';
import Icon from './icon.js';
const lineTypes = [
[
'thin',
'<svg xmlns="http://www.w3.org/2000/svg" width="50" height="1" style="user-select: none;"><line x1="0" y1="0.5" x2="50" y2="0.5" stroke-width="1" stroke="black" style="user-select: none;"></line></svg>',
],
[
'medium',
'<svg xmlns="http://www.w3.org/2000/svg" width="50" height="2" style="user-select: none;"><line x1="0" y1="1.0" x2="50" y2="1.0" stroke-width="2" stroke="black" style="user-select: none;"></line></svg>',
],
[
'thick',
'<svg xmlns="http://www.w3.org/2000/svg" width="50" height="3" style="user-select: none;"><line x1="0" y1="1.5" x2="50" y2="1.5" stroke-width="3" stroke="black" style="user-select: none;"></line></svg>',
],
[
'dashed',
'<svg xmlns="http://www.w3.org/2000/svg" width="50" height="1" style="user-select: none;"><line x1="0" y1="0.5" x2="50" y2="0.5" stroke-width="1" stroke="black" stroke-dasharray="2" style="user-select: none;"></line></svg>',
],
[
'dotted',
'<svg xmlns="http://www.w3.org/2000/svg" width="50" height="1" style="user-select: none;"><line x1="0" y1="0.5" x2="50" y2="0.5" stroke-width="1" stroke="black" stroke-dasharray="1" style="user-select: none;"></line></svg>',
],
// ['double', '<svg xmlns="http://www.w3.org/2000/svg" width="50" height="3" style="user-select: none;"><line x1="0" y1="0.5" x2="50" y2="0.5" stroke-width="1" stroke="black" style="user-select: none;"></line><line x1="0" y1="2.5" x2="50" y2="2.5" stroke-width="1" stroke="black" style="user-select: none;"></line></svg>'],
];
export default class DropdownLineType extends Dropdown {
constructor(type) {
const icon = new Icon('line-type');
let beforei = 0;
const lineTypeEls = lineTypes.map((it, iti) =>
h('div', `${cssPrefix}-item state ${type === it[0] ? 'checked' : ''}`)
.on('click', () => {
lineTypeEls[beforei].toggle('checked');
lineTypeEls[iti].toggle('checked');
beforei = iti;
this.hide();
this.change(it);
})
.child(h('div', `${cssPrefix}-line-type`).html(it[1])),
);
super(icon, 'auto', false, 'bottom-left', ...lineTypeEls);
}
}

View File

@@ -0,0 +1,284 @@
import { cssPrefix } from '../config.js';
import { h } from './element.js';
import Suggest from './suggest.js';
// TODO datepicker 暂不支持后续放开datepicker解决报错后使用
// import Datepicker from './datepicker';
// import { mouseMoveUp } from '../event.js';
function resetTextareaSize() {
const { inputText } = this;
if (!/^\s*$/.test(inputText)) {
const { textlineEl, textEl, areaOffset } = this;
const txts = inputText.split('\n');
const maxTxtSize = Math.max(...txts.map(it => it.length));
const tlOffset = textlineEl.offset();
const fontWidth = tlOffset.width / inputText.length;
const tlineWidth = (maxTxtSize + 1) * fontWidth + 5;
const maxWidth = this.viewFn().width - areaOffset.left - fontWidth;
let h1 = txts.length;
if (tlineWidth > areaOffset.width) {
let twidth = tlineWidth;
if (tlineWidth > maxWidth) {
twidth = maxWidth;
h1 += parseInt(tlineWidth / maxWidth, 10);
h1 += tlineWidth % maxWidth > 0 ? 1 : 0;
}
textEl.css('width', `${twidth}px`);
}
h1 *= this.rowHeight;
if (h1 > areaOffset.height) {
textEl.css('height', `${h1}px`);
}
}
}
function insertText({ target }, itxt) {
const { value, selectionEnd } = target;
const ntxt = `${value.slice(0, selectionEnd)}${itxt}${value.slice(selectionEnd)}`;
target.value = ntxt;
target.setSelectionRange(selectionEnd + 1, selectionEnd + 1);
this.inputText = ntxt;
this.textlineEl.html(ntxt);
resetTextareaSize.call(this);
}
function keydownEventHandler(evt) {
const { keyCode, altKey } = evt;
if (keyCode !== 13 && keyCode !== 9) evt.stopPropagation();
if (keyCode === 13 && altKey) {
insertText.call(this, evt, '\n');
evt.stopPropagation();
}
if (keyCode === 13 && !altKey) evt.preventDefault();
}
function inputEventHandler(evt) {
const v = evt.target.value;
// console.log(evt, 'v:', v);
const { suggest, textlineEl, validator } = this;
const { cell } = this;
if (cell !== null) {
if (('editable' in cell && cell.editable === true) || cell.editable === undefined) {
this.inputText = v;
if (validator) {
if (validator.type === 'list') {
suggest.search(v);
} else {
suggest.hide();
}
} else {
const start = v.lastIndexOf('=');
if (start !== -1) {
suggest.search(v.substring(start + 1));
} else {
suggest.hide();
}
}
textlineEl.html(v);
resetTextareaSize.call(this);
this.change('input', v);
} else {
evt.target.value = cell.text || '';
}
} else {
this.inputText = v;
if (validator) {
if (validator.type === 'list') {
suggest.search(v);
} else {
suggest.hide();
}
} else {
const start = v.lastIndexOf('=');
if (start !== -1) {
suggest.search(v.substring(start + 1));
} else {
suggest.hide();
}
}
textlineEl.html(v);
resetTextareaSize.call(this);
this.change('input', v);
}
}
function setTextareaRange(position) {
const { el } = this.textEl;
setTimeout(() => {
el.focus();
el.setSelectionRange(position, position);
}, 0);
}
function setText(text, position) {
const { textEl, textlineEl } = this;
// firefox bug
textEl.el.blur();
textEl.val(text);
textlineEl.html(text);
setTextareaRange.call(this, position);
}
function suggestItemClick(it) {
const { inputText, validator } = this;
let position = 0;
if (validator && validator.type === 'list') {
this.inputText = it;
position = this.inputText.length;
} else {
const start = inputText.lastIndexOf('=');
const sit = inputText.substring(0, start + 1);
let eit = inputText.substring(start + 1);
if (eit.indexOf(')') !== -1) {
eit = eit.substring(eit.indexOf(')'));
} else {
eit = '';
}
this.inputText = `${sit + it.key}(`;
// console.log('inputText:', this.inputText);
position = this.inputText.length;
this.inputText += `)${eit}`;
}
setText.call(this, this.inputText, position);
}
function resetSuggestItems() {
this.suggest.setItems(this.formulas);
}
function dateFormat(d) {
let month = d.getMonth() + 1;
let date = d.getDate();
if (month < 10) month = `0${month}`;
if (date < 10) date = `0${date}`;
return `${d.getFullYear()}-${month}-${date}`;
}
export default class Editor {
constructor(formulas, viewFn, rowHeight) {
this.viewFn = viewFn;
this.rowHeight = rowHeight;
this.formulas = formulas;
this.suggest = new Suggest(formulas, it => {
suggestItemClick.call(this, it);
});
// this.datepicker = new Datepicker();
// this.datepicker.change((d) => {
// // console.log('d:', d);
// this.setText(dateFormat(d));
// this.clear();
// });
this.areaEl = h('div', `${cssPrefix}-editor-area`)
.children(
(this.textEl = h('textarea', '')
.on('input', evt => inputEventHandler.call(this, evt))
.on('paste.stop', () => {
console.log('empty function');
})
.on('keydown', evt => keydownEventHandler.call(this, evt))),
(this.textlineEl = h('div', 'textline')),
this.suggest.el,
// this.datepicker.el,
)
.on('mousemove.stop', () => {
console.log('empty function');
})
.on('mousedown.stop', () => {
console.log('empty function');
});
this.el = h('div', `${cssPrefix}-editor`).child(this.areaEl).hide();
this.suggest.bindInputEvents(this.textEl);
this.areaOffset = null;
this.freeze = { w: 0, h: 0 };
this.cell = null;
this.inputText = '';
this.change = () => {
console.log('empty function');
};
}
setFreezeLengths(width, height) {
this.freeze.w = width;
this.freeze.h = height;
}
clear() {
// const { cell } = this;
// const cellText = (cell && cell.text) || '';
if (this.inputText !== '') {
this.change('finished', this.inputText);
}
this.cell = null;
this.areaOffset = null;
this.inputText = '';
this.el.hide();
this.textEl.val('');
this.textlineEl.html('');
resetSuggestItems.call(this);
// this.datepicker.hide();
}
setOffset(offset, suggestPosition = 'top') {
const { textEl, areaEl, suggest, freeze, el } = this;
if (offset) {
this.areaOffset = offset;
const { left, top, width, height, l, t } = offset;
// console.log('left:', left, ',top:', top, ', freeze:', freeze);
const elOffset = { left: 0, top: 0 };
// top left
if (freeze.w > l && freeze.h > t) {
//
} else if (freeze.w < l && freeze.h < t) {
elOffset.left = freeze.w;
elOffset.top = freeze.h;
} else if (freeze.w > l) {
elOffset.top = freeze.h;
} else if (freeze.h > t) {
elOffset.left = freeze.w;
}
el.offset(elOffset);
areaEl.offset({ left: left - elOffset.left - 0.8, top: top - elOffset.top - 0.8 });
textEl.offset({ width: width - 9 + 0.8, height: height - 3 + 0.8 });
const sOffset = { left: 0 };
sOffset[suggestPosition] = height;
suggest.setOffset(sOffset);
suggest.hide();
}
}
setCell(cell, validator) {
if (cell && cell.editable === false) return;
// console.log('::', validator);
const { el, datepicker, suggest } = this;
el.show();
this.cell = cell;
const text = (cell && cell.text) || '';
this.setText(text);
this.validator = validator;
if (validator) {
const { type } = validator;
if (type === 'date') {
// datepicker.show();
if (!/^\s*$/.test(text)) {
datepicker.setValue(text);
}
}
if (type === 'list') {
suggest.setItems(validator.values());
suggest.search('');
}
}
}
setText(text) {
this.inputText = text;
// console.log('text>>:', text);
setText.call(this, text, text.length);
resetTextareaSize.call(this);
}
}

View File

@@ -0,0 +1,268 @@
class Element {
constructor(tag, className = '') {
if (typeof tag === 'string') {
this.el = document.createElement(tag);
this.el.className = className;
} else {
this.el = tag;
}
this.data = {};
}
data(key, value) {
if (value !== undefined) {
this.data[key] = value;
return this;
}
return this.data[key];
}
on(eventNames, handler) {
const [fen, ...oen] = eventNames.split('.');
let eventName = fen;
if (eventName === 'mousewheel' && /Firefox/i.test(window.navigator.userAgent)) {
eventName = 'DOMMouseScroll';
}
this.el.addEventListener(eventName, evt => {
handler(evt);
for (let i = 0; i < oen.length; i += 1) {
const k = oen[i];
if (k === 'left' && evt.button !== 0) {
return;
}
if (k === 'right' && evt.button !== 2) {
return;
}
if (k === 'stop') {
evt.stopPropagation();
}
}
});
return this;
}
offset(value) {
if (value !== undefined) {
Object.keys(value).forEach(k => {
this.css(k, `${value[k]}px`);
});
return this;
}
const { offsetTop, offsetLeft, offsetHeight, offsetWidth } = this.el;
return {
top: offsetTop,
left: offsetLeft,
height: offsetHeight,
width: offsetWidth,
};
}
scroll(v) {
const { el } = this;
if (v !== undefined) {
if (v.left !== undefined) {
el.scrollLeft = v.left;
}
if (v.top !== undefined) {
el.scrollTop = v.top;
}
}
return { left: el.scrollLeft, top: el.scrollTop };
}
box() {
return this.el.getBoundingClientRect();
}
parent() {
return new Element(this.el.parentNode);
}
children(...eles) {
if (arguments.length === 0) {
return this.el.childNodes;
}
eles.forEach(ele => this.child(ele));
return this;
}
removeChild(el) {
this.el.removeChild(el);
}
/*
first() {
return this.el.firstChild;
}
last() {
return this.el.lastChild;
}
remove(ele) {
return this.el.removeChild(ele);
}
prepend(ele) {
const { el } = this;
if (el.children.length > 0) {
el.insertBefore(ele, el.firstChild);
} else {
el.appendChild(ele);
}
return this;
}
prev() {
return this.el.previousSibling;
}
next() {
return this.el.nextSibling;
}
*/
child(arg) {
let ele = arg;
if (typeof arg === 'string') {
ele = document.createTextNode(arg);
} else if (arg instanceof Element) {
ele = arg.el;
}
this.el.appendChild(ele);
return this;
}
contains(ele) {
return this.el.contains(ele);
}
className(v) {
if (v !== undefined) {
this.el.className = v;
return this;
}
return this.el.className;
}
addClass(name) {
this.el.classList.add(name);
return this;
}
hasClass(name) {
return this.el.classList.contains(name);
}
removeClass(name) {
this.el.classList.remove(name);
return this;
}
toggle(cls = 'active') {
return this.toggleClass(cls);
}
toggleClass(name) {
return this.el.classList.toggle(name);
}
active(flag = true, cls = 'active') {
if (flag) this.addClass(cls);
else this.removeClass(cls);
return this;
}
checked(flag = true) {
this.active(flag, 'checked');
return this;
}
disabled(flag = true) {
if (flag) this.addClass('disabled');
else this.removeClass('disabled');
return this;
}
// key, value
// key
// {k, v}...
attr(key, value) {
if (value !== undefined) {
this.el.setAttribute(key, value);
} else {
if (typeof key === 'string') {
return this.el.getAttribute(key);
}
Object.keys(key).forEach(k => {
this.el.setAttribute(k, key[k]);
});
}
return this;
}
removeAttr(key) {
this.el.removeAttribute(key);
return this;
}
html(content) {
if (content !== undefined) {
this.el.innerHTML = content;
return this;
}
return this.el.innerHTML;
}
val(v) {
if (v !== undefined) {
this.el.value = v;
return this;
}
return this.el.value;
}
focus() {
this.el.focus();
}
cssRemoveKeys(...keys) {
keys.forEach(k => this.el.style.removeProperty(k));
return this;
}
// css( propertyName )
// css( propertyName, value )
// css( properties )
css(name, value) {
if (value === undefined && typeof name !== 'string') {
Object.keys(name).forEach(k => {
this.el.style[k] = name[k];
});
return this;
}
if (value !== undefined) {
this.el.style[name] = value;
return this;
}
return this.el.style[name];
}
computedStyle() {
return window.getComputedStyle(this.el, null);
}
show() {
this.css('display', 'block');
return this;
}
hide() {
this.css('display', 'none');
return this;
}
}
const h = (tag, className = '') => new Element(tag, className);
export { Element, h };

View File

@@ -0,0 +1,148 @@
export function bind(target, name, fn) {
target.addEventListener(name, fn);
}
export function unbind(target, name, fn) {
target.removeEventListener(name, fn);
}
export function unbindClickoutside(el) {
if (el.xclickoutside) {
unbind(window.document.body, 'click', el.xclickoutside);
delete el.xclickoutside;
}
}
// the left mouse button: mousedown → mouseup → click
// the right mouse button: mousedown → contenxtmenu → mouseup
// the right mouse button in firefox(>65.0): mousedown → contenxtmenu → mouseup → click on window
export function bindClickoutside(el, cb) {
el.xclickoutside = evt => {
// ignore double click
// console.log('evt:', evt);
if (evt.detail === 2 || el.contains(evt.target)) return;
if (cb) cb(el);
else {
el.hide();
unbindClickoutside(el);
}
};
bind(window.document.body, 'click', el.xclickoutside);
}
export function mouseMoveUp(target, movefunc, upfunc) {
bind(target, 'mousemove', movefunc);
const t = target;
t.xEvtUp = evt => {
// console.log('mouseup>>>');
unbind(target, 'mousemove', movefunc);
unbind(target, 'mouseup', target.xEvtUp);
upfunc(evt);
};
bind(target, 'mouseup', target.xEvtUp);
}
function calTouchDirection(spanx, spany, evt, cb) {
let direction = '';
// console.log('spanx:', spanx, ', spany:', spany);
if (Math.abs(spanx) > Math.abs(spany)) {
// horizontal
direction = spanx > 0 ? 'right' : 'left';
cb(direction, spanx, evt);
} else {
// vertical
direction = spany > 0 ? 'down' : 'up';
cb(direction, spany, evt);
}
}
// cb = (direction, distance) => {}
export function bindTouch(target, { move, end }) {
let startx = 0;
let starty = 0;
bind(target, 'touchstart', evt => {
const { pageX, pageY } = evt.touches[0];
startx = pageX;
starty = pageY;
});
bind(target, 'touchmove', evt => {
if (!move) return;
const { pageX, pageY } = evt.changedTouches[0];
const spanx = pageX - startx;
const spany = pageY - starty;
if (Math.abs(spanx) > 10 || Math.abs(spany) > 10) {
// console.log('spanx:', spanx, ', spany:', spany);
calTouchDirection(spanx, spany, evt, move);
startx = pageX;
starty = pageY;
}
evt.preventDefault();
});
bind(target, 'touchend', evt => {
if (!end) return;
const { pageX, pageY } = evt.changedTouches[0];
const spanx = pageX - startx;
const spany = pageY - starty;
calTouchDirection(spanx, spany, evt, end);
});
}
// eventemiter
export function createEventEmitter() {
const listeners = new Map();
function on(eventName, callback) {
const push = () => {
const currentListener = listeners.get(eventName);
return (Array.isArray(currentListener) && currentListener.push(callback)) || false;
};
const create = () => listeners.set(eventName, [].concat(callback));
return (listeners.has(eventName) && push()) || create();
}
function fire(eventName, args) {
const exec = () => {
const currentListener = listeners.get(eventName);
for (const callback of currentListener) callback.call(null, ...args);
};
return listeners.has(eventName) && exec();
}
function removeListener(eventName, callback) {
const remove = () => {
const currentListener = listeners.get(eventName);
const idx = currentListener.indexOf(callback);
return (
idx >= 0 &&
currentListener.splice(idx, 1) &&
listeners.get(eventName).length === 0 &&
listeners.delete(eventName)
);
};
return listeners.has(eventName) && remove();
}
function once(eventName, callback) {
const execCalllback = (...args) => {
callback.call(null, ...args);
removeListener(eventName, execCalllback);
};
return on(eventName, execCalllback);
}
function removeAllListeners() {
listeners.clear();
}
return {
get current() {
return listeners;
},
on,
once,
fire,
removeListener,
removeAllListeners,
};
}

View File

@@ -0,0 +1,66 @@
import { cssPrefix } from '../config.js';
import { t } from '../locale/locale.js';
import { h } from './element.js';
const patterns = {
number: /(^\d+$)|(^\d+(\.\d{0,4})?$)/,
date: /^\d{4}-\d{1,2}-\d{1,2}$/,
};
// rule: { required: false, type, pattern: // }
export default class FormField {
constructor(input, rule, label, labelWidth) {
this.label = '';
this.rule = rule;
if (label) {
this.label = h('label', 'label').css('width', `${labelWidth}px`).html(label);
}
this.tip = h('div', 'tip').child('tip').hide();
this.input = input;
this.input.vchange = () => this.validate();
this.el = h('div', `${cssPrefix}-form-field`).children(this.label, input.el, this.tip);
}
isShow() {
return this.el.css('display') !== 'none';
}
show() {
this.el.show();
}
hide() {
this.el.hide();
return this;
}
val(v) {
return this.input.val(v);
}
hint(hint) {
this.input.hint(hint);
}
validate() {
const { input, rule, tip, el } = this;
const v = input.val();
if (rule.required) {
if (/^\s*$/.test(v)) {
tip.html(t('validation.required'));
el.addClass('error');
return false;
}
}
if (rule.type || rule.pattern) {
const pattern = rule.pattern || patterns[rule.type];
if (!pattern.test(v)) {
tip.html(t('validation.notMatch'));
el.addClass('error');
return false;
}
}
el.removeClass('error');
return true;
}
}

View File

@@ -0,0 +1,30 @@
import { cssPrefix } from '../config.js';
import { h } from './element.js';
export default class FormInput {
constructor(width, hint) {
this.vchange = () => {
console.log('empty function');
};
this.el = h('div', `${cssPrefix}-form-input`);
this.input = h('input', '')
.css('width', width)
.on('input', evt => this.vchange(evt))
.attr('placeholder', hint);
this.el.child(this.input);
}
focus() {
setTimeout(() => {
this.input.el.focus();
}, 10);
}
hint(v) {
this.input.attr('placeholder', v);
}
val(v) {
return this.input.val(v);
}
}

View File

@@ -0,0 +1,53 @@
import { cssPrefix } from '../config.js';
import { h } from './element.js';
import Suggest from './suggest.js';
export default class FormSelect {
constructor(
key,
items,
width,
getTitle = it => it,
change = () => {
console.log('empty function');
},
) {
this.key = key;
this.getTitle = getTitle;
this.vchange = () => {
console.log('empty function');
};
this.el = h('div', `${cssPrefix}-form-select`);
this.suggest = new Suggest(
items.map(it => ({ key: it, title: this.getTitle(it) })),
it => {
this.itemClick(it.key);
change(it.key);
this.vchange(it.key);
},
width,
this.el,
);
this.el
.children((this.itemEl = h('div', 'input-text').html(this.getTitle(key))), this.suggest.el)
.on('click', () => this.show());
}
show() {
this.suggest.search('');
}
itemClick(it) {
this.key = it;
this.itemEl.html(this.getTitle(it));
}
val(v) {
if (v !== undefined) {
this.key = v;
this.itemEl.html(this.getTitle(v));
return this;
}
return this.key;
}
}

View File

@@ -0,0 +1,14 @@
import { cssPrefix } from '../config.js';
import { Element, h } from './element.js';
export default class Icon extends Element {
constructor(name) {
super('div', `${cssPrefix}-icon`);
this.iconNameEl = h('div', `${cssPrefix}-icon-img ${name}`);
this.child(this.iconNameEl);
}
setName(name) {
this.iconNameEl.className(`${cssPrefix}-icon-img ${name}`);
}
}

View File

@@ -0,0 +1,31 @@
import { cssPrefix } from '../config.js';
import { h } from './element.js';
import Icon from './icon.js';
export function xtoast(title, content) {
const el = h('div', `${cssPrefix}-toast`);
const dimmer = h('div', `${cssPrefix}-dimmer active`);
const remove = () => {
document.body.removeChild(el.el);
document.body.removeChild(dimmer.el);
};
el.children(
h('div', `${cssPrefix}-toast-header`).children(
new Icon('close').on('click.stop', () => remove()),
title,
),
h('div', `${cssPrefix}-toast-content`).html(content),
);
document.body.appendChild(el.el);
document.body.appendChild(dimmer.el);
// set offset
const { width, height } = el.box();
const { clientHeight, clientWidth } = document.documentElement;
el.offset({
left: (clientWidth - width) / 2,
top: (clientHeight - height) / 3,
});
}
export default {};

View File

@@ -0,0 +1,45 @@
import { cssPrefix } from '../config.js';
import { h } from './element.js';
import Icon from './icon.js';
import { bind, unbind } from './event.js';
export default class Modal {
constructor(title, content, width = '600px') {
this.title = title;
this.el = h('div', `${cssPrefix}-modal`)
.css('width', width)
.children(
h('div', `${cssPrefix}-modal-header`).children(
new Icon('close').on('click.stop', () => this.hide()),
this.title,
),
h('div', `${cssPrefix}-modal-content`).children(...content),
)
.hide();
}
show() {
// dimmer
this.dimmer = h('div', `${cssPrefix}-dimmer active`);
document.body.appendChild(this.dimmer.el);
const { width, height } = this.el.show().box();
const { clientHeight, clientWidth } = document.documentElement;
this.el.offset({
left: (clientWidth - width) / 2,
top: (clientHeight - height) / 3,
});
window.xkeydownEsc = evt => {
if (evt.keyCode === 27) {
this.hide();
}
};
bind(window, 'keydown', window.xkeydownEsc);
}
hide() {
this.el.hide();
document.body.removeChild(this.dimmer.el);
unbind(window, 'keydown', window.xkeydownEsc);
delete window.xkeydownEsc;
}
}

View File

@@ -0,0 +1,214 @@
import { t } from '../locale/locale.js';
import { cssPrefix } from '../config.js';
import Modal from './modal.js';
import FormInput from './form_input.js';
import FormSelect from './form_select.js';
import FormField from './form_field.js';
import Button from './button.js';
import { h } from './element.js';
const fieldLabelWidth = 100;
export default class ModalValidation extends Modal {
constructor() {
const mf = new FormField(
new FormSelect(
'cell',
['cell'], // cell|row|column
'100%',
it => t(`dataValidation.modeType.${it}`),
),
{ required: true },
`${t('dataValidation.range')}:`,
fieldLabelWidth,
);
const rf = new FormField(new FormInput('120px', 'E3 or E3:F12'), {
required: true,
pattern: /^([A-Z]{1,2}[1-9]\d*)(:[A-Z]{1,2}[1-9]\d*)?$/,
});
const cf = new FormField(
new FormSelect(
'list',
['list', 'number', 'date', 'phone', 'email'],
'100%',
it => t(`dataValidation.type.${it}`),
it => this.criteriaSelected(it),
),
{ required: true },
`${t('dataValidation.criteria')}:`,
fieldLabelWidth,
);
// operator
const of = new FormField(
new FormSelect(
'be',
['be', 'nbe', 'eq', 'neq', 'lt', 'lte', 'gt', 'gte'],
'160px',
it => t(`dataValidation.operator.${it}`),
it => this.criteriaOperatorSelected(it),
),
{ required: true },
).hide();
// min, max
const minvf = new FormField(new FormInput('70px', '10'), { required: true }).hide();
const maxvf = new FormField(new FormInput('70px', '100'), {
required: true,
type: 'number',
}).hide();
// value
const svf = new FormField(new FormInput('120px', 'a,b,c'), { required: true });
const vf = new FormField(new FormInput('70px', '10'), {
required: true,
type: 'number',
}).hide();
super(t('contextmenu.validation'), [
h('div', `${cssPrefix}-form-fields`).children(mf.el, rf.el),
h('div', `${cssPrefix}-form-fields`).children(
cf.el,
of.el,
minvf.el,
maxvf.el,
vf.el,
svf.el,
),
h('div', `${cssPrefix}-buttons`).children(
new Button('cancel').on('click', () => this.btnClick('cancel')),
new Button('remove').on('click', () => this.btnClick('remove')),
new Button('save', 'primary').on('click', () => this.btnClick('save')),
),
]);
this.mf = mf;
this.rf = rf;
this.cf = cf;
this.of = of;
this.minvf = minvf;
this.maxvf = maxvf;
this.vf = vf;
this.svf = svf;
this.change = () => {
console.log('empty function');
};
}
showVf(it) {
const hint = it === 'date' ? '2018-11-12' : '10';
const { vf } = this;
vf.input.hint(hint);
vf.show();
}
criteriaSelected(it) {
const { of, minvf, maxvf, vf, svf } = this;
if (it === 'date' || it === 'number') {
of.show();
minvf.rule.type = it;
maxvf.rule.type = it;
if (it === 'date') {
minvf.hint('2018-11-12');
maxvf.hint('2019-11-12');
} else {
minvf.hint('10');
maxvf.hint('100');
}
minvf.show();
maxvf.show();
vf.hide();
svf.hide();
} else {
if (it === 'list') {
svf.show();
} else {
svf.hide();
}
vf.hide();
of.hide();
minvf.hide();
maxvf.hide();
}
}
criteriaOperatorSelected(it) {
if (!it) return;
const { minvf, maxvf, vf } = this;
if (it === 'be' || it === 'nbe') {
minvf.show();
maxvf.show();
vf.hide();
} else {
const type = this.cf.val();
vf.rule.type = type;
if (type === 'date') {
vf.hint('2018-11-12');
} else {
vf.hint('10');
}
vf.show();
minvf.hide();
maxvf.hide();
}
}
btnClick(action) {
if (action === 'cancel') {
this.hide();
} else if (action === 'remove') {
this.change('remove');
this.hide();
} else if (action === 'save') {
// validate
const attrs = ['mf', 'rf', 'cf', 'of', 'svf', 'vf', 'minvf', 'maxvf'];
for (let i = 0; i < attrs.length; i += 1) {
const field = this[attrs[i]];
// console.log('field:', field);
if (field.isShow()) {
// console.log('it:', it);
if (!field.validate()) return;
}
}
const mode = this.mf.val();
const ref = this.rf.val();
const type = this.cf.val();
const operator = this.of.val();
let value = this.svf.val();
if (type === 'number' || type === 'date') {
if (operator === 'be' || operator === 'nbe') {
value = [this.minvf.val(), this.maxvf.val()];
} else {
value = this.vf.val();
}
}
// console.log(mode, ref, type, operator, value);
this.change('save', mode, ref, type, operator, {
required: false,
value,
});
this.hide();
}
}
// validation: { mode, ref, validator }
setValue(v) {
if (v) {
const { mf, rf, cf, of, svf, vf, minvf, maxvf } = this;
const { mode, ref, validator } = v;
const { type, operator, value } = validator || { type: 'list' };
mf.val(mode || 'cell');
rf.val(ref);
cf.val(type);
of.val(operator);
if (Array.isArray(value)) {
minvf.val(value[0]);
maxvf.val(value[1]);
} else {
svf.val(value || '');
vf.val(value || '');
}
this.criteriaSelected(type);
this.criteriaOperatorSelected(operator);
}
this.show();
}
}

View File

@@ -0,0 +1,213 @@
import { cssPrefix } from '../config.js';
import { Draw } from '../canvas/draw.js';
import { t } from '../locale/locale.js';
import { h } from './element.js';
import Button from './button.js';
import { renderCell } from './table.js';
// resolution: 72 => 595 x 842
// 150 => 1240 x 1754
// 200 => 1654 x 2339
// 300 => 2479 x 3508
// 96 * cm / 2.54 , 96 * cm / 2.54
const PAGER_SIZES = [
['A3', 11.69, 16.54],
['A4', 8.27, 11.69],
['A5', 5.83, 8.27],
['B4', 9.84, 13.9],
['B5', 6.93, 9.84],
];
const PAGER_ORIENTATIONS = ['landscape', 'portrait'];
function inches2px(inc) {
return parseInt(96 * inc, 10);
}
function btnClick(type) {
if (type === 'cancel') {
this.el.hide();
} else {
this.toPrint();
}
}
function pagerSizeChange(evt) {
const { paper } = this;
const { value } = evt.target;
const ps = PAGER_SIZES[value];
paper.w = inches2px(ps[1]);
paper.h = inches2px(ps[2]);
// console.log('paper:', ps, paper);
this.preview();
}
function pagerOrientationChange(evt) {
const { paper } = this;
const { value } = evt.target;
const v = PAGER_ORIENTATIONS[value];
paper.orientation = v;
this.preview();
}
export default class Print {
constructor(data) {
this.paper = {
w: inches2px(PAGER_SIZES[0][1]),
h: inches2px(PAGER_SIZES[0][2]),
padding: 50,
orientation: PAGER_ORIENTATIONS[0],
get width() {
return this.orientation === 'landscape' ? this.h : this.w;
},
get height() {
return this.orientation === 'landscape' ? this.w : this.h;
},
};
this.data = data;
this.el = h('div', `${cssPrefix}-print`)
.children(
h('div', `${cssPrefix}-print-bar`).children(
h('div', '-title').child('Print settings'),
h('div', '-right').children(
h('div', `${cssPrefix}-buttons`).children(
new Button('cancel').on('click', btnClick.bind(this, 'cancel')),
new Button('next', 'primary').on('click', btnClick.bind(this, 'next')),
),
),
),
h('div', `${cssPrefix}-print-content`).children(
(this.contentEl = h('div', '-content')),
h('div', '-sider').child(
h('form', '').children(
h('fieldset', '').children(
h('label', '').child(`${t('print.size')}`),
h('select', '')
.children(
...PAGER_SIZES.map((it, index) =>
h('option', '')
.attr('value', index)
.child(`${it[0]} ( ${it[1]}''x${it[2]}'' )`),
),
)
.on('change', pagerSizeChange.bind(this)),
),
h('fieldset', '').children(
h('label', '').child(`${t('print.orientation')}`),
h('select', '')
.children(
...PAGER_ORIENTATIONS.map((it, index) =>
h('option', '')
.attr('value', index)
.child(`${t('print.orientations')[index]}`),
),
)
.on('change', pagerOrientationChange.bind(this)),
),
),
),
),
)
.hide();
}
resetData(data) {
this.data = data;
}
preview() {
const { data, paper } = this;
const { width, height, padding } = paper;
const iwidth = width - padding * 2;
const iheight = height - padding * 2;
const cr = data.contentRange();
const pages = parseInt(cr.h / iheight, 10) + 1;
const scale = iwidth / cr.w;
let left = padding;
const top = padding;
if (scale > 1) {
left += (iwidth - cr.w) / 2;
}
let ri = 0;
let yoffset = 0;
this.contentEl.html('');
this.canvases = [];
const mViewRange = {
sri: 0,
sci: 0,
eri: 0,
eci: 0,
};
for (let i = 0; i < pages; i += 1) {
let th = 0;
let yo = 0;
const wrap = h('div', `${cssPrefix}-canvas-card`);
const canvas = h('canvas', `${cssPrefix}-canvas`);
this.canvases.push(canvas.el);
const draw = new Draw(canvas.el, width, height);
// cell-content
draw.save();
draw.translate(left, top);
if (scale < 1) draw.scale(scale, scale);
// console.log('ri:', ri, cr.eri, yoffset);
for (; ri <= cr.eri; ri += 1) {
const rh = data.rows.getHeight(ri);
th += rh;
if (th < iheight) {
for (let ci = 0; ci <= cr.eci; ci += 1) {
renderCell(draw, data, ri, ci, yoffset);
mViewRange.eci = ci;
}
} else {
yo = -(th - rh);
break;
}
}
mViewRange.eri = ri;
draw.restore();
// merge-cell
draw.save();
draw.translate(left, top);
if (scale < 1) draw.scale(scale, scale);
const yof = yoffset;
data.eachMergesInView(mViewRange, ({ sri, sci }) => {
renderCell(draw, data, sri, sci, yof);
});
draw.restore();
mViewRange.sri = mViewRange.eri;
mViewRange.sci = mViewRange.eci;
yoffset += yo;
this.contentEl.child(h('div', `${cssPrefix}-canvas-card-wraper`).child(wrap.child(canvas)));
}
this.el.show();
}
toPrint() {
this.el.hide();
const { paper } = this;
const iframe = h('iframe', '').hide();
const { el } = iframe;
window.document.body.appendChild(el);
const { contentWindow } = el;
const idoc = contentWindow.document;
const style = document.createElement('style');
style.innerHTML = `
@page { size: ${paper.width}px ${paper.height}px; };
canvas {
page-break-before: auto;
page-break-after: always;
image-rendering: pixelated;
};
`;
idoc.head.appendChild(style);
this.canvases.forEach(it => {
const cn = it.cloneNode(false);
const ctx = cn.getContext('2d');
// ctx.imageSmoothingEnabled = true;
ctx.drawImage(it, 0, 0);
idoc.body.appendChild(cn);
});
contentWindow.print();
}
}

View File

@@ -0,0 +1,118 @@
import { cssPrefix } from '../config.js';
import { h } from './element.js';
import { mouseMoveUp } from './event.js';
export default class Resizer {
constructor(vertical = false, minDistance) {
this.moving = false;
this.vertical = vertical;
this.el = h('div', `${cssPrefix}-resizer ${vertical ? 'vertical' : 'horizontal'}`)
.children(
(this.unhideHoverEl = h('div', `${cssPrefix}-resizer-hover`)
.on('dblclick.stop', evt => this.mousedblclickHandler(evt))
.css('position', 'absolute')
.hide()),
(this.hoverEl = h('div', `${cssPrefix}-resizer-hover`).on('mousedown.stop', evt =>
this.mousedownHandler(evt),
)),
(this.lineEl = h('div', `${cssPrefix}-resizer-line`).hide()),
)
.hide();
// cell rect
this.cRect = null;
this.finishedFn = null;
this.minDistance = minDistance;
this.unhideFn = () => {
console.log('empty function');
};
}
showUnhide(index) {
this.unhideIndex = index;
this.unhideHoverEl.show();
}
hideUnhide() {
this.unhideHoverEl.hide();
}
// rect : {top, left, width, height}
// line : {width, height}
show(rect, line) {
const { moving, vertical, hoverEl, lineEl, el, unhideHoverEl } = this;
if (moving) return;
this.cRect = rect;
const { left, top, width, height } = rect;
el.offset({
left: vertical ? left + width - 5 : left,
top: vertical ? top : top + height - 5,
}).show();
hoverEl.offset({
width: vertical ? 5 : width,
height: vertical ? height : 5,
});
lineEl.offset({
width: vertical ? 0 : line.width,
height: vertical ? line.height : 0,
});
unhideHoverEl.offset({
left: vertical ? 5 - width : left,
top: vertical ? top : 5 - height,
width: vertical ? 5 : width,
height: vertical ? height : 5,
});
}
hide() {
this.el
.offset({
left: 0,
top: 0,
})
.hide();
this.hideUnhide();
}
mousedblclickHandler() {
if (this.unhideIndex) this.unhideFn(this.unhideIndex);
}
mousedownHandler(evt) {
let startEvt = evt;
const { el, lineEl, cRect, vertical, minDistance } = this;
let distance = vertical ? cRect.width : cRect.height;
// console.log('distance:', distance);
lineEl.show();
mouseMoveUp(
window,
e => {
this.moving = true;
if (startEvt !== null && e.buttons === 1) {
// console.log('top:', top, ', left:', top, ', cRect:', cRect);
if (vertical) {
distance += e.movementX;
if (distance > minDistance) {
el.css('left', `${cRect.left + distance}px`);
}
} else {
distance += e.movementY;
if (distance > minDistance) {
el.css('top', `${cRect.top + distance}px`);
}
}
startEvt = e;
}
},
() => {
startEvt = null;
lineEl.hide();
this.moving = false;
this.hide();
if (this.finishedFn) {
if (distance < minDistance) distance = minDistance;
this.finishedFn(cRect, distance);
}
},
);
}
}

View File

@@ -0,0 +1,47 @@
import { cssPrefix } from '../config.js';
import { h } from './element.js';
export default class Scrollbar {
constructor(vertical) {
this.vertical = vertical;
this.moveFn = null;
this.el = h('div', `${cssPrefix}-scrollbar ${vertical ? 'vertical' : 'horizontal'}`)
.child((this.contentEl = h('div', '')))
.on('mousemove.stop', () => {
console.log('mousemove.stop');
})
.on('scroll.stop', evt => {
const { scrollTop, scrollLeft } = evt.target;
// console.log('scrollTop:', scrollTop);
if (this.moveFn) {
this.moveFn(this.vertical ? scrollTop : scrollLeft, evt);
}
// console.log('evt:::', evt);
});
}
move(v) {
this.el.scroll(v);
return this;
}
scroll() {
return this.el.scroll();
}
set(distance, contentDistance) {
const d = distance - 1;
// console.log('distance:', distance, ', contentDistance:', contentDistance);
if (contentDistance > d) {
const cssKey = this.vertical ? 'height' : 'width';
// console.log('d:', d);
this.el.css(cssKey, `${d - 15}px`).show();
this.contentEl
.css(this.vertical ? 'width' : 'height', '1px')
.css(cssKey, `${contentDistance}px`);
} else {
this.el.hide();
}
return this;
}
}

View File

@@ -0,0 +1,401 @@
import { cssPrefix } from '../config.js';
import { CellRange } from '../core/cell_range.js';
import { h } from './element.js';
const selectorHeightBorderWidth = 2 * 2 - 1;
let startZIndex = 10;
class SelectorElement {
constructor(useHideInput = false, autoFocus = true) {
this.useHideInput = useHideInput;
this.autoFocus = autoFocus;
this.inputChange = () => {
console.log('empty function');
};
this.cornerEl = h('div', `${cssPrefix}-selector-corner`);
this.areaEl = h('div', `${cssPrefix}-selector-area`).child(this.cornerEl).hide();
this.clipboardEl = h('div', `${cssPrefix}-selector-clipboard`).hide();
this.autofillEl = h('div', `${cssPrefix}-selector-autofill`).hide();
this.el = h('div', `${cssPrefix}-selector`)
.css('z-index', `${startZIndex}`)
.children(this.areaEl, this.clipboardEl, this.autofillEl)
.hide();
if (useHideInput) {
this.hideInput = h('input', '').on('compositionend', evt => {
this.inputChange(evt.target.value);
});
this.el.child((this.hideInputDiv = h('div', 'hide-input').child(this.hideInput)));
this.el.child((this.hideInputDiv = h('div', 'hide-input').child(this.hideInput)));
}
startZIndex += 1;
}
setOffset(v) {
this.el.offset(v).show();
return this;
}
hide() {
this.el.hide();
return this;
}
setAreaOffset(v) {
const { left, top, width, height } = v;
const of = {
width: width - selectorHeightBorderWidth + 0.8,
height: height - selectorHeightBorderWidth + 0.8,
left: left - 0.8,
top: top - 0.8,
};
this.areaEl.offset(of).show();
if (this.useHideInput) {
this.hideInputDiv.offset(of);
if (this.autoFocus) {
this.hideInput.val('').focus();
} else {
this.hideInput.val('');
}
}
}
setClipboardOffset(v) {
const { left, top, width, height } = v;
this.clipboardEl.offset({
left,
top,
width: width - 5,
height: height - 5,
});
}
showAutofill(v) {
const { left, top, width, height } = v;
this.autofillEl
.offset({
width: width - selectorHeightBorderWidth,
height: height - selectorHeightBorderWidth,
left,
top,
})
.show();
}
hideAutofill() {
this.autofillEl.hide();
}
showClipboard() {
this.clipboardEl.show();
}
hideClipboard() {
this.clipboardEl.hide();
}
}
function calBRAreaOffset(offset) {
const { data } = this;
const { left, top, width, height, scroll, l, t } = offset;
const ftwidth = data.freezeTotalWidth();
const ftheight = data.freezeTotalHeight();
let left0 = left - ftwidth;
if (ftwidth > l) left0 -= scroll.x;
let top0 = top - ftheight;
if (ftheight > t) top0 -= scroll.y;
return {
left: left0,
top: top0,
width,
height,
};
}
function calTAreaOffset(offset) {
const { data } = this;
const { left, width, height, l, t, scroll } = offset;
const ftwidth = data.freezeTotalWidth();
let left0 = left - ftwidth;
if (ftwidth > l) left0 -= scroll.x;
return {
left: left0,
top: t,
width,
height,
};
}
function calLAreaOffset(offset) {
const { data } = this;
const { top, width, height, l, t, scroll } = offset;
const ftheight = data.freezeTotalHeight();
let top0 = top - ftheight;
// console.log('ftheight:', ftheight, ', t:', t);
if (ftheight > t) top0 -= scroll.y;
return {
left: l,
top: top0,
width,
height,
};
}
function setBRAreaOffset(offset) {
const { br } = this;
br.setAreaOffset(calBRAreaOffset.call(this, offset));
}
function setTLAreaOffset(offset) {
const { tl } = this;
tl.setAreaOffset(offset);
}
function setTAreaOffset(offset) {
const { t } = this;
t.setAreaOffset(calTAreaOffset.call(this, offset));
}
function setLAreaOffset(offset) {
const { l } = this;
l.setAreaOffset(calLAreaOffset.call(this, offset));
}
function setLClipboardOffset(offset) {
const { l } = this;
l.setClipboardOffset(calLAreaOffset.call(this, offset));
}
function setBRClipboardOffset(offset) {
const { br } = this;
br.setClipboardOffset(calBRAreaOffset.call(this, offset));
}
function setTLClipboardOffset(offset) {
const { tl } = this;
tl.setClipboardOffset(offset);
}
function setTClipboardOffset(offset) {
const { t } = this;
t.setClipboardOffset(calTAreaOffset.call(this, offset));
}
function setAllAreaOffset(offset) {
setBRAreaOffset.call(this, offset);
setTLAreaOffset.call(this, offset);
setTAreaOffset.call(this, offset);
setLAreaOffset.call(this, offset);
}
function setAllClipboardOffset(offset) {
setBRClipboardOffset.call(this, offset);
setTLClipboardOffset.call(this, offset);
setTClipboardOffset.call(this, offset);
setLClipboardOffset.call(this, offset);
}
export default class Selector {
constructor(data) {
const { autoFocus } = data.settings;
this.inputChange = () => {
console.log('empty function');
};
this.data = data;
this.br = new SelectorElement(true, autoFocus);
this.t = new SelectorElement();
this.l = new SelectorElement();
this.tl = new SelectorElement();
this.br.inputChange = v => {
this.inputChange(v);
};
this.br.el.show();
this.offset = null;
this.areaOffset = null;
this.indexes = null;
this.range = null;
this.arange = null;
this.el = h('div', `${cssPrefix}-selectors`)
.children(this.tl.el, this.t.el, this.l.el, this.br.el)
.hide();
// for performance
this.lastri = -1;
this.lastci = -1;
startZIndex += 1;
}
resetData(data) {
this.data = data;
this.range = data.selector.range;
this.resetAreaOffset();
}
hide() {
this.el.hide();
}
resetOffset() {
const { data, tl, t, l, br } = this;
const freezeHeight = data.freezeTotalHeight();
const freezeWidth = data.freezeTotalWidth();
if (freezeHeight > 0 || freezeWidth > 0) {
tl.setOffset({ width: freezeWidth, height: freezeHeight });
t.setOffset({ left: freezeWidth, height: freezeHeight });
l.setOffset({ top: freezeHeight, width: freezeWidth });
br.setOffset({ left: freezeWidth, top: freezeHeight });
} else {
tl.hide();
t.hide();
l.hide();
br.setOffset({ left: 0, top: 0 });
}
}
resetAreaOffset() {
// console.log('offset:', offset);
const offset = this.data.getSelectedRect();
const coffset = this.data.getClipboardRect();
setAllAreaOffset.call(this, offset);
setAllClipboardOffset.call(this, coffset);
this.resetOffset();
}
resetBRTAreaOffset() {
const offset = this.data.getSelectedRect();
const coffset = this.data.getClipboardRect();
setBRAreaOffset.call(this, offset);
setTAreaOffset.call(this, offset);
setBRClipboardOffset.call(this, coffset);
setTClipboardOffset.call(this, coffset);
this.resetOffset();
}
resetBRLAreaOffset() {
const offset = this.data.getSelectedRect();
const coffset = this.data.getClipboardRect();
setBRAreaOffset.call(this, offset);
setLAreaOffset.call(this, offset);
setBRClipboardOffset.call(this, coffset);
setLClipboardOffset.call(this, coffset);
this.resetOffset();
}
set(ri, ci, indexesUpdated = true) {
const { data } = this;
const cellRange = data.calSelectedRangeByStart(ri, ci);
const { sri, sci } = cellRange;
if (indexesUpdated) {
let [cri, cci] = [ri, ci];
if (ri < 0) cri = 0;
if (ci < 0) cci = 0;
data.selector.setIndexes(cri, cci);
this.indexes = [cri, cci];
}
this.moveIndexes = [sri, sci];
// this.sIndexes = sIndexes;
// this.eIndexes = eIndexes;
this.range = cellRange;
this.resetAreaOffset();
this.el.show();
}
setEnd(ri, ci, moving = true) {
const { data, lastri, lastci } = this;
if (moving) {
if (ri === lastri && ci === lastci) return;
this.lastri = ri;
this.lastci = ci;
}
this.range = data.calSelectedRangeByEnd(ri, ci);
setAllAreaOffset.call(this, this.data.getSelectedRect());
}
reset() {
// console.log('::::', this.data);
const { eri, eci } = this.data.selector.range;
this.setEnd(eri, eci);
}
showAutofill(ri, ci) {
if (ri === -1 && ci === -1) return;
// console.log('ri:', ri, ', ci:', ci);
// const [sri, sci] = this.sIndexes;
// const [eri, eci] = this.eIndexes;
const { sri, sci, eri, eci } = this.range;
const [nri, nci] = [ri, ci];
// const rn = eri - sri;
// const cn = eci - sci;
const srn = sri - ri;
const scn = sci - ci;
const ern = eri - ri;
const ecn = eci - ci;
if (scn > 0) {
// left
// console.log('left');
this.arange = new CellRange(sri, nci, eri, sci - 1);
// this.saIndexes = [sri, nci];
// this.eaIndexes = [eri, sci - 1];
// data.calRangeIndexes2(
} else if (srn > 0) {
// top
// console.log('top');
// nri = sri;
this.arange = new CellRange(nri, sci, sri - 1, eci);
// this.saIndexes = [nri, sci];
// this.eaIndexes = [sri - 1, eci];
} else if (ecn < 0) {
// right
// console.log('right');
// nci = eci;
this.arange = new CellRange(sri, eci + 1, eri, nci);
// this.saIndexes = [sri, eci + 1];
// this.eaIndexes = [eri, nci];
} else if (ern < 0) {
// bottom
// console.log('bottom');
// nri = eri;
this.arange = new CellRange(eri + 1, sci, nri, eci);
// this.saIndexes = [eri + 1, sci];
// this.eaIndexes = [nri, eci];
} else {
// console.log('else:');
this.arange = null;
// this.saIndexes = null;
// this.eaIndexes = null;
return;
}
if (this.arange !== null) {
// console.log(this.saIndexes, ':', this.eaIndexes);
const offset = this.data.getRect(this.arange);
offset.width += 2;
offset.height += 2;
const { br, l, t, tl } = this;
br.showAutofill(calBRAreaOffset.call(this, offset));
l.showAutofill(calLAreaOffset.call(this, offset));
t.showAutofill(calTAreaOffset.call(this, offset));
tl.showAutofill(offset);
}
}
hideAutofill() {
['br', 'l', 't', 'tl'].forEach(property => {
this[property].hideAutofill();
});
}
showClipboard() {
const coffset = this.data.getClipboardRect();
setAllClipboardOffset.call(this, coffset);
['br', 'l', 't', 'tl'].forEach(property => {
this[property].showClipboard();
});
}
hideClipboard() {
['br', 'l', 't', 'tl'].forEach(property => {
this[property].hideClipboard();
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
import { cssPrefix } from '../config.js';
import { t } from '../locale/locale.js';
import { h } from './element.js';
import Button from './button.js';
import { bindClickoutside, unbindClickoutside } from './event.js';
function buildMenu(clsName) {
return h('div', `${cssPrefix}-item ${clsName}`);
}
function buildSortItem(it) {
return buildMenu('state')
.child(t(`sort.${it}`))
.on('click.stop', () => this.itemClick(it));
}
function buildFilterBody(items) {
const { filterbEl, filterValues } = this;
filterbEl.html('');
const itemKeys = Object.keys(items);
itemKeys.forEach((it, index) => {
const cnt = items[it];
const active = filterValues.includes(it) ? 'checked' : '';
filterbEl.child(
h('div', `${cssPrefix}-item state ${active}`)
.on('click.stop', () => this.filterClick(index, it))
.children(it === '' ? t('filter.empty') : it, h('div', 'label').html(`(${cnt})`)),
);
});
}
function resetFilterHeader() {
const { filterhEl, filterValues, values } = this;
filterhEl.html(`${filterValues.length} / ${values.length}`);
filterhEl.checked(filterValues.length === values.length);
}
export default class SortFilter {
constructor() {
this.filterbEl = h('div', `${cssPrefix}-body`);
this.filterhEl = h('div', `${cssPrefix}-header state`).on('click.stop', () =>
this.filterClick(0, 'all'),
);
this.el = h('div', `${cssPrefix}-sort-filter`)
.children(
(this.sortAscEl = buildSortItem.call(this, 'asc')),
(this.sortDescEl = buildSortItem.call(this, 'desc')),
buildMenu('divider'),
h('div', `${cssPrefix}-filter`).children(this.filterhEl, this.filterbEl),
h('div', `${cssPrefix}-buttons`).children(
new Button('cancel').on('click', () => this.btnClick('cancel')),
new Button('ok', 'primary').on('click', () => this.btnClick('ok')),
),
)
.hide();
// this.setFilters(['test1', 'test2', 'text3']);
this.ci = null;
this.sortDesc = null;
this.values = null;
this.filterValues = [];
}
btnClick(it) {
if (it === 'ok') {
const { ci, sort, filterValues } = this;
if (this.ok) {
this.ok(ci, sort, 'in', filterValues);
}
}
this.hide();
}
itemClick(it) {
// console.log('it:', it);
this.sort = it;
const { sortAscEl, sortDescEl } = this;
sortAscEl.checked(it === 'asc');
sortDescEl.checked(it === 'desc');
}
filterClick(index, it) {
// console.log('index:', index, it);
const { filterbEl, filterValues, values } = this;
const children = filterbEl.children();
if (it === 'all') {
if (children.length === filterValues.length) {
this.filterValues = [];
children.forEach(i => h(i).checked(false));
} else {
this.filterValues = Array.from(values);
children.forEach(i => h(i).checked(true));
}
} else {
const checked = h(children[index]).toggle('checked');
if (checked) {
filterValues.push(it);
} else {
filterValues.splice(
filterValues.findIndex(i => i === it),
1,
);
}
}
resetFilterHeader.call(this);
}
// v: autoFilter
// items: {value: cnt}
// sort { ci, order }
set(ci, items, filter, sort) {
this.ci = ci;
const { sortAscEl, sortDescEl } = this;
if (sort !== null) {
this.sort = sort.order;
sortAscEl.checked(sort.asc());
sortDescEl.checked(sort.desc());
} else {
this.sortDesc = null;
sortAscEl.checked(false);
sortDescEl.checked(false);
}
// this.setFilters(items, filter);
this.values = Object.keys(items);
this.filterValues = filter ? Array.from(filter.value) : Object.keys(items);
buildFilterBody.call(this, items, filter);
resetFilterHeader.call(this);
}
setOffset(v) {
this.el.offset(v).show();
let tindex = 1;
bindClickoutside(this.el, () => {
if (tindex <= 0) {
this.hide();
}
tindex -= 1;
});
}
show() {
this.el.show();
}
hide() {
this.el.hide();
unbindClickoutside(this.el);
}
}

View File

@@ -0,0 +1,138 @@
import { cssPrefix } from '../config.js';
import { h } from './element.js';
import { bindClickoutside, unbindClickoutside } from './event.js';
function inputMovePrev(evt) {
evt.preventDefault();
evt.stopPropagation();
const { filterItems } = this;
if (filterItems.length <= 0) return;
if (this.itemIndex >= 0) filterItems[this.itemIndex].toggle();
this.itemIndex -= 1;
if (this.itemIndex < 0) {
this.itemIndex = filterItems.length - 1;
}
filterItems[this.itemIndex].toggle();
}
function inputMoveNext(evt) {
evt.stopPropagation();
const { filterItems } = this;
if (filterItems.length <= 0) return;
if (this.itemIndex >= 0) filterItems[this.itemIndex].toggle();
this.itemIndex += 1;
if (this.itemIndex > filterItems.length - 1) {
this.itemIndex = 0;
}
filterItems[this.itemIndex].toggle();
}
function inputEnter(evt) {
evt.preventDefault();
const { filterItems } = this;
if (filterItems.length <= 0) return;
evt.stopPropagation();
if (this.itemIndex < 0) this.itemIndex = 0;
filterItems[this.itemIndex].el.click();
this.hide();
}
function inputKeydownHandler(evt) {
const { keyCode } = evt;
if (evt.ctrlKey) {
evt.stopPropagation();
}
switch (keyCode) {
case 37: // left
evt.stopPropagation();
break;
case 38: // up
inputMovePrev.call(this, evt);
break;
case 39: // right
evt.stopPropagation();
break;
case 40: // down
inputMoveNext.call(this, evt);
break;
case 13: // enter
inputEnter.call(this, evt);
break;
case 9:
inputEnter.call(this, evt);
break;
default:
evt.stopPropagation();
break;
}
}
export default class Suggest {
constructor(items, itemClick, width = '200px') {
this.filterItems = [];
this.items = items;
this.el = h('div', `${cssPrefix}-suggest`).css('width', width).hide();
this.itemClick = itemClick;
this.itemIndex = -1;
}
setOffset(v) {
this.el.cssRemoveKeys('top', 'bottom').offset(v);
}
hide() {
const { el } = this;
this.filterItems = [];
this.itemIndex = -1;
el.hide();
unbindClickoutside(this.el.parent());
}
setItems(items) {
this.items = items;
// this.search('');
}
search(word) {
let { items } = this;
if (!/^\s*$/.test(word)) {
items = items.filter(it => (it.key || it).startsWith(word.toUpperCase()));
}
items = items.map(it => {
let { title } = it;
if (title) {
if (typeof title === 'function') {
title = title();
}
} else {
title = it;
}
const item = h('div', `${cssPrefix}-item`)
.child(title)
.on('click.stop', () => {
this.itemClick(it);
this.hide();
});
if (it.label) {
item.child(h('div', 'label').html(it.label));
}
return item;
});
this.filterItems = items;
if (items.length <= 0) {
return;
}
const { el } = this;
// items[0].toggle();
el.html('')
.children(...items)
.show();
bindClickoutside(el.parent(), () => {
this.hide();
});
}
bindInputEvents(input) {
input.on('keydown', evt => inputKeydownHandler.call(this, evt));
}
}

View File

@@ -0,0 +1,395 @@
import { stringAt } from '../core/alphabet.js';
import { getFontSizePxByPt } from '../core/font.js';
import _cell from '../core/cell.js';
import { formulam } from '../core/formula.js';
import { formatm } from '../core/format.js';
import { Draw, DrawBox, thinLineWidth, npx } from '../canvas/draw.js';
// gobal var
const cellPaddingWidth = 5;
const tableFixedHeaderCleanStyle = { fillStyle: '#fff' };
const tableGridStyle = {
fillStyle: '#fff',
lineWidth: thinLineWidth,
strokeStyle: '#e6e6e6',
};
function tableFixedHeaderStyle() {
return {
textAlign: 'center',
textBaseline: 'middle',
font: `500 ${npx(12)}px Source Sans Pro`,
fillStyle: '#585757',
lineWidth: thinLineWidth(),
strokeStyle: '#e6e6e6',
};
}
function getDrawBox(data, rindex, cindex, yoffset = 0) {
const { left, top, width, height } = data.cellRect(rindex, cindex);
return new DrawBox(left, top + yoffset, width, height, cellPaddingWidth);
}
/*
function renderCellBorders(bboxes, translateFunc) {
const { draw } = this;
if (bboxes) {
const rset = new Set();
// console.log('bboxes:', bboxes);
bboxes.forEach(({ ri, ci, box }) => {
if (!rset.has(ri)) {
rset.add(ri);
translateFunc(ri);
}
draw.strokeBorders(box);
});
}
}
*/
export function renderCell(draw, data, rindex, cindex, yoffset = 0, scroll) {
const { sortedRowMap, rows, cols } = data;
if (rows.isHide(rindex) || cols.isHide(cindex)) return;
let nrindex = rindex;
if (sortedRowMap.has(rindex)) {
nrindex = sortedRowMap.get(rindex);
}
const cell = data.getCell(nrindex, cindex);
if (cell === null) return;
let frozen = false;
if ('editable' in cell && cell.editable === false) {
frozen = true;
}
const style = data.getCellStyleOrDefault(nrindex, cindex);
const dbox = getDrawBox(data, rindex, cindex, yoffset);
dbox.bgcolor = style.bgcolor;
if (style.border !== undefined) {
dbox.setBorders(style.border);
// bboxes.push({ ri: rindex, ci: cindex, box: dbox });
draw.strokeBorders(dbox);
}
draw.rect(dbox, () => {
if (['image'].includes(cell.type) && !cell.hidden) {
let celldata = data.rows.getCell(rindex, cindex);
// 如果单元格类型是单选框,则添加前缀的圆弧画法
// 在这里传递一下行坐标与列坐标的宽度,方便异步加载图片时使用
const fixedIndexWidth = cols.indexWidth;
const fixedIndexHeight = rows.indexHeight;
draw.geometry(cell, dbox, { fixedIndexWidth, fixedIndexHeight }, style, scroll, celldata);
}
// render text
let cellText = '';
if (!data.settings.evalPaused) {
cellText = _cell.render(cell.text || '', formulam, (y, x) => data.getCellTextOrDefault(x, y));
} else {
cellText = cell.text || '';
}
if (style.format) {
// console.log(data.formatm, '>>', cell.format);
cellText = formatm[style.format].render(cellText);
}
const font = Object.assign({}, style.font);
font.size = getFontSizePxByPt(font.size);
// console.log('style:', style);
draw.text(
cellText,
dbox,
{
align: style.align,
valign: style.valign,
font,
color: style.color,
strike: style.strike,
underline: style.underline,
},
style.textwrap,
);
// error
const error = data.validations.getError(rindex, cindex);
if (error) {
// console.log('error:', rindex, cindex, error);
draw.error(dbox);
}
if (frozen) {
draw.frozen(dbox);
}
});
}
function renderAutofilter(viewRange) {
const { data, draw } = this;
if (viewRange) {
const { autoFilter } = data;
if (!autoFilter.active()) return;
const afRange = autoFilter.hrange();
if (viewRange.intersects(afRange)) {
afRange.each((ri, ci) => {
const dbox = getDrawBox(data, ri, ci);
draw.dropdown(dbox);
});
}
}
}
function renderContent(viewRange, fw, fh, tx, ty, scroll) {
const { draw, data } = this;
draw.save();
draw.translate(fw, fh).translate(tx, ty);
const { exceptRowSet } = data;
// const exceptRows = Array.from(exceptRowSet);
const filteredTranslateFunc = ri => {
const ret = exceptRowSet.has(ri);
if (ret) {
const height = data.rows.getHeight(ri);
draw.translate(0, -height);
}
return !ret;
};
const exceptRowTotalHeight = data.exceptRowTotalHeight(viewRange.sri, viewRange.eri);
// 1 render cell
draw.save();
draw.translate(0, -exceptRowTotalHeight);
viewRange.each(
(ri, ci) => {
renderCell(draw, data, ri, ci, 0, scroll);
},
ri => filteredTranslateFunc(ri),
);
draw.restore();
// 2 render mergeCell
const rset = new Set();
draw.save();
draw.translate(0, -exceptRowTotalHeight);
data.eachMergesInView(viewRange, ({ sri, sci, eri }) => {
if (!exceptRowSet.has(sri)) {
renderCell(draw, data, sri, sci, 0, scroll);
} else if (!rset.has(sri)) {
rset.add(sri);
const height = data.rows.sumHeight(sri, eri + 1);
draw.translate(0, -height);
}
});
draw.restore();
// 3 render autofilter
renderAutofilter.call(this, viewRange);
draw.restore();
}
function renderSelectedHeaderCell(x, y, w, h) {
const { draw } = this;
draw.save();
draw.attr({ fillStyle: 'rgba(76,76,76,.1)' }).fillRect(x, y, w, h);
draw.restore();
}
function renderLeftHeaderCell(x, y, w, h, op) {
const { draw } = this;
op = op || {};
draw.save();
draw.attr({ fillStyle: 'rgba(76,76,76,.1)', ...op }).fillRect(x, y, w, h);
draw.restore();
}
// viewRange
// type: all | left | top
// w: the fixed width of header
// h: the fixed height of header
// tx: moving distance on x-axis
// ty: moving distance on y-axis
function renderFixedHeaders(type, viewRange, w, h, tx, ty) {
const { draw, data } = this;
const sumHeight = viewRange.h; // rows.sumHeight(viewRange.sri, viewRange.eri + 1);
const sumWidth = viewRange.w; // cols.sumWidth(viewRange.sci, viewRange.eci + 1);
const nty = ty + h;
const ntx = tx + w;
draw.save();
// draw rect background
draw.attr(tableFixedHeaderCleanStyle);
if (type === 'all' || type === 'left') draw.fillRect(0, nty, w, sumHeight);
if (type === 'all' || type === 'top') draw.fillRect(ntx, 0, sumWidth, h);
const { sri, sci, eri, eci } = data.selector.range;
// console.log(data.selectIndexes);
// draw text
// text font, align...
draw.attr(tableFixedHeaderStyle());
// y-header-text
if (type === 'all' || type === 'left') {
data.rowEach(viewRange.sri, viewRange.eri, (i, y1, rowHeight) => {
const y = nty + y1;
const ii = i;
draw.line([0, y], [w, y]);
if (
data.settings.leftFixHeaderRender &&
data.settings.leftFixHeaderRender instanceof Function
) {
let cfg = data.settings.leftFixHeaderRender(i);
if (cfg) {
renderLeftHeaderCell.call(this, 0, y, w, rowHeight, cfg);
} else {
if (sri <= ii && ii < eri + 1) {
renderSelectedHeaderCell.call(this, 0, y, w, rowHeight);
}
}
} else {
if (sri <= ii && ii < eri + 1) {
renderSelectedHeaderCell.call(this, 0, y, w, rowHeight);
}
}
draw.fillText(ii + 1, w / 2, y + rowHeight / 2);
if (i > 0 && data.rows.isHide(i - 1)) {
draw.save();
draw.attr({ strokeStyle: '#c6c6c6' });
draw.line([5, y + 5], [w - 5, y + 5]);
draw.restore();
}
});
draw.line([0, sumHeight + nty], [w, sumHeight + nty]);
draw.line([w, nty], [w, sumHeight + nty]);
}
// x-header-text
if (type === 'all' || type === 'top') {
data.colEach(viewRange.sci, viewRange.eci, (i, x1, colWidth) => {
const x = ntx + x1;
const ii = i;
draw.line([x, 0], [x, h]);
if (sci <= ii && ii < eci + 1) {
renderSelectedHeaderCell.call(this, x, 0, colWidth, h);
}
draw.fillText(stringAt(ii), x + colWidth / 2, h / 2);
if (i > 0 && data.cols.isHide(i - 1)) {
draw.save();
draw.attr({ strokeStyle: '#c6c6c6' });
draw.line([x + 5, 5], [x + 5, h - 5]);
draw.restore();
}
});
draw.line([sumWidth + ntx, 0], [sumWidth + ntx, h]);
draw.line([0, h], [sumWidth + ntx, h]);
}
draw.restore();
}
function renderFixedLeftTopCell(fw, fh) {
const { draw, data } = this;
if (data.settings.mode !== 'edit') return;
draw.save();
// left-top-cell
draw.attr({ fillStyle: '#fff' }).fillRect(0, 0, fw, fh);
draw.restore();
}
function renderContentGrid({ sri, sci, eri, eci, w, h }, fw, fh, tx, ty) {
const { draw, data } = this;
const { settings } = data;
draw.save();
draw.attr(tableGridStyle).translate(fw + tx, fh + ty);
// const sumWidth = cols.sumWidth(sci, eci + 1);
// const sumHeight = rows.sumHeight(sri, eri + 1);
// console.log('sumWidth:', sumWidth);
// draw.clearRect(0, 0, w, h);
if (!settings.showGrid) {
draw.restore();
return;
}
// console.log('rowStart:', rowStart, ', rowLen:', rowLen);
data.rowEach(sri, eri, (i, y, ch) => {
// console.log('y:', y);
if (i !== sri) draw.line([0, y], [w, y]);
if (i === eri) draw.line([0, y + ch], [w, y + ch]);
});
data.colEach(sci, eci, (i, x, cw) => {
if (i !== sci) draw.line([x, 0], [x, h]);
if (i === eci) draw.line([x + cw, 0], [x + cw, h]);
});
draw.restore();
}
function renderFreezeHighlightLine(fw, fh, ftw, fth) {
const { draw, data } = this;
const twidth = data.viewWidth() - fw;
const theight = data.viewHeight() - fh;
draw.save().translate(fw, fh).attr({ strokeStyle: 'rgba(75, 137, 255, .6)' });
draw.line([0, fth], [twidth, fth]);
draw.line([ftw, 0], [ftw, theight]);
draw.restore();
}
/** end */
class Table {
constructor(el, data) {
this.el = el;
this.draw = new Draw(el, data.viewWidth(), data.viewHeight());
this.data = data;
}
resetData(data) {
this.data = data;
this.render();
}
render() {
// resize canvas
const { data } = this;
const { rows, cols } = data;
// fixed width of header
const fw = cols.indexWidth;
// fixed height of header
const fh = rows.height;
this.draw.resize(data.viewWidth(), data.viewHeight());
this.clear();
const viewRange = data.viewRange();
// renderAll.call(this, viewRange, data.scroll);
const tx = data.freezeTotalWidth();
const ty = data.freezeTotalHeight();
const { x, y } = data.scroll;
// 1
renderContentGrid.call(this, viewRange, fw, fh, tx, ty);
renderContent.call(this, viewRange, fw, fh, -x, -y, data);
renderFixedHeaders.call(this, 'all', viewRange, fw, fh, tx, ty);
renderFixedLeftTopCell.call(this, fw, fh);
const [fri, fci] = data.freeze;
if (fri > 0 || fci > 0) {
// 2
if (fri > 0) {
const vr = viewRange.clone();
vr.sri = 0;
vr.eri = fri - 1;
vr.h = ty;
renderContentGrid.call(this, vr, fw, fh, tx, 0);
renderContent.call(this, vr, fw, fh, -x, 0, data);
renderFixedHeaders.call(this, 'top', vr, fw, fh, tx, 0);
}
// 3
if (fci > 0) {
const vr = viewRange.clone();
vr.sci = 0;
vr.eci = fci - 1;
vr.w = tx;
renderContentGrid.call(this, vr, fw, fh, 0, ty);
renderFixedHeaders.call(this, 'left', vr, fw, fh, 0, ty);
renderContent.call(this, vr, fw, fh, 0, -y, data);
}
// 4
const freezeViewRange = data.freezeViewRange();
renderContentGrid.call(this, freezeViewRange, fw, fh, 0, 0);
renderFixedHeaders.call(this, 'all', freezeViewRange, fw, fh, 0, 0);
renderContent.call(this, freezeViewRange, fw, fh, 0, 0, data);
// 5
renderFreezeHighlightLine.call(this, fw, fh, tx, ty);
}
}
clear() {
this.draw.clear();
}
}
export default Table;

View File

@@ -0,0 +1,258 @@
import { cssPrefix } from '../config.js';
import { t } from '../locale/locale.js';
import { h } from './element.js';
import { bind } from './event.js';
import tooltip from './tooltip.js';
import DropdownFont from './dropdown_font.js';
import DropdownFontSize from './dropdown_fontsize.js';
import DropdownFormat from './dropdown_format.js';
import DropdownFormula from './dropdown_formula.js';
import DropdownColor from './dropdown_color.js';
import DropdownAlign from './dropdown_align.js';
import DropdownBorder from './dropdown_border.js';
import Dropdown from './dropdown.js';
import Icon from './icon.js';
function buildIcon(name) {
return new Icon(name);
}
function buildButton(tooltipdata) {
return h('div', `${cssPrefix}-toolbar-btn`)
.on('mouseenter', evt => {
tooltip(tooltipdata, evt.target);
})
.attr('data-tooltip', tooltipdata);
}
function buildDivider() {
return h('div', `${cssPrefix}-toolbar-divider`);
}
function buildButtonWithIcon(
tooltipdata,
iconName,
change = () => {
console.log('empty function');
},
) {
return buildButton(tooltipdata)
.child(buildIcon(iconName))
.on('click', () => change());
}
function bindDropdownChange() {
this.ddFormat.change = it => this.change('format', it.key);
this.ddFont.change = it => this.change('font-name', it.key);
this.ddFormula.change = it => this.change('formula', it.key);
this.ddFontSize.change = it => this.change('font-size', it.pt);
this.ddTextColor.change = it => this.change('color', it);
this.ddFillColor.change = it => this.change('bgcolor', it);
this.ddAlign.change = it => this.change('align', it);
this.ddVAlign.change = it => this.change('valign', it);
this.ddBorder.change = it => this.change('border', it);
}
function toggleChange(type) {
let elName = type;
const types = type.split('-');
if (types.length > 1) {
types.forEach((it, i) => {
if (i === 0) elName = it;
else elName += it[0].toUpperCase() + it.substring(1);
});
}
const el = this[`${elName}El`];
el.toggle();
this.change(type, el.hasClass('active'));
}
class DropdownMore extends Dropdown {
constructor() {
const icon = new Icon('ellipsis');
const moreBtns = h('div', `${cssPrefix}-toolbar-more`);
super(icon, 'auto', false, 'bottom-right', moreBtns);
this.moreBtns = moreBtns;
this.contentEl.css('max-width', '420px');
}
}
function initBtns2() {
this.btns2 = this.btnChildren.map(it => {
const rect = it.box();
const { marginLeft, marginRight } = it.computedStyle();
return [it, rect.width + parseInt(marginLeft, 10) + parseInt(marginRight, 10)];
});
}
function moreResize() {
const { el, btns, moreEl, ddMore, btns2 } = this;
const { moreBtns, contentEl } = ddMore;
el.css('width', `${this.widthFn() - 60}px`);
const elBox = el.box();
let sumWidth = 160;
let sumWidth2 = 12;
const list1 = [];
const list2 = [];
btns2.forEach(([it, w], index) => {
sumWidth += w;
if (index === btns2.length - 1 || sumWidth < elBox.width) {
list1.push(it);
} else {
sumWidth2 += w;
list2.push(it);
}
});
btns.html('').children(...list1);
moreBtns.html('').children(...list2);
contentEl.css('width', `${sumWidth2}px`);
if (list2.length > 0) {
moreEl.show();
} else {
moreEl.hide();
}
}
export default class Toolbar {
constructor(data, widthFn, isHide = false) {
this.data = data;
this.change = () => {
console.log('empty function');
};
this.widthFn = widthFn;
const style = data.defaultStyle();
// console.log('data:', data);
this.ddFormat = new DropdownFormat();
this.ddFont = new DropdownFont();
this.ddFormula = new DropdownFormula();
this.ddFontSize = new DropdownFontSize();
this.ddTextColor = new DropdownColor('text-color', style.color);
this.ddFillColor = new DropdownColor('fill-color', style.bgcolor);
this.ddAlign = new DropdownAlign(['left', 'center', 'right'], style.align);
this.ddVAlign = new DropdownAlign(['top', 'middle', 'bottom'], style.valign);
this.ddBorder = new DropdownBorder();
this.ddMore = new DropdownMore();
this.btnChildren = [
(this.undoEl = buildButtonWithIcon(`${t('toolbar.undo')} (Ctrl+Z)`, 'undo', () =>
this.change('undo'),
)),
(this.redoEl = buildButtonWithIcon(`${t('toolbar.undo')} (Ctrl+Y)`, 'redo', () =>
this.change('redo'),
)),
// this.printEl = buildButtonWithIcon('Print (Ctrl+P)', 'print', () => this.change('print')),
(this.paintformatEl = buildButtonWithIcon(`${t('toolbar.paintformat')}`, 'paintformat', () =>
toggleChange.call(this, 'paintformat'),
)),
(this.clearformatEl = buildButtonWithIcon(`${t('toolbar.clearformat')}`, 'clearformat', () =>
this.change('clearformat'),
)),
buildDivider(),
buildButton(`${t('toolbar.format')}`).child(this.ddFormat.el),
buildDivider(),
buildButton(`${t('toolbar.font')}`).child(this.ddFont.el),
buildButton(`${t('toolbar.fontSize')}`).child(this.ddFontSize.el),
buildDivider(),
(this.fontBoldEl = buildButtonWithIcon(`${t('toolbar.fontBold')} (Ctrl+B)`, 'bold', () =>
toggleChange.call(this, 'font-bold'),
)),
(this.fontItalicEl = buildButtonWithIcon(
`${t('toolbar.fontItalic')} (Ctrl+I)`,
'italic',
() => toggleChange.call(this, 'font-italic'),
)),
(this.underlineEl = buildButtonWithIcon(
`${t('toolbar.underline')} (Ctrl+U)`,
'underline',
() => toggleChange.call(this, 'underline'),
)),
(this.strikeEl = buildButtonWithIcon(`${t('toolbar.strike')}`, 'strike', () =>
toggleChange.call(this, 'strike'),
)),
buildButton(`${t('toolbar.textColor')}`).child(this.ddTextColor.el),
buildDivider(),
buildButton(`${t('toolbar.fillColor')}`).child(this.ddFillColor.el),
buildButton(`${t('toolbar.border')}`).child(this.ddBorder.el),
(this.mergeEl = buildButtonWithIcon(`${t('toolbar.merge')}`, 'merge', () =>
toggleChange.call(this, 'merge'),
)),
buildDivider(),
buildButton(`${t('toolbar.align')}`).child(this.ddAlign.el),
buildButton(`${t('toolbar.valign')}`).child(this.ddVAlign.el),
(this.textwrapEl = buildButtonWithIcon(`${t('toolbar.textwrap')}`, 'textwrap', () =>
toggleChange.call(this, 'textwrap'),
)),
buildDivider(),
// this.linkEl = buildButtonWithIcon('Insert link', 'link'),
// this.chartEl = buildButtonWithIcon('Insert chart', 'chart'),
(this.freezeEl = buildButtonWithIcon(`${t('toolbar.freeze')}`, 'freeze', () =>
toggleChange.call(this, 'freeze'),
)),
(this.autofilterEl = buildButtonWithIcon(`${t('toolbar.autofilter')}`, 'autofilter', () =>
toggleChange.call(this, 'autofilter'),
)),
buildButton(`${t('toolbar.formula')}`).child(this.ddFormula.el),
// buildDivider(),
(this.moreEl = buildButton(`${t('toolbar.more')}`)
.child(this.ddMore.el)
.hide()),
];
this.el = h('div', `${cssPrefix}-toolbar`);
this.btns = h('div', `${cssPrefix}-toolbar-btns`).children(...this.btnChildren);
this.el.child(this.btns);
if (isHide) this.el.hide();
bindDropdownChange.call(this);
this.reset();
setTimeout(() => {
initBtns2.call(this);
moreResize.call(this);
}, 0);
bind(window, 'resize', () => {
moreResize.call(this);
});
}
paintformatActive() {
return this.paintformatEl.hasClass('active');
}
paintformatToggle() {
this.paintformatEl.toggle();
}
trigger(type) {
toggleChange.call(this, type);
}
reset() {
const { data } = this;
const style = data.getSelectedCellStyle();
const cell = data.getSelectedCell();
// console.log('canUndo:', data.canUndo());
this.undoEl.disabled(!data.canUndo());
this.redoEl.disabled(!data.canRedo());
this.mergeEl.active(data.canUnmerge()).disabled(!data.selector.multiple());
this.autofilterEl.active(!data.canAutofilter());
// this.mergeEl.disabled();
// console.log('selectedCell:', style, cell);
const { font } = style;
this.ddFont.setTitle(font.name);
this.ddFontSize.setTitle(font.size);
this.fontBoldEl.active(font.bold);
this.fontItalicEl.active(font.italic);
this.underlineEl.active(style.underline);
this.strikeEl.active(style.strike);
this.ddTextColor.setTitle(style.color);
this.ddFillColor.setTitle(style.bgcolor);
this.ddAlign.setTitle(style.align);
this.ddVAlign.setTitle(style.valign);
this.textwrapEl.active(style.textwrap);
// console.log('freeze is Active:', data.freezeIsActive());
this.freezeEl.active(data.freezeIsActive());
if (cell) {
if (cell.format) {
this.ddFormat.setTitle(cell.format);
}
}
}
}

View File

@@ -0,0 +1,13 @@
import DropdownAlign from '../dropdown_align.js';
import DropdownItem from './dropdown_item.js';
export default class Align extends DropdownItem {
constructor(value) {
super('align', '', value);
}
dropdown() {
const { value } = this;
return new DropdownAlign(['left', 'center', 'right'], value);
}
}

View File

@@ -0,0 +1,11 @@
import ToggleItem from './toggle_item.js';
export default class Autofilter extends ToggleItem {
constructor() {
super('autofilter');
}
setState() {
console.log('empty function');
}
}

View File

@@ -0,0 +1,7 @@
import ToggleItem from './toggle_item.js';
export default class Bold extends ToggleItem {
constructor() {
super('font-bold', 'Ctrl+B');
}
}

View File

@@ -0,0 +1,12 @@
import DropdownBorder from '../dropdown_border.js';
import DropdownItem from './dropdown_item.js';
export default class Border extends DropdownItem {
constructor() {
super('border');
}
dropdown() {
return new DropdownBorder();
}
}

View File

@@ -0,0 +1,7 @@
import IconItem from './icon_item.js';
export default class Clearformat extends IconItem {
constructor() {
super('clearformat');
}
}

View File

@@ -0,0 +1,25 @@
import Item from './item.js';
export default class DropdownItem extends Item {
dropdown() {
console.log('empty function');
}
getValue(v) {
return v;
}
element() {
const { tag } = this;
this.dd = this.dropdown();
this.dd.change = it => this.change(tag, this.getValue(it));
return super.element().child(this.dd);
}
setState(v) {
if (v) {
this.value = v;
this.dd.setTitle(v);
}
}
}

View File

@@ -0,0 +1,13 @@
import DropdownColor from '../dropdown_color.js';
import DropdownItem from './dropdown_item.js';
export default class FillColor extends DropdownItem {
constructor(color) {
super('bgcolor', undefined, color);
}
dropdown() {
const { tag, value } = this;
return new DropdownColor(tag, value);
}
}

View File

@@ -0,0 +1,16 @@
import DropdownFont from '../dropdown_font.js';
import DropdownItem from './dropdown_item.js';
export default class Font extends DropdownItem {
constructor() {
super('font-name');
}
getValue(it) {
return it.key;
}
dropdown() {
return new DropdownFont();
}
}

View File

@@ -0,0 +1,16 @@
import DropdownFontsize from '../dropdown_fontsize.js';
import DropdownItem from './dropdown_item.js';
export default class Format extends DropdownItem {
constructor() {
super('font-size');
}
getValue(it) {
return it.pt;
}
dropdown() {
return new DropdownFontsize();
}
}

View File

@@ -0,0 +1,16 @@
import DropdownFormat from '../dropdown_format.js';
import DropdownItem from './dropdown_item.js';
export default class Format extends DropdownItem {
constructor() {
super('format');
}
getValue(it) {
return it.key;
}
dropdown() {
return new DropdownFormat();
}
}

View File

@@ -0,0 +1,16 @@
import DropdownFormula from '../dropdown_formula.js';
import DropdownItem from './dropdown_item.js';
export default class Format extends DropdownItem {
constructor() {
super('formula');
}
getValue(it) {
return it.key;
}
dropdown() {
return new DropdownFormula();
}
}

View File

@@ -0,0 +1,7 @@
import ToggleItem from './toggle_item.js';
export default class Freeze extends ToggleItem {
constructor() {
super('freeze');
}
}

View File

@@ -0,0 +1,15 @@
import Icon from '../icon.js';
import Item from './item.js';
export default class IconItem extends Item {
element() {
return super
.element()
.child(new Icon(this.tag))
.on('click', () => this.change(this.tag));
}
setState(disabled) {
this.el.disabled(disabled);
}
}

View File

@@ -0,0 +1,241 @@
import { h } from '../element.js';
import { cssPrefix } from '../../config.js';
import { bind } from '../event.js';
import Align from './align.js';
import Valign from './valign.js';
// import Autofilter from './autofilter.js';
import Bold from './bold.js';
import Italic from './italic.js';
import Strike from './strike.js';
import Underline from './underline.js';
import Border from './border.js';
import Clearformat from './clearformat.js';
import Paintformat from './paintformat.js';
import TextColor from './text_color.js';
import FillColor from './fill_color.js';
import FontSize from './font_size.js';
import Font from './font.js';
import Format from './format.js';
// import Formula from './formula.js';
// import Freeze from './freeze';
import Merge from './merge.js';
import Redo from './redo.js';
import Undo from './undo.js';
// import Print from './print.js';
// import Textwrap from './textwrap';
import More from './more.js';
import Item from './item.js';
function buildDivider() {
return h('div', `${cssPrefix}-toolbar-divider`);
}
function initBtns2() {
this.btns2 = [];
this.items.forEach(it => {
if (Array.isArray(it)) {
it.forEach(({ el }) => {
const rect = el.box();
const { marginLeft, marginRight } = el.computedStyle();
this.btns2.push([el, rect.width + parseInt(marginLeft, 10) + parseInt(marginRight, 10)]);
});
} else {
const rect = it.box();
const { marginLeft, marginRight } = it.computedStyle();
this.btns2.push([it, rect.width + parseInt(marginLeft, 10) + parseInt(marginRight, 10)]);
}
});
}
function moreResize() {
const { el, btns, moreEl, btns2 } = this;
const { moreBtns, contentEl } = moreEl.dd;
el.css('width', `${this.widthFn()}px`);
const elBox = el.box();
let sumWidth = 160;
let sumWidth2 = 12;
const list1 = [];
const list2 = [];
btns2.forEach(([it, w], index) => {
sumWidth += w;
if (index === btns2.length - 1 || sumWidth < elBox.width) {
list1.push(it);
} else {
sumWidth2 += w;
list2.push(it);
}
});
btns.html('').children(...list1);
moreBtns.html('').children(...list2);
contentEl.css('width', `${sumWidth2}px`);
if (list2.length > 0) {
moreEl.show();
} else {
moreEl.hide();
}
}
function genBtn(it) {
const btn = new Item();
btn.el.on('click', () => {
if (it.onClick) it.onClick(this.data.getData(), this.data);
});
btn.tip = it.tip || '';
let { el } = it;
if (it.icon) {
el = h('img').attr('src', it.icon);
}
if (el) {
const icon = h('div', `${cssPrefix}-icon`);
icon.child(el);
btn.el.child(icon);
}
return btn;
}
export default class Toolbar {
constructor(data, widthFn, isHide = false) {
this.data = data;
this.change = () => {
console.log('empty function');
};
this.widthFn = widthFn;
this.isHide = isHide;
const style = data.defaultStyle();
this.items = [
[
(this.undoEl = new Undo()),
(this.redoEl = new Redo()),
// new Print(),
(this.paintformatEl = new Paintformat()),
(this.clearformatEl = new Clearformat()),
],
buildDivider(),
[(this.formatEl = new Format())],
buildDivider(),
[(this.fontEl = new Font()), (this.fontSizeEl = new FontSize())],
buildDivider(),
[
(this.boldEl = new Bold()),
(this.italicEl = new Italic()),
(this.underlineEl = new Underline()),
(this.strikeEl = new Strike()),
(this.textColorEl = new TextColor(style.color)),
],
buildDivider(),
[
(this.fillColorEl = new FillColor(style.bgcolor)),
(this.borderEl = new Border()),
(this.mergeEl = new Merge()),
],
buildDivider(),
[
(this.alignEl = new Align(style.align)),
(this.valignEl = new Valign(style.valign)),
// this.textwrapEl = new Textwrap()
],
// buildDivider(),
// [
// this.freezeEl = new Freeze(),
// this.autofilterEl = new Autofilter(),
// this.formulaEl = new Formula()
// ]
];
const { extendToolbar = {} } = data.settings;
if (extendToolbar.left && extendToolbar.left.length > 0) {
this.items.unshift(buildDivider());
const btns = extendToolbar.left.map(genBtn.bind(this));
this.items.unshift(btns);
}
if (extendToolbar.right && extendToolbar.right.length > 0) {
this.items.push(buildDivider());
const btns = extendToolbar.right.map(genBtn.bind(this));
this.items.push(btns);
}
this.items.push([(this.moreEl = new More())]);
this.el = h('div', `${cssPrefix}-toolbar`);
this.btns = h('div', `${cssPrefix}-toolbar-btns`);
this.items.forEach(it => {
if (Array.isArray(it)) {
it.forEach(i => {
this.btns.child(i.el);
i.change = (...args) => {
this.change(...args);
};
});
} else {
this.btns.child(it.el);
}
});
this.el.child(this.btns);
if (isHide) {
this.el.hide();
} else {
this.reset();
setTimeout(() => {
initBtns2.call(this);
moreResize.call(this);
}, 0);
bind(window, 'resize', () => {
moreResize.call(this);
});
}
}
paintformatActive() {
return this.paintformatEl.active();
}
paintformatToggle() {
this.paintformatEl.toggle();
}
trigger(type) {
this[`${type}El`].click();
}
resetData(data) {
this.data = data;
this.reset();
}
reset() {
if (this.isHide) return;
const { data } = this;
const style = data.getSelectedCellStyle();
// console.log('canUndo:', data.canUndo());
this.undoEl.setState(!data.canUndo());
this.redoEl.setState(!data.canRedo());
this.mergeEl.setState(data.canUnmerge(), !data.selector.multiple());
// this.autofilterEl.setState(!data.canAutofilter());
// this.mergeEl.disabled();
// console.log('selectedCell:', style, cell);
const { font, format } = style;
this.formatEl.setState(format);
this.fontEl.setState(font.name);
this.fontSizeEl.setState(font.size);
this.boldEl.setState(font.bold);
this.italicEl.setState(font.italic);
this.underlineEl.setState(style.underline);
this.strikeEl.setState(style.strike);
this.textColorEl.setState(style.color);
this.fillColorEl.setState(style.bgcolor);
this.alignEl.setState(style.align);
this.valignEl.setState(style.valign);
// this.textwrapEl.setState(style.textwrap);
// console.log('freeze is Active:', data.freezeIsActive());
// this.freezeEl.setState(data.freezeIsActive());
}
}

View File

@@ -0,0 +1,7 @@
import ToggleItem from './toggle_item.js';
export default class Italic extends ToggleItem {
constructor() {
super('font-italic', 'Ctrl+I');
}
}

View File

@@ -0,0 +1,35 @@
import { cssPrefix } from '../../config.js';
import tooltip from '../tooltip.js';
import { h } from '../element.js';
import { t } from '../../locale/locale.js';
export default class Item {
// tooltip
// tag: the subclass type
// shortcut: shortcut key
constructor(tag, shortcut, value) {
this.tip = '';
if (tag) this.tip = t(`toolbar.${tag.replace(/-[a-z]/g, c => c[1].toUpperCase())}`);
if (shortcut) this.tip += ` (${shortcut})`;
this.tag = tag;
this.shortcut = shortcut;
this.value = value;
this.el = this.element();
this.change = () => {
console.log('empty function');
};
}
element() {
const { tip } = this;
return h('div', `${cssPrefix}-toolbar-btn`)
.on('mouseenter', evt => {
if (this.tip) tooltip(this.tip, evt.target);
})
.attr('data-tooltip', tip);
}
setState() {
console.log('empty function');
}
}

View File

@@ -0,0 +1,11 @@
import ToggleItem from './toggle_item.js';
export default class Merge extends ToggleItem {
constructor() {
super('merge');
}
setState(active, disabled) {
this.el.active(active).disabled(disabled);
}
}

View File

@@ -0,0 +1,35 @@
import Dropdown from '../dropdown.js';
import { cssPrefix } from '../../config.js';
import { h } from '../element.js';
import Icon from '../icon.js';
import DropdownItem from './dropdown_item.js';
class DropdownMore extends Dropdown {
constructor() {
const icon = new Icon('ellipsis');
const moreBtns = h('div', `${cssPrefix}-toolbar-more`);
super(icon, 'auto', false, 'bottom-right', moreBtns);
this.moreBtns = moreBtns;
this.contentEl.css('max-width', '420px');
}
}
export default class More extends DropdownItem {
constructor() {
super('more');
this.el.hide();
}
dropdown() {
return new DropdownMore();
}
show() {
this.el.show();
}
hide() {
this.el.hide();
}
}

View File

@@ -0,0 +1,11 @@
import ToggleItem from './toggle_item.js';
export default class Paintformat extends ToggleItem {
constructor() {
super('paintformat');
}
setState() {
console.log('empty function');
}
}

View File

@@ -0,0 +1,7 @@
import IconItem from './icon_item.js';
export default class Print extends IconItem {
constructor() {
super('print', 'Ctrl+P');
}
}

View File

@@ -0,0 +1,7 @@
import IconItem from './icon_item.js';
export default class Redo extends IconItem {
constructor() {
super('redo', 'Ctrl+Y');
}
}

View File

@@ -0,0 +1,7 @@
import ToggleItem from './toggle_item.js';
export default class Strike extends ToggleItem {
constructor() {
super('strike', 'Ctrl+U');
}
}

View File

@@ -0,0 +1,13 @@
import DropdownColor from '../dropdown_color.js';
import DropdownItem from './dropdown_item.js';
export default class TextColor extends DropdownItem {
constructor(color) {
super('color', undefined, color);
}
dropdown() {
const { tag, value } = this;
return new DropdownColor(tag, value);
}
}

View File

@@ -0,0 +1,7 @@
import ToggleItem from './toggle_item.js';
export default class Textwrap extends ToggleItem {
constructor() {
super('textwrap');
}
}

View File

@@ -0,0 +1,28 @@
import Icon from '../icon.js';
import Item from './item.js';
export default class ToggleItem extends Item {
element() {
const { tag } = this;
return super
.element()
.child(new Icon(tag))
.on('click', () => this.click());
}
click() {
this.change(this.tag, this.toggle());
}
setState(active) {
this.el.active(active);
}
toggle() {
return this.el.toggle();
}
active() {
return this.el.hasClass('active');
}
}

View File

@@ -0,0 +1,7 @@
import ToggleItem from './toggle_item.js';
export default class Underline extends ToggleItem {
constructor() {
super('underline', 'Ctrl+U');
}
}

View File

@@ -0,0 +1,7 @@
import IconItem from './icon_item.js';
export default class Undo extends IconItem {
constructor() {
super('undo', 'Ctrl+Z');
}
}

View File

@@ -0,0 +1,13 @@
import DropdownAlign from '../dropdown_align.js';
import DropdownItem from './dropdown_item.js';
export default class Valign extends DropdownItem {
constructor(value) {
super('valign', '', value);
}
dropdown() {
const { value } = this;
return new DropdownAlign(['top', 'middle', 'bottom'], value);
}
}

View File

@@ -0,0 +1,27 @@
import { cssPrefix } from '../config.js';
import { h } from './element.js';
import { bind } from './event.js';
export default function tooltip(html, target) {
if (target.classList.contains('active')) {
return;
}
const { left, top, width, height } = target.getBoundingClientRect();
const el = h('div', `${cssPrefix}-tooltip`).html(html).show();
document.body.appendChild(el.el);
const elBox = el.box();
// console.log('elBox:', elBox);
el.css('left', `${left + width / 2 - elBox.width / 2}px`).css('top', `${top + height + 2}px`);
bind(target, 'mouseleave', () => {
if (document.body.contains(el.el)) {
document.body.removeChild(el.el);
}
});
bind(target, 'click', () => {
if (document.body.contains(el.el)) {
document.body.removeChild(el.el);
}
});
}

View File

@@ -0,0 +1,6 @@
export const cssPrefix = 'x-spreadsheet';
export const dpr = window.devicePixelRatio || 1;
export default {
cssPrefix,
dpr,
};

View File

@@ -0,0 +1,28 @@
// font.js
/**
* @typedef {number} fontsizePX px for fontSize
*/
/**
* @typedef {number} fontsizePT pt for fontSize
*/
/**
* @typedef {object} BaseFont
* @property {string} key inner key
* @property {string} title title for display
*/
/**
* @typedef {object} FontSize
* @property {fontsizePT} pt
* @property {fontsizePX} px
*/
// alphabet.js
/**
* @typedef {string} tagA1 A1 tag for XY-tag (0, 0)
* @example "A1"
*/
/**
* @typedef {[number, number]} tagXY
* @example [0, 0]
*/

View File

@@ -0,0 +1,117 @@
import './_.prototypes.js';
const alphabets = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
/** index number 2 letters
* @example stringAt(26) ==> 'AA'
* @date 2019-10-10
* @export
* @param {number} index
* @returns {string}
*/
export function stringAt(index) {
let str = '';
let cindex = index;
while (cindex >= alphabets.length) {
cindex /= alphabets.length;
cindex -= 1;
str += alphabets[parseInt(cindex, 10) % alphabets.length];
}
const last = index % alphabets.length;
str += alphabets[last];
return str;
}
/** translate letter in A1-tag to number
* @date 2019-10-10
* @export
* @param {string} str "AA" in A1-tag "AA1"
* @returns {number}
*/
export function indexAt(str) {
let ret = 0;
for (let i = 0; i !== str.length; ++i) ret = 26 * ret + str.charCodeAt(i) - 64;
return ret - 1;
}
// B10 => x,y
/** translate A1-tag to XY-tag
* @date 2019-10-10
* @export
* @param {tagA1} src
* @returns {tagXY}
*/
export function expr2xy(src) {
let x = '';
let y = '';
for (let i = 0; i < src.length; i += 1) {
if (src.charAt(i) >= '0' && src.charAt(i) <= '9') {
y += src.charAt(i);
} else {
x += src.charAt(i);
}
}
return [indexAt(x), parseInt(y, 10) - 1];
}
/** translate XY-tag to A1-tag
* @example x,y => B10
* @date 2019-10-10
* @export
* @param {number} x
* @param {number} y
* @returns {tagA1}
*/
export function xy2expr(x, y) {
return `${stringAt(x)}${y + 1}`;
}
/** translate A1-tag src by (xn, yn)
* @date 2019-10-10
* @export
* @param {tagA1} src
* @param {number} xn
* @param {number} yn
* @returns {tagA1}
*/
export function expr2expr(src, xn, yn, condition = () => true) {
if (xn === 0 && yn === 0) return src;
const [x, y] = expr2xy(src);
if (!condition(x, y)) return src;
return xy2expr(x + xn, y + yn);
}
export default {
stringAt,
indexAt,
expr2xy,
xy2expr,
expr2expr,
};

View File

@@ -0,0 +1,183 @@
import { CellRange } from './cell_range.js';
// operator: all|eq|neq|gt|gte|lt|lte|in|be
// value:
// in => []
// be => [min, max]
class Filter {
constructor(ci, operator, value) {
this.ci = ci;
this.operator = operator;
this.value = value;
}
set(operator, value) {
this.operator = operator;
this.value = value;
}
includes(v) {
const { operator, value } = this;
if (operator === 'all') {
return true;
}
if (operator === 'in') {
return value.includes(v);
}
return false;
}
vlength() {
const { operator, value } = this;
if (operator === 'in') {
return value.length;
}
return 0;
}
getData() {
const { ci, operator, value } = this;
return { ci, operator, value };
}
}
class Sort {
constructor(ci, order) {
this.ci = ci;
this.order = order;
}
asc() {
return this.order === 'asc';
}
desc() {
return this.order === 'desc';
}
}
export default class AutoFilter {
constructor() {
this.ref = null;
this.filters = [];
this.sort = null;
}
setData({ ref, filters, sort }) {
if (ref != null) {
this.ref = ref;
this.filters = filters.map(it => new Filter(it.ci, it.operator, it.value));
if (sort) {
this.sort = new Sort(sort.ci, sort.order);
}
}
}
getData() {
if (this.active()) {
const { ref, filters, sort } = this;
return { ref, filters: filters.map(it => it.getData()), sort };
}
return {};
}
addFilter(ci, operator, value) {
const filter = this.getFilter(ci);
if (filter == null) {
this.filters.push(new Filter(ci, operator, value));
} else {
filter.set(operator, value);
}
}
setSort(ci, order) {
this.sort = order ? new Sort(ci, order) : null;
}
includes(ri, ci) {
if (this.active()) {
return this.hrange().includes(ri, ci);
}
return false;
}
getSort(ci) {
const { sort } = this;
if (sort && sort.ci === ci) {
return sort;
}
return null;
}
getFilter(ci) {
const { filters } = this;
for (let i = 0; i < filters.length; i += 1) {
if (filters[i].ci === ci) {
return filters[i];
}
}
return null;
}
filteredRows(getCell) {
// const ary = [];
// let lastri = 0;
const rset = new Set();
const fset = new Set();
if (this.active()) {
const { sri, eri } = this.range();
const { filters } = this;
for (let ri = sri + 1; ri <= eri; ri += 1) {
for (let i = 0; i < filters.length; i += 1) {
const filter = filters[i];
const cell = getCell(ri, filter.ci);
const ctext = cell ? cell.text : '';
if (!filter.includes(ctext)) {
rset.add(ri);
break;
} else {
fset.add(ri);
}
}
}
}
return { rset, fset };
}
items(ci, getCell) {
const m = {};
if (this.active()) {
const { sri, eri } = this.range();
for (let ri = sri + 1; ri <= eri; ri += 1) {
const cell = getCell(ri, ci);
if (cell !== null && !/^\s*$/.test(cell.text)) {
const key = cell.text;
const cnt = (m[key] || 0) + 1;
m[key] = cnt;
} else {
m[''] = (m[''] || 0) + 1;
}
}
}
return m;
}
range() {
return CellRange.valueOf(this.ref);
}
hrange() {
const r = this.range();
r.eri = r.sri;
return r;
}
clear() {
this.ref = null;
this.filters = [];
this.sort = null;
}
active() {
return this.ref !== null;
}
}

View File

@@ -0,0 +1,224 @@
import { expr2xy, xy2expr } from './alphabet.js';
import { numberCalc } from './helper.js';
// Converting infix expression to a suffix expression
// src: AVERAGE(SUM(A1,A2), B1) + 50 + B20
// return: [A1, A2], SUM[, B1],AVERAGE,50,+,B20,+
const infixExprToSuffixExpr = src => {
const operatorStack = [];
const stack = [];
let subStrs = []; // SUM, A1, B2, 50 ...
let fnArgType = 0; // 1 => , 2 => :
let fnArgOperator = '';
let fnArgsLen = 1; // A1,A2,A3...
let oldc = '';
for (let i = 0; i < src.length; i += 1) {
const c = src.charAt(i);
if (c !== ' ') {
if (c >= 'a' && c <= 'z') {
subStrs.push(c.toUpperCase());
} else if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || c === '.') {
subStrs.push(c);
} else if (c === '"') {
i += 1;
while (src.charAt(i) !== '"') {
subStrs.push(src.charAt(i));
i += 1;
}
stack.push(`"${subStrs.join('')}`);
subStrs = [];
} else if (c === '-' && /[+\-*/,(]/.test(oldc)) {
subStrs.push(c);
} else {
// console.log('subStrs:', subStrs.join(''), stack);
if (c !== '(' && subStrs.length > 0) {
stack.push(subStrs.join(''));
}
if (c === ')') {
let c1 = operatorStack.pop();
if (fnArgType === 2) {
// fn argument range => A1:B5
try {
const [ex, ey] = expr2xy(stack.pop());
const [sx, sy] = expr2xy(stack.pop());
// console.log('::', sx, sy, ex, ey);
let rangelen = 0;
for (let x = sx; x <= ex; x += 1) {
for (let y = sy; y <= ey; y += 1) {
stack.push(xy2expr(x, y));
rangelen += 1;
}
}
stack.push([c1, rangelen]);
} catch (e) {
// console.log(e);
}
} else if (fnArgType === 1 || fnArgType === 3) {
if (fnArgType === 3) stack.push(fnArgOperator);
// fn argument => A1,A2,B5
stack.push([c1, fnArgsLen]);
fnArgsLen = 1;
} else {
// console.log('c1:', c1, fnArgType, stack, operatorStack);
while (c1 !== '(') {
stack.push(c1);
if (operatorStack.length <= 0) break;
c1 = operatorStack.pop();
}
}
fnArgType = 0;
} else if (c === '=' || c === '>' || c === '<') {
const nc = src.charAt(i + 1);
fnArgOperator = c;
if (nc === '=' || nc === '-') {
fnArgOperator += nc;
i += 1;
}
fnArgType = 3;
} else if (c === ':') {
fnArgType = 2;
} else if (c === ',') {
if (fnArgType === 3) {
stack.push(fnArgOperator);
}
fnArgType = 1;
fnArgsLen += 1;
} else if (c === '(' && subStrs.length > 0) {
// function
operatorStack.push(subStrs.join(''));
} else {
// priority: */ > +-
// console.log('xxxx:', operatorStack, c, stack);
if (operatorStack.length > 0 && (c === '+' || c === '-')) {
let top = operatorStack[operatorStack.length - 1];
if (top !== '(') stack.push(operatorStack.pop());
if (top === '*' || top === '/') {
while (operatorStack.length > 0) {
top = operatorStack[operatorStack.length - 1];
if (top !== '(') stack.push(operatorStack.pop());
else break;
}
}
} else if (operatorStack.length > 0) {
const top = operatorStack[operatorStack.length - 1];
if (top === '*' || top === '/') stack.push(operatorStack.pop());
}
operatorStack.push(c);
}
subStrs = [];
}
oldc = c;
}
}
if (subStrs.length > 0) {
stack.push(subStrs.join(''));
}
while (operatorStack.length > 0) {
stack.push(operatorStack.pop());
}
return stack;
};
const evalSubExpr = (subExpr, cellRender) => {
const [fl] = subExpr;
let expr = subExpr;
if (fl === '"') {
return subExpr.substring(1);
}
let ret = 1;
if (fl === '-') {
expr = subExpr.substring(1);
ret = -1;
}
if (expr[0] >= '0' && expr[0] <= '9') {
return ret * Number(expr);
}
const [x, y] = expr2xy(expr);
return ret * cellRender(x, y);
};
// evaluate the suffix expression
// srcStack: <= infixExprToSufixExpr
// formulaMap: {'SUM': {}, ...}
// cellRender: (x, y) => {}
const evalSuffixExpr = (srcStack, formulaMap, cellRender, cellList) => {
const stack = [];
// console.log(':::::formulaMap:', formulaMap);
for (let i = 0; i < srcStack.length; i += 1) {
// console.log(':::>>>', srcStack[i]);
const expr = srcStack[i];
const fc = expr[0];
if (expr === '+') {
const top = stack.pop();
stack.push(numberCalc('+', stack.pop(), top));
} else if (expr === '-') {
if (stack.length === 1) {
const top = stack.pop();
stack.push(numberCalc('*', top, -1));
} else {
const top = stack.pop();
stack.push(numberCalc('-', stack.pop(), top));
}
} else if (expr === '*') {
stack.push(numberCalc('*', stack.pop(), stack.pop()));
} else if (expr === '/') {
const top = stack.pop();
stack.push(numberCalc('/', stack.pop(), top));
} else if (fc === '=' || fc === '>' || fc === '<') {
let top = stack.pop();
if (!Number.isNaN(top)) top = Number(top);
let left = stack.pop();
if (!Number.isNaN(left)) left = Number(left);
let ret = false;
if (fc === '=') {
ret = left === top;
} else if (expr === '>') {
ret = left > top;
} else if (expr === '>=') {
ret = left >= top;
} else if (expr === '<') {
ret = left < top;
} else if (expr === '<=') {
ret = left <= top;
}
stack.push(ret);
} else if (Array.isArray(expr)) {
const [formula, len] = expr;
const params = [];
for (let j = 0; j < len; j += 1) {
params.push(stack.pop());
}
stack.push(formulaMap[formula].render(params.reverse()));
} else {
if (cellList.includes(expr)) {
return 0;
}
if ((fc >= 'a' && fc <= 'z') || (fc >= 'A' && fc <= 'Z')) {
cellList.push(expr);
}
stack.push(evalSubExpr(expr, cellRender));
cellList.pop();
}
// console.log('stack:', stack);
}
return stack[0];
};
const cellRender = (src, formulaMap, getCellText, cellList = []) => {
if (src[0] === '=') {
const stack = infixExprToSuffixExpr(src.substring(1));
if (stack.length <= 0) return src;
return evalSuffixExpr(
stack,
formulaMap,
(x, y) => cellRender(getCellText(x, y), formulaMap, getCellText, cellList),
cellList,
);
}
return src;
};
export default {
render: cellRender,
};
export { infixExprToSuffixExpr };

View File

@@ -0,0 +1,214 @@
import { xy2expr, expr2xy } from './alphabet.js';
class CellRange {
constructor(sri, sci, eri, eci, w = 0, h = 0) {
this.sri = sri;
this.sci = sci;
this.eri = eri;
this.eci = eci;
this.w = w;
this.h = h;
}
set(sri, sci, eri, eci) {
this.sri = sri;
this.sci = sci;
this.eri = eri;
this.eci = eci;
}
multiple() {
return this.eri - this.sri > 0 || this.eci - this.sci > 0;
}
// cell-index: ri, ci
// cell-ref: A10
includes(...args) {
let [ri, ci] = [0, 0];
if (args.length === 1) {
[ci, ri] = expr2xy(args[0]);
} else if (args.length === 2) {
[ri, ci] = args;
}
const { sri, sci, eri, eci } = this;
return sri <= ri && ri <= eri && sci <= ci && ci <= eci;
}
each(cb, rowFilter = () => true) {
const { sri, sci, eri, eci } = this;
for (let i = sri; i <= eri; i += 1) {
if (rowFilter(i)) {
for (let j = sci; j <= eci; j += 1) {
cb(i, j);
}
}
}
}
contains(other) {
return (
this.sri <= other.sri &&
this.sci <= other.sci &&
this.eri >= other.eri &&
this.eci >= other.eci
);
}
// within
within(other) {
return (
this.sri >= other.sri &&
this.sci >= other.sci &&
this.eri <= other.eri &&
this.eci <= other.eci
);
}
// disjoint
disjoint(other) {
return (
this.sri > other.eri || this.sci > other.eci || other.sri > this.eri || other.sci > this.eci
);
}
// intersects
intersects(other) {
return (
this.sri <= other.eri &&
this.sci <= other.eci &&
other.sri <= this.eri &&
other.sci <= this.eci
);
}
// union
union(other) {
const { sri, sci, eri, eci } = this;
return new CellRange(
other.sri < sri ? other.sri : sri,
other.sci < sci ? other.sci : sci,
other.eri > eri ? other.eri : eri,
other.eci > eci ? other.eci : eci,
);
}
// intersection
// intersection(other) {}
// Returns Array<CellRange> that represents that part of this that does not intersect with other
// difference
difference(other) {
const ret = [];
const addRet = (sri, sci, eri, eci) => {
ret.push(new CellRange(sri, sci, eri, eci));
};
const { sri, sci, eri, eci } = this;
const dsr = other.sri - sri;
const dsc = other.sci - sci;
const der = eri - other.eri;
const dec = eci - other.eci;
if (dsr > 0) {
addRet(sri, sci, other.sri - 1, eci);
if (der > 0) {
addRet(other.eri + 1, sci, eri, eci);
if (dsc > 0) {
addRet(other.sri, sci, other.eri, other.sci - 1);
}
if (dec > 0) {
addRet(other.sri, other.eci + 1, other.eri, eci);
}
} else {
if (dsc > 0) {
addRet(other.sri, sci, eri, other.sci - 1);
}
if (dec > 0) {
addRet(other.sri, other.eci + 1, eri, eci);
}
}
} else if (der > 0) {
addRet(other.eri + 1, sci, eri, eci);
if (dsc > 0) {
addRet(sri, sci, other.eri, other.sci - 1);
}
if (dec > 0) {
addRet(sri, other.eci + 1, other.eri, eci);
}
}
if (dsc > 0) {
addRet(sri, sci, eri, other.sci - 1);
if (dec > 0) {
addRet(sri, other.eri + 1, eri, eci);
if (dsr > 0) {
addRet(sri, other.sci, other.sri - 1, other.eci);
}
if (der > 0) {
addRet(other.sri + 1, other.sci, eri, other.eci);
}
} else {
if (dsr > 0) {
addRet(sri, other.sci, other.sri - 1, eci);
}
if (der > 0) {
addRet(other.sri + 1, other.sci, eri, eci);
}
}
} else if (dec > 0) {
addRet(eri, other.eci + 1, eri, eci);
if (dsr > 0) {
addRet(sri, sci, other.sri - 1, other.eci);
}
if (der > 0) {
addRet(other.eri + 1, sci, eri, other.eci);
}
}
return ret;
}
size() {
return [this.eri - this.sri + 1, this.eci - this.sci + 1];
}
toString() {
const { sri, sci, eri, eci } = this;
let ref = xy2expr(sci, sri);
if (this.multiple()) {
ref = `${ref}:${xy2expr(eci, eri)}`;
}
return ref;
}
clone() {
const { sri, sci, eri, eci, w, h } = this;
return new CellRange(sri, sci, eri, eci, w, h);
}
/*
toJSON() {
return this.toString();
}
*/
equals(other) {
return (
this.eri === other.eri &&
this.eci === other.eci &&
this.sri === other.sri &&
this.sci === other.sci
);
}
static valueOf(ref) {
// B1:B8, B1 => 1 x 1 cell range
const refs = ref.split(':');
const [sci, sri] = expr2xy(refs[0]);
let [eri, eci] = [sri, sci];
if (refs.length > 1) {
[eci, eri] = expr2xy(refs[1]);
}
return new CellRange(sri, sci, eri, eci);
}
}
export default CellRange;
export { CellRange };

View File

@@ -0,0 +1,35 @@
export default class Clipboard {
constructor() {
this.range = null; // CellRange
this.state = 'clear';
}
copy(cellRange) {
this.range = cellRange;
this.state = 'copy';
return this;
}
cut(cellRange) {
this.range = cellRange;
this.state = 'cut';
return this;
}
isCopy() {
return this.state === 'copy';
}
isCut() {
return this.state === 'cut';
}
isClear() {
return this.state === 'clear';
}
clear() {
this.range = null;
this.state = 'clear';
}
}

View File

@@ -0,0 +1,80 @@
import helper from './helper.js';
class Cols {
constructor({ len, width, indexWidth, minWidth }) {
this._ = {};
this.len = len;
this.width = width;
this.indexWidth = indexWidth;
this.minWidth = minWidth;
}
setData(d) {
if (d.len) {
this.len = d.len;
delete d.len;
}
this._ = d;
}
getData() {
const { len } = this;
return Object.assign({ len }, this._);
}
getWidth(i) {
if (this.isHide(i)) return 0;
const col = this._[i];
if (col && col.width) {
return col.width;
}
return this.width;
}
getOrNew(ci) {
this._[ci] = this._[ci] || {};
return this._[ci];
}
setWidth(ci, width) {
const col = this.getOrNew(ci);
col.width = width;
}
unhide(idx) {
let index = idx;
while (index > 0) {
index -= 1;
if (this.isHide(index)) {
this.setHide(index, false);
} else break;
}
}
isHide(ci) {
const col = this._[ci];
return col && col.hide;
}
setHide(ci, v) {
const col = this.getOrNew(ci);
if (v === true) col.hide = true;
else delete col.hide;
}
setStyle(ci, style) {
const col = this.getOrNew(ci);
col.style = style;
}
sumWidth(min, max) {
return helper.rangeSum(min, max, i => this.getWidth(i));
}
totalWidth() {
return this.sumWidth(0, this.len);
}
}
export default {};
export { Cols };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
// docs
import './_.prototypes.js';
/** default font list
* @type {BaseFont[]}
*/
const baseFonts = [
{ key: 'Arial', title: 'Arial' },
{ key: 'Helvetica', title: 'Helvetica' },
{ key: 'Source Sans Pro', title: 'Source Sans Pro' },
{ key: 'Comic Sans MS', title: 'Comic Sans MS' },
{ key: 'Courier New', title: 'Courier New' },
{ key: 'Verdana', title: 'Verdana' },
{ key: 'Lato', title: 'Lato' },
];
/** default fontSize list
* @type {FontSize[]}
*/
const fontSizes = [
{ pt: 7.5, px: 10 },
{ pt: 8, px: 11 },
{ pt: 9, px: 12 },
{ pt: 10, px: 13 },
{ pt: 10.5, px: 14 },
{ pt: 11, px: 15 },
{ pt: 12, px: 16 },
{ pt: 14, px: 18.7 },
{ pt: 15, px: 20 },
{ pt: 16, px: 21.3 },
{ pt: 18, px: 24 },
{ pt: 22, px: 29.3 },
{ pt: 24, px: 32 },
{ pt: 26, px: 34.7 },
{ pt: 36, px: 48 },
{ pt: 42, px: 56 },
// { pt: 54, px: 71.7 },
// { pt: 63, px: 83.7 },
// { pt: 72, px: 95.6 },
];
/** map pt to px
* @date 2019-10-10
* @param {fontsizePT} pt
* @returns {fontsizePX}
*/
function getFontSizePxByPt(pt) {
for (let i = 0; i < fontSizes.length; i += 1) {
const fontSize = fontSizes[i];
if (fontSize.pt === pt) {
return fontSize.px;
}
}
return pt;
}
/** transform baseFonts to map
* @date 2019-10-10
* @param {BaseFont[]} [ary=[]]
* @returns {object}
*/
function fonts(ary = []) {
const map = {};
baseFonts.concat(ary).forEach(f => {
map[f.key] = f;
});
return map;
}
export default {};
export { fontSizes, fonts, baseFonts, getFontSizePxByPt };

View File

@@ -0,0 +1,106 @@
import { tf } from '../locale/locale.js';
const formatStringRender = v => v;
const formatNumberRender = v => {
// match "-12.1" or "12" or "12.1"
if (/^(-?\d*.?\d*)$/.test(v)) {
const v1 = Number(v).toFixed(2).toString();
const [first, ...parts] = v1.split('\\.');
return [first.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'), ...parts];
}
return v;
};
const baseFormats = [
{
key: 'normal',
title: tf('format.normal'),
type: 'string',
render: formatStringRender,
},
{
key: 'text',
title: tf('format.text'),
type: 'string',
render: formatStringRender,
},
{
key: 'number',
title: tf('format.number'),
type: 'number',
label: '1,000.12',
render: formatNumberRender,
},
{
key: 'percent',
title: tf('format.percent'),
type: 'number',
label: '10.12%',
render: v => `${v}%`,
},
{
key: 'rmb',
title: tf('format.rmb'),
type: 'number',
label: '¥10.00',
render: v => `${formatNumberRender(v)}`,
},
{
key: 'usd',
title: tf('format.usd'),
type: 'number',
label: '$10.00',
render: v => `$${formatNumberRender(v)}`,
},
{
key: 'eur',
title: tf('format.eur'),
type: 'number',
label: '€10.00',
render: v => `${formatNumberRender(v)}`,
},
{
key: 'date',
title: tf('format.date'),
type: 'date',
label: '26/09/2008',
render: formatStringRender,
},
{
key: 'time',
title: tf('format.time'),
type: 'date',
label: '15:59:00',
render: formatStringRender,
},
{
key: 'datetime',
title: tf('format.datetime'),
type: 'date',
label: '26/09/2008 15:59:00',
render: formatStringRender,
},
{
key: 'duration',
title: tf('format.duration'),
type: 'date',
label: '24:01:00',
render: formatStringRender,
},
];
// const formats = (ary = []) => {
// const map = {};
// baseFormats.concat(ary).forEach((f) => {
// map[f.key] = f;
// });
// return map;
// };
const formatm = {};
baseFormats.forEach(f => {
formatm[f.key] = f;
});
export default {};
export { formatm, baseFormats };

View File

@@ -0,0 +1,93 @@
/**
formula:
key
title
render
*/
/**
* @typedef {object} Formula
* @property {string} key
* @property {function} title
* @property {function} render
*/
import { tf } from '../locale/locale.js';
import { numberCalc } from './helper.js';
/** @type {Formula[]} */
const baseFormulas = [
{
key: 'SUM',
title: tf('formula.sum'),
render: ary => ary.reduce((a, b) => numberCalc('+', a, b), 0),
},
{
key: 'AVERAGE',
title: tf('formula.average'),
render: ary => ary.reduce((a, b) => Number(a) + Number(b), 0) / ary.length,
},
{
key: 'MAX',
title: tf('formula.max'),
render: ary => Math.max(...ary.map(v => Number(v))),
},
{
key: 'MIN',
title: tf('formula.min'),
render: ary => Math.min(...ary.map(v => Number(v))),
},
{
key: 'IF',
title: tf('formula._if'),
render: ([b, t, f]) => (b ? t : f),
},
{
key: 'AND',
title: tf('formula.and'),
render: ary => ary.every(it => it),
},
{
key: 'OR',
title: tf('formula.or'),
render: ary => ary.some(it => it),
},
{
key: 'CONCAT',
title: tf('formula.concat'),
render: ary => ary.join(''),
},
/* support: 1 + A1 + B2 * 3
{
key: 'DIVIDE',
title: tf('formula.divide'),
render: ary => ary.reduce((a, b) => Number(a) / Number(b)),
},
{
key: 'PRODUCT',
title: tf('formula.product'),
render: ary => ary.reduce((a, b) => Number(a) * Number(b),1),
},
{
key: 'SUBTRACT',
title: tf('formula.subtract'),
render: ary => ary.reduce((a, b) => Number(a) - Number(b)),
},
*/
];
const formulas = baseFormulas;
// const formulas = (formulaAry = []) => {
// const formulaMap = {};
// baseFormulas.concat(formulaAry).forEach((f) => {
// formulaMap[f.key] = f;
// });
// return formulaMap;
// };
const formulam = {};
baseFormulas.forEach(f => {
formulam[f.key] = f;
});
export default {};
export { formulam, formulas, baseFormulas };

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