Merge branch 'master' into release
This commit is contained in:
commit
9bde0ae4ea
|
@ -8,16 +8,15 @@ Homestead.yaml
|
|||
/public/css/*.map
|
||||
/public/js/*.map
|
||||
/public/bower
|
||||
/public/build/
|
||||
/storage/images
|
||||
_ide_helper.php
|
||||
/storage/debugbar
|
||||
.phpstorm.meta.php
|
||||
yarn.lock
|
||||
/bin
|
||||
nbproject
|
||||
.buildpath
|
||||
|
||||
.project
|
||||
|
||||
.settings/org.eclipse.wst.common.project.facet.core.xml
|
||||
|
||||
.settings/org.eclipse.php.core.prefs
|
||||
|
|
|
@ -2,7 +2,7 @@ dist: trusty
|
|||
sudo: false
|
||||
language: php
|
||||
php:
|
||||
- 7.0
|
||||
- 7.0.7
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
<?php namespace BookStack;
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
class Entity extends Ownable
|
||||
{
|
||||
|
||||
|
@ -65,6 +67,17 @@ class Entity extends Ownable
|
|||
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comments for an entity
|
||||
* @param bool $orderByCreated
|
||||
* @return MorphMany
|
||||
*/
|
||||
public function comments($orderByCreated = true)
|
||||
{
|
||||
$query = $this->morphMany(Comment::class, 'entity');
|
||||
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related search terms.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
|
|
|
@ -8,6 +8,7 @@ use BookStack\Exceptions\UserRegistrationException;
|
|||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\EmailConfirmationService;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use BookStack\SocialAccount;
|
||||
use BookStack\User;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
|
@ -103,7 +104,7 @@ class RegisterController extends Controller
|
|||
* @param Request|\Illuminate\Http\Request $request
|
||||
* @return Response
|
||||
* @throws UserRegistrationException
|
||||
* @throws \Illuminate\Foundation\Validation\ValidationException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function postRegister(Request $request)
|
||||
{
|
||||
|
@ -230,7 +231,6 @@ class RegisterController extends Controller
|
|||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
$this->emailConfirmationService->sendConfirmation($user);
|
||||
session()->flash('success', trans('auth.email_confirm_resent'));
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
@ -255,16 +255,13 @@ class RegisterController extends Controller
|
|||
*/
|
||||
public function socialCallback($socialDriver)
|
||||
{
|
||||
if (session()->has('social-callback')) {
|
||||
$action = session()->pull('social-callback');
|
||||
if ($action == 'login') {
|
||||
return $this->socialAuthService->handleLoginCallback($socialDriver);
|
||||
} elseif ($action == 'register') {
|
||||
return $this->socialRegisterCallback($socialDriver);
|
||||
}
|
||||
} else {
|
||||
if (!session()->has('social-callback')) {
|
||||
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
|
||||
}
|
||||
|
||||
$action = session()->pull('social-callback');
|
||||
if ($action == 'login') return $this->socialAuthService->handleLoginCallback($socialDriver);
|
||||
if ($action == 'register') return $this->socialRegisterCallback($socialDriver);
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
|
|
|
@ -36,11 +36,17 @@ class BookController extends Controller
|
|||
*/
|
||||
public function index()
|
||||
{
|
||||
$books = $this->entityRepo->getAllPaginated('book', 10);
|
||||
$books = $this->entityRepo->getAllPaginated('book', 20);
|
||||
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
|
||||
$popular = $this->entityRepo->getPopular('book', 4, 0);
|
||||
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
|
||||
$this->setPageTitle('Books');
|
||||
return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]);
|
||||
return view('books/index', [
|
||||
'books' => $books,
|
||||
'recents' => $recents,
|
||||
'popular' => $popular,
|
||||
'new' => $new
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,7 +90,12 @@ class BookController extends Controller
|
|||
$bookChildren = $this->entityRepo->getBookChildren($book);
|
||||
Views::add($book);
|
||||
$this->setPageTitle($book->getShortName());
|
||||
return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
|
||||
return view('books/show', [
|
||||
'book' => $book,
|
||||
'current' => $book,
|
||||
'bookChildren' => $bookChildren,
|
||||
'activity' => Activity::entityActivity($book, 20, 0)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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')]);
|
||||
}
|
||||
}
|
|
@ -29,15 +29,25 @@ class HomeController extends Controller
|
|||
$activity = Activity::latest(10);
|
||||
$draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
|
||||
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor);
|
||||
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5);
|
||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5);
|
||||
return view('home', [
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
|
||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
|
||||
|
||||
// Custom homepage
|
||||
$customHomepage = false;
|
||||
$homepageSetting = setting('app-homepage');
|
||||
if ($homepageSetting) {
|
||||
$id = intval(explode(':', $homepageSetting)[0]);
|
||||
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
|
||||
$this->entityRepo->renderPage($customHomepage, true);
|
||||
}
|
||||
|
||||
$view = $customHomepage ? 'home-custom' : 'home';
|
||||
return view($view, [
|
||||
'activity' => $activity,
|
||||
'recents' => $recents,
|
||||
'recentlyCreatedPages' => $recentlyCreatedPages,
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||
'draftPages' => $draftPages
|
||||
'draftPages' => $draftPages,
|
||||
'customHomepage' => $customHomepage
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -161,13 +161,14 @@ class PageController extends Controller
|
|||
$pageContent = $this->entityRepo->renderPage($page);
|
||||
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
|
||||
$pageNav = $this->entityRepo->getPageNav($pageContent);
|
||||
$page->load(['comments.createdBy']);
|
||||
|
||||
Views::add($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
return view('pages/show', [
|
||||
'page' => $page,'book' => $page->book,
|
||||
'current' => $page, 'sidebarTree' => $sidebarTree,
|
||||
'pageNav' => $pageNav, 'pageContent' => $pageContent]);
|
||||
'pageNav' => $pageNav]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -380,6 +381,7 @@ class PageController extends Controller
|
|||
return view('pages/revision', [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'revision' => $revision
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -409,6 +411,7 @@ class PageController extends Controller
|
|||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'diff' => $diff,
|
||||
'revision' => $revision
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -47,4 +47,16 @@ class PageRevision extends Model
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows checking of the exact class, Used to check entity type.
|
||||
* Included here to align with entities in similar use cases.
|
||||
* (Yup, Bit of an awkward hack)
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isA($type)
|
||||
{
|
||||
return $type === 'revision';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -137,10 +137,15 @@ class EntityRepo
|
|||
* @param string $type
|
||||
* @param integer $id
|
||||
* @param bool $allowDrafts
|
||||
* @param bool $ignorePermissions
|
||||
* @return Entity
|
||||
*/
|
||||
public function getById($type, $id, $allowDrafts = false)
|
||||
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
|
||||
{
|
||||
if ($ignorePermissions) {
|
||||
$entity = $this->getEntity($type);
|
||||
return $entity->newQuery()->find($id);
|
||||
}
|
||||
return $this->entityQuery($type, $allowDrafts)->find($id);
|
||||
}
|
||||
|
||||
|
@ -671,9 +676,10 @@ class EntityRepo
|
|||
/**
|
||||
* Render the page for viewing, Parsing and performing features such as page transclusion.
|
||||
* @param Page $page
|
||||
* @param bool $ignorePermissions
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function renderPage(Page $page)
|
||||
public function renderPage(Page $page, $ignorePermissions = false)
|
||||
{
|
||||
$content = $page->html;
|
||||
$matches = [];
|
||||
|
@ -685,19 +691,19 @@ class EntityRepo
|
|||
$pageId = intval($splitInclude[0]);
|
||||
if (is_nan($pageId)) continue;
|
||||
|
||||
$page = $this->getById('page', $pageId);
|
||||
if ($page === null) {
|
||||
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
|
||||
if ($matchedPage === null) {
|
||||
$content = str_replace($matches[0][$index], '', $content);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($splitInclude) === 1) {
|
||||
$content = str_replace($matches[0][$index], $page->html, $content);
|
||||
$content = str_replace($matches[0][$index], $matchedPage->html, $content);
|
||||
continue;
|
||||
}
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
||||
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
||||
$matchingElem = $doc->getElementById($splitInclude[1]);
|
||||
if ($matchingElem === null) {
|
||||
$content = str_replace($matches[0][$index], '', $content);
|
||||
|
@ -710,6 +716,7 @@ class EntityRepo
|
|||
$content = str_replace($matches[0][$index], trim($innerContent), $content);
|
||||
}
|
||||
|
||||
$page->setAttribute('renderedHTML', $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ class TagRepo
|
|||
* @param $entityType
|
||||
* @param $entityId
|
||||
* @param string $action
|
||||
* @return \Illuminate\Database\Eloquent\Model|null|static
|
||||
*/
|
||||
public function getEntity($entityType, $entityId, $action = 'view')
|
||||
{
|
||||
|
|
|
@ -27,9 +27,9 @@ class ExportService
|
|||
*/
|
||||
public function pageToContainedHtml(Page $page)
|
||||
{
|
||||
$this->entityRepo->renderPage($page);
|
||||
$pageHtml = view('pages/export', [
|
||||
'page' => $page,
|
||||
'pageContent' => $this->entityRepo->renderPage($page)
|
||||
'page' => $page
|
||||
])->render();
|
||||
return $this->containHtml($pageHtml);
|
||||
}
|
||||
|
@ -74,9 +74,9 @@ class ExportService
|
|||
*/
|
||||
public function pageToPdf(Page $page)
|
||||
{
|
||||
$this->entityRepo->renderPage($page);
|
||||
$html = view('pages/pdf', [
|
||||
'page' => $page,
|
||||
'pageContent' => $this->entityRepo->renderPage($page)
|
||||
'page' => $page
|
||||
])->render();
|
||||
return $this->htmlToPdf($html);
|
||||
}
|
||||
|
|
|
@ -468,7 +468,7 @@ class PermissionService
|
|||
$action = end($explodedPermission);
|
||||
$this->currentAction = $action;
|
||||
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment'];
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
|
||||
|
||||
// Handle non entity specific jointPermissions
|
||||
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
||||
|
|
|
@ -92,7 +92,7 @@ class SearchService
|
|||
return [
|
||||
'total' => $total,
|
||||
'count' => count($results),
|
||||
'results' => $results->sortByDesc('score')
|
||||
'results' => $results->sortByDesc('score')->values()
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ class ViewService
|
|||
$query->whereIn('viewable_type', $filterModel);
|
||||
} else if ($filterModel) {
|
||||
$query->where('viewable_type', '=', get_class($filterModel));
|
||||
};
|
||||
}
|
||||
|
||||
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ return [
|
|||
*/
|
||||
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl'],
|
||||
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl', 'it'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -71,3 +71,13 @@ $factory->define(BookStack\Image::class, function ($faker) {
|
|||
'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
|
||||
];
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,6 @@ class DummyContentSeeder extends Seeder
|
|||
$role = \BookStack\Role::getRole('editor');
|
||||
$user->attachRole($role);
|
||||
|
||||
|
||||
factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
|
||||
->each(function($book) use ($user) {
|
||||
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
|
||||
|
@ -33,7 +32,6 @@ class DummyContentSeeder extends Seeder
|
|||
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
|
||||
$largeBook->pages()->saveMany($pages);
|
||||
$largeBook->chapters()->saveMany($chapters);
|
||||
|
||||
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
|
||||
app(\BookStack\Services\SearchService::class)->indexAllEntities();
|
||||
}
|
||||
|
|
10
gulpfile.js
10
gulpfile.js
|
@ -1,16 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
const argv = require('yargs').argv;
|
||||
const gulp = require('gulp'),
|
||||
plumber = require('gulp-plumber');
|
||||
|
||||
const autoprefixer = require('gulp-autoprefixer');
|
||||
const uglify = require('gulp-uglify');
|
||||
const minifycss = require('gulp-clean-css');
|
||||
const sass = require('gulp-sass');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
|
||||
const browserify = require("browserify");
|
||||
const source = require('vinyl-source-stream');
|
||||
const buffer = require('vinyl-buffer');
|
||||
const babelify = require("babelify");
|
||||
const watchify = require("watchify");
|
||||
const envify = require("envify");
|
||||
const uglify = require('gulp-uglify');
|
||||
|
||||
const gutil = require("gulp-util");
|
||||
const liveReload = require('gulp-livereload');
|
||||
|
||||
|
@ -19,6 +25,7 @@ let isProduction = argv.production || process.env.NODE_ENV === 'production';
|
|||
|
||||
gulp.task('styles', () => {
|
||||
let chain = gulp.src(['resources/assets/sass/**/*.scss'])
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(plumber({
|
||||
errorHandler: function (error) {
|
||||
console.log(error.message);
|
||||
|
@ -27,6 +34,7 @@ gulp.task('styles', () => {
|
|||
.pipe(sass())
|
||||
.pipe(autoprefixer('last 2 versions'));
|
||||
if (isProduction) chain = chain.pipe(minifycss());
|
||||
chain = chain.pipe(sourcemaps.write());
|
||||
return chain.pipe(gulp.dest('public/css/')).pipe(liveReload());
|
||||
});
|
||||
|
||||
|
|
|
@ -31,15 +31,18 @@
|
|||
"angular-sanitize": "^1.5.5",
|
||||
"angular-ui-sortable": "^0.17.0",
|
||||
"axios": "^0.16.1",
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"clipboard": "^1.5.16",
|
||||
"clipboard": "^1.7.1",
|
||||
"codemirror": "^5.26.0",
|
||||
"dropzone": "^4.0.1",
|
||||
"gulp-sourcemaps": "^2.6.1",
|
||||
"gulp-util": "^3.0.8",
|
||||
"markdown-it": "^8.3.1",
|
||||
"markdown-it-task-lists": "^2.0.0",
|
||||
"moment": "^2.12.0",
|
||||
"vue": "^2.2.6"
|
||||
"vue": "^2.2.6",
|
||||
"vuedraggable": "^2.14.1"
|
||||
},
|
||||
"browser": {
|
||||
"vue": "vue/dist/vue.common.js"
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/logo.png
BIN
public/logo.png
Binary file not shown.
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 5.3 KiB |
12
readme.md
12
readme.md
|
@ -1,7 +1,7 @@
|
|||
# BookStack
|
||||
|
||||
[](https://github.com/BookStackApp/BookStack/releases/latest)
|
||||
[](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
|
||||
[](https://github.com/BookStackApp/BookStack/releases/latest)
|
||||
[](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
|
||||
[](https://travis-ci.org/BookStackApp/BookStack)
|
||||
|
||||
A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
|
||||
|
@ -13,6 +13,12 @@ A platform for storing and organising information and documentation. General inf
|
|||
* *Password: `password`*
|
||||
* [BookStack Blog](https://www.bookstackapp.com/blog)
|
||||
|
||||
## Project Definition
|
||||
|
||||
BookStack is an opinionated wiki system that provides a pleasant and simple out of the box experience. New users to an instance should find the experience intuitive and only basic word-processing skills should be required to get involved in creating content on BookStack. The platform should provide advanced power features to those that desire it but they should not interfere with the core simple user experience.
|
||||
|
||||
BookStack is not designed as an extensible platform to be used for purposes that differ to the statement above.
|
||||
|
||||
## Development & Testing
|
||||
|
||||
All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements:
|
||||
|
@ -79,7 +85,7 @@ These are the great open-source projects used to help build BookStack:
|
|||
* [jQuery Sortable](https://johnny.github.io/jquery-sortable/)
|
||||
* [Material Design Iconic Font](http://zavoloklom.github.io/material-design-iconic-font/icons.html)
|
||||
* [Dropzone.js](http://www.dropzonejs.com/)
|
||||
* [ZeroClipboard](http://zeroclipboard.org/)
|
||||
* [clipboard.js](https://clipboardjs.com/)
|
||||
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
|
||||
* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
|
||||
* [Moment.js](http://momentjs.com/)
|
||||
|
|
|
@ -53,13 +53,20 @@ const modeMap = {
|
|||
yml: 'yaml',
|
||||
};
|
||||
|
||||
module.exports.highlight = function() {
|
||||
let codeBlocks = document.querySelectorAll('.page-content pre');
|
||||
/**
|
||||
* Highlight pre elements on a page
|
||||
*/
|
||||
function highlight() {
|
||||
let codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre');
|
||||
for (let i = 0; i < codeBlocks.length; i++) {
|
||||
highlightElem(codeBlocks[i]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add code highlighting to a single element.
|
||||
* @param {HTMLElement} elem
|
||||
*/
|
||||
function highlightElem(elem) {
|
||||
let innerCodeElem = elem.querySelector('code[class^=language-]');
|
||||
let mode = '';
|
||||
|
@ -68,7 +75,7 @@ function highlightElem(elem) {
|
|||
mode = getMode(langName);
|
||||
}
|
||||
elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
|
||||
let content = elem.textContent;
|
||||
let content = elem.textContent.trim();
|
||||
|
||||
CodeMirror(function(elt) {
|
||||
elem.parentNode.replaceChild(elt, elem);
|
||||
|
@ -76,7 +83,7 @@ function highlightElem(elem) {
|
|||
value: content,
|
||||
mode: mode,
|
||||
lineNumbers: true,
|
||||
theme: 'base16-light',
|
||||
theme: getTheme(),
|
||||
readOnly: true
|
||||
});
|
||||
}
|
||||
|
@ -91,9 +98,21 @@ function getMode(suggestion) {
|
|||
return (typeof modeMap[suggestion] !== 'undefined') ? modeMap[suggestion] : '';
|
||||
}
|
||||
|
||||
module.exports.highlightElem = highlightElem;
|
||||
/**
|
||||
* Ge the theme to use for CodeMirror instances.
|
||||
* @returns {*|string}
|
||||
*/
|
||||
function getTheme() {
|
||||
return window.codeTheme || 'base16-light';
|
||||
}
|
||||
|
||||
module.exports.wysiwygView = function(elem) {
|
||||
/**
|
||||
* Create a CodeMirror instance for showing inside the WYSIWYG editor.
|
||||
* Manages a textarea element to hold code content.
|
||||
* @param {HTMLElement} elem
|
||||
* @returns {{wrap: Element, editor: *}}
|
||||
*/
|
||||
function wysiwygView(elem) {
|
||||
let doc = elem.ownerDocument;
|
||||
let codeElem = elem.querySelector('code');
|
||||
|
||||
|
@ -122,16 +141,22 @@ module.exports.wysiwygView = function(elem) {
|
|||
value: content,
|
||||
mode: getMode(lang),
|
||||
lineNumbers: true,
|
||||
theme: 'base16-light',
|
||||
theme: getTheme(),
|
||||
readOnly: true
|
||||
});
|
||||
setTimeout(() => {
|
||||
cm.refresh();
|
||||
}, 300);
|
||||
return {wrap: newWrap, editor: cm};
|
||||
};
|
||||
}
|
||||
|
||||
module.exports.popupEditor = function(elem, modeSuggestion) {
|
||||
/**
|
||||
* Create a CodeMirror instance to show in the WYSIWYG pop-up editor
|
||||
* @param {HTMLElement} elem
|
||||
* @param {String} modeSuggestion
|
||||
* @returns {*}
|
||||
*/
|
||||
function popupEditor(elem, modeSuggestion) {
|
||||
let content = elem.textContent;
|
||||
|
||||
return CodeMirror(function(elt) {
|
||||
|
@ -141,22 +166,38 @@ module.exports.popupEditor = function(elem, modeSuggestion) {
|
|||
value: content,
|
||||
mode: getMode(modeSuggestion),
|
||||
lineNumbers: true,
|
||||
theme: 'base16-light',
|
||||
theme: getTheme(),
|
||||
lineWrapping: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports.setMode = function(cmInstance, modeSuggestion) {
|
||||
/**
|
||||
* Set the mode of a codemirror instance.
|
||||
* @param cmInstance
|
||||
* @param modeSuggestion
|
||||
*/
|
||||
function setMode(cmInstance, modeSuggestion) {
|
||||
cmInstance.setOption('mode', getMode(modeSuggestion));
|
||||
};
|
||||
module.exports.setContent = function(cmInstance, codeContent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the content of a cm instance.
|
||||
* @param cmInstance
|
||||
* @param codeContent
|
||||
*/
|
||||
function setContent(cmInstance, codeContent) {
|
||||
cmInstance.setValue(codeContent);
|
||||
setTimeout(() => {
|
||||
cmInstance.refresh();
|
||||
}, 10);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports.markdownEditor = function(elem) {
|
||||
/**
|
||||
* Get a CodeMirror instace to use for the markdown editor.
|
||||
* @param {HTMLElement} elem
|
||||
* @returns {*}
|
||||
*/
|
||||
function markdownEditor(elem) {
|
||||
let content = elem.textContent;
|
||||
|
||||
return CodeMirror(function (elt) {
|
||||
|
@ -166,13 +207,27 @@ module.exports.markdownEditor = function(elem) {
|
|||
value: content,
|
||||
mode: "markdown",
|
||||
lineNumbers: true,
|
||||
theme: 'base16-light',
|
||||
theme: getTheme(),
|
||||
lineWrapping: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports.getMetaKey = function() {
|
||||
/**
|
||||
* Get the 'meta' key dependant on the user's system.
|
||||
* @returns {string}
|
||||
*/
|
||||
function getMetaKey() {
|
||||
let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
|
||||
return mac ? "Cmd" : "Ctrl";
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
highlight: highlight,
|
||||
highlightElem: highlightElem,
|
||||
wysiwygView: wysiwygView,
|
||||
popupEditor: popupEditor,
|
||||
setMode: setMode,
|
||||
setContent: setContent,
|
||||
markdownEditor: markdownEditor,
|
||||
getMetaKey: getMetaKey,
|
||||
};
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -8,256 +8,6 @@ moment.locale('en-gb');
|
|||
|
||||
module.exports = function (ngApp, events) {
|
||||
|
||||
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
|
||||
function ($scope, $attrs, $http, $timeout, imageManagerService) {
|
||||
|
||||
$scope.images = [];
|
||||
$scope.imageType = $attrs.imageType;
|
||||
$scope.selectedImage = false;
|
||||
$scope.dependantPages = false;
|
||||
$scope.showing = false;
|
||||
$scope.hasMore = false;
|
||||
$scope.imageUpdateSuccess = false;
|
||||
$scope.imageDeleteSuccess = false;
|
||||
$scope.uploadedTo = $attrs.uploadedTo;
|
||||
$scope.view = 'all';
|
||||
|
||||
$scope.searching = false;
|
||||
$scope.searchTerm = '';
|
||||
|
||||
let page = 0;
|
||||
let previousClickTime = 0;
|
||||
let previousClickImage = 0;
|
||||
let dataLoaded = false;
|
||||
let callback = false;
|
||||
|
||||
let preSearchImages = [];
|
||||
let preSearchHasMore = false;
|
||||
|
||||
/**
|
||||
* Used by dropzone to get the endpoint to upload to.
|
||||
* @returns {string}
|
||||
*/
|
||||
$scope.getUploadUrl = function () {
|
||||
return window.baseUrl('/images/' + $scope.imageType + '/upload');
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel the current search operation.
|
||||
*/
|
||||
function cancelSearch() {
|
||||
$scope.searching = false;
|
||||
$scope.searchTerm = '';
|
||||
$scope.images = preSearchImages;
|
||||
$scope.hasMore = preSearchHasMore;
|
||||
}
|
||||
$scope.cancelSearch = cancelSearch;
|
||||
|
||||
|
||||
/**
|
||||
* Runs on image upload, Adds an image to local list of images
|
||||
* and shows a success message to the user.
|
||||
* @param file
|
||||
* @param data
|
||||
*/
|
||||
$scope.uploadSuccess = function (file, data) {
|
||||
$scope.$apply(() => {
|
||||
$scope.images.unshift(data);
|
||||
});
|
||||
events.emit('success', trans('components.image_upload_success'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs the callback and hides the image manager.
|
||||
* @param returnData
|
||||
*/
|
||||
function callbackAndHide(returnData) {
|
||||
if (callback) callback(returnData);
|
||||
$scope.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Image select action. Checks if a double-click was fired.
|
||||
* @param image
|
||||
*/
|
||||
$scope.imageSelect = function (image) {
|
||||
let dblClickTime = 300;
|
||||
let currentTime = Date.now();
|
||||
let timeDiff = currentTime - previousClickTime;
|
||||
|
||||
if (timeDiff < dblClickTime && image.id === previousClickImage) {
|
||||
// If double click
|
||||
callbackAndHide(image);
|
||||
} else {
|
||||
// If single
|
||||
$scope.selectedImage = image;
|
||||
$scope.dependantPages = false;
|
||||
}
|
||||
previousClickTime = currentTime;
|
||||
previousClickImage = image.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Action that runs when the 'Select image' button is clicked.
|
||||
* Runs the callback and hides the image manager.
|
||||
*/
|
||||
$scope.selectButtonClick = function () {
|
||||
callbackAndHide($scope.selectedImage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the image manager.
|
||||
* Takes a callback to execute later on.
|
||||
* @param doneCallback
|
||||
*/
|
||||
function show(doneCallback) {
|
||||
callback = doneCallback;
|
||||
$scope.showing = true;
|
||||
$('#image-manager').find('.overlay').css('display', 'flex').hide().fadeIn(240);
|
||||
// Get initial images if they have not yet been loaded in.
|
||||
if (!dataLoaded) {
|
||||
fetchData();
|
||||
dataLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Connects up the image manger so it can be used externally
|
||||
// such as from TinyMCE.
|
||||
imageManagerService.show = show;
|
||||
imageManagerService.showExternal = function (doneCallback) {
|
||||
$scope.$apply(() => {
|
||||
show(doneCallback);
|
||||
});
|
||||
};
|
||||
window.ImageManager = imageManagerService;
|
||||
|
||||
/**
|
||||
* Hide the image manager
|
||||
*/
|
||||
$scope.hide = function () {
|
||||
$scope.showing = false;
|
||||
$('#image-manager').find('.overlay').fadeOut(240);
|
||||
};
|
||||
|
||||
let baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');
|
||||
|
||||
/**
|
||||
* Fetch the list image data from the server.
|
||||
*/
|
||||
function fetchData() {
|
||||
let url = baseUrl + page + '?';
|
||||
let components = {};
|
||||
if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo;
|
||||
if ($scope.searching) components['term'] = $scope.searchTerm;
|
||||
|
||||
|
||||
url += Object.keys(components).map((key) => {
|
||||
return key + '=' + encodeURIComponent(components[key]);
|
||||
}).join('&');
|
||||
|
||||
$http.get(url).then((response) => {
|
||||
$scope.images = $scope.images.concat(response.data.images);
|
||||
$scope.hasMore = response.data.hasMore;
|
||||
page++;
|
||||
});
|
||||
}
|
||||
$scope.fetchData = fetchData;
|
||||
|
||||
/**
|
||||
* Start a search operation
|
||||
*/
|
||||
$scope.searchImages = function() {
|
||||
|
||||
if ($scope.searchTerm === '') {
|
||||
cancelSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$scope.searching) {
|
||||
preSearchImages = $scope.images;
|
||||
preSearchHasMore = $scope.hasMore;
|
||||
}
|
||||
|
||||
$scope.searching = true;
|
||||
$scope.images = [];
|
||||
$scope.hasMore = false;
|
||||
page = 0;
|
||||
baseUrl = window.baseUrl('/images/' + $scope.imageType + '/search/');
|
||||
fetchData();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the current image listing view.
|
||||
* @param viewName
|
||||
*/
|
||||
$scope.setView = function(viewName) {
|
||||
cancelSearch();
|
||||
$scope.images = [];
|
||||
$scope.hasMore = false;
|
||||
page = 0;
|
||||
$scope.view = viewName;
|
||||
baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/');
|
||||
fetchData();
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the details of an image.
|
||||
* @param event
|
||||
*/
|
||||
$scope.saveImageDetails = function (event) {
|
||||
event.preventDefault();
|
||||
let url = window.baseUrl('/images/update/' + $scope.selectedImage.id);
|
||||
$http.put(url, this.selectedImage).then(response => {
|
||||
events.emit('success', trans('components.image_update_success'));
|
||||
}, (response) => {
|
||||
if (response.status === 422) {
|
||||
let errors = response.data;
|
||||
let message = '';
|
||||
Object.keys(errors).forEach((key) => {
|
||||
message += errors[key].join('\n');
|
||||
});
|
||||
events.emit('error', message);
|
||||
} else if (response.status === 403) {
|
||||
events.emit('error', response.data.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete an image from system and notify of success.
|
||||
* Checks if it should force delete when an image
|
||||
* has dependant pages.
|
||||
* @param event
|
||||
*/
|
||||
$scope.deleteImage = function (event) {
|
||||
event.preventDefault();
|
||||
let force = $scope.dependantPages !== false;
|
||||
let url = window.baseUrl('/images/' + $scope.selectedImage.id);
|
||||
if (force) url += '?force=true';
|
||||
$http.delete(url).then((response) => {
|
||||
$scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
|
||||
$scope.selectedImage = false;
|
||||
events.emit('success', trans('components.image_delete_success'));
|
||||
}, (response) => {
|
||||
// Pages failure
|
||||
if (response.status === 400) {
|
||||
$scope.dependantPages = response.data;
|
||||
} else if (response.status === 403) {
|
||||
events.emit('error', response.data.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple date creator used to properly format dates.
|
||||
* @param stringDate
|
||||
* @returns {Date}
|
||||
*/
|
||||
$scope.getDate = function (stringDate) {
|
||||
return new Date(stringDate);
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
|
||||
function ($scope, $http, $attrs, $interval, $timeout, $sce) {
|
||||
|
@ -394,285 +144,4 @@ module.exports = function (ngApp, events) {
|
|||
};
|
||||
|
||||
}]);
|
||||
|
||||
ngApp.controller('PageTagController', ['$scope', '$http', '$attrs',
|
||||
function ($scope, $http, $attrs) {
|
||||
|
||||
const pageId = Number($attrs.pageId);
|
||||
$scope.tags = [];
|
||||
|
||||
$scope.sortOptions = {
|
||||
handle: '.handle',
|
||||
items: '> tr',
|
||||
containment: "parent",
|
||||
axis: "y"
|
||||
};
|
||||
|
||||
/**
|
||||
* Push an empty tag to the end of the scope tags.
|
||||
*/
|
||||
function addEmptyTag() {
|
||||
$scope.tags.push({
|
||||
name: '',
|
||||
value: ''
|
||||
});
|
||||
}
|
||||
$scope.addEmptyTag = addEmptyTag;
|
||||
|
||||
/**
|
||||
* Get all tags for the current book and add into scope.
|
||||
*/
|
||||
function getTags() {
|
||||
let url = window.baseUrl(`/ajax/tags/get/page/${pageId}`);
|
||||
$http.get(url).then((responseData) => {
|
||||
$scope.tags = responseData.data;
|
||||
addEmptyTag();
|
||||
});
|
||||
}
|
||||
getTags();
|
||||
|
||||
/**
|
||||
* Set the order property on all tags.
|
||||
*/
|
||||
function setTagOrder() {
|
||||
for (let i = 0; i < $scope.tags.length; i++) {
|
||||
$scope.tags[i].order = i;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When an tag changes check if another empty editable
|
||||
* field needs to be added onto the end.
|
||||
* @param tag
|
||||
*/
|
||||
$scope.tagChange = function(tag) {
|
||||
let cPos = $scope.tags.indexOf(tag);
|
||||
if (cPos !== $scope.tags.length-1) return;
|
||||
|
||||
if (tag.name !== '' || tag.value !== '') {
|
||||
addEmptyTag();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When an tag field loses focus check the tag to see if its
|
||||
* empty and therefore could be removed from the list.
|
||||
* @param tag
|
||||
*/
|
||||
$scope.tagBlur = function(tag) {
|
||||
let isLast = $scope.tags.length - 1 === $scope.tags.indexOf(tag);
|
||||
if (tag.name === '' && tag.value === '' && !isLast) {
|
||||
let cPos = $scope.tags.indexOf(tag);
|
||||
$scope.tags.splice(cPos, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a tag from the current list.
|
||||
* @param tag
|
||||
*/
|
||||
$scope.removeTag = function(tag) {
|
||||
let cIndex = $scope.tags.indexOf(tag);
|
||||
$scope.tags.splice(cIndex, 1);
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
|
||||
ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs',
|
||||
function ($scope, $http, $attrs) {
|
||||
|
||||
const pageId = $scope.uploadedTo = $attrs.pageId;
|
||||
let currentOrder = '';
|
||||
$scope.files = [];
|
||||
$scope.editFile = false;
|
||||
$scope.file = getCleanFile();
|
||||
$scope.errors = {
|
||||
link: {},
|
||||
edit: {}
|
||||
};
|
||||
|
||||
function getCleanFile() {
|
||||
return {
|
||||
page_id: pageId
|
||||
};
|
||||
}
|
||||
|
||||
// Angular-UI-Sort options
|
||||
$scope.sortOptions = {
|
||||
handle: '.handle',
|
||||
items: '> tr',
|
||||
containment: "parent",
|
||||
axis: "y",
|
||||
stop: sortUpdate,
|
||||
};
|
||||
|
||||
/**
|
||||
* Event listener for sort changes.
|
||||
* Updates the file ordering on the server.
|
||||
* @param event
|
||||
* @param ui
|
||||
*/
|
||||
function sortUpdate(event, ui) {
|
||||
let newOrder = $scope.files.map(file => {return file.id}).join(':');
|
||||
if (newOrder === currentOrder) return;
|
||||
|
||||
currentOrder = newOrder;
|
||||
$http.put(window.baseUrl(`/attachments/sort/page/${pageId}`), {files: $scope.files}).then(resp => {
|
||||
events.emit('success', resp.data.message);
|
||||
}, checkError('sort'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by dropzone to get the endpoint to upload to.
|
||||
* @returns {string}
|
||||
*/
|
||||
$scope.getUploadUrl = function (file) {
|
||||
let suffix = (typeof file !== 'undefined') ? `/${file.id}` : '';
|
||||
return window.baseUrl(`/attachments/upload${suffix}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get files for the current page from the server.
|
||||
*/
|
||||
function getFiles() {
|
||||
let url = window.baseUrl(`/attachments/get/page/${pageId}`);
|
||||
$http.get(url).then(resp => {
|
||||
$scope.files = resp.data;
|
||||
currentOrder = resp.data.map(file => {return file.id}).join(':');
|
||||
}, checkError('get'));
|
||||
}
|
||||
getFiles();
|
||||
|
||||
/**
|
||||
* Runs on file upload, Adds an file to local file list
|
||||
* and shows a success message to the user.
|
||||
* @param file
|
||||
* @param data
|
||||
*/
|
||||
$scope.uploadSuccess = function (file, data) {
|
||||
$scope.$apply(() => {
|
||||
$scope.files.push(data);
|
||||
});
|
||||
events.emit('success', trans('entities.attachments_file_uploaded'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload and overwrite an existing file.
|
||||
* @param file
|
||||
* @param data
|
||||
*/
|
||||
$scope.uploadSuccessUpdate = function (file, data) {
|
||||
$scope.$apply(() => {
|
||||
let search = filesIndexOf(data);
|
||||
if (search !== -1) $scope.files[search] = data;
|
||||
|
||||
if ($scope.editFile) {
|
||||
$scope.editFile = angular.copy(data);
|
||||
data.link = '';
|
||||
}
|
||||
});
|
||||
events.emit('success', trans('entities.attachments_file_updated'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a file from the server and, on success, the local listing.
|
||||
* @param file
|
||||
*/
|
||||
$scope.deleteFile = function(file) {
|
||||
if (!file.deleting) {
|
||||
file.deleting = true;
|
||||
return;
|
||||
}
|
||||
$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
|
||||
events.emit('success', resp.data.message);
|
||||
$scope.files.splice($scope.files.indexOf(file), 1);
|
||||
}, checkError('delete'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach a link to a page.
|
||||
* @param file
|
||||
*/
|
||||
$scope.attachLinkSubmit = function(file) {
|
||||
file.uploaded_to = pageId;
|
||||
$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
|
||||
$scope.files.push(resp.data);
|
||||
events.emit('success', trans('entities.attachments_link_attached'));
|
||||
$scope.file = getCleanFile();
|
||||
}, checkError('link'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the edit mode for a file.
|
||||
* @param file
|
||||
*/
|
||||
$scope.startEdit = function(file) {
|
||||
$scope.editFile = angular.copy(file);
|
||||
$scope.editFile.link = (file.external) ? file.path : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel edit mode
|
||||
*/
|
||||
$scope.cancelEdit = function() {
|
||||
$scope.editFile = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the name and link of a file.
|
||||
* @param file
|
||||
*/
|
||||
$scope.updateFile = function(file) {
|
||||
$http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
|
||||
let search = filesIndexOf(resp.data);
|
||||
if (search !== -1) $scope.files[search] = resp.data;
|
||||
|
||||
if ($scope.editFile && !file.external) {
|
||||
$scope.editFile.link = '';
|
||||
}
|
||||
$scope.editFile = false;
|
||||
events.emit('success', trans('entities.attachments_updated_success'));
|
||||
}, checkError('edit'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the url of a file.
|
||||
*/
|
||||
$scope.getFileUrl = function(file) {
|
||||
return window.baseUrl('/attachments/' + file.id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Search the local files via another file object.
|
||||
* Used to search via object copies.
|
||||
* @param file
|
||||
* @returns int
|
||||
*/
|
||||
function filesIndexOf(file) {
|
||||
for (let i = 0; i < $scope.files.length; i++) {
|
||||
if ($scope.files[i].id == file.id) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an error response in a ajax request.
|
||||
* @param errorGroupName
|
||||
*/
|
||||
function checkError(errorGroupName) {
|
||||
$scope.errors[errorGroupName] = {};
|
||||
return function(response) {
|
||||
if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') {
|
||||
events.emit('error', response.data.error);
|
||||
}
|
||||
if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') {
|
||||
$scope.errors[errorGroupName] = response.data.validation;
|
||||
console.log($scope.errors[errorGroupName])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}]);
|
||||
|
||||
};
|
||||
|
|
|
@ -1,158 +1,10 @@
|
|||
"use strict";
|
||||
const DropZone = require("dropzone");
|
||||
const MarkdownIt = require("markdown-it");
|
||||
const mdTasksLists = require('markdown-it-task-lists');
|
||||
const code = require('./code');
|
||||
|
||||
module.exports = function (ngApp, events) {
|
||||
|
||||
/**
|
||||
* Common tab controls using simple jQuery functions.
|
||||
*/
|
||||
ngApp.directive('tabContainer', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
const $content = element.find('[tab-content]');
|
||||
const $buttons = element.find('[tab-button]');
|
||||
|
||||
if (attrs.tabContainer) {
|
||||
let initial = attrs.tabContainer;
|
||||
$buttons.filter(`[tab-button="${initial}"]`).addClass('selected');
|
||||
$content.hide().filter(`[tab-content="${initial}"]`).show();
|
||||
} else {
|
||||
$content.hide().first().show();
|
||||
$buttons.first().addClass('selected');
|
||||
}
|
||||
|
||||
$buttons.click(function() {
|
||||
let clickedTab = $(this);
|
||||
$buttons.removeClass('selected');
|
||||
$content.hide();
|
||||
let name = clickedTab.addClass('selected').attr('tab-button');
|
||||
$content.filter(`[tab-content="${name}"]`).show();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Sub form component to allow inner-form sections to act like their own forms.
|
||||
*/
|
||||
ngApp.directive('subForm', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
element.on('keypress', e => {
|
||||
if (e.keyCode === 13) {
|
||||
submitEvent(e);
|
||||
}
|
||||
});
|
||||
|
||||
element.find('button[type="submit"]').click(submitEvent);
|
||||
|
||||
function submitEvent(e) {
|
||||
e.preventDefault();
|
||||
if (attrs.subForm) scope.$eval(attrs.subForm);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* DropZone
|
||||
* Used for uploading images
|
||||
*/
|
||||
ngApp.directive('dropZone', [function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: `
|
||||
<div class="dropzone-container">
|
||||
<div class="dz-message">{{message}}</div>
|
||||
</div>
|
||||
`,
|
||||
scope: {
|
||||
uploadUrl: '@',
|
||||
eventSuccess: '=',
|
||||
eventError: '=',
|
||||
uploadedTo: '@',
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
scope.message = attrs.message;
|
||||
if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
|
||||
let dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
|
||||
url: scope.uploadUrl,
|
||||
init: function () {
|
||||
let dz = this;
|
||||
dz.on('sending', function (file, xhr, data) {
|
||||
let token = window.document.querySelector('meta[name=token]').getAttribute('content');
|
||||
data.append('_token', token);
|
||||
let uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo;
|
||||
data.append('uploaded_to', uploadedTo);
|
||||
});
|
||||
if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess);
|
||||
dz.on('success', function (file, data) {
|
||||
$(file.previewElement).fadeOut(400, function () {
|
||||
dz.removeFile(file);
|
||||
});
|
||||
});
|
||||
if (typeof scope.eventError !== 'undefined') dz.on('error', scope.eventError);
|
||||
dz.on('error', function (file, errorMessage, xhr) {
|
||||
console.log(errorMessage);
|
||||
console.log(xhr);
|
||||
function setMessage(message) {
|
||||
$(file.previewElement).find('[data-dz-errormessage]').text(message);
|
||||
}
|
||||
|
||||
if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
|
||||
if (errorMessage.file) setMessage(errorMessage.file[0]);
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
/**
|
||||
* Dropdown
|
||||
* Provides some simple logic to create small dropdown menus
|
||||
*/
|
||||
ngApp.directive('dropdown', [function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
const menu = element.find('ul');
|
||||
|
||||
function hide() {
|
||||
menu.hide();
|
||||
menu.removeClass('anim menuIn');
|
||||
}
|
||||
|
||||
function show() {
|
||||
menu.show().addClass('anim menuIn');
|
||||
element.mouseleave(hide);
|
||||
|
||||
// Focus on input if exist in dropdown and hide on enter press
|
||||
let inputs = menu.find('input');
|
||||
if (inputs.length > 0) inputs.first().focus();
|
||||
}
|
||||
|
||||
// Hide menu on option click
|
||||
element.on('click', '> ul a', hide);
|
||||
// Show dropdown on toggle click.
|
||||
element.find('[dropdown-toggle]').on('click', show);
|
||||
// Hide menu on enter press in inputs
|
||||
element.on('keypress', 'input', event => {
|
||||
if (event.keyCode !== 13) return true;
|
||||
event.preventDefault();
|
||||
hide();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
/**
|
||||
* TinyMCE
|
||||
* An angular wrapper around the tinyMCE editor.
|
||||
|
@ -168,7 +20,7 @@ module.exports = function (ngApp, events) {
|
|||
link: function (scope, element, attrs) {
|
||||
|
||||
function tinyMceSetup(editor) {
|
||||
editor.on('ExecCommand change NodeChange ObjectResized', (e) => {
|
||||
editor.on('ExecCommand change input NodeChange ObjectResized', (e) => {
|
||||
let content = editor.getContent();
|
||||
$timeout(() => {
|
||||
scope.mceModel = content;
|
||||
|
@ -177,7 +29,10 @@ module.exports = function (ngApp, events) {
|
|||
});
|
||||
|
||||
editor.on('keydown', (event) => {
|
||||
scope.$emit('editor-keydown', event);
|
||||
if (event.keyCode === 83 && (navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
scope.$emit('save-draft', event);
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('init', (e) => {
|
||||
|
@ -247,7 +102,7 @@ module.exports = function (ngApp, events) {
|
|||
extraKeys[`${metaKey}-7`] = function(cm) {wrapSelection('\n```\n', '\n```');};
|
||||
extraKeys[`${metaKey}-8`] = function(cm) {wrapSelection('`', '`');};
|
||||
extraKeys[`Shift-${metaKey}-E`] = function(cm) {wrapSelection('`', '`');};
|
||||
extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('<p class="callout info">', '</div>');};
|
||||
extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('<p class="callout info">', '</p>');};
|
||||
cm.setOption('extraKeys', extraKeys);
|
||||
|
||||
// Update data on content change
|
||||
|
@ -341,12 +196,13 @@ module.exports = function (ngApp, events) {
|
|||
}
|
||||
|
||||
cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
|
||||
cm.setCursor({line: cursor.line, ch: cursor.ch + (newLineContent.length - lineLen)});
|
||||
cm.setCursor({line: cursor.line, ch: cursor.ch + start.length});
|
||||
}
|
||||
|
||||
function wrapSelection(start, end) {
|
||||
let selection = cm.getSelection();
|
||||
if (selection === '') return wrapLine(start, end);
|
||||
|
||||
let newSelection = selection;
|
||||
let frontDiff = 0;
|
||||
let endDiff = 0;
|
||||
|
@ -400,7 +256,7 @@ module.exports = function (ngApp, events) {
|
|||
// Show the popup link selector and insert a link when finished
|
||||
function showLinkSelector() {
|
||||
let cursorPos = cm.getCursor('from');
|
||||
window.showEntityLinkSelector(entity => {
|
||||
window.EntitySelectorPopup.show(entity => {
|
||||
let selectedText = cm.getSelection() || entity.name;
|
||||
let newText = `[${selectedText}](${entity.link})`;
|
||||
cm.focus();
|
||||
|
@ -422,7 +278,7 @@ module.exports = function (ngApp, events) {
|
|||
// Show the image manager and handle image insertion
|
||||
function showImageManager() {
|
||||
let cursorPos = cm.getCursor('from');
|
||||
window.ImageManager.showExternal(image => {
|
||||
window.ImageManager.show(image => {
|
||||
let selectedText = cm.getSelection();
|
||||
let newText = "";
|
||||
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;
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
"use strict";
|
||||
require("babel-polyfill");
|
||||
require('./dom-polyfills');
|
||||
|
||||
// Url retrieval function
|
||||
window.baseUrl = function(path) {
|
||||
|
@ -8,44 +10,15 @@ window.baseUrl = function(path) {
|
|||
return basePath + '/' + path;
|
||||
};
|
||||
|
||||
const Vue = require("vue");
|
||||
const axios = require("axios");
|
||||
|
||||
let axiosInstance = axios.create({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
|
||||
'baseURL': window.baseUrl('')
|
||||
}
|
||||
});
|
||||
window.$http = axiosInstance;
|
||||
|
||||
Vue.prototype.$http = axiosInstance;
|
||||
|
||||
require("./vues/vues");
|
||||
|
||||
|
||||
// AngularJS - Create application and load components
|
||||
const angular = require("angular");
|
||||
require("angular-resource");
|
||||
require("angular-animate");
|
||||
require("angular-sanitize");
|
||||
require("angular-ui-sortable");
|
||||
|
||||
let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
|
||||
|
||||
// Translation setup
|
||||
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
|
||||
const Translations = require("./translations");
|
||||
let translator = new Translations(window.translations);
|
||||
window.trans = translator.get.bind(translator);
|
||||
|
||||
// Global Event System
|
||||
class EventManager {
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
this.stack = [];
|
||||
}
|
||||
|
||||
emit(eventName, eventData) {
|
||||
this.stack.push({name: eventName, data: eventData});
|
||||
if (typeof this.listeners[eventName] === 'undefined') return this;
|
||||
let eventsToStart = this.listeners[eventName];
|
||||
for (let i = 0; i < eventsToStart.length; i++) {
|
||||
|
@ -62,25 +35,95 @@ class EventManager {
|
|||
}
|
||||
}
|
||||
|
||||
window.Events = new EventManager();
|
||||
Vue.prototype.$events = window.Events;
|
||||
window.$events = new EventManager();
|
||||
|
||||
const Vue = require("vue");
|
||||
const axios = require("axios");
|
||||
|
||||
let axiosInstance = axios.create({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
|
||||
'baseURL': window.baseUrl('')
|
||||
}
|
||||
});
|
||||
axiosInstance.interceptors.request.use(resp => {
|
||||
return resp;
|
||||
}, err => {
|
||||
if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err);
|
||||
if (typeof err.response.data.error !== "undefined") window.$events.emit('error', err.response.data.error);
|
||||
if (typeof err.response.data.message !== "undefined") window.$events.emit('error', err.response.data.message);
|
||||
});
|
||||
window.$http = axiosInstance;
|
||||
|
||||
Vue.prototype.$http = axiosInstance;
|
||||
Vue.prototype.$events = window.$events;
|
||||
|
||||
|
||||
// AngularJS - Create application and load components
|
||||
const angular = require("angular");
|
||||
require("angular-resource");
|
||||
require("angular-animate");
|
||||
require("angular-sanitize");
|
||||
require("angular-ui-sortable");
|
||||
|
||||
let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
|
||||
|
||||
// Translation setup
|
||||
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
|
||||
const Translations = require("./translations");
|
||||
let translator = new Translations(window.translations);
|
||||
window.trans = translator.get.bind(translator);
|
||||
window.trans_choice = translator.getPlural.bind(translator);
|
||||
|
||||
|
||||
require("./vues/vues");
|
||||
require("./components");
|
||||
|
||||
// Load in angular specific items
|
||||
const Services = require('./services');
|
||||
const Directives = require('./directives');
|
||||
const Controllers = require('./controllers');
|
||||
Services(ngApp, window.Events);
|
||||
Directives(ngApp, window.Events);
|
||||
Controllers(ngApp, window.Events);
|
||||
Directives(ngApp, window.$events);
|
||||
Controllers(ngApp, window.$events);
|
||||
|
||||
//Global jQuery Config & Extensions
|
||||
|
||||
/**
|
||||
* Scroll the view to a specific element.
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
window.scrollToElement = function(element) {
|
||||
if (!element) return;
|
||||
let offset = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
|
||||
let top = element.getBoundingClientRect().top + offset;
|
||||
$('html, body').animate({
|
||||
scrollTop: top - 60 // Adjust to change final scroll position top margin
|
||||
}, 300);
|
||||
};
|
||||
|
||||
/**
|
||||
* Scroll and highlight an element.
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
window.scrollAndHighlight = function(element) {
|
||||
if (!element) return;
|
||||
window.scrollToElement(element);
|
||||
let color = document.getElementById('custom-styles').getAttribute('data-color-light');
|
||||
let initColor = window.getComputedStyle(element).getPropertyValue('background-color');
|
||||
element.style.backgroundColor = color;
|
||||
setTimeout(() => {
|
||||
element.classList.add('selectFade');
|
||||
element.style.backgroundColor = initColor;
|
||||
}, 10);
|
||||
setTimeout(() => {
|
||||
element.classList.remove('selectFade');
|
||||
element.style.backgroundColor = '';
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// Smooth scrolling
|
||||
jQuery.fn.smoothScrollTo = function () {
|
||||
if (this.length === 0) return;
|
||||
$('html, body').animate({
|
||||
scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin
|
||||
}, 300); // Adjust to change animations speed (ms)
|
||||
window.scrollToElement(this[0]);
|
||||
return this;
|
||||
};
|
||||
|
||||
|
@ -91,83 +134,11 @@ jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
|
|||
};
|
||||
});
|
||||
|
||||
// Global jQuery Elements
|
||||
let notifications = $('.notification');
|
||||
let successNotification = notifications.filter('.pos');
|
||||
let errorNotification = notifications.filter('.neg');
|
||||
let warningNotification = notifications.filter('.warning');
|
||||
// Notification Events
|
||||
window.Events.listen('success', function (text) {
|
||||
successNotification.hide();
|
||||
successNotification.find('span').text(text);
|
||||
setTimeout(() => {
|
||||
successNotification.show();
|
||||
}, 1);
|
||||
});
|
||||
window.Events.listen('warning', function (text) {
|
||||
warningNotification.find('span').text(text);
|
||||
warningNotification.show();
|
||||
});
|
||||
window.Events.listen('error', function (text) {
|
||||
errorNotification.find('span').text(text);
|
||||
errorNotification.show();
|
||||
});
|
||||
|
||||
// Notification hiding
|
||||
notifications.click(function () {
|
||||
$(this).fadeOut(100);
|
||||
});
|
||||
|
||||
// Chapter page list toggles
|
||||
$('.chapter-toggle').click(function (e) {
|
||||
e.preventDefault();
|
||||
$(this).toggleClass('open');
|
||||
$(this).closest('.chapter').find('.inset-list').slideToggle(180);
|
||||
});
|
||||
|
||||
// Back to top button
|
||||
$('#back-to-top').click(function() {
|
||||
$('#header').smoothScrollTo();
|
||||
});
|
||||
let scrollTopShowing = false;
|
||||
let scrollTop = document.getElementById('back-to-top');
|
||||
let scrollTopBreakpoint = 1200;
|
||||
window.addEventListener('scroll', function() {
|
||||
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) {
|
||||
scrollTop.style.display = 'block';
|
||||
scrollTopShowing = true;
|
||||
setTimeout(() => {
|
||||
scrollTop.style.opacity = 0.4;
|
||||
}, 1);
|
||||
} else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) {
|
||||
scrollTop.style.opacity = 0;
|
||||
scrollTopShowing = false;
|
||||
setTimeout(() => {
|
||||
scrollTop.style.display = 'none';
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Common jQuery actions
|
||||
$('[data-action="expand-entity-list-details"]').click(function() {
|
||||
$('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
|
||||
});
|
||||
|
||||
// Popup close
|
||||
$('.popup-close').click(function() {
|
||||
$(this).closest('.overlay').fadeOut(240);
|
||||
});
|
||||
$('.overlay').click(function(event) {
|
||||
if (!$(event.target).hasClass('overlay')) return;
|
||||
$(this).fadeOut(240);
|
||||
});
|
||||
|
||||
// Detect IE for css
|
||||
if(navigator.userAgent.indexOf('MSIE')!==-1
|
||||
|| navigator.appVersion.indexOf('Trident/') > 0
|
||||
|| navigator.userAgent.indexOf('Safari') !== -1){
|
||||
$('body').addClass('flexbox-support');
|
||||
document.body.classList.add('flexbox-support');
|
||||
}
|
||||
|
||||
// Page specific items
|
||||
|
|
|
@ -274,7 +274,7 @@ module.exports = function() {
|
|||
file_browser_callback: function (field_name, url, type, win) {
|
||||
|
||||
if (type === 'file') {
|
||||
window.showEntityLinkSelector(function(entity) {
|
||||
window.EntitySelectorPopup.show(function(entity) {
|
||||
let originalField = win.document.getElementById(field_name);
|
||||
originalField.value = entity.link;
|
||||
$(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
|
||||
|
@ -283,7 +283,7 @@ module.exports = function() {
|
|||
|
||||
if (type === 'image') {
|
||||
// Show image manager
|
||||
window.ImageManager.showExternal(function (image) {
|
||||
window.ImageManager.show(function (image) {
|
||||
|
||||
// Set popover link input to image url then fire change event
|
||||
// to ensure the new value sticks
|
||||
|
@ -365,7 +365,7 @@ module.exports = function() {
|
|||
icon: 'image',
|
||||
tooltip: 'Insert an image',
|
||||
onclick: function () {
|
||||
window.ImageManager.showExternal(function (image) {
|
||||
window.ImageManager.show(function (image) {
|
||||
let html = `<a href="${image.url}" target="_blank">`;
|
||||
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
|
||||
html += '</a>';
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
"use strict";
|
||||
// Configure ZeroClipboard
|
||||
const Clipboard = require("clipboard");
|
||||
const Code = require('../code');
|
||||
|
||||
|
@ -83,15 +81,7 @@ let setupPageShow = window.setupPageShow = function (pageId) {
|
|||
let idElem = document.getElementById(text);
|
||||
$('.page-content [data-highlighted]').attr('data-highlighted', '').css('background-color', '');
|
||||
if (idElem !== null) {
|
||||
let $idElem = $(idElem);
|
||||
let color = $('#custom-styles').attr('data-color-light');
|
||||
$idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo();
|
||||
setTimeout(() => {
|
||||
$idElem.addClass('anim').addClass('selectFade').css('background-color', '');
|
||||
setTimeout(() => {
|
||||
$idElem.removeClass('selectFade');
|
||||
}, 3000);
|
||||
}, 100);
|
||||
window.scrollAndHighlight(idElem);
|
||||
} else {
|
||||
$('.page-content').find(':contains("' + text + '")').smoothScrollTo();
|
||||
}
|
||||
|
@ -108,25 +98,25 @@ let setupPageShow = window.setupPageShow = function (pageId) {
|
|||
goToText(event.target.getAttribute('href').substr(1));
|
||||
});
|
||||
|
||||
// Make the book-tree sidebar stick in view on scroll
|
||||
// Make the sidebar stick in view on scroll
|
||||
let $window = $(window);
|
||||
let $bookTree = $(".book-tree");
|
||||
let $bookTreeParent = $bookTree.parent();
|
||||
let $sidebar = $("#sidebar .scroll-body");
|
||||
let $bookTreeParent = $sidebar.parent();
|
||||
// Check the page is scrollable and the content is taller than the tree
|
||||
let pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height());
|
||||
let pageScrollable = ($(document).height() > $window.height()) && ($sidebar.height() < $('.page-content').height());
|
||||
// Get current tree's width and header height
|
||||
let headerHeight = $("#header").height() + $(".toolbar").height();
|
||||
let isFixed = $window.scrollTop() > headerHeight;
|
||||
// Function to fix the tree as a sidebar
|
||||
function stickTree() {
|
||||
$bookTree.width($bookTreeParent.width() + 15);
|
||||
$bookTree.addClass("fixed");
|
||||
$sidebar.width($bookTreeParent.width() + 15);
|
||||
$sidebar.addClass("fixed");
|
||||
isFixed = true;
|
||||
}
|
||||
// Function to un-fix the tree back into position
|
||||
function unstickTree() {
|
||||
$bookTree.css('width', 'auto');
|
||||
$bookTree.removeClass("fixed");
|
||||
$sidebar.css('width', 'auto');
|
||||
$sidebar.removeClass("fixed");
|
||||
isFixed = false;
|
||||
}
|
||||
// Checks if the tree stickiness state should change
|
||||
|
@ -160,7 +150,6 @@ let setupPageShow = window.setupPageShow = function (pageId) {
|
|||
unstickTree();
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports = setupPageShow;
|
|
@ -1,12 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = function(ngApp, events) {
|
||||
|
||||
ngApp.factory('imageManagerService', function() {
|
||||
return {
|
||||
show: false,
|
||||
showExternal: false
|
||||
};
|
||||
});
|
||||
|
||||
};
|
|
@ -20,9 +20,63 @@ class Translator {
|
|||
* @returns {*}
|
||||
*/
|
||||
get(key, replacements) {
|
||||
let text = this.getTransText(key);
|
||||
return this.performReplacements(text, replacements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pluralised text, Dependant on the given count.
|
||||
* Same format at laravel's 'trans_choice' helper.
|
||||
* @param key
|
||||
* @param count
|
||||
* @param replacements
|
||||
* @returns {*}
|
||||
*/
|
||||
getPlural(key, count, replacements) {
|
||||
let text = this.getTransText(key);
|
||||
let splitText = text.split('|');
|
||||
let result = null;
|
||||
let exactCountRegex = /^{([0-9]+)}/;
|
||||
let rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
|
||||
|
||||
for (let i = 0, len = splitText.length; i < len; i++) {
|
||||
let t = splitText[i];
|
||||
|
||||
// Parse exact matches
|
||||
let exactMatches = t.match(exactCountRegex);
|
||||
if (exactMatches !== null && Number(exactMatches[1]) === count) {
|
||||
result = t.replace(exactCountRegex, '').trim();
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse range matches
|
||||
let rangeMatches = t.match(rangeRegex);
|
||||
if (rangeMatches !== null) {
|
||||
let rangeStart = Number(rangeMatches[1]);
|
||||
if (rangeStart <= count && (rangeMatches[2] === '*' || Number(rangeMatches[2]) >= count)) {
|
||||
result = t.replace(rangeRegex, '').trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result === null && splitText.length > 1) {
|
||||
result = (count === 1) ? splitText[0] : splitText[1];
|
||||
}
|
||||
|
||||
if (result === null) result = splitText[0];
|
||||
return this.performReplacements(result, replacements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetched translation text from the store for the given key.
|
||||
* @param key
|
||||
* @returns {String|Object}
|
||||
*/
|
||||
getTransText(key) {
|
||||
let splitKey = key.split('.');
|
||||
let value = splitKey.reduce((a, b) => {
|
||||
return a != undefined ? a[b] : a;
|
||||
return a !== undefined ? a[b] : a;
|
||||
}, this.store);
|
||||
|
||||
if (value === undefined) {
|
||||
|
@ -30,16 +84,25 @@ class Translator {
|
|||
value = key;
|
||||
}
|
||||
|
||||
if (replacements === undefined) return value;
|
||||
return value;
|
||||
}
|
||||
|
||||
let replaceMatches = value.match(/:([\S]+)/g);
|
||||
if (replaceMatches === null) return value;
|
||||
/**
|
||||
* Perform replacements on a string.
|
||||
* @param {String} string
|
||||
* @param {Object} replacements
|
||||
* @returns {*}
|
||||
*/
|
||||
performReplacements(string, replacements) {
|
||||
if (!replacements) return string;
|
||||
let replaceMatches = string.match(/:([\S]+)/g);
|
||||
if (replaceMatches === null) return string;
|
||||
replaceMatches.forEach(match => {
|
||||
let key = match.substring(1);
|
||||
if (typeof replacements[key] === 'undefined') return;
|
||||
value = value.replace(match, replacements[key]);
|
||||
string = string.replace(match, replacements[key]);
|
||||
});
|
||||
return value;
|
||||
return string;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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};
|
|
@ -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,
|
||||
};
|
|
@ -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},
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -6,16 +6,19 @@ function exists(id) {
|
|||
|
||||
let vueMapping = {
|
||||
'search-system': require('./search'),
|
||||
'entity-dashboard': require('./entity-search'),
|
||||
'code-editor': require('./code-editor')
|
||||
'entity-dashboard': require('./entity-dashboard'),
|
||||
'code-editor': require('./code-editor'),
|
||||
'image-manager': require('./image-manager'),
|
||||
'tag-manager': require('./tag-manager'),
|
||||
'attachment-manager': require('./attachment-manager'),
|
||||
};
|
||||
|
||||
window.vues = {};
|
||||
|
||||
Object.keys(vueMapping).forEach(id => {
|
||||
if (exists(id)) {
|
||||
let config = vueMapping[id];
|
||||
config.el = '#' + id;
|
||||
window.vues[id] = new Vue(config);
|
||||
let ids = Object.keys(vueMapping);
|
||||
for (let i = 0, len = ids.length; i < len; i++) {
|
||||
if (!exists(ids[i])) continue;
|
||||
let config = vueMapping[ids[i]];
|
||||
config.el = '#' + ids[i];
|
||||
window.vues[ids[i]] = new Vue(config);
|
||||
}
|
||||
});
|
|
@ -36,41 +36,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.anim.notification {
|
||||
transform: translate3d(580px, 0, 0);
|
||||
animation-name: notification;
|
||||
animation-duration: 3s;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
&.stopped {
|
||||
animation-name: notificationStopped;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes notification {
|
||||
0% {
|
||||
transform: translate3d(580px, 0, 0);
|
||||
}
|
||||
10% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(580px, 0, 0);
|
||||
}
|
||||
}
|
||||
@keyframes notificationStopped {
|
||||
0% {
|
||||
transform: translate3d(580px, 0, 0);
|
||||
}
|
||||
10% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
.anim.menuIn {
|
||||
transform-origin: 100% 0%;
|
||||
animation-name: menuIn;
|
||||
animation-duration: 120ms;
|
||||
animation-delay: 0s;
|
||||
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
|
||||
}
|
||||
|
||||
@keyframes menuIn {
|
||||
|
@ -85,14 +56,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.anim.menuIn {
|
||||
transform-origin: 100% 0%;
|
||||
animation-name: menuIn;
|
||||
animation-duration: 120ms;
|
||||
animation-delay: 0s;
|
||||
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
|
||||
}
|
||||
|
||||
@keyframes loadingBob {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
|
@ -128,6 +91,6 @@
|
|||
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
|
||||
}
|
||||
|
||||
.anim.selectFade {
|
||||
.selectFade {
|
||||
transition: background-color ease-in-out 3000ms;
|
||||
}
|
|
@ -134,8 +134,7 @@
|
|||
.callout {
|
||||
border-left: 3px solid #BBB;
|
||||
background-color: #EEE;
|
||||
padding: $-s;
|
||||
padding-left: $-xl;
|
||||
padding: $-s $-s $-s $-xl;
|
||||
display: block;
|
||||
position: relative;
|
||||
&:before {
|
||||
|
@ -182,3 +181,77 @@
|
|||
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;
|
||||
}
|
|
@ -2,9 +2,12 @@
|
|||
@mixin generate-button-colors($textColor, $backgroundColor) {
|
||||
background-color: $backgroundColor;
|
||||
color: $textColor;
|
||||
text-transform: uppercase;
|
||||
border: 1px solid $backgroundColor;
|
||||
vertical-align: top;
|
||||
&:hover {
|
||||
background-color: lighten($backgroundColor, 8%);
|
||||
box-shadow: $bs-med;
|
||||
//box-shadow: $bs-med;
|
||||
text-decoration: none;
|
||||
color: $textColor;
|
||||
}
|
||||
|
@ -26,17 +29,16 @@ $button-border-radius: 2px;
|
|||
text-decoration: none;
|
||||
font-size: $fs-m;
|
||||
line-height: 1.4em;
|
||||
padding: $-xs $-m;
|
||||
padding: $-xs*1.3 $-m;
|
||||
margin: $-xs $-xs $-xs 0;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
font-family: $text;
|
||||
font-weight: 400;
|
||||
outline: 0;
|
||||
border-radius: $button-border-radius;
|
||||
cursor: pointer;
|
||||
transition: all ease-in-out 120ms;
|
||||
box-shadow: 0 0.5px 1.5px 0 rgba(0, 0, 0, 0.21);
|
||||
box-shadow: 0;
|
||||
@include generate-button-colors(#EEE, $primary);
|
||||
}
|
||||
|
||||
|
@ -52,19 +54,54 @@ $button-border-radius: 2px;
|
|||
@include generate-button-colors(#EEE, $secondary);
|
||||
}
|
||||
&.muted {
|
||||
@include generate-button-colors(#EEE, #888);
|
||||
@include generate-button-colors(#EEE, #AAA);
|
||||
}
|
||||
&.muted-light {
|
||||
@include generate-button-colors(#666, #e4e4e4);
|
||||
}
|
||||
}
|
||||
|
||||
.button.outline {
|
||||
background-color: transparent;
|
||||
color: #888;
|
||||
border: 1px solid #DDD;
|
||||
&:hover, &:focus, &:active {
|
||||
box-shadow: none;
|
||||
background-color: #EEE;
|
||||
}
|
||||
&.page {
|
||||
border-color: $color-page;
|
||||
color: $color-page;
|
||||
&:hover, &:focus, &:active {
|
||||
background-color: $color-page;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
&.chapter {
|
||||
border-color: $color-chapter;
|
||||
color: $color-chapter;
|
||||
&:hover, &:focus, &:active {
|
||||
background-color: $color-chapter;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
&.book {
|
||||
border-color: $color-book;
|
||||
color: $color-book;
|
||||
&:hover, &:focus, &:active {
|
||||
background-color: $color-book;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-button {
|
||||
@extend .link;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
user-select: none;
|
||||
&:focus, &:active {
|
||||
outline: 0;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
.CodeMirror {
|
||||
/* Set height, width, borders, and global font properties here */
|
||||
font-family: monospace;
|
||||
height: 300px;
|
||||
color: black;
|
||||
}
|
||||
|
@ -235,7 +234,6 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
|||
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||
border-width: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
|
@ -368,9 +366,9 @@ span.CodeMirror-selectedtext { background: none; }
|
|||
.cm-s-base16-light span.cm-atom { color: #aa759f; }
|
||||
.cm-s-base16-light span.cm-number { color: #aa759f; }
|
||||
|
||||
.cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute { color: #90a959; }
|
||||
.cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute { color: #678c30; }
|
||||
.cm-s-base16-light span.cm-keyword { color: #ac4142; }
|
||||
.cm-s-base16-light span.cm-string { color: #f4bf75; }
|
||||
.cm-s-base16-light span.cm-string { color: #e09c3c; }
|
||||
|
||||
.cm-s-base16-light span.cm-variable { color: #90a959; }
|
||||
.cm-s-base16-light span.cm-variable-2 { color: #6a9fb5; }
|
||||
|
@ -386,7 +384,10 @@ span.CodeMirror-selectedtext { background: none; }
|
|||
/**
|
||||
* Custom BookStack overrides
|
||||
*/
|
||||
.cm-s-base16-light.CodeMirror {
|
||||
.CodeMirror, .CodeMirror pre {
|
||||
font-size: 12px;
|
||||
}
|
||||
.CodeMirror {
|
||||
font-size: 12px;
|
||||
height: auto;
|
||||
margin-bottom: $-l;
|
||||
|
@ -394,7 +395,7 @@ span.CodeMirror-selectedtext { background: none; }
|
|||
}
|
||||
.cm-s-base16-light .CodeMirror-gutters { background: #f5f5f5; border-right: 1px solid #DDD; }
|
||||
|
||||
.flex-fill .CodeMirror {
|
||||
.code-fill .CodeMirror {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
|
|
@ -1,4 +1,65 @@
|
|||
.overlay {
|
||||
// System wide notifications
|
||||
[notification] {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: $-xl*2 $-xl;
|
||||
padding: $-l $-xl;
|
||||
background-color: #EEE;
|
||||
border-radius: 3px;
|
||||
box-shadow: $bs-med;
|
||||
z-index: 999999;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
max-width: 480px;
|
||||
transition: transform ease-in-out 360ms;
|
||||
transform: translate3d(580px, 0, 0);
|
||||
i, span {
|
||||
display: table-cell;
|
||||
}
|
||||
i {
|
||||
font-size: 2em;
|
||||
padding-right: $-l;
|
||||
}
|
||||
span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
&.pos {
|
||||
background-color: $positive;
|
||||
color: #EEE;
|
||||
}
|
||||
&.neg {
|
||||
background-color: $negative;
|
||||
color: #EEE;
|
||||
}
|
||||
&.warning {
|
||||
background-color: $secondary;
|
||||
color: #EEE;
|
||||
}
|
||||
&.showing {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
[chapter-toggle] {
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
transition: all ease-in-out 180ms;
|
||||
user-select: none;
|
||||
i.zmdi-caret-right {
|
||||
transition: all ease-in-out 180ms;
|
||||
transform: rotate(0deg);
|
||||
transform-origin: 25% 50%;
|
||||
}
|
||||
&.open {
|
||||
//margin-bottom: 0;
|
||||
}
|
||||
&.open i.zmdi-caret-right {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
[overlay] {
|
||||
background-color: rgba(0, 0, 0, 0.333);
|
||||
position: fixed;
|
||||
z-index: 95536;
|
||||
|
@ -451,7 +512,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
|||
}
|
||||
|
||||
|
||||
[tab-container] .nav-tabs {
|
||||
.tab-container .nav-tabs {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #DDD;
|
||||
margin-bottom: $-m;
|
||||
|
@ -480,3 +541,44 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -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+ */
|
||||
}
|
|
@ -2,15 +2,13 @@
|
|||
.input-base {
|
||||
background-color: #FFF;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #CCC;
|
||||
border: 1px solid #D4D4D4;
|
||||
display: inline-block;
|
||||
font-size: $fs-s;
|
||||
font-family: $text;
|
||||
padding: $-xs;
|
||||
color: #222;
|
||||
padding: $-xs*1.5;
|
||||
color: #666;
|
||||
width: 250px;
|
||||
max-width: 100%;
|
||||
//-webkit-appearance:none;
|
||||
&.neg, &.invalid {
|
||||
border: 1px solid $negative;
|
||||
}
|
||||
|
@ -25,6 +23,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.fake-input {
|
||||
@extend .input-base;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#html-editor {
|
||||
display: none;
|
||||
}
|
||||
|
@ -33,7 +36,6 @@
|
|||
position: relative;
|
||||
z-index: 5;
|
||||
#markdown-editor-input {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
padding: $-xs $-m;
|
||||
|
@ -69,7 +71,6 @@
|
|||
.editor-toolbar {
|
||||
width: 100%;
|
||||
padding: $-xs $-m;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
border-bottom: 1px solid #DDD;
|
||||
|
@ -87,8 +88,9 @@ label {
|
|||
display: block;
|
||||
line-height: 1.4em;
|
||||
font-size: 0.94em;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
color: #999;
|
||||
text-transform: uppercase;
|
||||
padding-bottom: 2px;
|
||||
margin-bottom: 0.2em;
|
||||
&.inline {
|
||||
|
@ -189,28 +191,15 @@ input:checked + .toggle-switch {
|
|||
}
|
||||
|
||||
.inline-input-style {
|
||||
border: 2px dotted #BBB;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: $-xs $-s;
|
||||
}
|
||||
|
||||
.title-input .input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title-input label, .description-input label{
|
||||
margin-top: $-m;
|
||||
color: #666;
|
||||
padding: $-s;
|
||||
}
|
||||
|
||||
.title-input input[type="text"] {
|
||||
@extend h1;
|
||||
@extend .inline-input-style;
|
||||
margin-top: 0;
|
||||
padding-right: 0;
|
||||
width: 100%;
|
||||
color: #444;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.title-input.page-title {
|
||||
|
@ -251,21 +240,20 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
|
|||
border: none;
|
||||
color: $primary;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
margin-left: $-s;
|
||||
}
|
||||
button[type="submit"] {
|
||||
margin-left: -$-l;
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 9.5px;
|
||||
}
|
||||
input {
|
||||
padding-right: $-l;
|
||||
display: block;
|
||||
padding-left: $-l;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
input.outline {
|
||||
.outline > input {
|
||||
border: 0;
|
||||
border-bottom: 2px solid #DDD;
|
||||
border-radius: 0;
|
||||
|
|
|
@ -20,19 +20,128 @@ body.flexbox {
|
|||
align-items: stretch;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
.flex, &.flex {
|
||||
&.rows {
|
||||
flex-direction: row;
|
||||
}
|
||||
&.columns {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.flex {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex.scroll {
|
||||
//overflow-y: auto;
|
||||
display: flex;
|
||||
&.sidebar {
|
||||
margin-right: -14px;
|
||||
}
|
||||
}
|
||||
.flex.scroll .scroll-body {
|
||||
overflow-y: scroll;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex-child > div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
//body.ie .flex-child > div {
|
||||
// flex: 1 0 0px;
|
||||
//}
|
||||
.flex.sidebar {
|
||||
flex: 1;
|
||||
background-color: #F2F2F2;
|
||||
max-width: 360px;
|
||||
min-height: 90vh;
|
||||
}
|
||||
.flex.sidebar + .flex.content {
|
||||
flex: 3;
|
||||
background-color: #FFFFFF;
|
||||
padding: 0 $-l;
|
||||
border-left: 1px solid #DDD;
|
||||
max-width: 100%;
|
||||
}
|
||||
.flex.sidebar .sidebar-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include smaller-than($xl) {
|
||||
body.sidebar-layout {
|
||||
padding-left: 30px;
|
||||
}
|
||||
.flex.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
padding-right: 30px;
|
||||
width: 360px;
|
||||
box-shadow: none;
|
||||
transform: translate3d(-330px, 0, 0);
|
||||
transition: transform ease-in-out 120ms;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex.sidebar.open {
|
||||
box-shadow: 1px 2px 2px 1px rgba(0,0,0,.10);
|
||||
transform: translate3d(0, 0, 0);
|
||||
.sidebar-toggle i {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
.flex.sidebar .sidebar-toggle {
|
||||
display: block;
|
||||
position: absolute;
|
||||
opacity: 0.9;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 30px;
|
||||
color: #666;
|
||||
font-size: 20px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
border: 1px solid #DDD;
|
||||
border-top: 1px solid #BBB;
|
||||
padding-top: $-m;
|
||||
cursor: pointer;
|
||||
i {
|
||||
opacity: 0.5;
|
||||
transition: all ease-in-out 120ms;
|
||||
padding: 0;
|
||||
}
|
||||
&:hover i {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.sidebar .scroll-body {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#sidebar .scroll-body.fixed {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@include larger-than($xl) {
|
||||
#sidebar .scroll-body.fixed {
|
||||
z-index: 5;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding-right: $-m;
|
||||
width: 30%;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
-ms-overflow-style: none;
|
||||
//background-color: $primary-faded;
|
||||
border-left: 1px solid #DDD;
|
||||
&::-webkit-scrollbar { width: 0 !important }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Rules for all columns */
|
||||
div[class^="col-"] img {
|
||||
|
@ -54,6 +163,10 @@ div[class^="col-"] img {
|
|||
&.small {
|
||||
max-width: 840px;
|
||||
}
|
||||
&.nopad {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
|
|
|
@ -12,7 +12,6 @@ header {
|
|||
padding: $-m;
|
||||
}
|
||||
border-bottom: 1px solid #DDD;
|
||||
//margin-bottom: $-l;
|
||||
.links {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
@ -23,26 +22,27 @@ header {
|
|||
}
|
||||
.links a {
|
||||
display: inline-block;
|
||||
padding: $-l;
|
||||
padding: $-m $-l;
|
||||
color: #FFF;
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
@include smaller-than($screen-md) {
|
||||
padding: $-l $-s;
|
||||
padding: $-m $-s;
|
||||
}
|
||||
}
|
||||
.avatar, .user-name {
|
||||
display: inline-block;
|
||||
}
|
||||
.avatar {
|
||||
//margin-top: (45px/2);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.user-name {
|
||||
vertical-align: top;
|
||||
padding-top: $-l;
|
||||
padding-top: $-m;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
> * {
|
||||
|
@ -66,53 +66,57 @@ header {
|
|||
}
|
||||
}
|
||||
}
|
||||
@include smaller-than($screen-md) {
|
||||
@include smaller-than($screen-sm) {
|
||||
text-align: center;
|
||||
.float.right {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
@include smaller-than($screen-sm) {
|
||||
.links a {
|
||||
padding: $-s;
|
||||
}
|
||||
form.search-box {
|
||||
margin-top: 0;
|
||||
}
|
||||
.user-name {
|
||||
padding-top: $-s;
|
||||
}
|
||||
}
|
||||
.dropdown-container {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.header-search {
|
||||
display: inline-block;
|
||||
}
|
||||
header .search-box {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
input {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #EEE;
|
||||
}
|
||||
button {
|
||||
color: #EEE;
|
||||
}
|
||||
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: #DDD;
|
||||
}
|
||||
::-moz-placeholder { /* Firefox 19+ */
|
||||
color: #DDD;
|
||||
}
|
||||
:-ms-input-placeholder { /* IE 10+ */
|
||||
color: #DDD;
|
||||
}
|
||||
:-moz-placeholder { /* Firefox 18- */
|
||||
color: #DDD;
|
||||
}
|
||||
@include smaller-than($screen-lg) {
|
||||
max-width: 250px;
|
||||
}
|
||||
@include smaller-than($l) {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
form.search-box {
|
||||
margin-top: $-l *0.9;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
input {
|
||||
background-color: transparent;
|
||||
border-radius: 24px;
|
||||
border: 2px solid #EEE;
|
||||
color: #EEE;
|
||||
padding-left: $-m;
|
||||
padding-right: $-l;
|
||||
outline: 0;
|
||||
}
|
||||
button {
|
||||
vertical-align: top;
|
||||
margin-left: -$-l;
|
||||
color: #FFF;
|
||||
top: 6px;
|
||||
right: 4px;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
}
|
||||
@include smaller-than($s) {
|
||||
.header-search {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,12 +132,12 @@ form.search-box {
|
|||
font-size: 1.8em;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
padding: $-l $-l $-l 0;
|
||||
padding: 14px $-l 14px 0;
|
||||
vertical-align: top;
|
||||
line-height: 1;
|
||||
}
|
||||
.logo-image {
|
||||
margin: $-m $-s $-m 0;
|
||||
margin: $-xs $-s $-xs 0;
|
||||
vertical-align: top;
|
||||
height: 43px;
|
||||
}
|
||||
|
@ -167,6 +171,10 @@ form.search-box {
|
|||
background-color: $primary-faded;
|
||||
}
|
||||
|
||||
.toolbar-container {
|
||||
background-color: #FFF;
|
||||
}
|
||||
|
||||
.breadcrumbs .text-button, .action-buttons .text-button {
|
||||
display: inline-block;
|
||||
padding: $-s;
|
||||
|
@ -228,3 +236,6 @@ form.search-box {
|
|||
}
|
||||
}
|
||||
}
|
||||
.faded-small .nav-tabs a {
|
||||
padding: $-s $-m;
|
||||
}
|
|
@ -9,14 +9,19 @@ html {
|
|||
&.flexbox {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
&.shaded {
|
||||
background-color: #F2F2F2;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: $text;
|
||||
font-size: $fs-m;
|
||||
line-height: 1.6;
|
||||
color: #616161;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
&.shaded {
|
||||
background-color: #F2F2F2;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
.inset-list {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
margin-bottom: $-l;
|
||||
}
|
||||
h5 {
|
||||
display: block;
|
||||
|
@ -22,6 +21,9 @@
|
|||
border-left-color: $color-page-draft;
|
||||
}
|
||||
}
|
||||
.entity-list-item {
|
||||
margin-bottom: $-m;
|
||||
}
|
||||
hr {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
@ -51,23 +53,6 @@
|
|||
margin-right: $-s;
|
||||
}
|
||||
}
|
||||
.chapter-toggle {
|
||||
cursor: pointer;
|
||||
margin: 0 0 $-l 0;
|
||||
transition: all ease-in-out 180ms;
|
||||
user-select: none;
|
||||
i.zmdi-caret-right {
|
||||
transition: all ease-in-out 180ms;
|
||||
transform: rotate(0deg);
|
||||
transform-origin: 25% 50%;
|
||||
}
|
||||
&.open {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&.open i.zmdi-caret-right {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-page-nav {
|
||||
$nav-indent: $-s;
|
||||
|
@ -101,31 +86,8 @@
|
|||
|
||||
// Sidebar list
|
||||
.book-tree {
|
||||
padding: $-xs 0 0 0;
|
||||
position: relative;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: ease-in-out 240ms;
|
||||
transition-property: right, border;
|
||||
border-left: 0px solid #FFF;
|
||||
background-color: #FFF;
|
||||
max-width: 320px;
|
||||
&.fixed {
|
||||
background-color: #FFF;
|
||||
z-index: 5;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding-left: $-l;
|
||||
padding-right: $-l + 15;
|
||||
width: 30%;
|
||||
right: -15px;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
-ms-overflow-style: none;
|
||||
//background-color: $primary-faded;
|
||||
border-left: 1px solid #DDD;
|
||||
&::-webkit-scrollbar { width: 0 !important }
|
||||
}
|
||||
}
|
||||
.book-tree h4 {
|
||||
padding: $-m $-s 0 $-s;
|
||||
|
@ -171,7 +133,7 @@
|
|||
background-color: rgba($color-chapter, 0.12);
|
||||
}
|
||||
}
|
||||
.chapter-toggle {
|
||||
[chapter-toggle] {
|
||||
padding-left: $-s;
|
||||
}
|
||||
.list-item-chapter {
|
||||
|
@ -260,6 +222,9 @@
|
|||
.left + .right {
|
||||
margin-left: 30px + $-s;
|
||||
}
|
||||
&:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul.pagination {
|
||||
|
@ -312,9 +277,6 @@ ul.pagination {
|
|||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
p {
|
||||
margin: $-xs 0 0 0;
|
||||
}
|
||||
hr {
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -331,15 +293,24 @@ ul.pagination {
|
|||
}
|
||||
}
|
||||
|
||||
.card .entity-list-item, .card .activity-list-item {
|
||||
padding-left: $-m;
|
||||
padding-right: $-m;
|
||||
}
|
||||
|
||||
.entity-list.compact {
|
||||
font-size: 0.6em;
|
||||
h4, a {
|
||||
line-height: 1.2;
|
||||
}
|
||||
p {
|
||||
.entity-item-snippet {
|
||||
display: none;
|
||||
}
|
||||
.entity-list-item p {
|
||||
font-size: $fs-m * 0.8;
|
||||
padding-top: $-xs;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
> p.empty-text {
|
||||
|
@ -381,6 +352,7 @@ ul.pagination {
|
|||
}
|
||||
li.padded {
|
||||
padding: $-xs $-m;
|
||||
line-height: 1.2;
|
||||
}
|
||||
a {
|
||||
display: block;
|
||||
|
|
|
@ -1,12 +1,3 @@
|
|||
#page-show {
|
||||
>.row .col-md-9 {
|
||||
z-index: 2;
|
||||
}
|
||||
>.row .col-md-3 {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.page-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -36,6 +27,8 @@
|
|||
|
||||
.page-content {
|
||||
max-width: 840px;
|
||||
margin: 0 auto;
|
||||
margin-top: $-xxl;
|
||||
overflow-wrap: break-word;
|
||||
.align-left {
|
||||
text-align: left;
|
||||
|
@ -226,7 +219,7 @@
|
|||
width: 100%;
|
||||
min-width: 50px;
|
||||
}
|
||||
.tags td {
|
||||
.tags td, .tag-table > div > div > div {
|
||||
padding-right: $-s;
|
||||
padding-top: $-s;
|
||||
position: relative;
|
||||
|
@ -252,8 +245,6 @@
|
|||
}
|
||||
|
||||
.tag-display {
|
||||
width: 100%;
|
||||
//opacity: 0.7;
|
||||
position: relative;
|
||||
table {
|
||||
width: 100%;
|
||||
|
@ -311,3 +302,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
|
||||
min-height: 175px;
|
||||
}
|
|
@ -58,13 +58,3 @@ table.list-table {
|
|||
padding: $-xs;
|
||||
}
|
||||
}
|
||||
|
||||
table.file-table {
|
||||
@extend .no-style;
|
||||
td {
|
||||
padding: $-xs;
|
||||
}
|
||||
.ui-sortable-helper {
|
||||
display: table;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,14 @@
|
|||
/**
|
||||
* Fonts
|
||||
*/
|
||||
|
||||
body, button, input, select, label, textarea {
|
||||
font-family: $text;
|
||||
}
|
||||
.Codemirror, pre, #markdown-editor-input, .editor-toolbar, .code-base {
|
||||
font-family: $mono;
|
||||
}
|
||||
|
||||
/*
|
||||
* Header Styles
|
||||
*/
|
||||
|
@ -58,7 +69,6 @@ a, .link {
|
|||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color ease-in-out 80ms;
|
||||
font-family: $text;
|
||||
line-height: 1.6;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
@ -131,7 +141,6 @@ sub, .subscript {
|
|||
}
|
||||
|
||||
pre {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #DDD;
|
||||
|
@ -180,7 +189,6 @@ blockquote {
|
|||
|
||||
.code-base {
|
||||
background-color: #F8F8F8;
|
||||
font-family: monospace;
|
||||
font-size: 0.80em;
|
||||
border: 1px solid #DDD;
|
||||
border-radius: 3px;
|
||||
|
@ -370,12 +378,6 @@ span.sep {
|
|||
display: block;
|
||||
}
|
||||
|
||||
.action-header {
|
||||
h1 {
|
||||
margin-top: $-m;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Icons
|
||||
*/
|
||||
|
|
|
@ -49,3 +49,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.page-content.mce-content-body p {
|
||||
line-height: 1.6;
|
||||
}
|
|
@ -27,8 +27,12 @@ $-xs: 6px;
|
|||
$-xxs: 3px;
|
||||
|
||||
// Fonts
|
||||
$heading: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
|
||||
$text: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
|
||||
$text: -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell",
|
||||
"Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
$mono: "Lucida Console", "DejaVu Sans Mono", "Ubunto Mono", Monaco, monospace;
|
||||
$heading: $text;
|
||||
$fs-m: 15px;
|
||||
$fs-s: 14px;
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
//@import "reset";
|
||||
@import "variables";
|
||||
@import "mixins";
|
||||
@import "html";
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
@import "reset";
|
||||
@import "variables";
|
||||
@import "fonts";
|
||||
@import "mixins";
|
||||
@import "html";
|
||||
@import "text";
|
||||
|
@ -17,12 +16,11 @@
|
|||
@import "lists";
|
||||
@import "pages";
|
||||
|
||||
[v-cloak], [v-show] {
|
||||
[v-cloak] {
|
||||
display: none; opacity: 0;
|
||||
animation-name: none !important;
|
||||
}
|
||||
|
||||
|
||||
[ng\:cloak], [ng-cloak], .ng-cloak {
|
||||
display: none !important;
|
||||
user-select: none;
|
||||
|
@ -65,50 +63,11 @@ body.dragging, body.dragging * {
|
|||
}
|
||||
}
|
||||
|
||||
// System wide notifications
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: $-xl*2 $-xl;
|
||||
padding: $-l $-xl;
|
||||
background-color: #EEE;
|
||||
border-radius: 3px;
|
||||
box-shadow: $bs-med;
|
||||
z-index: 999999;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
max-width: 480px;
|
||||
i, span {
|
||||
display: table-cell;
|
||||
}
|
||||
i {
|
||||
font-size: 2em;
|
||||
padding-right: $-l;
|
||||
}
|
||||
span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
&.pos {
|
||||
background-color: $positive;
|
||||
color: #EEE;
|
||||
}
|
||||
&.neg {
|
||||
background-color: $negative;
|
||||
color: #EEE;
|
||||
}
|
||||
&.warning {
|
||||
background-color: $secondary;
|
||||
color: #EEE;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading icon
|
||||
$loadingSize: 10px;
|
||||
.loading-container {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: $loadingSize;
|
||||
margin: $-xl auto;
|
||||
> div {
|
||||
width: $loadingSize;
|
||||
|
@ -116,7 +75,8 @@ $loadingSize: 10px;
|
|||
border-radius: $loadingSize;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(-10px, 0, 0);
|
||||
margin-top: $-xs;
|
||||
animation-name: loadingBob;
|
||||
animation-duration: 1.4s;
|
||||
animation-iteration-count: infinite;
|
||||
|
@ -130,11 +90,17 @@ $loadingSize: 10px;
|
|||
background-color: $color-book;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
> div:last-child {
|
||||
> div:last-of-type {
|
||||
left: $loadingSize+$-xs;
|
||||
background-color: $color-chapter;
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
> span {
|
||||
margin-left: $-s;
|
||||
font-style: italic;
|
||||
color: #888;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -150,7 +116,7 @@ $loadingSize: 10px;
|
|||
|
||||
// Back to top link
|
||||
$btt-size: 40px;
|
||||
#back-to-top {
|
||||
[back-to-top] {
|
||||
background-color: $primary;
|
||||
position: fixed;
|
||||
bottom: $-m;
|
||||
|
@ -256,22 +222,15 @@ $btt-size: 40px;
|
|||
}
|
||||
|
||||
.center-box {
|
||||
margin: $-xl auto 0 auto;
|
||||
padding: $-m $-xxl $-xl $-xxl;
|
||||
margin: $-xxl auto 0 auto;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
//border: 1px solid #DDD;
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
&.login {
|
||||
background-color: #EEE;
|
||||
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #DDD;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -8,33 +8,33 @@ return [
|
|||
*/
|
||||
|
||||
// Pages
|
||||
'page_create' => 'Seite erstellt',
|
||||
'page_create_notification' => 'Seite erfolgreich erstellt',
|
||||
'page_update' => 'Seite aktualisiert',
|
||||
'page_update_notification' => 'Seite erfolgreich aktualisiert',
|
||||
'page_delete' => 'Seite gelöscht',
|
||||
'page_delete_notification' => 'Seite erfolgreich gelöscht',
|
||||
'page_restore' => 'Seite wiederhergstellt',
|
||||
'page_restore_notification' => 'Seite erfolgreich wiederhergstellt',
|
||||
'page_move' => 'Seite verschoben',
|
||||
'page_create' => 'hat Seite erstellt:',
|
||||
'page_create_notification' => 'hat Seite erfolgreich erstellt:',
|
||||
'page_update' => 'hat Seite aktualisiert:',
|
||||
'page_update_notification' => 'hat Seite erfolgreich aktualisiert:',
|
||||
'page_delete' => 'hat Seite gelöscht:',
|
||||
'page_delete_notification' => 'hat Seite erfolgreich gelöscht:',
|
||||
'page_restore' => 'hat Seite wiederhergstellt:',
|
||||
'page_restore_notification' => 'hat Seite erfolgreich wiederhergstellt:',
|
||||
'page_move' => 'hat Seite verschoben:',
|
||||
|
||||
// Chapters
|
||||
'chapter_create' => 'Kapitel erstellt',
|
||||
'chapter_create_notification' => 'Kapitel erfolgreich erstellt',
|
||||
'chapter_update' => 'Kapitel aktualisiert',
|
||||
'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert',
|
||||
'chapter_delete' => 'Kapitel gelöscht',
|
||||
'chapter_delete_notification' => 'Kapitel erfolgreich gelöscht',
|
||||
'chapter_move' => 'Kapitel verschoben',
|
||||
'chapter_create' => 'hat Kapitel erstellt:',
|
||||
'chapter_create_notification' => 'hat Kapitel erfolgreich erstellt:',
|
||||
'chapter_update' => 'hat Kapitel aktualisiert:',
|
||||
'chapter_update_notification' => 'hat Kapitel erfolgreich aktualisiert:',
|
||||
'chapter_delete' => 'hat Kapitel gelöscht',
|
||||
'chapter_delete_notification' => 'hat Kapitel erfolgreich gelöscht:',
|
||||
'chapter_move' => 'hat Kapitel verschoben:',
|
||||
|
||||
// Books
|
||||
'book_create' => 'Buch erstellt',
|
||||
'book_create_notification' => 'Buch erfolgreich erstellt',
|
||||
'book_update' => 'Buch aktualisiert',
|
||||
'book_update_notification' => 'Buch erfolgreich aktualisiert',
|
||||
'book_delete' => 'Buch gelöscht',
|
||||
'book_delete_notification' => 'Buch erfolgreich gelöscht',
|
||||
'book_sort' => 'Buch sortiert',
|
||||
'book_sort_notification' => 'Buch erfolgreich neu sortiert',
|
||||
'book_create' => 'hat Buch erstellt:',
|
||||
'book_create_notification' => 'hat Buch erfolgreich erstellt:',
|
||||
'book_update' => 'hat Buch aktualisiert:',
|
||||
'book_update_notification' => 'hat Buch erfolgreich aktualisiert:',
|
||||
'book_delete' => 'hat Buch gelöscht:',
|
||||
'book_delete_notification' => 'hat Buch erfolgreich gelöscht:',
|
||||
'book_sort' => 'hat Buch sortiert:',
|
||||
'book_sort_notification' => 'hat Buch erfolgreich neu sortiert:',
|
||||
|
||||
];
|
||||
|
|
|
@ -10,8 +10,8 @@ return [
|
|||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
'failed' => 'Dies sind keine gültigen Anmeldedaten.',
|
||||
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen sie es in :seconds Sekunden erneut.',
|
||||
'failed' => 'Die eingegebenen Anmeldedaten sind ungültig.',
|
||||
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.',
|
||||
|
||||
/**
|
||||
* Login & Register
|
||||
|
@ -29,16 +29,16 @@ return [
|
|||
'forgot_password' => 'Passwort vergessen?',
|
||||
'remember_me' => 'Angemeldet bleiben',
|
||||
'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.',
|
||||
'create_account' => 'Account anlegen',
|
||||
'social_login' => 'Social Login',
|
||||
'social_registration' => 'Social Registrierung',
|
||||
'social_registration_text' => 'Mit einem dieser Möglichkeiten registrieren oder anmelden.',
|
||||
'create_account' => 'Account registrieren',
|
||||
'social_login' => 'Mit Sozialem Netzwerk anmelden',
|
||||
'social_registration' => 'Mit Sozialem Netzwerk registrieren',
|
||||
'social_registration_text' => 'Mit einer dieser Dienste registrieren oder anmelden',
|
||||
|
||||
|
||||
'register_thanks' => 'Vielen Dank für Ihre Registrierung!',
|
||||
'register_confirm' => 'Bitte prüfen Sie Ihren E-Mail Eingang und klicken auf den Verifizieren-Button, um :appName nutzen zu können.',
|
||||
'registrations_disabled' => 'Die Registrierung ist momentan nicht möglich',
|
||||
'registration_email_domain_invalid' => 'Diese E-Mail-Domain ist für die Benutzer der Applikation nicht freigeschaltet.',
|
||||
'register_confirm' => 'Bitte prüfen Sie Ihren Posteingang und bestätigen Sie die Registrierung.',
|
||||
'registrations_disabled' => 'Eine Registrierung ist momentan nicht möglich',
|
||||
'registration_email_domain_invalid' => 'Sie können sich mit dieser E-Mail nicht registrieren.',
|
||||
'register_success' => 'Vielen Dank für Ihre Registrierung! Die Daten sind gespeichert und Sie sind angemeldet.',
|
||||
|
||||
|
||||
|
@ -46,30 +46,30 @@ return [
|
|||
* Password Reset
|
||||
*/
|
||||
'reset_password' => 'Passwort vergessen',
|
||||
'reset_password_send_instructions' => 'Bitte geben Sie unten Ihre E-Mail-Adresse ein und Sie erhalten eine E-Mail, um Ihr Passwort zurück zu setzen.',
|
||||
'reset_password_send_instructions' => 'Bitte geben Sie Ihre E-Mail-Adresse ein. Danach erhalten Sie eine E-Mail mit einem Link zum Zurücksetzen Ihres Passwortes.',
|
||||
'reset_password_send_button' => 'Passwort zurücksetzen',
|
||||
'reset_password_sent_success' => 'Eine E-Mail mit den Instruktionen, um Ihr Passwort zurückzusetzen wurde an :email gesendet.',
|
||||
'reset_password_sent_success' => 'Eine E-Mail mit dem Link zum Zurücksetzen Ihres Passwortes wurde an :email gesendet.',
|
||||
'reset_password_success' => 'Ihr Passwort wurde erfolgreich zurückgesetzt.',
|
||||
|
||||
'email_reset_subject' => 'Passwort zurücksetzen für :appName',
|
||||
'email_reset_text' => 'Sie erhalten diese E-Mail, weil eine Passwort-Rücksetzung für Ihren Account beantragt wurde.',
|
||||
'email_reset_not_requested' => 'Wenn Sie die Passwort-Rücksetzung nicht ausgelöst haben, ist kein weiteres Handeln notwendig.',
|
||||
'email_reset_text' => 'Sie erhalten diese E-Mail, weil jemand versucht hat, Ihr Passwort zurückzusetzen.',
|
||||
'email_reset_not_requested' => 'Wenn Sie das nicht waren, brauchen Sie nichts weiter zu tun.',
|
||||
|
||||
|
||||
/**
|
||||
* Email Confirmation
|
||||
*/
|
||||
'email_confirm_subject' => 'Bestätigen sie ihre E-Mail Adresse bei :appName',
|
||||
'email_confirm_greeting' => 'Danke, dass sie :appName beigetreten sind!',
|
||||
'email_confirm_text' => 'Bitte bestätigen sie ihre E-Mail Adresse, indem sie auf den Button klicken:',
|
||||
'email_confirm_action' => 'E-Mail Adresse bestätigen',
|
||||
'email_confirm_send_error' => 'Bestätigungs-E-Mail benötigt, aber das System konnte die E-Mail nicht versenden. Kontaktieren sie den Administrator, um sicherzustellen, dass das Sytsem korrekt eingerichtet ist.',
|
||||
'email_confirm_success' => 'Ihre E-Mail Adresse wurde bestätigt!',
|
||||
'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfen sie ihren Posteingang.',
|
||||
'email_confirm_subject' => 'Bestätigen Sie Ihre E-Mail-Adresse für :appName',
|
||||
'email_confirm_greeting' => 'Danke, dass Sie sich für :appName registriert haben!',
|
||||
'email_confirm_text' => 'Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf die Schaltfläche klicken:',
|
||||
'email_confirm_action' => 'E-Mail-Adresse bestätigen',
|
||||
'email_confirm_send_error' => 'Leider konnte die für die Registrierung notwendige E-Mail zur bestätigung Ihrer E-Mail-Adresse nicht versandt werden. Bitte kontaktieren Sie den Systemadministrator!',
|
||||
'email_confirm_success' => 'Ihre E-Mail-Adresse wurde bestätigt!',
|
||||
'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfen Sie Ihren Posteingang.',
|
||||
|
||||
'email_not_confirmed' => 'E-Mail-Adresse ist nicht bestätigt',
|
||||
'email_not_confirmed_text' => 'Ihre E-Mail-Adresse ist bisher nicht bestätigt.',
|
||||
'email_not_confirmed_click_link' => 'Bitte klicken Sie auf den Link in der E-Mail, die Sie nach der Registrierung erhalten haben.',
|
||||
'email_not_confirmed_resend' => 'Wenn Sie die E-Mail nicht erhalten haben, können Sie die Nachricht erneut anfordern. Füllen Sie hierzu bitte das folgende Formular aus:',
|
||||
'email_not_confirmed_resend_button' => 'Bestätigungs E-Mail erneut senden',
|
||||
'email_not_confirmed_resend_button' => 'Bestätigungs-E-Mail erneut senden',
|
||||
];
|
||||
|
|
|
@ -28,9 +28,9 @@ return [
|
|||
'edit' => 'Bearbeiten',
|
||||
'sort' => 'Sortieren',
|
||||
'move' => 'Verschieben',
|
||||
'delete' => 'Löschen',
|
||||
'delete' => 'Löschen',
|
||||
'search' => 'Suchen',
|
||||
'search_clear' => 'Suche löschen',
|
||||
'search_clear' => 'Suche löschen',
|
||||
'reset' => 'Zurücksetzen',
|
||||
'remove' => 'Entfernen',
|
||||
|
||||
|
@ -38,9 +38,9 @@ return [
|
|||
/**
|
||||
* Misc
|
||||
*/
|
||||
'deleted_user' => 'Gelöschte Benutzer',
|
||||
'no_activity' => 'Keine Aktivitäten zum Anzeigen',
|
||||
'no_items' => 'Keine Einträge gefunden.',
|
||||
'deleted_user' => 'Gelöschte Benutzer',
|
||||
'no_activity' => 'Keine Aktivitäten zum Anzeigen',
|
||||
'no_items' => 'Keine Einträge gefunden.',
|
||||
'back_to_top' => 'nach oben',
|
||||
'toggle_details' => 'Details zeigen/verstecken',
|
||||
|
||||
|
@ -53,6 +53,6 @@ return [
|
|||
/**
|
||||
* Email Content
|
||||
*/
|
||||
'email_action_help' => 'Sollte es beim Anklicken des ":actionText" Buttons Probleme geben, kopieren Sie folgende URL und fügen diese in Ihrem Webbrowser ein:',
|
||||
'email_action_help' => 'Sollte es beim Anklicken der Schaltfläche ":action_text" Probleme geben, öffnen Sie folgende URL in Ihrem Browser:',
|
||||
'email_rights' => 'Alle Rechte vorbehalten',
|
||||
];
|
|
@ -13,9 +13,9 @@ return [
|
|||
'image_uploaded' => 'Hochgeladen am :uploadedDate',
|
||||
'image_load_more' => 'Mehr',
|
||||
'image_image_name' => 'Bildname',
|
||||
'image_delete_confirm' => 'Dieses Bild wird auf den folgenden Seiten benutzt. Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild tatsächlich entfernen möchten.',
|
||||
'image_delete_confirm' => 'Dieses Bild wird auf den folgenden Seiten benutzt. Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild wirklich entfernen möchten.',
|
||||
'image_select_image' => 'Bild auswählen',
|
||||
'image_dropzone' => 'Ziehen Sie Bilder hier hinein oder klicken Sie hier, um ein Bild auszuwählen',
|
||||
'image_dropzone' => 'Ziehen Sie Bilder hierher oder klicken Sie, um ein Bild auszuwählen',
|
||||
'images_deleted' => 'Bilder gelöscht',
|
||||
'image_preview' => 'Bildvorschau',
|
||||
'image_upload_success' => 'Bild erfolgreich hochgeladen',
|
||||
|
|
|
@ -4,38 +4,38 @@ return [
|
|||
/**
|
||||
* Shared
|
||||
*/
|
||||
'recently_created' => 'Kürzlich angelegt',
|
||||
'recently_created_pages' => 'Kürzlich angelegte Seiten',
|
||||
'recently_updated_pages' => 'Kürzlich aktualisierte Seiten',
|
||||
'recently_created_chapters' => 'Kürzlich angelegte Kapitel',
|
||||
'recently_created_books' => 'Kürzlich angelegte Bücher',
|
||||
'recently_update' => 'Kürzlich aktualisiert',
|
||||
'recently_viewed' => 'Kürzlich angesehen',
|
||||
'recent_activity' => 'Kürzliche Aktivität',
|
||||
'recently_created' => 'Kürzlich angelegt',
|
||||
'recently_created_pages' => 'Kürzlich angelegte Seiten',
|
||||
'recently_updated_pages' => 'Kürzlich aktualisierte Seiten',
|
||||
'recently_created_chapters' => 'Kürzlich angelegte Kapitel',
|
||||
'recently_created_books' => 'Kürzlich angelegte Bücher',
|
||||
'recently_update' => 'Kürzlich aktualisiert',
|
||||
'recently_viewed' => 'Kürzlich angesehen',
|
||||
'recent_activity' => 'Kürzliche Aktivität',
|
||||
'create_now' => 'Jetzt anlegen',
|
||||
'revisions' => 'Revisionen',
|
||||
'meta_created' => 'Angelegt am :timeLength',
|
||||
'meta_created_name' => 'Angelegt am :timeLength durch :user',
|
||||
'meta_updated' => 'Aktualisiert am :timeLength',
|
||||
'meta_updated_name' => 'Aktualisiert am :timeLength durch :user',
|
||||
'x_pages' => ':count Seiten',
|
||||
'entity_select' => 'Eintrag auswählen',
|
||||
'revisions' => 'Versionen',
|
||||
'meta_revision' => 'Version #:revisionCount',
|
||||
'meta_created' => 'Erstellt: :timeLength',
|
||||
'meta_created_name' => 'Erstellt: :timeLength von :user',
|
||||
'meta_updated' => 'Zuletzt aktualisiert: :timeLength',
|
||||
'meta_updated_name' => 'Zuletzt aktualisiert: :timeLength von :user',
|
||||
'entity_select' => 'Eintrag auswählen',
|
||||
'images' => 'Bilder',
|
||||
'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
|
||||
'my_recently_viewed' => 'Kürzlich von mir angesehen',
|
||||
'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
|
||||
'my_recently_viewed' => 'Kürzlich von mir angesehen',
|
||||
'no_pages_viewed' => 'Sie haben bisher keine Seiten angesehen.',
|
||||
'no_pages_recently_created' => 'Sie haben bisher keine Seiten angelegt.',
|
||||
'no_pages_recently_updated' => 'Sie haben bisher keine Seiten aktualisiert.',
|
||||
'export' => 'Exportieren',
|
||||
'export_html' => 'HTML-Datei',
|
||||
'export_pdf' => 'PDF-Datei',
|
||||
'export_text' => 'Text-Datei',
|
||||
'export_text' => 'Textdatei',
|
||||
|
||||
/**
|
||||
* Permissions and restrictions
|
||||
*/
|
||||
'permissions' => 'Berechtigungen',
|
||||
'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, überschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.',
|
||||
'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, überschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.',
|
||||
'permissions_enable' => 'Individuelle Berechtigungen aktivieren',
|
||||
'permissions_save' => 'Berechtigungen speichern',
|
||||
|
||||
|
@ -43,41 +43,61 @@ return [
|
|||
* Search
|
||||
*/
|
||||
'search_results' => 'Suchergebnisse',
|
||||
'search_clear' => 'Suche zurücksetzen',
|
||||
'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden',
|
||||
'search_for_term' => 'Suche nach :term',
|
||||
'search_total_results_found' => ':count Ergebnis gefunden|:count Ergebnisse gesamt',
|
||||
'search_clear' => 'Filter löschen',
|
||||
'search_no_pages' => 'Keine Seiten gefunden',
|
||||
'search_for_term' => 'Nach :term suchen',
|
||||
'search_more' => 'Mehr Ergebnisse',
|
||||
'search_filters' => 'Filter',
|
||||
'search_content_type' => 'Inhaltstyp',
|
||||
'search_exact_matches' => 'Exakte Treffer',
|
||||
'search_tags' => 'Nach Schlagwort suchen',
|
||||
'search_viewed_by_me' => 'Schon von mir angesehen',
|
||||
'search_not_viewed_by_me' => 'Noch nicht von mir angesehen',
|
||||
'search_permissions_set' => 'Berechtigungen gesetzt',
|
||||
'search_created_by_me' => 'Von mir erstellt',
|
||||
'search_updated_by_me' => 'Von mir aktualisiert',
|
||||
'search_updated_before' => 'Aktualisiert vor',
|
||||
'search_updated_after' => 'Aktualisiert nach',
|
||||
'search_created_before' => 'Erstellt vor',
|
||||
'search_created_after' => 'Erstellt nach',
|
||||
'search_set_date' => 'Datum auswählen',
|
||||
'search_update' => 'Suche aktualisieren',
|
||||
|
||||
/**
|
||||
* Books
|
||||
*/
|
||||
'book' => 'Buch',
|
||||
'books' => 'Bücher',
|
||||
'books_empty' => 'Es wurden keine Bücher angelegt',
|
||||
'books_popular' => 'Populäre Bücher',
|
||||
'books_recent' => 'Kürzlich genutzte Bücher',
|
||||
'books_popular_empty' => 'Die populärsten Bücher werden hier angezeigt.',
|
||||
'books_create' => 'Neues Buch anlegen',
|
||||
'books_delete' => 'Buch löschen',
|
||||
'books_delete_named' => 'Buch :bookName löschen',
|
||||
'books_delete_explain' => 'Sie möchten das Buch \':bookName\' löschen und alle Seiten und Kapitel entfernen.',
|
||||
'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch löschen möchten?',
|
||||
'books' => 'Bücher',
|
||||
'x_books' => ':count Buch|:count Bücher',
|
||||
'books_empty' => 'Keine Bücher vorhanden',
|
||||
'books_popular' => 'Beliebte Bücher',
|
||||
'books_recent' => 'Kürzlich angesehene Bücher',
|
||||
'books_new' => 'Neue Bücher',
|
||||
'books_popular_empty' => 'Die beliebtesten Bücher werden hier angezeigt.',
|
||||
'books_new_empty' => 'Die neusten Bücher werden hier angezeigt.',
|
||||
'books_create' => 'Neues Buch erstellen',
|
||||
'books_delete' => 'Buch löschen',
|
||||
'books_delete_named' => 'Buch ":bookName" löschen',
|
||||
'books_delete_explain' => 'Das Buch ":bookName" wird gelöscht und alle zugehörigen Kapitel und Seiten entfernt.',
|
||||
'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch löschen möchten?',
|
||||
'books_edit' => 'Buch bearbeiten',
|
||||
'books_edit_named' => 'Buch :bookName bearbeiten',
|
||||
'books_form_book_name' => 'Buchname',
|
||||
'books_edit_named' => 'Buch ":bookName" bearbeiten',
|
||||
'books_form_book_name' => 'Name des Buches',
|
||||
'books_save' => 'Buch speichern',
|
||||
'books_permissions' => 'Buch-Berechtigungen',
|
||||
'books_permissions_updated' => 'Buch-Berechtigungen aktualisiert',
|
||||
'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel für dieses Buch angelegt.',
|
||||
'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel zu diesem Buch hinzugefügt worden.',
|
||||
'books_empty_create_page' => 'Neue Seite anlegen',
|
||||
'books_empty_or' => 'oder',
|
||||
'books_empty_sort_current_book' => 'Aktuelles Buch sortieren',
|
||||
'books_empty_add_chapter' => 'Neues Kapitel hinzufügen',
|
||||
'books_empty_add_chapter' => 'Neues Kapitel hinzufügen',
|
||||
'books_permissions_active' => 'Buch-Berechtigungen aktiv',
|
||||
'books_search_this' => 'Dieses Buch durchsuchen',
|
||||
'books_navigation' => 'Buch-Navigation',
|
||||
'books_navigation' => 'Buchnavigation',
|
||||
'books_sort' => 'Buchinhalte sortieren',
|
||||
'books_sort_named' => 'Buch :bookName sortieren',
|
||||
'books_sort_show_other' => 'Andere Bücher zeigen',
|
||||
'books_sort_named' => 'Buch ":bookName" sortieren',
|
||||
'books_sort_show_other' => 'Andere Bücher anzeigen',
|
||||
'books_sort_save' => 'Neue Reihenfolge speichern',
|
||||
|
||||
/**
|
||||
|
@ -85,132 +105,156 @@ return [
|
|||
*/
|
||||
'chapter' => 'Kapitel',
|
||||
'chapters' => 'Kapitel',
|
||||
'chapters_popular' => 'Populäre Kapitel',
|
||||
'x_chapters' => ':count Kapitel',
|
||||
'chapters_popular' => 'Beliebte Kapitel',
|
||||
'chapters_new' => 'Neues Kapitel',
|
||||
'chapters_create' => 'Neues Kapitel anlegen',
|
||||
'chapters_delete' => 'Kapitel entfernen',
|
||||
'chapters_delete_named' => 'Kapitel :chapterName entfernen',
|
||||
'chapters_delete_explain' => 'Sie möchten das Kapitel \':chapterName\' löschen und alle Seiten dem direkten Eltern-Buch hinzugefügen.',
|
||||
'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel löschen möchten?',
|
||||
'chapters_delete_named' => 'Kapitel ":chapterName" entfernen',
|
||||
'chapters_delete_explain' => 'Das Kapitel ":chapterName" wird gelöscht und alle zugehörigen Seiten dem übergeordneten Buch zugeordnet.',
|
||||
'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel löschen möchten?',
|
||||
'chapters_edit' => 'Kapitel bearbeiten',
|
||||
'chapters_edit_named' => 'Kapitel :chapterName bearbeiten',
|
||||
'chapters_edit_named' => 'Kapitel ":chapterName" bearbeiten',
|
||||
'chapters_save' => 'Kapitel speichern',
|
||||
'chapters_move' => 'Kapitel verschieben',
|
||||
'chapters_move_named' => 'Kapitel :chapterName verschieben',
|
||||
'chapter_move_success' => 'Kapitel in das Buch :bookName verschoben.',
|
||||
'chapters_move_named' => 'Kapitel ":chapterName" verschieben',
|
||||
'chapter_move_success' => 'Das Kapitel wurde in das Buch ":bookName" verschoben.',
|
||||
'chapters_permissions' => 'Kapitel-Berechtigungen',
|
||||
'chapters_empty' => 'Aktuell sind keine Kapitel in diesem Buch angelegt.',
|
||||
'chapters_empty' => 'Aktuell sind keine Kapitel diesem Buch hinzugefügt worden.',
|
||||
'chapters_permissions_active' => 'Kapitel-Berechtigungen aktiv',
|
||||
'chapters_permissions_success' => 'Kapitel-Berechtigungenen aktualisisert',
|
||||
'chapters_search_this' => 'Dieses Kapitel durchsuchen',
|
||||
|
||||
/**
|
||||
* Pages
|
||||
*/
|
||||
'page' => 'Seite',
|
||||
'pages' => 'Seiten',
|
||||
'pages_popular' => 'Populäre Seiten',
|
||||
'x_pages' => ':count Seite|:count Seiten',
|
||||
'pages_popular' => 'Beliebte Seiten',
|
||||
'pages_new' => 'Neue Seite',
|
||||
'pages_attachments' => 'Anhänge',
|
||||
'pages_attachments' => 'Anhänge',
|
||||
'pages_navigation' => 'Seitennavigation',
|
||||
'pages_delete' => 'Seite löschen',
|
||||
'pages_delete_named' => 'Seite :pageName löschen',
|
||||
'pages_delete_draft_named' => 'Seitenentwurf von :pageName löschen',
|
||||
'pages_delete_draft' => 'Seitenentwurf löschen',
|
||||
'pages_delete_success' => 'Seite gelöscht',
|
||||
'pages_delete_draft_success' => 'Seitenentwurf gelöscht',
|
||||
'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite löschen möchen?',
|
||||
'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf löschen möchten?',
|
||||
'pages_editing_named' => 'Seite :pageName bearbeiten',
|
||||
'pages_edit_toggle_header' => 'Toggle header',
|
||||
'pages_delete' => 'Seite löschen',
|
||||
'pages_delete_named' => 'Seite ":pageName" löschen',
|
||||
'pages_delete_draft_named' => 'Seitenentwurf von ":pageName" löschen',
|
||||
'pages_delete_draft' => 'Seitenentwurf löschen',
|
||||
'pages_delete_success' => 'Seite gelöscht',
|
||||
'pages_delete_draft_success' => 'Seitenentwurf gelöscht',
|
||||
'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite löschen möchen?',
|
||||
'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf löschen möchten?',
|
||||
'pages_editing_named' => 'Seite ":pageName" bearbeiten',
|
||||
'pages_edit_toggle_header' => 'Hauptmenü anzeigen/verstecken',
|
||||
'pages_edit_save_draft' => 'Entwurf speichern',
|
||||
'pages_edit_draft' => 'Seitenentwurf bearbeiten',
|
||||
'pages_editing_draft' => 'Seitenentwurf bearbeiten',
|
||||
'pages_editing_page' => 'Seite bearbeiten',
|
||||
'pages_edit_draft_save_at' => 'Entwurf gespeichert um ',
|
||||
'pages_edit_delete_draft' => 'Entwurf löschen',
|
||||
'pages_edit_delete_draft' => 'Entwurf löschen',
|
||||
'pages_edit_discard_draft' => 'Entwurf verwerfen',
|
||||
'pages_edit_set_changelog' => 'Veränderungshinweis setzen',
|
||||
'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer Änderungen ein',
|
||||
'pages_edit_enter_changelog' => 'Veränderungshinweis eingeben',
|
||||
'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen',
|
||||
'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer Änderungen ein',
|
||||
'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben',
|
||||
'pages_save' => 'Seite speichern',
|
||||
'pages_title' => 'Seitentitel',
|
||||
'pages_name' => 'Seitenname',
|
||||
'pages_md_editor' => 'Redakteur',
|
||||
'pages_md_preview' => 'Vorschau',
|
||||
'pages_md_insert_image' => 'Bild einfügen',
|
||||
'pages_md_insert_link' => 'Link zu einem Objekt einfügen',
|
||||
'pages_md_insert_image' => 'Bild einfügen',
|
||||
'pages_md_insert_link' => 'Link zu einem Objekt einfügen',
|
||||
'pages_not_in_chapter' => 'Seite ist in keinem Kapitel',
|
||||
'pages_move' => 'Seite verschieben',
|
||||
'pages_move_success' => 'Seite nach ":parentName" verschoben',
|
||||
'pages_permissions' => 'Seiten Berechtigungen',
|
||||
'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert',
|
||||
'pages_revision' => 'Version',
|
||||
'pages_revisions' => 'Seitenversionen',
|
||||
'pages_revisions_named' => 'Seitenversionen von :pageName',
|
||||
'pages_revision_named' => 'Seitenversion von :pageName',
|
||||
'pages_revisions_created_by' => 'Angelegt von',
|
||||
'pages_revisions_named' => 'Seitenversionen von ":pageName"',
|
||||
'pages_revision_named' => 'Seitenversion von ":pageName"',
|
||||
'pages_revisions_created_by' => 'Erstellt von',
|
||||
'pages_revisions_date' => 'Versionsdatum',
|
||||
'pages_revisions_changelog' => 'Veränderungshinweise',
|
||||
'pages_revisions_changes' => 'Veränderungen',
|
||||
'pages_revisions_number' => '#',
|
||||
'pages_revisions_changelog' => 'Änderungsprotokoll',
|
||||
'pages_revisions_changes' => 'Änderungen',
|
||||
'pages_revisions_current' => 'Aktuelle Version',
|
||||
'pages_revisions_preview' => 'Vorschau',
|
||||
'pages_revisions_restore' => 'Zurück sichern',
|
||||
'pages_revisions_none' => 'Diese Seite hat keine älteren Versionen.',
|
||||
'pages_revisions_restore' => 'Wiederherstellen',
|
||||
'pages_revisions_none' => 'Diese Seite hat keine älteren Versionen.',
|
||||
'pages_copy_link' => 'Link kopieren',
|
||||
'pages_permissions_active' => 'Seiten-Berechtigungen aktiv',
|
||||
'pages_initial_revision' => 'Erste Veröffentlichung',
|
||||
'pages_initial_revision' => 'Erste Veröffentlichung',
|
||||
'pages_initial_name' => 'Neue Seite',
|
||||
'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt um :timeDiff gespeichert wurde.',
|
||||
'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
|
||||
'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt :timeDiff gespeichert wurde.',
|
||||
'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count Benutzer haben die Bearbeitung dieser Seite begonnen.',
|
||||
'start_b' => ':userName hat die Bearbeitung dieser Seite begonnen.',
|
||||
'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.',
|
||||
'start_b' => ':userName bearbeitet jetzt diese Seite.',
|
||||
'time_a' => 'seit die Seiten zuletzt aktualisiert wurden.',
|
||||
'time_b' => 'in den letzten :minCount Minuten',
|
||||
'message' => ':start :time. Achten Sie darauf keine Aktualisierungen von anderen Benutzern zu überschreiben!',
|
||||
'message' => ':start :time. Achten Sie darauf, keine Änderungen von anderen Benutzern zu überschreiben!',
|
||||
],
|
||||
'pages_draft_discarded' => 'Entwurf verworfen. Der aktuelle Seiteninhalt wurde geladen.',
|
||||
|
||||
/**
|
||||
* Editor sidebar
|
||||
*/
|
||||
'page_tags' => 'Seiten-Schlagwörter',
|
||||
'page_tags' => 'Seiten-Schlagwörter',
|
||||
'tag' => 'Schlagwort',
|
||||
'tags' => 'Schlagworte',
|
||||
'tag_value' => 'Schlagwortinhalt (Optional)',
|
||||
'tags_explain' => "Fügen Sie Schlagworte hinzu, um Ihren Inhalt zu kategorisieren. \n Sie können einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.",
|
||||
'tags_add' => 'Weiteres Schlagwort hinzufügen',
|
||||
'attachments' => 'Anhänge',
|
||||
'attachments_explain' => 'Sie können auf Ihrer Seite Dateien hochladen oder Links anfügen. Diese werden in der seitlich angezeigt.',
|
||||
'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.',
|
||||
'attachments_items' => 'Angefügte Elemente',
|
||||
'tags' => 'Schlagwörter',
|
||||
'tag_value' => 'Inhalt (Optional)',
|
||||
'tags_explain' => "Fügen Sie Schlagwörter hinzu, um Ihren Inhalt zu kategorisieren.\nSie können einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.",
|
||||
'tags_add' => 'Weiteres Schlagwort hinzufügen',
|
||||
'attachments' => 'Anhänge',
|
||||
'attachments_explain' => 'Sie können auf Ihrer Seite Dateien hochladen oder Links hinzufügen. Diese werden in der Seitenleiste angezeigt.',
|
||||
'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.',
|
||||
'attachments_items' => 'Angefügte Elemente',
|
||||
'attachments_upload' => 'Datei hochladen',
|
||||
'attachments_link' => 'Link anfügen',
|
||||
'attachments_link' => 'Link hinzufügen',
|
||||
'attachments_set_link' => 'Link setzen',
|
||||
'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ählen',
|
||||
'attachments_delete_confirm' => 'Klicken Sie erneut auf löschen, um diesen Anhang zu entfernen.',
|
||||
'attachments_dropzone' => 'Ziehen Sie Dateien hierher oder klicken Sie, um eine Datei auszuwählen',
|
||||
'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.',
|
||||
'attachments_explain_link' => 'Wenn Sie keine Datei hochladen möchten, können Sie stattdessen einen Link anfügen. Dieser Link kann auf eine andere Seite oder zu einer Datei in der Cloud weisen.',
|
||||
'attachments_explain_link' => 'Wenn Sie keine Datei hochladen möchten, können Sie stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet weisen.',
|
||||
'attachments_link_name' => 'Link-Name',
|
||||
'attachment_link' => 'Link zum Anhang',
|
||||
'attachments_link_url' => 'Link zu einer Datei',
|
||||
'attachments_link_url_hint' => 'URL einer Seite oder Datei',
|
||||
'attach' => 'anfügen',
|
||||
'attach' => 'Hinzufügen',
|
||||
'attachments_edit_file' => 'Datei bearbeiten',
|
||||
'attachments_edit_file_name' => 'Dateiname',
|
||||
'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hier hinein, um diese hochzuladen und zu überschreiben',
|
||||
'attachments_order_updated' => 'Reihenfolge der Anhänge aktualisiert',
|
||||
'attachments_updated_success' => 'Anhang-Details aktualisiert',
|
||||
'attachments_deleted' => 'Anhang gelöscht',
|
||||
'attachments_file_uploaded' => 'Datei erfolgrecich hochgeladen',
|
||||
'attachments_file_updated' => 'Datei erfolgreich aktualisisert',
|
||||
'attachments_link_attached' => 'Link erfolgreich der Seite hinzugefügt',
|
||||
'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hierher, um diese hochzuladen und zu überschreiben',
|
||||
'attachments_order_updated' => 'Reihenfolge der Anhänge aktualisiert',
|
||||
'attachments_updated_success' => 'Anhangdetails aktualisiert',
|
||||
'attachments_deleted' => 'Anhang gelöscht',
|
||||
'attachments_file_uploaded' => 'Datei erfolgreich hochgeladen',
|
||||
'attachments_file_updated' => 'Datei erfolgreich aktualisiert',
|
||||
'attachments_link_attached' => 'Link erfolgreich der Seite hinzugefügt',
|
||||
|
||||
/**
|
||||
* Profile View
|
||||
*/
|
||||
'profile_user_for_x' => 'Benutzer seit :time',
|
||||
'profile_created_content' => 'Angelegte Inhalte',
|
||||
'profile_not_created_pages' => ':userName hat bisher keine Seiten angelegt.',
|
||||
'profile_not_created_chapters' => ':userName hat bisher keine Kapitel angelegt.',
|
||||
'profile_not_created_books' => ':userName hat bisher keine Bücher angelegt.',
|
||||
'profile_created_content' => 'Erstellte Inhalte',
|
||||
'profile_not_created_pages' => ':userName hat noch keine Seiten erstellt.',
|
||||
'profile_not_created_chapters' => ':userName hat noch keine Kapitel erstellt.',
|
||||
'profile_not_created_books' => ':userName hat noch keine Bücher erstellt.',
|
||||
|
||||
/**
|
||||
* Comments
|
||||
*/
|
||||
'comment' => 'Kommentar',
|
||||
'comments' => 'Kommentare',
|
||||
'comment_placeholder' => 'Geben Sie hier Ihre Kommentare ein (Markdown unterstützt)',
|
||||
'comment_count' => '{0} Keine Kommentare|{1} 1 Kommentar|[2,*] :count Kommentare',
|
||||
'comment_save' => 'Kommentar speichern',
|
||||
'comment_saving' => 'Kommentar wird gespeichert...',
|
||||
'comment_deleting' => 'Kommentar wird gelöscht...',
|
||||
'comment_new' => 'Neuer Kommentar',
|
||||
'comment_created' => ':createDiff kommentiert',
|
||||
'comment_updated' => ':updateDiff aktualisiert von :username',
|
||||
'comment_deleted_success' => 'Kommentar gelöscht',
|
||||
'comment_created_success' => 'Kommentar hinzugefügt',
|
||||
'comment_updated_success' => 'Kommentar aktualisiert',
|
||||
'comment_delete_confirm' => 'Möchten Sie diesen Kommentar wirklich löschen?',
|
||||
'comment_in_reply_to' => 'Antwort auf :commentId',
|
||||
];
|
|
@ -7,37 +7,37 @@ return [
|
|||
*/
|
||||
|
||||
// Pages
|
||||
'permission' => 'Sie haben keine Berechtigung auf diese Seite zuzugreifen.',
|
||||
'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuführen.',
|
||||
'permission' => 'Sie haben keine Berechtigung, auf diese Seite zuzugreifen.',
|
||||
'permissionJson' => 'Sie haben keine Berechtigung, die angeforderte Aktion auszuführen.',
|
||||
|
||||
// Auth
|
||||
'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten angelegt.',
|
||||
'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melden Sie sich an.',
|
||||
'email_confirmation_invalid' => 'Der Bestätigungs-Token ist nicht gültig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.',
|
||||
'email_confirmation_expired' => 'Der Bestätigungs-Token ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.',
|
||||
'ldap_fail_anonymous' => 'Anonymer LDAP Zugriff ist fehlgeschlafgen',
|
||||
'ldap_fail_authed' => 'LDAP Zugriff mit DN & Passwort ist fehlgeschlagen',
|
||||
'ldap_extension_not_installed' => 'LDAP PHP Erweiterung ist nicht installiert.',
|
||||
'ldap_cannot_connect' => 'Die Verbindung zu LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.',
|
||||
'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten registriert.',
|
||||
'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melden Sie sich an.',
|
||||
'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.',
|
||||
'email_confirmation_expired' => 'Der Bestätigungslink ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.',
|
||||
'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlafgen',
|
||||
'ldap_fail_authed' => 'LDAP-Zugriff mit DN und Passwort ist fehlgeschlagen',
|
||||
'ldap_extension_not_installed' => 'LDAP-PHP-Erweiterung ist nicht installiert.',
|
||||
'ldap_cannot_connect' => 'Die Verbindung zum LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.',
|
||||
'social_no_action_defined' => 'Es ist keine Aktion definiert',
|
||||
'social_account_in_use' => 'Dieses :socialAccount Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount Konto an.',
|
||||
'social_account_email_in_use' => 'Die E-Mail-Adresse :email ist bereits registriert. Wenn Sie bereits registriert sind, können Sie Ihr :socialAccount Konto in Ihren Profil-Einstellungen verknüpfen.',
|
||||
'social_account_existing' => 'Dieses :socialAccount Konto ist bereits mit Ihrem Profil verknüpft.',
|
||||
'social_account_already_used_existing' => 'Dieses :socialAccount Konto wird bereits durch einen anderen Benutzer verwendet.',
|
||||
'social_account_not_used' => 'Dieses :socialAccount Konto ist bisher keinem Benutzer zugeordnet. Bitte verknüpfen Sie deses in Ihrem Profil-Einstellungen.',
|
||||
'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_configured' => 'Ihr :socialAccount Konto ist nicht korrekt konfiguriert.',
|
||||
'social_account_in_use' => 'Dieses :socialAccount-Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount-Konto an.',
|
||||
'social_account_email_in_use' => 'Die E-Mail-Adresse ":email" ist bereits registriert. Wenn Sie bereits registriert sind, können Sie Ihr :socialAccount-Konto in Ihren Profil-Einstellungen verknüpfen.',
|
||||
'social_account_existing' => 'Dieses :socialAccount-Konto ist bereits mit Ihrem Profil verknüpft.',
|
||||
'social_account_already_used_existing' => 'Dieses :socialAccount-Konto wird bereits von einem anderen Benutzer verwendet.',
|
||||
'social_account_not_used' => 'Dieses :socialAccount-Konto ist bisher keinem Benutzer zugeordnet. Sie können es in Ihren Profil-Einstellung.',
|
||||
'social_account_register_instructions' => 'Wenn Sie bisher keinen Social-Media Konto besitzen, können Sie ein solches Konto mit der :socialAccount Option anlegen.',
|
||||
'social_driver_not_found' => 'Treiber für Social-Media-Konten nicht gefunden',
|
||||
'social_driver_not_configured' => 'Ihr :socialAccount-Konto ist nicht korrekt konfiguriert.',
|
||||
|
||||
// System
|
||||
'path_not_writable' => 'Die Datei kann nicht in den angegebenen Pfad :filePath hochgeladen werden. Stellen Sie sicher, dass dieser Ordner auf dem Server beschreibbar ist.',
|
||||
'cannot_get_image_from_url' => 'Bild konnte nicht von der URL :url geladen werden.',
|
||||
'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfen Sie, ob Sie die GD PHP Erweiterung installiert haben.',
|
||||
'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.',
|
||||
'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfen Sie, ob die GD PHP-Erweiterung installiert ist.',
|
||||
'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.',
|
||||
'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.',
|
||||
|
||||
// Attachments
|
||||
'attachment_page_mismatch' => 'Die Seite stimmt nach dem Hochladen des Anhangs nicht überein.',
|
||||
'attachment_page_mismatch' => 'Die Seite stimmte nach dem Hochladen des Anhangs nicht überein.',
|
||||
|
||||
// Pages
|
||||
'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stellen Sie sicher, dass Sie mit dem Internet verbunden sind, bevor Sie den Entwurf dieser Seite speichern.',
|
||||
|
@ -47,24 +47,31 @@ return [
|
|||
'book_not_found' => 'Buch nicht gefunden',
|
||||
'page_not_found' => 'Seite nicht gefunden',
|
||||
'chapter_not_found' => 'Kapitel nicht gefunden',
|
||||
'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden.',
|
||||
'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.',
|
||||
'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern',
|
||||
'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden.',
|
||||
'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.',
|
||||
'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern',
|
||||
|
||||
// Users
|
||||
'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen.',
|
||||
'users_cannot_delete_guest' => 'Sie können den Gast-Benutzer nicht löschen',
|
||||
'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen.',
|
||||
'users_cannot_delete_guest' => 'Sie können den Gast-Benutzer nicht löschen',
|
||||
|
||||
// Roles
|
||||
'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden.',
|
||||
'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden',
|
||||
'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden solange sie als Standardrolle für neue Registrierungen gesetzt ist',
|
||||
'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden',
|
||||
'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden, solange sie als Standardrolle für neue Registrierungen gesetzt ist',
|
||||
|
||||
// Comments
|
||||
'comment_list' => 'Beim Abrufen der Kommentare ist ein Fehler aufgetreten.',
|
||||
'cannot_add_comment_to_draft' => 'Du kannst keine Kommentare zu einem Entwurf hinzufügen.',
|
||||
'comment_add' => 'Beim Hinzufügen des Kommentars ist ein Fehler aufgetreten.',
|
||||
'comment_delete' => 'Beim Löschen des Kommentars ist ein Fehler aufgetreten.',
|
||||
'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen',
|
||||
|
||||
// Error pages
|
||||
'404_page_not_found' => 'Seite nicht gefunden',
|
||||
'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben wurde nicht gefunden.',
|
||||
'return_home' => 'Zurück zur Startseite',
|
||||
'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben, wurde nicht gefunden.',
|
||||
'return_home' => 'Zurück zur Startseite',
|
||||
'error_occurred' => 'Es ist ein Fehler aufgetreten',
|
||||
'app_down' => ':appName befindet sich aktuell im Wartungsmodus.',
|
||||
'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.',
|
||||
'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.'
|
||||
];
|
||||
|
|
|
@ -14,6 +14,6 @@ return [
|
|||
*/
|
||||
|
||||
'previous' => '« Vorherige',
|
||||
'next' => 'Nächste »',
|
||||
'next' => 'Nächste »',
|
||||
|
||||
];
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue