Commit 4abf518a authored by Aron Carroll's avatar Aron Carroll

Merge pull request #1479 from hypothesis/markdown

Introduce zero-dependency Markdown Editor.
parents fa8e94ba dbd633b6
......@@ -63,37 +63,6 @@ formValidate = ->
ctrl.submit()
markdown = ['$filter', '$timeout', ($filter, $timeout) ->
link: (scope, elem, attr, ctrl) ->
return unless ctrl?
input = elem.find('textarea')
output = elem.find('div')
# Re-render the markdown when the view needs updating.
ctrl.$render = ->
input.val (ctrl.$viewValue or '')
scope.rendered = ($filter 'converter') (ctrl.$viewValue or '')
# React to the changes to the text area
input.bind 'blur change keyup', ->
ctrl.$setViewValue input.val()
scope.$digest()
# Auto-focus the input box when the widget becomes editable.
# Re-render when it becomes uneditable.
scope.$watch 'readonly', (readonly) ->
ctrl.$render()
unless readonly then $timeout -> input.focus()
require: '?ngModel'
scope:
readonly: '@'
required: '@'
templateUrl: 'markdown.html'
]
privacy = ->
levels = ['Public', 'Only Me']
......@@ -246,7 +215,6 @@ match = ->
angular.module('h.directives', imports)
.directive('formInput', formInput)
.directive('formValidate', formValidate)
.directive('markdown', markdown)
.directive('privacy', privacy)
.directive('tabReveal', tabReveal)
.directive('showAccount', showAccount)
......
###*
# @ngdoc directive
# @name markdown
# @restrict A
# @description
# This directive controls both the rendering and display of markdown, as well as
# the markdown editor.
###
markdown = ['$filter', '$timeout', ($filter, $timeout) ->
link: (scope, elem, attr, ctrl) ->
return unless ctrl?
inputEl = elem.find('.js-markdown-input')
input = elem.find('.js-markdown-input')[0]
output = elem.find('.js-markdown-preview')[0]
userSelection = ->
if input.selectionStart != undefined
startPos = input.selectionStart
endPos = input.selectionEnd
selectedText = input.value.substring(startPos, endPos)
textBefore = input.value.substring(0, (startPos))
textAfter = input.value.substring(endPos)
selection = {
before: textBefore
after: textAfter
selection: selectedText
start: startPos
end: endPos
}
return selection
insertMarkup = (value, selectionStart, selectionEnd) ->
# New value is set for the input
input.value = value
# A new selection is set, or the cursor is positioned inside the input.
input.selectionStart = selectionStart
input.selectionEnd = selectionEnd
# Focus the input
input.focus()
applyInlineMarkup = (markup, innertext)->
text = userSelection()
if text.selection == ""
newtext = text.before + markup + innertext + markup + text.after
start = (text.before + markup).length
end = (text.before + innertext + markup).length
insertMarkup(newtext, start, end)
else
# Check to see if markup has already been applied before to the selection.
slice1 = text.before.slice(text.before.length - markup.length)
slice2 = text.after.slice(0, markup.length)
if slice1 == markup and slice2 == markup
# Remove markup
newtext = (
text.before.slice(0, (text.before.length - markup.length)) +
text.selection + text.after.slice(markup.length)
)
start = text.before.length - markup.length
end = (text.before + text.selection).length - markup.length
insertMarkup(newtext, start, end)
else
# Apply markup
newtext = text.before + markup + text.selection + markup + text.after
start = (text.before + markup).length
end = (text.before + text.selection + markup).length
insertMarkup(newtext, start, end)
scope.insertBold = ->
applyInlineMarkup("**", "Bold")
scope.insertItalic = ->
applyInlineMarkup("*", "Italic")
scope.insertMath = ->
applyInlineMarkup("$$", "LaTex")
scope.insertLink = ->
text = userSelection()
if text.selection == ""
newtext = text.before + "[Link Text](https://example.com)" + text.after
start = text.before.length + 1
end = text.before.length + 10
insertMarkup(newtext, start, end)
else
# Check to see if markup has already been applied to avoid double presses.
if text.selection == "Link Text" or text.selection == "https://example.com"
return
newtext = text.before + '[' + text.selection + '](https://example.com)' + text.after
start = (text.before + text.selection).length + 3
end = (text.before + text.selection).length + 22
insertMarkup(newtext, start, end)
scope.insertIMG = ->
text = userSelection()
if text.selection == ""
newtext = text.before + "![Image Description](https://yourimage.jpg)" + text.after
start = text.before.length + 21
end = text.before.length + 42
insertMarkup(newtext, start, end)
else
# Check to see if markup has already been applied to avoid double presses.
if text.selection == "https://yourimage.jpg"
return
newtext = text.before + '![' + text.selection + '](https://yourimage.jpg)' + text.after
start = (text.before + text.selection).length + 4
end = (text.before + text.selection).length + 25
insertMarkup(newtext, start, end)
scope.applyBlockMarkup = (markup) ->
text = userSelection()
if text.selection != ""
newstring = ""
index = text.before.length
if index == 0
# The selection takes place at the very start of the input
for char in text.selection
if char == "\n"
newstring = newstring + "\n" + markup
else if index == 0
newstring = newstring + markup + char
else
newstring = newstring + char
index += 1
else
newlinedetected = false
if input.value.substring(index - 1).charAt(0) == "\n"
# Look to see if the selection falls at the beginning of a new line.
newstring = newstring + markup
newlinedetected = true
for char in text.selection
if char == "\n"
newstring = newstring + "\n" + markup
newlinedetected = true
else
newstring = newstring + char
index += 1
if not newlinedetected
# Edge case: The selection does not include any new lines and does not start at 0.
# We need to find the newline before the currently selected text and add markup there.
i = 0
indexoflastnewline = undefined
newstring = ""
for char in (text.before + text.selection)
if char == "\n"
indexoflastnewline = i
newstring = newstring + char
i++
if indexoflastnewline == undefined
# The partial selection happens to fall on the firstline
newstring = markup + newstring
else
newstring = (
newstring.substring(0, (indexoflastnewline + 1)) +
markup + newstring.substring(indexoflastnewline + 1)
)
value = newstring + text.after
start = (text.before + markup).length
end = (text.before + text.selection + markup).length
insertMarkup(value, start, end)
return
# Sets input value and selection for cases where there are new lines in the selection
# or the selection is at the start
value = text.before + newstring + text.after
start = (text.before + newstring).length
end = (text.before + newstring).length
insertMarkup(value, start, end)
else if input.value.substring((text.start - 1 ), text.start) == "\n"
# Edge case, no selection, the cursor is on a new line.
value = text.before + markup + text.selection + text.after
start = (text.before + markup).length
end = (text.before + markup).length
insertMarkup(value, start, end)
else
# No selection, cursor is not on new line.
# Check to see if markup has already been inserted.
if text.before.slice(text.before.length - markup.length) == markup
newtext = (
text.before.substring(0, (index)) + "\n" +
text.before.substring(index + 1 + markup.length) + text.after
)
i = 0
for char in text.before
if char == "\n" and i != 0
index = i
i += 1
if !index # If the line of text happens to fall on the first line and index is not set.
# Check to see if markup has already been inserted and undo it.
if text.before.slice(0, markup.length) == markup
newtext = text.before.substring(markup.length) + text.after
start = text.before.length - markup.length
end = text.before.length - markup.length
insertMarkup(newtext, start, end)
else
newtext = markup + text.before.substring(0) + text.after
start = (text.before + markup).length
end = (text.before + markup).length
insertMarkup(newtext, start, end)
# Check to see if markup has already been inserted and undo it.
else if text.before.slice((index + 1), (index + 1 + markup.length)) == markup
newtext = (
text.before.substring(0, (index)) + "\n" +
text.before.substring(index + 1 + markup.length) + text.after
)
start = text.before.length - markup.length
end = text.before.length - markup.length
insertMarkup(newtext, start, end)
else
newtext = (
text.before.substring(0, (index)) + "\n" +
markup + text.before.substring(index + 1) + text.after
)
start = (text.before + markup).length
end = (text.before + markup).length
insertMarkup(newtext, start, end)
scope.insertList = ->
scope.applyBlockMarkup("* ")
scope.insertNumList = ->
scope.applyBlockMarkup("1. ")
scope.insertQuote = ->
scope.applyBlockMarkup("> ")
scope.insertCode = ->
scope.applyBlockMarkup(" ")
# Keyboard shortcuts for bold, italic, and link.
elem.on
keydown: (e) ->
shortcuts =
66: scope.insertBold
73: scope.insertItalic
75: scope.insertLink
shortcut = shortcuts[e.keyCode]
if shortcut && (e.ctrlKey || e.metaKey)
e.preventDefault()
shortcut()
scope.preview = false
scope.togglePreview = ->
if !scope.readonly
scope.preview = !scope.preview
if scope.preview
output.style.height = input.style.height
ctrl.$render()
else
input.style.height = output.style.height
$timeout -> inputEl.focus()
# Re-render the markdown when the view needs updating.
ctrl.$render = ->
inputEl.val (ctrl.$viewValue or '')
scope.rendered = ($filter 'converter') (ctrl.$viewValue or '')
# React to the changes to the input
inputEl.bind 'blur change keyup', ->
ctrl.$setViewValue inputEl.val()
scope.$digest()
# Reset height of output div incase it has been changed.
# Re-render when it becomes uneditable.
# Auto-focus the input box when the widget becomes editable.
scope.$watch 'readonly', (readonly) ->
scope.preview = false
output.style.height = ""
ctrl.$render()
unless readonly then $timeout -> inputEl.focus()
require: '?ngModel'
restrict: 'A'
scope:
readonly: '@'
required: '@'
templateUrl: 'markdown.html'
]
angular.module('h.directives').directive('markdown', markdown)
......@@ -42,12 +42,15 @@
font-size: .923em;
}
.icon-markdown {
color: $text-color;
line-height: 1.4;
margin-left: .5em;
//PRIVACY CONTROL////////////////////////////
privacy {
position: relative;
top: 2px;
}
//MAGICONTROL////////////////////////////////
.magicontrol {
margin-right: .8em;
color: $gray-lighter;
......
......@@ -8,6 +8,7 @@ $headings-color: $text-color;
@import 'annotations';
@import 'forms';
@import 'markdown-editor';
@import 'spinner';
@import 'responsive';
@import 'threads';
......
@import 'compass/css3/user-interface';
//MARKDOWN EDITOR //////////////////////////
.markdown-preview {
overflow: auto;
border: .1em solid $gray-lighter;
background-color: $gray-lightest;
min-height: 120px;
padding-left: 0.9em;
resize: vertical;
}
.markdown-tools {
border-top: .1em solid #D3D3D3;
border-left: .1em solid #D3D3D3;
border-right: .1em solid #D3D3D3;
border-radius: .15em .15em 0 0;
width: 100%;
margin-bottom: -.1em;
padding: .7em .7em .7em .5em;
@include user-select(none);
&.disable {
.markdown-tools-button {
color: $gray-lighter;
pointer-events: none;
}
}
.markdown-tools-button {padding: .4em;}
.markdown-tools-button, .markdown-tools-toggle, .icon-markdown {
color: $gray;
&:hover {
color: black;
}
}
.markdown-preview-toggle {
float: right;
}
}
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