From 356468bca0a66e44ce926d8227f1d305b4c2b82f Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Wed, 12 Aug 2015 15:55:44 -0400 Subject: [PATCH 1/6] Refactor some method into public postEditor methods --- src/js/commands/bold.js | 12 +- src/js/commands/italic.js | 12 +- src/js/editor/editor.js | 231 +++++---------------------------- src/js/editor/post.js | 262 ++++++++++++++++++++++++++++++++++++++ src/js/models/cursor.js | 8 +- 5 files changed, 320 insertions(+), 205 deletions(-) create mode 100644 src/js/editor/post.js diff --git a/src/js/commands/bold.js b/src/js/commands/bold.js index 969b3865b..5ab21cad4 100644 --- a/src/js/commands/bold.js +++ b/src/js/commands/bold.js @@ -14,10 +14,18 @@ export default class BoldCommand extends TextFormatCommand { this.markup = builder.createMarkup('strong'); } exec() { - this.editor.applyMarkupToSelection(this.markup); + let markerRange = this.editor.cursor.offsets; + let markers = this.editor.run((postEditor) => { + return postEditor.applyMarkupToMarkers(markerRange, this.markup); + }); + this.editor.selectMarkers(markers); } unexec() { - this.editor.removeMarkupFromSelection(this.markup); + let markerRange = this.editor.cursor.offsets; + let markers = this.editor.run((postEditor) => { + return postEditor.removeMarkupFromMarkers(markerRange, this.markup); + }); + this.editor.selectMarkers(markers); } isActive() { return any(this.editor.activeMarkers, m => m.hasMarkup(this.markup)); diff --git a/src/js/commands/italic.js b/src/js/commands/italic.js index ba1a7ec06..62ea968d0 100644 --- a/src/js/commands/italic.js +++ b/src/js/commands/italic.js @@ -14,10 +14,18 @@ export default class ItalicCommand extends TextFormatCommand { this.markup = builder.createMarkup('em'); } exec() { - this.editor.applyMarkupToSelection(this.markup); + let markerRange = this.editor.cursor.offsets; + let markers = this.editor.run((postEditor) => { + return postEditor.applyMarkupToMarkers(markerRange, this.markup); + }); + this.editor.selectMarkers(markers); } unexec() { - this.editor.removeMarkupFromSelection(this.markup); + let markerRange = this.editor.cursor.offsets; + let markers = this.editor.run((postEditor) => { + return postEditor.removeMarkupFromMarkers(markerRange, this.markup); + }); + this.editor.selectMarkers(markers); } isActive() { return any(this.editor.activeMarkers, m => m.hasMarkup(this.markup)); diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js index 1b44568dc..0d398dceb 100644 --- a/src/js/editor/editor.js +++ b/src/js/editor/editor.js @@ -1,6 +1,7 @@ import TextFormatToolbar from '../views/text-format-toolbar'; import Tooltip from '../views/tooltip'; import EmbedIntent from '../views/embed-intent'; +import PostEditor from './post'; import ReversibleToolbarButton from '../views/reversible-toolbar-button'; import BoldCommand from '../commands/bold'; @@ -17,9 +18,6 @@ import CardCommand from '../commands/card'; import ImageCard from '../cards/image'; import Key from '../utils/key'; -import { - getSelectionBlockElement -} from '../utils/selection-utils'; import EventEmitter from '../utils/event-emitter'; import MobiledocParser from "../parsers/mobiledoc"; @@ -32,7 +30,7 @@ import { import RenderTree from 'content-kit-editor/models/render-tree'; import MobiledocRenderer from '../renderers/mobiledoc'; -import { toArray, mergeWithOptions } from 'content-kit-utils'; +import { mergeWithOptions } from 'content-kit-utils'; import { clearChildNodes, addClassName @@ -44,7 +42,6 @@ import { getData, setData } from '../utils/element-utils'; import mixin from '../utils/mixin'; import EventListenerMixin from '../utils/event-listener'; import Cursor from '../models/cursor'; -import { MARKUP_SECTION_TYPE } from '../models/markup-section'; import PostNodeBuilder from '../models/post-node-builder'; export const EDITOR_ELEMENT_CLASS_NAME = 'ck-editor'; @@ -150,10 +147,13 @@ function bindKeyListeners(editor) { editor.handleDeletion(event); event.preventDefault(); } else if (key.isEnter()) { - editor.handleNewline(event); + editor.handleNewline(event); } else if (key.isPrintable()) { if (editor.cursor.hasSelection()) { - editor.deleteSelection(event, {preventDefault:false}); + let result = editor.run((postEditor) => { + return postEditor.deleteRange(editor.cursor.offsets); + }); + editor.cursor.moveToMarker(result.currentMarker, result.currentOffset); } } }); @@ -311,124 +311,21 @@ class Editor { this._renderer.render(this._renderTree); } - deleteSelection(event, options={preventDefault:true}) { - if (options.preventDefault) { - event.preventDefault(); - } - - // types of selection deletion: - // * a selection starts at the beginning of a section - // -- cursor should end up at the beginning of that section - // -- if the section not longer has markers, add a blank one for the cursor to focus on - // * a selection is entirely within a section - // -- split the markers with the selection, remove those new markers from their section - // -- cursor goes at end of the marker before the selection start, or if the - // -- selection was at the start of the section, cursor goes at section start - // * a selection crosses multiple sections - // -- remove all the sections that are between (exclusive ) selection start and end - // -- join the start and end sections - // -- mark the end section for removal - // -- cursor goes at end of marker before the selection start - - const markers = this.splitMarkersFromSelection(); - - const {changedSections, removedSections, currentMarker, currentOffset} = this.post.cutMarkers(markers); - - changedSections.forEach(section => section.renderNode.markDirty()); - removedSections.forEach(section => section.renderNode.scheduleForRemoval()); - - this.rerender(); - - let currentTextNode = currentMarker.renderNode.element; - this.cursor.moveToNode(currentTextNode, currentOffset); - - this.trigger('update'); - } - - // FIXME ensure we handle deletion when there is a selection handleDeletion(event) { - let { - leftRenderNode, - leftOffset - } = this.cursor.offsets; - - // need to handle these cases: - // when cursor is: - // * A in the middle of a marker -- just delete the character - // * B offset is 0 and there is a previous marker - // * delete last char of previous marker - // * C offset is 0 and there is no previous marker - // * join this section with previous section - - if (this.cursor.hasSelection()) { - this.deleteSelection(event); - return; - } + event.preventDefault(); - const currentMarker = leftRenderNode.postNode; - let nextCursorMarker = currentMarker; - let nextCursorOffset = leftOffset - 1; - - // A: in the middle of a marker - if (leftOffset !== 0) { - currentMarker.deleteValueAtOffset(leftOffset-1); - if (currentMarker.length === 0 && currentMarker.section.markers.length > 1) { - leftRenderNode.scheduleForRemoval(); - - let isFirstRenderNode = leftRenderNode === leftRenderNode.parent.childNodes.head; - if (isFirstRenderNode) { - // move cursor to start of next node - nextCursorMarker = leftRenderNode.next.postNode; - nextCursorOffset = 0; - } else { - // move cursor to end of prev node - nextCursorMarker = leftRenderNode.prev.postNode; - nextCursorOffset = leftRenderNode.prev.postNode.length; - } + this.run((postEditor) => { + if (this.cursor.hasSelection()) { + postEditor.deleteRange(this.cursor.offsets); } else { - leftRenderNode.markDirty(); + let { + headMarker: marker, + headOffset: offset + } = this.cursor.offsets; + // FIXME: perhaps this should accept this.cursor.offsets? + postEditor.deleteCharAt(marker, offset-1); } - } else { - let currentSection = currentMarker.section; - let previousMarker = currentMarker.prev; - if (previousMarker) { // (B) - let markerLength = previousMarker.length; - previousMarker.deleteValueAtOffset(markerLength - 1); - } else { // (C) - // possible previous sections: - // * none -- do nothing - // * markup section -- join to it - // * non-markup section (card) -- select it? delete it? - let previousSection = currentSection.prev; - if (previousSection) { - let isMarkupSection = previousSection.type === MARKUP_SECTION_TYPE; - - if (isMarkupSection) { - let lastPreviousMarker = previousSection.markers.tail; - previousSection.join(currentSection); - previousSection.renderNode.markDirty(); - currentSection.renderNode.scheduleForRemoval(); - - nextCursorMarker = lastPreviousMarker.next; - nextCursorOffset = 0; - /* - } else { - // card section: ?? - */ - } - } else { // no previous section -- do nothing - nextCursorMarker = currentMarker; - nextCursorOffset = 0; - } - } - } - - this.rerender(); - - this.cursor.moveToNode(nextCursorMarker.renderNode.element, - nextCursorOffset); - - this.trigger('update'); + }); } handleNewline(event) { @@ -499,57 +396,18 @@ class Editor { this.hasSelection(); } - /* - * @return {Array} of markers that are "inside the split" - */ - splitMarkersFromSelection() { - const { - startMarker, - leftOffset:startMarkerOffset, - endMarker, - rightOffset:endMarkerOffset, - startSection, - endSection - } = this.cursor.offsets; - - let selectedMarkers = []; - - startMarker.renderNode.scheduleForRemoval(); - endMarker.renderNode.scheduleForRemoval(); - - if (startMarker === endMarker) { - let newMarkers = startSection.splitMarker( - startMarker, startMarkerOffset, endMarkerOffset - ); - selectedMarkers = this.markersInOffset(newMarkers, startMarkerOffset, endMarkerOffset); - } else { - let newStartMarkers = startSection.splitMarker(startMarker, startMarkerOffset); - let selectedStartMarkers = this.markersInOffset(newStartMarkers, startMarkerOffset); - - let newEndMarkers = endSection.splitMarker(endMarker, endMarkerOffset); - let selectedEndMarkers = this.markersInOffset(newEndMarkers, 0, endMarkerOffset); - - let newStartMarker = selectedStartMarkers[0], - newEndMarker = selectedEndMarkers[selectedEndMarkers.length - 1]; - - this.post.markersFrom(newStartMarker, newEndMarker, m => selectedMarkers.push(m)); - } - - return selectedMarkers; - } - - markersInOffset(markers, startOffset, endOffset) { + markersInRange({headMarker, headOffset, tailMarker, tailOffset}) { let offset = 0; let foundMarkers = []; - let toEnd = endOffset === undefined; - if (toEnd) { endOffset = 0; } + let toEnd = tailOffset === undefined; + if (toEnd) { tailOffset = 0; } - markers.forEach(marker => { + this.post.markersFrom(headMarker, tailMarker, marker => { if (toEnd) { - endOffset += marker.length; + tailOffset += marker.length; } - if (offset >= startOffset && offset < endOffset) { + if (offset >= headOffset && offset < tailOffset) { foundMarkers.push(marker); } @@ -559,30 +417,6 @@ class Editor { return foundMarkers; } - applyMarkupToSelection(markup) { - const markers = this.splitMarkersFromSelection(); - markers.forEach(marker => { - marker.addMarkup(markup); - marker.section.renderNode.markDirty(); - }); - - this.rerender(); - this.selectMarkers(markers); - this.didUpdate(); - } - - removeMarkupFromSelection(markup) { - const markers = this.splitMarkersFromSelection(); - markers.forEach(marker => { - marker.removeMarkup(markup); - marker.section.renderNode.markDirty(); - }); - - this.rerender(); - this.selectMarkers(markers); - this.didUpdate(); - } - selectMarkers(markers) { this.cursor.selectMarkers(markers); this.hasSelection(); @@ -592,12 +426,6 @@ class Editor { return new Cursor(this); } - getCurrentBlockIndex() { - var selectionEl = this.element || getSelectionBlockElement(); - var blockElements = toArray(this.element.children); - return blockElements.indexOf(selectionEl); - } - applyClassName(className) { addClassName(this.element, className); } @@ -725,10 +553,6 @@ class Editor { } } - get cursorSelection() { - return this.cursor.cursorSelection; - } - /* * Returns the active sections. If the cursor selection is collapsed this will be * an array of 1 item. Else will return an array containing each section that is either @@ -805,6 +629,13 @@ class Editor { this.removeAllEventListeners(); this.removeAllViews(); } + + run(callback) { + let postEditor = new PostEditor(this); + let result = callback(postEditor); + postEditor.complete(); + return result; + } } mixin(Editor, EventEmitter); diff --git a/src/js/editor/post.js b/src/js/editor/post.js new file mode 100644 index 000000000..cb87c646b --- /dev/null +++ b/src/js/editor/post.js @@ -0,0 +1,262 @@ +import { MARKUP_SECTION_TYPE } from '../models/markup-section'; +class PostEditor { + constructor(editor) { + this.editor = editor; + this._completionWorkQueue = []; + this._didRerender = false; + this._didUpdate = false; + this._didComplete = false; + } + + // types of selection deletion: + // * a selection starts at the beginning of a section + // -- cursor should end up at the beginning of that section + // -- if the section not longer has markers, add a blank one for the cursor to focus on + // * a selection is entirely within a section + // -- split the markers with the selection, remove those new markers from their section + // -- cursor goes at end of the marker before the selection start, or if the + // -- selection was at the start of the section, cursor goes at section start + // * a selection crosses multiple sections + // -- remove all the sections that are between (exclusive ) selection start and end + // -- join the start and end sections + // -- mark the end section for removal + // -- cursor goes at end of marker before the selection start + deleteRange(rangeMarkers) { + // rangeMarkers should be akin to this.cursor.offset + const markers = this.splitMarkers(rangeMarkers); + + const { + changedSections, + removedSections, + currentMarker, + currentOffset + } = this.editor.post.cutMarkers(markers); + + changedSections.forEach(section => section.renderNode.markDirty()); + removedSections.forEach(section => section.renderNode.scheduleForRemoval()); + + this.didUpdate(); + this.rerender(); + this.schedule(() => { + // FIXME the cursor API should accept markers, not elements. Or there + // should be a proxy method on editor + let currentTextNode = currentMarker.renderNode.element; + this.editor.cursor.moveToNode(currentTextNode, currentOffset); + }); + } + + // need to handle these cases: + // when cursor is: + // * A in the middle of a marker -- just delete the character + // * B offset is 0 and there is a previous marker + // * delete last char of previous marker + // * C offset is 0 and there is no previous marker + // * join this section with previous section + deleteCharAt(marker, offset) { + const currentMarker = marker; + let nextCursorMarker = currentMarker; + let nextCursorOffset = offset; + let renderNode = marker.renderNode; + + // A: in the middle of a marker + if (offset >= 0) { + currentMarker.deleteValueAtOffset(offset); + if (currentMarker.length === 0 && currentMarker.section.markers.length > 1) { + if (marker.renderNode) { + marker.renderNode.scheduleForRemoval(); + } + + let isFirstRenderNode = renderNode === renderNode.parent.childNodes.head; + if (isFirstRenderNode) { + // move cursor to start of next node + nextCursorMarker = renderNode.next.postNode; + nextCursorOffset = 0; + } else { + // move cursor to end of prev node + nextCursorMarker = renderNode.prev.postNode; + nextCursorOffset = renderNode.prev.postNode.length; + } + } else { + renderNode.markDirty(); + } + } else { + let currentSection = currentMarker.section; + let previousMarker = currentMarker.prev; + if (previousMarker) { // (B) + let markerLength = previousMarker.length; + previousMarker.deleteValueAtOffset(markerLength - 1); + } else { // (C) + // possible previous sections: + // * none -- do nothing + // * markup section -- join to it + // * non-markup section (card) -- select it? delete it? + let previousSection = currentSection.prev; + if (previousSection) { + let isMarkupSection = previousSection.type === MARKUP_SECTION_TYPE; + + if (isMarkupSection) { + let lastPreviousMarker = previousSection.markers.tail; + previousSection.join(currentSection); + previousSection.renderNode.markDirty(); + currentSection.renderNode.scheduleForRemoval(); + + nextCursorMarker = lastPreviousMarker.next; + nextCursorOffset = 0; + /* + } else { + // card section: ?? + */ + } + } else { // no previous section -- do nothing + nextCursorMarker = currentMarker; + nextCursorOffset = 0; + } + } + } + + this.didUpdate(); + this.rerender(); + this.schedule(() => { + let nextElement = nextCursorMarker.renderNode.element; + this.editor.cursor.moveToNode( + nextElement, + nextCursorOffset + ); + }); + } + + /* + * @return {Array} of markers that are "inside the split" + */ + splitMarkers({headMarker, headOffset, tailMarker, tailOffset}) { + let selectedMarkers = []; + + let headSection = headMarker.section; + let tailSection = tailMarker.section; + + // These render will be removed by the split functions. Mark them + // for removal before doing that. FIXME this seems prime for + // refactoring onto the postEditor as a split function + headMarker.renderNode.scheduleForRemoval(); + headMarker.renderNode.scheduleForRemoval(); + headMarker.section.renderNode.markDirty(); + headMarker.section.renderNode.markDirty(); + + if (headMarker === tailMarker) { + let markers = headSection.splitMarker(headMarker, headOffset, tailOffset); + selectedMarkers = this.editor.markersInRange({ + headMarker: markers[0], + tailMarker: markers[markers.length-1], + headOffset, + tailOffset + }); + } else { + let newHeadMarkers = headSection.splitMarker(headMarker, headOffset); + let selectedHeadMarkers = this.editor.markersInRange({ + headMarker: newHeadMarkers[0], + tailMarker: newHeadMarkers[newHeadMarkers.length-1], + headOffset + }); + + let newTailMarkers = tailSection.splitMarker(tailMarker, tailOffset); + let selectedTailMarkers = this.editor.markersInRange({ + headMarker: newTailMarkers[0], + tailMarker: newTailMarkers[newTailMarkers.length-1], + headOffset: 0, + tailOffset + }); + + let newHeadMarker = selectedHeadMarkers[0], + newTailMarker = selectedTailMarkers[selectedTailMarkers.length - 1]; + + this.editor.post.markersFrom(newHeadMarker, newTailMarker, m => { + selectedMarkers.push(m); + }); + } + + this.didUpdate(); + this.rerender(); + + return selectedMarkers; + } + + applyMarkupToMarkers(markerRange, markup) { + const markers = this.splitMarkers(markerRange); + markers.forEach(marker => { + marker.addMarkup(markup); + marker.section.renderNode.markDirty(); + }); + + this.rerender(); + this.didUpdate(); + + return markers; + } + + removeMarkupFromMarkers(markerRange, markup) { + const markers = this.splitMarkers(markerRange); + markers.forEach(marker => { + marker.removeMarkup(markup); + marker.section.renderNode.markDirty(); + }); + + this.rerender(); + this.didUpdate(); + + return markers; + } + + + /** + * A method for adding work the deferred queue + * + */ + schedule(callback) { + if (this._didComplete) { + throw new Error('Work can only be scheduled before a post edit has completed'); + } + this._completionWorkQueue.push(callback); + } + + /** + * Add a rerender job to the queue + * + */ + rerender() { + this.schedule(() => { + if (!this._didRerender) { + this._didRerender = true; + this.editor.rerender(); + } + }); + } + + /** + * Add an update notice job to the queue + * + */ + didUpdate() { + this.schedule(() => { + if (!this._didUpdate) { + this._didUpdate = true; + this.editor.didUpdate(); + } + }); + } + + /** + * Flush the work queue + * + */ + complete() { + if (this._didComplete) { + throw new Error('Post editing can only be completed once'); + } + this._didComplete = true; + this._completionWorkQueue.forEach(callback => { + callback(); + }); + } +} + +export default PostEditor; diff --git a/src/js/models/cursor.js b/src/js/models/cursor.js index d26c001ec..07b4106f3 100644 --- a/src/js/models/cursor.js +++ b/src/js/models/cursor.js @@ -95,7 +95,13 @@ export default class Cursor { startMarker, endMarker, startSection, - endSection + endSection, + + // FIXME: this should become the public API + headMarker: startMarker, + tailMarker: endMarker, + headOffset: leftOffset, + tailOffset: rightOffset }; } From b4db5041421ace62cf197b29de3cd4d818aac428 Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Wed, 12 Aug 2015 17:56:24 -0400 Subject: [PATCH 2/6] Refactor image card to use postEditor --- src/js/commands/image.js | 18 ++++++++++-------- src/js/editor/editor.js | 10 ---------- src/js/editor/post.js | 14 ++++++++++++++ src/js/models/markup-section.js | 10 ++++++++-- src/js/models/post.js | 2 +- tests/unit/models/markup-section-test.js | 18 ++++++++++++++++++ 6 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/js/commands/image.js b/src/js/commands/image.js index 3557f9d77..97c1d2a5f 100644 --- a/src/js/commands/image.js +++ b/src/js/commands/image.js @@ -9,14 +9,16 @@ export default class ImageCommand extends Command { } exec() { - let {post, builder} = this.editor; - let sections = this.editor.activeSections; - let lastSection = sections[sections.length - 1]; - let section = builder.createCardSection('image'); - post.sections.insertAfter(section, lastSection); - sections.forEach(section => section.renderNode.scheduleForRemoval()); + let {headMarker} = this.editor.cursor.offsets; + let beforeSection = headMarker.section; + let afterSection = beforeSection.next; + let section = this.editor.builder.createCardSection('image'); - this.editor.rerender(); - this.editor.didUpdate(); + this.editor.run((postEditor) => { + if (beforeSection.isBlank) { + postEditor.removeSection(beforeSection); + } + postEditor.insertSectionBefore(section, afterSection); + }); } } diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js index 0d398dceb..26d2eee02 100644 --- a/src/js/editor/editor.js +++ b/src/js/editor/editor.js @@ -615,16 +615,6 @@ class Editor { this._views = []; } - insertSectionAtCursor(newSection) { - let newRenderNode = this._renderTree.buildRenderNode(newSection); - let renderNodes = this.cursor.activeSections.map(s => s.renderNode); - let lastRenderNode = renderNodes[renderNodes.length-1]; - lastRenderNode.parent.childNodes.insertAfter(newRenderNode, lastRenderNode); - this.post.sections.insertAfter(newSection, lastRenderNode.postNode); - renderNodes.forEach(renderNode => renderNode.scheduleForRemoval()); - this.trigger('update'); - } - destroy() { this.removeAllEventListeners(); this.removeAllViews(); diff --git a/src/js/editor/post.js b/src/js/editor/post.js index cb87c646b..e1ee9d48d 100644 --- a/src/js/editor/post.js +++ b/src/js/editor/post.js @@ -206,6 +206,20 @@ class PostEditor { return markers; } + insertSectionBefore(section, beforeSection) { + this.editor.post.sections.insertBefore(section, beforeSection); + this.editor.post.renderNode.markDirty(); + + this.rerender(); + this.didUpdate(); + } + + removeSection(section) { + section.renderNode.scheduleForRemoval(); + + this.rerender(); + this.didUpdate(); + } /** * A method for adding work the deferred queue diff --git a/src/js/models/markup-section.js b/src/js/models/markup-section.js index 5dc09994c..b3c139a3d 100644 --- a/src/js/models/markup-section.js +++ b/src/js/models/markup-section.js @@ -41,8 +41,14 @@ export default class Section extends LinkedItem { return this._tagName; } - get isEmpty() { - return this.markers.isEmpty; + get isBlank() { + if (!this.markers.length) { + return true; + } + let markerWithLength = this.markers.detect((marker) => { + return !!marker.length; + }); + return !markerWithLength; } setTagName(newTagName) { diff --git a/src/js/models/post.js b/src/js/models/post.js index 14d61aa8f..b420f153f 100644 --- a/src/js/models/post.js +++ b/src/js/models/post.js @@ -38,7 +38,7 @@ export default class Post { // add a blank marker to any sections that are now empty changedSections.forEach(section => { - if (section.isEmpty) { + if (section.markers.isEmpty) { section.markers.append(this.builder.createBlankMarker()); } }); diff --git a/tests/unit/models/markup-section-test.js b/tests/unit/models/markup-section-test.js index 80a40f9d0..b1295778d 100644 --- a/tests/unit/models/markup-section-test.js +++ b/tests/unit/models/markup-section-test.js @@ -149,3 +149,21 @@ test('#coalesceMarkers appends a single blank marker if all the markers were bla assert.equal(s.markers.length, 1, 'has 1 marker after coalescing'); assert.ok(s.markers.head.isEmpty, 'remaining marker is empty'); }); + +test('#isBlank returns true if the text length is zero for two markers', (assert) => { + const m1 = builder.createBlankMarker(); + const m2 = builder.createBlankMarker(); + const s = builder.createMarkupSection('p', [m1,m2]); + assert.ok(s.isBlank, 'section with two blank markers is blank'); +}); + +test('#isBlank returns true if there are no markers', (assert) => { + const s = builder.createMarkupSection('p'); + assert.ok(s.isBlank, 'section with no markers is blank'); +}); + +test('#isBlank returns false if there is a marker with length', (assert) => { + const m = builder.createMarker('a'); + const s = builder.createMarkupSection('p', [m]); + assert.ok(!s.isBlank, 'section with marker is not blank'); +}); From 9a5c62e5e96a33a659ec1e57c0bcface61f45ea1 Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Mon, 17 Aug 2015 12:22:59 -0400 Subject: [PATCH 3/6] Refactor newline insertion to use postEditor --- src/js/commands/bold.js | 3 ++ src/js/commands/italic.js | 3 ++ src/js/editor/editor.js | 57 ++++++++++------------ src/js/editor/post.js | 57 +++++++++++++++------- src/js/models/cursor.js | 18 +++---- tests/acceptance/editor-selections-test.js | 31 ++++++++++++ 6 files changed, 110 insertions(+), 59 deletions(-) diff --git a/src/js/commands/bold.js b/src/js/commands/bold.js index 5ab21cad4..a669148e2 100644 --- a/src/js/commands/bold.js +++ b/src/js/commands/bold.js @@ -15,6 +15,9 @@ export default class BoldCommand extends TextFormatCommand { } exec() { let markerRange = this.editor.cursor.offsets; + if (!markerRange.leftRenderNode || !markerRange.rightRenderNode) { + return; + } let markers = this.editor.run((postEditor) => { return postEditor.applyMarkupToMarkers(markerRange, this.markup); }); diff --git a/src/js/commands/italic.js b/src/js/commands/italic.js index 62ea968d0..b3618a3e7 100644 --- a/src/js/commands/italic.js +++ b/src/js/commands/italic.js @@ -15,6 +15,9 @@ export default class ItalicCommand extends TextFormatCommand { } exec() { let markerRange = this.editor.cursor.offsets; + if (!markerRange.leftRenderNode || !markerRange.rightRenderNode) { + return; + } let markers = this.editor.run((postEditor) => { return postEditor.applyMarkupToMarkers(markerRange, this.markup); }); diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js index 26d2eee02..bbe91a16b 100644 --- a/src/js/editor/editor.js +++ b/src/js/editor/editor.js @@ -314,53 +314,46 @@ class Editor { handleDeletion(event) { event.preventDefault(); + let offsets = this.cursor.offsets; + let currentMarker, currentOffset; this.run((postEditor) => { + let results; if (this.cursor.hasSelection()) { - postEditor.deleteRange(this.cursor.offsets); + results = postEditor.deleteRange(offsets); } else { - let { - headMarker: marker, - headOffset: offset - } = this.cursor.offsets; // FIXME: perhaps this should accept this.cursor.offsets? - postEditor.deleteCharAt(marker, offset-1); + results = postEditor.deleteCharAt(offsets.headMarker, offsets.headOffset-1); } + currentMarker = results.currentMarker; + currentOffset = results.currentOffset; }); + this.cursor.moveToMarker(currentMarker, currentOffset); } handleNewline(event) { - if (this.cursor.hasSelection()) { - this.handleDeletion(event); - } - - const { - leftRenderNode, - rightRenderNode, - leftOffset - } = this.cursor.offsets; + let offsets = this.cursor.offsets; // if there's no left/right nodes, we are probably not in the editor, // or we have selected some non-marker thing like a card - if (!leftRenderNode || !rightRenderNode) { return; } + if (!offsets.leftRenderNode || !offsets.rightRenderNode) { + return; + } event.preventDefault(); - const markerRenderNode = leftRenderNode; - const marker = markerRenderNode.postNode; - const section = marker.section; - - let [beforeSection, afterSection] = section.splitAtMarker(marker, leftOffset); - - section.renderNode.scheduleForRemoval(); - - this.post.sections.insertAfter(beforeSection, section); - this.post.sections.insertAfter(afterSection, beforeSection); - this.post.sections.remove(section); - - this.rerender(); - this.trigger('update'); - - this.cursor.moveToSection(afterSection); + let cursorSection; + this.run((postEditor) => { + let offsetAfterDeletion; + if (this.cursor.hasSelection()) { + let result = postEditor.deleteRange(offsets); + offsetAfterDeletion = { + headMarker: result.currentMarker, + headOffset: result.currentOffset + }; + } + cursorSection = postEditor.splitSection(offsetAfterDeletion || offsets)[1]; + }); + this.cursor.moveToSection(cursorSection); } hasSelection() { diff --git a/src/js/editor/post.js b/src/js/editor/post.js index e1ee9d48d..8a92be22d 100644 --- a/src/js/editor/post.js +++ b/src/js/editor/post.js @@ -35,14 +35,13 @@ class PostEditor { changedSections.forEach(section => section.renderNode.markDirty()); removedSections.forEach(section => section.renderNode.scheduleForRemoval()); - this.didUpdate(); this.rerender(); - this.schedule(() => { - // FIXME the cursor API should accept markers, not elements. Or there - // should be a proxy method on editor - let currentTextNode = currentMarker.renderNode.element; - this.editor.cursor.moveToNode(currentTextNode, currentOffset); - }); + this.didUpdate(); + + return { + currentMarker, + currentOffset + }; } // need to handle these cases: @@ -114,15 +113,13 @@ class PostEditor { } } - this.didUpdate(); this.rerender(); - this.schedule(() => { - let nextElement = nextCursorMarker.renderNode.element; - this.editor.cursor.moveToNode( - nextElement, - nextCursorOffset - ); - }); + this.didUpdate(); + + return { + currentMarker: nextCursorMarker, + currentOffset: nextCursorOffset + }; } /* @@ -138,9 +135,9 @@ class PostEditor { // for removal before doing that. FIXME this seems prime for // refactoring onto the postEditor as a split function headMarker.renderNode.scheduleForRemoval(); - headMarker.renderNode.scheduleForRemoval(); - headMarker.section.renderNode.markDirty(); + tailMarker.renderNode.scheduleForRemoval(); headMarker.section.renderNode.markDirty(); + tailMarker.section.renderNode.markDirty(); if (headMarker === tailMarker) { let markers = headSection.splitMarker(headMarker, headOffset, tailOffset); @@ -174,12 +171,36 @@ class PostEditor { }); } - this.didUpdate(); this.rerender(); + this.didUpdate(); return selectedMarkers; } + /* + * @return {Array} of new sections + */ + splitSection({headMarker, headOffset}) { + const { post } = this.editor; + const { section } = headMarker; + + const [ + beforeSection, + afterSection + ] = section.splitAtMarker(headMarker, headOffset); + + this.removeSection(section); + + post.sections.insertAfter(beforeSection, section); + post.sections.insertAfter(afterSection, beforeSection); + post.renderNode.markDirty(); + + this.rerender(); + this.didUpdate(); + + return [beforeSection, afterSection]; + } + applyMarkupToMarkers(markerRange, markup) { const markers = this.splitMarkers(markerRange); markers.forEach(marker => { diff --git a/src/js/models/cursor.js b/src/js/models/cursor.js index 07b4106f3..e4afa7b7f 100644 --- a/src/js/models/cursor.js +++ b/src/js/models/cursor.js @@ -129,16 +129,16 @@ export default class Cursor { moveToSection(section) { const marker = section.markers.head; if (!marker) { throw new Error('Cannot move cursor to section without a marker'); } - const markerElement = marker.renderNode.element; + this.moveToMarker(marker); + } - let r = document.createRange(); - r.selectNode(markerElement); - r.collapse(true); - const selection = this.selection; - if (selection.rangeCount > 0) { - selection.removeAllRanges(); - } - selection.addRange(r); + // moves cursor to marker + moveToMarker(headMarker, headOffset=0, tailMarker=headMarker, tailOffset=headOffset) { + if (!headMarker) { throw new Error('Cannot move cursor to section without a marker'); } + const headElement = headMarker.renderNode.element; + const tailElement = tailMarker.renderNode.element; + + this.moveToNode(headElement, headOffset, tailElement, tailOffset); } selectSections(sections) { diff --git a/tests/acceptance/editor-selections-test.js b/tests/acceptance/editor-selections-test.js index f0f12ee63..b690f0fef 100644 --- a/tests/acceptance/editor-selections-test.js +++ b/tests/acceptance/editor-selections-test.js @@ -205,6 +205,37 @@ test('selecting text across markers deletes intermediary markers', (assert) => { }); }); +test('selecting text across markers preserves node after', (assert) => { + const done = assert.async(); + editor = new Editor(editorElement, {mobiledoc: mobileDocWith2Sections}); + + Helpers.dom.selectText('rst sec', editorElement); + Helpers.dom.triggerEvent(document, 'mouseup'); + + setTimeout(() => { + Helpers.toolbar.clickButton(assert, 'bold'); + + const textNode1 = editorElement.childNodes[0].childNodes[0], + textNode2 = editorElement.childNodes[0].childNodes[1]; + Helpers.dom.selectText('i', textNode1, + 'sec', textNode2); + Helpers.dom.triggerEvent(document, 'mouseup'); + + setTimeout(() => { + Helpers.dom.triggerDelete(editor); + + assert.deepEqual( + editorElement.childNodes[0].innerHTML, 'ftion', + 'has remaining first section' + ); + assert.deepEqual(Helpers.dom.getCursorPosition(), + {node: editorElement.childNodes[0].childNodes[0], + offset: 1}); + done(); + }); + }); +}); + test('selecting text across sections and hitting enter deletes and moves cursor to last selected section', (assert) => { const done = assert.async(); editor = new Editor(editorElement, {mobiledoc: mobileDocWith2Sections}); From 7e8a35caa8c84fee5b81ec657e4c94ea44e10161 Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Mon, 17 Aug 2015 15:08:08 -0400 Subject: [PATCH 4/6] rerender and didUpdate should be scheduled --- src/js/editor/post.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/js/editor/post.js b/src/js/editor/post.js index 8a92be22d..b84228962 100644 --- a/src/js/editor/post.js +++ b/src/js/editor/post.js @@ -35,8 +35,8 @@ class PostEditor { changedSections.forEach(section => section.renderNode.markDirty()); removedSections.forEach(section => section.renderNode.scheduleForRemoval()); - this.rerender(); - this.didUpdate(); + this.scheduleRerender(); + this.scheduleDidUpdate(); return { currentMarker, @@ -113,8 +113,8 @@ class PostEditor { } } - this.rerender(); - this.didUpdate(); + this.scheduleRerender(); + this.scheduleDidUpdate(); return { currentMarker: nextCursorMarker, @@ -171,8 +171,8 @@ class PostEditor { }); } - this.rerender(); - this.didUpdate(); + this.scheduleRerender(); + this.scheduleDidUpdate(); return selectedMarkers; } @@ -195,8 +195,8 @@ class PostEditor { post.sections.insertAfter(afterSection, beforeSection); post.renderNode.markDirty(); - this.rerender(); - this.didUpdate(); + this.scheduleRerender(); + this.scheduleDidUpdate(); return [beforeSection, afterSection]; } @@ -208,8 +208,8 @@ class PostEditor { marker.section.renderNode.markDirty(); }); - this.rerender(); - this.didUpdate(); + this.scheduleRerender(); + this.scheduleDidUpdate(); return markers; } @@ -221,8 +221,8 @@ class PostEditor { marker.section.renderNode.markDirty(); }); - this.rerender(); - this.didUpdate(); + this.scheduleRerender(); + this.scheduleDidUpdate(); return markers; } @@ -231,15 +231,15 @@ class PostEditor { this.editor.post.sections.insertBefore(section, beforeSection); this.editor.post.renderNode.markDirty(); - this.rerender(); - this.didUpdate(); + this.scheduleRerender(); + this.scheduleDidUpdate(); } removeSection(section) { section.renderNode.scheduleForRemoval(); - this.rerender(); - this.didUpdate(); + this.scheduleRerender(); + this.scheduleDidUpdate(); } /** @@ -257,7 +257,7 @@ class PostEditor { * Add a rerender job to the queue * */ - rerender() { + scheduleRerender() { this.schedule(() => { if (!this._didRerender) { this._didRerender = true; @@ -270,7 +270,7 @@ class PostEditor { * Add an update notice job to the queue * */ - didUpdate() { + scheduleDidUpdate() { this.schedule(() => { if (!this._didUpdate) { this._didUpdate = true; From d1061eb19b8d22fad04832034f7eba035872547d Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Mon, 17 Aug 2015 15:46:07 -0400 Subject: [PATCH 5/6] Drop loadModel --- src/js/editor/editor.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js index bbe91a16b..2d49c3d08 100644 --- a/src/js/editor/editor.js +++ b/src/js/editor/editor.js @@ -276,12 +276,6 @@ class Editor { this._views.push(view); } - loadModel(post) { - this.post = post; - this.rerender(); - this.trigger('update'); - } - parseModelFromDOM(element) { let parser = new DOMParser(this.builder); this.post = parser.parse(element); From aae4eda32b77bee6328049c6f2b92b64123e1898 Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Mon, 17 Aug 2015 16:29:59 -0400 Subject: [PATCH 6/6] Docs for postEditor, editor.run, README --- README.md | 27 +++++ src/js/editor/editor.js | 26 +++++ src/js/editor/post.js | 235 +++++++++++++++++++++++++++++++++++----- 3 files changed, 259 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 54bdb64db..e67301f22 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,33 @@ var editor = new ContentKit.Editor(element, options); Mobiledoc. * `editor.destroy()` - teardown the editor event listeners, free memory etc. +### Programmatic Post Editing + +A major goal of Content-Kit is to allow complete customization of user +interfaces using the editing surface. The programmatic editing API allows +the creation of completely custom interfaces for buttons, hot-keys, and +other interactions. + +To change the post in code, use the `editor.run` API. For example, the +following usage would mark currently selected text as bold: + +```js +let strongMarkup = editor.builder.createMarkup('strong'); +let markerRange = editor.cursor.offsets; +editor.run((postEditor) => { + postEditor.applyMarkupToMarkers(markerRange, strongMarkup); +}); +``` + +It is important that you make changes to posts, sections, and markers through +the `run` and `postEditor` API. This API allows Content-Kit to conserve +and better understand changes being made to the post. + +For more details on the API of `postEditor`, see the [API documentation](https://github.com/mixonic/content-kit-editor/blob/master/src/js/editor/post.js). + +For more details on the API for the builder, required to create new sections +and markers, see the [builder API](https://github.com/mixonic/content-kit-editor/blob/master/src/js/models/post-node-builder.js). + ### Contributing Fork the repo, write a test, make a change, open a PR. diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js index 2d49c3d08..76a0153e6 100644 --- a/src/js/editor/editor.js +++ b/src/js/editor/editor.js @@ -607,6 +607,32 @@ class Editor { this.removeAllViews(); } + /** + * Run a new post editing session. Yields a block with a new `postEditor` + * instance. This instance can be used to interact with the post abstract, + * and defers rendering until the end of all changes. + * + * Usage: + * + * let markerRange = this.cursor.offsets; + * editor.run((postEditor) => { + * postEditor.deleteRange(markerRange); + * // editing surface not updated yet + * postEditor.schedule(() => { + * console.log('logs during rerender flush'); + * }); + * // logging not yet flushed + * }); + * // editing surface now updated. + * // logging now flushed + * + * The return value of `run` is whatever was returned from the callback. + * + * @method run + * @param {Function} callback Function to handle post editing with, provided the `postEditor` as an argument. + * @return {} Whatever the return value of `callback` is. + * @public + */ run(callback) { let postEditor = new PostEditor(this); let result = callback(postEditor); diff --git a/src/js/editor/post.js b/src/js/editor/post.js index b84228962..e86a3716d 100644 --- a/src/js/editor/post.js +++ b/src/js/editor/post.js @@ -8,22 +8,45 @@ class PostEditor { this._didComplete = false; } - // types of selection deletion: - // * a selection starts at the beginning of a section - // -- cursor should end up at the beginning of that section - // -- if the section not longer has markers, add a blank one for the cursor to focus on - // * a selection is entirely within a section - // -- split the markers with the selection, remove those new markers from their section - // -- cursor goes at end of the marker before the selection start, or if the - // -- selection was at the start of the section, cursor goes at section start - // * a selection crosses multiple sections - // -- remove all the sections that are between (exclusive ) selection start and end - // -- join the start and end sections - // -- mark the end section for removal - // -- cursor goes at end of marker before the selection start - deleteRange(rangeMarkers) { - // rangeMarkers should be akin to this.cursor.offset - const markers = this.splitMarkers(rangeMarkers); + /** + * Remove a range of markers from the post. + * + * Usage: + * + * let marker = editor.post.sections.head.markers.head; + * editor.run((postEditor) => { + * postEditor.deleteRange({ + * headMarker: marker, + * headOffset: 2, + * tailMarker: marker, + * tailOffset: 4, + * }); + * }); + * + * `deleteRange` accepts the value of `this.cursor.offsets` for deletion. + * + * @method deleteRange + * @param {Object} markerRange Object with offsets, {headMarker, headOffset, tailMarker, tailOffset} + * @return {Object} {currentMarker, currentOffset} for cursor + * @public + */ + deleteRange(markerRange) { + // types of selection deletion: + // * a selection starts at the beginning of a section + // -- cursor should end up at the beginning of that section + // -- if the section not longer has markers, add a blank one for the cursor to focus on + // * a selection is entirely within a section + // -- split the markers with the selection, remove those new markers from their section + // -- cursor goes at end of the marker before the selection start, or if the + // -- selection was at the start of the section, cursor goes at section start + // * a selection crosses multiple sections + // -- remove all the sections that are between (exclusive ) selection start and end + // -- join the start and end sections + // -- mark the end section for removal + // -- cursor goes at end of marker before the selection start + + // markerRange should be akin to this.cursor.offset + const markers = this.splitMarkers(markerRange); const { changedSections, @@ -44,14 +67,36 @@ class PostEditor { }; } - // need to handle these cases: - // when cursor is: - // * A in the middle of a marker -- just delete the character - // * B offset is 0 and there is a previous marker - // * delete last char of previous marker - // * C offset is 0 and there is no previous marker - // * join this section with previous section + /** + * Remove a character from a marker. + * + * Usage: + * + * let marker = editor.post.sections.head.markers.head; + * // marker has text of "Howdy!" + * editor.run((postEditor) => { + * postEditor.deleteCharAt(marker, 3); + * }); + * // marker has text of "Hody!" + * + * `deleteCharAt` may remove a character from a different marker or section + * if the position of the deletion is at the 0th offset. Offset behaves like + * a cursor position, with the deletion going to the previous character. + * + * @method deleteCharAt + * @param {Object} marker the marker to delete the character from + * @param {Object} offset offset in the text of the marker to delete, first character is 1 + * @return {Object} {currentMarker, currentOffset} for cursor + * @public + */ deleteCharAt(marker, offset) { + // need to handle these cases: + // when cursor is: + // * A in the middle of a marker -- just delete the character + // * B offset is 0 and there is a previous marker + // * delete last char of previous marker + // * C offset is 0 and there is no previous marker + // * join this section with previous section const currentMarker = marker; let nextCursorMarker = currentMarker; let nextCursorOffset = offset; @@ -122,8 +167,25 @@ class PostEditor { }; } - /* - * @return {Array} of markers that are "inside the split" + /** + * Split makers at two positions, once at the head, and if necessary once + * at the tail. This method is designed to accept `editor.cursor.offsets` + * as an argument. + * + * Usage: + * + * let markerRange = this.cursor.offsets; + * editor.run((postEditor) => { + * postEditor.splitMarkers(markerRange); + * }); + * + * The return value will be marker object completely inside the offsets + * provided. Markers on the outside of the split may also have been modified. + * + * @method splitMarkers + * @param {Object} markerRange Object with offsets, {headMarker, headOffset, tailMarker, tailOffset} + * @return {Array} of markers that are inside the split + * @public */ splitMarkers({headMarker, headOffset, tailMarker, tailOffset}) { let selectedMarkers = []; @@ -177,8 +239,32 @@ class PostEditor { return selectedMarkers; } - /* - * @return {Array} of new sections + /** + * Split a section at one position. This method is designed to accept + * `editor.cursor.offsets` as an argument, but will only split at the + * head of the cursor position. + * + * Usage: + * + * let marker = editor.post.sections.head.marker.head; + * editor.run((postEditor) => { + * postEditor.splitSection({ + * headMarker: marker, + * headOffset: 3 + * }); + * }); + * // Will result in the marker and its old section being removed from + * // the post and rendered DOM, and in the creation of two new sections + * // replacing the old one. + * + * The return value will be the two new sections. One or both of these + * sections can be blank (contain only a blank marker), for example if the + * headOffset is 0. + * + * @method splitMarkers + * @param {Object} markerRange Object with offsets, {headMarker, headOffset, tailMarker, tailOffset} + * @return {Array} of new sections, one for the first half and one for the second + * @public */ splitSection({headMarker, headOffset}) { const { post } = this.editor; @@ -201,6 +287,30 @@ class PostEditor { return [beforeSection, afterSection]; } + /** + * Given a markerRange (for example `editor.cursor.offsets`) mark all markers + * inside it as a given markup. The markup must be provided as a post + * abstract node. + * + * Usage: + * + * let markerRange = editor.cursor.offsets; + * let strongMarkup = editor.builder.createMarkup('strong'); + * editor.run((postEditor) => { + * postEditor.applyMarkupToMarkers(markerRange, strongMarkup); + * }); + * // Will result some markers possibly being split, and the markup + * // being applied to all markers between the split. + * + * The return value will be all markers between the split, the same return + * value as `splitMarkers`. + * + * @method applyMarkupToMarkers + * @param {Object} markerRange Object with offsets, {headMarker, headOffset, tailMarker, tailOffset} + * @param {Object} markup A markup post abstract node + * @return {Array} of markers that are inside the split + * @public + */ applyMarkupToMarkers(markerRange, markup) { const markers = this.splitMarkers(markerRange); markers.forEach(marker => { @@ -214,6 +324,30 @@ class PostEditor { return markers; } + /** + * Given a markerRange (for example `editor.cursor.offsets`) remove the given + * markup from all contained markers. The markup must be provided as a post + * abstract node. + * + * Usage: + * + * let markerRange = editor.cursor.offsets; + * let markup = markerRange.headMarker.markups[0]; + * editor.run((postEditor) => { + * postEditor.removeMarkupFromMarkers(markerRange, markup); + * }); + * // Will result some markers possibly being split, and the markup + * // being removed from all markers between the split. + * + * The return value will be all markers between the split, the same return + * value as `splitMarkers`. + * + * @method removeMarkupFromMarkers + * @param {Object} markerRange Object with offsets, {headMarker, headOffset, tailMarker, tailOffset} + * @param {Object} markup A markup post abstract node + * @return {Array} of markers that are inside the split + * @public + */ removeMarkupFromMarkers(markerRange, markup) { const markers = this.splitMarkers(markerRange); markers.forEach(marker => { @@ -227,6 +361,24 @@ class PostEditor { return markers; } + /** + * Insert a given section before another one, updating the post abstract + * and the rendered UI. + * + * Usage: + * + * let markerRange = editor.cursor.offsets; + * let sectionWithCursor = markerRange.headMarker.section; + * let section = editor.builder.createCardSection('my-image'); + * editor.run((postEditor) => { + * postEditor.insertSectionBefore(section, sectionWithCursor); + * }); + * + * @method insertSectionBefore + * @param {Object} section The new section + * @param {Object} beforeSection The section "before" is relative to + * @public + */ insertSectionBefore(section, beforeSection) { this.editor.post.sections.insertBefore(section, beforeSection); this.editor.post.renderNode.markDirty(); @@ -235,6 +387,21 @@ class PostEditor { this.scheduleDidUpdate(); } + /** + * Remove a given section from the post abstract and the rendered UI. + * + * Usage: + * + * let markerRange = editor.cursor.offsets; + * let sectionWithCursor = markerRange.headMarker.section; + * editor.run((postEditor) => { + * postEditor.removeSection(sectionWithCursor); + * }); + * + * @method insertSectionBefore + * @param {Object} section The section to remove + * @public + */ removeSection(section) { section.renderNode.scheduleForRemoval(); @@ -245,6 +412,9 @@ class PostEditor { /** * A method for adding work the deferred queue * + * @method schedule + * @param {Function} callback to run during completion + * @public */ schedule(callback) { if (this._didComplete) { @@ -256,6 +426,8 @@ class PostEditor { /** * Add a rerender job to the queue * + * @method scheduleRerender + * @public */ scheduleRerender() { this.schedule(() => { @@ -267,8 +439,10 @@ class PostEditor { } /** - * Add an update notice job to the queue + * Add a didUpdate job to the queue * + * @method scheduleDidRender + * @public */ scheduleDidUpdate() { this.schedule(() => { @@ -280,8 +454,11 @@ class PostEditor { } /** - * Flush the work queue + * Flush any work on the queue. `editor.run` already does this, calling this + * method directly should not be needed outside `editor.run`. * + * @method complete + * @private */ complete() { if (this._didComplete) {