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