Skip to content

Commit

Permalink
Merge pull request #254 from mixonic/tab-next
Browse files Browse the repository at this point in the history
Add support for tabs
  • Loading branch information
mixonic committed Dec 7, 2015
2 parents 6eff771 + a677599 commit 3cfdd87
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 16 deletions.
25 changes: 19 additions & 6 deletions src/js/editor/editor.js
Expand Up @@ -36,6 +36,7 @@ import {
setClipboardCopyData
} from '../utils/paste-utils';
import { DIRECTION } from 'mobiledoc-kit/utils/key';
import { TAB } from 'mobiledoc-kit/utils/characters';

export const EDITOR_ELEMENT_CLASS_NAME = '__mobiledoc-editor';

Expand Down Expand Up @@ -598,8 +599,7 @@ class Editor {
this._insertEmptyMarkupSectionAtCursor();
}

const key = Key.fromEvent(event);

let key = Key.fromEvent(event);
let range, nextPosition;

switch(true) {
Expand Down Expand Up @@ -630,11 +630,24 @@ class Editor {
this.handleNewline(event);
break;
case key.isPrintable():
let range = this.cursor.offsets;
if (this.cursor.hasSelection()) {
let nextPosition = this.run(postEditor => postEditor.deleteRange(range));
let { offsets: range } = this.cursor;
let { isCollapsed } = range;
let nextPosition = range.head;
this.run(postEditor => {
if (!isCollapsed) {
nextPosition = postEditor.deleteRange(range);
}
if (key.isTab() && !range.head.section.isCardSection) {
nextPosition = postEditor.insertText(nextPosition, TAB);
}
});
if (nextPosition !== range.head) {
this.cursor.moveToPosition(nextPosition);
} else if (range.head.section.isCardSection) {
}
if (
(isCollapsed && range.head.section.isCardSection) ||
key.isTab()
) {
event.preventDefault();
}
break;
Expand Down
23 changes: 23 additions & 0 deletions src/js/editor/post.js
Expand Up @@ -674,6 +674,29 @@ class PostEditor {
return this.moveSectionBefore(collection, renderedSection, beforeSection);
}

insertText(position, text) {
let section = position.section;
if (!section.isMarkerable) {
return;
}

let {marker,offset} = position.markerPosition;
let nextPosition = position;
if (!marker) {
marker = this.builder.createMarker(text);
section.markers.append(marker);
this._markDirty(section);
nextPosition = new Position(section, 1);
} else if (marker) {
let markerHead = marker.value.slice(0, offset);
let markerTail = marker.value.slice(offset, marker.length);
marker.value = `${markerHead}${text}${markerTail}`;
this._markDirty(marker);
nextPosition = position.moveRight();
}
return nextPosition;
}

_replaceSection(section, newSections) {
let nextSection = section.next;
let collection = section.parent.sections;
Expand Down
6 changes: 1 addition & 5 deletions src/js/models/marker.js
Expand Up @@ -4,10 +4,6 @@ import { normalizeTagName } from '../utils/dom-utils';
import { detect, commonItemLength, forEach, filter } from '../utils/array-utils';
import LinkedItem from '../utils/linked-item';

function trim(str) {
return str.replace(/^\s+/, '').replace(/\s+$/, '');
}

const Marker = class Marker extends LinkedItem {
constructor(value='', markups=[]) {
super();
Expand All @@ -27,7 +23,7 @@ const Marker = class Marker extends LinkedItem {
}

get isBlank() {
return trim(this.value).length === 0;
return this.value.length === 0;
}

get length() {
Expand Down
14 changes: 12 additions & 2 deletions src/js/parsers/dom.js
@@ -1,4 +1,7 @@
import { NO_BREAK_SPACE } from '../renderers/editor-dom';
import {
NO_BREAK_SPACE,
TAB_CHARACTER
} from '../renderers/editor-dom';
import {
MARKUP_SECTION_TYPE,
LIST_SECTION_TYPE,
Expand All @@ -12,6 +15,7 @@ import {
detect,
forEach,
} from '../utils/array-utils';
import { TAB } from 'mobiledoc-kit/utils/characters';

import SectionParser from 'mobiledoc-kit/parsers/section';
import { getAttributes, walkTextNodes } from '../utils/dom-utils';
Expand All @@ -20,9 +24,11 @@ import Markup from 'mobiledoc-kit/models/markup';
const GOOGLE_DOCS_CONTAINER_ID_REGEX = /^docs\-internal\-guid/;

const NO_BREAK_SPACE_REGEX = new RegExp(NO_BREAK_SPACE, 'g');
const TAB_CHARACTER_REGEX = new RegExp(TAB_CHARACTER, 'g');
export function transformHTMLText(textContent) {
let text = textContent;
text = text.replace(NO_BREAK_SPACE_REGEX, ' ');
text = text.replace(TAB_CHARACTER_REGEX, TAB);
return text;
}

Expand Down Expand Up @@ -54,6 +60,10 @@ function remapTagName(tagName) {
return remapped || normalized;
}

function trim(str) {
return str.replace(/^\s+/, '').replace(/\s+$/, '');
}

/**
* Parses DOM element -> Post
*/
Expand All @@ -80,7 +90,7 @@ export default class DOMParser {
}

appendSection(post, section) {
if (section.isBlank) {
if (section.isBlank || (section.isMarkerable && trim(section.text) === '')) {
return;
}

Expand Down
7 changes: 4 additions & 3 deletions src/js/renderers/editor-dom.js
Expand Up @@ -13,9 +13,11 @@ import { startsWith, endsWith } from '../utils/string-utils';
import { addClassName } from '../utils/dom-utils';
import { MARKUP_SECTION_ELEMENT_NAMES } from '../models/markup-section';
import assert from '../utils/assert';
import { TAB } from 'mobiledoc-kit/utils/characters';

const CARD_ELEMENT_CLASS_NAME = '__mobiledoc-card';
export const NO_BREAK_SPACE = '\u00A0';
export const TAB_CHARACTER = '\u2003';
export const SPACE = ' ';

function createElementFromMarkup(doc, markup) {
Expand All @@ -40,9 +42,8 @@ function renderHTMLText(marker) {
} else if ((!marker.prev || endsWith(marker.prev.value, SPACE)) && startsWith(text, SPACE)) {
text = NO_BREAK_SPACE + text.substr(1);
}
text = text.replace(/ ( )/g, () => {
return ' '+NO_BREAK_SPACE;
});
text = text.replace(/ ( )/g, ' '+NO_BREAK_SPACE);
text = text.replace(new RegExp(TAB, 'g'), TAB_CHARACTER);
return text;
}

Expand Down
1 change: 1 addition & 0 deletions src/js/utils/characters.js
@@ -0,0 +1 @@
export const TAB = '\t';
5 changes: 5 additions & 0 deletions src/js/utils/key.js
Expand Up @@ -108,6 +108,10 @@ const Key = class Key {
return this.keyCode === Keycodes.SPACE;
}

isTab() {
return this.keyCode === Keycodes.TAB;
}

isEnter() {
return this.keyCode === Keycodes.ENTER;
}
Expand Down Expand Up @@ -158,6 +162,7 @@ const Key = class Key {
return (
(code >= Keycodes['0'] && code <= Keycodes['9']) || // number keys
this.isSpace() ||
this.isTab() ||
this.isEnter() ||
(code >= Keycodes.A && code <= Keycodes.Z) || // letter keys
(code >= Keycodes.NUMPAD_0 && code <= Keycodes.NUMPAD_9) || // numpad keys
Expand Down
27 changes: 27 additions & 0 deletions tests/acceptance/basic-editor-test.js
@@ -1,5 +1,6 @@
import { Editor } from 'mobiledoc-kit';
import Helpers from '../test-helpers';
import { TAB } from 'mobiledoc-kit/utils/characters';

const { test, module } = Helpers;

Expand Down Expand Up @@ -147,6 +148,32 @@ test('typing when on the start of a card is blocked', (assert) => {
assert.hasNoElement('#editor div:contains(Y)');
});

test('typing tab enters a tab character', (assert) => {
let done = assert.async();
let mobiledoc = Helpers.mobiledoc.build(({post}) => post());
editor = new Editor({mobiledoc});
editor.render(editorElement);

assert.hasElement('#editor');
assert.hasNoElement('#editor p');

Helpers.dom.moveCursorTo($('#editor')[0]);
Helpers.dom.insertText(editor, TAB);
Helpers.dom.insertText(editor, 'Y');
window.setTimeout(() => {
let editedMobiledoc = editor.serialize();
assert.deepEqual(editedMobiledoc.sections, [
[],
[
[1, 'p', [
[[], 0, `${TAB}Y`]
]]
]
], 'correctly encoded');
done();
}, 0);
});

// see https://github.com/bustlelabs/mobiledoc-kit/issues/215
test('select-all and type text works ok', (assert) => {
let done = assert.async();
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/parsers/dom-test.js
Expand Up @@ -3,6 +3,7 @@ import PostNodeBuilder from 'mobiledoc-kit/models/post-node-builder';
import Helpers from '../../test-helpers';
import { Editor } from 'mobiledoc-kit';
import { NO_BREAK_SPACE } from 'mobiledoc-kit/renderers/editor-dom';
import { TAB } from 'mobiledoc-kit/utils/characters';

const {module, test} = Helpers;

Expand Down Expand Up @@ -61,6 +62,14 @@ test('#parse can parse spaces and breaking spaces', (assert) => {
assert.equal(s1.markers.head.value, 'some text for you', 'has text');
});

test('#parse can parse tabs', (assert) => {
let element = buildDOM("<p>a\u2003b</p>");
let post = parser.parse(element);
let s1 = post.sections.head;
assert.equal(s1.markers.length, 1, 's1 has 1 marker');
assert.equal(s1.markers.head.value, `a${TAB}b`);
});

test('editor#reparse catches changes to section', (assert) => {
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) =>
post([
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/renderers/editor-dom-test.js
Expand Up @@ -3,6 +3,7 @@ import Renderer from 'mobiledoc-kit/renderers/editor-dom';
import RenderTree from 'mobiledoc-kit/models/render-tree';
import Helpers from '../../test-helpers';
import { NO_BREAK_SPACE } from 'mobiledoc-kit/renderers/editor-dom';
import { TAB } from 'mobiledoc-kit/utils/characters';
const { module, test } = Helpers;

const ZWNJ = '\u200c';
Expand Down Expand Up @@ -126,6 +127,19 @@ test('renders a post with marker', (assert) => {
assert.equal(renderTree.rootElement.innerHTML, '<p><strong>Hi</strong></p>');
});

test('renders a post with marker with a tab', (assert) => {
let post = Helpers.postAbstract.build(({post, markupSection, marker}) => {
return post([
markupSection('p', [marker(`a${TAB}b`)])
]);
});

const renderTree = new RenderTree(post);
render(renderTree);
assert.equal(renderTree.rootElement.innerHTML, '<p>a\u2003b</p>',
'HTML for a tab character is correct');
});

test('renders a post with markup empty section', (assert) => {
let post = builder.createPost();
let section = builder.createMarkupSection('P');
Expand Down

0 comments on commit 3cfdd87

Please sign in to comment.