mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Merge pull request #84 from ssddanbrown/markdown_editor
Initial implementation of a markdown editor. Closes #57.
This commit is contained in:
commit
e7d8a041a8
@ -164,6 +164,7 @@ class PageController extends Controller
|
|||||||
$draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
|
$draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
|
||||||
$page->name = $draft->name;
|
$page->name = $draft->name;
|
||||||
$page->html = $draft->html;
|
$page->html = $draft->html;
|
||||||
|
$page->markdown = $draft->markdown;
|
||||||
$page->isDraft = true;
|
$page->isDraft = true;
|
||||||
$warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
|
$warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
|
||||||
}
|
}
|
||||||
@ -204,9 +205,9 @@ class PageController extends Controller
|
|||||||
$page = $this->pageRepo->getById($pageId, true);
|
$page = $this->pageRepo->getById($pageId, true);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
if ($page->draft) {
|
if ($page->draft) {
|
||||||
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html']));
|
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
|
||||||
} else {
|
} else {
|
||||||
$draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html']));
|
$draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||||
}
|
}
|
||||||
$updateTime = $draft->updated_at->format('H:i');
|
$updateTime = $draft->updated_at->format('H:i');
|
||||||
return response()->json(['status' => 'success', 'message' => 'Draft saved at ' . $updateTime]);
|
return response()->json(['status' => 'success', 'message' => 'Draft saved at ' . $updateTime]);
|
||||||
|
@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
|
|
||||||
class Page extends Entity
|
class Page extends Entity
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'html', 'priority'];
|
protected $fillable = ['name', 'html', 'priority', 'markdown'];
|
||||||
|
|
||||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
protected $simpleAttributes = ['name', 'id', 'slug'];
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
|
|
||||||
class PageRevision extends Model
|
class PageRevision extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'html', 'text'];
|
protected $fillable = ['name', 'html', 'text', 'markdown'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user that created the page revision
|
* Get the user that created the page revision
|
||||||
|
@ -312,6 +312,7 @@ class PageRepo extends EntityRepo
|
|||||||
$page->fill($input);
|
$page->fill($input);
|
||||||
$page->html = $this->formatHtml($input['html']);
|
$page->html = $this->formatHtml($input['html']);
|
||||||
$page->text = strip_tags($page->html);
|
$page->text = strip_tags($page->html);
|
||||||
|
if (setting('app-editor') !== 'markdown') $page->markdown = '';
|
||||||
$page->updated_by = $userId;
|
$page->updated_by = $userId;
|
||||||
$page->save();
|
$page->save();
|
||||||
|
|
||||||
@ -348,6 +349,7 @@ class PageRepo extends EntityRepo
|
|||||||
public function saveRevision(Page $page)
|
public function saveRevision(Page $page)
|
||||||
{
|
{
|
||||||
$revision = $this->pageRevision->fill($page->toArray());
|
$revision = $this->pageRevision->fill($page->toArray());
|
||||||
|
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
|
||||||
$revision->page_id = $page->id;
|
$revision->page_id = $page->id;
|
||||||
$revision->slug = $page->slug;
|
$revision->slug = $page->slug;
|
||||||
$revision->book_slug = $page->book->slug;
|
$revision->book_slug = $page->book->slug;
|
||||||
@ -386,6 +388,8 @@ class PageRepo extends EntityRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
$draft->fill($data);
|
$draft->fill($data);
|
||||||
|
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
|
||||||
|
|
||||||
$draft->save();
|
$draft->save();
|
||||||
return $draft;
|
return $draft;
|
||||||
}
|
}
|
||||||
|
@ -44,28 +44,39 @@ class SettingService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a setting value from the cache or database.
|
* Gets a setting value from the cache or database.
|
||||||
|
* Looks at the system defaults if not cached or in database.
|
||||||
* @param $key
|
* @param $key
|
||||||
* @param $default
|
* @param $default
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
protected function getValueFromStore($key, $default)
|
protected function getValueFromStore($key, $default)
|
||||||
{
|
{
|
||||||
|
// Check for an overriding value
|
||||||
$overrideValue = $this->getOverrideValue($key);
|
$overrideValue = $this->getOverrideValue($key);
|
||||||
if ($overrideValue !== null) return $overrideValue;
|
if ($overrideValue !== null) return $overrideValue;
|
||||||
|
|
||||||
|
// Check the cache
|
||||||
$cacheKey = $this->cachePrefix . $key;
|
$cacheKey = $this->cachePrefix . $key;
|
||||||
if ($this->cache->has($cacheKey)) {
|
if ($this->cache->has($cacheKey)) {
|
||||||
return $this->cache->get($cacheKey);
|
return $this->cache->get($cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check the database
|
||||||
$settingObject = $this->getSettingObjectByKey($key);
|
$settingObject = $this->getSettingObjectByKey($key);
|
||||||
|
|
||||||
if ($settingObject !== null) {
|
if ($settingObject !== null) {
|
||||||
$value = $settingObject->value;
|
$value = $settingObject->value;
|
||||||
$this->cache->forever($cacheKey, $value);
|
$this->cache->forever($cacheKey, $value);
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check the defaults set in the app config.
|
||||||
|
$configPrefix = 'setting-defaults.' . $key;
|
||||||
|
if (config()->has($configPrefix)) {
|
||||||
|
$value = config($configPrefix);
|
||||||
|
$this->cache->forever($cacheKey, $value);
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ return [
|
|||||||
|
|
||||||
'env' => env('APP_ENV', 'production'),
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
'editor' => env('APP_EDITOR', 'html'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Debug Mode
|
| Application Debug Mode
|
||||||
|
10
config/setting-defaults.php
Normal file
10
config/setting-defaults.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The defaults for the system settings that are saved in the database.
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
|
||||||
|
'app-editor' => 'wysiwyg'
|
||||||
|
|
||||||
|
];
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddMarkdownSupport extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('pages', function (Blueprint $table) {
|
||||||
|
$table->longText('markdown')->default('');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('page_revisions', function (Blueprint $table) {
|
||||||
|
$table->longText('markdown')->default('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('pages', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('markdown');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('page_revisions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('markdown');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@
|
|||||||
"bootstrap-sass": "^3.0.0",
|
"bootstrap-sass": "^3.0.0",
|
||||||
"dropzone": "^4.0.1",
|
"dropzone": "^4.0.1",
|
||||||
"laravel-elixir": "^3.4.0",
|
"laravel-elixir": "^3.4.0",
|
||||||
|
"marked": "^0.3.5",
|
||||||
"zeroclipboard": "^2.2.0"
|
"zeroclipboard": "^2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
public/fonts/roboto-mono-v4-latin-regular.woff
Normal file
BIN
public/fonts/roboto-mono-v4-latin-regular.woff
Normal file
Binary file not shown.
BIN
public/fonts/roboto-mono-v4-latin-regular.woff2
Normal file
BIN
public/fonts/roboto-mono-v4-latin-regular.woff2
Normal file
Binary file not shown.
@ -45,3 +45,4 @@ These are the great projects used to help build BookStack:
|
|||||||
* [Dropzone.js](http://www.dropzonejs.com/)
|
* [Dropzone.js](http://www.dropzonejs.com/)
|
||||||
* [ZeroClipboard](http://zeroclipboard.org/)
|
* [ZeroClipboard](http://zeroclipboard.org/)
|
||||||
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
|
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
|
||||||
|
* [Marked](https://github.com/chjj/marked)
|
||||||
|
@ -216,16 +216,20 @@ module.exports = function (ngApp, events) {
|
|||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
|
||||||
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', function ($scope, $http, $attrs, $interval, $timeout) {
|
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
|
||||||
|
function ($scope, $http, $attrs, $interval, $timeout, $sce) {
|
||||||
|
|
||||||
$scope.editorOptions = require('./pages/page-form');
|
$scope.editorOptions = require('./pages/page-form');
|
||||||
$scope.editorHtml = '';
|
$scope.editContent = '';
|
||||||
$scope.draftText = '';
|
$scope.draftText = '';
|
||||||
var pageId = Number($attrs.pageId);
|
var pageId = Number($attrs.pageId);
|
||||||
var isEdit = pageId !== 0;
|
var isEdit = pageId !== 0;
|
||||||
var autosaveFrequency = 30; // AutoSave interval in seconds.
|
var autosaveFrequency = 30; // AutoSave interval in seconds.
|
||||||
|
var isMarkdown = $attrs.editorType === 'markdown';
|
||||||
$scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
|
$scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
|
||||||
$scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
|
$scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
|
||||||
|
|
||||||
|
// Set inital header draft text
|
||||||
if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
|
if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
|
||||||
$scope.draftText = 'Editing Draft'
|
$scope.draftText = 'Editing Draft'
|
||||||
} else {
|
} else {
|
||||||
@ -245,7 +249,18 @@ module.exports = function (ngApp, events) {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.editorChange = function () {}
|
// Actions specifically for the markdown editor
|
||||||
|
if (isMarkdown) {
|
||||||
|
$scope.displayContent = '';
|
||||||
|
// Editor change event
|
||||||
|
$scope.editorChange = function (content) {
|
||||||
|
$scope.displayContent = $sce.trustAsHtml(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMarkdown) {
|
||||||
|
$scope.editorChange = function() {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the AutoSave loop, Checks for content change
|
* Start the AutoSave loop, Checks for content change
|
||||||
@ -253,17 +268,18 @@ module.exports = function (ngApp, events) {
|
|||||||
*/
|
*/
|
||||||
function startAutoSave() {
|
function startAutoSave() {
|
||||||
currentContent.title = $('#name').val();
|
currentContent.title = $('#name').val();
|
||||||
currentContent.html = $scope.editorHtml;
|
currentContent.html = $scope.editContent;
|
||||||
|
|
||||||
autoSave = $interval(() => {
|
autoSave = $interval(() => {
|
||||||
var newTitle = $('#name').val();
|
var newTitle = $('#name').val();
|
||||||
var newHtml = $scope.editorHtml;
|
var newHtml = $scope.editContent;
|
||||||
|
|
||||||
if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
|
if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
|
||||||
currentContent.html = newHtml;
|
currentContent.html = newHtml;
|
||||||
currentContent.title = newTitle;
|
currentContent.title = newTitle;
|
||||||
saveDraft(newTitle, newHtml);
|
saveDraft();
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 1000 * autosaveFrequency);
|
}, 1000 * autosaveFrequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,20 +288,22 @@ module.exports = function (ngApp, events) {
|
|||||||
* @param title
|
* @param title
|
||||||
* @param html
|
* @param html
|
||||||
*/
|
*/
|
||||||
function saveDraft(title, html) {
|
function saveDraft() {
|
||||||
$http.put('/ajax/page/' + pageId + '/save-draft', {
|
var data = {
|
||||||
name: title,
|
name: $('#name').val(),
|
||||||
html: html
|
html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
|
||||||
}).then((responseData) => {
|
};
|
||||||
|
|
||||||
|
if (isMarkdown) data.markdown = $scope.editContent;
|
||||||
|
|
||||||
|
$http.put('/ajax/page/' + pageId + '/save-draft', data).then((responseData) => {
|
||||||
$scope.draftText = responseData.data.message;
|
$scope.draftText = responseData.data.message;
|
||||||
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
|
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.forceDraftSave = function() {
|
$scope.forceDraftSave = function() {
|
||||||
var newTitle = $('#name').val();
|
saveDraft();
|
||||||
var newHtml = $scope.editorHtml;
|
|
||||||
saveDraft(newTitle, newHtml);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -298,6 +316,7 @@ module.exports = function (ngApp, events) {
|
|||||||
$scope.draftText = 'Editing Page';
|
$scope.draftText = 'Editing Page';
|
||||||
$scope.isUpdateDraft = false;
|
$scope.isUpdateDraft = false;
|
||||||
$scope.$broadcast('html-update', responseData.data.html);
|
$scope.$broadcast('html-update', responseData.data.html);
|
||||||
|
$scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html);
|
||||||
$('#name').val(responseData.data.name);
|
$('#name').val(responseData.data.name);
|
||||||
$timeout(() => {
|
$timeout(() => {
|
||||||
startAutoSave();
|
startAutoSave();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
var DropZone = require('dropzone');
|
var DropZone = require('dropzone');
|
||||||
|
var markdown = require('marked');
|
||||||
|
|
||||||
var toggleSwitchTemplate = require('./components/toggle-switch.html');
|
var toggleSwitchTemplate = require('./components/toggle-switch.html');
|
||||||
var imagePickerTemplate = require('./components/image-picker.html');
|
var imagePickerTemplate = require('./components/image-picker.html');
|
||||||
@ -200,7 +201,82 @@ module.exports = function (ngApp, events) {
|
|||||||
tinymce.init(scope.tinymce);
|
tinymce.init(scope.tinymce);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
ngApp.directive('markdownInput', ['$timeout', function($timeout) {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
scope: {
|
||||||
|
mdModel: '=',
|
||||||
|
mdChange: '='
|
||||||
|
},
|
||||||
|
link: function (scope, element, attrs) {
|
||||||
|
|
||||||
|
// Set initial model content
|
||||||
|
var content = element.val();
|
||||||
|
scope.mdModel = content;
|
||||||
|
scope.mdChange(markdown(content));
|
||||||
|
|
||||||
|
element.on('change input', (e) => {
|
||||||
|
content = element.val();
|
||||||
|
$timeout(() => {
|
||||||
|
scope.mdModel = content;
|
||||||
|
scope.mdChange(markdown(content));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.$on('markdown-update', (event, value) => {
|
||||||
|
element.val(value);
|
||||||
|
scope.mdModel= value;
|
||||||
|
scope.mdChange(markdown(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
ngApp.directive('markdownEditor', ['$timeout', function($timeout) {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
link: function (scope, element, attrs) {
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
var input = element.find('textarea[markdown-input]');
|
||||||
|
var insertImage = element.find('button[data-action="insertImage"]');
|
||||||
|
|
||||||
|
var currentCaretPos = 0;
|
||||||
|
|
||||||
|
input.blur((event) => {
|
||||||
|
currentCaretPos = input[0].selectionStart;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert image shortcut
|
||||||
|
input.keydown((event) => {
|
||||||
|
if (event.which === 73 && event.ctrlKey && event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
var caretPos = input[0].selectionStart;
|
||||||
|
var currentContent = input.val();
|
||||||
|
var mdImageText = "![](http://)";
|
||||||
|
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
|
||||||
|
input.focus();
|
||||||
|
input[0].selectionStart = caretPos + ("![](".length);
|
||||||
|
input[0].selectionEnd = caretPos + ('![](http://'.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert image from image manager
|
||||||
|
insertImage.click((event) => {
|
||||||
|
window.ImageManager.showExternal((image) => {
|
||||||
|
var caretPos = currentCaretPos;
|
||||||
|
var currentContent = input.val();
|
||||||
|
var mdImageText = "![" + image.name + "](" + image.url + ")";
|
||||||
|
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
|
||||||
|
input.change();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}])
|
}])
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
@ -93,4 +93,15 @@
|
|||||||
url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg');
|
url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* roboto-mono-regular - latin */
|
||||||
|
// https://google-webfonts-helper.herokuapp.com
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Roboto Mono'), local('RobotoMono-Regular'),
|
||||||
|
url('/fonts/roboto-mono-v4-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
|
||||||
|
url('/fonts/roboto-mono-v4-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
}
|
}
|
@ -26,6 +26,59 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#markdown-editor {
|
||||||
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
|
textarea {
|
||||||
|
font-family: 'Roboto Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: $-xs $-m;
|
||||||
|
color: #444;
|
||||||
|
border-radius: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
flex: 1;
|
||||||
|
border: 0;
|
||||||
|
width: 100%;
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.markdown-display, .markdown-editor-wrap {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.markdown-editor-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.markdown-display {
|
||||||
|
padding: 0 $-m 0;
|
||||||
|
margin-left: -1px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
.page-content {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.editor-toolbar {
|
||||||
|
width: 100%;
|
||||||
|
padding: $-xs $-m;
|
||||||
|
font-family: 'Roboto Mono';
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.6;
|
||||||
|
border-bottom: 1px solid #DDD;
|
||||||
|
background-color: #EEE;
|
||||||
|
flex: none;
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
@ -160,6 +213,10 @@ input:checked + .toggle-switch {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div[editor-type="markdown"] .title-input.page-title input[type="text"] {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.search-box {
|
.search-box {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -157,6 +157,12 @@ span.code {
|
|||||||
@extend .code-base;
|
@extend .code-base;
|
||||||
padding: 1px $-xs;
|
padding: 1px $-xs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
* Text colors
|
* Text colors
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
<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 }}">
|
<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 }}">
|
||||||
|
|
||||||
{{ csrf_field() }}
|
{{ csrf_field() }}
|
||||||
<div class="faded-small toolbar">
|
<div class="faded-small toolbar">
|
||||||
@ -42,10 +42,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-area flex-fill flex">
|
<div class="edit-area flex-fill flex">
|
||||||
<textarea id="html-editor" tinymce="editorOptions" mce-change="editorChange" mce-model="editorHtml" name="html" rows="5"
|
@if(setting('app-editor') === 'wysiwyg')
|
||||||
@if($errors->has('html')) class="neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
|
<textarea id="html-editor" tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" name="html" rows="5"
|
||||||
@if($errors->has('html'))
|
@if($errors->has('html')) class="neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
|
||||||
<div class="text-neg text-small">{{ $errors->first('html') }}</div>
|
@if($errors->has('html'))
|
||||||
|
<div class="text-neg text-small">{{ $errors->first('html') }}</div>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(setting('app-editor') === 'markdown')
|
||||||
|
<div id="markdown-editor" markdown-editor class="flex-fill flex">
|
||||||
|
|
||||||
|
<div class="markdown-editor-wrap">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<span class="float left">Editor</span>
|
||||||
|
<div class="float right buttons">
|
||||||
|
<button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea markdown-input md-change="editorChange" md-model="editContent" name="markdown" rows="5"
|
||||||
|
@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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="markdown-editor-wrap">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<div class="">Preview</div>
|
||||||
|
</div>
|
||||||
|
<div class="markdown-display">
|
||||||
|
<div class="page-content" ng-bind-html="displayContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="html" ng-value="displayContent">
|
||||||
|
|
||||||
|
@if($errors->has('markdown'))
|
||||||
|
<div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -17,29 +17,37 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="setting-app-name">Application name</label>
|
<label for="setting-app-name">Application name</label>
|
||||||
<input type="text" value="{{ Setting::get('app-name', 'BookStack') }}" name="setting-app-name" id="setting-app-name">
|
<input type="text" value="{{ setting('app-name', 'BookStack') }}" name="setting-app-name" id="setting-app-name">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Allow public viewing?</label>
|
<label>Allow public viewing?</label>
|
||||||
<toggle-switch name="setting-app-public" value="{{ Setting::get('app-public') }}"></toggle-switch>
|
<toggle-switch name="setting-app-public" value="{{ setting('app-public') }}"></toggle-switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Enable higher security image uploads?</label>
|
<label>Enable higher security image uploads?</label>
|
||||||
<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>
|
<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>
|
||||||
<toggle-switch name="setting-app-secure-images" value="{{ Setting::get('app-secure-images') }}"></toggle-switch>
|
<toggle-switch name="setting-app-secure-images" value="{{ setting('app-secure-images') }}"></toggle-switch>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="setting-app-editor">Page Editor</label>
|
||||||
|
<p class="small">Select which editor will be used by all users to edit pages.</p>
|
||||||
|
<select name="setting-app-editor" id="setting-app-editor">
|
||||||
|
<option @if(setting('app-editor') === 'wysiwyg') selected @endif value="wysiwyg">WYSIWYG</option>
|
||||||
|
<option @if(setting('app-editor') === 'markdown') selected @endif value="markdown">Markdown</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group" id="logo-control">
|
<div class="form-group" id="logo-control">
|
||||||
<label for="setting-app-logo">Application Logo</label>
|
<label for="setting-app-logo">Application Logo</label>
|
||||||
<p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p>
|
<p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p>
|
||||||
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="color-control">
|
<div class="form-group" id="color-control">
|
||||||
<label for="setting-app-color">Application Primary Color</label>
|
<label for="setting-app-color">Application Primary Color</label>
|
||||||
<p class="small">This should be a hex value. <br> Leave empty to reset to the default color.</p>
|
<p class="small">This should be a hex value. <br> Leave empty to reset to the default color.</p>
|
||||||
<input type="text" value="{{ Setting::get('app-color', '') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
|
<input type="text" value="{{ setting('app-color', '') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
|
||||||
<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)">
|
<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)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -53,14 +61,14 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="setting-registration-enabled">Allow registration?</label>
|
<label for="setting-registration-enabled">Allow registration?</label>
|
||||||
<toggle-switch name="setting-registration-enabled" value="{{ Setting::get('registration-enabled') }}"></toggle-switch>
|
<toggle-switch name="setting-registration-enabled" value="{{ setting('registration-enabled') }}"></toggle-switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="setting-registration-role">Default user role after registration</label>
|
<label for="setting-registration-role">Default user role after registration</label>
|
||||||
<select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
|
<select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
|
||||||
@foreach(\BookStack\Role::all() as $role)
|
@foreach(\BookStack\Role::all() as $role)
|
||||||
<option value="{{$role->id}}"
|
<option value="{{$role->id}}"
|
||||||
@if(\Setting::get('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
|
@if(setting('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
|
||||||
>
|
>
|
||||||
{{ $role->display_name }}
|
{{ $role->display_name }}
|
||||||
</option>
|
</option>
|
||||||
@ -70,7 +78,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="setting-registration-confirmation">Require email confirmation?</label>
|
<label for="setting-registration-confirmation">Require email confirmation?</label>
|
||||||
<p class="small">If domain restriction is used then email confirmation will be required and the below value will be ignored.</p>
|
<p class="small">If domain restriction is used then email confirmation will be required and the below value will be ignored.</p>
|
||||||
<toggle-switch name="setting-registration-confirmation" value="{{ Setting::get('registration-confirmation') }}"></toggle-switch>
|
<toggle-switch name="setting-registration-confirmation" value="{{ setting('registration-confirmation') }}"></toggle-switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -78,7 +86,7 @@
|
|||||||
<label for="setting-registration-restrict">Restrict registration to domain</label>
|
<label for="setting-registration-restrict">Restrict registration to domain</label>
|
||||||
<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.
|
<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.
|
||||||
<br> Note that users will be able to change their email addresses after successful registration.</p>
|
<br> Note that users will be able to change their email addresses after successful registration.</p>
|
||||||
<input type="text" id="setting-registration-restrict" name="setting-registration-restrict" placeholder="No restriction set" value="{{ Setting::get('registration-restrict', '') }}">
|
<input type="text" id="setting-registration-restrict" name="setting-registration-restrict" placeholder="No restriction set" value="{{ setting('registration-restrict', '') }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
51
tests/Entity/MarkdownTest.php
Normal file
51
tests/Entity/MarkdownTest.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownTest extends TestCase
|
||||||
|
{
|
||||||
|
protected $page;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->page = \BookStack\Page::first();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setMarkdownEditor()
|
||||||
|
{
|
||||||
|
$this->setSettings(['app-editor' => 'markdown']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_default_editor_is_wysiwyg()
|
||||||
|
{
|
||||||
|
$this->assertEquals(setting('app-editor'), 'wysiwyg');
|
||||||
|
$this->asAdmin()->visit($this->page->getUrl() . '/edit')
|
||||||
|
->pageHasElement('#html-editor');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_markdown_setting_shows_markdown_editor()
|
||||||
|
{
|
||||||
|
$this->setMarkdownEditor();
|
||||||
|
$this->asAdmin()->visit($this->page->getUrl() . '/edit')
|
||||||
|
->pageNotHasElement('#html-editor')
|
||||||
|
->pageHasElement('#markdown-editor');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_markdown_content_given_to_editor()
|
||||||
|
{
|
||||||
|
$this->setMarkdownEditor();
|
||||||
|
$mdContent = '# hello. This is a test';
|
||||||
|
$this->page->markdown = $mdContent;
|
||||||
|
$this->page->save();
|
||||||
|
$this->asAdmin()->visit($this->page->getUrl() . '/edit')
|
||||||
|
->seeInField('markdown', $mdContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_html_content_given_to_editor_if_no_markdown()
|
||||||
|
{
|
||||||
|
$this->setMarkdownEditor();
|
||||||
|
$this->asAdmin()->visit($this->page->getUrl() . '/edit')
|
||||||
|
->seeInField('markdown', $this->page->html);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -170,4 +170,28 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
|
|||||||
$this->visit($link->link()->getUri());
|
$this->visit($link->link()->getUri());
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the page contains the given element.
|
||||||
|
* @param string $selector
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function pageHasElement($selector)
|
||||||
|
{
|
||||||
|
$elements = $this->crawler->filter($selector);
|
||||||
|
$this->assertTrue(count($elements) > 0, "The page does not contain an element matching " . $selector);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the page contains the given element.
|
||||||
|
* @param string $selector
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function pageNotHasElement($selector)
|
||||||
|
{
|
||||||
|
$elements = $this->crawler->filter($selector);
|
||||||
|
$this->assertFalse(count($elements) > 0, "The page contains " . count($elements) . " elements matching " . $selector);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user