mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-12-14 18:49:02 +08:00
Merge branch 'feature/collab-plugin'
# Conflicts: # yarn.lock
This commit is contained in:
commit
0ee4560265
@ -56,5 +56,7 @@ module.exports = {
|
|||||||
'class-methods-use-this': 'off',
|
'class-methods-use-this': 'off',
|
||||||
|
|
||||||
'global-require': 'off',
|
'global-require': 'off',
|
||||||
|
|
||||||
|
'func-names': ['error', 'never'],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,5 +4,6 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
'@babel/plugin-syntax-dynamic-import',
|
'@babel/plugin-syntax-dynamic-import',
|
||||||
|
'@babel/plugin-proposal-class-properties',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
123
examples/Components/Routes/Collaboration/index.vue
Normal file
123
examples/Components/Routes/Collaboration/index.vue
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<div class="editor">
|
||||||
|
<div class="message">
|
||||||
|
With the Collaboration Extension it's possible for several users to work on a document at the same time. To make this possible, client-side and server-side code is required. This example shows this using a <a href="https://glitch.com/edit/#!/tiptap-sockets" target="_blank">socket server</a>. Try it out below:
|
||||||
|
</div>
|
||||||
|
<template v-if="editor && !loading">
|
||||||
|
<div class="count">
|
||||||
|
{{ count }} {{ count === 1 ? 'user' : 'users' }} connected
|
||||||
|
</div>
|
||||||
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
|
</template>
|
||||||
|
<em v-else>
|
||||||
|
Connecting to socket server …
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import io from 'socket.io-client'
|
||||||
|
import { Editor, EditorContent } from 'tiptap'
|
||||||
|
import {
|
||||||
|
HardBreak,
|
||||||
|
Heading,
|
||||||
|
Bold,
|
||||||
|
Code,
|
||||||
|
Italic,
|
||||||
|
History,
|
||||||
|
Collaboration,
|
||||||
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
EditorContent,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
editor: null,
|
||||||
|
socket: null,
|
||||||
|
count: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onInit({ doc, version }) {
|
||||||
|
this.loading = false
|
||||||
|
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor = new Editor({
|
||||||
|
content: doc,
|
||||||
|
extensions: [
|
||||||
|
new HardBreak(),
|
||||||
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
|
new Bold(),
|
||||||
|
new Code(),
|
||||||
|
new Italic(),
|
||||||
|
new History(),
|
||||||
|
new Collaboration({
|
||||||
|
// the initial version we start with
|
||||||
|
// version is an integer which is incremented with every change
|
||||||
|
version,
|
||||||
|
// debounce changes so we can save some bandwidth
|
||||||
|
debounce: 250,
|
||||||
|
// onSendable is called whenever there are changed we have to send to our server
|
||||||
|
onSendable: data => {
|
||||||
|
this.socket.emit('update', data)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
setCount(count) {
|
||||||
|
this.count = count
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// server implementation: https://glitch.com/edit/#!/tiptap-sockets
|
||||||
|
this.socket = io('wss://tiptap-sockets.glitch.me')
|
||||||
|
// get the current document and its version
|
||||||
|
.on('init', data => this.onInit(data))
|
||||||
|
// send all updates to the collaboration extension
|
||||||
|
.on('update', data => this.editor.extensions.options.collaboration.update(data))
|
||||||
|
// get count of connected users
|
||||||
|
.on('getCount', count => this.setCount(count))
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.editor.destroy()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "~variables";
|
||||||
|
|
||||||
|
.count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgba($color-black, 0.5);
|
||||||
|
color: #27b127;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
display: inline-flex;
|
||||||
|
background-color: #27b127;
|
||||||
|
width: 0.4rem;
|
||||||
|
height: 0.4rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 0.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -45,6 +45,9 @@
|
|||||||
<router-link class="subnavigation__link" to="/placeholder">
|
<router-link class="subnavigation__link" to="/placeholder">
|
||||||
Placeholder
|
Placeholder
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link class="subnavigation__link" to="/collaboration">
|
||||||
|
Collaboration
|
||||||
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/export">
|
<router-link class="subnavigation__link" to="/export">
|
||||||
Export HTML or JSON
|
Export HTML or JSON
|
||||||
</router-link>
|
</router-link>
|
||||||
|
@ -74,6 +74,15 @@ h3 {
|
|||||||
background-color: rgba($color-black, 0.1);
|
background-color: rgba($color-black, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
background-color: rgba($color-black, 0.05);
|
||||||
|
color: rgba($color-black, 0.7);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
@import "./editor";
|
@import "./editor";
|
||||||
@import "./menubar";
|
@import "./menubar";
|
||||||
@import "./menububble";
|
@import "./menububble";
|
||||||
|
@ -117,6 +117,13 @@ const routes = [
|
|||||||
githubUrl: 'https://github.com/scrumpy/tiptap/tree/master/examples/Components/Routes/Placeholder',
|
githubUrl: 'https://github.com/scrumpy/tiptap/tree/master/examples/Components/Routes/Placeholder',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/collaboration',
|
||||||
|
component: () => import('Components/Routes/Collaboration'),
|
||||||
|
meta: {
|
||||||
|
githubUrl: 'https://github.com/scrumpy/tiptap/tree/master/examples/Components/Routes/Collaboration',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/export',
|
path: '/export',
|
||||||
component: () => import('Components/Routes/Export'),
|
component: () => import('Components/Routes/Export'),
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.4.4",
|
"@babel/core": "^7.4.4",
|
||||||
"@babel/node": "^7.2.2",
|
"@babel/node": "^7.2.2",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.4.4",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.4.4",
|
"@babel/plugin-transform-runtime": "^7.4.4",
|
||||||
"@babel/polyfill": "^7.4.4",
|
"@babel/polyfill": "^7.4.4",
|
||||||
@ -88,5 +89,7 @@
|
|||||||
"webpack-svgstore-plugin": "^4.1.0",
|
"webpack-svgstore-plugin": "^4.1.0",
|
||||||
"zlib": "^1.0.5"
|
"zlib": "^1.0.5"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {
|
||||||
|
"socket.io-client": "^2.2.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,8 +32,8 @@ index = $pos.index(d)
|
|||||||
// this is a copy of splitListItem
|
// this is a copy of splitListItem
|
||||||
// see https://github.com/ProseMirror/prosemirror-schema-list/blob/master/src/schema-list.js
|
// see https://github.com/ProseMirror/prosemirror-schema-list/blob/master/src/schema-list.js
|
||||||
|
|
||||||
export default function splitListItem(itemType) {
|
export default function splitToDefaultListItem(itemType) {
|
||||||
return function _splitListItem(state, dispatch) {
|
return function (state, dispatch) {
|
||||||
const { $from, $to, node } = state.selection
|
const { $from, $to, node } = state.selection
|
||||||
if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) return false
|
if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) return false
|
||||||
const grandParent = $from.node(-1)
|
const grandParent = $from.node(-1)
|
||||||
|
@ -22,9 +22,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lowlight": "^1.11.0",
|
"lowlight": "^1.11.0",
|
||||||
|
"prosemirror-collab": "^1.1.1",
|
||||||
"prosemirror-history": "^1.0.4",
|
"prosemirror-history": "^1.0.4",
|
||||||
"prosemirror-state": "^1.2.2",
|
"prosemirror-state": "^1.2.2",
|
||||||
"prosemirror-tables": "^0.7.11",
|
"prosemirror-tables": "^0.7.11",
|
||||||
|
"prosemirror-transform": "^1.1.3",
|
||||||
"prosemirror-utils": "^0.7.6",
|
"prosemirror-utils": "^0.7.6",
|
||||||
"prosemirror-view": "^1.8.9",
|
"prosemirror-view": "^1.8.9",
|
||||||
"tiptap": "^1.17.0",
|
"tiptap": "^1.17.0",
|
||||||
|
78
packages/tiptap-extensions/src/extensions/Collaboration.js
Normal file
78
packages/tiptap-extensions/src/extensions/Collaboration.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Extension } from 'tiptap'
|
||||||
|
import { Step } from 'prosemirror-transform'
|
||||||
|
import {
|
||||||
|
collab,
|
||||||
|
sendableSteps,
|
||||||
|
getVersion,
|
||||||
|
receiveTransaction,
|
||||||
|
} from 'prosemirror-collab'
|
||||||
|
|
||||||
|
export default class Collaboration extends Extension {
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return 'collaboration'
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.editor.on('update', ({ state }) => {
|
||||||
|
this.getSendableSteps(state)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultOptions() {
|
||||||
|
return {
|
||||||
|
version: 0,
|
||||||
|
clientID: Math.floor(Math.random() * 0xFFFFFFFF),
|
||||||
|
debounce: 250,
|
||||||
|
onSendable: () => {},
|
||||||
|
update: ({ steps, version }) => {
|
||||||
|
const { state, view, schema } = this.editor
|
||||||
|
|
||||||
|
if (getVersion(state) > version) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
view.dispatch(receiveTransaction(
|
||||||
|
state,
|
||||||
|
steps.map(item => Step.fromJSON(schema, item.step)),
|
||||||
|
steps.map(item => item.clientID),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get plugins() {
|
||||||
|
return [
|
||||||
|
collab({
|
||||||
|
version: this.options.version,
|
||||||
|
clientID: this.options.clientID,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
getSendableSteps = this.debounce(state => {
|
||||||
|
const sendable = sendableSteps(state)
|
||||||
|
|
||||||
|
if (sendable) {
|
||||||
|
this.options.onSendable({
|
||||||
|
version: sendable.version,
|
||||||
|
steps: sendable.steps.map(step => step.toJSON()),
|
||||||
|
clientID: sendable.clientID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, this.options.debounce)
|
||||||
|
|
||||||
|
debounce(fn, delay) {
|
||||||
|
let timeout
|
||||||
|
return function (...args) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
fn(...args)
|
||||||
|
timeout = null
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -23,6 +23,7 @@ export { default as Link } from './marks/Link'
|
|||||||
export { default as Strike } from './marks/Strike'
|
export { default as Strike } from './marks/Strike'
|
||||||
export { default as Underline } from './marks/Underline'
|
export { default as Underline } from './marks/Underline'
|
||||||
|
|
||||||
|
export { default as Collaboration } from './extensions/Collaboration'
|
||||||
export { default as History } from './extensions/History'
|
export { default as History } from './extensions/History'
|
||||||
export { default as Placeholder } from './extensions/Placeholder'
|
export { default as Placeholder } from './extensions/Placeholder'
|
||||||
|
|
||||||
|
@ -12,12 +12,16 @@ import { keymap } from 'prosemirror-keymap'
|
|||||||
import { baseKeymap } from 'prosemirror-commands'
|
import { baseKeymap } from 'prosemirror-commands'
|
||||||
import { inputRules, undoInputRule } from 'prosemirror-inputrules'
|
import { inputRules, undoInputRule } from 'prosemirror-inputrules'
|
||||||
import { markIsActive, nodeIsActive, getMarkAttrs } from 'tiptap-utils'
|
import { markIsActive, nodeIsActive, getMarkAttrs } from 'tiptap-utils'
|
||||||
import { ExtensionManager, ComponentView } from './Utils'
|
import {
|
||||||
|
camelCase, Emitter, ExtensionManager, ComponentView,
|
||||||
|
} from './Utils'
|
||||||
import { Doc, Paragraph, Text } from './Nodes'
|
import { Doc, Paragraph, Text } from './Nodes'
|
||||||
|
|
||||||
export default class Editor {
|
export default class Editor extends Emitter {
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
|
super()
|
||||||
|
|
||||||
this.defaultOptions = {
|
this.defaultOptions = {
|
||||||
editorProps: {},
|
editorProps: {},
|
||||||
editable: true,
|
editable: true,
|
||||||
@ -41,6 +45,15 @@ export default class Editor {
|
|||||||
onDrop: () => {},
|
onDrop: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.events = [
|
||||||
|
'init',
|
||||||
|
'update',
|
||||||
|
'focus',
|
||||||
|
'blur',
|
||||||
|
'paste',
|
||||||
|
'drop',
|
||||||
|
]
|
||||||
|
|
||||||
this.init(options)
|
this.init(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +82,11 @@ export default class Editor {
|
|||||||
}, 10)
|
}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.options.onInit({
|
this.events.forEach(name => {
|
||||||
|
this.on(name, this.options[camelCase(`on ${name}`)])
|
||||||
|
})
|
||||||
|
|
||||||
|
this.emit('init', {
|
||||||
view: this.view,
|
view: this.view,
|
||||||
state: this.state,
|
state: this.state,
|
||||||
})
|
})
|
||||||
@ -105,7 +122,7 @@ export default class Editor {
|
|||||||
return new ExtensionManager([
|
return new ExtensionManager([
|
||||||
...this.builtInExtensions,
|
...this.builtInExtensions,
|
||||||
...this.options.extensions,
|
...this.options.extensions,
|
||||||
])
|
], this)
|
||||||
}
|
}
|
||||||
|
|
||||||
createPlugins() {
|
createPlugins() {
|
||||||
@ -217,20 +234,20 @@ export default class Editor {
|
|||||||
createView() {
|
createView() {
|
||||||
const view = new EditorView(this.element, {
|
const view = new EditorView(this.element, {
|
||||||
state: this.state,
|
state: this.state,
|
||||||
handlePaste: this.options.onPaste,
|
handlePaste: (...args) => { this.emit('paste', ...args) },
|
||||||
handleDrop: this.options.onDrop,
|
handleDrop: (...args) => { this.emit('drop', ...args) },
|
||||||
dispatchTransaction: this.dispatchTransaction.bind(this),
|
dispatchTransaction: this.dispatchTransaction.bind(this),
|
||||||
})
|
})
|
||||||
|
|
||||||
view.dom.style.whiteSpace = 'pre-wrap'
|
view.dom.style.whiteSpace = 'pre-wrap'
|
||||||
|
|
||||||
view.dom.addEventListener('focus', event => this.options.onFocus({
|
view.dom.addEventListener('focus', event => this.emit('focus', {
|
||||||
event,
|
event,
|
||||||
state: this.state,
|
state: this.state,
|
||||||
view: this.view,
|
view: this.view,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
view.dom.addEventListener('blur', event => this.options.onBlur({
|
view.dom.addEventListener('blur', event => this.emit('blur', {
|
||||||
event,
|
event,
|
||||||
state: this.state,
|
state: this.state,
|
||||||
view: this.view,
|
view: this.view,
|
||||||
@ -295,7 +312,7 @@ export default class Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emitUpdate(transaction) {
|
emitUpdate(transaction) {
|
||||||
this.options.onUpdate({
|
this.emit('update', {
|
||||||
getHTML: this.getHTML.bind(this),
|
getHTML: this.getHTML.bind(this),
|
||||||
getJSON: this.getJSON.bind(this),
|
getJSON: this.getJSON.bind(this),
|
||||||
state: this.state,
|
state: this.state,
|
||||||
@ -325,6 +342,13 @@ export default class Editor {
|
|||||||
this.view.dom.blur()
|
this.view.dom.blur()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSchemaJSON() {
|
||||||
|
return JSON.parse(JSON.stringify({
|
||||||
|
nodes: this.extensions.nodes,
|
||||||
|
marks: this.extensions.marks,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
getHTML() {
|
getHTML() {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
const fragment = DOMSerializer
|
const fragment = DOMSerializer
|
||||||
|
57
packages/tiptap/src/Utils/Emitter.js
Normal file
57
packages/tiptap/src/Utils/Emitter.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
export default class Emitter {
|
||||||
|
// Add an event listener for given event
|
||||||
|
on(event, fn) {
|
||||||
|
this._callbacks = this._callbacks || {}
|
||||||
|
// Create namespace for this event
|
||||||
|
if (!this._callbacks[event]) {
|
||||||
|
this._callbacks[event] = []
|
||||||
|
}
|
||||||
|
this._callbacks[event].push(fn)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
emit(event, ...args) {
|
||||||
|
this._callbacks = this._callbacks || {}
|
||||||
|
const callbacks = this._callbacks[event]
|
||||||
|
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.forEach(callback => callback.apply(this, args))
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove event listener for given event. If fn is not provided, all event
|
||||||
|
// listeners for that event will be removed. If neither is provided, all
|
||||||
|
// event listeners will be removed.
|
||||||
|
off(event, fn) {
|
||||||
|
if (!this._callbacks || (arguments.length === 0)) {
|
||||||
|
this._callbacks = {}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// specific event
|
||||||
|
const callbacks = this._callbacks[event]
|
||||||
|
if (!callbacks) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove all handlers
|
||||||
|
if (arguments.length === 1) {
|
||||||
|
delete this._callbacks[event]
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove specific handler
|
||||||
|
for (let i = 0; i < callbacks.length; i += 1) {
|
||||||
|
const callback = callbacks[i]
|
||||||
|
if (callback === fn) {
|
||||||
|
callbacks.splice(i, 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,14 @@ export default class Extension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEditor(editor = null) {
|
||||||
|
this.editor = editor
|
||||||
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,11 @@ import { keymap } from 'prosemirror-keymap'
|
|||||||
|
|
||||||
export default class ExtensionManager {
|
export default class ExtensionManager {
|
||||||
|
|
||||||
constructor(extensions = []) {
|
constructor(extensions = [], editor) {
|
||||||
|
extensions.forEach(extension => {
|
||||||
|
extension.bindEditor(editor)
|
||||||
|
extension.init()
|
||||||
|
})
|
||||||
this.extensions = extensions
|
this.extensions = extensions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
packages/tiptap/src/Utils/camelCase.js
Normal file
3
packages/tiptap/src/Utils/camelCase.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function (str) {
|
||||||
|
return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => (index === 0 ? word.toLowerCase() : word.toUpperCase())).replace(/\s+/g, '')
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
|
export { default as camelCase } from './camelCase'
|
||||||
export { default as ComponentView } from './ComponentView'
|
export { default as ComponentView } from './ComponentView'
|
||||||
|
export { default as Emitter } from './Emitter'
|
||||||
export { default as Extension } from './Extension'
|
export { default as Extension } from './Extension'
|
||||||
export { default as ExtensionManager } from './ExtensionManager'
|
export { default as ExtensionManager } from './ExtensionManager'
|
||||||
export { default as Mark } from './Mark'
|
export { default as Mark } from './Mark'
|
||||||
|
Loading…
Reference in New Issue
Block a user