;
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------