diff --git a/src/js/commands/bold.js b/src/js/commands/bold.js
index e9af24aaa..3ee143921 100644
--- a/src/js/commands/bold.js
+++ b/src/js/commands/bold.js
@@ -1,24 +1,36 @@
import TextFormatCommand from './text-format';
-import { getSelectionBlockTagName } from '../utils/selection-utils';
import { inherit } from 'content-kit-utils';
+import Markup from '../models/markup';
+import {
+ any
+} from '../utils/array-utils';
-var RegExpHeadingTag = /^(h1|h2|h3|h4|h5|h6)$/i;
-
-function BoldCommand() {
+function BoldCommand(editor) {
TextFormatCommand.call(this, {
name: 'bold',
tag: 'strong',
mappedTags: ['b'],
button: ''
});
+ this.editor = editor;
}
inherit(BoldCommand, TextFormatCommand);
BoldCommand.prototype.exec = function() {
- // Don't allow executing bold command on heading tags
- if (!RegExpHeadingTag.test(getSelectionBlockTagName())) {
- BoldCommand._super.prototype.exec.call(this);
- }
+ const markup = Markup.ofType('b');
+ this.editor.applyMarkupToSelection(markup);
+};
+
+BoldCommand.prototype.isActive = function() {
+ let val = any(this.editor.activeMarkers, m => {
+ return any(this.mappedTags, tag => m.hasMarkup(tag));
+ });
+ return val;
+};
+
+BoldCommand.prototype.unexec = function() {
+ const markup = Markup.ofType('b');
+ this.editor.removeMarkupFromSelection(markup);
};
export default BoldCommand;
diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js
index a4767db01..fd27193da 100644
--- a/src/js/editor/editor.js
+++ b/src/js/editor/editor.js
@@ -60,7 +60,6 @@ const defaults = {
// in tests
stickyToolbar: false, // !!('ontouchstart' in window),
textFormatCommands: [
- new BoldCommand(),
new ItalicCommand(),
new LinkCommand()
],
@@ -190,10 +189,14 @@ function makeButtons(editor) {
const quoteCommand = new QuoteCommand(editor);
const quoteButton = new ReversibleToolbarButton(quoteCommand, editor);
+ const boldCommand = new BoldCommand(editor);
+ const boldButton = new ReversibleToolbarButton(boldCommand, editor);
+
return [
headingButton,
subheadingButton,
- quoteButton
+ quoteButton,
+ boldButton
];
}
@@ -399,7 +402,10 @@ class Editor {
const markerRenderNode = leftRenderNode;
const marker = markerRenderNode.postNode;
const section = marker.section;
- const [leftMarker, rightMarker] = marker.split(leftOffset);
+ const newMarkers = marker.split(leftOffset);
+
+ // FIXME rightMarker is not guaranteed to be there
+ let [leftMarker, rightMarker] = newMarkers;
section.insertMarkerAfter(leftMarker, marker);
markerRenderNode.scheduleForRemoval();
@@ -458,11 +464,100 @@ class Editor {
this.hasSelection();
}
- getActiveSections() {
- const cursor = this.cursor;
- return cursor.activeSections;
+ /*
+ * @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) {
+ let offset = 0;
+ let foundMarkers = [];
+ let toEnd = endOffset === undefined;
+ if (toEnd) { endOffset = 0; }
+
+ markers.forEach(marker => {
+ if (toEnd) {
+ endOffset += marker.length;
+ }
+
+ if (offset >= startOffset && offset < endOffset) {
+ foundMarkers.push(marker);
+ }
+
+ offset += marker.length;
+ });
+
+ 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.activeMarkers;
+ // FIXME-NEXT Now we need to ensure we are using the singleton
+ // markup for the 'B' tag
+ // in order to get http://localhost:4200/tests/?testId=8cb07cab
+ // to pass
+ 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();
}
+
get cursor() {
return new Cursor(this);
}
@@ -615,6 +710,21 @@ class Editor {
return this.cursor.activeSections;
}
+ get activeMarkers() {
+ const {
+ startMarker,
+ endMarker,
+ } = this.cursor.offsets;
+
+ if (!(startMarker && endMarker)) {
+ return [];
+ }
+
+ let activeMarkers = [];
+ this.post.markersFrom(startMarker, endMarker, m => activeMarkers.push(m));
+ return activeMarkers;
+ }
+
/*
* Clear the markups from each of the section's markers
*/
diff --git a/src/js/models/cursor.js b/src/js/models/cursor.js
index 0debefd27..d6c5e1ee8 100644
--- a/src/js/models/cursor.js
+++ b/src/js/models/cursor.js
@@ -34,7 +34,14 @@ export default class Cursor {
get offsets() {
let leftNode, rightNode,
leftOffset, rightOffset;
- const { anchorNode, focusNode, anchorOffset, focusOffset } = this.selection;
+ const selection = this.selection;
+ const { anchorNode, focusNode, anchorOffset, focusOffset } = selection;
+ const { rangeCount } = selection;
+ const range = rangeCount > 0 && selection.getRangeAt(0);
+
+ if (!range) {
+ return {};
+ }
const position = anchorNode.compareDocumentPosition(focusNode);
@@ -54,13 +61,23 @@ export default class Cursor {
const leftRenderNode = this.renderTree.elements.get(leftNode),
rightRenderNode = this.renderTree.elements.get(rightNode);
+ const startMarker = leftRenderNode && leftRenderNode.postNode,
+ endMarker = rightRenderNode && rightRenderNode.postNode;
+
+ const startSection = startMarker && startMarker.section;
+ const endSection = endMarker && endMarker.section;
+
return {
leftNode,
rightNode,
leftOffset,
rightOffset,
leftRenderNode,
- rightRenderNode
+ rightRenderNode,
+ startMarker,
+ endMarker,
+ startSection,
+ endSection
};
}
@@ -116,6 +133,17 @@ export default class Cursor {
this.moveToNode(startNode, startOffset, endNode, endOffset);
}
+ selectMarkers(markers) {
+ const startMarker = markers[0],
+ endMarker = markers[markers.length - 1];
+
+ const startNode = startMarker.renderNode.element,
+ endNode = endMarker.renderNode.element;
+ const startOffset = 0, endOffset = endMarker.length;
+
+ this.moveToNode(startNode, startOffset, endNode, endOffset);
+ }
+
moveToNode(node, offset=0, endNode=node, endOffset=offset) {
let r = document.createRange();
r.setStart(node, offset);
diff --git a/src/js/models/marker.js b/src/js/models/marker.js
index 3b9ad35b8..80824c1b3 100644
--- a/src/js/models/marker.js
+++ b/src/js/models/marker.js
@@ -40,9 +40,9 @@ const Marker = class Marker {
removeMarkup(markup) {
const index = this.markups.indexOf(markup);
- if (index === -1) { throw new Error('Cannot remove markup that is not there.'); }
-
- this.markups.splice(index, 1);
+ if (index !== -1) {
+ this.markups.splice(index, 1);
+ }
}
// delete the character at this offset,
@@ -72,14 +72,27 @@ const Marker = class Marker {
return joined;
}
- split(offset) {
- const [m1, m2] = [
- new Marker(this.value.substr(0, offset)),
- new Marker(this.value.substr(offset))
- ];
- this.markups.forEach(m => {m1.addMarkup(m); m2.addMarkup(m);});
+ split(offset=0, endOffset=this.length) {
+ let markers = [];
+
+ if (offset !== 0) {
+ markers.push(
+ new Marker(this.value.substring(0, offset))
+ );
+ }
+
+ markers.push(
+ new Marker(this.value.substring(offset, endOffset))
+ );
+
+ if (endOffset < this.length) {
+ markers.push(
+ new Marker(this.value.substring(endOffset))
+ );
+ }
- return [m1, m2];
+ this.markups.forEach(mu => markers.forEach(m => m.addMarkup(mu)));
+ return markers;
}
get openedMarkups() {
@@ -88,6 +101,8 @@ const Marker = class Marker {
}
let i;
for (i=0; i {
});
});
});
+
+test('click bold button applies bold to selected text', (assert) => {
+ const done = assert.async();
+
+ setTimeout(() => {
+ assertInactiveToolbarButton(assert, 'bold', 'precond - bold button is not active');
+ clickToolbarButton(assert, 'bold');
+ assertActiveToolbarButton(assert, 'bold');
+
+ assert.hasNoElement('#editor b:contains(THIS)');
+ assert.hasNoElement('#editor b:contains(TEST)');
+ assert.hasElement('#editor b:contains(IS A)');
+
+ assert.selectedText(selectedText);
+
+ clickToolbarButton(assert, 'bold');
+
+ assert.hasNoElement('#editor b:contains(IS A)', 'bold text is no longer bold');
+ assertInactiveToolbarButton(assert, 'bold');
+
+ done();
+ });
+});
+
+// test selecting across markers and boldening
+// test selecting across markers in sections and bolding
diff --git a/tests/unit/models/marker-test.js b/tests/unit/models/marker-test.js
index 07e7d781b..f192dc9a6 100644
--- a/tests/unit/models/marker-test.js
+++ b/tests/unit/models/marker-test.js
@@ -75,7 +75,7 @@ test('a marker can be joined to another', (assert) => {
assert.ok(m3.hasMarkup('i'));
});
-test('a marker can be split into two', (assert) => {
+test('#split splits a marker in 2 when no endOffset is passed', (assert) => {
const m1 = new Marker('hi there!');
m1.addMarkup(new Markup('b'));
@@ -86,3 +86,24 @@ test('a marker can be split into two', (assert) => {
assert.equal(_m1.value, 'hi th');
assert.equal(m2.value, 'ere!');
});
+
+test('#split splits a marker in 3 when endOffset is passed', (assert) => {
+ const m = new Marker('hi there!');
+ m.addMarkup(new Markup('b'));
+
+ const newMarkers = m.split(2, 4);
+
+ assert.equal(newMarkers.length, 3, 'creates 3 new markers');
+ newMarkers.forEach(m => assert.ok(m.hasMarkup('b'), 'marker has markup'));
+
+ assert.equal(newMarkers[0].value, 'hi');
+ assert.equal(newMarkers[1].value, ' t');
+ assert.equal(newMarkers[2].value, 'here!');
+});
+
+test('#split does not create an empty marker if the offset is 0', (assert) => {
+ const m = new Marker('hi there!');
+ const newMarkers = m.split(0);
+ assert.equal(newMarkers.length, 1);
+ assert.equal(newMarkers[0].value, 'hi there!');
+});
diff --git a/tests/unit/models/section-test.js b/tests/unit/models/markup-section-test.js
similarity index 76%
rename from tests/unit/models/section-test.js
rename to tests/unit/models/markup-section-test.js
index 9fc949d03..03981973b 100644
--- a/tests/unit/models/section-test.js
+++ b/tests/unit/models/markup-section-test.js
@@ -4,7 +4,7 @@ import Section from 'content-kit-editor/models/markup-section';
import Marker from 'content-kit-editor/models/marker';
import Markup from 'content-kit-editor/models/markup';
-module('Unit: Section');
+module('Unit: Markup Section');
test('Section exists', (assert) => {
assert.ok(Section);
@@ -115,9 +115,38 @@ test('a section can be split, splitting its markers when multiple markers', (ass
assert.equal(s2.markers[0].value, 'ere!');
});
-// test: a section can parse dom
+test('#splitMarker splits the marker at the offset', (assert) => {
+ const m1 = new Marker('hi ');
+ const m2 = new Marker('there!');
+ const s = new Section('h2', [m1,m2]);
+
+ s.splitMarker(m2, 3);
+ assert.equal(s.markers.length, 3, 'adds a 3rd marker');
+ assert.equal(s.markers[0].value, 'hi ', 'original marker unchanged');
+ assert.equal(s.markers[1].value, 'the');
+ assert.equal(s.markers[2].value, 're!');
+});
+
+test('#splitMarker splits the marker at the end offset if provided', (assert) => {
+ const m1 = new Marker('hi ');
+ const m2 = new Marker('there!');
+ const s = new Section('h2', [m1,m2]);
+
+ s.splitMarker(m2, 1, 3);
+ assert.equal(s.markers.length, 4, 'adds a marker for the split and has one on each side');
+ assert.equal(s.markers[0].value, 'hi ', 'original marker unchanged');
+ assert.equal(s.markers[1].value, 't');
+ assert.equal(s.markers[2].value, 'he');
+ assert.equal(s.markers[3].value, 're!');
+});
+
+test('#splitMarker does not create an empty marker if offset=0', (assert) => {
+ const m1 = new Marker('hi ');
+ const m2 = new Marker('there!');
+ const s = new Section('h2', [m1,m2]);
-// test: a section can clear a range:
-// * truncating the markers on the boundaries
-// * removing the intermediate markers
-// * connecting (but not joining) the truncated boundary markers
+ s.splitMarker(m2, 0);
+ assert.equal(s.markers.length, 2, 'still 2 markers');
+ assert.equal(s.markers[0].value, 'hi ', 'original 1st marker unchanged');
+ assert.equal(s.markers[1].value, 'there!', 'original 2nd marker unchanged');
+});
diff --git a/tests/unit/renderers/editor-dom-test.js b/tests/unit/renderers/editor-dom-test.js
index 0af46e402..28e86741e 100644
--- a/tests/unit/renderers/editor-dom-test.js
+++ b/tests/unit/renderers/editor-dom-test.js
@@ -393,7 +393,6 @@ test('rerender a marker after removing a markup from it (when both markers have
'text1text2
');
});
-
/*
test("It renders a renderTree with rendered dirty section", (assert) => {
/*