diff --git a/ghost/admin/assets/lib/editor/htmlPreview.js b/ghost/admin/assets/lib/editor/htmlPreview.js
new file mode 100644
index 0000000000..16d1cabb70
--- /dev/null
+++ b/ghost/admin/assets/lib/editor/htmlPreview.js
@@ -0,0 +1,44 @@
+// # Ghost Editor HTML Preview
+//
+// HTML Preview is the right pane in the split view editor.
+// It is effectively just a scrolling container for the HTML output from showdown
+// It knows how to update itself, and that's pretty much it.
+
+/*global Ghost, Showdown, Countable, _, $ */
+(function () {
+ 'use strict';
+
+ var HTMLPreview = function (markdown, uploadMgr) {
+ var converter = new Showdown.converter({extensions: ['typography', 'ghostdown', 'github']}),
+ preview = document.getElementsByClassName('rendered-markdown')[0],
+ update;
+
+ // Update the preview
+ // Includes replacing all the HTML, intialising upload dropzones, and updating the counter
+ update = function () {
+ preview.innerHTML = converter.makeHtml(markdown.value());
+
+ uploadMgr.enable();
+
+ Countable.once(preview, function (counter) {
+ $('.entry-word-count').text($.pluralize(counter.words, 'word'));
+ $('.entry-character-count').text($.pluralize(counter.characters, 'character'));
+ $('.entry-paragraph-count').text($.pluralize(counter.paragraphs, 'paragraph'));
+ });
+ };
+
+ // Public API
+ _.extend(this, {
+ scrollViewPort: function () {
+ return $('.entry-preview-content');
+ },
+ scrollContent: function () {
+ return $('.rendered-markdown');
+ },
+ update: update
+ });
+ };
+
+ Ghost.Editor = Ghost.Editor || {};
+ Ghost.Editor.HTMLPreview = HTMLPreview;
+} ());
\ No newline at end of file
diff --git a/ghost/admin/assets/lib/editor/index.js b/ghost/admin/assets/lib/editor/index.js
new file mode 100644
index 0000000000..cdeac320df
--- /dev/null
+++ b/ghost/admin/assets/lib/editor/index.js
@@ -0,0 +1,79 @@
+// # Ghost Editor
+//
+// Ghost Editor contains a set of modules which make up the editor component
+// It manages the left and right panes, and all of the communication between them
+// Including scrolling,
+
+/*global document, $, _, Ghost */
+(function () {
+ 'use strict';
+
+ var Editor = function () {
+ var self = this,
+ $document = $(document),
+ // Create all the needed editor components, passing them what they need to function
+ markdown = new Ghost.Editor.MarkdownEditor(),
+ uploadMgr = new Ghost.Editor.UploadManager(markdown),
+ preview = new Ghost.Editor.HTMLPreview(markdown, uploadMgr),
+ scrollHandler = new Ghost.Editor.ScrollHandler(markdown, preview),
+ unloadDirtyMessage,
+ handleChange,
+ handleDrag;
+
+ unloadDirtyMessage = function () {
+ return '==============================\n\n' +
+ 'Hey there! It looks like you\'re in the middle of writing' +
+ ' something and you haven\'t saved all of your content.' +
+ '\n\nSave before you go!\n\n' +
+ '==============================';
+ };
+
+ handleChange = function () {
+ self.setDirty(true);
+ preview.update();
+ };
+
+ handleDrag = function (e) {
+ e.preventDefault();
+ };
+
+ // Public API
+ _.extend(this, {
+ enable: function () {
+ // Listen for changes
+ $document.on('markdownEditorChange', handleChange);
+
+ // enable editing and scrolling
+ markdown.enable();
+ scrollHandler.enable();
+ },
+
+ disable: function () {
+ // Don't listen for changes
+ $document.off('markdownEditorChange', handleChange);
+
+ // disable editing and scrolling
+ markdown.disable();
+ scrollHandler.disable();
+ },
+
+ // Get the markdown value from the editor for saving
+ // Upload manager makes sure the upload markers are removed beforehand
+ value: function () {
+ return uploadMgr.value();
+ },
+
+ setDirty: function (dirty) {
+ window.onbeforeunload = dirty ? unloadDirtyMessage : null;
+ }
+ });
+
+ // Initialise
+ $document.on('drop dragover', handleDrag);
+ preview.update();
+ this.enable();
+ };
+
+ Ghost.Editor = Ghost.Editor || {};
+ Ghost.Editor.Main = Editor;
+}());
\ No newline at end of file
diff --git a/ghost/admin/assets/lib/editor/markdownEditor.js b/ghost/admin/assets/lib/editor/markdownEditor.js
new file mode 100644
index 0000000000..a35f7ab8fd
--- /dev/null
+++ b/ghost/admin/assets/lib/editor/markdownEditor.js
@@ -0,0 +1,92 @@
+// # Ghost Editor Markdown Editor
+//
+// Markdown Editor is a light wrapper around CodeMirror
+
+/*global Ghost, CodeMirror, shortcut, _, $ */
+(function () {
+ 'use strict';
+
+ var MarkdownShortcuts,
+ MarkdownEditor;
+
+ MarkdownShortcuts = [
+ {'key': 'Ctrl+B', 'style': 'bold'},
+ {'key': 'Meta+B', 'style': 'bold'},
+ {'key': 'Ctrl+I', 'style': 'italic'},
+ {'key': 'Meta+I', 'style': 'italic'},
+ {'key': 'Ctrl+Alt+U', 'style': 'strike'},
+ {'key': 'Ctrl+Shift+K', 'style': 'code'},
+ {'key': 'Meta+K', 'style': 'code'},
+ {'key': 'Ctrl+Alt+1', 'style': 'h1'},
+ {'key': 'Ctrl+Alt+2', 'style': 'h2'},
+ {'key': 'Ctrl+Alt+3', 'style': 'h3'},
+ {'key': 'Ctrl+Alt+4', 'style': 'h4'},
+ {'key': 'Ctrl+Alt+5', 'style': 'h5'},
+ {'key': 'Ctrl+Alt+6', 'style': 'h6'},
+ {'key': 'Ctrl+Shift+L', 'style': 'link'},
+ {'key': 'Ctrl+Shift+I', 'style': 'image'},
+ {'key': 'Ctrl+Q', 'style': 'blockquote'},
+ {'key': 'Ctrl+Shift+1', 'style': 'currentDate'},
+ {'key': 'Ctrl+U', 'style': 'uppercase'},
+ {'key': 'Ctrl+Shift+U', 'style': 'lowercase'},
+ {'key': 'Ctrl+Alt+Shift+U', 'style': 'titlecase'},
+ {'key': 'Ctrl+Alt+W', 'style': 'selectword'},
+ {'key': 'Ctrl+L', 'style': 'list'},
+ {'key': 'Ctrl+Alt+C', 'style': 'copyHTML'},
+ {'key': 'Meta+Alt+C', 'style': 'copyHTML'},
+ {'key': 'Meta+Enter', 'style': 'newLine'},
+ {'key': 'Ctrl+Enter', 'style': 'newLine'}
+ ];
+
+ MarkdownEditor = function () {
+ var codemirror = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
+ mode: 'gfm',
+ tabMode: 'indent',
+ tabindex: '2',
+ cursorScrollMargin: 10,
+ lineWrapping: true,
+ dragDrop: false,
+ extraKeys: {
+ Home: 'goLineLeft',
+ End: 'goLineRight'
+ }
+ });
+
+ // Markdown shortcuts for the editor
+ _.each(MarkdownShortcuts, function (combo) {
+ shortcut.add(combo.key, function () {
+ return codemirror.addMarkdown({style: combo.style});
+ });
+ });
+
+ // Public API
+ _.extend(this, {
+ codemirror: codemirror,
+
+ scrollViewPort: function () {
+ return $('.CodeMirror-scroll');
+ },
+ scrollContent: function () {
+ return $('.CodeMirror-sizer');
+ },
+ enable: function () {
+ codemirror.setOption('readOnly', false);
+ codemirror.on('change', function () {
+ $(document).trigger('markdownEditorChange');
+ });
+ },
+ disable: function () {
+ codemirror.setOption('readOnly', 'nocursor');
+ codemirror.off('change', function () {
+ $(document).trigger('markdownEditorChange');
+ });
+ },
+ value: function () {
+ return codemirror.getValue();
+ }
+ });
+ };
+
+ Ghost.Editor = Ghost.Editor || {};
+ Ghost.Editor.MarkdownEditor = MarkdownEditor;
+} ());
\ No newline at end of file
diff --git a/ghost/admin/assets/lib/editor/markerManager.js b/ghost/admin/assets/lib/editor/markerManager.js
new file mode 100644
index 0000000000..aef4cc02bf
--- /dev/null
+++ b/ghost/admin/assets/lib/editor/markerManager.js
@@ -0,0 +1,147 @@
+// # Ghost Editor Marker Manager
+//
+// MarkerManager looks after the array of markers which are attached to image markdown in the editor.
+//
+// Marker Manager is told by the Upload Manager to add a marker to a line.
+// A marker takes the form of a 'magic id' which looks like:
+// {<1>}
+// It is appended to the start of the given line, and then defined as a CodeMirror 'TextMarker' widget which is
+// subsequently added to an array of markers to keep track of all markers in the editor.
+// The TextMarker is also set to 'collapsed' mode which means it does not show up in the display.
+// Currently, the markers can be seen if you copy and paste your content out of Ghost into a text editor.
+// The markers are stripped on save so should not appear in the DB
+
+
+/*global _, Ghost */
+
+(function () {
+ 'use strict';
+
+ var imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
+ markerRegex = /\{<([\w\W]*?)>\}/,
+ MarkerManager;
+
+ MarkerManager = function (editor) {
+ var markers = {},
+ uploadPrefix = 'image_upload',
+ uploadId = 1,
+ addMarker,
+ removeMarker,
+ markerRegexForId,
+ stripMarkerFromLine,
+ findAndStripMarker,
+ checkMarkers,
+ initMarkers;
+
+ // the regex
+ markerRegexForId = function (id) {
+ id = id.replace('image_upload_', '');
+ return new RegExp('\\{<' + id + '>\\}', 'gmi');
+ };
+
+ // Add a marker to the given line
+ // Params:
+ // line - CodeMirror LineHandle
+ // ln - line number
+ addMarker = function (line, ln) {
+ var marker,
+ magicId = '{<' + uploadId + '>}';
+ editor.setLine(ln, magicId + line.text);
+ marker = editor.markText(
+ {line: ln, ch: 0},
+ {line: ln, ch: (magicId.length)},
+ {collapsed: true}
+ );
+
+ markers[uploadPrefix + '_' + uploadId] = marker;
+ uploadId += 1;
+ };
+
+ // Remove a marker
+ // Will be passed a LineHandle if we already know which line the marker is on
+ removeMarker = function (id, marker, line) {
+ delete markers[id];
+ marker.clear();
+
+ if (line) {
+ stripMarkerFromLine(line);
+ } else {
+ findAndStripMarker(id);
+ }
+ };
+
+ // Removes the marker on the given line if there is one
+ stripMarkerFromLine = function (line) {
+ var markerText = line.text.match(markerRegex),
+ ln = editor.getLineNumber(line);
+
+ if (markerText) {
+ editor.replaceRange(
+ '',
+ {line: ln, ch: markerText.index},
+ {line: ln, ch: markerText.index + markerText[0].length}
+ );
+ }
+ };
+
+ // Find a marker in the editor by id & remove it
+ // Goes line by line to find the marker by it's text if we've lost track of the TextMarker
+ findAndStripMarker = function (id) {
+ editor.eachLine(function (line) {
+ var markerText = markerRegexForId(id).exec(line.text),
+ ln;
+
+ if (markerText) {
+ ln = editor.getLineNumber(line);
+ editor.replaceRange(
+ '',
+ {line: ln, ch: markerText.index},
+ {line: ln, ch: markerText.index + markerText[0].length}
+ );
+ }
+ });
+ };
+
+ // Check each marker to see if it is still present in the editor and if it still corresponds to image markdown
+ // If it is no longer a valid image, remove it
+ checkMarkers = function () {
+ _.each(markers, function (marker, id) {
+ var line;
+ marker = markers[id];
+ if (marker.find()) {
+ line = editor.getLineHandle(marker.find().from.line);
+ if (!line.text.match(imageMarkdownRegex)) {
+ removeMarker(id, marker, line);
+ }
+ } else {
+ removeMarker(id, marker);
+ }
+ });
+ };
+
+ // Add markers to the line if it needs one
+ initMarkers = function (line) {
+ var isImage = line.text.match(imageMarkdownRegex),
+ hasMarker = line.text.match(markerRegex);
+
+ if (isImage && !hasMarker) {
+ addMarker(line, editor.getLineNumber(line));
+ }
+ };
+
+ // Initialise
+ editor.eachLine(initMarkers);
+
+ // Public API
+ _.extend(this, {
+ markers: markers,
+ checkMarkers: checkMarkers,
+ addMarker: addMarker,
+ stripMarkerFromLine: stripMarkerFromLine,
+ getMarkerRegexForId: markerRegexForId
+ });
+ };
+
+ Ghost.Editor = Ghost.Editor || {};
+ Ghost.Editor.MarkerManager = MarkerManager;
+}());
\ No newline at end of file
diff --git a/ghost/admin/assets/lib/editor/scrollHandler.js b/ghost/admin/assets/lib/editor/scrollHandler.js
new file mode 100644
index 0000000000..b996879240
--- /dev/null
+++ b/ghost/admin/assets/lib/editor/scrollHandler.js
@@ -0,0 +1,43 @@
+// # Ghost Editor Scroll Handler
+//
+// Scroll Handler does the (currently very simple / naive) job of syncing the right pane with the left pane
+// as the right pane scrolls
+
+/*global Ghost, _ */
+(function () {
+ 'use strict';
+
+ var ScrollHandler = function (markdown, preview) {
+ var $markdownViewPort = markdown.scrollViewPort(),
+ $previewViewPort = preview.scrollViewPort(),
+ $markdownContent = markdown.scrollContent(),
+ $previewContent = preview.scrollContent(),
+ syncScroll;
+
+ syncScroll = _.throttle(function () {
+ // calc position
+ var markdownHeight = $markdownContent.height() - $markdownViewPort.height(),
+ previewHeight = $previewContent.height() - $previewViewPort.height(),
+ ratio = previewHeight / markdownHeight,
+ previewPosition = $markdownViewPort.scrollTop() * ratio;
+
+ // apply new scroll
+ $previewViewPort.scrollTop(previewPosition);
+ }, 10);
+
+ _.extend(this, {
+ enable: function () { // Handle Scroll Events
+ $markdownViewPort.on('scroll', syncScroll);
+ $markdownViewPort.scrollClass({target: '.entry-markdown', offset: 10});
+ $previewViewPort.scrollClass({target: '.entry-preview', offset: 10});
+ },
+ disable: function () {
+ $markdownViewPort.off('scroll', syncScroll);
+ }
+ });
+
+ };
+
+ Ghost.Editor = Ghost.Editor || {};
+ Ghost.Editor.ScrollHandler = ScrollHandler;
+} ());
\ No newline at end of file
diff --git a/ghost/admin/assets/lib/editor/uploadManager.js b/ghost/admin/assets/lib/editor/uploadManager.js
new file mode 100644
index 0000000000..b83b37fb64
--- /dev/null
+++ b/ghost/admin/assets/lib/editor/uploadManager.js
@@ -0,0 +1,153 @@
+// # Ghost Editor Upload Manager
+//
+// UploadManager ensures that markdown gets updated when images get uploaded via the Preview.
+//
+// The Ghost Editor has a particularly tricky problem to solve, in that it is possible to upload an image by
+// interacting with the preview. The process of uploading an image is handled by uploader.js, but there is still
+// a lot of work needed to ensure that uploaded files end up in the right place - that is that the image
+// path gets added to the correct piece of markdown in the editor.
+//
+// To solve this, Ghost adds a unique 'marker' to each piece of markdown which represents an image:
+// More detail about how the markers work can be find in markerManager.js
+//
+// UploadManager handles changes in the editor, looking for text which matches image markdown, and telling the marker
+// manager to add a marker. It also checks changed lines to see if they have a marker but are no longer an image.
+//
+// UploadManager's most important job is handling uploads such that when a successful upload completes, the correct
+// piece of image markdown is updated with the path.
+// This is done in part by ghostImagePreview.js, which takes the marker from the markdown and uses it to create an ID
+// on the dropzone. When an upload completes successfully from uploader.js, the event thrown contains reference to the
+// dropzone, from which uploadManager can pull the ID & then get the right marker from the Marker Manager.
+//
+// Without a doubt, the separation of concerns between the uploadManager, and the markerManager could be vastly
+// improved
+
+
+/*global $, _, Ghost */
+(function () {
+ 'use strict';
+
+ var imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
+ markerRegex = /\{<([\w\W]*?)>\}/,
+ UploadManager;
+
+ UploadManager = function (markdown) {
+ var editor = markdown.codemirror,
+ markerMgr = new Ghost.Editor.MarkerManager(editor),
+ findLine,
+ checkLine,
+ value,
+ handleUpload,
+ handleChange;
+
+ // Find the line with the marker which matches
+ findLine = function (result_id) {
+ // try to find the right line to replace
+ if (markerMgr.markers.hasOwnProperty(result_id) && markerMgr.markers[result_id].find()) {
+ return editor.getLineHandle(markerMgr.markers[result_id].find().from.line);
+ }
+
+ return false;
+ };
+
+ // Check the given line to see if it has an image, and if it correctly has a marker
+ // In the special case of lines which were just pasted in, any markers are removed to prevent duplication
+ checkLine = function (ln, mode) {
+ var line = editor.getLineHandle(ln),
+ isImage = line.text.match(imageMarkdownRegex),
+ hasMarker;
+
+ // We care if it is an image
+ if (isImage) {
+ hasMarker = line.text.match(markerRegex);
+
+ if (hasMarker && mode === 'paste') {
+ // this could be a duplicate, and won't be a real marker
+ markerMgr.stripMarkerFromLine(line);
+ }
+
+ if (!hasMarker) {
+ markerMgr.addMarker(line, ln);
+ }
+ }
+ // TODO: hasMarker but no image?
+ };
+
+ // Get the markdown with all the markers stripped
+ value = function () {
+ var value = editor.getValue();
+
+ _.each(markerMgr.markers, function (marker, id) {
+ /*jshint unused:false*/
+ value = value.replace(markerMgr.getMarkerRegexForId(id), '');
+ });
+
+ return value;
+ };
+
+ // Match the uploaded file to a line in the editor, and update that line with a path reference
+ // ensuring that everything ends up in the correct place and format.
+ handleUpload = function (e, result_src) {
+ var line = findLine($(e.currentTarget).attr('id')),
+ lineNumber = editor.getLineNumber(line),
+ match = line.text.match(/\([^\n]*\)?/),
+ replacement = '(http://)';
+
+ if (match) {
+ // simple case, we have the parenthesis
+ editor.setSelection(
+ {line: lineNumber, ch: match.index + 1},
+ {line: lineNumber, ch: match.index + match[0].length - 1}
+ );
+ } else {
+ match = line.text.match(/\]/);
+ if (match) {
+ editor.replaceRange(
+ replacement,
+ {line: lineNumber, ch: match.index + 1},
+ {line: lineNumber, ch: match.index + 1}
+ );
+ editor.setSelection(
+ {line: lineNumber, ch: match.index + 2},
+ {line: lineNumber, ch: match.index + replacement.length }
+ );
+ }
+ }
+ editor.replaceSelection(result_src);
+ };
+
+ // Change events from CodeMirror tell us which lines have changed.
+ // Each changed line is then checked to see if a marker needs to be added or removed
+ handleChange = function (cm, changeObj) {
+ /*jshint unused:false*/
+ var linesChanged = _.range(changeObj.from.line, changeObj.from.line + changeObj.text.length);
+
+ _.each(linesChanged, function (ln) {
+ checkLine(ln, changeObj.origin);
+ });
+
+ // Is this a line which may have had a marker on it?
+ markerMgr.checkMarkers();
+ };
+
+ // Public API
+ _.extend(this, {
+ value: value,
+ enable: function () {
+ var filestorage = $('#entry-markdown-content').data('filestorage');
+ $('.js-drop-zone').upload({editor: true, fileStorage: filestorage});
+ $('.js-drop-zone').on('uploadstart', markdown.off);
+ $('.js-drop-zone').on('uploadfailure', markdown.on);
+ $('.js-drop-zone').on('uploadsuccess', markdown.on);
+ $('.js-drop-zone').on('uploadsuccess', handleUpload);
+ },
+ disable: function () {
+ $('.js-drop-zone').off('uploadsuccess', handleUpload);
+ }
+ });
+
+ editor.on('change', handleChange);
+ };
+ Ghost.Editor = Ghost.Editor || {};
+ Ghost.Editor.UploadManager = UploadManager;
+}());
\ No newline at end of file
diff --git a/ghost/admin/markdown-actions.js b/ghost/admin/markdown-actions.js
index 0d62700037..176fcf81c4 100644
--- a/ghost/admin/markdown-actions.js
+++ b/ghost/admin/markdown-actions.js
@@ -130,13 +130,13 @@
CodeMirror.prototype.addMarkdown.options = {
style: null,
syntax: {
- bold: "**$1**",
- italic: "*$1*",
- strike: "~~$1~~",
- code: "`$1`",
- link: "[$1](http://)",
- image: "",
- blockquote: "> $1"
+ bold: '**$1**',
+ italic: '*$1*',
+ strike: '~~$1~~',
+ code: '`$1`',
+ link: '[$1](http://)',
+ image: '',
+ blockquote: '> $1'
}
};
diff --git a/ghost/admin/views/editor-actions-widget.js b/ghost/admin/views/editor-actions-widget.js
new file mode 100644
index 0000000000..38fa4a27ef
--- /dev/null
+++ b/ghost/admin/views/editor-actions-widget.js
@@ -0,0 +1,255 @@
+// The Save / Publish button
+
+/*global $, _, Ghost, shortcut */
+
+(function () {
+ 'use strict';
+
+ // The Publish, Queue, Publish Now buttons
+ // ----------------------------------------
+ Ghost.View.EditorActionsWidget = Ghost.View.extend({
+
+ events: {
+ 'click [data-set-status]': 'handleStatus',
+ 'click .js-publish-button': 'handlePostButton'
+ },
+
+ statusMap: null,
+
+ createStatusMap: {
+ 'draft': 'Save Draft',
+ 'published': 'Publish Now'
+ },
+
+ updateStatusMap: {
+ 'draft': 'Unpublish',
+ 'published': 'Update Post'
+ },
+
+ //TODO: This has to be moved to the I18n localization file.
+ //This structure is supposed to be close to the i18n-localization which will be used soon.
+ messageMap: {
+ errors: {
+ post: {
+ published: {
+ 'published': 'Your post could not be updated.',
+ 'draft': 'Your post could not be saved as a draft.'
+ },
+ draft: {
+ 'published': 'Your post could not be published.',
+ 'draft': 'Your post could not be saved as a draft.'
+ }
+
+ }
+ },
+
+ success: {
+ post: {
+ published: {
+ 'published': 'Your post has been updated.',
+ 'draft': 'Your post has been saved as a draft.'
+ },
+ draft: {
+ 'published': 'Your post has been published.',
+ 'draft': 'Your post has been saved as a draft.'
+ }
+ }
+ }
+ },
+
+ initialize: function () {
+ var self = this;
+
+ // Toggle publish
+ shortcut.add('Ctrl+Alt+P', function () {
+ self.toggleStatus();
+ });
+ shortcut.add('Ctrl+S', function () {
+ self.updatePost();
+ });
+ shortcut.add('Meta+S', function () {
+ self.updatePost();
+ });
+ this.listenTo(this.model, 'change:status', this.render);
+ },
+
+ toggleStatus: function () {
+ var self = this,
+ keys = Object.keys(this.statusMap),
+ model = self.model,
+ prevStatus = model.get('status'),
+ currentIndex = keys.indexOf(prevStatus),
+ newIndex,
+ status;
+
+ newIndex = currentIndex + 1 > keys.length - 1 ? 0 : currentIndex + 1;
+ status = keys[newIndex];
+
+ this.setActiveStatus(keys[newIndex], this.statusMap[status], prevStatus);
+
+ this.savePost({
+ status: keys[newIndex]
+ }).then(function () {
+ self.reportSaveSuccess(status, prevStatus);
+ }, function (xhr) {
+ // Show a notification about the error
+ self.reportSaveError(xhr, model, status, prevStatus);
+ });
+ },
+
+ setActiveStatus: function (newStatus, displayText, currentStatus) {
+ var isPublishing = (newStatus === 'published' && currentStatus !== 'published'),
+ isUnpublishing = (newStatus === 'draft' && currentStatus === 'published'),
+ // Controls when background of button has the splitbutton-delete/button-delete classes applied
+ isImportantStatus = (isPublishing || isUnpublishing);
+
+ $('.js-publish-splitbutton')
+ .removeClass(isImportantStatus ? 'splitbutton-save' : 'splitbutton-delete')
+ .addClass(isImportantStatus ? 'splitbutton-delete' : 'splitbutton-save');
+
+ // Set the publish button's action and proper coloring
+ $('.js-publish-button')
+ .attr('data-status', newStatus)
+ .text(displayText)
+ .removeClass(isImportantStatus ? 'button-save' : 'button-delete')
+ .addClass(isImportantStatus ? 'button-delete' : 'button-save');
+
+ // Remove the animated popup arrow
+ $('.js-publish-splitbutton > a')
+ .removeClass('active');
+
+ // Set the active action in the popup
+ $('.js-publish-splitbutton .editor-options li')
+ .removeClass('active')
+ .filter(['li[data-set-status="', newStatus, '"]'].join(''))
+ .addClass('active');
+ },
+
+ handleStatus: function (e) {
+ if (e) { e.preventDefault(); }
+ var status = $(e.currentTarget).attr('data-set-status'),
+ currentStatus = this.model.get('status');
+
+ this.setActiveStatus(status, this.statusMap[status], currentStatus);
+
+ // Dismiss the popup menu
+ $('body').find('.overlay:visible').fadeOut();
+ },
+
+ handlePostButton: function (e) {
+ if (e) { e.preventDefault(); }
+ var status = $(e.currentTarget).attr('data-status');
+
+ this.updatePost(status);
+ },
+
+ updatePost: function (status) {
+ var self = this,
+ model = this.model,
+ prevStatus = model.get('status');
+
+ // Default to same status if not passed in
+ status = status || prevStatus;
+
+ model.trigger('willSave');
+
+ this.savePost({
+ status: status
+ }).then(function () {
+ self.reportSaveSuccess(status, prevStatus);
+ // Refresh publish button and all relevant controls with updated status.
+ self.render();
+ }, function (xhr) {
+ // Set the model status back to previous
+ model.set({ status: prevStatus });
+ // Set appropriate button status
+ self.setActiveStatus(status, self.statusMap[status], prevStatus);
+ // Show a notification about the error
+ self.reportSaveError(xhr, model, status, prevStatus);
+ });
+ },
+
+ savePost: function (data) {
+ var publishButton = $('.js-publish-button'),
+ saved,
+ enablePublish = function (deferred) {
+ deferred.always(function () {
+ publishButton.prop('disabled', false);
+ });
+ return deferred;
+ };
+
+ publishButton.prop('disabled', true);
+
+ _.each(this.model.blacklist, function (item) {
+ this.model.unset(item);
+ }, this);
+
+ saved = this.model.save(_.extend({
+ title: this.options.$title.val(),
+ markdown: this.options.editor.value()
+ }, data));
+
+ // TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone
+ // ourselves for more consistent promises.
+ if (saved) {
+ return enablePublish(saved);
+ }
+
+ return enablePublish($.Deferred().reject());
+ },
+
+ reportSaveSuccess: function (status, prevStatus) {
+ Ghost.notifications.clearEverything();
+ Ghost.notifications.addItem({
+ type: 'success',
+ message: this.messageMap.success.post[prevStatus][status],
+ status: 'passive'
+ });
+ this.options.editor.setDirty(false);
+ },
+
+ reportSaveError: function (response, model, status, prevStatus) {
+ var message = this.messageMap.errors.post[prevStatus][status];
+
+ if (response) {
+ // Get message from response
+ message += ' ' + Ghost.Views.Utils.getRequestErrorMessage(response);
+ } else if (model.validationError) {
+ // Grab a validation error
+ message += ' ' + model.validationError;
+ }
+
+ Ghost.notifications.clearEverything();
+ Ghost.notifications.addItem({
+ type: 'error',
+ message: message,
+ status: 'passive'
+ });
+ },
+
+ setStatusLabels: function (statusMap) {
+ _.each(statusMap, function (label, status) {
+ $('li[data-set-status="' + status + '"] > a').text(label);
+ });
+ },
+
+ render: function () {
+ var status = this.model.get('status');
+
+ // Assume that we're creating a new post
+ if (status !== 'published') {
+ this.statusMap = this.createStatusMap;
+ } else {
+ this.statusMap = this.updateStatusMap;
+ }
+
+ // Populate the publish menu with the appropriate verbiage
+ this.setStatusLabels(this.statusMap);
+
+ // Default the selected publish option to the current status of the post.
+ this.setActiveStatus(status, this.statusMap[status], status);
+ }
+
+ });
+}());
\ No newline at end of file
diff --git a/ghost/admin/views/editor.js b/ghost/admin/views/editor.js
index 35b29605c6..bf8a8b98d9 100644
--- a/ghost/admin/views/editor.js
+++ b/ghost/admin/views/editor.js
@@ -1,45 +1,10 @@
// # Article Editor
-/*global window, document, setTimeout, navigator, $, _, Backbone, Ghost, Showdown, CodeMirror, shortcut, Countable */
+/*global document, setTimeout, navigator, $, Backbone, Ghost, shortcut */
(function () {
- "use strict";
+ 'use strict';
- /*jslint regexp: true, bitwise: true */
- var PublishBar,
- ActionsWidget,
- UploadManager,
- MarkerManager,
- MarkdownShortcuts = [
- {'key': 'Ctrl+B', 'style': 'bold'},
- {'key': 'Meta+B', 'style': 'bold'},
- {'key': 'Ctrl+I', 'style': 'italic'},
- {'key': 'Meta+I', 'style': 'italic'},
- {'key': 'Ctrl+Alt+U', 'style': 'strike'},
- {'key': 'Ctrl+Shift+K', 'style': 'code'},
- {'key': 'Meta+K', 'style': 'code'},
- {'key': 'Ctrl+Alt+1', 'style': 'h1'},
- {'key': 'Ctrl+Alt+2', 'style': 'h2'},
- {'key': 'Ctrl+Alt+3', 'style': 'h3'},
- {'key': 'Ctrl+Alt+4', 'style': 'h4'},
- {'key': 'Ctrl+Alt+5', 'style': 'h5'},
- {'key': 'Ctrl+Alt+6', 'style': 'h6'},
- {'key': 'Ctrl+Shift+L', 'style': 'link'},
- {'key': 'Ctrl+Shift+I', 'style': 'image'},
- {'key': 'Ctrl+Q', 'style': 'blockquote'},
- {'key': 'Ctrl+Shift+1', 'style': 'currentDate'},
- {'key': 'Ctrl+U', 'style': 'uppercase'},
- {'key': 'Ctrl+Shift+U', 'style': 'lowercase'},
- {'key': 'Ctrl+Alt+Shift+U', 'style': 'titlecase'},
- {'key': 'Ctrl+Alt+W', 'style': 'selectword'},
- {'key': 'Ctrl+L', 'style': 'list'},
- {'key': 'Ctrl+Alt+C', 'style': 'copyHTML'},
- {'key': 'Meta+Alt+C', 'style': 'copyHTML'},
- {'key': 'Meta+Enter', 'style': 'newLine'},
- {'key': 'Ctrl+Enter', 'style': 'newLine'}
- ],
- imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
- markerRegex = /\{<([\w\W]*?)>\}/;
- /*jslint regexp: false, bitwise: false */
+ var PublishBar;
// The publish bar associated with a post, which has the TagWidget and
// Save button and options and such.
@@ -47,364 +12,88 @@
PublishBar = Ghost.View.extend({
initialize: function () {
- this.addSubview(new Ghost.View.EditorTagWidget({el: this.$('#entry-tags'), model: this.model})).render();
- this.addSubview(new ActionsWidget({el: this.$('#entry-actions'), model: this.model})).render();
- this.addSubview(new Ghost.View.PostSettings({el: $('#entry-controls'), model: this.model})).render();
+
+ this.addSubview(new Ghost.View.EditorTagWidget(
+ {el: this.$('#entry-tags'), model: this.model}
+ )).render();
+ this.addSubview(new Ghost.View.PostSettings(
+ {el: $('#entry-controls'), model: this.model}
+ )).render();
+
+ // Pass the Actions widget references to the title and editor so that it can get
+ // the values that need to be saved
+ this.addSubview(new Ghost.View.EditorActionsWidget(
+ {
+ el: this.$('#entry-actions'),
+ model: this.model,
+ $title: this.options.$title,
+ editor: this.options.editor
+ }
+ )).render();
+
},
render: function () { return this; }
-
});
- // The Publish, Queue, Publish Now buttons
- // ----------------------------------------
- ActionsWidget = Ghost.View.extend({
-
- events: {
- 'click [data-set-status]': 'handleStatus',
- 'click .js-publish-button': 'handlePostButton'
- },
-
- statusMap: null,
-
- createStatusMap: {
- 'draft': 'Save Draft',
- 'published': 'Publish Now'
- },
-
- updateStatusMap: {
- 'draft': 'Unpublish',
- 'published': 'Update Post'
- },
-
- //TODO: This has to be moved to the I18n localization file.
- //This structure is supposed to be close to the i18n-localization which will be used soon.
- messageMap: {
- errors: {
- post: {
- published: {
- 'published': 'Your post could not be updated.',
- 'draft': 'Your post could not be saved as a draft.'
- },
- draft: {
- 'published': 'Your post could not be published.',
- 'draft': 'Your post could not be saved as a draft.'
- }
-
- }
- },
-
- success: {
- post: {
- published: {
- 'published': 'Your post has been updated.',
- 'draft': 'Your post has been saved as a draft.'
- },
- draft: {
- 'published': 'Your post has been published.',
- 'draft': 'Your post has been saved as a draft.'
- }
- }
- }
- },
-
- initialize: function () {
- var self = this;
- // Toggle publish
- shortcut.add("Ctrl+Alt+P", function () {
- self.toggleStatus();
- });
- shortcut.add("Ctrl+S", function () {
- self.updatePost();
- });
- shortcut.add("Meta+S", function () {
- self.updatePost();
- });
- this.listenTo(this.model, 'change:status', this.render);
- },
-
- toggleStatus: function () {
- var self = this,
- keys = Object.keys(this.statusMap),
- model = self.model,
- prevStatus = model.get('status'),
- currentIndex = keys.indexOf(prevStatus),
- newIndex,
- status;
-
- newIndex = currentIndex + 1 > keys.length - 1 ? 0 : currentIndex + 1;
- status = keys[newIndex];
-
- this.setActiveStatus(keys[newIndex], this.statusMap[status], prevStatus);
-
- this.savePost({
- status: keys[newIndex]
- }).then(function () {
- self.reportSaveSuccess(status, prevStatus);
- }, function (xhr) {
- // Show a notification about the error
- self.reportSaveError(xhr, model, status, prevStatus);
- });
- },
-
- setActiveStatus: function (newStatus, displayText, currentStatus) {
- var isPublishing = (newStatus === 'published' && currentStatus !== 'published'),
- isUnpublishing = (newStatus === 'draft' && currentStatus === 'published'),
- // Controls when background of button has the splitbutton-delete/button-delete classes applied
- isImportantStatus = (isPublishing || isUnpublishing);
-
- $('.js-publish-splitbutton')
- .removeClass(isImportantStatus ? 'splitbutton-save' : 'splitbutton-delete')
- .addClass(isImportantStatus ? 'splitbutton-delete' : 'splitbutton-save');
-
- // Set the publish button's action and proper coloring
- $('.js-publish-button')
- .attr('data-status', newStatus)
- .text(displayText)
- .removeClass(isImportantStatus ? 'button-save' : 'button-delete')
- .addClass(isImportantStatus ? 'button-delete' : 'button-save');
-
- // Remove the animated popup arrow
- $('.js-publish-splitbutton > a')
- .removeClass('active');
-
- // Set the active action in the popup
- $('.js-publish-splitbutton .editor-options li')
- .removeClass('active')
- .filter(['li[data-set-status="', newStatus, '"]'].join(''))
- .addClass('active');
- },
-
- handleStatus: function (e) {
- if (e) { e.preventDefault(); }
- var status = $(e.currentTarget).attr('data-set-status'),
- currentStatus = this.model.get('status');
-
- this.setActiveStatus(status, this.statusMap[status], currentStatus);
-
- // Dismiss the popup menu
- $('body').find('.overlay:visible').fadeOut();
- },
-
- handlePostButton: function (e) {
- if (e) { e.preventDefault(); }
- var status = $(e.currentTarget).attr('data-status');
-
- this.updatePost(status);
- },
-
- updatePost: function (status) {
- var self = this,
- model = this.model,
- prevStatus = model.get('status');
-
- // Default to same status if not passed in
- status = status || prevStatus;
-
- model.trigger('willSave');
-
- this.savePost({
- status: status
- }).then(function () {
- self.reportSaveSuccess(status, prevStatus);
- // Refresh publish button and all relevant controls with updated status.
- self.render();
- }, function (xhr) {
- // Set the model status back to previous
- model.set({ status: prevStatus });
- // Set appropriate button status
- self.setActiveStatus(status, self.statusMap[status], prevStatus);
- // Show a notification about the error
- self.reportSaveError(xhr, model, status, prevStatus);
- });
- },
-
- savePost: function (data) {
- var publishButton = $('.js-publish-button'),
- saved,
- enablePublish = function (deferred) {
- deferred.always(function () {
- publishButton.prop('disabled', false);
- });
- return deferred;
- };
-
- publishButton.prop('disabled', true);
-
- _.each(this.model.blacklist, function (item) {
- this.model.unset(item);
- }, this);
-
- saved = this.model.save(_.extend({
- title: $('#entry-title').val(),
- markdown: Ghost.currentView.getEditorValue()
- }, data));
-
- // TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone
- // ourselves for more consistent promises.
- if (saved) {
- return enablePublish(saved);
- }
-
- return enablePublish($.Deferred().reject());
- },
-
- reportSaveSuccess: function (status, prevStatus) {
- Ghost.notifications.clearEverything();
- Ghost.notifications.addItem({
- type: 'success',
- message: this.messageMap.success.post[prevStatus][status],
- status: 'passive'
- });
- Ghost.currentView.setEditorDirty(false);
- },
-
- reportSaveError: function (response, model, status, prevStatus) {
- var message = this.messageMap.errors.post[prevStatus][status];
-
- if (response) {
- // Get message from response
- message += " " + Ghost.Views.Utils.getRequestErrorMessage(response);
- } else if (model.validationError) {
- // Grab a validation error
- message += " " + model.validationError;
- }
-
- Ghost.notifications.clearEverything();
- Ghost.notifications.addItem({
- type: 'error',
- message: message,
- status: 'passive'
- });
- },
-
- setStatusLabels: function (statusMap) {
- _.each(statusMap, function (label, status) {
- $('li[data-set-status="' + status + '"] > a').text(label);
- });
- },
-
- render: function () {
- var status = this.model.get('status');
-
- // Assume that we're creating a new post
- if (status !== 'published') {
- this.statusMap = this.createStatusMap;
- } else {
- this.statusMap = this.updateStatusMap;
- }
-
- // Populate the publish menu with the appropriate verbiage
- this.setStatusLabels(this.statusMap);
-
- // Default the selected publish option to the current status of the post.
- this.setActiveStatus(status, this.statusMap[status], status);
- }
-
- });
// The entire /editor page's route
// ----------------------------------------
Ghost.Views.Editor = Ghost.View.extend({
- initialize: function () {
- var self = this;
-
- // Add the container view for the Publish Bar
- this.addSubview(new PublishBar({el: "#publish-bar", model: this.model})).render();
-
- this.$('#entry-title').val(this.model.get('title')).focus();
- this.$('#entry-markdown').text(this.model.get('markdown'));
-
- this.listenTo(this.model, 'change:title', this.renderTitle);
- this.listenTo(this.model, 'change:id', function (m) {
- // This is a special case for browsers which fire an unload event when using navigate. The id change
- // happens before the save success and can cause the unload alert to appear incorrectly on first save
- // The id only changes in the event that the save has been successful, so this workaround is safes
- self.setEditorDirty(false);
- Backbone.history.navigate('/editor/' + m.id + '/');
- });
-
- this.initMarkdown();
- this.renderPreview();
-
- $('.entry-content header, .entry-preview header').on('click', function () {
- $('.entry-content, .entry-preview').removeClass('active');
- $(this).closest('section').addClass('active');
- });
-
- $('.entry-title .icon-fullscreen').on('click', function (e) {
- e.preventDefault();
- $('body').toggleClass('fullscreen');
- });
-
- this.$('.CodeMirror-scroll').on('scroll', this.syncScroll);
-
- this.$('.CodeMirror-scroll').scrollClass({target: '.entry-markdown', offset: 10});
- this.$('.entry-preview-content').scrollClass({target: '.entry-preview', offset: 10});
-
-
- // Zen writing mode shortcut
- shortcut.add("Alt+Shift+Z", function () {
- $('body').toggleClass('zen');
- });
-
- $('.entry-markdown header, .entry-preview header').click(function (e) {
- $('.entry-markdown, .entry-preview').removeClass('active');
- $(e.target).closest('section').addClass('active');
- });
-
- // Deactivate default drag/drop action
- $(document).bind('drop dragover', function (e) {
- e.preventDefault();
- });
- },
-
events: {
'click .markdown-help': 'showHelp',
'blur #entry-title': 'trimTitle',
'orientationchange': 'orientationChange'
},
- syncScroll: _.throttle(function (e) {
- var $codeViewport = $(e.target),
- $previewViewport = $('.entry-preview-content'),
- $codeContent = $('.CodeMirror-sizer'),
- $previewContent = $('.rendered-markdown'),
+ initialize: function () {
+ this.$title = this.$('#entry-title');
+ this.$editor = this.$('#entry-markdown');
- // calc position
- codeHeight = $codeContent.height() - $codeViewport.height(),
- previewHeight = $previewContent.height() - $previewViewport.height(),
- ratio = previewHeight / codeHeight,
- previewPostition = $codeViewport.scrollTop() * ratio;
+ this.$title.val(this.model.get('title')).focus();
+ this.$editor.text(this.model.get('markdown'));
- // apply new scroll
- $previewViewport.scrollTop(previewPostition);
- }, 10),
+ // Create a new editor
+ this.editor = new Ghost.Editor.Main();
- showHelp: function () {
- this.addSubview(new Ghost.Views.Modal({
- model: {
- options: {
- close: true,
- style: ["wide"],
- animation: 'fade'
- },
- content: {
- template: 'markdown',
- title: 'Markdown Help'
- }
- }
- }));
+ // Add the container view for the Publish Bar
+ // Passing reference to the title and editor
+ this.addSubview(new PublishBar(
+ {el: '#publish-bar', model: this.model, $title: this.$title, editor: this.editor}
+ )).render();
+
+ this.listenTo(this.model, 'change:title', this.renderTitle);
+ this.listenTo(this.model, 'change:id', this.handleIdChange);
+
+ this.bindShortcuts();
+
+ $('.entry-markdown header, .entry-preview header').on('click', function (e) {
+ $('.entry-markdown, .entry-preview').removeClass('active');
+ $(e.currentTarget).closest('section').addClass('active');
+ });
+ },
+
+ bindShortcuts: function () {
+ var self = this;
+
+ // Zen writing mode shortcut - full editor view
+ shortcut.add('Alt+Shift+Z', function () {
+ $('body').toggleClass('zen');
+ });
+
+ // HTML copy & paste
+ shortcut.add('Ctrl+Alt+C', function () {
+ self.showHTML();
+ });
},
trimTitle: function () {
- var $title = $('#entry-title'),
- rawTitle = $title.val(),
+ var rawTitle = this.$title.val(),
trimmedTitle = $.trim(rawTitle);
if (rawTitle !== trimmedTitle) {
- $title.val(trimmedTitle);
+ this.$title.val(trimmedTitle);
}
// Trigger title change for post-settings.js
@@ -412,7 +101,15 @@
},
renderTitle: function () {
- this.$('#entry-title').val(this.model.get('title'));
+ this.$title.val(this.model.get('title'));
+ },
+
+ handleIdChange: function (m) {
+ // This is a special case for browsers which fire an unload event when using navigate. The id change
+ // happens before the save success and can cause the unload alert to appear incorrectly on first save
+ // The id only changes in the event that the save has been successful, so this workaround is safes
+ this.editor.setDirty(false);
+ Backbone.history.navigate('/editor/' + m.id + '/');
},
// This is a hack to remove iOS6 white space on orientation change bug
@@ -427,307 +124,35 @@
}
},
- // This updates the editor preview panel.
- // Currently gets called on every key press.
- // Also trigger word count update
- renderPreview: function () {
- var self = this,
- preview = document.getElementsByClassName('rendered-markdown')[0];
- preview.innerHTML = this.converter.makeHtml(this.editor.getValue());
-
- this.initUploads();
-
- Countable.once(preview, function (counter) {
- self.$('.entry-word-count').text($.pluralize(counter.words, 'word'));
- self.$('.entry-character-count').text($.pluralize(counter.characters, 'character'));
- self.$('.entry-paragraph-count').text($.pluralize(counter.paragraphs, 'paragraph'));
- });
- },
-
- // Markdown converter & markdown shortcut initialization.
- initMarkdown: function () {
- var self = this;
-
- this.converter = new Showdown.converter({extensions: ['typography', 'ghostdown', 'github']});
- this.editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
- mode: 'gfm',
- tabMode: 'indent',
- tabindex: "2",
- lineWrapping: true,
- dragDrop: false,
- extraKeys: {
- Home: "goLineLeft",
- End: "goLineRight"
- }
- });
- this.uploadMgr = new UploadManager(this.editor);
-
- // Inject modal for HTML to be viewed in
- shortcut.add("Ctrl+Alt+C", function () {
- self.showHTML();
- });
- shortcut.add("Ctrl+Alt+C", function () {
- self.showHTML();
- });
-
- _.each(MarkdownShortcuts, function (combo) {
- shortcut.add(combo.key, function () {
- return self.editor.addMarkdown({style: combo.style});
- });
- });
-
- this.enableEditor();
- },
-
- options: {
- markers: {}
- },
-
- getEditorValue: function () {
- return this.uploadMgr.getEditorValue();
- },
-
- unloadDirtyMessage: function () {
- return "==============================\n\n" +
- "Hey there! It looks like you're in the middle of writing" +
- " something and you haven't saved all of your content." +
- "\n\nSave before you go!\n\n" +
- "==============================";
- },
-
- setEditorDirty: function (dirty) {
- window.onbeforeunload = dirty ? this.unloadDirtyMessage : null;
- },
-
- initUploads: function () {
- var filestorage = $('#entry-markdown-content').data('filestorage');
- this.$('.js-drop-zone').upload({editor: true, fileStorage: filestorage});
- this.$('.js-drop-zone').on('uploadstart', $.proxy(this.disableEditor, this));
- this.$('.js-drop-zone').on('uploadfailure', $.proxy(this.enableEditor, this));
- this.$('.js-drop-zone').on('uploadsuccess', $.proxy(this.enableEditor, this));
- this.$('.js-drop-zone').on('uploadsuccess', this.uploadMgr.handleUpload);
- },
-
- enableEditor: function () {
- var self = this;
- this.editor.setOption("readOnly", false);
- this.editor.on('change', function () {
- self.setEditorDirty(true);
- self.renderPreview();
- });
- },
-
- disableEditor: function () {
- var self = this;
- this.editor.setOption("readOnly", "nocursor");
- this.editor.off('change', function () {
- self.renderPreview();
- });
- },
-
- showHTML: function () {
+ showEditorModal: function (content) {
this.addSubview(new Ghost.Views.Modal({
model: {
options: {
close: true,
- style: ["wide"],
+ style: ['wide'],
animation: 'fade'
},
- content: {
- template: 'copyToHTML',
- title: 'Copied HTML'
- }
+ content: content
}
}));
},
+ showHelp: function () {
+ var content = {
+ template: 'markdown',
+ title: 'Markdown Help'
+ };
+ this.showEditorModal(content);
+ },
+
+ showHTML: function () {
+ var content = {
+ template: 'copyToHTML',
+ title: 'Copied HTML'
+ };
+ this.showEditorModal(content);
+ },
+
render: function () { return this; }
});
-
- MarkerManager = function (editor) {
- var markers = {},
- uploadPrefix = 'image_upload',
- uploadId = 1;
-
- function addMarker(line, ln) {
- var marker,
- magicId = '{<' + uploadId + '>}';
- editor.setLine(ln, magicId + line.text);
- marker = editor.markText(
- {line: ln, ch: 0},
- {line: ln, ch: (magicId.length)},
- {collapsed: true}
- );
-
- markers[uploadPrefix + '_' + uploadId] = marker;
- uploadId += 1;
- }
-
- function getMarkerRegexForId(id) {
- id = id.replace('image_upload_', '');
- return new RegExp('\\{<' + id + '>\\}', 'gmi');
- }
-
- function stripMarkerFromLine(line) {
- var markerText = line.text.match(markerRegex),
- ln = editor.getLineNumber(line);
-
- if (markerText) {
- editor.replaceRange('', {line: ln, ch: markerText.index}, {line: ln, ch: markerText.index + markerText[0].length});
- }
- }
-
- function findAndStripMarker(id) {
- editor.eachLine(function (line) {
- var markerText = getMarkerRegexForId(id).exec(line.text),
- ln;
-
- if (markerText) {
- ln = editor.getLineNumber(line);
- editor.replaceRange('', {line: ln, ch: markerText.index}, {line: ln, ch: markerText.index + markerText[0].length});
- }
- });
- }
-
- function removeMarker(id, marker, line) {
- delete markers[id];
- marker.clear();
-
- if (line) {
- stripMarkerFromLine(line);
- } else {
- findAndStripMarker(id);
- }
- }
-
- function checkMarkers() {
- _.each(markers, function (marker, id) {
- var line;
- marker = markers[id];
- if (marker.find()) {
- line = editor.getLineHandle(marker.find().from.line);
- if (!line.text.match(imageMarkdownRegex)) {
- removeMarker(id, marker, line);
- }
- } else {
- removeMarker(id, marker);
- }
- });
- }
-
- function initMarkers(line) {
- var isImage = line.text.match(imageMarkdownRegex),
- hasMarker = line.text.match(markerRegex);
-
- if (isImage && !hasMarker) {
- addMarker(line, editor.getLineNumber(line));
- }
- }
-
- // public api
- _.extend(this, {
- markers: markers,
- checkMarkers: checkMarkers,
- addMarker: addMarker,
- stripMarkerFromLine: stripMarkerFromLine,
- getMarkerRegexForId: getMarkerRegexForId
- });
-
- // Initialise
- editor.eachLine(initMarkers);
- };
-
- UploadManager = function (editor) {
- var markerMgr = new MarkerManager(editor);
-
- function findLine(result_id) {
- // try to find the right line to replace
- if (markerMgr.markers.hasOwnProperty(result_id) && markerMgr.markers[result_id].find()) {
- return editor.getLineHandle(markerMgr.markers[result_id].find().from.line);
- }
-
- return false;
- }
-
- function checkLine(ln, mode) {
- var line = editor.getLineHandle(ln),
- isImage = line.text.match(imageMarkdownRegex),
- hasMarker;
-
- // We care if it is an image
- if (isImage) {
- hasMarker = line.text.match(markerRegex);
-
- if (hasMarker && mode === 'paste') {
- // this could be a duplicate, and won't be a real marker
- markerMgr.stripMarkerFromLine(line);
- }
-
- if (!hasMarker) {
- markerMgr.addMarker(line, ln);
- }
- }
- // TODO: hasMarker but no image?
- }
-
- function handleUpload(e, result_src) {
- /*jslint regexp: true, bitwise: true */
- var line = findLine($(e.currentTarget).attr('id')),
- lineNumber = editor.getLineNumber(line),
- match = line.text.match(/\([^\n]*\)?/),
- replacement = '(http://)';
- /*jslint regexp: false, bitwise: false */
-
- if (match) {
- // simple case, we have the parenthesis
- editor.setSelection({line: lineNumber, ch: match.index + 1}, {line: lineNumber, ch: match.index + match[0].length - 1});
- } else {
- match = line.text.match(/\]/);
- if (match) {
- editor.replaceRange(
- replacement,
- {line: lineNumber, ch: match.index + 1},
- {line: lineNumber, ch: match.index + 1}
- );
- editor.setSelection(
- {line: lineNumber, ch: match.index + 2},
- {line: lineNumber, ch: match.index + replacement.length }
- );
- }
- }
- editor.replaceSelection(result_src);
- }
-
- function getEditorValue() {
- var value = editor.getValue();
-
- _.each(markerMgr.markers, function (marker, id) {
- /*jshint unused:false*/
- value = value.replace(markerMgr.getMarkerRegexForId(id), '');
- });
-
- return value;
- }
-
-
- // Public API
- _.extend(this, {
- getEditorValue: getEditorValue,
- handleUpload: handleUpload
- });
-
- // initialise
- editor.on('change', function (cm, changeObj) {
- /*jshint unused:false*/
- var linesChanged = _.range(changeObj.from.line, changeObj.from.line + changeObj.text.length);
-
- _.each(linesChanged, function (ln) {
- checkLine(ln, changeObj.origin);
- });
-
- // Is this a line which may have had a marker on it?
- markerMgr.checkMarkers();
- });
- };
-
-}());
+}());
\ No newline at end of file