wip
This commit is contained in:
parent
2bbb0ba25f
commit
9748d12ede
|
|
@ -146,75 +146,3 @@
|
||||||
#ribbit.vim-insert {
|
#ribbit.vim-insert {
|
||||||
border-left: 3px solid #4f4;
|
border-left: 3px solid #4f4;
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
* ribbit-core.css — functional editor styles. Always load this.
|
|
||||||
* These styles control editor state visibility and behavior.
|
|
||||||
* They should not be overridden by themes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ribbit {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ribbit.loaded {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ribbit.edit {
|
|
||||||
font-family: monospace;
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ribbit.wysiwyg .md {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ribbit-editing::before,
|
|
||||||
.ribbit-editing::after {
|
|
||||||
opacity: 0.3;
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-speculative]::before,
|
|
||||||
[data-speculative]::after {
|
|
||||||
content: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ribbit.wysiwyg strong.ribbit-editing::before,
|
|
||||||
#ribbit.wysiwyg strong.ribbit-editing::after {
|
|
||||||
content: "**";
|
|
||||||
}
|
|
||||||
|
|
||||||
#ribbit.wysiwyg em.ribbit-editing::before,
|
|
||||||
#ribbit.wysiwyg em.ribbit-editing::after {
|
|
||||||
content: "*";
|
|
||||||
}
|
|
||||||
|
|
||||||
#ribbit.wysiwyg code.ribbit-editing::before,
|
|
||||||
#ribbit.wysiwyg code.ribbit-editing::after {
|
|
||||||
content: "\`";
|
|
||||||
}
|
|
||||||
|
|
||||||
#ribbit.wysiwyg h1.ribbit-editing::before { content: "# "; font-size: 0.5em; }
|
|
||||||
#ribbit.wysiwyg h2.ribbit-editing::before { content: "## "; font-size: 0.5em; }
|
|
||||||
#ribbit.wysiwyg h3.ribbit-editing::before { content: "### "; font-size: 0.5em; }
|
|
||||||
#ribbit.wysiwyg h4.ribbit-editing::before { content: "#### "; font-size: 0.5em; }
|
|
||||||
#ribbit.wysiwyg h5.ribbit-editing::before { content: "##### "; font-size: 0.5em; }
|
|
||||||
#ribbit.wysiwyg h6.ribbit-editing::before { content: "###### "; font-size: 0.5em; }
|
|
||||||
|
|
||||||
#ribbit.wysiwyg blockquote.ribbit-editing::before {
|
|
||||||
content: "> ";
|
|
||||||
}
|
|
||||||
|
|
||||||
#ribbit.vim-normal {
|
|
||||||
cursor: default;
|
|
||||||
caret-color: transparent;
|
|
||||||
border-left: 3px solid #4af;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ribbit.vim-insert {
|
|
||||||
border-left: 3px solid #4f4;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
337
src/ts/vim.ts
337
src/ts/vim.ts
|
|
@ -1,337 +0,0 @@
|
||||||
/*
|
|
||||||
* vim.ts — vim keybinding handler for ribbit source edit mode.
|
|
||||||
*
|
|
||||||
* Two modes: normal and insert. Activated in source (edit) mode only.
|
|
||||||
* Esc enters normal mode, i/a/o/O enter insert mode.
|
|
||||||
*
|
|
||||||
* Normal mode commands:
|
|
||||||
* h/j/k/l — cursor movement
|
|
||||||
* w/b — word forward/back
|
|
||||||
* 0/$ — line start/end
|
|
||||||
* gg/G — document start/end
|
|
||||||
* i — insert before cursor
|
|
||||||
* a — insert after cursor
|
|
||||||
* o — new line below, insert
|
|
||||||
* O — new line above, insert
|
|
||||||
* x — delete char under cursor
|
|
||||||
* dd — delete line
|
|
||||||
* u — undo
|
|
||||||
* Ctrl+r — redo
|
|
||||||
*/
|
|
||||||
|
|
||||||
type VimMode = 'normal' | 'insert';
|
|
||||||
|
|
||||||
/** Direction constants for cursor movement to avoid magic strings. */
|
|
||||||
const DIRECTION = {
|
|
||||||
LEFT: 'left' as const,
|
|
||||||
RIGHT: 'right' as const,
|
|
||||||
UP: 'up' as const,
|
|
||||||
DOWN: 'down' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Selection API direction mappings. */
|
|
||||||
const SELECTION_DIRECTION = {
|
|
||||||
BACKWARD: 'backward' as const,
|
|
||||||
FORWARD: 'forward' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Selection API granularity mappings. */
|
|
||||||
const SELECTION_GRANULARITY = {
|
|
||||||
CHARACTER: 'character' as const,
|
|
||||||
LINE: 'line' as const,
|
|
||||||
WORD: 'word' as const,
|
|
||||||
LINE_BOUNDARY: 'lineboundary' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Regex to match digit keys for count prefix accumulation. */
|
|
||||||
const DIGIT_PATTERN = /^[0-9]$/;
|
|
||||||
|
|
||||||
/** Default repeat count when no count prefix is given. */
|
|
||||||
const DEFAULT_REPEAT_COUNT = '1';
|
|
||||||
|
|
||||||
/** Radix for parsing count prefix strings. */
|
|
||||||
const DECIMAL_RADIX = 10;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles vim-style keybindings in ribbit's source edit mode.
|
|
||||||
*
|
|
||||||
* Supports normal and insert modes with standard vim motions,
|
|
||||||
* editing commands, and count prefixes.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const vim = new VimHandler((mode) => {
|
|
||||||
* statusBar.textContent = mode;
|
|
||||||
* });
|
|
||||||
* vim.attach(editorElement);
|
|
||||||
*/
|
|
||||||
export class VimHandler {
|
|
||||||
mode: VimMode;
|
|
||||||
private element: HTMLElement | null;
|
|
||||||
private listener: ((event: KeyboardEvent) => void) | null;
|
|
||||||
private pending: string;
|
|
||||||
private count: string;
|
|
||||||
private onModeChange: (mode: VimMode) => void;
|
|
||||||
|
|
||||||
constructor(onModeChange: (mode: VimMode) => void) {
|
|
||||||
this.mode = 'insert';
|
|
||||||
this.element = null;
|
|
||||||
this.listener = null;
|
|
||||||
this.pending = '';
|
|
||||||
this.count = '';
|
|
||||||
this.onModeChange = onModeChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind vim keybindings to a DOM element.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* vim.attach(document.getElementById('editor'));
|
|
||||||
*/
|
|
||||||
attach(element: HTMLElement): void {
|
|
||||||
this.detach();
|
|
||||||
this.element = element;
|
|
||||||
this.pending = '';
|
|
||||||
this.listener = (event: KeyboardEvent) => this.handleKey(event);
|
|
||||||
this.element.addEventListener('keydown', this.listener);
|
|
||||||
this.setMode('insert');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove vim keybindings from the current element.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* vim.detach();
|
|
||||||
*/
|
|
||||||
detach(): void {
|
|
||||||
if (this.element && this.listener) {
|
|
||||||
this.element.removeEventListener('keydown', this.listener);
|
|
||||||
this.element.classList.remove('vim-normal', 'vim-insert');
|
|
||||||
}
|
|
||||||
this.element = null;
|
|
||||||
this.listener = null;
|
|
||||||
this.mode = 'insert';
|
|
||||||
this.pending = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private setMode(mode: VimMode): void {
|
|
||||||
this.mode = mode;
|
|
||||||
this.pending = '';
|
|
||||||
this.count = '';
|
|
||||||
this.onModeChange(mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Routes keystrokes to insert-mode or normal-mode handling.
|
|
||||||
* Insert mode only intercepts Escape; normal mode handles
|
|
||||||
* all vim commands and suppresses default text input.
|
|
||||||
*/
|
|
||||||
private handleKey(event: KeyboardEvent): void {
|
|
||||||
if (this.mode === 'insert') {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
this.setMode('normal');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress default text input in normal mode
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
if (event.key === 'r') {
|
|
||||||
document.execCommand('redo');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = event.key;
|
|
||||||
|
|
||||||
// Accumulate count prefix — 0 as first char is line-start, not count
|
|
||||||
if (DIGIT_PATTERN.test(key) && (this.count || key !== '0')) {
|
|
||||||
this.count += key;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const repeat = parseInt(this.count || DEFAULT_REPEAT_COUNT, DECIMAL_RADIX);
|
|
||||||
this.count = '';
|
|
||||||
|
|
||||||
if (this.pending) {
|
|
||||||
const combo = this.pending + key;
|
|
||||||
this.pending = '';
|
|
||||||
for (let step = 0; step < repeat; step++) {
|
|
||||||
this.handlePending(combo);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dispatchNormalKey(key, repeat);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches a normal-mode key to the appropriate command.
|
|
||||||
* Separated from handleKey to keep nesting shallow.
|
|
||||||
*/
|
|
||||||
private dispatchNormalKey(key: string, repeat: number): void {
|
|
||||||
switch (key) {
|
|
||||||
case 'i':
|
|
||||||
this.setMode('insert');
|
|
||||||
break;
|
|
||||||
case 'a':
|
|
||||||
this.moveCursor(DIRECTION.RIGHT);
|
|
||||||
this.setMode('insert');
|
|
||||||
break;
|
|
||||||
case 'o':
|
|
||||||
this.endOfLine();
|
|
||||||
this.insertNewline();
|
|
||||||
this.setMode('insert');
|
|
||||||
break;
|
|
||||||
case 'O':
|
|
||||||
this.startOfLine();
|
|
||||||
this.insertNewline();
|
|
||||||
this.moveCursor(DIRECTION.UP);
|
|
||||||
this.setMode('insert');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'h':
|
|
||||||
for (let step = 0; step < repeat; step++) {
|
|
||||||
this.moveCursor(DIRECTION.LEFT);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'j':
|
|
||||||
for (let step = 0; step < repeat; step++) {
|
|
||||||
this.moveCursor(DIRECTION.DOWN);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'k':
|
|
||||||
for (let step = 0; step < repeat; step++) {
|
|
||||||
this.moveCursor(DIRECTION.UP);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'l':
|
|
||||||
for (let step = 0; step < repeat; step++) {
|
|
||||||
this.moveCursor(DIRECTION.RIGHT);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'w':
|
|
||||||
for (let step = 0; step < repeat; step++) {
|
|
||||||
this.wordForward();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'b':
|
|
||||||
for (let step = 0; step < repeat; step++) {
|
|
||||||
this.wordBack();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case '0':
|
|
||||||
this.startOfLine();
|
|
||||||
break;
|
|
||||||
case '$':
|
|
||||||
this.endOfLine();
|
|
||||||
break;
|
|
||||||
case 'G':
|
|
||||||
this.endOfDocument();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'x':
|
|
||||||
for (let step = 0; step < repeat; step++) {
|
|
||||||
this.deleteChar();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'u':
|
|
||||||
for (let step = 0; step < repeat; step++) {
|
|
||||||
document.execCommand('undo');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Two-char commands — preserve count for the second key
|
|
||||||
case 'd':
|
|
||||||
case 'g':
|
|
||||||
this.pending = key;
|
|
||||||
if (repeat > 1) {
|
|
||||||
this.count = String(repeat);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handlePending(combo: string): void {
|
|
||||||
switch (combo) {
|
|
||||||
case 'dd':
|
|
||||||
this.deleteLine();
|
|
||||||
break;
|
|
||||||
case 'gg':
|
|
||||||
this.startOfDocument();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private moveCursor(direction: 'left' | 'right' | 'up' | 'down'): void {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (!selection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selectionDirection = (direction === DIRECTION.LEFT || direction === DIRECTION.UP)
|
|
||||||
? SELECTION_DIRECTION.BACKWARD
|
|
||||||
: SELECTION_DIRECTION.FORWARD;
|
|
||||||
const granularity = (direction === DIRECTION.UP || direction === DIRECTION.DOWN)
|
|
||||||
? SELECTION_GRANULARITY.LINE
|
|
||||||
: SELECTION_GRANULARITY.CHARACTER;
|
|
||||||
selection.modify('move', selectionDirection, granularity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private wordForward(): void {
|
|
||||||
window.getSelection()?.modify('move', SELECTION_DIRECTION.FORWARD, SELECTION_GRANULARITY.WORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
private wordBack(): void {
|
|
||||||
window.getSelection()?.modify('move', SELECTION_DIRECTION.BACKWARD, SELECTION_GRANULARITY.WORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
private startOfLine(): void {
|
|
||||||
window.getSelection()?.modify('move', SELECTION_DIRECTION.BACKWARD, SELECTION_GRANULARITY.LINE_BOUNDARY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private endOfLine(): void {
|
|
||||||
window.getSelection()?.modify('move', SELECTION_DIRECTION.FORWARD, SELECTION_GRANULARITY.LINE_BOUNDARY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private startOfDocument(): void {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (!selection || !this.element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const range = document.createRange();
|
|
||||||
range.setStart(this.element, 0);
|
|
||||||
range.collapse(true);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
private endOfDocument(): void {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (!selection || !this.element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(this.element);
|
|
||||||
range.collapse(false);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
private deleteChar(): void {
|
|
||||||
document.execCommand('forwardDelete');
|
|
||||||
}
|
|
||||||
|
|
||||||
private deleteLine(): void {
|
|
||||||
this.startOfLine();
|
|
||||||
window.getSelection()?.modify('extend', SELECTION_DIRECTION.FORWARD, SELECTION_GRANULARITY.LINE_BOUNDARY);
|
|
||||||
document.execCommand('delete');
|
|
||||||
// Remove the trailing newline left after deleting line content
|
|
||||||
document.execCommand('forwardDelete');
|
|
||||||
}
|
|
||||||
|
|
||||||
private insertNewline(): void {
|
|
||||||
document.execCommand('insertLineBreak');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -107,14 +107,6 @@ describe('RibbitEditor modes', () => {
|
||||||
expect(editor.element.contentEditable).toBe('true');
|
expect(editor.element.contentEditable).toBe('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('switches to edit', () => {
|
|
||||||
const editor = new lib.Editor({});
|
|
||||||
editor.run();
|
|
||||||
editor.wysiwyg();
|
|
||||||
editor.edit();
|
|
||||||
expect(editor.getState()).toBe('edit');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('switches back to view', () => {
|
it('switches back to view', () => {
|
||||||
const editor = new lib.Editor({});
|
const editor = new lib.Editor({});
|
||||||
editor.run();
|
editor.run();
|
||||||
|
|
@ -135,25 +127,10 @@ describe('RibbitEditor modes', () => {
|
||||||
});
|
});
|
||||||
editor.run();
|
editor.run();
|
||||||
editor.wysiwyg();
|
editor.wysiwyg();
|
||||||
editor.edit();
|
|
||||||
editor.view();
|
editor.view();
|
||||||
expect(modes).toEqual(['view', 'wysiwyg', 'edit', 'view']);
|
expect(modes).toEqual(['view', 'wysiwyg', 'edit', 'view']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sourceMode disabled blocks edit', () => {
|
|
||||||
resetDOM();
|
|
||||||
const editor = new lib.Editor({
|
|
||||||
currentTheme: 'no-source',
|
|
||||||
themes: [{
|
|
||||||
name: 'no-source',
|
|
||||||
features: { sourceMode: false },
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
editor.run();
|
|
||||||
editor.wysiwyg();
|
|
||||||
editor.edit();
|
|
||||||
expect(editor.getState()).toBe('wysiwyg');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ThemeManager', () => {
|
describe('ThemeManager', () => {
|
||||||
|
|
@ -229,7 +206,6 @@ describe('defaultTheme', () => {
|
||||||
it('has correct shape', () => {
|
it('has correct shape', () => {
|
||||||
expect(lib.defaultTheme.name).toBe('ribbit-default');
|
expect(lib.defaultTheme.name).toBe('ribbit-default');
|
||||||
expect(lib.defaultTheme.tags).toBeDefined();
|
expect(lib.defaultTheme.tags).toBeDefined();
|
||||||
expect(lib.defaultTheme.features.sourceMode).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -252,17 +228,28 @@ describe('Utility functions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Editor htmlToMarkdown', () => {
|
describe('Editor htmlToMarkdown', () => {
|
||||||
beforeEach(() => resetDOM());
|
it('returns markdown in view state', () => {
|
||||||
|
resetDOM('**bold**');
|
||||||
it('converts strong', () => {
|
|
||||||
const editor = new lib.Editor({});
|
const editor = new lib.Editor({});
|
||||||
|
console.log(editor.getMarkdown());
|
||||||
editor.run();
|
editor.run();
|
||||||
expect(editor.htmlToMarkdown('<strong>bold</strong>')).toBe('**bold**');
|
console.log(editor.getMarkdown());
|
||||||
|
expect(editor.getMarkdown()).toBe('**bold**');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('converts em', () => {
|
it('returns markdown in wysiwyg state', () => {
|
||||||
|
resetDOM('**bold**');
|
||||||
const editor = new lib.Editor({});
|
const editor = new lib.Editor({});
|
||||||
editor.run();
|
editor.run();
|
||||||
expect(editor.htmlToMarkdown('<em>italic</em>')).toBe('*italic*');
|
editor.wysiwyg();
|
||||||
|
expect(editor.getMarkdown()).toBe('**bold**');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('round-trips inline formatting', () => {
|
||||||
|
resetDOM('hello **world** and *italic*');
|
||||||
|
const editor = new lib.Editor({});
|
||||||
|
editor.run();
|
||||||
|
editor.wysiwyg();
|
||||||
|
expect(editor.getMarkdown()).toBe('hello **world** and *italic*');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
150
test/vim.test.ts
150
test/vim.test.ts
|
|
@ -1,150 +0,0 @@
|
||||||
import { ribbit, resetDOM } from './setup';
|
|
||||||
|
|
||||||
const lib = ribbit();
|
|
||||||
|
|
||||||
describe('VimHandler', () => {
|
|
||||||
beforeEach(() => resetDOM('hello world'));
|
|
||||||
|
|
||||||
it('starts in insert mode', () => {
|
|
||||||
const editor = new lib.Editor({
|
|
||||||
currentTheme: 'vim',
|
|
||||||
themes: [{
|
|
||||||
name: 'vim',
|
|
||||||
features: {
|
|
||||||
sourceMode: true,
|
|
||||||
vim: true,
|
|
||||||
},
|
|
||||||
tags: lib.defaultTags,
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
editor.run();
|
|
||||||
editor.edit();
|
|
||||||
expect(editor.element.classList.contains('vim-insert')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Esc enters normal mode', () => {
|
|
||||||
const editor = new lib.Editor({
|
|
||||||
currentTheme: 'vim',
|
|
||||||
themes: [{
|
|
||||||
name: 'vim',
|
|
||||||
features: {
|
|
||||||
sourceMode: true,
|
|
||||||
vim: true,
|
|
||||||
},
|
|
||||||
tags: lib.defaultTags,
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
editor.run();
|
|
||||||
editor.edit();
|
|
||||||
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
|
|
||||||
expect(editor.element.classList.contains('vim-normal')).toBe(true);
|
|
||||||
expect(editor.element.classList.contains('vim-insert')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('i returns to insert mode', () => {
|
|
||||||
const editor = new lib.Editor({
|
|
||||||
currentTheme: 'vim',
|
|
||||||
themes: [{
|
|
||||||
name: 'vim',
|
|
||||||
features: {
|
|
||||||
sourceMode: true,
|
|
||||||
vim: true,
|
|
||||||
},
|
|
||||||
tags: lib.defaultTags,
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
editor.run();
|
|
||||||
editor.edit();
|
|
||||||
// Enter normal mode
|
|
||||||
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
|
|
||||||
// Back to insert
|
|
||||||
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'i' }));
|
|
||||||
expect(editor.element.classList.contains('vim-insert')).toBe(true);
|
|
||||||
expect(editor.element.classList.contains('vim-normal')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disables toolbar in normal mode', () => {
|
|
||||||
const editor = new lib.Editor({
|
|
||||||
autoToolbar: false,
|
|
||||||
currentTheme: 'vim',
|
|
||||||
themes: [{
|
|
||||||
name: 'vim',
|
|
||||||
features: {
|
|
||||||
sourceMode: true,
|
|
||||||
vim: true,
|
|
||||||
},
|
|
||||||
tags: lib.defaultTags,
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
editor.run();
|
|
||||||
editor.toolbar.render();
|
|
||||||
editor.edit();
|
|
||||||
editor.toolbar.enable();
|
|
||||||
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
|
|
||||||
const bold = editor.toolbar.buttons.get('bold');
|
|
||||||
expect(bold?.element?.classList.contains('disabled')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('re-enables toolbar in insert mode', () => {
|
|
||||||
const editor = new lib.Editor({
|
|
||||||
autoToolbar: false,
|
|
||||||
currentTheme: 'vim',
|
|
||||||
themes: [{
|
|
||||||
name: 'vim',
|
|
||||||
features: {
|
|
||||||
sourceMode: true,
|
|
||||||
vim: true,
|
|
||||||
},
|
|
||||||
tags: lib.defaultTags,
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
editor.run();
|
|
||||||
editor.toolbar.render();
|
|
||||||
editor.edit();
|
|
||||||
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
|
|
||||||
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'i' }));
|
|
||||||
const bold = editor.toolbar.buttons.get('bold');
|
|
||||||
expect(bold?.element?.classList.contains('disabled')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('detaches when leaving edit mode', () => {
|
|
||||||
const editor = new lib.Editor({
|
|
||||||
currentTheme: 'vim',
|
|
||||||
themes: [{
|
|
||||||
name: 'vim',
|
|
||||||
features: {
|
|
||||||
sourceMode: true,
|
|
||||||
vim: true,
|
|
||||||
},
|
|
||||||
tags: lib.defaultTags,
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
editor.run();
|
|
||||||
editor.edit();
|
|
||||||
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
|
|
||||||
expect(editor.element.classList.contains('vim-normal')).toBe(true);
|
|
||||||
editor.wysiwyg();
|
|
||||||
// vim classes should be gone after mode switch
|
|
||||||
expect(editor.element.classList.contains('vim-normal')).toBe(false);
|
|
||||||
expect(editor.element.classList.contains('vim-insert')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('only activates in edit mode', () => {
|
|
||||||
const editor = new lib.Editor({
|
|
||||||
currentTheme: 'vim',
|
|
||||||
themes: [{
|
|
||||||
name: 'vim',
|
|
||||||
features: {
|
|
||||||
sourceMode: true,
|
|
||||||
vim: true,
|
|
||||||
},
|
|
||||||
tags: lib.defaultTags,
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
editor.run();
|
|
||||||
editor.wysiwyg();
|
|
||||||
// Esc in wysiwyg should not add vim classes
|
|
||||||
editor.element.dispatchEvent(new lib.window.KeyboardEvent('keydown', { key: 'Escape' }));
|
|
||||||
expect(editor.element.classList.contains('vim-normal')).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue
Block a user