first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

14
themes/bootstrap/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# OS
.DS_Store
# Applications & Tools
.idea
bower_components
config.codekit
node_modules
npm-debug.log
# Project Specific
.libraries
lib
starterkits/*/bootstrap

View File

@@ -0,0 +1,99 @@
################
# DrupalCI GitLabCI template
#
# Gitlab-ci.yml to replicate DrupalCI testing for Contrib
#
# With thanks to:
# * The GitLab Acceleration Initiative participants
# * DrupalSpoons
################
################
# Guidelines
#
# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification. It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
#
# However, you can modify this template if you have additional needs for your project.
################
################
# Includes
#
# Additional configuration can be provided through includes.
# One advantage of include files is that if they are updated upstream, the changes affect all pipelines using that include.
#
# Includes can be overridden by re-declaring anything provided in an include, here in gitlab-ci.yml
# https://docs.gitlab.com/ee/ci/yaml/includes.html#override-included-configuration-values
################
include:
################
# DrupalCI includes:
# As long as you include this, any future includes added by the Drupal Association will be accessible to your pipelines automatically.
# View these include files at https://git.drupalcode.org/project/gitlab_templates/
################
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
# EXPERIMENTAL: For Drupal 7, remove the above line and uncomment the below.
# - '/includes/include.drupalci.main-d7.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
################
# Pipeline configuration variables
#
# These are the variables provided to the Run Pipeline form that a user may want to override.
#
# Docs at https://git.drupalcode.org/project/gitlab_templates/-/blob/main/includes/include.drupalci.variables.yml
################
# variables:
# SKIP_ESLINT: '1'
###################################################################################
#
# *
# /(
# ((((,
# /(((((((
# ((((((((((*
# ,(((((((((((((((
# ,(((((((((((((((((((
# ((((((((((((((((((((((((*
# *(((((((((((((((((((((((((((((
# ((((((((((((((((((((((((((((((((((*
# *(((((((((((((((((( .((((((((((((((((((
# ((((((((((((((((((. /(((((((((((((((((*
# /((((((((((((((((( .(((((((((((((((((,
# ,(((((((((((((((((( ((((((((((((((((((
# .(((((((((((((((((((( .(((((((((((((((((
# ((((((((((((((((((((((( ((((((((((((((((/
# (((((((((((((((((((((((((((/ ,(((((((((((((((*
# .((((((((((((((/ /(((((((((((((. ,(((((((((((((((
# *(((((((((((((( ,(((((((((((((/ *((((((((((((((.
# ((((((((((((((, /(((((((((((((. ((((((((((((((,
# (((((((((((((/ ,(((((((((((((* ,(((((((((((((,
# *((((((((((((( .((((((((((((((( ,(((((((((((((
# ((((((((((((/ /((((((((((((((((((. ,((((((((((((/
# ((((((((((((( *(((((((((((((((((((((((* *((((((((((((
# ((((((((((((( ,(((((((((((((..((((((((((((( *((((((((((((
# ((((((((((((, /((((((((((((* /((((((((((((/ ((((((((((((
# ((((((((((((( /((((((((((((/ (((((((((((((* ((((((((((((
# (((((((((((((/ /(((((((((((( ,((((((((((((, *((((((((((((
# (((((((((((((( *(((((((((((/ *((((((((((((. ((((((((((((/
# *((((((((((((((((((((((((((, /(((((((((((((((((((((((((
# ((((((((((((((((((((((((( ((((((((((((((((((((((((,
# .(((((((((((((((((((((((/ ,(((((((((((((((((((((((
# ((((((((((((((((((((((/ ,(((((((((((((((((((((/
# *((((((((((((((((((((( (((((((((((((((((((((,
# ,(((((((((((((((((((((, ((((((((((((((((((((/
# ,(((((((((((((((((((((* /((((((((((((((((((((
# ((((((((((((((((((((((, ,/((((((((((((((((((((,
# ,(((((((((((((((((((((((((((((((((((((((((((((((((((
# .(((((((((((((((((((((((((((((((((((((((((((((
# .((((((((((((((((((((((((((((((((((((,.
# .,(((((((((((((((((((((((((.
#
###################################################################################

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
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
this service 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.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
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
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the 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 a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE 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.
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
convey 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 General Public License as published by
the Free Software Foundation; either version 2 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 General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,44 @@
<!-- @file Project Page -->
# Bootstrap
> Sleek, intuitive, and powerful mobile first front-end framework for faster and
> easier web development. Bootstrap has become one of the most popular front-end
> frameworks and open source projects in the world.
This base theme bridges the gap between Drupal and the [Bootstrap Framework].
### Features
- [jsDelivr CDN](http://www.jsdelivr.com) for "out-of-the-box" styling and
faster page load times.
- [Bootswatch](http://bootswatch.com) theme support, if using the CDN.
- Glyphicons support via [Icon API](https://www.drupal.org/project/icon).
- Extensive integration and template/preprocessor overrides for most of the
[Bootstrap Framework] CSS, Components and JavaScript
- Theme settings to further enhance the Drupal Bootstrap integration:
- [Breadcrumbs](https://getbootstrap.com/docs/3.4/components/#breadcrumbs)
- [Navbar](https://getbootstrap.com/docs/3.4/components/#navbar)
- [Popovers](https://getbootstrap.com/docs/3.4/javascript/#popovers)
- [Tooltips](https://getbootstrap.com/docs/3.4/javascript/#tooltips)
- [Wells](https://getbootstrap.com/docs/3.4/components/#wells) (per region)
### Documentation
Visit the project's [official documentation site](http://drupal-bootstrap.org/api/bootstrap)
or the markdown files inside the `./docs` directory.
### Supported modules
**Drupal 8**
- [Bootstrap Layouts](https://www.drupal.org/project/bootstrap_layouts)
**Drupal 7**
- [Bootstrap Core](https://www.drupal.org/project/bootstrap_core)
- [jQuery Update](https://www.drupal.org/project/jquery_update)
- [Icon API](https://www.drupal.org/project/icon)
- [Picture](https://www.drupal.org/project/picture)
- [Views](https://www.drupal.org/project/views) _(partial support)_
- [Webform](https://www.drupal.org/project/webform) _(partial support)_
### 5 Year Evolution (gource)
https://youtu.be/Cvq6MPJp2dI
[Bootstrap Framework]: https://getbootstrap.com/docs/3.4/

View File

@@ -0,0 +1,20 @@
<?php
/**
* @file
* Fix the class loader to allow cross-request class based theme callbacks.
*/
/**
* Invokes \Drupal\Core\Extension\ThemeHandler::listInfo.
*
* This is in an attempt to ensure theme autoloading works properly.
*
* @see \Drupal\bootstrap\Bootstrap::autoloadFixInclude
*/
try {
\Drupal::service('theme_handler')->listInfo();
}
catch (\Exception $e) {
// Intentionally left blank.
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* @file
* List of available procedural hook and alter APIs for use in your sub-theme.
*/
/**
* @addtogroup plugins_alter
*
* @{
*/
/**
* Allows sub-themes to alter the array used for colorizing text.
*
* @param array $texts
* An associative array containing the text and classes to be matched, passed
* by reference.
*
* @see \Drupal\bootstrap\Bootstrap::cssClassFromString()
*/
function hook_bootstrap_colorize_text_alter(array &$texts) {
// This matches the exact string: "My Unique Button Text".
// Note: the t() function in D8 returns a TranslatableMarkup object.
// It must be rendered to a string before it can be added as an array key.
$texts['matches'][t('My Unique Button Text')->render()] = 'primary';
// This would also match the string above, however the class returned would
// also be the one above; "matches" takes precedence over "contains".
$texts['contains'][t('Unique')->render()] = 'notice';
// Remove matching for strings that contain "apply":
unset($texts['contains'][t('Apply')->render()]);
// Change the class that matches "Rebuild" (originally "warning"):
$texts['contains'][t('Rebuild')->render()] = 'success';
}
/**
* Allows sub-themes to alter the array used for associating an icon with text.
*
* @param array $texts
* An associative array containing the text and icons to be matched, passed
* by reference.
*
* @see \Drupal\bootstrap\Bootstrap::glyphiconFromString()
*/
function hook_bootstrap_iconize_text_alter(array &$texts) {
// This matches the exact string: "My Unique Button Text".
// Note: the t() function in D8 returns a TranslatableMarkup object.
// It must be rendered to a string before it can be added as an array key.
$texts['matches'][t('My Unique Button Text')->render()] = 'heart';
// This would also match the string above, however the class returned would
// also be the one above; "matches" takes precedence over "contains".
$texts['contains'][t('Unique')->render()] = 'bullhorn';
// Remove matching for strings that contain "filter":
unset($texts['contains'][t('Filter')->render()]);
// Change the icon that matches "Upload" (originally "upload"):
$texts['contains'][t('Upload')->render()] = 'ok';
}
/**
* Allows sub-themes to alter element types that should be rendered as inline.
*
* @param array $types
* The list of element types that should be rendered as inline.
*
* @deprecated in bootstrap:8.x-3.21 and is removed from bootstrap:8.x-4.0.
* This method will be removed when process managers can be sub-classed.
*
* @see https://www.drupal.org/project/bootstrap/issues/2868538
*/
function hook_bootstrap_inline_element_types_alter(array &$types) {
// Remove certain types from the list.
foreach (['number', 'tel'] as $type) {
$index = array_search($type, $types);
if ($index !== FALSE) {
unset($types[$index]);
}
}
}
/**
* @} End of "addtogroup".
*/

View File

@@ -0,0 +1,42 @@
bootstrap.screen-xs-max:
label: screen-xs-max
mediaQuery: 'all and (max-width: 767px)'
weight: 1
multipliers:
- 1x
- 2x
bootstrap.screen-sm-min:
label: screen-sm-min
mediaQuery: 'all and (min-width: 768px)'
weight: 2
multipliers:
- 1x
- 2x
bootstrap.screen-sm-max:
label: screen-sm-max
mediaQuery: 'all and (max-width: 991px)'
weight: 3
multipliers:
- 1x
- 2x
bootstrap.screen-md-min:
label: screen-md-min
mediaQuery: 'all and (min-width: 992px)'
weight: 4
multipliers:
- 1x
- 2x
bootstrap.screen-md-max:
label: screen-md-max
mediaQuery: 'all and (max-width: 1199px)'
weight: 5
multipliers:
- 1x
- 2x
bootstrap.screen-lg-min:
label: screen-lg-min
mediaQuery: 'all and (min-width: 1200px)'
weight: 6
multipliers:
- 1x
- 2x

View File

@@ -0,0 +1,84 @@
type: theme
base theme: false
core_version_requirement: ^9.5 || ^10
php: 8.1
name: 'Bootstrap'
description: 'Built to use Bootstrap, a sleek, intuitive, and powerful front-end framework for faster and easier web development.'
package: 'Bootstrap'
regions:
navigation: 'Navigation'
navigation_collapsible: 'Navigation (Collapsible)'
header: 'Top Bar'
highlighted: 'Highlighted'
help: 'Help'
content: 'Content'
sidebar_first: 'Primary'
sidebar_second: 'Secondary'
footer: 'Footer'
page_top: 'Page top'
page_bottom: 'Page bottom'
libraries:
- 'bootstrap/theme'
libraries-extend:
core/drupal.ajax:
- bootstrap/drupal.ajax
core/drupal.autocomplete:
- bootstrap/drupal.autocomplete
core/drupal.dialog.ajax:
- bootstrap/drupal.dialog.ajax
core/drupal.form:
- bootstrap/drupal.form
core/drupal.message:
- bootstrap/drupal.message
core/drupal.progress:
- bootstrap/drupal.progress
core/drupal.states:
- bootstrap/drupal.states
core/drupal.tabledrag:
- bootstrap/drupal.tabledrag
core/drupal.tableheader:
- bootstrap/drupal.tableheader
image_widget_crop/cropper.integration:
- bootstrap/image_widget_crop
views/views.ajax:
- bootstrap/views.ajax
libraries-override:
core/drupal.batch: bootstrap/drupal.batch
core/drupal.dropbutton: bootstrap/dropdown
core/drupal.vertical-tabs: bootstrap/drupal.vertical-tabs
filter/drupal.filter: bootstrap/drupal.filter
system/admin: false
text/drupal.text: bootstrap/drupal.text
# Remove individual stylesheets.
system/base:
css:
component:
css/components/ajax-progress.module.css: false
css/components/autocomplete-loading.module.css: false
css/components/tabledrag.module.css: false
file/drupal.file:
css:
theme:
css/file.admin.css: false
filter/drupal.filter.admin:
css:
theme:
css/filter.admin.css: false
node/drupal.node.preview:
css:
theme:
css/node.preview.css: false
# Information added by Drupal.org packaging script on 2024-06-21
version: '8.x-3.31'
project: 'bootstrap'
datestamp: 1718994541

View File

@@ -0,0 +1,173 @@
attributes:
js:
js/attributes.js: {}
dependencies:
- core/jquery
- core/internal.underscore
# This is automatically extended with JavaScript and CSS for CDN based themes.
# If sub-theme uses a starterkit like LESS or SASS, then it should extend this
# library to add in the compiled CSS and JavaScript sources.
framework:
css: {}
js: {}
dependencies:
- core/jquery
drupal.bootstrap:
js:
js/drupal.bootstrap.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
- core/internal.underscore
- bootstrap/framework
dialog:
js:
js/dialog.js: {}
dependencies:
- bootstrap/theme
- bootstrap/modal
# Create a library placeholder for livereload.
# This is altered dynamically based on the set URL.
# @see \Drupal\bootstrap\Plugin\Alter\LibraryInfo::alter
livereload:
js:
livereload.js: {}
theme:
js:
js/theme.js: {}
dependencies:
- bootstrap/drupal.bootstrap
- bootstrap/attributes
theme-settings:
js:
js/theme-settings.js: {}
dependencies:
- core/jquery
- core/once
- core/drupal
- core/drupalSettings
dropdown:
js:
js/dropdown.js: {}
dependencies:
- bootstrap/theme
modal:
js:
js/modal.js: {}
dependencies:
- core/internal.underscore
- bootstrap/theme
modal.jquery.ui.bridge:
js:
js/modal.jquery.ui.bridge.js: {}
dependencies:
- bootstrap/modal
- jquery_ui/core
- jquery_ui_draggable/draggable
- jquery_ui_resizable/resizable
popover:
js:
js/popover.js: {}
dependencies:
- bootstrap/theme
tooltip:
js:
js/tooltip.js: {}
dependencies:
- bootstrap/theme
# libraries-extend
drupal.ajax:
js:
js/misc/ajax.js: {}
drupal.autocomplete:
js:
js/misc/autocomplete.js: {}
drupal.dialog.ajax:
js:
js/misc/dialog.ajax.js: {}
dependencies:
- bootstrap/drupal.bootstrap
drupal.form:
js:
js/misc/form.js: {}
dependencies:
- bootstrap/theme
drupal.message:
js:
js/misc/message.js: {}
drupal.progress:
js:
js/misc/progress.js: {}
drupal.states:
js:
js/misc/states.js: {}
drupal.tabledrag:
js:
js/misc/tabledrag.js: {}
drupal.tableheader:
js:
js/misc/tableheader.js: {}
image_widget_crop:
js:
js/modules/image_widget_crop/ImageWidgetCrop.js: {}
views.ajax:
js:
js/modules/views/ajax_view.js: {}
# libraries-override
drupal.batch:
js:
js/misc/batch.js: { cache: false }
dependencies:
- bootstrap/theme
- core/drupal.ajax
- core/drupal.progress
- core/once
drupal.filter:
version: VERSION
js:
js/modules/filter/filter.js: {}
dependencies:
- core/jquery
- core/drupal
- core/once
drupal.text:
js:
js/text/text.js: {}
dependencies:
- core/jquery
- core/once
- core/drupal
drupal.vertical-tabs:
js:
js/misc/vertical-tabs.js: {}
dependencies:
- bootstrap/theme
- core/once
- core/drupal.form

View File

@@ -0,0 +1,149 @@
<?php
/**
* @file
* The primary PHP file for the Drupal Bootstrap base theme.
*
* This file should only contain light helper functions and point to stubs in
* other files containing more complex functions.
*
* The stubs should point to files within the `./includes` directory named after
* the function itself minus the theme prefix. If the stub contains a group of
* functions, then please organize them so they are related in some way and name
* the file appropriately to at least hint at what it contains.
*
* All [pre]process functions, theme functions and template files lives inside
* the `./templates` directory. This is a highly automated and complex system
* designed to only load the necessary files when a given theme hook is invoked.
*
* Visit this project's official documentation site https://drupal-bootstrap.org
* or the markdown files inside the `./docs` directory.
*
* @see bootstrap_theme()
* @see \Drupal\bootstrap\Registry
*/
use Drupal\bootstrap\Bootstrap;
/**
* Initialize the active theme.
*/
Bootstrap::initialize();
/**
* {@inheritdoc}
*
* @see hook_bootstrap_colorize_text_alter()
*/
function bootstrap_bootstrap_colorize_text_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}
/**
* {@inheritdoc}
*
* @see hook_bootstrap_iconize_text_alter()
*/
function bootstrap_bootstrap_iconize_text_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}
/**
* {@inheritdoc}
*
* @see hook_bootstrap_layouts_class_options_alter()
*/
function bootstrap_bootstrap_layouts_class_options_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}
/**
* {@inheritdoc}
*
* @see \Drupal\bootstrap\Plugin\Alter\ElementInfo::alter()
*/
function bootstrap_element_info_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}
/**
* {@inheritdoc}
*
* @see \Drupal\bootstrap\Theme::alter()
*/
function bootstrap_form_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}
/**
* {@inheritdoc}
*
* @see \Drupal\bootstrap\Plugin\Form\SystemThemeSettings::alterForm()
*/
function bootstrap_form_system_theme_settings_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}
/**
* {@inheritdoc}
*/
function bootstrap_js_settings_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}
/**
* {@inheritdoc}
*
* @see \Drupal\bootstrap\Plugin\Alter\LibraryInfo::alter()
*/
function bootstrap_library_info_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}
/**
* {@inheritdoc}
*
* @see \Drupal\bootstrap\Plugin\Alter\PageAttachments::alter()
*/
function bootstrap_page_attachments_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}
/**
* {@inheritdoc}
*
* @see \Drupal\bootstrap\Bootstrap::preprocess()
*/
function bootstrap_preprocess(&$variables, $hook, $info) {
Bootstrap::preprocess($variables, $hook, $info);
}
/**
* {@inheritdoc}
*
* @see \Drupal\bootstrap\Bootstrap::getInfo()
*/
function bootstrap_theme() {
return Bootstrap::getThemeHooks();
}
/**
* {@inheritdoc}
*
* @see \Drupal\bootstrap\Plugin\Alter\ThemeRegistry::alter()
*
* @todo Remove once a proper replacement for the theme.registry service can be
* created in a bootstrap_core sub-module.
*/
function bootstrap_theme_registry_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}
/**
* {@inheritdoc}
*
* @see \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions::alter()
*/
function bootstrap_theme_suggestions_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}

View File

@@ -0,0 +1,47 @@
{
"name": "drupal/bootstrap",
"description": "Built to use Bootstrap, a sleek, intuitive, and powerful front-end framework for faster and easier web development.",
"type": "drupal-theme",
"license": "GPL-2.0-or-later",
"homepage": "https://www.drupal.org/project/bootstrap",
"authors": [
{
"name": "Mark Carver (markcarver)",
"homepage": "https://www.drupal.org/u/markcarver",
"role": "Maintainer"
},
{
"name": "John McCormick (neardark)",
"homepage": "https://www.drupal.org/u/neardark",
"role": "Co-maintainer"
},
{
"name": "Fabiano Sant'Ana (wundo)",
"homepage": "https://www.drupal.org/u/wundo",
"role": "Co-maintainer"
},
{
"name": "Shelane French (shelane)",
"homepage": "https://www.drupal.org/u/shelane",
"role": "Co-maintainer"
}
],
"support": {
"docs": "https://drupal-bootstrap.org",
"issues": "https://www.drupal.org/project/issues/bootstrap",
"irc": "irc://irc.freenode.org/drupal-bootstrap",
"source": "https://git.drupalcode.org/project/bootstrap"
},
"require": {
"drupal/core": "^9.5 || ^10",
"php": ">=8.1.0",
"drupal/jquery_ui": "^1.6.0",
"drupal/jquery_ui_draggable": "^2.0.0",
"drupal/jquery_ui_resizable": "^2.0.0"
},
"autoload": {
"psr-4": {
"Drupal\\bootstrap\\": "src"
}
}
}

View File

@@ -0,0 +1,938 @@
<?php
/**
* @file
* This contains deprecated functions that will be removed in a future release.
*/
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Plugin\ProviderManager;
use Drupal\bootstrap\Utility\Element;
use Drupal\bootstrap\Utility\Unicode;
use Drupal\Component\Utility\NestedArray;
/**
* The base file system path for CDN providers.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* echo BOOTSTRAP_CDN_PROVIDER_PATH;
*
* // After.
* use Drupal\bootstrap\Plugin\ProviderManager;
* echo ProviderManager::FILE_PATH;
* @endcode
*
* @see \Drupal\bootstrap\Plugin\ProviderManager::FILE_PATH
*/
define('BOOTSTRAP_CDN_PROVIDER_PATH', ProviderManager::FILE_PATH);
/**
* The current supported Bootstrap framework major version number.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* echo BOOTSTRAP_VERSION_MAJOR;
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* echo substr(Bootstrap::FRAMEWORK_VERSION, 0, 1);
* @endcode
*
* @see \Drupal\bootstrap\Bootstrap::FRAMEWORK_VERSION
*/
define('BOOTSTRAP_VERSION_MAJOR', substr(Bootstrap::FRAMEWORK_VERSION, 0, 1));
/**
* The current supported Bootstrap framework minor version number.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* echo BOOTSTRAP_VERSION_MINOR;
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* echo substr(Bootstrap::FRAMEWORK_VERSION, 2, 1);
* @endcode
*
* @see \Drupal\bootstrap\Bootstrap::FRAMEWORK_VERSION
*/
define('BOOTSTRAP_VERSION_MINOR', substr(Bootstrap::FRAMEWORK_VERSION, 2, 1));
/**
* The current supported Bootstrap framework patch version number.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* echo BOOTSTRAP_VERSION_PATCH;
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* echo substr(Bootstrap::FRAMEWORK_VERSION, 4, 1);
* @endcode
*
* @see \Drupal\bootstrap\Bootstrap::FRAMEWORK_VERSION
*/
define('BOOTSTRAP_VERSION_PATCH', substr(Bootstrap::FRAMEWORK_VERSION, 4, 1));
/**
* The current supported Bootstrap framework version.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* echo BOOTSTRAP_VERSION;
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* echo Bootstrap::FRAMEWORK_VERSION;
* @endcode
*
* @see \Drupal\bootstrap\Bootstrap::FRAMEWORK_VERSION
*/
define('BOOTSTRAP_VERSION', Bootstrap::FRAMEWORK_VERSION);
/**
* Adds a class to an element's render array.
*
* @param string|array $class
* An individual class or an array of classes to add.
* @param array $element
* The individual renderable array element. It is possible to also pass the
* $variables parameter in [pre]process functions and it will logically
* determine the correct path to that particular theme hook's classes array.
* Passed by reference.
* @param string $property
* Determines which attributes array to retrieve. By default, this is the
* element's normal "attributes", but it could also be one of the following:
* - "content_attributes"
* - "input_group_attributes"
* - "title_attributes"
* - "wrapper_attributes".
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* _bootstrap_add_class('my-class', $element);
*
* // After.
* use Drupal\bootstrap\Utility\Element;
* Element::create($element)->addClass('my-class');
* @endcode
*
* @see \Drupal\bootstrap\Utility\Element::addClass()
*/
function _bootstrap_add_class($class, array &$element, $property = 'attributes') {
Bootstrap::deprecated();
Element::create($element)->addClass($class, $property);
}
/**
* Adds a specific Bootstrap class to color a button based on its text value.
*
* @param array $element
* The form element, passed by reference.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* _bootstrap_colorize_button($element);
*
* // After.
* use Drupal\bootstrap\Utility\Element;
* Element::create($element)->colorize();
* @endcode
*
* @see \Drupal\bootstrap\Utility\Element::colorize()
* */
function _bootstrap_colorize_button(array &$element) {
Bootstrap::deprecated();
Element::create($element)->colorize();
}
/**
* Matches a Bootstrap class based on a string value.
*
* @param string $string
* The string to match classes against.
* @param string $default
* The default class to return if no match is found.
*
* @return string
* The Bootstrap class matched against the value of $haystack or $default if
* no match could be made.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $class = _bootstrap_colorize_text($string, $default);
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* $class = Bootstrap::cssClassFromString($string, $default);
* @endcode
*
* @see \Drupal\bootstrap\Bootstrap::cssClassFromString()
*/
function _bootstrap_colorize_text($string, $default = '') {
Bootstrap::deprecated();
return Bootstrap::cssClassFromString($string, $default);
}
/**
* Wrapper for the core file_scan_directory() function.
*
* Finds all files that match a given mask in a given directory and then caches
* the results. A general site cache clear will force new scans to be initiated
* for already cached directories.
*
* @param string $dir
* The base directory or URI to scan, without trailing slash.
* @param string $mask
* The preg_match() regular expression of the files to find.
* @param array $options
* Additional options to pass to file_scan_directory().
*
* @return array
* An associative array (keyed on the chosen key) of objects with 'uri',
* 'filename', and 'name' members corresponding to the matching files.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $files = _bootstrap_file_scan_directory($theme_path . '/js', '/\.js$/');
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* $files = Bootstrap::getTheme()->fileScan('/\.js$/', 'js');
* @endcode
*
* @see \Drupal\bootstrap\Theme::fileScan()
* @see file_scan_directory()
*/
function _bootstrap_file_scan_directory($dir, $mask, array $options = []) {
Bootstrap::deprecated();
$theme = Bootstrap::getTheme();
$dir = preg_replace('/^' . preg_quote($theme->getPath()) . '\//', '', $dir);
return $theme->fileScan($mask, $dir, $options);
}
/**
* Retrieves an element's "attributes" array.
*
* @param array $element
* The individual renderable array element, passed by reference.
* @param string $property
* Determines which attributes array to retrieve. By default, this is the
* element's normal "attributes", but it could also be one of the following:
* - "content_attributes"
* - "input_group_attributes"
* - "title_attributes"
* - "wrapper_attributes".
*
* @return array
* The attributes array.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $attributes = &_bootstrap_get_attributes($element);
* $attributes['class'][] = 'my-class';
*
* // After.
* use Drupal\bootstrap\Utility\Element;
* $attributes = &Element::create($element)->getAttributes();
* $attributes['class'][] = 'my-class';
* @endcode
*
* @see \Drupal\bootstrap\Utility\Element::getAttributes()
*/
function &_bootstrap_get_attributes(array &$element, $property = 'attributes') {
Bootstrap::deprecated();
return Element::create($element)->getAttributes($property);
}
/**
* Returns a list of base themes for active or provided theme.
*
* @param string $theme_key
* The machine name of the theme to check, if not set the active theme name
* will be used.
* @param bool $include_theme_key
* Whether to append the returned list with $theme_key.
*
* @return array
* An indexed array of base themes.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before (including active theme).
* $base_themes = _bootstrap_get_base_themes(NULL, TRUE);
*
* // Before (excluding active theme).
* $base_themes = _bootstrap_get_base_themes('my_subtheme');
*
* // After (including active theme).
* use Drupal\bootstrap\Bootstrap;
* $theme = Bootstrap::getTheme();
* $base_themes = array_keys($theme->getAncestry());
*
* // After (excluding active theme).
* use Drupal\bootstrap\Bootstrap;
* $my_subtheme = Bootstrap::getTheme('my_subtheme');
* $base_themes = array_keys($my_subtheme->getAncestry());
* array_pop($base_themes);
* @endcode
*
* @see \Drupal\bootstrap\Theme::getAncestry()
*/
function _bootstrap_get_base_themes($theme_key = NULL, $include_theme_key = FALSE) {
Bootstrap::deprecated();
$themes = array_keys(Bootstrap::getTheme($theme_key)->getAncestry());
if (!$include_theme_key) {
array_pop($themes);
}
return $themes;
}
/**
* Retrieves an element's "class" attribute array.
*
* @param array $element
* The individual renderable array element, passed by reference.
* @param string $property
* Determines which attributes array to retrieve. By default, this is the
* element's normal "attributes", but it could also be one of the following:
* - "content_attributes"
* - "input_group_attributes"
* - "title_attributes"
* - "wrapper_attributes".
*
* @return array
* The classes array.
*
* @deprecated Will be removed in a future release. There is no replacement.
*
* @code
* // Before.
* $classes = &_bootstrap_get_classes($element);
* $classes[] = 'my-class';
*
* // After.
* use Drupal\bootstrap\Utility\Element;
* $classes = &Element::create($element)->getClasses();
* $classes[] = 'my-class';
* @endcode
*
* @see \Drupal\bootstrap\Utility\Element::getClasses()
*/
function &_bootstrap_get_classes(array &$element, $property = 'attributes') {
Bootstrap::deprecated();
return Element::create($element)->getClasses($property);
}
/**
* Returns a specific Bootstrap Glyphicon as a render array.
*
* Note: This function was added in 7.x-3.17 to keep BC with the former
* _bootstrap_icon() implementation since it didn't return a render array. It
* is basically a backport of 8.x-3.x code so the added $attributes parameter
* can be more easily dealt with.
*
* @param string $name
* The icon name, minus the "glyphicon-" prefix.
* @param array|string $default
* (Optional) The default render array to use if $name is not available.
* @param array $attributes
* (Optional) Additional attributes to merge onto the icon.
*
* @return array
* The render containing the icon defined by $name, $default value if
* icon does not exist or returns NULL if no icon could be rendered.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $icon = _bootstrap_glyphicon($name, $default, $attributes);
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* use Drupal\bootstrap\Utility\Element;
* $icon = Bootstrap::glyphicon($name, ['#markup' => $default]);
* $icon_attributes = isset($icon['#attributes']) ? $icon['#attributes'] : [];
* unset($icon['#attributes']);
* $icon = Element::createStandalone($icon)->setAttributes($attributes)->setAttributes($icon_attributes)->getArray();
* @endcode
*
* @see https://www.drupal.org/project/bootstrap/issues/2844885
* @see \Drupal\bootstrap\Bootstrap::glyphicon()
* @see \Drupal\bootstrap\Utility\Element::createStandalone()
*/
function _bootstrap_glyphicon($name, $default = [], array $attributes = []) {
Bootstrap::deprecated();
$icon = Bootstrap::glyphicon($name, ['#markup' => $default]);
$icon_attributes = isset($icon['#attributes']) ? $icon['#attributes'] : [];
unset($icon['#attributes']);
return Element::createStandalone($icon)->setAttributes($attributes)->setAttributes($icon_attributes)->getArray();
}
/**
* Returns a list of available Bootstrap Glyphicons.
*
* @param string $version
* The specific version of glyphicons to return. If not set, the latest
* BOOTSTRAP_VERSION will be used.
*
* @return array
* An associative array of icons keyed by their classes.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $glyphicons = _bootstrap_glyphicons($version);
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* $glyphicons = Bootstrap::glyphicons($version);
* @endcode
*
* @see \Drupal\bootstrap\Bootstrap::glyphicons()
*/
function _bootstrap_glyphicons($version = NULL) {
Bootstrap::deprecated();
return Bootstrap::glyphicons($version);
}
/**
* Determine whether or not Bootstrap Glyphicons can be used.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $supported = _bootstrap_glyphicons_supported();
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* $theme = Bootstrap::getTheme();
* $supported = $theme->hasGlyphicons();
* @endcode
*
* @see \Drupal\bootstrap\Theme::hasGlyphicons()
*/
function _bootstrap_glyphicons_supported() {
Bootstrap::deprecated();
return Bootstrap::getTheme()->hasGlyphicons();
}
/**
* Returns a specific Bootstrap Glyphicon as rendered HTML markup.
*
* @param string $name
* The icon name, minus the "glyphicon-" prefix.
* @param string $default
* (Optional) The default value to return.
* @param array $attributes
* (Optional) Additional attributes to merge onto the icon.
*
* @return string
* The HTML markup containing the icon defined by $name, $default value if
* icon does not exist or returns empty output for whatever reason.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $icon = _bootstrap_icon($name, $default, $attributes);
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* use Drupal\bootstrap\Utility\Element;
* $icon = Bootstrap::glyphicon($name, ['#markup' => $default]);
* $icon_attributes = isset($icon['#attributes']) ? $icon['#attributes'] : [];
* unset($icon['#attributes']);
* $icon = (string) Element::createStandalone($icon)->setAttributes($attributes)->setAttributes($icon_attributes)->renderPlain();
* @endcode
*
* @see \Drupal\bootstrap\Bootstrap::glyphicon()
* @see \Drupal\bootstrap\Utility\Element::createStandalone()
*/
function _bootstrap_icon($name, $default = NULL, array $attributes = []) {
Bootstrap::deprecated();
$icon = Bootstrap::glyphicon($name, ['#markup' => $default]);
$icon_attributes = isset($icon['#attributes']) ? $icon['#attributes'] : [];
unset($icon['#attributes']);
return (string) Element::createStandalone($icon)->setAttributes($attributes)->setAttributes($icon_attributes)->renderPlain();
}
/**
* Adds an icon to button element based on its text value.
*
* @param array $element
* The form element, passed by reference.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* _bootstrap_iconize_button($element);
*
* // After.
* use Drupal\bootstrap\Utility\Element;
* Element::create($element)->setIcon();
* @endcode
*
* @see \Drupal\bootstrap\Utility\Element::setIcon()
*/
function _bootstrap_iconize_button(array &$element) {
Bootstrap::deprecated();
Element::create($element)->setIcon();
}
/**
* Matches a Bootstrap Glyphicon based on a string value.
*
* @param string $string
* The string to match classes against.
* @param string $default
* The default icon to return if no match is found.
*
* @return string
* The Bootstrap icon matched against the value of $haystack or $default if
* no match could be made.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $icon = _bootstrap_iconize_text($string, $default);
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* $icon = Bootstrap::glyphiconFromString($string, ['#markup' => $default]);
* @endcode
*
* @see \Drupal\bootstrap\Bootstrap::glyphiconFromString()
*/
function _bootstrap_iconize_text($string, $default = '') {
Bootstrap::deprecated();
return Bootstrap::glyphiconFromString($string, ['#markup' => $default]);
}
/**
* Determines if an element is a button.
*
* @param array $element
* A render array element.
*
* @return bool
* TRUE or FALSE.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $button = _bootstrap_is_button($element);
*
* // After.
* use Drupal\bootstrap\Utility\Element;
* $button = Element::create($element)->isButton();
* @endcode
*
* @see \Drupal\bootstrap\Utility\Element::isButton()
*/
function _bootstrap_is_button(array $element) {
Bootstrap::deprecated();
return Element::create($element)->isButton();
}
/**
* Determines if a string of text is considered "simple".
*
* @param string $string
* The string of text to check "simple" criteria on.
* @param int|false $length
* The length of characters used to determine whether or not $string is
* considered "simple". Set explicitly to FALSE to disable this criteria.
* @param array|false $allowed_tags
* An array of allowed tag elements. Set explicitly to FALSE to disable this
* criteria.
* @param bool $html
* A variable, passed by reference, that indicates whether or not the
* string contains HTML.
*
* @return bool
* Returns TRUE if the $string is considered "simple", FALSE otherwise.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $simple = _bootstrap_is_simple_string($string, $length, $allowed_tags, $html);
*
* // After.
* use Drupal\bootstrap\Utility\Unicode;
* $simple = Unicode::isSimple($string, $length, $allowed_tags, $html);
* @endcode
*
* @see \Drupal\bootstrap\Utility\Unicode::isSimple()
*/
function _bootstrap_is_simple_string($string, $length = 250, $allowed_tags = NULL, &$html = FALSE) {
Bootstrap::deprecated();
return Unicode::isSimple($string, $length, $allowed_tags, $html);
}
/**
* Removes a class from an element's attributes array.
*
* @param string|array $class
* An individual class or an array of classes to remove.
* @param array $element
* The individual renderable array element.
* @param string $property
* Determines which attributes array to retrieve. By default, this is the
* element's normal "attributes", but it could also be one of the following:
* - "content_attributes"
* - "input_group_attributes"
* - "title_attributes"
* - "wrapper_attributes".
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* _bootstrap_remove_class('my-class', $element);
*
* // After.
* use Drupal\bootstrap\Utility\Element;
* Element::create($element)->removeClass('my-class');
* @endcode
*
* @see \Drupal\bootstrap\Utility\Element::removeClass()
*/
function _bootstrap_remove_class($class, array &$element, $property = 'attributes') {
Bootstrap::deprecated();
Element::create($element)->removeClass($class, $property);
}
/**
* Retrieves a list of available CDN providers for the Bootstrap framework.
*
* @param string $provider
* A specific provider data to return.
* @param bool $reset
* Toggle determining whether or not to reset the database cache.
*
* @return array|false
* An associative array of CDN providers, keyed by their machine name if
* $provider is not set. If $provider is set and exists, it's individual
* data array will be returned. If $provider is set and the data does not
* exist then FALSE will be returned.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $providers = bootstrap_cdn_provider();
* $jsdelivr = bootstrap_cdn_provider('jsdelivr');
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* use Drupal\bootstrap\Plugin\ProviderManager;
*
* $theme = Bootstrap::getTheme();
*
* // Get provider definitions, the "equivalent" for bootstrap_cdn_provider().
* $provider_manager = new ProviderManager($theme);
* $providers = $provider_manager->getDefinitions();
* $jsdelivr = $provider_manager->getDefinition('jsdelivr');
*
* // You should, however, use the the fully initialized classes made
* // available through a theme instance.
* $providers = $theme->getProviders();
* $jsdelivr = $theme->getProvider('jsdelivr');
* @endcode
*
* @see \Drupal\bootstrap\Plugin\ProviderManager
* @see \Drupal\bootstrap\Theme::getCdnProviders()
* @see \Drupal\bootstrap\Theme::getCdnProvider()
*/
function bootstrap_cdn_provider($provider = NULL, $reset = FALSE) {
Bootstrap::deprecated();
$provider_manager = new ProviderManager(Bootstrap::getTheme());
if ($reset) {
$provider_manager->clearCachedDefinitions();
}
if (isset($provider)) {
if ($provider_manager->hasDefinition($provider)) {
return $provider_manager->getDefinition($provider);
}
return FALSE;
}
return $provider_manager->getDefinitions();
}
/**
* Converts an element description into a tooltip based on certain criteria.
*
* @param array $element
* An element render array, passed by reference.
* @param array $target
* The target element render array the tooltip is to be attached to, passed
* by reference. If not set, it will default to the $element passed.
* @param bool $input_only
* Toggle determining whether or not to only convert input elements.
* @param int $length
* The length of characters to determine if description is "simple".
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* bootstrap_element_smart_description($element, $target, $input_only, $length);
*
* // After.
* use Drupal\bootstrap\Utility\Element;
* Element::create($element)->smartDescription($target, $input_only, $length);
* @endcode
*
* @see \Drupal\bootstrap\Utility\Element::smartDescription()
*/
function bootstrap_element_smart_description(array &$element, array &$target = NULL, $input_only = TRUE, $length = NULL) {
Bootstrap::deprecated();
Element::create($element)->smartDescription($target, $input_only, $length);
}
/**
* Retrieves CDN assets for the active provider, if any.
*
* @param string|array $type
* The type of asset to retrieve: "css" or "js", defaults to an array
* array containing both if not set.
* @param string $provider
* The name of a specific CDN provider to use, defaults to the active provider
* set in the theme settings.
* @param string $theme
* The name of a specific theme the settings should be retrieved from,
* defaults to the active theme.
*
* @return array
* If $type is a string or an array with only one (1) item in it, the assets
* are returned as an indexed array of files. Otherwise, an associative array
* is returned keyed by the type.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* $assets = bootstrap_get_cdn_assets($type, $provider, $theme);
*
* // After.
* use Drupal\bootstrap\Plugin\ProviderManager;
* $original_type = $type;
* $config = \Drupal::config('system.performance');
* $cdnAssets = ProviderManager::load($theme, $provider)->getCdnAssets();
* $data = [];
* $types = !isset($type) ? ['css', 'js'] : (array) $type;
* foreach ($types as $type) {
* if ($config->get("$type.preprocess") && !empty($cdnAssets['min'][$type])) {
* $data[$type] = $cdnAssets['min'][$type];
* }
* elseif (!empty($data[$type])) {
* $data[$type] = $cdnAssets[$type];
* }
* }
* $assets = is_string($original_type) ? $data[$original_type] : $data;
* @endcode
*
* @see \Drupal\bootstrap\Plugin\Provider\Custom::getAssets()
* @see \Drupal\bootstrap\Plugin\Provider\JsDelivr::getAssets()
* @see \Drupal\bootstrap\Plugin\Provider\ProviderBase::getAssets()
* @see \Drupal\bootstrap\Plugin\Provider\ProviderInterface::getAssets()
* @see \Drupal\bootstrap\Plugin\ProviderManager::load()
*/
function bootstrap_get_cdn_assets($type = NULL, $provider = NULL, $theme = NULL) {
Bootstrap::deprecated();
$original_type = $type;
$return = [];
$config = \Drupal::config('system.performance');
$assets = ProviderManager::load($theme, $provider)->getCdnAssets();
$types = !isset($type) ? ['css', 'js'] : (array) $type;
foreach ($types as $type) {
$return[$type] = $assets->get($type, $config->get("$type.preprocess"));
}
return is_string($original_type) ? $return[$original_type] : $return;
}
/**
* Return information from the .info file of a theme (and possible base themes).
*
* @param string $theme_key
* The machine name of the theme.
* @param string $key
* The key name of the item to return from the .info file. This value can
* include "][" to automatically attempt to traverse any arrays.
* @param bool $base_themes
* Recursively search base themes, defaults to TRUE.
*
* @return string|array|false
* A string or array depending on the type of value and if a base theme also
* contains the same $key, FALSE if no $key is found.
*
* @deprecated Will be removed in a future release. There is no replacement.
*/
function bootstrap_get_theme_info($theme_key = NULL, $key = NULL, $base_themes = TRUE) {
Bootstrap::deprecated();
// If no $theme_key is given, use the current theme if we can determine it.
if (!isset($theme_key)) {
$theme_key = !empty($GLOBALS['theme_key']) ? $GLOBALS['theme_key'] : FALSE;
}
if ($theme_key) {
$themes = \Drupal::service('theme_handler')->listInfo();
if (!empty($themes[$theme_key])) {
$theme = $themes[$theme_key];
// If a key name was specified, return just that array.
if ($key) {
$value = FALSE;
// Recursively add base theme values.
if ($base_themes && isset($theme->base_themes)) {
foreach (array_keys($theme->base_themes) as $base_theme) {
$value = bootstrap_get_theme_info($base_theme, $key);
}
}
if (!empty($themes[$theme_key])) {
$info = $themes[$theme_key]->info;
// Allow array traversal.
$keys = explode('][', $key);
foreach ($keys as $parent) {
if (isset($info[$parent])) {
$info = $info[$parent];
}
else {
$info = FALSE;
}
}
if (is_array($value)) {
if (!empty($info)) {
if (!is_array($info)) {
$info = [$info];
}
$value = NestedArray::mergeDeep($value, $info);
}
}
else {
if (!empty($info)) {
if (empty($value)) {
$value = $info;
}
else {
if (!is_array($value)) {
$value = [$value];
}
if (!is_array($info)) {
$info = [$info];
}
$value = NestedArray::mergeDeep($value, $info);
}
}
}
}
return $value;
}
// If no info $key was specified, just return the entire info array.
return $theme->info;
}
}
return FALSE;
}
/**
* Includes a file from a theme.
*
* @param string $theme
* Name of the theme to use for base path.
* @param string $path
* Path relative to $theme.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before.
* bootstrap_include('my_subtheme', 'includes/file.inc');
* bootstrap_include('my_subtheme', 'some/other/path/file.inc');
*
* // After.
* use Drupal\bootstrap\Bootstrap;
* $my_subtheme = Bootstrap::getTheme('my_subtheme');
* $my_subtheme->includeOnce('file.inc');
* $my_subtheme->includeOnce('file.inc', 'some/other/path');
* @endcode
*
* @see \Drupal\bootstrap\Theme::includeOnce()
* @see \Drupal\bootstrap\Bootstrap::getTheme()
*/
function bootstrap_include($theme, $path) {
Bootstrap::deprecated();
$theme = Bootstrap::getTheme($theme);
$parts = explode('/', $path);
$file = array_pop($parts);
$dir = implode('/', $parts);
$theme->includeOnce($file, $dir);
}
/**
* Retrieves a setting for the current theme or for a given theme.
*
* @param string $name
* The name of the setting to be retrieved.
* @param string $theme
* The name of a given theme; defaults to the currently active theme.
* @param string $prefix
* The prefix used on the $name of the setting, this will be appended with
* "_" automatically if set.
* @param mixed $default
* The default value to return if setting doesn't exist or is not set.
*
* @return mixed
* The value of the requested setting, NULL if the setting does not exist.
*
* @deprecated Will be removed in a future release.
*
* @code
* // Before ("button_colorize" and "my_subtheme_custom_option").
* $colorize = bootstrap_setting('button_colorize', 'my_subtheme');
* $custom_option = bootstrap_setting('custom_option', 'my_subtheme', 'my_subtheme');
*
* // After ("button_colorize" and "my_subtheme_custom_option").
* use Drupal\bootstrap\Bootstrap;
* $my_subtheme = Bootstrap::getTheme('my_subtheme');
* $my_subtheme->getSetting('button_colorize');
* $my_subtheme->getSetting('my_subtheme_custom_option');
* @endcode
*
* @see \Drupal\bootstrap\Theme::getSetting()
* @see \Drupal\bootstrap\Bootstrap::getTheme()
*/
function bootstrap_setting($name, $theme = NULL, $prefix = 'bootstrap', $default = NULL) {
Bootstrap::deprecated();
$theme = Bootstrap::getTheme($theme);
$prefix = $prefix !== 'bootstrap' && !empty($prefix) ? $prefix . '_' : '';
return $theme->getSetting($prefix . $name, $default);
}

View File

@@ -0,0 +1,69 @@
<!-- @file Overview on how to contribute to the Drupal Bootstrap project. -->
<!-- @defgroup -->
# Contributing
Please read the [Getting Started](<!-- @url getting_started -->) topic and the
other various topics on this site first before creating an issue in this
project's issue queue.
Anything that falls within the scope of existing documentation or answered
questions will be marked as "Closed (works as designed)" or
"Closed (won't fix)".
The [Theme development](https://www.drupal.org/forum/3) support forum and
[Drupal StackExchange](https://drupal.stackexchange.com) are also amazing
resources for asking questions, learning new techniques and overall general
support.
## Drupal.org Handbook Pages
There has been some amazing Drupal Community work done around integration
between Drupal and the [Bootstrap Framework]. Additional community driven
documentation can be found on the [original Drupal.org handbook pages](https://www.drupal.org/node/1976938).
If you find that the documentation in these locations are inaccurate or
missing, please update it yourself (all logged in users have edit
capability).
## Creating New Issues
{.alert.alert-info} **Recommended reading:** [How To Solve All Your [Drupal] Problems](http://www.lullabot.com/blog/article/how-solve-all-your-problems)
{.alert.alert-warning} Please, [search the issue queue](https://www.drupal.org/project/issues/search/bootstrap)
first. **DO NOT** duplicate existing issues.
**If you find an existing issue and the issue status is:**
- Closed (fixed, duplicate, won't fix) - **DO NOT** re-open it. Open a new
issue (unless it's "Closed (won't fix)") and reference the existing issue in
the "Related Issues" field.
- Active, NR, NW, RTBC - Please update the issue accordingly, **DO NOT** create
a new issue.
**The [Drupal Bootstrap] issue queue IS for:**
- Fixing bugs and adding new features pertaining to the integration between
Drupal and the [Bootstrap Framework].
**The [Drupal Bootstrap] issue queue IS NOT for:**
- Bugs/feature requests pertaining to the [Bootstrap Framework] itself. Use
[their issue queue](https://github.com/twbs/bootstrap/issues) instead.
- Custom CSS/Layout (e.g. site specific)
- LESS/SASS - Compilation errors, syntax, mixins/functions
- JavaScript, jQuery, Bootstrap plugins or custom (site specific) plugins
- Modules that don't work in multiple themes. File the issue with that module.
It is likely they are not using [APIs](https://api.drupal.org) properly, not
following existing [Coding Standards](https://www.drupal.org/coding-standards)
or not developing with [Best Practices](https://www.drupal.org/best-practices) in
mind. It is actually a rare event when it is a legitimate issue with the
[Drupal Bootstrap] project.
## Slack
The [Drupal Bootstrap] project and its maintainers use the `#bootstrap` channel
in the `drupal.slack.com` workspace to communicate in real time. Please read
the following for more information on how to the community uses this technology:
[Chat with the Drupal community using Slack](https://www.drupal.org/slack).
Please keep in mind though, this **IS NOT** a "support" channel. It's primary
use is to discuss issues and to help fix bugs with the base theme itself.
[Drupal Bootstrap]: https://www.drupal.org/project/bootstrap
[Bootstrap Framework]: https://getbootstrap.com/docs/3.4/

View File

@@ -0,0 +1,127 @@
<!-- @file The "Getting Started" topic. -->
<!-- @defgroup -->
# Getting Started
## Installation
- Install the Bootstrap base theme in `themes` or a similar `sites/*/themes`
directory.
- Enable the [Drupal Bootstrap] base theme.
## Bootstrap Framework Fundamentals
Generally speaking, you should really read the entire [Bootstrap Framework]
documentation site, if you haven't already. Here are the four basic "sections"
that site is split into:
- [Getting Started](https://getbootstrap.com/docs/3.4/getting-started) - An overview of
the [Bootstrap Framework], how to download and use, basic templates and
examples, and more.
- [CSS](https://getbootstrap.com/docs/3.4/css/) - Global CSS settings, fundamental HTML
elements styled and enhanced with extensible classes, and an advanced grid
system.
- [Components](https://getbootstrap.com/docs/3.4/components/) - Over a dozen reusable
components built to provide iconography, dropdowns, input groups, navigation,
alerts, and much more.
- [JavaScript](https://getbootstrap.com/docs/3.4/javascript/) - Bring the
[Bootstrap Framework] components to life with over a dozen custom jQuery
plugins. Easily include them all, or one by one.
## FAQ - Frequently Asked Questions
- [Do you support X module?](#support)
- [Do you support Internet Explorer?](#ie)
- [Is Drupal Bootstrap a module or theme?](#module-or-theme)
- [Where can I discuss an issue in real time?](#discuss)
- [Where should I make changes?](#changes)
---
### Q: Do you support X module? {#support}
**A: Possibly**
Below are a list of modules the [Drupal Bootstrap] base theme actively supports.
This list is constantly growing and each module's support has usually been
implemented because of either extremely high usage or the fact it was designed
explicitly for use with this base theme and has maintainers in both projects.
**Supported modules:**
See project page for a list of supported modules.
**"Un-supported" modules:**
The following modules are "un-supported modules" and are not documented by the
[Drupal Bootstrap] base theme. This does not mean that the base theme will not
work with them or that they are "bad". It simply means that this project does
not have the time, energy or effort it would take to document "every possible
scenario".
It is certainly possible that some of these modules may eventually become
"officially" supported. That will happen only, of course, if there are enough
people to help contribute solid solutions and make supporting them by the base
theme maintainers a relatively "easy" task.
Some of these modules may have blogs or videos floating around on the internet.
However, if you choose to use one of these modules, you are really doing so
at your own expense. Do not expect support from this base theme or the project
you are attempting to integrate the base theme with.
- Color module (in core)
- [Bootstrap API](https://www.drupal.org/project/bootstrap_api)
- [Bootstrap Library](https://www.drupal.org/project/bootstrap_library)
- [LESS module](https://www.drupal.org/project/less)
---
### Q: Do you support Internet Explorer? {#ie}
**A: No, not "officially"**
The [Bootstrap Framework] itself does not officially support older Internet
Explorer [compatibility modes](https://getbootstrap.com/docs/3.4/getting-started/#support-ie-compatibility-modes).
To ensure you are using the latest rendering mode for IE, consider installing
the [HTML5 Tools](https://www.drupal.org/project/html5_tools) module.
Internet Explorer 8 requires the use of [Respond.js] to enable media queries
(Responsive Web Design). However, [Respond.js] does not work with CSS that is
referenced via a CSS `@import` statement, which is the default way Drupal
adds CSS files to a page when CSS aggregation is disabled. To ensure
[Respond.js] works properly, enable CSS aggregation at the bottom of:
`admin/config/development/performance`.
---
### Q: Is Drupal Bootstrap a module or theme? {#module-or-theme}
**A: Theme**
More specifically a base theme. It is _not_ a module. Modules are allowed to
participate in certain hooks, while themes cannot. This is a very important
concept to understand and limits themes from participating in a wider range of
functionality.
---
### Q: Where can I discuss an issue in real time? {#discuss}
**A: In Slack**
The [Drupal Bootstrap] project and its maintainers use the `#bootstrap` channel
in the `drupal.slack.com` workspace to communicate in real time. Please read
the following for more information on how to the community uses this technology:
[Chat with the Drupal community using Slack](https://www.drupal.org/slack).
Please keep in mind though, this **IS NOT** a "support" channel. It's primary
use is to discuss issues and to help fix bugs with the base theme itself.
---
### Q: Where should I make changes? {#changes}
**A: In a custom sub-theme**
You should **never** modify any theme or sub-theme that is packaged and released
from Drupal.org. If you do, all changes you have made would be lost once that
theme is updated. This makes keeping track of changes next to impossible.
Instead, you should create a custom sub-theme that isn't hosted on Drupal.org.
[Respond.js]: https://github.com/scottjehl/Respond
[Drush]: http://www.drush.org
[Drupal Bootstrap]: https://www.drupal.org/project/bootstrap
[Bootstrap Framework]: https://getbootstrap.com/docs/3.4/
[jQuery Update]: https://www.drupal.org/project/jquery_update

View File

@@ -0,0 +1,109 @@
<!-- @file Maintaining the Drupal Bootstrap project. -->
<!-- @defgroup -->
# Maintainers
Generally speaking, these topics will not be very helpful to you unless you are
a maintainer for this project. If you're simply curious about the process or
even want to help improve this aspect of the project, all suggestions will be
appreciated!
## Drupal Bootstrap Styles
The stylesheets bundled with this base theme (formerly known as "overrides")
have moved to a separate and dedicated project. Please file issues there:
https://github.com/unicorn-fail/drupal-bootstrap-styles
## Custom Scripts
This project also uses custom/standalone PHP scripts opposed to vendor specific
CLI programs (e.g. Drush or Drupal Console). This is primarily to ensure these
scripts can be executed regardless of which vendor specific CLI program or
version a maintainer may have installed.
### `./gen-theme-setting-docs.php`
Generates the markdown documentation for all available theme settings.
## Releases
This project attempts to provide more structured release notes. This allows the
project to communicate more effectively to the users what exactly has changed
and where to go for additional information. This documentation is intended for
the project maintainers to help provide consistent results between releases.
### Release notes template
The following is just a template to show a typical structured format used as
release notes for this project:
```html
<h3 id="change-records">Change Records</h3>
<!-- Change records table HTML -->
Optionally, you can insert any additional verbiage here.
However, if it is long, it should really be a change record.
<p>&nbsp;</p>
<h3 id="notes">Notes</h3>
<p>&nbsp;</p>
<p>Changes since <!-- previous release --> (<!-- commit count -->):</p>
<h3 id="security">Security Announcements</h3>
<ul>
<li><!-- Issue/Commit Message --></li>
</ul>
<h3 id="features">New Features</h3>
<ul>
<li><!-- Issue/Commit Message --></li>
</ul>
<h3 id="bugs">Bug Fixes</h3>
<ul>
<li><!-- Issue/Commit Message --></li>
</ul>
```
### Create a Release Node
{.alert.alert-info} **NOTE:** This project currently relies on the
[Drush Git Release Notes](https://www.drupal.org/project/grn) tool to
automatically generate the the bulk of the release notes. This does, however,
requires maintainers to do the following extra steps. This entire process will
eventually be converted into a fully automated grunt task. Until then, please
download and install this tool and follow the remaining steps.
1. Create a [tag in git](https://www.drupal.org/node/1066342) that follows the
previous version and push it to the repository.
2. Create a [project release node](https://www.drupal.org/node/1068944) for this
newly created tag.
3. _(Skip this step if this is a new "alpha/beta" release)_ In a separate tab,
go to this project's [releases](https://www.drupal.org/node/259843/release)
page. Open and edit the previous release node. It should have followed the
release note template. If it has, copy and paste its contents into the new
release node body.
4. In a separate tab, go to the [change records](https://www.drupal.org/list-changes/bootstrap)
for this project and filter by the new official release version
("alpha/beta/RC" releases should always use the next "official" version for
their change records). If there are no change records, then remove this
section. Otherwise, copy and paste the entire table into the template
(replacing any existing one, if necessary).
5. Generate a list of issues/commits by executing the following from the root
of the project:
`drush release-notes <old> <new> --commit-count`
(e.g. `drush release-notes 7.x-3.0 7.x-3.1 --commit-count`)
If this is a follow-up "alpha/beta/RC" release, always use the last
"alpha/beta/RC" release version instead. This will allow for a quicker
parsing of the list to merge into the previously copied release notes:
`drush release-notes <old> <new> --commit-count`
(e.g. `drush release-notes 7.x-3.1-beta2 7.x-3.1-beta3 --commit-count`)
6. Copy the entire generated output into the template, just under where the
"Change Records" section would be, replacing only the commit count (do not
replace the "since last {offical} version").
7. Go though each item (`<li>`) that contains an issue link, ignoring duplicates
and standalone verbiage (direct commits). Move (cut and paste) these items
into the appropriate "New Features" or "Bug Fixes" sections.
8. Once complete the generated list should be empty (e.g. `<ul></ul>`), remove
it.
9. Save the release node.

View File

@@ -0,0 +1,76 @@
<!-- @file Documentation landing page and topics for the https://drupal-bootstrap.org site. -->
<!-- @mainpage -->
# Drupal Bootstrap Documentation
{.lead} The official documentation site for the [Drupal Bootstrap] base theme
The majority of this site is automatically generated from source files
located through out the project's repository. Topics are extracted from Markdown
files and the rest is extracted from embedded PHP comments.
---
## Topics
Below are some topics to help get you started using the [Drupal Bootstrap] base
theme. They are ordered based on the level one typically progresses while using
a base theme like this.
#### [Contributing](<!-- @url contributing -->)
#### [Getting Started](<!-- @url getting_started -->)
#### [Theme Settings](<!-- @url theme_settings -->)
#### [Sub-Theming](<!-- @url sub_theming -->)
#### [Templates](<!-- @url templates -->)
#### [Utilities](<!-- @url utility -->)
#### [Plugin System](<!-- @url plugins -->)
- [@BootstrapAlter](<!-- @url plugins_alter -->)
- [@BootstrapForm](<!-- @url plugins_form -->)
- [@BootstrapPreprocess](<!-- @url plugins_preprocess -->)
- [@BootstrapPrerender](<!-- @url plugins_prerender -->)
- [@BootstrapProcess](<!-- @url plugins_process -->)
- [@BootstrapProvider](<!-- @url plugins_provider -->)
- [@BootstrapSetting](<!-- @url plugins_setting -->)
- [@BootstrapUpdate](<!-- @url plugins_update -->)
#### [Project Maintainers](<!-- @url maintainers -->)
---
## Terminology
The term **"bootstrap"** can be used excessively through out this project's
documentation. For clarity, we will always attempt to use this word verbosely
in one of the following ways:
- **[Drupal Bootstrap]** refers to the Drupal base theme project.
- **[Bootstrap Framework](https://getbootstrap.com/docs/3.4/)** refers to the
external front end framework.
- **[drupal_bootstrap](https://api.drupal.org/apis/drupal_bootstrap)** refers
to Drupal's bootstrapping process or phase.
When referring to files inside the [Drupal Bootstrap] project directory, they
will always start with `./themes/bootstrap` and continue to specify the full
path to the file or directory inside it. The dot (`.`) is representative of
your Drupal installation's `DOCROOT` folder. For example, the file that is
responsible for displaying the text on this page is located at
`./themes/bootstrap/docs/README.md`.
When referring to files inside a sub-theme, they will always start with
`./themes/THEMENAME/`, where `THEMENAME` is the machine name of your sub-theme.
They will continue to specify the full path to the file or directory inside it.
For example, the primary file Drupal uses to determine if a theme exists is:
`./themes/THEMENAME/THEMENAME.info.yml`.
{.alert.alert-info} **NOTE:** It is common practice to place projects found on
Drupal.org inside a sub-folder named `contrib` and custom/site-specific code
inside a `custom` folder. If your site is set up this way, please adjust all
paths accordingly (i.e. `./themes/contrib/bootstrap` and
`./themes/custom/THEMENAME`).
[Drupal Bootstrap]: https://www.drupal.org/project/bootstrap

View File

@@ -0,0 +1,153 @@
<!-- @file Instructions on how to sub-theme the Drupal Bootstrap base theme. -->
<!-- @defgroup sub_theming -->
# Sub-Theming
If you haven't already installed the Drupal Bootstrap theme, read the
[Getting Started](<!-- @url getting_started -->) topic. Below are instructions
on how to create a [Drupal Bootstrap] based sub-theme. There are several
different variations on how to accomplish this task, but this topic will focus
on the two primarily and most common ways.
You should never modify any theme or sub-theme that is packaged and released
from Drupal.org, such as Drupal Bootstrap. If you do, all changes you have made
will be lost once that theme is updated. Instead, you should create a sub-theme
from one of the provided starterkits (this is considered a best practice). Once
you've done that, you can override CSS, templates, and theme processing.
- [Using the Starterkit](#starterkit)
- [Using Source Files](#source)
- [LESS](#less)
- [SASS](#sass)
- [Compile](#compile)
- [Override Settings](#settings)
- [Override Templates](#templates)
## Using the Starterkit {#starterkit}
The starterkit provided by this base-theme supplies the basic file structure on
how to construct a proper Bootstrap based sub-theme for use with a [CDN
Provider] (like [jsDelivr]) or for use with compiling [Bootstrap Framework]
source files.
{.alert.alert-info} **NOTE:** Using a [CDN Provider] is the preferred method
for loading the [Bootstrap Framework] CSS and JS on simpler sites that do not
use a site-wide CDN. There are advantages and disadvantages to using a
[CDN Provider] and you will need to weigh the benefits based on your site's own
requirements. Using a [CDN Provider] does mean that it depends on a third-party
service. There is no obligation or commitment made by this project or these
third-party services that guarantees up-time or quality of service. If you need
to customize Bootstrap, you must compile the [Bootstrap Framework] source code
locally and disable the
[`cdn_provider` theme setting](<!-- @url theme_settings#cdn_provider -->).
{.alert.alert-warning} **WARNING:** All locally compiled versions of Bootstrap
will be superseded by any enabled [CDN Provider]; **do not use both**.
1. Copy `./themes/bootstrap/starterkits/THEMENAME` to `./themes`.
* Rename the `THEMENAME` directory to a unique machine readable name. This is
your sub-theme's "machine name". When referring to files inside a sub-theme,
they will always start with `./themes/THEMENAME/`, where `THEMENAME` is the
machine name of your sub-theme. They will continue to specify the full path
to the file or directory inside it. For example, the primary file Drupal
uses to determine if a theme exists is:
`./themes/THEMENAME/THEMENAME.info.yml`.
2. Rename `./themes/THEMENAME/THEMENAME.starterkit.yml` to match
`./themes/THEMENAME/THEMENAME.info.yml`.
* Open this file and change the name, description and any other properties
to suite your needs. Make sure to rename the library extension name as
well: `THEMENAME/framework`.
3. Rename `./themes/THEMENAME/THEMENAME.libraries.yml`.
* (Optional) If you plan on using a local precompiler (i.e., [Less] or
[Sass]) then uncomment the appropriate JavaScript entries inside this file
to enable the assets provided by the [Bootstrap Framework].
4. Rename `./themes/THEMENAME/THEMENAME.theme`.
5. Rename `./themes/THEMENAME/config/schema/THEMENAME.schema.yml`
* Open this file and rename `- THEMENAME.settings:` and `'THEMETITLE
settings'`
6. Rename `./themes/THEMENAME/config/install/THEMENAME.settings.yml`
* (Optional) If you plan on using a local precompiler (i.e., [Less] or
[Sass]) then you will need to disable the `cdn_provider`
[`cdn_provider` theme setting](<!-- @url theme_settings#cdn_provider -->).
You can do this several different ways, but it's recommended that you
uncomment the following line in this file so the [CDN Provider] is
automatically disabled when your sub-theme is installed:
```yaml
# Disable the CDN provider so compiled source files can be used.
cdn_provider: ''
```
{.alert.alert-warning} **WARNING:** Ensure that the `.starterkit` suffix is
not present on your sub-theme's `.info.yml` filename. This suffix is simply a
stop gap measure to ensure that the bundled starter kit sub-theme cannot be
enabled or used directly. This helps people unfamiliar with Drupal avoid
modifying the starter kit sub-theme directly and instead forces them to create
a new sub-theme to modify.
## Using Source Files {#source}
By default, the starterkit is designed to be used with a [CDN Provider] for
quick setup.
While there are a multitude of different approaches on how to actually compile
the [Bootstrap Framework] source files, this base-theme does not and will not
provide templates or suggest specific tools to use. It is up to you, the
developer, to figure out which solution is best for your particular needs.
### LESS {#less}
- You must understand the basic concept of using the [Less] CSS pre-processor.
- You must use a **[local Less compiler](https://www.google.com/search?q=less+compiler)**.
- You must use the latest `3.x.x` version of [Bootstrap Framework LESS Source
Files] ending in the `.less` extension, not files ending in `.css`.
- You must download a copy of [Drupal Bootstrap Styles] and copy over the `less`
folder located at `./drupal-bootstrap-styles/src/3.x.x/8.x-3.x/less`.
### SASS {#sass}
- You must understand the basic concept of using the [Sass] CSS pre-processor.
- You must use a **[local Sass compiler](https://www.google.com/search?q=sass+compiler)**.
- You must use the latest `3.x.x` version of [Bootstrap Framework SASS Source
Files] ending in the `.scss` extension, not files ending in `.css`.
- You must download a copy of [Drupal Bootstrap Styles] and copy over the `scss`
folder located at `./drupal-bootstrap-styles/src/3.x.x/8.x-3.x/scss`.
### Compile {#compile}
Download and extract the source files into the root of your new sub-theme:
`./themes/THEMENAME`. After it has been extracted, the directory should be
renamed (if needed) so it reads `./themes/THEMENAME/bootstrap`.
If for whatever reason you have an additional `bootstrap` directory wrapping
the first `bootstrap` directory (e.g. `./themes/THEMENAME/bootstrap/bootstrap`),
remove the wrapping `bootstrap` directory. You will only ever need to touch
these files if or when you upgrade your version of the [Bootstrap Framework].
{.alert.alert-warning} **WARNING:** Do not modify the files inside of
`./themes/THEMENAME/bootstrap` directly. Doing so may cause issues when
upgrading the [Bootstrap Framework] in the future.
Depending on which precompiler you chose, you should have a `less/style.less`
or `scss/style.scss` file respectively. This file is the main compiling entry
point. Follow further instructions provided by the `README.md` inside the
`less` or `scss` folder.
## Override Settings {#settings}
Please refer to the [Theme Settings](<!-- @url theme_settings -->) topic.
## Override Templates {#templates}
Please refer to the [Templates](<!-- @url templates -->) and
[Plugin System](<!-- @url plugins -->) topics.
## Enable Your New Sub-theme {#enable}
In your Drupal site, navigate to `admin/appearance` and click the `Enable and
set default` link next to your newly created sub-theme. Now that you've
enabled your starterkit, please refer to the starterkit's documentation page
to customize.
[Drupal Bootstrap](https://www.drupal.org/project/bootstrap)
[Drupal Bootstrap Styles](https://github.com/unicorn-fail/drupal-bootstrap-styles)
[Bootstrap Framework](https://getbootstrap.com/docs/3.4/)
[Bootstrap Framework LESS Source Files](https://github.com/twbs/bootstrap/releases)
[Bootstrap Framework SASS Source Files](https://github.com/twbs/bootstrap-sass)
[jsDelivr](http://www.jsdelivr.com)
[Less](http://lesscss.org)
[Sass](http://sass-lang.com)
[CDN Provider](<!-- @url plugins_provider -->)

View File

@@ -0,0 +1,5 @@
<!-- @file List of theme templates used in the Drupal Bootstrap base theme. -->
<!-- @defgroup -->
# Templates
List of theme templates used in the Drupal Bootstrap base theme.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
<!-- @file List of utility helper classes used in base theme. -->
<!-- @defgroup -->
# Utilities
List of theme utility helper classes used in the Drupal Bootstrap base theme.

View File

@@ -0,0 +1,137 @@
<!-- @file Documentation for the @BootstrapAlter annotated plugin. -->
<!-- @defgroup -->
<!-- @ingroup -->
# @BootstrapAlter
- [Pre-requisite](#prerequisite)
- [Supported alter hooks](#supported)
- [Form alter hooks](#form)
- [Create a plugin](#create)
- [Rebuild the cache](#rebuild)
---
## Pre-requisite {#prerequisite}
Due to the nature of how Drupal alter hooks work, there is no "catch all" alter
hook (like for forms with hook_form_alter). That means for you to use this
plugin, it must be invoked from inside each and every alter hook that lives in
`THEMENAME.theme`.
Luckily you don't have to worry about invoking the plugin directly. Instead,
all you have to do is call the `Bootstrap::alter` helper method and pass the
alter function name and parameters as arguments:
```php
<?php
use Drupal\bootstrap\Bootstrap;
/**
* Implements hook_HOOK_alter().
*/
function hook_some_hook_alter(&$data, &$context1 = NULL, &$context2 = NULL) {
Bootstrap::alter(__FUNCTION__, $data, $context1, $context2);
}
?>
```
## Supported alter hooks {#supported}
This base theme implements several of the most commonly used alter hooks in
themes and are automatically supported out-of-the-box.
Once a base theme has implemented an alter hook, like mentioned above, all
subsequent sub-themes will have the ability to implement a plugin for that
alter hook directly. All you have to do is simply create the plugin file in
`./themes/THEMENAME/src/Plugin/Alter`. No need to implement any code in
`THEMENAME.theme`:
- `hook_bootstrap_colorize_text_alter`
- `hook_bootstrap_iconize_text_alter`
- `hook_element_info_alter`
- `hook_js_settings_alter`
- `hook_library_info_alter`
- `hook_page_attachments_alter`
- `hook_theme_registry_alter`
- `hook_theme_suggestions_alter`
{.alert.alert-info}**Note:** if you do not see an alter hook here that you think
_should_ be here, please
[create an issue](https://www.drupal.org/node/add/project-issue/bootstrap)
## Form alter hooks {#form}
You were probably thinking: "Hey, where's `hook_form_alter`? Didn't you _just_
mention that above?"
As we all know, forms can be a tad more involved than just a simple "alter" and
we figured that we'd give you a little more power behind what you can actually
do with them. So if you're interested in those, please go see:
@link plugins_form @BootstrapForm @endlink
While, yes technically, `hook_form_system_theme_settings_alter` could also fall
under the form plugin, we decided to take those a step further as well, see:
@link plugins_setting @BootstrapSetting @endlink
## Create a plugin {#create}
We'll use `PageAttachments` implemented by this base theme as an example of
how to add a library from your sub-theme to every page request.
Replace all following instances of `THEMENAME` with the actual machine name of
your sub-theme.
Create a file at `./themes/THEMENAME/src/Plugin/Alter/PageAttachments.php` with the
following contents:
```php
<?php
/**
* @file
* Contains \Drupal\THEMENAME\Plugin\Alter\PageAttachments.
*/
namespace Drupal\THEMENAME\Plugin\Alter;
use Drupal\bootstrap\Plugin\Alter\PageAttachments as BootstrapPageAttachements;
/**
* Implements hook_page_attachments_alter().
*
* @ingroup plugins_alter
*
* @BootstrapAlter("page_attachments")
*/
class PageAttachments extends BootstrapPageAttachements {
/**
* {@inheritdoc}
*/
public function alter(&$attachments, &$context1 = NULL, &$context2 = NULL) {
// Call the parent method from the base theme, if applicable (which it is
// in this case because Bootstrap actually implements this alter).
parent::alter($attachments, $context1, $context2);
// Add your custom library.
$attachments['#attached']['library'][] = 'THEMENAME/my_library';
}
}
?>
```
## Rebuild the cache {#rebuild}
Once you have saved, you must rebuild your cache for this new plugin to be
discovered. This must happen anytime you make a change to the actual file name
or the information inside the `@BootstrapAlter` annotation.
To rebuild your cache, navigate to `admin/config/development/performance` and
click the `Clear all caches` button. Or if you prefer, run `drush cr` from the
command line.
Voilà! After this, you should have a fully functional `@BootstrapAlter` plugin!

View File

@@ -0,0 +1,110 @@
<!-- @file Documentation for the @BootstrapForm annotated discovery plugin. -->
<!-- @defgroup -->
<!-- @ingroup -->
# @BootstrapForm
- [Create a plugin](#create)
- [Rebuild the cache](#rebuild)
---
## Create a plugin {#create}
We'll use `SearchBlockForm` implemented by this base theme as an example of
how to remove `#input_group_button` from `search_block_form`.
Replace all following instances of `THEMENAME` with the actual machine name of
your sub-theme.
Create a file at `./themes/THEMENAME/src/Plugin/Form/SearchBlockForm.php` with the
following contents:
```php
<?php
namespace Drupal\THEMENAME\Plugin\Form;
use Drupal\bootstrap\Plugin\Form\SearchBlockForm as BootstrapSearchBlockForm;
use Drupal\bootstrap\Utility\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_FORM_ID_alter().
*
* @ingroup plugins_form
*
* @BootstrapForm("search_block_form")
*/
class SearchBlockForm extends BootstrapSearchBlockForm {
/**
* {@inheritdoc}
*/
public function alterForm(array &$form, FormStateInterface $form_state, $form_id = NULL) {
// Call the parent method from the base theme, if applicable (which it is
// in this case because Bootstrap actually implements this alter).
parent::alterForm($form, $form_state, $form_id);
// Disable #input_group_button the normal way:
$form['keys']['#input_group_button'] = FALSE;
}
/**
* {@inheritdoc}
*/
public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
// This method is the same as above, except the the $form argument passed is
// an instance of \Drupal\bootstrap\Utility\Element for easier manipulation.
// Using this method is preferable and considered "Best Practice".
//
// Disable #input_group_button using the $form Element object:
// $form->keys->setProperty('input_group_button', FALSE);.
}
/**
* {@inheritdoc}
*/
public static function submitForm(array &$form, FormStateInterface $form_state) {
// This method is automatically called when the form is submitted.
}
/**
* {@inheritdoc}
*/
public static function submitFormElement(Element $form, FormStateInterface $form_state) {
// This method is the same as above, except the the $form argument passed is
// an instance of \Drupal\bootstrap\Utility\Element for easier manipulation.
// Using this method is preferable and considered "Best Practice".
}
/**
* {@inheritdoc}
*/
public static function validateForm(array &$form, FormStateInterface $form_state) {
// This method is automatically called when the form is validated.
}
/**
* {@inheritdoc}
*/
public static function validateFormElement(Element $form, FormStateInterface $form_state) {
// This method is the same as above, except the the $form argument passed is
// an instance of \Drupal\bootstrap\Utility\Element for easier manipulation.
// Using this method is preferable and considered "Best Practice".
}
}
?>
```
## Rebuild the cache {#rebuild}
Once you have saved, you must rebuild your cache for this new plugin to be
discovered. This must happen anytime you make a change to the actual file name
or the information inside the `@BootstrapForm` annotation.
To rebuild your cache, navigate to `admin/config/development/performance` and
click the `Clear all caches` button. Or if you prefer, run `drush cr` from the
command line.
Voilà! After this, you should have a fully functional `@BootstrapForm` plugin!

View File

@@ -0,0 +1,118 @@
<!-- @file Documentation for the @BootstrapPreprocess annotated plugin. -->
<!-- @defgroup -->
<!-- @ingroup -->
# @BootstrapPreprocess
- [Create a plugin](#create)
- [Rebuild the cache](#rebuild)
---
## Create a plugin {#create}
We'll use `Page` implemented by this base theme as an example of how to add
custom classes for the `page.html.twig` template that should only be added
under certain conditions.
Replace all following instances of `THEMENAME` with the actual machine name of
your sub-theme.
Create a file at `./themes/THEMENAME/src/Plugin/Preprocess/Page.php` with the
following contents:
```php
<?php
namespace Drupal\THEMENAME\Plugin\Preprocess;
use Drupal\bootstrap\Plugin\Preprocess\Page as BootstrapPage;
use Drupal\bootstrap\Utility\Element;
use Drupal\bootstrap\Utility\Variables;
/**
* Pre-processes variables for the "page" theme hook.
*
* @ingroup plugins_preprocess
*
* @BootstrapPreprocess("page")
*/
class Page extends BootstrapPage {
/*
* It should be noted that you do not need all three methods here.
* This is to just show you the different examples of how this plugin
* works and how they can be tailored to your needs.
*/
/**
* {@inheritdoc}
*/
public function preprocess(array &$variables, $hook, array $info) {
$value = isset($variables['element']['child']['#value']) ? $variables['element']['child']['#value'] : FALSE;
if (_some_module_condition($value)) {
$variables['attributes']['class'][] = 'my-theme-class';
$variables['attributes']['class'][] = 'another-theme-class';
$key = array_search('page', $variables['attributes']['class']);
if ($key !== FALSE) {
unset($variables['attributes']['class'][$key]);
}
}
// If you are extending and overriding a preprocess method from the base
// theme, it is imperative that you also call the parent (base theme) method
// at some point in the process, typically after you have finished with your
// preprocessing.
parent::preprocess($variables, $hook, $info);
}
/**
* {@inheritdoc}
*/
public function preprocessVariables(Variables $variables) {
// This method is almost identical to the one above, but it introduces the
// Variables utility class in the base theme. This class has a plethora of
// helpful methods to quickly modify common tasks when you're in a
// preprocess function. It also acts like the normal $variables array when
// you need it to in instances of accessing nested content or in loop
// structures like foreach.
$value = isset($variables['element']['child']['#value']) ? $variables['element']['child']['#value'] : FALSE;
if (_some_module_condition($value)) {
$variables->addClass(['my-theme-class', 'another-theme-class'])->removeClass('page');
}
parent::preprocessVariables($variables);
}
/**
* {@inheritdoc}
*/
protected function preprocessElement(Element $element, Variables $variables) {
// This method is only ever invoked if either $variables['element'] or
// $variables['elements'] exists. These keys are usually only found in forms
// or render arrays when there is a #type being used. This introduces the
// Element utility class in the base theme. It too has a bucket-load of
// features, specific to the unique characteristics of render arrays with
// their "properties" (keys starting with #). This will quickly allow you to
// access some of the nested element data and reduce the overhead required
// for commonly used logic.
$value = $element->child->getProperty('value', FALSE);
if (_some_module_condition($value)) {
$variables->addClass(['my-theme-class', 'another-theme-class'])->removeClass('page');
}
parent::preprocessElement($element, $variables);
}
}
?>
```
## Rebuild the cache {#rebuild}
Once you have saved, you must rebuild your cache for this new plugin to be
discovered. This must happen anytime you make a change to the actual file name
or the information inside the `@BootstrapPreprocess` annotation.
To rebuild your cache, navigate to `admin/config/development/performance` and
click the `Clear all caches` button. Or if you prefer, run `drush cr` from the
command line.
Voilà! After this, you should have a fully functional `@BootstrapPreprocess`
plugin!

View File

@@ -0,0 +1,119 @@
<!-- @file Documentation for the @BootstrapPrerender annotated plugin. -->
<!-- @defgroup -->
<!-- @ingroup -->
# @BootstrapPrerender
- [Create a plugin](#create)
- [Rebuild the cache](#rebuild)
---
## Create a plugin {#create}
We'll use `Link` implemented by this base theme as an example of how to add
custom classes and an icon to make the link look like a Bootstrap button. This
example will only work if the link is passed some sort of `#context` when the
render array is built, like the following:
```php
<?php
$build['my_button'] = [
'#type' => 'link',
'#title' => t('Download'),
'#url' => Url::fromUserInput('/download', [
'query' => ['item' => '1234'],
]),
'#context' => [
'downloadButton' => TRUE,
],
];
?>
```
Replace all following instances of `THEMENAME` with the actual machine name of
your sub-theme.
Create a file at `./themes/THEMENAME/src/Plugin/Prerender/Link.php` with the
following contents:
```php
<?php
namespace Drupal\THEMENAME\Plugin\Prerender;
use Drupal\bootstrap\Plugin\Prerender\Link as BootstrapLink;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Utility\Element;
/**
* Pre-render callback for the "link" element type.
*
* @ingroup plugins_prerender
*
* @BootstrapPrerender("link",
* action = @BootstrapConstant(
* "\Drupal\bootstrap\Bootstrap::CALLBACK_PREPEND"
* )
* )
*
* @see \Drupal\Core\Render\Element\Link::preRenderLink()
*/
class Link extends BootstrapLink {
/*
* It should be noted that you do not need both methods here.
* This is to just show you the different examples of how this plugin
* works and how it can be tailored to your needs.
*/
/**
* {@inheritdoc}
*/
public static function preRender(array $element) {
$context = isset($element['#context']) ? $element['#context'] : [];
// Make downloadButton links into buttons.
if (!empty($context['downloadButton'])) {
$element['#icon'] = Bootstrap::glyphicon('download-alt');
$element['#attributes']['class'][] = 'btn';
$element['#attributes']['class'][] = 'btn-primary';
$element['#attributes']['class'][] = 'btn-lg';
}
// You must always return the element in this method, as well as call the
// parent method when sub-classing this method as it is used to invoke
// static::preRenderElement().
return parent::preRender($element);
}
/**
* {@inheritdoc}
*/
public static function preRenderElement(Element $element) {
// Make downloadButton links into buttons.
// Same as above, just a little cleaner with the Element utility class.
if ($element->getContext('downloadButton')) {
$element->addClass(['btn', 'btn-primary', 'btn-lg'])->setIcon(Bootstrap::glyphicon('download-alt'));
}
// You don't always have to call the parent method when sub-classing, but
// it is generally recommended that you do (otherwise the icon that was
// just added wouldn't work).
parent::preRenderElement($element);
}
}
?>
```
## Rebuild the cache {#rebuild}
Once you have saved, you must rebuild your cache for this new plugin to be
discovered. This must happen anytime you make a change to the actual file name
or the information inside the `@BootstrapPrerender` annotation.
To rebuild your cache, navigate to `admin/config/development/performance` and
click the `Clear all caches` button. Or if you prefer, run `drush cr` from the
command line.
Voilà! After this, you should have a fully functional `@BootstrapPrerender`
plugin!

View File

@@ -0,0 +1,99 @@
<!-- @file Documentation for the @BootstrapProcess annotated plugin. -->
<!-- @defgroup -->
<!-- @ingroup -->
# @BootstrapProcess
- [Create a plugin](#create)
- [Rebuild the cache](#rebuild)
---
## Create a plugin {#create}
{.alert.alert-warning}**Note:** This plugin is _not_ a re-implementation of the
D7 `hook_process_HOOK` for theme hooks in anyway. That layer was removed from
the theme system in D8 and for good reason (see:
[Remove the process layer](https://www.drupal.org/node/1843650)). This plugin
is about automatically adding a `#process` callback for a form element `#type`.
This is especially useful when dealing with core elements that have implemented
their own callbacks; either to alter their output or remove entirely.
We'll use `TextFormat` implemented by this base theme as an example of how to
override the class entirely and remove this base theme's over-simplification
for the "format tips" section.
Replace all following instances of `THEMENAME` with the actual machine name of
your sub-theme.
Create a file at `./themes/THEMENAME/src/Plugin/Process/TextFormat.php` with the
following contents:
```php
<?php
namespace Drupal\THEMENAME\Plugin\Process;
use Drupal\bootstrap\Plugin\Process\TextFormat as BootstrapTextFormat;
use Drupal\bootstrap\Utility\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Processes the "text_format" element.
*
* @ingroup plugins_process
*
* @BootstrapProcess("text_format")
*
* @see \Drupal\filter\Element\TextFormat::processFormat()
*/
class TextFormat extends BootstrapTextFormat {
/*
* It should be noted that you do not need both methods here.
* This is to just show you the different examples of how this plugin
* works and how it can be tailored to your needs.
*/
/**
* {@inheritdoc}
*/
public static function process(array $element, FormStateInterface $form_state, array &$complete_form) {
// You must return the element immediately if this is TRUE.
if (!empty($element['#bootstrap_ignore_process'])) {
return $element;
}
// Technically this isn't the method that we need to achieve our goal.
// But showing it just for example sake.
//
// You must always return the element in this method, as well as call the
// parent method when sub-classing this method as it is used to invoke
// static::processElement();
return parent::process($element, $form_state, $complete_form);
}
/**
* {@inheritdoc}
*/
public static function processElement(Element $element, FormStateInterface $form_state, array &$complete_form) {
// Normally, we'd call the parent method here. But this is actually an
// instance where we know we don't want to use the alterations made by
// the base theme. So we just comment it out and leave the method empty.
// parent::processElement($element, $form_state, $complete_form);.
}
}
?>
```
## Rebuild the cache {#rebuild}
Once you have saved, you must rebuild your cache for this new plugin to be
discovered. This must happen anytime you make a change to the actual file name
or the information inside the `@BootstrapProcess` annotation.
To rebuild your cache, navigate to `admin/config/development/performance` and
click the `Clear all caches` button. Or if you prefer, run `drush cr` from the
command line.
Voilà! After this, you should have a fully functional `@BootstrapProcess`
plugin!

View File

@@ -0,0 +1,90 @@
<!-- @file Documentation for the @BootstrapProvider annotated plugin. -->
<!-- @defgroup -->
<!-- @ingroup -->
# @BootstrapProvider
- [Create a plugin](#create)
- [Rebuild the cache](#rebuild)
---
## Create a plugin {#create}
We'll use the `\Drupal\bootstrap\Plugin\Provider\JsDelivr` CDN Provider as an
example of how to create a quick custom CDN provider using its API URLs.
Replace all following instances of `THEMENAME` with the actual machine name of
your sub-theme.
You may also feel free to replace the provided URLs with your own. Most of the
popular CDN API output can be easily parsed, however you may need to provide
addition parsing in your custom CDN Provider if you're not getting the desired
results.
If you're truly interested in implementing a CDN Provider, it is highly
recommended that you read the accompanying PHP based documentation on the
classes and methods responsible for actually retrieving, parsing and caching
the data from the CDN's API.
Create a file at `./themes/THEMENAME/src/Plugin/Provider/MyCdn.php` with the
following contents:
```php
<?php
namespace Drupal\THEMENAME\Plugin\Provider;
use Drupal\bootstrap\Plugin\Provider\ApiProviderBase;
/**
* The "mycdn" CDN Provider plugin.
*
* @ingroup plugins_provider
*
* @BootstrapProvider(
* id = "mycdn",
* label = @Translation("My CDN"),
* description = @Translation("My CDN (jsDelivr)"),
* weight = -1
* )
*/
class JsDelivr extends ApiProviderBase {
/**
* {@inheritdoc}
*/
protected function getApiAssetsUrlTemplate() {
return 'https://data.jsdelivr.com/v1/package/npm/@library@@version/flat';
}
/**
* {@inheritdoc}
*/
protected function getApiVersionsUrlTemplate() {
return 'https://data.jsdelivr.com/v1/package/npm/@library';
}
/**
* {@inheritdoc}
*/
protected function getCdnUrlTemplate() {
return 'https://cdn.jsdelivr.net/npm/@library@@version/@file';
}
}
?>
```
## Rebuild the cache {#rebuild}
Once you have saved, you must rebuild your cache for this new plugin to be
discovered. This must happen anytime you make a change to the actual file name
or the information inside the `@BootstrapProvider` annotation.
To rebuild your cache, navigate to `admin/config/development/performance` and
click the `Clear all caches` button. Or if you prefer, run `drush cr` from the
command line.
Voilà! After this, you should have a fully functional `@BootstrapProvider`
plugin!

View File

@@ -0,0 +1,70 @@
<!-- @file Documents the Plugin System for the Drupal Bootstrap base theme. -->
<!-- @defgroup -->
<!-- @ingroup -->
# Plugin System
- [Overview](#overview)
- [Helpful Tips](#helpful-tips)
---
## Overview {#overview}
The [Drupal Bootstrap] base theme handles some very complex theme registry
alterations and annotated plugin discoveries to assist with the organization
and maintenance of its source code.
By leveraging OOP (object oriented programming) with PHP namespacing and
Drupal's autoloading, we garner the ability to include files only when a
theme hook is actually invoked. This allows the base theme to reduce its per
page PHP memory footprint as much as possible. It also allows for easier
maintenance and organization with as much customization this base theme
implements.
The data and display logic of the [Drupal Bootstrap] base theme has been
divided into what we call the "Plugin System". It's nearly identical to the
other plugin system(s) found through out Drupal, with the exception that these
plugins are not bound to the container in any way.
This is, in part, due to the fact that themes are not allowed to participate in
container construction since a theme could vary from page to page (in theory).
So, instead, this base theme implements its own annotated discovery plugins
to leverage the powerful inheritance capabilities of PHP class instances.
All of these plugins can be found in the following directories and are
discussed, in length, below in their respective sub-topics:
- `./themes/bootstrap/src/Plugin/Alter`
- `./themes/bootstrap/src/Plugin/Form`
- `./themes/bootstrap/src/Plugin/Preprocess`
- `./themes/bootstrap/src/Plugin/Prerender`
- `./themes/bootstrap/src/Plugin/Process`
- `./themes/bootstrap/src/Plugin/Provider`
- `./themes/bootstrap/src/Plugin/Setting`
- `./themes/bootstrap/src/Plugin/Update`
While sub-themes are not required to do so, they can easily emulate this same
type of file structure/workflow and take advantage of this base theme's unique
ability and power. All you have to do is make sure you extend from this base
theme's implementation, if it exists.
Rest assured though, there is no need to structure your sub-theme this way. If
you feel more comfortable storing everything in your sub-theme's
`THEMENAME.theme` file and invoking the "normal" Drupal hooks, please feel free
to do so. It will not impact your sub-theme one way or the other.
It is, however, highly recommended that you at least read through this a bit to
gain some understanding on how this base theme structures its PHP and template
components. This will allow you to more easily copy stuff over to your
sub-theme, should the need arise.
## Helpful tips {#helpful-tips}
All plugins, except those that only have static methods, have the active Theme
object available to them: e.g. `$this->theme`. This will allow you to do things
like get a theme setting very, very easily: e.g.
`$this->theme->getSetting('button_size')`.
A helpful primer on Annotation-based plugins can be found at:
https://www.drupal.org/node/1882526
[Drupal Bootstrap]: https://www.drupal.org/project/bootstrap

View File

@@ -0,0 +1,162 @@
<!-- @file Documentation for the @BootstrapSetting annotated plugin. -->
<!-- @defgroup -->
<!-- @ingroup -->
# @BootstrapSetting
- [Create a plugin](#create)
- [Rebuild the cache](#rebuild)
- [Public Methods](#methods)
## Create a plugin {#create}
We will use `SkipLink` as our first `@BootstrapSetting` plugin to create. In
this example we want our sub-theme to specify a different skip link anchor id
to change in the Theme Settings interface altering the default of
`#main-content`.
Replace all of the following instances of `THEMENAME` with the actual machine
name of your sub-theme.
Create a file at
`./themes/THEMENAME/src/Plugin/Setting/THEMENAME/Accessibility/SkipLink.php`
with the following contents:
```php
<?php
namespace Drupal\THEMENAME\Plugin\Setting\THEMENAME\Accessibility;
use Drupal\bootstrap\Plugin\Setting\SettingBase;
/**
* The "THEMENAME_skip_link_id" theme setting.
*
* @ingroup plugins_setting
*
* @BootstrapSetting(
* id = "THEMENAME_skip_link_id",
* type = "textfield",
* title = @Translation("Anchor ID for the ""skip link"""),
* defaultValue = "main-content",
* description = @Translation("Specify the HTML ID of the element that the accessible-but-hidden ""skip link"" should link to. (<a href="":link"" target=""_blank"">Read more about skip links</a>.)",
* arguments = { ":link" = "https://www.drupal.org/node/467976" }),
* groups = {
* "THEMENAME" = "THEMETITLE",
* "accessibility" = @Translation("Accessibility"),
* },
* )
*/
class SkipLink extends SettingBase {}
?>
```
Helpfully Bootstrap adds a global `theme` variable added to every template
in `Bootstrap::preprocess()`.
This variable can now simply be called in the `html.html.twig` file with the
following contents:
```twig
<a href="#{{ theme.settings.THEMENAME_skip_link_id }}"
class="visually-hidden focusable skip-link">
{{ 'Skip to main content'|t }}
</a>
```
In addition, the `page.html.twig` file will also need to be adjusted for this to
work properly with the new anchor id.
```twig
<a id="{{ theme.settings.THEMENAME_skip_link_id }}"></a>
```
## Rebuild the cache {#rebuild}
Once you have saved, you must rebuild your cache for this new plugin to be
discovered. This must happen anytime you make a change to the actual file name
or the information inside the `@BootstrapSetting` annotation.
To rebuild your cache, navigate to `admin/config/development/performance` and
click the `Clear all caches` button. Or if you prefer, run `drush cr` from the
command line.
Voilà! After this, you should have a fully functional `@BootstrapSetting`
plugin!
## Public Methods {#methods}
Now that we covered how to create a basic `@BootstrapSetting` plugin, we can
discuss how to customize a setting to fulfill a range of requirements.
The `@BootstrapSetting` is implemented through the base class `SettingBase`
which provides a variety of public methods to assist in the customization of
a plugin.
#### SettingBase::alterForm(array &$form, FormStateInterface $form_state, $form_id = NULL)
#### SettingBase::alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL)
Both of these methods provide a way for you to alter the setting's form render
array element as well as the form state object.
The first method is similar to any standard `hook_form_alter`.
However, the second method passes the `$form` argument as an instance of the
`Element` utility helper class. This will allow easier manipulation of all the
elements in this method. Using this method is preferable and considered
"Best Practice".
Two useful examples to study:
- CDNProvider::alterFormElement
- RegionWells::alterFormElement
#### SettingBase::drupalSettings()
This method provides a way for you to determine whether a theme setting should
be added to the `drupalSettings` JavaScript variable. Please note that by
default this is set to `FALSE` to prevent any potentially sensitive information
from being leaked.
#### SettingBase::getCacheTags()
This method provides a way for you to add cache tags that when the instantiated
class is modified the associated cache tags will be invalidated. This is
incredibly useful for example with CDNCustomCss::getCacheTags() which returns an
array of `library_info`. So when a CdnProvider::getCacheTags() instantiated
plugin changes the `library_info` cache tag will be invalidated automatically.
It is important to note that the invalidation occurs because the base theme
loads external resources using libraries by altering the libraries it defines
based on settings in LibraryInfo::alter().
#### SettingBase::getGroupElement(Element $form, FormStateInterface $form_state)
This method provides a way for you to retrieve the last group (fieldset /
details form element) the setting is nested in; based on the plugin definition.
#### SettingBase::getGroups()
This method retrieves the associative array of groups; based on the plugin
definition. It's keyed by the group machine name and its value is the
translatable label.
#### SettingBase::getSettingElement(Element $form, FormStateInterface $form_state)
This method provides a way for you to retrieve the form element that was
automatically generated by the base theme for the setting; based on the plugin
definition.
#### SettingBase::submitForm(array &$form, FormStateInterface $form_state)
#### SettingBase::submitFormElement(Element $form, FormStateInterface $form_state)
Both of these methods provide a way for you to alter the submitted values
stored in the form state object before the setting's value is ultimately stored
in configuration by the base theme, which is performed automatically for you.
Two useful example to study:
- RegionWells::submitFormElement
#### SettingBase::validateForm(array &$form, FormStateInterface $form_state)
#### SettingBase::validateFormElement(Element $form, FormStateInterface $form_state)
Both of these methods provide a way for you to validate the setting's form.

View File

@@ -0,0 +1,9 @@
<!-- @file Documentation for the @BootstrapUpdate annotated plugin. -->
<!-- @defgroup -->
<!-- @ingroup -->
# @BootstrapUpdate
This plugin is a little too complex to explain (for now). If you would like to
help expand this documentation, please [create an issue](https://www.drupal.org/node/add/project-issue/bootstrap).
See the existing classes below on examples of how to implement your own.

View File

@@ -0,0 +1,59 @@
<!-- THEME SETTINGS GENERATION START -->
{% for heading, settings in groups %}
---
### {{ heading|raw }}
<table class="table table-striped table-responsive">
<thead>
<tr>
<th class="col-xs-3">{{ 'Setting name'|t }}</th>
<th>{{ 'Description and default value'|t }}</th>
</tr>
</thead>
<tbody>
{% for id, setting in settings %}<tr>
<td class="col-xs-3">
<span id="{{- id|clean_class -}}" data-anchor="true">{{- id -}}</span>
</td>
<td>
<div class="help-block">{{- setting.description -}}</div>
<pre class="language-yaml"><code>{{- setting.defaultValue -}}</code></pre>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
{% if deprecated %}
---
### {{ 'Deprecated'|t }}
<table class="table table-responsive">
<thead>
<tr>
<th class="col-xs-3">{{ 'Setting name'|t }}</th>
<th>{{ 'Description and default value'|t }}</th>
</tr>
</thead>
<tbody>
{% for id, setting in deprecated %}<tr class="bg-warning">
<td class="col-xs-3">
<span id="{{- id -}}" data-anchor="true">{{- id -}}</span>
</td>
<td>
<div class="help-block">{{- setting.description -}}</div>
<pre class="language-yaml"><code>{{- setting.defaultValue -}}</code></pre>
<div class="alert alert-danger alert-sm">
<strong>{{ 'Deprecated since @version'|t({'@version': setting.deprecated.version }) }}</strong> - {{ setting.deprecated.reason }} ({{ 'see: @replacement'|t({'@replacement': setting.deprecated.replacement}) }})
</div>
</td>
</tr>
{% endfor -%}
</tbody>
</table>
{% endif %}
<!-- THEME SETTINGS GENERATION END -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,42 @@
<?php
/**
* @file
* Icon API support to provide Bootstrap Framework glyphicons.
*/
use Drupal\bootstrap\Bootstrap;
/**
* Implements hook_icon_providers().
*/
function bootstrap_icon_providers() {
$providers['bootstrap'] = [
'title' => t('Bootstrap'),
'url' => 'https://getbootstrap.com/docs/3.4/components/#glyphicons',
];
return $providers;
}
/**
* Implements hook_icon_bundles().
*/
function bootstrap_icon_bundles() {
$bundles = [];
if (Bootstrap::getTheme()->hasGlyphicons()) {
$bundles['bootstrap'] = [
'render' => 'sprite',
'provider' => 'bootstrap',
'title' => t('Bootstrap'),
'version' => t('Icons by Glyphicons'),
'variations' => [
'icon-white' => t('White'),
],
'settings' => [
'tag' => 'span',
],
'icons' => Bootstrap::glyphicons(),
];
}
return $bundles;
}

View File

@@ -0,0 +1,380 @@
(function ($, _) {
/**
* @class Attributes
*
* Modifies attributes.
*
* @param {Object|Attributes} attributes
* An object to initialize attributes with.
*/
var Attributes = function (attributes) {
this.data = {};
this.data['class'] = [];
this.merge(attributes);
};
/**
* Renders the attributes object as a string to inject into an HTML element.
*
* @return {String}
* A rendered string suitable for inclusion in HTML markup.
*/
Attributes.prototype.toString = function () {
var output = '';
var name, value;
var checkPlain = function (str) {
return str && str.toString().replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;') || '';
};
var data = this.getData();
for (name in data) {
if (!data.hasOwnProperty(name)) continue;
value = data[name];
if (_.isFunction(value)) value = value();
if (_.isObject(value)) value = _.values(value);
if (_.isArray(value)) value = value.join(' ');
output += ' ' + checkPlain(name) + '="' + checkPlain(value) + '"';
}
return output;
};
/**
* Renders the Attributes object as a plain object.
*
* @return {Object}
* A plain object suitable for inclusion in DOM elements.
*/
Attributes.prototype.toPlainObject = function () {
var object = {};
var name, value;
var data = this.getData();
for (name in data) {
if (!data.hasOwnProperty(name)) continue;
value = data[name];
if (_.isFunction(value)) value = value();
if (_.isObject(value)) value = _.values(value);
if (_.isArray(value)) value = value.join(' ');
object[name] = value;
}
return object;
};
/**
* Add class(es) to the array.
*
* @param {string|Array} value
* An individual class or an array of classes to add.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.addClass = function (value) {
var args = Array.prototype.slice.call(arguments);
this.data['class'] = this.sanitizeClasses(this.data['class'].concat(args));
return this;
};
/**
* Returns whether the requested attribute exists.
*
* @param {string} name
* An attribute name to check.
*
* @return {boolean}
* TRUE or FALSE
*/
Attributes.prototype.exists = function (name) {
return this.data[name] !== void(0) && this.data[name] !== null;
};
/**
* Retrieve a specific attribute from the array.
*
* @param {string} name
* The specific attribute to retrieve.
* @param {*} defaultValue
* (optional) The default value to set if the attribute does not exist.
*
* @return {*}
* A specific attribute value, passed by reference.
*/
Attributes.prototype.get = function (name, defaultValue) {
if (!this.exists(name)) this.data[name] = defaultValue;
return this.data[name];
};
/**
* Retrieves a cloned copy of the internal attributes data object.
*
* @return {Object}
*/
Attributes.prototype.getData = function () {
return _.extend({}, this.data);
};
/**
* Retrieves classes from the array.
*
* @return {Array}
* The classes array.
*/
Attributes.prototype.getClasses = function () {
return this.get('class', []);
};
/**
* Indicates whether a class is present in the array.
*
* @param {string|Array} className
* The class(es) to search for.
*
* @return {boolean}
* TRUE or FALSE
*/
Attributes.prototype.hasClass = function (className) {
className = this.sanitizeClasses(Array.prototype.slice.call(arguments));
var classes = this.getClasses();
for (var i = 0, l = className.length; i < l; i++) {
// If one of the classes fails, immediately return false.
if (_.indexOf(classes, className[i]) === -1) {
return false;
}
}
return true;
};
/**
* Merges multiple values into the array.
*
* @param {Attributes|Node|jQuery|Object} object
* An Attributes object with existing data, a Node DOM element, a jQuery
* instance or a plain object where the key is the attribute name and the
* value is the attribute value.
* @param {boolean} [recursive]
* Flag determining whether or not to recursively merge key/value pairs.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.merge = function (object, recursive) {
// Immediately return if there is nothing to merge.
if (!object) {
return this;
}
// Get attributes from a jQuery element.
if (object instanceof $) {
object = object[0];
}
// Get attributes from a DOM element.
if (object instanceof Node) {
object = Array.prototype.slice.call(object.attributes).reduce(function (attributes, attribute) {
attributes[attribute.name] = attribute.value;
return attributes;
}, {});
}
// Get attributes from an Attributes instance.
else if (object instanceof Attributes) {
object = object.getData();
}
// Otherwise, clone the object.
else {
object = _.extend({}, object);
}
// By this point, there should be a valid plain object.
if (!$.isPlainObject(object)) {
setTimeout(function () {
throw new Error('Passed object is not supported: ' + object);
});
return this;
}
// Handle classes separately.
if (object && object['class'] !== void 0) {
this.addClass(object['class']);
delete object['class'];
}
if (recursive === void 0 || recursive) {
this.data = $.extend(true, {}, this.data, object);
}
else {
this.data = $.extend({}, this.data, object);
}
return this;
};
/**
* Removes an attribute from the array.
*
* @param {string} name
* The name of the attribute to remove.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.remove = function (name) {
if (this.exists(name)) delete this.data[name];
return this;
};
/**
* Removes a class from the attributes array.
*
* @param {...string|Array} className
* An individual class or an array of classes to remove.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.removeClass = function (className) {
var remove = this.sanitizeClasses(Array.prototype.slice.apply(arguments));
this.data['class'] = _.without(this.getClasses(), remove);
return this;
};
/**
* Replaces a class in the attributes array.
*
* @param {string} oldValue
* The old class to remove.
* @param {string} newValue
* The new class. It will not be added if the old class does not exist.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.replaceClass = function (oldValue, newValue) {
var classes = this.getClasses();
var i = _.indexOf(this.sanitizeClasses(oldValue), classes);
if (i >= 0) {
classes[i] = newValue;
this.set('class', classes);
}
return this;
};
/**
* Ensures classes are flattened into a single is an array and sanitized.
*
* @param {...String|Array} classes
* The class or classes to sanitize.
*
* @return {Array}
* A sanitized array of classes.
*/
Attributes.prototype.sanitizeClasses = function (classes) {
return _.chain(Array.prototype.slice.call(arguments))
// Flatten in case there's a mix of strings and arrays.
.flatten()
// Split classes that may have been added with a space as a separator.
.map(function (string) {
return string.split(' ');
})
// Flatten again since it was just split into arrays.
.flatten()
// Filter out empty items.
.filter()
// Clean the class to ensure it's a valid class name.
.map(function (value) {
return Attributes.cleanClass(value);
})
// Ensure classes are unique.
.uniq()
// Retrieve the final value.
.value();
};
/**
* Sets an attribute on the array.
*
* @param {string} name
* The name of the attribute to set.
* @param {*} value
* The value of the attribute to set.
*
* @return {Attributes}
*
* @chainable
*/
Attributes.prototype.set = function (name, value) {
var obj = $.isPlainObject(name) ? name : {};
if (typeof name === 'string') {
obj[name] = value;
}
return this.merge(obj);
};
/**
* Prepares a string for use as a CSS identifier (element, class, or ID name).
*
* Note: this is essentially a direct copy from
* \Drupal\Component\Utility\Html::cleanCssIdentifier
*
* @param {string} identifier
* The identifier to clean.
* @param {Object} [filter]
* An object of string replacements to use on the identifier.
*
* @return {string}
* The cleaned identifier.
*/
Attributes.cleanClass = function (identifier, filter) {
filter = filter || {
' ': '-',
'_': '-',
'/': '-',
'[': '-',
']': ''
};
identifier = identifier.toLowerCase();
if (filter['__'] === void 0) {
identifier = identifier.replace('__', '#DOUBLE_UNDERSCORE#');
}
identifier = identifier.replace(Object.keys(filter), Object.keys(filter).map(function(key) { return filter[key]; }));
if (filter['__'] === void 0) {
identifier = identifier.replace('#DOUBLE_UNDERSCORE#', '__');
}
identifier = identifier.replace(/[^\u002D\u0030-\u0039\u0041-\u005A\u005F\u0061-\u007A\u00A1-\uFFFF]/g, '');
identifier = identifier.replace(['/^[0-9]/', '/^(-[0-9])|^(--)/'], ['_', '__']);
return identifier;
};
/**
* Creates an Attributes instance.
*
* @param {object|Attributes} [attributes]
* An object to initialize attributes with.
*
* @return {Attributes}
* An Attributes instance.
*
* @constructor
*/
Attributes.create = function (attributes) {
return new Attributes(attributes);
};
window.Attributes = Attributes;
})(window.jQuery, window._);

View File

@@ -0,0 +1,172 @@
/**
* @file
* dialog.js
*/
(function ($, Drupal, Bootstrap, Attributes) {
Bootstrap.Dialog = Bootstrap.Dialog || {};
/**
* A collection of Drupal dialog handlers.
*
* @type {Object<String, Drupal.bootstrap.Dialog.Handler>}
*/
Bootstrap.Dialog.handlers = {};
/**
* @class Drupal.bootstrap.Dialog.Handler
*
* @param type
* @param data
*/
Bootstrap.Dialog.Handler = function (type, data) {
this.ctor = $.fn.modal;
this.extend = null;
this.plugin = 'modal';
this.prefix = 'modal';
this.themeHooks = {
modal: 'bootstrapModal',
dialog: 'bootstrapModalDialog',
header: 'bootstrapModalHeader',
title: 'bootstrapModalTitle',
close: 'bootstrapModalClose',
content: 'bootstrapModalContent',
body: 'bootstrapModalBody',
footer: 'bootstrapModalFooter',
};
this.type = type;
this.selectors = {
dialog: '.modal-dialog',
header: '.modal-header',
title: '.modal-title',
close: '.close',
content: '.modal-content',
body: '.modal-body',
footer: '.modal-footer',
buttons: '.modal-buttons'
};
// Extend the object with subclassed data.
$.extend(this, data);
// Extend the jQuery plugin.
if (this.extend) {
Bootstrap.extend(this.plugin, this.extend);
}
};
/**
* Retrieves a Drupal dialog type handler.
*
* @param {String|HTMLElement|jQuery} type
* The dialog type to retrieve.
*
* @return {Drupal.bootstrap.Dialog.Handler}
* A Bootstrap.Dialog.Handler instance.
*/
Bootstrap.Dialog.Handler.get = function (type) {
if (type instanceof $) {
type = type[0];
}
if (type instanceof HTMLElement) {
type = type.dialogType;
}
if (!type) {
type = 'modal';
}
if (!Bootstrap.Dialog.handlers[type]) {
Bootstrap.Dialog.handlers[type] = new Bootstrap.Dialog.Handler();
}
return Bootstrap.Dialog.handlers[type];
};
/**
* Registers a Drupal dialog type handler.
*
* @param {String} type
* The dialog type to
* @param {Object} [data]
* Optional. Additional data to use to create the dialog handler. By
* default, this assumes values relative to the Bootstrap Modal plugin.
*/
Bootstrap.Dialog.Handler.register = function (type, data) {
Bootstrap.Dialog.handlers[type] = new Bootstrap.Dialog.Handler(type, data);
};
Bootstrap.Dialog.Handler.prototype.invoke = function (context) {
var args = Array.prototype.slice.call(arguments);
return this.ctor.apply(context, args.slice(1));
};
Bootstrap.Dialog.Handler.prototype.theme = function (hook) {
var args = Array.prototype.slice.call(arguments);
return $(Drupal.theme.apply(Drupal.theme, [this.themeHooks[hook]].concat(args.slice(1))));
};
/**
* Ensures a DOM element has the appropriate structure for a modal.
*
* Note: this can get a little tricky. Core potentially already
* semi-processes a "dialog" if was created using an Ajax command
* (i.e. prepareDialogButtons in drupal.ajax.js). Because of this, the
* contents (HTML) of the existing element cannot simply be dumped into a
* newly created modal. This would destroy any existing event bindings.
* Instead, the contents must be "moved" (appended) to the new modal and
* then "moved" again back to the to the existing container as needed.
*
* @param {HTMLElement|jQuery} element
* The element to ensure is a modal structure.
* @param {Object} options
* THe dialog options to use to construct the modal.
*/
Bootstrap.Dialog.Handler.prototype.ensureModalStructure = function (element, options) {
var $element = $(element);
// Immediately return if the modal was already converted into a proper modal.
if ($element.is('[data-drupal-theme="' + this.themeHooks.modal + '"]')) {
return;
}
var attributes = Attributes.create(element).remove('style').set('data-drupal-theme', this.themeHooks.modal);
// Merge in trigger data attributes.
if (options.$trigger && options.$trigger[0]) {
/** @var {HTMLElement} trigger */
var trigger = options.$trigger[0];
var data = {};
for (var i = 0, l = trigger.attributes.length; i < l; i++) {
var name = trigger.attributes[i].name;
if (name && name.substring(0, 5) === 'data-') {
data[name] = trigger.getAttribute(name);
}
}
attributes.merge(data);
}
options = $.extend(true, {}, options, {
attributes: attributes,
});
// Create a new modal.
var $modal = this.theme('modal', options);
// Store a reference to the content inside the existing element container.
// This references the actual DOM node elements which will allow
// jQuery to "move" then when appending below. Using $.fn.children()
// does not return any text nodes present and $.fn.html() only returns
// a string representation of the content, which effectively destroys
// any prior event bindings or processing.
var $body = $element.find(this.selectors.body);
var $existing = $body[0] ? $body.contents() : $element.contents();
// Set the attributes of the dialog to that of the newly created modal.
$element.attr(Attributes.create($modal).toPlainObject());
// Append the newly created modal markup.
$element.append($modal.html());
// Move the existing HTML into the modal markup that was just appended.
$element.find(this.selectors.body).append($existing);
};
})(jQuery, Drupal, Drupal.bootstrap, Attributes);

View File

@@ -0,0 +1,76 @@
/**
* @file
* Provides an event handler for hidden elements in dropdown menus.
*/
(function ($, Drupal, Bootstrap) {
'use strict';
/**
* The list of supported events to proxy.
*
* @type {Array}
*/
var events = [
// MouseEvent.
'click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mouseup', 'mouseover', 'mousemove', 'mouseout',
// KeyboardEvent.
'keypress', 'keydown', 'keyup'
];
/**
* Bootstrap dropdown behaviors.
*
* Proxy any dropdown element events that should actually be fired on the
* original target (e.g. button, submits, etc.). This allows any registered
* event callbacks to be fired as they were intended (despite the fact that
* the markup has been changed to work with Bootstrap).
*
* @see \Drupal\bootstrap\Plugin\Preprocess\BootstrapDropdown::preprocessLinks
*
* @type {Drupal~behavior#bootstrapDropdown}
*/
Drupal.behaviors.bootstrapDropdown = {
attach: function (context) {
var elements = context.querySelectorAll('.dropdown [data-dropdown-target]');
for (var k in elements) {
if (!elements.hasOwnProperty(k)) {
continue;
}
var element = elements[k];
for (var i = 0, l = events.length; i < l; i++) {
var event = events[i];
element.removeEventListener(event, this.proxyEvent);
element.addEventListener(event, this.proxyEvent);
}
}
},
/**
* Proxy event handler for bootstrap dropdowns.
*
* @param {Event} e
* The event object.
*/
proxyEvent: function (e) {
// Ignore tabbing.
if (e.type.match(/^key/) && (e.which === 9 || e.keyCode === 9)) {
return;
}
var target = e.currentTarget.dataset && e.currentTarget.dataset.dropdownTarget || e.currentTarget.getAttribute('data-dropdown-target');
if (target) {
e.preventDefault();
e.stopPropagation();
var element = target && target !== '#' && document.querySelectorAll(target)[0];
if (element) {
Bootstrap.simulate(element, e.type, e);
}
else if (Bootstrap.settings.dev && window.console && !e.type.match(/^mouse/)) {
window.console.debug('[Drupal Bootstrap] Could not find a the target:', target);
}
}
}
}
})(jQuery, Drupal, Drupal.bootstrap);

View File

@@ -0,0 +1,600 @@
/**
* @file
* Drupal Bootstrap object.
*/
/**
* All Drupal Bootstrap JavaScript APIs are contained in this namespace.
*
* @param {underscore} _
* @param {jQuery} $
* @param {Drupal} Drupal
* @param {drupalSettings} drupalSettings
*/
(function (_, $, Drupal, drupalSettings) {
'use strict';
/**
* @typedef Drupal.bootstrap
*/
var Bootstrap = {
processedOnce: {},
settings: drupalSettings.bootstrap || {}
};
/**
* Wraps Drupal.checkPlain() to ensure value passed isn't empty.
*
* Encodes special characters in a plain-text string for display as HTML.
*
* @param {string} str
* The string to be encoded.
*
* @return {string}
* The encoded string.
*
* @ingroup sanitization
*/
Bootstrap.checkPlain = function (str) {
return str && Drupal.checkPlain(str) || '';
};
/**
* Creates a jQuery plugin.
*
* @param {String} id
* A jQuery plugin identifier located in $.fn.
* @param {Function} plugin
* A constructor function used to initialize the for the jQuery plugin.
* @param {Boolean} [noConflict]
* Flag indicating whether or not to create a ".noConflict()" helper method
* for the plugin.
*/
Bootstrap.createPlugin = function (id, plugin, noConflict) {
// Immediately return if plugin doesn't exist.
if ($.fn[id] !== void 0) {
return this.fatal('Specified jQuery plugin identifier already exists: @id. Use Drupal.bootstrap.replacePlugin() instead.', {'@id': id});
}
// Immediately return if plugin isn't a function.
if (typeof plugin !== 'function') {
return this.fatal('You must provide a constructor function to create a jQuery plugin "@id": @plugin', {'@id': id, '@plugin': plugin});
}
// Add a ".noConflict()" helper method.
this.pluginNoConflict(id, plugin, noConflict);
$.fn[id] = plugin;
};
/**
* Diff object properties.
*
* @param {...Object} objects
* Two or more objects. The first object will be used to return properties
* values.
*
* @return {Object}
* Returns the properties of the first passed object that are not present
* in all other passed objects.
*/
Bootstrap.diffObjects = function (objects) {
var args = Array.prototype.slice.call(arguments);
return _.pick(args[0], _.difference.apply(_, _.map(args, function (obj) {
return Object.keys(obj);
})));
};
/**
* Map of supported events by regular expression.
*
* @type {Object<Event|MouseEvent|KeyboardEvent|TouchEvent,RegExp>}
*/
Bootstrap.eventMap = {
Event: /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/,
MouseEvent: /^(?:click|dblclick|mouse(?:down|enter|leave|up|over|move|out))$/,
KeyboardEvent: /^(?:key(?:down|press|up))$/,
TouchEvent: /^(?:touch(?:start|end|move|cancel))$/
};
/**
* Extends a jQuery Plugin.
*
* @param {String} id
* A jQuery plugin identifier located in $.fn.
* @param {Function} callback
* A constructor function used to initialize the for the jQuery plugin.
*
* @return {Function|Boolean}
* The jQuery plugin constructor or FALSE if the plugin does not exist.
*/
Bootstrap.extendPlugin = function (id, callback) {
// Immediately return if plugin doesn't exist.
if (typeof $.fn[id] !== 'function') {
return this.fatal('Specified jQuery plugin identifier does not exist: @id', {'@id': id});
}
// Immediately return if callback isn't a function.
if (typeof callback !== 'function') {
return this.fatal('You must provide a callback function to extend the jQuery plugin "@id": @callback', {'@id': id, '@callback': callback});
}
// Determine existing plugin constructor.
var constructor = $.fn[id] && $.fn[id].Constructor || $.fn[id];
var plugin = callback.apply(constructor, [this.settings]);
if (!$.isPlainObject(plugin)) {
return this.fatal('Returned value from callback is not a plain object that can be used to extend the jQuery plugin "@id": @obj', {'@obj': plugin});
}
this.wrapPluginConstructor(constructor, plugin, true);
return $.fn[id];
};
Bootstrap.superWrapper = function (parent, fn) {
return function () {
var previousSuper = this.super;
this.super = parent;
var ret = fn.apply(this, arguments);
if (previousSuper) {
this.super = previousSuper;
}
else {
delete this.super;
}
return ret;
};
};
/**
* Provide a helper method for displaying when something is went wrong.
*
* @param {String} message
* The message to display.
* @param {Object} [args]
* An arguments to use in message.
*
* @return {Boolean}
* Always returns FALSE.
*/
Bootstrap.fatal = function (message, args) {
if (this.settings.dev && console.warn) {
for (var name in args) {
if (args.hasOwnProperty(name) && typeof args[name] === 'object') {
args[name] = JSON.stringify(args[name]);
}
}
Drupal.throwError(new Error(Drupal.formatString(message, args)));
}
return false;
};
/**
* Intersects object properties.
*
* @param {...Object} objects
* Two or more objects. The first object will be used to return properties
* values.
*
* @return {Object}
* Returns the properties of first passed object that intersects with all
* other passed objects.
*/
Bootstrap.intersectObjects = function (objects) {
var args = Array.prototype.slice.call(arguments);
return _.pick(args[0], _.intersection.apply(_, _.map(args, function (obj) {
return Object.keys(obj);
})));
};
/**
* Normalizes an object's values.
*
* @param {Object} obj
* The object to normalize.
*
* @return {Object}
* The normalized object.
*/
Bootstrap.normalizeObject = function (obj) {
if (!$.isPlainObject(obj)) {
return obj;
}
for (var k in obj) {
if (typeof obj[k] === 'string') {
if (obj[k] === 'true') {
obj[k] = true;
}
else if (obj[k] === 'false') {
obj[k] = false;
}
else if (obj[k].match(/^[\d-.]$/)) {
obj[k] = parseFloat(obj[k]);
}
}
else if ($.isPlainObject(obj[k])) {
obj[k] = Bootstrap.normalizeObject(obj[k]);
}
}
return obj;
};
/**
* An object based once plugin (similar to jquery.once, but without the DOM).
*
* @param {String} id
* A unique identifier.
* @param {Function} callback
* The callback to invoke if the identifier has not yet been seen.
*
* @return {Bootstrap}
*/
Bootstrap.once = function (id, callback) {
// Immediately return if identifier has already been processed.
if (this.processedOnce[id]) {
return this;
}
callback.call(this, this.settings);
this.processedOnce[id] = true;
return this;
};
/**
* Provide jQuery UI like ability to get/set options for Bootstrap plugins.
*
* @param {string|object} key
* A string value of the option to set, can be dot like to a nested key.
* An object of key/value pairs.
* @param {*} [value]
* (optional) A value to set for key.
*
* @returns {*}
* - Returns nothing if key is an object or both key and value parameters
* were provided to set an option.
* - Returns the a value for a specific setting if key was provided.
* - Returns an object of key/value pairs of all the options if no key or
* value parameter was provided.
*
* @see https://github.com/jquery/jquery-ui/blob/master/ui/widget.js
*/
Bootstrap.option = function (key, value) {
var options = $.isPlainObject(key) ? $.extend({}, key) : {};
// Get all options (clone so it doesn't reference the internal object).
if (arguments.length === 0) {
return $.extend({}, this.options);
}
// Get/set single option.
if (typeof key === "string") {
// Handle nested keys in dot notation.
// e.g., "foo.bar" => { foo: { bar: true } }
var parts = key.split('.');
key = parts.shift();
var obj = options;
if (parts.length) {
for (var i = 0; i < parts.length - 1; i++) {
obj[parts[i]] = obj[parts[i]] || {};
obj = obj[parts[i]];
}
key = parts.pop();
}
// Get.
if (arguments.length === 1) {
return obj[key] === void 0 ? null : obj[key];
}
// Set.
obj[key] = value;
}
// Set multiple options.
$.extend(true, this.options, options);
};
/**
* Adds a ".noConflict()" helper method if needed.
*
* @param {String} id
* A jQuery plugin identifier located in $.fn.
* @param {Function} plugin
* @param {Function} plugin
* A constructor function used to initialize the for the jQuery plugin.
* @param {Boolean} [noConflict]
* Flag indicating whether or not to create a ".noConflict()" helper method
* for the plugin.
*/
Bootstrap.pluginNoConflict = function (id, plugin, noConflict) {
if (plugin.noConflict === void 0 && (noConflict === void 0 || noConflict)) {
var old = $.fn[id];
plugin.noConflict = function () {
$.fn[id] = old;
return this;
};
}
};
/**
* Creates a handler that relays to another event name.
*
* @param {HTMLElement|jQuery} target
* A target element.
* @param {String} name
* The name of the event to trigger.
* @param {Boolean} [stopPropagation=true]
* Flag indicating whether to stop the propagation of the event, defaults
* to true.
*
* @return {Function}
* An even handler callback function.
*/
Bootstrap.relayEvent = function (target, name, stopPropagation) {
return function (e) {
if (stopPropagation === void 0 || stopPropagation) {
e.stopPropagation();
}
var $target = $(target);
var parts = name.split('.').filter(Boolean);
var type = parts.shift();
e.target = $target[0];
e.currentTarget = $target[0];
e.namespace = parts.join('.');
e.type = type;
$target.trigger(e);
};
};
/**
* Replaces a Bootstrap jQuery plugin definition.
*
* @param {String} id
* A jQuery plugin identifier located in $.fn.
* @param {Function} callback
* A callback function that is immediately invoked and must return a
* function that will be used as the plugin constructor.
* @param {Boolean} [noConflict]
* Flag indicating whether or not to create a ".noConflict()" helper method
* for the plugin.
*/
Bootstrap.replacePlugin = function (id, callback, noConflict) {
// Immediately return if plugin doesn't exist.
if (typeof $.fn[id] !== 'function') {
return this.fatal('Specified jQuery plugin identifier does not exist: @id', {'@id': id});
}
// Immediately return if callback isn't a function.
if (typeof callback !== 'function') {
return this.fatal('You must provide a valid callback function to replace a jQuery plugin: @callback', {'@callback': callback});
}
// Determine existing plugin constructor.
var constructor = $.fn[id] && $.fn[id].Constructor || $.fn[id];
var plugin = callback.apply(constructor, [this.settings]);
// Immediately return if plugin isn't a function.
if (typeof plugin !== 'function') {
return this.fatal('Returned value from callback is not a usable function to replace a jQuery plugin "@id": @plugin', {'@id': id, '@plugin': plugin});
}
this.wrapPluginConstructor(constructor, plugin);
// Add a ".noConflict()" helper method.
this.pluginNoConflict(id, plugin, noConflict);
$.fn[id] = plugin;
};
/**
* Simulates a native event on an element in the browser.
*
* Note: This is a fairly complete modern implementation. If things aren't
* working quite the way you intend (in older browsers), you may wish to use
* the jQuery.simulate plugin. If it's available, this method will defer to
* that plugin.
*
* @see https://github.com/jquery/jquery-simulate
*
* @param {HTMLElement|jQuery} element
* A DOM element to dispatch event on. Note: this may be a jQuery object,
* however be aware that this will trigger the same event for each element
* inside the jQuery collection; use with caution.
* @param {String|String[]} type
* The type(s) of event to simulate.
* @param {Object} [options]
* An object of options to pass to the event constructor. Typically, if
* an event is being proxied, you should just pass the original event
* object here. This allows, if the browser supports it, to be a truly
* simulated event.
*
* @return {Boolean}
* The return value is false if event is cancelable and at least one of the
* event handlers which handled this event called Event.preventDefault().
* Otherwise it returns true.
*/
Bootstrap.simulate = function (element, type, options) {
// Handle jQuery object wrappers so it triggers on each element.
var ret = true;
if (element instanceof $) {
element.each(function () {
if (!Bootstrap.simulate(this, type, options)) {
ret = false;
}
});
return ret;
}
if (!(element instanceof HTMLElement)) {
this.fatal('Passed element must be an instance of HTMLElement, got "@type" instead.', {
'@type': typeof element,
});
}
// Defer to the jQuery.simulate plugin, if it's available.
if (typeof $.simulate === 'function') {
new $.simulate(element, type, options);
return true;
}
var event;
var ctor;
var types = [].concat(type);
for (var i = 0, l = types.length; i < l; i++) {
type = types[i];
for (var name in this.eventMap) {
if (this.eventMap[name].test(type)) {
ctor = name;
break;
}
}
if (!ctor) {
throw new SyntaxError('Only rudimentary HTMLEvents, KeyboardEvents and MouseEvents are supported: ' + type);
}
var opts = {bubbles: true, cancelable: true};
if (ctor === 'KeyboardEvent' || ctor === 'MouseEvent') {
$.extend(opts, {ctrlKey: !1, altKey: !1, shiftKey: !1, metaKey: !1});
}
if (ctor === 'MouseEvent') {
$.extend(opts, {button: 0, pointerX: 0, pointerY: 0, view: window});
}
if (options) {
$.extend(opts, options);
}
if (typeof window[ctor] === 'function') {
event = new window[ctor](type, opts);
if (!element.dispatchEvent(event)) {
ret = false;
}
}
else if (document.createEvent) {
event = document.createEvent(ctor);
event.initEvent(type, opts.bubbles, opts.cancelable);
if (!element.dispatchEvent(event)) {
ret = false;
}
}
else if (typeof element.fireEvent === 'function') {
event = $.extend(document.createEventObject(), opts);
if (!element.fireEvent('on' + type, event)) {
ret = false;
}
}
else if (typeof element[type]) {
element[type]();
}
}
return ret;
};
/**
* Strips HTML and returns just text.
*
* @param {String|Element|jQuery} html
* A string of HTML content, an Element DOM object or a jQuery object.
*
* @return {String}
* The text without HTML tags.
*
* @todo Replace with http://locutus.io/php/strings/strip_tags/
*/
Bootstrap.stripHtml = function (html) {
if (html instanceof $) {
html = html.html();
}
else if (html instanceof Element) {
html = html.innerHTML;
}
var tmp = document.createElement('DIV');
tmp.innerHTML = html;
return (tmp.textContent || tmp.innerText || '').replace(/^[\s\n\t]*|[\s\n\t]*$/, '');
};
/**
* Provide a helper method for displaying when something is unsupported.
*
* @param {String} type
* The type of unsupported object, e.g. method or option.
* @param {String} name
* The name of the unsupported object.
* @param {*} [value]
* The value of the unsupported object.
*/
Bootstrap.unsupported = function (type, name, value) {
Bootstrap.warn('Unsupported by Drupal Bootstrap: (@type) @name -> @value', {
'@type': type,
'@name': name,
'@value': typeof value === 'object' ? JSON.stringify(value) : value
});
};
/**
* Provide a helper method to display a warning.
*
* @param {String} message
* The message to display.
* @param {Object} [args]
* Arguments to use as replacements in Drupal.formatString.
*/
Bootstrap.warn = function (message, args) {
if (this.settings.dev && console.warn) {
console.warn(Drupal.formatString(message, args));
}
};
/**
* Wraps a plugin with common functionality.
*
* @param {Function} constructor
* A plugin constructor being wrapped.
* @param {Object|Function} plugin
* The plugin being wrapped.
* @param {Boolean} [extend = false]
* Whether to add super extensibility.
*/
Bootstrap.wrapPluginConstructor = function (constructor, plugin, extend) {
var proto = constructor.prototype;
// Add a jQuery UI like option getter/setter method.
var option = this.option;
if (proto.option === void(0)) {
proto.option = function () {
return option.apply(this, arguments);
};
}
if (extend) {
// Handle prototype properties separately.
if (plugin.prototype !== void 0) {
for (var key in plugin.prototype) {
if (!plugin.prototype.hasOwnProperty(key)) continue;
var value = plugin.prototype[key];
if (typeof value === 'function') {
proto[key] = this.superWrapper(proto[key] || function () {}, value);
}
else {
proto[key] = $.isPlainObject(value) ? $.extend(true, {}, proto[key], value) : value;
}
}
}
delete plugin.prototype;
// Handle static properties.
for (key in plugin) {
if (!plugin.hasOwnProperty(key)) continue;
value = plugin[key];
if (typeof value === 'function') {
constructor[key] = this.superWrapper(constructor[key] || function () {}, value);
}
else {
constructor[key] = $.isPlainObject(value) ? $.extend(true, {}, constructor[key], value) : value;
}
}
}
};
// Add Bootstrap to the global Drupal object.
Drupal.bootstrap = Drupal.bootstrap || Bootstrap;
})(window._, window.jQuery, window.Drupal, window.drupalSettings);

View File

@@ -0,0 +1,131 @@
/**
* @file
* Extends methods from core/misc/ajax.js.
*/
(function ($, window, Drupal, drupalSettings) {
/**
* Attempts to find the closest glyphicon progress indicator.
*
* @param {jQuery|Element} element
* A DOM element.
*
* @returns {jQuery}
* A jQuery object.
*/
Drupal.Ajax.prototype.findGlyphicon = function (element) {
return $(element).closest('.form-item').find('.ajax-progress.glyphicon')
};
/**
* Starts the spinning of the glyphicon progress indicator.
*
* @param {jQuery|Element} element
* A DOM element.
* @param {string} [message]
* An optional message to display (tooltip) for the progress.
*
* @returns {jQuery}
* A jQuery object.
*/
Drupal.Ajax.prototype.glyphiconStart = function (element, message) {
var $glyphicon = this.findGlyphicon(element);
if ($glyphicon[0]) {
$glyphicon.addClass('glyphicon-spin');
// Add any message as a tooltip to the glyphicon.
if ($.fn.tooltip && drupalSettings.bootstrap.tooltip_enabled) {
$glyphicon
.removeAttr('data-toggle')
.removeAttr('data-original-title')
.removeAttr('title')
.tooltip('destroy')
;
if (message) {
$glyphicon.attr('data-toggle', 'tooltip').attr('title', message).tooltip();
}
}
// Append a message for screen readers.
if (message) {
$glyphicon.parent().append('<div class="sr-only message">' + message + '</div>');
}
}
return $glyphicon;
};
/**
* Stop the spinning of a glyphicon progress indicator.
*
* @param {jQuery|Element} element
* A DOM element.
*/
Drupal.Ajax.prototype.glyphiconStop = function (element) {
var $glyphicon = this.findGlyphicon(element);
if ($glyphicon[0]) {
$glyphicon.removeClass('glyphicon-spin');
if ($.fn.tooltip && drupalSettings.bootstrap.tooltip_enabled) {
$glyphicon
.removeAttr('data-toggle')
.removeAttr('data-original-title')
.removeAttr('title')
.tooltip('destroy')
;
}
}
};
/**
* Sets the throbber progress indicator.
*/
Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () {
var $element = $(this.element);
// Find an existing glyphicon progress indicator.
var $glyphicon = this.glyphiconStart($element, this.progress.message);
if ($glyphicon[0]) {
this.progress.element = $glyphicon.parent();
this.progress.glyphicon = true;
return;
}
// Otherwise, add a glyphicon throbber after the element.
if (!this.progress.element) {
this.progress.element = $(Drupal.theme('ajaxThrobber'));
}
if (this.progress.message) {
this.progress.element.after('<div class="message">' + this.progress.message + '</div>');
}
// If element is an input DOM element type (not :input), append after.
if ($element.is('input') || $element.is('select')) {
$element.after(this.progress.element);
}
// Otherwise append the throbber inside the element.
else {
$element.append(this.progress.element);
}
};
/**
* Handler for the form redirection completion.
*
* @param {Array.<Drupal.AjaxCommands~commandDefinition>} response
* @param {number} status
*/
var success = Drupal.Ajax.prototype.success;
Drupal.Ajax.prototype.success = function (response, status) {
if (this.progress.element) {
// Remove any message set.
this.progress.element.parent().find('.message').remove();
}
// Invoke the original success handler.
return success.apply(this, [response, status]);
};
})(jQuery, this, Drupal, drupalSettings);

View File

@@ -0,0 +1,36 @@
/**
* @file
* Extends autocomplete based on jQuery UI.
*
* @todo Remove once jQuery UI is no longer used?
*/
(function ($, Drupal) {
'use strict';
// Ensure the input element has a "change" event triggered. This is important
// so that summaries in vertical tabs can be updated properly.
// @see Drupal.behaviors.formUpdated
$(document).on('autocompleteselect', '.form-autocomplete', function (e) {
$(e.target).trigger('change.formUpdated');
});
// Extend ui.autocomplete widget so it triggers the glyphicon throbber.
$.widget('ui.autocomplete', $.ui.autocomplete, {
_search: function (value) {
this.pending++;
Drupal.Ajax.prototype.glyphiconStart(this.element);
this.cancelSearch = false;
this.source({term: value}, this._response());
},
_response: function () {
var index = ++this.requestIndex;
return $.proxy(function (content) {
if (index === this.requestIndex) this.__response(content);
this.pending--;
if (!this.pending) Drupal.Ajax.prototype.glyphiconStop(this.element);
}, this);
}
});
})(jQuery, Drupal);

View File

@@ -0,0 +1,43 @@
/**
* @file
* Drupal's batch API.
*/
(function ($, Drupal, once) {
'use strict';
/**
* Attaches the batch behavior to progress bars.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.batch = {
attach: function (context, settings) {
var batch = settings.batch;
var $progress = $(once('batch', '[data-drupal-progress]', context));
var progressBar;
// Success: redirect to the summary.
function updateCallback(progress, status, pb) {
if (progress === '100') {
pb.stopMonitoring();
window.location = batch.uri + '&op=finished';
}
}
function errorCallback(pb) {
$progress.prepend($('<p class="error"></p>').html(batch.errorMessage));
$('#wait').hide();
}
if ($progress.length) {
var id = $progress.find('.progress').attr('id') || 'updateprogress';
progressBar = new Drupal.ProgressBar(id, updateCallback, 'POST', errorCallback);
$progress.replaceWith(progressBar.element);
progressBar.setProgress(-1, batch.initMessage);
progressBar.startMonitoring(batch.uri + '&op=do', 10);
}
}
};
})(jQuery, Drupal, once);

View File

@@ -0,0 +1,115 @@
/**
* @file
* dialog.ajax.js
*/
(function ($, Drupal, Bootstrap) {
Drupal.behaviors.dialog.ajaxCurrentButton = null;
Drupal.behaviors.dialog.ajaxOriginalButton = null;
// Intercept the success event to add the dialog type to commands.
var success = Drupal.Ajax.prototype.success;
Drupal.Ajax.prototype.success = function (response, status) {
if (this.dialogType) {
for (var i = 0, l = response.length; i < l; i++) {
if (response[i].dialogOptions) {
response[i].dialogType = response[i].dialogOptions.dialogType = this.dialogType;
response[i].$trigger = response[i].dialogOptions.$trigger = $(this.element);
}
}
}
return success.apply(this, [response, status]);
};
var beforeSerialize = Drupal.Ajax.prototype.beforeSerialize;
Drupal.Ajax.prototype.beforeSerialize = function (element, options) {
// Add the dialog type currently in use.
if (this.dialogType) {
options.data['ajax_page_state[dialogType]'] = this.dialogType;
// Add the dialog element ID if it can be found (useful for closing it).
var id = $(this.element).parents('.js-drupal-dialog:first').attr('id');
if (id) {
options.data['ajax_page_state[dialogId]'] = id;
}
}
return beforeSerialize.apply(this, arguments);
};
/**
* Synchronizes a faux button with its original counterpart.
*
* @param {Boolean} [reset = false]
* Whether to reset the current and original buttons after synchronizing.
*/
Drupal.behaviors.dialog.ajaxUpdateButtons = function (reset) {
if (this.ajaxCurrentButton && this.ajaxOriginalButton) {
this.ajaxCurrentButton.html(this.ajaxOriginalButton.html() || this.ajaxOriginalButton.attr('value'));
this.ajaxCurrentButton.prop('disabled', this.ajaxOriginalButton.prop('disabled'));
}
if (reset) {
this.ajaxCurrentButton = null;
this.ajaxOriginalButton = null;
}
};
$(document)
.ajaxSend(function () {
Drupal.behaviors.dialog.ajaxUpdateButtons();
})
.ajaxComplete(function () {
Drupal.behaviors.dialog.ajaxUpdateButtons(true);
})
;
/**
* {@inheritdoc}
*/
Drupal.behaviors.dialog.prepareDialogButtons = function prepareDialogButtons($dialog) {
var _this = this;
var buttons = [];
var $buttons = $dialog.find('.form-actions').find('button, input[type=submit], a.button, .btn');
$buttons.each(function () {
var $originalButton = $(this)
// Prevent original button from being tabbed to.
.attr('tabindex', -1)
// Visually make the original button invisible, but don't actually hide
// or remove it from the DOM because the click needs to be proxied from
// the faux button created in the footer to its original counterpart.
.css({
display: 'block',
width: 0,
height: 0,
padding: 0,
border: 0,
overflow: 'hidden'
});
buttons.push({
// Strip all HTML from the actual text value. This value is escaped.
// It actual HTML value will be synced with the original button's HTML
// below in the "create" method.
text: Bootstrap.stripHtml($originalButton) || $originalButton.attr('value'),
class: $originalButton.attr('class').replace('use-ajax-submit', ''),
click: function click(e) {
e.preventDefault();
e.stopPropagation();
_this.ajaxCurrentButton = $(e.target);
_this.ajaxOriginalButton = $originalButton;
// Some core JS binds dialog buttons to the mousedown or mouseup
// events instead of click; all three events must be simulated here.
// @see https://www.drupal.org/project/bootstrap/issues/3016254
Bootstrap.simulate($originalButton, ['mousedown', 'mouseup', 'click']);
},
create: function () {
_this.ajaxCurrentButton = $(this);
_this.ajaxOriginalButton = $originalButton;
_this.ajaxUpdateButtons(true);
}
});
});
return buttons;
};
})(window.jQuery, window.Drupal, window.Drupal.bootstrap);

View File

@@ -0,0 +1,30 @@
/**
* @file
* Extends methods from core/misc/form.js.
*/
(function ($, window, Drupal, drupalSettings, once) {
/**
* Behavior for "forms_has_error_value_toggle" theme setting.
*/
Drupal.behaviors.bootstrapForm = {
attach: function (context) {
if (drupalSettings.bootstrap && drupalSettings.bootstrap.forms_has_error_value_toggle) {
var $context = $(context);
$(once('error', '.form-item.has-error:not(.form-type-password.has-feedback)', context)).each(function () {
var $formItem = $(this);
var $input = $formItem.find(':input');
$input.on('keyup focus blur', function () {
if (this.defaultValue !== void 0) {
$formItem[this.defaultValue !== this.value ? 'removeClass' : 'addClass']('has-error');
$input[this.defaultValue !== this.value ? 'removeClass' : 'addClass']('error');
}
});
});
}
}
};
})(jQuery, this, Drupal, drupalSettings, once);

View File

@@ -0,0 +1,181 @@
/**
* @file
* message.js
*/
(function ($, Drupal) {
/**
* Retrieves the classes for a specific message type.
*
* @param {String} type
* The type of message.
*
* @return {String}
* The classes to add, space separated.
*/
Drupal.Message.getMessageTypeClass = function (type) {
var classes = this.getMessageTypeClasses();
return 'alert alert-' + (classes[type] || 'success');
};
/**
* Helper function to map Drupal types to Bootstrap classes.
*
* @return {Object<String, String>}
* A map of classes, keyed by message type.
*/
Drupal.Message.getMessageTypeClasses = function () {
return {
status: 'success',
error: 'danger',
warning: 'warning',
info: 'info',
};
};
/**
* Retrieves a label for a specific message type.
*
* @param {String} type
* The type of message.
*
* @return {String}
* The message type label.
*/
Drupal.Message.getMessageTypeLabel = function (type) {
var labels = this.getMessageTypeLabels();
return labels[type];
};
/**
* @inheritDoc
*/
Drupal.Message.getMessageTypeLabels = function () {
return {
status: Drupal.t('Status message'),
error: Drupal.t('Error message'),
warning: Drupal.t('Warning message'),
info: Drupal.t('Informative message'),
};
};
/**
* Retrieves the aria-role for a specific message type.
*
* @param {String} type
* The type of message.
*
* @return {String}
* The message type role.
*/
Drupal.Message.getMessageTypeRole = function (type) {
var labels = this.getMessageTypeRoles();
return labels[type];
};
/**
* Map of the message type aria-role values.
*
* @return {Object<String, String>}
* A map of roles, keyed by message type.
*/
Drupal.Message.getMessageTypeRoles = function () {
return {
status: 'status',
error: 'alert',
warning: 'alert',
info: 'status',
};
};
/**
* @inheritDoc
*/
Drupal.theme.message = function (message, options) {
options = options || {};
var wrapper = Drupal.theme('messageWrapper', options.id || (new Date()).getTime(), options.type || 'status');
if (options.dismissible === void 0 || !!options.dismissible) {
wrapper.classList.add('alert-dismissible');
wrapper.appendChild(Drupal.theme('messageClose'));
}
wrapper.appendChild(Drupal.theme('messageContents', message && message.text));
return wrapper;
};
/**
* Themes the message container.
*
* @param {String} id
* The message identifier.
* @param {String} type
* The type of message.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageWrapper = function (id, type) {
var wrapper = document.createElement('div');
var label = Drupal.Message.getMessageTypeLabel(type);
wrapper.setAttribute('class', Drupal.Message.getMessageTypeClass(type));
wrapper.setAttribute('role', Drupal.Message.getMessageTypeRole(type));
wrapper.setAttribute('aria-label', label);
wrapper.setAttribute('data-drupal-message-id', id);
wrapper.setAttribute('data-drupal-message-type', type);
if (label) {
wrapper.appendChild(Drupal.theme('messageLabel', label));
}
return wrapper;
};
/**
* Themes the message close button.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageClose = function () {
var element = document.createElement('button');
element.setAttribute('class', 'close');
element.setAttribute('type', 'button');
element.setAttribute('role', 'button');
element.setAttribute('data-dismiss', 'alert');
element.setAttribute('aria-label', Drupal.t('Close'));
element.innerHTML = '<span aria-hidden="true">&times;</span>';
return element;
};
/**
* Themes the message container.
*
* @param {String} label
* The message label.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageLabel = function (label) {
var element = document.createElement('h2');
element.setAttribute('class', 'sr-only');
element.innerHTML = label;
return element;
};
/**
* Themes the message contents.
*
* @param {String} html
* The message identifier.
*
* @return {HTMLElement}
* A constructed HTMLElement.
*/
Drupal.theme.messageContents = function (html) {
var element = document.createElement('p');
element.innerHTML = '' + html;
return element;
}
})(window.jQuery, window.Drupal);

View File

@@ -0,0 +1,73 @@
/**
* @file
* Extends methods from core/misc/progress.js.
*/
(function ($, Drupal) {
'use strict';
/**
* Theme function for the progress bar.
*
* @param {string} id
*
* @return {string}
* The HTML for the progress bar.
*/
Drupal.theme.progressBar = function (id) {
return '<div class="progress-wrapper" aria-live="polite">' +
'<div class="message"></div>'+
'<div id ="' + id + '" class="progress progress-striped active">' +
'<div class="progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">' +
'<span class="percentage"></span>' +
'</div>' +
'</div>' +
'<div class="progress-label"></div>' +
'</div>';
};
$.extend(Drupal.ProgressBar.prototype, /** @lends Drupal.ProgressBar */{
/**
* Set the percentage and status message for the progressbar.
*
* @param {number} percentage
* @param {string} message
* @param {string} label
*/
setProgress: function (percentage, message, label) {
if (percentage >= 0 && percentage <= 100) {
$(this.element).find('.progress-bar').css('width', percentage + '%').attr('aria-valuenow', percentage);
$(this.element).find('.percentage').html(percentage + '%');
}
if (message) {
// Remove the unnecessary whitespace at the end of the message.
message = message.replace(/<br\/>&nbsp;|\s*$/, '');
$('.message', this.element).html(message);
}
if (label) {
$('.progress-label', this.element).html(label);
}
if (this.updateCallback) {
this.updateCallback(percentage, message, this);
}
},
/**
* Display errors on the page.
*
* @param {string} string
*/
displayError: function (string) {
var error = $('<div class="alert alert-block alert-error"><button class="close" data-dismiss="alert">&times;</button><h4>' + Drupal.t('Error message') + '</h4></div>').append(string);
$(this.element).before(error).hide();
if (this.errorCallback) {
this.errorCallback(this);
}
}
});
})(jQuery, Drupal);

View File

@@ -0,0 +1,30 @@
/**
* @file
* Extends core/misc/states.js.
*/
(function($) {
// Unbind core state.js from document first so we can then override below.
$(document).unbind('state:disabled');
/**
* Global state change handlers. These are bound to "document" to cover all
* elements whose state changes. Events sent to elements within the page
* bubble up to these handlers. We use this system so that themes and modules
* can override these state change handlers for particular parts of a page.
*/
$(document).bind('state:disabled', function(e) {
// Only act when this change was triggered by a dependency and not by the
// element monitoring itself.
if (e.trigger) {
$(e.target)
.attr('disabled', e.value)
.closest('.form-item, .form-submit, .form-wrapper').toggleClass('form-disabled', e.value)
.find(':input').attr('disabled', e.value);
// Note: WebKit nightlies don't reflect that change correctly.
// See https://bugs.webkit.org/show_bug.cgi?id=23789
}
});
})(jQuery);

View File

@@ -0,0 +1,463 @@
/**
* @file
* Extends methods from core/misc/tabledrag.js.
*/
(function ($) {
// Save the original prototype.
var prototype = Drupal.tableDrag.prototype;
/**
* Provides table and field manipulation.
*
* @constructor
*
* @param {HTMLElement} table
* DOM object for the table to be made draggable.
* @param {object} tableSettings
* Settings for the table added via drupal_add_dragtable().
*/
Drupal.tableDrag = function (table, tableSettings) {
var self = this;
var $table = $(table);
/**
* @type {jQuery}
*/
this.$table = $(table);
/**
*
* @type {HTMLElement}
*/
this.table = table;
/**
* @type {object}
*/
this.tableSettings = tableSettings;
/**
* Used to hold information about a current drag operation.
*
* @type {?HTMLElement}
*/
this.dragObject = null;
/**
* Provides operations for row manipulation.
*
* @type {?HTMLElement}
*/
this.rowObject = null;
/**
* Remember the previous element.
*
* @type {?HTMLElement}
*/
this.oldRowElement = null;
/**
* Used to determine up or down direction from last mouse move.
*
* @type {number}
*/
this.oldY = 0;
/**
* Whether anything in the entire table has changed.
*
* @type {bool}
*/
this.changed = false;
/**
* Maximum amount of allowed parenting.
*
* @type {number}
*/
this.maxDepth = 0;
/**
* Direction of the table.
*
* @type {number}
*/
this.rtl = $(this.table).css('direction') === 'rtl' ? -1 : 1;
/**
*
* @type {bool}
*/
this.striping = $(this.table).data('striping') === 1;
/**
* Configure the scroll settings.
*
* @type {object}
*
* @prop {number} amount
* @prop {number} interval
* @prop {number} trigger
*/
this.scrollSettings = {amount: 4, interval: 50, trigger: 70};
/**
*
* @type {?number}
*/
this.scrollInterval = null;
/**
*
* @type {number}
*/
this.scrollY = 0;
/**
*
* @type {number}
*/
this.windowHeight = 0;
/**
* @type {?HTMLElement}
*/
this.$toggleWeightButton = null;
/**
* Check this table's settings to see if there are parent relationships in
* this table. For efficiency, large sections of code can be skipped if we
* don't need to track horizontal movement and indentations.
*
* @type {bool}
*/
this.indentEnabled = false;
for (var group in tableSettings) {
if (tableSettings.hasOwnProperty(group)) {
for (var n in tableSettings[group]) {
if (tableSettings[group].hasOwnProperty(n)) {
if (tableSettings[group][n].relationship === 'parent') {
this.indentEnabled = true;
}
if (tableSettings[group][n].limit > 0) {
this.maxDepth = tableSettings[group][n].limit;
}
}
}
}
}
if (this.indentEnabled) {
/**
* Total width of indents, set in makeDraggable.
*
* @type {number}
*/
this.indentCount = 1;
// Find the width of indentations to measure mouse movements against.
// Because the table doesn't need to start with any indentations, we
// manually append 2 indentations in the first draggable row, measure
// the offset, then remove.
var indent = Drupal.theme('tableDragIndentation');
var testRow = $('<tr/>').addClass('draggable').appendTo(table);
var testCell = $('<td/>').appendTo(testRow).prepend(indent).prepend(indent);
var $indentation = testCell.find('.js-indentation');
/**
*
* @type {number}
*/
this.indentAmount = $indentation.get(1).offsetLeft - $indentation.get(0).offsetLeft;
testRow.remove();
}
// Make each applicable row draggable.
// Match immediate children of the parent element to allow nesting.
$table.find('> tr.draggable, > tbody > tr.draggable').each(function () { self.makeDraggable(this); });
// Add a link before the table for users to show or hide weight columns.
self.$toggleWeightButton = $(Drupal.theme('btn-sm', {
'class': ['tabledrag-toggle-weight'],
'data-drupal-selector': ['tabledrag-toggle-weight'],
title: Drupal.t('Re-order rows by numerical weight instead of dragging.'),
'data-toggle': 'tooltip'
}));
self.$toggleWeightButton
.on('click', $.proxy(function (e) {
e.preventDefault();
this.toggleColumns();
}, this))
.wrap('<div class="tabledrag-toggle-weight-wrapper"></div>')
.parent();
$table.before(self.$toggleWeightButton);
// Initialize the specified columns (for example, weight or parent columns)
// to show or hide according to user preference. This aids accessibility
// so that, e.g., screen reader users can choose to enter weight values and
// manipulate form elements directly, rather than using drag-and-drop..
self.initColumns();
// Add event bindings to the document. The self variable is passed along
// as event handlers do not have direct access to the tableDrag object.
$(document).on('touchmove', function (event) { return self.dragRow(event.originalEvent.touches[0], self); });
$(document).on('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); });
$(document).on('mousemove pointermove', function (event) { return self.dragRow(event, self); });
$(document).on('mouseup pointerup', function (event) { return self.dropRow(event, self); });
// React to localStorage event showing or hiding weight columns.
$(window).on('storage', $.proxy(function (e) {
// Only react to 'Drupal.tableDrag.showWeight' value change.
if (e.originalEvent.key === 'Drupal.tableDrag.showWeight') {
// This was changed in another window, get the new value for this
// window.
showWeight = JSON.parse(e.originalEvent.newValue);
this.displayColumns(showWeight);
}
}, this));
};
// Restore the original prototype.
Drupal.tableDrag.prototype = prototype;
/**
* Take an item and add event handlers to make it become draggable.
*
* @param {HTMLElement} item
*/
Drupal.tableDrag.prototype.makeDraggable = function (item) {
var self = this;
var $item = $(item);
// Add a class to the title link
$item.find('td:first-of-type').find('a').addClass('menu-item__link');
// Create the handle.
var handle = $('<a href="#" class="tabledrag-handle"/>');
// Insert the handle after indentations (if any).
var $indentationLast = $item.find('td:first-of-type').find('.js-indentation').eq(-1);
if ($indentationLast.length) {
$indentationLast.after(handle);
// Update the total width of indentation in this entire table.
self.indentCount = Math.max($item.find('.js-indentation').length, self.indentCount);
}
else {
$item.find('td').eq(0).prepend(handle);
}
// Add the glyphicon to the handle.
handle
.attr('title', Drupal.t('Drag to re-order'))
.attr('data-toggle', 'tooltip')
.append(Drupal.theme('bootstrapIcon', 'move'))
;
handle.on('mousedown touchstart pointerdown', function (event) {
event.preventDefault();
if (event.originalEvent.type === 'touchstart') {
event = event.originalEvent.touches[0];
}
self.dragStart(event, self, item);
});
// Prevent the anchor tag from jumping us to the top of the page.
handle.on('click', function (e) {
e.preventDefault();
});
// Set blur cleanup when a handle is focused.
handle.on('focus', function () {
self.safeBlur = true;
});
// On blur, fire the same function as a touchend/mouseup. This is used to
// update values after a row has been moved through the keyboard support.
handle.on('blur', function (event) {
if (self.rowObject && self.safeBlur) {
self.dropRow(event, self);
}
});
// Add arrow-key support to the handle.
handle.on('keydown', function (event) {
// If a rowObject doesn't yet exist and this isn't the tab key.
if (event.keyCode !== 9 && !self.rowObject) {
self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true);
}
var keyChange = false;
var groupHeight;
switch (event.keyCode) {
// Left arrow.
case 37:
// Safari left arrow.
case 63234:
keyChange = true;
self.rowObject.indent(-1 * self.rtl);
break;
// Up arrow.
case 38:
// Safari up arrow.
case 63232:
var $previousRow = $(self.rowObject.element).prev('tr:first-of-type');
var previousRow = $previousRow.get(0);
while (previousRow && $previousRow.is(':hidden')) {
$previousRow = $(previousRow).prev('tr:first-of-type');
previousRow = $previousRow.get(0);
}
if (previousRow) {
// Do not allow the onBlur cleanup.
self.safeBlur = false;
self.rowObject.direction = 'up';
keyChange = true;
if ($(item).is('.tabledrag-root')) {
// Swap with the previous top-level row.
groupHeight = 0;
while (previousRow && $previousRow.find('.js-indentation').length) {
$previousRow = $(previousRow).prev('tr:first-of-type');
previousRow = $previousRow.get(0);
groupHeight += $previousRow.is(':hidden') ? 0 : previousRow.offsetHeight;
}
if (previousRow) {
self.rowObject.swap('before', previousRow);
// No need to check for indentation, 0 is the only valid one.
window.scrollBy(0, -groupHeight);
}
}
else if (self.table.tBodies[0].rows[0] !== previousRow || $previousRow.is('.draggable')) {
// Swap with the previous row (unless previous row is the first
// one and undraggable).
self.rowObject.swap('before', previousRow);
self.rowObject.interval = null;
self.rowObject.indent(0);
window.scrollBy(0, -parseInt(item.offsetHeight, 10));
}
// Regain focus after the DOM manipulation.
handle.trigger('focus');
}
break;
// Right arrow.
case 39:
// Safari right arrow.
case 63235:
keyChange = true;
self.rowObject.indent(self.rtl);
break;
// Down arrow.
case 40:
// Safari down arrow.
case 63233:
var $nextRow = $(self.rowObject.group).eq(-1).next('tr:first-of-type');
var nextRow = $nextRow.get(0);
while (nextRow && $nextRow.is(':hidden')) {
$nextRow = $(nextRow).next('tr:first-of-type');
nextRow = $nextRow.get(0);
}
if (nextRow) {
// Do not allow the onBlur cleanup.
self.safeBlur = false;
self.rowObject.direction = 'down';
keyChange = true;
if ($(item).is('.tabledrag-root')) {
// Swap with the next group (necessarily a top-level one).
groupHeight = 0;
var nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false);
if (nextGroup) {
$(nextGroup.group).each(function () {
groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight;
});
var nextGroupRow = $(nextGroup.group).eq(-1).get(0);
self.rowObject.swap('after', nextGroupRow);
// No need to check for indentation, 0 is the only valid one.
window.scrollBy(0, parseInt(groupHeight, 10));
}
}
else {
// Swap with the next row.
self.rowObject.swap('after', nextRow);
self.rowObject.interval = null;
self.rowObject.indent(0);
window.scrollBy(0, parseInt(item.offsetHeight, 10));
}
// Regain focus after the DOM manipulation.
handle.trigger('focus');
}
break;
}
if (self.rowObject && self.rowObject.changed === true) {
$(item).addClass('drag');
if (self.oldRowElement) {
$(self.oldRowElement).removeClass('drag-previous');
}
self.oldRowElement = item;
if (self.striping === true) {
self.restripeTable();
}
self.onDrag();
}
// Returning false if we have an arrow key to prevent scrolling.
if (keyChange) {
return false;
}
});
// Compatibility addition, return false on keypress to prevent unwanted
// scrolling. IE and Safari will suppress scrolling on keydown, but all
// other browsers need to return false on keypress.
// http://www.quirksmode.org/js/keys.html
handle.on('keypress', function (event) {
switch (event.keyCode) {
// Left arrow.
case 37:
// Up arrow.
case 38:
// Right arrow.
case 39:
// Down arrow.
case 40:
return false;
}
});
};
/**
* Add an asterisk or other marker to the changed row.
*/
Drupal.tableDrag.prototype.row.prototype.markChanged = function () {
var $cell = $('td:first', this.element);
// Find the first appropriate place to insert the marker.
var $target = $($cell.find('.file-size').get(0) || $cell.find('.file').get(0) || $cell.find('.tabledrag-handle').get(0));
if (!$cell.find('.tabledrag-changed').length) {
$target.after(' ' + Drupal.theme('tableDragChangedMarker') + ' ');
}
};
$.extend(Drupal.theme, /** @lends Drupal.theme */{
/**
* @return {string}
*/
tableDragChangedMarker: function () {
return Drupal.theme('bootstrapIcon', 'warning-sign', {'class': ['tabledrag-changed', 'text-warning']});
},
/**
* @return {string}
*/
tableDragChangedWarning: function () {
return '<div class="tabledrag-changed-warning alert alert-sm alert-warning messages warning">' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t('You have unsaved changes.') + '</div>';
}
});
})(jQuery);

View File

@@ -0,0 +1,15 @@
/**
* @file
* Overrides core/misc/vertical-tabs.js.
*/
(function ($, Drupal) {
"use strict";
var createSticky = Drupal.TableHeader.prototype.createSticky;
Drupal.TableHeader.prototype.createSticky = function () {
createSticky.call(this);
this.$stickyTable.addClass(this.$originalTable.attr('class')).removeClass('sticky-enabled sticky-table');
};
})(window.jQuery, window.Drupal);

View File

@@ -0,0 +1,289 @@
/**
* @file
* Overrides core/misc/vertical-tabs.js.
*/
(function ($, window, Drupal, drupalSettings, once) {
"use strict";
/**
* Show the parent vertical tab pane of a targeted page fragment.
*
* In order to make sure a targeted element inside a vertical tab pane is
* visible on a hash change or fragment link click, show all parent panes.
*
* @param {jQuery.Event} e
* The event triggered.
* @param {jQuery} $target
* The targeted node as a jQuery object.
*/
var handleFragmentLinkClickOrHashChange = function handleFragmentLinkClickOrHashChange(e, $target) {
$target.parents('.vertical-tabs-pane').each(function (index, pane) {
$(pane).data('verticalTab').focus();
});
};
/**
* This script transforms a set of details into a stack of vertical
* tabs. Another tab pane can be selected by clicking on the respective
* tab.
*
* Each tab may have a summary which can be updated by another
* script. For that to work, each details element has an associated
* 'verticalTabCallback' (with jQuery.data() attached to the details),
* which is called every time the user performs an update to a form
* element inside the tab pane.
*/
Drupal.behaviors.verticalTabs = {
attach: function (context) {
var width = drupalSettings.widthBreakpoint || 640;
var mq = '(max-width: ' + width + 'px)';
if (window.matchMedia(mq).matches) {
return;
}
/**
* Binds a listener to handle fragment link clicks and URL hash changes.
*/
$(once('vertical-tabs-fragments', 'body')).on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange);
$(once('vertical-tabs', '[data-vertical-tabs-panes]', context)).each(function () {
var $this = $(this).addClass('tab-content vertical-tabs-panes');
var focusID = $(':hidden.vertical-tabs__active-tab', this).val();
if (typeof focusID === 'undefined' || !focusID.length) {
focusID = false;
}
var tab_focus;
// Check if there are some details that can be converted to vertical-tabs
var $details = $this.find('> .panel');
if ($details.length === 0) {
return;
}
// Create the tab column.
var tab_list = $('<ul class="nav nav-tabs vertical-tabs-list"></ul>');
$this.wrap('<div class="tabbable tabs-left vertical-tabs clearfix"></div>').before(tab_list);
// Transform each details into a tab.
$details.each(function () {
var $that = $(this);
var vertical_tab = new Drupal.verticalTab({
title: $that.find('> .panel-heading > .panel-title, > .panel-heading').last().html(),
details: $that
});
tab_list.append(vertical_tab.item);
$that
.removeClass('collapsed')
// prop() can't be used on browsers not supporting details element,
// the style won't apply to them if prop() is used.
.attr('open', true)
.removeClass('collapsible collapsed panel panel-default')
.addClass('tab-pane vertical-tabs-pane')
.data('verticalTab', vertical_tab)
.find('> .panel-heading').remove();
if (this.id === focusID) {
tab_focus = $that;
}
});
$(tab_list).find('> li:first').addClass('first');
$(tab_list).find('> li:last').addClass('last');
if (!tab_focus) {
// If the current URL has a fragment and one of the tabs contains an
// element that matches the URL fragment, activate that tab.
var $locationHash = $this.find(window.location.hash);
if (window.location.hash && $locationHash.length) {
tab_focus = $locationHash.closest('.vertical-tabs-pane');
}
else {
tab_focus = $this.find('> .vertical-tabs-pane:first');
}
}
if (tab_focus.length) {
tab_focus.data('verticalTab').focus();
}
});
// Provide some Bootstrap tab/Drupal integration.
// @todo merge this into the above code from core.
$(once('bootstrap-tabs', '.tabbable', context)).each(function () {
var $wrapper = $(this);
var $tabs = $wrapper.find('.nav-tabs');
var $content = $wrapper.find('.tab-content');
var borderRadius = parseInt($content.css('borderBottomRightRadius'), 10);
var bootstrapTabResize = function() {
if ($wrapper.hasClass('tabs-left') || $wrapper.hasClass('tabs-right')) {
$content.css('min-height', $tabs.outerHeight());
}
};
// Add min-height on content for left and right tabs.
bootstrapTabResize();
// Detect tab switch.
if ($wrapper.hasClass('tabs-left') || $wrapper.hasClass('tabs-right')) {
$tabs.on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
bootstrapTabResize();
if ($wrapper.hasClass('tabs-left')) {
if ($(e.target).parent().is(':first-child')) {
$content.css('borderTopLeftRadius', '0');
}
else {
$content.css('borderTopLeftRadius', borderRadius + 'px');
}
}
else {
if ($(e.target).parent().is(':first-child')) {
$content.css('borderTopRightRadius', '0');
}
else {
$content.css('borderTopRightRadius', borderRadius + 'px');
}
}
});
}
});
}
};
/**
* The vertical tab object represents a single tab within a tab group.
*
* @param settings
* An object with the following keys:
* - title: The name of the tab.
* - details: The jQuery object of the details element that is the tab pane.
*/
Drupal.verticalTab = function (settings) {
var self = this;
$.extend(this, settings, Drupal.theme('verticalTab', settings));
this.link.attr('href', '#' + settings.details.attr('id'));
this.link.on('click', function (e) {
e.preventDefault();
self.focus();
});
// Keyboard events added:
// Pressing the Enter key will open the tab pane.
this.link.on('keydown', function (event) {
if (event.keyCode === 13) {
event.preventDefault();
self.focus();
// Set focus on the first input field of the visible details/tab pane.
$(".vertical-tabs-pane :input:visible:enabled:first").trigger('focus');
}
});
this.details
.on('summaryUpdated', function () {
self.updateSummary();
})
.trigger('summaryUpdated');
};
Drupal.verticalTab.prototype = {
/**
* Displays the tab's content pane.
*/
focus: function () {
this.details
.siblings('.vertical-tabs-pane')
.each(function () {
$(this).removeClass('active').find('> div').removeClass('in');
var tab = $(this).data('verticalTab');
tab.item.removeClass('selected');
})
.end()
.addClass('active')
.siblings(':hidden.vertical-tabs-active-tab')
.val(this.details.attr('id'));
this.details.find('> div').addClass('in');
this.details.data('verticalTab').item.find('a').tab('show');
this.item.addClass('selected');
// Mark the active tab for screen readers.
$('#active-vertical-tab').remove();
this.link.append('<span id="active-vertical-tab" class="visually-hidden">' + Drupal.t('(active tab)') + '</span>');
},
/**
* Updates the tab's summary.
*/
updateSummary: function () {
this.summary.html(this.details.drupalGetSummary());
},
/**
* Shows a vertical tab pane.
*/
tabShow: function () {
// Display the tab.
this.item.show();
// Show the vertical tabs.
this.item.closest('.form-type-vertical-tabs').show();
// Update .first marker for items. We need recurse from parent to retain the
// actual DOM element order as jQuery implements sortOrder, but not as public
// method.
this.item.parent().children('.vertical-tab-button').removeClass('first')
.filter(':visible:first').addClass('first');
// Display the details element.
this.details.removeClass('vertical-tab-hidden').show();
// Focus this tab.
this.focus();
return this;
},
/**
* Hides a vertical tab pane.
*/
tabHide: function () {
// Hide this tab.
this.item.hide();
// Update .first marker for items. We need recurse from parent to retain the
// actual DOM element order as jQuery implements sortOrder, but not as public
// method.
this.item.parent().children('.vertical-tab-button').removeClass('first')
.filter(':visible:first').addClass('first');
// Hide the details element.
this.details.addClass('vertical-tab-hidden').hide();
// Focus the first visible tab (if there is one).
var $firstTab = this.details.siblings('.vertical-tabs-pane:not(.vertical-tab-hidden):first');
if ($firstTab.length) {
$firstTab.data('verticalTab').focus();
}
// Hide the vertical tabs (if no tabs remain).
else {
this.item.closest('.form-type-vertical-tabs').hide();
}
return this;
}
};
/**
* Theme function for a vertical tab.
*
* @param settings
* An object with the following keys:
* - title: The name of the tab.
* @return
* This function has to return an object with at least these keys:
* - item: The root tab jQuery element
* - link: The anchor tag that acts as the clickable area of the tab
* (jQuery version)
* - summary: The jQuery element that contains the tab summary
*/
Drupal.theme.verticalTab = function (settings) {
var tab = {};
tab.item = $('<li class="vertical-tab-button" tabindex="-1"></li>')
.append(tab.link = $('<a href="#' + settings.details[0].id + '" data-toggle="tab"></a>')
.append(tab.title = $('<span></span>').html(settings.title))
.append(tab.summary = $('<div class="summary"></div>')
)
);
return tab;
};
})(jQuery, this, Drupal, drupalSettings, once);

View File

@@ -0,0 +1,585 @@
/**
* @file
* Bootstrap Modals.
*
* @param {jQuery} $
* @param {Drupal} Drupal
* @param {Drupal.bootstrap} Bootstrap
* @param {Attributes} Attributes
* @param {drupalSettings} drupalSettings
*/
(function ($, Drupal, Bootstrap, Attributes, drupalSettings) {
'use strict';
/**
* Only process this once.
*/
Bootstrap.once('modal.jquery.ui.bridge', function (settings) {
// RTL support.
var rtl = document.documentElement.getAttribute('dir').toLowerCase() === 'rtl';
// Override drupal.dialog button classes. This must be done on DOM ready
// since core/drupal.dialog technically depends on this file and has not
// yet set their default settings.
$(function () {
drupalSettings.dialog.buttonClass = 'btn';
drupalSettings.dialog.buttonPrimaryClass = 'btn-primary';
});
// Create the "dialog" plugin bridge.
Bootstrap.Dialog.Bridge = function (options) {
var args = Array.prototype.slice.call(arguments);
var $element = $(this);
var type = options && options.dialogType || $element[0].dialogType || 'modal';
$element[0].dialogType = type;
var handler = Bootstrap.Dialog.Handler.get(type);
// When only options are passed, jQuery UI dialog treats this like a
// initialization method. Destroy any existing Bootstrap modal and
// recreate it using the contents of the dialog HTML.
if (args.length === 1 && typeof options === 'object') {
this.each(function () {
handler.ensureModalStructure(this, options);
});
// Proxy to the Bootstrap Modal plugin, indicating that this is a
// jQuery UI dialog bridge.
return handler.invoke(this, {
dialogOptions: options,
jQueryUiBridge: true
});
}
// Otherwise, proxy all arguments to the Bootstrap Modal plugin.
var ret;
try {
ret = handler.invoke.apply(handler, [this].concat(args));
}
catch (e) {
Bootstrap.warn(e);
}
// If just one element and there was a result returned for the option passed,
// then return the result. Otherwise, just return the jQuery object.
return this.length === 1 && ret !== void 0 ? ret : this;
};
// Assign the jQuery "dialog" plugin to use to the bridge.
Bootstrap.createPlugin('dialog', Bootstrap.Dialog.Bridge);
// Create the "modal" plugin bridge.
Bootstrap.Modal.Bridge = function () {
var Modal = this;
return {
DEFAULTS: {
// By default, this option is disabled. It's only flagged when a modal
// was created using $.fn.dialog above.
jQueryUiBridge: false
},
prototype: {
/**
* Handler for $.fn.dialog('close').
*/
close: function () {
var _this = this;
this.hide.apply(this, arguments);
// For some reason (likely due to the transition event not being
// registered properly), the backdrop doesn't always get removed
// after the above "hide" method is invoked . Instead, ensure the
// backdrop is removed after the transition duration by manually
// invoking the internal "hideModal" method shortly thereafter.
setTimeout(function () {
if (!_this.isShown && _this.$backdrop) {
_this.hideModal();
}
}, (Modal.TRANSITION_DURATION !== void 0 ? Modal.TRANSITION_DURATION : 300) + 10);
},
/**
* Creates any necessary buttons from dialog options.
*/
createButtons: function () {
var handler = Bootstrap.Dialog.Handler.get(this.$element);
this.$footer.find(handler.selectors.buttons).remove();
// jQuery UI supports both objects and arrays. Unfortunately
// developers have misunderstood and abused this by simply placing
// the objects that should be in an array inside an object with
// arbitrary keys (likely to target specific buttons as a hack).
var buttons = this.options.dialogOptions && this.options.dialogOptions.buttons || [];
if (!Array.isArray(buttons)) {
var array = [];
for (var k in buttons) {
// Support the proper object values: label => click callback.
if (typeof buttons[k] === 'function') {
array.push({
label: k,
click: buttons[k],
});
}
// Support nested objects, but log a warning.
else if (buttons[k].text || buttons[k].label) {
Bootstrap.warn('Malformed jQuery UI dialog button: @key. The button object should be inside an array.', {
'@key': k
});
array.push(buttons[k]);
}
else {
Bootstrap.unsupported('button', k, buttons[k]);
}
}
buttons = array;
}
if (buttons.length) {
var $buttons = $('<div class="modal-buttons"/>').appendTo(this.$footer);
for (var i = 0, l = buttons.length; i < l; i++) {
var button = buttons[i];
var $button = $(Drupal.theme('bootstrapModalDialogButton', button));
// Invoke the "create" method for jQuery UI buttons.
if (typeof button.create === 'function') {
button.create.call($button[0]);
}
// Bind the "click" method for jQuery UI buttons to the modal.
if (typeof button.click === 'function') {
$button.on('click', button.click.bind(this.$element));
}
$buttons.append($button);
}
}
// Toggle footer visibility based on whether it has child elements.
this.$footer[this.$footer.children()[0] ? 'show' : 'hide']();
},
/**
* Initializes the Bootstrap Modal.
*/
init: function () {
var handler = Bootstrap.Dialog.Handler.get(this.$element);
if (!this.$dialog) {
this.$dialog = this.$element.find(handler.selectors.dialog);
}
this.$dialog.addClass('js-drupal-dialog');
if (!this.$header) {
this.$header = this.$dialog.find(handler.selectors.header);
}
if (!this.$title) {
this.$title = this.$dialog.find(handler.selectors.title);
}
if (!this.$close) {
this.$close = this.$header.find(handler.selectors.close);
}
if (!this.$footer) {
this.$footer = this.$dialog.find(handler.selectors.footer);
}
if (!this.$content) {
this.$content = this.$dialog.find(handler.selectors.content);
}
if (!this.$dialogBody) {
this.$dialogBody = this.$dialog.find(handler.selectors.body);
}
// Relay necessary events.
if (this.options.jQueryUiBridge) {
this.$element.on('hide.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogbeforeclose', false));
this.$element.on('hidden.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogclose', false));
this.$element.on('show.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogcreate', false));
this.$element.on('shown.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogopen', false));
}
// Create a footer if one doesn't exist.
// This is necessary in case dialog.ajax.js decides to add buttons.
if (!this.$footer[0]) {
this.$footer = handler.theme('footer', {}, true).insertAfter(this.$dialogBody);
}
// Map the initial options.
$.extend(true, this.options, this.mapDialogOptions(this.options));
// Update buttons.
this.createButtons();
// Now call the parent init method.
this.super();
// Handle autoResize option (this is a drupal.dialog option).
if (this.options.dialogOptions && this.options.dialogOptions.autoResize && this.options.dialogOptions.position) {
this.position(this.options.dialogOptions.position);
}
// If show is enabled and currently not shown, show it.
if (this.options.jQueryUiBridge && this.options.show && !this.isShown) {
this.show();
}
},
/**
* Handler for $.fn.dialog('instance').
*/
instance: function () {
Bootstrap.unsupported('method', 'instance', arguments);
},
/**
* Handler for $.fn.dialog('isOpen').
*/
isOpen: function () {
return !!this.isShown;
},
/**
* Maps dialog options to the modal.
*
* @param {Object} options
* The options to map.
*/
mapDialogOptions: function (options) {
// Retrieve the dialog handler for this type.
var handler = Bootstrap.Dialog.Handler.get(this.$element);
var mappedOptions = {};
var dialogOptions = options.dialogOptions || {};
// Remove any existing dialog options.
delete options.dialogOptions;
// Separate Bootstrap modal options from jQuery UI dialog options.
for (var k in options) {
if (Modal.DEFAULTS.hasOwnProperty(k)) {
mappedOptions[k] = options[k];
}
else {
dialogOptions[k] = options[k];
}
}
// Handle CSS properties.
var cssUnitRegExp = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)?$/;
var parseCssUnit = function (value, defaultUnit) {
var parts = ('' + value).match(cssUnitRegExp);
return parts && parts[1] !== void 0 ? parts[1] + (parts[2] || defaultUnit || 'px') : null;
};
var styles = {};
var cssProperties = ['height', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'width'];
for (var i = 0, l = cssProperties.length; i < l; i++) {
var prop = cssProperties[i];
if (dialogOptions[prop] !== void 0) {
var value = parseCssUnit(dialogOptions[prop]);
if (value) {
styles[prop] = value;
// If there's a defined height of some kind, enforce the modal
// to use flex (on modern browsers). This will ensure that
// the core autoResize calculations don't cause the content
// to overflow.
if (dialogOptions.autoResize && (prop === 'height' || prop === 'maxHeight')) {
styles.display = 'flex';
styles.flexDirection = 'column';
this.$dialogBody.css('overflow', 'scroll');
}
}
}
}
// Apply mapped CSS styles to the modal-content container.
this.$content.css(styles);
// Handle deprecated "dialogClass" option by merging it with "classes".
var classesMap = {
'ui-dialog': 'modal-content',
'ui-dialog-titlebar': 'modal-header',
'ui-dialog-title': 'modal-title',
'ui-dialog-titlebar-close': 'close',
'ui-dialog-content': 'modal-body',
'ui-dialog-buttonpane': 'modal-footer'
};
if (dialogOptions.dialogClass) {
if (dialogOptions.classes === void 0) {
dialogOptions.classes = {};
}
if (dialogOptions.classes['ui-dialog'] === void 0) {
dialogOptions.classes['ui-dialog'] = '';
}
var dialogClass = dialogOptions.classes['ui-dialog'].split(' ');
dialogClass.push(dialogOptions.dialogClass);
dialogOptions.classes['ui-dialog'] = dialogClass.join(' ');
delete dialogOptions.dialogClass;
}
// Add jQuery UI classes to elements in case developers target them
// in callbacks.
for (k in classesMap) {
this.$element.find('.' + classesMap[k]).addClass(k);
}
// Bind events.
var events = [
'beforeClose', 'close',
'create',
'drag', 'dragStart', 'dragStop',
'focus',
'open',
'resize', 'resizeStart', 'resizeStop'
];
for (i = 0, l = events.length; i < l; i++) {
var event = events[i].toLowerCase();
if (dialogOptions[event] === void 0 || typeof dialogOptions[event] !== 'function') continue;
this.$element.on('dialog' + event, dialogOptions[event]);
}
// Support title attribute on the modal.
var title;
if ((dialogOptions.title === null || dialogOptions.title === void 0) && (title = this.$element.attr('title'))) {
dialogOptions.title = title;
}
// Handle the reset of the options.
for (var name in dialogOptions) {
if (!dialogOptions.hasOwnProperty(name) || dialogOptions[name] === void 0) continue;
switch (name) {
case 'appendTo':
Bootstrap.unsupported('option', name, dialogOptions.appendTo);
break;
case 'autoOpen':
mappedOptions.show = dialogOptions.show = !!dialogOptions.autoOpen;
break;
case 'classes':
if (dialogOptions.classes) {
for (var key in dialogOptions.classes) {
if (dialogOptions.classes.hasOwnProperty(key) && classesMap[key] !== void 0) {
// Run through Attributes to sanitize classes.
var attributes = Attributes.create().addClass(dialogOptions.classes[key]).toPlainObject();
var selector = '.' + classesMap[key];
this.$element.find(selector).addClass(attributes['class']);
}
}
}
break;
case 'closeOnEscape':
mappedOptions.keyboard = !!dialogOptions.closeOnEscape;
if (!dialogOptions.closeOnEscape && dialogOptions.modal) {
mappedOptions.backdrop = 'static';
}
break;
case 'closeText':
Bootstrap.unsupported('option', name, dialogOptions.closeText);
break;
case 'draggable':
this.$content
.draggable({
handle: handler.selectors.header,
drag: Bootstrap.relayEvent(this.$element, 'dialogdrag'),
start: Bootstrap.relayEvent(this.$element, 'dialogdragstart'),
end: Bootstrap.relayEvent(this.$element, 'dialogdragend')
})
.draggable(dialogOptions.draggable ? 'enable' : 'disable');
break;
case 'hide':
if (dialogOptions.hide === false || dialogOptions.hide === true) {
this.$element[dialogOptions.hide ? 'addClass' : 'removeClass']('fade');
mappedOptions.animation = dialogOptions.hide;
}
else {
Bootstrap.unsupported('option', name + ' (complex animation)', dialogOptions.hide);
}
break;
case 'modal':
if (!dialogOptions.closeOnEscape && dialogOptions.modal) {
mappedOptions.backdrop = 'static';
}
else {
mappedOptions.backdrop = dialogOptions.modal;
}
// If not a modal and no initial position, center it.
if (!dialogOptions.modal && !dialogOptions.position) {
this.position({ my: 'center', of: window });
}
break;
case 'position':
this.position(dialogOptions.position);
break;
// Resizable support (must initialize first).
case 'resizable':
this.$content
.resizable({
resize: Bootstrap.relayEvent(this.$element, 'dialogresize'),
start: Bootstrap.relayEvent(this.$element, 'dialogresizestart'),
end: Bootstrap.relayEvent(this.$element, 'dialogresizeend')
})
.resizable(dialogOptions.resizable ? 'enable' : 'disable');
break;
case 'show':
if (dialogOptions.show === false || dialogOptions.show === true) {
this.$element[dialogOptions.show ? 'addClass' : 'removeClass']('fade');
mappedOptions.animation = dialogOptions.show;
}
else {
Bootstrap.unsupported('option', name + ' (complex animation)', dialogOptions.show);
}
break;
case 'title':
this.$title.text(dialogOptions.title);
break;
}
}
// Add the supported dialog options to the mapped options.
mappedOptions.dialogOptions = dialogOptions;
return mappedOptions;
},
/**
* Handler for $.fn.dialog('moveToTop').
*/
moveToTop: function () {
Bootstrap.unsupported('method', 'moveToTop', arguments);
},
/**
* Handler for $.fn.dialog('option').
*/
option: function () {
var clone = {options: {}};
// Apply the parent option method to the clone of current options.
this.super.apply(clone, arguments);
// Merge in the cloned mapped options.
$.extend(true, this.options, this.mapDialogOptions(clone.options));
// Update buttons.
this.createButtons();
},
position: function(position) {
// Reset modal styling.
this.$element.css({
bottom: 'initial',
overflow: 'visible',
right: 'initial'
});
// Position the modal.
this.$element.position(position);
},
/**
* Handler for $.fn.dialog('open').
*/
open: function () {
this.show.apply(this, arguments);
},
/**
* Handler for $.fn.dialog('widget').
*/
widget: function () {
return this.$element;
}
}
};
};
// Extend the Bootstrap Modal plugin constructor class.
Bootstrap.extendPlugin('modal', Bootstrap.Modal.Bridge);
// Register default core dialog type handlers.
Bootstrap.Dialog.Handler.register('dialog');
Bootstrap.Dialog.Handler.register('modal');
/**
* Extend Drupal theming functions.
*/
$.extend(Drupal.theme, /** @lend Drupal.theme */ {
/**
* Renders a jQuery UI Dialog compatible button element.
*
* @param {Object} button
* The button object passed in the dialog options.
*
* @return {String}
* The modal dialog button markup.
*
* @see http://api.jqueryui.com/dialog/#option-buttons
* @see http://api.jqueryui.com/button/
*/
bootstrapModalDialogButton: function (button) {
var attributes = Attributes.create();
var icon = '';
var iconPosition = button.iconPosition || 'beginning';
iconPosition = (iconPosition === 'end' && !rtl) || (iconPosition === 'beginning' && rtl) ? 'after' : 'before';
// Handle Bootstrap icons differently.
if (button.bootstrapIcon) {
icon = Drupal.theme('icon', 'bootstrap', button.icon);
}
// Otherwise, assume it's a jQuery UI icon.
// @todo Map jQuery UI icons to Bootstrap icons?
else if (button.icon) {
var iconAttributes = Attributes.create()
.addClass(['ui-icon', button.icon])
.set('aria-hidden', 'true');
icon = '<span' + iconAttributes + '></span>';
}
// Label. Note: jQuery UI dialog has an inconsistency where it uses
// "text" instead of "label", so both need to be supported.
var value = button.label || button.text;
// Show/hide label.
if (icon && ((button.showLabel !== void 0 && !button.showLabel) || (button.text !== void 0 && !button.text))) {
value = '<span' + Attributes.create().addClass('sr-only') + '>' + value + '</span>';
}
attributes.set('value', iconPosition === 'before' ? icon + value : value + icon);
// Handle disabled.
attributes[button.disabled ? 'set' :'remove']('disabled', 'disabled');
if (button.classes) {
attributes.addClass(Object.keys(button.classes).map(function(key) { return button.classes[key]; }));
}
if (button['class']) {
attributes.addClass(button['class']);
}
if (button.primary) {
attributes.addClass('btn-primary');
}
return Drupal.theme('button', attributes);
}
});
});
})(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes, window.drupalSettings);

View File

@@ -0,0 +1,603 @@
/**
* @file
* Bootstrap Modals.
*
* @param {jQuery} $
* @param {Drupal} Drupal
* @param {Drupal.bootstrap} Bootstrap
* @param {Attributes} Attributes
*/
(function ($, Drupal, Bootstrap, Attributes) {
'use strict';
/**
* Document jQuery object.
*
* @type {jQuery}
*/
var $document = $(document);
/**
* Finds the first available and visible focusable input element.
*
* This is abstracted from the main code below so sub-themes can override
* this method to return their own element if desired.
*
* @param {Modal} modal
* The Bootstrap modal instance.
*
* @return {jQuery}
* A jQuery object containing the element that should be focused. Note: if
* this object contains multiple elements, only the first visible one will
* be used.
*/
Bootstrap.modalFindFocusableElement = function (modal) {
return modal.$dialogBody.find(':input,:button,.btn').not('.visually-hidden,.sr-only');
};
$document.on('shown.bs.modal', function (e) {
var $modal = $(e.target);
var modal = $modal.data('bs.modal');
// Check if there are any CKEditor 5 instances
var $ckeditor = $modal.find('[data-ckeditor5-id]');
if ($ckeditor.length) {
// Move the overlay wrapper inside the modal so it can be interacted with
$('.ck-body-wrapper').appendTo($modal);
}
// Focus the first input element found.
if (modal && modal.options.focusInput) {
var $focusable = Bootstrap.modalFindFocusableElement(modal);
if ($focusable && $focusable[0]) {
var $input = $focusable.filter(':visible:first').focus();
// Select text if input is text.
if (modal.options.selectText && $input.is(':text')) {
$input[0].setSelectionRange(0, $input[0].value.length)
}
}
else if (modal.$close.is(':visible')) {
modal.$close.focus();
}
}
});
/**
* Only process this once.
*/
Bootstrap.once('modal', function (settings) {
/**
* Replace the Bootstrap Modal jQuery plugin definition.
*
* This adds a little bit of functionality so it works better with Drupal.
*/
Bootstrap.replacePlugin('modal', function () {
var BootstrapModal = this;
// Override the Modal constructor.
Bootstrap.Modal = function (element, options) {
this.$body = $(document.body);
this.$element = $(element);
this.$dialog = this.$element.find('.modal-dialog');
this.$header = this.$dialog.find('.modal-header');
this.$title = this.$dialog.find('.modal-title');
this.$close = this.$header.find('.close');
this.$footer = this.$dialog.find('.modal-footer');
this.$content = this.$dialog.find('.modal-content');
this.$dialogBody = this.$dialog.find('.modal-body');
this.$backdrop = null;
this.isShown = null;
this.originalBodyPad = null;
this.scrollbarWidth = 0;
this.ignoreBackdropClick = false;
this.options = this.mapDialogOptions(options);
};
// Extend defaults to take into account for theme settings.
Bootstrap.Modal.DEFAULTS = $.extend({}, BootstrapModal.DEFAULTS, {
animation: !!settings.modal_animation,
backdrop: settings.modal_backdrop === 'static' ? 'static' : !!settings.modal_backdrop,
focusInput: !!settings.modal_focus_input,
selectText: !!settings.modal_select_text,
keyboard: !!settings.modal_keyboard,
remote: null,
show: !!settings.modal_show,
size: settings.modal_size
});
// Copy over the original prototype methods.
Bootstrap.Modal.prototype = BootstrapModal.prototype;
/**
* Handler for $.fn.modal('destroy').
*/
Bootstrap.Modal.prototype.destroy = function () {
this.hide();
Drupal.detachBehaviors(this.$element[0]);
this.$element.removeData('bs.modal').remove();
};
/**
* Initialize the modal.
*/
Bootstrap.Modal.prototype.init = function () {
if (this.options.remote) {
this.$content.load(this.options.remote, $.proxy(function () {
this.$element.trigger('loaded.bs.modal');
}, this));
}
};
/**
* Map dialog options.
*
* Note: this is primarily for use in modal.jquery.ui.bridge.js.
*
* @param {Object} options
* The passed options.
*/
Bootstrap.Modal.prototype.mapDialogOptions = function (options) {
return options || {};
}
// Modal jQuery Plugin Definition.
var Plugin = function () {
// Extract the arguments.
var args = Array.prototype.slice.call(arguments);
var method = args[0];
var options = args[1] || {};
var relatedTarget = args[2] || null;
// Move arguments down if no method was passed.
if ($.isPlainObject(method)) {
relatedTarget = options || null;
options = method;
method = null;
}
var ret = void 0;
this.each(function () {
var $this = $(this);
var data = $this.data('bs.modal');
var initialize = false;
// Immediately return if there's no instance to invoke a valid method.
var showMethods = ['open', 'show', 'toggle'];
if (!data && method && showMethods.indexOf(method) === -1) {
return;
}
options = Bootstrap.normalizeObject($.extend({}, Bootstrap.Modal.DEFAULTS, data && data.options, $this.data(), options));
delete options['bs.modal'];
if (!data) {
$this.data('bs.modal', (data = new Bootstrap.Modal(this, options)));
initialize = true;
}
// Initialize the modal.
if (initialize || (!method && !args.length)) {
data.init();
}
// Explicit method passed.
if (method) {
if (typeof data[method] === 'function') {
try {
ret = data[method].apply(data, args.slice(1));
}
catch (e) {
Drupal.throwError(e);
}
}
else {
Bootstrap.unsupported('method', method);
}
}
// No method, set options and open if necessary.
else {
data.option(options);
if (options.show && !data.isShown) {
data.show(relatedTarget);
}
}
});
// If just one element and there was a result returned for the option passed,
// then return the result. Otherwise, just return the jQuery object.
return this.length === 1 && ret !== void 0 ? ret : this;
};
// Replace the plugin constructor with the new Modal constructor.
Plugin.Constructor = Bootstrap.Modal;
// Replace the data API so that it calls $.fn.modal rather than Plugin.
// This allows sub-themes to replace the jQuery Plugin if they like with
// out having to redo all this boilerplate.
$document
.off('click.bs.modal.data-api')
.on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
var $this = $(this);
var href = $this.attr('href');
var target = $this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, '')); // strip for ie7
var $target = $document.find(target);
var options = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data());
if ($this.is('a')) e.preventDefault();
$target.one('show.bs.modal', function (showEvent) {
// Only register focus restorer if modal will actually get shown.
if (showEvent.isDefaultPrevented()) return;
$target.one('hidden.bs.modal', function () {
$this.is(':visible') && $this.trigger('focus');
});
});
$target.modal(options, this);
});
return Plugin;
});
/**
* Extend Drupal theming functions.
*/
$.extend(Drupal.theme, /** @lend Drupal.theme */ {
/**
* Theme function for a Bootstrap Modal.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal.
*/
bootstrapModal: function (variables) {
var output = '';
var settings = drupalSettings.bootstrap || {};
var defaults = {
attributes: {
class: ['modal'],
tabindex: -1,
role: 'dialog'
},
body: '',
closeButton: true,
description: {
attributes: {
class: ['help-block']
},
content: null,
position: 'before'
},
footer: '',
id: 'drupal-modal',
size: settings.modal_size ? settings.modal_size : '',
title: {
attributes: {
class: ['modal-title']
},
content: Drupal.t('Loading...'),
html: false,
tag: 'h4'
}
};
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id));
if (settings.modal_animation) {
attributes.addClass('fade');
}
// Build the modal wrapper.
output += '<div' + attributes + '>';
// Build the modal-dialog wrapper.
output += Drupal.theme('bootstrapModalDialog', _.omit(variables, 'attributes'));
// Close the modal wrapper.
output += '</div>';
// Return the constructed modal.
return output;
},
/**
* Theme function for a Bootstrap Modal dialog markup.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal close button.
*/
bootstrapModalDialog: function (variables) {
var output = '';
var defaults = {
attributes: {
class: ['modal-dialog'],
role: 'document'
},
id: 'drupal-modal'
};
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--dialog'));
if (variables.size) {
attributes.addClass(variables.size);
}
output += '<div' + attributes + '>';
// Build the modal-content wrapper.
output += Drupal.theme('bootstrapModalContent', _.omit(variables, 'attributes'));
// Close the modal-dialog wrapper.
output += '</div>';
return output;
},
/**
* Theme function for a Bootstrap Modal content markup.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal close button.
*/
bootstrapModalContent: function (variables) {
var output = '';
var defaults = {
attributes: {
class: ['modal-content']
},
id: 'drupal-modal'
};
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--content'));
// Build the modal-content wrapper.
output += '<div' + attributes + '>';
variables = _.omit(variables, 'attributes');
// Build the header wrapper and title.
output += Drupal.theme('bootstrapModalHeader', variables);
// Build the body.
output += Drupal.theme('bootstrapModalBody', variables);
// Build the footer.
output += Drupal.theme('bootstrapModalFooter', variables);
// Close the modal-content wrapper.
output += '</div>';
return output;
},
/**
* Theme function for a Bootstrap Modal body markup.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal close button.
*/
bootstrapModalBody: function (variables) {
var output = '';
var defaults = {
attributes: {
class: ['modal-body']
},
body: '',
description: {
attributes: {
class: ['help-block']
},
content: null,
position: 'before'
},
id: 'drupal-modal'
};
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--body'));
output += '<div' + attributes + '>';
if (typeof variables.description === 'string') {
variables.description = $.extend({}, defaults.description, { content: variables.description });
}
var description = variables.description;
description.attributes = Attributes.create(defaults.description.attributes).merge(description.attributes);
if (description.content && description.position === 'invisible') {
description.attributes.addClass('sr-only');
}
if (description.content && description.position === 'before') {
output += '<p' + description.attributes + '>' + description.content + '</p>';
}
output += variables.body;
if (description.content && (description.position === 'after' || description.position === 'invisible')) {
output += '<p' + description.attributes + '>' + description.content + '</p>';
}
output += '</div>';
return output;
},
/**
* Theme function for a Bootstrap Modal close button.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal close button.
*/
bootstrapModalClose: function (variables) {
var defaults = {
attributes: {
'aria-label': Drupal.t('Close'),
class: ['close'],
'data-dismiss': 'modal',
type: 'button'
}
};
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
return '<button' + attributes + '><span aria-hidden="true">&times;</span></button>';
},
/**
* Theme function for a Bootstrap Modal footer.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
* @param {boolean} [force]
* Flag to force rendering the footer, regardless if there's content.
*
* @return {string}
* The HTML for the modal footer.
*/
bootstrapModalFooter: function (variables, force) {
var output = '';
var defaults = {
attributes: {
class: ['modal-footer']
},
footer: '',
id: 'drupal-modal'
};
variables = $.extend(true, {}, defaults, variables);
if (force || variables.footer) {
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--footer'));
output += '<div' + attributes + '>';
output += variables.footer;
output += '</div>';
}
return output;
},
/**
* Theme function for a Bootstrap Modal header.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal header.
*/
bootstrapModalHeader: function (variables) {
var output = '';
var defaults = {
attributes: {
class: ['modal-header']
},
closeButton: true,
id: 'drupal-modal',
title: {
attributes: {
class: ['modal-title']
},
content: Drupal.t('Loading...'),
html: false,
tag: 'h4'
}
};
variables = $.extend(true, {}, defaults, variables);
if (typeof variables.title === 'string') {
variables.title = $.extend({}, defaults.title, { content: variables.title });
}
var title = Drupal.theme('bootstrapModalTitle', variables.title);
if (title) {
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--header'));
output += '<div' + attributes + '>';
if (variables.closeButton) {
output += Drupal.theme('bootstrapModalClose', _.omit(variables, 'attributes'));
}
output += title;
output += '</div>';
}
return output;
},
/**
* Theme function for a Bootstrap Modal title.
*
* @param {Object} [variables]
* An object containing key/value pairs of variables.
*
* @return {string}
* The HTML for the modal title.
*/
bootstrapModalTitle: function (variables) {
var output = '';
var defaults = {
attributes: {
class: ['modal-title']
},
closeButton: true,
id: 'drupal-modal',
content: Drupal.t('Loading...'),
html: false,
tag: 'h4'
};
if (typeof variables === 'string') {
variables = $.extend({}, defaults, { content: title });
}
variables = $.extend(true, {}, defaults, variables);
var attributes = Attributes.create(defaults.attributes).merge(variables.attributes);
attributes.set('id', attributes.get('id', variables.id + '--title'));
output += '<' + Drupal.checkPlain(variables.tag) + Attributes.create(defaults.attributes).merge(variables.attributes) + '>';
if (variables.closeButton) {
output += Drupal.theme('bootstrapModalClose', _.omit(variables, 'attributes'));
}
output += (variables.html ? variables.content : Drupal.checkPlain(variables.content));
output += '</' + Drupal.checkPlain(variables.tag) + '>';
return output;
}
})
});
})(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes);

View File

@@ -0,0 +1,35 @@
/**
* @file
* Attaches behavior for the Filter module.
*/
(function ($, once) {
'use strict';
function updateFilterHelpLink () {
var $link = $(this).parents('.filter-wrapper').find('.filter-help > a');
var originalLink = $link.data('originalLink');
if (!originalLink) {
originalLink = $link.attr('href');
$link.data('originalLink', originalLink);
}
$link.attr('href', originalLink + '/' + $(this).find(':selected').val());
}
$(document).on('change', '.filter-wrapper select.filter-list', updateFilterHelpLink);
/**
* Displays the guidelines of the selected text format automatically.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior for updating filter guidelines.
*/
Drupal.behaviors.filterGuidelines = {
attach: function (context) {
$(once('filter-list', '.filter-wrapper select.filter-list', context)).each(updateFilterHelpLink);
}
};
})(jQuery, once);

View File

@@ -0,0 +1,11 @@
/**
* @file
* Theme hooks for the Drupal Bootstrap base theme.
*/
(function ($, Drupal) {
if (Drupal.ImageWidgetCrop && Drupal.ImageWidgetCrop.prototype && Drupal.ImageWidgetCrop.prototype.selectors && Drupal.ImageWidgetCrop.prototype.selectors.summary) {
Drupal.ImageWidgetCrop.prototype.selectors.summary += ', > .panel-heading > .panel-title';
}
})(window.jQuery, window.Drupal);

View File

@@ -0,0 +1,24 @@
/**
* @file
* Extends core ajax_view.js.
*/
(function ($, Drupal) {
'use strict';
/**
* @method
*/
Drupal.views.ajaxView.prototype.attachExposedFormAjax = function () {
var that = this;
this.exposedFormAjax = [];
$('button[type=submit], input[type=submit], input[type=image]', this.$exposed_form).not('[data-drupal-selector=edit-reset]').each(function (index) {
var self_settings = $.extend({}, that.element_settings, {
base: $(this).attr('id'),
element: this
});
that.exposedFormAjax[index] = Drupal.ajax(self_settings);
});
};
})(jQuery, Drupal);

View File

@@ -0,0 +1,152 @@
/**
* @file
* Bootstrap Popovers.
*/
var Drupal = Drupal || {};
(function ($, Drupal, Bootstrap) {
"use strict";
var $document = $(document);
/**
* Extend the Bootstrap Popover plugin constructor class.
*/
Bootstrap.extendPlugin('popover', function (settings) {
return {
DEFAULTS: {
animation: !!settings.popover_animation,
autoClose: !!settings.popover_auto_close,
enabled: settings.popover_enabled,
html: !!settings.popover_html,
placement: settings.popover_placement,
selector: settings.popover_selector,
trigger: settings.popover_trigger,
title: settings.popover_title,
content: settings.popover_content,
delay: parseInt(settings.popover_delay, 10),
container: settings.popover_container
}
};
});
/**
* Bootstrap Popovers.
*
* @todo This should really be properly delegated if selector option is set.
*/
Drupal.behaviors.bootstrapPopovers = {
$activePopover: null,
attach: function (context) {
// Immediately return if popovers are not available.
if (!$.fn.popover || !$.fn.popover.Constructor.DEFAULTS.enabled) {
return;
}
var _this = this;
$document
.on('show.bs.popover', '[data-toggle=popover]', function () {
var $trigger = $(this);
var popover = $trigger.data('bs.popover');
// Only keep track of clicked triggers that we're manually handling.
if (popover.options.originalTrigger === 'click') {
if (_this.$activePopover && _this.getOption('autoClose') && !_this.$activePopover.is($trigger)) {
_this.$activePopover.popover('hide');
}
_this.$activePopover = $trigger;
}
})
// Unfortunately, :focusable is only made available when using jQuery
// UI. While this would be the most semantic pseudo selector to use
// here, jQuery UI may not always be loaded. Instead, just use :visible
// here as this just needs some sort of selector here. This activates
// delegate binding to elements in jQuery so it can work it's bubbling
// focus magic since elements don't really propagate their focus events.
// @see https://www.drupal.org/project/bootstrap/issues/3013236
.on('focus.bs.popover', ':visible', function (e) {
var $target = $(e.target);
if (_this.$activePopover && _this.getOption('autoClose') && !_this.$activePopover.is($target) && !$target.closest('.popover.in')[0]) {
_this.$activePopover.popover('hide');
_this.$activePopover = null;
}
})
.on('click.bs.popover', function (e) {
var $target = $(e.target);
if (_this.$activePopover && _this.getOption('autoClose') && !$target.is('[data-toggle=popover]') && !$target.closest('.popover.in')[0]) {
_this.$activePopover.popover('hide');
_this.$activePopover = null;
}
})
.on('keyup.bs.popover', function (e) {
if (_this.$activePopover && _this.getOption('autoClose') && e.which === 27) {
_this.$activePopover.popover('hide');
_this.$activePopover = null;
}
})
;
var elements = $(context).find('[data-toggle=popover]').toArray();
for (var i = 0; i < elements.length; i++) {
var $element = $(elements[i]);
var options = $.extend({}, $.fn.popover.Constructor.DEFAULTS, $element.data());
// Store the original trigger.
options.originalTrigger = options.trigger;
// If the trigger is "click", then we'll handle it manually here.
if (options.trigger === 'click') {
options.trigger = 'manual';
}
// Retrieve content from a target element.
var target = options.target || $element.is('a[href^="#"]') && $element.attr('href');
var $target = $document.find(target).clone();
if (!options.content && $target[0]) {
$target.removeClass('visually-hidden hidden').removeAttr('aria-hidden');
options.content = $target.wrap('<div/>').parent()[options.html ? 'html' : 'text']() || '';
}
// Initialize the popover.
$element.popover(options);
// Handle clicks manually.
if (options.originalTrigger === 'click') {
// To ensure the element is bound multiple times, remove any
// previously set event handler before adding another one.
$element
.off('click.drupal.bootstrap.popover')
.on('click.drupal.bootstrap.popover', function (e) {
$(this).popover('toggle');
e.preventDefault();
e.stopPropagation();
})
;
}
}
},
detach: function (context) {
// Immediately return if popovers are not available.
if (!$.fn.popover || !$.fn.popover.Constructor.DEFAULTS.enabled) {
return;
}
// Destroy all popovers.
$(context).find('[data-toggle="popover"]')
.off('click.drupal.bootstrap.popover')
.popover('destroy')
;
},
getOption: function(name, defaultValue, element) {
var $element = element ? $(element) : this.$activePopover;
var options = $.extend(true, {}, $.fn.popover.Constructor.DEFAULTS, ($element && $element.data('bs.popover') || {}).options);
if (options[name] !== void 0) {
return options[name];
}
return defaultValue !== void 0 ? defaultValue : void 0;
}
};
})(window.jQuery, window.Drupal, window.Drupal.bootstrap);

View File

@@ -0,0 +1,65 @@
/**
* @file
* Text behaviors.
*/
(function ($, Drupal, once) {
'use strict';
/**
* Auto-hide summary textarea if empty and show hide and unhide links.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches auto-hide behavior on `text-summary` events.
*/
Drupal.behaviors.textSummary = {
attach: function (context, settings) {
$(once('text-summary', '.js-text-summary', context)).each(function () {
var $widget = $(this).closest('.js-text-format-wrapper');
var $summary = $widget.find('.js-text-summary-wrapper');
var $summaryLabel = $summary.find('label').eq(0);
var $full = $widget.find('.js-text-full').closest('.js-form-item');
var $fullLabel = $full.find('label').eq(0);
// Create a placeholder label when the field cardinality is greater
// than 1.
if ($fullLabel.length === 0) {
$fullLabel = $('<label></label>').prependTo($full);
}
// Set up the edit/hide summary link.
var $link = $('<span class="field-edit-link"><button type="button" class="link link-edit-summary btn btn-default btn-xs pull-right" data-toggle="button" aria-pressed="false" autocomplete="off">' + Drupal.t('Hide summary') + '</button></span>');
var $button = $link.find('button');
var toggleClick = true;
$link.on('click', function (e) {
if (toggleClick) {
$summary.hide();
$button.html(Drupal.t('Edit summary'));
$fullLabel.before($link);
}
else {
$summary.show();
$button.html(Drupal.t('Hide summary'));
$summaryLabel.before($link);
}
e.preventDefault();
toggleClick = !toggleClick;
});
$summaryLabel.before($link);
// If no summary is set, hide the summary field.
if ($widget.find('.js-text-summary').val() === '') {
$link.trigger('click');
}
else {
$link.addClass('active');
}
});
}
};
})(jQuery, Drupal, once);

View File

@@ -0,0 +1,279 @@
(function ($, Drupal, Bootstrap, once) {
/*global jQuery:false */
/*global Drupal:false */
"use strict";
/**
* Provide vertical tab summaries for Bootstrap settings.
*/
Drupal.behaviors.bootstrapSettingSummaries = {
attach: function (context) {
var $context = $(context);
// General.
$context.find('[data-drupal-selector="edit-general"]').drupalSetSummary(function () {
var summary = [];
// Buttons.
var size = $context.find('select[name="button_size"] :selected');
if (size.val()) {
summary.push(Drupal.t('@size Buttons', {
'@size': size.text()
}));
}
// Images.
var shape = $context.find('select[name="image_shape"] :selected');
if (shape.val()) {
summary.push(Drupal.t('@shape Images', {
'@shape': shape.text()
}));
}
if ($context.find(':input[name="image_responsive"]').is(':checked')) {
summary.push(Drupal.t('Responsive Images'));
}
// Tables.
if ($context.find(':input[name="table_responsive"]').is(':checked')) {
summary.push(Drupal.t('Responsive Tables'));
}
return summary.join(', ');
});
// Components.
$context.find('[data-drupal-selector="edit-components"]').drupalSetSummary(function () {
var summary = [];
// Breadcrumbs.
var breadcrumb = parseInt($context.find('select[name="breadcrumb"]').val(), 10);
if (breadcrumb) {
summary.push(Drupal.t('Breadcrumbs'));
}
// Navbar.
var navbar = 'Navbar: ' + $context.find('select[name="navbar_position"] :selected').text();
if ($context.find('input[name="navbar_inverse"]').is(':checked')) {
navbar += ' (' + Drupal.t('Inverse') + ')';
}
summary.push(navbar);
return summary.join(', ');
});
// JavaScript.
var $jQueryUiBridge = $context.find('input[name="modal_jquery_ui_bridge"]');
$(once('bs.jquery.ui.dialog.bridge', 'input[name="modal_jquery_ui_bridge"]', context)).each(function () {
$jQueryUiBridge
.off('change.bs.jquery.ui.dialog.bridge')
.on('change.bs.jquery.ui.dialog.bridge', function (e) {
if ($jQueryUiBridge[0].checked) {
return;
}
var disable = false;
var title = Drupal.t('<p><strong>Warning: Disabling the jQuery UI Dialog bridge may have major consequences.</strong></p>');
var message = Drupal.t('<p>If you are unfamiliar with how to properly handle Bootstrap modals and jQuery UI dialogs together, it is highly recommended this remains <strong>enabled</strong>.</p> <p>Are you sure you want to disable this?</p>');
var callback = function () {
if (!disable) {
$jQueryUiBridge[0].checked = true;
$jQueryUiBridge.trigger('change');
}
};
if (!$.fn.dialog) {
disable = window.confirm(Bootstrap.stripHtml(title + ' ' + message));
callback();
}
else {
$('<div title="Disable jQuery UI Dialog Bridge?"><div class="alert alert-danger alert-sm">' + title + '</div>' + message + '</div>')
.appendTo('body')
.dialog({
modal: true,
close: callback,
buttons: [
{
text: Drupal.t('Disable'),
classes: {
'ui-button': 'btn-danger',
},
click: function () {
disable = true;
$(this).dialog('close');
}
},
{
text: 'Cancel',
primary: true,
click: function () {
$(this).dialog('close');
}
}
]
});
}
});
});
$context.find('[data-drupal-selector="edit-javascript"]').drupalSetSummary(function () {
var summary = [];
if ($context.find('input[name="modal_enabled"]').is(':checked')) {
if ($jQueryUiBridge.is(':checked')) {
summary.push(Drupal.t('Modals (Bridged)'));
}
else {
summary.push(Drupal.t('Modals'));
}
}
if ($context.find('input[name="popover_enabled"]').is(':checked')) {
summary.push(Drupal.t('Popovers'));
}
if ($context.find('input[name="tooltip_enabled"]').is(':checked')) {
summary.push(Drupal.t('Tooltips'));
}
return summary.join(', ');
});
// CDN.
$context.find('[data-drupal-selector="edit-cdn"]').drupalSetSummary(function () {
var summary = [];
var $cdnProvider = $context.find('select[name="cdn_provider"] :selected');
if ($cdnProvider.length) {
var provider = $cdnProvider.text();
var $version = $context.find('select[name="cdn_version"] :selected');
if ($version.length && $version.val().length) {
provider += ' - ' + $version.text();
var $theme = $context.find('select[name="cdn_theme"] :selected');
if ($theme.length) {
provider += ' (' + $theme.text() + ')';
}
}
else if ($cdnProvider.val() === 'custom') {
var $urls = $context.find('textarea[name="cdn_custom"]');
var urls = ($urls.val() + '').split(/\r\n|\n/).filter(Boolean);
provider += ' (' + Drupal.formatPlural(urls.length, '1 URL', '@count URLs') + ')';
}
summary.push(provider);
}
return summary.join(', ');
});
// Advanced.
$context.find('[data-drupal-selector="edit-advanced"]').drupalSetSummary(function () {
var summary = [];
var deprecations = [];
if ($context.find('input[name="include_deprecated"]').is(':checked')) {
deprecations.push(Drupal.t('Included'));
}
deprecations.push($context.find('input[name="suppress_deprecated_warnings"]').is(':checked') ? Drupal.t('Warnings Suppressed') : Drupal.t('Warnings Shown'));
summary.push(Drupal.t('Deprecations: @value', {
'@value': deprecations.join(', '),
}));
return summary.join(', ');
});
}
};
/**
* Provide Bootstrap Bootswatch preview.
*/
Drupal.behaviors.bootstrapBootswatchPreview = {
attach: function (context) {
var $context = $(context);
var $preview = $context.find('#bootstrap-theme-preview');
$(once('bootstrap-theme-preview', '#bootstrap-theme-preview', context)).each(function () {
// Construct the "Bootstrap Theme" preview here since it's not actually
// a Bootswatch theme, but rather one provided by Bootstrap itself.
// Unfortunately getbootstrap.com does not have HTTPS enabled, so the
// preview image cannot be protocol relative.
// @todo Make protocol relative if/when Bootstrap enables HTTPS.
$preview.append('<a id="bootstrap-theme-preview-bootstrap_theme" class="bootswatch-preview element-invisible" href="https://getbootstrap.com/docs/3.4/examples/theme/" target="_blank"><img class="img-responsive" src="//getbootstrap.com/docs/3.4/examples/screenshots/theme.jpg" alt="' + Drupal.t('Preview of the Bootstrap theme') + '" /></a>');
// Retrieve the Bootswatch theme preview images.
// @todo This should be moved into PHP.
$.ajax({
url: 'https://bootswatch.com/api/3.json',
dataType: 'json',
success: function (json) {
var themes = json.themes;
for (var i = 0, len = themes.length; i < len; i++) {
$preview.append('<a id="bootstrap-theme-preview-' + themes[i].name.toLowerCase() + '" class="bootswatch-preview element-invisible" href="' + themes[i].preview + '" target="_blank"><img class="img-responsive" src="' + themes[i].thumbnail.replace(/^http:/, 'https:') + '" alt="' + Drupal.t('Preview of the @title Bootswatch theme', { '@title': themes[i].name }) + '" /></a>');
}
},
complete: function () {
$preview.parent().find('select[name="cdn_theme"]').bind('change', function () {
$preview.find('.bootswatch-preview').addClass('visually-hidden');
var theme = $(this).val();
if (theme && theme.length) {
$preview.find('#bootstrap-theme-preview-' + theme).removeClass('visually-hidden');
}
}).change();
}
});
});
}
};
/**
* Provide Bootstrap navbar preview.
*/
Drupal.behaviors.bootstrapContainerPreview = {
attach: function (context) {
var $context = $(context);
var $container = $context.find('#edit-container');
$(once('container-preview', '#edit-container', context)).each(function () {
$container.find('[name="fluid_container"]').on('change.bootstrap', function () {
if ($(this).is(':checked')) {
$context.find('.container').removeClass('container').addClass('container-fluid');
}
else {
$context.find('.container-fluid').removeClass('container-fluid').addClass('container');
}
});
});
}
};
/**
* Provide Bootstrap navbar preview.
*/
Drupal.behaviors.bootstrapNavbarPreview = {
attach: function (context) {
var $context = $(context);
var $preview = $context.find('#edit-navbar');
$(once('navbar', '#edit-navbar', context)).each(function () {
var $body = $context.find('body');
var $navbar = $context.find('#navbar.navbar');
$preview.find('select[name="navbar_position"]').bind('change', function () {
var $position = $(this).find(':selected').val();
$navbar.removeClass('navbar-fixed-bottom navbar-fixed-top navbar-static-top container');
if ($position.length) {
$navbar.addClass('navbar-'+ $position);
}
else {
$navbar.addClass('container');
}
// Apply appropriate classes to body.
$body.removeClass('navbar-is-fixed-top navbar-is-fixed-bottom navbar-is-static-top');
switch ($position) {
case 'fixed-top':
$body.addClass('navbar-is-fixed-top');
break;
case 'fixed-bottom':
$body.addClass('navbar-is-fixed-bottom');
break;
case 'static-top':
$body.addClass('navbar-is-static-top');
break;
}
});
$preview.find('input[name="navbar_inverse"]').bind('change', function () {
$navbar.toggleClass('navbar-inverse navbar-default');
});
});
}
};
})(jQuery, Drupal, Drupal.bootstrap, once);

View File

@@ -0,0 +1,180 @@
/**
* @file
* Theme hooks for the Drupal Bootstrap base theme.
*/
(function ($, Drupal, Bootstrap, Attributes) {
/**
* Fallback for theming an icon if the Icon API module is not installed.
*/
if (!Drupal.icon) Drupal.icon = { bundles: {} };
if (!Drupal.theme.icon || Drupal.theme.prototype.icon) {
$.extend(Drupal.theme, /** @lends Drupal.theme */ {
/**
* Renders an icon.
*
* @param {string} bundle
* The bundle which the icon belongs to.
* @param {string} icon
* The name of the icon to render.
* @param {object|Attributes} [attributes]
* An object of attributes to also apply to the icon.
*
* @returns {string}
*/
icon: function (bundle, icon, attributes) {
if (!Drupal.icon.bundles[bundle]) return '';
attributes = Attributes.create(attributes).addClass('icon').set('aria-hidden', 'true');
icon = Drupal.icon.bundles[bundle](icon, attributes);
return '<span' + attributes + '></span>';
}
});
}
/**
* Callback for modifying an icon in the "bootstrap" icon bundle.
*
* @param {string} icon
* The icon being rendered.
* @param {Attributes} attributes
* Attributes object for the icon.
*/
Drupal.icon.bundles.bootstrap = function (icon, attributes) {
attributes.addClass(['glyphicon', 'glyphicon-' + icon]);
};
/**
* Add necessary theming hooks.
*/
$.extend(Drupal.theme, /** @lends Drupal.theme */ {
/**
* Renders a Bootstrap AJAX glyphicon throbber.
*
* @returns {string}
*/
ajaxThrobber: function () {
return Drupal.theme('bootstrapIcon', 'refresh', {'class': ['ajax-throbber', 'glyphicon-spin'] });
},
/**
* Renders a button element.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button. If it contains one of:
* - value: The label of the button.
* - context: The context type of Bootstrap button, can be one of:
* - default
* - primary
* - success
* - info
* - warning
* - danger
* - link
*
* @returns {string}
*/
button: function (attributes) {
attributes = Attributes.create(attributes).addClass('btn');
var context = attributes.get('context', 'default');
var label = attributes.get('value', '');
attributes.remove('context').remove('value');
if (!attributes.hasClass(['btn-default', 'btn-primary', 'btn-success', 'btn-info', 'btn-warning', 'btn-danger', 'btn-link'])) {
attributes.addClass('btn-' + Bootstrap.checkPlain(context));
}
// Attempt to, intelligently, provide a default button "type".
if (!attributes.exists('type')) {
attributes.set('type', attributes.hasClass('form-submit') ? 'submit' : 'button');
}
return '<button' + attributes + '>' + label + '</button>';
},
/**
* Alias for "button" theme hook.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button.
*
* @see Drupal.theme.button()
*
* @returns {string}
*/
btn: function (attributes) {
return Drupal.theme('button', attributes);
},
/**
* Renders a button block element.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button.
*
* @see Drupal.theme.button()
*
* @returns {string}
*/
'btn-block': function (attributes) {
return Drupal.theme('button', Attributes.create(attributes).addClass('btn-block'));
},
/**
* Renders a large button element.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button.
*
* @see Drupal.theme.button()
*
* @returns {string}
*/
'btn-lg': function (attributes) {
return Drupal.theme('button', Attributes.create(attributes).addClass('btn-lg'));
},
/**
* Renders a small button element.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button.
*
* @see Drupal.theme.button()
*
* @returns {string}
*/
'btn-sm': function (attributes) {
return Drupal.theme('button', Attributes.create(attributes).addClass('btn-sm'));
},
/**
* Renders an extra small button element.
*
* @param {object|Attributes} attributes
* An object of attributes to apply to the button.
*
* @see Drupal.theme.button()
*
* @returns {string}
*/
'btn-xs': function (attributes) {
return Drupal.theme('button', Attributes.create(attributes).addClass('btn-xs'));
},
/**
* Renders a glyphicon.
*
* @param {string} name
* The name of the glyphicon.
* @param {object|Attributes} [attributes]
* An object of attributes to apply to the icon.
*
* @returns {string}
*/
bootstrapIcon: function (name, attributes) {
return Drupal.theme('icon', 'bootstrap', name, attributes);
}
});
})(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes);

View File

@@ -0,0 +1,59 @@
/**
* @file
* Bootstrap Tooltips.
*/
var Drupal = Drupal || {};
(function ($, Drupal, Bootstrap) {
"use strict";
/**
* Extend the Bootstrap Tooltip plugin constructor class.
*/
Bootstrap.extendPlugin('tooltip', function (settings) {
return {
DEFAULTS: {
animation: !!settings.tooltip_animation,
enabled: settings.tooltip_enabled,
html: !!settings.tooltip_html,
placement: settings.tooltip_placement,
selector: settings.tooltip_selector,
trigger: settings.tooltip_trigger,
delay: parseInt(settings.tooltip_delay, 10),
container: settings.tooltip_container
}
};
});
/**
* Bootstrap Tooltips.
*
* @todo This should really be properly delegated if selector option is set.
*/
Drupal.behaviors.bootstrapTooltips = {
attach: function (context) {
// Immediately return if tooltips are not available.
if (!$.fn.tooltip || !$.fn.tooltip.Constructor.DEFAULTS.enabled) {
return;
}
var elements = $(context).find('[data-toggle="tooltip"]').toArray();
for (var i = 0; i < elements.length; i++) {
var $element = $(elements[i]);
var options = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, $element.data());
$element.tooltip(options);
}
},
detach: function (context) {
// Immediately return if tooltips are not available.
if (!$.fn.tooltip || !$.fn.tooltip.Constructor.DEFAULTS.enabled) {
return;
}
// Destroy all tooltips.
$(context).find('[data-toggle="tooltip"]').tooltip('destroy');
}
};
})(window.jQuery, window.Drupal, window.Drupal.bootstrap);

26
themes/bootstrap/logo.svg Normal file
View File

@@ -0,0 +1,26 @@
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="32px" height="32px"
viewBox="0 0 32 32" preserveAspectRatio="none">
<g>
<image width="32" height="32" xlink:href="data:image/png;base64,
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
bWFnZVJlYWR5ccllPAAAAohJREFUeNrEl1tIVGEQx12L0BQjoXwpoRfxIQUJuhAIvVRLIRQELYj1
oKDgpQgVNQhBChQfLFDLoovVS0RQYuqLPUgFgTcQia5CSBBERUiKuv0+GGGJPd/l7Nkc+DNn98w3
8/++mXNmTigajaasp4T8Ejhx+GIWavOTkbaviRBITWBtIbgDkfT1InAGHAGDkNj2X1NAwFLU3ZgN
fATnSMezpBIgcAaqWRBPHoFLEJlNVgpWRC943D8FJiDaA3IDOQEc5aNK5Ocou3vDf3u5HgFbNEu/
gzrs7/smQKAa1BWQEfN3L06ruFcmdWCSa+A8a1acUkCAetTVf4IrqeReBIf3pPhMojbR41QDBDiO
atc4LRU9ZVk7FXKaZgLyhus2OExbS6FDAV+OV5jxTqAa7DQ4e4qzTegiBwKZoFFLAKdqZ+UGR8Pk
vwu9A7i+AU8TY6vuBNTjtcvgJB0nJyGhCvAgeO9AIBsc0hHYb+GkGDyGxG1ITHIdlmfeVop1BAoc
HJ2FxENIqBOodVhXoCOw3TGnEfVCgsQDrmcc0uBJYNVHR20S3R/EPDDvw0c+p6AK94Wl/aKOwLTP
uSIPfAHLFrYfdAReJzBZ2abvpY7ABHjrg8AcyAEbDXZL4LknAapZGdxyDD4vpPdZ2A7JC0zbC3pl
R7bSJ70+YjFNtVoNJNKObQbMcXAA7AFjhhGvA6INVvMAhgOoBkNwNQsck5Z8wxB8wGuQ9VwEiQ7U
BSmcWPkGOiXnv2QS3q0Jru6rCWrZ71BaKH0/JLXxCmd/+F8RuKkJ/gO0YNsd6IcJgcPSfI56mLxT
3RJcJ/jnoD9MNsg3oZobcqWxqGf/J/gkdTFJ4N9J/zoOSv4KMAC++teHNUg1rQAAAABJRU5ErkJg
gg==
"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,10 @@
includes:
- phar://phpstan.phar/conf/bleedingEdge.neon
parameters:
level: 1
paths:
- .
ignoreErrors:
# new static() is a best practice in Drupal, so we cannot fix that.
- "#^Unsafe usage of new static#"

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,62 @@
<?php
/**
* @file
* Locates the Drupal root directory and bootstraps the kernel.
*/
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;
// Immediately return if classes are discoverable (already booted).
if (class_exists('\Drupal\Core\DrupalKernel') && class_exists('\Drupal')) {
return \Drupal::service('kernel');
}
/**
*
*/
function _find_autoloader($dir) {
if (file_exists($autoloadFile = $dir . '/autoload.php') || file_exists($autoloadFile = $dir . '/vendor/autoload.php')) {
return include_once $autoloadFile;
}
elseif (empty($dir) || $dir === DIRECTORY_SEPARATOR) {
return FALSE;
}
return _find_autoloader(dirname($dir));
}
$autoloader = _find_autoloader(empty($_SERVER['PWD']) ? getcwd() : $_SERVER['PWD']);
if (!$autoloader || !class_exists('\Drupal\Core\DrupalKernel')) {
print "This script must be invoked inside a Drupal 8 environment. Unable to continue.\n";
exit();
}
// Create a DrupalKernel instance.
DrupalKernel::bootEnvironment();
$kernel = new DrupalKernel('prod', $autoloader);
// Need to change the current working directory to the actual root path.
// This is needed in case the script is initiated inside a sub-directory.
chdir($kernel->getAppRoot());
// Initialize settings, this requires reflection since its a protected method.
$request = Request::createFromGlobals();
$initializeSettings = new \ReflectionMethod($kernel, 'initializeSettings');
$initializeSettings->setAccessible(TRUE);
$initializeSettings->invokeArgs($kernel, [$request]);
// Boot the kernel.
$kernel->boot();
$kernel->preHandle($request);
// Due to a core bug, the theme handler has to be invoked to register theme
// namespaces with the autoloader.
// @todo Remove once installed_extensions makes its way into core.
// @see https://www.drupal.org/project/drupal/issues/2941757
$container = $kernel->getContainer();
if (!$container->has('installed_extensions')) {
$container->get('theme_handler')->listInfo();
}
return $kernel;

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env php
<?php
/**
* @file
* Generates the markdown documentation for all available theme settings.
*/
/**
* Note: this script is intended to be executed independently via PHP, e.g.:
* $ ./scripts/gen-theme-setting-docs.php
*/
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
use Drupal\bootstrap\Plugin\Setting\SettingInterface;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\Serialization\Yaml;
$kernel = require_once __DIR__ . '/bootstrap.php';
$bootstrap = Bootstrap::getTheme('bootstrap');
/** @var \Drupal\bootstrap\Plugin\Setting\SettingInterface[] $settings */
$settings = array_filter($bootstrap->getSettingPlugin(NULL, TRUE), function (SettingInterface $setting) {
return !!$setting->getGroups();
});
// Populate the variables with settings.
$variables = ['groups' => []];
$deprecatedSettings = [];
$replacementPairs = [
'&quot;' => '"',
'\n' => "\n",
];
foreach ($settings as $id => $setting) {
$defaultValue = $setting->getDefaultValue();
$deprecated = FALSE;
if ($setting instanceof DeprecatedSettingInterface) {
$newSetting = $setting->getDeprecatedReplacementSetting()->getPluginId();
$deprecated = [
'reason' => new FormattableMarkup($setting->getDeprecatedReason(), []),
'replacement' => new FormattableMarkup('<a href="#@anchor">@setting</a>', [
'@anchor' => Html::cleanCssIdentifier($newSetting),
'@setting' => $newSetting,
]),
'version' => new FormattableMarkup($setting->getDeprecatedVersion(), []),
];
}
$data = [
'id' => $id,
'description' => new FormattableMarkup(strtr($setting->getDescription(), $replacementPairs), []),
'defaultValue' => $defaultValue !== NULL ? new FormattableMarkup(strtr(trim(Yaml::encode([$id => $defaultValue])), $replacementPairs), []) : NULL,
'deprecated' => $deprecated,
];
// Defer adding deprecated settings.
if ($deprecated) {
$deprecatedSettings[$id] = $data;
}
else {
// Only get the first two groups (we don't need 3rd, or more, levels).
$header = implode(' > ', array_slice(array_filter($setting->getGroups()), 0, 2, FALSE));
$variables['groups'][$header][$id] = $data;
}
}
// Add Deprecated settings last (special table).
if ($deprecatedSettings) {
$variables['deprecated'] = $deprecatedSettings;
}
$docsPath = "{$bootstrap->getPath()}/docs";
// Render the settings.
$output = Bootstrap::renderCustomTemplate("{$docsPath}/theme-settings.twig", $variables);
// Save the generated output to the appropriate file.
$result = Bootstrap::putContents("{$docsPath}/Theme-Settings.md", $output, '<!-- THEME SETTINGS GENERATION START -->', '<!-- THEME SETTINGS GENERATION END -->');
if ($result) {
echo 'Successfully generated theme documentation!';
exit(0);
}
echo 'Unable to generate theme documentation!';
exit(1);

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\bootstrap\Annotation;
use Drupal\Component\Annotation\PluginID;
/**
* Defines a BootstrapAlter annotation object.
*
* Plugin Namespace: "Plugin/Alter".
*
* @see \Drupal\bootstrap\Plugin\AlterInterface
* @see \Drupal\bootstrap\Plugin\AlterManager
* @see plugin_api
*
* @Annotation
*
* @ingroup plugins_alter
*/
class BootstrapAlter extends PluginID {}

View File

@@ -0,0 +1,60 @@
<?php
namespace Drupal\bootstrap\Annotation;
use Doctrine\Common\Annotations\AnnotationException;
use Drupal\Component\Annotation\AnnotationBase;
/**
* Defines a BootstrapConstant annotation object.
*
* @Annotation
*
* @ingroup utility
*/
class BootstrapConstant extends AnnotationBase {
/**
* The stored constant value.
*
* @var mixed
*/
protected $value;
/**
* {@inheritdoc}
*/
public function __construct(array $values) {
$string = $values['value'];
// Handle classes.
if (strpos($string, '::') !== FALSE) {
list($class, $constant) = explode('::', $string);
try {
$reflection = new \ReflectionClass($class);
if ($reflection->hasConstant($constant)) {
$this->value = $reflection->getConstant($constant);
return;
}
}
catch (\ReflectionException $e) {
}
}
// Handle procedural constants.
if (!$this->value && defined($string)) {
$this->value = constant($string);
return;
}
throw AnnotationException::semanticalErrorConstants($this->value);
}
/**
* {@inheritdoc}
*/
public function get() {
return $this->value;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\bootstrap\Annotation;
use Drupal\Component\Annotation\PluginID;
/**
* Defines a BootstrapForm annotation object.
*
* Plugin Namespace: "Plugin/Form".
*
* @see \Drupal\bootstrap\Plugin\FormInterface
* @see \Drupal\bootstrap\Plugin\FormManager
* @see plugin_api
*
* @Annotation
*
* @ingroup plugins_form
*/
class BootstrapForm extends PluginID {}

View File

@@ -0,0 +1,18 @@
<?php
namespace Drupal\bootstrap\Annotation;
/**
* Defines a BootstrapPreprocess annotation object.
*
* Plugin Namespace: "Plugin/Preprocess".
*
* @see \Drupal\bootstrap\Plugin\PreprocessInterface
* @see \Drupal\bootstrap\Plugin\PreprocessManager
* @see plugin_api
*
* @Annotation
*
* @ingroup plugins_preprocess
*/
class BootstrapPreprocess extends PluginCallback {}

View File

@@ -0,0 +1,18 @@
<?php
namespace Drupal\bootstrap\Annotation;
/**
* Defines a BootstrapPrerender annotation object.
*
* Plugin Namespace: "Plugin/Prerender".
*
* @see \Drupal\bootstrap\Plugin\PrerenderInterface
* @see \Drupal\bootstrap\Plugin\PrerenderManager
* @see plugin_api
*
* @Annotation
*
* @ingroup plugins_prerender
*/
class BootstrapPrerender extends PluginCallback {}

View File

@@ -0,0 +1,18 @@
<?php
namespace Drupal\bootstrap\Annotation;
/**
* Defines a BootstrapProcess annotation object.
*
* Plugin Namespace: "Plugin/Process".
*
* @see \Drupal\bootstrap\Plugin\ProcessInterface
* @see \Drupal\bootstrap\Plugin\ProcessManager
* @see plugin_api
*
* @Annotation
*
* @ingroup plugins_process
*/
class BootstrapProcess extends PluginCallback {}

View File

@@ -0,0 +1,110 @@
<?php
namespace Drupal\bootstrap\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a BootstrapProvider annotation object.
*
* Plugin Namespace: "Plugin/Provider".
*
* @see \Drupal\bootstrap\Plugin\ProviderInterface
* @see \Drupal\bootstrap\Plugin\ProviderManager
* @see \Drupal\bootstrap\Theme::getCdnProviders()
* @see \Drupal\bootstrap\Theme::getCdnProvider()
* @see plugin_api
*
* @Annotation
*
* @ingroup plugins_provider
*/
class BootstrapProvider extends Plugin {
/**
* An API URL used to retrieve data for the provider.
*
* @var string
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
protected $api = '';
/**
* An array of CSS assets.
*
* @var array
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
protected $css = [];
/**
* A description about the provider.
*
* @var string
*/
protected $description = '';
/**
* A flag determining whether or not the API request has failed.
*
* @var bool
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
protected $error = FALSE;
/**
* A flag determining whether or not data has been manually imported.
*
* @var bool
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
protected $imported = FALSE;
/**
* An array of JavaScript assets.
*
* @var array
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
protected $js = [];
/**
* A human-readable label.
*
* @var string
*/
protected $label = '';
/**
* An associative array of minified CSS and JavaScript assets.
*
* @var array
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
protected $min = ['css' => [], 'js' => []];
/**
* An array of themes supported by the provider.
*
* @var array
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
protected $themes = [];
/**
* An array of versions supported by the provider.
*
* @var array
*
* @deprecated in 8.x-3.18, will be removed in a future release.
*/
protected $versions = [];
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Drupal\bootstrap\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a BootstrapSetting annotation object.
*
* Plugin Namespace: "Plugin/Setting".
*
* @see \Drupal\bootstrap\Plugin\SettingInterface
* @see \Drupal\bootstrap\Plugin\SettingManager
* @see plugin_api
*
* @Annotation
*
* @Attributes({
*
* @Attribute("defaultValue", type = "mixed", required = true),
*
* @Attribute("type", type = "string", required = true),
* })
*
* @ingroup plugins_setting
*/
class BootstrapSetting extends Plugin {
/**
* The setting's description.
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
/**
* The setting's default value.
*
* @var mixed
*/
public $defaultValue;
/**
* The setting's groups.
*
* @var \Drupal\Core\Annotation\Translation[]
*/
public $groups = [];
/**
* The setting's title.
*
* @var \Drupal\Core\Annotation\Translation
*/
public $title;
/**
* The setting's type.
*
* @var string
*/
public $type;
/**
* The setting's see references.
*
* @var array
*/
public $see = [];
/**
* {@inheritdoc}
*/
public function __construct($values) {
if (!isset($values['groups'])) {
$values['groups'] = ['general' => t('General')];
}
parent::__construct($values);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Drupal\bootstrap\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a BootstrapUpdate annotation object.
*
* Plugin Namespace: "Plugin/Update".
*
* @see \Drupal\bootstrap\Plugin\UpdateInterface
* @see \Drupal\bootstrap\Plugin\UpdateManager
* @see plugin_api
*
* @Annotation
*
* @ingroup plugins_update
*/
class BootstrapUpdate extends Plugin {
/**
* The schema version.
*
* @var int
*/
public $id = '';
/**
* A short human-readable label.
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label = '';
/**
* A detailed description.
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description = '';
/**
* Level of severity. Should be one of: default, danger, info, warning.
*
* @var string
*/
public $severity = 'default';
/**
* Indicates whether or not the update should apply only to itself.
*
* Only the theme that implemented the plugin and none of its sub-themes.
*
* @var bool
*/
public $private = FALSE;
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Drupal\bootstrap\Annotation;
use Drupal\bootstrap\Bootstrap;
use Drupal\Component\Annotation\AnnotationInterface;
use Drupal\Component\Annotation\PluginID;
/**
* Defines a Plugin annotation object that just contains an ID.
*
* @Annotation
*
* @ingroup utility
*/
class PluginCallback extends PluginID {
/**
* The plugin ID.
*
* When an annotation is given no key, 'value' is assumed by Doctrine.
*
* @var string
*/
public $value;
/**
* Flag that determines how to add the plugin to a callback array.
*
* @var \Drupal\bootstrap\Annotation\BootstrapConstant
*
* Must be one of the following constants:
* - \Drupal\bootstrap\Bootstrap::CALLBACK_APPEND
* - \Drupal\bootstrap\Bootstrap::CALLBACK_PREPEND
* - \Drupal\bootstrap\Bootstrap::CALLBACK_REPLACE_APPEND
* - \Drupal\bootstrap\Bootstrap::CALLBACK_REPLACE_PREPEND
* Use with @ BootstrapConstant annotation.
*
* @see \Drupal\bootstrap\Bootstrap::addCallback()
*/
public $action = Bootstrap::CALLBACK_APPEND;
/**
* A callback to replace.
*
* @var string
*/
public $replace = FALSE;
/**
* {@inheritdoc}
*/
public function get() {
$definition = parent::get();
$parent_properties = array_keys($definition);
$parent_properties[] = 'value';
// Merge in the defined properties.
$reflection = new \ReflectionClass($this);
foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
$name = $property->getName();
if (in_array($name, $parent_properties)) {
continue;
}
$value = $property->getValue($this);
if ($value instanceof AnnotationInterface) {
$value = $value->get();
}
$definition[$name] = $value;
}
return $definition;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Drupal\bootstrap\BcSupport;
/**
* Provides a blank interface for BC interfaces when no suitable one exists.
*
* @deprecated in bootstrap:8.x-3.22 and is removed from bootstrap:5.0.0.
* No replacement.
* @see https://www.drupal.org/project/bootstrap/issues/3096963
*/
interface BcAliasedInterface {
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Drupal\bootstrap\BcSupport;
if (!interface_exists('\Drupal\Core\Security\TrustedCallbackInterface')) {
/* @noinspection PhpIgnoredClassAliasDeclaration */
class_alias('\Drupal\bootstrap\BcSupport\BcAliasedInterface', '\Drupal\Core\Security\TrustedCallbackInterface');
}
use Drupal\Core\Security\TrustedCallbackInterface as CoreTrustedCallbackInterface;
/**
* Interface to declare trusted callbacks.
*
* @deprecated in bootstrap:8.x-3.22 and is removed from bootstrap:5.0.0.
* Use \Drupal\Core\Security\TrustedCallbackInterface instead.
* @see https://www.drupal.org/project/bootstrap/issues/3096963
* @see \Drupal\Core\Security\TrustedCallbackInterface
*/
interface TrustedCallbackInterface extends CoreTrustedCallbackInterface {
/**
* {@inheritdoc}
*/
public static function trustedCallbacks();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
<?php
namespace Drupal\bootstrap;
/**
* Interface DeprecatedInterface.
*/
interface DeprecatedInterface {
/**
* The reason for deprecation.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* A TranslatableMarkup object.
*/
public function getDeprecatedReason();
/**
* The code that replaces the deprecated functionality.
*
* @return string|false
* The replacement code location or FALSE if there is no replacement.
*/
public function getDeprecatedReplacement();
/**
* The version this was deprecated in.
*
* @return string
* A version string.
*/
public function getDeprecatedVersion();
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Drupal\bootstrap\Plugin\Alter;
/**
* Defines the interface for an object oriented alter.
*
* @ingroup plugins_alter
*/
interface AlterInterface {
/**
* Alters data for a specific hook_TYPE_alter() implementation.
*
* @param mixed $data
* The variable that will be passed to hook_TYPE_alter() implementations to
* be altered. The type of this variable depends on the value of the $type
* argument. For example, when altering a 'form', $data will be a structured
* array. When altering a 'profile', $data will be an object.
* @param mixed $context1
* (optional) An additional variable that is passed by reference.
* @param mixed $context2
* (optional) An additional variable that is passed by reference. If more
* context needs to be provided to implementations, then this should be an
* associative array as described above.
*/
public function alter(&$data, &$context1 = NULL, &$context2 = NULL);
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Drupal\bootstrap\Plugin\Alter;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Plugin\PluginBase;
use Drupal\bootstrap\Plugin\PrerenderManager;
use Drupal\bootstrap\Plugin\ProcessManager;
/**
* Implements hook_element_info_alter().
*
* @ingroup plugins_alter
*
* @BootstrapAlter("element_info")
*/
class ElementInfo extends PluginBase implements AlterInterface {
/**
* {@inheritdoc}
*/
public function alter(&$types, &$context1 = NULL, &$context2 = NULL) {
// Sort the types for easier debugging.
ksort($types, SORT_NATURAL);
$extra_variables = Bootstrap::extraVariables();
$process_manager = new ProcessManager($this->theme);
$pre_render_manager = new PrerenderManager($this->theme);
foreach (array_keys($types) as $type) {
$element = &$types[$type];
// By default, the "checkboxes" and "radios" element types invoke
// CompositeFormElementTrait::preRenderCompositeFormElement which wraps
// the element in a fieldset and thus ultimately a panel. This isn't
// (usually) the desired effect for these elements, so to avoid rendering
// them as Bootstrap panels, the #bootstrap_panel should be set to FALSE
// by default. This allows those who wish to opt back in to do so.
if ($type === 'checkboxes' || $type === 'radios') {
$element['#bootstrap_panel'] = FALSE;
}
// Core does not actually use the "description_display" property on the
// "details" or "fieldset" element types because the positioning of the
// description is never used in core templates. However, the form builder
// automatically applies the value of "after", thus making it impossible
// to detect a valid value later in the rendering process. It looks better
// for the "details" and "fieldset" element types to display as "before".
// @see \Drupal\Core\Form\FormBuilder::doBuildForm()
if ($type === 'details' || $type === 'fieldset') {
$element['#description_display'] = 'before';
$element['#panel_type'] = 'default';
}
// Add extra variables as defaults to all elements.
foreach ($extra_variables as $key => $value) {
if (!isset($element["#$key"])) {
$element["#$key"] = $value;
}
}
// Only continue if the type isn't "form" (as it messes up AJAX).
if ($type !== 'form') {
$regex = "/^$type/";
// Add necessary #process callbacks.
$element['#process'][] = [get_class($process_manager), 'process'];
$definitions = $process_manager->getDefinitionsLike($regex);
foreach ($definitions as $definition) {
Bootstrap::addCallback($element['#process'], [$definition['class'], 'process'], $definition['replace'], $definition['action']);
}
// Add necessary #pre_render callbacks.
$element['#pre_render'][] = [get_class($pre_render_manager), 'preRender'];
foreach ($pre_render_manager->getDefinitionsLike($regex) as $definition) {
Bootstrap::addCallback($element['#pre_render'], [$definition['class'], 'preRender'], $definition['replace'], $definition['action']);
}
}
}
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Drupal\bootstrap\Plugin\Alter;
use Drupal\bootstrap\Plugin\PluginBase;
/**
* Implements hook_library_info_alter().
*
* @ingroup plugins_alter
*
* @BootstrapAlter("library_info")
*/
class LibraryInfo extends PluginBase implements AlterInterface {
/**
* {@inheritdoc}
*/
public function alter(&$libraries, &$extension = NULL, &$context2 = NULL) {
$livereload = $this->theme->livereloadUrl();
// Disable preprocess on all CSS/JS if "livereload" is enabled.
if ($livereload) {
$this->processLibrary($libraries, function (&$info, &$key, $type) {
if ($type === 'css' || $type === 'js') {
$info['preprocess'] = FALSE;
}
});
}
if ($extension === 'bootstrap') {
// Alter the "livereload.js" placeholder with the correct URL.
if ($livereload) {
$libraries['livereload']['js'][$livereload] = $libraries['livereload']['js']['livereload.js'];
unset($libraries['livereload']['js']['livereload.js']);
}
// Alter the framework library based on currently set CDN Provider.
$this->theme->getCdnProvider()->alterFrameworkLibrary($libraries['framework']);
// Add back deprecated library dependencies that are only available in D8.
if (((int) substr(\Drupal::VERSION, 0, 1)) < 9) {
$libraries['drupal.vertical-tabs']['dependencies'][] = 'core/matchmedia';
}
}
// Core replacements.
elseif ($extension === 'core') {
// Replace core dialog/jQuery UI implementations with Bootstrap Modals.
if ($this->theme->getSetting('modal_enabled')) {
// Replace dependencies if using bridge so jQuery UI is not loaded
// and remove dialog.jquery-ui.js since the dialog widget isn't loaded.
if ($this->theme->getSetting('modal_jquery_ui_bridge')) {
// Remove core's jquery.ui.dialog dependency.
$key = array_search('core/jquery.ui.dialog', $libraries['drupal.dialog']['dependencies']);
if ($key !== FALSE) {
unset($libraries['drupal.dialog']['dependencies'][$key]);
}
// Remove core's dialog.jquery-ui.js.
unset($libraries['drupal.dialog']['js']['misc/dialog/dialog.jquery-ui.js']);
// Remove CKEditor dialog fix as it assumes jquery ui dialogs are used
unset($libraries['drupal.dialog']['js']['modules/ckeditor5/js/ckeditor5.dialog.fix.js']);
// Add the Modal jQuery UI Bridge.
$libraries['drupal.dialog']['dependencies'][] = 'bootstrap/dialog';
$libraries['drupal.dialog']['dependencies'][] = 'bootstrap/modal.jquery.ui.bridge';
}
// Otherwise, just append the modal.
else {
$libraries['drupal.dialog']['dependencies'][] = 'bootstrap/modal';
$libraries['drupal.dialog']['dependencies'][] = 'bootstrap/dialog';
}
}
}
}
/**
* Processes library definitions.
*
* @param array $libraries
* The libraries array, passed by reference.
* @param callable $callback
* The callback to perform processing on the library.
*/
public function processLibrary(array &$libraries, callable $callback) {
foreach ($libraries as &$library) {
foreach ($library as $type => $definition) {
if (is_array($definition)) {
$modified = [];
// CSS needs special handling since it contains grouping.
if ($type === 'css') {
foreach ($definition as $group => $files) {
foreach ($files as $key => $info) {
call_user_func_array($callback, [&$info, &$key, $type]);
$modified[$group][$key] = $info;
}
}
}
else {
foreach ($definition as $key => $info) {
call_user_func_array($callback, [&$info, &$key, $type]);
$modified[$key] = $info;
}
}
$library[$type] = $modified;
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\bootstrap\Plugin\Alter;
use Drupal\bootstrap\Plugin\PluginBase;
/**
* Implements hook_page_attachments_alter().
*
* @ingroup plugins_alter
*
* @BootstrapAlter("page_attachments")
*/
class PageAttachments extends PluginBase implements AlterInterface {
/**
* {@inheritdoc}
*/
public function alter(&$attachments, &$context1 = NULL, &$context2 = NULL) {
if ($this->theme->livereloadUrl()) {
$attachments['#attached']['library'][] = 'bootstrap/livereload';
}
if ($this->theme->getSetting('popover_enabled')) {
$attachments['#attached']['library'][] = 'bootstrap/popover';
}
if ($this->theme->getSetting('tooltip_enabled')) {
$attachments['#attached']['library'][] = 'bootstrap/tooltip';
}
$attachments['#attached']['drupalSettings']['bootstrap'] = $this->theme->drupalSettings();
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace Drupal\bootstrap\Plugin\Alter;
use Composer\Semver\Comparator;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Plugin\PreprocessManager;
use Drupal\Core\Theme\Registry;
/**
* Extends the theme registry to override and use protected functions.
*
* @todo Refactor into a proper theme.registry service replacement in a
* bootstrap_core sub-module once this theme can add it as a dependency.
*
* @see https://www.drupal.org/node/474684
*
* @ingroup plugins_alter
*
* @BootstrapAlter("theme_registry")
*/
class ThemeRegistry extends Registry implements AlterInterface {
/**
* The currently set Bootstrap theme object.
*
* Cannot use "$theme" because this is the Registry's ActiveTheme object.
*
* @var \Drupal\bootstrap\Theme
*/
protected $currentTheme;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
// This is technically a plugin constructor, but because we wish to use the
// protected methods of the Registry class, we must extend from it. Thus,
// to properly construct the extended Registry object, we must pass the
// arguments it would normally get from the service container to "fake" it.
if (!isset($configuration['theme'])) {
$configuration['theme'] = Bootstrap::getTheme();
}
$this->currentTheme = $configuration['theme'];
if (Comparator::greaterThanOrEqualTo(\Drupal::VERSION, '10.3.0-dev')) {
parent::__construct(
\Drupal::root(),
\Drupal::service('cache.default'),
\Drupal::service('lock'),
\Drupal::service('module_handler'),
\Drupal::service('theme_handler'),
\Drupal::service('theme.initialization'),
\Drupal::service('cache.bootstrap'),
\Drupal::service('extension.list.module'),
\Drupal::service('kernel'),
$this->currentTheme->getName(),
);
}
else {
parent::__construct(
\Drupal::root(),
\Drupal::service('cache.default'),
\Drupal::service('lock'),
\Drupal::service('module_handler'),
\Drupal::service('theme_handler'),
\Drupal::service('theme.initialization'),
\Drupal::service('cache.bootstrap'),
\Drupal::service('extension.list.module'),
$this->currentTheme->getName(),
);
}
$this->setThemeManager(\Drupal::service('theme.manager'));
$this->init();
}
/**
* {@inheritdoc}
*/
public function alter(&$cache, &$context1 = NULL, &$context2 = NULL) {
// Sort the registry alphabetically (for easier debugging).
ksort($cache);
// Add extra variables to all theme hooks.
$extra_variables = Bootstrap::extraVariables();
foreach (array_keys($cache) as $hook) {
// Skip theme hooks that don't set variables.
if (!isset($cache[$hook]['variables'])) {
continue;
}
$cache[$hook]['variables'] += $extra_variables;
}
// Ensure paths to templates are set properly. This allows templates to
// be moved around in a theme without having to constantly ensuring that
// the theme's hook_theme() definitions have the correct static "path" set.
foreach ($this->currentTheme->getAncestry() as $ancestor) {
$current_theme = $ancestor->getName() === $this->currentTheme->getName();
$theme_path = $ancestor->getPath();
// Scan entire theme root path.
// @see https://www.drupal.org/project/bootstrap/issues/2951575
foreach ($ancestor->fileScan('/\.html\.twig$/') as $file) {
$hook = str_replace('-', '_', str_replace('.html.twig', '', $file->filename));
$path = dirname($file->uri);
$incomplete = !isset($cache[$hook]) || strrpos($hook, '__');
// Create a new theme hook. This primarily happens when theme hook
// suggestion templates are created. To prevent the new hook from
// inheriting parent hook's "template", it must be manually set here.
// @see https://www.drupal.org/node/2871551
if (!isset($cache[$hook])) {
$cache[$hook] = [
'template' => str_replace('.html.twig', '', $file->filename),
];
}
// Always ensure that "path", "type" and "theme path" are properly set.
$cache[$hook]['path'] = $path;
$cache[$hook]['type'] = $current_theme ? 'theme' : 'base_theme';
$cache[$hook]['theme path'] = $theme_path;
// Flag incomplete.
if ($incomplete) {
$cache[$hook]['incomplete preprocess functions'] = TRUE;
}
}
}
// Discover all the theme's preprocess plugins.
$preprocess_manager = new PreprocessManager($this->currentTheme);
$plugins = $preprocess_manager->getDefinitions();
ksort($plugins, SORT_NATURAL);
// Iterate over the preprocess plugins.
foreach ($plugins as $plugin_id => $definition) {
$incomplete = !isset($cache[$plugin_id]) || strrpos($plugin_id, '__');
if (!isset($cache[$plugin_id])) {
$cache[$plugin_id] = [];
}
array_walk($cache, function (&$info, $hook) use ($plugin_id, $definition) {
if ($hook === $plugin_id || strpos($hook, $plugin_id . '__') === 0) {
if (!isset($info['preprocess functions'])) {
$info['preprocess functions'] = [];
}
// Due to a limitation in \Drupal\Core\Theme\ThemeManager::render,
// callbacks must be functions and not classes. We always specify
// "bootstrap_preprocess" here and then assign the plugin ID to a
// separate property that we can later intercept and properly invoke.
// @todo Revisit if/when preprocess callbacks can be any callable.
Bootstrap::addCallback($info['preprocess functions'], 'bootstrap_preprocess', $definition['replace'], $definition['action']);
$info['preprocess functions'] = array_unique($info['preprocess functions']);
$info['bootstrap preprocess'] = $plugin_id;
}
});
if ($incomplete) {
$cache[$plugin_id]['incomplete preprocess functions'] = TRUE;
}
}
// Allow core to post process.
$this->postProcessExtension($cache, $this->theme);
}
}

View File

@@ -0,0 +1,372 @@
<?php
namespace Drupal\bootstrap\Plugin\Alter;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Plugin\PluginBase;
use Drupal\bootstrap\Utility\Variables;
use Drupal\Core\Entity\EntityInterface;
/**
* Implements hook_theme_suggestions_alter().
*
* @ingroup plugins_alter
*
* @BootstrapAlter("theme_suggestions")
*/
class ThemeSuggestions extends PluginBase implements AlterInterface {
/**
* The element types that should be converted into Panel markup.
*
* @var array
*/
protected $bootstrapPanelTypes = ['details', 'fieldset'];
/**
* An element object provided in the variables array, may not be set.
*
* @var \Drupal\bootstrap\Utility\Element|false
*/
protected $element;
/**
* The theme hook invoked.
*
* @var string
*/
protected $hook;
/**
* The theme hook suggestions, exploded by the "__" delimiter.
*
* @var array
*/
protected $hookSuggestions;
/**
* The types of elements to ignore for the "input__form_control" suggestion.
*
* @var array
*/
protected $ignoreFormControlTypes = ['checkbox', 'hidden', 'radio'];
/**
* The original "hook" value passed via hook_theme_suggestions_alter().
*
* @var string
*/
protected $originalHook;
/**
* The array of suggestions to return.
*
* @var array
*/
protected $suggestions;
/**
* The variables array object passed via hook_theme_suggestions_alter().
*
* @var \Drupal\bootstrap\Utility\Variables
*/
protected $variables;
/**
* {@inheritdoc}
*/
public function alter(&$suggestions, &$variables = [], &$hook = NULL) {
// This is intentionally backwards. The "original" theme hook is actually
// the hook being invoked. The provided $hook (to the alter) is the watered
// down version of said original hook.
$this->hook = !empty($variables['theme_hook_original']) ? $variables['theme_hook_original'] : $hook;
$this->hookSuggestions = explode('__', $this->hook);
$this->originalHook = $hook;
$this->suggestions = $suggestions;
$this->variables = Variables::create($variables);
$this->element = $this->variables->element;
// Processes the necessary theme hook suggestions.
$this->processSuggestions();
// Ensure the list of suggestions is unique.
$suggestions = array_unique($this->suggestions);
}
/***************************************************************************
* Dynamic alter methods.
***************************************************************************/
/**
* Dynamic alter method for "input".
*/
protected function alterInput() {
if ($this->element && $this->element->isButton()) {
$hook = 'input__button';
if ($this->element->getProperty('split')) {
$hook .= '__split';
}
$this->addSuggestion($hook);
}
elseif ($this->element && !$this->element->isType($this->ignoreFormControlTypes)) {
$this->addSuggestion('input__form_control');
}
}
/**
* Dynamic alter method for "links__dropbutton".
*/
protected function alterLinksDropbutton() {
// Remove the 'dropbutton' suggestion.
array_shift($this->hookSuggestions);
$this->addSuggestion('bootstrap_dropdown');
}
/**
* Dynamic alter method for "user".
*
* @see https://www.drupal.org/node/2828634
* @see https://www.drupal.org/node/2808481
* @todo Remove/refactor once core issue is resolved.
*/
protected function alterUser() {
$this->addSuggestionsForEntity('user');
}
/***************************************************************************
* Protected methods.
***************************************************************************/
/**
* Adds suggestions based on an array of hooks.
*
* @param string|string[] $hook
* A single theme hook suggestion or an array of theme hook suggestions.
*/
protected function addSuggestion($hook) {
$hooks = (array) $hook;
foreach ($hooks as $hook) {
$suggestions = $this->buildSuggestions($hook);
foreach ($suggestions as $suggestion) {
$this->suggestions[] = $suggestion;
}
}
}
/**
* Adds "bundle" and "view mode" suggestions for an entity.
*
* This is a helper method because core's implementation of theme hook
* suggestions on entities is inconsistent.
*
* @param string $entity_type
* Optional. A specific type of entity to look for.
* @param string $prefix
* Optional. A prefix (like "entity") to use. It will automatically be
* appended with the "__" separator.
*
* @see https://www.drupal.org/node/2808481
*
* @todo Remove/refactor once core issue is resolved.
*/
protected function addSuggestionsForEntity($entity_type = 'entity', $prefix = '') {
// Immediately return if there is no element.
if (!$this->element) {
return;
}
// Extract the entity.
if ($entity = $this->getEntityObject($entity_type)) {
$entity_type_id = $entity->getEntityTypeId();
$suggestions = [];
// Only add the entity type identifier if there's a prefix.
if (!empty($prefix)) {
$prefix .= '__';
$suggestions[] = $prefix . '__' . $entity_type_id;
}
// View mode.
if ($view_mode = preg_replace('/[^A-Za-z0-9]+/', '_', $this->element->getProperty('view_mode'))) {
$suggestions[] = $prefix . $entity_type_id . '__' . $view_mode;
// Bundle.
if ($entity->getEntityType()->hasKey('bundle')) {
$suggestions[] = $prefix . $entity_type_id . '__' . $entity->bundle();
$suggestions[] = $prefix . $entity_type_id . '__' . $entity->bundle() . '__' . $view_mode;
}
}
// Add suggestions.
if ($suggestions) {
$this->addSuggestion($suggestions);
}
}
}
/**
* Builds a list of suggestions.
*
* @param string $hook
* The theme hook suggestion to build.
*
* @return array
* An list of theme hook suggestions.
*/
protected function buildSuggestions($hook) {
$suggestions = [];
$hook_suggestions = $this->hookSuggestions;
// Replace the first hook suggestion with $hook.
array_shift($hook_suggestions);
array_unshift($suggestions, $hook);
$suggestions = [];
while ($hook_suggestions) {
$suggestions[] = $hook . '__' . implode('__', $hook_suggestions);
array_pop($hook_suggestions);
}
// Append the base hook.
$suggestions[] = $hook;
// Return the suggestions, reversed.
return array_reverse($suggestions);
}
/**
* Retrieves the methods to invoke to process the theme hook suggestion.
*
* @return array
* An indexed array of methods to be invoked.
*/
protected function getAlterMethods() {
// Retrieve cached theme hook suggestion alter methods.
$cache = $this->theme->getCache('theme_hook_suggestions');
if ($cache->has($this->hook)) {
return $cache->get($this->hook);
}
// Convert snake_cased hook suggestions into lowerCamelCase alter methods.
$methods = [];
$hook_suggestions = array_map('\Drupal\Component\Utility\Unicode::ucfirst', $this->hookSuggestions);
while ($hook_suggestions) {
// In order to provide backwards compatibility with sub-themes that used
// the previous malformed method names, both of the method names need to
// be checked.
// @see https://www.drupal.org/project/bootstrap/issues/3008004
// @todo Only use the last method name and remove array in 8.x-4.x.
$methodNames = [
'alter' . implode('', $hook_suggestions),
'alter' . implode('', array_map('\Drupal\Component\Utility\Unicode::ucfirst', explode('_', implode('', $hook_suggestions)))),
];
foreach (array_unique($methodNames) as $method) {
if (method_exists($this, $method)) {
$methods[] = $method;
}
}
array_pop($hook_suggestions);
}
// Reverse the methods.
$methods = array_reverse($methods);
// Cache the methods.
$cache->set($this->hook, $methods);
return $methods;
}
/**
* Extracts the entity from the element(s) passed in the Variables object.
*
* @param string $entity_type
* Optional. The entity type to attempt to retrieve.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The extracted entity, NULL if entity could not be found.
*/
protected function getEntityObject($entity_type = 'entity') {
// Immediately return if there is no element.
if (!$this->element) {
return NULL;
}
// Attempt to retrieve the provided element type.
$entity = $this->element->getProperty($entity_type);
// If the provided entity type doesn't exist, check to see if a generic
// "entity" property was used instead.
if ($entity_type !== 'entity' && (!$entity || !($entity instanceof EntityInterface))) {
$entity = $this->element->getProperty('entity');
}
// Only return the entity if it's the proper object.
return $entity instanceof EntityInterface ? $entity : NULL;
}
/**
* Processes the necessary theme hook suggestions.
*/
protected function processSuggestions() {
// Add special hook suggestions for Bootstrap panels.
if ((in_array($this->originalHook, $this->bootstrapPanelTypes)) && $this->element && $this->element->getProperty('bootstrap_panel', TRUE)) {
$this->addSuggestion('bootstrap_panel');
}
// Retrieve any dynamic alter methods.
$methods = $this->getAlterMethods();
foreach ($methods as $method) {
$this->$method();
}
}
/***************************************************************************
* Deprecated methods (DO NOT USE).
***************************************************************************/
/**
* Adds "bundle" and "view mode" suggestions for an entity.
*
* @param array $suggestions
* The suggestions array, this is ignored.
* @param \Drupal\bootstrap\Utility\Variables $variables
* The variables object, this is ignored.
* @param string $entity_type
* Optional. A specific type of entity to look for.
* @param string $prefix
* Optional. A prefix (like "entity") to use. It will automatically be
* appended with the "__" separator.
*
* @deprecated Since 8.x-3.2. Will be removed in a future release.
*
* @see \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions::addSuggestionsForEntity
*/
public function addEntitySuggestions(array &$suggestions, Variables $variables, $entity_type = 'entity', $prefix = '') {
Bootstrap::deprecated();
$this->addSuggestionsForEntity($entity_type, $prefix);
}
/**
* Extracts the entity from the element(s) passed in the Variables object.
*
* @param \Drupal\bootstrap\Utility\Variables $variables
* The Variables object, this is ignored.
* @param string $entity_type
* Optional. The entity type to attempt to retrieve.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The extracted entity, NULL if entity could not be found.
*
* @deprecated Since 8.x-3.2. Will be removed in a future release.
*
* @see \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions::getEntityObject
*/
public function getEntity(Variables $variables, $entity_type = 'entity') {
Bootstrap::deprecated();
return $this->getEntityObject($entity_type);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Drupal\bootstrap\Plugin;
use Drupal\bootstrap\Theme;
/**
* Manages discovery and instantiation of Bootstrap hook alters.
*
* @ingroup plugins_alter
*/
class AlterManager extends PluginManager {
/**
* Constructs a new \Drupal\bootstrap\Plugin\AlterManager object.
*
* @param \Drupal\bootstrap\Theme $theme
* The theme to use for discovery.
*/
public function __construct(Theme $theme) {
parent::__construct($theme, 'Plugin/Alter', 'Drupal\bootstrap\Plugin\Alter\AlterInterface', 'Drupal\bootstrap\Annotation\BootstrapAlter');
$this->setCacheBackend(\Drupal::cache('discovery'), 'theme:' . $theme->getName() . ':alter', $this->getCacheTags());
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Drupal\bootstrap\Plugin\Form;
use Drupal\bootstrap\Plugin\PluginBase;
use Drupal\bootstrap\Utility\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Base form alter class.
*
* @ingroup plugins_form
*/
class FormBase extends PluginBase implements FormInterface {
/**
* {@inheritdoc}
*/
public function alterForm(array &$form, FormStateInterface $form_state, $form_id = NULL) {
$this->alterFormElement(Element::create($form), $form_state, $form_id);
}
/**
* {@inheritdoc}
*/
public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {}
/**
* {@inheritdoc}
*/
public static function submitForm(array &$form, FormStateInterface $form_state) {
static::submitFormElement(Element::create($form), $form_state);
}
/**
* {@inheritdoc}
*/
public static function submitFormElement(Element $form, FormStateInterface $form_state) {}
/**
* {@inheritdoc}
*/
public static function validateForm(array &$form, FormStateInterface $form_state) {
static::validateFormElement(Element::create($form), $form_state);
}
/**
* {@inheritdoc}
*/
public static function validateFormElement(Element $form, FormStateInterface $form_state) {}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Drupal\bootstrap\Plugin\Form;
use Drupal\bootstrap\Utility\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines the interface for an object oriented form alter.
*
* @ingroup plugins_form
*/
interface FormInterface {
/**
* The alter method to store the code.
*
* @param array $form
* Nested array of form elements that comprises the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $form_id
* String representing the name of the form itself. Typically this is the
* name of the function that generated the form.
*/
public function alterForm(array &$form, FormStateInterface $form_state, $form_id = NULL);
/**
* The alter method to store the code.
*
* @param \Drupal\bootstrap\Utility\Element $form
* The Element object that comprises the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $form_id
* String representing the name of the form itself. Typically this is the
* name of the function that generated the form.
*/
public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL);
/**
* Form validation handler.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function validateForm(array &$form, FormStateInterface $form_state);
/**
* Form validation handler.
*
* @param \Drupal\bootstrap\Utility\Element $form
* The Element object that comprises the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function validateFormElement(Element $form, FormStateInterface $form_state);
/**
* Form submission handler.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function submitForm(array &$form, FormStateInterface $form_state);
/**
* Form submission handler.
*
* @param \Drupal\bootstrap\Utility\Element $form
* The Element object that comprises the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function submitFormElement(Element $form, FormStateInterface $form_state);
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Drupal\bootstrap\Plugin\Form;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Utility\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_FORM_ID_alter().
*
* @ingroup plugins_form
*
* @BootstrapForm("node_preview_form_select")
*/
class NodePreviewFormSelect extends FormBase {
/**
* {@inheritdoc}
*/
public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
$form->addClass(['form-inline', 'bg-info', 'text-center', 'clearfix']);
// Backlink.
$options = $form->backlink->getProperty('options', []);
$form->backlink->addClass(isset($options['attributes']['class']) ? $options['attributes']['class'] : []);
$form->backlink->addClass(['btn', 'btn-info', 'pull-left']);
$form->backlink->setButtonSize();
$form->backlink->setIcon(Bootstrap::glyphicon('chevron-left'));
// Ensure the UUID is set.
if ($uuid = $form->uuid->getProperty('value')) {
$options['query'] = ['uuid' => $uuid];
}
// Override the options attributes.
$options['attributes'] = $form->backlink->getAttributes()->getArrayCopy();
$form->backlink->setProperty('options', $options);
// View mode.
$form->view_mode->addClass('pull-right', $form::WRAPPER);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Drupal\bootstrap\Plugin\Form;
use Drupal\bootstrap\Utility\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_FORM_ID_alter().
*
* @ingroup plugins_form
*
* @BootstrapForm("search_block_form")
*/
class SearchBlockForm extends FormBase {
/**
* {@inheritdoc}
*/
public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
$form->actions->submit->setProperty('icon_only', TRUE);
$form->keys->setProperty('input_group_button', TRUE);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\bootstrap\Plugin\Form;
use Drupal\bootstrap\Utility\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_FORM_ID_alter().
*
* @ingroup plugins_form
*
* @BootstrapForm("search_form")
*/
class SearchForm extends FormBase {
/**
* {@inheritdoc}
*/
public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
$form->advanced->setProperty('collapsible', TRUE);
$form->advanced->setProperty('collapsed', TRUE);
$form->basic->removeClass('container-inline');
$form->basic->submit->setProperty('icon_only', TRUE);
$form->basic->keys->setProperty('input_group_button', TRUE);
}
}

View File

@@ -0,0 +1,271 @@
<?php
namespace Drupal\bootstrap\Plugin\Form;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Plugin\Setting\DeprecatedSettingInterface;
use Drupal\bootstrap\Utility\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_system_theme_settings_alter().
*
* @ingroup plugins_form
* @ingroup plugins_setting
*
* @BootstrapForm("system_theme_settings")
*/
class SystemThemeSettings extends FormBase implements FormInterface {
/**
* {@inheritdoc}
*/
public function alterFormElement(Element $form, FormStateInterface $form_state, $form_id = NULL) {
$theme = $this->getTheme($form, $form_state);
if (!$theme) {
return;
}
// Creates the necessary groups (vertical tabs) for a Bootstrap based theme.
$this->createGroups($form, $form_state);
// Iterate over all setting plugins and add them to the form.
foreach ($theme->getSettingPlugin() as $setting) {
// Skip settings that shouldn't be created automatically.
if (!$setting->autoCreateFormElement()) {
continue;
}
$setting->alterForm($form->getArray(), $form_state);
}
}
/**
* Sets up the vertical tab groupings.
*
* @param \Drupal\bootstrap\Utility\Element $form
* The Element object that comprises the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function createGroups(Element $form, FormStateInterface $form_state) {
// Vertical tabs for global settings provided by core or contrib modules.
if (!isset($form['global'])) {
$form['global'] = [
'#type' => 'vertical_tabs',
'#weight' => -9,
'#prefix' => '<h2><small>' . t('Override Global Settings') . '</small></h2>',
];
}
// Iterate over existing children and move appropriate ones to global group.
foreach ($form->children() as $child) {
if ($child->isType(['details', 'fieldset']) && !$child->hasProperty('group')) {
$child->setProperty('type', 'details');
$child->setProperty('group', 'global');
}
}
// Provide the necessary default groups.
$form['bootstrap'] = [
'#type' => 'vertical_tabs',
'#attached' => ['library' => ['bootstrap/theme-settings']],
'#prefix' => '<h2><small>' . t('Bootstrap Settings') . '</small></h2>',
'#weight' => -10,
];
$groups = [
'general' => t('General'),
'components' => t('Components'),
'javascript' => t('JavaScript'),
'cdn' => t('CDN'),
'advanced' => t('Advanced'),
];
foreach ($groups as $group => $title) {
$form[$group] = [
'#type' => 'details',
'#title' => $title,
'#group' => 'bootstrap',
];
// Show a button to reset cached HTTP requests.
if ($group === 'advanced') {
$cache = \Drupal::keyValueExpirable('theme:' . $this->theme->getName() . ':http');
$count = count($cache->getAll());
$form[$group]['reset_http_request_cache'] = [
'#type' => 'item',
'#title' => $this->t('Cached HTTP requests: @count', ['@count' => $count]),
'#weight' => 100,
'#smart_description' => FALSE,
'#description' => $this->t('All external HTTP requests initiated by this theme are subject to caching. Cacheability is determined automatically based on a manually passed TTL value by the initiator or if there is a "max-age" response header present. These cached requests will persist through cache rebuilds and will only be requested again once they have expired. If you believe there is some request not being properly retrieved, you can manually reset this cache here.'),
'#description_display' => 'before',
'#prefix' => '<div id="reset-http-request-cache">',
'#suffix' => '</div>',
];
$form[$group]['reset_http_request_cache']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Reset HTTP Request Cache'),
'#description' => $this->t('Note: this will not reset any cached CDN data; see "Advanced Cache" in the "CDN" section.'),
'#prefix' => '<div>',
'#suffix' => '</div>',
'#submit' => [
[get_class($this), 'submitResetHttpRequestCache'],
],
'#ajax' => [
'callback' => [get_class($this), 'ajaxResetHttpRequestCache'],
'wrapper' => 'reset-http-request-cache',
],
];
}
}
}
/**
* Submit callback for resetting the cached HTTP requests.
*
* @param array $form
* Nested array of form elements that comprise the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function submitResetHttpRequestCache(array $form, FormStateInterface $form_state) {
$form_state->setRebuild();
$theme = SystemThemeSettings::getTheme(Element::create($form), $form_state);
$cache = \Drupal::keyValueExpirable('theme:' . $theme->getName() . ':http');
$cache->deleteAll();
}
/**
* AJAX callback for reloading the cached HTTP request markup.
*
* @param array $form
* Nested array of form elements that comprise the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function ajaxResetHttpRequestCache(array $form, FormStateInterface $form_state) {
return $form['advanced']['reset_http_request_cache'];
}
/**
* Retrieves the currently selected theme on the settings form.
*
* @param \Drupal\bootstrap\Utility\Element $form
* The Element object that comprises the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return \Drupal\bootstrap\Theme|false
* The currently selected theme object or FALSE if not a Bootstrap theme.
*/
public static function getTheme(Element $form, FormStateInterface $form_state) {
$build_info = $form_state->getBuildInfo();
$theme = isset($build_info['args'][0]) ? Bootstrap::getTheme($build_info['args'][0]) : FALSE;
// Do not continue if the theme is not Bootstrap specific.
if (!$theme || !$theme->isBootstrap()) {
unset($form['#submit'][0]);
unset($form['#validate'][0]);
}
return $theme;
}
/**
* {@inheritdoc}
*/
public static function submitFormElement(Element $form, FormStateInterface $form_state) {
$theme = self::getTheme($form, $form_state);
if (!$theme) {
return;
}
$cache_tags = [];
$save = FALSE;
$settings = $theme->settings();
$rebuild_cdn_assets = FALSE;
// Iterate over all setting plugins and manually save them since core's
// process is severely limiting and somewhat broken.
foreach ($theme->getSettingPlugin() as $name => $setting) {
// Skip saving deprecated settings.
if ($setting instanceof DeprecatedSettingInterface) {
$form_state->unsetValue($name);
continue;
}
// Allow the setting to participate in the form submission process.
// Must call the "submitForm" method in case any setting actually uses it.
// It should, in turn, invoke "submitFormElement", if the setting that
// overrides it is implemented properly.
$setting->submitForm($form->getArray(), $form_state);
// Retrieve the submitted value.
$value = $form_state->getValue($name);
// Trim any new lines and convert to simple new line breaks.
$definition = $setting->getPluginDefinition();
if (isset($definition['type']) && $definition['type'] === 'textarea' && is_string($value)) {
$value = implode("\n", array_filter(array_map('trim', preg_split("/\r\n|\n/", $value))));
}
// Determine if the setting has a new value that overrides the original.
// Ignore the schemas "setting" because it's handled by UpdateManager.
if ($name !== 'schemas' && $settings->overridesValue($name, $value)) {
// Set the new value.
$settings->set($name, $value);
// Retrieve the cache tags for the setting.
$cache_tags = array_unique(array_merge($setting->getCacheTags()));
// A CDN setting has changed, flag that CDN assets should be rebuilt.
if (strpos($name, 'cdn') === 0) {
$rebuild_cdn_assets = TRUE;
}
// Flag the save.
$save = TRUE;
}
// Remove value from the form state object so core doesn't re-save it.
$form_state->unsetValue($name);
}
// Save the settings, if needed.
if ($save) {
// Remove any cached CDN assets so they can be rebuilt.
if ($rebuild_cdn_assets) {
$settings->clear('cdn_cache');
}
$settings->save();
// Invalidate necessary cache tags.
if ($cache_tags) {
\Drupal::service('cache_tags.invalidator')->invalidateTags($cache_tags);
}
// Clear our internal theme cache so it can be rebuilt properly.
$theme->getCache('settings')->deleteAll();
}
}
/**
* {@inheritdoc}
*/
public static function validateFormElement(Element $form, FormStateInterface $form_state) {
$theme = self::getTheme($form, $form_state);
if (!$theme) {
return;
}
// Iterate over all setting plugins and allow them to participate.
foreach ($theme->getSettingPlugin() as $setting) {
// Allow the setting to participate in the form validation process.
// Must call the "validateForm" method in case any setting actually uses
// it. It should, in turn, invoke "validateFormElement", if the setting
// that overrides it is implemented properly.
$setting->validateForm($form->getArray(), $form_state);
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Drupal\bootstrap\Plugin;
use Drupal\bootstrap\Theme;
/**
* Manages discovery and instantiation of Bootstrap form alters.
*
* @ingroup plugins_form
*/
class FormManager extends PluginManager {
/**
* Constructs a new \Drupal\bootstrap\Plugin\FormManager object.
*
* @param \Drupal\bootstrap\Theme $theme
* The theme to use for discovery.
*/
public function __construct(Theme $theme) {
parent::__construct($theme, 'Plugin/Form', 'Drupal\bootstrap\Plugin\Form\FormInterface', 'Drupal\bootstrap\Annotation\BootstrapForm');
$this->setCacheBackend(\Drupal::cache('discovery'), 'theme:' . $theme->getName() . ':form', $this->getCacheTags());
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\bootstrap\Plugin\Markdown\AllowedHtml;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Theme\ActiveTheme;
use Drupal\markdown\Plugin\Markdown\AllowedHtmlInterface;
use Drupal\markdown\Plugin\Markdown\ParserInterface;
/**
* Provides additional Bootstrap specific allowed HTML for Markdown.
*
* @MarkdownAllowedHtml(
* id = "bootstrap",
* description = @Translation("Provide common global attributes that are useful when dealing with Bootstrap specific output."),
* )
*/
class Bootstrap extends PluginBase implements AllowedHtmlInterface {
/**
* {@inheritdoc}
*/
public function allowedHtmlTags(ParserInterface $parser, ActiveTheme $activeTheme = NULL) {
return [
'*' => [
'data-complete-text' => TRUE,
'data-container' => TRUE,
'data-content' => TRUE,
'data-dismiss' => TRUE,
'data-loading-text' => TRUE,
'data-parent' => TRUE,
'data-placement' => TRUE,
'data-ride' => TRUE,
'data-slide' => TRUE,
'data-slide-to' => TRUE,
'data-spy' => TRUE,
'data-target' => TRUE,
'data-toggle' => TRUE,
],
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Drupal\bootstrap\Plugin;
use Drupal\Core\Plugin\PluginBase as CorePluginBase;
use Drupal\bootstrap\Bootstrap;
/**
* Base class for an update.
*
* @ingroup utility
*/
class PluginBase extends CorePluginBase {
/**
* The currently set theme object.
*
* @var \Drupal\bootstrap\Theme
*/
protected $theme;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
$this->theme = Bootstrap::getTheme(isset($configuration['theme']) ? $configuration['theme'] : NULL);
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace Drupal\bootstrap\Plugin;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Theme;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Base class for Bootstrap plugin managers.
*
* @ingroup utility
*/
class PluginManager extends DefaultPluginManager {
/**
* The current theme.
*
* @var \Drupal\bootstrap\Theme
*/
protected $theme;
/**
* The theme handler to check if theme exists.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* The theme manager to invoke alter hooks.
*
* @var \Drupal\Core\Theme\ThemeManager
*/
protected $themeManager;
/**
* Creates the discovery object.
*
* @param \Drupal\bootstrap\Theme $theme
* The theme to use for discovery.
* @param string|bool $subdir
* The plugin's subdirectory, for example Plugin/views/filter.
* @param string|null $plugin_interface
* (optional) The interface each plugin should implement.
* @param string $plugin_definition_annotation_name
* (optional) Name of the annotation that contains the plugin definition.
* Defaults to 'Drupal\Component\Annotation\Plugin'.
*/
public function __construct(Theme $theme, $subdir, $plugin_interface = NULL, $plugin_definition_annotation_name = 'Drupal\Component\Annotation\Plugin') {
// Get the active theme.
$this->theme = $theme;
// Determine the namespaces to search for.
$namespaces = [];
foreach ($theme->getAncestry() as $ancestor) {
$namespaces['Drupal\\' . $ancestor->getName()] = [DRUPAL_ROOT . '/' . $ancestor->getPath() . '/src'];
}
$this->namespaces = new \ArrayObject($namespaces);
$this->subdir = $subdir;
$this->pluginDefinitionAnnotationName = $plugin_definition_annotation_name;
$this->pluginInterface = $plugin_interface;
$this->themeHandler = \Drupal::service('theme_handler');
$this->themeManager = \Drupal::service('theme.manager');
}
/**
* {@inheritdoc}
*/
protected function alterDefinitions(&$definitions) {
if ($this->alterHook) {
$this->themeManager->alter($this->alterHook, $definitions);
}
}
/**
* {@inheritdoc}
*/
public function createInstance($plugin_id, array $configuration = []) {
if (!isset($configuration['theme'])) {
$configuration['theme'] = $this->theme;
}
return parent::createInstance($plugin_id, $configuration);
}
/**
* {@inheritdoc}
*/
protected function findDefinitions() {
$definitions = parent::findDefinitions();
$this->sortDefinitions($definitions);
return $definitions;
}
/**
* Retrieves the cache tags used to invalidate caches.
*
* @return array
* An indexed array of cache tags.
*/
public function getCacheTags() {
return [Bootstrap::CACHE_TAG];
}
/**
* Retrieves all definitions where the plugin ID matches a certain criteria.
*
* @param string $regex
* The regex pattern to match.
*
* @return array[]
* An array of plugin definitions (empty array if no definitions were
* found). Keys are plugin IDs.
*/
public function getDefinitionsLike($regex) {
$definitions = [];
foreach ($this->getDefinitions() as $plugin_id => $definition) {
if (preg_match($regex, $plugin_id)) {
$definitions[$plugin_id] = $definition;
}
}
ksort($definitions, SORT_NATURAL);
return $definitions;
}
/**
* {@inheritdoc}
*/
protected function providerExists($provider) {
return $this->themeHandler->themeExists($provider);
}
/**
* Sorts the plugin definitions.
*
* @param array $definitions
* The unsorted plugin definitions, passed by reference.
*/
protected function sortDefinitions(array &$definitions) {
uasort($definitions, ['\Drupal\Component\Utility\SortArray', 'sortByWeightElement']);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\bootstrap\Plugin\Preprocess;
use Drupal\bootstrap\Bootstrap;
use Drupal\bootstrap\Utility\Element;
use Drupal\bootstrap\Utility\Variables;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
/**
* Pre-processes variables for the "bootstrap_carousel" theme hook.
*
* @ingroup plugins_preprocess
*
* @BootstrapPreprocess("bootstrap_carousel")
*/
class BootstrapCarousel extends PreprocessBase implements PreprocessInterface {
/**
* {@inheritdoc}
*/
protected function preprocessVariables(Variables $variables) {
// Retrieve the ID, generating one if needed.
$id = $variables->getAttribute('id', Html::getUniqueId($variables->offsetGet('id', 'bootstrap-carousel')));
unset($variables['id']);
// Build slides.
foreach ($variables->slides as $key => &$slide) {
if (!isset($slide['attributes'])) {
$slide['attributes'] = [];
}
$slide['attributes'] = new Attribute($slide['attributes']);
}
// Build controls.
if ($variables->controls) {
$left_icon = Bootstrap::glyphicon('chevron-left');
$right_icon = Bootstrap::glyphicon('chevron-right');
$url = Url::fromUserInput("#$id");
$variables->controls = [
'left' => [
'#type' => 'link',
'#title' => new FormattableMarkup(Element::create($left_icon)->renderPlain() . '<span class="sr-only">@text</span>', ['@text' => t('Previous')]),
'#url' => $url,
'#attributes' => [
'class' => ['left', 'carousel-control'],
'role' => 'button',
'data-slide' => 'prev',
],
],
'right' => [
'#type' => 'link',
'#title' => new FormattableMarkup(Element::create($right_icon)->renderPlain() . '<span class="sr-only">@text</span>', ['@text' => t('Next')]),
'#url' => $url,
'#attributes' => [
'class' => ['right', 'carousel-control'],
'role' => 'button',
'data-slide' => 'next',
],
],
];
}
// Build indicators.
if ($variables->indicators) {
$variables->indicators = [
'#theme' => 'item_list__bootstrap_carousel_indicators',
'#list_type' => 'ol',
'#items' => array_keys($variables->slides),
'#context' => [
'target' => "#$id",
'start_index' => $variables->start_index,
],
];
}
// Ensure all attributes are proper objects.
$this->preprocessAttributes();
}
}

View File

@@ -0,0 +1,202 @@
<?php
namespace Drupal\bootstrap\Plugin\Preprocess;
use Drupal\bootstrap\Utility\Crypt;
use Drupal\bootstrap\Utility\Element;
use Drupal\bootstrap\Utility\Variables;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
/**
* Pre-processes variables for the "bootstrap_dropdown" theme hook.
*
* @ingroup plugins_preprocess
*
* @BootstrapPreprocess("bootstrap_dropdown")
*/
class BootstrapDropdown extends PreprocessBase implements PreprocessInterface {
/**
* {@inheritdoc}
*/
protected function preprocessVariables(Variables $variables) {
$this->preprocessLinks($variables);
$toggle = Element::create($variables->toggle);
$toggle->setProperty('split', $variables->split);
// Convert the items into a proper item list.
$variables->items = [
'#theme' => 'item_list__dropdown',
'#items' => $variables->items,
'#context' => [
'alignment' => $variables->alignment,
],
];
// Ensure all attributes are proper objects.
$this->preprocessAttributes();
}
/**
* Preprocess links in the variables array to convert them from dropbuttons.
*
* @param \Drupal\bootstrap\Utility\Variables $variables
* A variables object.
*/
protected function preprocessLinks(Variables $variables) {
// Convert "dropbutton" theme suggestion variables.
if (mb_strpos($variables->theme_hook_original, 'links__dropbutton') !== FALSE && !empty($variables->links)) {
$operations = !!mb_strpos($variables->theme_hook_original, 'operations');
// Normal dropbutton links are not actually render arrays, convert them.
foreach ($variables->links as &$element) {
// Only process links that have "title".
if (!isset($element['title'])) {
continue;
}
// If title is an actual render array, just move it up.
if (Element::isRenderArray($element['title']) && !isset($element['url'])) {
$element = $element['title'];
}
// Otherwise, convert into an actual "link" render array element.
else {
if (!isset($element['url'])) {
$element['url'] = Url::fromRoute('<none>');
}
$attributes = isset($element['attributes']) ? $element['attributes'] : [];
$wrapper_attributes = isset($element['wrapper_attributes']) ? $element['wrapper_attributes'] : [];
if (isset($element['language']) && $element['language'] instanceof LanguageInterface) {
$attributes['hreflang'] = $element['language']->getId();
$wrapper_attributes['hreflang'] = $element['language']->getId();
// Ensure the Url language is set on the object itself.
// @todo Revisit, possibly a core bug?
// @see https://www.drupal.org/project/bootstrap/issues/2868100
$element['url']->setOption('language', $element['language']);
}
// Preserve query parameters (if any)
if (!empty($element['query'])) {
$url_query = $element['url']->getOption('query') ?: [];
$element['url']->setOption('query', NestedArray::mergeDeep($url_query, $element['query']));
}
// Build render array.
$element = [
'#type' => 'link',
'#title' => $element['title'],
'#url' => $element['url'],
'#ajax' => isset($element['ajax']) ? $element['ajax'] : [],
'#attributes' => $attributes,
'#wrapper_attributes' => $wrapper_attributes,
];
}
}
$items = Element::createStandalone();
/** @var \Drupal\bootstrap\Utility\Element $primary_action */
$primary_action = NULL;
$links = Element::create($variables->links);
// Iterate over all provided "links". The array may be associative, so
// this cannot rely on the key to be numeric, it must be tracked manually.
$i = -1;
foreach ($links->children(TRUE) as $key => $child) {
$i++;
// Ensure validation errors are limited.
if ($child->getProperty('limit_validation_errors') !== FALSE) {
$child->setAttribute('formnovalidate', 'formnovalidate');
}
// Generate the unique suffix to use with identifiers. This helps
// eliminate any render cache issues when dealing with multiple
// dropdown elements on the same page, as in a listing.
// @see https://www.drupal.org/project/bootstrap/issues/2939166
$suffix = Crypt::randomBytesBase64(8);
// The first item is always the "primary link".
if ($i === 0) {
// Must generate an ID for this child because the toggle will use it.
if (!$child->getAttribute('id')) {
$child->setAttribute('id', $child->getProperty('id', Html::getUniqueId("dropdown-item-$suffix")));
}
$primary_action = $child->addClass('hidden');
}
// Convert into a proper link.
if (!$child->isType('link')) {
// Retrieve any set HTML identifier for the original element,
// generating a new one if necessary. This is needed to ensure events
// are bound on the original element (which may be DOM specific).
// When the corresponding link below is clicked, it proxies all
// events to the "dropdown-target" (the original element).
$id = $child->getAttribute('id');
if (!$id) {
$id = $child->getProperty('id', Html::getUniqueId("dropdown-item-$suffix"));
$child->setAttribute('id', $id);
}
// Add the original element to the item list, but hide it.
$items->{$key . '_original'} = $child->addClass('hidden')->getArrayCopy();
// Replace the child element with a proper link.
$child = Element::createStandalone([
'#type' => 'link',
'#title' => $child->getProperty('value', $child->getProperty('title', $child->getProperty('text'))),
'#url' => Url::fromUserInput('#'),
'#attributes' => ['data-dropdown-target' => "#$id"],
]);
// Also hide the real link if it's the primary action.
if ($i === 0) {
$child->addClass('hidden');
}
}
// If no HTML ID was found, automatically create one.
if ($child->hasProperty('ajax') && !$child->hasProperty('ajax_processed') && !$child->hasProperty('id')) {
$child->setProperty('id', $child->getAttribute('id', Html::getUniqueId("ajax-link-$suffix")));
}
$items->$key = $child->getArrayCopy();
}
// Create a toggle button, extracting relevant info from primary action.
$toggle = Element::createStandalone([
'#type' => 'button',
'#attributes' => $primary_action->getAttributes()->getArrayCopy(),
'#value' => $primary_action->getProperty('value', $primary_action->getProperty('title', $primary_action->getProperty('text'))),
]);
// Remove the "hidden" class that was added to the primary action.
$toggle->removeClass('hidden')->removeAttribute('id')->setAttribute('data-dropdown-target', '#' . $primary_action->getAttribute('id'));
// Make operations smaller.
if ($operations) {
$toggle->setButtonSize('btn-xs', FALSE);
}
// Add the toggle render array to the variables.
$variables->toggle = $toggle->getArrayCopy();
// Determine if toggle should be a split button.
$variables->split = count($items) > 1;
// Add the items variable for "bootstrap_dropdown".
$variables->items = $items->getArrayCopy();
// Remove the unnecessary "links" variable now.
unset($variables->links);
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\bootstrap\Plugin\Preprocess;
use Drupal\bootstrap\Utility\Variables;
use Drupal\Component\Utility\Html;
/**
* Pre-processes variables for the "bootstrap_modal" theme hook.
*
* @ingroup plugins_preprocess
*
* @BootstrapPreprocess("bootstrap_modal")
*/
class BootstrapModal extends PreprocessBase implements PreprocessInterface {
/**
* {@inheritdoc}
*/
protected function preprocessVariables(Variables $variables) {
// Immediately log an error and return if Bootstrap modals are not enabled.
if (!$this->theme->getSetting('modal_enabled')) {
\Drupal::logger('bootstrap')->error(t('Bootstrap modals are not enabled.'));
return;
}
// Retrieve the ID, generating one if needed.
$id = $variables->getAttribute('id', Html::getUniqueId($variables->offsetGet('id', 'bootstrap-modal')));
$variables->setAttribute('id', $id);
unset($variables['id']);
if ($variables->title) {
$title_id = $variables->getAttribute('id', "$id--title", $variables::TITLE);
$variables->setAttribute('id', $title_id, $variables::TITLE);
$variables->setAttribute('aria-labelledby', $title_id);
}
// Use a provided modal size or retrieve the default theme setting.
$variables->size = $variables->size ?: $this->theme->getSetting('modal_size');
// Convert the description variable.
$this->preprocessDescription();
// Ensure all attributes are proper objects.
$this->preprocessAttributes();
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Drupal\bootstrap\Plugin\Preprocess;
use Drupal\bootstrap\Utility\Element;
use Drupal\bootstrap\Utility\Variables;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Utility\Html;
/**
* Pre-processes variables for the "bootstrap_panel" theme hook.
*
* @ingroup plugins_preprocess
*
* @BootstrapPreprocess("bootstrap_panel")
*/
class BootstrapPanel extends PreprocessBase implements PreprocessInterface {
/**
* {@inheritdoc}
*/
protected function preprocessElement(Element $element, Variables $variables) {
// Assign the ID, if not already set.
$element->map(['id']);
// Add necessary classes.
$element->addClass([
'form-item',
'js-form-item',
'form-wrapper',
'js-form-wrapper',
]);
$body = [];
$properties = ['field_prefix', 'body', 'children'];
// Only add the #value property if it's a "details" or "fieldset" element
// type. Some form elements may use "CompositeFormElementTrait" which
// will inadvertently and eventually become preprocessed here and #value
// may actually be the element's value instead of a renderable element.
if ($element->isType(['details', 'fieldset'])) {
$properties[] = 'value';
}
// Add the "#field_suffix" property.
$properties[] = 'field_suffix';
// Merge all possible content from the element into a single render array.
foreach ($properties as $property) {
$body[$property] = Element::create($element->getProperty($property, []))->getArray();
}
$variables['body'] = array_filter($body);
$map = [
'attributes' => 'attributes',
'body_attributes' => 'body_attributes',
'content_attributes' => 'body_attributes',
'description' => 'description',
'description_attributes' => 'description_attributes',
'description_display' => 'description_display',
'errors' => 'errors',
'footer' => 'footer',
'required' => 'required',
'panel_type' => 'panel_type',
'title' => 'heading',
'title_attributes' => 'heading_attributes',
];
// Handle specific "details" elements.
if ($element->isType('details')) {
// Details are always collapsible per the HTML5 spec.
// @see https://www.drupal.org/node/1852020
$variables['collapsible'] = TRUE;
// Determine the collapsed state.
$variables['collapsed'] = !$element->getProperty('open', TRUE);
// Remove the unnecessary details attribute.
$element->removeAttribute('open');
}
// Handle specific "fieldset" elements.
elseif ($element->isType('fieldset')) {
// Override variables to mimic the default "fieldset" element info.
// They will be mapped below if they exist on the element.
unset($variables['collapsible'], $variables['collapsed']);
$map['collapsed'] = 'collapsed';
$map['collapsible'] = 'collapsible';
}
// Map the element properties to the variables array.
$variables->map($map);
}
/**
* {@inheritdoc}
*/
protected function preprocessVariables(Variables $variables) {
// Retrieve the ID, generating one if needed.
$id = $variables->getAttribute('id', Html::getUniqueId($variables->offsetGet('id', 'bootstrap-panel')));
unset($variables['id']);
// Handle collapsible state.
if ($variables['heading'] && $variables['collapsible']) {
// Retrieve the body ID attribute.
if ($body_id = $variables->getAttribute('id', "$id--content", 'body_attributes')) {
// Ensure the target is set.
if ($variables['target'] = $variables->offsetGet('target', "#$body_id")) {
// Set additional necessary attributes to the heading.
$variables->setAttributes([
'aria-controls' => preg_replace('/^#/', '', $variables['target']),
'aria-expanded' => !$variables['collapsed'] ? 'true' : 'false',
'aria-pressed' => !$variables['collapsed'] ? 'true' : 'false',
'data-toggle' => 'collapse',
'role' => 'button',
], 'heading_attributes');
}
}
}
// Ensure we render HTML from heading.
$heading = $variables->offsetGet('heading');
if ($heading && (is_string($heading) || ($heading instanceof MarkupInterface))) {
$variables->offsetSet('heading', ['#markup' => $heading]);
}
// Ensure there is a valid panel state.
if (!$variables->offsetGet('panel_type')) {
$variables->offsetSet('panel_type', 'default');
}
// Convert the description variable.
$this->preprocessDescription();
// Ensure all attributes are proper objects.
$this->preprocessAttributes();
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Drupal\bootstrap\Plugin\Preprocess;
use Drupal\bootstrap\Utility\Variables;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
/**
* Pre-processes variables for the "breadcrumb" theme hook.
*
* @ingroup plugins_preprocess
*
* @BootstrapPreprocess("breadcrumb")
*/
class Breadcrumb extends PreprocessBase implements PreprocessInterface {
/**
* {@inheritdoc}
*/
public function preprocessVariables(Variables $variables) {
$breadcrumb = &$variables['breadcrumb'];
// Determine if breadcrumbs should be displayed.
$breadcrumb_visibility = $this->theme->getSetting('breadcrumb');
if (($breadcrumb_visibility == 0 || ($breadcrumb_visibility == 2 && \Drupal::service('router.admin_context')->isAdminRoute())) || empty($breadcrumb)) {
$breadcrumb = [];
return;
}
// Remove first occurrence of the "Home" <front> link, provided by core.
if (!$this->theme->getSetting('breadcrumb_home')) {
$front = Url::fromRoute('<front>')->toString();
foreach ($breadcrumb as $key => $link) {
if (isset($link['url']) && $link['url'] === $front) {
unset($breadcrumb[$key]);
break;
}
}
}
if ($this->theme->getSetting('breadcrumb_title') && !empty($breadcrumb)) {
$request = \Drupal::request();
$route_match = \Drupal::routeMatch();
$page_title = \Drupal::service('title_resolver')->getTitle($request, $route_match->getRouteObject());
if (!empty($page_title)) {
$breadcrumb[] = [
'text' => $page_title,
'attributes' => new Attribute(['class' => ['active']]),
];
}
}
// Add cache context based on url.
$variables->addCacheContexts(['route', 'url.path', 'languages']);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\bootstrap\Plugin\Preprocess;
use Drupal\bootstrap\Utility\Variables;
/**
* Pre-processes variables for the "container__help_block" theme hook.
*
* @ingroup plugins_preprocess
*
* @BootstrapPreprocess("container__help_block")
*/
class ContainerHelpBlock extends PreprocessBase implements PreprocessInterface {
/**
* {@inheritdoc}
*/
public function preprocessVariables(Variables $variables) {
$variables->addClass('help-block');
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Drupal\bootstrap\Plugin\Preprocess;
use Drupal\bootstrap\Utility\Element;
use Drupal\bootstrap\Utility\Variables;
/**
* Pre-processes variables for the "field_multiple_value_form" theme hook.
*
* @ingroup plugins_preprocess
*
* @BootstrapPreprocess("field_multiple_value_form")
*/
class FieldMultipleValueForm extends PreprocessBase implements PreprocessInterface {
/**
* {@inheritdoc}
*/
public function preprocessElement(Element $element, Variables $variables) {
// Wrap header columns in label element for Bootstrap.
if ($variables['multiple'] && !empty($variables['table']['#header'])) {
$prefixes = [
'#prefix' => '<label class="label">',
'#suffix' => '</label>',
];
foreach ($variables['table']['#header'] as &$header_row) {
if (is_array($header_row) && isset($header_row['data'])) {
$header_row['data'] = is_array($header_row['data']) ? ($prefixes + $header_row['data']) : ($prefixes + ['#markup' => $header_row['data']]);
}
elseif (is_string($header_row)) {
$header_row = ['data' => $prefixes + ['#markup' => $header_row]];
}
else {
$header_row = ['data' => $prefixes];
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More