Skip to content

Commit

Permalink
Revert "chore(json-crdt-peritext-ui): 🤖 remove ui surface"
Browse files Browse the repository at this point in the history
This reverts commit a5553af.
  • Loading branch information
streamich committed Oct 31, 2024
1 parent 6ab990e commit 3eba2fa
Show file tree
Hide file tree
Showing 41 changed files with 1,780 additions and 0 deletions.
76 changes: 76 additions & 0 deletions src/json-crdt-peritext-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Peritext UI

## Controller Events

### Editing

- Events
- [ ] `insert` event
- [ ] Insert a character.
- [ ] Insert a string.
- [ ] `delete` event
- [ ] Delete one character backward.
- [ ] Delete one character forward.
- [ ] `mark` event
- [ ] Annotate range.
- [ ] Insert block split.
- [ ] `mark-update` event
- [ ] Change annotation metadata.
- [ ] Change annotation range.
- [ ] Change block metadata.
- [ ] `mark-delete` event
- [ ] Delete block split.
- [ ] Delete annotation.
- Imperative
- [ ] Delete range.
- [ ] Delete all annotations in range.
- [ ] Find block by position.
- [ ] Find annotations by position.
- [ ] Get block range.

### Selection

- Events
- `select` event
- [ ] Set caret.
- [ ] Set selection.
- `select-all`
- [ ] Select all.
- `select-object`
- [ ] Select block.
- [ ] Select word.
- `update-selection`
- [ ] Change position of left or right end of selection.
- Imperative

### Navigation

- [ ] Move one character left.
- [ ] Move one character right.
- [ ] Move one word left.
- [ ] Move one word right.
- [ ] Move left until end of line.
- [ ] Move right until end of line.
- [ ] Move up one line.
- [ ] Move down one line.

### Clipboard

- [ ] Export range.
- [ ] Import range.
- [ ] Insert plain text.
- [ ] Insert HTML.

### History

- [ ] Undo.
- [ ] Redo.

### Overlays

- [ ] Insert an overlay.
- [ ] Update overlay metadata.
- [ ] Delete an overlay.
- [ ] Change overlay range.
- [ ] Assign a set of overlays.
- [ ] Remove an overlay set.
24 changes: 24 additions & 0 deletions src/json-crdt-peritext-ui/__demos__/components/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';
import {Provider, GlobalCss} from 'nano-theme';
import {ModelWithExt, ext} from '../../../json-crdt-extensions';
import {PeritextView} from '../../react';
import {renderers} from '../../renderers/default';
import {renderers as debugRenderers} from '../../renderers/debug';

export const App: React.FC = ({}) => {
const [[model, peritext]] = React.useState(() => {
const model = ModelWithExt.create(ext.peritext.new('Hello world!'));
const peritext = model.s.toExt().txt;
peritext.refresh();
return [model, peritext] as const;
});

return (
<Provider theme={'light'}>
<GlobalCss />
<div style={{maxWidth: '640px', fontSize: '21px', margin: '32px auto'}}>
<PeritextView peritext={peritext} renderers={[debugRenderers({enabled: true}), renderers]} />
</div>
</Provider>
);
};
13 changes: 13 additions & 0 deletions src/json-crdt-peritext-ui/__demos__/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {createRoot} from 'react-dom/client';
import * as React from 'react';
import {App} from './components/App';

const div = document.createElement('div');
document.body.appendChild(div);

const root = createRoot(div);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
5 changes: 5 additions & 0 deletions src/json-crdt-peritext-ui/__demos__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.json",
"include": ["./"],
"compilerOptions": {}
}
33 changes: 33 additions & 0 deletions src/json-crdt-peritext-ui/__demos__/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
mode: 'development',
devtool: 'inline-source-map',
entry: __dirname + '/main.tsx',
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'ts-loader',
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve('../../..', 'dist'),
},
devServer: {
port: 9876,
hot: false,
},
};
12 changes: 12 additions & 0 deletions src/json-crdt-peritext-ui/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const enum Char {
ZeroLengthSpace = '\uFEFF',
}

export const enum ElementAttr {
InlineOffset = '__jsonjoy.com',
}

export enum CssClass {
Editor = 'jsonjoy-peritext-editor',
Inline = 'jsonjoy-peritext-inline',
}
226 changes: 226 additions & 0 deletions src/json-crdt-peritext-ui/dom/InputController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import type {Peritext} from '../../json-crdt-extensions/peritext';
import type {PeritextEventTarget} from '../events/PeritextEventTarget';
import type {TypedEventTarget} from '../events/TypedEventTarget';
import type {UiLifeCycles} from './types';

export interface InputControllerEventSourceMap {
beforeinput: HTMLElementEventMap['beforeinput'];
keydown: HTMLElementEventMap['keydown'];
}

export type InputControllerEventSource = TypedEventTarget<InputControllerEventSourceMap>;

const unit = (event: KeyboardEvent): '' | 'word' | 'line' =>
event.metaKey ? 'line' : event.altKey || event.ctrlKey ? 'word' : '';

export interface InputControllerOpts {
source: InputControllerEventSource;
txt: Peritext;
et: PeritextEventTarget;
}

/**
* Processes incoming DOM "input" events (such as "beforeinput", "input",
* "keydown", etc.) and translates them into Peritext events.
*/
export class InputController implements UiLifeCycles {
protected readonly source: InputControllerEventSource;
protected readonly txt: Peritext;
public readonly et: PeritextEventTarget;

public constructor(options: InputControllerOpts) {
this.source = options.source;
this.txt = options.txt;
this.et = options.et;
}

public start(): void {
this.source.addEventListener('beforeinput', this.onBeforeInput);
this.source.addEventListener('keydown', this.onKeyDown);
}

public stop(): void {
this.source.removeEventListener('beforeinput', this.onBeforeInput);
this.source.removeEventListener('keydown', this.onKeyDown);
}

private onBeforeInput = (event: InputEvent): void => {
// TODO: prevent default more selectively?
event.preventDefault();
const editor = this.txt.editor;
const et = this.et;
const inputType = event.inputType;
switch (inputType) {
case 'insertParagraph': {
// editor.saved.insMarker('p');
// editor.cursor.move(1);
// this.et.change(event);
break;
}
case 'insertFromComposition':
case 'insertFromDrop':
case 'insertFromPaste':
case 'insertFromYank':
case 'insertReplacementText': // insert or replace existing text by means of a spell checker, auto-correct, writing suggestions or similar
case 'insertText': {
// insert typed plain
if (typeof event.data === 'string') {
et.insert(event.data);
} else {
const item = event.dataTransfer ? event.dataTransfer.items[0] : null;
if (item) {
item.getAsString((text) => {
et.insert(text);
});
}
}
break;
}
case 'deleteContentBackward': // delete the content directly before the caret position and this intention is not covered by another inputType or delete the selection with the selection collapsing to its start after the deletion
case 'deleteContent': {
// delete the selection without specifying the direction of the deletion and this intention is not covered by another inputType
et.delete(-1, 'char');
break;
}
case 'deleteContentForward': {
// delete the content directly after the caret position and this intention is not covered by another inputType or delete the selection with the selection collapsing to its end after the deletion
et.delete(1, 'char');
break;
}
case 'deleteWordBackward': {
// delete a word directly before the caret position
et.delete(-1, 'word');
break;
}
case 'deleteWordForward': {
// delete a word directly after the caret position
et.delete(1, 'word');
break;
}
case 'deleteSoftLineBackward': {
// delete from the caret to the nearest visual line break before the caret position
et.delete(-1, 'line');
break;
}
case 'deleteSoftLineForward': {
// delete from the caret to the nearest visual line break after the caret position
et.delete(1, 'line');
break;
}
case 'deleteEntireSoftLine': // delete from the nearest visual line break before the caret position to the nearest visual line break after the caret position
case 'deleteHardLineBackward': // delete from the caret to the nearest beginning of a block element or br element before the caret position
case 'deleteHardLineForward': {
// delete from the caret to the nearest end of a block element or br element after the caret position
et.delete(-1, 'word');
break;
}
// case 'insertLineBreak': { // insert a line break
// }
// case 'insertParagraph': { // insert a paragraph break
// }
// case 'insertOrderedList': { // insert a numbered list
// }
// case 'insertUnorderedList': { // insert a bulleted list
// }
// case 'insertHorizontalRule': { // insert a horizontal rule
// }
// case 'insertFromYank': { // replace the current selection with content stored in a kill buffer
// }
// case 'insertFromDrop': { // insert content by means of drop
// }
// case 'insertFromPaste': { // paste content from clipboard or paste image from client provided image library
// }
// case 'insertFromPasteAsQuotation': { // paste content from the clipboard as a quotation
// }
// case 'insertTranspose': { // transpose the last two grapheme cluster. that were entered
// }
// case 'insertCompositionText': { // replace the current composition string
// }
// case 'insertLink': { // insert a link
// }
// case 'deleteByDrag': { // remove content from the DOM by means of drag
// }
// case 'deleteByCut': { // remove the current selection as part of a cut
// }
// case 'historyUndo': { // undo the last editing action
// }
// case 'historyRedo': { // to redo the last undone editing action
// }
// case 'formatBold': { // initiate bold text
// }
// case 'formatItalic': { // initiate italic text
// }
// case 'formatUnderline': { // initiate underline text
// }
// case 'formatStrikeThrough': { // initiate stricken through text
// }
// case 'formatSuperscript': { // initiate superscript text
// }
// case 'formatSubscript': { // initiate subscript text
// }
// case 'formatJustifyFull': { // make the current selection fully justified
// }
// case 'formatJustifyCenter': { // center align the current selection
// }
// case 'formatJustifyRight': { // right align the current selection
// }
// case 'formatJustifyLeft': { // left align the current selection
// }
// case 'formatIndent': { // indent the current selection
// }
// case 'formatOutdent': { // outdent the current selection
// }
// case 'formatRemove': { // remove all formatting from the current selection
// }
// case 'formatSetBlockTextDirection': { // set the text block direction
// }
// case 'formatSetInlineTextDirection': { // set the text inline direction
// }
// case 'formatBackColor': { // change the background color
// }
// case 'formatFontColor': { // change the font color
// }
// case 'formatFontName': { // change the font name
// }
}
};

private onKeyDown = (event: KeyboardEvent): void => {
const key = event.key;
const et = this.et;
switch (key) {
case 'ArrowLeft':
case 'ArrowRight': {
const direction = key === 'ArrowLeft' ? -1 : 1;
event.preventDefault();
if (event.shiftKey) et.move(direction, unit(event) || 'char', 'focus');
else if (event.metaKey) et.move(direction, 'line');
else if (event.altKey || event.ctrlKey) et.move(direction, 'word');
else et.move(direction);
break;
}
case 'Backspace':
case 'Delete': {
const direction = key === 'Delete' ? 1 : -1;
const deleteUnit = unit(event);
if (deleteUnit) {
event.preventDefault();
et.delete(direction, deleteUnit);
}
break;
}
case 'Home':
case 'End':
event.preventDefault();
const direction = key === 'End' ? 1 : -1;
const edge = event.shiftKey ? 'focus' : 'both';
return this.et.move(direction, 'line', edge);
case 'a':
if (event.metaKey || event.ctrlKey) {
event.preventDefault();
this.et.cursor({unit: 'all'});
return;
}
}
};
}
Loading

0 comments on commit 3eba2fa

Please sign in to comment.