Commit 108358bf by Jonathan Piacenti

Polished up backend. Markdown needs extraction. Frontend needs further CSS.

parent f56c62d4
"""TO-DO: Write a description of what this XBlock is.""" """TO-DO: Write a description of what this XBlock is."""
from collections import OrderedDict from collections import OrderedDict
from django.template import Template, Context from django.template import Template, Context
from bleach import clean
from markdown import markdown
import pkg_resources import pkg_resources
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Scope, List, Integer, String, Dict from xblock.fields import Scope, List, String, Dict, UserScope, BlockScope
from xblock.fragment import Fragment from xblock.fragment import Fragment
...@@ -21,12 +24,8 @@ class PollBlock(XBlock): ...@@ -21,12 +24,8 @@ class PollBlock(XBlock):
scope=Scope.settings, help="The questions on this poll." scope=Scope.settings, help="The questions on this poll."
) )
tally = Dict(default={'Red': 0, 'Blue': 0, 'Green': 0, 'Other': 0}, tally = Dict(default={'Red': 0, 'Blue': 0, 'Green': 0, 'Other': 0},
scope=Scope.user_state_summary, scope=Scope.content,
help="Total tally of answers from students.") help="Total tally of answers from students.")
# No default. Hopefully this will yield 'None', or do something
# distinctive when queried.
# Choices are always one above their place in the index so that choice
# is never false if it's provided.
choice = String(scope=Scope.user_state, help="The student's answer") choice = String(scope=Scope.user_state, help="The student's answer")
def resource_string(self, path): def resource_string(self, path):
...@@ -36,7 +35,7 @@ class PollBlock(XBlock): ...@@ -36,7 +35,7 @@ class PollBlock(XBlock):
@XBlock.json_handler @XBlock.json_handler
def get_results(self, data, suffix=''): def get_results(self, data, suffix=''):
return {'question': self.question, 'tally': self.tally_detail()} return {'question': markdown(clean(self.question)), 'tally': self.tally_detail()}
def tally_detail(self): def tally_detail(self):
""" """
...@@ -44,13 +43,15 @@ class PollBlock(XBlock): ...@@ -44,13 +43,15 @@ class PollBlock(XBlock):
""" """
tally = [] tally = []
answers = OrderedDict(self.answers) answers = OrderedDict(self.answers)
choice = self.get_choice()
total = 0 total = 0
for key, value in answers.items(): for key, value in answers.items():
tally.append({ tally.append({
'count': int(self.tally.get(key, 0)), 'count': int(self.tally.get(key, 0)),
'answer': value, 'answer': value,
'key': key, 'key': key,
'top': False 'top': False,
'choice': False
}) })
total += tally[-1]['count'] total += tally[-1]['count']
...@@ -58,6 +59,8 @@ class PollBlock(XBlock): ...@@ -58,6 +59,8 @@ class PollBlock(XBlock):
try: try:
percent = (answer['count'] / float(total)) percent = (answer['count'] / float(total))
answer['percent'] = int(percent * 100) answer['percent'] = int(percent * 100)
if answer['key'] == choice:
answer['choice'] = True
except ZeroDivisionError: except ZeroDivisionError:
answer['percent'] = 0 answer['percent'] = 0
...@@ -69,6 +72,17 @@ class PollBlock(XBlock): ...@@ -69,6 +72,17 @@ class PollBlock(XBlock):
tally[0]['top'] = True tally[0]['top'] = True
return tally return tally
def get_choice(self):
"""
It's possible for the choice to have been removed since
the student answered the poll. We don't want to take away
the user's progress, but they should be able to vote again.
"""
if self.choice and self.choice in OrderedDict(self.answers):
return self.choice
else:
return None
# TO-DO: change this view to display your data your own way. # TO-DO: change this view to display your data your own way.
def student_view(self, context=None): def student_view(self, context=None):
""" """
...@@ -81,11 +95,13 @@ class PollBlock(XBlock): ...@@ -81,11 +95,13 @@ class PollBlock(XBlock):
js_template = self.resource_string( js_template = self.resource_string(
'/public/handlebars/results.handlebars') '/public/handlebars/results.handlebars')
choice = self.get_choice()
context.update({ context.update({
'choice': self.choice, 'choice': choice,
# Offset so choices will always be True. # Offset so choices will always be True.
'answers': self.answers, 'answers': self.answers,
'question': self.question, 'question': markdown(clean(self.question)),
'js_template': js_template, 'js_template': js_template,
}) })
...@@ -116,7 +132,7 @@ class PollBlock(XBlock): ...@@ -116,7 +132,7 @@ class PollBlock(XBlock):
js_template = self.resource_string('/public/handlebars/studio.handlebars') js_template = self.resource_string('/public/handlebars/studio.handlebars')
context.update({ context.update({
'question': self.question, 'question': clean(self.question),
'js_template': js_template 'js_template': js_template
}) })
context = Context(context) context = Context(context)
...@@ -126,6 +142,15 @@ class PollBlock(XBlock): ...@@ -126,6 +142,15 @@ class PollBlock(XBlock):
frag.add_javascript_url( frag.add_javascript_url(
self.runtime.local_resource_url( self.runtime.local_resource_url(
self, 'public/js/vendor/handlebars.js')) self, 'public/js/vendor/handlebars.js'))
frag.add_javascript_url(
self.runtime.local_resource_url(
self, 'public/js/vendor/pen.js'))
frag.add_javascript_url(
self.runtime.local_resource_url(
self, 'public/js/vendor/markdown.js'))
frag.add_css_url(
self.runtime.local_resource_url(
self, 'public/css/vendor/pen.css'))
frag.add_css(self.resource_string('/public/css/poll_edit.css')) frag.add_css(self.resource_string('/public/css/poll_edit.css'))
frag.add_javascript(self.resource_string("public/js/poll_edit.js")) frag.add_javascript(self.resource_string("public/js/poll_edit.js"))
frag.initialize_js('PollEditBlock') frag.initialize_js('PollEditBlock')
...@@ -135,31 +160,49 @@ class PollBlock(XBlock): ...@@ -135,31 +160,49 @@ class PollBlock(XBlock):
@XBlock.json_handler @XBlock.json_handler
def studio_submit(self, data, suffix=''): def studio_submit(self, data, suffix=''):
# I wonder if there's something for live validation feedback already. # I wonder if there's something for live validation feedback already.
result = {'success': True, 'errors': {'fields': {}, 'general': []}} result = {'success': True, 'errors': []}
if 'question' not in data or not data['question']: if 'question' not in data or not data['question']:
result['errors']['question'] = "This field is required." result['errors'].append("You must specify a question.")
result['success'] = False result['success'] = False
# Need this meta information, otherwise the questions will be
# shuffled by Python's dictionary data type.
poll_order = [key.strip().replace('answer-', '')
for key in data.get('poll_order', [])
]
# Aggressively clean/sanity check answers list. # Aggressively clean/sanity check answers list.
answers = OrderedDict( answers = []
(key.replace('answer-', '', 1), value.strip()[:250]) for key, value in data.items():
for key, value in data.items() if not key.startswith('answer-'):
if (key.startswith('answer-') and not key == 'answer-') continue
and not value.isspace() key = key.replace('answer-', '')
) if not key or key.isspace():
continue
value = value.strip()[:250]
if not value or value.isspace():
continue
if key in poll_order:
answers.append((key, value))
if not len(answers) > 1: if not len(answers) > 1:
result['errors']['general'].append( result['errors'].append(
"You must include at least two answers.") "You must include at least two answers.")
result['success'] = False result['success'] = False
if not result['success']: if not result['success']:
return result return result
self.answers = answers.items() # Need to sort the answers.
answers.sort(key=lambda x: poll_order.index(x[0]), reverse=True)
self.answers = answers
self.question = data['question'] self.question = data['question']
print answers
tally = self.tally tally = self.tally
answers = OrderedDict(answers)
# Update tracking schema. # Update tracking schema.
for key, value in answers.items(): for key, value in answers.items():
if key not in tally: if key not in tally:
...@@ -176,24 +219,28 @@ class PollBlock(XBlock): ...@@ -176,24 +219,28 @@ class PollBlock(XBlock):
""" """
An example handler, which increments the data. An example handler, which increments the data.
""" """
result = {'success': False} result = {'success': False, 'errors': []}
if self.choice is not None: if self.get_choice() is not None:
result['message'] = 'You cannot vote twice.' result['errors'].append('You have already voted in this poll.')
return result return result
try: try:
choice = data['choice'] choice = data['choice']
except KeyError: except KeyError:
result['message'] = 'Answer not included with request.' result['errors'].append('Answer not included with request.')
return result return result
# Just to show data coming in... # Just to show data coming in...
try: try:
OrderedDict(self.answers)[choice] OrderedDict(self.answers)[choice]
except KeyError: except KeyError:
result['message'] = 'No key "{choice}" in answers table.'.format(choice=choice) result['errors'].append('No key "{choice}" in answers table.'.format(choice=choice))
return result return result
self.choice = choice self.choice = choice
self.tally[choice] += 1 tally = self.tally.copy()
tally[choice] = self.tally.get(choice, 0)
self.tally = tally
# Let the LMS know the user has answered the poll.
self.runtime.publish(self, 'progress', {})
result['success'] = True result['success'] = True
return result return result
......
.poll-answer {
margin-left: 1em;
font-weight: bold;
}
\ No newline at end of file
...@@ -2,4 +2,25 @@ ...@@ -2,4 +2,25 @@
.poll-delete-answer { .poll-delete-answer {
float: right; float: right;
}
#question-editor-container{
width: 100%;
text-align: center;
}
#question-editor{
width: 98%;
height: 7em;
text-align: left;
color: #4C4C4C;
margin-top: 0.5em;
margin-bottom: 0.5em;
box-shadow: 0px 0px 9px #555 inset;
border: 1px solid #B2B2B2;
border-radius: 3px;
padding: 10px;
}
h2 label {
font-weight: bold;
font-size: 16pt;
} }
\ No newline at end of file
/*! Licensed under MIT, https://github.com/sofish/pen */
/* basic reset */
.pen, .pen-menu, .pen-input, .pen textarea{font:400 1.16em/1.45 Palatino, Optima, Georgia, serif;color:#331;}
.pen:focus{outline:none;}
.pen fieldset, img {border: 0;}
.pen blockquote{padding-left:10px;margin-left:-14px;border-left:4px solid #1abf89;}
.pen a{color:#1abf89;}
.pen del{text-decoration:line-through;}
.pen sub, .pen sup {font-size:75%;position:relative;vertical-align:text-top;}
:root .pen sub, :root .pen sup{vertical-align:baseline; /* for ie9 and other mordern browsers */}
.pen sup {top:-0.5em;}
.pen sub {bottom:-0.25em;}
.pen hr{border:none;border-bottom:1px solid #cfcfcf;margin-bottom:25px;*color:pink;*filter:chroma(color=pink);height:10px;*margin:-7px 0 15px;}
.pen small{font-size:0.8em;color:#888;}
.pen em, .pen b, .pen strong{font-weight:700;}
.pen pre{white-space:pre-wrap;padding:0.85em;background:#f8f8f8;}
/* block-level element margin */
.pen p, .pen pre, .pen ul, .pen ol, .pen dl, .pen form, .pen table, .pen blockquote{margin-bottom:16px;}
/* headers */
.pen h1, .pen h2, .pen h3, .pen h4, .pen h5, .pen h6{margin-bottom:16px;font-weight:700;line-height:1.2;}
.pen h1{font-size:2em;}
.pen h2{font-size:1.8em;}
.pen h3{font-size:1.6em;}
.pen h4{font-size:1.4em;}
.pen h5, .pen h6{font-size:1.2em;}
/* list */
.pen ul, .pen ol{margin-left:1.2em;}
.pen ul, .pen-ul{list-style:disc;}
.pen ol, .pen-ol{list-style:decimal;}
.pen li ul, .pen li ol, .pen-ul ul, .pen-ul ol, .pen-ol ul, .pen-ol ol{margin:0 2em 0 1.2em;}
.pen li ul, .pen-ul ul, .pen-ol ul{list-style: circle;}
/* pen menu */
.pen-menu [class^="icon-"], .pen-menu [class*=" icon-"] { /* reset to avoid conflicts with Bootstrap */
background: transparent;
background-image: none;
}
.pen-menu, .pen-input{font-size:14px;line-height:1;}
.pen-menu{white-space:nowrap;box-shadow:1px 2px 3px -2px #222;background:#333;background-image:linear-gradient(to bottom, #222, #333);opacity:0.9;position:fixed;height:36px;border:1px solid #333;border-radius:3px;display:none;z-index:1000;}
.pen-menu:after {top:100%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none;}
.pen-menu:after {border-color:rgba(51, 51, 51, 0);border-top-color:#333;border-width:6px;left:50%;margin-left:-6px;}
.pen-menu-below:after {top: -11px; display:block; -moz-transform: rotate(180deg); -webkit-transform: rotate(180deg); -ms-transform: rotate(180deg); -o-transform: rotate(180deg); transform: rotate(180deg);}
.pen-icon{font:normal 900 16px/40px Georgia serif;min-width:20px;display:inline-block;padding:0 10px;height:36px;overflow:hidden;color:#fff;text-align:center;cursor:pointer;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;}
.pen-icon:first-of-type{border-top-left-radius:3px;border-bottom-left-radius:3px;}
.pen-icon:last-of-type{border-top-right-radius:3px;border-bottom-right-radius:3px;}
.pen-icon:hover{background:#000;}
.pen-icon.active{color:#1abf89;background:#000;box-shadow:inset 2px 2px 4px #000;}
.pen-input{position:absolute;width:100%;left:0;top:0;height:36px;line-height:20px;background:#333;color:#fff;border:none;text-align:center;display:none;font-family:arial, sans-serif;}
.pen-input:focus{outline:none;}
.pen-textarea{display:block;background:#f8f8f8;padding:20px;}
.pen textarea{font-size:14px;border:none;background:none;width:100%;_height:200px;min-height:200px;resize:none;}
@font-face {
font-family: 'pen';
src: url('font/fontello.eot?370dad08');
src: url('font/fontello.eot?370dad08#iefix') format('embedded-opentype'),
url('font/fontello.woff?370dad08') format('woff'),
url('font/fontello.ttf?370dad08') format('truetype'),
url('font/fontello.svg?370dad08#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
.pen-menu [class^="icon-"]:before, .pen-menu [class*=" icon-"]:before {
font-family: "pen";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
font-variant: normal;
text-transform: none;
line-height: 1em;
margin-left: .2em;
}
.pen-menu .icon-location:before { content: '\e815'; } /* '' */
.pen-menu .icon-fit:before { content: '\e80f'; } /* '' */
.pen-menu .icon-bold:before { content: '\e805'; } /* '' */
.pen-menu .icon-italic:before { content: '\e806'; } /* '' */
.pen-menu .icon-justifyleft:before { content: '\e80a'; } /* '' */
.pen-menu .icon-justifycenter:before { content: '\e80b'; } /* '' */
.pen-menu .icon-justifyright:before { content: '\e80c'; } /* '' */
.pen-menu .icon-justifyfull:before { content: '\e80d'; } /* '' */
.pen-menu .icon-outdent:before { content: '\e800'; } /* '' */
.pen-menu .icon-indent:before { content: '\e801'; } /* '' */
.pen-menu .icon-mode:before { content: '\e813'; } /* '' */
.pen-menu .icon-fullscreen:before { content: '\e80e'; } /* '' */
.pen-menu .icon-insertunorderedlist:before { content: '\e802'; } /* '' */
.pen-menu .icon-insertorderedlist:before { content: '\e803'; } /* '' */
.pen-menu .icon-strikethrough:before { content: '\e807'; } /* '' */
.pen-menu .icon-underline:before { content: '\e804'; } /* '' */
.pen-menu .icon-blockquote:before { content: '\e814'; } /* '' */
.pen-menu .icon-undo:before { content: '\e817'; } /* '' */
.pen-menu .icon-code:before { content: '\e816'; } /* '' */
.pen-menu .icon-pre:before { content: '\e816'; } /* '' */
.pen-menu .icon-unlink:before { content: '\e811'; } /* '' */
.pen-menu .icon-superscript:before { content: '\e808'; } /* '' */
.pen-menu .icon-subscript:before { content: '\e809'; } /* '' */
.pen-menu .icon-inserthorizontalrule:before { content: '\e818'; } /* '' */
.pen-menu .icon-pin:before { content: '\e812'; } /* '' */
.pen-menu .icon-createlink:before { content: '\e810'; } /* '' */
.pen-menu .icon-h1:before { content: 'H1'; }
.pen-menu .icon-h2:before { content: 'H2'; }
.pen-menu .icon-h3:before { content: 'H3'; }
.pen-menu .icon-h4:before { content: 'H4'; }
.pen-menu .icon-h5:before { content: 'H5'; }
.pen-menu .icon-h6:before { content: 'H6'; }
.pen-menu .icon-p:before { content: 'P'; }
.pen {
position: relative;
}
.pen.hinted h1:before,
.pen.hinted h2:before,
.pen.hinted h3:before,
.pen.hinted h4:before,
.pen.hinted h5:before,
.pen.hinted h6:before,
.pen.hinted blockquote:before,
.pen.hinted hr:before {
color: #eee;
position: absolute;
right: 100%;
white-space: nowrap;
padding-right: 10px;
}
.pen.hinted blockquote { border-left: 0; margin-left: 0; padding-left: 0; }
.pen.hinted blockquote:before {
color: #1abf89;
content: ">";
font-weight: bold;
vertical-align: center;
}
.pen.hinted h1:before { content: "#";}
.pen.hinted h2:before { content: "##";}
.pen.hinted h3:before { content: "###";}
.pen.hinted h4:before { content: "####";}
.pen.hinted h5:before { content: "#####";}
.pen.hinted h6:before { content: "######";}
.pen.hinted hr:before { content: "﹘﹘﹘"; line-height: 1.2; vertical-align: bottom; }
.pen.hinted pre:before, .pen.hinted pre:after {
content: "```";
display: block;
color: #ccc;
}
.pen.hinted ul { list-style: none; }
.pen.hinted ul li:before {
content: "*";
color: #999;
line-height: 1;
vertical-align: bottom;
margin-left: -1.2em;
display: inline-block;
width: 1.2em;
}
.pen.hinted b:before, .pen.hinted b:after { content: "**"; color: #eee; font-weight: normal; }
.pen.hinted i:before, .pen.hinted i:after { content: "*"; color: #eee; }
.pen.hinted a { text-decoration: none; }
.pen.hinted a:before {content: "["; color: #ddd; }
.pen.hinted a:after { content: "](" attr(href) ")"; color: #ddd; }
.pen-placeholder { color: #999; }
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
<ul> <ul>
{{#each tally}} {{#each tally}}
<li class="poll-result"> <li class="poll-result">
<input id="answer-{{key}}" type="radio" disabled {{#if choice}}checked="True"{{/if}} />
<span class="percentage-guage" style="width:{{percentage}}%;"></span> <span class="percentage-guage" style="width:{{percentage}}%;"></span>
<span>{{answer}}</span> <label for="answer-{{key}}">{{answer}}</label>
<span class="poll-percent-display{{#if top}} poll-top-choice{{/if}}">{{percent}}%</span> <span class="poll-percent-display{{#if top}} poll-top-choice{{/if}}">{{percent}}%</span>
</li> </li>
{{/each}} {{/each}}
......
...@@ -3,16 +3,17 @@ ...@@ -3,16 +3,17 @@
{# If no form is present, the Javascript will load the results instead. #} {# If no form is present, the Javascript will load the results instead. #}
{% if not choice %} {% if not choice %}
<form> <form>
<h2>{{ question }}</h2> <h2>Poll</h2>
{{question|safe}}
<ul> <ul>
{% for key, answer in answers %} {% for key, answer in answers %}
<li class="poll-answer"> <li class="poll-answer">
<label for="answer-{{number}}">{{answer}}</label>
<input type="radio" name="choice" id="answer-{{number}}" value="{{key}}"> <input type="radio" name="choice" id="answer-{{number}}" value="{{key}}">
<label class="poll-answer" for="answer-{{number}}">{{answer}}</label>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<input type="submit" name="poll-submit" onclick="return false;" disabled> <input type="submit" name="poll-submit" onclick="return false;" value="Submit" disabled>
</form> </form>
{% endif %} {% endif %}
</div> </div>
\ No newline at end of file
...@@ -2,12 +2,21 @@ ...@@ -2,12 +2,21 @@
<div class="wrapper-comp-settings is-active editor-with-buttons" id="settings-tab"> <div class="wrapper-comp-settings is-active editor-with-buttons" id="settings-tab">
<form id="poll-form"> <form id="poll-form">
<ul class="list-input settings-list" id="poll-line-items"> <ul class="list-input settings-list" id="poll-line-items">
<h2><label for="question-editor">Question/Prompt</label></h2>
Select text to bring up formatting options.
<li class="field comp-setting-entry is-set"> <li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting"> <div id="question-editor-container">
<label class="label setting-label" for="question">Question</label> <div class="input setting-input" name="question" id="question-editor">{{question|safe}}</div>
<input class="input setting-input" name="question" id="question" value="{{question}}" type="text" /> </div>
</div> </li>
<span class="tip setting-help">Example: 'What is your favorite color?'</span> <li class="field comp-setting-entry is-set">
<p>
<strong>Notes:</strong>
If you change an answer's text, all students who voted for that choice will have their votes updated to
the new text. You'll want to avoid changing an answer from something like 'True' to 'False', accordingly.
If you delete an answer, any votes for that answer will also be deleted. Students whose choices are deleted
may vote again, but will not lose course progress.
</p>
</li> </li>
</ul> </ul>
<div class="xblock-actions"> <div class="xblock-actions">
......
...@@ -7,6 +7,9 @@ function PollBlock(runtime, element) { ...@@ -7,6 +7,9 @@ function PollBlock(runtime, element) {
var submit = $('input[type=submit]', element); var submit = $('input[type=submit]', element);
var resultsTemplate = Handlebars.compile($("#results", element).html()); var resultsTemplate = Handlebars.compile($("#results", element).html());
function getResults(data) { function getResults(data) {
if (! data['success']) {
alert(data['errors'].join('\n'));
}
$.ajax({ $.ajax({
// Semantically, this would be better as GET, but I can use helper // Semantically, this would be better as GET, but I can use helper
// functions with POST. // functions with POST.
...@@ -44,7 +47,7 @@ function PollBlock(runtime, element) { ...@@ -44,7 +47,7 @@ function PollBlock(runtime, element) {
enableSubmit(); enableSubmit();
} }
} else { } else {
getResults(); getResults({'success': true});
} }
$(function ($) { $(function ($) {
......
/* Javascript for ReferenceBlock. */
function PollEditBlock(runtime, element) { function PollEditBlock(runtime, element) {
var loadAnswers = runtime.handlerUrl(element, 'load_answers'); var loadAnswers = runtime.handlerUrl(element, 'load_answers');
var temp = $('#answer-form-component', element).html(); var temp = $('#answer-form-component', element).html();
...@@ -40,14 +39,26 @@ function PollEditBlock(runtime, element) { ...@@ -40,14 +39,26 @@ function PollEditBlock(runtime, element) {
$(element).find('.save-button').bind('click', function() { $(element).find('.save-button').bind('click', function() {
var handlerUrl = runtime.handlerUrl(element, 'studio_submit'); var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
var data = {}; var data = {};
$('#poll-form input', element).each(function() { var poll_order = []
$('#poll-form input', element).each(function(i) {
data[this.name] = this.value; data[this.name] = this.value;
if (this.name.indexOf('answer-') >= 0){
poll_order.push(this.name);
}
}); });
data['poll_order'] = poll_order;
function check_return(data) {
if (data['success']) {
window.location.reload(false);
return;
}
alert(data['errors'].join('\n'));
}
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: handlerUrl, url: handlerUrl,
data: JSON.stringify(data), data: JSON.stringify(data),
success: function () {} success: check_return
}); });
}); });
...@@ -58,5 +69,7 @@ function PollEditBlock(runtime, element) { ...@@ -58,5 +69,7 @@ function PollEditBlock(runtime, element) {
data: JSON.stringify({}), data: JSON.stringify({}),
success: displayAnswers success: displayAnswers
}); });
var pen = new Pen("#question-editor");
$.focus(pen)
}); });
} }
\ No newline at end of file
/*! Licensed under MIT, https://github.com/sofish/pen */
(function(root) {
// only works with Pen
if(!root.Pen) return;
// markdown covertor obj
var covertor = {
keymap: { '96': '`', '62': '>', '49': '1', '46': '.', '45': '-', '42': '*', '35': '#'},
stack : []
};
// return valid markdown syntax
covertor.valid = function(str) {
var len = str.length;
if(str.match(/[#]{1,6}/)) {
return ['h' + len, len];
} else if(str === '```') {
return ['pre', len];
} else if(str === '>') {
return ['blockquote', len];
} else if(str === '1.') {
return ['insertorderedlist', len];
} else if(str === '-' || str === '*') {
return ['insertunorderedlist', len];
} else if(str.match(/(?:\.|\*|\-){3,}/)) {
return ['inserthorizontalrule', len];
}
};
// parse command
covertor.parse = function(e) {
var code = e.keyCode || e.which;
// when `space` is pressed
if(code === 32) {
var cmd = this.stack.join('');
this.stack.length = 0;
return this.valid(cmd);
}
// make cmd
if(this.keymap[code]) this.stack.push(this.keymap[code]);
return false;
};
// exec command
covertor.action = function(pen, cmd) {
// only apply effect at line start
if(pen.selection.focusOffset > cmd[1]) return;
var node = pen.selection.focusNode;
node.textContent = node.textContent.slice(cmd[1]);
pen.execCommand(cmd[0]);
};
// init covertor
covertor.init = function(pen) {
pen.on('keypress', function(e) {
var cmd = covertor.parse(e);
if(cmd) return covertor.action(pen, cmd);
});
};
// append to Pen
root.Pen.prototype.markdown = covertor;
}(window));
-e . -e .
bleach
\ No newline at end of file
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