feat: 移植 x-spreadsheet (#437)

Reviewed-on: daoyoucloud/tachycode#437
This commit is contained in:
sealday 2024-03-20 12:29:24 +08:00
parent db104c20ce
commit 5df5fb5be6
103 changed files with 12876 additions and 4 deletions

View File

@ -0,0 +1,53 @@
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import Spreadsheet from './x-sheet';
import { css } from '@nocobase/client';
import { demoData } from './x-sheet/demo';
export type SheetRef = {
getData: () => any;
};
export type SheetProps = {
data?: any;
style?: React.CSSProperties;
};
const Sheet = forwardRef<SheetRef, SheetProps>(({ data }, ref) => {
const workbookRef = useRef<Spreadsheet>(null);
const containerRef = useRef(null);
useImperativeHandle(ref, () => ({
getData,
}));
const getData = () => {
if (!workbookRef.current) {
throw new Error('Workbook is not initialized');
}
return workbookRef.current.getData();
};
useEffect(() => {
const workbook = new Spreadsheet('#sheet', {
view: {
height: () => containerRef.current.offsetHeight,
width: () => containerRef.current.offsetWidth,
},
}).loadData(demoData);
workbookRef.current = workbook;
}, [data]);
return (
<div
ref={containerRef}
id="sheet"
className={css`
height: 800px;
`}
/>
);
});
Sheet.displayName = 'Sheet';
export default Sheet;

View File

@ -1,3 +1,8 @@
export * from './GroupBlockConfigure/GroupBlockConfigure';
export * from './PDFViewer';
export * from './Sheet';
export * from './SignatureInput';
export * from './SignaturePad';
export * from './custom-components/CustomAssociatedField'; export * from './custom-components/CustomAssociatedField';
export * from './custom-components/CustomComponentDispatcher'; export * from './custom-components/CustomComponentDispatcher';
export * from './custom-components/CustomField'; export * from './custom-components/CustomField';
@ -7,7 +12,3 @@ export * from './fields/Expression';
export * from './system/MobileLink'; export * from './system/MobileLink';
export * from './system/Notifications'; export * from './system/Notifications';
export * from './system/OnlineUserProvider'; export * from './system/OnlineUserProvider';
export * from './PDFViewer';
export * from './SignatureInput';
export * from './SignaturePad';
export * from './GroupBlockConfigure/GroupBlockConfigure';

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: 170 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,403 @@
/* global window */
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;
}
/*
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,92 @@
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,60 @@
import { h } from './element';
import Icon from './icon';
import DropdownColor from './dropdown_color';
import DropdownLineType from './dropdown_linetype';
import { cssPrefix } from '../config';
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 = () => {};
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,187 @@
import { h } from './element';
import { bindClickoutside, unbindClickoutside } from './event';
import { cssPrefix } from '../config';
import Icon from './icon';
import FormInput from './form_input';
import Dropdown from './dropdown';
// Record: temp not used
// import { xtoast } from './message';
import { tf } from '../locale/locale';
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() {}
}
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 = () => {};
}
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 = () => {},
swapFunc = () => {},
deleteFunc = () => {},
updateFunc = () => {}) {
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 { Element } from './element';
import { cssPrefix } from '../config';
import { t } from '../locale/locale';
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,113 @@
import { h } from './element';
import Icon from './icon';
import { t } from '../locale/locale';
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 = () => {};
}
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,43 @@
import { h } from './element';
import { cssPrefix } from '../config';
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 = () => {};
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,100 @@
import { h } from './element';
import { bindClickoutside, unbindClickoutside } from './event';
import { cssPrefix } from '../config';
import { tf } from '../locale/locale';
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 = () => {};
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,41 @@
import Calendar from './calendar';
import { h } from './element';
import { cssPrefix } from '../config';
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,64 @@
import { Element, h } from './element';
import { bindClickoutside, unbindClickoutside } from './event';
import { cssPrefix } from '../config';
export default class Dropdown extends Element {
constructor(title, width, showArrow, placement, ...children) {
super('div', `${cssPrefix}-dropdown ${placement}`);
this.title = title;
this.change = () => {};
this.headerClick = () => {};
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,25 @@
import Dropdown from './dropdown';
import { h } from './element';
import Icon from './icon';
import { cssPrefix } from '../config';
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';
import Icon from './icon';
import BorderPalette from './border_palette';
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';
import Icon from './icon';
import ColorPalette from './color_palette';
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,16 @@
import Dropdown from './dropdown';
import { h } from './element';
import { baseFonts } from '../core/font';
import { cssPrefix } from '../config';
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,16 @@
import Dropdown from './dropdown';
import { h } from './element';
import { fontSizes } from '../core/font';
import { cssPrefix } from '../config';
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,36 @@
import Dropdown from './dropdown';
import { h } from './element';
import { baseFormats } from '../core/format';
import { cssPrefix } from '../config';
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,17 @@
import Dropdown from './dropdown';
import Icon from './icon';
import { h } from './element';
import { baseFormulas } from '../core/formula';
import { cssPrefix } from '../config';
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,33 @@
import Dropdown from './dropdown';
import { h } from './element';
import Icon from './icon';
import { cssPrefix } from '../config';
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,283 @@
//* global window */
import { h } from './element';
import Suggest from './suggest';
import Datepicker from './datepicker';
import { cssPrefix } from '../config';
// import { mouseMoveUp } from '../event';
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', () => {})
.on('keydown', evt => keydownEventHandler.call(this, evt)),
this.textlineEl = h('div', 'textline'),
this.suggest.el,
this.datepicker.el,
)
.on('mousemove.stop', () => {})
.on('mousedown.stop', () => {});
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 = () => {};
}
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,275 @@
/* global document */
/* global window */
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,153 @@
/* global window */
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,69 @@
import { h } from './element';
import { cssPrefix } from '../config';
import { t } from '../locale/locale';
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,27 @@
import { h } from './element';
import { cssPrefix } from '../config';
export default class FormInput {
constructor(width, hint) {
this.vchange = () => {};
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,39 @@
import { h } from './element';
import Suggest from './suggest';
import { cssPrefix } from '../config';
export default class FormSelect {
constructor(key, items, width, getTitle = it => it, change = () => {}) {
this.key = key;
this.getTitle = getTitle;
this.vchange = () => {};
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 { Element, h } from './element';
import { cssPrefix } from '../config';
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,32 @@
/* global document */
import { h } from './element';
import Icon from './icon';
import { cssPrefix } from '../config';
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,44 @@
/* global document */
/* global window */
import { h } from './element';
import Icon from './icon';
import { cssPrefix } from '../config';
import { bind, unbind } from './event';
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,227 @@
import Modal from './modal';
import FormInput from './form_input';
import FormSelect from './form_select';
import FormField from './form_field';
import Button from './button';
import { t } from '../locale/locale';
import { h } from './element';
import { cssPrefix } from '../config';
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 = () => {};
}
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,203 @@
/* global window document */
import { h } from './element';
import { cssPrefix } from '../config';
import Button from './button';
import { Draw } from '../canvas/draw';
import { renderCell } from './table';
import { t } from '../locale/locale';
// 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.90],
['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,114 @@
/* global window */
import { h } from './element';
import { mouseMoveUp } from './event';
import { cssPrefix } from '../config';
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 = () => {};
}
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,45 @@
import { h } from './element';
import { cssPrefix } from '../config';
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', () => {})
.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,413 @@
import { h } from './element';
import { cssPrefix } from '../config';
import { CellRange } from '../core/cell_range';
const selectorHeightBorderWidth = 2 * 2 - 1;
let startZIndex = 10;
class SelectorElement {
constructor(useHideInput = false, autoFocus = true) {
this.useHideInput = useHideInput;
this.autoFocus = autoFocus;
this.inputChange = () => {};
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 = () => {};
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();
});
}
}

View File

@ -0,0 +1,141 @@
import { h } from './element';
import Button from './button';
import { bindClickoutside, unbindClickoutside } from './event';
import { cssPrefix } from '../config';
import { t } from '../locale/locale';
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,135 @@
import { h } from './element';
import { bindClickoutside, unbindClickoutside } from './event';
import { cssPrefix } from '../config';
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,373 @@
import { stringAt } from '../core/alphabet';
import { getFontSizePxByPt } from '../core/font';
import _cell from '../core/cell';
import { formulam } from '../core/formula';
import { formatm } from '../core/format';
import {
Draw, DrawBox, thinLineWidth, npx,
} from '../canvas/draw';
// gobal var
const cellPaddingWidth = 5;
const tableFixedHeaderCleanStyle = { fillStyle: '#f4f5f8' };
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) {
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, () => {
// 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) {
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);
}, 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);
} 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(75, 137, 255, 0.08)' })
.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 (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 } = this;
draw.save();
// left-top-cell
draw.attr({ fillStyle: '#f4f5f8' })
.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);
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);
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);
}
// 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);
// 5
renderFreezeHighlightLine.call(this, fw, fh, tx, ty);
}
}
clear() {
this.draw.clear();
}
}
export default Table;

View File

@ -0,0 +1,225 @@
/* global window */
import { h } from './element';
import { bind } from './event';
import tooltip from './tooltip';
import DropdownFont from './dropdown_font';
import DropdownFontSize from './dropdown_fontsize';
import DropdownFormat from './dropdown_format';
import DropdownFormula from './dropdown_formula';
import DropdownColor from './dropdown_color';
import DropdownAlign from './dropdown_align';
import DropdownBorder from './dropdown_border';
import Dropdown from './dropdown';
import Icon from './icon';
import { cssPrefix } from '../config';
import { t } from '../locale/locale';
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 = () => {}) {
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 = () => {};
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 DropdownItem from './dropdown_item';
import DropdownAlign from '../dropdown_align';
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,9 @@
import ToggleItem from './toggle_item';
export default class Autofilter extends ToggleItem {
constructor() {
super('autofilter');
}
setState() {}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
import Item from './item';
export default class DropdownItem extends Item {
dropdown() {}
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 DropdownItem from './dropdown_item';
import DropdownColor from '../dropdown_color';
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 DropdownItem from './dropdown_item';
import DropdownFont from '../dropdown_font';
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 DropdownItem from './dropdown_item';
import DropdownFontsize from '../dropdown_fontsize';
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 DropdownItem from './dropdown_item';
import DropdownFormat from '../dropdown_format';
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 DropdownItem from './dropdown_item';
import DropdownFormula from '../dropdown_formula';
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';
export default class Freeze extends ToggleItem {
constructor() {
super('freeze');
}
}

View File

@ -0,0 +1,14 @@
import Item from './item';
import Icon from '../icon';
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,249 @@
/* global window */
import Align from './align';
import Valign from './valign';
import Autofilter from './autofilter';
import Bold from './bold';
import Italic from './italic';
import Strike from './strike';
import Underline from './underline';
import Border from './border';
import Clearformat from './clearformat';
import Paintformat from './paintformat';
import TextColor from './text_color';
import FillColor from './fill_color';
import FontSize from './font_size';
import Font from './font';
import Format from './format';
import Formula from './formula';
import Freeze from './freeze';
import Merge from './merge';
import Redo from './redo';
import Undo from './undo';
import Print from './print';
import Textwrap from './textwrap';
import More from './more';
import Item from './item';
import { h } from '../element';
import { cssPrefix } from '../../config';
import { bind } from '../event';
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 = () => {};
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';
export default class Italic extends ToggleItem {
constructor() {
super('font-italic', 'Ctrl+I');
}
}

View File

@ -0,0 +1,31 @@
import { cssPrefix } from '../../config';
import tooltip from '../tooltip';
import { h } from '../element';
import { t } from '../../locale/locale';
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 = () => {};
}
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() {}
}

View File

@ -0,0 +1,11 @@
import ToggleItem from './toggle_item';
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';
import DropdownItem from './dropdown_item';
import { cssPrefix } from '../../config';
import { h } from '../element';
import Icon from '../icon';
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,9 @@
import ToggleItem from './toggle_item';
export default class Paintformat extends ToggleItem {
constructor() {
super('paintformat');
}
setState() {}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
import DropdownItem from './dropdown_item';
import DropdownColor from '../dropdown_color';
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';
export default class Textwrap extends ToggleItem {
constructor() {
super('textwrap');
}
}

View File

@ -0,0 +1,27 @@
import Item from './item';
import Icon from '../icon';
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';
export default class Underline extends ToggleItem {
constructor() {
super('underline', 'Ctrl+U');
}
}

View File

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

View File

@ -0,0 +1,13 @@
import DropdownItem from './dropdown_item';
import DropdownAlign from '../dropdown_align';
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,31 @@
/* global document */
import { h } from './element';
import { bind } from './event';
import { cssPrefix } from '../config';
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,7 @@
/* global window */
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,91 @@
import './_.prototypes';
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';
// 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,226 @@
import { expr2xy, xy2expr } from './alphabet';
import { numberCalc } from './helper';
// 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,224 @@
import { xy2expr, expr2xy } from './alphabet';
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,84 @@
import helper from './helper';
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,
};

View File

@ -0,0 +1,76 @@
// docs
import './_.prototypes';
/** 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,110 @@
import { tf } from '../locale/locale';
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,98 @@
/**
formula:
key
title
render
*/
/**
* @typedef {object} Formula
* @property {string} key
* @property {function} title
* @property {function} render
*/
import { tf } from '../locale/locale';
import { numberCalc } from './helper';
/** @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,
};

View File

@ -0,0 +1,147 @@
/* eslint-disable no-param-reassign */
function cloneDeep(obj) {
return JSON.parse(JSON.stringify(obj));
}
const mergeDeep = (object = {}, ...sources) => {
sources.forEach((source) => {
Object.keys(source).forEach((key) => {
const v = source[key];
// console.log('k:', key, ', v:', source[key], typeof v, v instanceof Object);
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
object[key] = v;
} else if (typeof v !== 'function' && !Array.isArray(v) && v instanceof Object) {
object[key] = object[key] || {};
mergeDeep(object[key], v);
} else {
object[key] = v;
}
});
});
// console.log('::', object);
return object;
};
function equals(obj1, obj2) {
const keys = Object.keys(obj1);
if (keys.length !== Object.keys(obj2).length) return false;
for (let i = 0; i < keys.length; i += 1) {
const k = keys[i];
const v1 = obj1[k];
const v2 = obj2[k];
if (v2 === undefined) return false;
if (typeof v1 === 'string' || typeof v1 === 'number' || typeof v1 === 'boolean') {
if (v1 !== v2) return false;
} else if (Array.isArray(v1)) {
if (v1.length !== v2.length) return false;
for (let ai = 0; ai < v1.length; ai += 1) {
if (!equals(v1[ai], v2[ai])) return false;
}
} else if (typeof v1 !== 'function' && !Array.isArray(v1) && v1 instanceof Object) {
if (!equals(v1, v2)) return false;
}
}
return true;
}
/*
objOrAry: obejct or Array
cb: (value, index | key) => { return value }
*/
const sum = (objOrAry, cb = value => value) => {
let total = 0;
let size = 0;
Object.keys(objOrAry).forEach((key) => {
total += cb(objOrAry[key], key);
size += 1;
});
return [total, size];
};
function deleteProperty(obj, property) {
const oldv = obj[`${property}`];
delete obj[`${property}`];
return oldv;
}
function rangeReduceIf(min, max, inits, initv, ifv, getv) {
let s = inits;
let v = initv;
let i = min;
for (; i < max; i += 1) {
if (s > ifv) break;
v = getv(i);
s += v;
}
return [i, s - v, v];
}
function rangeSum(min, max, getv) {
let s = 0;
for (let i = min; i < max; i += 1) {
s += getv(i);
}
return s;
}
function rangeEach(min, max, cb) {
for (let i = min; i < max; i += 1) {
cb(i);
}
}
function arrayEquals(a1, a2) {
if (a1.length === a2.length) {
for (let i = 0; i < a1.length; i += 1) {
if (a1[i] !== a2[i]) return false;
}
} else return false;
return true;
}
function digits(a) {
const v = `${a}`;
let ret = 0;
let flag = false;
for (let i = 0; i < v.length; i += 1) {
if (flag === true) ret += 1;
if (v.charAt(i) === '.') flag = true;
}
return ret;
}
export function numberCalc(type, a1, a2) {
if (Number.isNaN(a1) || Number.isNaN(a2)) {
return a1 + type + a2;
}
const al1 = digits(a1);
const al2 = digits(a2);
const num1 = Number(a1);
const num2 = Number(a2);
let ret = 0;
if (type === '-') {
ret = num1 - num2;
} else if (type === '+') {
ret = num1 + num2;
} else if (type === '*') {
ret = num1 * num2;
} else if (type === '/') {
ret = num1 / num2;
if (digits(ret) > 5) return ret.toFixed(2);
return ret;
}
return ret.toFixed(Math.max(al1, al2));
}
export default {
cloneDeep,
merge: (...sources) => mergeDeep({}, ...sources),
equals,
arrayEquals,
sum,
rangeEach,
rangeSum,
rangeReduceIf,
deleteProperty,
numberCalc,
};

View File

@ -0,0 +1,37 @@
// import helper from '../helper';
export default class History {
constructor() {
this.undoItems = [];
this.redoItems = [];
}
add(data) {
this.undoItems.push(JSON.stringify(data));
this.redoItems = [];
}
canUndo() {
return this.undoItems.length > 0;
}
canRedo() {
return this.redoItems.length > 0;
}
undo(currentd, cb) {
const { undoItems, redoItems } = this;
if (this.canUndo()) {
redoItems.push(JSON.stringify(currentd));
cb(JSON.parse(undoItems.pop()));
}
}
redo(currentd, cb) {
const { undoItems, redoItems } = this;
if (this.canRedo()) {
undoItems.push(JSON.stringify(currentd));
cb(JSON.parse(redoItems.pop()));
}
}
}

View File

@ -0,0 +1,108 @@
import { CellRange } from './cell_range';
class Merges {
constructor(d = []) {
this._ = d;
}
forEach(cb) {
this._.forEach(cb);
}
deleteWithin(cr) {
this._ = this._.filter(it => !it.within(cr));
}
getFirstIncludes(ri, ci) {
for (let i = 0; i < this._.length; i += 1) {
const it = this._[i];
if (it.includes(ri, ci)) {
return it;
}
}
return null;
}
filterIntersects(cellRange) {
return new Merges(this._.filter(it => it.intersects(cellRange)));
}
intersects(cellRange) {
for (let i = 0; i < this._.length; i += 1) {
const it = this._[i];
if (it.intersects(cellRange)) {
// console.log('intersects');
return true;
}
}
return false;
}
union(cellRange) {
let cr = cellRange;
this._.forEach((it) => {
if (it.intersects(cr)) {
cr = it.union(cr);
}
});
return cr;
}
add(cr) {
this.deleteWithin(cr);
this._.push(cr);
}
// type: row | column
shift(type, index, n, cbWithin) {
this._.forEach((cellRange) => {
const {
sri, sci, eri, eci,
} = cellRange;
const range = cellRange;
if (type === 'row') {
if (sri >= index) {
range.sri += n;
range.eri += n;
} else if (sri < index && index <= eri) {
range.eri += n;
cbWithin(sri, sci, n, 0);
}
} else if (type === 'column') {
if (sci >= index) {
range.sci += n;
range.eci += n;
} else if (sci < index && index <= eci) {
range.eci += n;
cbWithin(sri, sci, 0, n);
}
}
});
}
move(cellRange, rn, cn) {
this._.forEach((it1) => {
const it = it1;
if (it.within(cellRange)) {
it.eri += rn;
it.sri += rn;
it.sci += cn;
it.eci += cn;
}
});
}
setData(merges) {
this._ = merges.map(merge => CellRange.valueOf(merge));
return this;
}
getData() {
return this._.map(merge => merge.toString());
}
}
export default {};
export {
Merges,
};

View File

@ -0,0 +1,356 @@
import helper from './helper';
import { expr2expr } from './alphabet';
class Rows {
constructor({ len, height }) {
this._ = {};
this.len = len;
// default row height
this.height = height;
}
getHeight(ri) {
if (this.isHide(ri)) return 0;
const row = this.get(ri);
if (row && row.height) {
return row.height;
}
return this.height;
}
setHeight(ri, v) {
const row = this.getOrNew(ri);
row.height = v;
}
unhide(idx) {
let index = idx;
while (index > 0) {
index -= 1;
if (this.isHide(index)) {
this.setHide(index, false);
} else break;
}
}
isHide(ri) {
const row = this.get(ri);
return row && row.hide;
}
setHide(ri, v) {
const row = this.getOrNew(ri);
if (v === true) row.hide = true;
else delete row.hide;
}
setStyle(ri, style) {
const row = this.getOrNew(ri);
row.style = style;
}
sumHeight(min, max, exceptSet) {
return helper.rangeSum(min, max, (i) => {
if (exceptSet && exceptSet.has(i)) return 0;
return this.getHeight(i);
});
}
totalHeight() {
return this.sumHeight(0, this.len);
}
get(ri) {
return this._[ri];
}
getOrNew(ri) {
this._[ri] = this._[ri] || { cells: {} };
return this._[ri];
}
getCell(ri, ci) {
const row = this.get(ri);
if (row !== undefined && row.cells !== undefined && row.cells[ci] !== undefined) {
return row.cells[ci];
}
return null;
}
getCellMerge(ri, ci) {
const cell = this.getCell(ri, ci);
if (cell && cell.merge) return cell.merge;
return [0, 0];
}
getCellOrNew(ri, ci) {
const row = this.getOrNew(ri);
row.cells[ci] = row.cells[ci] || {};
return row.cells[ci];
}
// what: all | text | format
setCell(ri, ci, cell, what = 'all') {
const row = this.getOrNew(ri);
if (what === 'all') {
row.cells[ci] = cell;
} else if (what === 'text') {
row.cells[ci] = row.cells[ci] || {};
row.cells[ci].text = cell.text;
} else if (what === 'format') {
row.cells[ci] = row.cells[ci] || {};
row.cells[ci].style = cell.style;
if (cell.merge) row.cells[ci].merge = cell.merge;
}
}
setCellText(ri, ci, text) {
const cell = this.getCellOrNew(ri, ci);
if (cell.editable !== false) cell.text = text;
}
// what: all | format | text
copyPaste(srcCellRange, dstCellRange, what, autofill = false, cb = () => {}) {
const {
sri, sci, eri, eci,
} = srcCellRange;
const dsri = dstCellRange.sri;
const dsci = dstCellRange.sci;
const deri = dstCellRange.eri;
const deci = dstCellRange.eci;
const [rn, cn] = srcCellRange.size();
const [drn, dcn] = dstCellRange.size();
// console.log(srcIndexes, dstIndexes);
let isAdd = true;
let dn = 0;
if (deri < sri || deci < sci) {
isAdd = false;
if (deri < sri) dn = drn;
else dn = dcn;
}
for (let i = sri; i <= eri; i += 1) {
if (this._[i]) {
for (let j = sci; j <= eci; j += 1) {
if (this._[i].cells && this._[i].cells[j]) {
for (let ii = dsri; ii <= deri; ii += rn) {
for (let jj = dsci; jj <= deci; jj += cn) {
const nri = ii + (i - sri);
const nci = jj + (j - sci);
const ncell = helper.cloneDeep(this._[i].cells[j]);
// ncell.text
if (autofill && ncell && ncell.text && ncell.text.length > 0) {
const { text } = ncell;
let n = (jj - dsci) + (ii - dsri) + 2;
if (!isAdd) {
n -= dn + 1;
}
if (text[0] === '=') {
ncell.text = text.replace(/[a-zA-Z]{1,3}\d+/g, (word) => {
let [xn, yn] = [0, 0];
if (sri === dsri) {
xn = n - 1;
// if (isAdd) xn -= 1;
} else {
yn = n - 1;
}
if (/^\d+$/.test(word)) return word;
return expr2expr(word, xn, yn);
});
} else if ((rn <= 1 && cn > 1 && (dsri > eri || deri < sri))
|| (cn <= 1 && rn > 1 && (dsci > eci || deci < sci))
|| (rn <= 1 && cn <= 1)) {
const result = /[\\.\d]+$/.exec(text);
// console.log('result:', result);
if (result !== null) {
const index = Number(result[0]) + n - 1;
ncell.text = text.substring(0, result.index) + index;
}
}
}
this.setCell(nri, nci, ncell, what);
cb(nri, nci, ncell);
}
}
}
}
}
}
}
cutPaste(srcCellRange, dstCellRange) {
const ncellmm = {};
this.each((ri) => {
this.eachCells(ri, (ci) => {
let nri = parseInt(ri, 10);
let nci = parseInt(ci, 10);
if (srcCellRange.includes(ri, ci)) {
nri = dstCellRange.sri + (nri - srcCellRange.sri);
nci = dstCellRange.sci + (nci - srcCellRange.sci);
}
ncellmm[nri] = ncellmm[nri] || { cells: {} };
ncellmm[nri].cells[nci] = this._[ri].cells[ci];
});
});
this._ = ncellmm;
}
// src: Array<Array<String>>
paste(src, dstCellRange) {
if (src.length <= 0) return;
const { sri, sci } = dstCellRange;
src.forEach((row, i) => {
const ri = sri + i;
row.forEach((cell, j) => {
const ci = sci + j;
this.setCellText(ri, ci, cell);
});
});
}
insert(sri, n = 1) {
const ndata = {};
this.each((ri, row) => {
let nri = parseInt(ri, 10);
if (nri >= sri) {
nri += n;
this.eachCells(ri, (ci, cell) => {
if (cell.text && cell.text[0] === '=') {
cell.text = cell.text.replace(/[a-zA-Z]{1,3}\d+/g, word => expr2expr(word, 0, n, (x, y) => y >= sri));
}
});
}
ndata[nri] = row;
});
this._ = ndata;
this.len += n;
}
delete(sri, eri) {
const n = eri - sri + 1;
const ndata = {};
this.each((ri, row) => {
const nri = parseInt(ri, 10);
if (nri < sri) {
ndata[nri] = row;
} else if (ri > eri) {
ndata[nri - n] = row;
this.eachCells(ri, (ci, cell) => {
if (cell.text && cell.text[0] === '=') {
cell.text = cell.text.replace(/[a-zA-Z]{1,3}\d+/g, word => expr2expr(word, 0, -n, (x, y) => y > eri));
}
});
}
});
this._ = ndata;
this.len -= n;
}
insertColumn(sci, n = 1) {
this.each((ri, row) => {
const rndata = {};
this.eachCells(ri, (ci, cell) => {
let nci = parseInt(ci, 10);
if (nci >= sci) {
nci += n;
if (cell.text && cell.text[0] === '=') {
cell.text = cell.text.replace(/[a-zA-Z]{1,3}\d+/g, word => expr2expr(word, n, 0, x => x >= sci));
}
}
rndata[nci] = cell;
});
row.cells = rndata;
});
}
deleteColumn(sci, eci) {
const n = eci - sci + 1;
this.each((ri, row) => {
const rndata = {};
this.eachCells(ri, (ci, cell) => {
const nci = parseInt(ci, 10);
if (nci < sci) {
rndata[nci] = cell;
} else if (nci > eci) {
rndata[nci - n] = cell;
if (cell.text && cell.text[0] === '=') {
cell.text = cell.text.replace(/[a-zA-Z]{1,3}\d+/g, word => expr2expr(word, -n, 0, x => x > eci));
}
}
});
row.cells = rndata;
});
}
// what: all | text | format | merge
deleteCells(cellRange, what = 'all') {
cellRange.each((i, j) => {
this.deleteCell(i, j, what);
});
}
// what: all | text | format | merge
deleteCell(ri, ci, what = 'all') {
const row = this.get(ri);
if (row !== null) {
const cell = this.getCell(ri, ci);
if (cell !== null && cell.editable !== false) {
if (what === 'all') {
delete row.cells[ci];
} else if (what === 'text') {
if (cell.text) delete cell.text;
if (cell.value) delete cell.value;
} else if (what === 'format') {
if (cell.style !== undefined) delete cell.style;
if (cell.merge) delete cell.merge;
} else if (what === 'merge') {
if (cell.merge) delete cell.merge;
}
}
}
}
maxCell() {
const keys = Object.keys(this._);
const ri = keys[keys.length - 1];
const col = this._[ri];
if (col) {
const { cells } = col;
const ks = Object.keys(cells);
const ci = ks[ks.length - 1];
return [parseInt(ri, 10), parseInt(ci, 10)];
}
return [0, 0];
}
each(cb) {
Object.entries(this._).forEach(([ri, row]) => {
cb(ri, row);
});
}
eachCells(ri, cb) {
if (this._[ri] && this._[ri].cells) {
Object.entries(this._[ri].cells).forEach(([ci, cell]) => {
cb(ci, cell);
});
}
}
setData(d) {
if (d.len) {
this.len = d.len;
delete d.len;
}
this._ = d;
}
getData() {
const { len } = this;
return Object.assign({ len }, this._);
}
}
export default {};
export {
Rows,
};

View File

@ -0,0 +1,8 @@
export default class Scroll {
constructor() {
this.x = 0; // left
this.y = 0; // top
this.ri = 0; // cell row-index
this.ci = 0; // cell col-index
}
}

View File

@ -0,0 +1,22 @@
import { CellRange } from './cell_range';
export default class Selector {
constructor() {
this.range = new CellRange(0, 0, 0, 0);
this.ri = 0;
this.ci = 0;
}
multiple() {
return this.range.multiple();
}
setIndexes(ri, ci) {
this.ri = ri;
this.ci = ci;
}
size() {
return this.range.size();
}
}

View File

@ -0,0 +1,139 @@
import Validator from './validator';
import { CellRange } from './cell_range';
class Validation {
constructor(mode, refs, validator) {
this.refs = refs;
this.mode = mode; // cell
this.validator = validator;
}
includes(ri, ci) {
const { refs } = this;
for (let i = 0; i < refs.length; i += 1) {
const cr = CellRange.valueOf(refs[i]);
if (cr.includes(ri, ci)) return true;
}
return false;
}
addRef(ref) {
this.remove(CellRange.valueOf(ref));
this.refs.push(ref);
}
remove(cellRange) {
const nrefs = [];
this.refs.forEach((it) => {
const cr = CellRange.valueOf(it);
if (cr.intersects(cellRange)) {
const crs = cr.difference(cellRange);
crs.forEach(it1 => nrefs.push(it1.toString()));
} else {
nrefs.push(it);
}
});
this.refs = nrefs;
}
getData() {
const { refs, mode, validator } = this;
const {
type, required, operator, value,
} = validator;
return {
refs, mode, type, required, operator, value,
};
}
static valueOf({
refs, mode, type, required, operator, value,
}) {
return new Validation(mode, refs, new Validator(type, required, value, operator));
}
}
class Validations {
constructor() {
this._ = [];
// ri_ci: errMessage
this.errors = new Map();
}
getError(ri, ci) {
return this.errors.get(`${ri}_${ci}`);
}
validate(ri, ci, text) {
const v = this.get(ri, ci);
const key = `${ri}_${ci}`;
const { errors } = this;
if (v !== null) {
const [flag, message] = v.validator.validate(text);
if (!flag) {
errors.set(key, message);
} else {
errors.delete(key);
}
} else {
errors.delete(key);
}
return true;
}
// type: date|number|phone|email|list
// validator: { required, value, operator }
add(mode, ref, {
type, required, value, operator,
}) {
const validator = new Validator(
type, required, value, operator,
);
const v = this.getByValidator(validator);
if (v !== null) {
v.addRef(ref);
} else {
this._.push(new Validation(mode, [ref], validator));
}
}
getByValidator(validator) {
for (let i = 0; i < this._.length; i += 1) {
const v = this._[i];
if (v.validator.equals(validator)) {
return v;
}
}
return null;
}
get(ri, ci) {
for (let i = 0; i < this._.length; i += 1) {
const v = this._[i];
if (v.includes(ri, ci)) return v;
}
return null;
}
remove(cellRange) {
this.each((it) => {
it.remove(cellRange);
});
}
each(cb) {
this._.forEach(it => cb(it));
}
getData() {
return this._.filter(it => it.refs.length > 0).map(it => it.getData());
}
setData(d) {
this._ = d.map(it => Validation.valueOf(it));
}
}
export default {};
export {
Validations,
};

View File

@ -0,0 +1,136 @@
import { t } from '../locale/locale';
import helper from './helper';
const rules = {
phone: /^[1-9]\d{10}$/,
email: /w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*/,
};
function returnMessage(flag, key, ...arg) {
let message = '';
if (!flag) {
message = t(`validation.${key}`, ...arg);
}
return [flag, message];
}
export default class Validator {
// operator: b|nb|eq|neq|lt|lte|gt|gte
// type: date|number|list|phone|email
constructor(type, required, value, operator) {
this.required = required;
this.value = value;
this.type = type;
this.operator = operator;
this.message = '';
}
parseValue(v) {
const { type } = this;
if (type === 'date') {
return new Date(v);
}
if (type === 'number') {
return Number(v);
}
return v;
}
equals(other) {
let flag = this.type === other.type
&& this.required === other.required
&& this.operator === other.operator;
if (flag) {
if (Array.isArray(this.value)) {
flag = helper.arrayEquals(this.value, other.value);
} else {
flag = this.value === other.value;
}
}
return flag;
}
values() {
return this.value.split(',');
}
validate(v) {
const {
required, operator, value, type,
} = this;
if (required && /^\s*$/.test(v)) {
return returnMessage(false, 'required');
}
if (/^\s*$/.test(v)) return [true];
if (rules[type] && !rules[type].test(v)) {
return returnMessage(false, 'notMatch');
}
if (type === 'list') {
return returnMessage(this.values().includes(v), 'notIn');
}
if (operator) {
const v1 = this.parseValue(v);
if (operator === 'be') {
const [min, max] = value;
return returnMessage(
v1 >= this.parseValue(min) && v1 <= this.parseValue(max),
'between',
min,
max,
);
}
if (operator === 'nbe') {
const [min, max] = value;
return returnMessage(
v1 < this.parseValue(min) || v1 > this.parseValue(max),
'notBetween',
min,
max,
);
}
if (operator === 'eq') {
return returnMessage(
v1 === this.parseValue(value),
'equal',
value,
);
}
if (operator === 'neq') {
return returnMessage(
v1 !== this.parseValue(value),
'notEqual',
value,
);
}
if (operator === 'lt') {
return returnMessage(
v1 < this.parseValue(value),
'lessThan',
value,
);
}
if (operator === 'lte') {
return returnMessage(
v1 <= this.parseValue(value),
'lessThanEqual',
value,
);
}
if (operator === 'gt') {
return returnMessage(
v1 > this.parseValue(value),
'greaterThan',
value,
);
}
if (operator === 'gte') {
return returnMessage(
v1 >= this.parseValue(value),
'greaterThanEqual',
value,
);
}
}
return [true];
}
}

View File

@ -0,0 +1,597 @@
export const demoData = [
{
name: 'sheet2',
freeze: 'A1',
styles: [
{
border: {
bottom: ['thin', '#000'],
top: ['thin', '#000'],
left: ['thin', '#000'],
right: ['thin', '#000'],
},
},
],
merges: ['B23:B34', 'B18:B22', 'B7:B17', 'B1:B6', 'A1:A34'],
rows: {
'0': {
cells: {
'0': {
style: 0,
merge: [33, 0],
text: '临平厂房',
},
'1': {
text: '一、1#厂房内支撑脚手架',
style: 0,
merge: [5, 0],
},
'2': {
text: '1层',
style: 0,
},
'3': {
text: '8 ',
style: 0,
},
'4': {
text: '6687 \r',
style: 0,
},
},
},
'1': {
cells: {
'2': {
text: '2层',
style: 0,
},
'3': {
text: '5 ',
style: 0,
},
'4': {
text: '6848 \r',
style: 0,
},
},
},
'2': {
cells: {
'2': {
text: '3层',
style: 0,
},
'3': {
text: '5 ',
style: 0,
},
'4': {
text: '6848 \r',
style: 0,
},
},
},
'3': {
cells: {
'2': {
text: '4层',
style: 0,
},
'3': {
text: '5 ',
style: 0,
},
'4': {
text: '6338 \r',
style: 0,
},
},
},
'4': {
cells: {
'2': {
text: '5层',
style: 0,
},
'3': {
text: '5 ',
style: 0,
},
'4': {
text: '6338 \r',
style: 0,
},
},
},
'5': {
cells: {
'2': {
text: '6层',
style: 0,
},
'3': {
text: '5 ',
style: 0,
},
'4': {
text: '6338 \r',
style: 0,
},
},
},
'6': {
cells: {
'1': {
text: '二、2#厂房内支撑脚手架',
style: 0,
merge: [10, 0],
},
'2': {
text: '1层',
style: 0,
},
'3': {
text: '7.90 ',
style: 0,
},
'4': {
text: '4149.04 \r',
style: 0,
},
},
},
'7': {
cells: {
'2': {
text: '2层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '4317.28 \r',
style: 0,
},
},
},
'8': {
cells: {
'2': {
text: '3层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '4317.28 \r',
style: 0,
},
},
},
'9': {
cells: {
'2': {
text: '4层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '4317.28 \r',
style: 0,
},
},
},
'10': {
cells: {
'2': {
text: '5层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '4317.28 \r',
style: 0,
},
},
},
'11': {
cells: {
'2': {
text: '6层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '4317.28 \r',
style: 0,
},
},
},
'12': {
cells: {
'2': {
text: '7层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '4317.28 \r',
style: 0,
},
},
},
'13': {
cells: {
'2': {
text: '8层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '4317.28 \r',
style: 0,
},
},
},
'14': {
cells: {
'2': {
text: '9层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '4317.28 \r',
style: 0,
},
},
},
'15': {
cells: {
'2': {
text: '10层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '4317.28 \r',
style: 0,
},
},
},
'16': {
cells: {
'2': {
text: '屋顶层',
style: 0,
},
'3': {
text: '2.85 ',
style: 0,
},
'4': {
text: '4260.88 \r',
style: 0,
},
},
},
'17': {
cells: {
'1': {
text: '三、3#厂房内支撑脚手架',
style: 0,
merge: [4, 0],
},
'2': {
text: '1层',
style: 0,
},
'3': {
text: '6.00 ',
style: 0,
},
'4': {
text: '3964.34 \r',
style: 0,
},
},
},
'18': {
cells: {
'2': {
text: '2层',
style: 0,
},
'3': {
text: '5.40 ',
style: 0,
},
'4': {
text: '4908.63 \r',
style: 0,
},
},
},
'19': {
cells: {
'2': {
text: '3层',
style: 0,
},
'3': {
text: '6.00 ',
style: 0,
},
'4': {
text: '4722.95 \r',
style: 0,
},
},
},
'20': {
cells: {
'2': {
text: '4层',
style: 0,
},
'3': {
text: '6.00 ',
style: 0,
},
'4': {
text: '4729.63 \r',
style: 0,
},
},
},
'21': {
cells: {
'2': {
text: '屋顶层',
style: 0,
},
'3': {
text: '3.50 ',
style: 0,
},
'4': {
text: '250.00 \r',
style: 0,
},
},
},
'22': {
cells: {
'1': {
text: '四、中建技智选酒店内支撑脚手架',
style: 0,
merge: [11, 0],
},
'2': {
text: '1层',
style: 0,
},
'3': {
text: '7.90 ',
style: 0,
},
'4': {
text: '1626.50 \r',
style: 0,
},
},
},
'23': {
cells: {
'2': {
text: '2层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '1598.40 \r',
style: 0,
},
},
},
'24': {
cells: {
'2': {
text: '3层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '1562.40 \r',
style: 0,
},
},
},
'25': {
cells: {
'2': {
text: '4层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '1562.40 \r',
style: 0,
},
},
},
'26': {
cells: {
'2': {
text: '5层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '1562.40 \r',
style: 0,
},
},
},
'27': {
cells: {
'2': {
text: '6层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '1562.40 \r',
style: 0,
},
},
},
'28': {
cells: {
'2': {
text: '7层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '1562.40 \r',
style: 0,
},
},
},
'29': {
cells: {
'2': {
text: '8层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '1562.40 \r',
style: 0,
},
},
},
'30': {
cells: {
'2': {
text: '9层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '1562.40 \r',
style: 0,
},
},
},
'31': {
cells: {
'2': {
text: '10层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '1562.40 \r',
style: 0,
},
},
},
'32': {
cells: {
'2': {
text: '11层',
style: 0,
},
'3': {
text: '4.50 ',
style: 0,
},
'4': {
text: '1252.40 \r',
style: 0,
},
},
},
'33': {
cells: {
'2': {
text: '屋顶层',
style: 0,
},
'3': {
text: '3.50 ',
style: 0,
},
'4': {
text: '250.00 \r',
style: 0,
},
},
},
len: 40,
},
cols: {
'1': {
width: 200,
},
len: 10,
},
validations: [],
autofilter: {},
},
];

View File

@ -0,0 +1,201 @@
export interface ExtendToolbarOption {
tip?: string;
el?: HTMLElement;
icon?: string;
onClick?: (data: object, sheet: object) => void
}
export interface Options {
mode?: 'edit' | 'read';
showToolbar?: boolean;
showGrid?: boolean;
showContextmenu?: boolean;
showBottomBar?: boolean;
extendToolbar?: {
left?: ExtendToolbarOption[],
right?: ExtendToolbarOption[],
};
autoFocus?: boolean;
view?: {
height: () => number;
width: () => number;
};
row?: {
len: number;
height: number;
};
col?: {
len: number;
width: number;
indexWidth: number;
minWidth: number;
};
style?: {
bgcolor: string;
align: 'left' | 'center' | 'right';
valign: 'top' | 'middle' | 'bottom';
textwrap: boolean;
strike: boolean;
underline: boolean;
color: string;
font: {
name: 'Helvetica';
size: number;
bold: boolean;
italic: false;
};
};
}
export type CELL_SELECTED = 'cell-selected';
export type CELLS_SELECTED = 'cells-selected';
export type CELL_EDITED = 'cell-edited';
export type CellMerge = [number, number];
export interface SpreadsheetEventHandler {
(
envt: CELL_SELECTED,
callback: (cell: Cell, rowIndex: number, colIndex: number) => void
): void;
(
envt: CELLS_SELECTED,
callback: (
cell: Cell,
parameters: { sri: number; sci: number; eri: number; eci: number }
) => void
): void;
(
evnt: CELL_EDITED,
callback: (text: string, rowIndex: number, colIndex: number) => void
): void;
}
export interface ColProperties {
width?: number;
}
/**
* Data for representing a cell
*/
export interface CellData {
text: string;
style?: number;
merge?: CellMerge;
}
/**
* Data for representing a row
*/
export interface RowData {
cells: {
[key: number]: CellData;
}
}
/**
* Data for representing a sheet
*/
export interface SheetData {
name?: string;
freeze?: string;
styles?: CellStyle[];
merges?: string[];
cols?: {
len?: number;
[key: number]: ColProperties;
};
rows?: {
[key: number]: RowData
};
}
/**
* Data for representing a spreadsheet
*/
export interface SpreadsheetData {
[index: number]: SheetData;
}
export interface CellStyle {
align?: 'left' | 'center' | 'right';
valign?: 'top' | 'middle' | 'bottom';
font?: {
bold?: boolean;
}
bgcolor?: string;
textwrap?: boolean;
color?: string;
border?: {
top?: string[];
right?: string[];
bottom?: string[];
left?: string[];
};
}
export interface Editor {}
export interface Element {}
export interface Row {}
export interface Table {}
export interface Cell {}
export interface Sheet {}
export default class Spreadsheet {
constructor(container: string | HTMLElement, opts?: Options);
on: SpreadsheetEventHandler;
/**
* retrieve cell
* @param rowIndex {number} row index
* @param colIndex {number} column index
* @param sheetIndex {number} sheet iindex
*/
cell(rowIndex: number, colIndex: number, sheetIndex: number): Cell;
/**
* retrieve cell style
* @param rowIndex
* @param colIndex
* @param sheetIndex
*/
cellStyle(
rowIndex: number,
colIndex: number,
sheetIndex: number
): CellStyle;
/**
* get/set cell text
* @param rowIndex
* @param colIndex
* @param text
* @param sheetIndex
*/
cellText(
rowIndex: number,
colIndex: number,
text: string,
sheetIndex?: number
): this;
/**
* remove current sheet
*/
deleteSheet(): void;
/**s
* load data
* @param json
*/
loadData(json: Record<string, any>): this;
/**
* get data
*/
getData(): Record<string, any>;
/**
* bind handler to change event, including data change and user actions
* @param callback
*/
change(callback: (json: Record<string, any>) => void): this;
/**
* set locale
* @param lang
* @param message
*/
static locale(lang: string, message: object): void;
}

View File

@ -0,0 +1,131 @@
/* global window, document */
import { h } from './component/element';
import DataProxy from './core/data_proxy';
import Sheet from './component/sheet';
import Bottombar from './component/bottombar';
import { cssPrefix } from './config';
import { locale } from './locale/locale';
import './index.less';
class Spreadsheet {
constructor(selectors, options = {}) {
let targetEl = selectors;
this.options = { showBottomBar: true, ...options };
this.sheetIndex = 1;
this.datas = [];
if (typeof selectors === 'string') {
targetEl = document.querySelector(selectors);
}
this.bottombar = this.options.showBottomBar ? new Bottombar(() => {
if (this.options.mode === 'read') return;
const d = this.addSheet();
this.sheet.resetData(d);
}, (index) => {
const d = this.datas[index];
this.sheet.resetData(d);
}, () => {
this.deleteSheet();
}, (index, value) => {
this.datas[index].name = value;
this.sheet.trigger('change');
}) : null;
this.data = this.addSheet();
const rootEl = h('div', `${cssPrefix}`)
.on('contextmenu', evt => evt.preventDefault());
// create canvas element
targetEl.appendChild(rootEl.el);
this.sheet = new Sheet(rootEl, this.data);
if (this.bottombar !== null) {
rootEl.child(this.bottombar.el);
}
}
addSheet(name, active = true) {
const n = name || `sheet${this.sheetIndex}`;
const d = new DataProxy(n, this.options);
d.change = (...args) => {
this.sheet.trigger('change', ...args);
};
this.datas.push(d);
// console.log('d:', n, d, this.datas);
if (this.bottombar !== null) {
this.bottombar.addItem(n, active, this.options);
}
this.sheetIndex += 1;
return d;
}
deleteSheet() {
if (this.bottombar === null) return;
const [oldIndex, nindex] = this.bottombar.deleteItem();
if (oldIndex >= 0) {
this.datas.splice(oldIndex, 1);
if (nindex >= 0) this.sheet.resetData(this.datas[nindex]);
this.sheet.trigger('change');
}
}
loadData(data) {
const ds = Array.isArray(data) ? data : [data];
if (this.bottombar !== null) {
this.bottombar.clear();
}
this.datas = [];
if (ds.length > 0) {
for (let i = 0; i < ds.length; i += 1) {
const it = ds[i];
const nd = this.addSheet(it.name, i === 0);
nd.setData(it);
if (i === 0) {
this.sheet.resetData(nd);
}
}
}
return this;
}
getData() {
return this.datas.map(it => it.getData());
}
cellText(ri, ci, text, sheetIndex = 0) {
this.datas[sheetIndex].setCellText(ri, ci, text, 'finished');
return this;
}
cell(ri, ci, sheetIndex = 0) {
return this.datas[sheetIndex].getCell(ri, ci);
}
cellStyle(ri, ci, sheetIndex = 0) {
return this.datas[sheetIndex].getCellStyle(ri, ci);
}
reRender() {
this.sheet.table.render();
return this;
}
on(eventName, func) {
this.sheet.on(eventName, func);
return this;
}
validate() {
const { validations } = this.data;
return validations.errors.size <= 0;
}
change(cb) {
this.sheet.on('change', cb);
return this;
}
static locale(lang, message) {
locale(lang, message);
}
}
export default Spreadsheet;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
export default {
toolbar: {
undo: 'Rückgängig machen',
redo: 'Wiederherstellen',
paintformat: 'Format kopieren/einfügen',
clearformat: 'Format löschen',
format: 'Format',
font: 'Schriftart',
fontSize: 'Schriftgrad',
fontBold: 'Fett',
fontItalic: 'Kursiv',
underline: 'Betonen',
strike: 'Streichen',
textColor: 'Text Farbe',
fillColor: 'Füllung Farbe',
border: 'Umrandung',
merge: 'Zellen verbinden',
align: 'Waagrechte Ausrichtung',
valign: 'Vertikale uitlijning',
textwrap: 'Textumbruch',
freeze: 'Zelle sperren',
formula: 'Funktionen',
more: 'Mehr',
},
contextmenu: {
copy: 'Kopieren',
cut: 'Ausschneiden',
paste: 'Einfügen',
pasteValue: 'Nur Werte einfügen',
pasteFormat: 'Nur Format einfügen',
insertRow: 'Zeile einfügen',
insertColumn: 'Spalte einfügen',
deleteRow: 'Zeile löschen',
deleteColumn: 'Spalte löschen',
deleteCell: 'Zelle löschen',
deleteCellText: 'Zellentext löschen',
},
format: {
normal: 'Regulär',
text: 'Text',
number: 'Nummer',
percent: 'Prozent',
rmb: 'RMB',
usd: 'USD',
date: 'Datum',
time: 'Termin',
datetime: 'Datum Termin',
duration: 'Dauer',
},
formula: {
sum: 'Summe',
average: 'Durchschnittliche',
max: 'Max',
min: 'Min',
concat: 'Concat',
},
};

View File

@ -0,0 +1,136 @@
export default {
toolbar: {
undo: 'Undo',
redo: 'Redo',
print: 'Print',
paintformat: 'Paint format',
clearformat: 'Clear format',
format: 'Format',
fontName: 'Font',
fontSize: 'Font size',
fontBold: 'Font bold',
fontItalic: 'Font italic',
underline: 'Underline',
strike: 'Strike',
color: 'Text color',
bgcolor: 'Fill color',
border: 'Borders',
merge: 'Merge cells',
align: 'Horizontal align',
valign: 'Vertical align',
textwrap: 'Text wrapping',
freeze: 'Freeze cell',
autofilter: 'Filter',
formula: 'Functions',
more: 'More',
},
contextmenu: {
copy: 'Copy',
cut: 'Cut',
paste: 'Paste',
pasteValue: 'Paste values only',
pasteFormat: 'Paste format only',
hide: 'Hide',
insertRow: 'Insert row',
insertColumn: 'Insert column',
deleteSheet: 'Delete',
deleteRow: 'Delete row',
deleteColumn: 'Delete column',
deleteCell: 'Delete cell',
deleteCellText: 'Delete cell text',
validation: 'Data validations',
cellprintable: 'Enable export',
cellnonprintable: 'Disable export',
celleditable: 'Enable editing',
cellnoneditable: 'Disable editing',
},
print: {
size: 'Paper size',
orientation: 'Page orientation',
orientations: ['Landscape', 'Portrait'],
},
format: {
normal: 'Normal',
text: 'Plain Text',
number: 'Number',
percent: 'Percent',
rmb: 'RMB',
usd: 'USD',
eur: 'EUR',
date: 'Date',
time: 'Time',
datetime: 'Date time',
duration: 'Duration',
},
formula: {
sum: 'Sum',
average: 'Average',
max: 'Max',
min: 'Min',
_if: 'IF',
and: 'AND',
or: 'OR',
concat: 'Concat',
},
validation: {
required: 'it must be required',
notMatch: 'it not match its validation rule',
between: 'it is between {} and {}',
notBetween: 'it is not between {} and {}',
notIn: 'it is not in list',
equal: 'it equal to {}',
notEqual: 'it not equal to {}',
lessThan: 'it less than {}',
lessThanEqual: 'it less than or equal to {}',
greaterThan: 'it greater than {}',
greaterThanEqual: 'it greater than or equal to {}',
},
error: {
pasteForMergedCell: 'Unable to do this for merged cells',
},
calendar: {
weeks: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
},
button: {
next: 'Next',
cancel: 'Cancel',
remove: 'Remove',
save: 'Save',
ok: 'OK',
},
sort: {
desc: 'Sort Z -> A',
asc: 'Sort A -> Z',
},
filter: {
empty: 'empty',
},
dataValidation: {
mode: 'Mode',
range: 'Cell Range',
criteria: 'Criteria',
modeType: {
cell: 'Cell',
column: 'Colun',
row: 'Row',
},
type: {
list: 'List',
number: 'Number',
date: 'Date',
phone: 'Phone',
email: 'Email',
},
operator: {
be: 'between',
nbe: 'not betwwen',
lt: 'less than',
lte: 'less than or equal to',
gt: 'greater than',
gte: 'greater than or equal to',
eq: 'equal to',
neq: 'not equal to',
},
},
};

View File

@ -0,0 +1,80 @@
/* global window */
import en from './en';
// Defines the fallback language as English
let $languages = ['en'];
const $messages = {
en,
};
function translate(key, messages) {
if (messages) {
// Return the translation from the first language in the languages array
// that has a value for the provided key.
for (const lang of $languages) {
if (!messages[lang]) break;
let message = messages[lang];
// Splits the key at '.' except where escaped as '\.'
const keys = key.match(/(?:\\.|[^.])+/g);
for (let i = 0; i < keys.length; i += 1) {
const property = keys[i];
const value = message[property];
// If value doesn't exist, try next language
if (!value) break;
if (i === keys.length - 1) return value;
// Move down to the next level of the messages object
message = value;
}
}
}
return undefined;
}
function t(key) {
let v = translate(key, $messages);
if (!v && window && window.x_spreadsheet && window.x_spreadsheet.$messages) {
v = translate(key, window.x_spreadsheet.$messages);
}
return v || '';
}
function tf(key) {
return () => t(key);
}
// If clearLangList is set to false, lang will be added to the front of the
// languages array. The languages in the language array are searched in order
// to find a translation. This allows the use of other languages as a fallback
// if lang is missing some keys. The language array is preloaded with English.
// To set the languages array to only include lang, set clearLangList to true.
function locale(lang, message, clearLangList = false) {
if (clearLangList) {
$languages = [lang];
} else {
// Append to front of array.
// Translation method will use the first language in the list that has a
// matching key.
$languages.unshift(lang);
}
if (message) {
$messages[lang] = message;
}
}
export default {
t,
};
export {
locale,
t,
tf,
};

View File

@ -0,0 +1,57 @@
export default {
toolbar: {
undo: 'Ongedaan maken',
redo: 'Opnieuw uitvoeren',
paintformat: 'Opmaak kopiëren/plakken',
clearformat: 'Opmaak wissen',
format: 'Opmaak',
font: 'Lettertype',
fontSize: 'Tekengrootte',
fontBold: 'Vet',
fontItalic: 'Cursief',
underline: 'Onderstrepen',
strike: 'Doorstrepen',
textColor: 'Tekstkleur',
fillColor: 'Opvulkleur',
border: 'Randen',
merge: 'Cellen samenvoegen',
align: 'Horizontale uitlijning',
valign: 'Verticale uitlijning',
textwrap: 'Terugloop',
freeze: 'Cel bevriezen',
formula: 'Functies',
more: 'Meer',
},
contextmenu: {
copy: 'Kopiëren',
cut: 'Knippen',
paste: 'Plakken',
pasteValue: 'Alleen waarden plakken',
pasteFormat: 'Alleen opmaak plakken',
insertRow: 'Rij invoegen',
insertColumn: 'Kolom invoegen',
deleteRow: 'Rij verwijderen',
deleteColumn: 'Kolom verwijderen',
deleteCell: 'Cel verwijderen',
deleteCellText: 'Celtekst verwijderen',
},
format: {
normal: 'Standaard',
text: 'Tekst',
number: 'Nummer',
percent: 'Percentage',
rmb: 'RMB',
usd: 'USD',
date: 'Datum',
time: 'Tijdstip',
datetime: 'Datum tijd',
duration: 'Duratie',
},
formula: {
sum: 'Som',
average: 'Gemiddelde',
max: 'Max',
min: 'Min',
concat: 'Concat',
},
};

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