Merge branch 'master' into release
|
@ -32,6 +32,11 @@ APP_LANG=en
|
||||||
# APP_LANG will be used if such a header is not provided.
|
# APP_LANG will be used if such a header is not provided.
|
||||||
APP_AUTO_LANG_PUBLIC=true
|
APP_AUTO_LANG_PUBLIC=true
|
||||||
|
|
||||||
|
# Application timezone
|
||||||
|
# Used where dates are displayed such as on exported content.
|
||||||
|
# Valid timezone values can be found here: https://www.php.net/manual/en/timezones.php
|
||||||
|
APP_TIMEZONE=UTC
|
||||||
|
|
||||||
# Database details
|
# Database details
|
||||||
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
|
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
|
|
|
@ -103,18 +103,22 @@ class ActivityService
|
||||||
* @param int $page
|
* @param int $page
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function entityActivity($entity, $count = 20, $page = 0)
|
public function entityActivity($entity, $count = 20, $page = 1)
|
||||||
{
|
{
|
||||||
if ($entity->isA('book')) {
|
if ($entity->isA('book')) {
|
||||||
$query = $this->activity->where('book_id', '=', $entity->id);
|
$query = $this->activity->where('book_id', '=', $entity->id);
|
||||||
} else {
|
} else {
|
||||||
$query = $this->activity->where('entity_type', '=', get_class($entity))
|
$query = $this->activity->where('entity_type', '=', $entity->getMorphClass())
|
||||||
->where('entity_id', '=', $entity->id);
|
->where('entity_id', '=', $entity->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$activity = $this->permissionService
|
$activity = $this->permissionService
|
||||||
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
|
->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);
|
return $this->filterSimilar($activity);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,21 +2,26 @@
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
use BookStack\Entities\Entity;
|
use BookStack\Entities\Entity;
|
||||||
|
use BookStack\Entities\EntityProvider;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class ViewService
|
class ViewService
|
||||||
{
|
{
|
||||||
protected $view;
|
protected $view;
|
||||||
protected $permissionService;
|
protected $permissionService;
|
||||||
|
protected $entityProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewService constructor.
|
* ViewService constructor.
|
||||||
* @param \BookStack\Actions\View $view
|
* @param \BookStack\Actions\View $view
|
||||||
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
|
* @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->view = $view;
|
||||||
$this->permissionService = $permissionService;
|
$this->permissionService = $permissionService;
|
||||||
|
$this->entityProvider = $entityProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,23 +55,21 @@ class ViewService
|
||||||
* Get the entities with the most views.
|
* Get the entities with the most views.
|
||||||
* @param int $count
|
* @param int $count
|
||||||
* @param int $page
|
* @param int $page
|
||||||
* @param Entity|false|array $filterModel
|
* @param string|array $filterModels
|
||||||
* @param string $action - used for permission checking
|
* @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;
|
$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'))
|
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
|
||||||
->groupBy('viewable_id', 'viewable_type')
|
->groupBy('viewable_id', 'viewable_type')
|
||||||
->orderBy('view_count', 'desc');
|
->orderBy('view_count', 'desc');
|
||||||
|
|
||||||
if ($filterModel && is_array($filterModel)) {
|
if ($filterModels) {
|
||||||
$query->whereIn('viewable_type', $filterModel);
|
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
|
||||||
} else if ($filterModel) {
|
|
||||||
$query->where('viewable_type', '=', $filterModel->getMorphClass());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||||
|
|
|
@ -182,25 +182,14 @@ class LdapService
|
||||||
throw new LdapException(trans('errors.ldap_extension_not_installed'));
|
throw new LdapException(trans('errors.ldap_extension_not_installed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get port from server string and protocol if specified.
|
// Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
|
||||||
$ldapServer = explode(':', $this->config['server']);
|
// the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not per handle.
|
||||||
$hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
|
|
||||||
if (!$hasProtocol) {
|
|
||||||
array_unshift($ldapServer, '');
|
|
||||||
}
|
|
||||||
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
|
|
||||||
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
|
|
||||||
* the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not
|
|
||||||
* per handle.
|
|
||||||
*/
|
|
||||||
if ($this->config['tls_insecure']) {
|
if ($this->config['tls_insecure']) {
|
||||||
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
|
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
|
$serverDetails = $this->parseServerString($this->config['server']);
|
||||||
|
$ldapConnection = $this->ldap->connect($serverDetails['host'], $serverDetails['port']);
|
||||||
|
|
||||||
if ($ldapConnection === false) {
|
if ($ldapConnection === false) {
|
||||||
throw new LdapException(trans('errors.ldap_cannot_connect'));
|
throw new LdapException(trans('errors.ldap_cannot_connect'));
|
||||||
|
@ -215,6 +204,27 @@ class LdapService
|
||||||
return $this->ldapConnection;
|
return $this->ldapConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a LDAP server string and return the host and port for
|
||||||
|
* a connection. Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'
|
||||||
|
* @param $serverString
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function parseServerString($serverString)
|
||||||
|
{
|
||||||
|
$serverNameParts = explode(':', $serverString);
|
||||||
|
|
||||||
|
// If we have a protocol just return the full string since PHP will ignore a separate port.
|
||||||
|
if ($serverNameParts[0] === 'ldaps' || $serverNameParts[0] === 'ldap') {
|
||||||
|
return ['host' => $serverString, 'port' => 389];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, extract the port out
|
||||||
|
$hostName = $serverNameParts[0];
|
||||||
|
$ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
|
||||||
|
return ['host' => $hostName, 'port' => $ldapPort];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a filter string by injecting common variables.
|
* Build a filter string by injecting common variables.
|
||||||
* @param string $filterString
|
* @param string $filterString
|
||||||
|
|
|
@ -704,7 +704,7 @@ class PermissionService
|
||||||
* @param string $entityIdColumn
|
* @param string $entityIdColumn
|
||||||
* @param string $entityTypeColumn
|
* @param string $entityTypeColumn
|
||||||
* @param string $action
|
* @param string $action
|
||||||
* @return mixed
|
* @return QueryBuilder
|
||||||
*/
|
*/
|
||||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
|
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
|
||||||
{
|
{
|
||||||
|
@ -732,18 +732,21 @@ class PermissionService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters pages that are a direct relation to another item.
|
* Add conditions to a query to filter the selection to related entities
|
||||||
|
* where permissions are granted.
|
||||||
|
* @param $entityType
|
||||||
* @param $query
|
* @param $query
|
||||||
* @param $tableName
|
* @param $tableName
|
||||||
* @param $entityIdColumn
|
* @param $entityIdColumn
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
|
||||||
{
|
{
|
||||||
$this->currentAction = 'view';
|
$this->currentAction = 'view';
|
||||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||||
|
|
||||||
$pageMorphClass = $this->entityProvider->page->getMorphClass();
|
$pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
|
||||||
|
|
||||||
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
||||||
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
|
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
|
||||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
|
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
|
||||||
|
@ -761,7 +764,9 @@ class PermissionService
|
||||||
});
|
});
|
||||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->clean();
|
$this->clean();
|
||||||
|
|
||||||
return $q;
|
return $q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php namespace BookStack\Auth;
|
<?php namespace BookStack\Auth;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\JointPermission;
|
use BookStack\Auth\Permissions\JointPermission;
|
||||||
|
use BookStack\Auth\Permissions\RolePermission;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
|
||||||
class Role extends Model
|
class Role extends Model
|
||||||
|
@ -13,7 +14,7 @@ class Role extends Model
|
||||||
*/
|
*/
|
||||||
public function users()
|
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()
|
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.
|
* 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);
|
$this->permissions()->attach($permission->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detach a single permission from this role.
|
* 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);
|
$this->permissions()->detach($permission->id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $fillable = ['name', 'email', 'image_id'];
|
protected $fillable = ['name', 'email'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes excluded from the model's JSON form.
|
* The attributes excluded from the model's JSON form.
|
||||||
|
|
|
@ -6,6 +6,7 @@ use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Exceptions\UserUpdateException;
|
use BookStack\Exceptions\UserUpdateException;
|
||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Images;
|
use Images;
|
||||||
|
|
||||||
class UserRepo
|
class UserRepo
|
||||||
|
@ -48,7 +49,7 @@ class UserRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the users with their permissions.
|
* Get all the users with their permissions.
|
||||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
* @return Builder|static
|
||||||
*/
|
*/
|
||||||
public function getAllUsers()
|
public function getAllUsers()
|
||||||
{
|
{
|
||||||
|
@ -59,7 +60,7 @@ class UserRepo
|
||||||
* Get all the users with their permissions in a paginated format.
|
* Get all the users with their permissions in a paginated format.
|
||||||
* @param int $count
|
* @param int $count
|
||||||
* @param $sortData
|
* @param $sortData
|
||||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
* @return Builder|static
|
||||||
*/
|
*/
|
||||||
public function getAllUsersPaginatedAndSorted($count, $sortData)
|
public function getAllUsersPaginatedAndSorted($count, $sortData)
|
||||||
{
|
{
|
||||||
|
@ -197,7 +198,7 @@ class UserRepo
|
||||||
$user->delete();
|
$user->delete();
|
||||||
|
|
||||||
// Delete user profile images
|
// Delete user profile images
|
||||||
$profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get();
|
$profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
|
||||||
foreach ($profileImages as $image) {
|
foreach ($profileImages as $image) {
|
||||||
Images::destroy($image);
|
Images::destroy($image);
|
||||||
}
|
}
|
||||||
|
@ -223,16 +224,15 @@ class UserRepo
|
||||||
*/
|
*/
|
||||||
public function getRecentlyCreated(User $user, $count = 20)
|
public function getRecentlyCreated(User $user, $count = 20)
|
||||||
{
|
{
|
||||||
|
$createdByUserQuery = function (Builder $query) use ($user) {
|
||||||
|
$query->where('created_by', '=', $user->id);
|
||||||
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, function ($query) use ($user) {
|
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, $createdByUserQuery),
|
||||||
$query->where('created_by', '=', $user->id);
|
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, $createdByUserQuery),
|
||||||
}),
|
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, $createdByUserQuery),
|
||||||
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, function ($query) use ($user) {
|
'shelves' => $this->entityRepo->getRecentlyCreated('bookshelf', $count, 0, $createdByUserQuery)
|
||||||
$query->where('created_by', '=', $user->id);
|
|
||||||
}),
|
|
||||||
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, function ($query) use ($user) {
|
|
||||||
$query->where('created_by', '=', $user->id);
|
|
||||||
})
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,6 +247,7 @@ class UserRepo
|
||||||
'pages' => $this->entityRepo->getUserTotalCreated('page', $user),
|
'pages' => $this->entityRepo->getUserTotalCreated('page', $user),
|
||||||
'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
|
'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
|
||||||
'books' => $this->entityRepo->getUserTotalCreated('book', $user),
|
'books' => $this->entityRepo->getUserTotalCreated('book', $user),
|
||||||
|
'shelves' => $this->entityRepo->getUserTotalCreated('bookshelf', $user),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,7 +257,7 @@ class UserRepo
|
||||||
*/
|
*/
|
||||||
public function getAllRoles()
|
public function getAllRoles()
|
||||||
{
|
{
|
||||||
return $this->role->all();
|
return $this->role->newQuery()->orderBy('name', 'asc')->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Book extends Entity
|
||||||
*/
|
*/
|
||||||
public function getBookCover($width = 440, $height = 250)
|
public function getBookCover($width = 440, $height = 250)
|
||||||
{
|
{
|
||||||
$default = baseUrl('/book_default_cover.png');
|
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||||
if (!$this->image_id) {
|
if (!$this->image_id) {
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,15 @@ class Book extends Entity
|
||||||
return $this->hasMany(Page::class);
|
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.
|
* Get all chapters within this book.
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
@ -92,7 +101,7 @@ class Book extends Entity
|
||||||
* @param int $length
|
* @param int $length
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getExcerpt($length = 100)
|
public function getExcerpt(int $length = 100)
|
||||||
{
|
{
|
||||||
$description = $this->description;
|
$description = $this->description;
|
||||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||||
|
|
|
@ -26,7 +26,9 @@ class Bookshelf extends Entity
|
||||||
*/
|
*/
|
||||||
public function books()
|
public function books()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc');
|
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')
|
||||||
|
->withPivot('order')
|
||||||
|
->orderBy('order', 'asc');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,7 +52,8 @@ class Bookshelf extends Entity
|
||||||
*/
|
*/
|
||||||
public function getBookCover($width = 440, $height = 250)
|
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) {
|
if (!$this->image_id) {
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
|
@ -64,7 +67,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
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
*/
|
*/
|
||||||
public function cover()
|
public function cover()
|
||||||
|
@ -77,7 +80,7 @@ class Bookshelf extends Entity
|
||||||
* @param int $length
|
* @param int $length
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getExcerpt($length = 100)
|
public function getExcerpt(int $length = 100)
|
||||||
{
|
{
|
||||||
$description = $this->description;
|
$description = $this->description;
|
||||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||||
|
@ -91,4 +94,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";
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,9 +53,9 @@ class Chapter extends Entity
|
||||||
* @param int $length
|
* @param int $length
|
||||||
* @return string
|
* @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;
|
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";
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,6 +102,11 @@ class Entity extends Ownable
|
||||||
return $this->morphMany(View::class, 'viewable');
|
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.
|
* Get the Tag models that have been user assigned to this entity.
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||||
|
@ -218,6 +223,20 @@ class Entity extends Ownable
|
||||||
return $this->{$this->textField};
|
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 a generalised, common raw query that can be 'unioned' across entities.
|
||||||
* @return string
|
* @return string
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -84,4 +84,23 @@ class EntityProvider
|
||||||
$type = strtolower($type);
|
$type = strtolower($type);
|
||||||
return $this->all()[$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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,17 +102,6 @@ class Page extends Entity
|
||||||
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent);
|
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.
|
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||||
* @param bool $withContent
|
* @param bool $withContent
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?php namespace BookStack\Entities\Repos;
|
<?php namespace BookStack\Entities\Repos;
|
||||||
|
|
||||||
|
use Activity;
|
||||||
use BookStack\Actions\TagRepo;
|
use BookStack\Actions\TagRepo;
|
||||||
use BookStack\Actions\ViewService;
|
use BookStack\Actions\ViewService;
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
|
@ -15,8 +16,13 @@ use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Exceptions\NotifyException;
|
use BookStack\Exceptions\NotifyException;
|
||||||
use BookStack\Uploads\AttachmentService;
|
use BookStack\Uploads\AttachmentService;
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
|
use DOMNode;
|
||||||
|
use DOMXPath;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class EntityRepo
|
class EntityRepo
|
||||||
{
|
{
|
||||||
|
@ -101,7 +107,7 @@ class EntityRepo
|
||||||
* @param integer $id
|
* @param integer $id
|
||||||
* @param bool $allowDrafts
|
* @param bool $allowDrafts
|
||||||
* @param bool $ignorePermissions
|
* @param bool $ignorePermissions
|
||||||
* @return \BookStack\Entities\Entity
|
* @return Entity
|
||||||
*/
|
*/
|
||||||
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
|
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
|
||||||
{
|
{
|
||||||
|
@ -119,7 +125,7 @@ class EntityRepo
|
||||||
* @param []int $ids
|
* @param []int $ids
|
||||||
* @param bool $allowDrafts
|
* @param bool $allowDrafts
|
||||||
* @param bool $ignorePermissions
|
* @param bool $ignorePermissions
|
||||||
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
|
* @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
|
||||||
*/
|
*/
|
||||||
public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
|
public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
|
||||||
{
|
{
|
||||||
|
@ -137,7 +143,7 @@ class EntityRepo
|
||||||
* @param string $type
|
* @param string $type
|
||||||
* @param string $slug
|
* @param string $slug
|
||||||
* @param string|bool $bookSlug
|
* @param string|bool $bookSlug
|
||||||
* @return \BookStack\Entities\Entity
|
* @return Entity
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
public function getBySlug($type, $slug, $bookSlug = false)
|
public function getBySlug($type, $slug, $bookSlug = false)
|
||||||
|
@ -179,11 +185,38 @@ class EntityRepo
|
||||||
* Get all entities in a paginated format
|
* Get all entities in a paginated format
|
||||||
* @param $type
|
* @param $type
|
||||||
* @param int $count
|
* @param int $count
|
||||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
* @param string $sort
|
||||||
|
* @param string $order
|
||||||
|
* @param null|callable $queryAddition
|
||||||
|
* @return 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 +298,14 @@ class EntityRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the most popular entities base on all views.
|
* Get the most popular entities base on all views.
|
||||||
* @param string|bool $type
|
* @param string $type
|
||||||
* @param int $count
|
* @param int $count
|
||||||
* @param int $page
|
* @param int $page
|
||||||
* @return mixed
|
* @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, $type);
|
||||||
return $this->viewService->getPopular($count, $page, $filter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -305,7 +337,7 @@ class EntityRepo
|
||||||
/**
|
/**
|
||||||
* Get the child items for a chapter sorted by priority but
|
* Get the child items for a chapter sorted by priority but
|
||||||
* with draft items floated to the top.
|
* with draft items floated to the top.
|
||||||
* @param \BookStack\Entities\Bookshelf $bookshelf
|
* @param Bookshelf $bookshelf
|
||||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||||
*/
|
*/
|
||||||
public function getBookshelfChildren(Bookshelf $bookshelf)
|
public function getBookshelfChildren(Bookshelf $bookshelf)
|
||||||
|
@ -313,11 +345,23 @@ class EntityRepo
|
||||||
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
|
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.
|
* Get all child objects of a book.
|
||||||
* Returns a sorted collection of Pages and Chapters.
|
* Returns a sorted collection of Pages and Chapters.
|
||||||
* Loads the book slug onto child elements to prevent access database access for getting the slug.
|
* Loads the book slug onto child elements to prevent access database access for getting the slug.
|
||||||
* @param \BookStack\Entities\Book $book
|
* @param Book $book
|
||||||
* @param bool $filterDrafts
|
* @param bool $filterDrafts
|
||||||
* @param bool $renderPages
|
* @param bool $renderPages
|
||||||
* @return mixed
|
* @return mixed
|
||||||
|
@ -367,7 +411,7 @@ class EntityRepo
|
||||||
/**
|
/**
|
||||||
* Get the child items for a chapter sorted by priority but
|
* Get the child items for a chapter sorted by priority but
|
||||||
* with draft items floated to the top.
|
* with draft items floated to the top.
|
||||||
* @param \BookStack\Entities\Chapter $chapter
|
* @param Chapter $chapter
|
||||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||||
*/
|
*/
|
||||||
public function getChapterChildren(Chapter $chapter)
|
public function getChapterChildren(Chapter $chapter)
|
||||||
|
@ -379,7 +423,7 @@ class EntityRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the next sequential priority for a new child element in the given book.
|
* Get the next sequential priority for a new child element in the given book.
|
||||||
* @param \BookStack\Entities\Book $book
|
* @param Book $book
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function getNewBookPriority(Book $book)
|
public function getNewBookPriority(Book $book)
|
||||||
|
@ -390,7 +434,7 @@ class EntityRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a new priority for a new page to be added to the given chapter.
|
* Get a new priority for a new page to be added to the given chapter.
|
||||||
* @param \BookStack\Entities\Chapter $chapter
|
* @param Chapter $chapter
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function getNewChapterPriority(Chapter $chapter)
|
public function getNewChapterPriority(Chapter $chapter)
|
||||||
|
@ -439,8 +483,8 @@ class EntityRepo
|
||||||
/**
|
/**
|
||||||
* Updates entity restrictions from a request
|
* Updates entity restrictions from a request
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param \BookStack\Entities\Entity $entity
|
* @param Entity $entity
|
||||||
* @throws \Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
|
public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
|
||||||
{
|
{
|
||||||
|
@ -470,7 +514,7 @@ class EntityRepo
|
||||||
* @param string $type
|
* @param string $type
|
||||||
* @param array $input
|
* @param array $input
|
||||||
* @param bool|Book $book
|
* @param bool|Book $book
|
||||||
* @return \BookStack\Entities\Entity
|
* @return Entity
|
||||||
*/
|
*/
|
||||||
public function createFromInput($type, $input = [], $book = false)
|
public function createFromInput($type, $input = [], $book = false)
|
||||||
{
|
{
|
||||||
|
@ -494,9 +538,9 @@ class EntityRepo
|
||||||
* Update entity details from request input.
|
* Update entity details from request input.
|
||||||
* Used for books and chapters
|
* Used for books and chapters
|
||||||
* @param string $type
|
* @param string $type
|
||||||
* @param \BookStack\Entities\Entity $entityModel
|
* @param Entity $entityModel
|
||||||
* @param array $input
|
* @param array $input
|
||||||
* @return \BookStack\Entities\Entity
|
* @return Entity
|
||||||
*/
|
*/
|
||||||
public function updateFromInput($type, Entity $entityModel, $input = [])
|
public function updateFromInput($type, Entity $entityModel, $input = [])
|
||||||
{
|
{
|
||||||
|
@ -519,7 +563,7 @@ class EntityRepo
|
||||||
/**
|
/**
|
||||||
* Sync the books assigned to a shelf from a comma-separated list
|
* Sync the books assigned to a shelf from a comma-separated list
|
||||||
* of book IDs.
|
* of book IDs.
|
||||||
* @param \BookStack\Entities\Bookshelf $shelf
|
* @param Bookshelf $shelf
|
||||||
* @param string $books
|
* @param string $books
|
||||||
*/
|
*/
|
||||||
public function updateShelfBooks(Bookshelf $shelf, string $books)
|
public function updateShelfBooks(Bookshelf $shelf, string $books)
|
||||||
|
@ -538,13 +582,28 @@ class EntityRepo
|
||||||
$shelf->books()->sync($syncData);
|
$shelf->books()->sync($syncData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a Book to a BookShelf.
|
||||||
|
* @param Bookshelf $shelf
|
||||||
|
* @param Book $book
|
||||||
|
*/
|
||||||
|
public function appendBookToShelf(Bookshelf $shelf, Book $book)
|
||||||
|
{
|
||||||
|
if ($shelf->contains($book)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxOrder = $shelf->books()->max('order');
|
||||||
|
$shelf->books()->attach($book->id, ['order' => $maxOrder + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the book that an entity belongs to.
|
* Change the book that an entity belongs to.
|
||||||
* @param string $type
|
* @param string $type
|
||||||
* @param integer $newBookId
|
* @param integer $newBookId
|
||||||
* @param Entity $entity
|
* @param Entity $entity
|
||||||
* @param bool $rebuildPermissions
|
* @param bool $rebuildPermissions
|
||||||
* @return \BookStack\Entities\Entity
|
* @return Entity
|
||||||
*/
|
*/
|
||||||
public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
|
public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
|
||||||
{
|
{
|
||||||
|
@ -661,6 +720,7 @@ class EntityRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
$doc = new DOMDocument();
|
$doc = new DOMDocument();
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
||||||
$matchingElem = $doc->getElementById($splitInclude[1]);
|
$matchingElem = $doc->getElementById($splitInclude[1]);
|
||||||
if ($matchingElem === null) {
|
if ($matchingElem === null) {
|
||||||
|
@ -676,6 +736,7 @@ class EntityRepo
|
||||||
$innerContent .= $doc->saveHTML($childNode);
|
$innerContent .= $doc->saveHTML($childNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
libxml_clear_errors();
|
||||||
$html = str_replace($matches[0][$index], trim($innerContent), $html);
|
$html = str_replace($matches[0][$index], trim($innerContent), $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -689,13 +750,35 @@ class EntityRepo
|
||||||
*/
|
*/
|
||||||
protected function escapeScripts(string $html) : string
|
protected function escapeScripts(string $html) : string
|
||||||
{
|
{
|
||||||
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
|
if ($html == '') {
|
||||||
$matches = [];
|
return $html;
|
||||||
preg_match_all($scriptSearchRegex, $html, $matches);
|
|
||||||
|
|
||||||
foreach ($matches[0] as $match) {
|
|
||||||
$html = str_replace($match, htmlentities($match), $html);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
$doc = new DOMDocument();
|
||||||
|
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||||
|
$xPath = new DOMXPath($doc);
|
||||||
|
|
||||||
|
// Remove standard script tags
|
||||||
|
$scriptElems = $xPath->query('//body//*//script');
|
||||||
|
foreach ($scriptElems as $scriptElem) {
|
||||||
|
$scriptElem->parentNode->removeChild($scriptElem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 'on*' attributes
|
||||||
|
$onAttributes = $xPath->query('//body//*/@*[starts-with(name(), \'on\')]');
|
||||||
|
foreach ($onAttributes as $attr) {
|
||||||
|
/** @var \DOMAttr $attr*/
|
||||||
|
$attrName = $attr->nodeName;
|
||||||
|
$attr->parentNode->removeAttribute($attrName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = '';
|
||||||
|
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
|
||||||
|
foreach ($topElems as $child) {
|
||||||
|
$html .= $doc->saveHTML($child);
|
||||||
|
}
|
||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -706,7 +789,7 @@ class EntityRepo
|
||||||
*/
|
*/
|
||||||
public function searchForImage($imageString)
|
public function searchForImage($imageString)
|
||||||
{
|
{
|
||||||
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get();
|
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
|
||||||
foreach ($pages as $page) {
|
foreach ($pages as $page) {
|
||||||
$page->url = $page->getUrl();
|
$page->url = $page->getUrl();
|
||||||
$page->html = '';
|
$page->html = '';
|
||||||
|
@ -717,8 +800,8 @@ class EntityRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy a bookshelf instance
|
* Destroy a bookshelf instance
|
||||||
* @param \BookStack\Entities\Bookshelf $shelf
|
* @param Bookshelf $shelf
|
||||||
* @throws \Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function destroyBookshelf(Bookshelf $shelf)
|
public function destroyBookshelf(Bookshelf $shelf)
|
||||||
{
|
{
|
||||||
|
@ -728,9 +811,9 @@ class EntityRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy the provided book and all its child entities.
|
* Destroy the provided book and all its child entities.
|
||||||
* @param \BookStack\Entities\Book $book
|
* @param Book $book
|
||||||
* @throws NotifyException
|
* @throws NotifyException
|
||||||
* @throws \Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function destroyBook(Book $book)
|
public function destroyBook(Book $book)
|
||||||
{
|
{
|
||||||
|
@ -746,8 +829,8 @@ class EntityRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy a chapter and its relations.
|
* Destroy a chapter and its relations.
|
||||||
* @param \BookStack\Entities\Chapter $chapter
|
* @param Chapter $chapter
|
||||||
* @throws \Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function destroyChapter(Chapter $chapter)
|
public function destroyChapter(Chapter $chapter)
|
||||||
{
|
{
|
||||||
|
@ -765,7 +848,7 @@ class EntityRepo
|
||||||
* Destroy a given page along with its dependencies.
|
* Destroy a given page along with its dependencies.
|
||||||
* @param Page $page
|
* @param Page $page
|
||||||
* @throws NotifyException
|
* @throws NotifyException
|
||||||
* @throws \Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function destroyPage(Page $page)
|
public function destroyPage(Page $page)
|
||||||
{
|
{
|
||||||
|
@ -788,12 +871,12 @@ class EntityRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy or handle the common relations connected to an entity.
|
* Destroy or handle the common relations connected to an entity.
|
||||||
* @param \BookStack\Entities\Entity $entity
|
* @param Entity $entity
|
||||||
* @throws \Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
protected function destroyEntityCommonRelations(Entity $entity)
|
protected function destroyEntityCommonRelations(Entity $entity)
|
||||||
{
|
{
|
||||||
\Activity::removeEntity($entity);
|
Activity::removeEntity($entity);
|
||||||
$entity->views()->delete();
|
$entity->views()->delete();
|
||||||
$entity->permissions()->delete();
|
$entity->permissions()->delete();
|
||||||
$entity->tags()->delete();
|
$entity->tags()->delete();
|
||||||
|
@ -805,9 +888,9 @@ class EntityRepo
|
||||||
/**
|
/**
|
||||||
* Copy the permissions of a bookshelf to all child books.
|
* Copy the permissions of a bookshelf to all child books.
|
||||||
* Returns the number of books that had permissions updated.
|
* Returns the number of books that had permissions updated.
|
||||||
* @param \BookStack\Entities\Bookshelf $bookshelf
|
* @param Bookshelf $bookshelf
|
||||||
* @return int
|
* @return int
|
||||||
* @throws \Throwable
|
* @throws Throwable
|
||||||
*/
|
*/
|
||||||
public function copyBookshelfPermissions(Bookshelf $bookshelf)
|
public function copyBookshelfPermissions(Bookshelf $bookshelf)
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,6 +7,7 @@ use BookStack\Entities\Page;
|
||||||
use BookStack\Entities\PageRevision;
|
use BookStack\Entities\PageRevision;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
|
use DOMElement;
|
||||||
use DOMXPath;
|
use DOMXPath;
|
||||||
|
|
||||||
class PageRepo extends EntityRepo
|
class PageRepo extends EntityRepo
|
||||||
|
@ -129,8 +130,7 @@ class PageRepo extends EntityRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a page's html to be tagged correctly
|
* Formats a page's html to be tagged correctly within the system.
|
||||||
* within the system.
|
|
||||||
* @param string $htmlText
|
* @param string $htmlText
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
@ -139,6 +139,7 @@ class PageRepo extends EntityRepo
|
||||||
if ($htmlText == '') {
|
if ($htmlText == '') {
|
||||||
return $htmlText;
|
return $htmlText;
|
||||||
}
|
}
|
||||||
|
|
||||||
libxml_use_internal_errors(true);
|
libxml_use_internal_errors(true);
|
||||||
$doc = new DOMDocument();
|
$doc = new DOMDocument();
|
||||||
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
|
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
|
||||||
|
@ -147,37 +148,17 @@ class PageRepo extends EntityRepo
|
||||||
$body = $container->childNodes->item(0);
|
$body = $container->childNodes->item(0);
|
||||||
$childNodes = $body->childNodes;
|
$childNodes = $body->childNodes;
|
||||||
|
|
||||||
// Ensure no duplicate ids are used
|
// Set ids on top-level nodes
|
||||||
$idArray = [];
|
$idMap = [];
|
||||||
|
|
||||||
foreach ($childNodes as $index => $childNode) {
|
foreach ($childNodes as $index => $childNode) {
|
||||||
/** @var \DOMElement $childNode */
|
$this->setUniqueId($childNode, $idMap);
|
||||||
if (get_class($childNode) !== 'DOMElement') {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overwrite id if not a BookStack custom id
|
// Ensure no duplicate ids within child items
|
||||||
if ($childNode->hasAttribute('id')) {
|
$xPath = new DOMXPath($doc);
|
||||||
$id = $childNode->getAttribute('id');
|
$idElems = $xPath->query('//body//*//*[@id]');
|
||||||
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
|
foreach ($idElems as $domElem) {
|
||||||
$idArray[] = $id;
|
$this->setUniqueId($domElem, $idMap);
|
||||||
continue;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an unique id for the element
|
|
||||||
// Uses the content as a basis to ensure output is the same every time
|
|
||||||
// the same content is passed through.
|
|
||||||
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
|
|
||||||
$newId = urlencode($contentId);
|
|
||||||
$loopIndex = 0;
|
|
||||||
while (in_array($newId, $idArray)) {
|
|
||||||
$newId = urlencode($contentId . '-' . $loopIndex);
|
|
||||||
$loopIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$childNode->setAttribute('id', $newId);
|
|
||||||
$idArray[] = $newId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate inner html as a string
|
// Generate inner html as a string
|
||||||
|
@ -189,6 +170,41 @@ class PageRepo extends EntityRepo
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a unique id on the given DOMElement.
|
||||||
|
* A map for existing ID's should be passed in to check for current existence.
|
||||||
|
* @param DOMElement $element
|
||||||
|
* @param array $idMap
|
||||||
|
*/
|
||||||
|
protected function setUniqueId($element, array &$idMap)
|
||||||
|
{
|
||||||
|
if (get_class($element) !== 'DOMElement') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite id if not a BookStack custom id
|
||||||
|
$existingId = $element->getAttribute('id');
|
||||||
|
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
|
||||||
|
$idMap[$existingId] = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an unique id for the element
|
||||||
|
// Uses the content as a basis to ensure output is the same every time
|
||||||
|
// the same content is passed through.
|
||||||
|
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
|
||||||
|
$newId = urlencode($contentId);
|
||||||
|
$loopIndex = 0;
|
||||||
|
|
||||||
|
while (isset($idMap[$newId])) {
|
||||||
|
$newId = urlencode($contentId . '-' . $loopIndex);
|
||||||
|
$loopIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$element->setAttribute('id', $newId);
|
||||||
|
$idMap[$newId] = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the plain text version of a page's content.
|
* Get the plain text version of a page's content.
|
||||||
* @param \BookStack\Entities\Page $page
|
* @param \BookStack\Entities\Page $page
|
||||||
|
|
|
@ -128,7 +128,7 @@ class LoginController extends Controller
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
return view('auth.login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -70,7 +70,7 @@ class RegisterController extends Controller
|
||||||
protected function validator(array $data)
|
protected function validator(array $data)
|
||||||
{
|
{
|
||||||
return Validator::make($data, [
|
return Validator::make($data, [
|
||||||
'name' => 'required|max:255',
|
'name' => 'required|min:2|max:255',
|
||||||
'email' => 'required|email|max:255|unique:users',
|
'email' => 'required|email|max:255|unique:users',
|
||||||
'password' => 'required|min:6',
|
'password' => 'required|min:6',
|
||||||
]);
|
]);
|
||||||
|
@ -176,7 +176,7 @@ class RegisterController extends Controller
|
||||||
*/
|
*/
|
||||||
public function getRegisterConfirmation()
|
public function getRegisterConfirmation()
|
||||||
{
|
{
|
||||||
return view('auth/register-confirm');
|
return view('auth.register-confirm');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -204,7 +204,7 @@ class RegisterController extends Controller
|
||||||
*/
|
*/
|
||||||
public function showAwaitingConfirmation()
|
public function showAwaitingConfirmation()
|
||||||
{
|
{
|
||||||
return view('auth/user-unconfirmed');
|
return view('auth.user-unconfirmed');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Auth\UserRepo;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Book;
|
||||||
|
use BookStack\Entities\EntityContextManager;
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
use BookStack\Entities\Repos\EntityRepo;
|
||||||
use BookStack\Entities\ExportService;
|
use BookStack\Entities\ExportService;
|
||||||
|
use BookStack\Uploads\ImageRepo;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Views;
|
use Views;
|
||||||
|
@ -15,18 +17,29 @@ class BookController extends Controller
|
||||||
protected $entityRepo;
|
protected $entityRepo;
|
||||||
protected $userRepo;
|
protected $userRepo;
|
||||||
protected $exportService;
|
protected $exportService;
|
||||||
|
protected $entityContextManager;
|
||||||
|
protected $imageRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BookController constructor.
|
* BookController constructor.
|
||||||
* @param EntityRepo $entityRepo
|
* @param EntityRepo $entityRepo
|
||||||
* @param \BookStack\Auth\UserRepo $userRepo
|
* @param UserRepo $userRepo
|
||||||
* @param \BookStack\Entities\ExportService $exportService
|
* @param ExportService $exportService
|
||||||
|
* @param EntityContextManager $entityContextManager
|
||||||
|
* @param ImageRepo $imageRepo
|
||||||
*/
|
*/
|
||||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
public function __construct(
|
||||||
{
|
EntityRepo $entityRepo,
|
||||||
|
UserRepo $userRepo,
|
||||||
|
ExportService $exportService,
|
||||||
|
EntityContextManager $entityContextManager,
|
||||||
|
ImageRepo $imageRepo
|
||||||
|
) {
|
||||||
$this->entityRepo = $entityRepo;
|
$this->entityRepo = $entityRepo;
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
$this->exportService = $exportService;
|
$this->exportService = $exportService;
|
||||||
|
$this->entityContextManager = $entityContextManager;
|
||||||
|
$this->imageRepo = $imageRepo;
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,67 +49,117 @@ class BookController extends Controller
|
||||||
*/
|
*/
|
||||||
public function index()
|
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;
|
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
|
||||||
$popular = $this->entityRepo->getPopular('book', 4, 0);
|
$popular = $this->entityRepo->getPopular('book', 4, 0);
|
||||||
$new = $this->entityRepo->getRecentlyCreated('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'));
|
$this->setPageTitle(trans('entities.books'));
|
||||||
return view('books/index', [
|
return view('books.index', [
|
||||||
'books' => $books,
|
'books' => $books,
|
||||||
'recents' => $recents,
|
'recents' => $recents,
|
||||||
'popular' => $popular,
|
'popular' => $popular,
|
||||||
'new' => $new,
|
'new' => $new,
|
||||||
'booksViewType' => $booksViewType
|
'view' => $view,
|
||||||
|
'sort' => $sort,
|
||||||
|
'order' => $order,
|
||||||
|
'sortOptions' => $sortOptions,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the form for creating a new book.
|
* Show the form for creating a new book.
|
||||||
|
* @param string $shelfSlug
|
||||||
* @return Response
|
* @return Response
|
||||||
|
* @throws \BookStack\Exceptions\NotFoundException
|
||||||
*/
|
*/
|
||||||
public function create()
|
public function create(string $shelfSlug = null)
|
||||||
{
|
{
|
||||||
|
$bookshelf = null;
|
||||||
|
if ($shelfSlug !== null) {
|
||||||
|
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
|
||||||
|
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
||||||
|
}
|
||||||
|
|
||||||
$this->checkPermission('book-create-all');
|
$this->checkPermission('book-create-all');
|
||||||
$this->setPageTitle(trans('entities.books_create'));
|
$this->setPageTitle(trans('entities.books_create'));
|
||||||
return view('books/create');
|
return view('books.create', [
|
||||||
|
'bookshelf' => $bookshelf
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a newly created book in storage.
|
* Store a newly created book in storage.
|
||||||
*
|
*
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
|
* @param string $shelfSlug
|
||||||
* @return Response
|
* @return Response
|
||||||
|
* @throws \BookStack\Exceptions\NotFoundException
|
||||||
|
* @throws \BookStack\Exceptions\ImageUploadException
|
||||||
*/
|
*/
|
||||||
public function store(Request $request)
|
public function store(Request $request, string $shelfSlug = null)
|
||||||
{
|
{
|
||||||
$this->checkPermission('book-create-all');
|
$this->checkPermission('book-create-all');
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'description' => 'string|max:1000'
|
'description' => 'string|max:1000',
|
||||||
|
'image' => $this->imageRepo->getImageValidationRules(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$bookshelf = null;
|
||||||
|
if ($shelfSlug !== null) {
|
||||||
|
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
|
||||||
|
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
||||||
|
}
|
||||||
|
|
||||||
$book = $this->entityRepo->createFromInput('book', $request->all());
|
$book = $this->entityRepo->createFromInput('book', $request->all());
|
||||||
|
$this->bookUpdateActions($book, $request);
|
||||||
Activity::add($book, 'book_create', $book->id);
|
Activity::add($book, 'book_create', $book->id);
|
||||||
|
|
||||||
|
if ($bookshelf) {
|
||||||
|
$this->entityRepo->appendBookToShelf($bookshelf, $book);
|
||||||
|
Activity::add($bookshelf, 'bookshelf_update');
|
||||||
|
}
|
||||||
|
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the specified book.
|
* Display the specified book.
|
||||||
* @param $slug
|
* @param $slug
|
||||||
|
* @param Request $request
|
||||||
* @return Response
|
* @return Response
|
||||||
|
* @throws \BookStack\Exceptions\NotFoundException
|
||||||
*/
|
*/
|
||||||
public function show($slug)
|
public function show($slug, Request $request)
|
||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||||
$this->checkOwnablePermission('book-view', $book);
|
$this->checkOwnablePermission('book-view', $book);
|
||||||
|
|
||||||
$bookChildren = $this->entityRepo->getBookChildren($book);
|
$bookChildren = $this->entityRepo->getBookChildren($book);
|
||||||
|
|
||||||
Views::add($book);
|
Views::add($book);
|
||||||
|
if ($request->has('shelf')) {
|
||||||
|
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
|
||||||
|
}
|
||||||
|
|
||||||
$this->setPageTitle($book->getShortName());
|
$this->setPageTitle($book->getShortName());
|
||||||
return view('books/show', [
|
return view('books.show', [
|
||||||
'book' => $book,
|
'book' => $book,
|
||||||
'current' => $book,
|
'current' => $book,
|
||||||
'bookChildren' => $bookChildren,
|
'bookChildren' => $bookChildren,
|
||||||
'activity' => Activity::entityActivity($book, 20, 0)
|
'activity' => Activity::entityActivity($book, 20, 1)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +173,7 @@ class BookController extends Controller
|
||||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||||
$this->checkOwnablePermission('book-update', $book);
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
|
$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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,17 +181,24 @@ class BookController extends Controller
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param $slug
|
* @param $slug
|
||||||
* @return Response
|
* @return Response
|
||||||
|
* @throws \BookStack\Exceptions\ImageUploadException
|
||||||
|
* @throws \BookStack\Exceptions\NotFoundException
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, $slug)
|
public function update(Request $request, string $slug)
|
||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||||
$this->checkOwnablePermission('book-update', $book);
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'description' => 'string|max:1000'
|
'description' => 'string|max:1000',
|
||||||
|
'image' => $this->imageRepo->getImageValidationRules(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
|
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
|
||||||
|
$this->bookUpdateActions($book, $request);
|
||||||
|
|
||||||
Activity::add($book, 'book_update', $book->id);
|
Activity::add($book, 'book_update', $book->id);
|
||||||
|
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,22 +212,24 @@ class BookController extends Controller
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('book-delete', $book);
|
$this->checkOwnablePermission('book-delete', $book);
|
||||||
$this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
|
$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.
|
* Shows the view which allows pages to be re-ordered and sorted.
|
||||||
* @param string $bookSlug
|
* @param string $bookSlug
|
||||||
* @return \Illuminate\View\View
|
* @return \Illuminate\View\View
|
||||||
|
* @throws \BookStack\Exceptions\NotFoundException
|
||||||
*/
|
*/
|
||||||
public function sort($bookSlug)
|
public function sort($bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('book-update', $book);
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
|
|
||||||
$bookChildren = $this->entityRepo->getBookChildren($book, true);
|
$bookChildren = $this->entityRepo->getBookChildren($book, true);
|
||||||
$books = $this->entityRepo->getAll('book', false, 'update');
|
|
||||||
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
|
$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 +242,7 @@ class BookController extends Controller
|
||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$bookChildren = $this->entityRepo->getBookChildren($book);
|
$bookChildren = $this->entityRepo->getBookChildren($book);
|
||||||
return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
|
return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -254,7 +326,12 @@ class BookController extends Controller
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('book-delete', $book);
|
$this->checkOwnablePermission('book-delete', $book);
|
||||||
Activity::addMessage('book_delete', 0, $book->name);
|
Activity::addMessage('book_delete', 0, $book->name);
|
||||||
|
|
||||||
|
if ($book->cover) {
|
||||||
|
$this->imageRepo->destroyImage($book->cover);
|
||||||
|
}
|
||||||
$this->entityRepo->destroyBook($book);
|
$this->entityRepo->destroyBook($book);
|
||||||
|
|
||||||
return redirect('/books');
|
return redirect('/books');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,12 +340,12 @@ class BookController extends Controller
|
||||||
* @param $bookSlug
|
* @param $bookSlug
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function showRestrict($bookSlug)
|
public function showPermissions($bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||||
$roles = $this->userRepo->getRestrictableRoles();
|
$roles = $this->userRepo->getRestrictableRoles();
|
||||||
return view('books/restrictions', [
|
return view('books.permissions', [
|
||||||
'book' => $book,
|
'book' => $book,
|
||||||
'roles' => $roles
|
'roles' => $roles
|
||||||
]);
|
]);
|
||||||
|
@ -277,11 +354,12 @@ class BookController extends Controller
|
||||||
/**
|
/**
|
||||||
* Set the restrictions for this book.
|
* Set the restrictions for this book.
|
||||||
* @param $bookSlug
|
* @param $bookSlug
|
||||||
* @param $bookSlug
|
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @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);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||||
|
@ -325,4 +403,29 @@ class BookController extends Controller
|
||||||
$textContent = $this->exportService->bookToPlainText($book);
|
$textContent = $this->exportService->bookToPlainText($book);
|
||||||
return $this->downloadResponse($textContent, $bookSlug . '.txt');
|
return $this->downloadResponse($textContent, $bookSlug . '.txt');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common actions to run on book update.
|
||||||
|
* Handles updating the cover image.
|
||||||
|
* @param Book $book
|
||||||
|
* @param Request $request
|
||||||
|
* @throws \BookStack\Exceptions\ImageUploadException
|
||||||
|
*/
|
||||||
|
protected function bookUpdateActions(Book $book, Request $request)
|
||||||
|
{
|
||||||
|
// Update the cover image if in request
|
||||||
|
if ($request->has('image')) {
|
||||||
|
$this->imageRepo->destroyImage($book->cover);
|
||||||
|
$newImage = $request->file('image');
|
||||||
|
$image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
|
||||||
|
$book->image_id = $image->id;
|
||||||
|
$book->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('image_reset')) {
|
||||||
|
$this->imageRepo->destroyImage($book->cover);
|
||||||
|
$book->image_id = 0;
|
||||||
|
$book->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Auth\UserRepo;
|
||||||
use BookStack\Entities\Bookshelf;
|
use BookStack\Entities\Bookshelf;
|
||||||
|
use BookStack\Entities\EntityContextManager;
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
use BookStack\Entities\Repos\EntityRepo;
|
||||||
use BookStack\Entities\ExportService;
|
use BookStack\Uploads\ImageRepo;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Views;
|
use Views;
|
||||||
|
@ -14,19 +15,22 @@ class BookshelfController extends Controller
|
||||||
|
|
||||||
protected $entityRepo;
|
protected $entityRepo;
|
||||||
protected $userRepo;
|
protected $userRepo;
|
||||||
protected $exportService;
|
protected $entityContextManager;
|
||||||
|
protected $imageRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BookController constructor.
|
* BookController constructor.
|
||||||
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
|
* @param EntityRepo $entityRepo
|
||||||
* @param UserRepo $userRepo
|
* @param UserRepo $userRepo
|
||||||
* @param \BookStack\Entities\ExportService $exportService
|
* @param EntityContextManager $entityContextManager
|
||||||
|
* @param ImageRepo $imageRepo
|
||||||
*/
|
*/
|
||||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo)
|
||||||
{
|
{
|
||||||
$this->entityRepo = $entityRepo;
|
$this->entityRepo = $entityRepo;
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
$this->exportService = $exportService;
|
$this->entityContextManager = $entityContextManager;
|
||||||
|
$this->imageRepo = $imageRepo;
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,19 +40,35 @@ class BookshelfController extends Controller
|
||||||
*/
|
*/
|
||||||
public function index()
|
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;
|
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
|
||||||
$popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
|
$popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
|
||||||
$new = $this->entityRepo->getRecentlyCreated('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'));
|
$this->setPageTitle(trans('entities.shelves'));
|
||||||
return view('shelves/index', [
|
return view('shelves.index', [
|
||||||
'shelves' => $shelves,
|
'shelves' => $shelves,
|
||||||
'recents' => $recents,
|
'recents' => $recents,
|
||||||
'popular' => $popular,
|
'popular' => $popular,
|
||||||
'new' => $new,
|
'new' => $new,
|
||||||
'shelvesViewType' => $shelvesViewType
|
'view' => $view,
|
||||||
|
'sort' => $sort,
|
||||||
|
'order' => $order,
|
||||||
|
'sortOptions' => $sortOptions,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,13 +81,14 @@ class BookshelfController extends Controller
|
||||||
$this->checkPermission('bookshelf-create-all');
|
$this->checkPermission('bookshelf-create-all');
|
||||||
$books = $this->entityRepo->getAll('book', false, 'update');
|
$books = $this->entityRepo->getAll('book', false, 'update');
|
||||||
$this->setPageTitle(trans('entities.shelves_create'));
|
$this->setPageTitle(trans('entities.shelves_create'));
|
||||||
return view('shelves/create', ['books' => $books]);
|
return view('shelves.create', ['books' => $books]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a newly created bookshelf in storage.
|
* Store a newly created bookshelf in storage.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return Response
|
* @return Response
|
||||||
|
* @throws \BookStack\Exceptions\ImageUploadException
|
||||||
*/
|
*/
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
|
@ -75,13 +96,14 @@ class BookshelfController extends Controller
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'description' => 'string|max:1000',
|
'description' => 'string|max:1000',
|
||||||
|
'image' => $this->imageRepo->getImageValidationRules(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
|
$shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
|
||||||
$this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', ''));
|
$this->shelfUpdateActions($shelf, $request);
|
||||||
Activity::add($bookshelf, 'bookshelf_create');
|
|
||||||
|
|
||||||
return redirect($bookshelf->getUrl());
|
Activity::add($shelf, 'bookshelf_create');
|
||||||
|
return redirect($shelf->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,17 +115,20 @@ class BookshelfController extends Controller
|
||||||
*/
|
*/
|
||||||
public function show(string $slug)
|
public function show(string $slug)
|
||||||
{
|
{
|
||||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
/** @var Bookshelf $shelf */
|
||||||
$this->checkOwnablePermission('book-view', $bookshelf);
|
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||||
|
$this->checkOwnablePermission('book-view', $shelf);
|
||||||
|
|
||||||
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
|
$books = $this->entityRepo->getBookshelfChildren($shelf);
|
||||||
Views::add($bookshelf);
|
Views::add($shelf);
|
||||||
|
$this->entityContextManager->setShelfContext($shelf->id);
|
||||||
|
|
||||||
$this->setPageTitle($bookshelf->getShortName());
|
$this->setPageTitle($shelf->getShortName());
|
||||||
return view('shelves/show', [
|
|
||||||
'shelf' => $bookshelf,
|
return view('shelves.show', [
|
||||||
|
'shelf' => $shelf,
|
||||||
'books' => $books,
|
'books' => $books,
|
||||||
'activity' => Activity::entityActivity($bookshelf, 20, 0)
|
'activity' => Activity::entityActivity($shelf, 20, 1)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,19 +140,19 @@ class BookshelfController extends Controller
|
||||||
*/
|
*/
|
||||||
public function edit(string $slug)
|
public function edit(string $slug)
|
||||||
{
|
{
|
||||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
|
||||||
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||||
|
|
||||||
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
|
$shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
|
||||||
$shelfBookIds = $shelfBooks->pluck('id');
|
$shelfBookIds = $shelfBooks->pluck('id');
|
||||||
$books = $this->entityRepo->getAll('book', false, 'update');
|
$books = $this->entityRepo->getAll('book', false, 'update');
|
||||||
$books = $books->filter(function ($book) use ($shelfBookIds) {
|
$books = $books->filter(function ($book) use ($shelfBookIds) {
|
||||||
return !$shelfBookIds->contains($book->id);
|
return !$shelfBookIds->contains($book->id);
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()]));
|
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
|
||||||
return view('shelves/edit', [
|
return view('shelves.edit', [
|
||||||
'shelf' => $bookshelf,
|
'shelf' => $shelf,
|
||||||
'books' => $books,
|
'books' => $books,
|
||||||
'shelfBooks' => $shelfBooks,
|
'shelfBooks' => $shelfBooks,
|
||||||
]);
|
]);
|
||||||
|
@ -140,6 +165,7 @@ class BookshelfController extends Controller
|
||||||
* @param string $slug
|
* @param string $slug
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
* @throws \BookStack\Exceptions\NotFoundException
|
||||||
|
* @throws \BookStack\Exceptions\ImageUploadException
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, string $slug)
|
public function update(Request $request, string $slug)
|
||||||
{
|
{
|
||||||
|
@ -148,10 +174,12 @@ class BookshelfController extends Controller
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'description' => 'string|max:1000',
|
'description' => 'string|max:1000',
|
||||||
|
'image' => $this->imageRepo->getImageValidationRules(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
|
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
|
||||||
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
|
$this->shelfUpdateActions($shelf, $request);
|
||||||
|
|
||||||
Activity::add($shelf, 'bookshelf_update');
|
Activity::add($shelf, 'bookshelf_update');
|
||||||
|
|
||||||
return redirect($shelf->getUrl());
|
return redirect($shelf->getUrl());
|
||||||
|
@ -166,11 +194,11 @@ class BookshelfController extends Controller
|
||||||
*/
|
*/
|
||||||
public function showDelete(string $slug)
|
public function showDelete(string $slug)
|
||||||
{
|
{
|
||||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
|
||||||
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
|
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
||||||
|
|
||||||
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
|
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
|
||||||
return view('shelves/delete', ['shelf' => $bookshelf]);
|
return view('shelves.delete', ['shelf' => $shelf]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -182,46 +210,52 @@ class BookshelfController extends Controller
|
||||||
*/
|
*/
|
||||||
public function destroy(string $slug)
|
public function destroy(string $slug)
|
||||||
{
|
{
|
||||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
|
||||||
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
|
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
||||||
Activity::addMessage('bookshelf_delete', 0, $bookshelf->name);
|
Activity::addMessage('bookshelf_delete', 0, $shelf->name);
|
||||||
$this->entityRepo->destroyBookshelf($bookshelf);
|
|
||||||
|
if ($shelf->cover) {
|
||||||
|
$this->imageRepo->destroyImage($shelf->cover);
|
||||||
|
}
|
||||||
|
$this->entityRepo->destroyBookshelf($shelf);
|
||||||
|
|
||||||
return redirect('/shelves');
|
return redirect('/shelves');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the Restrictions view.
|
* Show the permissions view.
|
||||||
* @param $slug
|
* @param string $slug
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
* @throws \BookStack\Exceptions\NotFoundException
|
||||||
*/
|
*/
|
||||||
public function showRestrict(string $slug)
|
public function showPermissions(string $slug)
|
||||||
{
|
{
|
||||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||||
|
|
||||||
$roles = $this->userRepo->getRestrictableRoles();
|
$roles = $this->userRepo->getRestrictableRoles();
|
||||||
return view('shelves.restrictions', [
|
return view('shelves.permissions', [
|
||||||
'shelf' => $bookshelf,
|
'shelf' => $shelf,
|
||||||
'roles' => $roles
|
'roles' => $roles
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the restrictions for this bookshelf.
|
* Set the permissions for this bookshelf.
|
||||||
* @param $slug
|
* @param string $slug
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
* @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);
|
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||||
|
|
||||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
|
$this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
|
||||||
session()->flash('success', trans('entities.shelves_permissions_updated'));
|
session()->flash('success', trans('entities.shelves_permissions_updated'));
|
||||||
return redirect($bookshelf->getUrl());
|
return redirect($shelf->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -232,11 +266,38 @@ class BookshelfController extends Controller
|
||||||
*/
|
*/
|
||||||
public function copyPermissions(string $slug)
|
public function copyPermissions(string $slug)
|
||||||
{
|
{
|
||||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||||
|
|
||||||
$updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
|
$updateCount = $this->entityRepo->copyBookshelfPermissions($shelf);
|
||||||
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
|
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
|
||||||
return redirect($bookshelf->getUrl());
|
return redirect($shelf->getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common actions to run on bookshelf update.
|
||||||
|
* @param Bookshelf $shelf
|
||||||
|
* @param Request $request
|
||||||
|
* @throws \BookStack\Exceptions\ImageUploadException
|
||||||
|
*/
|
||||||
|
protected function shelfUpdateActions(Bookshelf $shelf, Request $request)
|
||||||
|
{
|
||||||
|
// Update the books that the shelf references
|
||||||
|
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
|
||||||
|
|
||||||
|
// Update the cover image if in request
|
||||||
|
if ($request->has('image')) {
|
||||||
|
$newImage = $request->file('image');
|
||||||
|
$this->imageRepo->destroyImage($shelf->cover);
|
||||||
|
$image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true);
|
||||||
|
$shelf->image_id = $image->id;
|
||||||
|
$shelf->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('image_reset')) {
|
||||||
|
$this->imageRepo->destroyImage($shelf->cover);
|
||||||
|
$shelf->image_id = 0;
|
||||||
|
$shelf->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ class ChapterController extends Controller
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('chapter-create', $book);
|
$this->checkOwnablePermission('chapter-create', $book);
|
||||||
$this->setPageTitle(trans('entities.chapters_create'));
|
$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);
|
Views::add($chapter);
|
||||||
$this->setPageTitle($chapter->getShortName());
|
$this->setPageTitle($chapter->getShortName());
|
||||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||||
return view('chapters/show', [
|
return view('chapters.show', [
|
||||||
'book' => $chapter->book,
|
'book' => $chapter->book,
|
||||||
'chapter' => $chapter,
|
'chapter' => $chapter,
|
||||||
'current' => $chapter,
|
'current' => $chapter,
|
||||||
|
@ -98,7 +98,7 @@ class ChapterController extends Controller
|
||||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
|
$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);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
|
$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->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
|
||||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
return view('chapters/move', [
|
return view('chapters.move', [
|
||||||
'chapter' => $chapter,
|
'chapter' => $chapter,
|
||||||
'book' => $chapter->book
|
'book' => $chapter->book
|
||||||
]);
|
]);
|
||||||
|
@ -214,13 +214,14 @@ class ChapterController extends Controller
|
||||||
* @param $bookSlug
|
* @param $bookSlug
|
||||||
* @param $chapterSlug
|
* @param $chapterSlug
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
* @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);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||||
$roles = $this->userRepo->getRestrictableRoles();
|
$roles = $this->userRepo->getRestrictableRoles();
|
||||||
return view('chapters/restrictions', [
|
return view('chapters.permissions', [
|
||||||
'chapter' => $chapter,
|
'chapter' => $chapter,
|
||||||
'roles' => $roles
|
'roles' => $roles
|
||||||
]);
|
]);
|
||||||
|
@ -232,8 +233,10 @@ class ChapterController extends Controller
|
||||||
* @param $chapterSlug
|
* @param $chapterSlug
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @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);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||||
|
|
|
@ -54,7 +54,7 @@ class CommentController extends Controller
|
||||||
$this->checkPermission('comment-create-all');
|
$this->checkPermission('comment-create-all');
|
||||||
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
|
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
|
||||||
Activity::add($page, 'commented_on', $page->book->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);
|
$this->checkOwnablePermission('comment-update', $comment);
|
||||||
|
|
||||||
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
|
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
|
||||||
return view('comments/comment', ['comment' => $comment]);
|
return view('comments.comment', ['comment' => $comment]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -123,6 +123,20 @@ abstract class Controller extends BaseController
|
||||||
return true;
|
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.
|
* Send back a json error message.
|
||||||
* @param string $messageText
|
* @param string $messageText
|
||||||
|
|
|
@ -19,7 +19,6 @@ class HomeController extends Controller
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the homepage.
|
* Display the homepage.
|
||||||
* @return Response
|
* @return Response
|
||||||
|
@ -45,17 +44,36 @@ class HomeController extends Controller
|
||||||
'draftPages' => $draftPages,
|
'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') {
|
if ($homepageOption === 'bookshelves') {
|
||||||
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
|
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $commonData['sort'], $commonData['order']);
|
||||||
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
|
$data = array_merge($commonData, ['shelves' => $shelves]);
|
||||||
$data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]);
|
|
||||||
return view('common.home-shelves', $data);
|
return view('common.home-shelves', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($homepageOption === 'books') {
|
if ($homepageOption === 'books') {
|
||||||
$books = $this->entityRepo->getAllPaginated('book', 18);
|
$books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
|
||||||
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
|
$data = array_merge($commonData, ['books' => $books]);
|
||||||
$data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]);
|
|
||||||
return view('common.home-book', $data);
|
return view('common.home-book', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +123,7 @@ class HomeController extends Controller
|
||||||
*/
|
*/
|
||||||
public function customHeadContent()
|
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;
|
$allowRobots = $sitePublic;
|
||||||
}
|
}
|
||||||
return response()
|
return response()
|
||||||
->view('common/robots', ['allowRobots' => $allowRobots])
|
->view('common.robots', ['allowRobots' => $allowRobots])
|
||||||
->header('Content-Type', 'text/plain');
|
->header('Content-Type', 'text/plain');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +147,6 @@ class HomeController extends Controller
|
||||||
*/
|
*/
|
||||||
public function getNotFound()
|
public function getNotFound()
|
||||||
{
|
{
|
||||||
return response()->view('errors/404', [], 404);
|
return response()->view('errors.404', [], 404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,246 +0,0 @@
|
||||||
<?php namespace BookStack\Http\Controllers;
|
|
||||||
|
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
|
||||||
use BookStack\Repos\PageRepo;
|
|
||||||
use BookStack\Uploads\Image;
|
|
||||||
use BookStack\Uploads\ImageRepo;
|
|
||||||
use Illuminate\Filesystem\Filesystem as File;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class ImageController extends Controller
|
|
||||||
{
|
|
||||||
protected $image;
|
|
||||||
protected $file;
|
|
||||||
protected $imageRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ImageController constructor.
|
|
||||||
* @param Image $image
|
|
||||||
* @param File $file
|
|
||||||
* @param ImageRepo $imageRepo
|
|
||||||
*/
|
|
||||||
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
|
|
||||||
{
|
|
||||||
$this->image = $image;
|
|
||||||
$this->file = $file;
|
|
||||||
$this->imageRepo = $imageRepo;
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide an image file from storage.
|
|
||||||
* @param string $path
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function showImage(string $path)
|
|
||||||
{
|
|
||||||
$path = storage_path('uploads/images/' . $path);
|
|
||||||
if (!file_exists($path)) {
|
|
||||||
abort(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->file($path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all images for a specific type, Paginated
|
|
||||||
* @param string $type
|
|
||||||
* @param int $page
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*/
|
|
||||||
public function getAllByType($type, $page = 0)
|
|
||||||
{
|
|
||||||
$imgData = $this->imageRepo->getPaginatedByType($type, $page);
|
|
||||||
return response()->json($imgData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search through images within a particular type.
|
|
||||||
* @param $type
|
|
||||||
* @param int $page
|
|
||||||
* @param Request $request
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function searchByType(Request $request, $type, $page = 0)
|
|
||||||
{
|
|
||||||
$this->validate($request, [
|
|
||||||
'term' => 'required|string'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$searchTerm = $request->get('term');
|
|
||||||
$imgData = $this->imageRepo->searchPaginatedByType($type, $searchTerm, $page, 24);
|
|
||||||
return response()->json($imgData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all images for a user.
|
|
||||||
* @param int $page
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*/
|
|
||||||
public function getAllForUserType($page = 0)
|
|
||||||
{
|
|
||||||
$imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $this->currentUser->id);
|
|
||||||
return response()->json($imgData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get gallery images with a specific filter such as book or page
|
|
||||||
* @param $filter
|
|
||||||
* @param int $page
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
|
||||||
*/
|
|
||||||
public function getGalleryFiltered(Request $request, $filter, $page = 0)
|
|
||||||
{
|
|
||||||
$this->validate($request, [
|
|
||||||
'page_id' => 'required|integer'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$validFilters = collect(['page', 'book']);
|
|
||||||
if (!$validFilters->contains($filter)) {
|
|
||||||
return response('Invalid filter', 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pageId = $request->get('page_id');
|
|
||||||
$imgData = $this->imageRepo->getGalleryFiltered(strtolower($filter), $pageId, $page, 24);
|
|
||||||
|
|
||||||
return response()->json($imgData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles image uploads for use on pages.
|
|
||||||
* @param string $type
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function uploadByType($type, Request $request)
|
|
||||||
{
|
|
||||||
$this->checkPermission('image-create-all');
|
|
||||||
$this->validate($request, [
|
|
||||||
'file' => 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff'
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$this->imageRepo->isValidType($type)) {
|
|
||||||
return $this->jsonError(trans('errors.image_upload_type_error'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$imageUpload = $request->file('file');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$uploadedTo = $request->get('uploaded_to', 0);
|
|
||||||
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
|
|
||||||
} catch (ImageUploadException $e) {
|
|
||||||
return response($e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json($image);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload a drawing to the system.
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
|
||||||
*/
|
|
||||||
public function uploadDrawing(Request $request)
|
|
||||||
{
|
|
||||||
$this->validate($request, [
|
|
||||||
'image' => 'required|string',
|
|
||||||
'uploaded_to' => 'required|integer'
|
|
||||||
]);
|
|
||||||
$this->checkPermission('image-create-all');
|
|
||||||
$imageBase64Data = $request->get('image');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$uploadedTo = $request->get('uploaded_to', 0);
|
|
||||||
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
|
|
||||||
} catch (ImageUploadException $e) {
|
|
||||||
return response($e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json($image);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the content of an image based64 encoded.
|
|
||||||
* @param $id
|
|
||||||
* @return \Illuminate\Http\JsonResponse|mixed
|
|
||||||
*/
|
|
||||||
public function getBase64Image($id)
|
|
||||||
{
|
|
||||||
$image = $this->imageRepo->getById($id);
|
|
||||||
$imageData = $this->imageRepo->getImageData($image);
|
|
||||||
if ($imageData === null) {
|
|
||||||
return $this->jsonError("Image data could not be found");
|
|
||||||
}
|
|
||||||
return response()->json([
|
|
||||||
'content' => base64_encode($imageData)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a sized thumbnail for an image.
|
|
||||||
* @param $id
|
|
||||||
* @param $width
|
|
||||||
* @param $height
|
|
||||||
* @param $crop
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
* @throws ImageUploadException
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function getThumbnail($id, $width, $height, $crop)
|
|
||||||
{
|
|
||||||
$this->checkPermission('image-create-all');
|
|
||||||
$image = $this->imageRepo->getById($id);
|
|
||||||
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
|
|
||||||
return response()->json(['url' => $thumbnailUrl]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update image details
|
|
||||||
* @param integer $imageId
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
* @throws ImageUploadException
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function update($imageId, Request $request)
|
|
||||||
{
|
|
||||||
$this->validate($request, [
|
|
||||||
'name' => 'required|min:2|string'
|
|
||||||
]);
|
|
||||||
$image = $this->imageRepo->getById($imageId);
|
|
||||||
$this->checkOwnablePermission('image-update', $image);
|
|
||||||
$image = $this->imageRepo->updateImageDetails($image, $request->all());
|
|
||||||
return response()->json($image);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the usage of an image on pages.
|
|
||||||
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
|
|
||||||
* @param $id
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*/
|
|
||||||
public function usage(EntityRepo $entityRepo, $id)
|
|
||||||
{
|
|
||||||
$image = $this->imageRepo->getById($id);
|
|
||||||
$pageSearch = $entityRepo->searchForImage($image->url);
|
|
||||||
return response()->json($pageSearch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an image and all thumbnail/image files
|
|
||||||
* @param int $id
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function destroy($id)
|
|
||||||
{
|
|
||||||
$image = $this->imageRepo->getById($id);
|
|
||||||
$this->checkOwnablePermission('image-delete', $image);
|
|
||||||
|
|
||||||
$this->imageRepo->destroyImage($image);
|
|
||||||
return response()->json(trans('components.images_deleted'));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Http\Controllers\Images;
|
||||||
|
|
||||||
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
|
use BookStack\Uploads\ImageRepo;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use BookStack\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class DrawioImageController extends Controller
|
||||||
|
{
|
||||||
|
protected $imageRepo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DrawioImageController constructor.
|
||||||
|
* @param ImageRepo $imageRepo
|
||||||
|
*/
|
||||||
|
public function __construct(ImageRepo $imageRepo)
|
||||||
|
{
|
||||||
|
$this->imageRepo = $imageRepo;
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of gallery images, in a list.
|
||||||
|
* Can be paged and filtered by entity.
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function list(Request $request)
|
||||||
|
{
|
||||||
|
$page = $request->get('page', 1);
|
||||||
|
$searchTerm = $request->get('search', null);
|
||||||
|
$uploadedToFilter = $request->get('uploaded_to', null);
|
||||||
|
$parentTypeFilter = $request->get('filter_type', null);
|
||||||
|
|
||||||
|
$imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
|
||||||
|
return response()->json($imgData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a new gallery image in the system.
|
||||||
|
* @param Request $request
|
||||||
|
* @return Illuminate\Http\JsonResponse
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'image' => 'required|string',
|
||||||
|
'uploaded_to' => 'required|integer'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->checkPermission('image-create-all');
|
||||||
|
$imageBase64Data = $request->get('image');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$uploadedTo = $request->get('uploaded_to', 0);
|
||||||
|
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
|
||||||
|
} catch (ImageUploadException $e) {
|
||||||
|
return response($e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the content of an image based64 encoded.
|
||||||
|
* @param $id
|
||||||
|
* @return \Illuminate\Http\JsonResponse|mixed
|
||||||
|
*/
|
||||||
|
public function getAsBase64($id)
|
||||||
|
{
|
||||||
|
$image = $this->imageRepo->getById($id);
|
||||||
|
$page = $image->getPage();
|
||||||
|
if ($image === null || $image->type !== 'drawio' || !userCan('page-view', $page)) {
|
||||||
|
return $this->jsonError("Image data could not be found");
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageData = $this->imageRepo->getImageData($image);
|
||||||
|
if ($imageData === null) {
|
||||||
|
return $this->jsonError("Image data could not be found");
|
||||||
|
}
|
||||||
|
return response()->json([
|
||||||
|
'content' => base64_encode($imageData)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Http\Controllers\Images;
|
||||||
|
|
||||||
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
|
use BookStack\Uploads\ImageRepo;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use BookStack\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class GalleryImageController extends Controller
|
||||||
|
{
|
||||||
|
protected $imageRepo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GalleryImageController constructor.
|
||||||
|
* @param ImageRepo $imageRepo
|
||||||
|
*/
|
||||||
|
public function __construct(ImageRepo $imageRepo)
|
||||||
|
{
|
||||||
|
$this->imageRepo = $imageRepo;
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of gallery images, in a list.
|
||||||
|
* Can be paged and filtered by entity.
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function list(Request $request)
|
||||||
|
{
|
||||||
|
$page = $request->get('page', 1);
|
||||||
|
$searchTerm = $request->get('search', null);
|
||||||
|
$uploadedToFilter = $request->get('uploaded_to', null);
|
||||||
|
$parentTypeFilter = $request->get('filter_type', null);
|
||||||
|
|
||||||
|
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
|
||||||
|
return response()->json($imgData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a new gallery image in the system.
|
||||||
|
* @param Request $request
|
||||||
|
* @return Illuminate\Http\JsonResponse
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$this->checkPermission('image-create-all');
|
||||||
|
$this->validate($request, [
|
||||||
|
'file' => $this->imageRepo->getImageValidationRules()
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$imageUpload = $request->file('file');
|
||||||
|
$uploadedTo = $request->get('uploaded_to', 0);
|
||||||
|
$image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo);
|
||||||
|
} catch (ImageUploadException $e) {
|
||||||
|
return response($e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($image);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
<?php namespace BookStack\Http\Controllers\Images;
|
||||||
|
|
||||||
|
use BookStack\Entities\Repos\EntityRepo;
|
||||||
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
|
use BookStack\Http\Controllers\Controller;
|
||||||
|
use BookStack\Repos\PageRepo;
|
||||||
|
use BookStack\Uploads\Image;
|
||||||
|
use BookStack\Uploads\ImageRepo;
|
||||||
|
use Illuminate\Filesystem\Filesystem as File;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ImageController extends Controller
|
||||||
|
{
|
||||||
|
protected $image;
|
||||||
|
protected $file;
|
||||||
|
protected $imageRepo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ImageController constructor.
|
||||||
|
* @param Image $image
|
||||||
|
* @param File $file
|
||||||
|
* @param ImageRepo $imageRepo
|
||||||
|
*/
|
||||||
|
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
|
||||||
|
{
|
||||||
|
$this->image = $image;
|
||||||
|
$this->file = $file;
|
||||||
|
$this->imageRepo = $imageRepo;
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide an image file from storage.
|
||||||
|
* @param string $path
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function showImage(string $path)
|
||||||
|
{
|
||||||
|
$path = storage_path('uploads/images/' . $path);
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->file($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update image details
|
||||||
|
* @param integer $id
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
* @throws ImageUploadException
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function update($id, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'name' => 'required|min:2|string'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$image = $this->imageRepo->getById($id);
|
||||||
|
$this->checkImagePermission($image);
|
||||||
|
$this->checkOwnablePermission('image-update', $image);
|
||||||
|
|
||||||
|
$image = $this->imageRepo->updateImageDetails($image, $request->all());
|
||||||
|
return response()->json($image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the usage of an image on pages.
|
||||||
|
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
|
||||||
|
* @param $id
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function usage(EntityRepo $entityRepo, $id)
|
||||||
|
{
|
||||||
|
$image = $this->imageRepo->getById($id);
|
||||||
|
$this->checkImagePermission($image);
|
||||||
|
$pageSearch = $entityRepo->searchForImage($image->url);
|
||||||
|
return response()->json($pageSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an image and all thumbnail/image files
|
||||||
|
* @param int $id
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$image = $this->imageRepo->getById($id);
|
||||||
|
$this->checkOwnablePermission('image-delete', $image);
|
||||||
|
$this->checkImagePermission($image);
|
||||||
|
|
||||||
|
$this->imageRepo->destroyImage($image);
|
||||||
|
return response()->json(trans('components.images_deleted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check related page permission and ensure type is drawio or gallery.
|
||||||
|
* @param Image $image
|
||||||
|
*/
|
||||||
|
protected function checkImagePermission(Image $image)
|
||||||
|
{
|
||||||
|
if ($image->type !== 'drawio' && $image->type !== 'gallery') {
|
||||||
|
$this->showPermissionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
$relatedPage = $image->getPage();
|
||||||
|
if ($relatedPage) {
|
||||||
|
$this->checkOwnablePermission('page-view', $relatedPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,7 +61,7 @@ class PageController extends Controller
|
||||||
|
|
||||||
// Otherwise show the edit view if they're a guest
|
// Otherwise show the edit view if they're a guest
|
||||||
$this->setPageTitle(trans('entities.pages_new'));
|
$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'));
|
$this->setPageTitle(trans('entities.pages_edit_draft'));
|
||||||
|
|
||||||
$draftsEnabled = $this->signedIn;
|
$draftsEnabled = $this->signedIn;
|
||||||
return view('pages/edit', [
|
return view('pages.edit', [
|
||||||
'page' => $draft,
|
'page' => $draft,
|
||||||
'book' => $draft->book,
|
'book' => $draft->book,
|
||||||
'isDraft' => true,
|
'isDraft' => true,
|
||||||
|
@ -184,7 +184,7 @@ class PageController extends Controller
|
||||||
|
|
||||||
Views::add($page);
|
Views::add($page);
|
||||||
$this->setPageTitle($page->getShortName());
|
$this->setPageTitle($page->getShortName());
|
||||||
return view('pages/show', [
|
return view('pages.show', [
|
||||||
'page' => $page,'book' => $page->book,
|
'page' => $page,'book' => $page->book,
|
||||||
'current' => $page,
|
'current' => $page,
|
||||||
'sidebarTree' => $sidebarTree,
|
'sidebarTree' => $sidebarTree,
|
||||||
|
@ -239,7 +239,7 @@ class PageController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
$draftsEnabled = $this->signedIn;
|
$draftsEnabled = $this->signedIn;
|
||||||
return view('pages/edit', [
|
return view('pages.edit', [
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'book' => $page->book,
|
'book' => $page->book,
|
||||||
'current' => $page,
|
'current' => $page,
|
||||||
|
@ -317,7 +317,7 @@ class PageController extends Controller
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('page-delete', $page);
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
|
$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);
|
$page = $this->pageRepo->getById('page', $pageId, true);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
|
$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 $bookSlug
|
||||||
* @param string $pageSlug
|
* @param string $pageSlug
|
||||||
* @return \Illuminate\View\View
|
* @return \Illuminate\View\View
|
||||||
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
public function showRevisions($bookSlug, $pageSlug)
|
public function showRevisions($bookSlug, $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||||
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
|
$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());
|
$page->fill($revision->toArray());
|
||||||
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
|
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
|
||||||
|
|
||||||
return view('pages/revision', [
|
return view('pages.revision', [
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'book' => $page->book,
|
'book' => $page->book,
|
||||||
|
'diff' => null,
|
||||||
'revision' => $revision
|
'revision' => $revision
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -432,7 +434,7 @@ class PageController extends Controller
|
||||||
$page->fill($revision->toArray());
|
$page->fill($revision->toArray());
|
||||||
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
|
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
|
||||||
|
|
||||||
return view('pages/revision', [
|
return view('pages.revision', [
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'book' => $page->book,
|
'book' => $page->book,
|
||||||
'diff' => $diff,
|
'diff' => $diff,
|
||||||
|
@ -482,12 +484,12 @@ class PageController extends Controller
|
||||||
// Check if its the latest revision, cannot delete latest revision.
|
// Check if its the latest revision, cannot delete latest revision.
|
||||||
if (intval($currentRevision->id) === intval($revId)) {
|
if (intval($currentRevision->id) === intval($revId)) {
|
||||||
session()->flash('error', trans('entities.revision_cannot_delete_latest'));
|
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();
|
$revision->delete();
|
||||||
session()->flash('success', trans('entities.revision_delete_success'));
|
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');
|
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
|
* Show a listing of recently created pages
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function showRecentlyUpdated()
|
public function showRecentlyUpdated()
|
||||||
{
|
{
|
||||||
|
// TODO - Still exist?
|
||||||
$pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
|
$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'),
|
'title' => trans('entities.recently_updated_pages'),
|
||||||
'pages' => $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.
|
* Show the view to choose a new parent to move a page into.
|
||||||
* @param string $bookSlug
|
* @param string $bookSlug
|
||||||
|
@ -587,7 +560,7 @@ class PageController extends Controller
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
$this->checkOwnablePermission('page-delete', $page);
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
return view('pages/move', [
|
return view('pages.move', [
|
||||||
'book' => $page->book,
|
'book' => $page->book,
|
||||||
'page' => $page
|
'page' => $page
|
||||||
]);
|
]);
|
||||||
|
@ -645,7 +618,7 @@ class PageController extends Controller
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('page-view', $page);
|
$this->checkOwnablePermission('page-view', $page);
|
||||||
session()->flashInput(['name' => $page->name]);
|
session()->flashInput(['name' => $page->name]);
|
||||||
return view('pages/copy', [
|
return view('pages.copy', [
|
||||||
'book' => $page->book,
|
'book' => $page->book,
|
||||||
'page' => $page
|
'page' => $page
|
||||||
]);
|
]);
|
||||||
|
@ -690,6 +663,24 @@ class PageController extends Controller
|
||||||
return redirect($pageCopy->getUrl());
|
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.
|
* Set the permissions for this page.
|
||||||
* @param string $bookSlug
|
* @param string $bookSlug
|
||||||
|
@ -697,8 +688,9 @@ class PageController extends Controller
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
* @throws NotFoundException
|
* @throws NotFoundException
|
||||||
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function restrict($bookSlug, $pageSlug, Request $request)
|
public function permissions($bookSlug, $pageSlug, Request $request)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||||
|
|
|
@ -26,7 +26,7 @@ class PermissionController extends Controller
|
||||||
{
|
{
|
||||||
$this->checkPermission('user-roles-manage');
|
$this->checkPermission('user-roles-manage');
|
||||||
$roles = $this->permissionsRepo->getAllRoles();
|
$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()
|
public function createRole()
|
||||||
{
|
{
|
||||||
$this->checkPermission('user-roles-manage');
|
$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) {
|
if ($role->hidden) {
|
||||||
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
|
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);
|
$roles = $this->permissionsRepo->getAllRolesExcept($role);
|
||||||
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
|
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
|
||||||
$roles->prepend($blankRole);
|
$roles->prepend($blankRole);
|
||||||
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
|
return view('settings.roles.delete', ['role' => $role, 'roles' => $roles]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,34 +1,45 @@
|
||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\ViewService;
|
use BookStack\Actions\ViewService;
|
||||||
|
use BookStack\Entities\EntityContextManager;
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
use BookStack\Entities\Repos\EntityRepo;
|
||||||
use BookStack\Entities\SearchService;
|
use BookStack\Entities\SearchService;
|
||||||
|
use BookStack\Exceptions\NotFoundException;
|
||||||
|
use Illuminate\Contracts\View\Factory;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class SearchController extends Controller
|
class SearchController extends Controller
|
||||||
{
|
{
|
||||||
protected $entityRepo;
|
protected $entityRepo;
|
||||||
protected $viewService;
|
protected $viewService;
|
||||||
protected $searchService;
|
protected $searchService;
|
||||||
|
protected $entityContextManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchController constructor.
|
* SearchController constructor.
|
||||||
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
|
* @param EntityRepo $entityRepo
|
||||||
* @param ViewService $viewService
|
* @param ViewService $viewService
|
||||||
* @param SearchService $searchService
|
* @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->entityRepo = $entityRepo;
|
||||||
$this->viewService = $viewService;
|
$this->viewService = $viewService;
|
||||||
$this->searchService = $searchService;
|
$this->searchService = $searchService;
|
||||||
|
$this->entityContextManager = $entityContextManager;
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches all entities.
|
* Searches all entities.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Illuminate\View\View
|
* @return View
|
||||||
* @internal param string $searchTerm
|
* @internal param string $searchTerm
|
||||||
*/
|
*/
|
||||||
public function search(Request $request)
|
public function search(Request $request)
|
||||||
|
@ -41,7 +52,7 @@ class SearchController extends Controller
|
||||||
|
|
||||||
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
|
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
|
||||||
|
|
||||||
return view('search/all', [
|
return view('search.all', [
|
||||||
'entities' => $results['results'],
|
'entities' => $results['results'],
|
||||||
'totalResults' => $results['total'],
|
'totalResults' => $results['total'],
|
||||||
'searchTerm' => $searchTerm,
|
'searchTerm' => $searchTerm,
|
||||||
|
@ -55,28 +66,28 @@ class SearchController extends Controller
|
||||||
* Searches all entities within a book.
|
* Searches all entities within a book.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param integer $bookId
|
* @param integer $bookId
|
||||||
* @return \Illuminate\View\View
|
* @return View
|
||||||
* @internal param string $searchTerm
|
* @internal param string $searchTerm
|
||||||
*/
|
*/
|
||||||
public function searchBook(Request $request, $bookId)
|
public function searchBook(Request $request, $bookId)
|
||||||
{
|
{
|
||||||
$term = $request->get('term', '');
|
$term = $request->get('term', '');
|
||||||
$results = $this->searchService->searchBook($bookId, $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.
|
* Searches all entities within a chapter.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param integer $chapterId
|
* @param integer $chapterId
|
||||||
* @return \Illuminate\View\View
|
* @return View
|
||||||
* @internal param string $searchTerm
|
* @internal param string $searchTerm
|
||||||
*/
|
*/
|
||||||
public function searchChapter(Request $request, $chapterId)
|
public function searchChapter(Request $request, $chapterId)
|
||||||
{
|
{
|
||||||
$term = $request->get('term', '');
|
$term = $request->get('term', '');
|
||||||
$results = $this->searchService->searchChapter($chapterId, $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)
|
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);
|
$searchTerm = $request->get('term', false);
|
||||||
$permission = $request->get('permission', 'view');
|
$permission = $request->get('permission', 'view');
|
||||||
|
|
||||||
// Search for entities otherwise show most popular
|
// Search for entities otherwise show most popular
|
||||||
if ($searchTerm !== false) {
|
if ($searchTerm !== false) {
|
||||||
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
|
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
|
||||||
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
|
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
|
||||||
} else {
|
} else {
|
||||||
$entityNames = $entityTypes->map(function ($type) {
|
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
|
||||||
return 'BookStack\\' . ucfirst($type); // TODO - Extract this elsewhere, too specific and stringy
|
|
||||||
})->toArray();
|
|
||||||
$entities = $this->viewService->getPopular(20, 0, $entityNames, $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']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Auth\User;
|
||||||
|
use BookStack\Uploads\ImageRepo;
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
@ -7,6 +9,19 @@ use Setting;
|
||||||
|
|
||||||
class SettingController extends Controller
|
class SettingController extends Controller
|
||||||
{
|
{
|
||||||
|
protected $imageRepo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SettingController constructor.
|
||||||
|
* @param $imageRepo
|
||||||
|
*/
|
||||||
|
public function __construct(ImageRepo $imageRepo)
|
||||||
|
{
|
||||||
|
$this->imageRepo = $imageRepo;
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a listing of the settings.
|
* Display a listing of the settings.
|
||||||
* @return Response
|
* @return Response
|
||||||
|
@ -19,7 +34,10 @@ class SettingController extends Controller
|
||||||
// Get application version
|
// Get application version
|
||||||
$version = trim(file_get_contents(base_path('version')));
|
$version = trim(file_get_contents(base_path('version')));
|
||||||
|
|
||||||
return view('settings/index', ['version' => $version]);
|
return view('settings.index', [
|
||||||
|
'version' => $version,
|
||||||
|
'guestUser' => User::getDefault()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,6 +49,9 @@ class SettingController extends Controller
|
||||||
{
|
{
|
||||||
$this->preventAccessForDemoUsers();
|
$this->preventAccessForDemoUsers();
|
||||||
$this->checkPermission('settings-manage');
|
$this->checkPermission('settings-manage');
|
||||||
|
$this->validate($request, [
|
||||||
|
'app_logo' => $this->imageRepo->getImageValidationRules(),
|
||||||
|
]);
|
||||||
|
|
||||||
// Cycles through posted settings and update them
|
// Cycles through posted settings and update them
|
||||||
foreach ($request->all() as $name => $value) {
|
foreach ($request->all() as $name => $value) {
|
||||||
|
@ -38,7 +59,21 @@ class SettingController extends Controller
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$key = str_replace('setting-', '', trim($name));
|
$key = str_replace('setting-', '', trim($name));
|
||||||
Setting::put($key, $value);
|
setting()->put($key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update logo image if set
|
||||||
|
if ($request->has('app_logo')) {
|
||||||
|
$logoFile = $request->file('app_logo');
|
||||||
|
$this->imageRepo->destroyByType('system');
|
||||||
|
$image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);
|
||||||
|
setting()->put('app-logo', $image->url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear logo image if requested
|
||||||
|
if ($request->get('app_logo_reset', null)) {
|
||||||
|
$this->imageRepo->destroyByType('system');
|
||||||
|
setting()->remove('app-logo');
|
||||||
}
|
}
|
||||||
|
|
||||||
session()->flash('success', trans('settings.settings_save_success'));
|
session()->flash('success', trans('settings.settings_save_success'));
|
||||||
|
@ -57,7 +92,7 @@ class SettingController extends Controller
|
||||||
// Get application version
|
// Get application version
|
||||||
$version = trim(file_get_contents(base_path('version')));
|
$version = trim(file_get_contents(base_path('version')));
|
||||||
|
|
||||||
return view('settings/maintenance', ['version' => $version]);
|
return view('settings.maintenance', ['version' => $version]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,6 +4,7 @@ use BookStack\Auth\Access\SocialAuthService;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Auth\UserRepo;
|
||||||
use BookStack\Exceptions\UserUpdateException;
|
use BookStack\Exceptions\UserUpdateException;
|
||||||
|
use BookStack\Uploads\ImageRepo;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
|
@ -12,16 +13,19 @@ class UserController extends Controller
|
||||||
|
|
||||||
protected $user;
|
protected $user;
|
||||||
protected $userRepo;
|
protected $userRepo;
|
||||||
|
protected $imageRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UserController constructor.
|
* UserController constructor.
|
||||||
* @param User $user
|
* @param User $user
|
||||||
* @param UserRepo $userRepo
|
* @param UserRepo $userRepo
|
||||||
|
* @param ImageRepo $imageRepo
|
||||||
*/
|
*/
|
||||||
public function __construct(User $user, UserRepo $userRepo)
|
public function __construct(User $user, UserRepo $userRepo, ImageRepo $imageRepo)
|
||||||
{
|
{
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
|
$this->imageRepo = $imageRepo;
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +45,7 @@ class UserController extends Controller
|
||||||
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
|
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
|
||||||
$this->setPageTitle(trans('settings.users'));
|
$this->setPageTitle(trans('settings.users'));
|
||||||
$users->appends($listDetails);
|
$users->appends($listDetails);
|
||||||
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
|
return view('users.index', ['users' => $users, 'listDetails' => $listDetails]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,7 +57,7 @@ class UserController extends Controller
|
||||||
$this->checkPermission('users-manage');
|
$this->checkPermission('users-manage');
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
$roles = $this->userRepo->getAllRoles();
|
$roles = $this->userRepo->getAllRoles();
|
||||||
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
return view('users.create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,9 +111,7 @@ class UserController extends Controller
|
||||||
*/
|
*/
|
||||||
public function edit($id, SocialAuthService $socialAuthService)
|
public function edit($id, SocialAuthService $socialAuthService)
|
||||||
{
|
{
|
||||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||||
return $this->currentUser->id == $id;
|
|
||||||
});
|
|
||||||
|
|
||||||
$user = $this->user->findOrFail($id);
|
$user = $this->user->findOrFail($id);
|
||||||
|
|
||||||
|
@ -118,7 +120,7 @@ class UserController extends Controller
|
||||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
||||||
$this->setPageTitle(trans('settings.user_profile'));
|
$this->setPageTitle(trans('settings.user_profile'));
|
||||||
$roles = $this->userRepo->getAllRoles();
|
$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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -127,20 +129,20 @@ class UserController extends Controller
|
||||||
* @param int $id
|
* @param int $id
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws UserUpdateException
|
* @throws UserUpdateException
|
||||||
|
* @throws \BookStack\Exceptions\ImageUploadException
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, $id)
|
public function update(Request $request, $id)
|
||||||
{
|
{
|
||||||
$this->preventAccessForDemoUsers();
|
$this->preventAccessForDemoUsers();
|
||||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||||
return $this->currentUser->id == $id;
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => 'min:2',
|
'name' => 'min:2',
|
||||||
'email' => 'min:2|email|unique:users,email,' . $id,
|
'email' => 'min:2|email|unique:users,email,' . $id,
|
||||||
'password' => 'min:5|required_with:password_confirm',
|
'password' => 'min:5|required_with:password_confirm',
|
||||||
'password-confirm' => 'same:password|required_with:password',
|
'password-confirm' => 'same:password|required_with:password',
|
||||||
'setting' => 'array'
|
'setting' => 'array',
|
||||||
|
'profile_image' => $this->imageRepo->getImageValidationRules(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = $this->userRepo->getById($id);
|
$user = $this->userRepo->getById($id);
|
||||||
|
@ -170,10 +172,23 @@ class UserController extends Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save profile image if in request
|
||||||
|
if ($request->has('profile_image')) {
|
||||||
|
$imageUpload = $request->file('profile_image');
|
||||||
|
$this->imageRepo->destroyImage($user->avatar);
|
||||||
|
$image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id);
|
||||||
|
$user->image_id = $image->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the profile image if set to
|
||||||
|
if ($request->has('profile_image_reset')) {
|
||||||
|
$this->imageRepo->destroyImage($user->avatar);
|
||||||
|
}
|
||||||
|
|
||||||
$user->save();
|
$user->save();
|
||||||
session()->flash('success', trans('settings.users_edit_success'));
|
session()->flash('success', trans('settings.users_edit_success'));
|
||||||
|
|
||||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
|
$redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
|
||||||
return redirect($redirectUrl);
|
return redirect($redirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,13 +199,11 @@ class UserController extends Controller
|
||||||
*/
|
*/
|
||||||
public function delete($id)
|
public function delete($id)
|
||||||
{
|
{
|
||||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||||
return $this->currentUser->id == $id;
|
|
||||||
});
|
|
||||||
|
|
||||||
$user = $this->userRepo->getById($id);
|
$user = $this->userRepo->getById($id);
|
||||||
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
|
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
|
||||||
return view('users/delete', ['user' => $user]);
|
return view('users.delete', ['user' => $user]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -202,9 +215,7 @@ class UserController extends Controller
|
||||||
public function destroy($id)
|
public function destroy($id)
|
||||||
{
|
{
|
||||||
$this->preventAccessForDemoUsers();
|
$this->preventAccessForDemoUsers();
|
||||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||||
return $this->currentUser->id == $id;
|
|
||||||
});
|
|
||||||
|
|
||||||
$user = $this->userRepo->getById($id);
|
$user = $this->userRepo->getById($id);
|
||||||
|
|
||||||
|
@ -232,10 +243,12 @@ class UserController extends Controller
|
||||||
public function showProfilePage($id)
|
public function showProfilePage($id)
|
||||||
{
|
{
|
||||||
$user = $this->userRepo->getById($id);
|
$user = $this->userRepo->getById($id);
|
||||||
|
|
||||||
$userActivity = $this->userRepo->getActivity($user);
|
$userActivity = $this->userRepo->getActivity($user);
|
||||||
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
|
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
|
||||||
$assetCounts = $this->userRepo->getAssetCounts($user);
|
$assetCounts = $this->userRepo->getAssetCounts($user);
|
||||||
return view('users/profile', [
|
|
||||||
|
return view('users.profile', [
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'activity' => $userActivity,
|
'activity' => $userActivity,
|
||||||
'recentlyCreated' => $recentlyCreated,
|
'recentlyCreated' => $recentlyCreated,
|
||||||
|
@ -251,19 +264,7 @@ class UserController extends Controller
|
||||||
*/
|
*/
|
||||||
public function switchBookView($id, Request $request)
|
public function switchBookView($id, Request $request)
|
||||||
{
|
{
|
||||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
return $this->switchViewType($id, $request, 'books');
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -274,18 +275,97 @@ class UserController extends Controller
|
||||||
*/
|
*/
|
||||||
public function switchShelfView($id, Request $request)
|
public function switchShelfView($id, Request $request)
|
||||||
{
|
{
|
||||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
return $this->switchViewType($id, $request, 'bookshelves');
|
||||||
return $this->currentUser->id == $id;
|
}
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* 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');
|
$viewType = $request->get('view_type');
|
||||||
if (!in_array($viewType, ['grid', 'list'])) {
|
if (!in_array($viewType, ['grid', 'list'])) {
|
||||||
$viewType = 'list';
|
$viewType = 'list';
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $this->userRepo->getById($id);
|
$user = $this->userRepo->getById($userId);
|
||||||
setting()->putUser($user, 'bookshelves_view_type', $viewType);
|
$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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ class Authenticate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->auth->guest() && !setting('app-public')) {
|
if (!hasAppAccess()) {
|
||||||
if ($request->ajax()) {
|
if ($request->ajax()) {
|
||||||
return response('Unauthorized.', 401);
|
return response('Unauthorized.', 401);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,12 +3,14 @@
|
||||||
use Blade;
|
use Blade;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Entities\Book;
|
||||||
use BookStack\Entities\Bookshelf;
|
use BookStack\Entities\Bookshelf;
|
||||||
|
use BookStack\Entities\BreadcrumbsViewComposer;
|
||||||
use BookStack\Entities\Chapter;
|
use BookStack\Entities\Chapter;
|
||||||
use BookStack\Entities\Page;
|
use BookStack\Entities\Page;
|
||||||
use BookStack\Settings\Setting;
|
use BookStack\Settings\Setting;
|
||||||
use BookStack\Settings\SettingService;
|
use BookStack\Settings\SettingService;
|
||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\View;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Schema;
|
use Schema;
|
||||||
use Validator;
|
use Validator;
|
||||||
|
@ -33,7 +35,6 @@ class AppServiceProvider extends ServiceProvider
|
||||||
return substr_count($uploadName, '.') < 2;
|
return substr_count($uploadName, '.') < 2;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Custom blade view directives
|
// Custom blade view directives
|
||||||
Blade::directive('icon', function ($expression) {
|
Blade::directive('icon', function ($expression) {
|
||||||
return "<?php echo icon($expression); ?>";
|
return "<?php echo icon($expression); ?>";
|
||||||
|
@ -49,6 +50,9 @@ class AppServiceProvider extends ServiceProvider
|
||||||
'BookStack\\Chapter' => Chapter::class,
|
'BookStack\\Chapter' => Chapter::class,
|
||||||
'BookStack\\Page' => Page::class,
|
'BookStack\\Page' => Page::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// View Composers
|
||||||
|
View::composer('partials.breadcrumbs', BreadcrumbsViewComposer::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,20 +2,11 @@
|
||||||
|
|
||||||
namespace BookStack\Providers;
|
namespace BookStack\Providers;
|
||||||
|
|
||||||
use BookStack\Actions\Activity;
|
|
||||||
use BookStack\Actions\ActivityService;
|
use BookStack\Actions\ActivityService;
|
||||||
use BookStack\Actions\View;
|
|
||||||
use BookStack\Actions\ViewService;
|
use BookStack\Actions\ViewService;
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
|
||||||
use BookStack\Settings\Setting;
|
|
||||||
use BookStack\Settings\SettingService;
|
use BookStack\Settings\SettingService;
|
||||||
use BookStack\Uploads\HttpFetcher;
|
|
||||||
use BookStack\Uploads\Image;
|
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
use Illuminate\Contracts\Cache\Repository;
|
|
||||||
use Illuminate\Contracts\Filesystem\Factory;
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Intervention\Image\ImageManager;
|
|
||||||
|
|
||||||
class CustomFacadeProvider extends ServiceProvider
|
class CustomFacadeProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
@ -37,34 +28,19 @@ class CustomFacadeProvider extends ServiceProvider
|
||||||
public function register()
|
public function register()
|
||||||
{
|
{
|
||||||
$this->app->bind('activity', function () {
|
$this->app->bind('activity', function () {
|
||||||
return new ActivityService(
|
return $this->app->make(ActivityService::class);
|
||||||
$this->app->make(Activity::class),
|
|
||||||
$this->app->make(PermissionService::class)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->bind('views', function () {
|
$this->app->bind('views', function () {
|
||||||
return new ViewService(
|
return $this->app->make(ViewService::class);
|
||||||
$this->app->make(View::class),
|
|
||||||
$this->app->make(PermissionService::class)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->bind('setting', function () {
|
$this->app->bind('setting', function () {
|
||||||
return new SettingService(
|
return $this->app->make(SettingService::class);
|
||||||
$this->app->make(Setting::class),
|
|
||||||
$this->app->make(Repository::class)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->app->bind('images', function () {
|
$this->app->bind('images', function () {
|
||||||
return new ImageService(
|
return $this->app->make(ImageService::class);
|
||||||
$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)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,9 +61,23 @@ class SettingService
|
||||||
*/
|
*/
|
||||||
public function getUser($user, $key, $default = false)
|
public function getUser($user, $key, $default = false)
|
||||||
{
|
{
|
||||||
|
if ($user->isDefault()) {
|
||||||
|
return session()->get($key, $default);
|
||||||
|
}
|
||||||
return $this->get($this->userKey($user->id, $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.
|
* Gets a setting value from the cache or database.
|
||||||
* Looks at the system defaults if not cached or in database.
|
* Looks at the system defaults if not cached or in database.
|
||||||
|
@ -180,6 +194,9 @@ class SettingService
|
||||||
*/
|
*/
|
||||||
public function putUser($user, $key, $value)
|
public function putUser($user, $key, $value)
|
||||||
{
|
{
|
||||||
|
if ($user->isDefault()) {
|
||||||
|
return session()->put($key, $value);
|
||||||
|
}
|
||||||
return $this->put($this->userKey($user->id, $key), $value);
|
return $this->put($this->userKey($user->id, $key), $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?php namespace BookStack\Uploads;
|
<?php namespace BookStack\Uploads;
|
||||||
|
|
||||||
|
use BookStack\Entities\Page;
|
||||||
use BookStack\Ownable;
|
use BookStack\Ownable;
|
||||||
use Images;
|
use Images;
|
||||||
|
|
||||||
|
@ -20,4 +21,14 @@ class Image extends Ownable
|
||||||
{
|
{
|
||||||
return Images::getThumbnail($this, $width, $height, $keepRatio);
|
return Images::getThumbnail($this, $width, $height, $keepRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the page this image has been uploaded to.
|
||||||
|
* Only applicable to gallery or drawio image types.
|
||||||
|
* @return Page|null
|
||||||
|
*/
|
||||||
|
public function getPage()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Page::class, 'uploaded_to')->first();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
use BookStack\Entities\Page;
|
use BookStack\Entities\Page;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
|
||||||
class ImageRepo
|
class ImageRepo
|
||||||
|
@ -19,8 +20,12 @@ class ImageRepo
|
||||||
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
|
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
|
||||||
* @param \BookStack\Entities\Page $page
|
* @param \BookStack\Entities\Page $page
|
||||||
*/
|
*/
|
||||||
public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
|
public function __construct(
|
||||||
{
|
Image $image,
|
||||||
|
ImageService $imageService,
|
||||||
|
PermissionService $permissionService,
|
||||||
|
Page $page
|
||||||
|
) {
|
||||||
$this->image = $image;
|
$this->image = $image;
|
||||||
$this->imageService = $imageService;
|
$this->imageService = $imageService;
|
||||||
$this->restrictionService = $permissionService;
|
$this->restrictionService = $permissionService;
|
||||||
|
@ -31,7 +36,7 @@ class ImageRepo
|
||||||
/**
|
/**
|
||||||
* Get an image with the given id.
|
* Get an image with the given id.
|
||||||
* @param $id
|
* @param $id
|
||||||
* @return mixed
|
* @return Image
|
||||||
*/
|
*/
|
||||||
public function getById($id)
|
public function getById($id)
|
||||||
{
|
{
|
||||||
|
@ -44,81 +49,97 @@ class ImageRepo
|
||||||
* @param $query
|
* @param $query
|
||||||
* @param int $page
|
* @param int $page
|
||||||
* @param int $pageSize
|
* @param int $pageSize
|
||||||
|
* @param bool $filterOnPage
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
private function returnPaginated($query, $page = 0, $pageSize = 24)
|
private function returnPaginated($query, $page = 1, $pageSize = 24)
|
||||||
{
|
{
|
||||||
$images = $this->restrictionService->filterRelatedPages($query, 'images', 'uploaded_to');
|
$images = $query->orderBy('created_at', 'desc')->skip($pageSize * ($page - 1))->take($pageSize + 1)->get();
|
||||||
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
|
|
||||||
$hasMore = count($images) > $pageSize;
|
$hasMore = count($images) > $pageSize;
|
||||||
|
|
||||||
$returnImages = $images->take(24);
|
$returnImages = $images->take($pageSize);
|
||||||
$returnImages->each(function ($image) {
|
$returnImages->each(function ($image) {
|
||||||
$this->loadThumbs($image);
|
$this->loadThumbs($image);
|
||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'images' => $returnImages,
|
'images' => $returnImages,
|
||||||
'hasMore' => $hasMore
|
'has_more' => $hasMore
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a load images paginated, filtered by image type.
|
* Fetch a list of images in a paginated format, filtered by image type.
|
||||||
|
* Can be filtered by uploaded to and also by name.
|
||||||
* @param string $type
|
* @param string $type
|
||||||
* @param int $page
|
* @param int $page
|
||||||
* @param int $pageSize
|
* @param int $pageSize
|
||||||
* @param bool|int $userFilter
|
* @param int $uploadedTo
|
||||||
|
* @param string|null $search
|
||||||
|
* @param callable|null $whereClause
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
|
public function getPaginatedByType(
|
||||||
{
|
string $type,
|
||||||
$images = $this->image->where('type', '=', strtolower($type));
|
int $page = 0,
|
||||||
|
int $pageSize = 24,
|
||||||
|
int $uploadedTo = null,
|
||||||
|
string $search = null,
|
||||||
|
callable $whereClause = null
|
||||||
|
) {
|
||||||
|
$imageQuery = $this->image->newQuery()->where('type', '=', strtolower($type));
|
||||||
|
|
||||||
if ($userFilter !== false) {
|
if ($uploadedTo !== null) {
|
||||||
$images = $images->where('created_by', '=', $userFilter);
|
$imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->returnPaginated($images, $page, $pageSize);
|
if ($search !== null) {
|
||||||
|
$imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by page access
|
||||||
|
$imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to');
|
||||||
|
|
||||||
|
if ($whereClause !== null) {
|
||||||
|
$imageQuery = $imageQuery->where($whereClause);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->returnPaginated($imageQuery, $page, $pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for images by query, of a particular type.
|
* Get paginated gallery images within a specific page or book.
|
||||||
* @param string $type
|
* @param string $type
|
||||||
|
* @param string $filterType
|
||||||
* @param int $page
|
* @param int $page
|
||||||
* @param int $pageSize
|
* @param int $pageSize
|
||||||
* @param string $searchTerm
|
* @param int|null $uploadedTo
|
||||||
|
* @param string|null $search
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function searchPaginatedByType($type, $searchTerm, $page = 0, $pageSize = 24)
|
public function getEntityFiltered(
|
||||||
{
|
string $type,
|
||||||
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
|
string $filterType = null,
|
||||||
return $this->returnPaginated($images, $page, $pageSize);
|
int $page = 0,
|
||||||
|
int $pageSize = 24,
|
||||||
|
int $uploadedTo = null,
|
||||||
|
string $search = null
|
||||||
|
) {
|
||||||
|
$contextPage = $this->page->findOrFail($uploadedTo);
|
||||||
|
$parentFilter = null;
|
||||||
|
|
||||||
|
if ($filterType === 'book' || $filterType === 'page') {
|
||||||
|
$parentFilter = function (Builder $query) use ($filterType, $contextPage) {
|
||||||
|
if ($filterType === 'page') {
|
||||||
|
$query->where('uploaded_to', '=', $contextPage->id);
|
||||||
|
} elseif ($filterType === 'book') {
|
||||||
|
$validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray();
|
||||||
|
$query->whereIn('uploaded_to', $validPageIds);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return $this->getPaginatedByType($type, $page, $pageSize, null, $search, $parentFilter);
|
||||||
* Get gallery images with a particular filter criteria such as
|
|
||||||
* being within the current book or page.
|
|
||||||
* @param $filter
|
|
||||||
* @param $pageId
|
|
||||||
* @param int $pageNum
|
|
||||||
* @param int $pageSize
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getGalleryFiltered($filter, $pageId, $pageNum = 0, $pageSize = 24)
|
|
||||||
{
|
|
||||||
$images = $this->image->where('type', '=', 'gallery');
|
|
||||||
|
|
||||||
$page = $this->page->findOrFail($pageId);
|
|
||||||
|
|
||||||
if ($filter === 'page') {
|
|
||||||
$images = $images->where('uploaded_to', '=', $page->id);
|
|
||||||
} elseif ($filter === 'book') {
|
|
||||||
$validPageIds = $page->book->pages->pluck('id')->toArray();
|
|
||||||
$images = $images->whereIn('uploaded_to', $validPageIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->returnPaginated($images, $pageNum, $pageSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,13 +147,15 @@ class ImageRepo
|
||||||
* @param UploadedFile $uploadFile
|
* @param UploadedFile $uploadFile
|
||||||
* @param string $type
|
* @param string $type
|
||||||
* @param int $uploadedTo
|
* @param int $uploadedTo
|
||||||
|
* @param int|null $resizeWidth
|
||||||
|
* @param int|null $resizeHeight
|
||||||
|
* @param bool $keepRatio
|
||||||
* @return Image
|
* @return Image
|
||||||
* @throws \BookStack\Exceptions\ImageUploadException
|
* @throws \BookStack\Exceptions\ImageUploadException
|
||||||
* @throws \Exception
|
|
||||||
*/
|
*/
|
||||||
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
|
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true)
|
||||||
{
|
{
|
||||||
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo);
|
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio);
|
||||||
$this->loadThumbs($image);
|
$this->loadThumbs($image);
|
||||||
return $image;
|
return $image;
|
||||||
}
|
}
|
||||||
|
@ -175,12 +198,27 @@ class ImageRepo
|
||||||
* @return bool
|
* @return bool
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function destroyImage(Image $image)
|
public function destroyImage(Image $image = null)
|
||||||
{
|
{
|
||||||
|
if ($image) {
|
||||||
$this->imageService->destroy($image);
|
$this->imageService->destroy($image);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy all images of a certain type.
|
||||||
|
* @param string $imageType
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function destroyByType(string $imageType)
|
||||||
|
{
|
||||||
|
$images = $this->image->where('type', '=', $imageType)->get();
|
||||||
|
foreach ($images as $image) {
|
||||||
|
$this->destroyImage($image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load thumbnails onto an image object.
|
* Load thumbnails onto an image object.
|
||||||
|
@ -191,8 +229,8 @@ class ImageRepo
|
||||||
protected function loadThumbs(Image $image)
|
protected function loadThumbs(Image $image)
|
||||||
{
|
{
|
||||||
$image->thumbs = [
|
$image->thumbs = [
|
||||||
'gallery' => $this->getThumbnail($image, 150, 150),
|
'gallery' => $this->getThumbnail($image, 150, 150, false),
|
||||||
'display' => $this->getThumbnail($image, 840, 0, true)
|
'display' => $this->getThumbnail($image, 840, null, true)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,7 +246,7 @@ class ImageRepo
|
||||||
* @throws \BookStack\Exceptions\ImageUploadException
|
* @throws \BookStack\Exceptions\ImageUploadException
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
protected function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
||||||
|
@ -232,13 +270,11 @@ class ImageRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the provided image type is valid.
|
* Get the validation rules for image files.
|
||||||
* @param $type
|
* @return string
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public function isValidType($type)
|
public function getImageValidationRules()
|
||||||
{
|
{
|
||||||
$validTypes = ['gallery', 'cover', 'system', 'user'];
|
return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
|
||||||
return in_array($type, $validTypes);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ use Illuminate\Contracts\Cache\Repository as Cache;
|
||||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||||
use Intervention\Image\Exception\NotSupportedException;
|
use Intervention\Image\Exception\NotSupportedException;
|
||||||
use Intervention\Image\ImageManager;
|
use Intervention\Image\ImageManager;
|
||||||
|
use phpDocumentor\Reflection\Types\Integer;
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
|
||||||
class ImageService extends UploadService
|
class ImageService extends UploadService
|
||||||
|
@ -59,13 +60,27 @@ class ImageService extends UploadService
|
||||||
* @param UploadedFile $uploadedFile
|
* @param UploadedFile $uploadedFile
|
||||||
* @param string $type
|
* @param string $type
|
||||||
* @param int $uploadedTo
|
* @param int $uploadedTo
|
||||||
|
* @param int|null $resizeWidth
|
||||||
|
* @param int|null $resizeHeight
|
||||||
|
* @param bool $keepRatio
|
||||||
* @return mixed
|
* @return mixed
|
||||||
* @throws ImageUploadException
|
* @throws ImageUploadException
|
||||||
*/
|
*/
|
||||||
public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0)
|
public function saveNewFromUpload(
|
||||||
{
|
UploadedFile $uploadedFile,
|
||||||
|
string $type,
|
||||||
|
int $uploadedTo = 0,
|
||||||
|
int $resizeWidth = null,
|
||||||
|
int $resizeHeight = null,
|
||||||
|
bool $keepRatio = true
|
||||||
|
) {
|
||||||
$imageName = $uploadedFile->getClientOriginalName();
|
$imageName = $uploadedFile->getClientOriginalName();
|
||||||
$imageData = file_get_contents($uploadedFile->getRealPath());
|
$imageData = file_get_contents($uploadedFile->getRealPath());
|
||||||
|
|
||||||
|
if ($resizeWidth !== null || $resizeHeight !== null) {
|
||||||
|
$imageData = $this->resizeImage($imageData, $resizeWidth, $resizeHeight, $keepRatio);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
|
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +137,7 @@ class ImageService extends UploadService
|
||||||
$secureUploads = setting('app-secure-images');
|
$secureUploads = setting('app-secure-images');
|
||||||
$imageName = str_replace(' ', '-', $imageName);
|
$imageName = str_replace(' ', '-', $imageName);
|
||||||
|
|
||||||
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
|
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
|
||||||
|
|
||||||
while ($storage->exists($imagePath . $imageName)) {
|
while ($storage->exists($imagePath . $imageName)) {
|
||||||
$imageName = str_random(3) . $imageName;
|
$imageName = str_random(3) . $imageName;
|
||||||
|
@ -201,8 +216,28 @@ class ImageService extends UploadService
|
||||||
return $this->getPublicUrl($thumbFilePath);
|
return $this->getPublicUrl($thumbFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$thumbData = $this->resizeImage($storage->get($imagePath), $width, $height, $keepRatio);
|
||||||
|
|
||||||
|
$storage->put($thumbFilePath, $thumbData);
|
||||||
|
$storage->setVisibility($thumbFilePath, 'public');
|
||||||
|
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
|
||||||
|
|
||||||
|
return $this->getPublicUrl($thumbFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize image data.
|
||||||
|
* @param string $imageData
|
||||||
|
* @param int $width
|
||||||
|
* @param int $height
|
||||||
|
* @param bool $keepRatio
|
||||||
|
* @return string
|
||||||
|
* @throws ImageUploadException
|
||||||
|
*/
|
||||||
|
protected function resizeImage(string $imageData, $width = 220, $height = null, bool $keepRatio = true)
|
||||||
|
{
|
||||||
try {
|
try {
|
||||||
$thumb = $this->imageTool->make($storage->get($imagePath));
|
$thumb = $this->imageTool->make($imageData);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
||||||
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
||||||
|
@ -211,20 +246,14 @@ class ImageService extends UploadService
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($keepRatio) {
|
if ($keepRatio) {
|
||||||
$thumb->resize($width, null, function ($constraint) {
|
$thumb->resize($width, $height, function ($constraint) {
|
||||||
$constraint->aspectRatio();
|
$constraint->aspectRatio();
|
||||||
$constraint->upsize();
|
$constraint->upsize();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
$thumb->fit($width, $height);
|
$thumb->fit($width, $height);
|
||||||
}
|
}
|
||||||
|
return (string)$thumb->encode();
|
||||||
$thumbData = (string)$thumb->encode();
|
|
||||||
$storage->put($thumbFilePath, $thumbData);
|
|
||||||
$storage->setVisibility($thumbFilePath, 'public');
|
|
||||||
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
|
|
||||||
|
|
||||||
return $this->getPublicUrl($thumbFilePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -306,6 +335,7 @@ class ImageService extends UploadService
|
||||||
$image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName);
|
$image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName);
|
||||||
$image->created_by = $user->id;
|
$image->created_by = $user->id;
|
||||||
$image->updated_by = $user->id;
|
$image->updated_by = $user->id;
|
||||||
|
$image->uploaded_to = $user->id;
|
||||||
$image->save();
|
$image->save();
|
||||||
|
|
||||||
return $image;
|
return $image;
|
||||||
|
|
|
@ -43,11 +43,20 @@ function user()
|
||||||
* Check if current user is a signed in user.
|
* Check if current user is a signed in user.
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
function signedInUser()
|
function signedInUser() : bool
|
||||||
{
|
{
|
||||||
return auth()->user() && !auth()->user()->isDefault();
|
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.
|
* Check if the current user has a permission.
|
||||||
* If an ownable element is passed in the jointPermissions are checked against
|
* If an ownable element is passed in the jointPermissions are checked against
|
||||||
|
|
|
@ -46,7 +46,7 @@ return [
|
||||||
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
||||||
|
|
||||||
// Application timezone for back-end date functions.
|
// Application timezone for back-end date functions.
|
||||||
'timezone' => 'UTC',
|
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||||
|
|
||||||
// Default locale to use
|
// Default locale to use
|
||||||
'locale' => env('APP_LANG', 'en'),
|
'locale' => env('APP_LANG', 'en'),
|
||||||
|
|
12
package.json
|
@ -13,17 +13,17 @@
|
||||||
"@babel/core": "^7.1.6",
|
"@babel/core": "^7.1.6",
|
||||||
"@babel/polyfill": "^7.0.0",
|
"@babel/polyfill": "^7.0.0",
|
||||||
"@babel/preset-env": "^7.1.6",
|
"@babel/preset-env": "^7.1.6",
|
||||||
"autoprefixer": "^8.6.5",
|
"autoprefixer": "^9.4.7",
|
||||||
"babel-loader": "^8.0.4",
|
"babel-loader": "^8.0.4",
|
||||||
"css-loader": "^0.28.11",
|
"css-loader": "^2.1.0",
|
||||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
|
||||||
"livereload": "^0.7.0",
|
"livereload": "^0.7.0",
|
||||||
|
"mini-css-extract-plugin": "^0.5.0",
|
||||||
"node-sass": "^4.10.0",
|
"node-sass": "^4.10.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss-loader": "^2.1.6",
|
"postcss-loader": "^3.0.0",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"style-loader": "^0.21.0",
|
"style-loader": "^0.23.1",
|
||||||
"uglifyjs-webpack-plugin": "^1.3.0",
|
"uglifyjs-webpack-plugin": "^2.1.1",
|
||||||
"webpack": "^4.26.1",
|
"webpack": "^4.26.1",
|
||||||
"webpack-cli": "^3.1.2"
|
"webpack-cli": "^3.1.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
!function(){"use strict";var a=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return a(n())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),r=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),o=tinymce.util.Tools.resolve("tinymce.util.Tools"),i=function(t,e){var n=t||e,r=/^(\d+)([ms]?)$/.exec(""+n);return(r[2]?{s:1e3,m:6e4}[r[2]]:1)*parseInt(n,10)},u=function(t){var e=t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-");return e=(e=(e=(e=e.replace(/\{path\}/g,document.location.pathname)).replace(/\{query\}/g,document.location.search)).replace(/\{hash\}/g,document.location.hash)).replace(/\{id\}/g,t.id)},s=function(t,e){var n=t.settings.forced_root_block;return""===(e=o.trim(void 0===e?t.getBody().innerHTML:e))||new RegExp("^<"+n+"[^>]*>((\xa0| |[ \t]|<br[^>]*>)+?|)</"+n+">|<br>$","i").test(e)},c=function(t){var e=parseInt(r.getItem(u(t)+"time"),10)||0;return!((new Date).getTime()-e>i(t.settings.autosave_retention,"20m")&&(f(t,!1),1))},f=function(t,e){var n=u(t);r.removeItem(n+"draft"),r.removeItem(n+"time"),!1!==e&&t.fire("RemoveDraft")},l=function(t){var e=u(t);!s(t)&&t.isDirty()&&(r.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),r.setItem(e+"time",(new Date).getTime().toString()),t.fire("StoreDraft"))},m=function(t){var e=u(t);c(t)&&(t.setContent(r.getItem(e+"draft"),{format:"raw"}),t.fire("RestoreDraft"))},v=function(t,e){var n=i(t.settings.autosave_interval,"30s");e.get()||(setInterval(function(){t.removed||l(t)},n),e.set(!0))},d=function(t){t.undoManager.transact(function(){m(t),f(t)}),t.focus()};function g(r){for(var o=[],t=1;t<arguments.length;t++)o[t-1]=arguments[t];return function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=o.concat(t);return r.apply(null,n)}}var y=tinymce.util.Tools.resolve("tinymce.EditorManager");y._beforeUnloadHandler=function(){var e;return o.each(y.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e};var p=function(n,r){return function(t){var e=t.control;e.disabled(!c(n)),n.on("StoreDraft RestoreDraft RemoveDraft",function(){e.disabled(!c(n))}),v(n,r)}};t.add("autosave",function(t){var e,n,r,o=a(!1);return window.onbeforeunload=y._beforeUnloadHandler,n=o,(e=t).addButton("restoredraft",{title:"Restore last draft",onclick:function(){d(e)},onPostRender:p(e,n)}),e.addMenuItem("restoredraft",{text:"Restore last draft",onclick:function(){d(e)},onPostRender:p(e,n),context:"file"}),t.on("init",function(){t.getParam("autosave_restore_when_empty",!1)&&t.dom.isEmpty(t.getBody())&&m(t)}),{hasDraft:g(c,r=t),storeDraft:g(l,r),restoreDraft:g(m,r),removeDraft:g(f,r),isEmpty:g(s,r)}})}();
|
!function(a){"use strict";var i=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return i(n())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),r=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),o=tinymce.util.Tools.resolve("tinymce.util.Tools"),u=function(t,e){var n=t||e,r=/^(\d+)([ms]?)$/.exec(""+n);return(r[2]?{s:1e3,m:6e4}[r[2]]:1)*parseInt(n,10)},s=function(t){var e=t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-");return e=(e=(e=(e=e.replace(/\{path\}/g,a.document.location.pathname)).replace(/\{query\}/g,a.document.location.search)).replace(/\{hash\}/g,a.document.location.hash)).replace(/\{id\}/g,t.id)},c=function(t,e){var n=t.settings.forced_root_block;return""===(e=o.trim(void 0===e?t.getBody().innerHTML:e))||new RegExp("^<"+n+"[^>]*>((\xa0| |[ \t]|<br[^>]*>)+?|)</"+n+">|<br>$","i").test(e)},f=function(t){var e=parseInt(r.getItem(s(t)+"time"),10)||0;return!((new Date).getTime()-e>u(t.settings.autosave_retention,"20m")&&(l(t,!1),1))},l=function(t,e){var n=s(t);r.removeItem(n+"draft"),r.removeItem(n+"time"),!1!==e&&t.fire("RemoveDraft")},m=function(t){var e=s(t);!c(t)&&t.isDirty()&&(r.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),r.setItem(e+"time",(new Date).getTime().toString()),t.fire("StoreDraft"))},v=function(t){var e=s(t);f(t)&&(t.setContent(r.getItem(e+"draft"),{format:"raw"}),t.fire("RestoreDraft"))},d=function(t,e){var n=u(t.settings.autosave_interval,"30s");e.get()||(setInterval(function(){t.removed||m(t)},n),e.set(!0))},g=function(t){t.undoManager.transact(function(){v(t),l(t)}),t.focus()};function y(r){for(var o=[],t=1;t<arguments.length;t++)o[t-1]=arguments[t];return function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=o.concat(t);return r.apply(null,n)}}var p=tinymce.util.Tools.resolve("tinymce.EditorManager");p._beforeUnloadHandler=function(){var e;return o.each(p.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e};var h=function(n,r){return function(t){var e=t.control;e.disabled(!f(n)),n.on("StoreDraft RestoreDraft RemoveDraft",function(){e.disabled(!f(n))}),d(n,r)}};t.add("autosave",function(t){var e,n,r,o=i(!1);return a.window.onbeforeunload=p._beforeUnloadHandler,n=o,(e=t).addButton("restoredraft",{title:"Restore last draft",onclick:function(){g(e)},onPostRender:h(e,n)}),e.addMenuItem("restoredraft",{text:"Restore last draft",onclick:function(){g(e)},onPostRender:h(e,n),context:"file"}),t.on("init",function(){t.getParam("autosave_restore_when_empty",!1)&&t.dom.isEmpty(t.getBody())&&v(t)}),{hasDraft:y(f,r=t),storeDraft:y(m,r),restoreDraft:y(v,r),removeDraft:y(l,r),isEmpty:y(c,r)}})}(window);
|
|
@ -1 +1 @@
|
||||||
!function(){"use strict";var i=function(e){var n=e,t=function(){return n};return{get:t,set:function(e){n=e},clone:function(){return i(t())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(e){return{isFullscreen:function(){return null!==e.get()}}},n=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),m=function(e,n){e.fire("FullscreenStateChanged",{state:n})},g=n.DOM,r=function(e,n){var t,r,l,i,o,c,s=document.body,u=document.documentElement,d=n.get(),a=function(){var e,n,t,i;g.setStyle(l,"height",(t=window,i=document.body,i.offsetWidth&&(e=i.offsetWidth,n=i.offsetHeight),t.innerWidth&&t.innerHeight&&(e=t.innerWidth,n=t.innerHeight),{w:e,h:n}).h-(r.clientHeight-l.clientHeight))},h=function(){g.unbind(window,"resize",a)};if(t=(r=e.getContainer()).style,i=(l=e.getContentAreaContainer().firstChild).style,d)i.width=d.iframeWidth,i.height=d.iframeHeight,d.containerWidth&&(t.width=d.containerWidth),d.containerHeight&&(t.height=d.containerHeight),g.removeClass(s,"mce-fullscreen"),g.removeClass(u,"mce-fullscreen"),g.removeClass(r,"mce-fullscreen"),o=d.scrollPos,window.scrollTo(o.x,o.y),g.unbind(window,"resize",d.resizeHandler),e.off("remove",d.removeHandler),n.set(null),m(e,!1);else{var f={scrollPos:(c=g.getViewPort(),{x:c.x,y:c.y}),containerWidth:t.width,containerHeight:t.height,iframeWidth:i.width,iframeHeight:i.height,resizeHandler:a,removeHandler:h};i.width=i.height="100%",t.width=t.height="",g.addClass(s,"mce-fullscreen"),g.addClass(u,"mce-fullscreen"),g.addClass(r,"mce-fullscreen"),g.bind(window,"resize",a),e.on("remove",h),a(),n.set(f),m(e,!0)}},l=function(e,n){e.addCommand("mceFullScreen",function(){r(e,n)})},o=function(t){return function(e){var n=e.control;t.on("FullscreenStateChanged",function(e){n.active(e.state)})}},c=function(e){e.addMenuItem("fullscreen",{text:"Fullscreen",shortcut:"Ctrl+Shift+F",selectable:!0,cmd:"mceFullScreen",onPostRender:o(e),context:"view"}),e.addButton("fullscreen",{active:!1,tooltip:"Fullscreen",cmd:"mceFullScreen",onPostRender:o(e)})};e.add("fullscreen",function(e){var n=i(null);return e.settings.inline||(l(e,n),c(e),e.addShortcut("Ctrl+Shift+F","","mceFullScreen")),t(n)})}();
|
!function(m){"use strict";var i=function(e){var n=e,t=function(){return n};return{get:t,set:function(e){n=e},clone:function(){return i(t())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(e){return{isFullscreen:function(){return null!==e.get()}}},n=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),g=function(e,n){e.fire("FullscreenStateChanged",{state:n})},w=n.DOM,r=function(e,n){var t,r,l,i,o,c,s=m.document.body,u=m.document.documentElement,d=n.get(),a=function(){var e,n,t,i;w.setStyle(l,"height",(t=m.window,i=m.document.body,i.offsetWidth&&(e=i.offsetWidth,n=i.offsetHeight),t.innerWidth&&t.innerHeight&&(e=t.innerWidth,n=t.innerHeight),{w:e,h:n}).h-(r.clientHeight-l.clientHeight))},h=function(){w.unbind(m.window,"resize",a)};if(t=(r=e.getContainer()).style,i=(l=e.getContentAreaContainer().firstChild).style,d)i.width=d.iframeWidth,i.height=d.iframeHeight,d.containerWidth&&(t.width=d.containerWidth),d.containerHeight&&(t.height=d.containerHeight),w.removeClass(s,"mce-fullscreen"),w.removeClass(u,"mce-fullscreen"),w.removeClass(r,"mce-fullscreen"),o=d.scrollPos,m.window.scrollTo(o.x,o.y),w.unbind(m.window,"resize",d.resizeHandler),e.off("remove",d.removeHandler),n.set(null),g(e,!1);else{var f={scrollPos:(c=w.getViewPort(),{x:c.x,y:c.y}),containerWidth:t.width,containerHeight:t.height,iframeWidth:i.width,iframeHeight:i.height,resizeHandler:a,removeHandler:h};i.width=i.height="100%",t.width=t.height="",w.addClass(s,"mce-fullscreen"),w.addClass(u,"mce-fullscreen"),w.addClass(r,"mce-fullscreen"),w.bind(m.window,"resize",a),e.on("remove",h),a(),n.set(f),g(e,!0)}},l=function(e,n){e.addCommand("mceFullScreen",function(){r(e,n)})},o=function(t){return function(e){var n=e.control;t.on("FullscreenStateChanged",function(e){n.active(e.state)})}},c=function(e){e.addMenuItem("fullscreen",{text:"Fullscreen",shortcut:"Ctrl+Shift+F",selectable:!0,cmd:"mceFullScreen",onPostRender:o(e),context:"view"}),e.addButton("fullscreen",{active:!1,tooltip:"Fullscreen",cmd:"mceFullScreen",onPostRender:o(e)})};e.add("fullscreen",function(e){var n=i(null);return e.settings.inline||(l(e,n),c(e),e.addShortcut("Ctrl+Shift+F","","mceFullScreen")),t(n)})}(window);
|
|
@ -1 +1 @@
|
||||||
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),c=tinymce.util.Tools.resolve("tinymce.EditorManager"),s=tinymce.util.Tools.resolve("tinymce.Env"),a=tinymce.util.Tools.resolve("tinymce.util.Delay"),y=tinymce.util.Tools.resolve("tinymce.util.Tools"),f=tinymce.util.Tools.resolve("tinymce.util.VK"),d=function(e){return e.getParam("tab_focus",e.getParam("tabfocus_elements",":prev,:next"))},m=t.DOM,n=function(e){e.keyCode!==f.TAB||e.ctrlKey||e.altKey||e.metaKey||e.preventDefault()},i=function(r){function e(n){var i,o,e,l;if(!(n.keyCode!==f.TAB||n.ctrlKey||n.altKey||n.metaKey||n.isDefaultPrevented())&&(1===(e=y.explode(d(r))).length&&(e[1]=e[0],e[0]=":prev"),o=n.shiftKey?":prev"===e[0]?u(-1):m.get(e[0]):":next"===e[1]?u(1):m.get(e[1]))){var t=c.get(o.id||o.name);o.id&&t?t.focus():a.setTimeout(function(){s.webkit||window.focus(),o.focus()},10),n.preventDefault()}function u(e){function t(t){return/INPUT|TEXTAREA|BUTTON/.test(t.tagName)&&c.get(n.id)&&-1!==t.tabIndex&&function e(t){return"BODY"===t.nodeName||"hidden"!==t.type&&"none"!==t.style.display&&"hidden"!==t.style.visibility&&e(t.parentNode)}(t)}if(o=m.select(":input:enabled,*[tabindex]:not(iframe)"),y.each(o,function(e,t){if(e.id===r.id)return i=t,!1}),0<e){for(l=i+1;l<o.length;l++)if(t(o[l]))return o[l]}else for(l=i-1;0<=l;l--)if(t(o[l]))return o[l];return null}}r.on("init",function(){r.inline&&m.setAttrib(r.getBody(),"tabIndex",null),r.on("keyup",n),s.gecko?r.on("keypress keydown",e):r.on("keydown",e)})};e.add("tabfocus",function(e){i(e)})}();
|
!function(c){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),s=tinymce.util.Tools.resolve("tinymce.EditorManager"),a=tinymce.util.Tools.resolve("tinymce.Env"),y=tinymce.util.Tools.resolve("tinymce.util.Delay"),f=tinymce.util.Tools.resolve("tinymce.util.Tools"),d=tinymce.util.Tools.resolve("tinymce.util.VK"),m=function(e){return e.getParam("tab_focus",e.getParam("tabfocus_elements",":prev,:next"))},v=t.DOM,n=function(e){e.keyCode!==d.TAB||e.ctrlKey||e.altKey||e.metaKey||e.preventDefault()},i=function(r){function e(n){var i,o,e,l;if(!(n.keyCode!==d.TAB||n.ctrlKey||n.altKey||n.metaKey||n.isDefaultPrevented())&&(1===(e=f.explode(m(r))).length&&(e[1]=e[0],e[0]=":prev"),o=n.shiftKey?":prev"===e[0]?u(-1):v.get(e[0]):":next"===e[1]?u(1):v.get(e[1]))){var t=s.get(o.id||o.name);o.id&&t?t.focus():y.setTimeout(function(){a.webkit||c.window.focus(),o.focus()},10),n.preventDefault()}function u(e){function t(t){return/INPUT|TEXTAREA|BUTTON/.test(t.tagName)&&s.get(n.id)&&-1!==t.tabIndex&&function e(t){return"BODY"===t.nodeName||"hidden"!==t.type&&"none"!==t.style.display&&"hidden"!==t.style.visibility&&e(t.parentNode)}(t)}if(o=v.select(":input:enabled,*[tabindex]:not(iframe)"),f.each(o,function(e,t){if(e.id===r.id)return i=t,!1}),0<e){for(l=i+1;l<o.length;l++)if(t(o[l]))return o[l]}else for(l=i-1;0<=l;l--)if(t(o[l]))return o[l];return null}}r.on("init",function(){r.inline&&v.setAttrib(r.getBody(),"tabIndex",null),r.on("keyup",n),a.gecko?r.on("keypress keydown",e):r.on("keydown",e)})};e.add("tabfocus",function(e){i(e)})}(window);
|
|
@ -117,6 +117,15 @@ If you are looking to alter CSS or JavaScript content please edit the source fil
|
||||||
|
|
||||||
The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
|
The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Security information for administering a BookStack instance can be found on the [documentation site here](https://www.bookstackapp.com/docs/admin/security/).
|
||||||
|
|
||||||
|
If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](http://eepurl.com/glIh8z).
|
||||||
|
|
||||||
|
If you would like to report a security concern in a more confidential manner than via a GitHub issue, You can directly email the lead maintainer [ssddanbrown](https://github.com/ssddanbrown). You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown). Alternatively you can send a DM via twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The BookStack source is provided under the MIT License.
|
The BookStack source is provided under the MIT License.
|
||||||
|
|
|
@ -1,4 +1 @@
|
||||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/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>
|
||||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
|
||||||
<path d="M0 0h24v24H0z" fill="none"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 161 B After Width: | Height: | Size: 166 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19.252 1.708H8.663a1.77 1.77 0 0 0-1.765 1.764v14.12c0 .97.794 1.764 1.765 1.764h10.59a1.77 1.77 0 0 0 1.764-1.765V3.472a1.77 1.77 0 0 0-1.765-1.764zM8.663 3.472h4.412v7.06L10.87 9.208l-2.206 1.324z"/><path d="M30.61 3.203h24v24h-24z" fill="none"/><path d="M2.966 6.61v14c0 1.1.9 2 2 2h14v-2h-14v-14z"/></svg>
|
After Width: | Height: | Size: 416 B |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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;
|
|
@ -6,24 +6,60 @@ class DropDown {
|
||||||
|
|
||||||
constructor(elem) {
|
constructor(elem) {
|
||||||
this.container = elem;
|
this.container = elem;
|
||||||
this.menu = elem.querySelector('ul');
|
this.menu = elem.querySelector('.dropdown-menu, [dropdown-menu]');
|
||||||
|
this.moveMenu = elem.hasAttribute('dropdown-move-menu');
|
||||||
this.toggle = elem.querySelector('[dropdown-toggle]');
|
this.toggle = elem.querySelector('[dropdown-toggle]');
|
||||||
|
this.body = document.body;
|
||||||
this.setupListeners();
|
this.setupListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show(event) {
|
||||||
|
this.hideAll();
|
||||||
|
|
||||||
this.menu.style.display = 'block';
|
this.menu.style.display = 'block';
|
||||||
this.menu.classList.add('anim', 'menuIn');
|
this.menu.classList.add('anim', 'menuIn');
|
||||||
this.container.addEventListener('mouseleave', this.hide.bind(this));
|
|
||||||
|
if (this.moveMenu) {
|
||||||
|
// Move to body to prevent being trapped within scrollable sections
|
||||||
|
this.rect = this.menu.getBoundingClientRect();
|
||||||
|
this.body.appendChild(this.menu);
|
||||||
|
this.menu.style.position = 'fixed';
|
||||||
|
this.menu.style.left = `${this.rect.left}px`;
|
||||||
|
this.menu.style.top = `${this.rect.top}px`;
|
||||||
|
this.menu.style.width = `${this.rect.width}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set listener to hide on mouse leave or window click
|
||||||
|
this.menu.addEventListener('mouseleave', this.hide.bind(this));
|
||||||
|
window.addEventListener('click', event => {
|
||||||
|
if (!this.menu.contains(event.target)) {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Focus on first input if existing
|
// Focus on first input if existing
|
||||||
let input = this.menu.querySelector('input');
|
let input = this.menu.querySelector('input');
|
||||||
if (input !== null) input.focus();
|
if (input !== null) input.focus();
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAll() {
|
||||||
|
for (let dropdown of window.components.dropdown) {
|
||||||
|
dropdown.hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.menu.style.display = 'none';
|
this.menu.style.display = 'none';
|
||||||
this.menu.classList.remove('anim', 'menuIn');
|
this.menu.classList.remove('anim', 'menuIn');
|
||||||
|
if (this.moveMenu) {
|
||||||
|
this.menu.style.position = '';
|
||||||
|
this.menu.style.left = '';
|
||||||
|
this.menu.style.top = '';
|
||||||
|
this.menu.style.width = '';
|
||||||
|
this.container.appendChild(this.menu);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupListeners() {
|
setupListeners() {
|
||||||
|
|
|
@ -5,15 +5,17 @@ class EntitySelector {
|
||||||
this.elem = elem;
|
this.elem = elem;
|
||||||
this.search = '';
|
this.search = '';
|
||||||
this.lastClick = 0;
|
this.lastClick = 0;
|
||||||
|
this.selectedItemData = null;
|
||||||
|
|
||||||
let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
|
const entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
|
||||||
let entityPermission = elem.hasAttribute('entity-permission') ? elem.getAttribute('entity-permission') : 'view';
|
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.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}&permission=${encodeURIComponent(entityPermission)}`);
|
||||||
|
|
||||||
this.input = elem.querySelector('[entity-selector-input]');
|
this.input = elem.querySelector('[entity-selector-input]');
|
||||||
this.searchInput = elem.querySelector('[entity-selector-search]');
|
this.searchInput = elem.querySelector('[entity-selector-search]');
|
||||||
this.loading = elem.querySelector('[entity-selector-loading]');
|
this.loading = elem.querySelector('[entity-selector-loading]');
|
||||||
this.resultsContainer = elem.querySelector('[entity-selector-results]');
|
this.resultsContainer = elem.querySelector('[entity-selector-results]');
|
||||||
|
this.addButton = elem.querySelector('[entity-selector-add-button]');
|
||||||
|
|
||||||
this.elem.addEventListener('click', this.onClick.bind(this));
|
this.elem.addEventListener('click', this.onClick.bind(this));
|
||||||
|
|
||||||
|
@ -26,10 +28,20 @@ class EntitySelector {
|
||||||
this.searchEntities(this.searchInput.value);
|
this.searchEntities(this.searchInput.value);
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.searchInput.addEventListener('keydown', event => {
|
this.searchInput.addEventListener('keydown', event => {
|
||||||
if (event.keyCode === 13) event.preventDefault();
|
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.showLoading();
|
||||||
this.initialLoad();
|
this.initialLoad();
|
||||||
}
|
}
|
||||||
|
@ -53,7 +65,7 @@ class EntitySelector {
|
||||||
|
|
||||||
searchEntities(searchTerm) {
|
searchEntities(searchTerm) {
|
||||||
this.input.value = '';
|
this.input.value = '';
|
||||||
let url = this.searchUrl + `&term=${encodeURIComponent(searchTerm)}`;
|
let url = `${this.searchUrl}&term=${encodeURIComponent(searchTerm)}`;
|
||||||
window.$http.get(url).then(resp => {
|
window.$http.get(url).then(resp => {
|
||||||
this.resultsContainer.innerHTML = resp.data;
|
this.resultsContainer.innerHTML = resp.data;
|
||||||
this.hideLoading();
|
this.hideLoading();
|
||||||
|
@ -68,49 +80,54 @@ class EntitySelector {
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(event) {
|
onClick(event) {
|
||||||
let t = event.target;
|
const listItem = event.target.closest('[data-entity-type]');
|
||||||
|
if (listItem) {
|
||||||
if (t.matches('.entity-list-item *')) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
let item = t.closest('[data-entity-type]');
|
this.selectItem(listItem);
|
||||||
this.selectItem(item);
|
|
||||||
} else if (t.matches('[data-entity-type]')) {
|
|
||||||
this.selectItem(t)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectItem(item) {
|
selectItem(item) {
|
||||||
let isDblClick = this.isDoubleClick();
|
const isDblClick = this.isDoubleClick();
|
||||||
let type = item.getAttribute('data-entity-type');
|
const type = item.getAttribute('data-entity-type');
|
||||||
let id = item.getAttribute('data-entity-id');
|
const id = item.getAttribute('data-entity-id');
|
||||||
let isSelected = !item.classList.contains('selected') || isDblClick;
|
const isSelected = (!item.classList.contains('selected') || isDblClick);
|
||||||
|
|
||||||
this.unselectAll();
|
this.unselectAll();
|
||||||
this.input.value = isSelected ? `${type}:${id}` : '';
|
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) {
|
if (isSelected) {
|
||||||
item.classList.add('selected');
|
item.classList.add('selected');
|
||||||
item.classList.add('primary-background');
|
this.selectedItemData = data;
|
||||||
|
} else {
|
||||||
|
window.$events.emit('entity-select-change', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDblClick && !isSelected) return;
|
if (!isDblClick && !isSelected) return;
|
||||||
|
|
||||||
let link = item.querySelector('.entity-list-item-link').getAttribute('href');
|
if (isDblClick) {
|
||||||
let name = item.querySelector('.entity-list-item-name').textContent;
|
this.confirmSelection(data);
|
||||||
let data = {id: Number(id), name: name, link: link};
|
}
|
||||||
|
if (isSelected) {
|
||||||
|
window.$events.emit('entity-select-change', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isDblClick) window.$events.emit('entity-select-confirm', data);
|
confirmSelection(data) {
|
||||||
if (isSelected) window.$events.emit('entity-select-change', data);
|
window.$events.emit('entity-select-confirm', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
unselectAll() {
|
unselectAll() {
|
||||||
let selected = this.elem.querySelectorAll('.selected');
|
let selected = this.elem.querySelectorAll('.selected');
|
||||||
for (let i = 0, len = selected.length; i < len; i++) {
|
for (let selectedElem of selected) {
|
||||||
selected[i].classList.remove('selected');
|
selectedElem.classList.remove('selected', 'primary-background');
|
||||||
selected[i].classList.remove('primary-background');
|
|
||||||
}
|
}
|
||||||
|
this.selectedItemData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,13 @@ class ExpandToggle {
|
||||||
|
|
||||||
constructor(elem) {
|
constructor(elem) {
|
||||||
this.elem = 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');
|
this.selector = elem.getAttribute('expand-toggle');
|
||||||
|
|
||||||
|
// Listener setup
|
||||||
elem.addEventListener('click', this.click.bind(this));
|
elem.addEventListener('click', this.click.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,11 +58,20 @@ class ExpandToggle {
|
||||||
|
|
||||||
click(event) {
|
click(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let matchingElems = document.querySelectorAll(this.selector);
|
|
||||||
for (let i = 0, len = matchingElems.length; i < len; i++) {
|
const matchingElems = document.querySelectorAll(this.selector);
|
||||||
this.isOpen ? this.close(matchingElems[i]) : this.open(matchingElems[i]);
|
for (let match of matchingElems) {
|
||||||
|
this.isOpen ? this.close(match) : this.open(match);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isOpen = !this.isOpen;
|
this.isOpen = !this.isOpen;
|
||||||
|
this.updateSystemAjax(this.isOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSystemAjax(isOpen) {
|
||||||
|
window.$http.patch(this.updateEndpoint, {
|
||||||
|
expand: isOpen ? 'true' : 'false'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -4,54 +4,50 @@ class ImagePicker {
|
||||||
constructor(elem) {
|
constructor(elem) {
|
||||||
this.elem = elem;
|
this.elem = elem;
|
||||||
this.imageElem = elem.querySelector('img');
|
this.imageElem = elem.querySelector('img');
|
||||||
this.input = elem.querySelector('input');
|
this.imageInput = elem.querySelector('input[type=file]');
|
||||||
|
this.resetInput = elem.querySelector('input[data-reset-input]');
|
||||||
|
this.removeInput = elem.querySelector('input[data-remove-input]');
|
||||||
|
|
||||||
this.isUsingIds = elem.getAttribute('data-current-id') !== '';
|
this.defaultImage = elem.getAttribute('data-default-image');
|
||||||
this.isResizing = elem.getAttribute('data-resize-height') && elem.getAttribute('data-resize-width');
|
|
||||||
this.isResizeCropping = elem.getAttribute('data-resize-crop') !== '';
|
|
||||||
|
|
||||||
let selectButton = elem.querySelector('button[data-action="show-image-manager"]');
|
const resetButton = elem.querySelector('button[data-action="reset-image"]');
|
||||||
selectButton.addEventListener('click', this.selectImage.bind(this));
|
|
||||||
|
|
||||||
let resetButton = elem.querySelector('button[data-action="reset-image"]');
|
|
||||||
resetButton.addEventListener('click', this.reset.bind(this));
|
resetButton.addEventListener('click', this.reset.bind(this));
|
||||||
|
|
||||||
let removeButton = elem.querySelector('button[data-action="remove-image"]');
|
const removeButton = elem.querySelector('button[data-action="remove-image"]');
|
||||||
if (removeButton) {
|
if (removeButton) {
|
||||||
removeButton.addEventListener('click', this.removeImage.bind(this));
|
removeButton.addEventListener('click', this.removeImage.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.imageInput.addEventListener('change', this.fileInputChange.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
selectImage() {
|
fileInputChange() {
|
||||||
window.ImageManager.show(image => {
|
this.resetInput.setAttribute('disabled', 'disabled');
|
||||||
if (!this.isResizing) {
|
if (this.removeInput) {
|
||||||
this.setImage(image);
|
this.removeInput.setAttribute('disabled', 'disabled');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let requestString = '/images/thumb/' + image.id + '/' + this.elem.getAttribute('data-resize-width') + '/' + this.elem.getAttribute('data-resize-height') + '/' + (this.isResizeCropping ? 'true' : 'false');
|
for (let file of this.imageInput.files) {
|
||||||
|
this.imageElem.src = window.URL.createObjectURL(file);
|
||||||
window.$http.get(window.baseUrl(requestString)).then(resp => {
|
}
|
||||||
image.url = resp.data.url;
|
this.imageElem.classList.remove('none');
|
||||||
this.setImage(image);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.setImage({id: 0, url: this.elem.getAttribute('data-default-image')});
|
this.imageInput.value = '';
|
||||||
|
this.imageElem.src = this.defaultImage;
|
||||||
|
this.resetInput.removeAttribute('disabled');
|
||||||
|
if (this.removeInput) {
|
||||||
|
this.removeInput.setAttribute('disabled', 'disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
setImage(image) {
|
|
||||||
this.imageElem.src = image.url;
|
|
||||||
this.input.value = this.isUsingIds ? image.id : image.url;
|
|
||||||
this.imageElem.classList.remove('none');
|
this.imageElem.classList.remove('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
removeImage() {
|
removeImage() {
|
||||||
this.imageElem.src = this.elem.getAttribute('data-default-image');
|
this.imageInput.value = '';
|
||||||
this.imageElem.classList.add('none');
|
this.imageElem.classList.add('none');
|
||||||
this.input.value = 'none';
|
this.removeInput.removeAttribute('disabled');
|
||||||
|
this.resetInput.setAttribute('disabled', 'disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,11 @@ import toggleSwitch from "./toggle-switch";
|
||||||
import pageDisplay from "./page-display";
|
import pageDisplay from "./page-display";
|
||||||
import shelfSort from "./shelf-sort";
|
import shelfSort from "./shelf-sort";
|
||||||
import homepageControl from "./homepage-control";
|
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 = {
|
const componentMapping = {
|
||||||
'dropdown': dropdown,
|
'dropdown': dropdown,
|
||||||
|
@ -41,6 +45,11 @@ const componentMapping = {
|
||||||
'page-display': pageDisplay,
|
'page-display': pageDisplay,
|
||||||
'shelf-sort': shelfSort,
|
'shelf-sort': shelfSort,
|
||||||
'homepage-control': homepageControl,
|
'homepage-control': homepageControl,
|
||||||
|
'header-mobile-toggle': headerMobileToggle,
|
||||||
|
'list-sort-control': listSortControl,
|
||||||
|
'tri-layout': triLayout,
|
||||||
|
'breadcrumb-listing': breadcrumbListing,
|
||||||
|
'permissions-table': permissionsTable,
|
||||||
};
|
};
|
||||||
|
|
||||||
window.components = {};
|
window.components = {};
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* ListSortControl
|
||||||
|
* Manages the logic for the control which provides list sorting options.
|
||||||
|
*/
|
||||||
|
class ListSortControl {
|
||||||
|
|
||||||
|
constructor(elem) {
|
||||||
|
this.elem = elem;
|
||||||
|
this.menu = elem.querySelector('ul');
|
||||||
|
|
||||||
|
this.sortInput = elem.querySelector('[name="sort"]');
|
||||||
|
this.orderInput = elem.querySelector('[name="order"]');
|
||||||
|
this.form = elem.querySelector('form');
|
||||||
|
|
||||||
|
this.menu.addEventListener('click', event => {
|
||||||
|
if (event.target.closest('[data-sort-value]') !== null) {
|
||||||
|
this.sortOptionClick(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.elem.addEventListener('click', 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;
|
|
@ -64,13 +64,26 @@ class MarkdownEditor {
|
||||||
let action = button.getAttribute('data-action');
|
let action = button.getAttribute('data-action');
|
||||||
if (action === 'insertImage') this.actionInsertImage();
|
if (action === 'insertImage') this.actionInsertImage();
|
||||||
if (action === 'insertLink') this.actionShowLinkSelector();
|
if (action === 'insertLink') this.actionShowLinkSelector();
|
||||||
if (action === 'insertDrawing' && event.ctrlKey) {
|
if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
|
||||||
this.actionShowImageManager();
|
this.actionShowImageManager();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === 'insertDrawing') this.actionStartDrawing();
|
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 => {
|
window.$events.listen('editor-markdown-update', value => {
|
||||||
this.cm.setValue(value);
|
this.cm.setValue(value);
|
||||||
this.updateAndRender();
|
this.updateAndRender();
|
||||||
|
@ -381,9 +394,7 @@ class MarkdownEditor {
|
||||||
const drawingId = imgContainer.getAttribute('drawio-diagram');
|
const drawingId = imgContainer.getAttribute('drawio-diagram');
|
||||||
|
|
||||||
DrawIO.show(() => {
|
DrawIO.show(() => {
|
||||||
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {
|
return DrawIO.load(drawingId);
|
||||||
return `data:image/png;base64,${resp.data.content}`;
|
|
||||||
});
|
|
||||||
}, (pngData) => {
|
}, (pngData) => {
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
|
|
|
@ -6,7 +6,7 @@ class Overlay {
|
||||||
elem.addEventListener('click', event => {
|
elem.addEventListener('click', event => {
|
||||||
if (event.target === elem) return this.hide();
|
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++) {
|
for (let i=0; i < closeButtons.length; i++) {
|
||||||
closeButtons[i].addEventListener('click', this.hide.bind(this));
|
closeButtons[i].addEventListener('click', this.hide.bind(this));
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ class PageComments {
|
||||||
commentElem.querySelector('[comment-edit-container]').style.display = 'block';
|
commentElem.querySelector('[comment-edit-container]').style.display = 'block';
|
||||||
let textArea = commentElem.querySelector('[comment-edit-container] textarea');
|
let textArea = commentElem.querySelector('[comment-edit-container] textarea');
|
||||||
let lineCount = textArea.value.split('\n').length;
|
let lineCount = textArea.value.split('\n').length;
|
||||||
textArea.style.height = (lineCount * 20) + 'px';
|
textArea.style.height = ((lineCount * 20) + 40) + 'px';
|
||||||
this.editingComment = commentElem;
|
this.editingComment = commentElem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +88,7 @@ class PageComments {
|
||||||
commentElem.parentNode.removeChild(commentElem);
|
commentElem.parentNode.removeChild(commentElem);
|
||||||
window.$events.emit('success', window.trans('entities.comment_deleted_success'));
|
window.$events.emit('success', window.trans('entities.comment_deleted_success'));
|
||||||
this.updateCount();
|
this.updateCount();
|
||||||
|
this.hideForm();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +130,7 @@ class PageComments {
|
||||||
showForm() {
|
showForm() {
|
||||||
this.formContainer.style.display = 'block';
|
this.formContainer.style.display = 'block';
|
||||||
this.formContainer.parentNode.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();
|
this.formInput.focus();
|
||||||
window.scrollToElement(this.formInput);
|
window.scrollToElement(this.formInput);
|
||||||
}
|
}
|
||||||
|
@ -137,7 +138,18 @@ class PageComments {
|
||||||
hideForm() {
|
hideForm() {
|
||||||
this.formContainer.style.display = 'none';
|
this.formContainer.style.display = 'none';
|
||||||
this.formContainer.parentNode.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) {
|
setReply(commentElem) {
|
||||||
|
|
|
@ -184,9 +184,9 @@ class PageDisplay {
|
||||||
|
|
||||||
setupNavHighlighting() {
|
setupNavHighlighting() {
|
||||||
// Check if support is present for IntersectionObserver
|
// Check if support is present for IntersectionObserver
|
||||||
if (!'IntersectionObserver' in window ||
|
if (!('IntersectionObserver' in window) ||
|
||||||
!'IntersectionObserverEntry' in window ||
|
!('IntersectionObserverEntry' in window) ||
|
||||||
!'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
|
!('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,8 +208,8 @@ class PageDisplay {
|
||||||
let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
|
let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
|
||||||
|
|
||||||
// observe each heading
|
// observe each heading
|
||||||
for (let i = 0; i !== headings.length; ++i) {
|
for (let heading of headings) {
|
||||||
pageNavObserver.observe(headings[i]);
|
pageNavObserver.observe(heading);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,14 +221,9 @@ class PageDisplay {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAnchorHighlighting(elementId, shouldHighlight) {
|
function toggleAnchorHighlighting(elementId, shouldHighlight) {
|
||||||
let anchorsToHighlight = pageNav.querySelectorAll('a[href="#' + elementId + '"]');
|
const anchorsToHighlight = pageNav.querySelectorAll('a[href="#' + elementId + '"]');
|
||||||
for (let i = 0; i < anchorsToHighlight.length; i++) {
|
for (let anchor of anchorsToHighlight) {
|
||||||
// Change below to use classList.toggle when IE support is dropped.
|
anchor.closest('li').classList.toggle('current-heading', shouldHighlight);
|
||||||
if (shouldHighlight) {
|
|
||||||
anchorsToHighlight[i].classList.add('current-heading');
|
|
||||||
} else {
|
|
||||||
anchorsToHighlight[i].classList.remove('current-heading');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -3,15 +3,15 @@ class ToggleSwitch {
|
||||||
|
|
||||||
constructor(elem) {
|
constructor(elem) {
|
||||||
this.elem = 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) {
|
onClick(event) {
|
||||||
let checked = this.input.value !== 'true';
|
let checked = this.checkbox.checked;
|
||||||
this.input.value = checked ? 'true' : 'false';
|
this.input.value = checked ? 'true' : 'false';
|
||||||
checked ? this.elem.classList.add('active') : this.elem.classList.remove('active');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -257,39 +257,38 @@ function drawIoPlugin() {
|
||||||
DrawIO.show(drawingInit, updateContent);
|
DrawIO.show(drawingInit, updateContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateContent(pngData) {
|
async function updateContent(pngData) {
|
||||||
let id = "image-" + Math.random().toString(16).slice(2);
|
const id = "image-" + Math.random().toString(16).slice(2);
|
||||||
let loadingImage = window.baseUrl('/loading.gif');
|
const loadingImage = window.baseUrl('/loading.gif');
|
||||||
let data = {
|
const pageId = Number(document.getElementById('page-editor').getAttribute('page-id'));
|
||||||
image: pngData,
|
|
||||||
uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle updating an existing image
|
// Handle updating an existing image
|
||||||
if (currentNode) {
|
if (currentNode) {
|
||||||
DrawIO.close();
|
DrawIO.close();
|
||||||
let imgElem = currentNode.querySelector('img');
|
let imgElem = currentNode.querySelector('img');
|
||||||
window.$http.post(window.baseUrl(`/images/drawing/upload`), data).then(resp => {
|
try {
|
||||||
pageEditor.dom.setAttrib(imgElem, 'src', resp.data.url);
|
const img = await DrawIO.upload(pngData, pageId);
|
||||||
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', resp.data.id);
|
pageEditor.dom.setAttrib(imgElem, 'src', img.url);
|
||||||
}).catch(err => {
|
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
|
||||||
|
} catch (err) {
|
||||||
window.$events.emit('error', trans('errors.image_upload_error'));
|
window.$events.emit('error', trans('errors.image_upload_error'));
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
|
pageEditor.insertContent(`<div drawio-diagram contenteditable="false"><img src="${loadingImage}" id="${id}"></div>`);
|
||||||
DrawIO.close();
|
DrawIO.close();
|
||||||
window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => {
|
try {
|
||||||
pageEditor.dom.setAttrib(id, 'src', resp.data.url);
|
const img = await DrawIO.upload(pngData, pageId);
|
||||||
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id);
|
pageEditor.dom.setAttrib(id, 'src', img.url);
|
||||||
}).catch(err => {
|
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
|
||||||
|
} catch (err) {
|
||||||
pageEditor.dom.remove(id);
|
pageEditor.dom.remove(id);
|
||||||
window.$events.emit('error', trans('errors.image_upload_error'));
|
window.$events.emit('error', trans('errors.image_upload_error'));
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
}
|
||||||
}, 5);
|
}, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,9 +299,7 @@ function drawIoPlugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let drawingId = currentNode.getAttribute('drawio-diagram');
|
let drawingId = currentNode.getAttribute('drawio-diagram');
|
||||||
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {
|
return DrawIO.load(drawingId);
|
||||||
return `data:image/png;base64,${resp.data.content}`;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.tinymce.PluginManager.add('drawio', function(editor, url) {
|
window.tinymce.PluginManager.add('drawio', function(editor, url) {
|
||||||
|
@ -432,7 +429,7 @@ class WysiwygEditor {
|
||||||
plugins: this.plugins,
|
plugins: this.plugins,
|
||||||
imagetools_toolbar: 'imageoptions',
|
imagetools_toolbar: 'imageoptions',
|
||||||
toolbar: this.getToolBar(),
|
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: [
|
style_formats: [
|
||||||
{title: "Header Large", format: "h2"},
|
{title: "Header Large", format: "h2"},
|
||||||
{title: "Header Medium", format: "h3"},
|
{title: "Header Medium", format: "h3"},
|
||||||
|
@ -517,6 +514,16 @@ class WysiwygEditor {
|
||||||
if (scrollId) {
|
if (scrollId) {
|
||||||
scrollToText(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() {
|
function editorChange() {
|
||||||
|
@ -600,6 +607,7 @@ class WysiwygEditor {
|
||||||
|
|
||||||
// Paste image-uploads
|
// Paste image-uploads
|
||||||
editor.on('paste', event => editorPaste(event, editor, context));
|
editor.on('paste', event => editorPaste(event, editor, context));
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,17 @@ import 'codemirror/mode/diff/diff';
|
||||||
import 'codemirror/mode/go/go';
|
import 'codemirror/mode/go/go';
|
||||||
import 'codemirror/mode/htmlmixed/htmlmixed';
|
import 'codemirror/mode/htmlmixed/htmlmixed';
|
||||||
import 'codemirror/mode/javascript/javascript';
|
import 'codemirror/mode/javascript/javascript';
|
||||||
|
import 'codemirror/mode/julia/julia';
|
||||||
import 'codemirror/mode/lua/lua';
|
import 'codemirror/mode/lua/lua';
|
||||||
|
import 'codemirror/mode/haskell/haskell';
|
||||||
import 'codemirror/mode/markdown/markdown';
|
import 'codemirror/mode/markdown/markdown';
|
||||||
|
import 'codemirror/mode/mllike/mllike';
|
||||||
import 'codemirror/mode/nginx/nginx';
|
import 'codemirror/mode/nginx/nginx';
|
||||||
import 'codemirror/mode/php/php';
|
import 'codemirror/mode/php/php';
|
||||||
import 'codemirror/mode/powershell/powershell';
|
import 'codemirror/mode/powershell/powershell';
|
||||||
import 'codemirror/mode/python/python';
|
import 'codemirror/mode/python/python';
|
||||||
import 'codemirror/mode/ruby/ruby';
|
import 'codemirror/mode/ruby/ruby';
|
||||||
|
import 'codemirror/mode/rust/rust';
|
||||||
import 'codemirror/mode/shell/shell';
|
import 'codemirror/mode/shell/shell';
|
||||||
import 'codemirror/mode/sql/sql';
|
import 'codemirror/mode/sql/sql';
|
||||||
import 'codemirror/mode/toml/toml';
|
import 'codemirror/mode/toml/toml';
|
||||||
|
@ -35,21 +39,29 @@ const modeMap = {
|
||||||
csharp: 'text/x-csharp',
|
csharp: 'text/x-csharp',
|
||||||
diff: 'diff',
|
diff: 'diff',
|
||||||
go: 'go',
|
go: 'go',
|
||||||
|
haskell: 'haskell',
|
||||||
|
hs: 'haskell',
|
||||||
html: 'htmlmixed',
|
html: 'htmlmixed',
|
||||||
javascript: 'javascript',
|
javascript: 'javascript',
|
||||||
json: {name: 'javascript', json: true},
|
json: {name: 'javascript', json: true},
|
||||||
js: 'javascript',
|
js: 'javascript',
|
||||||
|
jl: 'julia',
|
||||||
|
julia: 'julia',
|
||||||
lua: 'lua',
|
lua: 'lua',
|
||||||
md: 'markdown',
|
md: 'markdown',
|
||||||
mdown: 'markdown',
|
mdown: 'markdown',
|
||||||
markdown: 'markdown',
|
markdown: 'markdown',
|
||||||
|
ml: 'mllike',
|
||||||
nginx: 'nginx',
|
nginx: 'nginx',
|
||||||
powershell: 'powershell',
|
powershell: 'powershell',
|
||||||
|
ocaml: 'mllike',
|
||||||
php: 'php',
|
php: 'php',
|
||||||
py: 'python',
|
py: 'python',
|
||||||
python: 'python',
|
python: 'python',
|
||||||
ruby: 'ruby',
|
ruby: 'ruby',
|
||||||
|
rust: 'rust',
|
||||||
rb: 'ruby',
|
rb: 'ruby',
|
||||||
|
rs: 'rust',
|
||||||
shell: 'shell',
|
shell: 'shell',
|
||||||
sh: 'shell',
|
sh: 'shell',
|
||||||
bash: 'shell',
|
bash: 'shell',
|
||||||
|
|
|
@ -66,4 +66,23 @@ function drawPostMessage(data) {
|
||||||
iFrame.contentWindow.postMessage(JSON.stringify(data), '*');
|
iFrame.contentWindow.postMessage(JSON.stringify(data), '*');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {show, close};
|
async function upload(imageData, pageUploadedToId) {
|
||||||
|
let data = {
|
||||||
|
image: imageData,
|
||||||
|
uploaded_to: pageUploadedToId,
|
||||||
|
};
|
||||||
|
const resp = await window.$http.post(window.baseUrl(`/images/drawio`), data);
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load an existing image, by fetching it as Base64 from the system.
|
||||||
|
* @param drawingId
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async function load(drawingId) {
|
||||||
|
const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
|
||||||
|
return `data:image/png;base64,${resp.data.content}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {show, close, upload, load};
|
|
@ -1,7 +1,7 @@
|
||||||
import * as Dates from "../services/dates";
|
import * as Dates from "../services/dates";
|
||||||
import dropzone from "./components/dropzone";
|
import dropzone from "./components/dropzone";
|
||||||
|
|
||||||
let page = 0;
|
let page = 1;
|
||||||
let previousClickTime = 0;
|
let previousClickTime = 0;
|
||||||
let previousClickImage = 0;
|
let previousClickImage = 0;
|
||||||
let dataLoaded = false;
|
let dataLoaded = false;
|
||||||
|
@ -20,7 +20,7 @@ const data = {
|
||||||
selectedImage: false,
|
selectedImage: false,
|
||||||
dependantPages: false,
|
dependantPages: false,
|
||||||
showing: false,
|
showing: false,
|
||||||
view: 'all',
|
filter: null,
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
searching: false,
|
searching: false,
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
@ -56,32 +56,37 @@ const methods = {
|
||||||
this.$el.children[0].components.overlay.hide();
|
this.$el.children[0].components.overlay.hide();
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchData() {
|
async fetchData() {
|
||||||
let url = baseUrl + page;
|
let query = {
|
||||||
let query = {};
|
page,
|
||||||
if (this.uploadedTo !== false) query.page_id = this.uploadedTo;
|
search: this.searching ? this.searchTerm : null,
|
||||||
if (this.searching) query.term = this.searchTerm;
|
uploaded_to: this.uploadedTo || null,
|
||||||
|
filter_type: this.filter,
|
||||||
|
};
|
||||||
|
|
||||||
this.$http.get(url, {params: query}).then(response => {
|
const {data} = await this.$http.get(baseUrl, {params: query});
|
||||||
this.images = this.images.concat(response.data.images);
|
this.images = this.images.concat(data.images);
|
||||||
this.hasMore = response.data.hasMore;
|
this.hasMore = data.has_more;
|
||||||
page++;
|
page++;
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setView(viewName) {
|
setFilterType(filterType) {
|
||||||
this.view = viewName;
|
this.filter = filterType;
|
||||||
this.resetState();
|
this.resetState();
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
},
|
},
|
||||||
|
|
||||||
resetState() {
|
resetState() {
|
||||||
this.cancelSearch();
|
this.cancelSearch();
|
||||||
|
this.resetListView();
|
||||||
|
this.deleteConfirm = false;
|
||||||
|
baseUrl = window.baseUrl(`/images/${this.imageType}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
resetListView() {
|
||||||
this.images = [];
|
this.images = [];
|
||||||
this.hasMore = false;
|
this.hasMore = false;
|
||||||
this.deleteConfirm = false;
|
page = 1;
|
||||||
page = 0;
|
|
||||||
baseUrl = window.baseUrl(`/images/${this.imageType}/${this.view}/`);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
searchImages() {
|
searchImages() {
|
||||||
|
@ -94,10 +99,7 @@ const methods = {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.searching = true;
|
this.searching = true;
|
||||||
this.images = [];
|
this.resetListView();
|
||||||
this.hasMore = false;
|
|
||||||
page = 0;
|
|
||||||
baseUrl = window.baseUrl(`/images/${this.imageType}/search/`);
|
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -110,10 +112,10 @@ const methods = {
|
||||||
},
|
},
|
||||||
|
|
||||||
imageSelect(image) {
|
imageSelect(image) {
|
||||||
let dblClickTime = 300;
|
const dblClickTime = 300;
|
||||||
let currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
let timeDiff = currentTime - previousClickTime;
|
const timeDiff = currentTime - previousClickTime;
|
||||||
let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage;
|
const isDblClick = timeDiff < dblClickTime && image.id === previousClickImage;
|
||||||
|
|
||||||
if (isDblClick) {
|
if (isDblClick) {
|
||||||
this.callbackAndHide(image);
|
this.callbackAndHide(image);
|
||||||
|
@ -132,11 +134,11 @@ const methods = {
|
||||||
this.hide();
|
this.hide();
|
||||||
},
|
},
|
||||||
|
|
||||||
saveImageDetails() {
|
async saveImageDetails() {
|
||||||
let url = window.baseUrl(`/images/update/${this.selectedImage.id}`);
|
let url = window.baseUrl(`/images/${this.selectedImage.id}`);
|
||||||
this.$http.put(url, this.selectedImage).then(response => {
|
try {
|
||||||
this.$events.emit('success', trans('components.image_update_success'));
|
await this.$http.put(url, this.selectedImage)
|
||||||
}).catch(error => {
|
} catch (error) {
|
||||||
if (error.response.status === 422) {
|
if (error.response.status === 422) {
|
||||||
let errors = error.response.data;
|
let errors = error.response.data;
|
||||||
let message = '';
|
let message = '';
|
||||||
|
@ -145,27 +147,29 @@ const methods = {
|
||||||
});
|
});
|
||||||
this.$events.emit('error', message);
|
this.$events.emit('error', message);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteImage() {
|
async deleteImage() {
|
||||||
|
|
||||||
if (!this.deleteConfirm) {
|
if (!this.deleteConfirm) {
|
||||||
let url = window.baseUrl(`/images/usage/${this.selectedImage.id}`);
|
const url = window.baseUrl(`/images/usage/${this.selectedImage.id}`);
|
||||||
this.$http.get(url).then(resp => {
|
try {
|
||||||
this.dependantPages = resp.data;
|
const {data} = await this.$http.get(url);
|
||||||
}).catch(console.error).then(() => {
|
this.dependantPages = data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
this.deleteConfirm = true;
|
this.deleteConfirm = true;
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let url = window.baseUrl(`/images/${this.selectedImage.id}`);
|
|
||||||
this.$http.delete(url).then(resp => {
|
const url = window.baseUrl(`/images/${this.selectedImage.id}`);
|
||||||
|
await this.$http.delete(url);
|
||||||
this.images.splice(this.images.indexOf(this.selectedImage), 1);
|
this.images.splice(this.images.indexOf(this.selectedImage), 1);
|
||||||
this.selectedImage = false;
|
this.selectedImage = false;
|
||||||
this.$events.emit('success', trans('components.image_delete_success'));
|
this.$events.emit('success', trans('components.image_delete_success'));
|
||||||
this.deleteConfirm = false;
|
this.deleteConfirm = false;
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getDate(stringDate) {
|
getDate(stringDate) {
|
||||||
|
@ -180,7 +184,7 @@ const methods = {
|
||||||
|
|
||||||
const computed = {
|
const computed = {
|
||||||
uploadUrl() {
|
uploadUrl() {
|
||||||
return window.baseUrl(`/images/${this.imageType}/upload`);
|
return window.baseUrl(`/images/${this.imageType}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -188,7 +192,7 @@ function mounted() {
|
||||||
window.ImageManager = this;
|
window.ImageManager = this;
|
||||||
this.imageType = this.$el.getAttribute('image-type');
|
this.imageType = this.$el.getAttribute('image-type');
|
||||||
this.uploadedTo = this.$el.getAttribute('uploaded-to');
|
this.uploadedTo = this.$el.getAttribute('uploaded-to');
|
||||||
baseUrl = window.baseUrl('/images/' + this.imageType + '/all/')
|
baseUrl = window.baseUrl('/images/' + this.imageType)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -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 {
|
@keyframes loadingBob {
|
||||||
0% {
|
0% {
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
|
@ -90,7 +70,3 @@
|
||||||
animation-delay: 0s;
|
animation-delay: 0s;
|
||||||
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
|
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectFade {
|
|
||||||
transition: background-color ease-in-out 3000ms;
|
|
||||||
}
|
|
|
@ -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
|
* Callouts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.callout {
|
.callout {
|
||||||
border-left: 3px solid #BBB;
|
border-left: 3px solid #BBB;
|
||||||
background-color: #EEE;
|
background-color: #EEE;
|
||||||
|
@ -143,7 +14,7 @@
|
||||||
content: '';
|
content: '';
|
||||||
width: 1.2em;
|
width: 1.2em;
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
left: $-xs + 1px;
|
left: $-xs + 2px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
margin-top: -9px;
|
margin-top: -9px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -153,7 +24,7 @@
|
||||||
}
|
}
|
||||||
&.success {
|
&.success {
|
||||||
border-left-color: $positive;
|
border-left-color: $positive;
|
||||||
background-color: lighten($positive, 45%);
|
background-color: lighten($positive, 68%);
|
||||||
color: darken($positive, 16%);
|
color: darken($positive, 16%);
|
||||||
}
|
}
|
||||||
&.success:before {
|
&.success:before {
|
||||||
|
@ -161,7 +32,7 @@
|
||||||
}
|
}
|
||||||
&.danger {
|
&.danger {
|
||||||
border-left-color: $negative;
|
border-left-color: $negative;
|
||||||
background-color: lighten($negative, 34%);
|
background-color: lighten($negative, 56%);
|
||||||
color: darken($negative, 20%);
|
color: darken($negative, 20%);
|
||||||
}
|
}
|
||||||
&.danger:before {
|
&.danger:before {
|
||||||
|
@ -170,35 +41,27 @@
|
||||||
&.info {
|
&.info {
|
||||||
border-left-color: $info;
|
border-left-color: $info;
|
||||||
background-color: lighten($info, 50%);
|
background-color: lighten($info, 50%);
|
||||||
color: darken($info, 16%);
|
color: darken($info, 20%);
|
||||||
}
|
}
|
||||||
&.warning {
|
&.warning {
|
||||||
border-left-color: $warning;
|
border-left-color: $warning;
|
||||||
background-color: lighten($warning, 36%);
|
background-color: lighten($warning, 50%);
|
||||||
color: darken($warning, 16%);
|
color: darken($warning, 20%);
|
||||||
}
|
}
|
||||||
&.warning:before {
|
&.warning:before {
|
||||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4=");
|
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4=");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Card-style blocks
|
||||||
|
*/
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
margin: $-m;
|
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.2);
|
box-shadow: $bs-card;
|
||||||
h3 {
|
border-radius: 3px;
|
||||||
padding: $-m;
|
border: 1px solid transparent;
|
||||||
border-bottom: 1px solid #E8E8E8;
|
|
||||||
margin: 0;
|
|
||||||
font-size: $fs-s;
|
|
||||||
color: #888;
|
|
||||||
fill: #888;
|
|
||||||
font-weight: 400;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
h3 a {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.body, p.empty-text {
|
.body, p.empty-text {
|
||||||
padding: $-m;
|
padding: $-m;
|
||||||
}
|
}
|
||||||
|
@ -208,18 +71,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .card {
|
.card-title {
|
||||||
h3, .body, .empty-text {
|
padding: $-m $-m $-xs;
|
||||||
padding: $-s $-m;
|
margin: 0;
|
||||||
|
font-size: $fs-m;
|
||||||
|
color: #222;
|
||||||
|
fill: #222;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
.card-title a {
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.drag-card {
|
.card.drag-card {
|
||||||
border: 1px solid #DDD;
|
border: 1px solid #DDD;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0;
|
padding: 0 0 0 ($-s + 28px);
|
||||||
padding-left: $-s + 28px;
|
|
||||||
margin: $-s 0;
|
margin: $-s 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
.drag-card-action {
|
.drag-card-action {
|
||||||
|
@ -227,14 +95,12 @@
|
||||||
}
|
}
|
||||||
.handle, .drag-card-action {
|
.handle, .drag-card-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
padding-left: $-xs;
|
padding: 0 $-xs;
|
||||||
padding-right: $-xs;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #EEE;
|
background-color: #EEE;
|
||||||
}
|
}
|
||||||
|
@ -246,9 +112,6 @@
|
||||||
margin: $-s 0;
|
margin: $-s 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
> div.padded {
|
|
||||||
padding: $-s 0 !important;
|
|
||||||
}
|
|
||||||
.handle {
|
.handle {
|
||||||
background-color: #EEE;
|
background-color: #EEE;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -263,12 +126,89 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.well {
|
.grid-card {
|
||||||
background-color: #F8F8F8;
|
display: flex;
|
||||||
padding: $-m;
|
flex-direction: column;
|
||||||
border: 1px solid #DDD;
|
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 {
|
.tag-item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-bottom: $-xs;
|
margin-bottom: $-xs;
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
|
button {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
@mixin generate-button-colors($textColor, $backgroundColor) {
|
@mixin generate-button-colors($textColor, $backgroundColor) {
|
||||||
background-color: $backgroundColor;
|
background-color: $backgroundColor;
|
||||||
color: $textColor;
|
color: $textColor;
|
||||||
fill: $textColor;
|
fill: $textColor;
|
||||||
text-transform: uppercase;
|
|
||||||
border: 1px solid $backgroundColor;
|
border: 1px solid $backgroundColor;
|
||||||
vertical-align: top;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: lighten($backgroundColor, 8%);
|
background-color: lighten($backgroundColor, 8%);
|
||||||
//box-shadow: $bs-med;
|
|
||||||
text-decoration: none;
|
|
||||||
color: $textColor;
|
color: $textColor;
|
||||||
}
|
}
|
||||||
&:active {
|
&:active {
|
||||||
|
@ -18,7 +17,6 @@
|
||||||
&:focus {
|
&:focus {
|
||||||
background-color: lighten($backgroundColor, 4%);
|
background-color: lighten($backgroundColor, 4%);
|
||||||
box-shadow: $bs-light;
|
box-shadow: $bs-light;
|
||||||
text-decoration: none;
|
|
||||||
color: $textColor;
|
color: $textColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,42 +24,36 @@
|
||||||
// Button Specific Variables
|
// Button Specific Variables
|
||||||
$button-border-radius: 2px;
|
$button-border-radius: 2px;
|
||||||
|
|
||||||
.button-base {
|
.button {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: $fs-m;
|
font-size: 0.85rem;
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
padding: $-xs*1.3 $-m;
|
padding: $-xs*1.3 $-m;
|
||||||
margin: $-xs $-xs $-xs 0;
|
margin-top: $-xs;
|
||||||
|
margin-bottom: $-xs;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: none;
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
border-radius: $button-border-radius;
|
border-radius: $button-border-radius;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all ease-in-out 120ms;
|
transition: background-color ease-in-out 120ms, box-shadow ease-in-out 120ms;
|
||||||
box-shadow: 0;
|
box-shadow: none;
|
||||||
@include generate-button-colors(#EEE, $primary);
|
background-color: $primary;
|
||||||
|
color: #FFF;
|
||||||
|
fill: #FFF;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border: 1px solid $primary;
|
||||||
|
vertical-align: top;
|
||||||
|
&:hover, &:focus {
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
&:active {
|
||||||
.button, input[type="button"], input[type="submit"] {
|
background-color: darken($primary, 8%);
|
||||||
@extend .button-base;
|
|
||||||
&.pos {
|
|
||||||
@include generate-button-colors(#EEE, $positive);
|
|
||||||
}
|
|
||||||
&.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.button.primary {
|
||||||
|
@include generate-button-colors(#FFFFFF, $primary);
|
||||||
|
}
|
||||||
.button.outline {
|
.button.outline {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: #888;
|
color: #888;
|
||||||
|
@ -71,78 +63,38 @@ $button-border-radius: 2px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
background-color: #EEE;
|
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 {
|
.text-button {
|
||||||
@extend .link;
|
cursor: pointer;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.4em;
|
||||||
&:focus, &:active {
|
&:focus, &:active {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
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 {
|
.button.block {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
|
||||||
display: block;
|
|
||||||
&.text-left {
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.icon {
|
.button.icon {
|
||||||
|
@ -160,9 +112,7 @@ $button-border-radius: 2px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
padding: $-s $-m;
|
padding: $-s $-m ($-s - 2px) ($-m*2 + 24px);
|
||||||
padding-bottom: $-s - 2px;
|
|
||||||
padding-left: $-m*2 + 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button[disabled] {
|
.button[disabled] {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -3,11 +3,12 @@
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin: $-xl*2 $-xl;
|
margin: $-xl;
|
||||||
padding: $-m $-l;
|
padding: $-m $-l;
|
||||||
background-color: #EEE;
|
background-color: #FFF;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
box-shadow: $bs-card;
|
border-left: 6px solid currentColor;
|
||||||
|
box-shadow: $bs-large;
|
||||||
z-index: 999999;
|
z-index: 999999;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
|
@ -15,30 +16,31 @@
|
||||||
transform: translateX(580px);
|
transform: translateX(580px);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 42px 1fr;
|
grid-template-columns: 42px 1fr;
|
||||||
color: #FFF;
|
color: #444;
|
||||||
|
font-weight: 700;
|
||||||
span, svg {
|
span, svg {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
svg {
|
svg {
|
||||||
fill: #EEEEEE;
|
|
||||||
width: 2.8rem;
|
width: 2.8rem;
|
||||||
height: 2.8rem;
|
height: 2.8rem;
|
||||||
padding-right: $-s;
|
padding-right: $-s;
|
||||||
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
&.pos {
|
&.pos {
|
||||||
background-color: $positive;
|
color: $positive;
|
||||||
}
|
}
|
||||||
&.neg {
|
&.neg {
|
||||||
background-color: $negative;
|
color: $negative;
|
||||||
}
|
}
|
||||||
&.warning {
|
&.warning {
|
||||||
background-color: $secondary;
|
color: $warning;
|
||||||
}
|
}
|
||||||
&.showing {
|
&.showing {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
|
@ -54,13 +56,18 @@
|
||||||
transition: all ease-in-out 180ms;
|
transition: all ease-in-out 180ms;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
svg[data-icon="caret-right"] {
|
svg[data-icon="caret-right"] {
|
||||||
|
margin-right: 0;
|
||||||
|
font-size: 1rem;
|
||||||
transition: all ease-in-out 180ms;
|
transition: all ease-in-out 180ms;
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
transform-origin: 25% 50%;
|
transform-origin: 50% 50%;
|
||||||
}
|
}
|
||||||
&.open svg[data-icon="caret-right"] {
|
&.open svg[data-icon="caret-right"] {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
svg[data-icon="caret-right"] + * {
|
||||||
|
margin-left: $-xs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[overlay] {
|
[overlay] {
|
||||||
|
@ -110,7 +117,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.corner-button {
|
.popup-footer button, .popup-header-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -118,6 +125,16 @@
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
box-shadow: none;
|
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 {
|
.popup-header, .popup-footer {
|
||||||
|
@ -130,6 +147,9 @@
|
||||||
padding: 8px $-m;
|
padding: 8px $-m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.popup-footer {
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||||
height: 444px;
|
height: 444px;
|
||||||
min-height: 444px;
|
min-height: 444px;
|
||||||
|
@ -137,6 +157,9 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||||
#entity-selector-wrap .popup-body .form-group {
|
#entity-selector-wrap .popup-body .form-group {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.popup-body .entity-selector-container {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.image-manager-body {
|
.image-manager-body {
|
||||||
min-height: 70vh;
|
min-height: 70vh;
|
||||||
|
@ -583,27 +606,26 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-box {
|
.comment-box {
|
||||||
clear: left;
|
|
||||||
border: 1px solid #DDD;
|
border: 1px solid #DDD;
|
||||||
margin-bottom: $-s;
|
border-radius: 4px;
|
||||||
border-radius: 3px;
|
background-color: #FFF;
|
||||||
.content {
|
.content {
|
||||||
padding: $-s;
|
|
||||||
font-size: 0.666em;
|
font-size: 0.666em;
|
||||||
p, ul, ol {
|
p, ul, ol {
|
||||||
font-size: $fs-m;
|
font-size: $fs-m;
|
||||||
margin: .5em 0;
|
margin: .5em 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.reply-row {
|
.actions {
|
||||||
padding: $-xs $-s;
|
opacity: 0;
|
||||||
|
transition: opacity ease-in-out 120ms;
|
||||||
|
}
|
||||||
|
&:hover .actions {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-box .header {
|
.comment-box .header {
|
||||||
padding: $-xs $-s;
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
border-bottom: 1px solid #DDD;
|
|
||||||
.meta {
|
.meta {
|
||||||
img, a, span {
|
img, a, span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -627,3 +649,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||||
#tag-manager .drag-card {
|
#tag-manager .drag-card {
|
||||||
max-width: 500px;
|
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;
|
||||||
|
}
|
|
@ -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 {
|
.markdown-display {
|
||||||
padding: 0 $-m 0;
|
padding: 0 $-m 0;
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
|
@ -98,7 +126,7 @@ label {
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
font-size: 0.94em;
|
font-size: 0.94em;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #999;
|
color: #666;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
margin-bottom: 0.2em;
|
margin-bottom: 0.2em;
|
||||||
&.inline {
|
&.inline {
|
||||||
|
@ -139,56 +167,77 @@ input[type=date] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-switch {
|
.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;
|
user-select: none;
|
||||||
&:after {
|
display: inline-grid;
|
||||||
content: '';
|
grid-template-columns: (16px + $-s) 1fr;
|
||||||
display: block;
|
align-items: center;
|
||||||
position: relative;
|
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;
|
left: 0;
|
||||||
margin-top: -3px;
|
transition: transform ease-in-out 120ms;
|
||||||
width: 20px;
|
transform: scale(0);
|
||||||
height: 20px;
|
transform-origin: center center;
|
||||||
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%);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
input[type=checkbox] {
|
||||||
.toggle-switch-checkbox {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
input:checked + .toggle-switch {
|
input[type=checkbox]:checked + .custom-checkbox .svg-icon {
|
||||||
background-color: rgba($positive, 0.4);
|
transform: scale(1);
|
||||||
&:after {
|
}
|
||||||
left: 16px;
|
.custom-checkbox:hover {
|
||||||
background-color: $positive;
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
border: darken($positive, 20%);
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toggle-switch-list {
|
||||||
|
.toggle-switch {
|
||||||
|
margin: $-xs 0;
|
||||||
|
}
|
||||||
|
&.compact .toggle-switch {
|
||||||
|
margin: 1px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: $-s;
|
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%;
|
width: 100%;
|
||||||
min-height: 64px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,20 +246,20 @@ input:checked + .toggle-switch {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
.text-pos, .text-neg {
|
div.text-pos, div.text-neg, p.text-post, p.text-neg {
|
||||||
padding: $-xs 0;
|
padding: $-xs 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group[collapsible] {
|
.form-group[collapsible] {
|
||||||
margin-left: -$-m;
|
|
||||||
margin-right: -$-m;
|
|
||||||
padding: 0 $-m;
|
padding: 0 $-m;
|
||||||
border-top: 1px solid #DDD;
|
border: 1px solid #DDD;
|
||||||
border-bottom: 1px solid #DDD;
|
border-radius: 4px;
|
||||||
.collapse-title {
|
.collapse-title {
|
||||||
margin-left: -$-m;
|
margin-left: -$-m;
|
||||||
margin-right: -$-m;
|
margin-right: -$-m;
|
||||||
|
@ -238,9 +287,6 @@ input:checked + .toggle-switch {
|
||||||
&.open .collapse-title label:before {
|
&.open .collapse-title label:before {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
&+.form-group[collapsible] {
|
|
||||||
margin-top: -($-s + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-input-style {
|
.inline-input-style {
|
||||||
|
@ -304,6 +350,13 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
&.flexible input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.search-box-cancel {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.outline > input {
|
.outline > input {
|
||||||
|
@ -317,13 +370,8 @@ 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 {
|
.image-picker img {
|
||||||
background-color: #BBB;
|
background-color: #BBB;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -2,21 +2,31 @@
|
||||||
* Includes the main navigation header and the faded toolbar.
|
* 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 {
|
header {
|
||||||
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
z-index: 2;
|
z-index: 6;
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: $primary-dark;
|
background-color: $primary-dark;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
fill: #fff;
|
fill: #fff;
|
||||||
.padded {
|
|
||||||
padding: $-m;
|
|
||||||
}
|
|
||||||
border-bottom: 1px solid #DDD;
|
border-bottom: 1px solid #DDD;
|
||||||
|
box-shadow: $bs-card;
|
||||||
|
padding: $-xxs 0;
|
||||||
.links {
|
.links {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin-left: $-m;
|
|
||||||
}
|
}
|
||||||
.links a {
|
.links a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -28,15 +38,6 @@ header {
|
||||||
padding-left: $-m;
|
padding-left: $-m;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
@include smaller-than($screen-md) {
|
|
||||||
.links a {
|
|
||||||
padding-left: $-s;
|
|
||||||
padding-right: $-s;
|
|
||||||
}
|
|
||||||
.dropdown-container {
|
|
||||||
padding-left: $-s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.avatar, .user-name {
|
.avatar, .user-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
@ -63,27 +64,17 @@ header {
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
@include smaller-than($screen-md) {
|
@include between($l, $xl) {
|
||||||
padding-left: $-xs;
|
padding-left: $-xs;
|
||||||
.name {
|
.name {
|
||||||
display: none;
|
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 {
|
.header-search {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
@ -92,13 +83,16 @@ header .search-box {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
input {
|
input {
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
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;
|
color: #EEE;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
padding-left: 40px;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
fill: #EEE;
|
fill: #EEE;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
left: 16px;
|
||||||
svg {
|
svg {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
@ -115,20 +109,11 @@ header .search-box {
|
||||||
:-moz-placeholder { /* Firefox 18- */
|
:-moz-placeholder { /* Firefox 18- */
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
}
|
}
|
||||||
@include smaller-than($screen-lg) {
|
@include between($l, $xl) {
|
||||||
max-width: 250px;
|
|
||||||
}
|
|
||||||
@include smaller-than($l) {
|
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smaller-than($s) {
|
|
||||||
.header-search {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -151,10 +136,185 @@ header .search-box {
|
||||||
height: 43px;
|
height: 43px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs span.sep {
|
.mobile-menu-toggle {
|
||||||
color: #aaa;
|
color: #FFF;
|
||||||
|
fill: #FFF;
|
||||||
|
font-size: 2em;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 4px;
|
||||||
padding: 0 $-xs;
|
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;
|
||||||
|
cursor: pointer;
|
||||||
|
&: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 {
|
.faded {
|
||||||
a, button, span, span > div {
|
a, button, span, span > div {
|
||||||
color: #666;
|
color: #666;
|
||||||
|
@ -175,20 +335,9 @@ header .search-box {
|
||||||
padding: $-s;
|
padding: $-s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.faded-small {
|
.action-buttons .text-button {
|
||||||
color: #000;
|
|
||||||
fill: #000;
|
|
||||||
font-size: 0.9em;
|
|
||||||
background-color: $primary-faded;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-container {
|
|
||||||
background-color: #FFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs .text-button, .action-buttons .text-button {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: $-s;
|
padding: $-xs $-s;
|
||||||
&:last-child {
|
&:last-child {
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
@ -217,28 +366,12 @@ header .search-box {
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smaller-than($m) {
|
@include smaller-than($m) {
|
||||||
.breadcrumbs .text-button, .action-buttons .text-button {
|
.action-buttons .text-button {
|
||||||
padding: $-xs $-xs;
|
padding: $-xs $-xs;
|
||||||
}
|
}
|
||||||
.action-buttons .dropdown-container:last-child a {
|
.action-buttons .dropdown-container:last-child a {
|
||||||
padding-left: $-xs;
|
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 {
|
.nav-tabs {
|
||||||
|
@ -254,6 +387,3 @@ header .search-box {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.faded-small .nav-tabs a {
|
|
||||||
padding: $-s $-m;
|
|
||||||
}
|
|
|
@ -3,27 +3,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background-color: #FFFFFF;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
background-color: #F2F2F2;
|
||||||
&.flexbox {
|
&.flexbox {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
&.shaded {
|
|
||||||
background-color: #F2F2F2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-size: $fs-m;
|
font-size: $fs-m;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #616161;
|
color: #444;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
&.shaded {
|
|
||||||
background-color: #F2F2F2;
|
background-color: #F2F2F2;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-size: 100%;
|
|
||||||
}
|
|
|
@ -0,0 +1,316 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|