Commit 009a80cc by Kelketek

Merge pull request #34 from edx-solutions/master

Drag and Drop v2 - Review for release on edx.org
parents 80ad8ee8 fae37c69
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
.noseids
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Integration test output:
/*.log
/tests.*.png
var/*
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IDEs
.idea
.idea/*
language: python
sudo: false
python:
- "2.7"
before_install:
- "export DISPLAY=:99"
- "sh -e /etc/init.d/xvfb start"
install:
- "sh install_test_deps.sh"
- "pip uninstall -y xblock-drag-and-drop-v2"
- "python setup.py sdist"
- "pip install dist/xblock-drag-and-drop-v2-2.0.0.tar.gz"
script:
- pep8 drag_and_drop_v2 tests --max-line-length=120
- pylint drag_and_drop_v2 tests
- python run_tests.py
notifications:
email: false
addons:
firefox: "43.0"
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
EdX Inc. wishes to state, in clarification of the above license terms, that
any public, independently available web service offered over the network and
communicating with edX's copyrighted works by any form of inter-service
communication, including but not limited to Remote Procedure Call (RPC)
interfaces, is not a work based on our copyrighted work within the meaning
of the license. "Corresponding Source" of this work, or works based on this
work, as defined by the terms of this license do not include source code
files for programs used solely to provide those public, independently
available web services.
Drag and Drop XBlock v2
=======================
This XBlock implements a friendly drag-and-drop style problem, where
the learner has to drag items to zones on a target image.
The editor is fully guided. Features include:
* custom target image
* free target zone positioning and sizing
* custom zone labels
* ability to show or hide zone borders
* custom text and background colors for items
* image items
* items prompting for additional (numerical) input after being dropped
* decoy items that don't have a zone
* feedback popups for both correct and incorrect attempts
* introductory and final feedback
The XBlock supports progressive grading and keeps progress across
refreshes. All checking and record keeping is done on the server side.
The following screenshot shows the Drag and Drop XBlock rendered
inside the edX LMS before the user starts solving the problem:
![Student view start](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/student-view-start.png)
This screenshot shows the XBlock after the learner successfully
completed the Drag and Drop problem:
![Student view finish](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/student-view-finish.png)
Installation
------------
Install the requirements into the Python virtual environment of your
`edx-platform` installation by running the following command from the
root folder:
```bash
$ pip install -r requirements.txt
```
Theming
-------
The Drag and Drop XBlock ships with an alternate theme called "Apros"
that you can enable by adding the following entry to `XBLOCK_SETTINGS`
in `lms.env.json`:
```json
"drag-and-drop-v2": {
"theme": {
"package": "drag_and_drop_v2",
"locations": ["public/themes/apros.css"]
}
}
```
You can use the same approach to apply a custom theme:
`"package"` can refer to any Python package in your virtualenv, which
means you can develop and maintain your own theme in a separate
package. There is no need to fork or modify this repository in any way
to customize the look and feel of your Drag and Drop problems.
`"locations"` is a list of relative paths pointing to CSS files
belonging to your theme. While the XBlock loads, files will be added
to it in the order that they appear in this list. (This means that if
there are rules with identical selectors spread out over different
files, rules in files that appear later in the list will take
precedence over those that appear earlier.)
Finally, note that the default (unthemed) appearance of the Drag and
Drop XBlock has been optimized for accessibility, so its use is
encouraged -- especially for courses targeting large and/or
potentially diverse audiences.
Enabling in Studio
------------------
You can enable the Drag and Drop XBlock in Studio through the Advanced
Settings.
1. From the main page of a specific course, navigate to `Settings ->
Advanced Settings` from the top menu.
2. Check for the `Advanced Module List` policy key, and add
`"drag-and-drop-v2"` to the policy value list.
3. Click the "Save changes" button.
Usage
-----
The Drag and Drop XBlock features an interactive editor. Add the Drag
and Drop component to a lesson, then click the `EDIT` button.
![Edit view](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view.png)
In the first step, you can set some basic properties of the component,
such as the title, the maximum score, the problem text to render
above the background image, the introductory feedback (shown
initially), and the final feedback (shown after the learner
successfully completes the drag and drop problem).
![Drop zone edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view-zones.png)
In the next step, you set the URL and description for the background
image and define the properties of the drop zones. For each zone you
can specify the text that should be rendered inside it (the "zone
label"), how wide and tall it should be, and where it should be placed
on the background image. In this step you can also specify whether you
would like zone labels to be shown to learners or not, as well as
whether or not to display borders outlining the zones. It is possible
to define an arbitrary number of drop zones as long as their labels
are unique.
![Drag item edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view-items.png)
In the final step, you define the background and text color for drag
items, as well as the drag items themselves. A drag item can contain
either text or an image. You can define custom success and error
feedback for each item. The feedback text is displayed in a popup
after the learner drops the item on a zone - the success feedback is
shown if the item is dropped on the correct zone, while the error
feedback is shown when dropping the item on an incorrect drop zone.
Additionally, items can have a numerical value (and an optional error
margin) associated with them. When a learner drops an item that has a
numerical value on the correct zone, an input field for entering a
value is shown next to the item. The value that the learner submits is
checked against the expected value for the item. If you also specify a
margin, the value entered by the learner will be considered correct if
it does not differ from the expected value by more than that margin
(and incorrect otherwise).
![Zone dropdown](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view-zone-dropdown.png)
The zone that an item belongs to is selected from a dropdown that
includes all drop zones defined in the previous step and a `none`
option that can be used for "decoy" items - items that don't belong to
any zone.
You can define an arbitrary number of drag items.
Analytics Events
----------------
The following analytics events are provided by this block.
## `edx.drag_and_drop_v2.loaded`
Fired when the Drag and Drop XBlock is finished loading.
Example ("common" fields that are not interesting in this context have been left out):
```
{
...
"event": {},
"event_source": "server", -- Common field, contains event source.
"event_type": "edx.drag_and_drop_v2.loaded", -- Common field, contains event name.
...
```
Real event example (taken from a devstack):
```
{
"username": "staff",
"event_type": "edx.drag_and_drop_v2.loaded",
"ip": "10.0.2.2",
"agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0",
"host": "precise64",
"referer": "http://example.com/courses/course-v1:DnD+DnD+DnD/courseware/ec546c58d2f447b7a9223c57b5de7344/756071f8de7f47c3b0ae726586ebbe16/1?activate_block_id=block-v1%3ADnD%2BDnD%2BDnD%2Btype%40vertical%2Bblock%40d2fc47476ca14c55816c4a1264a27280",
"accept_language": "en;q=1.0, en;q=0.5",
"event": {},
"event_source": "server",
"context": {
"course_user_tags": {},
"user_id": 5,
"org_id": "DnD",
"module": {
"usage_key": "block-v1:DnD+DnD+DnD+type@drag-and-drop-v2+block@6b80ce1e8b78426898b47a834d72ffd3",
"display_name": "Drag and Drop"
},
"course_id": "course-v1:DnD+DnD+DnD",
"path": "/courses/course-v1:DnD+DnD+DnD/xblock/block-v1:DnD+DnD+DnD+type@drag-and-drop-v2+block@6b80ce1e8b78426898b47a834d72ffd3/handler/publish_event"
},
"time": "2016-01-13T01:52:41.330049+00:00",
"page": "x_module"
}
```
## `edx.drag_and_drop_v2.item.picked_up`
Fired when a learner picks up a draggable item.
Example ("common" fields that are not interesting in this context have been left out):
```
{
...
"event": {
"item_id": 0, -- ID of the draggable item.
},
"event_source": "server", -- Common field, contains event source.
"event_type": "edx.drag_and_drop_v2.picked_up", -- Common field, contains event name.
...
```
Real event example (taken from a devstack):
```
{
"username": "staff",
"event_type": "edx.drag_and_drop_v2.item.picked_up",
"ip": "10.0.2.2",
"agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0",
"host": "precise64",
"referer": "http://example.com/courses/course-v1:DnD+DnD+DnD/courseware/ec546c58d2f447b7a9223c57b5de7344/756071f8de7f47c3b0ae726586ebbe16/1?activate_block_id=block-v1%3ADnD%2BDnD%2BDnD%2Btype%40vertical%2Bblock%40d2fc47476ca14c55816c4a1264a27280",
"accept_language": "en;q=1.0, en;q=0.5",
"event": {
"item_id": 0,
},
"event_source": "server",
"context": {
"course_user_tags": {},
"user_id": 5,
"org_id": "DnD",
"module": {
"usage_key": "block-v1:DnD+DnD+DnD+type@drag-and-drop-v2+block@6b80ce1e8b78426898b47a834d72ffd3",
"display_name": "Drag and Drop"
},
"course_id": "course-v1:DnD+DnD+DnD",
"path": "/courses/course-v1:DnD+DnD+DnD/xblock/block-v1:DnD+DnD+DnD+type@drag-and-drop-v2+block@6b80ce1e8b78426898b47a834d72ffd3/handler/publish_event"
},
"time": "2016-01-13T01:58:44.395935+00:00",
"page": "x_module"
}
```
## `edx.drag_and_drop_v2.item.dropped`
Fired when a learner drops a draggable item.
This event will be emitted when a learner drops a draggable item.
Example ("common" fields that are not interesting in this context have been left out):
```
{
...
"event": {
"input": null,
"is_correct": true, -- False if there is an input in the draggable item, and the learner provided the wrong answer. Otherwise true.
"is_correct_location": true, -- Whether the draggable item has been placed in the correct location.
"item_id": 0, -- ID of the draggable item.
"location": "The Top Zone", -- Name of the location the item was dragged to.
},
"event_source": "server", -- Common field, contains event source.
"event_type": "edx.drag_and_drop_v2.dropped", -- Common field, contains event name.
...
```
Real event example (taken from a devstack):
```
{
"username": "staff",
"event_type": "edx.drag_and_drop_v2.item.dropped",
"ip": "10.0.2.2",
"agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0",
"host": "precise64",
"referer": "http://example.com/courses/course-v1:DnD+DnD+DnD/courseware/ec546c58d2f447b7a9223c57b5de7344/756071f8de7f47c3b0ae726586ebbe16/1?activate_block_id=block-v1%3ADnD%2BDnD%2BDnD%2Btype%40vertical%2Bblock%40d2fc47476ca14c55816c4a1264a27280",
"accept_language": "en;q=1.0, en;q=0.5",
"event": {
"is_correct_location": true,
"is_correct": true,
"location": "The Top Zone",
"item_id": 0,
"input": null
},
"event_source": "server",
"context": {
"course_user_tags": {},
"user_id": 5,
"org_id": "DnD",
"module": {
"usage_key": "block-v1:DnD+DnD+DnD+type@drag-and-drop-v2+block@6b80ce1e8b78426898b47a834d72ffd3",
"display_name": "Drag and Drop"
},
"course_id": "course-v1:DnD+DnD+DnD",
"path": "/courses/course-v1:DnD+DnD+DnD/xblock/block-v1:DnD+DnD+DnD+type@drag-and-drop-v2+block@6b80ce1e8b78426898b47a834d72ffd3/handler/do_attempt"
},
"time": "2016-01-13T01:58:45.202313+00:00",
"page": "x_module"
}
```
## `edx.drag_and_drop_v2.feedback.opened`
Fired when the feedback pop-up is opened.
Example ("common" fields that are not interesting in this context have been left out):
```
{
...
"event": {
"content": "Correct! This one belongs to The Top Zone.", -- Content of the feedback popup.
"truncated": false, -- Boolean indicating whether "content" field was truncated.
},
"event_source": "server", -- Common field, contains event source.
"event_type": "edx.drag_and_drop_v2.feedback.opened", -- Common field, contains event name.
...
```
Real event example (taken from a devstack):
```
{
"username": "staff",
"event_type": "edx.drag_and_drop_v2.feedback.opened",
"ip": "10.0.2.2",
"agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0",
"host": "precise64",
"referer": "http://example.com/courses/course-v1:DnD+DnD+DnD/courseware/ec546c58d2f447b7a9223c57b5de7344/756071f8de7f47c3b0ae726586ebbe16/1?activate_block_id=block-v1%3ADnD%2BDnD%2BDnD%2Btype%40vertical%2Bblock%40d2fc47476ca14c55816c4a1264a27280",
"accept_language": "en;q=1.0, en;q=0.5",
"event": {
"content": "Correct! This one belongs to The Top Zone.",
"truncated": false,
},
"event_source": "server",
"context": {
"course_user_tags": {},
"user_id": 5,
"org_id": "DnD",
"module": {
"usage_key": "block-v1:DnD+DnD+DnD+type@drag-and-drop-v2+block@6b80ce1e8b78426898b47a834d72ffd3",
"display_name": "Drag and Drop"
},
"course_id": "course-v1:DnD+DnD+DnD",
"path": "/courses/course-v1:DnD+DnD+DnD/xblock/block-v1:DnD+DnD+DnD+type@drag-and-drop-v2+block@6b80ce1e8b78426898b47a834d72ffd3/handler/publish_event"
},
"time": "2016-01-13T01:58:45.844986+00:00",
"page": "x_module"
}
```
## `edx.drag_and_drop_v2.feedback.closed`
Fired when the feedback popup is closed.
Example ("common" fields that are not interesting in this context have been left out):
```
{
...
"event": {
"content": "No, this item does not belong here. Try again." -- Message of the feedback popup that was closed.
"manually": true, -- Whether or not the user closed the feedback window manually or if it was auto-closed.
"truncated": false, -- Boolean indicating whether "content" field was truncated.
},
"event_source": "server", -- Common field, contains event source.
"event_type": "edx.drag_and_drop_v2.feedback.closed", -- Common field, contains event name.
...
```
Real event example (taken from a devstack):
```
{
"username": "staff",
"event_type": "edx.drag_and_drop_v2.feedback.closed",
"ip": "10.0.2.2",
"agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) Gecko/20100101 Firefox/43.0",
"host": "precise64",
"referer": "http://example.com/courses/course-v1:DnD+DnD+DnD/courseware/ec546c58d2f447b7a9223c57b5de7344/756071f8de7f47c3b0ae726586ebbe16/1?activate_block_id=block-v1%3ADnD%2BDnD%2BDnD%2Btype%40vertical%2Bblock%40d2fc47476ca14c55816c4a1264a27280",
"accept_language": "en;q=1.0, en;q=0.5",
"event": {
"content": "No, this item does not belong here. Try again."
"manually": true
"truncated": false,
},
"event_source": "server",
"context": {
"course_user_tags": {},
"user_id": 5,
"org_id": "DnD",
"module": {
"usage_key": "block-v1:DnD+DnD+DnD+type@drag-and-drop-v2+block@13d1b859a2304c858e1810ccc23f29b2",
"display_name": "Drag and Drop"
},
"course_id": "course-v1:DnD+DnD+DnD",
"path": "/courses/course-v1:DnD+DnD+DnD/xblock/block-v1:DnD+DnD+DnD+type@drag-and-drop-v2+block@13d1b859a2304c858e1810ccc23f29b2/handler/publish_event"
},
"time": "2016-01-13T02:07:00.988534+00:00",
"page": "x_module"
}
```
Testing
-------
Inside a fresh virtualenv, `cd` into the root folder of this repository
(`xblock-drag-and-drop-v2`) and run
```bash
$ sh install_test_deps.sh
```
You can then run the entire test suite via
```bash
$ python run_tests.py
```
To only run the unit test suite, do
```bash
$ python run_tests.py tests/unit/
```
Similarly, you can run the integration test suite via
```bash
$ python run_tests.py tests/integration/
```
from .drag_and_drop_v2 import DragAndDropBlock
from .utils import _
TARGET_IMG_DESCRIPTION = _(
"An isosceles triangle with three layers of similar height. "
"It is shown upright, so the widest layer is located at the bottom, "
"and the narrowest layer is located at the top."
)
TOP_ZONE_TITLE = _("The Top Zone")
MIDDLE_ZONE_TITLE = _("The Middle Zone")
BOTTOM_ZONE_TITLE = _("The Bottom Zone")
TOP_ZONE_DESCRIPTION = _("Use this zone to associate an item with the top layer of the triangle.")
MIDDLE_ZONE_DESCRIPTION = _("Use this zone to associate an item with the middle layer of the triangle.")
BOTTOM_ZONE_DESCRIPTION = _("Use this zone to associate an item with the bottom layer of the triangle.")
ITEM_CORRECT_FEEDBACK = _("Correct! This one belongs to {zone}.")
ITEM_INCORRECT_FEEDBACK = _("No, this item does not belong here. Try again.")
ITEM_NO_ZONE_FEEDBACK = _("You silly, there are no zones for this one.")
START_FEEDBACK = _("Drag the items onto the image above.")
FINISH_FEEDBACK = _("Good work! You have completed this drag and drop problem.")
DEFAULT_DATA = {
"targetImgDescription": TARGET_IMG_DESCRIPTION,
"zones": [
{
"index": 1,
"id": "zone-1",
"title": TOP_ZONE_TITLE,
"description": TOP_ZONE_DESCRIPTION,
"x": 160,
"y": 30,
"width": 196,
"height": 178,
},
{
"index": 2,
"id": "zone-2",
"title": MIDDLE_ZONE_TITLE,
"description": MIDDLE_ZONE_DESCRIPTION,
"x": 86,
"y": 210,
"width": 340,
"height": 138,
},
{
"index": 3,
"id": "zone-3",
"title": BOTTOM_ZONE_TITLE,
"description": BOTTOM_ZONE_DESCRIPTION,
"x": 15,
"y": 350,
"width": 485,
"height": 135,
}
],
"items": [
{
"displayName": _("Goes to the top"),
"feedback": {
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE)
},
"zone": TOP_ZONE_TITLE,
"imageURL": "",
"id": 0,
},
{
"displayName": _("Goes to the middle"),
"feedback": {
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=MIDDLE_ZONE_TITLE)
},
"zone": MIDDLE_ZONE_TITLE,
"imageURL": "",
"id": 1,
},
{
"displayName": _("Goes to the bottom"),
"feedback": {
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=BOTTOM_ZONE_TITLE)
},
"zone": BOTTOM_ZONE_TITLE,
"imageURL": "",
"id": 2,
},
{
"displayName": _("I don't belong anywhere"),
"feedback": {
"incorrect": ITEM_NO_ZONE_FEEDBACK,
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 3,
},
],
"feedback": {
"start": START_FEEDBACK,
"finish": FINISH_FEEDBACK,
},
}
# -*- coding: utf-8 -*-
#
# Imports ###########################################################
import json
import webob
import copy
import urllib
from xblock.core import XBlock
from xblock.fields import Scope, String, Dict, Float, Boolean
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.settings import XBlockWithSettingsMixin, ThemableXBlockMixin
from .utils import _ # pylint: disable=unused-import
from .default_data import DEFAULT_DATA
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
@XBlock.wants('settings')
class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"""
XBlock that implements a friendly Drag-and-Drop problem
"""
display_name = String(
display_name=_("Title"),
help=_("The title of the drag and drop problem. The title is displayed to learners."),
scope=Scope.settings,
default=_("Drag and Drop"),
)
show_title = Boolean(
display_name=_("Show title"),
help=_("Display the title to the learner?"),
scope=Scope.settings,
default=True,
)
question_text = String(
display_name=_("Problem text"),
help=_("The description of the problem or instructions shown to the learner"),
scope=Scope.settings,
default="",
)
show_question_header = Boolean(
display_name=_('Show "Problem" heading'),
help=_('Display the heading "Problem" above the problem text?'),
scope=Scope.settings,
default=True,
)
weight = Float(
display_name=_("Weight"),
help=_("The maximum score the learner can receive for the problem"),
scope=Scope.settings,
default=1,
)
item_background_color = String(
display_name=_("Item background color"),
help=_("The background color of draggable items in the problem."),
scope=Scope.settings,
default="",
)
item_text_color = String(
display_name=_("Item text color"),
help=_("Text color to use for draggable items."),
scope=Scope.settings,
default="",
)
data = Dict(
display_name=_("Problem data"),
help=_(
"Information about zones, items, feedback, and background image for this problem. "
"This information is derived from the input that a course author provides via the interactive editor "
"when configuring the problem."
),
scope=Scope.content,
default=DEFAULT_DATA,
)
item_state = Dict(
help=_("Information about current positions of items that a learner has dropped on the target image."),
scope=Scope.user_state,
default={},
)
completed = Boolean(
help=_("Indicates whether a learner has completed the problem at least once"),
scope=Scope.user_state,
default=False,
)
block_settings_key = 'drag-and-drop-v2'
has_score = True
def _(self, text):
""" Translate text """
return self.runtime.service(self, "i18n").ugettext(text)
@XBlock.supports("multi_device") # Enable this block for use in the mobile app via webview
def student_view(self, context):
"""
Player view, displayed to the student
"""
fragment = Fragment()
fragment.add_content(loader.render_template('/templates/html/drag_and_drop.html'))
css_urls = (
'public/css/vendor/jquery-ui-1.10.4.custom.min.css',
'public/css/drag_and_drop.css'
)
js_urls = (
'public/js/vendor/jquery-ui-1.10.4.custom.min.js',
'public/js/vendor/jquery-ui-touch-punch-0.2.3.min.js', # Makes it work on touch devices
'public/js/vendor/virtual-dom-1.3.0.min.js',
'public/js/drag_and_drop.js',
)
for css_url in css_urls:
fragment.add_css_url(self.runtime.local_resource_url(self, css_url))
for js_url in js_urls:
fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url))
self.include_theme_files(fragment)
fragment.initialize_js('DragAndDropBlock', self.get_configuration())
return fragment
def get_configuration(self):
"""
Get the configuration data for the student_view.
The configuration is all the settings defined by the author, except for correct answers
and feedback.
"""
def items_without_answers():
items = copy.deepcopy(self.data.get('items', ''))
for item in items:
del item['feedback']
del item['zone']
item['inputOptions'] = 'inputOptions' in item
return items
return {
"zones": self.data.get('zones', []),
# SDK doesn't supply url_name.
"url_name": getattr(self, 'url_name', ''),
"display_zone_labels": self.data.get('displayLabels', False),
"display_zone_borders": self.data.get('displayBorders', False),
"items": items_without_answers(),
"title": self.display_name,
"show_title": self.show_title,
"problem_text": self.question_text,
"show_problem_header": self.show_question_header,
"target_img_expanded_url": self.target_img_expanded_url,
"target_img_description": self.target_img_description,
"item_background_color": self.item_background_color or None,
"item_text_color": self.item_text_color or None,
"initial_feedback": self.data['feedback']['start'],
# final feedback (data.feedback.finish) is not included - it may give away answers.
}
def studio_view(self, context):
"""
Editing view in Studio
"""
js_templates = loader.load_unicode('/templates/html/js_templates.html')
help_texts = {
field_name: self._(field.help)
for field_name, field in self.fields.viewitems() if hasattr(field, "help")
}
context = {
'js_templates': js_templates,
'help_texts': help_texts,
'self': self,
'data': urllib.quote(json.dumps(self.data)),
}
fragment = Fragment()
fragment.add_content(loader.render_template('/templates/html/drag_and_drop_edit.html', context))
css_urls = (
'public/css/vendor/jquery-ui-1.10.4.custom.min.css',
'public/css/drag_and_drop_edit.css'
)
js_urls = (
'public/js/vendor/jquery-ui-1.10.4.custom.min.js',
'public/js/vendor/handlebars-v1.1.2.js',
'public/js/drag_and_drop_edit.js',
)
for css_url in css_urls:
fragment.add_css_url(self.runtime.local_resource_url(self, css_url))
for js_url in js_urls:
fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url))
fragment.initialize_js('DragAndDropEditBlock', {
'data': self.data,
'target_img_expanded_url': self.target_img_expanded_url,
'default_background_image_url': self.default_background_image_url,
})
return fragment
@XBlock.json_handler
def studio_submit(self, submissions, suffix=''):
self.display_name = submissions['display_name']
self.show_title = submissions['show_title']
self.question_text = submissions['problem_text']
self.show_question_header = submissions['show_problem_header']
self.weight = float(submissions['weight'])
self.item_background_color = submissions['item_background_color']
self.item_text_color = submissions['item_text_color']
self.data = submissions['data']
return {
'result': 'success',
}
@XBlock.json_handler
def do_attempt(self, attempt, suffix=''):
item = self._get_item_definition(attempt['val'])
state = None
feedback = item['feedback']['incorrect']
overall_feedback = None
is_correct = False
is_correct_location = False
if 'input' in attempt: # Student submitted numerical value for item
state = self._get_item_state().get(str(item['id']))
if state:
state['input'] = attempt['input']
is_correct_location = True
if self._is_correct_input(item, attempt['input']):
is_correct = True
feedback = item['feedback']['correct']
else:
is_correct = False
elif item['zone'] == attempt['zone']: # Student placed item in correct zone
is_correct_location = True
if 'inputOptions' in item:
# Input value will have to be provided for the item.
# It is not (yet) correct and no feedback should be shown yet.
is_correct = False
feedback = None
else:
# If this item has no input value set, we are done with it.
is_correct = True
feedback = item['feedback']['correct']
state = {
'zone': attempt['zone'],
'x_percent': attempt['x_percent'],
'y_percent': attempt['y_percent'],
}
if state:
self.item_state[str(item['id'])] = state
if self._is_finished():
overall_feedback = self.data['feedback']['finish']
# don't publish the grade if the student has already completed the problem
if not self.completed:
if self._is_finished():
self.completed = True
try:
self.runtime.publish(self, 'grade', {
'value': self._get_grade(),
'max_value': self.weight,
})
except NotImplementedError:
# Note, this publish method is unimplemented in Studio runtimes,
# so we have to figure that we're running in Studio for now
pass
self.runtime.publish(self, 'edx.drag_and_drop_v2.item.dropped', {
'item_id': item['id'],
'location': attempt.get('zone'),
'input': attempt.get('input'),
'is_correct_location': is_correct_location,
'is_correct': is_correct,
})
return {
'correct': is_correct,
'correct_location': is_correct_location,
'finished': self._is_finished(),
'overall_feedback': overall_feedback,
'feedback': feedback
}
@XBlock.json_handler
def reset(self, data, suffix=''):
self.item_state = {}
return self._get_user_state()
def _expand_static_url(self, url):
"""
This is required to make URLs like '/static/dnd-test-image.png' work (note: that is the
only portable URL format for static files that works across export/import and reruns).
This method is unfortunately a bit hackish since XBlock does not provide a low-level API
for this.
"""
if hasattr(self.runtime, 'replace_urls'):
url = self.runtime.replace_urls('"{}"'.format(url))[1:-1]
elif hasattr(self.runtime, 'course_id'):
# edX Studio uses a different runtime for 'studio_view' than 'student_view',
# and the 'studio_view' runtime doesn't provide the replace_urls API.
try:
from static_replace import replace_static_urls # pylint: disable=import-error
url = replace_static_urls('"{}"'.format(url), None, course_id=self.runtime.course_id)[1:-1]
except ImportError:
pass
return url
@XBlock.json_handler
def expand_static_url(self, url, suffix=''):
""" AJAX-accessible handler for expanding URLs to static [image] files """
return {'url': self._expand_static_url(url)}
@property
def target_img_expanded_url(self):
""" Get the expanded URL to the target image (the image items are dragged onto). """
if self.data.get("targetImg"):
return self._expand_static_url(self.data["targetImg"])
else:
return self.default_background_image_url
@property
def target_img_description(self):
""" Get the description for the target image (the image items are dragged onto). """
return self.data.get("targetImgDescription", "")
@property
def default_background_image_url(self):
""" The URL to the default background image, shown when no custom background is used """
return self.runtime.local_resource_url(self, "public/img/triangle.png")
@XBlock.handler
def get_user_state(self, request, suffix=''):
""" GET all user-specific data, and any applicable feedback """
data = self._get_user_state()
return webob.Response(body=json.dumps(data), content_type='application/json')
def _get_user_state(self):
""" Get all user-specific data, and any applicable feedback """
item_state = self._get_item_state()
for item_id, item in item_state.iteritems():
definition = self._get_item_definition(int(item_id))
item['correct_input'] = self._is_correct_input(definition, item.get('input'))
# If information about zone is missing
# (because problem was completed before a11y enhancements were implemented),
# deduce zone in which item is placed from definition:
if item.get('zone') is None:
item['zone'] = definition.get('zone', 'unknown')
is_finished = self._is_finished()
return {
'items': item_state,
'finished': is_finished,
'overall_feedback': self.data['feedback']['finish' if is_finished else 'start'],
}
def _get_item_state(self):
"""
Returns the user item state.
Converts to a dict if data is stored in legacy tuple form.
"""
state = {}
for item_id, item in self.item_state.iteritems():
if isinstance(item, dict):
state[item_id] = item
else:
state[item_id] = {'top': item[0], 'left': item[1]}
return state
def _get_item_definition(self, item_id):
"""
Returns definition (settings) for item identified by `item_id`.
"""
return next(i for i in self.data['items'] if i['id'] == item_id)
def _get_grade(self):
"""
Returns the student's grade for this block.
"""
correct_count = 0
total_count = 0
item_state = self._get_item_state()
for item in self.data['items']:
if item['zone'] != 'none':
total_count += 1
item_id = str(item['id'])
if item_id in item_state:
if self._is_correct_input(item, item_state[item_id].get('input')):
correct_count += 1
return correct_count / float(total_count) * self.weight
def _is_finished(self):
"""
All items are at their correct place and a value has been
submitted for each item that expects a value.
"""
completed_count = 0
total_count = 0
item_state = self._get_item_state()
for item in self.data['items']:
if item['zone'] != 'none':
total_count += 1
item_id = str(item['id'])
if item_id in item_state:
if 'inputOptions' in item:
if 'input' in item_state[item_id]:
completed_count += 1
else:
completed_count += 1
return completed_count == total_count
@XBlock.json_handler
def publish_event(self, data, suffix=''):
try:
event_type = data.pop('event_type')
except KeyError:
return {'result': 'error', 'message': 'Missing event_type in JSON data'}
self.runtime.publish(self, event_type, data)
return {'result': 'success'}
def _get_unique_id(self):
usage_id = self.scope_ids.usage_id
try:
return usage_id.name
except AttributeError:
# workaround for xblock workbench
return usage_id
@staticmethod
def _is_correct_input(item, val):
"""
Is submitted numerical value within the tolerated margin for this item.
"""
input_options = item.get('inputOptions')
if input_options:
try:
submitted_value = float(val)
except (ValueError, TypeError):
return False
else:
expected_value = input_options['value']
margin = input_options['margin']
return abs(submitted_value - expected_value) <= margin
else:
return True
@staticmethod
def workbench_scenarios():
"""
A canned scenario for display in the workbench.
"""
return [("Drag-and-drop-v2 scenario", "<vertical_demo><drag-and-drop-v2/></vertical_demo>")]
.xblock--drag-and-drop {
width: auto;
max-width: 770px;
margin: 0;
padding: 0;
}
.xblock--drag-and-drop .initial-load-spinner {
margin-right: 3px;
}
/* Button style tricks, defined higher so they can be overridden */
.xblock--drag-and-drop .unbutton {
background: none;
box-shadow: none;
border: none;
padding: 0;
margin: 0;
}
.xblock--drag-and-drop .unbutton:active {
background: none;
box-shadow: none;
border: none;
}
.xblock--drag-and-drop .unbutton:hover {
background: none;
box-shadow: none;
border: none;
}
.xblock--drag-and-drop .unbutton:focus {
background: none;
box-shadow: none;
border: none;
}
/* Header, instruction text, etc. */
.xblock--drag-and-drop .problem-title {
display: inline-block;
margin: 0 0 15px 0;
}
.xblock--drag-and-drop .problem p {
margin-bottom: 1.41575em;
}
/* Shared styles used in header and footer */
.xblock--drag-and-drop .title1 {
color: #555555;
text-transform: uppercase;
font-weight: bold;
font-style: normal;
margin: 10px 0;
margin-top: 20px;
}
/* drag-container holds the .items and the .target image */
.xblock--drag-and-drop .drag-container {
width: auto;
padding: 1%;
background-color: #ebf0f2;
}
/*** DRAGGABLE ITEMS ***/
.xblock--drag-and-drop .item-bank {
display: -ms-flexbox;
display: flex;
-ms-flex-flow: row wrap;
flex-flow: row wrap;
-ms-justify-content: flex-start;
justify-content: flex-start;
-ms-flex-align: center;
align-items: center;
position: relative;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
padding: 0; /* padding: 5px looks better but makes some blocks to change in size when dropped onto the target; */
}
.xblock--drag-and-drop .drag-container .option {
display: inline-block;
width: auto;
min-width: 4em;
max-width: 30%;
border: 1px solid transparent;
border-radius: 3px;
box-sizing: border-box;
margin: 5px;
padding: 10px;
text-align: center;
background-color: #1d5280;
font-size: 14px;
color: #fff;
opacity: 1;
outline-color: #fff;
outline-style: none;
/* Some versions of the drag and drop library try to fiddle with this */
z-index: 10 !important;
}
.xblock--drag-and-drop .drag-container .option.specified-width img {
width: 100%; /* If the image is smaller than the specified width, make it larger */
}
.xblock--drag-and-drop .drag-container .option.option-with-image .spinner-wrapper {
position: absolute;
float: none;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: #000;
opacity: 0.6;
color: #fff;
margin: 0;
display: flex;
justify-content: center; /* align horizontal */
align-items: center; /* align vertical */
}
.xblock--drag-and-drop .drag-container .option .item-content {
display: inline-block;
width: 100%; /* Make sure size of content never exceeds size of item */
/* (this can happen if item displays image whose width exceeds computed max-width of item) */
}
/* Placed option */
.xblock--drag-and-drop .drag-container .target .option {
position: absolute;
margin: 0;
transform: translate(-50%, -50%); /* These blocks are to be centered on their absolute x,y position */
}
/* Focused option */
.xblock--drag-and-drop .drag-container .item-bank .option:focus,
.xblock--drag-and-drop .drag-container .item-bank .option:hover,
.xblock--drag-and-drop .drag-container .item-bank .option[aria-grabbed='true'] {
outline-width: 2px;
outline-style: solid;
outline-offset: -4px;
}
.xblock--drag-and-drop .drag-container .ui-draggable-dragging.option {
box-shadow: 0 16px 32px 0 rgba(0, 0, 0, 0.3);
border: 1px solid #ccc;
opacity: .65;
z-index: 20 !important;
margin: 0; /* Allow the draggable to touch the edges of the target image */
}
.xblock--drag-and-drop .drag-container .option img {
display: inline-block;
max-width: 100%;
}
.xblock--drag-and-drop .drag-container .option .numerical-input {
height: 32px;
position: absolute;
left: calc(100% + 5px);
top: calc(50% - 16px);
}
.xblock--drag-and-drop .drag-container .option .numerical-input .spinner-wrapper {
position: absolute;
right: 0;
top: 0;
margin-right: 5px;
padding-top: 6px;
color: #333;
}
.xblock--drag-and-drop .drag-container .option .numerical-input .input {
display: inline-block;
width: 144px;
}
.xblock--drag-and-drop .drag-container .option .numerical-input .submit-input {
position: absolute;
left: 150px;
top: 4px;
white-space: nowrap; /* Fix cross-browser issue: Without this declaration, button text wraps in Chrome/Chromium */
}
.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input-submit,
.xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input-submit {
display: none;
}
.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input {
background-color: #ceffce;
color: #087108;
}
.xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input {
background-color: #ffcece;
color: #ad0d0d;
}
.xblock--drag-and-drop .drag-container .option.fade {
opacity: 0.80;
}
/*** DROP TARGET ***/
.xblock--drag-and-drop .target {
display: table;
/* 'display: table' makes this have the smallest size that fits the .target-img
while still allowing the image to use 'max-width: 100%' and scale proportionally.
The end result is that this element has the same width and height as the image, so we
can use it as a 'position: relative' anchor for the placed elements. */
height: auto;
position: relative;
margin-top: 1%;
background-color: #fff;
}
.xblock--drag-and-drop .target-img-wrapper {
/* This element is required for Firefox due to https://bugzilla.mozilla.org/show_bug.cgi?id=975632 */
display: table-row;
}
.xblock--drag-and-drop .target-img {
display: table-cell;
width: 100%;
max-width: 100%;
height: auto;
}
.xblock--drag-and-drop .zone {
position: absolute;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
/* Internet Explorer 10 */
-ms-flex-pack:center;
-ms-flex-align:center;
/* Firefox */
-moz-box-pack:center;
-moz-box-align:center;
/* Safari, Opera, and Chrome */
-webkit-box-pack:center;
-webkit-box-align:center;
/* W3C */
box-pack:center;
box-align:center;
}
.xblock--drag-and-drop .zone-with-borders {
border: 1px dotted #565656;
}
/* Focused zone */
.xblock--drag-and-drop .zone:focus {
border: 2px solid #a5a5a5;
}
.xblock--drag-and-drop .drag-container .target .zone p {
width: 100%;
font-family: Arial;
font-size: 16px;
font-weight: bold;
text-align: center;
margin-top: auto;
margin-bottom: auto;
}
/*** FEEDBACK ***/
.xblock--drag-and-drop .feedback {
margin-bottom: 1%;
border-top: solid 1px #bdbdbd;
}
.xblock--drag-and-drop .popup {
position: absolute;
display: none;
top: 5%;
right: 5%;
border: 1px solid #fff;
background-color: rgba(0, 0, 0, 0.8);
width: 500px;
max-width: 90%;
min-height: 50px;
max-height: 90%;
overflow-y: auto;
z-index: 100;
}
.xblock--drag-and-drop .popup.popup-incorrect {
background-color: rgba(173, 13, 13, 0.8);
}
.xblock--drag-and-drop .popup .popup-content {
color: #ffffff;
margin-left: 15px;
margin-top: 35px;
margin-bottom: 15px;
font-size: 14px;
}
.xblock--drag-and-drop .popup .close {
cursor: pointer;
float: right;
margin-right: 8px;
margin-top: 8px;
margin-left: 20px;
color: #ffffff;
font-family: "fontawesome";
font-size: 18pt;
}
.xblock--drag-and-drop .popup .close:focus {
outline: 2px solid white;
}
/*** KEYBOARD HELP ***/
.xblock--drag-and-drop .keyboard-help {
margin-top: 3px;
margin-bottom: 6px;
}
.xblock--drag-and-drop .keyboard-help-dialog {
position: fixed;
left: 50%;
top: 50%;
width: 1px;
height: 1px;
z-index: 1500;
}
.xblock--drag-and-drop .modal-window-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: 0.5;
z-index: 1500;
}
.xblock--drag-and-drop .modal-window {
display: none;
position: absolute;
width: 600px;
max-width: 90vw;
height: auto;
transform: translate(-50%, -50%);
box-sizing: border-box;
box-shadow: 0 0 7px rgba(0, 0, 0, 0.4);
border-radius: 4px;
padding: 7px;
background-color: #e5e5e5;
text-align: left;
direction: ltr;
z-index: 1500;
}
.xblock--drag-and-drop .modal-content {
border-radius: 5px;
background-color: #ffffff;
margin-bottom: 5px;
padding: 5px;
}
.xblock--drag-and-drop .modal-content li {
margin-left: 2%;
}
.xblock--drag-and-drop .link-button {
padding: 0;
margin: 0;
cursor: pointer;
color: #2d74b3;
font-weight: normal;
font-size: 12pt;
background: none;
box-shadow: none;
border: none;
}
.xblock--drag-and-drop .reset-button {
float: right;
margin-top: 3px;
}
.xblock--drag-and-drop .link-button:focus {
outline: 2px solid #2d74b3;
}
/* Make sure screen-reader content is hidden in the workbench: */
.xblock--drag-and-drop .sr {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
background-color: #ffffff;
color: #000000;
}
.xblock--drag-and-drop--editor {
width: 100%;
height: 100%;
}
.modal-window .drag-builder {
width: 100%;
height: calc(100% - 60px);
position: absolute;
overflow-y: scroll;
}
/*** Drop Target ***/
.xblock--drag-and-drop--editor .zone {
position: absolute;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
/* Internet Explorer 10 */
-ms-flex-pack:center;
-ms-flex-align:center;
/* Firefox */
-moz-box-pack:center;
-moz-box-align:center;
/* Safari, Opera, and Chrome */
-webkit-box-pack:center;
-webkit-box-align:center;
/* W3C */
box-pack:center;
box-align:center;
border: 1px dotted #565656;
box-sizing: border-box;
}
.xblock--drag-and-drop--editor .zone p {
width: 100%;
font-family: Arial;
font-size: 16px;
font-weight: bold;
text-align: center;
margin-top: auto;
margin-bottom: auto;
}
/** Builder **/
.xblock--drag-and-drop--editor .hidden {
display: none !important;
}
.xblock--drag-and-drop--editor .tab {
width: 100%;
background-color: #eee;
padding: 3px 0;
position: relative;
}
.xblock--drag-and-drop--editor .tab::after,
.xblock--drag-and-drop--editor .tab-footer::after {
content: "";
display: table;
clear: both;
}
.xblock--drag-and-drop--editor .tab h3,
.xblock--drag-and-drop--editor .tab .h3 {
margin: 20px 0 8px 0;
}
.xblock--drag-and-drop--editor .tab .h3 {
display: block;
font: inherit;
font-size: 100%;
}
.xblock--drag-and-drop--editor .tab-header,
.xblock--drag-and-drop--editor .tab-content,
.xblock--drag-and-drop--editor .tab-footer {
width: 96%;
margin: 2%;
}
.xblock--drag-and-drop--editor .tab-footer {
height: 25px;
position: relative;
display: block;
float: left;
}
.xblock--drag-and-drop--editor .items {
width: calc(100% - 515px);
margin: 10px 0 0 0;
}
.xblock--drag-and-drop--editor .target-image-form input,
.xblock--drag-and-drop--editor .target-image-form textarea {
width: 50%;
}
/* Zones Tab */
.xblock--drag-and-drop--editor .zones-tab .zone-editor {
position: relative;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: flex-start;
justify-content: space-between;
}
.xblock--drag-and-drop--editor .zones-tab .tab-content .controls {
width: 40%;
max-width: 50%;
min-width: 330px;
margin-right: 15px;
}
.xblock--drag-and-drop--editor .zones-tab .tab-content .target {
position: relative;
border: 1px solid #ccc;
overflow: hidden;
}
.xblock--drag-and-drop--editor .zones-tab .tab-content .target-img {
display: block;
width: auto;
height: auto;
max-width: 100%;
}
.xblock--drag-and-drop--editor .zones-form .zone-row label {
display: inline-block;
width: 18%;
}
.xblock--drag-and-drop--editor .zones-form .zone-row > 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;
here we make sure that both input fields get the same value for line-height */
}
.xblock--drag-and-drop--editor .zones-form .zone-row .layout {
margin-bottom: 15px;
}
.xblock--drag-and-drop--editor .zones-form .zone-row .layout .size,
.xblock--drag-and-drop--editor .zones-form .zone-row .layout .coord {
width: 15%;
margin: 0 19px 5px 0;
}
.xblock--drag-and-drop--editor .feedback-form textarea {
width: 99%;
height: 128px;
}
.xblock--drag-and-drop--editor .target-image-form .target-image-form-help,
.xblock--drag-and-drop--editor .item-styles-form .item-styles-form-help {
margin-top: 5px;
font-size: small;
}
.xblock--drag-and-drop--editor .item-styles-form,
.xblock--drag-and-drop--editor .items-form {
margin-bottom: 30px;
}
.xblock--drag-and-drop--editor .items-form .item {
background-color: #8fcaec;
padding: 10px 0 1px;
margin: 15px 0;
}
.xblock--drag-and-drop--editor .items-form label {
margin: 0 1%;
}
.xblock--drag-and-drop--editor .items-form input,
.xblock--drag-and-drop--editor .items-form select {
width: 35%;
}
.xblock--drag-and-drop--editor .items-form .item-image-url {
width: 81%;
margin-right: 1%;
}
.xblock--drag-and-drop--editor .items-form .item-width {
width: 50px;
}
.xblock--drag-and-drop--editor .items-form .item-numerical-value,
.xblock--drag-and-drop--editor .items-form .item-numerical-margin {
margin: 0 1%;
width: 50%;
}
.xblock--drag-and-drop--editor .items-form textarea {
width: 97%;
margin: 0 1%;
}
.xblock--drag-and-drop--editor .items-form .row {
margin-bottom: 20px;
}
.xblock--drag-and-drop--editor .items-form .row.advanced {
display: none;
}
.xblock--drag-and-drop--editor .items-form .row.advanced-link {
padding-left: 1em;
font-size: 80%;
}
/** Buttons **/
.xblock--drag-and-drop--editor .btn {
background-color: #1d5280;
color: #fff;
border: 1px solid #156ab4;
border-radius: 6px;
padding: 5px 10px;
margin-top: 15px;
}
.xblock--drag-and-drop--editor .btn:hover {
opacity: 0.8;
cursor: pointer;
}
.xblock--drag-and-drop--editor .btn:focus {
outline: none;
opacity: 0.5;
}
.xblock--drag-and-drop--editor .add-element {
text-decoration: none;
color: #1d5280;
}
.xblock--drag-and-drop--editor .remove-zone {
float: right;
margin-top: 2px;
margin-right: 16px;
}
.xblock--drag-and-drop--editor .remove-item {
display: inline-block;
margin-left: 95px;
}
.xblock--drag-and-drop--editor .icon {
width: 14px;
height: 14px;
border-radius: 7px;
background-color: #1d5280;
position: relative;
float: left;
margin: 0 5px 0 0;
}
.xblock--drag-and-drop--editor .add-zone:hover,
.xblock--drag-and-drop--editor .add-zone:hover .icon,
.xblock--drag-and-drop--editor .remove-zone:hover,
.xblock--drag-and-drop--editor .remove-zone:hover .icon {
opacity: 0.7;
}
.xblock--drag-and-drop--editor .tab .field-error {
outline: none;
box-shadow: 0 0 10px darkred;
}
.xblock--drag-and-drop--editor .icon.add:before {
content: '';
height: 10px;
width: 2px;
background-color: #fff;
position: relative;
display: inline;
float: left;
top: 2px;
left: 6px;
}
.xblock--drag-and-drop--editor .icon.add:after {
content: '';
height: 2px;
width: 10px;
background-color: #fff;
position: relative;
display: inline;
float: left;
top: 6px;
left: 0;
}
.xblock--drag-and-drop--editor .icon.remove:before {
content: '';
height: 10px;
width: 2px;
background-color: #fff;
position: relative;
display: inline;
float: left;
top: 2px;
left: 6px;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.xblock--drag-and-drop--editor .icon.remove:after {
content: '';
height: 2px;
width: 10px;
background-color: #fff;
position: relative;
display: inline;
float: left;
top: 6px;
left: 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.xblock--drag-and-drop--editor .remove-item .icon.remove {
background-color: #fff;
color: #0072a7; /* Override default color from Studio to ensure contrast is large enough */
}
.xblock--drag-and-drop--editor .remove-item .icon.remove:before,
.xblock--drag-and-drop--editor .remove-item .icon.remove:after {
background-color: #1d5280;
}
/*! jQuery UI - v1.10.4 - 2014-07-07
* http://jqueryui.com
* Includes: jquery.ui.core.css, jquery.ui.resizable.css, jquery.ui.selectable.css, jquery.ui.button.css, jquery.ui.dialog.css, jquery.ui.theme.css
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url("images/ui-bg_flat_75_ffffff_40x100.png") 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url("images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url("images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url("images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url("images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url("images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_888888_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_2e83ff_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cd0a0a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url("images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url("images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px}
\ No newline at end of file
function DragNDropTemplates(url_name) {
"use strict";
var h = virtualDom.h;
// Set up a mock for gettext if it isn't available in the client runtime:
if (!window.gettext) { window.gettext = function gettext_stub(string) { return string; }; }
var FocusHook = function() {
if (!(this instanceof FocusHook)) {
return new FocusHook();
}
};
FocusHook.prototype.hook = function(node, prop, prev) {
setTimeout(function() {
if (document.activeElement !== node) {
node.focus();
}
}, 0);
};
var itemSpinnerTemplate = function(xhr_active) {
if (!xhr_active) {
return null;
}
return (h(
"div.spinner-wrapper",
[
h("i.fa.fa-spin.fa-spinner")
]
));
};
var renderCollection = function(template, collection, ctx) {
return collection.map(function(item) {
return template(item, ctx);
});
};
var itemInputTemplate = function(input) {
if (!input) {
return null;
}
var focus_hook = input.has_value ? undefined : FocusHook();
return (
h('div.numerical-input', {className: input.class_name,
style: {display: input.is_visible ? 'block' : 'none'}}, [
h('input.input', {type: 'text', value: input.value, disabled: input.has_value,
focusHook: focus_hook}),
itemSpinnerTemplate(input.xhr_active),
h('button.submit-input', {disabled: input.has_value}, gettext('ok'))
])
);
};
var itemTemplate = function(item, ctx) {
// Define properties
var className = (item.class_name) ? item.class_name : "";
if (item.has_image) {
className += " " + "option-with-image";
}
if (item.widthPercent) {
className += " specified-width"; // The author has specified a width for this item.
}
var attributes = {
'role': 'button',
'draggable': !item.drag_disabled,
'aria-grabbed': item.grabbed,
'data-value': item.value,
'data-drag-disabled': item.drag_disabled
};
var style = {};
if (item.background_color) {
style['background-color'] = item.background_color;
}
if (item.color) {
style.color = item.color;
// Ensure contrast between outline-color and background color
// matches contrast between text color and background color:
style['outline-color'] = item.color;
}
if (item.is_placed) {
style.left = item.x_percent + "%";
style.top = item.y_percent + "%";
if (item.widthPercent) {
style.width = item.widthPercent + "%";
style.maxWidth = item.widthPercent + "%"; // default maxWidth is ~33%
} else if (item.imgNaturalWidth) {
style.width = (item.imgNaturalWidth + 22) + "px"; // 22px is for 10px padding + 1px border each side
// ^ Hack to detect image width at runtime and make webkit consistent with Firefox
}
} else {
// If an item has not been placed it must be possible to move focus to it using the keyboard:
attributes.tabindex = 0;
if (item.widthPercent) {
// The item bank container is often wider than the background image, and the
// widthPercent is specified relative to the background image so we have to
// convert it to pixels. But if the browser window / mobile screen is not as
// wide as the image, then the background image will be scaled down and this
// pixel value would be too large, so we also specify it as a max-width
// percentage.
style.width = (item.widthPercent / 100 * ctx.bg_image_width) + "px";
style.maxWidth = item.widthPercent + "%";
}
}
// Define children
var children = [
itemSpinnerTemplate(item.xhr_active),
itemInputTemplate(item.input)
];
var item_content_html = item.displayName;
if (item.imageURL) {
item_content_html = '<img src="' + item.imageURL + '" alt="' + item.imageDescription + '" />';
}
var item_content = h('div', { innerHTML: item_content_html, className: "item-content" });
if (item.is_placed) {
// Insert information about zone in which this item has been placed
var item_description_id = url_name + '-item-' + item.value + '-description';
item_content.properties.attributes = { 'aria-describedby': item_description_id };
var item_description = h(
'div',
{ id: item_description_id, className: 'sr' },
gettext('Correctly placed in: ') + item.zone
);
children.splice(1, 0, item_description);
}
children.splice(1, 0, item_content);
return (
h(
'div.option',
{
// Unique key for virtual dom change tracking. Key must be different for
// Placed vs Unplaced, or weird bugs can occur.
key: item.value + (item.is_placed ? "-p" : "-u"),
className: className,
attributes: attributes,
style: style
},
children
)
);
};
var zoneTemplate = function(zone, ctx) {
var className = ctx.display_zone_labels ? 'zone-name' : 'zone-name sr';
var selector = ctx.display_zone_borders ? 'div.zone.zone-with-borders' : 'div.zone';
return (
h(
selector,
{
id: zone.id,
attributes: {
'tabindex': 0,
'dropzone': 'move',
'aria-dropeffect': 'move',
'data-zone': zone.title,
'role': 'button',
},
style: {
top: zone.y_percent + '%', left: zone.x_percent + "%",
width: zone.width_percent + '%', height: zone.height_percent + "%",
}
},
[
h('p', { className: className }, zone.title),
h('p', { className: 'zone-description sr' }, zone.description)
]
)
);
};
var feedbackTemplate = function(ctx) {
var feedback_display = ctx.feedback_html ? 'block' : 'none';
var reset_button_display = ctx.display_reset_button ? 'block' : 'none';
var properties = { attributes: { 'aria-live': 'polite' } };
return (
h('section.feedback', properties, [
h(
'button.reset-button.unbutton.link-button',
{ style: { display: reset_button_display }, attributes: { tabindex: 0 }, 'aria-live': 'off'},
gettext('Reset problem')
),
h('h3.title1', { style: { display: feedback_display } }, gettext('Feedback')),
h('p.message', { style: { display: feedback_display }, innerHTML: ctx.feedback_html })
])
);
};
var keyboardHelpTemplate = function(ctx) {
var dialog_attributes = { role: 'dialog', 'aria-labelledby': 'modal-window-title' };
var dialog_style = {};
return (
h('section.keyboard-help', [
h('button.keyboard-help-button.unbutton.link-button', { attributes: { tabindex: 0 } }, gettext('Keyboard Help')),
h('div.keyboard-help-dialog', [
h('div.modal-window-overlay'),
h('div.modal-window', { attributes: dialog_attributes, style: dialog_style }, [
h('div.modal-header', [
h('h2.modal-window-title', gettext('Keyboard Help'))
]),
h('div.modal-content', [
h('p', gettext('You can complete this problem using only your keyboard.')),
h('ul', [
h('li', gettext('Use "Tab" and "Shift-Tab" to navigate between items and zones.')),
h('li', gettext('Press "Enter", "Space", "Ctrl-m", or "⌘-m" on an item to select it for dropping, then navigate to the zone you want to drop it on.')),
h('li', gettext('Press "Enter", "Space", "Ctrl-m", or "⌘-m" to drop the item on the current zone.')),
h('li', gettext('Press "Esc" if you want to cancel the drop operation (for example, to select a different item).')),
])
]),
h('div.modal-actions', [
h('button.modal-dismiss-button', gettext("OK"))
])
])
])
])
);
};
var mainTemplate = function(ctx) {
var problemTitle = ctx.show_title ? h('h2.problem-title', {innerHTML: ctx.title_html}) : null;
var problemHeader = ctx.show_problem_header ? h('h3.title1', gettext('Problem')) : null;
var popupSelector = 'div.popup';
if (ctx.popup_html && !ctx.last_action_correct) {
popupSelector += '.popup-incorrect';
}
var is_item_placed = function(i) { return i.is_placed; };
var items_placed = $.grep(ctx.items, is_item_placed);
var items_in_bank = $.grep(ctx.items, is_item_placed, true);
return (
h('section.themed-xblock.xblock--drag-and-drop', [
problemTitle,
h('section.problem', [
problemHeader,
h('p', {innerHTML: ctx.problem_html}),
]),
h('section.drag-container', { attributes: { role: 'application' } }, [
h(
'div.item-bank',
renderCollection(itemTemplate, items_in_bank, ctx)
),
h('div.target',
{
attributes: {
'aria-live': 'polite',
'aria-atomic': 'true',
'aria-relevant': 'additions',
},
},
[
h(
popupSelector,
{
style: {display: ctx.popup_html ? 'block' : 'none'},
},
[
h('div.close.icon-remove-sign.fa-times-circle'),
h('p.popup-content', {innerHTML: ctx.popup_html}),
]
),
h('div.target-img-wrapper', [
h('img.target-img', {src: ctx.target_img_src, alt: ctx.target_img_description}),
]
),
renderCollection(zoneTemplate, ctx.zones, ctx),
renderCollection(itemTemplate, items_placed, ctx),
]
),
]),
keyboardHelpTemplate(ctx),
feedbackTemplate(ctx),
])
);
};
DragAndDropBlock.renderView = mainTemplate;
}
function DragAndDropBlock(runtime, element, configuration) {
"use strict";
DragNDropTemplates(configuration.url_name);
// Set up a mock for gettext if it isn't available in the client runtime:
if (!window.gettext) { window.gettext = function gettext_stub(string) { return string; }; }
var $element = $(element);
// root: root node managed by the virtual DOM
var $root = $element.find('.xblock--drag-and-drop');
var root = $root[0];
var state = undefined;
var bgImgNaturalWidth = undefined; // pixel width of the background image (when not scaled)
var __vdom = virtualDom.h(); // blank virtual DOM
// Event string size limit.
var MAX_LENGTH = 255;
// Keyboard accessibility
var ESC = 27;
var RET = 13;
var SPC = 32;
var TAB = 9;
var M = 77;
var placementMode = false;
var $selectedItem;
var $focusedElement;
var init = function() {
// Load the current user state, and load the image, then render the block.
// We load the user state via AJAX rather than passing it in statically (like we do with
// configuration) due to how the LMS handles unit tabs. If you click on a unit with this
// block, make changes, click on the tab for another unit, then click back, this block
// would re-initialize with the old state. To avoid that, we always fetch the state
// using AJAX during initialization.
$.when(
$.ajax(runtime.handlerUrl(element, 'get_user_state'), {dataType: 'json'}),
loadBackgroundImage()
).done(function(stateResult, bgImg){
// Render problem
configuration.zones.forEach(function (zone) {
computeZoneDimension(zone, bgImg.width, bgImg.height);
});
state = stateResult[0]; // stateResult is an array of [data, statusText, jqXHR]
migrateConfiguration(bgImg.width);
migrateState(bgImg.width, bgImg.height);
bgImgNaturalWidth = bgImg.width;
// Set up event handlers:
$(document).on('keydown mousedown touchstart', closePopup);
$element.on('click', '.keyboard-help-button', showKeyboardHelp);
$element.on('keydown', '.keyboard-help-button', function(evt) {
runOnKey(evt, RET, showKeyboardHelp);
});
$element.on('click', '.reset-button', resetProblem);
$element.on('keydown', '.reset-button', function(evt) {
runOnKey(evt, RET, resetProblem);
});
$element.on('click', '.submit-input', submitInput);
// For the next one, we need to use addEventListener with useCapture 'true' in order
// to watch for load events on any child element, since load events do not bubble.
element.addEventListener('load', webkitFix, true);
applyState();
initDroppable();
// Indicate that problem is done loading
publishEvent({event_type: 'edx.drag_and_drop_v2.loaded'});
}).fail(function() {
$root.text(gettext("An error occurred. Unable to load drag and drop problem."));
});
};
var runOnKey = function(evt, key, handler) {
if (evt.which === key) {
handler(evt);
}
};
var keyboardEventDispatcher = function(evt) {
if (evt.which === TAB) {
trapFocus(evt);
} else if (evt.which === ESC) {
hideKeyboardHelp(evt);
}
};
var trapFocus = function(evt) {
if (evt.which === TAB) {
evt.preventDefault();
focusModalButton();
}
};
var truncateField = function(data, fieldName){
if (data[fieldName].length > MAX_LENGTH) {
data[fieldName] = data[fieldName].substring(0, MAX_LENGTH);
data['truncated'] = true;
} else {
data['truncated'] = false;
}
};
var focusModalButton = function() {
$root.find('.keyboard-help-dialog .modal-dismiss-button ').focus();
};
var showKeyboardHelp = function(evt) {
evt.preventDefault();
// Show dialog
var $keyboardHelpDialog = $root.find('.keyboard-help-dialog');
$keyboardHelpDialog.find('.modal-window-overlay').show();
$keyboardHelpDialog.find('.modal-window').show();
// Handle focus
$focusedElement = $(':focus');
focusModalButton();
// Set up event handlers
$(document).on('keydown', keyboardEventDispatcher);
$keyboardHelpDialog.find('.modal-dismiss-button').on('click', hideKeyboardHelp);
};
var hideKeyboardHelp = function(evt) {
evt.preventDefault();
// Hide dialog
var $keyboardHelpDialog = $root.find('.keyboard-help-dialog');
$keyboardHelpDialog.find('.modal-window-overlay').hide();
$keyboardHelpDialog.find('.modal-window').hide();
// Handle focus
$focusedElement.focus();
// Remove event handlers
$(document).off('keydown', keyboardEventDispatcher);
$keyboardHelpDialog.find('.modal-dismiss-button').off();
};
/** Asynchronously load the main background image used for this block. */
var loadBackgroundImage = function() {
var promise = $.Deferred();
var img = new Image();
img.addEventListener("load", function() {
if (img.width > 0 && img.height > 0) {
promise.resolve(img);
} else {
promise.reject();
}
}, false);
img.addEventListener("error", function() { promise.reject(); });
img.src = configuration.target_img_expanded_url;
img.alt = configuration.target_img_description;
return promise;
};
/** Zones are specified in the configuration via pixel values - convert to percentages */
var computeZoneDimension = function(zone, bg_image_width, bg_image_height) {
if (zone.x_percent === undefined) {
// We can assume that if 'x_percent' is not set, 'y_percent', 'width_percent', and
// 'height_percent' will also not be set.
zone.x_percent = (+zone.x) / bg_image_width * 100;
delete zone.x;
zone.y_percent = (+zone.y) / bg_image_height * 100;
delete zone.y;
zone.width_percent = (+zone.width) / bg_image_width * 100;
delete zone.width;
zone.height_percent = (+zone.height) / bg_image_height * 100;
delete zone.height;
zone.id = configuration.url_name + '-' + zone.id;
}
};
/**
* webkitFix:
* When our draggables do not have a width specified by the author, we want them sized using
* the following algorithm: "be as wide as possible but never wider than ~30% of the
* background image width and never wider than the natural size of the text or image
* that is this draggable's content." (this works well for both desktop and mobile)
*
* The current CSS rules to achieve this work fine for draggables in the "tray" at the top,
* but when they are placed (position:absolute), there seems to be no way to achieve this
* that works consistently in both Webkit and firefox. (Using display: table works in Webkit
* but not Firefox; using the current CSS works in Firefox but not Webkit. This is due to
* the amiguous nature of 'max-width' when refering to a parent whose width is computed from
* the child (<div style='width: auto;'><img style='width:auto; max-width: x%;'></div>)
*
* This workaround simply detects the image width when any image loads, then sets the width
* on the [grand]parent element, resolving the ambiguity.
*/
var webkitFix = function(event) {
var $img = $(event.target);
var $option = $img.parent().parent();
if (!$option.is('.option')) {
return;
}
var itemId = $option.data('value');
configuration.items.forEach(function(item) {
if (item.id == itemId) {
item.imgNaturalWidth = event.target.naturalWidth;
}
});
setTimeout(applyState, 0); // Apply changes to the DOM after the event handling completes.
};
var previousFeedback = undefined;
/**
* Update the DOM to reflect 'state'.
*/
var applyState = function() {
// Has the feedback popup been closed?
if (state.closing) {
var data = {
event_type: 'edx.drag_and_drop_v2.feedback.closed',
content: previousFeedback || state.feedback,
manually: state.manually_closed,
};
truncateField(data, 'content');
publishEvent(data);
delete state.feedback;
delete state.closing;
}
// Has feedback been set?
if (state.feedback) {
var data = {
event_type: 'edx.drag_and_drop_v2.feedback.opened',
content: state.feedback,
};
truncateField(data, 'content');
publishEvent(data);
}
updateDOM();
destroyDraggable();
if (!state.finished) {
initDraggable();
}
};
var updateDOM = function(state) {
var new_vdom = render(state);
var patches = virtualDom.diff(__vdom, new_vdom);
root = virtualDom.patch(root, patches);
$root = $(root);
__vdom = new_vdom;
};
var publishEvent = function(data) {
$.ajax({
type: 'POST',
url: runtime.handlerUrl(element, 'publish_event'),
data: JSON.stringify(data)
});
};
var isCycleKey = function(evt) {
return !evt.ctrlKey && !evt.metaKey && evt.which === TAB;
};
var isCancelKey = function(evt) {
return !evt.ctrlKey && !evt.metaKey && evt.which === ESC;
};
var isActionKey = function(evt) {
var key = evt.which;
if (evt.ctrlKey || evt.metaKey) {
return key === M;
}
return key === RET || key === SPC;
};
var focusNextZone = function(evt, $currentZone) {
if (evt.shiftKey) { // Going backward
var isFirstZone = $currentZone.prev('.zone').length === 0;
if (isFirstZone) {
evt.preventDefault();
$root.find('.target .zone').last().focus();
}
} else { // Going forward
var isLastZone = $currentZone.next('.zone').length === 0;
if (isLastZone) {
evt.preventDefault();
$root.find('.target .zone').first().focus();
}
}
};
var placeItem = function($zone, $item) {
var item_id;
var $anchor;
if ($item !== undefined) {
item_id = $item.data('value');
// Element was placed using the mouse,
// so use relevant properties of *item* when calculating new position below.
$anchor = $item;
} else {
item_id = $selectedItem.data('value');
// Element was placed using the keyboard,
// so use relevant properties of *zone* when calculating new position below.
$anchor = $zone;
}
var zone = $zone.data('zone');
var $target_img = $root.find('.target-img');
// Calculate the position of the item to place relative to the image.
var x_pos = $anchor.offset().left + ($anchor.outerWidth()/2) - $target_img.offset().left;
var y_pos = $anchor.offset().top + ($anchor.outerHeight()/2) - $target_img.offset().top;
var x_pos_percent = x_pos / $target_img.width() * 100;
var y_pos_percent = y_pos / $target_img.height() * 100;
state.items[item_id] = {
zone: zone,
x_percent: x_pos_percent,
y_percent: y_pos_percent,
submitting_location: true,
};
// Wrap in setTimeout to let the droppable event finish.
setTimeout(function() {
applyState();
submitLocation(item_id, zone, x_pos_percent, y_pos_percent);
}, 0);
};
var initDroppable = function() {
// Set up zones for keyboard interaction
$root.find('.zone').each(function() {
var $zone = $(this);
$zone.on('keydown', function(evt) {
if (placementMode) {
if (isCycleKey(evt)) {
focusNextZone(evt, $zone);
} else if (isCancelKey(evt)) {
evt.preventDefault();
placementMode = false;
releaseItem($selectedItem);
} else if (isActionKey(evt)) {
evt.preventDefault();
placementMode = false;
placeItem($zone);
releaseItem($selectedItem);
}
}
});
});
// Make zone accept items that are dropped using the mouse
$root.find('.zone').droppable({
accept: '.item-bank .option',
tolerance: 'pointer',
drop: function(evt, ui) {
var $zone = $(this);
var $item = ui.helper;
placeItem($zone, $item);
}
});
};
var initDraggable = function() {
$root.find('.item-bank .option').not('[data-drag-disabled=true]').each(function() {
var $item = $(this);
// Allow item to be "picked up" using the keyboard
$item.on('keydown', function(evt) {
if (isActionKey(evt)) {
evt.preventDefault();
placementMode = true;
grabItem($item);
$selectedItem = $item;
$root.find('.target .zone').first().focus();
}
});
// Make item draggable using the mouse
try {
$item.draggable({
containment: $root.find('.drag-container'),
cursor: 'move',
stack: $root.find('.item-bank .option'),
revert: 'invalid',
revertDuration: 150,
start: function(evt, ui) {
var $item = $(this);
grabItem($item);
publishEvent({
event_type: 'edx.drag_and_drop_v2.item.picked_up',
item_id: $item.data('value'),
});
},
stop: function(evt, ui) {
releaseItem($(this));
}
});
} catch (e) {
// Initializing the draggable will fail if draggable was already
// initialized. That's expected, ignore the exception.
}
});
};
var grabItem = function($item) {
var item_id = $item.data('value');
setGrabbedState(item_id, true);
updateDOM();
};
var releaseItem = function($item) {
var item_id = $item.data('value');
setGrabbedState(item_id, false);
updateDOM();
};
var setGrabbedState = function(item_id, grabbed) {
for (var i = 0; i < configuration.items.length; i++) {
if (configuration.items[i].id === item_id) {
configuration.items[i].grabbed = grabbed;
}
}
};
var destroyDraggable = function() {
$root.find('.item-bank .option[data-drag-disabled=true]').each(function() {
var $item = $(this);
$item.off();
try {
$item.draggable('destroy');
} catch (e) {
// Destroying the draggable will fail if draggable was
// not initialized in the first place. Ignore the exception.
}
});
};
var submitLocation = function(item_id, zone, x_percent, y_percent) {
if (!zone) {
return;
}
var url = runtime.handlerUrl(element, 'do_attempt');
var data = {
val: item_id,
zone: zone,
x_percent: x_percent,
y_percent: y_percent,
};
$.post(url, JSON.stringify(data), 'json')
.done(function(data){
state.last_action_correct = data.correct_location;
if (data.correct_location) {
state.items[item_id].correct_input = Boolean(data.correct);
state.items[item_id].submitting_location = false;
} else {
delete state.items[item_id];
}
state.feedback = data.feedback;
if (data.finished) {
state.finished = true;
state.overall_feedback = data.overall_feedback;
}
applyState();
})
.fail(function (data) {
delete state.items[item_id];
applyState();
});
};
var submitInput = function(evt) {
var item = $(evt.target).closest('.option');
var input_div = item.find('.numerical-input');
var input = input_div.find('.input');
var input_value = input.val();
var item_id = item.data('value');
if (!input_value) {
// Don't submit if the user didn't enter anything yet.
return;
}
state.items[item_id].input = input_value;
state.items[item_id].submitting_input = true;
applyState();
var url = runtime.handlerUrl(element, 'do_attempt');
var data = {val: item_id, input: input_value};
$.post(url, JSON.stringify(data), 'json')
.done(function(data) {
state.last_action_correct = data.correct;
state.items[item_id].submitting_input = false;
state.items[item_id].correct_input = data.correct;
state.feedback = data.feedback;
if (data.finished) {
state.finished = true;
state.overall_feedback = data.overall_feedback;
}
applyState();
})
.fail(function(data) {
state.items[item_id].submitting_input = false;
applyState();
});
};
var closePopup = function(evt) {
if (!state.feedback) {
return;
}
var target = $(evt.target);
var popup_box = '.xblock--drag-and-drop .popup';
var close_button = '.xblock--drag-and-drop .popup .close';
var submit_input_button = '.xblock--drag-and-drop .submit-input';
if (target.is(popup_box) || target.is(submit_input_button)) {
return;
}
if (target.parents(popup_box).length && !target.is(close_button)) {
return;
}
state.closing = true;
previousFeedback = state.feedback;
if (target.is(close_button)) {
state.manually_closed = true;
} else {
state.manually_closed = false;
}
applyState();
};
var resetProblem = function(evt) {
evt.preventDefault();
$.ajax({
type: 'POST',
url: runtime.handlerUrl(element, 'reset'),
data: '{}',
});
state = {
'items': [],
'finished': false,
'overall_feedback': configuration.initial_feedback,
};
applyState();
};
var render = function() {
var items = configuration.items.map(function(item) {
var input = null;
var item_user_state = state.items[item.id];
if (item.inputOptions) {
input = {
is_visible: item_user_state && !item_user_state.submitting_location,
has_value: Boolean(item_user_state && 'input' in item_user_state),
value : (item_user_state && item_user_state.input) || '',
class_name: undefined,
xhr_active: (item_user_state && item_user_state.submitting_input)
};
if (input.has_value && !item_user_state.submitting_input) {
input.class_name = item_user_state.correct_input ? 'correct' : 'incorrect';
}
}
var imageURL = item.imageURL || item.backgroundImage; // Fall back on "backgroundImage" to be backward-compatible
var grabbed = false;
if (item.grabbed !== undefined) {
grabbed = item.grabbed;
}
var placed = item_user_state && ('input' in item_user_state || item_user_state.correct_input);
var itemProperties = {
value: item.id,
drag_disabled: Boolean(item_user_state || state.finished),
class_name: placed || state.finished ? 'fade' : undefined,
xhr_active: (item_user_state && item_user_state.submitting_location),
input: input,
displayName: item.displayName,
imageURL: imageURL,
imageDescription: item.imageDescription,
has_image: !!imageURL,
grabbed: grabbed,
widthPercent: item.widthPercent, // widthPercent may be undefined (auto width)
imgNaturalWidth: item.imgNaturalWidth,
};
if (item_user_state) {
itemProperties.is_placed = true;
itemProperties.zone = item_user_state.zone;
itemProperties.x_percent = item_user_state.x_percent;
itemProperties.y_percent = item_user_state.y_percent;
}
if (configuration.item_background_color) {
itemProperties.background_color = configuration.item_background_color;
}
if (configuration.item_text_color) {
itemProperties.color = configuration.item_text_color;
}
return itemProperties;
});
var context = {
// configuration - parts that never change:
bg_image_width: bgImgNaturalWidth, // Not stored in configuration since it's unknown on the server side
title_html: configuration.title,
show_title: configuration.show_title,
problem_html: configuration.problem_text,
show_problem_header: configuration.show_problem_header,
target_img_src: configuration.target_img_expanded_url,
target_img_description: configuration.target_img_description,
display_zone_labels: configuration.display_zone_labels,
display_zone_borders: configuration.display_zone_borders,
zones: configuration.zones,
items: items,
// state - parts that can change:
last_action_correct: state.last_action_correct,
popup_html: state.feedback || '',
feedback_html: $.trim(state.overall_feedback),
display_reset_button: Object.keys(state.items).length > 0,
};
return DragAndDropBlock.renderView(context);
};
/**
* migrateConfiguration: Apply any changes to support older versions of the configuration.
* We have to do this in JS, not python, since some migrations depend on the image size,
* which is not known in Python-land.
*/
var migrateConfiguration = function(bg_image_width) {
for (var i in configuration.items) {
var item = configuration.items[i];
// Convert from old-style pixel widths to new-style percentage widths:
if (item.widthPercent === undefined && item.size && parseInt(item.size.width) > 0) {
item.widthPercent = parseInt(item.size.width) / bg_image_width * 100;
}
}
};
/**
* migrateState: Apply any changes necessary to support the 'state' format used by older
* versions of this XBlock.
* We have to do this in JS, not python, since some migrations depend on the image size,
* which is not known in Python-land.
*/
var migrateState = function(bg_image_width, bg_image_height) {
Object.keys(state.items).forEach(function(item_id) {
var item = state.items[item_id];
if (item.x_percent === undefined) {
// Find the matching item in the configuration
var width = 190;
var height = 44;
for (var i in configuration.items) {
if (configuration.items[i].id === +item_id) {
var size = configuration.items[i].size;
// size is an object like '{width: "50px", height: "auto"}'
if (parseInt(size.width ) > 0) { width = parseInt(size.width); }
if (parseInt(size.height) > 0) { height = parseInt(size.height); }
break;
}
}
// Update the user's item state to use centered relative coordinates
var left_px = parseFloat(item.left) - 220; // 220 px for the items container that used to be on the left
var top_px = parseFloat(item.top);
item.x_percent = (left_px + width/2) / bg_image_width * 100;
item.y_percent = (top_px + height/2) / bg_image_height * 100;
delete item.left;
delete item.top;
delete item.absolute;
}
});
};
init();
}
function DragAndDropEditBlock(runtime, element, params) {
// Set up gettext in case it isn't available in the client runtime:
if (typeof gettext == "undefined") {
window.gettext = function gettext_stub(string) { return string; };
}
// Make gettext available in Handlebars templates
Handlebars.registerHelper('i18n', function(str) { return gettext(str); });
// Numeric rounding in Handlebars templates
Handlebars.registerHelper('singleDecimalFloat', function(value) {
if (value === "" || isNaN(Number(value))) {
return "";
}
return Number(value).toFixed(Number(value) == parseInt(value) ? 0 : 1);
});
var $element = $(element);
var dragAndDrop = (function($) {
var _fn = {
// Templates
tpl: {
init: function() {
_fn.tpl = {
zoneInput: Handlebars.compile($("#zone-input-tpl", element).html()),
zoneElement: Handlebars.compile($("#zone-element-tpl", element).html()),
zoneDropdown: Handlebars.compile($("#zone-dropdown-tpl", element).html()),
itemInput: Handlebars.compile($("#item-input-tpl", element).html()),
};
}
},
build: {
$el: {
feedback: {
form: $('.drag-builder .feedback-form', element),
tab: $('.drag-builder .feedback-tab', element)
},
zones: {
form: $('.drag-builder .zones-form', element),
tab: $('.drag-builder .zones-tab', element)
},
items: {
form: $('.drag-builder .items-form', element),
tab: $('.drag-builder .items-tab', element)
},
targetImage: $('.drag-builder .target .target-img', element),
zonesPreview: $('.drag-builder .target .zones-preview', element),
},
init: function() {
_fn.data = params.data;
// Compile templates
_fn.tpl.init();
// Display target image
_fn.build.$el.targetImage.show();
_fn.build.clickHandlers();
},
validate: function() {
var fields = $element.find('.tab').not('.hidden').find('input, textarea');
var success = true;
fields.each(function(index, field) {
field = $(field);
// Right now our only check is if a field is set or not.
field.removeClass('field-error');
if (! field[0].checkValidity()) {
field.addClass('field-error');
success = false;
}
});
if (! success) {
runtime.notify('error', {
'title': window.gettext("There was an error with your form."),
'message': window.gettext("Please check over your submission.")
});
}
return success
},
clickHandlers: function() {
var $fbkTab = _fn.build.$el.feedback.tab,
$zoneTab = _fn.build.$el.zones.tab,
$itemTab = _fn.build.$el.items.tab;
var self = this;
$element.one('click', '.continue-button', function loadSecondTab(e) {
// $fbkTab -> $zoneTab
e.preventDefault();
if (!self.validate()) {
$(e.target).one('click', loadSecondTab);
return
}
_fn.build.form.feedback(_fn.build.$el.feedback.form);
for (var i = 0; i < _fn.data.zones.length; i++) {
_fn.build.form.zone.add(_fn.data.zones[i]);
}
if (_fn.data.zones.length === 0) {
_fn.build.form.zone.add();
}
// 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);
_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);
if (_fn.data.displayLabels) {
$('.display-labels-form input', element).prop('checked', true);
}
if (_fn.data.displayBorders) {
$('.display-borders-form input', element).prop('checked', true);
}
$fbkTab.addClass('hidden');
$zoneTab.removeClass('hidden');
$(this).one('click', function loadThirdTab(e) {
// $zoneTab -> $itemTab
e.preventDefault();
if (!self.validate()) {
$(e.target).one('click', loadThirdTab);
return
}
for (var i = 0; i < _fn.data.items.length; i++) {
_fn.build.form.item.add(_fn.data.items[i]);
}
if (_fn.data.items.length === 0) {
_fn.build.form.item.add();
}
$zoneTab.addClass('hidden');
$itemTab.removeClass('hidden');
$(this).addClass('hidden');
$('.save-button', element).parent()
.removeClass('hidden')
.one('click', function submitForm(e) {
// $itemTab -> submit
e.preventDefault();
if (!self.validate()) {
$(e.target).one('click', submitForm);
return
}
_fn.build.form.submit();
});
});
});
$zoneTab
.on('click', '.add-zone', function(e) {
_fn.build.form.zone.add();
})
.on('click', '.remove-zone', _fn.build.form.zone.remove)
.on('input', '.zone-row input', _fn.build.form.zone.changedInputHandler)
.on('click', '.target-image-form button', function(e) {
e.preventDefault();
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'
var handlerUrl = runtime.handlerUrl(element, 'expand_static_url');
$.post(handlerUrl, JSON.stringify(new_img_url), function(result) {
_fn.build.$el.targetImage.attr('src', result.url);
});
} else {
new_img_url = params.default_background_image_url;
_fn.build.$el.targetImage.attr('src', new_img_url);
}
_fn.data.targetImg = new_img_url;
var new_description = $.trim(
$('.target-image-form #background-description', element).val()
);
_fn.build.$el.targetImage.attr('alt', new_description);
_fn.data.targetImgDescription = new_description;
})
.on('click', '.display-labels-form input', function(e) {
_fn.data.displayLabels = $('.display-labels-form input', element).is(':checked');
})
.on('click', '.display-borders-form input', function(e) {
_fn.data.displayBorders = $('.display-borders-form input', element).is(':checked');
});
$itemTab
.on('click', '.add-item', function(e) {
_fn.build.form.item.add();
})
.on('click', '.remove-item', _fn.build.form.item.remove)
.on('click', '.advanced-link a', _fn.build.form.item.showAdvancedSettings);
},
form: {
zone: {
count: 0,
formCount: 0,
zoneObjects: [],
getObjByIndex: function(num) {
for (var i = 0; i < _fn.build.form.zone.zoneObjects.length; i++) {
if (_fn.build.form.zone.zoneObjects[i].index == num)
return _fn.build.form.zone.zoneObjects[i];
}
},
add: function(oldZone) {
var inputTemplate = _fn.tpl.zoneInput,
name = 'zone-',
$elements = _fn.build.$el,
num;
if (!oldZone) oldZone = {};
_fn.build.form.zone.count++;
_fn.build.form.zone.formCount++;
num = _fn.build.form.zone.count;
name += num;
// Update zone obj
var zoneObj = {
title: oldZone.title || 'Zone ' + num,
description: oldZone.description,
id: name,
index: num,
width: oldZone.width || 200,
height: oldZone.height || 100,
x: oldZone.x || 0,
y: oldZone.y || 0,
};
_fn.build.form.zone.zoneObjects.push(zoneObj);
// Add fields to zone position form
$zoneNode = $(inputTemplate(zoneObj));
$zoneNode.data('index', num);
$elements.zones.form.append($zoneNode);
_fn.build.form.zone.enableDelete();
// Add zone div to target
_fn.build.form.zone.renderZonesPreview();
},
remove: function(e) {
var $el = $(e.currentTarget).closest('.zone-row'),
classes = $el.attr('class'),
id = classes.slice(classes.indexOf('zone-row') + 9),
index = $el.data('index'),
array_index;
e.preventDefault();
$el.detach();
// Find the index of the zone in the array and remove it.
for (array_index = 0; array_index < _fn.build.form.zone.zoneObjects.length;
array_index++) {
if (_fn.build.form.zone.zoneObjects[array_index].index == index) break;
}
_fn.build.form.zone.zoneObjects.splice(array_index, 1);
_fn.build.form.zone.renderZonesPreview();
_fn.build.form.zone.formCount--;
_fn.build.form.zone.disableDelete();
},
enableDelete: function() {
if (_fn.build.form.zone.formCount > 1) {
_fn.build.$el.zones.form.find('.remove-zone').removeClass('hidden');
}
},
disableDelete: function() {
if (_fn.build.form.zone.formCount === 1) {
_fn.build.$el.zones.form.find('.remove-zone').addClass('hidden');
}
},
renderZonesPreview: function() {
// Refresh the div which shows a preview of the zones over top of
// the background image.
_fn.build.$el.zonesPreview.html('');
var imgWidth = _fn.build.$el.targetImage[0].naturalWidth;
var imgHeight = _fn.build.$el.targetImage[0].naturalHeight;
if (imgWidth == 0 || imgHeight == 0) {
// Set a non-zero value to avoid divide-by-zero:
imgWidth = imgHeight = 400;
}
this.zoneObjects.forEach(function(zoneObj) {
_fn.build.$el.zonesPreview.append(
_fn.tpl.zoneElement({
id: zoneObj.id,
title: zoneObj.title,
description: zoneObj.description,
x_percent: (+zoneObj.x) / imgWidth * 100,
y_percent: (+zoneObj.y) / imgHeight * 100,
width_percent: (+zoneObj.width) / imgWidth * 100,
height_percent: (+zoneObj.height) / imgHeight * 100,
})
);
});
},
getZoneNames: function() {
var zoneNames = [];
var $form = _fn.build.$el.zones.form.find('.title');
$form.each(function(i, el) {
var val = $(el).val();
if (val.length > 0) {
zoneNames.push(val);
}
});
return zoneNames;
},
changedInputHandler: function(ev) {
// Called when any of the inputs have changed.
var $changedInput = $(ev.currentTarget);
var $row = $changedInput.closest('.zone-row');
var record = _fn.build.form.zone.getObjByIndex($row.data('index'));
if ($changedInput.hasClass('title')) {
record.title = $changedInput.val();
} else if ($changedInput.hasClass('width')) {
record.width = $changedInput.val();
} else if ($changedInput.hasClass('description')) {
record.description = $changedInput.val();
} else if ($changedInput.hasClass('height')) {
record.height = $changedInput.val();
} else if ($changedInput.hasClass('x')) {
record.x = $changedInput.val();
} else if ($changedInput.hasClass('y')) {
record.y = $changedInput.val();
}
_fn.build.form.zone.renderZonesPreview();
},
imageLoaded: function() {
// The target background image has loaded (or reloaded, if changed).
_fn.build.form.zone.renderZonesPreview();
},
},
createDropdown: function(selected) {
var tpl = _fn.tpl.zoneDropdown,
dropdown = [],
html,
dropdown_items = _fn.build.form.zone.getZoneNames().concat('none');
for (var i=0; i<dropdown_items.length; i++) {
var is_sel = (dropdown_items[i] == selected) ? 'selected' : '';
dropdown.push(tpl({ value: dropdown_items[i], selected: is_sel }));
}
html = dropdown.join('');
return new Handlebars.SafeString(html);
},
feedback: function($form) {
_fn.data.feedback = {
start: $form.find('#intro-feedback').val(),
finish: $form.find('#final-feedback').val()
};
},
item: {
count: 0,
add: function(itemData) {
var $form = _fn.build.$el.items.form,
tpl = _fn.tpl.itemInput,
ctx = {};
if (itemData) {
ctx = itemData;
if (itemData.backgroundImage && !ctx.imageURL) {
ctx.imageURL = itemData.backgroundImage; // This field was renamed.
}
if (itemData.size && parseInt(itemData.size.width) > 0) {
// Convert old fixed pixel width setting values (hard to
// make mobile friendly) to new percentage format.
// Note itemData.size.width is a string like "380px" (it can
// also be "auto" but that's excluded by the if condition above)
var bgImgWidth = _fn.build.$el.targetImage[0].naturalWidth;
if (bgImgWidth > 0 && typeof ctx.widthPercent === "undefined") {
ctx.widthPercent = parseInt(itemData.size.width) / bgImgWidth * 100;
}
// Preserve the old-style data in case we need it again:
ctx.pixelWidth = itemData.size.width.substr(0, itemData.size.width.length - 2); // Remove 'px'
}
if (itemData.size && parseInt(itemData.size.height) > 0) {
// Item fixed pixel height is ignored in new versions of the
// block, but preserve the data in case we need it again:
ctx.pixelHeight = itemData.size.height.substr(0, itemData.size.height.length - 2); // Remove 'px'
}
if (itemData.inputOptions) {
ctx.numericalValue = itemData.inputOptions.value;
ctx.numericalMargin = itemData.inputOptions.margin;
}
}
ctx.dropdown = _fn.build.form.createDropdown(ctx.zone);
_fn.build.form.item.count++;
$form.append(tpl(ctx));
_fn.build.form.item.enableDelete();
},
remove: function(e) {
var $el = $(e.currentTarget).closest('.item');
e.preventDefault();
$el.detach();
_fn.build.form.item.count--;
_fn.build.form.item.disableDelete();
},
enableDelete: function() {
if (_fn.build.form.item.count > 1) {
_fn.build.$el.items.form.find('.remove-item').removeClass('hidden');
}
},
disableDelete: function() {
if (_fn.build.form.item.count === 1) {
_fn.build.$el.items.form.find('.remove-item').addClass('hidden');
}
},
showAdvancedSettings: function(e) {
e.preventDefault();
var $el = $(e.currentTarget).closest('.item');
$el.find('.row.advanced').show();
$el.find('.row.advanced-link').hide();
},
},
submit: function() {
var items = [],
$form = _fn.build.$el.items.form.find('.item');
$form.each(function(i, el) {
var $el = $(el),
name = $el.find('.item-text').val(),
imageURL = $el.find('.item-image-url').val(),
imageDescription = $el.find('.item-image-description').val();
if (name.length > 0 || imageURL.length > 0) {
var data = {
displayName: name,
zone: $el.find('.zone-select').val(),
id: i,
feedback: {
correct: $el.find('.success-feedback').val(),
incorrect: $el.find('.error-feedback').val()
},
imageURL: imageURL,
imageDescription: imageDescription,
};
// Optional preferred width as a percentage of the bg image's width:
var widthPercent = $el.find('.item-width').val();
if (widthPercent && +widthPercent > 0) { data.widthPercent = widthPercent; }
var numValue = parseFloat($el.find('.item-numerical-value').val());
var numMargin = parseFloat($el.find('.item-numerical-margin').val());
if (isFinite(numValue)) {
data.inputOptions = {
value: numValue,
margin: isFinite(numMargin) ? numMargin : 0
};
}
items.push(data);
}
});
_fn.data.items = items;
_fn.data.zones = _fn.build.form.zone.zoneObjects;
var data = {
'display_name': $element.find('#display-name').val(),
'show_title': $element.find('.show-title').is(':checked'),
'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(),
'data': _fn.data,
};
$('.xblock-editor-error-message', element).html();
$('.xblock-editor-error-message', element).css('display', 'none');
var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
$.post(handlerUrl, JSON.stringify(data), 'json').done(function(response) {
if (response.result === 'success') {
window.location.reload(false);
} else {
$('.xblock-editor-error-message', element)
.html(gettext('Error: ') + response.message);
$('.xblock-editor-error-message', element).css('display', 'block');
}
});
}
}
},
data: null
};
return {
init: _fn.build.init
};
})(jQuery);
$element.find('.cancel-button').bind('click', function() {
runtime.notify('cancel', {});
});
dragAndDrop.init();
}
/*!
handlebars v1.1.2
Copyright (C) 2011 by Yehuda Katz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@license
*/
var Handlebars = (function() {
// handlebars/safe-string.js
var __module4__ = (function() {
"use strict";
var __exports__;
// Build out our basic SafeString type
function SafeString(string) {
this.string = string;
}
SafeString.prototype.toString = function() {
return "" + this.string;
};
__exports__ = SafeString;
return __exports__;
})();
// handlebars/utils.js
var __module3__ = (function(__dependency1__) {
"use strict";
var __exports__ = {};
var SafeString = __dependency1__;
var escape = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;"
};
var badChars = /[&<>"'`]/g;
var possible = /[&<>"'`]/;
function escapeChar(chr) {
return escape[chr] || "&amp;";
}
function extend(obj, value) {
for(var key in value) {
if(value.hasOwnProperty(key)) {
obj[key] = value[key];
}
}
}
__exports__.extend = extend;var toString = Object.prototype.toString;
__exports__.toString = toString;
// Sourced from lodash
// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt
var isFunction = function(value) {
return typeof value === 'function';
};
// fallback for older versions of Chrome and Safari
if (isFunction(/x/)) {
isFunction = function(value) {
return typeof value === 'function' && toString.call(value) === '[object Function]';
};
}
var isFunction;
__exports__.isFunction = isFunction;
var isArray = Array.isArray || function(value) {
return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false;
};
__exports__.isArray = isArray;
function escapeExpression(string) {
// don't escape SafeStrings, since they're already safe
if (string instanceof SafeString) {
return string.toString();
} else if (!string && string !== 0) {
return "";
}
// Force a string conversion as this will be done by the append regardless and
// the regex test will do this transparently behind the scenes, causing issues if
// an object's to string has escaped characters in it.
string = "" + string;
if(!possible.test(string)) { return string; }
return string.replace(badChars, escapeChar);
}
__exports__.escapeExpression = escapeExpression;function isEmpty(value) {
if (!value && value !== 0) {
return true;
} else if (isArray(value) && value.length === 0) {
return true;
} else {
return false;
}
}
__exports__.isEmpty = isEmpty;
return __exports__;
})(__module4__);
// handlebars/exception.js
var __module5__ = (function() {
"use strict";
var __exports__;
var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
function Exception(/* message */) {
var tmp = Error.prototype.constructor.apply(this, arguments);
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
for (var idx = 0; idx < errorProps.length; idx++) {
this[errorProps[idx]] = tmp[errorProps[idx]];
}
}
Exception.prototype = new Error();
__exports__ = Exception;
return __exports__;
})();
// handlebars/base.js
var __module2__ = (function(__dependency1__, __dependency2__) {
"use strict";
var __exports__ = {};
/*globals Exception, Utils */
var Utils = __dependency1__;
var Exception = __dependency2__;
var VERSION = "1.1.2";
__exports__.VERSION = VERSION;var COMPILER_REVISION = 4;
__exports__.COMPILER_REVISION = COMPILER_REVISION;
var REVISION_CHANGES = {
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3',
3: '== 1.0.0-rc.4',
4: '>= 1.0.0'
};
__exports__.REVISION_CHANGES = REVISION_CHANGES;
var isArray = Utils.isArray,
isFunction = Utils.isFunction,
toString = Utils.toString,
objectType = '[object Object]';
function HandlebarsEnvironment(helpers, partials) {
this.helpers = helpers || {};
this.partials = partials || {};
registerDefaultHelpers(this);
}
__exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = {
constructor: HandlebarsEnvironment,
logger: logger,
log: log,
registerHelper: function(name, fn, inverse) {
if (toString.call(name) === objectType) {
if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); }
Utils.extend(this.helpers, name);
} else {
if (inverse) { fn.not = inverse; }
this.helpers[name] = fn;
}
},
registerPartial: function(name, str) {
if (toString.call(name) === objectType) {
Utils.extend(this.partials, name);
} else {
this.partials[name] = str;
}
}
};
function registerDefaultHelpers(instance) {
instance.registerHelper('helperMissing', function(arg) {
if(arguments.length === 2) {
return undefined;
} else {
throw new Error("Missing helper: '" + arg + "'");
}
});
instance.registerHelper('blockHelperMissing', function(context, options) {
var inverse = options.inverse || function() {}, fn = options.fn;
if (isFunction(context)) { context = context.call(this); }
if(context === true) {
return fn(this);
} else if(context === false || context == null) {
return inverse(this);
} else if (isArray(context)) {
if(context.length > 0) {
return instance.helpers.each(context, options);
} else {
return inverse(this);
}
} else {
return fn(context);
}
});
instance.registerHelper('each', function(context, options) {
var fn = options.fn, inverse = options.inverse;
var i = 0, ret = "", data;
if (isFunction(context)) { context = context.call(this); }
if (options.data) {
data = createFrame(options.data);
}
if(context && typeof context === 'object') {
if (isArray(context)) {
for(var j = context.length; i<j; i++) {
if (data) {
data.index = i;
data.first = (i === 0)
data.last = (i === (context.length-1));
}
ret = ret + fn(context[i], { data: data });
}
} else {
for(var key in context) {
if(context.hasOwnProperty(key)) {
if(data) { data.key = key; }
ret = ret + fn(context[key], {data: data});
i++;
}
}
}
}
if(i === 0){
ret = inverse(this);
}
return ret;
});
instance.registerHelper('if', function(conditional, options) {
if (isFunction(conditional)) { conditional = conditional.call(this); }
// Default behavior is to render the positive path if the value is truthy and not empty.
// The `includeZero` option may be set to treat the condtional as purely not empty based on the
// behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative.
if ((!options.hash.includeZero && !conditional) || Utils.isEmpty(conditional)) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
instance.registerHelper('unless', function(conditional, options) {
return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash});
});
instance.registerHelper('with', function(context, options) {
if (isFunction(context)) { context = context.call(this); }
if (!Utils.isEmpty(context)) return options.fn(context);
});
instance.registerHelper('log', function(context, options) {
var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
instance.log(level, context);
});
}
var logger = {
methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' },
// State enum
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
level: 3,
// can be overridden in the host environment
log: function(level, obj) {
if (logger.level <= level) {
var method = logger.methodMap[level];
if (typeof console !== 'undefined' && console[method]) {
console[method].call(console, obj);
}
}
}
};
__exports__.logger = logger;
function log(level, obj) { logger.log(level, obj); }
__exports__.log = log;var createFrame = function(object) {
var obj = {};
Utils.extend(obj, object);
return obj;
};
__exports__.createFrame = createFrame;
return __exports__;
})(__module3__, __module5__);
// handlebars/runtime.js
var __module6__ = (function(__dependency1__, __dependency2__, __dependency3__) {
"use strict";
var __exports__ = {};
/*global Utils */
var Utils = __dependency1__;
var Exception = __dependency2__;
var COMPILER_REVISION = __dependency3__.COMPILER_REVISION;
var REVISION_CHANGES = __dependency3__.REVISION_CHANGES;
function checkRevision(compilerInfo) {
var compilerRevision = compilerInfo && compilerInfo[0] || 1,
currentRevision = COMPILER_REVISION;
if (compilerRevision !== currentRevision) {
if (compilerRevision < currentRevision) {
var runtimeVersions = REVISION_CHANGES[currentRevision],
compilerVersions = REVISION_CHANGES[compilerRevision];
throw new Error("Template was precompiled with an older version of Handlebars than the current runtime. "+
"Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").");
} else {
// Use the embedded version info since the runtime doesn't know about this revision yet
throw new Error("Template was precompiled with a newer version of Handlebars than the current runtime. "+
"Please update your runtime to a newer version ("+compilerInfo[1]+").");
}
}
}
// TODO: Remove this line and break up compilePartial
function template(templateSpec, env) {
if (!env) {
throw new Error("No environment passed to template");
}
var invokePartialWrapper;
if (env.compile) {
invokePartialWrapper = function(partial, name, context, helpers, partials, data) {
// TODO : Check this for all inputs and the options handling (partial flag, etc). This feels
// like there should be a common exec path
var result = invokePartial.apply(this, arguments);
if (result) { return result; }
var options = { helpers: helpers, partials: partials, data: data };
partials[name] = env.compile(partial, { data: data !== undefined }, env);
return partials[name](context, options);
};
} else {
invokePartialWrapper = function(partial, name /* , context, helpers, partials, data */) {
var result = invokePartial.apply(this, arguments);
if (result) { return result; }
throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
};
}
// Just add water
var container = {
escapeExpression: Utils.escapeExpression,
invokePartial: invokePartialWrapper,
programs: [],
program: function(i, fn, data) {
var programWrapper = this.programs[i];
if(data) {
programWrapper = program(i, fn, data);
} else if (!programWrapper) {
programWrapper = this.programs[i] = program(i, fn);
}
return programWrapper;
},
merge: function(param, common) {
var ret = param || common;
if (param && common && (param !== common)) {
ret = {};
Utils.extend(ret, common);
Utils.extend(ret, param);
}
return ret;
},
programWithDepth: programWithDepth,
noop: noop,
compilerInfo: null
};
return function(context, options) {
options = options || {};
var namespace = options.partial ? options : env,
helpers,
partials;
if (!options.partial) {
helpers = options.helpers;
partials = options.partials;
}
var result = templateSpec.call(
container,
namespace, context,
helpers,
partials,
options.data);
if (!options.partial) {
checkRevision(container.compilerInfo);
}
return result;
};
}
__exports__.template = template;function programWithDepth(i, fn, data /*, $depth */) {
var args = Array.prototype.slice.call(arguments, 3);
var prog = function(context, options) {
options = options || {};
return fn.apply(this, [context, options.data || data].concat(args));
};
prog.program = i;
prog.depth = args.length;
return prog;
}
__exports__.programWithDepth = programWithDepth;function program(i, fn, data) {
var prog = function(context, options) {
options = options || {};
return fn(context, options.data || data);
};
prog.program = i;
prog.depth = 0;
return prog;
}
__exports__.program = program;function invokePartial(partial, name, context, helpers, partials, data) {
var options = { partial: true, helpers: helpers, partials: partials, data: data };
if(partial === undefined) {
throw new Exception("The partial " + name + " could not be found");
} else if(partial instanceof Function) {
return partial(context, options);
}
}
__exports__.invokePartial = invokePartial;function noop() { return ""; }
__exports__.noop = noop;
return __exports__;
})(__module3__, __module5__, __module2__);
// handlebars.runtime.js
var __module1__ = (function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__) {
"use strict";
var __exports__;
var base = __dependency1__;
// Each of these augment the Handlebars object. No need to setup here.
// (This is done to easily share code between commonjs and browse envs)
var SafeString = __dependency2__;
var Exception = __dependency3__;
var Utils = __dependency4__;
var runtime = __dependency5__;
// For compatibility and usage outside of module systems, make the Handlebars object a namespace
var create = function() {
var hb = new base.HandlebarsEnvironment();
Utils.extend(hb, base);
hb.SafeString = SafeString;
hb.Exception = Exception;
hb.Utils = Utils;
hb.VM = runtime;
hb.template = function(spec) {
return runtime.template(spec, hb);
};
return hb;
};
var Handlebars = create();
Handlebars.create = create;
__exports__ = Handlebars;
return __exports__;
})(__module2__, __module4__, __module5__, __module3__, __module6__);
// handlebars/compiler/ast.js
var __module7__ = (function(__dependency1__) {
"use strict";
var __exports__ = {};
var Exception = __dependency1__;
function ProgramNode(statements, inverseStrip, inverse) {
this.type = "program";
this.statements = statements;
this.strip = {};
if(inverse) {
this.inverse = new ProgramNode(inverse, inverseStrip);
this.strip.right = inverseStrip.left;
} else if (inverseStrip) {
this.strip.left = inverseStrip.right;
}
}
__exports__.ProgramNode = ProgramNode;function MustacheNode(rawParams, hash, open, strip) {
this.type = "mustache";
this.hash = hash;
this.strip = strip;
var escapeFlag = open[3] || open[2];
this.escaped = escapeFlag !== '{' && escapeFlag !== '&';
var id = this.id = rawParams[0];
var params = this.params = rawParams.slice(1);
// a mustache is an eligible helper if:
// * its id is simple (a single part, not `this` or `..`)
var eligibleHelper = this.eligibleHelper = id.isSimple;
// a mustache is definitely a helper if:
// * it is an eligible helper, and
// * it has at least one parameter or hash segment
this.isHelper = eligibleHelper && (params.length || hash);
// if a mustache is an eligible helper but not a definite
// helper, it is ambiguous, and will be resolved in a later
// pass or at runtime.
}
__exports__.MustacheNode = MustacheNode;function PartialNode(partialName, context, strip) {
this.type = "partial";
this.partialName = partialName;
this.context = context;
this.strip = strip;
}
__exports__.PartialNode = PartialNode;function BlockNode(mustache, program, inverse, close) {
if(mustache.id.original !== close.path.original) {
throw new Exception(mustache.id.original + " doesn't match " + close.path.original);
}
this.type = "block";
this.mustache = mustache;
this.program = program;
this.inverse = inverse;
this.strip = {
left: mustache.strip.left,
right: close.strip.right
};
(program || inverse).strip.left = mustache.strip.right;
(inverse || program).strip.right = close.strip.left;
if (inverse && !program) {
this.isInverse = true;
}
}
__exports__.BlockNode = BlockNode;function ContentNode(string) {
this.type = "content";
this.string = string;
}
__exports__.ContentNode = ContentNode;function HashNode(pairs) {
this.type = "hash";
this.pairs = pairs;
}
__exports__.HashNode = HashNode;function IdNode(parts) {
this.type = "ID";
var original = "",
dig = [],
depth = 0;
for(var i=0,l=parts.length; i<l; i++) {
var part = parts[i].part;
original += (parts[i].separator || '') + part;
if (part === ".." || part === "." || part === "this") {
if (dig.length > 0) { throw new Exception("Invalid path: " + original); }
else if (part === "..") { depth++; }
else { this.isScoped = true; }
}
else { dig.push(part); }
}
this.original = original;
this.parts = dig;
this.string = dig.join('.');
this.depth = depth;
// an ID is simple if it only has one part, and that part is not
// `..` or `this`.
this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
this.stringModeValue = this.string;
}
__exports__.IdNode = IdNode;function PartialNameNode(name) {
this.type = "PARTIAL_NAME";
this.name = name.original;
}
__exports__.PartialNameNode = PartialNameNode;function DataNode(id) {
this.type = "DATA";
this.id = id;
}
__exports__.DataNode = DataNode;function StringNode(string) {
this.type = "STRING";
this.original =
this.string =
this.stringModeValue = string;
}
__exports__.StringNode = StringNode;function IntegerNode(integer) {
this.type = "INTEGER";
this.original =
this.integer = integer;
this.stringModeValue = Number(integer);
}
__exports__.IntegerNode = IntegerNode;function BooleanNode(bool) {
this.type = "BOOLEAN";
this.bool = bool;
this.stringModeValue = bool === "true";
}
__exports__.BooleanNode = BooleanNode;function CommentNode(comment) {
this.type = "comment";
this.comment = comment;
}
__exports__.CommentNode = CommentNode;
return __exports__;
})(__module5__);
// handlebars/compiler/parser.js
var __module9__ = (function() {
"use strict";
var __exports__;
/* Jison generated parser */
var handlebars = (function(){
var parser = {trace: function trace() { },
yy: {},
symbols_: {"error":2,"root":3,"statements":4,"EOF":5,"program":6,"simpleInverse":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"CLOSE_UNESCAPED":24,"OPEN_PARTIAL":25,"partialName":26,"partial_option0":27,"inMustache_repetition0":28,"inMustache_option0":29,"dataName":30,"param":31,"STRING":32,"INTEGER":33,"BOOLEAN":34,"hash":35,"hash_repetition_plus0":36,"hashSegment":37,"ID":38,"EQUALS":39,"DATA":40,"pathSegments":41,"SEP":42,"$accept":0,"$end":1},
terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"CLOSE_UNESCAPED",25:"OPEN_PARTIAL",32:"STRING",33:"INTEGER",34:"BOOLEAN",38:"ID",39:"EQUALS",40:"DATA",42:"SEP"},
productions_: [0,[3,2],[3,1],[6,2],[6,3],[6,2],[6,1],[6,1],[6,0],[4,1],[4,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,4],[7,2],[17,3],[17,1],[31,1],[31,1],[31,1],[31,1],[31,1],[35,1],[37,3],[26,1],[26,1],[26,1],[30,2],[21,1],[41,3],[41,1],[27,0],[27,1],[28,0],[28,2],[29,0],[29,1],[36,1],[36,2]],
performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) {
var $0 = $$.length - 1;
switch (yystate) {
case 1: return new yy.ProgramNode($$[$0-1]);
break;
case 2: return new yy.ProgramNode([]);
break;
case 3:this.$ = new yy.ProgramNode([], $$[$0-1], $$[$0]);
break;
case 4:this.$ = new yy.ProgramNode($$[$0-2], $$[$0-1], $$[$0]);
break;
case 5:this.$ = new yy.ProgramNode($$[$0-1], $$[$0], []);
break;
case 6:this.$ = new yy.ProgramNode($$[$0]);
break;
case 7:this.$ = new yy.ProgramNode([]);
break;
case 8:this.$ = new yy.ProgramNode([]);
break;
case 9:this.$ = [$$[$0]];
break;
case 10: $$[$0-1].push($$[$0]); this.$ = $$[$0-1];
break;
case 11:this.$ = new yy.BlockNode($$[$0-2], $$[$0-1].inverse, $$[$0-1], $$[$0]);
break;
case 12:this.$ = new yy.BlockNode($$[$0-2], $$[$0-1], $$[$0-1].inverse, $$[$0]);
break;
case 13:this.$ = $$[$0];
break;
case 14:this.$ = $$[$0];
break;
case 15:this.$ = new yy.ContentNode($$[$0]);
break;
case 16:this.$ = new yy.CommentNode($$[$0]);
break;
case 17:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0]));
break;
case 18:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0]));
break;
case 19:this.$ = {path: $$[$0-1], strip: stripFlags($$[$0-2], $$[$0])};
break;
case 20:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0]));
break;
case 21:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0]));
break;
case 22:this.$ = new yy.PartialNode($$[$0-2], $$[$0-1], stripFlags($$[$0-3], $$[$0]));
break;
case 23:this.$ = stripFlags($$[$0-1], $$[$0]);
break;
case 24:this.$ = [[$$[$0-2]].concat($$[$0-1]), $$[$0]];
break;
case 25:this.$ = [[$$[$0]], null];
break;
case 26:this.$ = $$[$0];
break;
case 27:this.$ = new yy.StringNode($$[$0]);
break;
case 28:this.$ = new yy.IntegerNode($$[$0]);
break;
case 29:this.$ = new yy.BooleanNode($$[$0]);
break;
case 30:this.$ = $$[$0];
break;
case 31:this.$ = new yy.HashNode($$[$0]);
break;
case 32:this.$ = [$$[$0-2], $$[$0]];
break;
case 33:this.$ = new yy.PartialNameNode($$[$0]);
break;
case 34:this.$ = new yy.PartialNameNode(new yy.StringNode($$[$0]));
break;
case 35:this.$ = new yy.PartialNameNode(new yy.IntegerNode($$[$0]));
break;
case 36:this.$ = new yy.DataNode($$[$0]);
break;
case 37:this.$ = new yy.IdNode($$[$0]);
break;
case 38: $$[$0-2].push({part: $$[$0], separator: $$[$0-1]}); this.$ = $$[$0-2];
break;
case 39:this.$ = [{part: $$[$0]}];
break;
case 42:this.$ = [];
break;
case 43:$$[$0-1].push($$[$0]);
break;
case 46:this.$ = [$$[$0]];
break;
case 47:$$[$0-1].push($$[$0]);
break;
}
},
table: [{3:1,4:2,5:[1,3],8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],25:[1,15]},{1:[3]},{5:[1,16],8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],25:[1,15]},{1:[2,2]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],25:[2,9]},{4:20,6:18,7:19,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,8],22:[1,13],23:[1,14],25:[1,15]},{4:20,6:22,7:19,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,8],22:[1,13],23:[1,14],25:[1,15]},{5:[2,13],14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],25:[2,13]},{5:[2,14],14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],25:[2,14]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],25:[2,15]},{5:[2,16],14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],25:[2,16]},{17:23,21:24,30:25,38:[1,28],40:[1,27],41:26},{17:29,21:24,30:25,38:[1,28],40:[1,27],41:26},{17:30,21:24,30:25,38:[1,28],40:[1,27],41:26},{17:31,21:24,30:25,38:[1,28],40:[1,27],41:26},{21:33,26:32,32:[1,34],33:[1,35],38:[1,28],41:26},{1:[2,1]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],25:[2,10]},{10:36,20:[1,37]},{4:38,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,7],22:[1,13],23:[1,14],25:[1,15]},{7:39,8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,6],22:[1,13],23:[1,14],25:[1,15]},{17:23,18:[1,40],21:24,30:25,38:[1,28],40:[1,27],41:26},{10:41,20:[1,37]},{18:[1,42]},{18:[2,42],24:[2,42],28:43,32:[2,42],33:[2,42],34:[2,42],38:[2,42],40:[2,42]},{18:[2,25],24:[2,25]},{18:[2,37],24:[2,37],32:[2,37],33:[2,37],34:[2,37],38:[2,37],40:[2,37],42:[1,44]},{21:45,38:[1,28],41:26},{18:[2,39],24:[2,39],32:[2,39],33:[2,39],34:[2,39],38:[2,39],40:[2,39],42:[2,39]},{18:[1,46]},{18:[1,47]},{24:[1,48]},{18:[2,40],21:50,27:49,38:[1,28],41:26},{18:[2,33],38:[2,33]},{18:[2,34],38:[2,34]},{18:[2,35],38:[2,35]},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],25:[2,11]},{21:51,38:[1,28],41:26},{8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,3],22:[1,13],23:[1,14],25:[1,15]},{4:52,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,5],22:[1,13],23:[1,14],25:[1,15]},{14:[2,23],15:[2,23],16:[2,23],19:[2,23],20:[2,23],22:[2,23],23:[2,23],25:[2,23]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],25:[2,12]},{14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],25:[2,18]},{18:[2,44],21:56,24:[2,44],29:53,30:60,31:54,32:[1,57],33:[1,58],34:[1,59],35:55,36:61,37:62,38:[1,63],40:[1,27],41:26},{38:[1,64]},{18:[2,36],24:[2,36],32:[2,36],33:[2,36],34:[2,36],38:[2,36],40:[2,36]},{14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],25:[2,17]},{5:[2,20],14:[2,20],15:[2,20],16:[2,20],19:[2,20],20:[2,20],22:[2,20],23:[2,20],25:[2,20]},{5:[2,21],14:[2,21],15:[2,21],16:[2,21],19:[2,21],20:[2,21],22:[2,21],23:[2,21],25:[2,21]},{18:[1,65]},{18:[2,41]},{18:[1,66]},{8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],25:[1,15]},{18:[2,24],24:[2,24]},{18:[2,43],24:[2,43],32:[2,43],33:[2,43],34:[2,43],38:[2,43],40:[2,43]},{18:[2,45],24:[2,45]},{18:[2,26],24:[2,26],32:[2,26],33:[2,26],34:[2,26],38:[2,26],40:[2,26]},{18:[2,27],24:[2,27],32:[2,27],33:[2,27],34:[2,27],38:[2,27],40:[2,27]},{18:[2,28],24:[2,28],32:[2,28],33:[2,28],34:[2,28],38:[2,28],40:[2,28]},{18:[2,29],24:[2,29],32:[2,29],33:[2,29],34:[2,29],38:[2,29],40:[2,29]},{18:[2,30],24:[2,30],32:[2,30],33:[2,30],34:[2,30],38:[2,30],40:[2,30]},{18:[2,31],24:[2,31],37:67,38:[1,68]},{18:[2,46],24:[2,46],38:[2,46]},{18:[2,39],24:[2,39],32:[2,39],33:[2,39],34:[2,39],38:[2,39],39:[1,69],40:[2,39],42:[2,39]},{18:[2,38],24:[2,38],32:[2,38],33:[2,38],34:[2,38],38:[2,38],40:[2,38],42:[2,38]},{5:[2,22],14:[2,22],15:[2,22],16:[2,22],19:[2,22],20:[2,22],22:[2,22],23:[2,22],25:[2,22]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],25:[2,19]},{18:[2,47],24:[2,47],38:[2,47]},{39:[1,69]},{21:56,30:60,31:70,32:[1,57],33:[1,58],34:[1,59],38:[1,28],40:[1,27],41:26},{18:[2,32],24:[2,32],38:[2,32]}],
defaultActions: {3:[2,2],16:[2,1],50:[2,41]},
parseError: function parseError(str, hash) {
throw new Error(str);
},
parse: function parse(input) {
var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = "", yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1;
this.lexer.setInput(input);
this.lexer.yy = this.yy;
this.yy.lexer = this.lexer;
this.yy.parser = this;
if (typeof this.lexer.yylloc == "undefined")
this.lexer.yylloc = {};
var yyloc = this.lexer.yylloc;
lstack.push(yyloc);
var ranges = this.lexer.options && this.lexer.options.ranges;
if (typeof this.yy.parseError === "function")
this.parseError = this.yy.parseError;
function popStack(n) {
stack.length = stack.length - 2 * n;
vstack.length = vstack.length - n;
lstack.length = lstack.length - n;
}
function lex() {
var token;
token = self.lexer.lex() || 1;
if (typeof token !== "number") {
token = self.symbols_[token] || token;
}
return token;
}
var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected;
while (true) {
state = stack[stack.length - 1];
if (this.defaultActions[state]) {
action = this.defaultActions[state];
} else {
if (symbol === null || typeof symbol == "undefined") {
symbol = lex();
}
action = table[state] && table[state][symbol];
}
if (typeof action === "undefined" || !action.length || !action[0]) {
var errStr = "";
if (!recovering) {
expected = [];
for (p in table[state])
if (this.terminals_[p] && p > 2) {
expected.push("'" + this.terminals_[p] + "'");
}
if (this.lexer.showPosition) {
errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'";
} else {
errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'");
}
this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected});
}
}
if (action[0] instanceof Array && action.length > 1) {
throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol);
}
switch (action[0]) {
case 1:
stack.push(symbol);
vstack.push(this.lexer.yytext);
lstack.push(this.lexer.yylloc);
stack.push(action[1]);
symbol = null;
if (!preErrorSymbol) {
yyleng = this.lexer.yyleng;
yytext = this.lexer.yytext;
yylineno = this.lexer.yylineno;
yyloc = this.lexer.yylloc;
if (recovering > 0)
recovering--;
} else {
symbol = preErrorSymbol;
preErrorSymbol = null;
}
break;
case 2:
len = this.productions_[action[1]][1];
yyval.$ = vstack[vstack.length - len];
yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column};
if (ranges) {
yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]];
}
r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack);
if (typeof r !== "undefined") {
return r;
}
if (len) {
stack = stack.slice(0, -1 * len * 2);
vstack = vstack.slice(0, -1 * len);
lstack = lstack.slice(0, -1 * len);
}
stack.push(this.productions_[action[1]][0]);
vstack.push(yyval.$);
lstack.push(yyval._$);
newState = table[stack[stack.length - 2]][stack[stack.length - 1]];
stack.push(newState);
break;
case 3:
return true;
}
}
return true;
}
};
function stripFlags(open, close) {
return {
left: open[2] === '~',
right: close[0] === '~' || close[1] === '~'
};
}
/* Jison generated lexer */
var lexer = (function(){
var lexer = ({EOF:1,
parseError:function parseError(str, hash) {
if (this.yy.parser) {
this.yy.parser.parseError(str, hash);
} else {
throw new Error(str);
}
},
setInput:function (input) {
this._input = input;
this._more = this._less = this.done = false;
this.yylineno = this.yyleng = 0;
this.yytext = this.matched = this.match = '';
this.conditionStack = ['INITIAL'];
this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0};
if (this.options.ranges) this.yylloc.range = [0,0];
this.offset = 0;
return this;
},
input:function () {
var ch = this._input[0];
this.yytext += ch;
this.yyleng++;
this.offset++;
this.match += ch;
this.matched += ch;
var lines = ch.match(/(?:\r\n?|\n).*/g);
if (lines) {
this.yylineno++;
this.yylloc.last_line++;
} else {
this.yylloc.last_column++;
}
if (this.options.ranges) this.yylloc.range[1]++;
this._input = this._input.slice(1);
return ch;
},
unput:function (ch) {
var len = ch.length;
var lines = ch.split(/(?:\r\n?|\n)/g);
this._input = ch + this._input;
this.yytext = this.yytext.substr(0, this.yytext.length-len-1);
//this.yyleng -= len;
this.offset -= len;
var oldLines = this.match.split(/(?:\r\n?|\n)/g);
this.match = this.match.substr(0, this.match.length-1);
this.matched = this.matched.substr(0, this.matched.length-1);
if (lines.length-1) this.yylineno -= lines.length-1;
var r = this.yylloc.range;
this.yylloc = {first_line: this.yylloc.first_line,
last_line: this.yylineno+1,
first_column: this.yylloc.first_column,
last_column: lines ?
(lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length:
this.yylloc.first_column - len
};
if (this.options.ranges) {
this.yylloc.range = [r[0], r[0] + this.yyleng - len];
}
return this;
},
more:function () {
this._more = true;
return this;
},
less:function (n) {
this.unput(this.match.slice(n));
},
pastInput:function () {
var past = this.matched.substr(0, this.matched.length - this.match.length);
return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
},
upcomingInput:function () {
var next = this.match;
if (next.length < 20) {
next += this._input.substr(0, 20-next.length);
}
return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, "");
},
showPosition:function () {
var pre = this.pastInput();
var c = new Array(pre.length + 1).join("-");
return pre + this.upcomingInput() + "\n" + c+"^";
},
next:function () {
if (this.done) {
return this.EOF;
}
if (!this._input) this.done = true;
var token,
match,
tempMatch,
index,
col,
lines;
if (!this._more) {
this.yytext = '';
this.match = '';
}
var rules = this._currentRules();
for (var i=0;i < rules.length; i++) {
tempMatch = this._input.match(this.rules[rules[i]]);
if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
match = tempMatch;
index = i;
if (!this.options.flex) break;
}
}
if (match) {
lines = match[0].match(/(?:\r\n?|\n).*/g);
if (lines) this.yylineno += lines.length;
this.yylloc = {first_line: this.yylloc.last_line,
last_line: this.yylineno+1,
first_column: this.yylloc.last_column,
last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length};
this.yytext += match[0];
this.match += match[0];
this.matches = match;
this.yyleng = this.yytext.length;
if (this.options.ranges) {
this.yylloc.range = [this.offset, this.offset += this.yyleng];
}
this._more = false;
this._input = this._input.slice(match[0].length);
this.matched += match[0];
token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]);
if (this.done && this._input) this.done = false;
if (token) return token;
else return;
}
if (this._input === "") {
return this.EOF;
} else {
return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(),
{text: "", token: null, line: this.yylineno});
}
},
lex:function lex() {
var r = this.next();
if (typeof r !== 'undefined') {
return r;
} else {
return this.lex();
}
},
begin:function begin(condition) {
this.conditionStack.push(condition);
},
popState:function popState() {
return this.conditionStack.pop();
},
_currentRules:function _currentRules() {
return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules;
},
topState:function () {
return this.conditionStack[this.conditionStack.length-2];
},
pushState:function begin(condition) {
this.begin(condition);
}});
lexer.options = {};
lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
function strip(start, end) {
return yy_.yytext = yy_.yytext.substr(start, yy_.yyleng-end);
}
var YYSTATE=YY_START
switch($avoiding_name_collisions) {
case 0:
if(yy_.yytext.slice(-2) === "\\\\") {
strip(0,1);
this.begin("mu");
} else if(yy_.yytext.slice(-1) === "\\") {
strip(0,1);
this.begin("emu");
} else {
this.begin("mu");
}
if(yy_.yytext) return 14;
break;
case 1:return 14;
break;
case 2:
if(yy_.yytext.slice(-1) !== "\\") this.popState();
if(yy_.yytext.slice(-1) === "\\") strip(0,1);
return 14;
break;
case 3:strip(0,4); this.popState(); return 15;
break;
case 4:return 25;
break;
case 5:return 16;
break;
case 6:return 20;
break;
case 7:return 19;
break;
case 8:return 19;
break;
case 9:return 23;
break;
case 10:return 22;
break;
case 11:this.popState(); this.begin('com');
break;
case 12:strip(3,5); this.popState(); return 15;
break;
case 13:return 22;
break;
case 14:return 39;
break;
case 15:return 38;
break;
case 16:return 38;
break;
case 17:return 42;
break;
case 18:/*ignore whitespace*/
break;
case 19:this.popState(); return 24;
break;
case 20:this.popState(); return 18;
break;
case 21:yy_.yytext = strip(1,2).replace(/\\"/g,'"'); return 32;
break;
case 22:yy_.yytext = strip(1,2).replace(/\\'/g,"'"); return 32;
break;
case 23:return 40;
break;
case 24:return 34;
break;
case 25:return 34;
break;
case 26:return 33;
break;
case 27:return 38;
break;
case 28:yy_.yytext = strip(1,2); return 38;
break;
case 29:return 'INVALID';
break;
case 30:return 5;
break;
}
};
lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{(~)?>)/,/^(?:\{\{(~)?#)/,/^(?:\{\{(~)?\/)/,/^(?:\{\{(~)?\^)/,/^(?:\{\{(~)?\s*else\b)/,/^(?:\{\{(~)?\{)/,/^(?:\{\{(~)?&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{(~)?)/,/^(?:=)/,/^(?:\.\.)/,/^(?:\.(?=([=~}\s\/.])))/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}(~)?\}\})/,/^(?:(~)?\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=([~}\s])))/,/^(?:false(?=([~}\s])))/,/^(?:-?[0-9]+(?=([~}\s])))/,/^(?:([^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=([=~}\s\/.]))))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/];
lexer.conditions = {"mu":{"rules":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"com":{"rules":[3],"inclusive":false},"INITIAL":{"rules":[0,1,30],"inclusive":true}};
return lexer;})()
parser.lexer = lexer;
function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser;
return new Parser;
})();__exports__ = handlebars;
return __exports__;
})();
// handlebars/compiler/base.js
var __module8__ = (function(__dependency1__, __dependency2__) {
"use strict";
var __exports__ = {};
var parser = __dependency1__;
var AST = __dependency2__;
__exports__.parser = parser;
function parse(input) {
// Just return if an already-compile AST was passed in.
if(input.constructor === AST.ProgramNode) { return input; }
parser.yy = AST;
return parser.parse(input);
}
__exports__.parse = parse;
return __exports__;
})(__module9__, __module7__);
// handlebars/compiler/javascript-compiler.js
var __module11__ = (function(__dependency1__) {
"use strict";
var __exports__;
var COMPILER_REVISION = __dependency1__.COMPILER_REVISION;
var REVISION_CHANGES = __dependency1__.REVISION_CHANGES;
var log = __dependency1__.log;
function Literal(value) {
this.value = value;
}
function JavaScriptCompiler() {}
JavaScriptCompiler.prototype = {
// PUBLIC API: You can override these methods in a subclass to provide
// alternative compiled forms for name lookup and buffering semantics
nameLookup: function(parent, name /* , type*/) {
var wrap,
ret;
if (parent.indexOf('depth') === 0) {
wrap = true;
}
if (/^[0-9]+$/.test(name)) {
ret = parent + "[" + name + "]";
} else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
ret = parent + "." + name;
}
else {
ret = parent + "['" + name + "']";
}
if (wrap) {
return '(' + parent + ' && ' + ret + ')';
} else {
return ret;
}
},
appendToBuffer: function(string) {
if (this.environment.isSimple) {
return "return " + string + ";";
} else {
return {
appendToBuffer: true,
content: string,
toString: function() { return "buffer += " + string + ";"; }
};
}
},
initializeBuffer: function() {
return this.quotedString("");
},
namespace: "Handlebars",
// END PUBLIC API
compile: function(environment, options, context, asObject) {
this.environment = environment;
this.options = options || {};
log('debug', this.environment.disassemble() + "\n\n");
this.name = this.environment.name;
this.isChild = !!context;
this.context = context || {
programs: [],
environments: [],
aliases: { }
};
this.preamble();
this.stackSlot = 0;
this.stackVars = [];
this.registers = { list: [] };
this.compileStack = [];
this.inlineStack = [];
this.compileChildren(environment, options);
var opcodes = environment.opcodes, opcode;
this.i = 0;
for(var l=opcodes.length; this.i<l; this.i++) {
opcode = opcodes[this.i];
if(opcode.opcode === 'DECLARE') {
this[opcode.name] = opcode.value;
} else {
this[opcode.opcode].apply(this, opcode.args);
}
// Reset the stripNext flag if it was not set by this operation.
if (opcode.opcode !== this.stripNext) {
this.stripNext = false;
}
}
// Flush any trailing content that might be pending.
this.pushSource('');
return this.createFunctionContext(asObject);
},
preamble: function() {
var out = [];
if (!this.isChild) {
var namespace = this.namespace;
var copies = "helpers = this.merge(helpers, " + namespace + ".helpers);";
if (this.environment.usePartial) { copies = copies + " partials = this.merge(partials, " + namespace + ".partials);"; }
if (this.options.data) { copies = copies + " data = data || {};"; }
out.push(copies);
} else {
out.push('');
}
if (!this.environment.isSimple) {
out.push(", buffer = " + this.initializeBuffer());
} else {
out.push("");
}
// track the last context pushed into place to allow skipping the
// getContext opcode when it would be a noop
this.lastContext = 0;
this.source = out;
},
createFunctionContext: function(asObject) {
var locals = this.stackVars.concat(this.registers.list);
if(locals.length > 0) {
this.source[1] = this.source[1] + ", " + locals.join(", ");
}
// Generate minimizer alias mappings
if (!this.isChild) {
for (var alias in this.context.aliases) {
if (this.context.aliases.hasOwnProperty(alias)) {
this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
}
}
}
if (this.source[1]) {
this.source[1] = "var " + this.source[1].substring(2) + ";";
}
// Merge children
if (!this.isChild) {
this.source[1] += '\n' + this.context.programs.join('\n') + '\n';
}
if (!this.environment.isSimple) {
this.pushSource("return buffer;");
}
var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"];
for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
params.push("depth" + this.environment.depths.list[i]);
}
// Perform a second pass over the output to merge content when possible
var source = this.mergeSource();
if (!this.isChild) {
var revision = COMPILER_REVISION,
versions = REVISION_CHANGES[revision];
source = "this.compilerInfo = ["+revision+",'"+versions+"'];\n"+source;
}
if (asObject) {
params.push(source);
return Function.apply(this, params);
} else {
var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n ' + source + '}';
log('debug', functionSource + "\n\n");
return functionSource;
}
},
mergeSource: function() {
// WARN: We are not handling the case where buffer is still populated as the source should
// not have buffer append operations as their final action.
var source = '',
buffer;
for (var i = 0, len = this.source.length; i < len; i++) {
var line = this.source[i];
if (line.appendToBuffer) {
if (buffer) {
buffer = buffer + '\n + ' + line.content;
} else {
buffer = line.content;
}
} else {
if (buffer) {
source += 'buffer += ' + buffer + ';\n ';
buffer = undefined;
}
source += line + '\n ';
}
}
return source;
},
// [blockValue]
//
// On stack, before: hash, inverse, program, value
// On stack, after: return value of blockHelperMissing
//
// The purpose of this opcode is to take a block of the form
// `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and
// replace it on the stack with the result of properly
// invoking blockHelperMissing.
blockValue: function() {
this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
var params = ["depth0"];
this.setupParams(0, params);
this.replaceStack(function(current) {
params.splice(1, 0, current);
return "blockHelperMissing.call(" + params.join(", ") + ")";
});
},
// [ambiguousBlockValue]
//
// On stack, before: hash, inverse, program, value
// Compiler value, before: lastHelper=value of last found helper, if any
// On stack, after, if no lastHelper: same as [blockValue]
// On stack, after, if lastHelper: value
ambiguousBlockValue: function() {
this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
var params = ["depth0"];
this.setupParams(0, params);
var current = this.topStack();
params.splice(1, 0, current);
// Use the options value generated from the invocation
params[params.length-1] = 'options';
this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
},
// [appendContent]
//
// On stack, before: ...
// On stack, after: ...
//
// Appends the string value of `content` to the current buffer
appendContent: function(content) {
if (this.pendingContent) {
content = this.pendingContent + content;
}
if (this.stripNext) {
content = content.replace(/^\s+/, '');
}
this.pendingContent = content;
},
// [strip]
//
// On stack, before: ...
// On stack, after: ...
//
// Removes any trailing whitespace from the prior content node and flags
// the next operation for stripping if it is a content node.
strip: function() {
if (this.pendingContent) {
this.pendingContent = this.pendingContent.replace(/\s+$/, '');
}
this.stripNext = 'strip';
},
// [append]
//
// On stack, before: value, ...
// On stack, after: ...
//
// Coerces `value` to a String and appends it to the current buffer.
//
// If `value` is truthy, or 0, it is coerced into a string and appended
// Otherwise, the empty string is appended
append: function() {
// Force anything that is inlined onto the stack so we don't have duplication
// when we examine local
this.flushInline();
var local = this.popStack();
this.pushSource("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
if (this.environment.isSimple) {
this.pushSource("else { " + this.appendToBuffer("''") + " }");
}
},
// [appendEscaped]
//
// On stack, before: value, ...
// On stack, after: ...
//
// Escape `value` and append it to the buffer
appendEscaped: function() {
this.context.aliases.escapeExpression = 'this.escapeExpression';
this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
},
// [getContext]
//
// On stack, before: ...
// On stack, after: ...
// Compiler value, after: lastContext=depth
//
// Set the value of the `lastContext` compiler value to the depth
getContext: function(depth) {
if(this.lastContext !== depth) {
this.lastContext = depth;
}
},
// [lookupOnContext]
//
// On stack, before: ...
// On stack, after: currentContext[name], ...
//
// Looks up the value of `name` on the current context and pushes
// it onto the stack.
lookupOnContext: function(name) {
this.push(this.nameLookup('depth' + this.lastContext, name, 'context'));
},
// [pushContext]
//
// On stack, before: ...
// On stack, after: currentContext, ...
//
// Pushes the value of the current context onto the stack.
pushContext: function() {
this.pushStackLiteral('depth' + this.lastContext);
},
// [resolvePossibleLambda]
//
// On stack, before: value, ...
// On stack, after: resolved value, ...
//
// If the `value` is a lambda, replace it on the stack by
// the return value of the lambda
resolvePossibleLambda: function() {
this.context.aliases.functionType = '"function"';
this.replaceStack(function(current) {
return "typeof " + current + " === functionType ? " + current + ".apply(depth0) : " + current;
});
},
// [lookup]
//
// On stack, before: value, ...
// On stack, after: value[name], ...
//
// Replace the value on the stack with the result of looking
// up `name` on `value`
lookup: function(name) {
this.replaceStack(function(current) {
return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context');
});
},
// [lookupData]
//
// On stack, before: ...
// On stack, after: data, ...
//
// Push the data lookup operator
lookupData: function() {
this.push('data');
},
// [pushStringParam]
//
// On stack, before: ...
// On stack, after: string, currentContext, ...
//
// This opcode is designed for use in string mode, which
// provides the string value of a parameter along with its
// depth rather than resolving it immediately.
pushStringParam: function(string, type) {
this.pushStackLiteral('depth' + this.lastContext);
this.pushString(type);
if (typeof string === 'string') {
this.pushString(string);
} else {
this.pushStackLiteral(string);
}
},
emptyHash: function() {
this.pushStackLiteral('{}');
if (this.options.stringParams) {
this.register('hashTypes', '{}');
this.register('hashContexts', '{}');
}
},
pushHash: function() {
this.hash = {values: [], types: [], contexts: []};
},
popHash: function() {
var hash = this.hash;
this.hash = undefined;
if (this.options.stringParams) {
this.register('hashContexts', '{' + hash.contexts.join(',') + '}');
this.register('hashTypes', '{' + hash.types.join(',') + '}');
}
this.push('{\n ' + hash.values.join(',\n ') + '\n }');
},
// [pushString]
//
// On stack, before: ...
// On stack, after: quotedString(string), ...
//
// Push a quoted version of `string` onto the stack
pushString: function(string) {
this.pushStackLiteral(this.quotedString(string));
},
// [push]
//
// On stack, before: ...
// On stack, after: expr, ...
//
// Push an expression onto the stack
push: function(expr) {
this.inlineStack.push(expr);
return expr;
},
// [pushLiteral]
//
// On stack, before: ...
// On stack, after: value, ...
//
// Pushes a value onto the stack. This operation prevents
// the compiler from creating a temporary variable to hold
// it.
pushLiteral: function(value) {
this.pushStackLiteral(value);
},
// [pushProgram]
//
// On stack, before: ...
// On stack, after: program(guid), ...
//
// Push a program expression onto the stack. This takes
// a compile-time guid and converts it into a runtime-accessible
// expression.
pushProgram: function(guid) {
if (guid != null) {
this.pushStackLiteral(this.programExpression(guid));
} else {
this.pushStackLiteral(null);
}
},
// [invokeHelper]
//
// On stack, before: hash, inverse, program, params..., ...
// On stack, after: result of helper invocation
//
// Pops off the helper's parameters, invokes the helper,
// and pushes the helper's return value onto the stack.
//
// If the helper is not found, `helperMissing` is called.
invokeHelper: function(paramSize, name) {
this.context.aliases.helperMissing = 'helpers.helperMissing';
var helper = this.lastHelper = this.setupHelper(paramSize, name, true);
var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
this.push(helper.name + ' || ' + nonHelper);
this.replaceStack(function(name) {
return name + ' ? ' + name + '.call(' +
helper.callParams + ") " + ": helperMissing.call(" +
helper.helperMissingParams + ")";
});
},
// [invokeKnownHelper]
//
// On stack, before: hash, inverse, program, params..., ...
// On stack, after: result of helper invocation
//
// This operation is used when the helper is known to exist,
// so a `helperMissing` fallback is not required.
invokeKnownHelper: function(paramSize, name) {
var helper = this.setupHelper(paramSize, name);
this.push(helper.name + ".call(" + helper.callParams + ")");
},
// [invokeAmbiguous]
//
// On stack, before: hash, inverse, program, params..., ...
// On stack, after: result of disambiguation
//
// This operation is used when an expression like `{{foo}}`
// is provided, but we don't know at compile-time whether it
// is a helper or a path.
//
// This operation emits more code than the other options,
// and can be avoided by passing the `knownHelpers` and
// `knownHelpersOnly` flags at compile-time.
invokeAmbiguous: function(name, helperCall) {
this.context.aliases.functionType = '"function"';
this.pushStackLiteral('{}'); // Hash value
var helper = this.setupHelper(0, name, helperCall);
var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
var nextStack = this.nextStack();
this.pushSource('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }');
this.pushSource('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.call(' + helper.callParams + ') : ' + nextStack + '; }');
},
// [invokePartial]
//
// On stack, before: context, ...
// On stack after: result of partial invocation
//
// This operation pops off a context, invokes a partial with that context,
// and pushes the result of the invocation back.
invokePartial: function(name) {
var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers", "partials"];
if (this.options.data) {
params.push("data");
}
this.context.aliases.self = "this";
this.push("self.invokePartial(" + params.join(", ") + ")");
},
// [assignToHash]
//
// On stack, before: value, hash, ...
// On stack, after: hash, ...
//
// Pops a value and hash off the stack, assigns `hash[key] = value`
// and pushes the hash back onto the stack.
assignToHash: function(key) {
var value = this.popStack(),
context,
type;
if (this.options.stringParams) {
type = this.popStack();
context = this.popStack();
}
var hash = this.hash;
if (context) {
hash.contexts.push("'" + key + "': " + context);
}
if (type) {
hash.types.push("'" + key + "': " + type);
}
hash.values.push("'" + key + "': (" + value + ")");
},
// HELPERS
compiler: JavaScriptCompiler,
compileChildren: function(environment, options) {
var children = environment.children, child, compiler;
for(var i=0, l=children.length; i<l; i++) {
child = children[i];
compiler = new this.compiler();
var index = this.matchExistingProgram(child);
if (index == null) {
this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
index = this.context.programs.length;
child.index = index;
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(child, options, this.context);
this.context.environments[index] = child;
} else {
child.index = index;
child.name = 'program' + index;
}
}
},
matchExistingProgram: function(child) {
for (var i = 0, len = this.context.environments.length; i < len; i++) {
var environment = this.context.environments[i];
if (environment && environment.equals(child)) {
return i;
}
}
},
programExpression: function(guid) {
this.context.aliases.self = "this";
if(guid == null) {
return "self.noop";
}
var child = this.environment.children[guid],
depths = child.depths.list, depth;
var programParams = [child.index, child.name, "data"];
for(var i=0, l = depths.length; i<l; i++) {
depth = depths[i];
if(depth === 1) { programParams.push("depth0"); }
else { programParams.push("depth" + (depth - 1)); }
}
return (depths.length === 0 ? "self.program(" : "self.programWithDepth(") + programParams.join(", ") + ")";
},
register: function(name, val) {
this.useRegister(name);
this.pushSource(name + " = " + val + ";");
},
useRegister: function(name) {
if(!this.registers[name]) {
this.registers[name] = true;
this.registers.list.push(name);
}
},
pushStackLiteral: function(item) {
return this.push(new Literal(item));
},
pushSource: function(source) {
if (this.pendingContent) {
this.source.push(this.appendToBuffer(this.quotedString(this.pendingContent)));
this.pendingContent = undefined;
}
if (source) {
this.source.push(source);
}
},
pushStack: function(item) {
this.flushInline();
var stack = this.incrStack();
if (item) {
this.pushSource(stack + " = " + item + ";");
}
this.compileStack.push(stack);
return stack;
},
replaceStack: function(callback) {
var prefix = '',
inline = this.isInline(),
stack;
// If we are currently inline then we want to merge the inline statement into the
// replacement statement via ','
if (inline) {
var top = this.popStack(true);
if (top instanceof Literal) {
// Literals do not need to be inlined
stack = top.value;
} else {
// Get or create the current stack name for use by the inline
var name = this.stackSlot ? this.topStackName() : this.incrStack();
prefix = '(' + this.push(name) + ' = ' + top + '),';
stack = this.topStack();
}
} else {
stack = this.topStack();
}
var item = callback.call(this, stack);
if (inline) {
if (this.inlineStack.length || this.compileStack.length) {
this.popStack();
}
this.push('(' + prefix + item + ')');
} else {
// Prevent modification of the context depth variable. Through replaceStack
if (!/^stack/.test(stack)) {
stack = this.nextStack();
}
this.pushSource(stack + " = (" + prefix + item + ");");
}
return stack;
},
nextStack: function() {
return this.pushStack();
},
incrStack: function() {
this.stackSlot++;
if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
return this.topStackName();
},
topStackName: function() {
return "stack" + this.stackSlot;
},
flushInline: function() {
var inlineStack = this.inlineStack;
if (inlineStack.length) {
this.inlineStack = [];
for (var i = 0, len = inlineStack.length; i < len; i++) {
var entry = inlineStack[i];
if (entry instanceof Literal) {
this.compileStack.push(entry);
} else {
this.pushStack(entry);
}
}
}
},
isInline: function() {
return this.inlineStack.length;
},
popStack: function(wrapped) {
var inline = this.isInline(),
item = (inline ? this.inlineStack : this.compileStack).pop();
if (!wrapped && (item instanceof Literal)) {
return item.value;
} else {
if (!inline) {
this.stackSlot--;
}
return item;
}
},
topStack: function(wrapped) {
var stack = (this.isInline() ? this.inlineStack : this.compileStack),
item = stack[stack.length - 1];
if (!wrapped && (item instanceof Literal)) {
return item.value;
} else {
return item;
}
},
quotedString: function(str) {
return '"' + str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4
.replace(/\u2029/g, '\\u2029') + '"';
},
setupHelper: function(paramSize, name, missingParams) {
var params = [];
this.setupParams(paramSize, params, missingParams);
var foundHelper = this.nameLookup('helpers', name, 'helper');
return {
params: params,
name: foundHelper,
callParams: ["depth0"].concat(params).join(", "),
helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ")
};
},
// the params and contexts arguments are passed in arrays
// to fill in
setupParams: function(paramSize, params, useRegister) {
var options = [], contexts = [], types = [], param, inverse, program;
options.push("hash:" + this.popStack());
inverse = this.popStack();
program = this.popStack();
// Avoid setting fn and inverse if neither are set. This allows
// helpers to do a check for `if (options.fn)`
if (program || inverse) {
if (!program) {
this.context.aliases.self = "this";
program = "self.noop";
}
if (!inverse) {
this.context.aliases.self = "this";
inverse = "self.noop";
}
options.push("inverse:" + inverse);
options.push("fn:" + program);
}
for(var i=0; i<paramSize; i++) {
param = this.popStack();
params.push(param);
if(this.options.stringParams) {
types.push(this.popStack());
contexts.push(this.popStack());
}
}
if (this.options.stringParams) {
options.push("contexts:[" + contexts.join(",") + "]");
options.push("types:[" + types.join(",") + "]");
options.push("hashContexts:hashContexts");
options.push("hashTypes:hashTypes");
}
if(this.options.data) {
options.push("data:data");
}
options = "{" + options.join(",") + "}";
if (useRegister) {
this.register('options', options);
params.push('options');
} else {
params.push(options);
}
return params.join(", ");
}
};
var reservedWords = (
"break else new var" +
" case finally return void" +
" catch for switch while" +
" continue function this with" +
" default if throw" +
" delete in try" +
" do instanceof typeof" +
" abstract enum int short" +
" boolean export interface static" +
" byte extends long super" +
" char final native synchronized" +
" class float package throws" +
" const goto private transient" +
" debugger implements protected volatile" +
" double import public let yield"
).split(" ");
var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
for(var i=0, l=reservedWords.length; i<l; i++) {
compilerWords[reservedWords[i]] = true;
}
JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) {
return true;
}
return false;
};
__exports__ = JavaScriptCompiler;
return __exports__;
})(__module2__);
// handlebars/compiler/compiler.js
var __module10__ = (function(__dependency1__, __dependency2__, __dependency3__, __dependency4__) {
"use strict";
var __exports__ = {};
var Exception = __dependency1__;
var parse = __dependency2__.parse;
var JavaScriptCompiler = __dependency3__;
var AST = __dependency4__;
function Compiler() {}
__exports__.Compiler = Compiler;// the foundHelper register will disambiguate helper lookup from finding a
// function in a context. This is necessary for mustache compatibility, which
// requires that context functions in blocks are evaluated by blockHelperMissing,
// and then proceed as if the resulting value was provided to blockHelperMissing.
Compiler.prototype = {
compiler: Compiler,
disassemble: function() {
var opcodes = this.opcodes, opcode, out = [], params, param;
for (var i=0, l=opcodes.length; i<l; i++) {
opcode = opcodes[i];
if (opcode.opcode === 'DECLARE') {
out.push("DECLARE " + opcode.name + "=" + opcode.value);
} else {
params = [];
for (var j=0; j<opcode.args.length; j++) {
param = opcode.args[j];
if (typeof param === "string") {
param = "\"" + param.replace("\n", "\\n") + "\"";
}
params.push(param);
}
out.push(opcode.opcode + " " + params.join(" "));
}
}
return out.join("\n");
},
equals: function(other) {
var len = this.opcodes.length;
if (other.opcodes.length !== len) {
return false;
}
for (var i = 0; i < len; i++) {
var opcode = this.opcodes[i],
otherOpcode = other.opcodes[i];
if (opcode.opcode !== otherOpcode.opcode || opcode.args.length !== otherOpcode.args.length) {
return false;
}
for (var j = 0; j < opcode.args.length; j++) {
if (opcode.args[j] !== otherOpcode.args[j]) {
return false;
}
}
}
len = this.children.length;
if (other.children.length !== len) {
return false;
}
for (i = 0; i < len; i++) {
if (!this.children[i].equals(other.children[i])) {
return false;
}
}
return true;
},
guid: 0,
compile: function(program, options) {
this.opcodes = [];
this.children = [];
this.depths = {list: []};
this.options = options;
// These changes will propagate to the other compiler components
var knownHelpers = this.options.knownHelpers;
this.options.knownHelpers = {
'helperMissing': true,
'blockHelperMissing': true,
'each': true,
'if': true,
'unless': true,
'with': true,
'log': true
};
if (knownHelpers) {
for (var name in knownHelpers) {
this.options.knownHelpers[name] = knownHelpers[name];
}
}
return this.accept(program);
},
accept: function(node) {
var strip = node.strip || {},
ret;
if (strip.left) {
this.opcode('strip');
}
ret = this[node.type](node);
if (strip.right) {
this.opcode('strip');
}
return ret;
},
program: function(program) {
var statements = program.statements;
for(var i=0, l=statements.length; i<l; i++) {
this.accept(statements[i]);
}
this.isSimple = l === 1;
this.depths.list = this.depths.list.sort(function(a, b) {
return a - b;
});
return this;
},
compileProgram: function(program) {
var result = new this.compiler().compile(program, this.options);
var guid = this.guid++, depth;
this.usePartial = this.usePartial || result.usePartial;
this.children[guid] = result;
for(var i=0, l=result.depths.list.length; i<l; i++) {
depth = result.depths.list[i];
if(depth < 2) { continue; }
else { this.addDepth(depth - 1); }
}
return guid;
},
block: function(block) {
var mustache = block.mustache,
program = block.program,
inverse = block.inverse;
if (program) {
program = this.compileProgram(program);
}
if (inverse) {
inverse = this.compileProgram(inverse);
}
var type = this.classifyMustache(mustache);
if (type === "helper") {
this.helperMustache(mustache, program, inverse);
} else if (type === "simple") {
this.simpleMustache(mustache);
// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.opcode('emptyHash');
this.opcode('blockValue');
} else {
this.ambiguousMustache(mustache, program, inverse);
// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.opcode('emptyHash');
this.opcode('ambiguousBlockValue');
}
this.opcode('append');
},
hash: function(hash) {
var pairs = hash.pairs, pair, val;
this.opcode('pushHash');
for(var i=0, l=pairs.length; i<l; i++) {
pair = pairs[i];
val = pair[1];
if (this.options.stringParams) {
if(val.depth) {
this.addDepth(val.depth);
}
this.opcode('getContext', val.depth || 0);
this.opcode('pushStringParam', val.stringModeValue, val.type);
} else {
this.accept(val);
}
this.opcode('assignToHash', pair[0]);
}
this.opcode('popHash');
},
partial: function(partial) {
var partialName = partial.partialName;
this.usePartial = true;
if(partial.context) {
this.ID(partial.context);
} else {
this.opcode('push', 'depth0');
}
this.opcode('invokePartial', partialName.name);
this.opcode('append');
},
content: function(content) {
this.opcode('appendContent', content.string);
},
mustache: function(mustache) {
var options = this.options;
var type = this.classifyMustache(mustache);
if (type === "simple") {
this.simpleMustache(mustache);
} else if (type === "helper") {
this.helperMustache(mustache);
} else {
this.ambiguousMustache(mustache);
}
if(mustache.escaped && !options.noEscape) {
this.opcode('appendEscaped');
} else {
this.opcode('append');
}
},
ambiguousMustache: function(mustache, program, inverse) {
var id = mustache.id,
name = id.parts[0],
isBlock = program != null || inverse != null;
this.opcode('getContext', id.depth);
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.opcode('invokeAmbiguous', name, isBlock);
},
simpleMustache: function(mustache) {
var id = mustache.id;
if (id.type === 'DATA') {
this.DATA(id);
} else if (id.parts.length) {
this.ID(id);
} else {
// Simplified ID for `this`
this.addDepth(id.depth);
this.opcode('getContext', id.depth);
this.opcode('pushContext');
}
this.opcode('resolvePossibleLambda');
},
helperMustache: function(mustache, program, inverse) {
var params = this.setupFullMustacheParams(mustache, program, inverse),
name = mustache.id.parts[0];
if (this.options.knownHelpers[name]) {
this.opcode('invokeKnownHelper', params.length, name);
} else if (this.options.knownHelpersOnly) {
throw new Error("You specified knownHelpersOnly, but used the unknown helper " + name);
} else {
this.opcode('invokeHelper', params.length, name);
}
},
ID: function(id) {
this.addDepth(id.depth);
this.opcode('getContext', id.depth);
var name = id.parts[0];
if (!name) {
this.opcode('pushContext');
} else {
this.opcode('lookupOnContext', id.parts[0]);
}
for(var i=1, l=id.parts.length; i<l; i++) {
this.opcode('lookup', id.parts[i]);
}
},
DATA: function(data) {
this.options.data = true;
if (data.id.isScoped || data.id.depth) {
throw new Exception('Scoped data references are not supported: ' + data.original);
}
this.opcode('lookupData');
var parts = data.id.parts;
for(var i=0, l=parts.length; i<l; i++) {
this.opcode('lookup', parts[i]);
}
},
STRING: function(string) {
this.opcode('pushString', string.string);
},
INTEGER: function(integer) {
this.opcode('pushLiteral', integer.integer);
},
BOOLEAN: function(bool) {
this.opcode('pushLiteral', bool.bool);
},
comment: function() {},
// HELPERS
opcode: function(name) {
this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) });
},
declare: function(name, value) {
this.opcodes.push({ opcode: 'DECLARE', name: name, value: value });
},
addDepth: function(depth) {
if(isNaN(depth)) { throw new Error("EWOT"); }
if(depth === 0) { return; }
if(!this.depths[depth]) {
this.depths[depth] = true;
this.depths.list.push(depth);
}
},
classifyMustache: function(mustache) {
var isHelper = mustache.isHelper;
var isEligible = mustache.eligibleHelper;
var options = this.options;
// if ambiguous, we can possibly resolve the ambiguity now
if (isEligible && !isHelper) {
var name = mustache.id.parts[0];
if (options.knownHelpers[name]) {
isHelper = true;
} else if (options.knownHelpersOnly) {
isEligible = false;
}
}
if (isHelper) { return "helper"; }
else if (isEligible) { return "ambiguous"; }
else { return "simple"; }
},
pushParams: function(params) {
var i = params.length, param;
while(i--) {
param = params[i];
if(this.options.stringParams) {
if(param.depth) {
this.addDepth(param.depth);
}
this.opcode('getContext', param.depth || 0);
this.opcode('pushStringParam', param.stringModeValue, param.type);
} else {
this[param.type](param);
}
}
},
setupMustacheParams: function(mustache) {
var params = mustache.params;
this.pushParams(params);
if(mustache.hash) {
this.hash(mustache.hash);
} else {
this.opcode('emptyHash');
}
return params;
},
// this will replace setupMustacheParams when we're done
setupFullMustacheParams: function(mustache, program, inverse) {
var params = mustache.params;
this.pushParams(params);
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
if(mustache.hash) {
this.hash(mustache.hash);
} else {
this.opcode('emptyHash');
}
return params;
}
};
function precompile(input, options) {
if (input == null || (typeof input !== 'string' && input.constructor !== AST.ProgramNode)) {
throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
}
options = options || {};
if (!('data' in options)) {
options.data = true;
}
var ast = parse(input);
var environment = new Compiler().compile(ast, options);
return new JavaScriptCompiler().compile(environment, options);
}
__exports__.precompile = precompile;function compile(input, options, env) {
if (input == null || (typeof input !== 'string' && input.constructor !== AST.ProgramNode)) {
throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
}
options = options || {};
if (!('data' in options)) {
options.data = true;
}
var compiled;
function compileInput() {
var ast = parse(input);
var environment = new Compiler().compile(ast, options);
var templateSpec = new JavaScriptCompiler().compile(environment, options, undefined, true);
return env.template(templateSpec);
}
// Template is only compiled on first use and cached after that point.
return function(context, options) {
if (!compiled) {
compiled = compileInput();
}
return compiled.call(this, context, options);
};
}
__exports__.compile = compile;
return __exports__;
})(__module5__, __module8__, __module11__, __module7__);
// handlebars.js
var __module0__ = (function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__) {
"use strict";
var __exports__;
var Handlebars = __dependency1__;
// Compiler imports
var AST = __dependency2__;
var Parser = __dependency3__.parser;
var parse = __dependency3__.parse;
var Compiler = __dependency4__.Compiler;
var compile = __dependency4__.compile;
var precompile = __dependency4__.precompile;
var JavaScriptCompiler = __dependency5__;
var _create = Handlebars.create;
var create = function() {
var hb = _create();
hb.compile = function(input, options) {
return compile(input, options, hb);
};
hb.precompile = precompile;
hb.AST = AST;
hb.Compiler = Compiler;
hb.JavaScriptCompiler = JavaScriptCompiler;
hb.Parser = Parser;
hb.parse = parse;
return hb;
};
Handlebars = create();
Handlebars.create = create;
__exports__ = Handlebars;
return __exports__;
})(__module1__, __module7__, __module8__, __module10__, __module11__);
return __module0__;
})();
This source diff could not be displayed because it is too large. You can view the blob instead.
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 2011–2014, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
\ No newline at end of file
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define("virtual-dom-1.3.0",[],e);else{var n;"undefined"!=typeof window?n=window:"undefined"!=typeof global?n=global:"undefined"!=typeof self&&(n=self),n.virtualDom=e()}}(function(){return function e(n,t,r){function o(s,u){if(!t[s]){if(!n[s]){var a="function"==typeof require&&require;if(!u&&a)return a(s,!0);if(i)return i(s,!0);var f=new Error("Cannot find module '"+s+"'");throw f.code="MODULE_NOT_FOUND",f}var v=t[s]={exports:{}};n[s][0].call(v.exports,function(e){var t=n[s][1][e];return o(t?t:e)},v,v.exports,e,n,t,r)}return t[s].exports}for(var i="function"==typeof require&&require,s=0;s<r.length;s++)o(r[s]);return o}({1:[function(e,n){var t=e("./vdom/create-element.js");n.exports=t},{"./vdom/create-element.js":15}],2:[function(e,n){var t=e("./vtree/diff.js");n.exports=t},{"./vtree/diff.js":35}],3:[function(e,n){var t=e("./virtual-hyperscript/index.js");n.exports=t},{"./virtual-hyperscript/index.js":22}],4:[function(e,n){var t=e("./diff.js"),r=e("./patch.js"),o=e("./h.js"),i=e("./create-element.js");n.exports={diff:t,patch:r,h:o,create:i}},{"./create-element.js":1,"./diff.js":2,"./h.js":3,"./patch.js":13}],5:[function(e,n){n.exports=function(e){var n,t=String.prototype.split,r=/()??/.exec("")[1]===e;return n=function(n,o,i){if("[object RegExp]"!==Object.prototype.toString.call(o))return t.call(n,o,i);var s,u,a,f,v=[],d=(o.ignoreCase?"i":"")+(o.multiline?"m":"")+(o.extended?"x":"")+(o.sticky?"y":""),c=0,o=new RegExp(o.source,d+"g");for(n+="",r||(s=new RegExp("^"+o.source+"$(?!\\s)",d)),i=i===e?-1>>>0:i>>>0;(u=o.exec(n))&&(a=u.index+u[0].length,!(a>c&&(v.push(n.slice(c,u.index)),!r&&u.length>1&&u[0].replace(s,function(){for(var n=1;n<arguments.length-2;n++)arguments[n]===e&&(u[n]=e)}),u.length>1&&u.index<n.length&&Array.prototype.push.apply(v,u.slice(1)),f=u[0].length,c=a,v.length>=i)));)o.lastIndex===u.index&&o.lastIndex++;return c===n.length?(f||!o.test(""))&&v.push(""):v.push(n.slice(c)),v.length>i?v.slice(0,i):v}}()},{}],6:[function(){},{}],7:[function(e,n){"use strict";function t(e){var n=e[i];return n||(n=e[i]={}),n}var r=e("individual/one-version"),o="7";r("ev-store",o);var i="__EV_STORE_KEY@"+o;n.exports=t},{"individual/one-version":9}],8:[function(e,n){(function(e){"use strict";function t(e,n){return e in r?r[e]:(r[e]=n,n)}var r="undefined"!=typeof window?window:"undefined"!=typeof e?e:{};n.exports=t}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],9:[function(e,n){"use strict";function t(e,n,t){var o="__INDIVIDUAL_ONE_VERSION_"+e,i=o+"_ENFORCE_SINGLETON",s=r(i,n);if(s!==n)throw new Error("Can only have one copy of "+e+".\nYou already have version "+s+" installed.\nThis means you cannot install version "+n);return r(o,t)}var r=e("./index.js");n.exports=t},{"./index.js":8}],10:[function(e,n){(function(t){var r="undefined"!=typeof t?t:"undefined"!=typeof window?window:{},o=e("min-document");if("undefined"!=typeof document)n.exports=document;else{var i=r["__GLOBAL_DOCUMENT_CACHE@4"];i||(i=r["__GLOBAL_DOCUMENT_CACHE@4"]=o),n.exports=i}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"min-document":6}],11:[function(e,n){"use strict";n.exports=function(e){return"object"==typeof e&&null!==e}},{}],12:[function(e,n){function t(e){return"[object Array]"===o.call(e)}var r=Array.isArray,o=Object.prototype.toString;n.exports=r||t},{}],13:[function(e,n){var t=e("./vdom/patch.js");n.exports=t},{"./vdom/patch.js":18}],14:[function(e,n){function t(e,n,t){for(var i in n){var a=n[i];void 0===a?r(e,i,a,t):u(a)?(r(e,i,a,t),a.hook&&a.hook(e,i,t?t[i]:void 0)):s(a)?o(e,n,t,i,a):e[i]=a}}function r(e,n,t,r){if(r){var o=r[n];if(u(o))o.unhook&&o.unhook(e,n,t);else if("attributes"===n)for(var i in o)e.removeAttribute(i);else if("style"===n)for(var s in o)e.style[s]="";else e[n]="string"==typeof o?"":null}}function o(e,n,t,r,o){var u=t?t[r]:void 0;if("attributes"!==r){if(u&&s(u)&&i(u)!==i(o))return void(e[r]=o);s(e[r])||(e[r]={});var a="style"===r?"":void 0;for(var f in o){var v=o[f];e[r][f]=void 0===v?a:v}}else for(var d in o){var c=o[d];void 0===c?e.removeAttribute(d):e.setAttribute(d,c)}}function i(e){return Object.getPrototypeOf?Object.getPrototypeOf(e):e.__proto__?e.__proto__:e.constructor?e.constructor.prototype:void 0}var s=e("is-object"),u=e("../vnode/is-vhook.js");n.exports=t},{"../vnode/is-vhook.js":26,"is-object":11}],15:[function(e,n){function t(e,n){var f=n?n.document||r:r,v=n?n.warn:null;if(e=a(e).a,u(e))return e.init();if(s(e))return f.createTextNode(e.text);if(!i(e))return v&&v("Item is not a valid virtual dom node",e),null;var d=null===e.namespace?f.createElement(e.tagName):f.createElementNS(e.namespace,e.tagName),c=e.properties;o(d,c);for(var p=e.children,l=0;l<p.length;l++){var h=t(p[l],n);h&&d.appendChild(h)}return d}var r=e("global/document"),o=e("./apply-properties"),i=e("../vnode/is-vnode.js"),s=e("../vnode/is-vtext.js"),u=e("../vnode/is-widget.js"),a=e("../vnode/handle-thunk.js");n.exports=t},{"../vnode/handle-thunk.js":24,"../vnode/is-vnode.js":27,"../vnode/is-vtext.js":28,"../vnode/is-widget.js":29,"./apply-properties":14,"global/document":10}],16:[function(e,n){function t(e,n,t,o){return t&&0!==t.length?(t.sort(i),r(e,n,t,o,0)):{}}function r(e,n,t,i,u){if(i=i||{},e){o(t,u,u)&&(i[u]=e);var a=n.children;if(a)for(var f=e.childNodes,v=0;v<n.children.length;v++){u+=1;var d=a[v]||s,c=u+(d.count||0);o(t,u,c)&&r(f[v],d,t,i,u),u=c}}return i}function o(e,n,t){if(0===e.length)return!1;for(var r,o,i=0,s=e.length-1;s>=i;){if(r=(s+i)/2>>0,o=e[r],i===s)return o>=n&&t>=o;if(n>o)i=r+1;else{if(!(o>t))return!0;s=r-1}}return!1}function i(e,n){return e>n?1:-1}var s={};n.exports=t},{}],17:[function(e,n){function t(e,n,t){var a=e.type,c=e.vNode,l=e.patch;switch(a){case p.REMOVE:return r(n,c);case p.INSERT:return o(n,l,t);case p.VTEXT:return i(n,c,l,t);case p.WIDGET:return s(n,c,l,t);case p.VNODE:return u(n,c,l,t);case p.ORDER:return f(n,l),n;case p.PROPS:return d(n,l,c.properties),n;case p.THUNK:return v(n,t.patch(n,l,t));default:return n}}function r(e,n){var t=e.parentNode;return t&&t.removeChild(e),a(e,n),null}function o(e,n,t){var r=l(n,t);return e&&e.appendChild(r),e}function i(e,n,t,r){var o;if(3===e.nodeType)e.replaceData(0,e.length,t.text),o=e;else{var i=e.parentNode;o=l(t,r),i&&i.replaceChild(o,e)}return o}function s(e,n,t,r){var o,i=h(n,t);o=i?t.update(n,e)||e:l(t,r);var s=e.parentNode;return s&&o!==e&&s.replaceChild(o,e),i||a(e,n),o}function u(e,n,t,r){var o=e.parentNode,i=l(t,r);return o&&o.replaceChild(i,e),i}function a(e,n){"function"==typeof n.destroy&&c(n)&&n.destroy(e)}function f(e,n){var t,r=[],o=e.childNodes,i=o.length,s=n.reverse;for(t=0;i>t;t++)r.push(e.childNodes[t]);var u,a,f,v,d,c=0;for(t=0;i>t;){if(u=n[t],v=1,void 0!==u&&u!==t){for(;n[t+v]===u+v;)v++;for(s[t]>t+v&&c++,a=r[u],f=o[t+c]||null,d=0;a!==f&&d++<v;)e.insertBefore(a,f),a=r[u+d];t>u+v&&c--}t in n.removes&&c++,t+=v}}function v(e,n){return e&&n&&e!==n&&e.parentNode&&(console.log(e),e.parentNode.replaceChild(n,e)),n}var d=e("./apply-properties"),c=e("../vnode/is-widget.js"),p=e("../vnode/vpatch.js"),l=e("./create-element"),h=e("./update-widget");n.exports=t},{"../vnode/is-widget.js":29,"../vnode/vpatch.js":32,"./apply-properties":14,"./create-element":15,"./update-widget":19}],18:[function(e,n){function t(e,n){return r(e,n)}function r(e,n,t){var u=i(n);if(0===u.length)return e;var f=a(e,n.a,u),v=e.ownerDocument;t||(t={patch:r},v!==s&&(t.document=v));for(var d=0;d<u.length;d++){var c=u[d];e=o(e,f[c],n[c],t)}return e}function o(e,n,t,r){if(!n)return e;var o;if(u(t))for(var i=0;i<t.length;i++)o=f(t[i],n,r),n===e&&(e=o);else o=f(t,n,r),n===e&&(e=o);return e}function i(e){var n=[];for(var t in e)"a"!==t&&n.push(Number(t));return n}var s=e("global/document"),u=e("x-is-array"),a=e("./dom-index"),f=e("./patch-op");n.exports=t},{"./dom-index":16,"./patch-op":17,"global/document":10,"x-is-array":12}],19:[function(e,n){function t(e,n){return r(e)&&r(n)?"name"in e&&"name"in n?e.id===n.id:e.init===n.init:!1}var r=e("../vnode/is-widget.js");n.exports=t},{"../vnode/is-widget.js":29}],20:[function(e,n){"use strict";function t(e){return this instanceof t?void(this.value=e):new t(e)}var r=e("ev-store");n.exports=t,t.prototype.hook=function(e,n){var t=r(e),o=n.substr(3);t[o]=this.value},t.prototype.unhook=function(e,n){var t=r(e),o=n.substr(3);t[o]=void 0}},{"ev-store":7}],21:[function(e,n){"use strict";function t(e){return this instanceof t?void(this.value=e):new t(e)}n.exports=t,t.prototype.hook=function(e,n){e[n]!==this.value&&(e[n]=this.value)}},{}],22:[function(e,n){"use strict";function t(e,n,t){var i,u,a,f,d=[];return!t&&s(n)&&(t=n,u={}),u=u||n||{},i=g(e,u),u.hasOwnProperty("key")&&(a=u.key,u.key=void 0),u.hasOwnProperty("namespace")&&(f=u.namespace,u.namespace=void 0),"INPUT"!==i||f||!u.hasOwnProperty("value")||void 0===u.value||h(u.value)||(u.value=x(u.value)),o(u),void 0!==t&&null!==t&&r(t,d,i,u),new v(i,u,d,a,f)}function r(e,n,t,o){if("string"==typeof e)n.push(new d(e));else if(i(e))n.push(e);else{if(!f(e)){if(null===e||void 0===e)return;throw u({foreignObject:e,parentVnode:{tagName:t,properties:o}})}for(var s=0;s<e.length;s++)r(e[s],n,t,o)}}function o(e){for(var n in e)if(e.hasOwnProperty(n)){var t=e[n];if(h(t))continue;"ev-"===n.substr(0,3)&&(e[n]=w(t))}}function i(e){return c(e)||p(e)||l(e)||y(e)}function s(e){return"string"==typeof e||f(e)||i(e)}function u(e){var n=new Error;return n.type="virtual-hyperscript.unexpected.virtual-element",n.message="Unexpected virtual child passed to h().\nExpected a VNode / Vthunk / VWidget / string but:\ngot:\n"+a(e.foreignObject)+".\nThe parent vnode is:\n"+a(e.parentVnode),n.foreignObject=e.foreignObject,n.parentVnode=e.parentVnode,n}function a(e){try{return JSON.stringify(e,null," ")}catch(n){return String(e)}}var f=e("x-is-array"),v=e("../vnode/vnode.js"),d=e("../vnode/vtext.js"),c=e("../vnode/is-vnode"),p=e("../vnode/is-vtext"),l=e("../vnode/is-widget"),h=e("../vnode/is-vhook"),y=e("../vnode/is-thunk"),g=e("./parse-tag.js"),x=e("./hooks/soft-set-hook.js"),w=e("./hooks/ev-hook.js");n.exports=t},{"../vnode/is-thunk":25,"../vnode/is-vhook":26,"../vnode/is-vnode":27,"../vnode/is-vtext":28,"../vnode/is-widget":29,"../vnode/vnode.js":31,"../vnode/vtext.js":33,"./hooks/ev-hook.js":20,"./hooks/soft-set-hook.js":21,"./parse-tag.js":23,"x-is-array":12}],23:[function(e,n){"use strict";function t(e,n){if(!e)return"DIV";var t=!n.hasOwnProperty("id"),s=r(e,o),u=null;i.test(s[1])&&(u="DIV");var a,f,v,d;for(d=0;d<s.length;d++)f=s[d],f&&(v=f.charAt(0),u?"."===v?(a=a||[],a.push(f.substring(1,f.length))):"#"===v&&t&&(n.id=f.substring(1,f.length)):u=f);return a&&(n.className&&a.push(n.className),n.className=a.join(" ")),n.namespace?u:u.toUpperCase()}var r=e("browser-split"),o=/([\.#]?[a-zA-Z0-9_:-]+)/,i=/^\.|#/;n.exports=t},{"browser-split":5}],24:[function(e,n){function t(e,n){var t=e,o=n;return u(n)&&(o=r(n,e)),u(e)&&(t=r(e,null)),{a:t,b:o}}function r(e,n){var t=e.vnode;if(t||(t=e.vnode=e.render(n)),!(o(t)||i(t)||s(t)))throw new Error("thunk did not return a valid node");return t}var o=e("./is-vnode"),i=e("./is-vtext"),s=e("./is-widget"),u=e("./is-thunk");n.exports=t},{"./is-thunk":25,"./is-vnode":27,"./is-vtext":28,"./is-widget":29}],25:[function(e,n){function t(e){return e&&"Thunk"===e.type}n.exports=t},{}],26:[function(e,n){function t(e){return e&&("function"==typeof e.hook&&!e.hasOwnProperty("hook")||"function"==typeof e.unhook&&!e.hasOwnProperty("unhook"))}n.exports=t},{}],27:[function(e,n){function t(e){return e&&"VirtualNode"===e.type&&e.version===r}var r=e("./version");n.exports=t},{"./version":30}],28:[function(e,n){function t(e){return e&&"VirtualText"===e.type&&e.version===r}var r=e("./version");n.exports=t},{"./version":30}],29:[function(e,n){function t(e){return e&&"Widget"===e.type}n.exports=t},{}],30:[function(e,n){n.exports="1"},{}],31:[function(e,n){function t(e,n,t,r,v){this.tagName=e,this.properties=n||a,this.children=t||f,this.key=null!=r?String(r):void 0,this.namespace="string"==typeof v?v:null;var d,c=t&&t.length||0,p=0,l=!1,h=!1,y=!1;for(var g in n)if(n.hasOwnProperty(g)){var x=n[g];u(x)&&x.unhook&&(d||(d={}),d[g]=x)}for(var w=0;c>w;w++){var m=t[w];o(m)?(p+=m.count||0,!l&&m.hasWidgets&&(l=!0),!h&&m.hasThunks&&(h=!0),y||!m.hooks&&!m.descendantHooks||(y=!0)):!l&&i(m)?"function"==typeof m.destroy&&(l=!0):!h&&s(m)&&(h=!0)}this.count=c+p,this.hasWidgets=l,this.hasThunks=h,this.hooks=d,this.descendantHooks=y}var r=e("./version"),o=e("./is-vnode"),i=e("./is-widget"),s=e("./is-thunk"),u=e("./is-vhook");n.exports=t;var a={},f=[];t.prototype.version=r,t.prototype.type="VirtualNode"},{"./is-thunk":25,"./is-vhook":26,"./is-vnode":27,"./is-widget":29,"./version":30}],32:[function(e,n){function t(e,n,t){this.type=Number(e),this.vNode=n,this.patch=t}var r=e("./version");t.NONE=0,t.VTEXT=1,t.VNODE=2,t.WIDGET=3,t.PROPS=4,t.ORDER=5,t.INSERT=6,t.REMOVE=7,t.THUNK=8,n.exports=t,t.prototype.version=r,t.prototype.type="VirtualPatch"},{"./version":30}],33:[function(e,n){function t(e){this.text=String(e)}var r=e("./version");n.exports=t,t.prototype.version=r,t.prototype.type="VirtualText"},{"./version":30}],34:[function(e,n){function t(e,n){var s;for(var u in e){u in n||(s=s||{},s[u]=void 0);var a=e[u],f=n[u];if(a!==f)if(o(a)&&o(f))if(r(f)!==r(a))s=s||{},s[u]=f;else if(i(f))s=s||{},s[u]=f;else{var v=t(a,f);v&&(s=s||{},s[u]=v)}else s=s||{},s[u]=f}for(var d in n)d in e||(s=s||{},s[d]=n[d]);return s}function r(e){return Object.getPrototypeOf?Object.getPrototypeOf(e):e.__proto__?e.__proto__:e.constructor?e.constructor.prototype:void 0}var o=e("is-object"),i=e("../vnode/is-vhook");n.exports=t},{"../vnode/is-vhook":26,"is-object":11}],35:[function(e,n){function t(e,n){var t={a:e};return r(e,n,t,0),t}function r(e,n,t,r){if(e!==n){var s=t[r],a=!1;if(w(e)||w(n))u(e,n,t,r);else if(null==n)x(e)||(i(e,t,r),s=t[r]),s=p(s,new h(h.REMOVE,e,n));else if(y(n))if(y(e))if(e.tagName===n.tagName&&e.namespace===n.namespace&&e.key===n.key){var f=j(e.properties,n.properties);f&&(s=p(s,new h(h.PROPS,e,f))),s=o(e,n,t,s,r)}else s=p(s,new h(h.VNODE,e,n)),a=!0;else s=p(s,new h(h.VNODE,e,n)),a=!0;else g(n)?g(e)?e.text!==n.text&&(s=p(s,new h(h.VTEXT,e,n))):(s=p(s,new h(h.VTEXT,e,n)),a=!0):x(n)&&(x(e)||(a=!0),s=p(s,new h(h.WIDGET,e,n)));s&&(t[r]=s),a&&i(e,t,r)}}function o(e,n,t,o,i){for(var s=e.children,u=d(s,n.children),a=s.length,f=u.length,v=a>f?a:f,c=0;v>c;c++){var l=s[c],g=u[c];i+=1,l?r(l,g,t,i):g&&(o=p(o,new h(h.INSERT,null,g))),y(l)&&l.count&&(i+=l.count)}return u.moves&&(o=p(o,new h(h.ORDER,e,u.moves))),o}function i(e,n,t){f(e,n,t),s(e,n,t)}function s(e,n,t){if(x(e))"function"==typeof e.destroy&&(n[t]=p(n[t],new h(h.REMOVE,e,null)));else if(y(e)&&(e.hasWidgets||e.hasThunks))for(var r=e.children,o=r.length,i=0;o>i;i++){var a=r[i];t+=1,s(a,n,t),y(a)&&a.count&&(t+=a.count)}else w(e)&&u(e,null,n,t)}function u(e,n,r,o){var i=m(e,n),s=t(i.a,i.b);a(s)&&(r[o]=new h(h.THUNK,null,s))}function a(e){for(var n in e)if("a"!==n)return!0;return!1}function f(e,n,t){if(y(e)){if(e.hooks&&(n[t]=p(n[t],new h(h.PROPS,e,v(e.hooks)))),e.descendantHooks||e.hasThunks)for(var r=e.children,o=r.length,i=0;o>i;i++){var s=r[i];t+=1,f(s,n,t),y(s)&&s.count&&(t+=s.count)}}else w(e)&&u(e,null,n,t)}function v(e){var n={};for(var t in e)n[t]=void 0;return n}function d(e,n){var t=c(n);if(!t)return n;var r=c(e);if(!r)return n;var o={},i={};for(var s in t)o[t[s]]=r[s];for(var u in r)i[r[u]]=t[u];for(var a=e.length,f=n.length,v=a>f?a:f,d=[],p=0,l=0,h=0,y={},g=y.removes={},x=y.reverse={},w=!1;v>p;){var m=i[l];if(void 0!==m)d[l]=n[m],m!==h&&(y[m]=h,x[h]=m,w=!0),h++;else if(l in i)d[l]=void 0,g[l]=h++,w=!0;else{for(;void 0!==o[p];)p++;if(v>p){var j=n[p];j&&(d[l]=j,p!==h&&(w=!0,y[p]=h,x[h]=p),h++),p++}}l++}return w&&(d.moves=y),d}function c(e){var n,t;for(n=0;n<e.length;n++){var r=e[n];void 0!==r.key&&(t=t||{},t[r.key]=n)}return t}function p(e,n){return e?(l(e)?e.push(n):e=[e,n],e):n}var l=e("x-is-array"),h=e("../vnode/vpatch"),y=e("../vnode/is-vnode"),g=e("../vnode/is-vtext"),x=e("../vnode/is-widget"),w=e("../vnode/is-thunk"),m=e("../vnode/handle-thunk"),j=e("./diff-props");n.exports=t},{"../vnode/handle-thunk":24,"../vnode/is-thunk":25,"../vnode/is-vnode":27,"../vnode/is-vtext":28,"../vnode/is-widget":29,"../vnode/vpatch":32,"./diff-props":34,"x-is-array":12}]},{},[4])(4)});
if (window.require && !window.virtualDom) {
require(['virtual-dom-1.3.0'], function(virtualDom) { window.virtualDom = virtualDom; });
}
.themed-xblock.xblock--drag-and-drop {
background-color: #fff;
}
/* Shared styles used in header and footer */
.themed-xblock.xblock--drag-and-drop .title1 {
color: #555555;
text-transform: uppercase;
font-weight: bold;
font-style: normal;
}
/* drag-container holds the .item-bank and the .target */
.themed-xblock.xblock--drag-and-drop .drag-container {
background-color: #ebf0f2;
}
.themed-xblock.xblock--drag-and-drop .item-bank {
border-radius: 0px;
}
/*** DRAGGABLE ITEMS ***/
.themed-xblock.xblock--drag-and-drop .drag-container .option {
border-radius: 0px;
text-align: initial;
font-size: 14px;
background-color: #2e83cd;
color: #fff;
opacity: 1;
}
.themed-xblock.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input {
background-color: #ceffce;
color: #087108;
}
.themed-xblock.xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input {
background-color: #ffcece;
color: #ad0d0d;
}
.themed-xblock.xblock--drag-and-drop .drag-container .option.fade {
opacity: 0.5;
}
/*** DROP TARGET ***/
.themed-xblock.xblock--drag-and-drop .target {
background-color: #fff;
}
.themed-xblock.xblock--drag-and-drop .drag-container .target .zone p {
font-family: Arial;
font-size: 16px;
font-weight: bold;
text-align: center;
text-transform: uppercase;
}
/*** FEEDBACK ***/
.themed-xblock.xblock--drag-and-drop .feedback {
border-top: solid 1px #bdbdbd;
}
.themed-xblock.xblock--drag-and-drop .popup {
background-color: #66a5b5;
}
.themed-xblock.xblock--drag-and-drop .popup .popup-content {
color: #ffffff;
font-size: 14px;
}
.themed-xblock.xblock--drag-and-drop .popup .close {
cursor: pointer;
color: #ffffff;
font-family: "fontawesome";
font-size: 18pt;
}
.themed-xblock.xblock--drag-and-drop .link-button {
cursor: pointer;
color: #3384ca;
}
\ No newline at end of file
{% load i18n %}
<section class="themed-xblock xblock--drag-and-drop">
<i class="fa fa-spin fa-spinner initial-load-spinner"></i>{% trans "Loading drag and drop problem." %}
</section>
{% load i18n %}
<div class="xblock--drag-and-drop--editor editor-with-buttons">
{{ js_templates|safe }}
<section class="drag-builder">
<div class="tab feedback-tab">
<p class="tab-content">
{% trans "Note: do not edit the problem if students have already completed it. Delete the problem and create a new one." %}
</p>
<section class="tab-content">
<form class="feedback-form">
<label class="h3" for="display-name">{% trans "Problem title" %}</label>
<input id="display-name" value="{{ self.display_name }}" />
<label title="{{ help_texts.show_title }}">
<input class="show-title" type="checkbox"
{% if self.show_title %}checked="checked"{% endif %}>
{% trans "Show title" %}
<span class="sr">{{ help_texts.show_title }}</span>
</label>
<label class="h3" for="weight">{% trans "Maximum score" %}</label>
<input id="weight" type="number" step="0.1" value="{{ self.weight }}" />
<label class="h3" for="problem-text">{% trans "Problem text" %}</label>
<textarea id="problem-text">{{ self.question_text }}</textarea>
<label title="{{ help_texts.show_question_header }}">
<input class="show-problem-header" type="checkbox"
{% if self.show_question_header %}checked="checked"{% endif %}>
{% trans 'Show "Problem" heading' %}
<span class="sr">{{ help_texts.show_question_header }}</span>
</label>
<label class="h3" for="intro-feedback">{% trans "Introductory Feedback" %}</label>
<textarea id="intro-feedback">{{ self.data.feedback.start }}</textarea>
<label class="h3" for="final-feedback">{% trans "Final Feedback" %}</label>
<textarea id="final-feedback">{{ self.data.feedback.finish }}</textarea>
</form>
</section>
</div>
<div class="tab zones-tab hidden">
<header class="tab-header">
<h3>{% trans "Zones" %}</h3>
</header>
<section class="tab-content">
<form class="target-image-form">
<label class="h3" for="background-url">{% trans "Background URL" %}</label>
<input id="background-url"
type="text"
placeholder="{% trans 'For example, http://example.com/background.png or /static/background.png' %}">
<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="target-image-form-help">
{% blocktrans %}
Please provide a description of the image for non-visual users.
The description should provide sufficient information to allow anyone
to solve the problem even without seeing the image.
{% endblocktrans %}
</div>
<button class="btn">{% trans "Change background" %}</button>
</form>
</section>
<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" />
</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" />
</form>
</section>
<section class="tab-content">
<div class="zone-editor">
<div class="controls">
<form class="zones-form"></form>
<a href="#" class="add-zone add-element"><div class="icon add"></div>{% trans "Add a zone" %}</a>
</div>
<div class="target">
<img class="target-img">
<div class="zones-preview">
</div>
</div>
</div>
</section>
</div>
<div class="tab items-tab hidden">
<header class="tab-header">
<h3>{% trans "Items" %}</h3>
</header>
<section class="tab-content">
<form class="item-styles-form">
<label class="h3" for="item-background-color">{% trans "Background color" %}</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="item-styles-form-help">
{{ help_texts.item_background_color }}
</div>
<label class="h3" for="item-text-color">{% trans "Text color" %}</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="item-styles-form-help">
{{ help_texts.item_text_color }}
</div>
</form>
</section>
<section class="tab-content">
<form class="items-form"></form>
</section>
<footer class="tab-footer">
<a href="#" class="add-item add-element"><div class="icon add"></div>{% trans "Add an item" %}</a>
</footer>
</div>
</section>
<div class="xblock-actions">
<span class="xblock-editor-error-message"></span>
<ul>
<li class="action-item">
<a href="#" class="button action-primary continue-button">{% trans "Continue" %}</a>
</li>
<li class="action-item hidden">
<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>
<script id="zone-element-tpl" type="text/html">
<div id="{{ id }}" class="zone" data-zone="{{ title }}" style="
top:{{ y_percent }}%;
left:{{ x_percent }}%;
width:{{ width_percent }}%;
height:{{ height_percent }}%;">
<p>{{{ title }}}</p>
<p class="sr">{{{ description }}}</p>
</div>
</script>
<script id="zone-input-tpl" type="text/html">
<div class="zone-row {{ id }}" data-index="{{index}}">
<label for="zone-{{index}}-title">{{i18n "Text"}}</label>
<input type="text"
id="zone-{{index}}-title"
class="title"
value="{{ title }}"
required />
<a href="#" class="remove-zone hidden">
<div class="icon remove"></div>
</a>
<label for="zone-{{index}}-description">{{i18n "Description"}}</label>
<input type="text"
id="zone-{{index}}-description"
class="description"
value="{{ 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"
id="zone-{{index}}-width"
class="size width"
value="{{ width }}" />
<label for="zone-{{index}}-height">{{i18n "height"}}</label>
<input type="text"
id="zone-{{index}}-height"
class="size height"
value="{{ height }}" />
<br />
<label for="zone-{{index}}-x">x</label>
<input type="text"
id="zone-{{index}}-x"
class="coord x"
value="{{ x }}" />
<label for="zone-{{index}}-y">y</label>
<input type="text"
id="zone-{{index}}-y"
class="coord y"
value="{{ y }}" />
</div>
</div>
</script>
<script id="zone-dropdown-tpl" type="text/html">
<option value="{{ value }}" {{ selected }}>{{ value }}</option>
</script>
<script id="item-input-tpl" type="text/html">
<div class="item">
<div class="row">
<label for="item-{{id}}-text">{{i18n "Text"}}</label>
<input type="text"
id="item-{{id}}-text"
class="item-text"
value="{{ displayName }}" />
<label for="item-{{id}}-zone">{{i18n "Zone"}}</label>
<select id="item-{{id}}-zone"
class="zone-select">{{ dropdown }}</select>
<a href="#" class="remove-item hidden">
<div class="icon remove"></div>
</a>
</div>
<div class="row">
<label for="item-{{id}}-image-url">{{i18n "Image URL (alternative to the text)"}}</label>
<input type="text"
id="item-{{id}}-image-url"
class="item-image-url"
value="{{ imageURL }}" />
</div>
<div class="row">
<label 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 required id="item-{{id}}-image-description"
class="item-image-description">{{ imageDescription }}</textarea>
</div>
<div class="row">
<label for="item-{{id}}-success-feedback">{{i18n "Success Feedback"}}</label>
<textarea id="item-{{id}}-success-feedback"
class="success-feedback">{{ feedback.correct }}</textarea>
</div>
<div class="row">
<label for="item-{{id}}-error-feedback">{{i18n "Error Feedback"}}</label>
<textarea id="item-{{id}}-error-feedback"
class="error-feedback">{{ feedback.incorrect }}</textarea>
</div>
<div class="row advanced-link">
<a href="#">{{i18n "Show advanced settings" }}</a>
</div>
<div class="row advanced">
<label for="item-{{id}}-width-percent">{{i18n "Preferred width as a percentage of the background image width (or blank for automatic width):"}}</label>
<input type="number" id="item-{{id}}-width-percent" class="item-width" value="{{ singleDecimalFloat widthPercent }}" step="0.1" min="1" max="99" />%
</div>
<div class="row advanced">
<label for="item-{{id}}-numerical-value">
{{i18n "Optional numerical value (if you set this, learners will be prompted for this value after dropping this item)"}}
</label>
<input type="number"
step="0.1"
id="item-{{id}}-numerical-value"
class="item-numerical-value" value="{{ numericalValue }}" />
</div>
<div class="row advanced">
<label for="item-{{id}}-numerical-margin">
{{i18n "Margin ± (when a numerical value is required, values entered by learners must not differ from the expected value by more than this margin; default is zero)"}}
</label>
<input type="number"
step="0.1"
id="item-{{id}}-numerical-margin"
class="item-numerical-margin" value="{{ numericalMargin }}" />
</div>
</div>
</script>
# -*- coding: utf-8 -*-
#
# Make '_' a no-op so we can scrape strings
def _(text):
return text
# Installs xblock-sdk and dependencies needed to run the tests suite.
# Run this script inside a fresh virtual environment.
pip install -e git://github.com/edx/xblock-sdk.git@4e8e713e7dd886b8d2eb66b5001216b66b9af81a#egg=xblock-sdk
cd $VIRTUAL_ENV/src/xblock-sdk/ && pip install -r requirements/base.txt \
&& pip install -r requirements/test.txt && cd -
pip install -r requirements.txt
[REPORTS]
reports=no
[FORMAT]
max-line-length=120
[MESSAGES CONTROL]
disable=
attribute-defined-outside-init,
locally-disabled,
missing-docstring,
too-many-ancestors,
too-many-arguments,
too-many-instance-attributes,
too-few-public-methods,
too-many-public-methods,
unused-argument,
invalid-name,
no-member
[SIMILARITIES]
min-similarity-lines=8
git+https://github.com/edx/xblock-utils.git@v1.0.2#egg=xblock-utils==1.0.2
-e .
#!/usr/bin/env python
"""
Run tests for the Drag and Drop V2 XBlock.
This script is required to run our selenium tests inside the xblock-sdk workbench
because the workbench SDK's settings file is not inside any python module.
"""
import logging
import os
import sys
import workbench
if __name__ == "__main__":
# Find the location of the XBlock SDK. Note: it must be installed in development mode.
# ('python setup.py develop' or 'pip install -e')
xblock_sdk_dir = os.path.dirname(os.path.dirname(workbench.__file__))
sys.path.append(xblock_sdk_dir)
# Use the workbench settings file:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
# Configure a range of ports in case the default port of 8081 is in use
os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8081-8099")
# Silence too verbose Django logging
logging.disable(logging.DEBUG)
try:
os.mkdir('var')
except OSError:
# The var dir may already exist.
pass
from django.core.management import execute_from_command_line
args = sys.argv[1:]
paths = [arg for arg in args if arg[0] != '-']
if not paths:
paths = ["tests/"]
options = [arg for arg in args if arg not in paths]
execute_from_command_line([sys.argv[0], "test"] + paths + options)
# -*- coding: utf-8 -*-
# Imports ###########################################################
import os
from setuptools import setup
# Functions #########################################################
def package_data(pkg, root_list):
"""Generic function to find package_data for `pkg` under `root`."""
data = []
for root in root_list:
for dirname, _, files in os.walk(os.path.join(pkg, root)):
for fname in files:
data.append(os.path.relpath(os.path.join(dirname, fname), pkg))
return {pkg: data}
# Main ##############################################################
setup(
name='xblock-drag-and-drop-v2',
version='2.0.0',
description='XBlock - Drag-and-Drop v2',
packages=['drag_and_drop_v2'],
install_requires=[
'XBlock',
'xblock-utils',
'ddt',
'mock',
],
entry_points={
'xblock.v1': 'drag-and-drop-v2 = drag_and_drop_v2:DragAndDropBlock',
},
package_data=package_data("drag_and_drop_v2", ["static", "templates", "public"]),
)
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 200 200" width="200px" height="200px" xml:space="preserve">
<style type="text/css">
.st0{fill:#FBB03B;stroke:#000000;stroke-miterlimit:10;}
.st1{font-family:'Helvetica';}
.st2{font-size:24px;}
</style>
<rect class="st0" width="200" height="200"/>
<text transform="matrix(1 0 0 1 34.6152 104.5322)" class="st1 st2">200 x 200px</text>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 300" width="400px" height="300px" xml:space="preserve">
<style type="text/css">
.st0{fill:#8CC63F;stroke:#000000;stroke-miterlimit:10;}
.st1{font-family:'Helvetica';}
.st2{font-size:48px;}
</style>
<rect class="st0" width="400" height="300"/>
<text transform="matrix(1 0 0 1 71 160)" class="st1 st2">400 x 300px</text>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 60 60" width="60px" height="60px" xml:space="preserve">
<style type="text/css">
.st0{fill:#BBF03B;}
.st1{font-family:'Helvetica';}
.st2{font-size:12px;}
</style>
<rect class="st0" width="60" height="60"/>
<text transform="matrix(1 0 0 1 3 35)" class="st1 st2">60 x 60px</text>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 500 500" width="500px" height="500px" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#999999;}
.st2{fill:#F2F2F2;}
.st3{font-family:'Helvetica-Bold';}
.st4{font-size:24px;}
.st5{letter-spacing:-1;}
</style>
<g id="Layer_2">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="1.127" y1="250" x2="500" y2="250">
<stop offset="0" style="stop-color:#CCE0F4"/>
<stop offset="1" style="stop-color:#005B97"/>
</linearGradient>
<rect x="0" class="st0" width="500" height="500"/>
</g>
<g id="Layer_1">
<rect x="0" y="200" class="st1" width="250" height="100"/>
<rect x="0" y="400" class="st1" width="375" height="100"/>
<rect x="0" class="st1" width="166.7" height="100"/>
<text transform="matrix(1 0 0 1 36 58)" class="st2 st3 st4">&lt;- 1/3 -&gt;</text>
<text transform="matrix(1 0 0 1 74 260)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="22" y="0" class="st2 st3 st4"> </tspan><tspan x="29.2" y="0" class="st2 st3 st4">50% -</tspan><tspan x="91.9" y="0" class="st2 st3 st4 st5">&gt;</tspan></text>
<text transform="matrix(1 0 0 1 150 455)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="22" y="0" class="st2 st3 st4"> </tspan><tspan x="29.2" y="0" class="st2 st3 st4">75% -</tspan><tspan x="91.9" y="0" class="st2 st3 st4 st5">&gt;</tspan></text>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1600 900" width="1600px" height="900px" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#999999;}
.st2{fill:#F2F2F2;}
.st3{font-family:'Helvetica-Bold';}
.st4{font-size:60px;}
.st5{letter-spacing:1;}
.st6{letter-spacing:-3;}
</style>
<g id="Layer_2">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="1.127" y1="450" x2="1600" y2="450">
<stop offset="0" style="stop-color:#CCE0F4"/>
<stop offset="1" style="stop-color:#005B97"/>
</linearGradient>
<rect x="0" class="st0" width="1600" height="900"/>
</g>
<g id="Layer_1">
<rect y="350" class="st1" width="800" height="200"/>
<rect x="0" y="700" class="st1" width="1200" height="200"/>
<rect x="0" class="st1" width="533.33" height="200"/>
<text transform="matrix(1 0 0 1 145 115)" class="st2 st3 st4">&lt;- 1/3 -&gt;</text>
<text transform="matrix(1 0 0 1 288 462)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="55" y="0" class="st2 st3 st4 st5"> </tspan><tspan x="72.9" y="0" class="st2 st3 st4">50% -</tspan><tspan x="229.6" y="0" class="st2 st3 st4 st6">&gt;</tspan></text>
<text transform="matrix(1 0 0 1 486 821)"><tspan x="0" y="0" class="st2 st3 st4">&lt;-</tspan><tspan x="55" y="0" class="st2 st3 st4 st5"> </tspan><tspan x="72.9" y="0" class="st2 st3 st4">75% -</tspan><tspan x="229.6" y="0" class="st2 st3 st4 st6">&gt;</tspan></text>
</g>
</svg>
{
"question_text": "Note: This is an example of data in the format used by prior versions of this block. Not in particular the old `size` data.",
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone 1",
"height": 100,
"y": "200",
"x": "100",
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone 2",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"feedback": {
"incorrect": "No 1",
"correct": "Yes 1"
},
"zone": "Zone 1",
"backgroundImage": "",
"id": 0,
"size": {
"width": "190px",
"height": "auto"
}
},
{
"displayName": "2",
"feedback": {
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "Zone 2",
"backgroundImage": "",
"id": 1,
"size": {
"width": "190px",
"height": "100px"
},
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "Pic",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "Zone 1",
"backgroundImage": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==",
"id": 2,
"size": {
"width": "100px",
"height": "auto"
}
}
],
"state": {
"items": {},
"finished": true
},
"feedback": {
"start": "Intro Feedback",
"finish": "Final Feedback"
},
"title": "Drag and Drop (Old-style data)"
}
\ No newline at end of file
{
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone 1",
"height": 100,
"y": "200",
"x": "100",
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone 2",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1 here",
"feedback": {
"incorrect": "No 1",
"correct": "Yes 1"
},
"zone": "Zone 1",
"imageURL": "",
"id": 0
},
{
"displayName": "2 here",
"feedback": {
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "Zone 2",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "No Zone for this",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 2
}
],
"feedback": {
"start": "Some Intro Feed",
"finish": "Some Final Feed"
}
}
{
"zones": [
{
"index": 1,
"title": "Zone 1",
"description": "This describes zone 1",
"height": 178,
"width": 196,
"y": "30",
"x": "160",
"id": "zone-1"
},
{
"index": 2,
"title": "Zone 2",
"description": "This describes zone 2",
"height": 140,
"width": 340,
"y": "210",
"x": "86",
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"imageURL": "https://placehold.it/100x100",
"imageDescription": "This describes the background image of item 1",
"feedback": {
"incorrect": "No, 1 does not belong here",
"correct": "Yes, 1 goes here"
},
"zone": "Zone 1",
"id": 0
},
{
"displayName": "2",
"imageURL": "https://placehold.it/100x100",
"imageDescription": "This describes the background image of item 2",
"feedback": {
"incorrect": "No, 2 does not belong here",
"correct": "Yes, 2 goes here"
},
"zone": "Zone 2",
"id": 1
},
{
"displayName": "X",
"imageURL": "",
"feedback": {
"incorrect": "You silly, there are no zones for X",
"correct": ""
},
"zone": "none",
"id": 2
}
],
"feedback": {
"start": "Drag the items onto the image above.",
"finish": "Good work! You have completed this drag and drop problem."
},
"targetImgDescription": "This describes the target image",
"displayLabels": {display_labels_value},
"displayBorders": {display_borders_value},
}
{
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone 51",
"height": 100,
"y": "400",
"x": "200",
"id": "zone-51"
},
{
"index": 2,
"width": 200,
"title": "Zone 52",
"height": 100,
"y": "200",
"x": "100",
"id": "zone-52"
}
],
"items": [
{
"displayName": "Item 1",
"feedback": {
"incorrect": "Incorrect 1",
"correct": "Correct 1"
},
"zone": "Zone 51",
"imageURL": "",
"id": 10
},
{
"displayName": "Item 2",
"feedback": {
"incorrect": "Incorrect 2",
"correct": "Correct 2"
},
"zone": "Zone 52",
"imageURL": "",
"id": 20,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "No Zone for this",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 30
}
],
"feedback": {
"start": "Other Intro Feed",
"finish": "Other Final Feed"
}
}
{
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone <i>1</i>",
"height": 100,
"y": 200,
"x": 100,
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone <b>2</b>",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "<b>1</b>",
"feedback": {
"incorrect": "No <b>1</b>",
"correct": "Yes <b>1</b>"
},
"zone": "Zone <i>1</i>",
"imageURL": "",
"id": 0
},
{
"displayName": "<i>2</i>",
"feedback": {
"incorrect": "No <i>2</i>",
"correct": "Yes <i>2</i>"
},
"zone": "Zone <b>2</b>",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "<span style='color:red'>X</span>",
"feedback": {
"incorrect": "No Zone for <i>X</i>",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 2
}
],
"feedback": {
"start": "Intro <i>Feed</i>",
"finish": "Final <b>Feed</b>"
},
"targetImg": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg==",
"targetImgDescription": "This describes the target image"
}
{
{% if img == "wide" %}
"targetImg": "{{img_wide_url}}",
"zones": [
{"index": 1, "title": "Zone 1/3", "width": 533, "height": 200, "x": "0", "y": "0", "id": "zone-1"},
{"index": 2, "title": "Zone 50%", "width": 800, "height": 200, "x": "0", "y": "350", "id": "zone-2"},
{"index": 3, "title": "Zone 75%", "width": 1200, "height": 200, "x": "0", "y": "700", "id": "zone-3"}
],
{% else %}
"targetImg": "{{img_square_url}}",
"zones": [
{"index": 1, "title": "Zone 1/3", "width": 166, "height": 100, "x": "0", "y": "0", "id": "zone-1"},
{"index": 2, "title": "Zone 50%", "width": 250, "height": 100, "x": "0", "y": "200", "id": "zone-2"},
{"index": 3, "title": "Zone 75%", "width": 375, "height": 100, "x": "0", "y": "400", "id": "zone-3"}
],
{% endif %}
"displayBorders": true,
"items": [
{
"displayName": "Auto",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "",
"id": 0
},
{
"displayName": "Auto with long text that should wrap because draggables are given a maximum width",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "",
"id": 1
},
{
"displayName": "33.3%",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "",
"id": 2,
"widthPercent": 33.3
},
{
"displayName": "50%",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "",
"id": 3,
"widthPercent": 50
},
{
"displayName": "75%",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 75%",
"imageURL": "",
"id": 4,
"widthPercent": 75
},
{
"displayName": "IMG 400x300",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_400x300_url}}",
"id": 5,
},
{
"displayName": "IMG 200x200",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_200x200_url}}",
"id": 6,
},
{
"displayName": "IMG 400x300",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_400x300_url}}",
"id": 7,
"widthPercent": 50
},
{
"displayName": "IMG 200x200",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 50%",
"imageURL": "{{img_200x200_url}}",
"id": 8,
"widthPercent": 50
},
{
"displayName": "IMG 60x60",
"feedback": {"incorrect": "", "correct": ""},
"zone": "Zone 1/3",
"imageURL": "{{img_60x60_url}}",
"id": 9
}
],
"feedback": {"start": "Some Intro Feedback", "finish": "Some Final Feedback"}
}
# Imports ###########################################################
from xml.sax.saxutils import escape
from selenium.webdriver.support.ui import WebDriverWait
from workbench import scenarios
from xblockutils.resources import ResourceLoader
from xblockutils.base_test import SeleniumBaseTest
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
class BaseIntegrationTest(SeleniumBaseTest):
default_css_selector = 'section.themed-xblock.xblock--drag-and-drop'
module_name = __name__
_additional_escapes = {
'"': "&quot;",
"'": "&apos;"
}
@staticmethod
def _make_scenario_xml(display_name, show_title, problem_text, completed=False, show_problem_header=True):
return """
<vertical_demo>
<drag-and-drop-v2
display_name='{display_name}'
show_title='{show_title}'
question_text='{problem_text}'
show_question_header='{show_problem_header}'
weight='1'
completed='{completed}'
/>
</vertical_demo>
""".format(
display_name=escape(display_name),
show_title=show_title,
problem_text=escape(problem_text),
show_problem_header=show_problem_header,
completed=completed,
)
def _get_custom_scenario_xml(self, filename):
data = loader.load_unicode(filename)
return "<vertical_demo><drag-and-drop-v2 data='{data}'/></vertical_demo>".format(
data=escape(data, self._additional_escapes)
)
def _add_scenario(self, identifier, title, xml):
scenarios.add_xml_scenario(identifier, title, xml)
self.addCleanup(scenarios.remove_scenario, identifier)
def _get_items(self):
items_container = self._page.find_element_by_css_selector('.item-bank')
return items_container.find_elements_by_css_selector('.option')
def _get_zones(self):
return self._page.find_elements_by_css_selector(".drag-container .zone")
def _get_popup(self):
return self._page.find_element_by_css_selector(".popup")
def _get_popup_content(self):
return self._page.find_element_by_css_selector(".popup .popup-content")
def _get_keyboard_help(self):
return self._page.find_element_by_css_selector(".keyboard-help")
def _get_keyboard_help_button(self):
return self._page.find_element_by_css_selector(".keyboard-help .keyboard-help-button")
def _get_keyboard_help_dialog(self):
return self._page.find_element_by_css_selector(".keyboard-help .keyboard-help-dialog")
def _get_reset_button(self):
return self._page.find_element_by_css_selector('.reset-button')
def _get_feedback(self):
return self._page.find_element_by_css_selector(".feedback")
def _get_feedback_message(self):
return self._page.find_element_by_css_selector(".feedback .message")
def scroll_down(self, pixels=50):
self.browser.execute_script("$(window).scrollTop({})".format(pixels))
@staticmethod
def get_element_html(element):
return element.get_attribute('innerHTML').strip()
@staticmethod
def get_element_classes(element):
return element.get_attribute('class').split()
def wait_until_html_in(self, html, elem):
wait = WebDriverWait(elem, 2)
wait.until(lambda e: html in e.get_attribute('innerHTML'),
u"{} should be in {}".format(html, elem.get_attribute('innerHTML')))
@staticmethod
def wait_until_has_class(class_name, elem):
wait = WebDriverWait(elem, 2)
wait.until(lambda e: class_name in e.get_attribute('class').split(),
u"Class name {} not in {}".format(class_name, elem.get_attribute('class')))
from .test_base import BaseIntegrationTest
class TestCustomDataDragAndDropRendering(BaseIntegrationTest):
PAGE_TITLE = 'Drag and Drop v2'
PAGE_ID = 'drag_and_drop_v2'
def setUp(self):
super(TestCustomDataDragAndDropRendering, self).setUp()
scenario_xml = self._get_custom_scenario_xml("data/test_html_data.json")
self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml)
self._page = self.go_to_page(self.PAGE_TITLE)
header1 = self.browser.find_element_by_css_selector('h1')
self.assertEqual(header1.text, 'XBlock: ' + self.PAGE_TITLE)
def test_items_rendering(self):
items = self._get_items()
self.assertEqual(len(items), 3)
self.assertIn('<b>1</b>', self.get_element_html(items[0]))
self.assertIn('<i>2</i>', self.get_element_html(items[1]))
self.assertIn('<input class="input" type="text">', self.get_element_html(items[1]))
self.assertIn('<span style="color:red">X</span>', self.get_element_html(items[2]))
def test_background_image(self):
bg_image = self.browser.find_element_by_css_selector(".xblock--drag-and-drop .target-img")
custom_image_url = (
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciI"
"HdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiBzdHlsZT0iYmFja2dyb3VuZDogI2VlZjsiPjwvc3ZnPg=="
)
custom_image_description = "This describes the target image"
self.assertEqual(bg_image.get_attribute("src"), custom_image_url)
self.assertEqual(bg_image.get_attribute("alt"), custom_image_description)
# Imports ###########################################################
from ddt import ddt, data, unpack
from mock import Mock, patch
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
from workbench.runtime import WorkbenchRuntime
from xblockutils.resources import ResourceLoader
from drag_and_drop_v2.default_data import (
TOP_ZONE_TITLE, MIDDLE_ZONE_TITLE, BOTTOM_ZONE_TITLE,
ITEM_CORRECT_FEEDBACK, ITEM_INCORRECT_FEEDBACK, ITEM_NO_ZONE_FEEDBACK,
START_FEEDBACK, FINISH_FEEDBACK
)
from .test_base import BaseIntegrationTest
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
class ItemDefinition(object):
def __init__(self, item_id, zone_id, feedback_positive, feedback_negative, input_value=None):
self.feedback_negative = feedback_negative
self.feedback_positive = feedback_positive
self.zone_id = zone_id
self.item_id = item_id
self.input = input_value
class InteractionTestBase(object):
@classmethod
def _get_items_with_zone(cls, items_map):
return {
item_key: definition for item_key, definition in items_map.items()
if definition.zone_id is not None
}
@classmethod
def _get_items_without_zone(cls, items_map):
return {
item_key: definition for item_key, definition in items_map.items()
if definition.zone_id is None
}
def setUp(self):
super(InteractionTestBase, self).setUp()
scenario_xml = self._get_scenario_xml()
self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml)
self._page = self.go_to_page(self.PAGE_TITLE)
# Resize window so that the entire drag container is visible.
# Selenium has issues when dragging to an area that is off screen.
self.browser.set_window_size(1024, 800)
def _get_item_by_value(self, item_value):
return self._page.find_elements_by_xpath(".//div[@data-value='{item_id}']".format(item_id=item_value))[0]
def _get_unplaced_item_by_value(self, item_value):
items_container = self._page.find_element_by_css_selector('.item-bank')
return items_container.find_elements_by_xpath(".//div[@data-value='{item_id}']".format(item_id=item_value))[0]
def _get_placed_item_by_value(self, item_value):
items_container = self._page.find_element_by_css_selector('.target')
return items_container.find_elements_by_xpath(".//div[@data-value='{item_id}']".format(item_id=item_value))[0]
def _get_zone_by_id(self, zone_id):
zones_container = self._page.find_element_by_css_selector('.target')
return zones_container.find_elements_by_xpath(".//div[@data-zone='{zone_id}']".format(zone_id=zone_id))[0]
def _get_input_div_by_value(self, item_value):
element = self._get_item_by_value(item_value)
return element.find_element_by_class_name('numerical-input')
def _get_dialog_components(self, dialog): # pylint: disable=no-self-use
dialog_modal_overlay = dialog.find_element_by_css_selector('.modal-window-overlay')
dialog_modal = dialog.find_element_by_css_selector('.modal-window')
return dialog_modal_overlay, dialog_modal
def _get_dialog_dismiss_button(self, dialog_modal): # pylint: disable=no-self-use
return dialog_modal.find_element_by_css_selector('.modal-dismiss-button')
def _get_zone_position(self, zone_id):
return self.browser.execute_script(
'return $("div[data-zone=\'{zone_id}\']").prevAll(".zone").length'.format(zone_id=zone_id)
)
def place_item(self, item_value, zone_id, action_key=None):
if action_key is None:
self.drag_item_to_zone(item_value, zone_id)
else:
self.move_item_to_zone(item_value, zone_id, action_key)
def drag_item_to_zone(self, item_value, zone_id):
element = self._get_unplaced_item_by_value(item_value)
target = self._get_zone_by_id(zone_id)
action_chains = ActionChains(self.browser)
action_chains.drag_and_drop(element, target).perform()
def move_item_to_zone(self, item_value, zone_id, action_key):
# Get zone position
zone_position = self._get_zone_position(zone_id)
# Focus on the item:
item = self._get_unplaced_item_by_value(item_value)
ActionChains(self.browser).move_to_element(item).perform()
# Press the action key:
item.send_keys(action_key) # Focus is on first *zone* now
self.assert_grabbed_item(item)
for _ in range(zone_position):
self._page.send_keys(Keys.TAB)
self._get_zone_by_id(zone_id).send_keys(action_key)
def send_input(self, item_value, value):
element = self._get_item_by_value(item_value)
self.wait_until_visible(element)
element.find_element_by_class_name('input').send_keys(value)
element.find_element_by_class_name('submit-input').click()
def assert_grabbed_item(self, item):
self.assertEqual(item.get_attribute('aria-grabbed'), 'true')
def assert_placed_item(self, item_value, zone_id):
item = self._get_placed_item_by_value(item_value)
self.wait_until_visible(item)
item_content = item.find_element_by_css_selector('.item-content')
item_description = item.find_element_by_css_selector('.sr')
item_description_id = '-item-{}-description'.format(item_value)
self.assertIsNone(item.get_attribute('tabindex'))
self.assertEqual(item.get_attribute('aria-grabbed'), 'false')
self.assertEqual(item.get_attribute('data-drag-disabled'), 'true')
self.assertEqual(item_content.get_attribute('aria-describedby'), item_description_id)
self.assertEqual(item_description.get_attribute('id'), item_description_id)
self.assertEqual(item_description.text, 'Correctly placed in: {}'.format(zone_id))
def assert_reverted_item(self, item_value):
item = self._get_item_by_value(item_value)
self.wait_until_visible(item)
item_content = item.find_element_by_css_selector('.item-content')
self.assertEqual(item.get_attribute('class'), 'option ui-draggable')
self.assertEqual(item.get_attribute('tabindex'), '0')
self.assertEqual(item.get_attribute('draggable'), 'true')
self.assertEqual(item.get_attribute('aria-grabbed'), 'false')
self.assertEqual(item.get_attribute('data-drag-disabled'), 'false')
self.assertIsNone(item_content.get_attribute('aria-describedby'))
try:
item.find_element_by_css_selector('.sr')
except NoSuchElementException:
pass
else:
self.fail('Reverted item should not have .sr description.')
def assert_decoy_items(self, items_map):
decoy_items = self._get_items_without_zone(items_map)
for item_key in decoy_items:
item = self._get_item_by_value(item_key)
self.assertEqual(item.get_attribute('class'), 'option fade')
self.assertEqual(item.get_attribute('aria-grabbed'), 'false')
self.assertEqual(item.get_attribute('data-drag-disabled'), 'true')
def parameterized_item_positive_feedback_on_good_move(self, items_map, scroll_down=100, action_key=None):
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
# Scroll drop zones into view to make sure Selenium can successfully drop items
self.scroll_down(pixels=scroll_down)
for definition in self._get_items_with_zone(items_map).values():
if not definition.input:
self.place_item(definition.item_id, definition.zone_id, action_key)
self.wait_until_html_in(definition.feedback_positive, feedback_popup_content)
self.assertEqual(popup.get_attribute('class'), 'popup')
self.assert_placed_item(definition.item_id, definition.zone_id)
def parameterized_item_positive_feedback_on_good_input(self, items_map, scroll_down=100, action_key=None):
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
# Scroll drop zones into view to make sure Selenium can successfully drop items
self.scroll_down(pixels=scroll_down)
for definition in self._get_items_with_zone(items_map).values():
if definition.input:
self.place_item(definition.item_id, definition.zone_id, action_key)
self.send_input(definition.item_id, definition.input)
self.wait_until_html_in(definition.feedback_positive, feedback_popup_content)
self.assertEqual(popup.get_attribute('class'), 'popup')
self.assert_placed_item(definition.item_id, definition.zone_id)
input_div = self._get_input_div_by_value(definition.item_id)
self.wait_until_has_class('correct', input_div)
def parameterized_item_negative_feedback_on_bad_move(self, items_map, all_zones, scroll_down=100, action_key=None):
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
# Scroll drop zones into view to make sure Selenium can successfully drop items
self.scroll_down(pixels=scroll_down)
for definition in items_map.values():
for zone in all_zones:
if zone == definition.zone_id:
continue
self.place_item(definition.item_id, zone, action_key)
self.wait_until_html_in(definition.feedback_negative, feedback_popup_content)
self.assertEqual(popup.get_attribute('class'), 'popup popup-incorrect')
self.assert_reverted_item(definition.item_id)
def parameterized_item_negative_feedback_on_bad_input(self, items_map, scroll_down=100, action_key=None):
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
# Scroll drop zones into view to make sure Selenium can successfully drop items
self.scroll_down(pixels=scroll_down)
for definition in self._get_items_with_zone(items_map).values():
if definition.input:
self.place_item(definition.item_id, definition.zone_id, action_key)
self.send_input(definition.item_id, '1999999')
self.wait_until_html_in(definition.feedback_negative, feedback_popup_content)
self.assertEqual(popup.get_attribute('class'), 'popup popup-incorrect')
self.assert_placed_item(definition.item_id, definition.zone_id)
input_div = self._get_input_div_by_value(definition.item_id)
self.wait_until_has_class('incorrect', input_div)
def parameterized_final_feedback_and_reset(self, items_map, feedback, scroll_down=100, action_key=None):
feedback_message = self._get_feedback_message()
self.assertEqual(self.get_element_html(feedback_message), feedback['intro']) # precondition check
items = self._get_items_with_zone(items_map)
def get_locations():
return {item_id: self._get_item_by_value(item_id).location for item_id in items.keys()}
initial_locations = get_locations()
# Scroll drop zones into view to make sure Selenium can successfully drop items
self.scroll_down(pixels=scroll_down)
for item_key, definition in items.items():
self.place_item(definition.item_id, definition.zone_id, action_key)
if definition.input:
self.send_input(item_key, definition.input)
input_div = self._get_input_div_by_value(item_key)
self.wait_until_has_class('correct', input_div)
self.assert_placed_item(definition.item_id, definition.zone_id)
self.wait_until_html_in(feedback['final'], self._get_feedback_message())
# Check decoy items
self.assert_decoy_items(items_map)
# Scroll "Reset problem" button into view to make sure Selenium can successfully click it
self.scroll_down(pixels=scroll_down+150)
reset = self._get_reset_button()
if action_key is not None: # Using keyboard to interact with block
reset.send_keys(Keys.RETURN)
else:
reset.click()
self.wait_until_html_in(feedback['intro'], self._get_feedback_message())
locations_after_reset = get_locations()
for item_key in items.keys():
self.assertDictEqual(locations_after_reset[item_key], initial_locations[item_key])
self.assert_reverted_item(item_key)
def interact_with_keyboard_help(self, scroll_down=250, use_keyboard=False):
keyboard_help_button = self._get_keyboard_help_button()
keyboard_help_dialog = self._get_keyboard_help_dialog()
dialog_modal_overlay, dialog_modal = self._get_dialog_components(keyboard_help_dialog)
dialog_dismiss_button = self._get_dialog_dismiss_button(dialog_modal)
# Scroll "Keyboard help" button into view to make sure Selenium can successfully click it
self.scroll_down(pixels=scroll_down)
if use_keyboard:
keyboard_help_button.send_keys(Keys.RETURN)
else:
keyboard_help_button.click()
self.assertTrue(dialog_modal_overlay.is_displayed())
self.assertTrue(dialog_modal.is_displayed())
if use_keyboard:
dialog_dismiss_button.send_keys(Keys.RETURN)
else:
dialog_dismiss_button.click()
self.assertFalse(dialog_modal_overlay.is_displayed())
self.assertFalse(dialog_modal.is_displayed())
if use_keyboard: # Check if "Keyboard Help" dialog can be dismissed using "ESC"
keyboard_help_button.send_keys(Keys.RETURN)
self.assertTrue(dialog_modal_overlay.is_displayed())
self.assertTrue(dialog_modal.is_displayed())
self._page.send_keys(Keys.ESCAPE)
self.assertFalse(dialog_modal_overlay.is_displayed())
self.assertFalse(dialog_modal.is_displayed())
def _switch_to_block(self, idx):
""" Only needed if ther eare multiple blocks on the page. """
self._page = self.browser.find_elements_by_css_selector(self.default_css_selector)[idx]
self.scroll_down(0)
class DefaultDataTestMixin(object):
"""
Provides a test scenario with default options.
"""
PAGE_TITLE = 'Drag and Drop v2'
PAGE_ID = 'drag_and_drop_v2'
items_map = {
0: ItemDefinition(
0, TOP_ZONE_TITLE, ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE), ITEM_INCORRECT_FEEDBACK
),
1: ItemDefinition(
1, MIDDLE_ZONE_TITLE, ITEM_CORRECT_FEEDBACK.format(zone=MIDDLE_ZONE_TITLE), ITEM_INCORRECT_FEEDBACK
),
2: ItemDefinition(
2, BOTTOM_ZONE_TITLE, ITEM_CORRECT_FEEDBACK.format(zone=BOTTOM_ZONE_TITLE), ITEM_INCORRECT_FEEDBACK
),
3: ItemDefinition(3, None, "", ITEM_NO_ZONE_FEEDBACK),
}
all_zones = [TOP_ZONE_TITLE, MIDDLE_ZONE_TITLE, BOTTOM_ZONE_TITLE]
feedback = {
"intro": START_FEEDBACK,
"final": FINISH_FEEDBACK,
}
def _get_scenario_xml(self): # pylint: disable=no-self-use
return "<vertical_demo><drag-and-drop-v2/></vertical_demo>"
class BasicInteractionTest(DefaultDataTestMixin, InteractionTestBase):
"""
Testing interactions with Drag and Drop XBlock against default data. If default data changes this will break.
"""
def test_item_positive_feedback_on_good_move(self):
self.parameterized_item_positive_feedback_on_good_move(self.items_map)
def test_item_positive_feedback_on_good_input(self):
self.parameterized_item_positive_feedback_on_good_input(self.items_map)
def test_item_negative_feedback_on_bad_move(self):
self.parameterized_item_negative_feedback_on_bad_move(self.items_map, self.all_zones)
def test_item_negative_feedback_on_bad_input(self):
self.parameterized_item_negative_feedback_on_bad_input(self.items_map)
def test_final_feedback_and_reset(self):
self.parameterized_final_feedback_and_reset(self.items_map, self.feedback)
def test_keyboard_help(self):
self.interact_with_keyboard_help()
@ddt
class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest):
"""
Tests that the analytics events are fired and in the proper order.
"""
# These events must be fired in this order.
scenarios = (
{
'name': 'edx.drag_and_drop_v2.loaded',
'data': {},
},
{
'name': 'edx.drag_and_drop_v2.item.picked_up',
'data': {'item_id': 0},
},
{
'name': 'grade',
'data': {'max_value': 1, 'value': (1.0 / 3)},
},
{
'name': 'edx.drag_and_drop_v2.item.dropped',
'data': {
'input': None,
'is_correct': True,
'is_correct_location': True,
'item_id': 0,
'location': TOP_ZONE_TITLE,
},
},
{
'name': 'edx.drag_and_drop_v2.feedback.opened',
'data': {
'content': ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE),
'truncated': False,
},
},
{
'name': 'edx.drag_and_drop_v2.feedback.closed',
'data': {
'manually': False,
'content': ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE),
'truncated': False,
},
},
)
def setUp(self):
mock = Mock()
context = patch.object(WorkbenchRuntime, 'publish', mock)
context.start()
self.addCleanup(context.stop)
self.publish = mock
super(EventsFiredTest, self).setUp()
def _get_scenario_xml(self): # pylint: disable=no-self-use
return "<vertical_demo><drag-and-drop-v2/></vertical_demo>"
@data(*enumerate(scenarios)) # pylint: disable=star-args
@unpack
def test_event(self, index, event):
self.parameterized_item_positive_feedback_on_good_move(self.items_map)
dummy, name, published_data = self.publish.call_args_list[index][0]
self.assertEqual(name, event['name'])
self.assertEqual(
published_data, event['data']
)
@ddt
class KeyboardInteractionTest(BasicInteractionTest, BaseIntegrationTest):
@data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_item_positive_feedback_on_good_move_with_keyboard(self, action_key):
self.parameterized_item_positive_feedback_on_good_move(self.items_map, action_key=action_key)
@data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_item_positive_feedback_on_good_input_with_keyboard(self, action_key):
self.parameterized_item_positive_feedback_on_good_input(self.items_map, action_key=action_key)
@data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_item_negative_feedback_on_bad_move_with_keyboard(self, action_key):
self.parameterized_item_negative_feedback_on_bad_move(self.items_map, self.all_zones, action_key=action_key)
@data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_item_negative_feedback_on_bad_input_with_keyboard(self, action_key):
self.parameterized_item_negative_feedback_on_bad_input(self.items_map, action_key=action_key)
@data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_final_feedback_and_reset_with_keyboard(self, action_key):
self.parameterized_final_feedback_and_reset(self.items_map, self.feedback, action_key=action_key)
def test_keyboard_help(self):
self.interact_with_keyboard_help(use_keyboard=True)
class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
items_map = {
0: ItemDefinition(0, 'Zone 1', "Yes 1", "No 1"),
1: ItemDefinition(1, 'Zone 2', "Yes 2", "No 2", "102"),
2: ItemDefinition(2, None, "", "No Zone for this")
}
all_zones = ['Zone 1', 'Zone 2']
feedback = {
"intro": "Some Intro Feed",
"final": "Some Final Feed"
}
def _get_scenario_xml(self):
return self._get_custom_scenario_xml("data/test_data.json")
class CustomHtmlDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
items_map = {
0: ItemDefinition(0, 'Zone <i>1</i>', "Yes <b>1</b>", "No <b>1</b>"),
1: ItemDefinition(1, 'Zone <b>2</b>', "Yes <i>2</i>", "No <i>2</i>", "95"),
2: ItemDefinition(2, None, "", "No Zone for <i>X</i>")
}
all_zones = ['Zone <i>1</i>', 'Zone <b>2</b>']
feedback = {
"intro": "Intro <i>Feed</i>",
"final": "Final <b>Feed</b>"
}
def _get_scenario_xml(self):
return self._get_custom_scenario_xml("data/test_html_data.json")
class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
PAGE_TITLE = 'Drag and Drop v2 Multiple Blocks'
PAGE_ID = 'drag_and_drop_v2_multi'
BLOCK1_DATA_FILE = "data/test_data.json"
BLOCK2_DATA_FILE = "data/test_data_other.json"
item_maps = {
'block1': {
0: ItemDefinition(0, 'Zone 1', "Yes 1", "No 1"),
1: ItemDefinition(1, 'Zone 2', "Yes 2", "No 2", "102"),
2: ItemDefinition(2, None, "", "No Zone for this")
},
'block2': {
10: ItemDefinition(10, 'Zone 51', "Correct 1", "Incorrect 1"),
20: ItemDefinition(20, 'Zone 52', "Correct 2", "Incorrect 2", "102"),
30: ItemDefinition(30, None, "", "No Zone for this")
},
}
all_zones = {
'block1': ['Zone 1', 'Zone 2'],
'block2': ['Zone 51', 'Zone 52']
}
feedback = {
'block1': {"intro": "Some Intro Feed", "final": "Some Final Feed"},
'block2': {"intro": "Other Intro Feed", "final": "Other Final Feed"},
}
def _get_scenario_xml(self):
blocks_xml = "\n".join([
"<drag-and-drop-v2 data='{data}'/>".format(data=loader.load_unicode(filename))
for filename in (self.BLOCK1_DATA_FILE, self.BLOCK2_DATA_FILE)
])
return "<vertical_demo>{dnd_blocks}</vertical_demo>".format(dnd_blocks=blocks_xml)
def test_item_positive_feedback_on_good_move(self):
self._switch_to_block(0)
self.parameterized_item_positive_feedback_on_good_move(self.item_maps['block1'])
self._switch_to_block(1)
self.parameterized_item_positive_feedback_on_good_move(self.item_maps['block2'], scroll_down=900)
def test_item_positive_feedback_on_good_input(self):
self._switch_to_block(0)
self.parameterized_item_positive_feedback_on_good_input(self.item_maps['block1'])
self._switch_to_block(1)
self.parameterized_item_positive_feedback_on_good_input(self.item_maps['block2'], scroll_down=900)
def test_item_negative_feedback_on_bad_move(self):
self._switch_to_block(0)
self.parameterized_item_negative_feedback_on_bad_move(self.item_maps['block1'], self.all_zones['block1'])
self._switch_to_block(1)
self.parameterized_item_negative_feedback_on_bad_move(
self.item_maps['block2'], self.all_zones['block2'], scroll_down=900
)
def test_item_negative_feedback_on_bad_input(self):
self._switch_to_block(0)
self.parameterized_item_negative_feedback_on_bad_input(self.item_maps['block1'])
self._switch_to_block(1)
self.parameterized_item_negative_feedback_on_bad_input(self.item_maps['block2'], scroll_down=900)
def test_final_feedback_and_reset(self):
self._switch_to_block(0)
self.parameterized_final_feedback_and_reset(self.item_maps['block1'], self.feedback['block1'])
self._switch_to_block(1)
self.parameterized_final_feedback_and_reset(self.item_maps['block2'], self.feedback['block2'], scroll_down=900)
def test_keyboard_help(self):
self._switch_to_block(0)
# Test mouse and keyboard interaction
self.interact_with_keyboard_help()
self.interact_with_keyboard_help(use_keyboard=True)
self._switch_to_block(1)
# Test mouse and keyboard interaction
self.interact_with_keyboard_help(scroll_down=900)
self.interact_with_keyboard_help(scroll_down=0, use_keyboard=True)
# Imports ###########################################################
from ddt import ddt, unpack, data
from selenium.common.exceptions import NoSuchElementException
from xblockutils.resources import ResourceLoader
from drag_and_drop_v2.default_data import START_FEEDBACK
from .test_base import BaseIntegrationTest
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
class Colors(object):
WHITE = 'rgb(255, 255, 255)'
BLUE = 'rgb(29, 82, 128)'
GREY = 'rgb(237, 237, 237)'
CORAL = '#ff7f50'
DARK_GREY = 'rgb(86, 86, 86)' # == #565656 in CSS-land
CORNFLOWERBLUE = 'cornflowerblue'
@classmethod
def rgb(cls, color):
if color in (cls.WHITE, cls.BLUE, cls.GREY):
return color
elif color == cls.CORAL:
return 'rgb(255, 127, 80)'
elif color == cls.CORNFLOWERBLUE:
return 'rgb(100, 149, 237)'
@ddt
class TestDragAndDropRender(BaseIntegrationTest):
"""
Verifying Drag and Drop XBlock rendering against default data - if default data changes this
will probably break.
"""
PAGE_TITLE = 'Drag and Drop v2'
PAGE_ID = 'drag_and_drop_v2'
ITEM_PROPERTIES = [{'text': '1'}, {'text': '2'}, {'text': 'X'}, ]
SIDES = ['Top', 'Bottom', 'Left', 'Right']
def load_scenario(self, item_background_color="", item_text_color="", zone_labels=False, zone_borders=False):
problem_data = loader.load_unicode("data/test_data_a11y.json")
problem_data = problem_data.replace('{display_labels_value}', 'true' if zone_labels else 'false')
problem_data = problem_data.replace('{display_borders_value}', 'true' if zone_borders else 'false')
scenario_xml = """
<vertical_demo>
<drag-and-drop-v2 item_background_color='{item_background_color}'
item_text_color='{item_text_color}'
data='{problem_data}' />
</vertical_demo>
""".format(
item_background_color=item_background_color,
item_text_color=item_text_color,
problem_data=problem_data
)
self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml)
self.browser.get(self.live_server_url)
self._page = self.go_to_page(self.PAGE_TITLE)
def _get_style(self, selector, style, computed=True):
if computed:
query = 'return getComputedStyle($("{selector}").get(0)).{style}'
else:
query = 'return $("{selector}").get(0).style.{style}'
return self.browser.execute_script(query.format(selector=selector, style=style))
def _assert_box_percentages(self, selector, left, top, width, height):
""" Assert that the element 'selector' has the specified position/size percentages """
values = {key: self._get_style(selector, key, False) for key in ['left', 'top', 'width', 'height']}
for key in values:
self.assertTrue(values[key].endswith('%'))
values[key] = float(values[key][:-1])
self.assertAlmostEqual(values['left'], left, places=2)
self.assertAlmostEqual(values['top'], top, places=2)
self.assertAlmostEqual(values['width'], width, places=2)
self.assertAlmostEqual(values['height'], height, places=2)
def _test_item_style(self, item_element, style_settings):
item_val = item_element.get_attribute('data-value')
item_selector = '.item-bank .option[data-value=' + item_val + ']'
style = item_element.get_attribute('style')
# Check background color
background_color_property = 'background-color'
if background_color_property not in style_settings:
self.assertNotIn(background_color_property, style)
expected_background_color = Colors.BLUE
else:
expected_background_color = Colors.rgb(style_settings['background-color'])
background_color = self._get_style(item_selector, 'backgroundColor')
self.assertEquals(background_color, expected_background_color)
# Check text color
color_property = 'color'
if color_property not in style_settings:
# Leading space below ensures that test does not find "color" in "background-color"
self.assertNotIn(' ' + color_property, style)
expected_color = Colors.WHITE
else:
expected_color = Colors.rgb(style_settings['color'])
color = self._get_style(item_selector, 'color')
self.assertEquals(color, expected_color)
# Check outline color
outline_color_property = 'outline-color'
if outline_color_property not in style_settings:
self.assertNotIn(outline_color_property, style)
# Outline color should match text color to ensure it does not meld into background color:
expected_outline_color = expected_color
outline_color = self._get_style(item_selector, 'outlineColor')
self.assertEquals(outline_color, expected_outline_color)
def test_items_default_colors(self):
self.load_scenario()
self._test_items()
@unpack
@data(
(Colors.CORNFLOWERBLUE, Colors.GREY),
(Colors.CORAL, ''),
('', Colors.GREY),
)
def test_items_custom_colors(self, item_background_color, item_text_color):
self.load_scenario(item_background_color, item_text_color)
color_settings = {}
if item_background_color:
color_settings['background-color'] = item_background_color
if item_text_color:
color_settings['color'] = item_text_color
color_settings['outline-color'] = item_text_color
self._test_items(color_settings=color_settings)
def _test_items(self, color_settings=None):
color_settings = color_settings or {}
items = self._get_items()
self.assertEqual(len(items), 3)
for index, item in enumerate(items):
item_number = index + 1
self.assertEqual(item.get_attribute('role'), 'button')
self.assertEqual(item.get_attribute('tabindex'), '0')
self.assertEqual(item.get_attribute('draggable'), 'true')
self.assertEqual(item.get_attribute('aria-grabbed'), 'false')
self.assertEqual(item.get_attribute('data-value'), str(index))
self.assertIn('ui-draggable', self.get_element_classes(item))
self._test_item_style(item, color_settings)
try:
background_image = item.find_element_by_css_selector('img')
except NoSuchElementException:
self.assertEqual(item.text, self.ITEM_PROPERTIES[index]['text'])
else:
self.assertEqual(
background_image.get_attribute('alt'),
'This describes the background image of item {}'.format(item_number)
)
def test_drag_container(self):
self.load_scenario()
item_bank = self._page.find_element_by_css_selector('.drag-container')
self.assertEqual(item_bank.get_attribute('role'), 'application')
def test_zones(self):
self.load_scenario()
zones = self._get_zones()
self.assertEqual(len(zones), 2)
box_percentages = [
{"left": 31.1284, "top": 6.17284, "width": 38.1323, "height": 36.6255},
{"left": 16.7315, "top": 43.2099, "width": 66.1479, "height": 28.8066}
]
for index, zone in enumerate(zones):
zone_number = index + 1
self.assertEqual(zone.get_attribute('tabindex'), '0')
self.assertEqual(zone.get_attribute('dropzone'), 'move')
self.assertEqual(zone.get_attribute('aria-dropeffect'), 'move')
self.assertEqual(zone.get_attribute('data-zone'), 'Zone {}'.format(zone_number))
self.assertIn('ui-droppable', self.get_element_classes(zone))
zone_box_percentages = box_percentages[index]
self._assert_box_percentages( # pylint: disable=star-args
'#-zone-{}'.format(zone_number), **zone_box_percentages
)
zone_name = zone.find_element_by_css_selector('p.zone-name')
self.assertEqual(zone_name.text, 'Zone {}'.format(zone_number))
zone_description = zone.find_element_by_css_selector('p.zone-description')
self.assertEqual(zone_description.text, 'This describes zone {}'.format(zone_number))
# Zone description should only be visible to screen readers:
self.assertEqual(zone_description.get_attribute('class'), 'zone-description sr')
def test_popup(self):
self.load_scenario()
popup = self._get_popup()
popup_content = self._get_popup_content()
self.assertFalse(popup.is_displayed())
self.assertEqual(popup.get_attribute('class'), 'popup')
self.assertEqual(popup_content.text, "")
def test_keyboard_help(self):
self.load_scenario()
self._get_keyboard_help()
keyboard_help_button = self._get_keyboard_help_button()
keyboard_help_dialog = self._get_keyboard_help_dialog()
dialog_modal_overlay = keyboard_help_dialog.find_element_by_css_selector('.modal-window-overlay')
dialog_modal = keyboard_help_dialog.find_element_by_css_selector('.modal-window')
self.assertEqual(keyboard_help_button.get_attribute('tabindex'), '0')
self.assertFalse(dialog_modal_overlay.is_displayed())
self.assertFalse(dialog_modal.is_displayed())
self.assertEqual(dialog_modal.get_attribute('role'), 'dialog')
self.assertEqual(dialog_modal.get_attribute('aria-labelledby'), 'modal-window-title')
def test_feedback(self):
self.load_scenario()
feedback = self._get_feedback()
feedback_message = self._get_feedback_message()
self.assertEqual(feedback.get_attribute('aria-live'), 'polite')
self.assertEqual(feedback_message.text, START_FEEDBACK)
def test_background_image(self):
self.load_scenario()
bg_image = self.browser.find_element_by_css_selector(".xblock--drag-and-drop .target-img")
image_path = '/resource/drag-and-drop-v2/public/img/triangle.png'
self.assertTrue(bg_image.get_attribute("src").endswith(image_path))
self.assertEqual(bg_image.get_attribute("alt"), 'This describes the target image')
def test_zone_borders_hidden(self):
self.load_scenario()
zones = self._get_zones()
for index, dummy in enumerate(zones, start=1):
zone = '#-zone-{}'.format(index)
for side in self.SIDES:
self.assertEqual(self._get_style(zone, 'border{}Width'.format(side), True), '0px')
self.assertEqual(self._get_style(zone, 'border{}Style'.format(side), True), 'none')
def test_zone_borders_shown(self):
self.load_scenario(zone_borders=True)
zones = self._get_zones()
for index, dummy in enumerate(zones, start=1):
zone = '#-zone-{}'.format(index)
for side in self.SIDES:
self.assertEqual(self._get_style(zone, 'border{}Width'.format(side), True), '1px')
self.assertEqual(self._get_style(zone, 'border{}Style'.format(side), True), 'dotted')
self.assertEqual(self._get_style(zone, 'border{}Color'.format(side), True), Colors.DARK_GREY)
def test_zone_labels_hidden(self):
self.load_scenario()
zones = self._get_zones()
for zone in zones:
zone_name = zone.find_element_by_css_selector('p.zone-name')
self.assertIn('sr', zone_name.get_attribute('class'))
def test_zone_labels_shown(self):
self.load_scenario(zone_labels=True)
zones = self._get_zones()
for zone in zones:
zone_name = zone.find_element_by_css_selector('p.zone-name')
self.assertNotIn('sr', zone_name.get_attribute('class'))
from __future__ import division
import base64
from collections import namedtuple
import os.path
from selenium.webdriver.common.keys import Keys
from xblockutils.resources import ResourceLoader
from .test_base import BaseIntegrationTest
from .test_interaction import InteractionTestBase
loader = ResourceLoader(__name__)
def _svg_to_data_uri(path):
""" Convert an SVG image (by path) to a data URI """
data_path = os.path.dirname(__file__) + "/data/"
with open(data_path + path, "rb") as svg_fh:
encoded = base64.b64encode(svg_fh.read())
return "data:image/svg+xml;base64,{}".format(encoded)
Expectation = namedtuple('Expectation', [
'item_id',
'zone_id',
'width_percent', # we expect this item to have this width relative to its container (item bank or image target)
'fixed_width_percent', # we expect this item to have this width (always relative to the target image)
'img_pixel_size_exact', # we expect the image inside the draggable to have the exact size [w, h] in pixels
])
Expectation.__new__.__defaults__ = (None,) * len(Expectation._fields) # pylint: disable=protected-access
ZONE_33 = "Zone 1/3" # Title of top zone in each image used in these tests (33% width)
ZONE_50 = "Zone 50%"
ZONE_75 = "Zone 75%"
AUTO_MAX_WIDTH = 30 # Maximum width (as % of the parent container) for items with automatic sizing
class SizingTests(InteractionTestBase, BaseIntegrationTest):
"""
Tests that cover features like draggable blocks with automatic sizes vs. specified sizes,
different background image ratios, and responsive behavior.
Tip: To see how these tests work, throw in an 'import time; time.sleep(200)' at the start of
one of the tests, so you can check it out in the selenium browser window that opens.
These tests intentionally do not use ddt in order to run faster. Instead, each test iterates
through data and uses verbose assertion messages to clearly indicate where failures occur.
"""
PAGE_TITLE = 'Drag and Drop v2 Sizing'
PAGE_ID = 'drag_and_drop_v2_sizing'
@staticmethod
def _get_scenario_xml():
"""
Set up the test scenario:
* An upper dndv2 xblock with a wide image (1600x900 SVG)
(on desktop and mobile, this background image will always fill the available width
and should have the same width as the item bank above)
* A lower dndv2 xblock with a small square image (500x500 SVG)
(on desktop, the square image is not as wide as the item bank, but on mobile it
may take up the whole width of the screen)
"""
params = {
"img": "wide",
"img_wide_url": _svg_to_data_uri('dnd-bg-wide.svg'),
"img_square_url": _svg_to_data_uri('dnd-bg-square.svg'),
"img_400x300_url": _svg_to_data_uri('400x300.svg'),
"img_200x200_url": _svg_to_data_uri('200x200.svg'),
"img_60x60_url": _svg_to_data_uri('60x60.svg'),
}
upper_block = "<drag-and-drop-v2 data='{data}'/>".format(
data=loader.render_django_template("data/test_sizing_template.json", params)
)
params["img"] = "square"
lower_block = "<drag-and-drop-v2 data='{data}'/>".format(
data=loader.render_django_template("data/test_sizing_template.json", params)
)
return "<vertical_demo>{}\n{}</vertical_demo>".format(upper_block, lower_block)
EXPECTATIONS = [
# The text 'Auto' with no fixed size specified should be 5-20% wide
Expectation(item_id=0, zone_id=ZONE_33, width_percent=[5, 20]),
# The long text with no fixed size specified should be wrapped at the maximum width
Expectation(item_id=1, zone_id=ZONE_33, width_percent=AUTO_MAX_WIDTH),
# The text items that specify specific widths as a percentage of the background image:
Expectation(item_id=2, zone_id=ZONE_33, fixed_width_percent=33.3),
Expectation(item_id=3, zone_id=ZONE_50, fixed_width_percent=50),
Expectation(item_id=4, zone_id=ZONE_75, fixed_width_percent=75),
# A 400x300 image with automatic sizing should be constrained to the maximum width
Expectation(item_id=5, zone_id=ZONE_50, width_percent=AUTO_MAX_WIDTH),
# A 200x200 image with automatic sizing
Expectation(item_id=6, zone_id=ZONE_50, width_percent=[25, 30]),
# A 400x300 image with a specified width of 50%
Expectation(item_id=7, zone_id=ZONE_50, fixed_width_percent=50),
# A 200x200 image with a specified width of 50%
Expectation(item_id=8, zone_id=ZONE_50, fixed_width_percent=50),
# A 60x60 auto-sized image should appear with pixel dimensions of 60x60 since it's
# too small to be shrunk be the default max-size.
Expectation(item_id=9, zone_id=ZONE_33, img_pixel_size_exact=[60, 60]),
]
def test_wide_image_desktop(self):
""" Test the upper, larger, wide image in a desktop-sized window """
self._check_sizes(0, self.EXPECTATIONS)
def test_square_image_desktop(self):
""" Test the lower, smaller, square image in a desktop-sized window """
self._check_sizes(1, self.EXPECTATIONS, expected_img_width=500)
def _size_for_mobile(self):
self.browser.set_window_size(375, 627) # iPhone 6 viewport size
def test_wide_image_mobile(self):
""" Test the upper, larger, wide image in a mobile-sized window """
self._size_for_mobile()
self._check_sizes(0, self.EXPECTATIONS, is_desktop=False)
def test_square_image_mobile(self):
""" Test the lower, smaller, square image in a mobile-sized window """
self._size_for_mobile()
self._check_sizes(1, self.EXPECTATIONS, expected_img_width=375, is_desktop=False)
def _check_width(self, item_description, item, container_width, expected_percent):
"""
Check that item 'item' has a width that is approximately the specified percentage
of container_width, or if expected_percent is a pair of numbers, that it is within
that range.
"""
width_percent = item.size["width"] / container_width * 100
if isinstance(expected_percent, (list, tuple)):
min_expected, max_expected = expected_percent
msg = "{} should have width of {}% - {}%. Actual: {:.2f}%".format(
item_description, min_expected, max_expected, width_percent
)
self.assertGreaterEqual(width_percent, min_expected, msg)
self.assertLessEqual(width_percent, max_expected, msg)
else:
self.assertAlmostEqual(
width_percent, expected_percent, delta=1,
msg="{} should have width of ~{}% (+/- 1%). Actual: {:.2f}%".format(
item_description, expected_percent, width_percent
)
)
if item.find_elements_by_css_selector("img"):
# This item contains an image. The image should always fill the width of the draggable.
image = item.find_element_by_css_selector("img")
image_width_expected = item.size["width"] - 22
self.assertAlmostEqual(
image.size["width"], image_width_expected, delta=1,
msg="{} image does not take up the full width of the draggable (width is {}px; expected {}px)".format(
item_description, image.size["width"], image_width_expected,
)
)
def _check_img_pixel_dimensions(self, item_description, item, expect_w, expect_h):
img_element = item.find_element_by_css_selector("img")
self.assertEqual(
img_element.size, {"width": expect_w, "height": expect_h},
msg="Expected {}'s image to have exact dimensions {}x{}px; found {}x{}px instead.".format(
item_description, expect_w, expect_h, img_element.size["width"], img_element.size["height"]
)
)
def _check_sizes(self, block_index, expectations, expected_img_width=755, is_desktop=True):
""" Test the actual dimensions that each draggable has, in the bank and when placed """
# Check assumptions - the container wrapping this XBlock should be 770px wide
self._switch_to_block(block_index)
target_img = self._page.find_element_by_css_selector('.target-img')
target_img_width = target_img.size["width"]
item_bank = self._page.find_element_by_css_selector('.item-bank')
item_bank_width = item_bank.size["width"]
if is_desktop:
# If using a desktop-sized window, we can know the exact dimensions of various containers:
self.assertEqual(self._page.size["width"], 770) # self._page is the .xblock--drag-and-drop div
self.assertEqual(target_img_width, expected_img_width)
self.assertEqual(item_bank_width, 755)
# Test each element, before it is placed (while it is in the item bank).
for expect in expectations:
if expect.width_percent is not None:
self._check_width(
item_description="Unplaced item {}".format(expect.item_id),
item=self._get_unplaced_item_by_value(expect.item_id),
container_width=item_bank_width,
expected_percent=expect.width_percent,
)
if expect.fixed_width_percent is not None:
self._check_width(
item_description="Unplaced item {} with fixed width".format(expect.item_id),
item=self._get_unplaced_item_by_value(expect.item_id),
container_width=target_img_width,
expected_percent=expect.fixed_width_percent,
)
if expect.img_pixel_size_exact is not None:
self._check_img_pixel_dimensions(
"Unplaced item {}".format(expect.item_id),
self._get_unplaced_item_by_value(expect.item_id),
*expect.img_pixel_size_exact
)
# Test each element, after it it placed.
for expect in expectations:
self.place_item(expect.item_id, expect.zone_id, action_key=Keys.RETURN)
expected_width_percent = expect.fixed_width_percent or expect.width_percent
if expected_width_percent is not None:
self._check_width(
item_description="Placed item {}".format(expect.item_id),
item=self._get_placed_item_by_value(expect.item_id),
container_width=target_img_width,
expected_percent=expected_width_percent,
)
if expect.img_pixel_size_exact is not None:
self._check_img_pixel_dimensions(
"Placed item {}".format(expect.item_id),
self._get_placed_item_by_value(expect.item_id),
*expect.img_pixel_size_exact
)
class SizingBackwardsCompatibilityTests(InteractionTestBase, BaseIntegrationTest):
"""
Test backwards compatibility with data generated in older versions of this block.
Older versions allowed authors to specify a fixed width and height for each draggable, in
pixels (new versions only have a configurable width, and it is a percent width).
"""
PAGE_TITLE = 'Drag and Drop v2 Sizing Backwards Compatibility'
PAGE_ID = 'drag_and_drop_v2_sizing_backwards_compatibility'
@staticmethod
def _get_scenario_xml():
"""
Set up the test scenario:
* One DndDv2 block using 'old_version_data.json'
"""
dnd_block = "<drag-and-drop-v2 data='{data}'/>".format(
data=loader.load_unicode("data/old_version_data.json")
)
return "<vertical_demo>{}</vertical_demo>".format(dnd_block)
def test_draggable_sizes(self):
""" Test the fixed pixel widths set in old versions of the block """
self._expect_width_px(item_id=0, width_px=190, zone_id="Zone 1")
self._expect_width_px(item_id=1, width_px=190, zone_id="Zone 2")
self._expect_width_px(item_id=2, width_px=100, zone_id="Zone 1")
def _expect_width_px(self, item_id, width_px, zone_id):
item = self._get_unplaced_item_by_value(item_id)
self.assertEqual(item.size["width"], width_px)
self.place_item(item_id, zone_id)
item = self._get_placed_item_by_value(item_id)
self.assertEqual(item.size["width"], width_px)
from ddt import ddt, unpack, data
from selenium.common.exceptions import NoSuchElementException
from .test_base import BaseIntegrationTest
from workbench import scenarios
@ddt
class TestDragAndDropTitleAndProblem(BaseIntegrationTest):
@unpack
@data(
('Plain text problem 1, header visible.', True),
('Plain text problem 2, header hidden.', False),
('Problem/instructions with <i>HTML</i> and header.', True),
('<span style="color: red;">Span problem, no header</span>', False),
)
def test_problem_parameters(self, problem_text, show_problem_header):
const_page_name = 'Test title and problem parameters'
const_page_id = 'test_block_title_and_problem'
scenario_xml = self._make_scenario_xml(
display_name="Title",
show_title=True,
problem_text=problem_text,
show_problem_header=show_problem_header,
)
scenarios.add_xml_scenario(const_page_id, const_page_name, scenario_xml)
self.addCleanup(scenarios.remove_scenario, const_page_id)
page = self.go_to_page(const_page_name)
is_problem_header_visible = len(page.find_elements_by_css_selector('section.problem > h3')) > 0
self.assertEqual(is_problem_header_visible, show_problem_header)
problem = page.find_element_by_css_selector('section.problem > p')
self.assertEqual(self.get_element_html(problem), problem_text)
@unpack
@data(
('plain shown', 'title1', True),
('plain hidden', 'title2', False),
('html shown', 'title with <i>HTML</i>', True),
('html hidden', '<span style="color:red">Title: HTML?</span>', False)
)
def test_title_parameters(self, _, display_name, show_title):
const_page_name = 'Test show title parameter'
const_page_id = 'test_block_show_title'
scenario_xml = self._make_scenario_xml(
display_name=display_name,
show_title=show_title,
problem_text='Generic problem',
)
scenarios.add_xml_scenario(const_page_id, const_page_name, scenario_xml)
self.addCleanup(scenarios.remove_scenario, const_page_id)
page = self.go_to_page(const_page_name)
if show_title:
problem_header = page.find_element_by_css_selector('h2.problem-title')
self.assertEqual(self.get_element_html(problem_header), display_name)
else:
with self.assertRaises(NoSuchElementException):
page.find_element_by_css_selector('h2.problem-title')
{
"title": "DnDv2 XBlock with HTML instructions",
"show_title": false,
"problem_text": "Solve this <strong>drag-and-drop</strong> problem.",
"show_problem_header": false,
"target_img_expanded_url": "/expanded/url/to/drag_and_drop_v2/public/img/triangle.png",
"target_img_description": "This describes the target image",
"item_background_color": "white",
"item_text_color": "#000080",
"initial_feedback": "HTML <strong>Intro</strong> Feed",
"display_zone_borders": false,
"display_zone_labels": false,
"url_name": "unique_name",
"zones": [
{
"index": 1,
"title": "Zone <i>1</i>",
"x": 100,
"y": 200,
"width": 200,
"height": 100,
"id": "zone-1"
},
{
"index": 2,
"title": "Zone <b>2</b>",
"x": 0,
"y": 0,
"width": 200,
"height": 100,
"id": "zone-2"
}
],
"items": [
{
"displayName": "<b>1</b>",
"imageURL": "",
"id": 0,
"inputOptions": false
},
{
"displayName": "<i>2</i>",
"imageURL": "",
"id": 1,
"inputOptions": true
},
{
"displayName": "X",
"imageURL": "",
"id": 2,
"inputOptions": false
},
{
"displayName": "",
"imageURL": "http://placehold.it/100x300",
"id": 3,
"inputOptions": false
}
]
}
{
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone <i>1</i>",
"height": 100,
"y": 200,
"x": 100,
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone <b>2</b>",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "<b>1</b>",
"feedback": {
"incorrect": "No <b>1</b>",
"correct": "Yes <b>1</b>"
},
"zone": "Zone <i>1</i>",
"imageURL": "",
"id": 0
},
{
"displayName": "<i>2</i>",
"feedback": {
"incorrect": "No <i>2</i>",
"correct": "Yes <i>2</i>"
},
"zone": "Zone <b>2</b>",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 2
},
{
"displayName": "",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "http://placehold.it/100x300",
"id": 3
}
],
"feedback": {
"start": "HTML <strong>Intro</strong> Feed",
"finish": "Final <strong>feedback</strong>!"
},
"targetImgDescription": "This describes the target image"
}
{
"display_name": "DnDv2 XBlock with HTML instructions",
"show_title": false,
"question_text": "Solve this <strong>drag-and-drop</strong> problem.",
"show_question_header": false,
"weight": 1,
"item_background_color": "white",
"item_text_color": "#000080",
"url_name": "unique_name"
}
{
"title": "Drag and Drop",
"show_title": true,
"problem_text": "",
"show_problem_header": true,
"target_img_expanded_url": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"target_img_description": "This describes the target image",
"item_background_color": null,
"item_text_color": null,
"initial_feedback": "Intro Feed",
"display_zone_borders": false,
"display_zone_labels": false,
"url_name": "",
"zones": [
{
"index": 1,
"title": "Zone 1",
"x": "100",
"y": "200",
"width": 200,
"height": 100,
"id": "zone-1"
},
{
"index": 2,
"title": "Zone 2",
"x": 0,
"y": 0,
"width": 200,
"height": 100,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"imageURL": "",
"id": 0,
"inputOptions": false,
"size": {"height": "auto", "width": "190px"}
},
{
"displayName": "2",
"imageURL": "",
"id": 1,
"inputOptions": true,
"size": {"height": "auto", "width": "190px"}
},
{
"displayName": "X",
"imageURL": "",
"id": 2,
"inputOptions": false,
"size": {"height": "100px", "width": "100px"}
},
{
"displayName": "",
"imageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg",
"id": 3,
"inputOptions": false,
"size": {"height": "auto", "width": "190px"}
}
]
}
{
"zones": [
{
"index": 1,
"width": 200,
"title": "Zone 1",
"height": 100,
"y": "200",
"x": "100",
"id": "zone-1"
},
{
"index": 2,
"width": 200,
"title": "Zone 2",
"height": 100,
"y": 0,
"x": 0,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"feedback": {
"incorrect": "No 1",
"correct": "Yes 1"
},
"zone": "Zone 1",
"imageURL": "",
"id": 0,
"size": {
"width": "190px",
"height": "auto"
}
},
{
"displayName": "2",
"feedback": {
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "Zone 2",
"imageURL": "",
"id": 1,
"size": {
"width": "190px",
"height": "auto"
},
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 2,
"size": {
"width": "100px",
"height": "100px"
}
},
{
"displayName": "",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg",
"id": 3,
"size": {
"width": "190px",
"height": "auto"
}
}
],
"state": {
"items": {},
"finished": true
},
"feedback": {
"start": "Intro Feed",
"finish": "Final Feed"
},
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"targetImgDescription": "This describes the target image"
}
{
"title": "DnDv2 XBlock with plain text instructions",
"show_title": true,
"problem_text": "Can you solve this drag-and-drop problem?",
"show_problem_header": true,
"target_img_expanded_url": "http://placehold.it/800x600",
"target_img_description": "This describes the target image",
"item_background_color": null,
"item_text_color": null,
"initial_feedback": "This is the initial feedback.",
"display_zone_borders": false,
"display_zone_labels": false,
"url_name": "test",
"zones": [
{
"index": 1,
"title": "Zone 1",
"y": 123,
"x": 234,
"width": 345,
"height": 456,
"id": "zone-1"
},
{
"index": 2,
"title": "Zone 2",
"y": 20,
"x": 10,
"width": 30,
"height": 40,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"imageURL": "",
"id": 0,
"inputOptions": false
},
{
"displayName": "2",
"imageURL": "",
"id": 1,
"inputOptions": true
},
{
"displayName": "X",
"imageURL": "",
"id": 2,
"inputOptions": false
},
{
"displayName": "",
"imageURL": "http://placehold.it/200x100",
"id": 3,
"inputOptions": false
}
]
}
{
"zones": [
{
"index": 1,
"title": "Zone 1",
"y": 123,
"x": 234,
"width": 345,
"height": 456,
"id": "zone-1"
},
{
"index": 2,
"title": "Zone 2",
"y": 20,
"x": 10,
"width": 30,
"height": 40,
"id": "zone-2"
}
],
"items": [
{
"displayName": "1",
"feedback": {
"incorrect": "No 1",
"correct": "Yes 1"
},
"zone": "Zone 1",
"imageURL": "",
"id": 0
},
{
"displayName": "2",
"feedback": {
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "Zone 2",
"imageURL": "",
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 2
},
{
"displayName": "",
"feedback": {
"incorrect": "",
"correct": ""
},
"zone": "none",
"imageURL": "http://placehold.it/200x100",
"id": 3
}
],
"feedback": {
"start": "This is the initial feedback.",
"finish": "This is the final feedback."
},
"targetImg": "http://placehold.it/800x600",
"targetImgDescription": "This describes the target image",
"displayLabels": false
}
{
"display_name": "DnDv2 XBlock with plain text instructions",
"show_title": true,
"question_text": "Can you solve this drag-and-drop problem?",
"show_question_header": true,
"weight": 1,
"item_background_color": "",
"item_text_color": "",
"url_name": "test"
}
# Imports ###########################################################
import json
import unittest
from xblockutils.resources import ResourceLoader
from ..utils import make_block, TestCaseMixin
# Globals ###########################################################
loader = ResourceLoader(__name__)
# Classes ###########################################################
class BaseDragAndDropAjaxFixture(TestCaseMixin):
ZONE_1 = None
ZONE_2 = None
FEEDBACK = {
0: {"correct": None, "incorrect": None},
1: {"correct": None, "incorrect": None},
2: {"correct": None, "incorrect": None}
}
FINAL_FEEDBACK = None
FOLDER = None
def setUp(self):
self.patch_workbench()
self.block = make_block()
initial_settings = self.initial_settings()
for field in initial_settings:
setattr(self.block, field, initial_settings[field])
self.block.data = self.initial_data()
@classmethod
def initial_data(cls):
return json.loads(loader.load_unicode('data/{}/data.json'.format(cls.FOLDER)))
@classmethod
def initial_settings(cls):
return json.loads(loader.load_unicode('data/{}/settings.json'.format(cls.FOLDER)))
@classmethod
def expected_configuration(cls):
return json.loads(loader.load_unicode('data/{}/config_out.json'.format(cls.FOLDER)))
@classmethod
def initial_feedback(cls):
""" The initial overall_feedback value """
return cls.expected_configuration()["initial_feedback"]
def test_get_configuration(self):
self.assertEqual(self.expected_configuration(), self.block.get_configuration())
def test_do_attempt_wrong_with_feedback(self):
item_id, zone_id = 0, self.ZONE_2
data = {"val": item_id, "zone": zone_id, "x_percent": "33%", "y_percent": "11%"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"overall_feedback": None,
"finished": False,
"correct": False,
"correct_location": False,
"feedback": self.FEEDBACK[item_id]["incorrect"]
})
def test_do_attempt_wrong_without_feedback(self):
item_id, zone_id = 2, self.ZONE_1
data = {"val": item_id, "zone": zone_id, "x_percent": "33%", "y_percent": "11%"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"overall_feedback": None,
"finished": False,
"correct": False,
"correct_location": False,
"feedback": self.FEEDBACK[item_id]["incorrect"]
})
def test_do_attempt_correct(self):
item_id, zone_id = 0, self.ZONE_1
data = {"val": item_id, "zone": zone_id, "x_percent": "33%", "y_percent": "11%"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"overall_feedback": None,
"finished": False,
"correct": True,
"correct_location": True,
"feedback": self.FEEDBACK[item_id]["correct"]
})
def test_do_attempt_with_input(self):
# Drop item that requires numerical input
data = {"val": 1, "zone": self.ZONE_2, "x_percent": "0%", "y_percent": "85%"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"finished": False,
"correct": False,
"correct_location": True,
"feedback": None,
"overall_feedback": None,
})
expected_state = {
'items': {
"1": {
"x_percent": "0%", "y_percent": "85%", "correct_input": False, "zone": self.ZONE_2,
},
},
'finished': False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
# Submit incorrect value
data = {"val": 1, "input": "250"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"finished": False,
"correct": False,
"correct_location": True,
"feedback": self.FEEDBACK[1]['incorrect'],
"overall_feedback": None
})
expected_state = {
'items': {
"1": {
"x_percent": "0%", "y_percent": "85%", "correct_input": False, "zone": self.ZONE_2,
"input": "250",
},
},
'finished': False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
# Submit correct value
data = {"val": 1, "input": "103"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"finished": False,
"correct": True,
"correct_location": True,
"feedback": self.FEEDBACK[1]['correct'],
"overall_feedback": None,
})
expected_state = {
'items': {
"1": {
"x_percent": "0%", "y_percent": "85%", "correct_input": True, "zone": self.ZONE_2,
"input": "103",
},
},
'finished': False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
def test_grading(self):
published_grades = []
def mock_publish(self, event, params):
if event == 'grade':
published_grades.append(params)
self.block.runtime.publish = mock_publish
self.call_handler('do_attempt', {
"val": 0, "zone": self.ZONE_1, "y_percent": "11%", "x_percent": "33%"
})
self.assertEqual(1, len(published_grades))
self.assertEqual({'value': 0.5, 'max_value': 1}, published_grades[-1])
self.call_handler('do_attempt', {
"val": 1, "zone": self.ZONE_2, "y_percent": "90%", "x_percent": "42%"
})
self.assertEqual(2, len(published_grades))
self.assertEqual({'value': 0.5, 'max_value': 1}, published_grades[-1])
self.call_handler('do_attempt', {"val": 1, "input": "99"})
self.assertEqual(3, len(published_grades))
self.assertEqual({'value': 1, 'max_value': 1}, published_grades[-1])
def test_do_attempt_final(self):
data = {"val": 0, "zone": self.ZONE_1, "x_percent": "33%", "y_percent": "11%"}
self.call_handler('do_attempt', data)
expected_state = {
"items": {
"0": {"x_percent": "33%", "y_percent": "11%", "correct_input": True, "zone": self.ZONE_1}
},
"finished": False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
data = {"val": 1, "zone": self.ZONE_2, "x_percent": "22%", "y_percent": "22%"}
res = self.call_handler('do_attempt', data)
data = {"val": 1, "input": "99"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"overall_feedback": self.FINAL_FEEDBACK,
"finished": True,
"correct": True,
"correct_location": True,
"feedback": self.FEEDBACK[1]["correct"]
})
expected_state = {
"items": {
"0": {
"x_percent": "33%", "y_percent": "11%", "correct_input": True, "zone": self.ZONE_1,
},
"1": {
"x_percent": "22%", "y_percent": "22%", "correct_input": True, "zone": self.ZONE_2,
"input": "99",
}
},
"finished": True,
'overall_feedback': self.FINAL_FEEDBACK,
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
class TestDragAndDropHtmlData(BaseDragAndDropAjaxFixture, unittest.TestCase):
FOLDER = "html"
ZONE_1 = "Zone <i>1</i>"
ZONE_2 = "Zone <b>2</b>"
FEEDBACK = {
0: {"correct": "Yes <b>1</b>", "incorrect": "No <b>1</b>"},
1: {"correct": "Yes <i>2</i>", "incorrect": "No <i>2</i>"},
2: {"correct": "", "incorrect": ""}
}
FINAL_FEEDBACK = "Final <strong>feedback</strong>!"
class TestDragAndDropPlainData(BaseDragAndDropAjaxFixture, unittest.TestCase):
FOLDER = "plain"
ZONE_1 = "Zone 1"
ZONE_2 = "Zone 2"
FEEDBACK = {
0: {"correct": "Yes 1", "incorrect": "No 1"},
1: {"correct": "Yes 2", "incorrect": "No 2"},
2: {"correct": "", "incorrect": ""}
}
FINAL_FEEDBACK = "This is the final feedback."
class TestOldDataFormat(TestDragAndDropPlainData):
"""
Make sure we can work with the slightly-older format for 'data' field values.
"""
FOLDER = "old"
FINAL_FEEDBACK = "Final Feed"
import unittest
from drag_and_drop_v2.default_data import (
TARGET_IMG_DESCRIPTION, TOP_ZONE_TITLE, MIDDLE_ZONE_TITLE, BOTTOM_ZONE_TITLE,
START_FEEDBACK, FINISH_FEEDBACK, DEFAULT_DATA
)
from ..utils import make_block, TestCaseMixin
class BasicTests(TestCaseMixin, unittest.TestCase):
""" Basic unit tests for the Drag and Drop block, using its default settings """
def setUp(self):
self.block = make_block()
self.patch_workbench()
def test_template_contents(self):
context = {}
student_fragment = self.block.runtime.render(self.block, 'student_view', context)
self.assertIn('<section class="themed-xblock xblock--drag-and-drop">', student_fragment.content)
self.assertIn('Loading drag and drop problem.', student_fragment.content)
def test_get_configuration(self):
"""
Test the get_configuration() method.
The result of this method is passed to the block's JavaScript during initialization.
"""
config = self.block.get_configuration()
zones = config.pop("zones")
items = config.pop("items")
self.assertEqual(config, {
"display_zone_borders": False,
"display_zone_labels": False,
"title": "Drag and Drop",
"show_title": True,
"problem_text": "",
"show_problem_header": True,
"target_img_expanded_url": '/expanded/url/to/drag_and_drop_v2/public/img/triangle.png',
"target_img_description": TARGET_IMG_DESCRIPTION,
"item_background_color": None,
"item_text_color": None,
"initial_feedback": START_FEEDBACK,
"url_name": "",
})
self.assertEqual(zones, DEFAULT_DATA["zones"])
# Items should contain no answer data:
self.assertEqual(items, [
{"id": 0, "displayName": "Goes to the top", "imageURL": "", "inputOptions": False},
{"id": 1, "displayName": "Goes to the middle", "imageURL": "", "inputOptions": False},
{"id": 2, "displayName": "Goes to the bottom", "imageURL": "", "inputOptions": False},
{"id": 3, "displayName": "I don't belong anywhere", "imageURL": "", "inputOptions": False},
])
def test_ajax_solve_and_reset(self):
# Check assumptions / initial conditions:
self.assertFalse(self.block.completed)
def assert_user_state_empty():
self.assertEqual(self.block.item_state, {})
self.assertEqual(self.call_handler("get_user_state"), {
'items': {},
'finished': False,
'overall_feedback': START_FEEDBACK,
})
assert_user_state_empty()
# Drag three items into the correct spot:
data = {"val": 0, "zone": TOP_ZONE_TITLE, "x_percent": "33%", "y_percent": "11%"}
self.call_handler('do_attempt', data)
data = {"val": 1, "zone": MIDDLE_ZONE_TITLE, "x_percent": "67%", "y_percent": "80%"}
self.call_handler('do_attempt', data)
data = {"val": 2, "zone": BOTTOM_ZONE_TITLE, "x_percent": "99%", "y_percent": "95%"}
self.call_handler('do_attempt', data)
# Check the result:
self.assertTrue(self.block.completed)
self.assertEqual(self.block.item_state, {
'0': {'x_percent': '33%', 'y_percent': '11%', 'zone': TOP_ZONE_TITLE},
'1': {'x_percent': '67%', 'y_percent': '80%', 'zone': MIDDLE_ZONE_TITLE},
'2': {'x_percent': '99%', 'y_percent': '95%', 'zone': BOTTOM_ZONE_TITLE},
})
self.assertEqual(self.call_handler('get_user_state'), {
'items': {
'0': {'x_percent': '33%', 'y_percent': '11%', 'correct_input': True, 'zone': TOP_ZONE_TITLE},
'1': {'x_percent': '67%', 'y_percent': '80%', 'correct_input': True, 'zone': MIDDLE_ZONE_TITLE},
'2': {'x_percent': '99%', 'y_percent': '95%', 'correct_input': True, 'zone': BOTTOM_ZONE_TITLE},
},
'finished': True,
'overall_feedback': FINISH_FEEDBACK,
})
# Reset to initial conditions
self.call_handler('reset', {})
self.assertTrue(self.block.completed)
assert_user_state_empty()
def test_studio_submit(self):
body = {
'display_name': "Test Drag & Drop",
'show_title': False,
'problem_text': "Problem Drag & Drop",
'show_problem_header': False,
'item_background_color': 'cornflowerblue',
'item_text_color': 'coral',
'weight': '5',
'data': {
'foo': 1
},
}
res = self.call_handler('studio_submit', body)
self.assertEqual(res, {'result': 'success'})
self.assertEqual(self.block.show_title, False)
self.assertEqual(self.block.display_name, "Test Drag & Drop")
self.assertEqual(self.block.question_text, "Problem Drag & Drop")
self.assertEqual(self.block.show_question_header, False)
self.assertEqual(self.block.item_background_color, "cornflowerblue")
self.assertEqual(self.block.item_text_color, "coral")
self.assertEqual(self.block.weight, 5)
self.assertEqual(self.block.data, {'foo': 1})
def test_expand_static_url(self):
""" Test the expand_static_url handler needed in Studio when changing the image """
res = self.call_handler('expand_static_url', '/static/blah.png')
self.assertEqual(res, {'url': '/course/test-course/assets/blah.png'})
def test_image_url(self):
""" Ensure that the default image and custom URLs are both expanded by the runtime """
self.assertEqual(self.block.data.get("targetImg"), None)
self.assertEqual(
self.block.get_configuration()["target_img_expanded_url"],
'/expanded/url/to/drag_and_drop_v2/public/img/triangle.png',
)
self.block.data["targetImg"] = "/static/foo.png"
self.assertEqual(
self.block.get_configuration()["target_img_expanded_url"],
'/course/test-course/assets/foo.png',
)
import json
import re
from mock import patch
from webob import Request
from workbench.runtime import WorkbenchRuntime
from xblock.fields import ScopeIds
from xblock.runtime import KvsFieldData, DictKeyValueStore
import drag_and_drop_v2
def make_request(data, method='POST'):
""" Make a webob JSON Request """
request = Request.blank('/')
request.method = 'POST'
request.body = json.dumps(data).encode('utf-8') if data is not None else ""
request.method = method
return request
def make_block():
""" Instantiate a DragAndDropBlock XBlock inside a WorkbenchRuntime """
block_type = 'drag_and_drop_v2'
key_store = DictKeyValueStore()
field_data = KvsFieldData(key_store)
runtime = WorkbenchRuntime()
def_id = runtime.id_generator.create_definition(block_type)
usage_id = runtime.id_generator.create_usage(def_id)
scope_ids = ScopeIds('user', block_type, def_id, usage_id)
return drag_and_drop_v2.DragAndDropBlock(runtime, field_data, scope_ids=scope_ids)
class TestCaseMixin(object):
""" Helpful mixins for unittest TestCase subclasses """
maxDiff = None
def patch_workbench(self):
self.apply_patch(
'workbench.runtime.WorkbenchRuntime.local_resource_url',
lambda _, _block, path: '/expanded/url/to/drag_and_drop_v2/' + path
)
self.apply_patch(
'workbench.runtime.WorkbenchRuntime.replace_urls',
lambda _, html: re.sub(r'"/static/([^"]*)"', r'"/course/test-course/assets/\1"', html),
create=True,
)
def apply_patch(self, *args, **kwargs):
new_patch = patch(*args, **kwargs)
mock = new_patch.start()
self.addCleanup(new_patch.stop)
return mock
def call_handler(self, handler_name, data=None, expect_json=True, method='POST'):
response = self.block.handle(handler_name, make_request(data, method=method))
if expect_json:
self.assertEqual(response.status_code, 200)
return json.loads(response.body)
return response
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