Skip to content

Commit

Permalink
Handle blank mobiledoc in editor
Browse files Browse the repository at this point in the history
 * Rename Section methods semantically
 * add "simple-card" demo, with "remove" button
 * Clean up cursor detection and embedIntent
 * Add Position.emptyPosition and Range.emptyRange
 * allow mobiledoc to have 0 sections
 * add Post#isBlank
 * Add tests for typing in blank mobiledoc
 * Tests for removing the last item (e.g., card) in a mobiledoc
 * Ensure that replaceSection works when section is undefined
 * Add empty mobiledoc to demo

fixes #125 #35 #71
  • Loading branch information
bantic committed Sep 14, 2015
1 parent 501ec53 commit dca9722
Show file tree
Hide file tree
Showing 19 changed files with 433 additions and 130 deletions.
17 changes: 13 additions & 4 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,13 @@ var selfieCard = {
var simpleCard = {
name: 'simple-card',
display: {
setup: function(element) {
setup: function(element, options, env) {
var card = document.createElement('span');
card.innerHTML = 'Hello, world';
element.appendChild(card);
var button = $('<button>Remove card</button>');
button.on('click', env.remove);
$(element).append(button);
}
}
};
Expand Down Expand Up @@ -284,6 +287,7 @@ function bootEditor(element, mobiledoc) {
editor = new ContentKit.Editor({
autofocus: false,
mobiledoc: mobiledoc,
placeholder: 'Write something here...',
cards: [simpleCard, cardWithEditMode, cardWithInput, selfieCard],
cardOptions: {
image: {
Expand Down Expand Up @@ -336,6 +340,14 @@ var sampleMobiledocs = {
]
},

emptyMobiledoc: {
version: MOBILEDOC_VERSION,
sections: [
[],
[]
]
},

simpleMobiledocWithList: {
version: MOBILEDOC_VERSION,
sections: [
Expand Down Expand Up @@ -408,9 +420,6 @@ var sampleMobiledocs = {
sections: [
[],
[
[1, "H2", [
[[], 0, "Simple Card"]
]],
[10, "simple-card"]
]
]
Expand Down
2 changes: 2 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ <h2>Try a Demo</h2>
<select id='select-mobiledoc'>
<option disabled>Load a new Mobiledoc</option>
<option value='simpleMobiledoc'>Simple text content</option>
<option value='emptyMobiledoc'>Empty mobiledoc</option>
<option value='simpleMobiledocWithList'>List example</option>
<option value='mobileDocWithSimpleCard'>Simple Card</option>
<option value='mobileDocWithInputCard'>Card with Input</option>
<option value='mobileDocWithSelfieCard'>Selfie Card</option>
</select>
Expand Down
16 changes: 14 additions & 2 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ImageCard from '../cards/image';
import Key from '../utils/key';
import EventEmitter from '../utils/event-emitter';

import MobiledocParser from "../parsers/mobiledoc";
import MobiledocParser from '../parsers/mobiledoc';
import PostParser from '../parsers/post';
import DOMParser from '../parsers/dom';
import Renderer from 'content-kit-editor/renderers/editor-dom';
Expand Down Expand Up @@ -234,7 +234,7 @@ class Editor {
}

handleNewline(event) {
if (!this.cursor.hasCursor()) { return ;}
if (!this.cursor.hasCursor()) { return; }

const range = this.cursor.offsets;
event.preventDefault();
Expand Down Expand Up @@ -570,8 +570,20 @@ class Editor {
e.preventDefault(); // FIXME for now, just prevent default
}

_insertEmptyMarkupSectionAtCursor() {
const section = this.run(postEditor => {
const section = postEditor.builder.createMarkupSection('p');
postEditor.insertSectionBefore(this.post.sections, section);
return section;
});
this.cursor.moveToSection(section);
}

handleKeydown(event) {
if (!this.isEditable) { return; }
if (this.post.isBlank) {
this._insertEmptyMarkupSectionAtCursor();
}

const key = Key.fromEvent(event);

Expand Down
20 changes: 15 additions & 5 deletions src/js/editor/post.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MARKUP_SECTION_TYPE, LIST_ITEM_TYPE } from '../models/types';
import { POST_TYPE, MARKUP_SECTION_TYPE, LIST_ITEM_TYPE } from '../models/types';
import Position from '../utils/cursor/position';
import { filter, compact } from '../utils/array-utils';
import { DIRECTION } from '../utils/key';
Expand Down Expand Up @@ -484,11 +484,20 @@ class PostEditor {
}

/**
* @method replaceSection
* @param {Section} section
* @param {Section} newSection
* @return null
* @public
* FIXME: add tests for this
*/
replaceSection(section, newSection) {
return this._replaceSection(section, [newSection]);
if (!section) {
// The section may be undefined if the user used the embed intent
// ("+" icon) to insert a new "ul" section in a blank post
this.insertSectionBefore(this.editor.post.sections, newSection);
} else {
this._replaceSection(section, [newSection]);
}
}

_replaceSection(section, newSections) {
Expand Down Expand Up @@ -597,7 +606,8 @@ class PostEditor {
* @method insertSectionBefore
* @param {LinkedList} collection The list of sections to insert into
* @param {Object} section The new section
* @param {Object} beforeSection The section "before" is relative to
* @param {Object} beforeSection Optional The section "before" is relative to,
* if falsy the new section will be appended to the collection
* @public
*/
insertSectionBefore(collection, section, beforeSection) {
Expand Down Expand Up @@ -637,7 +647,7 @@ class PostEditor {

parent.sections.remove(section);

if (parent.isBlank) {
if (parent.isBlank && parent.type !== POST_TYPE) {
// If we removed the last child from a parent (e.g. the last li in a ul),
// also remove the parent
this.removeSection(parent);
Expand Down
38 changes: 25 additions & 13 deletions src/js/models/_section.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import { LIST_ITEM_TYPE, LIST_SECTION_TYPE } from './types';
import { LIST_ITEM_TYPE } from './types';
import { normalizeTagName } from '../utils/dom-utils';
import LinkedItem from '../utils/linked-item';

function isMarkerable(section) {
return !!section.markers;
}

function isListSection(section) {
return section.type === LIST_SECTION_TYPE;
function getParentSection(section) {
return section.parent;
}

function isListItem(section) {
function hasSubsections(section) {
return !!section.sections;
}

function isSubsection(section) {
return section.type === LIST_ITEM_TYPE;
}

function firstMarkerableChild(section) {
return section.items.head;
}

function lastMarkerableChild(section) {
return section.items.tail;
}

export default class Section extends LinkedItem {
constructor(type) {
super();
Expand All @@ -34,13 +46,13 @@ export default class Section extends LinkedItem {
if (next) {
if (isMarkerable(next)) {
return next;
} else if (isListSection(next)) {
const firstListItem = next.items.head;
return firstListItem;
} else if (hasSubsections(next)) {
const firstChild = firstMarkerableChild(next);
return firstChild;
}
} else if (isListItem(this)) {
const listSection = this.parent;
return listSection.immediatelyNextMarkerableSection();
} else if (isSubsection(this)) {
const parentSection = getParentSection(this);
return parentSection.immediatelyNextMarkerableSection();
}
}

Expand All @@ -49,9 +61,9 @@ export default class Section extends LinkedItem {
if (!prev) { return null; }
if (isMarkerable(prev)) {
return prev;
} else if (isListSection(prev)) {
const lastListItem = prev.items.tail;
return lastListItem;
} else if (hasSubsections(prev)) {
const lastChild = lastMarkerableChild(prev);
return lastChild;
}
}
}
4 changes: 4 additions & 0 deletions src/js/models/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export default class Post {
});
}

get isBlank() {
return this.sections.isEmpty;
}

/**
* @param {Range} range
* @return {Array} markers that are completely contained by the range
Expand Down
5 changes: 0 additions & 5 deletions src/js/parsers/mobiledoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ export default class MobiledocParser {
this.markerTypes = this.parseMarkerTypes(markerTypes);
this.parseSections(sections, post);

if (post.sections.isEmpty) {
let section = this.builder.createMarkupSection('p');
post.sections.append(section);
}

return post;
}

Expand Down
4 changes: 3 additions & 1 deletion src/js/utils/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const Cursor = class Cursor {
}

isInCard() {
if (!this.hasCursor()) { return false; }

const {head, tail} = this.offsets;
return head && tail && (head._inCard || tail._inCard);
}
Expand All @@ -40,7 +42,7 @@ const Cursor = class Cursor {
* @return {Range} Cursor#Range object
*/
get offsets() {
if (!this.hasCursor()) { return {}; }
if (!this.hasCursor()) { return Range.emptyRange(); }

const { selection, renderTree } = this;

Expand Down
42 changes: 25 additions & 17 deletions src/js/utils/cursor/position.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { isTextNode, walkTextNodes } from 'content-kit-editor/utils/dom-utils';
import {
isTextNode, findOffsetInElement
} from 'content-kit-editor/utils/dom-utils';
import {
MARKUP_SECTION_TYPE, LIST_ITEM_TYPE, CARD_TYPE
} from 'content-kit-editor/models/types';
Expand All @@ -25,27 +27,26 @@ function findParentSectionFromNode(renderTree, node) {
}
}

function findOffsetInElement(elementNode, textNode, offsetInTextNode) {
let offset = 0, found = false;
walkTextNodes(elementNode, _textNode => {
if (found) { return; }
if (_textNode === textNode) {
found = true;
offset += offsetInTextNode;
} else {
offset += _textNode.textContent.length;
}
});
return offset;
}

const Position = class Position {
constructor(section, offset=0) {
this.section = section;
this.offset = offset;
this._inCard = isCardSection(section);
}

static emptyPosition() {
return {
section: null,
offset: 0,
_inCard: false,
marker: null,
offsetInTextNode: 0,
_isEmpty: true,
isEqual(other) { return other._isEmpty; },
markerPosition: {}
};
}

clone() {
return new Position(this.section, this.offset);
}
Expand All @@ -67,7 +68,7 @@ const Position = class Position {
if (isTextNode(node)) {
return Position.fromTextNode(renderTree, node, offset);
} else {
return Position.fromElementNode(renderTree, node, offset);
return Position.fromElementNode(renderTree, node);
}
}

Expand All @@ -76,7 +77,7 @@ const Position = class Position {
let section, offsetInSection;

if (renderNode) {
let marker = renderNode.postNode;
const marker = renderNode.postNode;
section = marker.section;

if (!section) { throw new Error(`Could not find parent section for mapped text node "${textNode.textContent}"`); }
Expand All @@ -101,6 +102,13 @@ const Position = class Position {
}

static fromElementNode(renderTree, elementNode) {
// The browser may change the reported selection to equal the editor's root
// element if the user clicks an element that is immediately removed,
// which can happen when clicking to remove a card.
if (elementNode === renderTree.rootElement) {
return Position.emptyPosition();
}

let section, offsetInSection = 0;

section = findParentSectionFromNode(renderTree, elementNode);
Expand Down
4 changes: 4 additions & 0 deletions src/js/utils/cursor/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export default class Range {
);
}

static emptyRange() {
return new Range(Position.emptyPosition(), Position.emptyPosition());
}

/**
* @param {Markerable} section
* @return {Range} A range that is constrained to only the part that
Expand Down
Loading

0 comments on commit dca9722

Please sign in to comment.