Dan Brown

Added a markdown editor

...@@ -164,6 +164,7 @@ class PageController extends Controller ...@@ -164,6 +164,7 @@ class PageController extends Controller
164 $draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id); 164 $draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
165 $page->name = $draft->name; 165 $page->name = $draft->name;
166 $page->html = $draft->html; 166 $page->html = $draft->html;
167 + $page->markdown = $draft->markdown;
167 $page->isDraft = true; 168 $page->isDraft = true;
168 $warnings [] = $this->pageRepo->getUserPageDraftMessage($draft); 169 $warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
169 } 170 }
...@@ -204,9 +205,9 @@ class PageController extends Controller ...@@ -204,9 +205,9 @@ class PageController extends Controller
204 $page = $this->pageRepo->getById($pageId, true); 205 $page = $this->pageRepo->getById($pageId, true);
205 $this->checkOwnablePermission('page-update', $page); 206 $this->checkOwnablePermission('page-update', $page);
206 if ($page->draft) { 207 if ($page->draft) {
207 - $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html'])); 208 + $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
208 } else { 209 } else {
209 - $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html'])); 210 + $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
210 } 211 }
211 $updateTime = $draft->updated_at->format('H:i'); 212 $updateTime = $draft->updated_at->format('H:i');
212 return response()->json(['status' => 'success', 'message' => 'Draft saved at ' . $updateTime]); 213 return response()->json(['status' => 'success', 'message' => 'Draft saved at ' . $updateTime]);
......
...@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model; ...@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
6 6
7 class Page extends Entity 7 class Page extends Entity
8 { 8 {
9 - protected $fillable = ['name', 'html', 'priority']; 9 + protected $fillable = ['name', 'html', 'priority', 'markdown'];
10 10
11 protected $simpleAttributes = ['name', 'id', 'slug']; 11 protected $simpleAttributes = ['name', 'id', 'slug'];
12 12
......
...@@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Model; ...@@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Model;
4 4
5 class PageRevision extends Model 5 class PageRevision extends Model
6 { 6 {
7 - protected $fillable = ['name', 'html', 'text']; 7 + protected $fillable = ['name', 'html', 'text', 'markdown'];
8 8
9 /** 9 /**
10 * Get the user that created the page revision 10 * Get the user that created the page revision
......
...@@ -5,6 +5,8 @@ return [ ...@@ -5,6 +5,8 @@ return [
5 5
6 'env' => env('APP_ENV', 'production'), 6 'env' => env('APP_ENV', 'production'),
7 7
8 + 'editor' => env('APP_EDITOR', 'html'),
9 +
8 /* 10 /*
9 |-------------------------------------------------------------------------- 11 |--------------------------------------------------------------------------
10 | Application Debug Mode 12 | Application Debug Mode
......
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class AddMarkdownSupport extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + Schema::table('pages', function (Blueprint $table) {
16 + $table->longText('markdown');
17 + });
18 +
19 + Schema::table('page_revisions', function (Blueprint $table) {
20 + $table->longText('markdown');
21 + });
22 + }
23 +
24 + /**
25 + * Reverse the migrations.
26 + *
27 + * @return void
28 + */
29 + public function down()
30 + {
31 + Schema::table('pages', function (Blueprint $table) {
32 + $table->dropColumn('markdown');
33 + });
34 +
35 + Schema::table('page_revisions', function (Blueprint $table) {
36 + $table->dropColumn('markdown');
37 + });
38 + }
39 +}
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
12 "bootstrap-sass": "^3.0.0", 12 "bootstrap-sass": "^3.0.0",
13 "dropzone": "^4.0.1", 13 "dropzone": "^4.0.1",
14 "laravel-elixir": "^3.4.0", 14 "laravel-elixir": "^3.4.0",
15 + "marked": "^0.3.5",
15 "zeroclipboard": "^2.2.0" 16 "zeroclipboard": "^2.2.0"
16 } 17 }
17 } 18 }
......
No preview for this file type
...@@ -176,3 +176,4 @@ These are the great projects used to help build BookStack: ...@@ -176,3 +176,4 @@ These are the great projects used to help build BookStack:
176 * [Dropzone.js](http://www.dropzonejs.com/) 176 * [Dropzone.js](http://www.dropzonejs.com/)
177 * [ZeroClipboard](http://zeroclipboard.org/) 177 * [ZeroClipboard](http://zeroclipboard.org/)
178 * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) 178 * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
179 +* [Marked](https://github.com/chjj/marked)
......
...@@ -216,16 +216,20 @@ module.exports = function (ngApp, events) { ...@@ -216,16 +216,20 @@ module.exports = function (ngApp, events) {
216 }]); 216 }]);
217 217
218 218
219 - ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', function ($scope, $http, $attrs, $interval, $timeout) { 219 + ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
220 + function ($scope, $http, $attrs, $interval, $timeout, $sce) {
220 221
221 $scope.editorOptions = require('./pages/page-form'); 222 $scope.editorOptions = require('./pages/page-form');
222 - $scope.editorHtml = ''; 223 + $scope.editContent = '';
223 $scope.draftText = ''; 224 $scope.draftText = '';
224 var pageId = Number($attrs.pageId); 225 var pageId = Number($attrs.pageId);
225 var isEdit = pageId !== 0; 226 var isEdit = pageId !== 0;
226 var autosaveFrequency = 30; // AutoSave interval in seconds. 227 var autosaveFrequency = 30; // AutoSave interval in seconds.
228 + var isMarkdown = $attrs.editorType === 'markdown';
227 $scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1; 229 $scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
228 $scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1; 230 $scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
231 +
232 + // Set inital header draft text
229 if ($scope.isUpdateDraft || $scope.isNewPageDraft) { 233 if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
230 $scope.draftText = 'Editing Draft' 234 $scope.draftText = 'Editing Draft'
231 } else { 235 } else {
...@@ -245,7 +249,14 @@ module.exports = function (ngApp, events) { ...@@ -245,7 +249,14 @@ module.exports = function (ngApp, events) {
245 }, 1000); 249 }, 1000);
246 } 250 }
247 251
248 - $scope.editorChange = function () {} 252 + // Actions specifically for the markdown editor
253 + if (isMarkdown) {
254 + $scope.displayContent = '';
255 + // Editor change event
256 + $scope.editorChange = function (content) {
257 + $scope.displayContent = $sce.trustAsHtml(content);
258 + }
259 + }
249 260
250 /** 261 /**
251 * Start the AutoSave loop, Checks for content change 262 * Start the AutoSave loop, Checks for content change
...@@ -253,17 +264,18 @@ module.exports = function (ngApp, events) { ...@@ -253,17 +264,18 @@ module.exports = function (ngApp, events) {
253 */ 264 */
254 function startAutoSave() { 265 function startAutoSave() {
255 currentContent.title = $('#name').val(); 266 currentContent.title = $('#name').val();
256 - currentContent.html = $scope.editorHtml; 267 + currentContent.html = $scope.editContent;
257 268
258 autoSave = $interval(() => { 269 autoSave = $interval(() => {
259 var newTitle = $('#name').val(); 270 var newTitle = $('#name').val();
260 - var newHtml = $scope.editorHtml; 271 + var newHtml = $scope.editContent;
261 272
262 if (newTitle !== currentContent.title || newHtml !== currentContent.html) { 273 if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
263 currentContent.html = newHtml; 274 currentContent.html = newHtml;
264 currentContent.title = newTitle; 275 currentContent.title = newTitle;
265 - saveDraft(newTitle, newHtml); 276 + saveDraft();
266 } 277 }
278 +
267 }, 1000 * autosaveFrequency); 279 }, 1000 * autosaveFrequency);
268 } 280 }
269 281
...@@ -272,20 +284,24 @@ module.exports = function (ngApp, events) { ...@@ -272,20 +284,24 @@ module.exports = function (ngApp, events) {
272 * @param title 284 * @param title
273 * @param html 285 * @param html
274 */ 286 */
275 - function saveDraft(title, html) { 287 + function saveDraft() {
276 - $http.put('/ajax/page/' + pageId + '/save-draft', { 288 + var data = {
277 - name: title, 289 + name: $('#name').val(),
278 - html: html 290 + html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
279 - }).then((responseData) => { 291 + };
292 +
293 + if (isMarkdown) data.markdown = $scope.editContent;
294 +
295 + console.log(data.markdown);
296 +
297 + $http.put('/ajax/page/' + pageId + '/save-draft', data).then((responseData) => {
280 $scope.draftText = responseData.data.message; 298 $scope.draftText = responseData.data.message;
281 if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true; 299 if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
282 }); 300 });
283 } 301 }
284 302
285 $scope.forceDraftSave = function() { 303 $scope.forceDraftSave = function() {
286 - var newTitle = $('#name').val(); 304 + saveDraft();
287 - var newHtml = $scope.editorHtml;
288 - saveDraft(newTitle, newHtml);
289 }; 305 };
290 306
291 /** 307 /**
...@@ -298,6 +314,7 @@ module.exports = function (ngApp, events) { ...@@ -298,6 +314,7 @@ module.exports = function (ngApp, events) {
298 $scope.draftText = 'Editing Page'; 314 $scope.draftText = 'Editing Page';
299 $scope.isUpdateDraft = false; 315 $scope.isUpdateDraft = false;
300 $scope.$broadcast('html-update', responseData.data.html); 316 $scope.$broadcast('html-update', responseData.data.html);
317 + $scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html);
301 $('#name').val(responseData.data.name); 318 $('#name').val(responseData.data.name);
302 $timeout(() => { 319 $timeout(() => {
303 startAutoSave(); 320 startAutoSave();
......
1 "use strict"; 1 "use strict";
2 var DropZone = require('dropzone'); 2 var DropZone = require('dropzone');
3 +var markdown = require( "marked" );
3 4
4 var toggleSwitchTemplate = require('./components/toggle-switch.html'); 5 var toggleSwitchTemplate = require('./components/toggle-switch.html');
5 var imagePickerTemplate = require('./components/image-picker.html'); 6 var imagePickerTemplate = require('./components/image-picker.html');
...@@ -202,5 +203,36 @@ module.exports = function (ngApp, events) { ...@@ -202,5 +203,36 @@ module.exports = function (ngApp, events) {
202 } 203 }
203 }]) 204 }])
204 205
206 + ngApp.directive('markdownEditor', ['$timeout', function($timeout) {
207 + return {
208 + restrict: 'A',
209 + scope: {
210 + mdModel: '=',
211 + mdChange: '='
212 + },
213 + link: function (scope, element, attrs) {
214 +
215 + // Set initial model content
216 + var content = element.val();
217 + scope.mdModel = content;
218 + scope.mdChange(markdown(content));
219 +
220 + element.on('change input', (e) => {
221 + content = element.val();
222 + $timeout(() => {
223 + scope.mdModel = content;
224 + scope.mdChange(markdown(content));
225 + });
226 + });
227 +
228 + scope.$on('markdown-update', (event, value) => {
229 + element.val(value);
230 + scope.mdModel= value;
231 + scope.mdChange(markdown(value));
232 + });
233 +
234 + }
235 + }
236 + }])
205 237
206 }; 238 };
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -94,3 +94,14 @@ ...@@ -94,3 +94,14 @@
94 font-weight: normal; 94 font-weight: normal;
95 font-style: normal; 95 font-style: normal;
96 } 96 }
97 +
98 +/* roboto-mono-regular - latin */
99 +// https://google-webfonts-helper.herokuapp.com
100 +@font-face {
101 + font-family: 'Roboto Mono';
102 + font-style: normal;
103 + font-weight: 400;
104 + src: local('Roboto Mono'), local('RobotoMono-Regular'),
105 + url('/fonts/roboto-mono-v4-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
106 + url('/fonts/roboto-mono-v4-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
107 +}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -26,6 +26,58 @@ ...@@ -26,6 +26,58 @@
26 display: none; 26 display: none;
27 } 27 }
28 28
29 +#markdown-editor {
30 + position: relative;
31 + z-index: 5;
32 + textarea {
33 + font-family: 'Roboto Mono';
34 + font-style: normal;
35 + font-weight: 400;
36 + padding: $-xs $-m;
37 + color: #444;
38 + border-radius: 0;
39 + max-height: 100%;
40 + flex: 1;
41 + border: 0;
42 + &:focus {
43 + outline: 0;
44 + }
45 + }
46 + .markdown-display, .markdown-editor-wrap {
47 + flex: 1;
48 + padding-top: 28px;
49 + position: relative;
50 + border: 1px solid #DDD;
51 + &:before {
52 + display: block;
53 + position: absolute;
54 + top: 0;
55 + left: 0;
56 + width: 100%;
57 + padding: $-xs $-m;
58 + font-family: 'Roboto Mono';
59 + font-size: 11px;
60 + line-height: 1;
61 + border-bottom: 1px solid #DDD;
62 + background-color: #EEE;
63 + }
64 + }
65 + .markdown-editor-wrap {
66 + display: flex;
67 + &:before {
68 + content: 'Editor';
69 + }
70 + }
71 + .markdown-display {
72 + padding: 0 $-m;
73 + padding-top: 28px;
74 + margin-left: -1px;
75 + &:before {
76 + content: 'Preview';
77 + }
78 + }
79 +}
80 +
29 label { 81 label {
30 display: block; 82 display: block;
31 line-height: 1.4em; 83 line-height: 1.4em;
...@@ -160,6 +212,10 @@ input:checked + .toggle-switch { ...@@ -160,6 +212,10 @@ input:checked + .toggle-switch {
160 width: 100%; 212 width: 100%;
161 } 213 }
162 214
215 +div[editor-type="markdown"] .title-input.page-title input[type="text"] {
216 + max-width: 100%;
217 +}
218 +
163 .search-box { 219 .search-box {
164 max-width: 100%; 220 max-width: 100%;
165 position: relative; 221 position: relative;
......
...@@ -157,6 +157,12 @@ span.code { ...@@ -157,6 +157,12 @@ span.code {
157 @extend .code-base; 157 @extend .code-base;
158 padding: 1px $-xs; 158 padding: 1px $-xs;
159 } 159 }
160 +
161 +pre code {
162 + background-color: transparent;
163 + border: 0;
164 + font-size: 1em;
165 +}
160 /* 166 /*
161 * Text colors 167 * Text colors
162 */ 168 */
......
1 1
2 -<div class="page-editor flex-fill flex" ng-controller="PageEditController" page-id="{{ $model->id or 0 }}" page-new-draft="{{ $model->draft or 0 }}" page-update-draft="{{ $model->isDraft or 0 }}"> 2 +<div class="page-editor flex-fill flex" ng-controller="PageEditController" editor-type="{{ config('app.editor') }}" page-id="{{ $model->id or 0 }}" page-new-draft="{{ $model->draft or 0 }}" page-update-draft="{{ $model->isDraft or 0 }}">
3 3
4 {{ csrf_field() }} 4 {{ csrf_field() }}
5 <div class="faded-small toolbar"> 5 <div class="faded-small toolbar">
...@@ -42,10 +42,31 @@ ...@@ -42,10 +42,31 @@
42 </div> 42 </div>
43 </div> 43 </div>
44 <div class="edit-area flex-fill flex"> 44 <div class="edit-area flex-fill flex">
45 - <textarea id="html-editor" tinymce="editorOptions" mce-change="editorChange" mce-model="editorHtml" name="html" rows="5" 45 + @if(config('app.editor') === 'html')
46 + <textarea id="html-editor" tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" name="html" rows="5"
46 @if($errors->has('html')) class="neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea> 47 @if($errors->has('html')) class="neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
47 @if($errors->has('html')) 48 @if($errors->has('html'))
48 <div class="text-neg text-small">{{ $errors->first('html') }}</div> 49 <div class="text-neg text-small">{{ $errors->first('html') }}</div>
49 @endif 50 @endif
51 + @endif
52 +
53 + @if(config('app.editor') === 'markdown')
54 + <div id="markdown-editor" class="flex-fill flex">
55 +
56 + <div class="markdown-editor-wrap">
57 + <textarea markdown-editor md-change="editorChange" md-model="editContent" name="markdown" rows="5"
58 + @if($errors->has('markdown')) class="neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
59 + </div>
60 +
61 + <div class="markdown-display page-content" ng-bind-html="displayContent"></div>
62 + </div>
63 +
64 + <input type="hidden" name="html" ng-value="displayContent">
65 +
66 + @if($errors->has('markdown'))
67 + <div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
68 + @endif
69 +
70 + @endif
50 </div> 71 </div>
51 </div> 72 </div>
...\ No newline at end of file ...\ No newline at end of file
......