diff --git a/package-lock.json b/package-lock.json index cda8086..dadee31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,12 @@ "name": "story-editor", "version": "0.1.0", "license": "ISC", + "dependencies": { + "lodash": "^4.17.21", + "yjs": "^13.6.27" + }, "devDependencies": { + "@types/lodash": "^4.17.20", "prettier": "^3.6.2", "typescript": "^5.9.2", "vite": "^7.1.1" @@ -743,6 +748,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", @@ -815,6 +827,43 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/lib0": { + "version": "0.2.114", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", + "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", + "license": "MIT", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1054,6 +1103,23 @@ "optional": true } } + }, + "node_modules/yjs": { + "version": "13.6.27", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", + "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } } } } diff --git a/package.json b/package.json index eb94b90..a87b606 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,13 @@ "format": "prettier --write ." }, "devDependencies": { + "@types/lodash": "^4.17.20", "prettier": "^3.6.2", "typescript": "^5.9.2", "vite": "^7.1.1" + }, + "dependencies": { + "lodash": "^4.17.21", + "yjs": "^13.6.27" } } diff --git a/src/main.ts b/src/main.ts index ebd13c7..dda05c2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,73 @@ +import _ from 'lodash'; import './style.css'; +import * as Y from 'yjs'; const app = document.querySelector('#app')!; app.innerHTML = `

Story Editor

Your TypeScript + Vite app is ready!

+ +
+
+ `; + +const ta1 = document.getElementById('ta1') as HTMLTextAreaElement; +const ta2 = document.getElementById('ta2') as HTMLTextAreaElement; + +function bind(text: Y.Text, el: HTMLTextAreaElement) { + el.value = text.toString(); + + const handle = _.debounce(() => { + let prev = text.toString(); + let curr = el.value; + let i = 0; + for (i = 0; prev[i] && prev[i] == curr[i]; i++) {} + const offset = i; + prev = prev.slice(offset); + curr = curr.slice(offset); + for ( + i = 0; + prev[prev.length - 1 - i] && + prev[prev.length - 1 - i] === curr[curr.length - 1 - i]; + i++ + ) {} + prev = prev.slice(0, prev.length - i); + curr = curr.slice(0, curr.length - i); + + text.doc!.transact(() => { + text.delete(offset, prev.length); + text.insert(offset, curr); + }); + }, 500); + + el.addEventListener('input', (_ev) => { + handle(); + }); +} + +const doc1 = new Y.Doc(); +bind(doc1.getText(), ta1); +const doc2 = new Y.Doc(); +bind(doc2.getText(), ta2); + +doc1.on('update', async (update) => { + await new Promise((res, _rej) => + setTimeout(res, 3000 * Math.random() + 2000) + ); + Y.applyUpdate(doc2, update); + ta2.value = doc2.getText().toString(); +}); +doc2.on('update', async (update) => { + await new Promise((res, _rej) => + setTimeout(res, 3000 * Math.random() + 2000) + ); + Y.applyUpdate(doc1, update); + ta1.value = doc1.getText().toString(); +}); + +doc1.getText().insert(0, 'foo bar baz'); +ta1.value = doc1.getText().toString(); + +(window as any).Y = Y;