From 8e8d582bc67c6ec440229a7fa9d4998fc9129ac1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 3 Feb 2016 20:52:25 +0000 Subject: [PATCH 1/7] Updated app requirements & Added some friendlier errors --- app/Exceptions/ConfirmationEmailException.php | 5 +---- app/Exceptions/Handler.php | 16 +++++++++++--- app/Exceptions/ImageUploadException.php | 5 +---- app/Exceptions/LdapException.php | 8 +------ app/Exceptions/PrettyException.php | 5 +++++ app/Exceptions/SocialDriverNotConfigured.php | 4 +--- app/Exceptions/SocialSignInException.php | 5 +---- app/Exceptions/UserRegistrationException.php | 5 +---- app/Services/ImageService.php | 21 ++++++++++++++----- readme.md | 19 +++++++++-------- resources/views/errors/500.blade.php | 11 ++++++++++ 11 files changed, 61 insertions(+), 43 deletions(-) create mode 100644 app/Exceptions/PrettyException.php create mode 100644 resources/views/errors/500.blade.php diff --git a/app/Exceptions/ConfirmationEmailException.php b/app/Exceptions/ConfirmationEmailException.php index f343eff82..8736422c4 100644 --- a/app/Exceptions/ConfirmationEmailException.php +++ b/app/Exceptions/ConfirmationEmailException.php @@ -1,7 +1,4 @@ message); return response()->redirectTo($e->redirectLocation); } + // Handle pretty exceptions which will show a friendly application-fitting page + // Which will include the basic message to point the user roughly to the cause. + if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) { + $message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage(); + return response()->view('errors/500', ['message' => $message], 500); + } + return parent::render($request, $e); } } diff --git a/app/Exceptions/ImageUploadException.php b/app/Exceptions/ImageUploadException.php index 205bdd4ff..6f4c73037 100644 --- a/app/Exceptions/ImageUploadException.php +++ b/app/Exceptions/ImageUploadException.php @@ -1,6 +1,3 @@ getPublicUrl($thumbFilePath); } - // Otherwise create the thumbnail - $thumb = $this->imageTool->make($storage->get($image->path)); + try { + $thumb = $this->imageTool->make($storage->get($image->path)); + } catch (Exception $e) { + if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { + throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.'); + } else { + throw $e; + } + } + if ($keepRatio) { $thumb->resize($width, null, function ($constraint) { $constraint->aspectRatio(); diff --git a/readme.md b/readme.md index b2f909efb..a191e1694 100644 --- a/readme.md +++ b/readme.md @@ -17,19 +17,13 @@ A platform to create documentation/wiki content. General information about BookS ## Requirements -BookStack has similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing. +BookStack has similar requirements to Laravel: * PHP >= 5.5.9, Will need to be usable from the command line. -* OpenSSL PHP Extension -* PDO PHP Extension -* MBstring PHP Extension -* Tokenizer PHP Extension +* PHP Extensions: `OpenSSL`, `PDO`, `MBstring`, `Tokenizer`, `GD` * MySQL >= 5.6 * Git (Not strictly required but helps manage updates) * [Composer](https://getcomposer.org/) -* [Node.js](https://nodejs.org/en/) **Development Only** -* [Gulp](http://gulpjs.com/) **Development Only** - ## Installation @@ -144,7 +138,14 @@ A user in BookStack will be linked to a LDAP user via a 'uid'. If a LDAP user ui You may find that you cannot log in with your initial Admin account after changing the `AUTH_METHOD` to `ldap`. To get around this set the `AUTH_METHOD` to `standard`, login with your admin account then change it back to `ldap`. You get then edit your profile and add your LDAP uid under the 'External Authentication ID' field. You will then be able to login in with that ID. -## Testing +## Development & Testing + +All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements: + +* [Node.js](https://nodejs.org/en/) **Development Only** +* [Gulp](http://gulpjs.com/) **Development Only** + +SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp. BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit installed and accessible via command line. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing. diff --git a/resources/views/errors/500.blade.php b/resources/views/errors/500.blade.php new file mode 100644 index 000000000..47dcb88c7 --- /dev/null +++ b/resources/views/errors/500.blade.php @@ -0,0 +1,11 @@ +@extends('base') + +@section('content') + + +
+

An Error Occurred

+

{{ $message }}

+
+ +@stop \ No newline at end of file From 82967821499fd87fb4cf2c24667b5b856ea9498e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 7 Feb 2016 10:14:11 +0000 Subject: [PATCH 2/7] Updated image controller styling and added preview option The notification system was also updated so it can be used from JavaScript events such as image manager uploads. Closes #25 --- resources/assets/js/controllers.js | 73 +++++++++++++++---- resources/assets/js/directives.js | 2 +- resources/assets/js/global.js | 49 +++++++++++-- resources/assets/js/pages/page-form.js | 7 +- resources/assets/js/services.js | 2 +- resources/assets/sass/_image-manager.scss | 36 ++++++++- .../views/partials/image-manager.blade.php | 23 +++--- .../views/partials/notifications.blade.php | 17 ++--- 8 files changed, 164 insertions(+), 45 deletions(-) diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 5d8990754..76def6abd 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -1,6 +1,6 @@ "use strict"; -module.exports = function (ngApp) { +module.exports = function (ngApp, events) { ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', function ($scope, $attrs, $http, $timeout, imageManagerService) { @@ -17,21 +17,40 @@ module.exports = function (ngApp) { var dataLoaded = false; var callback = false; + /** + * Simple returns the appropriate upload url depending on the image type set. + * @returns {string} + */ $scope.getUploadUrl = function () { return '/images/' + $scope.imageType + '/upload'; }; + /** + * Runs on image upload, Adds an image to local list of images + * and shows a success message to the user. + * @param file + * @param data + */ $scope.uploadSuccess = function (file, data) { $scope.$apply(() => { $scope.images.unshift(data); }); + events.emit('success', 'Image uploaded'); }; + /** + * Runs the callback and hides the image manager. + * @param returnData + */ function callbackAndHide(returnData) { if (callback) callback(returnData); $scope.showing = false; } + /** + * Image select action. Checks if a double-click was fired. + * @param image + */ $scope.imageSelect = function (image) { var dblClickTime = 300; var currentTime = Date.now(); @@ -48,10 +67,19 @@ module.exports = function (ngApp) { previousClickTime = currentTime; }; + /** + * Action that runs when the 'Select image' button is clicked. + * Runs the callback and hides the image manager. + */ $scope.selectButtonClick = function () { callbackAndHide($scope.selectedImage); }; + /** + * Show the image manager. + * Takes a callback to execute later on. + * @param doneCallback + */ function show(doneCallback) { callback = doneCallback; $scope.showing = true; @@ -62,6 +90,8 @@ module.exports = function (ngApp) { } } + // Connects up the image manger so it can be used externally + // such as from TinyMCE. imageManagerService.show = show; imageManagerService.showExternal = function (doneCallback) { $scope.$apply(() => { @@ -70,10 +100,16 @@ module.exports = function (ngApp) { }; window.ImageManager = imageManagerService; + /** + * Hide the image manager + */ $scope.hide = function () { $scope.showing = false; }; + /** + * Fetch the list image data from the server. + */ function fetchData() { var url = '/images/' + $scope.imageType + '/all/' + page; $http.get(url).then((response) => { @@ -82,28 +118,33 @@ module.exports = function (ngApp) { page++; }); } + $scope.fetchData = fetchData; + /** + * Save the details of an image. + * @param event + */ $scope.saveImageDetails = function (event) { event.preventDefault(); var url = '/images/update/' + $scope.selectedImage.id; $http.put(url, this.selectedImage).then((response) => { - $scope.imageUpdateSuccess = true; - $timeout(() => { - $scope.imageUpdateSuccess = false; - }, 3000); + events.emit('success', 'Image details updated'); }, (response) => { var errors = response.data; var message = ''; Object.keys(errors).forEach((key) => { message += errors[key].join('\n'); }); - $scope.imageUpdateFailure = message; - $timeout(() => { - $scope.imageUpdateFailure = false; - }, 5000); + events.emit('error', message); }); }; + /** + * Delete an image from system and notify of success. + * Checks if it should force delete when an image + * has dependant pages. + * @param event + */ $scope.deleteImage = function (event) { event.preventDefault(); var force = $scope.dependantPages !== false; @@ -112,10 +153,7 @@ module.exports = function (ngApp) { $http.delete(url).then((response) => { $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1); $scope.selectedImage = false; - $scope.imageDeleteSuccess = true; - $timeout(() => { - $scope.imageDeleteSuccess = false; - }, 3000); + events.emit('success', 'Image successfully deleted'); }, (response) => { // Pages failure if (response.status === 400) { @@ -124,6 +162,15 @@ module.exports = function (ngApp) { }); }; + /** + * Simple date creator used to properly format dates. + * @param stringDate + * @returns {Date} + */ + $scope.getDate = function(stringDate) { + return new Date(stringDate); + }; + }]); diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 6ccfcf855..60abde6e9 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -5,7 +5,7 @@ var toggleSwitchTemplate = require('./components/toggle-switch.html'); var imagePickerTemplate = require('./components/image-picker.html'); var dropZoneTemplate = require('./components/drop-zone.html'); -module.exports = function (ngApp) { +module.exports = function (ngApp, events) { /** * Toggle Switches diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 5b0b6f9f0..00c5f13c2 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -1,4 +1,4 @@ - +"use strict"; // AngularJS - Create application and load components var angular = require('angular'); @@ -7,9 +7,31 @@ var ngAnimate = require('angular-animate'); var ngSanitize = require('angular-sanitize'); var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']); -var services = require('./services')(ngApp); -var directives = require('./directives')(ngApp); -var controllers = require('./controllers')(ngApp); + + +// Global Event System +var Events = { + listeners: {}, + emit: function (eventName, eventData) { + if (typeof this.listeners[eventName] === 'undefined') return this; + var eventsToStart = this.listeners[eventName]; + for (let i = 0; i < eventsToStart.length; i++) { + var event = eventsToStart[i]; + event(eventData); + } + return this; + }, + listen: function (eventName, callback) { + if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = []; + this.listeners[eventName].push(callback); + return this; + } +}; +window.Events = Events; + +var services = require('./services')(ngApp, Events); +var directives = require('./directives')(ngApp, Events); +var controllers = require('./controllers')(ngApp, Events); //Global jQuery Config & Extensions @@ -32,8 +54,25 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) { // Global jQuery Elements $(function () { + + var notifications = $('.notification'); + var successNotification = notifications.filter('.pos'); + var errorNotification = notifications.filter('.neg'); + // Notification Events + window.Events.listen('success', function (text) { + successNotification.hide(); + successNotification.find('span').text(text); + setTimeout(() => { + successNotification.show(); + }, 1); + }); + window.Events.listen('error', function (text) { + errorNotification.find('span').text(text); + errorNotification.show(); + }); + // Notification hiding - $('.notification').click(function () { + notifications.click(function () { $(this).fadeOut(100); }); diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 756a9211b..290b7c653 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -8,7 +8,6 @@ module.exports = { statusbar: false, menubar: false, paste_data_images: false, - //height: 700, extended_valid_elements: 'pre[*]', automatic_uploads: false, valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]", @@ -31,7 +30,7 @@ module.exports = { alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'}, }, file_browser_callback: function (field_name, url, type, win) { - ImageManager.show(function (image) { + window.ImageManager.showExternal(function (image) { win.document.getElementById(field_name).value = image.url; if ("createEvent" in document) { var evt = document.createEvent("HTMLEvents"); @@ -40,6 +39,10 @@ module.exports = { } else { win.document.getElementById(field_name).fireEvent("onchange"); } + var html = ''; + html += '' + image.name + ''; + html += ''; + win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html); }); }, paste_preprocess: function (plugin, args) { diff --git a/resources/assets/js/services.js b/resources/assets/js/services.js index 684a68450..cd2759c54 100644 --- a/resources/assets/js/services.js +++ b/resources/assets/js/services.js @@ -1,6 +1,6 @@ "use strict"; -module.exports = function(ngApp) { +module.exports = function(ngApp, events) { ngApp.factory('imageManagerService', function() { return { diff --git a/resources/assets/sass/_image-manager.scss b/resources/assets/sass/_image-manager.scss index babbad0c1..8b18d24f3 100644 --- a/resources/assets/sass/_image-manager.scss +++ b/resources/assets/sass/_image-manager.scss @@ -21,7 +21,6 @@ border-radius: 4px; box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3); overflow: hidden; - max-width: 1340px; position: fixed; top: 0; bottom: 0; @@ -44,18 +43,49 @@ right: 0; } -.image-manager-list img { +.image-manager-list .image { display: block; + position: relative; border-radius: 0; float: left; margin: 0; cursor: pointer; width: (100%/6); height: auto; - border: 1px solid #FFF; + border: 1px solid #DDD; + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); transition: all cubic-bezier(.4, 0, 1, 1) 160ms; + overflow: hidden; &.selected { transform: scale3d(0.92, 0.92, 0.92); + border: 1px solid #444; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2); + } + img { + width: 100%; + max-width: 100%; + display: block; + } + .image-meta { + position: absolute; + width: 100%; + bottom: 0; + left: 0; + color: #EEE; + background-color: rgba(0, 0, 0, 0.4); + font-size: 10px; + padding: 3px 4px; + span { + display: block; + } + } + @include smaller-than($xl) { + width: (100%/4); + } + @include smaller-than($m) { + .image-meta { + display: none; + } } } diff --git a/resources/views/partials/image-manager.blade.php b/resources/views/partials/image-manager.blade.php index 6dc528d23..bf7bf445c 100644 --- a/resources/views/partials/image-manager.blade.php +++ b/resources/views/partials/image-manager.blade.php @@ -5,11 +5,14 @@
- +
+ +
+ + Uploaded @{{ getDate(image.created_at) | date:'mediumDate' }} +
+
Load More
@@ -19,18 +22,20 @@

Images

-

+
+ + + +
-

Image name updated

-

@@ -53,8 +58,6 @@
-

Image deleted

-
From d32460070f8e608ec20cc58ddcfa137057086dc5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 8 Feb 2016 19:45:01 +0000 Subject: [PATCH 4/7] Made ldap auth use the 'dn' if a 'uid' is not present. Fixes #56 --- app/Services/LdapService.php | 2 +- ...1_11_210908_add_external_auth_to_users.php | 2 +- tests/Auth/LdapTest.php | 28 +++++++++++++++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/app/Services/LdapService.php b/app/Services/LdapService.php index 84883b09a..3d89e1e44 100644 --- a/app/Services/LdapService.php +++ b/app/Services/LdapService.php @@ -46,7 +46,7 @@ class LdapService $user = $users[0]; return [ - 'uid' => $user['uid'][0], + 'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'], 'name' => $user['cn'][0], 'dn' => $user['dn'], 'email' => (isset($user['mail'])) ? $user['mail'][0] : null diff --git a/database/migrations/2016_01_11_210908_add_external_auth_to_users.php b/database/migrations/2016_01_11_210908_add_external_auth_to_users.php index dda8f3d74..b7663054c 100644 --- a/database/migrations/2016_01_11_210908_add_external_auth_to_users.php +++ b/database/migrations/2016_01_11_210908_add_external_auth_to_users.php @@ -28,4 +28,4 @@ class AddExternalAuthToUsers extends Migration $table->dropColumn('external_auth_id'); }); } -} +} \ No newline at end of file diff --git a/tests/Auth/LdapTest.php b/tests/Auth/LdapTest.php index 14f2f8196..d80b8d50d 100644 --- a/tests/Auth/LdapTest.php +++ b/tests/Auth/LdapTest.php @@ -28,7 +28,7 @@ class LdapTest extends \TestCase ->andReturn(['count' => 1, 0 => [ 'uid' => [$this->mockUser->name], 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test'.config('services.ldap.base_dn')] + 'dn' => ['dc=test' . config('services.ldap.base_dn')] ]]); $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true); @@ -46,6 +46,30 @@ class LdapTest extends \TestCase ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]); } + public function test_login_works_when_no_uid_provided_by_ldap_server() + { + $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); + $this->mockLdap->shouldReceive('setOption')->once(); + $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn'); + $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) + ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array')) + ->andReturn(['count' => 1, 0 => [ + 'cn' => [$this->mockUser->name], + 'dn' => $ldapDn, + 'mail' => [$this->mockUser->email] + ]]); + $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true); + + $this->visit('/login') + ->see('Username') + ->type($this->mockUser->name, '#username') + ->type($this->mockUser->password, '#password') + ->press('Sign In') + ->seePageIs('/') + ->see($this->mockUser->name) + ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $ldapDn]); + } + public function test_initial_incorrect_details() { $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); @@ -55,7 +79,7 @@ class LdapTest extends \TestCase ->andReturn(['count' => 1, 0 => [ 'uid' => [$this->mockUser->name], 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test'.config('services.ldap.base_dn')] + 'dn' => ['dc=test' . config('services.ldap.base_dn')] ]]); $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false); From 5d73d17c74774c80cee3f6b94bb6977105060435 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 8 Feb 2016 20:35:23 +0000 Subject: [PATCH 5/7] Fixed bug preventing LDAP users updating thier profile Made email not required when a profile is updated. LDAP users without admin privileges could did not have an email field to submit therefore could previously not update thier profile. --- app/Http/Controllers/UserController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 325d3118f..bf25eafb2 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -130,8 +130,8 @@ class UserController extends Controller }); $this->validate($request, [ - 'name' => 'required', - 'email' => 'required|email|unique:users,email,' . $id, + 'name' => 'min:2', + 'email' => 'min:2|email|unique:users,email,' . $id, 'password' => 'min:5|required_with:password_confirm', 'password-confirm' => 'same:password|required_with:password', 'role' => 'exists:roles,id' From 9b83c573163cd7f60b3a82d6b8c0f89c86d1aab7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 8 Feb 2016 20:41:40 +0000 Subject: [PATCH 6/7] Fixed some design issues and improved page export styling Fixed alignment on export options dropdown. Fixed bullet list items sitting too close next to floated content. Fixes #34. Fixed text overlaying images in PDF exports (Floats removed for now). Fixes #53. Fixed spaced table cells on html & PDF exports. --- resources/assets/sass/_lists.scss | 4 ++-- resources/assets/sass/_text.scss | 8 ++++---- resources/assets/sass/export-styles.scss | 7 ++++++- resources/views/pages/pdf.blade.php | 6 ++++++ resources/views/pages/show.blade.php | 6 +++--- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index fa609ee33..748694635 100644 --- a/resources/assets/sass/_lists.scss +++ b/resources/assets/sass/_lists.scss @@ -105,8 +105,8 @@ } .book-tree .sidebar-page-list { list-style: none; - margin: 0; - margin-top: $-xs; + margin: $-xs 0 0; + padding-left: 0; border-left: 5px solid $color-book; li a { display: block; diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss index d3663dee3..499896631 100644 --- a/resources/assets/sass/_text.scss +++ b/resources/assets/sass/_text.scss @@ -223,13 +223,13 @@ span.highlight { * Lists */ ul { - list-style: disc; - margin-left: $-m*1.5; + padding-left: $-m * 1.5; + list-style: disc inside; } ol { - list-style: decimal; - margin-left: $-m*1.5; + list-style: decimal inside; + padding-left: $-m * 1.5; } /* diff --git a/resources/assets/sass/export-styles.scss b/resources/assets/sass/export-styles.scss index 90fdb196c..60450f3e2 100644 --- a/resources/assets/sass/export-styles.scss +++ b/resources/assets/sass/export-styles.scss @@ -9,4 +9,9 @@ @import "tables"; @import "header"; @import "lists"; -@import "pages"; \ No newline at end of file +@import "pages"; + +table { + border-spacing: 0; + border-collapse: collapse; +} \ No newline at end of file diff --git a/resources/views/pages/pdf.blade.php b/resources/views/pages/pdf.blade.php index 1077c0931..3a376334d 100644 --- a/resources/views/pages/pdf.blade.php +++ b/resources/views/pages/pdf.blade.php @@ -20,5 +20,11 @@ table td { width: auto !important; } + + .page-content img.align-left, .page-content img.align-right { + float: none !important; + clear: both; + display: block; + } @stop \ No newline at end of file diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index a2c82fd9d..8a5bffca4 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -22,9 +22,9 @@
Export
@if($currentUser->can('page-update')) From e0279f93f907428141f6d67287c3412710153e19 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 8 Feb 2016 20:42:41 +0000 Subject: [PATCH 7/7] Added a back-to-top button on all pages The new back-to-top button will show after scrolling a short distance down a long page. Closes #44. --- resources/assets/js/global.js | 23 +++++++++++++++++++ resources/assets/sass/styles.scss | 38 +++++++++++++++++++++++++++++++ resources/views/base.blade.php | 5 ++++ 3 files changed, 66 insertions(+) diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 00c5f13c2..a61299d21 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -83,6 +83,29 @@ $(function () { $(this).closest('.chapter').find('.inset-list').slideToggle(180); }); + // Back to top button + $('#back-to-top').click(function() { + $('#header').smoothScrollTo(); + }); + var scrollTopShowing = false; + var scrollTop = document.getElementById('back-to-top'); + var scrollTopBreakpoint = 1200; + window.addEventListener('scroll', function() { + if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) { + scrollTop.style.display = 'block'; + scrollTopShowing = true; + setTimeout(() => { + scrollTop.style.opacity = 1; + }, 1); + } else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) { + scrollTop.style.opacity = 0; + scrollTopShowing = false; + setTimeout(() => { + scrollTop.style.display = 'none'; + }, 500); + } + }); + }); diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index c419c08b6..b81e43296 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -126,4 +126,42 @@ $loadingSize: 10px; i { padding-right: $-s; } +} + +// Back to top link +$btt-size: 40px; +#back-to-top { + background-color: rgba($primary, 0.4); + position: fixed; + bottom: $-m; + right: $-m; + padding: $-xs $-s; + cursor: pointer; + color: #FFF; + width: $btt-size; + height: $btt-size; + border-radius: $btt-size; + transition: all ease-in-out 180ms; + opacity: 0; + z-index: 999; + &:hover { + width: $btt-size*3.4; + background-color: rgba($primary, 1); + span { + display: inline-block; + } + } + .inner { + width: $btt-size*3.4; + } + i { + margin: 0; + font-size: 28px; + padding: 0 $-s 0 0; + } + span { + line-height: 12px; + position: relative; + top: -5px; + } } \ No newline at end of file diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index a2979f858..2dfa7ddfd 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -77,6 +77,11 @@ @yield('content') +
+
+ Back to top +
+
@yield('bottom') @yield('scripts')