├── packages ├── ssr │ ├── .example-env │ ├── server │ │ ├── common │ │ │ ├── index.ts │ │ │ ├── config.ts │ │ │ ├── logger.ts │ │ │ ├── error.ts │ │ │ └── errorHandler.ts │ │ ├── index.ts │ │ ├── routes.ts │ │ ├── routes │ │ │ └── ssr │ │ │ │ ├── ssr.ctrl.tsx │ │ │ │ └── ssr.service.tsx │ │ └── app.ts │ ├── client │ │ ├── index.tsx │ │ ├── pages │ │ │ ├── MinimalPage.tsx │ │ │ └── AtlassianPage.tsx │ │ ├── components │ │ │ └── Layout.tsx │ │ └── routes.tsx │ ├── rollup.config.server.js │ ├── tsconfig.json │ └── rollup.config.client.js ├── api-collab │ ├── .example-env │ ├── src │ │ ├── types │ │ │ ├── user.ts │ │ │ ├── document.ts │ │ │ └── request.ts │ │ ├── common │ │ │ ├── index.ts │ │ │ ├── config.ts │ │ │ ├── logger.ts │ │ │ ├── error.ts │ │ │ └── errorHandler.ts │ │ ├── index.ts │ │ ├── routes.ts │ │ ├── routes │ │ │ ├── doc_collab │ │ │ │ └── doc-collab.io.ts │ │ │ └── doc │ │ │ │ ├── document.io.ts │ │ │ │ └── document.svc.ts │ │ ├── app.ts │ │ └── socket-io │ │ │ └── types.ts │ ├── rollup.config.js │ ├── tsconfig.json │ └── README.md ├── minimal │ ├── src │ │ ├── index.ts │ │ ├── nodeviews │ │ │ ├── BlockQuote.scss │ │ │ ├── index.ts │ │ │ └── BlockQuote.tsx │ │ ├── Editor.scss │ │ ├── plugins.ts │ │ ├── actions.ts │ │ └── schema.ts │ ├── README.md │ ├── rollup.config.js │ ├── tsconfig.json │ └── package.json ├── full-v2 │ ├── src │ │ ├── core │ │ │ ├── pm │ │ │ │ ├── index.ts │ │ │ │ └── plugin.ts │ │ │ ├── types │ │ │ │ ├── editor-view.ts │ │ │ │ ├── command.ts │ │ │ │ └── editor.ts │ │ │ ├── index.ts │ │ │ ├── keymaps │ │ │ │ └── consts.ts │ │ │ └── utils │ │ │ │ └── document.ts │ │ ├── extensions │ │ │ ├── base │ │ │ │ ├── nodes │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── doc.ts │ │ │ │ │ ├── text.ts │ │ │ │ │ └── paragraph.ts │ │ │ │ ├── marks │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── em.ts │ │ │ │ │ ├── strong.ts │ │ │ │ │ └── groups.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pm-plugins │ │ │ │ │ ├── state.ts │ │ │ │ │ └── main.ts │ │ │ │ └── pm-utils │ │ │ │ │ └── getActive.ts │ │ │ ├── collab │ │ │ │ ├── types │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── pm-plugins │ │ │ │ │ └── state.ts │ │ │ ├── createPlugins.ts │ │ │ ├── blockquote │ │ │ │ ├── nodes │ │ │ │ │ └── blockquote.ts │ │ │ │ ├── pm-plugins │ │ │ │ │ ├── keymap.ts │ │ │ │ │ └── state.ts │ │ │ │ ├── pm-utils │ │ │ │ │ └── findBlockQuote.ts │ │ │ │ ├── commands │ │ │ │ │ └── index.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── BlockQuoteExtension.tsx │ │ │ │ └── nodeviews │ │ │ │ │ └── BlockQuoteView.ts │ │ │ ├── createReactExtension.ts │ │ │ ├── createSchema.ts │ │ │ └── index.ts │ │ ├── react │ │ │ ├── hooks │ │ │ │ ├── useSsrLayoutEffect.tsx │ │ │ │ └── createListenProps.ts │ │ │ └── index.ts │ │ ├── context │ │ │ ├── EditorContext.ts │ │ │ ├── index.ts │ │ │ ├── ExtensionProvider.ts │ │ │ ├── PluginsProvider.ts │ │ │ ├── analytics │ │ │ │ └── is-performance-api-available.ts │ │ │ └── collab │ │ │ │ └── replaceDocument.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── README.md │ └── rollup.config.js ├── full │ ├── src │ │ ├── core │ │ │ ├── pm │ │ │ │ ├── index.ts │ │ │ │ └── plugin.ts │ │ │ ├── types │ │ │ │ ├── editor-view.ts │ │ │ │ ├── command.ts │ │ │ │ ├── index.ts │ │ │ │ ├── editor-config.ts │ │ │ │ ├── pm-plugin.ts │ │ │ │ └── editor-plugin.ts │ │ │ ├── index.ts │ │ │ ├── keymaps │ │ │ │ └── consts.ts │ │ │ ├── PluginsProvider.ts │ │ │ ├── create │ │ │ │ ├── create-schema.ts │ │ │ │ └── ranks.ts │ │ │ └── EditorContext.ts │ │ ├── types │ │ │ └── editor-ui.ts │ │ ├── editor-plugins │ │ │ ├── index.ts │ │ │ ├── blockquote │ │ │ │ ├── pm-plugins │ │ │ │ │ └── keymap.ts │ │ │ │ ├── commands │ │ │ │ │ └── index.ts │ │ │ │ ├── pm-utils │ │ │ │ │ └── findBlockQuote.ts │ │ │ │ ├── nodeviews │ │ │ │ │ └── BlockQuoteView.ts │ │ │ │ └── index.tsx │ │ │ └── base │ │ │ │ └── pm-utils │ │ │ │ └── getActive.ts │ │ ├── react │ │ │ ├── portals │ │ │ │ └── index.ts │ │ │ └── hooks │ │ │ │ ├── useSsrLayoutEffect.tsx │ │ │ │ └── createListenProps.ts │ │ ├── schema │ │ │ ├── marks │ │ │ │ ├── index.ts │ │ │ │ ├── em.ts │ │ │ │ ├── strong.ts │ │ │ │ └── groups.ts │ │ │ ├── marks-obj.ts │ │ │ ├── nodes │ │ │ │ ├── index.ts │ │ │ │ ├── text.ts │ │ │ │ ├── doc.ts │ │ │ │ └── blockquote.ts │ │ │ └── inline-content.ts │ │ ├── index.ts │ │ ├── create-defaults.ts │ │ ├── ui │ │ │ ├── FullPage.tsx │ │ │ └── MarkButton.tsx │ │ ├── performance │ │ │ └── is-performance-api-available.ts │ │ ├── collab-api.ts │ │ └── utils │ │ │ └── document.ts │ ├── rollup.config.js │ ├── README.md │ └── tsconfig.json ├── atlassian │ ├── src │ │ ├── elements │ │ │ ├── Popup │ │ │ │ └── index.ts │ │ │ └── Item │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── plugins │ │ │ ├── quick-insert │ │ │ │ ├── plugin-key.ts │ │ │ │ ├── assets │ │ │ │ │ ├── panel-note.tsx │ │ │ │ │ ├── panel-success.tsx │ │ │ │ │ ├── panel-error.tsx │ │ │ │ │ ├── table.tsx │ │ │ │ │ ├── panel.tsx │ │ │ │ │ ├── panel-warning.tsx │ │ │ │ │ ├── decision.tsx │ │ │ │ │ ├── heading1.tsx │ │ │ │ │ ├── list.tsx │ │ │ │ │ ├── quote.tsx │ │ │ │ │ ├── action.tsx │ │ │ │ │ ├── divider.tsx │ │ │ │ │ ├── heading4.tsx │ │ │ │ │ ├── mention.tsx │ │ │ │ │ ├── heading2.tsx │ │ │ │ │ ├── fallback.tsx │ │ │ │ │ ├── heading5.tsx │ │ │ │ │ ├── expand.tsx │ │ │ │ │ ├── link.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── heading3.tsx │ │ │ │ │ ├── heading6.tsx │ │ │ │ │ └── emoji.tsx │ │ │ │ └── types.ts │ │ │ ├── type-ahead │ │ │ │ ├── pm-plugins │ │ │ │ │ ├── plugin-key.ts │ │ │ │ │ └── actions.ts │ │ │ │ ├── utils │ │ │ │ │ ├── is-query-active.ts │ │ │ │ │ └── find-query-mark.ts │ │ │ │ └── commands │ │ │ │ │ ├── set-current-index.ts │ │ │ │ │ ├── items-list-updated.ts │ │ │ │ │ ├── dismiss.ts │ │ │ │ │ ├── update-query.ts │ │ │ │ │ └── insert-query.ts │ │ │ ├── index.ts │ │ │ ├── blockquote │ │ │ │ ├── pm-plugins │ │ │ │ │ └── keymap.ts │ │ │ │ ├── commands │ │ │ │ │ └── index.ts │ │ │ │ ├── pm-utils │ │ │ │ │ └── findBlockQuote.ts │ │ │ │ └── ui │ │ │ │ │ └── BlockQuote.tsx │ │ │ └── base │ │ │ │ └── index.ts │ │ ├── schema │ │ │ ├── marks │ │ │ │ ├── index.ts │ │ │ │ ├── underline.ts │ │ │ │ ├── type-ahead-query.ts │ │ │ │ └── groups.ts │ │ │ ├── marks-obj.ts │ │ │ ├── nodes │ │ │ │ ├── index.ts │ │ │ │ ├── text.ts │ │ │ │ ├── blockquote.ts │ │ │ │ └── doc.ts │ │ │ └── inline-content.ts │ │ ├── react-portals │ │ │ ├── index.ts │ │ │ ├── PortalProvider.tsx │ │ │ └── PortalRenderer.tsx │ │ ├── provider-factory │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── typeAhead.ts │ │ ├── types │ │ │ ├── command.ts │ │ │ ├── pm-config.ts │ │ │ ├── index.ts │ │ │ ├── editor-config.ts │ │ │ ├── pm-plugin.ts │ │ │ └── editor-ui.ts │ │ ├── keymaps │ │ │ └── consts.ts │ │ ├── prosemirror-utils │ │ │ ├── helpers.ts │ │ │ ├── findSelectedNodeOfType.ts │ │ │ └── findParentOfNodeType.ts │ │ ├── theme │ │ │ └── math.ts │ │ ├── create-editor │ │ │ ├── create-plugins-list.ts │ │ │ ├── create-schema.ts │ │ │ └── sort-by-order.ts │ │ ├── EditorContext.tsx │ │ ├── utils │ │ │ ├── magic-box.ts │ │ │ └── selection.ts │ │ ├── performance │ │ │ └── is-performance-api-available.ts │ │ └── editor-appearance │ │ │ └── MarkButton.tsx │ ├── rollup.config.js │ └── tsconfig.json ├── client-cra │ ├── src │ │ ├── react-app-env.d.ts │ │ ├── types │ │ │ ├── document.ts │ │ │ └── toast.ts │ │ ├── stores │ │ │ ├── mobxConf.ts │ │ │ ├── AuthStore.ts │ │ │ ├── EditorStore.ts │ │ │ ├── index.ts │ │ │ └── ToastStore.ts │ │ ├── pages │ │ │ ├── MinimalPage.tsx │ │ │ ├── AtlassianPage.tsx │ │ │ └── FrontPage.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── document-api.ts │ │ └── components │ │ │ ├── Layout.tsx │ │ │ ├── editor │ │ │ ├── DesktopLayout.tsx │ │ │ └── MarkButton.tsx │ │ │ └── CollabInfo.tsx │ ├── public │ │ ├── favicon.ico │ │ └── manifest.json │ ├── README.md │ └── tsconfig.json ├── types │ ├── src │ │ ├── index.ts │ │ ├── utils.ts │ │ └── socket.ts │ ├── types │ │ ├── auth.d.ts │ │ ├── index.d.ts │ │ ├── utils.d.ts │ │ ├── collab.d.ts │ │ ├── socket-doc.d.ts │ │ ├── document.d.ts │ │ └── socket-collab.d.ts │ ├── rollup.config.js │ ├── tsconfig.json │ └── package.json ├── nextjs │ ├── .babelrc │ ├── src │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ ├── minimal.tsx │ │ │ ├── atlassian.tsx │ │ │ └── _document.tsx │ │ └── components │ │ │ ├── Layout.tsx │ │ │ └── NavBar.tsx │ ├── next-env.d.ts │ ├── tsconfig.json │ └── package.json └── prosemirror-utils │ ├── src │ ├── index.ts │ ├── helpers.ts │ └── findSelectedNodeOfType.ts │ ├── rollup.config.js │ ├── tsconfig.json │ └── package.json ├── pnpm-workspace.yaml ├── .prettierignore ├── .prettierrc ├── NOTICE ├── .gitignore ├── .eslintrc └── LICENSE /packages/ssr/.example-env: -------------------------------------------------------------------------------- 1 | PORT=3300 -------------------------------------------------------------------------------- /packages/api-collab/.example-env: -------------------------------------------------------------------------------- 1 | PORT=3400 -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /packages/minimal/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Editor } from './Editor' 2 | -------------------------------------------------------------------------------- /packages/full-v2/src/core/pm/index.ts: -------------------------------------------------------------------------------- 1 | export { PluginKey } from './plugin' 2 | -------------------------------------------------------------------------------- /packages/full/src/core/pm/index.ts: -------------------------------------------------------------------------------- 1 | export { PluginKey } from './plugin' 2 | -------------------------------------------------------------------------------- /packages/atlassian/src/elements/Popup/index.ts: -------------------------------------------------------------------------------- 1 | export { Popup } from './Popup' 2 | -------------------------------------------------------------------------------- /packages/full/src/types/editor-ui.ts: -------------------------------------------------------------------------------- 1 | export type EditorAppearance = 'full-page' 2 | -------------------------------------------------------------------------------- /packages/client-cra/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/types/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './socket' 2 | export * from './utils' 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | node_modules 3 | build 4 | .next 5 | package.json 6 | dist 7 | tmp -------------------------------------------------------------------------------- /packages/types/types/auth.d.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | id: string 3 | name: string 4 | } 5 | -------------------------------------------------------------------------------- /packages/api-collab/src/types/user.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | id: number 3 | name: string 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/atlassian/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Editor } from './Editor' 2 | export { Editor as default } from './Editor' 3 | -------------------------------------------------------------------------------- /packages/nextjs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [["styled-components", { "ssr": true }]] 4 | } 5 | -------------------------------------------------------------------------------- /packages/ssr/server/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config' 2 | export * from './logger' 3 | export * from './errorHandler' 4 | -------------------------------------------------------------------------------- /packages/api-collab/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config' 2 | export * from './logger' 3 | export * from './errorHandler' 4 | -------------------------------------------------------------------------------- /packages/full/src/editor-plugins/index.ts: -------------------------------------------------------------------------------- 1 | export { basePlugin } from './base' 2 | export { blockQuotePlugin } from './blockquote' 3 | -------------------------------------------------------------------------------- /packages/full/src/react/portals/index.ts: -------------------------------------------------------------------------------- 1 | export { PortalProvider } from './PortalProvider' 2 | export { PortalRenderer } from './PortalRenderer' 3 | -------------------------------------------------------------------------------- /packages/nextjs/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | export default function MyApp({ Component, pageProps }) { 2 | return 3 | } 4 | -------------------------------------------------------------------------------- /packages/api-collab/src/types/document.ts: -------------------------------------------------------------------------------- 1 | import { Step } from 'prosemirror-transform' 2 | 3 | export type PatchedStep = Step & { clientID: number } 4 | -------------------------------------------------------------------------------- /packages/client-cra/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeemuKoivisto/prosemirror-react-typescript-example/HEAD/packages/client-cra/public/favicon.ico -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/nodes/index.ts: -------------------------------------------------------------------------------- 1 | export { doc } from './doc' 2 | export { paragraph } from './paragraph' 3 | export { text } from './text' 4 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/plugin-key.ts: -------------------------------------------------------------------------------- 1 | import { PluginKey } from 'prosemirror-state' 2 | export const pluginKey = new PluginKey('quickInsertPluginKey') 3 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/type-ahead/pm-plugins/plugin-key.ts: -------------------------------------------------------------------------------- 1 | import { PluginKey } from 'prosemirror-state' 2 | export const pluginKey = new PluginKey('typeAheadPlugin') 3 | -------------------------------------------------------------------------------- /packages/client-cra/src/types/document.ts: -------------------------------------------------------------------------------- 1 | export type PMDoc = { 2 | [key: string]: any 3 | } 4 | export interface IDBDocument { 5 | id: string 6 | title: string 7 | doc: PMDoc 8 | } 9 | -------------------------------------------------------------------------------- /packages/full-v2/src/core/types/editor-view.ts: -------------------------------------------------------------------------------- 1 | export interface JSONEditorState { 2 | doc: { [key: string]: any } 3 | selection: { [key: string]: any } 4 | plugins: { [key: string]: any } 5 | } 6 | -------------------------------------------------------------------------------- /packages/full/src/core/types/editor-view.ts: -------------------------------------------------------------------------------- 1 | export interface JSONEditorState { 2 | doc: { [key: string]: any } 3 | selection: { [key: string]: any } 4 | plugins: { [key: string]: any } 5 | } 6 | -------------------------------------------------------------------------------- /packages/atlassian/src/schema/marks/index.ts: -------------------------------------------------------------------------------- 1 | export { underline } from './underline' 2 | export type { UnderlineDefinition } from './underline' 3 | 4 | export { typeAheadQuery } from './type-ahead-query' 5 | -------------------------------------------------------------------------------- /packages/atlassian/src/react-portals/index.ts: -------------------------------------------------------------------------------- 1 | export { PortalProvider } from './PortalProvider' 2 | export { PortalProviderAPI } from './PortalProviderAPI' 3 | export { PortalRenderer } from './PortalRenderer' 4 | -------------------------------------------------------------------------------- /packages/client-cra/src/stores/mobxConf.ts: -------------------------------------------------------------------------------- 1 | import { configure } from 'mobx' 2 | 3 | const settings = { enforceActions: 'observed' as 'observed' } 4 | 5 | export const confMobx = () => configure(settings) 6 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/collab/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface CollabParticipant { 2 | lastActive: number 3 | sessionId: string 4 | avatar: string 5 | name: string 6 | email: string 7 | } 8 | -------------------------------------------------------------------------------- /packages/full/src/schema/marks/index.ts: -------------------------------------------------------------------------------- 1 | export { em } from './em' 2 | export type { EmDefinition } from './em' 3 | 4 | export { strong } from './strong' 5 | export type { StrongDefinition } from './strong' 6 | -------------------------------------------------------------------------------- /packages/types/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './auth' 2 | export * from './collab' 3 | export * from './document' 4 | export * from './socket-collab' 5 | export * from './socket-doc' 6 | export * from './utils' 7 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/marks/index.ts: -------------------------------------------------------------------------------- 1 | export { em } from './em' 2 | export type { EmDefinition } from './em' 3 | 4 | export { strong } from './strong' 5 | export type { StrongDefinition } from './strong' 6 | -------------------------------------------------------------------------------- /packages/prosemirror-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export { findParentNodeOfType } from 'prosemirror-utils' 2 | 3 | export { findSelectedNodeOfType } from './findSelectedNodeOfType' 4 | export { safeInsert } from './safeInsert' 5 | -------------------------------------------------------------------------------- /packages/ssr/client/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { hydrate } from 'react-dom' 3 | 4 | import { ClientRoutes } from './routes' 5 | 6 | hydrate(, document.getElementById('root')) 7 | -------------------------------------------------------------------------------- /packages/minimal/src/nodeviews/BlockQuote.scss: -------------------------------------------------------------------------------- 1 | .blockquote { 2 | box-sizing: border-box; 3 | color: #6a737d; 4 | padding: 0 1em; 5 | border-left: 4px solid #dfe2e5; 6 | margin: 0.2rem 0 0 0; 7 | margin-right: 0; 8 | } 9 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export { basePlugin } from './base' 2 | export { blockQuotePlugin } from './blockquote' 3 | export { quickInsertPlugin } from './quick-insert' 4 | export { typeAheadPlugin } from './type-ahead' 5 | -------------------------------------------------------------------------------- /packages/full/src/react/hooks/useSsrLayoutEffect.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react' 2 | 3 | const useSsrLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect 4 | 5 | export default useSsrLayoutEffect 6 | -------------------------------------------------------------------------------- /packages/types/types/utils.d.ts: -------------------------------------------------------------------------------- 1 | export type Ok = { 2 | data: T 3 | } 4 | export type Error = { 5 | err: string 6 | code: number 7 | } 8 | export type Maybe = Ok | Error 9 | 10 | export { uuidv4 } from '../src/utils' 11 | -------------------------------------------------------------------------------- /packages/full-v2/src/react/hooks/useSsrLayoutEffect.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react' 2 | 3 | const useSsrLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect 4 | 5 | export default useSsrLayoutEffect 6 | -------------------------------------------------------------------------------- /packages/nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/full/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export { EditorViewProvider } from './EditorViewProvider' 2 | export { Context as EditorContext } from './EditorContext' 3 | export { PluginsProvider } from './PluginsProvider' 4 | export { AnalyticsProvider } from './AnalyticsProvider' 5 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/nodes/doc.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model' 2 | 3 | export const doc: NodeSpec = { 4 | content: 'block+', 5 | // marks: 6 | // 'alignment breakout indentation link unsupportedMark unsupportedNodeAttribute', 7 | } 8 | -------------------------------------------------------------------------------- /packages/types/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function uuidv4() { 2 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 3 | const r = (Math.random() * 16) | 0, 4 | v = c == 'x' ? r : (r & 0x3) | 0x8 5 | return v.toString(16) 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/index.ts: -------------------------------------------------------------------------------- 1 | export { basePluginKey } from './pm-plugins/state' 2 | export type { BaseState } from './pm-plugins/state' 3 | export { BaseExtension, baseSchema } from './BaseExtension' 4 | export type { BaseExtensionProps } from './BaseExtension' 5 | -------------------------------------------------------------------------------- /packages/full-v2/src/core/pm/plugin.ts: -------------------------------------------------------------------------------- 1 | import { PluginKey as PMPluginKey } from 'prosemirror-state' 2 | 3 | export class PluginKey extends PMPluginKey { 4 | readonly name: string 5 | constructor(name: string) { 6 | super(name) 7 | this.name = name 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/collab/index.ts: -------------------------------------------------------------------------------- 1 | export { collabEditPluginKey } from './pm-plugins/state' 2 | export type { CollabState } from './pm-plugins/state' 3 | export { CollabExtension } from './CollabExtension' 4 | export type { CollabExtensionProps } from './CollabExtension' 5 | -------------------------------------------------------------------------------- /packages/full/src/core/pm/plugin.ts: -------------------------------------------------------------------------------- 1 | import { PluginKey as PMPluginKey } from 'prosemirror-state' 2 | 3 | export class PluginKey extends PMPluginKey { 4 | readonly name: string 5 | constructor(name: string) { 6 | super(name) 7 | this.name = name 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/nodes/text.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model' 2 | 3 | export const text: NodeSpec & { toDebugString?: () => string } = { 4 | group: 'inline', 5 | toDebugString: process.env.NODE_ENV !== 'production' ? undefined : () => 'text_node', 6 | } 7 | -------------------------------------------------------------------------------- /packages/ssr/server/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from './app' 2 | import { config, log } from './common' 3 | 4 | app.listen(config.PORT, () => { 5 | log.info(`App started at port: ${config.PORT}`) 6 | }) 7 | 8 | process.on('exit', () => { 9 | log.info('Shutting down server') 10 | }) 11 | -------------------------------------------------------------------------------- /packages/client-cra/src/types/toast.ts: -------------------------------------------------------------------------------- 1 | export type ToastType = 'success' | 'danger' | 'warning' | 'info' 2 | export type ToastLocation = 'bottom-left' | 'top-right' 3 | 4 | export interface IToast { 5 | id: number 6 | message: string 7 | type: ToastType 8 | duration: number 9 | } 10 | -------------------------------------------------------------------------------- /packages/full/src/schema/marks-obj.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @additionalProperties true 3 | */ 4 | export interface MarksObject { 5 | marks?: Array 6 | } 7 | 8 | /** 9 | * @additionalProperties true 10 | */ 11 | export interface NoMark { 12 | /** 13 | * @maxItems 0 14 | */ 15 | marks?: Array 16 | } 17 | -------------------------------------------------------------------------------- /packages/api-collab/src/types/request.ts: -------------------------------------------------------------------------------- 1 | // import * as Joi from '@hapi/joi' 2 | import { Request } from 'express' 3 | import { ParamsDictionary } from 'express-serve-static-core' 4 | 5 | export interface IRequest extends Request { 6 | body: T 7 | queryParams: P 8 | } 9 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/type-ahead/pm-plugins/actions.ts: -------------------------------------------------------------------------------- 1 | export const ACTIONS = { 2 | SELECT_PREV: 'SELECT_PREV', 3 | SELECT_NEXT: 'SELECT_NEXT', 4 | SELECT_CURRENT: 'SELECT_CURRENT', 5 | SET_CURRENT_INDEX: 'SET_CURRENT_INDEX', 6 | SET_QUERY: 'SET_QUERY', 7 | ITEMS_LIST_UPDATED: 'ITEMS_LIST_UPDATED', 8 | } 9 | -------------------------------------------------------------------------------- /packages/atlassian/src/schema/marks-obj.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @additionalProperties true 3 | */ 4 | export interface MarksObject { 5 | marks?: Array 6 | } 7 | 8 | /** 9 | * @additionalProperties true 10 | */ 11 | export interface NoMark { 12 | /** 13 | * @maxItems 0 14 | */ 15 | marks?: Array 16 | } 17 | -------------------------------------------------------------------------------- /packages/atlassian/src/elements/Item/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Item' 2 | export { default as ItemGroup } from './ItemGroup' 3 | // export { default as withItemClick } from './hoc/withItemClick'; 4 | // export { default as withItemFocus } from './hoc/withItemFocus'; 5 | 6 | export { themeNamespace as itemThemeNamespace } from './util/theme' 7 | -------------------------------------------------------------------------------- /packages/full-v2/src/react/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useSsrLayoutEffect } from './hooks/useSsrLayoutEffect' 2 | 3 | export { PortalProvider } from './portals/PortalProvider' 4 | export { PortalRenderer } from './portals/PortalRenderer' 5 | 6 | export { ReactNodeView } from './ReactNodeView' 7 | export type { NodeViewProps } from './ReactNodeView' 8 | -------------------------------------------------------------------------------- /packages/ssr/server/routes.ts: -------------------------------------------------------------------------------- 1 | import express, { Router } from 'express' 2 | 3 | import * as ssrCtrl from './routes/ssr/ssr.ctrl' 4 | 5 | const router: Router = Router() 6 | 7 | router.use('', express.static('./client-dist')) 8 | 9 | router.get('/raw', ssrCtrl.ssrRawHtml) 10 | router.get('/*', ssrCtrl.ssrReactApp) 11 | 12 | export default router 13 | -------------------------------------------------------------------------------- /packages/full/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Editor } from './Editor' 2 | export { EditorViewProvider } from './core/EditorViewProvider' 3 | export type { JSONEditorState } from './core/types/editor-view' 4 | 5 | export { createDefaultSchema } from './create-defaults' 6 | 7 | export { EditorView } from 'prosemirror-view' 8 | export { EditorState } from 'prosemirror-state' 9 | -------------------------------------------------------------------------------- /packages/atlassian/src/provider-factory/index.ts: -------------------------------------------------------------------------------- 1 | export { ProviderFactory } from './ProviderFactory' 2 | 3 | export type { QuickInsertItem, QuickInsertProvider } from './quick-insert-provider' 4 | 5 | export type { TypeAheadItemRenderProps, TypeAheadItem } from './typeAhead' 6 | 7 | export type { ProviderHandler, ProviderName, Providers, ProviderType } from './types' 8 | -------------------------------------------------------------------------------- /packages/minimal/src/nodeviews/index.ts: -------------------------------------------------------------------------------- 1 | import { Node } from 'prosemirror-model' 2 | import { EditorView, Decoration } from 'prosemirror-view' 3 | 4 | import { BlockQuoteView } from './BlockQuoteView' 5 | 6 | export const nodeViews = { 7 | blockquote: (node: Node, view: EditorView, getPos: () => number) => 8 | new BlockQuoteView(node, view, getPos), 9 | } 10 | -------------------------------------------------------------------------------- /packages/types/src/socket.ts: -------------------------------------------------------------------------------- 1 | export enum ECollabAction { 2 | COLLAB_USERS_CHANGED = 'COLLAB:USERS_CHANGED', 3 | COLLAB_CLIENT_EDIT = 'COLLAB:CLIENT_EDIT', 4 | COLLAB_SERVER_UPDATE = 'COLLAB:SERVER_UPDATE', 5 | } 6 | 7 | export enum EDocAction { 8 | DOC_CREATE = 'doc:create', 9 | DOC_DELETE = 'doc:delete', 10 | DOC_VISIBILITY = 'doc:visibility', 11 | } 12 | -------------------------------------------------------------------------------- /packages/atlassian/src/types/command.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, Transaction } from 'prosemirror-state' 2 | import { EditorView } from 'prosemirror-view' 3 | 4 | export type CommandDispatch = (tr: Transaction) => void 5 | export type Command = (state: EditorState, dispatch?: CommandDispatch, view?: EditorView) => boolean 6 | export type HigherOrderCommand = (command: Command) => Command 7 | -------------------------------------------------------------------------------- /packages/full/src/core/types/command.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, Transaction } from 'prosemirror-state' 2 | import { EditorView } from 'prosemirror-view' 3 | 4 | export type CommandDispatch = (tr: Transaction) => void 5 | export type Command = (state: EditorState, dispatch?: CommandDispatch, view?: EditorView) => boolean 6 | export type HigherOrderCommand = (command: Command) => Command 7 | -------------------------------------------------------------------------------- /packages/full-v2/src/core/types/command.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, Transaction } from 'prosemirror-state' 2 | import { EditorView } from 'prosemirror-view' 3 | 4 | export type CommandDispatch = (tr: Transaction) => void 5 | export type Command = (state: EditorState, dispatch?: CommandDispatch, view?: EditorView) => boolean 6 | export type HigherOrderCommand = (command: Command) => Command 7 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/createPlugins.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'prosemirror-state' 2 | import { IExtension, ExtensionPlugin } from './Extension' 3 | 4 | export function createPlugins(extensions: IExtension[]) { 5 | const plugins = extensions.reduce((acc, cur) => [...acc, ...cur.plugins], [] as ExtensionPlugin[]) 6 | return plugins.reduce((acc, p) => [...acc, p.plugin()], [] as Plugin[]) 7 | } 8 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | prosemirror-react-typescript-example 2 | Copyright 2020 Teemu Koivisto 3 | 4 | The Initial Developer of some parts of the application, which are copied from, derived from, or 5 | inspired by Atlassian Editor (at https://bitbucket.org/atlassian/atlassian-frontend-mirror/src/master/), 6 | is Atlassian Pty Ltd (https://www.atlassian.com/), available under the Apache 2.0 license. 7 | Copyright 2020 Atlassian Pty Ltd -------------------------------------------------------------------------------- /packages/client-cra/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/full-v2/src/context/EditorContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | 3 | import { EditorContext, createDefaultProviders } from './Providers' 4 | 5 | export type { EditorContext } from './Providers' 6 | 7 | export const ReactEditorContext = createContext(createDefaultProviders()) 8 | 9 | export const useEditorContext = () => useContext(ReactEditorContext) 10 | -------------------------------------------------------------------------------- /packages/api-collab/src/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from './app' 2 | import { config, log } from './common' 3 | import { socketIO } from './socket-io/socketIO' 4 | 5 | const server = app.listen(config.PORT, () => { 6 | log.info(`App started at port: ${config.PORT}`) 7 | }) 8 | 9 | socketIO.start(server) 10 | 11 | process.on('exit', () => { 12 | socketIO.stop() 13 | log.info('Shutting down server') 14 | }) 15 | -------------------------------------------------------------------------------- /packages/full/src/schema/marks/em.ts: -------------------------------------------------------------------------------- 1 | import { MarkSpec } from 'prosemirror-model' 2 | import { FONT_STYLE } from './groups' 3 | 4 | export interface EmDefinition { 5 | type: 'em' 6 | } 7 | 8 | export const em: MarkSpec = { 9 | inclusive: true, 10 | group: FONT_STYLE, 11 | parseDOM: [{ tag: 'i' }, { tag: 'em' }, { style: 'font-style=italic' }], 12 | toDOM() { 13 | return ['em'] 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/type-ahead/utils/is-query-active.ts: -------------------------------------------------------------------------------- 1 | import { MarkType, Node } from 'prosemirror-model' 2 | 3 | export function isQueryActive(mark: MarkType, doc: Node, from: number, to: number) { 4 | let active = false 5 | 6 | doc.nodesBetween(from, to, (node) => { 7 | if (!active && mark.isInSet(node.marks)) { 8 | active = true 9 | } 10 | }) 11 | 12 | return active 13 | } 14 | -------------------------------------------------------------------------------- /packages/atlassian/src/types/pm-config.ts: -------------------------------------------------------------------------------- 1 | import { MarkSpec, NodeSpec } from 'prosemirror-model' 2 | import { NodeView } from 'prosemirror-view' 3 | 4 | export interface NodeConfig { 5 | name: string 6 | node: NodeSpec 7 | } 8 | 9 | export interface MarkConfig { 10 | name: string 11 | mark: MarkSpec 12 | } 13 | 14 | export interface NodeViewConfig { 15 | name: string 16 | nodeView: NodeView 17 | } 18 | -------------------------------------------------------------------------------- /packages/full-v2/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export { Editor } from './Editor' 2 | export type { JSONEditorState } from './types/editor-view' 3 | 4 | export { PluginKey } from './pm/plugin' 5 | export type { Command, CommandDispatch } from './types/command' 6 | 7 | export * as keymaps from './keymaps' 8 | 9 | export { EventDispatcher } from './utils/EventDispatcher' 10 | export { parseRawValue } from './utils/document' 11 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/blockquote/nodes/blockquote.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model' 2 | 3 | export const blockquote: NodeSpec = { 4 | content: 'paragraph+', 5 | group: 'block', 6 | defining: true, 7 | selectable: false, 8 | attrs: { 9 | class: { default: '' }, 10 | }, 11 | parseDOM: [{ tag: 'blockquote' }], 12 | toDOM() { 13 | return ['blockquote', 0] 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /packages/atlassian/src/keymaps/consts.ts: -------------------------------------------------------------------------------- 1 | export const LEFT = 37 2 | export const RIGHT = 39 3 | export const UP = 38 4 | export const DOWN = 40 5 | export const KEY_0 = 48 6 | export const KEY_1 = 49 7 | export const KEY_2 = 50 8 | export const KEY_3 = 51 9 | export const KEY_4 = 52 10 | export const KEY_5 = 53 11 | export const KEY_6 = 54 12 | export const HEADING_KEYS = [KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6] 13 | -------------------------------------------------------------------------------- /packages/atlassian/src/schema/nodes/index.ts: -------------------------------------------------------------------------------- 1 | export { doc } from './doc' 2 | export type { DocNode } from './doc' 3 | export { blockquote } from './blockquote' 4 | export type { BlockQuoteDefinition } from './blockquote' 5 | export { paragraph } from './paragraph' 6 | export type { ParagraphDefinition, ParagraphBaseDefinition } from './paragraph' 7 | export { text } from './text' 8 | export type { TextDefinition } from './text' 9 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/marks/em.ts: -------------------------------------------------------------------------------- 1 | import { MarkSpec } from 'prosemirror-model' 2 | import { FONT_STYLE } from './groups' 3 | 4 | export interface EmDefinition { 5 | type: 'em' 6 | } 7 | 8 | export const em: MarkSpec = { 9 | inclusive: true, 10 | group: FONT_STYLE, 11 | parseDOM: [{ tag: 'i' }, { tag: 'em' }, { style: 'font-style=italic' }], 12 | toDOM() { 13 | return ['em'] 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /packages/full/src/core/keymaps/consts.ts: -------------------------------------------------------------------------------- 1 | export const LEFT = 37 2 | export const RIGHT = 39 3 | export const UP = 38 4 | export const DOWN = 40 5 | export const KEY_0 = 48 6 | export const KEY_1 = 49 7 | export const KEY_2 = 50 8 | export const KEY_3 = 51 9 | export const KEY_4 = 52 10 | export const KEY_5 = 53 11 | export const KEY_6 = 54 12 | export const HEADING_KEYS = [KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6] 13 | -------------------------------------------------------------------------------- /packages/full-v2/src/core/keymaps/consts.ts: -------------------------------------------------------------------------------- 1 | export const LEFT = 37 2 | export const RIGHT = 39 3 | export const UP = 38 4 | export const DOWN = 40 5 | export const KEY_0 = 48 6 | export const KEY_1 = 49 7 | export const KEY_2 = 50 8 | export const KEY_3 = 51 9 | export const KEY_4 = 52 10 | export const KEY_5 = 53 11 | export const KEY_6 = 54 12 | export const HEADING_KEYS = [KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6] 13 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/nodes/paragraph.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model' 2 | 3 | export const paragraph: NodeSpec = { 4 | content: 'inline*', 5 | group: 'block', 6 | selectable: false, 7 | // marks: 8 | // 'strong code em link strike subsup textColor underline unsupportedMark unsupportedNodeAttribute', 9 | parseDOM: [{ tag: 'p' }], 10 | toDOM() { 11 | return ['p', 0] 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/full/src/schema/nodes/index.ts: -------------------------------------------------------------------------------- 1 | export { doc } from './doc' 2 | export type { DocNode } from './doc' 3 | export { blockquote, pmBlockquote } from './blockquote' 4 | export type { BlockQuoteDefinition } from './blockquote' 5 | export { paragraph } from './paragraph' 6 | export type { ParagraphDefinition, ParagraphBaseDefinition } from './paragraph' 7 | export { text } from './text' 8 | export type { TextDefinition } from './text' 9 | -------------------------------------------------------------------------------- /packages/full/src/core/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { CommandDispatch, Command, HigherOrderCommand } from './command' 2 | 3 | export type { EditorConfig, NodeConfig, MarkConfig, NodeViewConfig } from './editor-config' 4 | 5 | export type { PluginsOptions, EditorPlugin } from './editor-plugin' 6 | 7 | export type { 8 | PMPluginFactoryParams, 9 | PMPluginFactory, 10 | PMPluginCreateConfig, 11 | PMPlugin, 12 | } from './pm-plugin' 13 | -------------------------------------------------------------------------------- /packages/full-v2/src/core/types/editor.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditorState } from 'prosemirror-state' 3 | 4 | import { EditorContext } from '@context' 5 | 6 | export type EditorAppearance = 'full-page' 7 | 8 | export interface EditorProps { 9 | children: React.ReactNode 10 | disabled?: boolean 11 | appearance?: EditorAppearance 12 | onEditorReady?: (ctx: EditorContext) => void 13 | onDocumentEdit?: (newState: EditorState) => void 14 | } 15 | -------------------------------------------------------------------------------- /packages/api-collab/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | 3 | import pkg from './package.json' 4 | 5 | export default { 6 | input: ['src/index.ts'], 7 | output: [ 8 | { 9 | file: './dist/index.js', 10 | format: 'cjs', 11 | }, 12 | { 13 | file: './dist/index.es.js', 14 | format: 'es', 15 | }, 16 | ], 17 | external: [...Object.keys(pkg.dependencies || {})], 18 | plugins: [typescript()], 19 | } 20 | -------------------------------------------------------------------------------- /packages/full/src/schema/nodes/text.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model' 2 | 3 | /** 4 | * @name text_node 5 | */ 6 | export interface TextDefinition { 7 | type: 'text' 8 | /** 9 | * @minLength 1 10 | */ 11 | text: string 12 | marks?: Array 13 | } 14 | 15 | export const text: NodeSpec & { toDebugString?: () => string } = { 16 | group: 'inline', 17 | toDebugString: process.env.NODE_ENV !== 'production' ? undefined : () => 'text_node', 18 | } 19 | -------------------------------------------------------------------------------- /packages/minimal/src/Editor.scss: -------------------------------------------------------------------------------- 1 | #minimal-editor { 2 | border: 1px solid black; 3 | & > .ProseMirror { 4 | min-height: 140px; 5 | overflow-wrap: break-word; 6 | outline: none; 7 | padding: 10px; 8 | white-space: pre-wrap; 9 | } 10 | 11 | .pm-blockquote { 12 | box-sizing: border-box; 13 | color: #2d82e1; 14 | padding: 0 1em; 15 | border-left: 4px solid #48a1fa; 16 | margin: 0.2rem 0 0 0; 17 | margin-right: 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/atlassian/src/schema/nodes/text.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model' 2 | 3 | /** 4 | * @name text_node 5 | */ 6 | export interface TextDefinition { 7 | type: 'text' 8 | /** 9 | * @minLength 1 10 | */ 11 | text: string 12 | marks?: Array 13 | } 14 | 15 | export const text: NodeSpec & { toDebugString?: () => string } = { 16 | group: 'inline', 17 | toDebugString: process.env.NODE_ENV !== 'production' ? undefined : () => 'text_node', 18 | } 19 | -------------------------------------------------------------------------------- /packages/atlassian/src/provider-factory/types.ts: -------------------------------------------------------------------------------- 1 | import { QuickInsertProvider } from './quick-insert-provider' 2 | 3 | export interface Providers { 4 | quickInsertProvider?: Promise 5 | } 6 | 7 | export type ProviderName = keyof Providers 8 | export type ProviderType = T extends keyof Providers ? Providers[T] : Promise 9 | 10 | export type ProviderHandler = ( 11 | name: T, 12 | provider?: ProviderType 13 | ) => void 14 | -------------------------------------------------------------------------------- /packages/minimal/README.md: -------------------------------------------------------------------------------- 1 | # Minimal editor 2 | 3 | This is the minimal editor as its own npm library. 4 | 5 | ## How to install 6 | 7 | This project uses Yarn workspaces so you don't really have to install anything. By running `yarn` in the root folder all the dependencies should be installed automatically. 8 | 9 | ## Commands 10 | 11 | - `yarn watch` starts the Rollup compiler and watches changes to the editor 12 | - `yarn build` compiles the code as both CommonJS and ES module. 13 | -------------------------------------------------------------------------------- /packages/ssr/rollup.config.server.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | 3 | import pkg from './package.json' 4 | 5 | export default { 6 | input: ['server/index.ts'], 7 | output: [ 8 | { 9 | file: './server-dist/index.js', 10 | format: 'cjs', 11 | }, 12 | { 13 | file: './server-dist/index.es.js', 14 | format: 'es', 15 | }, 16 | ], 17 | external: [...Object.keys(pkg.dependencies || {})], 18 | plugins: [typescript()], 19 | } 20 | -------------------------------------------------------------------------------- /packages/types/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | 3 | import pkg from './package.json' 4 | 5 | export default { 6 | input: 'src/index.ts', 7 | output: [ 8 | { 9 | file: pkg.main, 10 | format: 'cjs', 11 | }, 12 | { 13 | file: pkg.module, 14 | format: 'es', 15 | }, 16 | ], 17 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})], 18 | plugins: [typescript()], 19 | } 20 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/blockquote/pm-plugins/keymap.ts: -------------------------------------------------------------------------------- 1 | import { keymap } from 'prosemirror-keymap' 2 | import { Plugin } from 'prosemirror-state' 3 | import { bindKeymapWithCommand, toggleBlockQuote } from '../../../keymaps' 4 | import { createNewBlockQuote } from '../commands' 5 | 6 | export function keymapPlugin(): Plugin { 7 | const keymapObj = {} 8 | 9 | bindKeymapWithCommand(toggleBlockQuote.common!, createNewBlockQuote(), keymapObj) 10 | 11 | return keymap(keymapObj) 12 | } 13 | -------------------------------------------------------------------------------- /packages/prosemirror-utils/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Node as PMNode, Fragment, NodeType, ResolvedPos } from 'prosemirror-model' 2 | 3 | export const canInsert = ($pos: ResolvedPos, content: PMNode | Fragment) => { 4 | const index = $pos.index() 5 | 6 | if (content instanceof Fragment) { 7 | return $pos.parent.canReplace(index, index, content) 8 | } else if (content instanceof PMNode) { 9 | return $pos.parent.canReplaceWith(index, index, content.type) 10 | } 11 | return false 12 | } 13 | -------------------------------------------------------------------------------- /packages/full/src/editor-plugins/blockquote/pm-plugins/keymap.ts: -------------------------------------------------------------------------------- 1 | import { keymap } from 'prosemirror-keymap' 2 | import { Plugin } from 'prosemirror-state' 3 | import { bindKeymapWithCommand, toggleBlockQuote } from '../../../core/keymaps' 4 | import { createNewBlockQuote } from '../commands' 5 | 6 | export function keymapPlugin(): Plugin { 7 | const keymapObj = {} 8 | 9 | bindKeymapWithCommand(toggleBlockQuote.common!, createNewBlockQuote(), keymapObj) 10 | 11 | return keymap(keymapObj) 12 | } 13 | -------------------------------------------------------------------------------- /packages/full/src/react/hooks/createListenProps.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { PortalProvider } from '../portals' 3 | 4 | export function createListenProps(container: HTMLElement, portalProvider: PortalProvider) { 5 | return function (cb: (newProps: A) => void) { 6 | useEffect(() => { 7 | portalProvider.subscribe(container, cb) 8 | return () => { 9 | portalProvider.unsubscribe(container, cb) 10 | } 11 | }, []) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/prosemirror-utils/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | 3 | import pkg from './package.json' 4 | 5 | export default { 6 | input: 'src/index.ts', 7 | output: [ 8 | { 9 | file: pkg.main, 10 | format: 'cjs', 11 | }, 12 | { 13 | file: pkg.module, 14 | format: 'es', 15 | }, 16 | ], 17 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})], 18 | plugins: [typescript()], 19 | } 20 | -------------------------------------------------------------------------------- /packages/nextjs/src/pages/minimal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { Editor } from '@example/minimal' 5 | 6 | import { Layout } from '../components/Layout' 7 | import { PageHeader } from '../components/PageHeader' 8 | 9 | interface IProps {} 10 | 11 | export default function MinimalPage(props: IProps) { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/full-v2/src/react/hooks/createListenProps.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { PortalProvider } from '../portals/PortalProvider' 3 | 4 | export function createListenProps(container: HTMLElement, portalProvider: PortalProvider) { 5 | return function (cb: (newProps: A) => void) { 6 | useEffect(() => { 7 | portalProvider.subscribe(container, cb) 8 | return () => { 9 | portalProvider.unsubscribe(container, cb) 10 | } 11 | }, []) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/type-ahead/commands/set-current-index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../types' 2 | import { ACTIONS, pluginKey } from '../pm-plugins/main' 3 | 4 | export const setCurrentIndex = 5 | (currentIndex: number): Command => 6 | (state, dispatch) => { 7 | if (dispatch) { 8 | dispatch( 9 | state.tr.setMeta(pluginKey, { 10 | action: ACTIONS.SET_CURRENT_INDEX, 11 | params: { currentIndex }, 12 | }) 13 | ) 14 | } 15 | return true 16 | } 17 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/blockquote/pm-plugins/keymap.ts: -------------------------------------------------------------------------------- 1 | import { keymap } from 'prosemirror-keymap' 2 | import { Plugin } from 'prosemirror-state' 3 | import { keymaps } from '@core' 4 | import { createNewBlockQuote } from '../commands' 5 | 6 | const { bindKeymapWithCommand, toggleBlockQuote } = keymaps 7 | 8 | export function keymapPlugin(): Plugin { 9 | const keymapObj = {} 10 | 11 | bindKeymapWithCommand(toggleBlockQuote.common!, createNewBlockQuote(), keymapObj) 12 | 13 | return keymap(keymapObj) 14 | } 15 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/blockquote/pm-utils/findBlockQuote.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, Selection } from 'prosemirror-state' 2 | import { findSelectedNodeOfType, findParentNodeOfType } from '@example/prosemirror-utils' 3 | 4 | export function findBlockQuote(state: EditorState, selection?: Selection | null) { 5 | const { blockquote } = state.schema.nodes 6 | return ( 7 | findSelectedNodeOfType(blockquote)(selection || state.selection) || 8 | findParentNodeOfType(blockquote)(selection || state.selection) 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /packages/atlassian/src/provider-factory/typeAhead.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | 3 | export type TypeAheadItemRenderProps = { 4 | onClick: () => void 5 | onHover: () => void 6 | isSelected: boolean 7 | } 8 | 9 | export type TypeAheadItem = { 10 | title: string 11 | description?: string 12 | keyshortcut?: string 13 | key?: string | number 14 | icon?: () => ReactElement 15 | render?: (props: TypeAheadItemRenderProps) => React.ReactElement | null 16 | [key: string]: any 17 | } 18 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/blockquote/commands/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@core' 2 | 3 | export const createNewBlockQuote = 4 | (): Command => 5 | (state, dispatch): boolean => { 6 | const { $from, $to } = state.selection 7 | const blockquote = state.schema.nodes.blockquote 8 | const empty = blockquote.createAndFill() 9 | const endOfBlock = $from.end() 10 | if (empty && dispatch) { 11 | const tr = state.tr.insert(endOfBlock + 1, empty) 12 | dispatch(tr) 13 | } 14 | return false 15 | } 16 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/blockquote/commands/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../types' 2 | 3 | export const createNewBlockQuote = 4 | (): Command => 5 | (state, dispatch): boolean => { 6 | const { $from, $to } = state.selection 7 | const blockquote = state.schema.nodes.blockquote 8 | const empty = blockquote.createAndFill() 9 | const endOfBlock = $from.end() 10 | if (empty && dispatch) { 11 | const tr = state.tr.insert(endOfBlock + 1, empty) 12 | dispatch(tr) 13 | } 14 | return false 15 | } 16 | -------------------------------------------------------------------------------- /packages/full/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import postcss from 'rollup-plugin-postcss' 3 | 4 | import pkg from './package.json' 5 | 6 | export default { 7 | input: ['src/index.ts'], 8 | output: [ 9 | { 10 | file: pkg.main, 11 | format: 'cjs', 12 | }, 13 | { 14 | file: pkg.module, 15 | format: 'es', 16 | }, 17 | ], 18 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], 19 | plugins: [typescript(), postcss()], 20 | } 21 | -------------------------------------------------------------------------------- /packages/atlassian/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import postcss from 'rollup-plugin-postcss' 3 | 4 | import pkg from './package.json' 5 | 6 | export default { 7 | input: ['src/index.ts'], 8 | output: [ 9 | { 10 | file: pkg.main, 11 | format: 'cjs', 12 | }, 13 | { 14 | file: pkg.module, 15 | format: 'es', 16 | }, 17 | ], 18 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], 19 | plugins: [typescript(), postcss()], 20 | } 21 | -------------------------------------------------------------------------------- /packages/full/src/editor-plugins/blockquote/commands/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../core/types' 2 | 3 | export const createNewBlockQuote = 4 | (): Command => 5 | (state, dispatch): boolean => { 6 | const { $from, $to } = state.selection 7 | const blockquote = state.schema.nodes.blockquote 8 | const empty = blockquote.createAndFill() 9 | const endOfBlock = $from.end() 10 | if (empty && dispatch) { 11 | const tr = state.tr.insert(endOfBlock + 1, empty) 12 | dispatch(tr) 13 | } 14 | return false 15 | } 16 | -------------------------------------------------------------------------------- /packages/minimal/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import postcss from 'rollup-plugin-postcss' 3 | 4 | import pkg from './package.json' 5 | 6 | export default { 7 | input: 'src/index.ts', 8 | output: [ 9 | { 10 | file: pkg.main, 11 | format: 'cjs', 12 | }, 13 | { 14 | file: pkg.module, 15 | format: 'es', 16 | }, 17 | ], 18 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], 19 | plugins: [typescript(), postcss()], 20 | } 21 | -------------------------------------------------------------------------------- /packages/api-collab/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "baseUrl": "src", 9 | "allowJs": true, 10 | // "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true 16 | }, 17 | "include": ["src"], 18 | "exclude": ["*/__tests__"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/ssr/server/common/config.ts: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === undefined || process.env.NODE_ENV === 'local') { 2 | require('dotenv').config() 3 | } 4 | 5 | function parseNodeEnv(NODE_ENV): 'production' | 'local' { 6 | if (NODE_ENV === 'production') return 'production' 7 | return 'local' 8 | } 9 | 10 | export const config = { 11 | ENV: parseNodeEnv(process.env.NODE_ENV), 12 | PORT: process.env.PORT || 3300, 13 | CORS_SAME_ORIGIN: process.env.CORS_SAME_ORIGIN || false, 14 | LOG: { 15 | LEVEL: process.env.LOG_LEVEL || 'info', 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /packages/api-collab/src/common/config.ts: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === undefined || process.env.NODE_ENV === 'local') { 2 | require('dotenv').config() 3 | } 4 | 5 | function parseNodeEnv(NODE_ENV): 'production' | 'local' { 6 | if (NODE_ENV === 'production') return 'production' 7 | return 'local' 8 | } 9 | 10 | export const config = { 11 | ENV: parseNodeEnv(process.env.NODE_ENV), 12 | PORT: process.env.PORT || 3400, 13 | CORS_SAME_ORIGIN: process.env.CORS_SAME_ORIGIN || false, 14 | LOG: { 15 | LEVEL: process.env.LOG_LEVEL || 'info', 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /packages/client-cra/src/pages/MinimalPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { Editor } from '@example/minimal' 5 | 6 | import { PageHeader } from '../components/PageHeader' 7 | 8 | interface IProps { 9 | className?: string 10 | } 11 | 12 | export function MinimalPage(props: IProps) { 13 | const { className } = props 14 | return ( 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | const Container = styled.div`` 23 | -------------------------------------------------------------------------------- /packages/atlassian/src/prosemirror-utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { NodeSelection, Selection, Transaction } from 'prosemirror-state' 2 | import { Node as PMNode, Fragment, NodeType, ResolvedPos } from 'prosemirror-model' 3 | 4 | export const canInsert = ($pos: ResolvedPos, content: PMNode | Fragment) => { 5 | const index = $pos.index() 6 | 7 | if (content instanceof Fragment) { 8 | return $pos.parent.canReplace(index, index, content) 9 | } else if (content instanceof PMNode) { 10 | return $pos.parent.canReplaceWith(index, index, content.type) 11 | } 12 | return false 13 | } 14 | -------------------------------------------------------------------------------- /packages/full/README.md: -------------------------------------------------------------------------------- 1 | # Full editor 2 | 3 | This is supposed to be the final result of this project with all the necessary boilerplate derived from the `atlassian` editor but re-implemented in my own way. 4 | 5 | ## How to install 6 | 7 | This project uses Yarn workspaces so you don't really have to install anything. By running `yarn` in the root folder all the dependencies should be installed automatically. 8 | 9 | ## Commands 10 | 11 | - `yarn watch` starts the Rollup compiler and watches changes to the editor 12 | - `yarn build` compiles the code as both CommonJS and ES module. 13 | -------------------------------------------------------------------------------- /packages/ssr/client/pages/MinimalPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { Editor } from '@example/minimal' 5 | 6 | import { PageHeader } from '../components/PageHeader' 7 | 8 | interface IProps { 9 | className?: string 10 | } 11 | 12 | export function MinimalPage(props: IProps) { 13 | const { className } = props 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | 23 | const Container = styled.div`` 24 | -------------------------------------------------------------------------------- /packages/atlassian/src/schema/marks/underline.ts: -------------------------------------------------------------------------------- 1 | import { MarkSpec } from 'prosemirror-model' 2 | import { FONT_STYLE } from './groups' 3 | 4 | /** 5 | * @name underline_mark 6 | */ 7 | export interface UnderlineDefinition { 8 | type: 'underline' 9 | } 10 | 11 | export const underline: MarkSpec = { 12 | inclusive: true, 13 | group: FONT_STYLE, 14 | parseDOM: [ 15 | { tag: 'u' }, 16 | { 17 | style: 'text-decoration', 18 | getAttrs: (value) => value === 'underline' && null, 19 | }, 20 | ], 21 | toDOM(): [string] { 22 | return ['u'] 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /packages/atlassian/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { CommandDispatch, Command, HigherOrderCommand } from './command' 2 | 3 | export type { EditorConfig } from './editor-config' 4 | 5 | export type { PluginsOptions, EditorPlugin } from './editor-plugin' 6 | 7 | export type { EditorAppearance, ToolbarUIComponentFactory, UIComponentFactory } from './editor-ui' 8 | 9 | export type { NodeConfig, MarkConfig, NodeViewConfig } from './pm-config' 10 | 11 | export type { 12 | PMPluginFactoryParams, 13 | PMPluginFactory, 14 | PMPluginCreateConfig, 15 | PMPlugin, 16 | } from './pm-plugin' 17 | -------------------------------------------------------------------------------- /packages/client-cra/src/pages/AtlassianPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { Editor } from '@example/atlassian' 5 | 6 | import { PageHeader } from '../components/PageHeader' 7 | 8 | interface IProps { 9 | className?: string 10 | } 11 | 12 | export function AtlassianPage(props: IProps) { 13 | const { className } = props 14 | return ( 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | const Container = styled.div`` 23 | -------------------------------------------------------------------------------- /packages/full-v2/src/context/index.ts: -------------------------------------------------------------------------------- 1 | export { useEditorContext, ReactEditorContext } from './EditorContext' 2 | export type { EditorContext } from './EditorContext' 3 | 4 | export { createDefaultProviders } from './Providers' 5 | export type { EditorContext as IProviders } from './Providers' 6 | 7 | export { EditorViewProvider } from './EditorViewProvider' 8 | export { PluginsProvider } from './PluginsProvider' 9 | export { AnalyticsProvider } from './analytics/AnalyticsProvider' 10 | export { APIProvider } from './APIProvider' 11 | export { CollabProvider } from './collab/CollabProvider' 12 | -------------------------------------------------------------------------------- /packages/full/src/core/types/editor-config.ts: -------------------------------------------------------------------------------- 1 | import { NodeView } from 'prosemirror-view' 2 | import { MarkSpec, NodeSpec } from 'prosemirror-model' 3 | 4 | import { PMPlugin } from './pm-plugin' 5 | 6 | export interface EditorConfig { 7 | nodes: NodeConfig[] 8 | marks: MarkConfig[] 9 | pmPlugins: PMPlugin[] 10 | } 11 | 12 | export interface NodeConfig { 13 | name: string 14 | node: NodeSpec 15 | } 16 | 17 | export interface MarkConfig { 18 | name: string 19 | mark: MarkSpec 20 | } 21 | 22 | export interface NodeViewConfig { 23 | name: string 24 | nodeView: NodeView 25 | } 26 | -------------------------------------------------------------------------------- /packages/full/src/editor-plugins/blockquote/pm-utils/findBlockQuote.ts: -------------------------------------------------------------------------------- 1 | import { Node, Slice, Fragment, Schema } from 'prosemirror-model' 2 | import { EditorState, Selection } from 'prosemirror-state' 3 | import { findSelectedNodeOfType, findParentNodeOfType } from '@example/prosemirror-utils' 4 | 5 | export function findBlockQuote(state: EditorState, selection?: Selection | null) { 6 | const { blockquote } = state.schema.nodes 7 | return ( 8 | findSelectedNodeOfType(blockquote)(selection || state.selection) || 9 | findParentNodeOfType(blockquote)(selection || state.selection) 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/blockquote/pm-utils/findBlockQuote.ts: -------------------------------------------------------------------------------- 1 | import { Node as PMNode, NodeType } from 'prosemirror-model' 2 | import { EditorState, NodeSelection, Selection } from 'prosemirror-state' 3 | import { findSelectedNodeOfType, findParentNodeOfType } from '@example/prosemirror-utils' 4 | 5 | export function findBlockQuote(state: EditorState, selection?: Selection | null) { 6 | const { blockquote } = state.schema.nodes 7 | return ( 8 | findSelectedNodeOfType(blockquote)(selection || state.selection) || 9 | findParentNodeOfType(blockquote)(selection || state.selection) 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/type-ahead/commands/items-list-updated.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../types' 2 | import { ACTIONS } from '../pm-plugins/actions' 3 | import { pluginKey } from '../pm-plugins/plugin-key' 4 | import { TypeAheadItem } from '../types' 5 | 6 | export const itemsListUpdated = 7 | (items: Array): Command => 8 | (state, dispatch) => { 9 | if (dispatch) { 10 | dispatch( 11 | state.tr.setMeta(pluginKey, { 12 | action: ACTIONS.ITEMS_LIST_UPDATED, 13 | items, 14 | }) 15 | ) 16 | } 17 | return true 18 | } 19 | -------------------------------------------------------------------------------- /packages/ssr/client/pages/AtlassianPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { Editor } from '@example/atlassian' 5 | 6 | import { PageHeader } from '../components/PageHeader' 7 | 8 | interface IProps { 9 | className?: string 10 | } 11 | 12 | export function AtlassianPage(props: IProps) { 13 | const { className } = props 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | 23 | const Container = styled.div`` 24 | -------------------------------------------------------------------------------- /packages/ssr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "baseUrl": ".", 9 | "typeRoots": ["./node_modules/@types"], 10 | "allowJs": true, 11 | // "skipLibCheck": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "esModuleInterop": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true 17 | }, 18 | "include": ["server/**/*"], 19 | "exclude": ["*/__tests__"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/atlassian/src/schema/nodes/blockquote.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model' 2 | import { ParagraphDefinition as Paragraph } from './paragraph' 3 | 4 | /** 5 | * @name blockquote_node 6 | */ 7 | export interface BlockQuoteDefinition { 8 | type: 'blockquote' 9 | /** 10 | * @minItems 1 11 | */ 12 | content: Array 13 | } 14 | 15 | export const blockquote: NodeSpec = { 16 | content: 'paragraph+', 17 | group: 'block', 18 | defining: true, 19 | selectable: false, 20 | parseDOM: [{ tag: 'blockquote' }], 21 | toDOM() { 22 | return ['blockquote', 0] 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /packages/client-cra/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-bg-lightest: #fff; 3 | --color-bg-base: #fffafa; 4 | --color-gray-light: #e2e2e2; 5 | --color-gray: #bbb; 6 | --color-primary-lighter: #b9ceff; 7 | --color-primary-light: #9a69c7; 8 | --color-primary: #551a8b; 9 | --color-text-dark: #222; 10 | --color-light-red: #ffbbbb; 11 | 12 | --width-tablet: 768px; 13 | } 14 | html { 15 | font-size: 16px; 16 | color: var(--color-text-dark); 17 | } 18 | body { 19 | /* background: var(--color-bg-base); */ 20 | margin: 0.5rem; 21 | padding: 0; 22 | } 23 | * { 24 | box-sizing: border-box; 25 | } 26 | -------------------------------------------------------------------------------- /packages/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/atlassian/src/types/editor-config.ts: -------------------------------------------------------------------------------- 1 | import { ToolbarUIComponentFactory } from './editor-ui' 2 | 3 | // TODO: Check if this circular dependency is still needed or is just legacy 4 | // eslint-disable-next-line import/no-cycle 5 | import { PMPlugin } from './pm-plugin' 6 | import { MarkConfig, NodeConfig } from './pm-config' 7 | import { UIComponentFactory } from './editor-ui' 8 | 9 | export interface EditorConfig { 10 | nodes: NodeConfig[] 11 | marks: MarkConfig[] 12 | pmPlugins: Array 13 | contentComponents: UIComponentFactory[] 14 | primaryToolbarComponents: ToolbarUIComponentFactory[] 15 | } 16 | -------------------------------------------------------------------------------- /packages/full-v2/src/context/ExtensionProvider.ts: -------------------------------------------------------------------------------- 1 | import { Extension, createSchema, createPlugins } from '@extensions' 2 | 3 | export class ExtensionProvider { 4 | extensions: Set> = new Set() 5 | 6 | register(extension: Extension) { 7 | this.extensions.add(extension) 8 | } 9 | 10 | unregister(extension: Extension) { 11 | this.extensions.delete(extension) 12 | } 13 | 14 | createSchema() { 15 | return createSchema(Array.from(this.extensions.values())) 16 | } 17 | 18 | createPlugins() { 19 | return createPlugins(Array.from(this.extensions.values())) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/blockquote/index.tsx: -------------------------------------------------------------------------------- 1 | import { NodeViewProps } from '@react' 2 | 3 | export interface BlockQuoteOptions {} 4 | export interface IViewProps { 5 | options?: BlockQuoteOptions 6 | } 7 | export type UIProps = NodeViewProps 8 | export interface IBlockQuoteAttrs { 9 | size: number 10 | } 11 | 12 | export { blockquotePluginKey } from './pm-plugins/state' 13 | export type { BlockQuoteState } from './pm-plugins/state' 14 | export { BlockQuoteExtension, blockQuoteSchema } from './BlockQuoteExtension' 15 | export type { BlockQuoteExtensionProps } from './BlockQuoteExtension' 16 | -------------------------------------------------------------------------------- /packages/full/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "declaration": true, 7 | "declarationDir": "./dist", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react" 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/atlassian/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "declaration": true, 7 | "declarationDir": "./dist", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react" 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/full/src/core/types/pm-plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'prosemirror-state' 2 | import { Schema } from 'prosemirror-model' 3 | 4 | import { EditorConfig } from './editor-config' 5 | import { EditorContext } from '../EditorContext' 6 | 7 | export type PMPluginFactoryParams = { 8 | schema: Schema 9 | ctx: EditorContext 10 | } 11 | 12 | export type PMPluginCreateConfig = PMPluginFactoryParams & { 13 | editorConfig: EditorConfig 14 | } 15 | 16 | export type PMPluginFactory = (params: PMPluginFactoryParams) => Plugin | undefined 17 | 18 | export type PMPlugin = { 19 | name: string 20 | plugin: PMPluginFactory 21 | } 22 | -------------------------------------------------------------------------------- /packages/full/src/schema/nodes/doc.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model' 2 | import { ParagraphDefinition } from './paragraph' 3 | 4 | /** 5 | * @name doc_node 6 | */ 7 | export interface DocNode { 8 | version: 1 9 | type: 'doc' 10 | /** 11 | * @allowUnsupportedBlock true 12 | */ 13 | content: Array 18 | } 19 | 20 | export const doc: NodeSpec = { 21 | content: 'block+', 22 | // marks: 23 | // 'alignment breakout indentation link unsupportedMark unsupportedNodeAttribute', 24 | } 25 | -------------------------------------------------------------------------------- /packages/minimal/src/plugins.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'prosemirror-state' 2 | 3 | import { history } from 'prosemirror-history' 4 | import { keymap } from 'prosemirror-keymap' 5 | import { baseKeymap } from 'prosemirror-commands' 6 | 7 | import { createNewBlockQuote, createNewPmBlockQuote } from './actions' 8 | 9 | export const plugins = () => { 10 | const plugins: Plugin[] = [] 11 | 12 | plugins.push(history()) 13 | plugins.push(keymap(baseKeymap)) 14 | plugins.push( 15 | keymap({ 16 | 'Ctrl-Alt-b': createNewBlockQuote, 17 | 'Ctrl-Alt-p': createNewPmBlockQuote, 18 | }) 19 | ) 20 | 21 | return plugins 22 | } 23 | -------------------------------------------------------------------------------- /packages/atlassian/src/schema/marks/type-ahead-query.ts: -------------------------------------------------------------------------------- 1 | import { MarkSpec } from 'prosemirror-model' 2 | import { SEARCH_QUERY } from './groups' 3 | import { B400 } from '../../theme/colors' 4 | 5 | export const typeAheadQuery: MarkSpec = { 6 | inclusive: true, 7 | group: SEARCH_QUERY, 8 | parseDOM: [{ tag: 'span[data-type-ahead-query]' }], 9 | toDOM(node) { 10 | return [ 11 | 'span', 12 | { 13 | 'data-type-ahead-query': 'true', 14 | 'data-trigger': node.attrs.trigger, 15 | style: `color: ${B400}`, 16 | }, 17 | ] 18 | }, 19 | attrs: { 20 | trigger: { default: '' }, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /packages/atlassian/src/schema/nodes/doc.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model' 2 | import { ParagraphDefinition } from './paragraph' 3 | 4 | /** 5 | * @name doc_node 6 | */ 7 | export interface DocNode { 8 | version: 1 9 | type: 'doc' 10 | /** 11 | * @allowUnsupportedBlock true 12 | */ 13 | content: Array 18 | } 19 | 20 | export const doc: NodeSpec = { 21 | content: 'block+', 22 | // marks: 23 | // 'alignment breakout indentation link unsupportedMark unsupportedNodeAttribute', 24 | } 25 | -------------------------------------------------------------------------------- /packages/ssr/server/routes/ssr/ssr.ctrl.tsx: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | 3 | import { ssrService } from './ssr.service' 4 | 5 | export const ssrReactApp = async (req: Request<{}>, res: Response, next: NextFunction) => { 6 | try { 7 | const html = ssrService.render(req.url) 8 | 9 | res.send(html) 10 | } catch (err) { 11 | next(err) 12 | } 13 | } 14 | 15 | export const ssrRawHtml = async (req: Request<{}>, res: Response, next: NextFunction) => { 16 | try { 17 | const html = ssrService.render('/', false) 18 | 19 | res.send(html) 20 | } catch (err) { 21 | next(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/client-cra/README.md: -------------------------------------------------------------------------------- 1 | # Example app 2 | 3 | This is an example app to show how to use the editor libraries in a React app. 4 | 5 | A source of inconvenience is `prosemirror-dev-tools` since it doesn't unmount properly when navigating the pages and uses some deprecated API interfaces filling the console with various errors and warnings. 6 | 7 | ## How to install 8 | 9 | This project uses Yarn workspaces so you don't really have to install anything. By running `yarn` in the root folder all the dependencies should be installed automatically. 10 | 11 | ## Commands 12 | 13 | - `yarn start` to start the example app 14 | - `yarn lint` to run ESLint 15 | -------------------------------------------------------------------------------- /packages/client-cra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "experimentalDecorators": true, 18 | "noFallthroughCasesInSwitch": true 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/client-cra/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { Provider } from 'mobx-react' 4 | 5 | import { Stores } from './stores' 6 | import { confMobx } from './stores/mobxConf' 7 | 8 | import { Routes } from './routes' 9 | import { Toaster } from './components/Toaster' 10 | 11 | import './index.css' 12 | 13 | export const stores = new Stores() 14 | 15 | confMobx() 16 | 17 | const container = document.getElementById('root') 18 | const root = createRoot(container as HTMLElement) 19 | root.render( 20 | 21 | 22 | 23 | 24 | ) 25 | -------------------------------------------------------------------------------- /packages/full-v2/src/context/PluginsProvider.ts: -------------------------------------------------------------------------------- 1 | import { PluginKey, EventDispatcher } from '@core' 2 | 3 | interface PluginState { 4 | [key: string]: any 5 | } 6 | 7 | export class PluginsProvider { 8 | dispatcher: EventDispatcher = new EventDispatcher() 9 | 10 | publish(pluginKey: PluginKey, nextPluginState: PluginState) { 11 | this.dispatcher.emit(pluginKey.name, nextPluginState) 12 | } 13 | 14 | subscribe(pluginKey: PluginKey, cb: (data: any) => void) { 15 | this.dispatcher.on(pluginKey.name, cb) 16 | } 17 | 18 | unsubscribe(pluginKey: PluginKey, cb: (data: any) => void) { 19 | this.dispatcher.off(pluginKey.name, cb) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/types/types/collab.d.ts: -------------------------------------------------------------------------------- 1 | import { Step } from 'prosemirror-transform' 2 | import { Transaction } from 'prosemirror-state' 3 | 4 | import { PatchedStep, PMDoc } from './document' 5 | 6 | // collab /join 7 | export interface IJoinResponse { 8 | doc: PMDoc 9 | steps: PatchedStep[] 10 | version: number 11 | userCount: number 12 | } 13 | export interface ISaveCollabStepsParams { 14 | version: number 15 | steps: Step[] 16 | clientID: number 17 | origins: readonly Transaction[] 18 | } 19 | export interface INewStepsResponse { 20 | version: number 21 | steps: { [key: string]: any }[] 22 | clientIDs: number[] 23 | usersCount: number 24 | } 25 | -------------------------------------------------------------------------------- /packages/atlassian/src/theme/math.ts: -------------------------------------------------------------------------------- 1 | // If a generic is used here, props can be inferred never and passed up (even with defaults) 2 | export function add(fn: (props?: any) => number, addend: number) { 3 | return (props?: any) => fn(props) + addend 4 | } 5 | 6 | export function subtract(fn: (props?: any) => number, subtrahend: number) { 7 | return (props?: any) => fn(props) - subtrahend 8 | } 9 | 10 | export function multiply(fn: (props?: any) => number, factor: number) { 11 | return (props?: any) => fn(props) * factor 12 | } 13 | 14 | export function divide(fn: (props?: any) => number, divisor: number) { 15 | return (props?: any) => fn(props) / divisor 16 | } 17 | -------------------------------------------------------------------------------- /packages/minimal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "declaration": true, 7 | "declarationDir": "./dist", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react" 19 | }, 20 | "include": ["src/**/*"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/nextjs/src/pages/atlassian.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | import dynamic from 'next/dynamic' 4 | 5 | // import { Editor } from '@example/atlassian' 6 | 7 | import { Layout } from '../components/Layout' 8 | import { PageHeader } from '../components/PageHeader' 9 | 10 | interface IProps {} 11 | 12 | const EditorWithNoSSR = dynamic(() => import('@example/atlassian'), { ssr: false }) 13 | 14 | export default function AtlassianPage(props: IProps) { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/prosemirror-utils/src/findSelectedNodeOfType.ts: -------------------------------------------------------------------------------- 1 | import { Node as PMNode, NodeType } from 'prosemirror-model' 2 | import { NodeSelection, Selection } from 'prosemirror-state' 3 | 4 | export const equalNodeType = (nodeType: NodeType, node: PMNode) => { 5 | return (Array.isArray(nodeType) && nodeType.indexOf(node.type) > -1) || node.type === nodeType 6 | } 7 | 8 | export const findSelectedNodeOfType = (nodeType: NodeType) => (selection: Selection) => { 9 | if (selection instanceof NodeSelection) { 10 | const { node, $from } = selection 11 | if (equalNodeType(nodeType, node)) { 12 | return { node, pos: $from.pos, depth: $from.depth } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "module": "esnext", 6 | "lib": ["esnext"], 7 | "target": "es2018", 8 | "declaration": true, 9 | "declarationDir": "./dist", 10 | "isolatedModules": true, 11 | "resolveJsonModule": true, 12 | "sourceMap": true, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "allowSyntheticDefaultImports": true, 17 | "allowJs": true, 18 | "strict": true 19 | }, 20 | "include": ["types"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/atlassian/src/prosemirror-utils/findSelectedNodeOfType.ts: -------------------------------------------------------------------------------- 1 | import { Node as PMNode, NodeType } from 'prosemirror-model' 2 | import { NodeSelection, Selection } from 'prosemirror-state' 3 | 4 | export const equalNodeType = (nodeType: NodeType, node: PMNode) => { 5 | return (Array.isArray(nodeType) && nodeType.indexOf(node.type) > -1) || node.type === nodeType 6 | } 7 | 8 | export const findSelectedNodeOfType = (nodeType: NodeType) => (selection: Selection) => { 9 | if (selection instanceof NodeSelection) { 10 | const { node, $from } = selection 11 | if (equalNodeType(nodeType, node)) { 12 | return { node, pos: $from.pos, depth: $from.depth } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/prosemirror-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "module": "esnext", 6 | "lib": ["esnext"], 7 | "target": "es2018", 8 | "declaration": true, 9 | "declarationDir": "./dist", 10 | "isolatedModules": true, 11 | "resolveJsonModule": true, 12 | "sourceMap": true, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "allowSyntheticDefaultImports": true, 17 | "allowJs": true, 18 | "strict": true 19 | }, 20 | "include": ["src"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /.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 | # production 12 | build 13 | dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | .env 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log 27 | 28 | tmp/ 29 | 30 | .eslintcache 31 | 32 | # example ssr app 33 | server-dist/ 34 | client-dist/ 35 | 36 | # example nextjs app 37 | .next/ 38 | out/ 39 | .vercel 40 | 41 | # collab server 42 | doc.db.json 43 | collab.db.json 44 | history.db.json -------------------------------------------------------------------------------- /packages/atlassian/src/create-editor/create-plugins-list.ts: -------------------------------------------------------------------------------- 1 | import { EditorProps } from '../Editor' 2 | import { EditorPlugin } from '../types' 3 | import { basePlugin, blockQuotePlugin, quickInsertPlugin, typeAheadPlugin } from '../plugins' 4 | 5 | import { Preset } from './preset' 6 | 7 | /** 8 | * Maps EditorProps to EditorPlugins 9 | */ 10 | export function createPluginsList(props: EditorProps, prevProps?: EditorProps): EditorPlugin[] { 11 | const preset = new Preset() 12 | 13 | preset.add(basePlugin) 14 | 15 | preset.add(blockQuotePlugin) 16 | 17 | preset.add(quickInsertPlugin) 18 | 19 | preset.add(typeAheadPlugin) 20 | 21 | return preset.getEditorPlugins() 22 | } 23 | -------------------------------------------------------------------------------- /packages/client-cra/src/document-api.ts: -------------------------------------------------------------------------------- 1 | import { IDBDocument, IGetDocumentsResponse, ICreateDocumentParams } from '@example/types' 2 | 3 | import { get, post, put, del } from './api' 4 | 5 | export const getDocuments = () => get('docs', 'Fetching documents failed') 6 | 7 | export const createDocument = (payload: ICreateDocumentParams) => 8 | post('doc', payload, 'Document create failed') 9 | 10 | export const updateDocument = (docId: string, payload: Partial) => 11 | put(`doc/${docId}`, payload, 'Document update failed') 12 | 13 | export const deleteDocument = (docId: string) => 14 | del(`doc/${docId}`, 'Document delete failed') 15 | -------------------------------------------------------------------------------- /packages/full/src/core/PluginsProvider.ts: -------------------------------------------------------------------------------- 1 | import { PluginKey } from './pm' 2 | 3 | import { EventDispatcher } from './utils/EventDispatcher' 4 | 5 | interface PluginState { 6 | [key: string]: any 7 | } 8 | 9 | export class PluginsProvider { 10 | dispatcher: EventDispatcher = new EventDispatcher() 11 | 12 | publish(pluginKey: PluginKey, nextPluginState: PluginState) { 13 | this.dispatcher.emit(pluginKey.name, nextPluginState) 14 | } 15 | 16 | subscribe(pluginKey: PluginKey, cb: (data: any) => void) { 17 | this.dispatcher.on(pluginKey.name, cb) 18 | } 19 | 20 | unsubscribe(pluginKey: PluginKey, cb: (data: any) => void) { 21 | this.dispatcher.off(pluginKey.name, cb) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/panel-note.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconPanelNote({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/panel-success.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconPanelSuccess({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/full-v2/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | useEditorContext, 3 | createDefaultProviders, 4 | ReactEditorContext, 5 | EditorViewProvider, 6 | PluginsProvider, 7 | AnalyticsProvider, 8 | APIProvider, 9 | CollabProvider, 10 | } from '@context' 11 | export type { EditorContext, IProviders } from '@context' 12 | 13 | export { Editor } from '@core' 14 | 15 | export { 16 | Base, 17 | BaseExtension, 18 | BlockQuote, 19 | BlockQuoteExtension, 20 | Collab, 21 | CollabExtension, 22 | Extension, 23 | createSchema, 24 | createDefaultSchema, 25 | createPlugins, 26 | } from '@extensions' 27 | export type { BaseState, BlockQuoteState } from '@extensions' 28 | 29 | export { PortalRenderer } from '@react' 30 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/types.ts: -------------------------------------------------------------------------------- 1 | import { QuickInsertItem, QuickInsertProvider } from '../../provider-factory' 2 | 3 | export type QuickInsertOptions = 4 | | boolean 5 | | { 6 | provider: Promise 7 | } 8 | 9 | export type QuickInsertHandler = Array 10 | 11 | export type IconProps = { 12 | label?: string 13 | } 14 | 15 | export type QuickInsertPluginState = { 16 | isElementBrowserModalOpen: boolean 17 | lazyDefaultItems: () => QuickInsertItem[] 18 | providedItems?: QuickInsertItem[] 19 | provider?: QuickInsertProvider 20 | } 21 | 22 | export interface QuickInsertPluginOptions { 23 | headless?: boolean 24 | disableDefaultItems?: boolean 25 | enableElementBrowser?: boolean 26 | } 27 | -------------------------------------------------------------------------------- /packages/api-collab/src/common/logger.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston' 2 | import { config } from './config' 3 | 4 | let logFormat 5 | // Add colors in local environment 6 | if (config.ENV === 'production') { 7 | logFormat = winston.format.combine(winston.format.json()) 8 | } else { 9 | logFormat = winston.format.combine(winston.format.colorize(), winston.format.simple()) 10 | } 11 | 12 | export const log: winston.Logger = winston.createLogger({ 13 | level: config.LOG.LEVEL, 14 | format: logFormat, 15 | transports: [ 16 | new winston.transports.Console({ 17 | level: config.LOG.LEVEL, 18 | }), 19 | ], 20 | exitOnError: false, 21 | }) 22 | 23 | export const logStream = { 24 | write: (message: string) => { 25 | log.info(message) 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /packages/nextjs/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import styled from 'styled-components' 4 | 5 | import { NavBar } from './NavBar' 6 | 7 | interface IProps { 8 | children: React.ReactNode 9 | title?: string 10 | } 11 | 12 | export const Layout = ({ children }: IProps) => ( 13 | 14 | 15 | {children} 16 | 17 | ) 18 | 19 | const MainWrapper = styled.div` 20 | background: snow; 21 | min-height: 100vh; 22 | ` 23 | const MainContainer = styled.main` 24 | margin: 40px auto 0 auto; 25 | max-width: 680px; 26 | padding-bottom: 20px; 27 | @media only screen and (max-width: 720px) { 28 | margin: 40px 20px 0 20px; 29 | padding-bottom: 20px; 30 | } 31 | ` 32 | -------------------------------------------------------------------------------- /packages/ssr/server/common/logger.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston' 2 | import { config } from './config' 3 | 4 | let logFormat 5 | // Add colors in local environment 6 | if (config.ENV === 'production') { 7 | logFormat = winston.format.combine(winston.format.json()) 8 | } else { 9 | logFormat = winston.format.combine(winston.format.colorize(), winston.format.simple()) 10 | } 11 | 12 | export const log: winston.Logger = winston.createLogger({ 13 | level: config.LOG.LEVEL, 14 | format: logFormat, 15 | transports: [ 16 | new winston.transports.Console({ 17 | level: config.LOG.LEVEL, 18 | }), 19 | ], 20 | exitOnError: false, 21 | }) 22 | 23 | export const logStream = { 24 | write: (message: string) => { 25 | log.info(message) 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/panel-error.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconPanelError({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/api-collab/README.md: -------------------------------------------------------------------------------- 1 | # Collaboration server 2 | 3 | This is an example collaboration server based on this example https://github.com/ProseMirror/website/tree/master/src/collab to show how one can integrate it with ProseMirror editor. 4 | 5 | ## How to install 6 | 7 | This project uses Yarn workspaces so you don't really have to install anything. By running `yarn` in the root folder all the dependencies should be installed automatically. Then, you should run in two terminals: 8 | 9 | 1. `yarn dev` 10 | 2. `yarn watch` 11 | 12 | ## Commands 13 | 14 | - `yarn dev` starts a Nodemon process to restart the Node.js server on source file changes 15 | - `yarn watch` starts the Rollup compiler and watches changes to the source files 16 | - `yarn build` compiles the code as both CommonJS and ES module. 17 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/table.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconTable({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/client-cra/src/pages/FrontPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { PageHeader } from '../components/PageHeader' 5 | import { CollabInfo } from '../components/CollabInfo' 6 | import { DocumentBrowser } from '../components/DocumentBrowser' 7 | import { Editor } from '../components/editor/Editor' 8 | 9 | interface IProps { 10 | className?: string 11 | } 12 | 13 | export function FrontPage(props: IProps) { 14 | const { className } = props 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | const Container = styled.div` 26 | & > ${DocumentBrowser} { 27 | margin: 1rem 0; 28 | } 29 | ` 30 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/createReactExtension.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect, useMemo } from 'react' 2 | import { Extension } from './Extension' 3 | 4 | import { useEditorContext, EditorContext } from '@context' 5 | 6 | export const createReactExtension = 7 | (Ext: new (ctx: EditorContext, props: T) => Extension) => 8 | (props: T) => { 9 | const ctx = useEditorContext() 10 | const { extensionProvider } = ctx 11 | const extension = useMemo(() => new Ext(ctx, props), []) 12 | useLayoutEffect(() => { 13 | extensionProvider.register(extension) 14 | return () => { 15 | extensionProvider.unregister(extension) 16 | } 17 | }, []) 18 | useEffect(() => { 19 | extension.onPropsChanged(props) 20 | }, [props]) 21 | return null 22 | } 23 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/pm-plugins/state.ts: -------------------------------------------------------------------------------- 1 | import { EditorState } from 'prosemirror-state' 2 | import { PluginKey } from '@core' 3 | import { CommandDispatch } from '@core' 4 | 5 | export interface BaseState { 6 | activeNodes: string[] 7 | activeMarks: string[] 8 | } 9 | 10 | export const basePluginKey = new PluginKey('basePlugin') 11 | 12 | export const getPluginState = (state: EditorState): BaseState => basePluginKey.getState(state) 13 | 14 | export const setPluginState = 15 | (stateProps: Object) => 16 | (state: EditorState, dispatch: CommandDispatch): boolean => { 17 | const pluginState = getPluginState(state) 18 | dispatch( 19 | state.tr.setMeta(basePluginKey, { 20 | ...pluginState, 21 | ...stateProps, 22 | }) 23 | ) 24 | return true 25 | } 26 | -------------------------------------------------------------------------------- /packages/atlassian/src/react-portals/PortalProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PortalProviderAPI } from './PortalProviderAPI' 3 | 4 | export type PortalProviderProps = { 5 | render: (portalProviderAPI: PortalProviderAPI) => React.ReactNode | JSX.Element | null 6 | children?: React.ReactNode 7 | } 8 | 9 | export class PortalProvider extends React.Component { 10 | static displayName = 'PortalProvider' 11 | 12 | portalProviderAPI: PortalProviderAPI 13 | 14 | constructor(props: PortalProviderProps) { 15 | super(props) 16 | this.portalProviderAPI = new PortalProviderAPI() 17 | } 18 | 19 | render() { 20 | return this.props.render(this.portalProviderAPI) 21 | } 22 | 23 | componentDidUpdate() { 24 | this.portalProviderAPI.forceUpdate() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/atlassian/src/prosemirror-utils/findParentOfNodeType.ts: -------------------------------------------------------------------------------- 1 | // import { Node as PMNode, NodeType } from 'prosemirror-model'; 2 | // import { NodeSelection, Selection } from 'prosemirror-state'; 3 | 4 | // // :: (nodeType: union) → (selection: Selection) → ?{pos: number, start: number, depth: number, node: ProseMirrorNode} 5 | // // Iterates over parent nodes, returning closest node of a given `nodeType`. `start` points to the start position of the node, `pos` points directly before the node. 6 | // // 7 | // // ```javascript 8 | // // const parent = findParentNodeOfType(schema.nodes.paragraph)(selection); 9 | // // ``` 10 | // export const findParentNodeOfType = (nodeType: NodeType) => (selection: Selection) => { 11 | // return findParentNode(node => equalNodeType(nodeType, node))(selection); 12 | // }; 13 | export {} 14 | -------------------------------------------------------------------------------- /packages/atlassian/src/EditorContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { EditorActions } from './EditorActions' 4 | 5 | export type EditorContextProps = { 6 | editorActions?: EditorActions 7 | children: React.ReactNode 8 | } 9 | 10 | export class EditorContext extends React.Component { 11 | static childContextTypes = { 12 | editorActions: PropTypes.object, 13 | } 14 | 15 | private editorActions: EditorActions 16 | 17 | constructor(props: EditorContextProps) { 18 | super(props) 19 | this.editorActions = props.editorActions || new EditorActions() 20 | } 21 | 22 | getChildContext() { 23 | return { 24 | editorActions: this.editorActions, 25 | } 26 | } 27 | 28 | render() { 29 | return React.Children.only(this.props.children) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/type-ahead/utils/find-query-mark.ts: -------------------------------------------------------------------------------- 1 | import { MarkType, Node } from 'prosemirror-model' 2 | import { EditorState } from 'prosemirror-state' 3 | 4 | export function findQueryMark(mark: MarkType, doc: Node, from: number, to: number) { 5 | let queryMark = { start: -1, end: -1 } 6 | doc.nodesBetween(from, to, (node, pos) => { 7 | if (queryMark.start === -1 && mark.isInSet(node.marks)) { 8 | queryMark = { 9 | start: pos, 10 | end: pos + Math.max(node.textContent.length, 1), 11 | } 12 | } 13 | }) 14 | 15 | return queryMark 16 | } 17 | 18 | export function findTypeAheadQuery(state: EditorState) { 19 | const { doc, schema } = state 20 | const { typeAheadQuery } = schema.marks 21 | const { from, to } = state.selection 22 | return findQueryMark(typeAheadQuery, doc, from - 1, to) 23 | } 24 | -------------------------------------------------------------------------------- /packages/api-collab/src/routes.ts: -------------------------------------------------------------------------------- 1 | import express, { Router } from 'express' 2 | 3 | import * as documentCtrl from './routes/doc/document.ctrl' 4 | import * as docCollabCtrl from './routes/doc_collab/doc-collab.ctrl' 5 | 6 | const router: Router = Router() 7 | 8 | router.get('/docs', documentCtrl.getDocuments) 9 | router.post('/doc', documentCtrl.createDocument) 10 | router.get('/doc/:documentId', documentCtrl.getDocument) 11 | router.put('/doc/:documentId', documentCtrl.updateDocument) 12 | router.delete('/doc/:documentId', documentCtrl.deleteDocument) 13 | 14 | router.post('/doc/:documentId/join', docCollabCtrl.clientJoin) 15 | router.post('/doc/:documentId/leave', docCollabCtrl.clientLeave) 16 | // router.get('/doc/:documentId/events', docEventCtrl.getDocumentEvents) 17 | router.post('/doc/:documentId/steps', docCollabCtrl.saveSteps) 18 | 19 | export default router 20 | -------------------------------------------------------------------------------- /packages/full/src/core/create/create-schema.ts: -------------------------------------------------------------------------------- 1 | import { MarkSpec, NodeSpec, Schema } from 'prosemirror-model' 2 | 3 | import { sortByOrder } from './ranks' 4 | import { fixExcludes } from './create-plugins' 5 | import { MarkConfig, NodeConfig } from '../types' 6 | 7 | export function createSchema(editorConfig: { marks: MarkConfig[]; nodes: NodeConfig[] }) { 8 | const createdMarks = fixExcludes( 9 | editorConfig.marks.sort(sortByOrder('marks')).reduce((acc, mark) => { 10 | acc[mark.name] = mark.mark 11 | return acc 12 | }, {} as { [nodeName: string]: MarkSpec }) 13 | ) 14 | const createdNodes = editorConfig.nodes.sort(sortByOrder('nodes')).reduce((acc, node) => { 15 | acc[node.name] = node.node 16 | return acc 17 | }, {} as { [nodeName: string]: NodeSpec }) 18 | 19 | return new Schema({ nodes: createdNodes, marks: createdMarks }) 20 | } 21 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/panel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconPanel({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/full/src/core/EditorContext.ts: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react' 2 | 3 | import { EditorViewProvider } from './EditorViewProvider' 4 | import { PluginsProvider } from './PluginsProvider' 5 | import { PortalProvider } from '../react/portals/PortalProvider' 6 | import { AnalyticsProvider } from './AnalyticsProvider' 7 | 8 | export interface EditorContext { 9 | portalProvider: PortalProvider 10 | viewProvider: EditorViewProvider 11 | pluginsProvider: PluginsProvider 12 | analyticsProvider: AnalyticsProvider 13 | } 14 | 15 | export const Context = createContext({ 16 | portalProvider: new PortalProvider(), 17 | viewProvider: new EditorViewProvider(), 18 | pluginsProvider: new PluginsProvider(), 19 | analyticsProvider: new AnalyticsProvider(), 20 | }) 21 | 22 | export const useEditorContext = () => useContext(Context) 23 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/blockquote/pm-plugins/state.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, Plugin } from 'prosemirror-state' 2 | import { PluginKey } from '@core' 3 | import { CommandDispatch } from '@core' 4 | 5 | export interface BlockQuoteState { 6 | blockQuoteActive: boolean 7 | // blockQuoteDisabled: boolean 8 | } 9 | 10 | export const blockquotePluginKey = new PluginKey('blockQuotePlugin') 11 | 12 | export const getPluginState = (state: EditorState): BlockQuoteState => 13 | blockquotePluginKey.getState(state) 14 | 15 | export const setPluginState = 16 | (stateProps: Object) => 17 | (state: EditorState, dispatch: CommandDispatch): boolean => { 18 | const pluginState = getPluginState(state) 19 | dispatch( 20 | state.tr.setMeta(blockquotePluginKey, { 21 | ...pluginState, 22 | ...stateProps, 23 | }) 24 | ) 25 | return true 26 | } 27 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/panel-warning.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconPanelWarning({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/atlassian/src/create-editor/create-schema.ts: -------------------------------------------------------------------------------- 1 | import { sortByOrder } from './sort-by-order' 2 | import { MarkSpec, NodeSpec, Schema } from 'prosemirror-model' 3 | import { fixExcludes } from './create-plugins' 4 | import { MarkConfig, NodeConfig } from '../types/pm-config' 5 | 6 | export function createSchema(editorConfig: { marks: MarkConfig[]; nodes: NodeConfig[] }) { 7 | const createdMarks = fixExcludes( 8 | editorConfig.marks.sort(sortByOrder('marks')).reduce((acc, mark) => { 9 | acc[mark.name] = mark.mark 10 | return acc 11 | }, {} as { [nodeName: string]: MarkSpec }) 12 | ) 13 | const createdNodes = editorConfig.nodes.sort(sortByOrder('nodes')).reduce((acc, node) => { 14 | acc[node.name] = node.node 15 | return acc 16 | }, {} as { [nodeName: string]: NodeSpec }) 17 | 18 | return new Schema({ nodes: createdNodes, marks: createdMarks }) 19 | } 20 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/decision.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconDecision({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/heading1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconHeading1({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/full-v2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "declaration": true, 7 | "declarationDir": "./dist", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react", 19 | "baseUrl": ".", 20 | "paths": { 21 | "@context": ["src/context"], 22 | "@core": ["src/core"], 23 | "@extensions": ["src/extensions"], 24 | "@react": ["src/react"] 25 | }, 26 | "plugins": [{ "transform": "@zerollup/ts-transform-paths" }] 27 | }, 28 | "include": ["src"] 29 | } 30 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconList({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/ssr/client/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { NavBar } from './NavBar' 5 | 6 | interface Props { 7 | children: React.ReactNode 8 | } 9 | 10 | export const NoContainerLayout: React.FC = ({ children }) => ( 11 | 12 | 13 | {children} 14 | 15 | ) 16 | 17 | export const DefaultLayout: React.FC = ({ children }) => ( 18 | 19 | 20 | {children} 21 | 22 | ) 23 | 24 | const MainWrapper = styled.div` 25 | min-height: 100vh; 26 | ` 27 | const MainContainer = styled.main` 28 | margin: 40px auto 0 auto; 29 | max-width: 680px; 30 | padding-bottom: 20px; 31 | @media only screen and (max-width: 720px) { 32 | margin: 40px 20px 0 20px; 33 | padding-bottom: 20px; 34 | } 35 | ` 36 | -------------------------------------------------------------------------------- /packages/api-collab/src/routes/doc_collab/doc-collab.io.ts: -------------------------------------------------------------------------------- 1 | import { socketIO } from 'socket-io/socketIO' 2 | 3 | import { 4 | ECollabAction, 5 | ICollabUsersChangedAction, 6 | ICollabEditAction, 7 | ICollabEditPayload, 8 | } from '@example/types' 9 | 10 | export const docCollabIO = { 11 | emitCollabUsersChanged(documentId: string, userId: string, userCount: number) { 12 | const action: ICollabUsersChangedAction = { 13 | type: ECollabAction.COLLAB_USERS_CHANGED, 14 | payload: { 15 | documentId, 16 | userId, 17 | userCount, 18 | }, 19 | } 20 | socketIO.emitToRoom(action, documentId) 21 | }, 22 | emitEditDocument(documentId: string, payload: ICollabEditPayload) { 23 | const action: ICollabEditAction = { 24 | type: ECollabAction.COLLAB_CLIENT_EDIT, 25 | payload, 26 | } 27 | socketIO.emitToRoom(action, documentId) 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /packages/client-cra/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { NavBar } from './NavBar' 5 | 6 | interface Props { 7 | children: React.ReactNode 8 | } 9 | 10 | export const NoContainerLayout: React.FC = ({ children }) => ( 11 | 12 | 13 | {children} 14 | 15 | ) 16 | 17 | export const DefaultLayout: React.FC = ({ children }) => ( 18 | 19 | 20 | {children} 21 | 22 | ) 23 | 24 | const MainWrapper = styled.div` 25 | min-height: 100vh; 26 | ` 27 | const MainContainer = styled.main` 28 | margin: 40px auto 0 auto; 29 | max-width: 680px; 30 | padding-bottom: 20px; 31 | @media only screen and (max-width: 720px) { 32 | margin: 40px 20px 0 20px; 33 | padding-bottom: 20px; 34 | } 35 | ` 36 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/quote.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconQuote({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/type-ahead/commands/dismiss.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../types' 2 | import { pluginKey } from '../pm-plugins/plugin-key' 3 | import { findTypeAheadQuery } from '../utils/find-query-mark' 4 | 5 | export const dismissCommand = (): Command => (state, dispatch) => { 6 | const queryMark = findTypeAheadQuery(state) 7 | 8 | if (queryMark === null) { 9 | return false 10 | } 11 | 12 | const { start, end } = queryMark 13 | const { schema } = state 14 | const markType = schema.marks.typeAheadQuery 15 | if (start === -1) { 16 | return false 17 | } 18 | 19 | const { typeAheadHandler } = pluginKey.getState(state) 20 | if (typeAheadHandler && typeAheadHandler.dismiss) { 21 | typeAheadHandler.dismiss(state) 22 | } 23 | 24 | if (dispatch) { 25 | dispatch(state.tr.removeMark(start, end, markType).removeStoredMark(markType)) 26 | } 27 | return true 28 | } 29 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/blockquote/ui/BlockQuote.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import styled from 'styled-components' 3 | 4 | interface IProps {} 5 | 6 | export const BlockQuote = forwardRef((props: IProps, ref: any) => { 7 | return 8 | }) 9 | 10 | export const StyledBlockQuote = styled.blockquote` 11 | box-sizing: border-box; 12 | color: #6a737d; 13 | padding: 0 1em; 14 | border-left: 4px solid #dfe2e5; 15 | margin: 0.2rem 0 0 0; 16 | margin-right: 0; 17 | 18 | [dir='rtl'] & { 19 | padding-left: 0; 20 | padding-right: 4px; 21 | } 22 | 23 | &:first-child { 24 | margin-top: 0; 25 | } 26 | 27 | &::before { 28 | content: ''; 29 | } 30 | 31 | &::after { 32 | content: none; 33 | } 34 | 35 | & p { 36 | display: block; 37 | } 38 | 39 | & table, 40 | & table:last-child { 41 | display: inline-table; 42 | } 43 | ` 44 | -------------------------------------------------------------------------------- /packages/minimal/src/actions.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, Transaction } from 'prosemirror-state' 2 | 3 | export function createNewBlockQuote(state: EditorState, dispatch?: (tr: Transaction) => void) { 4 | const { $from, $to } = state.selection 5 | const blockquote = state.schema.nodes.blockquote 6 | const empty = blockquote.createAndFill() 7 | const endOfBlock = $from.end() 8 | if (empty) { 9 | dispatch && dispatch(state.tr.insert(endOfBlock + 1, empty)) 10 | return true 11 | } 12 | return false 13 | } 14 | 15 | export function createNewPmBlockQuote(state: EditorState, dispatch?: (tr: Transaction) => void) { 16 | const { $from, $to } = state.selection 17 | const blockquote = state.schema.nodes.pmBlockquote 18 | const empty = blockquote.createAndFill() 19 | const endOfBlock = $from.end() 20 | if (empty && dispatch) { 21 | const tr = state.tr.insert(endOfBlock + 1, empty) 22 | dispatch(tr) 23 | } 24 | return false 25 | } 26 | -------------------------------------------------------------------------------- /packages/api-collab/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import morgan from 'morgan' 3 | import cors from 'cors' 4 | 5 | import routes from './routes' 6 | 7 | import { errorHandler, logStream, config } from './common' 8 | 9 | const app = express() 10 | 11 | const corsOptions: cors.CorsOptions = { 12 | origin(origin, callback) { 13 | callback(null, true) 14 | }, 15 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 16 | } 17 | 18 | app.use(cors(corsOptions)) 19 | app.use(express.urlencoded({ extended: true })) 20 | app.use(express.json()) 21 | 22 | // By adding this route before morgan prevents it being logged which in production setting 23 | // is annoying and pollutes the logs with gazillion "GET /health" lines 24 | app.get('/health', (req: any, res: any) => { 25 | res.sendStatus(200) 26 | }) 27 | 28 | app.use(morgan('short', { stream: logStream })) 29 | 30 | app.use('', routes) 31 | app.use(errorHandler) 32 | 33 | export { app } 34 | -------------------------------------------------------------------------------- /packages/types/types/socket-doc.d.ts: -------------------------------------------------------------------------------- 1 | import { IDBDocument, DocVisibility } from './document' 2 | 3 | import { EDocAction } from '../src' 4 | 5 | export { EDocAction } from '../src' 6 | 7 | // Document sync actions 8 | export enum EDocAction { 9 | DOC_CREATE = 'doc:create', 10 | DOC_DELETE = 'doc:delete', 11 | DOC_VISIBILITY = 'doc:visibility', 12 | } 13 | export type DocAction = IDocCreateAction | IDocDeleteAction | IDocVisibilityAction 14 | export interface IDocCreateAction { 15 | type: EDocAction.DOC_CREATE 16 | payload: { 17 | doc: IDBDocument 18 | userId: string 19 | } 20 | } 21 | export interface IDocDeleteAction { 22 | type: EDocAction.DOC_DELETE 23 | payload: { 24 | documentId: string 25 | userId: string 26 | } 27 | } 28 | export interface IDocVisibilityAction { 29 | type: EDocAction.DOC_VISIBILITY 30 | payload: { 31 | documentId: string 32 | visibility: DocVisibility 33 | userId: string 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/api-collab/src/socket-io/types.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io' 2 | import { 3 | ECollabAction, 4 | ICollabUsersChangedAction, 5 | ICollabEditAction, 6 | EDocAction, 7 | IDocCreateAction, 8 | IDocDeleteAction, 9 | IDocVisibilityAction, 10 | ICollabServerUpdateAction, 11 | } from '@example/types' 12 | 13 | export type ExampleAppSocket = Socket 14 | 15 | export interface ISocketListenEvents {} 16 | 17 | export interface ISocketEmitEvents { 18 | [EDocAction.DOC_CREATE]: (action: IDocCreateAction) => void 19 | [EDocAction.DOC_DELETE]: (action: IDocDeleteAction) => void 20 | [EDocAction.DOC_VISIBILITY]: (action: IDocVisibilityAction) => void 21 | [ECollabAction.COLLAB_USERS_CHANGED]: (action: ICollabUsersChangedAction) => void 22 | [ECollabAction.COLLAB_CLIENT_EDIT]: (action: ICollabEditAction) => void 23 | [ECollabAction.COLLAB_SERVER_UPDATE]: (action: ICollabServerUpdateAction) => void 24 | } 25 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/action.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconAction({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/divider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconDivider({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/full/src/create-defaults.ts: -------------------------------------------------------------------------------- 1 | import { EditorProps } from './Editor' 2 | import { EditorPlugin } from './core/types' 3 | import { basePlugin, blockQuotePlugin } from './editor-plugins' 4 | 5 | import { Preset } from './core/create/preset' 6 | import { createSchema } from './core/create/create-schema' 7 | import { processPluginsList } from './core/create/create-plugins' 8 | 9 | export function createDefaultEditorPlugins( 10 | props: EditorProps, 11 | prevProps?: EditorProps 12 | ): EditorPlugin[] { 13 | const preset = new Preset() 14 | 15 | preset.add(basePlugin) 16 | 17 | preset.add(blockQuotePlugin) 18 | 19 | return preset.getEditorPlugins() 20 | } 21 | 22 | export function createDefaultSchema() { 23 | const editorProps: EditorProps = {} 24 | const editorPlugins = createDefaultEditorPlugins(editorProps) 25 | const config = processPluginsList(editorPlugins) 26 | const schema = createSchema(config) 27 | return schema 28 | } 29 | -------------------------------------------------------------------------------- /packages/ssr/rollup.config.client.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import postcss from 'rollup-plugin-postcss' 3 | import { nodeResolve } from '@rollup/plugin-node-resolve' 4 | import commonjs from '@rollup/plugin-commonjs' 5 | import json from '@rollup/plugin-json' 6 | import injectProcessEnv from 'rollup-plugin-inject-process-env' 7 | import nodePolyfills from 'rollup-plugin-node-polyfills' 8 | 9 | export default { 10 | input: ['client/index.tsx'], 11 | preserveEntrySignatures: false, 12 | output: [ 13 | { 14 | file: './client-dist/bundle.js', 15 | format: 'iife', 16 | inlineDynamicImports: true, 17 | sourcemap: true, 18 | }, 19 | ], 20 | plugins: [ 21 | json(), 22 | typescript(), 23 | postcss(), 24 | commonjs(), 25 | injectProcessEnv({ 26 | NODE_ENV: 'production', 27 | }), 28 | nodePolyfills(), 29 | nodeResolve({ 30 | browser: true, 31 | }), 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /packages/client-cra/src/stores/AuthStore.ts: -------------------------------------------------------------------------------- 1 | import { action, observable } from 'mobx' 2 | 3 | import { IUser, uuidv4 } from '@example/types' 4 | 5 | export class AuthStore { 6 | @observable user?: IUser = undefined 7 | resetAllStores: () => void 8 | STORAGE_KEY = 'full-user' 9 | 10 | constructor(resetFn: () => void) { 11 | this.resetAllStores = resetFn 12 | if (typeof window === 'undefined') return 13 | const existing = sessionStorage.getItem(this.STORAGE_KEY) 14 | if (existing && existing !== null && existing.length > 0) { 15 | this.user = JSON.parse(existing) 16 | } else { 17 | const id = uuidv4() 18 | this.user = { 19 | id, 20 | name: `User ${id.substring(0, 5)}`, 21 | } 22 | sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.user)) 23 | } 24 | } 25 | 26 | @action reset() { 27 | this.user = undefined 28 | } 29 | 30 | @action logout = () => { 31 | this.resetAllStores() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/client-nextjs", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "dev": "next dev -p 3450", 6 | "build": "next build", 7 | "serve": "serve out", 8 | "static": "next build && next export", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@example/atlassian": "workspace:*", 13 | "@example/full": "workspace:*", 14 | "@example/minimal": "workspace:*", 15 | "lodash.debounce": "^4.0.8", 16 | "next": "12.2.3", 17 | "react": "18.2.0", 18 | "react-dom": "18.2.0", 19 | "react-icons": "^4.4.0", 20 | "styled-components": "^5.3.5" 21 | }, 22 | "devDependencies": { 23 | "@types/lodash.debounce": "^4.0.7", 24 | "@types/node": "18.0.6", 25 | "@types/react": "18.0.15", 26 | "@types/react-dom": "18.0.6", 27 | "@types/styled-components": "5.1.25", 28 | "babel-plugin-styled-components": "^2.0.7", 29 | "serve": "^14.0.1", 30 | "typescript": "4.7" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/collab/pm-plugins/state.ts: -------------------------------------------------------------------------------- 1 | import { EditorState } from 'prosemirror-state' 2 | import { Decoration, DecorationSet } from 'prosemirror-view' 3 | import { PluginKey, CommandDispatch } from '@core' 4 | 5 | import { CollabParticipant } from '../types' 6 | 7 | export interface CollabState { 8 | decorations: DecorationSet 9 | participants: Map 10 | isCollabInitialized: boolean 11 | } 12 | 13 | export const collabEditPluginKey = new PluginKey('collabEditPlugin') 14 | 15 | export const getPluginState = (state: EditorState): CollabState => 16 | collabEditPluginKey.getState(state) 17 | 18 | export const setPluginState = 19 | (stateProps: Object) => 20 | (state: EditorState, dispatch: CommandDispatch): boolean => { 21 | const pluginState = getPluginState(state) 22 | dispatch( 23 | state.tr.setMeta(collabEditPluginKey, { 24 | ...pluginState, 25 | ...stateProps, 26 | }) 27 | ) 28 | return true 29 | } 30 | -------------------------------------------------------------------------------- /packages/full/src/schema/inline-content.ts: -------------------------------------------------------------------------------- 1 | import { TextDefinition as Text } from './nodes/text' 2 | import { MarksObject } from './marks-obj' 3 | import { UnderlineDefinition as Underline } from './marks' 4 | 5 | /** 6 | * @name formatted_text_inline_node 7 | */ 8 | export type InlineFormattedText = Text & MarksObject // Link | Em | Strong | Strike | SubSup | TextColor | Annotation 9 | /** 10 | * @name link_text_inline_node 11 | */ 12 | // export type InlineLinkText = Text & MarksObject; 13 | /** 14 | * @name code_inline_node 15 | */ 16 | // export type InlineCode = Text & MarksObject; 17 | /** 18 | * @name atomic_inline_node 19 | */ 20 | // export type InlineAtomic = 21 | // | HardBreak 22 | // | Mention 23 | // | Emoji 24 | // | InlineExtension 25 | // | Date 26 | // | Placeholder 27 | // | InlineCard 28 | // | Status; 29 | /** 30 | * @name inline_node 31 | */ 32 | export type Inline = InlineFormattedText // | InlineCode | InlineAtomic; 33 | -------------------------------------------------------------------------------- /packages/full/src/ui/FullPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { Toolbar } from './Toolbar' 5 | 6 | interface IProps { 7 | className?: string 8 | children: React.ReactNode 9 | } 10 | 11 | export function FullPage(props: IProps) { 12 | const { className, children } = props 13 | return ( 14 | 15 | 16 | {children} 17 | 18 | ) 19 | } 20 | 21 | const Container = styled.div`` 22 | const ContentArea = styled.div` 23 | border: 1px solid black; 24 | & > .ProseMirror { 25 | min-height: 140px; 26 | overflow-wrap: break-word; 27 | outline: none; 28 | padding: 10px; 29 | white-space: pre-wrap; 30 | } 31 | 32 | .pm-blockquote { 33 | box-sizing: border-box; 34 | color: #2d82e1; 35 | padding: 0 1em; 36 | border-left: 4px solid #48a1fa; 37 | margin: 0.2rem 0 0 0; 38 | margin-right: 0; 39 | } 40 | ` 41 | -------------------------------------------------------------------------------- /packages/api-collab/src/common/error.ts: -------------------------------------------------------------------------------- 1 | export interface IError extends Error { 2 | statusCode?: number 3 | } 4 | 5 | export class CustomError extends Error implements IError { 6 | statusCode: number 7 | 8 | constructor(message: string, errorCode = 500) { 9 | super(message) 10 | this.name = this.constructor.name 11 | this.statusCode = errorCode 12 | Error.captureStackTrace(this, this.constructor) 13 | } 14 | } 15 | 16 | export class ValidationError extends Error implements IError { 17 | readonly statusCode: number = 400 18 | 19 | constructor(message: string) { 20 | super(message) 21 | this.name = this.constructor.name 22 | Error.captureStackTrace(this, this.constructor) 23 | } 24 | } 25 | 26 | export class DBError extends Error implements IError { 27 | readonly statusCode: number = 500 28 | 29 | constructor(message: string) { 30 | super(message) 31 | this.name = this.constructor.name 32 | Error.captureStackTrace(this, this.constructor) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/atlassian/src/schema/inline-content.ts: -------------------------------------------------------------------------------- 1 | import { TextDefinition as Text } from './nodes/text' 2 | import { MarksObject } from './marks-obj' 3 | import { UnderlineDefinition as Underline } from './marks' 4 | 5 | /** 6 | * @name formatted_text_inline_node 7 | */ 8 | export type InlineFormattedText = Text & MarksObject // Link | Em | Strong | Strike | SubSup | TextColor | Annotation 9 | /** 10 | * @name link_text_inline_node 11 | */ 12 | // export type InlineLinkText = Text & MarksObject; 13 | /** 14 | * @name code_inline_node 15 | */ 16 | // export type InlineCode = Text & MarksObject; 17 | /** 18 | * @name atomic_inline_node 19 | */ 20 | // export type InlineAtomic = 21 | // | HardBreak 22 | // | Mention 23 | // | Emoji 24 | // | InlineExtension 25 | // | Date 26 | // | Placeholder 27 | // | InlineCard 28 | // | Status; 29 | /** 30 | * @name inline_node 31 | */ 32 | export type Inline = InlineFormattedText // | InlineCode | InlineAtomic; 33 | -------------------------------------------------------------------------------- /packages/atlassian/src/utils/magic-box.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * From Modernizr 3 | * Returns the kind of transitionevent available for the element 4 | */ 5 | export function whichTransitionEvent() { 6 | const el = document.createElement('fakeelement') 7 | const transitions: Record = { 8 | transition: 'transitionend', 9 | MozTransition: 'transitionend', 10 | OTransition: 'oTransitionEnd', 11 | WebkitTransition: 'webkitTransitionEnd', 12 | } 13 | 14 | for (const t in transitions) { 15 | if (el.style[t as keyof CSSStyleDeclaration] !== undefined) { 16 | // Use a generic as the return type because TypeScript doesnt know 17 | // about cross browser features, so we cast here to align to the 18 | // standard Event spec and propagate the type properly to the callbacks 19 | // of `addEventListener` and `removeEventListener`. 20 | return transitions[t] as TransitionEventName 21 | } 22 | } 23 | 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /packages/full/src/schema/nodes/blockquote.ts: -------------------------------------------------------------------------------- 1 | import { NodeSpec } from 'prosemirror-model' 2 | import { ParagraphDefinition as Paragraph } from './paragraph' 3 | 4 | /** 5 | * @name blockquote_node 6 | */ 7 | export interface BlockQuoteDefinition { 8 | type: 'blockquote' 9 | /** 10 | * @minItems 1 11 | */ 12 | content: Array 13 | } 14 | 15 | export const blockquote: NodeSpec = { 16 | content: 'paragraph+', 17 | group: 'block', 18 | defining: true, 19 | selectable: false, 20 | attrs: { 21 | class: { default: '' }, 22 | }, 23 | parseDOM: [{ tag: 'blockquote' }], 24 | toDOM() { 25 | return ['blockquote', 0] 26 | }, 27 | } 28 | 29 | export const pmBlockquote: NodeSpec = { 30 | content: 'paragraph+', 31 | group: 'block', 32 | defining: true, 33 | selectable: false, 34 | attrs: { 35 | class: { default: 'pm-blockquote' }, 36 | }, 37 | parseDOM: [{ tag: 'blockquote' }], 38 | toDOM(node) { 39 | return ['blockquote', node.attrs, 0] 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /packages/ssr/server/common/error.ts: -------------------------------------------------------------------------------- 1 | export interface IError extends Error { 2 | statusCode?: number 3 | } 4 | 5 | export class CustomError extends Error implements IError { 6 | statusCode: number 7 | 8 | constructor(message: string, errorCode = 500) { 9 | super(message) 10 | this.name = this.constructor.name 11 | this.statusCode = errorCode 12 | Error.captureStackTrace(this, this.constructor) 13 | } 14 | } 15 | 16 | export class ValidationError extends Error implements IError { 17 | readonly statusCode: number = 400 18 | 19 | constructor(message: string) { 20 | super(message) 21 | this.name = this.constructor.name 22 | Error.captureStackTrace(this, this.constructor) 23 | } 24 | } 25 | 26 | export class DBError extends Error implements IError { 27 | readonly statusCode: number = 500 28 | 29 | constructor(message: string) { 30 | super(message) 31 | this.name = this.constructor.name 32 | Error.captureStackTrace(this, this.constructor) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/types/types/document.d.ts: -------------------------------------------------------------------------------- 1 | import { Step } from 'prosemirror-transform' 2 | 3 | export interface EditorStateJSON { 4 | doc: { [key: string]: any } 5 | selection: { [key: string]: any } 6 | plugins: { [key: string]: any } 7 | } 8 | export type PMDoc = { 9 | [key: string]: any 10 | } 11 | export type PatchedStep = Step & { clientID: number } 12 | 13 | export type DocVisibility = 'private' | 'global' 14 | export interface IDBDocument { 15 | id: string 16 | title: string 17 | userId: string 18 | // createdAt: number 19 | // updatedAt: number 20 | doc: PMDoc 21 | visibility: DocVisibility 22 | } 23 | 24 | // POST /document 25 | export interface ICreateDocumentParams { 26 | title: string 27 | doc: PMDoc 28 | visibility: DocVisibility 29 | } 30 | // GET /documents 31 | export interface IGetDocumentsResponse { 32 | docs: IDBDocument[] 33 | } 34 | // GET /documents 35 | export interface IGetDocumentResponse { 36 | doc: PMDoc 37 | userCount: number 38 | version: number 39 | } 40 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/heading4.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconHeading4({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/type-ahead/commands/update-query.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../../../types' 2 | import { ACTIONS } from '../pm-plugins/actions' 3 | import { pluginKey } from '../pm-plugins/plugin-key' 4 | import { findTypeAheadQuery } from '../utils/find-query-mark' 5 | import { isQueryActive } from '../utils/is-query-active' 6 | 7 | export const updateQueryCommand = 8 | (query: string): Command => 9 | (state, dispatch) => { 10 | const queryMark = findTypeAheadQuery(state) 11 | const activeQuery = isQueryActive( 12 | state.schema.marks.typeAheadQuery, 13 | state.doc, 14 | state.selection.from, 15 | state.selection.to 16 | ) 17 | 18 | if (queryMark === null || activeQuery === false) { 19 | return false 20 | } 21 | 22 | if (dispatch) { 23 | dispatch( 24 | state.tr.setMeta(pluginKey, { 25 | action: ACTIONS.SET_QUERY, 26 | params: { query }, 27 | }) 28 | ) 29 | } 30 | return true 31 | } 32 | -------------------------------------------------------------------------------- /packages/atlassian/src/create-editor/sort-by-order.ts: -------------------------------------------------------------------------------- 1 | const ranks = { 2 | plugins: ['underline', 'blockquote', 'quickInsert', 'typeAhead'], 3 | nodes: [ 4 | 'doc', 5 | 'paragraph', 6 | 'text', 7 | 'bulletList', 8 | 'orderedList', 9 | 'listItem', 10 | 'heading', 11 | 'blockquote', 12 | 'codeBlock', 13 | ], 14 | marks: [ 15 | // Inline marks 16 | 'link', 17 | 'em', 18 | 'strong', 19 | 'textColor', 20 | 'strike', 21 | 'subsup', 22 | 'underline', 23 | 'code', 24 | 'typeAheadQuery', 25 | 26 | // Block marks 27 | 'alignment', 28 | 'breakout', 29 | 'indentation', 30 | 'annotation', 31 | 32 | //Unsupported mark 33 | 'unsupportedMark', 34 | 'unsupportedNodeAttribute', 35 | ], 36 | } 37 | 38 | export function sortByOrder(item: 'plugins' | 'nodes' | 'marks') { 39 | return function (a: { name: string }, b: { name: string }): number { 40 | return ranks[item].indexOf(a.name) - ranks[item].indexOf(b.name) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/atlassian/src/react-portals/PortalRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createPortal } from 'react-dom' 3 | import { PortalProviderAPI } from './PortalProviderAPI' 4 | 5 | export type Portals = Map 6 | 7 | interface IProps { 8 | portalProviderAPI: PortalProviderAPI 9 | } 10 | interface IState { 11 | portals: Portals 12 | } 13 | 14 | export class PortalRenderer extends React.Component { 15 | constructor(props: IProps) { 16 | super(props) 17 | props.portalProviderAPI.setContext(this) 18 | props.portalProviderAPI.on('update', this.handleUpdate) 19 | this.state = { portals: new Map() } 20 | } 21 | 22 | handleUpdate = (portals: Portals) => this.setState({ portals }) 23 | 24 | render() { 25 | const { portals } = this.state 26 | return ( 27 | <> 28 | {Array.from(portals.entries()).map(([container, children]) => 29 | createPortal(children, container) 30 | )} 31 | 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/createSchema.ts: -------------------------------------------------------------------------------- 1 | import { Schema, NodeSpec, MarkSpec } from 'prosemirror-model' 2 | import { IExtension, IExtensionSchema } from './Extension' 3 | 4 | export function createSchema(extensions: IExtension[]) { 5 | const nodes = extensions.reduce( 6 | (acc, cur) => ({ ...acc, ...cur.schema?.nodes }), 7 | {} as { [key: string]: NodeSpec } 8 | ) 9 | const marks = extensions.reduce( 10 | (acc, cur) => ({ ...acc, ...cur.schema?.marks }), 11 | {} as { [key: string]: MarkSpec } 12 | ) 13 | return new Schema({ 14 | nodes, 15 | marks, 16 | }) 17 | } 18 | 19 | export function createSchemaFromSpecs(specs: IExtensionSchema[]) { 20 | const nodes = specs.reduce( 21 | (acc, cur) => ({ ...acc, ...cur.nodes }), 22 | {} as { [key: string]: NodeSpec } 23 | ) 24 | const marks = specs.reduce( 25 | (acc, cur) => ({ ...acc, ...cur.marks }), 26 | {} as { [key: string]: MarkSpec } 27 | ) 28 | return new Schema({ 29 | nodes, 30 | marks, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /packages/atlassian/src/types/pm-plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'prosemirror-state' 2 | import { Schema } from 'prosemirror-model' 3 | 4 | // TODO: Check if this circular dependency is still needed or is just legacy 5 | // eslint-disable-next-line import/no-cycle 6 | import { EditorConfig } from './editor-config' 7 | import { Dispatch, EventDispatcher } from '../utils/event-dispatcher' 8 | import { PortalProviderAPI } from '../react-portals/PortalProviderAPI' 9 | import { ProviderFactory } from '../provider-factory' 10 | 11 | export type PMPluginFactoryParams = { 12 | schema: Schema 13 | dispatch: Dispatch 14 | eventDispatcher: EventDispatcher 15 | providerFactory: ProviderFactory 16 | portalProviderAPI: PortalProviderAPI 17 | } 18 | 19 | export type PMPluginCreateConfig = PMPluginFactoryParams & { 20 | editorConfig: EditorConfig 21 | } 22 | 23 | export type PMPluginFactory = (params: PMPluginFactoryParams) => Plugin | undefined 24 | 25 | export type PMPlugin = { 26 | name: string 27 | plugin: PMPluginFactory 28 | } 29 | -------------------------------------------------------------------------------- /packages/client-cra/src/components/editor/DesktopLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { Toolbar } from './Toolbar' 5 | 6 | interface IProps { 7 | className?: string 8 | children: React.ReactNode 9 | } 10 | 11 | export function DesktopLayout(props: IProps) { 12 | const { className, children } = props 13 | return ( 14 | 15 | 16 | {children} 17 | 18 | ) 19 | } 20 | 21 | const Container = styled.div`` 22 | const ContentArea = styled.div` 23 | border: 1px solid black; 24 | & > .ProseMirror { 25 | min-height: 140px; 26 | overflow-wrap: break-word; 27 | outline: none; 28 | padding: 10px; 29 | white-space: pre-wrap; 30 | } 31 | 32 | .pm-blockquote { 33 | box-sizing: border-box; 34 | color: #2d82e1; 35 | padding: 0 1em; 36 | border-left: 4px solid #48a1fa; 37 | margin: 0.2rem 0 0 0; 38 | margin-right: 0; 39 | } 40 | ` 41 | -------------------------------------------------------------------------------- /packages/full/src/core/create/ranks.ts: -------------------------------------------------------------------------------- 1 | const ranks = { 2 | plugins: ['underline', 'blockquote', 'quickInsert', 'typeAhead'], 3 | nodes: [ 4 | 'doc', 5 | 'paragraph', 6 | 'text', 7 | 'bulletList', 8 | 'orderedList', 9 | 'listItem', 10 | 'heading', 11 | 'blockquote', 12 | 'pmBlockquote', 13 | 'codeBlock', 14 | ], 15 | marks: [ 16 | // Inline marks 17 | 'link', 18 | 'em', 19 | 'strong', 20 | 'textColor', 21 | 'strike', 22 | 'subsup', 23 | 'underline', 24 | 'code', 25 | 'typeAheadQuery', 26 | 27 | // Block marks 28 | 'alignment', 29 | 'breakout', 30 | 'indentation', 31 | 'annotation', 32 | 33 | //Unsupported mark 34 | 'unsupportedMark', 35 | 'unsupportedNodeAttribute', 36 | ], 37 | } 38 | 39 | export function sortByOrder(item: 'plugins' | 'nodes' | 'marks') { 40 | return function (a: { name: string }, b: { name: string }): number { 41 | return ranks[item].indexOf(a.name) - ranks[item].indexOf(b.name) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "eslint-plugin-import", "eslint-plugin-prettier"], 5 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 6 | "env": { 7 | "es6": true, 8 | "node": true 9 | }, 10 | "rules": { 11 | "@typescript-eslint/explicit-function-return-type": 0, 12 | "@typescript-eslint/explicit-member-accessibility": 0, 13 | "@typescript-eslint/indent": 0, 14 | "@typescript-eslint/member-delimiter-style": 0, 15 | "@typescript-eslint/no-explicit-any": 0, 16 | "@typescript-eslint/explicit-module-boundary-types": "off", 17 | "@typescript-eslint/no-var-requires": 0, 18 | "@typescript-eslint/no-use-before-define": 0, 19 | "@typescript-eslint/no-unused-vars": [ 20 | 2, 21 | { 22 | "argsIgnorePattern": "^_" 23 | } 24 | ], 25 | "no-console": [ 26 | 2, 27 | { 28 | "allow": ["warn", "error"] 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/full/src/core/types/editor-plugin.ts: -------------------------------------------------------------------------------- 1 | import { PMPlugin } from './pm-plugin' 2 | import { MarkConfig, NodeConfig } from './editor-config' 3 | 4 | export interface PluginsOptions { 5 | [pluginName: string]: any 6 | // blockQuote?: BlockQuoteHandler; 7 | } 8 | 9 | export interface EditorPlugin { 10 | /** 11 | * Name of a plugin, that other plugins can use to provide options to it or exclude via a preset. 12 | */ 13 | name: string 14 | 15 | /** 16 | * Options that will be passed to a plugin with a corresponding name if it exists and enabled. 17 | */ 18 | pluginsOptions?: any 19 | 20 | /** 21 | * List of ProseMirror-plugins. This is where we define which plugins will be added to EditorView (main-plugin, keybindings, input-rules, etc.). 22 | */ 23 | pmPlugins?: (pluginOptions?: any) => PMPlugin[] 24 | 25 | /** 26 | * List of Nodes to add to the schema. 27 | */ 28 | nodes?: () => NodeConfig[] 29 | 30 | /** 31 | * List of Marks to add to the schema. 32 | */ 33 | marks?: () => MarkConfig[] 34 | } 35 | -------------------------------------------------------------------------------- /packages/types/types/socket-collab.d.ts: -------------------------------------------------------------------------------- 1 | import { ECollabAction } from '../src' 2 | 3 | export { ECollabAction } from '../src' 4 | 5 | export type EditorSocketAction = CollabAction 6 | export type EditorSocketActionType = ECollabAction 7 | 8 | // Collab actions 9 | // REMEMBER: when adding enums, update the shared.js file 10 | 11 | export type CollabAction = ICollabUsersChangedAction | ICollabEditAction | ICollabServerUpdateAction 12 | export interface ICollabUsersChangedAction { 13 | type: ECollabAction.COLLAB_USERS_CHANGED 14 | payload: { 15 | documentId: string 16 | userCount: number 17 | userId: string 18 | } 19 | } 20 | export interface ICollabEditPayload { 21 | version: number 22 | steps: { [key: string]: any }[] 23 | clientIDs: number[] 24 | } 25 | export interface ICollabEditAction { 26 | type: ECollabAction.COLLAB_CLIENT_EDIT 27 | payload: ICollabEditPayload 28 | } 29 | export interface ICollabServerUpdateAction { 30 | type: ECollabAction.COLLAB_SERVER_UPDATE 31 | payload: { 32 | cursors: any 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Teemu Koivisto 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/mention.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconMention({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/heading2.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconHeading2({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/full/src/performance/is-performance-api-available.ts: -------------------------------------------------------------------------------- 1 | let hasRequiredPerformanceAPIs: boolean | undefined 2 | 3 | export function isPerformanceAPIAvailable(): boolean { 4 | if (hasRequiredPerformanceAPIs === undefined) { 5 | hasRequiredPerformanceAPIs = 6 | typeof window !== 'undefined' && 7 | 'performance' in window && 8 | [ 9 | 'measure', 10 | 'clearMeasures', 11 | 'clearMarks', 12 | 'getEntriesByName', 13 | 'getEntriesByType', 14 | 'now', 15 | ].every((api) => !!(performance as any)[api]) 16 | } 17 | 18 | return hasRequiredPerformanceAPIs 19 | } 20 | 21 | export function isPerformanceObserverAvailable(): boolean { 22 | return !!(typeof window !== 'undefined' && 'PerformanceObserver' in window) 23 | } 24 | 25 | export function isPerformanceObserverLongTaskAvailable(): boolean { 26 | return ( 27 | isPerformanceObserverAvailable() && 28 | PerformanceObserver.supportedEntryTypes && 29 | PerformanceObserver.supportedEntryTypes.includes('longtask') 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/atlassian/src/performance/is-performance-api-available.ts: -------------------------------------------------------------------------------- 1 | let hasRequiredPerformanceAPIs: boolean | undefined 2 | 3 | export function isPerformanceAPIAvailable(): boolean { 4 | if (hasRequiredPerformanceAPIs === undefined) { 5 | hasRequiredPerformanceAPIs = 6 | typeof window !== 'undefined' && 7 | 'performance' in window && 8 | [ 9 | 'measure', 10 | 'clearMeasures', 11 | 'clearMarks', 12 | 'getEntriesByName', 13 | 'getEntriesByType', 14 | 'now', 15 | ].every((api) => !!(performance as any)[api]) 16 | } 17 | 18 | return hasRequiredPerformanceAPIs 19 | } 20 | 21 | export function isPerformanceObserverAvailable(): boolean { 22 | return !!(typeof window !== 'undefined' && 'PerformanceObserver' in window) 23 | } 24 | 25 | export function isPerformanceObserverLongTaskAvailable(): boolean { 26 | return ( 27 | isPerformanceObserverAvailable() && 28 | PerformanceObserver.supportedEntryTypes && 29 | PerformanceObserver.supportedEntryTypes.includes('longtask') 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/fallback.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconFallback({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/full-v2/src/context/analytics/is-performance-api-available.ts: -------------------------------------------------------------------------------- 1 | let hasRequiredPerformanceAPIs: boolean | undefined 2 | 3 | export function isPerformanceAPIAvailable(): boolean { 4 | if (hasRequiredPerformanceAPIs === undefined) { 5 | hasRequiredPerformanceAPIs = 6 | typeof window !== 'undefined' && 7 | 'performance' in window && 8 | [ 9 | 'measure', 10 | 'clearMeasures', 11 | 'clearMarks', 12 | 'getEntriesByName', 13 | 'getEntriesByType', 14 | 'now', 15 | ].every((api) => !!(performance as any)[api]) 16 | } 17 | 18 | return hasRequiredPerformanceAPIs 19 | } 20 | 21 | export function isPerformanceObserverAvailable(): boolean { 22 | return !!(typeof window !== 'undefined' && 'PerformanceObserver' in window) 23 | } 24 | 25 | export function isPerformanceObserverLongTaskAvailable(): boolean { 26 | return ( 27 | isPerformanceObserverAvailable() && 28 | PerformanceObserver.supportedEntryTypes && 29 | PerformanceObserver.supportedEntryTypes.includes('longtask') 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/ssr/server/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import morgan from 'morgan' 3 | import cors from 'cors' 4 | 5 | import routes from './routes' 6 | 7 | import { errorHandler, logStream, config } from './common' 8 | 9 | const app = express() 10 | 11 | const corsOptions: cors.CorsOptions = { 12 | origin(origin, callback) { 13 | if (config.CORS_SAME_ORIGIN === 'false') { 14 | callback(null, true) 15 | } else { 16 | callback(null, false) 17 | } 18 | }, 19 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 20 | } 21 | 22 | app.use(cors(corsOptions)) 23 | app.use(express.urlencoded({ extended: true })) 24 | app.use(express.json()) 25 | 26 | // By adding this route before morgan prevents it being logged which in production setting 27 | // is annoying and pollutes the logs with gazillion "GET /health" lines 28 | app.get('/health', (req: any, res: any) => { 29 | res.sendStatus(200) 30 | }) 31 | 32 | app.use(morgan('short', { stream: logStream })) 33 | 34 | app.use('', routes) 35 | app.use(errorHandler) 36 | 37 | export { app } 38 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/types", 3 | "version": "0.0.1", 4 | "main": "dist/index.cjs", 5 | "module": "dist/index.js", 6 | "type": "module", 7 | "types": "types/index.d.ts", 8 | "exports": { 9 | "./package.json": "./package.json", 10 | ".": { 11 | "import": "./dist/index.js", 12 | "require": "./dist/index.cjs" 13 | } 14 | }, 15 | "files": [ 16 | "dist", 17 | "src", 18 | "types" 19 | ], 20 | "scripts": { 21 | "build": "rimraf dist && rollup -c", 22 | "format": "prettier --write \"*.+(js|json|yml|yaml|ts|md|graphql|mdx)\" src/", 23 | "lint": "eslint --cache --ext .js,.ts, ./src ./types", 24 | "lint:fix": "eslint --fix --ext .js,.ts, ./src ./types", 25 | "watch": "rollup -cw" 26 | }, 27 | "devDependencies": { 28 | "rimraf": "^3.0.2", 29 | "rollup": "^2.72.0", 30 | "rollup-plugin-typescript2": "^0.30.0", 31 | "typescript": "^4.6.4" 32 | }, 33 | "dependencies": { 34 | "prosemirror-state": "^1.4.1", 35 | "prosemirror-transform": "^1.6.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/ssr/server/common/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { log } from './logger' 2 | import { Request, Response, NextFunction } from 'express' 3 | 4 | import { IError } from './error' 5 | 6 | import { config } from './config' 7 | 8 | export function errorHandler(err: IError, req: Request, res: Response, next: NextFunction): void { 9 | if (err) { 10 | const statusCode = err.statusCode ? err.statusCode : 500 11 | const message = statusCode === 500 ? 'Internal server error.' : 'Something went wrong.' 12 | const body: { message: string; stack?: string } = { message } 13 | if (statusCode === 500) { 14 | log.error('Handled internal server error:') 15 | log.error(err) 16 | log.error(err.stack || 'no stacktrace') 17 | } else { 18 | log.info('Handled error: ') 19 | log.info(err) 20 | log.debug(err.stack || 'no stacktrace') 21 | } 22 | if (config.ENV === 'local') { 23 | body.message = err.message 24 | body.stack = err.stack 25 | } 26 | res.status(statusCode).json(body) 27 | } else { 28 | next() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/api-collab/src/common/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { log } from './logger' 2 | import { Request, Response, NextFunction } from 'express' 3 | 4 | import { IError } from './error' 5 | 6 | import { config } from './config' 7 | 8 | export function errorHandler(err: IError, req: Request, res: Response, next: NextFunction): void { 9 | if (err) { 10 | const statusCode = err.statusCode ? err.statusCode : 500 11 | const message = statusCode === 500 ? 'Internal server error.' : 'Something went wrong.' 12 | const body: { message: string; stack?: string } = { message } 13 | if (statusCode === 500) { 14 | log.error('Handled internal server error:') 15 | log.error(err) 16 | log.error(err.stack || 'no stacktrace') 17 | } else { 18 | log.info('Handled error: ') 19 | log.info(err) 20 | log.debug(err.stack || 'no stacktrace') 21 | } 22 | if (config.ENV === 'local') { 23 | body.message = err.message 24 | body.stack = err.stack 25 | } 26 | res.status(statusCode).json(body) 27 | } else { 28 | next() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/heading5.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconHeading5({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/full/src/ui/MarkButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | interface IProps { 5 | className?: string 6 | active: boolean 7 | name: string 8 | icon: React.ReactNode 9 | onClick: () => void 10 | } 11 | function MarkButtonEl(props: IProps) { 12 | const { className, active, name, icon, onClick } = props 13 | return ( 14 | 17 | ) 18 | } 19 | 20 | interface IButtonProps { 21 | active: boolean 22 | } 23 | const SvgWrapper = styled.span` 24 | display: flex; 25 | ` 26 | const Button = styled.button` 27 | background: ${(props: IButtonProps) => (props.active ? '#f0f8ff' : 'transparent')}; 28 | border: ${(props: IButtonProps) => 29 | props.active ? '1px solid #5a6ecd' : '1px solid transparent'}; 30 | cursor: pointer; 31 | display: flex; 32 | margin-right: 5px; 33 | padding: 1px; 34 | &:hover { 35 | background: #f0f8ff; 36 | opacity: 0.6; 37 | } 38 | ` 39 | export const MarkButton = styled(MarkButtonEl)`` 40 | -------------------------------------------------------------------------------- /packages/minimal/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { Node as PMNode, Schema } from 'prosemirror-model' 2 | 3 | export const schema = new Schema({ 4 | nodes: { 5 | doc: { 6 | content: 'block+', 7 | }, 8 | paragraph: { 9 | content: 'inline*', 10 | group: 'block', 11 | selectable: false, 12 | parseDOM: [{ tag: 'p' }], 13 | toDOM() { 14 | return ['p', 0] 15 | }, 16 | }, 17 | pmBlockquote: { 18 | content: 'paragraph+', 19 | group: 'block', 20 | defining: true, 21 | selectable: false, 22 | attrs: { 23 | class: { default: 'pm-blockquote' }, 24 | }, 25 | parseDOM: [{ tag: 'blockquote' }], 26 | toDOM(node: PMNode) { 27 | return ['blockquote', node.attrs, 0] 28 | }, 29 | }, 30 | blockquote: { 31 | content: 'paragraph+', 32 | group: 'block', 33 | defining: true, 34 | selectable: false, 35 | parseDOM: [{ tag: 'blockquote' }], 36 | toDOM() { 37 | return ['blockquote', 0] 38 | }, 39 | }, 40 | text: { 41 | group: 'inline', 42 | }, 43 | }, 44 | }) 45 | -------------------------------------------------------------------------------- /packages/prosemirror-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/prosemirror-utils", 3 | "version": "0.0.1", 4 | "main": "dist/index.cjs", 5 | "module": "dist/index.js", 6 | "type": "module", 7 | "types": "dist/index.d.ts", 8 | "exports": { 9 | "./package.json": "./package.json", 10 | ".": { 11 | "import": "./dist/index.js", 12 | "require": "./dist/index.cjs" 13 | } 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "build": "rimraf dist && rollup -c", 20 | "format": "prettier --write \"*.+(js|json|yml|yaml|ts|md|graphql|mdx)\" src/", 21 | "lint": "eslint --cache --ext .js,.ts, ./src", 22 | "lint:fix": "eslint --fix --ext .js,.ts, ./src", 23 | "watch": "rollup -cw" 24 | }, 25 | "devDependencies": { 26 | "rimraf": "^3.0.2", 27 | "rollup": "^2.72.0", 28 | "rollup-plugin-typescript2": "^0.30.0", 29 | "typescript": "^4.6.4" 30 | }, 31 | "dependencies": { 32 | "prosemirror-model": "^1.18.1", 33 | "prosemirror-state": "^1.4.1", 34 | "prosemirror-transform": "^1.6.0", 35 | "prosemirror-utils": "^0.9.6" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/full-v2/README.md: -------------------------------------------------------------------------------- 1 | # Full editor v2 2 | 3 | As I started adding collaboration, I noticed that the approach taken by the Atlassian editor and in turn, taken by me in the full editor, didn't really seem optimal for managing the sprawling complexity of an editor. Especially adding multiple providers, hidden somewhere inside the editorPlugins seemed overly obtuse and also, fixing the layout to the editor, toolbar et cetera, seemed non-optimal. 4 | 5 | So I again restructured the whole thing, yey! But now I took some inspiration from the other PM-React editors out there, mainly TipTap's v2, to design the API. So instead of editorPlugins, the editor now uses extensions which are basically the same but include the schema. Let's see how good of a choice this is. 6 | 7 | ## How to install 8 | 9 | This project uses Yarn workspaces so you don't really have to install anything. By running `yarn` in the root folder all the dependencies should be installed automatically. 10 | 11 | ## Commands 12 | 13 | - `yarn watch` starts the Rollup compiler and watches changes to the editor 14 | - `yarn build` compiles the code as both CommonJS and ES module. 15 | -------------------------------------------------------------------------------- /packages/atlassian/src/editor-appearance/MarkButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | interface IProps { 5 | className?: string 6 | active: boolean 7 | name: string 8 | icon: React.ReactNode 9 | onClick: () => void 10 | } 11 | function MarkButtonEl(props: IProps) { 12 | const { className, active, name, icon, onClick } = props 13 | return ( 14 | 17 | ) 18 | } 19 | 20 | interface IButtonProps { 21 | active: boolean 22 | } 23 | const SvgWrapper = styled.span` 24 | display: flex; 25 | ` 26 | const Button = styled.button` 27 | background: ${(props: IButtonProps) => (props.active ? '#f0f8ff' : 'transparent')}; 28 | border: ${(props: IButtonProps) => 29 | props.active ? '1px solid #5a6ecd' : '1px solid transparent'}; 30 | cursor: pointer; 31 | display: flex; 32 | margin-right: 5px; 33 | padding: 1px; 34 | &:hover { 35 | background: #f0f8ff; 36 | opacity: 0.6; 37 | } 38 | ` 39 | export const MarkButton = styled(MarkButtonEl)`` 40 | -------------------------------------------------------------------------------- /packages/client-cra/src/components/editor/MarkButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | interface IProps { 5 | className?: string 6 | active: boolean 7 | name: string 8 | icon: React.ReactNode 9 | onClick: () => void 10 | } 11 | function MarkButtonEl(props: IProps) { 12 | const { className, active, name, icon, onClick } = props 13 | return ( 14 | 17 | ) 18 | } 19 | 20 | interface IButtonProps { 21 | active: boolean 22 | } 23 | const SvgWrapper = styled.span` 24 | display: flex; 25 | ` 26 | const Button = styled.button` 27 | background: ${(props: IButtonProps) => (props.active ? '#f0f8ff' : 'transparent')}; 28 | border: ${(props: IButtonProps) => 29 | props.active ? '1px solid #5a6ecd' : '1px solid transparent'}; 30 | cursor: pointer; 31 | display: flex; 32 | margin-right: 5px; 33 | padding: 1px; 34 | &:hover { 35 | background: #f0f8ff; 36 | opacity: 0.6; 37 | } 38 | ` 39 | export const MarkButton = styled(MarkButtonEl)`` 40 | -------------------------------------------------------------------------------- /packages/client-cra/src/stores/EditorStore.ts: -------------------------------------------------------------------------------- 1 | import { EditorContext } from '@example/full-v2' 2 | import { EditorStateJSON } from '@example/types' 3 | import { PMDoc } from '../types/document' 4 | 5 | export class EditorStore { 6 | editorCtx?: EditorContext 7 | currentEditorState?: EditorStateJSON 8 | 9 | setEditorContext = (ctx: EditorContext) => { 10 | this.editorCtx = ctx 11 | } 12 | 13 | getEditorState = () => { 14 | return this.editorCtx!.viewProvider.stateToJSON() 15 | } 16 | 17 | createEmptyDoc = (): PMDoc => { 18 | const json = JSON.parse('{"type":"doc","content":[{"type":"paragraph","content":[]}]}') 19 | if (!this.editorCtx) { 20 | throw Error('Undefined editorCtx, did you forget to call setEditorContext?') 21 | } 22 | const node = this.editorCtx.viewProvider.editorView.state.schema.nodeFromJSON(json) 23 | node.check() 24 | return node.toJSON() 25 | } 26 | 27 | setCurrentDoc = (doc?: PMDoc) => { 28 | const pmDoc = doc ?? this.createEmptyDoc() 29 | this.editorCtx?.viewProvider.replaceState(pmDoc) 30 | } 31 | 32 | reset = () => { 33 | this.setCurrentDoc() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/full-v2/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import postcss from 'rollup-plugin-postcss' 3 | import alias from '@rollup/plugin-alias' 4 | import ttypescript from 'ttypescript' 5 | 6 | import path from 'path' 7 | 8 | import pkg from './package.json' 9 | 10 | export default { 11 | input: ['src/index.ts'], 12 | output: [ 13 | { 14 | file: pkg.main, 15 | format: 'cjs', 16 | }, 17 | { 18 | file: pkg.module, 19 | format: 'es', 20 | }, 21 | ], 22 | external: [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})], 23 | plugins: [ 24 | alias({ 25 | entries: [ 26 | { find: '@context', replacement: path.resolve(__dirname, 'src/context') }, 27 | { find: '@core', replacement: path.resolve(__dirname, 'src/core') }, 28 | { find: '@extensions', replacement: path.resolve(__dirname, 'src/extensions') }, 29 | { find: '@react', replacement: path.resolve(__dirname, 'src/react') }, 30 | ], 31 | }), 32 | typescript({ 33 | typescript: ttypescript, 34 | }), 35 | postcss(), 36 | ], 37 | } 38 | -------------------------------------------------------------------------------- /packages/full/src/collab-api.ts: -------------------------------------------------------------------------------- 1 | import { IGetDocumentResponse, ISaveCollabStepsParams } from '@example/types' 2 | 3 | const COLLAB_API_URL = 'http://localhost:3400' 4 | 5 | export async function sendSteps(payload: ISaveCollabStepsParams): Promise<{ version: number }> { 6 | const resp = await fetch(`${COLLAB_API_URL}/doc/1/events`, { 7 | method: 'POST', 8 | headers: { 9 | Accept: 'application/json', 10 | 'Content-Type': 'application/json', 11 | }, 12 | body: JSON.stringify(payload), 13 | }) 14 | return resp.json() 15 | } 16 | 17 | export async function getDocument(): Promise { 18 | const resp = await fetch(`${COLLAB_API_URL}/doc/1`, { 19 | method: 'GET', 20 | headers: { 21 | Accept: 'application/json', 22 | 'Content-Type': 'application/json', 23 | }, 24 | }) 25 | return resp.json() 26 | } 27 | 28 | export async function fetchEvents(version: number) { 29 | return fetch(`${COLLAB_API_URL}/doc/1/events?version=${version}`, { 30 | method: 'GET', 31 | headers: { 32 | Accept: 'application/json', 33 | 'Content-Type': 'application/json', 34 | }, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /packages/client-cra/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthStore } from './AuthStore' 2 | import { DocumentStore } from './DocumentStore' 3 | import { EditorStore } from './EditorStore' 4 | import { SyncStore } from './SyncStore' 5 | import { ToastStore } from './ToastStore' 6 | 7 | export class Stores { 8 | authStore: AuthStore 9 | documentStore: DocumentStore 10 | editorStore: EditorStore 11 | syncStore: SyncStore 12 | toastStore: ToastStore 13 | 14 | constructor() { 15 | this.authStore = new AuthStore(this.reset) 16 | this.toastStore = new ToastStore() 17 | this.editorStore = new EditorStore() 18 | this.documentStore = new DocumentStore({ 19 | authStore: this.authStore, 20 | editorStore: this.editorStore, 21 | toastStore: this.toastStore, 22 | }) 23 | this.syncStore = new SyncStore({ 24 | authStore: this.authStore, 25 | documentStore: this.documentStore, 26 | toastStore: this.toastStore, 27 | }) 28 | } 29 | 30 | reset = () => { 31 | this.authStore.reset() 32 | this.documentStore.reset() 33 | this.editorStore.reset() 34 | this.syncStore.reset() 35 | this.toastStore.reset() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/expand.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconExpand({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/api-collab/src/routes/doc/document.io.ts: -------------------------------------------------------------------------------- 1 | import { socketIO } from 'socket-io/socketIO' 2 | 3 | import { 4 | EDocAction, 5 | IDBDocument, 6 | DocVisibility, 7 | IDocCreateAction, 8 | IDocDeleteAction, 9 | IDocVisibilityAction, 10 | } from '@example/types' 11 | 12 | export const documentIO = { 13 | emitDocCreated(doc: IDBDocument, userId: string) { 14 | const action: IDocCreateAction = { 15 | type: EDocAction.DOC_CREATE, 16 | payload: { 17 | doc, 18 | userId, 19 | }, 20 | } 21 | socketIO.emitToAll(action) 22 | }, 23 | emitDocDeleted(documentId: string, userId: string) { 24 | const action: IDocDeleteAction = { 25 | type: EDocAction.DOC_DELETE, 26 | payload: { 27 | documentId, 28 | userId, 29 | }, 30 | } 31 | socketIO.emitToAll(action) 32 | }, 33 | emitVisibilityChanged(documentId: string, visibility: DocVisibility, userId: string) { 34 | const action: IDocVisibilityAction = { 35 | type: EDocAction.DOC_VISIBILITY, 36 | payload: { 37 | documentId, 38 | visibility, 39 | userId, 40 | }, 41 | } 42 | socketIO.emitToAll(action) 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /packages/full/src/schema/marks/strong.ts: -------------------------------------------------------------------------------- 1 | import { MarkSpec } from 'prosemirror-model' 2 | import { FONT_STYLE } from './groups' 3 | 4 | export interface StrongDefinition { 5 | type: 'strong' 6 | } 7 | 8 | export const strong: MarkSpec = { 9 | inclusive: true, 10 | group: FONT_STYLE, 11 | parseDOM: [ 12 | { tag: 'strong' }, 13 | // This works around a Google Docs misbehavior where 14 | // pasted content will be inexplicably wrapped in `` 15 | // tags with a font-weight normal. 16 | { 17 | tag: 'b', 18 | getAttrs(node) { 19 | const element = node as HTMLElement 20 | return element.style.fontWeight !== 'normal' && null 21 | }, 22 | }, 23 | { 24 | tag: 'span', 25 | getAttrs(node) { 26 | const element = node as HTMLElement 27 | const { fontWeight } = element.style 28 | return ( 29 | typeof fontWeight === 'string' && 30 | (fontWeight === 'bold' || 31 | fontWeight === 'bolder' || 32 | /^(bold(er)?|[5-9]\d{2,})$/.test(fontWeight)) && 33 | null 34 | ) 35 | }, 36 | }, 37 | ], 38 | toDOM() { 39 | return ['strong'] 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/type-ahead/commands/insert-query.ts: -------------------------------------------------------------------------------- 1 | import { TextSelection } from 'prosemirror-state' 2 | 3 | import { safeInsert } from '@example/prosemirror-utils' 4 | 5 | import { Command } from '../../../types' 6 | 7 | export function insertTypeAheadQuery(trigger: string, replaceLastChar = false): Command { 8 | return (state, dispatch) => { 9 | if (!dispatch) { 10 | return false 11 | } 12 | 13 | if (replaceLastChar) { 14 | const { tr, selection } = state 15 | const marks = selection.$from.marks() 16 | 17 | dispatch( 18 | tr 19 | .setSelection(TextSelection.create(tr.doc, selection.$from.pos - 1, selection.$from.pos)) 20 | .replaceSelectionWith( 21 | state.doc.type.schema.text(trigger, [ 22 | state.schema.marks.typeAheadQuery.create({ trigger }), 23 | ...marks, 24 | ]), 25 | false 26 | ) 27 | ) 28 | return true 29 | } 30 | 31 | dispatch( 32 | safeInsert( 33 | state.schema.text(trigger, [state.schema.marks.typeAheadQuery.create({ trigger })]) 34 | )(state.tr) 35 | ) 36 | 37 | return true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/marks/strong.ts: -------------------------------------------------------------------------------- 1 | import { MarkSpec } from 'prosemirror-model' 2 | import { FONT_STYLE } from './groups' 3 | 4 | export interface StrongDefinition { 5 | type: 'strong' 6 | } 7 | 8 | export const strong: MarkSpec = { 9 | inclusive: true, 10 | group: FONT_STYLE, 11 | parseDOM: [ 12 | { tag: 'strong' }, 13 | // This works around a Google Docs misbehavior where 14 | // pasted content will be inexplicably wrapped in `` 15 | // tags with a font-weight normal. 16 | { 17 | tag: 'b', 18 | getAttrs(node) { 19 | const element = node as HTMLElement 20 | return element.style.fontWeight !== 'normal' && null 21 | }, 22 | }, 23 | { 24 | tag: 'span', 25 | getAttrs(node) { 26 | const element = node as HTMLElement 27 | const { fontWeight } = element.style 28 | return ( 29 | typeof fontWeight === 'string' && 30 | (fontWeight === 'bold' || 31 | fontWeight === 'bolder' || 32 | /^(bold(er)?|[5-9]\d{2,})$/.test(fontWeight)) && 33 | null 34 | ) 35 | }, 36 | }, 37 | ], 38 | toDOM() { 39 | return ['strong'] 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /packages/client-cra/src/stores/ToastStore.ts: -------------------------------------------------------------------------------- 1 | import { action, observable, makeObservable } from 'mobx' 2 | 3 | import { IToast, ToastLocation, ToastType } from '../types/toast' 4 | 5 | export class ToastStore { 6 | @observable toasts: IToast[] = [] 7 | @observable toasterLocation: ToastLocation = 'top-right' 8 | idCounter: number = 0 9 | 10 | constructor() { 11 | makeObservable(this) 12 | } 13 | 14 | @action reset() { 15 | this.toasts = [] 16 | } 17 | 18 | @action setToasterLocation = (topRight?: boolean) => { 19 | if (topRight) { 20 | this.toasterLocation = 'top-right' 21 | } else { 22 | this.toasterLocation = 'bottom-left' 23 | } 24 | } 25 | 26 | @action createToast = (message: string, type: ToastType = 'success', duration: number = 5000) => { 27 | const newToast = { 28 | id: this.idCounter, 29 | message, 30 | type, 31 | duration, 32 | } 33 | this.idCounter += 1 34 | this.toasts.push(newToast) 35 | if (this.toasts.length > 2) { 36 | this.toasts = this.toasts.slice(-2) 37 | } 38 | } 39 | 40 | @action removeToast = (id: number) => { 41 | this.toasts = this.toasts.filter((t) => t.id !== id) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/minimal/src/nodeviews/BlockQuote.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import './BlockQuote.scss' 4 | 5 | // Copied from here https://gist.github.com/esmevane/7326b19e20a5670954b51ea8618d096d 6 | // Here we have the (too simple) React component which 7 | // we'll be rendering content into. 8 | // 9 | export class BlockQuote extends React.Component<{}, {}> { 10 | hole: React.RefObject 11 | 12 | constructor(props: {}) { 13 | super(props) 14 | this.hole = React.createRef() 15 | } 16 | // We'll put the content into what we render using 17 | // this function, which appends a given node to 18 | // a ref HTMLElement, if present. 19 | // 20 | append(node: HTMLElement) { 21 | if (this.hole) { 22 | this.hole.current!.appendChild(node) 23 | } 24 | } 25 | 26 | render() { 27 | // The styled components version is basically just a wrapper to do SCSS styling. 28 | // Questionable if it's even needed for such simple styling and because you can't clearly see the 29 | // DOM structure from the code (hence making `& > ${Component}` selectors quite unintuitive) 30 | return
31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/ssr/server/routes/ssr/ssr.service.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { renderToString } from 'react-dom/server' 3 | import { ServerStyleSheet } from 'styled-components' 4 | 5 | import { ServerRoutes } from '../../../client/routes' 6 | 7 | export const ssrService = { 8 | render(url: string, bundle = true) { 9 | const sheet = new ServerStyleSheet() 10 | 11 | const app = renderToString(sheet.collectStyles()) 12 | 13 | const initialState = { ssr: true } 14 | 15 | const html = ` 16 | 17 | 18 | 19 | 20 | 21 | SSR example 22 | 23 | 24 |
${app}
25 | 28 | ${bundle ? '' : ''} 29 | ${sheet.getStyleTags()} 30 | 31 | 32 | ` 33 | sheet.seal() 34 | return html 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconLink({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconLayout({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 23 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/heading3.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconHeading3({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/heading6.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconHeading6({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/blockquote/BlockQuoteExtension.tsx: -------------------------------------------------------------------------------- 1 | import { EditorState } from 'prosemirror-state' 2 | import { Extension, IExtensionSchema } from '../Extension' 3 | 4 | import { blockquote } from './nodes/blockquote' 5 | import { blockQuotePluginFactory } from './pm-plugins/main' 6 | import { blockquotePluginKey, getPluginState } from './pm-plugins/state' 7 | import { keymapPlugin } from './pm-plugins/keymap' 8 | 9 | export interface BlockQuoteExtensionProps {} 10 | export const blockQuoteSchema: IExtensionSchema = { 11 | nodes: { blockquote: blockquote }, 12 | } 13 | export class BlockQuoteExtension extends Extension { 14 | get name() { 15 | return 'blockquote' as const 16 | } 17 | 18 | get schema() { 19 | return blockQuoteSchema 20 | } 21 | 22 | static get pluginKey() { 23 | return blockquotePluginKey 24 | } 25 | 26 | static getPluginState(state: EditorState) { 27 | return getPluginState(state) 28 | } 29 | 30 | get plugins() { 31 | return [ 32 | { 33 | name: 'blockquote', 34 | plugin: () => blockQuotePluginFactory(this.ctx, this.props), 35 | }, 36 | { name: 'blockquoteKeyMap', plugin: () => keymapPlugin() }, 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/blockquote/nodeviews/BlockQuoteView.ts: -------------------------------------------------------------------------------- 1 | import { NodeView, EditorView, Decoration, DecorationSource } from 'prosemirror-view' 2 | import { Node as PMNode } from 'prosemirror-model' 3 | 4 | import { ReactNodeView } from '@react' 5 | import { EditorContext } from '@context' 6 | 7 | import { BlockQuote } from '../ui/BlockQuote' 8 | 9 | import { BlockQuoteOptions, IViewProps, IBlockQuoteAttrs } from '..' 10 | 11 | export class BlockQuoteView extends ReactNodeView { 12 | createContentDOM() { 13 | const contentDOM = document.createElement('div') 14 | contentDOM.classList.add(`${this.node.type.name}__content-dom`) 15 | return contentDOM 16 | } 17 | } 18 | 19 | export function blockQuoteNodeView(ctx: EditorContext, options?: BlockQuoteOptions) { 20 | return ( 21 | node: PMNode, 22 | view: EditorView, 23 | getPos: () => number, 24 | decorations: readonly Decoration[], 25 | innerDecorations: DecorationSource 26 | ): NodeView => 27 | new BlockQuoteView( 28 | node, 29 | view, 30 | getPos, 31 | decorations, 32 | innerDecorations, 33 | ctx, 34 | { 35 | options, 36 | }, 37 | BlockQuote 38 | ).init() 39 | } 40 | -------------------------------------------------------------------------------- /packages/minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/minimal", 3 | "version": "0.0.1", 4 | "main": "dist/index.cjs", 5 | "module": "dist/index.js", 6 | "type": "module", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "build": "rollup -c", 13 | "watch": "rollup -cw", 14 | "format": "prettier --write \"*.+(js|json|yml|yaml|ts|md|graphql|mdx)\" src/", 15 | "lint": "eslint --cache --ext .js,.ts, ./src", 16 | "lint:fix": "eslint --fix --ext .js,.ts, ./src" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "18.0.15", 20 | "@types/react-dom": "18.0.6", 21 | "postcss": "^8.4.14", 22 | "rollup": "^2.77.0", 23 | "rollup-plugin-postcss": "^4.0.2", 24 | "rollup-plugin-typescript2": "^0.32.1", 25 | "sass": "^1.53.0", 26 | "tslib": "^2.4.0", 27 | "typescript": "^4.7.4" 28 | }, 29 | "peerDependencies": { 30 | "react": ">=17.0.2", 31 | "react-dom": ">=17.0.2" 32 | }, 33 | "dependencies": { 34 | "prosemirror-commands": "^1.3.0", 35 | "prosemirror-history": "^1.3.0", 36 | "prosemirror-keymap": "^1.2.0", 37 | "prosemirror-model": "^1.18.1", 38 | "prosemirror-state": "^1.4.1", 39 | "prosemirror-view": "^1.27.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/api-collab/src/routes/doc/document.svc.ts: -------------------------------------------------------------------------------- 1 | import { docDb } from 'db/doc.db' 2 | import { collabDb } from 'db/collab.db' 3 | 4 | import { IDBDocument, ICreateDocumentParams } from '@example/types' 5 | 6 | export const docService = { 7 | addDocument(params: ICreateDocumentParams, userId: string) { 8 | const { title, doc, visibility } = params 9 | const dbDoc = docDb.add(title, doc, userId, visibility) 10 | // TODO create in collab mode if already enabled 11 | collabDb.startEditing(userId, dbDoc.id, visibility) 12 | return dbDoc 13 | }, 14 | getDocument(id: string, userId: string) { 15 | const doc = docDb.get(id) 16 | if (doc && doc.userId === userId) { 17 | return doc 18 | } 19 | return false 20 | }, 21 | getDocuments() { 22 | return docDb.getAll() 23 | }, 24 | updateDocument(documentId: string, data: Partial, userId: string) { 25 | if (!collabDb.isUserOwner(userId, documentId)) { 26 | return false 27 | } 28 | docDb.update(documentId, data) 29 | return true 30 | }, 31 | deleteDocument(documentId: string, userId: string) { 32 | if (!collabDb.isUserOwner(userId, documentId)) { 33 | return false 34 | } 35 | docDb.delete(documentId) 36 | return true 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/quick-insert/assets/emoji.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IconProps } from '../types' 3 | 4 | export default function IconEmoji({ label = '' }: IconProps) { 5 | return ( 6 | 7 | 8 | 9 | 13 | 17 | 21 | 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/full-v2/src/context/collab/replaceDocument.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, Selection } from 'prosemirror-state' 2 | import { Node as PMNode } from 'prosemirror-model' 3 | 4 | import { Command } from '@core' 5 | 6 | export const replaceDocument = 7 | (doc: any, version: number): Command => 8 | (state, dispatch): boolean => { 9 | const { schema, tr } = state 10 | 11 | const content: PMNode[] = (doc.content || []).map((child: any) => schema.nodeFromJSON(child)) 12 | const hasContent = !!content.length 13 | 14 | if (hasContent) { 15 | tr.setMeta('addToHistory', false) 16 | tr.replaceWith(0, state.doc.nodeSize - 2, content!) 17 | tr.setSelection(Selection.atStart(tr.doc)) 18 | tr.setMeta('replaceDocument', true) 19 | 20 | if (version) { 21 | const collabState = { version, unconfirmed: [] } 22 | tr.setMeta('collab$', collabState) 23 | } 24 | } 25 | 26 | dispatch!(tr) 27 | return true 28 | } 29 | 30 | export const setCollab = 31 | (version: number): Command => 32 | (state, dispatch): boolean => { 33 | const collabState = { version, unconfirmed: [] } 34 | if (dispatch) { 35 | const tr = state.tr.setMeta('collab$', collabState) 36 | dispatch(tr) 37 | } 38 | return false 39 | } 40 | -------------------------------------------------------------------------------- /packages/full/src/editor-plugins/blockquote/nodeviews/BlockQuoteView.ts: -------------------------------------------------------------------------------- 1 | import { NodeView, EditorView, Decoration, DecorationSource } from 'prosemirror-view' 2 | import { Node as PMNode } from 'prosemirror-model' 3 | 4 | import { ReactNodeView } from '../../../react/ReactNodeView' 5 | 6 | import { BlockQuote } from '../ui/BlockQuote' 7 | 8 | import { BlockQuoteOptions, IViewProps, IBlockQuoteAttrs } from '..' 9 | import { EditorContext } from '../../../core/EditorContext' 10 | 11 | export class BlockQuoteView extends ReactNodeView { 12 | createContentDOM() { 13 | const contentDOM = document.createElement('div') 14 | contentDOM.classList.add(`${this.node.type.name}__content-dom`) 15 | return contentDOM 16 | } 17 | } 18 | 19 | export function blockQuoteNodeView(ctx: EditorContext, options?: BlockQuoteOptions) { 20 | return ( 21 | node: PMNode, 22 | view: EditorView, 23 | getPos: () => number, 24 | decorations: readonly Decoration[], 25 | innerDecorations: DecorationSource 26 | ): NodeView => 27 | new BlockQuoteView( 28 | node, 29 | view, 30 | getPos, 31 | decorations, 32 | innerDecorations, 33 | ctx, 34 | { 35 | options, 36 | }, 37 | BlockQuote 38 | ).init() 39 | } 40 | -------------------------------------------------------------------------------- /packages/client-cra/src/components/CollabInfo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | 4 | interface IProps { 5 | className?: string 6 | } 7 | 8 | export function CollabInfo(props: IProps) { 9 | const { className } = props 10 | return ( 11 | 12 | Collaboration 13 |

14 | The full editor implements a simplistic collaboration. Clicking the 'sync' icon should start 15 | a bidirectional connection to the collab-server to sync docs with the database. By default 16 | only one user can edit document at a time and the document should appear locked to other 17 | users. 18 |

19 |

20 | Once 'collab' icon is clicked, a collaboration editing session is initiated. Disabling it{' '} 21 | should lock it for the other users. However, the implementation is still pending.. 22 |

23 |

24 | NOTE: the example collab-server works only locally. I might deploy it to Heroku at some 25 | point. 26 |

27 |
28 | ) 29 | } 30 | 31 | const Container = styled.details` 32 | & > summary { 33 | cursor: pointer; 34 | font-size: 1.25rem; 35 | font-weight: 600; 36 | } 37 | ` 38 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/pm-utils/getActive.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, Selection } from 'prosemirror-state' 2 | 3 | // From https://github.com/PierBover/prosemirror-cookbook 4 | export function getActiveMarks(state: EditorState): string[] { 5 | const isEmpty = state.selection.empty 6 | if (isEmpty) { 7 | const $from = state.selection.$from 8 | const storedMarks = state.storedMarks 9 | 10 | // Return either the stored marks, or the marks at the cursor position. 11 | // Stored marks are the marks that are going to be applied to the next input 12 | // if you dispatched a mark toggle with an empty cursor. 13 | if (storedMarks) { 14 | return storedMarks.map((mark) => mark.type.name) 15 | } else { 16 | return $from.marks().map((mark) => mark.type.name) 17 | } 18 | } else { 19 | const $head = state.selection.$head 20 | const $anchor = state.selection.$anchor 21 | 22 | // We're using a Set to not get duplicate values 23 | const activeMarks = new Set() 24 | 25 | // Here we're getting the marks at the head and anchor of the selection 26 | $head.marks().forEach((mark) => activeMarks.add(mark.type.name)) 27 | $anchor.marks().forEach((mark) => activeMarks.add(mark.type.name)) 28 | 29 | return Array.from(activeMarks) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/full/src/editor-plugins/base/pm-utils/getActive.ts: -------------------------------------------------------------------------------- 1 | import { EditorState, Selection } from 'prosemirror-state' 2 | 3 | // From https://github.com/PierBover/prosemirror-cookbook 4 | export function getActiveMarks(state: EditorState): string[] { 5 | const isEmpty = state.selection.empty 6 | if (isEmpty) { 7 | const $from = state.selection.$from 8 | const storedMarks = state.storedMarks 9 | 10 | // Return either the stored marks, or the marks at the cursor position. 11 | // Stored marks are the marks that are going to be applied to the next input 12 | // if you dispatched a mark toggle with an empty cursor. 13 | if (storedMarks) { 14 | return storedMarks.map((mark) => mark.type.name) 15 | } else { 16 | return $from.marks().map((mark) => mark.type.name) 17 | } 18 | } else { 19 | const $head = state.selection.$head 20 | const $anchor = state.selection.$anchor 21 | 22 | // We're using a Set to not get duplicate values 23 | const activeMarks = new Set() 24 | 25 | // Here we're getting the marks at the head and anchor of the selection 26 | $head.marks().forEach((mark) => activeMarks.add(mark.type.name)) 27 | $anchor.marks().forEach((mark) => activeMarks.add(mark.type.name)) 28 | 29 | return Array.from(activeMarks) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/nextjs/src/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | 4 | import styled from 'styled-components' 5 | 6 | interface IProps { 7 | className?: string 8 | } 9 | 10 | export function NavBar(props: IProps) { 11 | const { className } = props 12 | return ( 13 | 14 | 25 | 26 | ) 27 | } 28 | 29 | const Container = styled.div` 30 | background: #551a8b; 31 | box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.18); 32 | padding: 1rem; 33 | ` 34 | const Nav = styled.nav` 35 | align-items: center; 36 | display: flex; 37 | ` 38 | const StyledLink = styled.a` 39 | box-sizing: border-box; 40 | color: #fff; 41 | cursor: pointer; 42 | font-size: 1rem; 43 | padding: 0.5rem 1rem; 44 | text-decoration: none; 45 | transition: 0.2s hover; 46 | &:hover { 47 | text-decoration: underline; 48 | } 49 | &.current { 50 | font-weight: 600; 51 | } 52 | ` 53 | -------------------------------------------------------------------------------- /packages/full/src/schema/marks/groups.ts: -------------------------------------------------------------------------------- 1 | // # What do marks exist? 2 | // 3 | // Marks are categorised into different groups. One motivation for this was to allow the `code` mark 4 | // to exclude other marks, without needing to explicitly name them. Explicit naming requires the 5 | // named mark to exist in the schema. This is undesirable because we want to construct different 6 | // schemas that have different sets of nodes/marks. 7 | // 8 | // Groups provide a level of indirection, and solve this problem. For the immediate use-case, one 9 | // group called "not_code" would have sufficed, but this an ultra-specialised to code. 10 | 11 | // Mark group for font styling (e.g. bold, italic, underline, superscript). 12 | export const FONT_STYLE = 'fontStyle' 13 | 14 | // Marks group for search queries. 15 | export const SEARCH_QUERY = 'searchQuery' 16 | 17 | // Marks group for links. 18 | export const LINK = 'link' 19 | 20 | // Marks group for colors (text-color, background-color, etc). 21 | export const COLOR = 'color' 22 | 23 | // They need to be on their own group so that they can exclude each other 24 | // and also work when one of them is disabled. 25 | 26 | // Marks group for alignment. 27 | export const ALIGNMENT = 'alignment' 28 | 29 | // Marks group for indentation. 30 | export const INDENTATION = 'indentation' 31 | -------------------------------------------------------------------------------- /packages/nextjs/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { DocumentContext, Html, Head, Main, NextScript } from 'next/document' 2 | import { ServerStyleSheet } from 'styled-components' 3 | 4 | export default class MyDocument extends Document { 5 | static async getInitialProps(ctx: DocumentContext) { 6 | const sheet = new ServerStyleSheet() 7 | const originalRenderPage = ctx.renderPage 8 | 9 | try { 10 | ctx.renderPage = () => 11 | originalRenderPage({ 12 | enhanceApp: (App) => (props) => sheet.collectStyles(), 13 | }) 14 | 15 | const initialProps = await Document.getInitialProps(ctx) 16 | return { 17 | ...initialProps, 18 | styles: ( 19 | <> 20 | {initialProps.styles} 21 | {sheet.getStyleElement()} 22 | 23 | ), 24 | } 25 | } finally { 26 | sheet.seal() 27 | } 28 | } 29 | render() { 30 | return ( 31 | 32 | 33 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/atlassian/src/schema/marks/groups.ts: -------------------------------------------------------------------------------- 1 | // # What do marks exist? 2 | // 3 | // Marks are categorised into different groups. One motivation for this was to allow the `code` mark 4 | // to exclude other marks, without needing to explicitly name them. Explicit naming requires the 5 | // named mark to exist in the schema. This is undesirable because we want to construct different 6 | // schemas that have different sets of nodes/marks. 7 | // 8 | // Groups provide a level of indirection, and solve this problem. For the immediate use-case, one 9 | // group called "not_code" would have sufficed, but this an ultra-specialised to code. 10 | 11 | // Mark group for font styling (e.g. bold, italic, underline, superscript). 12 | export const FONT_STYLE = 'fontStyle' 13 | 14 | // Marks group for search queries. 15 | export const SEARCH_QUERY = 'searchQuery' 16 | 17 | // Marks group for links. 18 | export const LINK = 'link' 19 | 20 | // Marks group for colors (text-color, background-color, etc). 21 | export const COLOR = 'color' 22 | 23 | // They need to be on their own group so that they can exclude each other 24 | // and also work when one of them is disabled. 25 | 26 | // Marks group for alignment. 27 | export const ALIGNMENT = 'alignment' 28 | 29 | // Marks group for indentation. 30 | export const INDENTATION = 'indentation' 31 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/marks/groups.ts: -------------------------------------------------------------------------------- 1 | // # What do marks exist? 2 | // 3 | // Marks are categorised into different groups. One motivation for this was to allow the `code` mark 4 | // to exclude other marks, without needing to explicitly name them. Explicit naming requires the 5 | // named mark to exist in the schema. This is undesirable because we want to construct different 6 | // schemas that have different sets of nodes/marks. 7 | // 8 | // Groups provide a level of indirection, and solve this problem. For the immediate use-case, one 9 | // group called "not_code" would have sufficed, but this an ultra-specialised to code. 10 | 11 | // Mark group for font styling (e.g. bold, italic, underline, superscript). 12 | export const FONT_STYLE = 'fontStyle' 13 | 14 | // Marks group for search queries. 15 | export const SEARCH_QUERY = 'searchQuery' 16 | 17 | // Marks group for links. 18 | export const LINK = 'link' 19 | 20 | // Marks group for colors (text-color, background-color, etc). 21 | export const COLOR = 'color' 22 | 23 | // They need to be on their own group so that they can exclude each other 24 | // and also work when one of them is disabled. 25 | 26 | // Marks group for alignment. 27 | export const ALIGNMENT = 'alignment' 28 | 29 | // Marks group for indentation. 30 | export const INDENTATION = 'indentation' 31 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/index.ts: -------------------------------------------------------------------------------- 1 | import { createReactExtension } from './createReactExtension' 2 | 3 | import { BaseExtension, baseSchema } from './base' 4 | import type { BaseExtensionProps } from './base' 5 | import { BlockQuoteExtension, blockQuoteSchema } from './blockquote' 6 | import type { BlockQuoteExtensionProps } from './blockquote' 7 | import { CollabExtension } from './collab' 8 | import type { CollabExtensionProps } from './collab' 9 | 10 | export const Base = createReactExtension(BaseExtension) 11 | export const BlockQuote = createReactExtension(BlockQuoteExtension) 12 | export const Collab = createReactExtension(CollabExtension) 13 | 14 | export { BaseExtension } from './base' 15 | export type { BaseState } from './base' 16 | export { BlockQuoteExtension } from './blockquote' 17 | export type { BlockQuoteState } from './blockquote' 18 | export { CollabExtension } from './collab' 19 | export type { CollabState } from './collab' 20 | 21 | export { Extension } from './Extension' 22 | 23 | import { createSchemaFromSpecs } from './createSchema' 24 | export const createDefaultSchema = () => createSchemaFromSpecs([baseSchema, blockQuoteSchema]) 25 | export { createSchema } from './createSchema' 26 | export { createPlugins } from './createPlugins' 27 | -------------------------------------------------------------------------------- /packages/atlassian/src/types/editor-ui.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditorView } from 'prosemirror-view' 3 | import { EditorActions } from '../EditorActions' 4 | import { EventDispatcher } from '../utils/event-dispatcher' 5 | import { ProviderFactory } from '../provider-factory' 6 | 7 | export type EditorAppearance = 'full-page' 8 | 9 | export enum ToolbarSize { 10 | XXL = 6, 11 | XL = 5, 12 | L = 4, 13 | M = 3, 14 | S = 2, 15 | XXXS = 1, 16 | } 17 | 18 | export type ToolbarUiComponentFactoryParams = UiComponentFactoryParams & { 19 | toolbarSize: ToolbarSize 20 | isToolbarReducedSpacing: boolean 21 | isLastItem?: boolean 22 | } 23 | export type ToolbarUIComponentFactory = ( 24 | params: ToolbarUiComponentFactoryParams 25 | ) => React.ReactElement | null 26 | 27 | export type UiComponentFactoryParams = { 28 | editorView: EditorView 29 | editorActions: EditorActions 30 | eventDispatcher: EventDispatcher 31 | providerFactory: ProviderFactory 32 | appearance: EditorAppearance 33 | popupsMountPoint?: HTMLElement 34 | popupsBoundariesElement?: HTMLElement 35 | popupsScrollableElement?: HTMLElement 36 | containerElement: HTMLElement | null 37 | disabled: boolean 38 | } 39 | export type UIComponentFactory = ( 40 | params: UiComponentFactoryParams 41 | ) => React.ReactElement | null 42 | -------------------------------------------------------------------------------- /packages/atlassian/src/utils/selection.ts: -------------------------------------------------------------------------------- 1 | import { EditorState } from 'prosemirror-state' 2 | import { MarkType, Node, ResolvedPos } from 'prosemirror-model' 3 | import { browser } from './browser' 4 | 5 | export const normaliseNestedLayout = (state: EditorState, node: Node) => { 6 | if (state.selection.$from.depth > 1) { 7 | if (node.attrs.layout && node.attrs.layout !== 'default') { 8 | return node.type.createChecked( 9 | { 10 | ...node.attrs, 11 | layout: 'default', 12 | }, 13 | node.content, 14 | node.marks 15 | ) 16 | } 17 | 18 | // If its a breakout layout, we can remove the mark 19 | // Since default isn't a valid breakout mode. 20 | const breakoutMark: MarkType = state.schema.marks.breakout 21 | if (breakoutMark && breakoutMark.isInSet(node.marks)) { 22 | const newMarks = breakoutMark.removeFromSet(node.marks) 23 | return node.type.createChecked(node.attrs, node.content, newMarks) 24 | } 25 | } 26 | 27 | return node 28 | } 29 | 30 | // @see: https://github.com/ProseMirror/prosemirror/issues/710 31 | // @see: https://bugs.chromium.org/p/chromium/issues/detail?id=740085 32 | // Chrome >= 58 (desktop only) 33 | export const isChromeWithSelectionBug = 34 | browser.chrome && !browser.android && browser.chrome_version >= 58 35 | -------------------------------------------------------------------------------- /packages/atlassian/src/plugins/base/index.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'prosemirror-state' 2 | 3 | import { history } from 'prosemirror-history' 4 | import { keymap } from 'prosemirror-keymap' 5 | import { baseKeymap } from 'prosemirror-commands' 6 | 7 | import { doc, paragraph, text } from '../../schema/nodes' 8 | 9 | import { createParagraphNear, splitBlock } from './commands/general' 10 | 11 | import { EditorPlugin, PMPluginFactory } from '../../types' 12 | // import { keymap } from '../../utils/keymap'; 13 | 14 | export interface BasePluginOptions {} 15 | 16 | export const basePlugin = (options?: BasePluginOptions): EditorPlugin => ({ 17 | name: 'base', 18 | 19 | pmPlugins() { 20 | const plugins: { name: string; plugin: PMPluginFactory }[] = [ 21 | { name: 'history', plugin: () => history() }, 22 | { name: 'baseKeyMap', plugin: () => keymap(baseKeymap) }, 23 | { 24 | name: 'otherKeyMap', 25 | plugin: () => 26 | keymap({ 27 | 'Ctrl-Alt-p': createParagraphNear, 28 | 'Ctrl-Alt-s': splitBlock, 29 | }), 30 | }, 31 | ] 32 | 33 | return plugins 34 | }, 35 | nodes() { 36 | return [ 37 | { name: 'doc', node: doc }, 38 | { name: 'paragraph', node: paragraph }, 39 | { name: 'text', node: text }, 40 | ] 41 | }, 42 | }) 43 | -------------------------------------------------------------------------------- /packages/full-v2/src/extensions/base/pm-plugins/main.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'prosemirror-state' 2 | 3 | import { EditorContext } from '@context' 4 | 5 | import { getActiveMarks } from '../pm-utils/getActive' 6 | 7 | import { BaseState, basePluginKey } from './state' 8 | 9 | export function basePluginFactory(ctx: EditorContext, options?: {}) { 10 | const { pluginsProvider } = ctx 11 | return new Plugin({ 12 | state: { 13 | init(_, state): BaseState { 14 | return { 15 | activeNodes: [], 16 | activeMarks: [], 17 | } 18 | }, 19 | apply(tr, pluginState: BaseState, _oldState, newState): BaseState { 20 | if (tr.docChanged || tr.selectionSet) { 21 | const marks = getActiveMarks(newState) 22 | const eqMarks = 23 | marks.every((m) => pluginState.activeMarks.includes(m)) && 24 | marks.length === pluginState.activeMarks.length 25 | if (!eqMarks) { 26 | const nextPluginState = { 27 | ...pluginState, 28 | activeMarks: marks, 29 | } 30 | pluginsProvider.publish(basePluginKey, nextPluginState) 31 | return nextPluginState 32 | } 33 | } 34 | 35 | return pluginState 36 | }, 37 | }, 38 | key: basePluginKey, 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /packages/full/src/editor-plugins/blockquote/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EditorState } from 'prosemirror-state' 3 | 4 | import { blockquote } from '../../schema/nodes' 5 | import { blockQuotePluginFactory, blockquotePluginKey } from './pm-plugins/main' 6 | import { keymapPlugin } from './pm-plugins/keymap' 7 | import * as keymaps from '../../core/keymaps' 8 | 9 | import { NodeViewProps } from '../../react/ReactNodeView' 10 | import { EditorPlugin, PMPluginFactory } from '../../core/types' 11 | 12 | export interface BlockQuoteOptions {} 13 | export interface IViewProps { 14 | options?: BlockQuoteOptions 15 | } 16 | export type UIProps = NodeViewProps 17 | export interface IBlockQuoteAttrs { 18 | size: number 19 | } 20 | 21 | export const blockQuotePlugin = (options: BlockQuoteOptions = {}): EditorPlugin => ({ 22 | name: 'blockquote', 23 | 24 | nodes() { 25 | return [{ name: 'blockquote', node: blockquote }] 26 | }, 27 | 28 | pmPlugins() { 29 | const plugins: { name: string; plugin: PMPluginFactory }[] = [ 30 | { 31 | name: 'blockquote', 32 | plugin: ({ ctx }) => blockQuotePluginFactory(ctx, options), 33 | }, 34 | { name: 'blockquoteKeyMap', plugin: () => keymapPlugin() }, 35 | ] 36 | return plugins 37 | }, 38 | 39 | pluginsOptions: {}, 40 | }) 41 | -------------------------------------------------------------------------------- /packages/full/src/utils/document.ts: -------------------------------------------------------------------------------- 1 | import { Node as PMNode, Schema } from 'prosemirror-model' 2 | 3 | export function parseRawValue(value: Object | string, schema: Schema) { 4 | let parsedNode 5 | if (typeof value === 'string') { 6 | try { 7 | parsedNode = JSON.parse(value) 8 | } catch (err) { 9 | // eslint-disable-next-line no-console 10 | console.error(`Error processing value: ${value} isn't a valid JSON`) 11 | return 12 | } 13 | } else { 14 | parsedNode = value 15 | } 16 | 17 | try { 18 | // ProseMirror always require a child under doc 19 | if (parsedNode.type === 'doc') { 20 | if (Array.isArray(parsedNode.content) && parsedNode.content.length === 0) { 21 | parsedNode.content.push({ 22 | type: 'paragraph', 23 | content: [], 24 | }) 25 | } 26 | // Just making sure doc is always valid 27 | if (!parsedNode.version) { 28 | parsedNode.version = 1 29 | } 30 | } 31 | 32 | const parsedDoc = PMNode.fromJSON(schema, parsedNode) 33 | 34 | // throws an error if the document is invalid 35 | parsedDoc.check() 36 | 37 | return parsedDoc 38 | } catch (err: any) { 39 | // eslint-disable-next-line no-console 40 | console.error(`Error processing document:\n${err.message}\n\n`, JSON.stringify(parsedNode)) 41 | return 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/ssr/client/routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { BrowserRouter, Navigate, Route, Routes as RouterRoutes } from 'react-router-dom' 3 | import { StaticRouter } from 'react-router-dom/server' 4 | 5 | import { DefaultLayout } from './components/Layout' 6 | 7 | import { FrontPage } from './pages/FrontPage' 8 | import { MinimalPage } from './pages/MinimalPage' 9 | import { AtlassianPage } from './pages/AtlassianPage' 10 | 11 | const Routes = () => ( 12 | 13 | 17 | 18 | 19 | } 20 | /> 21 | 25 | 26 | 27 | } 28 | /> 29 | 33 | 34 | 35 | } 36 | /> 37 | } /> 38 | 39 | ) 40 | 41 | export const ClientRoutes = () => ( 42 | 43 | 44 | 45 | ) 46 | 47 | export const ServerRoutes = (props: { url: string }) => ( 48 | 49 | 50 | 51 | ) 52 | -------------------------------------------------------------------------------- /packages/full-v2/src/core/utils/document.ts: -------------------------------------------------------------------------------- 1 | import { Node as PMNode, Schema } from 'prosemirror-model' 2 | 3 | export function parseRawValue(value: Object | string, schema: Schema) { 4 | let parsedNode 5 | if (typeof value === 'string') { 6 | try { 7 | parsedNode = JSON.parse(value) 8 | } catch (err) { 9 | // eslint-disable-next-line no-console 10 | console.error(`Error processing value: ${value} isn't a valid JSON`) 11 | return 12 | } 13 | } else { 14 | parsedNode = value 15 | } 16 | 17 | try { 18 | // ProseMirror always require a child under doc 19 | if (parsedNode.type === 'doc') { 20 | if (Array.isArray(parsedNode.content) && parsedNode.content.length === 0) { 21 | parsedNode.content.push({ 22 | type: 'paragraph', 23 | content: [], 24 | }) 25 | } 26 | // Just making sure doc is always valid 27 | if (!parsedNode.version) { 28 | parsedNode.version = 1 29 | } 30 | } 31 | 32 | const parsedDoc = PMNode.fromJSON(schema, parsedNode) 33 | 34 | // throws an error if the document is invalid 35 | parsedDoc.check() 36 | 37 | return parsedDoc 38 | } catch (err: any) { 39 | // eslint-disable-next-line no-console 40 | console.error(`Error processing document:\n${err?.message}\n\n`, JSON.stringify(parsedNode)) 41 | return 42 | } 43 | } 44 | --------------------------------------------------------------------------------