Merge branch 'master' into release
This commit is contained in:
commit
7caed3b0db
|
@ -0,0 +1,11 @@
|
||||||
|
### For Feature Requests
|
||||||
|
Desired Feature:
|
||||||
|
|
||||||
|
### For Bug Reports
|
||||||
|
PHP Version:
|
||||||
|
|
||||||
|
MySQL Version:
|
||||||
|
|
||||||
|
Expected Behavior:
|
||||||
|
|
||||||
|
Actual Behavior:
|
12
.travis.yml
12
.travis.yml
|
@ -6,8 +6,6 @@ php:
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- vendor
|
|
||||||
- node_modules
|
|
||||||
- $HOME/.composer/cache
|
- $HOME/.composer/cache
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
|
@ -17,19 +15,17 @@ addons:
|
||||||
- mysql-client-core-5.6
|
- mysql-client-core-5.6
|
||||||
- mysql-client-5.6
|
- mysql-client-5.6
|
||||||
|
|
||||||
before_install:
|
|
||||||
- npm install -g npm@latest
|
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- mysql -u root -e 'create database `bookstack-test`;'
|
- mysql -u root -e 'create database `bookstack-test`;'
|
||||||
- composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
|
- composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
|
||||||
- phpenv config-rm xdebug.ini
|
- phpenv config-rm xdebug.ini
|
||||||
- composer self-update
|
- composer self-update
|
||||||
|
- composer dump-autoload --no-interaction
|
||||||
- composer install --prefer-dist --no-interaction
|
- composer install --prefer-dist --no-interaction
|
||||||
- npm install
|
- php artisan clear-compiled -n
|
||||||
- ./node_modules/.bin/gulp
|
- php artisan optimize -n
|
||||||
- php artisan migrate --force -n --database=mysql_testing
|
- php artisan migrate --force -n --database=mysql_testing
|
||||||
- php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
|
- php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- vendor/bin/phpunit
|
- phpunit
|
|
@ -167,7 +167,8 @@ class Entity extends Ownable
|
||||||
foreach ($terms as $key => $term) {
|
foreach ($terms as $key => $term) {
|
||||||
$term = htmlentities($term, ENT_QUOTES);
|
$term = htmlentities($term, ENT_QUOTES);
|
||||||
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||||
if (preg_match('/\s/', $term)) {
|
if (preg_match('/".*?"/', $term)) {
|
||||||
|
$term = str_replace('"', '', $term);
|
||||||
$exactTerms[] = '%' . $term . '%';
|
$exactTerms[] = '%' . $term . '%';
|
||||||
$term = '"' . $term . '"';
|
$term = '"' . $term . '"';
|
||||||
} else {
|
} else {
|
||||||
|
@ -206,5 +207,5 @@ class Entity extends Ownable
|
||||||
|
|
||||||
return $search->orderBy($orderBy, 'desc');
|
return $search->orderBy($orderBy, 'desc');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,19 +47,44 @@ class Handler extends ExceptionHandler
|
||||||
{
|
{
|
||||||
// Handle notify exceptions which will redirect to the
|
// Handle notify exceptions which will redirect to the
|
||||||
// specified location then show a notification message.
|
// specified location then show a notification message.
|
||||||
if ($e instanceof NotifyException) {
|
if ($this->isExceptionType($e, NotifyException::class)) {
|
||||||
session()->flash('error', $e->message);
|
session()->flash('error', $this->getOriginalMessage($e));
|
||||||
return redirect($e->redirectLocation);
|
return redirect($e->redirectLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle pretty exceptions which will show a friendly application-fitting page
|
// Handle pretty exceptions which will show a friendly application-fitting page
|
||||||
// Which will include the basic message to point the user roughly to the cause.
|
// Which will include the basic message to point the user roughly to the cause.
|
||||||
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
|
if ($this->isExceptionType($e, PrettyException::class) && !config('app.debug')) {
|
||||||
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
|
$message = $this->getOriginalMessage($e);
|
||||||
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
|
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
|
||||||
return response()->view('errors/' . $code, ['message' => $message], $code);
|
return response()->view('errors/' . $code, ['message' => $message], $code);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::render($request, $e);
|
return parent::render($request, $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the exception chain to compare against the original exception type.
|
||||||
|
* @param Exception $e
|
||||||
|
* @param $type
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function isExceptionType(Exception $e, $type) {
|
||||||
|
do {
|
||||||
|
if (is_a($e, $type)) return true;
|
||||||
|
} while ($e = $e->getPrevious());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get original exception message.
|
||||||
|
* @param Exception $e
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getOriginalMessage(Exception $e) {
|
||||||
|
do {
|
||||||
|
$message = $e->getMessage();
|
||||||
|
} while ($e = $e->getPrevious());
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
use Exception;
|
class PrettyException extends \Exception {}
|
||||||
|
|
||||||
class PrettyException extends Exception {}
|
|
|
@ -3,7 +3,6 @@
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Repos\UserRepo;
|
use BookStack\Repos\UserRepo;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use BookStack\Http\Requests;
|
use BookStack\Http\Requests;
|
||||||
use BookStack\Repos\BookRepo;
|
use BookStack\Repos\BookRepo;
|
||||||
use BookStack\Repos\ChapterRepo;
|
use BookStack\Repos\ChapterRepo;
|
||||||
|
@ -180,21 +179,31 @@ class BookController extends Controller
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
$sortedBooks = [];
|
|
||||||
// Sort pages and chapters
|
// Sort pages and chapters
|
||||||
|
$sortedBooks = [];
|
||||||
|
$updatedModels = collect();
|
||||||
$sortMap = json_decode($request->get('sort-tree'));
|
$sortMap = json_decode($request->get('sort-tree'));
|
||||||
$defaultBookId = $book->id;
|
$defaultBookId = $book->id;
|
||||||
foreach ($sortMap as $index => $bookChild) {
|
|
||||||
$id = $bookChild->id;
|
// Loop through contents of provided map and update entities accordingly
|
||||||
|
foreach ($sortMap as $bookChild) {
|
||||||
|
$priority = $bookChild->sort;
|
||||||
|
$id = intval($bookChild->id);
|
||||||
$isPage = $bookChild->type == 'page';
|
$isPage = $bookChild->type == 'page';
|
||||||
$bookId = $this->bookRepo->exists($bookChild->book) ? $bookChild->book : $defaultBookId;
|
$bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId;
|
||||||
|
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
|
||||||
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
|
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
|
||||||
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
|
|
||||||
$model->priority = $index;
|
// Update models only if there's a change in parent chain or ordering.
|
||||||
if ($isPage) {
|
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
|
||||||
$model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter;
|
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
|
||||||
|
$model->priority = $priority;
|
||||||
|
if ($isPage) $model->chapter_id = $chapterId;
|
||||||
|
$model->save();
|
||||||
|
$updatedModels->push($model);
|
||||||
}
|
}
|
||||||
$model->save();
|
|
||||||
|
// Store involved books to be sorted later
|
||||||
if (!in_array($bookId, $sortedBooks)) {
|
if (!in_array($bookId, $sortedBooks)) {
|
||||||
$sortedBooks[] = $bookId;
|
$sortedBooks[] = $bookId;
|
||||||
}
|
}
|
||||||
|
@ -203,10 +212,12 @@ class BookController extends Controller
|
||||||
// Add activity for books
|
// Add activity for books
|
||||||
foreach ($sortedBooks as $bookId) {
|
foreach ($sortedBooks as $bookId) {
|
||||||
$updatedBook = $this->bookRepo->getById($bookId);
|
$updatedBook = $this->bookRepo->getById($bookId);
|
||||||
$this->bookRepo->updateBookPermissions($updatedBook);
|
|
||||||
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
|
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update permissions on changed models
|
||||||
|
$this->bookRepo->buildJointPermissions($updatedModels);
|
||||||
|
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -204,7 +204,7 @@ class ChapterController extends Controller
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->chapterRepo->changeBook($parent->id, $chapter);
|
$this->chapterRepo->changeBook($parent->id, $chapter, true);
|
||||||
Activity::add($chapter, 'chapter_move', $chapter->book->id);
|
Activity::add($chapter, 'chapter_move', $chapter->book->id);
|
||||||
session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
|
session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
class PageRevision extends Model
|
class PageRevision extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'html', 'text', 'markdown'];
|
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user that created the page revision
|
* Get the user that created the page revision
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
<?php
|
<?php namespace BookStack\Providers;
|
||||||
|
|
||||||
namespace BookStack\Providers;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use BookStack\User;
|
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use Alpha\B;
|
use Alpha\B;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use BookStack\Book;
|
use BookStack\Book;
|
||||||
use Views;
|
use Views;
|
||||||
|
@ -173,15 +174,6 @@ class BookRepo extends EntityRepo
|
||||||
$book->delete();
|
$book->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Alias method to update the book jointPermissions in the PermissionService.
|
|
||||||
* @param Book $book
|
|
||||||
*/
|
|
||||||
public function updateBookPermissions(Book $book)
|
|
||||||
{
|
|
||||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the next child element priority.
|
* Get the next child element priority.
|
||||||
* @param Book $book
|
* @param Book $book
|
||||||
|
|
|
@ -195,11 +195,12 @@ class ChapterRepo extends EntityRepo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the book relation of this chapter.
|
* Changes the book relation of this chapter.
|
||||||
* @param $bookId
|
* @param $bookId
|
||||||
* @param Chapter $chapter
|
* @param Chapter $chapter
|
||||||
|
* @param bool $rebuildPermissions
|
||||||
* @return Chapter
|
* @return Chapter
|
||||||
*/
|
*/
|
||||||
public function changeBook($bookId, Chapter $chapter)
|
public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
|
||||||
{
|
{
|
||||||
$chapter->book_id = $bookId;
|
$chapter->book_id = $bookId;
|
||||||
// Update related activity
|
// Update related activity
|
||||||
|
@ -213,9 +214,12 @@ class ChapterRepo extends EntityRepo
|
||||||
foreach ($chapter->pages as $page) {
|
foreach ($chapter->pages as $page) {
|
||||||
$this->pageRepo->changeBook($bookId, $page);
|
$this->pageRepo->changeBook($bookId, $page);
|
||||||
}
|
}
|
||||||
// Update permissions
|
|
||||||
$chapter->load('book');
|
// Update permissions if applicable
|
||||||
$this->permissionService->buildJointPermissionsForEntity($chapter->book);
|
if ($rebuildPermissions) {
|
||||||
|
$chapter->load('book');
|
||||||
|
$this->permissionService->buildJointPermissionsForEntity($chapter->book);
|
||||||
|
}
|
||||||
|
|
||||||
return $chapter;
|
return $chapter;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use BookStack\Entity;
|
||||||
use BookStack\Page;
|
use BookStack\Page;
|
||||||
use BookStack\Services\PermissionService;
|
use BookStack\Services\PermissionService;
|
||||||
use BookStack\User;
|
use BookStack\User;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class EntityRepo
|
class EntityRepo
|
||||||
|
@ -168,15 +169,16 @@ class EntityRepo
|
||||||
* @param $termString
|
* @param $termString
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function prepareSearchTerms($termString)
|
public function prepareSearchTerms($termString)
|
||||||
{
|
{
|
||||||
$termString = $this->cleanSearchTermString($termString);
|
$termString = $this->cleanSearchTermString($termString);
|
||||||
preg_match_all('/"(.*?)"/', $termString, $matches);
|
preg_match_all('/(".*?")/', $termString, $matches);
|
||||||
|
$terms = [];
|
||||||
if (count($matches[1]) > 0) {
|
if (count($matches[1]) > 0) {
|
||||||
$terms = $matches[1];
|
foreach ($matches[1] as $match) {
|
||||||
|
$terms[] = $match;
|
||||||
|
}
|
||||||
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
|
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
|
||||||
} else {
|
|
||||||
$terms = [];
|
|
||||||
}
|
}
|
||||||
if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
|
if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
|
||||||
return $terms;
|
return $terms;
|
||||||
|
@ -259,6 +261,15 @@ class EntityRepo
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias method to update the book jointPermissions in the PermissionService.
|
||||||
|
* @param Collection $collection collection on entities
|
||||||
|
*/
|
||||||
|
public function buildJointPermissions(Collection $collection)
|
||||||
|
{
|
||||||
|
$this->permissionService->buildJointPermissionsForEntities($collection);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -157,6 +157,8 @@ class PageRepo extends EntityRepo
|
||||||
$draftPage->draft = false;
|
$draftPage->draft = false;
|
||||||
|
|
||||||
$draftPage->save();
|
$draftPage->save();
|
||||||
|
$this->saveRevision($draftPage, 'Initial Publish');
|
||||||
|
|
||||||
return $draftPage;
|
return $draftPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,10 +310,9 @@ class PageRepo extends EntityRepo
|
||||||
*/
|
*/
|
||||||
public function updatePage(Page $page, $book_id, $input)
|
public function updatePage(Page $page, $book_id, $input)
|
||||||
{
|
{
|
||||||
// Save a revision before updating
|
// Hold the old details to compare later
|
||||||
if ($page->html !== $input['html'] || $page->name !== $input['name']) {
|
$oldHtml = $page->html;
|
||||||
$this->saveRevision($page);
|
$oldName = $page->name;
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent slug being updated if no name change
|
// Prevent slug being updated if no name change
|
||||||
if ($page->name !== $input['name']) {
|
if ($page->name !== $input['name']) {
|
||||||
|
@ -335,6 +336,11 @@ class PageRepo extends EntityRepo
|
||||||
// Remove all update drafts for this user & page.
|
// Remove all update drafts for this user & page.
|
||||||
$this->userUpdateDraftsQuery($page, $userId)->delete();
|
$this->userUpdateDraftsQuery($page, $userId)->delete();
|
||||||
|
|
||||||
|
// Save a revision after updating
|
||||||
|
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
|
||||||
|
$this->saveRevision($page, $input['summary']);
|
||||||
|
}
|
||||||
|
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,9 +366,10 @@ class PageRepo extends EntityRepo
|
||||||
/**
|
/**
|
||||||
* Saves a page revision into the system.
|
* Saves a page revision into the system.
|
||||||
* @param Page $page
|
* @param Page $page
|
||||||
|
* @param null|string $summary
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function saveRevision(Page $page)
|
public function saveRevision(Page $page, $summary = null)
|
||||||
{
|
{
|
||||||
$revision = $this->pageRevision->fill($page->toArray());
|
$revision = $this->pageRevision->fill($page->toArray());
|
||||||
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
|
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
|
||||||
|
@ -372,6 +379,7 @@ class PageRepo extends EntityRepo
|
||||||
$revision->created_by = auth()->user()->id;
|
$revision->created_by = auth()->user()->id;
|
||||||
$revision->created_at = $page->updated_at;
|
$revision->created_at = $page->updated_at;
|
||||||
$revision->type = 'version';
|
$revision->type = 'version';
|
||||||
|
$revision->summary = $summary;
|
||||||
$revision->save();
|
$revision->save();
|
||||||
// Clear old revisions
|
// Clear old revisions
|
||||||
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
|
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
|
||||||
|
|
|
@ -48,11 +48,13 @@ class ExportService
|
||||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||||
$oldImgString = $imgMatch;
|
$oldImgString = $imgMatch;
|
||||||
$srcString = $imageTagsOutput[2][$index];
|
$srcString = $imageTagsOutput[2][$index];
|
||||||
if (strpos(trim($srcString), 'http') !== 0) {
|
$isLocal = strpos(trim($srcString), 'http') !== 0;
|
||||||
$pathString = public_path($srcString);
|
if ($isLocal) {
|
||||||
|
$pathString = public_path(trim($srcString, '/'));
|
||||||
} else {
|
} else {
|
||||||
$pathString = $srcString;
|
$pathString = $srcString;
|
||||||
}
|
}
|
||||||
|
if ($isLocal && !file_exists($pathString)) continue;
|
||||||
$imageContent = file_get_contents($pathString);
|
$imageContent = file_get_contents($pathString);
|
||||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
||||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
||||||
|
|
|
@ -95,6 +95,7 @@ class ImageService
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$storage->put($fullPath, $imageData);
|
$storage->put($fullPath, $imageData);
|
||||||
|
$storage->setVisibility($fullPath, 'public');
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
|
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
|
||||||
}
|
}
|
||||||
|
@ -167,6 +168,7 @@ class ImageService
|
||||||
|
|
||||||
$thumbData = (string)$thumb->encode();
|
$thumbData = (string)$thumb->encode();
|
||||||
$storage->put($thumbFilePath, $thumbData);
|
$storage->put($thumbFilePath, $thumbData);
|
||||||
|
$storage->setVisibility($thumbFilePath, 'public');
|
||||||
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
|
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
|
||||||
|
|
||||||
return $this->getPublicUrl($thumbFilePath);
|
return $this->getPublicUrl($thumbFilePath);
|
||||||
|
@ -257,9 +259,15 @@ class ImageService
|
||||||
$storageUrl = config('filesystems.url');
|
$storageUrl = config('filesystems.url');
|
||||||
|
|
||||||
// Get the standard public s3 url if s3 is set as storage type
|
// Get the standard public s3 url if s3 is set as storage type
|
||||||
|
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
|
||||||
|
// region-based url will be used to prevent http issues.
|
||||||
if ($storageUrl == false && config('filesystems.default') === 's3') {
|
if ($storageUrl == false && config('filesystems.default') === 's3') {
|
||||||
$storageDetails = config('filesystems.disks.s3');
|
$storageDetails = config('filesystems.disks.s3');
|
||||||
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
if (strpos($storageDetails['bucket'], '.') === false) {
|
||||||
|
$storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
|
||||||
|
} else {
|
||||||
|
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->storageUrl = $storageUrl;
|
$this->storageUrl = $storageUrl;
|
||||||
|
@ -269,4 +277,4 @@ class ImageService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use BookStack\Ownable;
|
||||||
use BookStack\Page;
|
use BookStack\Page;
|
||||||
use BookStack\Role;
|
use BookStack\Role;
|
||||||
use BookStack\User;
|
use BookStack\User;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class PermissionService
|
class PermissionService
|
||||||
{
|
{
|
||||||
|
@ -25,6 +25,8 @@ class PermissionService
|
||||||
protected $jointPermission;
|
protected $jointPermission;
|
||||||
protected $role;
|
protected $role;
|
||||||
|
|
||||||
|
protected $entityCache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PermissionService constructor.
|
* PermissionService constructor.
|
||||||
* @param JointPermission $jointPermission
|
* @param JointPermission $jointPermission
|
||||||
|
@ -48,6 +50,57 @@ class PermissionService
|
||||||
$this->page = $page;
|
$this->page = $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the local entity cache and ensure it's empty
|
||||||
|
*/
|
||||||
|
protected function readyEntityCache()
|
||||||
|
{
|
||||||
|
$this->entityCache = [
|
||||||
|
'books' => collect(),
|
||||||
|
'chapters' => collect()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a book via ID, Checks local cache
|
||||||
|
* @param $bookId
|
||||||
|
* @return Book
|
||||||
|
*/
|
||||||
|
protected function getBook($bookId)
|
||||||
|
{
|
||||||
|
if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
|
||||||
|
return $this->entityCache['books']->get($bookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$book = $this->book->find($bookId);
|
||||||
|
if ($book === null) $book = false;
|
||||||
|
if (isset($this->entityCache['books'])) {
|
||||||
|
$this->entityCache['books']->put($bookId, $book);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $book;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a chapter via ID, Checks local cache
|
||||||
|
* @param $chapterId
|
||||||
|
* @return Book
|
||||||
|
*/
|
||||||
|
protected function getChapter($chapterId)
|
||||||
|
{
|
||||||
|
if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
|
||||||
|
return $this->entityCache['chapters']->get($chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$chapter = $this->chapter->find($chapterId);
|
||||||
|
if ($chapter === null) $chapter = false;
|
||||||
|
if (isset($this->entityCache['chapters'])) {
|
||||||
|
$this->entityCache['chapters']->put($chapterId, $chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $chapter;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the roles for the current user;
|
* Get the roles for the current user;
|
||||||
* @return array|bool
|
* @return array|bool
|
||||||
|
@ -76,6 +129,7 @@ class PermissionService
|
||||||
public function buildJointPermissions()
|
public function buildJointPermissions()
|
||||||
{
|
{
|
||||||
$this->jointPermission->truncate();
|
$this->jointPermission->truncate();
|
||||||
|
$this->readyEntityCache();
|
||||||
|
|
||||||
// Get all roles (Should be the most limited dimension)
|
// Get all roles (Should be the most limited dimension)
|
||||||
$roles = $this->role->with('permissions')->get();
|
$roles = $this->role->with('permissions')->get();
|
||||||
|
@ -97,7 +151,7 @@ class PermissionService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the entity jointPermissions for a particular entity.
|
* Rebuild the entity jointPermissions for a particular entity.
|
||||||
* @param Entity $entity
|
* @param Entity $entity
|
||||||
*/
|
*/
|
||||||
public function buildJointPermissionsForEntity(Entity $entity)
|
public function buildJointPermissionsForEntity(Entity $entity)
|
||||||
|
@ -116,6 +170,17 @@ class PermissionService
|
||||||
$this->createManyJointPermissions($entities, $roles);
|
$this->createManyJointPermissions($entities, $roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuild the entity jointPermissions for a collection of entities.
|
||||||
|
* @param Collection $entities
|
||||||
|
*/
|
||||||
|
public function buildJointPermissionsForEntities(Collection $entities)
|
||||||
|
{
|
||||||
|
$roles = $this->role->with('jointPermissions')->get();
|
||||||
|
$this->deleteManyJointPermissionsForEntities($entities);
|
||||||
|
$this->createManyJointPermissions($entities, $roles);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the entity jointPermissions for a particular role.
|
* Build the entity jointPermissions for a particular role.
|
||||||
* @param Role $role
|
* @param Role $role
|
||||||
|
@ -177,9 +242,14 @@ class PermissionService
|
||||||
*/
|
*/
|
||||||
protected function deleteManyJointPermissionsForEntities($entities)
|
protected function deleteManyJointPermissionsForEntities($entities)
|
||||||
{
|
{
|
||||||
|
$query = $this->jointPermission->newQuery();
|
||||||
foreach ($entities as $entity) {
|
foreach ($entities as $entity) {
|
||||||
$entity->jointPermissions()->delete();
|
$query->orWhere(function($query) use ($entity) {
|
||||||
|
$query->where('entity_id', '=', $entity->id)
|
||||||
|
->where('entity_type', '=', $entity->getMorphClass());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
$query->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -189,6 +259,7 @@ class PermissionService
|
||||||
*/
|
*/
|
||||||
protected function createManyJointPermissions($entities, $roles)
|
protected function createManyJointPermissions($entities, $roles)
|
||||||
{
|
{
|
||||||
|
$this->readyEntityCache();
|
||||||
$jointPermissions = [];
|
$jointPermissions = [];
|
||||||
foreach ($entities as $entity) {
|
foreach ($entities as $entity) {
|
||||||
foreach ($roles as $role) {
|
foreach ($roles as $role) {
|
||||||
|
@ -248,8 +319,9 @@ class PermissionService
|
||||||
} elseif ($entity->isA('chapter')) {
|
} elseif ($entity->isA('chapter')) {
|
||||||
|
|
||||||
if (!$entity->restricted) {
|
if (!$entity->restricted) {
|
||||||
$hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
|
$book = $this->getBook($entity->book_id);
|
||||||
$hasPermissiveAccessToBook = !$entity->book->restricted;
|
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
|
||||||
|
$hasPermissiveAccessToBook = !$book->restricted;
|
||||||
return $this->createJointPermissionDataArray($entity, $role, $action,
|
return $this->createJointPermissionDataArray($entity, $role, $action,
|
||||||
($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
|
($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
|
||||||
($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
|
($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
|
||||||
|
@ -261,11 +333,14 @@ class PermissionService
|
||||||
} elseif ($entity->isA('page')) {
|
} elseif ($entity->isA('page')) {
|
||||||
|
|
||||||
if (!$entity->restricted) {
|
if (!$entity->restricted) {
|
||||||
$hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
|
$book = $this->getBook($entity->book_id);
|
||||||
$hasPermissiveAccessToBook = !$entity->book->restricted;
|
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
|
||||||
$hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction);
|
$hasPermissiveAccessToBook = !$book->restricted;
|
||||||
$hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted;
|
|
||||||
$acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted);
|
$chapter = $this->getChapter($entity->chapter_id);
|
||||||
|
$hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
|
||||||
|
$hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
|
||||||
|
$acknowledgeChapter = ($chapter && $chapter->restricted);
|
||||||
|
|
||||||
$hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
|
$hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
|
||||||
$hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
|
$hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
|
||||||
|
|
|
@ -158,7 +158,7 @@ class SocialAuthService
|
||||||
$driver = trim(strtolower($socialDriver));
|
$driver = trim(strtolower($socialDriver));
|
||||||
|
|
||||||
if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
|
if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
|
||||||
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured;
|
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured("Your {$driver} social settings are not configured correctly.");
|
||||||
|
|
||||||
return $driver;
|
return $driver;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,33 +2,38 @@
|
||||||
|
|
||||||
use BookStack\Ownable;
|
use BookStack\Ownable;
|
||||||
|
|
||||||
if (!function_exists('versioned_asset')) {
|
/**
|
||||||
/**
|
* Get the path to a versioned file.
|
||||||
* Get the path to a versioned file.
|
*
|
||||||
*
|
* @param string $file
|
||||||
* @param string $file
|
* @return string
|
||||||
* @return string
|
* @throws Exception
|
||||||
*
|
*/
|
||||||
* @throws \InvalidArgumentException
|
function versioned_asset($file = '')
|
||||||
*/
|
{
|
||||||
function versioned_asset($file)
|
// Don't require css and JS assets for testing
|
||||||
{
|
if (config('app.env') === 'testing') return '';
|
||||||
static $manifest = null;
|
|
||||||
|
|
||||||
if (is_null($manifest)) {
|
static $manifest = null;
|
||||||
$manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);
|
$manifestPath = 'build/manifest.json';
|
||||||
|
|
||||||
|
if (is_null($manifest) && file_exists($manifestPath)) {
|
||||||
|
$manifest = json_decode(file_get_contents(public_path($manifestPath)), true);
|
||||||
|
} else if (!file_exists($manifestPath)) {
|
||||||
|
if (config('app.env') !== 'production') {
|
||||||
|
$path = public_path($manifestPath);
|
||||||
|
$error = "No {$path} file found, Ensure you have built the css/js assets using gulp.";
|
||||||
|
} else {
|
||||||
|
$error = "No {$manifestPath} file found, Ensure you are using the release version of BookStack";
|
||||||
}
|
}
|
||||||
|
throw new \Exception($error);
|
||||||
if (isset($manifest[$file])) {
|
|
||||||
return baseUrl($manifest[$file]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_exists(public_path($file))) {
|
|
||||||
return baseUrl($file);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($manifest[$file])) {
|
||||||
|
return baseUrl($manifest[$file]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddSummaryToPageRevisions extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('page_revisions', function ($table) {
|
||||||
|
$table->string('summary')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('page_revisions', function ($table) {
|
||||||
|
$table->dropColumn('summary');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,7 @@
|
||||||
<env name="DB_CONNECTION" value="mysql_testing"/>
|
<env name="DB_CONNECTION" value="mysql_testing"/>
|
||||||
<env name="MAIL_DRIVER" value="log"/>
|
<env name="MAIL_DRIVER" value="log"/>
|
||||||
<env name="AUTH_METHOD" value="standard"/>
|
<env name="AUTH_METHOD" value="standard"/>
|
||||||
<env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
|
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
|
||||||
<env name="LDAP_VERSION" value="3"/>
|
<env name="LDAP_VERSION" value="3"/>
|
||||||
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
|
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
|
||||||
<env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
|
<env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
|
||||||
|
|
|
@ -69,7 +69,7 @@ module.exports = function (ngApp, events) {
|
||||||
*/
|
*/
|
||||||
function callbackAndHide(returnData) {
|
function callbackAndHide(returnData) {
|
||||||
if (callback) callback(returnData);
|
if (callback) callback(returnData);
|
||||||
$scope.showing = false;
|
$scope.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -109,6 +109,7 @@ module.exports = function (ngApp, events) {
|
||||||
function show(doneCallback) {
|
function show(doneCallback) {
|
||||||
callback = doneCallback;
|
callback = doneCallback;
|
||||||
$scope.showing = true;
|
$scope.showing = true;
|
||||||
|
$('#image-manager').find('.overlay').css('display', 'flex').hide().fadeIn(240);
|
||||||
// Get initial images if they have not yet been loaded in.
|
// Get initial images if they have not yet been loaded in.
|
||||||
if (!dataLoaded) {
|
if (!dataLoaded) {
|
||||||
fetchData();
|
fetchData();
|
||||||
|
@ -131,6 +132,7 @@ module.exports = function (ngApp, events) {
|
||||||
*/
|
*/
|
||||||
$scope.hide = function () {
|
$scope.hide = function () {
|
||||||
$scope.showing = false;
|
$scope.showing = false;
|
||||||
|
$('#image-manager').find('.overlay').fadeOut(240);
|
||||||
};
|
};
|
||||||
|
|
||||||
var baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');
|
var baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');
|
||||||
|
@ -357,8 +359,6 @@ module.exports = function (ngApp, events) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a draft update into the system via an AJAX request.
|
* Save a draft update into the system via an AJAX request.
|
||||||
* @param title
|
|
||||||
* @param html
|
|
||||||
*/
|
*/
|
||||||
function saveDraft() {
|
function saveDraft() {
|
||||||
var data = {
|
var data = {
|
||||||
|
@ -373,9 +373,17 @@ module.exports = function (ngApp, events) {
|
||||||
var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
|
var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
|
||||||
$scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
|
$scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
|
||||||
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
|
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
|
||||||
|
showDraftSaveNotification();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showDraftSaveNotification() {
|
||||||
|
$scope.draftUpdated = true;
|
||||||
|
$timeout(() => {
|
||||||
|
$scope.draftUpdated = false;
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
$scope.forceDraftSave = function() {
|
$scope.forceDraftSave = function() {
|
||||||
saveDraft();
|
saveDraft();
|
||||||
};
|
};
|
||||||
|
|
|
@ -158,9 +158,22 @@ module.exports = function (ngApp, events) {
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
link: function (scope, element, attrs) {
|
link: function (scope, element, attrs) {
|
||||||
var menu = element.find('ul');
|
const menu = element.find('ul');
|
||||||
element.find('[dropdown-toggle]').on('click', function () {
|
element.find('[dropdown-toggle]').on('click', function () {
|
||||||
menu.show().addClass('anim menuIn');
|
menu.show().addClass('anim menuIn');
|
||||||
|
let inputs = menu.find('input');
|
||||||
|
let hasInput = inputs.length > 0;
|
||||||
|
if (hasInput) {
|
||||||
|
inputs.first().focus();
|
||||||
|
element.on('keypress', 'input', event => {
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
event.preventDefault();
|
||||||
|
menu.hide();
|
||||||
|
menu.removeClass('anim menuIn');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
element.mouseleave(function () {
|
element.mouseleave(function () {
|
||||||
menu.hide();
|
menu.hide();
|
||||||
menu.removeClass('anim menuIn');
|
menu.removeClass('anim menuIn');
|
||||||
|
@ -258,8 +271,6 @@ module.exports = function (ngApp, events) {
|
||||||
scope.mdModel = content;
|
scope.mdModel = content;
|
||||||
scope.mdChange(markdown(content));
|
scope.mdChange(markdown(content));
|
||||||
|
|
||||||
console.log('test');
|
|
||||||
|
|
||||||
element.on('change input', (event) => {
|
element.on('change input', (event) => {
|
||||||
content = element.val();
|
content = element.val();
|
||||||
$timeout(() => {
|
$timeout(() => {
|
||||||
|
@ -291,6 +302,7 @@ module.exports = function (ngApp, events) {
|
||||||
const input = element.find('[markdown-input] textarea').first();
|
const input = element.find('[markdown-input] textarea').first();
|
||||||
const display = element.find('.markdown-display').first();
|
const display = element.find('.markdown-display').first();
|
||||||
const insertImage = element.find('button[data-action="insertImage"]');
|
const insertImage = element.find('button[data-action="insertImage"]');
|
||||||
|
const insertEntityLink = element.find('button[data-action="insertEntityLink"]')
|
||||||
|
|
||||||
let currentCaretPos = 0;
|
let currentCaretPos = 0;
|
||||||
|
|
||||||
|
@ -342,6 +354,13 @@ module.exports = function (ngApp, events) {
|
||||||
input[0].selectionEnd = caretPos + (';
|
input[0].selectionEnd = caretPos + (';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert entity link shortcut
|
||||||
|
if (event.which === 75 && event.ctrlKey && event.shiftKey) {
|
||||||
|
showLinkSelector();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Pass key presses to controller via event
|
// Pass key presses to controller via event
|
||||||
scope.$emit('editor-keydown', event);
|
scope.$emit('editor-keydown', event);
|
||||||
});
|
});
|
||||||
|
@ -351,12 +370,109 @@ module.exports = function (ngApp, events) {
|
||||||
window.ImageManager.showExternal(image => {
|
window.ImageManager.showExternal(image => {
|
||||||
let caretPos = currentCaretPos;
|
let caretPos = currentCaretPos;
|
||||||
let currentContent = input.val();
|
let currentContent = input.val();
|
||||||
let mdImageText = "";
|
let mdImageText = "";
|
||||||
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
|
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
|
||||||
input.change();
|
input.change();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function showLinkSelector() {
|
||||||
|
window.showEntityLinkSelector((entity) => {
|
||||||
|
let selectionStart = currentCaretPos;
|
||||||
|
let selectionEnd = input[0].selectionEnd;
|
||||||
|
let textSelected = (selectionEnd !== selectionStart);
|
||||||
|
let currentContent = input.val();
|
||||||
|
|
||||||
|
if (textSelected) {
|
||||||
|
let selectedText = currentContent.substring(selectionStart, selectionEnd);
|
||||||
|
let linkText = `[${selectedText}](${entity.link})`;
|
||||||
|
input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionEnd));
|
||||||
|
} else {
|
||||||
|
let linkText = ` [${entity.name}](${entity.link}) `;
|
||||||
|
input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionStart))
|
||||||
|
}
|
||||||
|
input.change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
insertEntityLink.click(showLinkSelector);
|
||||||
|
|
||||||
|
// Upload and insert image on paste
|
||||||
|
function editorPaste(e) {
|
||||||
|
e = e.originalEvent;
|
||||||
|
if (!e.clipboardData) return
|
||||||
|
var items = e.clipboardData.items;
|
||||||
|
if (!items) return;
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
uploadImage(items[i].getAsFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.on('paste', editorPaste);
|
||||||
|
|
||||||
|
// Handle image drop, Uploads images to BookStack.
|
||||||
|
function handleImageDrop(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
let files = event.originalEvent.dataTransfer.files;
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
uploadImage(files[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.on('drop', handleImageDrop);
|
||||||
|
|
||||||
|
// Handle image upload and add image into markdown content
|
||||||
|
function uploadImage(file) {
|
||||||
|
if (file.type.indexOf('image') !== 0) return;
|
||||||
|
var formData = new FormData();
|
||||||
|
var ext = 'png';
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
if (file.name) {
|
||||||
|
var fileNameMatches = file.name.match(/\.(.+)$/);
|
||||||
|
if (fileNameMatches) {
|
||||||
|
ext = fileNameMatches[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert image into markdown
|
||||||
|
let id = "image-" + Math.random().toString(16).slice(2);
|
||||||
|
let selectStart = input[0].selectionStart;
|
||||||
|
let selectEnd = input[0].selectionEnd;
|
||||||
|
let content = input[0].value;
|
||||||
|
let selectText = content.substring(selectStart, selectEnd);
|
||||||
|
let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
|
||||||
|
let innerContent = ((selectEnd > selectStart) ? `![${selectText}]` : '![]') + `(${placeholderImage})`;
|
||||||
|
input[0].value = content.substring(0, selectStart) + innerContent + content.substring(selectEnd);
|
||||||
|
|
||||||
|
input.focus();
|
||||||
|
input[0].selectionStart = selectStart;
|
||||||
|
input[0].selectionEnd = selectStart;
|
||||||
|
|
||||||
|
let remoteFilename = "image-" + Date.now() + "." + ext;
|
||||||
|
formData.append('file', file, remoteFilename);
|
||||||
|
formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content'));
|
||||||
|
|
||||||
|
xhr.open('POST', window.baseUrl('/images/gallery/upload'));
|
||||||
|
xhr.onload = function () {
|
||||||
|
let selectStart = input[0].selectionStart;
|
||||||
|
if (xhr.status === 200 || xhr.status === 201) {
|
||||||
|
var result = JSON.parse(xhr.responseText);
|
||||||
|
input[0].value = input[0].value.replace(placeholderImage, result.thumbs.display);
|
||||||
|
input.change();
|
||||||
|
} else {
|
||||||
|
console.log('An error occurred uploading the image');
|
||||||
|
console.log(xhr.responseText);
|
||||||
|
input[0].value = input[0].value.replace(innerContent, '');
|
||||||
|
input.change();
|
||||||
|
}
|
||||||
|
input.focus();
|
||||||
|
input[0].selectionStart = selectStart;
|
||||||
|
input[0].selectionEnd = selectStart;
|
||||||
|
};
|
||||||
|
xhr.send(formData);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
@ -587,6 +703,58 @@ module.exports = function (ngApp, events) {
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
ngApp.directive('entityLinkSelector', [function($http) {
|
||||||
|
return {
|
||||||
|
restict: 'A',
|
||||||
|
link: function(scope, element, attrs) {
|
||||||
|
|
||||||
|
const selectButton = element.find('.entity-link-selector-confirm');
|
||||||
|
let callback = false;
|
||||||
|
let entitySelection = null;
|
||||||
|
|
||||||
|
// Handle entity selection change, Stores the selected entity locally
|
||||||
|
function entitySelectionChange(entity) {
|
||||||
|
entitySelection = entity;
|
||||||
|
if (entity === null) {
|
||||||
|
selectButton.attr('disabled', 'true');
|
||||||
|
} else {
|
||||||
|
selectButton.removeAttr('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events.listen('entity-select-change', entitySelectionChange);
|
||||||
|
|
||||||
|
// Handle selection confirm button click
|
||||||
|
selectButton.click(event => {
|
||||||
|
hide();
|
||||||
|
if (entitySelection !== null) callback(entitySelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show selector interface
|
||||||
|
function show() {
|
||||||
|
element.fadeIn(240);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide selector interface
|
||||||
|
function hide() {
|
||||||
|
element.fadeOut(240);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to confirmation of entity selections (doubleclick)
|
||||||
|
events.listen('entity-select-confirm', entity => {
|
||||||
|
hide();
|
||||||
|
callback(entity);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show entity selector, Accessible globally, and store the callback
|
||||||
|
window.showEntityLinkSelector = function(passedCallback) {
|
||||||
|
show();
|
||||||
|
callback = passedCallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}]);
|
||||||
|
|
||||||
|
|
||||||
ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) {
|
ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) {
|
||||||
return {
|
return {
|
||||||
|
@ -600,26 +768,60 @@ module.exports = function (ngApp, events) {
|
||||||
// Add input for forms
|
// Add input for forms
|
||||||
const input = element.find('[entity-selector-input]').first();
|
const input = element.find('[entity-selector-input]').first();
|
||||||
|
|
||||||
|
// Detect double click events
|
||||||
|
var lastClick = 0;
|
||||||
|
function isDoubleClick() {
|
||||||
|
let now = Date.now();
|
||||||
|
let answer = now - lastClick < 300;
|
||||||
|
lastClick = now;
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
// Listen to entity item clicks
|
// Listen to entity item clicks
|
||||||
element.on('click', '.entity-list a', function(event) {
|
element.on('click', '.entity-list a', function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
let item = $(this).closest('[data-entity-type]');
|
let item = $(this).closest('[data-entity-type]');
|
||||||
itemSelect(item);
|
itemSelect(item, isDoubleClick());
|
||||||
});
|
});
|
||||||
element.on('click', '[data-entity-type]', function(event) {
|
element.on('click', '[data-entity-type]', function(event) {
|
||||||
itemSelect($(this));
|
itemSelect($(this), isDoubleClick());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select entity action
|
// Select entity action
|
||||||
function itemSelect(item) {
|
function itemSelect(item, doubleClick) {
|
||||||
let entityType = item.attr('data-entity-type');
|
let entityType = item.attr('data-entity-type');
|
||||||
let entityId = item.attr('data-entity-id');
|
let entityId = item.attr('data-entity-id');
|
||||||
let isSelected = !item.hasClass('selected');
|
let isSelected = !item.hasClass('selected') || doubleClick;
|
||||||
element.find('.selected').removeClass('selected').removeClass('primary-background');
|
element.find('.selected').removeClass('selected').removeClass('primary-background');
|
||||||
if (isSelected) item.addClass('selected').addClass('primary-background');
|
if (isSelected) item.addClass('selected').addClass('primary-background');
|
||||||
let newVal = isSelected ? `${entityType}:${entityId}` : '';
|
let newVal = isSelected ? `${entityType}:${entityId}` : '';
|
||||||
input.val(newVal);
|
input.val(newVal);
|
||||||
|
|
||||||
|
if (!isSelected) {
|
||||||
|
events.emit('entity-select-change', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doubleClick && !isSelected) return;
|
||||||
|
|
||||||
|
let link = item.find('.entity-list-item-link').attr('href');
|
||||||
|
let name = item.find('.entity-list-item-name').text();
|
||||||
|
|
||||||
|
if (doubleClick) {
|
||||||
|
events.emit('entity-select-confirm', {
|
||||||
|
id: Number(entityId),
|
||||||
|
name: name,
|
||||||
|
link: link
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
events.emit('entity-select-change', {
|
||||||
|
id: Number(entityId),
|
||||||
|
name: name,
|
||||||
|
link: link
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get search url with correct types
|
// Get search url with correct types
|
||||||
|
|
|
@ -18,9 +18,12 @@ window.baseUrl = function(path) {
|
||||||
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
|
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
|
||||||
|
|
||||||
// Global Event System
|
// Global Event System
|
||||||
var Events = {
|
class EventManager {
|
||||||
listeners: {},
|
constructor() {
|
||||||
emit: function (eventName, eventData) {
|
this.listeners = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(eventName, eventData) {
|
||||||
if (typeof this.listeners[eventName] === 'undefined') return this;
|
if (typeof this.listeners[eventName] === 'undefined') return this;
|
||||||
var eventsToStart = this.listeners[eventName];
|
var eventsToStart = this.listeners[eventName];
|
||||||
for (let i = 0; i < eventsToStart.length; i++) {
|
for (let i = 0; i < eventsToStart.length; i++) {
|
||||||
|
@ -28,33 +31,35 @@ var Events = {
|
||||||
event(eventData);
|
event(eventData);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
},
|
}
|
||||||
listen: function (eventName, callback) {
|
|
||||||
|
listen(eventName, callback) {
|
||||||
if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
|
if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
|
||||||
this.listeners[eventName].push(callback);
|
this.listeners[eventName].push(callback);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.Events = Events;
|
window.Events = new EventManager();
|
||||||
|
|
||||||
|
|
||||||
var services = require('./services')(ngApp, Events);
|
var services = require('./services')(ngApp, window.Events);
|
||||||
var directives = require('./directives')(ngApp, Events);
|
var directives = require('./directives')(ngApp, window.Events);
|
||||||
var controllers = require('./controllers')(ngApp, Events);
|
var controllers = require('./controllers')(ngApp, window.Events);
|
||||||
|
|
||||||
//Global jQuery Config & Extensions
|
//Global jQuery Config & Extensions
|
||||||
|
|
||||||
// Smooth scrolling
|
// Smooth scrolling
|
||||||
jQuery.fn.smoothScrollTo = function () {
|
jQuery.fn.smoothScrollTo = function () {
|
||||||
if (this.length === 0) return;
|
if (this.length === 0) return;
|
||||||
$('body').animate({
|
let scrollElem = document.documentElement.scrollTop === 0 ? document.body : document.documentElement;
|
||||||
|
$(scrollElem).animate({
|
||||||
scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin
|
scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin
|
||||||
}, 800); // Adjust to change animations speed (ms)
|
}, 800); // Adjust to change animations speed (ms)
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Making contains text expression not worry about casing
|
// Making contains text expression not worry about casing
|
||||||
$.expr[":"].contains = $.expr.createPseudo(function (arg) {
|
jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
|
||||||
return function (elem) {
|
return function (elem) {
|
||||||
return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
|
return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
|
||||||
};
|
};
|
||||||
|
@ -104,13 +109,14 @@ $(function () {
|
||||||
var scrollTop = document.getElementById('back-to-top');
|
var scrollTop = document.getElementById('back-to-top');
|
||||||
var scrollTopBreakpoint = 1200;
|
var scrollTopBreakpoint = 1200;
|
||||||
window.addEventListener('scroll', function() {
|
window.addEventListener('scroll', function() {
|
||||||
if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) {
|
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||||
|
if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) {
|
||||||
scrollTop.style.display = 'block';
|
scrollTop.style.display = 'block';
|
||||||
scrollTopShowing = true;
|
scrollTopShowing = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
scrollTop.style.opacity = 0.4;
|
scrollTop.style.opacity = 0.4;
|
||||||
}, 1);
|
}, 1);
|
||||||
} else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) {
|
} else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) {
|
||||||
scrollTop.style.opacity = 0;
|
scrollTop.style.opacity = 0;
|
||||||
scrollTopShowing = false;
|
scrollTopShowing = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -124,6 +130,27 @@ $(function () {
|
||||||
$('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
|
$('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Popup close
|
||||||
|
$('.popup-close').click(function() {
|
||||||
|
$(this).closest('.overlay').fadeOut(240);
|
||||||
|
});
|
||||||
|
$('.overlay').click(function(event) {
|
||||||
|
if (!$(event.target).hasClass('overlay')) return;
|
||||||
|
$(this).fadeOut(240);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent markdown display link click redirect
|
||||||
|
$('.markdown-display').on('click', 'a', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.open($(this).attr('href'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detect IE for css
|
||||||
|
if(navigator.userAgent.indexOf('MSIE')!==-1
|
||||||
|
|| navigator.appVersion.indexOf('Trident/') > 0
|
||||||
|
|| navigator.userAgent.indexOf('Safari') !== -1){
|
||||||
|
$('body').addClass('flexbox-support');
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,65 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle pasting images from clipboard.
|
||||||
|
* @param e - event
|
||||||
|
* @param editor - editor instance
|
||||||
|
*/
|
||||||
|
function editorPaste(e, editor) {
|
||||||
|
if (!e.clipboardData) return
|
||||||
|
let items = e.clipboardData.items;
|
||||||
|
if (!items) return;
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].type.indexOf("image") === -1) return
|
||||||
|
|
||||||
|
let file = items[i].getAsFile();
|
||||||
|
let formData = new FormData();
|
||||||
|
let ext = 'png';
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
|
||||||
|
if (file.name) {
|
||||||
|
let fileNameMatches = file.name.match(/\.(.+)$/);
|
||||||
|
if (fileNameMatches) {
|
||||||
|
ext = fileNameMatches[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = "image-" + Math.random().toString(16).slice(2);
|
||||||
|
let loadingImage = window.baseUrl('/loading.gif');
|
||||||
|
editor.execCommand('mceInsertContent', false, `<img src="${loadingImage}" id="${id}">`);
|
||||||
|
|
||||||
|
let remoteFilename = "image-" + Date.now() + "." + ext;
|
||||||
|
formData.append('file', file, remoteFilename);
|
||||||
|
formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content'));
|
||||||
|
|
||||||
|
xhr.open('POST', window.baseUrl('/images/gallery/upload'));
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.status === 200 || xhr.status === 201) {
|
||||||
|
let result = JSON.parse(xhr.responseText);
|
||||||
|
editor.dom.setAttrib(id, 'src', result.thumbs.display);
|
||||||
|
} else {
|
||||||
|
console.log('An error occurred uploading the image', xhr.responseText);
|
||||||
|
editor.dom.remove(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(formData);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerEditorShortcuts(editor) {
|
||||||
|
// Headers
|
||||||
|
for (let i = 1; i < 5; i++) {
|
||||||
|
editor.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other block shortcuts
|
||||||
|
editor.addShortcut('ctrl+q', '', ['FormatBlock', false, 'blockquote']);
|
||||||
|
editor.addShortcut('ctrl+d', '', ['FormatBlock', false, 'p']);
|
||||||
|
editor.addShortcut('ctrl+e', '', ['FormatBlock', false, 'pre']);
|
||||||
|
editor.addShortcut('ctrl+s', '', ['FormatBlock', false, 'code']);
|
||||||
|
}
|
||||||
|
|
||||||
var mceOptions = module.exports = {
|
var mceOptions = module.exports = {
|
||||||
selector: '#html-editor',
|
selector: '#html-editor',
|
||||||
content_css: [
|
content_css: [
|
||||||
|
@ -6,6 +68,8 @@ var mceOptions = module.exports = {
|
||||||
],
|
],
|
||||||
body_class: 'page-content',
|
body_class: 'page-content',
|
||||||
relative_urls: false,
|
relative_urls: false,
|
||||||
|
remove_script_host: false,
|
||||||
|
document_base_url: window.baseUrl('/'),
|
||||||
statusbar: false,
|
statusbar: false,
|
||||||
menubar: false,
|
menubar: false,
|
||||||
paste_data_images: false,
|
paste_data_images: false,
|
||||||
|
@ -38,23 +102,41 @@ var mceOptions = module.exports = {
|
||||||
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
|
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
|
||||||
},
|
},
|
||||||
file_browser_callback: function (field_name, url, type, win) {
|
file_browser_callback: function (field_name, url, type, win) {
|
||||||
window.ImageManager.showExternal(function (image) {
|
|
||||||
win.document.getElementById(field_name).value = image.url;
|
if (type === 'file') {
|
||||||
if ("createEvent" in document) {
|
window.showEntityLinkSelector(function(entity) {
|
||||||
var evt = document.createEvent("HTMLEvents");
|
let originalField = win.document.getElementById(field_name);
|
||||||
evt.initEvent("change", false, true);
|
originalField.value = entity.link;
|
||||||
win.document.getElementById(field_name).dispatchEvent(evt);
|
$(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
|
||||||
} else {
|
});
|
||||||
win.document.getElementById(field_name).fireEvent("onchange");
|
}
|
||||||
}
|
|
||||||
var html = '<a href="' + image.url + '" target="_blank">';
|
if (type === 'image') {
|
||||||
html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">';
|
// Show image manager
|
||||||
html += '</a>';
|
window.ImageManager.showExternal(function (image) {
|
||||||
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
|
|
||||||
});
|
// Set popover link input to image url then fire change event
|
||||||
|
// to ensure the new value sticks
|
||||||
|
win.document.getElementById(field_name).value = image.url;
|
||||||
|
if ("createEvent" in document) {
|
||||||
|
let evt = document.createEvent("HTMLEvents");
|
||||||
|
evt.initEvent("change", false, true);
|
||||||
|
win.document.getElementById(field_name).dispatchEvent(evt);
|
||||||
|
} else {
|
||||||
|
win.document.getElementById(field_name).fireEvent("onchange");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the actively selected content with the linked image
|
||||||
|
let html = `<a href="${image.url}" target="_blank">`;
|
||||||
|
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
|
||||||
|
html += '</a>';
|
||||||
|
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
paste_preprocess: function (plugin, args) {
|
paste_preprocess: function (plugin, args) {
|
||||||
var content = args.content;
|
let content = args.content;
|
||||||
if (content.indexOf('<img src="file://') !== -1) {
|
if (content.indexOf('<img src="file://') !== -1) {
|
||||||
args.content = '';
|
args.content = '';
|
||||||
}
|
}
|
||||||
|
@ -62,10 +144,14 @@ var mceOptions = module.exports = {
|
||||||
extraSetups: [],
|
extraSetups: [],
|
||||||
setup: function (editor) {
|
setup: function (editor) {
|
||||||
|
|
||||||
for (var i = 0; i < mceOptions.extraSetups.length; i++) {
|
// Run additional setup actions
|
||||||
|
// Used by the angular side of things
|
||||||
|
for (let i = 0; i < mceOptions.extraSetups.length; i++) {
|
||||||
mceOptions.extraSetups[i](editor);
|
mceOptions.extraSetups[i](editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerEditorShortcuts(editor);
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
var wrap;
|
var wrap;
|
||||||
|
|
||||||
|
@ -76,12 +162,11 @@ var mceOptions = module.exports = {
|
||||||
editor.on('dragstart', function () {
|
editor.on('dragstart', function () {
|
||||||
var node = editor.selection.getNode();
|
var node = editor.selection.getNode();
|
||||||
|
|
||||||
if (node.nodeName === 'IMG') {
|
if (node.nodeName !== 'IMG') return;
|
||||||
wrap = editor.dom.getParent(node, '.mceTemp');
|
wrap = editor.dom.getParent(node, '.mceTemp');
|
||||||
|
|
||||||
if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
|
if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
|
||||||
wrap = node.parentNode;
|
wrap = node.parentNode;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -106,15 +191,15 @@ var mceOptions = module.exports = {
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Image picker button
|
// Custom Image picker button
|
||||||
editor.addButton('image-insert', {
|
editor.addButton('image-insert', {
|
||||||
title: 'My title',
|
title: 'My title',
|
||||||
icon: 'image',
|
icon: 'image',
|
||||||
tooltip: 'Insert an image',
|
tooltip: 'Insert an image',
|
||||||
onclick: function () {
|
onclick: function () {
|
||||||
window.ImageManager.showExternal(function (image) {
|
window.ImageManager.showExternal(function (image) {
|
||||||
var html = '<a href="' + image.url + '" target="_blank">';
|
let html = `<a href="${image.url}" target="_blank">`;
|
||||||
html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">';
|
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
|
||||||
html += '</a>';
|
html += '</a>';
|
||||||
editor.execCommand('mceInsertContent', false, html);
|
editor.execCommand('mceInsertContent', false, html);
|
||||||
});
|
});
|
||||||
|
@ -122,49 +207,8 @@ var mceOptions = module.exports = {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Paste image-uploads
|
// Paste image-uploads
|
||||||
editor.on('paste', function (e) {
|
editor.on('paste', function(event) {
|
||||||
if (e.clipboardData) {
|
editorPaste(event, editor);
|
||||||
var items = e.clipboardData.items;
|
|
||||||
if (items) {
|
|
||||||
for (var i = 0; i < items.length; i++) {
|
|
||||||
if (items[i].type.indexOf("image") !== -1) {
|
|
||||||
|
|
||||||
var file = items[i].getAsFile();
|
|
||||||
var formData = new FormData();
|
|
||||||
var ext = 'png';
|
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
if (file.name) {
|
|
||||||
var fileNameMatches = file.name.match(/\.(.+)$/);
|
|
||||||
if (fileNameMatches) {
|
|
||||||
ext = fileNameMatches[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = "image-" + Math.random().toString(16).slice(2);
|
|
||||||
editor.execCommand('mceInsertContent', false, '<img src="/loading.gif" id="' + id + '">');
|
|
||||||
|
|
||||||
var remoteFilename = "image-" + Date.now() + "." + ext;
|
|
||||||
formData.append('file', file, remoteFilename);
|
|
||||||
formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content'));
|
|
||||||
|
|
||||||
xhr.open('POST', window.baseUrl('/images/gallery/upload'));
|
|
||||||
xhr.onload = function () {
|
|
||||||
if (xhr.status === 200 || xhr.status === 201) {
|
|
||||||
var result = JSON.parse(xhr.responseText);
|
|
||||||
editor.dom.setAttrib(id, 'src', result.url);
|
|
||||||
} else {
|
|
||||||
console.log('An error occured uploading the image');
|
|
||||||
console.log(xhr.responseText);
|
|
||||||
editor.dom.remove(id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.send(formData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -100,3 +100,13 @@ $button-border-radius: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button[disabled] {
|
||||||
|
background-color: #BBB;
|
||||||
|
cursor: default;
|
||||||
|
&:hover {
|
||||||
|
background-color: #BBB;
|
||||||
|
cursor: default;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.overlay {
|
.overlay {
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.333);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 95536;
|
z-index: 95536;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -10,26 +10,76 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-manager-body {
|
.popup-body-wrap {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-body {
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
max-height: 90%;
|
max-height: 90%;
|
||||||
width: 90%;
|
width: 1200px;
|
||||||
height: 90%;
|
height: auto;
|
||||||
margin: 2% 5%;
|
margin: 2% 5%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);
|
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
display: flex;
|
display: flex;
|
||||||
h1, h2, h3 {
|
flex-direction: column;
|
||||||
font-weight: 300;
|
&.small {
|
||||||
|
margin: 2% auto;
|
||||||
|
width: 800px;
|
||||||
|
max-width: 90%;
|
||||||
}
|
}
|
||||||
|
&:before {
|
||||||
|
display: flex;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//body.ie .popup-body {
|
||||||
|
// min-height: 100%;
|
||||||
|
//}
|
||||||
|
|
||||||
|
.corner-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-header, .popup-footer {
|
||||||
|
display: block !important;
|
||||||
|
position: relative;
|
||||||
|
height: 40px;
|
||||||
|
flex: none !important;
|
||||||
|
.popup-title {
|
||||||
|
color: #FFF;
|
||||||
|
padding: 8px $-m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||||
|
height: 444px;
|
||||||
|
min-height: 444px;
|
||||||
|
}
|
||||||
|
#entity-selector-wrap .popup-body .form-group {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
//body.ie #entity-selector-wrap .popup-body .form-group {
|
||||||
|
// min-height: 60vh;
|
||||||
|
//}
|
||||||
|
|
||||||
|
.image-manager-body {
|
||||||
|
min-height: 70vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#image-manager .dropzone-container {
|
#image-manager .dropzone-container {
|
||||||
|
@ -37,12 +87,6 @@
|
||||||
border: 3px dashed #DDD;
|
border: 3px dashed #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-manager-bottom {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-manager-list .image {
|
.image-manager-list .image {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -103,18 +147,13 @@
|
||||||
|
|
||||||
.image-manager-sidebar {
|
.image-manager-sidebar {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 100%;
|
|
||||||
margin-left: 1px;
|
margin-left: 1px;
|
||||||
padding: 0 $-l;
|
padding: $-m $-l;
|
||||||
|
overflow-y: auto;
|
||||||
border-left: 1px solid #DDD;
|
border-left: 1px solid #DDD;
|
||||||
}
|
.dropzone-container {
|
||||||
|
margin-top: $-m;
|
||||||
.image-manager-close {
|
}
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-manager-list {
|
.image-manager-list {
|
||||||
|
@ -125,7 +164,6 @@
|
||||||
.image-manager-content {
|
.image-manager-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -141,12 +179,13 @@
|
||||||
* Copyright (c) 2012 Matias Meno <m@tias.me>
|
* Copyright (c) 2012 Matias Meno <m@tias.me>
|
||||||
*/
|
*/
|
||||||
.dz-message {
|
.dz-message {
|
||||||
font-size: 1.4em;
|
font-size: 1.2em;
|
||||||
|
line-height: 1.1;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: $-xl $-m;
|
padding: $-l $-m;
|
||||||
transition: all ease-in-out 120ms;
|
transition: all ease-in-out 120ms;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,14 @@ body.flexbox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-child > div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//body.ie .flex-child > div {
|
||||||
|
// flex: 1 0 0px;
|
||||||
|
//}
|
||||||
|
|
||||||
/** Rules for all columns */
|
/** Rules for all columns */
|
||||||
div[class^="col-"] img {
|
div[class^="col-"] img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -39,6 +47,9 @@ div[class^="col-"] img {
|
||||||
&.fluid {
|
&.fluid {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
&.medium {
|
||||||
|
max-width: 992px;
|
||||||
|
}
|
||||||
&.small {
|
&.small {
|
||||||
max-width: 840px;
|
max-width: 840px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,6 +155,7 @@ form.search-box {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.faded span.faded-text {
|
.faded span.faded-text {
|
||||||
|
|
|
@ -375,6 +375,9 @@ ul.pagination {
|
||||||
.text-muted {
|
.text-muted {
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
li.padded {
|
||||||
|
padding: $-xs $-m;
|
||||||
|
}
|
||||||
a {
|
a {
|
||||||
display: block;
|
display: block;
|
||||||
padding: $-xs $-m;
|
padding: $-xs $-m;
|
||||||
|
@ -384,10 +387,10 @@ ul.pagination {
|
||||||
background-color: #EEE;
|
background-color: #EEE;
|
||||||
}
|
}
|
||||||
i {
|
i {
|
||||||
margin-right: $-m;
|
margin-right: $-s;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
display: inline;
|
display: inline-block;
|
||||||
width: 22px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
li.border-bottom {
|
li.border-bottom {
|
||||||
|
|
|
@ -20,6 +20,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.draft-notification {
|
||||||
|
pointer-events: none;
|
||||||
|
transform: scale(0);
|
||||||
|
transition: transform ease-in-out 120ms;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
&.visible {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.page-style.editor {
|
.page-style.editor {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
@ -238,7 +248,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-display {
|
.tag-display {
|
||||||
margin: $-xl $-xs;
|
margin: $-xl $-m;
|
||||||
border: 1px solid #DDD;
|
border: 1px solid #DDD;
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
@import "animations";
|
@import "animations";
|
||||||
@import "tinymce";
|
@import "tinymce";
|
||||||
@import "highlightjs";
|
@import "highlightjs";
|
||||||
@import "image-manager";
|
@import "components";
|
||||||
@import "header";
|
@import "header";
|
||||||
@import "lists";
|
@import "lists";
|
||||||
@import "pages";
|
@import "pages";
|
||||||
|
@ -72,7 +72,7 @@ body.dragging, body.dragging * {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: $bs-med;
|
box-shadow: $bs-med;
|
||||||
z-index: 999999;
|
z-index: 999999;
|
||||||
display: table;
|
display: block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
i, span {
|
i, span {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}">
|
<div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}">
|
||||||
<h3 class="text-book"><a class="text-book" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></h3>
|
<h3 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i><span class="entity-list-item-name">{{$book->name}}</span></a></h3>
|
||||||
@if(isset($book->searchSnippet))
|
@if(isset($book->searchSnippet))
|
||||||
<p class="text-muted">{!! $book->searchSnippet !!}</p>
|
<p class="text-muted">{!! $book->searchSnippet !!}</p>
|
||||||
@else
|
@else
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
var sortableOptions = {
|
var sortableOptions = {
|
||||||
group: 'serialization',
|
group: 'serialization',
|
||||||
onDrop: function($item, container, _super) {
|
onDrop: function($item, container, _super) {
|
||||||
var pageMap = buildPageMap();
|
var pageMap = buildEntityMap();
|
||||||
$('#sort-tree-input').val(JSON.stringify(pageMap));
|
$('#sort-tree-input').val(JSON.stringify(pageMap));
|
||||||
_super($item, container);
|
_super($item, container);
|
||||||
},
|
},
|
||||||
|
@ -74,29 +74,42 @@
|
||||||
$link.remove();
|
$link.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildPageMap() {
|
/**
|
||||||
var pageMap = [];
|
* Build up a mapping of entities with their ordering and nesting.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
function buildEntityMap() {
|
||||||
|
var entityMap = [];
|
||||||
var $lists = $('.sort-list');
|
var $lists = $('.sort-list');
|
||||||
$lists.each(function(listIndex) {
|
$lists.each(function(listIndex) {
|
||||||
var list = $(this);
|
var list = $(this);
|
||||||
var bookId = list.closest('[data-type="book"]').attr('data-id');
|
var bookId = list.closest('[data-type="book"]').attr('data-id');
|
||||||
var $childElements = list.find('[data-type="page"], [data-type="chapter"]');
|
var $directChildren = list.find('> [data-type="page"], > [data-type="chapter"]');
|
||||||
$childElements.each(function(childIndex) {
|
$directChildren.each(function(directChildIndex) {
|
||||||
var $childElem = $(this);
|
var $childElem = $(this);
|
||||||
var type = $childElem.attr('data-type');
|
var type = $childElem.attr('data-type');
|
||||||
var parentChapter = false;
|
var parentChapter = false;
|
||||||
if(type === 'page' && $childElem.closest('[data-type="chapter"]').length === 1) {
|
var childId = $childElem.attr('data-id');
|
||||||
parentChapter = $childElem.closest('[data-type="chapter"]').attr('data-id');
|
entityMap.push({
|
||||||
}
|
id: childId,
|
||||||
pageMap.push({
|
sort: directChildIndex,
|
||||||
id: $childElem.attr('data-id'),
|
|
||||||
parentChapter: parentChapter,
|
parentChapter: parentChapter,
|
||||||
type: type,
|
type: type,
|
||||||
book: bookId
|
book: bookId
|
||||||
});
|
});
|
||||||
|
$chapterChildren = $childElem.find('[data-type="page"]').each(function(pageIndex) {
|
||||||
|
var $chapterChild = $(this);
|
||||||
|
entityMap.push({
|
||||||
|
id: $chapterChild.attr('data-id'),
|
||||||
|
sort: pageIndex,
|
||||||
|
parentChapter: childId,
|
||||||
|
type: 'page',
|
||||||
|
book: bookId
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return pageMap;
|
return entityMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
</a>
|
</a>
|
||||||
<span class="text-muted"> » </span>
|
<span class="text-muted"> » </span>
|
||||||
@endif
|
@endif
|
||||||
<a href="{{ $chapter->getUrl() }}" class="text-chapter">
|
<a href="{{ $chapter->getUrl() }}" class="text-chapter entity-list-item-link">
|
||||||
<i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->name }}
|
<i class="zmdi zmdi-collection-bookmark"></i><span class="entity-list-item-name">{{ $chapter->name }}</span>
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
@if(isset($chapter->searchSnippet))
|
@if(isset($chapter->searchSnippet))
|
||||||
|
|
|
@ -19,6 +19,14 @@
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
|
@include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
|
||||||
|
@include('partials/entity-selector-popup')
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
@stop
|
@stop
|
|
@ -13,8 +13,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4 faded text-center">
|
<div class="col-sm-4 faded text-center">
|
||||||
|
|
||||||
<div dropdown class="dropdown-container">
|
<div dropdown class="dropdown-container draft-display">
|
||||||
<a dropdown-toggle class="text-primary text-button"><span class="faded-text" ng-bind="draftText"></span> <i class="zmdi zmdi-more-vert"></i></a>
|
<a dropdown-toggle class="text-primary text-button"><span class="faded-text" ng-bind="draftText"></span> <i class="zmdi zmdi-more-vert"></i></a>
|
||||||
|
<i class="zmdi zmdi-check-circle text-pos draft-notification" ng-class="{visible: draftUpdated}"></i>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a ng-click="forceDraftSave()" class="text-pos"><i class="zmdi zmdi-save"></i>Save Draft</a>
|
<a ng-click="forceDraftSave()" class="text-pos"><i class="zmdi zmdi-save"></i>Save Draft</a>
|
||||||
|
@ -22,14 +23,25 @@
|
||||||
<li ng-if="isNewPageDraft">
|
<li ng-if="isNewPageDraft">
|
||||||
<a href="{{ $model->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete Draft</a>
|
<a href="{{ $model->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete Draft</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4 faded">
|
<div class="col-sm-4 faded">
|
||||||
<div class="action-buttons" ng-cloak>
|
<div class="action-buttons" ng-cloak>
|
||||||
|
<div dropdown class="dropdown-container">
|
||||||
|
<a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-edit"></i> @{{(changeSummary | limitTo:16) + (changeSummary.length>16?'...':'') || 'Set Changelog'}}</a>
|
||||||
|
<ul class="wide">
|
||||||
|
<li class="padded">
|
||||||
|
<p class="text-muted">Enter a brief description of the changes you've made</p>
|
||||||
|
<input name="summary" id="summary-input" type="text" placeholder="Enter Changelog" ng-model="changeSummary" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" ng-if="isUpdateDraft" ng-click="discardDraft()" class="text-button text-neg"><i class="zmdi zmdi-close-circle"></i>Discard Draft</button>
|
<button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
|
||||||
<button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,6 +74,8 @@
|
||||||
<span class="float left">Editor</span>
|
<span class="float left">Editor</span>
|
||||||
<div class="float right buttons">
|
<div class="float right buttons">
|
||||||
<button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button>
|
<button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button>
|
||||||
|
|
|
||||||
|
<button class="text-button" type="button" data-action="insertEntityLink"><i class="zmdi zmdi-link"></i>Insert Entity Link</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}">
|
<div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}">
|
||||||
<h3>
|
<h3>
|
||||||
<a href="{{ $page->getUrl() }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ $page->name }}</a>
|
<a href="{{ $page->getUrl() }}" class="text-page entity-list-item-link"><i class="zmdi zmdi-file-text"></i><span class="entity-list-item-name">{{ $page->name }}</span></a>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@if(isset($page->searchSnippet))
|
@if(isset($page->searchSnippet))
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
<span class="sep">»</span>
|
<span class="sep">»</span>
|
||||||
<a href="{{ $page->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
|
<a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,45 +5,59 @@
|
||||||
<div class="faded-small toolbar">
|
<div class="faded-small toolbar">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 faded">
|
<div class="col-sm-12 faded">
|
||||||
<div class="breadcrumbs">
|
<div class="breadcrumbs">
|
||||||
<a href="{{ $page->getUrl() }}" class="text-primary text-button"><i class="zmdi zmdi-arrow-left"></i>Back to page</a>
|
<a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
|
||||||
|
@if($page->hasChapter())
|
||||||
|
<span class="sep">»</span>
|
||||||
|
<a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
|
||||||
|
<i class="zmdi zmdi-collection-bookmark"></i>
|
||||||
|
{{ $page->chapter->getShortName() }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
<span class="sep">»</span>
|
||||||
|
<a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 faded">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="container small" ng-non-bindable>
|
|
||||||
|
<div class="container" ng-non-bindable>
|
||||||
<h1>Page Revisions <span class="subheader">For "{{ $page->name }}"</span></h1>
|
<h1>Page Revisions <span class="subheader">For "{{ $page->name }}"</span></h1>
|
||||||
|
|
||||||
@if(count($page->revisions) > 0)
|
@if(count($page->revisions) > 0)
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<th width="40%">Name</th>
|
<th width="25%">Name</th>
|
||||||
<th colspan="2" width="20%">Created By</th>
|
<th colspan="2" width="10%">Created By</th>
|
||||||
<th width="20%">Revision Date</th>
|
<th width="15%">Revision Date</th>
|
||||||
<th width="20%">Actions</th>
|
<th width="25%">Changelog</th>
|
||||||
|
<th width="15%">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@foreach($page->revisions as $revision)
|
@foreach($page->revisions as $index => $revision)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{$revision->name}}</td>
|
<td>{{ $revision->name }}</td>
|
||||||
<td style="line-height: 0;">
|
<td style="line-height: 0;">
|
||||||
@if($revision->createdBy)
|
@if($revision->createdBy)
|
||||||
<img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{$revision->createdBy->name}}">
|
<img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td> @if($revision->createdBy) {{$revision->createdBy->name}} @else Deleted User @endif</td>
|
<td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif</td>
|
||||||
<td><small>{{$revision->created_at->format('jS F, Y H:i:s')}} <br> ({{$revision->created_at->diffForHumans()}})</small></td>
|
<td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
|
||||||
<td>
|
<td>{{ $revision->summary }}</td>
|
||||||
<a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
|
@if ($index !== 0)
|
||||||
<span class="text-muted"> | </span>
|
<td>
|
||||||
<a href="{{ $revision->getUrl('/restore') }}">Restore</a>
|
<a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
|
||||||
</td>
|
<span class="text-muted"> | </span>
|
||||||
|
<a href="{{ $revision->getUrl() }}/restore">Restore</a>
|
||||||
|
</td>
|
||||||
|
@else
|
||||||
|
<td><a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a></td>
|
||||||
|
@endif
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
<div class="container" id="page-show" ng-non-bindable>
|
<div class="container" id="page-show" ng-non-bindable>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-9 print-full-width">
|
<div class="col-md-9 print-full-width">
|
||||||
<div class="page-content anim fadeIn">
|
<div class="page-content">
|
||||||
|
|
||||||
<div class="pointer-container" id="pointer">
|
<div class="pointer-container" id="pointer">
|
||||||
<div class="pointer anim">
|
<div class="pointer anim">
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<div id="entity-selector-wrap">
|
||||||
|
<div class="overlay" entity-link-selector>
|
||||||
|
<div class="popup-body small flex-child">
|
||||||
|
<div class="popup-header primary-background">
|
||||||
|
<div class="popup-title">Entity Select</div>
|
||||||
|
<button type="button" class="corner-button neg button popup-close">x</button>
|
||||||
|
</div>
|
||||||
|
@include('partials/entity-selector', ['name' => 'entity-selector'])
|
||||||
|
<div class="popup-footer">
|
||||||
|
<button type="button" disabled="true" class="button entity-link-selector-confirm pos corner-button">Select</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,84 +1,94 @@
|
||||||
<div id="image-manager" image-type="{{ $imageType }}" ng-controller="ImageManagerController" uploaded-to="{{ $uploaded_to or 0 }}">
|
<div id="image-manager" image-type="{{ $imageType }}" ng-controller="ImageManagerController" uploaded-to="{{ $uploaded_to or 0 }}">
|
||||||
<div class="overlay anim-slide" ng-show="showing" ng-cloak ng-click="hide()">
|
<div class="overlay" ng-cloak ng-click="hide()">
|
||||||
<div class="image-manager-body" ng-click="$event.stopPropagation()">
|
<div class="popup-body" ng-click="$event.stopPropagation()">
|
||||||
|
|
||||||
<div class="image-manager-content">
|
<div class="popup-header primary-background">
|
||||||
<div ng-if="imageType === 'gallery'" class="container">
|
<div class="popup-title">Image Select</div>
|
||||||
<div class="image-manager-header row faded-small nav-tabs">
|
<button class="popup-close neg corner-button button">x</button>
|
||||||
<div class="col-xs-4 tab-item" title="View all images" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> All</div>
|
</div>
|
||||||
<div class="col-xs-4 tab-item" title="View images uploaded to this book" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> Book</div>
|
|
||||||
<div class="col-xs-4 tab-item" title="View images uploaded to this page" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> Page</div>
|
<div class="flex-fill image-manager-body">
|
||||||
|
|
||||||
|
<div class="image-manager-content">
|
||||||
|
<div ng-if="imageType === 'gallery'" class="container">
|
||||||
|
<div class="image-manager-header row faded-small nav-tabs">
|
||||||
|
<div class="col-xs-4 tab-item" title="View all images" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> All</div>
|
||||||
|
<div class="col-xs-4 tab-item" title="View images uploaded to this book" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> Book</div>
|
||||||
|
<div class="col-xs-4 tab-item" title="View images uploaded to this page" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> Page</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div ng-show="view === 'all'" >
|
||||||
<div ng-show="view === 'all'" >
|
<form ng-submit="searchImages()" class="contained-search-box">
|
||||||
<form ng-submit="searchImages()" class="contained-search-box">
|
<input type="text" placeholder="Search by image name" ng-model="searchTerm">
|
||||||
<input type="text" placeholder="Search by image name" ng-model="searchTerm">
|
<button ng-class="{active: searching}" title="Clear Search" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button>
|
||||||
<button ng-class="{active: searching}" title="Clear Search" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button>
|
<button title="Search" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
|
||||||
<button title="Search" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
|
</form>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
<div class="image-manager-list">
|
||||||
<div class="image-manager-list">
|
<div ng-repeat="image in images">
|
||||||
<div ng-repeat="image in images">
|
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
|
||||||
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
|
ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)">
|
||||||
ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)">
|
<img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
|
||||||
<img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
|
<div class="image-meta">
|
||||||
<div class="image-meta">
|
<span class="name" ng-bind="image.name"></span>
|
||||||
<span class="name" ng-bind="image.name"></span>
|
<span class="date">Uploaded @{{ getDate(image.created_at) }}</span>
|
||||||
<span class="date">Uploaded @{{ getDate(image.created_at) | date:'mediumDate' }}</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="image-manager-sidebar">
|
||||||
|
<div class="inner">
|
||||||
|
|
||||||
|
<div class="image-manager-details anim fadeIn" ng-show="selectedImage">
|
||||||
|
|
||||||
|
<form ng-submit="saveImageDetails($event)">
|
||||||
|
<div>
|
||||||
|
<a ng-href="@{{selectedImage.url}}" target="_blank" style="display: block;">
|
||||||
|
<img ng-src="@{{selectedImage.thumbs.gallery}}" ng-attr-alt="@{{selectedImage.title}}" ng-attr-title="@{{selectedImage.name}}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Image Name</label>
|
||||||
|
<input type="text" id="name" name="name" ng-model="selectedImage.name">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div ng-show="dependantPages">
|
||||||
|
<p class="text-neg text-small">
|
||||||
|
This image is used in the pages below, Click delete again to confirm you want to delete
|
||||||
|
this image.
|
||||||
|
</p>
|
||||||
|
<ul class="text-neg">
|
||||||
|
<li ng-repeat="page in dependantPages">
|
||||||
|
<a ng-href="@{{ page.url }}" target="_blank" class="text-neg" ng-bind="page.name"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clearfix">
|
||||||
|
<form class="float left" ng-submit="deleteImage($event)">
|
||||||
|
<button class="button icon neg"><i class="zmdi zmdi-delete"></i></button>
|
||||||
|
</form>
|
||||||
|
<button class="button pos anim fadeIn float right" ng-show="selectedImage" ng-click="selectButtonClick()">
|
||||||
|
<i class="zmdi zmdi-square-right"></i>Select Image
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="neg button image-manager-close" ng-click="hide()">x</button>
|
|
||||||
|
|
||||||
<div class="image-manager-sidebar">
|
|
||||||
<h2>Images</h2>
|
|
||||||
<drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
|
|
||||||
<div class="image-manager-details anim fadeIn" ng-show="selectedImage">
|
|
||||||
|
|
||||||
<hr class="even">
|
|
||||||
|
|
||||||
<form ng-submit="saveImageDetails($event)">
|
|
||||||
<div>
|
|
||||||
<a ng-href="@{{selectedImage.url}}" target="_blank" style="display: block;">
|
|
||||||
<img ng-src="@{{selectedImage.thumbs.gallery}}" ng-attr-alt="@{{selectedImage.title}}" ng-attr-title="@{{selectedImage.name}}">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="name">Image Name</label>
|
|
||||||
<input type="text" id="name" name="name" ng-model="selectedImage.name">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<hr class="even">
|
|
||||||
|
|
||||||
<div ng-show="dependantPages">
|
|
||||||
<p class="text-neg text-small">
|
|
||||||
This image is used in the pages below, Click delete again to confirm you want to delete
|
|
||||||
this image.
|
|
||||||
</p>
|
|
||||||
<ul class="text-neg">
|
|
||||||
<li ng-repeat="page in dependantPages">
|
|
||||||
<a ng-href="@{{ page.url }}" target="_blank" class="text-neg" ng-bind="page.name"></a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form ng-submit="deleteImage($event)">
|
|
||||||
<button class="button neg"><i class="zmdi zmdi-delete"></i>Delete Image</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="image-manager-bottom">
|
|
||||||
<button class="button pos anim fadeIn" ng-show="selectedImage" ng-click="selectButtonClick()">
|
|
||||||
<i class="zmdi zmdi-square-right"></i>Select Image
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -76,6 +76,14 @@ class EntitySearchTest extends TestCase
|
||||||
->see('Chapter Search Results')->seeInElement('.entity-list', $chapter->name);
|
->see('Chapter Search Results')->seeInElement('.entity-list', $chapter->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_search_quote_term_preparation()
|
||||||
|
{
|
||||||
|
$termString = '"192" cat "dog hat"';
|
||||||
|
$repo = $this->app[\BookStack\Repos\EntityRepo::class];
|
||||||
|
$preparedTerms = $repo->prepareSearchTerms($termString);
|
||||||
|
$this->assertTrue($preparedTerms === ['"192"','"dog hat"', 'cat']);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_books_search_listing()
|
public function test_books_search_listing()
|
||||||
{
|
{
|
||||||
$book = \BookStack\Book::all()->last();
|
$book = \BookStack\Book::all()->last();
|
||||||
|
|
|
@ -218,13 +218,24 @@ class EntityTest extends TestCase
|
||||||
|
|
||||||
public function test_old_page_slugs_redirect_to_new_pages()
|
public function test_old_page_slugs_redirect_to_new_pages()
|
||||||
{
|
{
|
||||||
$page = \BookStack\Page::all()->first();
|
$page = \BookStack\Page::first();
|
||||||
$pageUrl = $page->getUrl();
|
$pageUrl = $page->getUrl();
|
||||||
$newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
|
$newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
|
||||||
|
// Need to save twice since revisions are not generated in seeder.
|
||||||
$this->asAdmin()->visit($pageUrl)
|
$this->asAdmin()->visit($pageUrl)
|
||||||
|
->clickInElement('#content', 'Edit')
|
||||||
|
->type('super test', '#name')
|
||||||
|
->press('Save Page');
|
||||||
|
|
||||||
|
$page = \BookStack\Page::first();
|
||||||
|
$pageUrl = $page->getUrl();
|
||||||
|
|
||||||
|
// Second Save
|
||||||
|
$this->visit($pageUrl)
|
||||||
->clickInElement('#content', 'Edit')
|
->clickInElement('#content', 'Edit')
|
||||||
->type('super test page', '#name')
|
->type('super test page', '#name')
|
||||||
->press('Save Page')
|
->press('Save Page')
|
||||||
|
// Check redirect
|
||||||
->seePageIs($newPageUrl)
|
->seePageIs($newPageUrl)
|
||||||
->visit($pageUrl)
|
->visit($pageUrl)
|
||||||
->seePageIs($newPageUrl);
|
->seePageIs($newPageUrl);
|
||||||
|
|
Loading…
Reference in New Issue