@pdfme/ui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/__mocks__/assetsTransformer.js +7 -0
- package/__mocks__/form-render.js +7 -0
- package/__mocks__/lucide-react.js +19 -0
- package/dist/index.es.js +159393 -0
- package/dist/index.umd.js +1041 -0
- package/dist/types/__tests__/assets/helper.d.ts +3 -0
- package/dist/types/__tests__/components/Designer.test.d.ts +1 -0
- package/dist/types/__tests__/components/PluginIcon.test.d.ts +1 -0
- package/dist/types/__tests__/components/Preview.test.d.ts +1 -0
- package/dist/types/__tests__/helper.test.d.ts +1 -0
- package/dist/types/src/Designer.d.ts +21 -0
- package/dist/types/src/Form.d.ts +24 -0
- package/dist/types/src/Viewer.d.ts +15 -0
- package/dist/types/src/class.d.ts +89 -0
- package/dist/types/src/components/AppContextProvider.d.ts +11 -0
- package/dist/types/src/components/CtlBar.d.ts +14 -0
- package/dist/types/src/components/Designer/Canvas/Guides.d.ts +9 -0
- package/dist/types/src/components/Designer/Canvas/Mask.d.ts +4 -0
- package/dist/types/src/components/Designer/Canvas/Moveable.d.ts +37 -0
- package/dist/types/src/components/Designer/Canvas/Padding.d.ts +6 -0
- package/dist/types/src/components/Designer/Canvas/Selecto.d.ts +10 -0
- package/dist/types/src/components/Designer/Canvas/index.d.ts +22 -0
- package/dist/types/src/components/Designer/LeftSidebar.d.ts +8 -0
- package/dist/types/src/components/Designer/PluginIcon.d.ts +10 -0
- package/dist/types/src/components/Designer/RightSidebar/DetailView/AlignWidget.d.ts +4 -0
- package/dist/types/src/components/Designer/RightSidebar/DetailView/ButtonGroupWidget.d.ts +4 -0
- package/dist/types/src/components/Designer/RightSidebar/DetailView/WidgetRenderer.d.ts +7 -0
- package/dist/types/src/components/Designer/RightSidebar/DetailView/index.d.ts +8 -0
- package/dist/types/src/components/Designer/RightSidebar/ListView/Item.d.ts +45 -0
- package/dist/types/src/components/Designer/RightSidebar/ListView/SelectableSortableContainer.d.ts +4 -0
- package/dist/types/src/components/Designer/RightSidebar/ListView/SelectableSortableItem.d.ts +14 -0
- package/dist/types/src/components/Designer/RightSidebar/ListView/index.d.ts +4 -0
- package/dist/types/src/components/Designer/RightSidebar/index.d.ts +4 -0
- package/dist/types/src/components/Designer/RightSidebar/layout.d.ts +15 -0
- package/dist/types/src/components/Designer/index.d.ts +11 -0
- package/dist/types/src/components/ErrorScreen.d.ts +7 -0
- package/dist/types/src/components/Paper.d.ts +20 -0
- package/dist/types/src/components/Preview.d.ts +15 -0
- package/dist/types/src/components/Renderer.d.ts +13 -0
- package/dist/types/src/components/Root.d.ts +9 -0
- package/dist/types/src/components/Spinner.d.ts +3 -0
- package/dist/types/src/components/StaticSchema.d.ts +10 -0
- package/dist/types/src/components/UnitPager.d.ts +10 -0
- package/dist/types/src/constants.d.ts +11 -0
- package/dist/types/src/contexts.d.ts +10 -0
- package/dist/types/src/helper.d.ts +73 -0
- package/dist/types/src/hooks.d.ts +46 -0
- package/dist/types/src/i18n.d.ts +3 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/theme.d.ts +2 -0
- package/dist/types/src/types.d.ts +19 -0
- package/eslint.config.mjs +41 -0
- package/package.json +127 -0
- package/src/Designer.tsx +107 -0
- package/src/Form.tsx +102 -0
- package/src/Viewer.tsx +59 -0
- package/src/class.ts +188 -0
- package/src/components/AppContextProvider.tsx +78 -0
- package/src/components/CtlBar.tsx +183 -0
- package/src/components/Designer/Canvas/Guides.tsx +49 -0
- package/src/components/Designer/Canvas/Mask.tsx +20 -0
- package/src/components/Designer/Canvas/Moveable.tsx +91 -0
- package/src/components/Designer/Canvas/Padding.tsx +56 -0
- package/src/components/Designer/Canvas/Selecto.tsx +45 -0
- package/src/components/Designer/Canvas/index.tsx +536 -0
- package/src/components/Designer/LeftSidebar.tsx +120 -0
- package/src/components/Designer/PluginIcon.tsx +87 -0
- package/src/components/Designer/RightSidebar/DetailView/AlignWidget.tsx +229 -0
- package/src/components/Designer/RightSidebar/DetailView/ButtonGroupWidget.tsx +78 -0
- package/src/components/Designer/RightSidebar/DetailView/WidgetRenderer.tsx +28 -0
- package/src/components/Designer/RightSidebar/DetailView/index.tsx +469 -0
- package/src/components/Designer/RightSidebar/ListView/Item.tsx +158 -0
- package/src/components/Designer/RightSidebar/ListView/SelectableSortableContainer.tsx +204 -0
- package/src/components/Designer/RightSidebar/ListView/SelectableSortableItem.tsx +88 -0
- package/src/components/Designer/RightSidebar/ListView/index.tsx +116 -0
- package/src/components/Designer/RightSidebar/index.tsx +72 -0
- package/src/components/Designer/RightSidebar/layout.tsx +75 -0
- package/src/components/Designer/index.tsx +389 -0
- package/src/components/ErrorScreen.tsx +33 -0
- package/src/components/Paper.tsx +117 -0
- package/src/components/Preview.tsx +220 -0
- package/src/components/Renderer.tsx +165 -0
- package/src/components/Root.tsx +38 -0
- package/src/components/Spinner.tsx +45 -0
- package/src/components/StaticSchema.tsx +50 -0
- package/src/components/UnitPager.tsx +119 -0
- package/src/constants.ts +21 -0
- package/src/contexts.ts +14 -0
- package/src/helper.ts +534 -0
- package/src/hooks.ts +308 -0
- package/src/i18n.ts +903 -0
- package/src/index.ts +5 -0
- package/src/theme.ts +20 -0
- package/src/types.ts +20 -0
- package/tsconfig.json +48 -0
- package/vite.config.mts +22 -0
package/src/class.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import ReactDOM from 'react-dom';
|
|
2
|
+
import { DESTROYED_ERR_MSG, DEFAULT_LANG } from './constants.js';
|
|
3
|
+
import { debounce } from './helper.js';
|
|
4
|
+
import {
|
|
5
|
+
cloneDeep,
|
|
6
|
+
Template,
|
|
7
|
+
Size,
|
|
8
|
+
Lang,
|
|
9
|
+
Font,
|
|
10
|
+
UIProps,
|
|
11
|
+
UIOptions,
|
|
12
|
+
PluginRegistry,
|
|
13
|
+
PreviewProps,
|
|
14
|
+
getDefaultFont,
|
|
15
|
+
checkUIProps,
|
|
16
|
+
checkTemplate,
|
|
17
|
+
checkInputs,
|
|
18
|
+
checkUIOptions,
|
|
19
|
+
checkPreviewProps,
|
|
20
|
+
pluginRegistry,
|
|
21
|
+
} from '@pdfme/common';
|
|
22
|
+
import { builtInPlugins } from '@pdfme/schemas';
|
|
23
|
+
|
|
24
|
+
export abstract class BaseUIClass {
|
|
25
|
+
protected domContainer!: HTMLElement | null;
|
|
26
|
+
|
|
27
|
+
protected template!: Template;
|
|
28
|
+
|
|
29
|
+
protected size!: Size;
|
|
30
|
+
|
|
31
|
+
private lang: Lang = DEFAULT_LANG;
|
|
32
|
+
|
|
33
|
+
private font: Font = getDefaultFont();
|
|
34
|
+
|
|
35
|
+
private pluginsRegistry: PluginRegistry = pluginRegistry(builtInPlugins);
|
|
36
|
+
|
|
37
|
+
private options: UIOptions = {};
|
|
38
|
+
|
|
39
|
+
private readonly setSize = debounce(() => {
|
|
40
|
+
if (!this.domContainer) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rect = this.domContainer.getBoundingClientRect();
|
|
45
|
+
const vw = window.innerWidth;
|
|
46
|
+
const vh = window.innerHeight;
|
|
47
|
+
|
|
48
|
+
const visibleWidth = Math.max(0, Math.min(rect.right, vw) - Math.max(rect.left, 0));
|
|
49
|
+
const visibleHeight = Math.max(0, Math.min(rect.bottom, vh) - Math.max(rect.top, 0));
|
|
50
|
+
|
|
51
|
+
this.size = {
|
|
52
|
+
height: visibleHeight,
|
|
53
|
+
width: visibleWidth,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
this.render();
|
|
57
|
+
}, 100);
|
|
58
|
+
|
|
59
|
+
resizeObserver = new ResizeObserver(this.setSize);
|
|
60
|
+
|
|
61
|
+
constructor(props: UIProps) {
|
|
62
|
+
checkUIProps(props);
|
|
63
|
+
|
|
64
|
+
const { domContainer, template, options = {}, plugins = {} } = props;
|
|
65
|
+
this.domContainer = domContainer;
|
|
66
|
+
this.template = cloneDeep(template);
|
|
67
|
+
this.options = options;
|
|
68
|
+
this.size = {
|
|
69
|
+
height: this.domContainer.clientHeight || window.innerHeight,
|
|
70
|
+
width: this.domContainer.clientWidth || window.innerWidth,
|
|
71
|
+
};
|
|
72
|
+
this.resizeObserver.observe(this.domContainer);
|
|
73
|
+
|
|
74
|
+
const { lang, font } = options;
|
|
75
|
+
if (lang) {
|
|
76
|
+
this.lang = lang;
|
|
77
|
+
}
|
|
78
|
+
if (font) {
|
|
79
|
+
this.font = font;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Object.values(plugins).length > 0) {
|
|
83
|
+
this.pluginsRegistry = pluginRegistry(plugins);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected getLang() {
|
|
88
|
+
return this.lang;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
protected getFont() {
|
|
92
|
+
return this.font;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
protected getPluginsRegistry() {
|
|
96
|
+
return this.pluginsRegistry;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public getOptions() {
|
|
100
|
+
return this.options;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public getTemplate() {
|
|
104
|
+
if (!this.domContainer) throw Error(DESTROYED_ERR_MSG);
|
|
105
|
+
|
|
106
|
+
return this.template;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public updateTemplate(template: Template) {
|
|
110
|
+
checkTemplate(template);
|
|
111
|
+
if (!this.domContainer) throw Error(DESTROYED_ERR_MSG);
|
|
112
|
+
|
|
113
|
+
this.template = cloneDeep(template);
|
|
114
|
+
this.render();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public updateOptions(options: UIOptions) {
|
|
118
|
+
checkUIOptions(options);
|
|
119
|
+
const { lang, font } = options || {};
|
|
120
|
+
|
|
121
|
+
if (lang) {
|
|
122
|
+
this.lang = lang;
|
|
123
|
+
}
|
|
124
|
+
if (font) {
|
|
125
|
+
this.font = font;
|
|
126
|
+
}
|
|
127
|
+
this.options = Object.assign(this.options, options);
|
|
128
|
+
this.render();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public destroy() {
|
|
132
|
+
if (!this.domContainer) throw Error(DESTROYED_ERR_MSG);
|
|
133
|
+
ReactDOM.unmountComponentAtNode(this.domContainer);
|
|
134
|
+
|
|
135
|
+
this.resizeObserver.unobserve(this.domContainer);
|
|
136
|
+
this.domContainer = null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
protected abstract render(): void;
|
|
140
|
+
}
|
|
141
|
+
export abstract class PreviewUI extends BaseUIClass {
|
|
142
|
+
protected inputs!: { [key: string]: string }[];
|
|
143
|
+
|
|
144
|
+
constructor(props: PreviewProps) {
|
|
145
|
+
super(props);
|
|
146
|
+
checkPreviewProps(props);
|
|
147
|
+
this.inputs = convertToStingObjectArray(cloneDeep(props.inputs));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public getInputs() {
|
|
151
|
+
if (!this.domContainer) throw Error(DESTROYED_ERR_MSG);
|
|
152
|
+
|
|
153
|
+
return this.inputs;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public setInputs(inputs: { [key: string]: string }[]) {
|
|
157
|
+
if (!this.domContainer) throw Error(DESTROYED_ERR_MSG);
|
|
158
|
+
checkInputs(inputs);
|
|
159
|
+
|
|
160
|
+
this.inputs = convertToStingObjectArray(inputs);
|
|
161
|
+
this.render();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
protected abstract render(): void;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
type DataItem = {
|
|
168
|
+
[key: string]: string | string[][];
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
type StringifiedDataItem = {
|
|
172
|
+
[key: string]: string;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
function convertToStingObjectArray(data: DataItem[]): StringifiedDataItem[] {
|
|
176
|
+
return data.map((item) => {
|
|
177
|
+
const stringifiedItem: StringifiedDataItem = {};
|
|
178
|
+
Object.keys(item).forEach((key) => {
|
|
179
|
+
const value = item[key];
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
stringifiedItem[key] = JSON.stringify(value);
|
|
182
|
+
} else {
|
|
183
|
+
stringifiedItem[key] = value;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return stringifiedItem;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ConfigProvider as ThemeConfigProvider } from 'antd';
|
|
3
|
+
import { I18nContext, FontContext, PluginsRegistry, OptionsContext } from '../contexts.js';
|
|
4
|
+
import { i18n, getDict } from '../i18n.js';
|
|
5
|
+
import { defaultTheme } from '../theme.js';
|
|
6
|
+
import type { Dict, Font, Lang, UIOptions, PluginRegistry } from '@pdfme/common';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
lang: Lang;
|
|
11
|
+
font: Font;
|
|
12
|
+
plugins: PluginRegistry;
|
|
13
|
+
options: UIOptions;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const isObject = (item: unknown): item is Record<string, unknown> =>
|
|
17
|
+
Boolean(item) && typeof item === 'object' && !Array.isArray(item);
|
|
18
|
+
|
|
19
|
+
const deepMerge = <T extends Record<string, unknown>, U extends Record<string, unknown>>(
|
|
20
|
+
target: T,
|
|
21
|
+
source: U,
|
|
22
|
+
): T & U => {
|
|
23
|
+
let output = { ...target } as T & U;
|
|
24
|
+
|
|
25
|
+
if (isObject(target) && isObject(source)) {
|
|
26
|
+
Object.keys(source).forEach((key) => {
|
|
27
|
+
const sourceValue = source[key];
|
|
28
|
+
if (isObject(sourceValue)) {
|
|
29
|
+
if (!(key in target)) {
|
|
30
|
+
Object.assign(output, { [key]: sourceValue });
|
|
31
|
+
} else {
|
|
32
|
+
const targetValue = target[key];
|
|
33
|
+
if (isObject(targetValue)) {
|
|
34
|
+
// Using Record<string, unknown> for recursive type
|
|
35
|
+
(output as Record<string, unknown>)[key] = deepMerge(targetValue, sourceValue);
|
|
36
|
+
} else {
|
|
37
|
+
Object.assign(output, { [key]: sourceValue });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
Object.assign(output, { [key]: sourceValue });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return output;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const AppContextProvider = ({ children, lang, font, plugins, options }: Props) => {
|
|
49
|
+
let theme = defaultTheme;
|
|
50
|
+
if (options.theme) {
|
|
51
|
+
theme = deepMerge(
|
|
52
|
+
theme as unknown as Record<string, unknown>,
|
|
53
|
+
options.theme as unknown as Record<string, unknown>,
|
|
54
|
+
) as typeof theme;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let dict = getDict(lang);
|
|
58
|
+
if (options.labels) {
|
|
59
|
+
dict = deepMerge(
|
|
60
|
+
dict as unknown as Record<string, unknown>,
|
|
61
|
+
options.labels as unknown as Record<string, unknown>,
|
|
62
|
+
) as typeof dict;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<ThemeConfigProvider theme={theme}>
|
|
67
|
+
<I18nContext.Provider value={(key: keyof Dict) => i18n(key, dict)}>
|
|
68
|
+
<FontContext.Provider value={font}>
|
|
69
|
+
<PluginsRegistry.Provider value={plugins}>
|
|
70
|
+
<OptionsContext.Provider value={options}>{children}</OptionsContext.Provider>
|
|
71
|
+
</PluginsRegistry.Provider>
|
|
72
|
+
</FontContext.Provider>
|
|
73
|
+
</I18nContext.Provider>
|
|
74
|
+
</ThemeConfigProvider>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default AppContextProvider;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import { Size } from '@pdfme/common';
|
|
3
|
+
// Import icons from lucide-react
|
|
4
|
+
// Note: In tests, these will be mocked by the mock file in __mocks__/lucide-react.js
|
|
5
|
+
import { Plus, Minus, ChevronLeft, ChevronRight, Ellipsis } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import type { MenuProps } from 'antd';
|
|
8
|
+
import { theme, Typography, Button, Dropdown } from 'antd';
|
|
9
|
+
import { I18nContext } from '../contexts.js';
|
|
10
|
+
import { useMaxZoom } from '../helper.js';
|
|
11
|
+
import { UI_CLASSNAME } from '../constants.js';
|
|
12
|
+
|
|
13
|
+
const { Text } = Typography;
|
|
14
|
+
|
|
15
|
+
type TextStyle = { color: string; fontSize: number; margin: number };
|
|
16
|
+
type ZoomProps = {
|
|
17
|
+
zoomLevel: number;
|
|
18
|
+
setZoomLevel: (zoom: number) => void;
|
|
19
|
+
style: { textStyle: TextStyle };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const Zoom = ({ zoomLevel, setZoomLevel, style }: ZoomProps) => {
|
|
23
|
+
const zoomStep = 0.25;
|
|
24
|
+
const maxZoom = useMaxZoom();
|
|
25
|
+
const minZoom = 0.25;
|
|
26
|
+
|
|
27
|
+
const nextZoomOut = zoomLevel - zoomStep;
|
|
28
|
+
const nextZoomIn = zoomLevel + zoomStep;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
32
|
+
<Button
|
|
33
|
+
className={UI_CLASSNAME + 'zoom-out'}
|
|
34
|
+
type="text"
|
|
35
|
+
disabled={minZoom >= nextZoomOut}
|
|
36
|
+
onClick={() => setZoomLevel(nextZoomOut)}
|
|
37
|
+
icon={<Minus size={16} color={style.textStyle.color} />}
|
|
38
|
+
/>
|
|
39
|
+
<Text strong style={style.textStyle}>
|
|
40
|
+
{Math.round(zoomLevel * 100)}%
|
|
41
|
+
</Text>
|
|
42
|
+
<Button
|
|
43
|
+
className={UI_CLASSNAME + 'zoom-in'}
|
|
44
|
+
type="text"
|
|
45
|
+
disabled={maxZoom < nextZoomIn}
|
|
46
|
+
onClick={() => setZoomLevel(nextZoomIn)}
|
|
47
|
+
icon={<Plus size={16} color={style.textStyle.color} />}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type PagerProps = {
|
|
54
|
+
pageCursor: number;
|
|
55
|
+
pageNum: number;
|
|
56
|
+
setPageCursor: (page: number) => void;
|
|
57
|
+
style: { textStyle: TextStyle };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const Pager = ({ pageCursor, pageNum, setPageCursor, style }: PagerProps) => {
|
|
61
|
+
return (
|
|
62
|
+
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
63
|
+
<Button className={UI_CLASSNAME + 'page-prev'} type="text" disabled={pageCursor <= 0} onClick={() => setPageCursor(pageCursor - 1)}>
|
|
64
|
+
<ChevronLeft size={16} color={style.textStyle.color} />
|
|
65
|
+
</Button>
|
|
66
|
+
<Text strong style={style.textStyle}>
|
|
67
|
+
{pageCursor + 1}/{pageNum}
|
|
68
|
+
</Text>
|
|
69
|
+
<Button
|
|
70
|
+
className={UI_CLASSNAME + 'page-next'}
|
|
71
|
+
type="text"
|
|
72
|
+
disabled={pageCursor + 1 >= pageNum}
|
|
73
|
+
onClick={() => setPageCursor(pageCursor + 1)}
|
|
74
|
+
>
|
|
75
|
+
<ChevronRight size={16} color={style.textStyle.color} />
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type ContextMenuProps = {
|
|
82
|
+
items: MenuProps['items'];
|
|
83
|
+
style: { textStyle: TextStyle };
|
|
84
|
+
};
|
|
85
|
+
const ContextMenu = ({ items, style }: ContextMenuProps) => (
|
|
86
|
+
<Dropdown menu={{ items }} placement="top" arrow trigger={['click']}>
|
|
87
|
+
<Button className={UI_CLASSNAME + 'context-menu'} type="text">
|
|
88
|
+
<Ellipsis size={16} color={style.textStyle.color} />
|
|
89
|
+
</Button>
|
|
90
|
+
</Dropdown>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
type CtlBarProps = {
|
|
94
|
+
size: Size;
|
|
95
|
+
pageCursor: number;
|
|
96
|
+
pageNum: number;
|
|
97
|
+
setPageCursor: (page: number) => void;
|
|
98
|
+
zoomLevel: number;
|
|
99
|
+
setZoomLevel: (zoom: number) => void;
|
|
100
|
+
addPageAfter?: () => void;
|
|
101
|
+
removePage?: () => void;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const CtlBar = (props: CtlBarProps) => {
|
|
105
|
+
const { token } = theme.useToken();
|
|
106
|
+
const i18n = useContext(I18nContext);
|
|
107
|
+
|
|
108
|
+
const {
|
|
109
|
+
size,
|
|
110
|
+
pageCursor,
|
|
111
|
+
pageNum,
|
|
112
|
+
setPageCursor,
|
|
113
|
+
zoomLevel,
|
|
114
|
+
setZoomLevel,
|
|
115
|
+
addPageAfter,
|
|
116
|
+
removePage,
|
|
117
|
+
} = props;
|
|
118
|
+
|
|
119
|
+
const contextMenuItems: MenuProps['items'] = [];
|
|
120
|
+
if (addPageAfter) {
|
|
121
|
+
contextMenuItems.push({
|
|
122
|
+
key: '1',
|
|
123
|
+
label: <div onClick={addPageAfter}>{i18n('addPageAfter')}</div>,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (removePage && pageNum > 1 && pageCursor !== 0) {
|
|
127
|
+
contextMenuItems.push({
|
|
128
|
+
key: '2',
|
|
129
|
+
label: <div onClick={removePage}>{i18n('removePage')}</div>,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const barWidth = 300;
|
|
134
|
+
const contextMenuWidth = contextMenuItems.length > 0 ? 50 : 0;
|
|
135
|
+
const width = (pageNum > 1 ? barWidth : barWidth / 2) + contextMenuWidth;
|
|
136
|
+
|
|
137
|
+
const textStyle = {
|
|
138
|
+
color: token.colorWhite,
|
|
139
|
+
fontSize: token.fontSize,
|
|
140
|
+
margin: token.marginXS,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div style={{ position: 'absolute', top: 'auto', bottom: '6%', width: size.width }}>
|
|
145
|
+
<div
|
|
146
|
+
className={UI_CLASSNAME + 'control-bar'}
|
|
147
|
+
style={{
|
|
148
|
+
display: 'flex',
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
justifyContent: 'space-evenly',
|
|
151
|
+
position: 'relative',
|
|
152
|
+
zIndex: 1,
|
|
153
|
+
left: `calc(50% - ${width / 2}px)`,
|
|
154
|
+
width,
|
|
155
|
+
height: 40,
|
|
156
|
+
boxSizing: 'border-box',
|
|
157
|
+
padding: token.paddingSM,
|
|
158
|
+
borderRadius: token.borderRadius,
|
|
159
|
+
backgroundColor: token.colorBgMask,
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{pageNum > 1 && (
|
|
163
|
+
<div className={UI_CLASSNAME + 'pager'}>
|
|
164
|
+
<Pager
|
|
165
|
+
style={{ textStyle }}
|
|
166
|
+
pageCursor={pageCursor}
|
|
167
|
+
pageNum={pageNum}
|
|
168
|
+
setPageCursor={setPageCursor}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
<div className={UI_CLASSNAME + 'zoom'}>
|
|
173
|
+
<Zoom style={{ textStyle }} zoomLevel={zoomLevel} setZoomLevel={setZoomLevel} />
|
|
174
|
+
</div>
|
|
175
|
+
{contextMenuItems.length > 0 && (
|
|
176
|
+
<ContextMenu items={contextMenuItems} style={{ textStyle }} />
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export default CtlBar;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { Ref } from 'react';
|
|
2
|
+
import GuidesComponent from '@scena/react-guides';
|
|
3
|
+
import { ZOOM, Size } from '@pdfme/common';
|
|
4
|
+
import { RULER_HEIGHT } from '../../../constants.js';
|
|
5
|
+
|
|
6
|
+
const guideStyle = (
|
|
7
|
+
top: number,
|
|
8
|
+
left: number,
|
|
9
|
+
height: number,
|
|
10
|
+
width: number,
|
|
11
|
+
): React.CSSProperties => ({
|
|
12
|
+
position: 'absolute',
|
|
13
|
+
top,
|
|
14
|
+
left,
|
|
15
|
+
height,
|
|
16
|
+
width,
|
|
17
|
+
background: '#333333',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const _Guides = ({
|
|
21
|
+
paperSize,
|
|
22
|
+
horizontalRef,
|
|
23
|
+
verticalRef,
|
|
24
|
+
}: {
|
|
25
|
+
paperSize: Size;
|
|
26
|
+
horizontalRef: Ref<GuidesComponent> | undefined;
|
|
27
|
+
verticalRef: Ref<GuidesComponent> | undefined;
|
|
28
|
+
}) => (
|
|
29
|
+
<>
|
|
30
|
+
<div
|
|
31
|
+
className="ruler-container"
|
|
32
|
+
style={guideStyle(-RULER_HEIGHT, -RULER_HEIGHT, RULER_HEIGHT, RULER_HEIGHT)}
|
|
33
|
+
/>
|
|
34
|
+
<GuidesComponent
|
|
35
|
+
zoom={ZOOM}
|
|
36
|
+
style={guideStyle(-RULER_HEIGHT, 0, RULER_HEIGHT, paperSize.width)}
|
|
37
|
+
type="horizontal"
|
|
38
|
+
ref={horizontalRef}
|
|
39
|
+
/>
|
|
40
|
+
<GuidesComponent
|
|
41
|
+
zoom={ZOOM}
|
|
42
|
+
style={guideStyle(0, -RULER_HEIGHT, paperSize.height, RULER_HEIGHT)}
|
|
43
|
+
type="vertical"
|
|
44
|
+
ref={verticalRef}
|
|
45
|
+
/>
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export default _Guides;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Size } from '@pdfme/common';
|
|
3
|
+
import { RULER_HEIGHT } from '../../../constants.js';
|
|
4
|
+
import { theme } from 'antd';
|
|
5
|
+
|
|
6
|
+
const Mask = ({ width, height }: Size) => (
|
|
7
|
+
<div
|
|
8
|
+
style={{
|
|
9
|
+
position: 'absolute',
|
|
10
|
+
top: -RULER_HEIGHT,
|
|
11
|
+
left: -RULER_HEIGHT,
|
|
12
|
+
zIndex: 100,
|
|
13
|
+
width,
|
|
14
|
+
height,
|
|
15
|
+
background: theme.useToken().token.colorBgMask,
|
|
16
|
+
}}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export default Mask;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { useEffect, forwardRef, Ref, useRef } from 'react';
|
|
2
|
+
import MoveableComponent, {
|
|
3
|
+
OnDrag,
|
|
4
|
+
OnRotate,
|
|
5
|
+
OnRotateEnd,
|
|
6
|
+
OnClick,
|
|
7
|
+
OnResize,
|
|
8
|
+
} from 'react-moveable';
|
|
9
|
+
import { uuid } from '../../../helper.js';
|
|
10
|
+
import { theme } from 'antd';
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
target: HTMLElement[];
|
|
14
|
+
bounds: { left: number; top: number; bottom: number; right: number };
|
|
15
|
+
horizontalGuidelines: number[];
|
|
16
|
+
verticalGuidelines: number[];
|
|
17
|
+
keepRatio: boolean;
|
|
18
|
+
rotatable: boolean;
|
|
19
|
+
onDrag: ({ target, left, top }: OnDrag) => void;
|
|
20
|
+
onDragEnd: ({ target }: { target: HTMLElement | SVGElement }) => void;
|
|
21
|
+
onDragGroupEnd: ({ targets }: { targets: (HTMLElement | SVGElement)[] }) => void;
|
|
22
|
+
onRotate: ({ target, rotate }: OnRotate) => void;
|
|
23
|
+
onRotateEnd: ({ target }: OnRotateEnd) => void;
|
|
24
|
+
onRotateGroupEnd: ({ targets }: { targets: (HTMLElement | SVGElement)[] }) => void;
|
|
25
|
+
onResize: ({ target, width, height, direction }: OnResize) => void;
|
|
26
|
+
onResizeEnd: ({ target }: { target: HTMLElement | SVGElement }) => void;
|
|
27
|
+
onResizeGroupEnd: ({ targets }: { targets: (HTMLElement | SVGElement)[] }) => void;
|
|
28
|
+
onClick: (e: OnClick) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const baseClassName = 'pdfme-moveable';
|
|
32
|
+
|
|
33
|
+
const Moveable = (props: Props, ref: Ref<MoveableComponent>) => {
|
|
34
|
+
const { token } = theme.useToken();
|
|
35
|
+
const instanceId = useRef(uuid());
|
|
36
|
+
const uniqueClassName = `${baseClassName}-${instanceId.current}`;
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const containerElement = document.querySelector(`.${uniqueClassName}`);
|
|
40
|
+
const moveableLines = document.querySelectorAll(`.${uniqueClassName} .moveable-line`);
|
|
41
|
+
if (containerElement instanceof HTMLElement) {
|
|
42
|
+
containerElement.style.setProperty('--moveable-color', token.colorPrimary);
|
|
43
|
+
moveableLines.forEach((e) => {
|
|
44
|
+
if (e instanceof HTMLElement) {
|
|
45
|
+
e.style.setProperty('--moveable-color', token.colorPrimary);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}, [props.target, token.colorPrimary, uniqueClassName]);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<MoveableComponent
|
|
53
|
+
className={uniqueClassName}
|
|
54
|
+
rootContainer={document ? document.body : undefined}
|
|
55
|
+
snappable
|
|
56
|
+
draggable
|
|
57
|
+
rotatable={props.rotatable}
|
|
58
|
+
resizable
|
|
59
|
+
throttleDrag={1}
|
|
60
|
+
throttleRotate={1}
|
|
61
|
+
throttleResize={1}
|
|
62
|
+
ref={ref}
|
|
63
|
+
target={props.target}
|
|
64
|
+
bounds={props.bounds}
|
|
65
|
+
horizontalGuidelines={props.horizontalGuidelines}
|
|
66
|
+
verticalGuidelines={props.verticalGuidelines}
|
|
67
|
+
keepRatio={props.keepRatio}
|
|
68
|
+
onRotate={props.onRotate}
|
|
69
|
+
onRotateEnd={props.onRotateEnd}
|
|
70
|
+
onRotateGroup={({ events }: { events: OnRotate[] }) => {
|
|
71
|
+
events.forEach(props.onRotate);
|
|
72
|
+
}}
|
|
73
|
+
onRotateGroupEnd={props.onRotateGroupEnd}
|
|
74
|
+
onDrag={props.onDrag}
|
|
75
|
+
onDragGroup={({ events }: { events: OnDrag[] }) => {
|
|
76
|
+
events.forEach(props.onDrag);
|
|
77
|
+
}}
|
|
78
|
+
onDragEnd={props.onDragEnd}
|
|
79
|
+
onDragGroupEnd={props.onDragGroupEnd}
|
|
80
|
+
onResize={props.onResize}
|
|
81
|
+
onResizeGroup={({ events }: { events: OnResize[] }) => {
|
|
82
|
+
events.forEach(props.onResize);
|
|
83
|
+
}}
|
|
84
|
+
onResizeEnd={props.onResizeEnd}
|
|
85
|
+
onResizeGroupEnd={props.onResizeGroupEnd}
|
|
86
|
+
onClick={props.onClick}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default forwardRef<MoveableComponent, Props>(Moveable);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type * as CSS from 'csstype';
|
|
3
|
+
import { ZOOM, BasePdf, isBlankPdf } from '@pdfme/common';
|
|
4
|
+
import { theme } from 'antd';
|
|
5
|
+
|
|
6
|
+
const getPaddingStyle = (i: number, p: number, color: string): CSS.Properties => {
|
|
7
|
+
const style: CSS.Properties = {
|
|
8
|
+
position: 'absolute',
|
|
9
|
+
background: color,
|
|
10
|
+
opacity: 0.25,
|
|
11
|
+
pointerEvents: 'none',
|
|
12
|
+
};
|
|
13
|
+
switch (i) {
|
|
14
|
+
case 0:
|
|
15
|
+
style.top = 0;
|
|
16
|
+
style.height = `${p * ZOOM}px`;
|
|
17
|
+
style.left = 0;
|
|
18
|
+
style.right = 0;
|
|
19
|
+
break;
|
|
20
|
+
case 1:
|
|
21
|
+
style.right = 0;
|
|
22
|
+
style.width = `${p * ZOOM}px`;
|
|
23
|
+
style.top = 0;
|
|
24
|
+
style.bottom = 0;
|
|
25
|
+
break;
|
|
26
|
+
case 2:
|
|
27
|
+
style.bottom = 0;
|
|
28
|
+
style.height = `${p * ZOOM}px`;
|
|
29
|
+
style.left = 0;
|
|
30
|
+
style.right = 0;
|
|
31
|
+
break;
|
|
32
|
+
case 3:
|
|
33
|
+
style.left = 0;
|
|
34
|
+
style.width = `${p * ZOOM}px`;
|
|
35
|
+
style.top = 0;
|
|
36
|
+
style.bottom = 0;
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return style;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const Padding = ({ basePdf }: { basePdf: BasePdf }) => {
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
{isBlankPdf(basePdf) &&
|
|
49
|
+
basePdf.padding.map((p, i) => (
|
|
50
|
+
<div key={String(i)} style={getPaddingStyle(i, p, theme.useToken().token.colorError)} />
|
|
51
|
+
))}
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default Padding;
|