src/browser/notes/note-editor.js
/*****************************************************************
* Contains code to determine and change the current state
* of the note. For example to determine when the note is
* currently editable, complete and to make it editable.
* Only deals with the DOM side of things.
*
* @author : Abijeet Patro
****************************************************************/
'use strict';
var _appConfig = require(__dirname + '/../../../config.js');
var _marked = require('mark-it-down');
var NoteEditor = function() {
const NOTE_COMPLETE_CLASS = 'complete';
const DEFAULT_NOTE_CLASS = 'note';
const NOTE_NOT_EDITABLE_CLASS = 'readonly';
const NOTE_TO_BE_MOVED = 'modified';
var TEXT_MODIFIERS = {
BOLD: 1,
ITALICS: 2
};
/**
* Checks if a note is editable
* @param {Object} note The note element
* @return {Boolean} Returns true if the note is editable, else false
*/
function _isEditable(note) {
if (!note) {
return false;
}
return note.getAttribute('contenteditable') === "true";
}
function _turnOnEditing(note) {
note.setAttribute('contenteditable', true);
note.focus();
// Now set the cursor at the end.
var range = document.createRange();
range.selectNodeContents(note);
range.collapse(false);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
function _turnOffEditing(note) {
note.setAttribute('contenteditable', false);
}
function _toggleNoteComplete(note) {
var isComplete = false;
if (note.classList.contains(NOTE_COMPLETE_CLASS)) {
isComplete = true;
note.classList.remove(NOTE_COMPLETE_CLASS);
} else {
note.classList.add(NOTE_COMPLETE_CLASS);
}
return isComplete;
}
/**
* Returns the HTML for a new note. Adds default values if `note` object
* is null.
* @param {String} notebookDbID Notebook ID
* @param {Object} note The note object from the database
* @return {String} HTML String for note
*/
function _getNoteHTML(notebookDbID, note, isEditable) {
var noteText = '';
var noteID = '';
var noteClasses = DEFAULT_NOTE_CLASS;
if (note) {
noteText = _marked(note.text);
noteClasses = note.isComplete ? (DEFAULT_NOTE_CLASS + ' ' +
NOTE_COMPLETE_CLASS) : DEFAULT_NOTE_CLASS;
noteID = 'data-noteid="' + note._id + '"';
}
if (!isEditable) {
noteClasses += ' ' + NOTE_NOT_EDITABLE_CLASS;
}
return '<div class="' + noteClasses + '" ' + noteID + ' data-notebookid="' +
notebookDbID + '" tabindex="0">' + noteText +
'</div><div class="pull-right note-footer"></div>';
}
function _isComplete(note) {
if (note.classList.contains(NOTE_COMPLETE_CLASS)) {
return true;
}
return false;
}
// TODO Refactor this code
function handleTextModifier(note, modifierType) {
var sel = window.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0);
var noteStr = note.innerText;
var nmRange = getNormalizedRange(range, note);
var selectedRange = nmRange.endOffset - nmRange.startOffset;
var chunk = {};
var nodeStack = [note],
node, foundStart = false,
stop = false;
var foundStartNode = false;
var charIndex = 0;
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType === 3) {
var nextCharIndex = charIndex + node.length;
if (!foundStart && nmRange.startOffset >= charIndex && nmRange.startOffset <= nextCharIndex) {
// Found the start of the selection..
foundStart = true;
chunk.beforeNode = node;
foundStartNode = true;
}
if (!stop && nmRange.endOffset >= charIndex && nmRange.endOffset <= nextCharIndex) {
// Found the end of the selection..
stop = true;
chunk.afterNode = node;
if (foundStartNode) {
chunk.sameNodes = true;
} else {
chunk.sameNodes = false;
}
}
foundStartNode = false;
charIndex = nextCharIndex;
} else {
var i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
chunk.after = chunk.afterNode.nodeValue.substr(range.endOffset, chunk.afterNode.nodeValue.length);
chunk.before = chunk.beforeNode.nodeValue.substr(0, range.startOffset);
if (modifierType === TEXT_MODIFIERS.BOLD) {
doBoldOrItalics(chunk, 2);
} else if (modifierType === TEXT_MODIFIERS.ITALICS) {
doBoldOrItalics(chunk, 1);
}
var startRangeOffset = range.startOffset;
var endRangeOffset = range.endOffset;
// after first, before later.
// if the node is same, and the node value changes, the range collapses.
if (chunk.sameNodes) {
var finalStr = chunk.afterNode.nodeValue.substr(0, range.endOffset) + chunk.after;
finalStr = chunk.before + finalStr.substr(range.startOffset, finalStr.length);
chunk.afterNode.nodeValue = finalStr;
} else {
chunk.afterNode.nodeValue = chunk.afterNode.nodeValue.substr(0, range.endOffset) + chunk.after;
chunk.beforeNode.nodeValue = chunk.before + chunk.beforeNode.nodeValue.substr(range.startOffset, chunk.beforeNode.nodeValue.length);
}
restoreHighlight(chunk, startRangeOffset, endRangeOffset);
chunk = null;
}
}
function getNormalizedRange(range, note) {
var preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(note);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
var start = preSelectionRange.toString().length;
return {
startOffset: start,
endOffset: start + range.toString().length
};
}
function restoreHighlight(chunk, startRangeOffset, endRangeOffset) {
var sel = window.getSelection();
sel.removeAllRanges();
var range = document.createRange();
if (chunk.added) {
startRangeOffset = startRangeOffset + chunk.added;
if (chunk.sameNodes) {
endRangeOffset = endRangeOffset + chunk.added;
}
}
range.setEnd(chunk.afterNode, endRangeOffset);
range.setStart(chunk.beforeNode, startRangeOffset);
sel.addRange(range);
}
function doBoldOrItalics(chunk, nStars) {
// Look for stars before and after. Is the chunk already marked up?
// note that these regex matches cannot fail
var starsBefore = /(\**$)/.exec(chunk.before)[0];
var starsAfter = /(^\**)/.exec(chunk.after)[0];
var prevStars = Math.min(starsBefore.length, starsAfter.length);
// Remove stars if we have to since the button acts as a toggle.
if ((prevStars >= nStars) && (prevStars !== 2 || nStars !== 1)) {
chunk.before = chunk.before.replace(new RegExp("[*]{" + nStars + "}$", ""), "");
chunk.after = chunk.after.replace(new RegExp("^[*]{" + nStars + "}", ""), "");
chunk.added = -nStars;
} else {
chunk.added = nStars;
// Add the true markup.
var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
chunk.before = chunk.before + markup;
chunk.after = markup + chunk.after;
}
return chunk;
}
function _isReadOnly(currNote) {
if (currNote.classList.contains(NOTE_NOT_EDITABLE_CLASS)) {
return true;
}
return false;
}
function _getCurrentStateOfNote(currNote) {
var noteState = {};
noteState.isComplete = _isComplete(currNote);
noteState.isEditable = _isEditable(currNote);
noteState.isReadOnly = _isReadOnly(currNote);
return noteState;
}
function _markAsComplete(note) {
if (_isEditable(note)) {
// Currently returns false, and we don't allow marking editable notes
// as complete.
return false;
}
if (!note.innerText) {
// TODO Maybe show a message stating that an empty note
// can't be marked as complete.
return false;
}
// Toggle the classes as necessary.
if (_isComplete(note)) {
note.classList.remove(NOTE_COMPLETE_CLASS);
} else {
note.classList.add(NOTE_COMPLETE_CLASS);
}
return true;
}
/**
* Determines the note to be focused once the current note is removed,
* focuses it and then removes the current note.
* @param {note element} note Note element to be removed
* @return {undefined}
*/
function _removeNote(note, autoFocus) {
if (typeof autoFocus === 'undefined') {
autoFocus = true;
}
if (autoFocus) {
setAutoFocus(note);
}
note.parentNode.remove();
}
function _isNoteToBeMoved(note) {
if (note.classList.contains(NOTE_TO_BE_MOVED)) {
return true;
}
return false;
}
function _moveNote(note) {
if (_isComplete(note)) {
moveNoteToBottom(note);
} else {
moveNoteToTop(note);
}
markNoteAsMoved(note);
}
function _toggleNoteToBeMoved(note) {
if (_isNoteToBeMoved(note)) {
markNoteAsMoved(note);
} else {
markNoteAsToBeMoved(note);
}
}
function _getNoteByID(noteID, container) {
var note = null;
if (container) {
note = container.querySelector('.note[data-noteid="' + noteID + '"]');
} else {
note = document.querySelector('.note[data-noteid="' + noteID + '"]');
}
return note;
}
function setAutoFocus(note) {
// Find a note to focus
var parentNodeOfNote = null;
if (note.parentNode.nextElementSibling) {
// Does it have a next sibling??
parentNodeOfNote = note.parentNode.nextElementSibling;
} else if (note.parentNode.previousElementSibling) {
// Does not have a next sibling, does it have
// a previous sibling??
parentNodeOfNote = note.parentNode.previousElementSibling;
}
if (parentNodeOfNote) {
// Now focus that note.
var noteToFocus = parentNodeOfNote.querySelector('.note');
if (noteToFocus) {
noteToFocus.focus();
}
}
}
function getNotesContainer(note) {
let notebookID;
if (typeof note === 'string') {
notebookID = note;
} else {
let notebookDbID = note.dataset['notebookid'];
notebookID = _appConfig.getNotebookContentID(notebookDbID);
}
let notebookContainer = document.getElementById(notebookID);
return notebookContainer.querySelector('.notes-container');
}
function moveNoteToBottom(note) {
let notesContainer = getNotesContainer(note);
let noteParent = note.parentNode;
_removeNote(note, false);
notesContainer.appendChild(noteParent);
}
function moveNoteToTop(note) {
let notesContainer = getNotesContainer(note);
let noteParent = note.parentNode;
_removeNote(note, false);
notesContainer.insertBefore(noteParent, notesContainer.firstChild);
}
function markNoteAsToBeMoved(note) {
note.classList.add(NOTE_TO_BE_MOVED);
}
function markNoteAsMoved(note) {
note.classList.remove(NOTE_TO_BE_MOVED);
}
return {
isEditable: _isEditable,
turnOnEditing: _turnOnEditing,
turnOffEditing: _turnOffEditing,
markAsComplete: _markAsComplete,
getNoteHTML: _getNoteHTML,
isComplete: _isComplete,
toggleNoteComplete: _toggleNoteComplete,
getCurrState: _getCurrentStateOfNote,
removeNote: _removeNote,
TEXT_MODIFIERS: TEXT_MODIFIERS,
getNoteByID: _getNoteByID,
isToBeMoved: _isNoteToBeMoved,
toggleMove: _toggleNoteToBeMoved,
move: _moveNote
};
};
module.exports = new NoteEditor();