├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── stale.yml └── workflows │ └── release.yaml ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── UPGRADE.md ├── babel.config.js ├── config ├── jestTestSetup.js ├── postcss.colors.js ├── postcss.config.js └── tsconfig.test.json ├── docs-images ├── background_plugin.png ├── basic.png ├── image-plugin.gif ├── image_upload.png ├── json-example-1.png ├── json-example-2.png ├── quick-example.gif ├── react-example-app.png ├── spacer-plugin.gif ├── text-editing-plugin.gif └── video-plugin.gif ├── docs ├── .nojekyll ├── CONTRIBUTING.md ├── README.md ├── _sidebar.md ├── builtin_plugins.md ├── bundle-size.md ├── custom-cell-plugins.md ├── docs-images ├── editor.md ├── examples │ ├── pages │ ├── plugins │ └── slate-plugin-src ├── ie11.md ├── index.html ├── integration-react-admin.md ├── quick-start.md ├── recipes.md ├── server-side-rendering.md ├── slate.md └── utils.md ├── examples ├── .gitignore ├── README.md ├── components │ ├── CodeSnippet.tsx │ ├── ContactFormExample.tsx │ ├── ExampleCustomBottomToolbar │ │ ├── CollapseButton.tsx │ │ └── index.tsx │ ├── Navigation.tsx │ └── PageLayout.tsx ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── debug_no_content.tsx │ ├── empty.tsx │ ├── examples │ │ ├── bare.tsx │ │ ├── cellSpacing.tsx │ │ ├── conditionalForm.tsx │ │ ├── constraints.tsx │ │ ├── contactform.tsx │ │ ├── customEditorChildren.tsx │ │ ├── customListInSlate.tsx │ │ ├── customMissingPlugin.tsx │ │ ├── customToolbar.tsx │ │ ├── customformlayout.tsx │ │ ├── customuniformsexperiments.tsx │ │ ├── dark-editor.tsx │ │ ├── dark-full.tsx │ │ ├── decorateplugins.tsx │ │ ├── extractTextContents.tsx │ │ ├── formFieldInText.tsx │ │ ├── i18n.tsx │ │ ├── languageSwitch.tsx │ │ ├── multicontrols.tsx │ │ ├── multipleEditors.tsx │ │ ├── nestedPlugins.tsx │ │ ├── reactadmin.tsx │ │ ├── readonly.tsx │ │ ├── sidebarPosition.tsx │ │ ├── simple.tsx │ │ ├── stretchCells.tsx │ │ └── switchValueTest.tsx │ ├── index.tsx │ ├── old │ │ ├── demo.tsx │ │ └── fromhtml.tsx │ ├── readonly-bare-empty.tsx │ ├── readonly-bare.tsx │ └── readonly.tsx ├── plugins │ ├── cellPlugins.ts │ ├── codeSnippet.tsx │ ├── contactForm.tsx │ ├── customContentPlugin.tsx │ ├── customContentPluginTwitter.tsx │ ├── customContentPluginWithListField.tsx │ ├── customLayoutPlugin.tsx │ ├── customLayoutPluginWithCellSpacing.tsx │ ├── customLayoutPluginWithInitialState.tsx │ ├── customSlatePlugin.tsx │ ├── katexSlatePlugin.tsx │ ├── react-katex.d.ts │ └── slate.tsx ├── public │ ├── docs │ └── images │ │ ├── app-preview.mp4 │ │ ├── callisto-preview.mp4 │ │ ├── clarke-preview.mp4 │ │ ├── create-content.png │ │ ├── front.png │ │ ├── grass-header.jpg │ │ ├── khorana-preview.mp4 │ │ ├── layouts.png │ │ ├── mountain.jpg │ │ ├── react.png │ │ ├── responsive.png │ │ ├── sane-markup.png │ │ ├── sea-bg.jpg │ │ └── sites-demo.mp4 ├── sampleContents │ ├── cellSpacing.tsx │ ├── demo.tsx │ ├── demoSimpleReadOnly.tsx │ ├── raAboutUs.tsx │ └── v0.ts ├── styles │ ├── elements.css │ ├── styles.css │ └── typography.css ├── tsconfig.json └── utils │ └── createEmotionCache.ts ├── lerna.json ├── package.json ├── packages ├── editor │ ├── .DS_Store │ ├── .npmignore │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── src │ │ ├── core │ │ │ ├── EditorStore.ts │ │ │ ├── Provider │ │ │ │ ├── CallbacksProvider.tsx │ │ │ │ ├── DndProvider.tsx │ │ │ │ ├── EditorStoreProvider.tsx │ │ │ │ ├── OptionsProvider.tsx │ │ │ │ ├── RenderOptionsProvider.tsx │ │ │ │ └── index.tsx │ │ │ ├── actions │ │ │ │ ├── cell │ │ │ │ │ ├── core.ts │ │ │ │ │ ├── drag.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── insert.ts │ │ │ │ ├── display.ts │ │ │ │ ├── helpers.ts │ │ │ │ ├── setting.ts │ │ │ │ ├── undo.ts │ │ │ │ └── value.ts │ │ │ ├── components │ │ │ │ ├── BlurGate.tsx │ │ │ │ ├── Cell │ │ │ │ │ ├── CellErrorGate.tsx │ │ │ │ │ ├── Draggable │ │ │ │ │ │ ├── index.css │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── useDragHandle.tsx │ │ │ │ │ ├── Droppable │ │ │ │ │ │ ├── helper │ │ │ │ │ │ │ └── dnd.ts │ │ │ │ │ │ ├── index.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ErrorCell │ │ │ │ │ │ ├── index.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Handle │ │ │ │ │ │ ├── index.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Inner │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── InsertNew.tsx │ │ │ │ │ ├── MoveActions │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── NoopProvider.tsx │ │ │ │ │ ├── PluginComponent │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── PluginControls │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── PluginMissing.tsx │ │ │ │ │ ├── Rows │ │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ │ └── index.test.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── index.css │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── utils │ │ │ │ │ │ └── scrollIntoViewWithOffset.ts │ │ │ │ ├── Editable │ │ │ │ │ ├── FallbackDropArea.tsx │ │ │ │ │ ├── Inner │ │ │ │ │ │ ├── Rows.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── hooks │ │ │ │ │ │ └── useKeepScrollPosition.ts │ │ │ │ │ ├── index.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── HotKey │ │ │ │ │ └── GlobalHotKeys.tsx │ │ │ │ ├── Row │ │ │ │ │ ├── Droppable │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ResizableRowCell.tsx │ │ │ │ │ ├── index.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── nodeMove.test.tsx │ │ │ │ │ │ └── useDebouncedCellData.test.tsx │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── callbacks.ts │ │ │ │ │ ├── debug │ │ │ │ │ │ └── useWhyDidYouUpdate.ts │ │ │ │ │ ├── display.ts │ │ │ │ │ ├── displayMode.ts │ │ │ │ │ ├── dragDropActions.ts │ │ │ │ │ ├── focus.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── node.ts │ │ │ │ │ ├── nodeActions.ts │ │ │ │ │ ├── nodeMove.ts │ │ │ │ │ ├── options.ts │ │ │ │ │ ├── renderOptions.ts │ │ │ │ │ ├── screen.tsx │ │ │ │ │ ├── utils │ │ │ │ │ │ ├── findSiblingRow.test.ts │ │ │ │ │ │ └── findSiblingRow.ts │ │ │ │ │ └── value.ts │ │ │ │ └── index.css │ │ │ ├── const.ts │ │ │ ├── defaultOptions.ts │ │ │ ├── grid.css │ │ │ ├── helper │ │ │ │ ├── lazyLoad │ │ │ │ │ └── index.tsx │ │ │ │ └── throttle │ │ │ │ │ └── index.ts │ │ │ ├── index.css │ │ │ ├── migrations │ │ │ │ ├── EDITABLE_MIGRATIONS │ │ │ │ │ ├── from0to1.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── Migration.ts │ │ │ │ ├── __tests__ │ │ │ │ │ ├── migrate.test.ts │ │ │ │ │ └── migrateValue.test.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── serialzeValue.ts │ │ │ ├── reducer │ │ │ │ ├── display │ │ │ │ │ └── index.ts │ │ │ │ ├── focus │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ └── index.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── hover │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── settings │ │ │ │ │ └── index.ts │ │ │ │ ├── value │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── initialReduced.test.ts │ │ │ │ │ │ ├── removeCell.test.ts │ │ │ │ │ │ ├── resizeCell.test.ts │ │ │ │ │ │ └── updateCellContent.test.ts │ │ │ │ │ ├── helper │ │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ │ ├── optimize.test.ts │ │ │ │ │ │ │ └── sizing.test.ts │ │ │ │ │ │ ├── empty.ts │ │ │ │ │ │ ├── optimize.ts │ │ │ │ │ │ ├── setAllSizesAndOptimize.ts │ │ │ │ │ │ └── sizing.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── testUtils.ts │ │ │ │ │ └── tree.ts │ │ │ │ └── values │ │ │ │ │ └── index.ts │ │ │ ├── reduxConnect.tsx │ │ │ ├── selector │ │ │ │ ├── display │ │ │ │ │ └── index.ts │ │ │ │ ├── editable │ │ │ │ │ └── index.ts │ │ │ │ ├── focus.ts │ │ │ │ └── setting.ts │ │ │ ├── service │ │ │ │ ├── hover │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ └── index.test.ts │ │ │ │ │ ├── computeHover.ts │ │ │ │ │ └── input.ts │ │ │ │ └── logger │ │ │ │ │ └── index.ts │ │ │ ├── store.ts │ │ │ ├── types │ │ │ │ ├── callbacks.ts │ │ │ │ ├── components.ts │ │ │ │ ├── constraints.ts │ │ │ │ ├── display.ts │ │ │ │ ├── hover.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jsonSchema.ts │ │ │ │ ├── node.ts │ │ │ │ ├── options.ts │ │ │ │ ├── plugins.ts │ │ │ │ ├── renderOptions.ts │ │ │ │ └── state.ts │ │ │ └── utils │ │ │ │ ├── ancestorTree.ts │ │ │ │ ├── cloneWithNewIds.ts │ │ │ │ ├── createId.ts │ │ │ │ ├── createValue.ts │ │ │ │ ├── deepEquals.ts │ │ │ │ ├── getAvailablePlugins.ts │ │ │ │ ├── getCellData.ts │ │ │ │ ├── getCellSpacing.ts │ │ │ │ ├── getCellStylingProps.ts │ │ │ │ ├── getDropLevels.ts │ │ │ │ ├── getTextContents.test.ts │ │ │ │ ├── getTextContents.ts │ │ │ │ ├── mapNode.test.ts │ │ │ │ ├── mapNode.ts │ │ │ │ ├── objIsNode.ts │ │ │ │ ├── removeUndefinedProps.test.ts │ │ │ │ └── removeUndefinedProps.ts │ │ ├── editor │ │ │ ├── EditableEditor.tsx │ │ │ ├── Editor.tsx │ │ │ └── StickyWrapper.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── renderer │ │ │ ├── HTMLRenderer.tsx │ │ │ └── __tests__ │ │ │ │ └── index.test.tsx │ │ ├── types.d.ts │ │ ├── ui │ │ │ ├── AutoformControls │ │ │ │ ├── AutoField.tsx │ │ │ │ ├── AutoFieldContext.tsx │ │ │ │ ├── AutoFields.tsx │ │ │ │ ├── AutoForm.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── makeUniformsSchema.ts │ │ │ ├── BottomToolbar │ │ │ │ ├── Drawer.tsx │ │ │ │ ├── MoveActions.tsx │ │ │ │ ├── NodeTools.tsx │ │ │ │ ├── ScaleButton.tsx │ │ │ │ ├── Tools.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── types.ts │ │ │ ├── ColorPicker │ │ │ │ ├── ColorPicker.tsx │ │ │ │ ├── ColorPickerField.tsx │ │ │ │ ├── colorToString.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── types.ts │ │ │ ├── DraftSwitch │ │ │ │ └── index.tsx │ │ │ ├── DuplicateButton │ │ │ │ └── index.tsx │ │ │ ├── EditorUI │ │ │ │ └── index.tsx │ │ │ ├── I18nTools │ │ │ │ ├── I18nDialog.tsx │ │ │ │ ├── SelectLang.tsx │ │ │ │ └── index.tsx │ │ │ ├── ImageUpload │ │ │ │ ├── ImageUpload.tsx │ │ │ │ ├── defaultTranslations.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── types.tsx │ │ │ ├── MultiNodesBottomToolbar │ │ │ │ ├── DeleteAll.tsx │ │ │ │ ├── DuplicateAll.tsx │ │ │ │ └── index.tsx │ │ │ ├── PluginDrawer │ │ │ │ ├── Draggable │ │ │ │ │ └── index.tsx │ │ │ │ ├── Item │ │ │ │ │ ├── index.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── SelectParentButton │ │ │ │ └── index.tsx │ │ │ ├── Sidebar │ │ │ │ ├── Button │ │ │ │ │ ├── index.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── ToggleEdit │ │ │ │ │ └── index.tsx │ │ │ │ ├── ToggleInsert │ │ │ │ │ └── index.tsx │ │ │ │ ├── ToggleLayout │ │ │ │ │ └── index.tsx │ │ │ │ ├── TogglePreview │ │ │ │ │ └── index.tsx │ │ │ │ ├── ToggleResize │ │ │ │ │ └── index.tsx │ │ │ │ ├── UndoRedo │ │ │ │ │ └── index.tsx │ │ │ │ ├── Zoom │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── Trash │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── defaultTheme │ │ │ │ └── index.ts │ │ │ ├── index.css │ │ │ ├── index.tsx │ │ │ ├── moveButtons │ │ │ │ └── index.tsx │ │ │ └── uniform-mui │ │ │ │ ├── AutoField.tsx │ │ │ │ ├── AutoFields.tsx │ │ │ │ ├── BoolField.tsx │ │ │ │ ├── DateField.tsx │ │ │ │ ├── ErrorField.tsx │ │ │ │ ├── ErrorsField.tsx │ │ │ │ ├── HiddenField.tsx │ │ │ │ ├── ListAddField.tsx │ │ │ │ ├── ListDelField.tsx │ │ │ │ ├── ListField.tsx │ │ │ │ ├── ListItemField.tsx │ │ │ │ ├── ListSortField.tsx │ │ │ │ ├── LongTextField.tsx │ │ │ │ ├── NestField.tsx │ │ │ │ ├── NumField.tsx │ │ │ │ ├── README.md │ │ │ │ ├── RadioField.tsx │ │ │ │ ├── SelectField.tsx │ │ │ │ ├── SubmitField.tsx │ │ │ │ ├── TextField.tsx │ │ │ │ ├── index.ts │ │ │ │ └── wrapField.tsx │ │ ├── variables.css │ │ └── wdyr.ts │ ├── tsconfig-es.json │ └── tsconfig.json ├── index.d.ts ├── plugins │ ├── README.md │ ├── content │ │ ├── divider │ │ │ ├── .npmignore │ │ │ ├── babel.config.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── Renderer │ │ │ │ │ └── DividerHtmlRenderer.tsx │ │ │ │ ├── createPlugin.tsx │ │ │ │ ├── default │ │ │ │ │ └── settings.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.ts │ │ │ │ └── types │ │ │ │ │ ├── settings.ts │ │ │ │ │ └── translations.ts │ │ │ ├── tsconfig-es.json │ │ │ └── tsconfig.json │ │ ├── html5-video │ │ │ ├── .npmignore │ │ │ ├── babel.config.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── Renderer │ │ │ │ │ └── Html5VideoHtmlRenderer.tsx │ │ │ │ ├── createPlugin.tsx │ │ │ │ ├── default │ │ │ │ │ ├── settings.tsx │ │ │ │ │ └── state.ts │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ └── types │ │ │ │ │ ├── settings.ts │ │ │ │ │ ├── state.ts │ │ │ │ │ └── translations.ts │ │ │ ├── tsconfig-es.json │ │ │ └── tsconfig.json │ │ ├── image │ │ │ ├── .npmignore │ │ │ ├── babel.config.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── Controls │ │ │ │ │ └── ImageControls.tsx │ │ │ │ ├── Renderer │ │ │ │ │ └── ImageHtmlRenderer.tsx │ │ │ │ ├── common │ │ │ │ │ └── styles.ts │ │ │ │ ├── createPlugin.tsx │ │ │ │ ├── default │ │ │ │ │ └── settings.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ └── types │ │ │ │ │ ├── controls.ts │ │ │ │ │ ├── settings.ts │ │ │ │ │ ├── state.ts │ │ │ │ │ └── translations.ts │ │ │ ├── tsconfig-es.json │ │ │ └── tsconfig.json │ │ ├── slate │ │ │ ├── .npmignore │ │ │ ├── babel.config.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── __tests__ │ │ │ │ │ ├── getTextContents.test.ts │ │ │ │ │ └── htmlToSlate.test.ts │ │ │ │ ├── components │ │ │ │ │ ├── ConditionalWrapper.tsx │ │ │ │ │ ├── Controls.tsx │ │ │ │ │ ├── DialogVisibleProvider.tsx │ │ │ │ │ ├── HoverButtons.tsx │ │ │ │ │ ├── PluginButton.tsx │ │ │ │ │ ├── PluginControls.tsx │ │ │ │ │ ├── ReadOnlySlate.tsx │ │ │ │ │ ├── SlateEditor.tsx │ │ │ │ │ ├── SlateProvider.tsx │ │ │ │ │ ├── ToolbarButton.tsx │ │ │ │ │ ├── VoidEditableElement.tsx │ │ │ │ │ ├── hotkeyHooks.ts │ │ │ │ │ ├── pluginHooks.ts │ │ │ │ │ └── renderHooks.tsx │ │ │ │ ├── default │ │ │ │ │ └── settings.ts │ │ │ │ ├── hooks │ │ │ │ │ ├── useAddPlugin.ts │ │ │ │ │ ├── useCurrentNodeDataWithPlugin.ts │ │ │ │ │ ├── useCurrentNodeWithPlugin.ts │ │ │ │ │ ├── useCurrentSelection.ts │ │ │ │ │ ├── usePluginIsActive.ts │ │ │ │ │ ├── usePluginIsDisabled.ts │ │ │ │ │ ├── useRemovePlugin.ts │ │ │ │ │ ├── useTextIsSelected.ts │ │ │ │ │ └── useWhyDidYouUpdate.ts │ │ │ │ ├── htmlToSlate │ │ │ │ │ ├── HtmlToSlate.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── parseHtml.browser.ts │ │ │ │ │ └── parseHtml.ts │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ ├── migrations │ │ │ │ │ ├── deep-rename-keys.d.ts │ │ │ │ │ ├── v002.ts │ │ │ │ │ ├── v003.ts │ │ │ │ │ └── v004.ts │ │ │ │ ├── none.tsx │ │ │ │ ├── pluginFactories │ │ │ │ │ ├── components │ │ │ │ │ │ └── UniformsControls.tsx │ │ │ │ │ ├── createComponentPlugin.tsx │ │ │ │ │ ├── createDataPlugin.tsx │ │ │ │ │ ├── createHeadingsPlugin.tsx │ │ │ │ │ ├── createListIndentionPlugin.tsx │ │ │ │ │ ├── createListItemPlugin.tsx │ │ │ │ │ ├── createListPlugin.tsx │ │ │ │ │ ├── createMarkPlugin.tsx │ │ │ │ │ ├── createSimpleHtmlBlockPlugin.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils │ │ │ │ │ │ └── listUtils.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── alignment.tsx │ │ │ │ │ ├── code │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── emphasize │ │ │ │ │ │ ├── em.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── strong.tsx │ │ │ │ │ │ └── underline.tsx │ │ │ │ │ ├── headings │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── links │ │ │ │ │ │ ├── anchor.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── link.tsx │ │ │ │ │ ├── lists │ │ │ │ │ │ ├── constants.ts │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── paragraphs │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── node.css │ │ │ │ │ └── quotes.tsx │ │ │ │ ├── slateEnhancer │ │ │ │ │ ├── withInline.ts │ │ │ │ │ └── withPaste.ts │ │ │ │ ├── slateTypes.d.ts │ │ │ │ ├── types.ts │ │ │ │ ├── types │ │ │ │ │ ├── SlatePlugin.ts │ │ │ │ │ ├── component.ts │ │ │ │ │ ├── initialSlateState.ts │ │ │ │ │ ├── slatePluginDefinitions.ts │ │ │ │ │ ├── state.ts │ │ │ │ │ └── translations.ts │ │ │ │ └── utils │ │ │ │ │ ├── flattenDeep.ts │ │ │ │ │ ├── getCurrentData.ts │ │ │ │ │ ├── getTextContent.ts │ │ │ │ │ ├── makeSlatePluginsFromDef.ts │ │ │ │ │ ├── transformInitialSlateState.ts │ │ │ │ │ └── useSafeSetState.ts │ │ │ ├── tsconfig-es.json │ │ │ └── tsconfig.json │ │ ├── spacer │ │ │ ├── .npmignore │ │ │ ├── babel.config.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── Renderer │ │ │ │ │ ├── SpacerHtmlRenderer.tsx │ │ │ │ │ └── SpacerResizable.tsx │ │ │ │ ├── createPlugin.tsx │ │ │ │ ├── default │ │ │ │ │ └── settings.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.tsx │ │ │ │ └── types │ │ │ │ │ ├── settings.ts │ │ │ │ │ ├── state.ts │ │ │ │ │ └── translations.ts │ │ │ ├── tsconfig-es.json │ │ │ └── tsconfig.json │ │ └── video │ │ │ ├── .npmignore │ │ │ ├── babel.config.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ ├── Renderer │ │ │ │ ├── VideoHtmlRenderer.tsx │ │ │ │ └── index.css │ │ │ ├── common │ │ │ │ └── styles.ts │ │ │ ├── createPlugin.tsx │ │ │ ├── default │ │ │ │ ├── settings.tsx │ │ │ │ └── state.ts │ │ │ ├── index.css │ │ │ ├── index.tsx │ │ │ └── types │ │ │ │ ├── api.ts │ │ │ │ ├── component.ts │ │ │ │ ├── controls.ts │ │ │ │ ├── renderer.ts │ │ │ │ ├── settings.ts │ │ │ │ ├── state.ts │ │ │ │ └── translations.ts │ │ │ ├── tsconfig-es.json │ │ │ └── tsconfig.json │ └── layout │ │ └── background │ │ ├── .npmignore │ │ ├── babel.config.js │ │ ├── package.json │ │ ├── src │ │ ├── Controls │ │ │ ├── Controls.tsx │ │ │ ├── Inner.tsx │ │ │ └── sub │ │ │ │ ├── Color.tsx │ │ │ │ ├── Image.tsx │ │ │ │ └── LinearGradient.tsx │ │ ├── Renderer │ │ │ └── BackgroundHtmlRenderer.tsx │ │ ├── const │ │ │ └── mode.ts │ │ ├── createPlugin.tsx │ │ ├── default │ │ │ └── settings.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ └── types │ │ │ ├── ModeEnum.ts │ │ │ ├── api.ts │ │ │ ├── component.ts │ │ │ ├── controls.ts │ │ │ ├── gradient.ts │ │ │ ├── makeOptional.ts │ │ │ ├── omit.ts │ │ │ ├── renderer.ts │ │ │ ├── settings.ts │ │ │ ├── state.ts │ │ │ └── translations.ts │ │ ├── tsconfig-es.json │ │ └── tsconfig.json ├── react-admin │ ├── .npmignore │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── src │ │ ├── RaReactPageInput.tsx │ │ ├── RaSelectReferenceInputField.tsx │ │ └── index.tsx │ ├── tsconfig-es.json │ └── tsconfig.json ├── tsconfig.json └── tsconfig.settings.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | _book/** 2 | config/** 3 | coverage/** 4 | examples/build/** 5 | examples/src/contents.ts 6 | **/lib/** 7 | **/lib-es/** 8 | node_modules -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | --- 7 | 8 | **Describe the bug** 9 | A clear and concise description of what the bug is. 10 | 11 | **To Reproduce** 12 | Steps to reproduce the behavior: 13 | 1. Go to '...' 14 | 2. Click on '....' 15 | 3. Scroll down to '....' 16 | 4. See error 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Desktop (please complete the following information):** 25 | - OS: [e.g. iOS] 26 | - Browser [e.g. chrome, safari] 27 | - Version [e.g. 22] 28 | 29 | **Smartphone (please complete the following information):** 30 | - Device: [e.g. iPhone6] 31 | - OS: [e.g. iOS8.1] 32 | - Browser [e.g. stock browser, safari] 33 | - Version [e.g. 22] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | --- 7 | 8 | **Is your feature request related to a problem? Please describe.** 9 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 10 | 11 | **Describe the solution you'd like** 12 | A clear and concise description of what you want to happen. 13 | 14 | **Describe alternatives you've considered** 15 | A clear and concise description of any alternative solutions or features you've considered. 16 | 17 | **Additional context** 18 | Add any other context or screenshots about the feature request here. 19 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity 😏. It will be closed if no further activity occurs. Thank you 15 | for your contributions! ❤️ 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Use Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '14.x' 21 | cache: 'yarn' 22 | - run: yarn --frozen-lockfile 23 | - run: yarn build:lib || yarn build:lib 24 | #- run: yarn run lint 25 | #- run: yarn run test 26 | - name: semantic release 27 | env: 28 | GH_TOKEN_DOCS: ${{ secrets.GH_TOKEN_DOCS }} 29 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | run: 'echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc && yarn run semantic-release' 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | logs 4 | *.log 5 | *.log.* 6 | node_modules/ 7 | doc/ 8 | _book/ 9 | coverage/ 10 | lib/ 11 | lib-es/ 12 | dist/ 13 | examples/build 14 | *.tsbuildinfo 15 | .DS_store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2019 Aeneas Rekkas 4 | Copyright (c) 2020 react-page 5 | 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-react'], 3 | plugins: [ 4 | '@babel/plugin-transform-modules-commonjs', 5 | '@babel/plugin-proposal-class-properties', 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /config/jestTestSetup.js: -------------------------------------------------------------------------------- 1 | const enzyme = require('enzyme'); 2 | const EnzymeAdapter = require('enzyme-adapter-react-16'); 3 | const enableHooks = require('jest-react-hooks-shallow').default; 4 | 5 | import React from 'react'; 6 | React.useLayoutEffect = React.useEffect; 7 | 8 | enzyme.configure({ adapter: new EnzymeAdapter() }); 9 | enableHooks(jest); 10 | -------------------------------------------------------------------------------- /config/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (ctx) => ({ 2 | map: ctx.options.map, 3 | parser: ctx.options.parser, 4 | plugins: [ 5 | require('postcss-nested'), 6 | require('postcss-import')({ root: ctx.file.dirname }), 7 | 8 | require('postcss-preset-env')({ 9 | stage: 0, 10 | }), 11 | ], 12 | }); 13 | -------------------------------------------------------------------------------- /config/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDirs": ["src"], 4 | "outDir": "lib", 5 | 6 | "moduleResolution": "node", 7 | "downlevelIteration": true, 8 | "target": "es5", 9 | "module": "commonjs", 10 | "allowJs": true, 11 | "jsx": "react", 12 | "experimentalDecorators": true, 13 | "esModuleInterop": true, 14 | "sourceMap": true, 15 | "skipLibCheck": true, 16 | "skipDefaultLibCheck": true, 17 | "noUnusedLocals": false, 18 | "declaration": true, 19 | "declarationMap": true, 20 | "composite": true, 21 | "lib": ["es7", "dom"], 22 | "types": [ 23 | "node", 24 | "jest", 25 | "../packages/plugins/content/slate/src/slateTypes" 26 | ], 27 | "paths": { 28 | "react": ["./node_modules/@types/react/index"], 29 | "slate": ["./node_modules/@types/slate/index"] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs-images/background_plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs-images/background_plugin.png -------------------------------------------------------------------------------- /docs-images/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs-images/basic.png -------------------------------------------------------------------------------- /docs-images/image-plugin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs-images/image-plugin.gif -------------------------------------------------------------------------------- /docs-images/image_upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs-images/image_upload.png -------------------------------------------------------------------------------- /docs-images/json-example-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs-images/json-example-1.png -------------------------------------------------------------------------------- /docs-images/json-example-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs-images/json-example-2.png -------------------------------------------------------------------------------- /docs-images/quick-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs-images/quick-example.gif -------------------------------------------------------------------------------- /docs-images/react-example-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs-images/react-example-app.png -------------------------------------------------------------------------------- /docs-images/spacer-plugin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs-images/spacer-plugin.gif -------------------------------------------------------------------------------- /docs-images/text-editing-plugin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs-images/text-editing-plugin.gif -------------------------------------------------------------------------------- /docs-images/video-plugin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs-images/video-plugin.gif -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/docs/.nojekyll -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ../CONTRIBUTING.md -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | <!-- docs/_sidebar.md --> 2 | 3 | - [Readme](/) 4 | - [Getting started](/quick-start.md) 5 | - [Editor component](/editor.md) 6 | - [Rich text editing](/slate.md) 7 | - [Inbuilt Cell Plugins](/builtin_plugins.md) 8 | - [Custom Cell plugins](/custom-cell-plugins.md) 9 | - [Utils](/utils.md) 10 | - [Recipes](/recipes.md) 11 | - Integrations 12 | - [React Admin](/integration-react-admin.md) 13 | - FAQ 14 | - [SSR](/server-side-rendering.md) 15 | - [Bundle size](/bundle-size.md) 16 | - [IE11](/ie11.md) 17 | - [CONTRIBUTING](/CONTRIBUTING.md) 18 | -------------------------------------------------------------------------------- /docs/bundle-size.md: -------------------------------------------------------------------------------- 1 | We try to keep the initial bundle size low so that you can use this library also to render the content statically without edit functionality. 2 | 3 | We achieve that by lazy-loading using `import()` functions. Most modern bundlers like webpack (e.g. in nextjs) support this kind of lazy loading. So the default editor-ui (based on material-ui) is only loaded if the editor is in editMode. 4 | -------------------------------------------------------------------------------- /docs/docs-images: -------------------------------------------------------------------------------- 1 | ../docs-images -------------------------------------------------------------------------------- /docs/examples/pages: -------------------------------------------------------------------------------- 1 | ../../examples/pages -------------------------------------------------------------------------------- /docs/examples/plugins: -------------------------------------------------------------------------------- 1 | ../../examples/plugins -------------------------------------------------------------------------------- /docs/examples/slate-plugin-src: -------------------------------------------------------------------------------- 1 | ../../packages/plugins/content/slate/src -------------------------------------------------------------------------------- /docs/server-side-rendering.md: -------------------------------------------------------------------------------- 1 | # Server side rendering 2 | 3 | SSR should work out-of-the-box! 4 | 5 | In fact the demo page is using nextjs and does server side rendering -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /examples/components/CodeSnippet.tsx: -------------------------------------------------------------------------------- 1 | // lazy load this file to keep initial bundle small 2 | 3 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 4 | import { vscDarkPlus as style } from 'react-syntax-highlighter/dist/cjs/styles/prism'; 5 | import React from 'react'; 6 | 7 | const CodeSnippet: React.FC<{ 8 | code: string; 9 | language: string; 10 | }> = ({ code, language }) => ( 11 | <SyntaxHighlighter wrapLongLines language={language} style={style}> 12 | {code} 13 | </SyntaxHighlighter> 14 | ); 15 | 16 | export default CodeSnippet; 17 | -------------------------------------------------------------------------------- /examples/components/ExampleCustomBottomToolbar/CollapseButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconButton, Tooltip } from '@mui/material'; 3 | import IconCollapse from '@mui/icons-material/KeyboardArrowDown'; 4 | import IconRestore from '@mui/icons-material/KeyboardArrowUp'; 5 | 6 | interface CollapseButtonProps { 7 | collapsed: boolean; 8 | setCollapsed: (c: boolean) => void; 9 | } 10 | 11 | const CollapseButton: React.FC<CollapseButtonProps> = ({ 12 | collapsed, 13 | setCollapsed, 14 | }) => { 15 | const toggleCollapsed = React.useCallback(() => { 16 | setCollapsed(!collapsed); 17 | }, [collapsed, setCollapsed]); 18 | return ( 19 | <Tooltip title={collapsed ? 'Restore Panel' : 'Collapse Panel'}> 20 | <IconButton onClick={toggleCollapsed} aria-label="delete" color="default"> 21 | {collapsed ? <IconRestore /> : <IconCollapse />} 22 | </IconButton> 23 | </Tooltip> 24 | ); 25 | }; 26 | 27 | export default React.memo(CollapseButton); 28 | -------------------------------------------------------------------------------- /examples/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="next" /> 2 | /// <reference types="next/image-types/global" /> 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: process.env.ANALYZE === 'true', 3 | }); 4 | const path = require('path'); 5 | module.exports = withBundleAnalyzer({ 6 | basePath: process.env.RELEASE_CHANNEL 7 | ? !process.env.RELEASE_CHANNEL || process.env.RELEASE_CHANNEL === 'latest' 8 | ? '/' 9 | : '/' + process.env.RELEASE_CHANNEL 10 | : undefined, 11 | async rewrites() { 12 | return [ 13 | { 14 | source: '/docs', 15 | destination: '/docs/index.html', 16 | }, 17 | ]; 18 | }, 19 | productionBrowserSourceMaps: true, 20 | compiler: { 21 | // ssr and displayName are configured by default 22 | styledComponents: true, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /examples/pages/debug_no_content.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageLayout from '../components/PageLayout'; 3 | 4 | export default function Empty() { 5 | return <PageLayout>this is purposly empty</PageLayout>; 6 | } 7 | -------------------------------------------------------------------------------- /examples/pages/empty.tsx: -------------------------------------------------------------------------------- 1 | import type { Value, Options } from '@react-page/editor'; 2 | import Editor from '@react-page/editor'; 3 | 4 | import React, { useState } from 'react'; 5 | import PageLayout from '../components/PageLayout'; 6 | import { cellPlugins } from '../plugins/cellPlugins'; 7 | 8 | const LANGUAGES = [ 9 | { 10 | lang: 'en', 11 | label: 'English', 12 | }, 13 | { 14 | lang: 'de', 15 | label: 'Deutsch', 16 | }, 17 | ]; 18 | 19 | export default function Empty() { 20 | const [value, setValue] = useState<Value | null>(null); 21 | 22 | return ( 23 | <PageLayout> 24 | <Editor 25 | cellPlugins={cellPlugins} 26 | value={value} 27 | lang={LANGUAGES[0].lang} 28 | onChange={setValue} 29 | languages={LANGUAGES} 30 | /> 31 | </PageLayout> 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /examples/pages/examples/bare.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import type { Value } from '@react-page/editor'; 3 | import Editor from '@react-page/editor'; 4 | import slate from '@react-page/plugins-slate'; 5 | import image from '@react-page/plugins-image'; 6 | 7 | const cellPlugins = [slate(), image]; 8 | 9 | // Bare without page layout for bundle size debugging 10 | const Bare = () => { 11 | const [value] = useState<Value | null>(null); 12 | 13 | return ( 14 | <> 15 | <Editor cellPlugins={cellPlugins} value={value} /> 16 | </> 17 | ); 18 | }; 19 | export default Bare; 20 | -------------------------------------------------------------------------------- /examples/pages/examples/customListInSlate.tsx: -------------------------------------------------------------------------------- 1 | // The editor core 2 | import type { Value } from '@react-page/editor'; 3 | import Editor from '@react-page/editor'; 4 | import slate, { pluginFactories } from '@react-page/plugins-slate'; 5 | import React, { useState } from 'react'; 6 | import PageLayout from '../../components/PageLayout'; 7 | 8 | const cellPlugins = [ 9 | slate((def) => ({ 10 | ...def, 11 | plugins: { 12 | ...def.plugins, 13 | lists: { 14 | alpha: pluginFactories.createListPlugin({ 15 | type: 'alpha', 16 | icon: <>a)</>, 17 | label: 'alphabetic List', 18 | tagName: 'ol', 19 | getStyle: () => ({ listStyleType: 'lower-alpha' }), 20 | }), 21 | ...def.plugins.lists, 22 | }, 23 | }, 24 | })), 25 | ]; 26 | export default function CustomFormLayout() { 27 | const [value, setValue] = useState<Value>(); 28 | 29 | return ( 30 | <PageLayout> 31 | <Editor cellPlugins={cellPlugins} value={value} onChange={setValue} /> 32 | </PageLayout> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/pages/examples/dark-editor.tsx: -------------------------------------------------------------------------------- 1 | import type { Value } from '@react-page/editor'; 2 | import Editor, { defaultThemeOptions } from '@react-page/editor'; 3 | import { demo } from '../../sampleContents/demo'; 4 | import React, { useState } from 'react'; 5 | import { cellPlugins } from '../../plugins/cellPlugins'; 6 | import PageLayout from '../../components/PageLayout'; 7 | import { Button, createTheme } from '@mui/material'; 8 | const LANGUAGES = [ 9 | { 10 | lang: 'en', 11 | label: 'English', 12 | }, 13 | { 14 | lang: 'de', 15 | label: 'Deutsch', 16 | }, 17 | ]; 18 | 19 | const darkTheme = createTheme({ 20 | ...defaultThemeOptions, 21 | palette: { mode: 'dark' }, 22 | }); 23 | export default function Dark() { 24 | const [value, setValue] = useState<Value>(demo); 25 | const reset = () => setValue(demo); 26 | 27 | return ( 28 | <PageLayout> 29 | <Editor 30 | cellPlugins={cellPlugins} 31 | value={value} 32 | lang={LANGUAGES[0].lang} 33 | onChange={setValue} 34 | languages={LANGUAGES} 35 | uiTheme={darkTheme} 36 | /> 37 | <Button onClick={reset}>Reset</Button> 38 | </PageLayout> 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /examples/pages/examples/dark-full.tsx: -------------------------------------------------------------------------------- 1 | import type { Value } from '@react-page/editor'; 2 | import { defaultThemeOptions } from '@react-page/editor'; 3 | import Editor from '@react-page/editor'; 4 | import { demo } from '../../sampleContents/demo'; 5 | import React, { useState } from 'react'; 6 | import { cellPlugins } from '../../plugins/cellPlugins'; 7 | import PageLayout from '../../components/PageLayout'; 8 | import { Button, createTheme, ThemeProvider } from '@mui/material'; 9 | const LANGUAGES = [ 10 | { 11 | lang: 'en', 12 | label: 'English', 13 | }, 14 | { 15 | lang: 'de', 16 | label: 'Deutsch', 17 | }, 18 | ]; 19 | 20 | const darkTheme = createTheme({ 21 | ...defaultThemeOptions, 22 | palette: { mode: 'dark' }, 23 | }); 24 | export default function Dark() { 25 | const [value, setValue] = useState<Value>(demo); 26 | const reset = () => setValue(demo); 27 | 28 | return ( 29 | <ThemeProvider theme={darkTheme}> 30 | <PageLayout> 31 | <Editor 32 | cellPlugins={cellPlugins} 33 | value={value} 34 | lang={LANGUAGES[0].lang} 35 | onChange={setValue} 36 | languages={LANGUAGES} 37 | uiTheme={darkTheme} 38 | /> 39 | <Button onClick={reset}>Reset</Button> 40 | </PageLayout> 41 | </ThemeProvider> 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /examples/pages/examples/extractTextContents.tsx: -------------------------------------------------------------------------------- 1 | import Editor, { getTextContents } from '@react-page/editor'; 2 | import React, { useEffect, useState } from 'react'; 3 | import PageLayout from '../../components/PageLayout'; 4 | import { cellPlugins } from '../../plugins/cellPlugins'; 5 | import { demo } from '../../sampleContents/demo'; 6 | 7 | export default function ReadOnly() { 8 | const [value, setValue] = useState(demo); 9 | useEffect(() => { 10 | console.log( 11 | 'raw text contents', 12 | getTextContents(value, { cellPlugins, lang: 'en' }) 13 | ); 14 | }); 15 | return ( 16 | <PageLayout> 17 | <Editor 18 | cellPlugins={cellPlugins} 19 | value={value} 20 | lang="en" 21 | onChange={setValue} 22 | /> 23 | </PageLayout> 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/pages/examples/readonly.tsx: -------------------------------------------------------------------------------- 1 | // The editor core 2 | import Editor from '@react-page/editor'; 3 | // image 4 | import image from '@react-page/plugins-image'; 5 | // The rich text area plugin 6 | import slate from '@react-page/plugins-slate'; 7 | import React from 'react'; 8 | import PageLayout from '../../components/PageLayout'; 9 | import { demoSimpleReadOnly } from '../../sampleContents/demoSimpleReadOnly'; 10 | 11 | // Stylesheets for the rich text area plugin 12 | // uncomment this 13 | //import '@react-page/plugins-slate/lib/index.css'; 14 | 15 | // Stylesheets for the imagea plugin 16 | //import '@react-page/plugins-image/lib/index.css'; 17 | 18 | // Define which plugins we want to use. 19 | const cellPlugins = [slate(), image]; 20 | 21 | export default function ReadOnlyExample() { 22 | // you would usually load SAMPLE_CONTENT from some api / endpoint / database 23 | return ( 24 | <PageLayout> 25 | <Editor value={demoSimpleReadOnly} cellPlugins={cellPlugins} readOnly /> 26 | </PageLayout> 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /examples/pages/examples/simple.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | // The editor core 4 | import type { Value } from '@react-page/editor'; 5 | import Editor from '@react-page/editor'; 6 | 7 | // import the main css, uncomment this: (this is commented in the example because of https://github.com/vercel/next.js/issues/19717) 8 | // import '@react-page/editor/lib/index.css'; 9 | 10 | // The rich text area plugin 11 | import slate from '@react-page/plugins-slate'; 12 | // image 13 | import image from '@react-page/plugins-image'; 14 | import PageLayout from '../../components/PageLayout'; 15 | 16 | // Stylesheets for the rich text area plugin 17 | // uncomment this 18 | //import '@react-page/plugins-slate/lib/index.css'; 19 | 20 | // Stylesheets for the imagea plugin 21 | //import '@react-page/plugins-image/lib/index.css'; 22 | 23 | // Define which plugins we want to use. 24 | const cellPlugins = [slate(), image]; 25 | 26 | export default function SimpleExample() { 27 | const [value, setValue] = useState<Value | null>(null); 28 | 29 | return ( 30 | <PageLayout> 31 | <Editor cellPlugins={cellPlugins} value={value} onChange={setValue} /> 32 | </PageLayout> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@mui/material'; 2 | import type { Value } from '@react-page/editor'; 3 | import Editor from '@react-page/editor'; 4 | import React, { useState } from 'react'; 5 | import PageLayout from '../components/PageLayout'; 6 | import { cellPlugins } from '../plugins/cellPlugins'; 7 | import { demo } from '../sampleContents/demo'; 8 | const LANGUAGES = [ 9 | { 10 | lang: 'en', 11 | label: 'English', 12 | }, 13 | { 14 | lang: 'de', 15 | label: 'Deutsch', 16 | }, 17 | ]; 18 | 19 | export default function Home() { 20 | const [value, setValue] = useState<Value>(demo); 21 | const reset = () => setValue(demo); 22 | 23 | return ( 24 | <PageLayout> 25 | <Editor 26 | cellPlugins={cellPlugins} 27 | value={value} 28 | lang={LANGUAGES[0].lang} 29 | onChange={setValue} 30 | languages={LANGUAGES} 31 | /> 32 | <Button onClick={reset}>Reset</Button> 33 | </PageLayout> 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /examples/pages/old/demo.tsx: -------------------------------------------------------------------------------- 1 | import type { Options, Value, Value_v0 } from '@react-page/editor'; 2 | import Editor from '@react-page/editor'; 3 | 4 | import React, { useState } from 'react'; 5 | import PageLayout from '../../components/PageLayout'; 6 | import { cellPlugins } from '../../plugins/cellPlugins'; 7 | import contents from '../../sampleContents/v0'; 8 | const LANGUAGES = [ 9 | { 10 | lang: 'en', 11 | label: 'English', 12 | }, 13 | { 14 | lang: 'de', 15 | label: 'Deutsch', 16 | }, 17 | ]; 18 | 19 | export default function Home() { 20 | const [value, setValue] = useState<Value_v0 | Value>(contents[0]); 21 | 22 | return ( 23 | <PageLayout> 24 | <Editor 25 | cellPlugins={cellPlugins} 26 | value={value} 27 | onChange={setValue} 28 | languages={LANGUAGES} 29 | /> 30 | </PageLayout> 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /examples/pages/readonly-bare-empty.tsx: -------------------------------------------------------------------------------- 1 | import Editor from '@react-page/editor'; 2 | import React from 'react'; 3 | 4 | export default function ReadOnlyBareEmpty() { 5 | return <Editor cellPlugins={[]} value={null} lang="en" readOnly />; 6 | } 7 | -------------------------------------------------------------------------------- /examples/pages/readonly-bare.tsx: -------------------------------------------------------------------------------- 1 | import Editor from '@react-page/editor'; 2 | import React from 'react'; 3 | import { cellPlugins } from '../plugins/cellPlugins'; 4 | import { demo } from '../sampleContents/demo'; 5 | 6 | export default function ReadOnlyBare() { 7 | return <Editor cellPlugins={cellPlugins} value={demo} lang="en" readOnly />; 8 | } 9 | -------------------------------------------------------------------------------- /examples/pages/readonly.tsx: -------------------------------------------------------------------------------- 1 | import Editor from '@react-page/editor'; 2 | import React from 'react'; 3 | import PageLayout from '../components/PageLayout'; 4 | import { cellPlugins } from '../plugins/cellPlugins'; 5 | import { demo } from '../sampleContents/demo'; 6 | 7 | export default function ReadOnly() { 8 | return ( 9 | <PageLayout> 10 | <Editor cellPlugins={cellPlugins} value={demo} lang="en" readOnly /> 11 | </PageLayout> 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/plugins/codeSnippet.tsx: -------------------------------------------------------------------------------- 1 | import type { CellPlugin } from '@react-page/editor'; 2 | import dynamic from 'next/dynamic'; 3 | import React from 'react'; 4 | 5 | // lazy load to keep initial bundle small 6 | const CodeSnippet = dynamic(() => import('../components/CodeSnippet')); 7 | 8 | const codeSnippet: CellPlugin<{ 9 | code: string; 10 | language: string; 11 | }> = { 12 | Renderer: ({ data }) => 13 | data?.code ? ( 14 | <CodeSnippet language={data.language} code={data.code} /> 15 | ) : null, 16 | id: 'code-snippet', 17 | title: 'Code snippet', 18 | description: 'A code snippet', 19 | version: 1, 20 | controls: { 21 | type: 'autoform', 22 | schema: { 23 | properties: { 24 | language: { 25 | type: 'string', 26 | }, 27 | code: { 28 | type: 'string', 29 | uniforms: { 30 | multiline: true, 31 | }, 32 | }, 33 | }, 34 | required: ['code'], 35 | }, 36 | }, 37 | }; 38 | export default codeSnippet; 39 | -------------------------------------------------------------------------------- /examples/plugins/customLayoutPlugin.tsx: -------------------------------------------------------------------------------- 1 | import type { CellPlugin } from '@react-page/editor'; 2 | import React from 'react'; 3 | import { defaultSlate, customizedSlate } from './slate'; 4 | 5 | const customLayoutPlugin: CellPlugin<{ 6 | backgroundColor: string; 7 | }> = { 8 | Renderer: ({ children, data }) => ( 9 | <div 10 | style={{ 11 | border: '1px solid black', 12 | backgroundColor: data.backgroundColor, 13 | }} 14 | > 15 | {children} 16 | </div> 17 | ), 18 | createInitialChildren: () => { 19 | return [ 20 | [ 21 | { 22 | plugin: defaultSlate, 23 | }, 24 | { 25 | plugin: defaultSlate, 26 | }, 27 | ], 28 | [ 29 | { 30 | plugin: customizedSlate, 31 | }, 32 | { 33 | plugin: customizedSlate, 34 | }, 35 | ], 36 | ]; 37 | }, 38 | 39 | id: 'custom-layout-plugin', 40 | title: 'Custom layout plugin', 41 | description: 'Some custom layout plugin', 42 | version: 1, 43 | controls: { 44 | type: 'autoform', 45 | schema: { 46 | required: ['backgroundColor'], 47 | properties: { 48 | backgroundColor: { type: 'string' }, 49 | }, 50 | }, 51 | }, 52 | }; 53 | 54 | export default customLayoutPlugin; 55 | -------------------------------------------------------------------------------- /examples/plugins/customSlatePlugin.tsx: -------------------------------------------------------------------------------- 1 | import { ColorPickerField } from '@react-page/editor'; 2 | import { pluginFactories } from '@react-page/plugins-slate'; 3 | import React from 'react'; 4 | 5 | export default pluginFactories.createComponentPlugin<{ 6 | color: string; 7 | }>({ 8 | addHoverButton: true, // whether to show it above the text when selected 9 | addToolbarButton: true, // whether to show it in the bottom toolbar 10 | type: 'SetColor', // a well defined string, this is kind of the id of the plugin 11 | object: 'mark', // mark is like a span, other options are inline and block 12 | icon: <span>Color</span>, // an icon to show 13 | label: 'Set Color', 14 | Component: 'span', // the component to render 15 | getStyle: ({ color }) => ({ color }), 16 | controls: { 17 | // identical to custom cell plugins 18 | type: 'autoform', 19 | schema: { 20 | type: 'object', 21 | required: ['color'], 22 | properties: { 23 | color: { 24 | uniforms: { 25 | component: ColorPickerField, 26 | }, 27 | default: 'rgba(0,0,255,1)', 28 | type: 'string', 29 | }, 30 | }, 31 | }, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /examples/plugins/react-katex.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-katex' { 2 | export const InlineMath: React.FC<{ math: string }>; 3 | } 4 | -------------------------------------------------------------------------------- /examples/public/docs: -------------------------------------------------------------------------------- 1 | ../../docs/ -------------------------------------------------------------------------------- /examples/public/images/app-preview.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/app-preview.mp4 -------------------------------------------------------------------------------- /examples/public/images/callisto-preview.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/callisto-preview.mp4 -------------------------------------------------------------------------------- /examples/public/images/clarke-preview.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/clarke-preview.mp4 -------------------------------------------------------------------------------- /examples/public/images/create-content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/create-content.png -------------------------------------------------------------------------------- /examples/public/images/front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/front.png -------------------------------------------------------------------------------- /examples/public/images/grass-header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/grass-header.jpg -------------------------------------------------------------------------------- /examples/public/images/khorana-preview.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/khorana-preview.mp4 -------------------------------------------------------------------------------- /examples/public/images/layouts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/layouts.png -------------------------------------------------------------------------------- /examples/public/images/mountain.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/mountain.jpg -------------------------------------------------------------------------------- /examples/public/images/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/react.png -------------------------------------------------------------------------------- /examples/public/images/responsive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/responsive.png -------------------------------------------------------------------------------- /examples/public/images/sane-markup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/sane-markup.png -------------------------------------------------------------------------------- /examples/public/images/sea-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/sea-bg.jpg -------------------------------------------------------------------------------- /examples/public/images/sites-demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/examples/public/images/sites-demo.mp4 -------------------------------------------------------------------------------- /examples/sampleContents/demoSimpleReadOnly.tsx: -------------------------------------------------------------------------------- 1 | import type { Value } from '@react-page/editor'; 2 | export const demoSimpleReadOnly: Value = { 3 | id: '2390df', 4 | version: 1, 5 | rows: [ 6 | { 7 | id: '4c7d90', 8 | cells: [ 9 | { 10 | id: '95d678', 11 | size: 12, 12 | plugin: { id: 'ory/editor/core/content/slate', version: 1 }, 13 | dataI18n: { 14 | undefined: { 15 | slate: [ 16 | { 17 | children: [{ text: 'Next Level Content Editing' }], 18 | type: 'HEADINGS/HEADING-TWO', 19 | data: { align: 'center' }, 20 | }, 21 | { 22 | children: [{ text: 'ReactPage' }], 23 | type: 'HEADINGS/HEADING-ONE', 24 | data: { align: 'center' }, 25 | }, 26 | ], 27 | }, 28 | }, 29 | rows: [], 30 | inline: null, 31 | }, 32 | ], 33 | }, 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /examples/styles/elements.css: -------------------------------------------------------------------------------- 1 | .react-page-plugins-content-divider { 2 | background-color: #aaa; 3 | width: 100%; 4 | height: 2px; 5 | border: none; 6 | } 7 | 8 | .editable-area code, 9 | .editable-area pre { 10 | font-family: monospace; 11 | border-radius: 0.3em; 12 | padding: 0.4em; 13 | } 14 | 15 | .editable-area code { 16 | display: inline; 17 | margin: 0 0.5em; 18 | white-space: pre; 19 | } 20 | 21 | .editable-area pre { 22 | display: block; 23 | margin: 1em; 24 | } 25 | 26 | .editable-area ul, 27 | .editable-area ol { 28 | margin: 1em 0; 29 | list-style-type: inside; 30 | } 31 | 32 | .editable-area li { 33 | margin: 0.2em 0 0.2em 1em; 34 | } 35 | 36 | .editable-area li p { 37 | margin: 0; 38 | } 39 | 40 | .editable-area ol { 41 | list-style-type: decimal; 42 | } 43 | 44 | .editable-area ul { 45 | list-style-type: disc; 46 | } 47 | -------------------------------------------------------------------------------- /examples/styles/styles.css: -------------------------------------------------------------------------------- 1 | 2 | @import './typography.css'; 3 | @import './elements.css'; 4 | 5 | body { 6 | margin: 0px; 7 | 8 | color: #333333; 9 | font-size: 18px; 10 | line-height: 28px; 11 | 12 | font-family: 'Open Sans', serif; 13 | } 14 | 15 | p { 16 | margin: 0 0 28px; 17 | } 18 | 19 | pre { 20 | overflow-x: auto; 21 | } 22 | 23 | a { 24 | color: #c73036; 25 | font-family: Georgia, serif; 26 | text-decoration: underline; 27 | } 28 | 29 | a:hover { 30 | color: #333333; 31 | text-decoration: underline; 32 | } 33 | 34 | 35 | .react-page-cell-inner-leaf { 36 | padding: 20px; 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /examples/utils/createEmotionCache.ts: -------------------------------------------------------------------------------- 1 | import createCache from '@emotion/cache'; 2 | 3 | // prepend: true moves MUI styles to the top of the <head> so they're loaded first. 4 | // It allows developers to easily override MUI styles with other styling solutions, like CSS modules. 5 | export default function createEmotionCache() { 6 | return createCache({ key: 'css', prepend: true }); 7 | } 8 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "3.6.0", 3 | "npmClient": "yarn", 4 | "packages": [ 5 | "packages/editor", 6 | "packages/plugins/content/*", 7 | "packages/plugins/layout/*", 8 | "packages/react-admin", 9 | "examples" 10 | ], 11 | "useWorkspaces": true, 12 | "version": "0.0.0", 13 | "verifyAccess": false 14 | } 15 | -------------------------------------------------------------------------------- /packages/editor/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/packages/editor/.DS_Store -------------------------------------------------------------------------------- /packages/editor/.npmignore: -------------------------------------------------------------------------------- 1 | src/**/* -------------------------------------------------------------------------------- /packages/editor/README.md: -------------------------------------------------------------------------------- 1 | # React-Page Editor 2 | 3 | This is ReactPage's main Component. 4 | 5 | Read the full readme here https://github.com/react-page/react-page 6 | 7 | Docs: https://react-page.github.io/docs 8 | 9 | Demo: https://react-page.github.io/ 10 | -------------------------------------------------------------------------------- /packages/editor/babel.config.js: -------------------------------------------------------------------------------- 1 | const vendor = require('../../babel.config.js'); 2 | module.exports = Object.assign({}, vendor); 3 | -------------------------------------------------------------------------------- /packages/editor/src/core/Provider/CallbacksProvider.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, PropsWithChildren } from 'react'; 2 | import React, { useRef } from 'react'; 3 | import deepEquals from '../utils/deepEquals'; 4 | import { CallbacksContext } from '../components/hooks'; 5 | 6 | import type { Callbacks } from '../types'; 7 | 8 | const CallbacksProvider: FC<PropsWithChildren<Callbacks>> = ({ 9 | children, 10 | ...callbacks 11 | }) => { 12 | const lastCallbacks = useRef<Callbacks>(); 13 | 14 | const isEqual = lastCallbacks.current 15 | ? deepEquals(lastCallbacks.current, callbacks) 16 | : false; 17 | if (!isEqual) { 18 | lastCallbacks.current = callbacks; 19 | } 20 | 21 | return lastCallbacks.current ? ( 22 | <CallbacksContext.Provider value={lastCallbacks.current}> 23 | {children} 24 | </CallbacksContext.Provider> 25 | ) : null; 26 | }; 27 | 28 | export default CallbacksProvider; 29 | -------------------------------------------------------------------------------- /packages/editor/src/core/Provider/DndProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DndProvider as DndProviderOrg } from 'react-dnd'; 3 | import { useOption } from '../components/hooks'; 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | const DndProvider = ({ children }: any) => { 6 | const dndBackend = useOption('dndBackend'); 7 | return dndBackend ? ( 8 | <DndProviderOrg backend={dndBackend}>{children}</DndProviderOrg> 9 | ) : ( 10 | <>{children}</> 11 | ); 12 | }; 13 | 14 | export default DndProvider; 15 | -------------------------------------------------------------------------------- /packages/editor/src/core/Provider/OptionsProvider.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, PropsWithChildren } from 'react'; 2 | import React, { useRef } from 'react'; 3 | import deepEquals from '../utils/deepEquals'; 4 | import { OptionsContext } from '../components/hooks'; 5 | import { DEFAULT_OPTIONS } from '../defaultOptions'; 6 | import type { Options } from '../types'; 7 | /* 8 | we memoize the options, so that if you access them, you won't get a fresh object every time. 9 | */ 10 | 11 | const OptionsProvider: FC<PropsWithChildren<Options>> = ({ 12 | children, 13 | ...options 14 | }) => { 15 | const lastOptions = useRef<Required<Options>>(); 16 | const fullOptions = { 17 | ...DEFAULT_OPTIONS, 18 | ...options, 19 | }; 20 | 21 | const isEqual = lastOptions.current 22 | ? deepEquals(lastOptions.current, fullOptions) 23 | : false; 24 | if (!isEqual) { 25 | lastOptions.current = fullOptions; 26 | } 27 | 28 | return lastOptions.current ? ( 29 | <OptionsContext.Provider value={lastOptions.current}> 30 | {children} 31 | </OptionsContext.Provider> 32 | ) : null; 33 | }; 34 | 35 | export default OptionsProvider; 36 | -------------------------------------------------------------------------------- /packages/editor/src/core/Provider/RenderOptionsProvider.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, PropsWithChildren } from 'react'; 2 | import React, { useRef } from 'react'; 3 | import deepEquals from '../utils/deepEquals'; 4 | import { RenderOptionsContext } from '../components/hooks'; 5 | import type { RenderOptions } from '../types'; 6 | import { DEFAULT_RENDER_OPTIONS } from '../defaultOptions'; 7 | /* 8 | we memoize the RenderOptions, so that if you access them, you won't get a fresh object every time. 9 | 10 | */ 11 | const RenderOptionsProvider: FC<PropsWithChildren<RenderOptions>> = ({ 12 | children, 13 | ...renderOptions 14 | }) => { 15 | const lastRenderOptions = useRef<Required<RenderOptions>>(); 16 | const fullRenderOptions = { 17 | ...DEFAULT_RENDER_OPTIONS, 18 | ...renderOptions, 19 | }; 20 | 21 | const isEqual = lastRenderOptions.current 22 | ? deepEquals(lastRenderOptions.current, fullRenderOptions) 23 | : false; 24 | if (!isEqual) { 25 | lastRenderOptions.current = fullRenderOptions; 26 | } 27 | 28 | return lastRenderOptions.current ? ( 29 | <RenderOptionsContext.Provider value={lastRenderOptions.current}> 30 | {children} 31 | </RenderOptionsContext.Provider> 32 | ) : null; 33 | }; 34 | 35 | export default RenderOptionsProvider; 36 | -------------------------------------------------------------------------------- /packages/editor/src/core/actions/cell/index.ts: -------------------------------------------------------------------------------- 1 | import type { CellHoverAction } from './drag'; 2 | import { dragActions } from './drag'; 3 | import type { InsertAction } from './insert'; 4 | import { insertActions } from './insert'; 5 | import type { CellCoreAction } from './core'; 6 | import { coreActions } from './core'; 7 | export const cellActions = { ...dragActions, ...insertActions, ...coreActions }; 8 | export * from './insert'; 9 | export * from './core'; 10 | export * from './drag'; 11 | 12 | export type CellAction = CellCoreAction | CellHoverAction | InsertAction; 13 | -------------------------------------------------------------------------------- /packages/editor/src/core/actions/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { NewIds } from '../types/node'; 2 | import { createId } from '../utils/createId'; 3 | 4 | export const generateIds = (): NewIds => { 5 | return { 6 | cell: createId(), 7 | item: createId(), 8 | others: [createId(), createId(), createId()], 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/editor/src/core/actions/setting.ts: -------------------------------------------------------------------------------- 1 | import type { Action } from 'redux'; 2 | 3 | export const SET_LANG = 'SET_LANG'; 4 | 5 | export interface SetLangAction extends Action { 6 | lang: string; 7 | } 8 | 9 | export const setLang = (lang: string): SetLangAction => ({ 10 | type: SET_LANG, 11 | lang, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/editor/src/core/actions/undo.ts: -------------------------------------------------------------------------------- 1 | import type { Action } from 'redux'; 2 | import { ActionTypes } from 'redux-undo'; 3 | 4 | export const undo = (): Action => ({ 5 | type: ActionTypes.UNDO, 6 | }); 7 | 8 | export const redo = (): Action => ({ 9 | type: ActionTypes.REDO, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/editor/src/core/actions/value.ts: -------------------------------------------------------------------------------- 1 | import type { Action } from 'redux'; 2 | import type { Value, NewIds } from '../types/node'; 3 | import { generateIds } from './helpers'; 4 | 5 | export const UPDATE_VALUE = 'UPDATE_VALUE'; 6 | 7 | export interface UpdateEditableAction extends Action { 8 | ts: Date; 9 | value: Value | null; 10 | ids: NewIds; 11 | } 12 | 13 | export const updateValue = (value: Value | null): UpdateEditableAction => ({ 14 | type: UPDATE_VALUE, 15 | ts: new Date(), 16 | value, 17 | ids: generateIds(), 18 | }); 19 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Cell/CellErrorGate.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ErrorCell from './ErrorCell'; 3 | 4 | export const CellErrorGate = class extends React.Component< 5 | { 6 | children: React.ReactNode; 7 | nodeId: string; 8 | shouldShowError?: boolean; 9 | }, 10 | { error: Error | null } 11 | > { 12 | state = { 13 | error: null, 14 | }; 15 | componentDidCatch(error: Error) { 16 | this.setState({ error }); 17 | console.error(error); 18 | } 19 | 20 | reset() { 21 | this.setState({ error: null }); 22 | } 23 | 24 | render() { 25 | if (this.state.error && this.props.shouldShowError) { 26 | return ( 27 | <ErrorCell 28 | nodeId={this.props.nodeId} 29 | error={this.state.error} 30 | resetError={this.reset.bind(this)} 31 | /> 32 | ); 33 | } 34 | return this.props.children; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Cell/ErrorCell/index.css: -------------------------------------------------------------------------------- 1 | .react-page-cell-error { 2 | background-color: red; 3 | padding: 8px; 4 | margin: 2px; 5 | overflow: hidden; 6 | } 7 | 8 | .react-page-cell-error strong { 9 | margin: 0 auto; 10 | } 11 | 12 | .react-page-cell-error code { 13 | overflow: scroll; 14 | } 15 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Cell/ErrorCell/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useIsEditMode, useRemoveCell, useUiTranslator } from '../../hooks'; 3 | 4 | const ErrorCell: React.FC<{ 5 | nodeId: string; 6 | error: Error; 7 | resetError?: () => void; 8 | }> = ({ nodeId, error, resetError }) => { 9 | const isEditMode = useIsEditMode(); 10 | const removeCell = useRemoveCell(nodeId); 11 | const { t } = useUiTranslator(); 12 | return ( 13 | <div className="react-page-cell-error"> 14 | <strong>{t('An error occurred!')}</strong> 15 | <small> 16 | <dl> 17 | <dt>{t('Cause:')}</dt> 18 | <dd>{error.message}</dd> 19 | <dt>{t('Cell:')}</dt> 20 | <dd>{nodeId}</dd> 21 | </dl> 22 | </small> 23 | {isEditMode ? ( 24 | <> 25 | {resetError ? ( 26 | <button onClick={() => resetError()}>{t('Reset')}</button> 27 | ) : null} 28 | <button onClick={() => removeCell()}>{t('Remove')}</button> 29 | </> 30 | ) : null} 31 | </div> 32 | ); 33 | }; 34 | 35 | export default ErrorCell; 36 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Cell/Handle/index.css: -------------------------------------------------------------------------------- 1 | .react-page-editable { 2 | .react-page-cell-handle { 3 | display: none; 4 | } 5 | &&-mode-edit, 6 | &&-mode-resizing, 7 | &&-mode-layout { 8 | .react-page-cell-handle { 9 | position: absolute; 10 | top: 0px; 11 | left: 50%; 12 | transform: translateX(-50%) translateY(-100%); 13 | transition: opacity ease 0.4s; 14 | opacity: 0; 15 | 16 | color: rgba(0, 0, 0, 0.97); 17 | 18 | background: rgba(255, 255, 255, 0.95); 19 | text-align: center; 20 | color: rgba(0, 0, 0, 0.97); 21 | 22 | display: inline-block; 23 | padding: 12px 24px; 24 | margin: 0 auto; 25 | border-radius: 12px 12px 0 0; 26 | text-transform: uppercase; 27 | font-size: 14px; 28 | line-height: 1.4; 29 | letter-spacing: 0.15em; 30 | 31 | box-shadow: 0 -5px 5px rgb(0 0 0 / 22%); 32 | pointer-events: none; 33 | } 34 | 35 | .react-page-cell-handle-drag-enabled { 36 | cursor: move; 37 | } 38 | 39 | .react-page-cell:hover > .react-page-cell-handle, 40 | .react-page-cell.react-page-cell-focused > .react-page-cell-handle { 41 | opacity: 1; 42 | pointer-events: all; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Cell/NoopProvider.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | 4 | const NoopProvider: FC<PropsWithChildren> = ({ children }) => <>{children}</>; 5 | 6 | export default NoopProvider; 7 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Cell/PluginMissing.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | 4 | import type { CellPluginMissingProps } from '../../types/plugins'; 5 | 6 | const PluginMissing: React.FC<PropsWithChildren<CellPluginMissingProps>> = ({ 7 | children, 8 | ...props 9 | }) => ( 10 | <div> 11 | <div 12 | style={{ 13 | backgroundColor: 'red', 14 | padding: '8px', 15 | border: '1px solid black', 16 | margin: '2px', 17 | overflowX: 'scroll', 18 | }} 19 | > 20 | The requested plugin `{props.pluginId}` could not be found. 21 | <button onClick={props.remove}>Delete Plugin</button> 22 | <pre>{JSON.stringify(props, null, 2)}</pre> 23 | </div> 24 | {children} 25 | </div> 26 | ); 27 | 28 | export default PluginMissing; 29 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Cell/Rows/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | 4 | import Component from '../index'; 5 | 6 | describe('components/Cell/Rows', () => { 7 | xit('renders a single div', () => { 8 | const wrapper = shallow(<Component nodeId="some-node-id" />); 9 | expect(wrapper.find('.react-page-cell-rows')).toHaveLength(1); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Cell/Rows/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Row from '../../Row'; 3 | import { useNodeChildrenIds } from '../../hooks'; 4 | 5 | const Rows: React.FC<{ 6 | nodeId: string; 7 | }> = ({ nodeId }) => { 8 | const rowIds = useNodeChildrenIds(nodeId); 9 | 10 | return ( 11 | <div className="react-page-cell-rows"> 12 | {rowIds.map((id) => ( 13 | <Row nodeId={id} key={id} /> 14 | ))} 15 | </div> 16 | ); 17 | }; 18 | 19 | export default React.memo(Rows); 20 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Cell/utils/scrollIntoViewWithOffset.ts: -------------------------------------------------------------------------------- 1 | export default ( 2 | element: HTMLElement, 3 | offset = 0, 4 | behavior: ScrollBehavior = 'smooth' 5 | ) => { 6 | if (!element) { 7 | return; 8 | } 9 | const bodyRect = document.body.getBoundingClientRect().top; 10 | const elementRect = element.getBoundingClientRect().top; 11 | const elementPosition = elementRect - bodyRect; 12 | const offsetPosition = elementPosition - offset; 13 | 14 | window.scrollTo({ 15 | top: offsetPosition, 16 | behavior, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Editable/Inner/Rows.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InsertNew from '../../Cell/InsertNew'; 3 | import { useCellSpacing, useOption, useValueNode } from '../../hooks'; 4 | import Row from '../../Row'; 5 | 6 | const Rows: React.FC = () => { 7 | const { rowIds } = useValueNode((editable) => ({ 8 | rowIds: editable?.rows?.map((c) => c.id) ?? [], 9 | })); 10 | 11 | const childConstraints = useOption('childConstraints'); 12 | const components = useOption('components'); 13 | 14 | const { y: cellSpacingY } = useCellSpacing(); 15 | const insertAllowed = childConstraints?.maxChildren 16 | ? childConstraints?.maxChildren > rowIds.length 17 | : true; 18 | 19 | const InsertNewWithDefault = components?.InsertNew ?? InsertNew; 20 | 21 | return ( 22 | <> 23 | {rowIds.length > 0 ? ( 24 | <div 25 | style={ 26 | cellSpacingY !== 0 27 | ? { margin: `${-cellSpacingY / 2}px 0` } 28 | : undefined 29 | } 30 | > 31 | {rowIds.map((id) => ( 32 | <Row nodeId={id} key={id} /> 33 | ))} 34 | </div> 35 | ) : null} 36 | {insertAllowed ? <InsertNewWithDefault childrenIds={rowIds} /> : null} 37 | </> 38 | ); 39 | }; 40 | 41 | export default React.memo(Rows); 42 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Editable/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FallbackDropArea from './FallbackDropArea'; 3 | import Inner from './Inner'; 4 | 5 | const Editable: React.FC = () => { 6 | return ( 7 | <FallbackDropArea> 8 | <Inner /> 9 | </FallbackDropArea> 10 | ); 11 | }; 12 | 13 | export default React.memo(Editable); 14 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/Row/Droppable/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | import { useCellDrop } from '../../Cell/Droppable'; 4 | import { useIsInsertMode, useIsLayoutMode } from '../../hooks'; 5 | 6 | const Droppable: FC<PropsWithChildren<{ nodeId: string }>> = ({ 7 | children, 8 | nodeId, 9 | }) => { 10 | const isLayoutMode = useIsLayoutMode(); 11 | const isInsertMode = useIsInsertMode(); 12 | 13 | const [ref, isAllowed] = useCellDrop(nodeId); 14 | if (!(isLayoutMode || isInsertMode)) { 15 | return <div className="react-page-row-droppable-container">{children}</div>; 16 | } 17 | return ( 18 | <div ref={ref} className="react-page-row-droppable"> 19 | {children} 20 | </div> 21 | ); 22 | }; 23 | 24 | export default Droppable; 25 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/hooks/actions.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { redo, undo } from '../../actions/undo'; 3 | import { useDispatch, useSelector } from '../../reduxConnect'; 4 | 5 | /** 6 | * @returns function, that undos last change if called 7 | */ 8 | export const useUndo = () => { 9 | const dispatch = useDispatch(); 10 | return useCallback(() => dispatch(undo()), [dispatch]); 11 | }; 12 | 13 | /** 14 | * @returns function, that redos last change if called 15 | */ 16 | export const useRedo = () => { 17 | const dispatch = useDispatch(); 18 | return useCallback(() => dispatch(redo()), [dispatch]); 19 | }; 20 | 21 | /** 22 | * @returns whether user can undo 23 | */ 24 | export const useCanUndo = () => { 25 | return useSelector((s) => s.reactPage.values.past.length > 0); 26 | }; 27 | /** 28 | * @returns whether user can undo 29 | */ 30 | export const useCanRedo = () => { 31 | return useSelector((s) => s.reactPage.values.future.length > 0); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/hooks/callbacks.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useRef } from 'react'; 2 | import type { Callbacks } from '../../types'; 3 | 4 | export const CallbacksContext = createContext<Callbacks>({}); 5 | 6 | /** 7 | * @returns the callbacks object of the current Editor. 8 | * 9 | * this object is memoized, alltough its better to use `usecallback` instead if you want to use a single callback 10 | */ 11 | export const useCallbackOptions = () => useContext(CallbacksContext); 12 | 13 | /** 14 | * get a single (memoized) callback 15 | * @param key the callback key 16 | * @returns the callback value 17 | */ 18 | export const useCallbackOption = <K extends keyof Callbacks>(key: K) => { 19 | const callbacks = useCallbackOptions(); 20 | const callback = callbacks[key]; 21 | const lastcallback = useRef(callback); 22 | if (lastcallback.current !== callback) { 23 | lastcallback.current = callback; 24 | } 25 | return lastcallback.current; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './value'; 2 | export * from './node'; 3 | export * from './focus'; 4 | export * from './options'; 5 | export * from './renderOptions'; 6 | export * from './callbacks'; 7 | export * from './actions'; 8 | export * from './nodeActions'; 9 | export * from './displayMode'; 10 | export * from './dragDropActions'; 11 | export * from './screen'; 12 | export * from './display'; 13 | export * from './nodeMove'; 14 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/hooks/renderOptions.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useRef } from 'react'; 2 | import deepEquals from '../../utils/deepEquals'; 3 | import { DEFAULT_RENDER_OPTIONS } from '../../defaultOptions'; 4 | import type { RenderOptions } from '../../types'; 5 | 6 | export const RenderOptionsContext = createContext<RenderOptions>( 7 | DEFAULT_RENDER_OPTIONS 8 | ); 9 | 10 | /** 11 | * @returns the options object of the current Editor. 12 | * 13 | * this object is memoized, alltough its better to use `useOption` instead if you want to use a single option 14 | */ 15 | export const useRenderOptions = () => useContext(RenderOptionsContext); 16 | 17 | /** 18 | * get a single (memoized) option value 19 | * @param key the option key 20 | * @returns the option value 21 | */ 22 | export const useRenderOption = <K extends keyof RenderOptions>(key: K) => { 23 | const options = useRenderOptions(); 24 | const option = options[key]; 25 | const lastOption = useRef(option); 26 | if (!deepEquals(lastOption.current, option)) { 27 | lastOption.current = option; 28 | } 29 | return lastOption.current; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/hooks/screen.tsx: -------------------------------------------------------------------------------- 1 | import { useMediaQuery, useTheme } from '@mui/material'; 2 | 3 | export const useIsSmallScreen = () => { 4 | const theme = useTheme(); 5 | return useMediaQuery(theme.breakpoints.down('sm')); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/hooks/value.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from '../../reduxConnect'; 2 | 3 | import { currentValue } from '../../selector/editable'; 4 | import type { Value } from '../../types/node'; 5 | import deepEquals from '../../utils/deepEquals'; 6 | 7 | type ValueSelector<T> = (node: Value | null) => T; 8 | /** 9 | * 10 | * @param selector receives the current value node object and returns T 11 | * @returns the selection T 12 | */ 13 | export const useValueNode = <T>(selector: ValueSelector<T>) => { 14 | return useSelector((state) => selector(currentValue(state)), deepEquals); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/editor/src/core/components/index.css: -------------------------------------------------------------------------------- 1 | @import './Row/index.css'; 2 | @import './Editable/index.css'; 3 | @import './Cell/index.css'; 4 | -------------------------------------------------------------------------------- /packages/editor/src/core/const.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A list of positions in the layout space. 3 | */ 4 | export enum PositionEnum { 5 | LEFT_OF = 'left-of', 6 | RIGHT_OF = 'right-of', 7 | ABOVE = 'above', 8 | BELOW = 'below', 9 | INLINE_LEFT = 'inline-left', 10 | INLINE_RIGHT = 'inline-right', 11 | } 12 | 13 | /** 14 | * Is true if built in production mode. 15 | */ 16 | export const isProduction = process.env.NODE_ENV === 'production'; 17 | -------------------------------------------------------------------------------- /packages/editor/src/core/defaultOptions.ts: -------------------------------------------------------------------------------- 1 | import type { Options, RenderOptions } from './types'; 2 | 3 | import { HTML5Backend } from 'react-dnd-html5-backend'; 4 | import { DISPLAY_MODE_EDIT } from './actions/display'; 5 | import { defaultTheme } from '../ui'; 6 | 7 | export const DEFAULT_OPTIONS: Required<Options> = { 8 | allowMoveInEditMode: true, 9 | allowResizeInEditMode: true, 10 | 11 | childConstraints: {}, 12 | components: {}, 13 | languages: [], 14 | uiTranslator: null, 15 | zoomEnabled: true, 16 | zoomFactors: [1, 0.75, 0.5, 0.25], 17 | undoRedoEnabled: true, 18 | editEnabled: true, 19 | insertEnabled: true, 20 | layoutEnabled: true, 21 | resizeEnabled: true, 22 | previewEnabled: true, 23 | 24 | dndBackend: HTML5Backend, 25 | blurGateDefaultMode: DISPLAY_MODE_EDIT, 26 | blurGateDisabled: false, 27 | middleware: [], 28 | store: null, 29 | hideEditorSidebar: false, 30 | showMoveButtonsInBottomToolbar: true, 31 | showMoveButtonsInLayoutMode: true, 32 | sidebarPosition: 'rightAbsolute', 33 | customOptions: [], 34 | uiTheme: defaultTheme, 35 | shouldShowErrorInCells: false, 36 | }; 37 | 38 | export const DEFAULT_RENDER_OPTIONS: Required<RenderOptions> = { 39 | cellPlugins: [], 40 | cellSpacing: null, 41 | }; 42 | -------------------------------------------------------------------------------- /packages/editor/src/core/helper/throttle/index.ts: -------------------------------------------------------------------------------- 1 | import { isProduction } from '../../const'; 2 | 3 | export const delay = isProduction ? 40 : 60; 4 | -------------------------------------------------------------------------------- /packages/editor/src/core/index.css: -------------------------------------------------------------------------------- 1 | @import 'grid.css'; 2 | @import 'components/index.css'; 3 | -------------------------------------------------------------------------------- /packages/editor/src/core/migrations/EDITABLE_MIGRATIONS/index.ts: -------------------------------------------------------------------------------- 1 | import from0to1 from './from0to1'; 2 | 3 | export const CURRENT_EDITABLE_VERSION = 1; 4 | export default [from0to1]; 5 | -------------------------------------------------------------------------------- /packages/editor/src/core/migrations/serialzeValue.ts: -------------------------------------------------------------------------------- 1 | import type { CellPluginList } from '../types/plugins'; 2 | import type { Cell, Value, Row } from '../types/node'; 3 | 4 | const serializeRow = (r: Row, cellPlugins: CellPluginList): Row => { 5 | return { 6 | ...r, 7 | cells: r.cells.map((c) => serializeCell(c, cellPlugins)), 8 | }; 9 | }; 10 | const serializeCell = (c: Cell, cellPlugins: CellPluginList): Cell => { 11 | const pluginDef = c.plugin; 12 | const pluginFound = pluginDef 13 | ? cellPlugins.find((p) => p.id === pluginDef.id) 14 | : null; 15 | 16 | const transformData = (dataIn: unknown) => { 17 | return pluginFound?.serialize ? pluginFound.serialize(dataIn) : dataIn; 18 | }; 19 | const dataI18n = c.dataI18n 20 | ? Object.keys(c.dataI18n).reduce( 21 | (acc, lang) => ({ 22 | ...acc, 23 | [lang]: transformData(c.dataI18n?.[lang]), 24 | }), 25 | {} 26 | ) 27 | : null; 28 | 29 | return { 30 | ...c, 31 | rows: c.rows?.map((r) => serializeRow(r, cellPlugins)), 32 | dataI18n: dataI18n ?? {}, 33 | }; 34 | }; 35 | 36 | export const serialzeValue = ( 37 | { rows, ...rest }: Value, 38 | plugins: CellPluginList 39 | ) => { 40 | return { 41 | ...rest, 42 | rows: rows.map((c) => serializeRow(c, plugins)), 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/editor/src/core/reducer/hover/index.ts: -------------------------------------------------------------------------------- 1 | import type { CellHoverAction, ClearHoverAction } from '../../actions/cell'; 2 | import { CELL_DRAG_HOVER, CLEAR_CLEAR_HOVER } from '../../actions/cell'; 3 | import type { PositionEnum } from '../../const'; 4 | 5 | export type Hover = { 6 | nodeId?: string; 7 | position: PositionEnum; 8 | } | null; 9 | 10 | export const hover = ( 11 | state: Hover = null, 12 | action: CellHoverAction | ClearHoverAction 13 | ): Hover => { 14 | switch (action.type) { 15 | case CELL_DRAG_HOVER: { 16 | return { 17 | nodeId: action.hoverId, 18 | position: action.position, 19 | }; 20 | } 21 | case CLEAR_CLEAR_HOVER: 22 | return null; 23 | 24 | default: 25 | return state; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /packages/editor/src/core/reducer/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import type { Value, RootState } from '../types'; 3 | 4 | import { values } from './values'; 5 | import { display } from './display'; 6 | import { focus } from './focus'; 7 | 8 | import { hover } from './hover'; 9 | import { settings } from './settings'; 10 | 11 | const reducer = combineReducers({ 12 | values, 13 | display, 14 | focus, 15 | settings, 16 | hover, 17 | __nodeCache: () => null, // always empty __nodeCache 18 | }); 19 | 20 | export { reducer }; 21 | 22 | export default combineReducers({ reactPage: reducer }); 23 | 24 | export function initialState(value: Value | null, lang: string): RootState { 25 | return { 26 | reactPage: { 27 | __nodeCache: {}, 28 | hover: null, 29 | focus: null, 30 | display: { 31 | mode: 'edit', 32 | zoom: 1, 33 | }, 34 | settings: { 35 | lang, 36 | }, 37 | values: { 38 | past: [], 39 | present: value, 40 | future: [], 41 | }, 42 | }, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /packages/editor/src/core/reducer/settings/index.ts: -------------------------------------------------------------------------------- 1 | import { SET_LANG } from '../../actions/setting'; 2 | 3 | export const settings = ( 4 | state = { 5 | lang: null, 6 | }, 7 | action: { 8 | type: string; 9 | 10 | [key: string]: unknown; 11 | } 12 | ) => { 13 | switch (action.type) { 14 | case SET_LANG: 15 | return { 16 | ...state, 17 | lang: action.lang, 18 | }; 19 | default: 20 | return state; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /packages/editor/src/core/reducer/value/helper/__tests__/sizing.test.ts: -------------------------------------------------------------------------------- 1 | import expect from 'unexpected'; 2 | 3 | import { sumSizes, resizeCells } from '../sizing'; 4 | import type { Cell } from '../../../../types/node'; 5 | 6 | describe('sumSizes', () => { 7 | [ 8 | { 9 | cells: [{ size: 6 } as Cell, { size: 6 } as Cell], 10 | e: 12, 11 | }, 12 | { 13 | cells: [{ size: 6 } as Cell, { size: 2 } as Cell], 14 | e: 8, 15 | }, 16 | { 17 | cells: [{ size: 6 } as Cell, { size: 6 } as Cell, { size: 3 } as Cell], 18 | e: 15, 19 | }, 20 | ].forEach((c, k) => { 21 | it(`should pass test case ${k}`, () => { 22 | expect(sumSizes(c.cells), 'to equal', c.e); 23 | }); 24 | }); 25 | }); 26 | 27 | describe('resizeCells', () => { 28 | [ 29 | { 30 | a: { id: '2', size: 6 } as Cell, 31 | cells: [ 32 | { id: '1', size: 4 } as Cell, 33 | { id: '2', size: 4 } as Cell, 34 | { id: '3', size: 4 } as Cell, 35 | ], 36 | e: [ 37 | { id: '1', size: 4 }, 38 | { id: '2', size: 6 }, 39 | { id: '3', size: 2 }, 40 | ], 41 | }, 42 | ].forEach((c, k) => { 43 | it(`should pass test case ${k}`, () => { 44 | expect(resizeCells(c.cells, c.a), 'to equal', c.e); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/editor/src/core/reducer/value/helper/empty.ts: -------------------------------------------------------------------------------- 1 | import type { Node } from '../../../types/node'; 2 | import { isRow } from '../../../types/node'; 3 | 4 | export const isEmpty = (node: Node): boolean => { 5 | if (!node) { 6 | return true; 7 | } 8 | if (isRow(node)) { 9 | return node.cells.length === 0; 10 | } 11 | if (node.rows && node.rows?.length > 0) { 12 | return false; 13 | } 14 | return !node.plugin; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/editor/src/core/reducer/value/helper/setAllSizesAndOptimize.ts: -------------------------------------------------------------------------------- 1 | import { computeSizes, computeInlines } from './sizing'; 2 | import { optimizeRow, optimizeRows, optimizeCells } from './optimize'; 3 | import type { Row } from '../../../types/node'; 4 | 5 | export const setAllSizesAndOptimize = (rows: Array<Row> = []): Array<Row> => 6 | optimizeRows(rows).map((r: Row): Row => { 7 | const optimized = optimizeRow(r); 8 | if (optimized.cells) { 9 | optimized.cells = computeInlines( 10 | computeSizes( 11 | optimizeCells( 12 | optimized.cells.map((cell) => ({ 13 | ...cell, 14 | rows: cell.rows ? setAllSizesAndOptimize(cell.rows) : undefined, 15 | })) 16 | ) 17 | ) 18 | ); 19 | } 20 | return optimized; 21 | }); 22 | -------------------------------------------------------------------------------- /packages/editor/src/core/reducer/value/index.ts: -------------------------------------------------------------------------------- 1 | import type { AnyAction } from 'redux'; 2 | import type { Value } from '../../types/node'; 3 | 4 | import { setAllSizesAndOptimize } from './helper/setAllSizesAndOptimize'; 5 | import { rows } from './tree'; 6 | 7 | export const value = (state: Value | null | undefined, action: AnyAction) => { 8 | switch (action.type) { 9 | case 'UPDATE_VALUE': { 10 | return action.value; 11 | } 12 | } 13 | const newRows = state?.rows 14 | ? setAllSizesAndOptimize(rows(state.rows, action, 0)) 15 | : []; 16 | 17 | return { 18 | ...state, 19 | rows: newRows, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/editor/src/core/reducer/value/testUtils.ts: -------------------------------------------------------------------------------- 1 | import type { Action } from 'redux'; 2 | import { applyMiddleware, combineReducers, createStore } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | import type { Value } from '../../types/node'; 5 | 6 | import { value } from './index'; 7 | 8 | export const simulateDispatch = ( 9 | initialState: Value, 10 | action?: Action 11 | ): Value => { 12 | const reducer = combineReducers({ value }); 13 | const store = createStore( 14 | reducer, 15 | { value: initialState }, 16 | applyMiddleware(thunk) 17 | ); 18 | if (action) store.dispatch(action); 19 | 20 | return store.getState().value; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/editor/src/core/reduxConnect.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import type { Dispatch } from 'react'; 3 | import React from 'react'; 4 | import { 5 | createDispatchHook, 6 | createSelectorHook, 7 | createStoreHook, 8 | Provider, 9 | } from 'react-redux'; 10 | import type { RootState } from './types'; 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | export const ReduxContext = React.createContext<any>(null); 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | export const ReduxProvider = ({ store, ...props }: any) => ( 16 | <Provider store={store} context={ReduxContext} {...props} /> 17 | ); 18 | 19 | export const useStore = createStoreHook<RootState>(ReduxContext); 20 | export const useDispatch = createDispatchHook( 21 | ReduxContext 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | ) as () => Dispatch<any>; 24 | export const useSelector = createSelectorHook<RootState>(ReduxContext); 25 | -------------------------------------------------------------------------------- /packages/editor/src/core/selector/display/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DISPLAY_MODE_EDIT, 3 | DISPLAY_MODE_LAYOUT, 4 | DISPLAY_MODE_PREVIEW, 5 | DISPLAY_MODE_INSERT, 6 | DISPLAY_MODE_RESIZING, 7 | } from '../../actions/display'; 8 | 9 | import type { RootState } from '../../types/state'; 10 | 11 | export const isPreviewMode = ({ 12 | reactPage: { 13 | display: { mode }, 14 | }, 15 | }: RootState): boolean => mode === DISPLAY_MODE_PREVIEW; 16 | export const isLayoutMode = ({ 17 | reactPage: { 18 | display: { mode }, 19 | }, 20 | }: RootState): boolean => mode === DISPLAY_MODE_LAYOUT; 21 | export const isEditMode = ({ 22 | reactPage: { 23 | display: { mode }, 24 | }, 25 | }: RootState): boolean => mode === DISPLAY_MODE_EDIT; 26 | export const isInsertMode = ({ 27 | reactPage: { 28 | display: { mode }, 29 | }, 30 | }: RootState): boolean => mode === DISPLAY_MODE_INSERT; 31 | export const isResizeMode = ({ 32 | reactPage: { 33 | display: { mode }, 34 | }, 35 | }: RootState): boolean => mode === DISPLAY_MODE_RESIZING; 36 | -------------------------------------------------------------------------------- /packages/editor/src/core/selector/focus.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '../types/state'; 2 | import { findNodeInState } from './editable'; 3 | 4 | export const focus = (state: RootState) => 5 | state && state.reactPage && state.reactPage.focus; 6 | 7 | export const allFocusedNodeIds = (state: RootState) => { 8 | return ( 9 | focus(state)?.nodeIds?.filter((n) => findNodeInState(state, n)?.node) ?? [] 10 | ); 11 | }; 12 | export const singleFocusedNode = (state: RootState) => { 13 | const nodeIds = allFocusedNodeIds(state); 14 | 15 | if (nodeIds?.length === 1) return nodeIds[0]; 16 | return null; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/editor/src/core/selector/setting.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '../types/state'; 2 | 3 | export const getLang = ({ reactPage: { settings } }: RootState) => 4 | settings.lang ?? 'default'; 5 | -------------------------------------------------------------------------------- /packages/editor/src/core/store.ts: -------------------------------------------------------------------------------- 1 | import type { Store, Middleware } from 'redux'; 2 | import { createStore, applyMiddleware, compose } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | import rootReducer from './reducer'; 5 | import type { RootState } from './types/state'; 6 | import { isProduction } from './const'; 7 | 8 | declare global { 9 | interface Window { 10 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: (settings: unknown) => void; 11 | } 12 | } 13 | 14 | /** 15 | * Returns a new redux store. 16 | */ 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | export default ( 19 | initialState: Record<string, unknown>, 20 | middleware: Middleware[] = [] 21 | ): Store<RootState> => { 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | const v: any = 24 | !isProduction && 25 | typeof window === 'object' && 26 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 27 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) 28 | : compose; 29 | 30 | return createStore( 31 | rootReducer, 32 | initialState, 33 | v(applyMiddleware(...middleware, thunk)) 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/editor/src/core/types/callbacks.ts: -------------------------------------------------------------------------------- 1 | import type { Value } from './node'; 2 | 3 | export type Callbacks = { 4 | /** 5 | * is called when the value has changed. 6 | * Use this to save the new value 7 | */ 8 | onChange?: null | ((v: Value) => void); 9 | 10 | /** 11 | * is called when the language has changed 12 | */ 13 | onChangeLang?: null | ((l: string) => void); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/editor/src/core/types/components.ts: -------------------------------------------------------------------------------- 1 | import type { BottomToolbarProps } from '../../ui/BottomToolbar/types'; 2 | import type { InsertNewProps } from '../components/Cell/InsertNew'; 3 | import type { CellPluginMissingProps } from './plugins'; 4 | 5 | /** 6 | * Internal component overrides for the editor. 7 | */ 8 | export type Components = { 9 | /** 10 | * BottomToolbar used for rendering plugin controls. 11 | */ 12 | BottomToolbar?: React.ComponentType<BottomToolbarProps>; 13 | CellPluginMissing?: React.ComponentType<CellPluginMissingProps>; 14 | EditModeResizeHandle?: React.ComponentType<{ onClick: () => void }>; 15 | InsertNew?: React.ComponentType<InsertNewProps>; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/editor/src/core/types/constraints.ts: -------------------------------------------------------------------------------- 1 | export type ChildConstraints = { 2 | /** 3 | * EXPERIMENTAL 4 | * 5 | * how many direct children are allowed? 6 | * 7 | * currently only affects the "+" button 8 | */ 9 | maxChildren?: number; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/editor/src/core/types/display.ts: -------------------------------------------------------------------------------- 1 | import type { DisplayModes } from '../actions/display'; 2 | 3 | export type Display = { 4 | mode: DisplayModes; 5 | referenceNodeId?: string; 6 | zoom: number; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/editor/src/core/types/hover.ts: -------------------------------------------------------------------------------- 1 | import type { InsertOptions } from '../actions/cell'; 2 | import type { HoverTarget } from '../service/hover/computeHover'; 3 | import type { PartialCell } from './node'; 4 | 5 | export type Room = { 6 | height: number; 7 | width: number; 8 | }; 9 | 10 | export type Vector = { 11 | y: number; 12 | x: number; 13 | }; 14 | 15 | export type MatrixIndex = { 16 | row: number; 17 | cell: number; 18 | }; 19 | 20 | export type HoverInsertActions = { 21 | dragCell(id: string): void; 22 | cancelCellDrag(): void; 23 | clear(): void; 24 | above(drag: PartialCell, hover: HoverTarget, options?: InsertOptions): void; 25 | below(drag: PartialCell, hover: HoverTarget, options?: InsertOptions): void; 26 | leftOf(drag: PartialCell, hover: HoverTarget, options?: InsertOptions): void; 27 | rightOf(drag: PartialCell, hover: HoverTarget, options?: InsertOptions): void; 28 | inlineLeft(drag: PartialCell, hover: HoverTarget): void; 29 | inlineRight(drag: PartialCell, hover: HoverTarget): void; 30 | }; 31 | 32 | export type Matrix = Array<Array<number>>; 33 | -------------------------------------------------------------------------------- /packages/editor/src/core/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Value } from '../..'; 2 | import type { Value_v0 } from '../migrations/EDITABLE_MIGRATIONS/from0to1'; 3 | 4 | export * from './display'; 5 | export * from './node'; 6 | export * from './hover'; 7 | export * from './jsonSchema'; 8 | export * from './plugins'; 9 | export * from './state'; 10 | export * from './constraints'; 11 | export * from './options'; 12 | export * from './renderOptions'; 13 | export * from './callbacks'; 14 | export type ValueWithLegacy = Value | Value_v0; 15 | export type { Value_v0 }; 16 | -------------------------------------------------------------------------------- /packages/editor/src/core/types/renderOptions.ts: -------------------------------------------------------------------------------- 1 | import type { CellPluginList } from '.'; 2 | 3 | export type CellSpacing = { 4 | x: number; 5 | y: number; 6 | }; 7 | export type RenderOptions = { 8 | /** 9 | * an array of cell plugins. These plugins can be added as cells and usually render a component and a control. 10 | * @see CellPlugin 11 | */ 12 | cellPlugins: CellPluginList; 13 | 14 | /** 15 | * Sets the size of the cell grid gutters in pixels. 16 | */ 17 | cellSpacing?: number | CellSpacing | null; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/editor/src/core/types/state.ts: -------------------------------------------------------------------------------- 1 | import type { NodeWithAncestors, ValueWithHistory } from './node'; 2 | import type { Display } from './display'; 3 | import type { Focus } from '../reducer/focus'; 4 | import type { Hover } from '../reducer/hover'; 5 | 6 | export type RootState = { 7 | reactPage: { 8 | values: ValueWithHistory; 9 | display: Display; 10 | focus: Focus | null; 11 | hover: Hover | null; 12 | settings: { 13 | lang?: string; 14 | }; 15 | __nodeCache?: Record<string, NodeWithAncestors | null>; 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/editor/src/core/utils/cloneWithNewIds.ts: -------------------------------------------------------------------------------- 1 | import type { Cell, Node } from '../types'; 2 | import { isRow } from '../types'; 3 | import { createId } from './createId'; 4 | import { mapNode } from './mapNode'; 5 | 6 | export const cloneWithNewIds = (node: Node): Node => { 7 | return mapNode(node, { 8 | mapCell: (n) => ({ 9 | ...n, 10 | // clone data as well 11 | dataI18n: n?.dataI18n ? JSON.parse(JSON.stringify(n.dataI18n)) : {}, 12 | id: createId(), 13 | }), 14 | mapRow: (n) => ({ 15 | ...n, 16 | id: createId(), 17 | }), 18 | }) as Node; 19 | }; 20 | 21 | export const cloneAsCell = (node: Node): Cell => { 22 | const cell = isRow(node) 23 | ? { 24 | id: createId(), 25 | rows: [node], 26 | } 27 | : node; 28 | return cloneWithNewIds(cell) as Cell; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/editor/src/core/utils/createId.ts: -------------------------------------------------------------------------------- 1 | // import ShortUniqueId from 'short-unique-id'; 2 | // REMOVED BECAUSE OF https://github.com/jeanlescure/short-unique-id/issues/31 3 | 4 | // we do not need cryptographic save unique ids, so this poor mans solution is probably ok: 5 | 6 | const LENGTH = 6; 7 | export const createId = () => Math.random().toString(36).substr(2, LENGTH); 8 | -------------------------------------------------------------------------------- /packages/editor/src/core/utils/createValue.ts: -------------------------------------------------------------------------------- 1 | import type { PluginsAndLang } from '../actions/cell/insert'; 2 | import { createRow } from '../actions/cell/insert'; 3 | import { CURRENT_EDITABLE_VERSION } from '../migrations/EDITABLE_MIGRATIONS'; 4 | import type { Value, PartialRow } from '../types/node'; 5 | import { createId } from './createId'; 6 | 7 | type PartialValue = { 8 | id?: string; 9 | rows?: PartialRow[]; 10 | }; 11 | export const createValue = ( 12 | partial: PartialValue, 13 | options: PluginsAndLang 14 | ): Value => { 15 | return { 16 | id: partial.id || createId(), 17 | rows: partial.rows?.map((c) => createRow(c, options)) ?? [], 18 | version: CURRENT_EDITABLE_VERSION, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/editor/src/core/utils/deepEquals.ts: -------------------------------------------------------------------------------- 1 | import equals from 'fast-deep-equal'; 2 | 3 | export default equals as <T>(a: T, b: T) => boolean; 4 | -------------------------------------------------------------------------------- /packages/editor/src/core/utils/getCellData.ts: -------------------------------------------------------------------------------- 1 | import type { Cell } from '../types'; 2 | 3 | export const getCellData = ( 4 | cell: null | Pick<Cell, 'dataI18n'>, 5 | lang: string 6 | ) => { 7 | const dataI18n = cell?.dataI18n; 8 | 9 | return ( 10 | dataI18n?.[lang] ?? 11 | // find first non-empty 12 | dataI18n?.[Object.keys(dataI18n).find((l) => dataI18n[l]) ?? 'default'] ?? 13 | {} 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/editor/src/core/utils/getCellSpacing.ts: -------------------------------------------------------------------------------- 1 | import type { CellPlugin, CellSpacing, DataTType } from '../types'; 2 | 3 | export const getPluginCellSpacing = <DataT extends DataTType>( 4 | plugin: CellPlugin<DataT> | null, 5 | data: DataT 6 | ): number | CellSpacing | null => 7 | plugin?.cellSpacing 8 | ? typeof plugin?.cellSpacing === 'function' 9 | ? plugin?.cellSpacing(data) 10 | : plugin?.cellSpacing 11 | : null; 12 | 13 | export const normalizeCellSpacing = ( 14 | cellSpacing: null | number | CellSpacing = 0 15 | ): CellSpacing => { 16 | if (!cellSpacing) { 17 | return { x: 0, y: 0 }; 18 | } 19 | if (['number', 'string'].indexOf(typeof cellSpacing) !== -1) { 20 | return { x: +cellSpacing || 0, y: +cellSpacing || 0 }; 21 | } else { 22 | return { 23 | x: +(cellSpacing as CellSpacing).x || 0, 24 | y: +(cellSpacing as CellSpacing).y || 0, 25 | }; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /packages/editor/src/core/utils/objIsNode.ts: -------------------------------------------------------------------------------- 1 | import type { Node } from '../types'; 2 | 3 | // poor check 4 | export const objIsNode = (obj: Record<string, unknown>): obj is Node => { 5 | if (!obj) return false; 6 | if (!('id' in obj)) { 7 | return false; 8 | } 9 | return true; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/editor/src/core/utils/removeUndefinedProps.test.ts: -------------------------------------------------------------------------------- 1 | import { removeUndefinedProps } from './removeUndefinedProps'; 2 | 3 | describe('removeUndefinedProps', () => { 4 | it('should remove undefined and null properties from object', () => { 5 | const obj = { 6 | a: 'a', 7 | b: undefined, 8 | c: 'something', 9 | d: null, 10 | }; 11 | expect(removeUndefinedProps(obj)).toEqual({ 12 | a: 'a', 13 | c: 'something', 14 | }); 15 | }); 16 | 17 | it('does not touch nested stuff', () => { 18 | const obj = { 19 | a: 'a', 20 | b: undefined, 21 | c: 'something', 22 | d: { 23 | some: undefined, 24 | bar: 'bar', 25 | }, 26 | }; 27 | expect(removeUndefinedProps(obj)).toEqual({ 28 | a: 'a', 29 | c: 'something', 30 | d: { 31 | some: undefined, 32 | bar: 'bar', 33 | }, 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/editor/src/core/utils/removeUndefinedProps.ts: -------------------------------------------------------------------------------- 1 | export const removeUndefinedProps = <T extends { [key: string]: unknown }>( 2 | obj: T 3 | ): T => 4 | Object.keys(obj).reduce((acc, key) => { 5 | const value = obj[key]; 6 | if (typeof value === 'undefined' || value == null) { 7 | return acc; 8 | } 9 | return { 10 | ...acc, 11 | [key]: value, 12 | }; 13 | }, {} as T); 14 | -------------------------------------------------------------------------------- /packages/editor/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | @import "variables.css"; 3 | @import "core/index.css"; 4 | @import "ui/index.css"; 5 | 6 | -------------------------------------------------------------------------------- /packages/editor/src/index.tsx: -------------------------------------------------------------------------------- 1 | //import './wdyr'; 2 | export * from './core/types'; 3 | export * from './core/components/hooks'; 4 | export * from './ui'; 5 | 6 | import lazyLoad from './core/helper/lazyLoad'; 7 | import { Migration } from './core/migrations/Migration'; 8 | 9 | import Editor, { EditorProps } from './editor/Editor'; 10 | import makeUniformsSchema from './ui/AutoformControls/makeUniformsSchema'; 11 | 12 | import { migrateValue } from './core/migrations/migrate'; 13 | import deepEquals from './core/utils/deepEquals'; 14 | 15 | import { createValue } from './core/utils/createValue'; 16 | import { objIsNode } from './core/utils/objIsNode'; 17 | import { getTextContents } from './core/utils/getTextContents'; 18 | export { objIsNode }; 19 | export { lazyLoad }; 20 | export { EditorProps }; 21 | export { Migration }; 22 | export { makeUniformsSchema }; 23 | export { createValue, getTextContents }; 24 | export { migrateValue }; 25 | 26 | export { deepEquals }; 27 | export default Editor; 28 | 29 | export const VERSION = '###VERSION###'; 30 | -------------------------------------------------------------------------------- /packages/editor/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'color-parse' { 2 | function parse(color: string): { 3 | space: 'hsl' | 'rgb'; 4 | values: [number, number, number]; 5 | alpha: number; 6 | }; 7 | export default parse; 8 | } 9 | -------------------------------------------------------------------------------- /packages/editor/src/ui/AutoformControls/AutoField.tsx: -------------------------------------------------------------------------------- 1 | import { AutoField } from '../uniform-mui'; 2 | 3 | export default AutoField; 4 | -------------------------------------------------------------------------------- /packages/editor/src/ui/AutoformControls/AutoFieldContext.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | import { AutoField } from '../uniform-mui'; 4 | 5 | const AutofieldContextProvider: FC<PropsWithChildren> = ({ children }) => ( 6 | <AutoField.componentDetectorContext.Provider 7 | value={(props, uniforms) => { 8 | const show = props.showIf?.(uniforms.model) ?? true; 9 | if (!show) return () => null; 10 | 11 | // see https://github.com/react-page/react-page/issues/1187 12 | // we remap props.component to props._customComponent to avoid the underlying issue in uniforms 13 | if (props._customComponent) { 14 | return props._customComponent; 15 | } 16 | return AutoField.defaultComponentDetector(props, uniforms); 17 | }} 18 | > 19 | {children} 20 | </AutoField.componentDetectorContext.Provider> 21 | ); 22 | 23 | export default AutofieldContextProvider; 24 | -------------------------------------------------------------------------------- /packages/editor/src/ui/AutoformControls/AutoFields.tsx: -------------------------------------------------------------------------------- 1 | import { AutoFields } from '../uniform-mui'; 2 | 3 | export default AutoFields; 4 | -------------------------------------------------------------------------------- /packages/editor/src/ui/AutoformControls/AutoForm.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { PropsWithChildren, ReactNode } from 'react'; 3 | import React, { forwardRef } from 'react'; 4 | import type { AutoFormProps } from 'uniforms'; 5 | import { AutoForm } from 'uniforms'; 6 | import AutofieldContextProvider from './AutoFieldContext'; 7 | 8 | type OptionalFields = 9 | | 'autosaveDelay' 10 | | 'error' 11 | | 'label' 12 | | 'noValidate' 13 | | 'onValidate' 14 | | 'validate' 15 | | 'autosave'; 16 | type Props = Omit<AutoFormProps<unknown>, OptionalFields> & 17 | Partial<AutoFormProps<unknown>>; 18 | 19 | export default forwardRef<any, PropsWithChildren<Props>>( 20 | (props: Props, ref) => ( 21 | <AutofieldContextProvider> 22 | <AutoForm {...props} ref={ref as any} /> 23 | </AutofieldContextProvider> 24 | ) 25 | ); 26 | -------------------------------------------------------------------------------- /packages/editor/src/ui/BottomToolbar/MoveActions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MoveLeft, MoveRight, MoveDown, MoveUp } from '../moveButtons'; 3 | const MoveActions: React.FC<{ nodeId: string }> = ({ nodeId }) => { 4 | return ( 5 | <div style={{ transform: 'scale(0.8)' }}> 6 | <MoveLeft nodeId={nodeId} /> 7 | <MoveUp nodeId={nodeId} /> 8 | 9 | <MoveDown nodeId={nodeId} /> 10 | <MoveRight nodeId={nodeId} /> 11 | </div> 12 | ); 13 | }; 14 | 15 | export default MoveActions; 16 | -------------------------------------------------------------------------------- /packages/editor/src/ui/BottomToolbar/ScaleButton.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Tooltip } from '@mui/material'; 2 | import ScaleIcon from '@mui/icons-material/AspectRatio'; 3 | import React from 'react'; 4 | import { useUiTranslator } from '../../core/components/hooks'; 5 | const SCALING_FACTORS = [1, 0.8, 0.6, 1.2]; 6 | let lastScale = SCALING_FACTORS[0]; // poor mans redux 7 | 8 | export const ScaleButton: React.FC<{ 9 | scale: number; 10 | setScale: (s: number) => void; 11 | }> = ({ scale, setScale }) => { 12 | const { t } = useUiTranslator(); 13 | const toggleScale = React.useCallback(() => { 14 | const newScalingFactor = 15 | SCALING_FACTORS[ 16 | (SCALING_FACTORS.indexOf(lastScale ?? scale) + 1) % 17 | SCALING_FACTORS.length 18 | ]; 19 | setScale(newScalingFactor); 20 | // poor man's redux 21 | lastScale = newScalingFactor; 22 | }, [scale, lastScale, setScale]); 23 | return ( 24 | <Tooltip title={t('Change size of this window') ?? ''}> 25 | <IconButton 26 | onClick={toggleScale} 27 | aria-label="Change size of this window" 28 | color="primary" 29 | > 30 | <ScaleIcon /> 31 | </IconButton> 32 | </Tooltip> 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/editor/src/ui/BottomToolbar/types.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | export type BottomToolbarProps = { 3 | open?: boolean; 4 | style?: React.CSSProperties; 5 | children?: React.ReactNode; 6 | className?: string; 7 | 8 | anchor?: 'top' | 'bottom' | 'left' | 'right'; 9 | pluginControls?: ReactNode; 10 | actionsLeft?: ReactNode; 11 | } & BottomToolbarToolsProps; 12 | 13 | export type BottomToolbarToolsProps = { 14 | nodeId: string; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/editor/src/ui/ColorPicker/ColorPickerField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connectField } from 'uniforms'; 3 | import ColorPicker from './ColorPicker'; 4 | import { colorToString, stringToColor } from './colorToString'; 5 | import { useUiTranslator } from '../../core/components/hooks'; 6 | 7 | const ColorPickerField = connectField<{ 8 | value: string; 9 | label: string; 10 | onChange: (v: string | void) => void; 11 | }>((props) => { 12 | const { t } = useUiTranslator(); 13 | return ( 14 | <ColorPicker 15 | style={{ marginBottom: 8 }} 16 | color={stringToColor(props.value)} 17 | buttonContent={t(props.label) ?? ''} 18 | onChange={(v) => { 19 | props.onChange(colorToString(v)); 20 | }} 21 | /> 22 | ); 23 | }); 24 | 25 | /** 26 | * A component that can be used in autoforms (uniforms) 27 | */ 28 | export default ColorPickerField; 29 | -------------------------------------------------------------------------------- /packages/editor/src/ui/ColorPicker/colorToString.tsx: -------------------------------------------------------------------------------- 1 | import type { RGBColor } from './types'; 2 | import parse from 'color-parse'; 3 | export const colorToString = (c?: RGBColor | null) => 4 | c ? 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + c.a + ')' : undefined; 5 | 6 | export const stringToColor = (c: string) => { 7 | const match = parse(c); 8 | 9 | if (!match || match.space !== 'rgb') return null; 10 | return { 11 | r: match.values[0], 12 | g: match.values[1], 13 | b: match.values[2], 14 | a: match.alpha, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/editor/src/ui/ColorPicker/index.tsx: -------------------------------------------------------------------------------- 1 | import lazyLoad from '../../core/helper/lazyLoad'; 2 | export const ColorPicker = lazyLoad(() => import('./ColorPicker')); 3 | export const ColorPickerField = lazyLoad(() => import('./ColorPickerField')); 4 | export * from './colorToString'; 5 | export * from './types'; 6 | -------------------------------------------------------------------------------- /packages/editor/src/ui/ColorPicker/types.ts: -------------------------------------------------------------------------------- 1 | import { RGBColor } from 'react-color'; 2 | 3 | export interface ColorPickerProps { 4 | onChange: (color: RGBColor) => void; 5 | onChangeComplete: (color: RGBColor) => void; 6 | color?: RGBColor | null; 7 | buttonContent?: JSX.Element | string; 8 | icon?: JSX.Element | string; 9 | onDialogOpen?: () => void; 10 | style?: React.CSSProperties; 11 | } 12 | 13 | export type ColorPickerState = { 14 | isColorPickerVisible: boolean; 15 | }; 16 | 17 | export { RGBColor }; 18 | -------------------------------------------------------------------------------- /packages/editor/src/ui/DuplicateButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Tooltip } from '@mui/material'; 2 | import Icon from '@mui/icons-material/FileCopy'; 3 | import React from 'react'; 4 | import { useDuplicateCell, useUiTranslator } from '../../core/components/hooks'; 5 | 6 | export const DuplicateButton: React.FC<{ nodeId: string }> = React.memo( 7 | ({ nodeId }) => { 8 | const duplicateCell = useDuplicateCell(nodeId); 9 | const { t } = useUiTranslator(); 10 | return ( 11 | <Tooltip title={t('Duplicate Plugin') ?? ''}> 12 | <IconButton onClick={duplicateCell} aria-label="delete" color="default"> 13 | <Icon /> 14 | </IconButton> 15 | </Tooltip> 16 | ); 17 | } 18 | ); 19 | -------------------------------------------------------------------------------- /packages/editor/src/ui/EditorUI/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { PluginDrawer } from '../PluginDrawer'; 4 | import { Trash } from '../Trash'; 5 | import type { StickyNess } from '../Sidebar'; 6 | import { Sidebar } from '../Sidebar'; 7 | import { useOption } from '../../core/components/hooks'; 8 | import { MultiNodesBottomToolbar } from '../MultiNodesBottomToolbar'; 9 | 10 | export default React.memo( 11 | ({ 12 | stickyNess = { 13 | shouldStickToTop: false, 14 | shouldStickToBottom: false, 15 | rightOffset: 0, 16 | rightOffsetFixed: 0, 17 | }, 18 | }: { 19 | stickyNess?: StickyNess; 20 | }) => { 21 | const hideEditorSidebar = useOption('hideEditorSidebar'); 22 | return ( 23 | <> 24 | <Trash /> 25 | {!hideEditorSidebar && <Sidebar stickyNess={stickyNess} />} 26 | <PluginDrawer /> 27 | <MultiNodesBottomToolbar /> 28 | </> 29 | ); 30 | } 31 | ); 32 | -------------------------------------------------------------------------------- /packages/editor/src/ui/I18nTools/SelectLang.tsx: -------------------------------------------------------------------------------- 1 | import { Select, MenuItem } from '@mui/material'; 2 | import React, { memo } from 'react'; 3 | import { useLang, useOption, useSetLang } from '../../core/components/hooks'; 4 | 5 | const SelectLang = () => { 6 | const languages = useOption('languages'); 7 | const lang = useLang(); 8 | const setLang = useSetLang(); 9 | if (languages && languages?.length > 0) { 10 | return ( 11 | <Select 12 | variant="standard" 13 | value={lang || ''} 14 | onChange={(e) => setLang(e.target.value as string)} 15 | > 16 | {languages.map((l) => ( 17 | <MenuItem key={l.lang} value={l.lang}> 18 | {l.label} 19 | </MenuItem> 20 | ))} 21 | </Select> 22 | ); 23 | } 24 | return null; 25 | }; 26 | 27 | export default memo(SelectLang); 28 | -------------------------------------------------------------------------------- /packages/editor/src/ui/I18nTools/index.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Dialog } from '@mui/material'; 2 | import Translate from '@mui/icons-material/Translate'; 3 | 4 | import React, { useState } from 'react'; 5 | import SelectLang from './SelectLang'; 6 | import I18nDialog from './I18nDialog'; 7 | import { useOption } from '../../core/components/hooks'; 8 | 9 | export const I18nTools: React.FC<{ 10 | nodeId: string; 11 | }> = React.memo(({ nodeId }) => { 12 | const languages = useOption('languages'); 13 | 14 | const [showI18nDialog, setShowI18nDialog] = useState(false); 15 | const hasI18n = languages && languages?.length > 0; 16 | const onClose = () => setShowI18nDialog(false); 17 | if (!hasI18n) { 18 | return null; 19 | } 20 | 21 | return ( 22 | <> 23 | <Dialog open={showI18nDialog} onClose={onClose}> 24 | <I18nDialog nodeId={nodeId} onClose={onClose} /> 25 | </Dialog> 26 | 27 | <div style={{ display: 'flex', alignItems: 'center' }}> 28 | <IconButton 29 | onClick={() => setShowI18nDialog(true)} 30 | aria-label="i18n" 31 | color="secondary" 32 | > 33 | <Translate /> 34 | </IconButton> 35 | 36 | <SelectLang /> 37 | </div> 38 | </> 39 | ); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/editor/src/ui/ImageUpload/defaultTranslations.tsx: -------------------------------------------------------------------------------- 1 | export const defaultTranslations = { 2 | buttonContent: 'Upload image', 3 | noFileError: 'No file selected', 4 | badExtensionError: 'Bad file type', 5 | tooBigError: 'Too big', 6 | uploadingError: 'Error while uploading', 7 | unknownError: 'Unknown error', 8 | }; 9 | -------------------------------------------------------------------------------- /packages/editor/src/ui/ImageUpload/index.tsx: -------------------------------------------------------------------------------- 1 | import lazyLoad from '../../core/helper/lazyLoad'; 2 | 3 | export * from './types'; 4 | 5 | export const ImageUpload = lazyLoad(() => import('./ImageUpload')); 6 | -------------------------------------------------------------------------------- /packages/editor/src/ui/ImageUpload/types.tsx: -------------------------------------------------------------------------------- 1 | export type ImageLoaded = { 2 | file: File; 3 | dataUrl: string; 4 | }; 5 | 6 | export type ImageUploaded = { 7 | url: string; 8 | }; 9 | export type ImageUploadType = ( 10 | file: File, 11 | reportProgress: (progress: number) => void 12 | ) => Promise<ImageUploaded>; 13 | 14 | export type ImageUploadProps = { 15 | imageLoaded?: (image: ImageLoaded) => void; 16 | imageUpload: ImageUploadType; 17 | imageUploadError?: (errorCode: number) => void; 18 | imageUploaded: (resp: ImageUploaded) => void; 19 | icon?: JSX.Element; 20 | style?: React.CSSProperties; 21 | maxFileSize?: number; 22 | allowedExtensions?: string[]; 23 | translations?: { [key: string]: string }; 24 | }; 25 | 26 | export type ImageUploadState = { 27 | isUploading: boolean; 28 | hasError: boolean; 29 | errorText: string | null; 30 | progress?: number; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/editor/src/ui/MultiNodesBottomToolbar/DeleteAll.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconButton, Tooltip } from '@mui/material'; 3 | import Delete from '@mui/icons-material/Delete'; 4 | import { 5 | useAllFocusedNodeIds, 6 | useRemoveMultipleNodeIds, 7 | useUiTranslator, 8 | } from '../../core/components/hooks'; 9 | 10 | const DeleteAll: React.FC = () => { 11 | const remove = useRemoveMultipleNodeIds(); 12 | const focused = useAllFocusedNodeIds(); 13 | const { t } = useUiTranslator(); 14 | return ( 15 | <Tooltip title={t('Remove all selected') ?? ''}> 16 | <IconButton 17 | onClick={() => remove(focused)} 18 | aria-label="delete" 19 | color="secondary" 20 | > 21 | <Delete /> 22 | </IconButton> 23 | </Tooltip> 24 | ); 25 | }; 26 | 27 | export default DeleteAll; 28 | -------------------------------------------------------------------------------- /packages/editor/src/ui/MultiNodesBottomToolbar/DuplicateAll.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Tooltip } from '@mui/material'; 2 | import React from 'react'; 3 | import { 4 | useAllFocusedNodeIds, 5 | useDuplicateMultipleCells, 6 | useUiTranslator, 7 | } from '../../core/components/hooks'; 8 | import Icon from '@mui/icons-material/FileCopy'; 9 | 10 | const DuplicateAll: React.FC = () => { 11 | const duplicate = useDuplicateMultipleCells(); 12 | const { t } = useUiTranslator(); 13 | const nodeIds = useAllFocusedNodeIds(); 14 | return ( 15 | <Tooltip title={t('Duplicate al') ?? ''}> 16 | <IconButton 17 | onClick={() => duplicate(nodeIds)} 18 | aria-label="delete" 19 | color="default" 20 | > 21 | <Icon /> 22 | </IconButton> 23 | </Tooltip> 24 | ); 25 | }; 26 | 27 | export default DuplicateAll; 28 | -------------------------------------------------------------------------------- /packages/editor/src/ui/PluginDrawer/Item/index.css: -------------------------------------------------------------------------------- 1 | .react-page-plugin-drawer-item { 2 | cursor: pointer; 3 | 4 | z-index: 1; 5 | } 6 | .react-page-plugin-drawer-item:hover { 7 | z-index: 2; 8 | box-shadow: 0 0 20px #ccc; 9 | } 10 | -------------------------------------------------------------------------------- /packages/editor/src/ui/PluginDrawer/index.css: -------------------------------------------------------------------------------- 1 | @import 'Item/index.css'; 2 | -------------------------------------------------------------------------------- /packages/editor/src/ui/SelectParentButton/index.tsx: -------------------------------------------------------------------------------- 1 | import IconButton from '@mui/material/IconButton'; 2 | import VerticalAlignTopIcon from '@mui/icons-material/VerticalAlignTop'; 3 | 4 | import React from 'react'; 5 | import { 6 | useFocusCell, 7 | useParentCellId, 8 | useUiTranslator, 9 | } from '../../core/components/hooks'; 10 | 11 | export const SelectParentButton: React.FC<{ 12 | nodeId: string; 13 | }> = React.memo(({ nodeId }) => { 14 | const parentCellId = useParentCellId(nodeId); 15 | const { t } = useUiTranslator(); 16 | const focusParent = useFocusCell(parentCellId); 17 | 18 | return parentCellId ? ( 19 | <IconButton 20 | className="bottomToolbar__selectParentButton" 21 | onClick={() => focusParent()} 22 | color="default" 23 | title={t('Select parent') ?? ''} 24 | > 25 | <VerticalAlignTopIcon /> 26 | </IconButton> 27 | ) : null; 28 | }); 29 | -------------------------------------------------------------------------------- /packages/editor/src/ui/Sidebar/Button/index.css: -------------------------------------------------------------------------------- 1 | .react-page-controls-mode-toggle-button-inner { 2 | float: right; 3 | margin: 8px; 4 | } 5 | 6 | .react-page-controls-mode-toggle-button-description { 7 | font-family: Roboto, sans-serif; 8 | font-size: 16px; 9 | margin-top: 18px; 10 | float: right; 11 | background: transparent; 12 | color: transparent; 13 | border: 1px transparent solid; 14 | padding: 2px 8px; 15 | text-align: right; 16 | display: none; 17 | transition: all 200ms ease; 18 | white-space: nowrap; 19 | overflow: hidden; 20 | } 21 | 22 | .react-page-controls-mode-toggle-button:hover 23 | .react-page-controls-mode-toggle-button-description { 24 | max-width: 999px; 25 | background: var(--darkBlack); 26 | color: var(--white); 27 | display: block; 28 | border: 1px solid var(--faintBlack); 29 | } 30 | -------------------------------------------------------------------------------- /packages/editor/src/ui/Sidebar/ToggleEdit/index.tsx: -------------------------------------------------------------------------------- 1 | import Create from '@mui/icons-material/Create'; 2 | 3 | import React from 'react'; 4 | import { useIsEditMode, useSetEditMode } from '../../../core/components/hooks'; 5 | import Button from '../Button/index'; 6 | 7 | type Props = { 8 | label: string; 9 | }; 10 | 11 | const ToggleEdit: React.FC<Props> = ({ label }) => { 12 | const isEditMode = useIsEditMode(); 13 | const setEditMode = useSetEditMode(); 14 | return ( 15 | <Button 16 | icon={<Create />} 17 | description={label} 18 | active={isEditMode} 19 | onClick={setEditMode} 20 | /> 21 | ); 22 | }; 23 | 24 | export default React.memo(ToggleEdit); 25 | -------------------------------------------------------------------------------- /packages/editor/src/ui/Sidebar/ToggleInsert/index.tsx: -------------------------------------------------------------------------------- 1 | import ContentAdd from '@mui/icons-material/Add'; 2 | import React from 'react'; 3 | import { 4 | useIsInsertMode, 5 | useSetInsertMode, 6 | } from '../../../core/components/hooks'; 7 | import Button from '../Button/index'; 8 | 9 | type Props = { 10 | label: string; 11 | }; 12 | const ToggleInsert: React.FC<Props> = ({ label }) => { 13 | const isInsertMode = useIsInsertMode(); 14 | const setInsertMode = useSetInsertMode(); 15 | return ( 16 | <Button 17 | icon={<ContentAdd />} 18 | description={label} 19 | active={isInsertMode} 20 | onClick={setInsertMode} 21 | /> 22 | ); 23 | }; 24 | 25 | export default React.memo(ToggleInsert); 26 | -------------------------------------------------------------------------------- /packages/editor/src/ui/Sidebar/ToggleLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import ViewQuilt from '@mui/icons-material/ViewQuilt'; 2 | import React from 'react'; 3 | import { 4 | useIsLayoutMode, 5 | useSetLayoutMode, 6 | } from '../../../core/components/hooks'; 7 | import Button from '../Button'; 8 | type Props = { 9 | label: string; 10 | }; 11 | 12 | const ToggleLayout: React.FC<Props> = ({ label }) => { 13 | const isLayoutMode = useIsLayoutMode(); 14 | const setLayoutMode = useSetLayoutMode(); 15 | return ( 16 | <Button 17 | icon={<ViewQuilt />} 18 | description={label} 19 | active={isLayoutMode} 20 | onClick={setLayoutMode} 21 | /> 22 | ); 23 | }; 24 | 25 | export default React.memo(ToggleLayout); 26 | -------------------------------------------------------------------------------- /packages/editor/src/ui/Sidebar/TogglePreview/index.tsx: -------------------------------------------------------------------------------- 1 | import Devices from '@mui/icons-material/Devices'; 2 | import React from 'react'; 3 | import { 4 | useIsPreviewMode, 5 | useSetPreviewMode, 6 | } from '../../../core/components/hooks'; 7 | import Button from '../Button/index'; 8 | 9 | type Props = { 10 | label: string; 11 | }; 12 | const TogglePreview: React.FC<Props> = ({ label }) => { 13 | const isPreviewMode = useIsPreviewMode(); 14 | const setIsPreviewMode = useSetPreviewMode(); 15 | return ( 16 | <Button 17 | icon={<Devices />} 18 | description={label} 19 | active={isPreviewMode} 20 | onClick={setIsPreviewMode} 21 | /> 22 | ); 23 | }; 24 | 25 | export default React.memo(TogglePreview); 26 | -------------------------------------------------------------------------------- /packages/editor/src/ui/Sidebar/ToggleResize/index.tsx: -------------------------------------------------------------------------------- 1 | import Resize from '@mui/icons-material/SettingsOverscan'; 2 | import React from 'react'; 3 | import { 4 | useIsResizeMode, 5 | useSetResizeMode, 6 | } from '../../../core/components/hooks'; 7 | import Button from '../Button/index'; 8 | 9 | type Props = { 10 | label: string; 11 | }; 12 | 13 | const ToggleResize: React.FC<Props> = (props) => { 14 | const isResizeMode = useIsResizeMode(); 15 | const setResizeMode = useSetResizeMode(); 16 | return ( 17 | <Button 18 | icon={<Resize />} 19 | description={props.label} 20 | active={isResizeMode} 21 | onClick={setResizeMode} 22 | /> 23 | ); 24 | }; 25 | 26 | export default React.memo(ToggleResize); 27 | -------------------------------------------------------------------------------- /packages/editor/src/ui/Sidebar/index.css: -------------------------------------------------------------------------------- 1 | @import 'Button/index.css'; 2 | 3 | .react-page-controls-mode-toggle-clearfix { 4 | clear: both; 5 | } 6 | 7 | @keyframes fadeIn { 8 | 0% { 9 | opacity: 0; 10 | transform: scale(0); 11 | } 12 | 80% { 13 | opacity: 1; 14 | transform: scale(1.05); 15 | } 16 | 100% { 17 | opacity: 1; 18 | transform: scale(1); 19 | } 20 | } 21 | 22 | .react-page-controls-mode-toggle-control { 23 | z-index: 1; 24 | animation: fadeIn 0.8s forwards; 25 | opacity: 0; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /packages/editor/src/ui/Trash/index.css: -------------------------------------------------------------------------------- 1 | .react-page-controls-trash { 2 | position: fixed; 3 | bottom: -64px; 4 | z-index: 500; 5 | left: 50%; 6 | transition: bottom 200ms ease; 7 | padding: 8px; 8 | } 9 | 10 | .react-page-controls-trash.react-page-controls-trash-active { 11 | bottom: 16px; 12 | } 13 | -------------------------------------------------------------------------------- /packages/editor/src/ui/Trash/index.tsx: -------------------------------------------------------------------------------- 1 | import Fab from '@mui/material/Fab'; 2 | import Delete from '@mui/icons-material/Delete'; 3 | import classNames from 'classnames'; 4 | import React from 'react'; 5 | import { useIsLayoutMode, useTrashDrop } from '../../core/components/hooks'; 6 | 7 | export const Trash: React.FC = React.memo(() => { 8 | const isLayoutMode = useIsLayoutMode(); 9 | const [{ isHovering }, ref] = useTrashDrop(); 10 | return ( 11 | <div 12 | ref={ref} 13 | className={classNames('react-page-controls-trash', { 14 | 'react-page-controls-trash-active': isLayoutMode, 15 | })} 16 | > 17 | <Fab color="secondary" disabled={!isHovering}> 18 | <Delete /> 19 | </Fab> 20 | </div> 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/editor/src/ui/defaultTheme/index.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOptions } from '@mui/material'; 2 | import { createTheme } from '@mui/material'; 3 | 4 | export const defaultThemeOptions: ThemeOptions = { 5 | components: { 6 | MuiTextField: { 7 | defaultProps: { 8 | variant: 'standard', 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export const defaultTheme = createTheme(defaultThemeOptions); 15 | -------------------------------------------------------------------------------- /packages/editor/src/ui/index.css: -------------------------------------------------------------------------------- 1 | @import 'Sidebar/index.css'; 2 | @import 'PluginDrawer/index.css'; 3 | @import 'Trash/index.css'; 4 | -------------------------------------------------------------------------------- /packages/editor/src/ui/index.tsx: -------------------------------------------------------------------------------- 1 | // re-exports for customization 2 | // TODO: add more if required 3 | export * from './BottomToolbar'; 4 | export * from './AutoformControls'; 5 | 6 | export * from './I18nTools'; 7 | export * from './DuplicateButton'; 8 | export * from './Trash'; 9 | export * from './ImageUpload'; 10 | export * from './SelectParentButton'; 11 | export * from './PluginDrawer'; 12 | 13 | export * from './ColorPicker'; 14 | export * from './Sidebar'; 15 | 16 | export * from './defaultTheme'; 17 | -------------------------------------------------------------------------------- /packages/editor/src/ui/uniform-mui/AutoField.tsx: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import { createAutoField } from 'uniforms'; 3 | export { AutoFieldProps } from 'uniforms'; 4 | 5 | import BoolField from './BoolField'; 6 | import DateField from './DateField'; 7 | import ListField from './ListField'; 8 | import NestField from './NestField'; 9 | import NumField from './NumField'; 10 | import RadioField from './RadioField'; 11 | import SelectField from './SelectField'; 12 | import TextField from './TextField'; 13 | 14 | const AutoField = createAutoField((props) => { 15 | if (props.allowedValues) { 16 | return props.checkboxes && props.fieldType !== Array 17 | ? RadioField 18 | : SelectField; 19 | } 20 | 21 | switch (props.fieldType) { 22 | case Array: 23 | return ListField; 24 | case Boolean: 25 | return BoolField; 26 | case Date: 27 | return DateField; 28 | case Number: 29 | return NumField; 30 | case Object: 31 | return NestField; 32 | case String: 33 | return TextField; 34 | } 35 | 36 | return invariant(false, 'Unsupported field type: %s', props.fieldType); 37 | }); 38 | 39 | export default AutoField; 40 | -------------------------------------------------------------------------------- /packages/editor/src/ui/uniform-mui/AutoFields.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentType } from 'react'; 2 | import { createElement } from 'react'; 3 | import { useForm } from 'uniforms'; 4 | 5 | import AutoField from './AutoField'; 6 | 7 | export type AutoFieldsProps = { 8 | autoField?: ComponentType<{ name: string }>; 9 | element?: ComponentType | string; 10 | fields?: string[]; 11 | omitFields?: string[]; 12 | showInlineError?: boolean; 13 | }; 14 | 15 | export default function AutoFields({ 16 | autoField = AutoField, 17 | element = 'div', 18 | fields, 19 | omitFields = [], 20 | showInlineError, 21 | ...props 22 | }: AutoFieldsProps) { 23 | const { schema } = useForm(); 24 | 25 | return createElement( 26 | element, 27 | props, 28 | (fields ?? schema.getSubfields()) 29 | .filter((field) => !omitFields.includes(field)) 30 | .map((field) => 31 | createElement( 32 | autoField, 33 | Object.assign( 34 | { key: field, name: field }, 35 | showInlineError === undefined ? null : { showInlineError } 36 | ) 37 | ) 38 | ) 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /packages/editor/src/ui/uniform-mui/ErrorField.tsx: -------------------------------------------------------------------------------- 1 | import FormControl from '@mui/material/FormControl'; 2 | import type { FormHelperTextProps } from '@mui/material/FormHelperText'; 3 | import FormHelperText from '@mui/material/FormHelperText'; 4 | import React from 'react'; 5 | import type { Override } from 'uniforms'; 6 | import { connectField, filterDOMProps } from 'uniforms'; 7 | 8 | export type ErrorFieldProps = Override< 9 | FormHelperTextProps, 10 | { 11 | errorMessage?: string; 12 | fullWidth?: boolean; 13 | margin?: 'dense' | 'normal' | 'none'; 14 | } 15 | >; 16 | 17 | function Error({ 18 | children, 19 | error, 20 | errorMessage, 21 | fullWidth, 22 | margin, 23 | variant, 24 | ...props 25 | }: ErrorFieldProps) { 26 | return !error ? null : ( 27 | <FormControl 28 | error={!!error} 29 | fullWidth={!!fullWidth} 30 | margin={margin === 'dense' ? margin : undefined} 31 | variant={variant} 32 | > 33 | <FormHelperText {...filterDOMProps(props)}> 34 | {children || errorMessage} 35 | </FormHelperText> 36 | </FormControl> 37 | ); 38 | } 39 | 40 | export default connectField<ErrorFieldProps>(Error, { 41 | initialValue: false, 42 | kind: 'leaf', 43 | }); 44 | -------------------------------------------------------------------------------- /packages/editor/src/ui/uniform-mui/HiddenField.tsx: -------------------------------------------------------------------------------- 1 | import type { HTMLProps, Ref } from 'react'; 2 | import React, { useEffect } from 'react'; 3 | import type { Override } from 'uniforms'; 4 | import { filterDOMProps, useField } from 'uniforms'; 5 | 6 | export type HiddenFieldProps = Override< 7 | HTMLProps<HTMLInputElement>, 8 | { 9 | inputRef?: Ref<HTMLInputElement>; 10 | name: string; 11 | noDOM?: boolean; 12 | value?: any; 13 | } 14 | >; 15 | 16 | export default function HiddenField({ value, ...rawProps }: HiddenFieldProps) { 17 | const props = useField(rawProps.name, rawProps, { initialValue: false })[0]; 18 | 19 | useEffect(() => { 20 | if (value !== undefined && value !== props.value) { 21 | props.onChange(value); 22 | } 23 | }); 24 | 25 | return props.noDOM ? null : ( 26 | <input 27 | disabled={props.disabled} 28 | name={props.name} 29 | readOnly={props.readOnly} 30 | ref={props.inputRef} 31 | type="hidden" 32 | value={value ?? props.value ?? ''} 33 | {...filterDOMProps(props)} 34 | /> 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/editor/src/ui/uniform-mui/LongTextField.tsx: -------------------------------------------------------------------------------- 1 | import type { TextFieldProps } from '@mui/material/TextField'; 2 | import TextField from '@mui/material/TextField'; 3 | import React from 'react'; 4 | import type { FieldProps } from 'uniforms'; 5 | import { connectField, filterDOMProps } from 'uniforms'; 6 | 7 | export type LongTextFieldProps = FieldProps<string, TextFieldProps>; 8 | 9 | const LongText = ({ 10 | disabled, 11 | error, 12 | errorMessage, 13 | helperText, 14 | inputRef, 15 | label, 16 | name, 17 | onChange, 18 | placeholder, 19 | readOnly, 20 | showInlineError, 21 | type = 'text', 22 | value, 23 | ...props 24 | }: LongTextFieldProps) => { 25 | return ( 26 | <TextField 27 | disabled={disabled} 28 | error={!!error} 29 | fullWidth 30 | helperText={(error && showInlineError && errorMessage) || helperText} 31 | inputProps={{ readOnly }} 32 | label={label} 33 | margin="dense" 34 | multiline 35 | name={name} 36 | onChange={(event) => disabled || onChange(event.target.value)} 37 | placeholder={placeholder} 38 | ref={inputRef} 39 | type={type} 40 | value={value ?? ''} 41 | {...filterDOMProps(props)} 42 | /> 43 | ); 44 | }; 45 | 46 | export default connectField<LongTextFieldProps>(LongText, { kind: 'leaf' }); 47 | -------------------------------------------------------------------------------- /packages/editor/src/ui/uniform-mui/NestField.tsx: -------------------------------------------------------------------------------- 1 | import FormLabel from '@mui/material/FormLabel'; 2 | import React from 'react'; 3 | import type { HTMLFieldProps } from 'uniforms'; 4 | import { connectField } from 'uniforms'; 5 | 6 | import AutoField from './AutoField'; 7 | import wrapField from './wrapField'; 8 | 9 | // FIXME: wrapField is not typed correctly. 10 | export type NestFieldProps = HTMLFieldProps< 11 | Record<string, any>, 12 | HTMLDivElement, 13 | { 14 | helperText?: string; 15 | itemProps?: Record<string, any>; 16 | fullWidth?: boolean; 17 | margin?: any; 18 | } 19 | >; 20 | 21 | function Nest({ 22 | children, 23 | fields, 24 | fullWidth = true, 25 | itemProps, 26 | label, 27 | margin = 'dense', 28 | ...props 29 | }: NestFieldProps) { 30 | return wrapField( 31 | { 32 | fullWidth, 33 | margin, 34 | ...props, 35 | component: undefined, 36 | }, 37 | label && <FormLabel component="legend">{label}</FormLabel>, 38 | children || 39 | fields.map((field) => ( 40 | <AutoField key={field} name={field} {...itemProps} /> 41 | )) 42 | ); 43 | } 44 | 45 | export default connectField<NestFieldProps>(Nest); 46 | -------------------------------------------------------------------------------- /packages/editor/src/ui/uniform-mui/README.md: -------------------------------------------------------------------------------- 1 | until https://github.com/vazco/uniforms/pull/1091 is merged 2 | -------------------------------------------------------------------------------- /packages/editor/src/ui/uniform-mui/SubmitField.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps } from '@mui/material/Button'; 2 | import Button from '@mui/material/Button'; 3 | import type { ReactNode, Ref } from 'react'; 4 | import React from 'react'; 5 | import type { Override } from 'uniforms'; 6 | import { filterDOMProps, useForm } from 'uniforms'; 7 | 8 | export type SubmitFieldProps = Override< 9 | ButtonProps, 10 | // FIXME: What kind of `ref` is it? 11 | { inputRef?: Ref<any>; label?: ReactNode } 12 | >; 13 | 14 | function SubmitField({ 15 | children, 16 | disabled, 17 | inputRef, 18 | label = 'Submit', 19 | value, 20 | ...props 21 | }: SubmitFieldProps) { 22 | const { error, state } = useForm(); 23 | 24 | return ( 25 | <Button 26 | disabled={disabled === undefined ? !!(error || state.disabled) : disabled} 27 | ref={inputRef} 28 | type="submit" 29 | value={value} 30 | variant="contained" 31 | {...filterDOMProps(props)} 32 | > 33 | {children || label} 34 | </Button> 35 | ); 36 | } 37 | 38 | export default SubmitField; 39 | -------------------------------------------------------------------------------- /packages/editor/src/ui/uniform-mui/TextField.tsx: -------------------------------------------------------------------------------- 1 | import type { TextFieldProps as MUITextFieldProps } from '@mui/material/TextField'; 2 | import TextField from '@mui/material/TextField'; 3 | import React from 'react'; 4 | import type { FieldProps } from 'uniforms'; 5 | import { connectField, filterDOMProps } from 'uniforms'; 6 | 7 | export type TextFieldProps = FieldProps<string, MUITextFieldProps>; 8 | 9 | function Text({ 10 | disabled, 11 | error, 12 | errorMessage, 13 | helperText, 14 | inputRef, 15 | label, 16 | name, 17 | onChange, 18 | placeholder, 19 | readOnly, 20 | showInlineError, 21 | type = 'text', 22 | value = '', 23 | ...props 24 | }: TextFieldProps) { 25 | return ( 26 | <TextField 27 | disabled={disabled} 28 | error={!!error} 29 | fullWidth 30 | helperText={(error && showInlineError && errorMessage) || helperText} 31 | inputProps={{ readOnly }} 32 | label={label} 33 | margin="dense" 34 | name={name} 35 | onChange={(event) => disabled || onChange(event.target.value)} 36 | placeholder={placeholder} 37 | ref={inputRef} 38 | type={type} 39 | value={value} 40 | {...filterDOMProps(props)} 41 | /> 42 | ); 43 | } 44 | 45 | export default connectField<TextFieldProps>(Text, { kind: 'leaf' }); 46 | -------------------------------------------------------------------------------- /packages/editor/src/ui/uniform-mui/wrapField.tsx: -------------------------------------------------------------------------------- 1 | import FormControl from '@mui/material/FormControl'; 2 | import FormHelperText from '@mui/material/FormHelperText'; 3 | import type { ReactNode } from 'react'; 4 | import React, { createElement } from 'react'; 5 | 6 | export default function wrapField( 7 | { 8 | component, 9 | disabled, 10 | error, 11 | errorMessage, 12 | fullWidth, 13 | helperText, 14 | margin, 15 | readOnly, 16 | required, 17 | showInlineError, 18 | variant, 19 | }: any, 20 | ...children: ReactNode[] 21 | ) { 22 | const formHelperText = showInlineError && error ? errorMessage : helperText; 23 | const props = { 24 | component, 25 | disabled: !!disabled, 26 | error: !!error, 27 | fullWidth: !!fullWidth, 28 | margin, 29 | readOnly, 30 | required, 31 | variant, 32 | }; 33 | 34 | return createElement( 35 | FormControl, 36 | props, 37 | ...children, 38 | !!formHelperText && <FormHelperText>{formHelperText}</FormHelperText> 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /packages/editor/src/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --black: #000000; 3 | --white: #ffffff; 4 | --transparent: rgba(0, 0, 0, 0); 5 | --fullBlack: rgba(0, 0, 0, 1); 6 | --darkBlack: rgba(0, 0, 0, 0.87); 7 | --lightBlack: rgba(0, 0, 0, 0.54); 8 | --minBlack: rgba(0, 0, 0, 0.26); 9 | --faintBlack: rgba(0, 0, 0, 0.12); 10 | --fullWhite: rgba(255, 255, 255, 1); 11 | --darkWhite: rgba(255, 255, 255, 0.87); 12 | --lightWhite: rgba(255, 255, 255, 0.54); 13 | --minWhite: rgba(255, 255, 255, 0.26); 14 | --faintWhite: rgba(255, 255, 255, 0.12); 15 | --grey300: #e0e0e0; 16 | --grey900: #212121; 17 | } -------------------------------------------------------------------------------- /packages/editor/src/wdyr.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | if (process.env.NODE_ENV === 'development') { 4 | const whyDidYouRender = require('@welldone-software/why-did-you-render'); 5 | whyDidYouRender(React, { 6 | trackAllPureComponents: true, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /packages/editor/tsconfig-es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib-es", 5 | "module": "esNext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/editor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib", "node_modules", "src/**/__tests__/**/*.*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/index.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-page/react-page/3bb72bd52ce0857a8f4a6ca1f15d4ce2242c89d5/packages/index.d.ts -------------------------------------------------------------------------------- /packages/plugins/README.md: -------------------------------------------------------------------------------- 1 | # ORY Editor Plugins 2 | 3 | This directory contains various plugins for the ORY Editor which are maintained by ORY. 4 | -------------------------------------------------------------------------------- /packages/plugins/content/divider/.npmignore: -------------------------------------------------------------------------------- 1 | src/**/* -------------------------------------------------------------------------------- /packages/plugins/content/divider/babel.config.js: -------------------------------------------------------------------------------- 1 | const vendor = require('../../../../babel.config.js'); 2 | module.exports = Object.assign({}, vendor); 3 | -------------------------------------------------------------------------------- /packages/plugins/content/divider/src/Renderer/DividerHtmlRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DividerHtmlRenderer: React.FC = () => { 4 | return <hr className="react-page-plugins-content-divider" />; 5 | }; 6 | 7 | export default DividerHtmlRenderer; 8 | -------------------------------------------------------------------------------- /packages/plugins/content/divider/src/createPlugin.tsx: -------------------------------------------------------------------------------- 1 | import type { CellPlugin } from '@react-page/editor'; 2 | import { lazyLoad } from '@react-page/editor'; 3 | import React from 'react'; 4 | import { defaultSettings } from './default/settings'; 5 | import DividerHtmlRenderer from './Renderer/DividerHtmlRenderer'; 6 | 7 | import type { DividerSettings } from './types/settings'; 8 | 9 | const Remove = lazyLoad(() => import('@mui/icons-material/Remove')); 10 | 11 | const createPlugin: (settings: DividerSettings) => CellPlugin = (settings) => { 12 | const mergedSettings = { ...defaultSettings, ...settings }; 13 | return { 14 | Renderer: settings.Renderer || DividerHtmlRenderer, 15 | id: 'ory/editor/core/content/divider', 16 | version: 1, 17 | icon: <Remove />, 18 | title: mergedSettings.translations?.pluginName, 19 | description: mergedSettings.translations?.pluginDescription, 20 | }; 21 | }; 22 | 23 | export default createPlugin; 24 | -------------------------------------------------------------------------------- /packages/plugins/content/divider/src/default/settings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const defaultTranslations = { 4 | pluginName: 'Divider', 5 | pluginDescription: 'A horizontal divider', 6 | }; 7 | 8 | export const defaultSettings = { 9 | translations: defaultTranslations, 10 | Renderer: () => <>Renderer for this plugin was not provided</>, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/plugins/content/divider/src/index.css: -------------------------------------------------------------------------------- 1 | .react-page-plugins-content-divider { 2 | background-color: #eee; 3 | width: 100%; 4 | height: 2px; 5 | border-color: #eee; 6 | } 7 | -------------------------------------------------------------------------------- /packages/plugins/content/divider/src/index.ts: -------------------------------------------------------------------------------- 1 | import createPlugin from './createPlugin'; 2 | import DividerHtmlRenderer from './Renderer/DividerHtmlRenderer'; 3 | 4 | const plugin = createPlugin({ 5 | Renderer: DividerHtmlRenderer, 6 | }); 7 | 8 | export default plugin; 9 | -------------------------------------------------------------------------------- /packages/plugins/content/divider/src/types/settings.ts: -------------------------------------------------------------------------------- 1 | import type { CellPluginComponentProps } from '@react-page/editor'; 2 | import type { Translations } from './translations'; 3 | 4 | export interface DividerSettings { 5 | Renderer: React.ComponentType<CellPluginComponentProps>; 6 | translations?: Translations; 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/content/divider/src/types/translations.ts: -------------------------------------------------------------------------------- 1 | import type { defaultTranslations } from '../default/settings'; 2 | 3 | export type Translations = typeof defaultTranslations; 4 | -------------------------------------------------------------------------------- /packages/plugins/content/divider/tsconfig-es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib-es", 5 | "module": "esnext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/content/divider/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib", "node_modules", "src/**/__tests__/**/*.*"], 9 | "references": [{ "path": "../../../editor" }] 10 | } 11 | -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/.npmignore: -------------------------------------------------------------------------------- 1 | src/**/* -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/babel.config.js: -------------------------------------------------------------------------------- 1 | const vendor = require('../../../../babel.config.js'); 2 | module.exports = Object.assign({}, vendor); 3 | -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/src/Renderer/Html5VideoHtmlRenderer.tsx: -------------------------------------------------------------------------------- 1 | import type { CellPluginComponentProps } from '@react-page/editor'; 2 | import React from 'react'; 3 | import { defaultHtml5VideoState } from '../default/state'; 4 | import type { Html5VideoState } from '../types/state'; 5 | 6 | const Html5VideoHtmlRenderer: React.FC< 7 | CellPluginComponentProps<Html5VideoState> 8 | > = ({ data = defaultHtml5VideoState }) => { 9 | return ( 10 | <div className="react-page-content-plugin-html5-video"> 11 | <video 12 | autoPlay={true} 13 | controls={true} 14 | loop={true} 15 | muted={true} 16 | width="100%" 17 | key={data?.url} 18 | > 19 | <source src={data?.url} type={`video/${data?.url?.split('.').pop()}`} /> 20 | </video> 21 | </div> 22 | ); 23 | }; 24 | 25 | export default Html5VideoHtmlRenderer; 26 | -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/src/default/settings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Html5VideoSettings } from '../types/settings'; 3 | import { lazyLoad } from '@react-page/editor'; 4 | 5 | const PlayArrow = lazyLoad(() => import('@mui/icons-material/PlayArrow')); 6 | 7 | export const defaultTranslations = { 8 | pluginName: 'HTML 5 Video', 9 | pluginDescription: 'Add webm, ogg and other HTML5 video', 10 | urlLabel: 'Video url', 11 | urlPlaceholder: 'https://example.com/video.webm', 12 | isInlineable: true, 13 | }; 14 | 15 | export const defaultSettings: Html5VideoSettings = { 16 | Renderer: () => <>Renderer; for this plugin was not provided </>, 17 | translations: defaultTranslations, 18 | icon: <PlayArrow />, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/src/default/state.ts: -------------------------------------------------------------------------------- 1 | import type { Html5VideoState } from './../types/state'; 2 | export const defaultHtml5VideoState: Html5VideoState = { 3 | url: '', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/src/index.tsx: -------------------------------------------------------------------------------- 1 | import createPlugin from './createPlugin'; 2 | 3 | import Html5VideoHtmlRenderer from './Renderer/Html5VideoHtmlRenderer'; 4 | 5 | const plugin = createPlugin({ 6 | Renderer: Html5VideoHtmlRenderer, 7 | }); 8 | 9 | export default plugin; 10 | -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/src/types/settings.ts: -------------------------------------------------------------------------------- 1 | import type { CellPluginComponentProps } from '@react-page/editor'; 2 | 3 | import type { Html5VideoState } from './state'; 4 | import type { Translations } from './translations'; 5 | 6 | export interface Html5VideoSettings { 7 | Renderer: React.ComponentType<CellPluginComponentProps<Html5VideoState>>; 8 | 9 | translations?: Translations; 10 | icon?: React.ReactNode; 11 | isInlineable?: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/src/types/state.ts: -------------------------------------------------------------------------------- 1 | export type Html5VideoState = { 2 | url: string; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/src/types/translations.ts: -------------------------------------------------------------------------------- 1 | import type { defaultTranslations } from '../default/settings'; 2 | 3 | export type Translations = typeof defaultTranslations; 4 | -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/tsconfig-es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib-es", 5 | "module": "esnext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/content/html5-video/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib", "node_modules", "src/**/__tests__/**/*.*"], 9 | "references": [{ "path": "../../../editor" }] 10 | } 11 | -------------------------------------------------------------------------------- /packages/plugins/content/image/.npmignore: -------------------------------------------------------------------------------- 1 | src/**/* -------------------------------------------------------------------------------- /packages/plugins/content/image/babel.config.js: -------------------------------------------------------------------------------- 1 | const vendor = require('../../../../babel.config.js'); 2 | module.exports = Object.assign({}, vendor); 3 | -------------------------------------------------------------------------------- /packages/plugins/content/image/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-page/plugins-image", 3 | "version": "0.0.0", 4 | "main": "./lib/index.js", 5 | "module": "./lib-es/index.js", 6 | "sideEffects": false, 7 | "typings": "./lib/index.d.ts", 8 | "author": "ORY GmbH", 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "npm-run-all --parallel build:lib build:css", 12 | "build:watch": "npm-run-all --parallel build:lib:watch build:css:watch", 13 | "build:lib": "tsc -p ./tsconfig.json && tsc -p ./tsconfig-es.json", 14 | "build:lib:watch": "yarn build:lib -- --watch", 15 | "build:css": "postcss --config ../../../../config/postcss.config.js --dir lib/ src/index.css", 16 | "build:css:watch": "npm-run-all build:css -- -w", 17 | "docs": "documentation build ./lib/**/*.js --format md --github -o ../../../../docs/api/plugins/image.md", 18 | "clean": "rimraf \"lib\" && rimraf \"lib-es\" && rm -f *.tsbuildinfo" 19 | }, 20 | "peerDependencies": { 21 | "@mui/material": "*", 22 | "react": ">= 16.14", 23 | "react-dom": ">= 16.14" 24 | }, 25 | "dependencies": { 26 | "@mui/icons-material": "^5.8.0", 27 | "@react-page/editor": "0.0.0" 28 | }, 29 | "publishConfig": { 30 | "access": "public" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/plugins/content/image/src/common/styles.ts: -------------------------------------------------------------------------------- 1 | // material icons isn't allowing us to override style properties with className/styleName 2 | export const iconStyle: React.CSSProperties = { 3 | width: '100%', 4 | height: 'auto', 5 | padding: '0', 6 | color: '#aaa', 7 | textAlign: 'center', 8 | minWidth: 64, 9 | minHeight: 64, 10 | maxHeight: 256, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/plugins/content/image/src/createPlugin.tsx: -------------------------------------------------------------------------------- 1 | import type { CellPlugin } from '@react-page/editor'; 2 | import React from 'react'; 3 | import { defaultSettings } from './default/settings'; 4 | import type { ImageSettings } from './types/settings'; 5 | import type { ImageState } from './types/state'; 6 | 7 | const createPlugin = (settings?: ImageSettings): CellPlugin<ImageState> => { 8 | const mergedSettings = { ...defaultSettings, ...settings }; 9 | const Controls = mergedSettings.Controls; 10 | return { 11 | controls: { 12 | type: 'custom', 13 | Component: (props) => ( 14 | <Controls 15 | {...props} 16 | translations={mergedSettings.translations} 17 | imageUpload={mergedSettings.imageUpload} 18 | /> 19 | ), 20 | }, 21 | Renderer: mergedSettings.Renderer, 22 | id: 'ory/editor/core/content/image', 23 | version: 1, 24 | icon: mergedSettings.icon, 25 | title: mergedSettings.translations?.pluginName, 26 | isInlineable: true, 27 | description: mergedSettings.translations?.pluginDescription, 28 | }; 29 | }; 30 | export default createPlugin; 31 | -------------------------------------------------------------------------------- /packages/plugins/content/image/src/default/settings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { ImageSettings } from '../types/settings'; 3 | import { lazyLoad } from '@react-page/editor'; 4 | const Panorama = lazyLoad(() => import('@mui/icons-material/Panorama')); 5 | 6 | export const defaultTranslations = { 7 | pluginName: 'Image', 8 | pluginDescription: 'Loads an image from an url.', 9 | or: 'OR', 10 | haveUrl: 'Existing image URL', 11 | imageUrl: 'Image URL', 12 | hrefPlaceholder: 'http://example.com', 13 | hrefLabel: 'Link to open upon image click', 14 | altPlaceholder: "Image's description", 15 | altLabel: "Image's alternative description", 16 | openNewWindow: 'Open link in new window', 17 | srcPlaceholder: 'http://example.com/image.png', 18 | 19 | // Strings used in ImageUpload 20 | buttonContent: 'Choose for upload', 21 | noFileError: 'No file selected', 22 | badExtensionError: 'Wrong file type', 23 | tooBigError: 'Image file > 5MB', 24 | uploadingError: 'Error while uploading', 25 | unknownError: 'Unknown error', 26 | }; 27 | 28 | export const defaultSettings: ImageSettings = { 29 | Controls: () => <> Controls for this plugin were not provided</>, 30 | Renderer: () => <>Renderer; for this plugin was not provided </>, 31 | translations: defaultTranslations, 32 | icon: <Panorama />, 33 | }; 34 | -------------------------------------------------------------------------------- /packages/plugins/content/image/src/index.css: -------------------------------------------------------------------------------- 1 | .react-page-plugins-content-image { 2 | width: 100%; 3 | } 4 | 5 | .react-page-plugins-content-image-placeholder { 6 | position: relative; 7 | width: 100%; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugins/content/image/src/index.tsx: -------------------------------------------------------------------------------- 1 | import type { CellPlugin } from '@react-page/editor'; 2 | import { lazyLoad, ImageUploadType } from '@react-page/editor'; 3 | import createPlugin from './createPlugin'; 4 | import ImageHtmlRenderer from './Renderer/ImageHtmlRenderer'; 5 | import type { ImageSettings } from './types/settings'; 6 | import type { ImageState } from './types/state'; 7 | 8 | const ImageControls = lazyLoad(() => import('./Controls/ImageControls')); 9 | 10 | const imagePlugin: ( 11 | settings?: Partial<ImageSettings> 12 | ) => CellPlugin<ImageState> = (settings) => 13 | createPlugin({ 14 | Renderer: ImageHtmlRenderer, 15 | Controls: ImageControls, 16 | ...settings, 17 | }); 18 | 19 | const image = imagePlugin(); 20 | export default image; 21 | export { ImageUploadType }; 22 | export { imagePlugin }; 23 | -------------------------------------------------------------------------------- /packages/plugins/content/image/src/types/controls.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CellPluginComponentProps, 3 | ImageUploadType, 4 | } from '@react-page/editor'; 5 | 6 | import type { ImageState } from './state'; 7 | import type { Translations } from './translations'; 8 | 9 | export type ImageControlType = React.ComponentType< 10 | CellPluginComponentProps<ImageState> & { 11 | imageUpload?: ImageUploadType; 12 | translations?: Translations; 13 | } 14 | >; 15 | -------------------------------------------------------------------------------- /packages/plugins/content/image/src/types/settings.ts: -------------------------------------------------------------------------------- 1 | import type { CellPluginRenderer, ImageUploadType } from '@react-page/editor'; 2 | import type { ImageControlType } from './controls'; 3 | import type { ImageState } from './state'; 4 | 5 | import type { Translations } from './translations'; 6 | 7 | export type ImageSettings = { 8 | imageUpload?: ImageUploadType; 9 | Renderer: CellPluginRenderer<ImageState>; 10 | Controls: ImageControlType; 11 | translations?: Translations; 12 | icon?: React.ReactNode; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/plugins/content/image/src/types/state.ts: -------------------------------------------------------------------------------- 1 | export type ImageState = { 2 | src: string; 3 | href?: string; 4 | alt?: string; 5 | openInNewWindow?: boolean; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/plugins/content/image/src/types/translations.ts: -------------------------------------------------------------------------------- 1 | import type { defaultTranslations } from '../default/settings'; 2 | 3 | export type Translations = typeof defaultTranslations; 4 | -------------------------------------------------------------------------------- /packages/plugins/content/image/tsconfig-es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib-es", 5 | "module": "esnext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/content/image/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib", "node_modules", "src/**/__tests__/**/*.*"], 9 | "references": [{ "path": "../../../editor" }] 10 | } 11 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/.npmignore: -------------------------------------------------------------------------------- 1 | src/**/* -------------------------------------------------------------------------------- /packages/plugins/content/slate/babel.config.js: -------------------------------------------------------------------------------- 1 | const vendor = require('../../../../babel.config.js'); 2 | module.exports = Object.assign({}, vendor); 3 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/components/ConditionalWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ConditionalWrapper: React.FC<{ 4 | condition: boolean; 5 | children: React.ReactElement; 6 | wrapper: (children: React.ReactElement) => React.ReactElement; 7 | }> = ({ condition, wrapper, children }) => ( 8 | <>{condition ? wrapper(children) : children}</> 9 | ); 10 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/components/Controls.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { SlateProps } from '../types/component'; 3 | import PluginButton from './PluginButton'; 4 | import { useTheme } from '@mui/material'; 5 | const Controls = (props: Pick<SlateProps, 'translations' | 'plugins'>) => { 6 | const { plugins, translations } = props; 7 | const theme = useTheme(); 8 | 9 | const dark = theme.palette.mode === 'dark'; 10 | 11 | return ( 12 | <div> 13 | {plugins && 14 | plugins.map((plugin, i: number) => 15 | plugin.addToolbarButton ? ( 16 | <PluginButton 17 | key={i} 18 | translations={translations} 19 | plugin={plugin} 20 | dark={dark} 21 | /> 22 | ) : null 23 | )} 24 | </div> 25 | ); 26 | }; 27 | 28 | export default React.memo(Controls); 29 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/components/DialogVisibleProvider.tsx: -------------------------------------------------------------------------------- 1 | import type { Dispatch, FC, PropsWithChildren, SetStateAction } from 'react'; 2 | import React, { useContext, useMemo, useState } from 'react'; 3 | 4 | const DialogContext = React.createContext<{ 5 | visible?: boolean; 6 | setVisible?: Dispatch<SetStateAction<boolean>>; 7 | }>({}); 8 | 9 | const DialogVisibleProvider: FC<PropsWithChildren> = ({ children }) => { 10 | const [visible, setVisible] = useState(false); 11 | const value = useMemo(() => ({ visible, setVisible }), [visible, setVisible]); 12 | return ( 13 | <DialogContext.Provider value={value}>{children}</DialogContext.Provider> 14 | ); 15 | }; 16 | export const useDialogIsVisible = () => { 17 | return useContext(DialogContext)?.visible; 18 | }; 19 | export const useSetDialogIsVisible = () => { 20 | return useContext(DialogContext)?.setVisible; 21 | }; 22 | export default DialogVisibleProvider; 23 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/components/ReadOnlySlate.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SlateReactPresentation } from 'slate-react-presentation'; 3 | import type { SlateProps } from '../types/component'; 4 | import { useRenderElement, useRenderLeave } from './renderHooks'; 5 | 6 | const ReadOnlySlate = (props: SlateProps) => { 7 | const { plugins, defaultPluginType } = props; 8 | 9 | const renderElement = useRenderElement( 10 | { 11 | plugins, 12 | defaultPluginType, 13 | }, 14 | [] 15 | ); 16 | const renderLeaf = useRenderLeave({ plugins, readOnly: true }, []); 17 | // the div around is required to be consistent in styling with the default editor 18 | return ( 19 | <div 20 | style={{ 21 | position: 'relative', 22 | outline: 'none', 23 | whiteSpace: 'pre-wrap', 24 | overflowWrap: 'break-word', 25 | }} 26 | > 27 | <SlateReactPresentation 28 | renderElement={renderElement} 29 | renderLeaf={renderLeaf} 30 | value={props.data.slate} 31 | LeafWrapper={React.Fragment} 32 | /> 33 | </div> 34 | ); 35 | }; 36 | 37 | export default React.memo(ReadOnlySlate); 38 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/components/pluginHooks.ts: -------------------------------------------------------------------------------- 1 | import type { DependencyList } from 'react'; 2 | import { useMemo } from 'react'; 3 | import type { SlatePlugin } from '../types/SlatePlugin'; 4 | import type { SlateComponentPluginDefinition } from '../types/slatePluginDefinitions'; 5 | 6 | export const useComponentNodePlugins = ( 7 | { plugins }: { plugins: SlatePlugin[] }, 8 | deps: DependencyList 9 | ) => 10 | useMemo( 11 | () => 12 | plugins.filter( 13 | (plugin) => 14 | plugin.pluginType === 'component' && plugin.object !== 'mark' 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | ) as SlateComponentPluginDefinition<any>[], 17 | deps 18 | ); 19 | 20 | export const useComponentMarkPlugins = ( 21 | { plugins }: { plugins: SlatePlugin[] }, 22 | deps: DependencyList 23 | ) => 24 | useMemo( 25 | () => 26 | plugins.filter( 27 | (plugin) => 28 | plugin.pluginType === 'component' && plugin.object === 'mark' 29 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 30 | ) as SlateComponentPluginDefinition<any>[], 31 | deps 32 | ); 33 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/default/settings.ts: -------------------------------------------------------------------------------- 1 | export const defaultTranslations = { 2 | pluginName: 'Text', 3 | pluginDescription: 'An advanced rich text area.', 4 | placeholder: 'Write here...', 5 | linkPlugin: { 6 | cancel: 'Cancel', 7 | ok: 'Ok', 8 | createLink: 'Create a link', 9 | linkTitlePlaceholder: 'Link title', 10 | linkHrefPlaceholder: 'http://example.com/my/link.html', 11 | linkOpenInNewWindowLabel: 'Open in new window', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/hooks/useCurrentNodeDataWithPlugin.ts: -------------------------------------------------------------------------------- 1 | import type { DataTType } from '@react-page/editor'; 2 | import type { Editor } from 'slate'; 3 | import { useSlate } from 'slate-react'; 4 | import type { SlatePluginDefinition } from '../types/slatePluginDefinitions'; 5 | import { getCurrentNodeWithPlugin } from './useCurrentNodeWithPlugin'; 6 | 7 | export const getCurrentNodeDataWithPlugin = <T extends DataTType>( 8 | editor: Editor, 9 | plugin: SlatePluginDefinition<T> 10 | ): T => { 11 | const currentNodeEntry = getCurrentNodeWithPlugin(editor, plugin); 12 | 13 | if (currentNodeEntry) { 14 | const currentNode = currentNodeEntry[0]; 15 | if (plugin.pluginType === 'component' && plugin.object === 'mark') { 16 | return (currentNode as Record<string, unknown>)[plugin.type] as T; 17 | } 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | const { data } = currentNode as any; 21 | return data as T; 22 | } else if (plugin.getInitialData) { 23 | return plugin.getInitialData(); 24 | } else { 25 | return {} as T; 26 | } 27 | }; 28 | 29 | export default <T extends DataTType>(plugin: SlatePluginDefinition<T>): T => { 30 | const editor = useSlate(); 31 | return getCurrentNodeDataWithPlugin(editor, plugin); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/hooks/useCurrentSelection.ts: -------------------------------------------------------------------------------- 1 | import { useSlate } from 'slate-react'; 2 | 3 | export default () => { 4 | const editor = useSlate(); 5 | return editor.selection; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/hooks/usePluginIsActive.ts: -------------------------------------------------------------------------------- 1 | import type { DataTType } from '@react-page/editor'; 2 | import type { SlatePluginDefinition } from '../types/slatePluginDefinitions'; 3 | import useCurrentNodeWithPlugin from './useCurrentNodeWithPlugin'; 4 | 5 | export default <T extends DataTType>(plugin: SlatePluginDefinition<T>) => { 6 | const nodeEntry = useCurrentNodeWithPlugin<T>(plugin); 7 | return Boolean(nodeEntry); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/hooks/usePluginIsDisabled.ts: -------------------------------------------------------------------------------- 1 | import type { DataTType } from '@react-page/editor'; 2 | import { useEffect, useState } from 'react'; 3 | import { useSlate } from 'slate-react'; 4 | import type { SlatePluginDefinition } from '../types/slatePluginDefinitions'; 5 | 6 | export default <T extends DataTType>( 7 | plugin: SlatePluginDefinition<T> 8 | ): boolean => { 9 | const editor = useSlate(); 10 | const [disabled, setDisabled] = useState(false); 11 | 12 | useEffect(() => { 13 | if (plugin.isDisabled) { 14 | try { 15 | plugin.isDisabled(editor).then((d) => { 16 | setDisabled(d); 17 | }); 18 | } catch (e) { 19 | // slate sometimes throws when dom node cant be found in undo 20 | } 21 | } 22 | }, [editor.selection, plugin]); 23 | if (!editor) { 24 | return true; 25 | } 26 | return disabled; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/hooks/useTextIsSelected.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from 'slate'; 2 | import { useSlate } from 'slate-react'; 3 | 4 | const useTextIsSelected = () => { 5 | const editor = useSlate(); 6 | try { 7 | return Boolean( 8 | editor.selection && Editor.string(editor, editor.selection) !== '' 9 | ); 10 | } catch (e) { 11 | // can in some cases throw currently 12 | return false; 13 | } 14 | }; 15 | 16 | export default useTextIsSelected; 17 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/htmlToSlate/index.tsx: -------------------------------------------------------------------------------- 1 | import type { SlatePlugin } from '../types/SlatePlugin'; 2 | 3 | export const HtmlToSlate = ({ plugins }: { plugins: SlatePlugin[] }) => { 4 | return async (htmlString: string) => { 5 | const impl = (await import('./HtmlToSlate')).default; 6 | 7 | return impl({ plugins })(htmlString); 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/htmlToSlate/parseHtml.browser.ts: -------------------------------------------------------------------------------- 1 | export default (html: string) => { 2 | if (typeof DOMParser === 'undefined') { 3 | throw new Error( 4 | 'The native `DOMParser` global which the `Html` serializer uses by default is not present in this environment. You must supply the `options.parseHtml` function instead.' 5 | ); 6 | } 7 | 8 | return new DOMParser().parseFromString(html, 'text/html'); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/htmlToSlate/parseHtml.ts: -------------------------------------------------------------------------------- 1 | import { DOMParser } from '@xmldom/xmldom'; 2 | 3 | export default (html: string) => { 4 | return new DOMParser().parseFromString(html, 'text/html'); 5 | }; 6 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/index.css: -------------------------------------------------------------------------------- 1 | @import 'plugins/paragraphs/node.css'; 2 | 3 | .react-page-plugins-content-slate-inline-toolbar { 4 | position: absolute; 5 | z-index: 10; 6 | top: -10000px; 7 | left: -10000px; 8 | margin-top: -6px; 9 | opacity: 0; 10 | background-color: var(--grey900); 11 | border-radius: 4px; 12 | transition: opacity 0.75s; 13 | } 14 | 15 | .react-page-plugins-content-slate-inline-toolbar--hidden { 16 | opacity: 0 !important; 17 | pointer-events: none; 18 | } 19 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/migrations/deep-rename-keys.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module 'deep-rename-keys' { 3 | function rename(obj: any, cb: (key: string) => string): any; 4 | export default rename; 5 | } 6 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/migrations/v002.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '@react-page/editor'; 2 | 3 | import rename from 'deep-rename-keys'; 4 | const migration = new Migration({ 5 | toVersion: '0.0.2', 6 | fromVersionRange: '^0.0.1', 7 | migrate: (state) => { 8 | // wrap with document 9 | state = { 10 | ...state, 11 | ...(state.serialized 12 | ? { serialized: { document: state.serialized } } 13 | : {}), 14 | }; 15 | // rename keys 16 | state = rename(state, (key: string) => { 17 | switch (key) { 18 | case 'kind': 19 | return 'object'; 20 | case 'ranges': 21 | return 'leaves'; 22 | default: 23 | return key; 24 | } 25 | }); 26 | 27 | return state; 28 | }, 29 | }); 30 | 31 | export default migration; 32 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/none.tsx: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/pluginFactories/createComponentPlugin.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import type { SlatePlugin } from '../types/SlatePlugin'; 3 | import type { SlateComponentPluginDefinition } from '../types/slatePluginDefinitions'; 4 | 5 | function createComponentPlugin<T extends Record<string, unknown>>( 6 | def: SlateComponentPluginDefinition<T> 7 | ) { 8 | const customizablePlugin = function <CT extends Record<string, unknown> = T>( 9 | customize: ( 10 | t: SlateComponentPluginDefinition<T> 11 | ) => SlateComponentPluginDefinition<CT> = (d) => 12 | d as unknown as SlateComponentPluginDefinition<CT> 13 | ) { 14 | return createComponentPlugin(customize(def)); 15 | }; 16 | customizablePlugin.toPlugin = (): SlatePlugin => ({ 17 | ...def, 18 | pluginType: 'component', 19 | }); 20 | return customizablePlugin; 21 | } 22 | 23 | export default createComponentPlugin; 24 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/pluginFactories/createDataPlugin.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import type { SlatePlugin } from '../types/SlatePlugin'; 3 | import type { SlateDataPluginDefinition } from '../types/slatePluginDefinitions'; 4 | 5 | function createDataPlugin<T extends Record<string, unknown>>( 6 | def: SlateDataPluginDefinition<T> 7 | ) { 8 | const customizablePlugin = function <CT>( 9 | customize: ( 10 | t: SlateDataPluginDefinition<T> 11 | ) => SlateDataPluginDefinition<T & CT> = (d) => 12 | d as unknown as SlateDataPluginDefinition<T & CT> 13 | ) { 14 | return createDataPlugin(customize(def)); 15 | }; 16 | customizablePlugin.toPlugin = (): SlatePlugin => ({ 17 | pluginType: 'data', 18 | ...def, 19 | }); 20 | return customizablePlugin; 21 | } 22 | 23 | export default createDataPlugin; 24 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/pluginFactories/createHeadingsPlugin.tsx: -------------------------------------------------------------------------------- 1 | import type { SlateComponentPluginDefinition } from '../types/slatePluginDefinitions'; 2 | import type { HtmlBlockData } from './createSimpleHtmlBlockPlugin'; 3 | import createSimpleHtmlBlockPlugin from './createSimpleHtmlBlockPlugin'; 4 | 5 | export type HeadingsDef<T> = { 6 | level: 1 | 2 | 3 | 4 | 5 | 6; 7 | } & Pick< 8 | SlateComponentPluginDefinition<HtmlBlockData<T>>, 9 | 'type' | 'getInitialData' | 'icon' 10 | >; 11 | // eslint-disable-next-line @typescript-eslint/ban-types 12 | function createHeadingsPlugin<T = {}>(def: HeadingsDef<T>) { 13 | return createSimpleHtmlBlockPlugin<T>({ 14 | type: def.type, 15 | hotKey: 'mod+' + def.level, 16 | replaceWithDefaultOnRemove: true, 17 | icon: def.icon, 18 | label: `Heading ${def.level}`, 19 | tagName: ('h' + def.level) as keyof JSX.IntrinsicElements, 20 | }); 21 | } 22 | 23 | export default createHeadingsPlugin; 24 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/pluginFactories/createListItemPlugin.tsx: -------------------------------------------------------------------------------- 1 | import createSimpleHtmlBlockPlugin from './createSimpleHtmlBlockPlugin'; 2 | 3 | type ListItemDef = { 4 | type: string; 5 | tagName: keyof JSX.IntrinsicElements; 6 | }; 7 | 8 | export default function <T>(def: ListItemDef) { 9 | return createSimpleHtmlBlockPlugin<T>({ 10 | noButton: true, 11 | tagName: def.tagName, 12 | type: def.type, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/pluginFactories/createMarkPlugin.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentType, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | import createComponentPlugin from './createComponentPlugin'; 4 | 5 | type MarkPluginDefinition = { 6 | type: string; 7 | tagName: keyof JSX.IntrinsicElements; 8 | icon?: JSX.Element; 9 | hotKey?: string; 10 | label?: string; 11 | }; 12 | 13 | export default (markDef: MarkPluginDefinition) => { 14 | return createComponentPlugin({ 15 | type: markDef.type, 16 | object: 'mark', 17 | hotKey: markDef.hotKey, 18 | icon: markDef.icon, 19 | label: markDef.label, 20 | addToolbarButton: false, 21 | addHoverButton: true, 22 | deserialize: { 23 | tagName: markDef.tagName, 24 | }, 25 | Component: ({ children, attributes }) => { 26 | const Tag = 27 | markDef.tagName as unknown as ComponentType<PropsWithChildren>; 28 | return <Tag {...attributes}>{children}</Tag>; 29 | }, 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/pluginFactories/index.ts: -------------------------------------------------------------------------------- 1 | import createComponentPlugin from './createComponentPlugin'; 2 | import createDataPlugin from './createDataPlugin'; 3 | import createHeadingsPlugin from './createHeadingsPlugin'; 4 | import createListIndentionPlugin from './createListIndentionPlugin'; 5 | import createListItemPlugin from './createListItemPlugin'; 6 | import createListPlugin from './createListPlugin'; 7 | import createMarkPlugin from './createMarkPlugin'; 8 | import createSimpleHtmlBlockPlugin from './createSimpleHtmlBlockPlugin'; 9 | 10 | export { 11 | createComponentPlugin, 12 | createDataPlugin, 13 | createHeadingsPlugin, 14 | createListIndentionPlugin, 15 | createListItemPlugin, 16 | createListPlugin, 17 | createMarkPlugin, 18 | createSimpleHtmlBlockPlugin, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/plugins/emphasize/em.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { lazyLoad } from '@react-page/editor'; 4 | import createMarkPlugin from '../../pluginFactories/createMarkPlugin'; 5 | 6 | const ItalicIcon = lazyLoad(() => import('@mui/icons-material/FormatItalic')); 7 | 8 | export default createMarkPlugin({ 9 | type: 'EMPHASIZE/EM', 10 | tagName: 'em', 11 | icon: <ItalicIcon />, 12 | label: 'Italic', 13 | hotKey: 'mod+i', 14 | }); 15 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/plugins/emphasize/index.tsx: -------------------------------------------------------------------------------- 1 | import em from './em'; 2 | import strong from './strong'; 3 | import underline from './underline'; 4 | 5 | export default { em, strong, underline }; 6 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/plugins/emphasize/strong.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { lazyLoad } from '@react-page/editor'; 4 | import createMarkPlugin from '../../pluginFactories/createMarkPlugin'; 5 | 6 | const BoldIcon = lazyLoad(() => import('@mui/icons-material/FormatBold')); 7 | 8 | export default createMarkPlugin({ 9 | type: 'EMPHASIZE/STRONG', 10 | tagName: 'strong', 11 | icon: <BoldIcon />, 12 | label: 'Bold', 13 | hotKey: 'mod+b', 14 | }); 15 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/plugins/emphasize/underline.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { lazyLoad } from '@react-page/editor'; 4 | import createMarkPlugin from '../../pluginFactories/createMarkPlugin'; 5 | 6 | const UnderlinedIcon = lazyLoad( 7 | () => import('@mui/icons-material/FormatUnderlined') 8 | ); 9 | 10 | export default createMarkPlugin({ 11 | type: 'EMPHASIZE/U', 12 | tagName: 'u', 13 | icon: <UnderlinedIcon />, 14 | label: 'Underline', 15 | hotKey: 'mod+u', 16 | }); 17 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import alignment from './alignment'; 2 | import code from './code/'; 3 | import emphasize from './emphasize'; 4 | import headings from './headings'; 5 | import link from './links'; 6 | import lists from './lists'; 7 | import paragraphs from './paragraphs'; 8 | import quotes from './quotes'; 9 | 10 | export default { 11 | paragraphs, 12 | headings, 13 | link, 14 | lists, 15 | 16 | quotes, 17 | code, 18 | emphasize, 19 | alignment, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/plugins/links/anchor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createDataPlugin from '../../pluginFactories/createDataPlugin'; 3 | 4 | const anchor = createDataPlugin<{ id: string }>({ 5 | addHoverButton: false, 6 | addToolbarButton: true, 7 | object: 'block', 8 | label: 'Id for Link Anchor', 9 | icon: <span>#</span>, 10 | properties: ['id'], 11 | dataMatches: (data) => { 12 | return Boolean(data?.id); 13 | }, 14 | controls: { 15 | type: 'autoform', 16 | schema: { 17 | type: 'object', 18 | required: ['id'], 19 | properties: { 20 | id: { 21 | type: 'string', 22 | }, 23 | }, 24 | }, 25 | }, 26 | }); 27 | 28 | export default anchor; 29 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/plugins/links/index.tsx: -------------------------------------------------------------------------------- 1 | import link from './link'; 2 | import anchor from './anchor'; 3 | export default { 4 | anchor, 5 | link, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/plugins/lists/constants.ts: -------------------------------------------------------------------------------- 1 | export const LISTS_TYPE_PREFIX = 'LISTS/'; 2 | 3 | export const UL = 'UNORDERED-LIST'; 4 | export const OL = 'ORDERED-LIST'; 5 | export const LI = 'LISTS/LIST-ITEM'; // FIXME: this should have a different prefix to avoid hard coded check, but changing that would need a migration 6 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/plugins/paragraphs/index.tsx: -------------------------------------------------------------------------------- 1 | import createComponentPlugin from '../../pluginFactories/createComponentPlugin'; 2 | 3 | type Align = 'left' | 'right' | 'center' | 'justify'; 4 | export const getAlignmentFromElement = (el: HTMLElement) => { 5 | const align = el?.style?.textAlign as Align; 6 | if (align) { 7 | return { 8 | align, 9 | }; 10 | } 11 | }; 12 | export default { 13 | paragraph: createComponentPlugin<{ 14 | align?: Align; 15 | }>({ 16 | type: 'PARAGRAPH/PARAGRAPH', 17 | label: 'Paragraph', 18 | object: 'block', 19 | addToolbarButton: false, 20 | addHoverButton: false, 21 | deserialize: { 22 | tagName: 'p', 23 | getData: getAlignmentFromElement, 24 | }, 25 | getStyle: ({ align }) => ({ textAlign: align }), 26 | 27 | Component: 'p', 28 | }), 29 | // currently only for deserialize 30 | pre: createComponentPlugin<{ 31 | align?: Align; 32 | }>({ 33 | type: 'PARAGRAPH/PRE', 34 | label: 'Pre', 35 | object: 'block', 36 | addToolbarButton: false, 37 | addHoverButton: false, 38 | deserialize: { 39 | tagName: 'pre', 40 | getData: getAlignmentFromElement, 41 | }, 42 | getStyle: ({ align }) => ({ textAlign: align }), 43 | 44 | Component: 'pre', 45 | }), 46 | }; 47 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/plugins/paragraphs/node.css: -------------------------------------------------------------------------------- 1 | .react-page-plugins-content-slate-paragraph-placeholder { 2 | font-style: italic; 3 | color: var(--lightBlack); 4 | } 5 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/plugins/quotes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { lazyLoad } from '@react-page/editor'; 3 | import createSimpleHtmlBlockPlugin from '../pluginFactories/createSimpleHtmlBlockPlugin'; 4 | 5 | const BlockquoteIcon = lazyLoad( 6 | () => import('@mui/icons-material/FormatQuote') 7 | ); 8 | 9 | export default { 10 | blockQuote: createSimpleHtmlBlockPlugin({ 11 | type: 'BLOCKQUOTE/BLOCKQUOTE', 12 | icon: <BlockquoteIcon />, 13 | label: 'Quote', 14 | tagName: 'blockquote', 15 | }), 16 | }; 17 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/slateEnhancer/withInline.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from 'slate'; 2 | import type { SlatePlugin } from '../types/SlatePlugin'; 3 | 4 | const withInline = (plugins: SlatePlugin[]) => (editor: Editor) => { 5 | const { isInline, isVoid } = editor; 6 | editor.isInline = (element) => { 7 | return plugins.some( 8 | (plugin) => 9 | plugin.pluginType === 'component' && 10 | plugin.object === 'inline' && 11 | plugin.type === element.type 12 | ) 13 | ? true 14 | : isInline(element); 15 | }; 16 | 17 | editor.isVoid = (element) => { 18 | return plugins.some( 19 | (plugin) => 20 | plugin.pluginType === 'component' && 21 | (plugin.object === 'block' || plugin.object === 'inline') && 22 | plugin.type === element.type && 23 | plugin.isVoid 24 | ) 25 | ? true 26 | : isVoid(element); 27 | }; 28 | return editor; 29 | }; 30 | 31 | export default withInline; 32 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/slateTypes.d.ts: -------------------------------------------------------------------------------- 1 | import type { BaseEditor, Descendant } from 'slate'; 2 | import type { ReactEditor } from 'slate-react'; 3 | import type { Data, CustomText } from './types'; 4 | 5 | declare module 'slate' { 6 | interface CustomTypes { 7 | Editor: BaseEditor & 8 | ReactEditor & { 9 | type: string | null; 10 | data?: Data | null; 11 | }; 12 | Element: { 13 | type?: string | null; 14 | data?: Data | null; 15 | children: Descendant[]; 16 | }; 17 | Text: CustomText; 18 | } 19 | } 20 | 21 | export {}; 22 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { DataTType } from '@react-page/editor'; 2 | 3 | export type Data = DataTType; 4 | export type CustomText = { 5 | text: string; 6 | data?: Data; 7 | type?: string; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/types/SlatePlugin.ts: -------------------------------------------------------------------------------- 1 | import type { SlatePluginDefinition } from './slatePluginDefinitions'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export type SlatePlugin = SlatePluginDefinition<any>; 5 | 6 | export type SlatePluginOrListOfPlugins = SlatePlugin | SlatePlugin[]; 7 | 8 | export type SlatePluginOrFactory = 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | | { 11 | toPlugin: () => SlatePluginOrListOfPlugins; 12 | } 13 | | SlatePluginOrListOfPlugins; 14 | export type SlatePluginCollection = { 15 | [group: string]: { 16 | [key: string]: SlatePluginOrFactory; 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/types/component.ts: -------------------------------------------------------------------------------- 1 | import type { CellPluginComponentProps } from '@react-page/editor'; 2 | import type { SlatePluginDefinition } from './slatePluginDefinitions'; 3 | import type { SlateState } from './state'; 4 | import type { Translations } from './translations'; 5 | 6 | export type SlateProps = CellPluginComponentProps<SlateState> & { 7 | plugins: SlatePluginDefinition[]; 8 | defaultPluginType: string; 9 | translations?: Translations; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/types/initialSlateState.ts: -------------------------------------------------------------------------------- 1 | import type { SlatePluginOrFactory } from './SlatePlugin'; 2 | 3 | export type SlatePluginNode = { 4 | plugin: SlatePluginOrFactory; 5 | children?: SlateDefNode[]; 6 | // eslint-disable-next-line @typescript-eslint/ban-types 7 | data?: object; 8 | }; 9 | 10 | export type SlateDefNode = SlatePluginNode | string; 11 | export type InitialSlateStateDef = { 12 | children: SlateDefNode[]; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/types/state.ts: -------------------------------------------------------------------------------- 1 | import type { Node, Range } from 'slate'; 2 | 3 | export type SlateState = { 4 | slate: Node[]; 5 | selection?: Range | null; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/types/translations.ts: -------------------------------------------------------------------------------- 1 | import type { defaultTranslations } from '../default/settings'; 2 | 3 | export type Translations = typeof defaultTranslations; 4 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/utils/flattenDeep.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export default function flattenDeep<T>(arr1: any): T[] { 3 | if (!Array.isArray(arr1)) { 4 | return [arr1]; 5 | } 6 | return arr1.reduce( 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | (acc: T[], val: any) => 9 | Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), 10 | [] 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/utils/getCurrentData.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from 'slate'; 2 | 3 | const getCurrentData = (editor: Editor): Record<string, unknown> => { 4 | const [existingNodeWithData] = Editor.nodes(editor, { 5 | mode: 'all', 6 | match: (node) => { 7 | return Boolean(node.data); 8 | }, 9 | }); 10 | const existingData = existingNodeWithData 11 | ? (existingNodeWithData[0]?.data as Record<string, unknown>) 12 | : {}; 13 | 14 | return existingData; 15 | }; 16 | 17 | export default getCurrentData; 18 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/utils/getTextContent.ts: -------------------------------------------------------------------------------- 1 | import type { Node, Text } from 'slate'; 2 | import type { SlatePlugin } from '../types/SlatePlugin'; 3 | 4 | const isText = (node: Node): node is Text => { 5 | return Boolean((node as Text).text); 6 | }; 7 | export const getTextContents = ( 8 | nodes: Node[], 9 | options: { slatePlugins: SlatePlugin[] } 10 | ): string[] => { 11 | return nodes.reduce<string[]>((acc, node) => { 12 | if (isText(node)) { 13 | return [...acc, node.text]; 14 | } else if (node.children) { 15 | const childTexts = getTextContents(node.children as Node[], options); 16 | 17 | const everyChildIsTextOrInline = node.children.every((n) => { 18 | if (isText(n)) return true; 19 | 20 | const p = options.slatePlugins.find( 21 | (f) => f.pluginType === 'component' && f.type === n.type 22 | ); 23 | if (!p) return true; // could be data plugin or custom 24 | 25 | if (p.object === 'block') { 26 | return false; 27 | } 28 | 29 | return true; 30 | }); 31 | 32 | return [ 33 | ...acc, 34 | ...(everyChildIsTextOrInline ? [childTexts.join('')] : childTexts), 35 | ]; 36 | } else { 37 | return acc; 38 | } 39 | }, []); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/utils/makeSlatePluginsFromDef.ts: -------------------------------------------------------------------------------- 1 | import type { SlatePlugin, SlatePluginCollection } from '../types/SlatePlugin'; 2 | import flattenDeep from './flattenDeep'; 3 | 4 | export default (plugins: SlatePluginCollection) => { 5 | return Object.keys(plugins).reduce((acc, groupKey) => { 6 | const group = plugins[groupKey]; 7 | const groupPlugins = Object.keys(group).reduce((innerAcc, key) => { 8 | const pluginOrFactory = plugins[groupKey][key]; 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | const result = (pluginOrFactory as any).toPlugin 11 | ? // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | (pluginOrFactory as any).toPlugin() 13 | : pluginOrFactory; 14 | 15 | return [...innerAcc, ...flattenDeep(result)] as SlatePlugin[]; 16 | }, [] as SlatePlugin[]); 17 | 18 | return [...acc, ...groupPlugins]; 19 | }, [] as SlatePlugin[]); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/src/utils/useSafeSetState.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function useSafeSetState<T>( 4 | initialState?: T | (() => T) 5 | ): [T | undefined, React.Dispatch<React.SetStateAction<T>>] { 6 | const [state, setState] = React.useState(initialState); 7 | 8 | const mountedRef = React.useRef(false); 9 | React.useEffect(() => { 10 | mountedRef.current = true; 11 | return () => { 12 | mountedRef.current = false; 13 | }; 14 | }, []); 15 | const safeSetState = React.useCallback( 16 | (args: any) => { 17 | if (mountedRef.current) { 18 | return setState(args); 19 | } 20 | }, 21 | [mountedRef, setState] 22 | ); 23 | 24 | return [state, safeSetState]; 25 | } 26 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/tsconfig-es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib-es", 5 | "module": "esnext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/content/slate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "downlevelIteration": true, 5 | "rootDir": "src", 6 | "outDir": "lib", 7 | "paths": { 8 | "react": ["../../../../node_modules/@types/react/index"] 9 | } 10 | }, 11 | "include": ["src"], 12 | 13 | "exclude": ["lib", "node_modules", "src/**/__tests__/**/*.*"], 14 | "references": [{ "path": "../../../editor" }] 15 | } 16 | -------------------------------------------------------------------------------- /packages/plugins/content/spacer/.npmignore: -------------------------------------------------------------------------------- 1 | src/**/* -------------------------------------------------------------------------------- /packages/plugins/content/spacer/babel.config.js: -------------------------------------------------------------------------------- 1 | const vendor = require('../../../../babel.config.js'); 2 | module.exports = Object.assign({}, vendor); 3 | -------------------------------------------------------------------------------- /packages/plugins/content/spacer/src/Renderer/SpacerHtmlRenderer.tsx: -------------------------------------------------------------------------------- 1 | import type { CellPluginComponentProps } from '@react-page/editor'; 2 | import { lazyLoad } from '@react-page/editor'; 3 | 4 | import React from 'react'; 5 | import type { SpacerState } from '../types/state'; 6 | 7 | const SpacerResizable = lazyLoad(() => import('./SpacerResizable')); 8 | const SpacerHtmlRenderer: React.FC<CellPluginComponentProps<SpacerState>> = ( 9 | props 10 | ) => { 11 | return ( 12 | <div className={'react-page-plugins-content-spacer'}> 13 | {props.isEditMode ? ( 14 | <SpacerResizable {...props} /> 15 | ) : ( 16 | <div style={{ height: `${(props.data?.height || 0).toString()}px` }} /> 17 | )} 18 | </div> 19 | ); 20 | }; 21 | 22 | export default SpacerHtmlRenderer; 23 | -------------------------------------------------------------------------------- /packages/plugins/content/spacer/src/default/settings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { SpacerSettings } from '../types/settings'; 3 | 4 | export const defaultTranslations = { 5 | pluginName: 'Spacer', 6 | pluginDescription: 'Resizeable, horizontal and vertical empty space.', 7 | elementHeightLabel: 'Element height (px)', 8 | }; 9 | 10 | export const defaultSettings: SpacerSettings = { 11 | Renderer: () => <>Renderer; for this plugin was not provided </>, 12 | translations: defaultTranslations, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/plugins/content/spacer/src/index.css: -------------------------------------------------------------------------------- 1 | .react-page-plugins-content-spacer { 2 | border-bottom: none; 3 | position: relative; 4 | } 5 | 6 | .react-page-editable .react-page-plugins-content-spacer { 7 | outline: 1px dashed var(--minBlack); 8 | } 9 | .react-page-editable-mode-preview .react-page-plugins-content-spacer { 10 | outline: none; 11 | } 12 | 13 | .react-page-plugins-content-spacer 14 | > .react-resizable 15 | > .react-resizable-handle:before, 16 | .react-page-plugins-content-spacer 17 | > .react-resizable 18 | > .react-resizable-handle:hover:before { 19 | content: ' '; 20 | position: absolute; 21 | text-align: center; 22 | width: 100%; 23 | bottom: 0; 24 | right: 0; 25 | cursor: n-resize; 26 | line-height: 12px; 27 | font-size: 1.5em; 28 | height: 24px; 29 | } 30 | -------------------------------------------------------------------------------- /packages/plugins/content/spacer/src/index.tsx: -------------------------------------------------------------------------------- 1 | import createPlugin from './createPlugin'; 2 | 3 | import SpacerHtmlRenderer from './Renderer/SpacerHtmlRenderer'; 4 | 5 | const plugin = createPlugin({ 6 | Renderer: SpacerHtmlRenderer, 7 | }); 8 | 9 | export default plugin; 10 | -------------------------------------------------------------------------------- /packages/plugins/content/spacer/src/types/settings.ts: -------------------------------------------------------------------------------- 1 | import type { CellPluginComponentProps } from '@react-page/editor'; 2 | import type { SpacerState } from './state'; 3 | import type { Translations } from './translations'; 4 | 5 | export interface SpacerSettings { 6 | Renderer: React.ComponentType<CellPluginComponentProps<SpacerState>>; 7 | 8 | translations?: Translations; 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugins/content/spacer/src/types/state.ts: -------------------------------------------------------------------------------- 1 | export type SpacerState = { 2 | height: number; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/plugins/content/spacer/src/types/translations.ts: -------------------------------------------------------------------------------- 1 | import type { defaultTranslations } from '../default/settings'; 2 | 3 | export type Translations = typeof defaultTranslations; 4 | -------------------------------------------------------------------------------- /packages/plugins/content/spacer/tsconfig-es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib-es", 5 | "module": "esnext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/content/spacer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib", "node_modules", "src/**/__tests__/**/*.*"], 9 | "references": [{ "path": "../../../editor" }] 10 | } 11 | -------------------------------------------------------------------------------- /packages/plugins/content/video/.npmignore: -------------------------------------------------------------------------------- 1 | src/**/* -------------------------------------------------------------------------------- /packages/plugins/content/video/babel.config.js: -------------------------------------------------------------------------------- 1 | const vendor = require('../../../../babel.config.js'); 2 | module.exports = Object.assign({}, vendor); 3 | -------------------------------------------------------------------------------- /packages/plugins/content/video/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-page/plugins-video", 3 | "version": "0.0.0", 4 | "main": "./lib/index.js", 5 | "module": "./lib-es/index.js", 6 | "sideEffects": false, 7 | "typings": "./lib/index.d.ts", 8 | "author": "ORY GmbH", 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "npm-run-all --parallel build:lib build:css", 12 | "build:watch": "npm-run-all --parallel build:lib:watch build:css:watch", 13 | "build:lib": "tsc -p ./tsconfig.json && tsc -p ./tsconfig-es.json", 14 | "build:lib:watch": "yarn build:lib -- --watch", 15 | "build:css": "postcss --config ../../../../config/postcss.config.js --dir lib/ src/index.css", 16 | "build:css:watch": "npm-run-all build:css -- -w", 17 | "docs": "documentation build ./lib/**/*.js --format md --github -o ../../../../docs/api/plugins/video.md", 18 | "clean": "rimraf \"lib\" && rimraf \"lib-es\" && rm -f *.tsbuildinfo" 19 | }, 20 | "peerDependencies": { 21 | "@mui/material": "*", 22 | "react": ">= 16.14", 23 | "react-dom": ">= 16.14" 24 | }, 25 | "dependencies": { 26 | "@mui/icons-material": "^5.8.0", 27 | "@react-page/editor": "0.0.0", 28 | "react-player": "^2.10.1" 29 | }, 30 | "devDependencies": {}, 31 | "publishConfig": { 32 | "access": "public" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/Renderer/index.css: -------------------------------------------------------------------------------- 1 | .react-page-plugins-content-video-placeholder { 2 | position: relative; 3 | width: 100%; 4 | text-align: center; 5 | } 6 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/common/styles.ts: -------------------------------------------------------------------------------- 1 | import type React from 'react'; 2 | export const iconStyle: React.CSSProperties = { 3 | width: '100%', 4 | height: 'auto', 5 | padding: '0', 6 | color: '#aaa', 7 | textAlign: 'center', 8 | minWidth: 64, 9 | minHeight: 64, 10 | maxHeight: 256, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/createPlugin.tsx: -------------------------------------------------------------------------------- 1 | import type { CellPlugin } from '@react-page/editor'; 2 | 3 | import { defaultSettings } from './default/settings'; 4 | 5 | import type { VideoSettings } from './types/settings'; 6 | import type { VideoState } from './types/state'; 7 | 8 | const createPlugin: (settings: VideoSettings) => CellPlugin<VideoState> = ( 9 | settings 10 | ) => { 11 | const mergedSettings = { ...defaultSettings, ...settings }; 12 | 13 | return { 14 | controls: { 15 | type: 'autoform', 16 | 17 | schema: { 18 | required: ['src'], 19 | type: 'object', 20 | properties: { 21 | src: { 22 | type: 'string', 23 | uniforms: { 24 | placeholder: mergedSettings.translations?.placeholder, 25 | label: mergedSettings.translations?.label, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | Renderer: mergedSettings.Renderer, 32 | id: 'ory/editor/core/content/video', 33 | version: 1, 34 | icon: mergedSettings.icon, 35 | title: mergedSettings.translations?.pluginName, 36 | description: mergedSettings.translations?.pluginDescription, 37 | isInlineable: true, 38 | }; 39 | }; 40 | 41 | export default createPlugin; 42 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/default/settings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { VideoSettings } from '../types/settings'; 3 | import { lazyLoad } from '@react-page/editor'; 4 | 5 | const PlayArrow = lazyLoad(() => import('@mui/icons-material/PlayArrow')); 6 | 7 | export const defaultTranslations = { 8 | pluginName: 'Video', 9 | pluginDescription: 'Include videos from Vimeo or YouTube', 10 | label: 'Video location (YouTube / Vimeo)', 11 | placeholder: 'https://www.youtube.com/watch?v=ER97mPHhgtM', 12 | }; 13 | 14 | export const defaultSettings: VideoSettings = { 15 | Renderer: () => <>Renderer; for this plugin was not provided </>, 16 | translations: defaultTranslations, 17 | icon: <PlayArrow />, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/default/state.ts: -------------------------------------------------------------------------------- 1 | import type { VideoState } from './../types/state'; 2 | export const defaultVideoState: VideoState = { 3 | src: '', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/index.css: -------------------------------------------------------------------------------- 1 | @import './Renderer/index.css'; 2 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/index.tsx: -------------------------------------------------------------------------------- 1 | import createPlugin from './createPlugin'; 2 | 3 | import VideoHtmlRenderer from './Renderer/VideoHtmlRenderer'; 4 | 5 | const plugin = createPlugin({ 6 | Renderer: VideoHtmlRenderer, 7 | }); 8 | 9 | export default plugin; 10 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/types/api.ts: -------------------------------------------------------------------------------- 1 | export interface VideoApi { 2 | changeSrcPreview: (src: string) => void; 3 | commitSrc: () => void; 4 | } 5 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/types/component.ts: -------------------------------------------------------------------------------- 1 | import type { CellPluginComponentProps } from '@react-page/editor'; 2 | 3 | import type { VideoState } from './state'; 4 | 5 | export type VideoProps = CellPluginComponentProps<VideoState>; 6 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/types/controls.ts: -------------------------------------------------------------------------------- 1 | import type { VideoProps } from './component'; 2 | import type { VideoApi } from './api'; 3 | 4 | export type VideoControlsProps = VideoProps & VideoApi; 5 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/types/renderer.ts: -------------------------------------------------------------------------------- 1 | import type { VideoProps } from './component'; 2 | 3 | export type VideoHtmlRendererProps = VideoProps; 4 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/types/settings.ts: -------------------------------------------------------------------------------- 1 | import type { VideoHtmlRendererProps } from './renderer'; 2 | import type { Translations } from './translations'; 3 | 4 | export type VideoSettings = { 5 | Renderer: React.ComponentType<VideoHtmlRendererProps>; 6 | 7 | placeholder?: string; 8 | label?: string; 9 | translations?: Translations; 10 | icon?: React.ReactNode; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/types/state.ts: -------------------------------------------------------------------------------- 1 | export type VideoState = { 2 | src: string; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/plugins/content/video/src/types/translations.ts: -------------------------------------------------------------------------------- 1 | import type { defaultTranslations } from '../default/settings'; 2 | 3 | export type Translations = typeof defaultTranslations; 4 | -------------------------------------------------------------------------------- /packages/plugins/content/video/tsconfig-es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib-es", 5 | "module": "esnext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/content/video/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib", "node_modules", "src/**/__tests__/**/*.*"], 9 | "references": [{ "path": "../../../editor" }] 10 | } 11 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/.npmignore: -------------------------------------------------------------------------------- 1 | src/**/* -------------------------------------------------------------------------------- /packages/plugins/layout/background/babel.config.js: -------------------------------------------------------------------------------- 1 | const vendor = require('../../../../babel.config.js'); 2 | module.exports = Object.assign({}, vendor); 3 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-page/plugins-background", 3 | "version": "0.0.0", 4 | "main": "./lib/index.js", 5 | "module": "./lib-es/index.js", 6 | "sideEffects": false, 7 | "author": "ORY GmbH", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "npm-run-all --parallel build:lib build:css", 11 | "build:watch": "npm-run-all --parallel build:lib:watch build:css:watch", 12 | "build:lib": "tsc -p ./tsconfig.json && tsc -p ./tsconfig-es.json", 13 | "build:lib:watch": "yarn build:lib -- --watch", 14 | "build:css": "postcss --config ../../../../config/postcss.config.js --dir lib/ src/index.css", 15 | "build:css:watch": "npm-run-all build:css -- -w", 16 | "docs": "documentation build ./lib/**/*.js --format md --github -o ../../../../docs/api/plugins/background.md", 17 | "clean": "rimraf \"lib\" && rimraf \"lib-es\" && rm -f *.tsbuildinfo" 18 | }, 19 | "peerDependencies": { 20 | "@mui/material": "*", 21 | "react": ">= 16.14", 22 | "react-dom": ">= 16.14" 23 | }, 24 | "dependencies": { 25 | "@mui/icons-material": "^5.8.0", 26 | "@react-page/editor": "0.0.0" 27 | }, 28 | "devDependencies": {}, 29 | "publishConfig": { 30 | "access": "public" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/const/mode.ts: -------------------------------------------------------------------------------- 1 | export const IMAGE_MODE_FLAG = 1; 2 | export const COLOR_MODE_FLAG = 2; 3 | export const GRADIENT_MODE_FLAG = 4; 4 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/index.css: -------------------------------------------------------------------------------- 1 | .react-page-plugins-layout-background { 2 | background-position: center; 3 | background-repeat: no-repeat; 4 | background-size: cover; 5 | color: white; 6 | padding: 12px; 7 | position: relative; 8 | } 9 | 10 | .react-page-plugins-layout-background > .react-page-row { 11 | position: relative; 12 | } 13 | 14 | .react-page-plugins-layout-background__backstretch { 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | right: 0; 19 | bottom: 0; 20 | } 21 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/index.tsx: -------------------------------------------------------------------------------- 1 | import type { BackgroundSettings } from './types/settings'; 2 | import createPlugin from './createPlugin'; 3 | 4 | import BackgroundHtmlRenderer from './Renderer/BackgroundHtmlRenderer'; 5 | import type { MakeOptional } from './types/makeOptional'; 6 | import { ModeEnum } from './types/ModeEnum'; 7 | 8 | export { ModeEnum }; 9 | import { lazyLoad } from '@react-page/editor'; 10 | 11 | const BackgroundDefaultControls = lazyLoad(() => import('./Controls/Controls')); 12 | 13 | export default ( 14 | settings: MakeOptional<BackgroundSettings, 'Renderer' | 'Controls'> 15 | ) => { 16 | const plugin = createPlugin({ 17 | Controls: BackgroundDefaultControls, 18 | Renderer: BackgroundHtmlRenderer, 19 | ...settings, 20 | }); 21 | return plugin; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/types/ModeEnum.ts: -------------------------------------------------------------------------------- 1 | export enum ModeEnum { 2 | IMAGE_MODE_FLAG = 1, 3 | COLOR_MODE_FLAG = 2, 4 | GRADIENT_MODE_FLAG = 4, 5 | } 6 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/types/api.ts: -------------------------------------------------------------------------------- 1 | import type { ModeEnum } from './ModeEnum'; 2 | 3 | import type { ImageLoaded, RGBColor } from '@react-page/editor'; 4 | 5 | export interface BackgroundApi { 6 | handleChangeDarken: () => void; 7 | handleChangeDarkenPreview: (darken: number) => void; 8 | handleChangeLighten: () => void; 9 | handleChangeLightenPreview: (lighten: number) => void; 10 | handleChangeHasPadding: () => void; 11 | handleChangeModeSwitch: ( 12 | mode: ModeEnum | undefined, 13 | modeFlag: ModeEnum | undefined 14 | ) => () => void; 15 | handleChangeBackgroundColorPreview: (color?: RGBColor) => void; 16 | handleChangeGradientDegPreview: ( 17 | gradientDegPreview: number | undefined, 18 | gradientDegPreviewIndex?: number 19 | ) => void; 20 | handleChangeGradientOpacityPreview: ( 21 | gradientOpacityPreview: number | undefined, 22 | gradientOpacityPreviewIndex?: number 23 | ) => void; 24 | handleChangeGradientColorPreview: ( 25 | gradientColorPreview: RGBColor | undefined, 26 | gradientColorPreviewIndex?: number, 27 | gradientColorPreviewColorIndex?: number 28 | ) => void; 29 | handleImageLoaded: (imagePreview: ImageLoaded) => void; 30 | handleImageUploaded: () => void; 31 | } 32 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/types/component.ts: -------------------------------------------------------------------------------- 1 | import type { CellPluginComponentProps } from '@react-page/editor'; 2 | import type { BackgroundSettings } from './settings'; 3 | import type { BackgroundState } from './state'; 4 | 5 | export type BackgroundProps = CellPluginComponentProps<BackgroundState> & 6 | BackgroundSettings; 7 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/types/controls.ts: -------------------------------------------------------------------------------- 1 | import type { BackgroundProps } from './component'; 2 | 3 | import type { BackgroundRendererExtraProps } from './renderer'; 4 | 5 | export type BackgroundControlsProps = BackgroundProps & 6 | BackgroundRendererExtraProps; 7 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/types/gradient.ts: -------------------------------------------------------------------------------- 1 | import type { RGBColor } from '@react-page/editor'; 2 | 3 | export type Gradient = { 4 | opacity: number; 5 | deg: number; 6 | colors?: { color?: RGBColor }[]; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/types/makeOptional.ts: -------------------------------------------------------------------------------- 1 | import type { Omit } from './omit'; 2 | export type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<T>; 3 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/types/omit.ts: -------------------------------------------------------------------------------- 1 | export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; 2 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/types/renderer.ts: -------------------------------------------------------------------------------- 1 | import type { BackgroundProps } from './component'; 2 | import type { ImageLoaded, RGBColor } from '@react-page/editor'; 3 | 4 | export interface BackgroundRendererExtraProps { 5 | backgroundColorPreview?: RGBColor; 6 | gradientDegPreview?: number; 7 | gradientDegPreviewIndex?: number; 8 | gradientOpacityPreview?: number; 9 | gradientOpacityPreviewIndex?: number; 10 | gradientColorPreview?: RGBColor; 11 | gradientColorPreviewIndex?: number; 12 | gradientColorPreviewColorIndex?: number; 13 | darkenPreview?: number; 14 | lightenPreview?: number; 15 | imagePreview?: ImageLoaded; 16 | } 17 | 18 | export type BackgroundRendererProps = BackgroundProps & 19 | BackgroundRendererExtraProps; 20 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/types/settings.ts: -------------------------------------------------------------------------------- 1 | import type { CellPlugin, ImageUploadType, RGBColor } from '@react-page/editor'; 2 | 3 | import type { BackgroundControlsProps } from './controls'; 4 | import type { ModeEnum } from './ModeEnum'; 5 | import type { BackgroundRendererProps } from './renderer'; 6 | import type { BackgroundState } from './state'; 7 | import type { Translations } from './translations'; 8 | 9 | export type BackgroundSettings = { 10 | Renderer: React.ComponentType<BackgroundRendererProps>; 11 | Controls: React.ComponentType<BackgroundControlsProps>; 12 | enabledModes?: ModeEnum; 13 | getInitialChildren?: CellPlugin<BackgroundState>['createInitialChildren']; 14 | defaultBackgroundColor?: RGBColor; 15 | defaultGradientColor?: RGBColor; 16 | defaultGradientSecondaryColor?: RGBColor; 17 | defaultMode?: ModeEnum; 18 | defaultModeFlag?: ModeEnum; 19 | defaultDarken?: number; 20 | defaultLighten?: number; 21 | defaultHasPadding?: boolean; 22 | defaultIsParallax?: boolean; 23 | imageUpload?: ImageUploadType; 24 | translations?: Translations; 25 | cellStyle?: CellPlugin<BackgroundState>['cellStyle']; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/types/state.ts: -------------------------------------------------------------------------------- 1 | import type { RGBColor } from '@react-page/editor'; 2 | import type { Gradient } from './gradient'; 3 | import type { ModeEnum } from './ModeEnum'; 4 | 5 | export type BackgroundState = { 6 | background: string; 7 | backgroundColor: RGBColor; 8 | isParallax: boolean; 9 | modeFlag: ModeEnum; 10 | padding: number; 11 | lighten: number; 12 | darken: number; 13 | hasPadding: boolean; 14 | gradients: Gradient[]; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/src/types/translations.ts: -------------------------------------------------------------------------------- 1 | import type { defaultTranslations } from '../default/settings'; 2 | 3 | export type Translations = typeof defaultTranslations; 4 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/tsconfig-es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib-es", 5 | "module": "esnext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/plugins/layout/background/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib", "node_modules", "src/**/__tests__/**/*.*"], 9 | "references": [{ "path": "../../../editor" }] 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-admin/.npmignore: -------------------------------------------------------------------------------- 1 | src/**/* -------------------------------------------------------------------------------- /packages/react-admin/README.md: -------------------------------------------------------------------------------- 1 | # ReadMe 2 | 3 | see the ReactAdmin example in the docs 4 | -------------------------------------------------------------------------------- /packages/react-admin/babel.config.js: -------------------------------------------------------------------------------- 1 | const vendor = require('../../../babel.config.js'); 2 | module.exports = Object.assign({}, vendor); 3 | -------------------------------------------------------------------------------- /packages/react-admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-page/react-admin", 3 | "version": "0.0.0", 4 | "main": "./lib/index.js", 5 | "module": "./lib-es/index.js", 6 | "sideEffects": false, 7 | "typings": "./lib/index.d.ts", 8 | "author": "Panter AG", 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "npm-run-all --parallel build:lib", 12 | "build:watch": "npm-run-all --parallel build:lib:watch", 13 | "build:lib": "tsc -p ./tsconfig.json && tsc -p ./tsconfig-es.json", 14 | "build:lib:watch": "yarn build:lib -- --watch", 15 | "clean": "rimraf \"lib\" && rimraf \"lib-es\" && rm -f *.tsbuildinfo" 16 | }, 17 | "peerDependencies": { 18 | "@mui/material": "*", 19 | "react": ">= 16.14", 20 | "react-admin": "^3.0.0", 21 | "react-dom": ">= 16.14", 22 | "react-final-form": "*", 23 | "uniforms": "*" 24 | }, 25 | "dependencies": { 26 | "@mui/icons-material": "^5.8.0", 27 | "@react-page/editor": "0.0.0" 28 | }, 29 | "devDependencies": { 30 | "react-admin": "^3.12.2", 31 | "uniforms": "^3.2.0" 32 | }, 33 | "publishConfig": { 34 | "access": "public" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/react-admin/src/RaReactPageInput.tsx: -------------------------------------------------------------------------------- 1 | import { Paper } from '@mui/material'; 2 | import type { EditorProps } from '@react-page/editor'; 3 | import Editor from '@react-page/editor'; 4 | import React from 'react'; 5 | 6 | import { Labeled, useInput } from 'react-admin'; 7 | 8 | export type RaReactPageInputProps = { 9 | label?: string; 10 | source: string; 11 | style?: React.CSSProperties; 12 | } & EditorProps; 13 | const RaReactPageInput: React.FC<RaReactPageInputProps> = ({ 14 | label = 'Content', 15 | source, 16 | style, 17 | ...editorProps 18 | }) => { 19 | const { 20 | input: { value, onChange }, 21 | } = useInput({ source }); 22 | return ( 23 | <Labeled label={label} source={source} fullWidth> 24 | <> 25 | <Paper 26 | elevation={5} 27 | style={{ 28 | overflow: 'visible', 29 | padding: 16, 30 | marginRight: 64, 31 | 32 | ...style, 33 | }} 34 | > 35 | <Editor value={value} onChange={onChange} {...editorProps} /> 36 | </Paper> 37 | </> 38 | </Labeled> 39 | ); 40 | }; 41 | 42 | export default RaReactPageInput; 43 | -------------------------------------------------------------------------------- /packages/react-admin/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazyLoad } from '@react-page/editor'; 2 | 3 | // lazyload everything to avoid accidental bundle size increase 4 | export const RaReactPageInput = lazyLoad(() => import('./RaReactPageInput')); 5 | export const RaSelectReferenceInputField = lazyLoad( 6 | () => import('./RaSelectReferenceInputField') 7 | ); 8 | -------------------------------------------------------------------------------- /packages/react-admin/tsconfig-es.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "lib-es", 5 | "module": "esnext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["lib", "node_modules", "src/**/__tests__/**/*.*"], 9 | "references": [{ "path": "../editor" }] 10 | } 11 | -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "editor" }, 5 | 6 | { "path": "plugins/content/divider" }, 7 | { "path": "plugins/content/html5-video" }, 8 | { "path": "plugins/content/image" }, 9 | 10 | { "path": "plugins/content/slate" }, 11 | { "path": "plugins/content/spacer" }, 12 | { "path": "plugins/content/video" }, 13 | { "path": "plugins/layout/background" }, 14 | { "path": "react-admin" } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "downlevelIteration": true, 5 | "rootDirs": ["src"], 6 | "outDir": "lib", 7 | "strict": true, 8 | "moduleResolution": "node", 9 | "target": "es5", 10 | "module": "commonjs", 11 | "jsx": "react", 12 | "experimentalDecorators": true, 13 | "esModuleInterop": true, 14 | "sourceMap": true, 15 | "skipLibCheck": true, 16 | "skipDefaultLibCheck": true, 17 | "noUnusedLocals": false, 18 | "declaration": true, 19 | "declarationMap": true, 20 | "composite": true, 21 | "lib": ["es7", "dom"], 22 | "types": ["node", "jest"] 23 | } 24 | } 25 | --------------------------------------------------------------------------------