diff --git a/.eslintrc.js b/.eslintrc.js index a9e9619f8..5c0df6ab0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,6 +3,7 @@ const WARN = 1; module.exports = { extends: ['@poool/eslint-config-react'], + parser: '@typescript-eslint/parser', rules: { 'react/prop-types': OFF, 'react/jsx-uses-react': OFF, @@ -11,16 +12,16 @@ module.exports = { 'n/no-callback-literal': OFF, }, overrides: [{ - files: ['packages/**/*.test.js', 'packages/**/tests/**/*.js'], + files: [ + 'packages/**/*.test.js', + 'packages/**/*.test.tsx', + 'packages/**/*.test.ts', + ], env: { jest: true, }, - rules: { - 'import/order': OFF, - }, }, { files: ['packages/**/*.{ts,tsx}'], - parser: '@typescript-eslint/parser', globals: { JSX: 'readonly', React: 'readonly', diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4424d5af0..736888483 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: - name: Test run: yarn test - name: Codecov upload - uses: codecov/codecov-action@v4.0.1 + uses: codecov/codecov-action@v4.4.1 with: token: ${{ secrets.CODECOV_TOKEN }} build: diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 2a337c115..ffc8fae42 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -28,7 +28,7 @@ jobs: node-version: 20 cache: yarn - name: Setup Pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@v5 - name: Install and build run: | yarn install diff --git a/.storybook/babel.config.js b/.storybook/babel.config.js deleted file mode 100644 index 8ede4875a..000000000 --- a/.storybook/babel.config.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - presets: [ - ['@babel/env', { - corejs: 3, - useBuiltIns: 'usage', - }], - ['@babel/react', { - runtime: 'automatic', - // importSource: 'preact', - }], - ], - plugins: [ - ['@babel/transform-runtime', { - corejs: 3, - }], - ], -}; diff --git a/.storybook/main.js b/.storybook/main.js deleted file mode 100644 index d7b0e9979..000000000 --- a/.storybook/main.js +++ /dev/null @@ -1,98 +0,0 @@ -const path = require('path'); -const autoprefixer = require('autoprefixer'); -const tailwindcss = require('tailwindcss'); -const { bundler, styles } = require('@ckeditor/ckeditor5-dev-utils'); - -module.exports = { - stories: [ - '../packages/react/lib/index.stories.js', - '../packages/*/lib/**/*.stories.js', - ], - addons: ['@storybook/addon-actions', 'storybook-dark-mode'], - webpackFinal: config => { - config.resolve.alias = { - '@oakjs/core': path.resolve('./packages/core/lib'), - '@oakjs/react': path.resolve('./packages/react/lib'), - '@oakjs/ckeditor5-build-custom': path.resolve('./packages/ckeditor5-build-custom/lib'), - '@poool/oak/lib': path.resolve('./packages/oak/lib'), - '@poool/oak': path.resolve('./packages/oak/lib') - }; - - config.module.rules.push({ - test: /\.sass$/, - use: ['style-loader', 'css-loader', { - loader: 'postcss-loader', - options: { - postcssOptions: { - sourceMap: true, - plugins: [ - [tailwindcss, { - config: path.resolve('../tailwind.config.js') - }], - autoprefixer, - ], - }, - }, - }, 'resolve-url-loader', { - loader: 'sass-loader', - options: { - sourceMap: true, - sassOptions: { - includePaths: [ - path.resolve('./node_modules'), - path.resolve('./packages/oak/node_modules'), - path.resolve('./packages/oak/lib/theme'), - path.resolve('./packages/oak-addon-basic-components/node_modules'), - path.resolve('./packages/oak-addon-richtext-field/node_modules'), - path.resolve('./packages/oak-addon-richtext-field-prosemirror/node_modules') - ], - }, - }, - }], - }); - - // CKEditor config - config.module.rules.push({ - test: /@ckeditor\/(.+)\.svg$/, - type: 'asset/source', - }); - - const cssRule = config.module.rules - .find(r => r.test.toString() === '/\\.css$/'); - cssRule.exclude = /@ckeditor/; - - config.module.rules.push({ - test: /@ckeditor\/(.+)\.css$/, - exclude: /addon-/, - use: [ - { - loader: 'style-loader', - options: { - injectType: 'singletonStyleTag', - attributes: { - 'data-cke': true, - }, - }, - }, - 'css-loader', - { - loader: 'postcss-loader', - options: { - postcssOptions: styles.getPostCssConfig({ - themeImporter: { - themePath: require.resolve('@ckeditor/ckeditor5-theme-lark'), - }, - minify: true, - }), - }, - }, - ], - }); - - return config; - }, - framework: { - name: '@storybook/react-webpack5', - options: {} - } -}; diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 000000000..da8115956 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,124 @@ +import path from 'node:path'; + +import type { RuleSetRule } from 'webpack'; +import type { StorybookConfig } from '@storybook/react-webpack5'; +import { styles } from '@ckeditor/ckeditor5-dev-utils'; + +const config: StorybookConfig = { + stories: [ + '../packages/react/lib/index.stories.{js,tsx}', + '../packages/*/lib/**/*.stories.{js,tsx}', + ], + addons: [ + '@storybook/addon-storysource', + '@storybook/addon-actions', + '@storybook/addon-themes', + { + name: '@storybook/addon-styling-webpack', + options: { + rules: [ + { + test: /\.sass$/, + use: [ + 'style-loader', + 'css-loader', + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + 'autoprefixer', + 'tailwindcss', + ], + }, + }, + }, + 'sass-loader', + ], + }, + ], + }, + }, + '@storybook/addon-webpack5-compiler-swc', + ], + framework: '@storybook/react-webpack5', + webpackFinal: config => { + config.resolve = config.resolve || {}; + config.resolve.alias = { + '@oakjs/core': path.resolve('./packages/core/lib'), + '@oakjs/react': path.resolve('./packages/react/lib'), + '@oakjs/ckeditor5-build-custom': path.resolve('./packages/ckeditor5-build-custom/lib'), + '@poool/oak/lib': path.resolve('./packages/oak/lib'), + '@poool/oak': path.resolve('./packages/oak/lib') + }; + + // CKEditor config + config.module = config.module || {}; + config.module.rules = config.module.rules || []; + config.module.rules.push({ + test: /@ckeditor\/(.+)\.svg$/, + type: 'asset/source', + }); + + // @ts-ignore webpack is weird bro + const cssRule: RuleSetRule = config.module.rules.find((r => ( + r && (r as RuleSetRule).test && + (r as RuleSetRule).test?.toString() === '/\\.css$/' + ))) || ({} as RuleSetRule); + cssRule.exclude = /@ckeditor/; + + config.module.rules.push({ + test: /@ckeditor\/(.+)\.css$/, + exclude: /addon-/, + use: [ + { + loader: 'style-loader', + options: { + injectType: 'singletonStyleTag', + attributes: { + 'data-cke': true, + }, + }, + }, + 'css-loader', + { + loader: 'postcss-loader', + options: { + postcssOptions: styles.getPostCssConfig({ + themeImporter: { + themePath: require.resolve('@ckeditor/ckeditor5-theme-lark'), + }, + minify: true, + }), + }, + }, + ], + }); + + return config; + }, + swc: config => ({ + ...config, + jsc: { + ...config.jsc, + transform: { + ...config.jsc?.transform, + react: { + ...config.jsc?.transform?.react, + runtime: 'automatic', + }, + }, + parser: { + ...config.jsc?.parser, + syntax: 'typescript', + tsx: true, + jsx: true, + }, + }, + }), + typescript: { + reactDocgen: 'react-docgen-typescript', + }, +}; + +export default config; diff --git a/.storybook/preview.js b/.storybook/preview.js deleted file mode 100644 index abe1e4024..000000000 --- a/.storybook/preview.js +++ /dev/null @@ -1,13 +0,0 @@ -import { themes } from '@storybook/theming'; - -import './index.sass'; - -export const parameters = { - darkMode: { - dark: { ...themes.dark, appBg: '#181818', appContentBg: '#1A1A1A' }, - darkClass: 'dark', - light: { ...themes.light, appBg: '#FEFEFE', appContentBg: '#FEFEFE' }, - classTarget: 'html', - stylePreview: true, - }, -}; diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 000000000..8c9e21537 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1 @@ +import './index.sass'; diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index e299bf109..000000000 --- a/babel.config.js +++ /dev/null @@ -1,10 +0,0 @@ -// Only used by eslint -module.exports = { - presets: [ - '@babel/env', - ['@babel/react', { - runtime: 'automatic', - }], - ], - plugins: [], -}; diff --git a/package.json b/package.json index bffbafe87..4737015a4 100644 --- a/package.json +++ b/package.json @@ -7,73 +7,85 @@ "scripts": { "lerna": "lerna", "changelog": "lerna-changelog", - "prepack": "yarn lint && yarn build && yarn test", - "lint": "eslint --max-warnings=0 . && tsc --noEmit", - "unlink-all": "lerna exec --parallel -- yarn unlink", - "link-all": "lerna exec --parallel -- yarn link", - "test": "BABEL_ENV=tests jest", - "build": "lerna exec -- npm run build", + "prepack": "yarn lint && yarn tsc && yarn build && yarn test", + "lint": "eslint --max-warnings=0 .", + "test": "jest", + "build": "lerna exec -- yarn build", "storybook": "storybook", "dev": "yarn storybook dev", - "serve": "yarn dev" + "serve": "yarn dev", + "p:core": "yarn workspace @oakjs/core", + "p:react": "yarn workspace @oakjs/react", + "p:ckeditor": "yarn workspace @oakjs/ckeditor5-build-custom", + "p:addon-ckeditor": "yarn workspace @oakjs/addon-ckeditor5-react", + "p:addon-remirror": "yarn workspace @oakjs/addon-remirror", + "p:theme": "yarn workspace @oakjs/theme", + "p:strapi": "yarn workspace @oakjs/strapi-plugin" }, "devDependencies": { - "@babel/core": "7.24.6", - "@babel/eslint-parser": "7.24.6", - "@babel/eslint-plugin": "7.24.6", - "@babel/plugin-transform-runtime": "7.24.6", - "@babel/preset-env": "7.24.6", - "@babel/preset-react": "7.24.6", - "@ckeditor/ckeditor5-alignment": "40.2.0", - "@ckeditor/ckeditor5-basic-styles": "40.2.0", - "@ckeditor/ckeditor5-block-quote": "40.2.0", - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-dev-translations": "39.6.1", - "@ckeditor/ckeditor5-dev-utils": "39.6.1", - "@ckeditor/ckeditor5-editor-classic": "40.2.0", - "@ckeditor/ckeditor5-essentials": "40.2.0", - "@ckeditor/ckeditor5-font": "40.2.0", - "@ckeditor/ckeditor5-horizontal-line": "40.2.0", - "@ckeditor/ckeditor5-indent": "40.2.0", - "@ckeditor/ckeditor5-link": "40.2.0", - "@ckeditor/ckeditor5-list": "40.2.0", - "@ckeditor/ckeditor5-paragraph": "40.2.0", - "@ckeditor/ckeditor5-react": "6.2.0", - "@ckeditor/ckeditor5-remove-format": "40.2.0", - "@ckeditor/ckeditor5-table": "40.2.0", - "@ckeditor/ckeditor5-theme-lark": "40.2.0", - "@ckeditor/ckeditor5-typing": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@junipero/core": "3.4.13", - "@junipero/tailwind-plugin": "3.4.11", - "@percy/storybook": "5.0.3", + "@babel/eslint-plugin": "7.24.7", + "@ckeditor/ckeditor5-alignment": "41.4.2", + "@ckeditor/ckeditor5-basic-styles": "41.4.2", + "@ckeditor/ckeditor5-block-quote": "41.4.2", + "@ckeditor/ckeditor5-core": "41.4.2", + "@ckeditor/ckeditor5-dev-translations": "40.2.1", + "@ckeditor/ckeditor5-dev-utils": "40.2.1", + "@ckeditor/ckeditor5-editor-classic": "41.4.2", + "@ckeditor/ckeditor5-editor-multi-root": "41.4.2", + "@ckeditor/ckeditor5-engine": "41.4.2", + "@ckeditor/ckeditor5-essentials": "41.4.2", + "@ckeditor/ckeditor5-font": "41.4.2", + "@ckeditor/ckeditor5-horizontal-line": "41.4.2", + "@ckeditor/ckeditor5-indent": "41.4.2", + "@ckeditor/ckeditor5-link": "41.4.2", + "@ckeditor/ckeditor5-list": "41.4.2", + "@ckeditor/ckeditor5-paragraph": "41.4.2", + "@ckeditor/ckeditor5-react": "7.0.0", + "@ckeditor/ckeditor5-remove-format": "41.4.2", + "@ckeditor/ckeditor5-table": "41.4.2", + "@ckeditor/ckeditor5-theme-lark": "41.4.2", + "@ckeditor/ckeditor5-typing": "41.4.2", + "@ckeditor/ckeditor5-ui": "41.4.2", + "@ckeditor/ckeditor5-utils": "41.4.2", + "@ckeditor/ckeditor5-watchdog": "41.4.2", + "@junipero/core": "3.5.0", + "@junipero/tailwind-plugin": "3.5.0", + "@percy/storybook": "6.0.0", "@poool/eslint-config": "3.0.1", "@poool/eslint-config-react": "3.0.1", "@poool/eslint-plugin": "3.0.0", "@remirror/pm": "2.0.8", "@remirror/react": "2.0.35", "@rollup/plugin-alias": "5.1.0", - "@rollup/plugin-babel": "6.0.4", - "@rollup/plugin-commonjs": "25.0.8", + "@rollup/plugin-commonjs": "26.0.1", "@rollup/plugin-node-resolve": "15.2.3", + "@rollup/plugin-swc": "0.3.1", "@rollup/plugin-terser": "0.4.4", - "@storybook/addon-actions": "7.6.19", - "@storybook/react": "7.6.19", - "@storybook/react-webpack5": "7.6.19", - "@storybook/theming": "7.6.19", + "@storybook/addon-actions": "8.1.6", + "@storybook/addon-storysource": "8.1.6", + "@storybook/addon-styling-webpack": "1.0.0", + "@storybook/addon-themes": "8.1.6", + "@storybook/addon-webpack5-compiler-swc": "1.0.3", + "@storybook/react": "8.1.6", + "@storybook/react-webpack5": "8.1.6", + "@storybook/theming": "8.1.6", + "@swc/core": "1.5.25", + "@swc/jest": "0.2.36", + "@testing-library/dom": "10.1.0", "@testing-library/jest-dom": "6.4.5", - "@testing-library/react": "14.3.1", - "@typescript-eslint/parser": "7.11.0", + "@testing-library/react": "16.0.0", + "@types/jest": "29.5.12", + "@types/node": "20.14.2", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/parser": "7.12.0", "autoprefixer": "10.4.19", - "babel-jest": "29.7.0", - "babel-loader": "9.1.3", - "ckeditor5": "40.2.0", + "ckeditor5": "41.4.2", "css-loader": "7.1.2", "eslint": "8.57.0", "eslint-config-standard": "17.1.0", - "eslint-plugin-babel": "5.3.1", "eslint-plugin-import": "2.29.1", - "eslint-plugin-n": "17.7.0", + "eslint-plugin-n": "17.8.1", "eslint-plugin-promise": "6.2.0", "eslint-plugin-react": "7.34.2", "jest": "29.7.0", @@ -84,21 +96,18 @@ "lerna-changelog": "2.2.0", "postcss": "8.4.38", "postcss-loader": "8.1.1", - "postcss-url": "10.1.3", - "react": "18.2.0", - "react-dom": "18.2.0", - "react-popper": "2.3.0", + "react": "18.3.1", + "react-dom": "18.3.1", "remirror": "2.0.39", - "resolve-url-loader": "5.0.0", "rollup": "4.18.0", - "rollup-plugin-dts": "6.1.1", "rollup-plugin-postcss": "4.0.2", - "sass": "1.77.2", + "sass": "1.77.4", "sass-loader": "14.2.1", - "storybook": "7.6.19", - "storybook-dark-mode": "3.0.3", - "style-loader": "3.3.4", - "tailwindcss": "3.4.3", + "storybook": "8.1.6", + "storybook-dark-mode": "4.0.1", + "style-loader": "4.0.0", + "swc-loader": "0.2.6", + "tailwindcss": "3.4.4", "terser-webpack-plugin": "5.3.10", "typescript": "5.4.5", "webpack": "5.91.0", diff --git a/packages/addon-ckeditor5-react/.browserslistrc b/packages/addon-ckeditor5-react/.browserslistrc index 5601a3b5b..cf8634949 100644 --- a/packages/addon-ckeditor5-react/.browserslistrc +++ b/packages/addon-ckeditor5-react/.browserslistrc @@ -1,5 +1,5 @@ >=0.5% -node >= 14 +node >= 18 not ie >= 0 not ie_mob >= 0 not dead diff --git a/packages/addon-ckeditor5-react/babel.config.js b/packages/addon-ckeditor5-react/babel.config.js deleted file mode 100644 index 4595c67be..000000000 --- a/packages/addon-ckeditor5-react/babel.config.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - presets: [ - ['@babel/env', { - corejs: 3, - useBuiltIns: 'usage', - }], - ['@babel/react', { - runtime: 'automatic', - }], - ], - plugins: [ - ['@babel/transform-runtime', { - corejs: 3, - }], - ], -}; diff --git a/packages/addon-ckeditor5-react/lib/Field/index.d.ts b/packages/addon-ckeditor5-react/lib/Field/index.d.ts deleted file mode 100644 index c879bf97f..000000000 --- a/packages/addon-ckeditor5-react/lib/Field/index.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ComponentPropsWithoutRef, ReactNode } from 'react'; -import { EditorConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig'; -import { Editor } from '@ckeditor/ckeditor5-core'; -import { EventInfo } from 'ckeditor5/src/utils'; - -export declare interface CKEditorFieldProps - extends ComponentPropsWithoutRef { - className?: string, - editor?: Editor, - config?: EditorConfig, - value?: string, - onChange: (event: EventInfo, editor: Editor) => void; -} - -declare function CKEditorField( - props: CKEditorFieldProps -): ReactNode | JSX.Element; - -export default CKEditorField; diff --git a/packages/addon-ckeditor5-react/lib/Field/index.js b/packages/addon-ckeditor5-react/lib/Field/index.js deleted file mode 100644 index 3a7d1a243..000000000 --- a/packages/addon-ckeditor5-react/lib/Field/index.js +++ /dev/null @@ -1,24 +0,0 @@ -import { useCallback } from 'react'; -import { CKEditor } from '@ckeditor/ckeditor5-react'; -import { classNames } from '@oakjs/react'; - -const CKEditorField = ({ className, editor, config, value, onChange }) => { - const onChange_ = useCallback((_, ed) => { - onChange({ value: ed.getData() }); - }, []); - - return ( -
- -
- ); -}; - -CKEditorField.displayName = 'CKEditorField'; - -export default CKEditorField; diff --git a/packages/addon-ckeditor5-react/lib/Field/index.tsx b/packages/addon-ckeditor5-react/lib/Field/index.tsx new file mode 100644 index 000000000..a35007552 --- /dev/null +++ b/packages/addon-ckeditor5-react/lib/Field/index.tsx @@ -0,0 +1,45 @@ +import type { EditorConfig } from '@ckeditor/ckeditor5-core'; +import type { EventInfo } from '@ckeditor/ckeditor5-utils'; +import type { Editor } from '@oakjs/ckeditor5-build-custom'; +import { type ComponentPropsWithoutRef, useCallback } from 'react'; +import { CKEditor } from '@ckeditor/ckeditor5-react'; +import { + type FieldContent, + type ElementObject, + classNames, +} from '@oakjs/react'; + +export interface CKEditorFieldProps extends ComponentPropsWithoutRef { + editor?: Editor; + config?: EditorConfig; + value?: string; + onChange?(field: FieldContent, element?: ElementObject): void; +} + +const CKEditorField = ({ + className, + editor, + config, + value, + onChange, +}: CKEditorFieldProps) => { + const onChange_ = useCallback((_: EventInfo, ed: Editor) => { + onChange?.({ value: ed.getData() }); + }, []); + + return ( +
+ +
+ ); +}; + +CKEditorField.displayName = 'CKEditorField'; + +export default CKEditorField; diff --git a/packages/addon-ckeditor5-react/lib/addons.d.ts b/packages/addon-ckeditor5-react/lib/addons.d.ts deleted file mode 100644 index bd85630a9..000000000 --- a/packages/addon-ckeditor5-react/lib/addons.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Editor } from '@ckeditor/ckeditor5-core'; -import { EditorConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig'; - -declare interface ckeditorFieldType { - type: 'ckeditor', - render: () => any, - [_: string]: any, - props: { - editor: Editor - config: EditorConfig - } -} - -declare interface ckeditorFieldProps { - config?: EditorConfig, - editor?: Editor, - [_: string]: any -} - -declare interface ckeditorFieldAddonType { - fields: Array, -} - -declare function ckeditorField( - props: ckeditorFieldProps -): ckeditorFieldType; - -declare function ckeditorFieldAddon( - props: ckeditorFieldProps -): ckeditorFieldAddonType; - -export { - ckeditorField, - ckeditorFieldAddon, - ckeditorFieldType, - ckeditorFieldProps, - ckeditorFieldAddonType, -}; diff --git a/packages/addon-ckeditor5-react/lib/addons.js b/packages/addon-ckeditor5-react/lib/addons.ts similarity index 73% rename from packages/addon-ckeditor5-react/lib/addons.js rename to packages/addon-ckeditor5-react/lib/addons.ts index 1ea7b29c3..750f8e521 100644 --- a/packages/addon-ckeditor5-react/lib/addons.js +++ b/packages/addon-ckeditor5-react/lib/addons.ts @@ -1,11 +1,17 @@ +import type { ReactFieldObject, AddonObject } from '@oakjs/react'; +import { omit } from '@oakjs/react'; import ClassicEditor from '@oakjs/ckeditor5-build-custom'; -import Field from './Field'; +import Field, { CKEditorFieldProps } from './Field'; -export const ckeditorField = ({ config, editor, ...props } = {}) => ({ +export const ckeditorField = ({ + config, + editor, + ...props +}: CKEditorFieldProps = {}): ReactFieldObject => ({ type: 'ckeditor', render: Field, - ...props, + ...omit(props, ['onChange']), props: { editor: editor || ClassicEditor, config: { @@ -53,6 +59,8 @@ export const ckeditorField = ({ config, editor, ...props } = {}) => ({ }, }); -export const ckeditorFieldAddon = props => ({ +export const ckeditorFieldAddon = ( + props?: CKEditorFieldProps +): AddonObject => ({ fields: [ckeditorField(props)], }); diff --git a/packages/addon-ckeditor5-react/lib/index.d.ts b/packages/addon-ckeditor5-react/lib/index.d.ts deleted file mode 100644 index a627b897b..000000000 --- a/packages/addon-ckeditor5-react/lib/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { - default as CKEditorField, - CKEditorFieldProps, -} from './Field'; - -export { - ckeditorField, - ckeditorFieldAddon, - ckeditorFieldAddonType, - ckeditorFieldProps, - ckeditorFieldType, -} from './addons'; diff --git a/packages/addon-ckeditor5-react/lib/index.js b/packages/addon-ckeditor5-react/lib/index.js deleted file mode 100644 index 81a13e128..000000000 --- a/packages/addon-ckeditor5-react/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './addons'; diff --git a/packages/addon-ckeditor5-react/lib/index.sass b/packages/addon-ckeditor5-react/lib/index.sass index f4d728a93..f9ab1d6c4 100644 --- a/packages/addon-ckeditor5-react/lib/index.sass +++ b/packages/addon-ckeditor5-react/lib/index.sass @@ -35,11 +35,16 @@ --border-color: var(--oak-main-color) /* Toolbar */ + .ck-editor__top + & > .ck-sticky-panel > .ck-sticky-panel__content + border: none + .ck-toolbar border: none box-shadow: 0 0 0 2px var(--border-color) padding-bottom: 0 background: var(--background-color) + border-radius: var(--ck-border-radius) var(--ck-border-radius) 0 0 .ck-button.ck-insert-table-dropdown-grid-box --ck-border-radius: 2px @@ -55,7 +60,7 @@ .ck-dropdown .ck-button - border-radius: var(--ck-border-radius) + border-radius: var(--ck-border-radius) .ck-dropdown__panel border: none diff --git a/packages/addon-ckeditor5-react/lib/index.stories.js b/packages/addon-ckeditor5-react/lib/index.stories.tsx similarity index 78% rename from packages/addon-ckeditor5-react/lib/index.stories.js rename to packages/addon-ckeditor5-react/lib/index.stories.tsx index bb4a5b92a..b3662cf29 100644 --- a/packages/addon-ckeditor5-react/lib/index.stories.js +++ b/packages/addon-ckeditor5-react/lib/index.stories.tsx @@ -1,15 +1,20 @@ import { action } from '@storybook/addon-actions'; -import { Builder, baseAddon } from '@oakjs/react'; +import { + type ElementObject, + type AddonObject, + Builder, + baseAddon, +} from '@oakjs/react'; import { ckeditorFieldAddon } from './addons'; export default { title: 'React/With addon: CKEditor' }; -const baseContent = [ +const baseContent: ElementObject = [ { type: 'text', content: 'This is a title' }, ]; -const addon = { +const addon: AddonObject = { overrides: [{ type: 'component', targets: ['text', 'title', 'button'], diff --git a/packages/addon-ckeditor5-react/lib/index.ts b/packages/addon-ckeditor5-react/lib/index.ts new file mode 100644 index 000000000..41a22da1c --- /dev/null +++ b/packages/addon-ckeditor5-react/lib/index.ts @@ -0,0 +1,6 @@ +export type { + default as CKEditorField, + CKEditorFieldProps, +} from './Field'; + +export * from './addons'; diff --git a/packages/addon-ckeditor5-react/package.json b/packages/addon-ckeditor5-react/package.json index f80a4bfe7..cb6474834 100644 --- a/packages/addon-ckeditor5-react/package.json +++ b/packages/addon-ckeditor5-react/package.json @@ -3,12 +3,8 @@ "version": "3.5.5", "description": "🌳 Modern, lightweight & modulable page builder", "main": "dist/oak-addon-ckeditor.cjs.js", - "jsnext:main": "dist/esm/index.js", "module": "dist/esm/index.js", - "esnext": "src/index.js", - "unpkg": "dist/oak-addon-ckeditor.min.js", - "cdn": "dist/oak-addon-ckeditor.min.js", - "types": "dist/oak-addon-ckeditor.d.ts", + "types": "dist/types/index.d.ts", "repository": { "type": "git", "url": "https://github.com/p3ol/oak.git", @@ -18,23 +14,26 @@ "license": "MIT", "sideEffects": false, "engines": { - "node": ">= 16", - "npm": ">= 8" + "node": ">= 18" }, "peerDependencies": { - "@ckeditor/ckeditor5-react": "^6.0.0", - "@oakjs/ckeditor5-build-custom": "^3.0.0", + "@ckeditor/ckeditor5-react": "^6.0.0 || ^7.0.0", "@oakjs/react": "^3.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" }, "dependencies": { - "@babel/runtime-corejs3": "7.24.6", - "core-js": "3.37.1" + "@oakjs/ckeditor5-build-custom": "^3.0.0" + }, + "devDependencies": { + "@oakjs/ckeditor5-build-custom": "workspace:*", + "@oakjs/react": "workspace:*" }, "scripts": { "clean": "rm -rf ./dist || true", - "build": "yarn clean && rollup -c", + "build": "yarn clean && yarn build:code && yarn build:dts", + "build:code": "yarn run -T rollup --configPlugin @rollup/plugin-swc -c", + "build:dts": "yarn run -T tsc --project ./tsconfig.build.json", "test": "jest" }, "publishConfig": { @@ -43,11 +42,13 @@ "exports": { ".": { "import": "./dist/esm/index.js", - "require": "./dist/oak-addon-ckeditor.cjs.js" + "require": "./dist/oak-addon-ckeditor.cjs.js", + "types": "./dist/types/index.d.ts" }, "./addons": { "import": "./dist/esm/addons.js", - "require": "./dist/oak-addon-ckeditor.cjs.js" + "require": "./dist/oak-addon-ckeditor.cjs.js", + "types": "./dist/types/addons.d.ts" }, "./dist/*": "./dist/*" } diff --git a/packages/addon-ckeditor5-react/rollup.config.mjs b/packages/addon-ckeditor5-react/rollup.config.ts similarity index 72% rename from packages/addon-ckeditor5-react/rollup.config.mjs rename to packages/addon-ckeditor5-react/rollup.config.ts index 67dfcda38..863950ca0 100644 --- a/packages/addon-ckeditor5-react/rollup.config.mjs +++ b/packages/addon-ckeditor5-react/rollup.config.ts @@ -1,18 +1,18 @@ -import path from 'path'; +import path from 'node:path'; -import babel from '@rollup/plugin-babel'; +import type { ModuleFormat, Plugin, RollupOptions } from 'rollup'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; +import terser from '@rollup/plugin-terser'; import postcss from 'rollup-plugin-postcss'; +import swc from '@rollup/plugin-swc'; import autoprefixer from 'autoprefixer'; -import terser from '@rollup/plugin-terser'; import tailwindcss from 'tailwindcss'; -import { dts } from 'rollup-plugin-dts'; -const input = './lib/index.js'; +const input = './lib/index.ts'; const defaultOutput = './dist'; const name = 'oak-addon-ckeditor'; -const formats = ['umd', 'cjs', 'esm']; +const formats: ModuleFormat[] = ['umd', 'cjs', 'esm']; const defaultExternals = [ 'react', 'react-dom', '@oakjs/react', '@oakjs/ckeditor5-build-custom', @@ -27,26 +27,41 @@ const defaultGlobals = { '@oakjs/ckeditor5-build-custom': 'ClassicEditor', }; -const defaultPlugins = [ - babel({ - exclude: /node_modules/, - babelHelpers: 'runtime', +const defaultPlugins: Plugin[] = [ + swc({ + swc: { + jsc: { + transform: { + react: { + runtime: 'automatic', + }, + }, + parser: { + syntax: 'typescript', + tsx: true, + }, + }, + }, }), resolve({ rootDir: path.resolve('../../'), + extensions: ['.ts', '.tsx', '.js', '.jsx'], }), commonjs({ include: /node_modules/, - requireReturnsDefault: 'auto', }), terser(), ]; -const getConfig = (format, { +const getConfig = (format: ModuleFormat, { output = defaultOutput, external = defaultExternals, globals = defaultGlobals, -} = {}) => ({ +}: { + output?: string; + external?: string[]; + globals?: Record; +} = {}): RollupOptions => ({ input, plugins: [ ...defaultPlugins, @@ -76,7 +91,7 @@ const getConfig = (format, { }, }); -export default [ +const config: RollupOptions[] = [ ...formats.map(f => getConfig(f)), { input: './lib/index.sass', @@ -95,10 +110,12 @@ export default [ path.resolve('../../node_modules'), ], }, + stylus: {}, + less: {}, }, plugins: [ - tailwindcss({ config: path.resolve('../../tailwind.config.js') }), - autoprefixer({ env: process.env.BROWSERSLIST_ENV }), + autoprefixer, + tailwindcss({ config: path.resolve('../../tailwind.config.ts') }), ], }), ], @@ -113,9 +130,6 @@ export default [ warn(warning); }, }, - { - input: './lib/index.d.ts', - output: [{ file: `dist/${name}.d.ts`, format: 'es' }], - plugins: [dts()], - }, ]; + +export default config; diff --git a/packages/addon-ckeditor5-react/tsconfig.build.json b/packages/addon-ckeditor5-react/tsconfig.build.json new file mode 100644 index 000000000..dd7c4a73a --- /dev/null +++ b/packages/addon-ckeditor5-react/tsconfig.build.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "noEmit": false, + "emitDeclarationOnly": true, + "outDir": "dist/types", + "baseUrl": "." + }, + "exclude": [ + "tests", + "**/*.test.ts", + "**/*.test.tsx", + "rollup.config.ts", + "**/*.stories.tsx", + ] +} diff --git a/packages/addon-ckeditor5-react/tsconfig.json b/packages/addon-ckeditor5-react/tsconfig.json new file mode 100644 index 000000000..9a11edc9a --- /dev/null +++ b/packages/addon-ckeditor5-react/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./lib/**/*"], +} diff --git a/packages/addon-remirror/.browserslistrc b/packages/addon-remirror/.browserslistrc index 5601a3b5b..cf8634949 100644 --- a/packages/addon-remirror/.browserslistrc +++ b/packages/addon-remirror/.browserslistrc @@ -1,5 +1,5 @@ >=0.5% -node >= 14 +node >= 18 not ie >= 0 not ie_mob >= 0 not dead diff --git a/packages/addon-remirror/babel.config.js b/packages/addon-remirror/babel.config.js deleted file mode 100644 index 4595c67be..000000000 --- a/packages/addon-remirror/babel.config.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - presets: [ - ['@babel/env', { - corejs: 3, - useBuiltIns: 'usage', - }], - ['@babel/react', { - runtime: 'automatic', - }], - ], - plugins: [ - ['@babel/transform-runtime', { - corejs: 3, - }], - ], -}; diff --git a/packages/addon-remirror/lib/Field/ColorButton.js b/packages/addon-remirror/lib/Field/ColorButton.tsx similarity index 73% rename from packages/addon-remirror/lib/Field/ColorButton.js rename to packages/addon-remirror/lib/Field/ColorButton.tsx index 5068d3dfc..27598a4b6 100644 --- a/packages/addon-remirror/lib/Field/ColorButton.js +++ b/packages/addon-remirror/lib/Field/ColorButton.tsx @@ -1,20 +1,23 @@ import { useEffect, useRef, useState } from 'react'; import { + type ColorFieldRef, + type FieldContent, + type DropdownProps, ColorField, Dropdown, DropdownMenu, DropdownToggle, Text, - /*Icon, */ } from '@oakjs/react'; import { useActive, useChainedCommands, useCommands } from '@remirror/react'; -import MenuButton from './MenuButton'; +import type { Extensions } from '../types'; +import MenuButton, { type MenuButtonProps } from './MenuButton'; -const ColorButton = ({ children }) => { - const fieldRef = useRef(); +const ColorButton = (props: MenuButtonProps) => { + const fieldRef = useRef(); const { setTextColor } = useCommands(); - const { textColor } = useActive(); + const { textColor } = useActive(); const chain = useChainedCommands(); const [opened, setOpened] = useState(false); const [color, setColor] = useState('#000000'); @@ -27,11 +30,11 @@ const ColorButton = ({ children }) => { } }, [opened]); - const onToggle = ({ opened: o }) => { + const onToggle: DropdownProps['onToggle'] = ({ opened: o }) => { setOpened(o); }; - const onChange = field => { + const onChange = (field: FieldContent) => { setColor(field.value); chain.setTextColor(field.value).run(); }; @@ -41,7 +44,7 @@ const ColorButton = ({ children }) => { setTextColor?.enabled(color)} isActive={textColor} onClick={() => fieldRef.current?.open()} className="color-button oak-inline-flex oak-items-center" @@ -50,9 +53,8 @@ const ColorButton = ({ children }) => { Color )} - > - { children } - + { ...props } + /> diff --git a/packages/addon-remirror/lib/Field/LinkButton.js b/packages/addon-remirror/lib/Field/LinkButton.tsx similarity index 74% rename from packages/addon-remirror/lib/Field/LinkButton.js rename to packages/addon-remirror/lib/Field/LinkButton.tsx index 328608314..477be5217 100644 --- a/packages/addon-remirror/lib/Field/LinkButton.js +++ b/packages/addon-remirror/lib/Field/LinkButton.tsx @@ -1,3 +1,4 @@ +import type { LinkAttributes } from 'remirror/extensions'; import { useCallback, useState } from 'react'; import { Dropdown, @@ -7,20 +8,23 @@ import { Toggle, Label, Text, - /*Icon, */ + FieldContent, } from '@oakjs/react'; import { useAttrs, useChainedCommands, useCommands } from '@remirror/react'; -import MenuButton from './MenuButton'; +import type { Extensions } from '../types'; +import MenuButton, { type MenuButtonProps } from './MenuButton'; -const LinkButton = ({ children }) => { +const LinkButton = (props: MenuButtonProps) => { const { updateLink } = useCommands(); const chain = useChainedCommands(); - const { link } = useAttrs(); - const [href, setHref] = useState(link?.()?.href || ''); - const [target, setTarget] = useState(link?.()?.target || null); + const { link } = useAttrs(); + const [href, setHref] = useState(link?.()?.href as string || ''); + const [target, setTarget] = useState< + LinkAttributes['target'] + >(link()?.target as LinkAttributes['target']); - const onUrlChange = useCallback(field => { + const onUrlChange = useCallback((field: FieldContent) => { setHref(field.value); if (!field.value) { @@ -30,7 +34,7 @@ const LinkButton = ({ children }) => { } }, [chain, target]); - const onTargetChange = useCallback(field => { + const onTargetChange = useCallback((field: FieldContent) => { const t = field.value ? '_blank' : null; setTarget(t); @@ -47,16 +51,15 @@ const LinkButton = ({ children }) => { !!updateLink} - isActive={() => link()} + isActive={() => !!link()} className="link-button oak-inline-flex oak-items-center" tooltipText={( Link )} - > - { children } - + { ...props } + /> diff --git a/packages/addon-remirror/lib/Field/Menu.js b/packages/addon-remirror/lib/Field/Menu.tsx similarity index 96% rename from packages/addon-remirror/lib/Field/Menu.js rename to packages/addon-remirror/lib/Field/Menu.tsx index 845d14166..1e03c1a88 100644 --- a/packages/addon-remirror/lib/Field/Menu.js +++ b/packages/addon-remirror/lib/Field/Menu.tsx @@ -6,6 +6,7 @@ import { } from '@remirror/react'; import { Icon } from '@oakjs/react'; +import type { Extensions } from '../types'; import MenuButton from './MenuButton'; import LinkButton from './LinkButton'; import ColorButton from './ColorButton'; @@ -22,7 +23,7 @@ const Menu = () => { centerAlign, rightAlign, justifyAlign, - } = useCommands(); + } = useCommands(); const { getFontSizeForSelection } = useHelpers(); const { bold, italic, underline, paragraph } = useActive(); @@ -32,7 +33,7 @@ const Menu = () => { if (Array.isArray(size)) { return size[0]; } else if (typeof size === 'string') { - return size.replace('px', ''); + return ('' + size).replace('px', ''); } return 16; diff --git a/packages/addon-remirror/lib/Field/MenuButton.js b/packages/addon-remirror/lib/Field/MenuButton.tsx similarity index 63% rename from packages/addon-remirror/lib/Field/MenuButton.js rename to packages/addon-remirror/lib/Field/MenuButton.tsx index a3985676a..88a849673 100644 --- a/packages/addon-remirror/lib/Field/MenuButton.js +++ b/packages/addon-remirror/lib/Field/MenuButton.tsx @@ -1,14 +1,22 @@ +import type { ComponentPropsWithoutRef, MouseEvent, ReactNode } from 'react'; import { Tooltip, classNames } from '@junipero/react'; +export interface MenuButtonProps extends ComponentPropsWithoutRef<'a'> { + onClick?: () => void; + enabled?: () => boolean; + isActive?: () => boolean; + tooltipText?: ReactNode; +} + const MenuButton = ({ onClick, enabled, isActive, className, tooltipText, - children, -}) => { - const onClick_ = e => { + ...rest +}: MenuButtonProps) => { + const onClick_ = (e: MouseEvent) => { e.preventDefault(); onClick?.(); }; @@ -20,15 +28,15 @@ const MenuButton = ({ return ( ); diff --git a/packages/addon-remirror/lib/Field/index.js b/packages/addon-remirror/lib/Field/index.js deleted file mode 100644 index c033b549d..000000000 --- a/packages/addon-remirror/lib/Field/index.js +++ /dev/null @@ -1,33 +0,0 @@ -import { classNames } from '@oakjs/react'; -import { prosemirrorNodeToHtml } from 'remirror'; -import { Remirror, EditorComponent, useRemirror } from '@remirror/react'; -import { useCallback } from 'react'; - -import Menu from './Menu'; - -const RemirrorField = ({ className, value, extensions, onChange }) => { - const { manager, setState, state } = useRemirror({ - extensions, - content: value, - stringHandler: 'html', - selection: 'end', - }); - - const onChange_ = useCallback(({ state }) => { - setState(state); - onChange({ value: prosemirrorNodeToHtml(state.doc) }); - }, [setState]); - - return ( -
- - - - -
- ); -}; - -RemirrorField.displayName = 'RemirrorField'; - -export default RemirrorField; diff --git a/packages/addon-remirror/lib/Field/index.tsx b/packages/addon-remirror/lib/Field/index.tsx new file mode 100644 index 000000000..c7f54eba2 --- /dev/null +++ b/packages/addon-remirror/lib/Field/index.tsx @@ -0,0 +1,49 @@ +import { type ComponentPropsWithoutRef, useCallback } from 'react'; +import { type FieldContent, classNames } from '@oakjs/react'; +import { type RemirrorEventListener, prosemirrorNodeToHtml } from 'remirror'; +import { Remirror, EditorComponent, useRemirror } from '@remirror/react'; + +import type { Extensions } from '../types'; +import Menu from './Menu'; + +export interface RemirrorFieldProps + extends Omit, 'onChange'> { + value?: string; + extensions?(): Extensions[]; + onChange?(field: FieldContent): void; +} + +const RemirrorField = ({ + className, + value, + extensions, + onChange, + ...rest +}: RemirrorFieldProps) => { + const { manager, setState, state } = useRemirror({ + extensions, + content: value, + stringHandler: 'html', + selection: 'end', + }); + + const onChange_ = useCallback< + RemirrorEventListener + >(({ state }) => { + setState(state); + onChange({ value: prosemirrorNodeToHtml(state.doc) }); + }, [setState]); + + return ( +
+ + + + +
+ ); +}; + +RemirrorField.displayName = 'RemirrorField'; + +export default RemirrorField; diff --git a/packages/addon-remirror/lib/addons.js b/packages/addon-remirror/lib/addons.js deleted file mode 100644 index d64220456..000000000 --- a/packages/addon-remirror/lib/addons.js +++ /dev/null @@ -1,40 +0,0 @@ -import { - BoldExtension, - FontSizeExtension, - ItalicExtension, - UnderlineExtension, - LinkExtension, - TextColorExtension, - NodeFormattingExtension, -} from 'remirror/extensions'; - -import Field from './Field'; - -export const remirrorField = ({ extensions, ...props } = {}) => ({ - type: 'remirror', - render: Field, - ...props, - props: { - extensions, - }, -}); - -export const basicExtensions = () => [ - new BoldExtension(), - new ItalicExtension(), - new UnderlineExtension(), - new LinkExtension(), - new TextColorExtension(), - new FontSizeExtension({ - defaultSize: '16px', - unit: 'px', - }), - new NodeFormattingExtension(), -]; - -export const remirrorFieldAddon = props => ({ - fields: [remirrorField({ - extensions: basicExtensions, - ...props, - })], -}); diff --git a/packages/addon-remirror/lib/addons.ts b/packages/addon-remirror/lib/addons.ts new file mode 100644 index 000000000..8b252801f --- /dev/null +++ b/packages/addon-remirror/lib/addons.ts @@ -0,0 +1,46 @@ +import type { AddonObject, ReactFieldObject } from '@oakjs/react'; +import { omit } from '@oakjs/react'; +import { + BoldExtension, + FontSizeExtension, + ItalicExtension, + UnderlineExtension, + LinkExtension, + TextColorExtension, + NodeFormattingExtension, +} from 'remirror/extensions'; + +import type { Extensions } from './types'; +import Field, { type RemirrorFieldProps } from './Field'; + +export const remirrorField = ({ + extensions, + ...props +}: RemirrorFieldProps = {}): ReactFieldObject => ({ + type: 'remirror', + render: Field, + ...omit(props, ['onChange']), + props: { + extensions, + }, +}); + +export const basicExtensions = (): Extensions[] => [ + new BoldExtension({}), + new ItalicExtension(), + new UnderlineExtension(), + new LinkExtension({}), + new TextColorExtension({}), + new FontSizeExtension({ + defaultSize: '16px', + unit: 'px', + }), + new NodeFormattingExtension({}), +]; + +export const remirrorFieldAddon = (props?: ReactFieldObject): AddonObject => ({ + fields: [remirrorField({ + extensions: basicExtensions, + ...props, + })], +}); diff --git a/packages/addon-remirror/lib/index.js b/packages/addon-remirror/lib/index.js deleted file mode 100644 index 735f91b4d..000000000 --- a/packages/addon-remirror/lib/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { default as RemirrorField } from './Field'; - -export * from './addons'; diff --git a/packages/addon-remirror/lib/index.stories.js b/packages/addon-remirror/lib/index.stories.tsx similarity index 78% rename from packages/addon-remirror/lib/index.stories.js rename to packages/addon-remirror/lib/index.stories.tsx index 6ba75baff..3b2cbec7b 100644 --- a/packages/addon-remirror/lib/index.stories.js +++ b/packages/addon-remirror/lib/index.stories.tsx @@ -1,15 +1,20 @@ import { action } from '@storybook/addon-actions'; -import { Builder, baseAddon } from '@oakjs/react'; +import { + type ElementObject, + type AddonObject, + Builder, + baseAddon, +} from '@oakjs/react'; import { remirrorFieldAddon } from './addons'; export default { title: 'React/With addon: Remirror' }; -const baseContent = [ +const baseContent: ElementObject[] = [ { type: 'text', content: 'This is a title' }, ]; -const addon = { +const addon: AddonObject = { overrides: [{ type: 'component', targets: ['text', 'title', 'button'], diff --git a/packages/addon-remirror/lib/index.ts b/packages/addon-remirror/lib/index.ts new file mode 100644 index 000000000..c5cb3149d --- /dev/null +++ b/packages/addon-remirror/lib/index.ts @@ -0,0 +1,8 @@ +export { + default as RemirrorField, + type RemirrorFieldProps, +} from './Field'; + +export * from './addons'; + +export type * from './types'; diff --git a/packages/addon-remirror/lib/types.ts b/packages/addon-remirror/lib/types.ts new file mode 100644 index 000000000..2293a9b21 --- /dev/null +++ b/packages/addon-remirror/lib/types.ts @@ -0,0 +1,18 @@ +import type { + BoldExtension, + FontSizeExtension, + ItalicExtension, + LinkExtension, + NodeFormattingExtension, + TextColorExtension, + UnderlineExtension, +} from 'remirror/extensions'; + +export type Extensions = + | BoldExtension + | ItalicExtension + | UnderlineExtension + | LinkExtension + | TextColorExtension + | FontSizeExtension + | NodeFormattingExtension; diff --git a/packages/addon-remirror/package.json b/packages/addon-remirror/package.json index e07e14ac2..e19194860 100644 --- a/packages/addon-remirror/package.json +++ b/packages/addon-remirror/package.json @@ -3,11 +3,8 @@ "version": "3.5.5", "description": "🌳 Modern, lightweight & modulable page builder", "main": "dist/oak-addon-remirror.cjs.js", - "jsnext:main": "dist/esm/index.js", "module": "dist/esm/index.js", - "esnext": "src/index.js", - "unpkg": "dist/oak-addon-remirror.min.js", - "cdn": "dist/oak-addon-remirror.min.js", + "types": "dist/types/index.d.ts", "repository": { "type": "git", "url": "https://github.com/p3ol/oak.git", @@ -17,8 +14,7 @@ "license": "MIT", "sideEffects": false, "engines": { - "node": ">= 16", - "npm": ">= 8" + "node": ">= 18" }, "peerDependencies": { "@oakjs/react": "^3.0.0", @@ -28,13 +24,14 @@ "react-dom": "^18.0.0", "remirror": "^2.0.0" }, - "dependencies": { - "@babel/runtime-corejs3": "7.24.6", - "core-js": "3.37.1" + "devDependencies": { + "@oakjs/react": "workspace:*" }, "scripts": { "clean": "rm -rf ./dist || true", - "build": "yarn clean && rollup -c", + "build": "yarn clean && yarn build:code && yarn build:dts", + "build:code": "yarn run -T rollup --configPlugin @rollup/plugin-swc -c", + "build:dts": "yarn run -T tsc --project ./tsconfig.build.json", "test": "jest" }, "publishConfig": { diff --git a/packages/addon-remirror/rollup.config.mjs b/packages/addon-remirror/rollup.config.ts similarity index 70% rename from packages/addon-remirror/rollup.config.mjs rename to packages/addon-remirror/rollup.config.ts index f760333ec..de38f551c 100644 --- a/packages/addon-remirror/rollup.config.mjs +++ b/packages/addon-remirror/rollup.config.ts @@ -1,21 +1,23 @@ -import path from 'path'; +import path from 'node:path'; -import babel from '@rollup/plugin-babel'; +import type { ModuleFormat, Plugin, RollupOptions } from 'rollup'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import postcss from 'rollup-plugin-postcss'; +import swc from '@rollup/plugin-swc'; import autoprefixer from 'autoprefixer'; import terser from '@rollup/plugin-terser'; -const input = './lib/index.js'; +const input = './lib/index.ts'; const defaultOutput = './dist'; const name = 'oak-addon-remirror'; -const formats = ['umd', 'cjs', 'esm']; +const formats: ModuleFormat[] = ['umd', 'cjs', 'esm']; const defaultExternals = [ 'react', 'react-dom', '@oakjs/react', '@remirror/core', '@remirror/react', '@remirror/pm', 'remirror', 'remirror/extensions', ]; + const defaultGlobals = { react: 'React', 'react-dom': 'ReactDOM', @@ -27,23 +29,41 @@ const defaultGlobals = { 'remirror/extensions': 'RemirrorExtensions', }; -const defaultPlugins = [ - babel({ - exclude: /node_modules/, - babelHelpers: 'runtime', +const defaultPlugins: Plugin[] = [ + swc({ + swc: { + jsc: { + transform: { + react: { + runtime: 'automatic', + }, + }, + parser: { + syntax: 'typescript', + tsx: true, + }, + }, + }, }), resolve({ rootDir: path.resolve('../../'), + extensions: ['.ts', '.tsx', '.js', '.jsx'], + }), + commonjs({ + include: /node_modules/, }), - commonjs(), terser(), ]; -const getConfig = (format, { +const getConfig = (format: ModuleFormat, { output = defaultOutput, external = defaultExternals, globals = defaultGlobals, -} = {}) => ({ +}: { + output?: string; + external?: string[]; + globals?: Record; +} = {}): RollupOptions => ({ input, plugins: [ ...defaultPlugins, @@ -71,7 +91,7 @@ const getConfig = (format, { }, }); -export default [ +const config: RollupOptions[] = [ ...formats.map(f => getConfig(f)), { input: './lib/index.sass', @@ -90,9 +110,11 @@ export default [ path.resolve('../../node_modules'), ], }, + less: {}, + stylus: {}, }, plugins: [ - autoprefixer({ env: process.env.BROWSERSLIST_ENV }), + autoprefixer, ], }), ], @@ -108,3 +130,5 @@ export default [ }, }, ]; + +export default config; diff --git a/packages/addon-remirror/tsconfig.build.json b/packages/addon-remirror/tsconfig.build.json new file mode 100644 index 000000000..dd7c4a73a --- /dev/null +++ b/packages/addon-remirror/tsconfig.build.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "noEmit": false, + "emitDeclarationOnly": true, + "outDir": "dist/types", + "baseUrl": "." + }, + "exclude": [ + "tests", + "**/*.test.ts", + "**/*.test.tsx", + "rollup.config.ts", + "**/*.stories.tsx", + ] +} diff --git a/packages/addon-remirror/tsconfig.json b/packages/addon-remirror/tsconfig.json new file mode 100644 index 000000000..9a11edc9a --- /dev/null +++ b/packages/addon-remirror/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./lib/**/*"], +} diff --git a/packages/ckeditor5-build-custom/.browserslistrc b/packages/ckeditor5-build-custom/.browserslistrc new file mode 100644 index 000000000..cf8634949 --- /dev/null +++ b/packages/ckeditor5-build-custom/.browserslistrc @@ -0,0 +1,5 @@ +>=0.5% +node >= 18 +not ie >= 0 +not ie_mob >= 0 +not dead diff --git a/packages/ckeditor5-build-custom/babel.config.js b/packages/ckeditor5-build-custom/babel.config.js deleted file mode 100644 index 4595c67be..000000000 --- a/packages/ckeditor5-build-custom/babel.config.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - presets: [ - ['@babel/env', { - corejs: 3, - useBuiltIns: 'usage', - }], - ['@babel/react', { - runtime: 'automatic', - }], - ], - plugins: [ - ['@babel/transform-runtime', { - corejs: 3, - }], - ], -}; diff --git a/packages/ckeditor5-build-custom/global.d.ts b/packages/ckeditor5-build-custom/global.d.ts new file mode 100644 index 000000000..7e28760a7 --- /dev/null +++ b/packages/ckeditor5-build-custom/global.d.ts @@ -0,0 +1 @@ +declare module '@ckeditor/ckeditor5-font/theme/icons/font-color.svg'; diff --git a/packages/ckeditor5-build-custom/lib/ColorPlugin/Field.js b/packages/ckeditor5-build-custom/lib/ColorPlugin/Field.js deleted file mode 100644 index 6853d9616..000000000 --- a/packages/ckeditor5-build-custom/lib/ColorPlugin/Field.js +++ /dev/null @@ -1,25 +0,0 @@ -import { ColorField } from '@oakjs/react'; - -const Field = ({ editor }) => { - const value = editor.model.document.selection.getAttribute('fontColor'); - - const onChange = ({ value: val }) => { - editor.execute('fontColor', { value: val }); - }; - - return ( -
- -
- ); -}; - -Field.displayName = 'Field'; - -export default Field; diff --git a/packages/ckeditor5-build-custom/lib/ColorPlugin/Field.tsx b/packages/ckeditor5-build-custom/lib/ColorPlugin/Field.tsx new file mode 100644 index 000000000..b5e43133b --- /dev/null +++ b/packages/ckeditor5-build-custom/lib/ColorPlugin/Field.tsx @@ -0,0 +1,33 @@ +import type { FieldContent } from '@oakjs/core'; +import { ColorField } from '@oakjs/react'; + +import Editor from '..'; + +export interface FieldProps { + editor: Editor; +} + +const Field = ({ editor }: FieldProps) => { + const value = editor.model.document.selection + .getAttribute('fontColor') as string; + + const onChange = (field: FieldContent) => { + editor.execute('fontColor', { value: field.value }); + }; + + return ( +
+ +
+ ); +}; + +Field.displayName = 'Field'; + +export default Field; diff --git a/packages/ckeditor5-build-custom/lib/ColorPlugin/index.js b/packages/ckeditor5-build-custom/lib/ColorPlugin/index.tsx similarity index 78% rename from packages/ckeditor5-build-custom/lib/ColorPlugin/index.js rename to packages/ckeditor5-build-custom/lib/ColorPlugin/index.tsx index 93c97d6f7..d58f855b3 100644 --- a/packages/ckeditor5-build-custom/lib/ColorPlugin/index.js +++ b/packages/ckeditor5-build-custom/lib/ColorPlugin/index.tsx @@ -1,9 +1,12 @@ -import { version } from 'react'; +import type { Root } from 'react-dom/client'; +import type { ViewEditableElement } from '@ckeditor/ckeditor5-engine'; +import { type ReactElement, version } from 'react'; import { Plugin } from '@ckeditor/ckeditor5-core'; import { createDropdown } from '@ckeditor/ckeditor5-ui'; import FontColorCommand from '@ckeditor/ckeditor5-font/src/fontcolor/fontcolorcommand.js'; import icon from '@ckeditor/ckeditor5-font/theme/icons/font-color.svg'; +import type Editor from '..'; import Field from './Field'; export default class ColorPlugin extends Plugin { @@ -38,12 +41,15 @@ export default class ColorPlugin extends Plugin { }, model: { key: 'color', - value: viewElement => viewElement.getStyle('color'), + value: (viewElement: ViewEditableElement) => + viewElement.getStyle('color'), }, }); conversion.for('downcast').attributeToAttribute({ - model: 'color', + model: { + key: 'color', + }, view: modelAttributeValue => ({ key: 'style', value: { @@ -68,20 +74,20 @@ export default class ColorPlugin extends Plugin { icon, }); - let root; + let root: Root; dropdown.panelView.on('render', async () => { root = await this.createReactRoot(dropdown.panelView.element); }); dropdown.buttonView.on('open', () => { - dropdown.buttonView.labelView.destroy(); + dropdown.buttonView.label = null; }); dropdown.on('change:isOpen', () => { const key = Math.random().toString(36).substring(7); if (dropdown.isOpen) { - root.render(); + root.render(); } }); @@ -89,7 +95,7 @@ export default class ColorPlugin extends Plugin { }); } - async createReactRoot (element) { + async createReactRoot (element: HTMLElement) { try { if (version.startsWith('18')) { const { createRoot } = await import('react-dom/client'); @@ -105,7 +111,8 @@ export default class ColorPlugin extends Plugin { const { render } = await import('react-dom'); return { - render: component => render(component, element), - }; + render: (component: ReactElement) => render(component, element), + unmount: () => render(null, element), + } as Root; } } diff --git a/packages/ckeditor5-build-custom/lib/index.stories.js b/packages/ckeditor5-build-custom/lib/index.stories.tsx similarity index 100% rename from packages/ckeditor5-build-custom/lib/index.stories.js rename to packages/ckeditor5-build-custom/lib/index.stories.tsx diff --git a/packages/ckeditor5-build-custom/lib/index.js b/packages/ckeditor5-build-custom/lib/index.ts similarity index 98% rename from packages/ckeditor5-build-custom/lib/index.js rename to packages/ckeditor5-build-custom/lib/index.ts index 94a6bb2a4..82c9903db 100644 --- a/packages/ckeditor5-build-custom/lib/index.js +++ b/packages/ckeditor5-build-custom/lib/index.ts @@ -82,3 +82,8 @@ class Editor extends ClassicEditor { } export default Editor; + +export type { + Editor, + ClassicEditor, +}; diff --git a/packages/ckeditor5-build-custom/package.json b/packages/ckeditor5-build-custom/package.json index e51c57a84..09129a970 100644 --- a/packages/ckeditor5-build-custom/package.json +++ b/packages/ckeditor5-build-custom/package.json @@ -5,24 +5,25 @@ "version": "3.5.5", "license": "SEE LICENSE IN LICENSE.md", "main": "dist/ckeditor.js", + "types": "dist/types/index.d.ts", "files": [ "dist" ], "engines": { - "node": ">= 16", - "npm": ">= 8" + "node": ">= 18" }, "peerDependencies": { "@oakjs/react": "^3.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" }, - "dependencies": { - "@babel/runtime-corejs3": "7.24.6", - "core-js": "3.37.1" + "devDependencies": { + "@oakjs/react": "workspace:*" }, "scripts": { - "build": "webpack --mode production" + "build": "yarn build:code && yarn build:dts", + "build:code": "webpack --mode production", + "build:dts": "yarn run -T tsc --project ./tsconfig.build.json" }, "publishConfig": { "access": "public" diff --git a/packages/ckeditor5-build-custom/tsconfig.build.json b/packages/ckeditor5-build-custom/tsconfig.build.json new file mode 100644 index 000000000..9faaf412c --- /dev/null +++ b/packages/ckeditor5-build-custom/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "noEmit": false, + "emitDeclarationOnly": true, + "outDir": "dist/types", + "baseUrl": "." + }, + "exclude": ["tests", "**/*.test.ts", "rollup.config.ts"] +} diff --git a/packages/ckeditor5-build-custom/tsconfig.json b/packages/ckeditor5-build-custom/tsconfig.json new file mode 100644 index 000000000..e9a54c4dd --- /dev/null +++ b/packages/ckeditor5-build-custom/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./lib/**/*", "global.d.ts"], +} diff --git a/packages/ckeditor5-build-custom/webpack.config.js b/packages/ckeditor5-build-custom/webpack.config.js index a8c835f12..703ecfbf5 100644 --- a/packages/ckeditor5-build-custom/webpack.config.js +++ b/packages/ckeditor5-build-custom/webpack.config.js @@ -10,7 +10,11 @@ const TerserWebpackPlugin = require('terser-webpack-plugin'); module.exports = { devtool: 'source-map', performance: { hints: false }, - entry: path.resolve(__dirname, 'lib', 'index.js'), + resolve: { + extensions: ['.js', '.ts', '.tsx'], + }, + target: 'web', + entry: path.resolve(__dirname, 'lib', 'index.ts'), output: { path: path.resolve(__dirname, 'dist'), filename: 'ckeditor.js', @@ -47,9 +51,9 @@ module.exports = { module: { rules: [ { - test: /\.js/, + test: /\.[jt]sx?/, exclude: /node_modules/, - use: ['babel-loader'], + use: ['swc-loader'], }, { test: /\.svg$/, diff --git a/packages/core/.browserslistrc b/packages/core/.browserslistrc index 5601a3b5b..cf8634949 100644 --- a/packages/core/.browserslistrc +++ b/packages/core/.browserslistrc @@ -1,5 +1,5 @@ >=0.5% -node >= 14 +node >= 18 not ie >= 0 not ie_mob >= 0 not dead diff --git a/packages/core/babel.config.js b/packages/core/babel.config.js deleted file mode 100644 index b43dca832..000000000 --- a/packages/core/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = { - presets: [ - ['@babel/env', { - corejs: 3, - useBuiltIns: 'usage', - }], - ], - plugins: [ - ['@babel/transform-runtime', { - corejs: 3, - }], - ], -}; diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js index bfff54f72..79d1bb082 100644 --- a/packages/core/jest.config.js +++ b/packages/core/jest.config.js @@ -16,7 +16,18 @@ module.exports = { '^.+\\.styl$', ], transform: { - '^.+\\.js$': 'babel-jest', + '^.+\\.(t|j)sx?$': [ + '@swc/jest', + { + jsc: { + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], }, transformIgnorePatterns: [ '/node_modules/(?!(uuid))', diff --git a/packages/core/lib/Builder/index.d.ts b/packages/core/lib/Builder/index.d.ts deleted file mode 100644 index 1b1c53646..000000000 --- a/packages/core/lib/Builder/index.d.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { - AddonObject, - ElementObject, - BuilderOptions, - Component, - ComponentObject, - ComponentsGroup, - ComponentOverride, - Field, - FieldObject, - FieldOverride, - TextsSheet, - TextsSheetObject, - ComponentSettingsField, - SettingOverride, - ComponentOverrideObject, - ElementId, - ComponentSettingsFieldKeyTuple, - GetTextCallback, - ComponentSettingsTab, -} from '../types'; -import { Logger } from '../Logger'; -import { Emitter, EmitterCallback } from '../Emitter'; - -export declare class Builder extends Emitter { - constructor(opts?: { - addons?: AddonObject[]; - content?: ElementObject[]; - options?: BuilderOptions; - }); - - options: BuilderOptions; - logger: Logger; - - subscribe(cb: EmitterCallback): (() => void); - setAddons(addons: AddonObject[]): void; - addAddon(addon: AddonObject): void; - removeAddon(addon: AddonObject): void; - getAvailableComponents(): ComponentsGroup[]; - getComponent(type: string): Component; - getComponentDisplayableSettings( - element: ElementObject, - opts?: { - component?: Component | ComponentObject; - fields?: (Field | FieldObject)[]; - }, - ): ComponentSettingsField[]; - getAvailableFields(): Array; - getField(type: string): Field; - getOverride( - type: string, - target: string, - opts?: { - output?: 'field'; - setting?: ComponentSettingsField; - }, - ): ComponentOverride | FieldOverride; - mergeOverrides( - overrides: (ComponentOverride | FieldOverride | SettingOverride)[] - ): void; - getContent(): ElementObject[]; - setContent(content: ElementObject[], opts?: { emit?: boolean }): void; - createElement(type: string, opts: { - component?: Component | ComponentObject; - override?: ComponentOverride | ComponentOverrideObject; - baseElement?: ElementObject; - resetIds?: boolean; - }): object; - addElement(element: ElementObject, opts?: { - component?: Component | ComponentObject; - parent?: ElementObject[]; - position?: 'before' | 'after'; - }): Element; - addElements(elements: ElementObject[], options?: { - parent?: ElementObject[]; - position?: 'before' | 'after'; - }): ElementObject[]; - getElement(id: ElementId): ElementObject; - removeElement(id: ElementId, opts?: { - deep?: boolean; - }): boolean; - setElement(id: ElementId, updates: ElementObject, opts?: { - deep?: boolean; - }): ElementObject; - moveElement( - element: ElementObject, - sibling: ElementObject, - opts?: { - deep?: boolean; - position?: 'before' | 'after'; - parent: ElementObject; - } - ): ElementObject; - duplicateElement( - element: ElementObject, - opts?: { - deep?: boolean; - } - ): ElementObject; - getElementSettings( - element: ElementObject, - key: string | string[] | ComponentSettingsFieldKeyTuple[], - def?: any - ): any; - canUndo(): boolean; - canRedo(): boolean; - undo(): void; - redo(): void; - resetHistory(): void; - generateId(): string; - getTextSheet(id: string): TextsSheet; - getText(key: string | GetTextCallback, def?: any): any; - setText(key: string, value: any): void; - getActiveTextSheet(): TextsSheet; - setActiveTextSheet(id: string): void; - getAvailableSettings(): (ComponentSettingsTab | ComponentSettingsField)[]; -} diff --git a/packages/core/lib/Builder/index.test.js b/packages/core/lib/Builder/index.test.ts similarity index 94% rename from packages/core/lib/Builder/index.test.js rename to packages/core/lib/Builder/index.test.ts index 5bfede47a..6d82e12da 100644 --- a/packages/core/lib/Builder/index.test.js +++ b/packages/core/lib/Builder/index.test.ts @@ -1,16 +1,17 @@ +import type { ElementObject } from '../types'; import { baseAddon } from '../addons'; import Builder from './index'; describe('Builder', () => { it('should create a builder', () => { - const content = [ + const content: ElementObject[] = [ { type: 'title', content: 'This is a title' }, { type: 'text', content: 'This is a text' }, ]; let id = 0; const builder = new Builder({ - addons: [baseAddon], + addons: [baseAddon()], content, options: { generateId: () => id++, @@ -43,7 +44,7 @@ describe('Builder', () => { it('should allow to listen to changes', () => { const builder = new Builder({ - addons: [baseAddon], + addons: [baseAddon()], }); const listener = jest.fn(); diff --git a/packages/core/lib/Builder/index.test.js.snap b/packages/core/lib/Builder/index.test.ts.snap similarity index 91% rename from packages/core/lib/Builder/index.test.js.snap rename to packages/core/lib/Builder/index.test.ts.snap index bd6fad888..4c3bd3dcf 100644 --- a/packages/core/lib/Builder/index.test.js.snap +++ b/packages/core/lib/Builder/index.test.ts.snap @@ -6,6 +6,7 @@ exports[`Builder should allow to add addons: Components 1`] = ` "components": [ Component { "construct": undefined, + "deserialize": undefined, "disallow": [], "draggable": true, "droppable": true, @@ -20,6 +21,7 @@ exports[`Builder should allow to add addons: Components 1`] = ` "options": [], "render": undefined, "sanitize": undefined, + "serialize": undefined, "settings": ComponentSettingsForm { "defaults": {}, "fields": [], @@ -52,6 +54,7 @@ exports[`Builder should create a builder: Base content 1`] = ` [ { "content": "This is a title", + "headingLevel": "h1", "id": 0, "type": "title", }, @@ -72,6 +75,7 @@ exports[`Builder should create a builder: With button & title swapped 1`] = ` }, { "content": "This is a title", + "headingLevel": "h1", "id": 0, "type": "title", }, @@ -82,6 +86,7 @@ exports[`Builder should create a builder: With button 1`] = ` [ { "content": "This is a title", + "headingLevel": "h1", "id": 0, "type": "title", }, @@ -107,6 +112,7 @@ exports[`Builder should create a builder: With button duplicated 1`] = ` }, { "content": "This is a title", + "headingLevel": "h1", "id": 0, "type": "title", }, @@ -117,6 +123,7 @@ exports[`Builder should create a builder: With button updated 1`] = ` [ { "content": "This is a title", + "headingLevel": "h1", "id": 0, "type": "title", }, @@ -132,6 +139,7 @@ exports[`Builder should create a builder: Without text 1`] = ` [ { "content": "This is a title", + "headingLevel": "h1", "id": 0, "type": "title", }, diff --git a/packages/core/lib/Builder/index.js b/packages/core/lib/Builder/index.ts similarity index 53% rename from packages/core/lib/Builder/index.js rename to packages/core/lib/Builder/index.ts index 88d570071..f940bef2f 100644 --- a/packages/core/lib/Builder/index.js +++ b/packages/core/lib/Builder/index.ts @@ -1,29 +1,55 @@ import { exists } from '@junipero/core'; import { v4 as uuid } from 'uuid'; -import { BuilderOptions } from '../types'; -import Components from '../Components'; +import type { + AddonObject, + BuilderObject, + ComponentObject, + ComponentOverrideObject, + ComponentSettingsFieldObject, + ComponentSettingsTabObject, + ComponentsGroupObject, + ElementId, + ElementObject, + ElementSettingsKeyObject, + EventCallback, + FieldOverrideObject, +} from '../types'; +import { + BuilderOptions, + Component, + ComponentOverride, + FieldOverride, +} from '../classes'; import Emitter from '../Emitter'; +import Logger from '../Logger'; +import Components from '../Components'; import Fields from '../Fields'; import Overrides from '../Overrides'; import Store from '../Store'; import Texts from '../Texts'; -import Logger from '../Logger'; import Settings from '../Settings'; export default class Builder extends Emitter { - #components = null; - #fields = null; - #overrides = null; - #texts = null; - #store = null; - #settings = null; - #addons = []; - - constructor ({ addons, content, options = {} } = {}) { + logger: Logger = null; + options: BuilderOptions = null; + + #components: Components = null; + #fields: Fields = null; + #overrides: Overrides = null; + #texts: Texts = null; + #store: Store = null; + #settings: Settings = null; + #addons: Array = []; + + constructor (opts: { + addons?: AddonObject[], + content?: ElementObject[], + options?: BuilderObject + } = {}) { super(); - this.options = new BuilderOptions(options); + this.options = new BuilderOptions(opts.options); this.logger = new Logger({ builder: this }); this.#components = new Components({ builder: this }); @@ -33,21 +59,21 @@ export default class Builder extends Emitter { this.#texts = new Texts({ builder: this }); this.#settings = new Settings({ builder: this }); - if (Array.isArray(addons)) { - this.#addons = addons; - addons.forEach(addon => { + if (Array.isArray(opts.addons)) { + this.#addons = opts.addons; + opts.addons.forEach(addon => { this.logger.log('Initializing builder with addon:', addon); this.addAddon(addon); }); } - if (content) { - this.logger.log('Initializing builder with content:', content); - this.#store.set(content); + if (opts.content) { + this.logger.log('Initializing builder with content:', opts.content); + this.#store.set(opts.content); } } - subscribe (callback) { + subscribe (callback: EventCallback) { const subscriptions = [ super.subscribe(callback), this.#store.subscribe(this.emit.bind(this)), @@ -63,7 +89,7 @@ export default class Builder extends Emitter { }; } - setAddons (addons) { + setAddons (addons: Array): void { this.#addons?.forEach(addon => { this.logger.log('Removing builder addon:', addon); this.removeAddon(addon); @@ -78,7 +104,7 @@ export default class Builder extends Emitter { this.emit('addons.update', addons); } - addAddon (addon) { + addAddon (addon: AddonObject): void { addon.fields?.forEach(field => { this.#fields.add(field); }); @@ -100,7 +126,7 @@ export default class Builder extends Emitter { }); } - removeAddon (addon) { + removeAddon (addon: AddonObject): void { addon.settings?.forEach(setting => { this.#settings.remove(setting.id); }); @@ -122,20 +148,30 @@ export default class Builder extends Emitter { }); } - getAvailableComponents () { - const { groups, defaultGroup } = this.#components.all(); + getAvailableComponents (): Array { + const { + groups, + defaultGroup, + } = this.#components.getAll(); return [...groups, defaultGroup]; } - getComponent (type) { + getComponent (type: string): ComponentObject { return this.#components.getComponent(type); } - getComponentDisplayableSettings (element, { component }) { + getComponentDisplayableSettings ( + element: ElementObject, + { component }: { component: ComponentObject} + ): Array { + const component_ = new Component(component); + return [ ...this.#components - .getDisplayableSettings?.(element, { component }) || [], + .getDisplayableSettings?.( + element, { component: component_ } + ) || [], ...this.#settings.getDisplayable?.(element) || [], ]; } @@ -144,63 +180,119 @@ export default class Builder extends Emitter { return this.#fields.all(); } - getField (type) { + getField (type: string) { return this.#fields.get(type); } - getOverride (type, target, opts) { + getOverride ( + type: 'component' | 'field' | 'setting', + target: string, + opts?: any + ) { return this.#overrides.get(type, target, opts); } - mergeOverrides (overrides) { - return this.#overrides.merge(overrides); + mergeOverrides ( + overrides: Array + ) { + const ovrrides_ = overrides.map(override => { + if (override.type === 'component') { + return new ComponentOverride(override); + } else { + return new FieldOverride(override); + } + }); + + return this.#overrides.merge(ovrrides_); } getContent () { return this.#store.get(); } - setContent (content, options) { + setContent (content: Array, options: { emit?: boolean }) { this.#store.set(content, options); } - createElement (type, options) { + createElement (type: string, options?: { + component?: ComponentObject | Component, + override?: ComponentOverrideObject, + baseElement?: ElementObject, + [_: string]: any + }) { return this.#store.createElement(type, options); } - addElement (element, options) { + addElement (element: ElementObject, options?: { + parent?: Array, + position?: 'before' | 'after', + component?: ComponentObject, + [_: string]: any + }) { return this.#store.addElement(element, options); } - addElements (elements, options) { + addElements (elements: Array, options?:{ + parent?: Array, + position?: 'before' | 'after', + component?: ComponentObject, + [_: string]: any + }) { return this.#store.addElements(elements, options); } - getElement (id, options) { + getElement (id: ElementId, options?: { + parent?: any[]; + deep?: boolean; + }) { return this.#store.getElement(id, options); } - removeElement (id, options) { + removeElement (id: ElementId, options?: { + parent?: any[]; + deep?: boolean; + }) { return this.#store.removeElement(id, options); } - setElement (id, updates, options) { + setElement (id: ElementId, updates: ElementObject, options?: { + element?: ElementObject; + parent?: ElementObject[]; + deep?: boolean; + }) { return this.#store.setElement(id, updates, options); } - moveElement (element, sibling, options) { + moveElement ( + element: ElementObject, + sibling: ElementObject, + options?: { + parent?: Array, + position?: 'before' | 'after' + } + ) { return this.#store.moveElement(element, sibling, options); } - duplicateElement (element, options) { + duplicateElement (element: ElementObject, options?: { + parent?: ElementObject[]; + }) { return this.#store.duplicateElement(element, options); } - getElementSettings (element, key, def) { + getElementSettings ( + element: ElementObject, + key: ElementSettingsKeyObject | string, + def?: any + ) { return this.#store.getElementSettings(element, key, def); } - setElementSettings (element, key, value) { + setElementSettings ( + element: ElementObject, + key: ElementSettingsKeyObject | string, + value: any + ) { return this.#store.setElementSettings(element, key, value); } @@ -230,15 +322,15 @@ export default class Builder extends Emitter { return exists(customId) && customId !== '' ? customId : uuid(); } - getTextSheet (id) { + getTextSheet (id: string) { return this.#texts.getSheet(id); } - getText (key, def) { + getText (key: string, def: string) { return this.#texts.get(key, def); } - setText (key, value) { + setText (key: string, value: string) { return this.#texts.set(key, value); } @@ -246,7 +338,7 @@ export default class Builder extends Emitter { return this.#texts.getActiveSheet(); } - setActiveTextSheet (id) { + setActiveTextSheet (id: string) { return this.#texts.setActiveSheet(id); } diff --git a/packages/core/lib/Components/index.d.ts b/packages/core/lib/Components/index.d.ts deleted file mode 100644 index b2fe5f78e..000000000 --- a/packages/core/lib/Components/index.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Component, ComponentsGroup } from '../types'; -import { Builder } from '../Builder'; -import { Emitter } from '../Emitter'; - -export declare class Components extends Emitter { - static TYPE_COMPONENT: string; - static TYPE_GROUP: string; - static COMPONENTS_GROUP_CORE: string; - static COMPONENTS_GROUP_OTHER: string; - - constructor(options?: { builder: Builder }); - - hasGroup(id: string): boolean; - getGroup(id: string): ComponentsGroup; - - hasComponent(id: string, opts?: { groupId?: string }): boolean; - getComponent(id: string, opts?: { groupId?: string }): Component; - - append(component: object): void; - prepend(component: object): void; - add(component: object, opts?: { mode?: string }): void; - remove(id: string): void; - getAll(): object; - toJSON(): object; -} diff --git a/packages/core/lib/Components/index.test.js b/packages/core/lib/Components/index.test.ts similarity index 100% rename from packages/core/lib/Components/index.test.js rename to packages/core/lib/Components/index.test.ts diff --git a/packages/core/lib/Components/index.test.js.snap b/packages/core/lib/Components/index.test.ts.snap similarity index 94% rename from packages/core/lib/Components/index.test.js.snap rename to packages/core/lib/Components/index.test.ts.snap index 0c88b349f..4a1171f30 100644 --- a/packages/core/lib/Components/index.test.js.snap +++ b/packages/core/lib/Components/index.test.ts.snap @@ -14,6 +14,7 @@ exports[`Components should allow to add a new group and add components 1`] = ` "components": [ Component { "construct": undefined, + "deserialize": undefined, "disallow": [], "draggable": true, "droppable": true, @@ -28,6 +29,7 @@ exports[`Components should allow to add a new group and add components 1`] = ` "options": [], "render": undefined, "sanitize": undefined, + "serialize": undefined, "settings": ComponentSettingsForm { "defaults": {}, "fields": [], diff --git a/packages/core/lib/Components/index.js b/packages/core/lib/Components/index.ts similarity index 55% rename from packages/core/lib/Components/index.js rename to packages/core/lib/Components/index.ts index 82458b425..19d644386 100644 --- a/packages/core/lib/Components/index.js +++ b/packages/core/lib/Components/index.ts @@ -1,18 +1,50 @@ -import { Component, ComponentsGroup } from '../types'; +import type { + ComponentObject, + ComponentsGroupObject, + ElementObject, + GetTextCallback, +} from '../types'; +import { + Component, + ComponentSettingsField, + ComponentSettingsTab, + ComponentsGroup, +} from '../classes'; import Emitter from '../Emitter'; +import Builder from '../Builder'; -export default class Components extends Emitter { +export declare abstract class IComponents { + static TYPE_COMPONENT: string; + static TYPE_GROUP: string; + static COMPONENTS_GROUP_CORE: string; + static COMPONENTS_GROUP_OTHER: string; + + constructor(options?: { builder: Builder }); + + hasGroup(id: string): boolean; + getGroup(id: string): ComponentsGroup; + + hasComponent(id: string, opts?: { groupId?: string }): boolean; + getComponent(id: string, opts?: { groupId?: string }): Component; + + add(component: object, opts?: { mode?: string }): void; + remove(id: string): void; + getAll(): object; + toJSON(): object; +} + +export default class Components extends Emitter implements IComponents { static TYPE_COMPONENT = 'component'; static TYPE_GROUP = 'group'; static COMPONENTS_GROUP_CORE = 'core'; static COMPONENTS_GROUP_OTHER = 'other'; - #builder = null; - #groups = null; - #defaultGroup = null; // Other tab + #builder: Builder = null; + #groups: Array = null; + #defaultGroup: ComponentsGroup = null; // Other tab - constructor ({ builder } = {}) { + constructor ({ builder }: { builder?: Builder} = {}) { super(); this.#builder = builder; @@ -20,20 +52,31 @@ export default class Components extends Emitter { this.#defaultGroup = new ComponentsGroup({ type: 'group', id: Components.COMPONENTS_GROUP_OTHER, - name: t => t('core.components.other.title', 'Other'), + name: (t: GetTextCallback) => t('core.components.other.title', 'Other'), components: [], }); } - hasGroup (id) { + hasGroup (id: string) { return this.#groups.some(ComponentsGroup.FIND_PREDICATE(id)); } - getGroup (id) { + getGroup (id: string) { return this.#groups.find(ComponentsGroup.FIND_PREDICATE(id)); } - hasComponent (id, { groupId } = {}) { + toObject (): ComponentsGroupObject[] { + return [ + ...this.#groups.map(group => group.toObject()), + this.#defaultGroup.toObject(), + ]; + + } + + hasComponent ( + id: string, + { groupId }: { groupId?: string } = {} + ) { if (groupId) { return this.getGroup(groupId)?.components .some(Component.FIND_PREDICATE.bind(null, id)); @@ -49,7 +92,10 @@ export default class Components extends Emitter { .some(Component.FIND_PREDICATE.bind(null, id)); } - getComponent (id, { groupId } = {}) { + getComponent ( + id: string, + { groupId }: { groupId?: string } = {} + ): Component { if (groupId) { return this.getGroup(groupId)?.components ?.find(Component.FIND_PREDICATE(id)); @@ -66,34 +112,30 @@ export default class Components extends Emitter { return this.#defaultGroup.components.find(Component.FIND_PREDICATE(id)); } - append (component) { - return this.add(component, { mode: 'append' }); - } - - prepend (component) { - return this.add(component, { mode: 'prepend' }); - } - - add (component, { mode = 'append' } = {}) { + add ( + component: ComponentObject | ComponentsGroupObject, + { mode = 'append' }: { mode?: 'append' | 'prepend' } = {} + ) { const mutateMethod = mode === 'append' ? 'push' : 'unshift'; // This component is a group, add a new group if (component.type === Components.TYPE_GROUP) { if (!this.hasGroup(component.id)) { - component = new ComponentsGroup(component); - component.components = component.components || []; + const group = new ComponentsGroup(component as ComponentsGroupObject); + group.components = (( + component as ComponentsGroupObject + ).components || []).map(component => new Component(component)); - this.#groups[mutateMethod](component); - this.emit('groups.add', component); + this.#groups[mutateMethod](group as ComponentsGroup); + this.emit('groups.add', group); } return; } - component = new Component(component); - - const group = component.group && this.hasGroup(component.group) - ? this.getGroup(component.group) + const component_ = new Component(component); + const group = component_.group && this.hasGroup(component_.group) + ? this.getGroup(component_.group) : this.#defaultGroup; const existing = this.getComponent(component.id, { groupId: group.id }); @@ -106,15 +148,15 @@ export default class Components extends Emitter { ); const index = group.components.indexOf(existing); - group.components[index] = component; + group.components[index as any] = component_; this.emit('components.update', component, group); } else { - group.components[mutateMethod](component); + group.components[mutateMethod](component_); this.emit('components.add', component, group); } } - remove (id) { + remove (id: string) { const groupIndex = this.#groups .findIndex(ComponentsGroup.FIND_PREDICATE(id)); @@ -155,15 +197,21 @@ export default class Components extends Emitter { } } - all () { + getAll () { return { groups: this.#groups, defaultGroup: this.#defaultGroup, }; } - getDisplayableSettings (element, { fields, component } = {}) { - const displayable = []; + getDisplayableSettings ( + element: ElementObject, + { fields, component }: { + fields?: Array; + component?: Component; + } = {} + ) { + const displayable: Array = []; if (!fields) { component = component || this.getComponent(element.type); @@ -182,14 +230,21 @@ export default class Components extends Emitter { })); } + const settingFieldObject = setting as ComponentSettingsField; + if ( - setting.displayable === true || - ( - typeof setting.displayable === 'function' && - setting.displayable({ element, builder: this.#builder }) - ) + settingFieldObject.displayable === true ) { - displayable.push(setting); + displayable.push(settingFieldObject); + } else if (typeof settingFieldObject.displayable === 'function') { + if ( + settingFieldObject.displayable( + element, + { component, builder: this.#builder } + ) + ) { + displayable.push(settingFieldObject); + } } } @@ -197,6 +252,6 @@ export default class Components extends Emitter { } toJSON () { - return this.all(); + return this.getAll(); } } diff --git a/packages/core/lib/Emitter/index.d.ts b/packages/core/lib/Emitter/index.d.ts deleted file mode 100644 index ea792175f..000000000 --- a/packages/core/lib/Emitter/index.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export declare type EmitterCallback = (...args: any[]) => void; - -export declare class Emitter { - constructor(); - - /** Subscribes to events and return an unsubscribe callback */ - subscribe(cb: EmitterCallback): EmitterCallback; - - /** Emits an event */ - emit(eventName: string, ...args: any[]): void; -} diff --git a/packages/core/lib/Emitter/index.js b/packages/core/lib/Emitter/index.js deleted file mode 100644 index 472e6909f..000000000 --- a/packages/core/lib/Emitter/index.js +++ /dev/null @@ -1,14 +0,0 @@ -export default class Emitter { - #subscribers = new Map(); - - subscribe (cb) { - const key = Symbol('store-subscriber'); - this.#subscribers.set(key, cb); - - return () => this.#subscribers.delete(key); - } - - emit (...args) { - this.#subscribers.forEach(c => c.bind(this)(...args)); - } -} diff --git a/packages/core/lib/Emitter/index.test.js b/packages/core/lib/Emitter/index.test.ts similarity index 100% rename from packages/core/lib/Emitter/index.test.js rename to packages/core/lib/Emitter/index.test.ts diff --git a/packages/core/lib/Emitter/index.ts b/packages/core/lib/Emitter/index.ts new file mode 100644 index 000000000..ab80c3bc9 --- /dev/null +++ b/packages/core/lib/Emitter/index.ts @@ -0,0 +1,26 @@ +import type { EmitterCallback } from '../types'; + +export declare abstract class IEmitter { + constructor(); + + /** Subscribes to events and return an unsubscribe callback */ + subscribe(cb: EmitterCallback): EmitterCallback; + + /** Emits an event */ + emit(eventName: string, ...args: any[]): void; +} + +export default class Emitter implements IEmitter { + #subscribers: Map = new Map(); + + subscribe (cb: Function) { + const key = Symbol('store-subscriber'); + this.#subscribers.set(key, cb); + + return () => { this.#subscribers.delete(key); }; + } + + emit (...args: any[]) { + this.#subscribers.forEach(c => c.bind(this)(...args)); + } +} diff --git a/packages/core/lib/Fields/index.d.ts b/packages/core/lib/Fields/index.d.ts deleted file mode 100644 index c009f4bcc..000000000 --- a/packages/core/lib/Fields/index.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Field, FieldObject } from '../types'; -import { Builder } from '../Builder'; -import { Emitter } from '../Emitter'; - -export declare class Fields extends Emitter { - constructor(options?: { builder: Builder }); - - /** Checkes if a field exists */ - has(type: string): boolean; - - /** Get a field by its type */ - get(type: string): Field; - - /** Add a new field definition */ - add(field: FieldObject | Field): Field; - - /** Remove a field definition */ - remove(type: string): void; - - /** Get all fields */ - all(): Array; -} diff --git a/packages/core/lib/Fields/index.js b/packages/core/lib/Fields/index.js deleted file mode 100644 index e6c867d02..000000000 --- a/packages/core/lib/Fields/index.js +++ /dev/null @@ -1,59 +0,0 @@ -import { Field } from '../types'; -import Emitter from '../Emitter'; - -export default class Fields extends Emitter { - #fields = []; - #builder = null; - - constructor ({ builder } = {}) { - super(); - - this.#builder = builder; - } - - has (type) { - return this.#fields.some(Field.FIND_PREDICATE(type)); - } - - get (type) { - return this.#fields.find(Field.FIND_PREDICATE(type)); - } - - add (field) { - field = new Field(field); - - const existing = this.get(field.type); - - if (existing) { - this.#builder.logger.log( - 'Field definition already exists, updating.', - 'Old:', existing, - 'New:', field, - ); - - const index = this.#fields.indexOf(existing); - this.#fields[index] = field; - this.emit('fields.update', this, field); - } else { - this.#fields.push(field); - this.emit('fields.add', this, field); - } - - return field; - } - - remove (type) { - const index = this.#fields - .findIndex(Field.FIND_PREDICATE(type)); - - if (index > -1) { - const field = this.#fields[index]; - this.#fields.splice(index, 1); - this.emit('fields.remove', this, field); - } - } - - all () { - return this.#fields; - } -} diff --git a/packages/core/lib/Fields/index.test.js b/packages/core/lib/Fields/index.test.ts similarity index 100% rename from packages/core/lib/Fields/index.test.js rename to packages/core/lib/Fields/index.test.ts diff --git a/packages/core/lib/Fields/index.test.js.snap b/packages/core/lib/Fields/index.test.ts.snap similarity index 100% rename from packages/core/lib/Fields/index.test.js.snap rename to packages/core/lib/Fields/index.test.ts.snap diff --git a/packages/core/lib/Fields/index.ts b/packages/core/lib/Fields/index.ts new file mode 100644 index 000000000..cd661f19f --- /dev/null +++ b/packages/core/lib/Fields/index.ts @@ -0,0 +1,82 @@ +import type { FieldObject } from '../types'; +import { Field } from '../classes'; +import Emitter from '../Emitter'; +import Builder from '../Builder'; + +export declare abstract class IFields { + constructor(options?: { builder: Builder }); + + /** Checkes if a field exists */ + has(type: string): boolean; + + /** Get a field by its type */ + get(type: string): Field; + + /** Add a new field definition */ + add(field: FieldObject | Field): Field; + + /** Remove a field definition */ + remove(type: string): void; + + /** Get all fields */ + all(): Array; +} + +export default class Fields extends Emitter implements IFields { + #fields: Field[] = []; + #builder: Builder = null; + + field: object; + + constructor ({ builder }: { builder?: Builder} = {}) { + super(); + + this.#builder = builder; + } + + has (type: string): boolean { + return this.#fields.some(Field.FIND_PREDICATE(type)); + } + + get (type: string) { + return this.#fields.find(Field.FIND_PREDICATE(type)); + } + + add (field: FieldObject) { + const field_ = new Field(field); + + const existing = this.get(field_.type); + + if (existing) { + this.#builder.logger.log( + 'Field definition already exists, updating.', + 'Old:', existing, + 'New:', field, + ); + + const index = this.#fields.indexOf(existing); + this.#fields[index] = field_; + this.emit('fields.update', this, field); + } else { + this.#fields.push(field_); + this.emit('fields.add', this, field); + } + + return field_; + } + + remove (type: string) { + const index = this.#fields + .findIndex(Field.FIND_PREDICATE(type)); + + if (index > -1) { + const field = this.#fields[index]; + this.#fields.splice(index, 1); + this.emit('fields.remove', this, field); + } + } + + all () { + return this.#fields; + } +} diff --git a/packages/core/lib/Logger/index.d.ts b/packages/core/lib/Logger/index.d.ts deleted file mode 100644 index 23a171b2d..000000000 --- a/packages/core/lib/Logger/index.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Builder } from '../Builder'; - -export declare class Logger { - constructor(options?: { builder: Builder }); - log(...args: any[]): void; - warn(...args: any[]): void; -} diff --git a/packages/core/lib/Logger/index.js b/packages/core/lib/Logger/index.js deleted file mode 100644 index 37d122437..000000000 --- a/packages/core/lib/Logger/index.js +++ /dev/null @@ -1,18 +0,0 @@ -export default class Logger { - #builder = null; - - constructor ({ builder }) { - this.#builder = builder; - } - - log (...args) { - if (this.#builder.options.debug) { - // eslint-disable-next-line no-console - console.log('[oak]', ...args); - } - } - - warn (...args) { - console.warn('[oak]', ...args); - } -} diff --git a/packages/core/lib/Logger/index.test.js b/packages/core/lib/Logger/index.test.ts similarity index 76% rename from packages/core/lib/Logger/index.test.js rename to packages/core/lib/Logger/index.test.ts index d8b5e2a84..4eb127881 100644 --- a/packages/core/lib/Logger/index.test.js +++ b/packages/core/lib/Logger/index.test.ts @@ -3,9 +3,15 @@ import Logger from './index'; /* eslint-disable no-console */ describe('Logger', () => { + let logSpy: ReturnType; + let warnSpy: ReturnType; + beforeEach(() => { - jest.spyOn(console, 'log').mockImplementation(() => {}); - jest.spyOn(console, 'warn').mockImplementation(() => {}); + logSpy = jest.spyOn(console, 'log'); + logSpy.mockImplementation(() => {}); + + warnSpy = jest.spyOn(console, 'warn'); + warnSpy.mockImplementation(() => {}); }); it('should log', () => { @@ -41,13 +47,12 @@ describe('Logger', () => { }); afterEach(() => { - console.log.mockClear(); - console.warn.mockClear(); + logSpy.mockClear(); + warnSpy.mockClear(); }); afterAll(() => { - // eslint-disable-next-line no-console - console.log.mockRestore(); - console.warn.mockRestore(); + logSpy.mockRestore(); + warnSpy.mockRestore(); }); }); diff --git a/packages/core/lib/Logger/index.ts b/packages/core/lib/Logger/index.ts new file mode 100644 index 000000000..3bf53b39a --- /dev/null +++ b/packages/core/lib/Logger/index.ts @@ -0,0 +1,26 @@ +import type Builder from '../Builder'; + +export declare abstract class ILogger { + constructor(options?: { builder: Builder }); + log(...args: any[]): void; + warn(...args: any[]): void; +} + +export default class Logger implements ILogger { + #builder: Builder = null; + + constructor ({ builder }: { builder?: Builder }) { + this.#builder = builder; + } + + log (...args: any[]) { + if (this.#builder.options.debug) { + // eslint-disable-next-line no-console + console.log('[oak]', ...args); + } + } + + warn (...args: any[]) { + console.warn('[oak]', ...args); + } +} diff --git a/packages/core/lib/Overrides/index.d.ts b/packages/core/lib/Overrides/index.d.ts deleted file mode 100644 index e9627fcfb..000000000 --- a/packages/core/lib/Overrides/index.d.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - ComponentOverride, - ComponentOverrideObject, - ComponentSettingsField, - Field, - FieldOverride, - FieldOverrideObject, -} from '../types'; -import { Emitter } from '../Emitter'; -import { Builder } from '../Builder'; - -export declare class Overrides extends Emitter { - constructor(options?: { builder: Builder }); - - /** Adds a new component or field override */ - add( - override: ComponentOverride | ComponentOverrideObject | FieldOverride | - FieldOverrideObject - ): ComponentOverride | FieldOverride; - - /** - * Retrieves an override for a component or a field. - * If the override type is 'component' and the output is set to 'field', - * the override will be returned as a field override object, potentially - * using a component setting field as base. - * - * Example: - * this.get('component', 'title', { - * output: 'field', - * setting: new ComponentSettingsField({ - * key: 'content', - * type: 'textarea', - * }), - * }) - * - * Will result in: - * { - * type: 'textarea', - * render: () => ..., - * } - * */ - get(type: 'component' | 'field', target: string, options?: { - output?: 'field', - setting?: ComponentSettingsField, - }): ComponentOverride | FieldOverrideObject; - - /** Removes an override by its id (if available) */ - remove (id: string): void; - - /** Merges overrides into a single non-typed object */ - merge(overrides: Array): object; - - /** Returns all available overrides */ - all(): Array; -} diff --git a/packages/core/lib/Overrides/index.js b/packages/core/lib/Overrides/index.js deleted file mode 100644 index 5362b7c6a..000000000 --- a/packages/core/lib/Overrides/index.js +++ /dev/null @@ -1,122 +0,0 @@ -import { omit } from '@junipero/core'; - -import { ComponentOverride, FieldOverride, SettingOverride } from '../types'; -import Emitter from '../Emitter'; - -export default class Overrides extends Emitter { - static FIND_PREDICATE = id => o => id ? o.id === id : null; - - #overrides = []; - #builder = null; - - constructor ({ builder } = {}) { - super(); - - this.#builder = builder; - } - - add (override) { - const existing = this.#overrides - .find(Overrides.FIND_PREDICATE(override.id)); - - if (existing) { - this.#builder?.logger.log( - 'Override already exists, updating definition.', - 'Old:', existing, - 'New:', override - ); - - this.#overrides.splice(this.#overrides.indexOf(existing), 1, override); - this.emit('overrides.update', override); - - return override; - } - - switch (override.type) { - case 'component': - override = new ComponentOverride(override); - this.#overrides.unshift(override); - break; - case 'field': - override = new FieldOverride(override); - this.#overrides.unshift(override); - break; - case 'setting': - override = new SettingOverride(override); - this.#overrides.unshift(override); - break; - } - - this.emit('overrides.add', this, override); - - return override; - } - - get (overrideType, target, { output, setting } = {}) { - const strategy = this.#builder?.options?.overrideStrategy; - const overrides = this.#overrides.filter(override => - override.type === overrideType && - (override.targets.includes('*') || override.targets.includes(target)) - ); - - switch (overrideType) { - case 'component': { - const override = strategy === 'merge' - ? this.merge(overrides) : overrides[0]; - - switch (output) { - case 'field': { - const newComponentField = override?.fields - ?.find(f => f.key === setting?.key); - - return Object.assign( - { type: newComponentField?.type || setting.type }, - this.#builder.getOverride('field', - newComponentField?.type || setting?.type), - omit(newComponentField || {}, ['type', 'key']) - ); - } - default: - return override; - } - } - case 'setting': - return overrides?.find(o => o.key === setting?.key); - default: - return strategy === 'merge' ? this.merge(overrides) : overrides[0]; - } - } - - remove (id) { - if (!id) { - return; - } - - const index = this.#overrides.findIndex(Overrides.FIND_PREDICATE(id)); - - if (index !== -1) { - this.#builder?.logger.log('Removing override:', this.#overrides[index]); - - const override = this.#overrides[index]; - this.#overrides.splice(index, 1); - this.emit('overrides.remove', this, override); - } - } - - merge (overrides) { - return overrides.reduce((res, override) => { - Object.keys(override).forEach(key => { - if (override[key] === null || override[key] === undefined) { - delete override[key]; - } - }); - Object.assign(res, override); - - return res; - }, {}); - } - - all () { - return this.#overrides; - } -} diff --git a/packages/core/lib/Overrides/index.test.js b/packages/core/lib/Overrides/index.test.ts similarity index 96% rename from packages/core/lib/Overrides/index.test.js rename to packages/core/lib/Overrides/index.test.ts index b81be432a..ee032dbb3 100644 --- a/packages/core/lib/Overrides/index.test.js +++ b/packages/core/lib/Overrides/index.test.ts @@ -4,21 +4,21 @@ import Overrides from './index'; describe('Overrides', () => { it('should allow to add overrides', () => { const overrides = new Overrides(); - overrides.add({ type: 'component', targets: ['foo'], fields: [{}] }); + overrides.add({ type: 'component', targets: ['foo'], fields: [] }); overrides.add({ type: 'field' }); expect(overrides.all()).toMatchSnapshot(); }); it('should allow to get an override', () => { const overrides = new Overrides(); - overrides.add({ type: 'component', targets: ['foo'], fields: [{}] }); + overrides.add({ type: 'component', targets: ['foo'], fields: [] }); expect(overrides.get('component', 'foo')).toMatchSnapshot(); }); it('should alow to merge overrides', () => { const builder = new Builder({ options: { overrideStrategy: 'merge' } }); const overrides = new Overrides({ builder }); - overrides.add({ type: 'component', targets: ['foo'], fields: [{}] }); + overrides.add({ type: 'component', targets: ['foo'], fields: [] }); overrides.add( { type: 'component', targets: ['foo'], construct: () => ({}) } ); diff --git a/packages/core/lib/Overrides/index.test.js.snap b/packages/core/lib/Overrides/index.test.ts.snap similarity index 70% rename from packages/core/lib/Overrides/index.test.js.snap rename to packages/core/lib/Overrides/index.test.ts.snap index 6b1eff242..8d67d0c99 100644 --- a/packages/core/lib/Overrides/index.test.js.snap +++ b/packages/core/lib/Overrides/index.test.ts.snap @@ -3,7 +3,10 @@ exports[`Overrides should allow to add overrides 1`] = ` [ FieldOverride { + "construct": undefined, "id": undefined, + "onChange": undefined, + "priority": 0, "props": {}, "render": undefined, "targets": [], @@ -11,13 +14,15 @@ exports[`Overrides should allow to add overrides 1`] = ` }, ComponentOverride { "construct": undefined, + "deserialize": undefined, "duplicate": undefined, - "fields": [ - {}, - ], + "fields": [], + "getContainers": undefined, "id": undefined, + "priority": 0, "render": undefined, "sanitize": undefined, + "serialize": undefined, "targets": [ "foo", ], @@ -29,13 +34,15 @@ exports[`Overrides should allow to add overrides 1`] = ` exports[`Overrides should allow to get an override 1`] = ` ComponentOverride { "construct": undefined, + "deserialize": undefined, "duplicate": undefined, - "fields": [ - {}, - ], + "fields": [], + "getContainers": undefined, "id": undefined, + "priority": 0, "render": undefined, "sanitize": undefined, + "serialize": undefined, "targets": [ "foo", ], @@ -46,9 +53,8 @@ ComponentOverride { exports[`Overrides should alow to merge overrides 1`] = ` { "construct": [Function], - "fields": [ - {}, - ], + "fields": [], + "priority": 0, "targets": [ "foo", ], diff --git a/packages/core/lib/Overrides/index.ts b/packages/core/lib/Overrides/index.ts new file mode 100644 index 000000000..af062eac4 --- /dev/null +++ b/packages/core/lib/Overrides/index.ts @@ -0,0 +1,198 @@ +import { omit } from '@junipero/core'; + +import type { + ComponentOverrideObject, + ComponentSettingsFieldObject, + FieldOverrideObject, + SettingOverrideObject, +} from '../types'; +import { + ComponentOverride, + FieldOverride, + SettingOverride, +} from '../classes'; +import Emitter from '../Emitter'; +import Builder from '../Builder'; + +export declare abstract class IOverrides { + constructor(options?: { builder: Builder }); + + /** Adds a new component or field override */ + add( + override: ComponentOverride | ComponentOverrideObject | FieldOverride | + FieldOverrideObject | SettingOverride | SettingOverrideObject + ): ComponentOverride | FieldOverride | SettingOverride; + + /** + * Retrieves an override for a component or a field. + * If the override type is 'component' and the output is set to 'field', + * the override will be returned as a field override object, potentially + * using a component setting field as base. + * + * Example: + * this.get('component', 'title', { + * output: 'field', + * setting: new ComponentSettingsField({ + * key: 'content', + * type: 'textarea', + * }), + * }) + * + * Will result in: + * { + * type: 'textarea', + * render: () => ..., + * } + * */ + get(type: 'component' | 'field' | 'setting', target: string, options?: { + output?: 'field', + setting?: ComponentSettingsFieldObject, + }): ComponentOverride | FieldOverride | SettingOverride; + + /** Removes an override by its id (if available) */ + remove (id: string): void; + + /** Merges overrides into a single non-typed object */ + merge(overrides: Array): + ComponentOverride | FieldOverride | SettingOverride; + + /** Returns all available overrides */ + all(): Array; +} + +export default class Overrides extends Emitter implements IOverrides { + static FIND_PREDICATE = (id: string) => ( + o: ComponentOverride | + FieldOverride | + SettingOverride + ) => id ? o.id === id : null; + + #overrides: Array = []; + #builder: Builder = null; + + constructor ({ builder }: { builder?: Builder } = {}) { + super(); + + this.#builder = builder; + } + + add ( + override: ComponentOverrideObject | FieldOverrideObject | + SettingOverrideObject + ) { + const existing = this.#overrides + .find(Overrides.FIND_PREDICATE(override.id)); + let override_: ComponentOverride | SettingOverride | FieldOverride; + + switch (override.type) { + case 'component': + override_ = new ComponentOverride(override as ComponentOverrideObject); + break; + case 'field': + override_ = new FieldOverride(override as FieldOverrideObject); + break; + case 'setting': + override_ = new SettingOverride(override as SettingOverrideObject); + break; + } + + if (existing) { + this.#builder?.logger.log( + 'Override already exists, updating definition.', + 'Old:', existing, + 'New:', override + ); + + this.#overrides.splice(this.#overrides.indexOf(existing), 1, override_); + this.emit('overrides.update', override); + + return override as ComponentOverride | FieldOverride | SettingOverride; + } + + this.#overrides.unshift(override_); + this.emit('overrides.add', this, override); + + return override as ComponentOverride | FieldOverride | SettingOverride; + } + + get ( + overrideType: 'component' | 'field' | 'setting', + target: string, + { output, setting }: { + output?: 'field' | 'component', + setting?: ComponentSettingsFieldObject + } = {} + ): FieldOverride | ComponentOverride | SettingOverride { + const strategy = this.#builder?.options?.overrideStrategy; + const overrides = this.#overrides.filter(override => + override.type === overrideType && + (override.targets.includes('*') || override.targets.includes(target)) + ); + + switch (overrideType) { + case 'component': { + const override = strategy === 'merge' + ? this.merge(overrides) : overrides[0]; + + switch (output) { + case 'field': { + const newComponentField = (override as ComponentOverride)?.fields + ?.find(f => f.key === setting?.key); + + return Object.assign( + { type: newComponentField?.type || setting.type }, + this.#builder.getOverride('field', + newComponentField?.type || setting?.type), + omit(newComponentField || {}, ['type', 'key']) + ) as FieldOverride; + } + default: + return override as ComponentOverride; + } + } + case 'setting': + return overrides?.find((o: SettingOverride) => ( + o.key === setting?.key + )) as SettingOverride; + default: + return strategy === 'merge' ? this.merge(overrides) : overrides[0]; + } + } + + remove (id: string) { + if (!id) { + return; + } + + const index = this.#overrides.findIndex(Overrides.FIND_PREDICATE(id)); + + if (index !== -1) { + this.#builder?.logger.log('Removing override:', this.#overrides[index]); + + const override = this.#overrides[index]; + this.#overrides.splice(index, 1); + this.emit('overrides.remove', this, override); + } + } + + merge ( + overrides: Array + ): FieldOverride | ComponentOverride | SettingOverride { + return overrides.reduce((res, override) => { + Object.keys(override).forEach((key: string) => { + if ( + (override as any)[key] === null || + (override as any)[key as any] === undefined) { + delete (override as any)[key]; + } + }); + Object.assign(res, override); + + return res; + }, {} as FieldOverride | ComponentOverride | SettingOverride); + } + + all () { + return this.#overrides; + } +} diff --git a/packages/core/lib/Settings/index.d.ts b/packages/core/lib/Settings/index.d.ts deleted file mode 100644 index dfead7c2b..000000000 --- a/packages/core/lib/Settings/index.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - ComponentSettingsTab, - ComponentSettingsField, - ComponentSettingsTabObject, - ComponentSettingsFieldObject, -} from '../types'; -import { Builder } from '../Builder'; -import { Emitter } from '../Emitter'; - -export declare class Settings extends Emitter { - static TYPE_TAB: string; - static TYPE_SETTING: string; - - static SETTINGS_TAB_GENERAL: string; - static SETTINGS_TAB_STYLING: string; - static SETTINGS_TAB_RESPONSIVE: string; - - constructor(options?: { builder: Builder }); - - hasTab(id: string): boolean; - getTab(id: string): ComponentSettingsTab; - - hasSetting(id: string, opts?: { tabId?: string }): boolean; - getSetting(id: string, opts?: { tabId?: string }): ComponentSettingsField; - - add( - setting: ComponentSettingsTab | - ComponentSettingsTabObject | - ComponentSettingsField | - ComponentSettingsFieldObject - ): void; - remove(id: string): boolean; - - all(): Array; -} diff --git a/packages/core/lib/Settings/index.test.js b/packages/core/lib/Settings/index.test.ts similarity index 88% rename from packages/core/lib/Settings/index.test.js rename to packages/core/lib/Settings/index.test.ts index 976c636f9..3f9877826 100644 --- a/packages/core/lib/Settings/index.test.js +++ b/packages/core/lib/Settings/index.test.ts @@ -1,3 +1,4 @@ +import type { ComponentSettingsFieldObject } from '../types'; import Settings from './index'; describe('Settings', () => { @@ -6,7 +7,9 @@ describe('Settings', () => { const settings = new Settings(); settings.subscribe(cb); - const setting = { key: 'test', fields: [], label: 'Test' }; + const setting: ComponentSettingsFieldObject = { + type: 'text', key: 'test', label: 'Test', + }; settings.add(setting); expect(settings.getSetting('test')).toMatchObject(setting); diff --git a/packages/core/lib/Settings/index.test.js.snap b/packages/core/lib/Settings/index.test.ts.snap similarity index 95% rename from packages/core/lib/Settings/index.test.js.snap rename to packages/core/lib/Settings/index.test.ts.snap index 719359e6d..c74639404 100644 --- a/packages/core/lib/Settings/index.test.js.snap +++ b/packages/core/lib/Settings/index.test.ts.snap @@ -4,6 +4,7 @@ exports[`Settings should allow to add a new tab and add settings to it 1`] = ` [ ComponentSettingsTab { "condition": undefined, + "displayable": undefined, "fields": [], "id": "general", "priority": 0, @@ -12,6 +13,7 @@ exports[`Settings should allow to add a new tab and add settings to it 1`] = ` }, ComponentSettingsTab { "condition": undefined, + "displayable": undefined, "fields": [ ComponentSettingsField { "condition": undefined, diff --git a/packages/core/lib/Settings/index.js b/packages/core/lib/Settings/index.ts similarity index 53% rename from packages/core/lib/Settings/index.js rename to packages/core/lib/Settings/index.ts index 240c18136..f86011637 100644 --- a/packages/core/lib/Settings/index.js +++ b/packages/core/lib/Settings/index.ts @@ -1,38 +1,76 @@ -import { ComponentSettingsTab, ComponentSettingsField } from '../types'; +import type { + ComponentSettingsTabObject, + ComponentSettingsFieldObject, + ElementObject, +} from '../types'; +import { + ComponentSettingsTab, + ComponentSettingsField, +} from '../classes'; import Emitter from '../Emitter'; +import Builder from '../Builder'; -export default class Settings extends Emitter { - static TYPE_TAB = 'tab'; +export declare abstract class ISettings { + static TYPE_TAB: string; + static TYPE_SETTING: string; + + static SETTINGS_TAB_GENERAL: string; + static SETTINGS_TAB_STYLING: string; + static SETTINGS_TAB_RESPONSIVE: string; + + constructor(options?: { builder: Builder }); + + hasTab(id: string): boolean; + getTab(id: string): ComponentSettingsTab; + + hasSetting(id: string, opts?: { tabId?: string }): boolean; + getSetting(id: string, opts?: { tabId?: string }): ComponentSettingsField; + + add( + setting: ComponentSettingsTab | + ComponentSettingsTabObject | + ComponentSettingsField | + ComponentSettingsFieldObject + ): void; + remove(id: string): boolean; + + all(): Array; +} + +export default class Settings extends Emitter implements ISettings { + static TYPE_TAB: string = 'tab'; static TYPE_SETTING = 'setting'; static SETTINGS_TAB_GENERAL = 'general'; static SETTINGS_TAB_STYLING = 'styling'; static SETTINGS_TAB_RESPONSIVE = 'responsive'; - #builder = null; - #tabs = null; + #builder: Builder = null; + #tabs: Array = null; - constructor ({ builder } = {}) { + constructor ({ builder }: { builder?: Builder } = {}) { super(); this.#builder = builder; this.#tabs = [new ComponentSettingsTab({ type: Settings.TYPE_TAB, id: Settings.SETTINGS_TAB_GENERAL, - title: t => t('core.settings.title', 'Settings'), + title: ( + t: (key: string, def: string) => string + ) => t('core.settings.title', 'Settings'), fields: [], })]; } - hasTab (id) { + hasTab (id: string) { return this.#tabs.some(ComponentSettingsTab.FIND_PREDICATE(id)); } - getTab (id) { + getTab (id: string) { return this.#tabs.find(ComponentSettingsTab.FIND_PREDICATE(id)); } - hasSetting (id, { tabId } = {}) { + hasSetting (id: string, { tabId }: { tabId?: string} = {}) { if (tabId) { return this.getTab(tabId)?.fields .some(ComponentSettingsField.FIND_PREDICATE(id)); @@ -47,7 +85,7 @@ export default class Settings extends Emitter { return false; } - getSetting (id, { tabId } = {}) { + getSetting (id: string, { tabId }: { tabId?: string} = {}) { if (tabId) { return this.getTab(tabId)?.fields .find(ComponentSettingsField.FIND_PREDICATE(id)); @@ -63,46 +101,53 @@ export default class Settings extends Emitter { } } - add (setting) { + add ( + setting: ComponentSettingsTab | ComponentSettingsTabObject | + ComponentSettingsField | ComponentSettingsFieldObject + ) { if ( - setting.type === Settings.TYPE_TAB && + (setting as ComponentSettingsFieldObject).type === Settings.TYPE_TAB && !this.hasTab(setting.id) ) { - setting = new ComponentSettingsTab(setting); - this.#tabs.push(setting); + const setting_ = new ComponentSettingsTab( + setting as ComponentSettingsTabObject + ); + this.#tabs.push(setting_); this.emit('tabs.add', setting); return; } - setting = new ComponentSettingsField(setting); + const setting_ = new ComponentSettingsField( + setting as ComponentSettingsFieldObject + ) as ComponentSettingsField; - const tab = setting.tab && this.hasTab(setting.tab) - ? this.getTab(setting.tab) + const tab = setting_.tab && this.hasTab(setting_.tab) + ? this.getTab(setting_.tab) : this.#tabs.find(ComponentSettingsTab.FIND_PREDICATE( Settings.SETTINGS_TAB_GENERAL )); const existing = this - .getSetting(setting.id || setting.key, { tabId: tab.id }); + .getSetting((setting_.id || setting_.key) as string, { tabId: tab.id }); if (existing) { this.#builder?.logger.log( 'Setting already exists, updating definition.', 'Old:', existing, - 'New:', setting + 'New:', setting_ ); const index = tab.fields.indexOf(existing); - tab.fields[index] = setting; - this.emit('settings.update', setting, tab); + tab.fields[index] = setting_; + this.emit('settings.update', setting_, tab); } else { - tab.fields.push(setting); - this.emit('settings.add', setting, tab); + tab.fields.push(setting_); + this.emit('settings.add', setting_, tab); } } - remove (id) { + remove (id: string) { const tabIndex = this.#tabs .findIndex(ComponentSettingsTab.FIND_PREDICATE(id)); @@ -132,7 +177,10 @@ export default class Settings extends Emitter { return false; } - getDisplayable (element, { fields = this.#tabs } = {}) { + getDisplayable ( + element: ElementObject, + { fields = this.#tabs }: { fields?: Array} = {} + ): Array { const displayable = []; for (const setting of fields) { diff --git a/packages/core/lib/Store/index.d.ts b/packages/core/lib/Store/index.d.ts deleted file mode 100644 index 55fd3502c..000000000 --- a/packages/core/lib/Store/index.d.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { - Component, - ComponentOverride, - ComponentSettingsFieldKeyTuple, - ElementId, - ElementObject, -} from '../types'; -import { Builder } from '../Builder'; -import { Emitter } from '../Emitter'; - -export declare interface StoreSanitizeOptions { - component?: Component; - override?: ComponentOverride; - resetIds?: boolean; -} - -export declare interface StoreFindOptions { - parent?: Array; -} - -export declare type StoreFindDeepOptions = Partial; - -export declare class Store extends Emitter { - constructor(options?: { builder: Builder }); - - /** - * Check if an element id is valid. - * An Element ID is a string or a number, and cannot be empty. - */ - isIdValid(id: string | number): boolean; - - /** Check if two element ids are the same */ - isSameElement(elementId: ElementId, siblingId: ElementId): boolean; - - /** Get the content of the store */ - get(): Array; - - /** - * Set the content of the store. - * If the emit option is set to false, the store will not emit a change event. - */ - set(content: Array, options?: { emit?: boolean }): void; - - /** - * Sanitize an element object (adds missing properties, ids, etc.) - * If the resetIds option is set to true, the element id will be reset even - * if it already exists. - */ - sanitize( - element: ElementObject, - options?: StoreSanitizeOptions - ): ElementObject; - - /** Creates a new element object based on an existing component (or not) */ - createElement(type: string, options?: Partial<{ - baseElement?: ElementObject; - } & StoreSanitizeOptions>): ElementObject; - - /** Adds an element to the store */ - addElement(element: ElementObject, options?: Partial<{ - position?: 'before' | 'after'; - } & StoreFindOptions & StoreSanitizeOptions>): ElementObject; - - /** Adds multiple elements to the store */ - addElements(elements: Array, options?: Partial<{ - position?: 'before' | 'after'; - } & StoreFindOptions & StoreSanitizeOptions>): Array; - - /** Finds an element in the store */ - getElement(id: ElementId, options?: StoreFindDeepOptions): ElementObject; - - /** Removes an element from the the store */ - removeElement(id: ElementId, options?: StoreFindDeepOptions): boolean; - - /** Updates an element in the store with new props */ - setElement( - id: ElementId, - newContent: object, - options?: StoreFindDeepOptions - ): ElementObject; - - /** Moves an element next to a sibling (inside the same parent or not) */ - moveElement( - element: ElementObject, - sibling: ElementObject, - options?: Partial - ): ElementObject; - - /** Duplicate an element */ - duplicateElement( - element: ElementObject, - options?: StoreFindOptions - ): ElementObject; - - /** Recursively finds the nearest parent of an element */ - findNearestParent( - id: ElementId, - options?: StoreFindOptions - ): Array; - - /** Recursively checks if an element is inside a parent */ - contains(id: ElementId, options?: StoreFindOptions): boolean; - - /** Retrieves the setting value of an element */ - getElementSettings( - element: ElementObject, - key: string | Array | Array, - def?: any - ): any; - - /** Sets the setting value of an element */ - setElementSettings( - element: ElementObject, - key: string | Array | Array, - value: any - ): void; - - /** Commit changes into history */ - commit(): void; - - /** Undo the last change */ - undo(): void; - - /** Redo the last change */ - redo(): void; - - canUndo(): boolean; - canRedo(): boolean; - resetHistory(): void; -} diff --git a/packages/core/lib/Store/index.test.js b/packages/core/lib/Store/index.test.ts similarity index 98% rename from packages/core/lib/Store/index.test.js rename to packages/core/lib/Store/index.test.ts index 8901e8386..e3509e483 100644 --- a/packages/core/lib/Store/index.test.js +++ b/packages/core/lib/Store/index.test.ts @@ -1,10 +1,10 @@ +import { ComponentOverride } from '../classes'; import { baseAddon, titleComponent } from '../addons'; import Builder from '../Builder'; -import { ComponentOverride } from '../types'; import Store from './index'; describe('Store', () => { - const getBuilder = opts => { + const getBuilder = (opts?: ConstructorParameters[0]) => { let id = 0; return new Builder({ diff --git a/packages/core/lib/Store/index.test.js.snap b/packages/core/lib/Store/index.test.ts.snap similarity index 100% rename from packages/core/lib/Store/index.test.js.snap rename to packages/core/lib/Store/index.test.ts.snap diff --git a/packages/core/lib/Store/index.js b/packages/core/lib/Store/index.ts similarity index 54% rename from packages/core/lib/Store/index.js rename to packages/core/lib/Store/index.ts index d97ae3542..179a578d6 100644 --- a/packages/core/lib/Store/index.js +++ b/packages/core/lib/Store/index.ts @@ -1,24 +1,154 @@ import { exists, get, set, cloneDeep } from '@junipero/core'; +import type { + ComponentObject, + ComponentOverrideObject, + ComponentSettingsFieldKeyTuple, + ElementId, + ElementObject, + ElementSettingsComplexKey, + ElementSettingsKeyObject, + StoreSanitizeOptions, + StoreFindOptions, + StoreFindDeepOptions, +} from '../types'; +import { + Component, + ComponentOverride, +} from '../classes'; import Emitter from '../Emitter'; +import Builder from '../Builder'; + +export declare abstract class IStore { + constructor(options?: { builder: Builder }); + + /** + * Check if an element id is valid. + * An Element ID is a string or a number, and cannot be empty. + */ + isIdValid(id: string | number): boolean; + + /** Check if two element ids are the same */ + isSameElement(elementId: ElementId, siblingId: ElementId): boolean; + + /** Get the content of the store */ + get(): Array; + + /** + * Set the content of the store. + * If the emit option is set to false, the store will not emit a change event. + */ + set(content: Array, options?: { emit?: boolean }): void; + + /** + * Sanitize an element object (adds missing properties, ids, etc.) + * If the resetIds option is set to true, the element id will be reset even + * if it already exists. + */ + sanitize( + element: ElementObject, + options?: StoreSanitizeOptions + ): ElementObject; + + /** Creates a new element object based on an existing component (or not) */ + createElement(type: string, options?: Partial<{ + baseElement?: ElementObject; + component?: ComponentObject; + override?: ComponentOverrideObject; + } & StoreSanitizeOptions>): ElementObject; + + /** Adds an element to the store */ + addElement(element: ElementObject, options?: Partial<{ + position?: 'before' | 'after'; + } & StoreFindOptions & StoreSanitizeOptions>): ElementObject; + + /** Adds multiple elements to the store */ + addElements(elements: Array, options?: Partial<{ + position?: 'before' | 'after'; + } & StoreFindOptions & StoreSanitizeOptions>): Array; + + /** Finds an element in the store */ + getElement(id: ElementId, options?: StoreFindDeepOptions): ElementObject; + + /** Removes an element from the the store */ + removeElement(id: ElementId, options?: StoreFindDeepOptions): boolean; + + /** Updates an element in the store with new props */ + setElement( + id: ElementId, + newContent: object, + options?: StoreFindDeepOptions + ): ElementObject; + + /** Moves an element next to a sibling (inside the same parent or not) */ + moveElement( + element: ElementObject, + sibling: ElementObject, + options?: Partial + ): ElementObject; + + /** Duplicate an element */ + duplicateElement( + element: ElementObject, + options?: StoreFindOptions + ): ElementObject; + + /** Recursively finds the nearest parent of an element */ + findNearestParent( + id: ElementId, + options?: StoreFindOptions + ): Array; + + /** Recursively checks if an element is inside a parent */ + contains(id: ElementId, options?: StoreFindOptions): boolean; + + /** Retrieves the setting value of an element */ + getElementSettings( + element: ElementObject, + key: string | Array | Array, + def?: any + ): any; + + /** Sets the setting value of an element */ + setElementSettings( + element: ElementObject, + key: string | Array | Array, + value: any + ): void; + + /** Commit changes into history */ + commit(): void; + + /** Undo the last change */ + undo(): void; + + /** Redo the last change */ + redo(): void; + + canUndo(): boolean; + canRedo(): boolean; + resetHistory(): void; +} -export default class Store extends Emitter { - #content = []; - #history = []; - #historyIndex = 0; - #builder = null; +export default class Store extends Emitter implements IStore { + #content: ElementObject[] = []; + #history: ElementObject[] = []; + #historyIndex: number = 0; + #builder: Builder = null; - constructor ({ builder }) { + constructor ({ builder }: { builder: Builder }) { super(); this.#builder = builder; } - isIdValid (id) { + isIdValid (id: ElementId) { return exists(id) && id !== ''; } - isSameElement (elementId, siblingId) { + isSameElement (elementId: ElementId, siblingId: ElementId) { return this.isIdValid(elementId) && this.isIdValid(siblingId) && elementId === siblingId; } @@ -27,7 +157,7 @@ export default class Store extends Emitter { return this.#content; } - set (content, { emit = true } = {}) { + set (content: Array, { emit = true } = {}) { this.#content = (Array.isArray(content) ? content : []) .map(e => this.sanitize(e, { withDefaults: true })); @@ -35,17 +165,22 @@ export default class Store extends Emitter { this.commit(); } - sanitize (element, { + sanitize (element: ElementObject, { component: c, override: o, ...opts + }: { + component?: Component | ComponentObject, + override?: ComponentOverride, + [_: string]: any } = {}) { if (!this.isIdValid(element.id) || opts.resetIds) { element.id = this.#builder.generateId(); } const component = c || this.#builder.getComponent(element.type); - const override = o || this.#builder.getOverride('component', element.type); + const override = o || this.#builder + .getOverride('component', element.type) as ComponentOverride; if (opts.withDefaults) { element = { @@ -66,7 +201,7 @@ export default class Store extends Emitter { component?.getContainers?.(element) || [element.content]; - containers.forEach(container => { + containers.forEach((container: ElementObject[]) => { if (Array.isArray(container)) { container.forEach((elmt, i) => { container[i] = this.sanitize(elmt, opts); @@ -77,14 +212,25 @@ export default class Store extends Emitter { return element; } - createElement (type, { + createElement (type: string, { component: c, override: o, baseElement, ...opts + }: { + component?: ComponentObject, + override?: ComponentOverride | ComponentOverrideObject, + baseElement?: ElementObject, + [_: string]: any } = {}) { - const component = c || this.#builder.getComponent(type); - const override = o || this.#builder.getOverride('component', component.id); + const component = c + ? new Component(c) : new Component(this.#builder.getComponent(type)); + const override = o + ? new ComponentOverride(o as ComponentOverrideObject) + : this.#builder.getOverride( + 'component', + component.id + ) as ComponentOverride; baseElement = { ...baseElement, ...component.construct?.({ @@ -106,14 +252,19 @@ export default class Store extends Emitter { }) || {}), }; - return this.sanitize(element, { component: c, override: o, ...opts }); + return this.sanitize(element, { component, override, ...opts }); } - addElement (element, { + addElement (element: ElementObject, { parent = this.#content, position = 'after', component, ...opts + }: { + parent?: Array, + position?: 'before' | 'after', + component?: ComponentObject, + [_: string]: any } = {}) { this.#builder.logger.log('Adding element:', element, { parent, position }); @@ -138,7 +289,7 @@ export default class Store extends Emitter { return element; } - addElements (elements, { + addElements (elements: Array, { parent = this.#content, position = 'after', ...opts @@ -163,7 +314,13 @@ export default class Store extends Emitter { return elements; } - getElement (id, { parent = this.#content, deep = false } = {}) { + getElement ( + id: ElementId, + { parent = this.#content, deep = false }: { + deep?: boolean, + parent?: ElementObject[] + } = {} + ): ElementObject { if (!this.isIdValid(id)) { return; } @@ -178,9 +335,10 @@ export default class Store extends Emitter { for (const child of parent) { const component = this.#builder.getComponent(child.type); const override = this.#builder.getOverride('component', child.type); - const containers = override?.getContainers?.(child) || + const containers: ElementObject[][] = + (override as ComponentOverride)?.getContainers?.(child) || component?.getContainers?.(child) || - [child.content]; + [child.content as ElementObject[]]; for (const container of containers) { const found = this.getElement(id, { parent: container, deep }); @@ -193,7 +351,13 @@ export default class Store extends Emitter { } } - removeElement (id, { parent = this.#content, deep } = {}) { + removeElement ( + id: ElementId, + { parent = this.#content, deep }: { + parent?: Array, + deep?: boolean + } = {} + ) { if (!this.isIdValid(id)) { return; } @@ -212,9 +376,10 @@ export default class Store extends Emitter { for (const child of parent) { const component = this.#builder.getComponent(child.type); const override = this.#builder.getOverride('component', child.type); - const containers = override?.getContainers?.(child) || + const containers = + (override as ComponentOverride)?.getContainers?.(child) || component?.getContainers?.(child) || - [child.content]; + [child.content as ElementObject[]]; for (const container of containers) { const removed = this.removeElement(id, { parent: container, deep }); @@ -229,10 +394,13 @@ export default class Store extends Emitter { return false; } - setElement (id, newContent, { - element: e, - parent = this.#content, - deep, + setElement ( + id: ElementId, + newContent: Partial, + { element: e, parent = this.#content, deep }: { + element?: ElementObject, + parent?: Array, + deep?: boolean } = {}) { if (!this.isIdValid(id)) { return; @@ -240,7 +408,8 @@ export default class Store extends Emitter { const element = e || this.getElement(id, { parent, deep }); const component = this.#builder.getComponent(element.type); - const override = this.#builder.getOverride('component', element.type); + const override: ComponentOverride = + this.#builder.getOverride('component', element.type) as ComponentOverride; const serialize = override?.serialize || component?.serialize; @@ -256,7 +425,14 @@ export default class Store extends Emitter { return element; } - moveElement (element, sibling, { parent = this.#content, position } = {}) { + moveElement ( + element?: ElementObject, + sibling?: ElementObject, + { parent = this.#content, position }: { + parent?: Array, + position?: 'before' | 'after' + } = {} + ) { if ( this.isSameElement(element?.id, sibling?.id) || this.contains(sibling.id, { parent: element }) @@ -286,10 +462,11 @@ export default class Store extends Emitter { return retrievedElement; } - duplicateElement (element, { parent = this.#content } = {}) { + duplicateElement (element: ElementObject, { parent = this.#content } = {}) { let newElmt = this.sanitize(cloneDeep(element), { resetIds: true }); const component = this.#builder.getComponent(element.type); - const override = this.#builder.getOverride('component', element.type); + const override: ComponentOverride = + this.#builder.getOverride('component', element.type) as ComponentOverride; const duplicate = override?.duplicate || component?.duplicate; if (typeof duplicate === 'function') { @@ -308,7 +485,10 @@ export default class Store extends Emitter { return newElmt; } - findNearestParent (id, { parent = this.#content } = {}) { + findNearestParent ( + id: ElementId, + { parent = this.#content } = {} + ): Array { // First check if element in inside direct parent to avoid trying to // find every component & override for every nested level for (const e of parent) { @@ -323,7 +503,8 @@ export default class Store extends Emitter { // (using the cols property) for (const e of parent) { const component = this.#builder.getComponent(e.type); - const override = this.#builder.getOverride('component', e.type); + const override: ComponentOverride = + this.#builder.getOverride('component', e.type) as ComponentOverride; const containers = override?.getContainers?.(e) || component?.getContainers?.(e) || @@ -345,23 +526,27 @@ export default class Store extends Emitter { return null; } - contains (id, { parent = this.#content } = {}) { + contains ( + id: ElementId, + { parent = this.#content }: { parent?: ElementObject | Array } = {} + ) { // Force parent to be an array to be able to loop over it // ----- // In some cases (Store.moveElement for example), parent cannot be // `element.content` because it would prevent rows (with `element.cols`) // or other container-based elements from being checked by `contains` - parent = [].concat(parent); + const parent_ = [].concat(parent); - for (const e of parent) { + for (const e of parent_) { if (this.isSameElement(e?.id, id)) { return true; } } - for (const e of parent) { + for (const e of parent_) { const component = this.#builder.getComponent(e.type); - const override = this.#builder.getOverride('component', e.type); + const override: ComponentOverride = + this.#builder.getOverride('component', e.type) as ComponentOverride; const containers = override?.getContainers?.(e) || component?.getContainers?.(e) || @@ -384,15 +569,31 @@ export default class Store extends Emitter { // (element, { from: 'size', to: 'settings.size' }) // (element, [{ from: 'size', to: 'settings.size' }]) // (element, ['settings.size', 'settings.url']) - getElementSettings (element, key, def) { + getElementSettings ( + element: ElementObject, + key: ElementSettingsKeyObject, + def?: any + ) { if (Array.isArray(key)) { - return key.reduce((res, k) => { - res[k?.to || k] = get(element, k?.from || k, k?.default ?? def); + return key.reduce(( + res: any, + k: { from: string, to: string, default: string} | string + ) => { + res[(k as ElementSettingsComplexKey)?.to || k as string] = + get( + element, + (k as ElementSettingsComplexKey)?.from || k as string, + (k as ElementSettingsComplexKey)?.default ?? def as string + ); return res; }, {}); } else { - return get(element, key?.from || key, key?.default ?? def); + return get( + element, + (key as ElementSettingsComplexKey)?.from || key as string, + (key as ElementSettingsComplexKey)?.default ?? def + ); } } @@ -401,15 +602,29 @@ export default class Store extends Emitter { // (element, { from: 'size', to: 'settings.size' }, { size: 20 }) // (element, [{ from: 'size', to: 'settings.size' }], { size: 20 }) // (element, ['size', 'url'], { size: 20, url: '...' }) - setElementSettings (element, key, value) { + setElementSettings ( + element: ElementObject, + key: ElementSettingsKeyObject | string, + value: any + ) { if (Array.isArray(key)) { key.forEach(k => - set(element, k.to || k, get(value, k.from || k, k.default))); + set( + element, + (k as ElementSettingsComplexKey)?.to || k as string, + get(value, (k as ElementSettingsComplexKey)?.from || k as string, + (k as ElementSettingsComplexKey).default) + ) + ); } else { set( element, - key?.to || key, - key?.from ? get(value, key.from, key?.default) : value ?? key?.default + (key as ElementSettingsComplexKey)?.to || key as string, + (key as ElementSettingsComplexKey)?.from ? get( + value, + (key as ElementSettingsComplexKey).from, + (key as ElementSettingsComplexKey)?.default + ) : value ?? (key as ElementSettingsComplexKey)?.default ); } diff --git a/packages/core/lib/Texts/index.d.ts b/packages/core/lib/Texts/index.d.ts deleted file mode 100644 index 82f867f57..000000000 --- a/packages/core/lib/Texts/index.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TextsSheet, TextsSheetObject, GetTextCallback } from '../types'; -import { Builder } from '../Builder'; -import { Emitter } from '../Emitter'; - -export declare class Texts extends Emitter { - constructor(options?: { builder: Builder }); - - /** Retrieves a text sheet using its id */ - getSheet(id: string): TextsSheet; - - /** Adds a new texts sheet */ - addSheet(sheet: TextsSheetObject | TextsSheet): TextsSheet; - - /** Updates an existing text sheet */ - setSheet(sheet: TextsSheet): TextsSheet; - - /** Removes a text sheet */ - removeSheet(id: string): boolean; - - /** Retrieves the current active sheet id */ - getActiveSheet(): string; - - /** Sets the current active sheet id */ - setActiveSheet(id: string): void; - - /** Retrieves a text from the current active sheet */ - get(key: string | GetTextCallback, def?: any): any; - - /** Updates a text key in the current active sheet */ - set(key: string, value: any): void; -} diff --git a/packages/core/lib/Texts/index.test.js b/packages/core/lib/Texts/index.test.ts similarity index 100% rename from packages/core/lib/Texts/index.test.js rename to packages/core/lib/Texts/index.test.ts diff --git a/packages/core/lib/Texts/index.js b/packages/core/lib/Texts/index.ts similarity index 61% rename from packages/core/lib/Texts/index.js rename to packages/core/lib/Texts/index.ts index 0eab8db71..1b5decb44 100644 --- a/packages/core/lib/Texts/index.js +++ b/packages/core/lib/Texts/index.ts @@ -1,24 +1,54 @@ import { get, set } from '@junipero/core'; +import type { GetTextCallback, TextsSheetObject } from '../types'; +import { TextsSheet } from '../classes'; import Emitter from '../Emitter'; -import { TextsSheet } from '../types'; +import Builder from '../Builder'; -export default class Texts extends Emitter { - #sheets = []; - #activeSheet = null; - #builder = null; +export declare abstract class ITexts { + constructor(options?: { builder: Builder }); - constructor ({ builder } = {}) { + /** Retrieves a text sheet using its id */ + getSheet(id: string): TextsSheet; + + /** Adds a new texts sheet */ + addSheet(sheet: TextsSheetObject | TextsSheet): TextsSheet; + + /** Updates an existing text sheet */ + setSheet(sheet: TextsSheet): TextsSheet; + + /** Removes a text sheet */ + removeSheet(id: string): boolean; + + /** Retrieves the current active sheet id */ + getActiveSheet(): string; + + /** Sets the current active sheet id */ + setActiveSheet(id: string): void; + + /** Retrieves a text from the current active sheet */ + get(key: string | GetTextCallback, def?: any): any; + + /** Updates a text key in the current active sheet */ + set(key: string, value: any): void; +} + +export default class Texts extends Emitter implements ITexts { + #sheets: Array = []; + #activeSheet: number = null; + #builder: Builder = null; + + constructor ({ builder }: { builder?: Builder } = {}) { super(); this.#builder = builder; } - getSheet (id) { + getSheet (id: string) { return this.#sheets.find(TextsSheet.FIND_PREDICATE(id)); } - addSheet (sheet) { + addSheet (sheet: TextsSheetObject) { sheet = new TextsSheet(sheet); const existing = this.getSheet(sheet.id); @@ -40,7 +70,7 @@ export default class Texts extends Emitter { return sheet; } - setSheet (sheet) { + setSheet (sheet: TextsSheetObject) { const index = this.#sheets.findIndex(TextsSheet.FIND_PREDICATE(sheet.id)); if (index !== -1) { @@ -51,7 +81,7 @@ export default class Texts extends Emitter { return sheet; } - removeSheet (id) { + removeSheet (id: string) { const index = this.#sheets.findIndex(TextsSheet.FIND_PREDICATE(id)); if (index !== -1) { @@ -68,7 +98,7 @@ export default class Texts extends Emitter { return this.#sheets[this.#activeSheet ?? 0]?.id; } - setActiveSheet (id) { + setActiveSheet (id: string) { const index = this.#sheets.findIndex(TextsSheet.FIND_PREDICATE(id)); if (index !== -1) { @@ -80,7 +110,7 @@ export default class Texts extends Emitter { } } - get (key, def) { + get (key: Function | string, def: string) { if (typeof key === 'function') { return key(this.get.bind(this)); } @@ -96,7 +126,7 @@ export default class Texts extends Emitter { return get(activeSheet.texts, key, def ?? key); } - set (key, value) { + set (key: string, value: string) { const activeSheet = this.#sheets[this.#activeSheet ?? 0]; if (!activeSheet) { diff --git a/packages/core/lib/addons.d.ts b/packages/core/lib/addons.d.ts deleted file mode 100644 index 31301c59f..000000000 --- a/packages/core/lib/addons.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - AddonObject, - ComponentObject, - ComponentSettingsTabObject, - ComponentsGroupObject, - FieldObject, -} from './types'; - -export function textField(...props: any[]): FieldObject; -export function textareaField(...props: any[]): FieldObject; -export function selectField(...props: any[]): FieldObject; -export function colorField(...props: any[]): FieldObject; -export function imageField(...props: any[]): FieldObject; -export function dateField(...props: any[]): FieldObject; -export function toggleField(...props: any[]): FieldObject; - -export function rowComponent(...props: any[]): ComponentObject; -export function colComponent(...props: any[]): ComponentObject; -export function emptySpaceComponent(...props: any[]): ComponentObject; -export function titleComponent(...props: any[]): ComponentObject; -export function textComponent(...props: any[]): ComponentObject; -export function imageComponent(...props: any[]): ComponentObject; -export function buttonComponent(...props: any[]): ComponentObject; -export function foldableComponent(...props: any[]): ComponentObject; - -export function stylingSettings(...props: any[]): ComponentSettingsTabObject; -export function responsiveSettings(...props: any[]): ComponentSettingsTabObject; - -export function baseFields(): Array; -export function baseComponents(): Array; -export function baseSettings(): Array; - -export function coreComponentsGroup(...props: any[]): ComponentsGroupObject; - -export function baseAddon(): AddonObject; diff --git a/packages/core/lib/addons.js b/packages/core/lib/addons.js deleted file mode 100644 index 9a0901431..000000000 --- a/packages/core/lib/addons.js +++ /dev/null @@ -1,904 +0,0 @@ -export const textField = (...props) => ({ - type: 'text', - deserialize: val => '' + val, - render: () => null, - ...props, -}); - -export const textareaField = (...props) => ({ - type: 'textarea', - deserialize: val => '' + val, - render: () => null, - ...props, -}); - -export const selectField = (...props) => ({ - type: 'select', - render: () => null, - ...props, -}); - -export const colorField = (...props) => ({ - type: 'color', - render: () => null, - ...props, -}); - -export const imageField = (...props) => ({ - type: 'image', - render: () => null, - ...props, -}); - -export const dateField = (...props) => ({ - type: 'date', - render: () => null, - ...props, -}); - -export const toggleField = (...props) => ({ - type: 'toggle', - render: () => null, - ...props, -}); - -export const rowComponent = (...props) => ({ - id: 'row', - name: t => t('core.components.row.name', 'Row'), - type: 'component', - render: () => null, - icon: 'row', - draggable: false, - droppable: false, - hasCustomInnerContent: true, - editable: true, - options: [], - settings: { - title: t => t('core.components.row.settings.title', 'Row options'), - floatingSettings: { - placement: 'right-start', - shift: { enabled: true }, - autoPlacement: { alignment: 'start' }, - }, - fields: [{ - type: 'select', - key: 'settings.flexDirection', - default: 'row', - label: t => - t('core.components.row.settings.flexDirection.title', 'Direction'), - options: [{ - title: t => t('core.components.row.settings.flexDirection.row', - 'Row (left to right)'), - value: 'row', - }, { - title: t => t('core.components.row.settings.flexDirection.rowReverse', - 'Reversed row (right to left)'), - value: 'row-reverse', - }, { - title: t => t('core.components.row.settings.flexDirection.column', - 'Column (top to bottom)'), - value: 'column', - }, { - title: t => t( - 'core.components.row.settings.flexDirection.columnReverse', - 'Reversed column (bottom to top)', - ), - value: 'column-reverse', - }], - }, { - type: 'select', - key: 'settings.justifyContent', - default: 'flex-start', - label: t => t('core.components.row.settings.justifyContent.title', - 'Horizontal alignment'), - options: [{ - title: t => t('core.components.row.settings.justifyContent.flexStart', - 'Left'), - value: 'flex-start', - }, { - title: t => t('core.components.row.settings.justifyContent.center', - 'Center'), - value: 'center', - }, { - title: t => t('core.components.row.settings.justifyContent.flexEnd', - 'Right'), - value: 'flex-end', - }, { - title: t => t( - 'core.components.row.settings.justifyContent.spaceBetween', - 'Space between columns' - ), - value: 'space-between', - }, { - title: t => t('core.components.row.settings.justifyContent.spaceAround', - 'Space around columns'), - value: 'space-around', - }], - }, { - type: 'select', - key: 'settings.alignItems', - default: 'flex-start', - label: t => t('core.components.row.settings.alignItems.title', - 'Vertical alignment'), - options: [{ - title: t => t('core.components.row.settings.alignItems.flexStart', - 'Top'), - value: 'flex-start', - }, { - title: t => t('core.components.row.settings.alignItems.center', - 'Center'), - value: 'center', - }, { - title: t => t('core.components.row.settings.alignItems.flexEnd', - 'Bottom'), - value: 'flex-end', - }, { - title: t => t('core.components.row.settings.alignItems.stretch', - 'Stretch'), - value: 'stretch', - }], - }, { - type: 'select', - key: 'settings.gutters', - default: true, - label: t => t('core.components.row.settings.gutters.title', - 'Column gap'), - options: [{ - title: t => t('core.components.row.settings.gutters.enabled', - 'Enabled'), - value: true, - }, { - title: t => t('core.components.row.settings.gutters.disabled', - 'Disabled'), - value: false, - }], - }], - }, - getContainers: element => [element.cols], - sanitize: (element, { builder } = {}) => { - const colComponent = builder.getComponent('col'); - - if (!colComponent) { - throw new Error('The `col` component is required to use rows.'); - } - - return { - ...element, - cols: !element.cols || !element.cols.length - ? [builder.createElement('col', { component: colComponent })] - : element.cols, - }; - }, - construct: ({ builder } = {}) => { - const colComponent = builder.getComponent('col'); - - if (!colComponent) { - throw new Error('The `col` component is required to use rows.'); - } - - return { - type: 'row', - settings: { - alignItems: 'flex-start', - }, - cols: [builder.createElement('col', { component: colComponent })], - }; - }, - ...props, -}); - -const COL_SIZES = Array.from({ length: 12 }).map((_, i) => ({ - title: t => (i + 1) + ' ' + t('core.components.col.settings.size.value', - 'column(s)'), - value: i + 1, -})).reverse(); - -const COL_RESPONSIVE_SETTINGS = [{ - title: t => t('core.responsive.fluid', 'Flexible'), - value: 'fluid', -}, { - title: t => t('core.responsive.auto', 'Adapted to content'), - value: 'auto', -}, ...COL_SIZES, { - title: t => t('core.responsive.hide', 'Hidden'), - value: 'hide', -}]; - -export const colComponent = (...props) => ({ - id: 'col', - type: 'component', - draggable: false, - droppable: false, - construct: ({ builder } = {}) => ({ - type: 'col', - content: [], - id: builder.generateId(), - style: {}, - }), - editable: true, - usable: false, - settings: { - title: t => t('core.components.col.settings.title', 'Col options'), - floatingSettings: { - placement: 'left-start', - shift: { enabled: true }, - autoPlacement: { enabled: false }, - }, - fields: [{ - type: 'select', - key: 'size', - default: 'fluid', - label: t => t('core.components.col.settings.size.title', 'Column size'), - options: [ - { - title: t => t('core.responsive.fluid', 'Flexible'), - value: 'fluid', - }, - { - title: t => t('core.responsive.auto', 'Adapted to content'), - value: 'auto', - }, - ...COL_SIZES, - ], - }, { - tab: 'responsive', - key: 'responsive.xl', - type: 'select', - label: t => t('core.responsive.xl', 'Extra-large screens'), - default: 'fluid', - options: COL_RESPONSIVE_SETTINGS, - }, { - tab: 'responsive', - key: 'responsive.lg', - type: 'select', - label: t => t('core.responsive.lg', 'Large screens (desktop)'), - default: 'fluid', - options: COL_RESPONSIVE_SETTINGS, - }, { - tab: 'responsive', - key: 'responsive.md', - type: 'select', - label: t => t('core.responsive.md', 'Medium screens (tablet)'), - default: 'fluid', - options: COL_RESPONSIVE_SETTINGS, - }, { - tab: 'responsive', - key: 'responsive.sm', - type: 'select', - label: t => t('core.responsive.sm', 'Small screens (phones)'), - default: 'fluid', - options: COL_RESPONSIVE_SETTINGS, - }, { - tab: 'responsive', - key: 'responsive.xs', - type: 'select', - label: t => t('core.responsive.xs', 'Extra-small screens (old phones)'), - default: 'fluid', - options: COL_RESPONSIVE_SETTINGS, - }], - defaults: { - responsive: false, - }, - }, - ...props, -}); - -export const emptySpaceComponent = (...props) => ({ - id: 'empty-space', - name: t => t('core.components.emptySpace.name', 'Blank space'), - type: 'component', - render: () => null, - icon: 'blank_space', - options: [], - settings: { - title: t => - t('core.components.emptySpace.settings.title', 'Blank space options'), - fields: [{ - type: 'text', - key: 'settings.height', - default: '32px', - displayable: true, - label: t => t('core.components.emptySpace.settings.height', 'Height'), - }], - }, - editable: true, - construct: () => ({ - type: 'empty-space', - settings: { - height: '32px', - }, - }), - ...props, -}); - -export const titleComponent = (...props) => ({ - id: 'title', - name: t => t('core.components.title.name', 'Title'), - type: 'component', - icon: 'title', - editable: true, - render: () => null, - options: [], - settings: { - title: t => t('core.components.title.settings.title', 'Title options'), - fields: [{ - type: 'select', - key: 'headingLevel', - default: 'h1', - displayable: true, - label: t => t('core.components.title.settings.type.title', 'Type'), - options: Array.from({ length: 6 }).map((_, i) => ({ - title: t => t('core.components.type.value', 'Title') + - ` ${i + 1} (h${i + 1})`, - value: `h${i + 1}`, - })), - }, { - type: 'textarea', - key: 'content', - default: '', - label: t => t('core.components.title.settings.content.title', 'Content'), - }], - }, - construct: ({ builder } = {}) => ({ - type: 'title', - content: builder - .getText('core.components.title.default', 'This is a title'), - headingLevel: 'h1', - }), - ...props, -}); - -export const textComponent = (...props) => ({ - id: 'text', - name: t => t('core.components.text.name', 'Text'), - type: 'component', - render: () => null, - icon: 'multiline', - options: [], - settings: { - title: t => t('core.components.text.settings.title', 'Text options'), - fields: [{ - type: 'textarea', - key: 'content', - default: '', - label: t => t('core.components.text.settings.content.title', 'Content'), - }], - }, - editable: true, - construct: ({ builder } = {}) => ({ - type: 'text', - content: builder.getText( - 'core.components.text.default', - 'This is some fancy text content' - ), - }), - ...props, -}); - -export const imageComponent = (...props) => ({ - id: 'image', - name: t => t('core.components.image.name', 'Image'), - type: 'component', - render: () => null, - icon: 'image', - editable: true, - options: [], - settings: { - title: t => t('core.components.image.settings.title', 'Image options'), - fields: [{ - type: 'image', - key: ['url', 'name'], - default: '', - label: t => t('core.components.image.settings.image.title', 'Image'), - }, { - type: 'select', - key: 'settings.size', - label: t => t( - 'core.components.image.settings.image.size.title', - 'Image size' - ), - default: 'auto', - displayable: true, - options: [{ - title: t => t( - 'core.components.image.settings.image.size.auto', - 'Adapted to content' - ), - value: 'auto', - }, { - title: t => t( - 'core.components.image.settings.image.size.full', - 'Real size' - ), - value: 'full', - }, { - title: t => t( - 'core.components.image.settings.image.size.custom', - 'Custom' - ), - value: 'custom', - }], - }, { - type: 'text', - key: 'settings.width', - displayable: true, - condition: element => - element?.settings?.size === 'custom', - label: t => t( - 'core.components.image.settings.image.size.width', - 'Image width' - ), - placeholder: t => t( - 'core.components.image.settings.image.size.width', - 'Image width' - ), - }, { - type: 'text', - key: 'settings.height', - displayable: true, - condition: element => - element?.settings?.size === 'custom', - label: t => t( - 'core.components.image.settings.image.size.height', - 'Image height' - ), - placeholder: t => t( - 'core.components.image.settings.image.size.height', - 'Image height' - ), - }, { - type: 'select', - key: 'settings.textAlign', - displayable: true, - label: t => t( - 'core.components.image.settings.image.align.title', - 'Image alignment' - ), - default: 'left', - options: [{ - title: t => t( - 'core.components.image.settings.image.align.left', - 'Left' - ), - value: 'left', - }, { - title: t => t( - 'core.components.image.settings.image.align.center', - 'Center' - ), - value: 'center', - }, { - title: t => t( - 'core.components.image.settings.image.align.right', - 'Right' - ), - value: 'right', - }], - }], - }, - construct: () => ({ - type: 'image', - url: '', - name: '', - }), - ...props, -}); - -export const buttonComponent = (...props) => ({ - id: 'button', - name: t => t('core.components.button.name', 'Button'), - type: 'component', - render: () => null, - icon: 'button', - options: [], - settings: { - title: t => t('core.components.button.settings.title', 'Button options'), - fields: [{ - type: 'textarea', - key: 'content', - default: '', - label: t => t('core.components.button.settings.content.title', 'Content'), - }, { - type: 'select', - key: 'action', - default: 'link', - displayable: true, - label: t => t('core.components.button.settings.action.title', 'Action'), - options: [{ - title: t => t( - 'core.components.button.settings.action.openLink', - 'Open a link' - ), - value: 'link', - }, { - title: t => t( - 'core.components.button.settings.action.fireEvent', - 'Trigger an event' - ), - value: 'event', - }], - }, { - type: 'text', - key: 'url', - default: '', - displayable: true, - label: t => t('core.components.button.settings.url.title', 'URL link'), - condition: element => element.action === 'link', - }, { - type: 'text', - key: 'event', - default: '', - displayable: true, - label: t => t( - 'core.components.button.settings.event.title', - 'Javascript event name' - ), - condition: element => element.action === 'event', - }, { - type: 'select', - key: 'settings.buttonType', - default: 'button', - label: t => t( - 'core.components.button.settings.type.title', - 'HTML element type' - ), - options: [{ - title: t => t('core.components.button.settings.type.button', 'Button'), - value: 'button', - }, { - title: t => t('core.components.button.settings.type.links', 'Link'), - value: 'link', - }], - }], - }, - editable: true, - construct: ({ builder } = {}) => ({ - type: 'button', - content: builder.getText('core.components.button.default', 'Click me !'), - action: 'link', - url: '', - settings: { - buttonType: 'button', - }, - }), - ...props, -}); - -export const foldableComponent = (...props) => ({ - id: 'foldable', - name: t => t('core.components.foldable.name', 'Foldable'), - type: 'component', - render: () => null, - icon: 'foldable', - editable: true, - hasCustomInnerContent: true, - draggable: false, - droppable: false, - getContainers: element => - [element.content, element.seeMore, element.seeLess], - settings: { - title: t => t( - 'core.components.foldable.settings.title', 'Foldable options'), - floatingSettings: { - placement: 'right-start', - autoPlacement: { - alignment: 'start', - }, - }, - fields: [{ - type: 'select', - key: 'settings.seeMorePosition', - default: 'after', - displayable: true, - label: t => t( - 'core.components.foldable.settings.seeMorePosition.title', - 'See more placement' - ), - options: [{ - title: t => t( - 'core.components.foldable.settings.seeMorePosition.before', - 'Before' - ), - value: 'before', - }, { - title: t => t( - 'core.components.foldable.settings.seeMorePosition.after', - 'After' - ), - value: 'after', - }], - }], - }, - construct: () => ({ - type: 'foldable', - settings: { - seeMorePosition: 'after', - }, - content: [], - seeMore: [], - seeLess: [], - }), - ...props, -}); - -export const stylingSettings = (...props) => ({ - id: 'styling', - type: 'tab', - title: t => t('core.styling.title', 'Styling'), - ...props, - fields: [...(props?.fields || []), { - label: t => t('core.styling.paddings.title', 'Inside spacing'), - fields: [{ - type: 'text', - key: 'styles.paddingTop', - placeholder: t => t('core.styling.paddings.top', 'Top'), - }, { - type: 'text', - key: 'styles.paddingRight', - placeholder: t => t('core.styling.paddings.right', 'Right'), - }, { - type: 'text', - key: 'styles.paddingBottom', - placeholder: t => t('core.styling.paddings.bottom', 'Bottom'), - }, { - type: 'text', - key: 'styles.paddingLeft', - placeholder: t => t('core.styling.paddings.left', 'Left'), - }], - }, { - label: t => t('core.styling.margins.title', 'Outside spacing'), - fields: [{ - type: 'text', - key: 'styles.marginTop', - placeholder: t => t('core.styling.margins.top', 'Top'), - }, { - type: 'text', - key: 'styles.marginRight', - placeholder: t => t('core.styling.margins.right', 'Right'), - }, { - type: 'text', - key: 'styles.marginBottom', - placeholder: t => t('core.styling.margins.bottom', 'Bottom'), - }, { - type: 'text', - key: 'styles.marginLeft', - placeholder: t => t('core.styling.margins.left', 'Left'), - }], - }, { - label: t => t('core.styling.background.image.title', 'Background image'), - fields: [{ - key: 'styles.backgroundImage', - type: 'image', - props: { - iconOnly: true, - }, - }, { - label: t => t('core.styling.background.size.title', 'Size'), - key: 'styles.backgroundSize', - type: 'select', - default: 'default', - placeholder: t => - t('core.styling.background.size.title', 'Background size'), - options: [{ - title: t => t('core.styling.background.size.default', 'Default'), - value: 'default', - }, { - title: t => t('core.styling.background.size.cover', 'Fill'), - value: 'cover', - }, { - title: t => t('core.styling.background.size.contain', 'Fit'), - value: 'contain', - }], - }, { - label: t => t('core.styling.background.position.title', 'Position'), - key: 'styles.backgroundPosition', - type: 'select', - default: 'center', - placeholder: t => - t('core.styling.background.position.title', 'Background position'), - options: [{ - title: t => t('core.styling.background.position.center', 'Centered'), - value: 'center', - }, { - title: t => t('core.styling.background.position.top', 'Top'), - value: 'top', - }, { - title: t => t('core.styling.background.position.right', 'Right'), - value: 'right', - }, { - title: t => t('core.styling.background.position.bottom', 'Bottom'), - value: 'bottom', - }, { - title: t => t('core.styling.background.position.left', 'Left'), - value: 'left', - }, { - title: t => - t('core.styling.background.position.centerTop', 'Center top'), - value: 'center top', - }, { - title: t => - t('core.styling.background.position.centerBottom', 'Center bottom'), - value: 'center bottom', - }, { - title: t => - t('core.styling.background.position.leftCenter', 'Center left'), - value: 'left center', - }, { - title: t => - t('core.styling.background.position.leftTop', 'Top left'), - value: 'left top', - }, { - title: t => - t('core.styling.background.position.leftBottom', 'Bottom left'), - value: 'left bottom', - }, { - title: t => - t('core.styling.background.position.rightCenter', 'Center right'), - value: 'right center', - }, { - title: t => - t('core.styling.background.position.rightTop', 'Top right'), - value: 'right top', - }, { - title: t => - t('core.styling.background.position.rightBottom', 'Bottom right'), - value: 'right bottom', - }], - }, { - label: t => t('core.styling.background.repeat.title', 'Repeat'), - key: 'styles.backgroundRepeat', - type: 'select', - default: 'no-repeat', - placeholder: t => - t('core.styling.background.repeat.title', 'Background repeat'), - options: [{ - title: t => - t('core.styling.background.repeat.noRepeat', 'No repeat'), - value: 'no-repeat', - }, { - title: t => - t('core.styling.background.repeat.repeatX', 'Repeat horizontally'), - value: 'repeat-x', - }, { - title: t => - t('core.styling.background.repeat.repeatY', 'Repeat vertically'), - value: 'repeat-y', - }, { - title: t => t('core.styling.background.repeat.both', - 'Repeat horizontally & vertically'), - value: 'repeat', - }, - ], - }], - }, { - label: t => - t('core.styling.background.color.title', 'Background color'), - placeholder: '#FFF', - type: 'color', - key: 'styles.backgroundColor', - }, { - label: t => - t('core.styling.className.title', 'Additional CSS class'), - type: 'text', - placeholder: 'my-button', - key: 'settings.className', - }], -}); - -export const responsiveSettings = (...props) => ({ - id: 'responsive', - type: 'tab', - title: t => t('core.responsive.title', 'Responsive'), - ...props, - fields: [...(props?.fields || []), { - key: 'responsive.xl', - type: 'select', - label: t => t('core.responsive.xl', 'Extra-large screens'), - default: 'show', - options: [{ - title: t => t('core.responsive.show', 'Visible'), - value: 'show', - }, { - title: t => t('core.responsive.hide', 'Hidden'), - value: 'hide', - }], - condition: (_, { component } = {}) => - component.settings?.defaults?.responsive !== false, - }, { - key: 'responsive.lg', - type: 'select', - label: t => t('core.responsive.lg', 'Large screens (desktop)'), - default: 'show', - options: [{ - title: t => t('core.responsive.show', 'Visible'), - value: 'show', - }, { - title: t => t('core.responsive.hide', 'Hidden'), - value: 'hide', - }], - condition: (_, { component } = {}) => - component.settings?.defaults?.responsive !== false, - }, { - key: 'responsive.md', - type: 'select', - label: t => t('core.responsive.md', 'Medium screens (tablet)'), - default: 'show', - options: [{ - title: t => t('core.responsive.show', 'Visible'), - value: 'show', - }, { - title: t => t('core.responsive.hide', 'Hidden'), - value: 'hide', - }], - condition: (_, { component } = {}) => - component.settings?.defaults?.responsive !== false, - }, { - key: 'responsive.sm', - type: 'select', - label: t => t('core.responsive.sm', 'Small screens (phones)'), - default: 'show', - options: [{ - title: t => t('core.responsive.show', 'Visible'), - value: 'show', - }, { - title: t => t('core.responsive.hide', 'Hidden'), - value: 'hide', - }], - condition: (_, { component } = {}) => - component.settings?.defaults?.responsive !== false, - }, { - key: 'responsive.xs', - type: 'select', - label: t => t('core.responsive.xs', 'Extra-small screens (old phones)'), - default: 'show', - options: [{ - title: t => t('core.responsive.show', 'Visible'), - value: 'show', - }, { - title: t => t('core.responsive.hide', 'Hidden'), - value: 'hide', - }], - condition: (_, { component } = {}) => - component.settings?.defaults?.responsive !== false, - }], -}); - -export const baseFields = () => [ - textField(), - textareaField(), - selectField(), - colorField(), - imageField(), - dateField(), - toggleField(), -]; - -export const baseComponents = () => [ - rowComponent(), - colComponent(), - emptySpaceComponent(), - titleComponent(), - textComponent(), - imageComponent(), - buttonComponent(), - foldableComponent(), -]; - -export const baseSettings = () => [ - stylingSettings(), - responsiveSettings(), -]; - -export const coreComponentsGroup = (...props) => ({ - id: 'core', - type: 'group', - name: t => t('core.components.core.title', 'Core components'), - components: baseComponents(), - ...props, -}); - -export const baseAddon = () => ({ - components: [coreComponentsGroup()], - fields: baseFields(), - settings: baseSettings(), -}); diff --git a/packages/core/lib/addons.ts b/packages/core/lib/addons.ts new file mode 100644 index 000000000..d65a670d0 --- /dev/null +++ b/packages/core/lib/addons.ts @@ -0,0 +1,1195 @@ +import type { + AddonObject, + ComponentObject, + ComponentSettingsFieldObject, + ComponentSettingsFormObject, + ComponentsGroupObject, + ElementObject, + FieldObject, + GetTextCallback, +} from './types'; +import Builder from './Builder'; + +export const textField = (props?: FieldObject): FieldObject => ({ + type: 'text', + deserialize: (val: string) => '' + val, + render: () => null, + ...props, +}); + +export const textareaField = (props?: FieldObject): FieldObject => ({ + type: 'textarea', + deserialize: (val: string) => '' + val, + render: () => null, + ...props, +}); + +export const selectField = (props?: FieldObject): FieldObject => ({ + type: 'select', + render: () => null, + ...props, +}); + +export const colorField = (props?: FieldObject): FieldObject => ({ + type: 'color', + render: () => null, + ...props, +}); + +export const imageField = (props?: FieldObject): FieldObject => ({ + type: 'image', + render: () => null, + ...props, +}); + +export const dateField = (props?: FieldObject): FieldObject => ({ + type: 'date', + render: () => null, + ...props, +}); + +export const toggleField = (props?: FieldObject): FieldObject => ({ + type: 'toggle', + render: () => null, + ...props, +}); + +export const rowComponent = (props?: ComponentObject): ComponentObject => ({ + id: 'row', + name: (t: GetTextCallback) => t('core.components.row.name', 'Row'), + type: 'component', + render: () => null, + icon: 'row', + draggable: false, + droppable: false, + hasCustomInnerContent: true, + editable: true, + options: [], + settings: { + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.title', 'Row options'), + floatingSettings: { + placement: 'right-start', + shift: { enabled: true }, + autoPlacement: { alignment: 'start' }, + }, + fields: [{ + type: 'select', + key: 'settings.flexDirection', + default: 'row', + label: (t: GetTextCallback) => + t('core.components.row.settings.flexDirection.title', 'Direction'), + options: [{ + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.flexDirection.row', + 'Row (left to right)'), + value: 'row', + }, { + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.flexDirection.rowReverse', + 'Reversed row (right to left)'), + value: 'row-reverse', + }, { + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.flexDirection.column', + 'Column (top to bottom)'), + value: 'column', + }, { + title: (t: GetTextCallback) => t( + 'core.components.row.settings.flexDirection.columnReverse', + 'Reversed column (bottom to top)', + ), + value: 'column-reverse', + }], + }, { + type: 'select', + key: 'settings.justifyContent', + default: 'flex-start', + label: ( + t: GetTextCallback + ) => t('core.components.row.settings.justifyContent.title', + 'Horizontal alignment'), + options: [{ + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.justifyContent.flexStart', + 'Left'), + value: 'flex-start', + }, { + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.justifyContent.center', + 'Center'), + value: 'center', + }, { + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.justifyContent.flexEnd', + 'Right'), + value: 'flex-end', + }, { + title: ( + t: GetTextCallback + ) => t( + 'core.components.row.settings.justifyContent.spaceBetween', + 'Space between columns' + ), + value: 'space-between', + }, { + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.justifyContent.spaceAround', + 'Space around columns'), + value: 'space-around', + }], + }, { + type: 'select', + key: 'settings.alignItems', + default: 'flex-start', + label: ( + t: GetTextCallback + ) => t('core.components.row.settings.alignItems.title', + 'Vertical alignment'), + options: [{ + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.alignItems.flexStart', + 'Top'), + value: 'flex-start', + }, { + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.alignItems.center', + 'Center'), + value: 'center', + }, { + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.alignItems.flexEnd', + 'Bottom'), + value: 'flex-end', + }, { + title: ( + t: GetTextCallback + ) => t('core.components.row.settings.alignItems.stretch', + 'Stretch'), + value: 'stretch', + }], + }, { + type: 'text', + key: 'settings.gap', + label: (t: GetTextCallback) => + t('core.components.row.settings.gutters.title', 'Column gap'), + placeholder: (t: GetTextCallback) => + t('core.components.row.settings.gutters.placeholder', '10px'), + }], + }, + getContainers: element => [element.cols], + sanitize: ( + element: ElementObject, { builder }: { builder?: Builder } = {} + ) => { + const colComponent = builder.getComponent('col'); + + if (!colComponent) { + throw new Error('The `col` component is required to use rows.'); + } + + return { + ...element, + cols: !element.cols || !element.cols.length + ? [builder.createElement('col', { component: colComponent })] + : element.cols, + }; + }, + construct: ({ builder } = {}) => { + const colComponent = builder.getComponent('col'); + + if (!colComponent) { + throw new Error('The `col` component is required to use rows.'); + } + + return { + type: 'row', + settings: { + alignItems: 'flex-start', + }, + cols: [builder.createElement('col', { component: colComponent })], + }; + }, + ...props, +}); + +const COL_SIZES = Array.from({ length: 12 }).map((_, i) => ({ + title: (t: GetTextCallback) => + (i + 1) + ' ' + t( + 'core.components.col.settings.size.value', + 'column(s)' + ), + value: i + 1, +})).reverse(); + +const COL_RESPONSIVE_SETTINGS: Array< + { title: string | any, value: string | number} +> = [{ + title: (t: GetTextCallback) => t('core.responsive.fluid', 'Flexible'), + value: 'fluid', +}, { + title: ( + t: GetTextCallback + ) => t('core.responsive.auto', 'Adapted to content'), + value: 'auto', +}, ...COL_SIZES, { + title: (t: GetTextCallback) => t('core.responsive.hide', 'Hidden'), + value: 'hide', +}]; + +export const colComponent = (props?: ComponentObject): ComponentObject => ({ + id: 'col', + type: 'component', + draggable: false, + droppable: false, + construct: ({ builder } = {}) => ({ + type: 'col', + content: [], + id: builder.generateId(), + style: {}, + }), + editable: true, + usable: false, + settings: { + title: ( + t: GetTextCallback + ) => t('core.components.col.settings.title', 'Col options'), + floatingSettings: { + placement: 'left-start', + shift: { enabled: true }, + autoPlacement: { enabled: false }, + }, + fields: [{ + type: 'select', + key: 'size', + default: 'fluid', + label: ( + t: GetTextCallback + ) => t('core.components.col.settings.size.title', 'Column size'), + options: [ + { + title: (t: GetTextCallback) => t('core.responsive.fluid', 'Flexible'), + value: 'fluid', + }, + { + title: ( + t: GetTextCallback + ) => t('core.responsive.auto', 'Adapted to content'), + value: 'auto', + }, + ...COL_SIZES, + ], + }, { + tab: 'responsive', + key: 'responsive.xl', + type: 'select', + label: ( + t: GetTextCallback + ) => t('core.responsive.xl', 'Extra-large screens'), + default: 'fluid', + options: COL_RESPONSIVE_SETTINGS, + }, { + tab: 'responsive', + key: 'responsive.lg', + type: 'select', + label: ( + t: GetTextCallback + ) => t('core.responsive.lg', 'Large screens (desktop)'), + default: 'fluid', + options: COL_RESPONSIVE_SETTINGS, + }, { + tab: 'responsive', + key: 'responsive.md', + type: 'select', + label: ( + t: GetTextCallback + ) => t('core.responsive.md', 'Medium screens (tablet)'), + default: 'fluid', + options: COL_RESPONSIVE_SETTINGS, + }, { + tab: 'responsive', + key: 'responsive.sm', + type: 'select', + label: ( + t: GetTextCallback + ) => t('core.responsive.sm', 'Small screens (phones)'), + default: 'fluid', + options: COL_RESPONSIVE_SETTINGS, + }, { + tab: 'responsive', + key: 'responsive.xs', + type: 'select', + label: ( + t: GetTextCallback + ) => t('core.responsive.xs', 'Extra-small screens (old phones)'), + default: 'fluid', + options: COL_RESPONSIVE_SETTINGS, + }], + defaults: { + responsive: false, + }, + }, + ...props, +}); + +export const emptySpaceComponent = ( + props?: ComponentObject +): ComponentObject => ({ + id: 'empty-space', + name: ( + t: GetTextCallback + ) => t('core.components.emptySpace.name', 'Blank space'), + type: 'component', + render: () => null, + icon: 'blank_space', + options: [], + settings: { + title: (t: GetTextCallback) => + t('core.components.emptySpace.settings.title', 'Blank space options'), + fields: [{ + type: 'text', + key: 'settings.height', + default: '32px', + displayable: true, + label: ( + t: GetTextCallback + ) => t('core.components.emptySpace.settings.height', 'Height'), + }], + }, + editable: true, + construct: () => ({ + type: 'empty-space', + settings: { + height: '32px', + }, + }), + ...props, +}); + +export const titleComponent = (props?: ComponentObject): ComponentObject => ({ + id: 'title', + name: (t: GetTextCallback) => t('core.components.title.name', 'Title'), + type: 'component', + icon: 'title', + editable: true, + render: () => null, + options: [], + settings: { + title: ( + t: GetTextCallback + ) => t('core.components.title.settings.title', 'Title options'), + fields: [{ + type: 'select', + key: 'headingLevel', + default: 'h1', + displayable: true, + label: ( + t: GetTextCallback + ) => t('core.components.title.settings.type.title', 'Type'), + options: Array.from({ length: 6 }).map((_, i) => ({ + title: ( + t: GetTextCallback + ) => t('core.components.type.value', 'Title') + + ` ${i + 1} (h${i + 1})`, + value: `h${i + 1}`, + })), + }, { + type: 'textarea', + key: 'content', + default: '', + label: ( + t: GetTextCallback + ) => t('core.components.title.settings.content.title', 'Content'), + }], + }, + construct: ({ builder } = {}) => ({ + type: 'title', + content: builder + .getText('core.components.title.default', 'This is a title'), + headingLevel: 'h1', + }), + ...props, +}); + +export const textComponent = (props?: ComponentObject): ComponentObject => ({ + id: 'text', + name: (t: GetTextCallback) => t('core.components.text.name', 'Text'), + type: 'component', + render: () => null, + icon: 'multiline', + options: [], + settings: { + title: ( + t: GetTextCallback + ) => t('core.components.text.settings.title', 'Text options'), + fields: [{ + type: 'textarea', + key: 'content', + default: '', + label: ( + t: GetTextCallback + ) => t('core.components.text.settings.content.title', 'Content'), + }], + }, + editable: true, + construct: ({ builder } = {}) => ({ + type: 'text', + content: builder.getText( + 'core.components.text.default', + 'This is some fancy text content' + ), + }), + ...props, +}); + +export const imageComponent = (props?: ComponentObject): ComponentObject => ({ + id: 'image', + name: (t: GetTextCallback) => t('core.components.image.name', 'Image'), + type: 'component', + render: () => null, + icon: 'image', + editable: true, + options: [], + settings: { + title: ( + t: GetTextCallback + ) => t('core.components.image.settings.title', 'Image options'), + fields: [{ + type: 'image', + key: ['url', 'name'], + default: '', + label: ( + t: GetTextCallback + ) => t('core.components.image.settings.image.title', 'Image'), + }, { + type: 'select', + key: 'settings.size', + label: (t: GetTextCallback) => t( + 'core.components.image.settings.image.size.title', + 'Image size' + ), + default: 'auto', + displayable: true, + options: [{ + title: (t: GetTextCallback) => t( + 'core.components.image.settings.image.size.auto', + 'Adapted to content' + ), + value: 'auto', + }, { + title: (t: GetTextCallback) => t( + 'core.components.image.settings.image.size.full', + 'Real size' + ), + value: 'full', + }, { + title: (t: GetTextCallback) => t( + 'core.components.image.settings.image.size.custom', + 'Custom' + ), + value: 'custom', + }], + }, { + type: 'text', + key: 'settings.width', + displayable: true, + condition: (element: ElementObject) => + element?.settings?.size === 'custom', + label: (t: GetTextCallback) => t( + 'core.components.image.settings.image.size.width', + 'Image width' + ), + placeholder: (t: GetTextCallback) => t( + 'core.components.image.settings.image.size.width', + 'Image width' + ), + }, { + type: 'text', + key: 'settings.height', + displayable: true, + condition: (element: ElementObject) => + element?.settings?.size === 'custom', + label: (t: GetTextCallback) => t( + 'core.components.image.settings.image.size.height', + 'Image height' + ), + placeholder: (t: GetTextCallback) => t( + 'core.components.image.settings.image.size.height', + 'Image height' + ), + }, { + type: 'select', + key: 'settings.textAlign', + displayable: true, + label: (t: GetTextCallback) => t( + 'core.components.image.settings.image.align.title', + 'Image alignment' + ), + default: 'left', + options: [{ + title: (t: GetTextCallback) => t( + 'core.components.image.settings.image.align.left', + 'Left' + ), + value: 'left', + }, { + title: (t: GetTextCallback) => t( + 'core.components.image.settings.image.align.center', + 'Center' + ), + value: 'center', + }, { + title: (t: GetTextCallback) => t( + 'core.components.image.settings.image.align.right', + 'Right' + ), + value: 'right', + }], + }], + }, + construct: () => ({ + type: 'image', + url: '', + name: '', + }), + ...props, +}); + +export const buttonComponent = (props?: ComponentObject): ComponentObject => ({ + id: 'button', + name: (t: GetTextCallback) => t('core.components.button.name', 'Button'), + type: 'component', + render: () => null, + icon: 'button', + options: [], + settings: { + title: ( + t: GetTextCallback + ) => t('core.components.button.settings.title', 'Button options'), + fields: [{ + type: 'textarea', + key: 'content', + default: '', + label: ( + t: GetTextCallback + ) => t('core.components.button.settings.content.title', 'Content'), + }, { + type: 'select', + key: 'action', + default: 'link', + displayable: true, + label: ( + t: GetTextCallback + ) => t('core.components.button.settings.action.title', 'Action'), + options: [{ + title: (t: GetTextCallback) => t( + 'core.components.button.settings.action.openLink', + 'Open a link' + ), + value: 'link', + }, { + title: (t: GetTextCallback) => t( + 'core.components.button.settings.action.fireEvent', + 'Trigger an event' + ), + value: 'event', + }], + }, { + type: 'text', + key: 'url', + default: '', + displayable: true, + label: ( + t: GetTextCallback + ) => t('core.components.button.settings.url.title', 'URL link'), + condition: (element: ElementObject) => element.action === 'link', + }, { + type: 'text', + key: 'event', + default: '', + displayable: true, + label: (t: GetTextCallback) => t( + 'core.components.button.settings.event.title', + 'Javascript event name' + ), + condition: (element: ElementObject) => element.action === 'event', + }, { + type: 'select', + key: 'settings.buttonType', + default: 'button', + label: (t: GetTextCallback) => t( + 'core.components.button.settings.type.title', + 'HTML element type' + ), + options: [{ + title: ( + t: GetTextCallback + ) => t('core.components.button.settings.type.button', 'Button'), + value: 'button', + }, { + title: ( + t: GetTextCallback + ) => t('core.components.button.settings.type.links', 'Link'), + value: 'link', + }], + }], + }, + editable: true, + construct: ({ builder } = {}) => ({ + type: 'button', + content: builder.getText('core.components.button.default', 'Click me !'), + action: 'link', + url: '', + settings: { + buttonType: 'button', + }, + }), + ...props, +}); + +export const foldableComponent = ( + props?: ComponentObject +): ComponentObject => ({ + id: 'foldable', + name: (t: GetTextCallback) => t('core.components.foldable.name', 'Foldable'), + type: 'component', + render: () => null, + icon: 'foldable', + editable: true, + hasCustomInnerContent: true, + draggable: false, + droppable: false, + getContainers: element => + [element.content, element.seeMore, element.seeLess], + settings: { + title: (t: GetTextCallback) => t( + 'core.components.foldable.settings.title', 'Foldable options'), + floatingSettings: { + placement: 'right-start', + autoPlacement: { + alignment: 'start', + }, + }, + fields: [{ + type: 'select', + key: 'settings.seeMorePosition', + default: 'after', + displayable: true, + label: (t: GetTextCallback) => t( + 'core.components.foldable.settings.seeMorePosition.title', + 'See more placement' + ), + options: [{ + title: (t: GetTextCallback) => t( + 'core.components.foldable.settings.seeMorePosition.before', + 'Before' + ), + value: 'before', + }, { + title: (t: GetTextCallback) => t( + 'core.components.foldable.settings.seeMorePosition.after', + 'After' + ), + value: 'after', + }], + }], + }, + construct: () => ({ + type: 'foldable', + settings: { + seeMorePosition: 'after', + }, + content: [], + seeMore: [], + seeLess: [], + }), + ...props, +}); + +export const stylingSettings = ( + props?: ComponentSettingsFieldObject +): ComponentSettingsFieldObject => ({ + id: 'styling', + type: 'tab', + title: (t: GetTextCallback) => t('core.styling.title', 'Styling'), + ...props, + fields: [...(props?.fields || []), { + type: 'field', + label: ( + t: GetTextCallback + ) => t('core.styling.paddings.title', 'Inside spacing (padding)'), + fields: [{ + type: 'text', + key: 'styles.paddingTop', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.paddings.top', 'Top'), + }, { + type: 'text', + key: 'styles.paddingRight', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.paddings.right', 'Right'), + }, { + type: 'text', + key: 'styles.paddingBottom', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.paddings.bottom', 'Bottom'), + }, { + type: 'text', + key: 'styles.paddingLeft', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.paddings.left', 'Left'), + }], + }, { + type: 'field', + label: ( + t: GetTextCallback + ) => t('core.styling.margins.title', 'Outside spacing (margin)'), + fields: [{ + type: 'text', + key: 'styles.marginTop', + placeholder: (t: GetTextCallback) => t('core.styling.margins.top', 'Top'), + }, { + type: 'text', + key: 'styles.marginRight', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.margins.right', 'Right'), + }, { + type: 'text', + key: 'styles.marginBottom', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.margins.bottom', 'Bottom'), + }, { + type: 'text', + key: 'styles.marginLeft', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.margins.left', 'Left'), + }], + }, { + type: 'field', + label: ( + t: GetTextCallback + ) => t('core.styling.borders.title', 'Border size'), + fields: [{ + type: 'text', + key: 'styles.borderTop', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borders.top', 'Top'), + }, { + type: 'text', + key: 'styles.borderRight', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borders.right', 'Right'), + }, { + type: 'text', + key: 'styles.borderBottom', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borders.bottom', 'Bottom'), + }, { + type: 'text', + key: 'styles.borderLeft', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borders.left', 'Left'), + }], + }, { + type: 'field', + condition: (element: ElementObject) => + element?.styles?.borderTop || + element?.styles?.borderRight || + element?.styles?.borderBottom || + element?.styles?.borderLeft, + fields: [{ + type: 'color', + key: 'styles.borderTopColor', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borders.topColor', '#000'), + }, { + type: 'color', + key: 'styles.borderRightColor', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borders.rightColor', '#000'), + }, { + type: 'color', + key: 'styles.borderBottomColor', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borders.bottomColor', '#000'), + }, { + type: 'color', + key: 'styles.borderLeftColor', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borders.leftColor', '#000'), + }], + }, { + type: 'field', + label: ( + t: GetTextCallback + ) => t('core.styling.borderRadius.title', 'Border radius'), + fields: [{ + type: 'text', + key: 'styles.borderTopLeftRadius', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borderRadius.topLeft', 'Top left'), + }, { + type: 'text', + key: 'styles.borderTopRightRadius', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borderRadius.topRight', 'Top right'), + }, { + type: 'text', + key: 'styles.borderBottomRightRadius', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borderRadius.bottomRight', 'Bottom right'), + }, { + type: 'text', + key: 'styles.borderBottomLeftRadius', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.borderRadius.bottomLeft', 'Bottom left'), + }], + }, { + type: 'field', + label: ( + t: GetTextCallback + ) => t('core.styling.background.image.title', 'Background image'), + fields: [{ + key: 'styles.backgroundImage', + type: 'image', + props: { + iconOnly: true, + }, + }, { + label: ( + t: GetTextCallback + ) => t('core.styling.background.size.title', 'Size'), + key: 'styles.backgroundSize', + type: 'select', + default: 'default', + placeholder: (t: GetTextCallback) => + t('core.styling.background.size.title', 'Background size'), + options: [{ + title: ( + t: GetTextCallback + ) => t('core.styling.background.size.default', 'Default'), + value: 'default', + }, { + title: ( + t: GetTextCallback + ) => t('core.styling.background.size.cover', 'Fill'), + value: 'cover', + }, { + title: ( + t: GetTextCallback + ) => t('core.styling.background.size.contain', 'Fit'), + value: 'contain', + }], + }, { + label: ( + t: GetTextCallback + ) => t('core.styling.background.position.title', 'Position'), + key: 'styles.backgroundPosition', + type: 'select', + default: 'center', + placeholder: (t: GetTextCallback) => + t('core.styling.background.position.title', 'Background position'), + options: [{ + title: ( + t: GetTextCallback + ) => t('core.styling.background.position.center', 'Centered'), + value: 'center', + }, { + title: ( + t: GetTextCallback + ) => t('core.styling.background.position.top', 'Top'), + value: 'top', + }, { + title: ( + t: GetTextCallback + ) => t('core.styling.background.position.right', 'Right'), + value: 'right', + }, { + title: ( + t: GetTextCallback + ) => t('core.styling.background.position.bottom', 'Bottom'), + value: 'bottom', + }, { + title: ( + t: GetTextCallback + ) => t('core.styling.background.position.left', 'Left'), + value: 'left', + }, { + title: (t: GetTextCallback) => + t('core.styling.background.position.centerTop', 'Center top'), + value: 'center top', + }, { + title: (t: GetTextCallback) => + t('core.styling.background.position.centerBottom', 'Center bottom'), + value: 'center bottom', + }, { + title: (t: GetTextCallback) => + t('core.styling.background.position.leftCenter', 'Center left'), + value: 'left center', + }, { + title: (t: GetTextCallback) => + t('core.styling.background.position.leftTop', 'Top left'), + value: 'left top', + }, { + title: (t: GetTextCallback) => + t('core.styling.background.position.leftBottom', 'Bottom left'), + value: 'left bottom', + }, { + title: (t: GetTextCallback) => + t('core.styling.background.position.rightCenter', 'Center right'), + value: 'right center', + }, { + title: (t: GetTextCallback) => + t('core.styling.background.position.rightTop', 'Top right'), + value: 'right top', + }, { + title: (t: GetTextCallback) => + t('core.styling.background.position.rightBottom', 'Bottom right'), + value: 'right bottom', + }], + }, { + label: ( + t: GetTextCallback + ) => t('core.styling.background.repeat.title', 'Repeat'), + key: 'styles.backgroundRepeat', + type: 'select', + default: 'no-repeat', + placeholder: (t: GetTextCallback) => + t('core.styling.background.repeat.title', 'Background repeat'), + options: [{ + title: (t: GetTextCallback) => + t('core.styling.background.repeat.noRepeat', 'No repeat'), + value: 'no-repeat', + }, { + title: (t: GetTextCallback) => + t('core.styling.background.repeat.repeatX', 'Repeat horizontally'), + value: 'repeat-x', + }, { + title: (t: GetTextCallback) => + t('core.styling.background.repeat.repeatY', 'Repeat vertically'), + value: 'repeat-y', + }, { + title: (t: GetTextCallback) => t('core.styling.background.repeat.both', + 'Repeat horizontally & vertically'), + value: 'repeat', + }, + ], + }], + }, { + label: (t: GetTextCallback) => + t('core.styling.background.color.title', 'Background color'), + placeholder: '#FFF', + type: 'color', + key: 'styles.backgroundColor', + }, { + type: 'field', + label: ( + t: GetTextCallback + ) => t('core.styling.shadow.title', 'Box shadow'), + fields: [{ + type: 'text', + key: 'styles.boxShadowX', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.shadow.x', 'X'), + }, { + type: 'text', + key: 'styles.boxShadowY', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.shadow.y', 'Y'), + }, { + type: 'text', + key: 'styles.boxShadowBlur', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.shadow.blur', 'Blur'), + }, { + type: 'text', + key: 'styles.boxShadowSpread', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.shadow.spread', 'Spread'), + }, { + type: 'color', + key: 'styles.boxShadowColor', + placeholder: ( + t: GetTextCallback + ) => t('core.styling.shadow.color', '#000'), + }], + }, { + label: (t: GetTextCallback) => + t('core.styling.className.title', 'Additional CSS class'), + type: 'text', + placeholder: 'my-button', + key: 'settings.className', + }], +}); + +export const responsiveSettings = ( + props?: ComponentSettingsFieldObject +): ComponentSettingsFieldObject => ({ + id: 'responsive', + type: 'tab', + title: (t: GetTextCallback) => t('core.responsive.title', 'Responsive'), + ...props, + fields: [...(props?.fields || []), { + key: 'responsive.xl', + type: 'select', + label: ( + t: GetTextCallback + ) => t('core.responsive.xl', 'Extra-large screens'), + default: 'show', + options: [{ + title: (t: GetTextCallback) => t('core.responsive.show', 'Visible'), + value: 'show', + }, { + title: (t: GetTextCallback) => t('core.responsive.hide', 'Hidden'), + value: 'hide', + }], + condition: (_, { component }: { component?: ComponentObject} = {}) => + (component.settings as ComponentSettingsFormObject) + ?.defaults?.responsive !== false, + }, { + key: 'responsive.lg', + type: 'select', + label: ( + t: GetTextCallback + ) => t('core.responsive.lg', 'Large screens (desktop)'), + default: 'show', + options: [{ + title: (t: GetTextCallback) => t('core.responsive.show', 'Visible'), + value: 'show', + }, { + title: (t: GetTextCallback) => t('core.responsive.hide', 'Hidden'), + value: 'hide', + }], + condition: (_, { component }: { component?: ComponentObject} = {}) => + (component.settings as ComponentSettingsFormObject) + ?.defaults?.responsive !== false, + }, { + key: 'responsive.md', + type: 'select', + label: ( + t: GetTextCallback + ) => t('core.responsive.md', 'Medium screens (tablet)'), + default: 'show', + options: [{ + title: (t: GetTextCallback) => t('core.responsive.show', 'Visible'), + value: 'show', + }, { + title: (t: GetTextCallback) => t('core.responsive.hide', 'Hidden'), + value: 'hide', + }], + condition: (_, { component }: { component?: ComponentObject} = {}) => + (component.settings as ComponentSettingsFormObject) + ?.defaults?.responsive !== false, + }, { + key: 'responsive.sm', + type: 'select', + label: ( + t: GetTextCallback + ) => t('core.responsive.sm', 'Small screens (phones)'), + default: 'show', + options: [{ + title: (t: GetTextCallback) => t('core.responsive.show', 'Visible'), + value: 'show', + }, { + title: (t: GetTextCallback) => t('core.responsive.hide', 'Hidden'), + value: 'hide', + }], + condition: (_, { component }: { component?: ComponentObject} = {}) => + (component.settings as ComponentSettingsFormObject) + ?.defaults?.responsive !== false, + }, { + key: 'responsive.xs', + type: 'select', + label: ( + t: GetTextCallback + ) => t('core.responsive.xs', 'Extra-small screens (old phones)'), + default: 'show', + options: [{ + title: (t: GetTextCallback) => t('core.responsive.show', 'Visible'), + value: 'show', + }, { + title: (t: GetTextCallback) => t('core.responsive.hide', 'Hidden'), + value: 'hide', + }], + condition: (_, { component }: { component?: ComponentObject} = {}) => + (component.settings as ComponentSettingsFormObject) + ?.defaults?.responsive !== false, + }], +}); + +export const baseFields = (): Array => [ + textField(), + textareaField(), + selectField(), + colorField(), + imageField(), + dateField(), + toggleField(), +]; + +export const baseComponents = (): Array => [ + rowComponent(), + colComponent(), + emptySpaceComponent(), + titleComponent(), + textComponent(), + imageComponent(), + buttonComponent(), + foldableComponent(), +]; + +export const baseSettings = (): Array => [ + stylingSettings(), + responsiveSettings(), +]; + +export const coreComponentsGroup = ( + props?: ComponentsGroupObject +): ComponentsGroupObject => ({ + id: 'core', + type: 'group', + name: ( + t: GetTextCallback + ) => t('core.components.core.title', 'Core components'), + components: baseComponents(), + ...props, +}); + +export const baseAddon = (): AddonObject => ({ + components: [coreComponentsGroup()], + fields: baseFields(), + settings: baseSettings(), +}); diff --git a/packages/core/lib/classes.ts b/packages/core/lib/classes.ts new file mode 100644 index 000000000..94107e59a --- /dev/null +++ b/packages/core/lib/classes.ts @@ -0,0 +1,432 @@ +import type Builder from './Builder'; +import type { + BuilderObject, + ElementObject, + ComponentObject, + ComponentOptionObject, + ComponentsGroupObject, + FieldObject, + ComponentOverrideObject, + FieldOverrideObject, + SettingOverrideObject, + ComponentSettingsFieldObject, + ComponentSettingsFormObject, + ComponentSettingsTabObject, + ComponentSettingsFieldKeyTuple, + GetTextCallback, + ComponentSettingsFieldOptionObject, +} from './types'; + +export class BuilderOptions { + debug: boolean; + generateId: () => string | number; + historyLimit: number; + overrideStrategy: 'last' | 'merge'; + + constructor (props?: BuilderObject) { + this.debug = props?.debug ?? false; + this.generateId = props?.generateId; + this.historyLimit = props?.historyLimit ?? 20; + this.overrideStrategy = props?.overrideStrategy || 'last'; + } +} + +export class Component { + static FIND_PREDICATE = (id: string) => (c: Component) => c.id === id; + + type: 'component'; + id: string; + group: string; + render: () => any; + sanitize: ( + element: ElementObject, { builder }: { builder: Builder } + ) => ElementObject; + construct: (opts?: { builder?: Builder }) => ElementObject; + duplicate: (elmt?: ElementObject) => ElementObject; + icon: any; + getContainers: (element: ElementObject) => ElementObject[][]; + name: string; + hasCustomInnerContent: boolean; + draggable: boolean; + droppable: boolean; + usable: boolean; + editable: boolean; + disallow: any; + options: any; + settings: any; + deserialize: (opts: { builder: Builder }) => ElementObject; + serialize: Function; //TODO + + constructor (props: any) { + if (!props.id) { + throw new Error('Component must have an id'); + } + + this.type = 'component'; + this.id = props.id; + this.group = props.group; + this.render = props.render; + this.sanitize = props.sanitize; + this.construct = props.construct; + this.duplicate = props.duplicate; + this.icon = props.icon; + this.getContainers = props.getContainers; + this.name = props.name || ''; + this.hasCustomInnerContent = props.hasCustomInnerContent ?? false; + this.draggable = props.draggable ?? true; + this.droppable = props.droppable ?? true; + this.usable = props.usable ?? true; + this.editable = props.editable ?? true; + this.disallow = props.disallow || []; + this.serialize = props.serialize; + this.options = (props.options || []).map(( + o: ComponentOptionObject + ) => new ComponentOption(o)); + this.settings = new ComponentSettingsForm(props.settings || {}); + this.deserialize = props.deserialize; + this.serialize = props.serialize; + } + + toObject (): ComponentObject { + return { + id: this.id, + type: this.type, + group: this.group, + name: this.name, + icon: this.icon, + settings: this.settings, + hasCustomInnerContent: this.hasCustomInnerContent, + draggable: this.draggable, + droppable: this.droppable, + usable: this.usable, + editable: this.editable, + options: this.options, + disallow: this.disallow, + render: this.render, + sanitize: this.sanitize, + construct: this.construct, + duplicate: this.duplicate, + getContainers: this.getContainers, + }; + } +} + +export class ComponentsGroup { + static FIND_PREDICATE = (id: string) => (g: ComponentsGroup) => g.id === id; + + type: string; + id: string; + name: string | GetTextCallback; + usable: boolean; + components: Component[]; + + constructor (props: ComponentsGroupObject) { + if (!props.id) { + throw new Error('Component Group must have an id'); + } + + this.type = 'group'; + this.id = props.id; + this.name = props.name; + this.usable = props.usable ?? true; + this.components = (props.components || []).map(c => + c instanceof Component ? c : new Component(c) + ); + } + + toObject (): ComponentsGroupObject { + return { + type: this.type, + id: this.id, + name: this.name, + usable: this.usable, + components: this.components.map(c => c.toObject()), + }; + } +} + +export class Field { + static FIND_PREDICATE = (type: string) => (f: Field) => f.type === type; + + type: string; + render: (props: any) => any; + props: object; + + constructor (props: FieldObject) { + if (!props.type) { + throw new Error('Field must have a type'); + } + + this.type = props.type; + this.render = props.render; + this.props = props.props || {}; + } + + toObject (): FieldObject { + return { + type: this.type, + render: this.render, + props: this.props, + }; + } +} + +export class Override { + type: 'component' | 'field' | 'setting'; +} + +export class ComponentOverride extends Override { + id: string; + targets: string[]; + fields: ComponentSettingsFieldObject[]; + render: Function; + construct: ( + opts: { builder: Builder, baseElement?: ElementObject } + ) => ElementObject; + sanitize: ( + element: ElementObject, { builder }: { builder: Builder} + ) => ElementObject; + duplicate: Function; //TODO fix it + deserialize: Function; //TODO fix it + priority: number; + serialize: Function; //TODO fix it + getContainers: (element: ElementObject) => ElementObject[][]; + + constructor (props: ComponentOverrideObject) { + super(); + + this.type = 'component'; + this.id = props.id; + this.targets = props.targets || []; + this.fields = props.fields || []; + this.render = props.render; + this.sanitize = props.sanitize; + this.construct = props.construct; + this.duplicate = props.duplicate; + this.deserialize = props.deserialize; + this.priority = props.priority || 0; + } +} + +export class FieldOverride extends Override { + id: string; + targets: Array; + render: Function; + props: object; + construct: Function; + priority: number; + onChange: Function; + + constructor (props: FieldOverrideObject) { + super(); + + this.type = 'field'; + this.id = props.id; + this.targets = props.targets || []; + this.render = props.render; + this.priority = props.priority || 0; + this.props = props.props || {}; + this.construct = props.construct; + this.onChange = props.onChange; + } +} + +export class SettingOverride extends Override { + key: string | string[] | ComponentSettingsFieldKeyTuple[]; + targets: string[]; + id: string; + placeholder: string | GetTextCallback; + default: any; + options: ComponentSettingsFieldOptionObject[]; + label: string | GetTextCallback; + description: string | GetTextCallback; + displayable: boolean; + valueType: string; + condition: Function; + priority: number; + fields: ComponentSettingsFieldObject[]; + props: object; + + constructor (props: SettingOverrideObject) { + super(); + + this.key = props.key; + this.type = 'setting'; + this.targets = props.targets || []; + this.id = props.id; + this.placeholder = props.placeholder; + this.default = props.default; + this.options = props.options; + this.label = props.label; + this.description = props.description; + this.displayable = props.displayable; + this.valueType = props.valueType; + this.condition = props.condition; + this.priority = props.priority || 0; + this.fields = (props.fields || []).map(( + f: ComponentSettingsFieldObject + ) => new ComponentSettingsField(f)); + this.props = props.props; + } +} + +export class ComponentOption { + icon: any; + render: Function; + + constructor (props: ComponentOptionObject) { + this.icon = props.icon; + this.render = props.render; + } +} + +export class ComponentSettingsForm { + title: string | GetTextCallback; + floatingSettings: any; + defaults: object; + fields: Array; + + constructor (props: ComponentSettingsFormObject) { + this.title = props.title; + this.floatingSettings = props.floatingSettings; + this.defaults = props.defaults || {}; + this.fields = (props.fields || []).map( + (t: ComponentSettingsFieldObject | ComponentSettingsTabObject) => + t.type === 'tab' + ? t instanceof ComponentSettingsTab + ? t : new ComponentSettingsTab(t as ComponentSettingsTabObject) + : t instanceof ComponentSettingsField + ? t : new ComponentSettingsField(t as ComponentSettingsFieldObject) + ); + } +} + +export class ComponentSettingsTab { + static FIND_PREDICATE = (id: string) => + (t: ComponentSettingsTab) => t.id === id; + + type: string; + id: string; + title: string | GetTextCallback; + priority: number; + condition: (element: Element | ElementObject, opts?: { + component: Component | ComponentObject; + builder: Builder; + }) => boolean; + fields: Array; + displayable: boolean | ((...rest: any) => false); + + constructor (props: ComponentSettingsTabObject) { + if (!props.id) { + throw new Error('ComponentSettingsTab must have an id'); + } + + this.type = 'tab'; + this.id = props.id; + this.priority = props.priority || 0; + this.title = props.title; + this.condition = props.condition; + this.fields = (props.fields || []).map(f => + f instanceof ComponentSettingsField ? f : new ComponentSettingsField(f)); + } + + toObject (): ComponentSettingsTabObject { + return { + id: this.id, + type: this.type, + title: this.title, + priority: this.priority, + condition: this.condition, + fields: this.fields.map(f => f.toObject()), + }; + } +} + +export class ComponentSettingsField { + static FIND_PREDICATE = (id: string) => + (f: ComponentSettingsField) => f.id === id || f.key === id; + + type: string; + tab: string; + key: string | string[]; + id: string; + placeholder: string | GetTextCallback; + default: any; + options: Array; + label: string | GetTextCallback; + info: string | GetTextCallback; + description: string | GetTextCallback; + displayable: boolean | ((element: Element | ElementObject, opts?: { + component: Component | ComponentObject; + builder: Builder; + }) => boolean); + valueType: string; + condition: (element: Element | ElementObject, opts?: { + component: Component | ComponentObject; + builder: Builder; + }) => boolean; + priority: number; + fields: Array; + props: object; + + constructor (props: ComponentSettingsFieldObject) { + if (!props.fields && !props.type) { + throw new Error('ComponentSettingsField must have a type (or be ' + + 'a group of fields)'); + } + + this.type = props.type; + this.tab = props.tab; + this.key = props.key; + this.id = props.id; + this.placeholder = props.placeholder; + this.default = props.default; + this.options = props.options; + this.label = props.label; + this.info = props.info; + this.description = props.description; + this.displayable = props.displayable; + this.valueType = props.valueType; + this.condition = props.condition; + this.priority = props.priority || 0; + this.fields = (props.fields || []).map(f => new ComponentSettingsField(f)); + this.props = props.props; + } + + toObject (): ComponentSettingsFieldObject { + return { + type: this.type, + tab: this.tab, + key: this.key, + id: this.id, + placeholder: this.placeholder, + default: this.default, + options: this.options, + label: this.label, + info: this.info, + description: this.description, + displayable: this.displayable, + valueType: this.valueType, + condition: this.condition, + priority: this.priority, + fields: this.fields.map(f => f.toObject()), + props: this.props, + }; + } +} + +export class TextsSheet { + static FIND_PREDICATE = (id: string) => (s: { id: string }) => s.id === id; + + id: string; + texts: object; + + constructor (props: {id: string, texts: object}) { + if (!props.id) { + throw new Error('TextsSheet must have an id'); + } + + this.id = props.id; + this.texts = props.texts || {}; + } +} diff --git a/packages/core/lib/index.d.ts b/packages/core/lib/index.d.ts deleted file mode 100644 index caef49878..000000000 --- a/packages/core/lib/index.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { Builder } from './Builder'; -export { Components } from './Components'; -export { Emitter } from './Emitter'; -export { Fields } from './Fields'; -export { Logger } from './Logger'; -export { Overrides } from './Overrides'; -export { - Store, - StoreSanitizeOptions, - StoreFindOptions, - StoreFindDeepOptions, -} from './Store'; -export { Texts } from './Texts'; -export { Settings } from './Settings'; - -export * from './addons'; -export * from './types'; diff --git a/packages/core/lib/index.js b/packages/core/lib/index.js deleted file mode 100644 index 253a5e0a2..000000000 --- a/packages/core/lib/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export { default as Builder } from './Builder'; - -export * from './types'; - -export * from './addons'; diff --git a/packages/core/lib/index.ts b/packages/core/lib/index.ts new file mode 100644 index 000000000..07953f3cf --- /dev/null +++ b/packages/core/lib/index.ts @@ -0,0 +1,6 @@ +export { default as Builder } from './Builder'; + +export * as coreAddons from './addons'; +export * from './classes'; + +export type * from './types'; diff --git a/packages/core/lib/types.d.ts b/packages/core/lib/types.d.ts deleted file mode 100644 index a7232599a..000000000 --- a/packages/core/lib/types.d.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { Builder } from './Builder'; -import { Logger } from './Logger'; - -export declare interface GetTextCallback { - (key: string | GetTextCallback, def?: any): any; -} - -export declare type ElementId = string | number; - -export declare interface ElementObject { - id?: ElementId; - type: string; - [_: string]: any -} - -export declare class BuilderOptions { - debug?: boolean; - historyLimit?: number; - overrideStrategy?: 'last' | 'merge'; - generateId?(): string | number; -} - -export declare interface ComponentOptionObject { - icon: any; - render?(): any; -} - -export declare class ComponentOption { - constructor(props: object); - icon: any; - render?(): any; -} - -export declare interface ComponentSettingsFieldKeyTuple { - from: string; - to: string; - default: any; -} - -export declare interface FieldObject { - type: string; - props?: object; - render?(props: any): any; -} - -export declare class Field { - static FIND_PREDICATE(type: string): (field: Field) => boolean; - - constructor(props: object); - type: string; - props: object; - render?(): any; -} - -export declare interface ComponentOverrideObject { - id?: string; - type?: string; - targets?: string[]; - fields?: ComponentSettingsFieldObject[]; - construct?(opts?: { builder?: Builder }): ElementObject; - render?(props?: any): any; - sanitize?(): any; - duplicate?(elmt?: ElementObject): ElementObject; -} - -export declare class ComponentOverride { - constructor(props: object); - id: string; - type: string; - targets: string[]; - fields: ComponentSettingsField[]; - construct(opts?: { builder?: Builder }): ElementObject; - render?(props?: any): any; - sanitize?(): any; - duplicate?(elmt?: ElementObject): ElementObject; -} - -export declare interface FieldOverrideObject { - id: string; - type: string; - targets: string[]; - props: Record; - render?(): any; -} - -export declare class FieldOverride { - constructor(props: object); - id: string; - type: string; - targets: string[]; - props: Record; - render?(): any; -} - -export declare interface SettingOverrideObject { - type: string; - targets: string[]; - key: string | string[] | ComponentSettingsFieldKeyTuple[]; - id?: string; - label?: string | GetTextCallback; - info?: string | GetTextCallback; - description?: string | GetTextCallback; - placeholder?: string | GetTextCallback; - default?: any; - displayable?: boolean; - valueType?: string; - priority?: number; - fields?: (ComponentSettingsField | ComponentSettingsFieldObject)[]; - props?: Record; - condition?(element: Element | ElementObject, opts?: { - component: Component | ComponentObject; - builder: Builder; - }): boolean; -} - -export declare class SettingOverride { - constructor(props: Record); - type: string; - targets: string[]; - key: string | string[] | ComponentSettingsFieldKeyTuple[]; - id: string; - label: string | GetTextCallback; - info: string | GetTextCallback; - description: string | GetTextCallback; - placeholder: string; - default: any; - displayable: boolean; - valueType: string; - priority: number; - fields: (ComponentSettingsField | ComponentSettingsFieldObject)[]; - props: Record; - condition?(element: Element | ElementObject, opts?: { - component: Component | ComponentObject; - builder: Builder; - }): boolean; -} - -export declare interface ComponentSettingsFieldOptionObject { - value?: any; - title?: string | GetTextCallback; - imageTransformation?: { - width: number; - height: number; - }; -} - -export declare interface ComponentSettingsFieldObject { - priority?: number; - type: string; - key: string | string[] | ComponentSettingsFieldKeyTuple[]; - tab?: string; - id?: string; - label?: string | GetTextCallback; - info?: string | GetTextCallback; - description?: string | GetTextCallback; - placeholder?: string | GetTextCallback; - default?: any; - options?: ComponentSettingsFieldOptionObject[] | Record[]; - displayable?: boolean; - valueType?: string; - fields?: ComponentSettingsFieldObject[]; - props?: Record; - condition?(element: Element | ElementObject, opts?: { - component: Component | ComponentObject; - builder: Builder; - }): boolean; -} - -export declare class ComponentSettingsField { - static FIND_PREDICATE(type: string): - (field: ComponentSettingsField) => boolean; - - constructor(props: Record); - priority: number; - type: string; - tab: string; - id: string; - key: string | string[] | ComponentSettingsFieldKeyTuple[]; - placeholder: string | GetTextCallback; - default: any; - label: string | GetTextCallback; - info: string | GetTextCallback; - description: string | GetTextCallback; - displayable: boolean; - valueType: string; - fields: ComponentSettingsField[]; - props: Record; - condition?(element: Element | ElementObject, opts?: { - component: Component | ComponentObject; - builder: Builder; - }): boolean; -} - -export declare interface ComponentSettingsTabObject { - id: string; - priority?: number; - title?: string | GetTextCallback; - fields?: (ComponentSettingsField | ComponentSettingsFieldObject)[]; - condition?(element: Element | ElementObject, opts?: { - component: Component | ComponentObject; - builder: Builder; - }): boolean; -} - -export declare class ComponentSettingsTab { - static FIND_PREDICATE(id: string): (tab: ComponentSettingsTab) => boolean; - - constructor(props: Record); - type: string; - id: string; - priority: number; - title: string | GetTextCallback; - fields: ComponentSettingsField[]; - condition?(element: Element | ElementObject, opts?: { - component: Component | ComponentObject; - builder: Builder; - }): boolean; -} - -export declare class ComponentSettingsFormObject { - title?: string | GetTextCallback; - floatingSettings?: Record; - defaults?: any; - fields?: ( - ComponentSettingsTab | - ComponentSettingsTabObject | - ComponentSettingsField | - ComponentSettingsFieldObject - )[]; -} - -export declare class ComponentSettingsForm { - constructor(props: Record); - title?: string | GetTextCallback; - floatingSettings?: Record; - defaults?: any; - fields?: (ComponentSettingsTab | ComponentSettingsField)[]; -} - -export declare interface ComponentObject { - type?: string; - id?: string; - group?: string; - icon?: any; - name?: string | GetTextCallback; - hasCustomInnerContent?: boolean; - draggable?: boolean; - droppable?: boolean; - usable?: boolean; - editable?: boolean; - options?: (ComponentOption | ComponentOptionObject)[]; - settings?: ComponentSettingsForm | ComponentSettingsFormObject; - disallow?: string[]; - render?(props?: any): any; - sanitize?(): any; - construct?(opts?: { builder?: Builder }): ElementObject; - duplicate?(elmt?: ElementObject): ElementObject; - getContainers?(element: ElementObject): ElementObject[]; -} - -export declare class Component { - static FIND_PREDICATE(id: string): (component: Component) => boolean; - - constructor(props: Record); - type: string; - id: string; - group: string; - icon: any; - name: string | GetTextCallback; - hasCustomInnerContent: boolean; - draggable: boolean; - droppable: boolean; - usable: boolean; - editable: boolean; - options: ComponentOption[]; - settings: ComponentSettingsForm; - disallow: Array; - render(): any; - sanitize(): any; - construct(): ElementObject; - duplicate(): ElementObject; - getContainers(element: ElementObject): ElementObject[]; -} - -export declare class ComponentsGroup { - static FIND_PREDICATE(id: string): (group: ComponentsGroup) => boolean; - - constructor(props: Record); - type: string; - id: string; - name: string; - usable: boolean; - components: Component[]; -} -export declare interface ComponentsGroupObject { - type: string; - id: string; - name: string; - usable: boolean; - components: (Component | ComponentObject)[]; -} - -export declare interface TextsSheetObject { - id: string; - texts: Record; -} - -export declare class TextsSheet { - static FIND_PREDICATE(id: string): (sheet: TextsSheet) => boolean; - - constructor(props: Record); - id: string; - texts: Record; -} - -export declare interface ComponentTabOject { - id: string; - title: string | GetTextCallback; - components: (Component | ComponentObject)[]; -} -export declare interface AddonObject { - components?: (Component | ComponentObject | ComponentTabOject)[]; - fields?: (Field | FieldObject)[]; - texts?: (TextsSheet | TextsSheetObject)[]; - overrides?: ( - ComponentOverride | - ComponentOverrideObject | - FieldOverride | - FieldOverrideObject - )[]; - settings?: ( - ComponentSettingsTab | - ComponentSettingsTabObject | - ComponentSettingsField | - ComponentSettingsFieldObject - )[]; -} diff --git a/packages/core/lib/types.js b/packages/core/lib/types.js deleted file mode 100644 index 6fd78572b..000000000 --- a/packages/core/lib/types.js +++ /dev/null @@ -1,192 +0,0 @@ -export class BuilderOptions { - constructor (props) { - this.debug = props.debug || false; - this.generateId = props.generateId; - this.historyLimit = props.historyLimit || 20; - this.overrideStrategy = props.overrideStrategy || 'last'; - } -} - -export class Component { - static FIND_PREDICATE = id => c => c.id === id; - - constructor (props) { - if (!props.id) { - throw new Error('Component must have an id'); - } - - this.type = 'component'; - this.id = props.id; - this.group = props.group; - this.render = props.render; - this.sanitize = props.sanitize; - this.construct = props.construct; - this.duplicate = props.duplicate; - this.icon = props.icon; - this.getContainers = props.getContainers; - this.name = props.name || ''; - this.hasCustomInnerContent = props.hasCustomInnerContent ?? false; - this.draggable = props.draggable ?? true; - this.droppable = props.droppable ?? true; - this.usable = props.usable ?? true; - this.editable = props.editable ?? true; - this.disallow = props.disallow || []; - this.options = (props.options || []).map(o => new ComponentOption(o)); - this.settings = new ComponentSettingsForm(props.settings || {}); - } -} - -export class ComponentsGroup { - static FIND_PREDICATE = id => g => g.id === id; - - constructor (props) { - if (!props.id) { - throw new Error('Component Group must have an id'); - } - - this.type = 'group'; - this.id = props.id; - this.name = props.name; - this.usable = props.usable ?? true; - this.components = (props.components || []).map(c => - c instanceof Component ? c : new Component(c) - ); - } -} - -export class Field { - static FIND_PREDICATE = type => f => f.type === type; - - constructor (props) { - if (!props.type) { - throw new Error('Field must have a type'); - } - - this.type = props.type; - this.render = props.render; - this.props = props.props || {}; - } -} - -export class ComponentOverride { - constructor (props) { - this.type = 'component'; - this.id = props.id; - this.targets = props.targets || []; - this.fields = props.fields || []; - this.render = props.render; - this.sanitize = props.sanitize; - this.construct = props.construct; - this.duplicate = props.duplicate; - } -} - -export class FieldOverride { - constructor (props) { - this.type = 'field'; - this.id = props.id; - this.targets = props.targets || []; - this.render = props.render; - this.props = props.props || {}; - } -} - -export class SettingOverride { - constructor (props) { - this.type = 'setting'; - this.key = props.key; - this.targets = props.targets || []; - this.id = props.id; - this.placeholder = props.placeholder; - this.default = props.default; - this.options = props.options; - this.label = props.label; - this.description = props.description; - this.displayable = props.displayable; - this.valueType = props.valueType; - this.condition = props.condition; - this.priority = props.priority || 0; - this.fields = (props.fields || []).map(f => new ComponentSettingsField(f)); - this.props = props.props; - } -} - -export class ComponentOption { - constructor (props) { - this.icon = props.icon; - this.render = props.render; - } -} - -export class ComponentSettingsForm { - constructor (props) { - this.title = props.title; - this.floatingSettings = props.floatingSettings; - this.defaults = props.defaults || {}; - this.fields = (props.fields || []).map(t => - t.type === 'tab' - ? t instanceof ComponentSettingsTab ? t : new ComponentSettingsTab(t) - : t instanceof ComponentSettingsField - ? t : new ComponentSettingsField(t) - ); - } -} - -export class ComponentSettingsTab { - static FIND_PREDICATE = id => t => t.id === id; - - constructor (props) { - if (!props.id) { - throw new Error('ComponentSettingsTab must have an id'); - } - - this.type = 'tab'; - this.id = props.id; - this.priority = props.priority || 0; - this.title = props.title; - this.condition = props.condition; - this.fields = (props.fields || []).map(f => - f instanceof ComponentSettingsField ? f : new ComponentSettingsField(f)); - } -} - -export class ComponentSettingsField { - static FIND_PREDICATE = id => f => f.id === id || f.key === id; - - constructor (props) { - if (!props.fields && !props.type) { - throw new Error('ComponentSettingsField must have a type (or be ' + - 'a group of fields)'); - } - - this.type = props.type; - this.tab = props.tab; - this.key = props.key; - this.id = props.id; - this.placeholder = props.placeholder; - this.default = props.default; - this.options = props.options; - this.label = props.label; - this.info = props.info; - this.description = props.description; - this.displayable = props.displayable; - this.valueType = props.valueType; - this.condition = props.condition; - this.priority = props.priority || 0; - this.fields = (props.fields || []).map(f => new ComponentSettingsField(f)); - this.props = props.props; - } -} - -export class TextsSheet { - static FIND_PREDICATE = id => s => s.id === id; - - constructor (props) { - if (!props.id) { - throw new Error('TextsSheet must have an id'); - } - - this.id = props.id; - this.texts = props.texts || {}; - } -} diff --git a/packages/core/lib/types.ts b/packages/core/lib/types.ts new file mode 100644 index 000000000..791e0a1c7 --- /dev/null +++ b/packages/core/lib/types.ts @@ -0,0 +1,263 @@ +import type { Component, ComponentOverride } from './classes'; +import type Builder from './Builder'; + +export declare interface GetTextCallback { + (key: string | GetTextCallback, def?: any): any; +} + +export declare type EmitterCallback = (...args: any[]) => void; + +export declare type ElementId = string | number; + +export declare interface ElementObject { + id?: ElementId; + type?: string; + content?: string | Function | ElementObject[]; + [_: string]: any; +} + +export interface ComponentOptionObject { + icon?: any; + render?(props?: any): any; +} + +export declare interface ComponentSettingsFieldKeyTuple { + from: string; + to: string; + default: any; +} + +export declare interface FieldObject { + type: string; + props?: object; + render?(props: any, opts?: { + setting: ComponentSettingsFieldObject; + }): any; + deserialize?(val: string): any; + onChange?( + field: FieldContent, + element?: ElementObject + ): void; +} + +export declare interface ComponentOverrideObject { + id?: string; + type?: 'component'; + targets?: string[]; + fields?: ComponentSettingsFieldObject[]; + construct?(opts?: { builder?: Builder }): ElementObject; + deserialize?(opts?: { builder?: Builder }): ElementObject; + render?(props?: any): any; + sanitize?(elmt?: ElementObject): any; + duplicate?(elmt?: ElementObject): ElementObject; + priority?: number; +} + +export declare interface FieldOverrideObject { + type: 'field'; + construct?: Function; //TODO fix it + targets?: string[]; + props?: Record; + id?: string; + render?(): any; + priority?: number; + onChange?( + name: string, + field: FieldContent, + element?: ElementObject + ): void; +} + +export declare interface SettingOverrideObject { + type: 'setting'; + targets?: string[]; + key?: string | string[] | ComponentSettingsFieldKeyTuple[]; + id?: string; + label?: string | GetTextCallback; + title?: string | GetTextCallback; + info?: string | GetTextCallback; + description?: string | GetTextCallback; + placeholder?: string | GetTextCallback; + default?: any; + displayable?: boolean; + valueType?: string; + priority?: number; + options?: Array; + fields?: (ComponentSettingsFieldObject)[]; + props?: Record; + parseTitle?(value: any): string; + parseValue?(value: any): any; + condition?(element: Element | ElementObject, opts?: { + component: Component | ComponentObject; + builder: Builder; + }): boolean; +} + +export declare interface ComponentSettingsFieldOptionObject { + value?: any; + title?: string | GetTextCallback; + imageTransformation?: { + width: number; + height: number; + }; +} + +export declare interface ComponentSettingsFieldObject { + name?: string; + priority?: number; + type: string; + key?: string | string[]; + tab?: string; + id?: string; + label?: string | GetTextCallback; + info?: string | GetTextCallback; + description?: string | GetTextCallback; + title?: string | GetTextCallback; + placeholder?: string | GetTextCallback; + default?: any; + options?: ComponentSettingsFieldOptionObject[] | Record[]; + displayable?: boolean | ((element: Element | ElementObject, opts?: { + component: Component | ComponentObject; + builder: Builder; + }) => boolean); + valueType?: string; + fields?: ComponentSettingsFieldObject[]; + props?: Record; + checkedLabel?: string | GetTextCallback; + uncheckedLabel?: string | GetTextCallback; + parseTitle?(value: any): string; + parseValue?(value: any): any; + condition?(element: Element | ElementObject, opts?: { + component: ComponentObject; + builder: Builder; + }): boolean; + disabled?: boolean; + required?: boolean; +} + +export declare interface ComponentSettingsTabObject { + id: string; + type?: string; + priority?: number; + title?: string | GetTextCallback; + fields?: (ComponentSettingsFieldObject)[]; + condition?(element: Element | ElementObject, opts?: { + component: Component | ComponentObject; + builder: Builder; + }): boolean; + renderForm?(props: any): any; +} + +export declare class ComponentSettingsFormObject { + title?: string | GetTextCallback; + floatingSettings?: Record | Function; + defaults?: any; + fields?: ( + ComponentSettingsTabObject | + ComponentSettingsFieldObject + )[]; +} + +export declare interface ComponentObject { + type?: string; + id?: string; + group?: string; + icon?: any; + name?: string | GetTextCallback; + hasCustomInnerContent?: boolean; + draggable?: boolean; + droppable?: boolean; + usable?: boolean; + editable?: boolean; + options?: ComponentOptionObject[]; + settings?: ComponentSettingsFormObject | ComponentSettingsTabObject; + disallow?: string[]; + render?(props?: any): any; + deserialize?: Function; + serialize?: Function; + sanitize?( + element: ElementObject, { builder }: { builder: Builder } + ): ElementObject + construct?(opts?: { builder?: Builder }): ElementObject; + duplicate?(elmt?: ElementObject): ElementObject; + getContainers?(element: ElementObject): ElementObject[][]; +} + +export declare interface ComponentsGroupObject { + type: string; + id: string; + name: string | GetTextCallback; + usable?: boolean; + components: (ComponentObject)[]; +} + +export declare interface TextsSheetObject { + id: string; + texts: Record; +} + +export declare interface ComponentTabOject { + id: string; + title: string | GetTextCallback; + components: (Component | ComponentObject)[]; +} + +export declare interface AddonObject { + components?: (ComponentObject | ComponentTabOject)[]; + fields?: (FieldObject)[]; + texts?: (TextsSheetObject)[]; + overrides?: ( + ComponentOverrideObject | + FieldOverrideObject | + SettingOverrideObject + )[]; + settings?: ( + ComponentSettingsTabObject | + ComponentSettingsFieldObject + )[]; +} + +export declare interface BuilderObject { + debug?: boolean; + generateId?: () => string | number + historyLimit?: number; + overrideStrategy?: 'last' | 'merge'; + content?: ElementObject[]; + addons?: AddonObject[]; + onChange?(content: ElementObject[]): void; +} + +export declare type ElementSettingsComplexKey = { + from: string, + to: string, + default?: string +} + +export declare type ElementSettingsKeyObject = + | string + | ElementSettingsComplexKey + | Array; + +export declare interface StoreSanitizeOptions { + component?: Component; + override?: ComponentOverride; + resetIds?: boolean; +} + +export declare interface StoreFindOptions { + parent?: Array; +} + +export declare type StoreFindDeepOptions = Partial; + +export declare type FieldContent = { + valid?: boolean; + checked?: boolean; + value?: T; +} + +export declare interface EventCallback { + (eventName: string, ...args: any[]): void; +} diff --git a/packages/core/package.json b/packages/core/package.json index dfebe5e22..b1444c34f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,12 +3,8 @@ "version": "3.5.5", "description": "🌳 Modern, lightweight & modulable page builder", "main": "dist/oak-core.cjs.js", - "jsnext:main": "dist/esm/index.js", "module": "dist/esm/index.js", - "esnext": "lib/index.js", - "unpkg": "dist/oak-core.min.js", - "cdn": "dist/oak-core.min.js", - "types": "dist/oak-core.d.ts", + "types": "dist/types/index.d.ts", "repository": { "type": "git", "url": "https://github.com/p3ol/oak.git", @@ -21,18 +17,17 @@ "license": "MIT", "sideEffects": false, "engines": { - "node": ">= 16", - "npm": ">= 8" + "node": ">= 18" }, "dependencies": { - "@babel/runtime-corejs3": "7.24.6", - "@junipero/core": "3.4.13", - "core-js": "3.37.1", + "@junipero/core": "3.5.0", "uuid": "9.0.1" }, "scripts": { "clean": "rm -rf ./dist || true", - "build": "yarn clean && rollup -c", + "build": "yarn clean && yarn build:code && yarn build:dts", + "build:code": "yarn run -T rollup --configPlugin @rollup/plugin-swc -c", + "build:dts": "yarn run -T tsc --project ./tsconfig.build.json", "test": "jest" }, "publishConfig": { @@ -41,11 +36,13 @@ "exports": { ".": { "import": "./dist/esm/index.js", - "require": "./dist/oak-core.cjs.js" + "require": "./dist/oak-core.cjs.js", + "types": "./dist/types/index.d.ts" }, "./addons": { "import": "./dist/esm/addons.js", - "require": "./dist/oak-core.cjs.js" + "require": "./dist/oak-core.cjs.js", + "types": "./dist/types/addons.d.ts" } } } diff --git a/packages/core/rollup.config.mjs b/packages/core/rollup.config.mjs deleted file mode 100644 index 555aba1ec..000000000 --- a/packages/core/rollup.config.mjs +++ /dev/null @@ -1,70 +0,0 @@ -import path from 'path'; - -import babel from '@rollup/plugin-babel'; -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import terser from '@rollup/plugin-terser'; -import dts from 'rollup-plugin-dts'; - -const input = './lib/index.js'; -const defaultOutput = './dist'; -const name = 'oak-core'; -const libName = 'OakCore'; -const formats = ['umd', 'cjs', 'esm']; - -const defaultExternals = []; -const defaultGlobals = {}; - -const defaultPlugins = [ - babel({ - exclude: /node_modules/, - babelHelpers: 'runtime', - }), - resolve({ - rootDir: path.resolve('../../'), - }), - commonjs(), - terser({ mangle: false }), -]; - -const getConfig = (format, { - output = defaultOutput, - globals = defaultGlobals, - external = defaultExternals, -} = {}) => ({ - input, - plugins: [ - ...defaultPlugins, - ], - external, - output: { - ...(format === 'esm' ? { - dir: `${output}/esm`, - chunkFileNames: '[name].js', - } : { - file: `${output}/${name}.${format}.js`, - }), - format, - name: libName, - sourcemap: true, - globals, - ...(format === 'esm' ? { - manualChunks: id => { - if (/packages\/core\/lib\/(\w+)\/index.js/.test(id)) { - return path.parse(id).dir.split('/').pop(); - } else { - return id.includes('node_modules') ? 'vendor' : path.parse(id).name; - } - }, - } : {}), - }, -}); - -export default [ - ...formats.map(f => getConfig(f)), - { - input: './lib/index.d.ts', - output: [{ file: `dist/${name}.d.ts`, format: 'es' }], - plugins: [dts()], - }, -]; diff --git a/packages/core/rollup.config.ts b/packages/core/rollup.config.ts new file mode 100644 index 000000000..cda789314 --- /dev/null +++ b/packages/core/rollup.config.ts @@ -0,0 +1,65 @@ +import path from 'node:path'; + +import type { Plugin } from 'rollup'; +import swc from '@rollup/plugin-swc'; +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import terser from '@rollup/plugin-terser'; + +const input = './lib/index.ts'; +const output = './dist'; +const name = 'oak-core'; +const formats = ['umd', 'cjs', 'esm']; + +const defaultPlugins: Plugin[] = [ + commonjs({ include: /node_modules/ }), + resolve({ + rootDir: path.resolve('../../'), + extensions: ['.js', '.ts', '.json', '.node'], + }), + terser(), +]; + +const defaultExternals: string[] = []; +const defaultGlobals = {}; + +export default [ + ...formats.map(f => ({ + input, + plugins: [ + swc({ + swc: { + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + }, + }, + }, + }), + ...defaultPlugins, + ], + external: defaultExternals, + output: { + ...(f === 'esm' ? { + dir: `${output}/esm`, + chunkFileNames: '[name].js', + } : { + file: `${output}/${name}.${f}.js`, + }), + format: f, + name, + sourcemap: true, + globals: defaultGlobals, + ...(f === 'esm' ? { + manualChunks: (id: string) => { + if (/packages\/core\/lib\/(\w+)\/index.ts/.test(id)) { + return path.parse(id).dir.split('/').pop(); + } else { + return id.includes('node_modules') ? 'vendor' : path.parse(id).name; + } + }, + } : {}), + }, + })), +]; diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json new file mode 100644 index 000000000..9faaf412c --- /dev/null +++ b/packages/core/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "noEmit": false, + "emitDeclarationOnly": true, + "outDir": "dist/types", + "baseUrl": "." + }, + "exclude": ["tests", "**/*.test.ts", "rollup.config.ts"] +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 000000000..9a11edc9a --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./lib/**/*"], +} diff --git a/packages/react/.browserslistrc b/packages/react/.browserslistrc index 5601a3b5b..cf8634949 100644 --- a/packages/react/.browserslistrc +++ b/packages/react/.browserslistrc @@ -1,5 +1,5 @@ >=0.5% -node >= 14 +node >= 18 not ie >= 0 not ie_mob >= 0 not dead diff --git a/packages/react/babel.config.js b/packages/react/babel.config.js deleted file mode 100644 index 4595c67be..000000000 --- a/packages/react/babel.config.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - presets: [ - ['@babel/env', { - corejs: 3, - useBuiltIns: 'usage', - }], - ['@babel/react', { - runtime: 'automatic', - }], - ], - plugins: [ - ['@babel/transform-runtime', { - corejs: 3, - }], - ], -}; diff --git a/packages/react/jest.config.js b/packages/react/jest.config.js index fca38c48d..453b00867 100644 --- a/packages/react/jest.config.js +++ b/packages/react/jest.config.js @@ -18,7 +18,18 @@ module.exports = { '^.+\\.styl$', ], transform: { - '^.+\\.js$': 'babel-jest', + '^.+\\.(t|j)sx?$': [ + '@swc/jest', + { + jsc: { + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], '^.+\\.styl$': 'jest-css-modules-transform', }, transformIgnorePatterns: [ diff --git a/packages/react/lib/Builder/HistoryButtons.js b/packages/react/lib/Builder/HistoryButtons.tsx similarity index 76% rename from packages/react/lib/Builder/HistoryButtons.js rename to packages/react/lib/Builder/HistoryButtons.tsx index d2d855d95..fd562bfe2 100644 --- a/packages/react/lib/Builder/HistoryButtons.js +++ b/packages/react/lib/Builder/HistoryButtons.tsx @@ -1,10 +1,13 @@ +import type { MutableRefObject } from 'react'; import { Tooltip, Button } from '@junipero/react'; import { useBuilder } from '../hooks'; import Text from '../Text'; import Icon from '../Icon'; -const HistoryButtons = ({ canUndo, canRedo }) => { +const HistoryButtons = ( + { canUndo, canRedo }: { canUndo: boolean, canRedo: boolean} +) => { const { builder, rootRef, floatingsRef, rootBoundary } = useBuilder(); return ( @@ -13,8 +16,8 @@ const HistoryButtons = ({ canUndo, canRedo }) => { disabled={!canUndo} container={floatingsRef.current} floatingOptions={{ - boundary: rootBoundary?.current || rootRef?.current, - rootBoundary: rootBoundary?.current || rootRef?.current, + boundary: (rootBoundary as MutableRefObject)?.current || + rootRef?.current, }} text={Undo} > @@ -30,8 +33,8 @@ const HistoryButtons = ({ canUndo, canRedo }) => { disabled={!canRedo} container={floatingsRef.current} floatingOptions={{ - boundary: rootBoundary?.current || rootRef?.current, - rootBoundary: rootBoundary?.current || rootRef?.current, + boundary: (rootBoundary as MutableRefObject)?.current || + rootRef?.current, }} text={Redo} > diff --git a/packages/react/lib/Builder/index.d.ts b/packages/react/lib/Builder/index.d.ts deleted file mode 100644 index 810ac8460..000000000 --- a/packages/react/lib/Builder/index.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - ReactNode, - ComponentPropsWithRef, - FormEvent, - MutableRefObject, -} from 'react'; -import { - AddonObject, - Builder as CoreBuilder, - BuilderOptions, - ElementObject, - ComponentSettingsField, - ComponentSettingsFieldObject, -} from '@oakjs/core'; - -export declare interface ImageUploadCallbackResult { - url: string; - name: string; - [key: string]: any; -} - -export declare interface BuilderContextValue { - builder: CoreBuilder; - content: Array; - rootBoundary: MutableRefObject | Element | DocumentFragment; - onImageUpload?(event: FormEvent): Promise; - rootRef: MutableRefObject; - floatingsRef: MutableRefObject; -} - -export declare type BuilderRef = { - builder: CoreBuilder; - content: Array; - isOak: boolean; - catalogueRef: MutableRefObject; - innerRef: MutableRefObject; -}; - -export declare interface BuilderProps extends ComponentPropsWithRef { - activeTextSheet?: string; - addons: Array; - bottomHistoryButtonsContainer?: string | Element | DocumentFragment; - bottomHistoryButtonsEnabled?: boolean; - defaultValue?: Array; - historyEnabled?: boolean; - options?: BuilderOptions; - rootBoundary?: string | Element | DocumentFragment; - topHistoryButtonsContainer?: string | Element | DocumentFragment; - topHistoryButtonsEnabled?: boolean; - value?: Array; - [key: string]: any; - onChange?(content: Array): void; - onImageUpload?(event: FormEvent, opts: { - element?: ElementObject; - setting?: ComponentSettingsFieldObject | ComponentSettingsField; - }): Promise; - ref?: MutableRefObject; -} - -declare function Builder(props: BuilderProps): ReactNode | JSX.Element; - -export default Builder; diff --git a/packages/react/lib/Builder/index.test.js b/packages/react/lib/Builder/index.test.tsx similarity index 84% rename from packages/react/lib/Builder/index.test.js rename to packages/react/lib/Builder/index.test.tsx index 9e793f0cb..bdb3ccf43 100644 --- a/packages/react/lib/Builder/index.test.js +++ b/packages/react/lib/Builder/index.test.tsx @@ -1,11 +1,12 @@ import { useState } from 'react'; import { render, screen, fireEvent, within } from '@testing-library/react'; +import { BuilderObject } from '@oakjs/core'; import { baseAddon } from '../addons'; import Builder from './index'; describe('', () => { - const getOptions = (...props) => { + const getOptions = (props?: BuilderObject) => { let i = 0; return { @@ -76,8 +77,8 @@ describe('', () => { expect(container).toMatchSnapshot('Empty content'); - const floatings = container.querySelector('.floatings'); - const catalogue = container.querySelector('.catalogue'); + const floatings = container.querySelector('.floatings'); + const catalogue = container.querySelector('.catalogue'); // Open catalogue fireEvent.click(within(catalogue).getByText('add')); @@ -90,8 +91,8 @@ describe('', () => { expect(container).toMatchSnapshot('Content with row'); // Open row's first col catalogue - const row = container.querySelector('.element.type-row'); - const col = row.querySelector('.column .col-inner'); + const row = container.querySelector('.element.type-row'); + const col = row.querySelector('.column .col-inner'); fireEvent.click(within(col).getByText('add')); // Add a title @@ -101,7 +102,8 @@ describe('', () => { expect(row).toMatchSnapshot('Row with title'); // Open main prepend catalogue - const prependCatalogue = container.querySelectorAll('.catalogue')[0]; + const prependCatalogue = container + .querySelectorAll('.catalogue')[0]; fireEvent.click(within(prependCatalogue).getByText('add')); // Add a text diff --git a/packages/react/lib/Builder/index.test.js.snap b/packages/react/lib/Builder/index.test.tsx.snap similarity index 100% rename from packages/react/lib/Builder/index.test.js.snap rename to packages/react/lib/Builder/index.test.tsx.snap diff --git a/packages/react/lib/Builder/index.js b/packages/react/lib/Builder/index.tsx similarity index 60% rename from packages/react/lib/Builder/index.js rename to packages/react/lib/Builder/index.tsx index 4f0488ac8..23006271a 100644 --- a/packages/react/lib/Builder/index.js +++ b/packages/react/lib/Builder/index.tsx @@ -1,14 +1,61 @@ -import { forwardRef, useCallback, useRef, useImperativeHandle } from 'react'; +import { + type MutableRefObject, + type FormEvent, + type ComponentPropsWithRef, + forwardRef, + useCallback, + useRef, + useImperativeHandle, +} from 'react'; import { createPortal } from 'react-dom'; import { classNames, ensureNode } from '@junipero/react'; +import { + type AddonObject, + type ElementObject, + type BuilderOptions, + type ComponentSettingsFieldObject, + type ComponentObject, + Builder as CoreBuilder, +} from '@oakjs/core'; -import { BuilderContext } from '../contexts'; +import type { ImageUploadCallbackResult } from '../types'; +import { type BuilderContextValue, BuilderContext } from '../contexts'; import { useRootBuilder } from '../hooks'; import Element from '../Element'; import Catalogue from '../Catalogue'; import HistoryButtons from './HistoryButtons'; -const Builder = forwardRef(({ +export declare type BuilderRef = { + builder: CoreBuilder; + content: Array; + isOak: boolean; + catalogueRef: MutableRefObject; + innerRef: MutableRefObject; + close?: () => void +}; + +export declare interface BuilderProps extends ComponentPropsWithRef { + activeTextSheet?: string; + addons: Array; + bottomHistoryButtonsContainer?: string | HTMLElement | DocumentFragment; + bottomHistoryButtonsEnabled?: boolean; + defaultValue?: Array; + historyEnabled?: boolean; + options?: BuilderOptions; + rootBoundary?: MutableRefObject | string | Element | DocumentFragment; + topHistoryButtonsContainer?: string | HTMLElement | DocumentFragment; + topHistoryButtonsEnabled?: boolean; + value?: Array; + [key: string]: any; + onChange?(content: Array): void; + onImageUpload?(event: FormEvent, opts: { + element?: ElementObject; + setting?: ComponentSettingsFieldObject; + }): Promise; + ref?: MutableRefObject; +} + +const Builder = forwardRef(({ className, defaultValue, value, @@ -22,10 +69,10 @@ const Builder = forwardRef(({ topHistoryButtonsEnabled = true, bottomHistoryButtonsEnabled = true, ...opts -}, ref) => { +}: BuilderProps, ref) => { const innerRef = useRef(); - const catalogueRef = useRef(); - const floatingsRef = useRef(); + const catalogueRef = useRef(); + const floatingsRef = useRef(); const { builder, content, addons, canUndo, canRedo } = useRootBuilder({ content: value, defaultContent: defaultValue, @@ -42,28 +89,28 @@ const Builder = forwardRef(({ innerRef, })); - const getContext = useCallback(() => ({ + const getContext = useCallback((): BuilderContextValue => ({ builder, content, addons, - rootBoundary: rootBoundary?.current + rootBoundary: (rootBoundary as MutableRefObject)?.current ? rootBoundary : { current: rootBoundary }, onImageUpload, rootRef: innerRef, floatingsRef, }), [builder, content, addons, rootBoundary, onImageUpload]); - const onAppend = component => { + const onAppend = (component: ComponentObject) => { catalogueRef.current?.close(); builder.addElement({}, { component }); }; - const onPrepend = component => { + const onPrepend = (component: ComponentObject) => { catalogueRef.current?.close(); builder.addElement({}, { component, position: 'before' }); }; - const onPaste = (position, element) => { + const onPaste = (position: 'before' | 'after', element: ElementObject) => { catalogueRef.current?.close(); builder.addElements([].concat(element || []), { resetIds: true, position }); }; @@ -98,7 +145,7 @@ const Builder = forwardRef(({ ) } - { content?.map((element, i) => ( + { content?.map((element: ElementObject, i: number) => ( + { historyEnabled && bottomHistoryButtonsEnabled && ( bottomHistoryButtonsContainer ? createPortal(historyButtons, diff --git a/packages/react/lib/Catalogue/index.d.ts b/packages/react/lib/Catalogue/index.d.ts deleted file mode 100644 index fe5b7856e..000000000 --- a/packages/react/lib/Catalogue/index.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ComponentObject, ElementObject } from '@oakjs/core'; -import { ReactNode, MutableRefObject, ComponentPropsWithRef } from 'react'; - -export declare type CatalogueRef = { - open: () => void; - close: () => void; - toggle: () => void; - opened: boolean; - isOak: boolean; - innerRef: MutableRefObject; -}; - -export declare interface CatalogueProps extends ComponentPropsWithRef { - className?: string; - placement?: string; - onToggle?(props: { opened: boolean }): void; - onAppend?(props: { component: ComponentObject }): void; - onPaste?(clipboardData: ElementObject): void; - ref?: MutableRefObject; -} - -declare function Catalogue(props: CatalogueProps): ReactNode | JSX.Element; - -export default Catalogue; diff --git a/packages/react/lib/Catalogue/index.test.js b/packages/react/lib/Catalogue/index.test.tsx similarity index 100% rename from packages/react/lib/Catalogue/index.test.js rename to packages/react/lib/Catalogue/index.test.tsx diff --git a/packages/react/lib/Catalogue/index.test.js.snap b/packages/react/lib/Catalogue/index.test.tsx.snap similarity index 100% rename from packages/react/lib/Catalogue/index.test.js.snap rename to packages/react/lib/Catalogue/index.test.tsx.snap diff --git a/packages/react/lib/Catalogue/index.js b/packages/react/lib/Catalogue/index.tsx similarity index 75% rename from packages/react/lib/Catalogue/index.js rename to packages/react/lib/Catalogue/index.tsx index ec3979c8e..1aacd50a6 100644 --- a/packages/react/lib/Catalogue/index.js +++ b/packages/react/lib/Catalogue/index.tsx @@ -1,12 +1,17 @@ import { + type ComponentPropsWithRef, + type MouseEvent, + type MutableRefObject, forwardRef, useImperativeHandle, + useMemo, useReducer, useRef, - useMemo, } from 'react'; +import type { ComponentObject, ElementObject } from '@oakjs/core'; import { createPortal } from 'react-dom'; import { + type ForwardedProps, Tabs, Tab, classNames, @@ -15,26 +20,51 @@ import { } from '@junipero/react'; import { slideInDownMenu } from '@junipero/transitions'; import { + type UseFloatingOptions, + type Boundary, useFloating, useInteractions, useClick, useDismiss, offset, shift, + flip, } from '@floating-ui/react'; import { useBuilder } from '../hooks'; import Icon from '../Icon'; import Text from '../Text'; -const Catalogue = forwardRef(({ +export declare type CatalogueRef = { + open: () => void; + close: () => void; + toggle: () => void; + opened: boolean; + isOak: boolean; + innerRef: MutableRefObject; +}; + +export declare interface CatalogueProps extends ComponentPropsWithRef { + className?: string; + placement?: string; + floatingOptions?: UseFloatingOptions & { + boundary?: Boundary; + }; + onToggle?(props: { opened: boolean }): void; + onAppend?(component: ComponentObject): void; + onPaste?(clipboardData: ElementObject): void; + ref?: MutableRefObject; +} + +const Catalogue = forwardRef(({ component, className, placement = 'bottom', + floatingOptions, onToggle, onAppend, onPaste, -}, ref) => { +}: CatalogueProps, ref) => { const innerRef = useRef(); const { builder, rootRef, rootBoundary, floatingsRef } = useBuilder(); const [state, dispatch] = useReducer(mockState, { @@ -46,8 +76,13 @@ const Catalogue = forwardRef(({ onOpenChange: o => o ? open() : close(), middleware: [ offset(16), + flip({ + boundary: floatingOptions?.boundary || + floatingOptions?.elements?.reference, + }), shift({ - rootBoundary: rootBoundary?.current || rootRef?.current, + boundary: floatingOptions?.boundary || + floatingOptions?.elements?.reference, }), ], }); @@ -95,9 +130,7 @@ const Catalogue = forwardRef(({ onToggle?.({ opened: false }); }; - const toggle = e => { - e.preventDefault(); - + const toggle = () => { if (state.opened) { close(); } else { @@ -105,7 +138,10 @@ const Catalogue = forwardRef(({ } }; - const onAppend_ = (component, e) => { + const onAppend_ = ( + component: ComponentObject, + e: MouseEvent, + ) => { e?.preventDefault(); onAppend?.(component); }; @@ -116,7 +152,7 @@ const Catalogue = forwardRef(({ .filter(g => g.usable !== false) .map(g => ({ ...g, - components: g.components.filter(c => + components: g.components.filter((c: ComponentObject) => c.usable !== false && (!component || !component.disallow || !component.disallow.includes(c.id)) @@ -161,9 +197,12 @@ const Catalogue = forwardRef(({
- ), { opened: state.opened }), ensureNode(floatingsRef?.current)) } + ), + { opened: state.opened }), ensureNode(floatingsRef?.current)) } ); }); diff --git a/packages/react/lib/Container/index.d.ts b/packages/react/lib/Container/index.d.ts deleted file mode 100644 index 22e240755..000000000 --- a/packages/react/lib/Container/index.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ReactNode, ComponentPropsWithoutRef } from 'react'; -import { - ElementObject, - ComponentObject, - Component, -} from '@oakjs/core'; - -export declare interface ContainerProps extends ComponentPropsWithoutRef { - element?: ElementObject; - content?: Array; - component?: ComponentObject | Component; - depth?: number; -} - -declare function Container(props: ContainerProps): ReactNode | JSX.Element; - -export default Container; diff --git a/packages/react/lib/Container/index.test.js b/packages/react/lib/Container/index.test.tsx similarity index 74% rename from packages/react/lib/Container/index.test.js rename to packages/react/lib/Container/index.test.tsx index 9f7095a3b..6c10e1681 100644 --- a/packages/react/lib/Container/index.test.js +++ b/packages/react/lib/Container/index.test.tsx @@ -1,3 +1,4 @@ +import type { BuilderObject, ElementObject } from '@oakjs/core'; import { fireEvent, render, within } from '@testing-library/react'; import { BuilderLite } from '../../tests/utils'; @@ -5,7 +6,7 @@ import { baseAddon } from '../addons'; import Container from './index'; describe('', () => { - const getOptions = props => { + const getOptions = (props?: BuilderObject) => { let i = 0; return { @@ -15,19 +16,24 @@ describe('', () => { }; it('should render', () => { - const elmt = { id: '1', type: 'foldable', content: [] }; + const elmt: ElementObject = { + id: '1', + type: 'foldable', + content: [], + }; + const { container, unmount } = render( ); expect(container).toMatchSnapshot(); // Open catalogue - const catalogue = container.querySelector('.catalogue'); + const catalogue = container.querySelector('.catalogue'); fireEvent.click(within(catalogue).getByText('add')); // Add a text @@ -52,11 +58,12 @@ describe('', () => { ); // Open catalogue - const catalogue = container.querySelector('.catalogue:first-child'); + const catalogue = container + .querySelector('.catalogue:first-child'); fireEvent.click(within(catalogue).getByText('add')); // Add a text - const menu = container.querySelector('.catalogue-menu'); + const menu = container.querySelector('.catalogue-menu'); fireEvent.click(within(menu).getByText('Text')); expect(elmt).toMatchSnapshot('With element added before'); diff --git a/packages/react/lib/Container/index.test.js.snap b/packages/react/lib/Container/index.test.tsx.snap similarity index 100% rename from packages/react/lib/Container/index.test.js.snap rename to packages/react/lib/Container/index.test.tsx.snap diff --git a/packages/react/lib/Container/index.js b/packages/react/lib/Container/index.tsx similarity index 75% rename from packages/react/lib/Container/index.js rename to packages/react/lib/Container/index.tsx index e71715560..d180704a7 100644 --- a/packages/react/lib/Container/index.js +++ b/packages/react/lib/Container/index.tsx @@ -1,9 +1,18 @@ -import { useRef } from 'react'; +import { type ComponentPropsWithoutRef, useRef } from 'react'; +import type { Component, ComponentObject, ElementObject } from '@oakjs/core'; import { Droppable, classNames } from '@junipero/react'; import { useBuilder } from '../hooks'; import Element from '../Element'; -import Catalogue from '../Catalogue'; +import Catalogue, { type CatalogueRef } from '../Catalogue'; + +export declare interface ContainerProps extends ComponentPropsWithoutRef { + element?: ElementObject; + className?: string; + content?: Array; + component?: ComponentObject | Component; + depth?: number; +} const Container = ({ element, @@ -11,12 +20,12 @@ const Container = ({ className, content = [], depth = 0, -}) => { - const prependCatalogueRef = useRef(); - const appendCatalogueRef = useRef(); +}: ContainerProps) => { + const prependCatalogueRef = useRef(); + const appendCatalogueRef = useRef(); const { builder } = useBuilder(); - const onPrepend = c => { + const onPrepend = (c: Component | ComponentObject) => { prependCatalogueRef.current?.close(); builder.addElement?.({}, { parent: content, @@ -25,7 +34,7 @@ const Container = ({ }); }; - const onAppend = c => { + const onAppend = (c: Component | ComponentObject) => { appendCatalogueRef.current?.close(); builder.addElement?.({}, { parent: content, @@ -34,7 +43,7 @@ const Container = ({ }); }; - const onDrop = data => { + const onDrop = (data: ElementObject) => { if (component?.disallow?.includes?.(data.type)) { return; } @@ -45,7 +54,7 @@ const Container = ({ }); }; - const onPasteBefore = elmt => { + const onPasteBefore = (elmt: ElementObject) => { prependCatalogueRef.current?.close(); builder.addElements([].concat(elmt || []), { parent: content, @@ -54,7 +63,7 @@ const Container = ({ }); }; - const onPasteAfter = elmt => { + const onPasteAfter = (elmt: ElementObject) => { appendCatalogueRef.current?.close(); builder.addElements([].concat(elmt || []), { parent: content, diff --git a/packages/react/lib/DisplayableSettings/Property.js b/packages/react/lib/DisplayableSettings/Property.tsx similarity index 50% rename from packages/react/lib/DisplayableSettings/Property.js rename to packages/react/lib/DisplayableSettings/Property.tsx index 2da6c3a0a..07a63869b 100644 --- a/packages/react/lib/DisplayableSettings/Property.js +++ b/packages/react/lib/DisplayableSettings/Property.tsx @@ -1,23 +1,40 @@ -import { useMemo } from 'react'; +import { type ComponentPropsWithoutRef, useMemo } from 'react'; +import type { + ComponentOverride, + ComponentOverrideObject, + ComponentSettingsFieldObject, + ComponentSettingsFieldOptionObject, + ElementObject, +} from '@oakjs/core'; import { get } from '@junipero/react'; import Text from '../Text'; +interface PropertyProps extends ComponentPropsWithoutRef { + element: ElementObject; + field: ComponentSettingsFieldObject; + override?: ComponentOverrideObject | ComponentOverride; +} + const Property = ({ element, field: setting, override, -}) => { - const field = useMemo(() => ({ +}: PropertyProps) => { + const field = useMemo(() => ({ ...setting, ...override?.fields?.find(f => f.key === setting.key) || {}, }), [setting, override]); - const value = get(element, field.key, field.default); + const value = useMemo(() => ( + get(element, field.key as string, field.default) + ), [element, field]); const option = useMemo(() => ( field.options - ? field.options.find(o => o.value === value || o === value) + ? field.options.find( + (o: { value: ComponentSettingsFieldOptionObject[]}) => + o.value === value || o === value) : null ), [field, value]); diff --git a/packages/react/lib/DisplayableSettings/index.d.ts b/packages/react/lib/DisplayableSettings/index.d.ts deleted file mode 100644 index c2e2253aa..000000000 --- a/packages/react/lib/DisplayableSettings/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ReactNode, ComponentPropsWithoutRef } from 'react'; -import { - ElementObject, - ComponentObject, - Component, - ComponentOverride, - ComponentOverrideObject, -} from '@oakjs/core'; - -export declare interface DisplayableSettingsProps - extends ComponentPropsWithoutRef { - element?: ElementObject; - component?: ComponentObject | Component; - override?: ComponentOverrideObject | ComponentOverride; -} - -declare function DisplayableSettings(props: DisplayableSettingsProps): - ReactNode | JSX.Element; - -export default DisplayableSettings; diff --git a/packages/react/lib/DisplayableSettings/index.js b/packages/react/lib/DisplayableSettings/index.tsx similarity index 54% rename from packages/react/lib/DisplayableSettings/index.js rename to packages/react/lib/DisplayableSettings/index.tsx index 3baa5d6d7..19e9228ad 100644 --- a/packages/react/lib/DisplayableSettings/index.js +++ b/packages/react/lib/DisplayableSettings/index.tsx @@ -1,14 +1,35 @@ -import { Fragment, useMemo } from 'react'; +import { type ComponentPropsWithoutRef, Fragment, useMemo } from 'react'; +import type { + Component, + ComponentObject, + ComponentOverride, + ComponentOverrideObject, + ComponentSettingsFieldObject, + ElementObject, + SettingOverrideObject, +} from '@oakjs/core'; import { classNames } from '@junipero/react'; import { useBuilder } from '../hooks'; import Text from '../Text'; import Property from './Property'; -const DisplayableSettings = ({ className, element, component, override }) => { +export declare interface DisplayableSettingsProps + extends ComponentPropsWithoutRef { + element?: ElementObject; + component?: ComponentObject | Component; + override?: ComponentOverrideObject | ComponentOverride; +} + +const DisplayableSettings = ({ + className, + element, + component, + override, +}: DisplayableSettingsProps) => { const { builder } = useBuilder(); - const getSettingPriority = setting => { + const getSettingPriority = (setting: SettingOverrideObject) => { const fieldOverride = { ...builder.getOverride('setting', element.type, { setting }), ...builder.getOverride('component', element.type, { @@ -25,7 +46,10 @@ const DisplayableSettings = ({ className, element, component, override }) => { builder .getComponentDisplayableSettings(element, { component }) .filter(s => !s.condition || s.condition(element)) - .sort((a, b) => getSettingPriority(b) - getSettingPriority(a)) + .sort((a, b) => + getSettingPriority(b as SettingOverrideObject) - + getSettingPriority(a as SettingOverrideObject) + ) ), [element, component]); if (displayableSettings.length <= 0) { @@ -39,8 +63,11 @@ const DisplayableSettings = ({ className, element, component, override }) => { className, )} > - { displayableSettings.map((setting, i) => ( - + { displayableSettings.map(( + setting: ComponentSettingsFieldObject, + i: number, + ) => ( + { i < displayableSettings.length - 1 && ( @@ -51,4 +78,6 @@ const DisplayableSettings = ({ className, element, component, override }) => { ); }; +DisplayableSettings.displayName = 'DisplayableSettings'; + export default DisplayableSettings; diff --git a/packages/react/lib/Editable/Field.js b/packages/react/lib/Editable/Field.tsx similarity index 65% rename from packages/react/lib/Editable/Field.js rename to packages/react/lib/Editable/Field.tsx index 0a7817c46..01fb60a41 100644 --- a/packages/react/lib/Editable/Field.js +++ b/packages/react/lib/Editable/Field.tsx @@ -1,7 +1,27 @@ -import { useMemo } from 'react'; +import { + type ComponentPropsWithoutRef, + type MutableRefObject, + useMemo, +} from 'react'; +import type { + ComponentObject, + ComponentSettingsFieldObject, + ElementObject, + FieldObject, + FieldOverride, +} from '@oakjs/core'; import { useBuilder } from '../hooks'; +export interface FieldProps extends ComponentPropsWithoutRef { + setting: ComponentSettingsFieldObject; + element: ElementObject; + component: ComponentObject; + onChange: (key: string, value: any) => void; + onCustomChange: (key: string, field: FieldObject, value: any) => void; + editableRef: MutableRefObject; +} + const Field = ({ setting: fieldSetting, element, @@ -9,7 +29,7 @@ const Field = ({ onChange, onCustomChange, editableRef, -}) => { +}: FieldProps) => { const { builder, addons, floatingsRef } = useBuilder(); const overrides = useMemo(() => ({ @@ -36,11 +56,11 @@ const Field = ({ disabled: setting.disabled, value: builder.getElementSettings(element, setting.key, setting.default), required: setting.required, - onChange: overrides?.onChange + onChange: (overrides?.field as FieldOverride)?.onChange ? onCustomChange.bind(null, setting.key, overrides.field) : onChange.bind(null, setting.key), ...field?.props, - ...overrides.field?.props, + ...(overrides.field as FieldOverride)?.props, }; if (setting.condition && @@ -48,7 +68,8 @@ const Field = ({ return null; } - return (overrides.field?.render || field?.render)?.(fieldProps, { + return ( + (overrides.field as FieldOverride)?.render || field?.render)?.(fieldProps, { onChange: onChange.bind(null, setting.key), field, setting, diff --git a/packages/react/lib/Editable/Form.js b/packages/react/lib/Editable/Form.tsx similarity index 67% rename from packages/react/lib/Editable/Form.js rename to packages/react/lib/Editable/Form.tsx index 8633b26b6..f84ef9df0 100644 --- a/packages/react/lib/Editable/Form.js +++ b/packages/react/lib/Editable/Form.tsx @@ -1,22 +1,47 @@ -import { useReducer } from 'react'; import { - Button, - Tabs, - Tab, - Label, + type ComponentPropsWithoutRef, + type Key, + useReducer, +} from 'react'; +import type { + ComponentObject, + ComponentOverride, + ComponentSettingsFieldObject, + ComponentSettingsFormObject, + ComponentSettingsTabObject, + ElementObject, FieldContent, + FieldObject, + FieldOverride, + FieldOverrideObject, +} from '@oakjs/core'; +import { Abstract, + Button, FieldControl, + Label, + Tab, + Tabs, Tooltip, - mockState, - cloneDeep, classNames, + cloneDeep, + mockState, } from '@junipero/react'; import { useBuilder } from '../hooks'; -import Text from '../Text'; import Icon from '../Icon'; +import Text from '../Text'; import Field from './Field'; +export interface FormProps extends ComponentPropsWithoutRef { + placement?: string; + element: ElementObject; + component: ComponentObject; + className?: string; + onSave: () => void; + onCancel: () => void; + editableRef?: any; +} + const Form = ({ placement, element, @@ -26,34 +51,39 @@ const Form = ({ onCancel, editableRef, ...rest -}) => { +}: FormProps) => { const { builder } = useBuilder(); const overrides = builder.getOverride('component', element.type); - const deserialize = overrides?.deserialize || component?.deserialize || - (e => e); + const deserialize = (overrides as ComponentOverride)?.deserialize || + component?.deserialize || + ((e: ElementObject) => e); const [state, dispatch] = useReducer(mockState, { element: deserialize(cloneDeep(element)), }); - const onUpdate_ = elmt => { + const onUpdate_ = (elmt: ElementObject) => { dispatch({ element: elmt }); }; - const onSettingChange_ = (name, field) => { + const onSettingChange_ = (name: string, field: FieldContent) => { builder.setElementSettings(state.element, name, field.checked ?? field.value); dispatch({ element: state.element }); }; - const onSettingCustomChange_ = (name, renderer, field) => { + const onSettingCustomChange_ = ( + name: string, + renderer: FieldOverride | FieldObject | FieldOverrideObject, + field: FieldContent + ) => { const changes = renderer .onChange(name, field, state.element); dispatch({ element: Object.assign(state.element, changes) }); }; const onSave_ = () => { - builder.setElement(element.id, state.element || {}, { element }); + builder.setElement(element.id as string, state.element || {}, { element }); onSave(); }; @@ -62,7 +92,7 @@ const Form = ({ onCancel(); }; - const getFieldPriority = field => { + const getFieldPriority = (field: ComponentSettingsFieldObject) => { const fieldOverride = { ...builder.getOverride('setting', element.type, { setting: field }), ...builder.getOverride('component', element.type, { @@ -75,10 +105,12 @@ const Form = ({ : field.priority || 0; }; - const hasSubfields = setting => + const hasSubfields = (setting: ComponentSettingsFieldObject) => Array.isArray(setting.fields) && setting.fields.length > 0; - const tabs = builder.getAvailableSettings(); + const tabs: Array< + ComponentSettingsTabObject | ComponentSettingsFormObject + > = builder.getAvailableSettings(); return (
{ tabs .concat( - (component.settings?.fields || []).filter(f => f.type === 'tab') + (component.settings?.fields || []) + .filter((f: FieldObject) => f.type === 'tab') ) - .sort((a, b) => (b.priority || 0) - (a.priority || 0)) - .filter(tab => tab.type === 'tab' && + .sort(( + a: ComponentSettingsTabObject, + b: ComponentSettingsTabObject, + ) => (b.priority || 0) - (a.priority || 0)) + .filter((tab: ComponentSettingsTabObject) => tab.type === 'tab' && (!tab.condition || tab.condition(state.element, { component, builder }))) - .map((tab, t) => ( - { tab.title }}> + .map((tab: ComponentSettingsTabObject, t) => ( + { tab.title }} + >
{ (component.settings?.fields || []) - .filter(field => (tab.id === 'general' && !field.tab) || - field.tab === tab.id) + .filter((field: ComponentSettingsFieldObject) => + (tab.id === 'general' && !field.tab) || + field.tab === tab.id + ) .concat(tab.fields) - .sort((a, b) => getFieldPriority(b) - getFieldPriority(a)) - .filter(f => + .sort(( + a: ComponentSettingsFieldObject, + b: ComponentSettingsFieldObject + ) => getFieldPriority(b) - getFieldPriority(a)) + .filter((f: ComponentSettingsFieldObject) => !f.condition || f.condition(state.element, { component, builder }) ) - .map((setting, i) => ( + .map((setting: ComponentSettingsFieldObject, i: Key) => (
{ setting.label && (
) } diff --git a/packages/react/lib/components/EmptySpace/index.d.ts b/packages/react/lib/components/EmptySpace/index.d.ts deleted file mode 100644 index 317162bee..000000000 --- a/packages/react/lib/components/EmptySpace/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ElementObject } from '@oakjs/core'; -import { ReactNode, ComponentPropsWithoutRef } from 'react'; - -declare interface EmptySpaceProps extends ComponentPropsWithoutRef { - element: ElementObject; -} - -declare function EmptySpace(props: EmptySpaceProps): ReactNode | JSX.Element; - -export default EmptySpace; diff --git a/packages/react/lib/components/EmptySpace/index.js b/packages/react/lib/components/EmptySpace/index.ts similarity index 64% rename from packages/react/lib/components/EmptySpace/index.js rename to packages/react/lib/components/EmptySpace/index.ts index 49c086302..1710c15ec 100644 --- a/packages/react/lib/components/EmptySpace/index.js +++ b/packages/react/lib/components/EmptySpace/index.ts @@ -1,4 +1,4 @@ -const EmptySpace = () => null; +const EmptySpace = (): null => null; EmptySpace.displayName = 'EmptySpace'; diff --git a/packages/react/lib/components/Foldable/index.d.ts b/packages/react/lib/components/Foldable/index.d.ts deleted file mode 100644 index 0e7b0889a..000000000 --- a/packages/react/lib/components/Foldable/index.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode, ComponentPropsWithoutRef } from 'react'; -import { - ComponentObject, - ElementObject, - Component, -} from '@oakjs/core'; - -declare interface FoldableProps extends ComponentPropsWithoutRef { - element: ElementObject; - component?: ComponentObject | Component; - parentComponent?: ComponentObject | Component; - parent?: Array; - depth?: number; -} - -declare function Foldable(props: FoldableProps): ReactNode | JSX.Element; - -export default Foldable; diff --git a/packages/react/lib/components/Foldable/index.js b/packages/react/lib/components/Foldable/index.tsx similarity index 82% rename from packages/react/lib/components/Foldable/index.js rename to packages/react/lib/components/Foldable/index.tsx index ce1c4d3de..af1426004 100644 --- a/packages/react/lib/components/Foldable/index.js +++ b/packages/react/lib/components/Foldable/index.tsx @@ -1,9 +1,19 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import type { ComponentObject, ElementObject } from '@oakjs/core'; import { Droppable, omit, classNames } from '@junipero/react'; import { useBuilder } from '../../hooks'; import Text from '../../Text'; import Container from '../../Container'; +export interface FoldableProps extends ComponentPropsWithoutRef { + element: ElementObject; + parent: Array; + component: ComponentObject; + parentComponent: ComponentObject; + depth?: number; +} + const Foldable = ({ component, parentComponent, @@ -12,10 +22,13 @@ const Foldable = ({ className, depth = 0, ...rest -}) => { +}: FoldableProps) => { const { builder } = useBuilder(); - const onDropElement = (position, sibling) => { + const onDropElement = ( + position: 'before' | 'after', + sibling: ElementObject + ) => { if (parentComponent?.disallow?.includes?.(sibling.type)) { return; } @@ -78,7 +91,7 @@ const Foldable = ({
diff --git a/packages/react/lib/components/Image/index.d.ts b/packages/react/lib/components/Image/index.d.ts deleted file mode 100644 index 0371b0adc..000000000 --- a/packages/react/lib/components/Image/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ElementObject } from '@oakjs/core'; -import { ReactNode, ComponentPropsWithoutRef } from 'react'; - -declare interface ImageProps extends ComponentPropsWithoutRef { - element: ElementObject; -} - -declare function Image(props: ImageProps): ReactNode | JSX.Element; - -export default Image; diff --git a/packages/react/lib/components/Image/index.js b/packages/react/lib/components/Image/index.tsx similarity index 79% rename from packages/react/lib/components/Image/index.js rename to packages/react/lib/components/Image/index.tsx index c87d2096d..2e9aa9ed4 100644 --- a/packages/react/lib/components/Image/index.js +++ b/packages/react/lib/components/Image/index.tsx @@ -1,11 +1,17 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import type { ElementObject } from '@oakjs/core'; import { classNames } from '@junipero/react'; import Text from '../../Text'; +export interface ImageProps extends ComponentPropsWithoutRef { + element: ElementObject; +} + const Image = ({ element, className, -}) => { +}: ImageProps) => { const getName = () => element.name || (/data:/.test(element.url) ? ( diff --git a/packages/react/lib/components/Row/index.d.ts b/packages/react/lib/components/Row/index.d.ts deleted file mode 100644 index 50e3e9e82..000000000 --- a/packages/react/lib/components/Row/index.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode, ComponentPropsWithoutRef } from 'react'; -import { - ComponentObject, - ElementObject, - Component, -} from '@oakjs/core'; - -declare interface RowProps extends ComponentPropsWithoutRef { - element: ElementObject; - parent: Array; - component?: ComponentObject | Component; - parentComponent?: ComponentObject | Component; - depth?: number; -} - -declare function Row(props: RowProps): ReactNode | JSX.Element; - -export default Row; diff --git a/packages/react/lib/components/Row/index.js b/packages/react/lib/components/Row/index.tsx similarity index 71% rename from packages/react/lib/components/Row/index.js rename to packages/react/lib/components/Row/index.tsx index 48076526b..1888f88fd 100644 --- a/packages/react/lib/components/Row/index.js +++ b/packages/react/lib/components/Row/index.tsx @@ -1,8 +1,17 @@ +import type { ComponentPropsWithoutRef, Key } from 'react'; +import type { ComponentObject, ElementObject } from '@oakjs/core'; import { Droppable, classNames, omit } from '@junipero/react'; import { useBuilder } from '../../hooks'; import Col from '../Col'; +export interface RowProps extends ComponentPropsWithoutRef { + element: ElementObject; + parent: Array; + parentComponent: ComponentObject; + depth?: number; +} + const Row = ({ element, parent, @@ -10,10 +19,10 @@ const Row = ({ className, depth = 0, ...rest -}) => { +}: RowProps) => { const { builder } = useBuilder(); - const onDivide = (index, isBefore) => { + const onDivide = (index: number, isBefore: boolean) => { if (!element.cols || element.cols.length <= 0) { element.cols = [{ content: [], @@ -30,22 +39,33 @@ const Row = ({ type: 'col', }); - builder.setElement(element.id, { cols: element.cols }, { element }); + builder.setElement( + element.id as string, + { cols: element.cols }, + { element } + ); }; - const onRemoveCol = index => { + const onRemoveCol = (index: number) => { if (element.cols?.length > 0) { element.cols.splice(index, 1); } if (element.cols?.length <= 0) { - builder.removeElement(element.id, { parent }); + builder.removeElement(element.id as string, { parent }); } else { - builder.setElement(element.id, { cols: element.cols }, { element }); + builder.setElement( + element.id as string, + { cols: element.cols }, + { element } + ); } }; - const onDropElement = (position, sibling) => { + const onDropElement = ( + position: 'before' | 'after', + sibling: ElementObject + ) => { if (parentComponent?.disallow?.includes?.(sibling.type)) { return; } @@ -78,7 +98,7 @@ const Row = ({ 'oak-justify-' + element.settings.justifyContent, )} > - { element?.cols?.map((col, i) => ( + { element?.cols?.map((col: ElementObject, i: Key) => ( { - element: ElementObject; -} - -declare function Text(props: TextProps): ReactNode | JSX.Element; - -export default Text; diff --git a/packages/react/lib/components/Text/index.js b/packages/react/lib/components/Text/index.js deleted file mode 100644 index b9886414b..000000000 --- a/packages/react/lib/components/Text/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import { classNames } from '@junipero/react'; - -import { sanitizeHTML } from '../../utils'; - -const Text = ({ element, className }) => !element.content ? null : ( -
-); - -Text.displayName = 'TextComponent'; - -export default Text; diff --git a/packages/react/lib/components/Text/index.tsx b/packages/react/lib/components/Text/index.tsx new file mode 100644 index 000000000..5f108827a --- /dev/null +++ b/packages/react/lib/components/Text/index.tsx @@ -0,0 +1,25 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import type { ElementObject } from '@oakjs/core'; +import { classNames } from '@junipero/react'; + +import { sanitizeHTML } from '../../utils'; + +export interface TextComponentProps extends ComponentPropsWithoutRef { + element: ElementObject; +} + +const Text = ({ + element, + className, +}: TextComponentProps) => !element.content ? null : ( +
+); + +Text.displayName = 'TextComponent'; + +export default Text; diff --git a/packages/react/lib/components/Title/index.d.ts b/packages/react/lib/components/Title/index.d.ts deleted file mode 100644 index b6bd3586e..000000000 --- a/packages/react/lib/components/Title/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ElementObject } from '@oakjs/core'; -import { ReactNode, ComponentPropsWithoutRef } from 'react'; - -declare interface TitleProps extends ComponentPropsWithoutRef { - element: ElementObject; -} - -declare function Title(props: TitleProps): ReactNode | JSX.Element; - -export default Title; diff --git a/packages/react/lib/components/Title/index.js b/packages/react/lib/components/Title/index.tsx similarity index 56% rename from packages/react/lib/components/Title/index.js rename to packages/react/lib/components/Title/index.tsx index 0a4859f0b..1fd63c9fc 100644 --- a/packages/react/lib/components/Title/index.js +++ b/packages/react/lib/components/Title/index.tsx @@ -1,10 +1,16 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import type { ElementObject } from '@oakjs/core'; import { classNames } from '@junipero/react'; import { sanitizeHTML } from '../../utils'; -const Title = ({ element, className }) => { +export interface TitleProps extends ComponentPropsWithoutRef { + element: ElementObject; +} + +const Title = ({ element, className }: TitleProps) => { const Tag = element.headingLevel || 'h1'; - const sizes = { + const sizes: { [key: string]: string } = { h1: '!oak-text-4xl', h2: '!oak-text-3xl', h3: '!oak-text-2xl', @@ -22,7 +28,9 @@ const Title = ({ element, className }) => { sizes[Tag], className )} - dangerouslySetInnerHTML={{ __html: sanitizeHTML(element.content) }} + dangerouslySetInnerHTML={ + { __html: sanitizeHTML(element.content as string) } + } /> ); }; diff --git a/packages/react/lib/components/index.d.ts b/packages/react/lib/components/index.d.ts deleted file mode 100644 index 88b20db01..000000000 --- a/packages/react/lib/components/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { default as ButtonComponent } from './Button'; -export { default as ColComponent } from './Col'; -export { default as EmptySpaceComponent } from './EmptySpace'; -export { default as FoldableComponent } from './Foldable'; -export { default as ImageComponent } from './Image'; -export { default as RowComponent } from './Row'; -export { default as TextComponent } from './Text'; -export { default as TitleComponent } from './Title'; diff --git a/packages/react/lib/components/index.js b/packages/react/lib/components/index.js deleted file mode 100644 index bb48046e2..000000000 --- a/packages/react/lib/components/index.js +++ /dev/null @@ -1,8 +0,0 @@ -export { default as Button } from './Button'; -export { default as Col } from './Col'; -export { default as EmptySpace } from './EmptySpace'; -export { default as Foldable } from './Foldable'; -export { default as Image } from './Image'; -export { default as Row } from './Row'; -export { default as Text } from './Text'; -export { default as Title } from './Title'; diff --git a/packages/react/lib/components/index.ts b/packages/react/lib/components/index.ts new file mode 100644 index 000000000..880f527e4 --- /dev/null +++ b/packages/react/lib/components/index.ts @@ -0,0 +1,38 @@ +export { + default as Button, + type ButtonProps, +} from './Button'; + +export { + default as Col, + type ColProps, +} from './Col'; + +export { + default as EmptySpace, +} from './EmptySpace'; + +export { + default as Foldable, + type FoldableProps, +} from './Foldable'; + +export { + default as Image, + type ImageProps, +} from './Image'; + +export { + default as Row, + type RowProps, +} from './Row'; + +export { + default as Text, + type TextComponentProps, +} from './Text'; + +export { + default as Title, + type TitleProps, +} from './Title'; diff --git a/packages/react/lib/contexts.d.ts b/packages/react/lib/contexts.d.ts deleted file mode 100644 index 4c5d92b3c..000000000 --- a/packages/react/lib/contexts.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ChangeEvent, Context, MutableRefObject } from 'react'; -import { - AddonObject, - Builder, - ComponentSettingsField, - ElementObject, -} from '@oakjs/core'; - -export declare type BuilderContext = Context<{ - builder: Builder; - content: ElementObject[]; - addons: AddonObject[]; - rootBoundary: MutableRefObject; - onImageUpload(event: ChangeEvent, opts?: { - element?: ElementObject; - setting?: ComponentSettingsField; - }): { name?: string; url?: string; }; - rootRef: MutableRefObject; - floatingsRef: MutableRefObject[]; -}>; diff --git a/packages/react/lib/contexts.js b/packages/react/lib/contexts.js deleted file mode 100644 index 20e16362a..000000000 --- a/packages/react/lib/contexts.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react'; - -export const BuilderContext = createContext({}); diff --git a/packages/react/lib/contexts.ts b/packages/react/lib/contexts.ts new file mode 100644 index 000000000..7bd009002 --- /dev/null +++ b/packages/react/lib/contexts.ts @@ -0,0 +1,37 @@ +import { + type ChangeEvent, + type MutableRefObject, + createContext, +} from 'react'; +import type { + AddonObject, + Builder, + ComponentSettingsFieldObject, + ElementObject, +} from '@oakjs/core'; + +import type { ImageUploadCallbackResult } from './types'; + +export declare type BuilderContextValue = { + builder?: Builder; + content?: ElementObject[]; + addons?: AddonObject[]; + rootBoundary?: + MutableRefObject | + string | + Element | + DocumentFragment; + boundary?: + MutableRefObject | + string | + Element | + DocumentFragment; + onImageUpload?(event: ChangeEvent, opts?: { + element?: ElementObject; + setting?: ComponentSettingsFieldObject; + }): Promise; + rootRef?: MutableRefObject; + floatingsRef?: MutableRefObject; +}; + +export const BuilderContext = createContext({}); diff --git a/packages/react/lib/fields/ImageField/index.d.ts b/packages/react/lib/fields/ImageField/index.d.ts deleted file mode 100644 index fbf7a5569..000000000 --- a/packages/react/lib/fields/ImageField/index.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode, ComponentPropsWithoutRef } from 'react'; - -export declare interface ImageFieldValue { - url: string; - name: string; - [key: string]: any; -} - -declare interface ImageFieldProps extends ComponentPropsWithoutRef { - value?: ImageFieldValue; - iconOnly?: boolean; - accept?: ('image/jpeg' | 'image/jpg' | 'image/png' | 'image/svg+xml')[]; - onChange?(field: { value: ImageFieldValue, valid: boolean }): void; -} - -declare function ImageField(props: ImageFieldProps): ReactNode | JSX.Element; - -export default ImageField; diff --git a/packages/react/lib/fields/ImageField/index.js b/packages/react/lib/fields/ImageField/index.tsx similarity index 69% rename from packages/react/lib/fields/ImageField/index.js rename to packages/react/lib/fields/ImageField/index.tsx index 3c75adb9d..ca8e2356c 100644 --- a/packages/react/lib/fields/ImageField/index.js +++ b/packages/react/lib/fields/ImageField/index.tsx @@ -1,9 +1,44 @@ -import { useEffect, useReducer } from 'react'; -import { TouchableZone, Spinner, classNames, mockState } from '@junipero/react'; +import { + type ComponentPropsWithRef, + type MouseEvent, + useEffect, + useReducer, + ChangeEvent, +} from 'react'; +import type { + ComponentSettingsField, + ComponentSettingsFieldObject, + ElementObject, +} from '@oakjs/core'; +import { + TouchableZone, + Spinner, + classNames, + mockState, +} from '@junipero/react'; import { useBuilder } from '../../hooks'; -import Text from '../../Text'; import Icon from '../../Icon'; +import Text from '../../Text'; + +export declare interface ImageFieldValue { + name: string; + url: string | ArrayBuffer; +} + +export declare interface ImageFieldContent { + value?: ImageFieldValue; +} + +export interface ImageFieldProps extends ComponentPropsWithRef { + value?: ImageFieldValue; + element?: ElementObject; + setting?: ComponentSettingsFieldObject | ComponentSettingsField; + onOpenDialog?: () => void; + onChange?: (value: ImageFieldContent) => void; + iconOnly?: boolean; + accept?: string[]; +} const ImageField = ({ className, @@ -14,7 +49,7 @@ const ImageField = ({ onChange, iconOnly = false, accept = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg+xml'], -}) => { +}: ImageFieldProps) => { const { onImageUpload } = useBuilder(); const [state, dispatch] = useReducer(mockState, { value: { @@ -30,7 +65,7 @@ const ImageField = ({ } }, [value]); - const onOpenFileDialog = e => { + const onOpenFileDialog = (e: MouseEvent) => { e.preventDefault(); if (state.loading) { @@ -45,11 +80,11 @@ const ImageField = ({ input.type = 'file'; input.accept = accept.join(','); - input.addEventListener('change', onFile, false); + input.addEventListener('change', onFile.bind(null), false); input.click(); }; - const onFile = async e => { + const onFile = async (e: ChangeEvent) => { if (state.loading) { return; } @@ -65,7 +100,7 @@ const ImageField = ({ dispatch({ loading: false }); } } else { - const file = e.target.files[0]; + const file = (e.target as HTMLInputElement).files[0]; if (file) { const fr = new FileReader(); @@ -80,14 +115,18 @@ const ImageField = ({ } }; - const onUrlReady = ({ url, name, ...rest } = {}) => { - const val = { url, name, ...rest }; + const onUrlReady = ({ + url, + name, + ...rest + }: { url?: string | ArrayBuffer, name?: string } = {}) => { + const val: ImageFieldValue = { url, name, ...rest }; dispatch({ value: val }); onChange?.({ value: val }); dispatch({ loading: false }); }; - const onReset = e => { + const onReset = (e: MouseEvent) => { e.preventDefault(); dispatch({ value: null }); onChange?.({ value: null }); @@ -101,10 +140,10 @@ const ImageField = ({ return (
@@ -135,16 +176,12 @@ const ImageField = ({ { state.loading ? ( diff --git a/packages/react/lib/fields/index.d.ts b/packages/react/lib/fields/index.d.ts deleted file mode 100644 index 14434fc14..000000000 --- a/packages/react/lib/fields/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ImageField, ImageFieldValue } from './ImageField'; diff --git a/packages/react/lib/fields/index.js b/packages/react/lib/fields/index.js deleted file mode 100644 index 8622986e7..000000000 --- a/packages/react/lib/fields/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as ImageField } from './ImageField'; diff --git a/packages/react/lib/fields/index.ts b/packages/react/lib/fields/index.ts new file mode 100644 index 000000000..c00873f03 --- /dev/null +++ b/packages/react/lib/fields/index.ts @@ -0,0 +1,6 @@ +export { + default as ImageField, + type ImageFieldProps, + type ImageFieldValue, + type ImageFieldContent, +} from './ImageField'; diff --git a/packages/react/lib/hooks.d.ts b/packages/react/lib/hooks.d.ts deleted file mode 100644 index da5e71cba..000000000 --- a/packages/react/lib/hooks.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { AddonObject, Builder, BuilderOptions, ElementObject, Logger } from '@oakjs/core'; - -import { BuilderContextValue } from './Builder'; - -export declare function useRootBuilder(opts?: Partial; - activeTextSheet?: string; -}>): { - builder: Builder; - content: Array; - activeTextSheet: string; - canUndo: boolean; - canRedo: boolean; - addons: Array; -}; - -export declare function useBuilder(): BuilderContextValue; - -export declare function useLogger(): Logger; diff --git a/packages/react/lib/hooks.js b/packages/react/lib/hooks.ts similarity index 77% rename from packages/react/lib/hooks.js rename to packages/react/lib/hooks.ts index a9a5f1820..1d2f3966f 100644 --- a/packages/react/lib/hooks.js +++ b/packages/react/lib/hooks.ts @@ -1,9 +1,21 @@ import { useContext, useEffect, useMemo, useReducer } from 'react'; -import { Builder } from '@oakjs/core'; -import { mockState, useEffectAfterMount } from '@junipero/react'; +import { + type AddonObject, + type ElementObject, + Builder, +} from '@oakjs/core'; +import { MockState, mockState, useEffectAfterMount } from '@junipero/react'; import { BuilderContext } from './contexts'; +export interface RootBuilderState { + content: ElementObject[]; + activeTextSheet: string | null; + addons: AddonObject[]; + canUndo: boolean; + canRedo: boolean; +} + export const useRootBuilder = ({ activeTextSheet, content, @@ -11,14 +23,21 @@ export const useRootBuilder = ({ onChange, onEvent, ...opts -}) => { +}: { + activeTextSheet?: string; + content?: Array; + defaultContent?: Array; + onChange?: (content: Array) => void; + onEvent?: (eventName: string, ...args: any[]) => void; + addons?: AddonObject[], +} = {}) => { const builder = useMemo(() => ( new Builder({ ...opts, content: defaultContent || content, }) ), []); - const [state, dispatch] = useReducer(mockState, { + const [state, dispatch] = useReducer>(mockState, { content: builder.getContent(), activeTextSheet: null, addons: opts.addons, @@ -44,7 +63,10 @@ export const useRootBuilder = ({ }, [content]); useEffect(() => { - const unsubscribe = builder.subscribe((eventName, ...args) => { + const unsubscribe = builder.subscribe(( + eventName: string, + ...args: any[] + ) => { switch (eventName) { case 'content.update': { const [content_] = args; @@ -115,9 +137,3 @@ export const useRootBuilder = ({ }; export const useBuilder = () => useContext(BuilderContext); - -export const useLogger = rootBuilder => { - const { builder } = useBuilder(); - - return builder.logger || rootBuilder?.builder?.logger; -}; diff --git a/packages/react/lib/index.d.ts b/packages/react/lib/index.d.ts deleted file mode 100644 index face0a3f8..000000000 --- a/packages/react/lib/index.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -export { - TextField, - SelectField, - DateField, - ColorField, - Card, - Dropdown, - DropdownMenu, - DropdownToggle, - Toggle, - TouchableZone, - Label, - classNames, - useTimeout, -} from '@junipero/react'; - -export * from '@oakjs/core/lib/types'; - -export { - default as Builder, - BuilderProps, - BuilderRef, - ImageUploadCallbackResult, -} from './Builder'; -export { - default as Catalogue, - CatalogueProps, - CatalogueRef, -} from './Catalogue'; -export { - default as Container, - ContainerProps, -} from './Container'; -export { - default as Editable, - EditableProps, - Form, - FormProps, - Field as EditableField, - FieldProps as EditableFieldProps, -} from './Editable'; -export { - default as Element, - ElementProps, -} from './Element'; -export { - default as Icon, - IconProps, -} from './Icon'; -export { - default as Option, - OptionProps, - OptionObject, - OptionRef, -} from './Option'; -export { - default as ReactText, - TextProps, -} from './Text'; -export { - default as DisplayableSettings, - DisplayableSettingsProps, -} from './DisplayableSettings'; - -export * from './addons'; -export * from './components'; -export * from './contexts'; -export * from './fields'; -export * from './hooks'; -export * from './options'; -export * from './utils'; diff --git a/packages/react/lib/index.js b/packages/react/lib/index.js deleted file mode 100644 index 067c861d0..000000000 --- a/packages/react/lib/index.js +++ /dev/null @@ -1,32 +0,0 @@ -export { - TextField, - SelectField, - DateField, - ColorField, - Card, - Dropdown, - DropdownMenu, - DropdownToggle, - Toggle, - TouchableZone, - Label, - classNames, - useTimeout, -} from '@junipero/react'; - -export { default as Builder } from './Builder'; -export { default as Container } from './Container'; -export { default as Catalogue } from './Catalogue'; -export { default as Icon } from './Icon'; -export { default as Text } from './Text'; -export { default as Field } from './Editable/Field'; -export { default as DisplayableSettings } from './DisplayableSettings'; - -export { - useBuilder, - useLogger, -} from './hooks'; - -export * from './fields'; -export * from './options'; -export * from './addons'; diff --git a/packages/react/lib/index.stories.js b/packages/react/lib/index.stories.tsx similarity index 79% rename from packages/react/lib/index.stories.js rename to packages/react/lib/index.stories.tsx index 9ef2891e8..1a2d612de 100644 --- a/packages/react/lib/index.stories.js +++ b/packages/react/lib/index.stories.tsx @@ -1,12 +1,18 @@ +import type { + AddonObject, + ComponentSettingsFieldObject, + ComponentTabOject, + ElementObject, +} from '@oakjs/core'; import { useEffect, useRef, useState } from 'react'; import { action } from '@storybook/addon-actions'; -import Builder from './Builder'; +import Builder, { type BuilderRef } from './Builder'; import { baseAddon } from './addons'; export default { title: 'React/Builder' }; -const baseContent = [ +const baseContent: ElementObject[] = [ { type: 'row', cols: [ { type: 'col', content: [ { type: 'title', content: 'This is a title' }, @@ -54,7 +60,7 @@ export const controlled = () => { }; export const uncontrolled = () => { - const builderRef = useRef(); + const builderRef = useRef(); const addElement = () => { builderRef.current?.builder.addElement({ @@ -80,7 +86,7 @@ export const uncontrolled = () => { }; export const withCustomTexts = () => { - const builderRef = useRef(); + const builderRef = useRef(); useEffect(() => { builderRef.current?.builder.setActiveTextSheet('fr'); @@ -97,7 +103,7 @@ export const withCustomTexts = () => { pasteFromClipboard: 'Coller depuis le presse-papier', }, } }], - }]} + } as AddonObject]} value={baseContent} rootBoundary={document.documentElement} options={{ debug: true }} @@ -108,7 +114,7 @@ export const withCustomTexts = () => { }; export const withMultipleLanguages = () => { - const builderRef = useRef(); + const builderRef = useRef(); const [locale, setLocale] = useState('fr'); const texts = [ @@ -136,7 +142,7 @@ export const withMultipleLanguages = () => {
"Paste from clipboard" should be in {locale}
{ key: 'settings.className', placeholder: 'This is a global setting placeholder', }], - }] : []} + } as AddonObject] : []} value={baseContent} options={{ debug: true }} onChange={action('change')} @@ -216,14 +222,14 @@ export const withMultipleCustomSettingsAndFields = () => ( label: 'Foo', type: 'weird-text', displayable: true, - condition: e => e.type === 'weird-component', + condition: (e: ElementObject) => e.type === 'weird-component', }, { key: 'settings.bar', label: 'Bar', type: 'weird-text', - condition: e => e.type === 'weird-component', - }], - }]} + condition: (e: ElementObject) => e.type === 'weird-component', + }] as ComponentSettingsFieldObject[], + } as AddonObject]} value={baseContent} options={{ debug: true }} onChange={action('change')} @@ -241,12 +247,11 @@ export const disallowSomeChildren = () => { ...addon, components: addon.components.map(c => c.id === 'core' ? { ...c, - components: c.components.map(c_ => c_.id === 'col' ? { - ...c_, - disallow: ['text'], - } : c_), + components: (c as ComponentTabOject).components.map(c_ => + c_.id === 'col' ? { ...c_, disallow: ['text'] } : c_ + ), } : c), - }]} + } as AddonObject]} value={baseContent} options={{ debug: true }} onChange={action('change')} @@ -255,37 +260,31 @@ export const disallowSomeChildren = () => { ); }; -export const withMergeOverrides = () => { - - return ( - <> - ({ - type: 'title', - headingLevel: 't4', - content: 'This is an updated title', - }), - }], - }]} - value={baseContent} - options={{ debug: true, overrideStrategy: 'merge' }} - onChange={action('change')} - /> - - ); -}; +export const withMergeOverrides = () => ( + ({ + type: 'title', + headingLevel: 't4', + content: 'This is an updated title', + }), + }], + } as AddonObject]} + value={baseContent} + options={{ debug: true, overrideStrategy: 'merge' }} + onChange={action('change')} + /> +); diff --git a/packages/react/lib/index.ts b/packages/react/lib/index.ts new file mode 100644 index 000000000..9b789a32a --- /dev/null +++ b/packages/react/lib/index.ts @@ -0,0 +1,93 @@ +export { + TextField, + type TextFieldProps, + type TextFieldRef, + SelectField, + type SelectFieldProps, + type SelectFieldRef, + DateField, + type DateFieldProps, + type DateFieldRef, + ColorField, + type ColorFieldRef, + type ColorFieldProps, + Card, + type CardProps, + type CardRef, + Dropdown, + type DropdownProps, + type DropdownRef, + DropdownMenu, + type DropdownMenuProps, + type DropdownMenuRef, + DropdownToggle, + type DropdownToggleProps, + type DropdownToggleRef, + Toggle, + type ToggleProps, + type ToggleRef, + TouchableZone, + type TouchableZoneProps, + Label, + type LabelProps, + classNames, + useTimeout, + omit, + pick, + get, + set, +} from '@junipero/react'; + +export type { + Builder as CoreBuilder, + FieldContent, + ElementObject, + AddonObject, + FieldObject, + ComponentObject, +} from '@oakjs/core'; + +export { + default as Builder, + type BuilderProps, +} from './Builder'; + +export { + default as Container, + type ContainerProps, +} from './Container'; + +export { + default as Catalogue, + type CatalogueProps, +} from './Catalogue'; + +export { + default as Icon, +} from './Icon'; + +export { + default as Text, + type TextProps as TextComponentProps, +} from './Text'; + +export { + default as Field, + type FieldProps, +} from './Editable/Field'; + +export { + default as DisplayableSettings, + type DisplayableSettingsProps, +} from './DisplayableSettings'; + +export { + useBuilder, +} from './hooks'; + +export * from './fields'; +export * from './options'; +export * from './addons'; +export * from './utils'; + +export type * from './types'; diff --git a/packages/react/lib/options.d.ts b/packages/react/lib/options.d.ts deleted file mode 100644 index 1a9d5d03f..000000000 --- a/packages/react/lib/options.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ComponentOptionObject } from '.'; - -export declare function dragOption(): ComponentOptionObject; -export declare function backgroundColorOption(): ComponentOptionObject; diff --git a/packages/react/lib/options.js b/packages/react/lib/options.tsx similarity index 71% rename from packages/react/lib/options.js rename to packages/react/lib/options.tsx index cfe187581..c69e92e62 100644 --- a/packages/react/lib/options.js +++ b/packages/react/lib/options.tsx @@ -1,16 +1,25 @@ -import { useRef, useState } from 'react'; +import { ComponentPropsWithoutRef, DragEvent, MouseEvent, MutableRefObject, useRef, useState } from 'react'; import { Draggable, classNames } from '@junipero/react'; +import type { ComponentOptionObject, ElementObject } from '@oakjs/core'; -import Option from './Option'; +import type { EditableRef } from './Editable'; +import Option, { OptionRef } from './Option'; import Text from './Text'; +import { ReactComponentOptionObject } from './types'; + +export interface DragOptionProps extends ComponentPropsWithoutRef<'a'> { + element: ElementObject | ElementObject[]; + elementInnerRef: MutableRefObject; + editableRef: MutableRefObject; +} export const DragOption = ({ element, elementInnerRef, editableRef, className, -}) => { - const optionRef = useRef(); +}: DragOptionProps) => { + const optionRef = useRef(); const [hasTooltip, setHasTooltip] = useState(true); - const onBeforeDragStart = e => { + const onBeforeDragStart = (e: DragEvent) => { setHasTooltip(false); // optionRef.current?.tooltipRef?.current?.close(); @@ -39,7 +48,7 @@ export const DragOption = ({ setHasTooltip(false); }; - const onClick = e => { + const onClick = (e: MouseEvent) => { e.preventDefault(); }; @@ -62,11 +71,11 @@ export const DragOption = ({ ); }; -export const dragOption = () => ({ - render: props => , +export const dragOption = (): ComponentOptionObject => ({ + render: (props: DragOptionProps) => , }); -export const backgroundColorOption = () => ({ +export const backgroundColorOption = (): ReactComponentOptionObject => ({ render: ({ element = {} }) => (element.styles?.backgroundColor || element.styles?.backgroundImage) && (
; + element?: ElementObject | ElementObject[]; + t?: GetTextCallback; + }, + ): ReactNode; +} + +export declare interface ReactComponentObject + extends Omit { + options?: ReactComponentOptionObject[]; + render?( + props: ComponentProps, + opts?: { + t?: GetTextCallback; + }, + ): ReactNode; +} + +export declare interface ReactComponentOptionObject + extends ComponentOptionObject { + render?( + props: { + element: ElementObject; + builder: Builder; + option: ComponentOptionObject; + className: string; + parent: ElementObject[]; + component: ReactComponentObject; + index: number; + elementInnerRef: MutableRefObject; + editableRef: MutableRefObject; + }, + ): ReactNode; +} diff --git a/packages/react/lib/utils.d.ts b/packages/react/lib/utils.d.ts deleted file mode 100644 index 05c0bc773..000000000 --- a/packages/react/lib/utils.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export declare function copyToClipboard(value: string): void; - -export declare function sanitizeHTML(html: string): string; diff --git a/packages/react/lib/utils.js b/packages/react/lib/utils.ts similarity index 85% rename from packages/react/lib/utils.js rename to packages/react/lib/utils.ts index 7e95161fb..de898ee23 100644 --- a/packages/react/lib/utils.js +++ b/packages/react/lib/utils.ts @@ -1,7 +1,7 @@ -export const copyToClipboard = value => +export const copyToClipboard = (value: string) => globalThis.navigator.clipboard.writeText(value); -export const sanitizeHTML = content => { +export const sanitizeHTML = (content: string) => { try { const parsed = new DOMParser().parseFromString(content, 'text/html'); diff --git a/packages/react/package.json b/packages/react/package.json index 570cdc704..23045dec0 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -8,7 +8,7 @@ "esnext": "src/index.js", "unpkg": "dist/oak-react.min.js", "cdn": "dist/oak-react.min.js", - "types": "dist/oak-react.d.ts", + "types": "dist/types/index.d.ts", "repository": { "type": "git", "url": "https://github.com/p3ol/oak.git", @@ -21,25 +21,24 @@ "license": "MIT", "sideEffects": false, "engines": { - "node": ">= 16", - "npm": ">= 8" + "node": ">= 18" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" }, "dependencies": { - "@babel/runtime-corejs3": "7.24.6", "@floating-ui/react": "0.26.16", - "@junipero/hooks": "3.4.11", - "@junipero/react": "3.4.14", - "@junipero/transitions": "3.4.11", - "core-js": "3.37.1", + "@junipero/hooks": "3.5.0", + "@junipero/react": "3.5.3", + "@junipero/transitions": "3.5.3", "uuid": "9.0.1" }, "scripts": { "clean": "rm -rf ./dist || true", - "build": "yarn clean && rollup -c", + "build": "yarn clean && yarn build:code && yarn build:dts", + "build:code": "yarn run -T rollup --configPlugin @rollup/plugin-swc -c", + "build:dts": "yarn run -T tsc --project ./tsconfig.build.json", "test": "jest" }, "publishConfig": { @@ -48,15 +47,18 @@ "exports": { ".": { "import": "./dist/esm/index.js", - "require": "./dist/oak-react.cjs.js" + "require": "./dist/oak-react.cjs.js", + "types": "./dist/types/index.d.ts" }, "./addons": { "import": "./dist/esm/addons.js", - "require": "./dist/oak-react.cjs.js" + "require": "./dist/oak-react.cjs.js", + "types": "./dist/types/addons.d.ts" }, "./options": { "import": "./dist/esm/options.js", - "require": "./dist/oak-react.cjs.js" + "require": "./dist/oak-react.cjs.js", + "types": "./dist/types/options.d.ts" } } } diff --git a/packages/react/rollup.config.mjs b/packages/react/rollup.config.ts similarity index 65% rename from packages/react/rollup.config.mjs rename to packages/react/rollup.config.ts index f06574e6d..6737628e3 100644 --- a/packages/react/rollup.config.mjs +++ b/packages/react/rollup.config.ts @@ -1,16 +1,16 @@ -import path from 'path'; +import path from 'node:path'; -import babel from '@rollup/plugin-babel'; +import type { ModuleFormat, Plugin } from 'rollup'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import terser from '@rollup/plugin-terser'; -import dts from 'rollup-plugin-dts'; import alias from '@rollup/plugin-alias'; +import swc from '@rollup/plugin-swc'; -const input = './lib/index.js'; +const input = './lib/index.ts'; const defaultOutput = './dist'; const name = 'oak-react'; -const formats = ['umd', 'cjs', 'esm']; +const formats: ModuleFormat[] = ['umd', 'cjs', 'esm']; const defaultExternals = [ 'react', 'react-dom', @@ -20,27 +20,42 @@ const defaultGlobals = { 'react-dom': 'ReactDOM', }; -const defaultPlugins = [ - babel({ - exclude: /node_modules\/(?!@oakjs)/, - babelHelpers: 'runtime', +const defaultPlugins: Plugin[] = [ + swc({ + swc: { + jsc: { + transform: { + react: { + runtime: 'automatic', + }, + }, + parser: { + syntax: 'typescript', + tsx: true, + }, + }, + }, }), alias({ entries: [ { find: '@oakjs/core', replacement: path.resolve('../core/lib') }, ], }), + commonjs({ include: /node_modules/ }), resolve({ - rootDir: path.resolve('../../'), + extensions: ['.js', '.ts', '.tsx', '.json', '.node'], }), - commonjs(), terser(), ]; -const getConfig = (format, { +const getConfig = (format: string, { output = defaultOutput, globals = defaultGlobals, external = defaultExternals, +}: { + output?: string; + globals?: Record; + external?: string[]; } = {}) => ({ input, plugins: [ @@ -59,7 +74,7 @@ const getConfig = (format, { sourcemap: true, globals, ...(format === 'esm' ? { - manualChunks: id => { + manualChunks: (id: string) => { if (/packages\/core\/lib\/(\w+)\/index.js/.test(id)) { return path.parse(id).dir.split('/').pop(); } else { @@ -74,18 +89,13 @@ export default [ ...formats.map(f => getConfig(f)), getConfig('esm', { output: './dist/no-junipero', - defaultExternals: [ + external: [ ...defaultExternals, '@junipero/react', ], - defaultGlobals: { + globals: { ...defaultGlobals, '@junipero/react': 'JuniperoReact', }, }), - { - input: './lib/index.d.ts', - output: [{ file: `dist/${name}.d.ts`, format: 'es' }], - plugins: [dts()], - }, ]; diff --git a/packages/react/tests/utils.js b/packages/react/tests/utils.tsx similarity index 85% rename from packages/react/tests/utils.js rename to packages/react/tests/utils.tsx index 60ab7fe11..6f2f348f3 100644 --- a/packages/react/tests/utils.js +++ b/packages/react/tests/utils.tsx @@ -1,10 +1,14 @@ import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; -import Element from '../lib/Element'; +import type { BuilderProps, BuilderRef } from '../lib/Builder'; import { BuilderContext } from '../lib/contexts'; import { useRootBuilder } from '../lib/hooks'; +import Element from '../lib/Element'; -export const BuilderLite = forwardRef(({ children, ...opts }, ref) => { +export const BuilderLite = forwardRef(({ + children, + ...opts +}, ref) => { const innerRef = useRef(); const { content, builder } = useRootBuilder(opts); diff --git a/packages/react/tsconfig.build.json b/packages/react/tsconfig.build.json new file mode 100644 index 000000000..dd7c4a73a --- /dev/null +++ b/packages/react/tsconfig.build.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "noEmit": false, + "emitDeclarationOnly": true, + "outDir": "dist/types", + "baseUrl": "." + }, + "exclude": [ + "tests", + "**/*.test.ts", + "**/*.test.tsx", + "rollup.config.ts", + "**/*.stories.tsx", + ] +} diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json new file mode 100644 index 000000000..9a11edc9a --- /dev/null +++ b/packages/react/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./lib/**/*"], +} diff --git a/packages/strapi-plugin/package.json b/packages/strapi-plugin/package.json index 0f538e16d..2e6085258 100644 --- a/packages/strapi-plugin/package.json +++ b/packages/strapi-plugin/package.json @@ -10,8 +10,7 @@ "author": "Ugo Stephant ", "license": "MIT", "engines": { - "node": ">= 16", - "npm": ">= 8" + "node": ">= 18" }, "peerDependencies": { "@strapi/strapi": "^4.4.0", @@ -22,7 +21,7 @@ "build": "true" }, "dependencies": { - "@ckeditor/ckeditor5-react": "6.2.0", + "@ckeditor/ckeditor5-react": "7.0.0", "@oakjs/addon-ckeditor5-react": "^3.5.5", "@oakjs/addon-remirror": "^3.5.5", "@oakjs/ckeditor5-build-custom": "^3.5.5", @@ -30,7 +29,8 @@ "@oakjs/theme": "^3.5.6", "@remirror/pm": "2.0.8", "@remirror/react": "2.0.35", - "@strapi/design-system": "1.14.1", + "@strapi/design-system": "1.19.0", + "@strapi/icons": "1.19.0", "remirror": "2.0.39", "styled-components": "6.1.11" }, diff --git a/packages/theme/lib/Editable.sass b/packages/theme/lib/Editable.sass index a9faf4408..7c1f72a6b 100644 --- a/packages/theme/lib/Editable.sass +++ b/packages/theme/lib/Editable.sass @@ -34,6 +34,9 @@ width: 100% min-width: 0 + .junipero.dropdown-menu + z-index: 30 + .sub-fields .field-label font-size: 13px diff --git a/packages/theme/lib/reset.sass b/packages/theme/lib/reset.sass index b88b2f179..dcafa31a4 100644 --- a/packages/theme/lib/reset.sass +++ b/packages/theme/lib/reset.sass @@ -1,4 +1,4 @@ -@import "./utils/tailwind" +@import "tailwindcss/utilities" * --oak-main-color: var(--junipero-velvet) diff --git a/packages/theme/lib/utils/tailwind.css b/packages/theme/lib/utils/tailwind.css deleted file mode 100644 index 65dd5f63a..000000000 --- a/packages/theme/lib/utils/tailwind.css +++ /dev/null @@ -1 +0,0 @@ -@tailwind utilities; diff --git a/packages/theme/package.json b/packages/theme/package.json index fd324365d..3112e84cd 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -13,15 +13,15 @@ "license": "MIT", "sideEffects": false, "engines": { - "node": ">= 16", - "npm": ">= 8" + "node": ">= 18" }, "dependencies": { - "@junipero/theme": "3.4.15" + "@junipero/theme": "3.5.0" }, "scripts": { "clean": "rm -rf ./dist || true", - "build": "npm run clean && node script/build.js" + "build": "yarn clean && yarn build:code", + "build:code": "node script/build.js" }, "publishConfig": { "access": "public" diff --git a/packages/theme/script/build.js b/packages/theme/script/build.js index 68f67181e..0ab54ec4b 100644 --- a/packages/theme/script/build.js +++ b/packages/theme/script/build.js @@ -27,7 +27,7 @@ const compile = async ({ input, output }) => { const { css: prefixedCss } = await postcss([ tailwindcss({ - config: path.resolve(__dirname, '../tailwind.config.js'), + config: path.resolve(__dirname, '../tailwind.config.ts'), }), autoprefixer(), ]).process(css, { diff --git a/packages/theme/tailwind.config.js b/packages/theme/tailwind.config.js deleted file mode 100644 index b3a18f691..000000000 --- a/packages/theme/tailwind.config.js +++ /dev/null @@ -1,9 +0,0 @@ -const rootConfig = require('../../tailwind.config.js'); - -module.exports = { - ...rootConfig, - content: [ - '../react/**/*.js', - '../addon-*/**/*.js', - ], -}; diff --git a/packages/theme/tailwind.config.ts b/packages/theme/tailwind.config.ts new file mode 100644 index 000000000..de4e0f545 --- /dev/null +++ b/packages/theme/tailwind.config.ts @@ -0,0 +1,13 @@ +import type { Config } from 'tailwindcss/types/config'; + +import rootConfig from '../../tailwind.config'; + +const config: Config = { + ...rootConfig, + content: [ + '../react/**/*.{js,ts,tsx}', + '../addon-*/**/*.{js,ts,tsx}', + ], +}; + +export default config; diff --git a/scripts/check-package-manager.js b/scripts/check-package-manager.js deleted file mode 100644 index 35386ebbe..000000000 --- a/scripts/check-package-manager.js +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable no-console,max-len */ - -/* - * Note: don't use any non-node-native dependency here as this script is - * executed before any of them is installed. - * - * source: https://github.com/ampproject/amphtml/blob/main/build-system/common/check-package-manager.js - */ - -const red = text => - '\x1b[31m' + text + '\x1b[0m'; - -const green = text => - '\x1b[32m' + text + '\x1b[0m'; - -const cyan = text => - '\x1b[36m' + text + '\x1b[0m'; - -const bold = text => - '\x1b[1m' + text + '\x1b[0m'; - -if (!process.env.npm_execpath?.includes('npm')) { - console.log('\n'); - console.log(red( - '*** Oak now uses npm to install dependencies, please use it ' + - 'instead of yarn ***' - )); - console.log('\n'); - console.log(cyan( - 'Yarn v1 has issues with many duplicate peer dependencies that ' + - 'won\'t be fixed (as it has reached end of life), \nand Yarn >= v2 is ' + - 'a change of mindset that requires too much efforts from everybody ' + - 'to be adopted painlessly \nand we are simply against.' - )); - console.log('\n'); - console.log('⤷ To install dependencies, run:', bold(green('npm i')), 'or', - bold(green('npm install'))); - console.log('⤷ To add a dependency to a particular workspace, run:', - bold(green('npm i -w '))); - console.log('⤷ To run a script, run:', bold(green('npm run