mobiledoc-kit
Advanced tools
Comparing version 0.7.2 to 0.7.3
@@ -61,2 +61,4 @@ 'use strict'; | ||
var _utilsAssert = require('../utils/assert'); | ||
var EDITOR_ELEMENT_CLASS_NAME = '__mobiledoc-editor'; | ||
@@ -155,4 +157,4 @@ | ||
} else { | ||
// DOM | ||
return this._parser.parse(this.html); | ||
var dom = this.html; | ||
return this._parser.parse(dom); | ||
} | ||
@@ -170,5 +172,3 @@ } else { | ||
if (!postRenderNode.element) { | ||
if (!this.element) { | ||
throw new Error('Initial call to `render` must happen before `rerender` can be called.'); | ||
} | ||
(0, _utilsAssert['default'])('Must call `render` before `rerender` can be called', this.hasRendered); | ||
postRenderNode.element = this.element; | ||
@@ -187,5 +187,3 @@ postRenderNode.markDirty(); | ||
value: function render(element) { | ||
if (this.element) { | ||
throw new Error('Cannot render an editor twice. Use `rerender` to update the rendering of an existing editor instance'); | ||
} | ||
(0, _utilsAssert['default'])('Cannot render an editor twice. Use `rerender` to update the ' + 'rendering of an existing editor instance.', !this.hasRendered); | ||
@@ -476,2 +474,5 @@ (0, _utilsDomUtils.addClassName)(element, EDITOR_ELEMENT_CLASS_NAME); | ||
this._isDestroyed = true; | ||
if (this.cursor.hasCursor()) { | ||
this.cursor.clearSelection(); | ||
} | ||
this.removeMutationObserver(); | ||
@@ -928,2 +929,7 @@ this._mutationObserver = null; | ||
} | ||
}, { | ||
key: 'hasRendered', | ||
get: function get() { | ||
return !!this.element; | ||
} | ||
}]); | ||
@@ -930,0 +936,0 @@ |
@@ -25,2 +25,4 @@ 'use strict'; | ||
var _postPostInserter = require('./post/post-inserter'); | ||
function isListSectionTagName(tagName) { | ||
@@ -703,27 +705,22 @@ return tagName === 'ul' || tagName === 'ol'; | ||
} else if (section.isListItem) { | ||
var _section$splitAtPosition = section.splitAtPosition(position); | ||
var isLastAndBlank = section.isBlank && !section.next; | ||
if (isLastAndBlank) { | ||
// if is last, replace the item with a blank markup section | ||
var _parent = section.parent; | ||
var collection = this.editor.post.sections; | ||
var blank = this.builder.createMarkupSection(); | ||
this.removeSection(section); | ||
this.insertSectionBefore(collection, blank, _parent.next); | ||
var _section$splitAtPosition2 = _slicedToArray(_section$splitAtPosition, 2); | ||
return [null, blank]; | ||
} else { | ||
var _splitListItem2 = this._splitListItem(section, position); | ||
var beforeSection = _section$splitAtPosition2[0]; | ||
var afterSection = _section$splitAtPosition2[1]; | ||
var _splitListItem22 = _slicedToArray(_splitListItem2, 2); | ||
this._coalesceMarkers(beforeSection); | ||
this._coalesceMarkers(afterSection); | ||
var pre = _splitListItem22[0]; | ||
var post = _splitListItem22[1]; | ||
var newSections = [beforeSection, afterSection]; | ||
var replacementSections = [beforeSection, afterSection]; | ||
if (beforeSection.isBlank && section.isBlank) { | ||
var isLastItemInList = section === section.parent.items.tail; | ||
if (isLastItemInList) { | ||
// when hitting enter in a final empty list item, do not insert a new | ||
// empty item | ||
replacementSections.shift(); | ||
} | ||
return [pre, post]; | ||
} | ||
this._replaceSection(section, replacementSections); | ||
return newSections; | ||
} else { | ||
@@ -835,2 +832,31 @@ var splitSections = section.splitAtPosition(position); | ||
}, { | ||
key: 'insertMarkers', | ||
value: function insertMarkers(position, markers) { | ||
var _this9 = this; | ||
var section = position.section; | ||
var offset = position.offset; | ||
(0, _utilsAssert['default'])('Cannot insert markers at non-markerable position', section.isMarkerable); | ||
var edit = section.splitMarkerAtOffset(offset); | ||
edit.removed.forEach(function (marker) { | ||
return _this9._scheduleForRemoval(marker); | ||
}); | ||
var prevMarker = section.markerBeforeOffset(offset); | ||
markers.forEach(function (marker) { | ||
section.markers.insertAfter(marker, prevMarker); | ||
offset += marker.length; | ||
prevMarker = marker; | ||
}); | ||
this._coalesceMarkers(section); | ||
this._markDirty(section); | ||
var nextPosition = new _utilsCursorPosition['default'](position.section, offset); | ||
this.setRange(new _utilsCursorRange['default'](nextPosition)); | ||
return nextPosition; | ||
} | ||
}, { | ||
key: 'insertText', | ||
@@ -865,3 +891,3 @@ value: function insertText(position, text) { | ||
value: function _replaceSection(section, newSections) { | ||
var _this9 = this; | ||
var _this10 = this; | ||
@@ -873,3 +899,4 @@ var nextSection = section.next; | ||
if (nextNewSection.isMarkupSection && section.isListItem) { | ||
// put the new section after the ListSection (section.parent) instead of after the ListItem | ||
// put the new section after the ListSection (section.parent) | ||
// instead of after the ListItem | ||
collection = section.parent.parent.sections; | ||
@@ -880,3 +907,3 @@ nextSection = section.parent.next; | ||
newSections.forEach(function (s) { | ||
return _this9.insertSectionBefore(collection, s, nextSection); | ||
return _this10.insertSectionBefore(collection, s, nextSection); | ||
}); | ||
@@ -912,3 +939,3 @@ this.removeSection(section); | ||
value: function addMarkupToRange(range, markup) { | ||
var _this10 = this; | ||
var _this11 = this; | ||
@@ -920,3 +947,3 @@ if (range.isCollapsed) { | ||
marker.addMarkup(markup); | ||
_this10._markDirty(marker); | ||
_this11._markDirty(marker); | ||
}); | ||
@@ -948,3 +975,3 @@ } | ||
value: function removeMarkupFromRange(range, markupOrMarkupCallback) { | ||
var _this11 = this; | ||
var _this12 = this; | ||
@@ -956,3 +983,3 @@ if (range.isCollapsed) { | ||
marker.removeMarkup(markupOrMarkupCallback); | ||
_this11._markDirty(marker); | ||
_this12._markDirty(marker); | ||
}); | ||
@@ -987,3 +1014,3 @@ } | ||
value: function toggleMarkup(markupOrMarkupString) { | ||
var _this12 = this; | ||
var _this13 = this; | ||
@@ -1006,3 +1033,3 @@ var range = this.editor.cursor.offsets; | ||
this.scheduleAfterRender(function () { | ||
return _this12.editor.selectRange(range); | ||
return _this13.editor.selectRange(range); | ||
}); | ||
@@ -1026,3 +1053,3 @@ } | ||
value: function toggleSection(sectionTagName) { | ||
var _this13 = this; | ||
var _this14 = this; | ||
@@ -1036,3 +1063,3 @@ var range = arguments.length <= 1 || arguments[1] === undefined ? this.editor.range : arguments[1]; | ||
post.walkMarkerableSections(range, function (section) { | ||
if (!_this13._isSameSectionType(section, sectionTagName)) { | ||
if (!_this14._isSameSectionType(section, sectionTagName)) { | ||
everySectionHasTagName = false; | ||
@@ -1045,3 +1072,3 @@ } | ||
post.walkMarkerableSections(range, function (section) { | ||
var changedSection = _this13.changeSectionTagName(section, tagName); | ||
var changedSection = _this14.changeSectionTagName(section, tagName); | ||
firstChanged = firstChanged || changedSection; | ||
@@ -1079,2 +1106,96 @@ }); | ||
/** | ||
* Splits the item at the position given. | ||
* If thse position is at the start or end of the item, the pre- or post-item | ||
* will contain a single empty ("") marker. | ||
* @return {Array} the pre-item and post-item on either side of the split | ||
*/ | ||
}, { | ||
key: '_splitListItem', | ||
value: function _splitListItem(item, position) { | ||
var section = position.section; | ||
var offset = position.offset; | ||
(0, _utilsAssert['default'])('Cannot split list item at position that does not include item', item === section); | ||
item.splitMarkerAtOffset(offset); | ||
var prevMarker = item.markerBeforeOffset(offset); | ||
var preItem = this.builder.createListItem(), | ||
postItem = this.builder.createListItem(); | ||
var currentItem = preItem; | ||
item.markers.forEach(function (marker) { | ||
currentItem.markers.append(marker.clone()); | ||
if (marker === prevMarker) { | ||
currentItem = postItem; | ||
} | ||
}); | ||
this._replaceSection(item, [preItem, postItem]); | ||
return [preItem, postItem]; | ||
} | ||
/** | ||
* Splits the list at the position given. | ||
* @return {Array} pre-split list and post-split list, either of which could | ||
* be blank (0-item list) if the position is at the start or end of the list. | ||
* | ||
* Note: Contiguous list sections will be joined in the before_complete queue | ||
* of the postEditor. | ||
*/ | ||
}, { | ||
key: '_splitListAtPosition', | ||
value: function _splitListAtPosition(list, position) { | ||
var _this15 = this; | ||
(0, _utilsAssert['default'])('Cannot split list at position not in list', position.section.parent === list); | ||
var positionIsMiddle = !position.isHead() && !position.isTail(); | ||
if (positionIsMiddle) { | ||
var item = position.section; | ||
var _splitListItem3 = // jshint ignore:line | ||
this._splitListItem(item, position); | ||
var _splitListItem32 = _slicedToArray(_splitListItem3, 2); | ||
var pre = _splitListItem32[0]; | ||
var post = _splitListItem32[1]; | ||
position = pre.tailPosition(); | ||
} | ||
var positionIsStart = position.isEqual(list.headPosition()), | ||
positionIsEnd = position.isEqual(list.tailPosition()); | ||
if (positionIsStart || positionIsEnd) { | ||
var blank = this.builder.createListSection(list.tagName); | ||
var reference = position.isEqual(list.headPosition()) ? list : list.next; | ||
var collection = this.editor.post.sections; | ||
this.insertSectionBefore(collection, blank, reference); | ||
var lists = positionIsStart ? [blank, list] : [list, blank]; | ||
return lists; | ||
} else { | ||
var _ret2 = (function () { | ||
var preList = _this15.builder.createListSection(list.tagName), | ||
postList = _this15.builder.createListSection(list.tagName); | ||
var preItem = position.section; | ||
var currentList = preList; | ||
list.items.forEach(function (item) { | ||
currentList.items.append(item.clone()); | ||
if (item === preItem) { | ||
currentList = postList; | ||
} | ||
}); | ||
_this15._replaceSection(list, [preList, postList]); | ||
return { | ||
v: [preList, postList] | ||
}; | ||
})(); | ||
if (typeof _ret2 === 'object') return _ret2.v; | ||
} | ||
} | ||
/** | ||
* @return Array of [prev, mid, next] lists. `prev` and `next` can | ||
@@ -1090,3 +1211,3 @@ * be blank, depending on the position of `item`. `mid` will always | ||
value: function _splitListAtItem(list, item) { | ||
var _this14 = this; | ||
var _this16 = this; | ||
@@ -1112,3 +1233,3 @@ var next = list; | ||
listToAppend.join(i); | ||
_this14.removeSection(i); | ||
_this16.removeSection(i); | ||
}); | ||
@@ -1127,3 +1248,3 @@ var found = !addToPrev; | ||
if (_list.isBlank && isAttached) { | ||
_this14.removeSection(_list); | ||
_this16.removeSection(_list); | ||
} | ||
@@ -1139,6 +1260,5 @@ }); | ||
(0, _utilsAssert['default'])('Must pass list item to `_changeSectionFromListItem`', section.isListItem); | ||
var builder = this.builder; | ||
var listSection = section.parent; | ||
var markupSection = builder.createMarkupSection(newTagName); | ||
var markupSection = this.builder.createMarkupSection(newTagName); | ||
markupSection.join(section); | ||
@@ -1248,3 +1368,2 @@ | ||
* @param {Post} post | ||
* @return {Position} position at end of inserted content | ||
* @private | ||
@@ -1255,46 +1374,5 @@ */ | ||
value: function insertPost(position, newPost) { | ||
var _this15 = this; | ||
if (newPost.isBlank) { | ||
return position; | ||
} | ||
var post = this.editor.post; | ||
var _splitSection = this.splitSection(position); | ||
var _splitSection2 = _slicedToArray(_splitSection, 2); | ||
var preSplit = _splitSection2[0]; | ||
var postSplit = _splitSection2[1]; | ||
var nextPosition = position.clone(); | ||
newPost.sections.forEach(function (section, index) { | ||
if (index === 0 && preSplit.canJoin(section)) { | ||
preSplit.join(section); | ||
_this15._markDirty(preSplit); | ||
nextPosition = preSplit.tailPosition(); | ||
} else { | ||
section = section.clone(); | ||
_this15.insertSectionBefore(post.sections, section, postSplit); | ||
nextPosition = section.tailPosition(); | ||
} | ||
}); | ||
if (postSplit.isBlank) { | ||
this.removeSection(postSplit); | ||
} | ||
if (preSplit.canJoin(postSplit) && preSplit.next === postSplit) { | ||
nextPosition = preSplit.tailPosition(); | ||
preSplit.join(postSplit); | ||
this._markDirty(preSplit); | ||
this.removeSection(postSplit); | ||
} else if (preSplit.isBlank) { | ||
this.removeSection(preSplit); | ||
} | ||
var inserter = new _postPostInserter['default'](this, post); | ||
var nextPosition = inserter.insert(position, newPost); | ||
return nextPosition; | ||
@@ -1332,3 +1410,3 @@ } | ||
value: function _scheduleListRemovalIfEmpty(listSection) { | ||
var _this16 = this; | ||
var _this17 = this; | ||
@@ -1340,3 +1418,3 @@ this.addCallback(CALLBACK_QUEUES.BEFORE_COMPLETE, function () { | ||
if (isAttached && listSection.isBlank) { | ||
_this16.removeSection(listSection); | ||
_this17.removeSection(listSection); | ||
} | ||
@@ -1369,3 +1447,3 @@ }); | ||
value: function scheduleRerender() { | ||
var _this17 = this; | ||
var _this18 = this; | ||
@@ -1377,3 +1455,3 @@ if (this._didScheduleRerender) { | ||
this.schedule(function () { | ||
return _this17.editor.rerender(); | ||
return _this18.editor.rerender(); | ||
}); | ||
@@ -1392,3 +1470,3 @@ this._didScheduleRerender = true; | ||
value: function scheduleDidUpdate() { | ||
var _this18 = this; | ||
var _this19 = this; | ||
@@ -1400,3 +1478,3 @@ if (this._didScheduleUpdate) { | ||
this.schedule(function () { | ||
return _this18.editor.didUpdate(); | ||
return _this19.editor.didUpdate(); | ||
}); | ||
@@ -1412,3 +1490,3 @@ this._didScheduleUpdate = true; | ||
/** | ||
* Flush any work on the queue. `editor.run` already does this, calling this | ||
* Flush any work on the queue. `editor.run` already does this. Calling this | ||
* method directly should not be needed outside `editor.run`. | ||
@@ -1422,5 +1500,3 @@ * | ||
value: function complete() { | ||
if (this._didComplete) { | ||
throw new Error('Post editing can only be completed once'); | ||
} | ||
(0, _utilsAssert['default'])('Post editing can only be completed once', !this._didComplete); | ||
@@ -1427,0 +1503,0 @@ this.runCallbacks(CALLBACK_QUEUES.BEFORE_COMPLETE); |
@@ -7,2 +7,4 @@ 'use strict'; | ||
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } | ||
function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); } | ||
@@ -24,2 +26,4 @@ | ||
var _utilsAssert = require('../utils/assert'); | ||
var Markerable = (function (_Section) { | ||
@@ -40,3 +44,4 @@ _inherits(Markerable, _Section); | ||
adoptItem: function adoptItem(m) { | ||
return m.section = m.parent = _this; | ||
(0, _utilsAssert['default'])('Cannot insert non-marker into markerable (was: ' + m.type + ')', m.isMarker); | ||
m.section = m.parent = _this; | ||
}, | ||
@@ -165,3 +170,5 @@ freeItem: function freeItem(m) { | ||
* @param {Number} sectionOffset The offset relative to start of this section | ||
* @return {EditObject} An edit object with 'removed' and 'added' keys with arrays of Markers | ||
* @return {EditObject} An edit object with 'removed' and 'added' keys with arrays of Markers. The added markers may be blank. | ||
* After calling `splitMarkerAtOffset(offset)`, there will always be a valid | ||
* result returned from `markerBeforeOffset(offset)`. | ||
*/ | ||
@@ -171,21 +178,33 @@ }, { | ||
value: function splitMarkerAtOffset(sectionOffset) { | ||
var edit = { removed: [], added: [] }; | ||
(0, _utilsAssert['default'])('Cannot splitMarkerAtOffset when offset is > length', sectionOffset <= this.length); | ||
var markerOffset = undefined; | ||
var len = 0; | ||
var currentMarker = this.markers.head; | ||
var edit = { added: [], removed: [] }; | ||
var _markerPositionAtOffset = this.markerPositionAtOffset(sectionOffset); | ||
if (!currentMarker) { | ||
var blankMarker = this.builder.createMarker(); | ||
this.markers.prepend(blankMarker); | ||
edit.added.push(blankMarker); | ||
} else { | ||
while (currentMarker) { | ||
len += currentMarker.length; | ||
if (len === sectionOffset) { | ||
// nothing to do, there is a gap at the requested offset | ||
break; | ||
} else if (len > sectionOffset) { | ||
var _edit$added; | ||
var marker = _markerPositionAtOffset.marker; | ||
var offset = _markerPositionAtOffset.offset; | ||
if (!marker) { | ||
return edit; | ||
markerOffset = currentMarker.length - (len - sectionOffset); | ||
var newMarkers = currentMarker.splitAtOffset(markerOffset); | ||
(_edit$added = edit.added).push.apply(_edit$added, _toConsumableArray(newMarkers)); | ||
edit.removed.push(currentMarker); | ||
this.markers.splice(currentMarker, 1, newMarkers); | ||
break; | ||
} else { | ||
currentMarker = currentMarker.next; | ||
} | ||
} | ||
} | ||
var newMarkers = (0, _utilsArrayUtils.filter)(marker.split(offset), function (m) { | ||
return !m.isEmpty; | ||
}); | ||
this.markers.splice(marker, 1, newMarkers); | ||
edit.removed = [marker]; | ||
edit.added = newMarkers; | ||
return edit; | ||
@@ -201,3 +220,23 @@ } | ||
} | ||
// returns the marker just before this offset. | ||
// It is an error to call this method with an offset that is in the middle | ||
// of a marker. | ||
}, { | ||
key: 'markerBeforeOffset', | ||
value: function markerBeforeOffset(sectionOffset) { | ||
var len = 0; | ||
var currentMarker = this.markers.head; | ||
while (currentMarker) { | ||
len += currentMarker.length; | ||
if (len === sectionOffset) { | ||
return currentMarker; | ||
} else { | ||
(0, _utilsAssert['default'])('markerBeforeOffset called with sectionOffset not between markers', len < sectionOffset); | ||
currentMarker = currentMarker.next; | ||
} | ||
} | ||
} | ||
}, { | ||
key: 'markerPositionAtOffset', | ||
@@ -204,0 +243,0 @@ value: function markerPositionAtOffset(offset) { |
@@ -37,2 +37,3 @@ 'use strict'; | ||
this.isMarkerable = false; | ||
this.isNested = false; | ||
} | ||
@@ -39,0 +40,0 @@ |
@@ -33,2 +33,3 @@ 'use strict'; | ||
this.isListItem = true; | ||
this.isNested = true; | ||
} | ||
@@ -35,0 +36,0 @@ |
@@ -21,2 +21,4 @@ 'use strict'; | ||
var _utilsAssert = require('../utils/assert'); | ||
var VALID_LIST_SECTION_TAGNAMES = ['ul', 'ol'].map(_utilsDomUtils.normalizeTagName); | ||
@@ -46,3 +48,4 @@ | ||
adoptItem: function adoptItem(i) { | ||
return i.section = i.parent = _this; | ||
(0, _utilsAssert['default'])('Cannot insert non-list-item to list (is: ' + i.type + ')', i.isListItem); | ||
i.section = i.parent = _this; | ||
}, | ||
@@ -83,3 +86,3 @@ freeItem: function freeItem(i) { | ||
value: function clone() { | ||
var newSection = this.builder.createListSection(); | ||
var newSection = this.builder.createListSection(this.tagName); | ||
(0, _utilsArrayUtils.forEach)(this.items, function (i) { | ||
@@ -86,0 +89,0 @@ return newSection.items.append(i.clone()); |
@@ -19,2 +19,4 @@ 'use strict'; | ||
var _utilsAssert = require('../utils/assert'); | ||
var Marker = (function (_LinkedItem) { | ||
@@ -35,2 +37,3 @@ _inherits(Marker, _LinkedItem); | ||
this.type = _types.MARKER_TYPE; | ||
this.isMarker = true; | ||
markups.forEach(function (m) { | ||
@@ -174,3 +177,24 @@ return _this.addMarkup(m); | ||
} | ||
/** | ||
* @return {Array} 2 markers either or both of which could be blank | ||
*/ | ||
}, { | ||
key: 'splitAtOffset', | ||
value: function splitAtOffset(offset) { | ||
(0, _utilsAssert['default'])('Cannot split a marker at an offset > its length', offset <= this.length); | ||
var value = this.value; | ||
var builder = this.builder; | ||
var pre = builder.createMarker(value.substring(0, offset)); | ||
var post = builder.createMarker(value.substring(offset)); | ||
this.markups.forEach(function (markup) { | ||
pre.addMarkup(markup); | ||
post.addMarkup(markup); | ||
}); | ||
return [pre, post]; | ||
} | ||
}, { | ||
key: 'isEmpty', | ||
@@ -177,0 +201,0 @@ get: function get() { |
@@ -17,2 +17,4 @@ 'use strict'; | ||
var _utilsCursorRange = require('../utils/cursor/range'); | ||
var Post = (function () { | ||
@@ -124,21 +126,7 @@ function Post() { | ||
} | ||
// FIXME if range.head is a listItem this will not work properly | ||
}, { | ||
key: 'walkPostSections', | ||
value: function walkPostSections(range, callback) { | ||
var head = range.head; | ||
var tail = range.tail; | ||
var currentSection = head.section; | ||
while (currentSection) { | ||
callback(currentSection); | ||
if (currentSection === tail.section) { | ||
break; | ||
} else { | ||
currentSection = currentSection.next; | ||
} | ||
} | ||
key: 'walkAllLeafSections', | ||
value: function walkAllLeafSections(callback) { | ||
var range = new _utilsCursorRange['default'](this.sections.head.headPosition(), this.sections.tail.tailPosition()); | ||
return this.walkLeafSections(range, callback); | ||
} | ||
@@ -277,6 +265,23 @@ }, { | ||
this.walkPostSections(range, function (section) { | ||
var sectionParent = post, | ||
listParent = null; | ||
this.walkLeafSections(range, function (section) { | ||
var newSection = undefined; | ||
if (section.isMarkerable) { | ||
newSection = builder.createMarkupSection(section.tagName); | ||
if (section.isListItem) { | ||
if (listParent) { | ||
sectionParent = null; | ||
} else { | ||
listParent = builder.createListSection(section.parent.tagName); | ||
post.sections.append(listParent); | ||
sectionParent = null; | ||
} | ||
newSection = builder.createListItem(); | ||
listParent.items.append(newSection); | ||
} else { | ||
listParent = null; | ||
sectionParent = post; | ||
newSection = builder.createMarkupSection(section.tagName); | ||
} | ||
var currentRange = range.trimTo(section); | ||
@@ -289,3 +294,5 @@ (0, _utilsArrayUtils.forEach)(section.markersFor(currentRange.headSectionOffset, currentRange.tailSectionOffset), function (m) { | ||
} | ||
post.sections.append(newSection); | ||
if (sectionParent) { | ||
sectionParent.sections.append(newSection); | ||
} | ||
}); | ||
@@ -292,0 +299,0 @@ return _renderersMobiledoc['default'].render(post); |
@@ -46,3 +46,3 @@ 'use strict'; | ||
* @param {DOMNode} element | ||
* Walk up from the element until we find a renderNode element | ||
* Walk up from the dom element until we find a renderNode element | ||
*/ | ||
@@ -49,0 +49,0 @@ }, { |
@@ -24,2 +24,7 @@ 'use strict'; | ||
/** | ||
* @param {String} html to parse | ||
* @return {Post} A post abstract | ||
*/ | ||
_createClass(HTMLParser, [{ | ||
@@ -26,0 +31,0 @@ key: 'parse', |
@@ -30,14 +30,8 @@ 'use strict'; | ||
function isListSection(section) { | ||
return section.type === _modelsTypes.LIST_SECTION_TYPE; | ||
} | ||
var SKIPPABLE_ELEMENT_TAG_NAMES = ['style', 'head', 'title', 'meta'].map(_utilsDomUtils.normalizeTagName); | ||
function isListItem(section) { | ||
return section.type === _modelsTypes.LIST_ITEM_TYPE; | ||
} | ||
/** | ||
* parses an element into a section, ignoring any non-markup | ||
* elements contained within | ||
* @return {Section} | ||
* @return {Array} sections | ||
*/ | ||
@@ -60,2 +54,5 @@ | ||
if (this._isSkippable(element)) { | ||
return []; | ||
} | ||
this.sections = []; | ||
@@ -68,3 +65,3 @@ this.state = {}; | ||
if (isListSection(this.state.section)) { | ||
if (this.state.section.isListSection) { | ||
this.parseListItems(childNodes); | ||
@@ -91,3 +88,3 @@ } else { | ||
var li = parsed[0]; | ||
if (li && isListItem(li)) { | ||
if (li && li.isListItem) { | ||
state.section.items.append(li); | ||
@@ -333,2 +330,7 @@ } | ||
} | ||
}, { | ||
key: '_isSkippable', | ||
value: function _isSkippable(element) { | ||
return element.nodeType === ELEMENT_NODE && (0, _utilsArrayUtils.contains)(SKIPPABLE_ELEMENT_TAG_NAMES, (0, _utilsDomUtils.normalizeTagName)(element.tagName)); | ||
} | ||
}]); | ||
@@ -335,0 +337,0 @@ |
@@ -412,2 +412,3 @@ 'use strict'; | ||
this.nodes = []; | ||
this.hasRendered = false; | ||
} | ||
@@ -418,2 +419,5 @@ | ||
value: function destroy() { | ||
if (!this.hasRendered) { | ||
return; | ||
} | ||
var renderNode = this.renderTree.rootNode; | ||
@@ -444,2 +448,3 @@ var force = true; | ||
this.hasRendered = true; | ||
this.renderTree = renderTree; | ||
@@ -446,0 +451,0 @@ var renderNode = renderTree.rootNode; |
@@ -42,3 +42,3 @@ 'use strict'; | ||
value: function hasCursor() { | ||
return this._hasCollapsedSelection() || this._hasSelection(); | ||
return this.editor.hasRendered && (this._hasCollapsedSelection() || this._hasSelection()); | ||
} | ||
@@ -48,3 +48,3 @@ }, { | ||
value: function hasSelection() { | ||
return this._hasSelection(); | ||
return this.editor.hasRendered && this._hasSelection(); | ||
} | ||
@@ -51,0 +51,0 @@ |
@@ -63,2 +63,21 @@ 'use strict'; | ||
}, { | ||
key: 'isHead', | ||
value: function isHead() { | ||
return this.isEqual(this.section.headPosition()); | ||
} | ||
}, { | ||
key: 'isTail', | ||
value: function isTail() { | ||
return this.isEqual(this.section.tailPosition()); | ||
} | ||
/** | ||
* This method returns a new Position instance, it does not modify | ||
* this instance. | ||
* | ||
* @param {Direction} direction to move | ||
* @return {Position|null} Return the position one unit in the given | ||
* direction, or null if it is not possible to move that direction | ||
*/ | ||
}, { | ||
key: 'move', | ||
@@ -75,22 +94,28 @@ value: function move(direction) { | ||
} | ||
/** | ||
* @return {Position|null} | ||
*/ | ||
}, { | ||
key: 'moveLeft', | ||
value: function moveLeft() { | ||
if (this.offset > 0) { | ||
if (this.isHead()) { | ||
var prev = this.section.previousLeafSection(); | ||
return prev && prev.tailPosition(); | ||
} else { | ||
return new Position(this.section, this.offset - 1); | ||
} else if (this.section.prev) { | ||
return new Position(this.section.prev, this.section.prev.length); | ||
} else { | ||
return null; | ||
} | ||
} | ||
/** | ||
* @return {Position|null} | ||
*/ | ||
}, { | ||
key: 'moveRight', | ||
value: function moveRight() { | ||
if (this.offset < this.section.length) { | ||
if (this.isTail()) { | ||
var next = this.section.nextLeafSection(); | ||
return next && next.headPosition(); | ||
} else { | ||
return new Position(this.section, this.offset + 1); | ||
} else if (this.section.next) { | ||
return new Position(this.section.next, 0); | ||
} else { | ||
return null; | ||
} | ||
@@ -115,5 +140,3 @@ } | ||
get: function get() { | ||
if (!this.section) { | ||
throw new Error('cannot get markerPosition without a section'); | ||
} | ||
(0, _utilsAssert['default'])('Cannot get markerPosition without a section', !!this.section); | ||
return this.section.markerPositionAtOffset(this.offset); | ||
@@ -156,5 +179,3 @@ } | ||
if (!section) { | ||
throw new Error('Could not find parent section for mapped text node "' + textNode.textContent + '"'); | ||
} | ||
(0, _utilsAssert['default'])('Could not find parent section for mapped text node "' + textNode.textContent + '"', !!section); | ||
offsetInSection = section.offsetOfMarker(marker, offsetInNode); | ||
@@ -167,5 +188,3 @@ } else { | ||
section = findParentSectionFromNode(renderTree, textNode); | ||
if (!section) { | ||
throw new Error('Could not find parent section for un-mapped text node "' + textNode.textContent + '"'); | ||
} | ||
(0, _utilsAssert['default'])('Could not find parent section for un-mapped text node "' + textNode.textContent + '"', !!section); | ||
@@ -172,0 +191,0 @@ offsetInSection = findOffsetInSection(section, textNode, offsetInNode); |
@@ -11,2 +11,4 @@ /* global JSON */ | ||
var _parsersText = require('../parsers/text'); | ||
var _mobiledocHtmlRenderer = require('mobiledoc-html-renderer'); | ||
@@ -16,2 +18,34 @@ | ||
var MOBILEDOC_REGEX = new RegExp(/data\-mobiledoc='(.*?)'>/); | ||
var MIME_TEXT_PLAIN = 'text/plain'; | ||
exports.MIME_TEXT_PLAIN = MIME_TEXT_PLAIN; | ||
var MIME_TEXT_HTML = 'text/html'; | ||
exports.MIME_TEXT_HTML = MIME_TEXT_HTML; | ||
function parsePostFromHTML(html, builder, plugins) { | ||
var post = undefined; | ||
if (MOBILEDOC_REGEX.test(html)) { | ||
var mobiledocString = html.match(MOBILEDOC_REGEX)[1]; | ||
var mobiledoc = JSON.parse(mobiledocString); | ||
post = _parsersMobiledoc['default'].parse(builder, mobiledoc); | ||
} else { | ||
post = new _parsersHtml['default'](builder, { plugins: plugins }).parse(html); | ||
} | ||
return post; | ||
} | ||
function parsePostFromText(text, builder, plugins) { | ||
var parser = new _parsersText['default'](builder, { plugins: plugins }); | ||
var post = parser.parse(text); | ||
return post; | ||
} | ||
/** | ||
* @param {Event} copyEvent | ||
* @param {Editor} | ||
* @return null | ||
*/ | ||
function setClipboardCopyData(copyEvent, editor) { | ||
@@ -37,26 +71,25 @@ var cursor = editor.cursor; | ||
clipboardData.setData('text/plain', plain); | ||
clipboardData.setData('text/html', html); | ||
clipboardData.setData(MIME_TEXT_PLAIN, plain); | ||
clipboardData.setData(MIME_TEXT_HTML, html); | ||
} | ||
/** | ||
* @param {Event} pasteEvent | ||
* @param {PostNodeBuilder} builder | ||
* @param {Array} plugins parser plugins | ||
* @return {Post} | ||
*/ | ||
function parsePostFromPaste(pasteEvent, builder) { | ||
var plugins = arguments.length <= 2 || arguments[2] === undefined ? [] : arguments[2]; | ||
var mobiledoc = undefined, | ||
post = undefined; | ||
var mobiledocRegex = new RegExp(/data\-mobiledoc='(.*?)'>/); | ||
var post = undefined; | ||
var html = pasteEvent.clipboardData.getData(MIME_TEXT_HTML); | ||
var html = pasteEvent.clipboardData.getData('text/html'); | ||
// Fallback to 'text/plain' | ||
if (!html || html.length === 0) { | ||
html = pasteEvent.clipboardData.getData('text/plain'); | ||
} | ||
if (mobiledocRegex.test(html)) { | ||
var mobiledocString = html.match(mobiledocRegex)[1]; | ||
mobiledoc = JSON.parse(mobiledocString); | ||
post = _parsersMobiledoc['default'].parse(builder, mobiledoc); | ||
// Fallback to 'text/plain' | ||
var text = pasteEvent.clipboardData.getData(MIME_TEXT_PLAIN); | ||
post = parsePostFromText(text, builder, plugins); | ||
} else { | ||
post = new _parsersHtml['default'](builder, { plugins: plugins }).parse(html); | ||
post = parsePostFromHTML(html, builder, plugins); | ||
} | ||
@@ -63,0 +96,0 @@ |
{ | ||
"name": "mobiledoc-kit", | ||
"version": "0.7.2", | ||
"version": "0.7.3", | ||
"description": "A toolkit for building WYSIWYG editors with Mobiledoc", | ||
@@ -5,0 +5,0 @@ "repository": "https://github.com/bustlelabs/mobiledoc-kit", |
@@ -41,2 +41,3 @@ import Tooltip from '../views/tooltip'; | ||
import { TAB } from 'mobiledoc-kit/utils/characters'; | ||
import assert from '../utils/assert'; | ||
@@ -123,4 +124,5 @@ export const EDITOR_ELEMENT_CLASS_NAME = '__mobiledoc-editor'; | ||
return new HTMLParser(this.builder).parse(this.html); | ||
} else { // DOM | ||
return this._parser.parse(this.html); | ||
} else { | ||
let dom = this.html; | ||
return this._parser.parse(dom); | ||
} | ||
@@ -137,5 +139,4 @@ } else { | ||
if (!postRenderNode.element) { | ||
if (!this.element) { | ||
throw new Error('Initial call to `render` must happen before `rerender` can be called.'); | ||
} | ||
assert('Must call `render` before `rerender` can be called', | ||
this.hasRendered); | ||
postRenderNode.element = this.element; | ||
@@ -153,7 +154,6 @@ postRenderNode.markDirty(); | ||
render(element) { | ||
if (this.element) { | ||
throw new Error('Cannot render an editor twice. Use `rerender` to update the rendering of an existing editor instance'); | ||
} | ||
assert('Cannot render an editor twice. Use `rerender` to update the ' + | ||
'rendering of an existing editor instance.', | ||
!this.hasRendered); | ||
addClassName(element, EDITOR_ELEMENT_CLASS_NAME); | ||
@@ -444,2 +444,5 @@ element.spellcheck = this.spellcheck; | ||
this._isDestroyed = true; | ||
if (this.cursor.hasCursor()) { | ||
this.cursor.clearSelection(); | ||
} | ||
this.removeMutationObserver(); | ||
@@ -777,2 +780,6 @@ this._mutationObserver = null; | ||
} | ||
get hasRendered() { | ||
return !!this.element; | ||
} | ||
} | ||
@@ -779,0 +786,0 @@ |
@@ -9,2 +9,3 @@ import Position from '../utils/cursor/position'; | ||
import Range from '../utils/cursor/range'; | ||
import PostInserter from './post/post-inserter'; | ||
@@ -610,21 +611,16 @@ function isListSectionTagName(tagName) { | ||
} else if (section.isListItem) { | ||
let [beforeSection, afterSection] = section.splitAtPosition(position); | ||
this._coalesceMarkers(beforeSection); | ||
this._coalesceMarkers(afterSection); | ||
let isLastAndBlank = section.isBlank && !section.next; | ||
if (isLastAndBlank) { | ||
// if is last, replace the item with a blank markup section | ||
let parent = section.parent; | ||
let collection = this.editor.post.sections; | ||
let blank = this.builder.createMarkupSection(); | ||
this.removeSection(section); | ||
this.insertSectionBefore(collection, blank, parent.next); | ||
let newSections = [beforeSection, afterSection]; | ||
let replacementSections = [beforeSection, afterSection]; | ||
if (beforeSection.isBlank && section.isBlank) { | ||
const isLastItemInList = section === section.parent.items.tail; | ||
if (isLastItemInList) { | ||
// when hitting enter in a final empty list item, do not insert a new | ||
// empty item | ||
replacementSections.shift(); | ||
} | ||
return [null, blank]; | ||
} else { | ||
let [pre, post] = this._splitListItem(section, position); | ||
return [pre, post]; | ||
} | ||
this._replaceSection(section, replacementSections); | ||
return newSections; | ||
} else { | ||
@@ -725,2 +721,25 @@ let splitSections = section.splitAtPosition(position); | ||
insertMarkers(position, markers) { | ||
let { section, offset } = position; | ||
assert('Cannot insert markers at non-markerable position', | ||
section.isMarkerable); | ||
let edit = section.splitMarkerAtOffset(offset); | ||
edit.removed.forEach(marker => this._scheduleForRemoval(marker)); | ||
let prevMarker = section.markerBeforeOffset(offset); | ||
markers.forEach(marker => { | ||
section.markers.insertAfter(marker, prevMarker); | ||
offset += marker.length; | ||
prevMarker = marker; | ||
}); | ||
this._coalesceMarkers(section); | ||
this._markDirty(section); | ||
let nextPosition = new Position(position.section, offset); | ||
this.setRange(new Range(nextPosition)); | ||
return nextPosition; | ||
} | ||
insertText(position, text) { | ||
@@ -755,3 +774,4 @@ let section = position.section; | ||
if (nextNewSection.isMarkupSection && section.isListItem) { | ||
// put the new section after the ListSection (section.parent) instead of after the ListItem | ||
// put the new section after the ListSection (section.parent) | ||
// instead of after the ListItem | ||
collection = section.parent.parent.sections; | ||
@@ -930,2 +950,78 @@ nextSection = section.parent.next; | ||
/** | ||
* Splits the item at the position given. | ||
* If thse position is at the start or end of the item, the pre- or post-item | ||
* will contain a single empty ("") marker. | ||
* @return {Array} the pre-item and post-item on either side of the split | ||
*/ | ||
_splitListItem(item, position) { | ||
let { section, offset } = position; | ||
assert('Cannot split list item at position that does not include item', | ||
item === section); | ||
item.splitMarkerAtOffset(offset); | ||
let prevMarker = item.markerBeforeOffset(offset); | ||
let preItem = this.builder.createListItem(), | ||
postItem = this.builder.createListItem(); | ||
let currentItem = preItem; | ||
item.markers.forEach(marker => { | ||
currentItem.markers.append(marker.clone()); | ||
if (marker === prevMarker) { | ||
currentItem = postItem; | ||
} | ||
}); | ||
this._replaceSection(item, [preItem, postItem]); | ||
return [preItem, postItem]; | ||
} | ||
/** | ||
* Splits the list at the position given. | ||
* @return {Array} pre-split list and post-split list, either of which could | ||
* be blank (0-item list) if the position is at the start or end of the list. | ||
* | ||
* Note: Contiguous list sections will be joined in the before_complete queue | ||
* of the postEditor. | ||
*/ | ||
_splitListAtPosition(list, position) { | ||
assert('Cannot split list at position not in list', | ||
position.section.parent === list); | ||
let positionIsMiddle = !position.isHead() && !position.isTail(); | ||
if (positionIsMiddle) { | ||
let item = position.section; | ||
let [pre, post] = // jshint ignore:line | ||
this._splitListItem(item, position); | ||
position = pre.tailPosition(); | ||
} | ||
let positionIsStart = position.isEqual(list.headPosition()), | ||
positionIsEnd = position.isEqual(list.tailPosition()); | ||
if (positionIsStart || positionIsEnd) { | ||
let blank = this.builder.createListSection(list.tagName); | ||
let reference = position.isEqual(list.headPosition()) ? list : | ||
list.next; | ||
let collection = this.editor.post.sections; | ||
this.insertSectionBefore(collection, blank, reference); | ||
let lists = positionIsStart ? [blank, list] : [list, blank]; | ||
return lists; | ||
} else { | ||
let preList = this.builder.createListSection(list.tagName), | ||
postList = this.builder.createListSection(list.tagName); | ||
let preItem = position.section; | ||
let currentList = preList; | ||
list.items.forEach(item => { | ||
currentList.items.append(item.clone()); | ||
if (item === preItem) { | ||
currentList = postList; | ||
} | ||
}); | ||
this._replaceSection(list, [preList, postList]); | ||
return [preList, postList]; | ||
} | ||
} | ||
/** | ||
* @return Array of [prev, mid, next] lists. `prev` and `next` can | ||
@@ -981,6 +1077,7 @@ * be blank, depending on the position of `item`. `mid` will always | ||
_changeSectionFromListItem(section, newTagName) { | ||
assert('Must pass list item to `_changeSectionFromListItem`', section.isListItem); | ||
let { builder } = this; | ||
assert('Must pass list item to `_changeSectionFromListItem`', | ||
section.isListItem); | ||
let listSection = section.parent; | ||
let markupSection = builder.createMarkupSection(newTagName); | ||
let markupSection = this.builder.createMarkupSection(newTagName); | ||
markupSection.join(section); | ||
@@ -1070,42 +1167,8 @@ | ||
* @param {Post} post | ||
* @return {Position} position at end of inserted content | ||
* @private | ||
*/ | ||
insertPost(position, newPost) { | ||
if (newPost.isBlank) { | ||
return position; | ||
} | ||
const post = this.editor.post; | ||
let [preSplit, postSplit] = this.splitSection(position); | ||
let nextPosition = position.clone(); | ||
newPost.sections.forEach((section, index) => { | ||
if (index === 0 && preSplit.canJoin(section)) { | ||
preSplit.join(section); | ||
this._markDirty(preSplit); | ||
nextPosition = preSplit.tailPosition(); | ||
} else { | ||
section = section.clone(); | ||
this.insertSectionBefore(post.sections, section, postSplit); | ||
nextPosition = section.tailPosition(); | ||
} | ||
}); | ||
if (postSplit.isBlank) { | ||
this.removeSection(postSplit); | ||
} | ||
if (preSplit.canJoin(postSplit) && preSplit.next === postSplit) { | ||
nextPosition = preSplit.tailPosition(); | ||
preSplit.join(postSplit); | ||
this._markDirty(preSplit); | ||
this.removeSection(postSplit); | ||
} else if (preSplit.isBlank) { | ||
this.removeSection(preSplit); | ||
} | ||
let post = this.editor.post; | ||
let inserter = new PostInserter(this, post); | ||
let nextPosition = inserter.insert(position, newPost); | ||
return nextPosition; | ||
@@ -1194,3 +1257,3 @@ } | ||
/** | ||
* Flush any work on the queue. `editor.run` already does this, calling this | ||
* Flush any work on the queue. `editor.run` already does this. Calling this | ||
* method directly should not be needed outside `editor.run`. | ||
@@ -1202,5 +1265,3 @@ * | ||
complete() { | ||
if (this._didComplete) { | ||
throw new Error('Post editing can only be completed once'); | ||
} | ||
assert('Post editing can only be completed once', !this._didComplete); | ||
@@ -1207,0 +1268,0 @@ this.runCallbacks(CALLBACK_QUEUES.BEFORE_COMPLETE); |
@@ -7,2 +7,3 @@ import { forEach, filter, reduce } from '../utils/array-utils'; | ||
import Position from '../utils/cursor/position'; | ||
import assert from '../utils/assert'; | ||
@@ -15,3 +16,7 @@ export default class Markerable extends Section { | ||
this.markers = new LinkedList({ | ||
adoptItem: m => m.section = m.parent = this, | ||
adoptItem: m => { | ||
assert(`Cannot insert non-marker into markerable (was: ${m.type})`, | ||
m.isMarker); | ||
m.section = m.parent = this; | ||
}, | ||
freeItem: m => m.section = m.parent = null | ||
@@ -116,15 +121,37 @@ }); | ||
* @param {Number} sectionOffset The offset relative to start of this section | ||
* @return {EditObject} An edit object with 'removed' and 'added' keys with arrays of Markers | ||
* @return {EditObject} An edit object with 'removed' and 'added' keys with arrays of Markers. The added markers may be blank. | ||
* After calling `splitMarkerAtOffset(offset)`, there will always be a valid | ||
* result returned from `markerBeforeOffset(offset)`. | ||
*/ | ||
splitMarkerAtOffset(sectionOffset) { | ||
const edit = {removed:[], added:[]}; | ||
const {marker,offset} = this.markerPositionAtOffset(sectionOffset); | ||
if (!marker) { return edit; } | ||
assert('Cannot splitMarkerAtOffset when offset is > length', | ||
sectionOffset <= this.length); | ||
let markerOffset; | ||
let len = 0; | ||
let currentMarker = this.markers.head; | ||
let edit = {added: [], removed: []}; | ||
const newMarkers = filter(marker.split(offset), m => !m.isEmpty); | ||
this.markers.splice(marker, 1, newMarkers); | ||
if (!currentMarker) { | ||
let blankMarker = this.builder.createMarker(); | ||
this.markers.prepend(blankMarker); | ||
edit.added.push(blankMarker); | ||
} else { | ||
while (currentMarker) { | ||
len += currentMarker.length; | ||
if (len === sectionOffset) { | ||
// nothing to do, there is a gap at the requested offset | ||
break; | ||
} else if (len > sectionOffset) { | ||
markerOffset = currentMarker.length - (len - sectionOffset); | ||
let newMarkers = currentMarker.splitAtOffset(markerOffset); | ||
edit.added.push(...newMarkers); | ||
edit.removed.push(currentMarker); | ||
this.markers.splice(currentMarker, 1, newMarkers); | ||
break; | ||
} else { | ||
currentMarker = currentMarker.next; | ||
} | ||
} | ||
} | ||
edit.removed = [marker]; | ||
edit.added = newMarkers; | ||
return edit; | ||
@@ -138,2 +165,21 @@ } | ||
// returns the marker just before this offset. | ||
// It is an error to call this method with an offset that is in the middle | ||
// of a marker. | ||
markerBeforeOffset(sectionOffset) { | ||
let len = 0; | ||
let currentMarker = this.markers.head; | ||
while (currentMarker) { | ||
len += currentMarker.length; | ||
if (len === sectionOffset) { | ||
return currentMarker; | ||
} else { | ||
assert('markerBeforeOffset called with sectionOffset not between markers', | ||
len < sectionOffset); | ||
currentMarker = currentMarker.next; | ||
} | ||
} | ||
} | ||
markerPositionAtOffset(offset) { | ||
@@ -140,0 +186,0 @@ let currentOffset = 0; |
@@ -20,2 +20,3 @@ import { LIST_ITEM_TYPE } from './types'; | ||
this.isMarkerable = false; | ||
this.isNested = false; | ||
} | ||
@@ -22,0 +23,0 @@ |
@@ -16,2 +16,3 @@ import Markerable from './_markerable'; | ||
this.isListItem = true; | ||
this.isNested = true; | ||
} | ||
@@ -18,0 +19,0 @@ |
@@ -6,2 +6,3 @@ import LinkedList from '../utils/linked-list'; | ||
import { normalizeTagName } from '../utils/dom-utils'; | ||
import assert from '../utils/assert'; | ||
@@ -21,3 +22,7 @@ export const VALID_LIST_SECTION_TAGNAMES = [ | ||
this.items = new LinkedList({ | ||
adoptItem: i => i.section = i.parent = this, | ||
adoptItem: i => { | ||
assert(`Cannot insert non-list-item to list (is: ${i.type})`, | ||
i.isListItem); | ||
i.section = i.parent = this; | ||
}, | ||
freeItem: i => i.section = i.parent = null | ||
@@ -51,3 +56,3 @@ }); | ||
clone() { | ||
let newSection = this.builder.createListSection(); | ||
let newSection = this.builder.createListSection(this.tagName); | ||
forEach(this.items, i => newSection.items.append(i.clone())); | ||
@@ -54,0 +59,0 @@ return newSection; |
import { MARKER_TYPE } from './types'; | ||
import { normalizeTagName } from '../utils/dom-utils'; | ||
import { detect, commonItemLength, forEach, filter } from '../utils/array-utils'; | ||
import LinkedItem from '../utils/linked-item'; | ||
import assert from '../utils/assert'; | ||
@@ -13,2 +13,3 @@ const Marker = class Marker extends LinkedItem { | ||
this.type = MARKER_TYPE; | ||
this.isMarker = true; | ||
markups.forEach(m => this.addMarkup(m)); | ||
@@ -120,2 +121,21 @@ } | ||
/** | ||
* @return {Array} 2 markers either or both of which could be blank | ||
*/ | ||
splitAtOffset(offset) { | ||
assert('Cannot split a marker at an offset > its length', | ||
offset <= this.length); | ||
let { value, builder } = this; | ||
let pre = builder.createMarker(value.substring(0, offset)); | ||
let post = builder.createMarker(value.substring(offset)); | ||
this.markups.forEach(markup => { | ||
pre.addMarkup(markup); | ||
post.addMarkup(markup); | ||
}); | ||
return [pre, post]; | ||
} | ||
get openedMarkups() { | ||
@@ -122,0 +142,0 @@ let count = 0; |
@@ -6,2 +6,3 @@ import { POST_TYPE } from './types'; | ||
import mobiledocRenderers from 'mobiledoc-kit/renderers/mobiledoc'; | ||
import Range from 'mobiledoc-kit/utils/cursor/range'; | ||
@@ -103,17 +104,6 @@ export default class Post { | ||
// FIXME if range.head is a listItem this will not work properly | ||
walkPostSections(range, callback) { | ||
const {head, tail} = range; | ||
let currentSection = head.section; | ||
while (currentSection) { | ||
callback(currentSection); | ||
if (currentSection === tail.section) { | ||
break; | ||
} else { | ||
currentSection = currentSection.next; | ||
} | ||
} | ||
walkAllLeafSections(callback) { | ||
let range = new Range(this.sections.head.headPosition(), | ||
this.sections.tail.tailPosition()); | ||
return this.walkLeafSections(range, callback); | ||
} | ||
@@ -222,6 +212,23 @@ | ||
this.walkPostSections(range, section => { | ||
let sectionParent = post, | ||
listParent = null; | ||
this.walkLeafSections(range, section => { | ||
let newSection; | ||
if (section.isMarkerable) { | ||
newSection = builder.createMarkupSection(section.tagName); | ||
if (section.isListItem) { | ||
if (listParent) { | ||
sectionParent = null; | ||
} else { | ||
listParent = builder.createListSection(section.parent.tagName); | ||
post.sections.append(listParent); | ||
sectionParent = null; | ||
} | ||
newSection = builder.createListItem(); | ||
listParent.items.append(newSection); | ||
} else { | ||
listParent = null; | ||
sectionParent = post; | ||
newSection = builder.createMarkupSection(section.tagName); | ||
} | ||
let currentRange = range.trimTo(section); | ||
@@ -235,3 +242,5 @@ forEach( | ||
} | ||
post.sections.append(newSection); | ||
if (sectionParent) { | ||
sectionParent.sections.append(newSection); | ||
} | ||
}); | ||
@@ -238,0 +247,0 @@ return mobiledocRenderers.render(post); |
@@ -36,3 +36,3 @@ import RenderNode from 'mobiledoc-kit/models/render-node'; | ||
* @param {DOMNode} element | ||
* Walk up from the element until we find a renderNode element | ||
* Walk up from the dom element until we find a renderNode element | ||
*/ | ||
@@ -39,0 +39,0 @@ findRenderNodeFromElement(element, conditionFn=()=>true) { |
@@ -12,2 +12,6 @@ import { parseHTML } from '../utils/dom-utils'; | ||
/** | ||
* @param {String} html to parse | ||
* @return {Post} A post abstract | ||
*/ | ||
parse(html) { | ||
@@ -14,0 +18,0 @@ let dom = parseHTML(html); |
@@ -44,14 +44,10 @@ const TEXT_NODE = 3; | ||
function isListSection(section) { | ||
return section.type === LIST_SECTION_TYPE; | ||
} | ||
const SKIPPABLE_ELEMENT_TAG_NAMES = [ | ||
'style', 'head', 'title', 'meta' | ||
].map(normalizeTagName); | ||
function isListItem(section) { | ||
return section.type === LIST_ITEM_TYPE; | ||
} | ||
/** | ||
* parses an element into a section, ignoring any non-markup | ||
* elements contained within | ||
* @return {Section} | ||
* @return {Array} sections | ||
*/ | ||
@@ -65,2 +61,5 @@ export default class SectionParser { | ||
parse(element) { | ||
if (this._isSkippable(element)) { | ||
return []; | ||
} | ||
this.sections = []; | ||
@@ -73,3 +72,3 @@ this.state = {}; | ||
if (isListSection(this.state.section)) { | ||
if (this.state.section.isListSection) { | ||
this.parseListItems(childNodes); | ||
@@ -92,3 +91,3 @@ } else { | ||
let li = parsed[0]; | ||
if (li && isListItem(li)) { | ||
if (li && li.isListItem) { | ||
state.section.items.append(li); | ||
@@ -307,2 +306,7 @@ } | ||
_isSkippable(element) { | ||
return element.nodeType === ELEMENT_NODE && | ||
contains(SKIPPABLE_ELEMENT_TAG_NAMES, | ||
normalizeTagName(element.tagName)); | ||
} | ||
} |
@@ -396,5 +396,9 @@ import CardNode from 'mobiledoc-kit/models/card-node'; | ||
this.nodes = []; | ||
this.hasRendered = false; | ||
} | ||
destroy() { | ||
if (!this.hasRendered) { | ||
return; | ||
} | ||
let renderNode = this.renderTree.rootNode; | ||
@@ -417,2 +421,3 @@ let force = true; | ||
render(renderTree) { | ||
this.hasRendered = true; | ||
this.renderTree = renderTree; | ||
@@ -419,0 +424,0 @@ let renderNode = renderTree.rootNode; |
@@ -28,7 +28,9 @@ import { | ||
hasCursor() { | ||
return this._hasCollapsedSelection() || this._hasSelection(); | ||
return this.editor.hasRendered && | ||
(this._hasCollapsedSelection() || this._hasSelection()); | ||
} | ||
hasSelection() { | ||
return this._hasSelection(); | ||
return this.editor.hasRendered && | ||
this._hasSelection(); | ||
} | ||
@@ -35,0 +37,0 @@ |
@@ -71,2 +71,18 @@ import { | ||
isHead() { | ||
return this.isEqual(this.section.headPosition()); | ||
} | ||
isTail() { | ||
return this.isEqual(this.section.tailPosition()); | ||
} | ||
/** | ||
* This method returns a new Position instance, it does not modify | ||
* this instance. | ||
* | ||
* @param {Direction} direction to move | ||
* @return {Position|null} Return the position one unit in the given | ||
* direction, or null if it is not possible to move that direction | ||
*/ | ||
move(direction) { | ||
@@ -83,19 +99,23 @@ switch (direction) { | ||
/** | ||
* @return {Position|null} | ||
*/ | ||
moveLeft() { | ||
if (this.offset > 0) { | ||
if (this.isHead()) { | ||
let prev = this.section.previousLeafSection(); | ||
return prev && prev.tailPosition(); | ||
} else { | ||
return new Position(this.section, this.offset - 1); | ||
} else if (this.section.prev) { | ||
return new Position(this.section.prev, this.section.prev.length); | ||
} else { | ||
return null; | ||
} | ||
} | ||
/** | ||
* @return {Position|null} | ||
*/ | ||
moveRight() { | ||
if (this.offset < this.section.length) { | ||
if (this.isTail()) { | ||
let next = this.section.nextLeafSection(); | ||
return next && next.headPosition(); | ||
} else { | ||
return new Position(this.section, this.offset + 1); | ||
} else if (this.section.next) { | ||
return new Position(this.section.next, 0); | ||
} else { | ||
return null; | ||
} | ||
@@ -120,3 +140,4 @@ } | ||
if (!section) { throw new Error(`Could not find parent section for mapped text node "${textNode.textContent}"`); } | ||
assert(`Could not find parent section for mapped text node "${textNode.textContent}"`, | ||
!!section); | ||
offsetInSection = section.offsetOfMarker(marker, offsetInNode); | ||
@@ -129,3 +150,4 @@ } else { | ||
section = findParentSectionFromNode(renderTree, textNode); | ||
if (!section) { throw new Error(`Could not find parent section for un-mapped text node "${textNode.textContent}"`); } | ||
assert(`Could not find parent section for un-mapped text node "${textNode.textContent}"`, | ||
!!section); | ||
@@ -177,3 +199,3 @@ offsetInSection = findOffsetInSection(section, textNode, offsetInNode); | ||
get markerPosition() { | ||
if (!this.section) { throw new Error('cannot get markerPosition without a section'); } | ||
assert('Cannot get markerPosition without a section', !!this.section); | ||
return this.section.markerPositionAtOffset(this.offset); | ||
@@ -180,0 +202,0 @@ } |
/* global JSON */ | ||
import mobiledocParsers from '../parsers/mobiledoc'; | ||
import HTMLParser from '../parsers/html'; | ||
import TextParser from '../parsers/text'; | ||
import HTMLRenderer from 'mobiledoc-html-renderer'; | ||
import TextRenderer from 'mobiledoc-text-renderer'; | ||
const MOBILEDOC_REGEX = new RegExp(/data\-mobiledoc='(.*?)'>/); | ||
export const MIME_TEXT_PLAIN = 'text/plain'; | ||
export const MIME_TEXT_HTML = 'text/html'; | ||
function parsePostFromHTML(html, builder, plugins) { | ||
let post; | ||
if (MOBILEDOC_REGEX.test(html)) { | ||
let mobiledocString = html.match(MOBILEDOC_REGEX)[1]; | ||
let mobiledoc = JSON.parse(mobiledocString); | ||
post = mobiledocParsers.parse(builder, mobiledoc); | ||
} else { | ||
post = new HTMLParser(builder, {plugins}).parse(html); | ||
} | ||
return post; | ||
} | ||
function parsePostFromText(text, builder, plugins) { | ||
let parser = new TextParser(builder, {plugins}); | ||
let post = parser.parse(text); | ||
return post; | ||
} | ||
/** | ||
* @param {Event} copyEvent | ||
* @param {Editor} | ||
* @return null | ||
*/ | ||
export function setClipboardCopyData(copyEvent, editor) { | ||
@@ -15,31 +45,29 @@ const { cursor, post } = editor; | ||
let unknownCardHandler = () => {}; // ignore unknown cards | ||
let {result: innerHTML } = new HTMLRenderer({unknownCardHandler}) | ||
.render(mobiledoc); | ||
let {result: innerHTML } = | ||
new HTMLRenderer({unknownCardHandler}).render(mobiledoc); | ||
const html = | ||
`<div data-mobiledoc='${JSON.stringify(mobiledoc)}'>${innerHTML}</div>`; | ||
const {result: plain} = new TextRenderer({unknownCardHandler}) | ||
.render(mobiledoc); | ||
const {result: plain} = | ||
new TextRenderer({unknownCardHandler}).render(mobiledoc); | ||
clipboardData.setData('text/plain', plain); | ||
clipboardData.setData('text/html', html); | ||
clipboardData.setData(MIME_TEXT_PLAIN, plain); | ||
clipboardData.setData(MIME_TEXT_HTML, html); | ||
} | ||
/** | ||
* @param {Event} pasteEvent | ||
* @param {PostNodeBuilder} builder | ||
* @param {Array} plugins parser plugins | ||
* @return {Post} | ||
*/ | ||
export function parsePostFromPaste(pasteEvent, builder, plugins=[]) { | ||
let mobiledoc, post; | ||
const mobiledocRegex = new RegExp(/data\-mobiledoc='(.*?)'>/); | ||
let post; | ||
let html = pasteEvent.clipboardData.getData(MIME_TEXT_HTML); | ||
let html = pasteEvent.clipboardData.getData('text/html'); | ||
// Fallback to 'text/plain' | ||
if (!html || html.length === 0) { | ||
html = pasteEvent.clipboardData.getData('text/plain'); | ||
} | ||
if (mobiledocRegex.test(html)) { | ||
let mobiledocString = html.match(mobiledocRegex)[1]; | ||
mobiledoc = JSON.parse(mobiledocString); | ||
post = mobiledocParsers.parse(builder, mobiledoc); | ||
if (!html || html.length === 0) { // Fallback to 'text/plain' | ||
let text = pasteEvent.clipboardData.getData(MIME_TEXT_PLAIN); | ||
post = parsePostFromText(text, builder, plugins); | ||
} else { | ||
post = new HTMLParser(builder, {plugins}).parse(html); | ||
post = parsePostFromHTML(html, builder, plugins); | ||
} | ||
@@ -46,0 +74,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2157130
127
30870