proteum 1.0.0-1
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/.dockerignore +10 -0
- package/Rte.zip +0 -0
- package/cli/app/config.ts +54 -0
- package/cli/app/index.ts +195 -0
- package/cli/bin.js +11 -0
- package/cli/commands/build.ts +34 -0
- package/cli/commands/deploy/app.ts +29 -0
- package/cli/commands/deploy/web.ts +60 -0
- package/cli/commands/dev.ts +109 -0
- package/cli/commands/init.ts +85 -0
- package/cli/compiler/client/identite.ts +72 -0
- package/cli/compiler/client/index.ts +334 -0
- package/cli/compiler/common/babel/index.ts +170 -0
- package/cli/compiler/common/babel/plugins/index.ts +0 -0
- package/cli/compiler/common/babel/plugins/services.ts +579 -0
- package/cli/compiler/common/babel/routes/imports.ts +127 -0
- package/cli/compiler/common/babel/routes/routes.ts +1130 -0
- package/cli/compiler/common/files/autres.ts +39 -0
- package/cli/compiler/common/files/images.ts +35 -0
- package/cli/compiler/common/files/style.ts +78 -0
- package/cli/compiler/common/index.ts +154 -0
- package/cli/compiler/index.ts +532 -0
- package/cli/compiler/server/index.ts +211 -0
- package/cli/index.ts +189 -0
- package/cli/paths.ts +165 -0
- package/cli/print.ts +12 -0
- package/cli/tsconfig.json +38 -0
- package/cli/utils/index.ts +22 -0
- package/cli/utils/keyboard.ts +78 -0
- package/client/app/component.tsx +54 -0
- package/client/app/index.ts +142 -0
- package/client/app/service.ts +34 -0
- package/client/app.tsconfig.json +28 -0
- package/client/components/Button.tsx +298 -0
- package/client/components/Dialog/Manager.tsx +309 -0
- package/client/components/Dialog/card.tsx +208 -0
- package/client/components/Dialog/index.less +151 -0
- package/client/components/Dialog/status.less +176 -0
- package/client/components/Dialog/status.tsx +48 -0
- package/client/components/index.ts +2 -0
- package/client/components/types.d.ts +3 -0
- package/client/data/input.ts +32 -0
- package/client/global.d.ts +5 -0
- package/client/hooks.ts +22 -0
- package/client/index.ts +6 -0
- package/client/pages/_layout/index.less +6 -0
- package/client/pages/_layout/index.tsx +43 -0
- package/client/pages/bug.tsx.old +60 -0
- package/client/pages/useHeader.tsx +50 -0
- package/client/services/captcha/index.ts +67 -0
- package/client/services/router/components/Link.tsx +46 -0
- package/client/services/router/components/Page.tsx +55 -0
- package/client/services/router/components/router.tsx +218 -0
- package/client/services/router/index.tsx +521 -0
- package/client/services/router/request/api.ts +267 -0
- package/client/services/router/request/history.ts +5 -0
- package/client/services/router/request/index.ts +53 -0
- package/client/services/router/request/multipart.ts +147 -0
- package/client/services/router/response/index.tsx +128 -0
- package/client/services/router/response/page.ts +86 -0
- package/client/services/socket/index.ts +147 -0
- package/client/utils/dom.ts +77 -0
- package/common/app/index.ts +9 -0
- package/common/data/chaines/index.ts +54 -0
- package/common/data/dates.ts +179 -0
- package/common/data/markdown.ts +73 -0
- package/common/data/rte/nodes.ts +83 -0
- package/common/data/stats.ts +90 -0
- package/common/errors/index.tsx +326 -0
- package/common/router/index.ts +213 -0
- package/common/router/layouts.ts +93 -0
- package/common/router/register.ts +55 -0
- package/common/router/request/api.ts +77 -0
- package/common/router/request/index.ts +35 -0
- package/common/router/response/index.ts +45 -0
- package/common/router/response/page.ts +128 -0
- package/common/utils/rte.ts +183 -0
- package/common/utils.ts +7 -0
- package/doc/TODO.md +71 -0
- package/doc/front/router.md +27 -0
- package/doc/workspace/workspace.png +0 -0
- package/doc/workspace/workspace2.png +0 -0
- package/doc/workspace/workspace_26.01.22.png +0 -0
- package/package.json +171 -0
- package/server/app/commands.ts +141 -0
- package/server/app/container/config.ts +203 -0
- package/server/app/container/console/index.ts +550 -0
- package/server/app/container/index.ts +137 -0
- package/server/app/index.ts +273 -0
- package/server/app/service/container.ts +88 -0
- package/server/app/service/index.ts +235 -0
- package/server/app.tsconfig.json +28 -0
- package/server/context.ts +4 -0
- package/server/index.ts +4 -0
- package/server/services/auth/index.ts +250 -0
- package/server/services/auth/old.ts +277 -0
- package/server/services/auth/router/index.ts +95 -0
- package/server/services/auth/router/request.ts +54 -0
- package/server/services/auth/router/service.json +6 -0
- package/server/services/auth/service.json +6 -0
- package/server/services/cache/commands.ts +41 -0
- package/server/services/cache/index.ts +297 -0
- package/server/services/cache/service.json +6 -0
- package/server/services/cron/CronTask.ts +86 -0
- package/server/services/cron/index.ts +112 -0
- package/server/services/cron/service.json +6 -0
- package/server/services/disks/driver.ts +103 -0
- package/server/services/disks/drivers/local/index.ts +188 -0
- package/server/services/disks/drivers/local/service.json +6 -0
- package/server/services/disks/drivers/s3/index.ts +301 -0
- package/server/services/disks/drivers/s3/service.json +6 -0
- package/server/services/disks/index.ts +90 -0
- package/server/services/disks/service.json +6 -0
- package/server/services/email/index.ts +188 -0
- package/server/services/email/utils.ts +53 -0
- package/server/services/fetch/index.ts +201 -0
- package/server/services/fetch/service.json +7 -0
- package/server/services/models.7z +0 -0
- package/server/services/prisma/Facet.ts +142 -0
- package/server/services/prisma/index.ts +201 -0
- package/server/services/prisma/service.json +6 -0
- package/server/services/router/http/index.ts +217 -0
- package/server/services/router/http/multipart.ts +102 -0
- package/server/services/router/http/session.ts.old +40 -0
- package/server/services/router/index.ts +801 -0
- package/server/services/router/request/api.ts +87 -0
- package/server/services/router/request/index.ts +184 -0
- package/server/services/router/request/service.ts +21 -0
- package/server/services/router/request/validation/zod.ts +180 -0
- package/server/services/router/response/index.ts +338 -0
- package/server/services/router/response/mask/Filter.ts +323 -0
- package/server/services/router/response/mask/index.ts +60 -0
- package/server/services/router/response/mask/selecteurs.ts +92 -0
- package/server/services/router/response/page/document.tsx +160 -0
- package/server/services/router/response/page/index.tsx +196 -0
- package/server/services/router/service.json +6 -0
- package/server/services/router/service.ts +36 -0
- package/server/services/schema/index.ts +44 -0
- package/server/services/schema/request.ts +49 -0
- package/server/services/schema/router/index.ts +28 -0
- package/server/services/schema/router/service.json +6 -0
- package/server/services/schema/service.json +6 -0
- package/server/services/security/encrypt/aes/index.ts +85 -0
- package/server/services/security/encrypt/aes/service.json +6 -0
- package/server/services/socket/index.ts +162 -0
- package/server/services/socket/scope.ts +226 -0
- package/server/services/socket/service.json +6 -0
- package/server/services_old/SocketClient.ts +92 -0
- package/server/services_old/Token.old.ts +97 -0
- package/server/utils/slug.ts +79 -0
- package/tsconfig.common.json +45 -0
- package/tsconfig.json +3 -0
- package/types/aliases.d.ts +54 -0
- package/types/global/modules.d.ts +49 -0
- package/types/global/utils.d.ts +103 -0
- package/types/icons.d.ts +1 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// npm
|
|
6
|
+
import readline, { Key } from 'readline';
|
|
7
|
+
|
|
8
|
+
/*----------------------------------
|
|
9
|
+
- TYPES
|
|
10
|
+
----------------------------------*/
|
|
11
|
+
|
|
12
|
+
type TKeyboardCommand = {
|
|
13
|
+
remove?: boolean,
|
|
14
|
+
run: (str: string, chunk: string, key: Key) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/*----------------------------------
|
|
18
|
+
- METHODS
|
|
19
|
+
----------------------------------*/
|
|
20
|
+
class KeyboardCommands {
|
|
21
|
+
|
|
22
|
+
private commands: { [input: string]: TKeyboardCommand } = {}
|
|
23
|
+
|
|
24
|
+
public constructor() {
|
|
25
|
+
this.listen();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private listen() {
|
|
29
|
+
|
|
30
|
+
readline.emitKeypressEvents(process.stdin);
|
|
31
|
+
process.stdin.setRawMode(true);
|
|
32
|
+
process.stdin.on('keypress', async (chunk: string, key: Key) => {
|
|
33
|
+
|
|
34
|
+
let str = key.name;
|
|
35
|
+
if (!str) return;
|
|
36
|
+
if (str === 'return') str = 'enter';
|
|
37
|
+
|
|
38
|
+
if (key.ctrl) str = 'ctrl+' + str;
|
|
39
|
+
if (key.shift) str = 'shift+' + str;
|
|
40
|
+
if (key.meta) str = 'meta+' + str;
|
|
41
|
+
|
|
42
|
+
const kCommand = this.commands[str] || this.commands.fallback;
|
|
43
|
+
if (kCommand) {
|
|
44
|
+
|
|
45
|
+
kCommand.run(str, chunk, key);
|
|
46
|
+
|
|
47
|
+
if (kCommand.remove)
|
|
48
|
+
delete this.commands[str];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (str === 'ctrl+c') {
|
|
52
|
+
|
|
53
|
+
console.log(`Exiting ...`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
public input(str: string, run: TKeyboardCommand["run"], options: Omit<TKeyboardCommand, 'run'> = {}) {
|
|
64
|
+
this.commands[str] = { run, ...options }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public waitForInput(str: string): Promise<void> {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
this.commands[str] = {
|
|
70
|
+
run: () => resolve(),
|
|
71
|
+
remove: true
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default new KeyboardCommands
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
// Core
|
|
9
|
+
import type { Layout } from '@common/router';
|
|
10
|
+
import { ReactClientContext } from '@/client/context';
|
|
11
|
+
import DialogManager from '@client/components/Dialog/Manager'
|
|
12
|
+
|
|
13
|
+
// Core components
|
|
14
|
+
import RouterComponent from '@client/services/router/components/router';
|
|
15
|
+
import type { TClientOrServerContextForPage } from '@common/router';
|
|
16
|
+
|
|
17
|
+
/*----------------------------------
|
|
18
|
+
- COMPOSANT
|
|
19
|
+
----------------------------------*/
|
|
20
|
+
export default function App({ context }: {
|
|
21
|
+
context: TClientOrServerContextForPage,
|
|
22
|
+
}) {
|
|
23
|
+
|
|
24
|
+
const curLayout = context.page?.layout;
|
|
25
|
+
const [layout, setLayout] = React.useState<Layout | false | undefined>(curLayout);
|
|
26
|
+
const [apiData, setApiData] = React.useState<{ [k: string]: any } | null>(context.page?.data || {});
|
|
27
|
+
|
|
28
|
+
// TODO: context.page is always provided in the context on the client side
|
|
29
|
+
if (context.app.side === "client")
|
|
30
|
+
context.app.setLayout = setLayout;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<ReactClientContext.Provider value={context}>
|
|
34
|
+
|
|
35
|
+
<DialogManager context={context} />
|
|
36
|
+
|
|
37
|
+
{!layout ? <>
|
|
38
|
+
{/* TODO: move to app, because here, we're not aware that the router service has been defined */}
|
|
39
|
+
<RouterComponent service={context.Router} />
|
|
40
|
+
</> : <> {/* Same as router/components/Page.tsx */}
|
|
41
|
+
<layout.Component
|
|
42
|
+
// Services
|
|
43
|
+
{...context}
|
|
44
|
+
// API data & URL params
|
|
45
|
+
data={{
|
|
46
|
+
...apiData,
|
|
47
|
+
...context.request.data
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
</>}
|
|
51
|
+
|
|
52
|
+
</ReactClientContext.Provider>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
if (typeof window === 'undefined')
|
|
9
|
+
throw new Error(`This file shouldn't be loaded on server side !!!!`);
|
|
10
|
+
|
|
11
|
+
window.dev && require('preact/debug');
|
|
12
|
+
|
|
13
|
+
// Core
|
|
14
|
+
import { CoreError, InputErrorSchema } from '@common/errors';
|
|
15
|
+
import type { Layout } from '@common/router';
|
|
16
|
+
import { createDialog } from '@client/components/Dialog/Manager';
|
|
17
|
+
|
|
18
|
+
// Local
|
|
19
|
+
import type { AnyService } from './service';
|
|
20
|
+
|
|
21
|
+
export { default as Service } from './service';
|
|
22
|
+
|
|
23
|
+
/*----------------------------------
|
|
24
|
+
- TYPES
|
|
25
|
+
----------------------------------*/
|
|
26
|
+
|
|
27
|
+
declare global {
|
|
28
|
+
interface Window {
|
|
29
|
+
dev: boolean,
|
|
30
|
+
/*context: ClientContext,
|
|
31
|
+
user: User,*/
|
|
32
|
+
/*context: ClientContext,
|
|
33
|
+
user: User,*/
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type TBugReportInfos = {
|
|
38
|
+
stacktrace?: string,
|
|
39
|
+
observation?: string,
|
|
40
|
+
before?: string,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type TClientBugReportInfos = TBugReportInfos & {
|
|
44
|
+
context?: string,
|
|
45
|
+
guiVersion: string,
|
|
46
|
+
url: string,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Without prettify, we don't get a clear list of the class properties
|
|
50
|
+
type Prettify<T> = {
|
|
51
|
+
[K in keyof T]: T[K];
|
|
52
|
+
} & {};
|
|
53
|
+
|
|
54
|
+
export type ApplicationProperties = Prettify<keyof Application>;
|
|
55
|
+
|
|
56
|
+
/*----------------------------------
|
|
57
|
+
- CLASS
|
|
58
|
+
----------------------------------*/
|
|
59
|
+
export default abstract class Application {
|
|
60
|
+
|
|
61
|
+
public side = 'client' as 'client';
|
|
62
|
+
|
|
63
|
+
private servicesList: AnyService[] = []
|
|
64
|
+
|
|
65
|
+
// TODO: merge modal and toast in the same instance
|
|
66
|
+
public modal = createDialog(this, false);
|
|
67
|
+
public toast = createDialog(this, true);
|
|
68
|
+
|
|
69
|
+
public constructor() {
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public registerService( service: AnyService ) {
|
|
74
|
+
console.log(`[app] Register service`, service.constructor?.name);
|
|
75
|
+
this.servicesList.push(service);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public start() {
|
|
79
|
+
this.bindErrorHandlers();
|
|
80
|
+
this.startServices();
|
|
81
|
+
this.boot();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public abstract boot(): void;
|
|
85
|
+
|
|
86
|
+
public startServices() {
|
|
87
|
+
|
|
88
|
+
console.log(`[app] Starting ${this.servicesList.length} services.`);
|
|
89
|
+
|
|
90
|
+
for (const service of this.servicesList) {
|
|
91
|
+
console.log(`[app] Start service`, service);
|
|
92
|
+
service.start();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`[app] All ${this.servicesList.length} services were started.`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public bindErrorHandlers() {
|
|
99
|
+
|
|
100
|
+
// Impossible de recup le stacktrace ...
|
|
101
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
102
|
+
const error = new Error(e.reason); // How to get stacktrace ?
|
|
103
|
+
this.handleError(error);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
window.onerror = (message, file, line, col, stacktrace) => {
|
|
107
|
+
console.error(`Exception catched by method B`, message);
|
|
108
|
+
this.reportBug({
|
|
109
|
+
stacktrace: stacktrace?.stack || JSON.stringify({ message, file, line, col })
|
|
110
|
+
}).then(() => {
|
|
111
|
+
|
|
112
|
+
// TODO in toas service: app.on('bug', () => toast.warning( ... ))
|
|
113
|
+
/*context?.toast.warning("Bug detected",
|
|
114
|
+
"A bug report has been sent, because I've detected a bug on the interface. I'm really sorry for the interruption.",
|
|
115
|
+
null,
|
|
116
|
+
{ autohide: false });*/
|
|
117
|
+
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public abstract handleError( error: CoreError | Error );
|
|
123
|
+
|
|
124
|
+
// TODO: move on app side
|
|
125
|
+
public reportBug = (infos: TBugReportInfos) => fetch('/feedback/bug/ui', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
'Accept': "application/json",
|
|
129
|
+
'Content-Type': 'application/json'
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
url: window.location.pathname,
|
|
133
|
+
context: JSON.stringify(window["ssr"]),
|
|
134
|
+
guiVersion: BUILD_DATE,
|
|
135
|
+
...infos
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
public setLayout(layout: Layout) {
|
|
140
|
+
throw new Error(`page.setLayout has been called before the function is assigned from the <App /> component.`);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
import type Application from ".";
|
|
6
|
+
|
|
7
|
+
/*----------------------------------
|
|
8
|
+
- TYPES: OPTIONS
|
|
9
|
+
----------------------------------*/
|
|
10
|
+
|
|
11
|
+
export type AnyService = Service<{}, Application>
|
|
12
|
+
|
|
13
|
+
/*----------------------------------
|
|
14
|
+
- CLASS
|
|
15
|
+
----------------------------------*/
|
|
16
|
+
export default abstract class Service<
|
|
17
|
+
TConfig extends {},
|
|
18
|
+
TApplication extends Application
|
|
19
|
+
> {
|
|
20
|
+
public constructor(
|
|
21
|
+
public app: TApplication,
|
|
22
|
+
public config: TConfig,
|
|
23
|
+
) {
|
|
24
|
+
|
|
25
|
+
// No client service should be loaded from server side
|
|
26
|
+
if (typeof window === 'undefined')
|
|
27
|
+
throw new Error(`Client services shouldn't be loaded on server side.`);
|
|
28
|
+
|
|
29
|
+
// Make the app aware of his services
|
|
30
|
+
app.registerService(this);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public abstract start(): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../node_modules/proteum/tsconfig.common.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "..",
|
|
5
|
+
"baseUrl": "..",
|
|
6
|
+
"paths": {
|
|
7
|
+
|
|
8
|
+
"@client/*": ["../node_modules/proteum/client/*"],
|
|
9
|
+
"@common/*": ["../node_modules/proteum/common/*"],
|
|
10
|
+
|
|
11
|
+
// Only used for typings (ex: ServerResponse)
|
|
12
|
+
// Removed before webpack compilation
|
|
13
|
+
"@server/*": ["../node_modules/proteum/server/*"],
|
|
14
|
+
"@/*": ["./*"],
|
|
15
|
+
|
|
16
|
+
// ATTENTION: Les références à preact doivent toujours pointer vers la même instance
|
|
17
|
+
"react": ["preact/compat"],
|
|
18
|
+
"react-dom/test-utils": ["preact/test-utils"],
|
|
19
|
+
"react-dom": ["preact/compat"], // Must be below test-utils
|
|
20
|
+
"react/jsx-runtime": ["preact/jsx-runtime"],
|
|
21
|
+
"preact/jsx-runtime": ["preact/jsx-runtime"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": [
|
|
25
|
+
".",
|
|
26
|
+
"../../node_modules/proteum/types/global"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { VNode, RefObject,ComponentChild } from 'preact';
|
|
8
|
+
|
|
9
|
+
// Core
|
|
10
|
+
import { shouldOpenNewTab } from '@client/services/router/components/Link';
|
|
11
|
+
import { history } from '@client/services/router/request/history';
|
|
12
|
+
import useContext from '@/client/context';
|
|
13
|
+
|
|
14
|
+
/*----------------------------------
|
|
15
|
+
- TYPES
|
|
16
|
+
----------------------------------*/
|
|
17
|
+
|
|
18
|
+
export type Props = {
|
|
19
|
+
|
|
20
|
+
id?: string,
|
|
21
|
+
refElem?: RefObject<HTMLElement>,
|
|
22
|
+
|
|
23
|
+
icon?: ComponentChild,
|
|
24
|
+
iconR?: ComponentChild,
|
|
25
|
+
|
|
26
|
+
prefix?: ComponentChild,
|
|
27
|
+
suffix?: ComponentChild,
|
|
28
|
+
|
|
29
|
+
tag?: "a" | "button",
|
|
30
|
+
type?: 'guide' | 'primary' | 'secondary' | 'link',
|
|
31
|
+
shape?: 'default' | 'icon' | 'tile' | 'pill' | 'custom',
|
|
32
|
+
size?: TComponentSize,
|
|
33
|
+
class?: string,
|
|
34
|
+
|
|
35
|
+
state?: [string, React.StateUpdater<string>],
|
|
36
|
+
active?: boolean,
|
|
37
|
+
selected?: boolean,
|
|
38
|
+
disabled?: boolean,
|
|
39
|
+
loading?: boolean,
|
|
40
|
+
autoFocus?: boolean,
|
|
41
|
+
onClick?: (e: MouseEvent) => any,
|
|
42
|
+
async?: boolean,
|
|
43
|
+
|
|
44
|
+
submenu?: ComponentChild,
|
|
45
|
+
nav?: boolean | 'exact'
|
|
46
|
+
|
|
47
|
+
// SEO: if icon only, should provinde a hint (aria-label)
|
|
48
|
+
} & ({
|
|
49
|
+
hint: string,
|
|
50
|
+
children?: ComponentChild | ComponentChild[],
|
|
51
|
+
} | {
|
|
52
|
+
children: ComponentChild | ComponentChild[],
|
|
53
|
+
hint?: string,
|
|
54
|
+
}) & (TButtonProps | TLinkProps)
|
|
55
|
+
|
|
56
|
+
export type TButtonProps = React.JSX.HTMLAttributes<HTMLButtonElement>
|
|
57
|
+
|
|
58
|
+
export type TLinkProps = React.JSX.HTMLAttributes<HTMLAnchorElement>
|
|
59
|
+
|
|
60
|
+
/*----------------------------------
|
|
61
|
+
- HELPERS
|
|
62
|
+
----------------------------------*/
|
|
63
|
+
const trimSlash = (str: string): string => {
|
|
64
|
+
return str.endsWith('/') ? str.slice(0, -1) : str;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isCurrentUrl = (currentUrl: string, url: string, exact?: boolean) => {
|
|
68
|
+
return (
|
|
69
|
+
(exact && (url === currentUrl || trimSlash(url) === currentUrl))
|
|
70
|
+
||
|
|
71
|
+
(!exact && currentUrl.startsWith(url))
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/*----------------------------------
|
|
76
|
+
- CONTROLEUR
|
|
77
|
+
----------------------------------*/
|
|
78
|
+
export default ({
|
|
79
|
+
|
|
80
|
+
id,
|
|
81
|
+
|
|
82
|
+
// Content
|
|
83
|
+
icon, prefix,
|
|
84
|
+
children,
|
|
85
|
+
iconR, suffix,
|
|
86
|
+
submenu,
|
|
87
|
+
nav,
|
|
88
|
+
hint,
|
|
89
|
+
|
|
90
|
+
// Style
|
|
91
|
+
class: className,
|
|
92
|
+
shape,
|
|
93
|
+
size,
|
|
94
|
+
type,
|
|
95
|
+
|
|
96
|
+
// Interactions
|
|
97
|
+
active,
|
|
98
|
+
selected,
|
|
99
|
+
state: stateUpdater,
|
|
100
|
+
disabled,
|
|
101
|
+
loading,
|
|
102
|
+
//autoFocus,
|
|
103
|
+
async,
|
|
104
|
+
|
|
105
|
+
// HTML attributes
|
|
106
|
+
tag: Tag,
|
|
107
|
+
refElem,
|
|
108
|
+
...props
|
|
109
|
+
|
|
110
|
+
}: Props) => {
|
|
111
|
+
|
|
112
|
+
const ctx = useContext();
|
|
113
|
+
let [isSelected, setIsSelected] = React.useState(false);
|
|
114
|
+
let [isActive, setIsActive] = React.useState(false);
|
|
115
|
+
const [isLoading, setLoading] = React.useState(false);
|
|
116
|
+
|
|
117
|
+
if (isLoading || loading) {
|
|
118
|
+
icon = <i src="spin" />
|
|
119
|
+
iconR = undefined;
|
|
120
|
+
disabled = true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (stateUpdater && id !== undefined) {
|
|
124
|
+
const [active, setActive] = stateUpdater;
|
|
125
|
+
if (id === active)
|
|
126
|
+
isSelected = true;
|
|
127
|
+
props.onClick = () => setActive(id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Hint
|
|
131
|
+
if (hint !== undefined) {
|
|
132
|
+
props['aria-label'] = hint;
|
|
133
|
+
props.title = hint;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Shape classes
|
|
137
|
+
const classNames: string[] = ['btn'];
|
|
138
|
+
if (className)
|
|
139
|
+
classNames.push(className);
|
|
140
|
+
|
|
141
|
+
if (shape !== undefined) {
|
|
142
|
+
if (shape === 'tile')
|
|
143
|
+
classNames.push('col');
|
|
144
|
+
else
|
|
145
|
+
classNames.push(shape);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (size !== undefined)
|
|
149
|
+
classNames.push(size);
|
|
150
|
+
|
|
151
|
+
if (icon) {
|
|
152
|
+
if (children === undefined)
|
|
153
|
+
classNames.push('icon');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// state classes
|
|
157
|
+
const [isMouseDown, setMouseDown] = React.useState(false);
|
|
158
|
+
props.onMouseDown = () => setMouseDown(true);
|
|
159
|
+
props.onMouseUp = () => setMouseDown(false);
|
|
160
|
+
props.onMouseLeave = () => setMouseDown(false);
|
|
161
|
+
|
|
162
|
+
// Theming & state
|
|
163
|
+
if (isMouseDown)
|
|
164
|
+
classNames.push('pressed');
|
|
165
|
+
else if (selected || isSelected === true)
|
|
166
|
+
classNames.push('bg accent');
|
|
167
|
+
else if (type !== undefined)
|
|
168
|
+
classNames.push(type === 'link' ? type : (' bg ' + type));
|
|
169
|
+
|
|
170
|
+
if (active || isActive === true)
|
|
171
|
+
classNames.push('active');
|
|
172
|
+
|
|
173
|
+
// Icon
|
|
174
|
+
if (prefix === undefined && icon !== undefined)
|
|
175
|
+
prefix = typeof icon === "string" ? <i class={"svg-" + icon} /> : icon;
|
|
176
|
+
if (suffix === undefined && iconR !== undefined)
|
|
177
|
+
suffix = typeof iconR === "string" ? <i class={"svg-" + iconR} /> : iconR;
|
|
178
|
+
|
|
179
|
+
// Render
|
|
180
|
+
if ('link' in props || Tag === "a") {
|
|
181
|
+
|
|
182
|
+
// Link (only if enabled)
|
|
183
|
+
if (!disabled) {
|
|
184
|
+
|
|
185
|
+
props.href = props.link;
|
|
186
|
+
|
|
187
|
+
// External = open in new tab by default
|
|
188
|
+
if (shouldOpenNewTab( props.href, props.target ))
|
|
189
|
+
props.target = '_blank';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Nav
|
|
193
|
+
if (nav && props.target === undefined) {
|
|
194
|
+
|
|
195
|
+
const checkIfCurrentUrl = (url: string) =>
|
|
196
|
+
isCurrentUrl(url, props.link, nav === 'exact');
|
|
197
|
+
|
|
198
|
+
React.useEffect(() => {
|
|
199
|
+
|
|
200
|
+
// Init
|
|
201
|
+
if (checkIfCurrentUrl(ctx.request.path))
|
|
202
|
+
setIsActive(true);
|
|
203
|
+
|
|
204
|
+
// On location change
|
|
205
|
+
return history?.listen(({ location }) => {
|
|
206
|
+
|
|
207
|
+
setIsActive( checkIfCurrentUrl(location.pathname) );
|
|
208
|
+
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
}, []);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
Tag = 'a';
|
|
215
|
+
|
|
216
|
+
} else {
|
|
217
|
+
Tag = 'button';
|
|
218
|
+
|
|
219
|
+
// Avoid to trigget onclick when presing enter
|
|
220
|
+
if (type !== 'primary')
|
|
221
|
+
props.type = 'button';
|
|
222
|
+
else
|
|
223
|
+
props.type = 'submit';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let render: VNode = (
|
|
227
|
+
<Tag {...props} id={id} class={classNames.join(' ')} disabled={disabled} ref={refElem} onClick={(e: MouseEvent) => {
|
|
228
|
+
|
|
229
|
+
// annulation si:
|
|
230
|
+
// - Pas clic gauche
|
|
231
|
+
// - Event annulé
|
|
232
|
+
if (e.button !== 0)
|
|
233
|
+
return;
|
|
234
|
+
|
|
235
|
+
// Disabled
|
|
236
|
+
if (disabled)
|
|
237
|
+
return false;
|
|
238
|
+
|
|
239
|
+
// Custom event
|
|
240
|
+
if (props.onClick !== undefined) {
|
|
241
|
+
|
|
242
|
+
const returned = props.onClick(e);
|
|
243
|
+
if (async && returned?.then) {
|
|
244
|
+
setLoading(true);
|
|
245
|
+
returned.finally(() => setLoading(false));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Link
|
|
250
|
+
let nativeEvent: boolean = false;
|
|
251
|
+
if (('link' in props) && !e.defaultPrevented) {
|
|
252
|
+
|
|
253
|
+
// Nouvelle fenetre = event par defaut
|
|
254
|
+
if (props.target === '_blank') {
|
|
255
|
+
|
|
256
|
+
nativeEvent = true;
|
|
257
|
+
|
|
258
|
+
// Page change = loading indicator
|
|
259
|
+
} else if (props.target === "_self") {
|
|
260
|
+
|
|
261
|
+
setLoading(true);
|
|
262
|
+
window.location.href = props.link;
|
|
263
|
+
|
|
264
|
+
} else {
|
|
265
|
+
|
|
266
|
+
history?.push(props.link);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!nativeEvent) {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}}>
|
|
275
|
+
{prefix}
|
|
276
|
+
{children === undefined
|
|
277
|
+
? null
|
|
278
|
+
: shape === 'custom'
|
|
279
|
+
? children : (
|
|
280
|
+
<span class={"label"}>
|
|
281
|
+
{children}
|
|
282
|
+
</span>
|
|
283
|
+
)}
|
|
284
|
+
{suffix}
|
|
285
|
+
</Tag>
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if (Tag === "li" || submenu) {
|
|
289
|
+
render = (
|
|
290
|
+
<li>
|
|
291
|
+
{render}
|
|
292
|
+
{submenu}
|
|
293
|
+
</li>
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return render;
|
|
298
|
+
}
|