Compare commits

...

25 Commits

Author SHA1 Message Date
Dan Brown
e90da18ada Updated assets and version for v0.18.2 release 2017-10-01 18:12:59 +01:00
Dan Brown
a08d80e1cc Merge branch 'master' into release 2017-10-01 18:12:07 +01:00
Dan Brown
b711bc6816 Prevented 'Discard draft' option showing after saving a draft page 2017-10-01 18:11:24 +01:00
Dan Brown
247e6dba85 Fixed some design issues around cards
Reverted drop shadow change.
Fixed header line-height when linked.
Fixed overflowing paragraph text. Fixes #533.
2017-10-01 17:59:51 +01:00
Dan Brown
2b3d6e4e4a Updated search-regen command description 2017-10-01 17:51:59 +01:00
Dan Brown
6b1980c4f3 Merge branch 'master' of github.com:BookStackApp/BookStack 2017-10-01 13:19:41 +01:00
Dan Brown
9ba29770e1 Added azureAD social auth option
Closes #509
2017-10-01 13:19:17 +01:00
Dan Brown
3d375fae55 Merge pull request #529 from cipi1965/master
Updated italian translation
2017-10-01 11:44:45 +01:00
Dan Brown
c99a50de2c Merge pull request #528 from turbotankist/master
russian lang fixes
2017-10-01 11:43:31 +01:00
Dan Brown
1a32b25b5e Merge pull request #523 from sanderdw/master
Update dutch translations
2017-10-01 11:33:22 +01:00
Dan Brown
481aa5b5b0 Added 'last_commented' sort option to search
Closes #440
2017-10-01 11:24:33 +01:00
Dan Brown
c943eb4d0d Removed empty string null middleware as was causing issues 2017-09-30 14:44:52 +01:00
Dan Brown
aca6de49b0 Added missing middleware to trim input 2017-09-30 14:31:27 +01:00
Dan Brown
5fd04fa470 Updated search indexer to split words better
Will now split up words based on more chars than just spaces.
Not takes into account newlines, tabs, periods & commas.

Fixed #531
2017-09-30 14:14:23 +01:00
Dan Brown
87339e4cd0 Added missing codemirror theme class
Fixes #535
2017-09-30 13:48:38 +01:00
Dan Brown
a9eb058dad Updated issue template 2017-09-30 13:41:06 +01:00
Dan Brown
61fad6a665 Finished migration of last angular code 2017-09-30 13:27:08 +01:00
Matteo Piccina
fa4bee2d98 Updated italian translation 2017-09-27 10:44:08 +02:00
alexey
ce63260fa6 russian lang fixes 2017-09-27 11:17:56 +03:00
Dan Brown
a3557d5bb2 Tweaked shadows on cards 2017-09-24 18:47:34 +01:00
Dan Brown
9ca22976c3 Migrated editor toolbox, No more directives! 2017-09-24 18:30:21 +01:00
Dan Brown
9e2934fe17 Migrated editor inputs to non-angular JS 2017-09-23 12:24:06 +01:00
sanderdw
2259263214 Update entities.php 2017-09-23 00:52:08 +02:00
sanderdw
762cf5f183 Update components.php 2017-09-23 00:47:02 +02:00
sanderdw
07175f2b3e Update dutch translations 2017-09-23 00:28:25 +02:00
44 changed files with 953 additions and 859 deletions

View File

@@ -4,10 +4,18 @@ Desired Feature:
### For Bug Reports
* BookStack Version:
* BookStack Version *(Found in settings, Please don't put 'latest')*:
* PHP Version:
* MySQL Version:
##### Expected Behavior
##### Actual Behavior
##### Current Behavior
##### Steps to Reproduce

View File

@@ -19,7 +19,7 @@ class RegenerateSearch extends Command
*
* @var string
*/
protected $description = 'Command description';
protected $description = 'Re-index all content for searching';
protected $searchService;

View File

@@ -13,8 +13,8 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\BookStack\Http\Middleware\TrimStrings::class,
];
/**
@@ -26,6 +26,8 @@ class Kernel extends HttpKernel
'web' => [
\BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\BookStack\Http\Middleware\Localization::class
@@ -42,7 +44,7 @@ class Kernel extends HttpKernel
* @var array
*/
protected $routeMiddleware = [
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'auth' => \BookStack\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,

View File

@@ -0,0 +1,19 @@
<?php
namespace BookStack\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as BaseTrimmer;
class TrimStrings extends BaseTrimmer
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'password',
'password_confirmation',
'password-confirm',
];
}

View File

@@ -16,6 +16,7 @@ class EventServiceProvider extends ServiceProvider
protected $listen = [
SocialiteWasCalled::class => [
'SocialiteProviders\Slack\SlackExtendSocialite@handle',
'SocialiteProviders\Azure\AzureExtendSocialite@handle',
],
];

View File

@@ -382,11 +382,13 @@ class SearchService
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
{
$tokenMap = []; // {TextToken => OccurrenceCount}
$splitText = explode(' ', $text);
foreach ($splitText as $token) {
if ($token === '') continue;
$splitChars = " \n\t.,";
$token = strtok($text, $splitChars);
while ($token !== false) {
if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
$tokenMap[$token]++;
$token = strtok($splitChars);
}
$terms = [];
@@ -479,4 +481,23 @@ class SearchService
});
}
protected function filterSortBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$functionName = camel_case('sort_by_' . $input);
if (method_exists($this, $functionName)) $this->$functionName($query, $model);
}
/**
* Sorting filter options
*/
protected function sortByLastCommented(\Illuminate\Database\Eloquent\Builder $query, Entity $model)
{
$commentsTable = $this->db->getTablePrefix() . 'comments';
$morphClass = str_replace('\\', '\\\\', $model->getMorphClass());
$commentQuery = $this->db->raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM '.$commentsTable.' c1 LEFT JOIN '.$commentsTable.' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \''. $morphClass .'\' AND c2.created_at IS NULL) as comments');
$query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc');
}
}

View File

@@ -14,7 +14,7 @@ class SocialAuthService
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter'];
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure'];
/**
* SocialAuthService constructor.

View File

@@ -18,7 +18,8 @@
"gathercontent/htmldiff": "^0.2.1",
"barryvdh/laravel-snappy": "^0.3.1",
"laravel/browser-kit-testing": "^1.0",
"socialiteproviders/slack": "^3.0"
"socialiteproviders/slack": "^3.0",
"socialiteproviders/microsoft-azure": "^3.0"
},
"require-dev": {
"fzaninotto/faker": "~1.4",

41
composer.lock generated
View File

@@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "e6d32752d02dae662bedc69fa5856feb",
"content-hash": "5f0f4e912f1207e761caf9344f2308a0",
"hash": "aa5f3333b909857a179e6aa9c30ab9ab",
"content-hash": "dc4c98aa8942f27fde6a9faa440e1a74",
"packages": [
{
"name": "aws/aws-sdk-php",
@@ -2073,6 +2073,43 @@
"description": "Easily add new or override built-in providers in Laravel Socialite.",
"time": "2017-02-07 07:26:42"
},
{
"name": "socialiteproviders/microsoft-azure",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Microsoft-Azure.git",
"reference": "d7a703a782eb9f7eae0db803beaa3ddec19ef372"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Microsoft-Azure/zipball/d7a703a782eb9f7eae0db803beaa3ddec19ef372",
"reference": "d7a703a782eb9f7eae0db803beaa3ddec19ef372",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0",
"socialiteproviders/manager": "~3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"SocialiteProviders\\Azure\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Chris Hemmings",
"email": "chris@hemmin.gs"
}
],
"description": "Microsoft Azure OAuth2 Provider for Laravel Socialite",
"time": "2017-01-25 09:48:29"
},
{
"name": "socialiteproviders/slack",
"version": "v3.0.1",

View File

@@ -72,6 +72,14 @@ return [
'name' => 'Twitter',
],
'azure' => [
'client_id' => env('AZURE_APP_ID', false),
'client_secret' => env('AZURE_APP_SECRET', false),
'tenant' => env('AZURE_TENANT', false),
'redirect' => env('APP_URL') . '/login/service/azure/callback',
'name' => 'Microsoft Azure',
],
'ldap' => [
'server' => env('LDAP_SERVER', false),
'dn' => env('LDAP_DN', false),

View File

@@ -25,11 +25,6 @@
"yargs": "^7.1.0"
},
"dependencies": {
"angular": "^1.5.5",
"angular-animate": "^1.5.5",
"angular-resource": "^1.5.5",
"angular-sanitize": "^1.5.5",
"angular-ui-sortable": "^0.17.0",
"axios": "^0.16.1",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.1",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -76,7 +76,6 @@ The BookStack source is provided under the MIT License.
These are the great open-source projects used to help build BookStack:
* [Laravel](http://laravel.com/)
* [AngularJS](https://angularjs.org/)
* [jQuery](https://jquery.com/)
* [TinyMCE](https://www.tinymce.com/)
* [CodeMirror](https://codemirror.net)

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129 129"><path fill="#f25022" d="M0 0h61.3v61.3H0z"/><path fill="#7fba00" d="M67.7 0H129v61.3H67.7z"/><path fill="#00a4ef" d="M0 67.7h61.3V129H0z"/><path fill="#ffb900" d="M67.7 67.7H129V129H67.7z"/></svg>

After

Width:  |  Height:  |  Size: 258 B

View File

@@ -0,0 +1,47 @@
class EditorToolbox {
constructor(elem) {
// Elements
this.elem = elem;
this.buttons = elem.querySelectorAll('[toolbox-tab-button]');
this.contentElements = elem.querySelectorAll('[toolbox-tab-content]');
this.toggleButton = elem.querySelector('[toolbox-toggle]');
// Toolbox toggle button click
this.toggleButton.addEventListener('click', this.toggle.bind(this));
// Tab button click
this.elem.addEventListener('click', event => {
let button = event.target.closest('[toolbox-tab-button]');
if (button === null) return;
let name = button.getAttribute('toolbox-tab-button');
this.setActiveTab(name, true);
});
// Set the first tab as active on load
this.setActiveTab(this.contentElements[0].getAttribute('toolbox-tab-content'));
}
toggle() {
this.elem.classList.toggle('open');
}
setActiveTab(tabName, openToolbox = false) {
// Set button visibility
for (let i = 0, len = this.buttons.length; i < len; i++) {
this.buttons[i].classList.remove('active');
let bName = this.buttons[i].getAttribute('toolbox-tab-button');
if (bName === tabName) this.buttons[i].classList.add('active');
}
// Set content visibility
for (let i = 0, len = this.contentElements.length; i < len; i++) {
this.contentElements[i].style.display = 'none';
let cName = this.contentElements[i].getAttribute('toolbox-tab-content');
if (cName === tabName) this.contentElements[i].style.display = 'block';
}
if (openToolbox) this.elem.classList.add('open');
}
}
module.exports = EditorToolbox;

View File

@@ -11,6 +11,9 @@ let componentMapping = {
'sidebar': require('./sidebar'),
'page-picker': require('./page-picker'),
'page-comments': require('./page-comments'),
'wysiwyg-editor': require('./wysiwyg-editor'),
'markdown-editor': require('./markdown-editor'),
'editor-toolbox': require('./editor-toolbox'),
};
window.components = {};

View File

@@ -0,0 +1,293 @@
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
const code = require('../code');
class MarkdownEditor {
constructor(elem) {
this.elem = elem;
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});
this.display = this.elem.querySelector('.markdown-display');
this.input = this.elem.querySelector('textarea');
this.htmlInput = this.elem.querySelector('input[name=html]');
this.cm = code.markdownEditor(this.input);
this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
this.init();
}
init() {
// Prevent markdown display link click redirect
this.display.addEventListener('click', event => {
let link = event.target.closest('a');
if (link === null) return;
event.preventDefault();
window.open(link.getAttribute('href'));
});
// Button actions
this.elem.addEventListener('click', event => {
let button = event.target.closest('button[data-action]');
if (button === null) return;
let action = button.getAttribute('data-action');
if (action === 'insertImage') this.actionInsertImage();
if (action === 'insertLink') this.actionShowLinkSelector();
});
window.$events.listen('editor-markdown-update', value => {
this.cm.setValue(value);
this.updateAndRender();
});
this.codeMirrorSetup();
}
// Update the input content and render the display.
updateAndRender() {
let content = this.cm.getValue();
this.input.value = content;
let html = this.markdown.render(content);
window.$events.emit('editor-html-change', html);
window.$events.emit('editor-markdown-change', content);
this.display.innerHTML = html;
this.htmlInput.value = html;
}
onMarkdownScroll(lineCount) {
let elems = this.display.children;
if (elems.length <= lineCount) return;
let topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
// TODO - Replace jQuery
$(this.display).animate({
scrollTop: topElem.offsetTop
}, {queue: false, duration: 200, easing: 'linear'});
}
codeMirrorSetup() {
let cm = this.cm;
// Custom key commands
let metaKey = code.getMetaKey();
const extraKeys = {};
// Insert Image shortcut
extraKeys[`${metaKey}-Alt-I`] = function(cm) {
let selectedText = cm.getSelection();
let newText = `![${selectedText}](http://)`;
let cursorPos = cm.getCursor('from');
cm.replaceSelection(newText);
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length -1);
};
// Save draft
extraKeys[`${metaKey}-S`] = cm => {window.$events.emit('editor-save-draft')};
// Show link selector
extraKeys[`Shift-${metaKey}-K`] = cm => {this.actionShowLinkSelector()};
// Insert Link
extraKeys[`${metaKey}-K`] = cm => {insertLink()};
// FormatShortcuts
extraKeys[`${metaKey}-1`] = cm => {replaceLineStart('##');};
extraKeys[`${metaKey}-2`] = cm => {replaceLineStart('###');};
extraKeys[`${metaKey}-3`] = cm => {replaceLineStart('####');};
extraKeys[`${metaKey}-4`] = cm => {replaceLineStart('#####');};
extraKeys[`${metaKey}-5`] = cm => {replaceLineStart('');};
extraKeys[`${metaKey}-d`] = cm => {replaceLineStart('');};
extraKeys[`${metaKey}-6`] = cm => {replaceLineStart('>');};
extraKeys[`${metaKey}-q`] = cm => {replaceLineStart('>');};
extraKeys[`${metaKey}-7`] = cm => {wrapSelection('\n```\n', '\n```');};
extraKeys[`${metaKey}-8`] = cm => {wrapSelection('`', '`');};
extraKeys[`Shift-${metaKey}-E`] = cm => {wrapSelection('`', '`');};
extraKeys[`${metaKey}-9`] = cm => {wrapSelection('<p class="callout info">', '</p>');};
cm.setOption('extraKeys', extraKeys);
// Update data on content change
cm.on('change', (instance, changeObj) => {
this.updateAndRender();
});
// Handle scroll to sync display view
cm.on('scroll', instance => {
// Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
let scroll = instance.getScrollInfo();
let atEnd = scroll.top + scroll.clientHeight === scroll.height;
if (atEnd) {
this.onMarkdownScroll(-1);
return;
}
let lineNum = instance.lineAtHeight(scroll.top, 'local');
let range = instance.getRange({line: 0, ch: null}, {line: lineNum, ch: null});
let parser = new DOMParser();
let doc = parser.parseFromString(this.markdown.render(range), 'text/html');
let totalLines = doc.documentElement.querySelectorAll('body > *');
this.onMarkdownScroll(totalLines.length);
});
// Handle image paste
cm.on('paste', (cm, event) => {
if (!event.clipboardData || !event.clipboardData.items) return;
for (let i = 0; i < event.clipboardData.items.length; i++) {
uploadImage(event.clipboardData.items[i].getAsFile());
}
});
// Handle images on drag-drop
cm.on('drop', (cm, event) => {
event.stopPropagation();
event.preventDefault();
let cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
cm.setCursor(cursorPos);
if (!event.dataTransfer || !event.dataTransfer.files) return;
for (let i = 0; i < event.dataTransfer.files.length; i++) {
uploadImage(event.dataTransfer.files[i]);
}
});
// Helper to replace editor content
function replaceContent(search, replace) {
let text = cm.getValue();
let cursor = cm.listSelections();
cm.setValue(text.replace(search, replace));
cm.setSelections(cursor);
}
// Helper to replace the start of the line
function replaceLineStart(newStart) {
let cursor = cm.getCursor();
let lineContent = cm.getLine(cursor.line);
let lineLen = lineContent.length;
let lineStart = lineContent.split(' ')[0];
// Remove symbol if already set
if (lineStart === newStart) {
lineContent = lineContent.replace(`${newStart} `, '');
cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)});
return;
}
let alreadySymbol = /^[#>`]/.test(lineStart);
let posDif = 0;
if (alreadySymbol) {
posDif = newStart.length - lineStart.length;
lineContent = lineContent.replace(lineStart, newStart).trim();
} else if (newStart !== '') {
posDif = newStart.length + 1;
lineContent = newStart + ' ' + lineContent;
}
cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch + posDif});
}
function wrapLine(start, end) {
let cursor = cm.getCursor();
let lineContent = cm.getLine(cursor.line);
let lineLen = lineContent.length;
let newLineContent = lineContent;
if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
} else {
newLineContent = `${start}${lineContent}${end}`;
}
cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch + start.length});
}
function wrapSelection(start, end) {
let selection = cm.getSelection();
if (selection === '') return wrapLine(start, end);
let newSelection = selection;
let frontDiff = 0;
let endDiff = 0;
if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
newSelection = selection.slice(start.length, selection.length - end.length);
endDiff = -(end.length + start.length);
} else {
newSelection = `${start}${selection}${end}`;
endDiff = start.length + end.length;
}
let selections = cm.listSelections()[0];
cm.replaceSelection(newSelection);
let headFirst = selections.head.ch <= selections.anchor.ch;
selections.head.ch += headFirst ? frontDiff : endDiff;
selections.anchor.ch += headFirst ? endDiff : frontDiff;
cm.setSelections([selections]);
}
// Handle image upload and add image into markdown content
function uploadImage(file) {
if (file === null || file.type.indexOf('image') !== 0) return;
let ext = 'png';
if (file.name) {
let fileNameMatches = file.name.match(/\.(.+)$/);
if (fileNameMatches.length > 1) ext = fileNameMatches[1];
}
// Insert image into markdown
let id = "image-" + Math.random().toString(16).slice(2);
let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
let selectedText = cm.getSelection();
let placeHolderText = `![${selectedText}](${placeholderImage})`;
cm.replaceSelection(placeHolderText);
let remoteFilename = "image-" + Date.now() + "." + ext;
let formData = new FormData();
formData.append('file', file, remoteFilename);
window.$http.post('/images/gallery/upload', formData).then(resp => {
replaceContent(placeholderImage, resp.data.thumbs.display);
}).catch(err => {
events.emit('error', trans('errors.image_upload_error'));
replaceContent(placeHolderText, selectedText);
console.log(err);
});
}
function insertLink() {
let cursorPos = cm.getCursor('from');
let selectedText = cm.getSelection() || '';
let newText = `[${selectedText}]()`;
cm.focus();
cm.replaceSelection(newText);
let cursorPosDiff = (selectedText === '') ? -3 : -1;
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff);
}
this.updateAndRender();
}
actionInsertImage() {
let cursorPos = this.cm.getCursor('from');
window.ImageManager.show(image => {
let selectedText = this.cm.getSelection();
let newText = "![" + (selectedText || image.name) + "](" + image.thumbs.display + ")";
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
});
}
// Show the popup link selector and insert a link when finished
actionShowLinkSelector() {
let cursorPos = this.cm.getCursor('from');
window.EntitySelectorPopup.show(entity => {
let selectedText = this.cm.getSelection() || entity.name;
let newText = `[${selectedText}](${entity.link})`;
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
});
}
}
module.exports = MarkdownEditor ;

View File

@@ -0,0 +1,11 @@
class WysiwygEditor {
constructor(elem) {
this.elem = elem;
this.options = require("../pages/page-form");
tinymce.init(this.options);
}
}
module.exports = WysiwygEditor;

View File

@@ -1,147 +0,0 @@
"use strict";
const moment = require('moment');
require('moment/locale/en-gb');
const editorOptions = require("./pages/page-form");
moment.locale('en-gb');
module.exports = function (ngApp, events) {
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
function ($scope, $http, $attrs, $interval, $timeout, $sce) {
$scope.editorOptions = editorOptions();
$scope.editContent = '';
$scope.draftText = '';
let pageId = Number($attrs.pageId);
let isEdit = pageId !== 0;
let autosaveFrequency = 30; // AutoSave interval in seconds.
let isMarkdown = $attrs.editorType === 'markdown';
$scope.draftsEnabled = $attrs.draftsEnabled === 'true';
$scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
$scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
// Set initial header draft text
if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
$scope.draftText = trans('entities.pages_editing_draft');
} else {
$scope.draftText = trans('entities.pages_editing_page');
}
let autoSave = false;
let currentContent = {
title: false,
html: false
};
if (isEdit && $scope.draftsEnabled) {
setTimeout(() => {
startAutoSave();
}, 1000);
}
// 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() {};
}
let lastSave = 0;
/**
* Start the AutoSave loop, Checks for content change
* before performing the costly AJAX request.
*/
function startAutoSave() {
currentContent.title = $('#name').val();
currentContent.html = $scope.editContent;
autoSave = $interval(() => {
// Return if manually saved recently to prevent bombarding the server
if (Date.now() - lastSave < (1000*autosaveFrequency)/2) return;
let newTitle = $('#name').val();
let newHtml = $scope.editContent;
if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
currentContent.html = newHtml;
currentContent.title = newTitle;
saveDraft();
}
}, 1000 * autosaveFrequency);
}
let draftErroring = false;
/**
* Save a draft update into the system via an AJAX request.
*/
function saveDraft() {
if (!$scope.draftsEnabled) return;
let data = {
name: $('#name').val(),
html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
};
if (isMarkdown) data.markdown = $scope.editContent;
let url = window.baseUrl('/ajax/page/' + pageId + '/save-draft');
$http.put(url, data).then(responseData => {
draftErroring = false;
let updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
$scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
showDraftSaveNotification();
lastSave = Date.now();
}, errorRes => {
if (draftErroring) return;
events.emit('error', trans('errors.page_draft_autosave_fail'));
draftErroring = true;
});
}
function showDraftSaveNotification() {
$scope.draftUpdated = true;
$timeout(() => {
$scope.draftUpdated = false;
}, 2000)
}
$scope.forceDraftSave = function() {
saveDraft();
};
// Listen to save draft events from editor
$scope.$on('save-draft', saveDraft);
/**
* Discard the current draft and grab the current page
* content from the system via an AJAX request.
*/
$scope.discardDraft = function () {
let url = window.baseUrl('/ajax/page/' + pageId);
$http.get(url).then(responseData => {
if (autoSave) $interval.cancel(autoSave);
$scope.draftText = trans('entities.pages_editing_page');
$scope.isUpdateDraft = false;
$scope.$broadcast('html-update', responseData.data.html);
$scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html);
$('#name').val(responseData.data.name);
$timeout(() => {
startAutoSave();
}, 1000);
events.emit('success', trans('entities.pages_draft_discarded'));
});
};
}]);
};

View File

@@ -1,393 +0,0 @@
"use strict";
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
const code = require('./code');
module.exports = function (ngApp, events) {
/**
* TinyMCE
* An angular wrapper around the tinyMCE editor.
*/
ngApp.directive('tinymce', ['$timeout', function ($timeout) {
return {
restrict: 'A',
scope: {
tinymce: '=',
mceModel: '=',
mceChange: '='
},
link: function (scope, element, attrs) {
function tinyMceSetup(editor) {
editor.on('ExecCommand change input NodeChange ObjectResized', (e) => {
let content = editor.getContent();
$timeout(() => {
scope.mceModel = content;
});
scope.mceChange(content);
});
editor.on('keydown', (event) => {
if (event.keyCode === 83 && (navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey)) {
event.preventDefault();
scope.$emit('save-draft', event);
}
});
editor.on('init', (e) => {
scope.mceModel = editor.getContent();
});
scope.$on('html-update', (event, value) => {
editor.setContent(value);
editor.selection.select(editor.getBody(), true);
editor.selection.collapse(false);
scope.mceModel = editor.getContent();
});
}
scope.tinymce.extraSetups.push(tinyMceSetup);
tinymce.init(scope.tinymce);
}
}
}]);
const md = new MarkdownIt({html: true});
md.use(mdTasksLists, {label: true});
/**
* Markdown input
* Handles the logic for just the editor input field.
*/
ngApp.directive('markdownInput', ['$timeout', function ($timeout) {
return {
restrict: 'A',
scope: {
mdModel: '=',
mdChange: '='
},
link: function (scope, element, attrs) {
// Codemirror Setup
element = element.find('textarea').first();
let cm = code.markdownEditor(element[0]);
// Custom key commands
let metaKey = code.getMetaKey();
const extraKeys = {};
// Insert Image shortcut
extraKeys[`${metaKey}-Alt-I`] = function(cm) {
let selectedText = cm.getSelection();
let newText = `![${selectedText}](http://)`;
let cursorPos = cm.getCursor('from');
cm.replaceSelection(newText);
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length -1);
};
// Save draft
extraKeys[`${metaKey}-S`] = function(cm) {scope.$emit('save-draft');};
// Show link selector
extraKeys[`Shift-${metaKey}-K`] = function(cm) {showLinkSelector()};
// Insert Link
extraKeys[`${metaKey}-K`] = function(cm) {insertLink()};
// FormatShortcuts
extraKeys[`${metaKey}-1`] = function(cm) {replaceLineStart('##');};
extraKeys[`${metaKey}-2`] = function(cm) {replaceLineStart('###');};
extraKeys[`${metaKey}-3`] = function(cm) {replaceLineStart('####');};
extraKeys[`${metaKey}-4`] = function(cm) {replaceLineStart('#####');};
extraKeys[`${metaKey}-5`] = function(cm) {replaceLineStart('');};
extraKeys[`${metaKey}-d`] = function(cm) {replaceLineStart('');};
extraKeys[`${metaKey}-6`] = function(cm) {replaceLineStart('>');};
extraKeys[`${metaKey}-q`] = function(cm) {replaceLineStart('>');};
extraKeys[`${metaKey}-7`] = function(cm) {wrapSelection('\n```\n', '\n```');};
extraKeys[`${metaKey}-8`] = function(cm) {wrapSelection('`', '`');};
extraKeys[`Shift-${metaKey}-E`] = function(cm) {wrapSelection('`', '`');};
extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('<p class="callout info">', '</p>');};
cm.setOption('extraKeys', extraKeys);
// Update data on content change
cm.on('change', (instance, changeObj) => {
update(instance);
});
// Handle scroll to sync display view
cm.on('scroll', instance => {
// Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
let scroll = instance.getScrollInfo();
let atEnd = scroll.top + scroll.clientHeight === scroll.height;
if (atEnd) {
scope.$emit('markdown-scroll', -1);
return;
}
let lineNum = instance.lineAtHeight(scroll.top, 'local');
let range = instance.getRange({line: 0, ch: null}, {line: lineNum, ch: null});
let parser = new DOMParser();
let doc = parser.parseFromString(md.render(range), 'text/html');
let totalLines = doc.documentElement.querySelectorAll('body > *');
scope.$emit('markdown-scroll', totalLines.length);
});
// Handle image paste
cm.on('paste', (cm, event) => {
if (!event.clipboardData || !event.clipboardData.items) return;
for (let i = 0; i < event.clipboardData.items.length; i++) {
uploadImage(event.clipboardData.items[i].getAsFile());
}
});
// Handle images on drag-drop
cm.on('drop', (cm, event) => {
event.stopPropagation();
event.preventDefault();
let cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
cm.setCursor(cursorPos);
if (!event.dataTransfer || !event.dataTransfer.files) return;
for (let i = 0; i < event.dataTransfer.files.length; i++) {
uploadImage(event.dataTransfer.files[i]);
}
});
// Helper to replace editor content
function replaceContent(search, replace) {
let text = cm.getValue();
let cursor = cm.listSelections();
cm.setValue(text.replace(search, replace));
cm.setSelections(cursor);
}
// Helper to replace the start of the line
function replaceLineStart(newStart) {
let cursor = cm.getCursor();
let lineContent = cm.getLine(cursor.line);
let lineLen = lineContent.length;
let lineStart = lineContent.split(' ')[0];
// Remove symbol if already set
if (lineStart === newStart) {
lineContent = lineContent.replace(`${newStart} `, '');
cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)});
return;
}
let alreadySymbol = /^[#>`]/.test(lineStart);
let posDif = 0;
if (alreadySymbol) {
posDif = newStart.length - lineStart.length;
lineContent = lineContent.replace(lineStart, newStart).trim();
} else if (newStart !== '') {
posDif = newStart.length + 1;
lineContent = newStart + ' ' + lineContent;
}
cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch + posDif});
}
function wrapLine(start, end) {
let cursor = cm.getCursor();
let lineContent = cm.getLine(cursor.line);
let lineLen = lineContent.length;
let newLineContent = lineContent;
if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
} else {
newLineContent = `${start}${lineContent}${end}`;
}
cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch + start.length});
}
function wrapSelection(start, end) {
let selection = cm.getSelection();
if (selection === '') return wrapLine(start, end);
let newSelection = selection;
let frontDiff = 0;
let endDiff = 0;
if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
newSelection = selection.slice(start.length, selection.length - end.length);
endDiff = -(end.length + start.length);
} else {
newSelection = `${start}${selection}${end}`;
endDiff = start.length + end.length;
}
let selections = cm.listSelections()[0];
cm.replaceSelection(newSelection);
let headFirst = selections.head.ch <= selections.anchor.ch;
selections.head.ch += headFirst ? frontDiff : endDiff;
selections.anchor.ch += headFirst ? endDiff : frontDiff;
cm.setSelections([selections]);
}
// Handle image upload and add image into markdown content
function uploadImage(file) {
if (file === null || file.type.indexOf('image') !== 0) return;
let ext = 'png';
if (file.name) {
let fileNameMatches = file.name.match(/\.(.+)$/);
if (fileNameMatches.length > 1) ext = fileNameMatches[1];
}
// Insert image into markdown
let id = "image-" + Math.random().toString(16).slice(2);
let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
let selectedText = cm.getSelection();
let placeHolderText = `![${selectedText}](${placeholderImage})`;
cm.replaceSelection(placeHolderText);
let remoteFilename = "image-" + Date.now() + "." + ext;
let formData = new FormData();
formData.append('file', file, remoteFilename);
window.$http.post('/images/gallery/upload', formData).then(resp => {
replaceContent(placeholderImage, resp.data.thumbs.display);
}).catch(err => {
events.emit('error', trans('errors.image_upload_error'));
replaceContent(placeHolderText, selectedText);
console.log(err);
});
}
// Show the popup link selector and insert a link when finished
function showLinkSelector() {
let cursorPos = cm.getCursor('from');
window.EntitySelectorPopup.show(entity => {
let selectedText = cm.getSelection() || entity.name;
let newText = `[${selectedText}](${entity.link})`;
cm.focus();
cm.replaceSelection(newText);
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
});
}
function insertLink() {
let cursorPos = cm.getCursor('from');
let selectedText = cm.getSelection() || '';
let newText = `[${selectedText}]()`;
cm.focus();
cm.replaceSelection(newText);
let cursorPosDiff = (selectedText === '') ? -3 : -1;
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff);
}
// Show the image manager and handle image insertion
function showImageManager() {
let cursorPos = cm.getCursor('from');
window.ImageManager.show(image => {
let selectedText = cm.getSelection();
let newText = "![" + (selectedText || image.name) + "](" + image.thumbs.display + ")";
cm.focus();
cm.replaceSelection(newText);
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
});
}
// Update the data models and rendered output
function update(instance) {
let content = instance.getValue();
element.val(content);
$timeout(() => {
scope.mdModel = content;
scope.mdChange(md.render(content));
});
}
update(cm);
// Listen to commands from parent scope
scope.$on('md-insert-link', showLinkSelector);
scope.$on('md-insert-image', showImageManager);
scope.$on('markdown-update', (event, value) => {
cm.setValue(value);
element.val(value);
scope.mdModel = value;
scope.mdChange(md.render(value));
});
}
}
}]);
/**
* Markdown Editor
* Handles all functionality of the markdown editor.
*/
ngApp.directive('markdownEditor', ['$timeout', '$rootScope', function ($timeout, $rootScope) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
// Editor Elements
const $display = element.find('.markdown-display').first();
const $insertImage = element.find('button[data-action="insertImage"]');
const $insertEntityLink = element.find('button[data-action="insertEntityLink"]');
// Prevent markdown display link click redirect
$display.on('click', 'a', function(event) {
event.preventDefault();
window.open(this.getAttribute('href'));
});
// Editor UI Actions
$insertEntityLink.click(e => {scope.$broadcast('md-insert-link');});
$insertImage.click(e => {scope.$broadcast('md-insert-image');});
// Handle scroll sync event from editor scroll
$rootScope.$on('markdown-scroll', (event, lineCount) => {
let elems = $display[0].children[0].children;
if (elems.length > lineCount) {
let topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
$display.animate({
scrollTop: topElem.offsetTop
}, {queue: false, duration: 200, easing: 'linear'});
}
});
}
}
}]);
/**
* Page Editor Toolbox
* Controls all functionality for the sliding toolbox
* on the page edit view.
*/
ngApp.directive('toolbox', [function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
// Get common elements
const $buttons = elem.find('[toolbox-tab-button]');
const $content = elem.find('[toolbox-tab-content]');
const $toggle = elem.find('[toolbox-toggle]');
// Handle toolbox toggle click
$toggle.click((e) => {
elem.toggleClass('open');
});
// Set an active tab/content by name
function setActive(tabName, openToolbox) {
$buttons.removeClass('active');
$content.hide();
$buttons.filter(`[toolbox-tab-button="${tabName}"]`).addClass('active');
$content.filter(`[toolbox-tab-content="${tabName}"]`).show();
if (openToolbox) elem.addClass('open');
}
// Set the first tab content active on load
setActive($content.first().attr('toolbox-tab-content'), false);
// Handle tab button click
$buttons.click(function (e) {
let name = $(this).attr('toolbox-tab-button');
setActive(name, true);
});
}
}
}]);
};

View File

@@ -58,16 +58,6 @@ window.$http = axiosInstance;
Vue.prototype.$http = axiosInstance;
Vue.prototype.$events = window.$events;
// AngularJS - Create application and load components
const angular = require("angular");
require("angular-resource");
require("angular-animate");
require("angular-sanitize");
require("angular-ui-sortable");
let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Translation setup
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
const Translations = require("./translations");
@@ -79,11 +69,6 @@ window.trans_choice = translator.getPlural.bind(translator);
require("./vues/vues");
require("./components");
// Load in angular specific items
const Directives = require('./directives');
const Controllers = require('./controllers');
Directives(ngApp, window.$events);
Controllers(ngApp, window.$events);
//Global jQuery Config & Extensions

View File

@@ -1,5 +1,4 @@
"use strict";
const Code = require('../code');
/**
@@ -66,6 +65,12 @@ function registerEditorShortcuts(editor) {
editor.shortcuts.add('meta+e', '', ['codeeditor', false, 'pre']);
editor.shortcuts.add('meta+8', '', ['FormatBlock', false, 'code']);
editor.shortcuts.add('meta+shift+E', '', ['FormatBlock', false, 'code']);
// Save draft shortcut
editor.shortcuts.add('meta+S', '', () => {
window.$events.emit('editor-save-draft');
});
// Loop through callout styles
editor.shortcuts.add('meta+9', '', function() {
let selectedNode = editor.selection.getNode();
@@ -84,6 +89,7 @@ function registerEditorShortcuts(editor) {
}
editor.formatter.apply('p');
});
}
@@ -196,189 +202,192 @@ function codePlugin() {
});
}
codePlugin();
function hrPlugin() {
window.tinymce.PluginManager.add('customhr', function (editor) {
editor.addCommand('InsertHorizontalRule', function () {
let hrElem = document.createElement('hr');
let cNode = editor.selection.getNode();
let parentNode = cNode.parentNode;
parentNode.insertBefore(hrElem, cNode);
});
editor.addButton('hr', {
icon: 'hr',
tooltip: 'Horizontal line',
cmd: 'InsertHorizontalRule'
});
editor.addMenuItem('hr', {
icon: 'hr',
text: 'Horizontal line',
cmd: 'InsertHorizontalRule',
context: 'insert'
});
window.tinymce.PluginManager.add('customhr', function (editor) {
editor.addCommand('InsertHorizontalRule', function () {
let hrElem = document.createElement('hr');
let cNode = editor.selection.getNode();
let parentNode = cNode.parentNode;
parentNode.insertBefore(hrElem, cNode);
});
}
module.exports = function() {
hrPlugin();
codePlugin();
let settings = {
selector: '#html-editor',
content_css: [
window.baseUrl('/css/styles.css'),
window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css')
],
branding: false,
body_class: 'page-content',
browser_spellcheck: true,
relative_urls: false,
remove_script_host: false,
document_base_url: window.baseUrl('/'),
statusbar: false,
menubar: false,
paste_data_images: false,
extended_valid_elements: 'pre[*]',
automatic_uploads: false,
valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre]",
plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor",
imagetools_toolbar: 'imageoptions',
toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen",
content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
style_formats: [
{title: "Header Large", format: "h2"},
{title: "Header Medium", format: "h3"},
{title: "Header Small", format: "h4"},
{title: "Header Tiny", format: "h5"},
{title: "Paragraph", format: "p", exact: true, classes: ''},
{title: "Blockquote", format: "blockquote"},
{title: "Code Block", icon: "code", cmd: 'codeeditor', format: 'codeeditor'},
{title: "Inline Code", icon: "code", inline: "code"},
{title: "Callouts", items: [
{title: "Info", format: 'calloutinfo'},
{title: "Success", format: 'calloutsuccess'},
{title: "Warning", format: 'calloutwarning'},
{title: "Danger", format: 'calloutdanger'}
]},
],
style_formats_merge: false,
formats: {
codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
},
file_browser_callback: function (field_name, url, type, win) {
editor.addButton('hr', {
icon: 'hr',
tooltip: 'Horizontal line',
cmd: 'InsertHorizontalRule'
});
if (type === 'file') {
window.EntitySelectorPopup.show(function(entity) {
let originalField = win.document.getElementById(field_name);
originalField.value = entity.link;
$(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
editor.addMenuItem('hr', {
icon: 'hr',
text: 'Horizontal line',
cmd: 'InsertHorizontalRule',
context: 'insert'
});
});
module.exports = {
selector: '#html-editor',
content_css: [
window.baseUrl('/css/styles.css'),
window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css')
],
branding: false,
body_class: 'page-content',
browser_spellcheck: true,
relative_urls: false,
remove_script_host: false,
document_base_url: window.baseUrl('/'),
statusbar: false,
menubar: false,
paste_data_images: false,
extended_valid_elements: 'pre[*]',
automatic_uploads: false,
valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre]",
plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor",
imagetools_toolbar: 'imageoptions',
toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen",
content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
style_formats: [
{title: "Header Large", format: "h2"},
{title: "Header Medium", format: "h3"},
{title: "Header Small", format: "h4"},
{title: "Header Tiny", format: "h5"},
{title: "Paragraph", format: "p", exact: true, classes: ''},
{title: "Blockquote", format: "blockquote"},
{title: "Code Block", icon: "code", cmd: 'codeeditor', format: 'codeeditor'},
{title: "Inline Code", icon: "code", inline: "code"},
{title: "Callouts", items: [
{title: "Info", format: 'calloutinfo'},
{title: "Success", format: 'calloutsuccess'},
{title: "Warning", format: 'calloutwarning'},
{title: "Danger", format: 'calloutdanger'}
]},
],
style_formats_merge: false,
formats: {
codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
},
file_browser_callback: function (field_name, url, type, win) {
if (type === 'file') {
window.EntitySelectorPopup.show(function(entity) {
let originalField = win.document.getElementById(field_name);
originalField.value = entity.link;
$(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
});
}
if (type === 'image') {
// Show image manager
window.ImageManager.show(function (image) {
// Set popover link input to image url then fire change event
// to ensure the new value sticks
win.document.getElementById(field_name).value = image.url;
if ("createEvent" in document) {
let evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
win.document.getElementById(field_name).dispatchEvent(evt);
} else {
win.document.getElementById(field_name).fireEvent("onchange");
}
// Replace the actively selected content with the linked image
let html = `<a href="${image.url}" target="_blank">`;
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += '</a>';
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
});
}
},
paste_preprocess: function (plugin, args) {
let content = args.content;
if (content.indexOf('<img src="file://') !== -1) {
args.content = '';
}
},
setup: function (editor) {
editor.on('init ExecCommand change input NodeChange ObjectResized', editorChange);
function editorChange() {
let content = editor.getContent();
window.$events.emit('editor-html-change', content);
}
window.$events.listen('editor-html-update', html => {
editor.setContent(html);
editor.selection.select(editor.getBody(), true);
editor.selection.collapse(false);
editorChange(html);
});
registerEditorShortcuts(editor);
let wrap;
function hasTextContent(node) {
return node && !!( node.textContent || node.innerText );
}
editor.on('dragstart', function () {
let node = editor.selection.getNode();
if (node.nodeName !== 'IMG') return;
wrap = editor.dom.getParent(node, '.mceTemp');
if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
wrap = node.parentNode;
}
});
editor.on('drop', function (event) {
let dom = editor.dom,
rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
// Don't allow anything to be dropped in a captioned image.
if (dom.getParent(rng.startContainer, '.mceTemp')) {
event.preventDefault();
} else if (wrap) {
event.preventDefault();
editor.undoManager.transact(function () {
editor.selection.setRng(rng);
editor.selection.setNode(wrap);
dom.remove(wrap);
});
}
if (type === 'image') {
// Show image manager
wrap = null;
});
// Custom Image picker button
editor.addButton('image-insert', {
title: 'My title',
icon: 'image',
tooltip: 'Insert an image',
onclick: function () {
window.ImageManager.show(function (image) {
// Set popover link input to image url then fire change event
// to ensure the new value sticks
win.document.getElementById(field_name).value = image.url;
if ("createEvent" in document) {
let evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
win.document.getElementById(field_name).dispatchEvent(evt);
} else {
win.document.getElementById(field_name).fireEvent("onchange");
}
// Replace the actively selected content with the linked image
let html = `<a href="${image.url}" target="_blank">`;
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += '</a>';
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
editor.execCommand('mceInsertContent', false, html);
});
}
});
},
paste_preprocess: function (plugin, args) {
let content = args.content;
if (content.indexOf('<img src="file://') !== -1) {
args.content = '';
}
},
extraSetups: [],
setup: function (editor) {
// Run additional setup actions
// Used by the angular side of things
for (let i = 0; i < settings.extraSetups.length; i++) {
settings.extraSetups[i](editor);
}
registerEditorShortcuts(editor);
let wrap;
function hasTextContent(node) {
return node && !!( node.textContent || node.innerText );
}
editor.on('dragstart', function () {
let node = editor.selection.getNode();
if (node.nodeName !== 'IMG') return;
wrap = editor.dom.getParent(node, '.mceTemp');
if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
wrap = node.parentNode;
}
});
editor.on('drop', function (event) {
let dom = editor.dom,
rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
// Don't allow anything to be dropped in a captioned image.
if (dom.getParent(rng.startContainer, '.mceTemp')) {
event.preventDefault();
} else if (wrap) {
event.preventDefault();
editor.undoManager.transact(function () {
editor.selection.setRng(rng);
editor.selection.setNode(wrap);
dom.remove(wrap);
});
}
wrap = null;
});
// Custom Image picker button
editor.addButton('image-insert', {
title: 'My title',
icon: 'image',
tooltip: 'Insert an image',
onclick: function () {
window.ImageManager.show(function (image) {
let html = `<a href="${image.url}" target="_blank">`;
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += '</a>';
editor.execCommand('mceInsertContent', false, html);
});
}
});
// Paste image-uploads
editor.on('paste', event => { editorPaste(event, editor) });
}
};
return settings;
// Paste image-uploads
editor.on('paste', event => { editorPaste(event, editor) });
}
};

View File

@@ -0,0 +1,149 @@
const moment = require('moment');
require('moment/locale/en-gb');
moment.locale('en-gb');
let autoSaveFrequency = 30;
let autoSave = false;
let draftErroring = false;
let currentContent = {
title: false,
html: false
};
let lastSave = 0;
function mounted() {
let elem = this.$el;
this.draftsEnabled = elem.getAttribute('drafts-enabled') === 'true';
this.editorType = elem.getAttribute('editor-type');
this.pageId= Number(elem.getAttribute('page-id'));
this.isNewDraft = Number(elem.getAttribute('page-new-draft')) === 1;
this.isUpdateDraft = Number(elem.getAttribute('page-update-draft')) === 1;
if (this.pageId !== 0 && this.draftsEnabled) {
window.setTimeout(() => {
this.startAutoSave();
}, 1000);
}
if (this.isUpdateDraft || this.isNewDraft) {
this.draftText = trans('entities.pages_editing_draft');
} else {
this.draftText = trans('entities.pages_editing_page');
}
// Listen to save draft events from editor
window.$events.listen('editor-save-draft', this.saveDraft);
// Listen to content changes from the editor
window.$events.listen('editor-html-change', html => {
this.editorHTML = html;
});
window.$events.listen('editor-markdown-change', markdown => {
this.editorMarkdown = markdown;
});
}
let data = {
draftsEnabled: false,
editorType: 'wysiwyg',
pagedId: 0,
isNewDraft: false,
isUpdateDraft: false,
draftText: '',
draftUpdated : false,
changeSummary: '',
editorHTML: '',
editorMarkdown: '',
};
let methods = {
startAutoSave() {
currentContent.title = document.getElementById('name').value.trim();
currentContent.html = this.editorHTML;
autoSave = window.setInterval(() => {
// Return if manually saved recently to prevent bombarding the server
if (Date.now() - lastSave < (1000 * autoSaveFrequency)/2) return;
let newTitle = document.getElementById('name').value.trim();
let newHtml = this.editorHTML;
if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
currentContent.html = newHtml;
currentContent.title = newTitle;
this.saveDraft();
}
}, 1000 * autoSaveFrequency);
},
saveDraft() {
if (!this.draftsEnabled) return;
let data = {
name: document.getElementById('name').value.trim(),
html: this.editorHTML
};
if (this.editorType === 'markdown') data.markdown = this.editorMarkdown;
let url = window.baseUrl(`/ajax/page/${this.pageId}/save-draft`);
window.$http.put(url, data).then(response => {
draftErroring = false;
let updateTime = moment.utc(moment.unix(response.data.timestamp)).toDate();
if (!this.isNewDraft) this.isUpdateDraft = true;
this.draftNotifyChange(response.data.message + moment(updateTime).format('HH:mm'));
lastSave = Date.now();
}, errorRes => {
if (draftErroring) return;
window.$events('error', trans('errors.page_draft_autosave_fail'));
draftErroring = true;
});
},
draftNotifyChange(text) {
this.draftText = text;
this.draftUpdated = true;
window.setTimeout(() => {
this.draftUpdated = false;
}, 2000);
},
discardDraft() {
let url = window.baseUrl(`/ajax/page/${this.pageId}`);
window.$http.get(url).then(response => {
if (autoSave) window.clearInterval(autoSave);
this.draftText = trans('entities.pages_editing_page');
this.isUpdateDraft = false;
window.$events.emit('editor-html-update', response.data.html);
window.$events.emit('editor-markdown-update', response.data.markdown || response.data.html);
document.getElementById('name').value = response.data.name;
window.setTimeout(() => {
this.startAutoSave();
}, 1000);
window.$events.emit('success', trans('entities.pages_draft_discarded'));
});
},
};
let computed = {
changeSummaryShort() {
let len = this.changeSummary.length;
if (len === 0) return trans('entities.pages_edit_set_changelog');
if (len <= 16) return this.changeSummary;
return this.changeSummary.slice(0, 16) + '...';
}
};
module.exports = {
mounted, data, methods, computed,
};

View File

@@ -11,6 +11,7 @@ let vueMapping = {
'image-manager': require('./image-manager'),
'tag-manager': require('./tag-manager'),
'attachment-manager': require('./attachment-manager'),
'page-editor': require('./page-editor'),
};
window.vues = {};

View File

@@ -195,10 +195,13 @@
font-weight: 400;
text-transform: uppercase;
}
h3 a {
line-height: 1;
}
.body, p.empty-text {
padding: $-m;
}
a {
a, p {
word-wrap: break-word;
word-break: break-word;
}

View File

@@ -370,6 +370,7 @@ span.CodeMirror-selectedtext { background: none; }
.cm-s-base16-light span.cm-keyword { color: #ac4142; }
.cm-s-base16-light span.cm-string { color: #e09c3c; }
.cm-s-base16-light span.cm-builtin { color: #4c7f9e; }
.cm-s-base16-light span.cm-variable { color: #90a959; }
.cm-s-base16-light span.cm-variable-2 { color: #6a9fb5; }
.cm-s-base16-light span.cm-def { color: #d28445; }

View File

@@ -37,4 +37,6 @@ return [
'book_sort' => 'ha ordinato il libro',
'book_sort_notification' => 'Libro Riordinato Correttamente',
// Other
'commented_on' => 'ha commentato in',
];

View File

@@ -29,6 +29,7 @@ return [
'edit' => 'Modifica',
'sort' => 'Ordina',
'move' => 'Muovi',
'reply' => 'Rispondi',
'delete' => 'Elimina',
'search' => 'Cerca',
'search_clear' => 'Pulisci Ricerca',

View File

@@ -19,7 +19,6 @@ return [
'meta_created_name' => 'Creato :timeLength da :user',
'meta_updated' => 'Aggiornato :timeLength',
'meta_updated_name' => 'Aggiornato :timeLength da :user',
'x_pages' => ':count Pagine',
'entity_select' => 'Selezione Entità',
'images' => 'Immagini',
'my_recent_drafts' => 'Bozze Recenti',
@@ -70,6 +69,7 @@ return [
*/
'book' => 'Libro',
'books' => 'Libri',
'x_books' => ':count Libro|:count Libri',
'books_empty' => 'Nessun libro è stato creato',
'books_popular' => 'Libri Popolari',
'books_recent' => 'Libri Recenti',
@@ -105,6 +105,7 @@ return [
*/
'chapter' => 'Capitolo',
'chapters' => 'Capitoli',
'x_chapters' => ':count Capitolo|:count Capitoli',
'chapters_popular' => 'Capitoli Popolari',
'chapters_new' => 'Nuovo Capitolo',
'chapters_create' => 'Crea un nuovo capitolo',
@@ -129,20 +130,21 @@ return [
*/
'page' => 'Pagina',
'pages' => 'Pagine',
'x_pages' => ':count Pagina|:count Pagine',
'pages_popular' => 'Pagine Popolari',
'pages_new' => 'Nuova Pagina',
'pages_attachments' => 'Allegati',
'pages_navigation' => 'Page Navigation',
'pages_navigation' => 'Navigazione Pagine',
'pages_delete' => 'Elimina Pagina',
'pages_delete_named' => 'Elimina la pagina :pageName',
'pages_delete_draft_named' => 'Delete Draft Page :pageName',
'pages_delete_draft_named' => 'Elimina bozza della pagina :pageName',
'pages_delete_draft' => 'Elimina Bozza Pagina',
'pages_delete_success' => 'Pagina eliminata',
'pages_delete_draft_success' => 'Bozza di una pagina eliminata',
'pages_delete_confirm' => 'Sei sicuro di voler eliminare questa pagina?',
'pages_delete_draft_confirm' => 'Sei sicuro di voler eliminare la bozza di questa pagina?',
'pages_editing_named' => 'Modifica :pageName',
'pages_edit_toggle_header' => 'Toggle header',
'pages_edit_toggle_header' => 'Mostra/Nascondi header',
'pages_edit_save_draft' => 'Salva Bozza',
'pages_edit_draft' => 'Modifica Bozza della pagina',
'pages_editing_draft' => 'Modifica Bozza',
@@ -164,7 +166,7 @@ return [
'pages_move' => 'Muovi Pagina',
'pages_move_success' => 'Pagina mossa in ":parentName"',
'pages_permissions' => 'Permessi Pagina',
'pages_permissions_success' => 'Page permissions updated',
'pages_permissions_success' => 'Permessi pagina aggiornati',
'pages_revision' => 'Versione',
'pages_revisions' => 'Versioni Pagina',
'pages_revisions_named' => 'Versioni della pagina :pageName',
@@ -242,10 +244,17 @@ return [
*/
'comment' => 'Commento',
'comments' => 'Commenti',
'comment_count' => '1 Commento|:count Commenti',
'comment_placeholder' => 'Scrivi un commento',
'comment_count' => '{0} Nessun Commento|{1} 1 Commento|[2,*] :count Commenti',
'comment_save' => 'Salva Commento',
'comment_saving' => 'Salvataggio commento...',
'comment_deleting' => 'Eliminazione commento...',
'comment_new' => 'Nuovo Commento',
'comment_created' => 'ha commentato :createDiff',
'comment_updated' => 'Aggiornato :updateDiff da :username',
'comment_deleted_success' => 'Commento eliminato',
'comment_created_success' => 'Commento aggiunto',
'comment_updated_success' => 'Commento aggiornato',
'comment_delete_confirm' => 'Questo rimuoverà il contenuto del commento?',
'comment_delete_confirm' => 'Sei sicuro di voler elminare questo commento?',
'comment_in_reply_to' => 'In risposta a :commentId',
];

View File

@@ -37,4 +37,6 @@ return [
'book_sort' => 'sorteerde boek',
'book_sort_notification' => 'Boek Succesvol Gesorteerd',
// Other
'commented_on' => 'reactie op',
];

View File

@@ -10,6 +10,7 @@ return [
'save' => 'Opslaan',
'continue' => 'Doorgaan',
'select' => 'Kies',
'more' => 'Meer',
/**
* Form Labels
@@ -33,7 +34,7 @@ return [
'search_clear' => 'Zoekopdracht wissen',
'reset' => 'Reset',
'remove' => 'Verwijderen',
'add' => 'Toevoegen',
/**
* Misc

View File

@@ -20,5 +20,12 @@ return [
'image_preview' => 'Afbeelding Voorbeeld',
'image_upload_success' => 'Afbeelding succesvol geüpload',
'image_update_success' => 'Afbeeldingsdetails succesvol verwijderd',
'image_delete_success' => 'Afbeelding succesvol verwijderd'
];
'image_delete_success' => 'Afbeelding succesvol verwijderd',
/**
* Code editor
*/
'code_editor' => 'Code invoegen',
'code_language' => 'Code taal',
'code_content' => 'Code',
'code_save' => 'Sla code op',
];

View File

@@ -14,6 +14,7 @@ return [
'recent_activity' => 'Recente Activiteit',
'create_now' => 'Maak er zelf één',
'revisions' => 'Revisies',
'meta_revision' => 'Revisie #:revisionCount',
'meta_created' => 'Aangemaakt :timeLength',
'meta_created_name' => 'Aangemaakt: :timeLength door :user',
'meta_updated' => ':timeLength Aangepast',
@@ -43,18 +44,37 @@ return [
* Search
*/
'search_results' => 'Zoekresultaten',
'search_total_results_found' => ':count resultaten gevonden|:count resultaten gevonden',
'search_clear' => 'Zoekopdracht wissen',
'search_no_pages' => 'Er zijn geen pagina\'s gevonden',
'search_for_term' => 'Zoeken op :term',
'search_more' => 'Meer resultaten',
'search_filters' => 'Zoek filters',
'search_content_type' => 'Content Type',
'search_exact_matches' => 'Exacte Matches',
'search_tags' => 'Zoek tags',
'search_viewed_by_me' => 'Bekeken door mij',
'search_not_viewed_by_me' => 'Niet bekeken door mij',
'search_permissions_set' => 'Permissies gezet',
'search_created_by_me' => 'Door mij gemaakt',
'search_updated_by_me' => 'Door mij geupdate',
'search_updated_before' => 'Geupdate voor',
'search_updated_after' => 'Geupdate na',
'search_created_before' => 'Gecreeerd voor',
'search_created_after' => 'Gecreeerd na',
'search_set_date' => 'Zet datum',
'search_update' => 'Update zoekresultaten',
/**
* Books
*/
'book' => 'Boek',
'books' => 'Boeken',
'x_books' => ':count Boek|:count Boeken',
'books_empty' => 'Er zijn geen boeken aangemaakt',
'books_popular' => 'Populaire Boeken',
'books_recent' => 'Recente Boeken',
'books_new' => 'Nieuwe Boeken',
'books_popular_empty' => 'De meest populaire boeken worden hier weergegeven.',
'books_create' => 'Nieuw Boek Aanmaken',
'books_delete' => 'Boek Verwijderen',
@@ -85,6 +105,7 @@ return [
*/
'chapter' => 'Hoofdstuk',
'chapters' => 'Hoofdstukken',
'x_chapters' => ':count Hoofdstuk|:count Hoofdstukken',
'chapters_popular' => 'Populaire Hoofdstukken',
'chapters_new' => 'Nieuw Hoofdstuk',
'chapters_create' => 'Hoofdstuk Toevoegen',
@@ -103,12 +124,14 @@ return [
'chapters_empty' => 'Er zijn geen pagina\'s in dit hoofdstuk aangemaakt.',
'chapters_permissions_active' => 'Hoofdstuk Permissies Actief',
'chapters_permissions_success' => 'Hoofdstuk Permissies Bijgewerkt',
'chapters_search_this' => 'Doorzoek dit hoofdstuk',
/**
* Pages
*/
'page' => 'Pagina',
'pages' => 'Pagina\'s',
'x_pages' => ':count Pagina|:count Pagina\'s',
'pages_popular' => 'Populaire Pagina\'s',
'pages_new' => 'Nieuwe Pagina',
'pages_attachments' => 'Bijlages',
@@ -122,7 +145,7 @@ return [
'pages_delete_confirm' => 'Weet je zeker dat je deze pagina wilt verwijderen?',
'pages_delete_draft_confirm' => 'Weet je zeker dat je dit concept wilt verwijderen?',
'pages_editing_named' => 'Pagina :pageName Bewerken',
'pages_edit_toggle_header' => 'Toggle header',
'pages_edit_toggle_header' => 'Wissel header',
'pages_edit_save_draft' => 'Concept opslaan',
'pages_edit_draft' => 'Paginaconcept Bewerken',
'pages_editing_draft' => 'Concept Bewerken',
@@ -132,12 +155,12 @@ return [
'pages_edit_discard_draft' => 'Concept Verwijderen',
'pages_edit_set_changelog' => 'Changelog',
'pages_edit_enter_changelog_desc' => 'Geef een korte omschrijving van de wijzingen die je gemaakt hebt.',
'pages_edit_enter_changelog' => 'Enter Changelog',
'pages_edit_enter_changelog' => 'Zie logboek',
'pages_save' => 'Pagina Opslaan',
'pages_title' => 'Pagina Titel',
'pages_name' => 'Pagina Naam',
'pages_md_editor' => 'Bewerker',
'pages_md_preview' => 'Preview',
'pages_md_preview' => 'Voorbeeld',
'pages_md_insert_image' => 'Afbeelding Invoegen',
'pages_md_insert_link' => 'Entity Link Invoegen',
'pages_not_in_chapter' => 'Deze pagina staat niet in een hoofdstuk',
@@ -145,11 +168,13 @@ return [
'pages_move_success' => 'Pagina verplaatst naar ":parentName"',
'pages_permissions' => 'Pagina Permissies',
'pages_permissions_success' => 'Pagina Permissies bijgwerkt',
'pages_revision' => 'Revisie',
'pages_revisions' => 'Pagina Revisies',
'pages_revisions_named' => 'Pagina Revisies voor :pageName',
'pages_revision_named' => 'Pagina Revisie voor :pageName',
'pages_revisions_created_by' => 'Aangemaakt door',
'pages_revisions_date' => 'Revisiedatum',
'pages_revisions_number' => '#',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Wijzigingen',
'pages_revisions_current' => 'Huidige Versie',

View File

@@ -60,6 +60,12 @@ return [
'role_system_cannot_be_deleted' => 'Dit is een systeemrol en kan niet verwijderd worden',
'role_registration_default_cannot_delete' => 'Deze rol kan niet verwijerd worden zolang dit de standaardrol na registratie is.',
// Comments
'comment_list' => 'Er is een fout opgetreden tijdens het ophalen van de reacties.',
'cannot_add_comment_to_draft' => 'U kunt geen reacties toevoegen aan een concept.',
'comment_add' => 'Er is een fout opgetreden tijdens het toevoegen van de reactie.',
'comment_delete' => 'Er is een fout opgetreden tijdens het verwijderen van de reactie.',
'empty_comment' => 'Kan geen lege reactie toevoegen.',
// Error pages
'404_page_not_found' => 'Pagina Niet Gevonden',
'sorry_page_not_found' => 'Sorry, de pagina die je zocht is niet beschikbaar.',
@@ -67,11 +73,4 @@ return [
'error_occurred' => 'Er Ging Iets Fout',
'app_down' => ':appName is nu niet beschikbaar',
'back_soon' => 'Komt snel weer online.',
// Comments
'comment_list' => 'Er is een fout opgetreden tijdens het ophalen van de reacties.',
'cannot_add_comment_to_draft' => 'U kunt geen reacties toevoegen aan een ontwerp.',
'comment_add' => 'Er is een fout opgetreden tijdens het toevoegen van de reactie.',
'comment_delete' => 'Er is een fout opgetreden tijdens het verwijderen van de reactie.',
'empty_comment' => 'Kan geen lege reactie toevoegen.',
];

View File

@@ -8,35 +8,35 @@ return [
*/
// Pages
'page_create' => 'созданная страницу',
'page_create' => 'создал страницу',
'page_create_notification' => 'Страница успешно создана',
'page_update' => 'обновленная страница',
'page_update' => 'обновил страницу',
'page_update_notification' => 'Станица успешно обновлена',
'page_delete' => 'удалённая страница',
'page_delete' => 'удалил страницу',
'page_delete_notification' => 'Старница успешно удалена',
'page_restore' => 'восстановленная страница',
'page_restore' => 'восстановил страницу',
'page_restore_notification' => 'Страница успешно восстановлена',
'page_move' => 'перемещенная страница',
'page_move' => 'переместил страницу',
// Chapters
'chapter_create' => 'созданная глава',
'chapter_create' => 'создал главу',
'chapter_create_notification' => 'глава успешно создана',
'chapter_update' => 'обновлённая глава',
'chapter_update' => 'обновил главу',
'chapter_update_notification' => 'Глава успешно обновленна',
'chapter_delete' => 'удалённая глава',
'chapter_delete' => 'удалил главу',
'chapter_delete_notification' => 'Глава успешно удалено',
'chapter_move' => 'перемещённая глава',
'chapter_move' => 'переместил главу',
// Books
'book_create' => 'созданная книга',
'book_create' => 'создал книгу',
'book_create_notification' => 'Книга успешно создана',
'book_update' => 'обновлённая книга',
'book_update' => 'обновил книгу',
'book_update_notification' => 'Книга успешно обновлена',
'book_delete' => 'удалённая книга',
'book_delete' => 'удалил книгу',
'book_delete_notification' => 'Книга успешно удалена',
'book_sort' => 'отсортированная книга',
'book_sort' => 'отсортировал книгу',
'book_sort_notification' => 'Книга успешно отсортирована',
// Other
'commented_on' => 'прокомментировано',
'commented_on' => 'прокомментировал',
];

View File

@@ -9,7 +9,7 @@ return [
'back' => 'Назад',
'save' => 'Сохранить',
'continue' => 'Продолжить',
'select' => 'Выделить',
'select' => 'Выбрать',
'more' => 'Ещё',
/**

View File

@@ -4,7 +4,7 @@ return [
/**
* Image Manager
*/
'image_select' => 'Выделить изображение',
'image_select' => 'Выбрать изображение',
'image_all' => 'Все',
'image_all_title' => 'Простмотр всех изображений',
'image_book_title' => 'Просмотр всех изображений загруженных в эту книгу',
@@ -14,8 +14,8 @@ return [
'image_load_more' => 'Загрузить ещё',
'image_image_name' => 'Имя изображения',
'image_delete_confirm' => 'Это изображение используется на странице ниже. Снова кликните удалить для подтверждения того что вы хотите удалить.',
'image_select_image' => 'Выделить изображение',
'image_dropzone' => 'Перетащите изображение или кликните сюда для загрузки',
'image_select_image' => 'Выбрать изображение',
'image_dropzone' => 'Перетащите изображение или кликните для загрузки',
'images_deleted' => 'Изображения удалены',
'image_preview' => 'Предосмотр изображения',
'image_upload_success' => 'Изображение загружено успешно',

View File

@@ -11,7 +11,7 @@ return [
'recently_created_books' => 'Недавно созданные книги',
'recently_update' => 'Недавно обновленные',
'recently_viewed' => 'Недавно просмотренные',
'recent_activity' => 'Недвание действия',
'recent_activity' => 'Недавние действия',
'create_now' => 'Создать сейчас',
'revisions' => 'Версия',
'meta_revision' => 'Версия #:revisionCount',
@@ -22,14 +22,14 @@ return [
'entity_select' => 'Выбор объекта',
'images' => 'Изображения',
'my_recent_drafts' => 'Мои последние черновики',
'my_recently_viewed' => 'Миои недавние просмотры',
'my_recently_viewed' => 'Мои недавние просмотры',
'no_pages_viewed' => 'Вы не просматривали ни одной страницы',
'no_pages_recently_created' => 'Недавно не были созданы страницы',
'no_pages_recently_updated' => 'Недавно не обновлялись страницы',
'export' => 'Экспорт',
'export_html' => 'Собранный Web файл',
'export_html' => 'Веб файл',
'export_pdf' => 'PDF файл',
'export_text' => 'простой текстовый файл',
'export_text' => 'Текстовый файл',
/**
* Permissions and restrictions
@@ -98,7 +98,7 @@ return [
'books_sort' => 'Сортировка содержимого книги',
'books_sort_named' => 'Сортировка книги :bookName',
'books_sort_show_other' => 'Показать другие книги',
'books_sort_save' => 'Сохранить новый заказ',
'books_sort_save' => 'Сохранить новый порядок',
/**
* Chapters
@@ -130,7 +130,7 @@ return [
*/
'page' => 'Страница',
'pages' => 'Страницы',
'x_pages' => ':count страница|:count страниц',
'x_pages' => ':count страниц|:count страниц',
'pages_popular' => 'Популярные страницы',
'pages_new' => 'Новая страница',
'pages_attachments' => 'Вложения',
@@ -152,7 +152,7 @@ return [
'pages_edit_draft_save_at' => 'Черновик сохранить в ',
'pages_edit_delete_draft' => 'Удалить черновик',
'pages_edit_discard_draft' => 'отменить черновик',
'pages_edit_set_changelog' => 'Установить список изменений',
'pages_edit_set_changelog' => 'Задать список изменений',
'pages_edit_enter_changelog_desc' => 'Введите краткое описание изменений, которые вы сделали',
'pages_edit_enter_changelog' => 'Введите список изменений',
'pages_save' => 'Сохранить страницу',
@@ -203,7 +203,7 @@ return [
'tags' => '',
'tag_value' => 'Значение тэга (опционально)',
'tags_explain' => "Добавьте теги, чтобы лучше классифицировать ваш контент. \n Вы можете присвоить значение тегу для более глубокой организации.",
'tags_add' => 'До',
'tags_add' => 'Добавить тэг',
'attachments' => 'Вложение',
'attachments_explain' => 'Загрузите несколько файлов или добавьте ссылку для отображения на своей странице. Они видны на боковой панели страницы.',
'attachments_explain_instant_save' => 'Изменения здесь сохраняются мгновенно.',
@@ -212,7 +212,7 @@ return [
'attachments_link' => 'Присоединить ссылку',
'attachments_set_link' => 'Установить ссылку',
'attachments_delete_confirm' => 'Нажмите «Удалить» еще раз, чтобы подтвердить, что вы хотите удалить этот файл.',
'attachments_dropzone' => 'Сбросьте файлы или нажмите здесь, чтобы прикрепить файл',
'attachments_dropzone' => 'Перетащите файл сюда или нажмите здесь, чтобы загрузить файл',
'attachments_no_files' => 'Файлы не загружены',
'attachments_explain_link' => 'Вы можете присоединить ссылку, если вы предпочитаете не загружать файл. Это может быть ссылка на другую страницу или ссылку на файл в облаке',
'attachments_link_name' => 'Имя ссылки',
@@ -250,7 +250,7 @@ return [
'comment_saving' => 'Сохраниение комментария...',
'comment_deleting' => 'Удаление комментария...',
'comment_new' => 'Новый комментарий',
'comment_created' => 'комментирован :createDiff',
'comment_created' => 'прокомментировал :createDiff',
'comment_updated' => 'Обновлён :updateDiff пользователем :username',
'comment_deleted_success' => 'Комментарий удалён',
'comment_created_success' => 'Комментарий добавлён',

View File

@@ -33,17 +33,17 @@ return [
'app_primary_color_desc' => 'Это должно быть указано в hex. <br>Оставьте пустым чтобы использовать цвет по-умолчанию.',
'app_homepage' => 'Домашняя страница приложения',
'app_homepage_desc' => 'Выберите страницу, которая будет отображаться на главной странице вместо стандартной. Права на страницы игнорируются для выбранных страниц.',
'app_homepage_default' => 'Домашняя страница по-умолчанию выбрана',
'app_homepage_default' => 'Выбрана домашняя страница по-умолчанию',
/**
* Registration settings
* Registration
*/
'reg_settings' => 'Настройки регистрации',
'reg_allow' => 'Открыть регистрацию?',
'reg_default_role' => 'Роль пользователя по-умолчанию после регистрации',
'reg_confirm_email' => 'Требуется подтверждение по электронной почте?',
'reg_confirm_email_desc' => 'Если используется ограничение домена, тогда потребуется подтверждение по электронной почте, а значение ниже будет проигнорировано.',
'reg_confirm_email_desc' => 'Если используется ограничение домена, тогда потребуется подтверждение по электронной почте и этот пункт будет проигнорирован.',
'reg_confirm_restrict_domain' => 'Ограничить регистрацию по домену',
'reg_confirm_restrict_domain_desc' => 'EВведите список доменов электронной почты, разделенных запятыми, на которые вы хотели бы ограничить регистрацию. Пользователям будет отправлено электронное письмо, чтобы подтвердить их адрес, прежде чем им разрешат взаимодействовать с приложением. <br> Обратите внимание, что пользователи смогут изменять свои адреса электронной почты после успешной регистрации.',
'reg_confirm_restrict_domain_placeholder' => 'Нет ограничений',
@@ -90,9 +90,9 @@ return [
'user_profile' => 'Профиль пользователя',
'users_add_new' => 'Добавить нового пользователя',
'users_search' => 'Поиск пользователей',
'users_role' => 'Поли пользователя',
'users_role' => 'Роли пользователя',
'users_external_auth_id' => 'Внешний ID аутентификации',
'users_password_warning' => 'Просто заполните ниже, если вы хотите изменить свой пароль:',
'users_password_warning' => 'Введите ниже свой пароль новый пароль для его изменения:',
'users_system_public' => 'Этот пользователь представляет любых гостевых пользователей, которые посещают ваше приложение. Он не может использоваться для входа в систему и назначается автоматически.',
'users_delete' => 'Удалить пользователя',
'users_delete_named' => 'Удалить пользователя :userName',

View File

@@ -1,5 +1,5 @@
<div toolbox class="floating-toolbox">
<div editor-toolbox class="floating-toolbox">
<div class="tabs primary-background-light">
<span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>

View File

@@ -1,5 +1,5 @@
<div class="page-editor flex-fill flex" ng-controller="PageEditController" drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}" 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 }}">
<div class="page-editor flex-fill flex" id="page-editor" drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}" 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() }}
@@ -15,30 +15,30 @@
</div>
<div class="col-sm-4 faded text-center">
<div ng-show="draftsEnabled" dropdown class="dropdown-container draft-display">
<a dropdown-toggle class="text-primary text-button"><span class="faded-text" ng-bind="draftText"></span>&nbsp; <i class="zmdi zmdi-more-vert"></i></a>
<i class="zmdi zmdi-check-circle text-pos draft-notification" ng-class="{visible: draftUpdated}"></i>
<div v-show="draftsEnabled" dropdown class="dropdown-container draft-display">
<a dropdown-toggle class="text-primary text-button"><span class="faded-text" v-text="draftText"></span>&nbsp; <i class="zmdi zmdi-more-vert"></i></a>
<i class="zmdi zmdi-check-circle text-pos draft-notification" :class="{visible: draftUpdated}"></i>
<ul>
<li>
<a ng-click="forceDraftSave()" class="text-pos"><i class="zmdi zmdi-save"></i>{{ trans('entities.pages_edit_save_draft') }}</a>
<a @click="saveDraft()" class="text-pos"><i class="zmdi zmdi-save"></i>{{ trans('entities.pages_edit_save_draft') }}</a>
</li>
<li ng-if="isNewPageDraft">
<li v-if="isNewDraft">
<a href="{{ $model->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>{{ trans('entities.pages_edit_delete_draft') }}</a>
</li>
<li>
<a type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-neg"><i class="zmdi zmdi-close-circle"></i>{{ trans('entities.pages_edit_discard_draft') }}</a>
<li v-if="isUpdateDraft">
<a type="button" @click="discardDraft" class="text-neg"><i class="zmdi zmdi-close-circle"></i>{{ trans('entities.pages_edit_discard_draft') }}</a>
</li>
</ul>
</div>
</div>
<div class="col-sm-4 faded">
<div class="action-buttons" ng-cloak>
<div class="action-buttons" v-cloak>
<div dropdown class="dropdown-container">
<a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-edit"></i> <span ng-bind="(changeSummary | limitTo:16) + (changeSummary.length>16?'...':'') || '{{ trans('entities.pages_edit_set_changelog') }}'"></span></a>
<a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-edit"></i> <span v-text="changeSummaryShort"></span></a>
<ul class="wide">
<li class="padded">
<p class="text-muted">{{ trans('entities.pages_edit_enter_changelog_desc') }}</p>
<input name="summary" id="summary-input" type="text" placeholder="{{ trans('entities.pages_edit_enter_changelog') }}" ng-model="changeSummary" />
<input name="summary" id="summary-input" type="text" placeholder="{{ trans('entities.pages_edit_enter_changelog') }}" v-model="changeSummary" />
</li>
</ul>
</div>
@@ -62,9 +62,9 @@
{{--WYSIWYG Editor--}}
@if(setting('app-editor') === 'wysiwyg')
<div tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" class="flex-fill flex">
<div wysiwyg-editor class="flex-fill flex">
<textarea id="html-editor" name="html" rows="5" ng-non-bindable
@if($errors->has('html')) class="neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
@if($errors->has('html')) class="neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
</div>
@if($errors->has('html'))
@@ -74,7 +74,7 @@
{{--Markdown Editor--}}
@if(setting('app-editor') === 'markdown')
<div id="markdown-editor" markdown-editor class="flex-fill flex code-fill">
<div ng-non-bindable id="markdown-editor" markdown-editor class="flex-fill flex code-fill">
<div class="markdown-editor-wrap">
<div class="editor-toolbar">
@@ -82,12 +82,12 @@
<div class="float right buttons">
<button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>{{ trans('entities.pages_md_insert_image') }}</button>
&nbsp;|&nbsp;
<button class="text-button" type="button" data-action="insertEntityLink"><i class="zmdi zmdi-link"></i>{{ trans('entities.pages_md_insert_link') }}</button>
<button class="text-button" type="button" data-action="insertLink"><i class="zmdi zmdi-link"></i>{{ trans('entities.pages_md_insert_link') }}</button>
</div>
</div>
<div markdown-input md-change="editorChange" md-model="editContent" class="flex flex-fill">
<textarea ng-non-bindable id="markdown-editor-input" name="markdown" rows="5"
<div markdown-input class="flex flex-fill">
<textarea id="markdown-editor-input" 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>
@@ -98,13 +98,14 @@
<div class="">{{ trans('entities.pages_md_preview') }}</div>
</div>
<div class="markdown-display">
<div class="page-content" ng-bind-html="displayContent"></div>
<div class="page-content"></div>
</div>
</div>
<input type="hidden" name="html"/>
</div>
<input type="hidden" name="html" ng-value="displayContent">
@if($errors->has('markdown'))
<div class="text-neg text-small">{{ $errors->first('markdown') }}</div>

View File

@@ -1 +1 @@
v0.18.1
v0.18.2