diff --git a/.gitignore b/.gitignore index d7281b29c..00602d29a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,16 +8,15 @@ Homestead.yaml /public/css/*.map /public/js/*.map /public/bower +/public/build/ /storage/images _ide_helper.php /storage/debugbar .phpstorm.meta.php yarn.lock /bin +nbproject .buildpath - .project - .settings/org.eclipse.wst.common.project.facet.core.xml - .settings/org.eclipse.php.core.prefs diff --git a/.travis.yml b/.travis.yml index 839d3be3f..29f9fb7a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ dist: trusty sudo: false language: php php: - - 7.0 + - 7.0.7 cache: directories: diff --git a/app/Comment.php b/app/Comment.php new file mode 100644 index 000000000..2800ab21a --- /dev/null +++ b/app/Comment.php @@ -0,0 +1,43 @@ +morphTo('entity'); + } + + /** + * Check if a comment has been updated since creation. + * @return bool + */ + public function isUpdated() + { + return $this->updated_at->timestamp > $this->created_at->timestamp; + } + + /** + * Get created date as a relative diff. + * @return mixed + */ + public function getCreatedAttribute() + { + return $this->created_at->diffForHumans(); + } + + /** + * Get updated date as a relative diff. + * @return mixed + */ + public function getUpdatedAttribute() + { + return $this->updated_at->diffForHumans(); + } +} diff --git a/app/Entity.php b/app/Entity.php index e5dd04bf2..df8e4d38b 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -1,6 +1,8 @@ morphMany(Tag::class, 'entity')->orderBy('order', 'asc'); } + /** + * Get the comments for an entity + * @param bool $orderByCreated + * @return MorphMany + */ + public function comments($orderByCreated = true) + { + $query = $this->morphMany(Comment::class, 'entity'); + return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query; + } + /** * Get the related search terms. * @return \Illuminate\Database\Eloquent\Relations\MorphMany diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 8b0ef309a..1ba8b97db 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) { @@ -230,7 +231,6 @@ class RegisterController extends Controller return redirect('/register/confirm'); } - $this->emailConfirmationService->sendConfirmation($user); session()->flash('success', trans('auth.email_confirm_resent')); return redirect('/register/confirm'); } @@ -255,16 +255,13 @@ class RegisterController extends Controller */ public function socialCallback($socialDriver) { - if (session()->has('social-callback')) { - $action = session()->pull('social-callback'); - if ($action == 'login') { - return $this->socialAuthService->handleLoginCallback($socialDriver); - } elseif ($action == 'register') { - return $this->socialRegisterCallback($socialDriver); - } - } else { + if (!session()->has('social-callback')) { throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login'); } + + $action = session()->pull('social-callback'); + if ($action == 'login') return $this->socialAuthService->handleLoginCallback($socialDriver); + if ($action == 'register') return $this->socialRegisterCallback($socialDriver); return redirect()->back(); } diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index 8996ae64a..5342ece6b 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -36,11 +36,17 @@ class BookController extends Controller */ public function index() { - $books = $this->entityRepo->getAllPaginated('book', 10); + $books = $this->entityRepo->getAllPaginated('book', 20); $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false; $popular = $this->entityRepo->getPopular('book', 4, 0); + $new = $this->entityRepo->getRecentlyCreated('book', 4, 0); $this->setPageTitle('Books'); - return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]); + return view('books/index', [ + 'books' => $books, + 'recents' => $recents, + 'popular' => $popular, + 'new' => $new + ]); } /** @@ -84,7 +90,12 @@ class BookController extends Controller $bookChildren = $this->entityRepo->getBookChildren($book); Views::add($book); $this->setPageTitle($book->getShortName()); - return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]); + return view('books/show', [ + 'book' => $book, + 'current' => $book, + 'bookChildren' => $bookChildren, + 'activity' => Activity::entityActivity($book, 20, 0) + ]); } /** diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php new file mode 100644 index 000000000..7bf0a2aac --- /dev/null +++ b/app/Http/Controllers/CommentController.php @@ -0,0 +1,93 @@ +entityRepo = $entityRepo; + $this->commentRepo = $commentRepo; + parent::__construct(); + } + + /** + * Save a new comment for a Page + * @param Request $request + * @param integer $pageId + * @param null|integer $commentId + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response + */ + public function savePageComment(Request $request, $pageId, $commentId = null) + { + $this->validate($request, [ + 'text' => 'required|string', + 'html' => 'required|string', + ]); + + try { + $page = $this->entityRepo->getById('page', $pageId, true); + } catch (ModelNotFoundException $e) { + return response('Not found', 404); + } + + $this->checkOwnablePermission('page-view', $page); + + // Prevent adding comments to draft pages + if ($page->draft) { + return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400); + } + + // Create a new comment. + $this->checkPermission('comment-create-all'); + $comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id'])); + Activity::add($page, 'commented_on', $page->book->id); + return view('comments/comment', ['comment' => $comment]); + } + + /** + * Update an existing comment. + * @param Request $request + * @param integer $commentId + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function update(Request $request, $commentId) + { + $this->validate($request, [ + 'text' => 'required|string', + 'html' => 'required|string', + ]); + + $comment = $this->commentRepo->getById($commentId); + $this->checkOwnablePermission('page-view', $comment->entity); + $this->checkOwnablePermission('comment-update', $comment); + + $comment = $this->commentRepo->update($comment, $request->only(['html', 'text'])); + return view('comments/comment', ['comment' => $comment]); + } + + /** + * Delete a comment from the system. + * @param integer $id + * @return \Illuminate\Http\JsonResponse + */ + public function destroy($id) + { + $comment = $this->commentRepo->getById($id); + $this->checkOwnablePermission('comment-delete', $comment); + $this->commentRepo->delete($comment); + return response()->json(['message' => trans('entities.comment_deleted')]); + } +} diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 7f60d7009..7d109b70a 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -29,15 +29,25 @@ class HomeController extends Controller $activity = Activity::latest(10); $draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : []; $recentFactor = count($draftPages) > 0 ? 0.5 : 1; - $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor); - $recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5); - $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5); - return view('home', [ + $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor); + $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12); + + // Custom homepage + $customHomepage = false; + $homepageSetting = setting('app-homepage'); + if ($homepageSetting) { + $id = intval(explode(':', $homepageSetting)[0]); + $customHomepage = $this->entityRepo->getById('page', $id, false, true); + $this->entityRepo->renderPage($customHomepage, true); + } + + $view = $customHomepage ? 'home-custom' : 'home'; + return view($view, [ 'activity' => $activity, 'recents' => $recents, - 'recentlyCreatedPages' => $recentlyCreatedPages, 'recentlyUpdatedPages' => $recentlyUpdatedPages, - 'draftPages' => $draftPages + 'draftPages' => $draftPages, + 'customHomepage' => $customHomepage ]); } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index c97597bc4..c3090af83 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -161,13 +161,14 @@ class PageController extends Controller $pageContent = $this->entityRepo->renderPage($page); $sidebarTree = $this->entityRepo->getBookChildren($page->book); $pageNav = $this->entityRepo->getPageNav($pageContent); - + $page->load(['comments.createdBy']); + Views::add($page); $this->setPageTitle($page->getShortName()); return view('pages/show', [ 'page' => $page,'book' => $page->book, 'current' => $page, 'sidebarTree' => $sidebarTree, - 'pageNav' => $pageNav, 'pageContent' => $pageContent]); + 'pageNav' => $pageNav]); } /** @@ -376,10 +377,11 @@ class PageController extends Controller $page->fill($revision->toArray()); $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()])); - + return view('pages/revision', [ 'page' => $page, 'book' => $page->book, + 'revision' => $revision ]); } @@ -409,6 +411,7 @@ class PageController extends Controller 'page' => $page, 'book' => $page->book, 'diff' => $diff, + 'revision' => $revision ]); } diff --git a/app/PageRevision.php b/app/PageRevision.php index ff469f0ed..0a9764729 100644 --- a/app/PageRevision.php +++ b/app/PageRevision.php @@ -47,4 +47,16 @@ class PageRevision extends Model return null; } + /** + * Allows checking of the exact class, Used to check entity type. + * Included here to align with entities in similar use cases. + * (Yup, Bit of an awkward hack) + * @param $type + * @return bool + */ + public static function isA($type) + { + return $type === 'revision'; + } + } diff --git a/app/Repos/CommentRepo.php b/app/Repos/CommentRepo.php new file mode 100644 index 000000000..d8c57bdb3 --- /dev/null +++ b/app/Repos/CommentRepo.php @@ -0,0 +1,87 @@ +comment = $comment; + } + + /** + * Get a comment by ID. + * @param $id + * @return Comment|\Illuminate\Database\Eloquent\Model + */ + public function getById($id) + { + return $this->comment->newQuery()->findOrFail($id); + } + + /** + * Create a new comment on an entity. + * @param Entity $entity + * @param array $data + * @return Comment + */ + public function create (Entity $entity, $data = []) + { + $userId = user()->id; + $comment = $this->comment->newInstance($data); + $comment->created_by = $userId; + $comment->updated_by = $userId; + $comment->local_id = $this->getNextLocalId($entity); + $entity->comments()->save($comment); + return $comment; + } + + /** + * Update an existing comment. + * @param Comment $comment + * @param array $input + * @return mixed + */ + public function update($comment, $input) + { + $comment->updated_by = user()->id; + $comment->update($input); + return $comment; + } + + /** + * Delete a comment from the system. + * @param Comment $comment + * @return mixed + */ + public function delete($comment) + { + return $comment->delete(); + } + + /** + * Get the next local ID relative to the linked entity. + * @param Entity $entity + * @return int + */ + protected function getNextLocalId(Entity $entity) + { + $comments = $entity->comments(false)->orderBy('local_id', 'desc')->first(); + if ($comments === null) return 1; + return $comments->local_id + 1; + } +} \ No newline at end of file diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index d87c40f9b..9a6db3fc3 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -137,10 +137,15 @@ class EntityRepo * @param string $type * @param integer $id * @param bool $allowDrafts + * @param bool $ignorePermissions * @return Entity */ - public function getById($type, $id, $allowDrafts = false) + public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false) { + if ($ignorePermissions) { + $entity = $this->getEntity($type); + return $entity->newQuery()->find($id); + } return $this->entityQuery($type, $allowDrafts)->find($id); } @@ -671,9 +676,10 @@ class EntityRepo /** * Render the page for viewing, Parsing and performing features such as page transclusion. * @param Page $page + * @param bool $ignorePermissions * @return mixed|string */ - public function renderPage(Page $page) + public function renderPage(Page $page, $ignorePermissions = false) { $content = $page->html; $matches = []; @@ -685,19 +691,19 @@ class EntityRepo $pageId = intval($splitInclude[0]); if (is_nan($pageId)) continue; - $page = $this->getById('page', $pageId); - if ($page === null) { + $matchedPage = $this->getById('page', $pageId, false, $ignorePermissions); + if ($matchedPage === null) { $content = str_replace($matches[0][$index], '', $content); continue; } if (count($splitInclude) === 1) { - $content = str_replace($matches[0][$index], $page->html, $content); + $content = str_replace($matches[0][$index], $matchedPage->html, $content); continue; } $doc = new DOMDocument(); - $doc->loadHTML(mb_convert_encoding(''.$page->html.'', 'HTML-ENTITIES', 'UTF-8')); + $doc->loadHTML(mb_convert_encoding(''.$matchedPage->html.'', 'HTML-ENTITIES', 'UTF-8')); $matchingElem = $doc->getElementById($splitInclude[1]); if ($matchingElem === null) { $content = str_replace($matches[0][$index], '', $content); @@ -710,6 +716,7 @@ class EntityRepo $content = str_replace($matches[0][$index], trim($innerContent), $content); } + $page->setAttribute('renderedHTML', $content); return $content; } diff --git a/app/Repos/TagRepo.php b/app/Repos/TagRepo.php index c6350db1a..5edd6df3c 100644 --- a/app/Repos/TagRepo.php +++ b/app/Repos/TagRepo.php @@ -33,6 +33,7 @@ class TagRepo * @param $entityType * @param $entityId * @param string $action + * @return \Illuminate\Database\Eloquent\Model|null|static */ public function getEntity($entityType, $entityId, $action = 'view') { diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php index 78cef41a4..40402dbce 100644 --- a/app/Services/ExportService.php +++ b/app/Services/ExportService.php @@ -27,9 +27,9 @@ class ExportService */ public function pageToContainedHtml(Page $page) { + $this->entityRepo->renderPage($page); $pageHtml = view('pages/export', [ - 'page' => $page, - 'pageContent' => $this->entityRepo->renderPage($page) + 'page' => $page ])->render(); return $this->containHtml($pageHtml); } @@ -74,9 +74,9 @@ class ExportService */ public function pageToPdf(Page $page) { + $this->entityRepo->renderPage($page); $html = view('pages/pdf', [ - 'page' => $page, - 'pageContent' => $this->entityRepo->renderPage($page) + 'page' => $page ])->render(); return $this->htmlToPdf($html); } 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/app/Services/SearchService.php b/app/Services/SearchService.php index 3d1d45c3b..bb92a1d7c 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -92,7 +92,7 @@ class SearchService return [ 'total' => $total, 'count' => count($results), - 'results' => $results->sortByDesc('score') + 'results' => $results->sortByDesc('score')->values() ]; } diff --git a/app/Services/ViewService.php b/app/Services/ViewService.php index 3285745ce..770a9e39f 100644 --- a/app/Services/ViewService.php +++ b/app/Services/ViewService.php @@ -62,7 +62,7 @@ class ViewService $query->whereIn('viewable_type', $filterModel); } else if ($filterModel) { $query->where('viewable_type', '=', get_class($filterModel)); - }; + } return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable'); } diff --git a/config/app.php b/config/app.php old mode 100644 new mode 100755 index a390eaf83..25e891042 --- 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', 'pl'], + 'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl', 'it'], /* |-------------------------------------------------------------------------- diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index ebf78d1fa..c68f5c1e1 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(1); + $html = '

' . $text. '

'; + return [ + 'html' => $html, + 'text' => $text, + 'parent_id' => null + ]; }); \ No newline at end of file diff --git a/database/migrations/2017_08_01_130541_create_comments_table.php b/database/migrations/2017_08_01_130541_create_comments_table.php new file mode 100644 index 000000000..1d69d1fa7 --- /dev/null +++ b/database/migrations/2017_08_01_130541_create_comments_table.php @@ -0,0 +1,68 @@ +increments('id')->unsigned(); + $table->integer('entity_id')->unsigned(); + $table->string('entity_type'); + $table->longText('text')->nullable(); + $table->longText('html')->nullable(); + $table->integer('parent_id')->unsigned()->nullable(); + $table->integer('local_id')->unsigned()->nullable(); + $table->integer('created_by')->unsigned(); + $table->integer('updated_by')->unsigned()->nullable(); + $table->timestamps(); + + $table->index(['entity_id', 'entity_type']); + $table->index(['local_id']); + + // Assign new comment permissions to admin role + $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id; + // Create & attach new entity permissions + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; + $entity = 'Comment'; + foreach ($ops as $op) { + $permissionId = DB::table('role_permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'display_name' => $op . ' ' . $entity . 's', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId + ]); + } + + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('comments'); + // Delete comment role permissions + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; + $entity = 'Comment'; + foreach ($ops as $op) { + $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)); + DB::table('role_permissions')->where('name', '=', $permName)->delete(); + } + } +} diff --git a/database/seeds/DummyContentSeeder.php b/database/seeds/DummyContentSeeder.php index 3d92efab1..d18eb30db 100644 --- a/database/seeds/DummyContentSeeder.php +++ b/database/seeds/DummyContentSeeder.php @@ -15,12 +15,11 @@ class DummyContentSeeder extends Seeder $role = \BookStack\Role::getRole('editor'); $user->attachRole($role); - factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id]) ->each(function($book) use ($user) { $chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id]) ->each(function($chapter) use ($user, $book){ - $pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]); + $pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]); $chapter->pages()->saveMany($pages); }); $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]); @@ -33,7 +32,6 @@ class DummyContentSeeder extends Seeder $chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $user->id, 'updated_by' => $user->id]); $largeBook->pages()->saveMany($pages); $largeBook->chapters()->saveMany($chapters); - app(\BookStack\Services\PermissionService::class)->buildJointPermissions(); app(\BookStack\Services\SearchService::class)->indexAllEntities(); } diff --git a/gulpfile.js b/gulpfile.js index 08c8886bd..c9f3f7956 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,16 +1,22 @@ +'use strict'; + const argv = require('yargs').argv; const gulp = require('gulp'), plumber = require('gulp-plumber'); + const autoprefixer = require('gulp-autoprefixer'); -const uglify = require('gulp-uglify'); const minifycss = require('gulp-clean-css'); const sass = require('gulp-sass'); +const sourcemaps = require('gulp-sourcemaps'); + const browserify = require("browserify"); const source = require('vinyl-source-stream'); const buffer = require('vinyl-buffer'); const babelify = require("babelify"); const watchify = require("watchify"); const envify = require("envify"); +const uglify = require('gulp-uglify'); + const gutil = require("gulp-util"); const liveReload = require('gulp-livereload'); @@ -19,6 +25,7 @@ let isProduction = argv.production || process.env.NODE_ENV === 'production'; gulp.task('styles', () => { let chain = gulp.src(['resources/assets/sass/**/*.scss']) + .pipe(sourcemaps.init()) .pipe(plumber({ errorHandler: function (error) { console.log(error.message); @@ -27,6 +34,7 @@ gulp.task('styles', () => { .pipe(sass()) .pipe(autoprefixer('last 2 versions')); if (isProduction) chain = chain.pipe(minifycss()); + chain = chain.pipe(sourcemaps.write()); return chain.pipe(gulp.dest('public/css/')).pipe(liveReload()); }); diff --git a/package.json b/package.json index 429572882..ed8338abb 100644 --- a/package.json +++ b/package.json @@ -31,15 +31,18 @@ "angular-sanitize": "^1.5.5", "angular-ui-sortable": "^0.17.0", "axios": "^0.16.1", + "babel-polyfill": "^6.23.0", "babel-preset-es2015": "^6.24.1", - "clipboard": "^1.5.16", + "clipboard": "^1.7.1", "codemirror": "^5.26.0", "dropzone": "^4.0.1", + "gulp-sourcemaps": "^2.6.1", "gulp-util": "^3.0.8", "markdown-it": "^8.3.1", "markdown-it-task-lists": "^2.0.0", "moment": "^2.12.0", - "vue": "^2.2.6" + "vue": "^2.2.6", + "vuedraggable": "^2.14.1" }, "browser": { "vue": "vue/dist/vue.common.js" diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff b/public/fonts/roboto-mono-v4-latin-regular.woff deleted file mode 100644 index 8cb9e6fd8..000000000 Binary files a/public/fonts/roboto-mono-v4-latin-regular.woff and /dev/null differ diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff2 b/public/fonts/roboto-mono-v4-latin-regular.woff2 deleted file mode 100644 index 1f6598111..000000000 Binary files a/public/fonts/roboto-mono-v4-latin-regular.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-100.woff b/public/fonts/roboto-v15-cyrillic_latin-100.woff deleted file mode 100644 index 4eb2be6a1..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-100.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-100.woff2 b/public/fonts/roboto-v15-cyrillic_latin-100.woff2 deleted file mode 100644 index 007b90e85..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-100.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff b/public/fonts/roboto-v15-cyrillic_latin-100italic.woff deleted file mode 100644 index fa7e51bc8..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 deleted file mode 100644 index f27a169cb..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-300.woff b/public/fonts/roboto-v15-cyrillic_latin-300.woff deleted file mode 100644 index ace052941..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-300.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-300.woff2 b/public/fonts/roboto-v15-cyrillic_latin-300.woff2 deleted file mode 100644 index 0c093b91c..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-300.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff b/public/fonts/roboto-v15-cyrillic_latin-300italic.woff deleted file mode 100644 index 7984971e7..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 deleted file mode 100644 index 46ed6c7cc..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-500.woff b/public/fonts/roboto-v15-cyrillic_latin-500.woff deleted file mode 100644 index 8ae98f2de..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-500.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-500.woff2 b/public/fonts/roboto-v15-cyrillic_latin-500.woff2 deleted file mode 100644 index fba67842e..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-500.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff b/public/fonts/roboto-v15-cyrillic_latin-500italic.woff deleted file mode 100644 index 560968d16..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 deleted file mode 100644 index cc41bf873..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-700.woff b/public/fonts/roboto-v15-cyrillic_latin-700.woff deleted file mode 100644 index 7d19e332d..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-700.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-700.woff2 b/public/fonts/roboto-v15-cyrillic_latin-700.woff2 deleted file mode 100644 index e2274a4fb..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-700.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff b/public/fonts/roboto-v15-cyrillic_latin-700italic.woff deleted file mode 100644 index 1604c8763..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 deleted file mode 100644 index f950ca2aa..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-italic.woff b/public/fonts/roboto-v15-cyrillic_latin-italic.woff deleted file mode 100644 index d76d13d6a..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-italic.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-italic.woff2 deleted file mode 100644 index a80f41528..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-italic.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-regular.woff b/public/fonts/roboto-v15-cyrillic_latin-regular.woff deleted file mode 100644 index a2ada2f46..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-regular.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-regular.woff2 b/public/fonts/roboto-v15-cyrillic_latin-regular.woff2 deleted file mode 100644 index a3b35e686..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-regular.woff2 and /dev/null differ diff --git a/public/logo.png b/public/logo.png index 1803feebf..585f8895b 100644 Binary files a/public/logo.png and b/public/logo.png differ diff --git a/readme.md b/readme.md index 6067fbb94..1d4ed682e 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ # BookStack -[![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg?maxAge=2592000)](https://github.com/BookStackApp/BookStack/releases/latest) -[![license](https://img.shields.io/github/license/BookStackApp/BookStack.svg?maxAge=2592000)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE) +[![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest) +[![license](https://img.shields.io/github/license/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE) [![Build Status](https://travis-ci.org/BookStackApp/BookStack.svg)](https://travis-ci.org/BookStackApp/BookStack) A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/. @@ -13,6 +13,12 @@ A platform for storing and organising information and documentation. General inf * *Password: `password`* * [BookStack Blog](https://www.bookstackapp.com/blog) +## Project Definition + +BookStack is an opinionated wiki system that provides a pleasant and simple out of the box experience. New users to an instance should find the experience intuitive and only basic word-processing skills should be required to get involved in creating content on BookStack. The platform should provide advanced power features to those that desire it but they should not interfere with the core simple user experience. + +BookStack is not designed as an extensible platform to be used for purposes that differ to the statement above. + ## Development & Testing All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements: @@ -79,7 +85,7 @@ These are the great open-source projects used to help build BookStack: * [jQuery Sortable](https://johnny.github.io/jquery-sortable/) * [Material Design Iconic Font](http://zavoloklom.github.io/material-design-iconic-font/icons.html) * [Dropzone.js](http://www.dropzonejs.com/) -* [ZeroClipboard](http://zeroclipboard.org/) +* [clipboard.js](https://clipboardjs.com/) * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) * [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists) * [Moment.js](http://momentjs.com/) diff --git a/resources/assets/js/code.js b/resources/assets/js/code.js index 014c4eb77..8a5a7a022 100644 --- a/resources/assets/js/code.js +++ b/resources/assets/js/code.js @@ -53,13 +53,20 @@ const modeMap = { yml: 'yaml', }; -module.exports.highlight = function() { - let codeBlocks = document.querySelectorAll('.page-content pre'); +/** + * Highlight pre elements on a page + */ +function highlight() { + let codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre'); for (let i = 0; i < codeBlocks.length; i++) { highlightElem(codeBlocks[i]); } -}; +} +/** + * Add code highlighting to a single element. + * @param {HTMLElement} elem + */ function highlightElem(elem) { let innerCodeElem = elem.querySelector('code[class^=language-]'); let mode = ''; @@ -68,7 +75,7 @@ function highlightElem(elem) { mode = getMode(langName); } elem.innerHTML = elem.innerHTML.replace(//gi ,'\n'); - let content = elem.textContent; + let content = elem.textContent.trim(); CodeMirror(function(elt) { elem.parentNode.replaceChild(elt, elem); @@ -76,7 +83,7 @@ function highlightElem(elem) { value: content, mode: mode, lineNumbers: true, - theme: 'base16-light', + theme: getTheme(), readOnly: true }); } @@ -91,9 +98,21 @@ function getMode(suggestion) { return (typeof modeMap[suggestion] !== 'undefined') ? modeMap[suggestion] : ''; } -module.exports.highlightElem = highlightElem; +/** + * Ge the theme to use for CodeMirror instances. + * @returns {*|string} + */ +function getTheme() { + return window.codeTheme || 'base16-light'; +} -module.exports.wysiwygView = function(elem) { +/** + * Create a CodeMirror instance for showing inside the WYSIWYG editor. + * Manages a textarea element to hold code content. + * @param {HTMLElement} elem + * @returns {{wrap: Element, editor: *}} + */ +function wysiwygView(elem) { let doc = elem.ownerDocument; let codeElem = elem.querySelector('code'); @@ -122,16 +141,22 @@ module.exports.wysiwygView = function(elem) { value: content, mode: getMode(lang), lineNumbers: true, - theme: 'base16-light', + theme: getTheme(), readOnly: true }); setTimeout(() => { cm.refresh(); }, 300); return {wrap: newWrap, editor: cm}; -}; +} -module.exports.popupEditor = function(elem, modeSuggestion) { +/** + * Create a CodeMirror instance to show in the WYSIWYG pop-up editor + * @param {HTMLElement} elem + * @param {String} modeSuggestion + * @returns {*} + */ +function popupEditor(elem, modeSuggestion) { let content = elem.textContent; return CodeMirror(function(elt) { @@ -141,22 +166,38 @@ module.exports.popupEditor = function(elem, modeSuggestion) { value: content, mode: getMode(modeSuggestion), lineNumbers: true, - theme: 'base16-light', + theme: getTheme(), lineWrapping: true }); -}; +} -module.exports.setMode = function(cmInstance, modeSuggestion) { +/** + * Set the mode of a codemirror instance. + * @param cmInstance + * @param modeSuggestion + */ +function setMode(cmInstance, modeSuggestion) { cmInstance.setOption('mode', getMode(modeSuggestion)); -}; -module.exports.setContent = function(cmInstance, codeContent) { +} + +/** + * Set the content of a cm instance. + * @param cmInstance + * @param codeContent + */ +function setContent(cmInstance, codeContent) { cmInstance.setValue(codeContent); setTimeout(() => { cmInstance.refresh(); }, 10); -}; +} -module.exports.markdownEditor = function(elem) { +/** + * Get a CodeMirror instace to use for the markdown editor. + * @param {HTMLElement} elem + * @returns {*} + */ +function markdownEditor(elem) { let content = elem.textContent; return CodeMirror(function (elt) { @@ -166,13 +207,27 @@ module.exports.markdownEditor = function(elem) { value: content, mode: "markdown", lineNumbers: true, - theme: 'base16-light', + theme: getTheme(), lineWrapping: true }); -}; +} -module.exports.getMetaKey = function() { +/** + * Get the 'meta' key dependant on the user's system. + * @returns {string} + */ +function getMetaKey() { let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault; return mac ? "Cmd" : "Ctrl"; -}; +} +module.exports = { + highlight: highlight, + highlightElem: highlightElem, + wysiwygView: wysiwygView, + popupEditor: popupEditor, + setMode: setMode, + setContent: setContent, + markdownEditor: markdownEditor, + getMetaKey: getMetaKey, +}; \ No newline at end of file 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/entity-selector-popup.js b/resources/assets/js/components/entity-selector-popup.js new file mode 100644 index 000000000..64c0c62e9 --- /dev/null +++ b/resources/assets/js/components/entity-selector-popup.js @@ -0,0 +1,47 @@ + +class EntitySelectorPopup { + + constructor(elem) { + this.elem = elem; + window.EntitySelectorPopup = this; + + this.callback = null; + this.selection = null; + + this.selectButton = elem.querySelector('.entity-link-selector-confirm'); + this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this)); + + window.$events.listen('entity-select-change', this.onSelectionChange.bind(this)); + window.$events.listen('entity-select-confirm', this.onSelectionConfirm.bind(this)); + } + + show(callback) { + this.callback = callback; + this.elem.components.overlay.show(); + } + + hide() { + this.elem.components.overlay.hide(); + } + + onSelectButtonClick() { + this.hide(); + if (this.selection !== null && this.callback) this.callback(this.selection); + } + + onSelectionConfirm(entity) { + this.hide(); + if (this.callback && entity) this.callback(entity); + } + + onSelectionChange(entity) { + this.selection = entity; + if (entity === null) { + this.selectButton.setAttribute('disabled', 'true'); + } else { + this.selectButton.removeAttribute('disabled'); + } + } +} + +module.exports = EntitySelectorPopup; \ No newline at end of file diff --git a/resources/assets/js/components/entity-selector.js b/resources/assets/js/components/entity-selector.js new file mode 100644 index 000000000..53358378a --- /dev/null +++ b/resources/assets/js/components/entity-selector.js @@ -0,0 +1,118 @@ + +class EntitySelector { + + constructor(elem) { + this.elem = elem; + this.search = ''; + this.lastClick = 0; + + let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter'; + this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}`); + + this.input = elem.querySelector('[entity-selector-input]'); + this.searchInput = elem.querySelector('[entity-selector-search]'); + this.loading = elem.querySelector('[entity-selector-loading]'); + this.resultsContainer = elem.querySelector('[entity-selector-results]'); + + this.elem.addEventListener('click', this.onClick.bind(this)); + + let lastSearch = 0; + this.searchInput.addEventListener('input', event => { + lastSearch = Date.now(); + this.showLoading(); + setTimeout(() => { + if (Date.now() - lastSearch < 199) return; + this.searchEntities(this.searchInput.value); + }, 200); + }); + this.searchInput.addEventListener('keydown', event => { + if (event.keyCode === 13) event.preventDefault(); + }); + + this.showLoading(); + this.initialLoad(); + } + + showLoading() { + this.loading.style.display = 'block'; + this.resultsContainer.style.display = 'none'; + } + + hideLoading() { + this.loading.style.display = 'none'; + this.resultsContainer.style.display = 'block'; + } + + initialLoad() { + window.$http.get(this.searchUrl).then(resp => { + this.resultsContainer.innerHTML = resp.data; + this.hideLoading(); + }) + } + + searchEntities(searchTerm) { + this.input.value = ''; + let url = this.searchUrl + `&term=${encodeURIComponent(searchTerm)}`; + window.$http.get(url).then(resp => { + this.resultsContainer.innerHTML = resp.data; + this.hideLoading(); + }); + } + + isDoubleClick() { + let now = Date.now(); + let answer = now - this.lastClick < 300; + this.lastClick = now; + return answer; + } + + onClick(event) { + let t = event.target; + console.log('click', t); + + if (t.matches('.entity-list-item *')) { + event.preventDefault(); + event.stopPropagation(); + let item = t.closest('[data-entity-type]'); + this.selectItem(item); + } else if (t.matches('[data-entity-type]')) { + this.selectItem(t) + } + + } + + selectItem(item) { + let isDblClick = this.isDoubleClick(); + let type = item.getAttribute('data-entity-type'); + let id = item.getAttribute('data-entity-id'); + let isSelected = !item.classList.contains('selected') || isDblClick; + + this.unselectAll(); + this.input.value = isSelected ? `${type}:${id}` : ''; + + if (!isSelected) window.$events.emit('entity-select-change', null); + if (isSelected) { + item.classList.add('selected'); + item.classList.add('primary-background'); + } + if (!isDblClick && !isSelected) return; + + let link = item.querySelector('.entity-list-item-link').getAttribute('href'); + let name = item.querySelector('.entity-list-item-name').textContent; + let data = {id: Number(id), name: name, link: link}; + + if (isDblClick) window.$events.emit('entity-select-confirm', data); + if (isSelected) window.$events.emit('entity-select-change', data); + } + + unselectAll() { + let selected = this.elem.querySelectorAll('.selected'); + for (let i = 0, len = selected.length; i < len; i++) { + selected[i].classList.remove('selected'); + selected[i].classList.remove('primary-background'); + } + } + +} + +module.exports = EntitySelector; \ 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..b0e8b84aa --- /dev/null +++ b/resources/assets/js/components/index.js @@ -0,0 +1,51 @@ + +let componentMapping = { + 'dropdown': require('./dropdown'), + 'overlay': require('./overlay'), + 'back-to-top': require('./back-top-top'), + 'notification': require('./notification'), + 'chapter-toggle': require('./chapter-toggle'), + 'expand-toggle': require('./expand-toggle'), + 'entity-selector-popup': require('./entity-selector-popup'), + 'entity-selector': require('./entity-selector'), + 'sidebar': require('./sidebar'), + 'page-picker': require('./page-picker'), + 'page-comments': require('./page-comments'), +}; + +window.components = {}; + +let componentNames = Object.keys(componentMapping); +initAll(); + +/** + * Initialize components of the given name within the given element. + * @param {String} componentName + * @param {HTMLElement|Document} parentElement + */ +function initComponent(componentName, parentElement) { + let elems = parentElement.querySelectorAll(`[${componentName}]`); + if (elems.length === 0) return; + + let component = componentMapping[componentName]; + if (typeof window.components[componentName] === "undefined") window.components[componentName] = []; + for (let j = 0, jLen = elems.length; j < jLen; j++) { + let instance = new component(elems[j]); + if (typeof elems[j].components === 'undefined') elems[j].components = {}; + elems[j].components[componentName] = instance; + window.components[componentName].push(instance); + } +} + +/** + * Initialize all components found within the given element. + * @param parentElement + */ +function initAll(parentElement) { + if (typeof parentElement === 'undefined') parentElement = document; + for (let i = 0, len = componentNames.length; i < len; i++) { + initComponent(componentNames[i], parentElement); + } +} + +window.components.init = initAll; \ 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..daef5bd6f --- /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/components/page-comments.js b/resources/assets/js/components/page-comments.js new file mode 100644 index 000000000..ae2a30ff5 --- /dev/null +++ b/resources/assets/js/components/page-comments.js @@ -0,0 +1,175 @@ +const MarkdownIt = require("markdown-it"); +const md = new MarkdownIt({ html: false }); + +class PageComments { + + constructor(elem) { + this.elem = elem; + this.pageId = Number(elem.getAttribute('page-id')); + this.editingComment = null; + this.parentId = null; + + this.container = elem.querySelector('[comment-container]'); + this.formContainer = elem.querySelector('[comment-form-container]'); + + if (this.formContainer) { + this.form = this.formContainer.querySelector('form'); + this.formInput = this.form.querySelector('textarea'); + this.form.addEventListener('submit', this.saveComment.bind(this)); + } + + this.elem.addEventListener('click', this.handleAction.bind(this)); + this.elem.addEventListener('submit', this.updateComment.bind(this)); + } + + handleAction(event) { + let actionElem = event.target.closest('[action]'); + if (event.target.matches('a[href^="#"]')) { + let id = event.target.href.split('#')[1]; + window.scrollAndHighlight(document.querySelector('#' + id)); + } + if (actionElem === null) return; + event.preventDefault(); + + let action = actionElem.getAttribute('action'); + if (action === 'edit') this.editComment(actionElem.closest('[comment]')); + if (action === 'closeUpdateForm') this.closeUpdateForm(); + if (action === 'delete') this.deleteComment(actionElem.closest('[comment]')); + if (action === 'addComment') this.showForm(); + if (action === 'hideForm') this.hideForm(); + if (action === 'reply') this.setReply(actionElem.closest('[comment]')); + if (action === 'remove-reply-to') this.removeReplyTo(); + } + + closeUpdateForm() { + if (!this.editingComment) return; + this.editingComment.querySelector('[comment-content]').style.display = 'block'; + this.editingComment.querySelector('[comment-edit-container]').style.display = 'none'; + } + + editComment(commentElem) { + this.hideForm(); + if (this.editingComment) this.closeUpdateForm(); + commentElem.querySelector('[comment-content]').style.display = 'none'; + commentElem.querySelector('[comment-edit-container]').style.display = 'block'; + let textArea = commentElem.querySelector('[comment-edit-container] textarea'); + let lineCount = textArea.value.split('\n').length; + textArea.style.height = (lineCount * 20) + 'px'; + this.editingComment = commentElem; + } + + updateComment(event) { + let form = event.target; + event.preventDefault(); + let text = form.querySelector('textarea').value; + let reqData = { + text: text, + html: md.render(text), + parent_id: this.parentId || null, + }; + this.showLoading(form); + let commentId = this.editingComment.getAttribute('comment'); + window.$http.put(window.baseUrl(`/ajax/comment/${commentId}`), reqData).then(resp => { + let newComment = document.createElement('div'); + newComment.innerHTML = resp.data; + this.editingComment.innerHTML = newComment.children[0].innerHTML; + window.$events.emit('success', window.trans('entities.comment_updated_success')); + window.components.init(this.editingComment); + this.closeUpdateForm(); + this.editingComment = null; + this.hideLoading(form); + }); + } + + deleteComment(commentElem) { + let id = commentElem.getAttribute('comment'); + this.showLoading(commentElem.querySelector('[comment-content]')); + window.$http.delete(window.baseUrl(`/ajax/comment/${id}`)).then(resp => { + commentElem.parentNode.removeChild(commentElem); + window.$events.emit('success', window.trans('entities.comment_deleted_success')); + this.updateCount(); + }); + } + + saveComment(event) { + event.preventDefault(); + event.stopPropagation(); + let text = this.formInput.value; + let reqData = { + text: text, + html: md.render(text), + parent_id: this.parentId || null, + }; + this.showLoading(this.form); + window.$http.post(window.baseUrl(`/ajax/page/${this.pageId}/comment`), reqData).then(resp => { + let newComment = document.createElement('div'); + newComment.innerHTML = resp.data; + let newElem = newComment.children[0]; + this.container.appendChild(newElem); + window.components.init(newElem); + window.$events.emit('success', window.trans('entities.comment_created_success')); + this.resetForm(); + this.updateCount(); + }); + } + + updateCount() { + let count = this.container.children.length; + this.elem.querySelector('[comments-title]').textContent = window.trans_choice('entities.comment_count', count, {count}); + } + + resetForm() { + this.formInput.value = ''; + this.formContainer.appendChild(this.form); + this.hideForm(); + this.removeReplyTo(); + this.hideLoading(this.form); + } + + showForm() { + this.formContainer.style.display = 'block'; + this.formContainer.parentNode.style.display = 'block'; + this.elem.querySelector('[comment-add-button]').style.display = 'none'; + this.formInput.focus(); + window.scrollToElement(this.formInput); + } + + hideForm() { + this.formContainer.style.display = 'none'; + this.formContainer.parentNode.style.display = 'none'; + this.elem.querySelector('[comment-add-button]').style.display = 'block'; + } + + setReply(commentElem) { + this.showForm(); + this.parentId = Number(commentElem.getAttribute('local-id')); + this.elem.querySelector('[comment-form-reply-to]').style.display = 'block'; + let replyLink = this.elem.querySelector('[comment-form-reply-to] a'); + replyLink.textContent = `#${this.parentId}`; + replyLink.href = `#comment${this.parentId}`; + } + + removeReplyTo() { + this.parentId = null; + this.elem.querySelector('[comment-form-reply-to]').style.display = 'none'; + } + + showLoading(formElem) { + let groups = formElem.querySelectorAll('.form-group'); + for (let i = 0, len = groups.length; i < len; i++) { + groups[i].style.display = 'none'; + } + formElem.querySelector('.form-group.loading').style.display = 'block'; + } + + hideLoading(formElem) { + let groups = formElem.querySelectorAll('.form-group'); + for (let i = 0, len = groups.length; i < len; i++) { + groups[i].style.display = 'block'; + } + formElem.querySelector('.form-group.loading').style.display = 'none'; + } + +} + +module.exports = PageComments; \ No newline at end of file diff --git a/resources/assets/js/components/page-picker.js b/resources/assets/js/components/page-picker.js new file mode 100644 index 000000000..e697d5f68 --- /dev/null +++ b/resources/assets/js/components/page-picker.js @@ -0,0 +1,60 @@ + +class PagePicker { + + constructor(elem) { + this.elem = elem; + this.input = elem.querySelector('input'); + this.resetButton = elem.querySelector('[page-picker-reset]'); + this.selectButton = elem.querySelector('[page-picker-select]'); + this.display = elem.querySelector('[page-picker-display]'); + this.defaultDisplay = elem.querySelector('[page-picker-default]'); + this.buttonSep = elem.querySelector('span.sep'); + + this.value = this.input.value; + this.setupListeners(); + } + + setupListeners() { + // Select click + this.selectButton.addEventListener('click', event => { + window.EntitySelectorPopup.show(entity => { + this.setValue(entity.id, entity.name); + }); + }); + + this.resetButton.addEventListener('click', event => { + this.setValue('', ''); + }); + } + + setValue(value, name) { + this.value = value; + this.input.value = value; + this.controlView(name); + } + + controlView(name) { + let hasValue = this.value && this.value !== 0; + toggleElem(this.resetButton, hasValue); + toggleElem(this.buttonSep, hasValue); + toggleElem(this.defaultDisplay, !hasValue); + toggleElem(this.display, hasValue); + if (hasValue) { + let id = this.getAssetIdFromVal(); + this.display.textContent = `#${id}, ${name}`; + this.display.href = window.baseUrl(`/link/${id}`); + } + } + + getAssetIdFromVal() { + return Number(this.value); + } + +} + +function toggleElem(elem, show) { + let display = (elem.tagName === 'BUTTON' || elem.tagName === 'SPAN') ? 'inline-block' : 'block'; + elem.style.display = show ? display : 'none'; +} + +module.exports = PagePicker; \ No newline at end of file diff --git a/resources/assets/js/components/sidebar.js b/resources/assets/js/components/sidebar.js new file mode 100644 index 000000000..fc801618e --- /dev/null +++ b/resources/assets/js/components/sidebar.js @@ -0,0 +1,16 @@ + +class Sidebar { + + constructor(elem) { + this.elem = elem; + this.toggleElem = elem.querySelector('.sidebar-toggle'); + this.toggleElem.addEventListener('click', this.toggle.bind(this)); + } + + toggle(show = true) { + this.elem.classList.toggle('open'); + } + +} + +module.exports = Sidebar; \ No newline at end of file diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 9337ea889..32ff76fa1 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) { @@ -394,285 +144,4 @@ module.exports = function (ngApp, events) { }; }]); - - ngApp.controller('PageTagController', ['$scope', '$http', '$attrs', - function ($scope, $http, $attrs) { - - const pageId = Number($attrs.pageId); - $scope.tags = []; - - $scope.sortOptions = { - handle: '.handle', - items: '> tr', - containment: "parent", - axis: "y" - }; - - /** - * Push an empty tag to the end of the scope tags. - */ - function addEmptyTag() { - $scope.tags.push({ - name: '', - value: '' - }); - } - $scope.addEmptyTag = addEmptyTag; - - /** - * Get all tags for the current book and add into scope. - */ - function getTags() { - let url = window.baseUrl(`/ajax/tags/get/page/${pageId}`); - $http.get(url).then((responseData) => { - $scope.tags = responseData.data; - addEmptyTag(); - }); - } - getTags(); - - /** - * Set the order property on all tags. - */ - function setTagOrder() { - for (let i = 0; i < $scope.tags.length; i++) { - $scope.tags[i].order = i; - } - } - - /** - * When an tag changes check if another empty editable - * field needs to be added onto the end. - * @param tag - */ - $scope.tagChange = function(tag) { - let cPos = $scope.tags.indexOf(tag); - if (cPos !== $scope.tags.length-1) return; - - if (tag.name !== '' || tag.value !== '') { - addEmptyTag(); - } - }; - - /** - * When an tag field loses focus check the tag to see if its - * empty and therefore could be removed from the list. - * @param tag - */ - $scope.tagBlur = function(tag) { - let isLast = $scope.tags.length - 1 === $scope.tags.indexOf(tag); - if (tag.name === '' && tag.value === '' && !isLast) { - let cPos = $scope.tags.indexOf(tag); - $scope.tags.splice(cPos, 1); - } - }; - - /** - * Remove a tag from the current list. - * @param tag - */ - $scope.removeTag = function(tag) { - let cIndex = $scope.tags.indexOf(tag); - $scope.tags.splice(cIndex, 1); - }; - - }]); - - - ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs', - function ($scope, $http, $attrs) { - - const pageId = $scope.uploadedTo = $attrs.pageId; - let currentOrder = ''; - $scope.files = []; - $scope.editFile = false; - $scope.file = getCleanFile(); - $scope.errors = { - link: {}, - edit: {} - }; - - function getCleanFile() { - return { - page_id: pageId - }; - } - - // Angular-UI-Sort options - $scope.sortOptions = { - handle: '.handle', - items: '> tr', - containment: "parent", - axis: "y", - stop: sortUpdate, - }; - - /** - * Event listener for sort changes. - * Updates the file ordering on the server. - * @param event - * @param ui - */ - function sortUpdate(event, ui) { - let newOrder = $scope.files.map(file => {return file.id}).join(':'); - if (newOrder === currentOrder) return; - - currentOrder = newOrder; - $http.put(window.baseUrl(`/attachments/sort/page/${pageId}`), {files: $scope.files}).then(resp => { - events.emit('success', resp.data.message); - }, checkError('sort')); - } - - /** - * Used by dropzone to get the endpoint to upload to. - * @returns {string} - */ - $scope.getUploadUrl = function (file) { - let suffix = (typeof file !== 'undefined') ? `/${file.id}` : ''; - return window.baseUrl(`/attachments/upload${suffix}`); - }; - - /** - * Get files for the current page from the server. - */ - function getFiles() { - let url = window.baseUrl(`/attachments/get/page/${pageId}`); - $http.get(url).then(resp => { - $scope.files = resp.data; - currentOrder = resp.data.map(file => {return file.id}).join(':'); - }, checkError('get')); - } - getFiles(); - - /** - * Runs on file upload, Adds an file to local file list - * and shows a success message to the user. - * @param file - * @param data - */ - $scope.uploadSuccess = function (file, data) { - $scope.$apply(() => { - $scope.files.push(data); - }); - events.emit('success', trans('entities.attachments_file_uploaded')); - }; - - /** - * Upload and overwrite an existing file. - * @param file - * @param data - */ - $scope.uploadSuccessUpdate = function (file, data) { - $scope.$apply(() => { - let search = filesIndexOf(data); - if (search !== -1) $scope.files[search] = data; - - if ($scope.editFile) { - $scope.editFile = angular.copy(data); - data.link = ''; - } - }); - events.emit('success', trans('entities.attachments_file_updated')); - }; - - /** - * Delete a file from the server and, on success, the local listing. - * @param file - */ - $scope.deleteFile = function(file) { - if (!file.deleting) { - file.deleting = true; - return; - } - $http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => { - events.emit('success', resp.data.message); - $scope.files.splice($scope.files.indexOf(file), 1); - }, checkError('delete')); - }; - - /** - * Attach a link to a page. - * @param file - */ - $scope.attachLinkSubmit = function(file) { - file.uploaded_to = pageId; - $http.post(window.baseUrl('/attachments/link'), file).then(resp => { - $scope.files.push(resp.data); - events.emit('success', trans('entities.attachments_link_attached')); - $scope.file = getCleanFile(); - }, checkError('link')); - }; - - /** - * Start the edit mode for a file. - * @param file - */ - $scope.startEdit = function(file) { - $scope.editFile = angular.copy(file); - $scope.editFile.link = (file.external) ? file.path : ''; - }; - - /** - * Cancel edit mode - */ - $scope.cancelEdit = function() { - $scope.editFile = false; - }; - - /** - * Update the name and link of a file. - * @param file - */ - $scope.updateFile = function(file) { - $http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => { - let search = filesIndexOf(resp.data); - if (search !== -1) $scope.files[search] = resp.data; - - if ($scope.editFile && !file.external) { - $scope.editFile.link = ''; - } - $scope.editFile = false; - events.emit('success', trans('entities.attachments_updated_success')); - }, checkError('edit')); - }; - - /** - * Get the url of a file. - */ - $scope.getFileUrl = function(file) { - return window.baseUrl('/attachments/' + file.id); - }; - - /** - * Search the local files via another file object. - * Used to search via object copies. - * @param file - * @returns int - */ - function filesIndexOf(file) { - for (let i = 0; i < $scope.files.length; i++) { - if ($scope.files[i].id == file.id) return i; - } - return -1; - } - - /** - * Check for an error response in a ajax request. - * @param errorGroupName - */ - function checkError(errorGroupName) { - $scope.errors[errorGroupName] = {}; - return function(response) { - if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') { - events.emit('error', response.data.error); - } - if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') { - $scope.errors[errorGroupName] = response.data.validation; - console.log($scope.errors[errorGroupName]) - } - } - } - - }]); - }; diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 8d7d89cee..08b82357f 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -1,158 +1,10 @@ "use strict"; -const DropZone = require("dropzone"); const MarkdownIt = require("markdown-it"); const mdTasksLists = require('markdown-it-task-lists'); const code = require('./code'); module.exports = function (ngApp, events) { - /** - * Common tab controls using simple jQuery functions. - */ - ngApp.directive('tabContainer', function() { - return { - restrict: 'A', - link: function (scope, element, attrs) { - const $content = element.find('[tab-content]'); - const $buttons = element.find('[tab-button]'); - - if (attrs.tabContainer) { - let initial = attrs.tabContainer; - $buttons.filter(`[tab-button="${initial}"]`).addClass('selected'); - $content.hide().filter(`[tab-content="${initial}"]`).show(); - } else { - $content.hide().first().show(); - $buttons.first().addClass('selected'); - } - - $buttons.click(function() { - let clickedTab = $(this); - $buttons.removeClass('selected'); - $content.hide(); - let name = clickedTab.addClass('selected').attr('tab-button'); - $content.filter(`[tab-content="${name}"]`).show(); - }); - } - }; - }); - - /** - * Sub form component to allow inner-form sections to act like their own forms. - */ - ngApp.directive('subForm', function() { - return { - restrict: 'A', - link: function (scope, element, attrs) { - element.on('keypress', e => { - if (e.keyCode === 13) { - submitEvent(e); - } - }); - - element.find('button[type="submit"]').click(submitEvent); - - function submitEvent(e) { - e.preventDefault(); - if (attrs.subForm) scope.$eval(attrs.subForm); - } - } - }; - }); - - /** - * DropZone - * Used for uploading images - */ - ngApp.directive('dropZone', [function () { - return { - restrict: 'E', - template: ` -
-
{{message}}
-
- `, - scope: { - uploadUrl: '@', - eventSuccess: '=', - eventError: '=', - uploadedTo: '@', - }, - link: function (scope, element, attrs) { - scope.message = attrs.message; - if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder; - let dropZone = new DropZone(element[0].querySelector('.dropzone-container'), { - url: scope.uploadUrl, - init: function () { - let dz = this; - dz.on('sending', function (file, xhr, data) { - let token = window.document.querySelector('meta[name=token]').getAttribute('content'); - data.append('_token', token); - let uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo; - data.append('uploaded_to', uploadedTo); - }); - if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess); - dz.on('success', function (file, data) { - $(file.previewElement).fadeOut(400, function () { - dz.removeFile(file); - }); - }); - if (typeof scope.eventError !== 'undefined') dz.on('error', scope.eventError); - dz.on('error', function (file, errorMessage, xhr) { - console.log(errorMessage); - console.log(xhr); - function setMessage(message) { - $(file.previewElement).find('[data-dz-errormessage]').text(message); - } - - if (xhr.status === 413) setMessage(trans('errors.server_upload_limit')); - if (errorMessage.file) setMessage(errorMessage.file[0]); - - }); - } - }); - } - }; - }]); - - /** - * Dropdown - * Provides some simple logic to create small dropdown menus - */ - ngApp.directive('dropdown', [function () { - return { - restrict: 'A', - link: function (scope, element, attrs) { - const menu = element.find('ul'); - - function hide() { - menu.hide(); - menu.removeClass('anim menuIn'); - } - - function show() { - menu.show().addClass('anim menuIn'); - element.mouseleave(hide); - - // Focus on input if exist in dropdown and hide on enter press - let inputs = menu.find('input'); - if (inputs.length > 0) inputs.first().focus(); - } - - // Hide menu on option click - element.on('click', '> ul a', hide); - // Show dropdown on toggle click. - element.find('[dropdown-toggle]').on('click', show); - // Hide menu on enter press in inputs - element.on('keypress', 'input', event => { - if (event.keyCode !== 13) return true; - event.preventDefault(); - hide(); - return false; - }); - } - }; - }]); - /** * TinyMCE * An angular wrapper around the tinyMCE editor. @@ -168,7 +20,7 @@ module.exports = function (ngApp, events) { link: function (scope, element, attrs) { function tinyMceSetup(editor) { - editor.on('ExecCommand change NodeChange ObjectResized', (e) => { + editor.on('ExecCommand change input NodeChange ObjectResized', (e) => { let content = editor.getContent(); $timeout(() => { scope.mceModel = content; @@ -177,7 +29,10 @@ module.exports = function (ngApp, events) { }); editor.on('keydown', (event) => { - scope.$emit('editor-keydown', event); + if (event.keyCode === 83 && (navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey)) { + event.preventDefault(); + scope.$emit('save-draft', event); + } }); editor.on('init', (e) => { @@ -247,7 +102,7 @@ module.exports = function (ngApp, events) { extraKeys[`${metaKey}-7`] = function(cm) {wrapSelection('\n```\n', '\n```');}; extraKeys[`${metaKey}-8`] = function(cm) {wrapSelection('`', '`');}; extraKeys[`Shift-${metaKey}-E`] = function(cm) {wrapSelection('`', '`');}; - extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('

', '');}; + extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('

', '

');}; cm.setOption('extraKeys', extraKeys); // Update data on content change @@ -341,12 +196,13 @@ module.exports = function (ngApp, events) { } cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen}); - cm.setCursor({line: cursor.line, ch: cursor.ch + (newLineContent.length - lineLen)}); + cm.setCursor({line: cursor.line, ch: cursor.ch + start.length}); } function wrapSelection(start, end) { let selection = cm.getSelection(); if (selection === '') return wrapLine(start, end); + let newSelection = selection; let frontDiff = 0; let endDiff = 0; @@ -400,7 +256,7 @@ module.exports = function (ngApp, events) { // Show the popup link selector and insert a link when finished function showLinkSelector() { let cursorPos = cm.getCursor('from'); - window.showEntityLinkSelector(entity => { + window.EntitySelectorPopup.show(entity => { let selectedText = cm.getSelection() || entity.name; let newText = `[${selectedText}](${entity.link})`; cm.focus(); @@ -422,7 +278,7 @@ module.exports = function (ngApp, events) { // Show the image manager and handle image insertion function showImageManager() { let cursorPos = cm.getCursor('from'); - window.ImageManager.showExternal(image => { + window.ImageManager.show(image => { let selectedText = cm.getSelection(); let newText = "![" + (selectedText || image.name) + "](" + image.thumbs.display + ")"; cm.focus(); @@ -534,333 +390,4 @@ module.exports = function (ngApp, events) { } } }]); - - /** - * Tag Autosuggestions - * Listens to child inputs and provides autosuggestions depending on field type - * and input. Suggestions provided by server. - */ - ngApp.directive('tagAutosuggestions', ['$http', function ($http) { - return { - restrict: 'A', - link: function (scope, elem, attrs) { - - // Local storage for quick caching. - const localCache = {}; - - // Create suggestion element - const suggestionBox = document.createElement('ul'); - suggestionBox.className = 'suggestion-box'; - suggestionBox.style.position = 'absolute'; - suggestionBox.style.display = 'none'; - const $suggestionBox = $(suggestionBox); - - // General state tracking - let isShowing = false; - let currentInput = false; - let active = 0; - - // Listen to input events on autosuggest fields - elem.on('input focus', '[autosuggest]', function (event) { - let $input = $(this); - let val = $input.val(); - let url = $input.attr('autosuggest'); - let type = $input.attr('autosuggest-type'); - - // Add name param to request if for a value - if (type.toLowerCase() === 'value') { - let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first(); - let nameVal = $nameInput.val(); - if (nameVal !== '') { - url += '?name=' + encodeURIComponent(nameVal); - } - } - - let suggestionPromise = getSuggestions(val.slice(0, 3), url); - suggestionPromise.then(suggestions => { - if (val.length === 0) { - displaySuggestions($input, suggestions.slice(0, 6)); - } else { - suggestions = suggestions.filter(item => { - return item.toLowerCase().indexOf(val.toLowerCase()) !== -1; - }).slice(0, 4); - displaySuggestions($input, suggestions); - } - }); - }); - - // Hide autosuggestions when input loses focus. - // Slight delay to allow clicks. - let lastFocusTime = 0; - elem.on('blur', '[autosuggest]', function (event) { - let startTime = Date.now(); - setTimeout(() => { - if (lastFocusTime < startTime) { - $suggestionBox.hide(); - isShowing = false; - } - }, 200) - }); - elem.on('focus', '[autosuggest]', function (event) { - lastFocusTime = Date.now(); - }); - - elem.on('keydown', '[autosuggest]', function (event) { - if (!isShowing) return; - - let suggestionElems = suggestionBox.childNodes; - let suggestCount = suggestionElems.length; - - // Down arrow - if (event.keyCode === 40) { - let newActive = (active === suggestCount - 1) ? 0 : active + 1; - changeActiveTo(newActive, suggestionElems); - } - // Up arrow - else if (event.keyCode === 38) { - let newActive = (active === 0) ? suggestCount - 1 : active - 1; - changeActiveTo(newActive, suggestionElems); - } - // Enter or tab key - else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) { - currentInput[0].value = suggestionElems[active].textContent; - currentInput.focus(); - $suggestionBox.hide(); - isShowing = false; - if (event.keyCode === 13) { - event.preventDefault(); - return false; - } - } - }); - - // Change the active suggestion to the given index - function changeActiveTo(index, suggestionElems) { - suggestionElems[active].className = ''; - active = index; - suggestionElems[active].className = 'active'; - } - - // Display suggestions on a field - let prevSuggestions = []; - - function displaySuggestions($input, suggestions) { - - // Hide if no suggestions - if (suggestions.length === 0) { - $suggestionBox.hide(); - isShowing = false; - prevSuggestions = suggestions; - return; - } - - // Otherwise show and attach to input - if (!isShowing) { - $suggestionBox.show(); - isShowing = true; - } - if ($input !== currentInput) { - $suggestionBox.detach(); - $input.after($suggestionBox); - currentInput = $input; - } - - // Return if no change - if (prevSuggestions.join() === suggestions.join()) { - prevSuggestions = suggestions; - return; - } - - // Build suggestions - $suggestionBox[0].innerHTML = ''; - for (let i = 0; i < suggestions.length; i++) { - let suggestion = document.createElement('li'); - suggestion.textContent = suggestions[i]; - suggestion.onclick = suggestionClick; - if (i === 0) { - suggestion.className = 'active'; - active = 0; - } - $suggestionBox[0].appendChild(suggestion); - } - - prevSuggestions = suggestions; - } - - // Suggestion click event - function suggestionClick(event) { - currentInput[0].value = this.textContent; - currentInput.focus(); - $suggestionBox.hide(); - isShowing = false; - } - - // Get suggestions & cache - function getSuggestions(input, url) { - let hasQuery = url.indexOf('?') !== -1; - let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input); - - // Get from local cache if exists - if (typeof localCache[searchUrl] !== 'undefined') { - return new Promise((resolve, reject) => { - resolve(localCache[searchUrl]); - }); - } - - return $http.get(searchUrl).then(response => { - localCache[searchUrl] = response.data; - return response.data; - }); - } - - } - } - }]); - - ngApp.directive('entityLinkSelector', [function($http) { - return { - restrict: 'A', - link: function(scope, element, attrs) { - - const selectButton = element.find('.entity-link-selector-confirm'); - let callback = false; - let entitySelection = null; - - // Handle entity selection change, Stores the selected entity locally - function entitySelectionChange(entity) { - entitySelection = entity; - if (entity === null) { - selectButton.attr('disabled', 'true'); - } else { - selectButton.removeAttr('disabled'); - } - } - events.listen('entity-select-change', entitySelectionChange); - - // Handle selection confirm button click - selectButton.click(event => { - hide(); - if (entitySelection !== null) callback(entitySelection); - }); - - // Show selector interface - function show() { - element.fadeIn(240); - } - - // Hide selector interface - function hide() { - element.fadeOut(240); - } - - // Listen to confirmation of entity selections (doubleclick) - events.listen('entity-select-confirm', entity => { - hide(); - callback(entity); - }); - - // Show entity selector, Accessible globally, and store the callback - window.showEntityLinkSelector = function(passedCallback) { - show(); - callback = passedCallback; - }; - - } - }; - }]); - - - ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) { - return { - restrict: 'A', - scope: true, - link: function (scope, element, attrs) { - scope.loading = true; - scope.entityResults = false; - scope.search = ''; - - // Add input for forms - const input = element.find('[entity-selector-input]').first(); - - // Detect double click events - let lastClick = 0; - function isDoubleClick() { - let now = Date.now(); - let answer = now - lastClick < 300; - lastClick = now; - return answer; - } - - // Listen to entity item clicks - element.on('click', '.entity-list a', function(event) { - event.preventDefault(); - event.stopPropagation(); - let item = $(this).closest('[data-entity-type]'); - itemSelect(item, isDoubleClick()); - }); - element.on('click', '[data-entity-type]', function(event) { - itemSelect($(this), isDoubleClick()); - }); - - // Select entity action - function itemSelect(item, doubleClick) { - let entityType = item.attr('data-entity-type'); - let entityId = item.attr('data-entity-id'); - let isSelected = !item.hasClass('selected') || doubleClick; - element.find('.selected').removeClass('selected').removeClass('primary-background'); - if (isSelected) item.addClass('selected').addClass('primary-background'); - let newVal = isSelected ? `${entityType}:${entityId}` : ''; - input.val(newVal); - - if (!isSelected) { - events.emit('entity-select-change', null); - } - - if (!doubleClick && !isSelected) return; - - let link = item.find('.entity-list-item-link').attr('href'); - let name = item.find('.entity-list-item-name').text(); - - if (doubleClick) { - events.emit('entity-select-confirm', { - id: Number(entityId), - name: name, - link: link - }); - } - - if (isSelected) { - events.emit('entity-select-change', { - id: Number(entityId), - name: name, - link: link - }); - } - } - - // Get search url with correct types - function getSearchUrl() { - let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter'); - return window.baseUrl(`/ajax/search/entities?types=${types}`); - } - - // Get initial contents - $http.get(getSearchUrl()).then(resp => { - scope.entityResults = $sce.trustAsHtml(resp.data); - scope.loading = false; - }); - - // Search when typing - scope.searchEntities = function() { - scope.loading = true; - input.val(''); - let url = getSearchUrl() + '&term=' + encodeURIComponent(scope.search); - $http.get(url).then(resp => { - scope.entityResults = $sce.trustAsHtml(resp.data); - scope.loading = false; - }); - }; - } - }; - }]); }; diff --git a/resources/assets/js/dom-polyfills.js b/resources/assets/js/dom-polyfills.js new file mode 100644 index 000000000..fcd89b766 --- /dev/null +++ b/resources/assets/js/dom-polyfills.js @@ -0,0 +1,20 @@ +/** + * Polyfills for DOM API's + */ + +if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; +} + +if (!Element.prototype.closest) { + Element.prototype.closest = function (s) { + var el = this; + var ancestor = this; + if (!document.documentElement.contains(el)) return null; + do { + if (ancestor.matches(s)) return ancestor; + ancestor = ancestor.parentElement; + } while (ancestor !== null); + return null; + }; +} \ No newline at end of file diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 2ef062a5b..7126479c1 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -1,4 +1,6 @@ "use strict"; +require("babel-polyfill"); +require('./dom-polyfills'); // Url retrieval function window.baseUrl = function(path) { @@ -8,44 +10,15 @@ window.baseUrl = function(path) { return basePath + '/' + path; }; -const Vue = require("vue"); -const axios = require("axios"); - -let axiosInstance = axios.create({ - headers: { - 'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'), - 'baseURL': window.baseUrl('') - } -}); -window.$http = axiosInstance; - -Vue.prototype.$http = axiosInstance; - -require("./vues/vues"); - - -// AngularJS - Create application and load components -const angular = require("angular"); -require("angular-resource"); -require("angular-animate"); -require("angular-sanitize"); -require("angular-ui-sortable"); - -let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']); - -// Translation setup -// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system -const Translations = require("./translations"); -let translator = new Translations(window.translations); -window.trans = translator.get.bind(translator); - // Global Event System class EventManager { constructor() { this.listeners = {}; + this.stack = []; } emit(eventName, eventData) { + this.stack.push({name: eventName, data: eventData}); if (typeof this.listeners[eventName] === 'undefined') return this; let eventsToStart = this.listeners[eventName]; for (let i = 0; i < eventsToStart.length; i++) { @@ -62,25 +35,95 @@ class EventManager { } } -window.Events = new EventManager(); -Vue.prototype.$events = window.Events; +window.$events = new EventManager(); + +const Vue = require("vue"); +const axios = require("axios"); + +let axiosInstance = axios.create({ + headers: { + 'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'), + 'baseURL': window.baseUrl('') + } +}); +axiosInstance.interceptors.request.use(resp => { + return resp; +}, err => { + if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err); + if (typeof err.response.data.error !== "undefined") window.$events.emit('error', err.response.data.error); + if (typeof err.response.data.message !== "undefined") window.$events.emit('error', err.response.data.message); +}); +window.$http = axiosInstance; + +Vue.prototype.$http = axiosInstance; +Vue.prototype.$events = window.$events; + + +// AngularJS - Create application and load components +const angular = require("angular"); +require("angular-resource"); +require("angular-animate"); +require("angular-sanitize"); +require("angular-ui-sortable"); + +let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']); + +// Translation setup +// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system +const Translations = require("./translations"); +let translator = new Translations(window.translations); +window.trans = translator.get.bind(translator); +window.trans_choice = translator.getPlural.bind(translator); + + +require("./vues/vues"); +require("./components"); // Load in angular specific items -const Services = require('./services'); const Directives = require('./directives'); const Controllers = require('./controllers'); -Services(ngApp, window.Events); -Directives(ngApp, window.Events); -Controllers(ngApp, window.Events); +Directives(ngApp, window.$events); +Controllers(ngApp, window.$events); //Global jQuery Config & Extensions +/** + * Scroll the view to a specific element. + * @param {HTMLElement} element + */ +window.scrollToElement = function(element) { + if (!element) return; + let offset = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; + let top = element.getBoundingClientRect().top + offset; + $('html, body').animate({ + scrollTop: top - 60 // Adjust to change final scroll position top margin + }, 300); +}; + +/** + * Scroll and highlight an element. + * @param {HTMLElement} element + */ +window.scrollAndHighlight = function(element) { + if (!element) return; + window.scrollToElement(element); + let color = document.getElementById('custom-styles').getAttribute('data-color-light'); + let initColor = window.getComputedStyle(element).getPropertyValue('background-color'); + element.style.backgroundColor = color; + setTimeout(() => { + element.classList.add('selectFade'); + element.style.backgroundColor = initColor; + }, 10); + setTimeout(() => { + element.classList.remove('selectFade'); + element.style.backgroundColor = ''; + }, 3000); +}; + // Smooth scrolling jQuery.fn.smoothScrollTo = function () { if (this.length === 0) return; - $('html, body').animate({ - scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin - }, 300); // Adjust to change animations speed (ms) + window.scrollToElement(this[0]); return this; }; @@ -91,83 +134,11 @@ jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) { }; }); -// Global jQuery Elements -let notifications = $('.notification'); -let successNotification = notifications.filter('.pos'); -let errorNotification = notifications.filter('.neg'); -let warningNotification = notifications.filter('.warning'); -// Notification Events -window.Events.listen('success', function (text) { - successNotification.hide(); - successNotification.find('span').text(text); - setTimeout(() => { - successNotification.show(); - }, 1); -}); -window.Events.listen('warning', function (text) { - warningNotification.find('span').text(text); - warningNotification.show(); -}); -window.Events.listen('error', function (text) { - errorNotification.find('span').text(text); - errorNotification.show(); -}); - -// Notification hiding -notifications.click(function () { - $(this).fadeOut(100); -}); - -// Chapter page list toggles -$('.chapter-toggle').click(function (e) { - e.preventDefault(); - $(this).toggleClass('open'); - $(this).closest('.chapter').find('.inset-list').slideToggle(180); -}); - -// Back to top button -$('#back-to-top').click(function() { - $('#header').smoothScrollTo(); -}); -let scrollTopShowing = false; -let scrollTop = document.getElementById('back-to-top'); -let scrollTopBreakpoint = 1200; -window.addEventListener('scroll', function() { - let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0; - if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) { - scrollTop.style.display = 'block'; - scrollTopShowing = true; - setTimeout(() => { - scrollTop.style.opacity = 0.4; - }, 1); - } else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) { - scrollTop.style.opacity = 0; - scrollTopShowing = false; - setTimeout(() => { - scrollTop.style.display = 'none'; - }, 500); - } -}); - -// Common jQuery actions -$('[data-action="expand-entity-list-details"]').click(function() { - $('.entity-list.compact').find('p').not('.empty-text').slideToggle(240); -}); - -// Popup close -$('.popup-close').click(function() { - $(this).closest('.overlay').fadeOut(240); -}); -$('.overlay').click(function(event) { - if (!$(event.target).hasClass('overlay')) return; - $(this).fadeOut(240); -}); - // Detect IE for css if(navigator.userAgent.indexOf('MSIE')!==-1 || navigator.appVersion.indexOf('Trident/') > 0 || navigator.userAgent.indexOf('Safari') !== -1){ - $('body').addClass('flexbox-support'); + document.body.classList.add('flexbox-support'); } // Page specific items diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 4f4c1fbe0..497dc0212 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -274,7 +274,7 @@ module.exports = function() { file_browser_callback: function (field_name, url, type, win) { if (type === 'file') { - window.showEntityLinkSelector(function(entity) { + window.EntitySelectorPopup.show(function(entity) { let originalField = win.document.getElementById(field_name); originalField.value = entity.link; $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name); @@ -283,7 +283,7 @@ module.exports = function() { if (type === 'image') { // Show image manager - window.ImageManager.showExternal(function (image) { + window.ImageManager.show(function (image) { // Set popover link input to image url then fire change event // to ensure the new value sticks @@ -365,7 +365,7 @@ module.exports = function() { icon: 'image', tooltip: 'Insert an image', onclick: function () { - window.ImageManager.showExternal(function (image) { + window.ImageManager.show(function (image) { let html = ``; html += `${image.name}`; html += ''; diff --git a/resources/assets/js/pages/page-show.js b/resources/assets/js/pages/page-show.js index 67d339d63..14437cd68 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'); @@ -83,15 +81,7 @@ let setupPageShow = window.setupPageShow = function (pageId) { let idElem = document.getElementById(text); $('.page-content [data-highlighted]').attr('data-highlighted', '').css('background-color', ''); if (idElem !== null) { - let $idElem = $(idElem); - let color = $('#custom-styles').attr('data-color-light'); - $idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo(); - setTimeout(() => { - $idElem.addClass('anim').addClass('selectFade').css('background-color', ''); - setTimeout(() => { - $idElem.removeClass('selectFade'); - }, 3000); - }, 100); + window.scrollAndHighlight(idElem); } else { $('.page-content').find(':contains("' + text + '")').smoothScrollTo(); } @@ -108,25 +98,25 @@ let setupPageShow = window.setupPageShow = function (pageId) { goToText(event.target.getAttribute('href').substr(1)); }); - // Make the book-tree sidebar stick in view on scroll + // Make the sidebar stick in view on scroll let $window = $(window); - let $bookTree = $(".book-tree"); - let $bookTreeParent = $bookTree.parent(); + let $sidebar = $("#sidebar .scroll-body"); + let $bookTreeParent = $sidebar.parent(); // Check the page is scrollable and the content is taller than the tree - let pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height()); + let pageScrollable = ($(document).height() > $window.height()) && ($sidebar.height() < $('.page-content').height()); // Get current tree's width and header height let headerHeight = $("#header").height() + $(".toolbar").height(); let isFixed = $window.scrollTop() > headerHeight; // Function to fix the tree as a sidebar function stickTree() { - $bookTree.width($bookTreeParent.width() + 15); - $bookTree.addClass("fixed"); + $sidebar.width($bookTreeParent.width() + 15); + $sidebar.addClass("fixed"); isFixed = true; } // Function to un-fix the tree back into position function unstickTree() { - $bookTree.css('width', 'auto'); - $bookTree.removeClass("fixed"); + $sidebar.css('width', 'auto'); + $sidebar.removeClass("fixed"); isFixed = false; } // Checks if the tree stickiness state should change @@ -160,7 +150,6 @@ let setupPageShow = window.setupPageShow = function (pageId) { unstickTree(); } }); - }; module.exports = setupPageShow; \ 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/translations.js b/resources/assets/js/translations.js index ca6a7bd29..70ebfc255 100644 --- a/resources/assets/js/translations.js +++ b/resources/assets/js/translations.js @@ -20,9 +20,63 @@ class Translator { * @returns {*} */ get(key, replacements) { + let text = this.getTransText(key); + return this.performReplacements(text, replacements); + } + + /** + * Get pluralised text, Dependant on the given count. + * Same format at laravel's 'trans_choice' helper. + * @param key + * @param count + * @param replacements + * @returns {*} + */ + getPlural(key, count, replacements) { + let text = this.getTransText(key); + let splitText = text.split('|'); + let result = null; + let exactCountRegex = /^{([0-9]+)}/; + let rangeRegex = /^\[([0-9]+),([0-9*]+)]/; + + for (let i = 0, len = splitText.length; i < len; i++) { + let t = splitText[i]; + + // Parse exact matches + let exactMatches = t.match(exactCountRegex); + if (exactMatches !== null && Number(exactMatches[1]) === count) { + result = t.replace(exactCountRegex, '').trim(); + break; + } + + // Parse range matches + let rangeMatches = t.match(rangeRegex); + if (rangeMatches !== null) { + let rangeStart = Number(rangeMatches[1]); + if (rangeStart <= count && (rangeMatches[2] === '*' || Number(rangeMatches[2]) >= count)) { + result = t.replace(rangeRegex, '').trim(); + break; + } + } + } + + if (result === null && splitText.length > 1) { + result = (count === 1) ? splitText[0] : splitText[1]; + } + + if (result === null) result = splitText[0]; + return this.performReplacements(result, replacements); + } + + /** + * Fetched translation text from the store for the given key. + * @param key + * @returns {String|Object} + */ + getTransText(key) { let splitKey = key.split('.'); let value = splitKey.reduce((a, b) => { - return a != undefined ? a[b] : a; + return a !== undefined ? a[b] : a; }, this.store); if (value === undefined) { @@ -30,16 +84,25 @@ class Translator { value = key; } - if (replacements === undefined) return value; + return value; + } - let replaceMatches = value.match(/:([\S]+)/g); - if (replaceMatches === null) return value; + /** + * Perform replacements on a string. + * @param {String} string + * @param {Object} replacements + * @returns {*} + */ + performReplacements(string, replacements) { + if (!replacements) return string; + let replaceMatches = string.match(/:([\S]+)/g); + if (replaceMatches === null) return string; replaceMatches.forEach(match => { let key = match.substring(1); if (typeof replacements[key] === 'undefined') return; - value = value.replace(match, replacements[key]); + string = string.replace(match, replacements[key]); }); - return value; + return string; } } diff --git a/resources/assets/js/vues/attachment-manager.js b/resources/assets/js/vues/attachment-manager.js new file mode 100644 index 000000000..635622b93 --- /dev/null +++ b/resources/assets/js/vues/attachment-manager.js @@ -0,0 +1,138 @@ +const draggable = require('vuedraggable'); +const dropzone = require('./components/dropzone'); + +function mounted() { + this.pageId = this.$el.getAttribute('page-id'); + this.file = this.newFile(); + + this.$http.get(window.baseUrl(`/attachments/get/page/${this.pageId}`)).then(resp => { + this.files = resp.data; + }).catch(err => { + this.checkValidationErrors('get', err); + }); +} + +let data = { + pageId: null, + files: [], + fileToEdit: null, + file: {}, + tab: 'list', + editTab: 'file', + errors: {link: {}, edit: {}, delete: {}} +}; + +const components = {dropzone, draggable}; + +let methods = { + + newFile() { + return {page_id: this.pageId}; + }, + + getFileUrl(file) { + return window.baseUrl(`/attachments/${file.id}`); + }, + + fileSortUpdate() { + this.$http.put(window.baseUrl(`/attachments/sort/page/${this.pageId}`), {files: this.files}).then(resp => { + this.$events.emit('success', resp.data.message); + }).catch(err => { + this.checkValidationErrors('sort', err); + }); + }, + + startEdit(file) { + this.fileToEdit = Object.assign({}, file); + this.fileToEdit.link = file.external ? file.path : ''; + this.editTab = file.external ? 'link' : 'file'; + }, + + deleteFile(file) { + if (!file.deleting) return file.deleting = true; + + this.$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => { + this.$events.emit('success', resp.data.message); + this.files.splice(this.files.indexOf(file), 1); + }).catch(err => { + this.checkValidationErrors('delete', err) + }); + }, + + uploadSuccess(upload) { + this.files.push(upload.data); + this.$events.emit('success', trans('entities.attachments_file_uploaded')); + }, + + uploadSuccessUpdate(upload) { + let fileIndex = this.filesIndex(upload.data); + if (fileIndex === -1) { + this.files.push(upload.data) + } else { + this.files.splice(fileIndex, 1, upload.data); + } + + if (this.fileToEdit && this.fileToEdit.id === upload.data.id) { + this.fileToEdit = Object.assign({}, upload.data); + } + this.$events.emit('success', trans('entities.attachments_file_updated')); + }, + + checkValidationErrors(groupName, err) { + console.error(err); + if (typeof err.response.data === "undefined" && typeof err.response.data.validation === "undefined") return; + this.errors[groupName] = err.response.data.validation; + console.log(this.errors[groupName]); + }, + + getUploadUrl(file) { + let url = window.baseUrl(`/attachments/upload`); + if (typeof file !== 'undefined') url += `/${file.id}`; + return url; + }, + + cancelEdit() { + this.fileToEdit = null; + }, + + attachNewLink(file) { + file.uploaded_to = this.pageId; + this.$http.post(window.baseUrl('/attachments/link'), file).then(resp => { + this.files.push(resp.data); + this.file = this.newFile(); + this.$events.emit('success', trans('entities.attachments_link_attached')); + }).catch(err => { + this.checkValidationErrors('link', err); + }); + }, + + updateFile(file) { + $http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => { + let search = this.filesIndex(resp.data); + if (search === -1) { + this.files.push(resp.data); + } else { + this.files.splice(search, 1, resp.data); + } + + if (this.fileToEdit && !file.external) this.fileToEdit.link = ''; + this.fileToEdit = false; + + this.$events.emit('success', trans('entities.attachments_updated_success')); + }).catch(err => { + this.checkValidationErrors('edit', err); + }); + }, + + filesIndex(file) { + for (let i = 0, len = this.files.length; i < len; i++) { + if (this.files[i].id === file.id) return i; + } + return -1; + } + +}; + +module.exports = { + data, methods, mounted, components, +}; \ No newline at end of file diff --git a/resources/assets/js/vues/components/autosuggest.js b/resources/assets/js/vues/components/autosuggest.js new file mode 100644 index 000000000..4d6b97e55 --- /dev/null +++ b/resources/assets/js/vues/components/autosuggest.js @@ -0,0 +1,130 @@ + +const template = ` +
+ + +
+ +`; + +function data() { + return { + suggestions: [], + showSuggestions: false, + active: 0, + }; +} + +const ajaxCache = {}; + +const props = ['url', 'type', 'value', 'placeholder', 'name']; + +function getNameInputVal(valInput) { + let parentRow = valInput.parentNode.parentNode; + let nameInput = parentRow.querySelector('[autosuggest-type="name"]'); + return (nameInput === null) ? '' : nameInput.value; +} + +const methods = { + + inputUpdate(inputValue) { + this.$emit('input', inputValue); + let params = {}; + + if (this.type === 'value') { + let nameVal = getNameInputVal(this.$el); + if (nameVal !== "") params.name = nameVal; + } + + this.getSuggestions(inputValue.slice(0, 3), params).then(suggestions => { + if (inputValue.length === 0) { + this.displaySuggestions(suggestions.slice(0, 6)); + return; + } + // Filter to suggestions containing searched term + suggestions = suggestions.filter(item => { + return item.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1; + }).slice(0, 4); + this.displaySuggestions(suggestions); + }); + }, + + inputBlur() { + setTimeout(() => { + this.$emit('blur'); + this.showSuggestions = false; + }, 100); + }, + + inputKeydown(event) { + if (event.keyCode === 13) event.preventDefault(); + if (!this.showSuggestions) return; + + // Down arrow + if (event.keyCode === 40) { + this.active = (this.active === this.suggestions.length - 1) ? 0 : this.active+1; + } + // Up Arrow + else if (event.keyCode === 38) { + this.active = (this.active === 0) ? this.suggestions.length - 1 : this.active-1; + } + // Enter or tab keys + else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) { + this.selectSuggestion(this.suggestions[this.active]); + } + // Escape key + else if (event.keyCode === 27) { + this.showSuggestions = false; + } + }, + + displaySuggestions(suggestions) { + if (suggestions.length === 0) { + this.suggestions = []; + this.showSuggestions = false; + return; + } + + this.suggestions = suggestions; + this.showSuggestions = true; + this.active = 0; + }, + + selectSuggestion(suggestion) { + this.$refs.input.value = suggestion; + this.$refs.input.focus(); + this.$emit('input', suggestion); + this.showSuggestions = false; + }, + + /** + * Get suggestions from BookStack. Store and use local cache if already searched. + * @param {String} input + * @param {Object} params + */ + getSuggestions(input, params) { + params.search = input; + let cacheKey = `${this.url}:${JSON.stringify(params)}`; + + if (typeof ajaxCache[cacheKey] !== "undefined") return Promise.resolve(ajaxCache[cacheKey]); + + return this.$http.get(this.url, {params}).then(resp => { + ajaxCache[cacheKey] = resp.data; + return resp.data; + }); + } + +}; + +const computed = []; + +module.exports = {template, data, props, methods, computed}; \ No newline at end of file diff --git a/resources/assets/js/vues/components/dropzone.js b/resources/assets/js/vues/components/dropzone.js new file mode 100644 index 000000000..0f31bd579 --- /dev/null +++ b/resources/assets/js/vues/components/dropzone.js @@ -0,0 +1,60 @@ +const DropZone = require("dropzone"); + +const template = ` +
+
{{placeholder}}
+
+`; + +const props = ['placeholder', 'uploadUrl', 'uploadedTo']; + +// TODO - Remove jQuery usage +function mounted() { + let container = this.$el; + let _this = this; + new DropZone(container, { + url: function() { + return _this.uploadUrl; + }, + init: function () { + let dz = this; + + dz.on('sending', function (file, xhr, data) { + let token = window.document.querySelector('meta[name=token]').getAttribute('content'); + data.append('_token', token); + let uploadedTo = typeof _this.uploadedTo === 'undefined' ? 0 : _this.uploadedTo; + data.append('uploaded_to', uploadedTo); + }); + + dz.on('success', function (file, data) { + _this.$emit('success', {file, data}); + $(file.previewElement).fadeOut(400, function () { + dz.removeFile(file); + }); + }); + + dz.on('error', function (file, errorMessage, xhr) { + _this.$emit('error', {file, errorMessage, xhr}); + console.log(errorMessage); + console.log(xhr); + function setMessage(message) { + $(file.previewElement).find('[data-dz-errormessage]').text(message); + } + + if (xhr.status === 413) setMessage(trans('errors.server_upload_limit')); + if (errorMessage.file) setMessage(errorMessage.file[0]); + }); + } + }); +} + +function data() { + return {} +} + +module.exports = { + template, + props, + mounted, + data, +}; \ No newline at end of file diff --git a/resources/assets/js/vues/entity-search.js b/resources/assets/js/vues/entity-dashboard.js similarity index 100% rename from resources/assets/js/vues/entity-search.js rename to resources/assets/js/vues/entity-dashboard.js diff --git a/resources/assets/js/vues/image-manager.js b/resources/assets/js/vues/image-manager.js new file mode 100644 index 000000000..12ccc970d --- /dev/null +++ b/resources/assets/js/vues/image-manager.js @@ -0,0 +1,178 @@ +const dropzone = require('./components/dropzone'); + +let page = 0; +let previousClickTime = 0; +let previousClickImage = 0; +let dataLoaded = false; +let callback = false; +let baseUrl = ''; + +let preSearchImages = []; +let preSearchHasMore = false; + +const data = { + images: [], + + imageType: false, + uploadedTo: false, + + selectedImage: false, + dependantPages: false, + showing: false, + view: 'all', + hasMore: false, + searching: false, + searchTerm: '', + + imageUpdateSuccess: false, + imageDeleteSuccess: false, +}; + +const methods = { + + show(providedCallback) { + callback = providedCallback; + this.showing = true; + this.$el.children[0].components.overlay.show(); + + // Get initial images if they have not yet been loaded in. + if (dataLoaded) return; + this.fetchData(); + dataLoaded = true; + }, + + hide() { + this.showing = false; + this.$el.children[0].components.overlay.hide(); + }, + + fetchData() { + let url = baseUrl + page; + let query = {}; + if (this.uploadedTo !== false) query.page_id = this.uploadedTo; + if (this.searching) query.term = this.searchTerm; + + this.$http.get(url, {params: query}).then(response => { + this.images = this.images.concat(response.data.images); + this.hasMore = response.data.hasMore; + page++; + }); + }, + + setView(viewName) { + this.cancelSearch(); + this.images = []; + this.hasMore = false; + page = 0; + this.view = viewName; + baseUrl = window.baseUrl(`/images/${this.imageType}/${viewName}/`); + this.fetchData(); + }, + + searchImages() { + if (this.searchTerm === '') return this.cancelSearch(); + + // Cache current settings for later + if (!this.searching) { + preSearchImages = this.images; + preSearchHasMore = this.hasMore; + } + + this.searching = true; + this.images = []; + this.hasMore = false; + page = 0; + baseUrl = window.baseUrl(`/images/${this.imageType}/search/`); + this.fetchData(); + }, + + cancelSearch() { + this.searching = false; + this.searchTerm = ''; + this.images = preSearchImages; + this.hasMore = preSearchHasMore; + }, + + imageSelect(image) { + let dblClickTime = 300; + let currentTime = Date.now(); + let timeDiff = currentTime - previousClickTime; + let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage; + + if (isDblClick) { + this.callbackAndHide(image); + } else { + this.selectedImage = image; + this.dependantPages = false; + } + + previousClickTime = currentTime; + previousClickImage = image.id; + }, + + callbackAndHide(imageResult) { + if (callback) callback(imageResult); + this.hide(); + }, + + saveImageDetails() { + let url = window.baseUrl(`/images/update/${this.selectedImage.id}`); + this.$http.put(url, this.selectedImage).then(response => { + this.$events.emit('success', trans('components.image_update_success')); + }).catch(error => { + if (error.response.status === 422) { + let errors = error.response.data; + let message = ''; + Object.keys(errors).forEach((key) => { + message += errors[key].join('\n'); + }); + this.$events.emit('error', message); + } + }); + }, + + deleteImage() { + let force = this.dependantPages !== false; + let url = window.baseUrl('/images/' + this.selectedImage.id); + if (force) url += '?force=true'; + this.$http.delete(url).then(response => { + this.images.splice(this.images.indexOf(this.selectedImage), 1); + this.selectedImage = false; + this.$events.emit('success', trans('components.image_delete_success')); + }).catch(error=> { + if (error.response.status === 400) { + this.dependantPages = error.response.data; + } + }); + }, + + getDate(stringDate) { + return new Date(stringDate); + }, + + uploadSuccess(event) { + this.images.unshift(event.data); + this.$events.emit('success', trans('components.image_upload_success')); + }, +}; + +const computed = { + uploadUrl() { + return window.baseUrl(`/images/${this.imageType}/upload`); + } +}; + +function mounted() { + window.ImageManager = this; + this.imageType = this.$el.getAttribute('image-type'); + this.uploadedTo = this.$el.getAttribute('uploaded-to'); + baseUrl = window.baseUrl('/images/' + this.imageType + '/all/') +} + +module.exports = { + mounted, + methods, + data, + computed, + components: {dropzone}, +}; \ No newline at end of file diff --git a/resources/assets/js/vues/tag-manager.js b/resources/assets/js/vues/tag-manager.js new file mode 100644 index 000000000..d97ceb96b --- /dev/null +++ b/resources/assets/js/vues/tag-manager.js @@ -0,0 +1,68 @@ +const draggable = require('vuedraggable'); +const autosuggest = require('./components/autosuggest'); + +let data = { + pageId: false, + tags: [], +}; + +const components = {draggable, autosuggest}; +const directives = {}; + +let computed = {}; + +let methods = { + + addEmptyTag() { + this.tags.push({name: '', value: '', key: Math.random().toString(36).substring(7)}); + }, + + /** + * When an tag changes check if another empty editable field needs to be added onto the end. + * @param tag + */ + tagChange(tag) { + let tagPos = this.tags.indexOf(tag); + if (tagPos === this.tags.length-1 && (tag.name !== '' || tag.value !== '')) this.addEmptyTag(); + }, + + /** + * When an tag field loses focus check the tag to see if its + * empty and therefore could be removed from the list. + * @param tag + */ + tagBlur(tag) { + let isLast = (this.tags.indexOf(tag) === this.tags.length-1); + if (tag.name !== '' || tag.value !== '' || isLast) return; + let cPos = this.tags.indexOf(tag); + this.tags.splice(cPos, 1); + }, + + removeTag(tag) { + let tagPos = this.tags.indexOf(tag); + if (tagPos === -1) return; + this.tags.splice(tagPos, 1); + }, + + getTagFieldName(index, key) { + return `tags[${index}][${key}]`; + }, +}; + +function mounted() { + this.pageId = Number(this.$el.getAttribute('page-id')); + + let url = window.baseUrl(`/ajax/tags/get/page/${this.pageId}`); + this.$http.get(url).then(response => { + let tags = response.data; + for (let i = 0, len = tags.length; i < len; i++) { + tags[i].key = Math.random().toString(36).substring(7); + } + this.tags = tags; + this.addEmptyTag(); + }); +} + +module.exports = { + data, computed, methods, mounted, components, directives +}; \ No newline at end of file diff --git a/resources/assets/js/vues/vues.js b/resources/assets/js/vues/vues.js index 31d833bfb..a70d32009 100644 --- a/resources/assets/js/vues/vues.js +++ b/resources/assets/js/vues/vues.js @@ -6,16 +6,19 @@ function exists(id) { let vueMapping = { 'search-system': require('./search'), - 'entity-dashboard': require('./entity-search'), - 'code-editor': require('./code-editor') + 'entity-dashboard': require('./entity-dashboard'), + 'code-editor': require('./code-editor'), + 'image-manager': require('./image-manager'), + 'tag-manager': require('./tag-manager'), + 'attachment-manager': require('./attachment-manager'), }; window.vues = {}; -Object.keys(vueMapping).forEach(id => { - if (exists(id)) { - let config = vueMapping[id]; - config.el = '#' + id; - window.vues[id] = new Vue(config); - } -}); \ No newline at end of file +let ids = Object.keys(vueMapping); +for (let i = 0, len = ids.length; i < len; i++) { + if (!exists(ids[i])) continue; + let config = vueMapping[ids[i]]; + config.el = '#' + ids[i]; + window.vues[ids[i]] = new Vue(config); +} \ No newline at end of file diff --git a/resources/assets/sass/_animations.scss b/resources/assets/sass/_animations.scss index 467399a66..c03553d15 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); @@ -128,6 +91,6 @@ animation-timing-function: cubic-bezier(.62, .28, .23, .99); } -.anim.selectFade { +.selectFade { transition: background-color ease-in-out 3000ms; } \ No newline at end of file diff --git a/resources/assets/sass/_blocks.scss b/resources/assets/sass/_blocks.scss index bd3f8ff4e..61be99fe6 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 { @@ -181,4 +180,78 @@ &.warning:before { content: '\f1f1'; } +} + +.card { + margin: $-m; + background-color: #FFF; + box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.2); + h3 { + padding: $-m; + border-bottom: 1px solid #E8E8E8; + margin: 0; + font-size: $fs-s; + color: #888; + font-weight: 400; + text-transform: uppercase; + } + .body, p.empty-text { + padding: $-m; + } + a { + word-wrap: break-word; + word-break: break-word; + } +} + +.card.drag-card { + border: 1px solid #DDD; + border-radius: 4px; + display: flex; + padding: 0; + padding-left: $-s + 28px; + margin: $-s 0; + position: relative; + .drag-card-action { + cursor: pointer; + } + .handle, .drag-card-action { + display: flex; + padding: 0; + align-items: center; + text-align: center; + width: 28px; + padding-left: $-xs; + padding-right: $-xs; + &:hover { + background-color: #EEE; + } + i { + flex: 1; + padding: 0; + } + } + > div .outline input { + margin: $-s 0; + } + > div.padded { + padding: $-s 0 !important; + } + .handle { + background-color: #EEE; + left: 0; + position: absolute; + top: 0; + bottom: 0; + } + > div { + padding: 0 $-s; + max-width: 80%; + } +} + +.well { + background-color: #F8F8F8; + padding: $-m; + border: 1px solid #DDD; } \ No newline at end of file diff --git a/resources/assets/sass/_buttons.scss b/resources/assets/sass/_buttons.scss index 6e03c9217..f9c6d9b9a 100644 --- a/resources/assets/sass/_buttons.scss +++ b/resources/assets/sass/_buttons.scss @@ -2,9 +2,12 @@ @mixin generate-button-colors($textColor, $backgroundColor) { background-color: $backgroundColor; color: $textColor; + text-transform: uppercase; + border: 1px solid $backgroundColor; + vertical-align: top; &:hover { background-color: lighten($backgroundColor, 8%); - box-shadow: $bs-med; + //box-shadow: $bs-med; text-decoration: none; color: $textColor; } @@ -26,17 +29,16 @@ $button-border-radius: 2px; text-decoration: none; font-size: $fs-m; line-height: 1.4em; - padding: $-xs $-m; + padding: $-xs*1.3 $-m; margin: $-xs $-xs $-xs 0; display: inline-block; border: none; - font-weight: 500; - font-family: $text; + font-weight: 400; outline: 0; border-radius: $button-border-radius; cursor: pointer; transition: all ease-in-out 120ms; - box-shadow: 0 0.5px 1.5px 0 rgba(0, 0, 0, 0.21); + box-shadow: 0; @include generate-button-colors(#EEE, $primary); } @@ -52,19 +54,54 @@ $button-border-radius: 2px; @include generate-button-colors(#EEE, $secondary); } &.muted { - @include generate-button-colors(#EEE, #888); + @include generate-button-colors(#EEE, #AAA); } &.muted-light { @include generate-button-colors(#666, #e4e4e4); } } +.button.outline { + background-color: transparent; + color: #888; + border: 1px solid #DDD; + &:hover, &:focus, &:active { + box-shadow: none; + background-color: #EEE; + } + &.page { + border-color: $color-page; + color: $color-page; + &:hover, &:focus, &:active { + background-color: $color-page; + color: #FFF; + } + } + &.chapter { + border-color: $color-chapter; + color: $color-chapter; + &:hover, &:focus, &:active { + background-color: $color-chapter; + color: #FFF; + } + } + &.book { + border-color: $color-book; + color: $color-book; + &:hover, &:focus, &:active { + background-color: $color-book; + color: #FFF; + } + } +} + .text-button { @extend .link; background-color: transparent; padding: 0; margin: 0; border: none; + user-select: none; &:focus, &:active { outline: 0; } diff --git a/resources/assets/sass/_codemirror.scss b/resources/assets/sass/_codemirror.scss index bd85218a5..14bcd29cf 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; @@ -368,9 +366,9 @@ span.CodeMirror-selectedtext { background: none; } .cm-s-base16-light span.cm-atom { color: #aa759f; } .cm-s-base16-light span.cm-number { color: #aa759f; } -.cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute { color: #90a959; } +.cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute { color: #678c30; } .cm-s-base16-light span.cm-keyword { color: #ac4142; } -.cm-s-base16-light span.cm-string { color: #f4bf75; } +.cm-s-base16-light span.cm-string { color: #e09c3c; } .cm-s-base16-light span.cm-variable { color: #90a959; } .cm-s-base16-light span.cm-variable-2 { color: #6a9fb5; } @@ -386,7 +384,10 @@ span.CodeMirror-selectedtext { background: none; } /** * Custom BookStack overrides */ -.cm-s-base16-light.CodeMirror { +.CodeMirror, .CodeMirror pre { + font-size: 12px; +} +.CodeMirror { font-size: 12px; height: auto; margin-bottom: $-l; @@ -394,7 +395,7 @@ span.CodeMirror-selectedtext { background: none; } } .cm-s-base16-light .CodeMirror-gutters { background: #f5f5f5; border-right: 1px solid #DDD; } -.flex-fill .CodeMirror { +.code-fill .CodeMirror { position: absolute; top: 0; bottom: 0; diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss index 12babae73..54e109067 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; @@ -479,4 +540,45 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { margin-right: $-xs; text-decoration: underline; } +} + +.comment-box { + border: 1px solid #DDD; + margin-bottom: $-s; + border-radius: 3px; + .content { + padding: $-s; + font-size: 0.666em; + p, ul { + font-size: $fs-m; + margin: .5em 0; + } + } + .reply-row { + padding: $-xs $-s; + } +} + +.comment-box .header { + padding: $-xs $-s; + background-color: #f8f8f8; + border-bottom: 1px solid #DDD; + .meta { + img, a, span { + display: inline-block; + vertical-align: top; + } + a, span { + padding: $-xxs 0 $-xxs 0; + line-height: 1.6; + } + a { color: #666; } + span { + color: #888; + padding-left: $-xxs; + } + } + .text-muted { + color: #999; + } } \ No newline at end of file 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..700c15336 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -2,15 +2,13 @@ .input-base { background-color: #FFF; border-radius: 3px; - border: 1px solid #CCC; + border: 1px solid #D4D4D4; display: inline-block; font-size: $fs-s; - font-family: $text; - padding: $-xs; - color: #222; + padding: $-xs*1.5; + color: #666; width: 250px; max-width: 100%; - //-webkit-appearance:none; &.neg, &.invalid { border: 1px solid $negative; } @@ -25,6 +23,11 @@ } } +.fake-input { + @extend .input-base; + overflow: auto; +} + #html-editor { display: none; } @@ -33,7 +36,6 @@ position: relative; z-index: 5; #markdown-editor-input { - font-family: 'Roboto Mono', monospace; font-style: normal; font-weight: 400; padding: $-xs $-m; @@ -69,7 +71,6 @@ .editor-toolbar { width: 100%; padding: $-xs $-m; - font-family: 'Roboto Mono', monospace; font-size: 11px; line-height: 1.6; border-bottom: 1px solid #DDD; @@ -87,8 +88,9 @@ label { display: block; line-height: 1.4em; font-size: 0.94em; - font-weight: 500; - color: #666; + font-weight: 400; + color: #999; + text-transform: uppercase; padding-bottom: 2px; margin-bottom: 0.2em; &.inline { @@ -189,28 +191,15 @@ input:checked + .toggle-switch { } .inline-input-style { - border: 2px dotted #BBB; display: block; width: 100%; - padding: $-xs $-s; -} - -.title-input .input { - width: 100%; -} - -.title-input label, .description-input label{ - margin-top: $-m; - color: #666; + padding: $-s; } .title-input input[type="text"] { - @extend h1; @extend .inline-input-style; margin-top: 0; - padding-right: 0; - width: 100%; - color: #444; + font-size: 2em; } .title-input.page-title { @@ -251,21 +240,20 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] { border: none; color: $primary; padding: 0; - margin: 0; cursor: pointer; - margin-left: $-s; - } - button[type="submit"] { - margin-left: -$-l; + position: absolute; + left: 8px; + top: 9.5px; } input { - padding-right: $-l; + display: block; + padding-left: $-l; width: 300px; max-width: 100%; } } -input.outline { +.outline > input { border: 0; border-bottom: 2px solid #DDD; border-radius: 0; diff --git a/resources/assets/sass/_grid.scss b/resources/assets/sass/_grid.scss index de1ee83fb..b9a6ea53f 100644 --- a/resources/assets/sass/_grid.scss +++ b/resources/assets/sass/_grid.scss @@ -20,19 +20,128 @@ body.flexbox { align-items: stretch; min-height: 0; position: relative; - .flex, &.flex { - min-height: 0; - flex: 1; + &.rows { + flex-direction: row; } + &.columns { + flex-direction: column; + } +} + +.flex { + min-height: 0; + flex: 1; +} + +.flex.scroll { + //overflow-y: auto; + display: flex; + &.sidebar { + margin-right: -14px; + } +} +.flex.scroll .scroll-body { + overflow-y: scroll; + flex: 1; } .flex-child > div { flex: 1; } -//body.ie .flex-child > div { -// flex: 1 0 0px; -//} +.flex.sidebar { + flex: 1; + background-color: #F2F2F2; + max-width: 360px; + min-height: 90vh; +} +.flex.sidebar + .flex.content { + flex: 3; + background-color: #FFFFFF; + padding: 0 $-l; + border-left: 1px solid #DDD; + max-width: 100%; +} +.flex.sidebar .sidebar-toggle { + display: none; +} + +@include smaller-than($xl) { + body.sidebar-layout { + padding-left: 30px; + } + .flex.sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + z-index: 100; + padding-right: 30px; + width: 360px; + box-shadow: none; + transform: translate3d(-330px, 0, 0); + transition: transform ease-in-out 120ms; + display: flex; + flex-direction: column; + } + .flex.sidebar.open { + box-shadow: 1px 2px 2px 1px rgba(0,0,0,.10); + transform: translate3d(0, 0, 0); + .sidebar-toggle i { + transform: rotate(180deg); + } + } + .flex.sidebar .sidebar-toggle { + display: block; + position: absolute; + opacity: 0.9; + right: 0; + top: 0; + bottom: 0; + width: 30px; + color: #666; + font-size: 20px; + vertical-align: middle; + text-align: center; + border: 1px solid #DDD; + border-top: 1px solid #BBB; + padding-top: $-m; + cursor: pointer; + i { + opacity: 0.5; + transition: all ease-in-out 120ms; + padding: 0; + } + &:hover i { + opacity: 1; + } + } + .sidebar .scroll-body { + flex: 1; + overflow-y: scroll; + } + #sidebar .scroll-body.fixed { + width: auto !important; + } +} + +@include larger-than($xl) { + #sidebar .scroll-body.fixed { + z-index: 5; + position: fixed; + top: 0; + padding-right: $-m; + width: 30%; + left: 0; + height: 100%; + overflow-y: scroll; + -ms-overflow-style: none; + //background-color: $primary-faded; + border-left: 1px solid #DDD; + &::-webkit-scrollbar { width: 0 !important } + } +} + /** Rules for all columns */ div[class^="col-"] img { @@ -54,6 +163,10 @@ div[class^="col-"] img { &.small { max-width: 840px; } + &.nopad { + padding-left: 0; + padding-right: 0; + } } .row { diff --git a/resources/assets/sass/_header.scss b/resources/assets/sass/_header.scss index ae8dd3ff5..b9d9d68ef 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: 10px; + input { + background-color: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: #EEE; + } + button { + color: #EEE; + } + ::-webkit-input-placeholder { /* Chrome/Opera/Safari */ + color: #DDD; + } + ::-moz-placeholder { /* Firefox 19+ */ + color: #DDD; + } + :-ms-input-placeholder { /* IE 10+ */ + color: #DDD; + } + :-moz-placeholder { /* Firefox 18- */ + color: #DDD; + } + @include smaller-than($screen-lg) { + max-width: 250px; + } + @include smaller-than($l) { + max-width: 200px; } } -form.search-box { - margin-top: $-l *0.9; - display: inline-block; - position: relative; - text-align: left; - input { - background-color: transparent; - border-radius: 24px; - border: 2px solid #EEE; - color: #EEE; - padding-left: $-m; - padding-right: $-l; - outline: 0; - } - button { - vertical-align: top; - margin-left: -$-l; - color: #FFF; - top: 6px; - right: 4px; - display: inline-block; - position: absolute; - &:hover { - color: #FFF; - } +@include smaller-than($s) { + .header-search { + display: block; } } @@ -128,12 +132,12 @@ form.search-box { font-size: 1.8em; color: #fff; font-weight: 400; - padding: $-l $-l $-l 0; + padding: 14px $-l 14px 0; vertical-align: top; line-height: 1; } .logo-image { - margin: $-m $-s $-m 0; + margin: $-xs $-s $-xs 0; vertical-align: top; height: 43px; } @@ -167,6 +171,10 @@ form.search-box { background-color: $primary-faded; } +.toolbar-container { + background-color: #FFF; +} + .breadcrumbs .text-button, .action-buttons .text-button { display: inline-block; padding: $-s; @@ -227,4 +235,7 @@ form.search-box { border-bottom: 2px solid $primary; } } +} +.faded-small .nav-tabs a { + padding: $-s $-m; } \ No newline at end of file diff --git a/resources/assets/sass/_html.scss b/resources/assets/sass/_html.scss index c061f9d64..65f05a71d 100644 --- a/resources/assets/sass/_html.scss +++ b/resources/assets/sass/_html.scss @@ -9,14 +9,19 @@ html { &.flexbox { overflow-y: hidden; } + &.shaded { + background-color: #F2F2F2; + } } body { - font-family: $text; font-size: $fs-m; line-height: 1.6; color: #616161; -webkit-font-smoothing: antialiased; + &.shaded { + background-color: #F2F2F2; + } } button { diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index 051268926..d30d4d4a2 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; @@ -101,31 +86,8 @@ // Sidebar list .book-tree { - padding: $-xs 0 0 0; - position: relative; - right: 0; - top: 0; transition: ease-in-out 240ms; transition-property: right, border; - border-left: 0px solid #FFF; - background-color: #FFF; - max-width: 320px; - &.fixed { - background-color: #FFF; - z-index: 5; - position: fixed; - top: 0; - padding-left: $-l; - padding-right: $-l + 15; - width: 30%; - right: -15px; - height: 100%; - overflow-y: scroll; - -ms-overflow-style: none; - //background-color: $primary-faded; - border-left: 1px solid #DDD; - &::-webkit-scrollbar { width: 0 !important } - } } .book-tree h4 { padding: $-m $-s 0 $-s; @@ -171,7 +133,7 @@ background-color: rgba($color-chapter, 0.12); } } - .chapter-toggle { + [chapter-toggle] { padding-left: $-s; } .list-item-chapter { @@ -260,6 +222,9 @@ .left + .right { margin-left: 30px + $-s; } + &:last-of-type { + border-bottom: 0; + } } ul.pagination { @@ -312,9 +277,6 @@ ul.pagination { h4 { margin: 0; } - p { - margin: $-xs 0 0 0; - } hr { margin: 0; } @@ -331,15 +293,24 @@ ul.pagination { } } +.card .entity-list-item, .card .activity-list-item { + padding-left: $-m; + padding-right: $-m; +} + .entity-list.compact { font-size: 0.6em; h4, a { line-height: 1.2; } - p { + .entity-item-snippet { display: none; + } + .entity-list-item p { font-size: $fs-m * 0.8; padding-top: $-xs; + } + p { margin: 0; } > p.empty-text { @@ -381,6 +352,7 @@ ul.pagination { } li.padded { padding: $-xs $-m; + line-height: 1.2; } a { display: block; diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss index e5334c69c..f73bbd546 100755 --- a/resources/assets/sass/_pages.scss +++ b/resources/assets/sass/_pages.scss @@ -1,12 +1,3 @@ -#page-show { - >.row .col-md-9 { - z-index: 2; - } - >.row .col-md-3 { - z-index: 1; - } -} - .page-editor { display: flex; flex-direction: column; @@ -36,6 +27,8 @@ .page-content { max-width: 840px; + margin: 0 auto; + margin-top: $-xxl; overflow-wrap: break-word; .align-left { text-align: left; @@ -226,7 +219,7 @@ width: 100%; min-width: 50px; } - .tags td { + .tags td, .tag-table > div > div > div { padding-right: $-s; padding-top: $-s; position: relative; @@ -252,8 +245,6 @@ } .tag-display { - width: 100%; - //opacity: 0.7; position: relative; table { width: 100%; @@ -310,4 +301,8 @@ background-color: #EEE; } } +} + +.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll { + min-height: 175px; } \ No newline at end of file diff --git a/resources/assets/sass/_tables.scss b/resources/assets/sass/_tables.scss index 21553b839..38b044268 100644 --- a/resources/assets/sass/_tables.scss +++ b/resources/assets/sass/_tables.scss @@ -57,14 +57,4 @@ table.list-table { vertical-align: middle; padding: $-xs; } -} - -table.file-table { - @extend .no-style; - td { - padding: $-xs; - } - .ui-sortable-helper { - display: table; - } } \ No newline at end of file diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss index 2ef4bd16d..719126526 100644 --- a/resources/assets/sass/_text.scss +++ b/resources/assets/sass/_text.scss @@ -1,3 +1,14 @@ +/** + * Fonts + */ + +body, button, input, select, label, textarea { + font-family: $text; +} +.Codemirror, pre, #markdown-editor-input, .editor-toolbar, .code-base { + font-family: $mono; +} + /* * Header Styles */ @@ -58,7 +69,6 @@ a, .link { cursor: pointer; text-decoration: none; transition: color ease-in-out 80ms; - font-family: $text; line-height: 1.6; &:hover { text-decoration: underline; @@ -131,7 +141,6 @@ sub, .subscript { } pre { - font-family: monospace; font-size: 12px; background-color: #f5f5f5; border: 1px solid #DDD; @@ -180,7 +189,6 @@ blockquote { .code-base { background-color: #F8F8F8; - font-family: monospace; font-size: 0.80em; border: 1px solid #DDD; border-radius: 3px; @@ -370,12 +378,6 @@ span.sep { display: block; } -.action-header { - h1 { - margin-top: $-m; - } -} - /** * Icons */ diff --git a/resources/assets/sass/_tinymce.scss b/resources/assets/sass/_tinymce.scss index 969bbc968..b8394b25b 100644 --- a/resources/assets/sass/_tinymce.scss +++ b/resources/assets/sass/_tinymce.scss @@ -48,4 +48,7 @@ } } } +} +.page-content.mce-content-body p { + line-height: 1.6; } \ No newline at end of file diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss index 23bf2b219..d2b6acc9f 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; @@ -55,4 +59,4 @@ $text-light: #EEE; // Shadows $bs-light: 0 0 4px 1px #CCC; $bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26); -$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13); +$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13); \ No newline at end of file diff --git a/resources/assets/sass/export-styles.scss b/resources/assets/sass/export-styles.scss index 60450f3e2..1f7caf1d9 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"; diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index afb9d531b..e8d87d520 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"; @@ -17,12 +16,11 @@ @import "lists"; @import "pages"; -[v-cloak], [v-show] { +[v-cloak] { display: none; opacity: 0; animation-name: none !important; } - [ng\:cloak], [ng-cloak], .ng-cloak { display: none !important; user-select: none; @@ -65,50 +63,11 @@ body.dragging, body.dragging * { } } -// System wide notifications -.notification { - position: fixed; - top: 0; - right: 0; - margin: $-xl*2 $-xl; - padding: $-l $-xl; - background-color: #EEE; - border-radius: 3px; - box-shadow: $bs-med; - z-index: 999999; - display: block; - cursor: pointer; - max-width: 480px; - i, span { - display: table-cell; - } - i { - font-size: 2em; - padding-right: $-l; - } - span { - vertical-align: middle; - } - &.pos { - background-color: $positive; - color: #EEE; - } - &.neg { - background-color: $negative; - color: #EEE; - } - &.warning { - background-color: $secondary; - color: #EEE; - } -} - // Loading icon $loadingSize: 10px; .loading-container { position: relative; display: block; - height: $loadingSize; margin: $-xl auto; > div { width: $loadingSize; @@ -116,7 +75,8 @@ $loadingSize: 10px; border-radius: $loadingSize; display: inline-block; vertical-align: top; - transform: translate3d(0, 0, 0); + transform: translate3d(-10px, 0, 0); + margin-top: $-xs; animation-name: loadingBob; animation-duration: 1.4s; animation-iteration-count: infinite; @@ -130,11 +90,17 @@ $loadingSize: 10px; background-color: $color-book; animation-delay: 0s; } - > div:last-child { + > div:last-of-type { left: $loadingSize+$-xs; background-color: $color-chapter; animation-delay: 0.6s; } + > span { + margin-left: $-s; + font-style: italic; + color: #888; + vertical-align: top; + } } @@ -150,7 +116,7 @@ $loadingSize: 10px; // Back to top link $btt-size: 40px; -#back-to-top { +[back-to-top] { background-color: $primary; position: fixed; bottom: $-m; @@ -256,22 +222,15 @@ $btt-size: 40px; } .center-box { - margin: $-xl auto 0 auto; - padding: $-m $-xxl $-xl $-xxl; + margin: $-xxl auto 0 auto; width: 420px; max-width: 100%; display: inline-block; text-align: left; vertical-align: top; - //border: 1px solid #DDD; input { width: 100%; } - &.login { - background-color: #EEE; - box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1); - border: 1px solid #DDD; - } } diff --git a/resources/lang/de/activities.php b/resources/lang/de/activities.php index c2d20b3a6..3318ea752 100644 --- a/resources/lang/de/activities.php +++ b/resources/lang/de/activities.php @@ -8,33 +8,33 @@ return [ */ // Pages - 'page_create' => 'Seite erstellt', - 'page_create_notification' => 'Seite erfolgreich erstellt', - 'page_update' => 'Seite aktualisiert', - 'page_update_notification' => 'Seite erfolgreich aktualisiert', - 'page_delete' => 'Seite gelöscht', - 'page_delete_notification' => 'Seite erfolgreich gelöscht', - 'page_restore' => 'Seite wiederhergstellt', - 'page_restore_notification' => 'Seite erfolgreich wiederhergstellt', - 'page_move' => 'Seite verschoben', + 'page_create' => 'hat Seite erstellt:', + 'page_create_notification' => 'hat Seite erfolgreich erstellt:', + 'page_update' => 'hat Seite aktualisiert:', + 'page_update_notification' => 'hat Seite erfolgreich aktualisiert:', + 'page_delete' => 'hat Seite gelöscht:', + 'page_delete_notification' => 'hat Seite erfolgreich gelöscht:', + 'page_restore' => 'hat Seite wiederhergstellt:', + 'page_restore_notification' => 'hat Seite erfolgreich wiederhergstellt:', + 'page_move' => 'hat Seite verschoben:', // Chapters - 'chapter_create' => 'Kapitel erstellt', - 'chapter_create_notification' => 'Kapitel erfolgreich erstellt', - 'chapter_update' => 'Kapitel aktualisiert', - 'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert', - 'chapter_delete' => 'Kapitel gelöscht', - 'chapter_delete_notification' => 'Kapitel erfolgreich gelöscht', - 'chapter_move' => 'Kapitel verschoben', + 'chapter_create' => 'hat Kapitel erstellt:', + 'chapter_create_notification' => 'hat Kapitel erfolgreich erstellt:', + 'chapter_update' => 'hat Kapitel aktualisiert:', + 'chapter_update_notification' => 'hat Kapitel erfolgreich aktualisiert:', + 'chapter_delete' => 'hat Kapitel gelöscht', + 'chapter_delete_notification' => 'hat Kapitel erfolgreich gelöscht:', + 'chapter_move' => 'hat Kapitel verschoben:', // Books - 'book_create' => 'Buch erstellt', - 'book_create_notification' => 'Buch erfolgreich erstellt', - 'book_update' => 'Buch aktualisiert', - 'book_update_notification' => 'Buch erfolgreich aktualisiert', - 'book_delete' => 'Buch gelöscht', - 'book_delete_notification' => 'Buch erfolgreich gelöscht', - 'book_sort' => 'Buch sortiert', - 'book_sort_notification' => 'Buch erfolgreich neu sortiert', + 'book_create' => 'hat Buch erstellt:', + 'book_create_notification' => 'hat Buch erfolgreich erstellt:', + 'book_update' => 'hat Buch aktualisiert:', + 'book_update_notification' => 'hat Buch erfolgreich aktualisiert:', + 'book_delete' => 'hat Buch gelöscht:', + 'book_delete_notification' => 'hat Buch erfolgreich gelöscht:', + 'book_sort' => 'hat Buch sortiert:', + 'book_sort_notification' => 'hat Buch erfolgreich neu sortiert:', ]; diff --git a/resources/lang/de/auth.php b/resources/lang/de/auth.php index f253cdfa1..8f4afe654 100644 --- a/resources/lang/de/auth.php +++ b/resources/lang/de/auth.php @@ -10,8 +10,8 @@ return [ | these language lines according to your application's requirements. | */ - 'failed' => 'Dies sind keine gültigen Anmeldedaten.', - 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen sie es in :seconds Sekunden erneut.', + 'failed' => 'Die eingegebenen Anmeldedaten sind ungültig.', + 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.', /** * Login & Register @@ -29,16 +29,16 @@ return [ 'forgot_password' => 'Passwort vergessen?', 'remember_me' => 'Angemeldet bleiben', 'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.', - 'create_account' => 'Account anlegen', - 'social_login' => 'Social Login', - 'social_registration' => 'Social Registrierung', - 'social_registration_text' => 'Mit einem dieser Möglichkeiten registrieren oder anmelden.', + 'create_account' => 'Account registrieren', + 'social_login' => 'Mit Sozialem Netzwerk anmelden', + 'social_registration' => 'Mit Sozialem Netzwerk registrieren', + 'social_registration_text' => 'Mit einer dieser Dienste registrieren oder anmelden', 'register_thanks' => 'Vielen Dank für Ihre Registrierung!', - 'register_confirm' => 'Bitte prüfen Sie Ihren E-Mail Eingang und klicken auf den Verifizieren-Button, um :appName nutzen zu können.', - 'registrations_disabled' => 'Die Registrierung ist momentan nicht möglich', - 'registration_email_domain_invalid' => 'Diese E-Mail-Domain ist für die Benutzer der Applikation nicht freigeschaltet.', + 'register_confirm' => 'Bitte prüfen Sie Ihren Posteingang und bestätigen Sie die Registrierung.', + 'registrations_disabled' => 'Eine Registrierung ist momentan nicht möglich', + 'registration_email_domain_invalid' => 'Sie können sich mit dieser E-Mail nicht registrieren.', 'register_success' => 'Vielen Dank für Ihre Registrierung! Die Daten sind gespeichert und Sie sind angemeldet.', @@ -46,30 +46,30 @@ return [ * Password Reset */ 'reset_password' => 'Passwort vergessen', - 'reset_password_send_instructions' => 'Bitte geben Sie unten Ihre E-Mail-Adresse ein und Sie erhalten eine E-Mail, um Ihr Passwort zurück zu setzen.', + 'reset_password_send_instructions' => 'Bitte geben Sie Ihre E-Mail-Adresse ein. Danach erhalten Sie eine E-Mail mit einem Link zum Zurücksetzen Ihres Passwortes.', 'reset_password_send_button' => 'Passwort zurücksetzen', - 'reset_password_sent_success' => 'Eine E-Mail mit den Instruktionen, um Ihr Passwort zurückzusetzen wurde an :email gesendet.', - 'reset_password_success' => 'Ihr Passwort wurde erfolgreich zurück gesetzt.', + 'reset_password_sent_success' => 'Eine E-Mail mit dem Link zum Zurücksetzen Ihres Passwortes wurde an :email gesendet.', + 'reset_password_success' => 'Ihr Passwort wurde erfolgreich zurückgesetzt.', 'email_reset_subject' => 'Passwort zurücksetzen für :appName', - 'email_reset_text' => 'Sie erhalten diese E-Mail, weil eine Passwort-Rücksetzung für Ihren Account beantragt wurde.', - 'email_reset_not_requested' => 'Wenn Sie die Passwort-Rücksetzung nicht ausgelöst haben, ist kein weiteres Handeln notwendig.', + 'email_reset_text' => 'Sie erhalten diese E-Mail, weil jemand versucht hat, Ihr Passwort zurückzusetzen.', + 'email_reset_not_requested' => 'Wenn Sie das nicht waren, brauchen Sie nichts weiter zu tun.', /** * Email Confirmation */ - 'email_confirm_subject' => 'Bestätigen sie ihre E-Mail Adresse bei :appName', - 'email_confirm_greeting' => 'Danke, dass sie :appName beigetreten sind!', - 'email_confirm_text' => 'Bitte bestätigen sie ihre E-Mail Adresse, indem sie auf den Button klicken:', - 'email_confirm_action' => 'E-Mail Adresse bestätigen', - 'email_confirm_send_error' => 'Bestätigungs-E-Mail benötigt, aber das System konnte die E-Mail nicht versenden. Kontaktieren sie den Administrator, um sicherzustellen, dass das Sytsem korrekt eingerichtet ist.', - 'email_confirm_success' => 'Ihre E-Mail Adresse wurde bestätigt!', - 'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfen sie ihren Posteingang.', + 'email_confirm_subject' => 'Bestätigen Sie Ihre E-Mail-Adresse für :appName', + 'email_confirm_greeting' => 'Danke, dass Sie sich für :appName registriert haben!', + 'email_confirm_text' => 'Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf die Schaltfläche klicken:', + 'email_confirm_action' => 'E-Mail-Adresse bestätigen', + 'email_confirm_send_error' => 'Leider konnte die für die Registrierung notwendige E-Mail zur bestätigung Ihrer E-Mail-Adresse nicht versandt werden. Bitte kontaktieren Sie den Systemadministrator!', + 'email_confirm_success' => 'Ihre E-Mail-Adresse wurde bestätigt!', + 'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfen Sie Ihren Posteingang.', 'email_not_confirmed' => 'E-Mail-Adresse ist nicht bestätigt', 'email_not_confirmed_text' => 'Ihre E-Mail-Adresse ist bisher nicht bestätigt.', 'email_not_confirmed_click_link' => 'Bitte klicken Sie auf den Link in der E-Mail, die Sie nach der Registrierung erhalten haben.', 'email_not_confirmed_resend' => 'Wenn Sie die E-Mail nicht erhalten haben, können Sie die Nachricht erneut anfordern. Füllen Sie hierzu bitte das folgende Formular aus:', - 'email_not_confirmed_resend_button' => 'Bestätigungs E-Mail erneut senden', + 'email_not_confirmed_resend_button' => 'Bestätigungs-E-Mail erneut senden', ]; diff --git a/resources/lang/de/common.php b/resources/lang/de/common.php index 7ad1743a0..3c21a9d08 100644 --- a/resources/lang/de/common.php +++ b/resources/lang/de/common.php @@ -28,9 +28,9 @@ return [ 'edit' => 'Bearbeiten', 'sort' => 'Sortieren', 'move' => 'Verschieben', - 'delete' => 'Löschen', + 'delete' => 'Löschen', 'search' => 'Suchen', - 'search_clear' => 'Suche löschen', + 'search_clear' => 'Suche löschen', 'reset' => 'Zurücksetzen', 'remove' => 'Entfernen', @@ -38,9 +38,9 @@ return [ /** * Misc */ - 'deleted_user' => 'Gelöschte Benutzer', - 'no_activity' => 'Keine Aktivitäten zum Anzeigen', - 'no_items' => 'Keine Einträge gefunden.', + 'deleted_user' => 'Gelöschte Benutzer', + 'no_activity' => 'Keine Aktivitäten zum Anzeigen', + 'no_items' => 'Keine Einträge gefunden.', 'back_to_top' => 'nach oben', 'toggle_details' => 'Details zeigen/verstecken', @@ -53,6 +53,6 @@ return [ /** * Email Content */ - 'email_action_help' => 'Sollte es beim Anklicken des ":actionText" Buttons Probleme geben, kopieren Sie folgende URL und fügen diese in Ihrem Webbrowser ein:', + 'email_action_help' => 'Sollte es beim Anklicken der Schaltfläche ":action_text" Probleme geben, öffnen Sie folgende URL in Ihrem Browser:', 'email_rights' => 'Alle Rechte vorbehalten', -]; \ No newline at end of file +]; diff --git a/resources/lang/de/components.php b/resources/lang/de/components.php index a8538c465..26bf3e626 100644 --- a/resources/lang/de/components.php +++ b/resources/lang/de/components.php @@ -13,12 +13,12 @@ return [ 'image_uploaded' => 'Hochgeladen am :uploadedDate', 'image_load_more' => 'Mehr', 'image_image_name' => 'Bildname', - 'image_delete_confirm' => 'Dieses Bild wird auf den folgenden Seiten benutzt. Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild tatsächlich entfernen möchten.', + 'image_delete_confirm' => 'Dieses Bild wird auf den folgenden Seiten benutzt. Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild wirklich entfernen möchten.', 'image_select_image' => 'Bild auswählen', - 'image_dropzone' => 'Ziehen Sie Bilder hier hinein oder klicken Sie hier, um ein Bild auszuwählen', + 'image_dropzone' => 'Ziehen Sie Bilder hierher oder klicken Sie, um ein Bild auszuwählen', 'images_deleted' => 'Bilder gelöscht', 'image_preview' => 'Bildvorschau', 'image_upload_success' => 'Bild erfolgreich hochgeladen', 'image_update_success' => 'Bilddetails erfolgreich aktualisiert', 'image_delete_success' => 'Bild erfolgreich gelöscht' -]; \ No newline at end of file +]; diff --git a/resources/lang/de/entities.php b/resources/lang/de/entities.php index c9feb8497..b75c647bc 100644 --- a/resources/lang/de/entities.php +++ b/resources/lang/de/entities.php @@ -4,38 +4,38 @@ return [ /** * Shared */ - 'recently_created' => 'Kürzlich angelegt', - 'recently_created_pages' => 'Kürzlich angelegte Seiten', - 'recently_updated_pages' => 'Kürzlich aktualisierte Seiten', - 'recently_created_chapters' => 'Kürzlich angelegte Kapitel', - 'recently_created_books' => 'Kürzlich angelegte Bücher', - 'recently_update' => 'Kürzlich aktualisiert', - 'recently_viewed' => 'Kürzlich angesehen', - 'recent_activity' => 'Kürzliche Aktivität', + 'recently_created' => 'Kürzlich angelegt', + 'recently_created_pages' => 'Kürzlich angelegte Seiten', + 'recently_updated_pages' => 'Kürzlich aktualisierte Seiten', + 'recently_created_chapters' => 'Kürzlich angelegte Kapitel', + 'recently_created_books' => 'Kürzlich angelegte Bücher', + 'recently_update' => 'Kürzlich aktualisiert', + 'recently_viewed' => 'Kürzlich angesehen', + 'recent_activity' => 'Kürzliche Aktivität', 'create_now' => 'Jetzt anlegen', - 'revisions' => 'Revisionen', - 'meta_created' => 'Angelegt am :timeLength', - 'meta_created_name' => 'Angelegt am :timeLength durch :user', - 'meta_updated' => 'Aktualisiert am :timeLength', - 'meta_updated_name' => 'Aktualisiert am :timeLength durch :user', - 'x_pages' => ':count Seiten', - 'entity_select' => 'Eintrag auswählen', + 'revisions' => 'Versionen', + 'meta_revision' => 'Version #:revisionCount', + 'meta_created' => 'Erstellt: :timeLength', + 'meta_created_name' => 'Erstellt: :timeLength von :user', + 'meta_updated' => 'Zuletzt aktualisiert: :timeLength', + 'meta_updated_name' => 'Zuletzt aktualisiert: :timeLength von :user', + 'entity_select' => 'Eintrag auswählen', 'images' => 'Bilder', - 'my_recent_drafts' => 'Meine kürzlichen Entwürfe', - 'my_recently_viewed' => 'Kürzlich von mir angesehen', + 'my_recent_drafts' => 'Meine kürzlichen Entwürfe', + 'my_recently_viewed' => 'Kürzlich von mir angesehen', 'no_pages_viewed' => 'Sie haben bisher keine Seiten angesehen.', 'no_pages_recently_created' => 'Sie haben bisher keine Seiten angelegt.', 'no_pages_recently_updated' => 'Sie haben bisher keine Seiten aktualisiert.', 'export' => 'Exportieren', 'export_html' => 'HTML-Datei', 'export_pdf' => 'PDF-Datei', - 'export_text' => 'Text-Datei', + 'export_text' => 'Textdatei', /** * Permissions and restrictions */ 'permissions' => 'Berechtigungen', - 'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, überschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.', + 'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, überschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.', 'permissions_enable' => 'Individuelle Berechtigungen aktivieren', 'permissions_save' => 'Berechtigungen speichern', @@ -43,41 +43,61 @@ return [ * Search */ 'search_results' => 'Suchergebnisse', - 'search_clear' => 'Suche zurücksetzen', - 'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden', - 'search_for_term' => 'Suche nach :term', + 'search_total_results_found' => ':count Ergebnis gefunden|:count Ergebnisse gesamt', + 'search_clear' => 'Filter löschen', + 'search_no_pages' => 'Keine Seiten gefunden', + 'search_for_term' => 'Nach :term suchen', + 'search_more' => 'Mehr Ergebnisse', + 'search_filters' => 'Filter', + 'search_content_type' => 'Inhaltstyp', + 'search_exact_matches' => 'Exakte Treffer', + 'search_tags' => 'Nach Schlagwort suchen', + 'search_viewed_by_me' => 'Schon von mir angesehen', + 'search_not_viewed_by_me' => 'Noch nicht von mir angesehen', + 'search_permissions_set' => 'Berechtigungen gesetzt', + 'search_created_by_me' => 'Von mir erstellt', + 'search_updated_by_me' => 'Von mir aktualisiert', + 'search_updated_before' => 'Aktualisiert vor', + 'search_updated_after' => 'Aktualisiert nach', + 'search_created_before' => 'Erstellt vor', + 'search_created_after' => 'Erstellt nach', + 'search_set_date' => 'Datum auswählen', + 'search_update' => 'Suche aktualisieren', /** * Books */ 'book' => 'Buch', - 'books' => 'Bücher', - 'books_empty' => 'Es wurden keine Bücher angelegt', - 'books_popular' => 'Populäre Bücher', - 'books_recent' => 'Kürzlich genutzte Bücher', - 'books_popular_empty' => 'Die populärsten Bücher werden hier angezeigt.', - 'books_create' => 'Neues Buch anlegen', - 'books_delete' => 'Buch löschen', - 'books_delete_named' => 'Buch :bookName löschen', - 'books_delete_explain' => 'Sie möchten das Buch \':bookName\' löschen und alle Seiten und Kapitel entfernen.', - 'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch löschen möchten?', + 'books' => 'Bücher', + 'x_books' => ':count Buch|:count Bücher', + 'books_empty' => 'Keine Bücher vorhanden', + 'books_popular' => 'Beliebte Bücher', + 'books_recent' => 'Kürzlich angesehene Bücher', + 'books_new' => 'Neue Bücher', + 'books_popular_empty' => 'Die beliebtesten Bücher werden hier angezeigt.', + 'books_new_empty' => 'Die neusten Bücher werden hier angezeigt.', + 'books_create' => 'Neues Buch erstellen', + 'books_delete' => 'Buch löschen', + 'books_delete_named' => 'Buch ":bookName" löschen', + 'books_delete_explain' => 'Das Buch ":bookName" wird gelöscht und alle zugehörigen Kapitel und Seiten entfernt.', + 'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch löschen möchten?', 'books_edit' => 'Buch bearbeiten', - 'books_edit_named' => 'Buch :bookName bearbeiten', - 'books_form_book_name' => 'Buchname', + 'books_edit_named' => 'Buch ":bookName" bearbeiten', + 'books_form_book_name' => 'Name des Buches', 'books_save' => 'Buch speichern', 'books_permissions' => 'Buch-Berechtigungen', 'books_permissions_updated' => 'Buch-Berechtigungen aktualisiert', - 'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel für dieses Buch angelegt.', + 'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel zu diesem Buch hinzugefügt worden.', 'books_empty_create_page' => 'Neue Seite anlegen', 'books_empty_or' => 'oder', 'books_empty_sort_current_book' => 'Aktuelles Buch sortieren', - 'books_empty_add_chapter' => 'Neues Kapitel hinzufügen', + 'books_empty_add_chapter' => 'Neues Kapitel hinzufügen', 'books_permissions_active' => 'Buch-Berechtigungen aktiv', 'books_search_this' => 'Dieses Buch durchsuchen', - 'books_navigation' => 'Buch-Navigation', + 'books_navigation' => 'Buchnavigation', 'books_sort' => 'Buchinhalte sortieren', - 'books_sort_named' => 'Buch :bookName sortieren', - 'books_sort_show_other' => 'Andere Bücher zeigen', + 'books_sort_named' => 'Buch ":bookName" sortieren', + 'books_sort_show_other' => 'Andere Bücher anzeigen', 'books_sort_save' => 'Neue Reihenfolge speichern', /** @@ -85,132 +105,156 @@ return [ */ 'chapter' => 'Kapitel', 'chapters' => 'Kapitel', - 'chapters_popular' => 'Populäre Kapitel', + 'x_chapters' => ':count Kapitel', + 'chapters_popular' => 'Beliebte Kapitel', 'chapters_new' => 'Neues Kapitel', 'chapters_create' => 'Neues Kapitel anlegen', 'chapters_delete' => 'Kapitel entfernen', - 'chapters_delete_named' => 'Kapitel :chapterName entfernen', - 'chapters_delete_explain' => 'Sie möchten das Kapitel \':chapterName\' löschen und alle Seiten dem direkten Eltern-Buch hinzugefügen.', - 'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel löschen möchten?', + 'chapters_delete_named' => 'Kapitel ":chapterName" entfernen', + 'chapters_delete_explain' => 'Das Kapitel ":chapterName" wird gelöscht und alle zugehörigen Seiten dem übergeordneten Buch zugeordnet.', + 'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel löschen möchten?', 'chapters_edit' => 'Kapitel bearbeiten', - 'chapters_edit_named' => 'Kapitel :chapterName bearbeiten', + 'chapters_edit_named' => 'Kapitel ":chapterName" bearbeiten', 'chapters_save' => 'Kapitel speichern', 'chapters_move' => 'Kapitel verschieben', - 'chapters_move_named' => 'Kapitel :chapterName verschieben', - 'chapter_move_success' => 'Kapitel in das Buch :bookName verschoben.', + 'chapters_move_named' => 'Kapitel ":chapterName" verschieben', + 'chapter_move_success' => 'Das Kapitel wurde in das Buch ":bookName" verschoben.', 'chapters_permissions' => 'Kapitel-Berechtigungen', - 'chapters_empty' => 'Aktuell sind keine Kapitel in diesem Buch angelegt.', + 'chapters_empty' => 'Aktuell sind keine Kapitel diesem Buch hinzugefügt worden.', 'chapters_permissions_active' => 'Kapitel-Berechtigungen aktiv', 'chapters_permissions_success' => 'Kapitel-Berechtigungenen aktualisisert', + 'chapters_search_this' => 'Dieses Kapitel durchsuchen', /** * Pages */ 'page' => 'Seite', 'pages' => 'Seiten', - 'pages_popular' => 'Populäre Seiten', + 'x_pages' => ':count Seite|:count Seiten', + 'pages_popular' => 'Beliebte Seiten', 'pages_new' => 'Neue Seite', - 'pages_attachments' => 'Anhänge', + 'pages_attachments' => 'Anhänge', 'pages_navigation' => 'Seitennavigation', - 'pages_delete' => 'Seite löschen', - 'pages_delete_named' => 'Seite :pageName löschen', - 'pages_delete_draft_named' => 'Seitenentwurf von :pageName löschen', - 'pages_delete_draft' => 'Seitenentwurf löschen', - 'pages_delete_success' => 'Seite gelöscht', - 'pages_delete_draft_success' => 'Seitenentwurf gelöscht', - 'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite löschen möchen?', - 'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf löschen möchten?', - 'pages_editing_named' => 'Seite :pageName bearbeiten', - 'pages_edit_toggle_header' => 'Toggle header', + 'pages_delete' => 'Seite löschen', + 'pages_delete_named' => 'Seite ":pageName" löschen', + 'pages_delete_draft_named' => 'Seitenentwurf von ":pageName" löschen', + 'pages_delete_draft' => 'Seitenentwurf löschen', + 'pages_delete_success' => 'Seite gelöscht', + 'pages_delete_draft_success' => 'Seitenentwurf gelöscht', + 'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite löschen möchen?', + 'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf löschen möchten?', + 'pages_editing_named' => 'Seite ":pageName" bearbeiten', + 'pages_edit_toggle_header' => 'Hauptmenü anzeigen/verstecken', 'pages_edit_save_draft' => 'Entwurf speichern', 'pages_edit_draft' => 'Seitenentwurf bearbeiten', 'pages_editing_draft' => 'Seitenentwurf bearbeiten', 'pages_editing_page' => 'Seite bearbeiten', 'pages_edit_draft_save_at' => 'Entwurf gespeichert um ', - 'pages_edit_delete_draft' => 'Entwurf löschen', + 'pages_edit_delete_draft' => 'Entwurf löschen', 'pages_edit_discard_draft' => 'Entwurf verwerfen', - 'pages_edit_set_changelog' => 'Veränderungshinweis setzen', - 'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer Änderungen ein', - 'pages_edit_enter_changelog' => 'Veränderungshinweis eingeben', + 'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen', + 'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer Änderungen ein', + 'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben', 'pages_save' => 'Seite speichern', 'pages_title' => 'Seitentitel', 'pages_name' => 'Seitenname', 'pages_md_editor' => 'Redakteur', 'pages_md_preview' => 'Vorschau', - 'pages_md_insert_image' => 'Bild einfügen', - 'pages_md_insert_link' => 'Link zu einem Objekt einfügen', + 'pages_md_insert_image' => 'Bild einfügen', + 'pages_md_insert_link' => 'Link zu einem Objekt einfügen', 'pages_not_in_chapter' => 'Seite ist in keinem Kapitel', 'pages_move' => 'Seite verschieben', 'pages_move_success' => 'Seite nach ":parentName" verschoben', 'pages_permissions' => 'Seiten Berechtigungen', 'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert', + 'pages_revision' => 'Version', 'pages_revisions' => 'Seitenversionen', - 'pages_revisions_named' => 'Seitenversionen von :pageName', - 'pages_revision_named' => 'Seitenversion von :pageName', - 'pages_revisions_created_by' => 'Angelegt von', + 'pages_revisions_named' => 'Seitenversionen von ":pageName"', + 'pages_revision_named' => 'Seitenversion von ":pageName"', + 'pages_revisions_created_by' => 'Erstellt von', 'pages_revisions_date' => 'Versionsdatum', - 'pages_revisions_changelog' => 'Veränderungshinweise', - 'pages_revisions_changes' => 'Veränderungen', + 'pages_revisions_number' => '#', + 'pages_revisions_changelog' => 'Änderungsprotokoll', + 'pages_revisions_changes' => 'Änderungen', 'pages_revisions_current' => 'Aktuelle Version', 'pages_revisions_preview' => 'Vorschau', - 'pages_revisions_restore' => 'Zurück sichern', - 'pages_revisions_none' => 'Diese Seite hat keine älteren Versionen.', + 'pages_revisions_restore' => 'Wiederherstellen', + 'pages_revisions_none' => 'Diese Seite hat keine älteren Versionen.', 'pages_copy_link' => 'Link kopieren', 'pages_permissions_active' => 'Seiten-Berechtigungen aktiv', - 'pages_initial_revision' => 'Erste Veröffentlichung', + 'pages_initial_revision' => 'Erste Veröffentlichung', 'pages_initial_name' => 'Neue Seite', - 'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt um :timeDiff gespeichert wurde.', - 'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.', + 'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt :timeDiff gespeichert wurde.', + 'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.', 'pages_draft_edit_active' => [ - 'start_a' => ':count Benutzer haben die Bearbeitung dieser Seite begonnen.', - 'start_b' => ':userName hat die Bearbeitung dieser Seite begonnen.', + 'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.', + 'start_b' => ':userName bearbeitet jetzt diese Seite.', 'time_a' => 'seit die Seiten zuletzt aktualisiert wurden.', 'time_b' => 'in den letzten :minCount Minuten', - 'message' => ':start :time. Achten Sie darauf keine Aktualisierungen von anderen Benutzern zu überschreiben!', + 'message' => ':start :time. Achten Sie darauf, keine Änderungen von anderen Benutzern zu überschreiben!', ], 'pages_draft_discarded' => 'Entwurf verworfen. Der aktuelle Seiteninhalt wurde geladen.', /** * Editor sidebar */ - 'page_tags' => 'Seiten-Schlagwörter', + 'page_tags' => 'Seiten-Schlagwörter', 'tag' => 'Schlagwort', - 'tags' => 'Schlagworte', - 'tag_value' => 'Schlagwortinhalt (Optional)', - 'tags_explain' => "Fügen Sie Schlagworte hinzu, um Ihren Inhalt zu kategorisieren. \n Sie können einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.", - 'tags_add' => 'Weiteres Schlagwort hinzufügen', - 'attachments' => 'Anhänge', - 'attachments_explain' => 'Sie können auf Ihrer Seite Dateien hochladen oder Links anfügen. Diese werden in der seitlich angezeigt.', - 'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.', - 'attachments_items' => 'Angefügte Elemente', + 'tags' => 'Schlagwörter', + 'tag_value' => 'Inhalt (Optional)', + 'tags_explain' => "Fügen Sie Schlagwörter hinzu, um Ihren Inhalt zu kategorisieren.\nSie können einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.", + 'tags_add' => 'Weiteres Schlagwort hinzufügen', + 'attachments' => 'Anhänge', + 'attachments_explain' => 'Sie können auf Ihrer Seite Dateien hochladen oder Links hinzufügen. Diese werden in der Seitenleiste angezeigt.', + 'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.', + 'attachments_items' => 'Angefügte Elemente', 'attachments_upload' => 'Datei hochladen', - 'attachments_link' => 'Link anfügen', + 'attachments_link' => 'Link hinzufügen', 'attachments_set_link' => 'Link setzen', - 'attachments_delete_confirm' => 'Klicken Sie erneut auf löschen, um diesen Anhang zu entfernen.', - 'attachments_dropzone' => 'Ziehen Sie Dateien hier hinein oder klicken Sie hier, um eine Datei auszuwählen', + 'attachments_delete_confirm' => 'Klicken Sie erneut auf löschen, um diesen Anhang zu entfernen.', + 'attachments_dropzone' => 'Ziehen Sie Dateien hierher oder klicken Sie, um eine Datei auszuwählen', 'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.', - 'attachments_explain_link' => 'Wenn Sie keine Datei hochladen möchten, können Sie stattdessen einen Link anfügen. Dieser Link kann auf eine andere Seite oder zu einer Datei in der Cloud weisen.', + 'attachments_explain_link' => 'Wenn Sie keine Datei hochladen möchten, können Sie stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet weisen.', 'attachments_link_name' => 'Link-Name', 'attachment_link' => 'Link zum Anhang', 'attachments_link_url' => 'Link zu einer Datei', 'attachments_link_url_hint' => 'URL einer Seite oder Datei', - 'attach' => 'anfügen', + 'attach' => 'Hinzufügen', 'attachments_edit_file' => 'Datei bearbeiten', 'attachments_edit_file_name' => 'Dateiname', - 'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hier hinein, um diese hochzuladen und zu überschreiben', - 'attachments_order_updated' => 'Reihenfolge der Anhänge aktualisiert', - 'attachments_updated_success' => 'Anhang-Details aktualisiert', - 'attachments_deleted' => 'Anhang gelöscht', - 'attachments_file_uploaded' => 'Datei erfolgrecich hochgeladen', - 'attachments_file_updated' => 'Datei erfolgreich aktualisisert', - 'attachments_link_attached' => 'Link erfolgreich der Seite hinzugefügt', + 'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hierher, um diese hochzuladen und zu überschreiben', + 'attachments_order_updated' => 'Reihenfolge der Anhänge aktualisiert', + 'attachments_updated_success' => 'Anhangdetails aktualisiert', + 'attachments_deleted' => 'Anhang gelöscht', + 'attachments_file_uploaded' => 'Datei erfolgreich hochgeladen', + 'attachments_file_updated' => 'Datei erfolgreich aktualisiert', + 'attachments_link_attached' => 'Link erfolgreich der Seite hinzugefügt', /** * Profile View */ 'profile_user_for_x' => 'Benutzer seit :time', - 'profile_created_content' => 'Angelegte Inhalte', - 'profile_not_created_pages' => ':userName hat bisher keine Seiten angelegt.', - 'profile_not_created_chapters' => ':userName hat bisher keine Kapitel angelegt.', - 'profile_not_created_books' => ':userName hat bisher keine Bücher angelegt.', -]; \ 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.', + + /** + * Comments + */ + 'comment' => 'Kommentar', + 'comments' => 'Kommentare', + 'comment_placeholder' => 'Geben Sie hier Ihre Kommentare ein (Markdown unterstützt)', + 'comment_count' => '{0} Keine Kommentare|{1} 1 Kommentar|[2,*] :count Kommentare', + 'comment_save' => 'Kommentar speichern', + 'comment_saving' => 'Kommentar wird gespeichert...', + 'comment_deleting' => 'Kommentar wird gelöscht...', + 'comment_new' => 'Neuer Kommentar', + 'comment_created' => ':createDiff kommentiert', + 'comment_updated' => ':updateDiff aktualisiert von :username', + 'comment_deleted_success' => 'Kommentar gelöscht', + 'comment_created_success' => 'Kommentar hinzugefügt', + 'comment_updated_success' => 'Kommentar aktualisiert', + 'comment_delete_confirm' => 'Möchten Sie diesen Kommentar wirklich löschen?', + 'comment_in_reply_to' => 'Antwort auf :commentId', +]; diff --git a/resources/lang/de/errors.php b/resources/lang/de/errors.php index e085d9915..0b961f8ee 100644 --- a/resources/lang/de/errors.php +++ b/resources/lang/de/errors.php @@ -7,37 +7,37 @@ return [ */ // Pages - 'permission' => 'Sie haben keine Berechtigung auf diese Seite zuzugreifen.', - 'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuführen.', + 'permission' => 'Sie haben keine Berechtigung, auf diese Seite zuzugreifen.', + 'permissionJson' => 'Sie haben keine Berechtigung, die angeforderte Aktion auszuführen.', // Auth - 'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten angelegt.', - 'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melden Sie sich an.', - 'email_confirmation_invalid' => 'Der Bestätigungs-Token ist nicht gültig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.', - 'email_confirmation_expired' => 'Der Bestätigungs-Token ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.', - 'ldap_fail_anonymous' => 'Anonymer LDAP Zugriff ist fehlgeschlafgen', - 'ldap_fail_authed' => 'LDAP Zugriff mit DN & Passwort ist fehlgeschlagen', - 'ldap_extension_not_installed' => 'LDAP PHP Erweiterung ist nicht installiert.', - 'ldap_cannot_connect' => 'Die Verbindung zu LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.', + 'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten registriert.', + 'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melden Sie sich an.', + 'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.', + 'email_confirmation_expired' => 'Der Bestätigungslink ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.', + 'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlafgen', + 'ldap_fail_authed' => 'LDAP-Zugriff mit DN und Passwort ist fehlgeschlagen', + 'ldap_extension_not_installed' => 'LDAP-PHP-Erweiterung ist nicht installiert.', + 'ldap_cannot_connect' => 'Die Verbindung zum LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.', 'social_no_action_defined' => 'Es ist keine Aktion definiert', - 'social_account_in_use' => 'Dieses :socialAccount Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount Konto an.', - 'social_account_email_in_use' => 'Die E-Mail-Adresse :email ist bereits registriert. Wenn Sie bereits registriert sind, können Sie Ihr :socialAccount Konto in Ihren Profil-Einstellungen verknüpfen.', - 'social_account_existing' => 'Dieses :socialAccount Konto ist bereits mit Ihrem Profil verknüpft.', - 'social_account_already_used_existing' => 'Dieses :socialAccount Konto wird bereits durch einen anderen Benutzer verwendet.', - 'social_account_not_used' => 'Dieses :socialAccount Konto ist bisher keinem Benutzer zugeordnet. Bitte verknüpfen Sie deses in Ihrem Profil-Einstellungen.', - 'social_account_register_instructions' => 'Wenn Sie bisher keinen Social-Media Konto besitzen können Sie ein solches Konto mit der :socialAccount Option anlegen.', - 'social_driver_not_found' => 'Social-Media Konto Treiber nicht gefunden', - 'social_driver_not_configured' => 'Ihr :socialAccount Konto ist nicht korrekt konfiguriert.', + 'social_account_in_use' => 'Dieses :socialAccount-Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount-Konto an.', + 'social_account_email_in_use' => 'Die E-Mail-Adresse ":email" ist bereits registriert. Wenn Sie bereits registriert sind, können Sie Ihr :socialAccount-Konto in Ihren Profil-Einstellungen verknüpfen.', + 'social_account_existing' => 'Dieses :socialAccount-Konto ist bereits mit Ihrem Profil verknüpft.', + 'social_account_already_used_existing' => 'Dieses :socialAccount-Konto wird bereits von einem anderen Benutzer verwendet.', + 'social_account_not_used' => 'Dieses :socialAccount-Konto ist bisher keinem Benutzer zugeordnet. Sie können es in Ihren Profil-Einstellung.', + 'social_account_register_instructions' => 'Wenn Sie bisher keinen Social-Media Konto besitzen, können Sie ein solches Konto mit der :socialAccount Option anlegen.', + 'social_driver_not_found' => 'Treiber für Social-Media-Konten nicht gefunden', + 'social_driver_not_configured' => 'Ihr :socialAccount-Konto ist nicht korrekt konfiguriert.', // System 'path_not_writable' => 'Die Datei kann nicht in den angegebenen Pfad :filePath hochgeladen werden. Stellen Sie sicher, dass dieser Ordner auf dem Server beschreibbar ist.', 'cannot_get_image_from_url' => 'Bild konnte nicht von der URL :url geladen werden.', - 'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfen Sie, ob Sie die GD PHP Erweiterung installiert haben.', - 'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.', + 'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfen Sie, ob die GD PHP-Erweiterung installiert ist.', + 'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.', 'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.', // Attachments - 'attachment_page_mismatch' => 'Die Seite stimmt nach dem Hochladen des Anhangs nicht überein.', + 'attachment_page_mismatch' => 'Die Seite stimmte nach dem Hochladen des Anhangs nicht überein.', // Pages 'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stellen Sie sicher, dass Sie mit dem Internet verbunden sind, bevor Sie den Entwurf dieser Seite speichern.', @@ -47,24 +47,31 @@ return [ 'book_not_found' => 'Buch nicht gefunden', 'page_not_found' => 'Seite nicht gefunden', 'chapter_not_found' => 'Kapitel nicht gefunden', - 'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden.', - 'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.', - 'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern', + 'selected_book_not_found' => 'Das gewählte Buch wurde nicht gefunden.', + 'selected_book_chapter_not_found' => 'Das gewählte Buch oder Kapitel wurde nicht gefunden.', + 'guests_cannot_save_drafts' => 'Gäste können keine Entwürfe speichern', // Users - 'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen.', - 'users_cannot_delete_guest' => 'Sie können den Gast-Benutzer nicht löschen', + 'users_cannot_delete_only_admin' => 'Sie können den einzigen Administrator nicht löschen.', + 'users_cannot_delete_guest' => 'Sie können den Gast-Benutzer nicht löschen', // Roles 'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden.', - 'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden', - 'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden solange sie als Standardrolle für neue Registrierungen gesetzt ist', + 'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gelöscht werden', + 'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gelöscht werden, solange sie als Standardrolle für neue Registrierungen gesetzt ist', + + // Comments + 'comment_list' => 'Beim Abrufen der Kommentare ist ein Fehler aufgetreten.', + 'cannot_add_comment_to_draft' => 'Du kannst keine Kommentare zu einem Entwurf hinzufügen.', + 'comment_add' => 'Beim Hinzufügen des Kommentars ist ein Fehler aufgetreten.', + 'comment_delete' => 'Beim Löschen des Kommentars ist ein Fehler aufgetreten.', + 'empty_comment' => 'Kann keinen leeren Kommentar hinzufügen', // Error pages '404_page_not_found' => 'Seite nicht gefunden', - 'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben wurde nicht gefunden.', - 'return_home' => 'Zurück zur Startseite', + 'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben, wurde nicht gefunden.', + 'return_home' => 'Zurück zur Startseite', 'error_occurred' => 'Es ist ein Fehler aufgetreten', 'app_down' => ':appName befindet sich aktuell im Wartungsmodus.', - 'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.', + 'back_soon' => 'Wir werden so schnell wie möglich wieder online sein.' ]; diff --git a/resources/lang/de/pagination.php b/resources/lang/de/pagination.php index a3bf7c8c8..6ed0e30f0 100644 --- a/resources/lang/de/pagination.php +++ b/resources/lang/de/pagination.php @@ -14,6 +14,6 @@ return [ */ 'previous' => '« Vorherige', - 'next' => 'Nächste »', + 'next' => 'Nächste »', ]; diff --git a/resources/lang/de/passwords.php b/resources/lang/de/passwords.php index c44b49baa..25ed05a04 100644 --- a/resources/lang/de/passwords.php +++ b/resources/lang/de/passwords.php @@ -13,10 +13,10 @@ return [ | */ - 'password' => 'Passörter müssen mindestens sechs Zeichen enthalten und die Wiederholung muss identisch sein.', - 'user' => "Wir können keinen Benutzer mit dieser E-Mail Adresse finden.", - 'token' => 'Dieser Passwort-Reset-Token ist ungültig.', - 'sent' => 'Wir haben Ihnen eine E-Mail mit einem Link zum Zurücksetzen des Passworts zugesendet!', - 'reset' => 'Ihr Passwort wurde zurückgesetzt!', + 'password' => 'Passörter müssen mindestens sechs Zeichen enthalten und die Wiederholung muss übereinstimmen.', + 'user' => "Es konnte kein Benutzer mit dieser E-Mail-Adresse gefunden werden.", + 'token' => 'Dieser Link zum Zurücksetzen des Passwortes ist ungültig.', + 'sent' => 'Wir haben Ihnen eine E-Mail mit einem Link zum Zurücksetzen des Passworts zugesendet!', + 'reset' => 'Ihr Passwort wurde zurückgesetzt!', ]; diff --git a/resources/lang/de/settings.php b/resources/lang/de/settings.php index 668eecf33..9435ec808 100644 --- a/resources/lang/de/settings.php +++ b/resources/lang/de/settings.php @@ -20,17 +20,17 @@ return [ 'app_name' => 'Anwendungsname', 'app_name_desc' => 'Dieser Name wird im Header und in E-Mails angezeigt.', 'app_name_header' => 'Anwendungsname im Header anzeigen?', - 'app_public_viewing' => 'Öffentliche Ansicht erlauben?', - 'app_secure_images' => 'Erhöhte Sicherheit für Bilduploads aktivieren?', - 'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugriff zu verhindern.', + 'app_public_viewing' => 'Öffentliche Ansicht erlauben?', + 'app_secure_images' => 'Erhöhte Sicherheit für hochgeladene Bilder aktivieren?', + 'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu eratene, Zeichenketten zu Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnisindizes deaktiviert sind, um einen einfachen Zugriff zu verhindern.', 'app_editor' => 'Seiteneditor', - 'app_editor_desc' => 'Wählen sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.', + 'app_editor_desc' => 'Wählen Sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.', 'app_custom_html' => 'Benutzerdefinierter HTML Inhalt', - 'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS Styles anzupassen oder Analytics Code hinzuzufügen.', + 'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS Styles anzupassen oder Analytics-Code hinzuzufügen.', 'app_logo' => 'Anwendungslogo', - 'app_logo_desc' => 'Dieses Bild sollte 43px hoch sein.
Größere Bilder werden verkleinert.', - 'app_primary_color' => 'Primäre Anwendungsfarbe', - 'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein.
Wenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt.', + 'app_logo_desc' => "Dieses Bild sollte 43px hoch sein.\nGrößere Bilder werden verkleinert.", + 'app_primary_color' => 'Primäre Anwendungsfarbe', + 'app_primary_color_desc' => "Dies sollte ein HEX Wert sein.\nWenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt.", /** * Registration settings @@ -39,11 +39,11 @@ return [ 'reg_settings' => 'Registrierungseinstellungen', 'reg_allow' => 'Registrierung erlauben?', 'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung', - 'reg_confirm_email' => 'Bestätigung per E-Mail erforderlich?', - 'reg_confirm_email_desc' => 'Falls die Einschränkung für Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.', - 'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken', - 'reg_confirm_restrict_domain_desc' => 'Fügen sie eine, durch Komma getrennte, Liste von E-Mail Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können.
Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung ändern.', - 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt', + 'reg_confirm_email' => 'Bestätigung per E-Mail erforderlich?', + 'reg_confirm_email_desc' => 'Falls die Einschränkung für Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.', + 'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken', + 'reg_confirm_restrict_domain_desc' => "Fügen sie eine durch Komma getrennte Liste von Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können.\nHinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung ändern.", + 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt', /** * Role settings @@ -53,31 +53,31 @@ return [ 'role_user_roles' => 'Benutzer-Rollen', 'role_create' => 'Neue Rolle anlegen', 'role_create_success' => 'Rolle erfolgreich angelegt', - 'role_delete' => 'Rolle löschen', - 'role_delete_confirm' => 'Sie möchten die Rolle \':roleName\' löschen.', - 'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Sie können unten eine neue Rolle auswählen, die Sie diesen Benutzern zuordnen möchten.', + 'role_delete' => 'Rolle löschen', + 'role_delete_confirm' => 'Sie möchten die Rolle ":roleName" löschen.', + 'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Sie können unten eine neue Rolle auswählen, die Sie diesen Benutzern zuordnen möchten.', 'role_delete_no_migration' => "Den Benutzern keine andere Rolle zuordnen", - 'role_delete_sure' => 'Sind Sie sicher, dass Sie diese Rolle löschen möchten?', - 'role_delete_success' => 'Rolle erfolgreich gelöscht', + 'role_delete_sure' => 'Sind Sie sicher, dass Sie diese Rolle löschen möchten?', + 'role_delete_success' => 'Rolle erfolgreich gelöscht', 'role_edit' => 'Rolle bearbeiten', - 'role_details' => 'Rollen-Details', + 'role_details' => 'Rollendetails', 'role_name' => 'Rollenname', 'role_desc' => 'Kurzbeschreibung der Rolle', 'role_system' => 'System-Berechtigungen', 'role_manage_users' => 'Benutzer verwalten', - 'role_manage_roles' => 'Rollen & Rollen-Berechtigungen verwalten', - 'role_manage_entity_permissions' => 'Alle Buch-, Kapitel und Seiten-Berechtigungen verwalten', - 'role_manage_own_entity_permissions' => 'Nur Berechtigungen eigener Bücher, Kapitel und Seiten verwalten', - 'role_manage_settings' => 'Globaleinstellungen verwalrten', + 'role_manage_roles' => 'Rollen und Rollen-Berechtigungen verwalten', + 'role_manage_entity_permissions' => 'Alle Buch-, Kapitel- und Seiten-Berechtigungen verwalten', + 'role_manage_own_entity_permissions' => 'Nur Berechtigungen eigener Bücher, Kapitel und Seiten verwalten', + 'role_manage_settings' => 'Globaleinstellungen verwalten', 'role_asset' => 'Berechtigungen', - 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.', + 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.', 'role_all' => 'Alle', 'role_own' => 'Eigene', - 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt', 'role_save' => 'Rolle speichern', 'role_update_success' => 'Rolle erfolgreich gespeichert', 'role_users' => 'Dieser Rolle zugeordnete Benutzer', - 'role_users_none' => 'Bisher sind dieser Rolle keiner Benutzer zugeordnet,', + 'role_users_none' => 'Bisher sind dieser Rolle keine Benutzer zugeordnet', /** * Users @@ -85,27 +85,27 @@ return [ 'users' => 'Benutzer', 'user_profile' => 'Benutzerprofil', - 'users_add_new' => 'Benutzer hinzufügen', + 'users_add_new' => 'Benutzer hinzufügen', 'users_search' => 'Benutzer suchen', 'users_role' => 'Benutzerrollen', 'users_external_auth_id' => 'Externe Authentifizierungs-ID', - 'users_password_warning' => 'Füllen Sie die folgenden Felder nur aus, wenn Sie Ihr Passwort ändern möchten:', - 'users_system_public' => 'Dieser Benutzer repräsentiert alle Gast-Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.', - 'users_delete' => 'Benutzer löschen', - 'users_delete_named' => 'Benutzer :userName löschen', - 'users_delete_warning' => 'Sie möchten den Benutzer \':userName\' gänzlich aus dem System löschen.', - 'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?', - 'users_delete_success' => 'Benutzer erfolgreich gelöscht.', + 'users_password_warning' => 'Füllen Sie die folgenden Felder nur aus, wenn Sie Ihr Passwort ändern möchten:', + 'users_system_public' => 'Dieser Benutzer repräsentiert alle unangemeldeten Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.', + 'users_delete' => 'Benutzer löschen', + 'users_delete_named' => 'Benutzer ":userName" löschen', + 'users_delete_warning' => 'Der Benutzer ":userName" wird aus dem System gelöscht.', + 'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?', + 'users_delete_success' => 'Benutzer erfolgreich gelöscht.', 'users_edit' => 'Benutzer bearbeiten', 'users_edit_profile' => 'Profil bearbeiten', 'users_edit_success' => 'Benutzer erfolgreich aktualisisert', 'users_avatar' => 'Benutzer-Bild', - 'users_avatar_desc' => 'Dieses Bild sollte einen Durchmesser von ca. 256px haben.', + 'users_avatar_desc' => 'Das Bild sollte eine Auflösung von 256x256px haben.', 'users_preferred_language' => 'Bevorzugte Sprache', 'users_social_accounts' => 'Social-Media Konten', - 'users_social_accounts_info' => 'Hier können Sie andere Social-Media Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Sie ein Social-Media Konto hier lösen, bleibt der Zugriff erhalteb. Entfernen Sie in diesem Falle die Berechtigung in Ihren Profil-Einstellungen des verknüpften Social-Media Kontos.', - 'users_social_connect' => 'Social-Media Konto verknüpfen', - 'users_social_disconnect' => 'Social-Media Kontoverknüpfung lösen', - 'users_social_connected' => ':socialAccount Konto wurde erfolgreich mit dem Profil verknüpft.', - 'users_social_disconnected' => ':socialAccount Konto wurde erfolgreich vom Profil gelöst.', + 'users_social_accounts_info' => 'Hier können Sie andere Social-Media-Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Sie ein Social-Media Konto lösen, bleibt der Zugriff erhalten. Entfernen Sie in diesem Falle die Berechtigung in Ihren Profil-Einstellungen des verknüpften Social-Media-Kontos.', + 'users_social_connect' => 'Social-Media-Konto verknüpfen', + 'users_social_disconnect' => 'Social-Media-Konto lösen', + 'users_social_connected' => ':socialAccount-Konto wurde erfolgreich mit dem Profil verknüpft.', + 'users_social_disconnected' => ':socialAccount-Konto wurde erfolgreich vom Profil gelöst.', ]; diff --git a/resources/lang/de/validation.php b/resources/lang/de/validation.php index 3a6a1bc15..5ac4b1b27 100644 --- a/resources/lang/de/validation.php +++ b/resources/lang/de/validation.php @@ -19,54 +19,54 @@ return [ 'alpha' => ':attribute kann nur Buchstaben enthalten.', 'alpha_dash' => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.', 'alpha_num' => ':attribute kann nur Buchstaben und Zahlen enthalten.', - 'array' => ':attribute muss eine Array sein.', + 'array' => ':attribute muss ein Array sein.', 'before' => ':attribute muss ein Datum vor :date sein.', 'between' => [ 'numeric' => ':attribute muss zwischen :min und :max liegen.', - 'file' => ':attribute muss zwischen :min und :max Kilobytes groß sein.', + 'file' => ':attribute muss zwischen :min und :max Kilobytes groß sein.', 'string' => ':attribute muss zwischen :min und :max Zeichen lang sein.', 'array' => ':attribute muss zwischen :min und :max Elemente enthalten.', ], 'boolean' => ':attribute Feld muss wahr oder falsch sein.', - 'confirmed' => ':attribute Bestätigung stimmt nicht überein.', + 'confirmed' => ':attribute stimmt nicht überein.', 'date' => ':attribute ist kein valides Datum.', 'date_format' => ':attribute entspricht nicht dem Format :format.', - 'different' => ':attribute und :other müssen unterschiedlich sein.', + 'different' => ':attribute und :other müssen unterschiedlich sein.', 'digits' => ':attribute muss :digits Stellen haben.', 'digits_between' => ':attribute muss zwischen :min und :max Stellen haben.', - 'email' => ':attribute muss eine valide E-Mail Adresse sein.', - 'filled' => ':attribute Feld ist erforderlich.', - 'exists' => 'Markiertes :attribute ist ungültig.', + 'email' => ':attribute muss eine valide E-Mail-Adresse sein.', + 'filled' => ':attribute ist erforderlich.', + 'exists' => ':attribute ist ungültig.', 'image' => ':attribute muss ein Bild sein.', - 'in' => 'Markiertes :attribute ist ungültig.', + 'in' => ':attribute ist ungültig.', 'integer' => ':attribute muss eine Zahl sein.', 'ip' => ':attribute muss eine valide IP-Adresse sein.', 'max' => [ - 'numeric' => ':attribute darf nicht größer als :max sein.', - 'file' => ':attribute darf nicht größer als :max Kilobyte sein.', - 'string' => ':attribute darf nicht länger als :max Zeichen sein.', + 'numeric' => ':attribute darf nicht größer als :max sein.', + 'file' => ':attribute darf nicht größer als :max Kilobyte sein.', + 'string' => ':attribute darf nicht länger als :max Zeichen sein.', 'array' => ':attribute darf nicht mehr als :max Elemente enthalten.', ], 'mimes' => ':attribute muss eine Datei vom Typ: :values sein.', 'min' => [ - 'numeric' => ':attribute muss mindestens :min. sein', - 'file' => ':attribute muss mindestens :min Kilobyte groß sein.', + 'numeric' => ':attribute muss mindestens :min sein', + 'file' => ':attribute muss mindestens :min Kilobyte groß sein.', 'string' => ':attribute muss mindestens :min Zeichen lang sein.', 'array' => ':attribute muss mindesten :min Elemente enthalten.', ], - 'not_in' => 'Markiertes :attribute ist ungültig.', + 'not_in' => ':attribute ist ungültig.', 'numeric' => ':attribute muss eine Zahl sein.', - 'regex' => ':attribute Format ist ungültig.', - 'required' => ':attribute Feld ist erforderlich.', - 'required_if' => ':attribute Feld ist erforderlich, wenn :other :value ist.', - 'required_with' => ':attribute Feld ist erforderlich, wenn :values vorhanden ist.', - 'required_with_all' => ':attribute Feld ist erforderlich, wenn :values vorhanden sind.', - 'required_without' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden ist.', - 'required_without_all' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden sind.', - 'same' => ':attribute und :other muss übereinstimmen.', + 'regex' => ':attribute ist in einem ungültigen Format.', + 'required' => ':attribute ist erforderlich.', + 'required_if' => ':attribute ist erforderlich, wenn :other :value ist.', + 'required_with' => ':attribute ist erforderlich, wenn :values vorhanden ist.', + 'required_with_all' => ':attribute ist erforderlich, wenn :values vorhanden sind.', + 'required_without' => ':attribute ist erforderlich, wenn :values nicht vorhanden ist.', + 'required_without_all' => ':attribute ist erforderlich, wenn :values nicht vorhanden sind.', + 'same' => ':attribute und :other müssen übereinstimmen.', 'size' => [ 'numeric' => ':attribute muss :size sein.', - 'file' => ':attribute muss :size Kilobytes groß sein.', + 'file' => ':attribute muss :size Kilobytes groß sein.', 'string' => ':attribute muss :size Zeichen lang sein.', 'array' => ':attribute muss :size Elemente enthalten.', ], diff --git a/resources/lang/en/activities.php b/resources/lang/en/activities.php index 56af4ca07..187fe1e53 100644 --- a/resources/lang/en/activities.php +++ b/resources/lang/en/activities.php @@ -37,4 +37,6 @@ return [ 'book_sort' => 'sorted book', 'book_sort_notification' => 'Book Successfully Re-sorted', + // Other + 'commented_on' => 'commented on', ]; diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php index e1d74c95e..269905a59 100644 --- a/resources/lang/en/common.php +++ b/resources/lang/en/common.php @@ -10,6 +10,7 @@ return [ 'save' => 'Save', 'continue' => 'Continue', 'select' => 'Select', + 'more' => 'More', /** * Form Labels @@ -28,6 +29,7 @@ return [ 'edit' => 'Edit', 'sort' => 'Sort', 'move' => 'Move', + 'reply' => 'Reply', 'delete' => 'Delete', 'search' => 'Search', 'search_clear' => 'Clear Search', @@ -35,7 +37,6 @@ return [ 'remove' => 'Remove', 'add' => 'Add', - /** * Misc */ @@ -44,6 +45,7 @@ return [ 'no_items' => 'No items available', 'back_to_top' => 'Back to top', 'toggle_details' => 'Toggle Details', + 'details' => 'Details', /** * Header diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 450f4ce48..4dc5ccc38 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -19,7 +19,6 @@ return [ 'meta_created_name' => 'Created :timeLength by :user', 'meta_updated' => 'Updated :timeLength', 'meta_updated_name' => 'Updated :timeLength by :user', - 'x_pages' => ':count Pages', 'entity_select' => 'Entity Select', 'images' => 'Images', 'my_recent_drafts' => 'My Recent Drafts', @@ -70,14 +69,17 @@ return [ */ 'book' => 'Book', 'books' => 'Books', + 'x_books' => ':count Book|:count Books', 'books_empty' => 'No books have been created', 'books_popular' => 'Popular Books', 'books_recent' => 'Recent Books', + 'books_new' => 'New Books', 'books_popular_empty' => 'The most popular books will appear here.', + 'books_new_empty' => 'The most recently created books will appear here.', 'books_create' => 'Create New Book', 'books_delete' => 'Delete Book', 'books_delete_named' => 'Delete Book :bookName', - 'books_delete_explain' => 'This will delete the book with the name \':bookName\', All pages and chapters will be removed.', + 'books_delete_explain' => 'This will delete the book with the name \':bookName\'. All pages and chapters will be removed.', 'books_delete_confirmation' => 'Are you sure you want to delete this book?', 'books_edit' => 'Edit Book', 'books_edit_named' => 'Edit Book :bookName', @@ -103,13 +105,13 @@ return [ */ 'chapter' => 'Chapter', 'chapters' => 'Chapters', + 'x_chapters' => ':count Chapter|:count Chapters', 'chapters_popular' => 'Popular Chapters', 'chapters_new' => 'New Chapter', 'chapters_create' => 'Create New Chapter', 'chapters_delete' => 'Delete Chapter', 'chapters_delete_named' => 'Delete Chapter :chapterName', - 'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\', All pages will be removed - and added directly to the parent book.', + 'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages will be removed and added directly to the parent book.', 'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?', 'chapters_edit' => 'Edit Chapter', 'chapters_edit_named' => 'Edit Chapter :chapterName', @@ -128,6 +130,7 @@ return [ */ 'page' => 'Page', 'pages' => 'Pages', + 'x_pages' => ':count Page|:count Pages', 'pages_popular' => 'Popular Pages', 'pages_new' => 'New Page', 'pages_attachments' => 'Attachments', @@ -164,6 +167,7 @@ return [ 'pages_move_success' => 'Page moved to ":parentName"', 'pages_permissions' => 'Page Permissions', 'pages_permissions_success' => 'Page permissions updated', + 'pages_revision' => 'Revision', 'pages_revisions' => 'Page Revisions', 'pages_revisions_named' => 'Page Revisions for :pageName', 'pages_revision_named' => 'Page Revision for :pageName', @@ -234,4 +238,23 @@ 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' => 'Leave a comment here', + 'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments', + 'comment_save' => 'Save Comment', + 'comment_saving' => 'Saving comment...', + 'comment_deleting' => 'Deleting comment...', + 'comment_new' => 'New Comment', + 'comment_created' => 'commented :createDiff', + 'comment_updated' => 'Updated :updateDiff by :username', + 'comment_deleted_success' => 'Comment deleted', + 'comment_created_success' => 'Comment added', + 'comment_updated_success' => 'Comment updated', + 'comment_delete_confirm' => 'Are you sure you want to delete this comment?', + 'comment_in_reply_to' => 'In reply to :commentId', ]; \ No newline at end of file diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php index c4578a37a..09158caac 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 / updating 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 old mode 100644 new mode 100755 index 3eec7737f..8b07ccea3 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -31,6 +31,9 @@ return [ 'app_logo_desc' => 'This image should be 43px in height.
Large images will be scaled down.', 'app_primary_color' => 'Application primary color', 'app_primary_color_desc' => 'This should be a hex value.
Leave empty to reset to the default color.', + 'app_homepage' => 'Application Homepage', + 'app_homepage_desc' => 'Select a page to show on the homepage instead of the default view. Page permissions are ignored for selected pages.', + 'app_homepage_default' => 'Default homepage view chosen', /** * Registration settings @@ -123,6 +126,7 @@ return [ 'sk' => 'Slovensky', 'ja' => '日本語', 'pl' => 'Polski', + 'it' => 'Italian' ] /////////////////////////////////// ]; diff --git a/resources/lang/es/entities.php b/resources/lang/es/entities.php index d6b2810bc..ba4ca955a 100644 --- a/resources/lang/es/entities.php +++ b/resources/lang/es/entities.php @@ -214,4 +214,12 @@ 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í', + 'comment_save' => 'Guardar comentario', ]; diff --git a/resources/lang/es/errors.php b/resources/lang/es/errors.php index 1e39a3cb8..e488b6a1b 100644 --- a/resources/lang/es/errors.php +++ b/resources/lang/es/errors.php @@ -67,4 +67,11 @@ return [ 'error_occurred' => 'Ha ocurrido un error', 'app_down' => 'La aplicación :appName se encuentra caída en este momento', 'back_soon' => 'Volverá a estar operativa en corto tiempo.', + + // Comments + 'comment_list' => 'Se ha producido un error al buscar los comentarios.', + 'cannot_add_comment_to_draft' => 'No puedes añadir comentarios a un borrador.', + 'comment_add' => 'Se ha producido un error al añadir el comentario.', + 'comment_delete' => 'Se ha producido un error al eliminar el comentario.', + 'empty_comment' => 'No se puede agregar un comentario vacío.', ]; diff --git a/resources/lang/fr/entities.php b/resources/lang/fr/entities.php index 17b4ea913..d4d9d2475 100644 --- a/resources/lang/fr/entities.php +++ b/resources/lang/fr/entities.php @@ -213,4 +213,12 @@ return [ 'profile_not_created_pages' => ':userName n\'a pas créé de page', 'profile_not_created_chapters' => ':userName n\'a pas créé de chapitre', 'profile_not_created_books' => ':userName n\'a pas créé de livre', + + /** + * Comments + */ + 'comment' => 'Commentaire', + 'comments' => 'Commentaires', + 'comment_placeholder' => 'Entrez vos commentaires ici', + 'comment_save' => 'Enregistrer le commentaire', ]; diff --git a/resources/lang/fr/errors.php b/resources/lang/fr/errors.php index 4197b1708..9e20147b6 100644 --- a/resources/lang/fr/errors.php +++ b/resources/lang/fr/errors.php @@ -67,4 +67,11 @@ return [ 'error_occurred' => 'Une erreur est survenue', 'app_down' => ':appName n\'est pas en service pour le moment', 'back_soon' => 'Nous serons bientôt de retour.', + + // comments + 'comment_list' => 'Une erreur s\'est produite lors de la récupération des commentaires.', + 'cannot_add_comment_to_draft' => 'Vous ne pouvez pas ajouter de commentaires à un projet.', + 'comment_add' => 'Une erreur s\'est produite lors de l\'ajout du commentaire.', + 'comment_delete' => 'Une erreur s\'est produite lors de la suppression du commentaire.', + 'empty_comment' => 'Impossible d\'ajouter un commentaire vide.', ]; diff --git a/resources/lang/it/activities.php b/resources/lang/it/activities.php new file mode 100755 index 000000000..3fa5ad17d --- /dev/null +++ b/resources/lang/it/activities.php @@ -0,0 +1,40 @@ + 'ha creato la pagina', + 'page_create_notification' => 'Pagina Creata Correttamente', + 'page_update' => 'ha aggiornato la pagina', + 'page_update_notification' => 'Pagina Aggiornata Correttamente', + 'page_delete' => 'ha eliminato la pagina', + 'page_delete_notification' => 'Pagina Eliminata Correttamente', + 'page_restore' => 'ha ripristinato la pagina', + 'page_restore_notification' => 'Pagina Ripristinata Correttamente', + 'page_move' => 'ha mosso la pagina', + + // Chapters + 'chapter_create' => 'ha creato il capitolo', + 'chapter_create_notification' => 'Capitolo Creato Correttamente', + 'chapter_update' => 'ha aggiornato il capitolo', + 'chapter_update_notification' => 'Capitolo Aggiornato Correttamente', + 'chapter_delete' => 'ha eliminato il capitolo', + 'chapter_delete_notification' => 'Capitolo Eliminato Correttamente', + 'chapter_move' => 'ha mosso il capitolo', + + // Books + 'book_create' => 'ha creato il libro', + 'book_create_notification' => 'Libro Creato Correttamente', + 'book_update' => 'ha aggiornato il libro', + 'book_update_notification' => 'Libro Aggiornato Correttamente', + 'book_delete' => 'ha eliminato il libro', + 'book_delete_notification' => 'Libro Eliminato Correttamente', + 'book_sort' => 'ha ordinato il libro', + 'book_sort_notification' => 'Libro Riordinato Correttamente', + +]; diff --git a/resources/lang/it/auth.php b/resources/lang/it/auth.php new file mode 100755 index 000000000..68fee41a5 --- /dev/null +++ b/resources/lang/it/auth.php @@ -0,0 +1,76 @@ + 'Credenziali errate.', + 'throttle' => 'Troppi tentativi di login. Riprova in :seconds secondi.', + + /** + * Login & Register + */ + 'sign_up' => 'Registrati', + 'log_in' => 'Login', + 'log_in_with' => 'Login con :socialDriver', + 'sign_up_with' => 'Registrati con :socialDriver', + 'logout' => 'Esci', + + 'name' => 'Nome', + 'username' => 'Username', + 'email' => 'Email', + 'password' => 'Password', + 'password_confirm' => 'Conferma Password', + 'password_hint' => 'Deve essere più di 5 caratteri', + 'forgot_password' => 'Password dimenticata?', + 'remember_me' => 'Ricordami', + 'ldap_email_hint' => 'Inserisci un email per usare quest\'account.', + 'create_account' => 'Crea Account', + 'social_login' => 'Login Social', + 'social_registration' => 'Registrazione Social', + 'social_registration_text' => 'Registrati usando un altro servizio.', + + 'register_thanks' => 'Grazie per esserti registrato!', + 'register_confirm' => 'Controlla la tua mail e clicca il bottone di conferma per accedere a :appName.', + 'registrations_disabled' => 'La registrazione è disabilitata', + 'registration_email_domain_invalid' => 'Questo dominio della mail non ha accesso a questa applicazione', + 'register_success' => 'Grazie per la registrazione! Sei registrato e loggato.', + + + /** + * Password Reset + */ + 'reset_password' => 'Reimposta Password', + 'reset_password_send_instructions' => 'Inserisci il tuo indirizzo sotto e ti verrà inviata una mail contenente un link per resettare la tua password.', + 'reset_password_send_button' => 'Invia Link Reset', + 'reset_password_sent_success' => 'Un link di reset è stato mandato a :email.', + 'reset_password_success' => 'La tua password è stata resettata correttamente.', + + 'email_reset_subject' => 'Reimposta la password di :appName', + 'email_reset_text' => 'Stai ricevendo questa mail perché abbiamo ricevuto una richiesta di reset della password per il tuo account.', + 'email_reset_not_requested' => 'Se non hai richiesto un reset della password, ignora questa mail.', + + + /** + * Email Confirmation + */ + 'email_confirm_subject' => 'Conferma email per :appName', + 'email_confirm_greeting' => 'Grazie per esserti registrato a :appName!', + 'email_confirm_text' => 'Conferma il tuo indirizzo email cliccando il pulsante sotto:', + 'email_confirm_action' => 'Conferma Email', + 'email_confirm_send_error' => 'La conferma della mail è richiesta ma non è stato possibile mandare la mail. Contatta l\'amministratore.', + 'email_confirm_success' => 'La tua mail è stata confermata!', + 'email_confirm_resent' => 'Mail di conferma reinviata, controlla la tua posta.', + + 'email_not_confirmed' => 'Indirizzo Email Non Confermato', + 'email_not_confirmed_text' => 'Il tuo indirizzo email non è ancora stato confermato.', + 'email_not_confirmed_click_link' => 'Clicca il link nella mail mandata subito dopo la tua registrazione.', + 'email_not_confirmed_resend' => 'Se non riesci a trovare la mail puoi rimandarla cliccando il pulsante sotto.', + 'email_not_confirmed_resend_button' => 'Reinvia Conferma', +]; \ No newline at end of file diff --git a/resources/lang/it/common.php b/resources/lang/it/common.php new file mode 100755 index 000000000..8c25bfb3d --- /dev/null +++ b/resources/lang/it/common.php @@ -0,0 +1,60 @@ + 'Annulla', + 'confirm' => 'Conferma', + 'back' => 'Indietro', + 'save' => 'Salva', + 'continue' => 'Continua', + 'select' => 'Seleziona', + 'more' => 'More', + + /** + * Form Labels + */ + 'name' => 'Nome', + 'description' => 'Descrizione', + 'role' => 'Ruolo', + + /** + * Actions + */ + 'actions' => 'Azioni', + 'view' => 'Visualizza', + 'create' => 'Crea', + 'update' => 'Aggiorna', + 'edit' => 'Modifica', + 'sort' => 'Ordina', + 'move' => 'Muovi', + 'delete' => 'Elimina', + 'search' => 'Cerca', + 'search_clear' => 'Pulisci Ricerca', + 'reset' => 'Reset', + 'remove' => 'Rimuovi', + 'add' => 'Aggiungi', + + /** + * Misc + */ + 'deleted_user' => 'Utente Eliminato', + 'no_activity' => 'Nessuna attività da mostrare', + 'no_items' => 'Nessun elemento disponibile', + 'back_to_top' => 'Torna in alto', + 'toggle_details' => 'Mostra Dettagli', + 'details' => 'Dettagli', + + /** + * Header + */ + 'view_profile' => 'Visualizza Profilo', + 'edit_profile' => 'Modifica Profilo', + + /** + * Email Content + */ + 'email_action_help' => 'Se hai problemi nel cliccare il pulsante ":actionText", copia e incolla lo URL sotto nel tuo browser:', + 'email_rights' => 'Tutti i diritti riservati', +]; \ No newline at end of file diff --git a/resources/lang/it/components.php b/resources/lang/it/components.php new file mode 100755 index 000000000..081a4c0e6 --- /dev/null +++ b/resources/lang/it/components.php @@ -0,0 +1,32 @@ + 'Selezione Immagine', + 'image_all' => 'Tutte', + 'image_all_title' => 'Visualizza tutte le immagini', + 'image_book_title' => 'Visualizza immagini caricate in questo libro', + 'image_page_title' => 'Visualizza immagini caricate in questa pagina', + 'image_search_hint' => 'Cerca immagine per nome', + 'image_uploaded' => 'Uploaded :uploadedDate', + 'image_load_more' => 'Carica Altre', + 'image_image_name' => 'Nome Immagine', + 'image_delete_confirm' => 'Questa immagine è usata nelle pagine elencate, clicca elimina nuovamente per confermare.', + 'image_select_image' => 'Seleziona Immagine', + 'image_dropzone' => 'Rilascia immagini o clicca qui per caricarle', + 'images_deleted' => 'Immagini Eliminate', + 'image_preview' => 'Anteprima Immagine', + 'image_upload_success' => 'Immagine caricata correttamente', + 'image_update_success' => 'Dettagli immagine aggiornati correttamente', + 'image_delete_success' => 'Immagine eliminata correttamente', + + /** + * Code editor + */ + 'code_editor' => 'Modifica Codice', + 'code_language' => 'Linguaggio Codice', + 'code_content' => 'Contenuto Codice', + 'code_save' => 'Salva Codice', +]; \ No newline at end of file diff --git a/resources/lang/it/entities.php b/resources/lang/it/entities.php new file mode 100755 index 000000000..0389dd54f --- /dev/null +++ b/resources/lang/it/entities.php @@ -0,0 +1,251 @@ + 'Creati di recente', + 'recently_created_pages' => 'Pagine create di recente', + 'recently_updated_pages' => 'Pagine aggiornate di recente', + 'recently_created_chapters' => 'Capitoli creati di recente', + 'recently_created_books' => 'Libri creati di recente', + 'recently_update' => 'Aggiornati di recente', + 'recently_viewed' => 'Visti di recente', + 'recent_activity' => 'Attività Recente', + 'create_now' => 'Creane uno ora', + 'revisions' => 'Versioni', + 'meta_revision' => 'Versione #:revisionCount', + 'meta_created' => 'Creato :timeLength', + 'meta_created_name' => 'Creato :timeLength da :user', + 'meta_updated' => 'Aggiornato :timeLength', + 'meta_updated_name' => 'Aggiornato :timeLength da :user', + 'x_pages' => ':count Pagine', + 'entity_select' => 'Selezione Entità', + 'images' => 'Immagini', + 'my_recent_drafts' => 'Bozze Recenti', + 'my_recently_viewed' => 'Visti di recente', + 'no_pages_viewed' => 'Non hai visto nessuna pagina', + 'no_pages_recently_created' => 'Nessuna pagina è stata creata di recente', + 'no_pages_recently_updated' => 'Nessuna pagina è stata aggiornata di recente', + 'export' => 'Esporta', + 'export_html' => 'File Contenuto Web', + 'export_pdf' => 'File PDF', + 'export_text' => 'File di testo', + + /** + * Permissions and restrictions + */ + 'permissions' => 'Permessi', + 'permissions_intro' => 'Una volta abilitati, questi permessi avranno la priorità su tutti gli altri.', + 'permissions_enable' => 'Abilita Permessi Custom', + 'permissions_save' => 'Salva Permessi', + + /** + * Search + */ + 'search_results' => 'Risultati Ricerca', + 'search_total_results_found' => ':count risultato trovato|:count risultati trovati', + 'search_clear' => 'Pulisci Ricerca', + 'search_no_pages' => 'Nessuna pagina corrisponde alla ricerca', + 'search_for_term' => 'Ricerca per :term', + 'search_more' => 'Più Risultati', + 'search_filters' => 'Filtri Ricerca', + 'search_content_type' => 'Tipo di Contenuto', + 'search_exact_matches' => 'Corrispondenza Esatta', + 'search_tags' => 'Ricerche Tag', + 'search_viewed_by_me' => 'Visti', + 'search_not_viewed_by_me' => 'Non visti', + 'search_permissions_set' => 'Permessi impostati', + 'search_created_by_me' => 'Creati da me', + 'search_updated_by_me' => 'Aggiornati da me', + 'search_updated_before' => 'Aggiornati prima del', + 'search_updated_after' => 'Aggiornati dopo il', + 'search_created_before' => 'Creati prima del', + 'search_created_after' => 'Creati dopo il', + 'search_set_date' => 'Imposta Data', + 'search_update' => 'Aggiorna Ricerca', + + /** + * Books + */ + 'book' => 'Libro', + 'books' => 'Libri', + 'books_empty' => 'Nessun libro è stato creato', + 'books_popular' => 'Libri Popolari', + 'books_recent' => 'Libri Recenti', + 'books_new' => 'Nuovi Libri', + 'books_popular_empty' => 'I libri più popolari appariranno qui.', + 'books_new_empty' => 'I libri creati più di recente appariranno qui.', + 'books_create' => 'Crea Nuovo Libro', + 'books_delete' => 'Elimina Libro', + 'books_delete_named' => 'Elimina il libro :bookName', + 'books_delete_explain' => 'Questo eliminerà il libro di nome \':bookName\'. Tutte le pagine e i capitoli saranno rimossi.', + 'books_delete_confirmation' => 'Sei sicuro di voler eliminare questo libro?', + 'books_edit' => 'Modifica Libro', + 'books_edit_named' => 'Modifica il libro :bookName', + 'books_form_book_name' => 'Nome Libro', + 'books_save' => 'Salva Libro', + 'books_permissions' => 'Permessi Libro', + 'books_permissions_updated' => 'Permessi del libro aggiornati', + 'books_empty_contents' => 'Non ci sono pagine o capitoli per questo libro.', + 'books_empty_create_page' => 'Crea una nuova pagina', + 'books_empty_or' => 'o', + 'books_empty_sort_current_book' => 'Ordina il libro corrente', + 'books_empty_add_chapter' => 'Aggiungi un capitolo', + 'books_permissions_active' => 'Permessi libro attivi', + 'books_search_this' => 'Cerca in questo libro', + 'books_navigation' => 'Navigazione Libro', + 'books_sort' => 'Ordina il contenuto del libro', + 'books_sort_named' => 'Ordina il libro :bookName', + 'books_sort_show_other' => 'Mostra Altri Libri', + 'books_sort_save' => 'Salva il nuovo ordine', + + /** + * Chapters + */ + 'chapter' => 'Capitolo', + 'chapters' => 'Capitoli', + 'chapters_popular' => 'Capitoli Popolari', + 'chapters_new' => 'Nuovo Capitolo', + 'chapters_create' => 'Crea un nuovo capitolo', + 'chapters_delete' => 'Elimina Capitolo', + 'chapters_delete_named' => 'Elimina il capitolo :chapterName', + 'chapters_delete_explain' => 'Questo eliminerà il capitolo \':chapterName\'. Tutte le pagine verranno spostate nel libro.', + 'chapters_delete_confirm' => 'Sei sicuro di voler eliminare questo capitolo?', + 'chapters_edit' => 'Elimina Capitolo', + 'chapters_edit_named' => 'Modifica il capitolo :chapterName', + 'chapters_save' => 'Salva Capitolo', + 'chapters_move' => 'Muovi Capitolo', + 'chapters_move_named' => 'Muovi il capitolo :chapterName', + 'chapter_move_success' => 'Capitolo mosso in :bookName', + 'chapters_permissions' => 'Permessi Capitolo', + 'chapters_empty' => 'Non ci sono pagine in questo capitolo.', + 'chapters_permissions_active' => 'Permessi Capitolo Attivi', + 'chapters_permissions_success' => 'Permessi Capitolo Aggiornati', + 'chapters_search_this' => 'Cerca in questo capitolo', + + /** + * Pages + */ + 'page' => 'Pagina', + 'pages' => 'Pagine', + 'pages_popular' => 'Pagine Popolari', + 'pages_new' => 'Nuova Pagina', + 'pages_attachments' => 'Allegati', + 'pages_navigation' => 'Page Navigation', + 'pages_delete' => 'Elimina Pagina', + 'pages_delete_named' => 'Elimina la pagina :pageName', + 'pages_delete_draft_named' => 'Delete Draft Page :pageName', + 'pages_delete_draft' => 'Elimina Bozza Pagina', + 'pages_delete_success' => 'Pagina eliminata', + 'pages_delete_draft_success' => 'Bozza di una pagina eliminata', + 'pages_delete_confirm' => 'Sei sicuro di voler eliminare questa pagina?', + 'pages_delete_draft_confirm' => 'Sei sicuro di voler eliminare la bozza di questa pagina?', + 'pages_editing_named' => 'Modifica :pageName', + 'pages_edit_toggle_header' => 'Toggle header', + 'pages_edit_save_draft' => 'Salva Bozza', + 'pages_edit_draft' => 'Modifica Bozza della pagina', + 'pages_editing_draft' => 'Modifica Bozza', + 'pages_editing_page' => 'Modifica Pagina', + 'pages_edit_draft_save_at' => 'Bozza salvata alle ', + 'pages_edit_delete_draft' => 'Elimina Bozza', + 'pages_edit_discard_draft' => 'Scarta Bozza', + 'pages_edit_set_changelog' => 'Imposta Changelog', + 'pages_edit_enter_changelog_desc' => 'Inserisci una breve descrizione dei cambiamenti che hai apportato', + 'pages_edit_enter_changelog' => 'Inserisci Changelog', + 'pages_save' => 'Salva Pagina', + 'pages_title' => 'Titolo Pagina', + 'pages_name' => 'Nome Pagina', + 'pages_md_editor' => 'Editor', + 'pages_md_preview' => 'Anteprima', + 'pages_md_insert_image' => 'Inserisci Immagina', + 'pages_md_insert_link' => 'Inserisci Link Entità', + 'pages_not_in_chapter' => 'La pagina non è in un capitolo', + 'pages_move' => 'Muovi Pagina', + 'pages_move_success' => 'Pagina mossa in ":parentName"', + 'pages_permissions' => 'Permessi Pagina', + 'pages_permissions_success' => 'Page permissions updated', + 'pages_revision' => 'Versione', + 'pages_revisions' => 'Versioni Pagina', + 'pages_revisions_named' => 'Versioni della pagina :pageName', + 'pages_revision_named' => 'Versione della pagina :pageName', + 'pages_revisions_created_by' => 'Creata Da', + 'pages_revisions_date' => 'Data Versione', + 'pages_revisions_number' => '#', + 'pages_revisions_changelog' => 'Changelog', + 'pages_revisions_changes' => 'Cambiamenti', + 'pages_revisions_current' => 'Versione Corrente', + 'pages_revisions_preview' => 'Anteprima', + 'pages_revisions_restore' => 'Ripristina', + 'pages_revisions_none' => 'Questa pagina non ha versioni', + 'pages_copy_link' => 'Copia Link', + 'pages_permissions_active' => 'Permessi Pagina Attivi', + 'pages_initial_revision' => 'Pubblicazione iniziale', + 'pages_initial_name' => 'Nuova Pagina', + 'pages_editing_draft_notification' => 'Stai modificando una bozza che è stata salvata il :timeDiff.', + 'pages_draft_edited_notification' => 'Questa pagina è stata aggiornata. È consigliabile scartare questa bozza.', + 'pages_draft_edit_active' => [ + 'start_a' => ':count hanno iniziato a modificare questa pagina', + 'start_b' => ':userName ha iniziato a modificare questa pagina', + 'time_a' => 'da quando le pagine sono state aggiornate', + 'time_b' => 'negli ultimi :minCount minuti', + 'message' => ':start :time. Assicurati di non sovrascrivere le modifiche degli altri!', + ], + 'pages_draft_discarded' => "Bozza scartata, l'editor è stato aggiornato con il contenuto corrente della pagina", + + /** + * Editor sidebar + */ + 'page_tags' => 'Tag Pagina', + 'tag' => 'Tag', + 'tags' => '', + 'tag_value' => 'Valore (Opzionale)', + 'tags_explain' => "Aggiungi tag per categorizzare meglio il contenuto. \n Puoi assegnare un valore ai tag per una migliore organizzazione.", + 'tags_add' => 'Aggiungi un altro tag', + 'attachments' => 'Allegati', + 'attachments_explain' => 'Carica alcuni file o allega link per visualizzarli nella pagina. Questi sono visibili nella sidebar della pagina.', + 'attachments_explain_instant_save' => 'I cambiamenti qui sono salvati istantaneamente.', + 'attachments_items' => 'Oggetti Allegati', + 'attachments_upload' => 'Carica File', + 'attachments_link' => 'Allega Link', + 'attachments_set_link' => 'Imposta Link', + 'attachments_delete_confirm' => "Clicca elimina nuovamente per confermare l'eliminazione di questo allegato.", + 'attachments_dropzone' => 'Rilascia file o clicca qui per allegare un file', + 'attachments_no_files' => 'Nessun file è stato caricato', + 'attachments_explain_link' => 'Puoi allegare un link se preferisci non caricare un file. Questo può essere un link a un\'altra pagina o a un file in un cloud.', + 'attachments_link_name' => 'Nome Link', + 'attachment_link' => 'Link allegato', + 'attachments_link_url' => 'Link al file', + 'attachments_link_url_hint' => 'Url del sito o del file', + 'attach' => 'Allega', + 'attachments_edit_file' => 'Modifica File', + 'attachments_edit_file_name' => 'Nome File', + 'attachments_edit_drop_upload' => 'Rilascia file o clicca qui per caricare e sovrascrivere', + 'attachments_order_updated' => 'Ordine allegato aggiornato', + 'attachments_updated_success' => 'Dettagli allegato aggiornati', + 'attachments_deleted' => 'Allegato eliminato', + 'attachments_file_uploaded' => 'File caricato correttamente', + 'attachments_file_updated' => 'File aggiornato correttamente', + 'attachments_link_attached' => 'Link allegato correttamente alla pagina', + + /** + * Profile View + */ + 'profile_user_for_x' => 'Utente da :time', + 'profile_created_content' => 'Contenuti Creati', + 'profile_not_created_pages' => ':userName non ha creato pagine', + 'profile_not_created_chapters' => ':userName non ha creato capitoli', + 'profile_not_created_books' => ':userName non ha creato libri', + + /** + * Comments + */ + 'comment' => 'Commento', + 'comments' => 'Commenti', + 'comment_count' => '1 Commento|:count Commenti', + 'comment_save' => 'Salva Commento', + 'comment_deleted_success' => 'Commento eliminato', + 'comment_created_success' => 'Commento aggiunto', + 'comment_updated_success' => 'Commento aggiornato', + 'comment_delete_confirm' => 'Questo rimuoverà il contenuto del commento?', +]; \ No newline at end of file diff --git a/resources/lang/it/errors.php b/resources/lang/it/errors.php new file mode 100755 index 000000000..569b33cf2 --- /dev/null +++ b/resources/lang/it/errors.php @@ -0,0 +1,77 @@ + 'Non hai il permesso di accedere alla pagina richiesta.', + 'permissionJson' => "Non hai il permesso di eseguire l'azione richiesta.", + + // Auth + 'error_user_exists_different_creds' => 'Un utente con la mail :email esiste già ma con credenziali differenti.', + 'email_already_confirmed' => 'La mail è già stata confermata, esegui il login.', + 'email_confirmation_invalid' => 'Questo token di conferma non è valido o già stato utilizzato, registrati nuovamente.', + 'email_confirmation_expired' => 'Il token di conferma è scaduto, è stata inviata una nuova mail di conferma.', + 'ldap_fail_anonymous' => 'Accesso LDAP fallito usando bind anonimo', + 'ldap_fail_authed' => 'Accesso LDAP fallito usando il dn e la password inseriti', + 'ldap_extension_not_installed' => 'L\'estensione PHP LDAP non è installata', + 'ldap_cannot_connect' => 'Impossibile connettersi al server ldap, connessione iniziale fallita', + 'social_no_action_defined' => 'Nessuna azione definita', + 'social_account_in_use' => 'Questo account :socialAccount è già utilizzato, prova a loggarti usando l\'opzione :socialAccount.', + 'social_account_email_in_use' => 'La mail :email è già in uso. Se hai già un account puoi connettere il tuo account :socialAccount dalle impostazioni del tuo profilo.', + 'social_account_existing' => 'Questo account :socialAccount è già connesso al tuo profilo.', + 'social_account_already_used_existing' => 'Questo accoutn :socialAccount è già utilizzato da un altro utente.', + 'social_account_not_used' => 'Questo account :socialAccount non è collegato a nessun utente. Collegalo nelle impostazioni del profilo. ', + 'social_account_register_instructions' => 'Se non hai ancora un account, puoi registrarti usando l\'opzione :socialAccount.', + 'social_driver_not_found' => 'Driver social non trovato', + 'social_driver_not_configured' => 'Le impostazioni di :socialAccount non sono configurate correttamente.', + + // System + 'path_not_writable' => 'La path :filePath non può essere scritta. Controlla che abbia i permessi corretti.', + 'cannot_get_image_from_url' => 'Impossibile scaricare immagine da :url', + 'cannot_create_thumbs' => 'Il server non può creare thumbnail. Controlla che l\'estensione GD sia installata.', + 'server_upload_limit' => 'Il server non permette un upload di questa grandezza. Prova con un file più piccolo.', + 'image_upload_error' => 'C\'è stato un errore caricando l\'immagine', + + // Attachments + 'attachment_page_mismatch' => 'Page mismatch during attachment update', + + // Pages + 'page_draft_autosave_fail' => 'Impossibile salvare la bozza. Controlla di essere connesso ad internet prima di salvare questa pagina', + + // Entities + 'entity_not_found' => 'Entità non trovata', + 'book_not_found' => 'Libro non trovato', + 'page_not_found' => 'Pagina non trovata', + 'chapter_not_found' => 'Capitolo non trovato', + 'selected_book_not_found' => 'Il libro selezionato non è stato trovato', + 'selected_book_chapter_not_found' => 'Il libro selezionato o il capitolo non sono stati trovati', + 'guests_cannot_save_drafts' => 'Gli ospiti non possono salvare bozze', + + // Users + 'users_cannot_delete_only_admin' => 'Non puoi eliminare l\'unico adin', + 'users_cannot_delete_guest' => 'Non puoi eliminare l\'utente ospite', + + // Roles + 'role_cannot_be_edited' => 'Questo ruolo non può essere modificato', + 'role_system_cannot_be_deleted' => 'Questo ruolo è di sistema e non può essere eliminato', + 'role_registration_default_cannot_delete' => 'Questo ruolo non può essere eliminato finchè è impostato come default alla registrazione', + + // Comments + 'comment_list' => 'C\'è stato un errore scaricando i commenti.', + 'cannot_add_comment_to_draft' => 'Non puoi aggiungere commenti a una bozza.', + 'comment_add' => 'C\'è stato un errore aggiungendo / aggiornando il commento.', + 'comment_delete' => 'C\'è stato un errore eliminando il commento.', + 'empty_comment' => 'Impossibile aggiungere un commento vuoto.', + + // Error pages + '404_page_not_found' => 'Pagina Non Trovata', + 'sorry_page_not_found' => 'La pagina che stavi cercando non è stata trovata.', + 'return_home' => 'Ritorna alla home', + 'error_occurred' => 'C\'è Stato un errore', + 'app_down' => ':appName è offline', + 'back_soon' => 'Ritornerà presto.', +]; \ No newline at end of file diff --git a/resources/lang/it/pagination.php b/resources/lang/it/pagination.php new file mode 100755 index 000000000..e3c561423 --- /dev/null +++ b/resources/lang/it/pagination.php @@ -0,0 +1,19 @@ + '« Precedente', + 'next' => 'Successivo »', + +]; diff --git a/resources/lang/it/passwords.php b/resources/lang/it/passwords.php new file mode 100755 index 000000000..a584aa333 --- /dev/null +++ b/resources/lang/it/passwords.php @@ -0,0 +1,22 @@ + 'La password deve avere almeno sei caratteri e corrispondere alla conferma.', + 'user' => "Non possiamo trovare un utente per quella mail.", + 'token' => 'Questo token per reimpostare la password non è valido.', + 'sent' => 'Ti abbiamo inviato via mail il link per reimpostare la password!', + 'reset' => 'La tua password è stata resettata!', + +]; diff --git a/resources/lang/it/settings.php b/resources/lang/it/settings.php new file mode 100755 index 000000000..d112cbc62 --- /dev/null +++ b/resources/lang/it/settings.php @@ -0,0 +1,114 @@ + 'Impostazioni', + 'settings_save' => 'Salva Impostazioni', + 'settings_save_success' => 'Impostazioni salvate', + + /** + * App settings + */ + + 'app_settings' => 'Impostazioni App', + 'app_name' => 'Nome applicazione', + 'app_name_desc' => 'Questo nome è mostrato nell\'header e in tutte le mail.', + 'app_name_header' => 'Mostrare il nome nell\'header', + 'app_public_viewing' => 'Consentire la visione pubblica?', + 'app_secure_images' => 'Abilitare una sicurezza maggiore per le immagini caricate?', + 'app_secure_images_desc' => 'Per una ragione di prestazioni, tutte le immagini sono pubbliche. Questa opzione aaggiunge una stringa, difficile da indovinare, random negli url delle immagini. Assicurati che il listing delle cartelle non sia abilitato per prevenire un accesso semplice.', + 'app_editor' => 'Editor pagine', + 'app_editor_desc' => 'Seleziona quale editor verrà usato da tutti gli utenti per modificare le pagine.', + 'app_custom_html' => 'Contenuto Head HTML Custom', + 'app_custom_html_desc' => 'Qualsiasi contenuto aggiunto qui verrà inserito alla fine della sezione di tutte le pagine. Questo è utile per sovrascrivere lo stile o aggiungere il codice per gli analytics.', + 'app_logo' => 'Logo applicazione', + 'app_logo_desc' => 'Questa immagine dovrebbe essere 43px in altezza.
Immagini più grandi verranno scalate.', + 'app_primary_color' => 'Colore primario applicazione', + 'app_primary_color_desc' => 'Deve essere un valore hex.
Lascia vuoto per reimpostare il colore di default.', + 'app_homepage' => 'Homepage Applicazione', + 'app_homepage_desc' => 'Seleziona una pagina da mostrare nella home anzichè quella di default. I permessi della pagina sono ignorati per quella selezionata.', + 'app_homepage_default' => 'Homepage di default scelta', + + /** + * Registration settings + */ + + 'reg_settings' => 'Impostazioni Registrazione', + 'reg_allow' => 'Consentire Registrazione?', + 'reg_default_role' => 'Ruolo predefinito dopo la registrazione', + 'reg_confirm_email' => 'Richiedere la conferma della mail?', + 'reg_confirm_email_desc' => 'Se la restrizione per dominio è usata la conferma della mail sarà richiesta e la scelta ignorata.', + 'reg_confirm_restrict_domain' => 'Restringi la registrazione al dominio', + 'reg_confirm_restrict_domain_desc' => "Inserisci una lista separata da virgola di domini di email a cui vorresti restringere la registrazione. Agli utenti verrà inviata una mail per confermare il loro indirizzo prima che possano interagire con l'applicazione.
Nota che gli utenti saranno in grado di cambiare il loro indirizzo dopo aver completato la registrazione.", + 'reg_confirm_restrict_domain_placeholder' => 'Nessuna restrizione impostata', + + /** + * Role settings + */ + + 'roles' => 'Ruoli', + 'role_user_roles' => 'Ruoli Utente', + 'role_create' => 'Crea Nuovo Ruolo', + 'role_create_success' => 'Ruolo creato correttamente', + 'role_delete' => 'Elimina Ruolo', + 'role_delete_confirm' => 'Questo eliminerà il ruolo con il nome \':roleName\'.', + 'role_delete_users_assigned' => 'Questo ruolo ha :userCount utenti assegnati. Se vuoi migrare gli utenti da questo ruolo selezionane uno nuovo sotto.', + 'role_delete_no_migration' => "Non migrare gli utenti", + 'role_delete_sure' => 'Sei sicuro di voler eliminare questo ruolo?', + 'role_delete_success' => 'Ruolo eliminato correttamente', + 'role_edit' => 'Modifica Ruolo', + 'role_details' => 'Dettagli Ruolo', + 'role_name' => 'Nome Ruolo', + 'role_desc' => 'Breve Descrizione del Ruolo', + 'role_system' => 'Permessi di Sistema', + 'role_manage_users' => 'Gestire gli utenti', + 'role_manage_roles' => 'Gestire ruoli e permessi di essi', + 'role_manage_entity_permissions' => 'Gestire tutti i permessi di libri, capitoli e pagine', + 'role_manage_own_entity_permissions' => 'Gestire i permessi sui propri libri, capitoli e pagine', + 'role_manage_settings' => 'Gestire impostazioni app', + 'role_asset' => 'Permessi Entità', + 'role_asset_desc' => "Questi permessi controllano l'accesso di default alle entità. I permessi nei Libri, Capitoli e Pagine sovrascriveranno questi.", + 'role_all' => 'Tutti', + 'role_own' => 'Propri', + 'role_controlled_by_asset' => "Controllato dall'entità in cui sono caricati", + 'role_save' => 'Salva Ruolo', + 'role_update_success' => 'Ruolo aggiornato correttamente', + 'role_users' => 'Utenti in questo ruolo', + 'role_users_none' => 'Nessun utente assegnato a questo ruolo', + + /** + * Users + */ + + 'users' => 'Utenti', + 'user_profile' => 'Profilo Utente', + 'users_add_new' => 'Aggiungi Nuovo Utente', + 'users_search' => 'Cerca Utenti', + 'users_role' => 'Ruoli Utente', + 'users_external_auth_id' => 'ID Autenticazioni Esterna', + 'users_password_warning' => 'Riempi solo se desideri cambiare la tua password:', + 'users_system_public' => 'Questo utente rappresente qualsiasi ospite che visita il sito. Non può essere usato per effettuare il login ma è assegnato automaticamente.', + 'users_delete' => 'Elimina Utente', + 'users_delete_named' => "Elimina l'utente :userName", + 'users_delete_warning' => 'Questo eliminerà completamente l\'utente \':userName\' dal sistema.', + 'users_delete_confirm' => 'Sei sicuro di voler eliminare questo utente?', + 'users_delete_success' => 'Utenti rimossi correttamente', + 'users_edit' => 'Modifica Utente', + 'users_edit_profile' => 'Modifica Profilo', + 'users_edit_success' => 'Utente aggiornato correttamente', + 'users_avatar' => 'Avatar Utente', + 'users_avatar_desc' => "Quest'immagine dovrebbe essere approssimativamente 256px quadrata.", + 'users_preferred_language' => 'Lingua Preferita', + 'users_social_accounts' => 'Account Social', + 'users_social_accounts_info' => 'Qui puoi connettere gli altri account per un accesso più veloce e semplice. Disconnettere un account qui non rimuoverà le altre sessioni. Revoca l\'accesso dal tuo profilo negli account social connessi.', + 'users_social_connect' => 'Connetti Account', + 'users_social_disconnect' => 'Disconnetti Account', + 'users_social_connected' => 'L\'account :socialAccount è stato connesso correttamente al tuo profilo.', + 'users_social_disconnected' => 'L\'account :socialAccount è stato disconnesso correttamente dal tuo profilo.', +]; diff --git a/resources/lang/it/validation.php b/resources/lang/it/validation.php new file mode 100755 index 000000000..832480d4c --- /dev/null +++ b/resources/lang/it/validation.php @@ -0,0 +1,108 @@ + ':attribute deve essere accettato.', + 'active_url' => ':attribute non è uno URL valido.', + 'after' => ':attribute deve essere una data dopo il :date.', + 'alpha' => ':attribute deve contenere solo lettere.', + 'alpha_dash' => ':attribute deve contenere solo lettere, numeri e meno.', + 'alpha_num' => ':attribute deve contenere solo lettere e numeri.', + 'array' => ':attribute deve essere un array.', + 'before' => ':attribute deve essere una data prima del :date.', + 'between' => [ + 'numeric' => 'Il campo :attribute deve essere tra :min e :max.', + 'file' => 'Il campo :attribute deve essere tra :min e :max kilobytes.', + 'string' => 'Il campo :attribute deve essere tra :min e :max caratteri.', + 'array' => 'Il campo :attribute deve essere tra :min e :max oggetti.', + ], + 'boolean' => ':attribute deve contenere vero o falso.', + 'confirmed' => 'La conferma di :attribute non corrisponde.', + 'date' => ':attribute non è una data valida.', + 'date_format' => 'Il campo :attribute non corrisponde al formato :format.', + 'different' => 'Il campo :attribute e :other devono essere differenti.', + 'digits' => 'Il campo :attribute deve essere di :digits numeri.', + 'digits_between' => 'Il campo :attribute deve essere tra i numeri :min e :max.', + 'email' => 'Il campo :attribute deve essere un indirizzo email valido.', + 'filled' => 'Il campo :attribute field is required.', + 'exists' => 'Il campo :attribute non è valido.', + 'image' => 'Il campo :attribute deve essere un\'immagine.', + 'in' => 'Il campo :attribute selezionato non è valido.', + 'integer' => 'Il campo :attribute deve essere un intero.', + 'ip' => 'Il campo :attribute deve essere un indirizzo IP valido.', + 'max' => [ + 'numeric' => 'Il campo :attribute non deve essere maggiore di :max.', + 'file' => 'Il campo :attribute non deve essere maggiore di :max kilobytes.', + 'string' => 'Il campo :attribute non deve essere maggiore di :max caratteri.', + 'array' => 'Il campo :attribute non deve avere più di :max oggetti.', + ], + 'mimes' => 'Il campo :attribute deve essere: :values.', + 'min' => [ + 'numeric' => 'Il campo :attribute deve essere almeno :min.', + 'file' => 'Il campo :attribute deve essere almeno :min kilobytes.', + 'string' => 'Il campo :attribute deve essere almeno :min caratteri.', + 'array' => 'Il campo :attribute deve contenere almeno :min elementi.', + ], + 'not_in' => 'Il :attribute selezionato non è valido.', + 'numeric' => ':attribute deve essere un numero.', + 'regex' => 'Il formato di :attribute non è valido.', + 'required' => 'Il campo :attribute è richiesto.', + 'required_if' => 'Il campo :attribute è richiesto quando :other è :value.', + 'required_with' => 'Il campo :attribute è richiesto quando :values è presente.', + 'required_with_all' => 'Il campo :attribute è richiesto quando :values sono presenti.', + 'required_without' => 'Il campo :attribute è richiesto quando :values non è presente.', + 'required_without_all' => 'Il campo :attribute è richiesto quando nessuno dei :values sono presenti.', + 'same' => ':attribute e :other devono corrispondere.', + 'size' => [ + 'numeric' => 'Il campo :attribute deve essere :size.', + 'file' => 'Il campo :attribute deve essere :size kilobytes.', + 'string' => 'Il campo :attribute deve essere di :size caratteri.', + 'array' => 'Il campo :attribute deve contenere :size elementi.', + ], + 'string' => ':attribute deve essere una stringa.', + 'timezone' => ':attribute deve essere una zona valida.', + 'unique' => ':attribute è già preso.', + 'url' => 'Il formato :attribute non è valido.', + + /* + |-------------------------------------------------------------------------- + | 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' => 'Conferma della password richiesta', + ], + ], + + /* + |-------------------------------------------------------------------------- + | 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/ja/components.php b/resources/lang/ja/components.php index 89ed33ef3..8e9eb2e47 100644 --- a/resources/lang/ja/components.php +++ b/resources/lang/ja/components.php @@ -20,5 +20,13 @@ return [ 'image_preview' => '画像プレビュー', 'image_upload_success' => '画像がアップロードされました', 'image_update_success' => '画像が更新されました', - 'image_delete_success' => '画像が削除されました' + 'image_delete_success' => '画像が削除されました', + + /** + * Code editor + */ + 'code_editor' => 'プログラムブロック編集', + 'code_language' => 'プログラミング言語の選択', + 'code_content' => 'プログラム内容', + 'code_save' => 'プログラムを保存' ]; diff --git a/resources/lang/nl/entities.php b/resources/lang/nl/entities.php index d6975e130..a882294f1 100644 --- a/resources/lang/nl/entities.php +++ b/resources/lang/nl/entities.php @@ -214,4 +214,12 @@ 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', + 'comment_save' => 'Opslaan opslaan', ]; \ No newline at end of file diff --git a/resources/lang/nl/errors.php b/resources/lang/nl/errors.php index f8b635bce..b8fab59fd 100644 --- a/resources/lang/nl/errors.php +++ b/resources/lang/nl/errors.php @@ -67,4 +67,11 @@ return [ 'error_occurred' => 'Er Ging Iets Fout', 'app_down' => ':appName is nu niet beschikbaar', 'back_soon' => 'Komt snel weer online.', + + // Comments + 'comment_list' => 'Er is een fout opgetreden tijdens het ophalen van de reacties.', + 'cannot_add_comment_to_draft' => 'U kunt geen reacties toevoegen aan een ontwerp.', + 'comment_add' => 'Er is een fout opgetreden tijdens het toevoegen van de reactie.', + 'comment_delete' => 'Er is een fout opgetreden tijdens het verwijderen van de reactie.', + 'empty_comment' => 'Kan geen lege reactie toevoegen.', ]; \ No newline at end of file diff --git a/resources/lang/pt_BR/entities.php b/resources/lang/pt_BR/entities.php index 5a965fe62..bf0a8ac72 100644 --- a/resources/lang/pt_BR/entities.php +++ b/resources/lang/pt_BR/entities.php @@ -214,4 +214,12 @@ 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', + 'comment_save' => 'Salvar comentário', ]; \ 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..25a1af140 100644 --- a/resources/lang/sk/entities.php +++ b/resources/lang/sk/entities.php @@ -223,4 +223,12 @@ 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', + 'comment_save' => 'Uložiť komentár', ]; 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/auth/login.blade.php b/resources/views/auth/login.blade.php index 706747b8b..dda733645 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -9,36 +9,38 @@ @section('content')
-
-

{{ title_case(trans('auth.log_in')) }}

+
+

{{ title_case(trans('auth.log_in')) }}

-
- {!! csrf_field() !!} +
+ + {!! csrf_field() !!} - @include('auth/forms/login/' . $authMethod) + @include('auth/forms/login/' . $authMethod) -
- - - -
+
+ + + +
-
- -
- +
+ +
+ - @if(count($socialDrivers) > 0) -
- @foreach($socialDrivers as $driver => $name) - - @icon($driver) - {{ trans('auth.log_in_with', ['socialDriver' => $name]) }} - - @endforeach - @endif + @if(count($socialDrivers) > 0) +
+ @foreach($socialDrivers as $driver => $name) + + @icon($driver) + {{ trans('auth.log_in_with', ['socialDriver' => $name]) }} + + @endforeach + @endif +
diff --git a/resources/views/auth/register-confirm.blade.php b/resources/views/auth/register-confirm.blade.php index 364df9266..5d945ef81 100644 --- a/resources/views/auth/register-confirm.blade.php +++ b/resources/views/auth/register-confirm.blade.php @@ -9,9 +9,11 @@ @section('content')
-
-

{{ trans('auth.register_thanks') }}

-

{{ trans('auth.register_confirm', ['appName' => setting('app-name')]) }}

+
+

{{ trans('auth.register_thanks') }}

+
+

{{ trans('auth.register_confirm', ['appName' => setting('app-name')]) }}

+
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index d5db4afa8..7b319d30c 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -7,41 +7,42 @@ @section('content')
-
-

{{ title_case(trans('auth.sign_up')) }}

+
+

{{ title_case(trans('auth.sign_up')) }}

+
+
+ {!! csrf_field() !!} - - {!! csrf_field() !!} +
+ + @include('form/text', ['name' => 'name']) +
-
- - @include('form/text', ['name' => 'name']) -
+
+ + @include('form/text', ['name' => 'email']) +
-
- - @include('form/text', ['name' => 'email']) -
+
+ + @include('form/password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')]) +
-
- - @include('form/password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')]) -
+
+ +
+
-
- -
- - - @if(count($socialDrivers) > 0) -
- @foreach($socialDrivers as $driver => $name) - - @icon($driver) - {{ trans('auth.sign_up_with', ['socialDriver' => $name]) }} - - @endforeach - @endif + @if(count($socialDrivers) > 0) +
+ @foreach($socialDrivers as $driver => $name) + + @icon($driver) + {{ trans('auth.sign_up_with', ['socialDriver' => $name]) }} + + @endforeach + @endif +
diff --git a/resources/views/auth/user-unconfirmed.blade.php b/resources/views/auth/user-unconfirmed.blade.php index 13567b412..22c26d92b 100644 --- a/resources/views/auth/user-unconfirmed.blade.php +++ b/resources/views/auth/user-unconfirmed.blade.php @@ -2,29 +2,33 @@ @section('content') -
-
-

{{ trans('auth.email_not_confirmed') }}

-

{{ trans('auth.email_not_confirmed_text') }}
- {{ trans('auth.email_not_confirmed_click_link') }}
- {{ trans('auth.email_not_confirmed_resend') }} -

-
-
- {!! csrf_field() !!} -
- - @if(auth()->check()) - @include('form/text', ['name' => 'email', 'model' => auth()->user()]) - @else - @include('form/text', ['name' => 'email']) - @endif -
-
- -
-
+
+

 

+
+

{{ trans('auth.email_not_confirmed') }}

+
+

{{ trans('auth.email_not_confirmed_text') }}
+ {{ trans('auth.email_not_confirmed_click_link') }}
+ {{ trans('auth.email_not_confirmed_resend') }} +

+
+
+ {!! csrf_field() !!} +
+ + @if(auth()->check()) + @include('form/text', ['name' => 'email', 'model' => auth()->user()]) + @else + @include('form/text', ['name' => 'email']) + @endif +
+
+ +
+
+
+
@stop diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 95a9d72b0..edbd6a096 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -34,9 +34,9 @@ @include('partials/notifications')