diff --git a/less/header.less b/less/header.less index 73be6a5..cfada5a 100644 --- a/less/header.less +++ b/less/header.less @@ -1,4 +1,4 @@ -#menu, .header { +.header, .slideout-menu { .notification-list { overflow-x: hidden; overflow-y: auto; @@ -298,8 +298,8 @@ .unread-count:after { content: attr(data-content); position: absolute; - left: 23px; - top: 10px; + left: 23px; + top: 10px; font-size: 10px; text-align: center; border: 1px solid #890405; @@ -314,10 +314,10 @@ font-family: @font-family-sans-serif; } -#mobile-menu [component="notifications/icon"].unread-count:after, .slideout-menu .unread-count:after { +.slideout-menu .unread-count:after { position: relative; - left: -6px; - top: -7px; + left: -6px; + top: -7px; } #search-form { diff --git a/less/mobile.less b/less/mobile.less index 3c75d9c..74b3025 100644 --- a/less/mobile.less +++ b/less/mobile.less @@ -1,15 +1,7 @@ .slideout-menu { - position: fixed; - left: auto; - top: 0; - bottom: 0; - right: 0; - z-index: 0; - width: 256px; overflow-y: auto; -webkit-overflow-scrolling: touch; - display: none; } /* iPhone 5 and other such devices */ @@ -58,9 +50,31 @@ -webkit-overflow-scrolling: touch; } - #menu { + .navbar-toggle { + line-height: 10px; + + &.pull-left { + margin-left: @navbar-padding-horizontal; + margin-right: 0; + } + + .header & .notification-icon { + left: auto; + top: -3px; + right: -8px; + &.unread-count::after { + position: static; + } + } + } + + #menu { padding-top: 100px; + } + + .slideout-menu { + z-index: 10000 !important; background-color: #1D1F20; background-image: linear-gradient(145deg, #1D1F20, #404348); @@ -128,25 +142,27 @@ } } - .menu-section .chat-list, .menu-section .notification-list-mobile { - .user-link { - display: inline; - } - .unread { - background-color: inherit; - - a:after { - content: "new"; - text-transform: uppercase; - color: #FFF; - margin-left: 5px; - font-size: 10px; - background: #C91106; - border: 1px solid #890405; - padding: 2px 3px; - border-radius: 5px; + .menu-section { + .chat-list, .notification-list-mobile { + .user-link { + display: inline; + } + .unread { + background-color: inherit; } } + .chat-list .unread .room-name::after, + .notification-list-mobile .unread a::after { + content: "new"; + text-transform: uppercase; + color: #FFF; + margin-left: 5px; + font-size: 10px; + background: #C91106; + border: 1px solid #890405; + padding: 2px 3px; + border-radius: 5px; + } } .counter { @@ -204,11 +220,6 @@ } } - .slideout-panel { - position: relative; - z-index: 1; - } - .slideout-open, .slideout-open body, .slideout-open .slideout-panel { @@ -216,13 +227,42 @@ overflow-y: hidden !important; } - .slideout-open .slideout-menu { + @keyframes fade { + from { + opacity: 0; + } + to { + opacity: .3; + } + } + .subnav-is-opened .main-nav__secondary-nav { display: block; + animation: fade 250ms ease-in-out both; } - .slideout-open { - overflow-y: hidden; - height: 100%; + .slideout-panel { + &::after { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 1040; + content: ' '; + background: rgba(0, 0, 0, 0.300); + + opacity: 0; + visibility: hidden; + transition-property: opacity, visibility; + transition-duration: 200ms, 0s; + transition-delay: 0s, 100ms; + transition-timing-function: ease-in-out; + } + + .slideout-open &::after { + opacity: 1; + visibility: visible; + } } .menu-profile { diff --git a/less/style.less b/less/style.less index b068961..7093d12 100644 --- a/less/style.less +++ b/less/style.less @@ -19,7 +19,7 @@ body { } @media (max-width: @screen-xs-max) { - #panel.slideout-panel { + .slideout-panel { min-height: 100vh; } } diff --git a/lib/modules/slideout.min.js b/lib/modules/slideout.min.js deleted file mode 100644 index bc1d17b..0000000 --- a/lib/modules/slideout.min.js +++ /dev/null @@ -1,484 +0,0 @@ -!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.Slideout=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o self._tolerance) ? self.open() : self.close(); - } - self._moved = false; - }; - this.panel.addEventListener(touch.end, this._onTouchEndFn); - - /** - * Translates panel on touchmove - */ - this._onTouchMoveFn = function(eve) { - self.emit('touchmove', eve.target); - if (scrolling || self._preventOpen || typeof eve.touches === 'undefined') { return; } - - var dif_x = eve.touches[0].clientX - self._startOffsetX; - var translateX = self._currentOffsetX = dif_x; - - if (Math.abs(translateX) > self._padding) { return; } - - if (Math.abs(dif_x) > 20) { - self._opening = true; - - var oriented_dif_x = dif_x * self._orientation; - if (self._opened && oriented_dif_x > 0 || !self._opened && oriented_dif_x < 0) { return; } - if (oriented_dif_x <= 0) { - translateX = dif_x + self._padding * self._orientation; - self._opening = false; - } - - if (!self._moved && html.className.search('slideout-open') === -1) { - html.className += ' slideout-open'; - } - - self.panel.style[prefix + 'transform'] = self.panel.style.transform = 'translate3d(' + translateX + 'px, 0, 0)'; - self.emit('translate', translateX, eve.target); - self._moved = true; - } - - }; - this.panel.addEventListener(touch.move, this._onTouchMoveFn); -}; - -Slideout.prototype.enableTouch = function() { - this._touch = true; - return this; -}; - -Slideout.prototype.disableTouch = function() { - this._touch = false; - return this; -}; - -Slideout.prototype.destroy = function() { - // Close before clean - this.close(); - - // Remove event listeners - doc.removeEventListener(touch.move, this._preventMove); - this.panel.removeEventListener(touch.start, this._resetTouchFn); - this.panel.removeEventListener('touchcancel', this._onTouchCancelFn); - this.panel.removeEventListener(touch.end, this._onTouchEndFn); - this.panel.removeEventListener(touch.move, this._onTouchMoveFn); - doc.removeEventListener('scroll', this._onScrollFn); - - // Remove methods - this.open = this.close = function() {}; - - // Return the instance so it can be easily dereferenced - return this; -}; - -/** - * Expose Slideout - */ -module.exports = Slideout; - -},{"decouple":2,"emitter":3}],2:[function(require,module,exports){ -'use strict'; - -var requestAnimFrame = (function() { - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - function (callback) { - window.setTimeout(callback, 1000 / 60); - }; -}()); - -function decouple(node, event, fn) { - var eve, - tracking = false; - - function captureEvent(e) { - eve = e; - track(); - } - - function track() { - if (!tracking) { - requestAnimFrame(update); - tracking = true; - } - } - - function update() { - fn.call(node, eve); - tracking = false; - } - - node.addEventListener(event, captureEvent, false); - return captureEvent; -} - -/** - * Expose decouple - */ -module.exports = decouple; - -},{}],3:[function(require,module,exports){ -"use strict"; - -var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - -exports.__esModule = true; -/** - * Creates a new instance of Emitter. - * @class - * @returns {Object} Returns a new instance of Emitter. - * @example - * // Creates a new instance of Emitter. - * var Emitter = require('emitter'); - * - * var emitter = new Emitter(); - */ - -var Emitter = (function () { - function Emitter() { - _classCallCheck(this, Emitter); - } - - /** - * Adds a listener to the collection for the specified event. - * @memberof! Emitter.prototype - * @function - * @param {String} event - The event name. - * @param {Function} listener - A listener function to add. - * @returns {Object} Returns an instance of Emitter. - * @example - * // Add an event listener to "foo" event. - * emitter.on('foo', listener); - */ - - Emitter.prototype.on = function on(event, listener) { - // Use the current collection or create it. - this._eventCollection = this._eventCollection || {}; - - // Use the current collection of an event or create it. - this._eventCollection[event] = this._eventCollection[event] || []; - - // Appends the listener into the collection of the given event - this._eventCollection[event].push(listener); - - return this; - }; - - /** - * Adds a listener to the collection for the specified event that will be called only once. - * @memberof! Emitter.prototype - * @function - * @param {String} event - The event name. - * @param {Function} listener - A listener function to add. - * @returns {Object} Returns an instance of Emitter. - * @example - * // Will add an event handler to "foo" event once. - * emitter.once('foo', listener); - */ - - Emitter.prototype.once = function once(event, listener) { - var self = this; - - function fn() { - self.off(event, fn); - listener.apply(this, arguments); - } - - fn.listener = listener; - - this.on(event, fn); - - return this; - }; - - /** - * Removes a listener from the collection for the specified event. - * @memberof! Emitter.prototype - * @function - * @param {String} event - The event name. - * @param {Function} listener - A listener function to remove. - * @returns {Object} Returns an instance of Emitter. - * @example - * // Remove a given listener. - * emitter.off('foo', listener); - */ - - Emitter.prototype.off = function off(event, listener) { - - var listeners = undefined; - - // Defines listeners value. - if (!this._eventCollection || !(listeners = this._eventCollection[event])) { - return this; - } - - listeners.forEach(function (fn, i) { - if (fn === listener || fn.listener === listener) { - // Removes the given listener. - listeners.splice(i, 1); - } - }); - - // Removes an empty event collection. - if (listeners.length === 0) { - delete this._eventCollection[event]; - } - - return this; - }; - - /** - * Execute each item in the listener collection in order with the specified data. - * @memberof! Emitter.prototype - * @function - * @param {String} event - The name of the event you want to emit. - * @param {...Object} data - Data to pass to the listeners. - * @returns {Object} Returns an instance of Emitter. - * @example - * // Emits the "foo" event with 'param1' and 'param2' as arguments. - * emitter.emit('foo', 'param1', 'param2'); - */ - - Emitter.prototype.emit = function emit(event) { - var _this = this; - - for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; - } - - var listeners = undefined; - - // Defines listeners value. - if (!this._eventCollection || !(listeners = this._eventCollection[event])) { - return this; - } - - // Clone listeners - listeners = listeners.slice(0); - - listeners.forEach(function (fn) { - return fn.apply(_this, args); - }); - - return this; - }; - - return Emitter; -})(); - -/** - * Exports Emitter - */ -exports["default"] = Emitter; -module.exports = exports["default"]; -},{}]},{},[1])(1) -}); \ No newline at end of file diff --git a/lib/persona.js b/lib/persona.js index dae7ead..ac22536 100644 --- a/lib/persona.js +++ b/lib/persona.js @@ -1,6 +1,6 @@ "use strict"; -/*globals ajaxify, config, utils, app, socket, Slideout, NProgress*/ +/*globals ajaxify, config, utils, app, socket, NProgress*/ $(document).ready(function() { var env = utils.findBootstrapEnvironment(); @@ -113,87 +113,168 @@ $(document).ready(function() { return; } - $('#menu').removeClass('hidden'); - var slideout = new Slideout({ - 'panel': document.getElementById('panel'), - 'menu': document.getElementById('menu'), - 'padding': 256, - 'tolerance': 70, - 'side': 'right' - }); + require(['pulling', 'storage'], function (Pulling, Storage) { + // initialization - if (env !== 'xs') { - slideout.disableTouch(); - } + var guest = !app.user || !parseInt(app.user.uid, 10); + var legacy = !!Storage.getItem('persona:menus:legacy-layout'); + var margin = window.innerWidth * 0.4; - $('#mobile-menu').on('click', function() { - slideout.toggle(); - }); + if (legacy) { + $('#mobile-menu').removeClass('pull-left'); + $('#mobile-chats').addClass('pull-left'); + } - $('#menu a').on('click', function() { - slideout.close(); - }); + var navSlideout = Pulling.create({ + panel: document.getElementById('panel'), + menu: document.getElementById('menu'), + width: 256, + margin: margin, + side: legacy ? 'right' : 'left', + }); + $('#menu').removeClass('hidden'); + + var chatsSlideout; + if (!guest) { + chatsSlideout = Pulling.create({ + panel: document.getElementById('panel'), + menu: document.getElementById('chats-menu'), + width: 256, + margin: margin, + side: legacy ? 'left' : 'right', + }); + $('#chats-menu').removeClass('hidden'); + } - $(window).on('resize action:ajaxify.start', function() { - slideout.close(); - $('.account .cover').css('top', $('[component="navbar"]').height()); - }); + // all menus - function openingMenuAndLoad() { - openingMenu(); - loadNotificationsAndChat(); - } + function closeOnClick() { + navSlideout.close(); + if (chatsSlideout) { chatsSlideout.close(); } + } - function openingMenu() { - $('#header-menu').css({ - 'top': $(window).scrollTop() + 'px', - 'position': 'absolute' + function onBeforeOpen() { + document.documentElement.classList.add('slideout-open'); + } + + function onClose() { + document.documentElement.classList.remove('slideout-open'); + $('#panel').off('click', closeOnClick); + } + + $(window).on('resize action:ajaxify.start', function () { + navSlideout.close(); + chatsSlideout.close(); + $('.account .cover').css('top', $('[component="navbar"]').height()); }); - } - function loadNotificationsAndChat() { - require(['chat', 'notifications'], function(chat, notifications) { - chat.loadChatsDropdown($('#menu [data-section="chats"] ul')); - notifications.loadNotifications($('#menu [data-section="notifications"] ul')); + navSlideout + .ignore('code, code *, .preventSlideout, .preventSlideout *') + .on('closed', onClose) + .on('beforeopen', onBeforeOpen) + .on('opened', function () { + $('#panel').one('click', closeOnClick); + }); + + if (!guest) { + chatsSlideout + .ignore('code, code *, .preventSlideout, .preventSlideout *') + .on('closed', onClose) + .on('beforeopen', onBeforeOpen) + .on('opened', function () { + $('#panel').one('click', closeOnClick); + }); + } + + // left slideout navigation menu + + $('#mobile-menu').on('click', function () { + navSlideout.enable().toggle(); }); - } - slideout.on('open', openingMenuAndLoad); - slideout.on('touchmove', function(target) { - var $target = $(target); - if ($target.length && ($target.is('table') || $target.parents('table').length || $target.is('code') || $target.parents('code').length || $target.hasClass('preventSlideOut') || $target.parents('.preventSlideOut').length)) { - slideout._preventOpen = true; + function loadNotifications() { + require(['notifications'], function(notifications) { + notifications.loadNotifications($('#menu [data-section="notifications"] ul')); + }); } - }); - slideout.on('close', function() { - $('#header-menu').css({ - 'top': '0px', - 'position': 'fixed' + navSlideout.on('opened', loadNotifications); + + if (!guest) { + navSlideout.on('beforeopen', function () { + chatsSlideout.close(); + chatsSlideout.disable(); + }).on('closed', function () { + chatsSlideout.enable(); + }); + } + + $('#menu [data-section="navigation"] ul').html($('#main-nav').html() + ($('#search-menu').html() || '') + ($('#logged-out-menu').html() || '')); + + $('#user-control-list').children().clone(true, true).appendTo($('#menu [data-section="profile"] ul')); + $('#menu [data-section="profile"] ul').find('[component="user/status"]').remove(); + + socket.on('event:user_status_change', function(data) { + if (parseInt(data.uid, 10) === app.user.uid) { + app.updateUserStatus($('#menu [component="user/status"]'), data.status); + navSlideout.close(); + } }); - $('.slideout-open').removeClass('slideout-open'); - $('.topic .pagination-block').css({ bottom: 0 }); - }); - slideout.on('beforeopen', function() { - var paginator = $('.topic .pagination-block')[0]; - if (paginator) { - paginator.style.bottom = 0; // to trigger reflow - paginator.style.bottom = (paginator.getBoundingClientRect().bottom - window.innerHeight).toString() + 'px'; + // right slideout chats menu + + function loadChats() { + require(['chat'], function (chat) { + chat.loadChatsDropdown($('#chats-menu .chat-list')); + }); } - }); - $('#menu [data-section="navigation"] ul').html($('#main-nav').html() + ($('#search-menu').html() || '') + ($('#logged-out-menu').html() || '')); + if (!guest) { + $('#mobile-chats').removeClass('hidden').on('click', function() { + navSlideout.close(); + chatsSlideout.enable().toggle(); + }); + $('#chats-menu').on('click', 'li[data-roomid]', function () { + chatsSlideout.close(); + }); - $('#user-control-list').children().clone(true, true).appendTo($('#menu [data-section="profile"] ul')); - $('#menu [data-section="profile"] ul').find('[component="user/status"]').remove(); + chatsSlideout + .on('opened', loadChats) + .on('beforeopen', function () { + navSlideout.close().disable(); + }) + .on('closed', function () { + navSlideout.enable(); + }); + } - socket.on('event:user_status_change', function(data) { - if (parseInt(data.uid, 10) === app.user.uid) { - app.updateUserStatus($('#menu [component="user/status"]'), data.status); - slideout.close(); + // for debugging + // window.navSlideout = navSlideout; + // window.chatsSlideout = chatsSlideout; + + + // add a checkbox in the user settings page + // so users can swap the sides the menus appear on + + function setupSetting() { + if (ajaxify.data.template['account/settings'] && !document.getElementById('persona:menus:legacy-layout')) { + $('
') + .appendTo('#content .account > .row') + .find('input') + .prop('checked', Storage.getItem('persona:menus:legacy-layout', 'true')) + .change(function (e) { + if (e.target.checked) { + Storage.setItem('persona:menus:legacy-layout', 'true'); + } else { + Storage.removeItem('persona:menus:legacy-layout'); + } + }); + } } + + $(window).on('action:ajaxify.end', setupSetting); + setupSetting(); }); } @@ -299,12 +380,12 @@ $(document).ready(function() { drop.css({top: y + 'px', left: x + 'px'}).addClass('animate'); }); } - + function setupQuickReply() { $(window).on('action:ajaxify.end', function(ev, data) { if (data.url && data.url.match('^topic/')) { require(['persona/quickreply'], function(quickreply) { - quickreply.init(); + quickreply.init(); }); } }); diff --git a/package.json b/package.json index bf4a9b0..158ee4e 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "url": "https://github.com/psychobunny/nodebb-theme-persona/issues" }, "dependencies": { - "striptags": "^3.1.0" + "striptags": "^3.1.0", + "pulling": "^1.1.0" } } diff --git a/plugin.json b/plugin.json index 808a9d2..d643683 100644 --- a/plugin.json +++ b/plugin.json @@ -14,9 +14,11 @@ "lib/persona.js", "lib/modules/nprogress.js", "lib/modules/autohidingnavbar.min.js", - "lib/modules/slideout.min.js", "lib/modules/quickreply.js" ], + "modules": { + "pulling.js": "node_modules/pulling/build/pulling-drawer.js" + }, "acpScripts": [ "lib/admin.js" ] diff --git a/templates/header.tpl b/templates/header.tpl index 0a6bbfc..6ff4cfd 100644 --- a/templates/header.tpl +++ b/templates/header.tpl @@ -25,7 +25,7 @@ -