Skip to content

Commit

Permalink
Add support for tabs
Browse files Browse the repository at this point in the history
Tabs are represented by tab characters in Mobiledoc, ala \t, and in
rendered DOM are represened as an emspace: \u2003

Additionally fix an issue with white-space only markers being the first
thing in a section. They were disregarded by the renderer, errantly.
  • Loading branch information
mixonic committed Dec 7, 2015
1 parent 6eff771 commit a677599
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const TAB = '\t';
5 changes: 5 additions & 0 deletions src/js/utils/key.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 a677599

Please sign in to comment.