Extension declaration. Insert into the domain data to install the
extension. For example (assuming a m-ld clone
object and a property
docText
in the domain):
clone.write(TSeqText.declare(0, 'docText'));
the preferred index into the existing list of extensions (lower value is higher priority).
the properties to which to apply TSeq text behaviour
Generated using TypeDoc. Delivered by Vercel. @m-ld/m-ld - v0.10.0 Source code licensed MIT. Privacy policy
This extension allows an app to embed collaborative text in a domain.
When the extension is declared for a certain property with a string value, the string should be updated using the
@splice
operator; and updates coming from the domain will also provide precise changes using@splice
, as follows. The overall effect is that the string property can be manipulated concurrently by multiple users with the result being a merge of their edits (rather than an array of conflicting string values, as would otherwise be the case).The extension should be declared at runtime in the data using declare, or provided (combined with other plugins) during clone initialisation, e.g. for a hypothetical
docText
property:const meld = await clone(new MemoryLevel, IoRemotes, config, combinePlugins([ new TSeqText('docText'), ... ]));
Once installed, a new document with text could be inserted:
await meld.write({ '@id': 'myDoc', docText: 'Initial text' });
Changes to the document should be written using splice expressions in an
@update
:await meld.write({ '@update': { '@id': 'myDoc', docText: { '@splice': [0, 7, 'My'] } });
This update will be echoed by the local clone, also using the
@splice
operator.This update changes the text "Initial" to "My". If a remote user updates "text" at position 8 to "words", at the same time, the update notification at this clone will correctly identify the change as happening at index position 3. Thus both clones will converge to "My words".
To generate splices, applications may consider the utility function textDiff.
To apply splices, applications may consider using updateSubject.
import { clone, uuid } from 'https://js.m-ld.org/ext/index.mjs'; import { MemoryLevel } from 'https://js.m-ld.org/ext/memory-level.mjs'; import { IoRemotes } from 'https://js.m-ld.org/ext/socket.io.mjs'; // m-ld extensions are loaded using their package identity (@m-ld/m-ld/ext/..). // In a real app, this redirection should be done with an import map. globalThis.require = module => import(module .replace(/@m-ld\/m-ld\/ext\/(\w+)/, 'https://js.m-ld.org/ext/$1.mjs')); globalThis.changeDomain = async function (domain) { const genesis = !domain; if (genesis) domain = `${uuid()}.public.gw.m-ld.org`; if (window.model) { await window.model.state.close(); delete window.model; } const state = await clone(new MemoryLevel(), IoRemotes, { '@id': uuid(), '@domain': domain, genesis, io: { uri: "https://gw.m-ld.org" } }); // Uncomment the next line to log individual updates as they come in //state.follow(update => console.info('UDPATE', JSON.stringify(update))); domainInput.value = domain; appDiv.hidden = false; playgroundAnchor.setAttribute('href', `https://m-ld.org/playground/#domain=${domain}`); // Store the "model" as a global for access by other scripts, and tell them window.model = { domain, state, genesis }; document.dispatchEvent(new Event('domainChanged')); } /** * Utility to populate a template. Returns an object containing the cloned * children of the template, also indexed by tagName and classname. */ globalThis.templated = template => new Proxy({ $: template.content.cloneNode(true) }, { get: (t, p) => t[p] ?? t.$.querySelector(p) ?? t.$.querySelector(`.${p}`) }); document.querySelectorAll('.help').forEach(help => helpDetails.appendChild(templated(help).$));
<div> <a id="playgroundAnchor" target="_blank" title="go to playground">🛝</a> <input id="domainInput" type="text" placeholder="domain name" onfocus="this.select()"/> <button onclick="changeDomain(domainInput.value)">Join</button> <button onclick="changeDomain()">New ⭐️</button> <details id="helpDetails"> <summary>🔢 help...</summary> <p>This live code demo shows how to share live information with <b>m-ld</b>.</p> <p>To get started with a new set of information (a "domain"), click New ⭐️ above. You can then interact with the mini-application below to create some information.</p> <p>To share the information with a duplicate of this page:<ol><li>copy the domain name above</li><li>duplicate the browser tab</li><li>paste the domain name into the new page's domain input</li><li>click Join</li></ol></p> <p>You can also share with the <b>m-ld</b> playground using the 🛝 button.</p> </details> <hr/> </div>
import { TSeqText } from 'https://js.m-ld.org/ext/tseq.mjs'; import { updateSubject } from 'https://js.m-ld.org/ext/index.mjs'; import { ElementSpliceText } from 'https://js.m-ld.org/ext/html.mjs'; document.addEventListener('domainChanged', () => { if (window.model.genesis) { window.model.state.write(TSeqText.declare(0, 'text')) // Write some initial document content .then(() => window.model.state.write({ '@id': 'document', 'text': `Document created ${new Date().toLocaleString()}` })); } let documentTextProxy = null; const doc = { '@id': 'document', set text(initialText) { documentTextProxy = new ElementSpliceText( documentTextDiv, window.model.state, 'document', 'text', initialText ); }, get text() { return documentTextProxy; } }; window.model.state.read( async state => updateSubject(doc, await state.get('document')), update => updateSubject(doc, update) ); });
<div id="appDiv" hidden> <h2>Document</h2> <div contenteditable="plaintext-only" id="documentTextDiv"></div> </div>
div[contenteditable] { border: 1px inset #ccc; padding: 5px; background-color: white; font-family: monospace; height: 20em; }
TSeq