Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/scripts/containers/FormatterContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import ListFormatter from '../modules/ListFormatter';
import LinkFormatter from '../modules/LinkFormatter';
import Commands from '../modules/Commands';
import Paste from '../modules/Paste';
import Undo from '../modules/Undo';

/**
* @constructor FormatterContainer
Expand Down Expand Up @@ -63,6 +64,9 @@ const FormatterContainer = Container({
},
{
class: Paste
},
{
class: Undo
}
]
});
Expand Down
29 changes: 20 additions & 9 deletions src/scripts/modules/BaseFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ let validTags, blockTags, listTags;

const BaseFormatter = Module({
name: 'BaseFormatter',
props: {},
props: {
cachedRangeCoordinates: null
},
handlers: {
requests: {},
commands: {
Expand Down Expand Up @@ -62,6 +64,8 @@ const BaseFormatter = Module({
const { mediator } = this;
const rootElement = mediator.get('selection:rootelement');
const canvasBody = mediator.get('canvas:body');

mediator.emit('export:to:canvas:start');
this.injectHooks(rootElement);

const rangeCoordinates = mediator.get('selection:range:coordinates');
Expand All @@ -84,6 +88,8 @@ const BaseFormatter = Module({
const { mediator } = this;
const canvasBody = mediator.get('canvas:body');

mediator.emit('import:from:canvas:start');

mediator.exec('canvas:cache:selection');
mediator.exec('format:clean', canvasBody);
if (opts.importFilter) {
Expand Down Expand Up @@ -124,19 +130,24 @@ const BaseFormatter = Module({
this.ensureRootElems(rootElem);
this.removeStyleAttributes(rootElem);
this.removeEmptyNodes(rootElem, { recursive: true });
this.removeZeroWidthSpaces(rootElem);
DOM.trimNodeText(rootElem);

// -----

// this.removeBrNodes(rootElem);
// // this.removeEmptyNodes(rootElem);
// this.removeFontTags(rootElem);
// this.removeStyledSpans(rootElem);
// this.clearEntities(rootElem);
// this.removeZeroWidthSpaces(rootElem);
// this.defaultOrphanedTextNodes(rootElem);
// this.removeEmptyNodes(rootElem, { recursive: true });
},

/**
* PRIVATE METHODS:
*/
cloneNodes (rootElement) {
let clonedNodes = [];
rootElement.childNodes.forEach((node) => {
clonedNodes.push(node.cloneNode(true));
});
return clonedNodes;
},

injectHooks (rootElement) {
while (!/\w+/.test(rootElement.firstChild.textContent)) {
DOM.removeNode(rootElement.firstChild);
Expand Down
40 changes: 38 additions & 2 deletions src/scripts/modules/ContentEditable.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ const ContentEditable = Module({
name: 'ContentEditable',
props: {
styles: null,
cleanupTimeout: null
cleanupTimeout: null,
observer: null,
observerConfig: {
attributes: false,
childList: true,
subtree: true
}
},
dom: {},
handlers: {
Expand All @@ -53,6 +59,9 @@ const ContentEditable = Module({
'contenteditable:refocus' : 'reFocus',
'contenteditable:cleanup' : 'cleanup'
},
events: {
'app:destroy': 'destroy'
},
domEvents: {
'focus' : 'handleFocus',
'keydown' : 'handleKeydown',
Expand All @@ -75,6 +84,7 @@ const ContentEditable = Module({
this.ensureEditable();
this.updatePlaceholderState();
this.updateValue();
this.initObserver();
},

appendStyles () {
Expand Down Expand Up @@ -122,6 +132,19 @@ const ContentEditable = Module({
}
},

initObserver () {
const { dom, props } = this;
const rootEl = dom.el[0];

props.observer = new MutationObserver(this.observerCallback);
props.observer.observe(rootEl, props.observerConfig);
},

observerCallback () {
const { mediator } = this;
mediator.emit('contenteditable:mutation:observed');
},

ensureDefaultBlock () {
const { dom, mediator } = this;
const rootEl = dom.el[0];
Expand Down Expand Up @@ -206,6 +229,11 @@ const ContentEditable = Module({
}
},

destroy () {
const { props } = this;
props.observer.disconnect();
},

// DOM Event Handlers

/**
Expand All @@ -215,10 +243,18 @@ const ContentEditable = Module({
* @fires contenteditable:focus
*/
handleFocus () {
const { mediator } = this;
const { mediator, dom } = this;
this.clearCleanupTimeout();
this.ensureDefaultBlock();
this.updatePlaceholderState();

// Trim out orphaned empty root level text nodes. Should maybe move this somewhere else.
dom.el[0].childNodes.forEach((childNode) => {
if (childNode.nodeType === Node.TEXT_NODE && !childNode.textContent.trim().length) {
DOM.removeNode(childNode);
}
});

mediator.emit('contenteditable:focus');
},

Expand Down
4 changes: 2 additions & 2 deletions src/scripts/modules/Selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,12 +340,12 @@ const Selection = Module({
startCoordinates.unshift(startOffset);
endCoordinates.unshift(endOffset);

while (!this.isContentEditable(startContainer)) {
while (startContainer && !this.isContentEditable(startContainer)) {
startCoordinates.unshift(DOM.childIndex(startContainer));
startContainer = startContainer.parentNode;
}

while (!this.isContentEditable(endContainer)) {
while (endContainer && !this.isContentEditable(endContainer)) {
endCoordinates.unshift(DOM.childIndex(endContainer));
endContainer = endContainer.parentNode;
}
Expand Down
151 changes: 151 additions & 0 deletions src/scripts/modules/Undo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import Module from '../core/Module';
import DOM from '../utils/DOM';

const Undo = Module({
name: 'Undo',
props: {
contentEditableElem: null,
currentHistoryIndex: -1,
history: [],
ignoreSelectionChanges: false
},

handlers: {
events: {
'contenteditable:mutation:observed': 'handleMutation',
'contenteditable:focus': 'handleFocus',
'import:from:canvas:start': 'handleImportStart',
'import:from:canvas:complete': 'handleImportComplete',
'selection:change': 'handleSelectionChange',
'export:to:canvas:start': 'handleExportStart'
}
},

methods: {
setup () {},
init () {},

handleMutation () {
const { props, mediator } = this;
const { history, currentHistoryIndex } = props;
const states = {
currentHistoryIndex,
current: this.createHistoryState(),
previous: history[currentHistoryIndex],
beforePrevious: history[currentHistoryIndex - 1],
next: history[currentHistoryIndex + 1],
afterNext: history[currentHistoryIndex + 2]
};

const {
isUndo,
isRedo,
noChange
} = this.analyzeStates(states);

if (noChange) {
return;
} else if (!isUndo && !isRedo) {
props.history.length = currentHistoryIndex + 1;
props.history.push(states.current);
props.currentHistoryIndex += 1;
} else if (isUndo) {
props.currentHistoryIndex -= 1;
mediator.exec('format:clean', props.contentEditableElem);
mediator.exec('selection:select:coordinates', states.beforePrevious.selectionRangeCoordinates);
} else if (isRedo) {
props.currentHistoryIndex += 1;
mediator.exec('format:clean', props.contentEditableElem);
mediator.exec('selection:select:coordinates', states.next.selectionRangeCoordinates);
}
},

handleFocus () {
const { mediator, props } = this;
const contentEditableElem = mediator.get('contenteditable:element');

if (props.contentEditableElem !== contentEditableElem) {
setTimeout(() => {
props.contentEditableElem = contentEditableElem;
props.history = [this.createHistoryState()];
props.currentHistoryIndex = 0;
}, 150);
}
},

handleImportStart () {
const { props } = this;
props.ignoreSelectionChanges = true;
},

handleImportComplete () {
const { props } = this;
props.ignoreSelectionChanges = false;
},

handleExportStart () {
this.updateCurrentHistoryState();
},

handleSelectionChange () {
const { props } = this;
if (!props.ignoreSelectionChanges) {
this.updateCurrentHistoryState();
}
},

updateCurrentHistoryState () {
const { props } = this;
const { history, currentHistoryIndex } = props;
const currentHistoryState = history[currentHistoryIndex];

if (currentHistoryState) {
this.cacheSelectionRangeOnState(currentHistoryState);
}
},

createHistoryState () {
const { props } = this;

if (!props.contentEditableElem) { return; }

const editableContentString = DOM.nodesToHTMLString(DOM.cloneNodes(props.contentEditableElem, { trim: true })).replace(/\u200B/g, '');
const historyState = {
editableContentString,
};

this.cacheSelectionRangeOnState(historyState);

return historyState;
},

cacheSelectionRangeOnState (state) {
const { mediator } = this;
state.selectionRangeCoordinates = mediator.get('selection:range:coordinates');
},

analyzeStates (states) {
const {
current,
previous,
beforePrevious,
next
} = states;
let isUndo = beforePrevious && current.editableContentString === beforePrevious.editableContentString;
let isRedo = next && current.editableContentString === next.editableContentString;
let noChange = previous && current.editableContentString === previous.editableContentString;

isUndo = isUndo || false;
isRedo = isRedo || false;
noChange = noChange || false;

return {
isUndo,
isRedo,
noChange
};
}
}
});

export default Undo;
4 changes: 3 additions & 1 deletion src/scripts/utils/DOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,9 @@ const DOM = {

nodes.forEach((node) => {
if (node.nodeType === Node.TEXT_NODE) {
HTMLString += node.textContent;
if(node.textContent.match(/\w+/)) {
HTMLString += node.textContent;
}
} else {
HTMLString += node.outerHTML;
}
Expand Down
17 changes: 14 additions & 3 deletions test/server/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ <h1>Typester test server</h1>
node.childNodes.forEach(function (childNode) {
if (childNode.nodeType === Node.TEXT_NODE) {
if (childNode.textContent.trim().length) {
appendHtmlText(childNode.textContent.replace(/\s/g, '\u00B7'), opts.indentation + 1, true);
if (childNode.textContent.match(/\u200B/g)) {
appendHtmlText('<selection-hook />', opts.indentation, true);
} else {
appendHtmlText(childNode.textContent.replace(/\s/g, '\u00B7'), opts.indentation + 1, true);
}
}
} else {
appendHtmlText(generateHtmlText(childNode, {
Expand Down Expand Up @@ -208,10 +212,17 @@ <h1>Typester test server</h1>
generateHtmlText(targetEl);
contentInspector.innerText = generateHtmlText(targetEl);
hljs.highlightBlock(contentInspector);
requestAnimationFrame(updateInspector);
// requestAnimationFrame(updateInspector);
};

requestAnimationFrame(updateInspector);
const observerConfig = { attributes: true, childList: true, subtree: true };
const editorObserver = new MutationObserver(updateInspector);
const canvasObserver = new MutationObserver(updateInspector);

editorObserver.observe(contentEditable, observerConfig);
canvasObserver.observe(document.querySelector('.typester-canvas'), observerConfig);
updateInspector();
// requestAnimationFrame(updateInspector);
})();
</script>
</body>
Expand Down
Loading