Options
All
  • Public
  • Public/Protected
  • All
Menu

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;
}
see

TSeq

experimental

Hierarchy

  • TSeqText

Implements

Index

Constructors

Methods

Constructors

constructor

  • new TSeqText(...properties: Iri[]): TSeqText
  • The class constructor should only be used if declaring the extension in the call to clone, i.e. at design time. To apply the extension at runtime, declare it in the data using declare.

    Parameters

    • Rest ...properties: Iri[]

      the properties to target

    Returns TSeqText

Methods

Static declare

  • declare(priority: number, ...properties: Iri[]): Subject
  • 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'));
    

    Parameters

    • priority: number

      the preferred index into the existing list of extensions (lower value is higher priority).

    • Rest ...properties: Iri[]

      the properties to which to apply TSeq text behaviour

    Returns Subject

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Inherited property
  • Inherited method
  • Static property
  • Static method
  • Protected method

Generated using TypeDoc. Delivered by Vercel. @m-ld/m-ld - v0.10.0 Source code licensed MIT. Privacy policy