diff --git a/Gruntfile.js b/Gruntfile.js index 21d483b..3c23399 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,60 +1,125 @@ +'use strict'; module.exports = function(grunt) { // Import modules. - var path = require('path'); - - // Theme Bootstrap constants. - var LESSDIR = 'less', - THEMEDIR = path.basename(path.resolve('.')); + var path = require("path"); // PHP strings for exec task. - var moodleroot = path.dirname(path.dirname(__dirname)), - configfile = '', - decachephp = '', - dirrootopt = grunt.option('dirroot') || process.env.MOODLE_DIR || ''; + var moodleroot = path.dirname(path.dirname(__dirname)); + var dirrootopt = grunt.option("dirroot") || process.env.MOODLE_DIR || ""; // Allow user to explicitly define Moodle root dir. - if ('' !== dirrootopt) { + if ("" !== dirrootopt) { moodleroot = path.resolve(dirrootopt); } - configfile = path.join(moodleroot, 'config.php'); + var PWD = process.cwd(); - decachephp += 'define(\'CLI_SCRIPT\', true);'; - decachephp += 'require(\'' + configfile + '\');'; - decachephp += 'theme_reset_all_caches();'; + var decachephp = "../../admin/cli/purge_caches.php"; grunt.initConfig({ - exec: { decache: { - cmd: 'php -r "' + decachephp + '"', - callback: function(error, stdout, stderror) { - // exec will output error messages - // just add one to confirm success. + cmd: 'php "' + decachephp + '"', + callback: function(error) { + // The exec process will output error messages. + // Just add one to confirm success. if (!error) { grunt.log.writeln("Moodle theme cache reset."); } } + }, + postcss: { + command: 'npm run postcss' } }, watch: { // Watch for any changes to less files and compile. - files: ["less/**/*.less"], - tasks: ["decache"], + files: ["scss/**/*.scss"], + tasks: ["compile"], options: { spawn: false, livereload: true } - } + }, + stylelint: { + scss: { + options: { + syntax: "scss" + }, + src: ["scss/**/*.scss"] + }, + css: { + src: ["*/**/*.css"], + options: { + configOverrides: { + rules: { + // These rules have to be disabled in .stylelintrc for scss compat. + "at-rule-no-unknown": true, + "no-browser-hacks": [true, {"severity": "warning"}] + } + } + } + } + }, + jshint: { + options: { + jshintrc: true + }, + files: ["**/amd/src/*.js"] + }, + uglify: { + dynamic_mappings: { + files: grunt.file.expandMapping( + ["**/src/*.js", "!**/node_modules/**"], + "", + { + cwd: PWD, + rename: function(destBase, destPath) { + destPath = destPath.replace("src", "build"); + destPath = destPath.replace(".js", ".min.js"); + destPath = path.resolve(PWD, destPath); + return destPath; + } + } + ) + } + }, + sass: { + options: { + style: 'expanded' + }, + dist: { + files: { + 'style/elegance.css': 'scss/gruntcompile.scss' + } + } + }, }); // Load contrib tasks. grunt.loadNpmTasks("grunt-contrib-watch"); grunt.loadNpmTasks("grunt-exec"); + // Load core tasks. + grunt.loadNpmTasks("grunt-contrib-uglify"); + grunt.loadNpmTasks("grunt-contrib-jshint"); + grunt.loadNpmTasks("grunt-sass"); + grunt.loadNpmTasks("grunt-stylelint"); + + // Register CSS taks. + grunt.registerTask("css", ["stylelint:scss", "stylelint:css"]); + // Register tasks. grunt.registerTask("default", ["watch"]); grunt.registerTask("decache", ["exec:decache"]); -}; + + grunt.registerTask("compile", [ + "sass", + "exec:postcss", + "decache" + ]); + + grunt.registerTask("amd", ["jshint", "uglify"]); +}; \ No newline at end of file diff --git a/README.md b/README.md index 63bd8f4..a453e2b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ # Elegance -## A Bootstrap 3 Moodle Theme +## A Boost child theme ### Features -[Elegance](https://moodle.org/plugins/view.php?plugin=theme_elegance) is a beautiful Free Moodle theme with robust functionality and lots of custom settings. This theme is a full rewrite of the Moodle 2.8 theme elegance version. This theme requires the theme "[Bootstrap](https://moodle.org/plugins/view.php?plugin=theme_bootstrap)". If you're using Moodle auto-updater it will automatically install it too. - -If you have tried this theme and would like to have it customized and styled for your organisation in any way feel free to [contact me](http://theming.sonsbeekmedia.nl/blocks/dashboard/contact.php?dashboard=contact) to request a quotation. +[Elegance](https://moodle.org/plugins/view.php?plugin=theme_elegance) is a beautiful Free Moodle theme with robust functionality and lots of custom settings. This theme is a full rewrite of the Moodle 2.8 theme elegance version. Custom Settings * Custom "Quick Links". @@ -14,9 +12,7 @@ Custom Settings * Custom Footer. * A frontpage Slide Show. * Customizable colours. -* Message notifications. * Login Page backgroud images. -* Page loading indicator. ### Contributors version 2.9 @@ -34,11 +30,28 @@ Custom Settings * [David Bezemer](http://www.davidbezemer.nl) ### Notes -* +* * Please do not use the github version of this theme in a production environment. The current plugin repository version will always be the most stable. ### Changelog +### Version 3.0 Beta + +Moved to theme Boost as a parent theme_elegance. +Removed options: + +* blocks configuration, the theme only has the right blocks column +* videowidth +* mobilecss +* transparency + +Added features. + +* Show available courses on front page in a card layout +* Use the Boost navdrawer +* New Caroussel +* New style marketing spot + [v2.9](https://github.com/bmbrands/moodle-theme_elegance) ### New Features: @@ -80,4 +93,4 @@ fixed: H1-H6 sizes not sequential fixed: default category icons don’t show up enhanced: all default images are public domain from [unsplash](http://unsplash.com) enhanced: fontawesome updated to latest version -added: new custom css field for Moodle Mobile app \ No newline at end of file +added: new custom css field for Moodle Mobile app diff --git a/classes/output/carousel.php b/classes/output/carousel.php new file mode 100644 index 0000000..19c0450 --- /dev/null +++ b/classes/output/carousel.php @@ -0,0 +1,141 @@ +. + +/** + * Class theme_elegance + * + * @package theme_elgance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_elegance\output; + +use stdClass; +use theme_config; + +/** + * Class renderer + * + * @package theme_elgance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class carousel implements \templatable, \renderable { + + /** + * Function to export the renderer data in a format that is suitable for a + * mustache template. This means: + * 1. No complex types - only stdClass, array, int, string, float, bool + * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks). + * + * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return array + */ + public function export_for_template($output) { + $theme = theme_config::load('elegance'); + $settings = $theme->settings; + $slidenum = $settings->slidenumber; + $template = new stdClass(); + + if ($slidenum == 0) { + return ''; + } + + switch ($settings->togglebanner) { + case 1: + break; + case 2: + if (isloggedin()) { + return ''; + } + break; + case 3: + if (!isloggedin()) { + return ''; + } + break; + case 4: + return ''; + break; + } + + $template->slidespeed = $settings->slidespeed; + $banners = []; + $count = 0; + foreach (range(1, $slidenum) as $bannernumber) { + $template->hascontent = true; + $banner = new stdClass(); + $banner->active = ''; + $banner->count = $count++; + $enablebanner = 'enablebanner' . $bannernumber; + $banner->enablebanner = (!empty($settings->$enablebanner)); + + $bannerimage = 'bannerimage' . $bannernumber; + $bannerimageset = (!empty($settings->$bannerimage)); + if ($bannerimageset) { + $banner->bannerimage = $theme->setting_file_url($bannerimage, $bannerimage); + } + + $bannertitle = 'bannertitle' . $bannernumber; + if (!empty($settings->$bannertitle)) { + $banner->bannertitle = $settings->$bannertitle; + } else { + $banner->bannertitle = false; + } + + $bannertext = 'bannertext' . $bannernumber; + if (!empty($settings->$bannertext)) { + $banner->bannertext = $settings->$bannertext; + } else { + $banner->bannertext = false; + } + + $bannerurl = 'bannerurl' . $bannernumber; + if (!empty($settings->$bannerurl)) { + $banner->bannerurl = $settings->$bannerurl; + } else { + $banner->bannerurl = false; + } + + $bannercolor = 'bannercolor' . $bannernumber; + if (!empty($settings->$bannercolor)) { + $banner->bannercolor = $settings->$bannercolor; + } else { + $banner->bannercolor = "transparent"; + } + + $bannerlinktext = 'bannerlinktext' . $bannernumber; + if (!empty($settings->$bannerlinktext)) { + $banner->bannerlinktext = $settings->$bannerlinktext; + } else { + $banner->bannerlinktext = false; + } + + $bannerlinkurl = 'bannerlinkurl' . $bannernumber; + if (!empty($settings->$bannerlinkurl)) { + $banner->bannerlinkurl = $settings->$bannerlinkurl; + } else { + $banner->bannerlinkurl = false; + } + $banners[] = $banner; + } + $banners[0]->active = 'active'; + $template->banners = $banners; + + return $template; + } +} \ No newline at end of file diff --git a/classes/output/core/course_renderer.php b/classes/output/core/course_renderer.php new file mode 100644 index 0000000..30749f9 --- /dev/null +++ b/classes/output/core/course_renderer.php @@ -0,0 +1,227 @@ +. + +/** + * Course renderer. + * + * @package theme_noanme + * @copyright 2016 Frédéric Massart - FMCorz.net + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_elegance\output\core; +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; +use stdClass; +use html_writer; + +require_once($CFG->dirroot . '/course/renderer.php'); + +/** + * Course renderer class. + * + * @package theme_elegance + * @copyright 2019 Bas Brands - + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_renderer extends \core_course_renderer { + + /** + * Renders html to display a course search form. + * + * @param string $value default value to populate the search field + * @param string $format display format - 'plain' (default), 'short' or 'navbar' + * @return string + */ + public function course_search_form($value = '', $format = 'plain') { + static $count = 0; + $formid = 'coursesearch'; + if ((++$count) > 1) { + $formid .= $count; + } + + switch ($format) { + case 'navbar' : + $formid = 'coursesearchnavbar'; + $inputid = 'navsearchbox'; + $inputsize = 20; + break; + case 'short' : + $inputid = 'shortsearchbox'; + $inputsize = 12; + break; + default : + $inputid = 'coursesearchbox'; + $inputsize = 30; + } + + $data = (object) [ + 'searchurl' => (new moodle_url('/course/search.php'))->out(false), + 'id' => $formid, + 'inputid' => $inputid, + 'inputsize' => $inputsize, + 'value' => $value + ]; + if ($format != 'navbar') { + $helpicon = new \help_icon('coursesearch', 'core'); + $data->helpicon = $helpicon->export_for_template($this); + } + + return $this->render_from_template('theme_boost/course_search_form', $data); + } + + protected function coursecat_coursebox(\coursecat_helper $chelper, $course, $additionalclasses = '') { + if (!isset($this->strings->summary)) { + $this->strings->summary = get_string('summary'); + } + if ($chelper->get_show_courses() <= self::COURSECAT_SHOW_COURSES_COUNT) { + return ''; + } + if ($course instanceof stdClass) { + $course = new core_course_list_element($course); + } + $heading = ''; + $classes = trim('coursebox clearfix '. $additionalclasses); + if ($chelper->get_show_courses() >= self::COURSECAT_SHOW_COURSES_EXPANDED) { + $nametag = 'h4'; + } else { + $classes .= ' collapsed'; + $nametag = 'div'; + } + + // .coursebox + $heading .= html_writer::start_tag('div', array( + 'class' => "", + 'data-courseid' => $course->id, + 'data-type' => self::COURSECAT_TYPE_COURSE, + )); + + $heading .= html_writer::start_tag('div', array('class' => 'info')); + + // course name + $coursename = $chelper->get_course_formatted_name($course); + $coursenamelink = html_writer::link(new moodle_url('/course/view.php', array('id' => $course->id)), + $coursename, array('class' => $course->visible ? '' : 'dimmed')); + $heading .= html_writer::tag($nametag, $coursenamelink, array('class' => 'coursename')); + // If we display course in collapsed form but the course has summary or course contacts, display the link to the info page. + $heading .= html_writer::start_tag('div', array('class' => 'moreinfo')); + if ($chelper->get_show_courses() < self::COURSECAT_SHOW_COURSES_EXPANDED) { + if ($course->has_summary() || $course->has_course_contacts() || $course->has_course_overviewfiles() + || $course->has_custom_fields()) { + $url = new moodle_url('/course/info.php', array('id' => $course->id)); + $image = $this->output->pix_icon('i/info', $this->strings->summary); + $heading .= html_writer::link($url, $image, array('title' => $this->strings->summary)); + // Make sure JS file to expand course content is included. + $this->coursecat_include_js(); + } + } + $heading .= html_writer::end_tag('div'); // .moreinfo + + // print enrolmenticons + if ($icons = enrol_get_course_info_icons($course)) { + $heading .= html_writer::start_tag('div', array('class' => 'enrolmenticons')); + foreach ($icons as $pix_icon) { + $heading .= $this->render($pix_icon); + } + $heading .= html_writer::end_tag('div'); // .enrolmenticons + } + + return $this->custom_coursecat_coursebox_content($chelper, $course, $heading); + } + + protected function custom_coursecat_coursebox_content(\coursecat_helper $chelper, $course, $heading) { + global $CFG; + if ($chelper->get_show_courses() < self::COURSECAT_SHOW_COURSES_EXPANDED) { + return ''; + } + if ($course instanceof stdClass) { + $course = new core_course_list_element($course); + } + $content = ''; + + $template = new stdClass(); + + $template->heading = $heading; + + // display course summary + if ($course->has_summary()) { + $template->summary = $chelper->get_course_formatted_summary($course, + array('overflowdiv' => true, 'noclean' => true, 'para' => false)); + } + + // display course overview files + $template->contentimages = []; + $template->contentfiles = []; + foreach ($course->get_course_overviewfiles() as $file) { + $isimage = $file->is_valid_image(); + $url = file_encode_url("$CFG->wwwroot/pluginfile.php", + '/'. $file->get_contextid(). '/'. $file->get_component(). '/'. + $file->get_filearea(). $file->get_filepath(). $file->get_filename(), !$isimage); + if ($isimage) { + $template->contentimages[] = $url; + } else { + $image = $this->output->pix_icon(file_file_icon($file, 24), $file->get_filename(), 'moodle'); + $filename = html_writer::tag('span', $image, array('class' => 'fp-icon')). + html_writer::tag('span', $file->get_filename(), array('class' => 'fp-filename')); + $template->contentfiles[] = html_writer::tag('span', + html_writer::link($url, $filename), + array('class' => 'coursefile fp-filename-icon')); + } + } + + + $template->contacts = ''; + // Display course contacts. See core_course_list_element::get_course_contacts(). + if ($course->has_course_contacts()) { + $content = html_writer::start_tag('ul', array('class' => 'teachers')); + foreach ($course->get_course_contacts() as $coursecontact) { + $rolenames = array_map(function ($role) { + return $role->displayname; + }, $coursecontact['roles']); + $name = implode(", ", $rolenames).': '. + html_writer::link(new moodle_url('/user/view.php', + array('id' => $coursecontact['user']->id, 'course' => SITEID)), + $coursecontact['username']); + $content .= html_writer::tag('li', $name); + } + $content = html_writer::end_tag('ul'); // .teachers + $template->contacts .= $content; + } + + // display course category if necessary (for example in search results) + $template->category = ''; + if ($chelper->get_show_courses() == self::COURSECAT_SHOW_COURSES_EXPANDED_WITH_CAT) { + if ($cat = core_course_category::get($course->category, IGNORE_MISSING)) { + $content = html_writer::start_tag('div', array('class' => 'coursecat')); + $content .= get_string('category').': '. + html_writer::link(new moodle_url('/course/index.php', array('categoryid' => $cat->id)), + $cat->get_formatted_name(), array('class' => $cat->visible ? '' : 'dimmed')); + $content .= html_writer::end_tag('div'); // .coursecat + $template->category = $content; + } + } + + // Display custom fields. + // if ($course->has_custom_fields()) { + // $handler = core_course\customfield\course_handler::create(); + // $customfields = $handler->display_custom_fields_data($course->get_custom_fields()); + // $template->cutomfield = \html_writer::tag('div', $customfields, ['class' => 'customfields-container']); + // } + + return $this->render_from_template('theme_elegance/overridden_coursebox_content', $template); + } +} diff --git a/classes/output/core_renderer.php b/classes/output/core_renderer.php new file mode 100644 index 0000000..b418598 --- /dev/null +++ b/classes/output/core_renderer.php @@ -0,0 +1,90 @@ +. + +namespace theme_elegance\output; + +use \theme_boost\output\core_renderer as boost_core_renderer; +use stdClass; +use theme_config; + +defined('MOODLE_INTERNAL') || die; + +/** + * Renderers to align Moodle's HTML with that expected by Bootstrap + * + * @package theme_elegance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class core_renderer extends boost_core_renderer { + + + /** + * Fetch the social links from the theme settings. + * + * @return Object Object with array of footerlinks. + */ + public function social_links() { + $theme = theme_config::load('elegance'); + $settings = $theme->settings; + + $template = new stdClass(); + $template->links = []; + + $socialoptions = [ + 'facebook' => 'facebook', + 'twitter' => 'twitter', + 'linkedin' => 'linkedin', + 'youtube' => 'youtube', + 'flickr' => 'flickr', + 'vk' => 'vk', + 'pinterest' => 'pinterest', + 'instagram' => 'instagram', + 'skype' => 'skype', + 'website' => 'home', + 'blog' => 'blog', + 'vimeo' => 'vimeo', + 'tumblr' => 'tumblr' + ]; + + foreach ($socialoptions as $so => $icon) { + if (!empty($settings->$so)) { + $social = new stdClass(); + $social->url = $settings->$so; + $social->name = $so; + $social->icon = $icon; + $social->stringname = get_string($so, 'theme_elegance'); + $template->links[] = $social; + $template->hasicons = true; + } + } + return $this->render_from_template('theme_elegance/widget_social', $template); + } + + /** + * Fetch the custom footer text from the theme settings. + * + * @return Object Object with footer text. + */ + public function footertext() { + $theme = theme_config::load('elegance'); + $settings = $theme->settings; + $template = new stdClass(); + $template->footnote = $settings->footnote; + return $template; + } +} diff --git a/classes/output/frontpage_content.php b/classes/output/frontpage_content.php new file mode 100644 index 0000000..39b5db4 --- /dev/null +++ b/classes/output/frontpage_content.php @@ -0,0 +1,56 @@ +. + +/** + * Class theme_elegance + * + * @package theme_elgance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_elegance\output; + +use stdClass; +use theme_config; + +/** + * Class renderer + * + * @package theme_elgance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class frontpage_content implements \templatable, \renderable { + + /** + * Function to export the renderer data in a format that is suitable for a + * mustache template. This means: + * 1. No complex types - only stdClass, array, int, string, float, bool + * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks). + * + * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return array + */ + public function export_for_template($output) { + $theme = theme_config::load('elegance'); + $settings = $theme->settings; + + $template = new stdClass(); + $template->frontpagetext = $settings->frontpagecontent; + return $template; + } +} \ No newline at end of file diff --git a/classes/output/marketingspots.php b/classes/output/marketingspots.php new file mode 100644 index 0000000..edbc30f --- /dev/null +++ b/classes/output/marketingspots.php @@ -0,0 +1,129 @@ +. + +/** + * Class theme_elegance + * + * @package theme_elgance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_elegance\output; + +use stdClass; +use theme_config; + +/** + * Class renderer + * + * @package theme_elgance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class marketingspots implements \templatable, \renderable { + + /** + * Function to export the renderer data in a format that is suitable for a + * mustache template. This means: + * 1. No complex types - only stdClass, array, int, string, float, bool + * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks). + * + * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return array + */ + public function export_for_template($output) { + $theme = theme_config::load('elegance'); + $settings = $theme->settings; + + switch ($settings->togglemarketing) { + case 1: + break; + case 2: + if (isloggedin()) { + return ''; + } + break; + case 3: + if (!isloggedin()) { + return ''; + } + break; + case 4: + return ''; + break; + } + + $spotsnr = $settings->marketingspotsnr; + + if ($spotsnr == 0) { + return ''; + } + + $template = new stdClass(); + $template->spots = []; + $template->title = ''; + $template->marketingtitletitleicon = ''; + + if (!empty($settings->marketingtitle)) { + $template->marketingtitle = $settings->marketingtitle; + + } + if (!empty($settings->marketingtitleicon)) { + $template->marketingtitleicon = $settings->marketingtitleicon; + } + + $choices = array(); + $choices[1] = 'col-sm-6 col-md-6';//2; + $choices[2] = 'col-6 col-sm-4 col-md-3';//4 + $choices[3] = 'col-6 col-sm-3 col-md-2';//6; + $choices[4] = 'col-6 col-sm-3 col-md-2 col-lg-1';//12; + + if (!empty($settings->marketingspotsinrow)) { + $template->spotclass = $choices[$settings->marketingspotsinrow]; + } else { + $template->spotclass = $choices[2]; + } + + foreach (range(1, $spotsnr) as $spot) { + $title = 'marketingtitle' . $spot; + $icon = 'marketingicon' . $spot; + $content = 'marketingcontent' . $spot; + $url = 'marketingurl' . $spot; + $image = 'marketingimage' . $spot; + + $marketingspot = new stdClass(); + if (!empty($settings->$title)) { + $marketingspot->title = $settings->$title; + } + if (!empty($settings->$image)) { + $marketingspot->image = $theme->setting_file_url($image, $image); + } + if (!empty($settings->$icon)) { + $marketingspot->icon = $settings->$icon; + } + if (!empty($settings->$content)) { + $marketingspot->content = $settings->$content; + } + if (!empty($settings->$url)) { + $marketingspot->url = $settings->$url; + } + $template->spots[] = $marketingspot; + } + + return $template; + } +} \ No newline at end of file diff --git a/classes/output/quicklinks.php b/classes/output/quicklinks.php new file mode 100644 index 0000000..d340e7e --- /dev/null +++ b/classes/output/quicklinks.php @@ -0,0 +1,122 @@ +. + +/** + * Class theme_elegance + * + * @package theme_elgance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_elegance\output; + +use stdClass; +use theme_config; + +/** + * Class renderer + * + * @package theme_elgance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class quicklinks implements \templatable, \renderable { + + /** + * Function to export the renderer data in a format that is suitable for a + * mustache template. This means: + * 1. No complex types - only stdClass, array, int, string, float, bool + * 2. Any additional info that is required for the template is pre-calculated (e.g. capability checks). + * + * @param renderer_base $output Used to do a final render of any components that need to be rendered for export. + * @return array + */ + public function export_for_template($output) { + $theme = theme_config::load('elegance'); + $settings = $theme->settings; + + switch ($settings->togglequicklinks) { + case 1: + break; + case 2: + if (isloggedin()) { + return ''; + } + break; + case 3: + if (!isloggedin()) { + return ''; + } + break; + case 4: + return ''; + break; + } + + $template = new stdClass(); + + $template->quicklinksicon = $settings->quicklinksicon; + $template->quicklinkstitle = $settings->quicklinkstitle; + $quicklinksnumber = $settings->quicklinksnumber; + + if ($quicklinksnumber == 0) { + return ''; + } + + $template->quicklinks = array(); + foreach (range(1, $quicklinksnumber ) as $i) { + $icon = 'quicklinkicon' . $i; + $buttontext = 'quicklinkbuttontext' . $i; + $url = 'quicklinkbuttonurl' . $i; + $iconclass = 'quicklinkiconcolor' . $i; + $buttonclass = 'quicklinkbuttoncolor' . $i; + + $quicklink = new stdClass(); + + if (!empty($settings->$icon)) { + $quicklink->icon = $settings->$icon; + } else { + $quicklink->icon = 'check'; + } + if (!empty($settings->$buttontext)) { + $quicklink->buttontext = $settings->$buttontext; + } else { + $quicklink->buttontext = 'Click here'; + } + if (!empty($settings->$url)) { + $quicklink->url = $settings->$url; + } + $quicklink->iconclass = $iconclass; + $quicklink->buttonclass = $buttonclass; + $template->quicklinks[] = $quicklink; + } + + $count = count($template->quicklinks); + + if ($count < 4) { + $template->classlarge = 'col-lg-' . (12 / $count); + } else { + $template->classlarge = 'col-lg-3'; + } + if ($count < 3) { + $template->classmedium = 'col-md-' . (12 / $count); + } else { + $template->classmedium = 'col-md-4'; + } + return $template; + } +} \ No newline at end of file diff --git a/classes/output/renderer.php b/classes/output/renderer.php new file mode 100644 index 0000000..56fcadb --- /dev/null +++ b/classes/output/renderer.php @@ -0,0 +1,81 @@ +. + +/** + * Class renderer + * + * @package theme_elegance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace theme_elegance\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class renderer + * + * @package theme_elegance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends \plugin_renderer_base { + + /** + * Renderer for the carousel slides + * + * @param carousel $carousel + * @return bool|string + */ + protected function render_carousel(carousel $carousel) { + $context = $carousel->export_for_template($this); + return $this->render_from_template('theme_elegance/widget_carousel', $context); + } + + /** + * Renderer for the marketingspots + * + * @param marketingspots $view + * @return bool|string + */ + protected function render_marketingspots(marketingspots $marketingspots) { + $context = $marketingspots->export_for_template($this); + return $this->render_from_template('theme_elegance/widget_marketingspots', $context); + } + + /** + * Renderer for the quicklinks + * + * @param quicklinks $view + * @return bool|string + */ + protected function render_quicklinks(quicklinks $quicklinks) { + $context = $quicklinks->export_for_template($this); + return $this->render_from_template('theme_elegance/widget_quicklinks', $context); + } + + /** + * Renderer for the frontpage_content + * + * @param frontpage_content $view + * @return bool|string + */ + protected function render_frontpage_content(frontpage_content $frontpage_content) { + $context = $frontpage_content->export_for_template($this); + return $this->render_from_template('theme_elegance/widget_frontpage_content', $context); + } +} diff --git a/config.php b/config.php index dd1d29a..d1e0f2d 100644 --- a/config.php +++ b/config.php @@ -1,5 +1,5 @@ . - /** - * Renderers to align Moodle's HTML with that expected by elegance + * classic config. * - * @package theme_elegance - * @copyright 2015 Bas Brands http://basbrands.nl - * @authors Bas Brands, David Scotson. - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package theme_elegance + * @copyright 2019 Bas Brands + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$THEME->doctype = 'html5'; - -$THEME->yuicssmodules = array(); +// This line protects the file from being accessed by a URL directly. +defined('MOODLE_INTERNAL') || die(); $THEME->name = 'elegance'; -$THEME->enable_dock = true; -$THEME->parents = array('bootstrap'); -$THEME->sheets = array('custom', 'mobile'); -$THEME->lessfile = 'elegance'; -$THEME->parents_exclude_sheets = array('bootstrap' => array('moodle')); -$THEME->lessvariablescallback = 'theme_elegance_less_variables'; -$THEME->extralesscallback = 'theme_elegance_extra_less'; -$THEME->supportscssoptimisation = false; +$THEME->sheets = []; +$THEME->editor_sheets = []; +$THEME->parents = ['boost']; +$THEME->layouts['frontpage'] = [ + 'file' => 'frontpage.php', + 'regions' => ['side-pre'], + 'defaultregion' => 'side-pre', + 'options' => ['nonavbar'], +]; +$THEME->layouts['login'] = [ + 'file' => 'login.php', + 'options' => ['nonavbar'], +]; +$THEME->enable_dock = false; +$THEME->extrascsscallback = 'theme_elegance_get_extra_scss'; +$THEME->prescsscallback = 'theme_elegance_get_pre_scss'; +$THEME->yuicssmodules = array(); $THEME->rendererfactory = 'theme_overridden_renderer_factory'; -$THEME->csspostprocess = 'theme_elegance_process_css'; - -$themeconfig = get_config('theme_elegance'); - -$regions = array('side-pre', 'side-post'); -$singleregion = array('side-pre'); -$defaultregion = 'side-pre'; -$sidemiddle = array('side-middle'); - -if (empty($themeconfig->blocksconfig)) { - // Do nothing -} else if ($themeconfig->blocksconfig == 2) { - $regions = array('side-pre'); - $defaultregion = 'side-pre'; -} else if ($themeconfig->blocksconfig == 3) { - $regions = array('side-post'); - $singleregion = array('side-post'); - $defaultregion = 'side-post'; -} - - -$THEME->layouts = array( - // Most backwards compatible layout without the blocks - this is the layout used by default. - 'base' => array( - 'file' => 'default.php', - 'regions' => array(), - ), - // Standard layout with blocks, this is recommended for most pages with general information. - 'standard' => array( - 'file' => 'default.php', - 'regions' => $regions, - 'defaultregion' => $defaultregion, - ), - // Main course page. - 'course' => array( - 'file' => 'default.php', - 'regions' => $regions, - 'defaultregion' => $defaultregion, - 'options' => array('langmenu' => true), - ), - 'coursecategory' => array( - 'file' => 'default.php', - 'regions' => $regions, - 'defaultregion' => $defaultregion, - ), - // Part of course, typical for modules - default page layout if $cm specified in require_login(). - 'incourse' => array( - 'file' => 'default.php', - 'regions' => $regions, - 'defaultregion' => $defaultregion, - ), - // The site home page. - 'frontpage' => array( - 'file' => 'default.php', - 'regions' => array_merge($regions, $sidemiddle), - 'defaultregion' => $defaultregion, - 'options' => array( - 'nobreadcrumb' => true, - 'hasbanner' => true, - 'hasmarketing' => true, - 'hasquicklinks' => true, - 'hasfrontpagecontent' => true, - 'nomoodleheader' => true), - ), - // Server administration scripts. - 'admin' => array( - 'file' => 'default.php', - 'regions' => $regions, - 'defaultregion' => $defaultregion, - 'options' => array('fluid' => true), - ), - // My dashboard page. - 'mydashboard' => array( - 'file' => 'default.php', - 'regions' => $regions, - 'defaultregion' => $defaultregion, - 'options' => array('langmenu' => true), - ), - // My public page. - 'mypublic' => array( - 'file' => 'default.php', - 'regions' => $regions, - 'defaultregion' => $defaultregion, - ), - 'login' => array( - 'file' => 'default.php', - 'regions' => array(), - 'options' => array('langmenu' => true, 'nobreadcrumb' => true, 'transparentmain' => true, 'nomoodleheader' => true), - ), - - // Pages that appear in pop-up windows - no navigation, no blocks, no header. - 'popup' => array( - 'file' => 'popup.php', - 'regions' => array(), - 'options' => array('nofooter' => true, 'nobreadcrumb' => true), - ), - // No blocks and minimal footer - used for legacy frame layouts only! - 'frametop' => array( - 'file' => 'default.php', - 'regions' => array(), - 'options' => array('nofooter' => true, 'nocoursefooter' => true), - ), - // Embeded pages, like iframe/object embeded in moodleform - it needs as much space as possible. - 'embedded' => array( - 'file' => 'embedded.php', - 'regions' => array() - ), - // Used during upgrade and install, and for the 'This site is undergoing maintenance' message. - // This must not have any blocks, links, or API calls that would lead to database or cache interaction. - // Please be extremely careful if you are modifying this layout. - 'maintenance' => array( - 'file' => 'default.php', - 'regions' => array(), - 'options' => array('langmenu' => true, 'nonavbar' => true, 'nobreadcrumb' => true, 'transparentmain' => true, 'nomoodleheader' => true, 'nofooter' => true), - ), - // Should display the content and basic headers only. - 'print' => array( - 'file' => 'default.php', - 'regions' => array(), - 'options' => array('nofooter' => true, 'nobreadcrumb' => false), - ), - // The pagelayout used when a redirection is occuring. - 'redirect' => array( - 'file' => 'embedded.php', - 'regions' => array(), - ), - // The pagelayout used for reports. - 'report' => array( - 'file' => 'default.php', - 'regions' => $singleregion, - 'defaultregion' => $defaultregion, - ), - // The pagelayout used for safebrowser and securewindow. - 'secure' => array( - 'file' => 'default.php', - 'regions' => $regions, - 'defaultregion' => $defaultregion - ), -); - -$THEME->csspostprocess = 'theme_elegance_process_css'; - -$THEME->javascripts = array( -); - -$THEME->javascripts_footer = array( - 'fitvid', 'blocks','reader' -); - -$THEME->hidefromselector = false; +$THEME->scss = function($theme) { + return theme_elegance_get_main_scss_content($theme); +}; diff --git a/fonts/FontAwesome.otf b/fonts/FontAwesome.otf deleted file mode 100644 index 681bdd4..0000000 Binary files a/fonts/FontAwesome.otf and /dev/null differ diff --git a/fonts/fontawesome-webfont.eot b/fonts/fontawesome-webfont.eot deleted file mode 100644 index a30335d..0000000 Binary files a/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/fonts/fontawesome-webfont.svg b/fonts/fontawesome-webfont.svg deleted file mode 100644 index 6fd19ab..0000000 --- a/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,640 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/fontawesome-webfont.ttf b/fonts/fontawesome-webfont.ttf deleted file mode 100644 index d7994e1..0000000 Binary files a/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/fonts/fontawesome-webfont.woff b/fonts/fontawesome-webfont.woff deleted file mode 100644 index 6fd4ede..0000000 Binary files a/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/fonts/fontawesome-webfont.woff2 b/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 5560193..0000000 Binary files a/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/fonts/glyphicons-halflings-regular.eot b/fonts/glyphicons-halflings-regular.eot deleted file mode 100644 index 423bd5d..0000000 Binary files a/fonts/glyphicons-halflings-regular.eot and /dev/null differ diff --git a/fonts/glyphicons-halflings-regular.svg b/fonts/glyphicons-halflings-regular.svg deleted file mode 100644 index 753e28f..0000000 --- a/fonts/glyphicons-halflings-regular.svg +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/fonts/glyphicons-halflings-regular.ttf b/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index a498ef4..0000000 Binary files a/fonts/glyphicons-halflings-regular.ttf and /dev/null differ diff --git a/fonts/glyphicons-halflings-regular.woff b/fonts/glyphicons-halflings-regular.woff deleted file mode 100644 index d83c539..0000000 Binary files a/fonts/glyphicons-halflings-regular.woff and /dev/null differ diff --git a/javascript/blocks.js b/javascript/blocks.js deleted file mode 100644 index 1d22f5b..0000000 --- a/javascript/blocks.js +++ /dev/null @@ -1,39 +0,0 @@ -/* Clicking the Header of the block, hide/show the block's content - Credit to Itamar Zadok (https://moodle.org/mod/forum/discuss.php?d=218799#p982036) -*/ - -YUI().use("node-base","node-event-simulate", function(Y) { - var btn1_Click = function(e) - { - var targetBlock = e.target.ancestor('.block', true); - - // Don't run if in editing mode - if (targetBlock.hasClass('block_with_controls')) { - return false; - } - - var hiderShow = targetBlock.one('.block-hider-show'); - var hiderHide = targetBlock.one('.block-hider-hide'); - - // If propogating from simulation, halt - if (e.target == hiderShow || e.target == hiderHide) { - e.halt(true); - return false; - } - - if (targetBlock.hasClass('hidden')) { - // Try show - if (hiderShow) { - hiderShow.simulate('click'); - } - } else { - // Try hide - if (hiderHide) { - hiderHide.simulate('click'); - } - } - return false; - - }; - Y.on("click", btn1_Click, ".header .title"); -}); diff --git a/javascript/fitvid.js b/javascript/fitvid.js deleted file mode 100644 index 25a4e72..0000000 --- a/javascript/fitvid.js +++ /dev/null @@ -1,6 +0,0 @@ -/* - FitVid will dynamically fit embedded video in Moodle -*/ - $(document).ready(function(){ - $(".mediaplugin").fitVids(); - }); diff --git a/javascript/reader.js b/javascript/reader.js deleted file mode 100644 index 658a162..0000000 --- a/javascript/reader.js +++ /dev/null @@ -1,88 +0,0 @@ -/*! Reader.js - * copyright 2014 Bas Brands, www.basbrands.nl - * authors Bas Brands, David Scotson - * license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * */ -YUI.add('moodle-theme_elegance-reader', function(Y) { - - var reader = 'reader panel'; - - var reader = function() { - reader.superclass.constructor.apply(this, arguments); - }; - - // Make the colour switcher a fully fledged YUI module - Y.extend(reader, Y.Base, { - - initializer : function(config) { - - var onClick = function(e) { - var target = e.currentTarget.getAttribute('dataid'); - targetnode = Y.one(target); - - var openBtn = e.currentTarget, panel, bb; - - function showPanel() { - panel.show(); - } - - function hidePanel() { - panel.hide(); - } - - obj = Y.Node.create('
'); - contentcontainer = Y.Node.create('
'); - - Y.one('body').insert(obj,'#page'); - obj.prepend(contentcontainer,obj); - - var pagewidth = Y.one('body').get('docWidth'); - if ( pagewidth < 480 ) { - var marginleft = 0; - var width = pagewidth; - } else { - var marginleft = 15; - var width = pagewidth - 30; - } - - panel = new Y.Panel({ - srcNode: obj, - width : width, - zIndex : 6, - modal : true, - contstrain: 'body', - x: marginleft, - y: 10, - visible: false, - render : true, - }); - - bb = panel.get('boundingBox'); - - var readercontent = targetnode.getHTML() - contentcontainer.setHTML(readercontent); - - showPanel(); - panel.after('visibleChange', function (e) { - if (!e.newVal) { // panel is hidden - Y.later(0, this, this.destroy); - } - }); - }; - - Y.one('body').delegate('click', onClick, '.moodlereader'); - }, - - }, { - NAME : 'bootstrap yui modal', - ATTRS : { - } - }); - // Our leaf theme namespace - M.theme_elegance = M.theme_elegance || {}; - // Initialisation function for the colour switcher - M.theme_elegance.initreader = function(cfg) { - return new reader(cfg); - } - -}, '@VERSION@', {requires:['panel','node','node-load','attribute', 'event']}); diff --git a/jquery/backstretch_2.0.6.js b/jquery/backstretch_2.0.6.js deleted file mode 100644 index acfe726..0000000 --- a/jquery/backstretch_2.0.6.js +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Backstretch - * http://srobbin.com/jquery-plugins/backstretch/ - * - * Copyright (c) 2013 Scott Robbin - * Licensed under the MIT license. - */ - -;(function ($, window, undefined) { - 'use strict'; - - /* PLUGIN DEFINITION - * ========================= */ - - $.fn.backstretch = function (images, options) { - - /* - * Scroll the page one pixel to get the right window height on iOS - * Pretty harmless for everyone else - */ - if ($(window).scrollTop() === 0 ) { - window.scrollTo(0, 0); - } - - return this.each(function () { - var $this = $(this) - , obj = $this.data('backstretch'); - - // Do we already have an instance attached to this element? - if (obj) { - - // Is this a method they're trying to execute? - if (typeof images == 'string' && typeof obj[images] == 'function') { - // Call the method - obj[images](options); - - // No need to do anything further - return; - } - - // Merge the old options with the new - options = $.extend(obj.options, options); - - // Remove the old instance - obj.destroy(true); - } - - // We need at least one image - if (images === undefined) { - if ($this.css('backgroundImage')) { - images = [$this.css('backgroundImage').replace(/url\(|\)|"|'/g,"")]; - } else { - $.error('No images were supplied for Backstretch, or element must have a CSS-defined background image.'); - } - } - - obj = new Backstretch(this, images, options); - $this.data('backstretch', obj); - }); - }; - - // If no element is supplied, we'll attach to body - $.backstretch = function (images, options) { - // Return the instance - return $('body') - .backstretch(images, options) - .data('backstretch'); - }; - - // Custom selector - $.expr[':'].backstretch = function(elem) { - return $(elem).data('backstretch') !== undefined; - }; - - /* DEFAULTS - * ========================= */ - - $.fn.backstretch.defaults = { - centeredX: true // Should we center the image on the X axis? - , centeredY: true // Should we center the image on the Y axis? - , duration: 5000 // Amount of time in between slides (if slideshow) - , fade: 0 // Speed of fade transition between slides - }; - - /* STYLES - * - * Baked-in styles that we'll apply to our elements. - * In an effort to keep the plugin simple, these are not exposed as options. - * That said, anyone can override these in their own stylesheet. - * ========================= */ - var styles = { - wrap: { - left: 0 - , top: 0 - , overflow: 'hidden' - , margin: 0 - , padding: 0 - , height: '100%' - , width: '100%' - , zIndex: -999999 - } - , img: { - position: 'absolute' - , display: 'none' - , margin: 0 - , padding: 0 - , border: 'none' - , width: 'auto' - , height: 'auto' - , maxWidth: 'none' - , zIndex: -999999 - } - }; - - /* CLASS DEFINITION - * ========================= */ - var Backstretch = function (container, images, options) { - this.options = $.extend({}, $.fn.backstretch.defaults, options || {}); - - - /* In its simplest form, we allow Backstretch to be called on an image path. - * e.g. $.backstretch('/path/to/image.jpg') - * So, we need to turn this back into an array. - */ - this.images = $.isArray(images) ? images : [images]; - - // Preload images - $.each(this.images, function () { - $('')[0].src = this; - }); - - // Convenience reference to know if the container is body. - this.isBody = container === document.body; - - /* We're keeping track of a few different elements - * - * Container: the element that Backstretch was called on. - * Wrap: a DIV that we place the image into, so we can hide the overflow. - * Root: Convenience reference to help calculate the correct height. - */ - this.$container = $(container); - this.$root = this.isBody ? supportsFixedPosition ? $(window) : $(document) : this.$container; - - // Don't create a new wrap if one already exists (from a previous instance of Backstretch) - var $existing = this.$container.children(".backstretch").first(); - this.$wrap = $existing.length ? $existing : $('
').css(styles.wrap).appendTo(this.$container); - - // Non-body elements need some style adjustments - if (!this.isBody) { - // If the container is statically positioned, we need to make it relative, - // and if no zIndex is defined, we should set it to zero. - var position = this.$container.css('position') - , zIndex = this.$container.css('zIndex'); - - this.$container.css({ - position: position === 'static' ? 'relative' : position - , zIndex: zIndex === 'auto' ? 0 : zIndex - , background: 'none' - }); - - // Needs a higher z-index - this.$wrap.css({zIndex: -999998}); - } - - // Fixed or absolute positioning? - this.$wrap.css({ - position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute' - }); - - // Set the first image - this.index = 0; - this.show(this.index); - - // Listen for resize - $(window).on('resize.backstretch', $.proxy(this.resize, this)) - .on('orientationchange.backstretch', $.proxy(function () { - // Need to do this in order to get the right window height - if (this.isBody && window.pageYOffset === 0) { - window.scrollTo(0, 1); - this.resize(); - } - }, this)); - }; - - /* PUBLIC METHODS - * ========================= */ - Backstretch.prototype = { - resize: function () { - try { - var bgCSS = {left: 0, top: 0} - , rootWidth = this.isBody ? this.$root.width() : this.$root.innerWidth() - , bgWidth = rootWidth - , rootHeight = this.isBody ? ( window.innerHeight ? window.innerHeight : this.$root.height() ) : this.$root.innerHeight() - , bgHeight = bgWidth / this.$img.data('ratio') - , bgOffset; - - // Make adjustments based on image ratio - if (bgHeight >= rootHeight) { - bgOffset = (bgHeight - rootHeight) / 2; - if(this.options.centeredY) { - bgCSS.top = '-' + bgOffset + 'px'; - } - } else { - bgHeight = rootHeight; - bgWidth = bgHeight * this.$img.data('ratio'); - bgOffset = (bgWidth - rootWidth) / 2; - if(this.options.centeredX) { - bgCSS.left = '-' + bgOffset + 'px'; - } - } - - this.$wrap.css({width: rootWidth, height: rootHeight}) - .find('img:not(.deleteable)').css({width: bgWidth, height: bgHeight}).css(bgCSS); - } catch(err) { - // IE7 seems to trigger resize before the image is loaded. - // This try/catch block is a hack to let it fail gracefully. - } - - return this; - } - - // Show the slide at a certain position - , show: function (newIndex) { - - // Validate index - if (Math.abs(newIndex) > this.images.length - 1) { - return; - } - - // Vars - var self = this - , oldImage = self.$wrap.find('img').addClass('deleteable') - , evtOptions = { relatedTarget: self.$container[0] }; - - // Trigger the "before" event - self.$container.trigger($.Event('backstretch.before', evtOptions), [self, newIndex]); - - // Set the new index - this.index = newIndex; - - // Pause the slideshow - clearInterval(self.interval); - - // New image - self.$img = $('') - .css(styles.img) - .bind('load', function (e) { - var imgWidth = this.width || $(e.target).width() - , imgHeight = this.height || $(e.target).height(); - - // Save the ratio - $(this).data('ratio', imgWidth / imgHeight); - - // Show the image, then delete the old one - // "speed" option has been deprecated, but we want backwards compatibilty - $(this).fadeIn(self.options.speed || self.options.fade, function () { - oldImage.remove(); - - // Resume the slideshow - if (!self.paused) { - self.cycle(); - } - - // Trigger the "after" and "show" events - // "show" is being deprecated - $(['after', 'show']).each(function () { - self.$container.trigger($.Event('backstretch.' + this, evtOptions), [self, newIndex]); - }); - }); - - // Resize - self.resize(); - }) - .appendTo(self.$wrap); - - // Hack for IE img onload event - self.$img.attr('src', self.images[newIndex]); - return self; - } - - , next: function () { - // Next slide - return this.show(this.index < this.images.length - 1 ? this.index + 1 : 0); - } - - , prev: function () { - // Previous slide - return this.show(this.index === 0 ? this.images.length - 1 : this.index - 1); - } - - , pause: function () { - // Pause the slideshow - this.paused = true; - return this; - } - - , resume: function () { - // Resume the slideshow - this.paused = false; - this.next(); - return this; - } - - , cycle: function () { - // Start/resume the slideshow - if(this.images.length > 1) { - // Clear the interval, just in case - clearInterval(this.interval); - - this.interval = setInterval($.proxy(function () { - // Check for paused slideshow - if (!this.paused) { - this.next(); - } - }, this), this.options.duration); - } - return this; - } - - , destroy: function (preserveBackground) { - // Stop the resize events - $(window).off('resize.backstretch orientationchange.backstretch'); - - // Clear the interval - clearInterval(this.interval); - - // Remove Backstretch - if(!preserveBackground) { - this.$wrap.remove(); - } - this.$container.removeData('backstretch'); - } - }; - - /* SUPPORTS FIXED POSITION? - * - * Based on code from jQuery Mobile 1.1.0 - * http://jquerymobile.com/ - * - * In a nutshell, we need to figure out if fixed positioning is supported. - * Unfortunately, this is very difficult to do on iOS, and usually involves - * injecting content, scrolling the page, etc.. It's ugly. - * jQuery Mobile uses this workaround. It's not ideal, but works. - * - * Modified to detect IE6 - * ========================= */ - - var supportsFixedPosition = (function () { - var ua = navigator.userAgent - , platform = navigator.platform - // Rendering engine is Webkit, and capture major version - , wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ) - , wkversion = !!wkmatch && wkmatch[ 1 ] - , ffmatch = ua.match( /Fennec\/([0-9]+)/ ) - , ffversion = !!ffmatch && ffmatch[ 1 ] - , operammobilematch = ua.match( /Opera Mobi\/([0-9]+)/ ) - , omversion = !!operammobilematch && operammobilematch[ 1 ] - , iematch = ua.match( /MSIE ([0-9]+)/ ) - , ieversion = !!iematch && iematch[ 1 ]; - - return !( - // iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5) - ((platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ) && wkversion && wkversion < 534) || - - // Opera Mini - (window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]") || - (operammobilematch && omversion < 7458) || - - //Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2) - (ua.indexOf( "Android" ) > -1 && wkversion && wkversion < 533) || - - // Firefox Mobile before 6.0 - - (ffversion && ffversion < 6) || - - // WebOS less than 3 - ("palmGetResource" in window && wkversion && wkversion < 534) || - - // MeeGo - (ua.indexOf( "MeeGo" ) > -1 && ua.indexOf( "NokiaBrowser/8.5.0" ) > -1) || - - // IE6 - (ieversion && ieversion <= 6) - ); - }()); - -}(jQuery, window)); diff --git a/jquery/backstretch_2.1.17.js b/jquery/backstretch_2.1.17.js new file mode 100644 index 0000000..88d02a0 --- /dev/null +++ b/jquery/backstretch_2.1.17.js @@ -0,0 +1,1615 @@ +/* + * Backstretch + * http://srobbin.com/jquery-plugins/backstretch/ + * + * Copyright (c) 2013 Scott Robbin + * Licensed under the MIT license. + */ + +;(function ($, window, undefined) { + 'use strict'; + + /** @const */ + var YOUTUBE_REGEXP = /^.*(youtu\.be\/|youtube\.com\/v\/|youtube\.com\/embed\/|youtube\.com\/watch\?v=|youtube\.com\/watch\?.*\&v=)([^#\&\?]*).*/i; + + /* PLUGIN DEFINITION + * ========================= */ + + $.fn.backstretch = function (images, options) { + var args = arguments; + + /* + * Scroll the page one pixel to get the right window height on iOS + * Pretty harmless for everyone else + */ + if ($(window).scrollTop() === 0) { + window.scrollTo(0, 0); + } + + var returnValues; + + this.each(function (eachIndex) { + var $this = $(this) + , obj = $this.data('backstretch'); + + // Do we already have an instance attached to this element? + if (obj) { + + // Is this a method they're trying to execute? + if (typeof args[0] === 'string' && + typeof obj[args[0]] === 'function') { + + // Call the method + var returnValue = obj[args[0]].apply(obj, Array.prototype.slice.call(args, 1)); + if (returnValue === obj) { // If a method is chaining + returnValue = undefined; + } + if (returnValue !== undefined) { + returnValues = returnValues || []; + returnValues[eachIndex] = returnValue; + } + + return; // Nothing further to do + } + + // Merge the old options with the new + options = $.extend(obj.options, options); + + // Remove the old instance + if (typeof obj === 'object' && 'destroy' in obj) { + obj.destroy(true); + } + } + + // We need at least one image + if (!images || (images && images.length === 0)) { + var cssBackgroundImage = $this.css('background-image'); + if (cssBackgroundImage && cssBackgroundImage !== 'none') { + images = [{url: $this.css('backgroundImage').replace(/url\(|\)|"|'/g, "")}]; + } + else { + $.error('No images were supplied for Backstretch, or element must have a CSS-defined background image.'); + } + } + + obj = new Backstretch(this, images, options || {}); + $this.data('backstretch', obj); + }); + + return returnValues ? returnValues.length === 1 ? returnValues[0] : returnValues : this; + }; + + // If no element is supplied, we'll attach to body + $.backstretch = function (images, options) { + // Return the instance + return $('body') + .backstretch(images, options) + .data('backstretch'); + }; + + // Custom selector + $.expr[':'].backstretch = function (elem) { + return $(elem).data('backstretch') !== undefined; + }; + + /* DEFAULTS + * ========================= */ + + $.fn.backstretch.defaults = { + duration: 5000 // Amount of time in between slides (if slideshow) + , transition: 'fade' // Type of transition between slides + , transitionDuration: 0 // Duration of transition between slides + , animateFirst: true // Animate the transition of first image of slideshow in? + , alignX: 0.5 // The x-alignment for the image, can be 'left'|'center'|'right' or any number between 0.0 and 1.0 + , alignY: 0.5 // The y-alignment for the image, can be 'top'|'center'|'bottom' or any number between 0.0 and 1.0 + , paused: false // Whether the images should slide after given duration + , start: 0 // Index of the first image to show + , preload: 2 // How many images preload at a time? + , preloadSize: 1 // How many images can we preload in parallel? + , resolutionRefreshRate: 2500 // How long to wait before switching resolution? + , resolutionChangeRatioThreshold: 0.1 // How much a change should it be before switching resolution? + }; + + /* STYLES + * + * Baked-in styles that we'll apply to our elements. + * In an effort to keep the plugin simple, these are not exposed as options. + * That said, anyone can override these in their own stylesheet. + * ========================= */ + var styles = { + wrap: { + left: 0 + , top: 0 + , overflow: 'hidden' + , margin: 0 + , padding: 0 + , height: '100%' + , width: '100%' + , zIndex: -999999 + } + , itemWrapper: { + position: 'absolute' + , display: 'none' + , margin: 0 + , padding: 0 + , border: 'none' + , width: '100%' + , height: '100%' + , zIndex: -999999 + } + , item: { + position: 'absolute' + , margin: 0 + , padding: 0 + , border: 'none' + , width: '100%' + , height: '100%' + , maxWidth: 'none' + } + }; + + /* Given an array of different options for an image, + * choose the optimal image for the container size. + * + * Given an image template (a string with {{ width }} and/or + * {{height}} inside) and a container object, returns the + * image url with the exact values for the size of that + * container. + * + * Returns an array of urls optimized for the specified resolution. + * + */ + var optimalSizeImages = (function () { + + /* Sorts the array of image sizes based on width */ + var widthInsertSort = function (arr) { + for (var i = 1; i < arr.length; i++) { + var tmp = arr[i], + j = i; + while (arr[j - 1] && parseInt(arr[j - 1].width, 10) > parseInt(tmp.width, 10)) { + arr[j] = arr[j - 1]; + --j; + } + arr[j] = tmp; + } + + return arr; + }; + + /* Given an array of various sizes of the same image and a container width, + * return the best image. + */ + var selectBest = function (containerWidth, containerHeight, imageSizes) { + + var devicePixelRatio = window.devicePixelRatio || 1; + var deviceOrientation = getDeviceOrientation(); + var windowOrientation = getWindowOrientation(); + var wrapperOrientation = (containerHeight > containerWidth) ? + 'portrait' : + (containerWidth > containerHeight ? 'landscape' : 'square'); + + var lastAllowedImage = 0; + var testWidth; + + for (var j = 0, image; j < imageSizes.length; j++) { + + image = imageSizes[j]; + + // In case a new image was pushed in, process it: + if (typeof image === 'string') { + image = imageSizes[j] = {url: image}; + } + + if (image.pixelRatio && image.pixelRatio !== 'auto' && parseFloat(image.pixelRatio) !== devicePixelRatio) { + // We disallowed choosing this image for current device pixel ratio, + // So skip this one. + continue; + } + + if (image.deviceOrientation && image.deviceOrientation !== deviceOrientation) { + // We disallowed choosing this image for current device orientation, + // So skip this one. + continue; + } + + if (image.windowOrientation && image.windowOrientation !== deviceOrientation) { + // We disallowed choosing this image for current window orientation, + // So skip this one. + continue; + } + + if (image.orientation && image.orientation !== wrapperOrientation) { + // We disallowed choosing this image for current element's orientation, + // So skip this one. + continue; + } + + // Mark this one as the last one we investigated + // which does not violate device pixel ratio rules. + // We may choose this one later if there's no match. + lastAllowedImage = j; + + // For most images, we match the specified width against element width, + // And enforcing a limit depending on the "pixelRatio" property if specified. + // But if a pixelRatio="auto", then we consider the width as the physical width of the image, + // And match it while considering the device's pixel ratio. + testWidth = containerWidth; + if (image.pixelRatio === 'auto') { + containerWidth *= devicePixelRatio; + } + + // Stop when the width of the image is larger or equal to the container width + if (image.width >= testWidth) { + break; + } + } + + // Use the image located at where we stopped + return imageSizes[Math.min(j, lastAllowedImage)]; + }; + + var replaceTagsInUrl = function (url, templateReplacer) { + + if (typeof url === 'string') { + url = url.replace(/{{(width|height)}}/g, templateReplacer); + } + else if (url instanceof Array) { + for (var i = 0; i < url.length; i++) { + if (url[i].src) { + url[i].src = replaceTagsInUrl(url[i].src, templateReplacer); + } + else { + url[i] = replaceTagsInUrl(url[i], templateReplacer); + } + } + } + + return url; + }; + + return function ($container, images) { + var containerWidth = $container.width(), + containerHeight = $container.height(); + + var chosenImages = []; + + var templateReplacer = function (match, key) { + if (key === 'width') { + return containerWidth; + } + if (key === 'height') { + return containerHeight; + } + return match; + }; + + for (var i = 0; i < images.length; i++) { + if ($.isArray(images[i])) { + images[i] = widthInsertSort(images[i]); + var chosen = selectBest(containerWidth, containerHeight, images[i]); + chosenImages.push(chosen); + } + else { + // In case a new image was pushed in, process it: + if (typeof images[i] === 'string') { + images[i] = {url: images[i]}; + } + + var item = $.extend({}, images[i]); + item.url = replaceTagsInUrl(item.url, templateReplacer); + chosenImages.push(item); + } + } + return chosenImages; + }; + + })(); + + var isVideoSource = function (source) { + return YOUTUBE_REGEXP.test(source.url) || source.isVideo; + }; + + /* Preload images */ + var preload = (function (sources, startAt, count, batchSize, callback) { + // Plugin cache + var cache = []; + + // Wrapper for cache + var caching = function (image) { + for (var i = 0; i < cache.length; i++) { + if (cache[i].src === image.src) { + return cache[i]; + } + } + cache.push(image); + return image; + }; + + // Execute callback + var exec = function (sources, callback, last) { + if (typeof callback === 'function') { + callback.call(sources, last); + } + }; + + // Closure to hide cache + return function preload(sources, startAt, count, batchSize, callback) { + // Check input data + if (typeof sources === 'undefined') { + return; + } + if (!$.isArray(sources)) { + sources = [sources]; + } + + if (arguments.length < 5 && typeof arguments[arguments.length - 1] === 'function') { + callback = arguments[arguments.length - 1]; + } + + startAt = (typeof startAt === 'function' || !startAt) ? 0 : startAt; + count = (typeof count === 'function' || !count || count < 0) ? sources.length : Math.min(count, sources.length); + batchSize = (typeof batchSize === 'function' || !batchSize) ? 1 : batchSize; + + if (startAt >= sources.length) { + startAt = 0; + count = 0; + } + if (batchSize < 0) { + batchSize = count; + } + batchSize = Math.min(batchSize, count); + + var next = sources.slice(startAt + batchSize, count - batchSize); + sources = sources.slice(startAt, batchSize); + count = sources.length; + + // If sources array is empty + if (!count) { + exec(sources, callback, true); + return; + } + + // Image loading callback + var countLoaded = 0; + + var loaded = function () { + countLoaded++; + if (countLoaded !== count) { + return; + } + + exec(sources, callback, !next); + preload(next, 0, 0, batchSize, callback); + }; + + // Loop sources to preload + var image; + + for (var i = 0; i < sources.length; i++) { + + if (isVideoSource(sources[i])) { + + // Do not preload videos. There are issues with that. + // First - we need to keep an instance of the preloaded and use that exactly, not a copy. + // Second - there are memory issues. + // If there will be a requirement from users - I'll try to implement this. + + continue; + + } + else { + + image = new Image(); + image.src = sources[i].url; + + image = caching(image); + + if (image.complete) { + loaded(); + } + else { + $(image).on('load error', loaded); + } + + } + + } + }; + })(); + + /* Process images array */ + var processImagesArray = function (images) { + var processed = []; + for (var i = 0; i < images.length; i++) { + if (typeof images[i] === 'string') { + processed.push({url: images[i]}); + } + else if ($.isArray(images[i])) { + processed.push(processImagesArray(images[i])); + } + else { + processed.push(processOptions(images[i])); + } + } + return processed; + }; + + /* Process options */ + var processOptions = function (options, required) { + + // Convert old options + + // centeredX/centeredY are deprecated + if (options.centeredX || options.centeredY) { + if (window.console && window.console.log) { + window.console.log('jquery.backstretch: `centeredX`/`centeredY` is deprecated, please use `alignX`/`alignY`'); + } + if (options.centeredX) { + options.alignX = 0.5; + } + if (options.centeredY) { + options.alignY = 0.5; + } + } + + // Deprecated spec + if (options.speed !== undefined) { + + if (window.console && window.console.log) { + window.console.log('jquery.backstretch: `speed` is deprecated, please use `transitionDuration`'); + } + + options.transitionDuration = options.speed; + options.transition = 'fade'; + } + + // Typo + if (options.resolutionChangeRatioTreshold !== undefined) { + window.console.log('jquery.backstretch: `treshold` is a typo!'); + options.resolutionChangeRatioThreshold = options.resolutionChangeRatioTreshold; + } + + // Current spec that needs processing + + if (options.fadeFirst !== undefined) { + options.animateFirst = options.fadeFirst; + } + + if (options.fade !== undefined) { + options.transitionDuration = options.fade; + options.transition = 'fade'; + } + + if (options.scale) { + options.scale = validScale(options.scale); + } + + return processAlignOptions(options); + }; + + /* Process align options */ + var processAlignOptions = function (options, required) { + if (options.alignX === 'left') { + options.alignX = 0.0; + } + else if (options.alignX === 'center') { + options.alignX = 0.5; + } + else if (options.alignX === 'right') { + options.alignX = 1.0; + } + else { + if (options.alignX !== undefined || required) { + options.alignX = parseFloat(options.alignX); + if (isNaN(options.alignX)) { + options.alignX = 0.5; + } + } + } + + if (options.alignY === 'top') { + options.alignY = 0.0; + } + else if (options.alignY === 'center') { + options.alignY = 0.5; + } + else if (options.alignY === 'bottom') { + options.alignY = 1.0; + } + else { + if (options.alignX !== undefined || required) { + options.alignY = parseFloat(options.alignY); + if (isNaN(options.alignY)) { + options.alignY = 0.5; + } + } + } + + return options; + }; + + var SUPPORTED_SCALE_OPTIONS = { + 'cover': 'cover', + 'fit': 'fit', + 'fit-smaller': 'fit-smaller', + 'fill': 'fill' + }; + + function validScale(scale) { + if (!SUPPORTED_SCALE_OPTIONS.hasOwnProperty(scale)) { + return 'cover'; + } + return scale; + } + + /* CLASS DEFINITION + * ========================= */ + var Backstretch = function (container, images, options) { + this.options = $.extend({}, $.fn.backstretch.defaults, options || {}); + + this.firstShow = true; + + // Process options + processOptions(this.options, true); + + /* In its simplest form, we allow Backstretch to be called on an image path. + * e.g. $.backstretch('/path/to/image.jpg') + * So, we need to turn this back into an array. + */ + this.images = processImagesArray($.isArray(images) ? images : [images]); + + /** + * Paused-Option + */ + if (this.options.paused) { + this.paused = true; + } + + /** + * Start-Option (Index) + */ + if (this.options.start >= this.images.length) { + this.options.start = this.images.length - 1; + } + if (this.options.start < 0) { + this.options.start = 0; + } + + // Convenience reference to know if the container is body. + this.isBody = container === document.body; + + /* We're keeping track of a few different elements + * + * Container: the element that Backstretch was called on. + * Wrap: a DIV that we place the image into, so we can hide the overflow. + * Root: Convenience reference to help calculate the correct height. + */ + var $window = $(window); + this.$container = $(container); + this.$root = this.isBody ? supportsFixedPosition ? $window : $(document) : this.$container; + + this.originalImages = this.images; + this.images = optimalSizeImages( + this.options.alwaysTestWindowResolution ? $window : this.$root, + this.originalImages); + + /** + * Pre-Loading. + * This is the first image, so we will preload a minimum of 1 images. + */ + preload(this.images, this.options.start || 0, this.options.preload || 1); + + // Don't create a new wrap if one already exists (from a previous instance of Backstretch) + var $existing = this.$container.children(".backstretch").first(); + this.$wrap = $existing.length ? $existing : + $('
') + .css(this.options.bypassCss ? {} : styles.wrap) + .appendTo(this.$container); + + if (!this.options.bypassCss) { + + // Non-body elements need some style adjustments + if (!this.isBody) { + // If the container is statically positioned, we need to make it relative, + // and if no zIndex is defined, we should set it to zero. + var position = this.$container.css('position') + , zIndex = this.$container.css('zIndex'); + + this.$container.css({ + position: position === 'static' ? 'relative' : position + , zIndex: zIndex === 'auto' ? 0 : zIndex + }); + + // Needs a higher z-index + this.$wrap.css({zIndex: -999998}); + } + + // Fixed or absolute positioning? + this.$wrap.css({ + position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute' + }); + + } + + // Set the first image + this.index = this.options.start; + this.show(this.index); + + // Listen for resize + $window.on('resize.backstretch', $.proxy(this.resize, this)) + .on('orientationchange.backstretch', $.proxy(function () { + // Need to do this in order to get the right window height + if (this.isBody && window.pageYOffset === 0) { + window.scrollTo(0, 1); + this.resize(); + } + }, this)); + }; + + var performTransition = function (options) { + + var transition = options.transition || 'fade'; + + // Look for multiple options + if (typeof transition === 'string' && transition.indexOf('|') > -1) { + transition = transition.split('|'); + } + + if (transition instanceof Array) { + transition = transition[Math.round(Math.random() * (transition.length - 1))]; + } + + var $new = options['new']; + var $old = options['old'] ? options['old'] : $([]); + + switch (transition.toString().toLowerCase()) { + + default: + case 'fade': + $new.fadeIn({ + duration: options.duration, + complete: options.complete, + easing: options.easing || undefined + }); + break; + + case 'fadeinout': + case 'fade_in_out': + + var fadeInNew = function () { + $new.fadeIn({ + duration: options.duration / 2, + complete: options.complete, + easing: options.easing || undefined + }); + }; + + if ($old.length) { + $old.fadeOut({ + duration: options.duration / 2, + complete: fadeInNew, + easing: options.easing || undefined + }); + } + else { + fadeInNew(); + } + + break; + + case 'pushleft': + case 'push_left': + case 'pushright': + case 'push_right': + case 'pushup': + case 'push_up': + case 'pushdown': + case 'push_down': + case 'coverleft': + case 'cover_left': + case 'coverright': + case 'cover_right': + case 'coverup': + case 'cover_up': + case 'coverdown': + case 'cover_down': + + var transitionParts = transition.match(/^(cover|push)_?(.*)$/); + + var animProp = transitionParts[2] === 'left' ? 'right' : + transitionParts[2] === 'right' ? 'left' : + transitionParts[2] === 'down' ? 'top' : + transitionParts[2] === 'up' ? 'bottom' : + 'right'; + + var newCssStart = { + 'display': '' + }, newCssAnim = {}; + newCssStart[animProp] = '-100%'; + newCssAnim[animProp] = 0; + + $new + .css(newCssStart) + .animate(newCssAnim, { + duration: options.duration, + complete: function () { + $new.css(animProp, ''); + options.complete.apply(this, arguments); + }, + easing: options.easing || undefined + }); + + if (transitionParts[1] === 'push' && $old.length) { + var oldCssAnim = {}; + oldCssAnim[animProp] = '100%'; + + $old + .animate(oldCssAnim, { + duration: options.duration, + complete: function () { + $old.css('display', 'none'); + }, + easing: options.easing || undefined + }); + } + + break; + } + + }; + + /* PUBLIC METHODS + * ========================= */ + Backstretch.prototype = { + + resize: function () { + try { + + // Check for a better suited image after the resize + var $resTest = this.options.alwaysTestWindowResolution ? $(window) : this.$root; + var newContainerWidth = $resTest.width(); + var newContainerHeight = $resTest.height(); + var changeRatioW = newContainerWidth / (this._lastResizeContainerWidth || 0); + var changeRatioH = newContainerHeight / (this._lastResizeContainerHeight || 0); + var resolutionChangeRatioThreshold = this.options.resolutionChangeRatioThreshold || 0.0; + + // check for big changes in container size + if ((newContainerWidth !== this._lastResizeContainerWidth || + newContainerHeight !== this._lastResizeContainerHeight) && + ((Math.abs(changeRatioW - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioW)) || + (Math.abs(changeRatioH - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioH)))) { + + this._lastResizeContainerWidth = newContainerWidth; + this._lastResizeContainerHeight = newContainerHeight; + + // Big change: rebuild the entire images array + this.images = optimalSizeImages($resTest, this.originalImages); + + // Preload them (they will be automatically inserted on the next cycle) + if (this.options.preload) { + preload(this.images, (this.index + 1) % this.images.length, this.options.preload); + } + + // In case there is no cycle and the new source is different than the current + if (this.images.length === 1 && + this._currentImage.url !== this.images[0].url) { + + // Wait a little an update the image being showed + var that = this; + clearTimeout(that._selectAnotherResolutionTimeout); + that._selectAnotherResolutionTimeout = setTimeout(function () { + that.show(0); + }, this.options.resolutionRefreshRate); + } + } + + var bgCSS = {left: 0, top: 0, right: 'auto', bottom: 'auto'} + + , boxWidth = this.isBody ? this.$root.width() : this.$root.innerWidth() + , boxHeight = this.isBody + ? (window.innerHeight ? window.innerHeight : this.$root.height()) + : this.$root.innerHeight() + + , naturalWidth = this.$itemWrapper.data('width') + , naturalHeight = this.$itemWrapper.data('height') + + , ratio = (naturalWidth / naturalHeight) || 1 + + , alignX = this._currentImage.alignX === undefined ? this.options.alignX : this._currentImage.alignX + , alignY = this._currentImage.alignY === undefined ? this.options.alignY : this._currentImage.alignY + , scale = validScale(this._currentImage.scale || this.options.scale); + + var width, height; + + if (scale === 'fit' || scale === 'fit-smaller') { + width = naturalWidth; + height = naturalHeight; + + if (width > boxWidth || + height > boxHeight || + scale === 'fit-smaller') { + var boxRatio = boxWidth / boxHeight; + if (boxRatio > ratio) { + width = Math.floor(boxHeight * ratio); + height = boxHeight; + } + else if (boxRatio < ratio) { + width = boxWidth; + height = Math.floor(boxWidth / ratio); + } + else { + width = boxWidth; + height = boxHeight; + } + } + } + else if (scale === 'fill') { + width = boxWidth; + height = boxHeight; + } + else { // 'cover' + width = Math.max(boxHeight * ratio, boxWidth); + height = Math.max(width / ratio, boxHeight); + } + + // Make adjustments based on image ratio + bgCSS.top = -(height - boxHeight) * alignY; + bgCSS.left = -(width - boxWidth) * alignX; + bgCSS.width = width; + bgCSS.height = height; + + if (!this.options.bypassCss) { + + this.$wrap + .css({width: boxWidth, height: boxHeight}) + .find('>.backstretch-item').not('.deleteable') + .each(function () { + var $wrapper = $(this); + $wrapper.find('img,video,iframe') + .css(bgCSS); + }); + } + + var evt = $.Event('backstretch.resize', { + relatedTarget: this.$container[0] + }); + this.$container.trigger(evt, this); + + } + catch (err) { + // IE7 seems to trigger resize before the image is loaded. + // This try/catch block is a hack to let it fail gracefully. + } + + return this; + } + + // Show the slide at a certain position + , show: function (newIndex, overrideOptions) { + + // Validate index + if (Math.abs(newIndex) > this.images.length - 1) { + return; + } + + // Vars + var that = this + , $oldItemWrapper = that.$wrap.find('>.backstretch-item').addClass('deleteable') + , oldVideoWrapper = that.videoWrapper + , evtOptions = {relatedTarget: that.$container[0]}; + + // Trigger the "before" event + that.$container.trigger($.Event('backstretch.before', evtOptions), [that, newIndex]); + + // Set the new frame index + this.index = newIndex; + var selectedImage = that.images[newIndex]; + + // Pause the slideshow + clearTimeout(that._cycleTimeout); + + // New image + + delete that.videoWrapper; // Current item may not be a video + + var isVideo = isVideoSource(selectedImage); + if (isVideo) { + that.videoWrapper = new VideoWrapper(selectedImage); + that.$item = that.videoWrapper.$video.css('pointer-events', 'none'); + } + else { + that.$item = $(''); + } + + that.$itemWrapper = $('
') + .append(that.$item); + + if (this.options.bypassCss) { + that.$itemWrapper.css({ + 'display': 'none' + }); + } + else { + that.$itemWrapper.css(styles.itemWrapper); + that.$item.css(styles.item); + } + + that.$item.bind(isVideo ? 'canplay' : 'load', function (e) { + var $this = $(this) + , $wrapper = $this.parent() + , options = $wrapper.data('options'); + + if (overrideOptions) { + options = $.extend({}, options, overrideOptions); + } + + var imgWidth = this.naturalWidth || this.videoWidth || this.width + , imgHeight = this.naturalHeight || this.videoHeight || this.height; + + // Save the natural dimensions + $wrapper + .data('width', imgWidth) + .data('height', imgHeight); + + var getOption = function (opt) { + return options[opt] !== undefined ? + options[opt] : + that.options[opt]; + }; + + var transition = getOption('transition'); + var transitionEasing = getOption('transitionEasing'); + var transitionDuration = getOption('transitionDuration'); + + // Show the image, then delete the old one + var bringInNextImage = function () { + + if (oldVideoWrapper) { + oldVideoWrapper.stop(); + oldVideoWrapper.destroy(); + } + + $oldItemWrapper.remove(); + + // Resume the slideshow + if (!that.paused && that.images.length > 1) { + that.cycle(); + } + + // Now we can clear the background on the element, to spare memory + if (!that.options.bypassCss && !that.isBody) { + that.$container.css('background-image', 'none'); + } + + // Trigger the "after" and "show" events + // "show" is being deprecated + $(['after', 'show']).each(function () { + that.$container.trigger($.Event('backstretch.' + this, evtOptions), [that, newIndex]); + }); + + if (isVideo) { + that.videoWrapper.play(); + } + }; + + if ((that.firstShow && !that.options.animateFirst) || !transitionDuration || !transition) { + // Avoid transition on first show or if there's no transitionDuration value + $wrapper.show(); + bringInNextImage(); + } + else { + + performTransition({ + 'new': $wrapper, + old: $oldItemWrapper, + transition: transition, + duration: transitionDuration, + easing: transitionEasing, + complete: bringInNextImage + }); + + } + + that.firstShow = false; + + // Resize + that.resize(); + }); + + that.$itemWrapper.appendTo(that.$wrap); + + that.$item.attr('alt', selectedImage.alt || ''); + that.$itemWrapper.data('options', selectedImage); + + if (!isVideo) { + that.$item.attr('src', selectedImage.url); + } + + that._currentImage = selectedImage; + + return that; + } + + , current: function () { + return this.index; + } + + , next: function () { + var args = Array.prototype.slice.call(arguments, 0); + args.unshift(this.index < this.images.length - 1 ? this.index + 1 : 0); + return this.show.apply(this, args); + } + + , prev: function () { + var args = Array.prototype.slice.call(arguments, 0); + args.unshift(this.index === 0 ? this.images.length - 1 : this.index - 1); + return this.show.apply(this, args); + } + + , pause: function () { + // Pause the slideshow + this.paused = true; + + if (this.videoWrapper) { + this.videoWrapper.pause(); + } + + return this; + } + + , resume: function () { + // Resume the slideshow + this.paused = false; + + if (this.videoWrapper) { + this.videoWrapper.play(); + } + + this.cycle(); + return this; + } + + , cycle: function () { + // Start/resume the slideshow + if (this.images.length > 1) { + // Clear the timeout, just in case + clearTimeout(this._cycleTimeout); + + var duration = (this._currentImage && this._currentImage.duration) || this.options.duration; + var isVideo = isVideoSource(this._currentImage); + + var callNext = function () { + this.$item.off('.cycle'); + + // Check for paused slideshow + if (!this.paused) { + this.next(); + } + }; + + // Special video handling + if (isVideo) { + + // Leave video at last frame + if (!this._currentImage.loop) { + var lastFrameTimeout = 0; + + this.$item + .on('playing.cycle', function () { + var player = $(this).data('player'); + + clearTimeout(lastFrameTimeout); + lastFrameTimeout = setTimeout(function () { + player.pause(); + player.$video.trigger('ended'); + }, (player.getDuration() - player.getCurrentTime()) * 1000); + }) + .on('ended.cycle', function () { + clearTimeout(lastFrameTimeout); + }); + } + + // On error go to next + this.$item.on('error.cycle initerror.cycle', $.proxy(callNext, this)); + } + + if (isVideo && !this._currentImage.duration) { + // It's a video - playing until end + this.$item.on('ended.cycle', $.proxy(callNext, this)); + + } + else { + // Cycling according to specified duration + this._cycleTimeout = setTimeout($.proxy(callNext, this), duration); + } + + } + return this; + } + + , destroy: function (preserveBackground) { + // Stop the resize events + $(window).off('resize.backstretch orientationchange.backstretch'); + + // Stop any videos + if (this.videoWrapper) { + this.videoWrapper.destroy(); + } + + // Clear the timeout + clearTimeout(this._cycleTimeout); + + // Remove Backstretch + if (!preserveBackground) { + this.$wrap.remove(); + } + this.$container.removeData('backstretch'); + } + }; + + /** + * Video Abstraction Layer + * + * Static methods: + * > VideoWrapper.loadYoutubeAPI() -> Call in order to load the Youtube API. + * An 'youtube_api_load' event will be triggered on $(window) when the API is loaded. + * + * Generic: + * > player.type -> type of the video + * > player.video / player.$video -> contains the element holding the video + * > player.play() -> plays the video + * > player.pause() -> pauses the video + * > player.setCurrentTime(position) -> seeks to a position by seconds + * + * Youtube: + * > player.ytId will contain the youtube ID if the source is a youtube url + * > player.ytReady is a flag telling whether the youtube source is ready for playback + * */ + + var VideoWrapper = function () { this.init.apply(this, arguments); }; + + /** + * @param {Object} options + * @param {String|Array|Array<{{src: String, type: String?}}>} options.url + * @param {Boolean} options.loop=false + * @param {Boolean?} options.mute=true + * @param {String?} options.poster + * loop, mute, poster + */ + VideoWrapper.prototype.init = function (options) { + + var that = this; + + var $video; + + var setVideoElement = function () { + that.$video = $video; + that.video = $video[0]; + }; + + // Determine video type + + var videoType = 'video'; + + if (!(options.url instanceof Array) && + YOUTUBE_REGEXP.test(options.url)) { + videoType = 'youtube'; + } + + that.type = videoType; + + if (videoType === 'youtube') { + + // Try to load the API in the meantime + VideoWrapper.loadYoutubeAPI(); + + that.ytId = options.url.match(YOUTUBE_REGEXP)[2]; + var src = 'https://www.youtube.com/embed/' + that.ytId + + '?rel=0&autoplay=0&showinfo=0&controls=0&modestbranding=1' + + '&cc_load_policy=0&disablekb=1&iv_load_policy=3&loop=0' + + '&enablejsapi=1&origin=' + encodeURIComponent(window.location.origin); + + that.__ytStartMuted = !!options.mute || options.mute === undefined; + + $video = $('