Merge branch 'master' into release

This commit is contained in:
Dan Brown 2017-09-10 17:05:05 +01:00
commit 9bde0ae4ea
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
207 changed files with 6194 additions and 3705 deletions

5
.gitignore vendored
View File

@ -8,16 +8,15 @@ Homestead.yaml
/public/css/*.map
/public/js/*.map
/public/bower
/public/build/
/storage/images
_ide_helper.php
/storage/debugbar
.phpstorm.meta.php
yarn.lock
/bin
nbproject
.buildpath
.project
.settings/org.eclipse.wst.common.project.facet.core.xml
.settings/org.eclipse.php.core.prefs

View File

@ -2,7 +2,7 @@ dist: trusty
sudo: false
language: php
php:
- 7.0
- 7.0.7
cache:
directories:

43
app/Comment.php Normal file
View File

@ -0,0 +1,43 @@
<?php namespace BookStack;
class Comment extends Ownable
{
protected $fillable = ['text', 'html', 'parent_id'];
protected $appends = ['created', 'updated'];
/**
* Get the entity that this comment belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function entity()
{
return $this->morphTo('entity');
}
/**
* Check if a comment has been updated since creation.
* @return bool
*/
public function isUpdated()
{
return $this->updated_at->timestamp > $this->created_at->timestamp;
}
/**
* Get created date as a relative diff.
* @return mixed
*/
public function getCreatedAttribute()
{
return $this->created_at->diffForHumans();
}
/**
* Get updated date as a relative diff.
* @return mixed
*/
public function getUpdatedAttribute()
{
return $this->updated_at->diffForHumans();
}
}

View File

@ -1,6 +1,8 @@
<?php namespace BookStack;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Entity extends Ownable
{
@ -65,6 +67,17 @@ class Entity extends Ownable
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
}
/**
* Get the comments for an entity
* @param bool $orderByCreated
* @return MorphMany
*/
public function comments($orderByCreated = true)
{
$query = $this->morphMany(Comment::class, 'entity');
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
}
/**
* Get the related search terms.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany

View File

@ -8,6 +8,7 @@ use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
use BookStack\Services\EmailConfirmationService;
use BookStack\Services\SocialAuthService;
use BookStack\SocialAccount;
use BookStack\User;
use Exception;
use Illuminate\Http\Request;
@ -103,7 +104,7 @@ class RegisterController extends Controller
* @param Request|\Illuminate\Http\Request $request
* @return Response
* @throws UserRegistrationException
* @throws \Illuminate\Foundation\Validation\ValidationException
* @throws \Illuminate\Validation\ValidationException
*/
public function postRegister(Request $request)
{
@ -230,7 +231,6 @@ class RegisterController extends Controller
return redirect('/register/confirm');
}
$this->emailConfirmationService->sendConfirmation($user);
session()->flash('success', trans('auth.email_confirm_resent'));
return redirect('/register/confirm');
}
@ -255,16 +255,13 @@ class RegisterController extends Controller
*/
public function socialCallback($socialDriver)
{
if (session()->has('social-callback')) {
$action = session()->pull('social-callback');
if ($action == 'login') {
return $this->socialAuthService->handleLoginCallback($socialDriver);
} elseif ($action == 'register') {
return $this->socialRegisterCallback($socialDriver);
}
} else {
if (!session()->has('social-callback')) {
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
}
$action = session()->pull('social-callback');
if ($action == 'login') return $this->socialAuthService->handleLoginCallback($socialDriver);
if ($action == 'register') return $this->socialRegisterCallback($socialDriver);
return redirect()->back();
}

View File

@ -36,11 +36,17 @@ class BookController extends Controller
*/
public function index()
{
$books = $this->entityRepo->getAllPaginated('book', 10);
$books = $this->entityRepo->getAllPaginated('book', 20);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$this->setPageTitle('Books');
return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]);
return view('books/index', [
'books' => $books,
'recents' => $recents,
'popular' => $popular,
'new' => $new
]);
}
/**
@ -84,7 +90,12 @@ class BookController extends Controller
$bookChildren = $this->entityRepo->getBookChildren($book);
Views::add($book);
$this->setPageTitle($book->getShortName());
return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
return view('books/show', [
'book' => $book,
'current' => $book,
'bookChildren' => $bookChildren,
'activity' => Activity::entityActivity($book, 20, 0)
]);
}
/**

View File

@ -0,0 +1,93 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\CommentRepo;
use BookStack\Repos\EntityRepo;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
class CommentController extends Controller
{
protected $entityRepo;
protected $commentRepo;
/**
* CommentController constructor.
* @param EntityRepo $entityRepo
* @param CommentRepo $commentRepo
*/
public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
{
$this->entityRepo = $entityRepo;
$this->commentRepo = $commentRepo;
parent::__construct();
}
/**
* Save a new comment for a Page
* @param Request $request
* @param integer $pageId
* @param null|integer $commentId
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function savePageComment(Request $request, $pageId, $commentId = null)
{
$this->validate($request, [
'text' => 'required|string',
'html' => 'required|string',
]);
try {
$page = $this->entityRepo->getById('page', $pageId, true);
} catch (ModelNotFoundException $e) {
return response('Not found', 404);
}
$this->checkOwnablePermission('page-view', $page);
// Prevent adding comments to draft pages
if ($page->draft) {
return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400);
}
// Create a new comment.
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
Activity::add($page, 'commented_on', $page->book->id);
return view('comments/comment', ['comment' => $comment]);
}
/**
* Update an existing comment.
* @param Request $request
* @param integer $commentId
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function update(Request $request, $commentId)
{
$this->validate($request, [
'text' => 'required|string',
'html' => 'required|string',
]);
$comment = $this->commentRepo->getById($commentId);
$this->checkOwnablePermission('page-view', $comment->entity);
$this->checkOwnablePermission('comment-update', $comment);
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
return view('comments/comment', ['comment' => $comment]);
}
/**
* Delete a comment from the system.
* @param integer $id
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission('comment-delete', $comment);
$this->commentRepo->delete($comment);
return response()->json(['message' => trans('entities.comment_deleted')]);
}
}

View File

@ -29,15 +29,25 @@ class HomeController extends Controller
$activity = Activity::latest(10);
$draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor);
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5);
return view('home', [
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
// Custom homepage
$customHomepage = false;
$homepageSetting = setting('app-homepage');
if ($homepageSetting) {
$id = intval(explode(':', $homepageSetting)[0]);
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
$this->entityRepo->renderPage($customHomepage, true);
}
$view = $customHomepage ? 'home-custom' : 'home';
return view($view, [
'activity' => $activity,
'recents' => $recents,
'recentlyCreatedPages' => $recentlyCreatedPages,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages
'draftPages' => $draftPages,
'customHomepage' => $customHomepage
]);
}

View File

@ -161,13 +161,14 @@ class PageController extends Controller
$pageContent = $this->entityRepo->renderPage($page);
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
$pageNav = $this->entityRepo->getPageNav($pageContent);
$page->load(['comments.createdBy']);
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', [
'page' => $page,'book' => $page->book,
'current' => $page, 'sidebarTree' => $sidebarTree,
'pageNav' => $pageNav, 'pageContent' => $pageContent]);
'pageNav' => $pageNav]);
}
/**
@ -376,10 +377,11 @@ class PageController extends Controller
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages/revision', [
'page' => $page,
'book' => $page->book,
'revision' => $revision
]);
}
@ -409,6 +411,7 @@ class PageController extends Controller
'page' => $page,
'book' => $page->book,
'diff' => $diff,
'revision' => $revision
]);
}

View File

@ -47,4 +47,16 @@ class PageRevision extends Model
return null;
}
/**
* Allows checking of the exact class, Used to check entity type.
* Included here to align with entities in similar use cases.
* (Yup, Bit of an awkward hack)
* @param $type
* @return bool
*/
public static function isA($type)
{
return $type === 'revision';
}
}

87
app/Repos/CommentRepo.php Normal file
View File

@ -0,0 +1,87 @@
<?php namespace BookStack\Repos;
use BookStack\Comment;
use BookStack\Entity;
/**
* Class CommentRepo
* @package BookStack\Repos
*/
class CommentRepo {
/**
* @var Comment $comment
*/
protected $comment;
/**
* CommentRepo constructor.
* @param Comment $comment
*/
public function __construct(Comment $comment)
{
$this->comment = $comment;
}
/**
* Get a comment by ID.
* @param $id
* @return Comment|\Illuminate\Database\Eloquent\Model
*/
public function getById($id)
{
return $this->comment->newQuery()->findOrFail($id);
}
/**
* Create a new comment on an entity.
* @param Entity $entity
* @param array $data
* @return Comment
*/
public function create (Entity $entity, $data = [])
{
$userId = user()->id;
$comment = $this->comment->newInstance($data);
$comment->created_by = $userId;
$comment->updated_by = $userId;
$comment->local_id = $this->getNextLocalId($entity);
$entity->comments()->save($comment);
return $comment;
}
/**
* Update an existing comment.
* @param Comment $comment
* @param array $input
* @return mixed
*/
public function update($comment, $input)
{
$comment->updated_by = user()->id;
$comment->update($input);
return $comment;
}
/**
* Delete a comment from the system.
* @param Comment $comment
* @return mixed
*/
public function delete($comment)
{
return $comment->delete();
}
/**
* Get the next local ID relative to the linked entity.
* @param Entity $entity
* @return int
*/
protected function getNextLocalId(Entity $entity)
{
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
if ($comments === null) return 1;
return $comments->local_id + 1;
}
}

View File

@ -137,10 +137,15 @@ class EntityRepo
* @param string $type
* @param integer $id
* @param bool $allowDrafts
* @param bool $ignorePermissions
* @return Entity
*/
public function getById($type, $id, $allowDrafts = false)
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
{
if ($ignorePermissions) {
$entity = $this->getEntity($type);
return $entity->newQuery()->find($id);
}
return $this->entityQuery($type, $allowDrafts)->find($id);
}
@ -671,9 +676,10 @@ class EntityRepo
/**
* Render the page for viewing, Parsing and performing features such as page transclusion.
* @param Page $page
* @param bool $ignorePermissions
* @return mixed|string
*/
public function renderPage(Page $page)
public function renderPage(Page $page, $ignorePermissions = false)
{
$content = $page->html;
$matches = [];
@ -685,19 +691,19 @@ class EntityRepo
$pageId = intval($splitInclude[0]);
if (is_nan($pageId)) continue;
$page = $this->getById('page', $pageId);
if ($page === null) {
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
if ($matchedPage === null) {
$content = str_replace($matches[0][$index], '', $content);
continue;
}
if (count($splitInclude) === 1) {
$content = str_replace($matches[0][$index], $page->html, $content);
$content = str_replace($matches[0][$index], $matchedPage->html, $content);
continue;
}
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$matchingElem = $doc->getElementById($splitInclude[1]);
if ($matchingElem === null) {
$content = str_replace($matches[0][$index], '', $content);
@ -710,6 +716,7 @@ class EntityRepo
$content = str_replace($matches[0][$index], trim($innerContent), $content);
}
$page->setAttribute('renderedHTML', $content);
return $content;
}

View File

@ -33,6 +33,7 @@ class TagRepo
* @param $entityType
* @param $entityId
* @param string $action
* @return \Illuminate\Database\Eloquent\Model|null|static
*/
public function getEntity($entityType, $entityId, $action = 'view')
{

View File

@ -27,9 +27,9 @@ class ExportService
*/
public function pageToContainedHtml(Page $page)
{
$this->entityRepo->renderPage($page);
$pageHtml = view('pages/export', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
'page' => $page
])->render();
return $this->containHtml($pageHtml);
}
@ -74,9 +74,9 @@ class ExportService
*/
public function pageToPdf(Page $page)
{
$this->entityRepo->renderPage($page);
$html = view('pages/pdf', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
'page' => $page
])->render();
return $this->htmlToPdf($html);
}

View File

@ -468,7 +468,7 @@ class PermissionService
$action = end($explodedPermission);
$this->currentAction = $action;
$nonJointPermissions = ['restrictions', 'image', 'attachment'];
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
// Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) {

View File

@ -92,7 +92,7 @@ class SearchService
return [
'total' => $total,
'count' => count($results),
'results' => $results->sortByDesc('score')
'results' => $results->sortByDesc('score')->values()
];
}

View File

@ -62,7 +62,7 @@ class ViewService
$query->whereIn('viewable_type', $filterModel);
} else if ($filterModel) {
$query->where('viewable_type', '=', get_class($filterModel));
};
}
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
}

2
config/app.php Normal file → Executable file
View File

@ -58,7 +58,7 @@ return [
*/
'locale' => env('APP_LANG', 'en'),
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl'],
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl', 'it'],
/*
|--------------------------------------------------------------------------

View File

@ -70,4 +70,14 @@ $factory->define(BookStack\Image::class, function ($faker) {
'type' => 'gallery',
'uploaded_to' => 0
];
});
$factory->define(BookStack\Comment::class, function($faker) {
$text = $faker->paragraph(1);
$html = '<p>' . $text. '</p>';
return [
'html' => $html,
'text' => $text,
'parent_id' => null
];
});

View File

@ -0,0 +1,68 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCommentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->increments('id')->unsigned();
$table->integer('entity_id')->unsigned();
$table->string('entity_type');
$table->longText('text')->nullable();
$table->longText('html')->nullable();
$table->integer('parent_id')->unsigned()->nullable();
$table->integer('local_id')->unsigned()->nullable();
$table->integer('created_by')->unsigned();
$table->integer('updated_by')->unsigned()->nullable();
$table->timestamps();
$table->index(['entity_id', 'entity_type']);
$table->index(['local_id']);
// Assign new comment permissions to admin role
$adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
// Create & attach new entity permissions
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
$entity = 'Comment';
foreach ($ops as $op) {
$permissionId = DB::table('role_permissions')->insertGetId([
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
'display_name' => $op . ' ' . $entity . 's',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
'role_id' => $adminRoleId,
'permission_id' => $permissionId
]);
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('comments');
// Delete comment role permissions
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
$entity = 'Comment';
foreach ($ops as $op) {
$permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
DB::table('role_permissions')->where('name', '=', $permName)->delete();
}
}
}

View File

@ -15,12 +15,11 @@ class DummyContentSeeder extends Seeder
$role = \BookStack\Role::getRole('editor');
$user->attachRole($role);
factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($book) use ($user) {
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($chapter) use ($user, $book){
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
$chapter->pages()->saveMany($pages);
});
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
@ -33,7 +32,6 @@ class DummyContentSeeder extends Seeder
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
app(\BookStack\Services\SearchService::class)->indexAllEntities();
}

View File

@ -1,16 +1,22 @@
'use strict';
const argv = require('yargs').argv;
const gulp = require('gulp'),
plumber = require('gulp-plumber');
const autoprefixer = require('gulp-autoprefixer');
const uglify = require('gulp-uglify');
const minifycss = require('gulp-clean-css');
const sass = require('gulp-sass');
const sourcemaps = require('gulp-sourcemaps');
const browserify = require("browserify");
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const babelify = require("babelify");
const watchify = require("watchify");
const envify = require("envify");
const uglify = require('gulp-uglify');
const gutil = require("gulp-util");
const liveReload = require('gulp-livereload');
@ -19,6 +25,7 @@ let isProduction = argv.production || process.env.NODE_ENV === 'production';
gulp.task('styles', () => {
let chain = gulp.src(['resources/assets/sass/**/*.scss'])
.pipe(sourcemaps.init())
.pipe(plumber({
errorHandler: function (error) {
console.log(error.message);
@ -27,6 +34,7 @@ gulp.task('styles', () => {
.pipe(sass())
.pipe(autoprefixer('last 2 versions'));
if (isProduction) chain = chain.pipe(minifycss());
chain = chain.pipe(sourcemaps.write());
return chain.pipe(gulp.dest('public/css/')).pipe(liveReload());
});

View File

@ -31,15 +31,18 @@
"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",
"clipboard": "^1.5.16",
"clipboard": "^1.7.1",
"codemirror": "^5.26.0",
"dropzone": "^4.0.1",
"gulp-sourcemaps": "^2.6.1",
"gulp-util": "^3.0.8",
"markdown-it": "^8.3.1",
"markdown-it-task-lists": "^2.0.0",
"moment": "^2.12.0",
"vue": "^2.2.6"
"vue": "^2.2.6",
"vuedraggable": "^2.14.1"
},
"browser": {
"vue": "vue/dist/vue.common.js"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,7 +1,7 @@
# BookStack
[![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg?maxAge=2592000)](https://github.com/BookStackApp/BookStack/releases/latest)
[![license](https://img.shields.io/github/license/BookStackApp/BookStack.svg?maxAge=2592000)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest)
[![license](https://img.shields.io/github/license/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
[![Build Status](https://travis-ci.org/BookStackApp/BookStack.svg)](https://travis-ci.org/BookStackApp/BookStack)
A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
@ -13,6 +13,12 @@ A platform for storing and organising information and documentation. General inf
* *Password: `password`*
* [BookStack Blog](https://www.bookstackapp.com/blog)
## Project Definition
BookStack is an opinionated wiki system that provides a pleasant and simple out of the box experience. New users to an instance should find the experience intuitive and only basic word-processing skills should be required to get involved in creating content on BookStack. The platform should provide advanced power features to those that desire it but they should not interfere with the core simple user experience.
BookStack is not designed as an extensible platform to be used for purposes that differ to the statement above.
## Development & Testing
All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements:
@ -79,7 +85,7 @@ These are the great open-source projects used to help build BookStack:
* [jQuery Sortable](https://johnny.github.io/jquery-sortable/)
* [Material Design Iconic Font](http://zavoloklom.github.io/material-design-iconic-font/icons.html)
* [Dropzone.js](http://www.dropzonejs.com/)
* [ZeroClipboard](http://zeroclipboard.org/)
* [clipboard.js](https://clipboardjs.com/)
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
* [Moment.js](http://momentjs.com/)

View File

@ -53,13 +53,20 @@ const modeMap = {
yml: 'yaml',
};
module.exports.highlight = function() {
let codeBlocks = document.querySelectorAll('.page-content pre');
/**
* Highlight pre elements on a page
*/
function highlight() {
let codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre');
for (let i = 0; i < codeBlocks.length; i++) {
highlightElem(codeBlocks[i]);
}
};
}
/**
* Add code highlighting to a single element.
* @param {HTMLElement} elem
*/
function highlightElem(elem) {
let innerCodeElem = elem.querySelector('code[class^=language-]');
let mode = '';
@ -68,7 +75,7 @@ function highlightElem(elem) {
mode = getMode(langName);
}
elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
let content = elem.textContent;
let content = elem.textContent.trim();
CodeMirror(function(elt) {
elem.parentNode.replaceChild(elt, elem);
@ -76,7 +83,7 @@ function highlightElem(elem) {
value: content,
mode: mode,
lineNumbers: true,
theme: 'base16-light',
theme: getTheme(),
readOnly: true
});
}
@ -91,9 +98,21 @@ function getMode(suggestion) {
return (typeof modeMap[suggestion] !== 'undefined') ? modeMap[suggestion] : '';
}
module.exports.highlightElem = highlightElem;
/**
* Ge the theme to use for CodeMirror instances.
* @returns {*|string}
*/
function getTheme() {
return window.codeTheme || 'base16-light';
}
module.exports.wysiwygView = function(elem) {
/**
* Create a CodeMirror instance for showing inside the WYSIWYG editor.
* Manages a textarea element to hold code content.
* @param {HTMLElement} elem
* @returns {{wrap: Element, editor: *}}
*/
function wysiwygView(elem) {
let doc = elem.ownerDocument;
let codeElem = elem.querySelector('code');
@ -122,16 +141,22 @@ module.exports.wysiwygView = function(elem) {
value: content,
mode: getMode(lang),
lineNumbers: true,
theme: 'base16-light',
theme: getTheme(),
readOnly: true
});
setTimeout(() => {
cm.refresh();
}, 300);
return {wrap: newWrap, editor: cm};
};
}
module.exports.popupEditor = function(elem, modeSuggestion) {
/**
* Create a CodeMirror instance to show in the WYSIWYG pop-up editor
* @param {HTMLElement} elem
* @param {String} modeSuggestion
* @returns {*}
*/
function popupEditor(elem, modeSuggestion) {
let content = elem.textContent;
return CodeMirror(function(elt) {
@ -141,22 +166,38 @@ module.exports.popupEditor = function(elem, modeSuggestion) {
value: content,
mode: getMode(modeSuggestion),
lineNumbers: true,
theme: 'base16-light',
theme: getTheme(),
lineWrapping: true
});
};
}
module.exports.setMode = function(cmInstance, modeSuggestion) {
/**
* Set the mode of a codemirror instance.
* @param cmInstance
* @param modeSuggestion
*/
function setMode(cmInstance, modeSuggestion) {
cmInstance.setOption('mode', getMode(modeSuggestion));
};
module.exports.setContent = function(cmInstance, codeContent) {
}
/**
* Set the content of a cm instance.
* @param cmInstance
* @param codeContent
*/
function setContent(cmInstance, codeContent) {
cmInstance.setValue(codeContent);
setTimeout(() => {
cmInstance.refresh();
}, 10);
};
}
module.exports.markdownEditor = function(elem) {
/**
* Get a CodeMirror instace to use for the markdown editor.
* @param {HTMLElement} elem
* @returns {*}
*/
function markdownEditor(elem) {
let content = elem.textContent;
return CodeMirror(function (elt) {
@ -166,13 +207,27 @@ module.exports.markdownEditor = function(elem) {
value: content,
mode: "markdown",
lineNumbers: true,
theme: 'base16-light',
theme: getTheme(),
lineWrapping: true
});
};
}
module.exports.getMetaKey = function() {
/**
* Get the 'meta' key dependant on the user's system.
* @returns {string}
*/
function getMetaKey() {
let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
return mac ? "Cmd" : "Ctrl";
};
}
module.exports = {
highlight: highlight,
highlightElem: highlightElem,
wysiwygView: wysiwygView,
popupEditor: popupEditor,
setMode: setMode,
setContent: setContent,
markdownEditor: markdownEditor,
getMetaKey: getMetaKey,
};

View File

@ -0,0 +1,53 @@
class BackToTop {
constructor(elem) {
this.elem = elem;
this.targetElem = document.getElementById('header');
this.showing = false;
this.breakPoint = 1200;
this.elem.addEventListener('click', this.scrollToTop.bind(this));
window.addEventListener('scroll', this.onPageScroll.bind(this));
}
onPageScroll() {
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
if (!this.showing && scrollTopPos > this.breakPoint) {
this.elem.style.display = 'block';
this.showing = true;
setTimeout(() => {
this.elem.style.opacity = 0.4;
}, 1);
} else if (this.showing && scrollTopPos < this.breakPoint) {
this.elem.style.opacity = 0;
this.showing = false;
setTimeout(() => {
this.elem.style.display = 'none';
}, 500);
}
}
scrollToTop() {
let targetTop = this.targetElem.getBoundingClientRect().top;
let scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body;
let duration = 300;
let start = Date.now();
let scrollStart = this.targetElem.getBoundingClientRect().top;
function setPos() {
let percentComplete = (1-((Date.now() - start) / duration));
let target = Math.abs(percentComplete * scrollStart);
if (percentComplete > 0) {
scrollElem.scrollTop = target;
requestAnimationFrame(setPos.bind(this));
} else {
scrollElem.scrollTop = targetTop;
}
}
requestAnimationFrame(setPos.bind(this));
}
}
module.exports = BackToTop;

View File

@ -0,0 +1,67 @@
class ChapterToggle {
constructor(elem) {
this.elem = elem;
this.isOpen = elem.classList.contains('open');
elem.addEventListener('click', this.click.bind(this));
}
open() {
let list = this.elem.parentNode.querySelector('.inset-list');
this.elem.classList.add('open');
list.style.display = 'block';
list.style.height = '';
let height = list.getBoundingClientRect().height;
list.style.height = '0px';
list.style.overflow = 'hidden';
list.style.transition = 'height ease-in-out 240ms';
let transitionEndBound = onTransitionEnd.bind(this);
function onTransitionEnd() {
list.style.overflow = '';
list.style.height = '';
list.style.transition = '';
list.removeEventListener('transitionend', transitionEndBound);
}
setTimeout(() => {
list.style.height = `${height}px`;
list.addEventListener('transitionend', transitionEndBound)
}, 1);
}
close() {
let list = this.elem.parentNode.querySelector('.inset-list');
this.elem.classList.remove('open');
list.style.display = 'block';
list.style.height = list.getBoundingClientRect().height + 'px';
list.style.overflow = 'hidden';
list.style.transition = 'height ease-in-out 240ms';
let transitionEndBound = onTransitionEnd.bind(this);
function onTransitionEnd() {
list.style.overflow = '';
list.style.height = '';
list.style.transition = '';
list.style.display = 'none';
list.removeEventListener('transitionend', transitionEndBound);
}
setTimeout(() => {
list.style.height = `0px`;
list.addEventListener('transitionend', transitionEndBound)
}, 1);
}
click(event) {
event.preventDefault();
this.isOpen ? this.close() : this.open();
this.isOpen = !this.isOpen;
}
}
module.exports = ChapterToggle;

View File

@ -0,0 +1,48 @@
/**
* Dropdown
* Provides some simple logic to create simple dropdown menus.
*/
class DropDown {
constructor(elem) {
this.container = elem;
this.menu = elem.querySelector('ul');
this.toggle = elem.querySelector('[dropdown-toggle]');
this.setupListeners();
}
show() {
this.menu.style.display = 'block';
this.menu.classList.add('anim', 'menuIn');
this.container.addEventListener('mouseleave', this.hide.bind(this));
// Focus on first input if existing
let input = this.menu.querySelector('input');
if (input !== null) input.focus();
}
hide() {
this.menu.style.display = 'none';
this.menu.classList.remove('anim', 'menuIn');
}
setupListeners() {
// Hide menu on option click
this.container.addEventListener('click', event => {
let possibleChildren = Array.from(this.menu.querySelectorAll('a'));
if (possibleChildren.indexOf(event.target) !== -1) this.hide();
});
// Show dropdown on toggle click
this.toggle.addEventListener('click', this.show.bind(this));
// Hide menu on enter press
this.container.addEventListener('keypress', event => {
if (event.keyCode !== 13) return true;
event.preventDefault();
this.hide();
return false;
});
}
}
module.exports = DropDown;

View File

@ -0,0 +1,47 @@
class EntitySelectorPopup {
constructor(elem) {
this.elem = elem;
window.EntitySelectorPopup = this;
this.callback = null;
this.selection = null;
this.selectButton = elem.querySelector('.entity-link-selector-confirm');
this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this));
window.$events.listen('entity-select-change', this.onSelectionChange.bind(this));
window.$events.listen('entity-select-confirm', this.onSelectionConfirm.bind(this));
}
show(callback) {
this.callback = callback;
this.elem.components.overlay.show();
}
hide() {
this.elem.components.overlay.hide();
}
onSelectButtonClick() {
this.hide();
if (this.selection !== null && this.callback) this.callback(this.selection);
}
onSelectionConfirm(entity) {
this.hide();
if (this.callback && entity) this.callback(entity);
}
onSelectionChange(entity) {
this.selection = entity;
if (entity === null) {
this.selectButton.setAttribute('disabled', 'true');
} else {
this.selectButton.removeAttribute('disabled');
}
}
}
module.exports = EntitySelectorPopup;

View File

@ -0,0 +1,118 @@
class EntitySelector {
constructor(elem) {
this.elem = elem;
this.search = '';
this.lastClick = 0;
let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}`);
this.input = elem.querySelector('[entity-selector-input]');
this.searchInput = elem.querySelector('[entity-selector-search]');
this.loading = elem.querySelector('[entity-selector-loading]');
this.resultsContainer = elem.querySelector('[entity-selector-results]');
this.elem.addEventListener('click', this.onClick.bind(this));
let lastSearch = 0;
this.searchInput.addEventListener('input', event => {
lastSearch = Date.now();
this.showLoading();
setTimeout(() => {
if (Date.now() - lastSearch < 199) return;
this.searchEntities(this.searchInput.value);
}, 200);
});
this.searchInput.addEventListener('keydown', event => {
if (event.keyCode === 13) event.preventDefault();
});
this.showLoading();
this.initialLoad();
}
showLoading() {
this.loading.style.display = 'block';
this.resultsContainer.style.display = 'none';
}
hideLoading() {
this.loading.style.display = 'none';
this.resultsContainer.style.display = 'block';
}
initialLoad() {
window.$http.get(this.searchUrl).then(resp => {
this.resultsContainer.innerHTML = resp.data;
this.hideLoading();
})
}
searchEntities(searchTerm) {
this.input.value = '';
let url = this.searchUrl + `&term=${encodeURIComponent(searchTerm)}`;
window.$http.get(url).then(resp => {
this.resultsContainer.innerHTML = resp.data;
this.hideLoading();
});
}
isDoubleClick() {
let now = Date.now();
let answer = now - this.lastClick < 300;
this.lastClick = now;
return answer;
}
onClick(event) {
let t = event.target;
console.log('click', t);
if (t.matches('.entity-list-item *')) {
event.preventDefault();
event.stopPropagation();
let item = t.closest('[data-entity-type]');
this.selectItem(item);
} else if (t.matches('[data-entity-type]')) {
this.selectItem(t)
}
}
selectItem(item) {
let isDblClick = this.isDoubleClick();
let type = item.getAttribute('data-entity-type');
let id = item.getAttribute('data-entity-id');
let isSelected = !item.classList.contains('selected') || isDblClick;
this.unselectAll();
this.input.value = isSelected ? `${type}:${id}` : '';
if (!isSelected) window.$events.emit('entity-select-change', null);
if (isSelected) {
item.classList.add('selected');
item.classList.add('primary-background');
}
if (!isDblClick && !isSelected) return;
let link = item.querySelector('.entity-list-item-link').getAttribute('href');
let name = item.querySelector('.entity-list-item-name').textContent;
let data = {id: Number(id), name: name, link: link};
if (isDblClick) window.$events.emit('entity-select-confirm', data);
if (isSelected) window.$events.emit('entity-select-change', data);
}
unselectAll() {
let selected = this.elem.querySelectorAll('.selected');
for (let i = 0, len = selected.length; i < len; i++) {
selected[i].classList.remove('selected');
selected[i].classList.remove('primary-background');
}
}
}
module.exports = EntitySelector;

View File

@ -0,0 +1,65 @@
class ExpandToggle {
constructor(elem) {
this.elem = elem;
this.isOpen = false;
this.selector = elem.getAttribute('expand-toggle');
elem.addEventListener('click', this.click.bind(this));
}
open(elemToToggle) {
elemToToggle.style.display = 'block';
elemToToggle.style.height = '';
let height = elemToToggle.getBoundingClientRect().height;
elemToToggle.style.height = '0px';
elemToToggle.style.overflow = 'hidden';
elemToToggle.style.transition = 'height ease-in-out 240ms';
let transitionEndBound = onTransitionEnd.bind(this);
function onTransitionEnd() {
elemToToggle.style.overflow = '';
elemToToggle.style.height = '';
elemToToggle.style.transition = '';
elemToToggle.removeEventListener('transitionend', transitionEndBound);
}
setTimeout(() => {
elemToToggle.style.height = `${height}px`;
elemToToggle.addEventListener('transitionend', transitionEndBound)
}, 1);
}
close(elemToToggle) {
elemToToggle.style.display = 'block';
elemToToggle.style.height = elemToToggle.getBoundingClientRect().height + 'px';
elemToToggle.style.overflow = 'hidden';
elemToToggle.style.transition = 'all ease-in-out 240ms';
let transitionEndBound = onTransitionEnd.bind(this);
function onTransitionEnd() {
elemToToggle.style.overflow = '';
elemToToggle.style.height = '';
elemToToggle.style.transition = '';
elemToToggle.style.display = 'none';
elemToToggle.removeEventListener('transitionend', transitionEndBound);
}
setTimeout(() => {
elemToToggle.style.height = `0px`;
elemToToggle.addEventListener('transitionend', transitionEndBound)
}, 1);
}
click(event) {
event.preventDefault();
let matchingElems = document.querySelectorAll(this.selector);
for (let i = 0, len = matchingElems.length; i < len; i++) {
this.isOpen ? this.close(matchingElems[i]) : this.open(matchingElems[i]);
}
this.isOpen = !this.isOpen;
}
}
module.exports = ExpandToggle;

View File

@ -0,0 +1,51 @@
let componentMapping = {
'dropdown': require('./dropdown'),
'overlay': require('./overlay'),
'back-to-top': require('./back-top-top'),
'notification': require('./notification'),
'chapter-toggle': require('./chapter-toggle'),
'expand-toggle': require('./expand-toggle'),
'entity-selector-popup': require('./entity-selector-popup'),
'entity-selector': require('./entity-selector'),
'sidebar': require('./sidebar'),
'page-picker': require('./page-picker'),
'page-comments': require('./page-comments'),
};
window.components = {};
let componentNames = Object.keys(componentMapping);
initAll();
/**
* Initialize components of the given name within the given element.
* @param {String} componentName
* @param {HTMLElement|Document} parentElement
*/
function initComponent(componentName, parentElement) {
let elems = parentElement.querySelectorAll(`[${componentName}]`);
if (elems.length === 0) return;
let component = componentMapping[componentName];
if (typeof window.components[componentName] === "undefined") window.components[componentName] = [];
for (let j = 0, jLen = elems.length; j < jLen; j++) {
let instance = new component(elems[j]);
if (typeof elems[j].components === 'undefined') elems[j].components = {};
elems[j].components[componentName] = instance;
window.components[componentName].push(instance);
}
}
/**
* Initialize all components found within the given element.
* @param parentElement
*/
function initAll(parentElement) {
if (typeof parentElement === 'undefined') parentElement = document;
for (let i = 0, len = componentNames.length; i < len; i++) {
initComponent(componentNames[i], parentElement);
}
}
window.components.init = initAll;

View File

@ -0,0 +1,41 @@
class Notification {
constructor(elem) {
this.elem = elem;
this.type = elem.getAttribute('notification');
this.textElem = elem.querySelector('span');
this.autohide = this.elem.hasAttribute('data-autohide');
window.$events.listen(this.type, text => {
this.show(text);
});
elem.addEventListener('click', this.hide.bind(this));
if (elem.hasAttribute('data-show')) this.show(this.textElem.textContent);
this.hideCleanup = this.hideCleanup.bind(this);
}
show(textToShow = '') {
this.elem.removeEventListener('transitionend', this.hideCleanup);
this.textElem.textContent = textToShow;
this.elem.style.display = 'block';
setTimeout(() => {
this.elem.classList.add('showing');
}, 1);
if (this.autohide) setTimeout(this.hide.bind(this), 2000);
}
hide() {
this.elem.classList.remove('showing');
this.elem.addEventListener('transitionend', this.hideCleanup);
}
hideCleanup() {
this.elem.style.display = 'none';
this.elem.removeEventListener('transitionend', this.hideCleanup);
}
}
module.exports = Notification;

View File

@ -0,0 +1,39 @@
class Overlay {
constructor(elem) {
this.container = elem;
elem.addEventListener('click', event => {
if (event.target === elem) return this.hide();
});
let closeButtons = elem.querySelectorAll('.overlay-close');
for (let i=0; i < closeButtons.length; i++) {
closeButtons[i].addEventListener('click', this.hide.bind(this));
}
}
toggle(show = true) {
let start = Date.now();
let duration = 240;
function setOpacity() {
let elapsedTime = (Date.now() - start);
let targetOpacity = show ? (elapsedTime / duration) : 1-(elapsedTime / duration);
this.container.style.opacity = targetOpacity;
if (elapsedTime > duration) {
this.container.style.display = show ? 'flex' : 'none';
this.container.style.opacity = '';
} else {
requestAnimationFrame(setOpacity.bind(this));
}
}
requestAnimationFrame(setOpacity.bind(this));
}
hide() { this.toggle(false); }
show() { this.toggle(true); }
}
module.exports = Overlay;

View File

@ -0,0 +1,175 @@
const MarkdownIt = require("markdown-it");
const md = new MarkdownIt({ html: false });
class PageComments {
constructor(elem) {
this.elem = elem;
this.pageId = Number(elem.getAttribute('page-id'));
this.editingComment = null;
this.parentId = null;
this.container = elem.querySelector('[comment-container]');
this.formContainer = elem.querySelector('[comment-form-container]');
if (this.formContainer) {
this.form = this.formContainer.querySelector('form');
this.formInput = this.form.querySelector('textarea');
this.form.addEventListener('submit', this.saveComment.bind(this));
}
this.elem.addEventListener('click', this.handleAction.bind(this));
this.elem.addEventListener('submit', this.updateComment.bind(this));
}
handleAction(event) {
let actionElem = event.target.closest('[action]');
if (event.target.matches('a[href^="#"]')) {
let id = event.target.href.split('#')[1];
window.scrollAndHighlight(document.querySelector('#' + id));
}
if (actionElem === null) return;
event.preventDefault();
let action = actionElem.getAttribute('action');
if (action === 'edit') this.editComment(actionElem.closest('[comment]'));
if (action === 'closeUpdateForm') this.closeUpdateForm();
if (action === 'delete') this.deleteComment(actionElem.closest('[comment]'));
if (action === 'addComment') this.showForm();
if (action === 'hideForm') this.hideForm();
if (action === 'reply') this.setReply(actionElem.closest('[comment]'));
if (action === 'remove-reply-to') this.removeReplyTo();
}
closeUpdateForm() {
if (!this.editingComment) return;
this.editingComment.querySelector('[comment-content]').style.display = 'block';
this.editingComment.querySelector('[comment-edit-container]').style.display = 'none';
}
editComment(commentElem) {
this.hideForm();
if (this.editingComment) this.closeUpdateForm();
commentElem.querySelector('[comment-content]').style.display = 'none';
commentElem.querySelector('[comment-edit-container]').style.display = 'block';
let textArea = commentElem.querySelector('[comment-edit-container] textarea');
let lineCount = textArea.value.split('\n').length;
textArea.style.height = (lineCount * 20) + 'px';
this.editingComment = commentElem;
}
updateComment(event) {
let form = event.target;
event.preventDefault();
let text = form.querySelector('textarea').value;
let reqData = {
text: text,
html: md.render(text),
parent_id: this.parentId || null,
};
this.showLoading(form);
let commentId = this.editingComment.getAttribute('comment');
window.$http.put(window.baseUrl(`/ajax/comment/${commentId}`), reqData).then(resp => {
let newComment = document.createElement('div');
newComment.innerHTML = resp.data;
this.editingComment.innerHTML = newComment.children[0].innerHTML;
window.$events.emit('success', window.trans('entities.comment_updated_success'));
window.components.init(this.editingComment);
this.closeUpdateForm();
this.editingComment = null;
this.hideLoading(form);
});
}
deleteComment(commentElem) {
let id = commentElem.getAttribute('comment');
this.showLoading(commentElem.querySelector('[comment-content]'));
window.$http.delete(window.baseUrl(`/ajax/comment/${id}`)).then(resp => {
commentElem.parentNode.removeChild(commentElem);
window.$events.emit('success', window.trans('entities.comment_deleted_success'));
this.updateCount();
});
}
saveComment(event) {
event.preventDefault();
event.stopPropagation();
let text = this.formInput.value;
let reqData = {
text: text,
html: md.render(text),
parent_id: this.parentId || null,
};
this.showLoading(this.form);
window.$http.post(window.baseUrl(`/ajax/page/${this.pageId}/comment`), reqData).then(resp => {
let newComment = document.createElement('div');
newComment.innerHTML = resp.data;
let newElem = newComment.children[0];
this.container.appendChild(newElem);
window.components.init(newElem);
window.$events.emit('success', window.trans('entities.comment_created_success'));
this.resetForm();
this.updateCount();
});
}
updateCount() {
let count = this.container.children.length;
this.elem.querySelector('[comments-title]').textContent = window.trans_choice('entities.comment_count', count, {count});
}
resetForm() {
this.formInput.value = '';
this.formContainer.appendChild(this.form);
this.hideForm();
this.removeReplyTo();
this.hideLoading(this.form);
}
showForm() {
this.formContainer.style.display = 'block';
this.formContainer.parentNode.style.display = 'block';
this.elem.querySelector('[comment-add-button]').style.display = 'none';
this.formInput.focus();
window.scrollToElement(this.formInput);
}
hideForm() {
this.formContainer.style.display = 'none';
this.formContainer.parentNode.style.display = 'none';
this.elem.querySelector('[comment-add-button]').style.display = 'block';
}
setReply(commentElem) {
this.showForm();
this.parentId = Number(commentElem.getAttribute('local-id'));
this.elem.querySelector('[comment-form-reply-to]').style.display = 'block';
let replyLink = this.elem.querySelector('[comment-form-reply-to] a');
replyLink.textContent = `#${this.parentId}`;
replyLink.href = `#comment${this.parentId}`;
}
removeReplyTo() {
this.parentId = null;
this.elem.querySelector('[comment-form-reply-to]').style.display = 'none';
}
showLoading(formElem) {
let groups = formElem.querySelectorAll('.form-group');
for (let i = 0, len = groups.length; i < len; i++) {
groups[i].style.display = 'none';
}
formElem.querySelector('.form-group.loading').style.display = 'block';
}
hideLoading(formElem) {
let groups = formElem.querySelectorAll('.form-group');
for (let i = 0, len = groups.length; i < len; i++) {
groups[i].style.display = 'block';
}
formElem.querySelector('.form-group.loading').style.display = 'none';
}
}
module.exports = PageComments;

View File

@ -0,0 +1,60 @@
class PagePicker {
constructor(elem) {
this.elem = elem;
this.input = elem.querySelector('input');
this.resetButton = elem.querySelector('[page-picker-reset]');
this.selectButton = elem.querySelector('[page-picker-select]');
this.display = elem.querySelector('[page-picker-display]');
this.defaultDisplay = elem.querySelector('[page-picker-default]');
this.buttonSep = elem.querySelector('span.sep');
this.value = this.input.value;
this.setupListeners();
}
setupListeners() {
// Select click
this.selectButton.addEventListener('click', event => {
window.EntitySelectorPopup.show(entity => {
this.setValue(entity.id, entity.name);
});
});
this.resetButton.addEventListener('click', event => {
this.setValue('', '');
});
}
setValue(value, name) {
this.value = value;
this.input.value = value;
this.controlView(name);
}
controlView(name) {
let hasValue = this.value && this.value !== 0;
toggleElem(this.resetButton, hasValue);
toggleElem(this.buttonSep, hasValue);
toggleElem(this.defaultDisplay, !hasValue);
toggleElem(this.display, hasValue);
if (hasValue) {
let id = this.getAssetIdFromVal();
this.display.textContent = `#${id}, ${name}`;
this.display.href = window.baseUrl(`/link/${id}`);
}
}
getAssetIdFromVal() {
return Number(this.value);
}
}
function toggleElem(elem, show) {
let display = (elem.tagName === 'BUTTON' || elem.tagName === 'SPAN') ? 'inline-block' : 'block';
elem.style.display = show ? display : 'none';
}
module.exports = PagePicker;

View File

@ -0,0 +1,16 @@
class Sidebar {
constructor(elem) {
this.elem = elem;
this.toggleElem = elem.querySelector('.sidebar-toggle');
this.toggleElem.addEventListener('click', this.toggle.bind(this));
}
toggle(show = true) {
this.elem.classList.toggle('open');
}
}
module.exports = Sidebar;

View File

@ -8,256 +8,6 @@ moment.locale('en-gb');
module.exports = function (ngApp, events) {
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
function ($scope, $attrs, $http, $timeout, imageManagerService) {
$scope.images = [];
$scope.imageType = $attrs.imageType;
$scope.selectedImage = false;
$scope.dependantPages = false;
$scope.showing = false;
$scope.hasMore = false;
$scope.imageUpdateSuccess = false;
$scope.imageDeleteSuccess = false;
$scope.uploadedTo = $attrs.uploadedTo;
$scope.view = 'all';
$scope.searching = false;
$scope.searchTerm = '';
let page = 0;
let previousClickTime = 0;
let previousClickImage = 0;
let dataLoaded = false;
let callback = false;
let preSearchImages = [];
let preSearchHasMore = false;
/**
* Used by dropzone to get the endpoint to upload to.
* @returns {string}
*/
$scope.getUploadUrl = function () {
return window.baseUrl('/images/' + $scope.imageType + '/upload');
};
/**
* Cancel the current search operation.
*/
function cancelSearch() {
$scope.searching = false;
$scope.searchTerm = '';
$scope.images = preSearchImages;
$scope.hasMore = preSearchHasMore;
}
$scope.cancelSearch = cancelSearch;
/**
* Runs on image upload, Adds an image to local list of images
* and shows a success message to the user.
* @param file
* @param data
*/
$scope.uploadSuccess = function (file, data) {
$scope.$apply(() => {
$scope.images.unshift(data);
});
events.emit('success', trans('components.image_upload_success'));
};
/**
* Runs the callback and hides the image manager.
* @param returnData
*/
function callbackAndHide(returnData) {
if (callback) callback(returnData);
$scope.hide();
}
/**
* Image select action. Checks if a double-click was fired.
* @param image
*/
$scope.imageSelect = function (image) {
let dblClickTime = 300;
let currentTime = Date.now();
let timeDiff = currentTime - previousClickTime;
if (timeDiff < dblClickTime && image.id === previousClickImage) {
// If double click
callbackAndHide(image);
} else {
// If single
$scope.selectedImage = image;
$scope.dependantPages = false;
}
previousClickTime = currentTime;
previousClickImage = image.id;
};
/**
* Action that runs when the 'Select image' button is clicked.
* Runs the callback and hides the image manager.
*/
$scope.selectButtonClick = function () {
callbackAndHide($scope.selectedImage);
};
/**
* Show the image manager.
* Takes a callback to execute later on.
* @param doneCallback
*/
function show(doneCallback) {
callback = doneCallback;
$scope.showing = true;
$('#image-manager').find('.overlay').css('display', 'flex').hide().fadeIn(240);
// Get initial images if they have not yet been loaded in.
if (!dataLoaded) {
fetchData();
dataLoaded = true;
}
}
// Connects up the image manger so it can be used externally
// such as from TinyMCE.
imageManagerService.show = show;
imageManagerService.showExternal = function (doneCallback) {
$scope.$apply(() => {
show(doneCallback);
});
};
window.ImageManager = imageManagerService;
/**
* Hide the image manager
*/
$scope.hide = function () {
$scope.showing = false;
$('#image-manager').find('.overlay').fadeOut(240);
};
let baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');
/**
* Fetch the list image data from the server.
*/
function fetchData() {
let url = baseUrl + page + '?';
let components = {};
if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo;
if ($scope.searching) components['term'] = $scope.searchTerm;
url += Object.keys(components).map((key) => {
return key + '=' + encodeURIComponent(components[key]);
}).join('&');
$http.get(url).then((response) => {
$scope.images = $scope.images.concat(response.data.images);
$scope.hasMore = response.data.hasMore;
page++;
});
}
$scope.fetchData = fetchData;
/**
* Start a search operation
*/
$scope.searchImages = function() {
if ($scope.searchTerm === '') {
cancelSearch();
return;
}
if (!$scope.searching) {
preSearchImages = $scope.images;
preSearchHasMore = $scope.hasMore;
}
$scope.searching = true;
$scope.images = [];
$scope.hasMore = false;
page = 0;
baseUrl = window.baseUrl('/images/' + $scope.imageType + '/search/');
fetchData();
};
/**
* Set the current image listing view.
* @param viewName
*/
$scope.setView = function(viewName) {
cancelSearch();
$scope.images = [];
$scope.hasMore = false;
page = 0;
$scope.view = viewName;
baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/');
fetchData();
};
/**
* Save the details of an image.
* @param event
*/
$scope.saveImageDetails = function (event) {
event.preventDefault();
let url = window.baseUrl('/images/update/' + $scope.selectedImage.id);
$http.put(url, this.selectedImage).then(response => {
events.emit('success', trans('components.image_update_success'));
}, (response) => {
if (response.status === 422) {
let errors = response.data;
let message = '';
Object.keys(errors).forEach((key) => {
message += errors[key].join('\n');
});
events.emit('error', message);
} else if (response.status === 403) {
events.emit('error', response.data.error);
}
});
};
/**
* Delete an image from system and notify of success.
* Checks if it should force delete when an image
* has dependant pages.
* @param event
*/
$scope.deleteImage = function (event) {
event.preventDefault();
let force = $scope.dependantPages !== false;
let url = window.baseUrl('/images/' + $scope.selectedImage.id);
if (force) url += '?force=true';
$http.delete(url).then((response) => {
$scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
$scope.selectedImage = false;
events.emit('success', trans('components.image_delete_success'));
}, (response) => {
// Pages failure
if (response.status === 400) {
$scope.dependantPages = response.data;
} else if (response.status === 403) {
events.emit('error', response.data.error);
}
});
};
/**
* Simple date creator used to properly format dates.
* @param stringDate
* @returns {Date}
*/
$scope.getDate = function (stringDate) {
return new Date(stringDate);
};
}]);
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
function ($scope, $http, $attrs, $interval, $timeout, $sce) {
@ -394,285 +144,4 @@ module.exports = function (ngApp, events) {
};
}]);
ngApp.controller('PageTagController', ['$scope', '$http', '$attrs',
function ($scope, $http, $attrs) {
const pageId = Number($attrs.pageId);
$scope.tags = [];
$scope.sortOptions = {
handle: '.handle',
items: '> tr',
containment: "parent",
axis: "y"
};
/**
* Push an empty tag to the end of the scope tags.
*/
function addEmptyTag() {
$scope.tags.push({
name: '',
value: ''
});
}
$scope.addEmptyTag = addEmptyTag;
/**
* Get all tags for the current book and add into scope.
*/
function getTags() {
let url = window.baseUrl(`/ajax/tags/get/page/${pageId}`);
$http.get(url).then((responseData) => {
$scope.tags = responseData.data;
addEmptyTag();
});
}
getTags();
/**
* Set the order property on all tags.
*/
function setTagOrder() {
for (let i = 0; i < $scope.tags.length; i++) {
$scope.tags[i].order = i;
}
}
/**
* When an tag changes check if another empty editable
* field needs to be added onto the end.
* @param tag
*/
$scope.tagChange = function(tag) {
let cPos = $scope.tags.indexOf(tag);
if (cPos !== $scope.tags.length-1) return;
if (tag.name !== '' || tag.value !== '') {
addEmptyTag();
}
};
/**
* When an tag field loses focus check the tag to see if its
* empty and therefore could be removed from the list.
* @param tag
*/
$scope.tagBlur = function(tag) {
let isLast = $scope.tags.length - 1 === $scope.tags.indexOf(tag);
if (tag.name === '' && tag.value === '' && !isLast) {
let cPos = $scope.tags.indexOf(tag);
$scope.tags.splice(cPos, 1);
}
};
/**
* Remove a tag from the current list.
* @param tag
*/
$scope.removeTag = function(tag) {
let cIndex = $scope.tags.indexOf(tag);
$scope.tags.splice(cIndex, 1);
};
}]);
ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs',
function ($scope, $http, $attrs) {
const pageId = $scope.uploadedTo = $attrs.pageId;
let currentOrder = '';
$scope.files = [];
$scope.editFile = false;
$scope.file = getCleanFile();
$scope.errors = {
link: {},
edit: {}
};
function getCleanFile() {
return {
page_id: pageId
};
}
// Angular-UI-Sort options
$scope.sortOptions = {
handle: '.handle',
items: '> tr',
containment: "parent",
axis: "y",
stop: sortUpdate,
};
/**
* Event listener for sort changes.
* Updates the file ordering on the server.
* @param event
* @param ui
*/
function sortUpdate(event, ui) {
let newOrder = $scope.files.map(file => {return file.id}).join(':');
if (newOrder === currentOrder) return;
currentOrder = newOrder;
$http.put(window.baseUrl(`/attachments/sort/page/${pageId}`), {files: $scope.files}).then(resp => {
events.emit('success', resp.data.message);
}, checkError('sort'));
}
/**
* Used by dropzone to get the endpoint to upload to.
* @returns {string}
*/
$scope.getUploadUrl = function (file) {
let suffix = (typeof file !== 'undefined') ? `/${file.id}` : '';
return window.baseUrl(`/attachments/upload${suffix}`);
};
/**
* Get files for the current page from the server.
*/
function getFiles() {
let url = window.baseUrl(`/attachments/get/page/${pageId}`);
$http.get(url).then(resp => {
$scope.files = resp.data;
currentOrder = resp.data.map(file => {return file.id}).join(':');
}, checkError('get'));
}
getFiles();
/**
* Runs on file upload, Adds an file to local file list
* and shows a success message to the user.
* @param file
* @param data
*/
$scope.uploadSuccess = function (file, data) {
$scope.$apply(() => {
$scope.files.push(data);
});
events.emit('success', trans('entities.attachments_file_uploaded'));
};
/**
* Upload and overwrite an existing file.
* @param file
* @param data
*/
$scope.uploadSuccessUpdate = function (file, data) {
$scope.$apply(() => {
let search = filesIndexOf(data);
if (search !== -1) $scope.files[search] = data;
if ($scope.editFile) {
$scope.editFile = angular.copy(data);
data.link = '';
}
});
events.emit('success', trans('entities.attachments_file_updated'));
};
/**
* Delete a file from the server and, on success, the local listing.
* @param file
*/
$scope.deleteFile = function(file) {
if (!file.deleting) {
file.deleting = true;
return;
}
$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
events.emit('success', resp.data.message);
$scope.files.splice($scope.files.indexOf(file), 1);
}, checkError('delete'));
};
/**
* Attach a link to a page.
* @param file
*/
$scope.attachLinkSubmit = function(file) {
file.uploaded_to = pageId;
$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
$scope.files.push(resp.data);
events.emit('success', trans('entities.attachments_link_attached'));
$scope.file = getCleanFile();
}, checkError('link'));
};
/**
* Start the edit mode for a file.
* @param file
*/
$scope.startEdit = function(file) {
$scope.editFile = angular.copy(file);
$scope.editFile.link = (file.external) ? file.path : '';
};
/**
* Cancel edit mode
*/
$scope.cancelEdit = function() {
$scope.editFile = false;
};
/**
* Update the name and link of a file.
* @param file
*/
$scope.updateFile = function(file) {
$http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
let search = filesIndexOf(resp.data);
if (search !== -1) $scope.files[search] = resp.data;
if ($scope.editFile && !file.external) {
$scope.editFile.link = '';
}
$scope.editFile = false;
events.emit('success', trans('entities.attachments_updated_success'));
}, checkError('edit'));
};
/**
* Get the url of a file.
*/
$scope.getFileUrl = function(file) {
return window.baseUrl('/attachments/' + file.id);
};
/**
* Search the local files via another file object.
* Used to search via object copies.
* @param file
* @returns int
*/
function filesIndexOf(file) {
for (let i = 0; i < $scope.files.length; i++) {
if ($scope.files[i].id == file.id) return i;
}
return -1;
}
/**
* Check for an error response in a ajax request.
* @param errorGroupName
*/
function checkError(errorGroupName) {
$scope.errors[errorGroupName] = {};
return function(response) {
if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') {
events.emit('error', response.data.error);
}
if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') {
$scope.errors[errorGroupName] = response.data.validation;
console.log($scope.errors[errorGroupName])
}
}
}
}]);
};

View File

@ -1,158 +1,10 @@
"use strict";
const DropZone = require("dropzone");
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
const code = require('./code');
module.exports = function (ngApp, events) {
/**
* Common tab controls using simple jQuery functions.
*/
ngApp.directive('tabContainer', function() {
return {
restrict: 'A',
link: function (scope, element, attrs) {
const $content = element.find('[tab-content]');
const $buttons = element.find('[tab-button]');
if (attrs.tabContainer) {
let initial = attrs.tabContainer;
$buttons.filter(`[tab-button="${initial}"]`).addClass('selected');
$content.hide().filter(`[tab-content="${initial}"]`).show();
} else {
$content.hide().first().show();
$buttons.first().addClass('selected');
}
$buttons.click(function() {
let clickedTab = $(this);
$buttons.removeClass('selected');
$content.hide();
let name = clickedTab.addClass('selected').attr('tab-button');
$content.filter(`[tab-content="${name}"]`).show();
});
}
};
});
/**
* Sub form component to allow inner-form sections to act like their own forms.
*/
ngApp.directive('subForm', function() {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.on('keypress', e => {
if (e.keyCode === 13) {
submitEvent(e);
}
});
element.find('button[type="submit"]').click(submitEvent);
function submitEvent(e) {
e.preventDefault();
if (attrs.subForm) scope.$eval(attrs.subForm);
}
}
};
});
/**
* DropZone
* Used for uploading images
*/
ngApp.directive('dropZone', [function () {
return {
restrict: 'E',
template: `
<div class="dropzone-container">
<div class="dz-message">{{message}}</div>
</div>
`,
scope: {
uploadUrl: '@',
eventSuccess: '=',
eventError: '=',
uploadedTo: '@',
},
link: function (scope, element, attrs) {
scope.message = attrs.message;
if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
let dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
url: scope.uploadUrl,
init: function () {
let dz = this;
dz.on('sending', function (file, xhr, data) {
let token = window.document.querySelector('meta[name=token]').getAttribute('content');
data.append('_token', token);
let uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo;
data.append('uploaded_to', uploadedTo);
});
if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess);
dz.on('success', function (file, data) {
$(file.previewElement).fadeOut(400, function () {
dz.removeFile(file);
});
});
if (typeof scope.eventError !== 'undefined') dz.on('error', scope.eventError);
dz.on('error', function (file, errorMessage, xhr) {
console.log(errorMessage);
console.log(xhr);
function setMessage(message) {
$(file.previewElement).find('[data-dz-errormessage]').text(message);
}
if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
if (errorMessage.file) setMessage(errorMessage.file[0]);
});
}
});
}
};
}]);
/**
* Dropdown
* Provides some simple logic to create small dropdown menus
*/
ngApp.directive('dropdown', [function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
const menu = element.find('ul');
function hide() {
menu.hide();
menu.removeClass('anim menuIn');
}
function show() {
menu.show().addClass('anim menuIn');
element.mouseleave(hide);
// Focus on input if exist in dropdown and hide on enter press
let inputs = menu.find('input');
if (inputs.length > 0) inputs.first().focus();
}
// Hide menu on option click
element.on('click', '> ul a', hide);
// Show dropdown on toggle click.
element.find('[dropdown-toggle]').on('click', show);
// Hide menu on enter press in inputs
element.on('keypress', 'input', event => {
if (event.keyCode !== 13) return true;
event.preventDefault();
hide();
return false;
});
}
};
}]);
/**
* TinyMCE
* An angular wrapper around the tinyMCE editor.
@ -168,7 +20,7 @@ module.exports = function (ngApp, events) {
link: function (scope, element, attrs) {
function tinyMceSetup(editor) {
editor.on('ExecCommand change NodeChange ObjectResized', (e) => {
editor.on('ExecCommand change input NodeChange ObjectResized', (e) => {
let content = editor.getContent();
$timeout(() => {
scope.mceModel = content;
@ -177,7 +29,10 @@ module.exports = function (ngApp, events) {
});
editor.on('keydown', (event) => {
scope.$emit('editor-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) => {
@ -247,7 +102,7 @@ module.exports = function (ngApp, events) {
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">', '</div>');};
extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('<p class="callout info">', '</p>');};
cm.setOption('extraKeys', extraKeys);
// Update data on content change
@ -341,12 +196,13 @@ module.exports = function (ngApp, events) {
}
cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch + (newLineContent.length - 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;
@ -400,7 +256,7 @@ module.exports = function (ngApp, events) {
// Show the popup link selector and insert a link when finished
function showLinkSelector() {
let cursorPos = cm.getCursor('from');
window.showEntityLinkSelector(entity => {
window.EntitySelectorPopup.show(entity => {
let selectedText = cm.getSelection() || entity.name;
let newText = `[${selectedText}](${entity.link})`;
cm.focus();
@ -422,7 +278,7 @@ module.exports = function (ngApp, events) {
// Show the image manager and handle image insertion
function showImageManager() {
let cursorPos = cm.getCursor('from');
window.ImageManager.showExternal(image => {
window.ImageManager.show(image => {
let selectedText = cm.getSelection();
let newText = "![" + (selectedText || image.name) + "](" + image.thumbs.display + ")";
cm.focus();
@ -534,333 +390,4 @@ module.exports = function (ngApp, events) {
}
}
}]);
/**
* Tag Autosuggestions
* Listens to child inputs and provides autosuggestions depending on field type
* and input. Suggestions provided by server.
*/
ngApp.directive('tagAutosuggestions', ['$http', function ($http) {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
// Local storage for quick caching.
const localCache = {};
// Create suggestion element
const suggestionBox = document.createElement('ul');
suggestionBox.className = 'suggestion-box';
suggestionBox.style.position = 'absolute';
suggestionBox.style.display = 'none';
const $suggestionBox = $(suggestionBox);
// General state tracking
let isShowing = false;
let currentInput = false;
let active = 0;
// Listen to input events on autosuggest fields
elem.on('input focus', '[autosuggest]', function (event) {
let $input = $(this);
let val = $input.val();
let url = $input.attr('autosuggest');
let type = $input.attr('autosuggest-type');
// Add name param to request if for a value
if (type.toLowerCase() === 'value') {
let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
let nameVal = $nameInput.val();
if (nameVal !== '') {
url += '?name=' + encodeURIComponent(nameVal);
}
}
let suggestionPromise = getSuggestions(val.slice(0, 3), url);
suggestionPromise.then(suggestions => {
if (val.length === 0) {
displaySuggestions($input, suggestions.slice(0, 6));
} else {
suggestions = suggestions.filter(item => {
return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
}).slice(0, 4);
displaySuggestions($input, suggestions);
}
});
});
// Hide autosuggestions when input loses focus.
// Slight delay to allow clicks.
let lastFocusTime = 0;
elem.on('blur', '[autosuggest]', function (event) {
let startTime = Date.now();
setTimeout(() => {
if (lastFocusTime < startTime) {
$suggestionBox.hide();
isShowing = false;
}
}, 200)
});
elem.on('focus', '[autosuggest]', function (event) {
lastFocusTime = Date.now();
});
elem.on('keydown', '[autosuggest]', function (event) {
if (!isShowing) return;
let suggestionElems = suggestionBox.childNodes;
let suggestCount = suggestionElems.length;
// Down arrow
if (event.keyCode === 40) {
let newActive = (active === suggestCount - 1) ? 0 : active + 1;
changeActiveTo(newActive, suggestionElems);
}
// Up arrow
else if (event.keyCode === 38) {
let newActive = (active === 0) ? suggestCount - 1 : active - 1;
changeActiveTo(newActive, suggestionElems);
}
// Enter or tab key
else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
currentInput[0].value = suggestionElems[active].textContent;
currentInput.focus();
$suggestionBox.hide();
isShowing = false;
if (event.keyCode === 13) {
event.preventDefault();
return false;
}
}
});
// Change the active suggestion to the given index
function changeActiveTo(index, suggestionElems) {
suggestionElems[active].className = '';
active = index;
suggestionElems[active].className = 'active';
}
// Display suggestions on a field
let prevSuggestions = [];
function displaySuggestions($input, suggestions) {
// Hide if no suggestions
if (suggestions.length === 0) {
$suggestionBox.hide();
isShowing = false;
prevSuggestions = suggestions;
return;
}
// Otherwise show and attach to input
if (!isShowing) {
$suggestionBox.show();
isShowing = true;
}
if ($input !== currentInput) {
$suggestionBox.detach();
$input.after($suggestionBox);
currentInput = $input;
}
// Return if no change
if (prevSuggestions.join() === suggestions.join()) {
prevSuggestions = suggestions;
return;
}
// Build suggestions
$suggestionBox[0].innerHTML = '';
for (let i = 0; i < suggestions.length; i++) {
let suggestion = document.createElement('li');
suggestion.textContent = suggestions[i];
suggestion.onclick = suggestionClick;
if (i === 0) {
suggestion.className = 'active';
active = 0;
}
$suggestionBox[0].appendChild(suggestion);
}
prevSuggestions = suggestions;
}
// Suggestion click event
function suggestionClick(event) {
currentInput[0].value = this.textContent;
currentInput.focus();
$suggestionBox.hide();
isShowing = false;
}
// Get suggestions & cache
function getSuggestions(input, url) {
let hasQuery = url.indexOf('?') !== -1;
let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input);
// Get from local cache if exists
if (typeof localCache[searchUrl] !== 'undefined') {
return new Promise((resolve, reject) => {
resolve(localCache[searchUrl]);
});
}
return $http.get(searchUrl).then(response => {
localCache[searchUrl] = response.data;
return response.data;
});
}
}
}
}]);
ngApp.directive('entityLinkSelector', [function($http) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
const selectButton = element.find('.entity-link-selector-confirm');
let callback = false;
let entitySelection = null;
// Handle entity selection change, Stores the selected entity locally
function entitySelectionChange(entity) {
entitySelection = entity;
if (entity === null) {
selectButton.attr('disabled', 'true');
} else {
selectButton.removeAttr('disabled');
}
}
events.listen('entity-select-change', entitySelectionChange);
// Handle selection confirm button click
selectButton.click(event => {
hide();
if (entitySelection !== null) callback(entitySelection);
});
// Show selector interface
function show() {
element.fadeIn(240);
}
// Hide selector interface
function hide() {
element.fadeOut(240);
}
// Listen to confirmation of entity selections (doubleclick)
events.listen('entity-select-confirm', entity => {
hide();
callback(entity);
});
// Show entity selector, Accessible globally, and store the callback
window.showEntityLinkSelector = function(passedCallback) {
show();
callback = passedCallback;
};
}
};
}]);
ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) {
return {
restrict: 'A',
scope: true,
link: function (scope, element, attrs) {
scope.loading = true;
scope.entityResults = false;
scope.search = '';
// Add input for forms
const input = element.find('[entity-selector-input]').first();
// Detect double click events
let lastClick = 0;
function isDoubleClick() {
let now = Date.now();
let answer = now - lastClick < 300;
lastClick = now;
return answer;
}
// Listen to entity item clicks
element.on('click', '.entity-list a', function(event) {
event.preventDefault();
event.stopPropagation();
let item = $(this).closest('[data-entity-type]');
itemSelect(item, isDoubleClick());
});
element.on('click', '[data-entity-type]', function(event) {
itemSelect($(this), isDoubleClick());
});
// Select entity action
function itemSelect(item, doubleClick) {
let entityType = item.attr('data-entity-type');
let entityId = item.attr('data-entity-id');
let isSelected = !item.hasClass('selected') || doubleClick;
element.find('.selected').removeClass('selected').removeClass('primary-background');
if (isSelected) item.addClass('selected').addClass('primary-background');
let newVal = isSelected ? `${entityType}:${entityId}` : '';
input.val(newVal);
if (!isSelected) {
events.emit('entity-select-change', null);
}
if (!doubleClick && !isSelected) return;
let link = item.find('.entity-list-item-link').attr('href');
let name = item.find('.entity-list-item-name').text();
if (doubleClick) {
events.emit('entity-select-confirm', {
id: Number(entityId),
name: name,
link: link
});
}
if (isSelected) {
events.emit('entity-select-change', {
id: Number(entityId),
name: name,
link: link
});
}
}
// Get search url with correct types
function getSearchUrl() {
let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter');
return window.baseUrl(`/ajax/search/entities?types=${types}`);
}
// Get initial contents
$http.get(getSearchUrl()).then(resp => {
scope.entityResults = $sce.trustAsHtml(resp.data);
scope.loading = false;
});
// Search when typing
scope.searchEntities = function() {
scope.loading = true;
input.val('');
let url = getSearchUrl() + '&term=' + encodeURIComponent(scope.search);
$http.get(url).then(resp => {
scope.entityResults = $sce.trustAsHtml(resp.data);
scope.loading = false;
});
};
}
};
}]);
};

View File

@ -0,0 +1,20 @@
/**
* Polyfills for DOM API's
*/
if (!Element.prototype.matches) {
Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
}
if (!Element.prototype.closest) {
Element.prototype.closest = function (s) {
var el = this;
var ancestor = this;
if (!document.documentElement.contains(el)) return null;
do {
if (ancestor.matches(s)) return ancestor;
ancestor = ancestor.parentElement;
} while (ancestor !== null);
return null;
};
}

View File

@ -1,4 +1,6 @@
"use strict";
require("babel-polyfill");
require('./dom-polyfills');
// Url retrieval function
window.baseUrl = function(path) {
@ -8,44 +10,15 @@ window.baseUrl = function(path) {
return basePath + '/' + path;
};
const Vue = require("vue");
const axios = require("axios");
let axiosInstance = axios.create({
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
'baseURL': window.baseUrl('')
}
});
window.$http = axiosInstance;
Vue.prototype.$http = axiosInstance;
require("./vues/vues");
// 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");
let translator = new Translations(window.translations);
window.trans = translator.get.bind(translator);
// Global Event System
class EventManager {
constructor() {
this.listeners = {};
this.stack = [];
}
emit(eventName, eventData) {
this.stack.push({name: eventName, data: eventData});
if (typeof this.listeners[eventName] === 'undefined') return this;
let eventsToStart = this.listeners[eventName];
for (let i = 0; i < eventsToStart.length; i++) {
@ -62,25 +35,95 @@ class EventManager {
}
}
window.Events = new EventManager();
Vue.prototype.$events = window.Events;
window.$events = new EventManager();
const Vue = require("vue");
const axios = require("axios");
let axiosInstance = axios.create({
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
'baseURL': window.baseUrl('')
}
});
axiosInstance.interceptors.request.use(resp => {
return resp;
}, err => {
if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err);
if (typeof err.response.data.error !== "undefined") window.$events.emit('error', err.response.data.error);
if (typeof err.response.data.message !== "undefined") window.$events.emit('error', err.response.data.message);
});
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");
let translator = new Translations(window.translations);
window.trans = translator.get.bind(translator);
window.trans_choice = translator.getPlural.bind(translator);
require("./vues/vues");
require("./components");
// Load in angular specific items
const Services = require('./services');
const Directives = require('./directives');
const Controllers = require('./controllers');
Services(ngApp, window.Events);
Directives(ngApp, window.Events);
Controllers(ngApp, window.Events);
Directives(ngApp, window.$events);
Controllers(ngApp, window.$events);
//Global jQuery Config & Extensions
/**
* Scroll the view to a specific element.
* @param {HTMLElement} element
*/
window.scrollToElement = function(element) {
if (!element) return;
let offset = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
let top = element.getBoundingClientRect().top + offset;
$('html, body').animate({
scrollTop: top - 60 // Adjust to change final scroll position top margin
}, 300);
};
/**
* Scroll and highlight an element.
* @param {HTMLElement} element
*/
window.scrollAndHighlight = function(element) {
if (!element) return;
window.scrollToElement(element);
let color = document.getElementById('custom-styles').getAttribute('data-color-light');
let initColor = window.getComputedStyle(element).getPropertyValue('background-color');
element.style.backgroundColor = color;
setTimeout(() => {
element.classList.add('selectFade');
element.style.backgroundColor = initColor;
}, 10);
setTimeout(() => {
element.classList.remove('selectFade');
element.style.backgroundColor = '';
}, 3000);
};
// Smooth scrolling
jQuery.fn.smoothScrollTo = function () {
if (this.length === 0) return;
$('html, body').animate({
scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin
}, 300); // Adjust to change animations speed (ms)
window.scrollToElement(this[0]);
return this;
};
@ -91,83 +134,11 @@ jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
};
});
// Global jQuery Elements
let notifications = $('.notification');
let successNotification = notifications.filter('.pos');
let errorNotification = notifications.filter('.neg');
let warningNotification = notifications.filter('.warning');
// Notification Events
window.Events.listen('success', function (text) {
successNotification.hide();
successNotification.find('span').text(text);
setTimeout(() => {
successNotification.show();
}, 1);
});
window.Events.listen('warning', function (text) {
warningNotification.find('span').text(text);
warningNotification.show();
});
window.Events.listen('error', function (text) {
errorNotification.find('span').text(text);
errorNotification.show();
});
// Notification hiding
notifications.click(function () {
$(this).fadeOut(100);
});
// Chapter page list toggles
$('.chapter-toggle').click(function (e) {
e.preventDefault();
$(this).toggleClass('open');
$(this).closest('.chapter').find('.inset-list').slideToggle(180);
});
// Back to top button
$('#back-to-top').click(function() {
$('#header').smoothScrollTo();
});
let scrollTopShowing = false;
let scrollTop = document.getElementById('back-to-top');
let scrollTopBreakpoint = 1200;
window.addEventListener('scroll', function() {
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) {
scrollTop.style.display = 'block';
scrollTopShowing = true;
setTimeout(() => {
scrollTop.style.opacity = 0.4;
}, 1);
} else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) {
scrollTop.style.opacity = 0;
scrollTopShowing = false;
setTimeout(() => {
scrollTop.style.display = 'none';
}, 500);
}
});
// Common jQuery actions
$('[data-action="expand-entity-list-details"]').click(function() {
$('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
});
// Popup close
$('.popup-close').click(function() {
$(this).closest('.overlay').fadeOut(240);
});
$('.overlay').click(function(event) {
if (!$(event.target).hasClass('overlay')) return;
$(this).fadeOut(240);
});
// Detect IE for css
if(navigator.userAgent.indexOf('MSIE')!==-1
|| navigator.appVersion.indexOf('Trident/') > 0
|| navigator.userAgent.indexOf('Safari') !== -1){
$('body').addClass('flexbox-support');
document.body.classList.add('flexbox-support');
}
// Page specific items

View File

@ -274,7 +274,7 @@ module.exports = function() {
file_browser_callback: function (field_name, url, type, win) {
if (type === 'file') {
window.showEntityLinkSelector(function(entity) {
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);
@ -283,7 +283,7 @@ module.exports = function() {
if (type === 'image') {
// Show image manager
window.ImageManager.showExternal(function (image) {
window.ImageManager.show(function (image) {
// Set popover link input to image url then fire change event
// to ensure the new value sticks
@ -365,7 +365,7 @@ module.exports = function() {
icon: 'image',
tooltip: 'Insert an image',
onclick: function () {
window.ImageManager.showExternal(function (image) {
window.ImageManager.show(function (image) {
let html = `<a href="${image.url}" target="_blank">`;
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += '</a>';

View File

@ -1,5 +1,3 @@
"use strict";
// Configure ZeroClipboard
const Clipboard = require("clipboard");
const Code = require('../code');
@ -83,15 +81,7 @@ let setupPageShow = window.setupPageShow = function (pageId) {
let idElem = document.getElementById(text);
$('.page-content [data-highlighted]').attr('data-highlighted', '').css('background-color', '');
if (idElem !== null) {
let $idElem = $(idElem);
let color = $('#custom-styles').attr('data-color-light');
$idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo();
setTimeout(() => {
$idElem.addClass('anim').addClass('selectFade').css('background-color', '');
setTimeout(() => {
$idElem.removeClass('selectFade');
}, 3000);
}, 100);
window.scrollAndHighlight(idElem);
} else {
$('.page-content').find(':contains("' + text + '")').smoothScrollTo();
}
@ -108,25 +98,25 @@ let setupPageShow = window.setupPageShow = function (pageId) {
goToText(event.target.getAttribute('href').substr(1));
});
// Make the book-tree sidebar stick in view on scroll
// Make the sidebar stick in view on scroll
let $window = $(window);
let $bookTree = $(".book-tree");
let $bookTreeParent = $bookTree.parent();
let $sidebar = $("#sidebar .scroll-body");
let $bookTreeParent = $sidebar.parent();
// Check the page is scrollable and the content is taller than the tree
let pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height());
let pageScrollable = ($(document).height() > $window.height()) && ($sidebar.height() < $('.page-content').height());
// Get current tree's width and header height
let headerHeight = $("#header").height() + $(".toolbar").height();
let isFixed = $window.scrollTop() > headerHeight;
// Function to fix the tree as a sidebar
function stickTree() {
$bookTree.width($bookTreeParent.width() + 15);
$bookTree.addClass("fixed");
$sidebar.width($bookTreeParent.width() + 15);
$sidebar.addClass("fixed");
isFixed = true;
}
// Function to un-fix the tree back into position
function unstickTree() {
$bookTree.css('width', 'auto');
$bookTree.removeClass("fixed");
$sidebar.css('width', 'auto');
$sidebar.removeClass("fixed");
isFixed = false;
}
// Checks if the tree stickiness state should change
@ -160,7 +150,6 @@ let setupPageShow = window.setupPageShow = function (pageId) {
unstickTree();
}
});
};
module.exports = setupPageShow;

View File

@ -1,12 +0,0 @@
"use strict";
module.exports = function(ngApp, events) {
ngApp.factory('imageManagerService', function() {
return {
show: false,
showExternal: false
};
});
};

View File

@ -20,9 +20,63 @@ class Translator {
* @returns {*}
*/
get(key, replacements) {
let text = this.getTransText(key);
return this.performReplacements(text, replacements);
}
/**
* Get pluralised text, Dependant on the given count.
* Same format at laravel's 'trans_choice' helper.
* @param key
* @param count
* @param replacements
* @returns {*}
*/
getPlural(key, count, replacements) {
let text = this.getTransText(key);
let splitText = text.split('|');
let result = null;
let exactCountRegex = /^{([0-9]+)}/;
let rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
for (let i = 0, len = splitText.length; i < len; i++) {
let t = splitText[i];
// Parse exact matches
let exactMatches = t.match(exactCountRegex);
if (exactMatches !== null && Number(exactMatches[1]) === count) {
result = t.replace(exactCountRegex, '').trim();
break;
}
// Parse range matches
let rangeMatches = t.match(rangeRegex);
if (rangeMatches !== null) {
let rangeStart = Number(rangeMatches[1]);
if (rangeStart <= count && (rangeMatches[2] === '*' || Number(rangeMatches[2]) >= count)) {
result = t.replace(rangeRegex, '').trim();
break;
}
}
}
if (result === null && splitText.length > 1) {
result = (count === 1) ? splitText[0] : splitText[1];
}
if (result === null) result = splitText[0];
return this.performReplacements(result, replacements);
}
/**
* Fetched translation text from the store for the given key.
* @param key
* @returns {String|Object}
*/
getTransText(key) {
let splitKey = key.split('.');
let value = splitKey.reduce((a, b) => {
return a != undefined ? a[b] : a;
return a !== undefined ? a[b] : a;
}, this.store);
if (value === undefined) {
@ -30,16 +84,25 @@ class Translator {
value = key;
}
if (replacements === undefined) return value;
return value;
}
let replaceMatches = value.match(/:([\S]+)/g);
if (replaceMatches === null) return value;
/**
* Perform replacements on a string.
* @param {String} string
* @param {Object} replacements
* @returns {*}
*/
performReplacements(string, replacements) {
if (!replacements) return string;
let replaceMatches = string.match(/:([\S]+)/g);
if (replaceMatches === null) return string;
replaceMatches.forEach(match => {
let key = match.substring(1);
if (typeof replacements[key] === 'undefined') return;
value = value.replace(match, replacements[key]);
string = string.replace(match, replacements[key]);
});
return value;
return string;
}
}

View File

@ -0,0 +1,138 @@
const draggable = require('vuedraggable');
const dropzone = require('./components/dropzone');
function mounted() {
this.pageId = this.$el.getAttribute('page-id');
this.file = this.newFile();
this.$http.get(window.baseUrl(`/attachments/get/page/${this.pageId}`)).then(resp => {
this.files = resp.data;
}).catch(err => {
this.checkValidationErrors('get', err);
});
}
let data = {
pageId: null,
files: [],
fileToEdit: null,
file: {},
tab: 'list',
editTab: 'file',
errors: {link: {}, edit: {}, delete: {}}
};
const components = {dropzone, draggable};
let methods = {
newFile() {
return {page_id: this.pageId};
},
getFileUrl(file) {
return window.baseUrl(`/attachments/${file.id}`);
},
fileSortUpdate() {
this.$http.put(window.baseUrl(`/attachments/sort/page/${this.pageId}`), {files: this.files}).then(resp => {
this.$events.emit('success', resp.data.message);
}).catch(err => {
this.checkValidationErrors('sort', err);
});
},
startEdit(file) {
this.fileToEdit = Object.assign({}, file);
this.fileToEdit.link = file.external ? file.path : '';
this.editTab = file.external ? 'link' : 'file';
},
deleteFile(file) {
if (!file.deleting) return file.deleting = true;
this.$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
this.$events.emit('success', resp.data.message);
this.files.splice(this.files.indexOf(file), 1);
}).catch(err => {
this.checkValidationErrors('delete', err)
});
},
uploadSuccess(upload) {
this.files.push(upload.data);
this.$events.emit('success', trans('entities.attachments_file_uploaded'));
},
uploadSuccessUpdate(upload) {
let fileIndex = this.filesIndex(upload.data);
if (fileIndex === -1) {
this.files.push(upload.data)
} else {
this.files.splice(fileIndex, 1, upload.data);
}
if (this.fileToEdit && this.fileToEdit.id === upload.data.id) {
this.fileToEdit = Object.assign({}, upload.data);
}
this.$events.emit('success', trans('entities.attachments_file_updated'));
},
checkValidationErrors(groupName, err) {
console.error(err);
if (typeof err.response.data === "undefined" && typeof err.response.data.validation === "undefined") return;
this.errors[groupName] = err.response.data.validation;
console.log(this.errors[groupName]);
},
getUploadUrl(file) {
let url = window.baseUrl(`/attachments/upload`);
if (typeof file !== 'undefined') url += `/${file.id}`;
return url;
},
cancelEdit() {
this.fileToEdit = null;
},
attachNewLink(file) {
file.uploaded_to = this.pageId;
this.$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
this.files.push(resp.data);
this.file = this.newFile();
this.$events.emit('success', trans('entities.attachments_link_attached'));
}).catch(err => {
this.checkValidationErrors('link', err);
});
},
updateFile(file) {
$http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
let search = this.filesIndex(resp.data);
if (search === -1) {
this.files.push(resp.data);
} else {
this.files.splice(search, 1, resp.data);
}
if (this.fileToEdit && !file.external) this.fileToEdit.link = '';
this.fileToEdit = false;
this.$events.emit('success', trans('entities.attachments_updated_success'));
}).catch(err => {
this.checkValidationErrors('edit', err);
});
},
filesIndex(file) {
for (let i = 0, len = this.files.length; i < len; i++) {
if (this.files[i].id === file.id) return i;
}
return -1;
}
};
module.exports = {
data, methods, mounted, components,
};

View File

@ -0,0 +1,130 @@
const template = `
<div>
<input :value="value" :autosuggest-type="type" ref="input"
:placeholder="placeholder" :name="name"
@input="inputUpdate($event.target.value)" @focus="inputUpdate($event.target.value)"
@blur="inputBlur"
@keydown="inputKeydown"
/>
<ul class="suggestion-box" v-if="showSuggestions">
<li v-for="(suggestion, i) in suggestions"
@click="selectSuggestion(suggestion)"
:class="{active: (i === active)}">{{suggestion}}</li>
</ul>
</div>
`;
function data() {
return {
suggestions: [],
showSuggestions: false,
active: 0,
};
}
const ajaxCache = {};
const props = ['url', 'type', 'value', 'placeholder', 'name'];
function getNameInputVal(valInput) {
let parentRow = valInput.parentNode.parentNode;
let nameInput = parentRow.querySelector('[autosuggest-type="name"]');
return (nameInput === null) ? '' : nameInput.value;
}
const methods = {
inputUpdate(inputValue) {
this.$emit('input', inputValue);
let params = {};
if (this.type === 'value') {
let nameVal = getNameInputVal(this.$el);
if (nameVal !== "") params.name = nameVal;
}
this.getSuggestions(inputValue.slice(0, 3), params).then(suggestions => {
if (inputValue.length === 0) {
this.displaySuggestions(suggestions.slice(0, 6));
return;
}
// Filter to suggestions containing searched term
suggestions = suggestions.filter(item => {
return item.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1;
}).slice(0, 4);
this.displaySuggestions(suggestions);
});
},
inputBlur() {
setTimeout(() => {
this.$emit('blur');
this.showSuggestions = false;
}, 100);
},
inputKeydown(event) {
if (event.keyCode === 13) event.preventDefault();
if (!this.showSuggestions) return;
// Down arrow
if (event.keyCode === 40) {
this.active = (this.active === this.suggestions.length - 1) ? 0 : this.active+1;
}
// Up Arrow
else if (event.keyCode === 38) {
this.active = (this.active === 0) ? this.suggestions.length - 1 : this.active-1;
}
// Enter or tab keys
else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
this.selectSuggestion(this.suggestions[this.active]);
}
// Escape key
else if (event.keyCode === 27) {
this.showSuggestions = false;
}
},
displaySuggestions(suggestions) {
if (suggestions.length === 0) {
this.suggestions = [];
this.showSuggestions = false;
return;
}
this.suggestions = suggestions;
this.showSuggestions = true;
this.active = 0;
},
selectSuggestion(suggestion) {
this.$refs.input.value = suggestion;
this.$refs.input.focus();
this.$emit('input', suggestion);
this.showSuggestions = false;
},
/**
* Get suggestions from BookStack. Store and use local cache if already searched.
* @param {String} input
* @param {Object} params
*/
getSuggestions(input, params) {
params.search = input;
let cacheKey = `${this.url}:${JSON.stringify(params)}`;
if (typeof ajaxCache[cacheKey] !== "undefined") return Promise.resolve(ajaxCache[cacheKey]);
return this.$http.get(this.url, {params}).then(resp => {
ajaxCache[cacheKey] = resp.data;
return resp.data;
});
}
};
const computed = [];
module.exports = {template, data, props, methods, computed};

View File

@ -0,0 +1,60 @@
const DropZone = require("dropzone");
const template = `
<div class="dropzone-container">
<div class="dz-message">{{placeholder}}</div>
</div>
`;
const props = ['placeholder', 'uploadUrl', 'uploadedTo'];
// TODO - Remove jQuery usage
function mounted() {
let container = this.$el;
let _this = this;
new DropZone(container, {
url: function() {
return _this.uploadUrl;
},
init: function () {
let dz = this;
dz.on('sending', function (file, xhr, data) {
let token = window.document.querySelector('meta[name=token]').getAttribute('content');
data.append('_token', token);
let uploadedTo = typeof _this.uploadedTo === 'undefined' ? 0 : _this.uploadedTo;
data.append('uploaded_to', uploadedTo);
});
dz.on('success', function (file, data) {
_this.$emit('success', {file, data});
$(file.previewElement).fadeOut(400, function () {
dz.removeFile(file);
});
});
dz.on('error', function (file, errorMessage, xhr) {
_this.$emit('error', {file, errorMessage, xhr});
console.log(errorMessage);
console.log(xhr);
function setMessage(message) {
$(file.previewElement).find('[data-dz-errormessage]').text(message);
}
if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
if (errorMessage.file) setMessage(errorMessage.file[0]);
});
}
});
}
function data() {
return {}
}
module.exports = {
template,
props,
mounted,
data,
};

View File

@ -0,0 +1,178 @@
const dropzone = require('./components/dropzone');
let page = 0;
let previousClickTime = 0;
let previousClickImage = 0;
let dataLoaded = false;
let callback = false;
let baseUrl = '';
let preSearchImages = [];
let preSearchHasMore = false;
const data = {
images: [],
imageType: false,
uploadedTo: false,
selectedImage: false,
dependantPages: false,
showing: false,
view: 'all',
hasMore: false,
searching: false,
searchTerm: '',
imageUpdateSuccess: false,
imageDeleteSuccess: false,
};
const methods = {
show(providedCallback) {
callback = providedCallback;
this.showing = true;
this.$el.children[0].components.overlay.show();
// Get initial images if they have not yet been loaded in.
if (dataLoaded) return;
this.fetchData();
dataLoaded = true;
},
hide() {
this.showing = false;
this.$el.children[0].components.overlay.hide();
},
fetchData() {
let url = baseUrl + page;
let query = {};
if (this.uploadedTo !== false) query.page_id = this.uploadedTo;
if (this.searching) query.term = this.searchTerm;
this.$http.get(url, {params: query}).then(response => {
this.images = this.images.concat(response.data.images);
this.hasMore = response.data.hasMore;
page++;
});
},
setView(viewName) {
this.cancelSearch();
this.images = [];
this.hasMore = false;
page = 0;
this.view = viewName;
baseUrl = window.baseUrl(`/images/${this.imageType}/${viewName}/`);
this.fetchData();
},
searchImages() {
if (this.searchTerm === '') return this.cancelSearch();
// Cache current settings for later
if (!this.searching) {
preSearchImages = this.images;
preSearchHasMore = this.hasMore;
}
this.searching = true;
this.images = [];
this.hasMore = false;
page = 0;
baseUrl = window.baseUrl(`/images/${this.imageType}/search/`);
this.fetchData();
},
cancelSearch() {
this.searching = false;
this.searchTerm = '';
this.images = preSearchImages;
this.hasMore = preSearchHasMore;
},
imageSelect(image) {
let dblClickTime = 300;
let currentTime = Date.now();
let timeDiff = currentTime - previousClickTime;
let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage;
if (isDblClick) {
this.callbackAndHide(image);
} else {
this.selectedImage = image;
this.dependantPages = false;
}
previousClickTime = currentTime;
previousClickImage = image.id;
},
callbackAndHide(imageResult) {
if (callback) callback(imageResult);
this.hide();
},
saveImageDetails() {
let url = window.baseUrl(`/images/update/${this.selectedImage.id}`);
this.$http.put(url, this.selectedImage).then(response => {
this.$events.emit('success', trans('components.image_update_success'));
}).catch(error => {
if (error.response.status === 422) {
let errors = error.response.data;
let message = '';
Object.keys(errors).forEach((key) => {
message += errors[key].join('\n');
});
this.$events.emit('error', message);
}
});
},
deleteImage() {
let force = this.dependantPages !== false;
let url = window.baseUrl('/images/' + this.selectedImage.id);
if (force) url += '?force=true';
this.$http.delete(url).then(response => {
this.images.splice(this.images.indexOf(this.selectedImage), 1);
this.selectedImage = false;
this.$events.emit('success', trans('components.image_delete_success'));
}).catch(error=> {
if (error.response.status === 400) {
this.dependantPages = error.response.data;
}
});
},
getDate(stringDate) {
return new Date(stringDate);
},
uploadSuccess(event) {
this.images.unshift(event.data);
this.$events.emit('success', trans('components.image_upload_success'));
},
};
const computed = {
uploadUrl() {
return window.baseUrl(`/images/${this.imageType}/upload`);
}
};
function mounted() {
window.ImageManager = this;
this.imageType = this.$el.getAttribute('image-type');
this.uploadedTo = this.$el.getAttribute('uploaded-to');
baseUrl = window.baseUrl('/images/' + this.imageType + '/all/')
}
module.exports = {
mounted,
methods,
data,
computed,
components: {dropzone},
};

View File

@ -0,0 +1,68 @@
const draggable = require('vuedraggable');
const autosuggest = require('./components/autosuggest');
let data = {
pageId: false,
tags: [],
};
const components = {draggable, autosuggest};
const directives = {};
let computed = {};
let methods = {
addEmptyTag() {
this.tags.push({name: '', value: '', key: Math.random().toString(36).substring(7)});
},
/**
* When an tag changes check if another empty editable field needs to be added onto the end.
* @param tag
*/
tagChange(tag) {
let tagPos = this.tags.indexOf(tag);
if (tagPos === this.tags.length-1 && (tag.name !== '' || tag.value !== '')) this.addEmptyTag();
},
/**
* When an tag field loses focus check the tag to see if its
* empty and therefore could be removed from the list.
* @param tag
*/
tagBlur(tag) {
let isLast = (this.tags.indexOf(tag) === this.tags.length-1);
if (tag.name !== '' || tag.value !== '' || isLast) return;
let cPos = this.tags.indexOf(tag);
this.tags.splice(cPos, 1);
},
removeTag(tag) {
let tagPos = this.tags.indexOf(tag);
if (tagPos === -1) return;
this.tags.splice(tagPos, 1);
},
getTagFieldName(index, key) {
return `tags[${index}][${key}]`;
},
};
function mounted() {
this.pageId = Number(this.$el.getAttribute('page-id'));
let url = window.baseUrl(`/ajax/tags/get/page/${this.pageId}`);
this.$http.get(url).then(response => {
let tags = response.data;
for (let i = 0, len = tags.length; i < len; i++) {
tags[i].key = Math.random().toString(36).substring(7);
}
this.tags = tags;
this.addEmptyTag();
});
}
module.exports = {
data, computed, methods, mounted, components, directives
};

View File

@ -6,16 +6,19 @@ function exists(id) {
let vueMapping = {
'search-system': require('./search'),
'entity-dashboard': require('./entity-search'),
'code-editor': require('./code-editor')
'entity-dashboard': require('./entity-dashboard'),
'code-editor': require('./code-editor'),
'image-manager': require('./image-manager'),
'tag-manager': require('./tag-manager'),
'attachment-manager': require('./attachment-manager'),
};
window.vues = {};
Object.keys(vueMapping).forEach(id => {
if (exists(id)) {
let config = vueMapping[id];
config.el = '#' + id;
window.vues[id] = new Vue(config);
}
});
let ids = Object.keys(vueMapping);
for (let i = 0, len = ids.length; i < len; i++) {
if (!exists(ids[i])) continue;
let config = vueMapping[ids[i]];
config.el = '#' + ids[i];
window.vues[ids[i]] = new Vue(config);
}

View File

@ -36,41 +36,12 @@
}
}
.anim.notification {
transform: translate3d(580px, 0, 0);
animation-name: notification;
animation-duration: 3s;
animation-timing-function: ease-in-out;
animation-fill-mode: forwards;
&.stopped {
animation-name: notificationStopped;
}
}
@keyframes notification {
0% {
transform: translate3d(580px, 0, 0);
}
10% {
transform: translate3d(0, 0, 0);
}
90% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(580px, 0, 0);
}
}
@keyframes notificationStopped {
0% {
transform: translate3d(580px, 0, 0);
}
10% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
.anim.menuIn {
transform-origin: 100% 0%;
animation-name: menuIn;
animation-duration: 120ms;
animation-delay: 0s;
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
}
@keyframes menuIn {
@ -85,14 +56,6 @@
}
}
.anim.menuIn {
transform-origin: 100% 0%;
animation-name: menuIn;
animation-duration: 120ms;
animation-delay: 0s;
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
}
@keyframes loadingBob {
0% {
transform: translate3d(0, 0, 0);
@ -128,6 +91,6 @@
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
}
.anim.selectFade {
.selectFade {
transition: background-color ease-in-out 3000ms;
}

View File

@ -134,8 +134,7 @@
.callout {
border-left: 3px solid #BBB;
background-color: #EEE;
padding: $-s;
padding-left: $-xl;
padding: $-s $-s $-s $-xl;
display: block;
position: relative;
&:before {
@ -181,4 +180,78 @@
&.warning:before {
content: '\f1f1';
}
}
.card {
margin: $-m;
background-color: #FFF;
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.2);
h3 {
padding: $-m;
border-bottom: 1px solid #E8E8E8;
margin: 0;
font-size: $fs-s;
color: #888;
font-weight: 400;
text-transform: uppercase;
}
.body, p.empty-text {
padding: $-m;
}
a {
word-wrap: break-word;
word-break: break-word;
}
}
.card.drag-card {
border: 1px solid #DDD;
border-radius: 4px;
display: flex;
padding: 0;
padding-left: $-s + 28px;
margin: $-s 0;
position: relative;
.drag-card-action {
cursor: pointer;
}
.handle, .drag-card-action {
display: flex;
padding: 0;
align-items: center;
text-align: center;
width: 28px;
padding-left: $-xs;
padding-right: $-xs;
&:hover {
background-color: #EEE;
}
i {
flex: 1;
padding: 0;
}
}
> div .outline input {
margin: $-s 0;
}
> div.padded {
padding: $-s 0 !important;
}
.handle {
background-color: #EEE;
left: 0;
position: absolute;
top: 0;
bottom: 0;
}
> div {
padding: 0 $-s;
max-width: 80%;
}
}
.well {
background-color: #F8F8F8;
padding: $-m;
border: 1px solid #DDD;
}

View File

@ -2,9 +2,12 @@
@mixin generate-button-colors($textColor, $backgroundColor) {
background-color: $backgroundColor;
color: $textColor;
text-transform: uppercase;
border: 1px solid $backgroundColor;
vertical-align: top;
&:hover {
background-color: lighten($backgroundColor, 8%);
box-shadow: $bs-med;
//box-shadow: $bs-med;
text-decoration: none;
color: $textColor;
}
@ -26,17 +29,16 @@ $button-border-radius: 2px;
text-decoration: none;
font-size: $fs-m;
line-height: 1.4em;
padding: $-xs $-m;
padding: $-xs*1.3 $-m;
margin: $-xs $-xs $-xs 0;
display: inline-block;
border: none;
font-weight: 500;
font-family: $text;
font-weight: 400;
outline: 0;
border-radius: $button-border-radius;
cursor: pointer;
transition: all ease-in-out 120ms;
box-shadow: 0 0.5px 1.5px 0 rgba(0, 0, 0, 0.21);
box-shadow: 0;
@include generate-button-colors(#EEE, $primary);
}
@ -52,19 +54,54 @@ $button-border-radius: 2px;
@include generate-button-colors(#EEE, $secondary);
}
&.muted {
@include generate-button-colors(#EEE, #888);
@include generate-button-colors(#EEE, #AAA);
}
&.muted-light {
@include generate-button-colors(#666, #e4e4e4);
}
}
.button.outline {
background-color: transparent;
color: #888;
border: 1px solid #DDD;
&:hover, &:focus, &:active {
box-shadow: none;
background-color: #EEE;
}
&.page {
border-color: $color-page;
color: $color-page;
&:hover, &:focus, &:active {
background-color: $color-page;
color: #FFF;
}
}
&.chapter {
border-color: $color-chapter;
color: $color-chapter;
&:hover, &:focus, &:active {
background-color: $color-chapter;
color: #FFF;
}
}
&.book {
border-color: $color-book;
color: $color-book;
&:hover, &:focus, &:active {
background-color: $color-book;
color: #FFF;
}
}
}
.text-button {
@extend .link;
background-color: transparent;
padding: 0;
margin: 0;
border: none;
user-select: none;
&:focus, &:active {
outline: 0;
}

View File

@ -2,7 +2,6 @@
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
}
@ -235,7 +234,6 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
@ -368,9 +366,9 @@ span.CodeMirror-selectedtext { background: none; }
.cm-s-base16-light span.cm-atom { color: #aa759f; }
.cm-s-base16-light span.cm-number { color: #aa759f; }
.cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute { color: #90a959; }
.cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute { color: #678c30; }
.cm-s-base16-light span.cm-keyword { color: #ac4142; }
.cm-s-base16-light span.cm-string { color: #f4bf75; }
.cm-s-base16-light span.cm-string { color: #e09c3c; }
.cm-s-base16-light span.cm-variable { color: #90a959; }
.cm-s-base16-light span.cm-variable-2 { color: #6a9fb5; }
@ -386,7 +384,10 @@ span.CodeMirror-selectedtext { background: none; }
/**
* Custom BookStack overrides
*/
.cm-s-base16-light.CodeMirror {
.CodeMirror, .CodeMirror pre {
font-size: 12px;
}
.CodeMirror {
font-size: 12px;
height: auto;
margin-bottom: $-l;
@ -394,7 +395,7 @@ span.CodeMirror-selectedtext { background: none; }
}
.cm-s-base16-light .CodeMirror-gutters { background: #f5f5f5; border-right: 1px solid #DDD; }
.flex-fill .CodeMirror {
.code-fill .CodeMirror {
position: absolute;
top: 0;
bottom: 0;

View File

@ -1,4 +1,65 @@
.overlay {
// System wide notifications
[notification] {
position: fixed;
top: 0;
right: 0;
margin: $-xl*2 $-xl;
padding: $-l $-xl;
background-color: #EEE;
border-radius: 3px;
box-shadow: $bs-med;
z-index: 999999;
display: block;
cursor: pointer;
max-width: 480px;
transition: transform ease-in-out 360ms;
transform: translate3d(580px, 0, 0);
i, span {
display: table-cell;
}
i {
font-size: 2em;
padding-right: $-l;
}
span {
vertical-align: middle;
}
&.pos {
background-color: $positive;
color: #EEE;
}
&.neg {
background-color: $negative;
color: #EEE;
}
&.warning {
background-color: $secondary;
color: #EEE;
}
&.showing {
transform: translate3d(0, 0, 0);
}
}
[chapter-toggle] {
cursor: pointer;
margin: 0;
transition: all ease-in-out 180ms;
user-select: none;
i.zmdi-caret-right {
transition: all ease-in-out 180ms;
transform: rotate(0deg);
transform-origin: 25% 50%;
}
&.open {
//margin-bottom: 0;
}
&.open i.zmdi-caret-right {
transform: rotate(90deg);
}
}
[overlay] {
background-color: rgba(0, 0, 0, 0.333);
position: fixed;
z-index: 95536;
@ -451,7 +512,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
}
[tab-container] .nav-tabs {
.tab-container .nav-tabs {
text-align: left;
border-bottom: 1px solid #DDD;
margin-bottom: $-m;
@ -479,4 +540,45 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
margin-right: $-xs;
text-decoration: underline;
}
}
.comment-box {
border: 1px solid #DDD;
margin-bottom: $-s;
border-radius: 3px;
.content {
padding: $-s;
font-size: 0.666em;
p, ul {
font-size: $fs-m;
margin: .5em 0;
}
}
.reply-row {
padding: $-xs $-s;
}
}
.comment-box .header {
padding: $-xs $-s;
background-color: #f8f8f8;
border-bottom: 1px solid #DDD;
.meta {
img, a, span {
display: inline-block;
vertical-align: top;
}
a, span {
padding: $-xxs 0 $-xxs 0;
line-height: 1.6;
}
a { color: #666; }
span {
color: #888;
padding-left: $-xxs;
}
}
.text-muted {
color: #999;
}
}

View File

@ -1,102 +0,0 @@
// Generated using https://google-webfonts-helper.herokuapp.com
/* roboto-100 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
src: local('Roboto Thin'), local('Roboto-Thin'),
url('../fonts/roboto-v15-cyrillic_latin-100.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('../fonts/roboto-v15-cyrillic_latin-100.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-100italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 100;
src: local('Roboto Thin Italic'), local('Roboto-ThinItalic'),
url('../fonts/roboto-v15-cyrillic_latin-100italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('../fonts/roboto-v15-cyrillic_latin-100italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-300 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'),
url('../fonts/roboto-v15-cyrillic_latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('../fonts/roboto-v15-cyrillic_latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-300italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 300;
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
url('../fonts/roboto-v15-cyrillic_latin-300italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('../fonts/roboto-v15-cyrillic_latin-300italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-regular - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'),
url('../fonts/roboto-v15-cyrillic_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('../fonts/roboto-v15-cyrillic_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 400;
src: local('Roboto Italic'), local('Roboto-Italic'),
url('../fonts/roboto-v15-cyrillic_latin-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('../fonts/roboto-v15-cyrillic_latin-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-500 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'),
url('../fonts/roboto-v15-cyrillic_latin-500.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('../fonts/roboto-v15-cyrillic_latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-500italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 500;
src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'),
url('../fonts/roboto-v15-cyrillic_latin-500italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('../fonts/roboto-v15-cyrillic_latin-500italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-700 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url('../fonts/roboto-v15-cyrillic_latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('../fonts/roboto-v15-cyrillic_latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-700italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url('../fonts/roboto-v15-cyrillic_latin-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
url('../fonts/roboto-v15-cyrillic_latin-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* roboto-mono-regular - latin */
@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+ */
}

View File

@ -2,15 +2,13 @@
.input-base {
background-color: #FFF;
border-radius: 3px;
border: 1px solid #CCC;
border: 1px solid #D4D4D4;
display: inline-block;
font-size: $fs-s;
font-family: $text;
padding: $-xs;
color: #222;
padding: $-xs*1.5;
color: #666;
width: 250px;
max-width: 100%;
//-webkit-appearance:none;
&.neg, &.invalid {
border: 1px solid $negative;
}
@ -25,6 +23,11 @@
}
}
.fake-input {
@extend .input-base;
overflow: auto;
}
#html-editor {
display: none;
}
@ -33,7 +36,6 @@
position: relative;
z-index: 5;
#markdown-editor-input {
font-family: 'Roboto Mono', monospace;
font-style: normal;
font-weight: 400;
padding: $-xs $-m;
@ -69,7 +71,6 @@
.editor-toolbar {
width: 100%;
padding: $-xs $-m;
font-family: 'Roboto Mono', monospace;
font-size: 11px;
line-height: 1.6;
border-bottom: 1px solid #DDD;
@ -87,8 +88,9 @@ label {
display: block;
line-height: 1.4em;
font-size: 0.94em;
font-weight: 500;
color: #666;
font-weight: 400;
color: #999;
text-transform: uppercase;
padding-bottom: 2px;
margin-bottom: 0.2em;
&.inline {
@ -189,28 +191,15 @@ input:checked + .toggle-switch {
}
.inline-input-style {
border: 2px dotted #BBB;
display: block;
width: 100%;
padding: $-xs $-s;
}
.title-input .input {
width: 100%;
}
.title-input label, .description-input label{
margin-top: $-m;
color: #666;
padding: $-s;
}
.title-input input[type="text"] {
@extend h1;
@extend .inline-input-style;
margin-top: 0;
padding-right: 0;
width: 100%;
color: #444;
font-size: 2em;
}
.title-input.page-title {
@ -251,21 +240,20 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
border: none;
color: $primary;
padding: 0;
margin: 0;
cursor: pointer;
margin-left: $-s;
}
button[type="submit"] {
margin-left: -$-l;
position: absolute;
left: 8px;
top: 9.5px;
}
input {
padding-right: $-l;
display: block;
padding-left: $-l;
width: 300px;
max-width: 100%;
}
}
input.outline {
.outline > input {
border: 0;
border-bottom: 2px solid #DDD;
border-radius: 0;

View File

@ -20,19 +20,128 @@ body.flexbox {
align-items: stretch;
min-height: 0;
position: relative;
.flex, &.flex {
min-height: 0;
flex: 1;
&.rows {
flex-direction: row;
}
&.columns {
flex-direction: column;
}
}
.flex {
min-height: 0;
flex: 1;
}
.flex.scroll {
//overflow-y: auto;
display: flex;
&.sidebar {
margin-right: -14px;
}
}
.flex.scroll .scroll-body {
overflow-y: scroll;
flex: 1;
}
.flex-child > div {
flex: 1;
}
//body.ie .flex-child > div {
// flex: 1 0 0px;
//}
.flex.sidebar {
flex: 1;
background-color: #F2F2F2;
max-width: 360px;
min-height: 90vh;
}
.flex.sidebar + .flex.content {
flex: 3;
background-color: #FFFFFF;
padding: 0 $-l;
border-left: 1px solid #DDD;
max-width: 100%;
}
.flex.sidebar .sidebar-toggle {
display: none;
}
@include smaller-than($xl) {
body.sidebar-layout {
padding-left: 30px;
}
.flex.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 100;
padding-right: 30px;
width: 360px;
box-shadow: none;
transform: translate3d(-330px, 0, 0);
transition: transform ease-in-out 120ms;
display: flex;
flex-direction: column;
}
.flex.sidebar.open {
box-shadow: 1px 2px 2px 1px rgba(0,0,0,.10);
transform: translate3d(0, 0, 0);
.sidebar-toggle i {
transform: rotate(180deg);
}
}
.flex.sidebar .sidebar-toggle {
display: block;
position: absolute;
opacity: 0.9;
right: 0;
top: 0;
bottom: 0;
width: 30px;
color: #666;
font-size: 20px;
vertical-align: middle;
text-align: center;
border: 1px solid #DDD;
border-top: 1px solid #BBB;
padding-top: $-m;
cursor: pointer;
i {
opacity: 0.5;
transition: all ease-in-out 120ms;
padding: 0;
}
&:hover i {
opacity: 1;
}
}
.sidebar .scroll-body {
flex: 1;
overflow-y: scroll;
}
#sidebar .scroll-body.fixed {
width: auto !important;
}
}
@include larger-than($xl) {
#sidebar .scroll-body.fixed {
z-index: 5;
position: fixed;
top: 0;
padding-right: $-m;
width: 30%;
left: 0;
height: 100%;
overflow-y: scroll;
-ms-overflow-style: none;
//background-color: $primary-faded;
border-left: 1px solid #DDD;
&::-webkit-scrollbar { width: 0 !important }
}
}
/** Rules for all columns */
div[class^="col-"] img {
@ -54,6 +163,10 @@ div[class^="col-"] img {
&.small {
max-width: 840px;
}
&.nopad {
padding-left: 0;
padding-right: 0;
}
}
.row {

View File

@ -12,7 +12,6 @@ header {
padding: $-m;
}
border-bottom: 1px solid #DDD;
//margin-bottom: $-l;
.links {
display: inline-block;
vertical-align: top;
@ -23,26 +22,27 @@ header {
}
.links a {
display: inline-block;
padding: $-l;
padding: $-m $-l;
color: #FFF;
&:last-child {
padding-right: 0;
}
@include smaller-than($screen-md) {
padding: $-l $-s;
padding: $-m $-s;
}
}
.avatar, .user-name {
display: inline-block;
}
.avatar {
//margin-top: (45px/2);
width: 30px;
height: 30px;
}
.user-name {
vertical-align: top;
padding-top: $-l;
padding-top: $-m;
position: relative;
top: -3px;
display: inline-block;
cursor: pointer;
> * {
@ -66,53 +66,57 @@ header {
}
}
}
@include smaller-than($screen-md) {
@include smaller-than($screen-sm) {
text-align: center;
.float.right {
float: none;
}
}
@include smaller-than($screen-sm) {
.links a {
padding: $-s;
}
form.search-box {
margin-top: 0;
}
.user-name {
padding-top: $-s;
}
}
.dropdown-container {
font-size: 0.9em;
}
.header-search {
display: inline-block;
}
header .search-box {
display: inline-block;
margin-top: 10px;
input {
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #EEE;
}
button {
color: #EEE;
}
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
color: #DDD;
}
::-moz-placeholder { /* Firefox 19+ */
color: #DDD;
}
:-ms-input-placeholder { /* IE 10+ */
color: #DDD;
}
:-moz-placeholder { /* Firefox 18- */
color: #DDD;
}
@include smaller-than($screen-lg) {
max-width: 250px;
}
@include smaller-than($l) {
max-width: 200px;
}
}
form.search-box {
margin-top: $-l *0.9;
display: inline-block;
position: relative;
text-align: left;
input {
background-color: transparent;
border-radius: 24px;
border: 2px solid #EEE;
color: #EEE;
padding-left: $-m;
padding-right: $-l;
outline: 0;
}
button {
vertical-align: top;
margin-left: -$-l;
color: #FFF;
top: 6px;
right: 4px;
display: inline-block;
position: absolute;
&:hover {
color: #FFF;
}
@include smaller-than($s) {
.header-search {
display: block;
}
}
@ -128,12 +132,12 @@ form.search-box {
font-size: 1.8em;
color: #fff;
font-weight: 400;
padding: $-l $-l $-l 0;
padding: 14px $-l 14px 0;
vertical-align: top;
line-height: 1;
}
.logo-image {
margin: $-m $-s $-m 0;
margin: $-xs $-s $-xs 0;
vertical-align: top;
height: 43px;
}
@ -167,6 +171,10 @@ form.search-box {
background-color: $primary-faded;
}
.toolbar-container {
background-color: #FFF;
}
.breadcrumbs .text-button, .action-buttons .text-button {
display: inline-block;
padding: $-s;
@ -227,4 +235,7 @@ form.search-box {
border-bottom: 2px solid $primary;
}
}
}
.faded-small .nav-tabs a {
padding: $-s $-m;
}

View File

@ -9,14 +9,19 @@ html {
&.flexbox {
overflow-y: hidden;
}
&.shaded {
background-color: #F2F2F2;
}
}
body {
font-family: $text;
font-size: $fs-m;
line-height: 1.6;
color: #616161;
-webkit-font-smoothing: antialiased;
&.shaded {
background-color: #F2F2F2;
}
}
button {

View File

@ -9,7 +9,6 @@
.inset-list {
display: none;
overflow: hidden;
margin-bottom: $-l;
}
h5 {
display: block;
@ -22,6 +21,9 @@
border-left-color: $color-page-draft;
}
}
.entity-list-item {
margin-bottom: $-m;
}
hr {
margin-top: 0;
}
@ -51,23 +53,6 @@
margin-right: $-s;
}
}
.chapter-toggle {
cursor: pointer;
margin: 0 0 $-l 0;
transition: all ease-in-out 180ms;
user-select: none;
i.zmdi-caret-right {
transition: all ease-in-out 180ms;
transform: rotate(0deg);
transform-origin: 25% 50%;
}
&.open {
margin-bottom: 0;
}
&.open i.zmdi-caret-right {
transform: rotate(90deg);
}
}
.sidebar-page-nav {
$nav-indent: $-s;
@ -101,31 +86,8 @@
// Sidebar list
.book-tree {
padding: $-xs 0 0 0;
position: relative;
right: 0;
top: 0;
transition: ease-in-out 240ms;
transition-property: right, border;
border-left: 0px solid #FFF;
background-color: #FFF;
max-width: 320px;
&.fixed {
background-color: #FFF;
z-index: 5;
position: fixed;
top: 0;
padding-left: $-l;
padding-right: $-l + 15;
width: 30%;
right: -15px;
height: 100%;
overflow-y: scroll;
-ms-overflow-style: none;
//background-color: $primary-faded;
border-left: 1px solid #DDD;
&::-webkit-scrollbar { width: 0 !important }
}
}
.book-tree h4 {
padding: $-m $-s 0 $-s;
@ -171,7 +133,7 @@
background-color: rgba($color-chapter, 0.12);
}
}
.chapter-toggle {
[chapter-toggle] {
padding-left: $-s;
}
.list-item-chapter {
@ -260,6 +222,9 @@
.left + .right {
margin-left: 30px + $-s;
}
&:last-of-type {
border-bottom: 0;
}
}
ul.pagination {
@ -312,9 +277,6 @@ ul.pagination {
h4 {
margin: 0;
}
p {
margin: $-xs 0 0 0;
}
hr {
margin: 0;
}
@ -331,15 +293,24 @@ ul.pagination {
}
}
.card .entity-list-item, .card .activity-list-item {
padding-left: $-m;
padding-right: $-m;
}
.entity-list.compact {
font-size: 0.6em;
h4, a {
line-height: 1.2;
}
p {
.entity-item-snippet {
display: none;
}
.entity-list-item p {
font-size: $fs-m * 0.8;
padding-top: $-xs;
}
p {
margin: 0;
}
> p.empty-text {
@ -381,6 +352,7 @@ ul.pagination {
}
li.padded {
padding: $-xs $-m;
line-height: 1.2;
}
a {
display: block;

View File

@ -1,12 +1,3 @@
#page-show {
>.row .col-md-9 {
z-index: 2;
}
>.row .col-md-3 {
z-index: 1;
}
}
.page-editor {
display: flex;
flex-direction: column;
@ -36,6 +27,8 @@
.page-content {
max-width: 840px;
margin: 0 auto;
margin-top: $-xxl;
overflow-wrap: break-word;
.align-left {
text-align: left;
@ -226,7 +219,7 @@
width: 100%;
min-width: 50px;
}
.tags td {
.tags td, .tag-table > div > div > div {
padding-right: $-s;
padding-top: $-s;
position: relative;
@ -252,8 +245,6 @@
}
.tag-display {
width: 100%;
//opacity: 0.7;
position: relative;
table {
width: 100%;
@ -310,4 +301,8 @@
background-color: #EEE;
}
}
}
.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
min-height: 175px;
}

View File

@ -57,14 +57,4 @@ table.list-table {
vertical-align: middle;
padding: $-xs;
}
}
table.file-table {
@extend .no-style;
td {
padding: $-xs;
}
.ui-sortable-helper {
display: table;
}
}

View File

@ -1,3 +1,14 @@
/**
* Fonts
*/
body, button, input, select, label, textarea {
font-family: $text;
}
.Codemirror, pre, #markdown-editor-input, .editor-toolbar, .code-base {
font-family: $mono;
}
/*
* Header Styles
*/
@ -58,7 +69,6 @@ a, .link {
cursor: pointer;
text-decoration: none;
transition: color ease-in-out 80ms;
font-family: $text;
line-height: 1.6;
&:hover {
text-decoration: underline;
@ -131,7 +141,6 @@ sub, .subscript {
}
pre {
font-family: monospace;
font-size: 12px;
background-color: #f5f5f5;
border: 1px solid #DDD;
@ -180,7 +189,6 @@ blockquote {
.code-base {
background-color: #F8F8F8;
font-family: monospace;
font-size: 0.80em;
border: 1px solid #DDD;
border-radius: 3px;
@ -370,12 +378,6 @@ span.sep {
display: block;
}
.action-header {
h1 {
margin-top: $-m;
}
}
/**
* Icons
*/

View File

@ -48,4 +48,7 @@
}
}
}
}
.page-content.mce-content-body p {
line-height: 1.6;
}

View File

@ -27,8 +27,12 @@ $-xs: 6px;
$-xxs: 3px;
// Fonts
$heading: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
$text: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
$text: -apple-system, BlinkMacSystemFont,
"Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell",
"Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
$mono: "Lucida Console", "DejaVu Sans Mono", "Ubunto Mono", Monaco, monospace;
$heading: $text;
$fs-m: 15px;
$fs-s: 14px;
@ -55,4 +59,4 @@ $text-light: #EEE;
// Shadows
$bs-light: 0 0 4px 1px #CCC;
$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);

View File

@ -1,4 +1,3 @@
//@import "reset";
@import "variables";
@import "mixins";
@import "html";

View File

@ -1,6 +1,5 @@
@import "reset";
@import "variables";
@import "fonts";
@import "mixins";
@import "html";
@import "text";
@ -17,12 +16,11 @@
@import "lists";
@import "pages";
[v-cloak], [v-show] {
[v-cloak] {
display: none; opacity: 0;
animation-name: none !important;
}
[ng\:cloak], [ng-cloak], .ng-cloak {
display: none !important;
user-select: none;
@ -65,50 +63,11 @@ body.dragging, body.dragging * {
}
}
// System wide notifications
.notification {
position: fixed;
top: 0;
right: 0;
margin: $-xl*2 $-xl;
padding: $-l $-xl;
background-color: #EEE;
border-radius: 3px;
box-shadow: $bs-med;
z-index: 999999;
display: block;
cursor: pointer;
max-width: 480px;
i, span {
display: table-cell;
}
i {
font-size: 2em;
padding-right: $-l;
}
span {
vertical-align: middle;
}
&.pos {
background-color: $positive;
color: #EEE;
}
&.neg {
background-color: $negative;
color: #EEE;
}
&.warning {
background-color: $secondary;
color: #EEE;
}
}
// Loading icon
$loadingSize: 10px;
.loading-container {
position: relative;
display: block;
height: $loadingSize;
margin: $-xl auto;
> div {
width: $loadingSize;
@ -116,7 +75,8 @@ $loadingSize: 10px;
border-radius: $loadingSize;
display: inline-block;
vertical-align: top;
transform: translate3d(0, 0, 0);
transform: translate3d(-10px, 0, 0);
margin-top: $-xs;
animation-name: loadingBob;
animation-duration: 1.4s;
animation-iteration-count: infinite;
@ -130,11 +90,17 @@ $loadingSize: 10px;
background-color: $color-book;
animation-delay: 0s;
}
> div:last-child {
> div:last-of-type {
left: $loadingSize+$-xs;
background-color: $color-chapter;
animation-delay: 0.6s;
}
> span {
margin-left: $-s;
font-style: italic;
color: #888;
vertical-align: top;
}
}
@ -150,7 +116,7 @@ $loadingSize: 10px;
// Back to top link
$btt-size: 40px;
#back-to-top {
[back-to-top] {
background-color: $primary;
position: fixed;
bottom: $-m;
@ -256,22 +222,15 @@ $btt-size: 40px;
}
.center-box {
margin: $-xl auto 0 auto;
padding: $-m $-xxl $-xl $-xxl;
margin: $-xxl auto 0 auto;
width: 420px;
max-width: 100%;
display: inline-block;
text-align: left;
vertical-align: top;
//border: 1px solid #DDD;
input {
width: 100%;
}
&.login {
background-color: #EEE;
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
border: 1px solid #DDD;
}
}

View File

@ -8,33 +8,33 @@ return [
*/
// Pages
'page_create' => 'Seite erstellt',
'page_create_notification' => 'Seite erfolgreich erstellt',
'page_update' => 'Seite aktualisiert',
'page_update_notification' => 'Seite erfolgreich aktualisiert',
'page_delete' => 'Seite gel&ouml;scht',
'page_delete_notification' => 'Seite erfolgreich gel&ouml;scht',
'page_restore' => 'Seite wiederhergstellt',
'page_restore_notification' => 'Seite erfolgreich wiederhergstellt',
'page_move' => 'Seite verschoben',
'page_create' => 'hat Seite erstellt:',
'page_create_notification' => 'hat Seite erfolgreich erstellt:',
'page_update' => 'hat Seite aktualisiert:',
'page_update_notification' => 'hat Seite erfolgreich aktualisiert:',
'page_delete' => 'hat Seite gelöscht:',
'page_delete_notification' => 'hat Seite erfolgreich gelöscht:',
'page_restore' => 'hat Seite wiederhergstellt:',
'page_restore_notification' => 'hat Seite erfolgreich wiederhergstellt:',
'page_move' => 'hat Seite verschoben:',
// Chapters
'chapter_create' => 'Kapitel erstellt',
'chapter_create_notification' => 'Kapitel erfolgreich erstellt',
'chapter_update' => 'Kapitel aktualisiert',
'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert',
'chapter_delete' => 'Kapitel gel&ouml;scht',
'chapter_delete_notification' => 'Kapitel erfolgreich gel&ouml;scht',
'chapter_move' => 'Kapitel verschoben',
'chapter_create' => 'hat Kapitel erstellt:',
'chapter_create_notification' => 'hat Kapitel erfolgreich erstellt:',
'chapter_update' => 'hat Kapitel aktualisiert:',
'chapter_update_notification' => 'hat Kapitel erfolgreich aktualisiert:',
'chapter_delete' => 'hat Kapitel gelöscht',
'chapter_delete_notification' => 'hat Kapitel erfolgreich gelöscht:',
'chapter_move' => 'hat Kapitel verschoben:',
// Books
'book_create' => 'Buch erstellt',
'book_create_notification' => 'Buch erfolgreich erstellt',
'book_update' => 'Buch aktualisiert',
'book_update_notification' => 'Buch erfolgreich aktualisiert',
'book_delete' => 'Buch gel&ouml;scht',
'book_delete_notification' => 'Buch erfolgreich gel&ouml;scht',
'book_sort' => 'Buch sortiert',
'book_sort_notification' => 'Buch erfolgreich neu sortiert',
'book_create' => 'hat Buch erstellt:',
'book_create_notification' => 'hat Buch erfolgreich erstellt:',
'book_update' => 'hat Buch aktualisiert:',
'book_update_notification' => 'hat Buch erfolgreich aktualisiert:',
'book_delete' => 'hat Buch gelöscht:',
'book_delete_notification' => 'hat Buch erfolgreich gelöscht:',
'book_sort' => 'hat Buch sortiert:',
'book_sort_notification' => 'hat Buch erfolgreich neu sortiert:',
];

View File

@ -10,8 +10,8 @@ return [
| these language lines according to your application's requirements.
|
*/
'failed' => 'Dies sind keine g&uuml;ltigen Anmeldedaten.',
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen sie es in :seconds Sekunden erneut.',
'failed' => 'Die eingegebenen Anmeldedaten sind ungültig.',
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.',
/**
* Login & Register
@ -29,16 +29,16 @@ return [
'forgot_password' => 'Passwort vergessen?',
'remember_me' => 'Angemeldet bleiben',
'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.',
'create_account' => 'Account anlegen',
'social_login' => 'Social Login',
'social_registration' => 'Social Registrierung',
'social_registration_text' => 'Mit einem dieser Möglichkeiten registrieren oder anmelden.',
'create_account' => 'Account registrieren',
'social_login' => 'Mit Sozialem Netzwerk anmelden',
'social_registration' => 'Mit Sozialem Netzwerk registrieren',
'social_registration_text' => 'Mit einer dieser Dienste registrieren oder anmelden',
'register_thanks' => 'Vielen Dank für Ihre Registrierung!',
'register_confirm' => 'Bitte prüfen Sie Ihren E-Mail Eingang und klicken auf den Verifizieren-Button, um :appName nutzen zu können.',
'registrations_disabled' => 'Die Registrierung ist momentan nicht möglich',
'registration_email_domain_invalid' => 'Diese E-Mail-Domain ist für die Benutzer der Applikation nicht freigeschaltet.',
'register_confirm' => 'Bitte prüfen Sie Ihren Posteingang und bestätigen Sie die Registrierung.',
'registrations_disabled' => 'Eine Registrierung ist momentan nicht möglich',
'registration_email_domain_invalid' => 'Sie können sich mit dieser E-Mail nicht registrieren.',
'register_success' => 'Vielen Dank für Ihre Registrierung! Die Daten sind gespeichert und Sie sind angemeldet.',
@ -46,30 +46,30 @@ return [
* Password Reset
*/
'reset_password' => 'Passwort vergessen',
'reset_password_send_instructions' => 'Bitte geben Sie unten Ihre E-Mail-Adresse ein und Sie erhalten eine E-Mail, um Ihr Passwort zurück zu setzen.',
'reset_password_send_instructions' => 'Bitte geben Sie Ihre E-Mail-Adresse ein. Danach erhalten Sie eine E-Mail mit einem Link zum Zurücksetzen Ihres Passwortes.',
'reset_password_send_button' => 'Passwort zurücksetzen',
'reset_password_sent_success' => 'Eine E-Mail mit den Instruktionen, um Ihr Passwort zurückzusetzen wurde an :email gesendet.',
'reset_password_success' => 'Ihr Passwort wurde erfolgreich zurück gesetzt.',
'reset_password_sent_success' => 'Eine E-Mail mit dem Link zum Zurücksetzen Ihres Passwortes wurde an :email gesendet.',
'reset_password_success' => 'Ihr Passwort wurde erfolgreich zurückgesetzt.',
'email_reset_subject' => 'Passwort zurücksetzen für :appName',
'email_reset_text' => 'Sie erhalten diese E-Mail, weil eine Passwort-Rücksetzung für Ihren Account beantragt wurde.',
'email_reset_not_requested' => 'Wenn Sie die Passwort-Rücksetzung nicht ausgelöst haben, ist kein weiteres Handeln notwendig.',
'email_reset_text' => 'Sie erhalten diese E-Mail, weil jemand versucht hat, Ihr Passwort zurückzusetzen.',
'email_reset_not_requested' => 'Wenn Sie das nicht waren, brauchen Sie nichts weiter zu tun.',
/**
* Email Confirmation
*/
'email_confirm_subject' => 'Best&auml;tigen sie ihre E-Mail Adresse bei :appName',
'email_confirm_greeting' => 'Danke, dass sie :appName beigetreten sind!',
'email_confirm_text' => 'Bitte best&auml;tigen sie ihre E-Mail Adresse, indem sie auf den Button klicken:',
'email_confirm_action' => 'E-Mail Adresse best&auml;tigen',
'email_confirm_send_error' => 'Best&auml;tigungs-E-Mail ben&ouml;tigt, aber das System konnte die E-Mail nicht versenden. Kontaktieren sie den Administrator, um sicherzustellen, dass das Sytsem korrekt eingerichtet ist.',
'email_confirm_success' => 'Ihre E-Mail Adresse wurde best&auml;tigt!',
'email_confirm_resent' => 'Best&auml;tigungs-E-Mail wurde erneut versendet, bitte &uuml;berpr&uuml;fen sie ihren Posteingang.',
'email_confirm_subject' => 'Bestätigen Sie Ihre E-Mail-Adresse für :appName',
'email_confirm_greeting' => 'Danke, dass Sie sich für :appName registriert haben!',
'email_confirm_text' => 'Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf die Schaltfläche klicken:',
'email_confirm_action' => 'E-Mail-Adresse bestätigen',
'email_confirm_send_error' => 'Leider konnte die für die Registrierung notwendige E-Mail zur bestätigung Ihrer E-Mail-Adresse nicht versandt werden. Bitte kontaktieren Sie den Systemadministrator!',
'email_confirm_success' => 'Ihre E-Mail-Adresse wurde best&auml;tigt!',
'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfen Sie Ihren Posteingang.',
'email_not_confirmed' => 'E-Mail-Adresse ist nicht bestätigt',
'email_not_confirmed_text' => 'Ihre E-Mail-Adresse ist bisher nicht bestätigt.',
'email_not_confirmed_click_link' => 'Bitte klicken Sie auf den Link in der E-Mail, die Sie nach der Registrierung erhalten haben.',
'email_not_confirmed_resend' => 'Wenn Sie die E-Mail nicht erhalten haben, können Sie die Nachricht erneut anfordern. Füllen Sie hierzu bitte das folgende Formular aus:',
'email_not_confirmed_resend_button' => 'Bestätigungs E-Mail erneut senden',
'email_not_confirmed_resend_button' => 'Bestätigungs-E-Mail erneut senden',
];

View File

@ -28,9 +28,9 @@ return [
'edit' => 'Bearbeiten',
'sort' => 'Sortieren',
'move' => 'Verschieben',
'delete' => 'L&ouml;schen',
'delete' => 'Löschen',
'search' => 'Suchen',
'search_clear' => 'Suche l&ouml;schen',
'search_clear' => 'Suche löschen',
'reset' => 'Zurücksetzen',
'remove' => 'Entfernen',
@ -38,9 +38,9 @@ return [
/**
* Misc
*/
'deleted_user' => 'Gel&ouml;schte Benutzer',
'no_activity' => 'Keine Aktivit&auml;ten zum Anzeigen',
'no_items' => 'Keine Eintr&auml;ge gefunden.',
'deleted_user' => 'Gelöschte Benutzer',
'no_activity' => 'Keine Aktivitäten zum Anzeigen',
'no_items' => 'Keine Einträge gefunden.',
'back_to_top' => 'nach oben',
'toggle_details' => 'Details zeigen/verstecken',
@ -53,6 +53,6 @@ return [
/**
* Email Content
*/
'email_action_help' => 'Sollte es beim Anklicken des ":actionText" Buttons Probleme geben, kopieren Sie folgende URL und fügen diese in Ihrem Webbrowser ein:',
'email_action_help' => 'Sollte es beim Anklicken der Schaltfläche ":action_text" Probleme geben, öffnen Sie folgende URL in Ihrem Browser:',
'email_rights' => 'Alle Rechte vorbehalten',
];
];

View File

@ -13,12 +13,12 @@ return [
'image_uploaded' => 'Hochgeladen am :uploadedDate',
'image_load_more' => 'Mehr',
'image_image_name' => 'Bildname',
'image_delete_confirm' => 'Dieses Bild wird auf den folgenden Seiten benutzt. Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild tatsächlich entfernen möchten.',
'image_delete_confirm' => 'Dieses Bild wird auf den folgenden Seiten benutzt. Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild wirklich entfernen möchten.',
'image_select_image' => 'Bild auswählen',
'image_dropzone' => 'Ziehen Sie Bilder hier hinein oder klicken Sie hier, um ein Bild auszuwählen',
'image_dropzone' => 'Ziehen Sie Bilder hierher oder klicken Sie, um ein Bild auszuwählen',
'images_deleted' => 'Bilder gelöscht',
'image_preview' => 'Bildvorschau',
'image_upload_success' => 'Bild erfolgreich hochgeladen',
'image_update_success' => 'Bilddetails erfolgreich aktualisiert',
'image_delete_success' => 'Bild erfolgreich gelöscht'
];
];

View File

@ -4,38 +4,38 @@ return [
/**
* Shared
*/
'recently_created' => 'K&uuml;rzlich angelegt',
'recently_created_pages' => 'K&uuml;rzlich angelegte Seiten',
'recently_updated_pages' => 'K&uuml;rzlich aktualisierte Seiten',
'recently_created_chapters' => 'K&uuml;rzlich angelegte Kapitel',
'recently_created_books' => 'K&uuml;rzlich angelegte B&uuml;cher',
'recently_update' => 'K&uuml;rzlich aktualisiert',
'recently_viewed' => 'K&uuml;rzlich angesehen',
'recent_activity' => 'K&uuml;rzliche Aktivit&auml;t',
'recently_created' => 'Kürzlich angelegt',
'recently_created_pages' => 'Kürzlich angelegte Seiten',
'recently_updated_pages' => 'Kürzlich aktualisierte Seiten',
'recently_created_chapters' => 'Kürzlich angelegte Kapitel',
'recently_created_books' => 'Kürzlich angelegte Bücher',
'recently_update' => 'Kürzlich aktualisiert',
'recently_viewed' => 'Kürzlich angesehen',
'recent_activity' => 'Kürzliche Aktivität',
'create_now' => 'Jetzt anlegen',
'revisions' => 'Revisionen',
'meta_created' => 'Angelegt am :timeLength',
'meta_created_name' => 'Angelegt am :timeLength durch :user',
'meta_updated' => 'Aktualisiert am :timeLength',
'meta_updated_name' => 'Aktualisiert am :timeLength durch :user',
'x_pages' => ':count Seiten',
'entity_select' => 'Eintrag ausw&auml;hlen',
'revisions' => 'Versionen',
'meta_revision' => 'Version #:revisionCount',
'meta_created' => 'Erstellt: :timeLength',
'meta_created_name' => 'Erstellt: :timeLength von :user',
'meta_updated' => 'Zuletzt aktualisiert: :timeLength',
'meta_updated_name' => 'Zuletzt aktualisiert: :timeLength von :user',
'entity_select' => 'Eintrag auswählen',
'images' => 'Bilder',
'my_recent_drafts' => 'Meine k&uuml;rzlichen Entw&uuml;rfe',
'my_recently_viewed' => 'K&uuml;rzlich von mir angesehen',
'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
'my_recently_viewed' => 'Kürzlich von mir angesehen',
'no_pages_viewed' => 'Sie haben bisher keine Seiten angesehen.',
'no_pages_recently_created' => 'Sie haben bisher keine Seiten angelegt.',
'no_pages_recently_updated' => 'Sie haben bisher keine Seiten aktualisiert.',
'export' => 'Exportieren',
'export_html' => 'HTML-Datei',
'export_pdf' => 'PDF-Datei',
'export_text' => 'Text-Datei',
'export_text' => 'Textdatei',
/**
* Permissions and restrictions
*/
'permissions' => 'Berechtigungen',
'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, &uuml;berschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.',
'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, überschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.',
'permissions_enable' => 'Individuelle Berechtigungen aktivieren',
'permissions_save' => 'Berechtigungen speichern',
@ -43,41 +43,61 @@ return [
* Search
*/
'search_results' => 'Suchergebnisse',
'search_clear' => 'Suche zur&uuml;cksetzen',
'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden',
'search_for_term' => 'Suche nach :term',
'search_total_results_found' => ':count Ergebnis gefunden|:count Ergebnisse gesamt',
'search_clear' => 'Filter löschen',
'search_no_pages' => 'Keine Seiten gefunden',
'search_for_term' => 'Nach :term suchen',
'search_more' => 'Mehr Ergebnisse',
'search_filters' => 'Filter',
'search_content_type' => 'Inhaltstyp',
'search_exact_matches' => 'Exakte Treffer',
'search_tags' => 'Nach Schlagwort suchen',
'search_viewed_by_me' => 'Schon von mir angesehen',
'search_not_viewed_by_me' => 'Noch nicht von mir angesehen',
'search_permissions_set' => 'Berechtigungen gesetzt',
'search_created_by_me' => 'Von mir erstellt',
'search_updated_by_me' => 'Von mir aktualisiert',
'search_updated_before' => 'Aktualisiert vor',
'search_updated_after' => 'Aktualisiert nach',
'search_created_before' => 'Erstellt vor',
'search_created_after' => 'Erstellt nach',
'search_set_date' => 'Datum auswählen',
'search_update' => 'Suche aktualisieren',
/**
* Books
*/
'book' => 'Buch',
'books' => 'B&uuml;cher',
'books_empty' => 'Es wurden keine B&uuml;cher angelegt',
'books_popular' => 'Popul&auml;re B&uuml;cher',
'books_recent' => 'K&uuml;rzlich genutzte B&uuml;cher',
'books_popular_empty' => 'Die popul&auml;rsten B&uuml;cher werden hier angezeigt.',
'books_create' => 'Neues Buch anlegen',
'books_delete' => 'Buch l&ouml;schen',
'books_delete_named' => 'Buch :bookName l&ouml;schen',
'books_delete_explain' => 'Sie m&ouml;chten das Buch \':bookName\' l&ouml;schen und alle Seiten und Kapitel entfernen.',
'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch l&ouml;schen m&ouml;chten?',
'books' => 'Bücher',
'x_books' => ':count Buch|:count Bücher',
'books_empty' => 'Keine Bücher vorhanden',
'books_popular' => 'Beliebte Bücher',
'books_recent' => 'Kürzlich angesehene Bücher',
'books_new' => 'Neue Bücher',
'books_popular_empty' => 'Die beliebtesten Bücher werden hier angezeigt.',
'books_new_empty' => 'Die neusten Bücher werden hier angezeigt.',
'books_create' => 'Neues Buch erstellen',
'books_delete' => 'Buch löschen',
'books_delete_named' => 'Buch ":bookName" löschen',
'books_delete_explain' => 'Das Buch ":bookName" wird gelöscht und alle zugehörigen Kapitel und Seiten entfernt.',
'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch löschen möchten?',
'books_edit' => 'Buch bearbeiten',
'books_edit_named' => 'Buch :bookName bearbeiten',
'books_form_book_name' => 'Buchname',
'books_edit_named' => 'Buch ":bookName" bearbeiten',
'books_form_book_name' => 'Name des Buches',
'books_save' => 'Buch speichern',
'books_permissions' => 'Buch-Berechtigungen',
'books_permissions_updated' => 'Buch-Berechtigungen aktualisiert',
'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel f&uuml;r dieses Buch angelegt.',
'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel zu diesem Buch hinzugefügt worden.',
'books_empty_create_page' => 'Neue Seite anlegen',
'books_empty_or' => 'oder',
'books_empty_sort_current_book' => 'Aktuelles Buch sortieren',
'books_empty_add_chapter' => 'Neues Kapitel hinzuf&uuml;gen',
'books_empty_add_chapter' => 'Neues Kapitel hinzufügen',
'books_permissions_active' => 'Buch-Berechtigungen aktiv',
'books_search_this' => 'Dieses Buch durchsuchen',
'books_navigation' => 'Buch-Navigation',
'books_navigation' => 'Buchnavigation',
'books_sort' => 'Buchinhalte sortieren',
'books_sort_named' => 'Buch :bookName sortieren',
'books_sort_show_other' => 'Andere B&uuml;cher zeigen',
'books_sort_named' => 'Buch ":bookName" sortieren',
'books_sort_show_other' => 'Andere Bücher anzeigen',
'books_sort_save' => 'Neue Reihenfolge speichern',
/**
@ -85,132 +105,156 @@ return [
*/
'chapter' => 'Kapitel',
'chapters' => 'Kapitel',
'chapters_popular' => 'Popul&auml;re Kapitel',
'x_chapters' => ':count Kapitel',
'chapters_popular' => 'Beliebte Kapitel',
'chapters_new' => 'Neues Kapitel',
'chapters_create' => 'Neues Kapitel anlegen',
'chapters_delete' => 'Kapitel entfernen',
'chapters_delete_named' => 'Kapitel :chapterName entfernen',
'chapters_delete_explain' => 'Sie m&ouml;chten das Kapitel \':chapterName\' l&ouml;schen und alle Seiten dem direkten Eltern-Buch hinzugef&uuml;gen.',
'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel l&ouml;schen m&ouml;chten?',
'chapters_delete_named' => 'Kapitel ":chapterName" entfernen',
'chapters_delete_explain' => 'Das Kapitel ":chapterName" wird gelöscht und alle zugehörigen Seiten dem übergeordneten Buch zugeordnet.',
'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel löschen möchten?',
'chapters_edit' => 'Kapitel bearbeiten',
'chapters_edit_named' => 'Kapitel :chapterName bearbeiten',
'chapters_edit_named' => 'Kapitel ":chapterName" bearbeiten',
'chapters_save' => 'Kapitel speichern',
'chapters_move' => 'Kapitel verschieben',
'chapters_move_named' => 'Kapitel :chapterName verschieben',
'chapter_move_success' => 'Kapitel in das Buch :bookName verschoben.',
'chapters_move_named' => 'Kapitel ":chapterName" verschieben',
'chapter_move_success' => 'Das Kapitel wurde in das Buch ":bookName" verschoben.',
'chapters_permissions' => 'Kapitel-Berechtigungen',
'chapters_empty' => 'Aktuell sind keine Kapitel in diesem Buch angelegt.',
'chapters_empty' => 'Aktuell sind keine Kapitel diesem Buch hinzugefügt worden.',
'chapters_permissions_active' => 'Kapitel-Berechtigungen aktiv',
'chapters_permissions_success' => 'Kapitel-Berechtigungenen aktualisisert',
'chapters_search_this' => 'Dieses Kapitel durchsuchen',
/**
* Pages
*/
'page' => 'Seite',
'pages' => 'Seiten',
'pages_popular' => 'Popul&auml;re Seiten',
'x_pages' => ':count Seite|:count Seiten',
'pages_popular' => 'Beliebte Seiten',
'pages_new' => 'Neue Seite',
'pages_attachments' => 'Anh&auml;nge',
'pages_attachments' => 'Anhänge',
'pages_navigation' => 'Seitennavigation',
'pages_delete' => 'Seite l&ouml;schen',
'pages_delete_named' => 'Seite :pageName l&ouml;schen',
'pages_delete_draft_named' => 'Seitenentwurf von :pageName l&ouml;schen',
'pages_delete_draft' => 'Seitenentwurf l&ouml;schen',
'pages_delete_success' => 'Seite gel&ouml;scht',
'pages_delete_draft_success' => 'Seitenentwurf gel&ouml;scht',
'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite l&ouml;schen m&ouml;chen?',
'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf l&ouml;schen m&ouml;chten?',
'pages_editing_named' => 'Seite :pageName bearbeiten',
'pages_edit_toggle_header' => 'Toggle header',
'pages_delete' => 'Seite löschen',
'pages_delete_named' => 'Seite ":pageName" löschen',
'pages_delete_draft_named' => 'Seitenentwurf von ":pageName" löschen',
'pages_delete_draft' => 'Seitenentwurf löschen',
'pages_delete_success' => 'Seite gelöscht',
'pages_delete_draft_success' => 'Seitenentwurf gelöscht',
'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite löschen möchen?',
'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf löschen möchten?',
'pages_editing_named' => 'Seite ":pageName" bearbeiten',
'pages_edit_toggle_header' => 'Hauptmenü anzeigen/verstecken',
'pages_edit_save_draft' => 'Entwurf speichern',
'pages_edit_draft' => 'Seitenentwurf bearbeiten',
'pages_editing_draft' => 'Seitenentwurf bearbeiten',
'pages_editing_page' => 'Seite bearbeiten',
'pages_edit_draft_save_at' => 'Entwurf gespeichert um ',
'pages_edit_delete_draft' => 'Entwurf l&ouml;schen',
'pages_edit_delete_draft' => 'Entwurf löschen',
'pages_edit_discard_draft' => 'Entwurf verwerfen',
'pages_edit_set_changelog' => 'Ver&auml;nderungshinweis setzen',
'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer &Auml;nderungen ein',
'pages_edit_enter_changelog' => 'Ver&auml;nderungshinweis eingeben',
'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen',
'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer Änderungen ein',
'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben',
'pages_save' => 'Seite speichern',
'pages_title' => 'Seitentitel',
'pages_name' => 'Seitenname',
'pages_md_editor' => 'Redakteur',
'pages_md_preview' => 'Vorschau',
'pages_md_insert_image' => 'Bild einf&uuml;gen',
'pages_md_insert_link' => 'Link zu einem Objekt einf&uuml;gen',
'pages_md_insert_image' => 'Bild einfügen',
'pages_md_insert_link' => 'Link zu einem Objekt einfügen',
'pages_not_in_chapter' => 'Seite ist in keinem Kapitel',
'pages_move' => 'Seite verschieben',
'pages_move_success' => 'Seite nach ":parentName" verschoben',
'pages_permissions' => 'Seiten Berechtigungen',
'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert',
'pages_revision' => 'Version',
'pages_revisions' => 'Seitenversionen',
'pages_revisions_named' => 'Seitenversionen von :pageName',
'pages_revision_named' => 'Seitenversion von :pageName',
'pages_revisions_created_by' => 'Angelegt von',
'pages_revisions_named' => 'Seitenversionen von ":pageName"',
'pages_revision_named' => 'Seitenversion von ":pageName"',
'pages_revisions_created_by' => 'Erstellt von',
'pages_revisions_date' => 'Versionsdatum',
'pages_revisions_changelog' => 'Ver&auml;nderungshinweise',
'pages_revisions_changes' => 'Ver&auml;nderungen',
'pages_revisions_number' => '#',
'pages_revisions_changelog' => 'Änderungsprotokoll',
'pages_revisions_changes' => 'Änderungen',
'pages_revisions_current' => 'Aktuelle Version',
'pages_revisions_preview' => 'Vorschau',
'pages_revisions_restore' => 'Zur&uuml;ck sichern',
'pages_revisions_none' => 'Diese Seite hat keine &auml;lteren Versionen.',
'pages_revisions_restore' => 'Wiederherstellen',
'pages_revisions_none' => 'Diese Seite hat keine älteren Versionen.',
'pages_copy_link' => 'Link kopieren',
'pages_permissions_active' => 'Seiten-Berechtigungen aktiv',
'pages_initial_revision' => 'Erste Ver&ouml;ffentlichung',
'pages_initial_revision' => 'Erste Veröffentlichung',
'pages_initial_name' => 'Neue Seite',
'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt um :timeDiff gespeichert wurde.',
'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt ver&auml;ndert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt :timeDiff gespeichert wurde.',
'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
'pages_draft_edit_active' => [
'start_a' => ':count Benutzer haben die Bearbeitung dieser Seite begonnen.',
'start_b' => ':userName hat die Bearbeitung dieser Seite begonnen.',
'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.',
'start_b' => ':userName bearbeitet jetzt diese Seite.',
'time_a' => 'seit die Seiten zuletzt aktualisiert wurden.',
'time_b' => 'in den letzten :minCount Minuten',
'message' => ':start :time. Achten Sie darauf keine Aktualisierungen von anderen Benutzern zu &uuml;berschreiben!',
'message' => ':start :time. Achten Sie darauf, keine Änderungen von anderen Benutzern zu überschreiben!',
],
'pages_draft_discarded' => 'Entwurf verworfen. Der aktuelle Seiteninhalt wurde geladen.',
/**
* Editor sidebar
*/
'page_tags' => 'Seiten-Schlagw&ouml;rter',
'page_tags' => 'Seiten-Schlagwörter',
'tag' => 'Schlagwort',
'tags' => 'Schlagworte',
'tag_value' => 'Schlagwortinhalt (Optional)',
'tags_explain' => "F&uuml;gen Sie Schlagworte hinzu, um Ihren Inhalt zu kategorisieren. \n Sie k&ouml;nnen einen erkl&auml;renden Inhalt hinzuf&uuml;gen, um eine genauere Unterteilung vorzunehmen.",
'tags_add' => 'Weiteres Schlagwort hinzuf&uuml;gen',
'attachments' => 'Anh&auml;nge',
'attachments_explain' => 'Sie k&ouml;nnen auf Ihrer Seite Dateien hochladen oder Links anf&uuml;gen. Diese werden in der seitlich angezeigt.',
'attachments_explain_instant_save' => '&Auml;nderungen werden direkt gespeichert.',
'attachments_items' => 'Angef&uuml;gte Elemente',
'tags' => 'Schlagwörter',
'tag_value' => 'Inhalt (Optional)',
'tags_explain' => "Fügen Sie Schlagwörter hinzu, um Ihren Inhalt zu kategorisieren.\nSie können einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.",
'tags_add' => 'Weiteres Schlagwort hinzufügen',
'attachments' => 'Anhänge',
'attachments_explain' => 'Sie können auf Ihrer Seite Dateien hochladen oder Links hinzufügen. Diese werden in der Seitenleiste angezeigt.',
'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.',
'attachments_items' => 'Angefügte Elemente',
'attachments_upload' => 'Datei hochladen',
'attachments_link' => 'Link anf&uuml;gen',
'attachments_link' => 'Link hinzufügen',
'attachments_set_link' => 'Link setzen',
'attachments_delete_confirm' => 'Klicken Sie erneut auf l&ouml;schen, um diesen Anhang zu entfernen.',
'attachments_dropzone' => 'Ziehen Sie Dateien hier hinein oder klicken Sie hier, um eine Datei auszuw&auml;hlen',
'attachments_delete_confirm' => 'Klicken Sie erneut auf löschen, um diesen Anhang zu entfernen.',
'attachments_dropzone' => 'Ziehen Sie Dateien hierher oder klicken Sie, um eine Datei auszuwählen',
'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.',
'attachments_explain_link' => 'Wenn Sie keine Datei hochladen m&ouml;chten, k&ouml;nnen Sie stattdessen einen Link anf&uuml;gen. Dieser Link kann auf eine andere Seite oder zu einer Datei in der Cloud weisen.',
'attachments_explain_link' => 'Wenn Sie keine Datei hochladen möchten, können Sie stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet weisen.',
'attachments_link_name' => 'Link-Name',
'attachment_link' => 'Link zum Anhang',
'attachments_link_url' => 'Link zu einer Datei',
'attachments_link_url_hint' => 'URL einer Seite oder Datei',
'attach' => 'anf&uuml;gen',
'attach' => 'Hinzufügen',
'attachments_edit_file' => 'Datei bearbeiten',
'attachments_edit_file_name' => 'Dateiname',
'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hier hinein, um diese hochzuladen und zu &uuml;berschreiben',
'attachments_order_updated' => 'Reihenfolge der Anh&auml;nge aktualisiert',
'attachments_updated_success' => 'Anhang-Details aktualisiert',
'attachments_deleted' => 'Anhang gel&ouml;scht',
'attachments_file_uploaded' => 'Datei erfolgrecich hochgeladen',
'attachments_file_updated' => 'Datei erfolgreich aktualisisert',
'attachments_link_attached' => 'Link erfolgreich der Seite hinzugef&uuml;gt',
'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hierher, um diese hochzuladen und zu überschreiben',
'attachments_order_updated' => 'Reihenfolge der Anhänge aktualisiert',
'attachments_updated_success' => 'Anhangdetails aktualisiert',
'attachments_deleted' => 'Anhang gelöscht',
'attachments_file_uploaded' => 'Datei erfolgreich hochgeladen',
'attachments_file_updated' => 'Datei erfolgreich aktualisiert',
'attachments_link_attached' => 'Link erfolgreich der Seite hinzugefügt',
/**
* Profile View
*/
'profile_user_for_x' => 'Benutzer seit :time',
'profile_created_content' => 'Angelegte Inhalte',
'profile_not_created_pages' => ':userName hat bisher keine Seiten angelegt.',
'profile_not_created_chapters' => ':userName hat bisher keine Kapitel angelegt.',
'profile_not_created_books' => ':userName hat bisher keine B&uuml;cher angelegt.',
];
'profile_created_content' => 'Erstellte Inhalte',
'profile_not_created_pages' => ':userName hat noch keine Seiten erstellt.',
'profile_not_created_chapters' => ':userName hat noch keine Kapitel erstellt.',
'profile_not_created_books' => ':userName hat noch keine Bücher erstellt.',
/**
* Comments
*/
'comment' => 'Kommentar',
'comments' => 'Kommentare',
'comment_placeholder' => 'Geben Sie hier Ihre Kommentare ein (Markdown unterstützt)',
'comment_count' => '{0} Keine Kommentare|{1} 1 Kommentar|[2,*] :count Kommentare',
'comment_save' => 'Kommentar speichern',
'comment_saving' => 'Kommentar wird gespeichert...',
'comment_deleting' => 'Kommentar wird gelöscht...',
'comment_new' => 'Neuer Kommentar',
'comment_created' => ':createDiff kommentiert',
'comment_updated' => ':updateDiff aktualisiert von :username',
'comment_deleted_success' => 'Kommentar gelöscht',
'comment_created_success' => 'Kommentar hinzugefügt',
'comment_updated_success' => 'Kommentar aktualisiert',
'comment_delete_confirm' => 'Möchten Sie diesen Kommentar wirklich löschen?',
'comment_in_reply_to' => 'Antwort auf :commentId',
];

View File

@ -7,37 +7,37 @@ return [
*/
// Pages
'permission' => 'Sie haben keine Berechtigung auf diese Seite zuzugreifen.',
'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuf&uuml;hren.',
'permission' => 'Sie haben keine Berechtigung, auf diese Seite zuzugreifen.',
'permissionJson' => 'Sie haben keine Berechtigung, die angeforderte Aktion auszuführen.',
// Auth
'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten angelegt.',
'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits best&auml;tigt. Bitte melden Sie sich an.',
'email_confirmation_invalid' => 'Der Best&auml;tigungs-Token ist nicht g&uuml;ltig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.',
'email_confirmation_expired' => 'Der Best&auml;tigungs-Token ist abgelaufen. Es wurde eine neue Best&auml;tigungs-E-Mail gesendet.',
'ldap_fail_anonymous' => 'Anonymer LDAP Zugriff ist fehlgeschlafgen',
'ldap_fail_authed' => 'LDAP Zugriff mit DN & Passwort ist fehlgeschlagen',
'ldap_extension_not_installed' => 'LDAP PHP Erweiterung ist nicht installiert.',
'ldap_cannot_connect' => 'Die Verbindung zu LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.',
'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten registriert.',
'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melden Sie sich an.',
'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.',
'email_confirmation_expired' => 'Der Bestätigungslink ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.',
'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlafgen',
'ldap_fail_authed' => 'LDAP-Zugriff mit DN und Passwort ist fehlgeschlagen',
'ldap_extension_not_installed' => 'LDAP-PHP-Erweiterung ist nicht installiert.',
'ldap_cannot_connect' => 'Die Verbindung zum LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.',
'social_no_action_defined' => 'Es ist keine Aktion definiert',
'social_account_in_use' => 'Dieses :socialAccount Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount Konto an.',
'social_account_email_in_use' => 'Die E-Mail-Adresse :email ist bereits registriert. Wenn Sie bereits registriert sind, k&ouml;nnen Sie Ihr :socialAccount Konto in Ihren Profil-Einstellungen verkn&uuml;pfen.',
'social_account_existing' => 'Dieses :socialAccount Konto ist bereits mit Ihrem Profil verkn&uuml;pft.',
'social_account_already_used_existing' => 'Dieses :socialAccount Konto wird bereits durch einen anderen Benutzer verwendet.',
'social_account_not_used' => 'Dieses :socialAccount Konto ist bisher keinem Benutzer zugeordnet. Bitte verkn&uuml;pfen Sie deses in Ihrem Profil-Einstellungen.',
'social_account_register_instructions' => 'Wenn Sie bisher keinen Social-Media Konto besitzen k&ouml;nnen Sie ein solches Konto mit der :socialAccount Option anlegen.',
'social_driver_not_found' => 'Social-Media Konto Treiber nicht gefunden',
'social_driver_not_configured' => 'Ihr :socialAccount Konto ist nicht korrekt konfiguriert.',
'social_account_in_use' => 'Dieses :socialAccount-Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount-Konto an.',
'social_account_email_in_use' => 'Die E-Mail-Adresse ":email" ist bereits registriert. Wenn Sie bereits registriert sind, können Sie Ihr :socialAccount-Konto in Ihren Profil-Einstellungen verknüpfen.',
'social_account_existing' => 'Dieses :socialAccount-Konto ist bereits mit Ihrem Profil verknüpft.',
'social_account_already_used_existing' => 'Dieses :socialAccount-Konto wird bereits von einem anderen Benutzer verwendet.',
'social_account_not_used' => 'Dieses :socialAccount-Konto ist bisher keinem Benutzer zugeordnet. Sie können es in Ihren Profil-Einstellung.',
'social_account_register_instructions' => 'Wenn Sie bisher keinen Social-Media Konto besitzen, können Sie ein solches Konto mit der :socialAccount Option anlegen.',
'social_driver_not_found' => 'Treiber für Social-Media-Konten nicht gefunden',
'social_driver_not_configured' => 'Ihr :socialAccount-Konto ist nicht korrekt konfiguriert.',
// System
'path_not_writable' => 'Die Datei kann nicht in den angegebenen Pfad :filePath hochgeladen werden. Stellen Sie sicher, dass dieser Ordner auf dem Server beschreibbar ist.',
'cannot_get_image_from_url' => 'Bild konnte nicht von der URL :url geladen werden.',
'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte pr&uuml;fen Sie, ob Sie die GD PHP Erweiterung installiert haben.',
'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigr&ouml;&szlig;e. Bitte versuchen Sie es mit einer kleineren Datei.',
'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfen Sie, ob die GD PHP-Erweiterung installiert ist.',
'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.',
'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.',
// Attachments
'attachment_page_mismatch' => 'Die Seite stimmt nach dem Hochladen des Anhangs nicht &uuml;berein.',
'attachment_page_mismatch' => 'Die Seite stimmte nach dem Hochladen des Anhangs nicht überein.',
// Pages
'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stellen Sie sicher, dass Sie mit dem Internet verbunden sind, bevor Sie den Entwurf dieser Seite speichern.',
@ -47,24 +47,31 @@ return [
'book_not_found' => 'Buch nicht gefunden',
'page_not_found' => 'Seite nicht gefunden',
'chapter_not_found' => 'Kapitel nicht gefunden',
'selected_book_not_found' => 'Das gew&auml;hlte Buch wurde nicht gefunden.',
'selected_book_chapter_not_found' => 'Das gew&auml;hlte Buch oder Kapitel wurde nicht gefunden.',
'guests_cannot_save_drafts' => 'G&auml;ste k&ouml;nnen keine Entw&uuml;rfe speichern',
'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden.',
'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.',
'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern',
// Users
'users_cannot_delete_only_admin' => 'Sie k&ouml;nnen den einzigen Administrator nicht l&ouml;schen.',
'users_cannot_delete_guest' => 'Sie k&ouml;nnen den Gast-Benutzer nicht l&ouml;schen',
'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen.',
'users_cannot_delete_guest' => 'Sie können den Gast-Benutzer nicht löschen',
// Roles
'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden.',
'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gel&ouml;scht werden',
'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gel&ouml;scht werden solange sie als Standardrolle f&uuml;r neue Registrierungen gesetzt ist',
'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden',
'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden, solange sie als Standardrolle für neue Registrierungen gesetzt ist',
// Comments
'comment_list' => 'Beim Abrufen der Kommentare ist ein Fehler aufgetreten.',
'cannot_add_comment_to_draft' => 'Du kannst keine Kommentare zu einem Entwurf hinzufügen.',
'comment_add' => 'Beim Hinzufügen des Kommentars ist ein Fehler aufgetreten.',
'comment_delete' => 'Beim Löschen des Kommentars ist ein Fehler aufgetreten.',
'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen',
// Error pages
'404_page_not_found' => 'Seite nicht gefunden',
'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben wurde nicht gefunden.',
'return_home' => 'Zur&uuml;ck zur Startseite',
'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben, wurde nicht gefunden.',
'return_home' => 'Zurück zur Startseite',
'error_occurred' => 'Es ist ein Fehler aufgetreten',
'app_down' => ':appName befindet sich aktuell im Wartungsmodus.',
'back_soon' => 'Wir werden so schnell wie m&ouml;glich wieder online sein.',
'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.'
];

View File

@ -14,6 +14,6 @@ return [
*/
'previous' => '&laquo; Vorherige',
'next' => 'N&auml;chste &raquo;',
'next' => 'Nächste &raquo;',
];

Some files were not shown because too many files have changed in this diff Show More