From 78580d7be12ff9088ccd7bbfc1b84aa4c654499a Mon Sep 17 00:00:00 2001
From: Peter Jaszkowiak
Date: Fri, 10 Nov 2017 19:35:55 -0700
Subject: [PATCH 1/6] Move chats to a separate slideout menu on the right side
- Switched to the Pulling library
- Moved nav menu to the left side, as is normal elsewhere
- Added chat button on right side
- Unread icon on it shows unread chats
- Added slideout menu on right for only chats
- Both sides will slide open and everything
- Switched to drawer menus instead of the previous reveal style menus
---
less/header.less | 12 +-
less/mobile.less | 110 +++++---
less/style.less | 2 +-
lib/modules/slideout.min.js | 484 ------------------------------------
lib/persona.js | 209 +++++++++++-----
package.json | 3 +-
plugin.json | 4 +-
templates/header.tpl | 9 +-
templates/partials/menu.tpl | 6 +-
9 files changed, 243 insertions(+), 596 deletions(-)
delete mode 100644 lib/modules/slideout.min.js
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 @@
-