Commit f19ab786 by Tim Krones

WYSIWYG functionality for Studio, Part 1: Add support for definining

vectors by drawing them.
parent 708b2cca
.vectordraw_edit_block {
border-top: 1px solid #e5e5e5;
margin-left: 20px;
margin-right: 20px;
padding-top: 20px;
}
.vectordraw_edit_block h2 {
margin-bottom: 1em;
}
.vectordraw_edit_block p {
margin-bottom: 1em;
font-size: 0.9em;
}
.vectordraw_edit_block #vectordraw {
margin-bottom: 1.5em;
}
.vectordraw_edit_block .jxgboard {
border: 2px solid #1f628d;
margin-bottom: 1em;
}
.vectordraw_edit_block .jxgboard .JXGtext {
pointer-events: none; /* prevents cursor from turning into caret when over a label */
}
/* Javascript for StudioEditableXBlockMixin. */
function StudioEditableXBlockMixin(runtime, element) {
"use strict";
var fields = [];
var tinyMceAvailable = (typeof $.fn.tinymce !== 'undefined'); // Studio includes a copy of tinyMCE and its jQuery plugin
$(element).find('.field-data-control').each(function() {
var $field = $(this);
var $wrapper = $field.closest('li');
var $resetButton = $wrapper.find('button.setting-clear');
var type = $wrapper.data('cast');
fields.push({
name: $wrapper.data('field-name'),
isSet: function() { return $wrapper.hasClass('is-set'); },
hasEditor: function() { return tinyMceAvailable && $field.tinymce(); },
val: function() {
var val = $field.val();
// Cast values to the appropriate type so that we send nice clean JSON over the wire:
if (type == 'boolean')
return (val == 'true' || val == '1');
if (type == "integer")
return parseInt(val, 10);
if (type == "float")
return parseFloat(val);
return val;
},
removeEditor: function() {
$field.tinymce().remove();
}
});
var fieldChanged = function() {
// Field value has been modified:
$wrapper.addClass('is-set');
$resetButton.removeClass('inactive').addClass('active');
};
$field.bind("change input paste", fieldChanged);
$resetButton.click(function() {
$field.val($wrapper.attr('data-default')); // Use attr instead of data to force treating the default value as a string
$wrapper.removeClass('is-set');
$resetButton.removeClass('active').addClass('inactive');
});
if (type == 'html' && tinyMceAvailable) {
tinyMCE.baseURL = baseUrl + "/js/vendor/tinymce/js/tinymce";
$field.tinymce({
theme: 'modern',
skin: 'studio-tmce4',
height: '200px',
formats: { code: { inline: 'code' } },
codemirror: { path: "" + baseUrl + "/js/vendor" },
convert_urls: false,
plugins: "link codemirror",
menubar: false,
statusbar: false,
toolbar_items_size: 'small',
toolbar: "formatselect | styleselect | bold italic underline forecolor wrapAsCode | bullist numlist outdent indent blockquote | link unlink | code",
resize: "both",
setup : function(ed) {
ed.on('change', fieldChanged);
}
});
}
});
var studio_submit = function(data) {
var handlerUrl = runtime.handlerUrl(element, 'submit_studio_edits');
runtime.notify('save', {state: 'start', message: gettext("Saving")});
$.ajax({
type: "POST",
url: handlerUrl,
data: JSON.stringify(data),
dataType: "json",
global: false, // Disable Studio's error handling that conflicts with studio's notify('save') and notify('cancel') :-/
success: function(response) { runtime.notify('save', {state: 'end'}); }
}).fail(function(jqXHR) {
var message = gettext("This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.");
if (jqXHR.responseText) { // Is there a more specific error message we can show?
try {
message = JSON.parse(jqXHR.responseText).error;
if (typeof message === "object" && message.messages) {
// e.g. {"error": {"messages": [{"text": "Unknown user 'bob'!", "type": "error"}, ...]}} etc.
message = $.map(message.messages, function(msg) { return msg.text; }).join(", ");
}
} catch (error) { message = jqXHR.responseText.substr(0, 300); }
}
runtime.notify('error', {title: gettext("Unable to update settings"), message: message});
});
};
return {
save: function(data) {
var values = {};
var notSet = []; // List of field names that should be set to default values
for (var i in fields) {
var field = fields[i];
if (field.isSet()) {
values[field.name] = field.val();
} else {
notSet.push(field.name);
}
// Remove TinyMCE instances to make sure jQuery does not try to access stale instances
// when loading editor for another block:
if (field.hasEditor()) {
field.removeEditor();
}
}
// If WYSIWYG editor was used, prefer its data over value of "vectors" field:
if (data.vectors) {
values.vectors = JSON.stringify(data.vectors, undefined, 4);
}
studio_submit({values: values, defaults: notSet});
},
cancel: function() {
// Remove TinyMCE instances to make sure jQuery does not try to access stale instances
// when loading editor for another block:
for (var i in fields) {
var field = fields[i];
if (field.hasEditor()) {
field.removeEditor();
}
}
runtime.notify('cancel', {});
}
};
}
......@@ -10,7 +10,7 @@ function VectorDrawXBlock(runtime, element, init_args) {
this.drawMode = false;
this.history_stack = {undo: [], redo: []};
this.settings = settings;
this.element = $('#' + element_id);
this.element = $('#' + element_id, element);
this.element.on('click', '.reset', this.reset.bind(this));
this.element.on('click', '.add-vector', this.addElementFromList.bind(this));
......@@ -142,20 +142,6 @@ function VectorDrawXBlock(runtime, element, init_args) {
return coords;
};
VectorDraw.prototype.getVectorStyle = function(vec) {
//width, color, size of control point, label (which should be a JSXGraph option)
var default_style = {
pointSize: 1,
pointColor: 'red',
width: 4,
color: "blue",
label: null,
labelColor: 'black'
};
return _.extend(default_style, vec.style);
};
VectorDraw.prototype.renderVector = function(idx, coords) {
var vec = this.settings.vectors[idx];
coords = coords || this.getVectorCoordinates(vec);
......
{% load i18n %}
<div class="editor-with-buttons">
<div class="wrapper-comp-settings is-active editor-with-buttons" id="settings-tab">
<ul class="list-input settings-list">
{% for field in fields %}
<li class="field comp-setting-entry metadata_entry {% if field.is_set %}is-set{% endif %}"
data-field-name="{{field.name}}"
data-default="{% if field.type == 'boolean' %}{{ field.default|yesno:'1,0' }}{% else %}{{ field.default|default_if_none:"" }}{% endif %}"
data-cast="{{field.type}}">
<div class="wrapper-comp-setting{% if field.type == "set" %} metadata-list-enum {%endif%}">
<label class="label setting-label" for="xb-field-edit-{{field.name}}">{{field.display_name}}</label>
{% if field.type == "boolean" %}
<select class="field-data-control"
id="xb-field-edit-{{field.name}}"
aria-describedby="{{field.name}}-help">
<option value="1" {% if field.value %}selected{% endif %}>
True {% if field.default %}&nbsp;&nbsp;&nbsp;&nbsp;(Default){% endif %}
</option>
<option value="0" {% if not field.value %}selected{% endif %}>
False {% if not field.default %}&nbsp;&nbsp;&nbsp;&nbsp;(Default){% endif %}
</option>
</select>
{% elif field.type == "string" %}
<input type="text"
class="field-data-control"
id="xb-field-edit-{{field.name}}"
value="{{field.value|default_if_none:""}}"
aria-describedby="{{field.name}}-help">
{% elif field.type == "integer" or field.type == "float" %}
<input type="number"
class="field-data-control"
id="xb-field-edit-{{field.name}}"
{% if field.step %} step="{{field.step}}" {% elif field.type == "integer" %} step=1 {% endif %}
{% if field.max %} max="{{field.max}}" {% endif %}
{% if field.min %} min="{{field.min}}" {% endif %}
value="{{field.value|default_if_none:""}}"
aria-describedby="{{field.name}}-help">
{% elif field.type == "text" or field.type == "html" %}
<textarea class="field-data-control"
data-field-name="{{field.name}}"
id="xb-field-edit-{{field.name}}"
aria-describedby="{{field.name}}-help"
rows=10 cols=70>{{field.value}}</textarea>
{% else %}
Unsupported field type. This setting cannot be edited.
{% endif %}
{% if field.allow_reset %}
<button class="action setting-clear {% if field.is_set %}active{%else%}inactive{% endif %}"
type="button"
name="setting-clear"
value="{% trans "Clear" %}"
data-tooltip="{% trans "Clear" %}">
<i class="icon fa fa-undo"></i><span class="sr">{% trans "Clear Value" %}</span>
</button>
{% endif %}
</div>
{% if field.help %}
<span id="{{field.name}}-help" class="tip setting-help"> {{ field.help }} </span>
{% endif %}
</li>
{% endfor %}
<li>
<!-- WYSIWYG editor -->
<div class="vectordraw_edit_block">
<h2 aria-describedby="wysiwyg-description">WYSIWYG Editor</h2>
<p id="wysiwyg-description">
{% blocktrans %}
Use the board below to define a set of working vectors for this exercise.
To add a vector, left-click the board where you want the vector to originate.
Keep holding down the left mouse button and drag your mouse pointer across the board
to achieve the desired length and angle for the vector.
To modify an existing vector, left-click it, hold down the left mouse button,
and move your mouse pointer across the board.
{% endblocktrans %}
</p>
<div id="vectordraw">
<div class="jxgboard"
style="width: {{ self.width }}px; height: {{ self.height }}px;"
tabindex="0">
</div>
</div>
</div>
</li>
</ul>
</div>
<div class="xblock-actions">
<ul>
<li class="action-item">
<a href="#" class="button action-primary save-button">{% trans "Save" %}</a>
</li>
<li class="action-item">
<a href="#" class="button cancel-button">{% trans "Cancel" %}</a>
</li>
</ul>
</div>
</div>
......@@ -138,7 +138,9 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
help=(
"List of vectors to use for the exercise. "
"You must specify it as an array of entries "
"where each entry represents an individual vector."
"where each entry represents an individual vector. "
"Note that edits to vectors made via the WYSIWYG editor below "
"take precedence over changes you make here when saving."
),
default="[]",
multiline_editor=True,
......@@ -384,6 +386,39 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
)
return fragment
def studio_view(self, context):
fragment = Fragment()
context = {'fields': [], 'self': self}
# Build a list of all the fields that can be edited:
for field_name in self.editable_fields:
field = self.fields[field_name]
assert field.scope in (Scope.content, Scope.settings), (
"Only Scope.content or Scope.settings fields can be used with "
"StudioEditableXBlockMixin. Other scopes are for user-specific data and are "
"not generally created/configured by content authors in Studio."
)
field_info = self._make_field_info(field_name, field)
if field_info is not None:
context["fields"].append(field_info)
fragment.add_content(loader.render_template("templates/html/vectordraw_edit.html", context))
# Add resources to studio_view fragment
fragment.add_css_url(
self.runtime.local_resource_url(self, 'public/css/vectordraw_edit.css')
)
fragment.add_javascript_url(
"//cdnjs.cloudflare.com/ajax/libs/jsxgraph/0.98/jsxgraphcore.js"
)
fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/studio_edit.js')
)
fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/vectordraw_edit.js')
)
fragment.initialize_js(
'VectorDrawXBlockEdit', {"settings": self.settings}
)
return fragment
def _validate_check_answer_data(self, data): # pylint: disable=no-self-use
"""
Validate answer data submitted by user.
......
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