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,
};
......
......@@ -12,8 +12,10 @@
<section class="tab-content">
<form class="feedback-form">
<label class="h3" for="display-name">{% trans self.fields.display_name.display_name %}</label>
<input id="display-name" value="{{ self.display_name }}" />
<label class="h3">
<span>{% trans self.fields.display_name.display_name %}</span>
<input class="display-name" value="{{ self.display_name }}" />
</label>
<label title="{{ help_texts.show_title }}">
<input class="show-title" type="checkbox"
{% if self.show_title %}checked="checked"{% endif %}>
......@@ -21,50 +23,62 @@
<span class="sr">{{ help_texts.show_title }}</span>
</label>
<label class="h3" for="problem-mode">{% trans self.fields.mode.display_name %}</label>
<select id="problem-mode" aria-describedby="problem-mode-description">
{% for field_value in field_values.mode %}
<option value="{{ field_value.value }}" {% if self.mode == field_value.value %}selected{% endif %}>
{{ field_value.display_name }}
</option>
{% endfor %}
</select>
<div id="problem-mode-description" class="form-help">
<label class="h3">
<span>{% trans self.fields.mode.display_name %}</span>
<select class="problem-mode" aria-describedby="problem-mode-description-{{id_suffix}}">
{% for field_value in field_values.mode %}
<option value="{{ field_value.value }}" {% if self.mode == field_value.value %}selected{% endif %}>
{{ field_value.display_name }}
</option>
{% endfor %}
</select>
</label>
<div id="problem-mode-description-{{id_suffix}}" class="form-help">
{{ help_texts.mode }}
</div>
<label class="h3 setting assessment">
<label class="h3 assessment-setting">
{% trans self.fields.max_attempts.display_name %}
<input class="nested-input max-attempts"
<input class="max-attempts"
type="number"
min="1"
step="1"
aria-describedby="max-attempts-description"
aria-describedby="max-attempts-description-{{id_suffix}}"
{% if self.max_attempts %}value="{{ self.max_attempts }}"{% endif %} />
</label>
<div id="max-attempts-description" class="form-help">
<div id="max-attempts-description-{{id_suffix}}" class="assessment-setting form-help">
{{ help_texts.max_attempts }}
</div>
<label class="h3" for="weight">{% trans self.fields.weight.display_name %}</label>
<input id="weight" type="number" step="0.1" value="{{ self.weight|unlocalize }}" />
<label class="h3">
<span>{% trans self.fields.weight.display_name %}</span>
<input class="weight" type="number" step="0.1" value="{{ self.weight|unlocalize }}" />
</label>
<label class="h3" for="problem-text">{% trans self.fields.question_text.display_name %}</label>
<textarea id="problem-text">{{ self.question_text }}</textarea>
<label class="h3">
<span>{% trans self.fields.question_text.display_name %}</span>
<textarea class="problem-text">{{ self.question_text }}</textarea>
</label>
<label>
<input class="show-problem-header" type="checkbox" aria-describedby="show-problem-header-description"
<input class="show-problem-header"
type="checkbox"
aria-describedby="show-problem-header-description-{{id_suffix}}"
{% if self.show_question_header %}checked="checked"{% endif %}>
{% trans self.fields.show_question_header.display_name %}
</label>
<div id="show-problem-header-description" class="form-help">
<div id="show-problem-header-description-{{id_suffix}}" class="form-help">
{{ help_texts.show_question_header }}
</div>
<label class="h3" for="intro-feedback">{% trans "Introductory Feedback" %}</label>
<textarea id="intro-feedback">{{ self.data.feedback.start }}</textarea>
<label class="h3">
<span>{% trans "Introductory Feedback" %}</span>
<textarea class="intro-feedback">{{ self.data.feedback.start }}</textarea>
</label>
<label class="h3" for="final-feedback">{% trans "Final Feedback" %}</label>
<textarea id="final-feedback">{{ self.data.feedback.finish }}</textarea>
<label class="h3">
<span>{% trans "Final Feedback" %}</span>
<textarea class="final-feedback">{{ self.data.feedback.finish }}</textarea>
</label>
</form>
</section>
</div>
......@@ -75,15 +89,21 @@
</header>
<section class="tab-content">
<form class="target-image-form">
<label class="h3" for="background-url">{% trans "Background URL" %}</label>
<input id="background-url"
<label class="h3" for="background-url-{{id_suffix}}">
<span>{% trans "Background URL" %}</span>
</label>
<input class="background-url"
id="background-url-{{id_suffix}}"
type="text"
placeholder="{% trans 'For example, http://example.com/background.png or /static/background.png' %}">
</label>
<button class="btn">{% trans "Change background" %}</button>
<label class="h3" for="background-description">{% trans "Background description" %}</label>
<textarea required id="background-description"
aria-describedby="background-description-description"></textarea>
<div id="background-description-description" class="form-help">
<label class="h3">
<span>{% trans "Background description" %}</span>
<textarea required class="background-description"
aria-describedby="background-description-description-{{id_suffix}}"></textarea>
</label>
<div id="background-description-description-{{id_suffix}}" class="form-help">
{% blocktrans %}
Please provide a description of the image for non-visual users.
The description should provide sufficient information to allow anyone
......@@ -95,13 +115,17 @@
<section class="tab-content">
<form class="display-labels-form">
<h3>{% trans "Zone labels" %}</h3>
<label for="display-labels">{% trans "Display label names on the image" %}:</label>
<input name="display-labels" id="display-labels" type="checkbox" />
<label>
<span>{% trans "Display label names on the image" %}:</span>
<input name="display-labels" class="display-labels" type="checkbox" />
</label>
</form>
<form class="display-borders-form">
<h3>{% trans "Zone borders" %}</h3>
<label for="display-borders">{% trans "Display zone borders on the image" %}:</label>
<input name="display-borders" id="display-borders" type="checkbox" />
<label>
<span>{% trans "Display zone borders on the image" %}:</span>
<input name="display-borders" class="display-borders" type="checkbox" />
</label>
</form>
</section>
<section class="tab-content">
......@@ -125,20 +149,24 @@
</header>
<section class="tab-content">
<form class="item-styles-form">
<label class="h3" for="item-background-color">{% trans self.fields.item_background_color.display_name %}</label>
<input id="item-background-color"
placeholder="e.g. blue or #0000ff"
value="{{ self.item_background_color }}"
aria-describedby="item-background-color-description">
<div id="item-background-color-description" class="form-help">
<label class="h3">
<span>{% trans self.fields.item_background_color.display_name %}</span>
<input class="item-background-color"
placeholder="e.g. blue or #0000ff"
value="{{ self.item_background_color }}"
aria-describedby="item-background-color-description-{{id_suffix}}">
</label>
<div id="item-background-color-description-{{id_suffix}}" class="form-help">
{{ help_texts.item_background_color }}
</div>
<label class="h3" for="item-text-color">{% trans self.fields.item_text_color.display_name %}</label>
<input id="item-text-color"
placeholder="e.g. white or #ffffff"
value="{{ self.item_text_color}}"
aria-describedby="item-text-color-description">
<div id="item-text-color-description" class="form-help">
<label class="h3">
<span>{% trans self.fields.item_text_color.display_name %}</span>
<input class="item-text-color"
placeholder="e.g. white or #ffffff"
value="{{ self.item_text_color}}"
aria-describedby="item-text-color-description-{{id_suffix}}">
</label>
<div id="item-text-color-description-{{id_suffix}}" class="form-help">
{{ help_texts.item_text_color }}
</div>
</form>
......
<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,80 +9,84 @@
</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>
<input type="text"
id="zone-{{index}}-title"
class="title"
value="{{ zone.title }}"
required />
<a class="remove-zone hidden">
<a class="remove-zone hidden" title="{{i18n 'Remove zone'}}">
<div class="icon remove"></div>
</a>
<label for="zone-{{index}}-description">{{i18n "Description"}}</label>
<input type="text"
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>
<label>
<span>{{i18n "Text"}}</span>
<input type="text"
id="zone-{{index}}-width"
class="size width"
value="{{ zone.width }}" />
<label for="zone-{{index}}-height">{{i18n "height"}}</label>
class="title"
value="{{ zone.title }}"
required />
</label>
<label>
<span>{{i18n "Description"}}</span>
<input type="text"
id="zone-{{index}}-height"
class="size height"
value="{{ zone.height }}" />
class="description"
value="{{ zone.description }}"
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 />
<label for="zone-{{index}}-x">x</label>
<input type="text"
id="zone-{{index}}-x"
class="coord x"
value="{{ zone.x }}" />
<label for="zone-{{index}}-y">y</label>
<input type="text"
id="zone-{{index}}-y"
class="coord y"
value="{{ zone.y }}" />
<label>
<span>x</span>
<input type="text"
class="coord x"
value="{{ zone.x }}" />
</label>
<label>
<span>y</span>
<input type="text"
class="coord y"
value="{{ zone.y }}" />
</label>
</div>
<div class="alignment">
<label for="zone-{{index}}-align">
{{i18n "Alignment"}}
<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"}}
</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>
<select id="zone-{{index}}-align"
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">
<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>
{{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"
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