diff --git a/.gitignore b/.gitignore index 5f41a864e..e7e053505 100644 --- a/.gitignore +++ b/.gitignore @@ -8,16 +8,15 @@ Homestead.yaml /public/css /public/js /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/.travis.yml b/.travis.yml index 909e3e1f4..839d3be3f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,4 +25,4 @@ after_failure: - cat storage/logs/laravel.log script: - - phpunit \ No newline at end of file + - phpunit 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 @@ +<?php + +namespace BookStack; + +class Comment extends Ownable +{ + public $sub_comments = []; + protected $fillable = ['text', 'html', 'parent_id']; + protected $appends = ['created', 'updated', 'sub_comments']; + /** + * Get the entity that this comment belongs to + * @return \Illuminate\Database\Eloquent\Relations\MorphTo + */ + public function entity() + { + return $this->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/Console/Commands/UpgradeDatabaseEncoding.php b/app/Console/Commands/UpgradeDatabaseEncoding.php new file mode 100644 index 000000000..a17fc9523 --- /dev/null +++ b/app/Console/Commands/UpgradeDatabaseEncoding.php @@ -0,0 +1,57 @@ +<?php + +namespace BookStack\Console\Commands; + +use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; + +class UpgradeDatabaseEncoding extends Command +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'bookstack:db-utf8mb4 {--database= : The database connection to use.}'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Generate SQL commands to upgrade the database to UTF8mb4'; + + /** + * Create a new command instance. + * + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $connection = DB::getDefaultConnection(); + if ($this->option('database') !== null) { + DB::setDefaultConnection($this->option('database')); + } + + $database = DB::getDatabaseName(); + $tables = DB::select('SHOW TABLES'); + $this->line('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); + $this->line('USE `'.$database.'`;'); + $key = 'Tables_in_' . $database; + foreach ($tables as $table) { + $tableName = $table->$key; + $this->line('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); + } + + DB::setDefaultConnection($connection); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 4fa0b3c80..af9f5fd46 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -15,7 +15,8 @@ class Kernel extends ConsoleKernel Commands\ClearActivity::class, Commands\ClearRevisions::class, Commands\RegeneratePermissions::class, - Commands\RegenerateSearch::class + Commands\RegenerateSearch::class, + Commands\UpgradeDatabaseEncoding::class ]; /** 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 @@ +<?php namespace BookStack\Http\Controllers; + +use BookStack\Repos\CommentRepo; +use BookStack\Repos\EntityRepo; +use BookStack\Comment; +use Illuminate\Http\Request; + +class CommentController extends Controller +{ + protected $entityRepo; + + public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo, Comment $comment) + { + $this->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 @@ +<?php namespace BookStack\Repos; + +use BookStack\Comment; +use BookStack\Page; + +/** + * Class TagRepo + * @package BookStack\Repos + */ +class CommentRepo { + /** + * + * @var Comment $comment + */ + protected $comment; + + public function __construct(Comment $comment) + { + $this->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/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 7bc5fc4fc..d87c40f9b 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -571,7 +571,7 @@ class EntityRepo $draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id); $draftPage->html = $this->formatHtml($input['html']); - $draftPage->text = strip_tags($draftPage->html); + $draftPage->text = $this->pageToPlainText($draftPage); $draftPage->draft = false; $draftPage->revision_count = 1; @@ -713,6 +713,17 @@ class EntityRepo return $content; } + /** + * Get the plain text version of a page's content. + * @param Page $page + * @return string + */ + public function pageToPlainText(Page $page) + { + $html = $this->renderPage($page); + return strip_tags($html); + } + /** * Get a new draft page instance. * @param Book $book @@ -816,7 +827,7 @@ class EntityRepo $userId = user()->id; $page->fill($input); $page->html = $this->formatHtml($input['html']); - $page->text = strip_tags($page->html); + $page->text = $this->pageToPlainText($page); if (setting('app-editor') !== 'markdown') $page->markdown = ''; $page->updated_by = $userId; $page->revision_count++; @@ -933,7 +944,7 @@ class EntityRepo $revision = $page->revisions()->where('id', '=', $revisionId)->first(); $page->fill($revision->toArray()); $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id); - $page->text = strip_tags($page->html); + $page->text = $this->pageToPlainText($page); $page->updated_by = user()->id; $page->save(); $this->searchService->indexEntity($page); @@ -953,7 +964,7 @@ class EntityRepo if ($page->draft) { $page->fill($data); if (isset($data['html'])) { - $page->text = strip_tags($data['html']); + $page->text = $this->pageToPlainText($page); } $page->save(); return $page; 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/config/app.php b/config/app.php index 48348f837..a390eaf83 100644 --- a/config/app.php +++ b/config/app.php @@ -58,7 +58,7 @@ return [ */ 'locale' => env('APP_LANG', 'en'), - 'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja'], + 'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl'], /* |-------------------------------------------------------------------------- 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 = '<p>' . $text. '</p>'; + return [ + 'html' => $html, + 'text' => $text, + 'active' => 1 + ]; }); \ No newline at end of file diff --git a/database/migrations/2017_07_02_152834_update_db_encoding_to_ut8mb4.php b/database/migrations/2017_07_02_152834_update_db_encoding_to_ut8mb4.php index 550c95826..5681013ad 100644 --- a/database/migrations/2017_07_02_152834_update_db_encoding_to_ut8mb4.php +++ b/database/migrations/2017_07_02_152834_update_db_encoding_to_ut8mb4.php @@ -1,7 +1,5 @@ <?php -use Illuminate\Support\Facades\Schema; -use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class UpdateDbEncodingToUt8mb4 extends Migration @@ -13,16 +11,9 @@ class UpdateDbEncodingToUt8mb4 extends Migration */ public function up() { - $database = DB::getDatabaseName(); - $tables = DB::select('SHOW TABLES'); - $pdo = DB::getPdo(); - $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); - $pdo->exec('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); - $key = 'Tables_in_' . $database; - foreach ($tables as $table) { - $tableName = $table->$key; - $pdo->exec('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); - } + // Migration removed due to issues during live migration. + // Instead you can run the command `artisan bookstack:db-utf8mb4` + // which will generate out the SQL request to upgrade your DB to utf8mb4. } /** @@ -32,15 +23,6 @@ class UpdateDbEncodingToUt8mb4 extends Migration */ public function down() { - $database = DB::getDatabaseName(); - $tables = DB::select('SHOW TABLES'); - $pdo = DB::getPdo(); - $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); - $pdo->exec('ALTER DATABASE `'.$database.'` CHARACTER SET utf8 COLLATE utf8_unicode_ci'); - $key = 'Tables_in_' . $database; - foreach ($tables as $table) { - $tableName = $table->$key; - $pdo->exec('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci'); - } + // } } 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 @@ +<?php + +use Illuminate\Support\Facades\Schema; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +class CreateCommentsTable extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::create('comments', function (Blueprint $table) { + $table->increments('id')->unsigned(); + $table->integer('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 b72bb366d..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'); @@ -12,8 +14,10 @@ const babelify = require("babelify"); const watchify = require("watchify"); const envify = require("envify"); const gutil = require("gulp-util"); +const liveReload = require('gulp-livereload'); if (argv.production) process.env.NODE_ENV = 'production'; +let isProduction = argv.production || process.env.NODE_ENV === 'production'; gulp.task('styles', () => { let chain = gulp.src(['resources/assets/sass/**/*.scss']) @@ -24,31 +28,40 @@ gulp.task('styles', () => { }})) .pipe(sass()) .pipe(autoprefixer('last 2 versions')); - if (argv.production) chain = chain.pipe(minifycss()); - return chain.pipe(gulp.dest('public/css/')); + if (isProduction) chain = chain.pipe(minifycss()); + return chain.pipe(gulp.dest('public/css/')).pipe(liveReload()); }); -function scriptTask(watch=false) { +function scriptTask(watch = false) { let props = { basedir: 'resources/assets/js', debug: true, - entries: ['global.js'] + entries: ['global.js'], + fast: !isProduction, + cache: {}, + packageCache: {}, }; let bundler = watch ? watchify(browserify(props), { poll: true }) : browserify(props); - bundler.transform(envify, {global: true}).transform(babelify, {presets: ['es2015']}); + + if (isProduction) { + bundler.transform(envify, {global: true}).transform(babelify, {presets: ['es2015']}); + } + function rebundle() { let stream = bundler.bundle(); stream = stream.pipe(source('common.js')); - if (argv.production) stream = stream.pipe(buffer()).pipe(uglify()); - return stream.pipe(gulp.dest('public/js/')); + if (isProduction) stream = stream.pipe(buffer()).pipe(uglify()); + return stream.pipe(gulp.dest('public/js/')).pipe(liveReload()); } + bundler.on('update', function() { rebundle(); - gutil.log('Rebundle...'); + gutil.log('Rebundling assets...'); }); + bundler.on('log', gutil.log); return rebundle(); } @@ -57,6 +70,7 @@ gulp.task('scripts', () => {scriptTask(false)}); gulp.task('scripts-watch', () => {scriptTask(true)}); gulp.task('default', ['styles', 'scripts-watch'], () => { + liveReload.listen(); gulp.watch("resources/assets/sass/**/*.scss", ['styles']); }); diff --git a/package.json b/package.json index 93f62bf1f..f447ec786 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "build": "gulp build", "production": "gulp build --production", "dev": "gulp", - "watch": "gulp" + "watch": "gulp", + "permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads" }, "devDependencies": { "babelify": "^7.3.0", @@ -13,6 +14,7 @@ "gulp": "3.9.1", "gulp-autoprefixer": "3.1.1", "gulp-clean-css": "^3.0.4", + "gulp-livereload": "^3.8.1", "gulp-minify-css": "1.2.4", "gulp-plumber": "1.1.0", "gulp-sass": "3.1.0", @@ -29,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 4f025e3c2..5d099ad5f 100644 --- a/readme.md +++ b/readme.md @@ -22,9 +22,12 @@ All development on BookStack is currently done on the master branch. When it's t SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp. To run the build task you can use the following commands: ``` bash -# Build and minify for production +# Build assets for development npm run-script build +# Build and minify assets for production +npm run-script production + # Build for dev (With sourcemaps) and watch for changes npm run-script dev ``` @@ -76,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 ac1c3487c..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) { @@ -379,7 +129,7 @@ module.exports = function (ngApp, events) { */ $scope.discardDraft = function () { let url = window.baseUrl('/ajax/page/' + pageId); - $http.get(url).then((responseData) => { + $http.get(url).then(responseData => { if (autoSave) $interval.cancel(autoSave); $scope.draftText = trans('entities.pages_editing_page'); $scope.isUpdateDraft = false; @@ -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 51f1b7579..fc92121ff 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -1,152 +1,10 @@ "use strict"; -const DropZone = require("dropzone"); const MarkdownIt = require("markdown-it"); const mdTasksLists = require('markdown-it-task-lists'); const code = require('./code'); module.exports = function (ngApp, events) { - /** - * Common tab controls using simple jQuery functions. - */ - ngApp.directive('tabContainer', function() { - return { - restrict: 'A', - link: function (scope, element, attrs) { - const $content = element.find('[tab-content]'); - const $buttons = element.find('[tab-button]'); - - if (attrs.tabContainer) { - let initial = attrs.tabContainer; - $buttons.filter(`[tab-button="${initial}"]`).addClass('selected'); - $content.hide().filter(`[tab-content="${initial}"]`).show(); - } else { - $content.hide().first().show(); - $buttons.first().addClass('selected'); - } - - $buttons.click(function() { - let clickedTab = $(this); - $buttons.removeClass('selected'); - $content.hide(); - let name = clickedTab.addClass('selected').attr('tab-button'); - $content.filter(`[tab-content="${name}"]`).show(); - }); - } - }; - }); - - /** - * Sub form component to allow inner-form sections to act like their own forms. - */ - ngApp.directive('subForm', function() { - return { - restrict: 'A', - link: function (scope, element, attrs) { - element.on('keypress', e => { - if (e.keyCode === 13) { - submitEvent(e); - } - }); - - element.find('button[type="submit"]').click(submitEvent); - - function submitEvent(e) { - e.preventDefault(); - if (attrs.subForm) scope.$eval(attrs.subForm); - } - } - }; - }); - - /** - * DropZone - * Used for uploading images - */ - ngApp.directive('dropZone', [function () { - return { - restrict: 'E', - template: ` - <div class="dropzone-container"> - <div class="dz-message">{{message}}</div> - </div> - `, - scope: { - uploadUrl: '@', - eventSuccess: '=', - eventError: '=', - uploadedTo: '@', - }, - link: function (scope, element, attrs) { - scope.message = attrs.message; - if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder; - let dropZone = new DropZone(element[0].querySelector('.dropzone-container'), { - url: scope.uploadUrl, - init: function () { - let dz = this; - dz.on('sending', function (file, xhr, data) { - let token = window.document.querySelector('meta[name=token]').getAttribute('content'); - data.append('_token', token); - let uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo; - data.append('uploaded_to', uploadedTo); - }); - if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess); - dz.on('success', function (file, data) { - $(file.previewElement).fadeOut(400, function () { - dz.removeFile(file); - }); - }); - if (typeof scope.eventError !== 'undefined') dz.on('error', scope.eventError); - dz.on('error', function (file, errorMessage, xhr) { - console.log(errorMessage); - console.log(xhr); - function setMessage(message) { - $(file.previewElement).find('[data-dz-errormessage]').text(message); - } - - if (xhr.status === 413) setMessage(trans('errors.server_upload_limit')); - if (errorMessage.file) setMessage(errorMessage.file[0]); - - }); - } - }); - } - }; - }]); - - /** - * Dropdown - * Provides some simple logic to create small dropdown menus - */ - ngApp.directive('dropdown', [function () { - return { - restrict: 'A', - link: function (scope, element, attrs) { - const menu = element.find('ul'); - element.find('[dropdown-toggle]').on('click', function () { - menu.show().addClass('anim menuIn'); - let inputs = menu.find('input'); - let hasInput = inputs.length > 0; - if (hasInput) { - inputs.first().focus(); - element.on('keypress', 'input', event => { - if (event.keyCode === 13) { - event.preventDefault(); - menu.hide(); - menu.removeClass('anim menuIn'); - return false; - } - }); - } - element.mouseleave(function () { - menu.hide(); - menu.removeClass('anim menuIn'); - }); - }); - } - }; - }]); - /** * TinyMCE * An angular wrapper around the tinyMCE editor. @@ -187,30 +45,6 @@ module.exports = function (ngApp, events) { } scope.tinymce.extraSetups.push(tinyMceSetup); - - // Custom tinyMCE plugins - tinymce.PluginManager.add('customhr', function (editor) { - editor.addCommand('InsertHorizontalRule', function () { - let hrElem = document.createElement('hr'); - let cNode = editor.selection.getNode(); - let parentNode = cNode.parentNode; - parentNode.insertBefore(hrElem, cNode); - }); - - editor.addButton('hr', { - icon: 'hr', - tooltip: 'Horizontal line', - cmd: 'InsertHorizontalRule' - }); - - editor.addMenuItem('hr', { - icon: 'hr', - text: 'Horizontal line', - cmd: 'InsertHorizontalRule', - context: 'insert' - }); - }); - tinymce.init(scope.tinymce); } } @@ -251,6 +85,21 @@ module.exports = function (ngApp, events) { extraKeys[`${metaKey}-S`] = function(cm) {scope.$emit('save-draft');}; // Show link selector extraKeys[`Shift-${metaKey}-K`] = function(cm) {showLinkSelector()}; + // Insert Link + extraKeys[`${metaKey}-K`] = function(cm) {insertLink()}; + // FormatShortcuts + extraKeys[`${metaKey}-1`] = function(cm) {replaceLineStart('##');}; + extraKeys[`${metaKey}-2`] = function(cm) {replaceLineStart('###');}; + extraKeys[`${metaKey}-3`] = function(cm) {replaceLineStart('####');}; + extraKeys[`${metaKey}-4`] = function(cm) {replaceLineStart('#####');}; + extraKeys[`${metaKey}-5`] = function(cm) {replaceLineStart('');}; + extraKeys[`${metaKey}-d`] = function(cm) {replaceLineStart('');}; + extraKeys[`${metaKey}-6`] = function(cm) {replaceLineStart('>');}; + extraKeys[`${metaKey}-q`] = function(cm) {replaceLineStart('>');}; + extraKeys[`${metaKey}-7`] = function(cm) {wrapSelection('\n```\n', '\n```');}; + extraKeys[`${metaKey}-8`] = function(cm) {wrapSelection('`', '`');}; + extraKeys[`Shift-${metaKey}-E`] = function(cm) {wrapSelection('`', '`');}; + extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('<p class="callout info">', '</div>');}; cm.setOption('extraKeys', extraKeys); // Update data on content change @@ -303,6 +152,73 @@ module.exports = function (ngApp, events) { cm.setSelections(cursor); } + // Helper to replace the start of the line + function replaceLineStart(newStart) { + let cursor = cm.getCursor(); + let lineContent = cm.getLine(cursor.line); + let lineLen = lineContent.length; + let lineStart = lineContent.split(' ')[0]; + + // Remove symbol if already set + if (lineStart === newStart) { + lineContent = lineContent.replace(`${newStart} `, ''); + cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen}); + cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)}); + return; + } + + let alreadySymbol = /^[#>`]/.test(lineStart); + let posDif = 0; + if (alreadySymbol) { + posDif = newStart.length - lineStart.length; + lineContent = lineContent.replace(lineStart, newStart).trim(); + } else if (newStart !== '') { + posDif = newStart.length + 1; + lineContent = newStart + ' ' + lineContent; + } + cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen}); + cm.setCursor({line: cursor.line, ch: cursor.ch + posDif}); + } + + function wrapLine(start, end) { + let cursor = cm.getCursor(); + let lineContent = cm.getLine(cursor.line); + let lineLen = lineContent.length; + let newLineContent = lineContent; + + if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) { + newLineContent = lineContent.slice(start.length, lineContent.length - end.length); + } else { + newLineContent = `${start}${lineContent}${end}`; + } + + cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen}); + cm.setCursor({line: cursor.line, ch: cursor.ch + (newLineContent.length - lineLen)}); + } + + function wrapSelection(start, end) { + let selection = cm.getSelection(); + if (selection === '') return wrapLine(start, end); + let newSelection = selection; + let frontDiff = 0; + let endDiff = 0; + + if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) { + newSelection = selection.slice(start.length, selection.length - end.length); + endDiff = -(end.length + start.length); + } else { + newSelection = `${start}${selection}${end}`; + endDiff = start.length + end.length; + } + + let selections = cm.listSelections()[0]; + cm.replaceSelection(newSelection); + let headFirst = selections.head.ch <= selections.anchor.ch; + selections.head.ch += headFirst ? frontDiff : endDiff; + selections.anchor.ch += headFirst ? endDiff : frontDiff; + cm.setSelections([selections]); + } + // Handle image upload and add image into markdown content function uploadImage(file) { if (file === null || file.type.indexOf('image') !== 0) return; @@ -345,10 +261,20 @@ module.exports = function (ngApp, events) { }); } + function insertLink() { + let cursorPos = cm.getCursor('from'); + let selectedText = cm.getSelection() || ''; + let newText = `[${selectedText}]()`; + cm.focus(); + cm.replaceSelection(newText); + let cursorPosDiff = (selectedText === '') ? -3 : -1; + cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff); + } + // 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(); @@ -461,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', @@ -678,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 => { @@ -789,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('<comment-reply page-id="comment.pageId" parent-id="comment.id" parent="comment"></comment-reply>'); + } else { + lnkFunc = $compile('<comment-edit comment="comment"></comment-add>'); + } + 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 1ee5bf0d3..966adb75d 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); @@ -174,7 +182,7 @@ $('.overlay').click(function(event) { 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 81d1cdea6..08e4c0c34 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -52,14 +52,36 @@ function editorPaste(e, editor) { function registerEditorShortcuts(editor) { // Headers for (let i = 1; i < 5; i++) { - editor.addShortcut('meta+' + i, '', ['FormatBlock', false, 'h' + i]); + editor.shortcuts.add('meta+' + i, '', ['FormatBlock', false, 'h' + (i+1)]); } // Other block shortcuts - editor.addShortcut('meta+q', '', ['FormatBlock', false, 'blockquote']); - editor.addShortcut('meta+d', '', ['FormatBlock', false, 'p']); - editor.addShortcut('meta+e', '', ['codeeditor', false, 'pre']); - editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']); + editor.shortcuts.add('meta+5', '', ['FormatBlock', false, 'p']); + editor.shortcuts.add('meta+d', '', ['FormatBlock', false, 'p']); + editor.shortcuts.add('meta+6', '', ['FormatBlock', false, 'blockquote']); + editor.shortcuts.add('meta+q', '', ['FormatBlock', false, 'blockquote']); + editor.shortcuts.add('meta+7', '', ['codeeditor', false, 'pre']); + editor.shortcuts.add('meta+e', '', ['codeeditor', false, 'pre']); + editor.shortcuts.add('meta+8', '', ['FormatBlock', false, 'code']); + editor.shortcuts.add('meta+shift+E', '', ['FormatBlock', false, 'code']); + // Loop through callout styles + editor.shortcuts.add('meta+9', '', function() { + let selectedNode = editor.selection.getNode(); + let formats = ['info', 'success', 'warning', 'danger']; + + if (!selectedNode || selectedNode.className.indexOf('callout') === -1) { + editor.formatter.apply('calloutinfo'); + return; + } + + for (let i = 0; i < formats.length; i++) { + if (selectedNode.className.indexOf(formats[i]) === -1) continue; + let newFormat = (i === formats.length -1) ? formats[0] : formats[i+1]; + editor.formatter.apply('callout' + newFormat); + return; + } + editor.formatter.apply('p'); + }); } @@ -120,7 +142,7 @@ function codePlugin() { $codeMirrorContainer.replaceWith($pre); } - window.tinymce.PluginManager.add('codeeditor', (editor, url) => { + window.tinymce.PluginManager.add('codeeditor', function(editor, url) { let $ = editor.$; @@ -173,7 +195,32 @@ function codePlugin() { }); } +function hrPlugin() { + window.tinymce.PluginManager.add('customhr', function (editor) { + editor.addCommand('InsertHorizontalRule', function () { + let hrElem = document.createElement('hr'); + let cNode = editor.selection.getNode(); + let parentNode = cNode.parentNode; + parentNode.insertBefore(hrElem, cNode); + }); + + editor.addButton('hr', { + icon: 'hr', + tooltip: 'Horizontal line', + cmd: 'InsertHorizontalRule' + }); + + editor.addMenuItem('hr', { + icon: 'hr', + text: 'Horizontal line', + cmd: 'InsertHorizontalRule', + context: 'insert' + }); + }); +} + module.exports = function() { + hrPlugin(); codePlugin(); let settings = { selector: '#html-editor', @@ -207,10 +254,10 @@ module.exports = function() { {title: "Code Block", icon: "code", cmd: 'codeeditor', format: 'codeeditor'}, {title: "Inline Code", icon: "code", inline: "code"}, {title: "Callouts", items: [ - {title: "Success", block: 'p', exact: true, attributes : {'class' : 'callout success'}}, - {title: "Info", block: 'p', exact: true, attributes : {'class' : 'callout info'}}, - {title: "Warning", block: 'p', exact: true, attributes : {'class' : 'callout warning'}}, - {title: "Danger", block: 'p', exact: true, attributes : {'class' : 'callout danger'}} + {title: "Info", format: 'calloutinfo'}, + {title: "Success", format: 'calloutsuccess'}, + {title: "Warning", format: 'calloutwarning'}, + {title: "Danger", format: 'calloutdanger'} ]}, ], style_formats_merge: false, @@ -219,6 +266,10 @@ module.exports = function() { alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'}, aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'}, alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'}, + calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}}, + calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}}, + calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}}, + calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}} }, file_browser_callback: function (field_name, url, type, win) { @@ -232,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 @@ -314,7 +365,7 @@ module.exports = function() { icon: 'image', tooltip: 'Insert an image', onclick: function () { - window.ImageManager.showExternal(function (image) { + window.ImageManager.show(function (image) { let html = `<a href="${image.url}" target="_blank">`; html += `<img src="${image.thumbs.display}" alt="${image.name}">`; html += '</a>'; 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 = ` + <div> + <input :value="value" :autosuggest-type="type" ref="input" + :placeholder="placeholder" :name="name" + @input="inputUpdate($event.target.value)" @focus="inputUpdate($event.target.value)" + @blur="inputBlur" + @keydown="inputKeydown" + /> + <ul class="suggestion-box" v-if="showSuggestions"> + <li v-for="(suggestion, i) in suggestions" + @click="selectSuggestion(suggestion)" + :class="{active: (i === active)}">{{suggestion}}</li> + </ul> + </div> + +`; + +function data() { + return { + suggestions: [], + showSuggestions: false, + active: 0, + }; +} + +const ajaxCache = {}; + +const props = ['url', 'type', 'value', 'placeholder', 'name']; + +function getNameInputVal(valInput) { + let parentRow = valInput.parentNode.parentNode; + let nameInput = parentRow.querySelector('[autosuggest-type="name"]'); + return (nameInput === null) ? '' : nameInput.value; +} + +const methods = { + + inputUpdate(inputValue) { + this.$emit('input', inputValue); + let params = {}; + + if (this.type === 'value') { + let nameVal = getNameInputVal(this.$el); + if (nameVal !== "") params.name = nameVal; + } + + this.getSuggestions(inputValue.slice(0, 3), params).then(suggestions => { + if (inputValue.length === 0) { + this.displaySuggestions(suggestions.slice(0, 6)); + return; + } + // Filter to suggestions containing searched term + suggestions = suggestions.filter(item => { + return item.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1; + }).slice(0, 4); + this.displaySuggestions(suggestions); + }); + }, + + inputBlur() { + setTimeout(() => { + this.$emit('blur'); + this.showSuggestions = false; + }, 100); + }, + + inputKeydown(event) { + if (event.keyCode === 13) event.preventDefault(); + if (!this.showSuggestions) return; + + // Down arrow + if (event.keyCode === 40) { + this.active = (this.active === this.suggestions.length - 1) ? 0 : this.active+1; + } + // Up Arrow + else if (event.keyCode === 38) { + this.active = (this.active === 0) ? this.suggestions.length - 1 : this.active-1; + } + // Enter or tab keys + else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) { + this.selectSuggestion(this.suggestions[this.active]); + } + // Escape key + else if (event.keyCode === 27) { + this.showSuggestions = false; + } + }, + + displaySuggestions(suggestions) { + if (suggestions.length === 0) { + this.suggestions = []; + this.showSuggestions = false; + return; + } + + this.suggestions = suggestions; + this.showSuggestions = true; + this.active = 0; + }, + + selectSuggestion(suggestion) { + this.$refs.input.value = suggestion; + this.$refs.input.focus(); + this.$emit('input', suggestion); + this.showSuggestions = false; + }, + + /** + * Get suggestions from BookStack. Store and use local cache if already searched. + * @param {String} input + * @param {Object} params + */ + getSuggestions(input, params) { + params.search = input; + let cacheKey = `${this.url}:${JSON.stringify(params)}`; + + if (typeof ajaxCache[cacheKey] !== "undefined") return Promise.resolve(ajaxCache[cacheKey]); + + return this.$http.get(this.url, {params}).then(resp => { + ajaxCache[cacheKey] = resp.data; + return resp.data; + }); + } + +}; + +const computed = []; + +module.exports = {template, data, props, methods, computed}; \ 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 = ` + <div class="dropzone-container"> + <div class="dz-message">{{placeholder}}</div> + </div> +`; + +const props = ['placeholder', 'uploadUrl', 'uploadedTo']; + +// TODO - Remove jQuery usage +function mounted() { + let container = this.$el; + let _this = this; + new DropZone(container, { + url: function() { + return _this.uploadUrl; + }, + init: function () { + let dz = this; + + dz.on('sending', function (file, xhr, data) { + let token = window.document.querySelector('meta[name=token]').getAttribute('content'); + data.append('_token', token); + let uploadedTo = typeof _this.uploadedTo === 'undefined' ? 0 : _this.uploadedTo; + data.append('uploaded_to', uploadedTo); + }); + + dz.on('success', function (file, data) { + _this.$emit('success', {file, data}); + $(file.previewElement).fadeOut(400, function () { + dz.removeFile(file); + }); + }); + + dz.on('error', function (file, errorMessage, xhr) { + _this.$emit('error', {file, errorMessage, xhr}); + console.log(errorMessage); + console.log(xhr); + function setMessage(message) { + $(file.previewElement).find('[data-dz-errormessage]').text(message); + } + + if (xhr.status === 413) setMessage(trans('errors.server_upload_limit')); + if (errorMessage.file) setMessage(errorMessage.file[0]); + }); + } + }); +} + +function data() { + return {} +} + +module.exports = { + template, + props, + mounted, + data, +}; \ 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/search.js b/resources/assets/js/vues/search.js index 515ca3bc9..8cb790d24 100644 --- a/resources/assets/js/vues/search.js +++ b/resources/assets/js/vues/search.js @@ -149,7 +149,7 @@ let methods = { updateSearch(e) { e.preventDefault(); - window.location = '/search?term=' + encodeURIComponent(this.termString); + window.location = window.baseUrl('/search?term=' + encodeURIComponent(this.termString)); }, enableDate(optionName) { @@ -192,4 +192,4 @@ function created() { module.exports = { data, computed, methods, created -}; \ 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 16ed75545..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; } @@ -142,7 +146,6 @@ form.search-box { color: #aaa; padding: 0 $-xs; } - .faded { a, button, span, span > div { color: #666; @@ -178,6 +181,8 @@ form.search-box { padding-left: 0; } } + + .action-buttons .dropdown-container:last-child a { padding-right: 0; padding-left: $-s; @@ -196,6 +201,25 @@ form.search-box { } } +@include smaller-than($m) { + .breadcrumbs .text-button, .action-buttons .text-button { + padding: $-s $-xs; + } + .action-buttons .dropdown-container:last-child a { + padding-left: $-xs; + } + .breadcrumbs .text-button { + font-size: 0; + } + .breadcrumbs a i { + font-size: $fs-m; + padding-right: 0; + } + .breadcrumbs span.sep { + padding: 0 $-xxs; + } +} + .nav-tabs { text-align: center; a, .tab-item { @@ -207,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 ccef2a70f..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; @@ -152,6 +161,14 @@ pre { } } +@media print { + pre { + padding-left: 12px; + } + pre:after { + display: none; + } +} blockquote { display: block; @@ -172,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 0e8861905..51e47ea74 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 8c1bb4569..af2793dfc 100644 --- a/resources/lang/de/common.php +++ b/resources/lang/de/common.php @@ -30,9 +30,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', @@ -40,9 +40,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', 'toggle_thumbnails' => 'Thumbnails zeigen/verstecken', @@ -55,6 +55,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 9d72b7942..598f9f663 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 <head> Inhalt', - 'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der <head> 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 <head> 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. <br>Größere Bilder werden verkleinert.', - 'app_primary_color' => 'Primäre Anwendungsfarbe', - 'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein. <br>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. <br> 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,28 +85,28 @@ 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_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_books_display_type' => 'Bevorzugtes Display-Layout für Bücher', - '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_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/en/settings.php b/resources/lang/en/settings.php index d35857903..0a7547a58 100644 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -123,6 +123,7 @@ return [ 'pt_BR' => 'Português do Brasil', 'sk' => 'Slovensky', 'ja' => '日本語', + 'pl' => 'Polski', ] /////////////////////////////////// ]; 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/auth.php b/resources/lang/fr/auth.php index 41d051c5f..015bfdff0 100644 --- a/resources/lang/fr/auth.php +++ b/resources/lang/fr/auth.php @@ -10,7 +10,7 @@ return [ | these language lines according to your application's requirements. | */ - 'failed' => 'Ces informations ne correspondent a aucun compte.', + 'failed' => 'Ces informations ne correspondent à aucun compte.', 'throttle' => "Trop d'essais, veuillez réessayer dans :seconds secondes.", /** @@ -26,7 +26,7 @@ return [ 'password' => 'Mot de passe', 'password_confirm' => 'Confirmez le mot de passe', 'password_hint' => 'Doit faire plus de 5 caractères', - 'forgot_password' => 'Mot de passe oublié?', + 'forgot_password' => 'Mot de passe oublié ?', 'remember_me' => 'Se souvenir de moi', 'ldap_email_hint' => "Merci d'entrer une adresse e-mail pour ce compte", 'create_account' => 'Créer un compte', @@ -35,9 +35,9 @@ return [ 'social_registration_text' => "S'inscrire et se connecter avec un réseau social", 'register_thanks' => 'Merci pour votre enregistrement', - 'register_confirm' => 'Vérifiez vos e-mails et cliquer sur le lien de confirmation pour rejoindre :appName.', + 'register_confirm' => 'Vérifiez vos e-mails et cliquez sur le lien de confirmation pour rejoindre :appName.', 'registrations_disabled' => "L'inscription est désactivée pour le moment", - 'registration_email_domain_invalid' => 'Cette adresse e-mail ne peux pas adcéder à l\'application', + 'registration_email_domain_invalid' => 'Cette adresse e-mail ne peut pas accéder à l\'application', 'register_success' => 'Merci pour votre inscription. Vous êtes maintenant inscrit(e) et connecté(e)', @@ -51,7 +51,7 @@ return [ 'reset_password_success' => 'Votre mot de passe a été réinitialisé avec succès.', 'email_reset_subject' => 'Réinitialisez votre mot de passe pour :appName', - 'email_reset_text' => 'Vous recevez cet e-mail parceque nous avons reçu une demande de réinitialisation pour votre compte', + 'email_reset_text' => 'Vous recevez cet e-mail parce que nous avons reçu une demande de réinitialisation pour votre compte', 'email_reset_not_requested' => 'Si vous n\'avez pas effectué cette demande, vous pouvez ignorer cet e-mail.', @@ -59,11 +59,11 @@ return [ * Email Confirmation */ 'email_confirm_subject' => 'Confirmez votre adresse e-mail pour :appName', - 'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName!', - 'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous:', + 'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName !', + 'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous :', 'email_confirm_action' => 'Confirmez votre adresse e-mail', 'email_confirm_send_error' => 'La confirmation par e-mail est requise mais le système n\'a pas pu envoyer l\'e-mail. Contactez l\'administrateur système.', - 'email_confirm_success' => 'Votre adresse e-mail a été confirmée!', + 'email_confirm_success' => 'Votre adresse e-mail a été confirmée !', 'email_confirm_resent' => 'L\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de récéption.', 'email_not_confirmed' => 'Adresse e-mail non confirmée', diff --git a/resources/lang/fr/common.php b/resources/lang/fr/common.php index 129cf082e..74f174dca 100644 --- a/resources/lang/fr/common.php +++ b/resources/lang/fr/common.php @@ -9,7 +9,7 @@ return [ 'back' => 'Retour', 'save' => 'Enregistrer', 'continue' => 'Continuer', - 'select' => 'Selectionner', + 'select' => 'Sélectionner', /** * Form Labels @@ -55,6 +55,6 @@ return [ /** * Email Content */ - 'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur:', + 'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer sur le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur :', 'email_rights' => 'Tous droits réservés', ]; diff --git a/resources/lang/fr/entities.php b/resources/lang/fr/entities.php index 5562fb0fd..0d89993e9 100644 --- a/resources/lang/fr/entities.php +++ b/resources/lang/fr/entities.php @@ -12,7 +12,7 @@ return [ 'recently_update' => 'Mis à jour récemment', 'recently_viewed' => 'Vus récemment', 'recent_activity' => 'Activité récente', - 'create_now' => 'En créer un récemment', + 'create_now' => 'En créer un maintenant', 'revisions' => 'Révisions', 'meta_created' => 'Créé :timeLength', 'meta_created_name' => 'Créé :timeLength par :user', @@ -59,8 +59,8 @@ return [ 'books_create' => 'Créer un nouveau livre', 'books_delete' => 'Supprimer un livre', 'books_delete_named' => 'Supprimer le livre :bookName', - 'books_delete_explain' => 'Ceci va supprimer le livre nommé \':bookName\', Tous les chapitres et pages seront supprimés.', - 'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre?', + 'books_delete_explain' => 'Ceci va supprimer le livre nommé \':bookName\', tous les chapitres et pages seront supprimés.', + 'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre ?', 'books_edit' => 'Modifier le livre', 'books_edit_named' => 'Modifier le livre :bookName', 'books_form_book_name' => 'Nom du livre', @@ -90,18 +90,18 @@ return [ 'chapters_create' => 'Créer un nouveau chapitre', 'chapters_delete' => 'Supprimer le chapitre', 'chapters_delete_named' => 'Supprimer le chapitre :chapterName', - 'chapters_delete_explain' => 'Ceci va supprimer le chapitre \':chapterName\', Toutes les pages seront déplacée dans le livre parent.', - 'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre?', + 'chapters_delete_explain' => 'Ceci va supprimer le chapitre \':chapterName\', toutes les pages seront déplacées dans le livre parent.', + 'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre ?', 'chapters_edit' => 'Modifier le chapitre', 'chapters_edit_named' => 'Modifier le chapitre :chapterName', 'chapters_save' => 'Enregistrer le chapitre', - 'chapters_move' => 'Déplace le chapitre', + 'chapters_move' => 'Déplacer le chapitre', 'chapters_move_named' => 'Déplacer le chapitre :chapterName', 'chapter_move_success' => 'Chapitre déplacé dans :bookName', 'chapters_permissions' => 'Permissions du chapitre', - 'chapters_empty' => 'Il n\'y a pas de pages dans ce chapitre actuellement.', + 'chapters_empty' => 'Il n\'y a pas de page dans ce chapitre actuellement.', 'chapters_permissions_active' => 'Permissions du chapitre activées', - 'chapters_permissions_success' => 'Permissions du chapitres mises à jour', + 'chapters_permissions_success' => 'Permissions du chapitre mises à jour', /** * Pages @@ -118,8 +118,8 @@ return [ 'pages_delete_draft' => 'Supprimer le brouillon', 'pages_delete_success' => 'Page supprimée', 'pages_delete_draft_success' => 'Brouillon supprimé', - 'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page?', - 'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon?', + 'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page ?', + 'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon ?', 'pages_editing_named' => 'Modification de la page :pageName', 'pages_edit_toggle_header' => 'Afficher/cacher l\'en-tête', 'pages_edit_save_draft' => 'Enregistrer le brouillon', @@ -131,7 +131,7 @@ return [ 'pages_edit_discard_draft' => 'Ecarter le brouillon', 'pages_edit_set_changelog' => 'Remplir le journal des changements', 'pages_edit_enter_changelog_desc' => 'Entrez une brève description des changements effectués', - 'pages_edit_enter_changelog' => 'Entrez dans le journal des changements', + 'pages_edit_enter_changelog' => 'Entrer dans le journal des changements', 'pages_save' => 'Enregistrez la page', 'pages_title' => 'Titre de la page', 'pages_name' => 'Nom de la page', @@ -139,7 +139,7 @@ return [ 'pages_md_preview' => 'Prévisualisation', 'pages_md_insert_image' => 'Insérer une image', 'pages_md_insert_link' => 'Insérer un lien', - 'pages_not_in_chapter' => 'La page n\'est pas dans un chanpitre', + 'pages_not_in_chapter' => 'La page n\'est pas dans un chapitre', 'pages_move' => 'Déplacer la page', 'pages_move_success' => 'Page déplacée à ":parentName"', 'pages_permissions' => 'Permissions de la page', @@ -160,15 +160,15 @@ return [ 'pages_initial_revision' => 'Publication initiale', 'pages_initial_name' => 'Nouvelle page', 'pages_editing_draft_notification' => 'Vous éditez actuellement un brouillon qui a été sauvé :timeDiff.', - 'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visit. Vous devriez écarter ce brouillon.', + 'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visite. Vous devriez écarter ce brouillon.', 'pages_draft_edit_active' => [ - 'start_a' => ':count utilisateurs ont commencé a éditer cette page', + 'start_a' => ':count utilisateurs ont commencé à éditer cette page', 'start_b' => ':userName a commencé à éditer cette page', 'time_a' => 'depuis la dernière sauvegarde', 'time_b' => 'dans les :minCount dernières minutes', - 'message' => ':start :time. Attention a ne pas écraser les mises à jour de quelqu\'un d\'autre!', + 'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\'un d\'autre !', ], - 'pages_draft_discarded' => 'Brouuillon écarté, la page est dans sa version actuelle.', + 'pages_draft_discarded' => 'Brouillon écarté, la page est dans sa version actuelle.', /** * Editor sidebar @@ -210,7 +210,29 @@ return [ */ 'profile_user_for_x' => 'Utilisateur depuis :time', 'profile_created_content' => 'Contenu créé', - 'profile_not_created_pages' => ':userName n\'a pas créé de pages', - 'profile_not_created_chapters' => ':userName n\'a pas créé de chapitres', - 'profile_not_created_books' => ':userName n\'a pas créé de livres', + '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 72af89f7f..9e20147b6 100644 --- a/resources/lang/fr/errors.php +++ b/resources/lang/fr/errors.php @@ -18,21 +18,21 @@ return [ 'ldap_fail_anonymous' => 'L\'accès LDAP anonyme n\'a pas abouti', 'ldap_fail_authed' => 'L\'accès LDAP n\'a pas abouti avec cet utilisateur et ce mot de passe', 'ldap_extension_not_installed' => 'L\'extention LDAP PHP n\'est pas installée', - 'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed', - 'social_no_action_defined' => 'No action defined', - 'social_account_in_use' => 'Cet compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.', - 'social_account_email_in_use' => 'L\'email :email Est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.', + 'ldap_cannot_connect' => 'Impossible de se connecter au serveur LDAP, la connexion initiale a échoué', + 'social_no_action_defined' => 'Pas d\'action définie', + 'social_account_in_use' => 'Ce compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.', + 'social_account_email_in_use' => 'L\'email :email est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.', 'social_account_existing' => 'Ce compte :socialAccount est déjà rattaché à votre profil.', 'social_account_already_used_existing' => 'Ce compte :socialAccount est déjà utilisé par un autre utilisateur.', 'social_account_not_used' => 'Ce compte :socialAccount n\'est lié à aucun utilisateur. ', 'social_account_register_instructions' => 'Si vous n\'avez pas encore de compte, vous pouvez le lier avec l\'option :socialAccount.', - 'social_driver_not_found' => 'Social driver not found', - 'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.', + 'social_driver_not_found' => 'Pilote de compte social absent', + 'social_driver_not_configured' => 'Vos préférences pour le compte :socialAccount sont incorrectes.', // System - 'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.', + 'path_not_writable' => 'Impossible d\'écrire dans :filePath. Assurez-vous d\'avoir les droits d\'écriture sur le serveur', 'cannot_get_image_from_url' => 'Impossible de récupérer l\'image depuis :url', - 'cannot_create_thumbs' => 'Le serveur ne peux pas créer de miniatures, vérifier que l\extensions GD PHP est installée.', + 'cannot_create_thumbs' => 'Le serveur ne peut pas créer de miniature, vérifier que l\'extension PHP GD est installée.', 'server_upload_limit' => 'La taille du fichier est trop grande.', 'image_upload_error' => 'Une erreur est survenue pendant l\'envoi de l\'image', @@ -57,7 +57,7 @@ return [ // Roles 'role_cannot_be_edited' => 'Ce rôle ne peut pas être modifié', - 'role_system_cannot_be_deleted' => 'Ceci est un rôle du système et on ne peut pas le supprimer', + 'role_system_cannot_be_deleted' => 'Ceci est un rôle du système et ne peut pas être supprimé', 'role_registration_default_cannot_delete' => 'Ce rôle ne peut pas être supprimé tant qu\'il est le rôle par défaut', // Error pages @@ -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/fr/passwords.php b/resources/lang/fr/passwords.php index 7be81da23..484b4b20c 100644 --- a/resources/lang/fr/passwords.php +++ b/resources/lang/fr/passwords.php @@ -16,7 +16,7 @@ return [ 'password' => 'Les mots de passe doivent faire au moins 6 caractères et correspondre à la confirmation.', 'user' => "Nous n'avons pas trouvé d'utilisateur avec cette adresse.", 'token' => 'Le jeton de réinitialisation est invalide.', - 'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe!', - 'reset' => 'Votre mot de passe a été réinitialisé!', + 'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe !', + 'reset' => 'Votre mot de passe a été réinitialisé !', ]; diff --git a/resources/lang/fr/settings.php b/resources/lang/fr/settings.php index 5d9c966e7..5516e66a4 100644 --- a/resources/lang/fr/settings.php +++ b/resources/lang/fr/settings.php @@ -19,27 +19,27 @@ return [ 'app_settings' => 'Préférences de l\'application', 'app_name' => 'Nom de l\'application', 'app_name_desc' => 'Ce nom est affiché dans l\'en-tête et les e-mails.', - 'app_name_header' => 'Afficher le nom dans l\'en-tête?', - 'app_public_viewing' => 'Accepter le visionnage public des pages?', - 'app_secure_images' => 'Activer l\'ajout d\'image sécurisé?', + 'app_name_header' => 'Afficher le nom dans l\'en-tête ?', + 'app_public_viewing' => 'Accepter le visionnage public des pages ?', + 'app_secure_images' => 'Activer l\'ajout d\'image sécurisé ?', 'app_secure_images_desc' => 'Pour des questions de performances, toutes les images sont publiques. Cette option ajoute une chaîne aléatoire difficile à deviner dans les URLs des images.', 'app_editor' => 'Editeur des pages', 'app_editor_desc' => 'Sélectionnez l\'éditeur qui sera utilisé pour modifier les pages.', 'app_custom_html' => 'HTML personnalisé dans l\'en-tête', - 'app_custom_html_desc' => 'Le contenu inséré ici sera jouté en bas de la balise <head> de toutes les pages. Vous pouvez l\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.', + 'app_custom_html_desc' => 'Le contenu inséré ici sera ajouté en bas de la balise <head> de toutes les pages. Vous pouvez l\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.', 'app_logo' => 'Logo de l\'Application', 'app_logo_desc' => 'Cette image doit faire 43px de hauteur. <br>Les images plus larges seront réduites.', 'app_primary_color' => 'Couleur principale de l\'application', - 'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.', + 'app_primary_color_desc' => 'Cela devrait être une valeur hexadécimale. <br>Laisser vide pour rétablir la couleur par défaut.', /** * Registration settings */ 'reg_settings' => 'Préférence pour l\'inscription', - 'reg_allow' => 'Accepter l\'inscription?', + 'reg_allow' => 'Accepter l\'inscription ?', 'reg_default_role' => 'Rôle par défaut lors de l\'inscription', - 'reg_confirm_email' => 'Obliger la confirmation par e-mail?', + 'reg_confirm_email' => 'Obliger la confirmation par e-mail ?', 'reg_confirm_email_desc' => 'Si la restriction de domaine est activée, la confirmation sera automatiquement obligatoire et cette valeur sera ignorée.', 'reg_confirm_restrict_domain' => 'Restreindre l\'inscription à un domaine', 'reg_confirm_restrict_domain_desc' => 'Entrez une liste de domaines acceptés lors de l\'inscription, séparés par une virgule. Les utilisateur recevront un e-mail de confirmation à cette adresse. <br> Les utilisateurs pourront changer leur adresse après inscription s\'ils le souhaitent.', @@ -57,17 +57,17 @@ return [ 'role_delete_confirm' => 'Ceci va supprimer le rôle \':roleName\'.', 'role_delete_users_assigned' => 'Ce rôle a :userCount utilisateurs assignés. Vous pouvez choisir un rôle de remplacement pour ces utilisateurs.', 'role_delete_no_migration' => "Ne pas assigner de nouveau rôle", - 'role_delete_sure' => 'Êtes vous sûr(e) de vouloir supprimer ce rôle?', + 'role_delete_sure' => 'Êtes vous sûr(e) de vouloir supprimer ce rôle ?', 'role_delete_success' => 'Le rôle a été supprimé avec succès', 'role_edit' => 'Modifier le rôle', 'role_details' => 'Détails du rôle', - 'role_name' => 'Nom du Rôle', + 'role_name' => 'Nom du rôle', 'role_desc' => 'Courte description du rôle', 'role_system' => 'Permissions système', 'role_manage_users' => 'Gérer les utilisateurs', 'role_manage_roles' => 'Gérer les rôles et permissions', 'role_manage_entity_permissions' => 'Gérer les permissions sur les livres, chapitres et pages', - 'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres chapitres et pages', + 'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres, chapitres, et pages', 'role_manage_settings' => 'Gérer les préférences de l\'application', 'role_asset' => 'Asset Permissions', 'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.', @@ -95,7 +95,7 @@ return [ 'users_delete' => 'Supprimer un utilisateur', 'users_delete_named' => 'Supprimer l\'utilisateur :userName', 'users_delete_warning' => 'Ceci va supprimer \':userName\' du système.', - 'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur?', + 'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?', 'users_delete_success' => 'Utilisateurs supprimés avec succès', 'users_edit' => 'Modifier l\'utilisateur', 'users_edit_profile' => 'Modifier le profil', @@ -107,7 +107,7 @@ return [ 'users_social_accounts_info' => 'Vous pouvez connecter des réseaux sociaux à votre compte pour vous connecter plus rapidement. Déconnecter un compte n\'enlèvera pas les accès autorisés précédemment sur votre compte de réseau social.', 'users_social_connect' => 'Connecter le compte', 'users_social_disconnect' => 'Déconnecter le compte', - 'users_social_connected' => 'Votre compte :socialAccount a élté ajouté avec succès.', + 'users_social_connected' => 'Votre compte :socialAccount a été ajouté avec succès.', 'users_social_disconnected' => 'Votre compte :socialAccount a été déconnecté avec succès', ]; 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/pl/activities.php b/resources/lang/pl/activities.php new file mode 100644 index 000000000..5ef5acab0 --- /dev/null +++ b/resources/lang/pl/activities.php @@ -0,0 +1,40 @@ +<?php + +return [ + + /** + * Activity text strings. + * Is used for all the text within activity logs & notifications. + */ + + // Pages + 'page_create' => 'utworzono stronę', + 'page_create_notification' => 'Strona utworzona pomyślnie', + 'page_update' => 'zaktualizowano stronę', + 'page_update_notification' => 'Strona zaktualizowana pomyślnie', + 'page_delete' => 'usunięto stronę', + 'page_delete_notification' => 'Strona usunięta pomyślnie', + 'page_restore' => 'przywrócono stronę', + 'page_restore_notification' => 'Stronga przywrócona pomyślnie', + 'page_move' => 'przeniesiono stronę', + + // Chapters + 'chapter_create' => 'utworzono rozdział', + 'chapter_create_notification' => 'Rozdział utworzony pomyślnie', + 'chapter_update' => 'zaktualizowano rozdział', + 'chapter_update_notification' => 'Rozdział zaktualizowany pomyślnie', + 'chapter_delete' => 'usunięto rozdział', + 'chapter_delete_notification' => 'Rozdział usunięty pomyślnie', + 'chapter_move' => 'przeniesiono rozdział', + + // Books + 'book_create' => 'utworzono księgę', + 'book_create_notification' => 'Księga utworzona pomyślnie', + 'book_update' => 'zaktualizowano księgę', + 'book_update_notification' => 'Księga zaktualizowana pomyślnie', + 'book_delete' => 'usunięto księgę', + 'book_delete_notification' => 'Księga usunięta pomyślnie', + 'book_sort' => 'posortowano księgę', + 'book_sort_notification' => 'Księga posortowana pomyślnie', + +]; diff --git a/resources/lang/pl/auth.php b/resources/lang/pl/auth.php new file mode 100644 index 000000000..740e067ca --- /dev/null +++ b/resources/lang/pl/auth.php @@ -0,0 +1,76 @@ +<?php +return [ + /* + |-------------------------------------------------------------------------- + | Authentication Language Lines + |-------------------------------------------------------------------------- + | + | The following language lines are used during authentication for various + | messages that we need to display to the user. You are free to modify + | these language lines according to your application's requirements. + | + */ + 'failed' => 'These credentials do not match our records.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + + /** + * Login & Register + */ + 'sign_up' => 'Zarejestruj się', + 'log_in' => 'Zaloguj się', + 'log_in_with' => 'Zaloguj się za pomocą :socialDriver', + 'sign_up_with' => 'Zarejestruj się za pomocą :socialDriver', + 'logout' => 'Wyloguj', + + 'name' => 'Imię', + 'username' => 'Nazwa użytkownika', + 'email' => 'Email', + 'password' => 'Hasło', + 'password_confirm' => 'Potwierdzenie hasła', + 'password_hint' => 'Musi mieć więcej niż 5 znaków', + 'forgot_password' => 'Przypomnij hasło', + 'remember_me' => 'Zapamiętaj mnie', + 'ldap_email_hint' => 'Wprowadź adres email dla tego konta.', + 'create_account' => 'Utwórz konto', + 'social_login' => 'Logowanie za pomocą konta społecznościowego', + 'social_registration' => 'Rejestracja za pomocą konta społecznościowego', + 'social_registration_text' => 'Zarejestruj się za pomocą innej usługi.', + + 'register_thanks' => 'Dziękujemy za rejestrację!', + 'register_confirm' => 'Sprawdź podany adres e-mail i kliknij w link, by uzyskać dostęp do :appName.', + 'registrations_disabled' => 'Rejestracja jest obecnie zablokowana.', + 'registration_email_domain_invalid' => 'Adresy e-mail z tej domeny nie mają dostępu do tej aplikacji', + 'register_success' => 'Dziękujemy za rejestrację! Zalogowano Cię automatycznie.', + + + /** + * Password Reset + */ + 'reset_password' => 'Resetowanie hasła', + 'reset_password_send_instructions' => 'Wprowadź adres e-mail powiązany z Twoim kontem, by otrzymać link do resetowania hasła.', + 'reset_password_send_button' => 'Wyślij link do resetowania hasła', + 'reset_password_sent_success' => 'Wysłano link do resetowania hasła na adres :email.', + 'reset_password_success' => 'Hasło zostało zresetowane pomyślnie.', + + 'email_reset_subject' => 'Resetowanie hasła do :appName', + 'email_reset_text' => 'Otrzymujesz tę wiadomość ponieważ ktoś zażądał zresetowania hasła do Twojego konta.', + 'email_reset_not_requested' => 'Jeśli to nie Ty złożyłeś żądanie zresetowania hasła, zignoruj tę wiadomość.', + + + /** + * Email Confirmation + */ + 'email_confirm_subject' => 'Potwierdź swój adres email w :appName', + 'email_confirm_greeting' => 'Dziękujemy za dołączenie do :appName!', + 'email_confirm_text' => 'Prosimy byś potwierdził swoje hasło klikając przycisk poniżej:', + 'email_confirm_action' => 'Potwierdź email', + 'email_confirm_send_error' => 'Wymagane jest potwierdzenie hasła, lecz wiadomość nie mogła zostać wysłana. Skontaktuj się z administratorem w celu upewnienia się, że skrzynka została skonfigurowana prawidłowo.', + 'email_confirm_success' => 'Adres email został potwierdzony!', + 'email_confirm_resent' => 'Wiadomość potwierdzająca została wysłana, sprawdź swoją skrzynkę.', + + 'email_not_confirmed' => 'Adres email niepotwierdzony', + 'email_not_confirmed_text' => 'Twój adres email nie został jeszcze potwierdzony.', + 'email_not_confirmed_click_link' => 'Aby potwierdzić swoje konto kliknij w link wysłany w wiadomości po rejestracji.', + 'email_not_confirmed_resend' => 'Jeśli wiadomość do Ciebie nie dotarła możesz wysłać ją ponownie wypełniając formularz poniżej.', + 'email_not_confirmed_resend_button' => 'Wyślij ponownie wiadomość z potwierdzeniem', +]; \ No newline at end of file diff --git a/resources/lang/pl/common.php b/resources/lang/pl/common.php new file mode 100644 index 000000000..1c8963653 --- /dev/null +++ b/resources/lang/pl/common.php @@ -0,0 +1,59 @@ +<?php +return [ + + /** + * Buttons + */ + 'cancel' => 'Anuluj', + 'confirm' => 'Zatwierdź', + 'back' => 'Wstecz', + 'save' => 'Zapisz', + 'continue' => 'Kontynuuj', + 'select' => 'Wybierz', + + /** + * Form Labels + */ + 'name' => 'Nazwa', + 'description' => 'Opis', + 'role' => 'Rola', + + /** + * Actions + */ + 'actions' => 'Akcje', + 'view' => 'Widok', + 'create' => 'Utwórz', + 'update' => 'Zaktualizuj', + 'edit' => 'Edytuj', + 'sort' => 'Sortuj', + 'move' => 'Przenieś', + 'delete' => 'Usuń', + 'search' => 'Szukaj', + 'search_clear' => 'Wyczyść wyszukiwanie', + 'reset' => 'Resetuj', + 'remove' => 'Usuń', + 'add' => 'Dodaj', + + + /** + * Misc + */ + 'deleted_user' => 'Użytkownik usunięty', + 'no_activity' => 'Brak aktywności do pokazania', + 'no_items' => 'Brak elementów do wyświetlenia', + 'back_to_top' => 'Powrót na górę', + 'toggle_details' => 'Włącz/wyłącz szczegóły', + + /** + * Header + */ + 'view_profile' => 'Zobacz profil', + 'edit_profile' => 'Edytuj profil', + + /** + * Email Content + */ + 'email_action_help' => 'Jeśli masz problem z kliknięciem przycisku ":actionText", skopiuj i wklej poniższy adres URL w nowej karcie swojej przeglądarki:', + 'email_rights' => 'Wszelkie prawa zastrzeżone', +]; \ No newline at end of file diff --git a/resources/lang/pl/components.php b/resources/lang/pl/components.php new file mode 100644 index 000000000..c1dbcd44b --- /dev/null +++ b/resources/lang/pl/components.php @@ -0,0 +1,32 @@ +<?php +return [ + + /** + * Image Manager + */ + 'image_select' => 'Wybór obrazka', + 'image_all' => 'Wszystkie', + 'image_all_title' => 'Zobacz wszystkie obrazki', + 'image_book_title' => 'Zobacz obrazki zapisane w tej księdze', + 'image_page_title' => 'Zobacz obrazki zapisane na tej stronie', + 'image_search_hint' => 'Szukaj po nazwie obrazka', + 'image_uploaded' => 'Udostępniono :uploadedDate', + 'image_load_more' => 'Wczytaj więcej', + 'image_image_name' => 'Nazwa obrazka', + 'image_delete_confirm' => 'Ten obrazek jest używany na stronach poniżej, kliknij ponownie Usuń by potwierdzić usunięcie obrazka.', + 'image_select_image' => 'Wybierz obrazek', + 'image_dropzone' => 'Upuść obrazki tutaj lub kliknij by wybrać obrazki do udostępnienia', + 'images_deleted' => 'Usunięte obrazki', + 'image_preview' => 'Podgląd obrazka', + 'image_upload_success' => 'Obrazek wysłany pomyślnie', + 'image_update_success' => 'Szczegóły obrazka zaktualizowane pomyślnie', + 'image_delete_success' => 'Obrazek usunięty pomyślnie', + + /** + * Code editor + */ + 'code_editor' => 'Edytuj kod', + 'code_language' => 'Język kodu', + 'code_content' => 'Zawartość kodu', + 'code_save' => 'Zapisz kod', +]; \ No newline at end of file diff --git a/resources/lang/pl/entities.php b/resources/lang/pl/entities.php new file mode 100644 index 000000000..30e853bce --- /dev/null +++ b/resources/lang/pl/entities.php @@ -0,0 +1,237 @@ +<?php +return [ + + /** + * Shared + */ + 'recently_created' => 'Ostatnio utworzone', + 'recently_created_pages' => 'Ostatnio utworzone strony', + 'recently_updated_pages' => 'Ostatnio zaktualizowane strony', + 'recently_created_chapters' => 'Ostatnio utworzone rozdziały', + 'recently_created_books' => 'Ostatnio utworzone księgi', + 'recently_update' => 'Ostatnio zaktualizowane', + 'recently_viewed' => 'Ostatnio wyświetlane', + 'recent_activity' => 'Ostatnia aktywność', + 'create_now' => 'Utwórz teraz', + 'revisions' => 'Rewizje', + 'meta_revision' => 'Rewizja #:revisionCount', + 'meta_created' => 'Utworzono :timeLength', + 'meta_created_name' => 'Utworzono :timeLength przez :user', + 'meta_updated' => 'Zaktualizowano :timeLength', + 'meta_updated_name' => 'Zaktualizowano :timeLength przez :user', + 'x_pages' => ':count stron', + 'entity_select' => 'Wybór encji', + 'images' => 'Obrazki', + 'my_recent_drafts' => 'Moje ostatnie szkice', + 'my_recently_viewed' => 'Moje ostatnio wyświetlane', + 'no_pages_viewed' => 'Nie wyświetlano żadnych stron', + 'no_pages_recently_created' => 'Nie utworzono ostatnio żadnych stron', + 'no_pages_recently_updated' => 'Nie zaktualizowano ostatnio żadnych stron', + 'export' => 'Eksportuj', + 'export_html' => 'Plik HTML', + 'export_pdf' => 'Plik PDF', + 'export_text' => 'Plik tekstowy', + + /** + * Permissions and restrictions + */ + 'permissions' => 'Uprawnienia', + 'permissions_intro' => 'Jeśli odblokowane, te uprawnienia będą miały priorytet względem pozostałych ustawionych uprawnień ról.', + 'permissions_enable' => 'Odblokuj własne uprawnienia', + 'permissions_save' => 'Zapisz uprawnienia', + + /** + * Search + */ + 'search_results' => 'Wyniki wyszukiwania', + 'search_total_results_found' => ':count znalezionych wyników|:count ogółem znalezionych wyników', + 'search_clear' => 'Wyczyść wyszukiwanie', + 'search_no_pages' => 'Brak stron spełniających zadane kryterium', + 'search_for_term' => 'Szukaj :term', + 'search_more' => 'Więcej wyników', + 'search_filters' => 'Filtry wyszukiwania', + 'search_content_type' => 'Rodziaj treści', + 'search_exact_matches' => 'Dokładne frazy', + 'search_tags' => 'Tagi wyszukiwania', + 'search_viewed_by_me' => 'Wyświetlone przeze mnie', + 'search_not_viewed_by_me' => 'Niewyświetlone przeze mnie', + 'search_permissions_set' => 'Zbiór uprawnień', + 'search_created_by_me' => 'Utworzone przeze mnie', + 'search_updated_by_me' => 'Zaktualizowane przeze mnie', + 'search_updated_before' => 'Zaktualizowane przed', + 'search_updated_after' => 'Zaktualizowane po', + 'search_created_before' => 'Utworzone przed', + 'search_created_after' => 'Utworzone po', + 'search_set_date' => 'Ustaw datę', + 'search_update' => 'Zaktualizuj wyszukiwanie', + + /** + * Books + */ + 'book' => 'Księga', + 'books' => 'Księgi', + 'books_empty' => 'Brak utworzonych ksiąg', + 'books_popular' => 'Popularne księgi', + 'books_recent' => 'Ostatnie księgi', + 'books_popular_empty' => 'Najbardziej popularne księgi zostaną wyświetlone w tym miejscu.', + 'books_create' => 'Utwórz księgę', + 'books_delete' => 'Usuń księgę', + 'books_delete_named' => 'Usuń księgę :bookName', + 'books_delete_explain' => 'To spowoduje usunięcie księgi \':bookName\', Wszystkie strony i rozdziały zostaną usunięte.', + 'books_delete_confirmation' => 'Czy na pewno chcesz usunąc tę księgę?', + 'books_edit' => 'Edytuj księgę', + 'books_edit_named' => 'Edytuj księgę :bookName', + 'books_form_book_name' => 'Nazwa księgi', + 'books_save' => 'Zapisz księgę', + 'books_permissions' => 'Uprawnienia księgi', + 'books_permissions_updated' => 'Zaktualizowano uprawnienia księgi', + 'books_empty_contents' => 'Brak stron lub rozdziałów w tej księdze.', + 'books_empty_create_page' => 'Utwórz nową stronę', + 'books_empty_or' => 'lub', + 'books_empty_sort_current_book' => 'posortuj bieżącą księgę', + 'books_empty_add_chapter' => 'Dodaj rozdział', + 'books_permissions_active' => 'Uprawnienia księgi aktywne', + 'books_search_this' => 'Wyszukaj w tej księdze', + 'books_navigation' => 'Nawigacja po księdze', + 'books_sort' => 'Sortuj zawartość Księgi', + 'books_sort_named' => 'Sortuj księgę :bookName', + 'books_sort_show_other' => 'Pokaż inne księgi', + 'books_sort_save' => 'Zapisz nowy porządek', + + /** + * Chapters + */ + 'chapter' => 'Rozdział', + 'chapters' => 'Rozdziały', + 'chapters_popular' => 'Popularne rozdziały', + 'chapters_new' => 'Nowy rozdział', + 'chapters_create' => 'Utwórz nowy rozdział', + 'chapters_delete' => 'Usuń rozdział', + 'chapters_delete_named' => 'Usuń rozdział :chapterName', + 'chapters_delete_explain' => 'To spowoduje usunięcie rozdziału \':chapterName\', Wszystkie strony zostaną usunięte + i dodane bezpośrednio do księgi macierzystej.', + 'chapters_delete_confirm' => 'Czy na pewno chcesz usunąć ten rozdział?', + 'chapters_edit' => 'Edytuj rozdział', + 'chapters_edit_named' => 'Edytuj rozdział :chapterName', + 'chapters_save' => 'Zapisz rozdział', + 'chapters_move' => 'Przenieś rozdział', + 'chapters_move_named' => 'Przenieś rozdział :chapterName', + 'chapter_move_success' => 'Rozdział przeniesiony do :bookName', + 'chapters_permissions' => 'Uprawienia rozdziału', + 'chapters_empty' => 'Brak stron w tym rozdziale.', + 'chapters_permissions_active' => 'Uprawnienia rozdziału aktywne', + 'chapters_permissions_success' => 'Zaktualizowano uprawnienia rozdziału', + 'chapters_search_this' => 'Przeszukaj ten rozdział', + + /** + * Pages + */ + 'page' => 'Strona', + 'pages' => 'Strony', + 'pages_popular' => 'Popularne strony', + 'pages_new' => 'Nowa strona', + 'pages_attachments' => 'Załączniki', + 'pages_navigation' => 'Nawigacja po stronie', + 'pages_delete' => 'Usuń stronę', + 'pages_delete_named' => 'Usuń stronę :pageName', + 'pages_delete_draft_named' => 'Usuń szkic strony :pageName', + 'pages_delete_draft' => 'Usuń szkic strony', + 'pages_delete_success' => 'Strona usunięta pomyślnie', + 'pages_delete_draft_success' => 'Szkic strony usunięty pomyślnie', + 'pages_delete_confirm' => 'Czy na pewno chcesz usunąć tę stron?', + 'pages_delete_draft_confirm' => 'Czy na pewno chcesz usunąć szkic strony?', + 'pages_editing_named' => 'Edytowanie strony :pageName', + 'pages_edit_toggle_header' => 'Włącz/wyłącz nagłówek', + 'pages_edit_save_draft' => 'Zapisz szkic', + 'pages_edit_draft' => 'Edytuj szkic strony', + 'pages_editing_draft' => 'Edytowanie szkicu strony', + 'pages_editing_page' => 'Edytowanie strony', + 'pages_edit_draft_save_at' => 'Szkic zapisany ', + 'pages_edit_delete_draft' => 'Usuń szkic', + 'pages_edit_discard_draft' => 'Porzuć szkic', + 'pages_edit_set_changelog' => 'Ustaw log zmian', + 'pages_edit_enter_changelog_desc' => 'Opisz zmiany, które zostały wprowadzone', + 'pages_edit_enter_changelog' => 'Wyświetl log zmian', + 'pages_save' => 'Zapisz stronę', + 'pages_title' => 'Tytuł strony', + 'pages_name' => 'Nazwa strony', + 'pages_md_editor' => 'Edytor', + 'pages_md_preview' => 'Podgląd', + 'pages_md_insert_image' => 'Wstaw obrazek', + 'pages_md_insert_link' => 'Wstaw łącze do encji', + 'pages_not_in_chapter' => 'Strona nie została umieszczona w rozdziale', + 'pages_move' => 'Przenieś stronę', + 'pages_move_success' => 'Strona przeniesiona do ":parentName"', + 'pages_permissions' => 'Uprawnienia strony', + 'pages_permissions_success' => 'Zaktualizowano uprawnienia strony', + 'pages_revisions' => 'Rewizje strony', + 'pages_revisions_named' => 'Rewizje strony :pageName', + 'pages_revision_named' => 'Rewizja stroony :pageName', + 'pages_revisions_created_by' => 'Utworzona przez', + 'pages_revisions_date' => 'Data rewizji', + 'pages_revisions_number' => '#', + 'pages_revisions_changelog' => 'Log zmian', + 'pages_revisions_changes' => 'Zmiany', + 'pages_revisions_current' => 'Obecna wersja', + 'pages_revisions_preview' => 'Podgląd', + 'pages_revisions_restore' => 'Przywróć', + 'pages_revisions_none' => 'Ta strona nie posiada żadnych rewizji', + 'pages_copy_link' => 'Kopiuj link', + 'pages_permissions_active' => 'Uprawnienia strony aktywne', + 'pages_initial_revision' => 'Wydanie pierwotne', + 'pages_initial_name' => 'Nowa strona', + 'pages_editing_draft_notification' => 'Edytujesz obecnie szkic, który był ostatnio zapisany :timeDiff.', + 'pages_draft_edited_notification' => 'Od tego czasu ta strona była zmieniana. Zalecane jest odrzucenie tego szkicu.', + 'pages_draft_edit_active' => [ + 'start_a' => ':count użytkowników rozpoczęło edytowanie tej strony', + 'start_b' => ':userName edytuje stronę', + 'time_a' => ' od czasu ostatniej edycji', + 'time_b' => 'w ciągu ostatnich :minCount minut', + 'message' => ':start :time. Pamiętaj by nie nadpisywać czyichś zmian!', + ], + 'pages_draft_discarded' => 'Szkic odrzucony, edytor został uzupełniony najnowszą wersją strony', + + /** + * Editor sidebar + */ + 'page_tags' => 'Tagi strony', + 'tag' => 'Tag', + 'tags' => '', + 'tag_value' => 'Wartość tagu (opcjonalnie)', + 'tags_explain' => "Dodaj tagi by skategoryzować zawartość. \n W celu dokładniejszej organizacji zawartości możesz dodać wartości do tagów.", + 'tags_add' => 'Dodaj kolejny tag', + 'attachments' => 'Załączniki', + 'attachments_explain' => 'Udostępnij kilka plików lub załącz link. Będą one widoczne na marginesie strony.', + 'attachments_explain_instant_save' => 'Zmiany są zapisywane natychmiastowo.', + 'attachments_items' => 'Załączniki', + 'attachments_upload' => 'Dodaj plik', + 'attachments_link' => 'Dodaj link', + 'attachments_set_link' => 'Ustaw link', + 'attachments_delete_confirm' => 'Kliknij ponownie Usuń by potwierdzić usunięcie załącznika.', + 'attachments_dropzone' => 'Upuść pliki lub kliknij tutaj by udostępnić pliki', + 'attachments_no_files' => 'Nie udostępniono plików', + 'attachments_explain_link' => 'Możesz załączyć link jeśli nie chcesz udostępniać pliku. Może być to link do innej strony lub link do pliku w chmurze.', + 'attachments_link_name' => 'Nazwa linku', + 'attachment_link' => 'Link do załącznika', + 'attachments_link_url' => 'Link do pliku', + 'attachments_link_url_hint' => 'Strona lub plik', + 'attach' => 'Załącz', + 'attachments_edit_file' => 'Edytuj plik', + 'attachments_edit_file_name' => 'Nazwa pliku', + 'attachments_edit_drop_upload' => 'Upuść pliki lub kliknij tutaj by udostępnić pliki i nadpisać istniejące', + 'attachments_order_updated' => 'Kolejność załączników zaktualizowana', + 'attachments_updated_success' => 'Szczegóły załączników zaktualizowane', + 'attachments_deleted' => 'Załączniki usunięte', + 'attachments_file_uploaded' => 'Plik załączony pomyślnie', + 'attachments_file_updated' => 'Plik zaktualizowany pomyślnie', + 'attachments_link_attached' => 'Link pomyślnie dodany do strony', + + /** + * Profile View + */ + 'profile_user_for_x' => 'Użytkownik od :time', + 'profile_created_content' => 'Utworzona zawartość', + 'profile_not_created_pages' => ':userName nie utworzył żadnych stron', + 'profile_not_created_chapters' => ':userName nie utworzył żadnych rozdziałów', + 'profile_not_created_books' => ':userName nie utworzył żadnych ksiąg', +]; \ No newline at end of file diff --git a/resources/lang/pl/errors.php b/resources/lang/pl/errors.php new file mode 100644 index 000000000..633bf7a2d --- /dev/null +++ b/resources/lang/pl/errors.php @@ -0,0 +1,70 @@ +<?php + +return [ + + /** + * Error text strings. + */ + + // Permissions + 'permission' => 'Nie masz uprawnień do wyświetlenia tej strony.', + 'permissionJson' => 'Nie masz uprawnień do wykonania tej akcji.', + + // Auth + 'error_user_exists_different_creds' => 'Użytkownik o adresie :email już istnieje.', + 'email_already_confirmed' => 'Email został potwierdzony, spróbuj się zalogować.', + 'email_confirmation_invalid' => 'Ten token jest nieprawidłowy lub został już wykorzystany. Spróbuj zarejestrować się ponownie.', + 'email_confirmation_expired' => 'Ten token potwierdzający wygasł. Wysłaliśmy Ci kolejny.', + 'ldap_fail_anonymous' => 'Dostęp LDAP przy użyciu anonimowego powiązania nie powiódł się', + 'ldap_fail_authed' => 'Dostęp LDAP przy użyciu tego dn i hasła nie powiódł się', + 'ldap_extension_not_installed' => 'Rozszerzenie LDAP PHP nie zostało zainstalowane', + 'ldap_cannot_connect' => 'Nie można połączyć z serwerem LDAP, połączenie nie zostało ustanowione', + 'social_no_action_defined' => 'Brak zdefiniowanej akcji', + 'social_account_in_use' => 'To konto :socialAccount jest już w użyciu, spróbuj zalogować się za pomocą opcji :socialAccount.', + 'social_account_email_in_use' => 'Email :email jest już w użyciu. Jeśli masz już konto, połącz konto :socialAccount z poziomu ustawień profilu.', + 'social_account_existing' => 'Konto :socialAccount jest już połączone z Twoim profilem', + 'social_account_already_used_existing' => 'Konto :socialAccount jest już używane przez innego użytkownika.', + 'social_account_not_used' => 'To konto :socialAccount nie jest połączone z żadnym użytkownikiem. Połącz je ze swoim kontem w ustawieniach profilu. ', + 'social_account_register_instructions' => 'Jeśli nie masz jeszcze konta, możesz zarejestrować je używając opcji :socialAccount.', + 'social_driver_not_found' => 'Funkcja społecznościowa nie została odnaleziona', + 'social_driver_not_configured' => 'Ustawienia konta :socialAccount nie są poprawne.', + + // System + 'path_not_writable' => 'Zapis do ścieżki :filePath jest niemożliwy. Upewnij się że aplikacja ma prawa do zapisu w niej.', + 'cannot_get_image_from_url' => 'Nie można pobrać obrazka z :url', + 'cannot_create_thumbs' => 'Serwer nie może utworzyć miniaturek. Upewnij się że rozszerzenie GD PHP zostało zainstalowane.', + 'server_upload_limit' => 'Serwer nie pozwala na przyjęcie pliku o tym rozmiarze. Spróbuj udostępnić coś o mniejszym rozmiarze.', + 'image_upload_error' => 'Wystąpił błąd podczas udostępniania obrazka', + + // Attachments + 'attachment_page_mismatch' => 'Niezgodność stron podczas aktualizacji załącznika', + + // Pages + 'page_draft_autosave_fail' => 'Zapis szkicu nie powiódł się. Upewnij się że posiadasz połączenie z internetem.', + + // Entities + 'entity_not_found' => 'Encja nie została odnaleziona', + 'book_not_found' => 'Księga nie została odnaleziona', + 'page_not_found' => 'Strona nie została odnaleziona', + 'chapter_not_found' => 'Rozdział nie został odnaleziony', + 'selected_book_not_found' => 'Wybrana księga nie została odnaleziona', + 'selected_book_chapter_not_found' => 'Wybrana księga lub rozdział nie zostały odnalezione', + 'guests_cannot_save_drafts' => 'Goście nie mogą zapisywać szkiców', + + // Users + 'users_cannot_delete_only_admin' => 'Nie możesz usunąć jedynego administratora', + 'users_cannot_delete_guest' => 'Nie możesz usunąć użytkownika-gościa', + + // Roles + 'role_cannot_be_edited' => 'Ta rola nie może być edytowana', + 'role_system_cannot_be_deleted' => 'Ta rola jest rolą systemową i nie może zostać usunięta', + 'role_registration_default_cannot_delete' => 'Ta rola nie może zostać usunięta jeśli jest ustawiona jako domyślna rola użytkownika', + + // Error pages + '404_page_not_found' => 'Strona nie została odnaleziona', + 'sorry_page_not_found' => 'Przepraszamy, ale strona której szukasz nie została odnaleziona.', + 'return_home' => 'Powrót do strony głównej', + 'error_occurred' => 'Wystąpił błąd', + 'app_down' => ':appName jest aktualnie wyłączona', + 'back_soon' => 'Niedługo zostanie uruchomiona ponownie.', +]; \ No newline at end of file diff --git a/resources/lang/pl/pagination.php b/resources/lang/pl/pagination.php new file mode 100644 index 000000000..564694190 --- /dev/null +++ b/resources/lang/pl/pagination.php @@ -0,0 +1,19 @@ +<?php + +return [ + + /* + |-------------------------------------------------------------------------- + | Pagination Language Lines + |-------------------------------------------------------------------------- + | + | The following language lines are used by the paginator library to build + | the simple pagination links. You are free to change them to anything + | you want to customize your views to better match your application. + | + */ + + 'previous' => '« Poprzednia', + 'next' => 'Następna »', + +]; diff --git a/resources/lang/pl/passwords.php b/resources/lang/pl/passwords.php new file mode 100644 index 000000000..a9e669f4d --- /dev/null +++ b/resources/lang/pl/passwords.php @@ -0,0 +1,22 @@ +<?php + +return [ + + /* + |-------------------------------------------------------------------------- + | Password Reminder Language Lines + |-------------------------------------------------------------------------- + | + | The following language lines are the default lines which match reasons + | that are given by the password broker for a password update attempt + | has failed, such as for an invalid token or invalid new password. + | + */ + + 'password' => 'Hasło musi zawierać co najmniej 6 znaków i być zgodne z powtórzeniem.', + 'user' => "Nie znaleziono użytkownika o takim adresie email.", + 'token' => 'Ten token resetowania hasła jest nieprawidłowy.', + 'sent' => 'Wysłaliśmy Ci link do resetowania hasła!', + 'reset' => 'Twoje hasło zostało zresetowane!', + +]; diff --git a/resources/lang/pl/settings.php b/resources/lang/pl/settings.php new file mode 100644 index 000000000..381e5517a --- /dev/null +++ b/resources/lang/pl/settings.php @@ -0,0 +1,111 @@ +<?php + +return [ + + /** + * Settings text strings + * Contains all text strings used in the general settings sections of BookStack + * including users and roles. + */ + + 'settings' => 'Ustawienia', + 'settings_save' => 'Zapisz ustawienia', + 'settings_save_success' => 'Ustawienia zapisane', + + /** + * App settings + */ + + 'app_settings' => 'Ustawienia aplikacji', + 'app_name' => 'Nazwa aplikacji', + 'app_name_desc' => 'Ta nazwa jest wyświetlana w nagłówku i emailach.', + 'app_name_header' => 'Pokazać nazwę aplikacji w nagłówku?', + 'app_public_viewing' => 'Zezwolić na publiczne przeglądanie?', + 'app_secure_images' => 'Odblokować wyższe bezpieczeństwo obrazków?', + 'app_secure_images_desc' => 'Ze względów wydajnościowych wszystkie obrazki są publiczne. Ta opcja dodaje dodatkowy, trudny do zgadnienia losowy ciąg na początku nazwy obrazka. Upewnij się że indeksowanie ścieżek jest zablokowane, by uniknąć problemów z dostępem do obrazka.', + 'app_editor' => 'Edytor strony', + 'app_editor_desc' => 'Wybierz edytor używany przez użytkowników do edycji zawartości.', + 'app_custom_html' => 'Własna zawartość tagu <head>', + 'app_custom_html_desc' => 'Zawartość dodana tutaj zostanie dołączona do sekcji <head> każdej strony. Przydatne przy nadpisywaniu styli lub dodawaniu analityki.', + 'app_logo' => 'Logo aplikacji', + 'app_logo_desc' => 'Ten obrazek powinien mieć nie więcej niż 43px w pionie. <br>Większe obrazki będą skalowane w dół.', + 'app_primary_color' => 'Podstawowy kolor aplikacji', + 'app_primary_color_desc' => 'To powinna być wartość HEX. <br>Zostaw to pole puste, by powrócić do podstawowego koloru.', + + /** + * Registration settings + */ + + 'reg_settings' => 'Ustawienia rejestracji', + 'reg_allow' => 'Zezwolić na rejestrację?', + 'reg_default_role' => 'Domyślna rola użytkownika po rejestracji', + 'reg_confirm_email' => 'Wymagać potwierdzenia adresu email?', + 'reg_confirm_email_desc' => 'Jeśli restrykcje domenowe zostały uzupełnione potwierdzenie adresu stanie się konieczne, a poniższa wartośc zostanie zignorowana.', + 'reg_confirm_restrict_domain' => 'Restrykcje domenowe dot. adresu email', + 'reg_confirm_restrict_domain_desc' => 'Wprowadź listę domen adresów email rozdzieloną przecinkami, którym chciałbyś zezwolić na rejestrację. Wymusi to konieczność potwierdzenia adresu email przez użytkownika przed uzyskaniem dostępu do aplikacji. <br> Pamiętaj, że użytkownicy będą mogli zmienić adres email po rejestracji.', + 'reg_confirm_restrict_domain_placeholder' => 'Brak restrykcji', + + /** + * Role settings + */ + + 'roles' => 'Role', + 'role_user_roles' => 'Role użytkownika', + 'role_create' => 'Utwórz nową rolę', + 'role_create_success' => 'Rola utworzona pomyślnie', + 'role_delete' => 'Usuń rolę', + 'role_delete_confirm' => 'To spowoduje usunięcie roli \':roleName\'.', + 'role_delete_users_assigned' => 'Tę rolę ma przypisanych :userCount użytkowników. Jeśli chcesz zmigrować użytkowników z tej roli, wybierz nową poniżej.', + 'role_delete_no_migration' => "Nie migruj użytkowników", + 'role_delete_sure' => 'Czy na pewno chcesz usunąć tę rolę?', + 'role_delete_success' => 'Rola usunięta pomyślnie', + 'role_edit' => 'Edytuj rolę', + 'role_details' => 'Szczegóły roli', + 'role_name' => 'Nazwa roli', + 'role_desc' => 'Krótki opis roli', + 'role_system' => 'Uprawnienia systemowe', + 'role_manage_users' => 'Zarządzanie użytkownikami', + 'role_manage_roles' => 'Zarządzanie rolami i uprawnieniami ról', + 'role_manage_entity_permissions' => 'Zarządzanie uprawnieniami ksiąg, rozdziałów i stron', + 'role_manage_own_entity_permissions' => 'Zarządzanie uprawnieniami własnych ksiąg, rozdziałów i stron', + 'role_manage_settings' => 'Zarządzanie ustawieniami aplikacji', + 'role_asset' => 'Zarządzanie zasobami', + 'role_asset_desc' => 'Te ustawienia kontrolują zarządzanie zasobami systemu. Uprawnienia ksiąg, rozdziałów i stron nadpisują te ustawienia.', + 'role_all' => 'Wszyscy', + 'role_own' => 'Własne', + 'role_controlled_by_asset' => 'Kontrolowane przez zasób, do którego zostały udostępnione', + 'role_save' => 'Zapisz rolę', + 'role_update_success' => 'Rola zapisana pomyślnie', + 'role_users' => 'Użytkownicy w tej roli', + 'role_users_none' => 'Brak użytkowników zapisanych do tej roli', + + /** + * Users + */ + + 'users' => 'Użytkownicy', + 'user_profile' => 'Profil użytkownika', + 'users_add_new' => 'Dodaj użytkownika', + 'users_search' => 'Wyszukaj użytkownika', + 'users_role' => 'Role użytkownika', + 'users_external_auth_id' => 'Zewnętrzne ID autentykacji', + 'users_password_warning' => 'Wypełnij poniżej tylko jeśli chcesz zmienić swoje hasło:', + 'users_system_public' => 'Ten użytkownik reprezentuje każdego gościa odwiedzającego tę aplikację. Nie można się na niego zalogować, lecz jest przyznawany automatycznie.', + 'users_delete' => 'Usuń użytkownika', + 'users_delete_named' => 'Usuń :userName', + 'users_delete_warning' => 'To usunie użytkownika \':userName\' z systemu.', + 'users_delete_confirm' => 'Czy na pewno chcesz usunąć tego użytkownika?', + 'users_delete_success' => 'Użytkownik usunięty pomyślnie', + 'users_edit' => 'Edytuj użytkownika', + 'users_edit_profile' => 'Edytuj profil', + 'users_edit_success' => 'Użytkownik zaktualizowany pomyśłnie', + 'users_avatar' => 'Avatar użytkownika', + 'users_avatar_desc' => 'Ten obrazek powinien mieć 25px x 256px.', + 'users_preferred_language' => 'Preferowany język', + 'users_social_accounts' => 'Konta społecznościowe', + 'users_social_accounts_info' => 'Tutaj możesz połączyć kilka kont społecznościowych w celu łatwiejszego i szybszego logowania.', + 'users_social_connect' => 'Podłącz konto', + 'users_social_disconnect' => 'Odłącz konto', + 'users_social_connected' => ':socialAccount zostało dodane do Twojego profilu.', + 'users_social_disconnected' => ':socialAccount zostało odłączone od Twojego profilu.', +]; diff --git a/resources/lang/pl/validation.php b/resources/lang/pl/validation.php new file mode 100644 index 000000000..6a7c13e80 --- /dev/null +++ b/resources/lang/pl/validation.php @@ -0,0 +1,108 @@ +<?php + +return [ + + /* + |-------------------------------------------------------------------------- + | Validation Language Lines + |-------------------------------------------------------------------------- + | + | The following language lines contain the default error messages used by + | the validator class. Some of these rules have multiple versions such + | as the size rules. Feel free to tweak each of these messages here. + | + */ + + 'accepted' => ':attribute musi zostać zaakceptowany.', + 'active_url' => ':attribute nie jest prawidłowym adresem URL.', + 'after' => ':attribute musi być datą następującą po :date.', + 'alpha' => ':attribute może zawierać wyłącznie litery.', + 'alpha_dash' => ':attribute może zawierać wyłącznie litery, cyfry i myślniki.', + 'alpha_num' => ':attribute może zawierać wyłącznie litery i cyfry.', + 'array' => ':attribute musi być tablicą.', + 'before' => ':attribute musi być datą poprzedzającą :date.', + 'between' => [ + 'numeric' => ':attribute musi zawierać się w przedziale od :min do :max.', + 'file' => 'Waga :attribute musi zawierać się pomiędzy :min i :max kilobajtów.', + 'string' => 'Długość :attribute musi zawierać się pomiędzy :min i :max.', + 'array' => ':attribute musi mieć od :min do :max elementów.', + ], + 'boolean' => ':attribute musi być wartością prawda/fałsz.', + 'confirmed' => ':attribute i potwierdzenie muszą być zgodne.', + 'date' => ':attribute nie jest prawidłową datą.', + 'date_format' => ':attribute musi mieć format :format.', + 'different' => ':attribute i :other muszą się różnić.', + 'digits' => ':attribute musi mieć :digits cyfr.', + 'digits_between' => ':attribute musi mieć od :min do :max cyfr.', + 'email' => ':attribute musi być prawidłowym adresem e-mail.', + 'filled' => ':attribute jest wymagany.', + 'exists' => 'Wybrana wartość :attribute jest nieprawidłowa.', + 'image' => ':attribute musi być obrazkiem.', + 'in' => 'Wybrana wartość :attribute jest nieprawidłowa.', + 'integer' => ':attribute musi być liczbą całkowitą.', + 'ip' => ':attribute musi być prawidłowym adresem IP.', + 'max' => [ + 'numeric' => 'Wartość :attribute nie może być większa niż :max.', + 'file' => 'Wielkość :attribute nie może być większa niż :max kilobajtów.', + 'string' => 'Długość :attribute nie może być większa niż :max znaków.', + 'array' => 'Rozmiar :attribute nie może być większy niż :max elementów.', + ], + 'mimes' => ':attribute musi być plikiem typu: :values.', + 'min' => [ + 'numeric' => 'Wartość :attribute nie może być mniejsza od :min.', + 'file' => 'Wielkość :attribute nie może być mniejsza niż :min kilobajtów.', + 'string' => 'Długość :attribute nie może być mniejsza niż :min znaków.', + 'array' => 'Rozmiar :attribute musi posiadać co najmniej :min elementy.', + ], + 'not_in' => 'Wartość :attribute jest nieprawidłowa.', + 'numeric' => ':attribute musi być liczbą.', + 'regex' => 'Format :attribute jest nieprawidłowy.', + 'required' => 'Pole :attribute jest wymagane.', + 'required_if' => 'Pole :attribute jest wymagane jeśli :other ma wartość :value.', + 'required_with' => 'Pole :attribute jest wymagane jeśli :values zostało wprowadzone.', + 'required_with_all' => 'Pole :attribute jest wymagane jeśli :values są obecne.', + 'required_without' => 'Pole :attribute jest wymagane jeśli :values nie zostało wprowadzone.', + 'required_without_all' => 'Pole :attribute jest wymagane jeśli żadna z wartości :values nie została podana.', + 'same' => 'Pole :attribute i :other muszą być takie same.', + 'size' => [ + 'numeric' => ':attribute musi mieć długość :size.', + 'file' => ':attribute musi mieć :size kilobajtów.', + 'string' => ':attribute mmusi mieć długość :size znaków.', + 'array' => ':attribute musi posiadać :size elementów.', + ], + 'string' => ':attribute musi być ciągiem znaków.', + 'timezone' => ':attribute musi być prawidłową strefą czasową.', + 'unique' => ':attribute zostało już zajęte.', + 'url' => 'Format :attribute jest nieprawidłowy.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'password-confirm' => [ + 'required_with' => 'Potwierdzenie hasła jest wymagane.', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ + + 'attributes' => [], + +]; 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 @@ <header id="header"> <div class="container"> <div class="row"> - <div class="col-lg-4 col-sm-4" ng-non-bindable> + <div class="col-sm-4" ng-non-bindable> <a href="{{ baseUrl('/') }}" class="logo"> @if(setting('app-logo', '') !== 'none') <img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo"> @@ -46,14 +46,14 @@ @endif </a> </div> - <div class="col-lg-4 col-sm-3 text-center"> - <form action="{{ baseUrl('/search') }}" method="GET" class="search-box"> - <input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}"> - <button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button> - </form> - </div> - <div class="col-lg-4 col-sm-5"> + <div class="col-sm-8"> <div class="float right"> + <div class="header-search"> + <form action="{{ baseUrl('/search') }}" method="GET" class="search-box"> + <button id="header-search-box-button" type="submit"><i class="zmdi zmdi-search"></i> </button> + <input id="header-search-box-input" type="text" name="term" tabindex="2" placeholder="{{ trans('common.search') }}" value="{{ isset($searchTerm) ? $searchTerm : '' }}"> + </form> + </div> <div class="links text-center"> <a href="{{ baseUrl('/books') }}"><i class="zmdi zmdi-book"></i>{{ trans('entities.books') }}</a> @if(signedInUser() && userCan('settings-manage')) @@ -77,7 +77,7 @@ @yield('content') </section> - <div id="back-to-top"> + <div back-to-top> <div class="inner"> <i class="zmdi zmdi-chevron-up"></i> <span>{{ trans('common.back_to_top') }}</span> </div> diff --git a/resources/views/books/list-item.blade.php b/resources/views/books/list-item.blade.php index 605841f7f..92d0f9e2d 100644 --- a/resources/views/books/list-item.blade.php +++ b/resources/views/books/list-item.blade.php @@ -1,8 +1,10 @@ <div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}"> <h4 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i><span class="entity-list-item-name">{{$book->name}}</span></a></h4> - @if(isset($book->searchSnippet)) - <p class="text-muted">{!! $book->searchSnippet !!}</p> - @else - <p class="text-muted">{{ $book->getExcerpt() }}</p> - @endif + <div class="entity-item-snippet"> + @if(isset($book->searchSnippet)) + <p class="text-muted">{!! $book->searchSnippet !!}</p> + @else + <p class="text-muted">{{ $book->getExcerpt() }}</p> + @endif + </div> </div> \ No newline at end of file diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index ddbe7a0a4..97942ee67 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -5,10 +5,10 @@ <div class="faded-small toolbar"> <div class="container"> <div class="row"> - <div class="col-sm-6 faded"> + <div class="col-sm-6 col-xs-1 faded"> @include('books._breadcrumbs', ['book' => $book]) </div> - <div class="col-sm-6"> + <div class="col-sm-6 col-xs-11"> <div class="action-buttons faded"> <span dropdown class="dropdown-container"> <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>{{ trans('entities.export') }}</div> @@ -50,13 +50,13 @@ </div> - <div class="container" id="entity-dashboard" entity-id="{{ $book->id }}" entity-type="book"> + <div ng-non-bindable class="container" id="entity-dashboard" entity-id="{{ $book->id }}" entity-type="book"> <div class="row"> <div class="col-md-7"> <h1>{{$book->name}}</h1> - <div class="book-content" v-if="!searching"> - <p class="text-muted" v-pre>{{$book->description}}</p> + <div class="book-content" v-show="!searching"> + <p class="text-muted" v-pre>{!! nl2br(e($book->description)) !!}</p> <div class="page-list" v-pre> <hr> @@ -87,7 +87,7 @@ @include('partials.entity-meta', ['entity' => $book]) </div> </div> - <div class="search-results" v-cloak v-if="searching"> + <div class="search-results" v-cloak v-show="searching"> <h3 class="text-muted">{{ trans('entities.search_results') }} <a v-if="searching" v-on:click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3> <div v-if="!searchResults"> @include('partials/loading-icon') @@ -111,14 +111,12 @@ </p> @endif - <div class="search-box"> - <form v-on:submit="searchBook"> - <input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}"> - <button type="submit"><i class="zmdi zmdi-search"></i></button> - <button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button> - </form> - </div> - + <form v-on:submit.prevent="searchBook" class="search-box"> + <input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}"> + <button type="submit"><i class="zmdi zmdi-search"></i></button> + <button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button> + </form> + <div class="activity"> <h3>{{ trans('entities.recent_activity') }}</h3> @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)]) @@ -127,4 +125,4 @@ </div> </div> -@stop \ No newline at end of file +@stop diff --git a/resources/views/chapters/list-item.blade.php b/resources/views/chapters/list-item.blade.php index 8487a63a3..1572f0d9b 100644 --- a/resources/views/chapters/list-item.blade.php +++ b/resources/views/chapters/list-item.blade.php @@ -2,7 +2,7 @@ <h4> @if (isset($showPath) && $showPath) <a href="{{ $chapter->book->getUrl() }}" class="text-book"> - <i class="zmdi zmdi-book"></i>{{ $chapter->book->name }} + <i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }} </a> <span class="text-muted"> » </span> @endif @@ -10,14 +10,18 @@ <i class="zmdi zmdi-collection-bookmark"></i><span class="entity-list-item-name">{{ $chapter->name }}</span> </a> </h4> - @if(isset($chapter->searchSnippet)) - <p class="text-muted">{!! $chapter->searchSnippet !!}</p> - @else - <p class="text-muted">{{ $chapter->getExcerpt() }}</p> - @endif + + <div class="entity-item-snippet"> + @if(isset($chapter->searchSnippet)) + <p class="text-muted">{!! $chapter->searchSnippet !!}</p> + @else + <p class="text-muted">{{ $chapter->getExcerpt() }}</p> + @endif + </div> + @if(!isset($hidePages) && count($chapter->pages) > 0) - <p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans('entities.x_pages', ['count' => $chapter->pages->count()]) }}</span></p> + <p chapter-toggle class="text-muted"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans('entities.x_pages', ['count' => $chapter->pages->count()]) }}</span></p> <div class="inset-list"> @foreach($chapter->pages as $page) <h5 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h5> diff --git a/resources/views/chapters/show.blade.php b/resources/views/chapters/show.blade.php index d4126cbcc..62a7eaa74 100644 --- a/resources/views/chapters/show.blade.php +++ b/resources/views/chapters/show.blade.php @@ -5,10 +5,10 @@ <div class="faded-small toolbar"> <div class="container"> <div class="row"> - <div class="col-sm-8 faded" ng-non-bindable> + <div class="col-sm-6 col-xs-3 faded" ng-non-bindable> @include('chapters._breadcrumbs', ['chapter' => $chapter]) </div> - <div class="col-sm-4 faded"> + <div class="col-sm-6 col-xs-9 faded"> <div class="action-buttons"> <span dropdown class="dropdown-container"> <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>{{ trans('entities.export') }}</div> @@ -47,12 +47,12 @@ </div> - <div class="container" id="entity-dashboard" entity-id="{{ $chapter->id }}" entity-type="chapter"> + <div class="container" id="entity-dashboard" ng-non-bindable entity-id="{{ $chapter->id }}" entity-type="chapter"> <div class="row"> <div class="col-md-7"> <h1>{{ $chapter->name }}</h1> - <div class="chapter-content" v-if="!searching"> - <p class="text-muted">{{ $chapter->description }}</p> + <div class="chapter-content" v-show="!searching"> + <p class="text-muted">{!! nl2br(e($chapter->description)) !!}</p> @if(count($pages) > 0) <div class="page-list"> @@ -82,7 +82,7 @@ @include('partials.entity-meta', ['entity' => $chapter]) </div> - <div class="search-results" v-cloak v-if="searching"> + <div class="search-results" v-cloak v-show="searching"> <h3 class="text-muted">{{ trans('entities.search_results') }} <a v-if="searching" v-on:click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3> <div v-if="!searchResults"> @include('partials/loading-icon') @@ -115,13 +115,11 @@ </div> @endif - <div class="search-box"> - <form v-on:submit="searchBook"> - <input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.chapters_search_this') }}"> - <button type="submit"><i class="zmdi zmdi-search"></i></button> - <button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button> - </form> - </div> + <form v-on:submit.prevent="searchBook" class="search-box"> + <input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.chapters_search_this') }}"> + <button type="submit"><i class="zmdi zmdi-search"></i></button> + <button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button> + </form> @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) diff --git a/resources/views/comments/comment-reply.blade.php b/resources/views/comments/comment-reply.blade.php new file mode 100644 index 000000000..02535341c --- /dev/null +++ b/resources/views/comments/comment-reply.blade.php @@ -0,0 +1,12 @@ +<div class="comment-editor" ng-controller="CommentReplyController as vm" ng-cloak> + <form novalidate> + <textarea name="markdown" rows="3" ng-model="comment.text" placeholder="{{ trans('entities.comment_placeholder') }}"></textarea> + <input type="hidden" ng-model="comment.pageId" name="comment.pageId" value="{{$pageId}}" ng-init="comment.pageId = {{$pageId }}"> + <button type="button" ng-if="::(isReply || isEdit)" class="button muted" ng-click="closeBox()">{{ trans('entities.comment_cancel') }}</button> + <button type="submit" class="button pos" ng-click="vm.saveComment()">{{ trans('entities.comment_save') }}</button> + </form> +</div> + +@if($errors->has('markdown')) + <div class="text-neg text-small">{{ $errors->first('markdown') }}</div> +@endif \ No newline at end of file diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php new file mode 100644 index 000000000..ffa75cfed --- /dev/null +++ b/resources/views/comments/comments.blade.php @@ -0,0 +1,18 @@ +<script type="text/ng-template" id="comment-list-item.html"> + @include('comments/list-item') +</script> +<script type="text/ng-template" id="comment-reply.html"> + @include('comments/comment-reply', ['pageId' => $pageId]) +</script> +<div ng-controller="CommentListController as vm" ng-init="pageId = <?= $page->id ?>" class="comments-list" ng-cloak> +<h3>@{{vm.totalCommentsStr}}</h3> +<hr> + <div class="comment-box" ng-repeat="comment in vm.comments track by comment.id"> + <div ng-include src="'comment-list-item.html'"> + + </div> + </div> + <div ng-if="::vm.canComment()"> + @include('comments/comment-reply', ['pageId' => $pageId]) + </div> +</div> \ No newline at end of file diff --git a/resources/views/comments/list-item.blade.php b/resources/views/comments/list-item.blade.php new file mode 100644 index 000000000..f274d2ed2 --- /dev/null +++ b/resources/views/comments/list-item.blade.php @@ -0,0 +1,30 @@ +<div class='page-comment' id="comment-@{{::pageId}}-@{{::comment.id}}"> + <div class="user-image"> + <img ng-src="@{{::comment.created_by.avatar_url}}" alt="user avatar"> + </div> + <div class="comment-container"> + <div class="comment-header"> + <a href="@{{::comment.created_by.profile_url}}">@{{ ::comment.created_by.name }}</a> + </div> + <div ng-bind-html="comment.html" ng-if="::comment.active" class="comment-body" ng-class="!comment.active ? 'comment-inactive' : ''"> + + </div> + <div ng-if="::!comment.active" class="comment-body comment-inactive"> + {{ trans('entities.comment_deleted') }} + </div> + <div class="comment-actions"> + <ul ng-if="!comment.is_hidden"> + <li ng-if="::(level < 3 && vm.canComment())"><a href="#" comment-reply-link no-comment-reply-dupe="true" comment="comment" is-reply="true">{{ trans('entities.comment_reply') }}</a></li> + <li ng-if="::vm.canEditDelete(comment, 'comment_update')"><a href="#" comment-reply-link no-comment-reply-dupe="true" comment="comment" >{{ trans('entities.comment_edit') }}</a></li> + <li ng-if="::vm.canEditDelete(comment, 'comment_delete')"><a href="#" comment-delete-link comment="comment" >{{ trans('entities.comment_delete') }}</a></li> + <li>{{ trans('entities.comment_create') }} <a title="@{{::comment.created.day_time_str}}" href="#?cm=comment-@{{::pageId}}-@{{::comment.id}}">@{{::comment.created.diff}}</a></li> + <li ng-if="::comment.updated"><span title="@{{::comment.updated.day_time_str}}">@{{ ::vm.trans('entities.comment_updated_text', { updateDiff: comment.updated.diff }) }} + <a href="@{{::comment.updated_by.profile_url}}">@{{::comment.updated_by.name}}</a></span></li> + </ul> + </div> + <div class="comment-box" ng-repeat="comment in comments = comment.sub_comments track by comment.id" ng-init="level = level + 1"> + <div ng-include src="'comment-list-item.html'"> + </div> + </div> + </div> +</div> \ No newline at end of file diff --git a/resources/views/components/code-editor.blade.php b/resources/views/components/code-editor.blade.php index 5a385ef49..5788bd7f7 100644 --- a/resources/views/components/code-editor.blade.php +++ b/resources/views/components/code-editor.blade.php @@ -1,10 +1,10 @@ <div id="code-editor"> - <div class="overlay" ref="overlay" v-cloak @click="hide()"> + <div overlay ref="overlay" v-cloak @click="hide()"> <div class="popup-body" @click.stop> <div class="popup-header primary-background"> <div class="popup-title">{{ trans('components.code_editor') }}</div> - <button class="popup-close neg corner-button button" @click="hide()">x</button> + <button class="overlay-close neg corner-button button" @click="hide()">x</button> </div> <div class="padded"> diff --git a/resources/views/components/entity-selector-popup.blade.php b/resources/views/components/entity-selector-popup.blade.php index 1c4d1fadb..39d25bfa6 100644 --- a/resources/views/components/entity-selector-popup.blade.php +++ b/resources/views/components/entity-selector-popup.blade.php @@ -1,9 +1,9 @@ <div id="entity-selector-wrap"> - <div class="overlay" entity-link-selector> + <div overlay entity-link-selector> <div class="popup-body small flex-child"> <div class="popup-header primary-background"> <div class="popup-title">{{ trans('entities.entity_select') }}</div> - <button type="button" class="corner-button neg button popup-close">x</button> + <button type="button" class="corner-button neg button overlay-close">x</button> </div> @include('components.entity-selector', ['name' => 'entity-selector']) <div class="popup-footer"> diff --git a/resources/views/components/image-manager.blade.php b/resources/views/components/image-manager.blade.php index 39f3bcd3c..a4612a4ac 100644 --- a/resources/views/components/image-manager.blade.php +++ b/resources/views/components/image-manager.blade.php @@ -1,91 +1,89 @@ -<div id="image-manager" image-type="{{ $imageType }}" ng-controller="ImageManagerController" uploaded-to="{{ $uploaded_to or 0 }}"> - <div class="overlay" ng-cloak ng-click="hide()"> - <div class="popup-body" ng-click="$event.stopPropagation()"> +<div id="image-manager" image-type="{{ $imageType }}" uploaded-to="{{ $uploaded_to or 0 }}"> + <div overlay v-cloak> + <div class="popup-body" @click.stop=""> <div class="popup-header primary-background"> <div class="popup-title">{{ trans('components.image_select') }}</div> - <button class="popup-close neg corner-button button">x</button> + <button class="overlay-close neg corner-button button">x</button> </div> <div class="flex-fill image-manager-body"> <div class="image-manager-content"> - <div ng-if="imageType === 'gallery'" class="container"> + <div v-if="imageType === 'gallery'" class="container"> <div class="image-manager-header row faded-small nav-tabs"> - <div class="col-xs-4 tab-item" title="{{ trans('components.image_all_title') }}" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> {{ trans('components.image_all') }}</div> - <div class="col-xs-4 tab-item" title="{{ trans('components.image_book_title') }}" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> {{ trans('entities.book') }}</div> - <div class="col-xs-4 tab-item" title="{{ trans('components.image_page_title') }}" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> {{ trans('entities.page') }}</div> + <div class="col-xs-4 tab-item" title="{{ trans('components.image_all_title') }}" :class="{selected: (view=='all')}" @click="setView('all')"><i class="zmdi zmdi-collection-image"></i> {{ trans('components.image_all') }}</div> + <div class="col-xs-4 tab-item" title="{{ trans('components.image_book_title') }}" :class="{selected: (view=='book')}" @click="setView('book')"><i class="zmdi zmdi-book text-book"></i> {{ trans('entities.book') }}</div> + <div class="col-xs-4 tab-item" title="{{ trans('components.image_page_title') }}" :class="{selected: (view=='page')}" @click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> {{ trans('entities.page') }}</div> </div> </div> - <div ng-show="view === 'all'" > - <form ng-submit="searchImages()" class="contained-search-box"> - <input type="text" placeholder="{{ trans('components.image_search_hint') }}" ng-model="searchTerm"> - <button ng-class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button> - <button title="{{ trans('common.search') }}" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button> + <div v-show="view === 'all'" > + <form @submit="searchImages" class="contained-search-box"> + <input placeholder="{{ trans('components.image_search_hint') }}" v-model="searchTerm"> + <button :class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" @click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button> + <button title="{{ trans('common.search') }}" class="text-button"><i class="zmdi zmdi-search"></i></button> </form> </div> <div class="image-manager-list"> - <div ng-repeat="image in images"> - <div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}" - ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)"> - <img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}"> + <div v-if="images.length > 0" v-for="(image, idx) in images"> + <div class="image anim fadeIn" :style="{animationDelay: (idx > 26) ? '160ms' : ((idx * 25) + 'ms')}" + :class="{selected: (image==selectedImage)}" @click="imageSelect(image)"> + <img :src="image.thumbs.gallery" :alt="image.title" :title="image.name"> <div class="image-meta"> - <span class="name" ng-bind="image.name"></span> + <span class="name" v-text="image.name"></span> <span class="date">{{ trans('components.image_uploaded', ['uploadedDate' => "{{ getDate(image.created_at) }" . "}"]) }}</span> </div> </div> </div> - <div class="load-more" ng-show="hasMore" ng-click="fetchData()">{{ trans('components.image_load_more') }}</div> + <div class="load-more" v-show="hasMore" @click="fetchData">{{ trans('components.image_load_more') }}</div> </div> </div> <div class="image-manager-sidebar"> <div class="inner"> - <div class="image-manager-details anim fadeIn" ng-show="selectedImage"> + <div class="image-manager-details anim fadeIn" v-if="selectedImage"> - <form ng-submit="saveImageDetails($event)"> + <form @submit.prevent="saveImageDetails"> <div> - <a ng-href="@{{selectedImage.url}}" target="_blank" style="display: block;"> - <img ng-src="@{{selectedImage.thumbs.gallery}}" ng-attr-alt="@{{selectedImage.title}}" ng-attr-title="@{{selectedImage.name}}"> + <a :href="selectedImage.url" target="_blank" style="display: block;"> + <img :src="selectedImage.thumbs.gallery" :alt="selectedImage.title" + :title="selectedImage.name"> </a> </div> <div class="form-group"> <label for="name">{{ trans('components.image_image_name') }}</label> - <input type="text" id="name" name="name" ng-model="selectedImage.name"> + <input id="name" name="name" v-model="selectedImage.name"> </div> </form> - <div ng-show="dependantPages"> + <div v-show="dependantPages"> <p class="text-neg text-small"> {{ trans('components.image_delete_confirm') }} </p> <ul class="text-neg"> - <li ng-repeat="page in dependantPages"> - <a ng-href="@{{ page.url }}" target="_blank" class="text-neg" ng-bind="page.name"></a> + <li v-for="page in dependantPages"> + <a :href="page.url" target="_blank" class="text-neg" v-text="page.name"></a> </li> </ul> </div> <div class="clearfix"> - <form class="float left" ng-submit="deleteImage($event)"> + <form class="float left" @submit.prevent="deleteImage"> <button class="button icon neg"><i class="zmdi zmdi-delete"></i></button> </form> - <button class="button pos anim fadeIn float right" ng-show="selectedImage" ng-click="selectButtonClick()"> + <button class="button pos anim fadeIn float right" v-show="selectedImage" @click="callbackAndHide(selectedImage)"> <i class="zmdi zmdi-square-right"></i>{{ trans('components.image_select_image') }} </button> </div> </div> - <drop-zone message="{{ trans('components.image_dropzone') }}" upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone> - + <dropzone placeholder="{{ trans('components.image_dropzone') }}" :upload-url="uploadUrl" :uploaded-to="uploadedTo" @success="uploadSuccess"></dropzone> </div> </div> - - </div> </div> diff --git a/resources/views/components/image-picker.blade.php b/resources/views/components/image-picker.blade.php index 6fd81dabd..2aa39d3d2 100644 --- a/resources/views/components/image-picker.blade.php +++ b/resources/views/components/image-picker.blade.php @@ -43,7 +43,7 @@ } if (action === 'show-image-manager') { - window.ImageManager.showExternal((image) => { + window.ImageManager.show((image) => { if (!resize) { setImage(image); return; diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 49cd2a75a..87c84ec1e 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -7,7 +7,7 @@ <div class="row"> <div class="col-sm-6 faded"> <div class="action-buttons text-left"> - <a data-action="expand-entity-list-details" class="text-primary text-button"><i class="zmdi zmdi-wrap-text"></i>{{ trans('common.toggle_details') }}</a> + <a expand-toggle=".entity-list.compact .entity-item-snippet" class="text-primary text-button"><i class="zmdi zmdi-wrap-text"></i>{{ trans('common.toggle_details') }}</a> </div> </div> </div> diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php index ecf7619b7..bd60af89a 100644 --- a/resources/views/pages/form-toolbox.blade.php +++ b/resources/views/pages/form-toolbox.blade.php @@ -9,122 +9,131 @@ @endif </div> - <div toolbox-tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}"> + <div toolbox-tab-content="tags" id="tag-manager" page-id="{{ $page->id or 0 }}"> <h4>{{ trans('entities.page_tags') }}</h4> <div class="padded tags"> <p class="muted small">{!! nl2br(e(trans('entities.tags_explain'))) !!}</p> - <table class="no-style" tag-autosuggestions style="width: 100%;"> - <tbody ui-sortable="sortOptions" ng-model="tags" > - <tr ng-repeat="tag in tags track by $index"> - <td width="20" ><i class="handle zmdi zmdi-menu"></i></td> - <td><input autosuggest="{{ baseUrl('/ajax/tags/suggest/names') }}" autosuggest-type="name" class="outline" ng-attr-name="tags[@{{$index}}][name]" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="{{ trans('entities.tag') }}"></td> - <td><input autosuggest="{{ baseUrl('/ajax/tags/suggest/values') }}" autosuggest-type="value" class="outline" ng-attr-name="tags[@{{$index}}][value]" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="{{ trans('entities.tag_value') }}"></td> - <td width="10" ng-show="tags.length != 1" class="text-center text-neg" style="padding: 0;" ng-click="removeTag(tag)"><i class="zmdi zmdi-close"></i></td> - </tr> - </tbody> - </table> + + <draggable class="fake-table no-style tag-table" :options="{handle: '.handle'}" :list="tags" element="div"> + <transition-group tag="div"> + <div v-for="(tag, i) in tags" :key="tag.key"> + <div width="20" class="handle" ><i class="zmdi zmdi-menu"></i></div> + <div> + <autosuggest url="/ajax/tags/suggest/names" type="name" class="outline" :name="getTagFieldName(i, 'name')" + v-model="tag.name" @input="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag') }}"/> + </div> + <div> + <autosuggest url="/ajax/tags/suggest/values" type="value" class="outline" :name="getTagFieldName(i, 'value')" + v-model="tag.value" @change="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag') }}"/> + </div> + <div width="10" v-show="tags.length !== 1" class="text-center text-neg" style="padding: 0;" @click="removeTag(tag)"><i class="zmdi zmdi-close"></i></div> + </div> + </transition-group> + </draggable> + <table class="no-style" style="width: 100%;"> <tbody> <tr class="unsortable"> - <td width="34"></td> - <td ng-click="addEmptyTag()"> + <td width="34"></td> + <td @click="addEmptyTag"> <button type="button" class="text-button">{{ trans('entities.tags_add') }}</button> </td> <td></td> </tr> </tbody> </table> + </div> </div> @if(userCan('attachment-create-all')) - <div toolbox-tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}"> + <div toolbox-tab-content="files" id="attachment-manager" page-id="{{ $page->id or 0 }}"> <h4>{{ trans('entities.attachments') }}</h4> <div class="padded files"> - <div id="file-list" ng-show="!editFile"> + <div id="file-list" v-show="!fileToEdit"> <p class="muted small">{{ trans('entities.attachments_explain') }} <span class="secondary">{{ trans('entities.attachments_explain_instant_save') }}</span></p> - <div tab-container> + <div class="tab-container"> <div class="nav-tabs"> - <div tab-button="list" class="tab-item">{{ trans('entities.attachments_items') }}</div> - <div tab-button="file" class="tab-item">{{ trans('entities.attachments_upload') }}</div> - <div tab-button="link" class="tab-item">{{ trans('entities.attachments_link') }}</div> + <div @click="tab = 'list'" :class="{selected: tab === 'list'}" class="tab-item">{{ trans('entities.attachments_items') }}</div> + <div @click="tab = 'file'" :class="{selected: tab === 'file'}" class="tab-item">{{ trans('entities.attachments_upload') }}</div> + <div @click="tab = 'link'" :class="{selected: tab === 'link'}" class="tab-item">{{ trans('entities.attachments_link') }}</div> </div> - <div tab-content="list"> - <table class="file-table" style="width: 100%;"> - <tbody ui-sortable="sortOptions" ng-model="files" > - <tr ng-repeat="file in files track by $index"> - <td width="20" ><i class="handle zmdi zmdi-menu"></i></td> - <td> - <a ng-href="@{{getFileUrl(file)}}" target="_blank" ng-bind="file.name"></a> - <div ng-if="file.deleting"> + <div v-show="tab === 'list'"> + <draggable class="fake-table no-style " style="width: 100%;" :options="{handle: '.handle'}" @change="fileSortUpdate" :list="files" element="div"> + <transition-group tag="div"> + <div v-for="(file, index) in files" :key="file.id"> + <div width="20" ><i class="handle zmdi zmdi-menu"></i></div> + <div> + <a :href="getFileUrl(file)" target="_blank" v-text="file.name"></a> + <div v-if="file.deleting"> <span class="neg small">{{ trans('entities.attachments_delete_confirm') }}</span> <br> - <span class="text-primary small" ng-click="file.deleting=false;">{{ trans('common.cancel') }}</span> + <span class="text-primary small" @click="file.deleting = false;">{{ trans('common.cancel') }}</span> </div> - </td> - <td width="10" ng-click="startEdit(file)" class="text-center text-primary" style="padding: 0;"><i class="zmdi zmdi-edit"></i></td> - <td width="5"></td> - <td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td> - </tr> - </tbody> - </table> - <p class="small muted" ng-if="files.length == 0"> + </div> + <div width="10" @click="startEdit(file)" class="text-center text-primary" style="padding: 0;"><i class="zmdi zmdi-edit"></i></div> + <div width="5"></div> + <div width="10" @click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></div> + </div> + </transition-group> + </draggable> + <p class="small muted" v-if="files.length === 0"> {{ trans('entities.attachments_no_files') }} </p> </div> - <div tab-content="file"> - <drop-zone message="{{ trans('entities.attachments_dropzone') }}" upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone> + <div v-show="tab === 'file'"> + <dropzone placeholder="{{ trans('entities.attachments_dropzone') }}" :upload-url="getUploadUrl()" :uploaded-to="pageId" @success="uploadSuccess"></dropzone> </div> - <div tab-content="link" sub-form="attachLinkSubmit(file)"> + <div v-show="tab === 'link'" @keypress.enter.prevent="attachNewLink(file)"> <p class="muted small">{{ trans('entities.attachments_explain_link') }}</p> <div class="form-group"> <label for="attachment-via-link">{{ trans('entities.attachments_link_name') }}</label> - <input type="text" placeholder="{{ trans('entities.attachments_link_name') }}" ng-model="file.name"> - <p class="small neg" ng-repeat="error in errors.link.name" ng-bind="error"></p> + <input type="text" placeholder="{{ trans('entities.attachments_link_name') }}" v-model="file.name"> + <p class="small neg" v-for="error in errors.link.name" v-text="error"></p> </div> <div class="form-group"> <label for="attachment-via-link">{{ trans('entities.attachments_link_url') }}</label> - <input type="text" placeholder="{{ trans('entities.attachments_link_url_hint') }}" ng-model="file.link"> - <p class="small neg" ng-repeat="error in errors.link.link" ng-bind="error"></p> + <input type="text" placeholder="{{ trans('entities.attachments_link_url_hint') }}" v-model="file.link"> + <p class="small neg" v-for="error in errors.link.link" v-text="error"></p> </div> - <button type="submit" class="button pos">{{ trans('entities.attach') }}</button> + <button @click.prevent="attachNewLink(file)" class="button pos">{{ trans('entities.attach') }}</button> </div> </div> </div> - <div id="file-edit" ng-if="editFile" sub-form="updateFile(editFile)"> + <div id="file-edit" v-if="fileToEdit" @keypress.enter.prevent="updateFile(fileToEdit)"> <h5>{{ trans('entities.attachments_edit_file') }}</h5> <div class="form-group"> <label for="attachment-name-edit">{{ trans('entities.attachments_edit_file_name') }}</label> - <input type="text" id="attachment-name-edit" placeholder="{{ trans('entities.attachments_edit_file_name') }}" ng-model="editFile.name"> - <p class="small neg" ng-repeat="error in errors.edit.name" ng-bind="error"></p> + <input type="text" id="attachment-name-edit" placeholder="{{ trans('entities.attachments_edit_file_name') }}" v-model="fileToEdit.name"> + <p class="small neg" v-for="error in errors.edit.name" v-text="error"></p> </div> - <div tab-container="@{{ editFile.external ? 'link' : 'file' }}"> + <div class="tab-container"> <div class="nav-tabs"> - <div tab-button="file" class="tab-item">{{ trans('entities.attachments_upload') }}</div> - <div tab-button="link" class="tab-item">{{ trans('entities.attachments_set_link') }}</div> + <div @click="editTab = 'file'" :class="{selected: editTab === 'file'}" class="tab-item">{{ trans('entities.attachments_upload') }}</div> + <div @click="editTab = 'link'" :class="{selected: editTab === 'link'}" class="tab-item">{{ trans('entities.attachments_set_link') }}</div> </div> - <div tab-content="file"> - <drop-zone upload-url="@{{getUploadUrl(editFile)}}" uploaded-to="@{{uploadedTo}}" placeholder="{{ trans('entities.attachments_edit_drop_upload') }}" event-success="uploadSuccessUpdate"></drop-zone> + <div v-if="editTab === 'file'"> + <dropzone :upload-url="getUploadUrl(fileToEdit)" :uploaded-to="pageId" placeholder="{{ trans('entities.attachments_edit_drop_upload') }}" @success="uploadSuccessUpdate"></dropzone> <br> </div> - <div tab-content="link"> + <div v-if="editTab === 'link'"> <div class="form-group"> <label for="attachment-link-edit">{{ trans('entities.attachments_link_url') }}</label> - <input type="text" id="attachment-link-edit" placeholder="{{ trans('entities.attachment_link') }}" ng-model="editFile.link"> - <p class="small neg" ng-repeat="error in errors.edit.link" ng-bind="error"></p> + <input type="text" id="attachment-link-edit" placeholder="{{ trans('entities.attachment_link') }}" v-model="fileToEdit.link"> + <p class="small neg" v-for="error in errors.edit.link" v-text="error"></p> </div> </div> </div> - <button type="button" class="button" ng-click="cancelEdit()">{{ trans('common.back') }}</button> - <button type="submit" class="button pos">{{ trans('common.save') }}</button> + <button type="button" class="button" @click="cancelEdit">{{ trans('common.back') }}</button> + <button @click.enter.prevent="updateFile(fileToEdit)" class="button pos">{{ trans('common.save') }}</button> </div> </div> diff --git a/resources/views/pages/list-item.blade.php b/resources/views/pages/list-item.blade.php index 70b309e7d..593c2fc3c 100644 --- a/resources/views/pages/list-item.blade.php +++ b/resources/views/pages/list-item.blade.php @@ -1,13 +1,27 @@ <div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}"> <h4> + @if (isset($showPath) && $showPath) + <a href="{{ $page->book->getUrl() }}" class="text-book"> + <i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }} + </a> + <span class="text-muted"> » </span> + @if($page->chapter) + <a href="{{ $page->chapter->getUrl() }}" class="text-chapter"> + <i class="zmdi zmdi-collection-bookmark"></i>{{ $page->chapter->getShortName() }} + </a> + <span class="text-muted"> » </span> + @endif + @endif <a href="{{ $page->getUrl() }}" class="text-page entity-list-item-link"><i class="zmdi zmdi-file-text"></i><span class="entity-list-item-name">{{ $page->name }}</span></a> </h4> - @if(isset($page->searchSnippet)) - <p class="text-muted">{!! $page->searchSnippet !!}</p> - @else - <p class="text-muted">{{ $page->getExcerpt() }}</p> - @endif + <div class="entity-item-snippet"> + @if(isset($page->searchSnippet)) + <p class="text-muted">{!! $page->searchSnippet !!}</p> + @else + <p class="text-muted">{{ $page->getExcerpt() }}</p> + @endif + </div> @if(isset($style) && $style === 'detailed') <div class="row meta text-muted text-small"> diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index 6b2dc3c23..0d75a534a 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -5,10 +5,10 @@ <div class="faded-small toolbar"> <div class="container"> <div class="row"> - <div class="col-sm-6 faded"> + <div class="col-sm-8 col-xs-5 faded"> @include('pages._breadcrumbs', ['page' => $page]) </div> - <div class="col-sm-6 faded"> + <div class="col-sm-4 col-xs-7 faded"> <div class="action-buttons"> <span dropdown class="dropdown-container"> <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>{{ trans('entities.export') }}</div> @@ -46,13 +46,13 @@ </div> - <div class="container" id="page-show" ng-non-bindable> + <div class="container" id="page-show"> <div class="row"> <div class="col-md-9 print-full-width"> - <div class="page-content"> + <div class="page-content" ng-non-bindable> <div class="pointer-container" id="pointer"> - <div class="pointer anim"> + <div class="pointer anim" > <span class="icon text-primary"><i class="zmdi zmdi-link"></i></span> <input readonly="readonly" type="text" id="pointer-url" placeholder="url"> <button class="button icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}"><i class="zmdi zmdi-copy"></i></button> @@ -66,6 +66,7 @@ @include('partials.entity-meta', ['entity' => $page]) </div> + @include('comments/comments', ['pageId' => $page->id]) </div> <div class="col-md-3 print-hidden"> @@ -109,7 +110,6 @@ </div> </div> - @stop @section('scripts') diff --git a/resources/views/pages/sidebar-tree-list.blade.php b/resources/views/pages/sidebar-tree-list.blade.php index 0a10987d6..90737740c 100644 --- a/resources/views/pages/sidebar-tree-list.blade.php +++ b/resources/views/pages/sidebar-tree-list.blade.php @@ -51,7 +51,7 @@ </a> @if($bookChild->isA('chapter') && count($bookChild->pages) > 0) - <p class="text-muted chapter-toggle @if($bookChild->matchesOrContains($current)) open @endif"> + <p chapter-toggle class="text-muted @if($bookChild->matchesOrContains($current)) open @endif"> <i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans('entities.x_pages', ['count' => $bookChild->pages->count()]) }}</span> </p> <ul class="menu sub-menu inset-list @if($bookChild->matchesOrContains($current)) open @endif"> diff --git a/resources/views/partials/custom-styles.blade.php b/resources/views/partials/custom-styles.blade.php index 62bcc881f..ffa39c50a 100644 --- a/resources/views/partials/custom-styles.blade.php +++ b/resources/views/partials/custom-styles.blade.php @@ -1,5 +1,6 @@ <style id="custom-styles" data-color="{{ setting('app-color') }}" data-color-light="{{ setting('app-color-light') }}"> - header, #back-to-top, .primary-background { + @if(setting('app-color')) + header, [back-to-top], .primary-background { background-color: {{ setting('app-color') }} !important; } .faded-small, .primary-background-light { @@ -17,4 +18,5 @@ .text-primary, p.primary, p .primary, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus { color: {{ setting('app-color') }}; } + @endif </style> \ No newline at end of file diff --git a/resources/views/partials/notifications.blade.php b/resources/views/partials/notifications.blade.php index c079080db..215aee3ed 100644 --- a/resources/views/partials/notifications.blade.php +++ b/resources/views/partials/notifications.blade.php @@ -1,12 +1,12 @@ -<div class="notification anim pos" @if(!session()->has('success')) style="display:none;" @endif> +<div notification="success" data-autohide class="pos" @if(session()->has('success')) data-show @endif> <i class="zmdi zmdi-check-circle"></i> <span>{!! nl2br(htmlentities(session()->get('success'))) !!}</span> </div> -<div class="notification anim warning stopped" @if(!session()->has('warning')) style="display:none;" @endif> +<div notification="warning" class="warning" @if(session()->has('warning')) data-show @endif> <i class="zmdi zmdi-info"></i> <span>{!! nl2br(htmlentities(session()->get('warning'))) !!}</span> </div> -<div class="notification anim neg stopped" @if(!session()->has('error')) style="display:none;" @endif> +<div notification="error" class="neg" @if(session()->has('error')) data-show @endif> <i class="zmdi zmdi-alert-circle"></i> <span>{!! nl2br(htmlentities(session()->get('error'))) !!}</span> </div> diff --git a/resources/views/search/entity-ajax-list.blade.php b/resources/views/search/entity-ajax-list.blade.php index 93f2633aa..9a146506f 100644 --- a/resources/views/search/entity-ajax-list.blade.php +++ b/resources/views/search/entity-ajax-list.blade.php @@ -2,7 +2,7 @@ @if(count($entities) > 0) @foreach($entities as $index => $entity) @if($entity->isA('page')) - @include('pages/list-item', ['page' => $entity]) + @include('pages/list-item', ['page' => $entity, 'showPath' => true]) @elseif($entity->isA('book')) @include('books/list-item', ['book' => $entity]) @elseif($entity->isA('chapter')) diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index 71b8f551f..02ef525ea 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php @@ -117,6 +117,19 @@ <label>@include('settings/roles/checkbox', ['permission' => 'attachment-delete-all']) {{ trans('settings.role_all') }}</label> </td> </tr> + <tr> + <td>{{ trans('entities.comments') }}</td> + <td>@include('settings/roles/checkbox', ['permission' => 'comment-create-all'])</td> + <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td> + <td> + <label>@include('settings/roles/checkbox', ['permission' => 'comment-update-own']) {{ trans('settings.role_own') }}</label> + <label>@include('settings/roles/checkbox', ['permission' => 'comment-update-all']) {{ trans('settings.role_all') }}</label> + </td> + <td> + <label>@include('settings/roles/checkbox', ['permission' => 'comment-delete-own']) {{ trans('settings.role_own') }}</label> + <label>@include('settings/roles/checkbox', ['permission' => 'comment-delete-all']) {{ trans('settings.role_all') }}</label> + </td> + </tr> </table> </div> </div> diff --git a/routes/web.php b/routes/web.php index 8ecfd9465..463e4e77b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -119,6 +119,12 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/ajax/search/entities', 'SearchController@searchEntitiesAjax'); + // Comments + Route::post('/ajax/page/{pageId}/comment/', 'CommentController@save'); + Route::put('/ajax/page/{pageId}/comment/{commentId}', 'CommentController@save'); + Route::delete('/ajax/comment/{id}', 'CommentController@destroy'); + Route::get('/ajax/page/{pageId}/comments/', 'CommentController@getPageComments'); + // Links Route::get('/link/{id}', 'PageController@redirectFromLink'); diff --git a/tests/Auth/LdapTest.php b/tests/Auth/LdapTest.php index 681ead91c..8880c7b65 100644 --- a/tests/Auth/LdapTest.php +++ b/tests/Auth/LdapTest.php @@ -11,6 +11,7 @@ class LdapTest extends BrowserKitTest public function setUp() { parent::setUp(); + if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1); app('config')->set(['auth.method' => 'ldap', 'services.ldap.base_dn' => 'dc=ldap,dc=local', 'auth.providers.users.driver' => 'ldap']); $this->mockLdap = \Mockery::mock(\BookStack\Services\Ldap::class); $this->app['BookStack\Services\Ldap'] = $this->mockLdap; @@ -21,6 +22,7 @@ class LdapTest extends BrowserKitTest { $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); $this->mockLdap->shouldReceive('setVersion')->once(); + $this->mockLdap->shouldReceive('setOption')->times(4); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -49,6 +51,7 @@ class LdapTest extends BrowserKitTest $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); $this->mockLdap->shouldReceive('setVersion')->once(); $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn'); + $this->mockLdap->shouldReceive('setOption')->times(2); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -72,6 +75,7 @@ class LdapTest extends BrowserKitTest { $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); $this->mockLdap->shouldReceive('setVersion')->once(); + $this->mockLdap->shouldReceive('setOption')->times(2); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -129,4 +133,4 @@ class LdapTest extends BrowserKitTest ->dontSee('External Authentication'); } -} \ No newline at end of file +} diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentTest.php new file mode 100644 index 000000000..86eb31213 --- /dev/null +++ b/tests/Entity/CommentTest.php @@ -0,0 +1,111 @@ +<?php namespace Tests; + +use BookStack\Page; +use BookStack\Comment; + +class CommentTest extends BrowserKitTest +{ + + public function test_add_comment() + { + $this->asAdmin(); + $page = $this->getPage(); + + $this->addComment($page); + } + + public function test_comment_reply() + { + $this->asAdmin(); + $page = $this->getPage(); + + // add a normal comment + $createdComment = $this->addComment($page); + + // reply to the added comment + $this->addComment($page, $createdComment['id']); + } + + public function test_comment_edit() + { + $this->asAdmin(); + $page = $this->getPage(); + + $createdComment = $this->addComment($page); + $comment = [ + 'id' => $createdComment['id'], + 'page_id' => $createdComment['page_id'] + ]; + $this->updateComment($comment); + } + + public function test_comment_delete() + { + $this->asAdmin(); + $page = $this->getPage(); + + $createdComment = $this->addComment($page); + + $this->deleteComment($createdComment['id']); + } + + private function getPage() { + $page = Page::first(); + return $page; + } + + + private function addComment($page, $parentCommentId = null) { + $comment = factory(Comment::class)->make(); + $url = "/ajax/page/$page->id/comment/"; + $request = [ + 'text' => $comment->text, + 'html' => $comment->html + ]; + if (!empty($parentCommentId)) { + $request['parent_id'] = $parentCommentId; + } + $this->call('POST', $url, $request); + + $createdComment = $this->checkResponse(); + return $createdComment; + } + + private function updateComment($comment) { + $tmpComment = factory(Comment::class)->make(); + $url = '/ajax/page/' . $comment['page_id'] . '/comment/ ' . $comment['id']; + $request = [ + 'text' => $tmpComment->text, + 'html' => $tmpComment->html + ]; + + $this->call('PUT', $url, $request); + + $updatedComment = $this->checkResponse(); + return $updatedComment; + } + + private function deleteComment($commentId) { + // Route::delete('/ajax/comment/{id}', 'CommentController@destroy'); + $url = '/ajax/comment/' . $commentId; + $this->call('DELETE', $url); + + $deletedComment = $this->checkResponse(); + return $deletedComment; + } + + private function checkResponse() { + $expectedResp = [ + 'status' => 'success' + ]; + + $this->assertResponseOk(); + $this->seeJsonContains($expectedResp); + + $resp = $this->decodeResponseJson(); + $createdComment = $resp['comment']; + $this->assertArrayHasKey('id', $createdComment); + + return $createdComment; + } +} diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index 94e28e944..587430918 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -1,6 +1,9 @@ <?php namespace Tests; +use BookStack\Chapter; +use BookStack\Page; + class EntitySearchTest extends TestCase { @@ -75,10 +78,10 @@ class EntitySearchTest extends TestCase ]) ]; - $pageA = \BookStack\Page::first(); + $pageA = Page::first(); $pageA->tags()->saveMany($newTags); - $pageB = \BookStack\Page::all()->last(); + $pageB = Page::all()->last(); $pageB->tags()->create(['name' => 'animal', 'value' => 'dog']); $this->asEditor(); @@ -160,8 +163,8 @@ class EntitySearchTest extends TestCase public function test_ajax_entity_search() { - $page = \BookStack\Page::all()->last(); - $notVisitedPage = \BookStack\Page::first(); + $page = Page::all()->last(); + $notVisitedPage = Page::first(); // Visit the page to make popular $this->asEditor()->get($page->getUrl()); @@ -176,4 +179,20 @@ class EntitySearchTest extends TestCase $defaultListTest->assertSee($page->name); $defaultListTest->assertDontSee($notVisitedPage->name); } + + public function test_ajax_entity_serach_shows_breadcrumbs() + { + $chapter = Chapter::first(); + $page = $chapter->pages->first(); + $this->asEditor(); + + $pageSearch = $this->get('/ajax/search/entities?term=' . urlencode($page->name)); + $pageSearch->assertSee($page->name); + $pageSearch->assertSee($chapter->getShortName()); + $pageSearch->assertSee($page->book->getShortName()); + + $chapterSearch = $this->get('/ajax/search/entities?term=' . urlencode($chapter->name)); + $chapterSearch->assertSee($chapter->name); + $chapterSearch->assertSee($chapter->book->getShortName()); + } } diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index eda5d092a..f131ed885 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -657,4 +657,112 @@ class RolesTest extends BrowserKitTest ->dontSee('Sort the current book'); } + public function test_comment_create_permission () { + $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + + $this->actingAs($this->user)->addComment($ownPage); + + $this->assertResponseStatus(403); + + $this->giveUserPermissions($this->user, ['comment-create-all']); + + $this->actingAs($this->user)->addComment($ownPage); + $this->assertResponseOk(200)->seeJsonContains(['status' => 'success']); + } + + + public function test_comment_update_own_permission () { + $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $this->giveUserPermissions($this->user, ['comment-create-all']); + $comment = $this->actingAs($this->user)->addComment($ownPage); + + // no comment-update-own + $this->actingAs($this->user)->updateComment($ownPage, $comment['id']); + $this->assertResponseStatus(403); + + $this->giveUserPermissions($this->user, ['comment-update-own']); + + // now has comment-update-own + $this->actingAs($this->user)->updateComment($ownPage, $comment['id']); + $this->assertResponseOk()->seeJsonContains(['status' => 'success']); + } + + public function test_comment_update_all_permission () { + $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $comment = $this->asAdmin()->addComment($ownPage); + + // no comment-update-all + $this->actingAs($this->user)->updateComment($ownPage, $comment['id']); + $this->assertResponseStatus(403); + + $this->giveUserPermissions($this->user, ['comment-update-all']); + + // now has comment-update-all + $this->actingAs($this->user)->updateComment($ownPage, $comment['id']); + $this->assertResponseOk()->seeJsonContains(['status' => 'success']); + } + + public function test_comment_delete_own_permission () { + $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $this->giveUserPermissions($this->user, ['comment-create-all']); + $comment = $this->actingAs($this->user)->addComment($ownPage); + + // no comment-delete-own + $this->actingAs($this->user)->deleteComment($comment['id']); + $this->assertResponseStatus(403); + + $this->giveUserPermissions($this->user, ['comment-delete-own']); + + // now has comment-update-own + $this->actingAs($this->user)->deleteComment($comment['id']); + $this->assertResponseOk()->seeJsonContains(['status' => 'success']); + } + + public function test_comment_delete_all_permission () { + $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; + $comment = $this->asAdmin()->addComment($ownPage); + + // no comment-delete-all + $this->actingAs($this->user)->deleteComment($comment['id']); + $this->assertResponseStatus(403); + + $this->giveUserPermissions($this->user, ['comment-delete-all']); + + // now has comment-delete-all + $this->actingAs($this->user)->deleteComment($comment['id']); + $this->assertResponseOk()->seeJsonContains(['status' => 'success']); + } + + private function addComment($page) { + $comment = factory(\BookStack\Comment::class)->make(); + $url = "/ajax/page/$page->id/comment/"; + $request = [ + 'text' => $comment->text, + 'html' => $comment->html + ]; + + $this->json('POST', $url, $request); + $resp = $this->decodeResponseJson(); + if (isset($resp['comment'])) { + return $resp['comment']; + } + return null; + } + + private function updateComment($page, $commentId) { + $comment = factory(\BookStack\Comment::class)->make(); + $url = "/ajax/page/$page->id/comment/$commentId"; + $request = [ + 'text' => $comment->text, + 'html' => $comment->html + ]; + + return $this->json('PUT', $url, $request); + } + + private function deleteComment($commentId) { + $url = '/ajax/comment/' . $commentId; + return $this->json('DELETE', $url); + } + } diff --git a/version b/version index 1365ed940..de0ad7791 100644 --- a/version +++ b/version @@ -1 +1 @@ -v0.17-dev +v0.18-dev