mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
Removed local markdown editor within ghost (#379)
This commit is contained in:
parent
6f1a3f0261
commit
13eefe0243
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
@ -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());
|
||||
}
|
||||
}
|
||||
});
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
@ -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();
|
||||
}
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
@ -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
|
||||
/* ---------------------------------------------------------- */
|
||||
|
@ -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}}
|
@ -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');
|
||||
});
|
||||
}
|
||||
);
|
Loading…
Reference in New Issue
Block a user