Merge branch 'MDL-56853-master' of git://github.com/damyon/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 14 Nov 2016 06:36:45 +0000 (14:36 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 14 Nov 2016 06:36:45 +0000 (14:36 +0800)
191 files changed:
admin/environment.xml
admin/index.php
admin/message.php
admin/roles/allow.php
admin/roles/classes/define_role_table_advanced.php
admin/roles/classes/define_role_table_basic.php
admin/roles/define.php
admin/roles/module.js
admin/settings/appearance.php
admin/settings/security.php
admin/tool/behat/tests/behat/nasty_strings.feature
admin/tool/langimport/classes/controller.php
admin/tool/lp/classes/form/framework_autocomplete.php
admin/tool/lpimportcsv/classes/form/import.php
admin/tool/lpimportcsv/lang/en/tool_lpimportcsv.php
admin/tool/mobile/classes/external.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/settings.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/monitor/index.php
admin/tool/usertours/amd/build/popper.min.js
admin/tool/usertours/amd/build/tour.min.js
admin/tool/usertours/amd/src/popper.js
admin/tool/usertours/amd/src/tour.js
admin/tool/usertours/classes/manager.php
admin/tool/usertours/thirdpartylibs.xml
availability/classes/tree_node.php
availability/condition/completion/tests/behat/conditional_bug.feature
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js
availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js
availability/condition/completion/yui/src/form/js/form.js
availability/condition/date/classes/frontend.php
availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js
availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js
availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js
availability/condition/date/yui/src/form/js/form.js
availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-debug.js
availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js
availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form.js
availability/condition/grade/yui/src/form/js/form.js
availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-debug.js
availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js
availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form.js
availability/condition/group/yui/src/form/js/form.js
availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-debug.js
availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js
availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form.js
availability/condition/grouping/yui/src/form/js/form.js
availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-debug.js
availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js
availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form.js
availability/condition/profile/yui/src/form/js/form.js
availability/tests/tree_test.php
availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js
availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js
availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js
availability/yui/src/form/js/form.js
blog/rsslib.php
calendar/classes/export_form.php
composer.lock
course/externallib.php
course/lib.php
course/upgrade.txt
filter/mediaplugin/tests/filter_test.php
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js
grade/report/grader/yui/src/gradereporttable/js/floatingheaders.js
lang/en/admin.php
lang/en/cache.php
lang/en/calendar.php
lang/en/deprecated.txt
lang/en/moodle.php
lang/en/webservice.php
lib/adminlib.php
lib/amd/build/chart_output_chartjs.min.js
lib/amd/src/chart_output_chartjs.js
lib/classes/files/curl_security_helper.php [new file with mode: 0644]
lib/classes/files/curl_security_helper_base.php [new file with mode: 0644]
lib/classes/session/manager.php
lib/db/caches.php
lib/environmentlib.php
lib/externallib.php
lib/filelib.php
lib/form/amd/build/passwordunmask.min.js
lib/form/amd/src/passwordunmask.js
lib/form/select.php
lib/form/templatable_form_element.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputlib.php
lib/outputrenderers.php
lib/phpunit/classes/util.php
lib/setuplib.php
lib/tests/curl_security_helper_test.php [new file with mode: 0644]
lib/tests/filelib_test.php
lib/tests/upgradelib_test.php
lib/upgradelib.php
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js
lib/yui/src/notification/js/alert.js
lib/yui/src/notification/js/confirm.js
media/player/videojs/settings.php
message/classes/api.php
message/lib.php
message/output/popup/classes/api.php
message/output/popup/lib.php
message/tests/api_test.php
mod/assign/amd/build/grading_panel.min.js
mod/assign/amd/build/participant_selector.min.js
mod/assign/amd/src/grading_panel.js
mod/assign/amd/src/participant_selector.js
mod/assign/externallib.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/feedback/editpdf/tests/behat/group_annotations.feature
mod/assign/feedback/editpdf/tests/behat/view_previous_annotations.feature
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/locallib.php
mod/assign/styles.css
mod/assign/tests/behat/allow_another_attempt.feature
mod/assign/tests/behat/group_submission.feature
mod/data/edit.php
mod/data/export_form.php
mod/data/field.php
mod/data/field/checkbox/field.class.php
mod/data/field/date/field.class.php
mod/data/field/file/field.class.php
mod/data/field/latlong/field.class.php
mod/data/field/menu/field.class.php
mod/data/field/multimenu/field.class.php
mod/data/field/number/field.class.php
mod/data/field/picture/field.class.php
mod/data/field/radiobutton/field.class.php
mod/data/field/text/field.class.php
mod/data/field/textarea/field.class.php
mod/data/field/url/field.class.php
mod/data/lib.php
mod/data/renderer.php
mod/data/styles.css
mod/data/templates.php
mod/data/view.php
mod/feedback/edit_form.php
mod/feedback/show_nonrespondents.php
mod/forum/classes/observer.php
mod/forum/db/events.php
mod/forum/version.php
mod/lesson/edit.php
mod/lesson/lib.php
mod/lti/templates/cartridge_registration_form.mustache
mod/lti/templates/tool_card.mustache
mod/lti/templates/tool_configure.mustache
mod/lti/templates/tool_list.mustache
mod/lti/templates/tool_proxy_card.mustache
notes/index.php
question/type/essay/question.php
question/type/questionbase.php
theme/boost/classes/output/core_renderer.php
theme/boost/lang/en/theme_boost.php
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/filemanager.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/modules.scss
theme/boost/scss/moodle/user.scss
theme/boost/templates/core/block.mustache
theme/boost/templates/core/filemanager_loginform.mustache
theme/boost/templates/core/single_button.mustache
theme/boost/templates/core_form/element-autocomplete-inline.mustache
theme/boost/templates/core_form/element-select-inline.mustache
theme/boost/templates/core_form/element-select.mustache
theme/boost/templates/core_form/element-selectwithlink.mustache
theme/boost/templates/core_form/element-tags-inline.mustache
theme/boost/templates/core_form/element-tags.mustache
theme/boost/tests/behat/behat_theme_boost_behat_admin.php
theme/bootstrapbase/less/moodle/admin.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/style/moodle.css
user/externallib.php
user/tests/externallib_test.php
version.php
webservice/externallib.php
webservice/tests/externallib_test.php

index 5775d1a..8c35fb6 100644 (file)
           <ON_CHECK message="unsupporteddbtablerowformat" />
         </FEEDBACK>
       </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
   <MOODLE version="2.8" requires="2.2">
           <ON_CHECK message="unsupporteddbtablerowformat" />
         </FEEDBACK>
       </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
   <MOODLE version="3.0" requires="2.2">
       <VENDOR name="oracle" version="10.2" />
     </DATABASE>
     <PHP version="5.4.4" level="required">
+      <RESTRICT function="restrict_php_version_71" message="unsupportedphpversion71" />
     </PHP>
     <PCREUNICODE level="optional">
       <FEEDBACK>
           <ON_CHECK message="unsupporteddbtablerowformat" />
         </FEEDBACK>
       </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
   <MOODLE version="3.1" requires="2.7">
       <VENDOR name="oracle" version="10.2" />
     </DATABASE>
     <PHP version="5.4.4" level="required">
+      <RESTRICT function="restrict_php_version_71" message="unsupportedphpversion71" />
     </PHP>
     <PCREUNICODE level="optional">
       <FEEDBACK>
           <ON_CHECK message="unoconvwarning" />
         </FEEDBACK>
       </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
   <MOODLE version="3.2" requires="2.7">
           <ON_CHECK message="tlswarning" />
         </FEEDBACK>
       </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
 </COMPATIBILITY_MATRIX>
index 6f87285..30ba5cb 100644 (file)
@@ -315,6 +315,11 @@ if (!$cache and $version > $CFG->version) {  // upgrade
         $testsite = 'behat';
     }
 
+    if (isset($CFG->themerev)) {
+        // Store the themerev to restore after purging caches.
+        $themerev = $CFG->themerev;
+    }
+
     // We purge all of MUC's caches here.
     // Caches are disabled for upgrade by CACHE_DISABLE_ALL so we must set the first arg to true.
     // This ensures a real config object is loaded and the stores will be purged.
@@ -324,6 +329,11 @@ if (!$cache and $version > $CFG->version) {  // upgrade
     // We then purge the regular caches.
     purge_all_caches();
 
+    if (isset($themerev)) {
+        // Restore the themerev
+        set_config('themerev', $themerev);
+    }
+
     $output = $PAGE->get_renderer('core', 'admin');
 
     if (upgrade_stale_php_files_present()) {
index 52cb560..6924e61 100644 (file)
@@ -41,7 +41,7 @@ if (!empty($disable) && confirm_sesskey()) {
     if (!$processor = $DB->get_record('message_processors', array('id'=>$disable))) {
         print_error('outputdoesnotexist', 'message');
     }
-    $DB->set_field('message_processors', 'enabled', '0', array('id'=>$processor->id));      // Disable output
+    \core_message\api::update_processor_status($processor, 0);     // Disable output.
     core_plugin_manager::reset_caches();
 }
 
@@ -49,7 +49,7 @@ if (!empty($enable) && confirm_sesskey()) {
     if (!$processor = $DB->get_record('message_processors', array('id'=>$enable))) {
         print_error('outputdoesnotexist', 'message');
     }
-    $DB->set_field('message_processors', 'enabled', '1', array('id'=>$processor->id));      // Enable output
+    \core_message\api::update_processor_status($processor, 1);      // Enable output.
     core_plugin_manager::reset_caches();
 }
 
index 383455e..45f8b88 100644 (file)
@@ -80,7 +80,8 @@ echo $OUTPUT->box($controller->get_intro_text());
 echo '<form action="' . $baseurl . '" method="post">';
 echo '<input type="hidden" name="sesskey" value="' . sesskey() . '" />';
 echo html_writer::table($table);
-echo '<div class="buttons"><input type="submit" name="submit" value="'.get_string('savechanges').'"/>';
+echo '<div class="submitbuttons">';
+echo '<input type="submit" class="btn btn-primary" name="submit" value="' . get_string('savechanges') . '"/>';
 echo '</div></form>';
 
 echo $OUTPUT->footer();
index eb46c76..303855b 100644 (file)
@@ -471,15 +471,17 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
     }
 
     protected function get_name_field($id) {
-        return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="254" value="' . s($this->role->name) . '" />';
+        return '<input type="text" class="form-control" id="' . $id . '" name="' . $id . '" size="30"
+            value="' . s($this->role->name) . '" />';
     }
 
     protected function get_shortname_field($id) {
-        return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="254" value="' . s($this->role->shortname) . '" />';
+        return '<input type="text" class="form-control" id="' . $id . '" name="' . $id . '" size="30"
+            value="' . s($this->role->shortname) . '" />';
     }
 
     protected function get_description_field($id) {
-        return '<textarea class="form-textarea" id="'. s($id) .'" name="description" rows="10" cols="50">' .
+        return '<textarea class="form-textarea form-control" id="'. s($id) .'" name="description" rows="10" cols="50">' .
             htmlspecialchars($this->role->description) .
             '</textarea>';
     }
@@ -490,7 +492,8 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         foreach (get_role_archetypes() as $type) {
             $options[$type] = get_string('archetype'.$type, 'role');
         }
-        return html_writer::select($options, 'archetype', $this->role->archetype, false);
+        return html_writer::select($options, 'archetype', $this->role->archetype, false,
+            array('class' => 'custom-select'));
     }
 
     protected function get_assignable_levels_control() {
@@ -563,7 +566,8 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         if ($this->roleid == 0) {
             $options[-1] = get_string('thisnewrole', 'core_role');
         }
-        return html_writer::select($options, 'allow'.$type.'[]', $selected, false, array('multiple'=>'multiple', 'size'=>10));
+        return html_writer::select($options, 'allow'.$type.'[]', $selected, false, array('multiple' => 'multiple',
+            'size' => 10, 'class' => 'form-control'));
     }
 
     /**
@@ -575,11 +579,19 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         return '';
     }
 
-    protected function print_field($name, $caption, $field) {
+    /**
+     * Print labels, fields and help icon on role administration page.
+     *
+     * @param string $name The field name.
+     * @param string $caption The field caption.
+     * @param string $field The field type.
+     * @param null|string $helpicon The help icon content.
+     */
+    protected function print_field($name, $caption, $field, $helpicon = null) {
         global $OUTPUT;
         // Attempt to generate HTML like formslib.
-        echo '<div class="fitem">';
-        echo '<div class="fitemtitle">';
+        echo '<div class="fitem row form-group">';
+        echo '<div class="fitemtitle col-md-3">';
         if ($name) {
             echo '<label for="' . $name . '">';
         }
@@ -587,13 +599,16 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         if ($name) {
             echo "</label>\n";
         }
+        if ($helpicon) {
+            echo '<span class="pull-xs-right text-nowrap">'.$helpicon.'</span>';
+        }
         echo '</div>';
         if (isset($this->errors[$name])) {
             $extraclass = ' error';
         } else {
             $extraclass = '';
         }
-        echo '<div class="felement' . $extraclass . '">';
+        echo '<div class="felement col-md-9 form-inline' . $extraclass . '">';
         echo $field;
         if (isset($this->errors[$name])) {
             echo $OUTPUT->error_text($this->errors[$name]);
@@ -605,7 +620,8 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
     protected function print_show_hide_advanced_button() {
         echo '<p class="definenotice">' . get_string('highlightedcellsshowdefault', 'core_role') . ' </p>';
         echo '<div class="advancedbutton">';
-        echo '<input type="submit" name="toggleadvanced" value="' . get_string('hideadvanced', 'form') . '" />';
+        echo '<input type="submit" class="btn btn-secondary" name="toggleadvanced" value="' .
+            get_string('hideadvanced', 'form') . '" />';
         echo '</div>';
     }
 
@@ -613,11 +629,14 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         global $OUTPUT;
         // Extra fields at the top of the page.
         echo '<div class="topfields clearfix">';
-        $this->print_field('shortname', get_string('roleshortname', 'core_role').'&nbsp;'.$OUTPUT->help_icon('roleshortname', 'core_role'), $this->get_shortname_field('shortname'));
-        $this->print_field('name', get_string('customrolename', 'core_role').'&nbsp;'.$OUTPUT->help_icon('customrolename', 'core_role'), $this->get_name_field('name'));
-        $this->print_field('edit-description', get_string('customroledescription', 'core_role').'&nbsp;'.$OUTPUT->help_icon('customroledescription', 'core_role'),
-            $this->get_description_field('description'));
-        $this->print_field('menuarchetype', get_string('archetype', 'core_role').'&nbsp;'.$OUTPUT->help_icon('archetype', 'core_role'), $this->get_archetype_field('archetype'));
+        $this->print_field('shortname', get_string('roleshortname', 'core_role'),
+            $this->get_shortname_field('shortname'), $OUTPUT->help_icon('roleshortname', 'core_role'));
+        $this->print_field('name', get_string('customrolename', 'core_role'), $this->get_name_field('name'),
+            $OUTPUT->help_icon('customrolename', 'core_role'));
+        $this->print_field('edit-description', get_string('customroledescription', 'core_role'),
+            $this->get_description_field('description'), $OUTPUT->help_icon('customroledescription', 'core_role'));
+        $this->print_field('menuarchetype', get_string('archetype', 'core_role'), $this->get_archetype_field('archetype'),
+            $OUTPUT->help_icon('archetype', 'core_role'));
         $this->print_field('', get_string('maybeassignedin', 'core_role'), $this->get_assignable_levels_control());
         $this->print_field('menuallowassign', get_string('allowassign', 'core_role'), $this->get_allow_role_control('assign'));
         $this->print_field('menuallowoverride', get_string('allowoverride', 'core_role'), $this->get_allow_role_control('override'));
index 0c3d692..3349377 100644 (file)
@@ -37,7 +37,8 @@ class core_role_define_role_table_basic extends core_role_define_role_table_adva
 
     protected function print_show_hide_advanced_button() {
         echo '<div class="advancedbutton">';
-        echo '<input type="submit" name="toggleadvanced" value="' . get_string('showadvanced', 'form') . '" />';
+        echo '<input type="submit" class="btn btn-secondary" name="toggleadvanced"
+            value="' . get_string('showadvanced', 'form') . '" />';
         echo '</div>';
     }
 
index 093c693..d670bae 100644 (file)
@@ -252,7 +252,7 @@ if ($action === 'view') {
     echo '<div class="mform">';
 } else {
     ?>
-<form id="rolesform" class="mform" action="<?php p($baseurl->out(false)); ?>" method="post"><div>
+<form id="rolesform" class="mform fcontainer" action="<?php p($baseurl->out(false)); ?>" method="post"><div>
 <input type="hidden" name="sesskey" value="<?php p(sesskey()) ?>" />
 <input type="hidden" name="return" value="<?php p($return); ?>" />
 <input type="hidden" name="resettype" value="none" />
index 9c331f1..bbfe602 100644 (file)
@@ -45,17 +45,19 @@ M.core_role.init_cap_table_filter = function(Y, tableid, contextid) {
             this.table = Y.one('#'+this.tableid);
 
             // Create a div to hold the search UI.
-            this.div = Y.Node.create('<div class="capabilitysearchui"></div>').setStyles({
+            this.div = Y.Node.create('<div class="capabilitysearchui form-inline"></div>').setStyles({
                 width : this.table.get('offsetWidth'),
                 marginLeft : 'auto',
                 marginRight : 'auto'
             });
             // Create the capability search input.
-            this.input = Y.Node.create('<input type="text" id="'+this.table.get('id')+'capabilitysearch" value="'+Y.Escape.html(filtervalue)+'" />');
+            this.input = Y.Node.create('<input class="form-control m-x-1" type="text"' +
+                ' id="'+this.table.get('id')+'capabilitysearch" value="'+Y.Escape.html(filtervalue)+'" />');
             // Create a label for the search input.
             this.label = Y.Node.create('<label for="'+this.input.get('id')+'">'+M.util.get_string('filter', 'moodle')+' </label>');
             // Create a clear button to clear the input.
-            this.button = Y.Node.create('<input type="button" value="'+M.util.get_string('clear', 'moodle')+'" />').set('disabled', filtervalue=='');
+            this.button = Y.Node.create('<input type="button" class="btn btn-primary"' +
+                ' value="'+M.util.get_string('clear', 'moodle')+'" />').set('disabled', filtervalue=='');
 
             // Tie it all together
             this.div.append(this.label).append(this.input).append(this.button);
index 398c777..95ab246 100644 (file)
@@ -236,8 +236,11 @@ preferences,moodle|/user/preferences.php|preferences',
     $temp->add(new admin_setting_configcheckbox('modchooserdefault', new lang_string('modchooserdefault', 'admin'), new lang_string('configmodchooserdefault', 'admin'), 1));
     $ADMIN->add('appearance', $temp);
 
-    // link to tag management interface
-    $ADMIN->add('appearance', new admin_externalpage('managetags', new lang_string('managetags', 'tag'), $CFG->wwwroot.'/tag/manage.php', 'moodle/tag:manage'));
+    // Link to tag management interface.
+    $url = new moodle_url('/tag/manage.php');
+    $hidden = empty($CFG->usetags);
+    $page = new admin_externalpage('managetags', new lang_string('managetags', 'tag'), $url, 'moodle/tag:manage', $hidden);
+    $ADMIN->add('appearance', $page);
 
     $temp = new admin_settingpage('additionalhtml', new lang_string('additionalhtml', 'admin'));
     $temp->add(new admin_setting_heading('additionalhtml_heading', new lang_string('additionalhtml_heading', 'admin'), new lang_string('additionalhtml_desc', 'admin')));
index c892566..1d89d42 100644 (file)
@@ -118,8 +118,15 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configcheckbox('cookiehttponly', new lang_string('cookiehttponly', 'admin'), new lang_string('configcookiehttponly', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowframembedding', new lang_string('allowframembedding', 'admin'), new lang_string('allowframembedding_help', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('loginpasswordautocomplete', new lang_string('loginpasswordautocomplete', 'admin'), new lang_string('loginpasswordautocomplete_help', 'admin'), 0));
-    $ADMIN->add('security', $temp);
 
+    // Settings elements used by the \core\files\curl_security_helper class.
+    $temp->add(new admin_setting_configmixedhostiplist('curlsecurityblockedhosts',
+               new lang_string('curlsecurityblockedhosts', 'admin'),
+               new lang_string('curlsecurityblockedhostssyntax', 'admin'), ""));
+    $temp->add(new admin_setting_configportlist('curlsecurityallowedport',
+               new lang_string('curlsecurityallowedport', 'admin'),
+               new lang_string('curlsecurityallowedportsyntax', 'admin'), ""));
+    $ADMIN->add('security', $temp);
 
     // "notifications" settingpage
     $temp = new admin_settingpage('notifications', new lang_string('notifications', 'admin'));
index 1c54765..6981227 100644 (file)
@@ -31,9 +31,8 @@ Feature: Transform steps arguments
     And I press "Update profile"
     And I click on "Edit profile" "link" in the "region-main" "region"
     Then I should not see "NASTYSTRING"
-    # BEHAT Transformation regression - See MDL-56397
-    #And the field "Surname" matches value "$NASTYSTRING1"
-    #And the field "City/town" matches value "$NASTYSTRING3"
+    And the field "Surname" matches value "$NASTYSTRING1"
+    And the field "City/town" matches value "$NASTYSTRING3"
 
   Scenario: Use double quotes
     When I set the following fields to these values:
@@ -57,5 +56,4 @@ Feature: Transform steps arguments
     And I should see "My Firstname"
     And I should see "My Surname"
     And the field "First name" matches value "My Firstname $NASTYSTRING1"
-    # BEHAT Transformation regression - See MDL-56397
-    #And the field "Surname" matches value "My Surname $NASTYSTRING2"
+    And the field "Surname" matches value "My Surname $NASTYSTRING2"
index bf43258..3ea368c 100644 (file)
@@ -184,6 +184,8 @@ class controller {
 
         if ($updated) {
             $this->info[] = get_string('langupdatecomplete', 'tool_langimport');
+            // The strings have been changed so we need to purge their cache to ensure users see the changes.
+            get_string_manager()->reset_caches();
         } else {
             $this->info[] = get_string('nolangupdateneeded', 'tool_langimport');
         }
index 6107047..9505332 100644 (file)
@@ -91,7 +91,7 @@ class framework_autocomplete extends MoodleQuickForm_autocomplete {
         $ids = array();
 
         foreach ($values as $onevalue) {
-            if ((!$this->optionExists($onevalue)) &&
+            if (!empty($onevalue) && (!$this->optionExists($onevalue)) &&
                     ($onevalue !== '_qf__force_multiselect_submission')) {
                 array_push($ids, $onevalue);
             }
index d2c8071..d0d8d11 100644 (file)
@@ -52,6 +52,7 @@ class import extends moodleform {
         $mform = $this->_form;
         $element = $mform->createElement('filepicker', 'importfile', get_string('importfile', 'tool_lpimportcsv'));
         $mform->addElement($element);
+        $mform->addHelpButton('importfile', 'importfile', 'tool_lpimportcsv');
         $mform->addRule('importfile', null, 'required');
         $mform->addElement('hidden', 'confirm', 0);
         $mform->setType('confirm', PARAM_BOOL);
@@ -65,12 +66,10 @@ class import extends moodleform {
         } else {
             $mform->setDefault('delimiter_name', 'comma');
         }
-        $mform->addHelpButton('delimiter_name', 'csvdelimiter', 'tool_lpimportcsv');
 
         $choices = core_text::get_encodings();
         $mform->addElement('select', 'encoding', get_string('encoding', 'tool_lpimportcsv'), $choices);
         $mform->setDefault('encoding', 'UTF-8');
-        $mform->addHelpButton('encoding', 'encoding', 'tool_lpimportcsv');
 
         $this->add_action_buttons(false, get_string('import', 'tool_lpimportcsv'));
     }
index 9064eb2..8c10b04 100644 (file)
@@ -27,16 +27,16 @@ $string['competencyscaledescription'] = 'Competency scale created by import';
 $string['confirmcolumnmappings'] = 'Confirm the column mappings';
 $string['confirm'] = 'Confirm';
 $string['csvdelimiter'] = 'CSV delimiter';
-$string['csvdelimiter_help'] = 'The CSV delimiter is normally a comma.';
 $string['description'] = 'Description';
 $string['descriptionformat'] = 'Description format';
 $string['encoding'] = 'Encoding';
-$string['encoding_help'] = 'The CSV file encoding is usually UTF-8.';
 $string['export'] = 'Export';
 $string['exportid'] = 'Exported ID (optional)';
 $string['exportnavlink'] = 'Export competency framework';
 $string['idnumber'] = 'ID number';
 $string['importfile'] = 'CSV framework description file';
+$string['importfile_help'] = 'A competency framework may be imported via text file. The format of the file can be determined by creating a new competency framework on the site and then exporting it.';
+$string['importfile_link'] = 'admin/tool/lpimportcsv';
 $string['import'] = 'Import';
 $string['invalidimportfile'] = 'File format is invalid.';
 $string['isframework'] = 'Is framework';
index 3094ebd..1b3cdb6 100644 (file)
@@ -246,15 +246,9 @@ class external extends external_api {
         // We must toletare these two exceptions: forcepasswordchangenotice and usernotfullysetup.
         try {
             self::validate_context($context);
-        } catch (Exception $e) {
-            if ($e instanceof moodle_exception) {
-                if (($e->errorcode != 'usernotfullysetup') and
-                    ($e->errorcode != 'forcepasswordchangenotice')) {
-
-                    // In case we receive a different exception, throw it.
-                    throw $e;
-                }
-            } else {
+        } catch (moodle_exception $e) {
+            if ($e->errorcode != 'usernotfullysetup' && $e->errorcode != 'forcepasswordchangenotice') {
+                // In case we receive a different exception, throw it.
                 throw $e;
             }
         }
index 441a19a..c5862e7 100644 (file)
@@ -25,6 +25,7 @@
 $string['autologinkeygenerationlockout'] = 'Auto-login key generation is locked out, too much requests in an hour.';
 $string['autologinnotallowedtoadmins'] = 'Auto-login is not allowed to site admins';
 $string['clickheretolaunchtheapp'] = 'Click here if the app does not open automatically.';
+$string['configmobilecssurl'] = 'A CSS file to customise your mobile app interface.';
 $string['enablesmartappbanners'] = 'Enable Smart App Banners';
 $string['enablesmartappbanners_desc'] = 'This will display a banner promoting the Moodle Mobile app when visiting the site in Mobile Safari.';
 $string['forcedurlscheme'] = 'If you want to allow only your custom branded app to be opened via a browser window, then specify its URL scheme here; otherwise leave the field empty.';
@@ -36,6 +37,11 @@ $string['iosappid_desc'] = 'This setting may be left as default unless you have
 $string['loginintheapp'] = 'Via the app';
 $string['logininthebrowser'] = 'Via a browser window (for SSO plugins)';
 $string['loginintheembeddedbrowser'] = 'Via an embedded browser (for SSO plugins)';
+$string['mobileapp'] = 'Mobile app';
+$string['mobileappearance'] = 'Mobile appearance';
+$string['mobileauthentication'] = 'Mobile authentication';
+$string['mobilecssurl'] = 'CSS';
+$string['mobilesettings'] = 'Mobile settings';
 $string['pluginname'] = 'Moodle Mobile tools';
 $string['smartappbanners'] = 'Smart App Banners (iOS only)';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
index 496557c..9cc8d90 100644 (file)
@@ -28,7 +28,9 @@ defined('MOODLE_INTERNAL') || die();
 
 if ($hassiteconfig) {
 
-    $temp = new admin_settingpage('mobile', new lang_string('mobile', 'admin'), 'moodle/site:config', false);
+    $ADMIN->add('root', new admin_category('mobileapp', new lang_string('mobileapp', 'tool_mobile')), 'development');
+
+    $temp = new admin_settingpage('mobilesettings', new lang_string('mobilesettings', 'tool_mobile'), 'moodle/site:config', false);
 
     // We should wait to the installation to finish since we depend on some configuration values that are set once
     // the admin user profile is configured.
@@ -41,32 +43,43 @@ if ($hassiteconfig) {
                 new lang_string('configenablemobilewebservice', 'admin', $enablemobiledoclink), $default));
     }
 
-    $temp->add(new admin_setting_configtext('mobilecssurl', new lang_string('mobilecssurl', 'admin'),
-                new lang_string('configmobilecssurl', 'admin'), '', PARAM_URL));
+    $ADMIN->add('mobileapp', $temp);
+
+    // Show only mobile settings if the mobile service is enabled.
+    if (!empty($CFG->enablemobilewebservice)) {
+        // Type of login.
+        $temp = new admin_settingpage('mobileauthentication', new lang_string('mobileauthentication', 'tool_mobile'));
+        $options = array(
+            tool_mobile\api::LOGIN_VIA_APP => new lang_string('loginintheapp', 'tool_mobile'),
+            tool_mobile\api::LOGIN_VIA_BROWSER => new lang_string('logininthebrowser', 'tool_mobile'),
+            tool_mobile\api::LOGIN_VIA_EMBEDDED_BROWSER => new lang_string('loginintheembeddedbrowser', 'tool_mobile'),
+        );
+        $temp->add(new admin_setting_configselect('tool_mobile/typeoflogin',
+                    new lang_string('typeoflogin', 'tool_mobile'),
+                    new lang_string('typeoflogin_desc', 'tool_mobile'), 1, $options));
+
+        $temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
+                    new lang_string('forcedurlscheme_key', 'tool_mobile'),
+                    new lang_string('forcedurlscheme', 'tool_mobile'), '', PARAM_NOTAGS));
 
-    // Type of login.
-    $options = array(
-        tool_mobile\api::LOGIN_VIA_APP => new lang_string('loginintheapp', 'tool_mobile'),
-        tool_mobile\api::LOGIN_VIA_BROWSER => new lang_string('logininthebrowser', 'tool_mobile'),
-        tool_mobile\api::LOGIN_VIA_EMBEDDED_BROWSER => new lang_string('loginintheembeddedbrowser', 'tool_mobile'),
-    );
-    $temp->add(new admin_setting_configselect('tool_mobile/typeoflogin',
-                new lang_string('typeoflogin', 'tool_mobile'),
-                new lang_string('typeoflogin_desc', 'tool_mobile'), 1, $options));
+        $ADMIN->add('mobileapp', $temp);
 
-    $temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
-                new lang_string('forcedurlscheme_key', 'tool_mobile'),
-                new lang_string('forcedurlscheme', 'tool_mobile'), '', PARAM_NOTAGS));
+        // Appearance related settings.
+        $temp = new admin_settingpage('mobileappearance', new lang_string('mobileappearance', 'tool_mobile'));
 
-    $temp->add(new admin_setting_heading('tool_mobile/smartappbanners',
-                new lang_string('smartappbanners', 'tool_mobile'), ''));
+        $temp->add(new admin_setting_configtext('mobilecssurl', new lang_string('mobilecssurl', 'tool_mobile'),
+                    new lang_string('configmobilecssurl', 'tool_mobile'), '', PARAM_URL));
 
-    $temp->add(new admin_setting_configcheckbox('tool_mobile/enablesmartappbanners',
-                new lang_string('enablesmartappbanners', 'tool_mobile'),
-                new lang_string('enablesmartappbanners_desc', 'tool_mobile'), 0));
+        $temp->add(new admin_setting_heading('tool_mobile/smartappbanners',
+                    new lang_string('smartappbanners', 'tool_mobile'), ''));
 
-    $temp->add(new admin_setting_configtext('tool_mobile/iosappid', new lang_string('iosappid', 'tool_mobile'),
-                new lang_string('iosappid_desc', 'tool_mobile'), '633359593', PARAM_ALPHANUM));
+        $temp->add(new admin_setting_configcheckbox('tool_mobile/enablesmartappbanners',
+                    new lang_string('enablesmartappbanners', 'tool_mobile'),
+                    new lang_string('enablesmartappbanners_desc', 'tool_mobile'), 0));
 
-    $ADMIN->add('webservicesettings', $temp);
+        $temp->add(new admin_setting_configtext('tool_mobile/iosappid', new lang_string('iosappid', 'tool_mobile'),
+                    new lang_string('iosappid_desc', 'tool_mobile'), '633359593', PARAM_ALPHANUM));
+
+        $ADMIN->add('mobileapp', $temp);
+    }
 }
index 64657d9..010b1c9 100644 (file)
@@ -174,6 +174,9 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $CFG->enablemobilewebservice = 1;
         $_GET['wstoken'] = $token->token;   // Mock parameters.
 
+        // Even if we force the password change for the current user we should be able to retrieve the key.
+        set_user_preference('auth_forcepasswordchange', 1, $user->id);
+
         $this->setCurrentTimeStart();
         $result = external::get_autologin_key($token->privatetoken);
         $result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
index 1065bd1..b4c724b 100644 (file)
@@ -24,7 +24,7 @@
 
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
-require_once($CFG->dirroot . '/admin/tool/monitor/lib.php');
+require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/monitor/lib.php');
 
 $courseid = optional_param('courseid', 0, PARAM_INT);
 $action = optional_param('action', '', PARAM_ALPHA);
index ac29055..a86924e 100644 (file)
Binary files a/admin/tool/usertours/amd/build/popper.min.js and b/admin/tool/usertours/amd/build/popper.min.js differ
index 2f37014..20f5d4f 100644 (file)
Binary files a/admin/tool/usertours/amd/build/tour.min.js and b/admin/tool/usertours/amd/build/tour.min.js differ
index 16119ab..c0fb1d7 100644 (file)
-/**
- * @fileOverview Kickass library to create and place poppers near their reference elements.
- * @version 0.6.4
- * @license
- * Copyright (c) 2016 Federico Zivolo and contributors
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-//
-// Cross module loader
-// Supported: Node, AMD, Browser globals
-//
-;(function (root, factory) {
-    if (typeof define === 'function' && define.amd) {
-        // AMD. Register as an anonymous module.
-        define(factory);
-    } else if (typeof module === 'object' && module.exports) {
-        // Node. Does not work with strict CommonJS, but
-        // only CommonJS-like environments that support module.exports,
-        // like Node.
-        module.exports = factory();
-    } else {
-        // Browser globals (root is window)
-        root.Popper = factory();
-    }
-}(this, function () {
-
-    'use strict';
-
-    var root = window;
-
-    // default options
-    var DEFAULTS = {
-        // placement of the popper
-        placement: 'bottom',
-
-        gpuAcceleration: true,
-
-        // shift popper from its origin by the given amount of pixels (can be negative)
-        offset: 0,
-
-        // the element which will act as boundary of the popper
-        boundariesElement: 'viewport',
-
-        // amount of pixel used to define a minimum distance between the boundaries and the popper
-        boundariesPadding: 5,
-
-        // popper will try to prevent overflow following this order,
-        // by default, then, it could overflow on the left and on top of the boundariesElement
-        preventOverflowOrder: ['left', 'right', 'top', 'bottom'],
-
-        // the behavior used by flip to change the placement of the popper
-        flipBehavior: 'flip',
-
-        arrowElement: '[x-arrow]',
-
-        // list of functions used to modify the offsets before they are applied to the popper
-        modifiers: [ 'shift', 'offset', 'preventOverflow', 'keepTogether', 'arrow', 'flip', 'applyStyle'],
-
-        modifiersIgnored: [],
-    };
-
-    /**
-     * Create a new Popper.js instance
-     * @constructor Popper
-     * @param {HTMLElement} reference - The reference element used to position the popper
-     * @param {HTMLElement|Object} popper
-     *      The HTML element used as popper, or a configuration used to generate the popper.
-     * @param {String} [popper.tagName='div'] The tag name of the generated popper.
-     * @param {Array} [popper.classNames=['popper']] Array of classes to apply to the generated popper.
-     * @param {Array} [popper.attributes] Array of attributes to apply, specify `attr:value` to assign a value to it.
-     * @param {HTMLElement|String} [popper.parent=window.document.body] The parent element, given as HTMLElement or as query string.
-     * @param {String} [popper.content=''] The content of the popper, it can be text, html, or node; if it is not text, set `contentType` to `html` or `node`.
-     * @param {String} [popper.contentType='text'] If `html`, the `content` will be parsed as HTML. If `node`, it will be appended as-is.
-     * @param {String} [popper.arrowTagName='div'] Same as `popper.tagName` but for the arrow element.
-     * @param {Array} [popper.arrowClassNames='popper__arrow'] Same as `popper.classNames` but for the arrow element.
-     * @param {String} [popper.arrowAttributes=['x-arrow']] Same as `popper.attributes` but for the arrow element.
-     * @param {Object} options
-     * @param {String} [options.placement=bottom]
-     *      Placement of the popper accepted values: `top(-start, -end), right(-start, -end), bottom(-start, -right),
-     *      left(-start, -end)`
-     *
-     * @param {HTMLElement|String} [options.arrowElement='[x-arrow]']
-     *      The DOM Node used as arrow for the popper, or a CSS selector used to get the DOM node. It must be child of
-     *      its parent Popper. Popper.js will apply to the given element the style required to align the arrow with its
-     *      reference element.
-     *      By default, it will look for a child node of the popper with the `x-arrow` attribute.
-     *
-     * @param {Boolean} [options.gpuAcceleration=true]
-     *      When this property is set to true, the popper position will be applied using CSS3 translate3d, allowing the
-     *      browser to use the GPU to accelerate the rendering.
-     *      If set to false, the popper will be placed using `top` and `left` properties, not using the GPU.
-     *
-     * @param {Number} [options.offset=0]
-     *      Amount of pixels the popper will be shifted (can be negative).
-     *
-     * @param {String|Element} [options.boundariesElement='viewport']
-     *      The element which will define the boundaries of the popper position, the popper will never be placed outside
-     *      of the defined boundaries (except if `keepTogether` is enabled)
-     *
-     * @param {Number} [options.boundariesPadding=5]
-     *      Additional padding for the boundaries
-     *
-     * @param {Array} [options.preventOverflowOrder=['left', 'right', 'top', 'bottom']]
-     *      Order used when Popper.js tries to avoid overflows from the boundaries, they will be checked in order,
-     *      this means that the last ones will never overflow
-     *
-     * @param {String|Array} [options.flipBehavior='flip']
-     *      The behavior used by the `flip` modifier to change the placement of the popper when the latter is trying to
-     *      overlap its reference element. Defining `flip` as value, the placement will be flipped on
-     *      its axis (`right - left`, `top - bottom`).
-     *      You can even pass an array of placements (eg: `['right', 'left', 'top']` ) to manually specify
-     *      how alter the placement when a flip is needed. (eg. in the above example, it would first flip from right to left,
-     *      then, if even in its new placement, the popper is overlapping its reference element, it will be moved to top)
-     *
-     * @param {Array} [options.modifiers=[ 'shift', 'offset', 'preventOverflow', 'keepTogether', 'arrow', 'flip', 'applyStyle']]
-     *      List of functions used to modify the data before they are applied to the popper, add your custom functions
-     *      to this array to edit the offsets and placement.
-     *      The function should reflect the @params and @returns of preventOverflow
-     *
-     * @param {Array} [options.modifiersIgnored=[]]
-     *      Put here any built-in modifier name you want to exclude from the modifiers list
-     *      The function should reflect the @params and @returns of preventOverflow
-     *
-     * @param {Boolean} [options.removeOnDestroy=false]
-     *      Set to true if you want to automatically remove the popper when you call the `destroy` method.
-     */
-    function Popper(reference, popper, options) {
-        this._reference = reference.jquery ? reference[0] : reference;
-        this.state = { onCreateCalled: false };
-
-        // if the popper variable is a configuration object, parse it to generate an HTMLElement
-        // generate a default popper if is not defined
-        var isNotDefined = typeof popper === 'undefined' || popper === null;
-        var isConfig = popper && Object.prototype.toString.call(popper) === '[object Object]';
-        if (isNotDefined || isConfig) {
-            this._popper = this.parse(isConfig ? popper : {});
-        }
-        // otherwise, use the given HTMLElement as popper
-        else {
-            this._popper = popper.jquery ? popper[0] : popper;
-        }
-
-        // with {} we create a new object with the options inside it
-        this._options = Object.assign({}, DEFAULTS, options);
-
-        // refactoring modifiers' list
-        this._options.modifiers = this._options.modifiers.map(function(modifier){
-            // remove ignored modifiers
-            if (this._options.modifiersIgnored.indexOf(modifier) !== -1) return;
-
-            // set the x-placement attribute before everything else because it could be used to add margins to the popper
-            // margins needs to be calculated to get the correct popper offsets
-            if (modifier === 'applyStyle') {
-                this._popper.setAttribute('x-placement', this._options.placement);
-            }
-
-            // return predefined modifier identified by string or keep the custom one
-            return this.modifiers[modifier] || modifier;
-        }.bind(this));
-
-        // make sure to apply the popper position before any computation
-        this.state.position = this._getPosition(this._popper, this._reference);
-        setStyle(this._popper, { position: this.state.position});
-
-        // determine how we should set the origin of offsets
-        this.state.isParentTransformed = this._getIsParentTransformed(this._popper);
-
-        // fire the first update to position the popper in the right place
-        this.update();
-
-        // setup event listeners, they will take care of update the position in specific situations
-        this._setupEventListeners();
-        return this;
-    }
-
-
-    //
-    // Methods
-    //
-    /**
-     * Destroy the popper
-     * @method
-     * @memberof Popper
-     */
-    Popper.prototype.destroy = function() {
-        this._popper.removeAttribute('x-placement');
-        this._popper.style.left = '';
-        this._popper.style.position = '';
-        this._popper.style.top = '';
-        this._popper.style[getSupportedPropertyName('transform')] = '';
-        this._removeEventListeners();
-
-        // remove the popper if user explicity asked for the deletion on destroy
-        if (this._options.removeOnDestroy) {
-            this._popper.parentNode.removeChild(this._popper);
-        }
-        return this;
-    };
-
-    /**
-     * Updates the position of the popper, computing the new offsets and applying the new style
-     * @method
-     * @memberof Popper
-     */
-    Popper.prototype.update = function() {
-        var data = { instance: this, styles: {} };
-
-        // make sure to apply the popper position before any computation
-        this.state.position = this._getPosition(this._popper, this._reference);
-        setStyle(this._popper, { position: this.state.position});
-
-        // to avoid useless computations we throttle the popper position refresh to 60fps
-        root.requestAnimationFrame(function() {
-            var now = root.performance.now();
-            if(now - this.state.lastFrame <= 16) {
-                // this update fired to early! drop it
-                return;
-            }
-            this.state.lastFrame = now;
-
-            // store placement inside the data object, modifiers will be able to edit `placement` if needed
-            // and refer to _originalPlacement to know the original value
-            data.placement = this._options.placement;
-            data._originalPlacement = this._options.placement;
-
-            // compute the popper and trigger offsets and put them inside data.offsets
-            data.offsets = this._getOffsets(this._popper, this._reference, data.placement);
-
-            // get boundaries
-            data.boundaries = this._getBoundaries(data, this._options.boundariesPadding, this._options.boundariesElement);
-
-            data = this.runModifiers(data, this._options.modifiers);
-
-            if (!isFunction(this.state.createCalback)) {
-                this.state.onCreateCalled = true;
-            }
-            if (!this.state.onCreateCalled) {
-                this.state.onCreateCalled = true;
-                if (isFunction(this.state.createCalback)) {
-                    this.state.createCalback(this);
-                }
-            } else if (isFunction(this.state.updateCallback)) {
-                this.state.updateCallback(data);
-            }
-        }.bind(this));
-    };
-
-    /**
-     * If a function is passed, it will be executed after the initialization of popper with as first argument the Popper instance.
-     * @method
-     * @memberof Popper
-     * @param {Function} callback
-     */
-    Popper.prototype.onCreate = function(callback) {
-        // the createCallbacks return as first argument the popper instance
-        this.state.createCalback = callback;
-        return this;
-    };
-
-    /**
-     * If a function is passed, it will be executed after each update of popper with as first argument the set of coordinates and informations
-     * used to style popper and its arrow.
-     * NOTE: it doesn't get fired on the first call of the `Popper.update()` method inside the `Popper` constructor!
-     * @method
-     * @memberof Popper
-     * @param {Function} callback
-     */
-    Popper.prototype.onUpdate = function(callback) {
-        this.state.updateCallback = callback;
-        return this;
-    };
-
-    /**
-     * Helper used to generate poppers from a configuration file
-     * @method
-     * @memberof Popper
-     * @param config {Object} configuration
-     * @returns {HTMLElement} popper
-     */
-    Popper.prototype.parse = function(config) {
-        var defaultConfig = {
-            tagName: 'div',
-            classNames: [ 'popper' ],
-            attributes: [],
-            parent: root.document.body,
-            content: '',
-            contentType: 'text',
-            arrowTagName: 'div',
-            arrowClassNames: [ 'popper__arrow' ],
-            arrowAttributes: [ 'x-arrow']
-        };
-        config = Object.assign({}, defaultConfig, config);
-
-        var d = root.document;
-
-        var popper = d.createElement(config.tagName);
-        addClassNames(popper, config.classNames);
-        addAttributes(popper, config.attributes);
-        if (config.contentType === 'node') {
-            popper.appendChild(config.content.jquery ? config.content[0] : config.content);
-        }else if (config.contentType === 'html') {
-            popper.innerHTML = config.content;
-        } else {
-            popper.textContent = config.content;
-        }
-
-        if (config.arrowTagName) {
-            var arrow = d.createElement(config.arrowTagName);
-            addClassNames(arrow, config.arrowClassNames);
-            addAttributes(arrow, config.arrowAttributes);
-            popper.appendChild(arrow);
-        }
-
-        var parent = config.parent.jquery ? config.parent[0] : config.parent;
-
-        // if the given parent is a string, use it to match an element
-        // if more than one element is matched, the first one will be used as parent
-        // if no elements are matched, the script will throw an error
-        if (typeof parent === 'string') {
-            parent = d.querySelectorAll(config.parent);
-            if (parent.length > 1) {
-                console.warn('WARNING: the given `parent` query(' + config.parent + ') matched more than one element, the first one will be used');
-            }
-            if (parent.length === 0) {
-                throw 'ERROR: the given `parent` doesn\'t exists!';
-            }
-            parent = parent[0];
-        }
-        // if the given parent is a DOM nodes list or an array of nodes with more than one element,
-        // the first one will be used as parent
-        if (parent.length > 1 && parent instanceof Element === false) {
-            console.warn('WARNING: you have passed as parent a list of elements, the first one will be used');
-            parent = parent[0];
-        }
-
-        // append the generated popper to its parent
-        parent.appendChild(popper);
-
-        return popper;
-
-        /**
-         * Adds class names to the given element
-         * @function
-         * @ignore
-         * @param {HTMLElement} target
-         * @param {Array} classes
-         */
-        function addClassNames(element, classNames) {
-            classNames.forEach(function(className) {
-                element.classList.add(className);
-            });
-        }
-
-        /**
-         * Adds attributes to the given element
-         * @function
-         * @ignore
-         * @param {HTMLElement} target
-         * @param {Array} attributes
-         * @example
-         * addAttributes(element, [ 'data-info:foobar' ]);
-         */
-        function addAttributes(element, attributes) {
-            attributes.forEach(function(attribute) {
-                element.setAttribute(attribute.split(':')[0], attribute.split(':')[1] || '');
-            });
-        }
-
-    };
-
-    /**
-     * Helper used to get the position which will be applied to the popper
-     * @method
-     * @memberof Popper
-     * @param config {HTMLElement} popper element
-     * @returns {HTMLElement} reference element
-     */
-    Popper.prototype._getPosition = function(popper, reference) {
-        var container = getOffsetParent(reference);
-
-        // Decide if the popper will be fixed
-        // If the reference element is inside a fixed context, the popper will be fixed as well to allow them to scroll together
-        var isParentFixed = isFixed(container);
-        return isParentFixed ? 'fixed' : 'absolute';
-    };
-
-    /**
-     * Helper used to determine if the popper's parent is transformed.
-     * @param  {[type]} popper [description]
-     * @return {[type]}        [description]
-     */
-    Popper.prototype._getIsParentTransformed = function(popper) {
-      return isTransformed(popper.parentNode);
-    };
-
-    /**
-     * Get offsets to the popper
-     * @method
-     * @memberof Popper
-     * @access private
-     * @param {Element} popper - the popper element
-     * @param {Element} reference - the reference element (the popper will be relative to this)
-     * @returns {Object} An object containing the offsets which will be applied to the popper
-     */
-    Popper.prototype._getOffsets = function(popper, reference, placement) {
-        placement = placement.split('-')[0];
-        var popperOffsets = {};
-
-        popperOffsets.position = this.state.position;
-        var isParentFixed = popperOffsets.position === 'fixed';
-
-        var isParentTransformed = this.state.isParentTransformed;
-
-        //
-        // Get reference element position
-        //
-        var offsetParent = (isParentFixed && isParentTransformed) ? getOffsetParent(reference) : getOffsetParent(popper);
-        var referenceOffsets = getOffsetRectRelativeToCustomParent(reference, offsetParent, isParentFixed, isParentTransformed);
-
-        //
-        // Get popper sizes
-        //
-        var popperRect = getOuterSizes(popper);
-
-        //
-        // Compute offsets of popper
-        //
-
-        // depending by the popper placement we have to compute its offsets slightly differently
-        if (['right', 'left'].indexOf(placement) !== -1) {
-            popperOffsets.top = referenceOffsets.top + referenceOffsets.height / 2 - popperRect.height / 2;
-            if (placement === 'left') {
-                popperOffsets.left = referenceOffsets.left - popperRect.width;
-            } else {
-                popperOffsets.left = referenceOffsets.right;
-            }
-        } else {
-            popperOffsets.left = referenceOffsets.left + referenceOffsets.width / 2 - popperRect.width / 2;
-            if (placement === 'top') {
-                popperOffsets.top = referenceOffsets.top - popperRect.height;
-            } else {
-                popperOffsets.top = referenceOffsets.bottom;
-            }
-        }
-
-        // Add width and height to our offsets object
-        popperOffsets.width   = popperRect.width;
-        popperOffsets.height  = popperRect.height;
-
-
-        return {
-            popper: popperOffsets,
-            reference: referenceOffsets
-        };
-    };
-
-
-    /**
-     * Setup needed event listeners used to update the popper position
-     * @method
-     * @memberof Popper
-     * @access private
-     */
-    Popper.prototype._setupEventListeners = function() {
-        // NOTE: 1 DOM access here
-        this.state.updateBound = this.update.bind(this);
-        root.addEventListener('resize', this.state.updateBound);
-        // if the boundariesElement is window we don't need to listen for the scroll event
-        if (this._options.boundariesElement !== 'window') {
-            var target = getScrollParent(this._reference);
-            // here it could be both `body` or `documentElement` thanks to Firefox, we then check both
-            if (target === root.document.body || target === root.document.documentElement) {
-                target = root;
-            }
-            target.addEventListener('scroll', this.state.updateBound);
-        }
-    };
 
-    /**
-     * Remove event listeners used to update the popper position
-     * @method
-     * @memberof Popper
-     * @access private
-     */
-    Popper.prototype._removeEventListeners = function() {
-        // NOTE: 1 DOM access here
-        root.removeEventListener('resize', this.state.updateBound);
-        if (this._options.boundariesElement !== 'window') {
-            var target = getScrollParent(this._reference);
-            // here it could be both `body` or `documentElement` thanks to Firefox, we then check both
-            if (target === root.document.body || target === root.document.documentElement) {
-                target = root;
-            }
-            target.removeEventListener('scroll', this.state.updateBound);
-        }
-        this.state.updateBound = null;
-    };
-
-    /**
-     * Computed the boundaries limits and return them
-     * @method
-     * @memberof Popper
-     * @access private
-     * @param {Object} data - Object containing the property "offsets" generated by `_getOffsets`
-     * @param {Number} padding - Boundaries padding
-     * @param {Element} boundariesElement - Element used to define the boundaries
-     * @returns {Object} Coordinates of the boundaries
-     */
-    Popper.prototype._getBoundaries = function(data, padding, boundariesElement) {
-        // NOTE: 1 DOM access here
-        var boundaries = {};
-        var width, height;
-        if (boundariesElement === 'window') {
-            var body = root.document.body,
-                html = root.document.documentElement;
-
-            height = Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight );
-            width = Math.max( body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth );
-
-            boundaries = {
-                top: 0,
-                right: width,
-                bottom: height,
-                left: 0
-            };
-        } else if (boundariesElement === 'viewport') {
-            var offsetParent = getOffsetParent(this._popper);
-            var scrollParent = getScrollParent(this._popper);
-            var offsetParentRect = getOffsetRect(offsetParent);
-
-            // if the popper is fixed we don't have to substract scrolling from the boundaries
-            var scrollTop = data.offsets.popper.position === 'fixed' ? 0 : scrollParent.scrollTop;
-            var scrollLeft = data.offsets.popper.position === 'fixed' ? 0 : scrollParent.scrollLeft;
-
-            boundaries = {
-                top: 0 - (offsetParentRect.top - scrollTop),
-                right: root.document.documentElement.clientWidth - (offsetParentRect.left - scrollLeft),
-                bottom: root.document.documentElement.clientHeight - (offsetParentRect.top - scrollTop),
-                left: 0 - (offsetParentRect.left - scrollLeft)
-            };
-        } else {
-            if (getOffsetParent(this._popper) === boundariesElement) {
-                boundaries = {
-                    top: 0,
-                    left: 0,
-                    right: boundariesElement.clientWidth,
-                    bottom: boundariesElement.clientHeight
-                };
-            } else {
-                boundaries = getOffsetRect(boundariesElement);
-            }
-        }
-        boundaries.left += padding;
-        boundaries.right -= padding;
-        boundaries.top = boundaries.top + padding;
-        boundaries.bottom = boundaries.bottom - padding;
-        return boundaries;
-    };
-
-
-    /**
-     * Loop trough the list of modifiers and run them in order, each of them will then edit the data object
-     * @method
-     * @memberof Popper
-     * @access public
-     * @param {Object} data
-     * @param {Array} modifiers
-     * @param {Function} ends
-     */
-    Popper.prototype.runModifiers = function(data, modifiers, ends) {
-        var modifiersToRun = modifiers.slice();
-        if (ends !== undefined) {
-            modifiersToRun = this._options.modifiers.slice(0, getArrayKeyIndex(this._options.modifiers, ends));
-        }
-
-        modifiersToRun.forEach(function(modifier) {
-            if (isFunction(modifier)) {
-                data = modifier.call(this, data);
-            }
-        }.bind(this));
-
-        return data;
-    };
+/*
+* @fileOverview Kickass library to create and place poppers near their reference elements.
+* @version 1.0.0-alpha.3
+* @license
+* Copyright (c) 2016 Federico Zivolo and contributors
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+*/
+                    
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+  typeof define === 'function' && define.amd ? define(factory) :
+  (global.Popper = factory());
+}(this, function () { 'use strict';
+
+  /**
+   * The Object.assign() method is used to copy the values of all enumerable own properties from one or more source
+   * objects to a target object. It will return the target object.
+   * This polyfill doesn't support symbol properties, since ES5 doesn't have symbols anyway
+   * Source: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
+   * @function
+   * @ignore
+   */
+  if (!Object.assign) {
+      Object.defineProperty(Object, 'assign', {
+          enumerable: false,
+          configurable: true,
+          writable: true,
+          value: function value(target) {
+              if (target === undefined || target === null) {
+                  throw new TypeError('Cannot convert first argument to object');
+              }
+
+              var to = Object(target);
+              for (var i = 1; i < arguments.length; i++) {
+                  var nextSource = arguments[i];
+                  if (nextSource === undefined || nextSource === null) {
+                      continue;
+                  }
+                  nextSource = Object(nextSource);
+
+                  var keysArray = Object.keys(nextSource);
+                  for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
+                      var nextKey = keysArray[nextIndex];
+                      var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
+                      if (desc !== undefined && desc.enumerable) {
+                          to[nextKey] = nextSource[nextKey];
+                      }
+                  }
+              }
+              return to;
+          }
+      });
+  }
+
+  /**
+   * Polyfill for requestAnimationFrame
+   * @function
+   * @ignore
+   */
+  if (!window.requestAnimationFrame) {
+      var lastTime = 0;
+      var vendors = ['ms', 'moz', 'webkit', 'o'];
+      for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+          window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
+          window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
+      }
 
-    /**
-     * Helper used to know if the given modifier depends from another one.
-     * @method
-     * @memberof Popper
-     * @returns {Boolean}
-     */
-
-    Popper.prototype.isModifierRequired = function(requesting, requested) {
-        var index = getArrayKeyIndex(this._options.modifiers, requesting);
-        return !!this._options.modifiers.slice(0, index).filter(function(modifier) {
-            return modifier === requested;
-        }).length;
-    };
+      if (!window.requestAnimationFrame) {
+          window.requestAnimationFrame = function (callback) {
+              var currTime = new Date().getTime();
+              var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+              var id = window.setTimeout(function () {
+                  callback(currTime + timeToCall);
+              }, timeToCall);
+              lastTime = currTime + timeToCall;
+              return id;
+          };
+      }
 
-    //
-    // Modifiers
-    //
-
-    /**
-     * Modifiers list
-     * @namespace Popper.modifiers
-     * @memberof Popper
-     * @type {Object}
-     */
-    Popper.prototype.modifiers = {};
-
-    /**
-     * Apply the computed styles to the popper element
-     * @method
-     * @memberof Popper.modifiers
-     * @argument {Object} data - The data object generated by `update` method
-     * @returns {Object} The same data object
-     */
-    Popper.prototype.modifiers.applyStyle = function(data) {
-        // apply the final offsets to the popper
-        // NOTE: 1 DOM access here
-        var styles = {
-            position: data.offsets.popper.position
-        };
-
-        // round top and left to avoid blurry text
-        var left = Math.round(data.offsets.popper.left);
-        var top = Math.round(data.offsets.popper.top);
-
-        // if gpuAcceleration is set to true and transform is supported, we use `translate3d` to apply the position to the popper
-        // we automatically use the supported prefixed version if needed
-        var prefixedProperty;
-        if (this._options.gpuAcceleration && (prefixedProperty = getSupportedPropertyName('transform'))) {
-            styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';
-            styles.top = 0;
-            styles.left = 0;
-        }
-        // othwerise, we use the standard `left` and `top` properties
-        else {
-            styles.left =left;
-            styles.top = top;
-        }
-
-        // any property present in `data.styles` will be applied to the popper,
-        // in this way we can make the 3rd party modifiers add custom styles to it
-        // Be aware, modifiers could override the properties defined in the previous
-        // lines of this modifier!
-        Object.assign(styles, data.styles);
-
-        setStyle(this._popper, styles);
-
-        // set an attribute which will be useful to style the tooltip (use it to properly position its arrow)
-        // NOTE: 1 DOM access here
-        this._popper.setAttribute('x-placement', data.placement);
-
-        // if the arrow style has been computed, apply the arrow style
-        if (data.offsets.arrow) {
-            setStyle(data.arrowElement, data.offsets.arrow);
-        }
-
-        // return the data object to allow chaining of other modifiers
-        return data;
-    };
+      if (!window.cancelAnimationFrame) {
+          window.cancelAnimationFrame = function (id) {
+              clearTimeout(id);
+          };
+      }
+  }
+
+  /**
+   * Return the index of the matching object
+   * @method
+   * @memberof Popper.Utils
+   * @argument {Array} arr
+   * @argument prop
+   * @argument value
+   * @returns index or -1
+   */
+  function findIndex(arr, prop, value) {
+    // use filter instead of find because find has less cross-browser support
+    var match = arr.filter(function (obj) {
+      return obj[prop] === value;
+    })[0];
+    return arr.indexOf(match);
+  }
+
+  /**
+   * Returns the offset parent of the given element
+   * @method
+   * @memberof Popper.Utils
+   * @argument {Element} element
+   * @returns {Element} offset parent
+   */
+  function getOffsetParent(element) {
+    // NOTE: 1 DOM access here
+    var offsetParent = element.offsetParent;
+    return !offsetParent || offsetParent.nodeName === 'BODY' ? window.document.documentElement : offsetParent;
+  }
+
+  /**
+   * Get CSS computed property of the given element
+   * @method
+   * @memberof Popper.Utils
+   * @argument {Eement} element
+   * @argument {String} property
+   */
+  function getStyleComputedProperty(element, property) {
+      if (element.nodeType !== 1) {
+          return [];
+      }
+      // NOTE: 1 DOM access here
+      var css = window.getComputedStyle(element, null);
+      return css[property];
+  }
+
+  /**
+   * Returns the parentNode or the host of the element
+   * @method
+   * @memberof Popper.Utils
+   * @argument {Element} element
+   * @returns {Element} parent
+   */
+  function getParentNode(element) {
+    return element.parentNode || element.host;
+  }
+
+  /**
+   * Returns the scrolling parent of the given element
+   * @method
+   * @memberof Popper.Utils
+   * @argument {Element} element
+   * @returns {Element} offset parent
+   */
+  function getScrollParent(element) {
+      if (element === window.document) {
+          // Firefox puts the scrollTOp value on `documentElement` instead of `body`, we then check which of them is
+          // greater than 0 and return the proper element
+          if (window.document.body.scrollTop) {
+              return window.document.body;
+          } else {
+              return window.document.documentElement;
+          }
+      }
 
-    /**
-     * Modifier used to shift the popper on the start or end of its reference element side
-     * @method
-     * @memberof Popper.modifiers
-     * @argument {Object} data - The data object generated by `update` method
-     * @returns {Object} The data object, properly modified
-     */
-    Popper.prototype.modifiers.shift = function(data) {
-        var placement = data.placement;
-        var basePlacement = placement.split('-')[0];
-        var shiftVariation = placement.split('-')[1];
-
-        // if shift shiftVariation is specified, run the modifier
-        if (shiftVariation) {
-            var reference = data.offsets.reference;
-            var popper = getPopperClientRect(data.offsets.popper);
-
-            var shiftOffsets = {
-                y: {
-                    start:  { top: reference.top },
-                    end:    { top: reference.top + reference.height - popper.height }
-                },
-                x: {
-                    start:  { left: reference.left },
-                    end:    { left: reference.left + reference.width - popper.width }
-                }
-            };
-
-            var axis = ['bottom', 'top'].indexOf(basePlacement) !== -1 ? 'x' : 'y';
-
-            data.offsets.popper = Object.assign(popper, shiftOffsets[axis][shiftVariation]);
-        }
-
-        return data;
-    };
+      // Firefox want us to check `-x` and `-y` variations as well
+      if (['scroll', 'auto'].indexOf(getStyleComputedProperty(element, 'overflow')) !== -1 || ['scroll', 'auto'].indexOf(getStyleComputedProperty(element, 'overflow-x')) !== -1 || ['scroll', 'auto'].indexOf(getStyleComputedProperty(element, 'overflow-y')) !== -1) {
+          // If the detected scrollParent is body, we perform an additional check on its parentNode
+          // in this way we'll get body if the browser is Chrome-ish, or documentElement otherwise
+          // fixes issue #65
+          return element === window.document.body ? getScrollParent(getParentNode(element)) : element;
+      }
+      return getParentNode(element) ? getScrollParent(getParentNode(element)) : element;
+  }
+
+  /**
+   * Get the position of the given element, relative to its offset parent
+   * @method
+   * @memberof Popper.Utils
+   * @param {Element} element
+   * @return {Object} position - Coordinates of the element and its `scrollTop`
+   */
+  function getOffsetRect(element) {
+      var elementRect = {
+          width: element.offsetWidth,
+          height: element.offsetHeight,
+          left: element.offsetLeft,
+          top: element.offsetTop
+      };
+
+      elementRect.right = elementRect.left + elementRect.width;
+      elementRect.bottom = elementRect.top + elementRect.height;
+
+      // position
+      return elementRect;
+  }
+
+  /**
+   * Computed the boundaries limits and return them
+   * @method
+   * @memberof Popper.Utils
+   * @param {Object} data - Object containing the property "offsets" generated by `_getOffsets`
+   * @param {Number} padding - Boundaries padding
+   * @param {Element} boundariesElement - Element used to define the boundaries
+   * @returns {Object} Coordinates of the boundaries
+   */
+  function getBoundaries(popper, data, padding, boundariesElement) {
+      // NOTE: 1 DOM access here
+      var boundaries = {};
+      if (boundariesElement === 'window') {
+          var body = window.document.body;
+          var html = window.document.documentElement;
+          var height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
+          var width = Math.max(body.scrollWidth, body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth);
+
+          boundaries = {
+              top: 0,
+              right: width,
+              bottom: height,
+              left: 0
+          };
+      } else if (boundariesElement === 'viewport') {
+          var offsetParent = getOffsetParent(popper);
+          var scrollParent = getScrollParent(popper);
+          var offsetParentRect = getOffsetRect(offsetParent);
+
+          // if the popper is fixed we don't have to substract scrolling from the boundaries
+          var scrollTop = data.offsets.popper.position === 'fixed' ? 0 : scrollParent.scrollTop;
+          var scrollLeft = data.offsets.popper.position === 'fixed' ? 0 : scrollParent.scrollLeft;
+
+          boundaries = {
+              top: 0 - (offsetParentRect.top - scrollTop),
+              right: window.document.documentElement.clientWidth - (offsetParentRect.left - scrollLeft),
+              bottom: window.document.documentElement.clientHeight - (offsetParentRect.top - scrollTop),
+              left: 0 - (offsetParentRect.left - scrollLeft)
+          };
+      } else {
+          if (getOffsetParent(popper) === boundariesElement) {
+              boundaries = {
+                  top: 0,
+                  left: 0,
+                  right: boundariesElement.clientWidth,
+                  bottom: boundariesElement.clientHeight
+              };
+          } else {
+              boundaries = getOffsetRect(boundariesElement);
+          }
+      }
+      boundaries.left += padding;
+      boundaries.right -= padding;
+      boundaries.top = boundaries.top + padding;
+      boundaries.bottom = boundaries.bottom - padding;
+      return boundaries;
+  }
+
+  /**
+   * Get bounding client rect of given element
+   * @method
+   * @memberof Popper.Utils
+   * @param {HTMLElement} element
+   * @return {Object} client rect
+   */
+  function getBoundingClientRect(element) {
+      var rect = element.getBoundingClientRect();
+      return {
+          left: rect.left,
+          top: rect.top,
+          right: rect.right,
+          bottom: rect.bottom,
+          width: rect.right - rect.left,
+          height: rect.bottom - rect.top
+      };
+  }
+
+  /**
+   * Given an element and one of its parents, return the offset
+   * @method
+   * @memberof Popper.Utils
+   * @param {HTMLElement} element
+   * @param {HTMLElement} parent
+   * @return {Object} rect
+   */
+  function getOffsetRectRelativeToCustomParent(element, parent, fixed, transformed) {
+      var elementRect = getBoundingClientRect(element);
+      var parentRect = getBoundingClientRect(parent);
+
+      if (fixed && !transformed) {
+          var scrollParent = getScrollParent(parent);
+          parentRect.top += scrollParent.scrollTop;
+          parentRect.bottom += scrollParent.scrollTop;
+          parentRect.left += scrollParent.scrollLeft;
+          parentRect.right += scrollParent.scrollLeft;
+      }
 
+      var rect = {
+          top: elementRect.top - parentRect.top,
+          left: elementRect.left - parentRect.left,
+          bottom: elementRect.top - parentRect.top + elementRect.height,
+          right: elementRect.left - parentRect.left + elementRect.width,
+          width: elementRect.width,
+          height: elementRect.height
+      };
+      return rect;
+  }
+
+  /**
+   * Get the outer sizes of the given element (offset size + margins)
+   * @method
+   * @memberof Popper.Utils
+   * @argument {Element} element
+   * @returns {Object} object containing width and height properties
+   */
+  function getOuterSizes(element) {
+      // NOTE: 1 DOM access here
+      var display = element.style.display;
+      var visibility = element.style.visibility;
+
+      element.style.display = 'block';
+      element.style.visibility = 'hidden';
+
+      // original method
+      var styles = window.getComputedStyle(element);
+      var x = parseFloat(styles.marginTop) + parseFloat(styles.marginBottom);
+      var y = parseFloat(styles.marginLeft) + parseFloat(styles.marginRight);
+      var result = {
+          width: element.offsetWidth + y,
+          height: element.offsetHeight + x
+      };
+
+      // reset element styles
+      element.style.display = display;
+      element.style.visibility = visibility;
+
+      return result;
+  }
+
+  /**
+   * Given the popper offsets, generate an output similar to getBoundingClientRect
+   * @method
+   * @memberof Popper.Utils
+   * @argument {Object} popperOffsets
+   * @returns {Object} ClientRect like output
+   */
+  function getPopperClientRect(popperOffsets) {
+      return Object.assign({}, popperOffsets, {
+          right: popperOffsets.left + popperOffsets.width,
+          bottom: popperOffsets.top + popperOffsets.height
+      });
+  }
+
+  /**
+   * Check if the given element is fixed or is inside a fixed parent
+   * @method
+   * @memberof Popper.Utils
+   * @argument {Element} element
+   * @argument {Element} customContainer
+   * @returns {Boolean} answer to "isFixed?"
+   */
+  function isFixed(element) {
+      if (element === window.document.body) {
+          return false;
+      }
+      if (getStyleComputedProperty(element, 'position') === 'fixed') {
+          return true;
+      }
+      return getParentNode(element) ? isFixed(getParentNode(element)) : element;
+  }
+
+  /**
+   * Helper used to get the position which will be applied to the popper
+   * @method
+   * @memberof Popper.Utils
+   * @param config {HTMLElement} popper element
+   * @returns {HTMLElement} reference element
+   */
+  function getPosition(popper, reference) {
+    var container = getOffsetParent(reference);
+
+    // Decide if the popper will be fixed
+    // If the reference element is inside a fixed context, the popper will be fixed as well to allow them to scroll together
+    var isParentFixed = isFixed(container);
+    return isParentFixed ? 'fixed' : 'absolute';
+  }
+
+  /**
+   * Get the prefixed supported property name
+   * @method
+   * @memberof Popper.Utils
+   * @argument {String} property (camelCase)
+   * @returns {String} prefixed property (camelCase)
+   */
+  function getSupportedPropertyName(property) {
+      var prefixes = ['', 'ms', 'webkit', 'moz', 'o'];
+
+      for (var i = 0; i < prefixes.length; i++) {
+          var toCheck = prefixes[i] ? prefixes[i] + property.charAt(0).toUpperCase() + property.slice(1) : property;
+          if (typeof window.document.body.style[toCheck] !== 'undefined') {
+              return toCheck;
+          }
+      }
+      return null;
+  }
+
+  /**
+   * Check if the given variable is a function
+   * @method
+   * @memberof Popper.Utils
+   * @argument {Element} element - Element to check
+   * @returns {Boolean} answer to: is a function?
+   */
+  function isFunction(functionToCheck) {
+    var getType = {};
+    return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
+  }
+
+  /**
+   * Helper used to know if the given modifier depends from another one.
+   * @method
+   * @memberof Popper.Utils
+   * @returns {Boolean}
+   */
+  function isModifierRequired(modifiers, requesting, requested) {
+      return !!modifiers.filter(function (modifier) {
+          if (modifier.name === requested) {
+              return true;
+          } else if (modifier.name === requesting) {
+              return false;
+          }
+          return false;
+      }).length;
+  }
+
+  /**
+   * Tells if a given input is a number
+   * @method
+   * @memberof Popper.Utils
+   * @param {*} input to check
+   * @return {Boolean}
+   */
+  function isNumeric(n) {
+    return n !== '' && !isNaN(parseFloat(n)) && isFinite(n);
+  }
+
+  /**
+   * Check if the given element has transforms applied to itself or a parent
+   * @method
+   * @memberof Popper.Utils
+   * @param  {Element} element
+   * @return {Boolean} answer to "isTransformed?"
+   */
+  function isTransformed(element) {
+      if (element === window.document.body) {
+          return false;
+      }
+      if (getStyleComputedProperty(element, 'transform') !== 'none') {
+          return true;
+      }
+      return getParentNode(element) ? isTransformed(getParentNode(element)) : element;
+  }
+
+  /**
+   * Loop trough the list of modifiers and run them in order, each of them will then edit the data object
+   * @method
+   * @memberof Popper.Utils
+   * @param {Object} data
+   * @param {Array} modifiers
+   * @param {Function} ends
+   */
+  function runModifiers(modifiers, options, data, ends) {
+      var modifiersToRun = ends === undefined ? modifiers : modifiers.slice(0, findIndex(modifiers, 'name', ends));
+
+      modifiersToRun.forEach(function (modifier) {
+          if (modifier.enabled && isFunction(modifier.function)) {
+              data = modifier.function(data, modifier);
+          }
+      });
+
+      return data;
+  }
+
+  /**
+   * Set the style to the given popper
+   * @method
+   * @memberof Popper.Utils
+   * @argument {Element} element - Element to apply the style to
+   * @argument {Object} styles - Object with a list of properties and values which will be applied to the element
+   */
+  function setStyle(element, styles) {
+      Object.keys(styles).forEach(function (prop) {
+          var unit = '';
+          // add unit if the value is numeric and is one of the following
+          if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && isNumeric(styles[prop])) {
+              unit = 'px';
+          }
+          element.style[prop] = styles[prop] + unit;
+      });
+  }
+
+  /** @namespace Popper.Utils */
+  var Utils = {
+      findIndex: findIndex,
+      getBoundaries: getBoundaries,
+      getBoundingClientRect: getBoundingClientRect,
+      getOffsetParent: getOffsetParent,
+      getOffsetRectRelativeToCustomParent: getOffsetRectRelativeToCustomParent,
+      getOuterSizes: getOuterSizes,
+      getPopperClientRect: getPopperClientRect,
+      getPosition: getPosition,
+      getScrollParent: getScrollParent,
+      getStyleComputedProperty: getStyleComputedProperty,
+      getSupportedPropertyName: getSupportedPropertyName,
+      isFixed: isFixed,
+      isFunction: isFunction,
+      isModifierRequired: isModifierRequired,
+      isNumeric: isNumeric,
+      isTransformed: isTransformed,
+      runModifiers: runModifiers,
+      setStyle: setStyle
+  };
+
+  /**
+   * Get offsets to the popper
+   * @method
+   * @memberof Popper.Utils
+   * @param {Element} popper - the popper element
+   * @param {Element} reference - the reference element (the popper will be relative to this)
+   * @returns {Object} An object containing the offsets which will be applied to the popper
+   */
+  function getOffsets(state, popper, reference, placement) {
+      placement = placement.split('-')[0];
+
+      var popperOffsets = {};
+      popperOffsets.position = state.position;
+
+      var isParentFixed = popperOffsets.position === 'fixed';
+      var isParentTransformed = state.isParentTransformed;
+
+      //
+      // Get reference element position
+      //
+      var offsetParent = getOffsetParent(isParentFixed && isParentTransformed ? reference : popper);
+      var referenceOffsets = getOffsetRectRelativeToCustomParent(reference, offsetParent, isParentFixed, isParentTransformed);
+
+      //
+      // Get popper sizes
+      //
+      var popperRect = getOuterSizes(popper);
+
+      //
+      // Compute offsets of popper
+      //
+
+      // depending by the popper placement we have to compute its offsets slightly differently
+      if (['right', 'left'].indexOf(placement) !== -1) {
+          popperOffsets.top = referenceOffsets.top + referenceOffsets.height / 2 - popperRect.height / 2;
+          if (placement === 'left') {
+              popperOffsets.left = referenceOffsets.left - popperRect.width;
+          } else {
+              popperOffsets.left = referenceOffsets.right;
+          }
+      } else {
+          popperOffsets.left = referenceOffsets.left + referenceOffsets.width / 2 - popperRect.width / 2;
+          if (placement === 'top') {
+              popperOffsets.top = referenceOffsets.top - popperRect.height;
+          } else {
+              popperOffsets.top = referenceOffsets.bottom;
+          }
+      }
 
-    /**
-     * Modifier used to make sure the popper does not overflows from it's boundaries
-     * @method
-     * @memberof Popper.modifiers
-     * @argument {Object} data - The data object generated by `update` method
-     * @returns {Object} The data object, properly modified
-     */
-    Popper.prototype.modifiers.preventOverflow = function(data) {
-        var order = this._options.preventOverflowOrder;
-        var popper = getPopperClientRect(data.offsets.popper);
-
-        var check = {
-            left: function() {
-                var left = popper.left;
-                if (popper.left < data.boundaries.left) {
-                    left = Math.max(popper.left, data.boundaries.left);
-                }
-                return { left: left };
-            },
-            right: function() {
-                var left = popper.left;
-                if (popper.right > data.boundaries.right) {
-                    left = Math.min(popper.left, data.boundaries.right - popper.width);
-                }
-                return { left: left };
-            },
-            top: function() {
-                var top = popper.top;
-                if (popper.top < data.boundaries.top) {
-                    top = Math.max(popper.top, data.boundaries.top);
-                }
-                return { top: top };
-            },
-            bottom: function() {
-                var top = popper.top;
-                if (popper.bottom > data.boundaries.bottom) {
-                    top = Math.min(popper.top, data.boundaries.bottom - popper.height);
-                }
-                return { top: top };
-            }
-        };
-
-        order.forEach(function(direction) {
-            data.offsets.popper = Object.assign(popper, check[direction]());
-        });
-
-        return data;
-    };
+      // Add width and height to our offsets object
+      popperOffsets.width = popperRect.width;
+      popperOffsets.height = popperRect.height;
+
+      return {
+          popper: popperOffsets,
+          reference: referenceOffsets
+      };
+  }
+
+  /**
+   * Setup needed event listeners used to update the popper position
+   * @method
+   * @memberof Popper.Utils
+   * @private
+   */
+  function setupEventListeners(reference, options, state, updateBound) {
+      // NOTE: 1 DOM access here
+      state.updateBound = updateBound;
+      window.addEventListener('resize', state.updateBound, { passive: true });
+      // if the boundariesElement is window we don't need to listen for the scroll event
+      if (options.boundariesElement !== 'window') {
+          var target = getScrollParent(reference);
+          // here it could be both `body` or `documentElement` thanks to Firefox, we then check both
+          if (target === window.document.body || target === window.document.documentElement) {
+              target = window;
+          }
+          target.addEventListener('scroll', state.updateBound, { passive: true });
+      }
+  }
+
+  /**
+   * Remove event listeners used to update the popper position
+   * @method
+   * @memberof Popper.Utils
+   * @private
+   */
+  function removeEventListeners(reference, state, options) {
+      // NOTE: 1 DOM access here
+      window.removeEventListener('resize', state.updateBound);
+      if (options.boundariesElement !== 'window') {
+          var target = getScrollParent(reference);
+          // here it could be both `body` or `documentElement` thanks to Firefox, we then check both
+          if (target === window.document.body || target === window.document.documentElement) {
+              target = window;
+          }
+          target.removeEventListener('scroll', state.updateBound);
+      }
+      state.updateBound = null;
+      return state;
+  }
+
+  /**
+   * Sorts the modifiers based on their `order` property
+   * @method
+   * @memberof Popper.Utils
+   */
+  function sortModifiers(a, b) {
+      if (a.order < b.order) {
+          return -1;
+      } else if (a.order > b.order) {
+          return 1;
+      }
+      return 0;
+  }
+
+  /**
+   * Apply the computed styles to the popper element
+   * @method
+   * @memberof Modifiers
+   * @argument {Object} data - The data object generated by `update` method
+   * @argument {Object} options - Modifiers configuration and options
+   * @returns {Object} The same data object
+   */
+  function applyStyle(data) {
+      // apply the final offsets to the popper
+      // NOTE: 1 DOM access here
+      var styles = {
+          position: data.offsets.popper.position
+      };
+
+      // round top and left to avoid blurry text
+      var left = Math.round(data.offsets.popper.left);
+      var top = Math.round(data.offsets.popper.top);
+
+      // if gpuAcceleration is set to true and transform is supported, we use `translate3d` to apply the position to the popper
+      // we automatically use the supported prefixed version if needed
+      var prefixedProperty = getSupportedPropertyName('transform');
+      if (data.instance.options.gpuAcceleration && prefixedProperty) {
+          styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';
+          styles.top = 0;
+          styles.left = 0;
+      }
+      // othwerise, we use the standard `left` and `top` properties
+      else {
+              styles.left = left;
+              styles.top = top;
+          }
+
+      // any property present in `data.styles` will be applied to the popper,
+      // in this way we can make the 3rd party modifiers add custom styles to it
+      // Be aware, modifiers could override the properties defined in the previous
+      // lines of this modifier!
+      Object.assign(styles, data.styles);
+
+      setStyle(data.instance.popper, styles);
+
+      // set an attribute which will be useful to style the tooltip (use it to properly position its arrow)
+      // NOTE: 1 DOM access here
+      data.instance.popper.setAttribute('x-placement', data.placement);
+
+      // if the arrow style has been computed, apply the arrow style
+      if (data.offsets.arrow) {
+          setStyle(data.arrowElement, data.offsets.arrow);
+      }
 
-    /**
-     * Modifier used to make sure the popper is always near its reference
-     * @method
-     * @memberof Popper.modifiers
-     * @argument {Object} data - The data object generated by _update method
-     * @returns {Object} The data object, properly modified
-     */
-    Popper.prototype.modifiers.keepTogether = function(data) {
-        var popper  = getPopperClientRect(data.offsets.popper);
-        var reference = data.offsets.reference;
-        var f = Math.floor;
-
-        if (popper.right < f(reference.left)) {
-            data.offsets.popper.left = f(reference.left) - popper.width;
-        }
-        if (popper.left > f(reference.right)) {
-            data.offsets.popper.left = f(reference.right);
-        }
-        if (popper.bottom < f(reference.top)) {
-            data.offsets.popper.top = f(reference.top) - popper.height;
-        }
-        if (popper.top > f(reference.bottom)) {
-            data.offsets.popper.top = f(reference.bottom);
-        }
-
-        return data;
-    };
+      return data;
+  }
+
+  /**
+   * Set the x-placement attribute before everything else because it could be used to add margins to the popper
+   * margins needs to be calculated to get the correct popper offsets
+   * @method
+   * @memberof Popper.modifiers
+   * @param {HTMLElement} reference - The reference element used to position the popper
+   * @param {HTMLElement} popper - The HTML element used as popper.
+   * @param {Object} options - Popper.js options
+   */
+  function applyStyleOnLoad(reference, popper, options) {
+      popper.setAttribute('x-placement', options.placement);
+  }
+
+  /**
+   * Modifier used to move the arrows on the edge of the popper to make sure them are always between the popper and the reference element
+   * It will use the CSS outer size of the arrow element to know how many pixels of conjuction are needed
+   * @method
+   * @memberof Modifiers
+   * @argument {Object} data - The data object generated by update method
+   * @argument {Object} options - Modifiers configuration and options
+   * @returns {Object} The data object, properly modified
+   */
+  function arrow(data, options) {
+      var arrow = options.element;
+
+      // if the arrowElement is a string, suppose it's a CSS selector
+      if (typeof arrow === 'string') {
+          arrow = data.instance.popper.querySelector(arrow);
+      }
 
-    /**
-     * Modifier used to flip the placement of the popper when the latter is starting overlapping its reference element.
-     * Requires the `preventOverflow` modifier before it in order to work.
-     * **NOTE:** This modifier will run all its previous modifiers everytime it tries to flip the popper!
-     * @method
-     * @memberof Popper.modifiers
-     * @argument {Object} data - The data object generated by _update method
-     * @returns {Object} The data object, properly modified
-     */
-    Popper.prototype.modifiers.flip = function(data) {
-        // check if preventOverflow is in the list of modifiers before the flip modifier.
-        // otherwise flip would not work as expected.
-        if (!this.isModifierRequired(this.modifiers.flip, this.modifiers.preventOverflow)) {
-            console.warn('WARNING: preventOverflow modifier is required by flip modifier in order to work, be sure to include it before flip!');
-            return data;
-        }
-
-        if (data.flipped && data.placement === data._originalPlacement) {
-            // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides
-            return data;
-        }
-
-        var placement = data.placement.split('-')[0];
-        var placementOpposite = getOppositePlacement(placement);
-        var variation = data.placement.split('-')[1] || '';
-
-        var flipOrder = [];
-        if(this._options.flipBehavior === 'flip') {
-            flipOrder = [
-                placement,
-                placementOpposite
-            ];
-        } else {
-            flipOrder = this._options.flipBehavior;
-        }
-
-        flipOrder.forEach(function(step, index) {
-            if (placement !== step || flipOrder.length === index + 1) {
-                return;
-            }
-
-            placement = data.placement.split('-')[0];
-            placementOpposite = getOppositePlacement(placement);
-
-            var popperOffsets = getPopperClientRect(data.offsets.popper);
-
-            // this boolean is used to distinguish right and bottom from top and left
-            // they need different computations to get flipped
-            var a = ['right', 'bottom'].indexOf(placement) !== -1;
-
-            // using Math.floor because the reference offsets may contain decimals we are not going to consider here
-            if (
-                a && Math.floor(data.offsets.reference[placement]) > Math.floor(popperOffsets[placementOpposite]) ||
-                !a && Math.floor(data.offsets.reference[placement]) < Math.floor(popperOffsets[placementOpposite])
-            ) {
-                // we'll use this boolean to detect any flip loop
-                data.flipped = true;
-                data.placement = flipOrder[index + 1];
-                if (variation) {
-                    data.placement += '-' + variation;
-                }
-                data.offsets.popper = this._getOffsets(this._popper, this._reference, data.placement).popper;
-
-                data = this.runModifiers(data, this._options.modifiers, this._flip);
-            }
-        }.bind(this));
-        return data;
-    };
+      // if arrow element is not found, don't run the modifier
+      if (!arrow) {
+          return data;
+      }
 
-    /**
-     * Modifier used to add an offset to the popper, useful if you more granularity positioning your popper.
-     * The offsets will shift the popper on the side of its reference element.
-     * @method
-     * @memberof Popper.modifiers
-     * @argument {Object} data - The data object generated by _update method
-     * @returns {Object} The data object, properly modified
-     */
-    Popper.prototype.modifiers.offset = function(data) {
-        var offset = this._options.offset;
-        var popper  = data.offsets.popper;
-
-        if (data.placement.indexOf('left') !== -1) {
-            popper.top -= offset;
-        }
-        else if (data.placement.indexOf('right') !== -1) {
-            popper.top += offset;
-        }
-        else if (data.placement.indexOf('top') !== -1) {
-            popper.left -= offset;
-        }
-        else if (data.placement.indexOf('bottom') !== -1) {
-            popper.left += offset;
-        }
-        return data;
-    };
+      // the arrow element must be child of its popper
+      if (!data.instance.popper.contains(arrow)) {
+          console.warn('WARNING: `arrowElement` must be child of its popper element!');
+          return data;
+      }
 
-    /**
-     * Modifier used to move the arrows on the edge of the popper to make sure them are always between the popper and the reference element
-     * It will use the CSS outer size of the arrow element to know how many pixels of conjuction are needed
-     * @method
-     * @memberof Popper.modifiers
-     * @argument {Object} data - The data object generated by _update method
-     * @returns {Object} The data object, properly modified
-     */
-    Popper.prototype.modifiers.arrow = function(data) {
-        var arrow  = this._options.arrowElement;
-
-        // if the arrowElement is a string, suppose it's a CSS selector
-        if (typeof arrow === 'string') {
-            arrow = this._popper.querySelector(arrow);
-        }
-
-        // if arrow element is not found, don't run the modifier
-        if (!arrow) {
-            return data;
-        }
-
-        // the arrow element must be child of its popper
-        if (!this._popper.contains(arrow)) {
-            console.warn('WARNING: `arrowElement` must be child of its popper element!');
-            return data;
-        }
-
-        // arrow depends on keepTogether in order to work
-        if (!this.isModifierRequired(this.modifiers.arrow, this.modifiers.keepTogether)) {
-            console.warn('WARNING: keepTogether modifier is required by arrow modifier in order to work, be sure to include it before arrow!');
-            return data;
-        }
-
-        var arrowStyle  = {};
-        var placement   = data.placement.split('-')[0];
-        var popper      = getPopperClientRect(data.offsets.popper);
-        var reference   = data.offsets.reference;
-        var isVertical  = ['left', 'right'].indexOf(placement) !== -1;
-
-        var len         = isVertical ? 'height' : 'width';
-        var side        = isVertical ? 'top' : 'left';
-        var altSide     = isVertical ? 'left' : 'top';
-        var opSide      = isVertical ? 'bottom' : 'right';
-        var arrowSize   = getOuterSizes(arrow)[len];
-
-        //
-        // extends keepTogether behavior making sure the popper and its reference have enough pixels in conjuction
-        //
-
-        // top/left side
-        if (reference[opSide] - arrowSize < popper[side]) {
-            data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowSize);
-        }
-        // bottom/right side
-        if (reference[side] + arrowSize > popper[opSide]) {
-            data.offsets.popper[side] += (reference[side] + arrowSize) - popper[opSide];
-        }
-
-        // compute center of the popper
-        var center = reference[side] + (reference[len] / 2) - (arrowSize / 2);
-
-        // Compute the sideValue using the updated popper offsets
-        var sideValue = center - getPopperClientRect(data.offsets.popper)[side];
-
-        // prevent arrow from being placed not contiguously to its popper
-        sideValue = Math.max(Math.min(popper[len] - arrowSize, sideValue), 0);
-        arrowStyle[side] = sideValue;
-        arrowStyle[altSide] = ''; // make sure to remove any old style from the arrow
-
-        data.offsets.arrow = arrowStyle;
-        data.arrowElement = arrow;
-
-        return data;
-    };
+      // arrow depends on keepTogether in order to work
+      if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) {
+          console.warn('WARNING: keepTogether modifier is required by arrow modifier in order to work, be sure to include it before arrow!');
+          return data;
+      }
 
+      var arrowStyle = {};
+      var placement = data.placement.split('-')[0];
+      var popper = getPopperClientRect(data.offsets.popper);
+      var reference = data.offsets.reference;
+      var isVertical = ['left', 'right'].indexOf(placement) !== -1;
+
+      var len = isVertical ? 'height' : 'width';
+      var side = isVertical ? 'top' : 'left';
+      var altSide = isVertical ? 'left' : 'top';
+      var opSide = isVertical ? 'bottom' : 'right';
+      var arrowSize = getOuterSizes(arrow)[len];
+
+      //
+      // extends keepTogether behavior making sure the popper and its reference have enough pixels in conjuction
+      //
+
+      // top/left side
+      if (reference[opSide] - arrowSize < popper[side]) {
+          data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowSize);
+      }
+      // bottom/right side
+      if (reference[side] + arrowSize > popper[opSide]) {
+          data.offsets.popper[side] += reference[side] + arrowSize - popper[opSide];
+      }
 
-    //
-    // Helpers
-    //
-
-    /**
-     * Get the outer sizes of the given element (offset size + margins)
-     * @function
-     * @ignore
-     * @argument {Element} element
-     * @returns {Object} object containing width and height properties
-     */
-    function getOuterSizes(element) {
-        // NOTE: 1 DOM access here
-        var _display = element.style.display, _visibility = element.style.visibility;
-        element.style.display = 'block'; element.style.visibility = 'hidden';
-        var calcWidthToForceRepaint = element.offsetWidth;
-
-        // original method
-        var styles = root.getComputedStyle(element);
-        var x = parseFloat(styles.marginTop) + parseFloat(styles.marginBottom);
-        var y = parseFloat(styles.marginLeft) + parseFloat(styles.marginRight);
-        var result = { width: element.offsetWidth + y, height: element.offsetHeight + x };
-
-        // reset element styles
-        element.style.display = _display; element.style.visibility = _visibility;
-        return result;
-    }
+      // compute center of the popper
+      var center = reference[side] + reference[len] / 2 - arrowSize / 2;
+
+      // Compute the sideValue using the updated popper offsets
+      var sideValue = center - getPopperClientRect(data.offsets.popper)[side];
+
+      // prevent arrow from being placed not contiguously to its popper
+      sideValue = Math.max(Math.min(popper[len] - arrowSize, sideValue), 0);
+      arrowStyle[side] = sideValue;
+      arrowStyle[altSide] = ''; // make sure to remove any old style from the arrow
+
+      data.offsets.arrow = arrowStyle;
+      data.arrowElement = arrow;
+
+      return data;
+  }
+
+  /**
+   * Get the opposite placement of the given one/
+   * @method
+   * @memberof Popper.Utils
+   * @argument {String} placement
+   * @returns {String} flipped placement
+   */
+  function getOppositePlacement(placement) {
+    var hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' };
+    return placement.replace(/left|right|bottom|top/g, function (matched) {
+      return hash[matched];
+    });
+  }
+
+  /**
+   * Get the opposite placement variation of the given one/
+   * @method
+   * @memberof Popper.Utils
+   * @argument {String} placement variation
+   * @returns {String} flipped placement variation
+   */
+  function getOppositeVariation(variation) {
+      if (variation === 'end') {
+          return 'start';
+      } else if (variation === 'start') {
+          return 'end';
+      }
+      return variation;
+  }
+
+  /**
+   * Modifier used to flip the placement of the popper when the latter is starting overlapping its reference element.
+   * Requires the `preventOverflow` modifier before it in order to work.
+   * **NOTE:** data.instance modifier will run all its previous modifiers everytime it tries to flip the popper!
+   * @method
+   * @memberof Modifiers
+   * @argument {Object} data - The data object generated by update method
+   * @argument {Object} options - Modifiers configuration and options
+   * @returns {Object} The data object, properly modified
+   */
+  function flip(data, options) {
+      // check if preventOverflow is in the list of modifiers before the flip modifier.
+      // otherwise flip would not work as expected.
+      if (!isModifierRequired(data.instance.modifiers, 'flip', 'preventOverflow')) {
+          console.warn('WARNING: preventOverflow modifier is required by flip modifier in order to work, be sure to include it before flip!');
+          return data;
+      }
 
-    /**
-     * Get the opposite placement of the given one/
-     * @function
-     * @ignore
-     * @argument {String} placement
-     * @returns {String} flipped placement
-     */
-    function getOppositePlacement(placement) {
-        var hash = {left: 'right', right: 'left', bottom: 'top', top: 'bottom' };
-        return placement.replace(/left|right|bottom|top/g, function(matched){
-            return hash[matched];
-        });
-    }
+      if (data.flipped && data.placement === data.originalPlacement) {
+          // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides
+          return data;
+      }
 
-    /**
-     * Given the popper offsets, generate an output similar to getBoundingClientRect
-     * @function
-     * @ignore
-     * @argument {Object} popperOffsets
-     * @returns {Object} ClientRect like output
-     */
-    function getPopperClientRect(popperOffsets) {
-        var offsets = Object.assign({}, popperOffsets);
-        offsets.right = offsets.left + offsets.width;
-        offsets.bottom = offsets.top + offsets.height;
-        return offsets;
-    }
+      var placement = data.placement.split('-')[0];
+      var placementOpposite = getOppositePlacement(placement);
+      var variation = data.placement.split('-')[1] || '';
 
-    /**
-     * Given an array and the key to find, returns its index
-     * @function
-     * @ignore
-     * @argument {Array} arr
-     * @argument keyToFind
-     * @returns index or null
-     */
-    function getArrayKeyIndex(arr, keyToFind) {
-        var i = 0, key;
-        for (key in arr) {
-            if (arr[key] === keyToFind) {
-                return i;
-            }
-            i++;
-        }
-        return null;
-    }
+      var flipOrder = [];
 
-    /**
-     * Get CSS computed property of the given element
-     * @function
-     * @ignore
-     * @argument {Eement} element
-     * @argument {String} property
-     */
-    function getStyleComputedProperty(element, property) {
-        // NOTE: 1 DOM access here
-        var css = root.getComputedStyle(element, null);
-        return css[property];
-    }
+      if (options.behavior === 'flip') {
+          flipOrder = [placement, placementOpposite];
+      } else {
+          flipOrder = options.behavior;
+      }
 
-    /**
-     * Returns the offset parent of the given element
-     * @function
-     * @ignore
-     * @argument {Element} element
-     * @returns {Element} offset parent
-     */
-    function getOffsetParent(element) {
-        // NOTE: 1 DOM access here
-        var offsetParent = element.offsetParent;
-        return offsetParent === root.document.body || !offsetParent ? root.document.documentElement : offsetParent;
-    }
+      flipOrder.forEach(function (step, index) {
+          if (placement !== step || flipOrder.length === index + 1) {
+              return data;
+          }
+
+          placement = data.placement.split('-')[0];
+          placementOpposite = getOppositePlacement(placement);
+
+          var popperOffsets = getPopperClientRect(data.offsets.popper);
+
+          // this boolean is used to distinguish right and bottom from top and left
+          // they need different computations to get flipped
+          var a = ['right', 'bottom'].indexOf(placement) !== -1;
+          var b = ['top', 'bottom'].indexOf(placement) !== -1;
+
+          // using Math.floor because the reference offsets may contain decimals we are not going to consider here
+          var flippedPosition = a && Math.floor(data.offsets.reference[placement]) > Math.floor(popperOffsets[placementOpposite]) || !a && Math.floor(data.offsets.reference[placement]) < Math.floor(popperOffsets[placementOpposite]);
+
+          var flippedVariation = options.flipVariations && (b && variation === 'start' && Math.floor(popperOffsets.left) < Math.floor(data.boundaries.left) || b && variation === 'end' && Math.floor(popperOffsets.right) > Math.floor(data.boundaries.right) || !b && variation === 'start' && Math.floor(popperOffsets.top) < Math.floor(data.boundaries.top) || !b && variation === 'end' && Math.floor(popperOffsets.bottom) > Math.floor(data.boundaries.bottom));
+
+          if (flippedPosition || flippedVariation) {
+              // this boolean to detect any flip loop
+              data.flipped = true;
+
+              if (flippedPosition) {
+                  placement = flipOrder[index + 1];
+              }
+              if (flippedVariation) {
+                  variation = getOppositeVariation(variation);
+              }
+
+              data.placement = placement + (variation ? '-' + variation : '');
+              data.offsets.popper = getOffsets(data.instance.state, data.instance.popper, data.instance.reference, data.placement).popper;
+
+              data = runModifiers(data.instance.modifiers, data.instance.options, data, 'flip');
+          }
+      });
+      return data;
+  }
+
+  /**
+   * Modifier used to make sure the popper is always near its reference element
+   * It cares only about the first axis, you can still have poppers with margin
+   * between the popper and its reference element.
+   * @method
+   * @memberof Modifiers
+   * @argument {Object} data - The data object generated by update method
+   * @argument {Object} options - Modifiers configuration and options
+   * @returns {Object} The data object, properly modified
+   */
+  function keepTogether(data) {
+      var popper = getPopperClientRect(data.offsets.popper);
+      var reference = data.offsets.reference;
+      var f = Math.floor;
+      var placement = data.placement.split('-')[0];
+
+      if (['top', 'bottom'].indexOf(placement) !== -1) {
+          if (popper.right < f(reference.left)) {
+              data.offsets.popper.left = f(reference.left) - popper.width;
+          }
+          if (popper.left > f(reference.right)) {
+              data.offsets.popper.left = f(reference.right);
+          }
+      } else {
+          if (popper.bottom < f(reference.top)) {
+              data.offsets.popper.top = f(reference.top) - popper.height;
+          }
+          if (popper.top > f(reference.bottom)) {
+              data.offsets.popper.top = f(reference.bottom);
+          }
+      }
 
-    /**
-     * Returns the scrolling parent of the given element
-     * @function
-     * @ignore
-     * @argument {Element} element
-     * @returns {Element} offset parent
-     */
-    function getScrollParent(element) {
-        if (element === root.document) {
-            // Firefox puts the scrollTOp value on `documentElement` instead of `body`, we then check which of them is
-            // greater than 0 and return the proper element
-            if (root.document.body.scrollTop) {
-                return root.document.body;
-            } else {
-                return root.document.documentElement;
-            }
-        }
-
-        // Firefox want us to check `-x` and `-y` variations as well
-        if (
-            ['scroll', 'auto'].indexOf(getStyleComputedProperty(element, 'overflow')) !== -1 ||
-            ['scroll', 'auto'].indexOf(getStyleComputedProperty(element, 'overflow-x')) !== -1 ||
-            ['scroll', 'auto'].indexOf(getStyleComputedProperty(element, 'overflow-y')) !== -1
-        ) {
-            // If the detected scrollParent is body, we perform an additional check on its parentNode
-            // in this way we'll get body if the browser is Chrome-ish, or documentElement otherwise
-            // fixes issue #65
-            return element === root.document.body ? getScrollParent(element.parentNode) : element;
-        }
-        return element.parentNode ? getScrollParent(element.parentNode) : element;
-    }
+      return data;
+  }
+
+  /**
+   * Modifier used to add an offset to the popper, useful if you more granularity positioning your popper.
+   * The offsets will shift the popper on the side of its reference element.
+   * @method
+   * @memberof Modifiers
+   * @argument {Object} data - The data object generated by update method
+   * @argument {Object} options - Modifiers configuration and options
+   * @argument {Number|String} options.offset=0
+   *      Basic usage allows a number used to nudge the popper by the given amount of pixels.
+   *      You can pass a percentage value as string (eg. `20%`) to nudge by the given percentage (relative to reference element size)
+   *      Other supported units are `vh` and `vw` (relative to viewport)
+   *      Additionally, you can pass a pair of values (eg. `10 20` or `2vh 20%`) to nudge the popper
+   *      on both axis.
+   *      A note about percentage values, if you want to refer a percentage to the popper size instead of the reference element size,
+   *      use `%p` instead of `%` (eg: `20%p`). To make it clearer, you can replace `%` with `%r` and use eg.`10%p 25%r`.
+   *      > **Heads up!** The order of the axis is relative to the popper placement: `bottom` or `top` are `X,Y`, the other are `Y,X`
+   * @returns {Object} The data object, properly modified
+   */
+  function offset(data, options) {
+      var placement = data.placement;
+      var popper = data.offsets.popper;
+
+      var offsets = void 0;
+      if (isNumeric(options.offset)) {
+          offsets = [options.offset, 0];
+      } else {
+          // split the offset in case we are providing a pair of offsets separated
+          // by a blank space
+          offsets = options.offset.split(' ');
+
+          // itherate through each offset to compute them in case they are percentages
+          offsets = offsets.map(function (offset, index) {
+              // separate value from unit
+              var split = offset.match(/(\d*\.?\d*)(.*)/);
+              var value = +split[1];
+              var unit = split[2];
+
+              // use height if placement is left or right and index is 0
+              // otherwise use height
+              // in this way the first offset will use an axis and the second one
+              // will use the other one
+              var useHeight = placement.indexOf('right') !== -1 || placement.indexOf('left') !== -1;
+
+              if (index === 1) {
+                  useHeight = !useHeight;
+              }
+
+              // if is a percentage, we calculate the value of it using as base the
+              // sizes of the reference element
+              if (unit === '%' || unit === '%r') {
+                  var referenceRect = getPopperClientRect(data.offsets.reference);
+                  var len = void 0;
+                  if (useHeight) {
+                      len = referenceRect.height;
+                  } else {
+                      len = referenceRect.width;
+                  }
+                  return len / 100 * value;
+              }
+              // if is a percentage relative to the popper, we calculate the value of it using
+              // as base the sizes of the popper
+              else if (unit === '%p') {
+                      var popperRect = getPopperClientRect(data.offsets.popper);
+                      var _len = void 0;
+                      if (useHeight) {
+                          _len = popperRect.height;
+                      } else {
+                          _len = popperRect.width;
+                      }
+                      return _len / 100 * value;
+                  }
+                  // if is a vh or vw, we calculate the size based on the viewport
+                  else if (unit === 'vh' || unit === 'vw') {
+                          var size = void 0;
+                          if (unit === 'vh') {
+                              size = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
+                          } else {
+                              size = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
+                          }
+                          return size / 100 * value;
+                      }
+                      // if is an explicit pixel unit, we get rid of the unit and keep the value
+                      else if (unit === 'px') {
+                              return +value;
+                          }
+                          // if is an implicit unit, it's px, and we return just the value
+                          else {
+                                  return +offset;
+                              }
+          });
+      }
 
-    /**
-     * Check if the given element is fixed or is inside a fixed parent
-     * @function
-     * @ignore
-     * @argument {Element} element
-     * @argument {Element} customContainer
-     * @returns {Boolean} answer to "isFixed?"
-     */
-    function isFixed(element) {
-        if (element === root.document.body || element.nodeName === 'HTML') {
-            return false;
-        }
-        if (getStyleComputedProperty(element, 'position') === 'fixed') {
-            return true;
-        }
-        return element.parentNode ? isFixed(element.parentNode) : element;
-    }
+      if (data.placement.indexOf('left') !== -1) {
+          popper.top += offsets[0];
+          popper.left -= offsets[1] || 0;
+      } else if (data.placement.indexOf('right') !== -1) {
+          popper.top += offsets[0];
+          popper.left += offsets[1] || 0;
+      } else if (data.placement.indexOf('top') !== -1) {
+          popper.left += offsets[0];
+          popper.top -= offsets[1] || 0;
+      } else if (data.placement.indexOf('bottom') !== -1) {
+          popper.left += offsets[0];
+          popper.top += offsets[1] || 0;
+      }
+      return data;
+  }
+
+  /**
+   * Modifier used to make sure the popper does not overflows from it's boundaries
+   * @method
+   * @memberof Modifiers
+   * @argument {Object} data - The data object generated by `update` method
+   * @argument {Object} options - Modifiers configuration and options
+   * @returns {Object} The data object, properly modified
+   */
+  function preventOverflow(data, options) {
+      function shouldMoveWithTarget(direction) {
+          if (!options.moveWithTarget) {
+              return false;
+          }
+          var placement = data.originalPlacement.split('-')[0];
+
+          if (data.flipped && placement === direction || placement === getOppositePlacement(direction)) {
+              return true;
+          }
+          if (placement !== direction && placement !== getOppositePlacement(direction)) {
+              return true;
+          }
 
-    /**
-     * Check if the given element has transforms applied to itself or a parent
-     * @param  {Element} element
-     * @return {Boolean} answer to "isTransformed?"
-     */
-    function isTransformed(element) {
-      if (element === root.document.body) {
           return false;
       }
-      if (getStyleComputedProperty(element, 'transform') !== 'none') {
-          return true;
+      var order = options.priority;
+      var popper = getPopperClientRect(data.offsets.popper);
+
+      var check = {
+          left: function left() {
+              var left = popper.left;
+              if (popper.left < data.boundaries.left && !shouldMoveWithTarget('left')) {
+                  left = Math.max(popper.left, data.boundaries.left);
+              }
+              return { left: left };
+          },
+          right: function right() {
+              var left = popper.left;
+              if (popper.right > data.boundaries.right && !shouldMoveWithTarget('right')) {
+                  left = Math.min(popper.left, data.boundaries.right - popper.width);
+              }
+              return { left: left };
+          },
+          top: function top() {
+              var top = popper.top;
+              if (popper.top < data.boundaries.top && !shouldMoveWithTarget('top')) {
+                  top = Math.max(popper.top, data.boundaries.top);
+              }
+              return { top: top };
+          },
+          bottom: function bottom() {
+              var top = popper.top;
+              if (popper.bottom > data.boundaries.bottom && !shouldMoveWithTarget('bottom')) {
+                  top = Math.min(popper.top, data.boundaries.bottom - popper.height);
+              }
+              return { top: top };
+          }
+      };
+
+      order.forEach(function (direction) {
+          data.offsets.popper = Object.assign(popper, check[direction]());
+      });
+
+      return data;
+  }
+
+  /**
+   * Modifier used to shift the popper on the start or end of its reference element side
+   * @method
+   * @memberof Modifiers
+   * @argument {Object} data - The data object generated by `update` method
+   * @argument {Object} options - Modifiers configuration and options
+   * @returns {Object} The data object, properly modified
+   */
+  function shift(data) {
+      var placement = data.placement;
+      var basePlacement = placement.split('-')[0];
+      var shiftvariation = placement.split('-')[1];
+
+      // if shift shiftvariation is specified, run the modifier
+      if (shiftvariation) {
+          var reference = data.offsets.reference;
+          var popper = getPopperClientRect(data.offsets.popper);
+
+          var shiftOffsets = {
+              y: {
+                  start: { top: reference.top },
+                  end: { top: reference.top + reference.height - popper.height }
+              },
+              x: {
+                  start: { left: reference.left },
+                  end: { left: reference.left + reference.width - popper.width }
+              }
+          };
+
+          var axis = ['bottom', 'top'].indexOf(basePlacement) !== -1 ? 'x' : 'y';
+
+          data.offsets.popper = Object.assign(popper, shiftOffsets[axis][shiftvariation]);
       }
-      return element.parentNode ? isTransformed(element.parentNode) : element;
-    }
-
-    /**
-     * Set the style to the given popper
-     * @function
-     * @ignore
-     * @argument {Element} element - Element to apply the style to
-     * @argument {Object} styles - Object with a list of properties and values which will be applied to the element
-     */
-    function setStyle(element, styles) {
-        function is_numeric(n) {
-            return (n !== '' && !isNaN(parseFloat(n)) && isFinite(n));
-        }
-        Object.keys(styles).forEach(function(prop) {
-            var unit = '';
-            // add unit if the value is numeric and is one of the following
-            if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && is_numeric(styles[prop])) {
-                unit = 'px';
-            }
-            element.style[prop] = styles[prop] + unit;
-        });
-    }
 
-    /**
-     * Check if the given variable is a function
-     * @function
-     * @ignore
-     * @argument {Element} element - Element to check
-     * @returns {Boolean} answer to: is a function?
-     */
-    function isFunction(functionToCheck) {
-        var getType = {};
-        return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
-    }
-
-    /**
-     * Get the position of the given element, relative to its offset parent
-     * @function
-     * @ignore
-     * @param {Element} element
-     * @return {Object} position - Coordinates of the element and its `scrollTop`
-     */
-    function getOffsetRect(element) {
-        var elementRect = {
-            width: element.offsetWidth,
-            height: element.offsetHeight,
-            left: element.offsetLeft,
-            top: element.offsetTop
-        };
-
-        elementRect.right = elementRect.left + elementRect.width;
-        elementRect.bottom = elementRect.top + elementRect.height;
-
-        // position
-        return elementRect;
-    }
-
-    /**
-     * Get bounding client rect of given element
-     * @function
-     * @ignore
-     * @param {HTMLElement} element
-     * @return {Object} client rect
-     */
-    function getBoundingClientRect(element) {
-        var rect = element.getBoundingClientRect();
-        return {
-            left: rect.left,
-            top: rect.top,
-            right: rect.right,
-            bottom: rect.bottom,
-            width: rect.right - rect.left,
-            height: rect.bottom - rect.top
-        };
-    }
-
-    /**
-     * Given an element and one of its parents, return the offset
-     * @function
-     * @ignore
-     * @param {HTMLElement} element
-     * @param {HTMLElement} parent
-     * @return {Object} rect
-     */
-    function getOffsetRectRelativeToCustomParent(element, parent, fixed, transformed) {
-        var elementRect = getBoundingClientRect(element);
-        var parentRect = getBoundingClientRect(parent);
-
-        if (fixed && !transformed) {
-            var scrollParent = getScrollParent(parent);
-            parentRect.top += scrollParent.scrollTop;
-            parentRect.bottom += scrollParent.scrollTop;
-            parentRect.left += scrollParent.scrollLeft;
-            parentRect.right += scrollParent.scrollLeft;
-        }
-
-        var rect = {
-            top: elementRect.top - parentRect.top ,
-            left: elementRect.left - parentRect.left ,
-            bottom: (elementRect.top - parentRect.top) + elementRect.height,
-            right: (elementRect.left - parentRect.left) + elementRect.width,
-            width: elementRect.width,
-            height: elementRect.height
-        };
-        return rect;
-    }
+      return data;
+  }
+
+  /**
+   * Modifier used to hide the popper when its reference element is outside of the
+   * popper boundaries. It will set an x-hidden attribute which can be used to hide
+   * the popper when its reference is out of boundaries.
+   * @method
+   * @memberof Modifiers
+   * @argument {Object} data - The data object generated by update method
+   * @argument {Object} options - Modifiers configuration and options
+   * @returns {Object} The data object, properly modified
+   */
+  function hide(data) {
+      var refRect = data.offsets.reference;
+      var bound = data.boundaries;
+
+      if (refRect.bottom < bound.top || refRect.left > bound.right || refRect.top > bound.bottom || refRect.right < bound.left) {
+          data.hide = true;
+          data.instance.popper.setAttribute('x-out-of-boundaries', '');
+      } else {
+          data.hide = false;
+          data.instance.popper.removeAttribute('x-out-of-boundaries');
+      }
 
-    /**
-     * Get the prefixed supported property name
-     * @function
-     * @ignore
-     * @argument {String} property (camelCase)
-     * @returns {String} prefixed property (camelCase)
-     */
-    function getSupportedPropertyName(property) {
-        var prefixes = ['', 'ms', 'webkit', 'moz', 'o'];
-
-        for (var i = 0; i < prefixes.length; i++) {
-            var toCheck = prefixes[i] ? prefixes[i] + property.charAt(0).toUpperCase() + property.slice(1) : property;
-            if (typeof root.document.body.style[toCheck] !== 'undefined') {
-                return toCheck;
-            }
-        }
-        return null;
+      return data;
+  }
+
+  /**
+   * Modifiers are plugins used to alter the behavior of your poppers.
+   * Popper.js uses a set of 7 modifiers to provide all the basic functionalities
+   * needed by the library.
+   *
+   * Each modifier is an object containing several properties listed below.
+   * @namespace Modifiers
+   * @param {Object} modifier - Modifier descriptor
+   * @param {Integer} modifier.order
+   *      The `order` property defines the execution order of the modifiers.
+   *      The built-in modifiers have orders with a gap of 100 units in between,
+   *      this allows you to inject additional modifiers between the existing ones
+   *      without having to redefine the order of all of them.
+   *      The modifiers are executed starting from the one with the lowest order.
+   * @param {Boolean} modifier.enabled - When `true`, the modifier will be used.
+   * @param {Modifiers~modifier} modifier.function - Modifier function.
+   * @param {Modifiers~onLoad} modifier.onLoad - Function executed on popper initalization
+   * @return {Object} data - Each modifier must return the modified `data` object.
+   */
+
+  var modifiersFunctions = {
+    applyStyle: applyStyle,
+    arrow: arrow,
+    flip: flip,
+    keepTogether: keepTogether,
+    offset: offset,
+    preventOverflow: preventOverflow,
+    shift: shift,
+    hide: hide
+  };
+
+  var modifiersOnLoad = {
+    applyStyleOnLoad: applyStyleOnLoad
+  };
+
+  /**
+   * Modifiers can edit the `data` object to change the beheavior of the popper.
+   * This object contains all the informations used by Popper.js to compute the
+   * popper position.
+   * The modifier can edit the data as needed, and then `return` it as result.
+   *
+   * @callback Modifiers~modifier
+   * @param {dataObject} data
+   * @return {dataObject} modified data
+   */
+
+  /**
+   * The `dataObject` is an object containing all the informations used by Popper.js
+   * this object get passed to modifiers and to the `onCreate` and `onUpdate` callbacks.
+   * @name dataObject
+   * @property {Object} data.instance The Popper.js instance
+   * @property {String} data.placement Placement applied to popper
+   * @property {String} data.originalPlacement Placement originally defined on init
+   * @property {Boolean} data.flipped True if popper has been flipped by flip modifier
+   * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper.
+   * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier
+   * @property {Object} data.styles Any CSS property defined here will be applied to the popper, it expects the JavaScript nomenclature (eg. `marginBottom`)
+   * @property {Object} data.boundaries Offsets of the popper boundaries
+   * @property {Object} data.offsets The measurements of popper, reference and arrow elements.
+   * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values
+   * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values
+   * @property {Object} data.offsets.arro] `top` and `left` offsets, only one of them will be different from 0
+   */
+
+  var classCallCheck = function (instance, Constructor) {
+    if (!(instance instanceof Constructor)) {
+      throw new TypeError("Cannot call a class as a function");
     }
-
-    /**
-     * The Object.assign() method is used to copy the values of all enumerable own properties from one or more source
-     * objects to a target object. It will return the target object.
-     * This polyfill doesn't support symbol properties, since ES5 doesn't have symbols anyway
-     * Source: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
-     * @function
-     * @ignore
-     */
-    if (!Object.assign) {
-        Object.defineProperty(Object, 'assign', {
-            enumerable: false,
-            configurable: true,
-            writable: true,
-            value: function(target) {
-                if (target === undefined || target === null) {
-                    throw new TypeError('Cannot convert first argument to object');
-                }
-
-                var to = Object(target);
-                for (var i = 1; i < arguments.length; i++) {
-                    var nextSource = arguments[i];
-                    if (nextSource === undefined || nextSource === null) {
-                        continue;
-                    }
-                    nextSource = Object(nextSource);
-
-                    var keysArray = Object.keys(nextSource);
-                    for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
-                        var nextKey = keysArray[nextIndex];
-                        var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
-                        if (desc !== undefined && desc.enumerable) {
-                            to[nextKey] = nextSource[nextKey];
-                        }
-                    }
-                }
-                return to;
-            }
-        });
+  };
+
+  var createClass = function () {
+    function defineProperties(target, props) {
+      for (var i = 0; i < props.length; i++) {
+        var descriptor = props[i];
+        descriptor.enumerable = descriptor.enumerable || false;
+        descriptor.configurable = true;
+        if ("value" in descriptor) descriptor.writable = true;
+        Object.defineProperty(target, descriptor.key, descriptor);
+      }
     }
 
-    if (!root.requestAnimationFrame) {
-        var lastTime = 0;
-        var vendors = ['ms', 'moz', 'webkit', 'o'];
-        for(var x = 0; x < vendors.length && !root.requestAnimationFrame; ++x) {
-            root.requestAnimationFrame = root[vendors[x]+'RequestAnimationFrame'];
-            root.cancelAnimationFrame = root[vendors[x]+'CancelAnimationFrame'] || root[vendors[x]+'CancelRequestAnimationFrame'];
-        }
-
-        if (!root.requestAnimationFrame) {
-            root.requestAnimationFrame = function(callback, element) {
-                var currTime = new Date().getTime();
-                var timeToCall = Math.max(0, 16 - (currTime - lastTime));
-                var id = root.setTimeout(function() { callback(currTime + timeToCall); },
-                                           timeToCall);
-                lastTime = currTime + timeToCall;
-                return id;
-            };
-        }
-
-        if (!root.cancelAnimationFrame) {
-            root.cancelAnimationFrame = function(id) {
-                clearTimeout(id);
-            };
-        }
-    }
+    return function (Constructor, protoProps, staticProps) {
+      if (protoProps) defineProperties(Constructor.prototype, protoProps);
+      if (staticProps) defineProperties(Constructor, staticProps);
+      return Constructor;
+    };
+  }();
+
+  // default options
+  var DEFAULTS = {
+      // placement of the popper
+      placement: 'bottom',
+
+      // if true, it uses the CSS 3d transformation to position the popper
+      gpuAcceleration: true,
+
+      // the element which will act as boundary of the popper
+      boundariesElement: 'viewport',
+
+      // amount of pixel used to define a minimum distance between the boundaries and the popper
+      boundariesPadding: 5,
+
+      // list of functions used to modify the offsets before they are applied to the popper
+      modifiers: {
+          shift: {
+              order: 100,
+              enabled: true,
+              function: modifiersFunctions.shift
+          },
+          offset: {
+              order: 200,
+              enabled: true,
+              function: modifiersFunctions.offset,
+              // nudges popper from its origin by the given amount of pixels (can be negative)
+              offset: 0
+          },
+          preventOverflow: {
+              order: 300,
+              enabled: true,
+              function: modifiersFunctions.preventOverflow,
+              // popper will try to prevent overflow following these priorities
+              //  by default, then, it could overflow on the left and on top of the boundariesElement
+              priority: ['left', 'right', 'top', 'bottom']
+          },
+          keepTogether: {
+              order: 400,
+              enabled: true,
+              function: modifiersFunctions.keepTogether
+          },
+          arrow: {
+              order: 500,
+              enabled: true,
+              function: modifiersFunctions.arrow,
+              // selector or node used as arrow
+              element: '[x-arrow]'
+          },
+          flip: {
+              order: 600,
+              enabled: true,
+              function: modifiersFunctions.flip,
+              // the behavior used to change the popper's placement
+              behavior: 'flip'
+          },
+          hide: {
+              order: 700,
+              enabled: true,
+              function: modifiersFunctions.hide
+          },
+          applyStyle: {
+              order: 800,
+              enabled: true,
+              function: modifiersFunctions.applyStyle,
+              onLoad: modifiersOnLoad.applyStyleOnLoad
+          }
+      }
+  };
+
+  /**
+   * Create a new Popper.js instance
+   * @class Popper
+   * @param {HTMLElement} reference - The reference element used to position the popper
+   * @param {HTMLElement} popper - The HTML element used as popper.
+   * @param {Object} options
+   * @param {String} options.placement=bottom
+   *      Placement of the popper accepted values: `top(-start, -end), right(-start, -end), bottom(-start, -right),
+   *      left(-start, -end)`
+   *
+   * @param {Boolean} options.gpuAcceleration=true
+   *      When this property is set to true, the popper position will be applied using CSS3 translate3d, allowing the
+   *      browser to use the GPU to accelerate the rendering.
+   *      If set to false, the popper will be placed using `top` and `left` properties, not using the GPU.
+   *
+   * @param {String|Element} options.boundariesElement='viewport'
+   *      The element which will define the boundaries of the popper position, the popper will never be placed outside
+   *      of the defined boundaries (except if `keepTogether` is enabled)
+   *
+   * @param {Number} options.boundariesPadding=5
+   *      Additional padding for the boundaries
+   *
+   * @param {Boolean} options.removeOnDestroy=false
+   *      Set to true if you want to automatically remove the popper when you call the `destroy` method.
+   *
+   * @param {Object} options.modifiers
+   *      List of functions used to modify the data before they are applied to the popper (see source code for default values)
+   *
+   * @param {Object} options.modifiers.arrow - Arrow modifier configuration
+   * @param {HTMLElement|String} options.modifiers.arrow.element='[x-arrow]'
+   *      The DOM Node used as arrow for the popper, or a CSS selector used to get the DOM node. It must be child of
+   *      its parent Popper. Popper.js will apply to the given element the style required to align the arrow with its
+   *      reference element.
+   *      By default, it will look for a child node of the popper with the `x-arrow` attribute.
+   *
+   * @param {Object} options.modifiers.offset - Offset modifier configuration
+   * @param {Number} options.modifiers.offset.offset=0
+   *      Amount of pixels the popper will be shifted (can be negative).
+   *
+   * @param {Object} options.modifiers.preventOverflow - PreventOverflow modifier configuration
+   * @param {Array} [options.modifiers.preventOverflow.priority=['left', 'right', 'top', 'bottom']]
+   *      Priority used when Popper.js tries to avoid overflows from the boundaries, they will be checked in order,
+   *      this means that the last one will never overflow
+   *
+   * @param {Object} options.modifiers.flip - Flip modifier configuration
+   * @param {String|Array} options.modifiers.flip.behavior='flip'
+   *      The behavior used by the `flip` modifier to change the placement of the popper when the latter is trying to
+   *      overlap its reference element. Defining `flip` as value, the placement will be flipped on
+   *      its axis (`right - left`, `top - bottom`).
+   *      You can even pass an array of placements (eg: `['right', 'left', 'top']` ) to manually specify
+   *      how alter the placement when a flip is needed. (eg. in the above example, it would first flip from right to left,
+   *      then, if even in its new placement, the popper is overlapping its reference element, it will be moved to top)
+   *
+   * @return {Object} instance - The generated Popper.js instance
+   */
+
+  var Popper = function () {
+      function Popper(reference, popper) {
+          var _this = this;
+
+          var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
+          classCallCheck(this, Popper);
+          this.Defaults = DEFAULTS;
+
+          // init state
+          this.state = {
+              isDestroyed: false
+          };
+
+          // get reference and popper elements (allow jQuery wrappers)
+          this.reference = reference.jquery ? reference[0] : reference;
+          this.popper = popper.jquery ? popper[0] : popper;
+
+          // with {} we create a new object with the options inside it
+          this.options = Object.assign({}, DEFAULTS, options);
+
+          // refactoring modifiers' list (Object => Array)
+          this.modifiers = Object.keys(DEFAULTS.modifiers).map(function (name) {
+              return Object.assign({ name: name }, DEFAULTS.modifiers[name]);
+          });
+
+          // assign default values to modifiers, making sure to override them with
+          // the ones defined by user
+          this.modifiers = this.modifiers.map(function (defaultConfig) {
+              var userConfig = options.modifiers && options.modifiers[defaultConfig.name] || {};
+              var finalConfig = Object.assign({}, defaultConfig, userConfig);
+              return finalConfig;
+          });
+
+          // add custom modifiers to the modifiers list
+          if (options.modifiers) {
+              Object.keys(options.modifiers).forEach(function (name) {
+                  // take in account only custom modifiers
+                  if (DEFAULTS.modifiers[name] === undefined) {
+                      var modifier = options.modifiers[name];
+                      modifier.name = name;
+                      _this.modifiers.push(modifier);
+                  }
+              });
+          }
+
+          // sort the modifiers by order
+          this.modifiers = this.modifiers.sort(sortModifiers);
+
+          // modifiers have the ability to execute arbitrary code when Popper.js get inited
+          // such code is executed in the same order of its modifier
+          this.modifiers.forEach(function (modifier) {
+              if (modifier.enabled && isFunction(modifier.onLoad)) {
+                  modifier.onLoad(_this.reference, _this.popper, _this.options);
+              }
+          });
+
+          // get the popper position type
+          this.state.position = getPosition(this.popper, this.reference);
+
+          // determine how we should set the origin of offsets
+          this.state.isParentTransformed = isTransformed(this.popper.parentNode);
+
+          // fire the first update to position the popper in the right place
+          this.update(true);
+
+          // setup event listeners, they will take care of update the position in specific situations
+          setupEventListeners(this.reference, this.options, this.state, function () {
+              return _this.update();
+          });
+
+          // make it chainable
+          return this;
+      }
 
-    return Popper;
-}));
+      //
+      // Methods
+      //
+
+      /**
+       * Updates the position of the popper, computing the new offsets and applying the new style
+       * @method
+       * @param {Boolean} isFirstCall
+       *      When true, the onCreate callback is called, otherwise it calls the onUpdate callback
+       * @memberof Popper
+       */
+
+
+      createClass(Popper, [{
+          key: 'update',
+          value: function update(isFirstCall) {
+              var _this2 = this;
+
+              var data = { instance: this, styles: {} };
+
+              // make sure to apply the popper position before any computation
+              this.state.position = getPosition(this.popper, this.reference);
+              setStyle(this.popper, { position: this.state.position });
+
+              // to avoid useless computations we throttle the popper position refresh to 60fps
+              window.requestAnimationFrame(function () {
+                  // if popper is destroyed, don't perform any further update
+                  if (_this2.state.isDestroyed) {
+                      return;
+                  }
+
+                  var now = window.performance.now();
+                  if (now - _this2.state.lastFrame <= 16) {
+                      // this update fired to early! drop it
+                      // but schedule a new one that will be ran at the end of the updates
+                      // chain to make sure everything is proper updated
+                      return _this2.update();
+                  }
+                  _this2.state.lastFrame = now;
+
+                  // store placement inside the data object, modifiers will be able to edit `placement` if needed
+                  // and refer to originalPlacement to know the original value
+                  data.placement = _this2.options.placement;
+                  data.originalPlacement = _this2.options.placement;
+
+                  // compute the popper and reference offsets and put them inside data.offsets
+                  data.offsets = getOffsets(_this2.state, _this2.popper, _this2.reference, data.placement);
+
+                  // get boundaries
+                  data.boundaries = getBoundaries(_this2.popper, data, _this2.options.boundariesPadding, _this2.options.boundariesElement);
+
+                  // run the modifiers
+                  data = runModifiers(_this2.modifiers, _this2.options, data);
+
+                  // the first `update` will call `onCreate` callback
+                  // the other ones will call `onUpdate` callback
+                  if (isFirstCall && isFunction(_this2.state.createCalback)) {
+                      _this2.state.createCalback(data);
+                  } else if (!isFirstCall && isFunction(_this2.state.updateCallback)) {
+                      _this2.state.updateCallback(data);
+                  }
+              });
+          }
+
+          /**
+           * If a function is passed, it will be executed after the initialization of popper with as first argument the Popper instance.
+           * @method
+           * @memberof Popper
+           * @param {createCallback} callback
+           */
+
+      }, {
+          key: 'onCreate',
+          value: function onCreate(callback) {
+              // the createCallbacks return as first argument the popper instance
+              this.state.createCalback = callback;
+              return this;
+          }
+
+          /**
+           * Callback called when the popper is created.
+           * Access Popper.js instance with `data.instance`.
+           * @callback createCallback
+           * @static
+           * @param {dataObject} data
+           */
+
+          /**
+           * If a function is passed, it will be executed after each update of popper with as first argument the set of coordinates and informations
+           * used to style popper and its arrow.
+           * NOTE: it doesn't get fired on the first call of the `Popper.update()` method inside the `Popper` constructor!
+           * @method
+           * @memberof Popper
+           * @param {updateCallback} callback
+           */
+
+      }, {
+          key: 'onUpdate',
+          value: function onUpdate(callback) {
+              this.state.updateCallback = callback;
+              return this;
+          }
+
+          /**
+           * Callback called when the popper is updated, this callback is not called
+           * on the initialization/creation of the popper, but only on subsequent
+           * updates.
+           * Access Popper.js instance with `data.instance`.
+           * @callback updateCallback
+           * @static
+           * @param {dataObject} data
+           */
+
+          /**
+           * Destroy the popper
+           * @method
+           * @memberof Popper
+           */
+
+      }, {
+          key: 'destroy',
+          value: function destroy() {
+              this.state.isDestroyed = true;
+              this.popper.removeAttribute('x-placement');
+              this.popper.style.left = '';
+              this.popper.style.position = '';
+              this.popper.style.top = '';
+              this.popper.style[getSupportedPropertyName('transform')] = '';
+              this.state = removeEventListeners(this.reference, this.state, this.options);
+
+              // remove the popper if user explicity asked for the deletion on destroy
+              // do not use `remove` because IE11 doesn't support it
+              if (this.options.removeOnDestroy) {
+                  this.popper.parentNode.removeChild(this.popper);
+              }
+              return this;
+          }
+
+          /**
+           * Collection of utilities useful when writing custom modifiers
+           * @memberof Popper
+           */
+
+
+          /**
+           * Default Popper.js options
+           * @memberof Popper
+           */
+
+      }]);
+      return Popper;
+  }();
+
+  Popper.Utils = Utils;
+
+  return Popper;
+
+}));
\ No newline at end of file
index 1f7d0c6..3a77250 100644 (file)
@@ -521,6 +521,10 @@ Tour.prototype.normalizeStepConfig = function (stepConfig) {
         attachPoint: 'after'
     }, stepConfig);
 
+    if (stepConfig.attachTo) {
+        stepConfig.attachTo = $(stepConfig.attachTo).first();
+    }
+
     return stepConfig;
 };
 
@@ -612,7 +616,7 @@ Tour.prototype.processStepListeners = function (stepConfig) {
             args: ['click', $.proxy(function (e) {
                 if ($(e.target).parents('[data-flexitour="container"]').length === 0) {
                     // Ignore clicks when they are in the flexitour.
-                    window.setTimeout($.proxy(this.next, this), 100);
+                    window.setTimeout($.proxy(this.next, this), 500);
                 }
             }, this)]
         });
@@ -739,10 +743,10 @@ Tour.prototype.addStepToPage = function (stepConfig) {
         this.positionBackdrop(stepConfig);
 
         if (stepConfig.attachPoint === 'append') {
-            $(stepConfig.attachTo).append(currentStepNode);
+            stepConfig.attachTo.append(currentStepNode);
             this.currentStepNode = currentStepNode;
         } else {
-            this.currentStepNode = currentStepNode.insertAfter($(stepConfig.attachTo));
+            this.currentStepNode = currentStepNode.insertAfter(stepConfig.attachTo);
         }
 
         // Ensure that the step node is positioned.
@@ -754,15 +758,15 @@ Tour.prototype.addStepToPage = function (stepConfig) {
 
         animationTarget.animate({
             scrollTop: this.calculateScrollTop(stepConfig)
-        }).promise().then($.proxy(function () {
+        }).promise().then(function () {
             this.positionStep(stepConfig);
             this.revealStep(stepConfig);
-        }this));
+        }.bind(this));
     } else if (stepConfig.orphan) {
         stepConfig.isOrphan = true;
 
         // This will be appended to the body instead.
-        stepConfig.attachTo = 'body';
+        stepConfig.attachTo = $('body').first();
         stepConfig.attachPoint = 'append';
 
         // Add the backdrop.
@@ -772,17 +776,26 @@ Tour.prototype.addStepToPage = function (stepConfig) {
         currentStepNode.addClass('orphan');
 
         // It lives in the body.
-        $(stepConfig.attachTo).append(currentStepNode);
+        stepConfig.attachTo.append(currentStepNode);
         this.currentStepNode = currentStepNode;
 
         this.currentStepNode.offset(this.calculateStepPositionInPage());
+        this.currentStepNode.css('position', 'fixed');
 
         this.currentStepPopper = new Popper($('body'), this.currentStepNode[0], {
             removeOnDestroy: true,
             placement: stepConfig.placement + '-start',
             arrowElement: '[data-role="arrow"]',
             // Empty the modifiers. We've already placed the step and don't want it moved.
-            modifiers: []
+            modifiers: {
+                hide: {
+                    enabled: false
+                },
+                applyStyle: {
+                    onLoad: null,
+                    enabled: false
+                }
+            }
         });
 
         this.revealStep(stepConfig);
@@ -802,7 +815,7 @@ Tour.prototype.revealStep = function (stepConfig) {
         window.setTimeout($.proxy(function () {
             // After a brief delay, focus again.
             // There seems to be an issue with Jaws where it only reads the dialogue title initially.
-            // This second focus causes it to read the full dialogue.
+            // This second focus helps it to read the full dialogue.
             if (this.currentStepNode) {
                 this.currentStepNode.focus();
             }
@@ -1120,15 +1133,13 @@ Tour.prototype.calculateScrollTop = function (stepConfig) {
 Tour.prototype.calculateStepPositionInPage = function () {
     var viewportHeight = $(window).height();
     var stepHeight = this.currentStepNode.height();
-    var scrollTop = $(window).scrollTop();
 
     var viewportWidth = $(window).width();
     var stepWidth = this.currentStepNode.width();
-    var scrollLeft = $(window).scrollLeft();
 
     return {
-        top: Math.ceil(scrollTop + (viewportHeight - stepHeight) / 2),
-        left: Math.ceil(scrollLeft + (viewportWidth - stepWidth) / 2)
+        top: Math.ceil((viewportHeight - stepHeight) / 2),
+        left: Math.ceil((viewportWidth - stepWidth) / 2)
     };
 };
 
@@ -1166,18 +1177,29 @@ Tour.prototype.positionStep = function (stepConfig) {
     }
 
     var target = this.getStepTarget(stepConfig);
+    var config = {
+        placement: stepConfig.placement + '-start',
+        removeOnDestroy: true,
+        modifiers: {
+            flip: {
+                behaviour: flipBehavior
+            },
+            arrow: {
+                element: '[data-role="arrow"]'
+            }
+        }
+    };
+
+    var boundaryElement = target.closest('section');
+    if (boundaryElement.length) {
+        config.boundariesElement = boundaryElement[0];
+    }
+
     var background = $('[data-flexitour="step-background"]');
     if (background.length) {
         target = background;
     }
-
-    this.currentStepPopper = new Popper(target, content[0], {
-        placement: stepConfig.placement + '-start',
-        removeOnDestroy: true,
-        flipBehavior: flipBehavior,
-        arrowElement: '[data-role="arrow"]',
-        modifiers: ['shift', 'offset', 'preventOverflow', 'keepTogether', this.centerPopper, 'arrow', 'flip', 'applyStyle']
-    });
+    this.currentStepPopper = new Popper(target, content[0], config);
 
     return this;
 };
@@ -1196,9 +1218,9 @@ Tour.prototype.positionBackdrop = function (stepConfig) {
 
         if (stepConfig.zIndex) {
             if (stepConfig.attachPoint === 'append') {
-                $(stepConfig.attachTo).append(backdrop);
+                stepConfig.attachTo.append(backdrop);
             } else {
-                backdrop.insertAfter($(stepConfig.attachTo));
+                backdrop.insertAfter(stepConfig.attachTo);
             }
         } else {
             $('body').append(backdrop);
@@ -1259,10 +1281,10 @@ Tour.prototype.positionBackdrop = function (stepConfig) {
 
             if (stepConfig.zIndex) {
                 if (stepConfig.attachPoint === 'append') {
-                    $(stepConfig.attachTo).append(background);
+                    stepConfig.attachTo.append(background);
                 } else {
-                    fader.insertAfter($(stepConfig.attachTo));
-                    background.insertAfter($(stepConfig.attachTo));
+                    fader.insertAfter(stepConfig.attachTo);
+                    background.insertAfter(stepConfig.attachTo);
                 }
             } else {
                 $('body').append(fader);
@@ -1363,25 +1385,14 @@ Tour.prototype.calculatePosition = function (elem) {
     return null;
 };
 
-Tour.prototype.centerPopper = function (data) {
-    if (!this.isModifierRequired(Tour.prototype.centerPopper, this.modifiers.keepTogether)) {
-        console.warn('WARNING: keepTogether modifier is required by centerPopper modifier in order to work, be sure to include it before arrow!');
-        return data;
-    }
-
-    var placement = data.placement.split('-')[0];
-    var reference = data.offsets.reference;
-    var isVertical = ['left', 'right'].indexOf(placement) !== -1;
-
-    var len = isVertical ? 'height' : 'width';
-    var side = isVertical ? 'top' : 'left';
-
-    data.offsets.popper[side] += Math.max(reference[len] / 2 - data.offsets.popper[len] / 2, 0);
-
-    return data;
-};
-
-Tour.prototype.accessibilityShow = function (stepConfig) {
+/**
+ * Perform accessibility changes for step shown.
+ *
+ * This will add aria-hidden="true" to all siblings and parent siblings.
+ *
+ * @method  accessibilityShow
+ */
+Tour.prototype.accessibilityShow = function () {
     var stateHolder = 'data-has-hidden';
     var attrName = 'aria-hidden';
     var hideFunction = function hideFunction(child) {
@@ -1409,6 +1420,13 @@ Tour.prototype.accessibilityShow = function (stepConfig) {
     });
 };
 
+/**
+ * Perform accessibility changes for step hidden.
+ *
+ * This will remove any newly added aria-hidden="true".
+ *
+ * @method  accessibilityHide
+ */
 Tour.prototype.accessibilityHide = function () {
     var stateHolder = 'data-has-hidden';
     var attrName = 'aria-hidden';
index 47e4633..af94d76 100644 (file)
@@ -241,9 +241,7 @@ class manager {
                 'title' => get_string('importtour', 'tool_usertours'),
             ],
             (object) [
-                'link'  => new \moodle_url('https://moodle.net/mod/data/view.php', [
-                        'id' => 17,
-                    ]),
+                'link'  => new \moodle_url('https://moodle.net/tours'),
                 'linkproperties' => [
                         'target' => '_blank',
                     ],
index 45bad95..3904052 100644 (file)
@@ -4,14 +4,14 @@
     <location>amd/src/tour.js</location>
     <name>Flexitour</name>
     <license>GPLv3</license>
-    <version>0.9.7</version>
+    <version>0.9.9</version>
     <licenseversion>3</licenseversion>
   </library>
   <library>
     <location>amd/src/popper.js</location>
     <name>Popper.js</name>
     <license>MIT</license>
-    <version>v0.6.4</version>
+    <version>v1.0.0-alpha.3</version>
     <licenseversion></licenseversion>
   </library>
 </libraries>
index 2b1ddc0..218ae55 100644 (file)
@@ -34,6 +34,10 @@ defined('MOODLE_INTERNAL') || die();
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 abstract class tree_node {
+
+    /** @var int Counter to be used in {@link tree_node::unique_sql_parameter()}. */
+    protected static $uniquesqlparametercounter = 1;
+
     /**
      * Determines whether this particular item is currently available
      * according to the availability criteria.
@@ -242,10 +246,11 @@ abstract class tree_node {
      * @return SQL code for the parameter, e.g. ':pr1234'
      */
     protected static function unique_sql_parameter(array &$params, $value) {
-        static $count = 1;
+
+        // Note we intentionally do not use self:: here.
+        $count = tree_node::$uniquesqlparametercounter++;
         $unique = 'usp' . $count;
         $params[$unique] = $value;
-        $count++;
         return ':' . $unique;
     }
 }
index 402afdd..b2d3900 100644 (file)
@@ -46,7 +46,7 @@ Feature: Confirm that conditions on completion no longer cause a bug
     And I set the field "Activity or resource" to "Page1"
     And I press "Add restriction..."
     And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
-    And I set the field with xpath "//div[@class='availability-item'][preceding-sibling::div]//select[@name='cm']" to "Page2"
+    And I set the field with xpath "//div[contains(concat(' ', normalize-space(@class), ' '), ' availability-item ')][preceding-sibling::div]//select[@name='cm']" to "Page2"
     And I press "Save and return to course"
     And I should see "Not available unless:" in the ".activity.glossary" "css_element"
     And I should see "The activity Page1 is marked complete" in the ".activity.glossary" "css_element"
index 41d64ea..4885548 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js differ
index e780555..36efbb5 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js differ
index 41d64ea..4885548 100644 (file)
Binary files a/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js and b/availability/condition/completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js differ
index 89b1058..e7b418e 100644 (file)
@@ -23,9 +23,10 @@ M.availability_completion.form.initInner = function(cms) {
 
 M.availability_completion.form.getNode = function(json) {
     // Create HTML structure.
-    var html = M.util.get_string('title', 'availability_completion') + ' <span class="availability-group"><label>' +
+    var html = '<span class="col-form-label p-r-1"> ' + M.util.get_string('title', 'availability_completion') + '</span>' +
+               ' <span class="availability-group form-group"><label>' +
             '<span class="accesshide">' + M.util.get_string('label_cm', 'availability_completion') + ' </span>' +
-            '<select name="cm" title="' + M.util.get_string('label_cm', 'availability_completion') + '">' +
+            '<select class="custom-select" name="cm" title="' + M.util.get_string('label_cm', 'availability_completion') + '">' +
             '<option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>';
     for (var i = 0; i < this.cms.length; i++) {
         var cm = this.cms[i];
@@ -34,13 +35,14 @@ M.availability_completion.form.getNode = function(json) {
     }
     html += '</select></label> <label><span class="accesshide">' +
                 M.util.get_string('label_completion', 'availability_completion') +
-            ' </span><select name="e" title="' + M.util.get_string('label_completion', 'availability_completion') + '">' +
+            ' </span><select class="custom-select" ' +
+                            'name="e" title="' + M.util.get_string('label_completion', 'availability_completion') + '">' +
             '<option value="1">' + M.util.get_string('option_complete', 'availability_completion') + '</option>' +
             '<option value="0">' + M.util.get_string('option_incomplete', 'availability_completion') + '</option>' +
             '<option value="2">' + M.util.get_string('option_pass', 'availability_completion') + '</option>' +
             '<option value="3">' + M.util.get_string('option_fail', 'availability_completion') + '</option>' +
             '</select></label></span>';
-    var node = Y.Node.create('<span>' + html + '</span>');
+    var node = Y.Node.create('<span class="form-inline">' + html + '</span>');
 
     // Set initial values.
     if (json.cm !== undefined &&
index 3af4172..abca3e6 100644 (file)
@@ -151,7 +151,7 @@ class frontend extends \core_availability\frontend {
             // NOTE: The fields need to have these weird names in order that they
             // match the standard Moodle form control, otherwise the date selector
             // won't find them.
-            $html .= \html_writer::start_tag('select', array('name' => 'x[' . $field . ']'));
+            $html .= \html_writer::start_tag('select', array('name' => 'x[' . $field . ']', 'class' => 'custom-select'));
             foreach ($options as $key => $value) {
                 $params = array('value' => $key);
                 if ($current[$field] == $key) {
index 7530a28..71cc687 100644 (file)
Binary files a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js and b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js differ
index 8a9bd5d..6a9564d 100644 (file)
Binary files a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js and b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js differ
index 7530a28..71cc687 100644 (file)
Binary files a/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js and b/availability/condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js differ
index e1ee836..30e36fe 100644 (file)
@@ -27,9 +27,10 @@ M.availability_date.form.initInner = function(html, defaultTime) {
 };
 
 M.availability_date.form.getNode = function(json) {
-    var html = M.util.get_string('direction_before', 'availability_date') + ' <span class="availability-group">' +
+    var html = '<span class="col-form-label p-r-1">' +
+                    M.util.get_string('direction_before', 'availability_date') + '</span> <span class="availability-group">' +
             '<label><span class="accesshide">' + M.util.get_string('direction_label', 'availability_date') + ' </span>' +
-            '<select name="direction">' +
+            '<select name="direction" class="custom-select">' +
             '<option value="&gt;=">' + M.util.get_string('direction_from', 'availability_date') + '</option>' +
             '<option value="&lt;">' + M.util.get_string('direction_until', 'availability_date') + '</option>' +
             '</select></label></span> ' + this.html;
index 734994e..adb9f36 100644 (file)
Binary files a/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-debug.js and b/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-debug.js differ
index 4d4a50c..6df6554 100644 (file)
Binary files a/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js and b/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js differ
index 734994e..adb9f36 100644 (file)
Binary files a/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form.js and b/availability/condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form.js differ
index 0286eb7..7769679 100644 (file)
@@ -35,24 +35,27 @@ M.availability_grade.form.getNode = function(json) {
     this.nodesSoFar++;
 
     // Create HTML structure.
-    var html = '<label>' + M.util.get_string('title', 'availability_grade') + ' <span class="availability-group">' +
-            '<select name="id"><option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>';
+    var html = '<label class="form-group"><span class="p-r-1">' + M.util.get_string('title', 'availability_grade') + '</span> ' +
+            '<span class="availability-group">' +
+            '<select name="id" class="custom-select"><option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>';
     for (var i = 0; i < this.grades.length; i++) {
         var grade = this.grades[i];
         // String has already been escaped using format_string.
         html += '<option value="' + grade.id + '">' + grade.name + '</option>';
     }
-    html += '</select></span></label> <span class="availability-group">' +
-            '<label><input type="checkbox" name="min"/>' + M.util.get_string('option_min', 'availability_grade') +
+    html += '</select></span></label> <br><span class="availability-group form-group">' +
+            '<label><input type="checkbox" class="form-check-input m-x-1" name="min"/>' +
+            M.util.get_string('option_min', 'availability_grade') +
             '</label> <label><span class="accesshide">' + M.util.get_string('label_min', 'availability_grade') +
-            '</span><input type="text" name="minval" title="' +
-            M.util.get_string('label_min', 'availability_grade') + '"/></label>%</span>' +
-            '<span class="availability-group">' +
-            '<label><input type="checkbox" name="max"/>' + M.util.get_string('option_max', 'availability_grade') +
+            '</span><input type="text" class="form-control m-x-1" name="minval" title="' +
+            M.util.get_string('label_min', 'availability_grade') + '"/></label>%</span><br>' +
+            '<span class="availability-group form-group">' +
+            '<label><input type="checkbox" class="form-check-input m-x-1" name="max"/>' +
+            M.util.get_string('option_max', 'availability_grade') +
             '</label> <label><span class="accesshide">' + M.util.get_string('label_max', 'availability_grade') +
-            '</span><input type="text" name="maxval" title="' +
+            '</span><input type="text" class="form-control m-x-1" name="maxval" title="' +
             M.util.get_string('label_max', 'availability_grade') + '"/></label>%</span>';
-    var node = Y.Node.create('<span>' + html + '</span>');
+    var node = Y.Node.create('<div class="d-inline-block form-inline">' + html + '</div>');
 
     // Set initial values.
     if (json.id !== undefined &&
index 9ddaa66..e6272f9 100644 (file)
Binary files a/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-debug.js and b/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-debug.js differ
index 040d395..52658fe 100644 (file)
Binary files a/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js and b/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js differ
index 9ddaa66..e6272f9 100644 (file)
Binary files a/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form.js and b/availability/condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form.js differ
index 84eaa33..6c25361 100644 (file)
@@ -31,8 +31,9 @@ M.availability_group.form.initInner = function(groups) {
 
 M.availability_group.form.getNode = function(json) {
     // Create HTML structure.
-    var html = '<label>' + M.util.get_string('title', 'availability_group') + ' <span class="availability-group">' +
-            '<select name="id">' +
+    var html = '<label><span class="p-r-1">' + M.util.get_string('title', 'availability_group') + '</span> ' +
+            '<span class="availability-group">' +
+            '<select name="id" class="custom-select">' +
             '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>' +
             '<option value="any">' + M.util.get_string('anygroup', 'availability_group') + '</option>';
     for (var i = 0; i < this.groups.length; i++) {
@@ -41,7 +42,7 @@ M.availability_group.form.getNode = function(json) {
         html += '<option value="' + group.id + '">' + group.name + '</option>';
     }
     html += '</select></span></label>';
-    var node = Y.Node.create('<span>' + html + '</span>');
+    var node = Y.Node.create('<span class="form-inline">' + html + '</span>');
 
     // Set initial values (leave default 'choose' if creating afresh).
     if (json.creating === undefined) {
index c60c681..6dd1a72 100644 (file)
Binary files a/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-debug.js and b/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-debug.js differ
index 5f1bcbc..ebd80d8 100644 (file)
Binary files a/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js and b/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js differ
index c60c681..6dd1a72 100644 (file)
Binary files a/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form.js and b/availability/condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form.js differ
index ad3135e..4152385 100644 (file)
@@ -31,8 +31,9 @@ M.availability_grouping.form.initInner = function(groupings) {
 
 M.availability_grouping.form.getNode = function(json) {
     // Create HTML structure.
-    var html = '<label>' + M.util.get_string('title', 'availability_grouping') + ' <span class="availability-group">' +
-            '<select name="id">' +
+    var html = '<label><span class="p-r-1">' + M.util.get_string('title', 'availability_grouping') + '</span> ' +
+            '<span class="availability-group">' +
+            '<select name="id" class="custom-select">' +
             '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>';
     for (var i = 0; i < this.groupings.length; i++) {
         var grouping = this.groupings[i];
@@ -40,7 +41,7 @@ M.availability_grouping.form.getNode = function(json) {
         html += '<option value="' + grouping.id + '">' + grouping.name + '</option>';
     }
     html += '</select></span></label>';
-    var node = Y.Node.create('<span>' + html + '</span>');
+    var node = Y.Node.create('<span class="form-inline">' + html + '</span>');
 
     // Set initial value if specified.
     if (json.id !== undefined &&
index 9909ea2..f47ec11 100644 (file)
Binary files a/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-debug.js and b/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-debug.js differ
index 303b419..f870dda 100644 (file)
Binary files a/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js and b/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js differ
index 9909ea2..f47ec11 100644 (file)
Binary files a/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form.js and b/availability/condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form.js differ
index db73335..dcab53f 100644 (file)
@@ -33,8 +33,9 @@ M.availability_profile.form.initInner = function(standardFields, customFields) {
 
 M.availability_profile.form.getNode = function(json) {
     // Create HTML structure.
-    var html = '<span class="availability-group"><label>' + M.util.get_string('conditiontitle', 'availability_profile') + ' ' +
-            '<select name="field">' +
+    var html = '<span class="availability-group"><label><span class="p-r-1">' +
+            M.util.get_string('conditiontitle', 'availability_profile') + '</span> ' +
+            '<select name="field" class="custom-select">' +
             '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>';
     var fieldInfo;
     for (var i = 0; i < this.standardFields.length; i++) {
@@ -48,7 +49,8 @@ M.availability_profile.form.getNode = function(json) {
         html += '<option value="cf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>';
     }
     html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_operator', 'availability_profile') +
-            ' </span><select name="op" title="' + M.util.get_string('label_operator', 'availability_profile') + '">';
+            ' </span><select name="op" title="' + M.util.get_string('label_operator', 'availability_profile') + '"' +
+                     ' class="custom-select">';
     var operators = ['isequalto', 'contains', 'doesnotcontain', 'startswith', 'endswith',
             'isempty', 'isnotempty'];
     for (i = 0; i < operators.length; i++) {
@@ -56,9 +58,9 @@ M.availability_profile.form.getNode = function(json) {
                 M.util.get_string('op_' + operators[i], 'availability_profile') + '</option>';
     }
     html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_value', 'availability_profile') +
-            '</span><input name="value" type="text" style="width: 10em" title="' +
+            '</span><input name="value" type="text" class="form-control" style="width: 10em" title="' +
             M.util.get_string('label_value', 'availability_profile') + '"/></label></span>';
-    var node = Y.Node.create('<span>' + html + '</span>');
+    var node = Y.Node.create('<span class="form-inline">' + html + '</span>');
 
     // Set initial values if specified.
     if (json.sf !== undefined &&
index ab6c30c..1b131a9 100644 (file)
@@ -675,6 +675,63 @@ class tree_testcase extends \advanced_testcase {
                 json_encode(tree::get_root_json(array($child, $child), tree::OP_AND, array(true, false))));
     }
 
+    /**
+     * Tests the behaviour of the counter in unique_sql_parameter().
+     *
+     * There was a problem with static counters used to implement a sequence of
+     * parameter placeholders (MDL-53481). As always with static variables, it
+     * is a bit tricky to unit test the behaviour reliably as it depends on the
+     * actual tests executed and also their order.
+     *
+     * To minimise risk of false expected behaviour, this test method should be
+     * first one where {@link core_availability\tree::get_user_list_sql()} is
+     * used. We also use higher number of condition instances to increase the
+     * risk of the counter collision, should there remain a problem.
+     */
+    public function test_unique_sql_parameter_behaviour() {
+        global $DB;
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+
+        // Create a test course with multiple groupings and groups and a student in each of them.
+        $course = $generator->create_course();
+        $user = $generator->create_user();
+        $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
+        $generator->enrol_user($user->id, $course->id, $studentroleid);
+        // The total number of groupings and groups must not be greater than 61.
+        // There is a limit in MySQL on the max number of joined tables.
+        $groups = [];
+        for ($i = 0; $i < 25; $i++) {
+            $group = $generator->create_group(array('courseid' => $course->id));
+            groups_add_member($group, $user);
+            $groups[] = $group;
+        }
+        $groupings = [];
+        for ($i = 0; $i < 25; $i++) {
+            $groupings[] = $generator->create_grouping(array('courseid' => $course->id));
+        }
+        foreach ($groupings as $grouping) {
+            foreach ($groups as $group) {
+                groups_assign_grouping($grouping->id, $group->id);
+            }
+        }
+        $info = new \core_availability\mock_info($course);
+
+        // Make a huge tree with 'AND' of all groups and groupings conditions.
+        $conditions = [];
+        foreach ($groups as $group) {
+            $conditions[] = \availability_group\condition::get_json($group->id);
+        }
+        foreach ($groupings as $groupingid) {
+            $conditions[] = \availability_grouping\condition::get_json($grouping->id);
+        }
+        shuffle($conditions);
+        $tree = new tree(tree::get_root_json($conditions));
+        list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
+        // This must not throw exception.
+        $DB->fix_sql_params($sql, $params);
+    }
+
     /**
      * Tests get_user_list_sql.
      */
index 40b36ad..bfdb2fc 100644 (file)
Binary files a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js and b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js differ
index 2668730..7b569c3 100644 (file)
Binary files a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js and b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js differ
index 40b36ad..bfdb2fc 100644 (file)
Binary files a/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js and b/availability/yui/build/moodle-core_availability-form/moodle-core_availability-form.js differ
index e99abb0..a2cc382 100644 (file)
@@ -356,21 +356,25 @@ M.core_availability.List = function(json, root, parentRoot) {
     // Create DIV structure (without kids).
     this.node = Y.Node.create('<div class="availability-list"><h3 class="accesshide"></h3>' +
             '<div class="availability-inner">' +
-            '<div class="availability-header">' + M.util.get_string('listheader_sign_before', 'availability') +
+            '<div class="availability-header"><span class="p-l-1">' +
+            M.util.get_string('listheader_sign_before', 'availability') + '</span>' +
             ' <label><span class="accesshide">' + M.util.get_string('label_sign', 'availability') +
-            ' </span><select class="availability-neg" title="' + M.util.get_string('label_sign', 'availability') + '">' +
+            ' </span><select class="availability-neg custom-select m-x-1"' +
+            ' title="' + M.util.get_string('label_sign', 'availability') + '">' +
             '<option value="">' + M.util.get_string('listheader_sign_pos', 'availability') + '</option>' +
             '<option value="!">' + M.util.get_string('listheader_sign_neg', 'availability') + '</option></select></label> ' +
             '<span class="availability-single">' + M.util.get_string('listheader_single', 'availability') + '</span>' +
             '<span class="availability-multi">' + M.util.get_string('listheader_multi_before', 'availability') +
             ' <label><span class="accesshide">' + M.util.get_string('label_multi', 'availability') + ' </span>' +
-            '<select class="availability-op" title="' + M.util.get_string('label_multi', 'availability') + '"><option value="&">' +
+            '<select class="availability-op custom-select m-x-1"' +
+            ' title="' + M.util.get_string('label_multi', 'availability') + '"><option value="&">' +
             M.util.get_string('listheader_multi_and', 'availability') + '</option>' +
             '<option value="|">' + M.util.get_string('listheader_multi_or', 'availability') + '</option></select></label> ' +
             M.util.get_string('listheader_multi_after', 'availability') + '</span></div>' +
             '<div class="availability-children"></div>' +
-            '<div class="availability-none">' + M.util.get_string('none', 'moodle') + '</div>' +
-            '<div class="availability-button"></div></div></div>');
+            '<div class="availability-none"><span class="p-x-1">' + M.util.get_string('none', 'moodle') + '</span></div>' +
+            '<div class="clearfix m-t-1"></div>' +
+            '<div class="availability-button"></div></div><div class="clearfix"></div></div>');
     if (!root) {
         this.node.addClass('availability-childlist');
     }
@@ -404,12 +408,12 @@ M.core_availability.List = function(json, root, parentRoot) {
         noneNode.appendChild(deleteIcon.span);
 
         // Also if it's not the root, none is actually invalid, so add a label.
-        noneNode.appendChild(Y.Node.create('<span class="label label-warning">' +
+        noneNode.appendChild(Y.Node.create('<span class="m-t-1 label label-warning">' +
                 M.util.get_string('invalid', 'availability') + '</span>'));
     }
 
     // Create the button and add it.
-    var button = Y.Node.create('<button type="button" class="btn btn-default">' +
+    var button = Y.Node.create('<button type="button" class="btn btn-default m-t-1">' +
             M.util.get_string('addrestriction', 'availability') + '</button>');
     button.on("click", function() {
         this.clickAdd();
@@ -665,7 +669,7 @@ M.core_availability.List.prototype.deleteDescendant = function(descendant) {
  */
 M.core_availability.List.prototype.clickAdd = function() {
     var content = Y.Node.create('<div>' +
-            '<ul class="list-unstyled"></ul>' +
+            '<ul class="list-unstyled container-fluid"></ul>' +
             '<div class="availability-buttons mdl-align">' +
             '<button type="button" class="btn btn-default">' + M.util.get_string('cancel', 'moodle') +
             '</button></div></div>');
@@ -681,25 +685,25 @@ M.core_availability.List.prototype.clickAdd = function() {
             continue;
         }
         // Add entry for plugin.
-        li = Y.Node.create('<li class="clearfix"></li>');
+        li = Y.Node.create('<li class="clearfix row"></li>');
         id = 'availability_addrestriction_' + type;
-        button = Y.Node.create('<button type="button" class="btn btn-default"' +
+        button = Y.Node.create('<button type="button" class="btn btn-default col-xs-6"' +
                 'id="' + id + '">' + M.util.get_string('title', 'availability_' + type) + '</button>');
         button.on('click', this.getAddHandler(type, dialogRef), this);
         li.appendChild(button);
-        label = Y.Node.create('<label for="' + id + '">' +
+        label = Y.Node.create('<label for="' + id + '" class="col-xs-6">' +
                 M.util.get_string('description', 'availability_' + type) + '</label>');
         li.appendChild(label);
         ul.appendChild(li);
     }
     // Extra entry for lists.
-    li = Y.Node.create('<li class="clearfix"></li>');
+    li = Y.Node.create('<li class="clearfix row"></li>');
     id = 'availability_addrestriction_list_';
-    button = Y.Node.create('<button type="button" class="btn btn-default"' +
+    button = Y.Node.create('<button type="button" class="btn btn-default col-xs-6"' +
             'id="' + id + '">' + M.util.get_string('condition_group', 'availability') + '</button>');
     button.on('click', this.getAddHandler(null, dialogRef), this);
     li.appendChild(button);
-    label = Y.Node.create('<label for="' + id + '">' +
+    label = Y.Node.create('<label for="' + id + '" class="col-xs-6">' +
             M.util.get_string('condition_group_info', 'availability') + '</label>');
     li.appendChild(label);
     ul.appendChild(li);
@@ -895,7 +899,7 @@ M.core_availability.Item = function(json, root) {
         this.pluginNode.addClass('availability_' + json.type);
     }
 
-    this.node = Y.Node.create('<div class="availability-item"><h3 class="accesshide"></h3></div>');
+    this.node = Y.Node.create('<div class="availability-item d-inline-block"><h3 class="accesshide"></h3></div>');
 
     // Add eye icon if required. This icon is added for root items, but may be
     // hidden depending on the selected list operator.
@@ -918,7 +922,7 @@ M.core_availability.Item = function(json, root) {
 
     // Add the invalid marker (empty).
     this.node.appendChild(document.createTextNode(' '));
-    this.node.appendChild(Y.Node.create('<span class="label label-warning"/>'));
+    this.node.appendChild(Y.Node.create('<span class="m-t-1 label label-warning"/>'));
 };
 
 /**
@@ -1046,7 +1050,7 @@ M.core_availability.Item.prototype.pluginNode = null;
  */
 M.core_availability.EyeIcon = function(individual, shown) {
     this.individual = individual;
-    this.span = Y.Node.create('<a class="availability-eye" href="#" role="button">');
+    this.span = Y.Node.create('<a class="availability-eye col-form-label" href="#" role="button">');
     var icon = Y.Node.create('<img />');
     this.span.appendChild(icon);
 
@@ -1126,7 +1130,7 @@ M.core_availability.EyeIcon.prototype.isHidden = function() {
  * @param {M.core_availability.Item|M.core_availability.List} toDelete Thing to delete
  */
 M.core_availability.DeleteIcon = function(toDelete) {
-    this.span = Y.Node.create('<a class="availability-delete" href="#" title="' +
+    this.span = Y.Node.create('<a class="d-inline-block col-form-label availability-delete p-x-1" href="#" title="' +
             M.util.get_string('delete', 'moodle') + '" role="button">');
     var img = Y.Node.create('<img src="' + M.util.image_url('t/delete', 'core') +
             '" alt="' + M.util.get_string('delete', 'moodle') + '" />');
index 231071a..4c6c808 100644 (file)
@@ -211,7 +211,7 @@ function blog_rss_get_feed($context, $args) {
     if ($blogentries) {
         $items = array();
         foreach ($blogentries as $blogentry) {
-            $item = null;
+            $item = new stdClass();
             $item->author = fullname($DB->get_record('user', array('id' => $blogentry->userid))); // TODO: this is slow.
             $item->title = $blogentry->subject;
             $item->pubdate = $blogentry->lastmodified;
@@ -234,7 +234,7 @@ function blog_rss_get_feed($context, $args) {
 
     switch ($type) {
         case 'user':
-            $info = fullname($DB->get_record('user', array('id' => $id), 'firstname,lastname'));
+            $info = fullname($DB->get_record('user', array('id' => $id), get_all_user_name_fields(true)));
             break;
         case 'course':
             $info = $DB->get_field('course', 'fullname', array('id' => $id));
index 17d8bf7..c7a7825 100644 (file)
@@ -51,7 +51,7 @@ class core_calendar_export_form extends moodleform {
         $export[] = $mform->createElement('radio', 'exportevents', '', get_string('eventsrelatedtogroups', 'calendar'), 'groups');
         $export[] = $mform->createElement('radio', 'exportevents', '', get_string('eventspersonal', 'calendar'), 'user');
 
-        $mform->addGroup($export, 'events', get_string('export', 'calendar'), '<br/>');
+        $mform->addGroup($export, 'events', get_string('eventstoexport', 'calendar'), '<br/>');
         $mform->addGroupRule('events', get_string('required'), 'required');
         $mform->setDefault('events', 'all');
 
@@ -79,7 +79,7 @@ class core_calendar_export_form extends moodleform {
             $range[] = $mform->createElement('radio', 'timeperiod', '', get_string('customexport', 'calendar', $a), 'custom');
         }
 
-        $mform->addGroup($range, 'period', get_string('for', 'calendar'), '<br/>');
+        $mform->addGroup($range, 'period', get_string('timeperiod', 'calendar'), '<br/>');
         $mform->addGroupRule('period', get_string('required'), 'required');
         $mform->setDefault('period', 'recentupcoming');
 
@@ -88,4 +88,4 @@ class core_calendar_export_form extends moodleform {
         $buttons[] = $mform->createElement('submit', 'export', get_string('exportbutton', 'calendar'));
         $mform->addGroup($buttons);
     }
-}
\ No newline at end of file
+}
index 8ebc23e..1e231a8 100644 (file)
     "packages-dev": [
         {
             "name": "behat/behat",
-            "version": "v3.2.1",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Behat.git",
-                "reference": "df7d9225e9ee37fdaa54273e3e0aecf2515bbe76"
+                "reference": "30aa3836825416f28581ee55fcfe6a5b0cdeeb85"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Behat/zipball/df7d9225e9ee37fdaa54273e3e0aecf2515bbe76",
-                "reference": "df7d9225e9ee37fdaa54273e3e0aecf2515bbe76",
+                "url": "https://api.github.com/repos/Behat/Behat/zipball/30aa3836825416f28581ee55fcfe6a5b0cdeeb85",
+                "reference": "30aa3836825416f28581ee55fcfe6a5b0cdeeb85",
                 "shasum": ""
             },
             "require": {
                 "symfony",
                 "testing"
             ],
-            "time": "2016-09-25 09:40:39"
+            "time": "2016-11-05 17:13:53"
         },
         {
             "name": "behat/gherkin",
-            "version": "v4.4.4",
+            "version": "v4.4.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Gherkin.git",
-                "reference": "cf8cc94647101e02a33d690245896d83d880aea1"
+                "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Gherkin/zipball/cf8cc94647101e02a33d690245896d83d880aea1",
-                "reference": "cf8cc94647101e02a33d690245896d83d880aea1",
+                "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74",
+                "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74",
                 "shasum": ""
             },
             "require": {
                 "gherkin",
                 "parser"
             ],
-            "time": "2016-09-18 12:16:14"
+            "time": "2016-10-30 11:50:56"
         },
         {
             "name": "behat/mink",
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.2.1",
+            "version": "6.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "3f808fba627f2c5b69e2501217bf31af349c1427"
+                "reference": "ebf29dee597f02f09f4d5bbecc68230ea9b08f60"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/3f808fba627f2c5b69e2501217bf31af349c1427",
-                "reference": "3f808fba627f2c5b69e2501217bf31af349c1427",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ebf29dee597f02f09f4d5bbecc68230ea9b08f60",
+                "reference": "ebf29dee597f02f09f4d5bbecc68230ea9b08f60",
                 "shasum": ""
             },
             "require": {
                 "rest",
                 "web service"
             ],
-            "time": "2016-07-15 17:22:37"
+            "time": "2016-10-08 15:01:37"
         },
         {
             "name": "guzzlehttp/promises",
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.5.4",
+            "version": "1.5.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f"
+                "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/ea74994a3dc7f8d2f65a06009348f2d63c81e61f",
-                "reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/399c1f9781e222f6eb6cc238796f5200d1b7f108",
+                "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108",
                 "shasum": ""
             },
             "require": {
                 "object",
                 "object graph"
             ],
-            "time": "2016-09-16 13:37:59"
+            "time": "2016-10-31 17:19:45"
         },
         {
             "name": "phpdocumentor/reflection-common",
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "4.0.1",
+            "version": "4.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3"
+                "reference": "6cba06ff75a1a63a71033e1a01b89056f3af1e8d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5f3f7e736d6319d5f1fc402aff8b026da26709a3",
-                "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6cba06ff75a1a63a71033e1a01b89056f3af1e8d",
+                "reference": "6cba06ff75a1a63a71033e1a01b89056f3af1e8d",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2016-07-26 14:39:29"
+            "time": "2016-11-01 05:06:24"
         },
         {
             "name": "phpunit/php-file-iterator",
         },
         {
             "name": "phpunit/phpunit-mock-objects",
-            "version": "3.3.1",
+            "version": "3.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "03500345483e1e17b52e2e4d34a89c9408ab2902"
+                "reference": "238d7a2723bce689c79eeac9c7d5e1d623bb9dc2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/03500345483e1e17b52e2e4d34a89c9408ab2902",
-                "reference": "03500345483e1e17b52e2e4d34a89c9408ab2902",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/238d7a2723bce689c79eeac9c7d5e1d623bb9dc2",
+                "reference": "238d7a2723bce689c79eeac9c7d5e1d623bb9dc2",
                 "shasum": ""
             },
             "require": {
                 "mock",
                 "xunit"
             ],
-            "time": "2016-10-04 11:03:26"
+            "time": "2016-10-09 07:01:45"
         },
         {
             "name": "psr/http-message",
         },
         {
             "name": "psr/log",
-            "version": "1.0.1",
+            "version": "1.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/php-fig/log.git",
-                "reference": "5277094ed527a1c4477177d102fe4c53551953e0"
+                "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/log/zipball/5277094ed527a1c4477177d102fe4c53551953e0",
-                "reference": "5277094ed527a1c4477177d102fe4c53551953e0",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
+                "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
                 "shasum": ""
             },
             "require": {
                 "psr",
                 "psr-3"
             ],
-            "time": "2016-09-19 16:02:08"
+            "time": "2016-10-10 12:19:37"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
         },
         {
             "name": "symfony/config",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
         },
         {
             "name": "symfony/console",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "6cb0872fb57b38b3b09ff213c21ed693956b9eb0"
+                "reference": "c99da1119ae61e15de0e4829196b9fba6f73d065"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/6cb0872fb57b38b3b09ff213c21ed693956b9eb0",
-                "reference": "6cb0872fb57b38b3b09ff213c21ed693956b9eb0",
+                "url": "https://api.github.com/repos/symfony/console/zipball/c99da1119ae61e15de0e4829196b9fba6f73d065",
+                "reference": "c99da1119ae61e15de0e4829196b9fba6f73d065",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2016-09-28 00:11:12"
+            "time": "2016-10-06 01:44:51"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
         },
         {
             "name": "symfony/debug",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "8fa4be9a36f41e49b0c3969678c41c81ed463816"
+                "reference": "c578891216090069cd6d2e573402e13e39b3ad5c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8fa4be9a36f41e49b0c3969678c41c81ed463816",
-                "reference": "8fa4be9a36f41e49b0c3969678c41c81ed463816",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/c578891216090069cd6d2e573402e13e39b3ad5c",
+                "reference": "c578891216090069cd6d2e573402e13e39b3ad5c",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2016-09-24 15:56:48"
+            "time": "2016-10-24 15:52:44"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "bb7395e8b1db3654de82b9f35d019958276de4d7"
+                "reference": "59eee3c76eb89f21857798620ebdad7a05ad14f4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/bb7395e8b1db3654de82b9f35d019958276de4d7",
-                "reference": "bb7395e8b1db3654de82b9f35d019958276de4d7",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/59eee3c76eb89f21857798620ebdad7a05ad14f4",
+                "reference": "59eee3c76eb89f21857798620ebdad7a05ad14f4",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2016-08-05 08:37:39"
+            "time": "2016-10-18 15:46:07"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5"
+                "reference": "28b0832b2553ffb80cabef6a7a812ff1e670c0bc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c0c00c80b3a69132c4e55c3e7db32b4a387615e5",
-                "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/28b0832b2553ffb80cabef6a7a812ff1e670c0bc",
+                "reference": "28b0832b2553ffb80cabef6a7a812ff1e670c0bc",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2016-07-19 10:45:57"
+            "time": "2016-10-13 06:28:43"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "682fd8fdb3135fdf05fc496a01579ccf6c85c0e5"
+                "reference": "0565b61bf098cb4dc09f4f103f033138ae4f42c6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/682fd8fdb3135fdf05fc496a01579ccf6c85c0e5",
-                "reference": "682fd8fdb3135fdf05fc496a01579ccf6c85c0e5",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/0565b61bf098cb4dc09f4f103f033138ae4f42c6",
+                "reference": "0565b61bf098cb4dc09f4f103f033138ae4f42c6",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2016-09-14 00:18:46"
+            "time": "2016-10-18 04:30:12"
         },
         {
             "name": "symfony/polyfill-mbstring",
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.12",
+            "version": "v2.8.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
         },
         {
             "name": "symfony/translation",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "93013a18d272e59dab8e67f583155b78c68947eb"
+                "reference": "ff1285087397d2f64041b35e591f3025881c90cd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/93013a18d272e59dab8e67f583155b78c68947eb",
-                "reference": "93013a18d272e59dab8e67f583155b78c68947eb",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/ff1285087397d2f64041b35e591f3025881c90cd",
+                "reference": "ff1285087397d2f64041b35e591f3025881c90cd",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2016-09-06 11:02:40"
+            "time": "2016-10-18 04:30:12"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.1.5",
+            "version": "v3.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "368b9738d4033c8b93454cb0dbd45d305135a6d3"
+                "reference": "7ff51b06c6c3d5cc6686df69004a42c69df09e27"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/368b9738d4033c8b93454cb0dbd45d305135a6d3",
-                "reference": "368b9738d4033c8b93454cb0dbd45d305135a6d3",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/7ff51b06c6c3d5cc6686df69004a42c69df09e27",
+                "reference": "7ff51b06c6c3d5cc6686df69004a42c69df09e27",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2016-09-25 08:27:07"
+            "time": "2016-10-24 18:41:13"
         },
         {
             "name": "webmozart/assert",
index 999f71a..3221469 100644 (file)
@@ -160,6 +160,7 @@ class core_course_external extends external_api {
             //retrieve sections
             $modinfo = get_fast_modinfo($course);
             $sections = $modinfo->get_section_info_all();
+            $coursenumsections = course_get_format($course)->get_course()->numsections;
 
             //for each sections (first displayed to last displayed)
             $modinfosections = $modinfo->get_sections();
@@ -201,6 +202,7 @@ class core_course_external extends external_api {
                         external_format_text($section->summary, $section->summaryformat,
                                 $context->id, 'course', 'section', $section->id, $options);
                 $sectionvalues['section'] = $section->section;
+                $sectionvalues['hiddenbynumsections'] = $section->section > $coursenumsections ? 1 : 0;
                 $sectioncontents = array();
 
                 //for each module of the section
@@ -329,6 +331,8 @@ class core_course_external extends external_api {
                     'summary' => new external_value(PARAM_RAW, 'Section description'),
                     'summaryformat' => new external_format_value('summary'),
                     'section' => new external_value(PARAM_INT, 'Section number inside the course', VALUE_OPTIONAL),
+                    'hiddenbynumsections' => new external_value(PARAM_INT, 'Whether is a section hidden in the course format',
+                                                                VALUE_OPTIONAL),
                     'modules' => new external_multiple_structure(
                             new external_single_structure(
                                 array(
index 8a03bcd..926c1ae 100644 (file)
@@ -2337,6 +2337,7 @@ function create_course($data, $editoroptions = NULL) {
         'other' => array('shortname' => $course->shortname,
             'fullname' => $course->fullname)
     ));
+
     $event->trigger();
 
     // Setup the blocks
index b9309b1..047e0be 100644 (file)
@@ -4,6 +4,7 @@ information provided here is intended especially for developers.
 === 3.2 ===
 
  * External function core_course_external::get_course_contents now returns the section's number in the course (new section field).
+ * External function core_course_external::get_course_contents now returns if a section is hidden in the course format.
  * External functions that were returning file information now return the following file fields:
    filename, filepath, mimetype, filesize, timemodified and fileurl.
    Those fields are now marked as VALUE_OPTIONAL for backwards compatibility.
index d981582..3229085 100644 (file)
@@ -52,7 +52,7 @@ class filter_mediaplugin_testcase extends advanced_testcase {
         $validtexts = array (
             '<a href="http://moodle.org/testfile/test.mp3">test mp3</a>',
             '<a href="http://moodle.org/testfile/test.ogg">test ogg</a>',
-            '<a id="movie player" class="center" href="http://moodle.org/testfile/test.mpg">test mpg</a>',
+            '<a id="movie player" class="center" href="http://moodle.org/testfile/test.mp4">test mp4</a>',
             '<a href="http://moodle.org/testfile/test.webm">test</a>',
             '<a href="http://www.youtube.com/watch?v=JghQgA2HMX8" class="href=css">test file</a>',
             '<a href="http://www.youtube-nocookie.com/watch?v=JghQgA2HMX8" class="href=css">test file</a>',
index 5801eb5..0f85187 100644 (file)
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js differ
index f81abac..22251a2 100644 (file)
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js differ
index f2f8a0d..895b917 100644 (file)
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js differ
index 0c0fbff..3576f0a 100644 (file)
@@ -243,6 +243,15 @@ FloatingHeaders.prototype = {
      */
     _eventHandles: [],
 
+    /**
+     * The last value of the bodyMargin style. We need to recompute positions if it is changed.
+     *
+     * @property lastBodyMargin
+     * @type Number
+     * @protected
+     */
+    lastBodyMargin: 0,
+
     /**
      * Setup the grader report table.
      *
@@ -775,7 +784,19 @@ FloatingHeaders.prototype = {
             leftTitleFloats = false,
             floatingHeaderStyles = {},
             floatingFooterTitleStyles = {},
-            floatingFooterTitleRow = false;
+            floatingFooterTitleRow = false,
+            bodyMargin = 0;
+
+        if (window.right_to_left()) {
+            bodyMargin = parseInt(Y.one(Y.config.doc.body).getComputedStyle('marginRight'), 10);
+        } else {
+            bodyMargin = parseInt(Y.one(Y.config.doc.body).getComputedStyle('marginLeft'), 10);
+        }
+
+        if (bodyMargin != this.lastBodyMargin) {
+            // Recalculate the position of the edge cells for scroll positioning.
+            this._calculateCellPositions();
+        }
 
         // Header position.
         gradeItemHeadingContainerStyles.left = this._getRelativeXFromX(this.headerRow.getX());
@@ -798,16 +819,17 @@ FloatingHeaders.prototype = {
         }
 
         // User column position.
+
         if (window.right_to_left()) {
             floatingUserTriggerPoint = Y.config.win.innerWidth + Y.config.win.pageXOffset - this.dockWidth;
-            floatingUserRelativePoint = floatingUserTriggerPoint - this.firstUserCellWidth;
-            userFloats = floatingUserTriggerPoint < (this.firstUserCellLeft + this.firstUserCellWidth);
+            floatingUserRelativePoint = floatingUserTriggerPoint - this.firstUserCellWidth - bodyMargin;
+            userFloats = floatingUserTriggerPoint < (this.firstUserCellLeft + this.firstUserCellWidth + bodyMargin);
             leftTitleFloats = (floatingUserTriggerPoint - this.firstNonUserCellWidth) <
                               (this.firstNonUserCellLeft + this.firstUserCellWidth);
         } else {
-            floatingUserRelativePoint = Y.config.win.pageXOffset;
-            floatingUserTriggerPoint = floatingUserRelativePoint + this.dockWidth;
-            userFloats = floatingUserTriggerPoint > this.firstUserCellLeft;
+            floatingUserRelativePoint = Y.config.win.pageXOffset + bodyMargin;
+            floatingUserTriggerPoint = floatingUserRelativePoint + this.dockWidth + bodyMargin;
+            userFloats = floatingUserTriggerPoint > this.firstUserCellLeft + bodyMargin;
             leftTitleFloats = floatingUserTriggerPoint > (this.firstNonUserCellLeft - this.firstUserCellWidth);
         }
 
index d650274..accbbec 100644 (file)
@@ -271,7 +271,6 @@ $string['configminpasswordlength'] = 'Passwords must be at least these many char
 $string['configminpasswordlower'] = 'Passwords must have at least these many lower case letters.';
 $string['configminpasswordnonalphanum'] = 'Passwords must have at least these many non-alphanumeric characters.';
 $string['configminpasswordupper'] = 'Passwords must have at least these many upper case letters.';
-$string['configmobilecssurl'] = 'A CSS file to customise your mobile app interface.';
 $string['configmodchooserdefault'] = 'Should the activity chooser be presented to users by default?';
 $string['configmycoursesperpage'] = 'Maximum number of courses to display in any list of a user\'s own courses';
 $string['configmymoodleredirect'] = 'This setting forces redirects to /my on login for non-admins and replaces the top level site navigation with /my';
@@ -394,6 +393,11 @@ $string['cronremotepassword'] = 'Cron password for remote access';
 $string['cronwarning'] = 'The <a href="{$a}">cron.php maintenance script</a> has not been run for at least 24 hours.';
 $string['cronwarningcli'] = 'The cli/cron.php maintenance script has not been run for at least 24 hours.';
 $string['ctyperequired'] = 'The ctype PHP extension is now required by Moodle, in order to improve site performance and to offer multilingual compatibility.';
+$string['curlsecurityallowedport'] = 'cURL allowed ports list';
+$string['curlsecurityallowedportsyntax'] = 'Put every entry on one line. Valid entries are integer numbers only.';
+$string['curlsecurityblockedhosts'] = 'cURL blocked hosts list';
+$string['curlsecurityblockedhostssyntax'] = 'Put each entry on a new line. Valid entries are either full IPv4 or IPv6 addresses (such as <b>192.168.10.1, 0:0:0:0:0:0:0:1, ::1, fe80::</b>) which match a single host; or CIDR notation (such as <b>231.54.211.0/20 or fe80::/64</b>); or a range of IP addresses (such as <b>231.3.56.10-20 or fe80::1111-bbbb</b>) where the range applies to the last group of the address; or domain names (such as <b>localhost or example.com</b>); or wildcard domain names (such as <b>*.example.com or *.sub.example.com</b>). Blank lines are not allowed.';
+$string['curlsecurityurlblocked'] = 'The URL is blocked.';
 $string['curlcache'] = 'cURL cache TTL';
 $string['curlrequired'] = 'The cURL PHP extension is now required by Moodle, in order to communicate with Moodle repositories.';
 $string['curltimeoutkbitrate'] = 'Bitrate to use when calculating cURL timeouts (Kbps)';
@@ -631,6 +635,7 @@ $string['legacyfilesaddallowed'] = 'Allow adding to legacy course files';
 $string['legacyfilesaddallowed_help'] = 'If a course has legacy course files, allow new files and folders to be added to it.';
 $string['legacyfilesinnewcourses'] = 'Legacy course files in new courses';
 $string['legacyfilesinnewcourses_help'] = 'By default, legacy course files areas are available in upgraded courses only. Please note that some features such as activity backup and restore are not compatible with this setting.';
+$string['libcurlwarning'] = 'Libcurl with CURLOPT_PROTOCOL support has not been detected. It is recommended to have an up to date libcurl installation for security reasons.';
 $string['licensesettings'] = 'Licence settings';
 $string['linkadmincategories'] = 'Link admin categories';
 $string['linkadmincategories_help'] = 'If enabled admin setting categories will be displayed as links in the navigation and will lead to the admin category pages.';
@@ -740,8 +745,6 @@ $string['mnetrestore_extusers_admin'] = '<strong>Note:</strong> This backup file
 $string['mnetrestore_extusers_mismatch'] = '<strong>Note:</strong> This backup file apparently originates from a different Moodle installation and contains remote Moodle Network user accounts that may fail to restore. This operation is unsupported. If you are certain that it was created on this Moodle installation, or you can ensure that all the needed Moodle Network Hosts are configured, you may want to still try the restore.';
 $string['mnetrestore_extusers_noadmin'] = '<strong>Note:</strong> This backup file seems to come from a different Moodle installation and contains remote Moodle Network user accounts. You are not allowed to execute this type of restore. Contact the administrator of the site or, alternatively, restore this course without any user information (modules, files...)';
 $string['mnetrestore_extusers_switchuserauth'] = 'Remote Moodle Network user {$a->username} (coming from {$a->mnethosturl}) switched to local {$a->auth} authenticated user.';
-$string['mobile'] = 'Mobile';
-$string['mobilecssurl'] = 'CSS';
 $string['modchooserdefault'] = 'Activity chooser default';
 $string['modeditdefaults'] = 'Default values for activity settings';
 $string['modsettings'] = 'Manage activities';
@@ -1135,6 +1138,7 @@ $string['unsupported'] = 'Unsupported';
 $string['unsupporteddbstorageengine'] = 'The database storage engine being used is no longer supported.';
 $string['unsupporteddbtablerowformat'] = 'Your database has tables using Antelope as the file format. You are recommended to convert the tables to the Barracuda file format. See the documentation <a href="https://docs.moodle.org/en/cli">Administration via command line</a> for details of a tool for converting InnoDB tables to Barracuda.';
 $string['unsupportedphpversion7'] = 'PHP version 7 is not supported.';
+$string['unsupportedphpversion71'] = 'PHP version 7.1 is not supported.';
 $string['unsuspenduser'] = 'Activate user account';
 $string['updateaccounts'] = 'Update existing accounts';
 $string['updatecomponent'] = 'Update component';
@@ -1213,7 +1217,9 @@ $string['users'] = 'Users';
 $string['userquota'] = 'User quota';
 $string['usesitenameforsitepages'] = 'Use site name for site pages';
 $string['usetags'] = 'Enable tags functionality';
+$string['validateemptylineerror'] = 'Empty lines are not valid';
 $string['validateerror'] = 'This value is not valid';
+$string['validateerrorlist'] = 'These entries are invalid: {$a}';
 $string['validateiperror'] = 'These IP addresses are invalid: {$a}';
 $string['verifychangedemail'] = 'Restrict domains when changing email';
 $string['warningcurrentsetting'] = 'Invalid current value: {$a}';
@@ -1233,3 +1239,5 @@ $string['cachesession'] = 'Session cache';
 $string['cachesessionhelp'] = 'User specific cache that expires when the user\'s session ends. Designed to alleviate session bloat/strain.';
 $string['cacheapplication'] = 'Application cache';
 $string['cacheapplicationhelp'] = 'Cached items are shared among all users and expire by a determined time to live (ttl).';
+// Deprecated since Moodle 3.2.
+$string['mobile'] = 'Mobile';
index bf280d2..99e3310 100644 (file)
@@ -53,6 +53,7 @@ $string['cachedef_groupdata'] = 'Course group information';
 $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
 $string['cachedef_langmenu'] = 'List of available languages';
 $string['cachedef_locking'] = 'Locking';
+$string['cachedef_message_processors_enabled'] = "Message processors enabled status";
 $string['cachedef_navigation_expandcourse'] = 'Navigation expandable courses';
 $string['cachedef_observers'] = 'Event observers';
 $string['cachedef_plugin_functions'] = 'Plugins available callbacks';
index f85bb34..c2fd1e4 100644 (file)
@@ -90,6 +90,7 @@ $string['eventspersonal'] = 'My personal events';
 $string['eventsrelatedtocourses'] = 'Events related to courses';
 $string['eventsrelatedtogroups'] = 'Events related to groups';
 $string['eventstarttime'] = 'Start time';
+$string['eventstoexport'] = 'Events to export';
 $string['eventtime'] = 'Time';
 $string['eventview'] = 'Event details';
 $string['eventcalendareventcreated'] = 'Calendar event created';
@@ -103,7 +104,6 @@ $string['explain_site_timeformat'] = 'You can choose to see times in either 12 o
 $string['export'] = 'Export';
 $string['exportbutton'] = 'Export';
 $string['exportcalendar'] = 'Export calendar';
-$string['for'] = 'for';
 $string['forcecalendartype'] = 'Force calendar';
 $string['fri'] = 'Fri';
 $string['friday'] = 'Friday';
@@ -189,6 +189,7 @@ $string['thu'] = 'Thu';
 $string['thursday'] = 'Thursday';
 $string['timeformat_12'] = '12-hour (am/pm)';
 $string['timeformat_24'] = '24-hour';
+$string['timeperiod'] = 'Time period';
 $string['today'] = 'Today';
 $string['tomorrow'] = 'Tomorrow';
 $string['tt_deleteevent'] = 'Delete event';
@@ -220,3 +221,6 @@ $string['weeknext'] = 'Next week';
 $string['weekthis'] = 'This week';
 $string['yesterday'] = 'Yesterday';
 $string['youcandeleteallrepeats'] = 'This event is part of a repeating event series. You can delete this event only, or all {$a} events in the series at once.';
+
+// Deprecated since Moodle 3.2.
+$string['for'] = 'for';
index 42cf26a..68ca4b3 100644 (file)
@@ -38,3 +38,5 @@ revealpassword,core_form
 mediasettings,core_media
 legacyheading,core_media
 legacyheading_desc,core_media
+mobile,core_admin
+for,core_calendar
index 9cbbe57..a29760b 100644 (file)
@@ -1191,6 +1191,7 @@ $string['morehelp'] = 'More help';
 $string['moreinfo'] = 'More info';
 $string['moreinformation'] = 'More information about this error';
 $string['moreprofileinfoneeded'] = 'Please tell us more about yourself';
+$string['morenavigationlinks'] = 'More...';
 $string['mostrecently'] = 'most recently';
 $string['move'] = 'Move';
 $string['movecoursemodule'] = 'Move resource';
index 06cb7d1..ebfe938 100644 (file)
@@ -68,7 +68,6 @@ $string['editservice'] = 'Edit the service: {$a->name} (id: {$a->id})';
 $string['enabled'] = 'Enabled';
 $string['enabledocumentation'] = 'Enable developer documentation';
 $string['enabledocumentationdescription'] = 'Detailed web services documentation is available for enabled protocols.';
-$string['enablemobilewsoverview'] = 'Go to {$a->manageservicelink} administration page, check the "{$a->enablemobileservice}" setting and Save. Everything will be setup for you and all site\'s users will be able to use the offical Moodle app. Current status: {$a->wsmobilestatus}';
 $string['enableprotocols'] = 'Enable protocols';
 $string['enableprotocolsdescription'] = 'At least one protocol should be enabled. For security reasons, only protocols that are to be used should be enabled.';
 $string['enablews'] = 'Enable web services';
@@ -125,8 +124,6 @@ $string['missingpassword'] = 'Missing password';
 $string['missingrequiredcapability'] = 'The capability {$a} is required.';
 $string['missingusername'] = 'Missing username';
 $string['missingversionfile'] = 'Coding error: version.php file is missing for the component {$a}';
-$string['mobilewsdisabled'] = 'Disabled';
-$string['mobilewsenabled'] = 'Enabled';
 $string['nameexists'] = 'This name is already in use by another service';
 $string['nocapabilitytouseparameter'] = 'The user does not have the required capability to use the parameter {$a}';
 $string['nofunctions'] = 'This service has no functions.';
index da825e1..faa3b64 100644 (file)
@@ -3583,6 +3583,179 @@ class admin_setting_configiplist extends admin_setting_configtextarea {
     }
 }
 
+/**
+ * Used to validate a textarea used for domain names, wildcard domain names and IP addresses/ranges (both IPv4 and IPv6 format).
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2016 Jake Dallimore (jrhdallimore@gmail.com)
+ */
+class admin_setting_configmixedhostiplist extends admin_setting_configtextarea {
+
+    /**
+     * Validate the contents of the textarea as either IP addresses, domain name or wildcard domain name (RFC 4592).
+     * Used to validate a new line separated list of entries collected from a textarea control.
+     *
+     * This setting provides support for internationalised domain names (IDNs), however, such UTF-8 names will be converted to
+     * their ascii-compatible encoding (punycode) on save, and converted back to their UTF-8 representation when fetched
+     * via the get_setting() method, which has been overriden.
+     *
+     * @param string $data A list of FQDNs, DNS wildcard format domains, and IP addresses, separated by new lines.
+     * @return mixed bool true for success or string:error on failure
+     */
+    public function validate($data) {
+        if (empty($data)) {
+            return true;
+        }
+        $entries = explode("\n", $data);
+        $badentries = [];
+
+        foreach ($entries as $key => $entry) {
+            $entry = trim($entry);
+            if (empty($entry)) {
+                return get_string('validateemptylineerror', 'admin');
+            }
+
+            // Validate each string entry against the supported formats.
+            if (\core\ip_utils::is_ip_address($entry) || \core\ip_utils::is_ipv6_range($entry)
+                    || \core\ip_utils::is_ipv4_range($entry) || \core\ip_utils::is_domain_name($entry)
+                    || \core\ip_utils::is_domain_matching_pattern($entry)) {
+                continue;
+            }
+
+            // Otherwise, the entry is invalid.
+            $badentries[] = $entry;
+        }
+
+        if ($badentries) {
+            return get_string('validateerrorlist', 'admin', join(', ', $badentries));
+        }
+        return true;
+    }
+
+    /**
+     * Convert any lines containing international domain names (IDNs) to their ascii-compatible encoding (ACE).
+     *
+     * @param string $data the setting data, as sent from the web form.
+     * @return string $data the setting data, with all IDNs converted (using punycode) to their ascii encoded version.
+     */
+    protected function ace_encode($data) {
+        if (empty($data)) {
+            return $data;
+        }
+        $entries = explode("\n", $data);
+        foreach ($entries as $key => $entry) {
+            $entry = trim($entry);
+            // This regex matches any string which:
+            // a) contains at least one non-ascii unicode character AND
+            // b) starts with a-zA-Z0-9 or any non-ascii unicode character AND
+            // c) ends with a-zA-Z0-9 or any non-ascii unicode character
+            // d) contains a-zA-Z0-9, hyphen, dot or any non-ascii unicode characters in the middle.
+            if (preg_match('/^(?=[^\x00-\x7f])([^\x00-\x7f]|[a-zA-Z0-9])([^\x00-\x7f]|[a-zA-Z0-9-.])*([^\x00-\x7f]|[a-zA-Z0-9])$/',
+                $entry)) {
+                // If we can convert the unicode string to an idn, do so.
+                // Otherwise, leave the original unicode string alone and let the validation function handle it (it will fail).
+                $val = idn_to_ascii($entry);
+                $entries[$key] = $val ? $val : $entry;
+            }
+        }
+        return implode("\n", $entries);
+    }
+
+    /**
+     * Decode any ascii-encoded domain names back to their utf-8 representation for display.
+     *
+     * @param string $data the setting data, as found in the database.
+     * @return string $data the setting data, with all ascii-encoded IDNs decoded back to their utf-8 representation.
+     */
+    protected function ace_decode($data) {
+        $entries = explode("\n", $data);
+        foreach ($entries as $key => $entry) {
+            $entry = trim($entry);
+            if (strpos($entry, 'xn--') !== false) {
+                $entries[$key] = idn_to_utf8($entry);
+            }
+        }
+        return implode("\n", $entries);
+    }
+
+    /**
+     * Override, providing utf8-decoding for ascii-encoded IDN strings.
+     *
+     * @return mixed returns punycode-converted setting string if successful, else null.
+     */
+    public function get_setting() {
+        // Here, we need to decode any ascii-encoded IDNs back to their native, utf-8 representation.
+        $data = $this->config_read($this->name);
+        if (function_exists('idn_to_utf8') && !is_null($data)) {
+            $data = $this->ace_decode($data);
+        }
+        return $data;
+    }
+
+    /**
+     * Override, providing ascii-encoding for utf8 (native) IDN strings.
+     *
+     * @param string $data
+     * @return string
+     */
+    public function write_setting($data) {
+        if ($this->paramtype === PARAM_INT and $data === '') {
+            // Do not complain if '' used instead of 0.
+            $data = 0;
+        }
+
+        // Try to convert any non-ascii domains to ACE prior to validation - we can't modify anything in validate!
+        if (function_exists('idn_to_ascii')) {
+            $data = $this->ace_encode($data);
+        }
+
+        $validated = $this->validate($data);
+        if ($validated !== true) {
+            return $validated;
+        }
+        return ($this->config_write($this->name, $data) ? '' : get_string('errorsetting', 'admin'));
+    }
+}
+
+/**
+ * Used to validate a textarea used for port numbers.
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2016 Jake Dallimore (jrhdallimore@gmail.com)
+ */
+class admin_setting_configportlist extends admin_setting_configtextarea {
+
+    /**
+     * Validate the contents of the textarea as port numbers.
+     * Used to validate a new line separated list of ports collected from a textarea control.
+     *
+     * @param string $data A list of ports separated by new lines
+     * @return mixed bool true for success or string:error on failure
+     */
+    public function validate($data) {
+        if (empty($data)) {
+            return true;
+        }
+        $ports = explode("\n", $data);
+        $badentries = [];
+        foreach ($ports as $port) {
+            $port = trim($port);
+            if (empty($port)) {
+                return get_string('validateemptylineerror', 'admin');
+            }
+
+            // Is the string a valid integer number?
+            if (strval(intval($port)) !== $port || intval($port) <= 0) {
+                $badentries[] = $port;
+            }
+        }
+        if ($badentries) {
+            return get_string('validateerrorlist', 'admin', $badentries);
+        }
+        return true;
+    }
+}
+
 
 /**
  * An admin setting for selecting one or more users who have a capability
@@ -8634,22 +8807,6 @@ class admin_setting_webservicesoverview extends admin_setting {
         $return = "";
         $brtag = html_writer::empty_tag('br');
 
-        // Enable mobile web service
-        $enablemobile = new admin_setting_enablemobileservice('enablemobilewebservice',
-                get_string('enablemobilewebservice', 'admin'),
-                get_string('configenablemobilewebservice',
-                        'admin', ''), 0); //we don't want to display it but to know the ws mobile status
-        $manageserviceurl = new moodle_url("/admin/settings.php?section=mobile");
-        $wsmobileparam = new stdClass();
-        $wsmobileparam->enablemobileservice = get_string('enablemobilewebservice', 'admin');
-        $wsmobileparam->manageservicelink = html_writer::link($manageserviceurl,
-                get_string('mobile', 'admin'));
-        $mobilestatus = $enablemobile->get_setting()?get_string('mobilewsenabled', 'webservice'):get_string('mobilewsdisabled', 'webservice');
-        $wsmobileparam->wsmobilestatus = html_writer::tag('strong', $mobilestatus);
-        $return .= $OUTPUT->heading(get_string('enablemobilewebservice', 'admin'), 3, 'main');
-        $return .= $brtag . get_string('enablemobilewsoverview', 'webservice', $wsmobileparam)
-                . $brtag . $brtag;
-
         /// One system controlling Moodle with Token
         $return .= $OUTPUT->heading(get_string('onesystemcontrolling', 'webservice'), 3, 'main');
         $table = new html_table();
index a329bcd..69b06cd 100644 (file)
Binary files a/lib/amd/build/chart_output_chartjs.min.js and b/lib/amd/build/chart_output_chartjs.min.js differ
index 9665a37..99656e8 100644 (file)
@@ -98,6 +98,23 @@ define([
         this._chartjs = new Chartjs(this._canvas[0], this._config);
     };
 
+    /**
+     * Clean data.
+     *
+     * @param {(String|String[])} data A single string or an array of strings.
+     * @returns {(String|String[])}
+     * @protected
+     */
+    Output.prototype._cleanData = function(data) {
+        if (data instanceof Array) {
+            return data.map(function(value) {
+                return $('<span>').html(value).text();
+            });
+        } else {
+            return $('<span>').html(data).text();
+        }
+    };
+
     /**
      * Get the chart type and handles the Chart.js specific chart types.
      *
@@ -143,7 +160,7 @@ define([
         if (axis.getLabel() !== null) {
             scaleData.scaleLabel = {
                 display: true,
-                labelString: axis.getLabel()
+                labelString: this._cleanData(axis.getLabel())
             };
         }
 
@@ -176,13 +193,13 @@ define([
         var config = {
             type: this._getChartType(),
             data: {
-                labels: this._chart.getLabels(),
+                labels: this._cleanData(this._chart.getLabels()),
                 datasets: this._makeDatasetsConfig()
             },
             options: {
                 title: {
                     display: this._chart.getTitle() !== null,
-                    text: this._chart.getTitle()
+                    text: this._cleanData(this._chart.getTitle())
                 }
             }
         };
@@ -236,7 +253,7 @@ define([
         var sets = this._chart.getSeries().map(function(series) {
             var colors = series.hasColoredValues() ? series.getColors() : series.getColor();
             var dataset = {
-                label: series.getLabel(),
+                label: this._cleanData(series.getLabel()),
                 data: series.getValues(),
                 type: series.getType(),
                 fill: false,
@@ -276,11 +293,11 @@ define([
         var tooltipData = chartData[tooltipItem.index];
 
         // Build default tooltip.
-        var tooltip = serieLabel + ': ' + tooltipData;
+        var tooltip = this._cleanData(serieLabel) + ': ' + tooltipData;
 
-        // Add serie labels to the tooltip if any.
+        // Add series labels to the tooltip if any.
         if (serieLabels !== null) {
-            tooltip = serieLabels[tooltipItem.index];
+            tooltip = this._cleanData(serieLabels[tooltipItem.index]);
         }
 
         return tooltip;
diff --git a/lib/classes/files/curl_security_helper.php b/lib/classes/files/curl_security_helper.php
new file mode 100644 (file)
index 0000000..6c0639f
--- /dev/null
@@ -0,0 +1,254 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle 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 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains a class providing functions used to check the host/port black/whitelists for curl.
+ *
+ * @package   core
+ * @copyright 2016 Jake Dallimore
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Jake Dallimore <jrhdallimore@gmail.com>
+ */
+
+namespace core\files;
+use core\ip_utils;
+
+defined('MOODLE_INTERNAL') || exit();
+
+/**
+ * Host and port checking for curl.
+ *
+ * This class provides a means to check URL/host/port against the system-level cURL security entries.
+ * It does not provide a means to add URLs, hosts or ports to the black/white lists; this is configured manually
+ * via the site admin section of Moodle (See: 'Site admin' > 'Security' > 'HTTP Security').
+ *
+ * This class is currently used by the 'curl' wrapper class in lib/filelib.php.
+ * Depends on:
+ *  core\ip_utils (several functions)
+ *  moodlelib (clean_param)
+ *
+ * @package   core
+ * @copyright 2016 Jake Dallimore
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Jake Dallimore <jrhdallimore@gmail.com>
+ */
+class curl_security_helper extends curl_security_helper_base {
+    /**
+     * @var array of supported transport schemes and their respective default ports.
+     */
+    protected $transportschemes = [
+        'http' => 80,
+        'https' => 443
+    ];
+
+    /**
+     * Checks whether the given URL is blacklisted by checking its address and port number against the black/white lists.
+     * The behaviour of this function can be classified as strict, as it returns true for URLs which are invalid or
+     * could not be parsed, as well as those valid URLs which were found in the blacklist.
+     *
+     * @param string $urlstring the URL to check.
+     * @return bool true if the URL is blacklisted or invalid and false if the URL is not blacklisted.
+     */
+    public function url_is_blocked($urlstring) {
+        // If no config data is present, then all hosts/ports are allowed.
+        if (!$this->is_enabled()) {
+       &nb