181 lines
5.6 KiB
JavaScript
181 lines
5.6 KiB
JavaScript
/**
|
|
* @module bindings/textarea
|
|
*/
|
|
|
|
import { createMutex } from '../lib/mutex.js'
|
|
import * as math from '../lib/math.js'
|
|
import * as ypos from '../utils/relativePosition.js'
|
|
|
|
const typeObserver = (binding, event) => {
|
|
binding._mux(() => {
|
|
const cm = binding.target
|
|
cm.operation(() => {
|
|
const delta = event.delta
|
|
let index = 0
|
|
for (let i = 0; i < event.delta.length; i++) {
|
|
const d = delta[i]
|
|
if (d.retain) {
|
|
index += d.retain
|
|
} else if (d.insert) {
|
|
const pos = cm.posFromIndex(index)
|
|
cm.replaceRange(d.insert, pos, pos, 'prosemirror-binding')
|
|
index += d.insert.length
|
|
} else if (d.delete) {
|
|
const start = cm.posFromIndex(index)
|
|
const end = cm.posFromIndex(index + d.delete)
|
|
cm.replaceRange('', start, end, 'prosemirror-binding')
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
const targetObserver = (binding, change) => {
|
|
binding._mux(() => {
|
|
const start = binding.target.indexFromPos(change.from)
|
|
const delLen = change.removed.map(s => s.length).reduce(math.add) + change.removed.length - 1
|
|
if (delLen > 0) {
|
|
binding.type.delete(start, delLen)
|
|
}
|
|
if (change.text.length > 0) {
|
|
binding.type.insert(start, change.text.join('\n'))
|
|
}
|
|
})
|
|
}
|
|
|
|
const createRemoteCaret = (username, color) => {
|
|
const caret = document.createElement('span')
|
|
caret.classList.add('remote-caret')
|
|
caret.setAttribute('style', `border-color: ${color}`)
|
|
const userDiv = document.createElement('div')
|
|
userDiv.setAttribute('style', `background-color: ${color}`)
|
|
userDiv.insertBefore(document.createTextNode(username), null)
|
|
caret.insertBefore(userDiv, null)
|
|
return caret
|
|
}
|
|
|
|
const updateRemoteSelection = (y, cm, type, cursors, clientId) => {
|
|
// destroy current text mark
|
|
const m = cursors.get(clientId)
|
|
if (m !== undefined) {
|
|
m.caret.clear()
|
|
if (m.sel !== null) {
|
|
m.sel.clear()
|
|
}
|
|
cursors.delete(clientId)
|
|
}
|
|
// redraw caret and selection for clientId
|
|
const aw = y.awareness.get(clientId)
|
|
if (aw === undefined) {
|
|
return
|
|
}
|
|
const user = aw.user || {}
|
|
if (user.color == null) {
|
|
user.color = '#ffa500'
|
|
}
|
|
if (user.name == null) {
|
|
user.name = `User: ${clientId}`
|
|
}
|
|
const cursor = aw.cursor
|
|
if (cursor == null || cursor.anchor == null || cursor.head == null) {
|
|
return
|
|
}
|
|
const anchor = ypos.fromRelativePosition(y, cursor.anchor || null)
|
|
const head = ypos.fromRelativePosition(y, cursor.head || null)
|
|
if (anchor !== null && head !== null && anchor.type === type && head.type === type) {
|
|
const headpos = cm.posFromIndex(head.offset)
|
|
const anchorpos = cm.posFromIndex(anchor.offset)
|
|
let from, to
|
|
if (head.offset < anchor.offset) {
|
|
from = headpos
|
|
to = anchorpos
|
|
} else {
|
|
from = anchorpos
|
|
to = headpos
|
|
}
|
|
const caretEl = createRemoteCaret(user.name, user.color)
|
|
const caret = cm.setBookmark(headpos, { widget: caretEl, insertLeft: true })
|
|
let sel = null
|
|
if (head.offset !== anchor.offset) {
|
|
sel = cm.markText(from, to, { css: `background-color: ${user.color}70`, inclusiveRight: true, inclusiveLeft: false })
|
|
}
|
|
cursors.set(clientId, { caret, sel })
|
|
}
|
|
}
|
|
|
|
const prosemirrorCursorActivity = (y, cm, type) => {
|
|
if (!cm.hasFocus()) {
|
|
return
|
|
}
|
|
const aw = y.getLocalAwarenessInfo()
|
|
const anchor = ypos.getRelativePosition(type, cm.indexFromPos(cm.getCursor('anchor')))
|
|
const head = ypos.getRelativePosition(type, cm.indexFromPos(cm.getCursor('head')))
|
|
if (aw.cursor == null || !ypos.equal(aw.cursor.anchor, anchor) || !ypos.equal(aw.cursor.head, head)) {
|
|
y.setAwarenessField('cursor', {
|
|
anchor, head
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A binding that binds a YText to a CodeMirror editor.
|
|
*
|
|
* @example
|
|
* const ytext = ydocument.define('codemirror', Y.Text)
|
|
* const editor = new CodeMirror(document.querySelector('#container'), {
|
|
* mode: 'javascript',
|
|
* lineNumbers: true
|
|
* })
|
|
* const binding = new CodeMirrorBinding(editor)
|
|
*
|
|
*/
|
|
export class CodeMirrorBinding {
|
|
/**
|
|
* @param {YText} textType
|
|
* @param {CodeMirror} codeMirror
|
|
* @param {Object} [options={cursors: true}]
|
|
*/
|
|
constructor (textType, codeMirror, { cursors = true } = {}) {
|
|
const y = textType._y
|
|
this.type = textType
|
|
this.target = codeMirror
|
|
/**
|
|
* @private
|
|
*/
|
|
this._mux = createMutex()
|
|
// set initial value
|
|
codeMirror.setValue(textType.toString())
|
|
// observe type and target
|
|
this._typeObserver = event => typeObserver(this, event)
|
|
this._targetObserver = (_, change) => targetObserver(this, change)
|
|
this._cursors = new Map()
|
|
this._awarenessListener = event => {
|
|
const f = clientId => updateRemoteSelection(y, codeMirror, textType, this._cursors, clientId)
|
|
event.added.forEach(f)
|
|
event.removed.forEach(f)
|
|
event.updated.forEach(f)
|
|
}
|
|
this._cursorListener = () => prosemirrorCursorActivity(y, codeMirror, textType)
|
|
this._blurListeer = () =>
|
|
y.setAwarenessField('cursor', null)
|
|
textType.observe(this._typeObserver)
|
|
codeMirror.on('change', this._targetObserver)
|
|
if (cursors) {
|
|
y.on('awareness', this._awarenessListener)
|
|
codeMirror.on('cursorActivity', this._cursorListener)
|
|
codeMirror.on('blur', this._blurListeer)
|
|
codeMirror.on('focus', this._cursorListener)
|
|
}
|
|
}
|
|
destroy () {
|
|
this.type.unobserve(this._typeObserver)
|
|
this.target.off('change', this._targetObserver)
|
|
this.type.off('awareness', this._awarenessListener)
|
|
this.target.off('cursorActivity', this._cursorListener)
|
|
this.target.off('focus', this._cursorListener)
|
|
this.target.off('blur', this._blurListeer)
|
|
this.type = null
|
|
this.target = null
|
|
}
|
|
}
|