diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 19e4744ea..c6228a8bc 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -142,6 +142,23 @@ class PageController extends Controller return redirect($page->getUrl()); } + /** + * Save a draft update as a revision. + * @param Request $request + * @param $pageId + * @return \Illuminate\Http\JsonResponse + */ + public function saveUpdateDraft(Request $request, $pageId) + { + $this->validate($request, [ + 'name' => 'required|string|max:255' + ]); + $page = $this->pageRepo->getById($pageId); + $this->checkOwnablePermission('page-update', $page); + $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html'])); + return response()->json(['status' => 'success', 'message' => 'Draft successfully saved']); + } + /** * Redirect from a special link url which * uses the page id rather than the name. diff --git a/app/Http/routes.php b/app/Http/routes.php index 81bbb16bc..e16d4f8f9 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -75,6 +75,9 @@ Route::group(['middleware' => 'auth'], function () { Route::delete('/{imageId}', 'ImageController@destroy'); }); + // Ajax routes + Route::put('/ajax/page/{id}/save-draft', 'PageController@saveUpdateDraft'); + // Links Route::get('/link/{id}', 'PageController@redirectFromLink'); diff --git a/app/Page.php b/app/Page.php index 53724ec20..34dee2f2f 100644 --- a/app/Page.php +++ b/app/Page.php @@ -34,7 +34,7 @@ class Page extends Entity public function revisions() { - return $this->hasMany('BookStack\PageRevision')->orderBy('created_at', 'desc'); + return $this->hasMany('BookStack\PageRevision')->where('type', '=', 'version')->orderBy('created_at', 'desc'); } public function getUrl() diff --git a/app/PageRevision.php b/app/PageRevision.php index 52c37e390..f1b4bc587 100644 --- a/app/PageRevision.php +++ b/app/PageRevision.php @@ -1,6 +1,4 @@ -belongsTo('BookStack\User', 'created_by'); } + /** + * Get the page this revision originates from. + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function page() { return $this->belongsTo('BookStack\Page'); } + /** + * Get the url for this revision. + * @return string + */ public function getUrl() { return $this->page->getUrl() . '/revisions/' . $this->id; diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 4784ad407..ca97fc1e9 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -4,6 +4,7 @@ use Activity; use BookStack\Book; use BookStack\Exceptions\NotFoundException; +use DOMDocument; use Illuminate\Support\Str; use BookStack\Page; use BookStack\PageRevision; @@ -66,9 +67,10 @@ class PageRepo extends EntityRepo public function findPageUsingOldSlug($pageSlug, $bookSlug) { $revision = $this->pageRevision->where('slug', '=', $pageSlug) - ->whereHas('page', function($query) { + ->whereHas('page', function ($query) { $this->restrictionService->enforcePageRestrictions($query); }) + ->where('type', '=', 'version') ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc') ->with('page')->first(); return $revision !== null ? $revision->page : null; @@ -100,8 +102,8 @@ class PageRepo extends EntityRepo * Save a new page into the system. * Input validation must be done beforehand. * @param array $input - * @param Book $book - * @param int $chapterId + * @param Book $book + * @param int $chapterId * @return Page */ public function saveNew(array $input, Book $book, $chapterId = null) @@ -128,9 +130,9 @@ class PageRepo extends EntityRepo */ protected function formatHtml($htmlText) { - if($htmlText == '') return $htmlText; + if ($htmlText == '') return $htmlText; libxml_use_internal_errors(true); - $doc = new \DOMDocument(); + $doc = new DOMDocument(); $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8')); $container = $doc->documentElement; @@ -239,8 +241,8 @@ class PageRepo extends EntityRepo /** * Updates a page with any fillable data and saves it into the database. - * @param Page $page - * @param int $book_id + * @param Page $page + * @param int $book_id * @param string $input * @return Page */ @@ -297,6 +299,7 @@ class PageRepo extends EntityRepo $revision->book_slug = $page->book->slug; $revision->created_by = auth()->user()->id; $revision->created_at = $page->updated_at; + $revision->type = 'version'; $revision->save(); // Clear old revisions if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { @@ -306,6 +309,36 @@ class PageRepo extends EntityRepo return $revision; } + /** + * Save a page update draft. + * @param Page $page + * @param array $data + * @return PageRevision + */ + public function saveUpdateDraft(Page $page, $data = []) + { + $userId = auth()->user()->id; + $drafts = $this->pageRevision->where('created_by', '=', $userId) + ->where('type', 'update_draft') + ->where('page_id', '=', $page->id) + ->orderBy('created_at', 'desc')->get(); + + if ($drafts->count() > 0) { + $draft = $drafts->first(); + } else { + $draft = $this->pageRevision->newInstance(); + $draft->page_id = $page->id; + $draft->slug = $page->slug; + $draft->book_slug = $page->book->slug; + $draft->created_by = $userId; + $draft->type = 'update_draft'; + } + + $draft->fill($data); + $draft->save(); + return $draft; + } + /** * Gets a single revision via it's id. * @param $id @@ -333,7 +366,7 @@ class PageRepo extends EntityRepo /** * Changes the related book for the specified page. * Changes the book id of any relations to the page that store the book id. - * @param int $bookId + * @param int $bookId * @param Page $page * @return Page */ diff --git a/database/migrations/2016_03_09_203143_add_page_revision_types.php b/database/migrations/2016_03_09_203143_add_page_revision_types.php new file mode 100644 index 000000000..e39c77d18 --- /dev/null +++ b/database/migrations/2016_03_09_203143_add_page_revision_types.php @@ -0,0 +1,32 @@ +string('type')->default('version'); + $table->index('type'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('page_revisions', function (Blueprint $table) { + $table->dropColumn('type'); + }); + } +} diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 1f7388859..305e0c3c1 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -213,4 +213,49 @@ module.exports = function (ngApp, events) { }]); + ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', function ($scope, $http, $attrs, $interval) { + + $scope.editorOptions = require('./pages/page-form'); + $scope.editorHtml = ''; + $scope.draftText = ''; + var pageId = Number($attrs.pageId); + var isEdit = pageId !== 0; + + if (isEdit) { + startAutoSave(); + } + + $scope.editorChange = function() { + $scope.draftText = ''; + } + + function startAutoSave() { + var currentTitle = $('#name').val(); + var currentHtml = $scope.editorHtml; + + console.log('Starting auto save'); + + $interval(() => { + var newTitle = $('#name').val(); + var newHtml = $scope.editorHtml; + + if (newTitle !== currentTitle || newHtml !== currentHtml) { + currentHtml = newHtml; + currentTitle = newTitle; + saveDraftUpdate(newTitle, newHtml); + } + }, 1000*5); + } + + function saveDraftUpdate(title, html) { + $http.put('/ajax/page/' + pageId + '/save-draft', { + name: title, + html: html + }).then((responseData) => { + $scope.draftText = 'Draft saved' + }) + } + + }]); + }; \ No newline at end of file diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 60abde6e9..b6c41bb3c 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -162,5 +162,31 @@ module.exports = function (ngApp, events) { }; }]); + ngApp.directive('tinymce', [function() { + return { + restrict: 'A', + scope: { + tinymce: '=', + ngModel: '=', + ngChange: '=' + }, + link: function (scope, element, attrs) { + + function tinyMceSetup(editor) { + editor.on('keyup', (e) => { + var content = editor.getContent(); + scope.$apply(() => { + scope.ngModel = content; + }); + scope.ngChange(content); + }); + } + + scope.tinymce.extraSetups.push(tinyMceSetup); + tinymce.init(scope.tinymce); + } + } + }]) + }; \ No newline at end of file diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 5400a8af0..aa5e60ce4 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -119,11 +119,5 @@ function elemExists(selector) { return document.querySelector(selector) !== null; } -// TinyMCE editor -if (elemExists('#html-editor')) { - var tinyMceOptions = require('./pages/page-form'); - tinymce.init(tinyMceOptions); -} - // Page specific items -require('./pages/page-show'); \ No newline at end of file +require('./pages/page-show'); diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 290b7c653..0310b5fa2 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -1,4 +1,4 @@ -module.exports = { +var mceOptions = module.exports = { selector: '#html-editor', content_css: [ '/css/styles.css' @@ -51,8 +51,15 @@ module.exports = { args.content = ''; } }, + extraSetups: [], setup: function (editor) { + console.log(mceOptions.extraSetups); + + for (var i = 0; i < mceOptions.extraSetups.length; i++) { + mceOptions.extraSetups[i](editor); + } + (function () { var wrap; diff --git a/resources/views/pages/create.blade.php b/resources/views/pages/create.blade.php index 69c5f7c94..441379eae 100644 --- a/resources/views/pages/create.blade.php +++ b/resources/views/pages/create.blade.php @@ -8,7 +8,7 @@ @section('content') -