Skip to content

Commit

Permalink
Refactor editor to delegate selection methods to Cursor
Browse files Browse the repository at this point in the history
 * `Post.insertSectionAfter` can take a null reference section, like the DOM method `insertAfter`
 * remove unused methods from cursor
  • Loading branch information
bantic committed Aug 4, 2015
1 parent 5ff0500 commit 674d399
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 193 deletions.
135 changes: 36 additions & 99 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import CardCommand from '../commands/card';

import Keycodes from '../utils/keycodes';
import {
getSelectionBlockElement,
getCursorOffsetInElement
getSelectionBlockElement
} from '../utils/selection-utils';
import EventEmitter from '../utils/event-emitter';

Expand All @@ -29,8 +28,8 @@ import MobiledocRenderer from '../renderers/mobiledoc';

import { toArray, mergeWithOptions } from 'content-kit-utils';
import {
detectParentNode,
clearChildNodes,
addClassName
} from '../utils/dom-utils';
import {
forEach
Expand All @@ -42,6 +41,8 @@ import Cursor from '../models/cursor';
import { MARKUP_SECTION_TYPE } from '../models/markup-section';
import { generateBuilder } from '../utils/post-builder';

export const EDITOR_ELEMENT_CLASS_NAME = 'ck-editor';

const defaults = {
placeholder: 'Write here...',
spellcheck: true,
Expand Down Expand Up @@ -109,16 +110,6 @@ function bindAutoTypingListeners(editor) {
});
}

function handleSelection(editor) {
return () => {
if (editor.cursor.hasSelection()) {
editor.hasSelection();
} else {
editor.hasNoSelection();
}
};
}

function bindSelectionEvent(editor) {
/**
* The following events/sequences can create a selection and are handled:
Expand All @@ -131,11 +122,16 @@ function bindSelectionEvent(editor) {
* * ctrl-click -> context menu -> click "select all"
*/

const toggleSelection = () => {
return editor.cursor.hasSelection() ? editor.hasSelection() :
editor.hasNoSelection();
};

// mouseup will not properly report a selection until the next tick, so add a timeout:
const mouseupHandler = () => setTimeout(handleSelection(editor));
const mouseupHandler = () => setTimeout(toggleSelection);
editor.addEventListener(document, 'mouseup', mouseupHandler);

const keyupHandler = handleSelection(editor);
const keyupHandler = toggleSelection;
editor.addEventListener(editor.element, 'keyup', keyupHandler);
}

Expand Down Expand Up @@ -203,7 +199,7 @@ class Editor {
this._parser = PostParser;
this._renderer = new Renderer(this, this.cards, this.unknownCardHandler, this.cardOptions);

this.applyClassName();
this.applyClassName(EDITOR_ELEMENT_CLASS_NAME);
this.applyPlaceholder();

element.spellcheck = this.spellcheck;
Expand Down Expand Up @@ -238,9 +234,7 @@ class Editor {
showForTag: 'a'
}));

if (this.autofocus) {
element.focus();
}
if (this.autofocus) { element.focus(); }
}

addView(view) {
Expand Down Expand Up @@ -271,6 +265,8 @@ class Editor {

rerender() {
let postRenderNode = this.post.renderNode;

// if we haven't rendered this renderNode before, mark it dirty
if (!postRenderNode.element) {
postRenderNode.element = this.element;
postRenderNode.markDirty();
Expand Down Expand Up @@ -432,11 +428,6 @@ class Editor {
this.trigger('update');
}

getActiveMarkers() {
const cursor = this.cursor;
return cursor.activeMarkers;
}

getActiveSections() {
const cursor = this.cursor;
return cursor.activeSections;
Expand All @@ -452,23 +443,8 @@ class Editor {
return blockElements.indexOf(selectionEl);
}

getCursorIndexInCurrentBlock() {
var currentBlock = getSelectionBlockElement();
if (currentBlock) {
return getCursorOffsetInElement(currentBlock);
}
return -1;
}

applyClassName() {
var editorClassName = 'ck-editor';
var editorClassNameRegExp = new RegExp(editorClassName);
var existingClassName = this.element.className;

if (!editorClassNameRegExp.test(existingClassName)) {
existingClassName += (existingClassName ? ' ' : '') + editorClassName;
}
this.element.className = existingClassName;
applyClassName(className) {
addClassName(this.element, className);
}

applyPlaceholder() {
Expand Down Expand Up @@ -518,38 +494,35 @@ class Editor {
sectionRenderNode.element = node;
sectionRenderNode.markClean();

if (previousSection) {
// insert after existing section
this.post.insertSectionAfter(section, previousSection);
this._renderTree.node.insertAfter(sectionRenderNode, previousSection.renderNode);
} else {
// prepend at beginning (first section)
this.post.prependSection(section);
this._renderTree.node.insertAfter(sectionRenderNode, null);
}
let previousSectionRenderNode = previousSection && previousSection.renderNode;
this.post.insertSectionAfter(section, previousSection);
this._renderTree.node.insertAfter(sectionRenderNode, previousSectionRenderNode);
}

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

// remove deleted nodes
let i;
for (i=this.post.sections.length-1;i>=0;i--) {
let section = this.post.sections[i];
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) {
if (section.renderNode) {
section.renderNode.scheduleForRemoval();
} else {
throw new Error('All sections are expected to have a renderNode');
}
deletedSections.push(section);
}
}
});
forEach(deletedSections, (s) => s.renderNode.scheduleForRemoval());

// reparse the section(s) with the cursor
const sectionsWithCursor = this.getSectionsWithCursor();
sectionsWithCursor.forEach((section) => {
// 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);
}
Expand All @@ -567,7 +540,6 @@ class Editor {
//
// New sections are presumed clean, and thus do not get rerendered and lose
// their cursor position.
//
let resetCursor = (leftRenderNode &&
sectionsWithCursor.indexOf(leftRenderNode.postNode.section) !== -1);

Expand Down Expand Up @@ -598,41 +570,6 @@ class Editor {
}
}

getSectionsWithCursor() {
return this.getRenderNodesWithCursor().map( renderNode => {
return renderNode.postNode;
});
}

getRenderNodesWithCursor() {
const selection = document.getSelection();
if (selection.rangeCount === 0) {
return null;
}

const range = selection.getRangeAt(0);

let { startContainer:startElement, endContainer:endElement } = range;

let getElementRenderNode = (e) => {
let node = this._renderTree.getElementRenderNode(e);
if (node && node.postNode.type === MARKUP_SECTION_TYPE) {
return node;
}
};
let { result:startRenderNode } = detectParentNode(startElement, getElementRenderNode);
let { result:endRenderNode } = detectParentNode(endElement, getElementRenderNode);

let nodes = [];
let node = startRenderNode;
while (node && (!endRenderNode.nextSibling || endRenderNode.nextSibling !== node)) {
nodes.push(node);
node = node.nextSibling;
}

return nodes;
}

reparseSection(section) {
this._parser.reparseSection(section, this._renderTree);
}
Expand All @@ -648,7 +585,7 @@ class Editor {

insertSectionAtCursor(newSection) {
let newRenderNode = this._renderTree.buildRenderNode(newSection);
let renderNodes = this.getRenderNodesWithCursor();
let renderNodes = this.cursor.activeSections.map(s => s.renderNode);
let lastRenderNode = renderNodes[renderNodes.length-1];
lastRenderNode.parentNode.insertAfter(newRenderNode, lastRenderNode);
this.post.insertSectionAfter(newSection, lastRenderNode.postNode);
Expand Down
74 changes: 4 additions & 70 deletions src/js/models/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ import {
} from '../utils/selection-utils';

import {
detectParentNode,
containsNode,
walkTextNodes
detectParentNode
} from '../utils/dom-utils';

const Cursor = class Cursor {
export default class Cursor {
constructor(editor) {
this.editor = editor;
this.renderTree = editor._renderTree;
Expand All @@ -33,13 +31,6 @@ const Cursor = class Cursor {
return window.getSelection();
}

/**
* the offset from the left edge of the section
*/
get leftOffset() {
return this.offsets.leftOffset;
}

get offsets() {
let leftNode, rightNode,
leftOffset, rightOffset;
Expand Down Expand Up @@ -73,58 +64,6 @@ const Cursor = class Cursor {
};
}

get activeMarkers() {
const firstSection = this.activeSections[0];
if (!firstSection) { return []; }
const firstSectionElement = firstSection.renderNode.element;

const {
leftNode, rightNode,
leftOffset, rightOffset
} = this.offsets;

let textLeftOffset = 0,
textRightOffset = 0,
foundLeft = false,
foundRight = false;

walkTextNodes(firstSectionElement, (textNode) => {
let textLength = textNode.textContent.length;

if (!foundLeft) {
if (containsNode(leftNode, textNode)) {
textLeftOffset += leftOffset;
foundLeft = true;
} else {
textLeftOffset += textLength;
}
}
if (!foundRight) {
if (containsNode(rightNode, textNode)) {
textRightOffset += rightOffset;
foundRight = true;
} else {
textRightOffset += textLength;
}
}
});

// get section element
// walk it until we find one containing the left node, adding up textContent length along the way
// add the selection offset in the left node -- this is the offset in the parent textContent
// repeat for right node (subtract the remaining chars after selection offset) -- this is the end offset
//
// walk the section's markers, adding up length. Each marker with length >= offset and <= end offset is active

const leftMarker = firstSection.markerContaining(textLeftOffset, true);
const rightMarker = firstSection.markerContaining(textRightOffset, false);

const leftMarkerIndex = firstSection.markers.indexOf(leftMarker),
rightMarkerIndex = firstSection.markers.indexOf(rightMarker) + 1;

return firstSection.markers.slice(leftMarkerIndex, rightMarkerIndex);
}

get activeSections() {
const { sections } = this.post;
const selection = this.selection;
Expand All @@ -135,9 +74,7 @@ const Cursor = class Cursor {

const { startContainer, endContainer } = range;
const isSectionElement = (element) => {
return detect(sections, (section) => {
return section.renderNode.element === element;
});
return detect(sections, (s) => s.renderNode.element === element);
};
const {result:startSection} = detectParentNode(startContainer, isSectionElement);
const {result:endSection} = detectParentNode(endContainer, isSectionElement);
Expand Down Expand Up @@ -174,7 +111,4 @@ const Cursor = class Cursor {
}
selection.addRange(r);
}
};

export default Cursor;

}
12 changes: 7 additions & 5 deletions src/js/models/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ export default class Post {
this.removeSection(section);
}
insertSectionAfter(section, previousSection) {
var i, l;
for (i=0,l=this.sections.length;i<l;i++) {
let foundIndex = -1;

for (let i=0; i<this.sections.length; i++) {
if (this.sections[i] === previousSection) {
this.sections.splice(i+1, 0, section);
return;
foundIndex = i;
break;
}
}
throw new Error('Previous section was not found in post.sections');

this.sections.splice(foundIndex+1, 0, section);
}
removeSection(section) {
var i, l;
Expand Down
Loading

0 comments on commit 674d399

Please sign in to comment.