Skip to content

Commit

Permalink
Add ListSection, ListItem, bump MOBILEDOC_VERSION -> 0.2.0
Browse files Browse the repository at this point in the history
  * simplify editor reparse
  * update post parser to reparse list sections
  * hit enter to split a list item
  * can delete to exit a list
  * hitting enter in empty list item exits list section altogether
  * Use Helpers.dom.build rather than `makeDOM`
  * Test to ensure that hitting enter in a list exits the list appropriately
  • Loading branch information
bantic committed Aug 27, 2015
1 parent a3e8212 commit 9ce308c
Show file tree
Hide file tree
Showing 31 changed files with 1,006 additions and 302 deletions.
15 changes: 6 additions & 9 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ function attemptEditorReboot(editor, textPayload) {

var MOBILEDOC_VERSION = "0.1";
var sampleMobiledocs = {
xsimpleMobiledoc: {
simpleMobiledoc: {
version: MOBILEDOC_VERSION,
sections: [
[],
Expand All @@ -341,21 +341,18 @@ var sampleMobiledocs = {
]
},

//simpleMobiledocWithList: {
simpleMobiledoc: {
simpleMobiledocWithList: {
version: MOBILEDOC_VERSION,
sections: [
[],
[
[1, "H2", [
[[], 0, "To do today:"]
]],
[1, "UL", [
[
[[], 0, "buy milk"],
[[], 0, "water cows"],
[[], 0, "world domination"]
]
[3, 'ul', [
[[[], 0, 'buy milk']],
[[[], 0, 'water plants']],
[[[], 0, 'world domination']]
]]
]
]
Expand Down
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ <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='simpleMobiledocWithList'>List example</option>
<option value='mobileDocWithInputCard'>Card with Input</option>
<option value='mobileDocWithSelfieCard'>Selfie Card</option>
</select>
Expand Down
2 changes: 2 additions & 0 deletions src/js/commands/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export default class ImageCommand extends Command {
let beforeSection = headMarker.section;
let afterSection = beforeSection.next;
let section = this.editor.builder.createCardSection('image');
// the postEditor needs to know this section's parent in order to `insertSectionBefore`
section.parent = beforeSection.parent;

this.editor.run((postEditor) => {
if (beforeSection.isBlank) {
Expand Down
90 changes: 20 additions & 70 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
} from '../utils/dom-utils';
import {
forEach,
detect
filter
} from '../utils/array-utils';
import { getData, setData } from '../utils/element-utils';
import mixin from '../utils/mixin';
Expand Down Expand Up @@ -446,78 +446,27 @@ class Editor {
}

reparse() {
// find added sections
let sectionsInDOM = [];
let newSections = [];
let previousSection;

forEach(this.element.childNodes, (node) => {
// FIXME: this is kind of slow
let sectionRenderNode = detect(this._renderTree.node.childNodes, (renderNode) => {
return renderNode.element === node;
});
if (!sectionRenderNode) {
let section = this._parser.parseSection(node);
newSections.push(section);

// create a clean "already-rendered" node to represent the fact that
// this (new) section is already in DOM
sectionRenderNode = this._renderTree.buildRenderNode(section);
sectionRenderNode.element = node;
sectionRenderNode.markClean();

let previousSectionRenderNode = previousSection && previousSection.renderNode;
this.post.sections.insertAfter(section, previousSection);
this._renderTree.node.childNodes.insertAfter(sectionRenderNode, previousSectionRenderNode);
}

// may cause duplicates to be included
let section = sectionRenderNode.postNode;
sectionsInDOM.push(section);
previousSection = section;
});

// remove deleted nodes
const deletedSections = [];
forEach(this.post.sections, (section) => {
if (!section.renderNode) {
throw new Error('All sections are expected to have a renderNode');
}

if (sectionsInDOM.indexOf(section) === -1) {
deletedSections.push(section);
}
});
forEach(deletedSections, (s) => s.renderNode.scheduleForRemoval());

// reparse the new section(s) with the cursor
// to ensure that we catch any changed html that the browser might have
// added
const sectionsWithCursor = this.cursor.activeSections;
forEach(sectionsWithCursor, (section) => {
if (newSections.indexOf(section) === -1) {
this.reparseSection(section);
}
});

let {
headSection,
headSectionOffset
} = this.cursor.sectionOffsets;
let { headSection, headSectionOffset } = this.cursor.offsets;
if (headSectionOffset === 0) {
// FIXME if the offset is 0, the user is typing the first character
// in an empty section, so we need to move the cursor 1 letter forward
headSectionOffset = 1;
}

// The cursor will lose its textNode if we have reparsed (and thus will rerender, below)
// its section. Ensure the cursor is placed where it should be after render.
//
// New sections are presumed clean, and thus do not get rerendered and lose
// their cursor position.
let resetCursor = sectionsWithCursor.indexOf(headSection) !== -1;
this._reparseCurentSection();
this._removeDetachedSections();

this.rerender();
this.trigger('update');

if (resetCursor) {
this.cursor.moveToSection(headSection, headSectionOffset);
}
this.cursor.moveToSection(headSection, headSectionOffset);
}

_removeDetachedSections() {
forEach(
filter(this.post.sections, s => !s.renderNode.isAttached()),
s => s.renderNode.scheduleForRemoval()
);
}

/*
Expand Down Expand Up @@ -571,8 +520,9 @@ class Editor {
section.renderNode.markDirty();
}

reparseSection(section) {
this._parser.reparseSection(section, this._renderTree);
_reparseCurentSection() {
const {headSection:currentSection } = this.cursor.offsets;
this._parser.reparseSection(currentSection, this._renderTree);
}

serialize() {
Expand Down
74 changes: 60 additions & 14 deletions src/js/editor/post.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { MARKUP_SECTION_TYPE } from '../models/markup-section';
import { LIST_ITEM_TYPE } from '../models/list-item';
import {
filter
filter,
compact
} from '../utils/array-utils';

import { DIRECTION } from '../utils/key';
Expand All @@ -9,9 +11,24 @@ function isMarkupSection(section) {
return section.type === MARKUP_SECTION_TYPE;
}

function isTextEditableSection(section) {
return isMarkupSection(section);
}

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

function isBlankAndLastListItem(section) {
return isListItem(section) &&
section.isBlank &&
section.parent.sections.tail.isBlank; // existing tail is blank (FIXME)
}

class PostEditor {
constructor(editor) {
this.editor = editor;
this.builder = this.editor.builder;
this._completionWorkQueue = [];
this._didRerender = false;
this._didUpdate = false;
Expand Down Expand Up @@ -99,12 +116,11 @@ class PostEditor {
}

_coalesceMarkers(section) {
let {builder} = this.editor;
filter(section.markers, m => m.isEmpty).forEach(m => {
this.removeMarker(m);
});
if (section.markers.isEmpty) {
section.markers.append(builder.createBlankMarker());
section.markers.append(this.builder.createBlankMarker());
section.renderNode.markDirty();
}
}
Expand Down Expand Up @@ -168,7 +184,7 @@ class PostEditor {
this._deleteForwardFrom({marker: nextMarker, offset: 0});
} else {
const nextSection = marker.section.next;
if (nextSection && isMarkupSection(nextSection)) {
if (nextSection && isTextEditableSection(nextSection)) {
const currentSection = marker.section;

currentSection.join(nextSection);
Expand All @@ -191,6 +207,18 @@ class PostEditor {
};
}

_convertListItemToMarkupSection(listItem) {
const listSection = listItem.parent;

const newSections = listItem.splitIntoSections();
const newMarkupSection = newSections[1];

this._replaceSection(listSection, compact(newSections));

const newCursorPosition = {marker: newMarkupSection.markers.head, offset: 0};
return newCursorPosition;
}

/**
* delete 1 character in the BACKWARD direction from the given position
* @method _deleteBackwardFrom
Expand All @@ -206,10 +234,15 @@ class PostEditor {
if (prevMarker) {
return this._deleteBackwardFrom({marker: prevMarker, offset: prevMarker.length});
} else {
const prevSection = marker.section.prev;

if (prevSection) {
if (isMarkupSection(prevSection)) {
const section = marker.section;

if (isListItem(section)) {
const newCursorPos = this._convertListItemToMarkupSection(section);
nextCursorMarker = newCursorPos.marker;
nextCursorOffset = newCursorPos.offset;
} else {
const prevSection = section.prev;
if (prevSection && isTextEditableSection(prevSection)) {
nextCursorMarker = prevSection.markers.tail;
nextCursorOffset = nextCursorMarker.length;

Expand All @@ -225,7 +258,6 @@ class PostEditor {
nextCursorOffset = 0;
}
}
// ELSE: FIXME: card section -- what should deleting into it do?
}
}

Expand Down Expand Up @@ -375,16 +407,30 @@ class PostEditor {

const [beforeSection, afterSection] = section.splitAtMarker(headMarker, headMarkerOffset);

this._replaceSection(section, [beforeSection, afterSection]);
const newSections = [beforeSection, afterSection];
let replacementSections = [beforeSection, afterSection];

if (isBlankAndLastListItem(beforeSection)) {
replacementSections.shift();
}

this._replaceSection(section, replacementSections);

this.scheduleRerender();
this.scheduleDidUpdate();

return [beforeSection, afterSection];
// FIXME we must return 2 sections because other code expects this to always return 2
return newSections;
}

_replaceSection(section, newSections) {
let nextSection = section.next;
let nextNewSection = newSections[0];
if (isMarkupSection(nextNewSection) && isListItem(section)) {
// put the new section after the ListSection (section.parent) instead of after the ListItem
nextSection = section.parent.next;
}

newSections.forEach(s => this.insertSectionBefore(s, nextSection));
this.removeSection(section);
}
Expand Down Expand Up @@ -482,8 +528,8 @@ class PostEditor {
* @public
*/
insertSectionBefore(section, beforeSection) {
this.editor.post.sections.insertBefore(section, beforeSection);
this.editor.post.renderNode.markDirty();
section.parent.sections.insertBefore(section, beforeSection);
section.parent.renderNode.markDirty();

this.scheduleRerender();
this.scheduleDidUpdate();
Expand All @@ -506,7 +552,7 @@ class PostEditor {
*/
removeSection(section) {
section.renderNode.scheduleForRemoval();
section.post.sections.remove(section);
section.parent.sections.remove(section);

this.scheduleRerender();
this.scheduleDidUpdate();
Expand Down
51 changes: 51 additions & 0 deletions src/js/models/list-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Section from './markup-section';
import LinkedList from '../utils/linked-list';

export const LIST_ITEM_TYPE = 'list-item';

export default class ListItem extends Section {
constructor(tagName, markers=[]) {
super(tagName);
this.type = LIST_ITEM_TYPE;

this.markers = new LinkedList({
adoptItem: m => m.section = m.parent = this,
freeItem: m => m.section = m.parent = null
});

markers.forEach(m => this.markers.append(m));
}

splitAtMarker(marker, offset=0) {
// FIXME need to check if we are goign to split into two list items
// or a list item and a new markup section:
const isLastItem = !this.next;
const createNewSection =
(marker.isEmpty && offset === 0) && isLastItem;

// FIXME splitAtMarker for listItem duplicates its superclass logic
let [beforeSection, afterSection] = [
this.builder.createListItem(),
createNewSection ? this.builder.createMarkupSection('p') : this.builder.createListItem()
];

// for the postEditor to know how to `insertBefore` these sections,
// they must have a parent specified
beforeSection.parent = this.parent;
afterSection.parent = createNewSection ? this.parent.parent : // the post
this.parent; // the list section

return this._redistributeMarkers(beforeSection, afterSection, marker, offset);
}

splitIntoSections() {
return this.parent.splitAtListItem(this);
}

clone() {
const item = this.builder.createListItem();
this.markers.forEach(m => item.markers.append(m.clone()));
return item;
}
}

Loading

0 comments on commit 9ce308c

Please sign in to comment.