Improve website by slowing down updates
This commit is contained in:
parent
deffa195b3
commit
f22c17347a
2 changed files with 61 additions and 44 deletions
|
|
@ -17,9 +17,10 @@
|
||||||
/>
|
/>
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://schmelczer.dev/reconcile" />
|
<meta property="og:url" content="https://schmelczer.dev/reconcile" />
|
||||||
<meta property="og:image" content="/og-image.png" />
|
<meta property="og:image" content="https://schmelczer.dev/reconcile/og-image.png" />
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="630" />
|
<meta property="og:image:height" content="630" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
<title>reconcile-text: conflict-free 3-way text merging</title>
|
<title>reconcile-text: conflict-free 3-way text merging</title>
|
||||||
<link inline inline-asset="index.css" inline-asset-delete />
|
<link inline inline-asset="index.css" inline-asset-delete />
|
||||||
|
|
@ -190,12 +191,12 @@
|
||||||
<path d="M3 3l18 18"></path>
|
<path d="M3 3l18 18"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<div id="merged" role="textbox" aria-readonly="true" aria-live="polite"></div>
|
<div id="merged" role="textbox" aria-readonly="true" aria-live="off"></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>2025 Andras Schmelczer</p>
|
<p>© 2025-2026 András Schmelczer</p>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/schmelczer/reconcile"
|
href="https://github.com/schmelczer/reconcile"
|
||||||
class="github-link"
|
class="github-link"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { reconcile, reconcileWithHistory } from 'reconcile-text';
|
import { reconcileWithHistory } from 'reconcile-text';
|
||||||
import type { BuiltinTokenizer } from 'reconcile-text';
|
import type { BuiltinTokenizer } from 'reconcile-text';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
|
|
@ -12,34 +12,31 @@ const tokenizerRadios = document.querySelectorAll(
|
||||||
|
|
||||||
const sampleText = `The "reconcile-text" Rust library is embedded on this page as a WASM module and powers these text boxes. Experiment with changing the "Original", "First user's edit", and "Second user's edit" text boxes to see competing changes get merged in real-time within the "Merged result" box. Here, you will see color-coded tokens marking the origin of each token, including ones that got deleted. The result highly depends on the tokenisation strategy, for example, deciding how casing or whitespace is taken into account.`;
|
const sampleText = `The "reconcile-text" Rust library is embedded on this page as a WASM module and powers these text boxes. Experiment with changing the "Original", "First user's edit", and "Second user's edit" text boxes to see competing changes get merged in real-time within the "Merged result" box. Here, you will see color-coded tokens marking the origin of each token, including ones that got deleted. The result highly depends on the tokenisation strategy, for example, deciding how casing or whitespace is taken into account.`;
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
let pendingUpdate: number | null = null;
|
||||||
originalTextArea.addEventListener('input', updateMergedText);
|
function scheduleUpdate(): void {
|
||||||
leftTextArea.addEventListener('input', updateMergedText);
|
if (pendingUpdate === null) {
|
||||||
rightTextArea.addEventListener('input', updateMergedText);
|
pendingUpdate = requestAnimationFrame(() => {
|
||||||
|
pendingUpdate = null;
|
||||||
leftTextArea.addEventListener('selectionchange', updateMergedText);
|
updateMergedText();
|
||||||
rightTextArea.addEventListener('selectionchange', updateMergedText);
|
});
|
||||||
leftTextArea.addEventListener('select', updateMergedText);
|
|
||||||
rightTextArea.addEventListener('select', updateMergedText);
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
reconcile(
|
|
||||||
'Hello world',
|
|
||||||
{
|
|
||||||
text: 'Hello beautiful world',
|
|
||||||
cursors: [{ id: 1, position: 6 }], // After "Hello "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Hi world',
|
|
||||||
cursors: [{ id: 2, position: 0 }], // At the beginning
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
);
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
originalTextArea.addEventListener('input', scheduleUpdate);
|
||||||
|
leftTextArea.addEventListener('input', scheduleUpdate);
|
||||||
|
rightTextArea.addEventListener('input', scheduleUpdate);
|
||||||
|
|
||||||
|
document.addEventListener('selectionchange', () => {
|
||||||
|
if (document.activeElement === leftTextArea || document.activeElement === rightTextArea) {
|
||||||
|
scheduleUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('resize', resizeTextAreas);
|
window.addEventListener('resize', resizeTextAreas);
|
||||||
|
|
||||||
tokenizerRadios.forEach((radio) => {
|
tokenizerRadios.forEach((radio) => {
|
||||||
radio.addEventListener('change', updateMergedText);
|
radio.addEventListener('change', scheduleUpdate);
|
||||||
});
|
});
|
||||||
|
|
||||||
loadSample();
|
loadSample();
|
||||||
|
|
@ -84,7 +81,7 @@ function updateMergedText(): void {
|
||||||
|
|
||||||
let selectionStart: number = Number.NEGATIVE_INFINITY;
|
let selectionStart: number = Number.NEGATIVE_INFINITY;
|
||||||
let selectionEnd: number = Number.NEGATIVE_INFINITY;
|
let selectionEnd: number = Number.NEGATIVE_INFINITY;
|
||||||
if (results.cursors?.length ?? 0 > 0) {
|
if ((results.cursors?.length ?? 0) > 0) {
|
||||||
selectionStart = results.cursors![0].position;
|
selectionStart = results.cursors![0].position;
|
||||||
selectionEnd = results.cursors![1].position;
|
selectionEnd = results.cursors![1].position;
|
||||||
}
|
}
|
||||||
|
|
@ -99,29 +96,44 @@ function updateMergedText(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { text, history } of results.history) {
|
for (const { text, history } of results.history) {
|
||||||
for (const character of text) {
|
const isDelete = history === 'RemovedFromLeft' || history === 'RemovedFromRight';
|
||||||
const span = document.createElement('span');
|
let spanChars: string[] = [];
|
||||||
span.className = history;
|
let currentClass = '';
|
||||||
span.textContent = character;
|
|
||||||
|
|
||||||
if (selectionStart <= currentPosition && currentPosition < selectionEnd) {
|
const flushSpan = () => {
|
||||||
span.className += ` selection-${selectionSide}`;
|
if (spanChars.length > 0) {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = currentClass;
|
||||||
|
span.textContent = spanChars.join('');
|
||||||
|
fragment.appendChild(span);
|
||||||
|
spanChars = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const character of text) {
|
||||||
|
let className = history;
|
||||||
|
if (!isDelete && selectionStart <= currentPosition && currentPosition < selectionEnd) {
|
||||||
|
className += ` selection-${selectionSide}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment.appendChild(span);
|
if (className !== currentClass) {
|
||||||
|
flushSpan();
|
||||||
|
currentClass = className;
|
||||||
|
}
|
||||||
|
spanChars.push(character);
|
||||||
|
|
||||||
const isDelete = history === 'RemovedFromLeft' || history === 'RemovedFromRight';
|
if (!isDelete) {
|
||||||
if (currentPosition === selectionEnd - 1 && !isDelete) {
|
if (currentPosition === selectionEnd - 1) {
|
||||||
|
flushSpan();
|
||||||
fragment.appendChild(
|
fragment.appendChild(
|
||||||
createSelectionOverlay(selectionSide === 'left', isSelection)
|
createSelectionOverlay(selectionSide === 'left', isSelection)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDelete) {
|
|
||||||
// Only increment currentPosition for non-removed characters
|
|
||||||
currentPosition++;
|
currentPosition++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flushSpan();
|
||||||
}
|
}
|
||||||
|
|
||||||
mergedTextArea.innerHTML = '';
|
mergedTextArea.innerHTML = '';
|
||||||
|
|
@ -172,7 +184,7 @@ function createSelectionOverlay(isLeft: boolean, isSelection: boolean): HTMLSpan
|
||||||
|
|
||||||
function getSelectedTokenizer(): BuiltinTokenizer {
|
function getSelectedTokenizer(): BuiltinTokenizer {
|
||||||
const selectedRadio = Array.from(tokenizerRadios).find((radio) => radio.checked);
|
const selectedRadio = Array.from(tokenizerRadios).find((radio) => radio.checked);
|
||||||
return selectedRadio?.value as BuiltinTokenizer;
|
return (selectedRadio?.value ?? 'Word') as BuiltinTokenizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeTextAreas(): void {
|
function resizeTextAreas(): void {
|
||||||
|
|
@ -195,4 +207,8 @@ function focusTextArea(textarea: HTMLTextAreaElement): void {
|
||||||
textarea.selectionEnd = 0;
|
textarea.selectionEnd = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main().catch((error) => {
|
||||||
|
document.body.textContent =
|
||||||
|
'Failed to load the application. Please ensure your browser supports WebAssembly.';
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue