Commit 7187cf1c authored by Robert Knight's avatar Robert Knight

Convert `<markdown>` to a component

parent 5b36b58c
...@@ -145,6 +145,7 @@ module.exports = angular.module('h', [ ...@@ -145,6 +145,7 @@ module.exports = angular.module('h', [
.component('loggedoutMessage', require('./components/loggedout-message')) .component('loggedoutMessage', require('./components/loggedout-message'))
.component('loginControl', require('./components/login-control')) .component('loginControl', require('./components/login-control'))
.component('loginForm', require('./components/login-form').component) .component('loginForm', require('./components/login-form').component)
.component('markdown', require('./directive/markdown'))
.component('moderationBanner', require('./components/moderation-banner')) .component('moderationBanner', require('./components/moderation-banner'))
.component('publishAnnotationBtn', require('./components/publish-annotation-btn')) .component('publishAnnotationBtn', require('./components/publish-annotation-btn'))
.component('searchInput', require('./components/search-input')) .component('searchInput', require('./components/search-input'))
...@@ -161,7 +162,6 @@ module.exports = angular.module('h', [ ...@@ -161,7 +162,6 @@ module.exports = angular.module('h', [
.component('timestamp', require('./components/timestamp')) .component('timestamp', require('./components/timestamp'))
// These should use `component()` but will require some changes. // These should use `component()` but will require some changes.
.directive('markdown', require('./directive/markdown'))
.directive('topBar', require('./directive/top-bar')) .directive('topBar', require('./directive/top-bar'))
.directive('formInput', require('./directive/form-input')) .directive('formInput', require('./directive/form-input'))
......
...@@ -7,169 +7,167 @@ var mediaEmbedder = require('../media-embedder'); ...@@ -7,169 +7,167 @@ var mediaEmbedder = require('../media-embedder');
var renderMarkdown = require('../render-markdown'); var renderMarkdown = require('../render-markdown');
var scopeTimeout = require('../util/scope-timeout'); var scopeTimeout = require('../util/scope-timeout');
function MarkdownController($element, $sanitize, $scope) {
var input = $element[0].querySelector('.js-markdown-input');
var output = $element[0].querySelector('.js-markdown-preview');
var self = this;
/**
* Transform the editor's input field with an editor command.
*/
function updateState(newStateFn) {
var newState = newStateFn({
text: input.value,
selectionStart: input.selectionStart,
selectionEnd: input.selectionEnd,
});
input.value = newState.text;
input.selectionStart = newState.selectionStart;
input.selectionEnd = newState.selectionEnd;
// The input field currently loses focus when the contents are
// changed. This re-focuses the input field but really it should
// happen automatically.
input.focus();
self.onEditText({text: input.value});
}
function focusInput() {
// When the visibility of the editor changes, focus it.
// A timeout is used so that focus() is not called until
// the visibility change has been applied (by adding or removing
// the relevant CSS classes)
scopeTimeout($scope, function () {
input.focus();
}, 0);
}
this.insertBold = function() {
updateState(function (state) {
return commands.toggleSpanStyle(state, '**', '**', 'Bold');
});
};
this.insertItalic = function() {
updateState(function (state) {
return commands.toggleSpanStyle(state, '*', '*', 'Italic');
});
};
this.insertMath = function() {
updateState(function (state) {
var before = state.text.slice(0, state.selectionStart);
if (before.length === 0 ||
before.slice(-1) === '\n' ||
before.slice(-2) === '$$') {
return commands.toggleSpanStyle(state, '$$', '$$', 'Insert LaTeX');
} else {
return commands.toggleSpanStyle(state, '\\(', '\\)',
'Insert LaTeX');
}
});
};
this.insertLink = function() {
updateState(function (state) {
return commands.convertSelectionToLink(state);
});
};
this.insertIMG = function() {
updateState(function (state) {
return commands.convertSelectionToLink(state,
commands.LinkType.IMAGE_LINK);
});
};
this.insertList = function() {
updateState(function (state) {
return commands.toggleBlockStyle(state, '* ');
});
};
this.insertNumList = function() {
updateState(function (state) {
return commands.toggleBlockStyle(state, '1. ');
});
};
this.insertQuote = function() {
updateState(function (state) {
return commands.toggleBlockStyle(state, '> ');
});
};
// Keyboard shortcuts for bold, italic, and link.
$element.on('keydown', function(e) {
var shortcuts = {
66: self.insertBold,
73: self.insertItalic,
75: self.insertLink,
};
var shortcut = shortcuts[e.keyCode];
if (shortcut && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
shortcut();
}
});
this.preview = false;
this.togglePreview = function () {
self.preview = !self.preview;
};
var handleInputChange = debounce(function () {
$scope.$apply(function () {
self.onEditText({text: input.value});
});
}, 100);
input.addEventListener('input', handleInputChange);
// Re-render the markdown when the view needs updating.
$scope.$watch('vm.text', function () {
output.innerHTML = renderMarkdown(self.text || '', $sanitize);
mediaEmbedder.replaceLinksWithEmbeds(output);
});
this.showEditor = function () {
return !self.readOnly && !self.preview;
};
// Exit preview mode when leaving edit mode
$scope.$watch('vm.readOnly', function () {
self.preview = false;
});
$scope.$watch('vm.showEditor()', function (show) {
if (show) {
input.value = self.text || '';
focusInput();
}
});
}
/** /**
* @ngdoc directive
* @name markdown * @name markdown
* @restrict A
* @description * @description
* This directive controls both the rendering and display of markdown, as well as * This directive controls both the rendering and display of markdown, as well as
* the markdown editor. * the markdown editor.
*/ */
// @ngInject // @ngInject
module.exports = function($sanitize) { module.exports = {
return { controller: MarkdownController,
controller: function () {}, controllerAs: 'vm',
link: function(scope, elem) { bindings: {
var input = elem[0].querySelector('.js-markdown-input'); customTextClass: '<?',
var output = elem[0].querySelector('.js-markdown-preview'); readOnly: '<',
text: '<?',
/** onEditText: '&',
* Transform the editor's input field with an editor command. },
*/ template: require('../templates/markdown.html'),
function updateState(newStateFn) {
var newState = newStateFn({
text: input.value,
selectionStart: input.selectionStart,
selectionEnd: input.selectionEnd,
});
input.value = newState.text;
input.selectionStart = newState.selectionStart;
input.selectionEnd = newState.selectionEnd;
// The input field currently loses focus when the contents are
// changed. This re-focuses the input field but really it should
// happen automatically.
input.focus();
scope.onEditText({text: input.value});
}
function focusInput() {
// When the visibility of the editor changes, focus it.
// A timeout is used so that focus() is not called until
// the visibility change has been applied (by adding or removing
// the relevant CSS classes)
scopeTimeout(scope, function () {
input.focus();
}, 0);
}
scope.insertBold = function() {
updateState(function (state) {
return commands.toggleSpanStyle(state, '**', '**', 'Bold');
});
};
scope.insertItalic = function() {
updateState(function (state) {
return commands.toggleSpanStyle(state, '*', '*', 'Italic');
});
};
scope.insertMath = function() {
updateState(function (state) {
var before = state.text.slice(0, state.selectionStart);
if (before.length === 0 ||
before.slice(-1) === '\n' ||
before.slice(-2) === '$$') {
return commands.toggleSpanStyle(state, '$$', '$$', 'Insert LaTeX');
} else {
return commands.toggleSpanStyle(state, '\\(', '\\)',
'Insert LaTeX');
}
});
};
scope.insertLink = function() {
updateState(function (state) {
return commands.convertSelectionToLink(state);
});
};
scope.insertIMG = function() {
updateState(function (state) {
return commands.convertSelectionToLink(state,
commands.LinkType.IMAGE_LINK);
});
};
scope.insertList = function() {
updateState(function (state) {
return commands.toggleBlockStyle(state, '* ');
});
};
scope.insertNumList = function() {
updateState(function (state) {
return commands.toggleBlockStyle(state, '1. ');
});
};
scope.insertQuote = function() {
updateState(function (state) {
return commands.toggleBlockStyle(state, '> ');
});
};
// Keyboard shortcuts for bold, italic, and link.
elem.on('keydown', function(e) {
var shortcuts = {
66: scope.insertBold,
73: scope.insertItalic,
75: scope.insertLink,
};
var shortcut = shortcuts[e.keyCode];
if (shortcut && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
shortcut();
}
});
scope.preview = false;
scope.togglePreview = function () {
scope.preview = !scope.preview;
};
var handleInputChange = debounce(function () {
scope.$apply(function () {
scope.onEditText({text: input.value});
});
}, 100);
input.addEventListener('input', handleInputChange);
// Re-render the markdown when the view needs updating.
scope.$watch('text', function () {
output.innerHTML = renderMarkdown(scope.text || '', $sanitize);
mediaEmbedder.replaceLinksWithEmbeds(output);
});
scope.showEditor = function () {
return !scope.readOnly && !scope.preview;
};
// Exit preview mode when leaving edit mode
scope.$watch('readOnly', function () {
scope.preview = false;
});
scope.$watch('showEditor()', function (show) {
if (show) {
input.value = scope.text || '';
focusInput();
}
});
},
restrict: 'E',
scope: {
customTextClass: '<?',
readOnly: '<',
text: '<?',
onEditText: '&',
},
template: require('../templates/markdown.html'),
};
}; };
...@@ -41,7 +41,7 @@ describe('markdown', function () { ...@@ -41,7 +41,7 @@ describe('markdown', function () {
before(function () { before(function () {
angular.module('app', ['ngSanitize']) angular.module('app', ['ngSanitize'])
.directive('markdown', proxyquire('../markdown', noCallThru({ .component('markdown', proxyquire('../markdown', noCallThru({
angular: angular, angular: angular,
katex: { katex: {
renderToString: function (input) { renderToString: function (input) {
...@@ -217,7 +217,7 @@ describe('markdown', function () { ...@@ -217,7 +217,7 @@ describe('markdown', function () {
} }
function isPreviewing() { function isPreviewing() {
return editor.isolateScope().preview; return editor.ctrl.preview;
} }
beforeEach(function () { beforeEach(function () {
......
<div ng-if="!readOnly" class="markdown-tools" ng-class="preview && 'disable'"> <div ng-if="!vm.readOnly" class="markdown-tools" ng-class="vm.preview && 'disable'">
<span class="markdown-preview-toggle"> <span class="markdown-preview-toggle">
<a class="markdown-tools-badge h-icon-markdown" href="https://help.github.com/articles/markdown-basics" title="Parsed as Markdown" target="_blank"></a> <a class="markdown-tools-badge h-icon-markdown" href="https://help.github.com/articles/markdown-basics" title="Parsed as Markdown" target="_blank"></a>
<a href="" class="markdown-tools-toggle" ng-click="togglePreview()" ng-show="!preview">Preview</a> <a href="" class="markdown-tools-toggle" ng-click="vm.togglePreview()"
<a href="" class="markdown-tools-toggle" ng-click="togglePreview()" ng-show="preview">Write</a> ng-show="!vm.preview">Preview</a>
<a href="" class="markdown-tools-toggle" ng-click="vm.togglePreview()"
ng-show="vm.preview">Write</a>
</span> </span>
<i class="h-icon-format-bold markdown-tools-button" ng-click="insertBold()" title="Embolden text"></i> <i class="h-icon-format-bold markdown-tools-button" ng-click="vm.insertBold()" title="Embolden text"></i>
<i class="h-icon-format-italic markdown-tools-button" ng-click="insertItalic()" title="Italicize text"></i> <i class="h-icon-format-italic markdown-tools-button" ng-click="vm.insertItalic()" title="Italicize text"></i>
<i class="h-icon-format-quote markdown-tools-button" ng-click="insertQuote()" title="Quote text"></i> <i class="h-icon-format-quote markdown-tools-button" ng-click="vm.insertQuote()" title="Quote text"></i>
<i class="h-icon-insert-link markdown-tools-button" ng-click="insertLink()" title="Insert link"></i> <i class="h-icon-insert-link markdown-tools-button" ng-click="vm.insertLink()" title="Insert link"></i>
<i class="h-icon-insert-photo markdown-tools-button" ng-click="insertIMG()" title="Insert image"></i> <i class="h-icon-insert-photo markdown-tools-button" ng-click="vm.insertIMG()" title="Insert image"></i>
<i class="h-icon-functions markdown-tools-button" ng-click="insertMath()" title="Insert mathematical notation (LaTex is supported)"></i> <i class="h-icon-functions markdown-tools-button" ng-click="vm.insertMath()" title="Insert mathematical notation (LaTex is supported)"></i>
<i class="h-icon-format-list-numbered markdown-tools-button" ng-click="insertNumList()" title="Insert numbered list"></i> <i class="h-icon-format-list-numbered markdown-tools-button" ng-click="vm.insertNumList()" title="Insert numbered list"></i>
<i class="h-icon-format-list-bulleted markdown-tools-button" ng-click="insertList()" title="Insert list"></i> <i class="h-icon-format-list-bulleted markdown-tools-button" ng-click="vm.insertList()" title="Insert list"></i>
</div> </div>
<textarea class="form-input form-textarea js-markdown-input" <textarea class="form-input form-textarea js-markdown-input"
ng-show="showEditor()" ng-show="vm.showEditor()"
ng-click="$event.stopPropagation()" ng-click="$event.stopPropagation()"
h-branding="annotationFontFamily" h-branding="annotationFontFamily"></textarea>
></textarea>
<div class="markdown-body js-markdown-preview" <div class="markdown-body js-markdown-preview"
ng-class="(preview && 'markdown-preview') || customTextClass" ng-class="(vm.preview && 'markdown-preview') || vm.customTextClass"
ng-dblclick="togglePreview()" ng-dblclick="vm.togglePreview()"
ng-show="!showEditor()" ng-show="!vm.showEditor()"
h-branding="annotationFontFamily"></div> h-branding="annotationFontFamily"></div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment