Add: AutoSave Plugin for ProseMirror

This commit is contained in:
Raphael Beer 2020-04-10 12:52:08 +02:00
parent a9111973c8
commit 0d397655ee
No known key found for this signature in database
GPG Key ID: C1AC5E018B25EF11
5 changed files with 138 additions and 6 deletions

View File

@ -1,22 +1,28 @@
import { mount } from '@vue/test-utils'
import Editor from './Editor'
import MutationObserver from 'mutation-observer'
import Vue from 'vue'
import VueRouter from 'vue-router'
import { mount } from '@vue/test-utils'
import MutationObserver from 'mutation-observer'
import Editor from './Editor'
global.MutationObserver = MutationObserver
const localVue = global.localVue
localVue.use(VueRouter)
const localStorage = global.localStorage
describe('Editor.vue', () => {
let wrapper
let propsData
let mocks
let router
const Wrapper = () => {
return (wrapper = mount(Editor, {
mocks,
propsData,
router,
localVue,
sync: false,
stubs: {
@ -141,4 +147,65 @@ describe('Editor.vue', () => {
})
})
})
describe(':autosave', () => {
const getFirst = () => {
const storageKey = Object.keys(localStorage)[0]
const value = localStorage[storageKey]
return {
storageKey,
value,
}
}
describe('when false', () => {
let routerWrapper
beforeEach(() => {
router = new VueRouter({
routes: [{ path: 'post/create' }],
})
router.push('/post/create')
propsData.autosave = false
routerWrapper = Wrapper()
})
it('does nothing', () => {
jest.useFakeTimers()
const content = '<p>NOOP WIP</p>'
routerWrapper.vm.editor.setContent(content, true)
jest.runAllTimers()
expect(Object.keys(localStorage).length).toBe(0)
})
})
describe('when editing a post', () => {
let routerWrapper
const content = '<p>Post WIP</p>'
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem')
beforeEach(() => {
router = new VueRouter({
routes: [{ path: 'post/create' }],
})
router.push('/post/create')
routerWrapper = Wrapper()
})
afterEach(setItemSpy.mockReset)
it('saves editor content to localStorage on input', async () => {
jest.useFakeTimers()
routerWrapper.vm.editor.setContent(content, true)
await jest.runAllTimers()
const { storageKey, value } = getFirst()
expect(setItemSpy).toHaveBeenCalled()
expect(storageKey.startsWith('draft:post:')).toBe(true)
expect(value).toBe(content)
})
})
})
})

View File

@ -1,5 +1,6 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import StoryRouter from 'storybook-vue-router'
import HcEditor from '~/components/Editor/Editor.vue'
import helpers from '~/storybook/helpers'
import Vue from 'vue'
@ -35,6 +36,7 @@ const users = [
storiesOf('Editor', module)
.addDecorator(withA11y)
.addDecorator(StoryRouter())
.addDecorator((storyFn) => {
const ctx = storyFn()
return {

View File

@ -52,6 +52,7 @@ export default {
hashtags: { type: Array, default: () => null }, // If 'null', than the Hashtag extention is not assigned.
value: { type: String, default: '' },
doc: { type: Object, default: () => {} },
autosave: { type: Boolean, default: true },
},
data() {
return {

View File

@ -4,6 +4,7 @@ import Link from '~/components/Editor/nodes/Link.js'
import Strike from '~/components/Editor/marks/Strike'
import Italic from '~/components/Editor/marks/Italic'
import Bold from '~/components/Editor/marks/Bold'
import AutoSave from '~/components/Editor/plugins/autoSave'
import EmbedQuery from '~/graphql/EmbedQuery.js'
import {
Heading,
@ -18,8 +19,8 @@ import {
} from 'tiptap-extensions'
export default function defaultExtensions(component) {
const { placeholder, $t, $apollo } = component
return [
const { autosave, placeholder, $t, $apollo, $route } = component
const extensions = [
new Heading(),
new HardBreak(),
new Blockquote(),
@ -54,4 +55,10 @@ export default function defaultExtensions(component) {
},
}),
]
if (autosave && $route) {
extensions.push(new AutoSave({ $route }))
}
return extensions
}

View File

@ -0,0 +1,55 @@
import { Extension, Plugin, PluginKey } from 'tiptap'
import { DOMSerializer } from 'prosemirror-model'
export default class AutoSave extends Extension {
constructor({ $route }) {
super()
this.route = $route
this._postId = 'randomIdForPosts'
}
static toHTML(content, schema) {
const container = document.createElement('div')
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(content)
container.appendChild(fragment)
return container.innerHTML
}
get name() {
return 'auto_save'
}
get storageKey() {
if (this.route.path === '/post/create') {
return `draft:post:${this._postId}`
}
return null
}
get plugins() {
return [
new Plugin({
key: new PluginKey('auto_save'),
filterTransaction: (tr, editorState) => {
if (tr.docChanged) {
localStorage.setItem(
this.storageKey,
AutoSave.toHTML(tr.doc.content, editorState.config.schema),
)
}
return tr
},
state: {
init() {
return {
saveNextUpdate: false,
}
},
apply(_, prev) {
return { ...prev }
},
},
}),
]
}
}