-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: start on work-in-progress Loro integration with the backend.
- Loading branch information
Showing
24 changed files
with
593 additions
and
554 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -168,4 +168,4 @@ | |
.timestamp { | ||
color: #999; | ||
} | ||
</style> | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import type { DagNode } from './dag-view'; | ||
|
||
export interface ViewDagNode extends DagNode { | ||
message?: string; | ||
author?: string; | ||
timestamp?: number; | ||
} |
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import type { ViewDagNode } from './dag-view-types'; | ||
import type { LoroDoc } from 'loro-crdt'; | ||
|
||
export function convertSyncStepsToNodes(doc: LoroDoc): { | ||
nodes: ViewDagNode[]; | ||
frontiers: string[]; | ||
} { | ||
const frontiers = doc.oplogFrontiers(); | ||
const stack = frontiers.concat(); | ||
const nodes: ViewDagNode[] = []; | ||
const visited = new Set<string>(); | ||
const processedIds = new Set<string>(); | ||
|
||
// Build an ordered list of nodes using DFS | ||
while (stack.length > 0) { | ||
const top = stack.pop()!; | ||
const change = doc.getChangeAt(top); | ||
|
||
const nodeId = idToString({ | ||
peer: change.peer, | ||
counter: top.counter | ||
}); | ||
|
||
if (!processedIds.has(nodeId)) { | ||
processedIds.add(nodeId); | ||
|
||
const deps = change.deps.map(idToString); | ||
|
||
nodes.push({ | ||
id: nodeId, | ||
deps, | ||
lamport: change.lamport, | ||
message: `Change at ${change.counter} (length: ${change.length})`, | ||
// author: change.peer || '', | ||
timestamp: change.timestamp ? change.timestamp * 1000 : Date.now() | ||
}); | ||
|
||
for (const dep of change.deps) { | ||
const depId = idToString(dep); | ||
if (!visited.has(depId)) { | ||
stack.push(dep); | ||
visited.add(depId); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return { nodes: nodes, frontiers: frontiers.map(idToString) }; | ||
} | ||
|
||
export function idToString(id: { peer: string; counter: number }): string { | ||
return `${id.counter}@${id.peer}`; | ||
} |
164 changes: 164 additions & 0 deletions
164
src/lib/components/editors/LoroCompositeMarkdownEditor.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
<script lang="ts"> | ||
import type { HTMLAttributes } from 'svelte/elements'; | ||
import RichMarkdownEditor from './LoroRichMarkdownEditor.svelte'; | ||
import MarkdownEditor from './MarkdownEditor.svelte'; | ||
import DagView from './DagView/DagView.svelte'; | ||
import { LoroDoc } from 'loro-crdt'; | ||
import { CursorAwareness } from 'loro-prosemirror'; | ||
import { convertSyncStepsToNodes } from './DagView/editor-history'; | ||
import './CollaborativeEditor.css'; | ||
import type { ViewDagNode } from './DagView/DagView.svelte'; | ||
import { toByteArray } from 'base64-js'; | ||
let { | ||
content = $bindable(''), | ||
markdownMode = $bindable(false), | ||
maxLength, | ||
...attrs | ||
}: { | ||
content: string; | ||
maxLength?: number; | ||
markdownMode?: boolean; | ||
} & HTMLAttributes<HTMLDivElement> = $props(); | ||
let showHistory = $state(false); | ||
let dagInfo: { nodes: ViewDagNode[]; frontiers: string[] } = $state({ | ||
nodes: [], | ||
frontiers: [] | ||
}); | ||
let loroDoc = new LoroDoc(); | ||
let idA = loroDoc.peerIdStr; | ||
let awareness = new CursorAwareness(idA); | ||
const savedState = globalThis.localStorage?.getItem('loro-editor-state'); | ||
if (savedState) { | ||
try { | ||
const blob = toByteArray(savedState); | ||
loroDoc.import(blob); | ||
console.log("imported saved state") | ||
console.log(loroDoc.toJSON()) | ||
dagInfo = convertSyncStepsToNodes(loroDoc); | ||
} catch (e) { | ||
console.error('Failed to load saved state:', e); | ||
} | ||
} | ||
// 初始化时开启时间戳记录 | ||
loroDoc.setRecordTimestamp(true); | ||
loroDoc.setChangeMergeInterval(10); | ||
// 监听变化更新历史信息 | ||
loroDoc.subscribe((event) => { | ||
if (event.by === "local") { | ||
dagInfo = convertSyncStepsToNodes(loroDoc); | ||
} | ||
}); | ||
function toggleHistory() { | ||
showHistory = !showHistory; | ||
} | ||
let shouldWiggle = $state(false); | ||
const contentProxy = { | ||
get value() { | ||
return content; | ||
}, | ||
set value(value) { | ||
if (maxLength != undefined) { | ||
if (value && value.length > maxLength) { | ||
shouldWiggle = true; | ||
content = value.slice(0, maxLength); | ||
} else { | ||
content = value; | ||
} | ||
} else { | ||
content = value; | ||
} | ||
} | ||
}; | ||
</script> | ||
|
||
<div class="container"> | ||
<div class="editors-container"> | ||
<div class="editor-card"> | ||
<div class="editor-header"> | ||
<h3 class="editor-title">Editor</h3> | ||
<div class="flex gap-2"> | ||
{#if maxLength != undefined} | ||
<div | ||
class="variant-filled badge transition-transform" | ||
class:too-long-content-badge={shouldWiggle} | ||
onanimationend={() => (shouldWiggle = false)} | ||
> | ||
Length: {content.length} / {maxLength} | ||
</div> | ||
{/if} | ||
<button class="variant-filled badge" onclick={() => (markdownMode = !markdownMode)}> | ||
{markdownMode ? 'Switch to Rich Text' : 'Switch to Markdown'} | ||
</button> | ||
<button class="status-button" onclick={toggleHistory}> | ||
{showHistory ? 'Hide History' : 'Show History'} | ||
</button> | ||
</div> | ||
</div> | ||
<div class="editor-content"> | ||
{#if !markdownMode} | ||
<RichMarkdownEditor | ||
loro={loroDoc} | ||
awareness={awareness} | ||
containerId={loroDoc.getMap("doc").id} | ||
bind:content={contentProxy.value} | ||
/> | ||
{:else} | ||
<MarkdownEditor bind:content={contentProxy.value} /> | ||
{/if} | ||
</div> | ||
</div> | ||
</div> | ||
|
||
{#if showHistory} | ||
<div class="history-card"> | ||
<h3 class="history-title">Operation History</h3> | ||
<DagView nodes={dagInfo.nodes} frontiers={dagInfo.frontiers} /> | ||
</div> | ||
{/if} | ||
</div> | ||
|
||
<style> | ||
@keyframes wiggle { | ||
0% { | ||
transform: rotate(0deg); | ||
} | ||
20% { | ||
transform: rotate(-10deg); | ||
} | ||
40% { | ||
transform: rotate(10deg); | ||
} | ||
60% { | ||
transform: rotate(-10deg); | ||
} | ||
80% { | ||
transform: rotate(10deg); | ||
} | ||
100% { | ||
transform: rotate(0deg); | ||
} | ||
} | ||
.too-long-content-badge { | ||
@apply variant-filled-error; | ||
animation: wiggle; | ||
animation-duration: 1s; | ||
} | ||
.flex { | ||
display: flex; | ||
} | ||
.gap-2 { | ||
gap: 0.5rem; | ||
} | ||
</style> |
Oops, something went wrong.