diff --git a/.gitignore b/.gitignore
index d7281b29c..00602d29a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/app/Comment.php b/app/Comment.php
new file mode 100644
index 000000000..de01b6212
--- /dev/null
+++ b/app/Comment.php
@@ -0,0 +1,96 @@
+morphTo('entity');
+ }
+
+ /**
+ * Get the page that this comment is in.
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function page()
+ {
+ return $this->belongsTo(Page::class);
+ }
+
+ /**
+ * Get the owner of this comment.
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function user()
+ {
+ return $this->belongsTo(User::class);
+ }
+
+ /*
+ * Not being used, but left here because might be used in the future for performance reasons.
+ */
+ public function getPageComments($pageId) {
+ $query = static::newQuery();
+ $query->join('users AS u', 'comments.created_by', '=', 'u.id');
+ $query->leftJoin('users AS u1', 'comments.updated_by', '=', 'u1.id');
+ $query->leftJoin('images AS i', 'i.id', '=', 'u.image_id');
+ $query->selectRaw('comments.id, text, html, comments.created_by, comments.updated_by, '
+ . 'comments.created_at, comments.updated_at, comments.parent_id, '
+ . 'u.name AS created_by_name, u1.name AS updated_by_name, '
+ . 'i.url AS avatar ');
+ $query->whereRaw('page_id = ?', [$pageId]);
+ $query->orderBy('created_at');
+ return $query->get();
+ }
+
+ public function getAllPageComments($pageId) {
+ return self::where('page_id', '=', $pageId)->with(['createdBy' => function($query) {
+ $query->select('id', 'name', 'image_id');
+ }, 'updatedBy' => function($query) {
+ $query->select('id', 'name');
+ }, 'createdBy.avatar' => function ($query) {
+ $query->select('id', 'path', 'url');
+ }])->get();
+ }
+
+ public function getCommentById($commentId) {
+ return self::where('id', '=', $commentId)->with(['createdBy' => function($query) {
+ $query->select('id', 'name', 'image_id');
+ }, 'updatedBy' => function($query) {
+ $query->select('id', 'name');
+ }, 'createdBy.avatar' => function ($query) {
+ $query->select('id', 'path', 'url');
+ }])->first();
+ }
+
+ public function getCreatedAttribute() {
+ $created = [
+ 'day_time_str' => $this->created_at->toDayDateTimeString(),
+ 'diff' => $this->created_at->diffForHumans()
+ ];
+ return $created;
+ }
+
+ public function getUpdatedAttribute() {
+ if (empty($this->updated_at)) {
+ return null;
+ }
+ $updated = [
+ 'day_time_str' => $this->updated_at->toDayDateTimeString(),
+ 'diff' => $this->updated_at->diffForHumans()
+ ];
+ return $updated;
+ }
+
+ public function getSubCommentsAttribute() {
+ return $this->sub_comments;
+ }
+}
diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php
index 8b0ef309a..9a23fe2a1 100644
--- a/app/Http/Controllers/Auth/RegisterController.php
+++ b/app/Http/Controllers/Auth/RegisterController.php
@@ -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)
{
@@ -255,16 +256,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();
}
diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php
new file mode 100644
index 000000000..e8d5eab30
--- /dev/null
+++ b/app/Http/Controllers/CommentController.php
@@ -0,0 +1,99 @@
+entityRepo = $entityRepo;
+ $this->commentRepo = $commentRepo;
+ $this->comment = $comment;
+ parent::__construct();
+ }
+
+ public function save(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);
+ }
+
+ if($page->draft) {
+ // cannot add comments to drafts.
+ return response()->json([
+ 'status' => 'error',
+ 'message' => trans('errors.cannot_add_comment_to_draft'),
+ ], 400);
+ }
+
+ $this->checkOwnablePermission('page-view', $page);
+ if (empty($commentId)) {
+ // create a new comment.
+ $this->checkPermission('comment-create-all');
+ $comment = $this->commentRepo->create($page, $request->only(['text', 'html', 'parent_id']));
+ $respMsg = trans('entities.comment_created');
+ } else {
+ // update existing comment
+ // get comment by ID and check if this user has permission to update.
+ $comment = $this->comment->findOrFail($commentId);
+ $this->checkOwnablePermission('comment-update', $comment);
+ $this->commentRepo->update($comment, $request->all());
+ $respMsg = trans('entities.comment_updated');
+ }
+
+ $comment = $this->commentRepo->getCommentById($comment->id);
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => $respMsg,
+ 'comment' => $comment
+ ]);
+
+ }
+
+ public function destroy($id) {
+ $comment = $this->comment->findOrFail($id);
+ $this->checkOwnablePermission('comment-delete', $comment);
+ $this->commentRepo->delete($comment);
+ $updatedComment = $this->commentRepo->getCommentById($comment->id);
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => trans('entities.comment_deleted'),
+ 'comment' => $updatedComment
+ ]);
+ }
+
+
+ public function getPageComments($pageId) {
+ try {
+ $page = $this->entityRepo->getById('page', $pageId, true);
+ } catch (ModelNotFoundException $e) {
+ return response('Not found', 404);
+ }
+
+ $this->checkOwnablePermission('page-view', $page);
+
+ $comments = $this->commentRepo->getPageComments($pageId);
+ return response()->json(['status' => 'success', 'comments'=> $comments['comments'],
+ 'total' => $comments['total'], 'permissions' => [
+ 'comment_create' => $this->currentUser->can('comment-create-all'),
+ 'comment_update_own' => $this->currentUser->can('comment-update-own'),
+ 'comment_update_all' => $this->currentUser->can('comment-update-all'),
+ 'comment_delete_all' => $this->currentUser->can('comment-delete-all'),
+ 'comment_delete_own' => $this->currentUser->can('comment-delete-own'),
+ ], 'user_id' => $this->currentUser->id]);
+ }
+}
diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php
index c97597bc4..9a8525c23 100644
--- a/app/Http/Controllers/PageController.php
+++ b/app/Http/Controllers/PageController.php
@@ -161,7 +161,7 @@ class PageController extends Controller
$pageContent = $this->entityRepo->renderPage($page);
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
$pageNav = $this->entityRepo->getPageNav($pageContent);
-
+
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', [
@@ -376,7 +376,7 @@ 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,
diff --git a/app/Page.php b/app/Page.php
index c9823e7e4..d722e4e54 100644
--- a/app/Page.php
+++ b/app/Page.php
@@ -66,6 +66,10 @@ class Page extends Entity
return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc');
}
+ public function comments() {
+ return $this->hasMany(Comment::class, 'page_id')->orderBy('created_on', 'asc');
+ }
+
/**
* Get the url for this page.
* @param string|bool $path
diff --git a/app/Repos/CommentRepo.php b/app/Repos/CommentRepo.php
new file mode 100644
index 000000000..ce71b9234
--- /dev/null
+++ b/app/Repos/CommentRepo.php
@@ -0,0 +1,105 @@
+comment = $comment;
+ }
+
+ public function create (Page $page, $data = []) {
+ $userId = user()->id;
+ $comment = $this->comment->newInstance();
+ $comment->fill($data);
+ // new comment
+ $comment->page_id = $page->id;
+ $comment->created_by = $userId;
+ $comment->updated_at = null;
+ $comment->save();
+ return $comment;
+ }
+
+ public function update($comment, $input, $activeOnly = true) {
+ $userId = user()->id;
+ $comment->updated_by = $userId;
+ $comment->fill($input);
+
+ // only update active comments by default.
+ $whereClause = ['active' => 1];
+ if (!$activeOnly) {
+ $whereClause = [];
+ }
+ $comment->update($whereClause);
+ return $comment;
+ }
+
+ public function delete($comment) {
+ $comment->text = trans('entities.comment_deleted');
+ $comment->html = trans('entities.comment_deleted');
+ $comment->active = false;
+ $userId = user()->id;
+ $comment->updated_by = $userId;
+ $comment->save();
+ return $comment;
+ }
+
+ public function getPageComments($pageId) {
+ $comments = $this->comment->getAllPageComments($pageId);
+ $index = [];
+ $totalComments = count($comments);
+ $finalCommentList = [];
+
+ // normalizing the response.
+ for ($i = 0; $i < count($comments); ++$i) {
+ $comment = $this->normalizeComment($comments[$i]);
+ $parentId = $comment->parent_id;
+ if (empty($parentId)) {
+ $finalCommentList[] = $comment;
+ $index[$comment->id] = $comment;
+ continue;
+ }
+
+ if (empty($index[$parentId])) {
+ // weird condition should not happen.
+ continue;
+ }
+ if (empty($index[$parentId]->sub_comments)) {
+ $index[$parentId]->sub_comments = [];
+ }
+ array_push($index[$parentId]->sub_comments, $comment);
+ $index[$comment->id] = $comment;
+ }
+ return [
+ 'comments' => $finalCommentList,
+ 'total' => $totalComments
+ ];
+ }
+
+ public function getCommentById($commentId) {
+ return $this->normalizeComment($this->comment->getCommentById($commentId));
+ }
+
+ private function normalizeComment($comment) {
+ if (empty($comment)) {
+ return;
+ }
+ $comment->createdBy->avatar_url = $comment->createdBy->getAvatar(50);
+ $comment->createdBy->profile_url = $comment->createdBy->getProfileUrl();
+ if (!empty($comment->updatedBy)) {
+ $comment->updatedBy->profile_url = $comment->updatedBy->getProfileUrl();
+ }
+ return $comment;
+ }
+}
\ No newline at end of file
diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php
index c6c981337..93787a3e5 100644
--- a/app/Services/PermissionService.php
+++ b/app/Services/PermissionService.php
@@ -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)) {
diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php
index ebf78d1fa..b03e34b9b 100644
--- a/database/factories/ModelFactory.php
+++ b/database/factories/ModelFactory.php
@@ -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(3);
+ $html = '
' . $text. '
';
+ return [
+ 'html' => $html,
+ 'text' => $text,
+ 'active' => 1
+ ];
});
\ No newline at end of file
diff --git a/database/migrations/2017_08_01_130541_create_comments_table.php b/database/migrations/2017_08_01_130541_create_comments_table.php
new file mode 100644
index 000000000..bfb7eecbf
--- /dev/null
+++ b/database/migrations/2017_08_01_130541_create_comments_table.php
@@ -0,0 +1,66 @@
+increments('id')->unsigned();
+ $table->integer('page_id')->unsigned();
+ $table->longText('text')->nullable();
+ $table->longText('html')->nullable();
+ $table->integer('parent_id')->unsigned()->nullable();
+ $table->integer('created_by')->unsigned();
+ $table->integer('updated_by')->unsigned()->nullable();
+ $table->boolean('active')->default(true);
+
+ $table->index(['page_id']);
+ $table->timestamps();
+
+ // 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();
+ }
+ }
+}
diff --git a/database/seeds/DummyContentSeeder.php b/database/seeds/DummyContentSeeder.php
index 3d92efab1..996cd178d 100644
--- a/database/seeds/DummyContentSeeder.php
+++ b/database/seeds/DummyContentSeeder.php
@@ -20,7 +20,10 @@ class DummyContentSeeder extends Seeder
->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)->create(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id])->each(function($page) use ($user) {
+ $comments = factory(\BookStack\Comment::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'page_id' => $page->id]);
+ $page->comments()->saveMany($comments);
+ });
$chapter->pages()->saveMany($pages);
});
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
diff --git a/gulpfile.js b/gulpfile.js
index 08c8886bd..f851dd7d6 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -1,3 +1,5 @@
+'use strict';
+
const argv = require('yargs').argv;
const gulp = require('gulp'),
plumber = require('gulp-plumber');
diff --git a/package.json b/package.json
index 429572882..f447ec786 100644
--- a/package.json
+++ b/package.json
@@ -31,15 +31,17 @@
"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-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"
diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff b/public/fonts/roboto-mono-v4-latin-regular.woff
deleted file mode 100644
index 8cb9e6fd8..000000000
Binary files a/public/fonts/roboto-mono-v4-latin-regular.woff and /dev/null differ
diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff2 b/public/fonts/roboto-mono-v4-latin-regular.woff2
deleted file mode 100644
index 1f6598111..000000000
Binary files a/public/fonts/roboto-mono-v4-latin-regular.woff2 and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-100.woff b/public/fonts/roboto-v15-cyrillic_latin-100.woff
deleted file mode 100644
index 4eb2be6a1..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-100.woff and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-100.woff2 b/public/fonts/roboto-v15-cyrillic_latin-100.woff2
deleted file mode 100644
index 007b90e85..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-100.woff2 and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff b/public/fonts/roboto-v15-cyrillic_latin-100italic.woff
deleted file mode 100644
index fa7e51bc8..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-100italic.woff2
deleted file mode 100644
index f27a169cb..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-300.woff b/public/fonts/roboto-v15-cyrillic_latin-300.woff
deleted file mode 100644
index ace052941..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-300.woff and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-300.woff2 b/public/fonts/roboto-v15-cyrillic_latin-300.woff2
deleted file mode 100644
index 0c093b91c..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-300.woff2 and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff b/public/fonts/roboto-v15-cyrillic_latin-300italic.woff
deleted file mode 100644
index 7984971e7..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-300italic.woff2
deleted file mode 100644
index 46ed6c7cc..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-500.woff b/public/fonts/roboto-v15-cyrillic_latin-500.woff
deleted file mode 100644
index 8ae98f2de..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-500.woff and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-500.woff2 b/public/fonts/roboto-v15-cyrillic_latin-500.woff2
deleted file mode 100644
index fba67842e..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-500.woff2 and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff b/public/fonts/roboto-v15-cyrillic_latin-500italic.woff
deleted file mode 100644
index 560968d16..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-500italic.woff2
deleted file mode 100644
index cc41bf873..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-700.woff b/public/fonts/roboto-v15-cyrillic_latin-700.woff
deleted file mode 100644
index 7d19e332d..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-700.woff and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-700.woff2 b/public/fonts/roboto-v15-cyrillic_latin-700.woff2
deleted file mode 100644
index e2274a4fb..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-700.woff2 and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff b/public/fonts/roboto-v15-cyrillic_latin-700italic.woff
deleted file mode 100644
index 1604c8763..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-700italic.woff2
deleted file mode 100644
index f950ca2aa..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-italic.woff b/public/fonts/roboto-v15-cyrillic_latin-italic.woff
deleted file mode 100644
index d76d13d6a..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-italic.woff and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-italic.woff2
deleted file mode 100644
index a80f41528..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-italic.woff2 and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-regular.woff b/public/fonts/roboto-v15-cyrillic_latin-regular.woff
deleted file mode 100644
index a2ada2f46..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-regular.woff and /dev/null differ
diff --git a/public/fonts/roboto-v15-cyrillic_latin-regular.woff2 b/public/fonts/roboto-v15-cyrillic_latin-regular.woff2
deleted file mode 100644
index a3b35e686..000000000
Binary files a/public/fonts/roboto-v15-cyrillic_latin-regular.woff2 and /dev/null differ
diff --git a/public/logo.png b/public/logo.png
index 1803feebf..585f8895b 100644
Binary files a/public/logo.png and b/public/logo.png differ
diff --git a/readme.md b/readme.md
index 6067fbb94..5d099ad5f 100644
--- a/readme.md
+++ b/readme.md
@@ -79,7 +79,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/)
diff --git a/resources/assets/js/components/back-top-top.js b/resources/assets/js/components/back-top-top.js
new file mode 100644
index 000000000..5fa9b3436
--- /dev/null
+++ b/resources/assets/js/components/back-top-top.js
@@ -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;
\ No newline at end of file
diff --git a/resources/assets/js/components/chapter-toggle.js b/resources/assets/js/components/chapter-toggle.js
new file mode 100644
index 000000000..ad373a668
--- /dev/null
+++ b/resources/assets/js/components/chapter-toggle.js
@@ -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;
\ No newline at end of file
diff --git a/resources/assets/js/components/dropdown.js b/resources/assets/js/components/dropdown.js
new file mode 100644
index 000000000..0401efce0
--- /dev/null
+++ b/resources/assets/js/components/dropdown.js
@@ -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;
\ No newline at end of file
diff --git a/resources/assets/js/components/expand-toggle.js b/resources/assets/js/components/expand-toggle.js
new file mode 100644
index 000000000..61d9f54b7
--- /dev/null
+++ b/resources/assets/js/components/expand-toggle.js
@@ -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;
\ No newline at end of file
diff --git a/resources/assets/js/components/index.js b/resources/assets/js/components/index.js
new file mode 100644
index 000000000..43466a0d9
--- /dev/null
+++ b/resources/assets/js/components/index.js
@@ -0,0 +1,28 @@
+
+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'),
+};
+
+window.components = {};
+
+let componentNames = Object.keys(componentMapping);
+
+for (let i = 0, len = componentNames.length; i < len; i++) {
+ let name = componentNames[i];
+ let elems = document.querySelectorAll(`[${name}]`);
+ if (elems.length === 0) continue;
+
+ let component = componentMapping[name];
+ if (typeof window.components[name] === "undefined") window.components[name] = [];
+ 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[name] = instance;
+ window.components[name].push(instance);
+ }
+}
\ No newline at end of file
diff --git a/resources/assets/js/components/notification.js b/resources/assets/js/components/notification.js
new file mode 100644
index 000000000..1a9819702
--- /dev/null
+++ b/resources/assets/js/components/notification.js
@@ -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;
\ No newline at end of file
diff --git a/resources/assets/js/components/overlay.js b/resources/assets/js/components/overlay.js
new file mode 100644
index 000000000..6e7a598ac
--- /dev/null
+++ b/resources/assets/js/components/overlay.js
@@ -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;
\ No newline at end of file
diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js
index 9337ea889..8b37379fa 100644
--- a/resources/assets/js/controllers.js
+++ b/resources/assets/js/controllers.js
@@ -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) {
@@ -395,284 +145,225 @@ module.exports = function (ngApp, events) {
}]);
- ngApp.controller('PageTagController', ['$scope', '$http', '$attrs',
- function ($scope, $http, $attrs) {
+ // Controller used to reply to and add new comments
+ ngApp.controller('CommentReplyController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
+ const MarkdownIt = require("markdown-it");
+ const md = new MarkdownIt({html: true});
+ let vm = this;
- const pageId = Number($attrs.pageId);
- $scope.tags = [];
-
- $scope.sortOptions = {
- handle: '.handle',
- items: '> tr',
- containment: "parent",
- axis: "y"
+ vm.saveComment = function () {
+ let pageId = $scope.comment.pageId || $scope.pageId;
+ let comment = $scope.comment.text;
+ if (!comment) {
+ return events.emit('warning', trans('errors.empty_comment'));
+ }
+ let commentHTML = md.render($scope.comment.text);
+ let serviceUrl = `/ajax/page/${pageId}/comment/`;
+ let httpMethod = 'post';
+ let reqObj = {
+ text: comment,
+ html: commentHTML
};
- /**
- * Push an empty tag to the end of the scope tags.
- */
- function addEmptyTag() {
- $scope.tags.push({
- name: '',
- value: ''
- });
+ if ($scope.isEdit === true) {
+ // this will be set when editing the comment.
+ serviceUrl = `/ajax/page/${pageId}/comment/${$scope.comment.id}`;
+ httpMethod = 'put';
+ } else if ($scope.isReply === true) {
+ // if its reply, get the parent comment id
+ reqObj.parent_id = $scope.parentId;
}
- $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;
+ $http[httpMethod](window.baseUrl(serviceUrl), reqObj).then(resp => {
+ if (!isCommentOpSuccess(resp)) {
+ return;
}
- }
-
- /**
- * 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 = '';
+ // hide the comments first, and then retrigger the refresh
+ if ($scope.isEdit) {
+ updateComment($scope.comment, resp.data);
+ $scope.$emit('evt.comment-success', $scope.comment.id);
+ } else {
+ $scope.comment.text = '';
+ if ($scope.isReply === true && $scope.parent.sub_comments) {
+ $scope.parent.sub_comments.push(resp.data.comment);
+ } else {
+ $scope.$emit('evt.new-comment', resp.data.comment);
}
+ $scope.$emit('evt.comment-success', null, true);
+ }
+ $scope.comment.is_hidden = true;
+ $timeout(function() {
+ $scope.comment.is_hidden = false;
});
- 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;
+ events.emit('success', trans(resp.data.message));
+
+ }, checkError);
+
+ };
+
+ function checkError(response) {
+ let msg = null;
+ if (isCommentOpSuccess(response)) {
+ // all good
+ return;
+ } else if (response.data) {
+ msg = response.data.message;
+ } else {
+ msg = trans('errors.comment_add');
+ }
+ if (msg) {
+ events.emit('success', msg);
+ }
+ }
+ }]);
+
+ // Controller used to delete comments
+ ngApp.controller('CommentDeleteController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
+ let vm = this;
+
+ vm.delete = function(comment) {
+ $http.delete(window.baseUrl(`/ajax/comment/${comment.id}`)).then(resp => {
+ if (!isCommentOpSuccess(resp)) {
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;
+ updateComment(comment, resp.data, $timeout, true);
+ }, function (resp) {
+ if (isCommentOpSuccess(resp)) {
+ events.emit('success', trans('entities.comment_deleted'));
+ } else {
+ events.emit('error', trans('error.comment_delete'));
}
- return -1;
+ });
+ };
+ }]);
+
+ // Controller used to fetch all comments for a page
+ ngApp.controller('CommentListController', ['$scope', '$http', '$timeout', '$location', function ($scope, $http, $timeout, $location) {
+ let vm = this;
+ $scope.errors = {};
+ // keep track of comment levels
+ $scope.level = 1;
+ vm.totalCommentsStr = trans('entities.comments_loading');
+ vm.permissions = {};
+ vm.trans = window.trans;
+
+ $scope.$on('evt.new-comment', function (event, comment) {
+ // add the comment to the comment list.
+ vm.comments.push(comment);
+ ++vm.totalComments;
+ setTotalCommentMsg();
+ event.stopPropagation();
+ event.preventDefault();
+ });
+
+ vm.canEditDelete = function (comment, prop) {
+ if (!comment.active) {
+ return false;
+ }
+ let propAll = prop + '_all';
+ let propOwn = prop + '_own';
+
+ if (vm.permissions[propAll]) {
+ return true;
}
- /**
- * 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])
- }
- }
+ if (vm.permissions[propOwn] && comment.created_by.id === vm.current_user_id) {
+ return true;
}
- }]);
+ return false;
+ };
+ vm.canComment = function () {
+ return vm.permissions.comment_create;
+ };
+
+ // check if there are is any direct linking
+ let linkedCommentId = $location.search().cm;
+
+ $timeout(function() {
+ $http.get(window.baseUrl(`/ajax/page/${$scope.pageId}/comments/`)).then(resp => {
+ if (!isCommentOpSuccess(resp)) {
+ // just show that no comments are available.
+ vm.totalComments = 0;
+ setTotalCommentMsg();
+ return;
+ }
+ vm.comments = resp.data.comments;
+ vm.totalComments = +resp.data.total;
+ vm.permissions = resp.data.permissions;
+ vm.current_user_id = resp.data.user_id;
+ setTotalCommentMsg();
+ if (!linkedCommentId) {
+ return;
+ }
+ $timeout(function() {
+ // wait for the UI to render.
+ focusLinkedComment(linkedCommentId);
+ });
+ }, checkError);
+ });
+
+ function setTotalCommentMsg () {
+ if (vm.totalComments === 0) {
+ vm.totalCommentsStr = trans('entities.no_comments');
+ } else if (vm.totalComments === 1) {
+ vm.totalCommentsStr = trans('entities.one_comment');
+ } else {
+ vm.totalCommentsStr = trans('entities.x_comments', {
+ numComments: vm.totalComments
+ });
+ }
+ }
+
+ function focusLinkedComment(linkedCommentId) {
+ let comment = angular.element('#' + linkedCommentId);
+ if (comment.length === 0) {
+ return;
+ }
+
+ window.setupPageShow.goToText(linkedCommentId);
+ }
+
+ function checkError(response) {
+ let msg = null;
+ if (isCommentOpSuccess(response)) {
+ // all good
+ return;
+ } else if (response.data) {
+ msg = response.data.message;
+ } else {
+ msg = trans('errors.comment_list');
+ }
+ if (msg) {
+ events.emit('success', msg);
+ }
+ }
+ }]);
+
+ function updateComment(comment, resp, $timeout, isDelete) {
+ comment.text = resp.comment.text;
+ comment.updated = resp.comment.updated;
+ comment.updated_by = resp.comment.updated_by;
+ comment.active = resp.comment.active;
+ if (isDelete && !resp.comment.active) {
+ comment.html = trans('entities.comment_deleted');
+ } else {
+ comment.html = resp.comment.html;
+ }
+ if (!$timeout) {
+ return;
+ }
+ comment.is_hidden = true;
+ $timeout(function() {
+ comment.is_hidden = false;
+ });
+ }
+
+ function isCommentOpSuccess(resp) {
+ if (resp && resp.data && resp.data.status === 'success') {
+ return true;
+ }
+ return false;
+ }
};
diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js
index 8d7d89cee..fc92121ff 100644
--- a/resources/assets/js/directives.js
+++ b/resources/assets/js/directives.js
@@ -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: `
-
- `,
- 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.
@@ -422,7 +274,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();
@@ -535,188 +387,6 @@ 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',
@@ -752,6 +422,7 @@ module.exports = function (ngApp, events) {
function hide() {
element.fadeOut(240);
}
+ scope.hide = hide;
// Listen to confirmation of entity selections (doubleclick)
events.listen('entity-select-confirm', entity => {
@@ -863,4 +534,128 @@ module.exports = function (ngApp, events) {
}
};
}]);
+
+ ngApp.directive('commentReply', [function () {
+ return {
+ restrict: 'E',
+ templateUrl: 'comment-reply.html',
+ scope: {
+ pageId: '=',
+ parentId: '=',
+ parent: '='
+ },
+ link: function (scope, element) {
+ scope.isReply = true;
+ element.find('textarea').focus();
+ scope.$on('evt.comment-success', function (event) {
+ // no need for the event to do anything more.
+ event.stopPropagation();
+ event.preventDefault();
+ scope.closeBox();
+ });
+
+ scope.closeBox = function () {
+ element.remove();
+ scope.$destroy();
+ };
+ }
+ };
+ }]);
+
+ ngApp.directive('commentEdit', [function () {
+ return {
+ restrict: 'E',
+ templateUrl: 'comment-reply.html',
+ scope: {
+ comment: '='
+ },
+ link: function (scope, element) {
+ scope.isEdit = true;
+ element.find('textarea').focus();
+ scope.$on('evt.comment-success', function (event, commentId) {
+ // no need for the event to do anything more.
+ event.stopPropagation();
+ event.preventDefault();
+ if (commentId === scope.comment.id && !scope.isNew) {
+ scope.closeBox();
+ }
+ });
+
+ scope.closeBox = function () {
+ element.remove();
+ scope.$destroy();
+ };
+ }
+ };
+ }]);
+
+
+ ngApp.directive('commentReplyLink', ['$document', '$compile', function ($document, $compile) {
+ return {
+ scope: {
+ comment: '='
+ },
+ link: function (scope, element, attr) {
+ element.on('$destroy', function () {
+ element.off('click');
+ scope.$destroy();
+ });
+
+ element.on('click', function (e) {
+ e.preventDefault();
+ var $container = element.parents('.comment-actions').first();
+ if (!$container.length) {
+ console.error('commentReplyLink directive should be placed inside a container with class comment-box!');
+ return;
+ }
+ if (attr.noCommentReplyDupe) {
+ removeDupe();
+ }
+
+ compileHtml($container, scope, attr.isReply === 'true');
+ });
+ }
+ };
+
+ function compileHtml($container, scope, isReply) {
+ let lnkFunc = null;
+ if (isReply) {
+ lnkFunc = $compile('');
+ } else {
+ lnkFunc = $compile('');
+ }
+ var compiledHTML = lnkFunc(scope);
+ $container.append(compiledHTML);
+ }
+
+ function removeDupe() {
+ let $existingElement = $document.find('.comments-list comment-reply, .comments-list comment-edit');
+ if (!$existingElement.length) {
+ return;
+ }
+
+ $existingElement.remove();
+ }
+ }]);
+
+ ngApp.directive('commentDeleteLink', ['$window', function ($window) {
+ return {
+ controller: 'CommentDeleteController',
+ scope: {
+ comment: '='
+ },
+ link: function (scope, element, attr, ctrl) {
+
+ element.on('click', function(e) {
+ e.preventDefault();
+ var resp = $window.confirm(trans('entities.comment_delete_confirm'));
+ if (!resp) {
+ return;
+ }
+
+ ctrl.delete(scope.comment);
+ });
+ }
+ };
+ }]);
};
diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js
index 2ef062a5b..ee7cf3cc1 100644
--- a/resources/assets/js/global.js
+++ b/resources/assets/js/global.js
@@ -1,4 +1,5 @@
"use strict";
+require("babel-polyfill");
// Url retrieval function
window.baseUrl = function(path) {
@@ -8,37 +9,6 @@ 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() {
@@ -63,13 +33,51 @@ class EventManager {
}
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);
+
+
+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);
@@ -91,83 +99,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
diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js
index 4f4c1fbe0..08e4c0c34 100644
--- a/resources/assets/js/pages/page-form.js
+++ b/resources/assets/js/pages/page-form.js
@@ -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 = ``;
html += `
`;
html += '';
diff --git a/resources/assets/js/pages/page-show.js b/resources/assets/js/pages/page-show.js
index 67d339d63..7754840af 100644
--- a/resources/assets/js/pages/page-show.js
+++ b/resources/assets/js/pages/page-show.js
@@ -1,5 +1,3 @@
-"use strict";
-// Configure ZeroClipboard
const Clipboard = require("clipboard");
const Code = require('../code');
@@ -161,6 +159,8 @@ let setupPageShow = window.setupPageShow = function (pageId) {
}
});
+ // in order to call from other places.
+ window.setupPageShow.goToText = goToText;
};
module.exports = setupPageShow;
\ No newline at end of file
diff --git a/resources/assets/js/services.js b/resources/assets/js/services.js
deleted file mode 100644
index cd2759c54..000000000
--- a/resources/assets/js/services.js
+++ /dev/null
@@ -1,12 +0,0 @@
-"use strict";
-
-module.exports = function(ngApp, events) {
-
- ngApp.factory('imageManagerService', function() {
- return {
- show: false,
- showExternal: false
- };
- });
-
-};
\ No newline at end of file
diff --git a/resources/assets/js/vues/attachment-manager.js b/resources/assets/js/vues/attachment-manager.js
new file mode 100644
index 000000000..635622b93
--- /dev/null
+++ b/resources/assets/js/vues/attachment-manager.js
@@ -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,
+};
\ No newline at end of file
diff --git a/resources/assets/js/vues/components/autosuggest.js b/resources/assets/js/vues/components/autosuggest.js
new file mode 100644
index 000000000..4d6b97e55
--- /dev/null
+++ b/resources/assets/js/vues/components/autosuggest.js
@@ -0,0 +1,130 @@
+
+const template = `
+
+
+`;
+
+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};
\ No newline at end of file
diff --git a/resources/assets/js/vues/components/dropzone.js b/resources/assets/js/vues/components/dropzone.js
new file mode 100644
index 000000000..0f31bd579
--- /dev/null
+++ b/resources/assets/js/vues/components/dropzone.js
@@ -0,0 +1,60 @@
+const DropZone = require("dropzone");
+
+const template = `
+
+`;
+
+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,
+};
\ No newline at end of file
diff --git a/resources/assets/js/vues/entity-search.js b/resources/assets/js/vues/entity-dashboard.js
similarity index 100%
rename from resources/assets/js/vues/entity-search.js
rename to resources/assets/js/vues/entity-dashboard.js
diff --git a/resources/assets/js/vues/image-manager.js b/resources/assets/js/vues/image-manager.js
new file mode 100644
index 000000000..12ccc970d
--- /dev/null
+++ b/resources/assets/js/vues/image-manager.js
@@ -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},
+};
\ No newline at end of file
diff --git a/resources/assets/js/vues/tag-manager.js b/resources/assets/js/vues/tag-manager.js
new file mode 100644
index 000000000..d97ceb96b
--- /dev/null
+++ b/resources/assets/js/vues/tag-manager.js
@@ -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
+};
\ No newline at end of file
diff --git a/resources/assets/js/vues/vues.js b/resources/assets/js/vues/vues.js
index 31d833bfb..a70d32009 100644
--- a/resources/assets/js/vues/vues.js
+++ b/resources/assets/js/vues/vues.js
@@ -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);
- }
-});
\ No newline at end of file
+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);
+}
\ No newline at end of file
diff --git a/resources/assets/sass/_animations.scss b/resources/assets/sass/_animations.scss
index 467399a66..015a23ab1 100644
--- a/resources/assets/sass/_animations.scss
+++ b/resources/assets/sass/_animations.scss
@@ -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);
diff --git a/resources/assets/sass/_blocks.scss b/resources/assets/sass/_blocks.scss
index bd3f8ff4e..e3a0d6952 100644
--- a/resources/assets/sass/_blocks.scss
+++ b/resources/assets/sass/_blocks.scss
@@ -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 {
diff --git a/resources/assets/sass/_buttons.scss b/resources/assets/sass/_buttons.scss
index 6e03c9217..202eb935b 100644
--- a/resources/assets/sass/_buttons.scss
+++ b/resources/assets/sass/_buttons.scss
@@ -31,7 +31,6 @@ $button-border-radius: 2px;
display: inline-block;
border: none;
font-weight: 500;
- font-family: $text;
outline: 0;
border-radius: $button-border-radius;
cursor: pointer;
@@ -65,6 +64,7 @@ $button-border-radius: 2px;
padding: 0;
margin: 0;
border: none;
+ user-select: none;
&:focus, &:active {
outline: 0;
}
diff --git a/resources/assets/sass/_codemirror.scss b/resources/assets/sass/_codemirror.scss
index bd85218a5..e281d4c0d 100644
--- a/resources/assets/sass/_codemirror.scss
+++ b/resources/assets/sass/_codemirror.scss
@@ -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;
diff --git a/resources/assets/sass/_comments.scss b/resources/assets/sass/_comments.scss
new file mode 100644
index 000000000..5da53a14d
--- /dev/null
+++ b/resources/assets/sass/_comments.scss
@@ -0,0 +1,82 @@
+.comments-list {
+ .comment-box {
+ border-bottom: 1px solid $comment-border;
+ }
+
+ .comment-box:last-child {
+ border-bottom: 0px;
+ }
+}
+.page-comment {
+ .comment-container {
+ margin-left: 42px;
+ }
+
+ .comment-actions {
+ font-size: 0.8em;
+ padding-bottom: 2px;
+
+ ul {
+ padding-left: 0px;
+ margin-bottom: 2px;
+ }
+ li {
+ float: left;
+ list-style-type: none;
+ }
+
+ li:after {
+ content: '•';
+ color: #707070;
+ padding: 0 5px;
+ font-size: 1em;
+ }
+
+ li:last-child:after {
+ content: none;
+ }
+ }
+
+ .comment-actions {
+ border-bottom: 1px solid #DDD;
+ }
+
+ .comment-actions:last-child {
+ border-bottom: 0px;
+ }
+
+ .comment-header {
+ font-size: 1.25em;
+ margin-top: 0.6em;
+ }
+
+ .comment-body p {
+ margin-bottom: 1em;
+ }
+
+ .comment-inactive {
+ font-style: italic;
+ font-size: 0.85em;
+ padding-top: 5px;
+ }
+
+ .user-image {
+ float: left;
+ margin-right: 10px;
+ width: 32px;
+ img {
+ width: 100%;
+ }
+ }
+}
+
+.comment-editor {
+ margin-top: 2em;
+
+ textarea {
+ display: block;
+ width: 100%;
+ max-width: 100%;
+ min-height: 120px;
+ }
+}
diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss
index 12babae73..525b4f8f1 100644
--- a/resources/assets/sass/_components.scss
+++ b/resources/assets/sass/_components.scss
@@ -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;
diff --git a/resources/assets/sass/_fonts.scss b/resources/assets/sass/_fonts.scss
deleted file mode 100644
index c8e8ea833..000000000
--- a/resources/assets/sass/_fonts.scss
+++ /dev/null
@@ -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+ */
-}
\ No newline at end of file
diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss
index 392e9ec3e..d372359cc 100644
--- a/resources/assets/sass/_forms.scss
+++ b/resources/assets/sass/_forms.scss
@@ -5,7 +5,6 @@
border: 1px solid #CCC;
display: inline-block;
font-size: $fs-s;
- font-family: $text;
padding: $-xs;
color: #222;
width: 250px;
@@ -33,7 +32,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 +67,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;
@@ -251,21 +248,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: 7px;
+ top: 7px;
}
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;
diff --git a/resources/assets/sass/_header.scss b/resources/assets/sass/_header.scss
index ae8dd3ff5..49bd74b07 100644
--- a/resources/assets/sass/_header.scss
+++ b/resources/assets/sass/_header.scss
@@ -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: $-s;
+ 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;
}
@@ -227,4 +231,7 @@ form.search-box {
border-bottom: 2px solid $primary;
}
}
+}
+.faded-small .nav-tabs a {
+ padding: $-s $-m;
}
\ No newline at end of file
diff --git a/resources/assets/sass/_html.scss b/resources/assets/sass/_html.scss
index c061f9d64..27ca04eb7 100644
--- a/resources/assets/sass/_html.scss
+++ b/resources/assets/sass/_html.scss
@@ -12,7 +12,6 @@ html {
}
body {
- font-family: $text;
font-size: $fs-m;
line-height: 1.6;
color: #616161;
diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss
index 051268926..d08ccc9bb 100644
--- a/resources/assets/sass/_lists.scss
+++ b/resources/assets/sass/_lists.scss
@@ -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;
@@ -171,7 +156,7 @@
background-color: rgba($color-chapter, 0.12);
}
}
- .chapter-toggle {
+ [chapter-toggle] {
padding-left: $-s;
}
.list-item-chapter {
@@ -336,8 +321,10 @@ ul.pagination {
h4, a {
line-height: 1.2;
}
- p {
+ .entity-item-snippet {
display: none;
+ }
+ p {
font-size: $fs-m * 0.8;
padding-top: $-xs;
margin: 0;
diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss
index e5334c69c..65fdfbc4b 100755
--- a/resources/assets/sass/_pages.scss
+++ b/resources/assets/sass/_pages.scss
@@ -226,7 +226,7 @@
width: 100%;
min-width: 50px;
}
- .tags td {
+ .tags td, .tag-table > div > div > div {
padding-right: $-s;
padding-top: $-s;
position: relative;
@@ -310,4 +310,8 @@
background-color: #EEE;
}
}
+}
+
+.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
+ min-height: 175px;
}
\ No newline at end of file
diff --git a/resources/assets/sass/_tables.scss b/resources/assets/sass/_tables.scss
index 21553b839..31ac92f60 100644
--- a/resources/assets/sass/_tables.scss
+++ b/resources/assets/sass/_tables.scss
@@ -59,12 +59,16 @@ table.list-table {
}
}
-table.file-table {
- @extend .no-style;
- td {
- padding: $-xs;
+.fake-table {
+ display: table;
+ width: 100%;
+ > div {
+ display: table-row-group;
}
- .ui-sortable-helper {
- display: table;
+ > div > div {
+ display: table-row;
+ }
+ > div > div > div {
+ display: table-cell;
}
}
\ No newline at end of file
diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss
index 2ef4bd16d..d38a5c515 100644
--- a/resources/assets/sass/_text.scss
+++ b/resources/assets/sass/_text.scss
@@ -1,3 +1,14 @@
+/**
+ * Fonts
+ */
+
+body, button, input, select, label {
+ 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;
diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss
index 23bf2b219..18880fa5b 100644
--- a/resources/assets/sass/_variables.scss
+++ b/resources/assets/sass/_variables.scss
@@ -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;
@@ -56,3 +60,6 @@ $text-light: #EEE;
$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);
+
+// comments
+$comment-border: #DDD;
\ No newline at end of file
diff --git a/resources/assets/sass/export-styles.scss b/resources/assets/sass/export-styles.scss
index 60450f3e2..19579004b 100644
--- a/resources/assets/sass/export-styles.scss
+++ b/resources/assets/sass/export-styles.scss
@@ -1,4 +1,3 @@
-//@import "reset";
@import "variables";
@import "mixins";
@import "html";
@@ -10,6 +9,7 @@
@import "header";
@import "lists";
@import "pages";
+@import "comments";
table {
border-spacing: 0;
diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss
index afb9d531b..b912bf7ee 100644
--- a/resources/assets/sass/styles.scss
+++ b/resources/assets/sass/styles.scss
@@ -1,6 +1,5 @@
@import "reset";
@import "variables";
-@import "fonts";
@import "mixins";
@import "html";
@import "text";
@@ -16,13 +15,13 @@
@import "header";
@import "lists";
@import "pages";
+@import "comments";
-[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,44 +64,6 @@ 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 {
@@ -150,7 +111,7 @@ $loadingSize: 10px;
// Back to top link
$btt-size: 40px;
-#back-to-top {
+[back-to-top] {
background-color: $primary;
position: fixed;
bottom: $-m;
diff --git a/resources/lang/de/activities.php b/resources/lang/de/activities.php
index c2d20b3a6..3318ea752 100644
--- a/resources/lang/de/activities.php
+++ b/resources/lang/de/activities.php
@@ -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:',
];
diff --git a/resources/lang/de/auth.php b/resources/lang/de/auth.php
index f253cdfa1..8f4afe654 100644
--- a/resources/lang/de/auth.php
+++ b/resources/lang/de/auth.php
@@ -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',
];
diff --git a/resources/lang/de/common.php b/resources/lang/de/common.php
index 7ad1743a0..3c21a9d08 100644
--- a/resources/lang/de/common.php
+++ b/resources/lang/de/common.php
@@ -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',
-];
\ No newline at end of file
+];
diff --git a/resources/lang/de/components.php b/resources/lang/de/components.php
index a8538c465..26bf3e626 100644
--- a/resources/lang/de/components.php
+++ b/resources/lang/de/components.php
@@ -13,12 +13,12 @@ return [
'image_uploaded' => 'Hochgeladen am :uploadedDate',
'image_load_more' => 'Mehr',
'image_image_name' => 'Bildname',
- 'image_delete_confirm' => 'Dieses Bild wird auf den folgenden Seiten benutzt. Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild tatsächlich entfernen möchten.',
+ 'image_delete_confirm' => 'Dieses Bild wird auf den folgenden Seiten benutzt. Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild wirklich entfernen möchten.',
'image_select_image' => 'Bild auswählen',
- 'image_dropzone' => 'Ziehen Sie Bilder hier hinein oder klicken Sie hier, um ein Bild auszuwählen',
+ 'image_dropzone' => 'Ziehen Sie Bilder hierher oder klicken Sie, um ein Bild auszuwählen',
'images_deleted' => 'Bilder gelöscht',
'image_preview' => 'Bildvorschau',
'image_upload_success' => 'Bild erfolgreich hochgeladen',
'image_update_success' => 'Bilddetails erfolgreich aktualisiert',
'image_delete_success' => 'Bild erfolgreich gelöscht'
-];
\ No newline at end of file
+];
diff --git a/resources/lang/de/entities.php b/resources/lang/de/entities.php
index c9feb8497..910218a58 100644
--- a/resources/lang/de/entities.php
+++ b/resources/lang/de/entities.php
@@ -4,38 +4,39 @@ return [
/**
* Shared
*/
- 'recently_created' => 'Kürzlich angelegt',
- 'recently_created_pages' => 'Kürzlich angelegte Seiten',
- 'recently_updated_pages' => 'Kürzlich aktualisierte Seiten',
- 'recently_created_chapters' => 'Kürzlich angelegte Kapitel',
- 'recently_created_books' => 'Kürzlich angelegte Bücher',
- 'recently_update' => 'Kürzlich aktualisiert',
- 'recently_viewed' => 'Kürzlich angesehen',
- 'recent_activity' => 'Kürzliche Aktivität',
+ 'recently_created' => 'Kürzlich angelegt',
+ 'recently_created_pages' => 'Kürzlich angelegte Seiten',
+ 'recently_updated_pages' => 'Kürzlich aktualisierte Seiten',
+ 'recently_created_chapters' => 'Kürzlich angelegte Kapitel',
+ 'recently_created_books' => 'Kürzlich angelegte Bücher',
+ 'recently_update' => 'Kürzlich aktualisiert',
+ 'recently_viewed' => 'Kürzlich angesehen',
+ 'recent_activity' => 'Kürzliche Aktivität',
'create_now' => 'Jetzt anlegen',
- 'revisions' => 'Revisionen',
- 'meta_created' => 'Angelegt am :timeLength',
- 'meta_created_name' => 'Angelegt am :timeLength durch :user',
- 'meta_updated' => 'Aktualisiert am :timeLength',
- 'meta_updated_name' => 'Aktualisiert am :timeLength durch :user',
+ 'revisions' => 'Versionen',
+ 'meta_revision' => 'Version #:revisionCount',
+ 'meta_created' => 'Erstellt: :timeLength',
+ 'meta_created_name' => 'Erstellt: :timeLength von :user',
+ 'meta_updated' => 'Zuletzt aktualisiert: :timeLength',
+ 'meta_updated_name' => 'Zuletzt aktualisiert: :timeLength von :user',
'x_pages' => ':count Seiten',
- 'entity_select' => 'Eintrag auswählen',
+ 'entity_select' => 'Eintrag auswählen',
'images' => 'Bilder',
- 'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
- 'my_recently_viewed' => 'Kürzlich von mir angesehen',
+ 'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
+ 'my_recently_viewed' => 'Kürzlich von mir angesehen',
'no_pages_viewed' => 'Sie haben bisher keine Seiten angesehen.',
'no_pages_recently_created' => 'Sie haben bisher keine Seiten angelegt.',
'no_pages_recently_updated' => 'Sie haben bisher keine Seiten aktualisiert.',
'export' => 'Exportieren',
'export_html' => 'HTML-Datei',
'export_pdf' => 'PDF-Datei',
- 'export_text' => 'Text-Datei',
+ 'export_text' => 'Textdatei',
/**
* Permissions and restrictions
*/
'permissions' => 'Berechtigungen',
- 'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, überschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.',
+ 'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, überschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.',
'permissions_enable' => 'Individuelle Berechtigungen aktivieren',
'permissions_save' => 'Berechtigungen speichern',
@@ -43,41 +44,58 @@ return [
* Search
*/
'search_results' => 'Suchergebnisse',
- 'search_clear' => 'Suche zurücksetzen',
- 'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden',
- 'search_for_term' => 'Suche nach :term',
+ 'search_total_results_found' => ':count Ergebnis gefunden|:count Ergebnisse gesamt',
+ 'search_clear' => 'Filter löschen',
+ 'search_no_pages' => 'Keine Seiten gefunden',
+ 'search_for_term' => 'Nach :term suchen',
+ 'search_more' => 'Mehr Ergebnisse',
+ 'search_filters' => 'Filter',
+ 'search_content_type' => 'Inhaltstyp',
+ 'search_exact_matches' => 'Exakte Treffer',
+ 'search_tags' => 'Nach Schlagwort suchen',
+ 'search_viewed_by_me' => 'Schon von mir angesehen',
+ 'search_not_viewed_by_me' => 'Noch nicht von mir angesehen',
+ 'search_permissions_set' => 'Berechtigungen gesetzt',
+ 'search_created_by_me' => 'Von mir erstellt',
+ 'search_updated_by_me' => 'Von mir aktualisiert',
+ 'search_updated_before' => 'Aktualisiert vor',
+ 'search_updated_after' => 'Aktualisiert nach',
+ 'search_created_before' => 'Erstellt vor',
+ 'search_created_after' => 'Erstellt nach',
+ 'search_set_date' => 'Datum auswählen',
+ 'search_update' => 'Suche aktualisieren',
/**
* Books
*/
'book' => 'Buch',
- 'books' => 'Bücher',
- 'books_empty' => 'Es wurden keine Bücher angelegt',
- 'books_popular' => 'Populäre Bücher',
- 'books_recent' => 'Kürzlich genutzte Bücher',
- 'books_popular_empty' => 'Die populärsten Bücher werden hier angezeigt.',
- 'books_create' => 'Neues Buch anlegen',
- 'books_delete' => 'Buch löschen',
- 'books_delete_named' => 'Buch :bookName löschen',
- 'books_delete_explain' => 'Sie möchten das Buch \':bookName\' löschen und alle Seiten und Kapitel entfernen.',
- 'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch löschen möchten?',
+ 'books' => 'Bücher',
+ 'books_empty' => 'Keine Bücher vorhanden',
+ 'books_popular' => 'Beliebte Bücher',
+ 'books_recent' => 'Kürzlich angesehene Bücher',
+ 'books_popular_empty' => 'Die beliebtesten Bücher werden hier angezeigt.',
+ 'books_create' => 'Neues Buch erstellen',
+ 'books_delete' => 'Buch löschen',
+ 'books_delete_named' => 'Buch ":bookName" löschen',
+ 'books_delete_explain' => 'Das Buch ":bookName" wird gelöscht und alle zugehörigen Kapitel und Seiten entfernt.',
+ 'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch löschen möchten?',
'books_edit' => 'Buch bearbeiten',
- 'books_edit_named' => 'Buch :bookName bearbeiten',
- 'books_form_book_name' => 'Buchname',
+ 'books_edit_named' => 'Buch ":bookName" bearbeiten',
+ 'books_form_book_name' => 'Name des Buches',
'books_save' => 'Buch speichern',
'books_permissions' => 'Buch-Berechtigungen',
'books_permissions_updated' => 'Buch-Berechtigungen aktualisiert',
- 'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel für dieses Buch angelegt.',
+ 'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel zu diesem Buch hinzugefügt worden.',
'books_empty_create_page' => 'Neue Seite anlegen',
'books_empty_or' => 'oder',
'books_empty_sort_current_book' => 'Aktuelles Buch sortieren',
- 'books_empty_add_chapter' => 'Neues Kapitel hinzufügen',
+ 'books_empty_add_chapter' => 'Neues Kapitel hinzufügen',
'books_permissions_active' => 'Buch-Berechtigungen aktiv',
'books_search_this' => 'Dieses Buch durchsuchen',
- 'books_navigation' => 'Buch-Navigation',
+ 'books_navigation' => 'Buchnavigation',
'books_sort' => 'Buchinhalte sortieren',
- 'books_sort_named' => 'Buch :bookName sortieren',
- 'books_sort_show_other' => 'Andere Bücher zeigen',
+ 'books_sort_named' => 'Buch ":bookName" sortieren',
+ 'books_sort_show_other' => 'Andere Bücher anzeigen',
'books_sort_save' => 'Neue Reihenfolge speichern',
/**
@@ -85,132 +103,157 @@ return [
*/
'chapter' => 'Kapitel',
'chapters' => 'Kapitel',
- 'chapters_popular' => 'Populäre Kapitel',
+ 'chapters_popular' => 'Beliebte Kapitel',
'chapters_new' => 'Neues Kapitel',
'chapters_create' => 'Neues Kapitel anlegen',
'chapters_delete' => 'Kapitel entfernen',
- 'chapters_delete_named' => 'Kapitel :chapterName entfernen',
- 'chapters_delete_explain' => 'Sie möchten das Kapitel \':chapterName\' löschen und alle Seiten dem direkten Eltern-Buch hinzugefügen.',
- 'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel löschen möchten?',
+ 'chapters_delete_named' => 'Kapitel ":chapterName" entfernen',
+ 'chapters_delete_explain' => 'Das Kapitel ":chapterName" wird gelöscht und alle zugehörigen Seiten dem übergeordneten Buch zugeordnet.',
+ 'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel löschen möchten?',
'chapters_edit' => 'Kapitel bearbeiten',
- 'chapters_edit_named' => 'Kapitel :chapterName bearbeiten',
+ 'chapters_edit_named' => 'Kapitel ":chapterName" bearbeiten',
'chapters_save' => 'Kapitel speichern',
'chapters_move' => 'Kapitel verschieben',
- 'chapters_move_named' => 'Kapitel :chapterName verschieben',
- 'chapter_move_success' => 'Kapitel in das Buch :bookName verschoben.',
+ 'chapters_move_named' => 'Kapitel ":chapterName" verschieben',
+ 'chapter_move_success' => 'Das Kapitel wurde in das Buch ":bookName" verschoben.',
'chapters_permissions' => 'Kapitel-Berechtigungen',
- 'chapters_empty' => 'Aktuell sind keine Kapitel in diesem Buch angelegt.',
+ 'chapters_empty' => 'Aktuell sind keine Kapitel diesem Buch hinzugefügt worden.',
'chapters_permissions_active' => 'Kapitel-Berechtigungen aktiv',
'chapters_permissions_success' => 'Kapitel-Berechtigungenen aktualisisert',
+ 'chapters_search_this' => 'Dieses Kapitel durchsuchen',
/**
* Pages
*/
'page' => 'Seite',
'pages' => 'Seiten',
- 'pages_popular' => 'Populäre Seiten',
+ 'pages_popular' => 'Beliebte Seiten',
'pages_new' => 'Neue Seite',
- 'pages_attachments' => 'Anhänge',
+ 'pages_attachments' => 'Anhänge',
'pages_navigation' => 'Seitennavigation',
- 'pages_delete' => 'Seite löschen',
- 'pages_delete_named' => 'Seite :pageName löschen',
- 'pages_delete_draft_named' => 'Seitenentwurf von :pageName löschen',
- 'pages_delete_draft' => 'Seitenentwurf löschen',
- 'pages_delete_success' => 'Seite gelöscht',
- 'pages_delete_draft_success' => 'Seitenentwurf gelöscht',
- 'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite löschen möchen?',
- 'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf löschen möchten?',
- 'pages_editing_named' => 'Seite :pageName bearbeiten',
- 'pages_edit_toggle_header' => 'Toggle header',
+ 'pages_delete' => 'Seite löschen',
+ 'pages_delete_named' => 'Seite ":pageName" löschen',
+ 'pages_delete_draft_named' => 'Seitenentwurf von ":pageName" löschen',
+ 'pages_delete_draft' => 'Seitenentwurf löschen',
+ 'pages_delete_success' => 'Seite gelöscht',
+ 'pages_delete_draft_success' => 'Seitenentwurf gelöscht',
+ 'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite löschen möchen?',
+ 'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf löschen möchten?',
+ 'pages_editing_named' => 'Seite ":pageName" bearbeiten',
+ 'pages_edit_toggle_header' => 'Hauptmenü anzeigen/verstecken',
'pages_edit_save_draft' => 'Entwurf speichern',
'pages_edit_draft' => 'Seitenentwurf bearbeiten',
'pages_editing_draft' => 'Seitenentwurf bearbeiten',
'pages_editing_page' => 'Seite bearbeiten',
'pages_edit_draft_save_at' => 'Entwurf gespeichert um ',
- 'pages_edit_delete_draft' => 'Entwurf löschen',
+ 'pages_edit_delete_draft' => 'Entwurf löschen',
'pages_edit_discard_draft' => 'Entwurf verwerfen',
- 'pages_edit_set_changelog' => 'Veränderungshinweis setzen',
- 'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer Änderungen ein',
- 'pages_edit_enter_changelog' => 'Veränderungshinweis eingeben',
+ 'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen',
+ 'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer Änderungen ein',
+ 'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben',
'pages_save' => 'Seite speichern',
'pages_title' => 'Seitentitel',
'pages_name' => 'Seitenname',
'pages_md_editor' => 'Redakteur',
'pages_md_preview' => 'Vorschau',
- 'pages_md_insert_image' => 'Bild einfügen',
- 'pages_md_insert_link' => 'Link zu einem Objekt einfügen',
+ 'pages_md_insert_image' => 'Bild einfügen',
+ 'pages_md_insert_link' => 'Link zu einem Objekt einfügen',
'pages_not_in_chapter' => 'Seite ist in keinem Kapitel',
'pages_move' => 'Seite verschieben',
'pages_move_success' => 'Seite nach ":parentName" verschoben',
'pages_permissions' => 'Seiten Berechtigungen',
'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert',
'pages_revisions' => 'Seitenversionen',
- 'pages_revisions_named' => 'Seitenversionen von :pageName',
- 'pages_revision_named' => 'Seitenversion von :pageName',
- 'pages_revisions_created_by' => 'Angelegt von',
+ 'pages_revisions_named' => 'Seitenversionen von ":pageName"',
+ 'pages_revision_named' => 'Seitenversion von ":pageName"',
+ 'pages_revisions_created_by' => 'Erstellt von',
'pages_revisions_date' => 'Versionsdatum',
- 'pages_revisions_changelog' => 'Veränderungshinweise',
- 'pages_revisions_changes' => 'Veränderungen',
+ 'pages_revisions_number' => '#',
+ 'pages_revisions_changelog' => 'Änderungsprotokoll',
+ 'pages_revisions_changes' => 'Änderungen',
'pages_revisions_current' => 'Aktuelle Version',
'pages_revisions_preview' => 'Vorschau',
- 'pages_revisions_restore' => 'Zurück sichern',
- 'pages_revisions_none' => 'Diese Seite hat keine älteren Versionen.',
+ 'pages_revisions_restore' => 'Wiederherstellen',
+ 'pages_revisions_none' => 'Diese Seite hat keine älteren Versionen.',
'pages_copy_link' => 'Link kopieren',
'pages_permissions_active' => 'Seiten-Berechtigungen aktiv',
- 'pages_initial_revision' => 'Erste Veröffentlichung',
+ 'pages_initial_revision' => 'Erste Veröffentlichung',
'pages_initial_name' => 'Neue Seite',
- 'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt um :timeDiff gespeichert wurde.',
- 'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
+ 'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt :timeDiff gespeichert wurde.',
+ 'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
'pages_draft_edit_active' => [
- 'start_a' => ':count Benutzer haben die Bearbeitung dieser Seite begonnen.',
- 'start_b' => ':userName hat die Bearbeitung dieser Seite begonnen.',
+ 'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.',
+ 'start_b' => ':userName bearbeitet jetzt diese Seite.',
'time_a' => 'seit die Seiten zuletzt aktualisiert wurden.',
'time_b' => 'in den letzten :minCount Minuten',
- 'message' => ':start :time. Achten Sie darauf keine Aktualisierungen von anderen Benutzern zu überschreiben!',
+ 'message' => ':start :time. Achten Sie darauf, keine Änderungen von anderen Benutzern zu überschreiben!',
],
'pages_draft_discarded' => 'Entwurf verworfen. Der aktuelle Seiteninhalt wurde geladen.',
/**
* Editor sidebar
*/
- 'page_tags' => 'Seiten-Schlagwörter',
+ 'page_tags' => 'Seiten-Schlagwörter',
'tag' => 'Schlagwort',
- 'tags' => 'Schlagworte',
- 'tag_value' => 'Schlagwortinhalt (Optional)',
- 'tags_explain' => "Fügen Sie Schlagworte hinzu, um Ihren Inhalt zu kategorisieren. \n Sie können einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.",
- 'tags_add' => 'Weiteres Schlagwort hinzufügen',
- 'attachments' => 'Anhänge',
- 'attachments_explain' => 'Sie können auf Ihrer Seite Dateien hochladen oder Links anfügen. Diese werden in der seitlich angezeigt.',
- 'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.',
- 'attachments_items' => 'Angefügte Elemente',
+ 'tags' => 'Schlagwörter',
+ 'tag_value' => 'Inhalt (Optional)',
+ 'tags_explain' => "Fügen Sie Schlagwörter hinzu, um Ihren Inhalt zu kategorisieren.\nSie können einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.",
+ 'tags_add' => 'Weiteres Schlagwort hinzufügen',
+ 'attachments' => 'Anhänge',
+ 'attachments_explain' => 'Sie können auf Ihrer Seite Dateien hochladen oder Links hinzufügen. Diese werden in der Seitenleiste angezeigt.',
+ 'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.',
+ 'attachments_items' => 'Angefügte Elemente',
'attachments_upload' => 'Datei hochladen',
- 'attachments_link' => 'Link anfügen',
+ 'attachments_link' => 'Link hinzufügen',
'attachments_set_link' => 'Link setzen',
- 'attachments_delete_confirm' => 'Klicken Sie erneut auf löschen, um diesen Anhang zu entfernen.',
- 'attachments_dropzone' => 'Ziehen Sie Dateien hier hinein oder klicken Sie hier, um eine Datei auszuwählen',
+ 'attachments_delete_confirm' => 'Klicken Sie erneut auf löschen, um diesen Anhang zu entfernen.',
+ 'attachments_dropzone' => 'Ziehen Sie Dateien hierher oder klicken Sie, um eine Datei auszuwählen',
'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.',
- 'attachments_explain_link' => 'Wenn Sie keine Datei hochladen möchten, können Sie stattdessen einen Link anfügen. Dieser Link kann auf eine andere Seite oder zu einer Datei in der Cloud weisen.',
+ 'attachments_explain_link' => 'Wenn Sie keine Datei hochladen möchten, können Sie stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet weisen.',
'attachments_link_name' => 'Link-Name',
'attachment_link' => 'Link zum Anhang',
'attachments_link_url' => 'Link zu einer Datei',
'attachments_link_url_hint' => 'URL einer Seite oder Datei',
- 'attach' => 'anfügen',
+ 'attach' => 'Hinzufügen',
'attachments_edit_file' => 'Datei bearbeiten',
'attachments_edit_file_name' => 'Dateiname',
- 'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hier hinein, um diese hochzuladen und zu überschreiben',
- 'attachments_order_updated' => 'Reihenfolge der Anhänge aktualisiert',
- 'attachments_updated_success' => 'Anhang-Details aktualisiert',
- 'attachments_deleted' => 'Anhang gelöscht',
- 'attachments_file_uploaded' => 'Datei erfolgrecich hochgeladen',
- 'attachments_file_updated' => 'Datei erfolgreich aktualisisert',
- 'attachments_link_attached' => 'Link erfolgreich der Seite hinzugefügt',
+ 'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hierher, um diese hochzuladen und zu überschreiben',
+ 'attachments_order_updated' => 'Reihenfolge der Anhänge aktualisiert',
+ 'attachments_updated_success' => 'Anhangdetails aktualisiert',
+ 'attachments_deleted' => 'Anhang gelöscht',
+ 'attachments_file_uploaded' => 'Datei erfolgreich hochgeladen',
+ 'attachments_file_updated' => 'Datei erfolgreich aktualisiert',
+ 'attachments_link_attached' => 'Link erfolgreich der Seite hinzugefügt',
/**
* Profile View
*/
'profile_user_for_x' => 'Benutzer seit :time',
- 'profile_created_content' => 'Angelegte Inhalte',
- 'profile_not_created_pages' => ':userName hat bisher keine Seiten angelegt.',
- 'profile_not_created_chapters' => ':userName hat bisher keine Kapitel angelegt.',
- 'profile_not_created_books' => ':userName hat bisher keine Bücher angelegt.',
-];
\ No newline at end of file
+ 'profile_created_content' => 'Erstellte Inhalte',
+ 'profile_not_created_pages' => ':userName hat noch keine Seiten erstellt.',
+ 'profile_not_created_chapters' => ':userName hat noch keine Kapitel erstellt.',
+ 'profile_not_created_books' => ':userName hat noch keine Bücher erstellt.',
+
+ /**
+ * Comnents
+ */
+ 'comment' => 'Kommentar',
+ 'comments' => 'Kommentare',
+ 'comment_placeholder' => 'Geben Sie hier Ihre Kommentare ein (Markdown unterstützt)',
+ 'no_comments' => 'Keine Kommentare',
+ 'x_comments' => ':numComments Kommentare',
+ 'one_comment' => '1 Kommentar',
+ 'comments_loading' => 'Laden...',
+ 'comment_save' => 'Kommentar speichern',
+ 'comment_reply' => 'Antworten',
+ 'comment_edit' => 'Bearbeiten',
+ 'comment_delete' => 'Löschen',
+ 'comment_cancel' => 'Abbrechen',
+ 'comment_created' => 'Kommentar hinzugefügt',
+ 'comment_updated' => 'Kommentar aktualisiert',
+ 'comment_deleted' => 'Kommentar gelöscht',
+ 'comment_updated_text' => 'Aktualisiert vor :updateDiff von',
+ 'comment_delete_confirm' => 'Der Inhalt des Kommentars wird entfernt. Bist du sicher, dass du diesen Kommentar löschen möchtest?',
+ 'comment_create' => 'Erstellt'
+
+];
diff --git a/resources/lang/de/errors.php b/resources/lang/de/errors.php
index e085d9915..0b961f8ee 100644
--- a/resources/lang/de/errors.php
+++ b/resources/lang/de/errors.php
@@ -7,37 +7,37 @@ return [
*/
// Pages
- 'permission' => 'Sie haben keine Berechtigung auf diese Seite zuzugreifen.',
- 'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuführen.',
+ 'permission' => 'Sie haben keine Berechtigung, auf diese Seite zuzugreifen.',
+ 'permissionJson' => 'Sie haben keine Berechtigung, die angeforderte Aktion auszuführen.',
// Auth
- 'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten angelegt.',
- 'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melden Sie sich an.',
- 'email_confirmation_invalid' => 'Der Bestätigungs-Token ist nicht gültig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.',
- 'email_confirmation_expired' => 'Der Bestätigungs-Token ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.',
- 'ldap_fail_anonymous' => 'Anonymer LDAP Zugriff ist fehlgeschlafgen',
- 'ldap_fail_authed' => 'LDAP Zugriff mit DN & Passwort ist fehlgeschlagen',
- 'ldap_extension_not_installed' => 'LDAP PHP Erweiterung ist nicht installiert.',
- 'ldap_cannot_connect' => 'Die Verbindung zu LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.',
+ 'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten registriert.',
+ 'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melden Sie sich an.',
+ 'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.',
+ 'email_confirmation_expired' => 'Der Bestätigungslink ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.',
+ 'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlafgen',
+ 'ldap_fail_authed' => 'LDAP-Zugriff mit DN und Passwort ist fehlgeschlagen',
+ 'ldap_extension_not_installed' => 'LDAP-PHP-Erweiterung ist nicht installiert.',
+ 'ldap_cannot_connect' => 'Die Verbindung zum LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.',
'social_no_action_defined' => 'Es ist keine Aktion definiert',
- 'social_account_in_use' => 'Dieses :socialAccount Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount Konto an.',
- 'social_account_email_in_use' => 'Die E-Mail-Adresse :email ist bereits registriert. Wenn Sie bereits registriert sind, können Sie Ihr :socialAccount Konto in Ihren Profil-Einstellungen verknüpfen.',
- 'social_account_existing' => 'Dieses :socialAccount Konto ist bereits mit Ihrem Profil verknüpft.',
- 'social_account_already_used_existing' => 'Dieses :socialAccount Konto wird bereits durch einen anderen Benutzer verwendet.',
- 'social_account_not_used' => 'Dieses :socialAccount Konto ist bisher keinem Benutzer zugeordnet. Bitte verknüpfen Sie deses in Ihrem Profil-Einstellungen.',
- 'social_account_register_instructions' => 'Wenn Sie bisher keinen Social-Media Konto besitzen können Sie ein solches Konto mit der :socialAccount Option anlegen.',
- 'social_driver_not_found' => 'Social-Media Konto Treiber nicht gefunden',
- 'social_driver_not_configured' => 'Ihr :socialAccount Konto ist nicht korrekt konfiguriert.',
+ 'social_account_in_use' => 'Dieses :socialAccount-Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount-Konto an.',
+ 'social_account_email_in_use' => 'Die E-Mail-Adresse ":email" ist bereits registriert. Wenn Sie bereits registriert sind, können Sie Ihr :socialAccount-Konto in Ihren Profil-Einstellungen verknüpfen.',
+ 'social_account_existing' => 'Dieses :socialAccount-Konto ist bereits mit Ihrem Profil verknüpft.',
+ 'social_account_already_used_existing' => 'Dieses :socialAccount-Konto wird bereits von einem anderen Benutzer verwendet.',
+ 'social_account_not_used' => 'Dieses :socialAccount-Konto ist bisher keinem Benutzer zugeordnet. Sie können es in Ihren Profil-Einstellung.',
+ 'social_account_register_instructions' => 'Wenn Sie bisher keinen Social-Media Konto besitzen, können Sie ein solches Konto mit der :socialAccount Option anlegen.',
+ 'social_driver_not_found' => 'Treiber für Social-Media-Konten nicht gefunden',
+ 'social_driver_not_configured' => 'Ihr :socialAccount-Konto ist nicht korrekt konfiguriert.',
// System
'path_not_writable' => 'Die Datei kann nicht in den angegebenen Pfad :filePath hochgeladen werden. Stellen Sie sicher, dass dieser Ordner auf dem Server beschreibbar ist.',
'cannot_get_image_from_url' => 'Bild konnte nicht von der URL :url geladen werden.',
- 'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfen Sie, ob Sie die GD PHP Erweiterung installiert haben.',
- 'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.',
+ 'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfen Sie, ob die GD PHP-Erweiterung installiert ist.',
+ 'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.',
'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.',
// Attachments
- 'attachment_page_mismatch' => 'Die Seite stimmt nach dem Hochladen des Anhangs nicht überein.',
+ 'attachment_page_mismatch' => 'Die Seite stimmte nach dem Hochladen des Anhangs nicht überein.',
// Pages
'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stellen Sie sicher, dass Sie mit dem Internet verbunden sind, bevor Sie den Entwurf dieser Seite speichern.',
@@ -47,24 +47,31 @@ return [
'book_not_found' => 'Buch nicht gefunden',
'page_not_found' => 'Seite nicht gefunden',
'chapter_not_found' => 'Kapitel nicht gefunden',
- 'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden.',
- 'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.',
- 'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern',
+ 'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden.',
+ 'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.',
+ 'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern',
// Users
- 'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen.',
- 'users_cannot_delete_guest' => 'Sie können den Gast-Benutzer nicht löschen',
+ 'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen.',
+ 'users_cannot_delete_guest' => 'Sie können den Gast-Benutzer nicht löschen',
// Roles
'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden.',
- 'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden',
- 'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden solange sie als Standardrolle für neue Registrierungen gesetzt ist',
+ 'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden',
+ 'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden, solange sie als Standardrolle für neue Registrierungen gesetzt ist',
+
+ // Comments
+ 'comment_list' => 'Beim Abrufen der Kommentare ist ein Fehler aufgetreten.',
+ 'cannot_add_comment_to_draft' => 'Du kannst keine Kommentare zu einem Entwurf hinzufügen.',
+ 'comment_add' => 'Beim Hinzufügen des Kommentars ist ein Fehler aufgetreten.',
+ 'comment_delete' => 'Beim Löschen des Kommentars ist ein Fehler aufgetreten.',
+ 'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen',
// Error pages
'404_page_not_found' => 'Seite nicht gefunden',
- 'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben wurde nicht gefunden.',
- 'return_home' => 'Zurück zur Startseite',
+ 'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben, wurde nicht gefunden.',
+ 'return_home' => 'Zurück zur Startseite',
'error_occurred' => 'Es ist ein Fehler aufgetreten',
'app_down' => ':appName befindet sich aktuell im Wartungsmodus.',
- 'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.',
+ 'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.'
];
diff --git a/resources/lang/de/pagination.php b/resources/lang/de/pagination.php
index a3bf7c8c8..6ed0e30f0 100644
--- a/resources/lang/de/pagination.php
+++ b/resources/lang/de/pagination.php
@@ -14,6 +14,6 @@ return [
*/
'previous' => '« Vorherige',
- 'next' => 'Nächste »',
+ 'next' => 'Nächste »',
];
diff --git a/resources/lang/de/passwords.php b/resources/lang/de/passwords.php
index c44b49baa..25ed05a04 100644
--- a/resources/lang/de/passwords.php
+++ b/resources/lang/de/passwords.php
@@ -13,10 +13,10 @@ return [
|
*/
- 'password' => 'Passörter müssen mindestens sechs Zeichen enthalten und die Wiederholung muss identisch sein.',
- 'user' => "Wir können keinen Benutzer mit dieser E-Mail Adresse finden.",
- 'token' => 'Dieser Passwort-Reset-Token ist ungültig.',
- 'sent' => 'Wir haben Ihnen eine E-Mail mit einem Link zum Zurücksetzen des Passworts zugesendet!',
- 'reset' => 'Ihr Passwort wurde zurückgesetzt!',
+ 'password' => 'Passörter müssen mindestens sechs Zeichen enthalten und die Wiederholung muss übereinstimmen.',
+ 'user' => "Es konnte kein Benutzer mit dieser E-Mail-Adresse gefunden werden.",
+ 'token' => 'Dieser Link zum Zurücksetzen des Passwortes ist ungültig.',
+ 'sent' => 'Wir haben Ihnen eine E-Mail mit einem Link zum Zurücksetzen des Passworts zugesendet!',
+ 'reset' => 'Ihr Passwort wurde zurückgesetzt!',
];
diff --git a/resources/lang/de/settings.php b/resources/lang/de/settings.php
index 668eecf33..9435ec808 100644
--- a/resources/lang/de/settings.php
+++ b/resources/lang/de/settings.php
@@ -20,17 +20,17 @@ return [
'app_name' => 'Anwendungsname',
'app_name_desc' => 'Dieser Name wird im Header und in E-Mails angezeigt.',
'app_name_header' => 'Anwendungsname im Header anzeigen?',
- 'app_public_viewing' => 'Öffentliche Ansicht erlauben?',
- 'app_secure_images' => 'Erhöhte Sicherheit für Bilduploads aktivieren?',
- 'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugriff zu verhindern.',
+ 'app_public_viewing' => 'Öffentliche Ansicht erlauben?',
+ 'app_secure_images' => 'Erhöhte Sicherheit für hochgeladene Bilder aktivieren?',
+ 'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu eratene, Zeichenketten zu Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnisindizes deaktiviert sind, um einen einfachen Zugriff zu verhindern.',
'app_editor' => 'Seiteneditor',
- 'app_editor_desc' => 'Wählen sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.',
+ 'app_editor_desc' => 'Wählen Sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.',
'app_custom_html' => 'Benutzerdefinierter HTML Inhalt',
- 'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS Styles anzupassen oder Analytics Code hinzuzufügen.',
+ 'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS Styles anzupassen oder Analytics-Code hinzuzufügen.',
'app_logo' => 'Anwendungslogo',
- 'app_logo_desc' => 'Dieses Bild sollte 43px hoch sein.
Größere Bilder werden verkleinert.',
- 'app_primary_color' => 'Primäre Anwendungsfarbe',
- 'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein.
Wenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt.',
+ 'app_logo_desc' => "Dieses Bild sollte 43px hoch sein.\nGrößere Bilder werden verkleinert.",
+ 'app_primary_color' => 'Primäre Anwendungsfarbe',
+ 'app_primary_color_desc' => "Dies sollte ein HEX Wert sein.\nWenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt.",
/**
* Registration settings
@@ -39,11 +39,11 @@ return [
'reg_settings' => 'Registrierungseinstellungen',
'reg_allow' => 'Registrierung erlauben?',
'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung',
- 'reg_confirm_email' => 'Bestätigung per E-Mail erforderlich?',
- 'reg_confirm_email_desc' => 'Falls die Einschränkung für Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',
- 'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken',
- 'reg_confirm_restrict_domain_desc' => 'Fügen sie eine, durch Komma getrennte, Liste von E-Mail Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können.
Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung ändern.',
- 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt',
+ 'reg_confirm_email' => 'Bestätigung per E-Mail erforderlich?',
+ 'reg_confirm_email_desc' => 'Falls die Einschränkung für Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',
+ 'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken',
+ 'reg_confirm_restrict_domain_desc' => "Fügen sie eine durch Komma getrennte Liste von Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können.\nHinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung ändern.",
+ 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt',
/**
* Role settings
@@ -53,31 +53,31 @@ return [
'role_user_roles' => 'Benutzer-Rollen',
'role_create' => 'Neue Rolle anlegen',
'role_create_success' => 'Rolle erfolgreich angelegt',
- 'role_delete' => 'Rolle löschen',
- 'role_delete_confirm' => 'Sie möchten die Rolle \':roleName\' löschen.',
- 'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Sie können unten eine neue Rolle auswählen, die Sie diesen Benutzern zuordnen möchten.',
+ 'role_delete' => 'Rolle löschen',
+ 'role_delete_confirm' => 'Sie möchten die Rolle ":roleName" löschen.',
+ 'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Sie können unten eine neue Rolle auswählen, die Sie diesen Benutzern zuordnen möchten.',
'role_delete_no_migration' => "Den Benutzern keine andere Rolle zuordnen",
- 'role_delete_sure' => 'Sind Sie sicher, dass Sie diese Rolle löschen möchten?',
- 'role_delete_success' => 'Rolle erfolgreich gelöscht',
+ 'role_delete_sure' => 'Sind Sie sicher, dass Sie diese Rolle löschen möchten?',
+ 'role_delete_success' => 'Rolle erfolgreich gelöscht',
'role_edit' => 'Rolle bearbeiten',
- 'role_details' => 'Rollen-Details',
+ 'role_details' => 'Rollendetails',
'role_name' => 'Rollenname',
'role_desc' => 'Kurzbeschreibung der Rolle',
'role_system' => 'System-Berechtigungen',
'role_manage_users' => 'Benutzer verwalten',
- 'role_manage_roles' => 'Rollen & Rollen-Berechtigungen verwalten',
- 'role_manage_entity_permissions' => 'Alle Buch-, Kapitel und Seiten-Berechtigungen verwalten',
- 'role_manage_own_entity_permissions' => 'Nur Berechtigungen eigener Bücher, Kapitel und Seiten verwalten',
- 'role_manage_settings' => 'Globaleinstellungen verwalrten',
+ 'role_manage_roles' => 'Rollen und Rollen-Berechtigungen verwalten',
+ 'role_manage_entity_permissions' => 'Alle Buch-, Kapitel- und Seiten-Berechtigungen verwalten',
+ 'role_manage_own_entity_permissions' => 'Nur Berechtigungen eigener Bücher, Kapitel und Seiten verwalten',
+ 'role_manage_settings' => 'Globaleinstellungen verwalten',
'role_asset' => 'Berechtigungen',
- 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.',
+ 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.',
'role_all' => 'Alle',
'role_own' => 'Eigene',
- 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
+ 'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt',
'role_save' => 'Rolle speichern',
'role_update_success' => 'Rolle erfolgreich gespeichert',
'role_users' => 'Dieser Rolle zugeordnete Benutzer',
- 'role_users_none' => 'Bisher sind dieser Rolle keiner Benutzer zugeordnet,',
+ 'role_users_none' => 'Bisher sind dieser Rolle keine Benutzer zugeordnet',
/**
* Users
@@ -85,27 +85,27 @@ return [
'users' => 'Benutzer',
'user_profile' => 'Benutzerprofil',
- 'users_add_new' => 'Benutzer hinzufügen',
+ 'users_add_new' => 'Benutzer hinzufügen',
'users_search' => 'Benutzer suchen',
'users_role' => 'Benutzerrollen',
'users_external_auth_id' => 'Externe Authentifizierungs-ID',
- 'users_password_warning' => 'Füllen Sie die folgenden Felder nur aus, wenn Sie Ihr Passwort ändern möchten:',
- 'users_system_public' => 'Dieser Benutzer repräsentiert alle Gast-Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.',
- 'users_delete' => 'Benutzer löschen',
- 'users_delete_named' => 'Benutzer :userName löschen',
- 'users_delete_warning' => 'Sie möchten den Benutzer \':userName\' gänzlich aus dem System löschen.',
- 'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
- 'users_delete_success' => 'Benutzer erfolgreich gelöscht.',
+ 'users_password_warning' => 'Füllen Sie die folgenden Felder nur aus, wenn Sie Ihr Passwort ändern möchten:',
+ 'users_system_public' => 'Dieser Benutzer repräsentiert alle unangemeldeten Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.',
+ 'users_delete' => 'Benutzer löschen',
+ 'users_delete_named' => 'Benutzer ":userName" löschen',
+ 'users_delete_warning' => 'Der Benutzer ":userName" wird aus dem System gelöscht.',
+ 'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
+ 'users_delete_success' => 'Benutzer erfolgreich gelöscht.',
'users_edit' => 'Benutzer bearbeiten',
'users_edit_profile' => 'Profil bearbeiten',
'users_edit_success' => 'Benutzer erfolgreich aktualisisert',
'users_avatar' => 'Benutzer-Bild',
- 'users_avatar_desc' => 'Dieses Bild sollte einen Durchmesser von ca. 256px haben.',
+ 'users_avatar_desc' => 'Das Bild sollte eine Auflösung von 256x256px haben.',
'users_preferred_language' => 'Bevorzugte Sprache',
'users_social_accounts' => 'Social-Media Konten',
- 'users_social_accounts_info' => 'Hier können Sie andere Social-Media Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Sie ein Social-Media Konto hier lösen, bleibt der Zugriff erhalteb. Entfernen Sie in diesem Falle die Berechtigung in Ihren Profil-Einstellungen des verknüpften Social-Media Kontos.',
- 'users_social_connect' => 'Social-Media Konto verknüpfen',
- 'users_social_disconnect' => 'Social-Media Kontoverknüpfung lösen',
- 'users_social_connected' => ':socialAccount Konto wurde erfolgreich mit dem Profil verknüpft.',
- 'users_social_disconnected' => ':socialAccount Konto wurde erfolgreich vom Profil gelöst.',
+ 'users_social_accounts_info' => 'Hier können Sie andere Social-Media-Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Sie ein Social-Media Konto lösen, bleibt der Zugriff erhalten. Entfernen Sie in diesem Falle die Berechtigung in Ihren Profil-Einstellungen des verknüpften Social-Media-Kontos.',
+ 'users_social_connect' => 'Social-Media-Konto verknüpfen',
+ 'users_social_disconnect' => 'Social-Media-Konto lösen',
+ 'users_social_connected' => ':socialAccount-Konto wurde erfolgreich mit dem Profil verknüpft.',
+ 'users_social_disconnected' => ':socialAccount-Konto wurde erfolgreich vom Profil gelöst.',
];
diff --git a/resources/lang/de/validation.php b/resources/lang/de/validation.php
index 3a6a1bc15..5ac4b1b27 100644
--- a/resources/lang/de/validation.php
+++ b/resources/lang/de/validation.php
@@ -19,54 +19,54 @@ return [
'alpha' => ':attribute kann nur Buchstaben enthalten.',
'alpha_dash' => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.',
'alpha_num' => ':attribute kann nur Buchstaben und Zahlen enthalten.',
- 'array' => ':attribute muss eine Array sein.',
+ 'array' => ':attribute muss ein Array sein.',
'before' => ':attribute muss ein Datum vor :date sein.',
'between' => [
'numeric' => ':attribute muss zwischen :min und :max liegen.',
- 'file' => ':attribute muss zwischen :min und :max Kilobytes groß sein.',
+ 'file' => ':attribute muss zwischen :min und :max Kilobytes groß sein.',
'string' => ':attribute muss zwischen :min und :max Zeichen lang sein.',
'array' => ':attribute muss zwischen :min und :max Elemente enthalten.',
],
'boolean' => ':attribute Feld muss wahr oder falsch sein.',
- 'confirmed' => ':attribute Bestätigung stimmt nicht überein.',
+ 'confirmed' => ':attribute stimmt nicht überein.',
'date' => ':attribute ist kein valides Datum.',
'date_format' => ':attribute entspricht nicht dem Format :format.',
- 'different' => ':attribute und :other müssen unterschiedlich sein.',
+ 'different' => ':attribute und :other müssen unterschiedlich sein.',
'digits' => ':attribute muss :digits Stellen haben.',
'digits_between' => ':attribute muss zwischen :min und :max Stellen haben.',
- 'email' => ':attribute muss eine valide E-Mail Adresse sein.',
- 'filled' => ':attribute Feld ist erforderlich.',
- 'exists' => 'Markiertes :attribute ist ungültig.',
+ 'email' => ':attribute muss eine valide E-Mail-Adresse sein.',
+ 'filled' => ':attribute ist erforderlich.',
+ 'exists' => ':attribute ist ungültig.',
'image' => ':attribute muss ein Bild sein.',
- 'in' => 'Markiertes :attribute ist ungültig.',
+ 'in' => ':attribute ist ungültig.',
'integer' => ':attribute muss eine Zahl sein.',
'ip' => ':attribute muss eine valide IP-Adresse sein.',
'max' => [
- 'numeric' => ':attribute darf nicht größer als :max sein.',
- 'file' => ':attribute darf nicht größer als :max Kilobyte sein.',
- 'string' => ':attribute darf nicht länger als :max Zeichen sein.',
+ 'numeric' => ':attribute darf nicht größer als :max sein.',
+ 'file' => ':attribute darf nicht größer als :max Kilobyte sein.',
+ 'string' => ':attribute darf nicht länger als :max Zeichen sein.',
'array' => ':attribute darf nicht mehr als :max Elemente enthalten.',
],
'mimes' => ':attribute muss eine Datei vom Typ: :values sein.',
'min' => [
- 'numeric' => ':attribute muss mindestens :min. sein',
- 'file' => ':attribute muss mindestens :min Kilobyte groß sein.',
+ 'numeric' => ':attribute muss mindestens :min sein',
+ 'file' => ':attribute muss mindestens :min Kilobyte groß sein.',
'string' => ':attribute muss mindestens :min Zeichen lang sein.',
'array' => ':attribute muss mindesten :min Elemente enthalten.',
],
- 'not_in' => 'Markiertes :attribute ist ungültig.',
+ 'not_in' => ':attribute ist ungültig.',
'numeric' => ':attribute muss eine Zahl sein.',
- 'regex' => ':attribute Format ist ungültig.',
- 'required' => ':attribute Feld ist erforderlich.',
- 'required_if' => ':attribute Feld ist erforderlich, wenn :other :value ist.',
- 'required_with' => ':attribute Feld ist erforderlich, wenn :values vorhanden ist.',
- 'required_with_all' => ':attribute Feld ist erforderlich, wenn :values vorhanden sind.',
- 'required_without' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden ist.',
- 'required_without_all' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden sind.',
- 'same' => ':attribute und :other muss übereinstimmen.',
+ 'regex' => ':attribute ist in einem ungültigen Format.',
+ 'required' => ':attribute ist erforderlich.',
+ 'required_if' => ':attribute ist erforderlich, wenn :other :value ist.',
+ 'required_with' => ':attribute ist erforderlich, wenn :values vorhanden ist.',
+ 'required_with_all' => ':attribute ist erforderlich, wenn :values vorhanden sind.',
+ 'required_without' => ':attribute ist erforderlich, wenn :values nicht vorhanden ist.',
+ 'required_without_all' => ':attribute ist erforderlich, wenn :values nicht vorhanden sind.',
+ 'same' => ':attribute und :other müssen übereinstimmen.',
'size' => [
'numeric' => ':attribute muss :size sein.',
- 'file' => ':attribute muss :size Kilobytes groß sein.',
+ 'file' => ':attribute muss :size Kilobytes groß sein.',
'string' => ':attribute muss :size Zeichen lang sein.',
'array' => ':attribute muss :size Elemente enthalten.',
],
diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php
index 450f4ce48..43053df10 100644
--- a/resources/lang/en/entities.php
+++ b/resources/lang/en/entities.php
@@ -234,4 +234,27 @@ return [
'profile_not_created_pages' => ':userName has not created any pages',
'profile_not_created_chapters' => ':userName has not created any chapters',
'profile_not_created_books' => ':userName has not created any books',
+
+ /**
+ * Comments
+ */
+ 'comment' => 'Comment',
+ 'comments' => 'Comments',
+ 'comment_placeholder' => 'Enter your comments here, markdown supported...',
+ 'no_comments' => 'No Comments',
+ 'x_comments' => ':numComments Comments',
+ 'one_comment' => '1 Comment',
+ 'comments_loading' => 'Loading...',
+ 'comment_save' => 'Save Comment',
+ 'comment_reply' => 'Reply',
+ 'comment_edit' => 'Edit',
+ 'comment_delete' => 'Delete',
+ 'comment_cancel' => 'Cancel',
+ 'comment_created' => 'Comment added',
+ 'comment_updated' => 'Comment updated',
+ 'comment_deleted' => 'Comment deleted',
+ 'comment_updated_text' => 'Updated :updateDiff by',
+ 'comment_delete_confirm' => 'This will remove the contents of the comment. Are you sure you want to delete this comment?',
+ 'comment_create' => 'Created'
+
];
\ No newline at end of file
diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php
index c4578a37a..71bcd1f9a 100644
--- a/resources/lang/en/errors.php
+++ b/resources/lang/en/errors.php
@@ -60,6 +60,13 @@ return [
'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',
'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',
+ // Comments
+ 'comment_list' => 'An error occurred while fetching the comments.',
+ 'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',
+ 'comment_add' => 'An error occurred while adding the comment.',
+ 'comment_delete' => 'An error occurred while deleting the comment.',
+ 'empty_comment' => 'Cannot add an empty comment.',
+
// Error pages
'404_page_not_found' => 'Page Not Found',
'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',
diff --git a/resources/lang/es/entities.php b/resources/lang/es/entities.php
index d6b2810bc..2ca55a786 100644
--- a/resources/lang/es/entities.php
+++ b/resources/lang/es/entities.php
@@ -214,4 +214,26 @@ return [
'profile_not_created_pages' => ':userName no ha creado ninguna página',
'profile_not_created_chapters' => ':userName no ha creado ningún capítulo',
'profile_not_created_books' => ':userName no ha creado ningún libro',
+
+ /**
+ * Comments
+ */
+ 'comment' => 'Comentario',
+ 'comments' => 'Comentarios',
+ 'comment_placeholder' => 'Introduzca sus comentarios aquí, markdown supported ...',
+ 'no_comments' => 'No hay comentarios',
+ 'x_comments' => ':numComments Comentarios',
+ 'one_comment' => '1 Comentario',
+ 'comments_loading' => 'Cargando ...',
+ 'comment_save' => 'Guardar comentario',
+ 'comment_reply' => 'Responder',
+ 'comment_edit' => 'Editar',
+ 'comment_delete' => 'Eliminar',
+ 'comment_cancel' => 'Cancelar',
+ 'comment_created' => 'Comentario añadido',
+ 'comment_updated' => 'Comentario actualizado',
+ 'comment_deleted' => 'Comentario eliminado',
+ 'comment_updated_text' => 'Actualizado hace :updateDiff por',
+ 'comment_delete_confirm' => 'Esto eliminará el contenido del comentario. ¿Estás seguro de que quieres eliminar este comentario?',
+ 'comment_create' => 'Creado'
];
diff --git a/resources/lang/es/errors.php b/resources/lang/es/errors.php
index 1e39a3cb8..e488b6a1b 100644
--- a/resources/lang/es/errors.php
+++ b/resources/lang/es/errors.php
@@ -67,4 +67,11 @@ return [
'error_occurred' => 'Ha ocurrido un error',
'app_down' => 'La aplicación :appName se encuentra caída en este momento',
'back_soon' => 'Volverá a estar operativa en corto tiempo.',
+
+ // Comments
+ 'comment_list' => 'Se ha producido un error al buscar los comentarios.',
+ 'cannot_add_comment_to_draft' => 'No puedes añadir comentarios a un borrador.',
+ 'comment_add' => 'Se ha producido un error al añadir el comentario.',
+ 'comment_delete' => 'Se ha producido un error al eliminar el comentario.',
+ 'empty_comment' => 'No se puede agregar un comentario vacío.',
];
diff --git a/resources/lang/fr/entities.php b/resources/lang/fr/entities.php
index 17b4ea913..0d89993e9 100644
--- a/resources/lang/fr/entities.php
+++ b/resources/lang/fr/entities.php
@@ -213,4 +213,26 @@ return [
'profile_not_created_pages' => ':userName n\'a pas créé de page',
'profile_not_created_chapters' => ':userName n\'a pas créé de chapitre',
'profile_not_created_books' => ':userName n\'a pas créé de livre',
+
+ /**
+ * Comments
+ */
+ 'comment' => 'Commentaire',
+ 'comments' => 'Commentaires',
+ 'comment_placeholder' => 'Entrez vos commentaires ici, merci supporté ...',
+ 'no_comments' => 'No Comments',
+ 'x_comments' => ':numComments Commentaires',
+ 'one_comment' => '1 Commentaire',
+ 'comments_loading' => 'Loading ...',
+ 'comment_save' => 'Enregistrer le commentaire',
+ 'comment_reply' => 'Répondre',
+ 'comment_edit' => 'Modifier',
+ 'comment_delete' => 'Supprimer',
+ 'comment_cancel' => 'Annuler',
+ 'comment_created' => 'Commentaire ajouté',
+ 'comment_updated' => 'Commentaire mis à jour',
+ 'comment_deleted' => 'Commentaire supprimé',
+ 'comment_updated_text' => 'Mis à jour il y a :updateDiff par',
+ 'comment_delete_confirm' => 'Cela supprime le contenu du commentaire. Êtes-vous sûr de vouloir supprimer ce commentaire?',
+ 'comment_create' => 'Créé'
];
diff --git a/resources/lang/fr/errors.php b/resources/lang/fr/errors.php
index 4197b1708..9e20147b6 100644
--- a/resources/lang/fr/errors.php
+++ b/resources/lang/fr/errors.php
@@ -67,4 +67,11 @@ return [
'error_occurred' => 'Une erreur est survenue',
'app_down' => ':appName n\'est pas en service pour le moment',
'back_soon' => 'Nous serons bientôt de retour.',
+
+ // comments
+ 'comment_list' => 'Une erreur s\'est produite lors de la récupération des commentaires.',
+ 'cannot_add_comment_to_draft' => 'Vous ne pouvez pas ajouter de commentaires à un projet.',
+ 'comment_add' => 'Une erreur s\'est produite lors de l\'ajout du commentaire.',
+ 'comment_delete' => 'Une erreur s\'est produite lors de la suppression du commentaire.',
+ 'empty_comment' => 'Impossible d\'ajouter un commentaire vide.',
];
diff --git a/resources/lang/nl/entities.php b/resources/lang/nl/entities.php
index d6975e130..6df9e5dd9 100644
--- a/resources/lang/nl/entities.php
+++ b/resources/lang/nl/entities.php
@@ -214,4 +214,26 @@ return [
'profile_not_created_pages' => ':userName heeft geen pagina\'s gemaakt',
'profile_not_created_chapters' => ':userName heeft geen hoofdstukken gemaakt',
'profile_not_created_books' => ':userName heeft geen boeken gemaakt',
+
+ /**
+ * Comments
+ */
+ 'comment' => 'Commentaar',
+ 'comments' => 'Commentaren',
+ 'comment_placeholder' => 'Vul hier uw reacties in, markdown ondersteund ...',
+ 'no_comments' => 'No Comments',
+ 'x_comments' => ':numComments Opmerkingen',
+ 'one_comment' => '1 commentaar',
+ 'comments_loading' => 'Loading ...',
+ 'comment_save' => 'Opslaan opslaan',
+ 'comment_reply' => 'Antwoord',
+ 'comment_edit' => 'Bewerken',
+ 'comment_delete' => 'Verwijderen',
+ 'comment_cancel' => 'Annuleren',
+ 'comment_created' => 'Opmerking toegevoegd',
+ 'comment_updated' => 'Opmerking bijgewerkt',
+ 'comment_deleted' => 'Opmerking verwijderd',
+ 'comment_updated_text' => 'Bijgewerkt :updateDiff geleden door',
+ 'comment_delete_confirm' => 'Hiermee verwijdert u de inhoud van de reactie. Weet u zeker dat u deze reactie wilt verwijderen?',
+ 'comment_create' => 'Gemaakt'
];
\ No newline at end of file
diff --git a/resources/lang/nl/errors.php b/resources/lang/nl/errors.php
index f8b635bce..b8fab59fd 100644
--- a/resources/lang/nl/errors.php
+++ b/resources/lang/nl/errors.php
@@ -67,4 +67,11 @@ return [
'error_occurred' => 'Er Ging Iets Fout',
'app_down' => ':appName is nu niet beschikbaar',
'back_soon' => 'Komt snel weer online.',
+
+ // Comments
+ 'comment_list' => 'Er is een fout opgetreden tijdens het ophalen van de reacties.',
+ 'cannot_add_comment_to_draft' => 'U kunt geen reacties toevoegen aan een ontwerp.',
+ 'comment_add' => 'Er is een fout opgetreden tijdens het toevoegen van de reactie.',
+ 'comment_delete' => 'Er is een fout opgetreden tijdens het verwijderen van de reactie.',
+ 'empty_comment' => 'Kan geen lege reactie toevoegen.',
];
\ No newline at end of file
diff --git a/resources/lang/pt_BR/entities.php b/resources/lang/pt_BR/entities.php
index 5a965fe62..e6b900fdd 100644
--- a/resources/lang/pt_BR/entities.php
+++ b/resources/lang/pt_BR/entities.php
@@ -214,4 +214,26 @@ return [
'profile_not_created_pages' => ':userName não criou páginas',
'profile_not_created_chapters' => ':userName não criou capítulos',
'profile_not_created_books' => ':userName não criou livros',
+
+ /**
+ * Comments
+ */
+ 'comentário' => 'Comentário',
+ 'comentários' => 'Comentários',
+ 'comment_placeholder' => 'Digite seus comentários aqui, markdown suportado ...',
+ 'no_comments' => 'No Comments',
+ 'x_comments' => ':numComments Comentários',
+ 'one_comment' => '1 comentário',
+ 'comments_loading' => 'Carregando ....',
+ 'comment_save' => 'Salvar comentário',
+ 'comment_reply' => 'Responder',
+ 'comment_edit' => 'Editar',
+ 'comment_delete' => 'Excluir',
+ 'comment_cancel' => 'Cancelar',
+ 'comment_created' => 'Comentário adicionado',
+ 'comment_updated' => 'Comentário atualizado',
+ 'comment_deleted' => 'Comentário eliminado',
+ 'comment_updated_text' => 'Atualizado :updatedDiff atrás por',
+ 'comment_delete_confirm' => 'Isso removerá o conteúdo do comentário. Tem certeza de que deseja excluir esse comentário?',
+ 'comment_create' => 'Criada'
];
\ No newline at end of file
diff --git a/resources/lang/pt_BR/errors.php b/resources/lang/pt_BR/errors.php
index 91b85e3ef..16fc78ff5 100644
--- a/resources/lang/pt_BR/errors.php
+++ b/resources/lang/pt_BR/errors.php
@@ -67,4 +67,11 @@ return [
'error_occurred' => 'Um erro ocorreu',
'app_down' => ':appName está fora do ar no momento',
'back_soon' => 'Voltaremos em seguida.',
+
+ // comments
+ 'comment_list' => 'Ocorreu um erro ao buscar os comentários.',
+ 'cannot_add_comment_to_draft' => 'Você não pode adicionar comentários a um rascunho.',
+ 'comment_add' => 'Ocorreu um erro ao adicionar o comentário.',
+ 'comment_delete' => 'Ocorreu um erro ao excluir o comentário.',
+ 'empty_comment' => 'Não é possível adicionar um comentário vazio.',
];
\ No newline at end of file
diff --git a/resources/lang/sk/entities.php b/resources/lang/sk/entities.php
index e70864753..7c8f34368 100644
--- a/resources/lang/sk/entities.php
+++ b/resources/lang/sk/entities.php
@@ -223,4 +223,26 @@ return [
'profile_not_created_pages' => ':userName nevytvoril žiadne stránky',
'profile_not_created_chapters' => ':userName nevytvoril žiadne kapitoly',
'profile_not_created_books' => ':userName nevytvoril žiadne knihy',
+
+ /**
+ * Comments
+ */
+ 'comment' => 'Komentár',
+ 'comments' => 'Komentáre',
+ 'comment_placeholder' => 'Tu zadajte svoje pripomienky, podporované označenie ...',
+ 'no_comments' => 'No Comments',
+ 'x_comments' => ':numComments komentárov',
+ 'one_comment' => '1 komentár',
+ 'comments_loading' => 'Loading ..',
+ 'comment_save' => 'Uložiť komentár',
+ 'comment_reply' => 'Odpovedať',
+ 'comment_edit' => 'Upraviť',
+ 'comment_delete' => 'Odstrániť',
+ 'comment_cancel' => 'Zrušiť',
+ 'comment_created' => 'Pridaný komentár',
+ 'comment_updated' => 'Komentár aktualizovaný',
+ 'comment_deleted' => 'Komentár bol odstránený',
+ 'comment_updated_text' => 'Aktualizované pred :updateDiff',
+ 'comment_delete_confirm' => 'Tým sa odstráni obsah komentára. Naozaj chcete odstrániť tento komentár?',
+ 'comment_create' => 'Vytvorené'
];
diff --git a/resources/lang/sk/errors.php b/resources/lang/sk/errors.php
index e3420852a..d4c7b7a3a 100644
--- a/resources/lang/sk/errors.php
+++ b/resources/lang/sk/errors.php
@@ -67,4 +67,11 @@ return [
'error_occurred' => 'Nastala chyba',
'app_down' => ':appName je momentálne nedostupná',
'back_soon' => 'Čoskoro bude opäť dostupná.',
+
+ // comments
+ 'comment_list' => 'Pri načítaní komentárov sa vyskytla chyba',
+ 'cannot_add_comment_to_draft' => 'Do konceptu nemôžete pridávať komentáre.',
+ 'comment_add' => 'Počas pridávania komentára sa vyskytla chyba',
+ 'comment_delete' => 'Pri odstraňovaní komentára došlo k chybe',
+ 'empty_comment' => 'Nelze pridať prázdny komentár.',
];
diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php
index 95a9d72b0..1c972e4fb 100644
--- a/resources/views/base.blade.php
+++ b/resources/views/base.blade.php
@@ -36,7 +36,7 @@