diff --git a/README.md b/README.md index 93db146..1e61e16 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,15 @@ You can use the following CSS classes to style remote cursor selections: See [demo/index.html](demo/index.html) for example styles. Additionally, you can enable per-user styling (e.g.: different colors per user). The recommended approach for this is to listen to `awareness.on("update", () => ...));` and inject custom styles for every available clientId. You can use the following classnames for this: - `yRemoteSelection-${clientId}` -- `yRemoteSelectionHead-${clientId` +- `yRemoteSelectionHead-${clientId}` (where `${clientId}` is the Yjs clientId of the specific user). + +For multi-selection styling, use these classnames for the primary and secondary selections: + +- `yRemoteSelection-primary`, `yRemoteSelectionHead-primary` +- `yRemoteSelection-secondary`, `yRemoteSelectionHead-secondary` + ### License [The MIT License](./LICENSE) © Kevin Jahns diff --git a/demo/index.html b/demo/index.html index eeca203..d66f498 100644 --- a/demo/index.html +++ b/demo/index.html @@ -12,6 +12,9 @@ .yRemoteSelection { background-color: rgb(250, 129, 0, .5) } + .yRemoteSelection-secondary { + background-color: rgb(250, 129, 0, .3) + } .yRemoteSelectionHead { position: absolute; border-left: orange solid 2px; @@ -20,6 +23,11 @@ height: 100%; box-sizing: border-box; } + .yRemoteSelectionHead-secondary { + border-left: rgba(255, 166, 0, 0.7) solid 2px; + border-top: rgba(255, 166, 0, 0.7) solid 2px; + border-bottom: rgba(255, 166, 0, 0.7) solid 2px; + } .yRemoteSelectionHead::after { position: absolute; content: ' '; @@ -28,6 +36,9 @@ left: -4px; top: -5px; } + .yRemoteSelectionHead-secondary::after { + border: 3px solid rgba(255, 166, 0, 0.7); + }
diff --git a/src/y-monaco.js b/src/y-monaco.js index ebf550b..faa81ad 100644 --- a/src/y-monaco.js +++ b/src/y-monaco.js @@ -22,16 +22,16 @@ class RelativeSelection { * @param {monaco.editor.ITextModel} monacoModel * @param {Y.Text} type */ -const createRelativeSelection = (editor, monacoModel, type) => { - const sel = editor.getSelection() - if (sel !== null) { +const createRelativeSelections = (editor, monacoModel, type) => { + const selections = editor.getSelections() + if (!selections || !selections.length) return [] + return selections.map((sel) => { const startPos = sel.getStartPosition() const endPos = sel.getEndPosition() const start = Y.createRelativePositionFromTypeIndex(type, monacoModel.getOffsetAt(startPos)) const end = Y.createRelativePositionFromTypeIndex(type, monacoModel.getOffsetAt(endPos)) return new RelativeSelection(start, end, sel.getDirection()) - } - return null + }) } /** @@ -53,6 +53,69 @@ const createMonacoSelectionFromRelativeSelection = (editor, type, relSel, doc) = return null } +/** + * @param {monaco.editor.ITextModel} monacoModel + * @param {monaco.Selection} selection + * @param {Y.Text} ytext + */ +const createSelectionAnchorAndHead = (monacoModel, selection, ytext) => { + let anchor = monacoModel.getOffsetAt(selection.getStartPosition()) + let head = monacoModel.getOffsetAt(selection.getEndPosition()) + if (selection.getDirection() === monaco.SelectionDirection.RTL) { + const tmp = anchor + anchor = head + head = tmp + } + return { + anchor: Y.createRelativePositionFromTypeIndex(ytext, anchor), + head: Y.createRelativePositionFromTypeIndex(ytext, head), + } +} + +/** + * @param {monaco.editor.ITextModel} monacoModel + * @param {number} clientID + * @param {Boolean} isSecondarySelection + * @param {Y.RelativePosition} anchor + * @param {Y.RelativePosition} head + * @param {Y.Doc} doc + * @param {Y.Text} ytext + * @returns {null|monaco.editor.IModelDeltaDecoration} + */ +const createRemoteSelectionDecoration = (monacoModel, clientID, isSecondarySelection, anchor, head, doc, ytext) => { + const anchorAbs = Y.createAbsolutePositionFromRelativePosition(anchor, doc) + const headAbs = Y.createAbsolutePositionFromRelativePosition(head, doc) + if (anchorAbs !== null && headAbs !== null && anchorAbs.type === ytext && headAbs.type === ytext) { + let start, end, afterContentClassName, beforeContentClassName + if (anchorAbs.index < headAbs.index) { + start = monacoModel.getPositionAt(anchorAbs.index) + end = monacoModel.getPositionAt(headAbs.index) + afterContentClassName = `yRemoteSelectionHead yRemoteSelectionHead-${clientID} ${ + isSecondarySelection ? 'yRemoteSelectionHead-secondary' : 'yRemoteSelectionHead-primary' + }` + beforeContentClassName = null + } else { + start = monacoModel.getPositionAt(headAbs.index) + end = monacoModel.getPositionAt(anchorAbs.index) + afterContentClassName = null + beforeContentClassName = `yRemoteSelectionHead yRemoteSelectionHead-${clientID} ${ + isSecondarySelection ? 'yRemoteSelectionHead-secondary' : 'yRemoteSelectionHead-primary' + }` + } + return { + range: new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column), + options: { + className: `yRemoteSelection yRemoteSelection-${clientID} ${ + isSecondarySelection ? 'yRemoteSelection-secondary' : 'yRemoteSelection-primary' + }`, + afterContentClassName, + beforeContentClassName, + }, + } + } + return null +} + export class MonacoBinding { /** * @param {Y.Text} ytext @@ -67,7 +130,7 @@ export class MonacoBinding { this.editors = editors this.mux = createMutex() /** - * @type {Map