Dan Brown

Merge pull request #84 from ssddanbrown/markdown_editor

Initial implementation of a markdown editor. Closes #57.
...@@ -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
......
...@@ -312,6 +312,7 @@ class PageRepo extends EntityRepo ...@@ -312,6 +312,7 @@ class PageRepo extends EntityRepo
312 $page->fill($input); 312 $page->fill($input);
313 $page->html = $this->formatHtml($input['html']); 313 $page->html = $this->formatHtml($input['html']);
314 $page->text = strip_tags($page->html); 314 $page->text = strip_tags($page->html);
315 + if (setting('app-editor') !== 'markdown') $page->markdown = '';
315 $page->updated_by = $userId; 316 $page->updated_by = $userId;
316 $page->save(); 317 $page->save();
317 318
...@@ -348,6 +349,7 @@ class PageRepo extends EntityRepo ...@@ -348,6 +349,7 @@ class PageRepo extends EntityRepo
348 public function saveRevision(Page $page) 349 public function saveRevision(Page $page)
349 { 350 {
350 $revision = $this->pageRevision->fill($page->toArray()); 351 $revision = $this->pageRevision->fill($page->toArray());
352 + if (setting('app-editor') !== 'markdown') $revision->markdown = '';
351 $revision->page_id = $page->id; 353 $revision->page_id = $page->id;
352 $revision->slug = $page->slug; 354 $revision->slug = $page->slug;
353 $revision->book_slug = $page->book->slug; 355 $revision->book_slug = $page->book->slug;
...@@ -386,6 +388,8 @@ class PageRepo extends EntityRepo ...@@ -386,6 +388,8 @@ class PageRepo extends EntityRepo
386 } 388 }
387 389
388 $draft->fill($data); 390 $draft->fill($data);
391 + if (setting('app-editor') !== 'markdown') $draft->markdown = '';
392 +
389 $draft->save(); 393 $draft->save();
390 return $draft; 394 return $draft;
391 } 395 }
......
...@@ -44,28 +44,39 @@ class SettingService ...@@ -44,28 +44,39 @@ class SettingService
44 44
45 /** 45 /**
46 * Gets a setting value from the cache or database. 46 * Gets a setting value from the cache or database.
47 + * Looks at the system defaults if not cached or in database.
47 * @param $key 48 * @param $key
48 * @param $default 49 * @param $default
49 * @return mixed 50 * @return mixed
50 */ 51 */
51 protected function getValueFromStore($key, $default) 52 protected function getValueFromStore($key, $default)
52 { 53 {
54 + // Check for an overriding value
53 $overrideValue = $this->getOverrideValue($key); 55 $overrideValue = $this->getOverrideValue($key);
54 if ($overrideValue !== null) return $overrideValue; 56 if ($overrideValue !== null) return $overrideValue;
55 57
58 + // Check the cache
56 $cacheKey = $this->cachePrefix . $key; 59 $cacheKey = $this->cachePrefix . $key;
57 if ($this->cache->has($cacheKey)) { 60 if ($this->cache->has($cacheKey)) {
58 return $this->cache->get($cacheKey); 61 return $this->cache->get($cacheKey);
59 } 62 }
60 63
64 + // Check the database
61 $settingObject = $this->getSettingObjectByKey($key); 65 $settingObject = $this->getSettingObjectByKey($key);
62 -
63 if ($settingObject !== null) { 66 if ($settingObject !== null) {
64 $value = $settingObject->value; 67 $value = $settingObject->value;
65 $this->cache->forever($cacheKey, $value); 68 $this->cache->forever($cacheKey, $value);
66 return $value; 69 return $value;
67 } 70 }
68 71
72 + // Check the defaults set in the app config.
73 + $configPrefix = 'setting-defaults.' . $key;
74 + if (config()->has($configPrefix)) {
75 + $value = config($configPrefix);
76 + $this->cache->forever($cacheKey, $value);
77 + return $value;
78 + }
79 +
69 return $default; 80 return $default;
70 } 81 }
71 82
......
...@@ -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 +/**
4 + * The defaults for the system settings that are saved in the database.
5 + */
6 +return [
7 +
8 + 'app-editor' => 'wysiwyg'
9 +
10 +];
...\ No newline at end of file ...\ No newline at end of file
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')->default('');
17 + });
18 +
19 + Schema::table('page_revisions', function (Blueprint $table) {
20 + $table->longText('markdown')->default('');
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
...@@ -45,3 +45,4 @@ These are the great projects used to help build BookStack: ...@@ -45,3 +45,4 @@ These are the great projects used to help build BookStack:
45 * [Dropzone.js](http://www.dropzonejs.com/) 45 * [Dropzone.js](http://www.dropzonejs.com/)
46 * [ZeroClipboard](http://zeroclipboard.org/) 46 * [ZeroClipboard](http://zeroclipboard.org/)
47 * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) 47 * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
48 +* [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,18 @@ module.exports = function (ngApp, events) { ...@@ -245,7 +249,18 @@ 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 + }
260 +
261 + if (!isMarkdown) {
262 + $scope.editorChange = function() {};
263 + }
249 264
250 /** 265 /**
251 * Start the AutoSave loop, Checks for content change 266 * Start the AutoSave loop, Checks for content change
...@@ -253,17 +268,18 @@ module.exports = function (ngApp, events) { ...@@ -253,17 +268,18 @@ module.exports = function (ngApp, events) {
253 */ 268 */
254 function startAutoSave() { 269 function startAutoSave() {
255 currentContent.title = $('#name').val(); 270 currentContent.title = $('#name').val();
256 - currentContent.html = $scope.editorHtml; 271 + currentContent.html = $scope.editContent;
257 272
258 autoSave = $interval(() => { 273 autoSave = $interval(() => {
259 var newTitle = $('#name').val(); 274 var newTitle = $('#name').val();
260 - var newHtml = $scope.editorHtml; 275 + var newHtml = $scope.editContent;
261 276
262 if (newTitle !== currentContent.title || newHtml !== currentContent.html) { 277 if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
263 currentContent.html = newHtml; 278 currentContent.html = newHtml;
264 currentContent.title = newTitle; 279 currentContent.title = newTitle;
265 - saveDraft(newTitle, newHtml); 280 + saveDraft();
266 } 281 }
282 +
267 }, 1000 * autosaveFrequency); 283 }, 1000 * autosaveFrequency);
268 } 284 }
269 285
...@@ -272,20 +288,22 @@ module.exports = function (ngApp, events) { ...@@ -272,20 +288,22 @@ module.exports = function (ngApp, events) {
272 * @param title 288 * @param title
273 * @param html 289 * @param html
274 */ 290 */
275 - function saveDraft(title, html) { 291 + function saveDraft() {
276 - $http.put('/ajax/page/' + pageId + '/save-draft', { 292 + var data = {
277 - name: title, 293 + name: $('#name').val(),
278 - html: html 294 + html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
279 - }).then((responseData) => { 295 + };
296 +
297 + if (isMarkdown) data.markdown = $scope.editContent;
298 +
299 + $http.put('/ajax/page/' + pageId + '/save-draft', data).then((responseData) => {
280 $scope.draftText = responseData.data.message; 300 $scope.draftText = responseData.data.message;
281 if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true; 301 if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
282 }); 302 });
283 } 303 }
284 304
285 $scope.forceDraftSave = function() { 305 $scope.forceDraftSave = function() {
286 - var newTitle = $('#name').val(); 306 + saveDraft();
287 - var newHtml = $scope.editorHtml;
288 - saveDraft(newTitle, newHtml);
289 }; 307 };
290 308
291 /** 309 /**
...@@ -298,6 +316,7 @@ module.exports = function (ngApp, events) { ...@@ -298,6 +316,7 @@ module.exports = function (ngApp, events) {
298 $scope.draftText = 'Editing Page'; 316 $scope.draftText = 'Editing Page';
299 $scope.isUpdateDraft = false; 317 $scope.isUpdateDraft = false;
300 $scope.$broadcast('html-update', responseData.data.html); 318 $scope.$broadcast('html-update', responseData.data.html);
319 + $scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html);
301 $('#name').val(responseData.data.name); 320 $('#name').val(responseData.data.name);
302 $timeout(() => { 321 $timeout(() => {
303 startAutoSave(); 322 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');
...@@ -200,7 +201,82 @@ module.exports = function (ngApp, events) { ...@@ -200,7 +201,82 @@ module.exports = function (ngApp, events) {
200 tinymce.init(scope.tinymce); 201 tinymce.init(scope.tinymce);
201 } 202 }
202 } 203 }
203 - }]) 204 + }]);
205 +
206 + ngApp.directive('markdownInput', ['$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 + }]);
237 +
238 + ngApp.directive('markdownEditor', ['$timeout', function($timeout) {
239 + return {
240 + restrict: 'A',
241 + link: function (scope, element, attrs) {
242 +
243 + // Elements
244 + var input = element.find('textarea[markdown-input]');
245 + var insertImage = element.find('button[data-action="insertImage"]');
246 +
247 + var currentCaretPos = 0;
248 +
249 + input.blur((event) => {
250 + currentCaretPos = input[0].selectionStart;
251 + });
204 252
253 + // Insert image shortcut
254 + input.keydown((event) => {
255 + if (event.which === 73 && event.ctrlKey && event.shiftKey) {
256 + event.preventDefault();
257 + var caretPos = input[0].selectionStart;
258 + var currentContent = input.val();
259 + var mdImageText = "![](http://)";
260 + input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
261 + input.focus();
262 + input[0].selectionStart = caretPos + ("![](".length);
263 + input[0].selectionEnd = caretPos + ('![](http://'.length);
264 + }
265 + });
266 +
267 + // Insert image from image manager
268 + insertImage.click((event) => {
269 + window.ImageManager.showExternal((image) => {
270 + var caretPos = currentCaretPos;
271 + var currentContent = input.val();
272 + var mdImageText = "![" + image.name + "](" + image.url + ")";
273 + input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
274 + input.change();
275 + });
276 + });
277 +
278 + }
279 + }
280 + }])
205 281
206 }; 282 };
...\ 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,59 @@ ...@@ -26,6 +26,59 @@
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 + width: 100%;
43 + &:focus {
44 + outline: 0;
45 + }
46 + }
47 + .markdown-display, .markdown-editor-wrap {
48 + flex: 1;
49 + position: relative;
50 + }
51 + .markdown-editor-wrap {
52 + display: flex;
53 + flex-direction: column;
54 + border: 1px solid #DDD;
55 + }
56 + .markdown-display {
57 + padding: 0 $-m 0;
58 + margin-left: -1px;
59 + overflow-y: scroll;
60 + .page-content {
61 + margin: 0 auto;
62 + }
63 + }
64 +}
65 +.editor-toolbar {
66 + width: 100%;
67 + padding: $-xs $-m;
68 + font-family: 'Roboto Mono';
69 + font-size: 11px;
70 + line-height: 1.6;
71 + border-bottom: 1px solid #DDD;
72 + background-color: #EEE;
73 + flex: none;
74 + &:after {
75 + content: '';
76 + display: block;
77 + clear: both;
78 + }
79 +}
80 +
81 +
29 label { 82 label {
30 display: block; 83 display: block;
31 line-height: 1.4em; 84 line-height: 1.4em;
...@@ -160,6 +213,10 @@ input:checked + .toggle-switch { ...@@ -160,6 +213,10 @@ input:checked + .toggle-switch {
160 width: 100%; 213 width: 100%;
161 } 214 }
162 215
216 +div[editor-type="markdown"] .title-input.page-title input[type="text"] {
217 + max-width: 100%;
218 +}
219 +
163 .search-box { 220 .search-box {
164 max-width: 100%; 221 max-width: 100%;
165 position: relative; 222 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="{{ setting('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,45 @@ ...@@ -42,10 +42,45 @@
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(setting('app-editor') === 'wysiwyg')
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(setting('app-editor') === 'markdown')
54 + <div id="markdown-editor" markdown-editor class="flex-fill flex">
55 +
56 + <div class="markdown-editor-wrap">
57 + <div class="editor-toolbar">
58 + <span class="float left">Editor</span>
59 + <div class="float right buttons">
60 + <button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button>
61 + </div>
62 + </div>
63 + <textarea markdown-input md-change="editorChange" md-model="editContent" name="markdown" rows="5"
64 + @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>
65 + </div>
66 +
67 + <div class="markdown-editor-wrap">
68 + <div class="editor-toolbar">
69 + <div class="">Preview</div>
70 + </div>
71 + <div class="markdown-display">
72 + <div class="page-content" ng-bind-html="displayContent"></div>
73 + </div>
74 + </div>
75 +
76 + </div>
77 +
78 + <input type="hidden" name="html" ng-value="displayContent">
79 +
80 + @if($errors->has('markdown'))
81 + <div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
82 + @endif
83 +
84 + @endif
50 </div> 85 </div>
51 </div> 86 </div>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -17,29 +17,37 @@ ...@@ -17,29 +17,37 @@
17 <div class="col-md-6"> 17 <div class="col-md-6">
18 <div class="form-group"> 18 <div class="form-group">
19 <label for="setting-app-name">Application name</label> 19 <label for="setting-app-name">Application name</label>
20 - <input type="text" value="{{ Setting::get('app-name', 'BookStack') }}" name="setting-app-name" id="setting-app-name"> 20 + <input type="text" value="{{ setting('app-name', 'BookStack') }}" name="setting-app-name" id="setting-app-name">
21 </div> 21 </div>
22 <div class="form-group"> 22 <div class="form-group">
23 <label>Allow public viewing?</label> 23 <label>Allow public viewing?</label>
24 - <toggle-switch name="setting-app-public" value="{{ Setting::get('app-public') }}"></toggle-switch> 24 + <toggle-switch name="setting-app-public" value="{{ setting('app-public') }}"></toggle-switch>
25 </div> 25 </div>
26 <div class="form-group"> 26 <div class="form-group">
27 <label>Enable higher security image uploads?</label> 27 <label>Enable higher security image uploads?</label>
28 <p class="small">For performance reasons, all images are public by default, This option adds a random, hard-to-guess characters in front of image names. Ensure directory indexes are not enabled to prevent easy access.</p> 28 <p class="small">For performance reasons, all images are public by default, This option adds a random, hard-to-guess characters in front of image names. Ensure directory indexes are not enabled to prevent easy access.</p>
29 - <toggle-switch name="setting-app-secure-images" value="{{ Setting::get('app-secure-images') }}"></toggle-switch> 29 + <toggle-switch name="setting-app-secure-images" value="{{ setting('app-secure-images') }}"></toggle-switch>
30 + </div>
31 + <div class="form-group">
32 + <label for="setting-app-editor">Page Editor</label>
33 + <p class="small">Select which editor will be used by all users to edit pages.</p>
34 + <select name="setting-app-editor" id="setting-app-editor">
35 + <option @if(setting('app-editor') === 'wysiwyg') selected @endif value="wysiwyg">WYSIWYG</option>
36 + <option @if(setting('app-editor') === 'markdown') selected @endif value="markdown">Markdown</option>
37 + </select>
30 </div> 38 </div>
31 </div> 39 </div>
32 <div class="col-md-6"> 40 <div class="col-md-6">
33 <div class="form-group" id="logo-control"> 41 <div class="form-group" id="logo-control">
34 <label for="setting-app-logo">Application Logo</label> 42 <label for="setting-app-logo">Application Logo</label>
35 <p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p> 43 <p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p>
36 - <image-picker resize-height="43" show-remove="true" resize-width="200" current-image="{{ Setting::get('app-logo', '') }}" default-image="/logo.png" name="setting-app-logo" image-class="logo-image"></image-picker> 44 + <image-picker resize-height="43" show-remove="true" resize-width="200" current-image="{{ setting('app-logo', '') }}" default-image="/logo.png" name="setting-app-logo" image-class="logo-image"></image-picker>
37 </div> 45 </div>
38 <div class="form-group" id="color-control"> 46 <div class="form-group" id="color-control">
39 <label for="setting-app-color">Application Primary Color</label> 47 <label for="setting-app-color">Application Primary Color</label>
40 <p class="small">This should be a hex value. <br> Leave empty to reset to the default color.</p> 48 <p class="small">This should be a hex value. <br> Leave empty to reset to the default color.</p>
41 - <input type="text" value="{{ Setting::get('app-color', '') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1"> 49 + <input type="text" value="{{ setting('app-color', '') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
42 - <input type="hidden" value="{{ Setting::get('app-color-light', '') }}" name="setting-app-color-light" id="setting-app-color-light" placeholder="rgba(21, 101, 192, 0.15)"> 50 + <input type="hidden" value="{{ setting('app-color-light', '') }}" name="setting-app-color-light" id="setting-app-color-light" placeholder="rgba(21, 101, 192, 0.15)">
43 </div> 51 </div>
44 </div> 52 </div>
45 </div> 53 </div>
...@@ -53,14 +61,14 @@ ...@@ -53,14 +61,14 @@
53 <div class="col-md-6"> 61 <div class="col-md-6">
54 <div class="form-group"> 62 <div class="form-group">
55 <label for="setting-registration-enabled">Allow registration?</label> 63 <label for="setting-registration-enabled">Allow registration?</label>
56 - <toggle-switch name="setting-registration-enabled" value="{{ Setting::get('registration-enabled') }}"></toggle-switch> 64 + <toggle-switch name="setting-registration-enabled" value="{{ setting('registration-enabled') }}"></toggle-switch>
57 </div> 65 </div>
58 <div class="form-group"> 66 <div class="form-group">
59 <label for="setting-registration-role">Default user role after registration</label> 67 <label for="setting-registration-role">Default user role after registration</label>
60 <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif> 68 <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
61 @foreach(\BookStack\Role::all() as $role) 69 @foreach(\BookStack\Role::all() as $role)
62 <option value="{{$role->id}}" 70 <option value="{{$role->id}}"
63 - @if(\Setting::get('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif 71 + @if(setting('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
64 > 72 >
65 {{ $role->display_name }} 73 {{ $role->display_name }}
66 </option> 74 </option>
...@@ -70,7 +78,7 @@ ...@@ -70,7 +78,7 @@
70 <div class="form-group"> 78 <div class="form-group">
71 <label for="setting-registration-confirmation">Require email confirmation?</label> 79 <label for="setting-registration-confirmation">Require email confirmation?</label>
72 <p class="small">If domain restriction is used then email confirmation will be required and the below value will be ignored.</p> 80 <p class="small">If domain restriction is used then email confirmation will be required and the below value will be ignored.</p>
73 - <toggle-switch name="setting-registration-confirmation" value="{{ Setting::get('registration-confirmation') }}"></toggle-switch> 81 + <toggle-switch name="setting-registration-confirmation" value="{{ setting('registration-confirmation') }}"></toggle-switch>
74 </div> 82 </div>
75 </div> 83 </div>
76 <div class="col-md-6"> 84 <div class="col-md-6">
...@@ -78,7 +86,7 @@ ...@@ -78,7 +86,7 @@
78 <label for="setting-registration-restrict">Restrict registration to domain</label> 86 <label for="setting-registration-restrict">Restrict registration to domain</label>
79 <p class="small">Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. 87 <p class="small">Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application.
80 <br> Note that users will be able to change their email addresses after successful registration.</p> 88 <br> Note that users will be able to change their email addresses after successful registration.</p>
81 - <input type="text" id="setting-registration-restrict" name="setting-registration-restrict" placeholder="No restriction set" value="{{ Setting::get('registration-restrict', '') }}"> 89 + <input type="text" id="setting-registration-restrict" name="setting-registration-restrict" placeholder="No restriction set" value="{{ setting('registration-restrict', '') }}">
82 </div> 90 </div>
83 </div> 91 </div>
84 </div> 92 </div>
......
1 +<?php
2 +
3 +
4 +class MarkdownTest extends TestCase
5 +{
6 + protected $page;
7 +
8 + public function setUp()
9 + {
10 + parent::setUp();
11 + $this->page = \BookStack\Page::first();
12 + }
13 +
14 + protected function setMarkdownEditor()
15 + {
16 + $this->setSettings(['app-editor' => 'markdown']);
17 + }
18 +
19 + public function test_default_editor_is_wysiwyg()
20 + {
21 + $this->assertEquals(setting('app-editor'), 'wysiwyg');
22 + $this->asAdmin()->visit($this->page->getUrl() . '/edit')
23 + ->pageHasElement('#html-editor');
24 + }
25 +
26 + public function test_markdown_setting_shows_markdown_editor()
27 + {
28 + $this->setMarkdownEditor();
29 + $this->asAdmin()->visit($this->page->getUrl() . '/edit')
30 + ->pageNotHasElement('#html-editor')
31 + ->pageHasElement('#markdown-editor');
32 + }
33 +
34 + public function test_markdown_content_given_to_editor()
35 + {
36 + $this->setMarkdownEditor();
37 + $mdContent = '# hello. This is a test';
38 + $this->page->markdown = $mdContent;
39 + $this->page->save();
40 + $this->asAdmin()->visit($this->page->getUrl() . '/edit')
41 + ->seeInField('markdown', $mdContent);
42 + }
43 +
44 + public function test_html_content_given_to_editor_if_no_markdown()
45 + {
46 + $this->setMarkdownEditor();
47 + $this->asAdmin()->visit($this->page->getUrl() . '/edit')
48 + ->seeInField('markdown', $this->page->html);
49 + }
50 +
51 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -170,4 +170,28 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase ...@@ -170,4 +170,28 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
170 $this->visit($link->link()->getUri()); 170 $this->visit($link->link()->getUri());
171 return $this; 171 return $this;
172 } 172 }
173 +
174 + /**
175 + * Check if the page contains the given element.
176 + * @param string $selector
177 + * @return bool
178 + */
179 + protected function pageHasElement($selector)
180 + {
181 + $elements = $this->crawler->filter($selector);
182 + $this->assertTrue(count($elements) > 0, "The page does not contain an element matching " . $selector);
183 + return $this;
184 + }
185 +
186 + /**
187 + * Check if the page contains the given element.
188 + * @param string $selector
189 + * @return bool
190 + */
191 + protected function pageNotHasElement($selector)
192 + {
193 + $elements = $this->crawler->filter($selector);
194 + $this->assertFalse(count($elements) > 0, "The page contains " . count($elements) . " elements matching " . $selector);
195 + return $this;
196 + }
173 } 197 }
......