Removed local markdown editor within ghost (#379)

This commit is contained in:
Ryan McCarvill 2016-11-08 04:59:48 +13:00 committed by Kevin Ansfield
parent 6f1a3f0261
commit 13eefe0243
9 changed files with 0 additions and 1045 deletions

View File

@ -1,51 +0,0 @@
import TextArea from 'ember-components/text-area';
import run from 'ember-runloop';
import EditorAPI from 'ghost-admin/mixins/ed-editor-api';
import EditorShortcuts from 'ghost-admin/mixins/ed-editor-shortcuts';
import EditorScroll from 'ghost-admin/mixins/ed-editor-scroll';
import {InvokeActionMixin} from 'ember-invoke-action';
export default TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, InvokeActionMixin, {
focus: false,
/**
* Tell the controller about focusIn events, will trigger an autosave on a new document
*/
focusIn() {
this.sendAction('onFocusIn');
},
/**
* Sets the focus of the textarea if needed
*/
setFocus() {
if (this.get('focus')) {
this.$().val(this.$().val()).focus();
}
},
/**
* Sets up properties at render time
*/
didInsertElement() {
this._super(...arguments);
this.setFocus();
this.invokeAction('setEditor', this);
run.scheduleOnce('afterRender', this, this.afterRenderEvent);
},
afterRenderEvent() {
if (this.get('focus') && this.get('focusCursorAtEnd')) {
this.setSelection('end');
}
},
actions: {
toggleCopyHTMLModal(generatedHTML) {
this.invokeAction('toggleCopyHTMLModal', generatedHTML);
}
}
});

View File

@ -1,127 +0,0 @@
import Ember from 'ember';
import Component from 'ember-component';
import EmberObject from 'ember-object';
import run from 'ember-runloop';
import {A as emberA} from 'ember-array/utils';
import {formatMarkdown} from 'ghost-admin/helpers/gh-format-markdown';
// ember-cli-shims doesn't export uuid
const {uuid} = Ember;
export default Component.extend({
_scrollWrapper: null,
previewHTML: '',
init() {
this._super(...arguments);
this.set('imageUploadComponents', emberA([]));
this.buildPreviewHTML();
},
didInsertElement() {
this._super(...arguments);
this._scrollWrapper = this.$().closest('.entry-preview-content');
this.adjustScrollPosition(this.get('scrollPosition'));
},
didReceiveAttrs(attrs) {
this._super(...arguments);
if (!attrs.oldAttrs) {
return;
}
if (attrs.newAttrs.scrollPosition && attrs.newAttrs.scrollPosition.value !== attrs.oldAttrs.scrollPosition.value) {
this.adjustScrollPosition(attrs.newAttrs.scrollPosition.value);
}
if (attrs.newAttrs.markdown.value !== attrs.oldAttrs.markdown.value) {
run.throttle(this, this.buildPreviewHTML, 30, false);
}
},
adjustScrollPosition(scrollPosition) {
let scrollWrapper = this._scrollWrapper;
if (scrollWrapper) {
scrollWrapper.scrollTop(scrollPosition);
}
},
buildPreviewHTML() {
let markdown = this.get('markdown');
let html = formatMarkdown([markdown]).string;
let template = document.createElement('template');
template.innerHTML = html;
let fragment = template.content;
if (!fragment) {
fragment = document.createDocumentFragment();
while (template.childNodes[0]) {
fragment.appendChild(template.childNodes[0]);
}
}
let dropzones = fragment.querySelectorAll('.js-drop-zone');
let components = this.get('imageUploadComponents');
if (dropzones.length !== components.length) {
components = emberA([]);
this.set('imageUploadComponents', components);
}
[...dropzones].forEach((oldEl, i) => {
let el = oldEl.cloneNode(true);
let component = components[i];
let uploadTarget = el.querySelector('.js-upload-target');
let altTextWrapper = oldEl.querySelector('.js-drop-zone .description strong');
let id = uuid();
let destinationElementId = `image-uploader-${id}`;
let src, altText;
if (uploadTarget) {
src = uploadTarget.getAttribute('src');
}
if (altTextWrapper) {
altText = altTextWrapper.innerHTML;
}
if (component) {
component.set('destinationElementId', destinationElementId);
component.set('src', src);
component.set('altText', altText);
} else {
let imageUpload = EmberObject.create({
destinationElementId,
id,
src,
altText,
index: i
});
this.get('imageUploadComponents').pushObject(imageUpload);
}
el.id = destinationElementId;
el.innerHTML = '';
el.classList.remove('image-uploader');
oldEl.parentNode.replaceChild(el, oldEl);
});
this.set('previewHTML', fragment);
},
actions: {
updateImageSrc(index, url) {
this.attrs.updateImageSrc(index, url);
},
updateHeight() {
this.attrs.updateHeight(this.$().height());
}
}
});

View File

@ -1,123 +0,0 @@
import Component from 'ember-component';
import computed, {equal} from 'ember-computed';
import run from 'ember-runloop';
import ShortcutsMixin from 'ghost-admin/mixins/shortcuts';
import imageManager from 'ghost-admin/utils/ed-image-manager';
import editorShortcuts from 'ghost-admin/utils/editor-shortcuts';
import {invokeAction} from 'ember-invoke-action';
export default Component.extend(ShortcutsMixin, {
tagName: 'section',
classNames: ['view-container', 'view-editor'],
activeTab: 'markdown',
editor: null,
editorDisabled: undefined,
editorScrollInfo: null, // updated when gh-ed-editor component scrolls
height: null, // updated when markdown is rendered
shouldFocusEditor: false,
showCopyHTMLModal: false,
copyHTMLModalContent: null,
shortcuts: editorShortcuts,
markdownActive: equal('activeTab', 'markdown'),
previewActive: equal('activeTab', 'preview'),
// HTML Preview listens to scrollPosition and updates its scrollTop value
// This property receives scrollInfo from the textEditor, and height from the preview pane, and will update the
// scrollPosition value such that when either scrolling or typing-at-the-end of the text editor the preview pane
// stays in sync
scrollPosition: computed('editorScrollInfo', 'height', function () {
let scrollInfo = this.get('editorScrollInfo');
let {$previewContent, $previewViewPort} = this;
if (!scrollInfo || !$previewContent || !$previewViewPort) {
return 0;
}
let previewHeight = $previewContent.height() - $previewViewPort.height();
let previewPosition, ratio;
ratio = previewHeight / scrollInfo.diff;
previewPosition = scrollInfo.top * ratio;
return previewPosition;
}),
didInsertElement() {
this._super(...arguments);
this.registerShortcuts();
run.scheduleOnce('afterRender', this, this._cacheElements);
},
willDestroyElement() {
invokeAction(this, 'onTeardown');
this.removeShortcuts();
},
_cacheElements() {
// cache these elements for use in other methods
this.$previewViewPort = this.$('.js-entry-preview-content');
this.$previewContent = this.$('.js-rendered-markdown');
},
actions: {
selectTab(tab) {
this.set('activeTab', tab);
},
updateScrollInfo(scrollInfo) {
this.set('editorScrollInfo', scrollInfo);
},
updateHeight(height) {
this.set('height', height);
},
// set from a `sendAction` on the gh-ed-editor component,
// so that we get a reference for handling uploads.
setEditor(editor) {
this.set('editor', editor);
},
disableEditor() {
this.set('editorDisabled', true);
},
enableEditor() {
this.set('editorDisabled', undefined);
},
// The actual functionality is implemented in utils/ed-editor-shortcuts
editorShortcut(options) {
if (this.editor.$().is(':focus')) {
this.editor.shortcut(options.type);
}
},
// 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.
handleImgUpload(imageIndex, newSrc) {
let editor = this.get('editor');
let editorValue = editor.getValue();
let replacement = imageManager.getSrcRange(editorValue, imageIndex);
let cursorPosition;
if (replacement) {
cursorPosition = replacement.start + newSrc.length + 1;
if (replacement.needsParens) {
newSrc = `(${newSrc})`;
}
editor.replaceSelection(newSrc, replacement.start, replacement.end, cursorPosition);
}
},
toggleCopyHTMLModal(generatedHTML) {
this.set('copyHTMLModalContent', generatedHTML);
this.toggleProperty('showCopyHTMLModal');
}
}
});

View File

@ -1,138 +0,0 @@
import Mixin from 'ember-metal/mixin';
import run from 'ember-runloop';
export default Mixin.create({
/**
* Get Value
*
* Get the full contents of the textarea
*
* @returns {String}
*/
getValue() {
return this.readDOMAttr('value');
},
/**
* Get Selection
*
* Return the currently selected text from the textarea
*
* @returns {Selection}
*/
getSelection() {
return this.$().getSelection();
},
/**
* Get Line To Cursor
*
* Fetch the string of characters from the start of the given line up to the cursor
* @returns {{text: string, start: number}}
*/
getLineToCursor() {
let selection = this.$().getSelection();
let value = this.getValue();
let lineStart;
// Normalise newlines
value = value.replace('\r\n', '\n');
// We want to look at the characters behind the cursor
lineStart = value.lastIndexOf('\n', selection.start - 1) + 1;
return {
text: value.substring(lineStart, selection.start),
start: lineStart
};
},
/**
* Get Line
*
* Return the string of characters for the line the cursor is currently on
*
* @returns {{text: string, start: number, end: number}}
*/
getLine() {
let selection = this.$().getSelection();
let value = this.getValue();
let lineStart,
lineEnd;
// Normalise newlines
value = value.replace('\r\n', '\n');
// We want to look at the characters behind the cursor
lineStart = value.lastIndexOf('\n', selection.start - 1) + 1;
lineEnd = value.indexOf('\n', selection.start);
lineEnd = lineEnd === -1 ? value.length - 1 : lineEnd;
return {
// jscs:disable
text: value.substring(lineStart, lineEnd).replace(/^\n/, ''),
// jscs:enable
start: lineStart,
end: lineEnd
};
},
/**
* Set Selection
*
* Set the section of text in the textarea that should be selected by the cursor
*
* @param {number} start
* @param {number} end
*/
setSelection(start, end) {
let $textarea = this.$();
if (start === 'end') {
start = $textarea.val().length;
}
end = end || start;
$textarea.setSelection(start, end);
},
/**
* Replace Selection
*
* @param {String} replacement - the string to replace with
* @param {number} replacementStart - where to start replacing
* @param {number} [replacementEnd] - when to stop replacing, defaults to replacementStart
* @param {String|boolean|Object} [cursorPosition] - where to put the cursor after replacing
*
* Cursor position after replacement defaults to the end of the replacement.
* Providing selectionStart only will cause the cursor to be placed there, or alternatively a range can be selected
* by providing selectionEnd.
*/
replaceSelection(replacement, replacementStart, replacementEnd, cursorPosition) {
run.schedule('afterRender', this, function () {
let $textarea = this.$();
cursorPosition = cursorPosition || 'collapseToEnd';
replacementEnd = replacementEnd || replacementStart;
$textarea.setSelection(replacementStart, replacementEnd);
if (['select', 'collapseToStart', 'collapseToEnd'].indexOf(cursorPosition) !== -1) {
$textarea.replaceSelectedText(replacement, cursorPosition);
} else {
$textarea.replaceSelectedText(replacement);
if (cursorPosition.hasOwnProperty('start')) {
$textarea.setSelection(cursorPosition.start, cursorPosition.end);
} else {
$textarea.setSelection(cursorPosition, cursorPosition);
}
}
$textarea.focus();
// Tell the editor it has changed, as programmatic replacements won't trigger this automatically
this._elementValueDidChange();
this.sendAction('onChange');
});
}
});

View File

@ -1,99 +0,0 @@
import Mixin from 'ember-metal/mixin';
import run from 'ember-runloop';
import {invokeAction} from 'ember-invoke-action';
export default Mixin.create({
/**
* Determine if the cursor is at the end of the textarea
*/
isCursorAtEnd() {
let selection = this.$().getSelection();
let value = this.getValue();
let linesAtEnd = 3;
let match,
stringAfterCursor;
stringAfterCursor = value.substring(selection.end);
match = stringAfterCursor.match(/\n/g);
if (!match || match.length < linesAtEnd) {
return true;
}
return false;
},
/**
* Build an object that represents the scroll state
*/
getScrollInfo() {
let scroller = this.get('element');
let scrollInfo = {
top: scroller.scrollTop,
height: scroller.scrollHeight,
clientHeight: scroller.clientHeight,
diff: scroller.scrollHeight - scroller.clientHeight,
padding: 50,
isCursorAtEnd: this.isCursorAtEnd()
};
return scrollInfo;
},
/**
* Calculate if we're within scrollInfo.padding of the end of the document, and scroll the rest of the way
*/
adjustScrollPosition() {
// If we're receiving change events from the end of the document, i.e the user is typing-at-the-end, update the
// scroll position to ensure both panels stay in view and in sync
let scrollInfo = this.getScrollInfo();
if (scrollInfo.isCursorAtEnd && (scrollInfo.diff >= scrollInfo.top) &&
(scrollInfo.diff < scrollInfo.top + scrollInfo.padding)) {
scrollInfo.top += scrollInfo.padding;
// Scroll the left pane
this.$().scrollTop(scrollInfo.top);
}
},
/**
* Send the scrollInfo for scrollEvents to the view so that the preview pane can be synced
*/
scrollHandler() {
this.set('scrollThrottle', run.throttle(this, () => {
invokeAction(this, 'updateScrollInfo', this.getScrollInfo());
}, 10));
},
/**
* once the element is in the DOM bind to the events which control scroll behaviour
*/
attachScrollHandlers() {
let $el = this.$();
$el.on('keypress', run.bind(this, this.adjustScrollPosition));
$el.on('scroll', run.bind(this, this.scrollHandler));
},
/**
* once the element has been removed from the DOM unbind from the events which control scroll behaviour
*/
detachScrollHandlers() {
this.$().off('keypress');
this.$().off('scroll');
run.cancel(this.get('scrollThrottle'));
},
didInsertElement() {
this._super(...arguments);
this.attachScrollHandlers();
},
willDestroyElement() {
this._super(...arguments);
this.detachScrollHandlers();
}
});

View File

@ -1,171 +0,0 @@
/* global moment, Showdown */
import Mixin from 'ember-metal/mixin';
import titleize from 'ghost-admin/utils/titleize';
// Used for simple, noncomputational replace-and-go! shortcuts.
// See default case in shortcut function below.
let simpleShortcutSyntax = {
bold: {
regex: '**|**',
cursor: '|'
},
italic: {
regex: '*|*',
cursor: '|'
},
strike: {
regex: '~~|~~',
cursor: '|'
},
code: {
regex: '`|`',
cursor: '|'
},
blockquote: {
regex: '> |',
cursor: '|',
newline: true
},
list: {
regex: '* |',
cursor: '|',
newline: true
},
link: {
regex: '[|](http://)',
cursor: 'http://'
},
image: {
regex: '![|](http://)',
cursor: 'http://',
newline: true
}
};
let shortcuts = {
simple(type, replacement, selection, line) {
let startIndex = 0;
let shortcut;
if (simpleShortcutSyntax.hasOwnProperty(type)) {
shortcut = simpleShortcutSyntax[type];
// insert the markdown
replacement.text = shortcut.regex.replace('|', selection.text);
// add a newline if needed
if (shortcut.newline && line.text !== '') {
startIndex = 1;
replacement.text = `\n${replacement.text}`;
}
// handle cursor position
if (selection.text === '' && shortcut.cursor === '|') {
// the cursor should go where | was
replacement.position = startIndex + replacement.start + shortcut.regex.indexOf(shortcut.cursor);
} else if (shortcut.cursor !== '|') {
// the cursor should select the string which matches shortcut.cursor
replacement.position = {
start: replacement.start + replacement.text.indexOf(shortcut.cursor)
};
replacement.position.end = replacement.position.start + shortcut.cursor.length;
}
}
return replacement;
},
cycleHeaderLevel(replacement, line) {
let match = line.text.match(/^#+/);
let currentHeaderLevel,
hashPrefix;
if (!match) {
currentHeaderLevel = 1;
} else {
currentHeaderLevel = match[0].length;
}
if (currentHeaderLevel > 2) {
currentHeaderLevel = 1;
}
hashPrefix = new Array(currentHeaderLevel + 2).join('#');
replacement.text = `${hashPrefix} ${line.text.replace(/^#* /, '')}`;
replacement.start = line.start;
replacement.end = line.end;
return replacement;
},
copyHTML(editor, selection) {
let converter = new Showdown.converter();
let generatedHTML;
if (selection.text) {
generatedHTML = converter.makeHtml(selection.text);
} else {
generatedHTML = converter.makeHtml(editor.getValue());
}
// Talk to the editor
editor.send('toggleCopyHTMLModal', generatedHTML);
},
currentDate(replacement) {
replacement.text = moment(new Date()).format('D MMMM YYYY');
return replacement;
},
uppercase(replacement, selection) {
replacement.text = selection.text.toLocaleUpperCase();
return replacement;
},
lowercase(replacement, selection) {
replacement.text = selection.text.toLocaleLowerCase();
return replacement;
},
titlecase(replacement, selection) {
replacement.text = titleize(selection.text);
return replacement;
}
};
export default Mixin.create({
shortcut(type) {
let selection = this.getSelection();
let replacement = {
start: selection.start,
end: selection.end,
position: 'collapseToEnd'
};
switch (type) {
// This shortcut is special as it needs to send an action
case 'copyHTML':
shortcuts.copyHTML(this, selection);
break;
case 'cycleHeaderLevel':
replacement = shortcuts.cycleHeaderLevel(replacement, this.getLine());
break;
// These shortcuts all process the basic information
case 'currentDate':
case 'uppercase':
case 'lowercase':
case 'titlecase':
replacement = shortcuts[type](replacement, selection, this.getLineToCursor());
break;
// All the of basic formatting shortcuts work with a regex
default:
replacement = shortcuts.simple(type, replacement, selection, this.getLineToCursor());
}
if (replacement.text) {
this.replaceSelection(replacement.text, replacement.start, replacement.end, replacement.position);
}
}
});

View File

@ -32,229 +32,6 @@
}
/* Container & Headers
/* ---------------------------------------------------------- */
.view-editor {
display: flex;
}
.editor .entry-preview {
border-left: #dfe1e3 1px solid;
}
.editor .entry-markdown,
.editor .entry-preview {
position: relative; /*TODO: Remove*/
display: flex;
flex-direction: column;
width: 50%;
}
/* Content areas at the top, and fill available space */
.editor .entry-markdown-content,
.editor .entry-preview-content {
order: 1;
flex-grow: 1;
}
/* Headers at the bottom, and fixed height */
.editor .floatingheader {
order: 2;
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 15px;
height: 40px;
border-top: #dfe1e3 1px solid;
color: var(--midgrey);
font-size: 1.2rem;
line-height: 1em;
}
.editor .floatingheader a {
padding: 5px 15px;
color: var(--midgrey);
}
.editor .floatingheader a.active {
font-weight: bold;
}
.editor .floatingheader a:first-of-type {
padding-left: 0;
}
.editor .floatingheader a:last-of-type {
padding-right: 0;
}
.editor .floatingheader span a:not(:first-of-type) {
border-left: 1px solid #dfe1e3;
}
.editor .floatingheader .mobile-tabs {
display: none;
}
/* Switch to 1 col editor on small screens */
@media (max-width: 1000px) {
.editor .entry-markdown,
.editor .entry-preview {
width: 100%;
border-left: none;
}
/* We can't use display:none here as we want to keep widths/heights
* so that scrolling is kept in sync */
.editor .entry-markdown:not(.active),
.editor .entry-preview:not(.active) {
visibility: hidden;
position: absolute;
z-index: -1;
height: 100%;
}
.editor .floatingheader .mobile-tabs {
display: inline;
}
.editor .floatingheader .desktop-tabs {
display: none;
}
}
/* Editor (Left pane)
/* ---------------------------------------------------------- */
.editor .entry-markdown-content {
position: relative;
flex-grow: 1;
}
.editor .markdown-editor {
/* Legacy absolute positioning */
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
-webkit-overflow-scrolling: touch;
padding: 21px 20px 36px 20px;
max-width: 100%;
height: 100%;
border: 0;
color: color(var(--darkgrey) lightness(+10%));
font-family: var(--font-family-mono);
font-size: 1.6rem;
line-height: 2.5rem;
resize: none;
}
.editor .markdown-editor:focus {
outline: 0;
}
@media (max-width: 450px) {
.editor .markdown-editor {
padding: 15px;
}
}
/* FFF: Fucking Firefox Fixes
/* ---------------------------------------------------------- */
@-moz-document url-prefix() {
.editor .markdown-editor {
top: 40px;
padding-top: 0;
padding-bottom: 0;
height: calc(100% - 40px);
}
}
/* Preview (Right pane)
/* ---------------------------------------------------------- */
.editor .entry-preview-content {
flex-grow: 1;
overflow: auto;
-webkit-overflow-scrolling: touch;
padding: 19px 20px 37px 20px;
word-break: break-word;
hyphens: auto;
cursor: default;
}
/* The styles for the actual content inside the preview */
.entry-preview-content,
.content-preview-content {
font-size: 1.8rem;
line-height: 1.5em;
font-weight: 200;
}
.entry-preview-content *,
.content-preview-content * {
user-select: text;
}
.entry-preview-content a,
.content-preview-content a {
color: var(--blue);
text-decoration: underline;
}
.entry-preview-content sup a,
.content-preview-content sup a {
text-decoration: none;
}
.entry-preview-content .btn,
.content-preview-content .btn {
color: #dfe1e3;
text-decoration: none;
}
.entry-preview-content .img-placeholder,
.content-preview-content .img-placeholder {
position: relative;
height: 100px;
border: 5px dashed #dfe1e3;
}
.entry-preview-content .img-placeholder span,
.content-preview-content .img-placeholder span {
position: absolute;
top: 50%;
display: block;
margin-top: -15px;
width: 100%;
height: 30px;
text-align: center;
}
.entry-preview-content a.image-edit,
.content-preview-content a.image-edit {
width: 16px;
height: 16px;
}
.entry-preview-content img,
.content-preview-content img {
margin: 0 auto;
max-width: 100%;
height: auto;
}
/* Placeholder objects for <script> & <iframe> */
.js-embed-placeholder,
.iframe-embed-placeholder {
padding: 100px 20px;
border: none;
background: #f9f9f9;
text-align: center;
font-family: var(--font-family);
font-size: 1.6rem;
font-weight: bold;
}
/* Tags input CSS (TODO: needs some revision)
/* ------------------------------------------------------ */
.tags-input-list {
@ -376,32 +153,6 @@
}
/* Markdown Help Icon + Modal
/* ---------------------------------------------------------- */
.markdown-help-icon {
font-size: 16px;
}
.markdown-help-icon:hover,
.markdown-help-label:hover {
cursor: help;
}
.modal-markdown-help-table {
margin: 0 0 20px;
width: 100%;
}
.modal-markdown-help-table td,
.modal-markdown-help-table th {
padding: 8px 0;
}
.modal-markdown-help-table th {
text-align: left;
}
/* NEW editor
/* ---------------------------------------------------------- */

View File

@ -1,53 +0,0 @@
<section class="entry-markdown js-entry-markdown {{if markdownActive 'active'}}">
<header class="floatingheader">
<span class="desktop-tabs"><a class="markdown-help-label" href="" title="Markdown Help" {{action (route-action "toggleMarkdownHelpModal")}}>Markdown</a></span>
<span class="mobile-tabs">
<a href="#" {{action 'selectTab' 'markdown'}} class="{{if markdownActive 'active'}}">Markdown</a>
<a href="#" {{action 'selectTab' 'preview'}} class="{{if previewActive 'active'}}">Preview</a>
</span>
<a class="markdown-help-icon" href="" title="Markdown Help" {{action (route-action "toggleMarkdownHelpModal")}}><i class="icon-markdown"></i></a>
</header>
<section id="entry-markdown-content" class="entry-markdown-content">
{{gh-ed-editor value
classNames="markdown-editor js-markdown-editor"
tabindex="1"
spellcheck="true"
value=value
setEditor=(action "setEditor")
updateScrollInfo=(action "updateScrollInfo")
toggleCopyHTMLModal=(action "toggleCopyHTMLModal")
onFocusIn=editorFocused
height=height
focus=shouldFocusEditor
readonly=editorDisabled
update=(action (mut value))
}}
</section>
</section>
<section class="entry-preview js-entry-preview {{if previewActive 'active'}}">
<header class="floatingheader">
<span class="desktop-tabs"><a target="_blank" href="{{previewUrl}}">Preview</a></span>
<span class="mobile-tabs">
<a href="#" {{action 'selectTab' 'markdown'}} class="{{if markdownActive 'active'}}">Markdown</a>
<a href="#" {{action 'selectTab' 'preview'}} class="{{if previewActive 'active'}}">Preview</a>
</span>
<span class="entry-word-count">{{gh-count-words value}}</span>
</header>
<section class="entry-preview-content js-entry-preview-content">
{{gh-ed-preview classNames="rendered-markdown js-rendered-markdown"
markdown=value
scrollPosition=scrollPosition
updateHeight=(action "updateHeight")
uploadStarted=(action "disableEditor")
uploadFinished=(action "enableEditor")
updateImageSrc=(action "handleImgUpload")}}
</section>
</section>
{{#if showCopyHTMLModal}}
{{gh-fullscreen-modal "copy-html"
model=copyHTMLModalContent
close=(action "toggleCopyHTMLModal")
modifier="action"}}
{{/if}}

View File

@ -1,34 +0,0 @@
/* jshint expr:true */
import {expect} from 'chai';
import {
describeComponent,
it
} from 'ember-mocha';
describeComponent(
'gh-editor',
'Unit: Component: gh-editor',
{
unit: true,
// specify the other units that are required for this test
needs: [
'component:gh-ed-editor',
'component:gh-ed-preview',
'helper:gh-count-words',
'helper:route-action',
'service:notifications'
]
},
function () {
it('renders', function () {
// creates the component instance
let component = this.subject();
expect(component._state).to.equal('preRender');
// renders the component on the page
this.render();
expect(component._state).to.equal('inDOM');
});
}
);