mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-13 03:13:58 +03:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75915e8a94 | ||
|
|
9bde0ae4ea | ||
|
|
cd7e727f8c | ||
|
|
2329a5cedf | ||
|
|
d1a4ff9308 | ||
|
|
9a3bc27ef4 | ||
|
|
f8315fb9c4 | ||
|
|
bb62dee5a2 | ||
|
|
8acc188e16 | ||
|
|
874386ceab | ||
|
|
9dfbea8bf9 | ||
|
|
c1627a1468 | ||
|
|
576a59a693 | ||
|
|
fec6c65b78 | ||
|
|
f8c046d182 | ||
|
|
9ca2184f09 | ||
|
|
fd449582bd | ||
|
|
621142a46e | ||
|
|
0275d2ad58 | ||
|
|
41f56e659d | ||
|
|
fea5630ea4 | ||
|
|
e3f2bde26d | ||
|
|
756ee0b172 | ||
|
|
c81b63b56f | ||
|
|
70ee28ee13 | ||
|
|
1c9ecc3edd | ||
|
|
9bd5d6a422 | ||
|
|
1e41ccbc7a | ||
|
|
0a402e3c63 | ||
|
|
55759bd22a | ||
|
|
d8e1f52ddd | ||
|
|
baea92b206 | ||
|
|
cfc05c1b22 | ||
|
|
ebf78d49a8 | ||
|
|
4cb4c9e568 | ||
|
|
36f524a354 | ||
|
|
b60d2190ac | ||
|
|
2f8b8c580d | ||
|
|
5187d3fa78 | ||
|
|
9c07741972 | ||
|
|
8fcbe44d3e | ||
|
|
3966fb1df6 | ||
|
|
830614fb19 | ||
|
|
76ae5c7398 | ||
|
|
769935f99e | ||
|
|
6920d6eef1 | ||
|
|
b5cd3bff3c | ||
|
|
ac07cb41b6 | ||
|
|
e8fa58f201 | ||
|
|
1f6994b62c | ||
|
|
47d82a1ac2 | ||
|
|
703d579561 | ||
|
|
ed375bfaf7 | ||
|
|
3da8c01c1f | ||
|
|
295f520f21 | ||
|
|
fba7ae923d | ||
|
|
28a9bd514f | ||
|
|
666c86b108 | ||
|
|
194293664f | ||
|
|
039ee5d06c | ||
|
|
afc66b3c3d | ||
|
|
a04b31866d | ||
|
|
db3dde98ef | ||
|
|
0d6a6b5b63 | ||
|
|
4df3267521 | ||
|
|
d3e4a1a6f9 | ||
|
|
b023699f1b | ||
|
|
f338dbe3f8 | ||
|
|
ab07f7df6c | ||
|
|
a59d73de7b | ||
|
|
1ac7618bb1 | ||
|
|
2a069880cd | ||
|
|
5e5928a8a6 | ||
|
|
d6e87420c3 | ||
|
|
79cfd39fde | ||
|
|
e9831a7507 | ||
|
|
574ee820a9 | ||
|
|
7d02f77e67 | ||
|
|
fd50efb503 | ||
|
|
9dbd7fa618 | ||
|
|
8dab31b87a | ||
|
|
e155c52256 | ||
|
|
c76e7c706c | ||
|
|
552943c033 | ||
|
|
4efe3b41da | ||
|
|
218376a41c | ||
|
|
e647ec22b1 | ||
|
|
38fe756725 | ||
|
|
652a67ad65 | ||
|
|
5bd9da6054 | ||
|
|
7c6fe8c4e2 | ||
|
|
689d1eb082 | ||
|
|
06d75e1804 | ||
|
|
9558f84b97 | ||
|
|
2fd421b115 | ||
|
|
6ff440e677 | ||
|
|
0bda5554dd | ||
|
|
860d4d4be5 | ||
|
|
9a97995f18 | ||
|
|
1a1e71cd60 | ||
|
|
34802ff8a6 | ||
|
|
0ff5aad9c0 | ||
|
|
03e5d61798 | ||
|
|
4f231d1bf0 | ||
|
|
8b82753218 | ||
|
|
3368fe42d8 | ||
|
|
c3ea0d333e | ||
|
|
d447355a61 | ||
|
|
8e2437498f | ||
|
|
9de85283cd | ||
|
|
b3d4c199ae | ||
|
|
4e71a5a47b | ||
|
|
410e967eb1 | ||
|
|
388f2f40dc | ||
|
|
148350009c | ||
|
|
70991fc1e5 | ||
|
|
e5c4e0ac86 | ||
|
|
397db04428 | ||
|
|
cd6572b61a | ||
|
|
581881d0ca | ||
|
|
d2efc2f47f | ||
|
|
4cc73657a1 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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:
|
||||
|
||||
43
app/Comment.php
Normal file
43
app/Comment.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class Comment extends Ownable
|
||||
{
|
||||
protected $fillable = ['text', 'html', 'parent_id'];
|
||||
protected $appends = ['created', 'updated'];
|
||||
|
||||
/**
|
||||
* Get the entity that this comment belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment has been updated since creation.
|
||||
* @return bool
|
||||
*/
|
||||
public function isUpdated()
|
||||
{
|
||||
return $this->updated_at->timestamp > $this->created_at->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get created date as a relative diff.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCreatedAttribute()
|
||||
{
|
||||
return $this->created_at->diffForHumans();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get updated date as a relative diff.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUpdatedAttribute()
|
||||
{
|
||||
return $this->updated_at->diffForHumans();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
93
app/Http/Controllers/CommentController.php
Normal file
93
app/Http/Controllers/CommentController.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\CommentRepo;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
protected $entityRepo;
|
||||
protected $commentRepo;
|
||||
|
||||
/**
|
||||
* CommentController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param CommentRepo $commentRepo
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->commentRepo = $commentRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new comment for a Page
|
||||
* @param Request $request
|
||||
* @param integer $pageId
|
||||
* @param null|integer $commentId
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function savePageComment(Request $request, $pageId, $commentId = null)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'text' => 'required|string',
|
||||
'html' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return response('Not found', 404);
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
// Prevent adding comments to draft pages
|
||||
if ($page->draft) {
|
||||
return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400);
|
||||
}
|
||||
|
||||
// Create a new comment.
|
||||
$this->checkPermission('comment-create-all');
|
||||
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
|
||||
Activity::add($page, 'commented_on', $page->book->id);
|
||||
return view('comments/comment', ['comment' => $comment]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing comment.
|
||||
* @param Request $request
|
||||
* @param integer $commentId
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function update(Request $request, $commentId)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'text' => 'required|string',
|
||||
'html' => 'required|string',
|
||||
]);
|
||||
|
||||
$comment = $this->commentRepo->getById($commentId);
|
||||
$this->checkOwnablePermission('page-view', $comment->entity);
|
||||
$this->checkOwnablePermission('comment-update', $comment);
|
||||
|
||||
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
|
||||
return view('comments/comment', ['comment' => $comment]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a comment from the system.
|
||||
* @param integer $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$comment = $this->commentRepo->getById($id);
|
||||
$this->checkOwnablePermission('comment-delete', $comment);
|
||||
$this->commentRepo->delete($comment);
|
||||
return response()->json(['message' => trans('entities.comment_deleted')]);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,10 +377,11 @@ class PageController extends Controller
|
||||
|
||||
$page->fill($revision->toArray());
|
||||
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
|
||||
|
||||
|
||||
return view('pages/revision', [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'revision' => $revision
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -409,6 +411,7 @@ class PageController extends Controller
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'diff' => $diff,
|
||||
'revision' => $revision
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,4 +47,16 @@ class PageRevision extends Model
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows checking of the exact class, Used to check entity type.
|
||||
* Included here to align with entities in similar use cases.
|
||||
* (Yup, Bit of an awkward hack)
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isA($type)
|
||||
{
|
||||
return $type === 'revision';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
87
app/Repos/CommentRepo.php
Normal file
87
app/Repos/CommentRepo.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use BookStack\Comment;
|
||||
use BookStack\Entity;
|
||||
|
||||
/**
|
||||
* Class CommentRepo
|
||||
* @package BookStack\Repos
|
||||
*/
|
||||
class CommentRepo {
|
||||
|
||||
/**
|
||||
* @var Comment $comment
|
||||
*/
|
||||
protected $comment;
|
||||
|
||||
/**
|
||||
* CommentRepo constructor.
|
||||
* @param Comment $comment
|
||||
*/
|
||||
public function __construct(Comment $comment)
|
||||
{
|
||||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a comment by ID.
|
||||
* @param $id
|
||||
* @return Comment|\Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->comment->newQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new comment on an entity.
|
||||
* @param Entity $entity
|
||||
* @param array $data
|
||||
* @return Comment
|
||||
*/
|
||||
public function create (Entity $entity, $data = [])
|
||||
{
|
||||
$userId = user()->id;
|
||||
$comment = $this->comment->newInstance($data);
|
||||
$comment->created_by = $userId;
|
||||
$comment->updated_by = $userId;
|
||||
$comment->local_id = $this->getNextLocalId($entity);
|
||||
$entity->comments()->save($comment);
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing comment.
|
||||
* @param Comment $comment
|
||||
* @param array $input
|
||||
* @return mixed
|
||||
*/
|
||||
public function update($comment, $input)
|
||||
{
|
||||
$comment->updated_by = user()->id;
|
||||
$comment->update($input);
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a comment from the system.
|
||||
* @param Comment $comment
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete($comment)
|
||||
{
|
||||
return $comment->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next local ID relative to the linked entity.
|
||||
* @param Entity $entity
|
||||
* @return int
|
||||
*/
|
||||
protected function getNextLocalId(Entity $entity)
|
||||
{
|
||||
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
|
||||
if ($comments === null) return 1;
|
||||
return $comments->local_id + 1;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
2
config/app.php
Normal file → Executable file
2
config/app.php
Normal file → Executable file
@@ -58,7 +58,7 @@ return [
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl'],
|
||||
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl', 'it'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -70,4 +70,14 @@ $factory->define(BookStack\Image::class, function ($faker) {
|
||||
'type' => 'gallery',
|
||||
'uploaded_to' => 0
|
||||
];
|
||||
});
|
||||
|
||||
$factory->define(BookStack\Comment::class, function($faker) {
|
||||
$text = $faker->paragraph(1);
|
||||
$html = '<p>' . $text. '</p>';
|
||||
return [
|
||||
'html' => $html,
|
||||
'text' => $text,
|
||||
'parent_id' => null
|
||||
];
|
||||
});
|
||||
@@ -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,12 +15,11 @@ class DummyContentSeeder extends Seeder
|
||||
$role = \BookStack\Role::getRole('editor');
|
||||
$user->attachRole($role);
|
||||
|
||||
|
||||
factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
|
||||
->each(function($book) use ($user) {
|
||||
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
|
||||
->each(function($chapter) use ($user, $book){
|
||||
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
|
||||
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
|
||||
$chapter->pages()->saveMany($pages);
|
||||
});
|
||||
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
|
||||
@@ -33,7 +32,6 @@ class DummyContentSeeder extends Seeder
|
||||
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
|
||||
$largeBook->pages()->saveMany($pages);
|
||||
$largeBook->chapters()->saveMany($chapters);
|
||||
|
||||
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
|
||||
app(\BookStack\Services\SearchService::class)->indexAllEntities();
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
3
public/css/export-styles.css
vendored
3
public/css/export-styles.css
vendored
File diff suppressed because one or more lines are too long
3
public/css/print-styles.css
vendored
3
public/css/print-styles.css
vendored
@@ -1 +1,2 @@
|
||||
header{display:none}body{font-size:12px}.faded-small{display:none}.page-content{margin:0 auto}.print-hidden{display:none}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}
|
||||
header{display:none}body{font-size:12px}.faded-small{display:none}.page-content{margin:0 auto}.print-hidden{display:none}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}
|
||||
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInByaW50LXN0eWxlcy5zY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUVBLE9BQ0UsUUFBQSxLQUdGLEtBQ0UsVUFBQSxLQUdGLGFBQ0UsUUFBQSxLQUdGLGNBQ0UsT0FBQSxFQUFBLEtBR0YsY0FDRSxRQUFBLEtBR0Ysa0JBQ0UsTUFBQSxLQUNBLE1BQUEsS0FDQSxRQUFBLE1BR0YsR0FDRSxVQUFBLElBQ0EsWUFBQSxFQUNBLFdBQUEsS0FDQSxjQUFBIiwiZmlsZSI6InByaW50LXN0eWxlcy5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyJAaW1wb3J0IFwidmFyaWFibGVzXCI7XG5cbmhlYWRlciB7XG4gIGRpc3BsYXk6IG5vbmU7XG59XG5cbmJvZHkge1xuICBmb250LXNpemU6IDEycHg7XG59XG5cbi5mYWRlZC1zbWFsbCB7XG4gIGRpc3BsYXk6IG5vbmU7XG59XG5cbi5wYWdlLWNvbnRlbnQge1xuICBtYXJnaW46IDAgYXV0bztcbn1cblxuLnByaW50LWhpZGRlbiB7XG4gIGRpc3BsYXk6IG5vbmU7XG59XG5cbi5wcmludC1mdWxsLXdpZHRoIHtcbiAgd2lkdGg6IDEwMCU7XG4gIGZsb2F0OiBub25lO1xuICBkaXNwbGF5OiBibG9jaztcbn1cblxuaDIge1xuICBmb250LXNpemU6IDJlbTtcbiAgbGluZS1oZWlnaHQ6IDE7XG4gIG1hcmdpbi10b3A6IDAuNmVtO1xuICBtYXJnaW4tYm90dG9tOiAwLjNlbTtcbn0iXX0= */
|
||||
|
||||
3
public/css/styles.css
vendored
3
public/css/styles.css
vendored
File diff suppressed because one or more lines are too long
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.
File diff suppressed because one or more lines are too long
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,
|
||||
};
|
||||
53
resources/assets/js/components/back-top-top.js
Normal file
53
resources/assets/js/components/back-top-top.js
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
class BackToTop {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.targetElem = document.getElementById('header');
|
||||
this.showing = false;
|
||||
this.breakPoint = 1200;
|
||||
this.elem.addEventListener('click', this.scrollToTop.bind(this));
|
||||
window.addEventListener('scroll', this.onPageScroll.bind(this));
|
||||
}
|
||||
|
||||
onPageScroll() {
|
||||
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
if (!this.showing && scrollTopPos > this.breakPoint) {
|
||||
this.elem.style.display = 'block';
|
||||
this.showing = true;
|
||||
setTimeout(() => {
|
||||
this.elem.style.opacity = 0.4;
|
||||
}, 1);
|
||||
} else if (this.showing && scrollTopPos < this.breakPoint) {
|
||||
this.elem.style.opacity = 0;
|
||||
this.showing = false;
|
||||
setTimeout(() => {
|
||||
this.elem.style.display = 'none';
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
scrollToTop() {
|
||||
let targetTop = this.targetElem.getBoundingClientRect().top;
|
||||
let scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body;
|
||||
let duration = 300;
|
||||
let start = Date.now();
|
||||
let scrollStart = this.targetElem.getBoundingClientRect().top;
|
||||
|
||||
function setPos() {
|
||||
let percentComplete = (1-((Date.now() - start) / duration));
|
||||
let target = Math.abs(percentComplete * scrollStart);
|
||||
if (percentComplete > 0) {
|
||||
scrollElem.scrollTop = target;
|
||||
requestAnimationFrame(setPos.bind(this));
|
||||
} else {
|
||||
scrollElem.scrollTop = targetTop;
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(setPos.bind(this));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = BackToTop;
|
||||
67
resources/assets/js/components/chapter-toggle.js
Normal file
67
resources/assets/js/components/chapter-toggle.js
Normal file
@@ -0,0 +1,67 @@
|
||||
|
||||
class ChapterToggle {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.isOpen = elem.classList.contains('open');
|
||||
elem.addEventListener('click', this.click.bind(this));
|
||||
}
|
||||
|
||||
open() {
|
||||
let list = this.elem.parentNode.querySelector('.inset-list');
|
||||
|
||||
this.elem.classList.add('open');
|
||||
list.style.display = 'block';
|
||||
list.style.height = '';
|
||||
let height = list.getBoundingClientRect().height;
|
||||
list.style.height = '0px';
|
||||
list.style.overflow = 'hidden';
|
||||
list.style.transition = 'height ease-in-out 240ms';
|
||||
|
||||
let transitionEndBound = onTransitionEnd.bind(this);
|
||||
function onTransitionEnd() {
|
||||
list.style.overflow = '';
|
||||
list.style.height = '';
|
||||
list.style.transition = '';
|
||||
list.removeEventListener('transitionend', transitionEndBound);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
list.style.height = `${height}px`;
|
||||
list.addEventListener('transitionend', transitionEndBound)
|
||||
}, 1);
|
||||
}
|
||||
|
||||
close() {
|
||||
let list = this.elem.parentNode.querySelector('.inset-list');
|
||||
|
||||
this.elem.classList.remove('open');
|
||||
list.style.display = 'block';
|
||||
list.style.height = list.getBoundingClientRect().height + 'px';
|
||||
list.style.overflow = 'hidden';
|
||||
list.style.transition = 'height ease-in-out 240ms';
|
||||
|
||||
let transitionEndBound = onTransitionEnd.bind(this);
|
||||
function onTransitionEnd() {
|
||||
list.style.overflow = '';
|
||||
list.style.height = '';
|
||||
list.style.transition = '';
|
||||
list.style.display = 'none';
|
||||
list.removeEventListener('transitionend', transitionEndBound);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
list.style.height = `0px`;
|
||||
list.addEventListener('transitionend', transitionEndBound)
|
||||
}, 1);
|
||||
}
|
||||
|
||||
click(event) {
|
||||
event.preventDefault();
|
||||
this.isOpen ? this.close() : this.open();
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ChapterToggle;
|
||||
48
resources/assets/js/components/dropdown.js
Normal file
48
resources/assets/js/components/dropdown.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Dropdown
|
||||
* Provides some simple logic to create simple dropdown menus.
|
||||
*/
|
||||
class DropDown {
|
||||
|
||||
constructor(elem) {
|
||||
this.container = elem;
|
||||
this.menu = elem.querySelector('ul');
|
||||
this.toggle = elem.querySelector('[dropdown-toggle]');
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.menu.style.display = 'block';
|
||||
this.menu.classList.add('anim', 'menuIn');
|
||||
this.container.addEventListener('mouseleave', this.hide.bind(this));
|
||||
|
||||
// Focus on first input if existing
|
||||
let input = this.menu.querySelector('input');
|
||||
if (input !== null) input.focus();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.menu.style.display = 'none';
|
||||
this.menu.classList.remove('anim', 'menuIn');
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
// Hide menu on option click
|
||||
this.container.addEventListener('click', event => {
|
||||
let possibleChildren = Array.from(this.menu.querySelectorAll('a'));
|
||||
if (possibleChildren.indexOf(event.target) !== -1) this.hide();
|
||||
});
|
||||
// Show dropdown on toggle click
|
||||
this.toggle.addEventListener('click', this.show.bind(this));
|
||||
// Hide menu on enter press
|
||||
this.container.addEventListener('keypress', event => {
|
||||
if (event.keyCode !== 13) return true;
|
||||
event.preventDefault();
|
||||
this.hide();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = DropDown;
|
||||
47
resources/assets/js/components/entity-selector-popup.js
Normal file
47
resources/assets/js/components/entity-selector-popup.js
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
class EntitySelectorPopup {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
window.EntitySelectorPopup = this;
|
||||
|
||||
this.callback = null;
|
||||
this.selection = null;
|
||||
|
||||
this.selectButton = elem.querySelector('.entity-link-selector-confirm');
|
||||
this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this));
|
||||
|
||||
window.$events.listen('entity-select-change', this.onSelectionChange.bind(this));
|
||||
window.$events.listen('entity-select-confirm', this.onSelectionConfirm.bind(this));
|
||||
}
|
||||
|
||||
show(callback) {
|
||||
this.callback = callback;
|
||||
this.elem.components.overlay.show();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.elem.components.overlay.hide();
|
||||
}
|
||||
|
||||
onSelectButtonClick() {
|
||||
this.hide();
|
||||
if (this.selection !== null && this.callback) this.callback(this.selection);
|
||||
}
|
||||
|
||||
onSelectionConfirm(entity) {
|
||||
this.hide();
|
||||
if (this.callback && entity) this.callback(entity);
|
||||
}
|
||||
|
||||
onSelectionChange(entity) {
|
||||
this.selection = entity;
|
||||
if (entity === null) {
|
||||
this.selectButton.setAttribute('disabled', 'true');
|
||||
} else {
|
||||
this.selectButton.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EntitySelectorPopup;
|
||||
118
resources/assets/js/components/entity-selector.js
Normal file
118
resources/assets/js/components/entity-selector.js
Normal file
@@ -0,0 +1,118 @@
|
||||
|
||||
class EntitySelector {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.search = '';
|
||||
this.lastClick = 0;
|
||||
|
||||
let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
|
||||
this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}`);
|
||||
|
||||
this.input = elem.querySelector('[entity-selector-input]');
|
||||
this.searchInput = elem.querySelector('[entity-selector-search]');
|
||||
this.loading = elem.querySelector('[entity-selector-loading]');
|
||||
this.resultsContainer = elem.querySelector('[entity-selector-results]');
|
||||
|
||||
this.elem.addEventListener('click', this.onClick.bind(this));
|
||||
|
||||
let lastSearch = 0;
|
||||
this.searchInput.addEventListener('input', event => {
|
||||
lastSearch = Date.now();
|
||||
this.showLoading();
|
||||
setTimeout(() => {
|
||||
if (Date.now() - lastSearch < 199) return;
|
||||
this.searchEntities(this.searchInput.value);
|
||||
}, 200);
|
||||
});
|
||||
this.searchInput.addEventListener('keydown', event => {
|
||||
if (event.keyCode === 13) event.preventDefault();
|
||||
});
|
||||
|
||||
this.showLoading();
|
||||
this.initialLoad();
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
this.loading.style.display = 'block';
|
||||
this.resultsContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
this.loading.style.display = 'none';
|
||||
this.resultsContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
initialLoad() {
|
||||
window.$http.get(this.searchUrl).then(resp => {
|
||||
this.resultsContainer.innerHTML = resp.data;
|
||||
this.hideLoading();
|
||||
})
|
||||
}
|
||||
|
||||
searchEntities(searchTerm) {
|
||||
this.input.value = '';
|
||||
let url = this.searchUrl + `&term=${encodeURIComponent(searchTerm)}`;
|
||||
window.$http.get(url).then(resp => {
|
||||
this.resultsContainer.innerHTML = resp.data;
|
||||
this.hideLoading();
|
||||
});
|
||||
}
|
||||
|
||||
isDoubleClick() {
|
||||
let now = Date.now();
|
||||
let answer = now - this.lastClick < 300;
|
||||
this.lastClick = now;
|
||||
return answer;
|
||||
}
|
||||
|
||||
onClick(event) {
|
||||
let t = event.target;
|
||||
console.log('click', t);
|
||||
|
||||
if (t.matches('.entity-list-item *')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let item = t.closest('[data-entity-type]');
|
||||
this.selectItem(item);
|
||||
} else if (t.matches('[data-entity-type]')) {
|
||||
this.selectItem(t)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
selectItem(item) {
|
||||
let isDblClick = this.isDoubleClick();
|
||||
let type = item.getAttribute('data-entity-type');
|
||||
let id = item.getAttribute('data-entity-id');
|
||||
let isSelected = !item.classList.contains('selected') || isDblClick;
|
||||
|
||||
this.unselectAll();
|
||||
this.input.value = isSelected ? `${type}:${id}` : '';
|
||||
|
||||
if (!isSelected) window.$events.emit('entity-select-change', null);
|
||||
if (isSelected) {
|
||||
item.classList.add('selected');
|
||||
item.classList.add('primary-background');
|
||||
}
|
||||
if (!isDblClick && !isSelected) return;
|
||||
|
||||
let link = item.querySelector('.entity-list-item-link').getAttribute('href');
|
||||
let name = item.querySelector('.entity-list-item-name').textContent;
|
||||
let data = {id: Number(id), name: name, link: link};
|
||||
|
||||
if (isDblClick) window.$events.emit('entity-select-confirm', data);
|
||||
if (isSelected) window.$events.emit('entity-select-change', data);
|
||||
}
|
||||
|
||||
unselectAll() {
|
||||
let selected = this.elem.querySelectorAll('.selected');
|
||||
for (let i = 0, len = selected.length; i < len; i++) {
|
||||
selected[i].classList.remove('selected');
|
||||
selected[i].classList.remove('primary-background');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = EntitySelector;
|
||||
65
resources/assets/js/components/expand-toggle.js
Normal file
65
resources/assets/js/components/expand-toggle.js
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
class ExpandToggle {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.isOpen = false;
|
||||
this.selector = elem.getAttribute('expand-toggle');
|
||||
elem.addEventListener('click', this.click.bind(this));
|
||||
}
|
||||
|
||||
open(elemToToggle) {
|
||||
elemToToggle.style.display = 'block';
|
||||
elemToToggle.style.height = '';
|
||||
let height = elemToToggle.getBoundingClientRect().height;
|
||||
elemToToggle.style.height = '0px';
|
||||
elemToToggle.style.overflow = 'hidden';
|
||||
elemToToggle.style.transition = 'height ease-in-out 240ms';
|
||||
|
||||
let transitionEndBound = onTransitionEnd.bind(this);
|
||||
function onTransitionEnd() {
|
||||
elemToToggle.style.overflow = '';
|
||||
elemToToggle.style.height = '';
|
||||
elemToToggle.style.transition = '';
|
||||
elemToToggle.removeEventListener('transitionend', transitionEndBound);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
elemToToggle.style.height = `${height}px`;
|
||||
elemToToggle.addEventListener('transitionend', transitionEndBound)
|
||||
}, 1);
|
||||
}
|
||||
|
||||
close(elemToToggle) {
|
||||
elemToToggle.style.display = 'block';
|
||||
elemToToggle.style.height = elemToToggle.getBoundingClientRect().height + 'px';
|
||||
elemToToggle.style.overflow = 'hidden';
|
||||
elemToToggle.style.transition = 'all ease-in-out 240ms';
|
||||
|
||||
let transitionEndBound = onTransitionEnd.bind(this);
|
||||
function onTransitionEnd() {
|
||||
elemToToggle.style.overflow = '';
|
||||
elemToToggle.style.height = '';
|
||||
elemToToggle.style.transition = '';
|
||||
elemToToggle.style.display = 'none';
|
||||
elemToToggle.removeEventListener('transitionend', transitionEndBound);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
elemToToggle.style.height = `0px`;
|
||||
elemToToggle.addEventListener('transitionend', transitionEndBound)
|
||||
}, 1);
|
||||
}
|
||||
|
||||
click(event) {
|
||||
event.preventDefault();
|
||||
let matchingElems = document.querySelectorAll(this.selector);
|
||||
for (let i = 0, len = matchingElems.length; i < len; i++) {
|
||||
this.isOpen ? this.close(matchingElems[i]) : this.open(matchingElems[i]);
|
||||
}
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ExpandToggle;
|
||||
51
resources/assets/js/components/index.js
Normal file
51
resources/assets/js/components/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
let componentMapping = {
|
||||
'dropdown': require('./dropdown'),
|
||||
'overlay': require('./overlay'),
|
||||
'back-to-top': require('./back-top-top'),
|
||||
'notification': require('./notification'),
|
||||
'chapter-toggle': require('./chapter-toggle'),
|
||||
'expand-toggle': require('./expand-toggle'),
|
||||
'entity-selector-popup': require('./entity-selector-popup'),
|
||||
'entity-selector': require('./entity-selector'),
|
||||
'sidebar': require('./sidebar'),
|
||||
'page-picker': require('./page-picker'),
|
||||
'page-comments': require('./page-comments'),
|
||||
};
|
||||
|
||||
window.components = {};
|
||||
|
||||
let componentNames = Object.keys(componentMapping);
|
||||
initAll();
|
||||
|
||||
/**
|
||||
* Initialize components of the given name within the given element.
|
||||
* @param {String} componentName
|
||||
* @param {HTMLElement|Document} parentElement
|
||||
*/
|
||||
function initComponent(componentName, parentElement) {
|
||||
let elems = parentElement.querySelectorAll(`[${componentName}]`);
|
||||
if (elems.length === 0) return;
|
||||
|
||||
let component = componentMapping[componentName];
|
||||
if (typeof window.components[componentName] === "undefined") window.components[componentName] = [];
|
||||
for (let j = 0, jLen = elems.length; j < jLen; j++) {
|
||||
let instance = new component(elems[j]);
|
||||
if (typeof elems[j].components === 'undefined') elems[j].components = {};
|
||||
elems[j].components[componentName] = instance;
|
||||
window.components[componentName].push(instance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all components found within the given element.
|
||||
* @param parentElement
|
||||
*/
|
||||
function initAll(parentElement) {
|
||||
if (typeof parentElement === 'undefined') parentElement = document;
|
||||
for (let i = 0, len = componentNames.length; i < len; i++) {
|
||||
initComponent(componentNames[i], parentElement);
|
||||
}
|
||||
}
|
||||
|
||||
window.components.init = initAll;
|
||||
41
resources/assets/js/components/notification.js
Normal file
41
resources/assets/js/components/notification.js
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
class Notification {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.type = elem.getAttribute('notification');
|
||||
this.textElem = elem.querySelector('span');
|
||||
this.autohide = this.elem.hasAttribute('data-autohide');
|
||||
window.$events.listen(this.type, text => {
|
||||
this.show(text);
|
||||
});
|
||||
elem.addEventListener('click', this.hide.bind(this));
|
||||
if (elem.hasAttribute('data-show')) this.show(this.textElem.textContent);
|
||||
|
||||
this.hideCleanup = this.hideCleanup.bind(this);
|
||||
}
|
||||
|
||||
show(textToShow = '') {
|
||||
this.elem.removeEventListener('transitionend', this.hideCleanup);
|
||||
this.textElem.textContent = textToShow;
|
||||
this.elem.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
this.elem.classList.add('showing');
|
||||
}, 1);
|
||||
|
||||
if (this.autohide) setTimeout(this.hide.bind(this), 2000);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.elem.classList.remove('showing');
|
||||
this.elem.addEventListener('transitionend', this.hideCleanup);
|
||||
}
|
||||
|
||||
hideCleanup() {
|
||||
this.elem.style.display = 'none';
|
||||
this.elem.removeEventListener('transitionend', this.hideCleanup);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Notification;
|
||||
39
resources/assets/js/components/overlay.js
Normal file
39
resources/assets/js/components/overlay.js
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
class Overlay {
|
||||
|
||||
constructor(elem) {
|
||||
this.container = elem;
|
||||
elem.addEventListener('click', event => {
|
||||
if (event.target === elem) return this.hide();
|
||||
});
|
||||
let closeButtons = elem.querySelectorAll('.overlay-close');
|
||||
for (let i=0; i < closeButtons.length; i++) {
|
||||
closeButtons[i].addEventListener('click', this.hide.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
toggle(show = true) {
|
||||
let start = Date.now();
|
||||
let duration = 240;
|
||||
|
||||
function setOpacity() {
|
||||
let elapsedTime = (Date.now() - start);
|
||||
let targetOpacity = show ? (elapsedTime / duration) : 1-(elapsedTime / duration);
|
||||
this.container.style.opacity = targetOpacity;
|
||||
if (elapsedTime > duration) {
|
||||
this.container.style.display = show ? 'flex' : 'none';
|
||||
this.container.style.opacity = '';
|
||||
} else {
|
||||
requestAnimationFrame(setOpacity.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(setOpacity.bind(this));
|
||||
}
|
||||
|
||||
hide() { this.toggle(false); }
|
||||
show() { this.toggle(true); }
|
||||
|
||||
}
|
||||
|
||||
module.exports = Overlay;
|
||||
175
resources/assets/js/components/page-comments.js
Normal file
175
resources/assets/js/components/page-comments.js
Normal file
@@ -0,0 +1,175 @@
|
||||
const MarkdownIt = require("markdown-it");
|
||||
const md = new MarkdownIt({ html: false });
|
||||
|
||||
class PageComments {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.pageId = Number(elem.getAttribute('page-id'));
|
||||
this.editingComment = null;
|
||||
this.parentId = null;
|
||||
|
||||
this.container = elem.querySelector('[comment-container]');
|
||||
this.formContainer = elem.querySelector('[comment-form-container]');
|
||||
|
||||
if (this.formContainer) {
|
||||
this.form = this.formContainer.querySelector('form');
|
||||
this.formInput = this.form.querySelector('textarea');
|
||||
this.form.addEventListener('submit', this.saveComment.bind(this));
|
||||
}
|
||||
|
||||
this.elem.addEventListener('click', this.handleAction.bind(this));
|
||||
this.elem.addEventListener('submit', this.updateComment.bind(this));
|
||||
}
|
||||
|
||||
handleAction(event) {
|
||||
let actionElem = event.target.closest('[action]');
|
||||
if (event.target.matches('a[href^="#"]')) {
|
||||
let id = event.target.href.split('#')[1];
|
||||
window.scrollAndHighlight(document.querySelector('#' + id));
|
||||
}
|
||||
if (actionElem === null) return;
|
||||
event.preventDefault();
|
||||
|
||||
let action = actionElem.getAttribute('action');
|
||||
if (action === 'edit') this.editComment(actionElem.closest('[comment]'));
|
||||
if (action === 'closeUpdateForm') this.closeUpdateForm();
|
||||
if (action === 'delete') this.deleteComment(actionElem.closest('[comment]'));
|
||||
if (action === 'addComment') this.showForm();
|
||||
if (action === 'hideForm') this.hideForm();
|
||||
if (action === 'reply') this.setReply(actionElem.closest('[comment]'));
|
||||
if (action === 'remove-reply-to') this.removeReplyTo();
|
||||
}
|
||||
|
||||
closeUpdateForm() {
|
||||
if (!this.editingComment) return;
|
||||
this.editingComment.querySelector('[comment-content]').style.display = 'block';
|
||||
this.editingComment.querySelector('[comment-edit-container]').style.display = 'none';
|
||||
}
|
||||
|
||||
editComment(commentElem) {
|
||||
this.hideForm();
|
||||
if (this.editingComment) this.closeUpdateForm();
|
||||
commentElem.querySelector('[comment-content]').style.display = 'none';
|
||||
commentElem.querySelector('[comment-edit-container]').style.display = 'block';
|
||||
let textArea = commentElem.querySelector('[comment-edit-container] textarea');
|
||||
let lineCount = textArea.value.split('\n').length;
|
||||
textArea.style.height = (lineCount * 20) + 'px';
|
||||
this.editingComment = commentElem;
|
||||
}
|
||||
|
||||
updateComment(event) {
|
||||
let form = event.target;
|
||||
event.preventDefault();
|
||||
let text = form.querySelector('textarea').value;
|
||||
let reqData = {
|
||||
text: text,
|
||||
html: md.render(text),
|
||||
parent_id: this.parentId || null,
|
||||
};
|
||||
this.showLoading(form);
|
||||
let commentId = this.editingComment.getAttribute('comment');
|
||||
window.$http.put(window.baseUrl(`/ajax/comment/${commentId}`), reqData).then(resp => {
|
||||
let newComment = document.createElement('div');
|
||||
newComment.innerHTML = resp.data;
|
||||
this.editingComment.innerHTML = newComment.children[0].innerHTML;
|
||||
window.$events.emit('success', window.trans('entities.comment_updated_success'));
|
||||
window.components.init(this.editingComment);
|
||||
this.closeUpdateForm();
|
||||
this.editingComment = null;
|
||||
this.hideLoading(form);
|
||||
});
|
||||
}
|
||||
|
||||
deleteComment(commentElem) {
|
||||
let id = commentElem.getAttribute('comment');
|
||||
this.showLoading(commentElem.querySelector('[comment-content]'));
|
||||
window.$http.delete(window.baseUrl(`/ajax/comment/${id}`)).then(resp => {
|
||||
commentElem.parentNode.removeChild(commentElem);
|
||||
window.$events.emit('success', window.trans('entities.comment_deleted_success'));
|
||||
this.updateCount();
|
||||
});
|
||||
}
|
||||
|
||||
saveComment(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let text = this.formInput.value;
|
||||
let reqData = {
|
||||
text: text,
|
||||
html: md.render(text),
|
||||
parent_id: this.parentId || null,
|
||||
};
|
||||
this.showLoading(this.form);
|
||||
window.$http.post(window.baseUrl(`/ajax/page/${this.pageId}/comment`), reqData).then(resp => {
|
||||
let newComment = document.createElement('div');
|
||||
newComment.innerHTML = resp.data;
|
||||
let newElem = newComment.children[0];
|
||||
this.container.appendChild(newElem);
|
||||
window.components.init(newElem);
|
||||
window.$events.emit('success', window.trans('entities.comment_created_success'));
|
||||
this.resetForm();
|
||||
this.updateCount();
|
||||
});
|
||||
}
|
||||
|
||||
updateCount() {
|
||||
let count = this.container.children.length;
|
||||
this.elem.querySelector('[comments-title]').textContent = window.trans_choice('entities.comment_count', count, {count});
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
this.formInput.value = '';
|
||||
this.formContainer.appendChild(this.form);
|
||||
this.hideForm();
|
||||
this.removeReplyTo();
|
||||
this.hideLoading(this.form);
|
||||
}
|
||||
|
||||
showForm() {
|
||||
this.formContainer.style.display = 'block';
|
||||
this.formContainer.parentNode.style.display = 'block';
|
||||
this.elem.querySelector('[comment-add-button]').style.display = 'none';
|
||||
this.formInput.focus();
|
||||
window.scrollToElement(this.formInput);
|
||||
}
|
||||
|
||||
hideForm() {
|
||||
this.formContainer.style.display = 'none';
|
||||
this.formContainer.parentNode.style.display = 'none';
|
||||
this.elem.querySelector('[comment-add-button]').style.display = 'block';
|
||||
}
|
||||
|
||||
setReply(commentElem) {
|
||||
this.showForm();
|
||||
this.parentId = Number(commentElem.getAttribute('local-id'));
|
||||
this.elem.querySelector('[comment-form-reply-to]').style.display = 'block';
|
||||
let replyLink = this.elem.querySelector('[comment-form-reply-to] a');
|
||||
replyLink.textContent = `#${this.parentId}`;
|
||||
replyLink.href = `#comment${this.parentId}`;
|
||||
}
|
||||
|
||||
removeReplyTo() {
|
||||
this.parentId = null;
|
||||
this.elem.querySelector('[comment-form-reply-to]').style.display = 'none';
|
||||
}
|
||||
|
||||
showLoading(formElem) {
|
||||
let groups = formElem.querySelectorAll('.form-group');
|
||||
for (let i = 0, len = groups.length; i < len; i++) {
|
||||
groups[i].style.display = 'none';
|
||||
}
|
||||
formElem.querySelector('.form-group.loading').style.display = 'block';
|
||||
}
|
||||
|
||||
hideLoading(formElem) {
|
||||
let groups = formElem.querySelectorAll('.form-group');
|
||||
for (let i = 0, len = groups.length; i < len; i++) {
|
||||
groups[i].style.display = 'block';
|
||||
}
|
||||
formElem.querySelector('.form-group.loading').style.display = 'none';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = PageComments;
|
||||
60
resources/assets/js/components/page-picker.js
Normal file
60
resources/assets/js/components/page-picker.js
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
class PagePicker {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.input = elem.querySelector('input');
|
||||
this.resetButton = elem.querySelector('[page-picker-reset]');
|
||||
this.selectButton = elem.querySelector('[page-picker-select]');
|
||||
this.display = elem.querySelector('[page-picker-display]');
|
||||
this.defaultDisplay = elem.querySelector('[page-picker-default]');
|
||||
this.buttonSep = elem.querySelector('span.sep');
|
||||
|
||||
this.value = this.input.value;
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
// Select click
|
||||
this.selectButton.addEventListener('click', event => {
|
||||
window.EntitySelectorPopup.show(entity => {
|
||||
this.setValue(entity.id, entity.name);
|
||||
});
|
||||
});
|
||||
|
||||
this.resetButton.addEventListener('click', event => {
|
||||
this.setValue('', '');
|
||||
});
|
||||
}
|
||||
|
||||
setValue(value, name) {
|
||||
this.value = value;
|
||||
this.input.value = value;
|
||||
this.controlView(name);
|
||||
}
|
||||
|
||||
controlView(name) {
|
||||
let hasValue = this.value && this.value !== 0;
|
||||
toggleElem(this.resetButton, hasValue);
|
||||
toggleElem(this.buttonSep, hasValue);
|
||||
toggleElem(this.defaultDisplay, !hasValue);
|
||||
toggleElem(this.display, hasValue);
|
||||
if (hasValue) {
|
||||
let id = this.getAssetIdFromVal();
|
||||
this.display.textContent = `#${id}, ${name}`;
|
||||
this.display.href = window.baseUrl(`/link/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
getAssetIdFromVal() {
|
||||
return Number(this.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function toggleElem(elem, show) {
|
||||
let display = (elem.tagName === 'BUTTON' || elem.tagName === 'SPAN') ? 'inline-block' : 'block';
|
||||
elem.style.display = show ? display : 'none';
|
||||
}
|
||||
|
||||
module.exports = PagePicker;
|
||||
16
resources/assets/js/components/sidebar.js
Normal file
16
resources/assets/js/components/sidebar.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
class Sidebar {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.toggleElem = elem.querySelector('.sidebar-toggle');
|
||||
this.toggleElem.addEventListener('click', this.toggle.bind(this));
|
||||
}
|
||||
|
||||
toggle(show = true) {
|
||||
this.elem.classList.toggle('open');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Sidebar;
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
};
|
||||
|
||||
20
resources/assets/js/dom-polyfills.js
Normal file
20
resources/assets/js/dom-polyfills.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Polyfills for DOM API's
|
||||
*/
|
||||
|
||||
if (!Element.prototype.matches) {
|
||||
Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
|
||||
}
|
||||
|
||||
if (!Element.prototype.closest) {
|
||||
Element.prototype.closest = function (s) {
|
||||
var el = this;
|
||||
var ancestor = this;
|
||||
if (!document.documentElement.contains(el)) return null;
|
||||
do {
|
||||
if (ancestor.matches(s)) return ancestor;
|
||||
ancestor = ancestor.parentElement;
|
||||
} while (ancestor !== null);
|
||||
return null;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
138
resources/assets/js/vues/attachment-manager.js
Normal file
138
resources/assets/js/vues/attachment-manager.js
Normal file
@@ -0,0 +1,138 @@
|
||||
const draggable = require('vuedraggable');
|
||||
const dropzone = require('./components/dropzone');
|
||||
|
||||
function mounted() {
|
||||
this.pageId = this.$el.getAttribute('page-id');
|
||||
this.file = this.newFile();
|
||||
|
||||
this.$http.get(window.baseUrl(`/attachments/get/page/${this.pageId}`)).then(resp => {
|
||||
this.files = resp.data;
|
||||
}).catch(err => {
|
||||
this.checkValidationErrors('get', err);
|
||||
});
|
||||
}
|
||||
|
||||
let data = {
|
||||
pageId: null,
|
||||
files: [],
|
||||
fileToEdit: null,
|
||||
file: {},
|
||||
tab: 'list',
|
||||
editTab: 'file',
|
||||
errors: {link: {}, edit: {}, delete: {}}
|
||||
};
|
||||
|
||||
const components = {dropzone, draggable};
|
||||
|
||||
let methods = {
|
||||
|
||||
newFile() {
|
||||
return {page_id: this.pageId};
|
||||
},
|
||||
|
||||
getFileUrl(file) {
|
||||
return window.baseUrl(`/attachments/${file.id}`);
|
||||
},
|
||||
|
||||
fileSortUpdate() {
|
||||
this.$http.put(window.baseUrl(`/attachments/sort/page/${this.pageId}`), {files: this.files}).then(resp => {
|
||||
this.$events.emit('success', resp.data.message);
|
||||
}).catch(err => {
|
||||
this.checkValidationErrors('sort', err);
|
||||
});
|
||||
},
|
||||
|
||||
startEdit(file) {
|
||||
this.fileToEdit = Object.assign({}, file);
|
||||
this.fileToEdit.link = file.external ? file.path : '';
|
||||
this.editTab = file.external ? 'link' : 'file';
|
||||
},
|
||||
|
||||
deleteFile(file) {
|
||||
if (!file.deleting) return file.deleting = true;
|
||||
|
||||
this.$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
|
||||
this.$events.emit('success', resp.data.message);
|
||||
this.files.splice(this.files.indexOf(file), 1);
|
||||
}).catch(err => {
|
||||
this.checkValidationErrors('delete', err)
|
||||
});
|
||||
},
|
||||
|
||||
uploadSuccess(upload) {
|
||||
this.files.push(upload.data);
|
||||
this.$events.emit('success', trans('entities.attachments_file_uploaded'));
|
||||
},
|
||||
|
||||
uploadSuccessUpdate(upload) {
|
||||
let fileIndex = this.filesIndex(upload.data);
|
||||
if (fileIndex === -1) {
|
||||
this.files.push(upload.data)
|
||||
} else {
|
||||
this.files.splice(fileIndex, 1, upload.data);
|
||||
}
|
||||
|
||||
if (this.fileToEdit && this.fileToEdit.id === upload.data.id) {
|
||||
this.fileToEdit = Object.assign({}, upload.data);
|
||||
}
|
||||
this.$events.emit('success', trans('entities.attachments_file_updated'));
|
||||
},
|
||||
|
||||
checkValidationErrors(groupName, err) {
|
||||
console.error(err);
|
||||
if (typeof err.response.data === "undefined" && typeof err.response.data.validation === "undefined") return;
|
||||
this.errors[groupName] = err.response.data.validation;
|
||||
console.log(this.errors[groupName]);
|
||||
},
|
||||
|
||||
getUploadUrl(file) {
|
||||
let url = window.baseUrl(`/attachments/upload`);
|
||||
if (typeof file !== 'undefined') url += `/${file.id}`;
|
||||
return url;
|
||||
},
|
||||
|
||||
cancelEdit() {
|
||||
this.fileToEdit = null;
|
||||
},
|
||||
|
||||
attachNewLink(file) {
|
||||
file.uploaded_to = this.pageId;
|
||||
this.$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
|
||||
this.files.push(resp.data);
|
||||
this.file = this.newFile();
|
||||
this.$events.emit('success', trans('entities.attachments_link_attached'));
|
||||
}).catch(err => {
|
||||
this.checkValidationErrors('link', err);
|
||||
});
|
||||
},
|
||||
|
||||
updateFile(file) {
|
||||
$http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
|
||||
let search = this.filesIndex(resp.data);
|
||||
if (search === -1) {
|
||||
this.files.push(resp.data);
|
||||
} else {
|
||||
this.files.splice(search, 1, resp.data);
|
||||
}
|
||||
|
||||
if (this.fileToEdit && !file.external) this.fileToEdit.link = '';
|
||||
this.fileToEdit = false;
|
||||
|
||||
this.$events.emit('success', trans('entities.attachments_updated_success'));
|
||||
}).catch(err => {
|
||||
this.checkValidationErrors('edit', err);
|
||||
});
|
||||
},
|
||||
|
||||
filesIndex(file) {
|
||||
for (let i = 0, len = this.files.length; i < len; i++) {
|
||||
if (this.files[i].id === file.id) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
data, methods, mounted, components,
|
||||
};
|
||||
130
resources/assets/js/vues/components/autosuggest.js
Normal file
130
resources/assets/js/vues/components/autosuggest.js
Normal file
@@ -0,0 +1,130 @@
|
||||
|
||||
const template = `
|
||||
<div>
|
||||
<input :value="value" :autosuggest-type="type" ref="input"
|
||||
:placeholder="placeholder" :name="name"
|
||||
@input="inputUpdate($event.target.value)" @focus="inputUpdate($event.target.value)"
|
||||
@blur="inputBlur"
|
||||
@keydown="inputKeydown"
|
||||
/>
|
||||
<ul class="suggestion-box" v-if="showSuggestions">
|
||||
<li v-for="(suggestion, i) in suggestions"
|
||||
@click="selectSuggestion(suggestion)"
|
||||
:class="{active: (i === active)}">{{suggestion}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
`;
|
||||
|
||||
function data() {
|
||||
return {
|
||||
suggestions: [],
|
||||
showSuggestions: false,
|
||||
active: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const ajaxCache = {};
|
||||
|
||||
const props = ['url', 'type', 'value', 'placeholder', 'name'];
|
||||
|
||||
function getNameInputVal(valInput) {
|
||||
let parentRow = valInput.parentNode.parentNode;
|
||||
let nameInput = parentRow.querySelector('[autosuggest-type="name"]');
|
||||
return (nameInput === null) ? '' : nameInput.value;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
|
||||
inputUpdate(inputValue) {
|
||||
this.$emit('input', inputValue);
|
||||
let params = {};
|
||||
|
||||
if (this.type === 'value') {
|
||||
let nameVal = getNameInputVal(this.$el);
|
||||
if (nameVal !== "") params.name = nameVal;
|
||||
}
|
||||
|
||||
this.getSuggestions(inputValue.slice(0, 3), params).then(suggestions => {
|
||||
if (inputValue.length === 0) {
|
||||
this.displaySuggestions(suggestions.slice(0, 6));
|
||||
return;
|
||||
}
|
||||
// Filter to suggestions containing searched term
|
||||
suggestions = suggestions.filter(item => {
|
||||
return item.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1;
|
||||
}).slice(0, 4);
|
||||
this.displaySuggestions(suggestions);
|
||||
});
|
||||
},
|
||||
|
||||
inputBlur() {
|
||||
setTimeout(() => {
|
||||
this.$emit('blur');
|
||||
this.showSuggestions = false;
|
||||
}, 100);
|
||||
},
|
||||
|
||||
inputKeydown(event) {
|
||||
if (event.keyCode === 13) event.preventDefault();
|
||||
if (!this.showSuggestions) return;
|
||||
|
||||
// Down arrow
|
||||
if (event.keyCode === 40) {
|
||||
this.active = (this.active === this.suggestions.length - 1) ? 0 : this.active+1;
|
||||
}
|
||||
// Up Arrow
|
||||
else if (event.keyCode === 38) {
|
||||
this.active = (this.active === 0) ? this.suggestions.length - 1 : this.active-1;
|
||||
}
|
||||
// Enter or tab keys
|
||||
else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
|
||||
this.selectSuggestion(this.suggestions[this.active]);
|
||||
}
|
||||
// Escape key
|
||||
else if (event.keyCode === 27) {
|
||||
this.showSuggestions = false;
|
||||
}
|
||||
},
|
||||
|
||||
displaySuggestions(suggestions) {
|
||||
if (suggestions.length === 0) {
|
||||
this.suggestions = [];
|
||||
this.showSuggestions = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.suggestions = suggestions;
|
||||
this.showSuggestions = true;
|
||||
this.active = 0;
|
||||
},
|
||||
|
||||
selectSuggestion(suggestion) {
|
||||
this.$refs.input.value = suggestion;
|
||||
this.$refs.input.focus();
|
||||
this.$emit('input', suggestion);
|
||||
this.showSuggestions = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get suggestions from BookStack. Store and use local cache if already searched.
|
||||
* @param {String} input
|
||||
* @param {Object} params
|
||||
*/
|
||||
getSuggestions(input, params) {
|
||||
params.search = input;
|
||||
let cacheKey = `${this.url}:${JSON.stringify(params)}`;
|
||||
|
||||
if (typeof ajaxCache[cacheKey] !== "undefined") return Promise.resolve(ajaxCache[cacheKey]);
|
||||
|
||||
return this.$http.get(this.url, {params}).then(resp => {
|
||||
ajaxCache[cacheKey] = resp.data;
|
||||
return resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const computed = [];
|
||||
|
||||
module.exports = {template, data, props, methods, computed};
|
||||
60
resources/assets/js/vues/components/dropzone.js
Normal file
60
resources/assets/js/vues/components/dropzone.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const DropZone = require("dropzone");
|
||||
|
||||
const template = `
|
||||
<div class="dropzone-container">
|
||||
<div class="dz-message">{{placeholder}}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const props = ['placeholder', 'uploadUrl', 'uploadedTo'];
|
||||
|
||||
// TODO - Remove jQuery usage
|
||||
function mounted() {
|
||||
let container = this.$el;
|
||||
let _this = this;
|
||||
new DropZone(container, {
|
||||
url: function() {
|
||||
return _this.uploadUrl;
|
||||
},
|
||||
init: function () {
|
||||
let dz = this;
|
||||
|
||||
dz.on('sending', function (file, xhr, data) {
|
||||
let token = window.document.querySelector('meta[name=token]').getAttribute('content');
|
||||
data.append('_token', token);
|
||||
let uploadedTo = typeof _this.uploadedTo === 'undefined' ? 0 : _this.uploadedTo;
|
||||
data.append('uploaded_to', uploadedTo);
|
||||
});
|
||||
|
||||
dz.on('success', function (file, data) {
|
||||
_this.$emit('success', {file, data});
|
||||
$(file.previewElement).fadeOut(400, function () {
|
||||
dz.removeFile(file);
|
||||
});
|
||||
});
|
||||
|
||||
dz.on('error', function (file, errorMessage, xhr) {
|
||||
_this.$emit('error', {file, errorMessage, xhr});
|
||||
console.log(errorMessage);
|
||||
console.log(xhr);
|
||||
function setMessage(message) {
|
||||
$(file.previewElement).find('[data-dz-errormessage]').text(message);
|
||||
}
|
||||
|
||||
if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
|
||||
if (errorMessage.file) setMessage(errorMessage.file[0]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function data() {
|
||||
return {}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
template,
|
||||
props,
|
||||
mounted,
|
||||
data,
|
||||
};
|
||||
178
resources/assets/js/vues/image-manager.js
Normal file
178
resources/assets/js/vues/image-manager.js
Normal file
@@ -0,0 +1,178 @@
|
||||
const dropzone = require('./components/dropzone');
|
||||
|
||||
let page = 0;
|
||||
let previousClickTime = 0;
|
||||
let previousClickImage = 0;
|
||||
let dataLoaded = false;
|
||||
let callback = false;
|
||||
let baseUrl = '';
|
||||
|
||||
let preSearchImages = [];
|
||||
let preSearchHasMore = false;
|
||||
|
||||
const data = {
|
||||
images: [],
|
||||
|
||||
imageType: false,
|
||||
uploadedTo: false,
|
||||
|
||||
selectedImage: false,
|
||||
dependantPages: false,
|
||||
showing: false,
|
||||
view: 'all',
|
||||
hasMore: false,
|
||||
searching: false,
|
||||
searchTerm: '',
|
||||
|
||||
imageUpdateSuccess: false,
|
||||
imageDeleteSuccess: false,
|
||||
};
|
||||
|
||||
const methods = {
|
||||
|
||||
show(providedCallback) {
|
||||
callback = providedCallback;
|
||||
this.showing = true;
|
||||
this.$el.children[0].components.overlay.show();
|
||||
|
||||
// Get initial images if they have not yet been loaded in.
|
||||
if (dataLoaded) return;
|
||||
this.fetchData();
|
||||
dataLoaded = true;
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.showing = false;
|
||||
this.$el.children[0].components.overlay.hide();
|
||||
},
|
||||
|
||||
fetchData() {
|
||||
let url = baseUrl + page;
|
||||
let query = {};
|
||||
if (this.uploadedTo !== false) query.page_id = this.uploadedTo;
|
||||
if (this.searching) query.term = this.searchTerm;
|
||||
|
||||
this.$http.get(url, {params: query}).then(response => {
|
||||
this.images = this.images.concat(response.data.images);
|
||||
this.hasMore = response.data.hasMore;
|
||||
page++;
|
||||
});
|
||||
},
|
||||
|
||||
setView(viewName) {
|
||||
this.cancelSearch();
|
||||
this.images = [];
|
||||
this.hasMore = false;
|
||||
page = 0;
|
||||
this.view = viewName;
|
||||
baseUrl = window.baseUrl(`/images/${this.imageType}/${viewName}/`);
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
searchImages() {
|
||||
if (this.searchTerm === '') return this.cancelSearch();
|
||||
|
||||
// Cache current settings for later
|
||||
if (!this.searching) {
|
||||
preSearchImages = this.images;
|
||||
preSearchHasMore = this.hasMore;
|
||||
}
|
||||
|
||||
this.searching = true;
|
||||
this.images = [];
|
||||
this.hasMore = false;
|
||||
page = 0;
|
||||
baseUrl = window.baseUrl(`/images/${this.imageType}/search/`);
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
cancelSearch() {
|
||||
this.searching = false;
|
||||
this.searchTerm = '';
|
||||
this.images = preSearchImages;
|
||||
this.hasMore = preSearchHasMore;
|
||||
},
|
||||
|
||||
imageSelect(image) {
|
||||
let dblClickTime = 300;
|
||||
let currentTime = Date.now();
|
||||
let timeDiff = currentTime - previousClickTime;
|
||||
let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage;
|
||||
|
||||
if (isDblClick) {
|
||||
this.callbackAndHide(image);
|
||||
} else {
|
||||
this.selectedImage = image;
|
||||
this.dependantPages = false;
|
||||
}
|
||||
|
||||
previousClickTime = currentTime;
|
||||
previousClickImage = image.id;
|
||||
},
|
||||
|
||||
callbackAndHide(imageResult) {
|
||||
if (callback) callback(imageResult);
|
||||
this.hide();
|
||||
},
|
||||
|
||||
saveImageDetails() {
|
||||
let url = window.baseUrl(`/images/update/${this.selectedImage.id}`);
|
||||
this.$http.put(url, this.selectedImage).then(response => {
|
||||
this.$events.emit('success', trans('components.image_update_success'));
|
||||
}).catch(error => {
|
||||
if (error.response.status === 422) {
|
||||
let errors = error.response.data;
|
||||
let message = '';
|
||||
Object.keys(errors).forEach((key) => {
|
||||
message += errors[key].join('\n');
|
||||
});
|
||||
this.$events.emit('error', message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
deleteImage() {
|
||||
let force = this.dependantPages !== false;
|
||||
let url = window.baseUrl('/images/' + this.selectedImage.id);
|
||||
if (force) url += '?force=true';
|
||||
this.$http.delete(url).then(response => {
|
||||
this.images.splice(this.images.indexOf(this.selectedImage), 1);
|
||||
this.selectedImage = false;
|
||||
this.$events.emit('success', trans('components.image_delete_success'));
|
||||
}).catch(error=> {
|
||||
if (error.response.status === 400) {
|
||||
this.dependantPages = error.response.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getDate(stringDate) {
|
||||
return new Date(stringDate);
|
||||
},
|
||||
|
||||
uploadSuccess(event) {
|
||||
this.images.unshift(event.data);
|
||||
this.$events.emit('success', trans('components.image_upload_success'));
|
||||
},
|
||||
};
|
||||
|
||||
const computed = {
|
||||
uploadUrl() {
|
||||
return window.baseUrl(`/images/${this.imageType}/upload`);
|
||||
}
|
||||
};
|
||||
|
||||
function mounted() {
|
||||
window.ImageManager = this;
|
||||
this.imageType = this.$el.getAttribute('image-type');
|
||||
this.uploadedTo = this.$el.getAttribute('uploaded-to');
|
||||
baseUrl = window.baseUrl('/images/' + this.imageType + '/all/')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mounted,
|
||||
methods,
|
||||
data,
|
||||
computed,
|
||||
components: {dropzone},
|
||||
};
|
||||
68
resources/assets/js/vues/tag-manager.js
Normal file
68
resources/assets/js/vues/tag-manager.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const draggable = require('vuedraggable');
|
||||
const autosuggest = require('./components/autosuggest');
|
||||
|
||||
let data = {
|
||||
pageId: false,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const components = {draggable, autosuggest};
|
||||
const directives = {};
|
||||
|
||||
let computed = {};
|
||||
|
||||
let methods = {
|
||||
|
||||
addEmptyTag() {
|
||||
this.tags.push({name: '', value: '', key: Math.random().toString(36).substring(7)});
|
||||
},
|
||||
|
||||
/**
|
||||
* When an tag changes check if another empty editable field needs to be added onto the end.
|
||||
* @param tag
|
||||
*/
|
||||
tagChange(tag) {
|
||||
let tagPos = this.tags.indexOf(tag);
|
||||
if (tagPos === this.tags.length-1 && (tag.name !== '' || tag.value !== '')) this.addEmptyTag();
|
||||
},
|
||||
|
||||
/**
|
||||
* When an tag field loses focus check the tag to see if its
|
||||
* empty and therefore could be removed from the list.
|
||||
* @param tag
|
||||
*/
|
||||
tagBlur(tag) {
|
||||
let isLast = (this.tags.indexOf(tag) === this.tags.length-1);
|
||||
if (tag.name !== '' || tag.value !== '' || isLast) return;
|
||||
let cPos = this.tags.indexOf(tag);
|
||||
this.tags.splice(cPos, 1);
|
||||
},
|
||||
|
||||
removeTag(tag) {
|
||||
let tagPos = this.tags.indexOf(tag);
|
||||
if (tagPos === -1) return;
|
||||
this.tags.splice(tagPos, 1);
|
||||
},
|
||||
|
||||
getTagFieldName(index, key) {
|
||||
return `tags[${index}][${key}]`;
|
||||
},
|
||||
};
|
||||
|
||||
function mounted() {
|
||||
this.pageId = Number(this.$el.getAttribute('page-id'));
|
||||
|
||||
let url = window.baseUrl(`/ajax/tags/get/page/${this.pageId}`);
|
||||
this.$http.get(url).then(response => {
|
||||
let tags = response.data;
|
||||
for (let i = 0, len = tags.length; i < len; i++) {
|
||||
tags[i].key = Math.random().toString(36).substring(7);
|
||||
}
|
||||
this.tags = tags;
|
||||
this.addEmptyTag();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data, computed, methods, mounted, components, directives
|
||||
};
|
||||
@@ -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 {
|
||||
@@ -181,4 +180,78 @@
|
||||
&.warning:before {
|
||||
content: '\f1f1';
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: $-m;
|
||||
background-color: #FFF;
|
||||
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.2);
|
||||
h3 {
|
||||
padding: $-m;
|
||||
border-bottom: 1px solid #E8E8E8;
|
||||
margin: 0;
|
||||
font-size: $fs-s;
|
||||
color: #888;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.body, p.empty-text {
|
||||
padding: $-m;
|
||||
}
|
||||
a {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.card.drag-card {
|
||||
border: 1px solid #DDD;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
padding-left: $-s + 28px;
|
||||
margin: $-s 0;
|
||||
position: relative;
|
||||
.drag-card-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
.handle, .drag-card-action {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 28px;
|
||||
padding-left: $-xs;
|
||||
padding-right: $-xs;
|
||||
&:hover {
|
||||
background-color: #EEE;
|
||||
}
|
||||
i {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
> div .outline input {
|
||||
margin: $-s 0;
|
||||
}
|
||||
> div.padded {
|
||||
padding: $-s 0 !important;
|
||||
}
|
||||
.handle {
|
||||
background-color: #EEE;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
> div {
|
||||
padding: 0 $-s;
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.well {
|
||||
background-color: #F8F8F8;
|
||||
padding: $-m;
|
||||
border: 1px solid #DDD;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -479,4 +540,45 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
margin-right: $-xs;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-box {
|
||||
border: 1px solid #DDD;
|
||||
margin-bottom: $-s;
|
||||
border-radius: 3px;
|
||||
.content {
|
||||
padding: $-s;
|
||||
font-size: 0.666em;
|
||||
p, ul {
|
||||
font-size: $fs-m;
|
||||
margin: .5em 0;
|
||||
}
|
||||
}
|
||||
.reply-row {
|
||||
padding: $-xs $-s;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-box .header {
|
||||
padding: $-xs $-s;
|
||||
background-color: #f8f8f8;
|
||||
border-bottom: 1px solid #DDD;
|
||||
.meta {
|
||||
img, a, span {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
a, span {
|
||||
padding: $-xxs 0 $-xxs 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
a { color: #666; }
|
||||
span {
|
||||
color: #888;
|
||||
padding-left: $-xxs;
|
||||
}
|
||||
}
|
||||
.text-muted {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
&.rows {
|
||||
flex-direction: row;
|
||||
}
|
||||
&.columns {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.flex {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex.scroll {
|
||||
//overflow-y: auto;
|
||||
display: flex;
|
||||
&.sidebar {
|
||||
margin-right: -14px;
|
||||
}
|
||||
}
|
||||
.flex.scroll .scroll-body {
|
||||
overflow-y: scroll;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex-child > div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
//body.ie .flex-child > div {
|
||||
// flex: 1 0 0px;
|
||||
//}
|
||||
.flex.sidebar {
|
||||
flex: 1;
|
||||
background-color: #F2F2F2;
|
||||
max-width: 360px;
|
||||
min-height: 90vh;
|
||||
}
|
||||
.flex.sidebar + .flex.content {
|
||||
flex: 3;
|
||||
background-color: #FFFFFF;
|
||||
padding: 0 $-l;
|
||||
border-left: 1px solid #DDD;
|
||||
max-width: 100%;
|
||||
}
|
||||
.flex.sidebar .sidebar-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include smaller-than($xl) {
|
||||
body.sidebar-layout {
|
||||
padding-left: 30px;
|
||||
}
|
||||
.flex.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
padding-right: 30px;
|
||||
width: 360px;
|
||||
box-shadow: none;
|
||||
transform: translate3d(-330px, 0, 0);
|
||||
transition: transform ease-in-out 120ms;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex.sidebar.open {
|
||||
box-shadow: 1px 2px 2px 1px rgba(0,0,0,.10);
|
||||
transform: translate3d(0, 0, 0);
|
||||
.sidebar-toggle i {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
.flex.sidebar .sidebar-toggle {
|
||||
display: block;
|
||||
position: absolute;
|
||||
opacity: 0.9;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 30px;
|
||||
color: #666;
|
||||
font-size: 20px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
border: 1px solid #DDD;
|
||||
border-top: 1px solid #BBB;
|
||||
padding-top: $-m;
|
||||
cursor: pointer;
|
||||
i {
|
||||
opacity: 0.5;
|
||||
transition: all ease-in-out 120ms;
|
||||
padding: 0;
|
||||
}
|
||||
&:hover i {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.sidebar .scroll-body {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#sidebar .scroll-body.fixed {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@include larger-than($xl) {
|
||||
#sidebar .scroll-body.fixed {
|
||||
z-index: 5;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding-right: $-m;
|
||||
width: 30%;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
-ms-overflow-style: none;
|
||||
//background-color: $primary-faded;
|
||||
border-left: 1px solid #DDD;
|
||||
&::-webkit-scrollbar { width: 0 !important }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Rules for all columns */
|
||||
div[class^="col-"] img {
|
||||
@@ -54,6 +163,10 @@ div[class^="col-"] img {
|
||||
&.small {
|
||||
max-width: 840px;
|
||||
}
|
||||
&.nopad {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
|
||||
@@ -12,7 +12,6 @@ header {
|
||||
padding: $-m;
|
||||
}
|
||||
border-bottom: 1px solid #DDD;
|
||||
//margin-bottom: $-l;
|
||||
.links {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
@@ -23,26 +22,27 @@ header {
|
||||
}
|
||||
.links a {
|
||||
display: inline-block;
|
||||
padding: $-l;
|
||||
padding: $-m $-l;
|
||||
color: #FFF;
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
@include smaller-than($screen-md) {
|
||||
padding: $-l $-s;
|
||||
padding: $-m $-s;
|
||||
}
|
||||
}
|
||||
.avatar, .user-name {
|
||||
display: inline-block;
|
||||
}
|
||||
.avatar {
|
||||
//margin-top: (45px/2);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.user-name {
|
||||
vertical-align: top;
|
||||
padding-top: $-l;
|
||||
padding-top: $-m;
|
||||
position: relative;
|
||||
top: -3px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
> * {
|
||||
@@ -66,53 +66,57 @@ header {
|
||||
}
|
||||
}
|
||||
}
|
||||
@include smaller-than($screen-md) {
|
||||
@include smaller-than($screen-sm) {
|
||||
text-align: center;
|
||||
.float.right {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
@include smaller-than($screen-sm) {
|
||||
.links a {
|
||||
padding: $-s;
|
||||
}
|
||||
form.search-box {
|
||||
margin-top: 0;
|
||||
}
|
||||
.user-name {
|
||||
padding-top: $-s;
|
||||
}
|
||||
}
|
||||
.dropdown-container {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.header-search {
|
||||
display: inline-block;
|
||||
}
|
||||
header .search-box {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
input {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #EEE;
|
||||
}
|
||||
button {
|
||||
color: #EEE;
|
||||
}
|
||||
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: #DDD;
|
||||
}
|
||||
::-moz-placeholder { /* Firefox 19+ */
|
||||
color: #DDD;
|
||||
}
|
||||
:-ms-input-placeholder { /* IE 10+ */
|
||||
color: #DDD;
|
||||
}
|
||||
:-moz-placeholder { /* Firefox 18- */
|
||||
color: #DDD;
|
||||
}
|
||||
@include smaller-than($screen-lg) {
|
||||
max-width: 250px;
|
||||
}
|
||||
@include smaller-than($l) {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
form.search-box {
|
||||
margin-top: $-l *0.9;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
input {
|
||||
background-color: transparent;
|
||||
border-radius: 24px;
|
||||
border: 2px solid #EEE;
|
||||
color: #EEE;
|
||||
padding-left: $-m;
|
||||
padding-right: $-l;
|
||||
outline: 0;
|
||||
}
|
||||
button {
|
||||
vertical-align: top;
|
||||
margin-left: -$-l;
|
||||
color: #FFF;
|
||||
top: 6px;
|
||||
right: 4px;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
}
|
||||
@include smaller-than($s) {
|
||||
.header-search {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,12 +132,12 @@ form.search-box {
|
||||
font-size: 1.8em;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
padding: $-l $-l $-l 0;
|
||||
padding: 14px $-l 14px 0;
|
||||
vertical-align: top;
|
||||
line-height: 1;
|
||||
}
|
||||
.logo-image {
|
||||
margin: $-m $-s $-m 0;
|
||||
margin: $-xs $-s $-xs 0;
|
||||
vertical-align: top;
|
||||
height: 43px;
|
||||
}
|
||||
@@ -167,6 +171,10 @@ form.search-box {
|
||||
background-color: $primary-faded;
|
||||
}
|
||||
|
||||
.toolbar-container {
|
||||
background-color: #FFF;
|
||||
}
|
||||
|
||||
.breadcrumbs .text-button, .action-buttons .text-button {
|
||||
display: inline-block;
|
||||
padding: $-s;
|
||||
@@ -227,4 +235,7 @@ form.search-box {
|
||||
border-bottom: 2px solid $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
.faded-small .nav-tabs a {
|
||||
padding: $-s $-m;
|
||||
}
|
||||
@@ -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%;
|
||||
@@ -310,4 +301,8 @@
|
||||
background-color: #EEE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
|
||||
min-height: 175px;
|
||||
}
|
||||
@@ -57,14 +57,4 @@ table.list-table {
|
||||
vertical-align: middle;
|
||||
padding: $-xs;
|
||||
}
|
||||
}
|
||||
|
||||
table.file-table {
|
||||
@extend .no-style;
|
||||
td {
|
||||
padding: $-xs;
|
||||
}
|
||||
.ui-sortable-helper {
|
||||
display: table;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -48,4 +48,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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;
|
||||
|
||||
@@ -55,4 +59,4 @@ $text-light: #EEE;
|
||||
// Shadows
|
||||
$bs-light: 0 0 4px 1px #CCC;
|
||||
$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
|
||||
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
|
||||
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
|
||||
@@ -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_success' => 'Ihr Passwort wurde erfolgreich zurück gesetzt.',
|
||||
'reset_password_sent_success' => 'Eine E-Mail mit dem Link zum Zurücksetzen Ihres Passwortes wurde an :email gesendet.',
|
||||
'reset_password_success' => 'Ihr Passwort wurde erfolgreich zurückgesetzt.',
|
||||
|
||||
'email_reset_subject' => 'Passwort zurücksetzen für :appName',
|
||||
'email_reset_text' => 'Sie erhalten diese E-Mail, weil eine Passwort-Rücksetzung für Ihren Account beantragt wurde.',
|
||||
'email_reset_not_requested' => 'Wenn Sie die Passwort-Rücksetzung nicht ausgelöst haben, ist kein weiteres Handeln notwendig.',
|
||||
'email_reset_text' => 'Sie erhalten diese E-Mail, weil jemand versucht hat, Ihr Passwort zurückzusetzen.',
|
||||
'email_reset_not_requested' => 'Wenn Sie das nicht waren, brauchen Sie nichts weiter zu tun.',
|
||||
|
||||
|
||||
/**
|
||||
* Email Confirmation
|
||||
*/
|
||||
'email_confirm_subject' => 'Bestä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',
|
||||
];
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user