Commit 17334d9b by Matjaz Gregoric Committed by Tim Krones

Avoid using ID attributes in preference to classes.

ID attributes have to be unique on the document level. To avoid
accidentaly having multiple elements with the same ID, we prefer to use
class attributes.

There are two situations where we still need an ID attribute:

- Connecting <label> elements to the associated input element using the
  'for' attribute
- Connecting description of an element with the 'aria-describedby'
  attribute.

In the first case, we can often wrap the associated input inside the
<label> tag, so that we don't need IDs, although in some cases that is
not possible because it breaks the layout.

In cases when we still need to use ID attributes, we append a random
string to ensure uniqueness.
parent 142b49ce
# -*- coding: utf-8 -*-
#
""" Drag and Drop v2 XBlock """
# Imports ###########################################################
import copy
import json
import urllib
import uuid
import webob
from xblock.core import XBlock
......@@ -247,6 +249,12 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"""
js_templates = loader.load_unicode('/templates/html/js_templates.html')
# A short random string to append to HTML element ID attributes to avoid multiple instances
# of the DnDv2 block on the same page sharing the same ID values.
# We avoid using ID attributes in preference to classes, but sometimes we still need IDs to
# connect 'for' and 'aria-describedby' attributes to the associated elements.
id_suffix = uuid.uuid4().hex[:16]
js_templates = js_templates.replace('{{id_suffix}}', id_suffix)
help_texts = {
field_name: self.ugettext(field.help)
for field_name, field in self.fields.viewitems() if hasattr(field, "help")
......@@ -259,6 +267,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
'js_templates': js_templates,
'help_texts': help_texts,
'field_values': field_values,
'id_suffix': id_suffix,
'self': self,
'data': urllib.quote(json.dumps(self.data)),
}
......
......@@ -79,11 +79,6 @@
font-size: 100%;
}
.xblock--drag-and-drop--editor .tab .nested-input {
display: block;
margin-top: 8px;
}
.xblock--drag-and-drop--editor .tab-header,
.xblock--drag-and-drop--editor .tab-content,
.xblock--drag-and-drop--editor .tab-footer {
......@@ -108,6 +103,25 @@
width: 50%;
}
.xblock--drag-and-drop--editor .target-image-form textarea {
display: block;
}
.xblock--drag-and-drop--editor label > span {
display: inline-block;
margin-bottom: 0.25em;
}
/* Main Tab */
.xblock--drag-and-drop--editor .feedback-tab input,
.xblock--drag-and-drop--editor .feedback-tab select {
display: block;
}
.xblock--drag-and-drop--editor .feedback-tab input[type=checkbox] {
display: inline-block;
}
/* Zones Tab */
.xblock--drag-and-drop--editor .zones-tab .zone-editor {
position: relative;
......@@ -138,11 +152,15 @@
}
.xblock--drag-and-drop--editor .zones-form .zone-row label {
display: block;
}
.xblock--drag-and-drop--editor .zones-form .zone-row label > span {
display: inline-block;
width: 18%;
width: 6em;
}
.xblock--drag-and-drop--editor .zones-form .zone-row > input {
.xblock--drag-and-drop--editor .zones-form .zone-row label > input {
width: 60%;
margin: 0 0 5px;
line-height: 2.664rem; /* .title gets line-height from a Studio rule that does not apply to .description;
......@@ -153,11 +171,16 @@
margin-bottom: 15px;
}
.xblock--drag-and-drop--editor .zones-form .zone-row .layout label {
display: inline-block;
width: 40%;
}
.xblock--drag-and-drop--editor .zones-form .zone-row .layout .size,
.xblock--drag-and-drop--editor .zones-form .zone-row .layout .coord {
width: 15%;
width: 35%;
margin: 0 19px 5px 0;
line-height: inherit;
}
.xblock--drag-and-drop--editor .feedback-form textarea {
......@@ -206,7 +229,6 @@
.xblock--drag-and-drop--editor .items-form textarea {
width: 97%;
margin: 0 1%;
}
.xblock--drag-and-drop--editor .items-form .zone-checkbox-label {
......
......@@ -29,10 +29,10 @@ function DragAndDropEditBlock(runtime, element, params) {
tpl: {
init: function() {
_fn.tpl = {
zoneInput: Handlebars.compile($("#zone-input-tpl", element).html()),
zoneElement: Handlebars.compile($("#zone-element-tpl", element).html()),
zoneCheckbox: Handlebars.compile($("#zone-checkbox-tpl", element).html()),
itemInput: Handlebars.compile($("#item-input-tpl", element).html()),
zoneInput: Handlebars.compile($(".zone-input-tpl", element).html()),
zoneElement: Handlebars.compile($(".zone-element-tpl", element).html()),
zoneCheckbox: Handlebars.compile($(".zone-checkbox-tpl", element).html()),
itemInput: Handlebars.compile($(".item-input-tpl", element).html()),
};
}
},
......@@ -66,7 +66,7 @@ function DragAndDropEditBlock(runtime, element, params) {
_fn.build.clickHandlers();
// Hide settings that are specific to assessment mode
_fn.build.$el.feedback.form.find('#problem-mode').trigger('change');
_fn.build.$el.feedback.form.find('.problem-mode').trigger('change');
},
validate: function() {
......@@ -120,8 +120,8 @@ function DragAndDropEditBlock(runtime, element, params) {
}
// Set the target image and bind its event handler:
$('.target-image-form #background-url', element).val(_fn.data.targetImg);
$('.target-image-form #background-description', element).val(_fn.data.targetImgDescription);
$('.target-image-form .background-url', element).val(_fn.data.targetImg);
$('.target-image-form .background-description', element).val(_fn.data.targetImgDescription);
_fn.build.$el.targetImage.load(_fn.build.form.zone.imageLoaded);
_fn.build.$el.targetImage.attr('src', params.target_img_expanded_url);
_fn.build.$el.targetImage.attr('alt', _fn.data.targetImgDescription);
......@@ -175,7 +175,7 @@ function DragAndDropEditBlock(runtime, element, params) {
});
$fbkTab
.on('change', '#problem-mode', _fn.build.form.problem.toggleAssessmentSettings);
.on('change', '.problem-mode', _fn.build.form.problem.toggleAssessmentSettings);
$zoneTab
.on('click', '.add-zone', function(e) {
......@@ -188,7 +188,7 @@ function DragAndDropEditBlock(runtime, element, params) {
.on('click', '.target-image-form button', function(e) {
e.preventDefault();
var new_img_url = $.trim($('.target-image-form #background-url', element).val());
var new_img_url = $.trim($('.target-image-form .background-url', element).val());
if (new_img_url) {
// We may need to 'expand' the URL before it will be valid.
// e.g. '/static/blah.png' becomes '/asset-v1:course+id/blah.png'
......@@ -202,9 +202,9 @@ function DragAndDropEditBlock(runtime, element, params) {
}
_fn.data.targetImg = new_img_url;
})
.on('input', '.target-image-form #background-description', function(e) {
.on('input', '.target-image-form .background-description', function(e) {
var new_description = $.trim(
$('.target-image-form #background-description', element).val()
$('.target-image-form .background-description', element).val()
);
_fn.build.$el.targetImage.attr('alt', new_description);
_fn.data.targetImgDescription = new_description;
......@@ -230,8 +230,8 @@ function DragAndDropEditBlock(runtime, element, params) {
toggleAssessmentSettings: function(e) {
e.preventDefault();
var $modeSetting = $(e.currentTarget),
$problemForm = $modeSetting.parent('form'),
$assessmentSettings = $problemForm.find('.setting.assessment');
$problemForm = $modeSetting.closest('form'),
$assessmentSettings = $problemForm.find('.assessment-setting');
if ($modeSetting.val() === 'assessment') {
$assessmentSettings.show();
} else {
......@@ -394,8 +394,8 @@ function DragAndDropEditBlock(runtime, element, params) {
},
feedback: function($form) {
_fn.data.feedback = {
start: $form.find('#intro-feedback').val(),
finish: $form.find('#final-feedback').val()
start: $form.find('.intro-feedback').val(),
finish: $form.find('.final-feedback').val()
};
},
item: {
......@@ -505,15 +505,15 @@ function DragAndDropEditBlock(runtime, element, params) {
_fn.data.zones = _fn.build.form.zone.zoneObjects;
var data = {
'display_name': $element.find('#display-name').val(),
'mode': $element.find("#problem-mode").val(),
'display_name': $element.find('.display-name').val(),
'mode': $element.find(".problem-mode").val(),
'max_attempts': $element.find(".max-attempts").val(),
'show_title': $element.find('.show-title').is(':checked'),
'weight': $element.find('#weight').val(),
'problem_text': $element.find('#problem-text').val(),
'weight': $element.find('.weight').val(),
'problem_text': $element.find('.problem-text').val(),
'show_problem_header': $element.find('.show-problem-header').is(':checked'),
'item_background_color': $element.find('#item-background-color').val(),
'item_text_color': $element.find('#item-text-color').val(),
'item_background_color': $element.find('.item-background-color').val(),
'item_text_color': $element.find('.item-text-color').val(),
'data': _fn.data,
};
......
<script id="zone-element-tpl" type="text/html">
<script class="zone-element-tpl" type="text/html">
<div class="zone" data-zone="{{ uid }}" style="
top:{{ y_percent }}%;
left:{{ x_percent }}%;
......@@ -9,55 +9,58 @@
</div>
</script>
<script id="zone-input-tpl" type="text/html">
<script class="zone-input-tpl" type="text/html">
<div class="zone-row" data-uid="{{zone.uid}}">
<!-- uid values from old versions of the block may contain spaces and other characters, so we use 'index' as an alternate unique ID here. -->
<label for="zone-{{index}}-title">{{i18n "Text"}}</label>
<a class="remove-zone hidden" title="{{i18n 'Remove zone'}}">
<div class="icon remove"></div>
</a>
<label>
<span>{{i18n "Text"}}</span>
<input type="text"
id="zone-{{index}}-title"
class="title"
value="{{ zone.title }}"
required />
<a class="remove-zone hidden">
<div class="icon remove"></div>
</a>
<label for="zone-{{index}}-description">{{i18n "Description"}}</label>
</label>
<label>
<span>{{i18n "Description"}}</span>
<input type="text"
id="zone-{{index}}-description"
class="description"
value="{{ zone.description }}"
placeholder="{{i18n 'Describe this zone to non-visual users'}}"
required />
</label>
<div class="layout">
<label for="zone-{{index}}-width">{{i18n "width"}}</label>
<label>
<span>{{i18n "width"}}</span>
<input type="text"
id="zone-{{index}}-width"
class="size width"
value="{{ zone.width }}" />
<label for="zone-{{index}}-height">{{i18n "height"}}</label>
</label>
<label>
<span>{{i18n "height"}}</span>
<input type="text"
id="zone-{{index}}-height"
class="size height"
value="{{ zone.height }}" />
</label>
<br />
<label for="zone-{{index}}-x">x</label>
<label>
<span>x</span>
<input type="text"
id="zone-{{index}}-x"
class="coord x"
value="{{ zone.x }}" />
<label for="zone-{{index}}-y">y</label>
</label>
<label>
<span>y</span>
<input type="text"
id="zone-{{index}}-y"
class="coord y"
value="{{ zone.y }}" />
</label>
</div>
<div class="alignment">
<label for="zone-{{index}}-align">
{{i18n "Alignment"}}
</label>
<select id="zone-{{index}}-align"
class="align-select"
aria-describedby="zone-align-description">
<label>
<span>{{i18n "Alignment"}}</span>
<select class="align-select"
aria-describedby="zone-align-description-{{zone.uid}}-{{id_suffix}}">
<option value=""
{{#ifeq zone.align ""}}selected{{/ifeq}}>
{{i18n "none"}}
......@@ -75,14 +78,15 @@
{{i18n "right"}}
</option>
</select>
<div id="zone-align-description" class="zones-form-help">
</label>
<div id="zone-align-description-{{zone.uid}}-{{id_suffix}}" class="form-help">
{{i18n "Align dropped items to the left, center, or right. Default is no alignment (items stay exactly where the user drops them)."}}
</div>
</div>
</div>
</script>
<script id="zone-checkbox-tpl" type="text/html">
<script class="zone-checkbox-tpl" type="text/html">
<div class="zone-checkbox-row">
<label>
<input type="checkbox"
......@@ -94,7 +98,7 @@
</div>
</script>
<script id="item-input-tpl" type="text/html">
<script class="item-input-tpl" type="text/html">
<div class="item">
<div class="row">
<label class="h3">
......@@ -126,32 +130,38 @@
</label>
</div>
<div class="row">
<label class="h3" for="item-{{id}}-image-description">{{i18n "Image description (should provide sufficient information to place the item even if the image did not load)"}}</label>
<textarea id="item-{{id}}-image-description" {{#if imageURL}}required{{/if}}
class="item-image-description">{{ imageDescription }}</textarea>
<label class="h3">
<span>{{i18n "Image description (should provide sufficient information to place the item even if the image did not load)"}}</span>
<textarea {{#if imageURL}}required{{/if}} class="item-image-description">{{ imageDescription }}</textarea>
</label>
</div>
<div class="row">
<label class="h3" for="item-{{id}}-success-feedback">{{i18n "Success Feedback"}}</label>
<textarea id="item-{{id}}-success-feedback"
class="success-feedback">{{ feedback.correct }}</textarea>
<label class="h3">
<span>{{i18n "Success Feedback"}}</span>
<textarea class="success-feedback">{{ feedback.correct }}</textarea>
</label>
</div>
<div class="row">
<label class="h3" for="item-{{id}}-error-feedback">{{i18n "Error Feedback"}}</label>
<textarea id="item-{{id}}-error-feedback"
class="error-feedback">{{ feedback.incorrect }}</textarea>
<label class="h3">
<span>{{i18n "Error Feedback"}}</span>
<textarea class="error-feedback">{{ feedback.incorrect }}</textarea>
</label>
</div>
<div class="row advanced-link">
<a>{{i18n "Show advanced settings" }}</a>
</div>
<div class="row advanced">
<label>
<span>
{{i18n "Preferred width as a percentage of the background image width (or blank for automatic width):"}}
</span>
<input type="number"
class="item-width"
value="{{ singleDecimalFloat widthPercent }}"
step="0.1"
min="1"
max="99" />%
</label>
</div>
</div>
</script>
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