160 lines
5.8 KiB
JavaScript
160 lines
5.8 KiB
JavaScript
|
|
import { Plugin } from 'prosemirror-state'
|
|
import crel from 'crel'
|
|
import * as Y from '../src/index.js'
|
|
import { prosemirrorPluginKey } from 'y-prosemirror'
|
|
import * as encoding from 'lib0/encoding.js'
|
|
import * as decoding from 'lib0/decoding.js'
|
|
import * as historyProtocol from 'y-protocols/history.js'
|
|
|
|
const niceColors = ['#3cb44b', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#008080', '#9a6324', '#800000', '#808000', '#000075', '#808080']
|
|
|
|
const createUserCSS = (userid, username, color = 'rgb(250, 129, 0)', color2 = 'rgba(250, 129, 0, .41)') => `
|
|
[ychange_state][ychange_user="${userid}"]:hover::before {
|
|
content: "${username}" !important;
|
|
background-color: ${color} !important;
|
|
}
|
|
[ychange_state="added"][ychange_user="${userid}"] {
|
|
background-color: ${color2} !important;
|
|
}
|
|
[ychange_state="removed"][ychange_user="${userid}"] {
|
|
color: ${color} !important;
|
|
}
|
|
`
|
|
|
|
export const noteHistoryPlugin = new Plugin({
|
|
state: {
|
|
init (initargs, state) {
|
|
return new NoteHistoryPlugin()
|
|
},
|
|
apply (tr, pluginState) {
|
|
return pluginState
|
|
}
|
|
},
|
|
view (editorView) {
|
|
const hstate = noteHistoryPlugin.getState(editorView.state)
|
|
hstate.init(editorView)
|
|
return {
|
|
destroy: hstate.destroy.bind(hstate)
|
|
}
|
|
}
|
|
})
|
|
|
|
const createWrapper = () => {
|
|
const wrapper = crel('div', { style: 'display: flex;' })
|
|
const historyContainer = crel('div', { style: 'align-self: baseline; flex-basis: 250px;', class: 'shared-history' })
|
|
wrapper.insertBefore(historyContainer, null)
|
|
const userStyleContainer = crel('style')
|
|
wrapper.insertBefore(userStyleContainer, null)
|
|
return { wrapper, historyContainer, userStyleContainer }
|
|
}
|
|
|
|
class NoteHistoryPlugin {
|
|
init (editorView) {
|
|
this.editorView = editorView
|
|
const { historyContainer, wrapper, userStyleContainer } = createWrapper()
|
|
this.userStyleContainer = userStyleContainer
|
|
this.wrapper = wrapper
|
|
this.historyContainer = historyContainer
|
|
const n = editorView.dom.parentNode.parentNode
|
|
n.parentNode.replaceChild(this.wrapper, n)
|
|
n.style['flex-grow'] = '1'
|
|
wrapper.insertBefore(n, this.wrapper.firstChild)
|
|
this.render()
|
|
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
|
const history = y.define('history', Y.Array)
|
|
history.observe(this.render.bind(this))
|
|
}
|
|
destroy () {
|
|
this.wrapper.parentNode.replaceChild(this.wrapper.firstChild, this.wrapper)
|
|
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
|
const history = y.define('history', Y.Array)
|
|
history.unobserve(this.render)
|
|
}
|
|
render () {
|
|
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
|
const history = y.define('history', Y.Array).toArray()
|
|
const fragment = document.createDocumentFragment()
|
|
const snapshotBtn = crel('button', { type: 'button' }, ['snapshot'])
|
|
fragment.insertBefore(snapshotBtn, null)
|
|
let _prevSnap = null // empty
|
|
snapshotBtn.addEventListener('click', () => {
|
|
const awareness = y.getAwarenessInfo()
|
|
const userMap = new Map()
|
|
const aw = y.getLocalAwarenessInfo()
|
|
userMap.set(y.userID, aw.name || 'unknown')
|
|
awareness.forEach((a, userID) => {
|
|
userMap.set(userID, a.name || 'Unknown')
|
|
})
|
|
this.snapshot(userMap)
|
|
})
|
|
history.forEach(buf => {
|
|
const decoder = decoding.createDecoder(buf)
|
|
const snapshot = historyProtocol.readHistorySnapshot(decoder)
|
|
const date = new Date(decoding.readUint32(decoder) * 1000)
|
|
const restoreBtn = crel('button', { type: 'button' }, ['restore'])
|
|
const a = crel('a', [
|
|
'• ' + date.toUTCString(), restoreBtn
|
|
])
|
|
const el = crel('div', [ a ])
|
|
let prevSnapshot = _prevSnap // rebind to new variable
|
|
restoreBtn.addEventListener('click', event => {
|
|
if (prevSnapshot === null) {
|
|
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
|
|
}
|
|
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot, restore: true }))
|
|
event.stopPropagation()
|
|
})
|
|
a.addEventListener('click', () => {
|
|
console.log('setting snapshot')
|
|
if (prevSnapshot === null) {
|
|
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
|
|
}
|
|
this.renderSnapshot(snapshot, prevSnapshot)
|
|
})
|
|
fragment.insertBefore(el, null)
|
|
_prevSnap = snapshot
|
|
})
|
|
this.historyContainer.innerHTML = ''
|
|
this.historyContainer.insertBefore(fragment, null)
|
|
}
|
|
renderSnapshot (snapshot, prevSnapshot) {
|
|
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot }))
|
|
/**
|
|
* @type {Array<string|null>}
|
|
*/
|
|
let colors = niceColors.slice()
|
|
let style = ''
|
|
snapshot.userMap.forEach((name, userid) => {
|
|
/**
|
|
* @type {any}
|
|
*/
|
|
const randInt = name.split('').map(s => s.charCodeAt(0)).reduce((a, b) => a + b)
|
|
let color = null
|
|
let i = 0
|
|
for (; i < colors.length && color === null; i++) {
|
|
color = colors[(randInt + i) % colors.length]
|
|
}
|
|
if (color === null) {
|
|
colors = niceColors.slice()
|
|
i = 0
|
|
color = colors[randInt % colors.length]
|
|
}
|
|
colors[randInt % colors.length] = null
|
|
style += createUserCSS(userid, name, color, color + '69')
|
|
})
|
|
this.userStyleContainer.innerHTML = style
|
|
}
|
|
/**
|
|
* @param {Map<number, string>} [updatedUserMap] Maps from userid (yjs model) to account name (e.g. mail address)
|
|
*/
|
|
snapshot (updatedUserMap = new Map()) {
|
|
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
|
const history = y.define('history', Y.Array)
|
|
const encoder = encoding.createEncoder()
|
|
historyProtocol.writeHistorySnapshot(encoder, y, updatedUserMap)
|
|
encoding.writeUint32(encoder, Math.floor(Date.now() / 1000))
|
|
history.push([encoding.toBuffer(encoder)])
|
|
}
|
|
}
|