diff --git a/components/ionKeyboard/ionInputFocus.js b/components/ionKeyboard/ionInputFocus.js new file mode 100644 index 0000000..12ea2fb --- /dev/null +++ b/components/ionKeyboard/ionInputFocus.js @@ -0,0 +1,64 @@ +Meteor.startup(function() { + if (Meteor.isCordova) { + + var getScrollContainer = function($element) { + return $($element.parents('.content.overflow-scroll').get(0)); + } + + var focusPadding = 20; + var isBehindKeyboard = function($focused, keyboardHeight) { + var keyboardTop = $(window).innerHeight() - keyboardHeight; + var focusedBottom = $focused.offset().top + $focused.innerHeight(); + var focusedIsBehindKeyboard = focusedBottom > keyboardTop - focusPadding; + return focusedIsBehindKeyboard; + } + + var getScrollToPosition = function($focused, $container, keyboardHeight) { + + var scrollTo = $container.scrollTop() + $focused.offset().top - $container.offset().top - focusPadding; + return scrollTo; + + } + + // Scroll to make input on top of the page + // #TODO Correct behavior should be: if the input is behind the keyboard, scroll to make it visible on top of the keyboard + scrollToFocusedElement = function($focused, keyboardHeight) { + $focused = $focused || $(':focus'); + var $container = getScrollContainer($focused); + if (!$focused.length || !$container.length) return; + var focusedIsBehindKeyboard = isBehindKeyboard($focused, keyboardHeight); + if (!focusedIsBehindKeyboard) return; + var scrollTo = getScrollToPosition($focused, $container, keyboardHeight); + setTimeout(function() { + $container.animate({ scrollTop: scrollTo }, { + duration: 400, + complete: function() { + // Fix floating input cursor bug (https://github.com/twbs/bootstrap/issues/14708, https://github.com/cubiq/iscroll/issues/178) + var display = $focused.css('display'); + $focused.css({ display: 'none' }).css({ display: display }); + } + }); + }, 0); + } + + var $scrollContainer, scrollPosStart, scrollPosEnd, scrollDistance, scrollHappened, scrollThreshold = 10; + + // Trigger focus on input through touchend for long taps + $(document).on('touchstart', function(event) { + $scrollContainer = getScrollContainer($(event.target)); + scrollPosStart = $scrollContainer.scrollTop(); + }); + + $(document).on('touchend', function(event) { + $scrollContainer = getScrollContainer($(event.target)); + scrollPosEnd = $scrollContainer.scrollTop(); + scrollDistance = Math.abs(scrollPosStart - scrollPosEnd); + scrollHappened = scrollDistance > scrollThreshold; + var $target = $(event.target); + var isInput = _.contains(['INPUT', 'TEXTAREA'], event.target.tagName); + var isFocused = $target.is(':focus'); + if (isInput && !isFocused && !scrollHappened) $target.focus(); + }); + + } +}); diff --git a/components/ionKeyboard/ionKeyboard.js b/components/ionKeyboard/ionKeyboard.js index 9de86ea..4e65130 100644 --- a/components/ionKeyboard/ionKeyboard.js +++ b/components/ionKeyboard/ionKeyboard.js @@ -64,34 +64,19 @@ window.addEventListener('native.keyboardshow', function (event) { $(el).css({bottom: keyboardHeight}); }); -}); + // Scroll to the focused element + scrollToFocusedElement(null, keyboardHeight); -Meteor.startup(function() { - if (Meteor.isCordova) { - - // Scroll to make input on top of the page - // #TODO Correct behavior should be: if the input is behind the keyboard, scroll to make it visible on top of the keyboard - $(document).delegate('input, textarea', 'touchstart, focus', function(event) { - var $input = $(event.currentTarget); - var $container = $($(event.currentTarget).parents('.content.overflow-scroll').get(0)); - var contentOffset = $container.offset().top; - var padding = 10; - var scrollTo = $container.scrollTop() + $input.offset().top - contentOffset - padding; - setTimeout(function() { - $container.scrollTop(scrollTo); - }, 0); - }); - - } }); window.addEventListener('native.keyboardhide', function (event) { + // TODO: Android is having problems if (Platform.isAndroid()) { return; } - $('input, textarea').blur(); + // $('input, textarea').blur(); $('body').removeClass('keyboard-open'); // Detach any elements that were attached @@ -103,4 +88,5 @@ window.addEventListener('native.keyboardhide', function (event) { $('.content.overflow-scroll').each(function (index, el) { $(el).css({bottom: $(el).data('ionkeyboard.bottom')}); }); + }); diff --git a/components/ionLoading/ionLoading.js b/components/ionLoading/ionLoading.js index c9d2306..0f4740f 100644 --- a/components/ionLoading/ionLoading.js +++ b/components/ionLoading/ionLoading.js @@ -7,7 +7,7 @@ IonLoading = { customTemplate: null, backdrop: false }, userOptions); - + if (options.backdrop) { IonBackdrop.retain(); $('.backdrop').addClass('backdrop-loading'); @@ -42,7 +42,10 @@ IonLoading = { $loadingEl.removeClass('visible'); Blaze.remove(this.view); this.view = null; - }.bind(this), 400); + }.bind(this), 0); } + Meteor.setTimeout(function() { + $('.loading-container').remove(); + }, 0) } }; diff --git a/components/ionModal/ionModal.js b/components/ionModal/ionModal.js index 5a10c23..083bb4d 100644 --- a/components/ionModal/ionModal.js +++ b/components/ionModal/ionModal.js @@ -7,62 +7,79 @@ IonModal = { leaveActiveClass: 'ng-leave-active', view: {}, views: [], - open: function (templateName, data) { - this.template = Template[templateName]; - - var view = Blaze.renderWithData(this.template, data, $('body').get(0)); - - if (!this.view[templateName]) { - this.view[templateName] = [view]; - } else { - this.view[templateName].push(view); - } - - this.views.push(templateName); - - var $modalBackdrop = $(view.firstNode()); - - if (this.views.length === 1) { - $modalBackdrop.addClass('active'); - } - - var $modalEl = $modalBackdrop.find('.modal').eq(0); - $modalEl.addClass(this.enterClasses.join(' ')); - - $modalEl.on(this.transitionEndEvent, function () { - $('body').addClass('modal-open'); - $modalEl.removeClass(this.enterClasses.join(' ')).removeClass(this.enterActiveClass).off('webkitAnimationEnd'); - }.bind(this)); Meteor.setTimeout(function () { - $modalEl.addClass(this.enterActiveClass); - }.bind(this), 10); + + this.template = Template[templateName]; + this.views.push(templateName); + if (!this.view[templateName]) this.view[templateName] = []; + + var view = Blaze.renderWithData(this.template, data, $('.ionic-body').get(0)); + this.view[templateName].push(view); + + var $modalBackdrop = $(view.firstNode()); + var $modal = $('.modal', $modalBackdrop); + + if (this.views.length === 1) { + $modalBackdrop.addClass('active'); + } + + $modal.addClass(this.enterClasses.join(' ')); + Meteor.setTimeout(function () { + $modal.addClass(this.enterActiveClass); + }.bind(this), 50); + + }.bind(this), 0); + }, + close: function (templateName) { - close: function () { - var templateName = this.views.pop(); - var view = this.view[templateName].pop(); - var $modalBackdrop = $(view.firstNode()); + this.templateClosed = templateName; + Meteor.setTimeout(function () { - if (!this.views.length) { - $modalBackdrop.removeClass('active'); - } + var templateName = this.templateClosed || this.views.slice(-1)[0]; + delete this.templateClosed; - var $modalEl = $modalBackdrop.find('.modal').eq(0); - $modalEl.addClass(this.leaveClasses.join(' ')); + var view = (this.view[templateName] || []).slice(-1)[0]; + if (!view) return; - Meteor.setTimeout(function() { - $modalEl.addClass(this.leaveActiveClass); - }.bind(this), 10); + var $modalBackdrop = $(view.firstNode()); + var $modal = $('.modal', $modalBackdrop); + + $modal.addClass(this.leaveClasses.join(' ')); + Meteor.setTimeout(function () { + $modal.addClass(this.leaveActiveClass); + }.bind(this), 50); + + $modalBackdrop.fadeOut(500, function() { + $('body').removeClass('modal-open'); + }); + + }.bind(this), 0); - $modalEl.on(this.transitionEndEvent, function () { - $('body').removeClass('modal-open'); - Blaze.remove(view); - }.bind(this)); } }; +$(document).delegate('.modal', IonModal.transitionEndEvent, function(e) { + var $modal = $(e.currentTarget); + if ($modal.hasClass(IonModal.enterClasses.join(' ')) || $modal.hasClass(IonModal.enterActiveClasse)) { + $modal.removeClass(IonModal.enterClasses.join(' ')).removeClass(IonModal.enterActiveClass); + $('body').addClass('modal-open'); + } else if ($modal.hasClass(IonModal.leaveClasses.join(' ')) || $modal.hasClass(IonModal.leaveActiveClasse)) { + var firstChild = $modal.children().first(); + var templateName = getElementModalTemplateName(firstChild); + IonModal.views = _.without(IonModal.views, templateName); + var view = IonModal.view[templateName].pop(); + var $modalBackdrop = $(view.firstNode()); + $modalBackdrop.removeClass('active'); + $modal.removeClass(IonModal.leaveClasses.join(' ')).removeClass(IonModal.leaveActiveClass).off(IonModal.transitionEndEvent); + $('body').removeClass('modal-open'); + $(e.target).parents('.modal-backdrop').remove(); + Blaze.remove(view); + } +}); + Template.ionModal.created = function () { this.data = this.data || {}; this.customTemplate = this.data.customTemplate || false; @@ -75,10 +92,10 @@ Template.ionModal.created = function () { Template.ionModal.rendered = function () { if (this.focusFirstInput) { Meteor.setTimeout(function () { + if (!this._domrange) return; this.$('input:first').focus(); }.bind(this), 600); } - $(window).on('keyup.ionModal', function(event) { event.stopImmediatePropagation(); if (event.which == 27) { @@ -119,11 +136,7 @@ Template.ionModal.helpers({ }, animation: function () { - if (this.animation) { - return this.animation; - } else { - return 'slide-in-up'; - } + return this.animation || 'slide-in-up'; }, customTemplate: function () { @@ -139,6 +152,7 @@ Template.ionModal.helpers({ return classes.join(' '); } + }); Template.ionModal.events({ @@ -148,8 +162,16 @@ Template.ionModal.events({ IonModal.close(); } }, - 'click [data-dismiss=modal]': function (event, template) { - IonModal.close(); + var tplName = getElementModalTemplateName(event.currentTarget); + IonModal.close(tplName); } }); + +var getElementModalTemplateName = function(element) { + var modal = $(element).parents('.modal').get(0); + var modalView = Blaze.getView(modal); + var tplView = Meteor._get(modalView, 'parentView', 'parentView'); // Twice because the parent view is a #with block + var tplName = tplView.name.slice('Template.'.length, tplView.name.length); + return tplName; +}; diff --git a/components/ionPopup/ionPopup.js b/components/ionPopup/ionPopup.js index 504a0cb..4f44cbd 100644 --- a/components/ionPopup/ionPopup.js +++ b/components/ionPopup/ionPopup.js @@ -121,12 +121,12 @@ IonPopup = { }, close: function () { - var $backdrop = $(this.view.firstNode()); - var $popup = $backdrop.find('.popup-container'); + var $popup = this._domrange ? $(this.view.firstNode()).find('.popup-container') : $('.popup-container'); $popup.addClass('popup-hidden').removeClass('active'); setTimeout(function () { $('body').removeClass('popup-open'); + $('.backdrop').remove(); Blaze.remove(this.view); }.bind(this), 100); }, diff --git a/components/ionSlideBox/ionSlideBox.js b/components/ionSlideBox/ionSlideBox.js index a8323eb..7b7ca82 100644 --- a/components/ionSlideBox/ionSlideBox.js +++ b/components/ionSlideBox/ionSlideBox.js @@ -20,12 +20,12 @@ Template.ionSlideBox.rendered = function () { return ''; } }); - this.$('.ion-slide-box').on('afterChange', function (event, slick, currentSlide) { $(this).trigger({type: 'onSlideChanged', index: currentSlide}); }); }; Template.ionSlideBox.destroyed = function () { - this.$('.ion-slide-box').slick('unslick'); + var $slideBox = this.$('.ion-slide-box'); + if ($slideBox.hasClass('slick-initialized')) $slideBox.slick('unslick'); }; diff --git a/package.js b/package.js index 50c91fa..05a27be 100644 --- a/package.js +++ b/package.js @@ -11,7 +11,15 @@ Cordova.depends({ Package.onUse(function(api) { api.versionsFrom("1.0"); - api.use(["templating", "underscore", "fastclick", "iron:router@1.0.0", "tracker", "session"], "client"); + api.use([ + "templating", + "underscore", + "fastclick", + "iron:router@1.0.0", + "tracker", + "session", + "jquery" + ], "client"); api.addFiles([ "vendor/snap.js", @@ -46,6 +54,7 @@ Package.onUse(function(api) { "components/ionItem/ionItem.js", "components/ionKeyboard/ionKeyboard.js", + "components/ionKeyboard/ionInputFocus.js", "components/ionList/ionList.html", "components/ionList/ionList.js",