rename package folders

This commit is contained in:
Philipp Kühn
2020-03-30 10:42:59 +02:00
parent 18c5164af9
commit 14421a11fa
35 changed files with 2 additions and 2 deletions

6
packages/core/index.ts Normal file
View File

@@ -0,0 +1,6 @@
import { Editor } from './src/Editor'
export default Editor
export { Editor }
export { default as Extension } from './src/Extension'
export { default as Node } from './src/Node'

View File

@@ -0,0 +1,29 @@
{
"name": "@tiptap/core",
"version": "2.0.0",
"source": "index.ts",
"main": "dist/tiptap-core.js",
"umd:main": "dist/tiptap-core.umd.js",
"module": "dist/tiptap-core.mjs",
"unpkg": "dist/tiptap-core.js",
"jsdelivr": "dist/tiptap-core.js",
"files": [
"src",
"dist"
],
"dependencies": {
"@types/prosemirror-dropcursor": "^1.0.0",
"@types/prosemirror-gapcursor": "^1.0.1",
"collect.js": "^4.20.3",
"events": "^3.1.0",
"prosemirror-commands": "^1.1.3",
"prosemirror-dropcursor": "^1.3.2",
"prosemirror-gapcursor": "^1.1.4",
"prosemirror-inputrules": "^1.1.2",
"prosemirror-keymap": "^1.1.3",
"prosemirror-model": "^1.9.1",
"prosemirror-state": "^1.3.3",
"prosemirror-utils": "^0.9.6",
"prosemirror-view": "^1.14.6"
}
}

221
packages/core/src/Editor.ts Normal file
View File

@@ -0,0 +1,221 @@
import { EventEmitter } from 'events'
import { EditorState, TextSelection } from 'prosemirror-state'
import { EditorView} from 'prosemirror-view'
import { Schema, DOMParser, DOMSerializer } from 'prosemirror-model'
import { inputRules, undoInputRule } from 'prosemirror-inputrules'
import { keymap } from 'prosemirror-keymap'
import { baseKeymap } from 'prosemirror-commands'
import { dropCursor } from 'prosemirror-dropcursor'
import { gapCursor } from 'prosemirror-gapcursor'
import magicMethods from './utils/magicMethods'
import elementFromString from './utils/elementFromString'
import injectCSS from './utils/injectCSS'
import getAllMethodNames from './utils/getAllMethodNames'
import ExtensionManager from './ExtensionManager'
import Extension from './Extension'
import Node from './Node'
type EditorContent = string | JSON | null
type Command = (next: Function, editor: Editor, ...args: any) => any
interface Options {
content: EditorContent
extensions: (Extension | Node)[]
injectCSS: Boolean
}
@magicMethods
export class Editor extends EventEmitter {
proxy!: any
element = document.createElement('div')
extensionManager!: ExtensionManager
schema!: Schema
view!: EditorView
options: Options = {
content: '',
injectCSS: true,
extensions: [],
}
commands: { [key: string]: any } = {}
private lastCommand = Promise.resolve()
public selection = { from: 0, to: 0 }
constructor(options: Options) {
super()
this.options = { ...this.options, ...options }
this.createExtensionManager()
this.createSchema()
this.createView()
this.registerCommand('focus', require('./commands/focus').default)
this.registerCommand('insertText', require('./commands/insertText').default)
this.registerCommand('insertHTML', require('./commands/insertHTML').default)
this.registerCommand('setContent', require('./commands/setContent').default)
this.registerCommand('clearContent', require('./commands/clearContent').default)
if (this.options.injectCSS) {
injectCSS(require('./style.css'))
}
}
__get(name: string) {
const command = this.commands[name]
if (!command) {
throw new Error(`tiptap: command '${name}' not found.`)
}
return (...args: any) => command(...args)
}
public get state() {
return this.view.state
}
public registerCommand(name: string, callback: Command): Editor {
if (this.commands[name]) {
throw new Error(`tiptap: command '${name}' is already defined.`)
}
if (getAllMethodNames(this).includes(name)) {
throw new Error(`tiptap: '${name}' is a protected name.`)
}
this.commands[name] = this.chainCommand((...args: any) => {
return new Promise(resolve => callback(resolve, this.proxy, ...args))
})
return this.proxy
}
public command(name: string, ...args: any) {
return this.commands[name](...args)
}
private createExtensionManager() {
this.extensionManager = new ExtensionManager(this.options.extensions, this)
}
private createSchema() {
this.schema = new Schema({
topNode: this.extensionManager.topNode,
nodes: this.extensionManager.nodes,
marks: this.extensionManager.marks,
})
}
private get plugins() {
return [
...this.extensionManager.plugins,
keymap({ Backspace: undoInputRule }),
keymap(baseKeymap),
dropCursor(),
gapCursor(),
]
}
private createView() {
this.view = new EditorView(this.element, {
state: EditorState.create({
doc: this.createDocument(this.options.content),
plugins: this.plugins,
}),
dispatchTransaction: this.dispatchTransaction.bind(this),
})
}
private chainCommand = (method: Function) => (...args: any) => {
this.lastCommand = this.lastCommand
.then(() => method.apply(this, args))
.catch(console.error)
return this.proxy
}
public createDocument = (content: EditorContent, parseOptions: any = {}): any => {
if (content && typeof content === 'object') {
try {
return this.schema.nodeFromJSON(content)
} catch (error) {
console.warn('[tiptap warn]: Invalid content.', 'Passed value:', content, 'Error:', error)
return this.createDocument('')
}
}
if (typeof content === 'string') {
return DOMParser
.fromSchema(this.schema)
.parse(elementFromString(content), parseOptions)
}
return this.createDocument('')
}
private storeSelection() {
const { from, to } = this.state.selection
this.selection = { from, to }
}
private dispatchTransaction(transaction: any): void {
const state = this.state.apply(transaction)
this.view.updateState(state)
this.storeSelection()
this.setActiveNodesAndMarks()
this.emit('transaction', { transaction })
if (!transaction.docChanged || transaction.getMeta('preventUpdate')) {
return
}
this.emit('update', { transaction })
}
public setActiveNodesAndMarks() {
// TODO
}
// public setParentComponent(component = null) {
// if (!component) {
// return
// }
// this.view.setProps({
// nodeViews: this.initNodeViews({
// parent: component,
// extensions: [
// ...this.builtInExtensions,
// ...this.options.extensions,
// ],
// }),
// })
// }
public json() {
return this.state.doc.toJSON()
}
public html() {
const div = document.createElement('div')
const fragment = DOMSerializer
.fromSchema(this.schema)
.serializeFragment(this.state.doc.content)
div.appendChild(fragment)
return div.innerHTML
}
public destroy() {
if (!this.view) {
return
}
this.view.destroy()
this.removeAllListeners()
}
}

View File

@@ -0,0 +1,46 @@
import { Editor } from './Editor'
export default abstract class Extension {
constructor(options = {}) {
this.options = {
...this.defaultOptions,
...options,
}
}
editor!: Editor
options: { [key: string]: any } = {}
defaultOptions: { [key: string]: any } = {}
public abstract name: string
public type = 'extension'
public created() {}
public bindEditor(editor: Editor): void {
this.editor = editor
}
update(): any {
return () => {}
}
plugins(): any {
return []
}
inputRules(): any {
return []
}
pasteRules(): any {
return []
}
keys(): any {
return {}
}
}

View File

@@ -0,0 +1,47 @@
import { keymap } from 'prosemirror-keymap'
import collect from 'collect.js'
import { Editor } from './Editor'
import Extension from './Extension'
import Node from './Node'
export default class ExtensionManager {
extensions: (Extension | Node)[]
constructor(extensions: (Extension | Node)[], editor: Editor) {
this.extensions = extensions
this.extensions.forEach(extension => {
extension.bindEditor(editor)
extension.created()
})
}
get topNode() {
const topNode = collect(this.extensions).firstWhere('topNode', true)
if (topNode) {
return topNode.name
}
}
get nodes(): any {
return collect(this.extensions)
.where('type', 'node')
.mapWithKeys((extension: any) => [extension.name, extension.schema()])
.all()
}
get marks(): any {
return collect(this.extensions)
.where('type', 'mark')
.mapWithKeys((extension: any) => [extension.name, extension.schema()])
.all()
}
get plugins(): any {
return collect(this.extensions)
.flatMap(extension => extension.plugins())
.toArray()
}
}

17
packages/core/src/Node.ts Normal file
View File

@@ -0,0 +1,17 @@
import Extension from './Extension'
export default abstract class Node extends Extension {
constructor(options = {}) {
super(options)
}
public type = 'node'
public topNode = false
schema(): any {
return null
}
}

View File

@@ -0,0 +1,13 @@
import { Editor } from '../Editor'
import { TextSelection } from 'prosemirror-state'
declare module '../Editor' {
interface Editor {
clearContent(emitUpdate?: Boolean): Editor,
}
}
export default function clearContent(next: Function, editor: Editor, emitUpdate = false): void {
editor.setContent('', emitUpdate)
next()
}

View File

@@ -0,0 +1,65 @@
import { Editor } from '../Editor'
import { TextSelection } from 'prosemirror-state'
import sleep from '../utils/sleep'
import minMax from '../utils/minMax'
declare module '../Editor' {
interface Editor {
focus(position: Position): Editor
}
}
interface ResolvedSelection {
from: number,
to: number,
}
type Position = 'start' | 'end' | number | null
function resolveSelection(editor: Editor, position: Position = null): ResolvedSelection {
if (position === null) {
return editor.selection
}
if (position === 'start') {
return {
from: 0,
to: 0,
}
}
if (position === 'end') {
const { size } = editor.state.doc.content
return {
from: size,
to: size - 1, // TODO: -1 only for nodes with content
}
}
return {
from: position as number,
to: position as number,
}
}
export default async function focus(next: Function, editor: Editor, position: Position = null): Promise<void> {
const { view, state } = editor
if ((view.hasFocus() && position === null)) {
next()
return
}
const { from, to } = resolveSelection(editor, position)
const { doc, tr } = state
const resolvedFrom = minMax(from, 0, doc.content.size)
const resolvedEnd = minMax(to, 0, doc.content.size)
const selection = TextSelection.create(doc, resolvedFrom, resolvedEnd)
const transaction = tr.setSelection(selection)
view.dispatch(transaction)
await sleep(10)
view.focus()
next()
}

View File

@@ -0,0 +1,20 @@
import { DOMParser } from 'prosemirror-model'
import { Editor } from '../Editor'
import elementFromString from '../utils/elementFromString'
declare module '../Editor' {
interface Editor {
insertHTML(value: string): Editor,
}
}
export default function insertHTML(next: Function, editor: Editor, value: string): void {
const { view, state } = editor
const { selection } = state
const element = elementFromString(value)
const slice = DOMParser.fromSchema(state.schema).parseSlice(element)
const transaction = state.tr.insert(selection.anchor, slice.content)
view.dispatch(transaction)
next()
}

View File

@@ -0,0 +1,15 @@
import { Editor } from '../Editor'
declare module '../Editor' {
interface Editor {
insertText(value: string): Editor,
}
}
export default function insertText(next: Function, editor: Editor, value: string): void {
const { view, state } = editor
const transaction = state.tr.insertText(value)
view.dispatch(transaction)
next()
}

View File

@@ -0,0 +1,33 @@
import { Editor } from '../Editor'
import { TextSelection } from 'prosemirror-state'
declare module '../Editor' {
interface Editor {
setContent(content: string, emitUpdate?: Boolean, parseOptions?: any): Editor,
}
}
export default function setContent(
next: Function,
editor: Editor,
content = null,
emitUpdate = false,
parseOptions = {},
): void {
if (content === null) {
next()
return
}
const { view, state, createDocument } = editor
const { doc, tr } = state
const document = createDocument(content, parseOptions)
const selection = TextSelection.create(doc, 0, doc.content.size)
const transaction = tr
.setSelection(selection)
.replaceSelectionWith(document, false)
.setMeta('preventUpdate', !emitUpdate)
view.dispatch(transaction)
next()
}

View File

@@ -0,0 +1,52 @@
.ProseMirror {
position: relative;
}
.ProseMirror {
word-wrap: break-word;
white-space: pre-wrap;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
}
.ProseMirror pre {
white-space: pre-wrap;
}
.ProseMirror-gapcursor {
display: none;
pointer-events: none;
position: absolute;
}
.ProseMirror-gapcursor:after {
content: "";
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid black;
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to {
visibility: hidden;
}
}
.ProseMirror-hideselection *::selection {
background: transparent;
}
.ProseMirror-hideselection *::-moz-selection {
background: transparent;
}
.ProseMirror-hideselection * {
caret-color: transparent;
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}

View File

@@ -0,0 +1,6 @@
export default function elementFromString(value: string): HTMLDivElement {
const element = document.createElement('div')
element.innerHTML = value.trim()
return element
}

View File

@@ -0,0 +1,10 @@
export default function getAllMethodNames(obj: Object) {
let methods = new Set()
while (obj = Reflect.getPrototypeOf(obj)) {
let keys = Reflect.ownKeys(obj)
keys.forEach((k) => methods.add(k))
}
return Array.from(methods)
}

View File

@@ -0,0 +1,19 @@
import { EditorState } from 'prosemirror-state'
import { Mark, MarkType } from 'prosemirror-model'
export default function getMarkAttrs(state: EditorState, type: MarkType) {
const { from, to } = state.selection
let marks: Mark[] = []
state.doc.nodesBetween(from, to, node => {
marks = [...marks, ...node.marks]
})
const mark = marks.find(markItem => markItem.type.name === type.name)
if (mark) {
return mark.attrs
}
return {}
}

View File

@@ -0,0 +1,15 @@
export default function injectCSS(css: string) {
if (process.env.NODE_ENV !== 'test') {
const style = document.createElement('style')
style.type = 'text/css'
style.textContent = css
const { head } = document
const { firstChild } = head
if (firstChild) {
head.insertBefore(style, firstChild)
} else {
head.appendChild(style)
}
}
}

View File

@@ -0,0 +1,31 @@
export default function magicMethods(clazz: any) {
const classHandler = Object.create(null)
classHandler.construct = (target: any, args: any) => {
const instance = new clazz(...args)
const instanceHandler = Object.create(null)
const get = Object.getOwnPropertyDescriptor(clazz.prototype, '__get')
if (get) {
instanceHandler.get = (target: any, name: any) => {
if (typeof name !== 'string') {
return
}
const exists = name in target || name.startsWith('_')
if (exists) {
return target[name]
} else {
return get.value.call(target, name)
}
}
}
instance.proxy = new Proxy(instance, instanceHandler)
return instance.proxy
}
return new Proxy(clazz, classHandler)
}

View File

@@ -0,0 +1,17 @@
import { EditorState } from 'prosemirror-state'
import { MarkType } from 'prosemirror-model'
export default function markIsActive(state: EditorState, type: MarkType) {
const {
from,
$from,
to,
empty,
} = state.selection
if (empty) {
return !!type.isInSet(state.storedMarks || $from.marks())
}
return !!state.doc.rangeHasMark(from, to, type)
}

View File

@@ -0,0 +1,3 @@
export default function minMax(value: number = 0, min: number = 0, max: number = 0): number {
return Math.min(Math.max(value, min), max)
}

View File

@@ -0,0 +1,15 @@
import { findParentNode, findSelectedNodeOfType } from 'prosemirror-utils'
import { EditorState } from 'prosemirror-state'
import { Node, NodeType } from 'prosemirror-model'
export default function nodeIsActive(state: EditorState, type: NodeType, attrs = {}) {
const predicate = (node: Node) => node.type === type
const node = findSelectedNodeOfType(type)(state.selection)
|| findParentNode(predicate)(state.selection)
if (!Object.keys(attrs).length || !node) {
return !!node
}
return node.node.hasMarkup(type, attrs)
}

View File

@@ -0,0 +1,3 @@
export default function sleep(milliseconds: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}