Implemented find and replace extension

This commit is contained in:
benweet 2014-07-13 22:45:55 +01:00
parent e01db60b07
commit 82be3a44f2
12 changed files with 782 additions and 348 deletions

View File

@ -117,34 +117,65 @@ define([
this.selectionStart = 0;
this.selectionEnd = 0;
this.cursorY = 0;
this.findOffset = function(offset) {
this.adjustTop = 0;
this.adjustBottom = 0;
this.findOffsets = function(offsetList) {
var result = [];
if(!offsetList.length) {
return result;
}
var offset = offsetList.shift();
var walker = document.createTreeWalker(contentElt, 4, null, false);
var text = '';
var walkerOffset = 0;
while(walker.nextNode()) {
text = walker.currentNode.nodeValue || '';
if(text.length > offset) {
return {
var newWalkerOffset = walkerOffset + text.length;
while(newWalkerOffset > offset) {
result.push({
container: walker.currentNode,
offsetInContainer: offset - walkerOffset,
offset: offset
};
});
if(!offsetList.length) {
return result;
}
offset = offsetList.shift();
}
offset -= text.length;
walkerOffset = newWalkerOffset;
}
return {
container: walker.currentNode,
offset: text.length
};
do {
result.push({
container: walker.currentNode,
offsetInContainer: walkerOffset,
offset: offset
});
offset = offsetList.shift();
}
while(offset);
return result;
};
this.createRange = function(start, end) {
start = start < 0 ? 0 : start;
end = end < 0 ? 0 : end;
var range = document.createRange();
var offset = _.isObject(start) ? start : this.findOffset(start);
range.setStart(offset.container, offset.offset);
if(end && end != start) {
offset = _.isObject(end) ? end : this.findOffset(end);
var offsetList = [];
if(_.isNumber(start)) {
offsetList.push(start);
start = offsetList.length - 1;
}
range.setEnd(offset.container, offset.offset);
if(_.isNumber(end)) {
offsetList.push(end);
end = offsetList.length - 1;
}
offsetList = this.findOffsets(offsetList);
var startOffset = _.isObject(start) ? start : offsetList[start];
range.setStart(startOffset.container, startOffset.offsetInContainer);
var endOffset = startOffset;
if(end && end != start) {
endOffset = _.isObject(end) ? end : offsetList[end];
}
range.setEnd(endOffset.container, endOffset.offsetInContainer);
return range;
};
var adjustScroll;
@ -154,15 +185,20 @@ define([
if(this.cursorY !== coordinates.y) {
this.cursorY = coordinates.y;
eventMgr.onCursorCoordinates(coordinates.x, coordinates.y);
if(adjustScroll && settings.cursorFocusRatio) {
var adjust = inputElt.offsetHeight / 2 * settings.cursorFocusRatio;
var cursorMinY = inputElt.scrollTop + adjust;
var cursorMaxY = inputElt.scrollTop + inputElt.offsetHeight - adjust;
if(selectionMgr.cursorY < cursorMinY) {
inputElt.scrollTop += selectionMgr.cursorY - cursorMinY;
}
else if(selectionMgr.cursorY > cursorMaxY) {
inputElt.scrollTop += selectionMgr.cursorY - cursorMaxY;
if(adjustScroll) {
var adjustTop, adjustBottom;
adjustTop = adjustBottom = inputElt.offsetHeight / 2 * settings.cursorFocusRatio;
adjustTop = this.adjustTop || adjustTop;
adjustBottom = this.adjustBottom || adjustTop;
if(adjustTop && adjustBottom) {
var cursorMinY = inputElt.scrollTop + adjustTop;
var cursorMaxY = inputElt.scrollTop + inputElt.offsetHeight - adjustBottom;
if(selectionMgr.cursorY < cursorMinY) {
inputElt.scrollTop += selectionMgr.cursorY - cursorMinY;
}
else if(selectionMgr.cursorY > cursorMaxY) {
inputElt.scrollTop += selectionMgr.cursorY - cursorMaxY;
}
}
}
}
@ -253,11 +289,16 @@ define([
}
};
})();
this.getCoordinates = function(inputOffset, container, offset) {
this.getSelectedText = function() {
var min = Math.min(this.selectionStart, this.selectionEnd);
var max = Math.max(this.selectionStart, this.selectionEnd);
return textContent.substring(min, max);
};
this.getCoordinates = function(inputOffset, container, offsetInContainer) {
if(!container) {
offset = this.findOffset(inputOffset);
var offset = this.findOffsets([inputOffset])[0];
container = offset.container;
offset = offset.offset;
offsetInContainer = offset.offsetInContainer;
}
var x = 0;
var y = 0;
@ -268,26 +309,30 @@ define([
var selectedChar = textContent[inputOffset];
var startOffset = {
container: container,
offset: offset
offsetInContainer: offsetInContainer,
offset: inputOffset
};
var endOffset = {
container: container,
offset: offset
offsetInContainer: offsetInContainer,
offset: inputOffset
};
if(inputOffset > 0 && (selectedChar === undefined || selectedChar == '\n')) {
if(startOffset.offset === 0) {
// Need to calculate offset-1
startOffset = inputOffset - 1;
}
else {
startOffset.offset -= 1;
startOffset.offsetInContainer -= 1;
}
}
else {
if(endOffset.offset === container.textContent.length) {
// Need to calculate offset+1
endOffset = inputOffset + 1;
}
else {
endOffset.offset += 1;
endOffset.offsetInContainer += 1;
}
}
var selectionRange = this.createRange(startOffset, endOffset);
@ -351,10 +396,44 @@ define([
range.deleteContents();
range.insertNode(document.createTextNode(replacement));
range.detach();
return {
start: startOffset,
end: value.length - endOffset
};
}
editor.setValue = setValue;
function replace(selectionStart, selectionEnd, replacement) {
undoMgr.currentMode = undoMgr.currentMode || 'replace';
var range = selectionMgr.createRange(selectionStart, selectionEnd);
if('' + range == replacement) {
return;
}
range.deleteContents();
range.insertNode(document.createTextNode(replacement));
range.detach();
var endOffset = selectionStart + replacement.length;
selectionMgr.setSelectionStartEnd(endOffset, endOffset);
selectionMgr.updateSelectionRange();
selectionMgr.updateCursorCoordinates(true);
}
editor.replace = replace;
function replaceAll(search, replacement) {
undoMgr.currentMode = undoMgr.currentMode || 'replace';
var value = textContent.replace(search, replacement);
if(value != textContent) {
var offset = editor.setValue(value);
selectionMgr.setSelectionStartEnd(offset.end, offset.end);
selectionMgr.updateSelectionRange();
selectionMgr.updateCursorCoordinates(true);
}
}
editor.replaceAll = replaceAll;
function replacePreviousText(text, replacement) {
var offset = selectionMgr.selectionStart;
if(offset !== selectionMgr.selectionEnd) {
@ -370,7 +449,7 @@ define([
offset = offset - text.length + replacement.length;
selectionMgr.setSelectionStartEnd(offset, offset);
selectionMgr.updateSelectionRange();
selectionMgr.updateCursorCoordinates();
selectionMgr.updateCursorCoordinates(true);
return true;
}
@ -415,7 +494,11 @@ define([
this.saveState = utils.debounce(function() {
redoStack = [];
var currentTime = Date.now();
if(this.currentMode == 'comment' || lastMode == 'newlines' || this.currentMode != lastMode || currentTime - lastTime > 1000) {
if(this.currentMode == 'comment' ||
this.currentMode == 'replace' ||
lastMode == 'newlines' ||
this.currentMode != lastMode ||
currentTime - lastTime > 1000) {
undoStack.push(currentState);
// Limit the size of the stack
while(undoStack.length > 100) {
@ -441,11 +524,12 @@ define([
this.onButtonStateChange();
}, this);
this.saveSelectionState = _.debounce(function() {
// Should happen just after saveState
if(this.currentMode === undefined) {
selectionStartBefore = selectionMgr.selectionStart;
selectionEndBefore = selectionMgr.selectionEnd;
}
}, 10);
}, 50);
this.canUndo = function() {
return undoStack.length;
};
@ -462,7 +546,7 @@ define([
}
selectionMgr.setSelectionStartEnd(selectionStart, selectionEnd);
selectionMgr.updateSelectionRange();
selectionMgr.updateCursorCoordinates();
selectionMgr.updateCursorCoordinates(true);
var discussionListJSON = fileDesc.discussionListJSON;
if(discussionListJSON != state.discussionListJSON) {
var oldDiscussionList = fileDesc.discussionList;
@ -767,6 +851,12 @@ define([
.on('cut', function() {
undoMgr.currentMode = 'cut';
adjustCursorPosition();
})
.on('focus', function() {
selectionMgr.hasFocus = true;
})
.on('blur', function() {
selectionMgr.hasFocus = false;
});
var action = function(action, options) {

View File

@ -1,307 +1,319 @@
define([
"jquery",
"underscore",
"crel",
"utils",
"logger",
"classes/Extension",
"settings",
"text!html/settingsExtensionsAccordion.html",
"extensions/yamlFrontMatterParser",
"extensions/markdownSectionParser",
"extensions/partialRendering",
"extensions/buttonMarkdownSyntax",
"extensions/googleAnalytics",
"extensions/twitter",
"extensions/dialogAbout",
"extensions/dialogManagePublication",
"extensions/dialogManageSynchronization",
"extensions/dialogManageSharing",
"extensions/dialogOpenHarddrive",
"extensions/documentTitle",
"extensions/documentSelector",
"extensions/documentPanel",
"extensions/documentManager",
"extensions/workingIndicator",
"extensions/notifications",
"jquery",
"underscore",
"crel",
"mousetrap",
"utils",
"logger",
"classes/Extension",
"settings",
"text!html/settingsExtensionsAccordion.html",
"extensions/yamlFrontMatterParser",
"extensions/markdownSectionParser",
"extensions/partialRendering",
"extensions/buttonMarkdownSyntax",
"extensions/googleAnalytics",
"extensions/twitter",
"extensions/dialogAbout",
"extensions/dialogManagePublication",
"extensions/dialogManageSynchronization",
"extensions/dialogManageSharing",
"extensions/dialogOpenHarddrive",
"extensions/documentTitle",
"extensions/documentSelector",
"extensions/documentPanel",
"extensions/documentManager",
"extensions/workingIndicator",
"extensions/notifications",
"extensions/umlDiagrams",
"extensions/markdownExtra",
"extensions/toc",
"extensions/mathJax",
"extensions/emailConverter",
"extensions/scrollSync",
"extensions/buttonSync",
"extensions/buttonPublish",
"extensions/buttonStat",
"extensions/buttonHtmlCode",
"extensions/buttonViewer",
"extensions/welcomeTour",
"extensions/shortcuts",
"extensions/userCustom",
"extensions/comments",
"extensions/htmlSanitizer",
"bootstrap",
"jquery-waitforimages"
], function($, _, crel, utils, logger, Extension, settings, settingsExtensionsAccordionHTML) {
"extensions/markdownExtra",
"extensions/toc",
"extensions/mathJax",
"extensions/emailConverter",
"extensions/scrollSync",
"extensions/buttonSync",
"extensions/buttonPublish",
"extensions/buttonStat",
"extensions/buttonHtmlCode",
"extensions/buttonViewer",
"extensions/welcomeTour",
"extensions/shortcuts",
"extensions/userCustom",
"extensions/comments",
"extensions/findReplace",
"extensions/htmlSanitizer",
"bootstrap",
"jquery-waitforimages"
], function($, _, crel, mousetrap, utils, logger, Extension, settings, settingsExtensionsAccordionHTML) {
var eventMgr = {};
var eventMgr = {};
// Create a list of extensions from module arguments
var extensionList = _.chain(arguments).map(function(argument) {
return argument instanceof Extension && argument;
}).compact().value();
// Create a list of extensions from module arguments
var extensionList = _.chain(arguments).map(function(argument) {
return argument instanceof Extension && argument;
}).compact().value();
// Configure extensions
var extensionSettings = settings.extensionSettings || {};
_.each(extensionList, function(extension) {
// Set the extension.config attribute from settings or default
// configuration
extension.config = _.extend({}, extension.defaultConfig, extensionSettings[extension.extensionId]);
if(window.viewerMode === true && extension.disableInViewer === true) {
// Skip enabling the extension if we are in the viewer and extension
// doesn't support it
extension.enabled = false;
}
else {
// Enable the extension if it's not optional or it has not been
// disabled by the user
extension.enabled = !extension.isOptional || extension.config.enabled === undefined || extension.config.enabled === true;
}
});
// Configure extensions
var extensionSettings = settings.extensionSettings || {};
_.each(extensionList, function(extension) {
// Set the extension.config attribute from settings or default
// configuration
extension.config = _.extend({}, extension.defaultConfig, extensionSettings[extension.extensionId]);
if(window.viewerMode === true && extension.disableInViewer === true) {
// Skip enabling the extension if we are in the viewer and extension
// doesn't support it
extension.enabled = false;
}
else {
// Enable the extension if it's not optional or it has not been
// disabled by the user
extension.enabled = !extension.isOptional || extension.config.enabled === undefined || extension.config.enabled === true;
}
});
// Returns all listeners with the specified name that are implemented in the
// enabled extensions
function getExtensionListenerList(eventName) {
return _.chain(extensionList).map(function(extension) {
return extension.enabled && extension[eventName];
}).compact().value();
}
// Returns all listeners with the specified name that are implemented in the
// enabled extensions
function getExtensionListenerList(eventName) {
return _.chain(extensionList).map(function(extension) {
return extension.enabled && extension[eventName];
}).compact().value();
}
// Returns a function that calls every listeners with the specified name
// from all enabled extensions
var eventListenerListMap = {};
function createEventHook(eventName) {
eventListenerListMap[eventName] = getExtensionListenerList(eventName);
return function() {
logger.log(eventName, arguments);
var eventArguments = arguments;
_.each(eventListenerListMap[eventName], function(listener) {
// Use try/catch in case userCustom listener contains error
try {
listener.apply(null, eventArguments);
}
catch(e) {
console.error(_.isObject(e) ? e.stack : e);
}
});
};
}
// Returns a function that calls every listeners with the specified name
// from all enabled extensions
var eventListenerListMap = {};
// Declare an event Hook in the eventMgr that we can fire using eventMgr.eventName()
function addEventHook(eventName) {
eventMgr[eventName] = createEventHook(eventName);
}
function createEventHook(eventName) {
eventListenerListMap[eventName] = getExtensionListenerList(eventName);
return function() {
logger.log(eventName, arguments);
var eventArguments = arguments;
_.each(eventListenerListMap[eventName], function(listener) {
// Use try/catch in case userCustom listener contains error
try {
listener.apply(null, eventArguments);
}
catch(e) {
console.error(_.isObject(e) ? e.stack : e);
}
});
};
}
// Used by external modules (not extensions) to listen to events
eventMgr.addListener = function(eventName, listener) {
try {
eventListenerListMap[eventName].push(listener);
}
catch(e) {
console.error('No event listener called ' + eventName);
}
};
// Declare an event Hook in the eventMgr that we can fire using eventMgr.eventName()
function addEventHook(eventName) {
eventMgr[eventName] = createEventHook(eventName);
}
// Call every onInit listeners (enabled extensions only)
createEventHook("onInit")();
// Used by external modules (not extensions) to listen to events
eventMgr.addListener = function(eventName, listener) {
try {
eventListenerListMap[eventName].push(listener);
}
catch(e) {
console.error('No event listener called ' + eventName);
}
};
// Load/Save extension config from/to settings
eventMgr.onLoadSettings = function() {
logger.log("onLoadSettings");
_.each(extensionList, function(extension) {
var isChecked = !extension.isOptional || extension.config.enabled === undefined || extension.config.enabled === true;
utils.setInputChecked("#input-enable-extension-" + extension.extensionId, isChecked);
// Special case for Markdown Extra and MathJax
if(extension.extensionId == 'markdownExtra') {
utils.setInputChecked("#input-settings-markdown-extra", isChecked);
}
else if(extension.extensionId == 'mathJax') {
utils.setInputChecked("#input-settings-mathjax", isChecked);
}
var onLoadSettingsListener = extension.onLoadSettings;
onLoadSettingsListener && onLoadSettingsListener();
});
};
eventMgr.onSaveSettings = function(newExtensionSettings, event) {
logger.log("onSaveSettings");
_.each(extensionList, function(extension) {
var newExtensionConfig = _.extend({}, extension.defaultConfig);
newExtensionConfig.enabled = utils.getInputChecked("#input-enable-extension-" + extension.extensionId);
var isChecked;
// Special case for Markdown Extra and MathJax
if(extension.extensionId == 'markdownExtra') {
isChecked = utils.getInputChecked("#input-settings-markdown-extra");
if(isChecked != extension.enabled) {
newExtensionConfig.enabled = isChecked;
}
}
else if(extension.extensionId == 'mathJax') {
isChecked = utils.getInputChecked("#input-settings-mathjax");
if(isChecked != extension.enabled) {
newExtensionConfig.enabled = isChecked;
}
}
var onSaveSettingsListener = extension.onSaveSettings;
onSaveSettingsListener && onSaveSettingsListener(newExtensionConfig, event);
newExtensionSettings[extension.extensionId] = newExtensionConfig;
});
};
// Call every onInit listeners (enabled extensions only)
createEventHook("onInit")();
addEventHook("onMessage");
addEventHook("onError");
addEventHook("onOfflineChanged");
addEventHook("onUserActive");
addEventHook("onAsyncRunning");
addEventHook("onPeriodicRun");
// Load/Save extension config from/to settings
eventMgr.onLoadSettings = function() {
logger.log("onLoadSettings");
_.each(extensionList, function(extension) {
var isChecked = !extension.isOptional || extension.config.enabled === undefined || extension.config.enabled === true;
utils.setInputChecked("#input-enable-extension-" + extension.extensionId, isChecked);
// Special case for Markdown Extra and MathJax
if(extension.extensionId == 'markdownExtra') {
utils.setInputChecked("#input-settings-markdown-extra", isChecked);
}
else if(extension.extensionId == 'mathJax') {
utils.setInputChecked("#input-settings-mathjax", isChecked);
}
var onLoadSettingsListener = extension.onLoadSettings;
onLoadSettingsListener && onLoadSettingsListener();
});
};
eventMgr.onSaveSettings = function(newExtensionSettings, event) {
logger.log("onSaveSettings");
_.each(extensionList, function(extension) {
var newExtensionConfig = _.extend({}, extension.defaultConfig);
newExtensionConfig.enabled = utils.getInputChecked("#input-enable-extension-" + extension.extensionId);
var isChecked;
// Special case for Markdown Extra and MathJax
if(extension.extensionId == 'markdownExtra') {
isChecked = utils.getInputChecked("#input-settings-markdown-extra");
if(isChecked != extension.enabled) {
newExtensionConfig.enabled = isChecked;
}
}
else if(extension.extensionId == 'mathJax') {
isChecked = utils.getInputChecked("#input-settings-mathjax");
if(isChecked != extension.enabled) {
newExtensionConfig.enabled = isChecked;
}
}
var onSaveSettingsListener = extension.onSaveSettings;
onSaveSettingsListener && onSaveSettingsListener(newExtensionConfig, event);
newExtensionSettings[extension.extensionId] = newExtensionConfig;
});
};
// To access modules that are loaded after extensions
addEventHook("onEditorCreated");
addEventHook("onFileMgrCreated");
addEventHook("onSynchronizerCreated");
addEventHook("onPublisherCreated");
addEventHook("onEventMgrCreated");
addEventHook("onMessage");
addEventHook("onError");
addEventHook("onOfflineChanged");
addEventHook("onUserActive");
addEventHook("onAsyncRunning");
addEventHook("onPeriodicRun");
// Operations on files
addEventHook("onFileCreated");
addEventHook("onFileDeleted");
addEventHook("onFileSelected");
addEventHook("onFileOpen");
addEventHook("onFileClosed");
addEventHook("onContentChanged");
addEventHook("onTitleChanged");
// To access modules that are loaded after extensions
addEventHook("onEditorCreated");
addEventHook("onFileMgrCreated");
addEventHook("onSynchronizerCreated");
addEventHook("onPublisherCreated");
addEventHook("onEventMgrCreated");
// Operations on folders
addEventHook("onFoldersChanged");
// Operations on files
addEventHook("onFileCreated");
addEventHook("onFileDeleted");
addEventHook("onFileSelected");
addEventHook("onFileOpen");
addEventHook("onFileClosed");
addEventHook("onContentChanged");
addEventHook("onTitleChanged");
// Sync events
addEventHook("onSyncRunning");
addEventHook("onSyncSuccess");
addEventHook("onSyncImportSuccess");
addEventHook("onSyncExportSuccess");
addEventHook("onSyncRemoved");
// Operations on folders
addEventHook("onFoldersChanged");
// Publish events
addEventHook("onPublishRunning");
addEventHook("onPublishSuccess");
addEventHook("onNewPublishSuccess");
addEventHook("onPublishRemoved");
// Sync events
addEventHook("onSyncRunning");
addEventHook("onSyncSuccess");
addEventHook("onSyncImportSuccess");
addEventHook("onSyncExportSuccess");
addEventHook("onSyncRemoved");
// Operations on Layout
addEventHook("onLayoutCreated");
addEventHook("onLayoutResize");
addEventHook("onExtensionButtonResize");
// Publish events
addEventHook("onPublishRunning");
addEventHook("onPublishSuccess");
addEventHook("onNewPublishSuccess");
addEventHook("onPublishRemoved");
// Operations on editor
addEventHook("onPagedownConfigure");
addEventHook("onSectionsCreated");
addEventHook("onCursorCoordinates");
// Operations on Layout
addEventHook("onLayoutCreated");
addEventHook("onLayoutResize");
addEventHook("onExtensionButtonResize");
// Operations on comments
addEventHook("onDiscussionCreated");
addEventHook("onDiscussionRemoved");
addEventHook("onCommentsChanged");
// Operations on editor
addEventHook("onPagedownConfigure");
addEventHook("onSectionsCreated");
addEventHook("onCursorCoordinates");
addEventHook("onEditorPopover");
// Refresh twitter buttons
addEventHook("onTweet");
// Operations on comments
addEventHook("onDiscussionCreated");
addEventHook("onDiscussionRemoved");
addEventHook("onCommentsChanged");
// Refresh twitter buttons
addEventHook("onTweet");
var onPreviewFinished = createEventHook("onPreviewFinished");
var onAsyncPreviewListenerList = getExtensionListenerList("onAsyncPreview");
var previewContentsElt;
var $previewContentsElt;
eventMgr.onAsyncPreview = function() {
logger.log("onAsyncPreview");
function recursiveCall(callbackList) {
var callback = callbackList.length ? callbackList.shift() : function() {
setTimeout(function() {
var html = "";
_.each(previewContentsElt.children, function(elt) {
html += elt.innerHTML;
});
var htmlWithComments = utils.trim(html);
var htmlWithoutComments = htmlWithComments.replace(/ <span class="comment label label-danger">.*?<\/span> /g, '');
onPreviewFinished(htmlWithComments, htmlWithoutComments);
}, 10);
};
callback(function() {
recursiveCall(callbackList);
});
}
recursiveCall(onAsyncPreviewListenerList.concat([function(callback) {
// We assume some images are loading asynchronously after the preview
$previewContentsElt.waitForImages(callback);
}]));
};
var onPreviewFinished = createEventHook("onPreviewFinished");
var onAsyncPreviewListenerList = getExtensionListenerList("onAsyncPreview");
var previewContentsElt;
var $previewContentsElt;
eventMgr.onAsyncPreview = function() {
logger.log("onAsyncPreview");
function recursiveCall(callbackList) {
var callback = callbackList.length ? callbackList.shift() : function() {
setTimeout(function() {
var html = "";
_.each(previewContentsElt.children, function(elt) {
html += elt.innerHTML;
});
var htmlWithComments = utils.trim(html);
var htmlWithoutComments = htmlWithComments.replace(/ <span class="comment label label-danger">.*?<\/span> /g, '');
onPreviewFinished(htmlWithComments, htmlWithoutComments);
}, 10);
};
callback(function() {
recursiveCall(callbackList);
});
}
var onReady = createEventHook("onReady");
eventMgr.onReady = function() {
previewContentsElt = document.getElementById('preview-contents');
$previewContentsElt = $(previewContentsElt);
recursiveCall(onAsyncPreviewListenerList.concat([
function(callback) {
// We assume some images are loading asynchronously after the preview
$previewContentsElt.waitForImages(callback);
}
]));
};
// Create a button from an extension listener
var createBtn = function(listener) {
var buttonGrpElt = crel('div', {
class: 'btn-group'
});
var btnElt = listener();
if(_.isString(btnElt)) {
buttonGrpElt.innerHTML = btnElt;
}
else if(_.isElement(btnElt)) {
buttonGrpElt.appendChild(btnElt);
}
return buttonGrpElt;
};
var onReady = createEventHook("onReady");
eventMgr.onReady = function() {
previewContentsElt = document.getElementById('preview-contents');
$previewContentsElt = $(previewContentsElt);
if(window.viewerMode === false) {
// Create accordion in settings dialog
var accordionHtml = _.chain(extensionList).sortBy(function(extension) {
return extension.extensionName.toLowerCase();
}).reduce(function(html, extension) {
return html + (extension.settingsBlock ? _.template(settingsExtensionsAccordionHTML, {
extensionId: extension.extensionId,
extensionName: extension.extensionName,
isOptional: extension.isOptional,
settingsBlock: extension.settingsBlock
}) : "");
}, "").value();
document.querySelector('.accordion-extensions').innerHTML = accordionHtml;
// Create a button from an extension listener
var createBtn = function(listener) {
var buttonGrpElt = crel('div', {
class: 'btn-group'
});
var btnElt = listener();
if(_.isString(btnElt)) {
buttonGrpElt.innerHTML = btnElt;
}
else if(_.isElement(btnElt)) {
buttonGrpElt.appendChild(btnElt);
}
return buttonGrpElt;
};
// Create extension buttons
logger.log("onCreateButton");
var onCreateButtonListenerList = getExtensionListenerList("onCreateButton");
var extensionButtonsFragment = document.createDocumentFragment();
_.each(onCreateButtonListenerList, function(listener) {
extensionButtonsFragment.appendChild(createBtn(listener));
});
document.querySelector('.extension-buttons').appendChild(extensionButtonsFragment);
}
if(window.viewerMode === false) {
// Create accordion in settings dialog
var accordionHtml = _.chain(extensionList).sortBy(function(extension) {
return extension.extensionName.toLowerCase();
}).reduce(function(html, extension) {
return html + (extension.settingsBlock ? _.template(settingsExtensionsAccordionHTML, {
extensionId: extension.extensionId,
extensionName: extension.extensionName,
isOptional: extension.isOptional,
settingsBlock: extension.settingsBlock
}) : "");
}, "").value();
document.querySelector('.accordion-extensions').innerHTML = accordionHtml;
// Create extension preview buttons
logger.log("onCreatePreviewButton");
var onCreatePreviewButtonListenerList = getExtensionListenerList("onCreatePreviewButton");
var extensionPreviewButtonsFragment = document.createDocumentFragment();
_.each(onCreatePreviewButtonListenerList, function(listener) {
extensionPreviewButtonsFragment.appendChild(createBtn(listener));
});
var previewButtonsElt = document.querySelector('.extension-preview-buttons');
previewButtonsElt.appendChild(extensionPreviewButtonsFragment);
// Create extension buttons
logger.log("onCreateButton");
var onCreateButtonListenerList = getExtensionListenerList("onCreateButton");
var extensionButtonsFragment = document.createDocumentFragment();
_.each(onCreateButtonListenerList, function(listener) {
extensionButtonsFragment.appendChild(createBtn(listener));
});
document.querySelector('.extension-buttons').appendChild(extensionButtonsFragment);
}
// Call onReady listeners
onReady();
};
// Create extension preview buttons
logger.log("onCreatePreviewButton");
var onCreatePreviewButtonListenerList = getExtensionListenerList("onCreatePreviewButton");
var extensionPreviewButtonsFragment = document.createDocumentFragment();
_.each(onCreatePreviewButtonListenerList, function(listener) {
extensionPreviewButtonsFragment.appendChild(createBtn(listener));
});
var previewButtonsElt = document.querySelector('.extension-preview-buttons');
previewButtonsElt.appendChild(extensionPreviewButtonsFragment);
// For extensions that need to call other extensions
eventMgr.onEventMgrCreated(eventMgr);
return eventMgr;
// Shall close every popover
mousetrap.bind('escape', function() {
eventMgr.onEditorPopover();
});
// Call onReady listeners
onReady();
};
// For extensions that need to call other extensions
eventMgr.onEventMgrCreated(eventMgr);
return eventMgr;
});

View File

@ -226,6 +226,12 @@ define([
currentContext && currentContext.$commentElt.popover('toggle').popover('destroy');
}
comments.onEditorPopover = function() {
closeCurrentPopover();
editor.focus();
editor.adjustCursorPosition();
};
comments.onDiscussionCreated = function(fileDesc) {
currentFileDesc === fileDesc && refreshDiscussions();
};
@ -265,7 +271,7 @@ define([
}
comments.onReady = function() {
cssApplier = rangy.createCssClassApplier("comment-highlight", {
cssApplier = rangy.createCssClassApplier('comment-highlight', {
normalize: false
});
var previousContent = '';
@ -308,7 +314,7 @@ define([
selector: '#wmd-input > .editor-margin > .discussion'
});
$(marginElt).on('show.bs.popover', function(evt) {
closeCurrentPopover();
eventMgr.onEditorPopover();
var context = new Context(evt.target, currentFileDesc);
currentContext = context;
@ -348,18 +354,10 @@ define([
var $addButton = $(popoverElt.querySelector('.action-add-comment'));
$().add(context.$contentInputElt).add(context.$authorInputElt).keydown(function(evt) {
// Enter key
switch(evt.which) {
case 13:
evt.preventDefault();
$addButton.click();
return;
case 27:
evt.preventDefault();
closeCurrentPopover();
editor.focus();
editor.adjustCursorPosition();
return;
if(evt.which === 13) {
// Enter key
evt.preventDefault();
$addButton.click();
}
});
$addButton.click(function(evt) {
@ -418,7 +416,7 @@ define([
context.rangyRange = rangy.createRange();
context.rangyRange.setStart(context.selectionRange.startContainer, context.selectionRange.startOffset);
context.rangyRange.setEnd(context.selectionRange.endContainer, context.selectionRange.endOffset);
setTimeout(function() { // Need to delay this because it's not refreshed properly
setTimeout(function() { // Delay this because not refreshed properly
if(currentContext === context) {
cssApplier.applyToRange(context.rangyRange);
}
@ -452,7 +450,6 @@ define([
evt.stopPropagation();
});
var $newCommentElt = $(newCommentElt);
$openDiscussionElt = $('.button-open-discussion').click(function(evt) {
var $commentElt = $newCommentElt;

View File

@ -0,0 +1,267 @@
define([
"jquery",
"underscore",
"crel",
"utils",
"classes/Extension",
"mousetrap",
"rangy",
"text!html/findReplace.html",
"text!html/findReplaceSettingsBlock.html"
], function($, _, crel, utils, Extension, mousetrap, rangy, findReplaceHTML, findReplaceSettingsBlockHTML) {
var findReplace = new Extension("findReplace", 'Find and Replace', true, true);
findReplace.settingsBlock = findReplaceSettingsBlockHTML;
findReplace.defaultConfig = {
findReplaceShortcut: 'mod+f'
};
findReplace.onLoadSettings = function() {
utils.setInputValue("#input-find-replace-shortcut", findReplace.config.findReplaceShortcut);
};
findReplace.onSaveSettings = function(newConfig, event) {
newConfig.findReplaceShortcut = utils.getInputTextValue("#input-find-replace-shortcut", event);
};
var editor;
findReplace.onEditorCreated = function(editorParam) {
editor = editorParam;
};
var eventMgr;
findReplace.onEventMgrCreated = function(eventMgrParam) {
eventMgr = eventMgrParam;
};
var rangeList = [];
var offsetList = [];
var highlightCssApplier, selectCssApplier;
var selectRange;
function resetHighlight() {
resetSelect();
rangeList.forEach(function(rangyRange) {
try {
highlightCssApplier.undoToRange(rangyRange);
}
catch(e) {
}
rangyRange.detach();
});
rangeList = [];
}
function resetSelect() {
if(selectRange) {
try {
selectRange && selectCssApplier.undoToRange(selectRange);
}
catch(e) {}
selectRange.toBeDetached && selectRange.detach();
selectRange = undefined;
}
}
var contentElt;
var $findReplaceElt, $searchForInputElt, $replaceWithInputElt;
var foundCounterElt, $caseSensitiveElt, $regexpElt;
var previousText = '';
var previousCaseSensitive = false;
var previousUseRegexp = false;
var shown = false;
var regex;
function highlight(force) {
if(!shown) {
return;
}
var text = $searchForInputElt.val();
var caseSensitive = $caseSensitiveElt.prop('checked');
var useRegexp = $regexpElt.prop('checked');
if(!force && text == previousText && caseSensitive == previousCaseSensitive && useRegexp == previousUseRegexp) {
return;
}
previousText = text;
previousCaseSensitive = caseSensitive;
previousUseRegexp = useRegexp;
resetHighlight();
var lastOffset = {};
var lastRange;
function adjustOffset(offset) {
if(offset.container === lastOffset.container) {
// adjust the offset after rangy has modified the text node
return {
container: lastRange.endContainer.parentElement.nextSibling,
offsetInContainer: offset.offsetInContainer - lastOffset.offsetInContainer,
offset: offset.offset
};
}
return offset;
}
offsetList = [];
var found = 0;
var textLength = text.length;
if(textLength) {
try {
var flags = caseSensitive ? 'g' : 'gi';
text = useRegexp ? text : text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
regex = new RegExp(text, flags);
editor.getValue().replace(regex, function(match, offset) {
offsetList.push({
start: offset,
end: offset + match.length
});
});
found = offsetList.length;
// Highly CPU consuming, so add a limit
if(offsetList.length < 200) {
var rangeOffsets = [];
offsetList.forEach(function(offset) {
rangeOffsets.push(offset.start);
rangeOffsets.push(offset.end);
});
rangeOffsets = editor.selectionMgr.findOffsets(rangeOffsets);
for(var i = 0; i < rangeOffsets.length; i += 2) {
var offsetStart = rangeOffsets[i];
var offsetEnd = rangeOffsets[i + 1];
var adjustedOffsetStart = adjustOffset(offsetStart);
var adjustedOffsetEnd = adjustOffset(offsetEnd);
var rangyRange = rangy.createRange();
rangyRange.setStart(adjustedOffsetStart.container, adjustedOffsetStart.offsetInContainer);
rangyRange.setEnd(adjustedOffsetEnd.container, adjustedOffsetEnd.offsetInContainer);
lastOffset = offsetEnd;
lastRange = rangyRange;
highlightCssApplier.applyToRange(rangyRange);
rangeList[offsetStart.offset] = rangyRange;
}
editor.selectionMgr.hasFocus && editor.selectionMgr.updateSelectionRange();
}
}
catch(e) {
}
}
foundCounterElt.innerHTML = found;
}
function show() {
eventMgr.onEditorPopover();
shown = true;
$findReplaceElt.show();
$searchForInputElt.focus();
editor.selectionMgr.adjustTop = 50;
editor.selectionMgr.adjustBottom = 220;
highlight(true);
}
function hide() {
shown = false;
$findReplaceElt.hide();
resetHighlight();
editor.selectionMgr.adjustTop = 0;
editor.selectionMgr.adjustBottom = 0;
editor.focus();
}
findReplace.onEditorPopover = function() {
hide();
};
function find() {
resetSelect();
var position = Math.min(editor.selectionMgr.selectionStart, editor.selectionMgr.selectionEnd);
var offset = _.find(offsetList, function(offset) {
return offset.start > position;
});
if(!offset) {
offset = _.first(offsetList);
}
if(!offset) {
return;
}
selectRange = rangeList[offset.start];
if(!selectRange) {
var range = editor.selectionMgr.createRange(offset.start, offset.end);
selectRange = rangy.createRange();
selectRange.setStart(range.startContainer, range.startOffset);
selectRange.setEnd(range.endContainer, range.endOffset);
selectRange.toBeDetached = true;
}
selectCssApplier.applyToRange(selectRange);
selectRange.start = offset.start;
selectRange.end = offset.end;
editor.selectionMgr.setSelectionStartEnd(offset.start, offset.end);
editor.selectionMgr.updateCursorCoordinates(true);
}
function replace() {
if(!selectRange) {
return find();
}
var replacement = $replaceWithInputElt.val();
editor.replace(selectRange.start, selectRange.end, replacement);
setTimeout(function() {
find();
$replaceWithInputElt.focus();
}, 1);
}
function replaceAll() {
var replacement = $replaceWithInputElt.val();
editor.replaceAll(regex, replacement);
}
findReplace.onContentChanged = _.bind(highlight, null, true);
findReplace.onFileOpen = _.bind(highlight, null, true);
findReplace.onReady = function() {
highlightCssApplier = rangy.createCssClassApplier('find-replace-highlight', {
normalize: false
});
selectCssApplier = rangy.createCssClassApplier('find-replace-select', {
normalize: false
});
contentElt = document.querySelector('#wmd-input .editor-content');
var elt = crel('div', {
class: 'find-replace'
});
$findReplaceElt = $(elt).hide();
elt.innerHTML = findReplaceHTML;
document.querySelector('.layout-wrapper-l2').appendChild(elt);
$('.button-find-replace-dismiss').click(function() {
hide();
});
foundCounterElt = elt.querySelector('.found-counter');
$caseSensitiveElt = $findReplaceElt.find('.checkbox-case-sensitive').change(_.bind(highlight, null, false));
$regexpElt = $findReplaceElt.find('.checkbox-regexp').change(_.bind(highlight, null, false));
$findReplaceElt.find('.search-button').click(find);
$searchForInputElt = $('#input-find-replace-search-for').keyup(_.bind(highlight, null, false));
$findReplaceElt.find('.replace-button').click(replace);
$replaceWithInputElt = $('#input-find-replace-replace-with');
$findReplaceElt.find('.replace-all-button').click(replaceAll);
// Key bindings
$().add($searchForInputElt).add($replaceWithInputElt).keydown(function(evt) {
if(evt.which === 13) {
// Enter key
evt.preventDefault();
find();
}
});
mousetrap.bind(findReplace.config.findReplaceShortcut, function(e) {
var newSearch = editor.selectionMgr.getSelectedText();
if(newSearch) {
$searchForInputElt.val(newSearch);
}
show();
e.preventDefault();
});
};
return findReplace;
});

View File

@ -1,4 +1,4 @@
<button class="btn btn-info dropdown-toggle action-html-code" title="HTML code" data-toggle="dropdown">
<button class="btn btn-success dropdown-toggle action-html-code" title="HTML code" data-toggle="dropdown">
<i class="icon-code"></i>
</button>
<div class="dropdown-menu pull-right">

View File

@ -1,4 +1,4 @@
<button class="btn btn-info dropdown-toggle button-markdown-syntax" title="Markdown syntax" data-toggle="dropdown">
<button class="btn btn-success dropdown-toggle button-markdown-syntax" title="Markdown syntax" data-toggle="dropdown">
<i class="icon-help-circled"></i>
</button>
<div class="dropdown-menu pull-right">

View File

@ -1,4 +1,4 @@
<button class="btn btn-info dropdown-toggle stat-button" title="Document statistics" data-toggle="dropdown">
<button class="btn btn-success dropdown-toggle stat-button" title="Document statistics" data-toggle="dropdown">
<i class="icon-chart-bar"></i>
<span class="value"></span>
</button>

View File

@ -1,4 +1,4 @@
<button class="btn btn-info dropdown-toggle" title="Table of contents" data-toggle="dropdown">
<button class="btn btn-success dropdown-toggle" title="Table of contents" data-toggle="dropdown">
<i class="icon-list"></i>
</button>
<div class="dropdown-menu pull-right">

View File

@ -1,4 +1,4 @@
<a href="viewer" class="btn btn-info dropdown-toggle"
<a href="viewer" class="btn btn-success dropdown-toggle"
title="Open in viewer">
<i class="icon-resize-full"></i>
</a>

View File

@ -0,0 +1,33 @@
<button type="button" class="close button-find-replace-dismiss">×</button>
<div class="form-inline">
<div class="form-group">
<label for="input-find-replace-search-for">Search for</label>
<input class="form-control" id="input-find-replace-search-for" placeholder="Search for">
</div>
<div class="form-group">
<label for="input-find-replace-replace-with">Replace with</label>
<input class="form-control" id="input-find-replace-replace-with" placeholder="Replace with">
</div>
</div>
<div class="pull-right">
<div class="help-block text-right">
<span class="found-counter">0</span> found
</div>
<div>
<button type="button" class="btn btn-primary search-button">Search</button>
<button type="button" class="btn btn-default replace-button">Replace</button>
<button type="button" class="btn btn-default replace-all-button">All</button>
</div>
</div>
<div class="pull-left">
<div class="checkbox">
<label>
<input type="checkbox" class="checkbox-case-sensitive"> Case sensitive
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" class="checkbox-regexp"> Regular expression
</label>
</div>
</div>

View File

@ -0,0 +1,11 @@
<p>Helps to find and replace text in the current document.</p>
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-5 control-label"
for="input-find-replace-shortcut">Shortcut <a href="http://craig.is/killing/mice#keys" target="_blank">(?)</a></label>
<div class="col-sm-6">
<input type="text" id="input-find-replace-shortcut"
class="form-control">
</div>
</div>
</div>

View File

@ -105,10 +105,10 @@
@btn-primary-bg: @primary-bg;
@btn-primary-border: fade(@secondary, 5%);
@btn-primary-hover-bg: mix(@primary-desaturated, @btn-primary-bg, 7.5%);
@btn-success-color: darken(@primary-desaturated, 25%);
@btn-success-color: darken(@primary-desaturated, 20%);
@btn-success-bg: @transparent;
@btn-success-border: @transparent;
@btn-success-hover-bg: fade(@primary-desaturated, 7.5%);
@btn-success-hover-bg: fade(@primary-desaturated, 5%);
@btn-info-color: fade(@secondary-desaturated, 35%);
@btn-info-bg: @transparent;
@btn-info-border: @transparent;
@ -130,10 +130,10 @@
@popover-arrow-outer-color: @secondary-border-color;
@popover-title-bg: @transparent;
@alert-border-radius: 0;
@label-warning-bg: spin(darken(@logo-yellow, 4%), -10);
@state-warning-text: spin(darken(@logo-yellow, 14%), -10);
@state-warning-bg: fade(spin(@logo-yellow, -10), 12%);
@state-warning-border: fade(spin(@logo-yellow, -10), 24%);
@label-warning-bg: spin(darken(@logo-yellow, 4%), -5);
@state-warning-text: spin(darken(@logo-yellow, 14%), -5);
@state-warning-bg: fade(spin(@logo-yellow, -5), 12%);
@state-warning-border: fade(spin(@logo-yellow, -5), 24%);
@label-danger-bg: spin(darken(@logo-orange, 4%), -8);
@state-danger-text: spin(darken(@logo-orange, 18%), -8);
@state-danger-bg: fade(spin(@logo-orange, -8), 10%);
@ -162,7 +162,7 @@ body {
.user-select(none);
}
.dropdown-menu, .modal-content, .panel-content, .search-bar, .popover {
.dropdown-menu, .modal-content, .panel-content, .search-bar, .popover, .find-replace {
.box-shadow(0 4px 16px rgba(0,0,0,.225));
}
@ -699,7 +699,6 @@ a {
padding: 15px 20px;
z-index: 3;
border: 1px solid @secondary-border-color;
border-top: 0;
border-radius: 6px;
.nav {
margin-bottom: 10px;
@ -736,7 +735,7 @@ a {
// Dropdown document selector
.dropdown-file-selector {
top: 6px;
right: 30px;
right: 55px;
left: auto;
margin: 0;
min-width: 280px;
@ -830,8 +829,8 @@ a {
right: 0;
bottom: 0;
z-index: 40;
background-color: @btn-info-hover-bg;
border: 1px solid @btn-info-hover-border;
background-color: @navbar-default-bg;
border: 1px solid @navbar-default-border;
border-radius: @border-radius-base;
cursor: move;
@ -880,10 +879,7 @@ a {
}
.drag-me {
color: @btn-info-color;
&.info-tooltip {
color: @btn-info-hover-color;
}
color: @btn-success-color;
i:before {
width: 5px;
}
@ -1124,9 +1120,9 @@ a {
}
}
&.added {
color: fade(@label-warning-bg, 50%);
color: fade(@label-warning-bg, 70%);
&:hover, &.active, &.active:hover {
color: fade(@label-warning-bg, 80%) !important;
color: fade(@label-warning-bg, 100%) !important;
}
}
&.replied {
@ -1159,6 +1155,14 @@ a {
background-color: fade(@label-warning-bg, 30%);
}
.find-replace-highlight {
background-color: fade(@logo-yellow, 60%);
}
.find-replace-select {
background-color: rgb(181, 213, 255);
}
.conflict {
font-weight: bold;
color: @label-danger-bg;
@ -1282,6 +1286,26 @@ a {
}
}
.find-replace {
position: absolute;
bottom: 0;
width: 410px;
background-color: @secondary-bg;
padding: 15px 20px;
border-top: 1px solid @secondary-border-color;
border-right: 1px solid @secondary-border-color;
border-top-right-radius: 6px;
.form-group {
width: 180px;
padding: 0 5px;
}
.close {
position: absolute;
right: 20px;
top: 10px;
}
}
/*****************************
* Preview