1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-04-25 14:42:44 +03:00

Merge pull request #1153 from BookStackApp/2019-design

WIP: 2019 design
This commit is contained in:
Dan Brown 2019-04-14 13:54:36 +01:00 committed by GitHub
commit 03073dd9e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
229 changed files with 6723 additions and 7584 deletions

View File

@ -103,7 +103,7 @@ class ActivityService
* @param int $page
* @return array
*/
public function entityActivity($entity, $count = 20, $page = 0)
public function entityActivity($entity, $count = 20, $page = 1)
{
if ($entity->isA('book')) {
$query = $this->activity->where('book_id', '=', $entity->id);
@ -114,7 +114,7 @@ class ActivityService
$activity = $this->permissionService
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->with(['entity', 'user.avatar'])->skip($count * $page)->take($count)->get();
->orderBy('created_at', 'desc')->with(['entity', 'user.avatar'])->skip($count * ($page - 1))->take($count)->get();
return $this->filterSimilar($activity);
}

View File

@ -2,21 +2,26 @@
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Entity;
use BookStack\Entities\EntityProvider;
use Illuminate\Support\Collection;
class ViewService
{
protected $view;
protected $permissionService;
protected $entityProvider;
/**
* ViewService constructor.
* @param \BookStack\Actions\View $view
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
* @param EntityProvider $entityProvider
*/
public function __construct(View $view, PermissionService $permissionService)
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
{
$this->view = $view;
$this->permissionService = $permissionService;
$this->entityProvider = $entityProvider;
}
/**
@ -50,23 +55,21 @@ class ViewService
* Get the entities with the most views.
* @param int $count
* @param int $page
* @param Entity|false|array $filterModel
* @param string|array $filterModels
* @param string $action - used for permission checking
* @return
* @return Collection
*/
public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view')
public function getPopular(int $count = 10, int $page = 0, $filterModels = null, string $action = 'view')
{
// TODO - Standardise input filter
$skipCount = $count * $page;
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if ($filterModel && is_array($filterModel)) {
$query->whereIn('viewable_type', $filterModel);
} else if ($filterModel) {
$query->where('viewable_type', '=', $filterModel->getMorphClass());
if ($filterModels) {
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
}
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');

View File

@ -704,7 +704,7 @@ class PermissionService
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @param string $action
* @return mixed
* @return QueryBuilder
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Auth;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Model;
class Role extends Model
@ -13,7 +14,7 @@ class Role extends Model
*/
public function users()
{
return $this->belongsToMany(User::class);
return $this->belongsToMany(User::class)->orderBy('name', 'asc');
}
/**
@ -30,7 +31,7 @@ class Role extends Model
*/
public function permissions()
{
return $this->belongsToMany(Permissions\RolePermission::class, 'permission_role', 'role_id', 'permission_id');
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
}
/**
@ -51,18 +52,18 @@ class Role extends Model
/**
* Add a permission to this role.
* @param \BookStack\Auth\Permissions\RolePermission $permission
* @param RolePermission $permission
*/
public function attachPermission(Permissions\RolePermission $permission)
public function attachPermission(RolePermission $permission)
{
$this->permissions()->attach($permission->id);
}
/**
* Detach a single permission from this role.
* @param \BookStack\Auth\Permissions\RolePermission $permission
* @param RolePermission $permission
*/
public function detachPermission(Permissions\RolePermission $permission)
public function detachPermission(RolePermission $permission)
{
$this->permissions()->detach($permission->id);
}

View File

@ -6,6 +6,7 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\Image;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Images;
class UserRepo
@ -48,7 +49,7 @@ class UserRepo
/**
* Get all the users with their permissions.
* @return \Illuminate\Database\Eloquent\Builder|static
* @return Builder|static
*/
public function getAllUsers()
{
@ -59,7 +60,7 @@ class UserRepo
* Get all the users with their permissions in a paginated format.
* @param int $count
* @param $sortData
* @return \Illuminate\Database\Eloquent\Builder|static
* @return Builder|static
*/
public function getAllUsersPaginatedAndSorted($count, $sortData)
{
@ -223,16 +224,15 @@ class UserRepo
*/
public function getRecentlyCreated(User $user, $count = 20)
{
$createdByUserQuery = function(Builder $query) use ($user) {
$query->where('created_by', '=', $user->id);
};
return [
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
})
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, $createdByUserQuery),
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, $createdByUserQuery),
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, $createdByUserQuery),
'shelves' => $this->entityRepo->getRecentlyCreated('bookshelf', $count, 0, $createdByUserQuery)
];
}
@ -247,6 +247,7 @@ class UserRepo
'pages' => $this->entityRepo->getUserTotalCreated('page', $user),
'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
'books' => $this->entityRepo->getUserTotalCreated('book', $user),
'shelves' => $this->entityRepo->getUserTotalCreated('bookshelf', $user),
];
}
@ -256,7 +257,7 @@ class UserRepo
*/
public function getAllRoles()
{
return $this->role->all();
return $this->role->newQuery()->orderBy('name', 'asc')->get();
}
/**

View File

@ -38,7 +38,7 @@ class Book extends Entity
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
if (!$this->image_id) {
return $default;
}
@ -69,6 +69,15 @@ class Book extends Entity
return $this->hasMany(Page::class);
}
/**
* Get the direct child pages of this book.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function directPages()
{
return $this->pages()->where('chapter_id', '=', '0');
}
/**
* Get all chapters within this book.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
@ -92,7 +101,7 @@ class Book extends Entity
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
public function getExcerpt(int $length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;

View File

@ -50,7 +50,8 @@ class Bookshelf extends Entity
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
// TODO - Make generic, focused on books right now, Perhaps set-up a better image
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
if (!$this->image_id) {
return $default;
}
@ -64,7 +65,7 @@ class Bookshelf extends Entity
}
/**
* Get the cover image of the book
* Get the cover image of the shelf
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function cover()
@ -77,7 +78,7 @@ class Bookshelf extends Entity
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
public function getExcerpt(int $length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
@ -91,4 +92,14 @@ class Bookshelf extends Entity
{
return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
/**
* Check if this shelf contains the given book.
* @param Book $book
* @return bool
*/
public function contains(Book $book)
{
return $this->books()->where('id', '=', $book->id)->count() > 0;
}
}

View File

@ -0,0 +1,34 @@
<?php namespace BookStack\Entities;
use Illuminate\View\View;
class BreadcrumbsViewComposer
{
protected $entityContextManager;
/**
* BreadcrumbsViewComposer constructor.
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityContextManager $entityContextManager)
{
$this->entityContextManager = $entityContextManager;
}
/**
* Modify data when the view is composed.
* @param View $view
*/
public function compose(View $view)
{
$crumbs = $view->getData()['crumbs'];
if (array_first($crumbs) instanceof Book) {
$shelf = $this->entityContextManager->getContextualShelfForBook(array_first($crumbs));
if ($shelf) {
array_unshift($crumbs, $shelf);
$view->with('crumbs', $crumbs);
}
}
}
}

View File

@ -53,9 +53,9 @@ class Chapter extends Entity
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
public function getExcerpt(int $length = 100)
{
$description = $this->description;
$description = $this->text ?? $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
@ -67,4 +67,13 @@ class Chapter extends Entity
{
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
/**
* Check if this chapter has any child pages.
* @return bool
*/
public function hasChildren()
{
return count($this->pages) > 0;
}
}

View File

@ -102,6 +102,11 @@ class Entity extends Ownable
return $this->morphMany(View::class, 'viewable');
}
public function viewCountQuery()
{
return $this->views()->selectRaw('viewable_id, sum(views) as view_count')->groupBy('viewable_id');
}
/**
* Get the Tag models that have been user assigned to this entity.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
@ -218,6 +223,20 @@ class Entity extends Ownable
return $this->{$this->textField};
}
/**
* Get an excerpt of this entity's descriptive content to the specified length.
* @param int $length
* @return mixed
*/
public function getExcerpt(int $length = 100)
{
$text = $this->getText();
if (mb_strlen($text) > $length) {
$text = mb_substr($text, 0, $length-3) . '...';
}
return trim($text);
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string

View File

@ -0,0 +1,62 @@
<?php namespace BookStack\Entities;
use BookStack\Entities\Repos\EntityRepo;
use Illuminate\Session\Store;
class EntityContextManager
{
protected $session;
protected $entityRepo;
protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
/**
* EntityContextManager constructor.
* @param Store $session
* @param EntityRepo $entityRepo
*/
public function __construct(Store $session, EntityRepo $entityRepo)
{
$this->session = $session;
$this->entityRepo = $entityRepo;
}
/**
* Get the current bookshelf context for the given book.
* @param Book $book
* @return Bookshelf|null
*/
public function getContextualShelfForBook(Book $book)
{
$contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
if (is_int($contextBookshelfId)) {
/** @var Bookshelf $shelf */
$shelf = $this->entityRepo->getById('bookshelf', $contextBookshelfId);
if ($shelf && $shelf->contains($book)) {
return $shelf;
}
}
return null;
}
/**
* Store the current contextual shelf ID.
* @param int $shelfId
*/
public function setShelfContext(int $shelfId)
{
$this->session->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
}
/**
* Clear the session stored shelf context id.
*/
public function clearShelfContext()
{
$this->session->forget($this->KEY_SHELF_CONTEXT_ID);
}
}

View File

@ -84,4 +84,23 @@ class EntityProvider
$type = strtolower($type);
return $this->all()[$type];
}
/**
* Get the morph classes, as an array, for a single or multiple types.
* @param string|array $types
* @return array<string>
*/
public function getMorphClasses($types)
{
if (is_string($types)) {
$types = [$types];
}
$morphClasses = [];
foreach ($types as $type) {
$model = $this->get($type);
$morphClasses[] = $model->getMorphClass();
}
return $morphClasses;
}
}

View File

@ -102,17 +102,6 @@ class Page extends Entity
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent);
}
/**
* Get an excerpt of this page's content to the specified length.
* @param int $length
* @return mixed
*/
public function getExcerpt($length = 100)
{
$text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
return mb_convert_encoding($text, 'UTF-8');
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @param bool $withContent

View File

@ -62,4 +62,5 @@ class PageRevision extends Model
{
return $type === 'revision';
}
}

View File

@ -15,6 +15,7 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Uploads\AttachmentService;
use DOMDocument;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
@ -179,11 +180,38 @@ class EntityRepo
* Get all entities in a paginated format
* @param $type
* @param int $count
* @param string $sort
* @param string $order
* @param null|callable $queryAddition
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function getAllPaginated($type, $count = 10)
public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
{
return $this->entityQuery($type)->orderBy('name', 'asc')->paginate($count);
$query = $this->entityQuery($type);
$query = $this->addSortToQuery($query, $sort, $order);
if ($queryAddition) {
$queryAddition($query);
}
return $query->paginate($count);
}
/**
* Add sorting operations to an entity query.
* @param Builder $query
* @param string $sort
* @param string $order
* @return Builder
*/
protected function addSortToQuery(Builder $query, string $sort = 'name', string $order = 'asc')
{
$order = ($order === 'asc') ? 'asc' : 'desc';
$propertySorts = ['name', 'created_at', 'updated_at'];
if (in_array($sort, $propertySorts)) {
return $query->orderBy($sort, $order);
}
return $query;
}
/**
@ -265,15 +293,14 @@ class EntityRepo
/**
* Get the most popular entities base on all views.
* @param string|bool $type
* @param string $type
* @param int $count
* @param int $page
* @return mixed
*/
public function getPopular($type, $count = 10, $page = 0)
public function getPopular(string $type, int $count = 10, int $page = 0)
{
$filter = is_bool($type) ? false : $this->entityProvider->get($type);
return $this->viewService->getPopular($count, $page, $filter);
return $this->viewService->getPopular($count, $page, $type);
}
/**
@ -313,6 +340,18 @@ class EntityRepo
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
}
/**
* Get the direct children of a book.
* @param Book $book
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getBookDirectChildren(Book $book)
{
$pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
$chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
}
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.

View File

@ -128,7 +128,7 @@ class LoginController extends Controller
]);
}
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
return view('auth.login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}
/**

View File

@ -176,7 +176,7 @@ class RegisterController extends Controller
*/
public function getRegisterConfirmation()
{
return view('auth/register-confirm');
return view('auth.register-confirm');
}
/**
@ -204,7 +204,7 @@ class RegisterController extends Controller
*/
public function showAwaitingConfirmation()
{
return view('auth/user-unconfirmed');
return view('auth.user-unconfirmed');
}
/**

View File

@ -3,6 +3,7 @@
use Activity;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Book;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\ExportService;
use Illuminate\Http\Request;
@ -15,18 +16,25 @@ class BookController extends Controller
protected $entityRepo;
protected $userRepo;
protected $exportService;
protected $entityContextManager;
/**
* BookController constructor.
* @param EntityRepo $entityRepo
* @param \BookStack\Auth\UserRepo $userRepo
* @param \BookStack\Entities\ExportService $exportService
* @param UserRepo $userRepo
* @param ExportService $exportService
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
public function __construct(
EntityRepo $entityRepo,
UserRepo $userRepo,
ExportService $exportService,
EntityContextManager $entityContextManager
) {
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
$this->entityContextManager = $entityContextManager;
parent::__construct();
}
@ -36,18 +44,32 @@ class BookController extends Controller
*/
public function index()
{
$books = $this->entityRepo->getAllPaginated('book', 18);
$view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
$sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
$order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
$books = $this->entityRepo->getAllPaginated('book', 18, $sort, $order);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.books'));
return view('books/index', [
return view('books.index', [
'books' => $books,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'booksViewType' => $booksViewType
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
]);
}
@ -59,7 +81,7 @@ class BookController extends Controller
{
$this->checkPermission('book-create-all');
$this->setPageTitle(trans('entities.books_create'));
return view('books/create');
return view('books.create');
}
/**
@ -83,20 +105,28 @@ class BookController extends Controller
/**
* Display the specified book.
* @param $slug
* @param Request $request
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function show($slug)
public function show($slug, Request $request)
{
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-view', $book);
$bookChildren = $this->entityRepo->getBookChildren($book);
Views::add($book);
if ($request->has('shelf')) {
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
}
$this->setPageTitle($book->getShortName());
return view('books/show', [
return view('books.show', [
'book' => $book,
'current' => $book,
'bookChildren' => $bookChildren,
'activity' => Activity::entityActivity($book, 20, 0)
'activity' => Activity::entityActivity($book, 20, 1)
]);
}
@ -110,7 +140,7 @@ class BookController extends Controller
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
return view('books/edit', ['book' => $book, 'current' => $book]);
return view('books.edit', ['book' => $book, 'current' => $book]);
}
/**
@ -142,22 +172,24 @@ class BookController extends Controller
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
return view('books/delete', ['book' => $book, 'current' => $book]);
return view('books.delete', ['book' => $book, 'current' => $book]);
}
/**
* Shows the view which allows pages to be re-ordered and sorted.
* @param string $bookSlug
* @return \Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function sort($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->entityRepo->getBookChildren($book, true);
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
}
/**
@ -170,7 +202,7 @@ class BookController extends Controller
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$bookChildren = $this->entityRepo->getBookChildren($book);
return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
}
/**
@ -263,12 +295,12 @@ class BookController extends Controller
* @param $bookSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug)
public function showPermissions($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$roles = $this->userRepo->getRestrictableRoles();
return view('books/restrictions', [
return view('books.permissions', [
'book' => $book,
'roles' => $roles
]);
@ -277,11 +309,12 @@ class BookController extends Controller
/**
* Set the restrictions for this book.
* @param $bookSlug
* @param $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function restrict($bookSlug, Request $request)
public function permissions($bookSlug, Request $request)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);

View File

@ -3,8 +3,8 @@
use Activity;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\ExportService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@ -14,19 +14,19 @@ class BookshelfController extends Controller
protected $entityRepo;
protected $userRepo;
protected $exportService;
protected $entityContextManager;
/**
* BookController constructor.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param \BookStack\Entities\ExportService $exportService
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
$this->entityContextManager = $entityContextManager;
parent::__construct();
}
@ -36,19 +36,35 @@ class BookshelfController extends Controller
*/
public function index()
{
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$view = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$sort = setting()->getUser($this->currentUser, 'bookshelves_sort', 'name');
$order = setting()->getUser($this->currentUser, 'bookshelves_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $sort, $order);
foreach ($shelves as $shelf) {
$shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
}
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
$popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.shelves'));
return view('shelves/index', [
return view('shelves.index', [
'shelves' => $shelves,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'shelvesViewType' => $shelvesViewType
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
]);
}
@ -61,7 +77,7 @@ class BookshelfController extends Controller
$this->checkPermission('bookshelf-create-all');
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves/create', ['books' => $books]);
return view('shelves.create', ['books' => $books]);
}
/**
@ -93,17 +109,19 @@ class BookshelfController extends Controller
*/
public function show(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
/** @var Bookshelf $bookshelf */
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('book-view', $bookshelf);
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
Views::add($bookshelf);
$this->entityContextManager->setShelfContext($bookshelf->id);
$this->setPageTitle($bookshelf->getShortName());
return view('shelves/show', [
return view('shelves.show', [
'shelf' => $bookshelf,
'books' => $books,
'activity' => Activity::entityActivity($bookshelf, 20, 0)
'activity' => Activity::entityActivity($bookshelf, 20, 1)
]);
}
@ -126,7 +144,7 @@ class BookshelfController extends Controller
});
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/edit', [
return view('shelves.edit', [
'shelf' => $bookshelf,
'books' => $books,
'shelfBooks' => $shelfBooks,
@ -170,7 +188,7 @@ class BookshelfController extends Controller
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/delete', ['shelf' => $bookshelf]);
return view('shelves.delete', ['shelf' => $bookshelf]);
}
/**
@ -190,31 +208,32 @@ class BookshelfController extends Controller
}
/**
* Show the Restrictions view.
* @param $slug
* Show the permissions view.
* @param string $slug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showRestrict(string $slug)
public function showPermissions(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$roles = $this->userRepo->getRestrictableRoles();
return view('shelves.restrictions', [
return view('shelves.permissions', [
'shelf' => $bookshelf,
'roles' => $roles
]);
}
/**
* Set the restrictions for this bookshelf.
* @param $slug
* Set the permissions for this bookshelf.
* @param string $slug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function restrict(string $slug, Request $request)
public function permissions(string $slug, Request $request)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);

View File

@ -39,7 +39,7 @@ class ChapterController extends Controller
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle(trans('entities.chapters_create'));
return view('chapters/create', ['book' => $book, 'current' => $book]);
return view('chapters.create', ['book' => $book, 'current' => $book]);
}
/**
@ -78,7 +78,7 @@ class ChapterController extends Controller
Views::add($chapter);
$this->setPageTitle($chapter->getShortName());
$pages = $this->entityRepo->getChapterChildren($chapter);
return view('chapters/show', [
return view('chapters.show', [
'book' => $chapter->book,
'chapter' => $chapter,
'current' => $chapter,
@ -98,7 +98,7 @@ class ChapterController extends Controller
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
@ -130,7 +130,7 @@ class ChapterController extends Controller
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
@ -162,7 +162,7 @@ class ChapterController extends Controller
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
return view('chapters/move', [
return view('chapters.move', [
'chapter' => $chapter,
'book' => $chapter->book
]);
@ -214,13 +214,14 @@ class ChapterController extends Controller
* @param $bookSlug
* @param $chapterSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showRestrict($bookSlug, $chapterSlug)
public function showPermissions($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$roles = $this->userRepo->getRestrictableRoles();
return view('chapters/restrictions', [
return view('chapters.permissions', [
'chapter' => $chapter,
'roles' => $roles
]);
@ -232,8 +233,10 @@ class ChapterController extends Controller
* @param $chapterSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function restrict($bookSlug, $chapterSlug, Request $request)
public function permissions($bookSlug, $chapterSlug, Request $request)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);

View File

@ -54,7 +54,7 @@ class CommentController extends Controller
$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]);
return view('comments.comment', ['comment' => $comment]);
}
/**
@ -75,7 +75,7 @@ class CommentController extends Controller
$this->checkOwnablePermission('comment-update', $comment);
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
return view('comments/comment', ['comment' => $comment]);
return view('comments.comment', ['comment' => $comment]);
}
/**

View File

@ -123,6 +123,20 @@ abstract class Controller extends BaseController
return true;
}
/**
* Check if the current user has a permission or bypass if the provided user
* id matches the current user.
* @param string $permissionName
* @param int $userId
* @return bool
*/
protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
{
return $this->checkPermissionOr($permissionName, function() use ($userId) {
return $userId === $this->currentUser->id;
});
}
/**
* Send back a json error message.
* @param string $messageText

View File

@ -19,7 +19,6 @@ class HomeController extends Controller
parent::__construct();
}
/**
* Display the homepage.
* @return Response
@ -45,17 +44,36 @@ class HomeController extends Controller
'draftPages' => $draftPages,
];
// Add required list ordering & sorting for books & shelves views.
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
$key = $homepageOption;
$view = setting()->getUser($this->currentUser, $key . '_view_type', config('app.views.' . $key));
$sort = setting()->getUser($this->currentUser, $key . '_sort', 'name');
$order = setting()->getUser($this->currentUser, $key . '_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
$commonData = array_merge($commonData, [
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
]);
}
if ($homepageOption === 'bookshelves') {
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]);
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $commonData['sort'], $commonData['order']);
$data = array_merge($commonData, ['shelves' => $shelves]);
return view('common.home-shelves', $data);
}
if ($homepageOption === 'books') {
$books = $this->entityRepo->getAllPaginated('book', 18);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]);
$books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
$data = array_merge($commonData, ['books' => $books]);
return view('common.home-book', $data);
}
@ -105,7 +123,7 @@ class HomeController extends Controller
*/
public function customHeadContent()
{
return view('partials/custom-head-content');
return view('partials.custom-head-content');
}
/**
@ -120,7 +138,7 @@ class HomeController extends Controller
$allowRobots = $sitePublic;
}
return response()
->view('common/robots', ['allowRobots' => $allowRobots])
->view('common.robots', ['allowRobots' => $allowRobots])
->header('Content-Type', 'text/plain');
}
@ -129,6 +147,6 @@ class HomeController extends Controller
*/
public function getNotFound()
{
return response()->view('errors/404', [], 404);
return response()->view('errors.404', [], 404);
}
}

View File

@ -61,7 +61,7 @@ class PageController extends Controller
// Otherwise show the edit view if they're a guest
$this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]);
return view('pages.guest-create', ['parent' => $parent]);
}
/**
@ -110,7 +110,7 @@ class PageController extends Controller
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
return view('pages.edit', [
'page' => $draft,
'book' => $draft->book,
'isDraft' => true,
@ -184,7 +184,7 @@ class PageController extends Controller
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', [
return view('pages.show', [
'page' => $page,'book' => $page->book,
'current' => $page,
'sidebarTree' => $sidebarTree,
@ -239,7 +239,7 @@ class PageController extends Controller
}
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
return view('pages.edit', [
'page' => $page,
'book' => $page->book,
'current' => $page,
@ -317,7 +317,7 @@ class PageController extends Controller
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
}
@ -333,7 +333,7 @@ class PageController extends Controller
$page = $this->pageRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
}
/**
@ -377,12 +377,13 @@ class PageController extends Controller
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\View\View
* @throws NotFoundException
*/
public function showRevisions($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
return view('pages.revisions', ['page' => $page, 'current' => $page]);
}
/**
@ -403,9 +404,10 @@ class PageController extends Controller
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages/revision', [
return view('pages.revision', [
'page' => $page,
'book' => $page->book,
'diff' => null,
'revision' => $revision
]);
}
@ -432,7 +434,7 @@ class PageController extends Controller
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
return view('pages/revision', [
return view('pages.revision', [
'page' => $page,
'book' => $page->book,
'diff' => $diff,
@ -482,12 +484,12 @@ class PageController extends Controller
// Check if its the latest revision, cannot delete latest revision.
if (intval($currentRevision->id) === intval($revId)) {
session()->flash('error', trans('entities.revision_cannot_delete_latest'));
return response()->view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
return response()->view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
}
$revision->delete();
session()->flash('success', trans('entities.revision_delete_success'));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
return view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
}
/**
@ -532,49 +534,20 @@ class PageController extends Controller
return $this->downloadResponse($pageText, $pageSlug . '.txt');
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyCreated()
{
$pages = $this->pageRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
return view('pages/detailed-listing', [
'title' => trans('entities.recently_created_pages'),
'pages' => $pages
]);
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyUpdated()
{
// TODO - Still exist?
$pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
return view('pages/detailed-listing', [
return view('pages.detailed-listing', [
'title' => trans('entities.recently_updated_pages'),
'pages' => $pages
]);
}
/**
* Show the Restrictions view.
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [
'page' => $page,
'roles' => $roles
]);
}
/**
* Show the view to choose a new parent to move a page into.
* @param string $bookSlug
@ -587,7 +560,7 @@ class PageController extends Controller
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
return view('pages/move', [
return view('pages.move', [
'book' => $page->book,
'page' => $page
]);
@ -645,7 +618,7 @@ class PageController extends Controller
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]);
return view('pages/copy', [
return view('pages.copy', [
'book' => $page->book,
'page' => $page
]);
@ -690,6 +663,24 @@ class PageController extends Controller
return redirect($pageCopy->getUrl());
}
/**
* Show the Permissions view.
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws NotFoundException
*/
public function showPermissions($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages.permissions', [
'page' => $page,
'roles' => $roles
]);
}
/**
* Set the permissions for this page.
* @param string $bookSlug
@ -697,8 +688,9 @@ class PageController extends Controller
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws NotFoundException
* @throws \Throwable
*/
public function restrict($bookSlug, $pageSlug, Request $request)
public function permissions($bookSlug, $pageSlug, Request $request)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);

View File

@ -26,7 +26,7 @@ class PermissionController extends Controller
{
$this->checkPermission('user-roles-manage');
$roles = $this->permissionsRepo->getAllRoles();
return view('settings/roles/index', ['roles' => $roles]);
return view('settings.roles.index', ['roles' => $roles]);
}
/**
@ -36,7 +36,7 @@ class PermissionController extends Controller
public function createRole()
{
$this->checkPermission('user-roles-manage');
return view('settings/roles/create');
return view('settings.roles.create');
}
/**
@ -70,7 +70,7 @@ class PermissionController extends Controller
if ($role->hidden) {
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
}
return view('settings/roles/edit', ['role' => $role]);
return view('settings.roles.edit', ['role' => $role]);
}
/**
@ -106,7 +106,7 @@ class PermissionController extends Controller
$roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
$roles->prepend($blankRole);
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
return view('settings.roles.delete', ['role' => $role, 'roles' => $roles]);
}
/**

View File

@ -1,34 +1,45 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\ViewService;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\SearchService;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\View\View;
class SearchController extends Controller
{
protected $entityRepo;
protected $viewService;
protected $searchService;
protected $entityContextManager;
/**
* SearchController constructor.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param EntityRepo $entityRepo
* @param ViewService $viewService
* @param SearchService $searchService
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
{
public function __construct(
EntityRepo $entityRepo,
ViewService $viewService,
SearchService $searchService,
EntityContextManager $entityContextManager
) {
$this->entityRepo = $entityRepo;
$this->viewService = $viewService;
$this->searchService = $searchService;
$this->entityContextManager = $entityContextManager;
parent::__construct();
}
/**
* Searches all entities.
* @param Request $request
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function search(Request $request)
@ -41,7 +52,7 @@ class SearchController extends Controller
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
return view('search/all', [
return view('search.all', [
'entities' => $results['results'],
'totalResults' => $results['total'],
'searchTerm' => $searchTerm,
@ -55,28 +66,28 @@ class SearchController extends Controller
* Searches all entities within a book.
* @param Request $request
* @param integer $bookId
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function searchBook(Request $request, $bookId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchBook($bookId, $term);
return view('partials/entity-list', ['entities' => $results]);
return view('partials.entity-list', ['entities' => $results]);
}
/**
* Searches all entities within a chapter.
* @param Request $request
* @param integer $chapterId
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function searchChapter(Request $request, $chapterId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchChapter($chapterId, $term);
return view('partials/entity-list', ['entities' => $results]);
return view('partials.entity-list', ['entities' => $results]);
}
/**
@ -87,21 +98,64 @@ class SearchController extends Controller
*/
public function searchEntitiesAjax(Request $request)
{
$entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
$searchTerm = $request->get('term', false);
$permission = $request->get('permission', 'view');
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
} else {
$entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type); // TODO - Extract this elsewhere, too specific and stringy
})->toArray();
$entities = $this->viewService->getPopular(20, 0, $entityNames, $permission);
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
}
return view('search/entity-ajax-list', ['entities' => $entities]);
return view('search.entity-ajax-list', ['entities' => $entities]);
}
/**
* Search siblings items in the system.
* @param Request $request
* @return Factory|View|mixed
*/
public function searchSiblings(Request $request)
{
$type = $request->get('entity_type', null);
$id = $request->get('entity_id', null);
$entity = $this->entityRepo->getById($type, $id);
if (!$entity) {
return $this->jsonError(trans('errors.entity_not_found'), 404);
}
$entities = [];
// Page in chapter
if ($entity->isA('page') && $entity->chapter) {
$entities = $this->entityRepo->getChapterChildren($entity->chapter);
}
// Page in book or chapter
if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
$entities = $this->entityRepo->getBookDirectChildren($entity->book);
}
// Book
// Gets just the books in a shelf if shelf is in context
if ($entity->isA('book')) {
$contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
if ($contextShelf) {
$entities = $this->entityRepo->getBookshelfChildren($contextShelf);
} else {
$entities = $this->entityRepo->getAll('book');
}
}
// Shelve
if ($entity->isA('bookshelf')) {
$entities = $this->entityRepo->getAll('bookshelf');
}
return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
}
}

View File

@ -1,5 +1,6 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Auth\User;
use BookStack\Uploads\ImageService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@ -19,7 +20,10 @@ class SettingController extends Controller
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings/index', ['version' => $version]);
return view('settings.index', [
'version' => $version,
'guestUser' => User::getDefault()
]);
}
/**
@ -57,7 +61,7 @@ class SettingController extends Controller
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings/maintenance', ['version' => $version]);
return view('settings.maintenance', ['version' => $version]);
}
/**

View File

@ -41,7 +41,7 @@ class UserController extends Controller
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
$this->setPageTitle(trans('settings.users'));
$users->appends($listDetails);
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
return view('users.index', ['users' => $users, 'listDetails' => $listDetails]);
}
/**
@ -53,7 +53,7 @@ class UserController extends Controller
$this->checkPermission('users-manage');
$authMethod = config('auth.method');
$roles = $this->userRepo->getAllRoles();
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
return view('users.create', ['authMethod' => $authMethod, 'roles' => $roles]);
}
/**
@ -118,7 +118,7 @@ class UserController extends Controller
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
$this->setPageTitle(trans('settings.user_profile'));
$roles = $this->userRepo->getAllRoles();
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
return view('users.edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
}
/**
@ -190,7 +190,7 @@ class UserController extends Controller
$user = $this->userRepo->getById($id);
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
return view('users/delete', ['user' => $user]);
return view('users.delete', ['user' => $user]);
}
/**
@ -232,10 +232,12 @@ class UserController extends Controller
public function showProfilePage($id)
{
$user = $this->userRepo->getById($id);
$userActivity = $this->userRepo->getActivity($user);
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
$assetCounts = $this->userRepo->getAssetCounts($user);
return view('users/profile', [
return view('users.profile', [
'user' => $user,
'activity' => $userActivity,
'recentlyCreated' => $recentlyCreated,
@ -251,19 +253,7 @@ class UserController extends Controller
*/
public function switchBookView($id, Request $request)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
$user = $this->user->findOrFail($id);
setting()->putUser($user, 'books_view_type', $viewType);
return redirect()->back(302, [], "/settings/users/$id");
return $this->switchViewType($id, $request, 'books');
}
/**
@ -274,18 +264,98 @@ class UserController extends Controller
*/
public function switchShelfView($id, Request $request)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
return $this->switchViewType($id, $request, 'bookshelves');
}
/**
* For a type of list, switch with stored view type for a user.
* @param integer $userId
* @param Request $request
* @param string $listName
* @return \Illuminate\Http\RedirectResponse
*/
protected function switchViewType($userId, Request $request, string $listName)
{
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
$user = $this->userRepo->getById($id);
setting()->putUser($user, 'bookshelves_view_type', $viewType);
$user = $this->userRepo->getById($userId);
$key = $listName . '_view_type';
setting()->putUser($user, $key, $viewType);
return redirect()->back(302, [], "/settings/users/$id");
return redirect()->back(302, [], "/settings/users/$userId");
}
/**
* Change the stored sort type for a particular view.
* @param string $id
* @param string $type
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function changeSort(string $id, string $type, Request $request)
{
$validSortTypes = ['books', 'bookshelves'];
if (!in_array($type, $validSortTypes)) {
return redirect()->back(500);
}
return $this->changeListSort($id, $request, $type);
}
/**
* Update the stored section expansion preference for the given user.
* @param string $id
* @param string $key
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function updateExpansionPreference(string $id, string $key, Request $request)
{
$this->checkPermissionOrCurrentUser('users-manage', $id);
$keyWhitelist = ['home-details'];
if (!in_array($key, $keyWhitelist)) {
return response("Invalid key", 500);
}
$newState = $request->get('expand', 'false');
$user = $this->user->findOrFail($id);
setting()->putUser($user, 'section_expansion#' . $key, $newState);
return response("", 204);
}
/**
* Changed the stored preference for a list sort order.
* @param int $userId
* @param Request $request
* @param string $listName
* @return \Illuminate\Http\RedirectResponse
*/
protected function changeListSort(int $userId, Request $request, string $listName)
{
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$sort = $request->get('sort');
if (!in_array($sort, ['name', 'created_at', 'updated_at'])) {
$sort = 'name';
}
$order = $request->get('order');
if (!in_array($order, ['asc', 'desc'])) {
$order = 'asc';
}
$user = $this->user->findOrFail($userId);
$sortKey = $listName . '_sort';
$orderKey = $listName . '_sort_order';
setting()->putUser($user, $sortKey, $sort);
setting()->putUser($user, $orderKey, $order);
return redirect()->back(302, [], "/settings/users/$userId");
}
}

View File

@ -37,7 +37,7 @@ class Authenticate
}
}
if ($this->auth->guest() && !setting('app-public')) {
if (!hasAppAccess()) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {

View File

@ -3,12 +3,14 @@
use Blade;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\BreadcrumbsViewComposer;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Schema;
use Validator;
@ -33,7 +35,6 @@ class AppServiceProvider extends ServiceProvider
return substr_count($uploadName, '.') < 2;
});
// Custom blade view directives
Blade::directive('icon', function ($expression) {
return "<?php echo icon($expression); ?>";
@ -49,6 +50,9 @@ class AppServiceProvider extends ServiceProvider
'BookStack\\Chapter' => Chapter::class,
'BookStack\\Page' => Page::class,
]);
// View Composers
View::composer('partials.breadcrumbs', BreadcrumbsViewComposer::class);
}
/**

View File

@ -2,20 +2,11 @@
namespace BookStack\Providers;
use BookStack\Actions\Activity;
use BookStack\Actions\ActivityService;
use BookStack\Actions\View;
use BookStack\Actions\ViewService;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use BookStack\Uploads\HttpFetcher;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageService;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Filesystem\Factory;
use Illuminate\Support\ServiceProvider;
use Intervention\Image\ImageManager;
class CustomFacadeProvider extends ServiceProvider
{
@ -37,34 +28,19 @@ class CustomFacadeProvider extends ServiceProvider
public function register()
{
$this->app->bind('activity', function () {
return new ActivityService(
$this->app->make(Activity::class),
$this->app->make(PermissionService::class)
);
return $this->app->make(ActivityService::class);
});
$this->app->bind('views', function () {
return new ViewService(
$this->app->make(View::class),
$this->app->make(PermissionService::class)
);
return $this->app->make(ViewService::class);
});
$this->app->bind('setting', function () {
return new SettingService(
$this->app->make(Setting::class),
$this->app->make(Repository::class)
);
return $this->app->make(SettingService::class);
});
$this->app->bind('images', function () {
return new ImageService(
$this->app->make(Image::class),
$this->app->make(ImageManager::class),
$this->app->make(Factory::class),
$this->app->make(Repository::class),
$this->app->make(HttpFetcher::class)
);
return $this->app->make(ImageService::class);
});
}
}

View File

@ -61,9 +61,23 @@ class SettingService
*/
public function getUser($user, $key, $default = false)
{
if ($user->isDefault()) {
return session()->get($key, $default);
}
return $this->get($this->userKey($user->id, $key), $default);
}
/**
* Get a value for the current logged-in user.
* @param $key
* @param bool $default
* @return bool|string
*/
public function getForCurrentUser($key, $default = false)
{
return $this->getUser(user(), $key, $default);
}
/**
* Gets a setting value from the cache or database.
* Looks at the system defaults if not cached or in database.
@ -180,6 +194,9 @@ class SettingService
*/
public function putUser($user, $key, $value)
{
if ($user->isDefault()) {
return session()->put($key, $value);
}
return $this->put($this->userKey($user->id, $key), $value);
}

View File

@ -43,11 +43,19 @@ function user()
* Check if current user is a signed in user.
* @return bool
*/
function signedInUser()
function signedInUser() : bool
{
return auth()->user() && !auth()->user()->isDefault();
}
/**
* Check if the current user has general access.
* @return bool
*/
function hasAppAccess() : bool {
return !auth()->guest() || setting('app-public');
}
/**
* Check if the current user has a permission.
* If an ownable element is passed in the jointPermissions are checked against

2991
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,17 +13,17 @@
"@babel/core": "^7.1.6",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.1.6",
"autoprefixer": "^8.6.5",
"autoprefixer": "^9.4.7",
"babel-loader": "^8.0.4",
"css-loader": "^0.28.11",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"css-loader": "^2.1.0",
"livereload": "^0.7.0",
"mini-css-extract-plugin": "^0.5.0",
"node-sass": "^4.10.0",
"npm-run-all": "^4.1.5",
"postcss-loader": "^2.1.6",
"postcss-loader": "^3.0.0",
"sass-loader": "^7.1.0",
"style-loader": "^0.21.0",
"uglifyjs-webpack-plugin": "^1.3.0",
"style-loader": "^0.23.1",
"uglifyjs-webpack-plugin": "^2.1.1",
"webpack": "^4.26.1",
"webpack-cli": "^3.1.2"
},

View File

@ -1,4 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 13.3h-5.7V19h-2.6v-5.7H5v-2.6h5.7V5h2.6v5.7H19z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

Before

Width:  |  Height:  |  Size: 161 B

After

Width:  |  Height:  |  Size: 166 B

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 24 24"
version="1.1"
id="svg6"
sodipodi:docname="books.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1413"
id="namedview8"
showgrid="false"
inkscape:zoom="19.666667"
inkscape:cx="13.076733"
inkscape:cy="8.7801453"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
d="M0 0h24v24H0z"
fill="none"
id="path2" />
<path
d="M 19.252119,1.707627 H 8.6631356 c -0.9706568,0 -1.7648305,0.7941737 -1.7648305,1.7648305 V 17.591101 c 0,0.970657 0.7941737,1.764831 1.7648305,1.764831 H 19.252119 c 0.970656,0 1.76483,-0.794174 1.76483,-1.764831 V 3.4724575 c 0,-0.9706568 -0.794174,-1.7648305 -1.76483,-1.7648305 z M 8.6631356,3.4724575 H 13.075212 V 10.531779 L 10.869173,9.2081571 8.6631356,10.531779 Z"
id="path4"
inkscape:connector-curvature="0"
style="stroke-width:0.88241524" />
<g
id="g836"
transform="translate(30.610169,3.2033898)">
<path
id="path822"
d="M 0,0 H 24 V 24 H 0 Z"
inkscape:connector-curvature="0"
style="fill:none" />
<path
id="path824"
d="M -27.644068,3.4067797 V 17.40678 c 0,1.1 0.9,2 2,2 h 14 v -2 h -14 V 3.4067797 Z"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,2 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M1.088 2.566h17.42v17.42H1.088z" fill="none"/><path d="M4 20.058h15.892V22H4z"/><path d="M2.902 1.477h17.42v17.42H2.903z" fill="none"/><g><path d="M6.658 3.643V18h-2.38V3.643zM11.326 3.643V18H8.947V3.643zM14.722 3.856l5.613 13.214-2.19.93-5.613-13.214z"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M1.088 2.566h17.42v17.42H1.088z" fill="none"/><path d="M4 20.058h15.892V22H4z"/><path d="M2.902 1.477h17.42v17.42H2.903z" fill="none"/><g><path d="M6.658 3.643V18h-2.38V3.643zM11.326 3.643V18H8.947V3.643zM14.722 3.856l5.613 13.214-2.19.93-5.613-13.214z"/></g></svg>

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 373 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.86 4.118l-9.733 9.609-3.951-3.995-2.98 2.966 6.93 7.184L21.805 7.217z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 161 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.034l6.57-6.554h-4.927V2.966h-3.286V14.48H5.43z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 168 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2.966L5.43 9.52h4.927v11.514h3.286V9.52h4.927z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 165 B

View File

@ -0,0 +1,58 @@
class BreadcrumbListing {
constructor(elem) {
this.elem = elem;
this.searchInput = elem.querySelector('input');
this.loadingElem = elem.querySelector('.loading-container');
this.entityListElem = elem.querySelector('.breadcrumb-listing-entity-list');
this.toggleElem = elem.querySelector('[dropdown-toggle]');
// this.loadingElem.style.display = 'none';
const entityDescriptor = elem.getAttribute('breadcrumb-listing').split(':');
this.entityType = entityDescriptor[0];
this.entityId = Number(entityDescriptor[1]);
this.toggleElem.addEventListener('click', this.onShow.bind(this));
this.searchInput.addEventListener('input', this.onSearch.bind(this));
}
onShow() {
this.loadEntityView();
}
onSearch() {
const input = this.searchInput.value.toLowerCase().trim();
const listItems = this.entityListElem.querySelectorAll('.entity-list-item');
for (let listItem of listItems) {
const match = !input || listItem.textContent.toLowerCase().includes(input);
listItem.style.display = match ? 'flex' : 'none';
}
}
loadEntityView() {
this.toggleLoading(true);
const params = {
'entity_id': this.entityId,
'entity_type': this.entityType,
};
window.$http.get('/search/entity/siblings', {params}).then(resp => {
this.entityListElem.innerHTML = resp.data;
}).catch(err => {
console.error(err);
}).then(() => {
this.toggleLoading(false);
this.onSearch();
});
}
toggleLoading(show = false) {
this.loadingElem.style.display = show ? 'block' : 'none';
}
}
export default BreadcrumbListing;

View File

@ -6,7 +6,7 @@ class DropDown {
constructor(elem) {
this.container = elem;
this.menu = elem.querySelector('ul');
this.menu = elem.querySelector('ul, [dropdown-menu]');
this.toggle = elem.querySelector('[dropdown-toggle]');
this.setupListeners();
}

View File

@ -5,15 +5,17 @@ class EntitySelector {
this.elem = elem;
this.search = '';
this.lastClick = 0;
this.selectedItemData = null;
let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
let entityPermission = elem.hasAttribute('entity-permission') ? elem.getAttribute('entity-permission') : 'view';
const entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
const entityPermission = elem.hasAttribute('entity-permission') ? elem.getAttribute('entity-permission') : 'view';
this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}&permission=${encodeURIComponent(entityPermission)}`);
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.addButton = elem.querySelector('[entity-selector-add-button]');
this.elem.addEventListener('click', this.onClick.bind(this));
@ -26,10 +28,20 @@ class EntitySelector {
this.searchEntities(this.searchInput.value);
}, 200);
});
this.searchInput.addEventListener('keydown', event => {
if (event.keyCode === 13) event.preventDefault();
});
if (this.addButton) {
this.addButton.addEventListener('click', event => {
if (this.selectedItemData) {
this.confirmSelection(this.selectedItemData);
this.unselectAll();
}
});
}
this.showLoading();
this.initialLoad();
}
@ -53,7 +65,7 @@ class EntitySelector {
searchEntities(searchTerm) {
this.input.value = '';
let url = this.searchUrl + `&term=${encodeURIComponent(searchTerm)}`;
let url = `${this.searchUrl}&term=${encodeURIComponent(searchTerm)}`;
window.$http.get(url).then(resp => {
this.resultsContainer.innerHTML = resp.data;
this.hideLoading();
@ -68,49 +80,54 @@ class EntitySelector {
}
onClick(event) {
let t = event.target;
if (t.matches('.entity-list-item *')) {
const listItem = event.target.closest('[data-entity-type]');
if (listItem) {
event.preventDefault();
event.stopPropagation();
let item = t.closest('[data-entity-type]');
this.selectItem(item);
} else if (t.matches('[data-entity-type]')) {
this.selectItem(t)
this.selectItem(listItem);
}
}
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;
const isDblClick = this.isDoubleClick();
const type = item.getAttribute('data-entity-type');
const id = item.getAttribute('data-entity-id');
const isSelected = (!item.classList.contains('selected') || isDblClick);
this.unselectAll();
this.input.value = isSelected ? `${type}:${id}` : '';
if (!isSelected) window.$events.emit('entity-select-change', null);
const link = item.getAttribute('href');
const name = item.querySelector('.entity-list-item-name').textContent;
const data = {id: Number(id), name: name, link: link};
if (isSelected) {
item.classList.add('selected');
item.classList.add('primary-background');
this.selectedItemData = data;
} else {
window.$events.emit('entity-select-change', null)
}
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) {
this.confirmSelection(data);
}
if (isSelected) {
window.$events.emit('entity-select-change', data)
}
}
if (isDblClick) window.$events.emit('entity-select-confirm', data);
if (isSelected) window.$events.emit('entity-select-change', data);
confirmSelection(data) {
window.$events.emit('entity-select-confirm', 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');
for (let selectedElem of selected) {
selectedElem.classList.remove('selected', 'primary-background');
}
this.selectedItemData = null;
}
}

View File

@ -3,8 +3,13 @@ class ExpandToggle {
constructor(elem) {
this.elem = elem;
this.isOpen = false;
// Component state
this.isOpen = elem.getAttribute('expand-toggle-is-open') === 'yes';
this.updateEndpoint = elem.getAttribute('expand-toggle-update-endpoint');
this.selector = elem.getAttribute('expand-toggle');
// Listener setup
elem.addEventListener('click', this.click.bind(this));
}
@ -53,11 +58,20 @@ class ExpandToggle {
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]);
const matchingElems = document.querySelectorAll(this.selector);
for (let match of matchingElems) {
this.isOpen ? this.close(match) : this.open(match);
}
this.isOpen = !this.isOpen;
this.updateSystemAjax(this.isOpen);
}
updateSystemAjax(isOpen) {
window.$http.patch(this.updateEndpoint, {
expand: isOpen ? 'true' : 'false'
});
}
}

View File

@ -0,0 +1,31 @@
class HeaderMobileToggle {
constructor(elem) {
this.elem = elem;
this.toggleButton = elem.querySelector('.mobile-menu-toggle');
this.menu = elem.querySelector('.header-links');
this.open = false;
this.toggleButton.addEventListener('click', this.onToggle.bind(this));
this.onWindowClick = this.onWindowClick.bind(this);
}
onToggle(event) {
this.open = !this.open;
this.menu.classList.toggle('show', this.open);
if (this.open) {
window.addEventListener('click', this.onWindowClick)
} else {
window.removeEventListener('click', this.onWindowClick)
}
event.stopPropagation();
}
onWindowClick(event) {
this.onToggle(event);
}
}
module.exports = HeaderMobileToggle;

View File

@ -18,7 +18,11 @@ import toggleSwitch from "./toggle-switch";
import pageDisplay from "./page-display";
import shelfSort from "./shelf-sort";
import homepageControl from "./homepage-control";
import headerMobileToggle from "./header-mobile-toggle";
import listSortControl from "./list-sort-control";
import triLayout from "./tri-layout";
import breadcrumbListing from "./breadcrumb-listing";
import permissionsTable from "./permissions-table";
const componentMapping = {
'dropdown': dropdown,
@ -41,6 +45,11 @@ const componentMapping = {
'page-display': pageDisplay,
'shelf-sort': shelfSort,
'homepage-control': homepageControl,
'header-mobile-toggle': headerMobileToggle,
'list-sort-control': listSortControl,
'tri-layout': triLayout,
'breadcrumb-listing': breadcrumbListing,
'permissions-table': permissionsTable,
};
window.components = {};
@ -79,4 +88,4 @@ function initAll(parentElement) {
window.components.init = initAll;
export default initAll;
export default initAll;

View File

@ -0,0 +1,42 @@
/**
* ListSortControl
* Manages the logic for the control which provides list sorting options.
*/
class ListSortControl {
constructor(elem) {
this.elem = elem;
this.sortInput = elem.querySelector('[name="sort"]');
this.orderInput = elem.querySelector('[name="order"]');
this.form = elem.querySelector('form');
this.elem.addEventListener('click', event => {
if (event.target.closest('[data-sort-value]') !== null) {
this.sortOptionClick(event);
}
if (event.target.closest('[data-sort-dir]') !== null) {
this.sortDirectionClick(event);
}
})
}
sortOptionClick(event) {
const sortOption = event.target.closest('[data-sort-value]');
this.sortInput.value = sortOption.getAttribute('data-sort-value');
event.preventDefault();
this.form.submit();
}
sortDirectionClick(event) {
const currentDir = this.orderInput.value;
const newDir = (currentDir === 'asc') ? 'desc' : 'asc';
this.orderInput.value = newDir;
event.preventDefault();
this.form.submit();
}
}
export default ListSortControl;

View File

@ -71,6 +71,19 @@ class MarkdownEditor {
if (action === 'insertDrawing') this.actionStartDrawing();
});
// Mobile section toggling
this.elem.addEventListener('click', event => {
const toolbarLabel = event.target.closest('.editor-toolbar-label');
if (!toolbarLabel) return;
const currentActiveSections = this.elem.querySelectorAll('.markdown-editor-wrap');
for (let activeElem of currentActiveSections) {
activeElem.classList.remove('active');
}
toolbarLabel.closest('.markdown-editor-wrap').classList.add('active');
});
window.$events.listen('editor-markdown-update', value => {
this.cm.setValue(value);
this.updateAndRender();

View File

@ -6,7 +6,7 @@ class Overlay {
elem.addEventListener('click', event => {
if (event.target === elem) return this.hide();
});
let closeButtons = elem.querySelectorAll('.overlay-close');
let closeButtons = elem.querySelectorAll('.popup-header-close');
for (let i=0; i < closeButtons.length; i++) {
closeButtons[i].addEventListener('click', this.hide.bind(this));
}

View File

@ -54,7 +54,7 @@ class PageComments {
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';
textArea.style.height = ((lineCount * 20) + 40) + 'px';
this.editingComment = commentElem;
}
@ -88,6 +88,7 @@ class PageComments {
commentElem.parentNode.removeChild(commentElem);
window.$events.emit('success', window.trans('entities.comment_deleted_success'));
this.updateCount();
this.hideForm();
});
}
@ -129,7 +130,7 @@ class PageComments {
showForm() {
this.formContainer.style.display = 'block';
this.formContainer.parentNode.style.display = 'block';
this.elem.querySelector('[comment-add-button]').style.display = 'none';
this.elem.querySelector('[comment-add-button-container]').style.display = 'none';
this.formInput.focus();
window.scrollToElement(this.formInput);
}
@ -137,7 +138,18 @@ class PageComments {
hideForm() {
this.formContainer.style.display = 'none';
this.formContainer.parentNode.style.display = 'none';
this.elem.querySelector('[comment-add-button]').style.display = 'block';
const addButtonContainer = this.elem.querySelector('[comment-add-button-container]');
if (this.getCommentCount() > 0) {
this.elem.appendChild(addButtonContainer)
} else {
const countBar = this.elem.querySelector('[comment-count-bar]');
countBar.appendChild(addButtonContainer);
}
addButtonContainer.style.display = 'block';
}
getCommentCount() {
return this.elem.querySelectorAll('.comment-box[comment]').length;
}
setReply(commentElem) {

View File

@ -208,8 +208,8 @@ class PageDisplay {
let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
// observe each heading
for (let i = 0; i !== headings.length; ++i) {
pageNavObserver.observe(headings[i]);
for (let heading of headings) {
pageNavObserver.observe(heading);
}
}
@ -221,14 +221,9 @@ class PageDisplay {
}
function toggleAnchorHighlighting(elementId, shouldHighlight) {
let anchorsToHighlight = pageNav.querySelectorAll('a[href="#' + elementId + '"]');
for (let i = 0; i < anchorsToHighlight.length; i++) {
// Change below to use classList.toggle when IE support is dropped.
if (shouldHighlight) {
anchorsToHighlight[i].classList.add('current-heading');
} else {
anchorsToHighlight[i].classList.remove('current-heading');
}
const anchorsToHighlight = pageNav.querySelectorAll('a[href="#' + elementId + '"]');
for (let anchor of anchorsToHighlight) {
anchor.closest('li').classList.toggle('current-heading', shouldHighlight);
}
}
}

View File

@ -0,0 +1,66 @@
class PermissionsTable {
constructor(elem) {
this.container = elem;
// Handle toggle all event
const toggleAll = elem.querySelector('[permissions-table-toggle-all]');
toggleAll.addEventListener('click', this.toggleAllClick.bind(this));
// Handle toggle row event
const toggleRowElems = elem.querySelectorAll('[permissions-table-toggle-all-in-row]');
for (let toggleRowElem of toggleRowElems) {
toggleRowElem.addEventListener('click', this.toggleRowClick.bind(this));
}
// Handle toggle column event
const toggleColumnElems = elem.querySelectorAll('[permissions-table-toggle-all-in-column]');
for (let toggleColElem of toggleColumnElems) {
toggleColElem.addEventListener('click', this.toggleColumnClick.bind(this));
}
}
toggleAllClick(event) {
event.preventDefault();
this.toggleAllInElement(this.container);
}
toggleRowClick(event) {
event.preventDefault();
this.toggleAllInElement(event.target.closest('tr'));
}
toggleColumnClick(event) {
event.preventDefault();
const tableCell = event.target.closest('th,td');
const colIndex = Array.from(tableCell.parentElement.children).indexOf(tableCell);
const tableRows = tableCell.closest('table').querySelectorAll('tr');
const inputsToToggle = [];
for (let row of tableRows) {
const targetCell = row.children[colIndex];
if (targetCell) {
inputsToToggle.push(...targetCell.querySelectorAll('input[type=checkbox]'));
}
}
this.toggleAllInputs(inputsToToggle);
}
toggleAllInElement(domElem) {
const inputsToToggle = domElem.querySelectorAll('input[type=checkbox]');
this.toggleAllInputs(inputsToToggle);
}
toggleAllInputs(inputsToToggle) {
const currentState = inputsToToggle.length > 0 ? inputsToToggle[0].checked : false;
for (let checkbox of inputsToToggle) {
checkbox.checked = !currentState;
checkbox.dispatchEvent(new Event('change'));
}
}
}
export default PermissionsTable;

View File

@ -3,15 +3,15 @@ class ToggleSwitch {
constructor(elem) {
this.elem = elem;
this.input = elem.querySelector('input');
this.input = elem.querySelector('input[type=hidden]');
this.checkbox = elem.querySelector('input[type=checkbox]');
this.elem.onclick = this.onClick.bind(this);
this.checkbox.addEventListener('change', this.onClick.bind(this));
}
onClick(event) {
let checked = this.input.value !== 'true';
let checked = this.checkbox.checked;
this.input.value = checked ? 'true' : 'false';
checked ? this.elem.classList.add('active') : this.elem.classList.remove('active');
}
}

View File

@ -0,0 +1,95 @@
class TriLayout {
constructor(elem) {
this.elem = elem;
this.lastLayoutType = 'none';
this.onDestroy = null;
this.scrollCache = {
'content': 0,
'info': 0,
};
this.lastTabShown = 'content';
// Bind any listeners
this.mobileTabClick = this.mobileTabClick.bind(this);
// Watch layout changes
this.updateLayout();
window.addEventListener('resize', event => {
this.updateLayout();
}, {passive: true});
}
updateLayout() {
let newLayout = 'tablet';
if (window.innerWidth <= 1000) newLayout = 'mobile';
if (window.innerWidth >= 1400) newLayout = 'desktop';
if (newLayout === this.lastLayoutType) return;
if (this.onDestroy) {
this.onDestroy();
this.onDestroy = null;
}
if (newLayout === 'desktop') {
this.setupDesktop();
} else if (newLayout === 'mobile') {
this.setupMobile();
}
this.lastLayoutType = newLayout;
}
setupMobile() {
const layoutTabs = document.querySelectorAll('[tri-layout-mobile-tab]');
for (let tab of layoutTabs) {
tab.addEventListener('click', this.mobileTabClick);
}
this.onDestroy = () => {
for (let tab of layoutTabs) {
tab.removeEventListener('click', this.mobileTabClick);
}
}
}
setupDesktop() {
//
}
/**
* Action to run when the mobile info toggle bar is clicked/tapped
* @param event
*/
mobileTabClick(event) {
const tab = event.target.getAttribute('tri-layout-mobile-tab');
this.scrollCache[this.lastTabShown] = document.documentElement.scrollTop;
// Set tab status
const activeTabs = document.querySelectorAll('.tri-layout-mobile-tab.active');
for (let tab of activeTabs) {
tab.classList.remove('active');
}
event.target.classList.add('active');
// Toggle section
const showInfo = (tab === 'info');
this.elem.classList.toggle('show-info', showInfo);
// Set the scroll position from cache
const pageHeader = document.querySelector('header');
const defaultScrollTop = pageHeader.getBoundingClientRect().bottom;
document.documentElement.scrollTop = this.scrollCache[tab] || defaultScrollTop;
setTimeout(() => {
document.documentElement.scrollTop = this.scrollCache[tab] || defaultScrollTop;
}, 50);
this.lastTabShown = tab;
}
}
export default TriLayout;

View File

@ -432,7 +432,7 @@ class WysiwygEditor {
plugins: this.plugins,
imagetools_toolbar: 'imageoptions',
toolbar: this.getToolBar(),
content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
content_style: "html, body {background: #FFF;} body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
style_formats: [
{title: "Header Large", format: "h2"},
{title: "Header Medium", format: "h3"},
@ -517,6 +517,16 @@ class WysiwygEditor {
if (scrollId) {
scrollToText(scrollId);
}
// Override for touch events to allow scroll on mobile
const container = editor.getContainer();
const toolbarButtons = container.querySelectorAll('.mce-btn');
for (let button of toolbarButtons) {
button.addEventListener('touchstart', event => {
event.stopPropagation();
});
}
window.editor = editor;
});
function editorChange() {
@ -600,6 +610,7 @@ class WysiwygEditor {
// Paste image-uploads
editor.on('paste', event => editorPaste(event, editor, context));
}
};
}

View File

@ -36,26 +36,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 menuIn {
from {
opacity: 0;
transform: scale3d(0, 0, 1);
}
to {
opacity: 1;
transform: scale3d(1, 1, 1);
}
}
@keyframes loadingBob {
0% {
transform: translate3d(0, 0, 0);
@ -89,8 +69,4 @@
animation-duration: 180ms;
animation-delay: 0s;
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
}
.selectFade {
transition: background-color ease-in-out 3000ms;
}

View File

@ -1,136 +1,7 @@
/*
* This file container all block styling including background shading,
* margins, paddings & borders.
*/
/*
* Background Shading
*/
.shaded {
background-color: #f1f1f1;
&.pos {
background-color: lighten($positive, 40%);
}
&.neg {
background-color: lighten($negative, 20%);
}
&.primary {
background-color: lighten($primary, 40%);
}
&.secondary {
background-color: lighten($secondary, 30%);
}
}
/*
* Bordering
*/
.bordered {
border: 1px solid #BBB;
&.pos {
border-color: $positive;
}
&.neg {
border-color: $negative;
}
&.primary {
border-color: $primary;
}
&.secondary {
border-color: $secondary;
}
&.thick {
border-width: 2px;
}
}
.rounded {
border-radius: 3px;
}
/*
* Padding
*/
.nopadding {
padding: 0;
}
.padded {
padding: $-l;
&.large {
padding: $-xl;
}
>h1, >h2, >h3, >h4 {
&:first-child {
margin-top: 0.1em;
}
}
}
.padded-vertical, .padded-top {
padding-top: $-m;
&.large {
padding-top: $-xl;
}
}
.padded-vertical, .padded-bottom {
padding-bottom: $-m;
&.large {
padding-bottom: $-xl;
}
}
.padded-horizontal, .padded-left {
padding-left: $-m;
&.large {
padding-left: $-xl;
}
}
.padded-horizontal, .padded-right {
padding-right: $-m;
&.large {
padding-right: $-xl;
}
}
/*
* Margins
*/
.margins {
margin: $-l;
&.large {
margin: $-xl;
}
}
.margins-vertical, .margin-top {
margin-top: $-m;
&.large {
margin-top: $-xl;
}
}
.margins-vertical, .margin-bottom {
margin-bottom: $-m;
&.large {
margin-bottom: $-xl;
}
}
.margins-horizontal, .margin-left {
margin-left: $-m;
&.large {
margin-left: $-xl;
}
}
.margins-horizontal, .margin-right {
margin-right: $-m;
&.large {
margin-right: $-xl;
}
}
/**
* Callouts
*/
.callout {
border-left: 3px solid #BBB;
background-color: #EEE;
@ -182,19 +53,22 @@
}
}
/**
* Card-style blocks
*/
.card {
margin: $-m;
background-color: #FFF;
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.2);
box-shadow: $bs-card;
border-radius: 3px;
border: 1px solid transparent;
h3 {
padding: $-m;
border-bottom: 1px solid #E8E8E8;
padding: $-m $-m $-xs;
margin: 0;
font-size: $fs-s;
color: #888;
fill: #888;
font-size: $fs-m;
color: #222;
fill: #222;
font-weight: 400;
text-transform: uppercase;
}
h3 a {
line-height: 1;
@ -208,18 +82,11 @@
}
}
.sidebar .card {
h3, .body, .empty-text {
padding: $-s $-m;
}
}
.card.drag-card {
border: 1px solid #DDD;
border-radius: 4px;
display: flex;
padding: 0;
padding-left: $-s + 28px;
padding: 0 0 0 ($-s + 28px);
margin: $-s 0;
position: relative;
.drag-card-action {
@ -227,14 +94,12 @@
}
.handle, .drag-card-action {
display: flex;
padding: 0;
align-items: center;
text-align: center;
justify-content: center;
width: 28px;
flex-grow: 0;
padding-left: $-xs;
padding-right: $-xs;
padding: 0 $-xs;
&:hover {
background-color: #EEE;
}
@ -246,9 +111,6 @@
margin: $-s 0;
width: 100%;
}
> div.padded {
padding: $-s 0 !important;
}
.handle {
background-color: #EEE;
left: 0;
@ -263,12 +125,89 @@
}
}
.well {
background-color: #F8F8F8;
padding: $-m;
border: 1px solid #DDD;
.grid-card {
display: flex;
flex-direction: column;
border: 1px solid #ddd;
margin-bottom: $-l;
border-radius: 4px;
overflow: hidden;
min-width: 100px;
color: $text-dark;
transition: border-color ease-in-out 120ms, box-shadow ease-in-out 120ms;
&:hover {
color: $text-dark;
text-decoration: none;
box-shadow: $bs-card;
}
h2 {
width: 100%;
font-size: 1.5em;
margin: 0 0 10px;
}
p {
font-size: .7rem;
margin: 0;
line-height: 1.6em;
}
.grid-card-content {
flex: 1;
border-top: 0;
border-bottom-width: 2px;
}
.grid-card-content, .grid-card-footer {
padding: $-l;
}
.grid-card-content + .grid-card-footer {
padding-top: 0;
}
}
.bookshelf-grid-item .grid-card-content h2 a {
color: $color-bookshelf;
fill: $color-bookshelf;
}
.book-grid-item .grid-card-footer {
p.small {
font-size: .8em;
margin: 0;
}
}
.content-wrap.card {
padding: $-m $-xxl;
margin-left: auto;
margin-right: auto;
margin-bottom: $-xl;
overflow: auto;
min-height: 60vh;
&.auto-height {
min-height: 0;
}
&.fill-width {
width: 100%;
}
}
@include smaller-than($xxl) {
.content-wrap.card {
padding: $-l $-xl;
}
}
@include smaller-than($m) {
.content-wrap.card {
padding: $-m $-l;
}
}
@include smaller-than($s) {
.content-wrap.card {
padding: $-m $-s;
}
}
/**
* Tags
*/
.tag-item {
display: inline-flex;
margin-bottom: $-xs;

View File

@ -1,15 +1,14 @@
button {
font-size: 100%;
}
@mixin generate-button-colors($textColor, $backgroundColor) {
background-color: $backgroundColor;
color: $textColor;
fill: $textColor;
text-transform: uppercase;
border: 1px solid $backgroundColor;
vertical-align: top;
&:hover {
background-color: lighten($backgroundColor, 8%);
//box-shadow: $bs-med;
text-decoration: none;
color: $textColor;
}
&:active {
@ -18,7 +17,6 @@
&:focus {
background-color: lighten($backgroundColor, 4%);
box-shadow: $bs-light;
text-decoration: none;
color: $textColor;
}
}
@ -26,42 +24,36 @@
// Button Specific Variables
$button-border-radius: 2px;
.button-base {
.button {
text-decoration: none;
font-size: $fs-m;
font-size: 0.85rem;
line-height: 1.4em;
padding: $-xs*1.3 $-m;
margin: $-xs $-xs $-xs 0;
margin-top: $-xs;
margin-bottom: $-xs;
display: inline-block;
border: none;
font-weight: 400;
outline: 0;
border-radius: $button-border-radius;
cursor: pointer;
transition: all ease-in-out 120ms;
box-shadow: 0;
@include generate-button-colors(#EEE, $primary);
}
.button, input[type="button"], input[type="submit"] {
@extend .button-base;
&.pos {
@include generate-button-colors(#EEE, $positive);
transition: background-color ease-in-out 120ms, box-shadow ease-in-out 120ms;
box-shadow: none;
background-color: $primary;
color: #FFF;
fill: #FFF;
text-transform: uppercase;
border: 1px solid $primary;
vertical-align: top;
&:hover, &:focus {
text-decoration: none;
}
&.neg {
@include generate-button-colors(#EEE, $negative);
}
&.secondary {
@include generate-button-colors(#EEE, $secondary);
}
&.muted {
@include generate-button-colors(#EEE, #AAA);
}
&.muted-light {
@include generate-button-colors(#666, #e4e4e4);
&:active {
background-color: darken($primary, 8%);
}
}
.button.primary {
@include generate-button-colors(#FFFFFF, $primary);
}
.button.outline {
background-color: transparent;
color: #888;
@ -71,78 +63,38 @@ $button-border-radius: 2px;
box-shadow: none;
background-color: #EEE;
}
&.page {
border-color: $color-page;
color: $color-page;
fill: $color-page;
&:hover, &:focus, &:active {
background-color: $color-page;
color: #FFF;
fill: #FFF;
}
}
&.chapter {
border-color: $color-chapter;
color: $color-chapter;
fill: $color-chapter;
&:hover, &:focus, &:active {
background-color: $color-chapter;
color: #FFF;
fill: #FFF;
}
}
&.book {
border-color: $color-book;
color: $color-book;
fill: $color-book;
&:hover, &:focus, &:active {
background-color: $color-book;
color: #FFF;
fill: #FFF;
}
}
}
.button + .button {
margin-left: $-s;
}
.button.small {
font-size: 0.75rem;
padding: $-xs*1.2 $-s;
}
.text-button {
@extend .link;
cursor: pointer;
background-color: transparent;
padding: 0;
margin: 0;
border: none;
user-select: none;
font-size: 0.75rem;
line-height: 1.4em;
&:focus, &:active {
outline: 0;
}
&:hover {
text-decoration: none;
}
&.neg {
color: $negative;
}
}
.button-group {
@include clearfix;
.button, button[type="button"] {
margin: $-xs 0 $-xs 0;
float: left;
border-radius: 0;
&:first-child {
border-radius: $button-border-radius 0 0 $button-border-radius;
}
&:last-child {
border-radius: 0 $button-border-radius $button-border-radius 0;
}
}
}
.button.block {
width: 100%;
text-align: center;
text-align: left;
display: block;
&.text-left {
text-align: left;
}
}
.button.icon {
@ -160,9 +112,7 @@ $button-border-radius: 2px;
width: 24px;
height: 24px;
}
padding: $-s $-m;
padding-bottom: $-s - 2px;
padding-left: $-m*2 + 24px;
padding: $-s $-m ($-s - 2px) ($-m*2 + 24px);
}
.button[disabled] {

View File

@ -0,0 +1,72 @@
/*
* Status text colors
*/
.text-pos, .text-pos:hover, .text-pos-hover:hover {
color: $positive !important;
fill: $positive !important;
}
.text-warn, .text-warn:hover, .text-warn-hover:hover {
color: $warning !important;
fill: $warning !important;
}
.text-neg, .text-neg:hover, .text-neg-hover:hover {
color: $negative !important;
fill: $negative !important;
}
/*
* Style text colors
*/
.text-primary, .text-primary:hover, .text-primary-hover:hover {
color: $primary !important;
fill: $primary !important;
}
.text-muted {
color: lighten($text-dark, 26%) !important;
fill: lighten($text-dark, 26%) !important;
&.small, .small {
color: lighten($text-dark, 32%) !important;
fill: lighten($text-dark, 32%) !important;
}
}
/*
* Entity text colors
*/
.text-bookshelf, .text-bookshelf:hover {
color: $color-bookshelf;
fill: $color-bookshelf;
}
.text-book, .text-book:hover {
color: $color-book;
fill: $color-book;
}
.text-page, .text-page:hover {
color: $color-page;
fill: $color-page;
}
.text-page.draft, .text-page.draft:hover {
color: $color-page-draft;
fill: $color-page-draft;
}
.text-chapter, .text-chapter:hover {
color: $color-chapter;
fill: $color-chapter;
}
/*
* Entity background colors
*/
.bg-book {
background-color: $color-book;
}
.bg-chapter {
background-color: $color-chapter;
}
.bg-shelf {
background-color: $color-bookshelf;
}

View File

@ -54,13 +54,18 @@
transition: all ease-in-out 180ms;
user-select: none;
svg[data-icon="caret-right"] {
margin-right: 0;
font-size: 1rem;
transition: all ease-in-out 180ms;
transform: rotate(0deg);
transform-origin: 25% 50%;
transform-origin: 50% 50%;
}
&.open svg[data-icon="caret-right"] {
transform: rotate(90deg);
}
svg[data-icon="caret-right"] + * {
margin-left: $-xs;
}
}
[overlay] {
@ -110,7 +115,7 @@
}
}
.corner-button {
.popup-footer button, .popup-header-close {
position: absolute;
top: 0;
right: 0;
@ -118,6 +123,16 @@
height: 40px;
border-radius: 0;
box-shadow: none;
&:active {
outline: 0;
}
}
.popup-header-close {
background-color: transparent;
border: 0;
color: #FFF;
font-size: 16px;
padding: 0 $-m;
}
.popup-header, .popup-footer {
@ -130,6 +145,9 @@
padding: 8px $-m;
}
}
.popup-footer {
margin-top: 1px;
}
body.flexbox-support #entity-selector-wrap .popup-body .form-group {
height: 444px;
min-height: 444px;
@ -137,6 +155,9 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
#entity-selector-wrap .popup-body .form-group {
margin: 0;
}
.popup-body .entity-selector-container {
flex: 1;
}
.image-manager-body {
min-height: 70vh;
@ -583,27 +604,26 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
}
.comment-box {
clear: left;
border: 1px solid #DDD;
margin-bottom: $-s;
border-radius: 3px;
border-radius: 4px;
background-color: #FFF;
.content {
padding: $-s;
font-size: 0.666em;
p, ul, ol {
font-size: $fs-m;
margin: .5em 0;
}
}
.reply-row {
padding: $-xs $-s;
.actions {
opacity: 0;
transition: opacity ease-in-out 120ms;
}
&:hover .actions {
opacity: 1;
}
}
.comment-box .header {
padding: $-xs $-s;
background-color: #f8f8f8;
border-bottom: 1px solid #DDD;
.meta {
img, a, span {
display: inline-block;
@ -626,4 +646,11 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
#tag-manager .drag-card {
max-width: 500px;
}
.permissions-table [permissions-table-toggle-all-in-row] {
display: none;
}
.permissions-table tr:hover [permissions-table-toggle-all-in-row] {
display: inline;
}

View File

@ -63,6 +63,34 @@
}
}
@include smaller-than($m) {
#markdown-editor {
flex-direction: column;
}
#markdown-editor .markdown-editor-wrap {
width: 100%;
max-width: 100%;
}
#markdown-editor .editor-toolbar {
padding: 0;
}
#markdown-editor .editor-toolbar > * {
padding: $-xs $-s;
}
.editor-toolbar-label {
float: none !important;
border-bottom: 1px solid #DDD;
display: block;
}
.markdown-editor-wrap:not(.active) .editor-toolbar + div, .markdown-editor-wrap:not(.active) .editor-toolbar .buttons {
display: none;
}
#markdown-editor .markdown-editor-wrap:not(.active) {
flex-grow: 0;
flex: none;
}
}
.markdown-display {
padding: 0 $-m 0;
margin-left: -1px;
@ -98,7 +126,7 @@ label {
line-height: 1.4em;
font-size: 0.94em;
font-weight: 400;
color: #999;
color: #666;
padding-bottom: 2px;
margin-bottom: 0.2em;
&.inline {
@ -139,56 +167,77 @@ input[type=date] {
}
.toggle-switch {
display: inline-block;
background-color: #BBB;
width: 36px;
height: 14px;
border-radius: 7px;
position: relative;
transition: all ease-in-out 120ms;
cursor: pointer;
user-select: none;
&:after {
content: '';
display: block;
position: relative;
left: 0;
margin-top: -3px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #fafafa;
border: 1px solid #CCC;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
transition: all ease-in-out 120ms;
}
&.active {
background-color: rgba($positive, 0.4);
&:after {
left: 16px;
background-color: $positive;
border: darken($positive, 20%);
display: inline-grid;
grid-template-columns: (16px + $-s) 1fr;
align-items: center;
margin: $-m 0;
.custom-checkbox {
width: 16px;
height: 16px;
border-radius: 2px;
display: inline-block;
border: 2px solid currentColor;
opacity: 0.6;
overflow: hidden;
fill: currentColor;
.svg-icon {
width: 100%;
height: 100%;
margin: 0;
bottom: auto;
top: -1.5px;
left: 0;
transition: transform ease-in-out 120ms;
transform: scale(0);
transform-origin: center center;
}
}
input[type=checkbox] {
display: none;
}
input[type=checkbox]:checked + .custom-checkbox .svg-icon {
transform: scale(1);
}
.custom-checkbox:hover {
background-color: rgba(0, 0, 0, 0.05);
opacity: 0.8;
}
}
.toggle-switch-checkbox {
display: none;
}
input:checked + .toggle-switch {
background-color: rgba($positive, 0.4);
&:after {
left: 16px;
background-color: $positive;
border: darken($positive, 20%);
.toggle-switch-list {
.toggle-switch {
margin: $-xs 0;
}
&.compact .toggle-switch {
margin: 1px 0;
}
}
.form-group {
margin-bottom: $-s;
textarea {
display: block;
}
.setting-list > div {
border-bottom: 1px solid #DDD;
padding: $-xl 0;
&:last-child {
border-bottom: none;
}
}
.setting-list-label {
color: #222;
font-size: 1rem;
}
.setting-list-label + p.small {
margin-bottom: 0;
}
.setting-list-label + .grid {
margin-top: $-m;
}
.setting-list .grid, .stretch-inputs {
input[type=text], input[type=email], input[type=password], select {
width: 100%;
min-height: 64px;
}
}
@ -197,6 +246,8 @@ input:checked + .toggle-switch {
font-family: monospace;
font-size: 12px;
min-height: 100px;
display: block;
width: 100%;
}
.form-group {
@ -206,11 +257,9 @@ input:checked + .toggle-switch {
}
.form-group[collapsible] {
margin-left: -$-m;
margin-right: -$-m;
padding: 0 $-m;
border-top: 1px solid #DDD;
border-bottom: 1px solid #DDD;
border: 1px solid #DDD;
border-radius: 4px;
.collapse-title {
margin-left: -$-m;
margin-right: -$-m;
@ -238,9 +287,6 @@ input:checked + .toggle-switch {
&.open .collapse-title label:before {
transform: rotate(90deg);
}
&+.form-group[collapsible] {
margin-top: -($-s + 1);
}
}
.inline-input-style {
@ -304,6 +350,13 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
width: 300px;
max-width: 100%;
}
&.flexible input {
width: 100%;
}
.search-box-cancel {
left: auto;
right: 0;
}
}
.outline > input {
@ -317,13 +370,6 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
}
}
#login-form label[for="remember"] {
margin: 0;
}
#login-form label.toggle-switch {
margin-left: $-xl;
}
.image-picker img {
background-color: #BBB;
}

View File

@ -1,930 +0,0 @@
/** Flexbox styling rules **/
body.flexbox {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%;
min-height: 100%;
max-height: 100%;
overflow: hidden;
#content {
flex: 1;
display: flex;
min-height: 0;
}
}
.flex-fill {
display: flex;
align-items: stretch;
min-height: 0;
max-width: 100%;
position: relative;
&.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;
}
.flex.sidebar {
flex: 1;
background-color: #F2F2F2;
max-width: 360px;
min-height: 90vh;
section {
margin: $-m;
}
}
.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;
fill: #666;
font-size: 20px;
vertical-align: middle;
text-align: center;
border: 1px solid #DDD;
border-top: 1px solid #BBB;
padding-top: $-m;
cursor: pointer;
svg {
opacity: 0.5;
transition: all ease-in-out 120ms;
margin: 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: auto;
-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 {
max-width: 100%;
}
.container {
max-width: $max-width;
margin-left: auto;
margin-right: auto;
padding-left: $-m;
padding-right: $-m;
&.fluid {
max-width: 100%;
}
&.medium {
max-width: 992px;
}
&.small {
max-width: 840px;
}
&.nopad {
padding-left: 0;
padding-right: 0;
}
}
.row {
margin-left: -$-m;
margin-right: -$-m;
}
.grid {
display: grid;
grid-column-gap: $-l;
grid-row-gap: $-l;
&.third {
grid-template-columns: 1fr 1fr 1fr;
}
}
.grid-card {
display: flex;
flex-direction: column;
border: 1px solid #ddd;
min-width: 100px;
h2 {
width: 100%;
font-size: 1.5em;
margin: 0 0 10px;
}
h2 a {
display: block;
width: 100%;
line-height: 1.2;
text-decoration: none;
}
p {
font-size: .85em;
margin: 0;
line-height: 1.6em;
}
.grid-card-content {
flex: 1;
border-top: 0;
border-bottom-width: 2px;
}
.grid-card-content, .grid-card-footer {
padding: $-l;
}
.grid-card-content + .grid-card-footer {
padding-top: 0;
}
}
.book-grid-item .grid-card-content h2 a {
color: $color-book;
fill: $color-book;
}
.bookshelf-grid-item .grid-card-content h2 a {
color: $color-bookshelf;
fill: $color-bookshelf;
}
.book-grid-item .grid-card-footer {
p.small {
font-size: .8em;
margin: 0;
}
}
@include smaller-than($m) {
.grid.third {
grid-template-columns: 1fr 1fr;
}
}
@include smaller-than($s) {
.grid.third {
grid-template-columns: 1fr;
}
}
.float {
float: left;
&.right {
float: right;
}
}
.block {
display: block;
position: relative;
}
.inline {
display: inline;
}
.block.inline {
display: inline-block;
}
.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
position: relative;
min-height: 1px;
padding-left: $-m;
padding-right: $-m;
}
.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
float: left;
}
.col-xs-12 {
width: 100%;
}
.col-xs-11 {
width: 91.66666667%;
}
.col-xs-10 {
width: 83.33333333%;
}
.col-xs-9 {
width: 75%;
}
.col-xs-8 {
width: 66.66666667%;
}
.col-xs-7 {
width: 58.33333333%;
}
.col-xs-6 {
width: 50%;
}
.col-xs-5 {
width: 41.66666667%;
}
.col-xs-4 {
width: 33.33333333%;
}
.col-xs-3 {
width: 25%;
}
.col-xs-2 {
width: 16.66666667%;
}
.col-xs-1 {
width: 8.33333333%;
}
.col-xs-pull-12 {
right: 100%;
}
.col-xs-pull-11 {
right: 91.66666667%;
}
.col-xs-pull-10 {
right: 83.33333333%;
}
.col-xs-pull-9 {
right: 75%;
}
.col-xs-pull-8 {
right: 66.66666667%;
}
.col-xs-pull-7 {
right: 58.33333333%;
}
.col-xs-pull-6 {
right: 50%;
}
.col-xs-pull-5 {
right: 41.66666667%;
}
.col-xs-pull-4 {
right: 33.33333333%;
}
.col-xs-pull-3 {
right: 25%;
}
.col-xs-pull-2 {
right: 16.66666667%;
}
.col-xs-pull-1 {
right: 8.33333333%;
}
.col-xs-pull-0 {
right: auto;
}
.col-xs-push-12 {
left: 100%;
}
.col-xs-push-11 {
left: 91.66666667%;
}
.col-xs-push-10 {
left: 83.33333333%;
}
.col-xs-push-9 {
left: 75%;
}
.col-xs-push-8 {
left: 66.66666667%;
}
.col-xs-push-7 {
left: 58.33333333%;
}
.col-xs-push-6 {
left: 50%;
}
.col-xs-push-5 {
left: 41.66666667%;
}
.col-xs-push-4 {
left: 33.33333333%;
}
.col-xs-push-3 {
left: 25%;
}
.col-xs-push-2 {
left: 16.66666667%;
}
.col-xs-push-1 {
left: 8.33333333%;
}
.col-xs-push-0 {
left: auto;
}
.col-xs-offset-12 {
margin-left: 100%;
}
.col-xs-offset-11 {
margin-left: 91.66666667%;
}
.col-xs-offset-10 {
margin-left: 83.33333333%;
}
.col-xs-offset-9 {
margin-left: 75%;
}
.col-xs-offset-8 {
margin-left: 66.66666667%;
}
.col-xs-offset-7 {
margin-left: 58.33333333%;
}
.col-xs-offset-6 {
margin-left: 50%;
}
.col-xs-offset-5 {
margin-left: 41.66666667%;
}
.col-xs-offset-4 {
margin-left: 33.33333333%;
}
.col-xs-offset-3 {
margin-left: 25%;
}
.col-xs-offset-2 {
margin-left: 16.66666667%;
}
.col-xs-offset-1 {
margin-left: 8.33333333%;
}
.col-xs-offset-0 {
margin-left: 0%;
}
@media (min-width: $screen-sm) {
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
float: left;
}
.col-sm-12 {
width: 100%;
}
.col-sm-11 {
width: 91.66666667%;
}
.col-sm-10 {
width: 83.33333333%;
}
.col-sm-9 {
width: 75%;
}
.col-sm-8 {
width: 66.66666667%;
}
.col-sm-7 {
width: 58.33333333%;
}
.col-sm-6 {
width: 50%;
}
.col-sm-5 {
width: 41.66666667%;
}
.col-sm-4 {
width: 33.33333333%;
}
.col-sm-3 {
width: 25%;
}
.col-sm-2 {
width: 16.66666667%;
}
.col-sm-1 {
width: 8.33333333%;
}
.col-sm-pull-12 {
right: 100%;
}
.col-sm-pull-11 {
right: 91.66666667%;
}
.col-sm-pull-10 {
right: 83.33333333%;
}
.col-sm-pull-9 {
right: 75%;
}
.col-sm-pull-8 {
right: 66.66666667%;
}
.col-sm-pull-7 {
right: 58.33333333%;
}
.col-sm-pull-6 {
right: 50%;
}
.col-sm-pull-5 {
right: 41.66666667%;
}
.col-sm-pull-4 {
right: 33.33333333%;
}
.col-sm-pull-3 {
right: 25%;
}
.col-sm-pull-2 {
right: 16.66666667%;
}
.col-sm-pull-1 {
right: 8.33333333%;
}
.col-sm-pull-0 {
right: auto;
}
.col-sm-push-12 {
left: 100%;
}
.col-sm-push-11 {
left: 91.66666667%;
}
.col-sm-push-10 {
left: 83.33333333%;
}
.col-sm-push-9 {
left: 75%;
}
.col-sm-push-8 {
left: 66.66666667%;
}
.col-sm-push-7 {
left: 58.33333333%;
}
.col-sm-push-6 {
left: 50%;
}
.col-sm-push-5 {
left: 41.66666667%;
}
.col-sm-push-4 {
left: 33.33333333%;
}
.col-sm-push-3 {
left: 25%;
}
.col-sm-push-2 {
left: 16.66666667%;
}
.col-sm-push-1 {
left: 8.33333333%;
}
.col-sm-push-0 {
left: auto;
}
.col-sm-offset-12 {
margin-left: 100%;
}
.col-sm-offset-11 {
margin-left: 91.66666667%;
}
.col-sm-offset-10 {
margin-left: 83.33333333%;
}
.col-sm-offset-9 {
margin-left: 75%;
}
.col-sm-offset-8 {
margin-left: 66.66666667%;
}
.col-sm-offset-7 {
margin-left: 58.33333333%;
}
.col-sm-offset-6 {
margin-left: 50%;
}
.col-sm-offset-5 {
margin-left: 41.66666667%;
}
.col-sm-offset-4 {
margin-left: 33.33333333%;
}
.col-sm-offset-3 {
margin-left: 25%;
}
.col-sm-offset-2 {
margin-left: 16.66666667%;
}
.col-sm-offset-1 {
margin-left: 8.33333333%;
}
.col-sm-offset-0 {
margin-left: 0%;
}
}
@media (min-width: $screen-md) {
.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
float: left;
}
.col-md-12 {
width: 100%;
}
.col-md-11 {
width: 91.66666667%;
}
.col-md-10 {
width: 83.33333333%;
}
.col-md-9 {
width: 75%;
}
.col-md-8 {
width: 66.66666667%;
}
.col-md-7 {
width: 58.33333333%;
}
.col-md-6 {
width: 50%;
}
.col-md-5 {
width: 41.66666667%;
}
.col-md-4 {
width: 33.33333333%;
}
.col-md-3 {
width: 25%;
}
.col-md-2 {
width: 16.66666667%;
}
.col-md-1 {
width: 8.33333333%;
}
.col-md-pull-12 {
right: 100%;
}
.col-md-pull-11 {
right: 91.66666667%;
}
.col-md-pull-10 {
right: 83.33333333%;
}
.col-md-pull-9 {
right: 75%;
}
.col-md-pull-8 {
right: 66.66666667%;
}
.col-md-pull-7 {
right: 58.33333333%;
}
.col-md-pull-6 {
right: 50%;
}
.col-md-pull-5 {
right: 41.66666667%;
}
.col-md-pull-4 {
right: 33.33333333%;
}
.col-md-pull-3 {
right: 25%;
}
.col-md-pull-2 {
right: 16.66666667%;
}
.col-md-pull-1 {
right: 8.33333333%;
}
.col-md-pull-0 {
right: auto;
}
.col-md-push-12 {
left: 100%;
}
.col-md-push-11 {
left: 91.66666667%;
}
.col-md-push-10 {
left: 83.33333333%;
}
.col-md-push-9 {
left: 75%;
}
.col-md-push-8 {
left: 66.66666667%;
}
.col-md-push-7 {
left: 58.33333333%;
}
.col-md-push-6 {
left: 50%;
}
.col-md-push-5 {
left: 41.66666667%;
}
.col-md-push-4 {
left: 33.33333333%;
}
.col-md-push-3 {
left: 25%;
}
.col-md-push-2 {
left: 16.66666667%;
}
.col-md-push-1 {
left: 8.33333333%;
}
.col-md-push-0 {
left: auto;
}
.col-md-offset-12 {
margin-left: 100%;
}
.col-md-offset-11 {
margin-left: 91.66666667%;
}
.col-md-offset-10 {
margin-left: 83.33333333%;
}
.col-md-offset-9 {
margin-left: 75%;
}
.col-md-offset-8 {
margin-left: 66.66666667%;
}
.col-md-offset-7 {
margin-left: 58.33333333%;
}
.col-md-offset-6 {
margin-left: 50%;
}
.col-md-offset-5 {
margin-left: 41.66666667%;
}
.col-md-offset-4 {
margin-left: 33.33333333%;
}
.col-md-offset-3 {
margin-left: 25%;
}
.col-md-offset-2 {
margin-left: 16.66666667%;
}
.col-md-offset-1 {
margin-left: 8.33333333%;
}
.col-md-offset-0 {
margin-left: 0%;
}
}
@media (min-width: $screen-lg) {
.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
float: left;
}
.col-lg-12 {
width: 100%;
}
.col-lg-11 {
width: 91.66666667%;
}
.col-lg-10 {
width: 83.33333333%;
}
.col-lg-9 {
width: 75%;
}
.col-lg-8 {
width: 66.66666667%;
}
.col-lg-7 {
width: 58.33333333%;
}
.col-lg-6 {
width: 50%;
}
.col-lg-5 {
width: 41.66666667%;
}
.col-lg-4 {
width: 33.33333333%;
}
.col-lg-3 {
width: 25%;
}
.col-lg-2 {
width: 16.66666667%;
}
.col-lg-1 {
width: 8.33333333%;
}
.col-lg-pull-12 {
right: 100%;
}
.col-lg-pull-11 {
right: 91.66666667%;
}
.col-lg-pull-10 {
right: 83.33333333%;
}
.col-lg-pull-9 {
right: 75%;
}
.col-lg-pull-8 {
right: 66.66666667%;
}
.col-lg-pull-7 {
right: 58.33333333%;
}
.col-lg-pull-6 {
right: 50%;
}
.col-lg-pull-5 {
right: 41.66666667%;
}
.col-lg-pull-4 {
right: 33.33333333%;
}
.col-lg-pull-3 {
right: 25%;
}
.col-lg-pull-2 {
right: 16.66666667%;
}
.col-lg-pull-1 {
right: 8.33333333%;
}
.col-lg-pull-0 {
right: auto;
}
.col-lg-push-12 {
left: 100%;
}
.col-lg-push-11 {
left: 91.66666667%;
}
.col-lg-push-10 {
left: 83.33333333%;
}
.col-lg-push-9 {
left: 75%;
}
.col-lg-push-8 {
left: 66.66666667%;
}
.col-lg-push-7 {
left: 58.33333333%;
}
.col-lg-push-6 {
left: 50%;
}
.col-lg-push-5 {
left: 41.66666667%;
}
.col-lg-push-4 {
left: 33.33333333%;
}
.col-lg-push-3 {
left: 25%;
}
.col-lg-push-2 {
left: 16.66666667%;
}
.col-lg-push-1 {
left: 8.33333333%;
}
.col-lg-push-0 {
left: auto;
}
.col-lg-offset-12 {
margin-left: 100%;
}
.col-lg-offset-11 {
margin-left: 91.66666667%;
}
.col-lg-offset-10 {
margin-left: 83.33333333%;
}
.col-lg-offset-9 {
margin-left: 75%;
}
.col-lg-offset-8 {
margin-left: 66.66666667%;
}
.col-lg-offset-7 {
margin-left: 58.33333333%;
}
.col-lg-offset-6 {
margin-left: 50%;
}
.col-lg-offset-5 {
margin-left: 41.66666667%;
}
.col-lg-offset-4 {
margin-left: 33.33333333%;
}
.col-lg-offset-3 {
margin-left: 25%;
}
.col-lg-offset-2 {
margin-left: 16.66666667%;
}
.col-lg-offset-1 {
margin-left: 8.33333333%;
}
.col-lg-offset-0 {
margin-left: 0%;
}
}
.clearfix:before,
.clearfix:after,
.container:before,
.container:after,
.container-fluid:before,
.container-fluid:after,
.row:before,
.row:after {
content: " ";
display: table;
}
.clearfix:after,
.container:after,
.container-fluid:after,
.row:after {
clear: both;
}
.center-block {
display: block;
margin-left: auto;
margin-right: auto;
}

View File

@ -2,21 +2,31 @@
* Includes the main navigation header and the faded toolbar.
*/
header .grid {
grid-template-columns: auto min-content auto;
}
@include smaller-than($l) {
header .grid {
grid-template-columns: 1fr;
grid-row-gap: 0;
}
}
header {
position: relative;
display: block;
z-index: 2;
z-index: 6;
top: 0;
background-color: $primary-dark;
color: #fff;
fill: #fff;
.padded {
padding: $-m;
}
border-bottom: 1px solid #DDD;
box-shadow: $bs-card;
padding: $-xxs 0;
.links {
display: inline-block;
vertical-align: top;
margin-left: $-m;
}
.links a {
display: inline-block;
@ -28,15 +38,6 @@ header {
padding-left: $-m;
padding-right: 0;
}
@include smaller-than($screen-md) {
.links a {
padding-left: $-s;
padding-right: $-s;
}
.dropdown-container {
padding-left: $-s;
}
}
.avatar, .user-name {
display: inline-block;
}
@ -63,27 +64,17 @@ header {
padding-top: 4px;
font-size: 18px;
}
@include smaller-than($screen-md) {
@include between($l, $xl) {
padding-left: $-xs;
.name {
display: none;
}
}
}
@include smaller-than($screen-sm) {
text-align: center;
.float.right {
float: none;
}
.links a {
padding: $-s;
}
.user-name {
padding-top: $-s;
}
}
}
.header-search {
display: inline-block;
}
@ -92,13 +83,16 @@ header .search-box {
margin-top: 10px;
input {
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 40px;
color: #EEE;
z-index: 2;
padding-left: 40px;
}
button {
fill: #EEE;
z-index: 1;
left: 16px;
svg {
margin-right: 0;
}
@ -115,20 +109,11 @@ header .search-box {
:-moz-placeholder { /* Firefox 18- */
color: #DDD;
}
@include smaller-than($screen-lg) {
max-width: 250px;
}
@include smaller-than($l) {
@include between($l, $xl) {
max-width: 200px;
}
}
@include smaller-than($s) {
.header-search {
display: block;
}
}
.logo {
display: inline-block;
&:hover {
@ -151,10 +136,184 @@ header .search-box {
height: 43px;
}
.breadcrumbs span.sep {
color: #aaa;
.mobile-menu-toggle {
color: #FFF;
fill: #FFF;
font-size: 2em;
border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 4px;
padding: 0 $-xs;
position: absolute;
right: $-m;
top: 13px;
line-height: 1;
cursor: pointer;
user-select: none;
svg {
margin: 0;
bottom: -2px;
}
}
@include smaller-than($l) {
header .header-links {
display: none;
background-color: #FFF;
z-index: 10;
right: $-m;
border-radius: 4px;
overflow: hidden;
position: absolute;
box-shadow: $bs-hover;
margin-top: -$-xs;
&.show {
display: block;
}
}
header .links a, header .dropdown-container ul li a {
text-align: left;
display: block;
padding: $-s $-m;
color: $text-dark;
fill: $text-dark;
svg {
margin-right: $-s;
}
&:hover {
background-color: #EEE;
color: #444;
fill: #444;
text-decoration: none;
}
}
header .dropdown-container {
display: block;
padding-left: 0;
}
header .links {
display: block;
}
header .dropdown-container ul {
display: block !important;
position: relative;
background-color: transparent;
border: 0;
padding: 0;
margin: 0;
box-shadow: none;
}
}
.tri-layout-mobile-tabs {
position: sticky;
top: 0;
z-index: 5;
background-color: #FFF;
border-bottom: 1px solid #DDD;
box-shadow: $bs-card;
}
.tri-layout-mobile-tab {
text-align: center;
border-bottom: 3px solid #BBB;
&:first-child {
border-right: 1px solid #DDD;
}
&.active {
border-bottom-color: currentColor;
}
}
.breadcrumbs {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
opacity: 0.7;
.icon-list-item {
width: auto;
padding-top: $-xs;
padding-bottom: $-xs;
}
.separator {
display: inline-block;
fill: #aaa;
font-size: 1.6em;
line-height: 0.8;
margin: -2px 0 0;
}
&:hover {
opacity: 1;
}
}
@include smaller-than($l) {
.breadcrumbs .icon-list-item {
padding: $-xs;
> span + span {
display: none;
}
> span:first-child {
margin-right: 0;
}
}
}
.breadcrumb-listing {
position: relative;
.breadcrumb-listing-toggle {
padding: 6px;
border: 1px solid transparent;
border-radius: 4px;
&:hover {
border-color: #DDD;
}
}
.svg-icon {
margin-right: 0;
}
}
.breadcrumb-listing-dropdown {
box-shadow: $bs-med;
overflow: hidden;
min-height: 100px;
width: 240px;
display: none;
position: absolute;
z-index: 80;
right: -$-m;
.breadcrumb-listing-search .svg-icon {
position: absolute;
left: $-s;
top: 11px;
fill: #888;
pointer-events: none;
}
.breadcrumb-listing-entity-list {
max-height: 400px;
overflow-y: scroll;
text-align: left;
}
input {
padding-left: $-xl;
border-radius: 0;
border: 0;
border-bottom: 1px solid #DDD;
}
}
@include smaller-than($m) {
.breadcrumb-listing-dropdown {
position: fixed;
right: auto;
left: $-m;
}
.breadcrumb-listing-dropdown .breadcrumb-listing-entity-list {
max-height: 240px;
}
}
.faded {
a, button, span, span > div {
color: #666;
@ -175,20 +334,9 @@ header .search-box {
padding: $-s;
}
.faded-small {
color: #000;
fill: #000;
font-size: 0.9em;
background-color: $primary-faded;
}
.toolbar-container {
background-color: #FFF;
}
.breadcrumbs .text-button, .action-buttons .text-button {
.action-buttons .text-button {
display: inline-block;
padding: $-s;
padding: $-xs $-s;
&:last-child {
padding-right: 0;
}
@ -217,28 +365,12 @@ header .search-box {
}
@include smaller-than($m) {
.breadcrumbs .text-button, .action-buttons .text-button {
.action-buttons .text-button {
padding: $-xs $-xs;
}
.action-buttons .dropdown-container:last-child a {
padding-left: $-xs;
}
.breadcrumbs .text-button {
font-size: 0;
}
.breadcrumbs .text-button svg {
font-size: $fs-m;
}
.breadcrumbs a i {
font-size: $fs-m;
padding-right: 0;
}
.breadcrumbs span.sep {
padding: 0 $-xxs;
}
.toolbar .col-xs-1:first-child {
padding-right: 0;
}
}
.nav-tabs {
@ -253,7 +385,4 @@ header .search-box {
border-bottom: 2px solid $primary;
}
}
}
.faded-small .nav-tabs a {
padding: $-s $-m;
}

View File

@ -3,27 +3,18 @@
}
html {
background-color: #FFFFFF;
height: 100%;
overflow-y: scroll;
background-color: #F2F2F2;
&.flexbox {
overflow-y: hidden;
}
&.shaded {
background-color: #F2F2F2;
}
}
body {
font-size: $fs-m;
line-height: 1.6;
color: #616161;
color: #444;
-webkit-font-smoothing: antialiased;
&.shaded {
background-color: #F2F2F2;
}
}
button {
font-size: 100%;
background-color: #F2F2F2;
}

View File

@ -0,0 +1,312 @@
/**
* Generic content container
*/
.container {
max-width: $xxl;
margin-left: auto;
margin-right: auto;
padding-left: $-m;
padding-right: $-m;
&.small {
max-width: 840px;
}
&.very-small {
max-width: 480px;
}
}
/**
* Core grid layout system
*/
.grid {
display: grid;
grid-column-gap: $-l;
grid-row-gap: $-l;
&.half {
grid-template-columns: 1fr 1fr;
}
&.third {
grid-template-columns: 1fr 1fr 1fr;
}
&.left-focus {
grid-template-columns: 2fr 1fr;
}
&.right-focus {
grid-template-columns: 1fr 3fr;
}
&.gap-y-xs {
grid-row-gap: $-xs;
}
&.gap-xl {
grid-column-gap: $-xl;
grid-row-gap: $-xl;
}
&.gap-xxl {
grid-column-gap: $-xxl;
grid-row-gap: $-xxl;
}
&.v-center {
align-items: center;
}
&.no-gap {
grid-row-gap: 0;
grid-column-gap: 0;
}
&.no-row-gap {
grid-row-gap: 0;
}
}
@include smaller-than($m) {
.grid.third {
grid-template-columns: 1fr 1fr;
}
.grid.half:not(.no-break), .grid.left-focus:not(.no-break), .grid.right-focus:not(.no-break) {
grid-template-columns: 1fr;
}
.grid.half.collapse-xs {
grid-template-columns: 1fr 1fr;
}
.grid.gap-xl {
grid-column-gap: $-m;
grid-row-gap: $-m;
}
.grid.right-focus.reverse-collapse > *:nth-child(2) {
order: 0;
}
.grid.right-focus.reverse-collapse > *:nth-child(1) {
order: 1;
}
}
@include smaller-than($s) {
.grid.third {
grid-template-columns: 1fr;
}
}
@include smaller-than($xs) {
.grid.half.collapse-xs {
grid-template-columns: 1fr;
}
}
/**
* Flexbox layout system
*/
body.flexbox {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%;
min-height: 100%;
max-height: 100%;
overflow: hidden;
#content {
flex: 1;
display: flex;
min-height: 0;
}
}
.flex-fill {
display: flex;
align-items: stretch;
min-height: 0;
max-width: 100%;
position: relative;
}
.flex {
min-height: 0;
flex: 1;
}
/**
* Display and float utilities
*/
.block {
display: block;
position: relative;
}
.inline {
display: inline;
}
.block.inline {
display: inline-block;
}
.float {
float: left;
&.right {
float: right;
}
}
/**
* Visibility
*/
@each $sizeLetter, $size in $screen-sizes {
@include smaller-than($size) {
.hide-under-#{$sizeLetter} {
display: none !important;
}
}
@include larger-than($size) {
.hide-over-#{$sizeLetter} {
display: none !important;
}
}
}
/**
* Inline content columns
*/
.dual-column-content {
columns: 2;
}
@include smaller-than($m) {
.dual-column-content {
columns: 1;
}
}
/**
* Fixes
*/
.clearfix:before,
.clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}
/**
* View Layouts
*/
.tri-layout-container {
display: grid;
margin-left: $-xl;
margin-right: $-xl;
grid-template-columns: 1fr 4fr 1fr;
grid-template-areas: "a b c";
grid-column-gap: $-xxl;
.tri-layout-right {
grid-area: c;
min-width: 0;
}
.tri-layout-left {
grid-area: a;
min-width: 0;
}
.tri-layout-middle {
grid-area: b;
padding-top: $-m;
}
}
@include smaller-than($xxl) {
.tri-layout-container {
grid-template-areas: "c b b"
"a b b";
grid-template-columns: 1fr 3fr;
grid-template-rows: max-content min-content;
padding-right: $-l;
}
}
@include larger-than($xxl) {
.tri-layout-left-contents, .tri-layout-right-contents {
padding: $-m;
position: sticky;
top: $-m;
max-height: 100vh;
min-height: 50vh;
overflow-y: scroll;
overflow-x: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
.tri-layout-middle-contents {
max-width: 940px;
margin: 0 auto;
}
}
@include smaller-than($l) {
.tri-layout-container {
grid-template-areas: none;
grid-template-columns: 1fr;
grid-column-gap: 0;
padding-right: $-xs;
padding-left: $-xs;
.tri-layout-left-contents, .tri-layout-right-contents {
padding-left: $-m;
padding-right: $-m;
}
.tri-layout-right-contents > div, .tri-layout-left-contents > div {
opacity: 0.6;
z-index: 0;
}
.tri-layout-left > *, .tri-layout-right > * {
display: none;
pointer-events: none;
}
.tri-layout-left, .tri-layout-right {
grid-area: none;
grid-column: 1/1;
grid-row: 1;
padding-top: 0 !important;
}
.tri-layout-middle {
grid-area: none;
grid-row: 3;
grid-column: 1/1;
z-index: 1;
overflow: hidden;
transition: transform ease-in-out 240ms;
}
.tri-layout-left {
grid-row: 2;
}
&.show-info {
overflow: hidden;
.tri-layout-middle {
display: none;
}
.tri-layout-right > *, .tri-layout-left > * {
display: block;
pointer-events: auto;
}
}
}
}
@include larger-than($l) {
.tri-layout-mobile-tabs {
display: none;
}
}
@include smaller-than($m) {
.tri-layout-container {
margin-left: 0;
margin-right: 0;
}
}
.tri-layout-left-contents > div, .tri-layout-right-contents > div {
opacity: 0.6;
transition: opacity ease-in-out 120ms;
&:hover {
opacity: 1;
}
}

View File

@ -1,182 +1,217 @@
.page-list {
h4 {
margin: $-l 0 $-xs 0;
font-size: 1.666em;
.book-contents .entity-list-item {
.icon {
width: 4px;
border-radius: 1px;
justify-self: stretch;
align-self: stretch;
height: auto;
margin-right: $-l;
}
a.chapter {
color: $color-chapter;
.icon:after {
opacity: 0.5;
}
.inset-list {
.icon svg {
display: none;
overflow: hidden;
}
h5 {
display: block;
margin: $-s 0 0 0;
border-left: 5px solid $color-page;
padding: $-xs 0 $-xs $-m;
font-size: 1.1em;
font-weight: normal;
&.draft {
border-left-color: $color-page-draft;
}
p {
margin-bottom: 0;
}
.entity-list-item {
margin-bottom: $-m;
}
hr {
margin-top: 0;
}
.page, .chapter, .book {
padding-left: $-l;
}
.page {
border-left: 5px solid $color-page;
}
.page.draft {
border-left: 5px solid $color-page-draft;
.text-page {
color: $color-page-draft;
fill: $color-page-draft;
}
}
.chapter {
border-left: 5px solid $color-chapter;
}
.book {
border-left: 5px solid $color-book;
}
.meta {
margin-top: -$-m;
font-size: 0.95em;
}
.meta span {
margin-right: $-s;
.inner-page {
padding-top: 0;
padding-bottom: 0;
}
}
@include smaller-than($s) {
.page-list h4 {
font-size: 1.333em;
.entity-list-item + .chapter-expansion {
display: flex;
padding: 0 $-m $-m $-m;
align-items: center;
border: 0;
width: 100%;
position: relative;
> .icon {
width: 4px;
height: auto;
border-radius: 0 0 1px 1px;
align-self: stretch;
flex-shrink: 0;
&:before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1px;
background-color: currentColor;
content: '';
opacity: 0.5;
}
&:after {
opacity: 0.5;
}
}
.icon svg {
display: none;
}
> .content {
flex: 1;
}
.chapter-expansion-toggle {
border-radius: 0 4px 4px 0;
padding: $-xs $-m;
}
.chapter-expansion-toggle:hover {
background-color: rgba(0, 0, 0, 0.06);
}
}
.entity-list-item.has-children {
padding-bottom: 0;
> .icon {
border-radius: 4px 4px 0 0;
}
}
.inset-list {
display: none;
.entity-list-item-name {
font-size: 1rem;
}
.entity-list-item-children {
padding-top: 0;
padding-bottom: 0;
}
}
.sidebar-page-nav {
$nav-indent: $-s;
$nav-indent: $-m;
list-style: none;
margin: $-s 0 $-m 2px;
border-left: 2px dotted #BBB;
margin: $-s 0 $-m $-xs;
position: relative;
&:after {
content: '';
display: block;
position: absolute;
left: 0;
background-color: rgba(0, 0, 0, 0.2);
width: 2px;
top: 5px;
bottom: 5px;
z-index: 0;
}
li {
padding-left: $-s;
margin-bottom: 4px;
font-size: 0.95em;
position: relative;
}
.h1 {
margin-left: -2px;
padding-left: $nav-indent;
}
.h2 {
margin-left: -2px;
padding-left: $nav-indent;
}
.h3 {
margin-left: $nav-indent;
padding-left: $nav-indent * 2;
}
.h4 {
margin-left: $nav-indent*2;
padding-left: $nav-indent * 2.5;
}
.h5 {
margin-left: $nav-indent*3;
padding-left: $nav-indent*3;
}
.h6 {
margin-left: $nav-indent*4;
padding-left: $nav-indent*3.5;
}
.current-heading {
font-weight: bold;
}
li:not(.current-heading) .sidebar-page-nav-bullet {
background-color: #BBB !important;
}
.sidebar-page-nav-bullet {
width: 6px;
height: 6px;
position: absolute;
left: -2px;
top: 30%;
border-radius: 50%;
box-shadow: 0 0 0 6px #F2F2F2;
z-index: 1;
}
}
// Sidebar list
.book-tree {
transition: ease-in-out 240ms;
transition-property: right, border;
}
.book-tree h4 {
padding: $-m $-s 0 $-s;
i {
padding-right: $-s;
}
}
.book-tree .sidebar-page-list {
.book-tree .sidebar-page-list {
list-style: none;
margin: $-xs 0 0;
margin: $-xs -$-s 0 -$-s;
padding-left: 0;
border-left: 5px solid $color-book;
li a {
padding-right: 0;
position: relative;
&:after, .sub-menu:after {
content: '';
display: block;
border-bottom: none;
padding: $-xs 0 $-xs $-s;
&:hover {
text-decoration: none;
}
}
li a i {
padding-right: $-xs + 2px;
}
li, a {
display: block;
}
a.bold {
color: #EEE !important;
fill: #EEE !important;
position: absolute;
left: $-m;
top: 1rem;
bottom: 1rem;
border-left: 4px solid rgba(0, 0, 0, 0.1);
z-index: 0;
}
ul {
list-style: none;
padding-left: 1rem;
padding-right: 0;
}
.entity-list-item {
padding-top: $-xxs;
padding-bottom: $-xxs;
.content {
padding-top: $-xs;
padding-bottom: $-xs;
max-width: calc(100% - 20px);
}
}
.entity-list-item.no-hover {
margin-top: -$-xs;
padding-right: 0;
}
.entity-list-item-name {
font-size: 1em;
margin: 0;
}
.book {
color: $color-book !important;
fill: $color-book !important;
&.selected {
background-color: rgba($color-book, 0.29);
}
}
.chapter {
color: $color-chapter !important;
fill: $color-chapter !important;
&.selected {
background-color: rgba($color-chapter, 0.12);
}
}
.page {
color: $color-page !important;
fill: $color-page !important;
border-bottom: none;
&.selected {
background-color: rgba($color-page, 0.1);
}
.chapter-child-menu {
font-size: .8rem;
margin-top: -.2rem;
margin-left: -1rem;
}
[chapter-toggle] {
padding-left: $-s;
padding-left: .7rem;
padding-bottom: .2rem;
}
.list-item-chapter {
border-left: 5px solid $color-chapter;
margin: 10px 10px;
display: block;
.entity-list-item .icon {
z-index: 2;
width: 4px;
height: auto;
align-self: stretch;
flex-shrink: 0;
border-radius: 1px;
opacity: 0.6;
}
.list-item-page {
border-bottom: none;
border-left: 5px solid $color-page;
margin: 10px 10px;
.entity-list-item .icon:after {
opacity: 1;
}
.list-item-page.draft {
border-left: 5px solid $color-page-draft;
.entity-list-item .icon svg {
display: none;
}
.page.draft .page, .list-item-page.draft a.page {
color: $color-page-draft !important;
fill: $color-page-draft !important;
}
.sub-menu {
}
.chapter-child-menu {
ul.sub-menu {
display: none;
padding-left: 0;
position: relative;
}
[chapter-toggle].open + .sub-menu {
display: block;
@ -186,26 +221,41 @@
// Sortable Lists
.sortable-page-list, .sortable-page-list ul {
list-style: none;
background-color: #FFF;
}
.sort-box {
margin-bottom: $-m;
padding: 0 $-l 0 $-l;
border-left: 4px solid $color-book;
border: 2px solid rgba($color-book, 0.6);
padding: $-m $-xl;
border-radius: 4px;
}
.sort-box-options {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.sort-box-options .button {
margin-left: 0;
}
.sortable-page-list {
margin-left: 0;
padding: 0;
.entity-list-item > span:first-child {
align-self: flex-start;
}
.entity-list-item > div {
display: block;
flex: 1;
}
> ul {
margin-left: 0;
}
ul {
margin-bottom: 0;
margin-bottom: $-m;
margin-top: 0;
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.1);
padding-left: $-m;
}
li {
border: 1px solid #DDD;
padding: $-xs $-s;
margin-top: -1px;
min-height: 38px;
&.text-chapter {
@ -228,18 +278,26 @@
.activity-list-item {
padding: $-s 0;
display: grid;
grid-template-columns: min-content 1fr;
grid-column-gap: $-m;
color: #888;
fill: #888;
border-bottom: 1px solid #EEE;
font-size: 0.9em;
.left {
float: left;
}
.left + .right {
margin-left: 30px + $-s;
}
&:last-of-type {
border-bottom: 0;
}
.card .activity-list-item {
padding: $-s $-m;
}
.user-list-item {
display: inline-grid;
padding: $-s;
grid-template-columns: min-content 1fr;
grid-column-gap: $-m;
font-size: 0.9em;
align-items: center;
> div:first-child {
line-height: 0;
}
}
@ -280,10 +338,8 @@ ul.pagination {
margin: 0;
}
.entity-list {
> div {
padding: $-m 0;
}
.entity-list, .icon-list {
margin: 0 (-$-m);
h4 {
margin: 0;
}
@ -302,15 +358,125 @@ ul.pagination {
color: $color-page-draft;
fill: $color-page-draft;
}
> .dropdown-container {
display: block;
}
}
.card .entity-list-item, .card .activity-list-item {
padding-left: $-m;
padding-right: $-m;
.icon-list hr {
margin: $-s $-m;
max-width: 140px;
opacity: 0.25;
height: 1.1px;
}
.icon-list hr + hr, .icon-list hr:first-child, .icon-list hr:last-child {
display: none;
}
.entity-list-item, .icon-list-item {
padding: $-s $-m;
display: flex;
align-items: center;
background-color: transparent;
border: 0;
width: 100%;
position: relative;
word-break: break-word;
h4 a {
color: #666;
}
> span:first-child {
margin-right: $-m;
flex-basis: 1.88em;
flex: none;
}
> span:last-child {
flex: 1;
text-align: left;
}
&:not(.no-hover) {
cursor: pointer;
}
&:not(.no-hover):hover {
text-decoration: none;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
&.outline-hover {
border: 1px solid transparent;
}
&.outline-hover:hover {
background-color: transparent;
border-color: rgba(0, 0, 0, 0.1);
}
}
.entity-list-item-path-sep {
display: inline-block;
vertical-align: top;
position: relative;
top: 1px;
svg {
margin-right: 0;
}
}
.card .entity-list-item:not(.no-hover):hover {
background-color: #F2F2F2;
}
.card .entity-list-item .entity-list-item:hover {
background-color: #EEEEEE;
}
.entity-list-item-children {
padding: $-m;
> div {
overflow: hidden;
padding: $-xs 0;
margin-top: -$-xs;
}
.entity-chip {
text-overflow: ellipsis;
height: 2.5em;
overflow: hidden;
text-align: left;
display: block;
white-space: nowrap;
}
}
.entity-list-item-image {
align-self: stretch;
width: 140px;
flex: none;
background-size: cover;
background-position: 50% 50%;
border-radius: 3px;
position: relative;
margin-right: $-l;
.svg-icon {
color: #FFF;
fill: #FFF;
font-size: 1.66rem;
margin-right: 0;
position: absolute;
bottom: $-xs;
left: $-xs;
}
@include smaller-than($m) {
width: 80px;
}
}
.chapter > .entity-list-item-image {
width: 60px;
}
.entity-list.compact {
font-size: 0.6em;
font-size: 0.6 * $fs-m;
h4, a {
line-height: 1.2;
}
@ -331,6 +497,11 @@ ul.pagination {
hr {
margin: 0;
}
@include smaller-than($m) {
h4 {
font-size: 1.666em;
}
}
}
.dropdown-container {
@ -363,9 +534,8 @@ ul.pagination {
color: #999;
fill: #999;
}
li.padded {
padding: $-xs $-m;
line-height: 1.2;
li.active a {
font-weight: 600;
}
a, button {
display: block;
@ -396,7 +566,10 @@ ul.pagination {
.featured-image-container {
position: relative;
overflow: hidden;
background: #F2F2F2;
min-height: 140px;
background-size: cover;
background-position: 50% 50%;
transition: opacity ease-in-out 240ms;
a {
display: block;
}
@ -405,11 +578,46 @@ ul.pagination {
width: 100%;
max-width: 100%;
height: auto;
transition: all .5s ease-in-out;
}
img:hover {
transform: scale(1.15);
opacity: .5;
}
}
.featured-image-container-wrap {
position: relative;
.svg-icon {
color: #FFF;
fill: #FFF;
font-size: 2rem;
margin-right: 0;
position: absolute;
bottom: 10px;
left: 6px;
}
}
.grid-card:hover .featured-image-container {
opacity: .5;
}
.action-link-list {
//padding: $-s 0;
}
.action-link {
background: transparent;
border: none;
color: currentColor;
padding: $-m 0;
}
.active-link-list {
a {
display: inline-block;
padding: $-s;
}
a:not(.active) {
color: #444;
fill: #444;
}
a:hover {
background-color: rgba(0, 0, 0, 0.05);
border-radius: 3px;
text-decoration: none;
}
}

View File

@ -5,12 +5,6 @@
@mixin larger-than($size) {
@media screen and (min-width: $size) { @content; }
}
@mixin clearfix() {
&:after {
display: block;
content: '';
font-size: 0;
clear: both;
position: relative;
}
@mixin between($min, $max) {
@media screen and (min-width: $min) and (max-width: $max) { @content; }
}

View File

@ -3,12 +3,12 @@
flex-direction: column;
align-items: stretch;
overflow: hidden;
.faded-small {
height: auto;
}
background-color: #FFF;
.edit-area {
flex: 1;
flex-direction: column;
z-index: 1;
}
.mce-tinymce {
@ -20,6 +20,39 @@
}
}
@include smaller-than($l) {
.page-edit-toolbar {
overflow-x: scroll;
overflow-y: visible;
z-index: 4;
}
.page-edit-toolbar .grid.third {
display: block;
white-space: nowrap;
> div {
display: inline-block;
}
}
}
@include smaller-than($l) {
.page-edit-toolbar #save-button {
position: fixed;
z-index: 30;
background-color: #FFF;
border-radius: 50%;
width: 42px;
height: 42px;
font-size: 16px;
right: $-m;
bottom: $-xs;
box-shadow: $bs-med;
span {
display: none;
}
}
}
.draft-notification {
pointer-events: none;
transform: scale(0);
@ -38,11 +71,7 @@
width: 100%;
max-width: 840px;
margin: 0 auto;
margin-top: $-xxl;
overflow-wrap: break-word;
&.flex {
margin-top: $-m;
}
.align-left {
text-align: left;
}
@ -248,13 +277,6 @@
min-height: 0px;
overflow-y: scroll;
}
div[toolbox-tab-content] .padded {
flex: 1;
padding-top: 0;
}
div[toolbox-tab-content] .padded.files {
overflow-x: hidden;
}
h4 {
font-size: 24px;
margin: $-m 0 0 0;
@ -349,16 +371,10 @@
}
}
.comments-container {
width: 100%;
border-top: 1px solid #DDD;
margin-top: $-xl;
margin-bottom: $-m;
h5 {
color: #888;
font-weight: normal;
margin-top: 0.5em;
}
.comments-container h5 {
color: #888;
font-weight: normal;
margin-top: 0.5em;
}
.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
@ -370,4 +386,60 @@
.mce-open {
display: none;
}
}
.entity-list-item > span:first-child, .icon-list-item > span:first-child, .chapter-expansion > .icon {
font-size: 0.8rem;
width: 1.88em;
height: 1.88em;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
border-radius: 1em;
position: relative;
overflow: hidden;
svg {
margin: 0;
bottom: 0;
}
&:after {
content: '';
position: absolute;
background-color: currentColor;
opacity: 0.2;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
}
.entity-chip {
display: inline-block;
align-items: center;
justify-content: center;
text-align: center;
font-size: 0.9em;
border-radius: 3px;
position: relative;
overflow: hidden;
padding: $-xs $-s;
fill: currentColor;
opacity: 0.85;
transition: opacity ease-in-out 120ms;
&:after {
content: '';
position: absolute;
background-color: currentColor;
opacity: 0.15;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
&:hover {
text-decoration: none;
opacity: 1;
}
}

View File

@ -0,0 +1,32 @@
// Here we generate spacing utility classes for our sizes for all box sides and axis.
// These will output to classes like .px-m (Padding on x-axis, medium size) or .mr-l (Margin right, large size)
@mixin spacing($prop, $propLetter) {
@each $sizeLetter, $size in $spacing {
.#{$propLetter}-#{$sizeLetter} {
#{$prop}: $size !important;
}
.#{$propLetter}x-#{$sizeLetter} {
#{$prop}-left: $size !important;
#{$prop}-right: $size !important;
}
.#{$propLetter}y-#{$sizeLetter} {
#{$prop}-top: $size !important;
#{$prop}-bottom: $size !important;
}
.#{$propLetter}t-#{$sizeLetter} {
#{$prop}-top: $size !important;
}
.#{$propLetter}r-#{$sizeLetter} {
#{$prop}-right: $size !important;
}
.#{$propLetter}b-#{$sizeLetter} {
#{$prop}-bottom: $size !important;
}
.#{$propLetter}l-#{$sizeLetter} {
#{$prop}-left: $size !important;
}
}
}
@include spacing('margin', 'm')
@include spacing('padding', 'p')

View File

@ -19,13 +19,13 @@ table {
table.table {
width: 100%;
tr {
border-bottom: 1px solid #DDD;
tr td, tr th {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
th, td {
text-align: left;
border: none;
padding: $-xs $-xs;
padding: $-s $-s;
vertical-align: middle;
margin: 0;
}
@ -44,6 +44,9 @@ table.table {
td.actions {
overflow: visible;
}
a {
display: inline-block;
}
}
table.no-style {

View File

@ -42,7 +42,7 @@ h1, h2, h3, h4, h5, h6 {
font-weight: 400;
position: relative;
display: block;
color: #555;
color: #222;
.subheader {
font-size: 0.5em;
line-height: 1em;
@ -79,10 +79,18 @@ h5, h6 {
}
}
.list-heading {
font-size: 2rem;
}
h2.list-heading {
font-size: 1.333rem;
}
/*
* Link styling
*/
a, .link {
a {
color: $primary;
cursor: pointer;
text-decoration: none;
@ -141,11 +149,8 @@ em, i, .italic {
}
small, p.small, span.small, .text-small {
font-size: 0.8em;
color: lighten($text-dark, 20%);
small, p.small, span.small, .text-small {
font-size: 1em;
}
font-size: 0.75rem;
color: lighten($text-dark, 10%);
}
sup, .superscript {
@ -233,106 +238,6 @@ pre code {
display: block;
line-height: 1.6;
}
/*
* Text colors
*/
p.pos, p .pos, span.pos, .text-pos {
color: $positive;
fill: $positive;
&:hover {
color: $positive;
fill: $positive;
}
}
p.neg, p .neg, span.neg, .text-neg {
color: $negative;
fill: $negative;
&:hover {
color: $negative;
fill: $negative;
}
}
p.muted, p .muted, span.muted, .text-muted {
color: lighten($text-dark, 26%);
fill: lighten($text-dark, 26%);
&.small, .small {
color: lighten($text-dark, 32%);
fill: lighten($text-dark, 32%);
}
}
p.primary, p .primary, span.primary, .text-primary {
color: $primary;
fill: $primary;
&:hover {
color: $primary;
fill: $primary;
}
}
p.secondary, p .secondary, span.secondary, .text-secondary {
color: $secondary;
fill: $secondary;
&:hover {
color: $secondary;
fill: $secondary;
}
}
.text-bookshelf {
color: $color-bookshelf;
fill: $color-bookshelf;
&:hover {
color: $color-bookshelf;
fill: $color-bookshelf;
}
}
.text-book {
color: $color-book;
fill: $color-book;
&:hover {
color: $color-book;
fill: $color-book;
}
}
.text-page {
color: $color-page;
fill: $color-page;
&:hover {
color: $color-page;
fill: $color-page;
}
&.draft {
color: $color-page-draft;
fill: $color-page-draft;
}
&.draft:hover {
color: $color-page-draft;
fill: $color-page-draft;
}
}
.text-chapter {
color: $color-chapter;
fill: $color-chapter;
&:hover {
color: $color-chapter;
fill: $color-chapter;
}
}
.faded .text-book:hover {
color: $color-book !important;
fill: $color-book !important;
}
.faded .text-chapter:hover {
color: $color-chapter !important;
fill: $color-chapter !important;
}
.faded .text-page:hover {
color: $color-page !important;
fill: $color-page !important;
}
span.highlight {
//background-color: rgba($primary, 0.2);
@ -435,10 +340,6 @@ span.sep {
/**
* Icons
*/
i {
padding-right: $-xs;
}
.svg-icon {
width: 1em;
height: 1em;
@ -446,5 +347,6 @@ i {
position: relative;
bottom: -0.105em;
margin-right: $-xs;
pointer-events: none;
}

View File

@ -21,6 +21,13 @@
text-align: center;
}
@include smaller-than($l) {
.mce-container-body.mce-flow-layout {
overflow-x: scroll;
white-space: nowrap;
}
}
.edit-area.flex > div > .mce-tinymce.mce-container.mce-panel {
flex: 1 1 auto;
display: flex !important;

View File

@ -1,14 +1,12 @@
// Variables
///////////////
// Sizes
$max-width: 1400px;
// Screen breakpoints
$xxl: 1400px;
$xl: 1100px;
$ipad-width: 1028px; // Is actually 1024 but we go over to ensure functionality.
$l: 1000px;
$m: 800px;
$m: 880px;
$s: 600px;
$xs: 400px;
$xxs: 360px;
@ -16,6 +14,9 @@ $screen-lg: 1200px;
$screen-md: 992px;
$screen-sm: 768px;
// List of screen sizes
$screen-sizes: (('xxs', $xxs), ('xs', $xs), ('s', $s), ('m', $m), ('l', $l), ('xl', $xl));
// Spacing (Margins+Padding)
$-xxxl: 64px;
$-xxl: 48px;
@ -26,6 +27,9 @@ $-s: 12px;
$-xs: 6px;
$-xxs: 3px;
// List of our spacing sizes
$spacing: (('none', 0), ('xxs', $-xxs), ('xs', $-xs), ('s', $-s), ('m', $-m), ('l', $-l), ('xl', $-xl), ('xxl', $-xxl));
// Fonts
$text: -apple-system, BlinkMacSystemFont,
"Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell",
@ -33,8 +37,8 @@ $text: -apple-system, BlinkMacSystemFont,
sans-serif;
$mono: "Lucida Console", "DejaVu Sans Mono", "Ubunto Mono", Monaco, monospace;
$heading: $text;
$fs-m: 15px;
$fs-s: 14px;
$fs-m: 14px;
$fs-s: 12px;
// Colours
$primary: #0288D1;
@ -49,7 +53,7 @@ $primary-faded: rgba(21, 101, 192, 0.15);
// Item Colors
$color-bookshelf: #af5a5a;
$color-book: #009688;
$color-chapter: #ef7c3c;
$color-chapter: #d7804a;
$color-page: $primary;
$color-page-draft: #9A60DA;
@ -60,5 +64,5 @@ $text-light: #EEE;
// Shadows
$bs-light: 0 0 4px 1px #CCC;
$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
$bs-card: 0 1px 3px 1px rgba(76, 76, 76, 0.26), 0 1px 12px 0px rgba(76, 76, 76, 0.2);
$bs-card: 0 1px 6px -1px rgba(0, 0, 0, 0.1);
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);

View File

@ -1,8 +1,9 @@
@import "variables";
@import "mixins";
@import "spacing";
@import "html";
@import "text";
@import "grid";
@import "layout";
@import "blocks";
@import "forms";
@import "tables";
@ -12,6 +13,9 @@
body {
font-family: 'DejaVu Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
background-color: #FFF;
margin: 0;
padding: 0;
}
table {
@ -19,6 +23,10 @@ table {
border-collapse: collapse;
}
.page-content {
overflow: hidden;
}
// Prevent code block overflow on export
pre {
padding-left: 12px;

View File

@ -8,10 +8,6 @@ body {
font-size: 12px;
}
.faded-small {
display: none;
}
.page-content {
margin: 0 auto;
}

View File

@ -1,9 +1,11 @@
@import "reset";
@import "variables";
@import "mixins";
@import "spacing";
@import "html";
@import "text";
@import "grid";
@import "colors";
@import "layout";
@import "blocks";
@import "buttons";
@import "tables";
@ -94,17 +96,6 @@ $loadingSize: 10px;
}
}
// Search results
.search-results > h3 a {
font-size: 0.66em;
color: $primary;
padding-left: $-m;
i {
padding-right: $-s;
}
}
// Back to top link
$btt-size: 40px;
[back-to-top] {
@ -186,22 +177,28 @@ $btt-size: 40px;
overflow-y: scroll;
height: 400px;
background-color: #EEEEEE;
margin-right: 0;
margin-left: 0;
}
.entity-list-item {
background-color: #FFF;
}
.entity-list-item p {
margin-bottom: 0;
}
.entity-list-item.selected {
background-color: rgba(0, 0, 0, 0.15) !important;
}
.loading {
height: 400px;
padding-top: $-l;
}
.entity-list > p {
text-align: center;
padding-top: $-l;
font-size: 1.333em;
}
.entity-list > div {
padding-left: $-m;
padding-right: $-m;
background-color: #FFF;
transition: all ease-in-out 120ms;
cursor: pointer;
.entity-selector-add button {
margin: 0;
display: block;
width: 100%;
border: 0;
border-top: 1px solid #DDD;
}
&.compact {
font-size: 10px;
@ -211,12 +208,6 @@ $btt-size: 40px;
}
}
.entity-list-item.selected {
h3, i, p ,a, span {
color: #EEE;
}
}
.scroll-box {
max-height: 250px;
overflow-y: scroll;
@ -254,3 +245,39 @@ $btt-size: 40px;
height:100%;
z-index: 150;
}
.list-sort-container {
display: inline-block;
form {
display: inline-block;
}
.list-sort {
display: inline-grid;
margin-left: $-s;
grid-template-columns: 120px 40px;
border: 2px solid #DDD;
border-radius: 4px;
}
.list-sort-label {
font-weight: bold;
display: inline-block;
color: #888;
}
.list-sort-type {
text-align: left;
}
.list-sort-type, .list-sort-dir {
padding: $-xs $-s;
cursor: pointer;
}
.list-sort-dir {
border-left: 2px solid #DDD;
fill: #888;
.svg-icon {
transition: transform ease-in-out 120ms;
}
&:hover .svg-icon {
transform: rotate(180deg);
}
}
}

View File

@ -11,6 +11,7 @@ return [
'save' => 'Save',
'continue' => 'Continue',
'select' => 'Select',
'toggle_all' => 'Toggle All',
'more' => 'More',
// Form Labels
@ -23,6 +24,7 @@ return [
// Actions
'actions' => 'Actions',
'view' => 'View',
'view_all' => 'View All',
'create' => 'Create',
'update' => 'Update',
'edit' => 'Edit',
@ -37,6 +39,11 @@ return [
'remove' => 'Remove',
'add' => 'Add',
// Sort Options
'sort_name' => 'Name',
'sort_created_at' => 'Created Date',
'sort_updated_at' => 'Updated Date',
// Misc
'deleted_user' => 'Deleted User',
'no_activity' => 'No activity to show',
@ -53,7 +60,11 @@ return [
'view_profile' => 'View Profile',
'edit_profile' => 'Edit Profile',
// Layout tabs
'tab_info' => 'Info',
'tab_content' => 'Content',
// Email Content
'email_action_help' => 'If youre having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser:',
'email_rights' => 'All rights reserved',
];
];

View File

@ -11,6 +11,7 @@ return [
'recently_updated_pages' => 'Recently Updated Pages',
'recently_created_chapters' => 'Recently Created Chapters',
'recently_created_books' => 'Recently Created Books',
'recently_created_shelves' => 'Recently Created Shelves',
'recently_update' => 'Recently Updated',
'recently_viewed' => 'Recently Viewed',
'recent_activity' => 'Recent Activity',
@ -67,6 +68,7 @@ return [
// Shelves
'shelf' => 'Shelf',
'shelves' => 'Shelves',
'x_shelves' => ':count Shelf|:count Shelves',
'shelves_long' => 'Bookshelves',
'shelves_empty' => 'No shelves have been created',
'shelves_create' => 'Create New Shelf',
@ -117,7 +119,6 @@ return [
'books_permissions_updated' => 'Book Permissions Updated',
'books_empty_contents' => 'No pages or chapters have been created for this book.',
'books_empty_create_page' => 'Create a new page',
'books_empty_or' => 'or',
'books_empty_sort_current_book' => 'Sort the current book',
'books_empty_add_chapter' => 'Add a chapter',
'books_permissions_active' => 'Book Permissions Active',
@ -125,6 +126,11 @@ return [
'books_navigation' => 'Book Navigation',
'books_sort' => 'Sort Book Contents',
'books_sort_named' => 'Sort Book :bookName',
'books_sort_name' => 'Sort by Name',
'books_sort_created' => 'Sort by Created Date',
'books_sort_updated' => 'Sort by Updated Date',
'books_sort_chapters_first' => 'Chapters First',
'books_sort_chapters_last' => 'Chapters Last',
'books_sort_show_other' => 'Show Other Books',
'books_sort_save' => 'Save New Order',
@ -202,6 +208,8 @@ return [
'pages_revisions_created_by' => 'Created By',
'pages_revisions_date' => 'Revision Date',
'pages_revisions_number' => '#',
'pages_revisions_numbered' => 'Revision #:id',
'pages_revisions_numbered_changes' => 'Revision #:id Changes',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Changes',
'pages_revisions_current' => 'Current Version',
@ -267,6 +275,7 @@ 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',
'profile_not_created_shelves' => ':userName has not created any shelves',
// Comments
'comment' => 'Comment',

View File

@ -12,34 +12,44 @@ return [
'settings_save_success' => 'Settings saved',
// App Settings
'app_settings' => 'App Settings',
'app_name' => 'Application name',
'app_name_desc' => 'This name is shown in the header and any emails.',
'app_name_header' => 'Show Application name in header?',
'app_customization' => 'Customization',
'app_features_security' => 'Features & Security',
'app_name' => 'Application Name',
'app_name_desc' => 'This name is shown in the header and in any system-sent emails.',
'app_name_header' => 'Show name in header',
'app_public_access' => 'Public Access',
'app_public_access_desc' => 'Enabling this option will allow visitors, that are not logged-in, to access content in your BookStack instance.',
'app_public_access_desc_guest' => 'Access for public visitors can be controlled through the "Guest" user.',
'app_public_access_toggle' => 'Allow public access',
'app_public_viewing' => 'Allow public viewing?',
'app_secure_images' => 'Enable higher security image uploads?',
'app_secure_images' => 'Higher Security Image Uploads',
'app_secure_images_toggle' => 'Enable higher security image uploads',
'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',
'app_editor' => 'Page editor',
'app_editor' => 'Page Editor',
'app_editor_desc' => 'Select which editor will be used by all users to edit pages.',
'app_custom_html' => 'Custom HTML head content',
'app_custom_html' => 'Custom HTML Head Content',
'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',
'app_logo' => 'Application logo',
'app_logo' => 'Application Logo',
'app_logo_desc' => 'This image should be 43px in height. <br>Large images will be scaled down.',
'app_primary_color' => 'Application primary color',
'app_primary_color' => 'Application Primary Color',
'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.',
'app_homepage' => 'Application Homepage',
'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
'app_homepage_select' => 'Select a page',
'app_disable_comments' => 'Disable comments',
'app_disable_comments_desc' => 'Disable comments across all pages in the application. Existing comments are not shown.',
'app_disable_comments' => 'Disable Comments',
'app_disable_comments_toggle' => 'Disable comments',
'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',
// Registration Settings
'reg_settings' => 'Registration Settings',
'reg_allow' => 'Allow registration?',
'reg_settings' => 'Registration',
'reg_enable' => 'Enable Registration',
'reg_enable_toggle' => 'Enable registration',
'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',
'reg_default_role' => 'Default user role after registration',
'reg_confirm_email' => 'Require email confirmation?',
'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and the below value will be ignored.',
'reg_confirm_restrict_domain' => 'Restrict registration to domain',
'reg_email_confirmation' => 'Email Confirmation',
'reg_email_confirmation_toggle' => 'Require email confirmation',
'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',
'reg_confirm_restrict_domain' => 'Domain Restriction',
'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
@ -91,9 +101,16 @@ return [
'user_profile' => 'User Profile',
'users_add_new' => 'Add New User',
'users_search' => 'Search Users',
'users_details' => 'User Details',
'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',
'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',
'users_role' => 'User Roles',
'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
'users_password' => 'User Password',
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 5 characters long.',
'users_external_auth_id' => 'External Authentication ID',
'users_password_warning' => 'Only fill the below if you would like to change your password:',
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your LDAP system.',
'users_password_warning' => 'Only fill the below if you would like to change your password.',
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
'users_delete' => 'Delete User',
'users_delete_named' => 'Delete user :userName',
@ -104,8 +121,9 @@ return [
'users_edit_profile' => 'Edit Profile',
'users_edit_success' => 'User successfully updated',
'users_avatar' => 'User Avatar',
'users_avatar_desc' => 'This image should be approx 256px square.',
'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',
'users_preferred_language' => 'Preferred Language',
'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',
'users_social_accounts' => 'Social Accounts',
'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.',
'users_social_connect' => 'Connect Account',

View File

@ -1,12 +1,12 @@
<div class="form-group">
<label for="username">{{ trans('auth.username') }}</label>
@include('form/text', ['name' => 'username', 'tabindex' => 1])
@include('form.text', ['name' => 'username', 'tabindex' => 1])
</div>
@if(session('request-email', false) === true)
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
@include('form/text', ['name' => 'email', 'tabindex' => 1])
@include('form.text', ['name' => 'email', 'tabindex' => 1])
<span class="text-neg">
{{ trans('auth.ldap_email_hint') }}
</span>
@ -15,5 +15,5 @@
<div class="form-group">
<label for="password">{{ trans('auth.password') }}</label>
@include('form/password', ['name' => 'password', 'tabindex' => 2])
@include('form.password', ['name' => 'password', 'tabindex' => 2])
</div>

View File

@ -1,10 +1,10 @@
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
@include('form/text', ['name' => 'email', 'tabindex' => 1])
@include('form.text', ['name' => 'email', 'tabindex' => 1])
</div>
<div class="form-group">
<label for="password">{{ trans('auth.password') }}</label>
@include('form/password', ['name' => 'password', 'tabindex' => 2])
<span class="block small"><a href="{{ baseUrl('/password/email') }}">{{ trans('auth.forgot_password') }}</a></span>
@include('form.password', ['name' => 'password', 'tabindex' => 1])
<span class="block small mt-s"><a href="{{ baseUrl('/password/email') }}">{{ trans('auth.forgot_password') }}</a></span>
</div>

View File

@ -1,44 +1,48 @@
@extends('public')
@section('header-buttons')
@if(setting('registration-enabled', false))
<a href="{{ baseUrl("/register") }}">@icon('new-user') {{ trans('auth.sign_up') }}</a>
@endif
@stop
@extends('simple-layout')
@section('content')
<div class="text-center">
<div class="card center-box">
<h3>@icon('login') {{ title_case(trans('auth.log_in')) }}</h3>
<div class="container very-small">
<div class="body">
<form action="{{ baseUrl("/login") }}" method="POST" id="login-form">
{!! csrf_field() !!}
<div class="my-l">&nbsp;</div>
@include('auth/forms/login/' . $authMethod)
<div class="card content-wrap">
<h1 class="list-heading">{{ title_case(trans('auth.log_in')) }}</h1>
<div class="form-group">
<label for="remember" class="inline">{{ trans('auth.remember_me') }}</label>
<input type="checkbox" id="remember" name="remember" class="toggle-switch-checkbox">
<label for="remember" class="toggle-switch"></label>
<form action="{{ baseUrl("/login") }}" method="POST" id="login-form" class="mt-l">
{!! csrf_field() !!}
<div class="stretch-inputs">
@include('auth.forms.login.' . $authMethod)
</div>
<div class="grid half collapse-xs gap-xl v-center">
<div class="text-left ml-xxs">
@include('components.custom-checkbox', [
'name' => 'remember',
'checked' => false,
'value' => 'on',
'label' => trans('auth.remember_me'),
])
</div>
<div class="from-group">
<button class="button block pos" tabindex="3">@icon('login') {{ title_case(trans('auth.log_in')) }}</button>
<div class="text-right">
<button class="button primary" tabindex="3">{{ title_case(trans('auth.log_in')) }}</button>
</div>
</form>
</div>
@if(count($socialDrivers) > 0)
<hr class="margin-top">
@foreach($socialDrivers as $driver => $name)
<a id="social-login-{{$driver}}" class="button block muted-light svg text-left" href="{{ baseUrl("/login/service/" . $driver) }}">
</form>
@if(count($socialDrivers) > 0)
<hr class="my-l">
@foreach($socialDrivers as $driver => $name)
<div>
<a id="social-login-{{$driver}}" class="button outline block svg" href="{{ baseUrl("/login/service/" . $driver) }}">
@icon('auth/' . $driver)
{{ trans('auth.log_in_with', ['socialDriver' => $name]) }}
</a>
@endforeach
@endif
</div>
</div>
@endforeach
@endif
</div>
</div>

View File

@ -1,37 +1,25 @@
@extends('public')
@section('header-buttons')
<a href="{{ baseUrl("/login") }}">@icon('login') {{ trans('auth.log_in') }}</a>
@if(setting('registration-enabled'))
<a href="{{ baseUrl("/register") }}">@icon('new-user') {{ trans('auth.sign_up') }}</a>
@endif
@stop
@extends('simple-layout')
@section('content')
<div class="container very-small mt-xl">
<div class="card content-wrap auto-height">
<h1 class="list-heading">{{ trans('auth.reset_password') }}</h1>
<p class="text-muted small">{{ trans('auth.reset_password_send_instructions') }}</p>
<div class="text-center">
<div class="card center-box">
<h3>@icon('permission') {{ trans('auth.reset_password') }}</h3>
<form action="{{ baseUrl("/password/email") }}" method="POST" class="stretch-inputs">
{!! csrf_field() !!}
<div class="body">
<p class="muted small">{{ trans('auth.reset_password_send_instructions') }}</p>
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
@include('form.text', ['name' => 'email'])
</div>
<form action="{{ baseUrl("/password/email") }}" method="POST">
{!! csrf_field() !!}
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
@include('form/text', ['name' => 'email'])
</div>
<div class="from-group text-right">
<button class="button primary">{{ trans('auth.reset_password_send_button') }}</button>
</div>
</form>
</div>
<div class="from-group text-right mt-m">
<button class="button primary">{{ trans('auth.reset_password_send_button') }}</button>
</div>
</form>
</div>
</div>
@stop

View File

@ -1,43 +1,34 @@
@extends('public')
@section('header-buttons')
<a href="{{ baseUrl("/login") }}">@icon('login') {{ trans('auth.log_in') }}</a>
@if(setting('registration-enabled'))
<a href="{{ baseUrl("/register") }}">@icon('new-user') {{ trans('auth.sign_up') }}</a>
@endif
@stop
@extends('simple-layout')
@section('content')
<div class="text-center">
<div class="card center-box">
<h3>@icon('permission') {{ trans('auth.reset_password') }}</h3>
<div class="container very-small mt-xl">
<div class="card content-wrap auto-height">
<h1 class="list-heading">{{ trans('auth.reset_password') }}</h1>
<div class="body">
<form action="{{ baseUrl("/password/reset") }}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="token" value="{{ $token }}">
<form action="{{ baseUrl("/password/reset") }}" method="POST" class="stretch-inputs">
{!! csrf_field() !!}
<input type="hidden" name="token" value="{{ $token }}">
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
@include('form/text', ['name' => 'email'])
</div>
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
@include('form.text', ['name' => 'email'])
</div>
<div class="form-group">
<label for="password">{{ trans('auth.password') }}</label>
@include('form/password', ['name' => 'password'])
</div>
<div class="form-group">
<label for="password">{{ trans('auth.password') }}</label>
@include('form.password', ['name' => 'password'])
</div>
<div class="form-group">
<label for="password_confirmation">{{ trans('auth.password_confirm') }}</label>
@include('form/password', ['name' => 'password_confirmation'])
</div>
<div class="form-group">
<label for="password_confirmation">{{ trans('auth.password_confirm') }}</label>
@include('form.password', ['name' => 'password_confirmation'])
</div>
<div class="from-group text-right">
<button class="button primary">{{ trans('auth.reset_password') }}</button>
</div>
</form>
</div>
<div class="from-group text-right mt-m">
<button class="button primary">{{ trans('auth.reset_password') }}</button>
</div>
</form>
</div>
</div>

View File

@ -1,19 +1,11 @@
@extends('public')
@section('header-buttons')
@if(!$signedIn)
<a href="{{ baseUrl("/login") }}">@icon('login') {{ trans('auth.log_in') }}</a>
@endif
@stop
@extends('simple-layout')
@section('content')
<div class="text-center">
<div class="card center-box">
<h3>@icon('users') {{ trans('auth.register_thanks') }}</h3>
<div class="body">
<p>{{ trans('auth.register_confirm', ['appName' => setting('app-name')]) }}</p>
</div>
<div class="container very-small mt-xl">
<div class="card content-wrap auto-height">
<h1 class="list-heading">{{ trans('auth.register_thanks') }}</h1>
<p>{{ trans('auth.register_confirm', ['appName' => setting('app-name')]) }}</p>
</div>
</div>

View File

@ -1,50 +1,54 @@
@extends('public')
@section('header-buttons')
<a href="{{ baseUrl("/login") }}">@icon('login') {{ trans('auth.log_in') }}</a>
@stop
@extends('simple-layout')
@section('content')
<div class="container very-small">
<div class="text-center">
<div class="card center-box">
<h3>@icon('new-user') {{ title_case(trans('auth.sign_up')) }}</h3>
<div class="body">
<form action="{{ baseUrl("/register") }}" method="POST">
{!! csrf_field() !!}
<div class="my-l">&nbsp;</div>
<div class="form-group">
<label for="email">{{ trans('auth.name') }}</label>
@include('form/text', ['name' => 'name'])
<div class="card content-wrap">
<h1 class="list-heading">{{ title_case(trans('auth.sign_up')) }}</h1>
<form action="{{ baseUrl("/register") }}" method="POST" class="mt-l stretch-inputs">
{!! csrf_field() !!}
<div class="form-group">
<label for="email">{{ trans('auth.name') }}</label>
@include('form.text', ['name' => 'name'])
</div>
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
@include('form.text', ['name' => 'email'])
</div>
<div class="form-group">
<label for="password">{{ trans('auth.password') }}</label>
@include('form.password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')])
</div>
<div class="grid half collapse-xs gap-xl v-center mt-m">
<div class="text-small">
<a href="{{ baseUrl('/login') }}">Already have an account?</a>
</div>
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
@include('form/text', ['name' => 'email'])
<div class="from-group text-right">
<button class="button primary">{{ trans('auth.create_account') }}</button>
</div>
</div>
<div class="form-group">
<label for="password">{{ trans('auth.password') }}</label>
@include('form/password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')])
</div>
<div class="from-group">
<button class="button block pos">{{ trans('auth.create_account') }}</button>
</div>
</form>
</form>
@if(count($socialDrivers) > 0)
<hr class="margin-top">
@foreach($socialDrivers as $driver => $name)
<a id="social-register-{{$driver}}" class="button block muted-light svg text-left" href="{{ baseUrl("/register/service/" . $driver) }}">
@if(count($socialDrivers) > 0)
<hr class="my-l">
@foreach($socialDrivers as $driver => $name)
<div>
<a id="social-register-{{$driver}}" class="button block outline svg" href="{{ baseUrl("/register/service/" . $driver) }}">
@icon('auth/' . $driver)
{{ trans('auth.sign_up_with', ['socialDriver' => $name]) }}
</a>
@endforeach
@endif
</div>
</div>
@endforeach
@endif
</div>
</div>
@stop

View File

@ -1,34 +1,34 @@
@extends('public')
@extends('simple-layout')
@section('content')
<div class="container small">
<p>&nbsp;</p>
<div class="card">
<h3>@icon('users') {{ trans('auth.email_not_confirmed') }}</h3>
<div class="body">
<p class="text-muted">{{ trans('auth.email_not_confirmed_text') }}<br>
{{ trans('auth.email_not_confirmed_click_link') }} <br>
{{ trans('auth.email_not_confirmed_resend') }}
</p>
<hr>
<form action="{{ baseUrl("/register/confirm/resend") }}" method="POST">
{!! csrf_field() !!}
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
@if(auth()->check())
@include('form/text', ['name' => 'email', 'model' => auth()->user()])
@else
@include('form/text', ['name' => 'email'])
@endif
</div>
<div class="form-group">
<button type="submit" class="button pos">{{ trans('auth.email_not_confirmed_resend_button') }}</button>
</div>
</form>
</div>
</div>
<div class="container very-small mt-xl">
<div class="card content-wrap auto-height">
<h1 class="list-heading">{{ trans('auth.email_not_confirmed') }}</h1>
<p>{{ trans('auth.email_not_confirmed_text') }}<br>
{{ trans('auth.email_not_confirmed_click_link') }}
</p>
<p>
{{ trans('auth.email_not_confirmed_resend') }}
</p>
<form action="{{ baseUrl("/register/confirm/resend") }}" method="POST" class="stretch-inputs">
{!! csrf_field() !!}
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
@if(auth()->check())
@include('form.text', ['name' => 'email', 'model' => auth()->user()])
@else
@include('form.text', ['name' => 'email'])
@endif
</div>
<div class="form-group text-right mt-m">
<button type="submit" class="button primary">{{ trans('auth.email_not_confirmed_resend_button') }}</button>
</div>
</form>
</div>
</div>
@stop

View File

@ -17,75 +17,29 @@
<script src="{{ baseUrl('/translations') }}"></script>
@yield('head')
@include('partials/custom-styles')
@include('partials.custom-styles')
@include('partials.custom-head')
@stack('head')
</head>
<body class="@yield('body-class')" ng-app="bookStack">
<body class="@yield('body-class')">
@include('partials/notifications')
<header id="header">
<div class="container fluid">
<div class="row">
<div class="col-sm-4 col-md-3">
<a href="{{ baseUrl('/') }}" class="logo">
@if(setting('app-logo', '') !== 'none')
<img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo">
@endif
@if (setting('app-name-header'))
<span class="logo-text">{{ setting('app-name') }}</span>
@endif
</a>
</div>
<div class="col-sm-8 col-md-9">
<div class="float right">
<div class="header-search">
<form action="{{ baseUrl('/search') }}" method="GET" class="search-box">
<button id="header-search-box-button" type="submit">@icon('search') </button>
<input id="header-search-box-input" type="text" name="term" tabindex="2" placeholder="{{ trans('common.search') }}" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
</form>
</div>
<div class="links text-center">
@if(userCanOnAny('view', \BookStack\Entities\Bookshelf::class) || userCan('bookshelf-view-own'))
<a href="{{ baseUrl('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
@endif
<a href="{{ baseUrl('/books') }}">@icon('book'){{ trans('entities.books') }}</a>
@if(signedInUser() && userCan('settings-manage'))
<a href="{{ baseUrl('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
@endif
@if(signedInUser() && userCan('users-manage') && !userCan('settings-manage'))
<a href="{{ baseUrl('/settings/users') }}">@icon('users'){{ trans('settings.users') }}</a>
@endif
@if(!signedInUser())
@if(setting('registration-enabled', false))
<a href="{{ baseUrl("/register") }}">@icon('new-user') {{ trans('auth.sign_up') }}</a>
@endif
<a href="{{ baseUrl('/login') }}">@icon('login') {{ trans('auth.log_in') }}</a>
@endif
</div>
@if(signedInUser())
@include('partials._header-dropdown', ['currentUser' => user()])
@endif
</div>
</div>
</div>
</div>
</header>
@include('partials.notifications')
@include('common.header')
<section id="content" class="block">
@yield('content')
</section>
<div back-to-top>
<div back-to-top class="primary-background">
<div class="inner">
@icon('chevron-up') <span>{{ trans('common.back_to_top') }}</span>
</div>
</div>
@yield('bottom')
<script src="{{ versioned_asset('dist/app.js') }}"></script>
@yield('scripts')
@yield('bottom')
<script src="{{ versioned_asset('dist/app.js') }}"></script>
@yield('scripts')
</body>
</html>

View File

@ -1,28 +1,27 @@
@extends('simple-layout')
@section('toolbar')
<div class="col-sm-8 faded">
<div class="breadcrumbs">
<a href="{{ baseUrl('/books') }}" class="text-button">@icon('book'){{ trans('entities.books') }}</a>
<span class="sep">&raquo;</span>
<a href="{{ baseUrl('/create-book') }}" class="text-button">@icon('add'){{ trans('entities.books_create') }}</a>
</div>
</div>
@stop
@section('body')
<div class="container small">
<div class="my-s">
@include('partials.breadcrumbs', ['crumbs' => [
'/books' => [
'text' => trans('entities.books'),
'icon' => 'book'
],
'/create-book' => [
'text' => trans('entities.books_create'),
'icon' => 'add'
]
]])
</div>
<div class="container small">
<p>&nbsp;</p>
<div class="card">
<h3>@icon('add') {{ trans('entities.books_create') }}</h3>
<div class="body">
<div class="content-wrap card">
<h1 class="list-heading">{{ trans('entities.books_create') }}</h1>
<form action="{{ baseUrl("/books") }}" method="POST" enctype="multipart/form-data">
@include('books/form')
@include('books.form')
</form>
</div>
</div>
</div>
<p class="margin-top large"><br></p>
@include('components.image-manager', ['imageType' => 'cover'])
@stop

View File

@ -1,28 +1,30 @@
@extends('simple-layout')
@section('toolbar')
<div class="col-sm-12 faded">
@include('books._breadcrumbs', ['book' => $book])
</div>
@stop
@section('body')
<div class="container small">
<p>&nbsp;</p>
<div class="card">
<h3>@icon('delete') {{ trans('entities.books_delete') }}</h3>
<div class="body">
<p>{{ trans('entities.books_delete_explain', ['bookName' => $book->name]) }}</p>
<p class="text-neg">{{ trans('entities.books_delete_confirmation') }}</p>
<form action="{{$book->getUrl()}}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
<a href="{{$book->getUrl()}}" class="button outline">{{ trans('common.cancel') }}</a>
<button type="submit" class="button neg">{{ trans('common.confirm') }}</button>
</form>
</div>
<div class="my-s">
@include('partials.breadcrumbs', ['crumbs' => [
$book,
$book->getUrl('/delete') => [
'text' => trans('entities.books_delete'),
'icon' => 'delete',
]
]])
</div>
<div class="card content-wrap auto-height">
<h1 class="list-heading">{{ trans('entities.books_delete') }}</h1>
<p>{{ trans('entities.books_delete_explain', ['bookName' => $book->name]) }}</p>
<p class="text-neg"><strong>{{ trans('entities.books_delete_confirmation') }}</strong></p>
<form action="{{$book->getUrl()}}" method="POST" class="text-right">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
<a href="{{$book->getUrl()}}" class="button outline">{{ trans('common.cancel') }}</a>
<button type="submit" class="button primary">{{ trans('common.confirm') }}</button>
</form>
</div>
</div>

View File

@ -1,24 +1,27 @@
@extends('simple-layout')
@section('toolbar')
<div class="col-sm-12 faded">
@include('books._breadcrumbs', ['book' => $book])
</div>
@stop
@section('body')
<div class="container small">
<p>&nbsp;</p>
<div class="card">
<h3>@icon('edit') {{ trans('entities.books_edit') }}</h3>
<div class="body">
<form action="{{ $book->getUrl() }}" method="POST">
<input type="hidden" name="_method" value="PUT">
@include('books/form', ['model' => $book])
</form>
</div>
<div class="my-s">
@include('partials.breadcrumbs', ['crumbs' => [
$book,
$book->getUrl('/edit') => [
'text' => trans('entities.books_edit'),
'icon' => 'edit',
]
]])
</div>
<div class="content-wrap card">
<h1 class="list-heading">{{ trans('entities.books_edit') }}</h1>
<form action="{{ $book->getUrl() }}" method="POST">
<input type="hidden" name="_method" value="PUT">
@include('books.form', ['model' => $book])
</form>
</div>
</div>
@include('components.image-manager', ['imageType' => 'cover'])
@include('components.image-manager', ['imageType' => 'cover'])
@stop

View File

@ -31,51 +31,51 @@
@include('partials.custom-head')
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="page-content">
<h1 style="font-size: 4.8em">{{$book->name}}</h1>
<div class="page-content">
<p>{{ $book->description }}</p>
<h1 style="font-size: 4.8em">{{$book->name}}</h1>
@if(count($bookChildren) > 0)
<ul class="contents">
@foreach($bookChildren as $bookChild)
<li><a href="#{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</a></li>
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
<ul>
@foreach($bookChild->pages as $page)
<li><a href="#page-{{$page->id}}">{{ $page->name }}</a></li>
@endforeach
</ul>
@endif
@endforeach
</ul>
<p>{{ $book->description }}</p>
@if(count($bookChildren) > 0)
<ul class="contents">
@foreach($bookChildren as $bookChild)
<li><a href="#{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</a></li>
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
<ul>
@foreach($bookChild->pages as $page)
<li><a href="#page-{{$page->id}}">{{ $page->name }}</a></li>
@endforeach
</ul>
@endif
@endforeach
</ul>
@endif
@foreach($bookChildren as $bookChild)
@foreach($bookChildren as $bookChild)
<div class="page-break"></div>
<h1 id="{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</h1>
@if($bookChild->isA('chapter'))
<p>{{ $bookChild->description }}</p>
@if(count($bookChild->pages) > 0)
@foreach($bookChild->pages as $page)
<div class="page-break"></div>
<h1 id="{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</h1>
@if($bookChild->isA('chapter'))
<p>{{ $bookChild->description }}</p>
@if(count($bookChild->pages) > 0)
@foreach($bookChild->pages as $page)
<div class="page-break"></div>
<div class="chapter-hint">{{$bookChild->name}}</div>
<h1 id="page-{{$page->id}}">{{ $page->name }}</h1>
{!! $page->html !!}
@endforeach
@endif
@else
{!! $bookChild->html !!}
@endif
<div class="chapter-hint">{{$bookChild->name}}</div>
<h1 id="page-{{$page->id}}">{{ $page->name }}</h1>
{!! $page->html !!}
@endforeach
@endif
@else
{!! $bookChild->html !!}
@endif
@endforeach
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -2,12 +2,12 @@
{{ csrf_field() }}
<div class="form-group title-input">
<label for="name">{{ trans('common.name') }}</label>
@include('form/text', ['name' => 'name'])
@include('form.text', ['name' => 'name'])
</div>
<div class="form-group description-input">
<label for="description">{{ trans('common.description') }}</label>
@include('form/textarea', ['name' => 'description'])
@include('form.textarea', ['name' => 'description'])
</div>
<div class="form-group" collapsible id="logo-control">
@ -41,5 +41,5 @@
<div class="form-group text-right">
<a href="{{ isset($book) ? $book->getUrl() : baseUrl('/books') }}" class="button outline">{{ trans('common.cancel') }}</a>
<button type="submit" class="button pos">{{ trans('entities.books_save') }}</button>
<button type="submit" class="button primary">{{ trans('entities.books_save') }}</button>
</div>

View File

@ -1,18 +1,19 @@
<div class="book-grid-item grid-card" data-entity-type="book" data-entity-id="{{$book->id}}">
<div class="featured-image-container">
<a href="{{$book->getUrl()}}" title="{{$book->name}}">
<img src="{{$book->getBookCover()}}" alt="{{$book->name}}">
</a>
<a href="{{$book->getUrl()}}" class="grid-card" data-entity-type="book" data-entity-id="{{$book->id}}">
<div class="bg-book featured-image-container-wrap">
<div class="featured-image-container" @if($book->cover) style="background-image: url('{{ $book->getBookCover() }}')"@endif>
</div>
@icon('book')
</div>
<div class="grid-card-content">
<h2><a class="break-text" href="{{$book->getUrl()}}" title="{{$book->name}}">{{$book->getShortName(35)}}</a></h2>
<h2>{{$book->getShortName(35)}}</h2>
@if(isset($book->searchSnippet))
<p >{!! $book->searchSnippet !!}</p>
<p class="text-muted">{!! $book->searchSnippet !!}</p>
@else
<p >{{ $book->getExcerpt(130) }}</p>
<p class="text-muted">{{ $book->getExcerpt(130) }}</p>
@endif
</div>
<div class="grid-card-footer text-muted text-small">
<span>@include('partials.entity-meta', ['entity' => $book])</span>
<div class="grid-card-footer text-muted ">
<p>@icon('star')<span title="{{$book->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $book->created_at->diffForHumans()]) }}</span></p>
<p>@icon('edit')<span title="{{ $book->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $book->updated_at->diffForHumans()]) }}</span></p>
</div>
</div>
</a>

View File

@ -1,47 +1,52 @@
@extends('sidebar-layout')
@extends('tri-layout')
@section('toolbar')
<div class="col-xs-6">
<div class="action-buttons text-left">
@include('books/view-toggle', ['booksViewType' => $booksViewType])
</div>
</div>
<div class="col-xs-6 faded">
<div class="action-buttons">
@if($currentUser->can('book-create-all'))
<a href="{{ baseUrl("/create-book") }}" class="text-pos text-button">@icon('add'){{ trans('entities.books_create') }}</a>
@endif
</div>
</div>
@section('container-classes', 'mt-xl')
@section('body')
@include('books.list', ['books' => $books, 'view' => $view])
@stop
@section('sidebar')
@section('left')
@if($recents)
<div id="recents" class="card">
<h3>@icon('view') {{ trans('entities.recently_viewed') }}</h3>
@include('partials/entity-list', ['entities' => $recents, 'style' => 'compact'])
<div id="recents" class="mb-xl">
<h5>{{ trans('entities.recently_viewed') }}</h5>
@include('partials.entity-list', ['entities' => $recents, 'style' => 'compact'])
</div>
@endif
<div id="popular" class="card">
<h3>@icon('popular') {{ trans('entities.books_popular') }}</h3>
<div id="popular" class="mb-xl">
<h5>{{ trans('entities.books_popular') }}</h5>
@if(count($popular) > 0)
@include('partials/entity-list', ['entities' => $popular, 'style' => 'compact'])
@include('partials.entity-list', ['entities' => $popular, 'style' => 'compact'])
@else
<div class="body text-muted">{{ trans('entities.books_popular_empty') }}</div>
@endif
</div>
<div id="new" class="card">
<h3>@icon('star-circle') {{ trans('entities.books_new') }}</h3>
<div id="new" class="mb-xl">
<h5>{{ trans('entities.books_new') }}</h5>
@if(count($popular) > 0)
@include('partials/entity-list', ['entities' => $new, 'style' => 'compact'])
@include('partials.entity-list', ['entities' => $new, 'style' => 'compact'])
@else
<div class="body text-muted">{{ trans('entities.books_new_empty') }}</div>
@endif
</div>
@stop
@section('body')
@include('books/list', ['books' => $books, 'bookViewType' => $booksViewType])
@section('right')
<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@if($currentUser->can('book-create-all'))
<a href="{{ baseUrl("/create-book") }}" class="icon-list-item">
<span>@icon('add')</span>
<span>{{ trans('entities.books_create') }}</span>
</a>
@endif
@include('partials.view-toggle', ['view' => $view, 'type' => 'book'])
</div>
</div>
@stop

View File

@ -1,10 +1,11 @@
<div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}">
<h4 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}">@icon('book')<span class="entity-list-item-name break-text">{{$book->name}}</span></a></h4>
<div class="entity-item-snippet">
@if(isset($book->searchSnippet))
<p class="text-muted break-text">{!! $book->searchSnippet !!}</p>
@else
<p class="text-muted break-text">{{ $book->getExcerpt() }}</p>
@endif
<a href="{{ $book->getUrl() }}" class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}">
<div class="entity-list-item-image bg-book" style="background-image: url('{{ $book->getBookCover() }}')">
@icon('book')
</div>
</div>
<div class="content">
<h4 class="entity-list-item-name break-text">{{ $book->name }}</h4>
<div class="entity-item-snippet">
<p class="text-muted break-text mb-s">{{ $book->getExcerpt() }}</p>
</div>
</div>
</a>

View File

@ -1,23 +1,30 @@
<div class="container{{ $booksViewType === 'list' ? ' small' : '' }}">
<h1>{{ trans('entities.books') }}</h1>
<div class="content-wrap mt-m card">
<div class="grid half v-center">
<h1 class="list-heading">{{ trans('entities.books') }}</h1>
<div class="text-right">
@include('partials.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'books'])
</div>
</div>
@if(count($books) > 0)
@if($booksViewType === 'list')
@foreach($books as $book)
@include('books/list-item', ['book' => $book])
<hr>
@endforeach
{!! $books->render() !!}
@if($view === 'list')
<div class="entity-list">
@foreach($books as $book)
@include('books.list-item', ['book' => $book])
@endforeach
</div>
@else
<div class="grid third">
@foreach($books as $key => $book)
@include('books/grid-item', ['book' => $book])
@include('books.grid-item', ['book' => $book])
@endforeach
</div>
<div>
{!! $books->render() !!}
</div>
@endif
<div>
{!! $books->render() !!}
</div>
@else
<p class="text-muted">{{ trans('entities.books_empty') }}</p>
@if(userCan('books-create-all'))

View File

@ -0,0 +1,23 @@
@extends('simple-layout')
@section('body')
<div class="container">
<div class="my-s">
@include('partials.breadcrumbs', ['crumbs' => [
$book,
$book->getUrl('/permissions') => [
'text' => trans('entities.books_permissions'),
'icon' => 'lock',
]
]])
</div>
<div class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.books_permissions') }}</h1>
@include('form.entity-permissions', ['model' => $book])
</div>
</div>
@stop

Some files were not shown because too many files have changed in this diff Show More