From fd769f08dc73a79ffa066707525337e08041cb93 Mon Sep 17 00:00:00 2001 From: "stephen@cubedesigners.com" Date: Tue, 7 Jan 2020 18:10:37 +0000 Subject: [PATCH] Remove FastClick, replace unsupported dropdown library and fix PHP 7.4 warnings and errors. Done #3300 @8 --- framework/application/Bootstrap.php | 4 +- framework/application/Cubedesigners/Util.php | 4 +- .../application/layouts/scripts/layout.phtml | 2 +- .../views/helpers/CasestudiesDetail.php | 4 +- .../views/scripts/templates/casestudies.phtml | 2 +- .../views/scripts/templates/expertise.phtml | 1 - js/casestudies.js | 11 +- js/selectric.js | 1127 +++++++++++++++++ less/casestudies.less | 13 + less/contact.less | 6 +- less/selectric.less | 175 +++ 11 files changed, 1336 insertions(+), 13 deletions(-) create mode 100644 js/selectric.js create mode 100644 less/selectric.less diff --git a/framework/application/Bootstrap.php b/framework/application/Bootstrap.php index 631f0cf..855cb36 100644 --- a/framework/application/Bootstrap.php +++ b/framework/application/Bootstrap.php @@ -11,8 +11,8 @@ class Bootstrap extends CubeIT_Bootstrap { $this->bootstrap('scripts'); } - protected function _initRouter($initCms = true) { - $router = parent::_initRouter($initCms); + protected function _initRouter($initCms = true, $standard = true) { + $router = parent::_initRouter($initCms, $standard); $router->addStandardRoute('rss'); return $router; } diff --git a/framework/application/Cubedesigners/Util.php b/framework/application/Cubedesigners/Util.php index 7526c2b..9cde303 100644 --- a/framework/application/Cubedesigners/Util.php +++ b/framework/application/Cubedesigners/Util.php @@ -6,9 +6,11 @@ class Cubedesigners_Util { public static function getCategoryById($id, $locale) { self::_getCategories(); - if (!isset(self::$_categories[$id])) { + + if (is_array($id) || !isset(self::$_categories[$id])) { return; } + $res = CubeIT_Util_Cms::unserialize(self::$_categories[$id]->getName(), $locale); return $res; } diff --git a/framework/application/layouts/scripts/layout.phtml b/framework/application/layouts/scripts/layout.phtml index 465c950..bcb4cd3 100644 --- a/framework/application/layouts/scripts/layout.phtml +++ b/framework/application/layouts/scripts/layout.phtml @@ -16,7 +16,7 @@ $fonts = [ //$fonts = array('custom' => array('families' => array('Roboto Condensed'), 'urls' => array('/css/fonts/robotocondensed.css'))); $this->headScript()->addWebFont($fonts); -$this->headScript()->addFastclick(); +//$this->headScript()->addFastclick(); // Removed because it was causing errors on Android and is probably no longer needed for modern devices $this->headScript()->addWOW(); // For reveal on scroll animations if ($this->acl()->isAllowed('edition')) { diff --git a/framework/application/views/helpers/CasestudiesDetail.php b/framework/application/views/helpers/CasestudiesDetail.php index 8e1a790..cb4e917 100644 --- a/framework/application/views/helpers/CasestudiesDetail.php +++ b/framework/application/views/helpers/CasestudiesDetail.php @@ -62,7 +62,7 @@ class Cubedesigners_View_Helper_CasestudiesDetail extends CubeIT_View_Helper_Abs // fb($bloc); $margin = ''; - if ($bloc->margin != '') { + if (isset($bloc->margin) && $bloc->margin != '') { // Calculate margin as a percentage for responsive design // Note: margin is based on max-width of images (1200px) because this is how CSS handles % margins, even for margin-top $variableTopMargin = $bloc->margin / 1200 * 100; @@ -70,7 +70,7 @@ class Cubedesigners_View_Helper_CasestudiesDetail extends CubeIT_View_Helper_Abs } $zindex = ''; - if ($bloc->zindex != 'default') { + if (isset($bloc->zindex) && $bloc->zindex != 'default') { $zindex = 'z-index:' . $bloc->zindex . ';'; } diff --git a/framework/application/views/scripts/templates/casestudies.phtml b/framework/application/views/scripts/templates/casestudies.phtml index 46bf1b6..53b9fe2 100644 --- a/framework/application/views/scripts/templates/casestudies.phtml +++ b/framework/application/views/scripts/templates/casestudies.phtml @@ -1,5 +1,5 @@ headScript()->addScriptAndStyle('fancy-select'); +$this->headScript()->addScriptAndStyle('selectric'); $this->headScript()->addScriptAndStyle('casestudies'); ?> diff --git a/framework/application/views/scripts/templates/expertise.phtml b/framework/application/views/scripts/templates/expertise.phtml index 244221c..75b5583 100644 --- a/framework/application/views/scripts/templates/expertise.phtml +++ b/framework/application/views/scripts/templates/expertise.phtml @@ -1,7 +1,6 @@ headScript()->addScriptAndStyle('expertises'); -$this->headScript()->addScriptAndStyle('fancy-select'); echo $this->twocols(); if(!empty($this->citation)) { diff --git a/js/casestudies.js b/js/casestudies.js index 6123d26..21d4a69 100644 --- a/js/casestudies.js +++ b/js/casestudies.js @@ -1,9 +1,14 @@ TO_LOAD_ONCE[TO_LOAD_ONCE.length] = 'load_casestudies();'; function load_casestudies() { - $('#casestudies-list-filter').fancySelect().on('change.fs', function() { - $(this).trigger('change.$'); - }); // trigger the DOM's change event when changing FancySelect + $('#casestudies-list-filter').selectric({ + maxHeight: 450, + arrowButtonMarkup: '', // Disable dropdown element since we are using an SVG in the background + disableOnMobile: true, + onChange: function(element) { + $(element).change(); // Trigger change on select box so isotope will update (see isotope-select.js) + }, + }); } diff --git a/js/selectric.js b/js/selectric.js new file mode 100644 index 0000000..73ec143 --- /dev/null +++ b/js/selectric.js @@ -0,0 +1,1127 @@ +/*! + * ,/ + * ,'/ + * ,' / + * ,' /_____, + * .'____ ,' + * / ,' + * / ,' + * /,' + * /' + * + * Selectric ϟ v1.13.0 (Aug 22 2017) - http://lcdsantos.github.io/jQuery-Selectric/ + * + * Copyright (c) 2017 Leonardo Santos; MIT License + * + */ + +(function(factory) { + /* global define */ + /* istanbul ignore next */ + if ( typeof define === 'function' && define.amd ) { + define(['jquery'], factory); + } else if ( typeof module === 'object' && module.exports ) { + // Node/CommonJS + module.exports = function( root, jQuery ) { + if ( jQuery === undefined ) { + if ( typeof window !== 'undefined' ) { + jQuery = require('jquery'); + } else { + jQuery = require('jquery')(root); + } + } + factory(jQuery); + return jQuery; + }; + } else { + // Browser globals + factory(jQuery); + } +}(function($) { + 'use strict'; + + var $doc = $(document); + var $win = $(window); + + var pluginName = 'selectric'; + var classList = 'Input Items Open Disabled TempShow HideSelect Wrapper Focus Hover Responsive Above Below Scroll Group GroupLabel'; + var eventNamespaceSuffix = '.sl'; + + var chars = ['a', 'e', 'i', 'o', 'u', 'n', 'c', 'y']; + var diacritics = [ + /[\xE0-\xE5]/g, // a + /[\xE8-\xEB]/g, // e + /[\xEC-\xEF]/g, // i + /[\xF2-\xF6]/g, // o + /[\xF9-\xFC]/g, // u + /[\xF1]/g, // n + /[\xE7]/g, // c + /[\xFD-\xFF]/g // y + ]; + + /** + * Create an instance of Selectric + * + * @constructor + * @param {Node} element - The <select> element + * @param {object} opts - Options + */ + var Selectric = function(element, opts) { + var _this = this; + + _this.element = element; + _this.$element = $(element); + + _this.state = { + multiple : !!_this.$element.attr('multiple'), + enabled : false, + opened : false, + currValue : -1, + selectedIdx : -1, + highlightedIdx : -1 + }; + + _this.eventTriggers = { + open : _this.open, + close : _this.close, + destroy : _this.destroy, + refresh : _this.refresh, + init : _this.init + }; + + _this.init(opts); + }; + + Selectric.prototype = { + utils: { + /** + * Detect mobile browser + * + * @return {boolean} + */ + isMobile: function() { + return /android|ip(hone|od|ad)/i.test(navigator.userAgent); + }, + + /** + * Escape especial characters in string (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) + * + * @param {string} str - The string to be escaped + * @return {string} The string with the special characters escaped + */ + escapeRegExp: function(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + }, + + /** + * Replace diacritics + * + * @param {string} str - The string to replace the diacritics + * @return {string} The string with diacritics replaced with ascii characters + */ + replaceDiacritics: function(str) { + var k = diacritics.length; + + while (k--) { + str = str.toLowerCase().replace(diacritics[k], chars[k]); + } + + return str; + }, + + /** + * Format string + * https://gist.github.com/atesgoral/984375 + * + * @param {string} f - String to be formated + * @return {string} String formated + */ + format: function(f) { + var a = arguments; // store outer arguments + return ('' + f) // force format specifier to String + .replace( // replace tokens in format specifier + /\{(?:(\d+)|(\w+))\}/g, // match {token} references + function( + s, // the matched string (ignored) + i, // an argument index + p // a property name + ) { + return p && a[1] // if property name and first argument exist + ? a[1][p] // return property from first argument + : a[i]; // assume argument index and return i-th argument + }); + }, + + /** + * Get the next enabled item in the options list. + * + * @param {object} selectItems - The options object. + * @param {number} selected - Index of the currently selected option. + * @return {object} The next enabled item. + */ + nextEnabledItem: function(selectItems, selected) { + while ( selectItems[ selected = (selected + 1) % selectItems.length ].disabled ) { + // empty + } + return selected; + }, + + /** + * Get the previous enabled item in the options list. + * + * @param {object} selectItems - The options object. + * @param {number} selected - Index of the currently selected option. + * @return {object} The previous enabled item. + */ + previousEnabledItem: function(selectItems, selected) { + while ( selectItems[ selected = (selected > 0 ? selected : selectItems.length) - 1 ].disabled ) { + // empty + } + return selected; + }, + + /** + * Transform camelCase string to dash-case. + * + * @param {string} str - The camelCased string. + * @return {string} The string transformed to dash-case. + */ + toDash: function(str) { + return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); + }, + + /** + * Calls the events registered with function name. + * + * @param {string} fn - The name of the function. + * @param {number} scope - Scope that should be set on the function. + */ + triggerCallback: function(fn, scope) { + var elm = scope.element; + var func = scope.options['on' + fn]; + var args = [elm].concat([].slice.call(arguments).slice(1)); + + if ( $.isFunction(func) ) { + func.apply(elm, args); + } + + $(elm).trigger(pluginName + '-' + this.toDash(fn), args); + }, + + /** + * Transform array list to concatenated string and remove empty values + * @param {array} arr - Class list + * @return {string} Concatenated string + */ + arrayToClassname: function(arr) { + var newArr = $.grep(arr, function(item) { + return !!item; + }); + + return $.trim(newArr.join(' ')); + } + }, + + /** Initializes */ + init: function(opts) { + var _this = this; + + // Set options + _this.options = $.extend(true, {}, $.fn[pluginName].defaults, _this.options, opts); + + _this.utils.triggerCallback('BeforeInit', _this); + + // Preserve data + _this.destroy(true); + + // Disable on mobile browsers + if ( _this.options.disableOnMobile && _this.utils.isMobile() ) { + _this.disableOnMobile = true; + return; + } + + // Get classes + _this.classes = _this.getClassNames(); + + // Create elements + var input = $('', { 'class': _this.classes.input, 'readonly': _this.utils.isMobile() }); + var items = $('
', { 'class': _this.classes.items, 'tabindex': -1 }); + var itemsScroll = $('
', { 'class': _this.classes.scroll }); + var wrapper = $('
', { 'class': _this.classes.prefix, 'html': _this.options.arrowButtonMarkup }); + var label = $('', { 'class': 'label' }); + var outerWrapper = _this.$element.wrap('
').parent().append(wrapper.prepend(label), items, input); + var hideSelectWrapper = $('
', { 'class': _this.classes.hideselect }); + + _this.elements = { + input : input, + items : items, + itemsScroll : itemsScroll, + wrapper : wrapper, + label : label, + outerWrapper : outerWrapper + }; + + if ( _this.options.nativeOnMobile && _this.utils.isMobile() ) { + _this.elements.input = undefined; + hideSelectWrapper.addClass(_this.classes.prefix + '-is-native'); + + _this.$element.on('change', function() { + _this.refresh(); + }); + } + + _this.$element + .on(_this.eventTriggers) + .wrap(hideSelectWrapper); + + _this.originalTabindex = _this.$element.prop('tabindex'); + _this.$element.prop('tabindex', -1); + + _this.populate(); + _this.activate(); + + _this.utils.triggerCallback('Init', _this); + }, + + /** Activates the plugin */ + activate: function() { + var _this = this; + var hiddenChildren = _this.elements.items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow); + var originalWidth = _this.$element.width(); + + hiddenChildren.removeClass(_this.classes.tempshow); + + _this.utils.triggerCallback('BeforeActivate', _this); + + _this.elements.outerWrapper.prop('class', + _this.utils.arrayToClassname([ + _this.classes.wrapper, + _this.$element.prop('class').replace(/\S+/g, _this.classes.prefix + '-$&'), + _this.options.responsive ? _this.classes.responsive : '' + ]) + ); + + if ( _this.options.inheritOriginalWidth && originalWidth > 0 ) { + _this.elements.outerWrapper.width(originalWidth); + } + + _this.unbindEvents(); + + if ( !_this.$element.prop('disabled') ) { + _this.state.enabled = true; + + // Not disabled, so... Removing disabled class + _this.elements.outerWrapper.removeClass(_this.classes.disabled); + + // Remove styles from items box + // Fix incorrect height when refreshed is triggered with fewer options + _this.$li = _this.elements.items.removeAttr('style').find('li'); + + _this.bindEvents(); + } else { + _this.elements.outerWrapper.addClass(_this.classes.disabled); + + if ( _this.elements.input ) { + _this.elements.input.prop('disabled', true); + } + } + + _this.utils.triggerCallback('Activate', _this); + }, + + /** + * Generate classNames for elements + * + * @return {object} Classes object + */ + getClassNames: function() { + var _this = this; + var customClass = _this.options.customClass; + var classesObj = {}; + + $.each(classList.split(' '), function(i, currClass) { + var c = customClass.prefix + currClass; + classesObj[currClass.toLowerCase()] = customClass.camelCase ? c : _this.utils.toDash(c); + }); + + classesObj.prefix = customClass.prefix; + + return classesObj; + }, + + /** Set the label text */ + setLabel: function() { + var _this = this; + var labelBuilder = _this.options.labelBuilder; + + if ( _this.state.multiple ) { + // Make sure currentValues is an array + var currentValues = $.isArray(_this.state.currValue) ? _this.state.currValue : [_this.state.currValue]; + // I'm not happy with this, but currentValues can be an empty + // array and we need to fallback to the default option. + currentValues = currentValues.length === 0 ? [0] : currentValues; + + var labelMarkup = $.map(currentValues, function(value) { + return $.grep(_this.lookupItems, function(item) { + return item.index === value; + })[0]; // we don't want nested arrays here + }); + + labelMarkup = $.grep(labelMarkup, function(item) { + // Hide default (please choose) if more then one element were selected. + // If no option value were given value is set to option text by default + if ( labelMarkup.length > 1 || labelMarkup.length === 0 ) { + return $.trim(item.value) !== ''; + } + return item; + }); + + labelMarkup = $.map(labelMarkup, function(item) { + return $.isFunction(labelBuilder) + ? labelBuilder(item) + : _this.utils.format(labelBuilder, item); + }); + + // Limit the amount of selected values shown in label + if ( _this.options.multiple.maxLabelEntries ) { + if ( labelMarkup.length >= _this.options.multiple.maxLabelEntries + 1 ) { + labelMarkup = labelMarkup.slice(0, _this.options.multiple.maxLabelEntries); + labelMarkup.push( + $.isFunction(labelBuilder) + ? labelBuilder({ text: '...' }) + : _this.utils.format(labelBuilder, { text: '...' })); + } else { + labelMarkup.slice(labelMarkup.length - 1); + } + } + _this.elements.label.html(labelMarkup.join(_this.options.multiple.separator)); + + } else { + var currItem = _this.lookupItems[_this.state.currValue]; + + _this.elements.label.html( + $.isFunction(labelBuilder) + ? labelBuilder(currItem) + : _this.utils.format(labelBuilder, currItem) + ); + } + }, + + /** Get and save the available options */ + populate: function() { + var _this = this; + var $options = _this.$element.children(); + var $justOptions = _this.$element.find('option'); + var $selected = $justOptions.filter(':selected'); + var selectedIndex = $justOptions.index($selected); + var currIndex = 0; + var emptyValue = (_this.state.multiple ? [] : 0); + + if ( $selected.length > 1 && _this.state.multiple ) { + selectedIndex = []; + $selected.each(function() { + selectedIndex.push($(this).index()); + }); + } + + _this.state.currValue = (~selectedIndex ? selectedIndex : emptyValue); + _this.state.selectedIdx = _this.state.currValue; + _this.state.highlightedIdx = _this.state.currValue; + _this.items = []; + _this.lookupItems = []; + + if ( $options.length ) { + // Build options markup + $options.each(function(i) { + var $elm = $(this); + + if ( $elm.is('optgroup') ) { + + var optionsGroup = { + element : $elm, + label : $elm.prop('label'), + groupDisabled : $elm.prop('disabled'), + items : [] + }; + + $elm.children().each(function(i) { + var $elm = $(this); + + optionsGroup.items[i] = _this.getItemData(currIndex, $elm, optionsGroup.groupDisabled || $elm.prop('disabled')); + + _this.lookupItems[currIndex] = optionsGroup.items[i]; + + currIndex++; + }); + + _this.items[i] = optionsGroup; + + } else { + + _this.items[i] = _this.getItemData(currIndex, $elm, $elm.prop('disabled')); + + _this.lookupItems[currIndex] = _this.items[i]; + + currIndex++; + + } + }); + + _this.setLabel(); + _this.elements.items.append( _this.elements.itemsScroll.html( _this.getItemsMarkup(_this.items) ) ); + } + }, + + /** + * Generate items object data + * @param {integer} index - Current item index + * @param {node} $elm - Current element node + * @param {boolean} isDisabled - Current element disabled state + * @return {object} Item object + */ + getItemData: function(index, $elm, isDisabled) { + var _this = this; + + return { + index : index, + element : $elm, + value : $elm.val(), + className : $elm.prop('class'), + text : $elm.html(), + slug : $.trim(_this.utils.replaceDiacritics($elm.html())), + alt : $elm.attr('data-alt'), + selected : $elm.prop('selected'), + disabled : isDisabled + }; + }, + + /** + * Generate options markup + * + * @param {object} items - Object containing all available options + * @return {string} HTML for the options box + */ + getItemsMarkup: function(items) { + var _this = this; + var markup = '
    '; + + if ( $.isFunction(_this.options.listBuilder) && _this.options.listBuilder ) { + items = _this.options.listBuilder(items); + } + + $.each(items, function(i, elm) { + if ( elm.label !== undefined ) { + + markup += _this.utils.format('
    • {3}
    • ', + _this.utils.arrayToClassname([ + _this.classes.group, + elm.groupDisabled ? 'disabled' : '', + elm.element.prop('class') + ]), + _this.classes.grouplabel, + elm.element.prop('label') + ); + + $.each(elm.items, function(i, elm) { + markup += _this.getItemMarkup(elm.index, elm); + }); + + markup += '
    '; + + } else { + + markup += _this.getItemMarkup(elm.index, elm); + + } + }); + + return markup + '
'; + }, + + /** + * Generate every option markup + * + * @param {number} index - Index of current item + * @param {object} itemData - Current item + * @return {string} HTML for the option + */ + getItemMarkup: function(index, itemData) { + var _this = this; + var itemBuilder = _this.options.optionsItemBuilder; + // limit access to item data to provide a simple interface + // to most relevant options. + var filteredItemData = { + value: itemData.value, + text : itemData.text, + slug : itemData.slug, + index: itemData.index + }; + + return _this.utils.format('
  • {3}
  • ', + index, + _this.utils.arrayToClassname([ + itemData.className, + index === _this.items.length - 1 ? 'last' : '', + itemData.disabled ? 'disabled' : '', + itemData.selected ? 'selected' : '' + ]), + $.isFunction(itemBuilder) + ? _this.utils.format(itemBuilder(itemData, this.$element, index), itemData) + : _this.utils.format(itemBuilder, filteredItemData) + ); + }, + + /** Remove events on the elements */ + unbindEvents: function() { + var _this = this; + + _this.elements.wrapper + .add(_this.$element) + .add(_this.elements.outerWrapper) + .add(_this.elements.input) + .off(eventNamespaceSuffix); + }, + + /** Bind events on the elements */ + bindEvents: function() { + var _this = this; + + _this.elements.outerWrapper.on('mouseenter' + eventNamespaceSuffix + ' mouseleave' + eventNamespaceSuffix, function(e) { + $(this).toggleClass(_this.classes.hover, e.type === 'mouseenter'); + + // Delay close effect when openOnHover is true + if ( _this.options.openOnHover ) { + clearTimeout(_this.closeTimer); + + if ( e.type === 'mouseleave' ) { + _this.closeTimer = setTimeout($.proxy(_this.close, _this), _this.options.hoverIntentTimeout); + } else { + _this.open(); + } + } + }); + + // Toggle open/close + _this.elements.wrapper.on('click' + eventNamespaceSuffix, function(e) { + _this.state.opened ? _this.close() : _this.open(e); + }); + + // Translate original element focus event to dummy input. + // Disabled on mobile devices because the default option list isn't + // shown due the fact that hidden input gets focused + if ( !(_this.options.nativeOnMobile && _this.utils.isMobile()) ) { + _this.$element.on('focus' + eventNamespaceSuffix, function() { + _this.elements.input.focus(); + }); + + _this.elements.input + .prop({ tabindex: _this.originalTabindex, disabled: false }) + .on('keydown' + eventNamespaceSuffix, $.proxy(_this.handleKeys, _this)) + .on('focusin' + eventNamespaceSuffix, function(e) { + _this.elements.outerWrapper.addClass(_this.classes.focus); + + // Prevent the flicker when focusing out and back again in the browser window + _this.elements.input.one('blur', function() { + _this.elements.input.blur(); + }); + + if ( _this.options.openOnFocus && !_this.state.opened ) { + _this.open(e); + } + }) + .on('focusout' + eventNamespaceSuffix, function() { + _this.elements.outerWrapper.removeClass(_this.classes.focus); + }) + .on('input propertychange', function() { + var val = _this.elements.input.val(); + var searchRegExp = new RegExp('^' + _this.utils.escapeRegExp(val), 'i'); + + // Clear search + clearTimeout(_this.resetStr); + _this.resetStr = setTimeout(function() { + _this.elements.input.val(''); + }, _this.options.keySearchTimeout); + + if ( val.length ) { + // Search in select options + $.each(_this.items, function(i, elm) { + if (elm.disabled) { + return; + } + if (searchRegExp.test(elm.text) || searchRegExp.test(elm.slug)) { + _this.highlight(i); + return; + } + if (!elm.alt) { + return; + } + var altItems = elm.alt.split('|'); + for (var ai = 0; ai < altItems.length; ai++) { + if (!altItems[ai]) { + break; + } + if (searchRegExp.test(altItems[ai].trim())) { + _this.highlight(i); + return; + } + } + }); + } + }); + } + + _this.$li.on({ + // Prevent blur on Chrome + mousedown: function(e) { + e.preventDefault(); + e.stopPropagation(); + }, + click: function() { + _this.select($(this).data('index')); + + // Chrome doesn't close options box if select is wrapped with a label + // We need to 'return false' to avoid that + return false; + } + }); + }, + + /** + * Behavior when keyboard keys is pressed + * + * @param {object} e - Event object + */ + handleKeys: function(e) { + var _this = this; + var key = e.which; + var keys = _this.options.keys; + + var isPrevKey = $.inArray(key, keys.previous) > -1; + var isNextKey = $.inArray(key, keys.next) > -1; + var isSelectKey = $.inArray(key, keys.select) > -1; + var isOpenKey = $.inArray(key, keys.open) > -1; + var idx = _this.state.highlightedIdx; + var isFirstOrLastItem = (isPrevKey && idx === 0) || (isNextKey && (idx + 1) === _this.items.length); + var goToItem = 0; + + // Enter / Space + if ( key === 13 || key === 32 ) { + e.preventDefault(); + } + + // If it's a directional key + if ( isPrevKey || isNextKey ) { + if ( !_this.options.allowWrap && isFirstOrLastItem ) { + return; + } + + if ( isPrevKey ) { + goToItem = _this.utils.previousEnabledItem(_this.lookupItems, idx); + } + + if ( isNextKey ) { + goToItem = _this.utils.nextEnabledItem(_this.lookupItems, idx); + } + + _this.highlight(goToItem); + } + + // Tab / Enter / ESC + if ( isSelectKey && _this.state.opened ) { + _this.select(idx); + + if ( !_this.state.multiple || !_this.options.multiple.keepMenuOpen ) { + _this.close(); + } + + return; + } + + // Space / Enter / Left / Up / Right / Down + if ( isOpenKey && !_this.state.opened ) { + _this.open(); + } + }, + + /** Update the items object */ + refresh: function() { + var _this = this; + + _this.populate(); + _this.activate(); + _this.utils.triggerCallback('Refresh', _this); + }, + + /** Set options box width/height */ + setOptionsDimensions: function() { + var _this = this; + + // Calculate options box height + // Set a temporary class on the hidden parent of the element + var hiddenChildren = _this.elements.items.closest(':visible').children(':hidden').addClass(_this.classes.tempshow); + var maxHeight = _this.options.maxHeight; + var itemsWidth = _this.elements.items.outerWidth(); + var wrapperWidth = _this.elements.wrapper.outerWidth() - (itemsWidth - _this.elements.items.width()); + + // Set the dimensions, minimum is wrapper width, expand for long items if option is true + if ( !_this.options.expandToItemText || wrapperWidth > itemsWidth ) { + _this.finalWidth = wrapperWidth; + } else { + // Make sure the scrollbar width is included + _this.elements.items.css('overflow', 'scroll'); + + // Set a really long width for _this.elements.outerWrapper + _this.elements.outerWrapper.width(9e4); + _this.finalWidth = _this.elements.items.width(); + // Set scroll bar to auto + _this.elements.items.css('overflow', ''); + _this.elements.outerWrapper.width(''); + } + + _this.elements.items.width(_this.finalWidth).height() > maxHeight && _this.elements.items.height(maxHeight); + + // Remove the temporary class + hiddenChildren.removeClass(_this.classes.tempshow); + }, + + /** Detect if the options box is inside the window */ + isInViewport: function() { + var _this = this; + + if (_this.options.forceRenderAbove === true) { + _this.elements.outerWrapper.addClass(_this.classes.above); + } else if (_this.options.forceRenderBelow === true) { + _this.elements.outerWrapper.addClass(_this.classes.below); + } else { + var scrollTop = $win.scrollTop(); + var winHeight = $win.height(); + var uiPosX = _this.elements.outerWrapper.offset().top; + var uiHeight = _this.elements.outerWrapper.outerHeight(); + + var fitsDown = (uiPosX + uiHeight + _this.itemsHeight) <= (scrollTop + winHeight); + var fitsAbove = (uiPosX - _this.itemsHeight) > scrollTop; + + // If it does not fit below, only render it + // above it fit's there. + // It's acceptable that the user needs to + // scroll the viewport to see the cut off UI + var renderAbove = !fitsDown && fitsAbove; + var renderBelow = !renderAbove; + + _this.elements.outerWrapper.toggleClass(_this.classes.above, renderAbove); + _this.elements.outerWrapper.toggleClass(_this.classes.below, renderBelow); + } + }, + + /** + * Detect if currently selected option is visible and scroll the options box to show it + * + * @param {Number|Array} index - Index of the selected items + */ + detectItemVisibility: function(index) { + var _this = this; + var $filteredLi = _this.$li.filter('[data-index]'); + + if ( _this.state.multiple ) { + // If index is an array, we can assume a multiple select and we + // want to scroll to the uppermost selected item! + // Math.min.apply(Math, index) returns the lowest entry in an Array. + index = ($.isArray(index) && index.length === 0) ? 0 : index; + index = $.isArray(index) ? Math.min.apply(Math, index) : index; + } + + var liHeight = $filteredLi.eq(index).outerHeight(); + var liTop = $filteredLi[index].offsetTop; + var itemsScrollTop = _this.elements.itemsScroll.scrollTop(); + var scrollT = liTop + liHeight * 2; + + _this.elements.itemsScroll.scrollTop( + scrollT > itemsScrollTop + _this.itemsHeight ? scrollT - _this.itemsHeight : + liTop - liHeight < itemsScrollTop ? liTop - liHeight : + itemsScrollTop + ); + }, + + /** + * Open the select options box + * + * @param {Event} e - Event + */ + open: function(e) { + var _this = this; + + if ( _this.options.nativeOnMobile && _this.utils.isMobile()) { + return false; + } + + _this.utils.triggerCallback('BeforeOpen', _this); + + if ( e ) { + e.preventDefault(); + if (_this.options.stopPropagation) { + e.stopPropagation(); + } + } + + if ( _this.state.enabled ) { + _this.setOptionsDimensions(); + + // Find any other opened instances of select and close it + $('.' + _this.classes.hideselect, '.' + _this.classes.open).children()[pluginName]('close'); + + _this.state.opened = true; + _this.itemsHeight = _this.elements.items.outerHeight(); + _this.itemsInnerHeight = _this.elements.items.height(); + + // Toggle options box visibility + _this.elements.outerWrapper.addClass(_this.classes.open); + + // Give dummy input focus + _this.elements.input.val(''); + if ( e && e.type !== 'focusin' ) { + _this.elements.input.focus(); + } + + // Delayed binds events on Document to make label clicks work + setTimeout(function() { + $doc + .on('click' + eventNamespaceSuffix, $.proxy(_this.close, _this)) + .on('scroll' + eventNamespaceSuffix, $.proxy(_this.isInViewport, _this)); + }, 1); + + _this.isInViewport(); + + // Prevent window scroll when using mouse wheel inside items box + if ( _this.options.preventWindowScroll ) { + /* istanbul ignore next */ + $doc.on('mousewheel' + eventNamespaceSuffix + ' DOMMouseScroll' + eventNamespaceSuffix, '.' + _this.classes.scroll, function(e) { + var orgEvent = e.originalEvent; + var scrollTop = $(this).scrollTop(); + var deltaY = 0; + + if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; } + if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; } + if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; } + if ( 'deltaY' in orgEvent ) { deltaY = orgEvent.deltaY * -1; } + + if ( scrollTop === (this.scrollHeight - _this.itemsInnerHeight) && deltaY < 0 || scrollTop === 0 && deltaY > 0 ) { + e.preventDefault(); + } + }); + } + + _this.detectItemVisibility(_this.state.selectedIdx); + + _this.highlight(_this.state.multiple ? -1 : _this.state.selectedIdx); + + _this.utils.triggerCallback('Open', _this); + } + }, + + /** Close the select options box */ + close: function() { + var _this = this; + + _this.utils.triggerCallback('BeforeClose', _this); + + // Remove custom events on document + $doc.off(eventNamespaceSuffix); + + // Remove visible class to hide options box + _this.elements.outerWrapper.removeClass(_this.classes.open); + + _this.state.opened = false; + + _this.utils.triggerCallback('Close', _this); + }, + + /** Select current option and change the label */ + change: function() { + var _this = this; + + _this.utils.triggerCallback('BeforeChange', _this); + + if ( _this.state.multiple ) { + // Reset old selected + $.each(_this.lookupItems, function(idx) { + _this.lookupItems[idx].selected = false; + _this.$element.find('option').prop('selected', false); + }); + + // Set new selected + $.each(_this.state.selectedIdx, function(idx, value) { + _this.lookupItems[value].selected = true; + _this.$element.find('option').eq(value).prop('selected', true); + }); + + _this.state.currValue = _this.state.selectedIdx; + + _this.setLabel(); + + _this.utils.triggerCallback('Change', _this); + } else if ( _this.state.currValue !== _this.state.selectedIdx ) { + // Apply changed value to original select + _this.$element + .prop('selectedIndex', _this.state.currValue = _this.state.selectedIdx) + .data('value', _this.lookupItems[_this.state.selectedIdx].text); + + // Change label text + _this.setLabel(); + + _this.utils.triggerCallback('Change', _this); + } + }, + + /** + * Highlight option + * @param {number} index - Index of the options that will be highlighted + */ + highlight: function(index) { + var _this = this; + var $filteredLi = _this.$li.filter('[data-index]').removeClass('highlighted'); + + _this.utils.triggerCallback('BeforeHighlight', _this); + + // Parameter index is required and should not be a disabled item + if ( index === undefined || index === -1 || _this.lookupItems[index].disabled ) { + return; + } + + $filteredLi + .eq(_this.state.highlightedIdx = index) + .addClass('highlighted'); + + _this.detectItemVisibility(index); + + _this.utils.triggerCallback('Highlight', _this); + }, + + /** + * Select option + * + * @param {number} index - Index of the option that will be selected + */ + select: function(index) { + var _this = this; + var $filteredLi = _this.$li.filter('[data-index]'); + + _this.utils.triggerCallback('BeforeSelect', _this, index); + + // Parameter index is required and should not be a disabled item + if ( index === undefined || index === -1 || _this.lookupItems[index].disabled ) { + return; + } + + if ( _this.state.multiple ) { + // Make sure selectedIdx is an array + _this.state.selectedIdx = $.isArray(_this.state.selectedIdx) ? _this.state.selectedIdx : [_this.state.selectedIdx]; + + var hasSelectedIndex = $.inArray(index, _this.state.selectedIdx); + if ( hasSelectedIndex !== -1 ) { + _this.state.selectedIdx.splice(hasSelectedIndex, 1); + } else { + _this.state.selectedIdx.push(index); + } + + $filteredLi + .removeClass('selected') + .filter(function(index) { + return $.inArray(index, _this.state.selectedIdx) !== -1; + }) + .addClass('selected'); + } else { + $filteredLi + .removeClass('selected') + .eq(_this.state.selectedIdx = index) + .addClass('selected'); + } + + if ( !_this.state.multiple || !_this.options.multiple.keepMenuOpen ) { + _this.close(); + } + + _this.change(); + + _this.utils.triggerCallback('Select', _this, index); + }, + + /** + * Unbind and remove + * + * @param {boolean} preserveData - Check if the data on the element should be removed too + */ + destroy: function(preserveData) { + var _this = this; + + if ( _this.state && _this.state.enabled ) { + _this.elements.items.add(_this.elements.wrapper).add(_this.elements.input).remove(); + + if ( !preserveData ) { + _this.$element.removeData(pluginName).removeData('value'); + } + + _this.$element.prop('tabindex', _this.originalTabindex).off(eventNamespaceSuffix).off(_this.eventTriggers).unwrap().unwrap(); + + _this.state.enabled = false; + } + } + }; + + // A really lightweight plugin wrapper around the constructor, + // preventing against multiple instantiations + $.fn[pluginName] = function(args) { + return this.each(function() { + var data = $.data(this, pluginName); + + if ( data && !data.disableOnMobile ) { + (typeof args === 'string' && data[args]) ? data[args]() : data.init(args); + } else { + $.data(this, pluginName, new Selectric(this, args)); + } + }); + }; + + /** + * Default plugin options + * + * @type {object} + */ + $.fn[pluginName].defaults = { + onChange : function(elm) { $(elm).change(); }, + maxHeight : 300, + keySearchTimeout : 500, + arrowButtonMarkup : '', + disableOnMobile : false, + nativeOnMobile : true, + openOnFocus : true, + openOnHover : false, + hoverIntentTimeout : 500, + expandToItemText : false, + responsive : false, + preventWindowScroll : true, + inheritOriginalWidth : false, + allowWrap : true, + forceRenderAbove : false, + forceRenderBelow : false, + stopPropagation : true, + optionsItemBuilder : '{text}', // function(itemData, element, index) + labelBuilder : '{text}', // function(currItem) + listBuilder : false, // function(items) + keys : { + previous : [37, 38], // Left / Up + next : [39, 40], // Right / Down + select : [9, 13, 27], // Tab / Enter / Escape + open : [13, 32, 37, 38, 39, 40], // Enter / Space / Left / Up / Right / Down + close : [9, 27] // Tab / Escape + }, + customClass : { + prefix: pluginName, + camelCase: false + }, + multiple : { + separator: ', ', + keepMenuOpen: true, + maxLabelEntries: false + } + }; +})); diff --git a/less/casestudies.less b/less/casestudies.less index 7290523..3445ee3 100644 --- a/less/casestudies.less +++ b/less/casestudies.less @@ -3,6 +3,19 @@ .casestudies-filter { margin-bottom: 2em; text-align: right; + font-size: 20px; + + .selectric-items { + width: auto !important; + } + + select { + // The select box normally isn't visible but it is triggered + // and on iOS, if the font size is too small (below 16px after + // display scaling), the page will be zoomed when the select is focused + // Related: https://stackoverflow.com/questions/2989263/disable-auto-zoom-in-input-text-tag-safari-on-iphone + font-size: 32px; + } } .casestudies-list { diff --git a/less/contact.less b/less/contact.less index 7fd04a6..c014187 100644 --- a/less/contact.less +++ b/less/contact.less @@ -59,10 +59,12 @@ .contact-texte { background-image: url('../images/picto_footer_mail.svg'); background-repeat: no-repeat; - background-position: 0px 10px; + background-position: 0 0; font-weight: 300; padding-left: 60px; - padding-bottom: 50px; + padding-top: 10px; + min-height: 50px; + margin-bottom: 30px; } .titre { diff --git a/less/selectric.less b/less/selectric.less new file mode 100644 index 0000000..cd1bc90 --- /dev/null +++ b/less/selectric.less @@ -0,0 +1,175 @@ +@import "00-constants"; + +.selectric-wrapper { + position: relative; + cursor: pointer; +} + +.selectric-responsive { + width: 100%; +} + +.selectric { + position: relative; + overflow: hidden; + + .label { + display: inline-block; + background: url('data:image/svg+xml;utf8,') center right no-repeat; + background-size: auto 0.4em; + cursor: pointer; + padding: 0 1.5em 0 0.4em; + line-height: 2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + position: relative; + //font-size: 12px; + //line-height: 38px; + color: #444; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + //.button { + // display: none; + //} + + .selectric-focus & { + border-color: #aaa; + } + + .selectric-hover & { + border-color: #c4c4c4; + } + +} + +.selectric-open { + z-index: 9999; + + .selectric { + border-color: #c4c4c4; + } + + .selectric-items { + display: block; + } +} + + +.selectric-hide-select { + position: relative; + overflow: hidden; + //width: 0; + //height: 0; + + &.selectric-is-native { + position: absolute; + width: 100%; + height: 100%; + z-index: 10; + + select { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + width: 100%; + border: none; + z-index: 1; + box-sizing: border-box; + opacity: 0; + } + } + + select { + position: absolute; + left: -100%; + font-size: 20px; + } +} + + +.selectric-input { + position: absolute !important; + top: 0 !important; + left: 0 !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + margin: 0 !important; + padding: 0 !important; + width: 1px !important; + height: 1px !important; + outline: none !important; + border: none !important; + //*font: 0/0 a !important; + background: none !important; +} + +.selectric-temp-show { + position: absolute !important; + visibility: hidden !important; + display: block !important; +} + +/* Items box */ +.selectric-items { + display: none; + position: absolute; + top: 100%; + right: 0; + background: #fff; + z-index: -1; + + .selectric-scroll { + height: 100%; + overflow: auto; + } + + .selectric-above & { + top: auto; + bottom: 100%; + } + + ul, li { + list-style: none; + padding: 0; + margin: 0; + //font-size: 12px; + //line-height: 20px; + //min-height: 20px; + } + + li { + display: block; + padding: 0.5em 2em; + cursor: pointer; + + &.selected, &.selected.highlighted { + background-color: rgba(200,200,200,.35); + + &:hover { + background-color: @yellow; + } + } + + &.highlighted { + background-color: rgba(200,200,200,.15); + } + + &:before { + display: none; + } + + &:hover { + background: @yellow; + color: #fff; + } + } + +} + -- 2.39.5