diff --git a/public/res/editor.js b/public/res/editor.js index 86fc527a..b3dc4c06 100644 --- a/public/res/editor.js +++ b/public/res/editor.js @@ -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) { diff --git a/public/res/eventMgr.js b/public/res/eventMgr.js index f90aa57e..1356b498 100644 --- a/public/res/eventMgr.js +++ b/public/res/eventMgr.js @@ -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> /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> /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; }); diff --git a/public/res/extensions/comments.js b/public/res/extensions/comments.js index 0057c504..1b8d2673 100644 --- a/public/res/extensions/comments.js +++ b/public/res/extensions/comments.js @@ -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; diff --git a/public/res/extensions/findReplace.js b/public/res/extensions/findReplace.js new file mode 100644 index 00000000..2c2d2a46 --- /dev/null +++ b/public/res/extensions/findReplace.js @@ -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; +}); diff --git a/public/res/html/buttonHtmlCode.html b/public/res/html/buttonHtmlCode.html index 7e453e3d..79ad92c7 100644 --- a/public/res/html/buttonHtmlCode.html +++ b/public/res/html/buttonHtmlCode.html @@ -1,4 +1,4 @@ -