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/css/*.map
/public/js/*.map /public/js/*.map
/public/bower /public/bower
/public/build/
/storage/images /storage/images
_ide_helper.php _ide_helper.php
/storage/debugbar /storage/debugbar
.phpstorm.meta.php .phpstorm.meta.php
yarn.lock yarn.lock
/bin /bin
nbproject
.buildpath .buildpath
.project .project
.settings/org.eclipse.wst.common.project.facet.core.xml .settings/org.eclipse.wst.common.project.facet.core.xml
.settings/org.eclipse.php.core.prefs .settings/org.eclipse.php.core.prefs

View File

@ -2,7 +2,7 @@ dist: trusty
sudo: false sudo: false
language: php language: php
php: php:
- 7.0 - 7.0.7
cache: cache:
directories: 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; <?php namespace BookStack;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Entity extends Ownable class Entity extends Ownable
{ {
@ -65,6 +67,17 @@ class Entity extends Ownable
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc'); 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. * Get the related search terms.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany * @return \Illuminate\Database\Eloquent\Relations\MorphMany

View File

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

View File

@ -36,11 +36,17 @@ class BookController extends Controller
*/ */
public function index() 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; $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0); $popular = $this->entityRepo->getPopular('book', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$this->setPageTitle('Books'); $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); $bookChildren = $this->entityRepo->getBookChildren($book);
Views::add($book); Views::add($book);
$this->setPageTitle($book->getShortName()); $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); $activity = Activity::latest(10);
$draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : []; $draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
$recentFactor = count($draftPages) > 0 ? 0.5 : 1; $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor); $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5); $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5);
return view('home', [ // 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, 'activity' => $activity,
'recents' => $recents, 'recents' => $recents,
'recentlyCreatedPages' => $recentlyCreatedPages,
'recentlyUpdatedPages' => $recentlyUpdatedPages, 'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages 'draftPages' => $draftPages,
'customHomepage' => $customHomepage
]); ]);
} }

View File

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

View File

@ -47,4 +47,16 @@ class PageRevision extends Model
return null; 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 string $type
* @param integer $id * @param integer $id
* @param bool $allowDrafts * @param bool $allowDrafts
* @param bool $ignorePermissions
* @return Entity * @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); 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. * Render the page for viewing, Parsing and performing features such as page transclusion.
* @param Page $page * @param Page $page
* @param bool $ignorePermissions
* @return mixed|string * @return mixed|string
*/ */
public function renderPage(Page $page) public function renderPage(Page $page, $ignorePermissions = false)
{ {
$content = $page->html; $content = $page->html;
$matches = []; $matches = [];
@ -685,19 +691,19 @@ class EntityRepo
$pageId = intval($splitInclude[0]); $pageId = intval($splitInclude[0]);
if (is_nan($pageId)) continue; if (is_nan($pageId)) continue;
$page = $this->getById('page', $pageId); $matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
if ($page === null) { if ($matchedPage === null) {
$content = str_replace($matches[0][$index], '', $content); $content = str_replace($matches[0][$index], '', $content);
continue; continue;
} }
if (count($splitInclude) === 1) { if (count($splitInclude) === 1) {
$content = str_replace($matches[0][$index], $page->html, $content); $content = str_replace($matches[0][$index], $matchedPage->html, $content);
continue; continue;
} }
$doc = new DOMDocument(); $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]); $matchingElem = $doc->getElementById($splitInclude[1]);
if ($matchingElem === null) { if ($matchingElem === null) {
$content = str_replace($matches[0][$index], '', $content); $content = str_replace($matches[0][$index], '', $content);
@ -710,6 +716,7 @@ class EntityRepo
$content = str_replace($matches[0][$index], trim($innerContent), $content); $content = str_replace($matches[0][$index], trim($innerContent), $content);
} }
$page->setAttribute('renderedHTML', $content);
return $content; return $content;
} }

View File

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

View File

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

View File

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

View File

@ -92,7 +92,7 @@ class SearchService
return [ return [
'total' => $total, 'total' => $total,
'count' => count($results), '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); $query->whereIn('viewable_type', $filterModel);
} else if ($filterModel) { } else if ($filterModel) {
$query->where('viewable_type', '=', get_class($filterModel)); $query->where('viewable_type', '=', get_class($filterModel));
}; }
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable'); 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'), '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

@ -71,3 +71,13 @@ $factory->define(BookStack\Image::class, function ($faker) {
'uploaded_to' => 0 '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,7 +15,6 @@ class DummyContentSeeder extends Seeder
$role = \BookStack\Role::getRole('editor'); $role = \BookStack\Role::getRole('editor');
$user->attachRole($role); $user->attachRole($role);
factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id]) factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($book) use ($user) { ->each(function($book) use ($user) {
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id]) $chapters = factory(\BookStack\Chapter::class, 5)->create(['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]); $chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
$largeBook->pages()->saveMany($pages); $largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters); $largeBook->chapters()->saveMany($chapters);
app(\BookStack\Services\PermissionService::class)->buildJointPermissions(); app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
app(\BookStack\Services\SearchService::class)->indexAllEntities(); app(\BookStack\Services\SearchService::class)->indexAllEntities();
} }

View File

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

View File

@ -31,15 +31,18 @@
"angular-sanitize": "^1.5.5", "angular-sanitize": "^1.5.5",
"angular-ui-sortable": "^0.17.0", "angular-ui-sortable": "^0.17.0",
"axios": "^0.16.1", "axios": "^0.16.1",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"clipboard": "^1.5.16", "clipboard": "^1.7.1",
"codemirror": "^5.26.0", "codemirror": "^5.26.0",
"dropzone": "^4.0.1", "dropzone": "^4.0.1",
"gulp-sourcemaps": "^2.6.1",
"gulp-util": "^3.0.8", "gulp-util": "^3.0.8",
"markdown-it": "^8.3.1", "markdown-it": "^8.3.1",
"markdown-it-task-lists": "^2.0.0", "markdown-it-task-lists": "^2.0.0",
"moment": "^2.12.0", "moment": "^2.12.0",
"vue": "^2.2.6" "vue": "^2.2.6",
"vuedraggable": "^2.14.1"
}, },
"browser": { "browser": {
"vue": "vue/dist/vue.common.js" "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 # BookStack
[![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg?maxAge=2592000)](https://github.com/BookStackApp/BookStack/releases/latest) [![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?maxAge=2592000)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE) [![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) [![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/. 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`* * *Password: `password`*
* [BookStack Blog](https://www.bookstackapp.com/blog) * [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 ## 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: 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/) * [jQuery Sortable](https://johnny.github.io/jquery-sortable/)
* [Material Design Iconic Font](http://zavoloklom.github.io/material-design-iconic-font/icons.html) * [Material Design Iconic Font](http://zavoloklom.github.io/material-design-iconic-font/icons.html)
* [Dropzone.js](http://www.dropzonejs.com/) * [Dropzone.js](http://www.dropzonejs.com/)
* [ZeroClipboard](http://zeroclipboard.org/) * [clipboard.js](https://clipboardjs.com/)
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) * [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) * [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/) * [Moment.js](http://momentjs.com/)

View File

@ -53,13 +53,20 @@ const modeMap = {
yml: 'yaml', 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++) { for (let i = 0; i < codeBlocks.length; i++) {
highlightElem(codeBlocks[i]); highlightElem(codeBlocks[i]);
} }
}; }
/**
* Add code highlighting to a single element.
* @param {HTMLElement} elem
*/
function highlightElem(elem) { function highlightElem(elem) {
let innerCodeElem = elem.querySelector('code[class^=language-]'); let innerCodeElem = elem.querySelector('code[class^=language-]');
let mode = ''; let mode = '';
@ -68,7 +75,7 @@ function highlightElem(elem) {
mode = getMode(langName); mode = getMode(langName);
} }
elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n'); elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
let content = elem.textContent; let content = elem.textContent.trim();
CodeMirror(function(elt) { CodeMirror(function(elt) {
elem.parentNode.replaceChild(elt, elem); elem.parentNode.replaceChild(elt, elem);
@ -76,7 +83,7 @@ function highlightElem(elem) {
value: content, value: content,
mode: mode, mode: mode,
lineNumbers: true, lineNumbers: true,
theme: 'base16-light', theme: getTheme(),
readOnly: true readOnly: true
}); });
} }
@ -91,9 +98,21 @@ function getMode(suggestion) {
return (typeof modeMap[suggestion] !== 'undefined') ? modeMap[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 doc = elem.ownerDocument;
let codeElem = elem.querySelector('code'); let codeElem = elem.querySelector('code');
@ -122,16 +141,22 @@ module.exports.wysiwygView = function(elem) {
value: content, value: content,
mode: getMode(lang), mode: getMode(lang),
lineNumbers: true, lineNumbers: true,
theme: 'base16-light', theme: getTheme(),
readOnly: true readOnly: true
}); });
setTimeout(() => { setTimeout(() => {
cm.refresh(); cm.refresh();
}, 300); }, 300);
return {wrap: newWrap, editor: cm}; 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; let content = elem.textContent;
return CodeMirror(function(elt) { return CodeMirror(function(elt) {
@ -141,22 +166,38 @@ module.exports.popupEditor = function(elem, modeSuggestion) {
value: content, value: content,
mode: getMode(modeSuggestion), mode: getMode(modeSuggestion),
lineNumbers: true, lineNumbers: true,
theme: 'base16-light', theme: getTheme(),
lineWrapping: true 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)); 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); cmInstance.setValue(codeContent);
setTimeout(() => { setTimeout(() => {
cmInstance.refresh(); cmInstance.refresh();
}, 10); }, 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; let content = elem.textContent;
return CodeMirror(function (elt) { return CodeMirror(function (elt) {
@ -166,13 +207,27 @@ module.exports.markdownEditor = function(elem) {
value: content, value: content,
mode: "markdown", mode: "markdown",
lineNumbers: true, lineNumbers: true,
theme: 'base16-light', theme: getTheme(),
lineWrapping: true 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; let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
return mac ? "Cmd" : "Ctrl"; 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) { 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', ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
function ($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"; "use strict";
const DropZone = require("dropzone");
const MarkdownIt = require("markdown-it"); const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists'); const mdTasksLists = require('markdown-it-task-lists');
const code = require('./code'); const code = require('./code');
module.exports = function (ngApp, events) { 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 * TinyMCE
* An angular wrapper around the tinyMCE editor. * An angular wrapper around the tinyMCE editor.
@ -168,7 +20,7 @@ module.exports = function (ngApp, events) {
link: function (scope, element, attrs) { link: function (scope, element, attrs) {
function tinyMceSetup(editor) { function tinyMceSetup(editor) {
editor.on('ExecCommand change NodeChange ObjectResized', (e) => { editor.on('ExecCommand change input NodeChange ObjectResized', (e) => {
let content = editor.getContent(); let content = editor.getContent();
$timeout(() => { $timeout(() => {
scope.mceModel = content; scope.mceModel = content;
@ -177,7 +29,10 @@ module.exports = function (ngApp, events) {
}); });
editor.on('keydown', (event) => { 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) => { editor.on('init', (e) => {
@ -247,7 +102,7 @@ module.exports = function (ngApp, events) {
extraKeys[`${metaKey}-7`] = function(cm) {wrapSelection('\n```\n', '\n```');}; extraKeys[`${metaKey}-7`] = function(cm) {wrapSelection('\n```\n', '\n```');};
extraKeys[`${metaKey}-8`] = function(cm) {wrapSelection('`', '`');}; extraKeys[`${metaKey}-8`] = function(cm) {wrapSelection('`', '`');};
extraKeys[`Shift-${metaKey}-E`] = 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); cm.setOption('extraKeys', extraKeys);
// Update data on content change // 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.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) { function wrapSelection(start, end) {
let selection = cm.getSelection(); let selection = cm.getSelection();
if (selection === '') return wrapLine(start, end); if (selection === '') return wrapLine(start, end);
let newSelection = selection; let newSelection = selection;
let frontDiff = 0; let frontDiff = 0;
let endDiff = 0; let endDiff = 0;
@ -400,7 +256,7 @@ module.exports = function (ngApp, events) {
// Show the popup link selector and insert a link when finished // Show the popup link selector and insert a link when finished
function showLinkSelector() { function showLinkSelector() {
let cursorPos = cm.getCursor('from'); let cursorPos = cm.getCursor('from');
window.showEntityLinkSelector(entity => { window.EntitySelectorPopup.show(entity => {
let selectedText = cm.getSelection() || entity.name; let selectedText = cm.getSelection() || entity.name;
let newText = `[${selectedText}](${entity.link})`; let newText = `[${selectedText}](${entity.link})`;
cm.focus(); cm.focus();
@ -422,7 +278,7 @@ module.exports = function (ngApp, events) {
// Show the image manager and handle image insertion // Show the image manager and handle image insertion
function showImageManager() { function showImageManager() {
let cursorPos = cm.getCursor('from'); let cursorPos = cm.getCursor('from');
window.ImageManager.showExternal(image => { window.ImageManager.show(image => {
let selectedText = cm.getSelection(); let selectedText = cm.getSelection();
let newText = "![" + (selectedText || image.name) + "](" + image.thumbs.display + ")"; let newText = "![" + (selectedText || image.name) + "](" + image.thumbs.display + ")";
cm.focus(); 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"; "use strict";
require("babel-polyfill");
require('./dom-polyfills');
// Url retrieval function // Url retrieval function
window.baseUrl = function(path) { window.baseUrl = function(path) {
@ -8,44 +10,15 @@ window.baseUrl = function(path) {
return basePath + '/' + 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 // Global Event System
class EventManager { class EventManager {
constructor() { constructor() {
this.listeners = {}; this.listeners = {};
this.stack = [];
} }
emit(eventName, eventData) { emit(eventName, eventData) {
this.stack.push({name: eventName, data: eventData});
if (typeof this.listeners[eventName] === 'undefined') return this; if (typeof this.listeners[eventName] === 'undefined') return this;
let eventsToStart = this.listeners[eventName]; let eventsToStart = this.listeners[eventName];
for (let i = 0; i < eventsToStart.length; i++) { for (let i = 0; i < eventsToStart.length; i++) {
@ -62,25 +35,95 @@ class EventManager {
} }
} }
window.Events = new EventManager(); window.$events = new EventManager();
Vue.prototype.$events = window.Events;
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 // Load in angular specific items
const Services = require('./services');
const Directives = require('./directives'); const Directives = require('./directives');
const Controllers = require('./controllers'); const Controllers = require('./controllers');
Services(ngApp, window.Events); Directives(ngApp, window.$events);
Directives(ngApp, window.Events); Controllers(ngApp, window.$events);
Controllers(ngApp, window.Events);
//Global jQuery Config & Extensions //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 // Smooth scrolling
jQuery.fn.smoothScrollTo = function () { jQuery.fn.smoothScrollTo = function () {
if (this.length === 0) return; if (this.length === 0) return;
$('html, body').animate({ window.scrollToElement(this[0]);
scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin
}, 300); // Adjust to change animations speed (ms)
return this; 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 // Detect IE for css
if(navigator.userAgent.indexOf('MSIE')!==-1 if(navigator.userAgent.indexOf('MSIE')!==-1
|| navigator.appVersion.indexOf('Trident/') > 0 || navigator.appVersion.indexOf('Trident/') > 0
|| navigator.userAgent.indexOf('Safari') !== -1){ || navigator.userAgent.indexOf('Safari') !== -1){
$('body').addClass('flexbox-support'); document.body.classList.add('flexbox-support');
} }
// Page specific items // Page specific items

View File

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

View File

@ -1,5 +1,3 @@
"use strict";
// Configure ZeroClipboard
const Clipboard = require("clipboard"); const Clipboard = require("clipboard");
const Code = require('../code'); const Code = require('../code');
@ -83,15 +81,7 @@ let setupPageShow = window.setupPageShow = function (pageId) {
let idElem = document.getElementById(text); let idElem = document.getElementById(text);
$('.page-content [data-highlighted]').attr('data-highlighted', '').css('background-color', ''); $('.page-content [data-highlighted]').attr('data-highlighted', '').css('background-color', '');
if (idElem !== null) { if (idElem !== null) {
let $idElem = $(idElem); window.scrollAndHighlight(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);
} else { } else {
$('.page-content').find(':contains("' + text + '")').smoothScrollTo(); $('.page-content').find(':contains("' + text + '")').smoothScrollTo();
} }
@ -108,25 +98,25 @@ let setupPageShow = window.setupPageShow = function (pageId) {
goToText(event.target.getAttribute('href').substr(1)); 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 $window = $(window);
let $bookTree = $(".book-tree"); let $sidebar = $("#sidebar .scroll-body");
let $bookTreeParent = $bookTree.parent(); let $bookTreeParent = $sidebar.parent();
// Check the page is scrollable and the content is taller than the tree // 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 // Get current tree's width and header height
let headerHeight = $("#header").height() + $(".toolbar").height(); let headerHeight = $("#header").height() + $(".toolbar").height();
let isFixed = $window.scrollTop() > headerHeight; let isFixed = $window.scrollTop() > headerHeight;
// Function to fix the tree as a sidebar // Function to fix the tree as a sidebar
function stickTree() { function stickTree() {
$bookTree.width($bookTreeParent.width() + 15); $sidebar.width($bookTreeParent.width() + 15);
$bookTree.addClass("fixed"); $sidebar.addClass("fixed");
isFixed = true; isFixed = true;
} }
// Function to un-fix the tree back into position // Function to un-fix the tree back into position
function unstickTree() { function unstickTree() {
$bookTree.css('width', 'auto'); $sidebar.css('width', 'auto');
$bookTree.removeClass("fixed"); $sidebar.removeClass("fixed");
isFixed = false; isFixed = false;
} }
// Checks if the tree stickiness state should change // Checks if the tree stickiness state should change
@ -160,7 +150,6 @@ let setupPageShow = window.setupPageShow = function (pageId) {
unstickTree(); unstickTree();
} }
}); });
}; };
module.exports = setupPageShow; 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 {*} * @returns {*}
*/ */
get(key, replacements) { 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 splitKey = key.split('.');
let value = splitKey.reduce((a, b) => { let value = splitKey.reduce((a, b) => {
return a != undefined ? a[b] : a; return a !== undefined ? a[b] : a;
}, this.store); }, this.store);
if (value === undefined) { if (value === undefined) {
@ -30,16 +84,25 @@ class Translator {
value = key; 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 => { replaceMatches.forEach(match => {
let key = match.substring(1); let key = match.substring(1);
if (typeof replacements[key] === 'undefined') return; 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 = { let vueMapping = {
'search-system': require('./search'), 'search-system': require('./search'),
'entity-dashboard': require('./entity-search'), 'entity-dashboard': require('./entity-dashboard'),
'code-editor': require('./code-editor') 'code-editor': require('./code-editor'),
'image-manager': require('./image-manager'),
'tag-manager': require('./tag-manager'),
'attachment-manager': require('./attachment-manager'),
}; };
window.vues = {}; window.vues = {};
Object.keys(vueMapping).forEach(id => { let ids = Object.keys(vueMapping);
if (exists(id)) { for (let i = 0, len = ids.length; i < len; i++) {
let config = vueMapping[id]; if (!exists(ids[i])) continue;
config.el = '#' + id; let config = vueMapping[ids[i]];
window.vues[id] = new Vue(config); config.el = '#' + ids[i];
} window.vues[ids[i]] = new Vue(config);
}); }

View File

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

View File

@ -134,8 +134,7 @@
.callout { .callout {
border-left: 3px solid #BBB; border-left: 3px solid #BBB;
background-color: #EEE; background-color: #EEE;
padding: $-s; padding: $-s $-s $-s $-xl;
padding-left: $-xl;
display: block; display: block;
position: relative; position: relative;
&:before { &:before {
@ -182,3 +181,77 @@
content: '\f1f1'; 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) { @mixin generate-button-colors($textColor, $backgroundColor) {
background-color: $backgroundColor; background-color: $backgroundColor;
color: $textColor; color: $textColor;
text-transform: uppercase;
border: 1px solid $backgroundColor;
vertical-align: top;
&:hover { &:hover {
background-color: lighten($backgroundColor, 8%); background-color: lighten($backgroundColor, 8%);
box-shadow: $bs-med; //box-shadow: $bs-med;
text-decoration: none; text-decoration: none;
color: $textColor; color: $textColor;
} }
@ -26,17 +29,16 @@ $button-border-radius: 2px;
text-decoration: none; text-decoration: none;
font-size: $fs-m; font-size: $fs-m;
line-height: 1.4em; line-height: 1.4em;
padding: $-xs $-m; padding: $-xs*1.3 $-m;
margin: $-xs $-xs $-xs 0; margin: $-xs $-xs $-xs 0;
display: inline-block; display: inline-block;
border: none; border: none;
font-weight: 500; font-weight: 400;
font-family: $text;
outline: 0; outline: 0;
border-radius: $button-border-radius; border-radius: $button-border-radius;
cursor: pointer; cursor: pointer;
transition: all ease-in-out 120ms; 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); @include generate-button-colors(#EEE, $primary);
} }
@ -52,19 +54,54 @@ $button-border-radius: 2px;
@include generate-button-colors(#EEE, $secondary); @include generate-button-colors(#EEE, $secondary);
} }
&.muted { &.muted {
@include generate-button-colors(#EEE, #888); @include generate-button-colors(#EEE, #AAA);
} }
&.muted-light { &.muted-light {
@include generate-button-colors(#666, #e4e4e4); @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 { .text-button {
@extend .link; @extend .link;
background-color: transparent; background-color: transparent;
padding: 0; padding: 0;
margin: 0; margin: 0;
border: none; border: none;
user-select: none;
&:focus, &:active { &:focus, &:active {
outline: 0; outline: 0;
} }

View File

@ -2,7 +2,6 @@
.CodeMirror { .CodeMirror {
/* Set height, width, borders, and global font properties here */ /* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px; height: 300px;
color: black; color: black;
} }
@ -235,7 +234,6 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0; border-width: 0;
background: transparent; background: transparent;
font-family: inherit;
font-size: inherit; font-size: inherit;
margin: 0; margin: 0;
white-space: pre; 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-atom { color: #aa759f; }
.cm-s-base16-light span.cm-number { 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-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 { color: #90a959; }
.cm-s-base16-light span.cm-variable-2 { color: #6a9fb5; } .cm-s-base16-light span.cm-variable-2 { color: #6a9fb5; }
@ -386,7 +384,10 @@ span.CodeMirror-selectedtext { background: none; }
/** /**
* Custom BookStack overrides * Custom BookStack overrides
*/ */
.cm-s-base16-light.CodeMirror { .CodeMirror, .CodeMirror pre {
font-size: 12px;
}
.CodeMirror {
font-size: 12px; font-size: 12px;
height: auto; height: auto;
margin-bottom: $-l; 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; } .cm-s-base16-light .CodeMirror-gutters { background: #f5f5f5; border-right: 1px solid #DDD; }
.flex-fill .CodeMirror { .code-fill .CodeMirror {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 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); background-color: rgba(0, 0, 0, 0.333);
position: fixed; position: fixed;
z-index: 95536; 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; text-align: left;
border-bottom: 1px solid #DDD; border-bottom: 1px solid #DDD;
margin-bottom: $-m; margin-bottom: $-m;
@ -480,3 +541,44 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
text-decoration: underline; 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 { .input-base {
background-color: #FFF; background-color: #FFF;
border-radius: 3px; border-radius: 3px;
border: 1px solid #CCC; border: 1px solid #D4D4D4;
display: inline-block; display: inline-block;
font-size: $fs-s; font-size: $fs-s;
font-family: $text; padding: $-xs*1.5;
padding: $-xs; color: #666;
color: #222;
width: 250px; width: 250px;
max-width: 100%; max-width: 100%;
//-webkit-appearance:none;
&.neg, &.invalid { &.neg, &.invalid {
border: 1px solid $negative; border: 1px solid $negative;
} }
@ -25,6 +23,11 @@
} }
} }
.fake-input {
@extend .input-base;
overflow: auto;
}
#html-editor { #html-editor {
display: none; display: none;
} }
@ -33,7 +36,6 @@
position: relative; position: relative;
z-index: 5; z-index: 5;
#markdown-editor-input { #markdown-editor-input {
font-family: 'Roboto Mono', monospace;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
padding: $-xs $-m; padding: $-xs $-m;
@ -69,7 +71,6 @@
.editor-toolbar { .editor-toolbar {
width: 100%; width: 100%;
padding: $-xs $-m; padding: $-xs $-m;
font-family: 'Roboto Mono', monospace;
font-size: 11px; font-size: 11px;
line-height: 1.6; line-height: 1.6;
border-bottom: 1px solid #DDD; border-bottom: 1px solid #DDD;
@ -87,8 +88,9 @@ label {
display: block; display: block;
line-height: 1.4em; line-height: 1.4em;
font-size: 0.94em; font-size: 0.94em;
font-weight: 500; font-weight: 400;
color: #666; color: #999;
text-transform: uppercase;
padding-bottom: 2px; padding-bottom: 2px;
margin-bottom: 0.2em; margin-bottom: 0.2em;
&.inline { &.inline {
@ -189,28 +191,15 @@ input:checked + .toggle-switch {
} }
.inline-input-style { .inline-input-style {
border: 2px dotted #BBB;
display: block; display: block;
width: 100%; width: 100%;
padding: $-xs $-s; padding: $-s;
}
.title-input .input {
width: 100%;
}
.title-input label, .description-input label{
margin-top: $-m;
color: #666;
} }
.title-input input[type="text"] { .title-input input[type="text"] {
@extend h1;
@extend .inline-input-style; @extend .inline-input-style;
margin-top: 0; margin-top: 0;
padding-right: 0; font-size: 2em;
width: 100%;
color: #444;
} }
.title-input.page-title { .title-input.page-title {
@ -251,21 +240,20 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
border: none; border: none;
color: $primary; color: $primary;
padding: 0; padding: 0;
margin: 0;
cursor: pointer; cursor: pointer;
margin-left: $-s; position: absolute;
} left: 8px;
button[type="submit"] { top: 9.5px;
margin-left: -$-l;
} }
input { input {
padding-right: $-l; display: block;
padding-left: $-l;
width: 300px; width: 300px;
max-width: 100%; max-width: 100%;
} }
} }
input.outline { .outline > input {
border: 0; border: 0;
border-bottom: 2px solid #DDD; border-bottom: 2px solid #DDD;
border-radius: 0; border-radius: 0;

View File

@ -20,19 +20,128 @@ body.flexbox {
align-items: stretch; align-items: stretch;
min-height: 0; min-height: 0;
position: relative; position: relative;
.flex, &.flex { &.rows {
flex-direction: row;
}
&.columns {
flex-direction: column;
}
}
.flex {
min-height: 0; min-height: 0;
flex: 1; 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-child > div {
flex: 1; flex: 1;
} }
//body.ie .flex-child > div { .flex.sidebar {
// flex: 1 0 0px; 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 */ /** Rules for all columns */
div[class^="col-"] img { div[class^="col-"] img {
@ -54,6 +163,10 @@ div[class^="col-"] img {
&.small { &.small {
max-width: 840px; max-width: 840px;
} }
&.nopad {
padding-left: 0;
padding-right: 0;
}
} }
.row { .row {

View File

@ -12,7 +12,6 @@ header {
padding: $-m; padding: $-m;
} }
border-bottom: 1px solid #DDD; border-bottom: 1px solid #DDD;
//margin-bottom: $-l;
.links { .links {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
@ -23,26 +22,27 @@ header {
} }
.links a { .links a {
display: inline-block; display: inline-block;
padding: $-l; padding: $-m $-l;
color: #FFF; color: #FFF;
&:last-child { &:last-child {
padding-right: 0; padding-right: 0;
} }
@include smaller-than($screen-md) { @include smaller-than($screen-md) {
padding: $-l $-s; padding: $-m $-s;
} }
} }
.avatar, .user-name { .avatar, .user-name {
display: inline-block; display: inline-block;
} }
.avatar { .avatar {
//margin-top: (45px/2);
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
.user-name { .user-name {
vertical-align: top; vertical-align: top;
padding-top: $-l; padding-top: $-m;
position: relative;
top: -3px;
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
> * { > * {
@ -66,53 +66,57 @@ header {
} }
} }
} }
@include smaller-than($screen-md) { @include smaller-than($screen-sm) {
text-align: center; text-align: center;
.float.right { .float.right {
float: none; float: none;
} }
}
@include smaller-than($screen-sm) {
.links a { .links a {
padding: $-s; padding: $-s;
} }
form.search-box {
margin-top: 0;
}
.user-name { .user-name {
padding-top: $-s; 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 { @include smaller-than($s) {
margin-top: $-l *0.9; .header-search {
display: inline-block; display: 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;
}
} }
} }
@ -128,12 +132,12 @@ form.search-box {
font-size: 1.8em; font-size: 1.8em;
color: #fff; color: #fff;
font-weight: 400; font-weight: 400;
padding: $-l $-l $-l 0; padding: 14px $-l 14px 0;
vertical-align: top; vertical-align: top;
line-height: 1; line-height: 1;
} }
.logo-image { .logo-image {
margin: $-m $-s $-m 0; margin: $-xs $-s $-xs 0;
vertical-align: top; vertical-align: top;
height: 43px; height: 43px;
} }
@ -167,6 +171,10 @@ form.search-box {
background-color: $primary-faded; background-color: $primary-faded;
} }
.toolbar-container {
background-color: #FFF;
}
.breadcrumbs .text-button, .action-buttons .text-button { .breadcrumbs .text-button, .action-buttons .text-button {
display: inline-block; display: inline-block;
padding: $-s; padding: $-s;
@ -228,3 +236,6 @@ form.search-box {
} }
} }
} }
.faded-small .nav-tabs a {
padding: $-s $-m;
}

View File

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

View File

@ -9,7 +9,6 @@
.inset-list { .inset-list {
display: none; display: none;
overflow: hidden; overflow: hidden;
margin-bottom: $-l;
} }
h5 { h5 {
display: block; display: block;
@ -22,6 +21,9 @@
border-left-color: $color-page-draft; border-left-color: $color-page-draft;
} }
} }
.entity-list-item {
margin-bottom: $-m;
}
hr { hr {
margin-top: 0; margin-top: 0;
} }
@ -51,23 +53,6 @@
margin-right: $-s; 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 { .sidebar-page-nav {
$nav-indent: $-s; $nav-indent: $-s;
@ -101,31 +86,8 @@
// Sidebar list // Sidebar list
.book-tree { .book-tree {
padding: $-xs 0 0 0;
position: relative;
right: 0;
top: 0;
transition: ease-in-out 240ms; transition: ease-in-out 240ms;
transition-property: right, border; 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 { .book-tree h4 {
padding: $-m $-s 0 $-s; padding: $-m $-s 0 $-s;
@ -171,7 +133,7 @@
background-color: rgba($color-chapter, 0.12); background-color: rgba($color-chapter, 0.12);
} }
} }
.chapter-toggle { [chapter-toggle] {
padding-left: $-s; padding-left: $-s;
} }
.list-item-chapter { .list-item-chapter {
@ -260,6 +222,9 @@
.left + .right { .left + .right {
margin-left: 30px + $-s; margin-left: 30px + $-s;
} }
&:last-of-type {
border-bottom: 0;
}
} }
ul.pagination { ul.pagination {
@ -312,9 +277,6 @@ ul.pagination {
h4 { h4 {
margin: 0; margin: 0;
} }
p {
margin: $-xs 0 0 0;
}
hr { hr {
margin: 0; 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 { .entity-list.compact {
font-size: 0.6em; font-size: 0.6em;
h4, a { h4, a {
line-height: 1.2; line-height: 1.2;
} }
p { .entity-item-snippet {
display: none; display: none;
}
.entity-list-item p {
font-size: $fs-m * 0.8; font-size: $fs-m * 0.8;
padding-top: $-xs; padding-top: $-xs;
}
p {
margin: 0; margin: 0;
} }
> p.empty-text { > p.empty-text {
@ -381,6 +352,7 @@ ul.pagination {
} }
li.padded { li.padded {
padding: $-xs $-m; padding: $-xs $-m;
line-height: 1.2;
} }
a { a {
display: block; 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 { .page-editor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -36,6 +27,8 @@
.page-content { .page-content {
max-width: 840px; max-width: 840px;
margin: 0 auto;
margin-top: $-xxl;
overflow-wrap: break-word; overflow-wrap: break-word;
.align-left { .align-left {
text-align: left; text-align: left;
@ -226,7 +219,7 @@
width: 100%; width: 100%;
min-width: 50px; min-width: 50px;
} }
.tags td { .tags td, .tag-table > div > div > div {
padding-right: $-s; padding-right: $-s;
padding-top: $-s; padding-top: $-s;
position: relative; position: relative;
@ -252,8 +245,6 @@
} }
.tag-display { .tag-display {
width: 100%;
//opacity: 0.7;
position: relative; position: relative;
table { table {
width: 100%; width: 100%;
@ -311,3 +302,7 @@
} }
} }
} }
.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
min-height: 175px;
}

View File

@ -58,13 +58,3 @@ table.list-table {
padding: $-xs; 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 * Header Styles
*/ */
@ -58,7 +69,6 @@ a, .link {
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
transition: color ease-in-out 80ms; transition: color ease-in-out 80ms;
font-family: $text;
line-height: 1.6; line-height: 1.6;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
@ -131,7 +141,6 @@ sub, .subscript {
} }
pre { pre {
font-family: monospace;
font-size: 12px; font-size: 12px;
background-color: #f5f5f5; background-color: #f5f5f5;
border: 1px solid #DDD; border: 1px solid #DDD;
@ -180,7 +189,6 @@ blockquote {
.code-base { .code-base {
background-color: #F8F8F8; background-color: #F8F8F8;
font-family: monospace;
font-size: 0.80em; font-size: 0.80em;
border: 1px solid #DDD; border: 1px solid #DDD;
border-radius: 3px; border-radius: 3px;
@ -370,12 +378,6 @@ span.sep {
display: block; display: block;
} }
.action-header {
h1 {
margin-top: $-m;
}
}
/** /**
* Icons * Icons
*/ */

View File

@ -49,3 +49,6 @@
} }
} }
} }
.page-content.mce-content-body p {
line-height: 1.6;
}

View File

@ -27,8 +27,12 @@ $-xs: 6px;
$-xxs: 3px; $-xxs: 3px;
// Fonts // Fonts
$heading: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif; $text: -apple-system, BlinkMacSystemFont,
$text: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif; "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-m: 15px;
$fs-s: 14px; $fs-s: 14px;

View File

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

View File

@ -1,6 +1,5 @@
@import "reset"; @import "reset";
@import "variables"; @import "variables";
@import "fonts";
@import "mixins"; @import "mixins";
@import "html"; @import "html";
@import "text"; @import "text";
@ -17,12 +16,11 @@
@import "lists"; @import "lists";
@import "pages"; @import "pages";
[v-cloak], [v-show] { [v-cloak] {
display: none; opacity: 0; display: none; opacity: 0;
animation-name: none !important; animation-name: none !important;
} }
[ng\:cloak], [ng-cloak], .ng-cloak { [ng\:cloak], [ng-cloak], .ng-cloak {
display: none !important; display: none !important;
user-select: none; 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 // Loading icon
$loadingSize: 10px; $loadingSize: 10px;
.loading-container { .loading-container {
position: relative; position: relative;
display: block; display: block;
height: $loadingSize;
margin: $-xl auto; margin: $-xl auto;
> div { > div {
width: $loadingSize; width: $loadingSize;
@ -116,7 +75,8 @@ $loadingSize: 10px;
border-radius: $loadingSize; border-radius: $loadingSize;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
transform: translate3d(0, 0, 0); transform: translate3d(-10px, 0, 0);
margin-top: $-xs;
animation-name: loadingBob; animation-name: loadingBob;
animation-duration: 1.4s; animation-duration: 1.4s;
animation-iteration-count: infinite; animation-iteration-count: infinite;
@ -130,11 +90,17 @@ $loadingSize: 10px;
background-color: $color-book; background-color: $color-book;
animation-delay: 0s; animation-delay: 0s;
} }
> div:last-child { > div:last-of-type {
left: $loadingSize+$-xs; left: $loadingSize+$-xs;
background-color: $color-chapter; background-color: $color-chapter;
animation-delay: 0.6s; 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 // Back to top link
$btt-size: 40px; $btt-size: 40px;
#back-to-top { [back-to-top] {
background-color: $primary; background-color: $primary;
position: fixed; position: fixed;
bottom: $-m; bottom: $-m;
@ -256,22 +222,15 @@ $btt-size: 40px;
} }
.center-box { .center-box {
margin: $-xl auto 0 auto; margin: $-xxl auto 0 auto;
padding: $-m $-xxl $-xl $-xxl;
width: 420px; width: 420px;
max-width: 100%; max-width: 100%;
display: inline-block; display: inline-block;
text-align: left; text-align: left;
vertical-align: top; vertical-align: top;
//border: 1px solid #DDD;
input { input {
width: 100%; 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 // Pages
'page_create' => 'Seite erstellt', 'page_create' => 'hat Seite erstellt:',
'page_create_notification' => 'Seite erfolgreich erstellt', 'page_create_notification' => 'hat Seite erfolgreich erstellt:',
'page_update' => 'Seite aktualisiert', 'page_update' => 'hat Seite aktualisiert:',
'page_update_notification' => 'Seite erfolgreich aktualisiert', 'page_update_notification' => 'hat Seite erfolgreich aktualisiert:',
'page_delete' => 'Seite gel&ouml;scht', 'page_delete' => 'hat Seite gelöscht:',
'page_delete_notification' => 'Seite erfolgreich gel&ouml;scht', 'page_delete_notification' => 'hat Seite erfolgreich gelöscht:',
'page_restore' => 'Seite wiederhergstellt', 'page_restore' => 'hat Seite wiederhergstellt:',
'page_restore_notification' => 'Seite erfolgreich wiederhergstellt', 'page_restore_notification' => 'hat Seite erfolgreich wiederhergstellt:',
'page_move' => 'Seite verschoben', 'page_move' => 'hat Seite verschoben:',
// Chapters // Chapters
'chapter_create' => 'Kapitel erstellt', 'chapter_create' => 'hat Kapitel erstellt:',
'chapter_create_notification' => 'Kapitel erfolgreich erstellt', 'chapter_create_notification' => 'hat Kapitel erfolgreich erstellt:',
'chapter_update' => 'Kapitel aktualisiert', 'chapter_update' => 'hat Kapitel aktualisiert:',
'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert', 'chapter_update_notification' => 'hat Kapitel erfolgreich aktualisiert:',
'chapter_delete' => 'Kapitel gel&ouml;scht', 'chapter_delete' => 'hat Kapitel gelöscht',
'chapter_delete_notification' => 'Kapitel erfolgreich gel&ouml;scht', 'chapter_delete_notification' => 'hat Kapitel erfolgreich gelöscht:',
'chapter_move' => 'Kapitel verschoben', 'chapter_move' => 'hat Kapitel verschoben:',
// Books // Books
'book_create' => 'Buch erstellt', 'book_create' => 'hat Buch erstellt:',
'book_create_notification' => 'Buch erfolgreich erstellt', 'book_create_notification' => 'hat Buch erfolgreich erstellt:',
'book_update' => 'Buch aktualisiert', 'book_update' => 'hat Buch aktualisiert:',
'book_update_notification' => 'Buch erfolgreich aktualisiert', 'book_update_notification' => 'hat Buch erfolgreich aktualisiert:',
'book_delete' => 'Buch gel&ouml;scht', 'book_delete' => 'hat Buch gelöscht:',
'book_delete_notification' => 'Buch erfolgreich gel&ouml;scht', 'book_delete_notification' => 'hat Buch erfolgreich gelöscht:',
'book_sort' => 'Buch sortiert', 'book_sort' => 'hat Buch sortiert:',
'book_sort_notification' => 'Buch erfolgreich neu 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. | these language lines according to your application's requirements.
| |
*/ */
'failed' => 'Dies sind keine g&uuml;ltigen Anmeldedaten.', 'failed' => 'Die eingegebenen Anmeldedaten sind ungültig.',
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen sie es in :seconds Sekunden erneut.', 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.',
/** /**
* Login & Register * Login & Register
@ -29,16 +29,16 @@ return [
'forgot_password' => 'Passwort vergessen?', 'forgot_password' => 'Passwort vergessen?',
'remember_me' => 'Angemeldet bleiben', 'remember_me' => 'Angemeldet bleiben',
'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.', 'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.',
'create_account' => 'Account anlegen', 'create_account' => 'Account registrieren',
'social_login' => 'Social Login', 'social_login' => 'Mit Sozialem Netzwerk anmelden',
'social_registration' => 'Social Registrierung', 'social_registration' => 'Mit Sozialem Netzwerk registrieren',
'social_registration_text' => 'Mit einem dieser Möglichkeiten registrieren oder anmelden.', 'social_registration_text' => 'Mit einer dieser Dienste registrieren oder anmelden',
'register_thanks' => 'Vielen Dank für Ihre Registrierung!', '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.', 'register_confirm' => 'Bitte prüfen Sie Ihren Posteingang und bestätigen Sie die Registrierung.',
'registrations_disabled' => 'Die Registrierung ist momentan nicht möglich', 'registrations_disabled' => 'Eine Registrierung ist momentan nicht möglich',
'registration_email_domain_invalid' => 'Diese E-Mail-Domain ist für die Benutzer der Applikation nicht freigeschaltet.', '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.', 'register_success' => 'Vielen Dank für Ihre Registrierung! Die Daten sind gespeichert und Sie sind angemeldet.',
@ -46,30 +46,30 @@ return [
* Password Reset * Password Reset
*/ */
'reset_password' => 'Passwort vergessen', '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_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_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ück gesetzt.', 'reset_password_success' => 'Ihr Passwort wurde erfolgreich zurückgesetzt.',
'email_reset_subject' => 'Passwort zurücksetzen für :appName', '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_text' => 'Sie erhalten diese E-Mail, weil jemand versucht hat, Ihr Passwort zurückzusetzen.',
'email_reset_not_requested' => 'Wenn Sie die Passwort-Rücksetzung nicht ausgelöst haben, ist kein weiteres Handeln notwendig.', 'email_reset_not_requested' => 'Wenn Sie das nicht waren, brauchen Sie nichts weiter zu tun.',
/** /**
* Email Confirmation * Email Confirmation
*/ */
'email_confirm_subject' => 'Best&auml;tigen sie ihre E-Mail Adresse bei :appName', 'email_confirm_subject' => 'Bestätigen Sie Ihre E-Mail-Adresse für :appName',
'email_confirm_greeting' => 'Danke, dass sie :appName beigetreten sind!', 'email_confirm_greeting' => 'Danke, dass Sie sich für :appName registriert haben!',
'email_confirm_text' => 'Bitte best&auml;tigen sie ihre E-Mail Adresse, indem sie auf den Button klicken:', '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&auml;tigen', 'email_confirm_action' => 'E-Mail-Adresse bestä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_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_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_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' => 'E-Mail-Adresse ist nicht bestätigt',
'email_not_confirmed_text' => 'Ihre E-Mail-Adresse ist bisher 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_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' => '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', 'edit' => 'Bearbeiten',
'sort' => 'Sortieren', 'sort' => 'Sortieren',
'move' => 'Verschieben', 'move' => 'Verschieben',
'delete' => 'L&ouml;schen', 'delete' => 'Löschen',
'search' => 'Suchen', 'search' => 'Suchen',
'search_clear' => 'Suche l&ouml;schen', 'search_clear' => 'Suche löschen',
'reset' => 'Zurücksetzen', 'reset' => 'Zurücksetzen',
'remove' => 'Entfernen', 'remove' => 'Entfernen',
@ -38,9 +38,9 @@ return [
/** /**
* Misc * Misc
*/ */
'deleted_user' => 'Gel&ouml;schte Benutzer', 'deleted_user' => 'Gelöschte Benutzer',
'no_activity' => 'Keine Aktivit&auml;ten zum Anzeigen', 'no_activity' => 'Keine Aktivitäten zum Anzeigen',
'no_items' => 'Keine Eintr&auml;ge gefunden.', 'no_items' => 'Keine Einträge gefunden.',
'back_to_top' => 'nach oben', 'back_to_top' => 'nach oben',
'toggle_details' => 'Details zeigen/verstecken', 'toggle_details' => 'Details zeigen/verstecken',
@ -53,6 +53,6 @@ return [
/** /**
* Email Content * 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', 'email_rights' => 'Alle Rechte vorbehalten',
]; ];

View File

@ -13,9 +13,9 @@ return [
'image_uploaded' => 'Hochgeladen am :uploadedDate', 'image_uploaded' => 'Hochgeladen am :uploadedDate',
'image_load_more' => 'Mehr', 'image_load_more' => 'Mehr',
'image_image_name' => 'Bildname', '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_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', 'images_deleted' => 'Bilder gelöscht',
'image_preview' => 'Bildvorschau', 'image_preview' => 'Bildvorschau',
'image_upload_success' => 'Bild erfolgreich hochgeladen', 'image_upload_success' => 'Bild erfolgreich hochgeladen',

View File

@ -4,38 +4,38 @@ return [
/** /**
* Shared * Shared
*/ */
'recently_created' => 'K&uuml;rzlich angelegt', 'recently_created' => 'Kürzlich angelegt',
'recently_created_pages' => 'K&uuml;rzlich angelegte Seiten', 'recently_created_pages' => 'Kürzlich angelegte Seiten',
'recently_updated_pages' => 'K&uuml;rzlich aktualisierte Seiten', 'recently_updated_pages' => 'Kürzlich aktualisierte Seiten',
'recently_created_chapters' => 'K&uuml;rzlich angelegte Kapitel', 'recently_created_chapters' => 'Kürzlich angelegte Kapitel',
'recently_created_books' => 'K&uuml;rzlich angelegte B&uuml;cher', 'recently_created_books' => 'Kürzlich angelegte Bücher',
'recently_update' => 'K&uuml;rzlich aktualisiert', 'recently_update' => 'Kürzlich aktualisiert',
'recently_viewed' => 'K&uuml;rzlich angesehen', 'recently_viewed' => 'Kürzlich angesehen',
'recent_activity' => 'K&uuml;rzliche Aktivit&auml;t', 'recent_activity' => 'Kürzliche Aktivität',
'create_now' => 'Jetzt anlegen', 'create_now' => 'Jetzt anlegen',
'revisions' => 'Revisionen', 'revisions' => 'Versionen',
'meta_created' => 'Angelegt am :timeLength', 'meta_revision' => 'Version #:revisionCount',
'meta_created_name' => 'Angelegt am :timeLength durch :user', 'meta_created' => 'Erstellt: :timeLength',
'meta_updated' => 'Aktualisiert am :timeLength', 'meta_created_name' => 'Erstellt: :timeLength von :user',
'meta_updated_name' => 'Aktualisiert am :timeLength durch :user', 'meta_updated' => 'Zuletzt aktualisiert: :timeLength',
'x_pages' => ':count Seiten', 'meta_updated_name' => 'Zuletzt aktualisiert: :timeLength von :user',
'entity_select' => 'Eintrag ausw&auml;hlen', 'entity_select' => 'Eintrag auswählen',
'images' => 'Bilder', 'images' => 'Bilder',
'my_recent_drafts' => 'Meine k&uuml;rzlichen Entw&uuml;rfe', 'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
'my_recently_viewed' => 'K&uuml;rzlich von mir angesehen', 'my_recently_viewed' => 'Kürzlich von mir angesehen',
'no_pages_viewed' => 'Sie haben bisher keine Seiten angesehen.', 'no_pages_viewed' => 'Sie haben bisher keine Seiten angesehen.',
'no_pages_recently_created' => 'Sie haben bisher keine Seiten angelegt.', 'no_pages_recently_created' => 'Sie haben bisher keine Seiten angelegt.',
'no_pages_recently_updated' => 'Sie haben bisher keine Seiten aktualisiert.', 'no_pages_recently_updated' => 'Sie haben bisher keine Seiten aktualisiert.',
'export' => 'Exportieren', 'export' => 'Exportieren',
'export_html' => 'HTML-Datei', 'export_html' => 'HTML-Datei',
'export_pdf' => 'PDF-Datei', 'export_pdf' => 'PDF-Datei',
'export_text' => 'Text-Datei', 'export_text' => 'Textdatei',
/** /**
* Permissions and restrictions * Permissions and restrictions
*/ */
'permissions' => 'Berechtigungen', '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_enable' => 'Individuelle Berechtigungen aktivieren',
'permissions_save' => 'Berechtigungen speichern', 'permissions_save' => 'Berechtigungen speichern',
@ -43,41 +43,61 @@ return [
* Search * Search
*/ */
'search_results' => 'Suchergebnisse', 'search_results' => 'Suchergebnisse',
'search_clear' => 'Suche zur&uuml;cksetzen', 'search_total_results_found' => ':count Ergebnis gefunden|:count Ergebnisse gesamt',
'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden', 'search_clear' => 'Filter löschen',
'search_for_term' => 'Suche nach :term', '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 * Books
*/ */
'book' => 'Buch', 'book' => 'Buch',
'books' => 'B&uuml;cher', 'books' => 'Bücher',
'books_empty' => 'Es wurden keine B&uuml;cher angelegt', 'x_books' => ':count Buch|:count Bücher',
'books_popular' => 'Popul&auml;re B&uuml;cher', 'books_empty' => 'Keine Bücher vorhanden',
'books_recent' => 'K&uuml;rzlich genutzte B&uuml;cher', 'books_popular' => 'Beliebte Bücher',
'books_popular_empty' => 'Die popul&auml;rsten B&uuml;cher werden hier angezeigt.', 'books_recent' => 'Kürzlich angesehene Bücher',
'books_create' => 'Neues Buch anlegen', 'books_new' => 'Neue Bücher',
'books_delete' => 'Buch l&ouml;schen', 'books_popular_empty' => 'Die beliebtesten Bücher werden hier angezeigt.',
'books_delete_named' => 'Buch :bookName l&ouml;schen', 'books_new_empty' => 'Die neusten Bücher werden hier angezeigt.',
'books_delete_explain' => 'Sie m&ouml;chten das Buch \':bookName\' l&ouml;schen und alle Seiten und Kapitel entfernen.', 'books_create' => 'Neues Buch erstellen',
'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch l&ouml;schen m&ouml;chten?', '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' => 'Buch bearbeiten',
'books_edit_named' => 'Buch :bookName bearbeiten', 'books_edit_named' => 'Buch ":bookName" bearbeiten',
'books_form_book_name' => 'Buchname', 'books_form_book_name' => 'Name des Buches',
'books_save' => 'Buch speichern', 'books_save' => 'Buch speichern',
'books_permissions' => 'Buch-Berechtigungen', 'books_permissions' => 'Buch-Berechtigungen',
'books_permissions_updated' => 'Buch-Berechtigungen aktualisiert', '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_create_page' => 'Neue Seite anlegen',
'books_empty_or' => 'oder', 'books_empty_or' => 'oder',
'books_empty_sort_current_book' => 'Aktuelles Buch sortieren', '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_permissions_active' => 'Buch-Berechtigungen aktiv',
'books_search_this' => 'Dieses Buch durchsuchen', 'books_search_this' => 'Dieses Buch durchsuchen',
'books_navigation' => 'Buch-Navigation', 'books_navigation' => 'Buchnavigation',
'books_sort' => 'Buchinhalte sortieren', 'books_sort' => 'Buchinhalte sortieren',
'books_sort_named' => 'Buch :bookName sortieren', 'books_sort_named' => 'Buch ":bookName" sortieren',
'books_sort_show_other' => 'Andere B&uuml;cher zeigen', 'books_sort_show_other' => 'Andere Bücher anzeigen',
'books_sort_save' => 'Neue Reihenfolge speichern', 'books_sort_save' => 'Neue Reihenfolge speichern',
/** /**
@ -85,132 +105,156 @@ return [
*/ */
'chapter' => 'Kapitel', 'chapter' => 'Kapitel',
'chapters' => 'Kapitel', 'chapters' => 'Kapitel',
'chapters_popular' => 'Popul&auml;re Kapitel', 'x_chapters' => ':count Kapitel',
'chapters_popular' => 'Beliebte Kapitel',
'chapters_new' => 'Neues Kapitel', 'chapters_new' => 'Neues Kapitel',
'chapters_create' => 'Neues Kapitel anlegen', 'chapters_create' => 'Neues Kapitel anlegen',
'chapters_delete' => 'Kapitel entfernen', 'chapters_delete' => 'Kapitel entfernen',
'chapters_delete_named' => 'Kapitel :chapterName 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_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&ouml;schen m&ouml;chten?', 'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel löschen möchten?',
'chapters_edit' => 'Kapitel bearbeiten', 'chapters_edit' => 'Kapitel bearbeiten',
'chapters_edit_named' => 'Kapitel :chapterName bearbeiten', 'chapters_edit_named' => 'Kapitel ":chapterName" bearbeiten',
'chapters_save' => 'Kapitel speichern', 'chapters_save' => 'Kapitel speichern',
'chapters_move' => 'Kapitel verschieben', 'chapters_move' => 'Kapitel verschieben',
'chapters_move_named' => 'Kapitel :chapterName verschieben', 'chapters_move_named' => 'Kapitel ":chapterName" verschieben',
'chapter_move_success' => 'Kapitel in das Buch :bookName verschoben.', 'chapter_move_success' => 'Das Kapitel wurde in das Buch ":bookName" verschoben.',
'chapters_permissions' => 'Kapitel-Berechtigungen', '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_active' => 'Kapitel-Berechtigungen aktiv',
'chapters_permissions_success' => 'Kapitel-Berechtigungenen aktualisisert', 'chapters_permissions_success' => 'Kapitel-Berechtigungenen aktualisisert',
'chapters_search_this' => 'Dieses Kapitel durchsuchen',
/** /**
* Pages * Pages
*/ */
'page' => 'Seite', 'page' => 'Seite',
'pages' => 'Seiten', 'pages' => 'Seiten',
'pages_popular' => 'Popul&auml;re Seiten', 'x_pages' => ':count Seite|:count Seiten',
'pages_popular' => 'Beliebte Seiten',
'pages_new' => 'Neue Seite', 'pages_new' => 'Neue Seite',
'pages_attachments' => 'Anh&auml;nge', 'pages_attachments' => 'Anhänge',
'pages_navigation' => 'Seitennavigation', 'pages_navigation' => 'Seitennavigation',
'pages_delete' => 'Seite l&ouml;schen', 'pages_delete' => 'Seite löschen',
'pages_delete_named' => 'Seite :pageName l&ouml;schen', 'pages_delete_named' => 'Seite ":pageName" löschen',
'pages_delete_draft_named' => 'Seitenentwurf von :pageName l&ouml;schen', 'pages_delete_draft_named' => 'Seitenentwurf von ":pageName" löschen',
'pages_delete_draft' => 'Seitenentwurf l&ouml;schen', 'pages_delete_draft' => 'Seitenentwurf löschen',
'pages_delete_success' => 'Seite gel&ouml;scht', 'pages_delete_success' => 'Seite gelöscht',
'pages_delete_draft_success' => 'Seitenentwurf gel&ouml;scht', 'pages_delete_draft_success' => 'Seitenentwurf gelöscht',
'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite l&ouml;schen m&ouml;chen?', '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&ouml;schen m&ouml;chten?', 'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf löschen möchten?',
'pages_editing_named' => 'Seite :pageName bearbeiten', 'pages_editing_named' => 'Seite ":pageName" bearbeiten',
'pages_edit_toggle_header' => 'Toggle header', 'pages_edit_toggle_header' => 'Hauptmenü anzeigen/verstecken',
'pages_edit_save_draft' => 'Entwurf speichern', 'pages_edit_save_draft' => 'Entwurf speichern',
'pages_edit_draft' => 'Seitenentwurf bearbeiten', 'pages_edit_draft' => 'Seitenentwurf bearbeiten',
'pages_editing_draft' => 'Seitenentwurf bearbeiten', 'pages_editing_draft' => 'Seitenentwurf bearbeiten',
'pages_editing_page' => 'Seite bearbeiten', 'pages_editing_page' => 'Seite bearbeiten',
'pages_edit_draft_save_at' => 'Entwurf gespeichert um ', '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_discard_draft' => 'Entwurf verwerfen',
'pages_edit_set_changelog' => 'Ver&auml;nderungshinweis setzen', 'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen',
'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer &Auml;nderungen ein', 'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer Änderungen ein',
'pages_edit_enter_changelog' => 'Ver&auml;nderungshinweis eingeben', 'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben',
'pages_save' => 'Seite speichern', 'pages_save' => 'Seite speichern',
'pages_title' => 'Seitentitel', 'pages_title' => 'Seitentitel',
'pages_name' => 'Seitenname', 'pages_name' => 'Seitenname',
'pages_md_editor' => 'Redakteur', 'pages_md_editor' => 'Redakteur',
'pages_md_preview' => 'Vorschau', 'pages_md_preview' => 'Vorschau',
'pages_md_insert_image' => 'Bild einf&uuml;gen', 'pages_md_insert_image' => 'Bild einfügen',
'pages_md_insert_link' => 'Link zu einem Objekt einf&uuml;gen', 'pages_md_insert_link' => 'Link zu einem Objekt einfügen',
'pages_not_in_chapter' => 'Seite ist in keinem Kapitel', 'pages_not_in_chapter' => 'Seite ist in keinem Kapitel',
'pages_move' => 'Seite verschieben', 'pages_move' => 'Seite verschieben',
'pages_move_success' => 'Seite nach ":parentName" verschoben', 'pages_move_success' => 'Seite nach ":parentName" verschoben',
'pages_permissions' => 'Seiten Berechtigungen', 'pages_permissions' => 'Seiten Berechtigungen',
'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert', 'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert',
'pages_revision' => 'Version',
'pages_revisions' => 'Seitenversionen', 'pages_revisions' => 'Seitenversionen',
'pages_revisions_named' => 'Seitenversionen von :pageName', 'pages_revisions_named' => 'Seitenversionen von ":pageName"',
'pages_revision_named' => 'Seitenversion von :pageName', 'pages_revision_named' => 'Seitenversion von ":pageName"',
'pages_revisions_created_by' => 'Angelegt von', 'pages_revisions_created_by' => 'Erstellt von',
'pages_revisions_date' => 'Versionsdatum', 'pages_revisions_date' => 'Versionsdatum',
'pages_revisions_changelog' => 'Ver&auml;nderungshinweise', 'pages_revisions_number' => '#',
'pages_revisions_changes' => 'Ver&auml;nderungen', 'pages_revisions_changelog' => 'Änderungsprotokoll',
'pages_revisions_changes' => 'Änderungen',
'pages_revisions_current' => 'Aktuelle Version', 'pages_revisions_current' => 'Aktuelle Version',
'pages_revisions_preview' => 'Vorschau', 'pages_revisions_preview' => 'Vorschau',
'pages_revisions_restore' => 'Zur&uuml;ck sichern', 'pages_revisions_restore' => 'Wiederherstellen',
'pages_revisions_none' => 'Diese Seite hat keine &auml;lteren Versionen.', 'pages_revisions_none' => 'Diese Seite hat keine älteren Versionen.',
'pages_copy_link' => 'Link kopieren', 'pages_copy_link' => 'Link kopieren',
'pages_permissions_active' => 'Seiten-Berechtigungen aktiv', '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_initial_name' => 'Neue Seite',
'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt um :timeDiff gespeichert wurde.', '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&auml;ndert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.', 'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
'pages_draft_edit_active' => [ 'pages_draft_edit_active' => [
'start_a' => ':count Benutzer haben die Bearbeitung dieser Seite begonnen.', 'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.',
'start_b' => ':userName hat die Bearbeitung dieser Seite begonnen.', 'start_b' => ':userName bearbeitet jetzt diese Seite.',
'time_a' => 'seit die Seiten zuletzt aktualisiert wurden.', 'time_a' => 'seit die Seiten zuletzt aktualisiert wurden.',
'time_b' => 'in den letzten :minCount Minuten', '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.', 'pages_draft_discarded' => 'Entwurf verworfen. Der aktuelle Seiteninhalt wurde geladen.',
/** /**
* Editor sidebar * Editor sidebar
*/ */
'page_tags' => 'Seiten-Schlagw&ouml;rter', 'page_tags' => 'Seiten-Schlagwörter',
'tag' => 'Schlagwort', 'tag' => 'Schlagwort',
'tags' => 'Schlagworte', 'tags' => 'Schlagwörter',
'tag_value' => 'Schlagwortinhalt (Optional)', 'tag_value' => 'Inhalt (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_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&uuml;gen', 'tags_add' => 'Weiteres Schlagwort hinzufügen',
'attachments' => 'Anh&auml;nge', 'attachments' => 'Anhä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' => 'Sie können auf Ihrer Seite Dateien hochladen oder Links hinzufügen. Diese werden in der Seitenleiste angezeigt.',
'attachments_explain_instant_save' => '&Auml;nderungen werden direkt gespeichert.', 'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.',
'attachments_items' => 'Angef&uuml;gte Elemente', 'attachments_items' => 'Angefügte Elemente',
'attachments_upload' => 'Datei hochladen', 'attachments_upload' => 'Datei hochladen',
'attachments_link' => 'Link anf&uuml;gen', 'attachments_link' => 'Link hinzufügen',
'attachments_set_link' => 'Link setzen', 'attachments_set_link' => 'Link setzen',
'attachments_delete_confirm' => 'Klicken Sie erneut auf l&ouml;schen, um diesen Anhang zu entfernen.', 'attachments_delete_confirm' => 'Klicken Sie erneut auf löschen, um diesen Anhang zu entfernen.',
'attachments_dropzone' => 'Ziehen Sie Dateien hier hinein oder klicken Sie hier, um eine Datei auszuw&auml;hlen', 'attachments_dropzone' => 'Ziehen Sie Dateien hierher oder klicken Sie, um eine Datei auszuwählen',
'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.', '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', 'attachments_link_name' => 'Link-Name',
'attachment_link' => 'Link zum Anhang', 'attachment_link' => 'Link zum Anhang',
'attachments_link_url' => 'Link zu einer Datei', 'attachments_link_url' => 'Link zu einer Datei',
'attachments_link_url_hint' => 'URL einer Seite oder 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' => 'Datei bearbeiten',
'attachments_edit_file_name' => 'Dateiname', 'attachments_edit_file_name' => 'Dateiname',
'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hier hinein, um diese hochzuladen und zu &uuml;berschreiben', 'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hierher, um diese hochzuladen und zu überschreiben',
'attachments_order_updated' => 'Reihenfolge der Anh&auml;nge aktualisiert', 'attachments_order_updated' => 'Reihenfolge der Anhänge aktualisiert',
'attachments_updated_success' => 'Anhang-Details aktualisiert', 'attachments_updated_success' => 'Anhangdetails aktualisiert',
'attachments_deleted' => 'Anhang gel&ouml;scht', 'attachments_deleted' => 'Anhang gelöscht',
'attachments_file_uploaded' => 'Datei erfolgrecich hochgeladen', 'attachments_file_uploaded' => 'Datei erfolgreich hochgeladen',
'attachments_file_updated' => 'Datei erfolgreich aktualisisert', 'attachments_file_updated' => 'Datei erfolgreich aktualisiert',
'attachments_link_attached' => 'Link erfolgreich der Seite hinzugef&uuml;gt', 'attachments_link_attached' => 'Link erfolgreich der Seite hinzugefügt',
/** /**
* Profile View * Profile View
*/ */
'profile_user_for_x' => 'Benutzer seit :time', 'profile_user_for_x' => 'Benutzer seit :time',
'profile_created_content' => 'Angelegte Inhalte', 'profile_created_content' => 'Erstellte Inhalte',
'profile_not_created_pages' => ':userName hat bisher keine Seiten angelegt.', 'profile_not_created_pages' => ':userName hat noch keine Seiten erstellt.',
'profile_not_created_chapters' => ':userName hat bisher keine Kapitel angelegt.', 'profile_not_created_chapters' => ':userName hat noch keine Kapitel erstellt.',
'profile_not_created_books' => ':userName hat bisher keine B&uuml;cher angelegt.', '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 // Pages
'permission' => 'Sie haben keine Berechtigung auf diese Seite zuzugreifen.', 'permission' => 'Sie haben keine Berechtigung, auf diese Seite zuzugreifen.',
'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuf&uuml;hren.', 'permissionJson' => 'Sie haben keine Berechtigung, die angeforderte Aktion auszuführen.',
// Auth // Auth
'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten angelegt.', '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&auml;tigt. Bitte melden Sie sich an.', 'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestä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_invalid' => 'Der Bestätigungslink ist nicht gü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.', '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_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlafgen',
'ldap_fail_authed' => 'LDAP Zugriff mit DN & Passwort ist fehlgeschlagen', 'ldap_fail_authed' => 'LDAP-Zugriff mit DN und Passwort ist fehlgeschlagen',
'ldap_extension_not_installed' => 'LDAP PHP Erweiterung ist nicht installiert.', '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.', '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_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_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_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&uuml;pft.', 'social_account_existing' => 'Dieses :socialAccount-Konto ist bereits mit Ihrem Profil verknüpft.',
'social_account_already_used_existing' => 'Dieses :socialAccount Konto wird bereits durch einen anderen Benutzer verwendet.', '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. Bitte verkn&uuml;pfen Sie deses in Ihrem Profil-Einstellungen.', '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&ouml;nnen Sie ein solches Konto mit der :socialAccount Option anlegen.', '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' => 'Social-Media Konto Treiber nicht gefunden', 'social_driver_not_found' => 'Treiber für Social-Media-Konten nicht gefunden',
'social_driver_not_configured' => 'Ihr :socialAccount Konto ist nicht korrekt konfiguriert.', 'social_driver_not_configured' => 'Ihr :socialAccount-Konto ist nicht korrekt konfiguriert.',
// System // 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.', '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_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.', '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&ouml;&szlig;e. Bitte versuchen Sie es mit einer kleineren Datei.', '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.', 'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.',
// Attachments // 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 // 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.', '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', 'book_not_found' => 'Buch nicht gefunden',
'page_not_found' => 'Seite nicht gefunden', 'page_not_found' => 'Seite nicht gefunden',
'chapter_not_found' => 'Kapitel nicht gefunden', 'chapter_not_found' => 'Kapitel nicht gefunden',
'selected_book_not_found' => 'Das gew&auml;hlte Buch wurde nicht gefunden.', 'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden.',
'selected_book_chapter_not_found' => 'Das gew&auml;hlte Buch oder Kapitel wurde nicht gefunden.', 'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.',
'guests_cannot_save_drafts' => 'G&auml;ste k&ouml;nnen keine Entw&uuml;rfe speichern', 'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern',
// Users // Users
'users_cannot_delete_only_admin' => 'Sie k&ouml;nnen den einzigen Administrator nicht l&ouml;schen.', 'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen.',
'users_cannot_delete_guest' => 'Sie k&ouml;nnen den Gast-Benutzer nicht l&ouml;schen', 'users_cannot_delete_guest' => 'Sie können den Gast-Benutzer nicht löschen',
// Roles // Roles
'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden.', '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_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelö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_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 // Error pages
'404_page_not_found' => 'Seite nicht gefunden', '404_page_not_found' => 'Seite nicht gefunden',
'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben wurde nicht gefunden.', 'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben, wurde nicht gefunden.',
'return_home' => 'Zur&uuml;ck zur Startseite', 'return_home' => 'Zurück zur Startseite',
'error_occurred' => 'Es ist ein Fehler aufgetreten', 'error_occurred' => 'Es ist ein Fehler aufgetreten',
'app_down' => ':appName befindet sich aktuell im Wartungsmodus.', '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', '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