-
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #738 from streamich/peritext-dom-events
Add Peritext DOM event handlers to the main line
- Loading branch information
Showing
9 changed files
with
583 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import type {UiLifeCycles} from './types'; | ||
|
||
/** | ||
* Keeps track of all pressed down keys. | ||
*/ | ||
export class KeyController implements UiLifeCycles { | ||
/** | ||
* All currently pressed keys. | ||
*/ | ||
public readonly pressed = new Set<string>(); | ||
|
||
public start(): void { | ||
document.addEventListener('keydown', this.onKeyDown); | ||
document.addEventListener('keyup', this.onKeyUp); | ||
document.addEventListener('focus', this.onFocus); | ||
} | ||
|
||
public stop(): void { | ||
document.removeEventListener('keydown', this.onKeyDown); | ||
document.removeEventListener('keyup', this.onKeyUp); | ||
document.removeEventListener('focus', this.onFocus); | ||
} | ||
|
||
private onKeyDown = (event: KeyboardEvent): void => { | ||
this.pressed.add(event.key); | ||
}; | ||
|
||
private onKeyUp = (event: KeyboardEvent): void => { | ||
this.pressed.delete(event.key); | ||
}; | ||
|
||
private onFocus = (): void => { | ||
this.pressed.clear(); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Controllers in this folder subscribe to native DOM events and translate them | ||
into Peritext events. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import type {PeritextEventTarget} from '../events/PeritextEventTarget'; | ||
import type {UiLifeCycles} from './types'; | ||
import type {Peritext} from '../../json-crdt-extensions/peritext'; | ||
|
||
export interface RichTextControllerOpts { | ||
source: HTMLElement; | ||
txt: Peritext; | ||
et: PeritextEventTarget; | ||
} | ||
|
||
export class RichTextController implements UiLifeCycles { | ||
public constructor(public readonly opts: RichTextControllerOpts) {} | ||
|
||
/** -------------------------------------------------- {@link UiLifeCycles} */ | ||
|
||
public start(): void { | ||
const el = this.opts.source; | ||
el.addEventListener('keydown', this.onKeyDown); | ||
} | ||
|
||
public stop(): void { | ||
const el = this.opts.source; | ||
el.removeEventListener('keydown', this.onKeyDown); | ||
} | ||
|
||
private onKeyDown = (event: KeyboardEvent): void => { | ||
const key = event.key; | ||
const et = this.opts.et; | ||
if (key === 'b' && (event.ctrlKey || event.metaKey)) { | ||
event.preventDefault(); | ||
et.inline({type: 'b'}); | ||
return; | ||
} | ||
if (key === 'i' && (event.ctrlKey || event.metaKey)) { | ||
event.preventDefault(); | ||
et.inline({type: 'i'}); | ||
return; | ||
} | ||
}; | ||
} |
Oops, something went wrong.