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:
 | 
			
		||||
  directories:
 | 
			
		||||
    - vendor
 | 
			
		||||
    - node_modules
 | 
			
		||||
    - $HOME/.composer/cache
 | 
			
		||||
 | 
			
		||||
addons:
 | 
			
		||||
| 
						 | 
				
			
			@ -17,19 +15,17 @@ addons:
 | 
			
		|||
    - mysql-client-core-5.6
 | 
			
		||||
    - mysql-client-5.6
 | 
			
		||||
 | 
			
		||||
before_install:
 | 
			
		||||
  - npm install -g npm@latest
 | 
			
		||||
 | 
			
		||||
before_script:
 | 
			
		||||
  - mysql -u root -e 'create database `bookstack-test`;'
 | 
			
		||||
  - composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
 | 
			
		||||
  - phpenv config-rm xdebug.ini
 | 
			
		||||
  - composer self-update
 | 
			
		||||
  - composer dump-autoload --no-interaction
 | 
			
		||||
  - composer install --prefer-dist --no-interaction
 | 
			
		||||
  - npm install
 | 
			
		||||
  - ./node_modules/.bin/gulp
 | 
			
		||||
  - php artisan clear-compiled -n
 | 
			
		||||
  - php artisan optimize -n
 | 
			
		||||
  - php artisan migrate --force -n --database=mysql_testing
 | 
			
		||||
  - php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
 | 
			
		||||
 | 
			
		||||
script:
 | 
			
		||||
  - vendor/bin/phpunit
 | 
			
		||||
  - phpunit
 | 
			
		||||
| 
						 | 
				
			
			@ -167,7 +167,8 @@ class Entity extends Ownable
 | 
			
		|||
            foreach ($terms as $key => $term) {
 | 
			
		||||
                $term = htmlentities($term, ENT_QUOTES);
 | 
			
		||||
                $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
 | 
			
		||||
                if (preg_match('/\s/', $term)) {
 | 
			
		||||
                if (preg_match('/".*?"/', $term)) {
 | 
			
		||||
                    $term = str_replace('"', '', $term);
 | 
			
		||||
                    $exactTerms[] = '%' . $term . '%';
 | 
			
		||||
                    $term = '"' . $term . '"';
 | 
			
		||||
                } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -206,5 +207,5 @@ class Entity extends Ownable
 | 
			
		|||
 | 
			
		||||
        return $search->orderBy($orderBy, 'desc');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,19 +47,44 @@ class Handler extends ExceptionHandler
 | 
			
		|||
    {
 | 
			
		||||
        // Handle notify exceptions which will redirect to the
 | 
			
		||||
        // specified location then show a notification message.
 | 
			
		||||
        if ($e instanceof NotifyException) {
 | 
			
		||||
            session()->flash('error', $e->message);
 | 
			
		||||
        if ($this->isExceptionType($e, NotifyException::class)) {
 | 
			
		||||
            session()->flash('error', $this->getOriginalMessage($e));
 | 
			
		||||
            return redirect($e->redirectLocation);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 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.
 | 
			
		||||
        if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException)  && !config('app.debug')) {
 | 
			
		||||
            $message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
 | 
			
		||||
        if ($this->isExceptionType($e, PrettyException::class)  && !config('app.debug')) {
 | 
			
		||||
            $message = $this->getOriginalMessage($e);
 | 
			
		||||
            $code = ($e->getCode() === 0) ? 500 : $e->getCode();
 | 
			
		||||
            return response()->view('errors/' . $code, ['message' => $message], $code);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
 | 
			
		||||
use Exception;
 | 
			
		||||
 | 
			
		||||
class PrettyException extends Exception {}
 | 
			
		||||
class PrettyException extends \Exception {}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,7 +3,6 @@
 | 
			
		|||
use Activity;
 | 
			
		||||
use BookStack\Repos\UserRepo;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Facades\Auth;
 | 
			
		||||
use BookStack\Http\Requests;
 | 
			
		||||
use BookStack\Repos\BookRepo;
 | 
			
		||||
use BookStack\Repos\ChapterRepo;
 | 
			
		||||
| 
						 | 
				
			
			@ -180,21 +179,31 @@ class BookController extends Controller
 | 
			
		|||
            return redirect($book->getUrl());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $sortedBooks = [];
 | 
			
		||||
        // Sort pages and chapters
 | 
			
		||||
        $sortedBooks = [];
 | 
			
		||||
        $updatedModels = collect();
 | 
			
		||||
        $sortMap = json_decode($request->get('sort-tree'));
 | 
			
		||||
        $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';
 | 
			
		||||
            $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);
 | 
			
		||||
            $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
 | 
			
		||||
            $model->priority = $index;
 | 
			
		||||
            if ($isPage) {
 | 
			
		||||
                $model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter;
 | 
			
		||||
 | 
			
		||||
            // Update models only if there's a change in parent chain or ordering.
 | 
			
		||||
            if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
 | 
			
		||||
                $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)) {
 | 
			
		||||
                $sortedBooks[] = $bookId;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -203,10 +212,12 @@ class BookController extends Controller
 | 
			
		|||
        // Add activity for books
 | 
			
		||||
        foreach ($sortedBooks as $bookId) {
 | 
			
		||||
            $updatedBook = $this->bookRepo->getById($bookId);
 | 
			
		||||
            $this->bookRepo->updateBookPermissions($updatedBook);
 | 
			
		||||
            Activity::add($updatedBook, 'book_sort', $updatedBook->id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Update permissions on changed models
 | 
			
		||||
        $this->bookRepo->buildJointPermissions($updatedModels);
 | 
			
		||||
 | 
			
		||||
        return redirect($book->getUrl());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -204,7 +204,7 @@ class ChapterController extends Controller
 | 
			
		|||
            return redirect()->back();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->chapterRepo->changeBook($parent->id, $chapter);
 | 
			
		||||
        $this->chapterRepo->changeBook($parent->id, $chapter, true);
 | 
			
		||||
        Activity::add($chapter, 'chapter_move', $chapter->book->id);
 | 
			
		||||
        session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,6 @@
 | 
			
		|||
<?php
 | 
			
		||||
<?php namespace BookStack\Providers;
 | 
			
		||||
 | 
			
		||||
namespace BookStack\Providers;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Support\Facades\Auth;
 | 
			
		||||
use Illuminate\Support\ServiceProvider;
 | 
			
		||||
use BookStack\User;
 | 
			
		||||
 | 
			
		||||
class AppServiceProvider extends ServiceProvider
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
 | 
			
		||||
use Alpha\B;
 | 
			
		||||
use BookStack\Exceptions\NotFoundException;
 | 
			
		||||
use Illuminate\Database\Eloquent\Collection;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
use BookStack\Book;
 | 
			
		||||
use Views;
 | 
			
		||||
| 
						 | 
				
			
			@ -173,15 +174,6 @@ class BookRepo extends EntityRepo
 | 
			
		|||
        $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.
 | 
			
		||||
     * @param Book $book
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -195,11 +195,12 @@ class ChapterRepo extends EntityRepo
 | 
			
		|||
 | 
			
		||||
    /**
 | 
			
		||||
     * Changes the book relation of this chapter.
 | 
			
		||||
     * @param         $bookId
 | 
			
		||||
     * @param $bookId
 | 
			
		||||
     * @param Chapter $chapter
 | 
			
		||||
     * @param bool $rebuildPermissions
 | 
			
		||||
     * @return Chapter
 | 
			
		||||
     */
 | 
			
		||||
    public function changeBook($bookId, Chapter $chapter)
 | 
			
		||||
    public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
 | 
			
		||||
    {
 | 
			
		||||
        $chapter->book_id = $bookId;
 | 
			
		||||
        // Update related activity
 | 
			
		||||
| 
						 | 
				
			
			@ -213,9 +214,12 @@ class ChapterRepo extends EntityRepo
 | 
			
		|||
        foreach ($chapter->pages as $page) {
 | 
			
		||||
            $this->pageRepo->changeBook($bookId, $page);
 | 
			
		||||
        }
 | 
			
		||||
        // Update permissions
 | 
			
		||||
        $chapter->load('book');
 | 
			
		||||
        $this->permissionService->buildJointPermissionsForEntity($chapter->book);
 | 
			
		||||
 | 
			
		||||
        // Update permissions if applicable
 | 
			
		||||
        if ($rebuildPermissions) {
 | 
			
		||||
            $chapter->load('book');
 | 
			
		||||
            $this->permissionService->buildJointPermissionsForEntity($chapter->book);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $chapter;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ use BookStack\Entity;
 | 
			
		|||
use BookStack\Page;
 | 
			
		||||
use BookStack\Services\PermissionService;
 | 
			
		||||
use BookStack\User;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
use Illuminate\Support\Facades\Log;
 | 
			
		||||
 | 
			
		||||
class EntityRepo
 | 
			
		||||
| 
						 | 
				
			
			@ -168,15 +169,16 @@ class EntityRepo
 | 
			
		|||
     * @param $termString
 | 
			
		||||
     * @return array
 | 
			
		||||
     */
 | 
			
		||||
    protected function prepareSearchTerms($termString)
 | 
			
		||||
    public function prepareSearchTerms($termString)
 | 
			
		||||
    {
 | 
			
		||||
        $termString = $this->cleanSearchTermString($termString);
 | 
			
		||||
        preg_match_all('/"(.*?)"/', $termString, $matches);
 | 
			
		||||
        preg_match_all('/(".*?")/', $termString, $matches);
 | 
			
		||||
        $terms = [];
 | 
			
		||||
        if (count($matches[1]) > 0) {
 | 
			
		||||
            $terms = $matches[1];
 | 
			
		||||
            foreach ($matches[1] as $match) {
 | 
			
		||||
                $terms[] = $match;
 | 
			
		||||
            }
 | 
			
		||||
            $termString = trim(preg_replace('/"(.*?)"/', '', $termString));
 | 
			
		||||
        } else {
 | 
			
		||||
            $terms = [];
 | 
			
		||||
        }
 | 
			
		||||
        if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
 | 
			
		||||
        return $terms;
 | 
			
		||||
| 
						 | 
				
			
			@ -259,6 +261,15 @@ class EntityRepo
 | 
			
		|||
        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->save();
 | 
			
		||||
        $this->saveRevision($draftPage, 'Initial Publish');
 | 
			
		||||
        
 | 
			
		||||
        return $draftPage;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -308,10 +310,9 @@ class PageRepo extends EntityRepo
 | 
			
		|||
     */
 | 
			
		||||
    public function updatePage(Page $page, $book_id, $input)
 | 
			
		||||
    {
 | 
			
		||||
        // Save a revision before updating
 | 
			
		||||
        if ($page->html !== $input['html'] || $page->name !== $input['name']) {
 | 
			
		||||
            $this->saveRevision($page);
 | 
			
		||||
        }
 | 
			
		||||
        // Hold the old details to compare later
 | 
			
		||||
        $oldHtml = $page->html;
 | 
			
		||||
        $oldName = $page->name;
 | 
			
		||||
 | 
			
		||||
        // Prevent slug being updated if no name change
 | 
			
		||||
        if ($page->name !== $input['name']) {
 | 
			
		||||
| 
						 | 
				
			
			@ -335,6 +336,11 @@ class PageRepo extends EntityRepo
 | 
			
		|||
        // Remove all update drafts for this user & page.
 | 
			
		||||
        $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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -360,9 +366,10 @@ class PageRepo extends EntityRepo
 | 
			
		|||
    /**
 | 
			
		||||
     * Saves a page revision into the system.
 | 
			
		||||
     * @param Page $page
 | 
			
		||||
     * @param null|string $summary
 | 
			
		||||
     * @return $this
 | 
			
		||||
     */
 | 
			
		||||
    public function saveRevision(Page $page)
 | 
			
		||||
    public function saveRevision(Page $page, $summary = null)
 | 
			
		||||
    {
 | 
			
		||||
        $revision = $this->pageRevision->fill($page->toArray());
 | 
			
		||||
        if (setting('app-editor') !== 'markdown') $revision->markdown = '';
 | 
			
		||||
| 
						 | 
				
			
			@ -372,6 +379,7 @@ class PageRepo extends EntityRepo
 | 
			
		|||
        $revision->created_by = auth()->user()->id;
 | 
			
		||||
        $revision->created_at = $page->updated_at;
 | 
			
		||||
        $revision->type = 'version';
 | 
			
		||||
        $revision->summary = $summary;
 | 
			
		||||
        $revision->save();
 | 
			
		||||
        // Clear old revisions
 | 
			
		||||
        if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,11 +48,13 @@ class ExportService
 | 
			
		|||
            foreach ($imageTagsOutput[0] as $index => $imgMatch) {
 | 
			
		||||
                $oldImgString = $imgMatch;
 | 
			
		||||
                $srcString = $imageTagsOutput[2][$index];
 | 
			
		||||
                if (strpos(trim($srcString), 'http') !== 0) {
 | 
			
		||||
                    $pathString = public_path($srcString);
 | 
			
		||||
                $isLocal = strpos(trim($srcString), 'http') !== 0;
 | 
			
		||||
                if ($isLocal) {
 | 
			
		||||
                    $pathString = public_path(trim($srcString, '/'));
 | 
			
		||||
                } else {
 | 
			
		||||
                    $pathString = $srcString;
 | 
			
		||||
                }
 | 
			
		||||
                if ($isLocal && !file_exists($pathString)) continue;
 | 
			
		||||
                $imageContent = file_get_contents($pathString);
 | 
			
		||||
                $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
 | 
			
		||||
                $newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,6 +95,7 @@ class ImageService
 | 
			
		|||
 | 
			
		||||
        try {
 | 
			
		||||
            $storage->put($fullPath, $imageData);
 | 
			
		||||
            $storage->setVisibility($fullPath, 'public');
 | 
			
		||||
        } catch (Exception $e) {
 | 
			
		||||
            throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -167,6 +168,7 @@ class ImageService
 | 
			
		|||
 | 
			
		||||
        $thumbData = (string)$thumb->encode();
 | 
			
		||||
        $storage->put($thumbFilePath, $thumbData);
 | 
			
		||||
        $storage->setVisibility($thumbFilePath, 'public');
 | 
			
		||||
        $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
 | 
			
		||||
 | 
			
		||||
        return $this->getPublicUrl($thumbFilePath);
 | 
			
		||||
| 
						 | 
				
			
			@ -257,9 +259,15 @@ class ImageService
 | 
			
		|||
            $storageUrl = config('filesystems.url');
 | 
			
		||||
 | 
			
		||||
            // 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') {
 | 
			
		||||
                $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;
 | 
			
		||||
| 
						 | 
				
			
			@ -269,4 +277,4 @@ class ImageService
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ use BookStack\Ownable;
 | 
			
		|||
use BookStack\Page;
 | 
			
		||||
use BookStack\Role;
 | 
			
		||||
use BookStack\User;
 | 
			
		||||
use Illuminate\Database\Eloquent\Collection;
 | 
			
		||||
use Illuminate\Support\Collection;
 | 
			
		||||
 | 
			
		||||
class PermissionService
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +25,8 @@ class PermissionService
 | 
			
		|||
    protected $jointPermission;
 | 
			
		||||
    protected $role;
 | 
			
		||||
 | 
			
		||||
    protected $entityCache;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * PermissionService constructor.
 | 
			
		||||
     * @param JointPermission $jointPermission
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +50,57 @@ class PermissionService
 | 
			
		|||
        $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;
 | 
			
		||||
     * @return array|bool
 | 
			
		||||
| 
						 | 
				
			
			@ -76,6 +129,7 @@ class PermissionService
 | 
			
		|||
    public function buildJointPermissions()
 | 
			
		||||
    {
 | 
			
		||||
        $this->jointPermission->truncate();
 | 
			
		||||
        $this->readyEntityCache();
 | 
			
		||||
 | 
			
		||||
        // Get all roles (Should be the most limited dimension)
 | 
			
		||||
        $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
 | 
			
		||||
     */
 | 
			
		||||
    public function buildJointPermissionsForEntity(Entity $entity)
 | 
			
		||||
| 
						 | 
				
			
			@ -116,6 +170,17 @@ class PermissionService
 | 
			
		|||
        $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.
 | 
			
		||||
     * @param Role $role
 | 
			
		||||
| 
						 | 
				
			
			@ -177,9 +242,14 @@ class PermissionService
 | 
			
		|||
     */
 | 
			
		||||
    protected function deleteManyJointPermissionsForEntities($entities)
 | 
			
		||||
    {
 | 
			
		||||
        $query = $this->jointPermission->newQuery();
 | 
			
		||||
        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)
 | 
			
		||||
    {
 | 
			
		||||
        $this->readyEntityCache();
 | 
			
		||||
        $jointPermissions = [];
 | 
			
		||||
        foreach ($entities as $entity) {
 | 
			
		||||
            foreach ($roles as $role) {
 | 
			
		||||
| 
						 | 
				
			
			@ -248,8 +319,9 @@ class PermissionService
 | 
			
		|||
        } elseif ($entity->isA('chapter')) {
 | 
			
		||||
 | 
			
		||||
            if (!$entity->restricted) {
 | 
			
		||||
                $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
 | 
			
		||||
                $hasPermissiveAccessToBook = !$entity->book->restricted;
 | 
			
		||||
                $book = $this->getBook($entity->book_id);
 | 
			
		||||
                $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
 | 
			
		||||
                $hasPermissiveAccessToBook = !$book->restricted;
 | 
			
		||||
                return $this->createJointPermissionDataArray($entity, $role, $action,
 | 
			
		||||
                    ($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
 | 
			
		||||
                    ($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
 | 
			
		||||
| 
						 | 
				
			
			@ -261,11 +333,14 @@ class PermissionService
 | 
			
		|||
        } elseif ($entity->isA('page')) {
 | 
			
		||||
 | 
			
		||||
            if (!$entity->restricted) {
 | 
			
		||||
                $hasExplicitAccessToBook = $entity->book->hasActiveRestriction($role->id, $restrictionAction);
 | 
			
		||||
                $hasPermissiveAccessToBook = !$entity->book->restricted;
 | 
			
		||||
                $hasExplicitAccessToChapter = $entity->chapter && $entity->chapter->hasActiveRestriction($role->id, $restrictionAction);
 | 
			
		||||
                $hasPermissiveAccessToChapter = $entity->chapter && !$entity->chapter->restricted;
 | 
			
		||||
                $acknowledgeChapter = ($entity->chapter && $entity->chapter->restricted);
 | 
			
		||||
                $book = $this->getBook($entity->book_id);
 | 
			
		||||
                $hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
 | 
			
		||||
                $hasPermissiveAccessToBook = !$book->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;
 | 
			
		||||
                $hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -158,7 +158,7 @@ class SocialAuthService
 | 
			
		|||
        $driver = trim(strtolower($socialDriver));
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,33 +2,38 @@
 | 
			
		|||
 | 
			
		||||
use BookStack\Ownable;
 | 
			
		||||
 | 
			
		||||
if (!function_exists('versioned_asset')) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the path to a versioned file.
 | 
			
		||||
     *
 | 
			
		||||
     * @param  string $file
 | 
			
		||||
     * @return string
 | 
			
		||||
     *
 | 
			
		||||
     * @throws \InvalidArgumentException
 | 
			
		||||
     */
 | 
			
		||||
    function versioned_asset($file)
 | 
			
		||||
    {
 | 
			
		||||
        static $manifest = null;
 | 
			
		||||
/**
 | 
			
		||||
 * Get the path to a versioned file.
 | 
			
		||||
 *
 | 
			
		||||
 * @param  string $file
 | 
			
		||||
 * @return string
 | 
			
		||||
 * @throws Exception
 | 
			
		||||
 */
 | 
			
		||||
function versioned_asset($file = '')
 | 
			
		||||
{
 | 
			
		||||
    // Don't require css and JS assets for testing
 | 
			
		||||
    if (config('app.env') === 'testing') return '';
 | 
			
		||||
 | 
			
		||||
        if (is_null($manifest)) {
 | 
			
		||||
            $manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);
 | 
			
		||||
    static $manifest = null;
 | 
			
		||||
    $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";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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.");
 | 
			
		||||
        throw new \Exception($error);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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="MAIL_DRIVER" value="log"/>
 | 
			
		||||
        <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="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
 | 
			
		||||
        <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,7 +69,7 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
             */
 | 
			
		||||
            function callbackAndHide(returnData) {
 | 
			
		||||
                if (callback) callback(returnData);
 | 
			
		||||
                $scope.showing = false;
 | 
			
		||||
                $scope.hide();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            /**
 | 
			
		||||
| 
						 | 
				
			
			@ -109,6 +109,7 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
            function show(doneCallback) {
 | 
			
		||||
                callback = doneCallback;
 | 
			
		||||
                $scope.showing = true;
 | 
			
		||||
                $('#image-manager').find('.overlay').css('display', 'flex').hide().fadeIn(240);
 | 
			
		||||
                // Get initial images if they have not yet been loaded in.
 | 
			
		||||
                if (!dataLoaded) {
 | 
			
		||||
                    fetchData();
 | 
			
		||||
| 
						 | 
				
			
			@ -131,6 +132,7 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
             */
 | 
			
		||||
            $scope.hide = function () {
 | 
			
		||||
                $scope.showing = false;
 | 
			
		||||
                $('#image-manager').find('.overlay').fadeOut(240);
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            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.
 | 
			
		||||
         * @param title
 | 
			
		||||
         * @param html
 | 
			
		||||
         */
 | 
			
		||||
        function saveDraft() {
 | 
			
		||||
            var data = {
 | 
			
		||||
| 
						 | 
				
			
			@ -373,9 +373,17 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
 | 
			
		||||
                $scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
 | 
			
		||||
                if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
 | 
			
		||||
                showDraftSaveNotification();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function showDraftSaveNotification() {
 | 
			
		||||
            $scope.draftUpdated = true;
 | 
			
		||||
            $timeout(() => {
 | 
			
		||||
                $scope.draftUpdated = false;
 | 
			
		||||
            }, 2000)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $scope.forceDraftSave = function() {
 | 
			
		||||
            saveDraft();
 | 
			
		||||
        };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -158,9 +158,22 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
        return {
 | 
			
		||||
            restrict: 'A',
 | 
			
		||||
            link: function (scope, element, attrs) {
 | 
			
		||||
                var menu = element.find('ul');
 | 
			
		||||
                const menu = element.find('ul');
 | 
			
		||||
                element.find('[dropdown-toggle]').on('click', function () {
 | 
			
		||||
                    menu.show().addClass('anim menuIn');
 | 
			
		||||
                    let inputs = menu.find('input');
 | 
			
		||||
                    let hasInput = inputs.length > 0;
 | 
			
		||||
                    if (hasInput) {
 | 
			
		||||
                        inputs.first().focus();
 | 
			
		||||
                        element.on('keypress', 'input', event => {
 | 
			
		||||
                            if (event.keyCode === 13) {
 | 
			
		||||
                                event.preventDefault();
 | 
			
		||||
                                menu.hide();
 | 
			
		||||
                                menu.removeClass('anim menuIn');
 | 
			
		||||
                                return false;
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                    element.mouseleave(function () {
 | 
			
		||||
                        menu.hide();
 | 
			
		||||
                        menu.removeClass('anim menuIn');
 | 
			
		||||
| 
						 | 
				
			
			@ -258,8 +271,6 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                scope.mdModel = content;
 | 
			
		||||
                scope.mdChange(markdown(content));
 | 
			
		||||
 | 
			
		||||
                console.log('test');
 | 
			
		||||
 | 
			
		||||
                element.on('change input', (event) => {
 | 
			
		||||
                    content = element.val();
 | 
			
		||||
                    $timeout(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -291,6 +302,7 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                const input = element.find('[markdown-input] textarea').first();
 | 
			
		||||
                const display = element.find('.markdown-display').first();
 | 
			
		||||
                const insertImage = element.find('button[data-action="insertImage"]');
 | 
			
		||||
                const insertEntityLink = element.find('button[data-action="insertEntityLink"]')
 | 
			
		||||
 | 
			
		||||
                let currentCaretPos = 0;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -342,6 +354,13 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                        input[0].selectionEnd = caretPos + (';
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Insert entity link shortcut
 | 
			
		||||
                    if (event.which === 75 && event.ctrlKey && event.shiftKey) {
 | 
			
		||||
                        showLinkSelector();
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Pass key presses to controller via event
 | 
			
		||||
                    scope.$emit('editor-keydown', event);
 | 
			
		||||
                });
 | 
			
		||||
| 
						 | 
				
			
			@ -351,12 +370,109 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                    window.ImageManager.showExternal(image => {
 | 
			
		||||
                        let caretPos = currentCaretPos;
 | 
			
		||||
                        let currentContent = input.val();
 | 
			
		||||
                        let mdImageText = "";
 | 
			
		||||
                        let mdImageText = "";
 | 
			
		||||
                        input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
 | 
			
		||||
                        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) {
 | 
			
		||||
        return {
 | 
			
		||||
| 
						 | 
				
			
			@ -600,26 +768,60 @@ module.exports = function (ngApp, events) {
 | 
			
		|||
                // Add input for forms
 | 
			
		||||
                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
 | 
			
		||||
                element.on('click', '.entity-list a', function(event) {
 | 
			
		||||
                    event.preventDefault();
 | 
			
		||||
                    event.stopPropagation();
 | 
			
		||||
                    let item = $(this).closest('[data-entity-type]');
 | 
			
		||||
                    itemSelect(item);
 | 
			
		||||
                    itemSelect(item, isDoubleClick());
 | 
			
		||||
                });
 | 
			
		||||
                element.on('click', '[data-entity-type]', function(event) {
 | 
			
		||||
                    itemSelect($(this));
 | 
			
		||||
                    itemSelect($(this), isDoubleClick());
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                // Select entity action
 | 
			
		||||
                function itemSelect(item) {
 | 
			
		||||
                function itemSelect(item, doubleClick) {
 | 
			
		||||
                    let entityType = item.attr('data-entity-type');
 | 
			
		||||
                    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');
 | 
			
		||||
                    if (isSelected) item.addClass('selected').addClass('primary-background');
 | 
			
		||||
                    let newVal = isSelected ? `${entityType}:${entityId}` : '';
 | 
			
		||||
                    input.val(newVal);
 | 
			
		||||
 | 
			
		||||
                    if (!isSelected) {
 | 
			
		||||
                        events.emit('entity-select-change', null);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (!doubleClick && !isSelected) return;
 | 
			
		||||
 | 
			
		||||
                    let link = item.find('.entity-list-item-link').attr('href');
 | 
			
		||||
                    let name = item.find('.entity-list-item-name').text();
 | 
			
		||||
 | 
			
		||||
                    if (doubleClick) {
 | 
			
		||||
                        events.emit('entity-select-confirm', {
 | 
			
		||||
                            id: Number(entityId),
 | 
			
		||||
                            name: name,
 | 
			
		||||
                            link: link
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (isSelected) {
 | 
			
		||||
                        events.emit('entity-select-change', {
 | 
			
		||||
                            id: Number(entityId),
 | 
			
		||||
                            name: name,
 | 
			
		||||
                            link: link
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Get search url with correct types
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,9 +18,12 @@ window.baseUrl = function(path) {
 | 
			
		|||
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
 | 
			
		||||
 | 
			
		||||
// Global Event System
 | 
			
		||||
var Events = {
 | 
			
		||||
    listeners: {},
 | 
			
		||||
    emit: function (eventName, eventData) {
 | 
			
		||||
class EventManager {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.listeners = {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    emit(eventName, eventData) {
 | 
			
		||||
        if (typeof this.listeners[eventName] === 'undefined') return this;
 | 
			
		||||
        var eventsToStart = this.listeners[eventName];
 | 
			
		||||
        for (let i = 0; i < eventsToStart.length; i++) {
 | 
			
		||||
| 
						 | 
				
			
			@ -28,33 +31,35 @@ var Events = {
 | 
			
		|||
            event(eventData);
 | 
			
		||||
        }
 | 
			
		||||
        return this;
 | 
			
		||||
    },
 | 
			
		||||
    listen: function (eventName, callback) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    listen(eventName, callback) {
 | 
			
		||||
        if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
 | 
			
		||||
        this.listeners[eventName].push(callback);
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
window.Events = Events;
 | 
			
		||||
window.Events = new EventManager();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
var services = require('./services')(ngApp, Events);
 | 
			
		||||
var directives = require('./directives')(ngApp, Events);
 | 
			
		||||
var controllers = require('./controllers')(ngApp, Events);
 | 
			
		||||
var services = require('./services')(ngApp, window.Events);
 | 
			
		||||
var directives = require('./directives')(ngApp, window.Events);
 | 
			
		||||
var controllers = require('./controllers')(ngApp, window.Events);
 | 
			
		||||
 | 
			
		||||
//Global jQuery Config & Extensions
 | 
			
		||||
 | 
			
		||||
// Smooth scrolling
 | 
			
		||||
jQuery.fn.smoothScrollTo = function () {
 | 
			
		||||
    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
 | 
			
		||||
    }, 800); // Adjust to change animations speed (ms)
 | 
			
		||||
    return this;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 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 $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			@ -104,13 +109,14 @@ $(function () {
 | 
			
		|||
    var scrollTop = document.getElementById('back-to-top');
 | 
			
		||||
    var scrollTopBreakpoint = 1200;
 | 
			
		||||
    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';
 | 
			
		||||
            scrollTopShowing = true;
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                scrollTop.style.opacity = 0.4;
 | 
			
		||||
            }, 1);
 | 
			
		||||
        } else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) {
 | 
			
		||||
        } else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) {
 | 
			
		||||
            scrollTop.style.opacity = 0;
 | 
			
		||||
            scrollTopShowing = false;
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +130,27 @@ $(function () {
 | 
			
		|||
        $('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Popup close
 | 
			
		||||
    $('.popup-close').click(function() {
 | 
			
		||||
        $(this).closest('.overlay').fadeOut(240);
 | 
			
		||||
    });
 | 
			
		||||
    $('.overlay').click(function(event) {
 | 
			
		||||
        if (!$(event.target).hasClass('overlay')) return;
 | 
			
		||||
        $(this).fadeOut(240);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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 = {
 | 
			
		||||
    selector: '#html-editor',
 | 
			
		||||
    content_css: [
 | 
			
		||||
| 
						 | 
				
			
			@ -6,6 +68,8 @@ var mceOptions = module.exports = {
 | 
			
		|||
    ],
 | 
			
		||||
    body_class: 'page-content',
 | 
			
		||||
    relative_urls: false,
 | 
			
		||||
    remove_script_host: false,
 | 
			
		||||
    document_base_url: window.baseUrl('/'),
 | 
			
		||||
    statusbar: false,
 | 
			
		||||
    menubar: 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'},
 | 
			
		||||
    },
 | 
			
		||||
    file_browser_callback: function (field_name, url, type, win) {
 | 
			
		||||
        window.ImageManager.showExternal(function (image) {
 | 
			
		||||
            win.document.getElementById(field_name).value = image.url;
 | 
			
		||||
            if ("createEvent" in document) {
 | 
			
		||||
                var evt = document.createEvent("HTMLEvents");
 | 
			
		||||
                evt.initEvent("change", false, true);
 | 
			
		||||
                win.document.getElementById(field_name).dispatchEvent(evt);
 | 
			
		||||
            } else {
 | 
			
		||||
                win.document.getElementById(field_name).fireEvent("onchange");
 | 
			
		||||
            }
 | 
			
		||||
            var 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);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (type === 'file') {
 | 
			
		||||
            window.showEntityLinkSelector(function(entity) {
 | 
			
		||||
                let originalField = win.document.getElementById(field_name);
 | 
			
		||||
                originalField.value = entity.link;
 | 
			
		||||
                $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (type === 'image') {
 | 
			
		||||
            // Show image manager
 | 
			
		||||
            window.ImageManager.showExternal(function (image) {
 | 
			
		||||
 | 
			
		||||
                // 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) {
 | 
			
		||||
        var content = args.content;
 | 
			
		||||
        let content = args.content;
 | 
			
		||||
        if (content.indexOf('<img src="file://') !== -1) {
 | 
			
		||||
            args.content = '';
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -62,10 +144,14 @@ var mceOptions = module.exports = {
 | 
			
		|||
    extraSetups: [],
 | 
			
		||||
    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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        registerEditorShortcuts(editor);
 | 
			
		||||
 | 
			
		||||
        (function () {
 | 
			
		||||
            var wrap;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -76,12 +162,11 @@ var mceOptions = module.exports = {
 | 
			
		|||
            editor.on('dragstart', function () {
 | 
			
		||||
                var node = editor.selection.getNode();
 | 
			
		||||
 | 
			
		||||
                if (node.nodeName === 'IMG') {
 | 
			
		||||
                    wrap = editor.dom.getParent(node, '.mceTemp');
 | 
			
		||||
                if (node.nodeName !== 'IMG') return;
 | 
			
		||||
                wrap = editor.dom.getParent(node, '.mceTemp');
 | 
			
		||||
 | 
			
		||||
                    if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
 | 
			
		||||
                        wrap = node.parentNode;
 | 
			
		||||
                    }
 | 
			
		||||
                if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
 | 
			
		||||
                    wrap = node.parentNode;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -106,15 +191,15 @@ var mceOptions = module.exports = {
 | 
			
		|||
            });
 | 
			
		||||
        })();
 | 
			
		||||
 | 
			
		||||
        // Image picker button
 | 
			
		||||
        // Custom Image picker button
 | 
			
		||||
        editor.addButton('image-insert', {
 | 
			
		||||
            title: 'My title',
 | 
			
		||||
            icon: 'image',
 | 
			
		||||
            tooltip: 'Insert an image',
 | 
			
		||||
            onclick: function () {
 | 
			
		||||
                window.ImageManager.showExternal(function (image) {
 | 
			
		||||
                    var html = '<a href="' + image.url + '" target="_blank">';
 | 
			
		||||
                    html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">';
 | 
			
		||||
                    let html = `<a href="${image.url}" target="_blank">`;
 | 
			
		||||
                    html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
 | 
			
		||||
                    html += '</a>';
 | 
			
		||||
                    editor.execCommand('mceInsertContent', false, html);
 | 
			
		||||
                });
 | 
			
		||||
| 
						 | 
				
			
			@ -122,49 +207,8 @@ var mceOptions = module.exports = {
 | 
			
		|||
        });
 | 
			
		||||
 | 
			
		||||
        // Paste image-uploads
 | 
			
		||||
        editor.on('paste', function (e) {
 | 
			
		||||
            if (e.clipboardData) {
 | 
			
		||||
                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);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
        editor.on('paste', function(event) {
 | 
			
		||||
            editorPaste(event, editor);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
  background-color: rgba(0, 0, 0, 0.2);
 | 
			
		||||
  background-color: rgba(0, 0, 0, 0.333);
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  z-index: 95536;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			@ -10,26 +10,76 @@
 | 
			
		|||
  left: 0;
 | 
			
		||||
  right: 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;
 | 
			
		||||
  max-height: 90%;
 | 
			
		||||
  width: 90%;
 | 
			
		||||
  height: 90%;
 | 
			
		||||
  width: 1200px;
 | 
			
		||||
  height: auto;
 | 
			
		||||
  margin: 2% 5%;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  z-index: 999;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  h1, h2, h3 {
 | 
			
		||||
    font-weight: 300;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  &.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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -37,12 +87,6 @@
 | 
			
		|||
  border: 3px dashed #DDD;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.image-manager-bottom {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.image-manager-list .image {
 | 
			
		||||
  display: block;
 | 
			
		||||
  position: relative;
 | 
			
		||||
| 
						 | 
				
			
			@ -103,18 +147,13 @@
 | 
			
		|||
 | 
			
		||||
.image-manager-sidebar {
 | 
			
		||||
  width: 300px;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  margin-left: 1px;
 | 
			
		||||
  padding: 0 $-l;
 | 
			
		||||
  padding: $-m $-l;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
  border-left: 1px solid #DDD;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.image-manager-close {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  border-radius: 0;
 | 
			
		||||
  .dropzone-container {
 | 
			
		||||
    margin-top: $-m;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.image-manager-list {
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +164,6 @@
 | 
			
		|||
.image-manager-content {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  .container {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			@ -141,12 +179,13 @@
 | 
			
		|||
 * Copyright (c) 2012 Matias Meno <m@tias.me>
 | 
			
		||||
 */
 | 
			
		||||
.dz-message {
 | 
			
		||||
  font-size: 1.4em;
 | 
			
		||||
  font-size: 1.2em;
 | 
			
		||||
  line-height: 1.1;
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
  color: #aaa;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  padding: $-xl $-m;
 | 
			
		||||
  padding: $-l $-m;
 | 
			
		||||
  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 */
 | 
			
		||||
div[class^="col-"] img {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +47,9 @@ div[class^="col-"] img {
 | 
			
		|||
  &.fluid {
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
  &.medium {
 | 
			
		||||
    max-width: 992px;
 | 
			
		||||
  }
 | 
			
		||||
  &.small {
 | 
			
		||||
    max-width: 840px;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -155,6 +155,7 @@ form.search-box {
 | 
			
		|||
      text-decoration: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.faded span.faded-text {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -375,6 +375,9 @@ ul.pagination {
 | 
			
		|||
  .text-muted {
 | 
			
		||||
    color: #999;
 | 
			
		||||
  }
 | 
			
		||||
  li.padded {
 | 
			
		||||
    padding: $-xs $-m;
 | 
			
		||||
  }
 | 
			
		||||
  a {
 | 
			
		||||
    display: block;
 | 
			
		||||
    padding: $-xs $-m;
 | 
			
		||||
| 
						 | 
				
			
			@ -384,10 +387,10 @@ ul.pagination {
 | 
			
		|||
      background-color: #EEE;
 | 
			
		||||
    }
 | 
			
		||||
    i {
 | 
			
		||||
      margin-right: $-m;
 | 
			
		||||
      margin-right: $-s;
 | 
			
		||||
      padding-right: 0;
 | 
			
		||||
      display: inline;
 | 
			
		||||
      width: 22px;
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
      width: 16px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  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 {
 | 
			
		||||
  padding: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -238,7 +248,7 @@
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.tag-display {
 | 
			
		||||
  margin: $-xl $-xs;
 | 
			
		||||
  margin: $-xl $-m;
 | 
			
		||||
  border: 1px solid #DDD;
 | 
			
		||||
  min-width: 180px;
 | 
			
		||||
  max-width: 320px;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@
 | 
			
		|||
@import "animations";
 | 
			
		||||
@import "tinymce";
 | 
			
		||||
@import "highlightjs";
 | 
			
		||||
@import "image-manager";
 | 
			
		||||
@import "components";
 | 
			
		||||
@import "header";
 | 
			
		||||
@import "lists";
 | 
			
		||||
@import "pages";
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +72,7 @@ body.dragging, body.dragging * {
 | 
			
		|||
  border-radius: 3px;
 | 
			
		||||
  box-shadow: $bs-med;
 | 
			
		||||
  z-index: 999999;
 | 
			
		||||
  display: table;
 | 
			
		||||
  display: block;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  max-width: 480px;
 | 
			
		||||
  i, span {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<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))
 | 
			
		||||
        <p class="text-muted">{!! $book->searchSnippet !!}</p>
 | 
			
		||||
    @else
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,7 +50,7 @@
 | 
			
		|||
            var sortableOptions = {
 | 
			
		||||
                group: 'serialization',
 | 
			
		||||
                onDrop: function($item, container, _super) {
 | 
			
		||||
                    var pageMap = buildPageMap();
 | 
			
		||||
                    var pageMap = buildEntityMap();
 | 
			
		||||
                    $('#sort-tree-input').val(JSON.stringify(pageMap));
 | 
			
		||||
                    _super($item, container);
 | 
			
		||||
                },
 | 
			
		||||
| 
						 | 
				
			
			@ -74,29 +74,42 @@
 | 
			
		|||
                $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');
 | 
			
		||||
                $lists.each(function(listIndex) {
 | 
			
		||||
                    var list = $(this);
 | 
			
		||||
                    var bookId = list.closest('[data-type="book"]').attr('data-id');
 | 
			
		||||
                    var $childElements = list.find('[data-type="page"], [data-type="chapter"]');
 | 
			
		||||
                    $childElements.each(function(childIndex) {
 | 
			
		||||
                    var $directChildren = list.find('> [data-type="page"], > [data-type="chapter"]');
 | 
			
		||||
                    $directChildren.each(function(directChildIndex) {
 | 
			
		||||
                        var $childElem = $(this);
 | 
			
		||||
                        var type = $childElem.attr('data-type');
 | 
			
		||||
                        var parentChapter = false;
 | 
			
		||||
                        if(type === 'page' && $childElem.closest('[data-type="chapter"]').length === 1) {
 | 
			
		||||
                            parentChapter = $childElem.closest('[data-type="chapter"]').attr('data-id');
 | 
			
		||||
                        }
 | 
			
		||||
                        pageMap.push({
 | 
			
		||||
                            id: $childElem.attr('data-id'),
 | 
			
		||||
                        var childId = $childElem.attr('data-id');
 | 
			
		||||
                        entityMap.push({
 | 
			
		||||
                            id: childId,
 | 
			
		||||
                            sort: directChildIndex,
 | 
			
		||||
                            parentChapter: parentChapter,
 | 
			
		||||
                            type: type,
 | 
			
		||||
                            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>
 | 
			
		||||
            <span class="text-muted">  »  </span>
 | 
			
		||||
        @endif
 | 
			
		||||
        <a href="{{ $chapter->getUrl() }}" class="text-chapter">
 | 
			
		||||
            <i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->name }}
 | 
			
		||||
        <a href="{{ $chapter->getUrl() }}" class="text-chapter entity-list-item-link">
 | 
			
		||||
            <i class="zmdi zmdi-collection-bookmark"></i><span class="entity-list-item-name">{{ $chapter->name }}</span>
 | 
			
		||||
        </a>
 | 
			
		||||
    </h3>
 | 
			
		||||
    @if(isset($chapter->searchSnippet))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,14 @@
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
 | 
			
		||||
    @include('partials/entity-selector-popup')
 | 
			
		||||
 | 
			
		||||
    <script>
 | 
			
		||||
        (function() {
 | 
			
		||||
 | 
			
		||||
        })();
 | 
			
		||||
    </script>
 | 
			
		||||
 | 
			
		||||
@stop
 | 
			
		||||
| 
						 | 
				
			
			@ -13,8 +13,9 @@
 | 
			
		|||
                </div>
 | 
			
		||||
                <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>
 | 
			
		||||
                        <i class="zmdi zmdi-check-circle text-pos draft-notification" ng-class="{visible: draftUpdated}"></i>
 | 
			
		||||
                        <ul>
 | 
			
		||||
                            <li>
 | 
			
		||||
                                <a ng-click="forceDraftSave()" class="text-pos"><i class="zmdi zmdi-save"></i>Save Draft</a>
 | 
			
		||||
| 
						 | 
				
			
			@ -22,14 +23,25 @@
 | 
			
		|||
                            <li ng-if="isNewPageDraft">
 | 
			
		||||
                                <a href="{{ $model->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete Draft</a>
 | 
			
		||||
                            </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>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-sm-4 faded">
 | 
			
		||||
                    <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>
 | 
			
		||||
| 
						 | 
				
			
			@ -62,6 +74,8 @@
 | 
			
		|||
                        <span class="float left">Editor</span>
 | 
			
		||||
                        <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="insertEntityLink"><i class="zmdi zmdi-link"></i>Insert Entity Link</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}">
 | 
			
		||||
    <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>
 | 
			
		||||
 | 
			
		||||
    @if(isset($page->searchSnippet))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@
 | 
			
		|||
                            </a>
 | 
			
		||||
                        @endif
 | 
			
		||||
                        <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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,45 +5,59 @@
 | 
			
		|||
    <div class="faded-small toolbar">
 | 
			
		||||
        <div class="container">
 | 
			
		||||
            <div class="row">
 | 
			
		||||
                <div class="col-md-6 faded">
 | 
			
		||||
                <div class="col-sm-12 faded">
 | 
			
		||||
                    <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 class="col-md-6 faded">
 | 
			
		||||
                </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>
 | 
			
		||||
 | 
			
		||||
        @if(count($page->revisions) > 0)
 | 
			
		||||
 | 
			
		||||
            <table class="table">
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th width="40%">Name</th>
 | 
			
		||||
                    <th colspan="2" width="20%">Created By</th>
 | 
			
		||||
                    <th width="20%">Revision Date</th>
 | 
			
		||||
                    <th width="20%">Actions</th>
 | 
			
		||||
                    <th width="25%">Name</th>
 | 
			
		||||
                    <th colspan="2" width="10%">Created By</th>
 | 
			
		||||
                    <th width="15%">Revision Date</th>
 | 
			
		||||
                    <th width="25%">Changelog</th>
 | 
			
		||||
                    <th width="15%">Actions</th>
 | 
			
		||||
                </tr>
 | 
			
		||||
                @foreach($page->revisions as $revision)
 | 
			
		||||
                @foreach($page->revisions as $index => $revision)
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <td>{{$revision->name}}</td>
 | 
			
		||||
                        <td>{{ $revision->name }}</td>
 | 
			
		||||
                        <td style="line-height: 0;">
 | 
			
		||||
                            @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
 | 
			
		||||
                        </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>
 | 
			
		||||
                            <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
 | 
			
		||||
                            <span class="text-muted"> | </span>
 | 
			
		||||
                            <a href="{{ $revision->getUrl('/restore') }}">Restore</a>
 | 
			
		||||
                        </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>{{ $revision->summary }}</td>
 | 
			
		||||
                        @if ($index !== 0)
 | 
			
		||||
                            <td>
 | 
			
		||||
                                <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
 | 
			
		||||
                                <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>
 | 
			
		||||
                @endforeach
 | 
			
		||||
            </table>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,7 +58,7 @@
 | 
			
		|||
    <div class="container" id="page-show" ng-non-bindable>
 | 
			
		||||
        <div class="row">
 | 
			
		||||
            <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 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 class="overlay anim-slide" ng-show="showing" ng-cloak ng-click="hide()">
 | 
			
		||||
        <div class="image-manager-body" ng-click="$event.stopPropagation()">
 | 
			
		||||
    <div class="overlay" ng-cloak ng-click="hide()">
 | 
			
		||||
        <div class="popup-body" ng-click="$event.stopPropagation()">
 | 
			
		||||
 | 
			
		||||
            <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 class="popup-header primary-background">
 | 
			
		||||
                <div class="popup-title">Image Select</div>
 | 
			
		||||
                <button class="popup-close neg corner-button button">x</button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="flex-fill image-manager-body">
 | 
			
		||||
 | 
			
		||||
                <div class="image-manager-content">
 | 
			
		||||
                    <div ng-if="imageType === 'gallery'" class="container">
 | 
			
		||||
                        <div 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 ng-show="view === 'all'" >
 | 
			
		||||
                    <form ng-submit="searchImages()" class="contained-search-box">
 | 
			
		||||
                        <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 title="Search" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="image-manager-list">
 | 
			
		||||
                    <div ng-repeat="image in images">
 | 
			
		||||
                        <div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
 | 
			
		||||
                             ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)">
 | 
			
		||||
                            <img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
 | 
			
		||||
                            <div class="image-meta">
 | 
			
		||||
                                <span class="name" ng-bind="image.name"></span>
 | 
			
		||||
                                <span class="date">Uploaded @{{ getDate(image.created_at) | date:'mediumDate' }}</span>
 | 
			
		||||
                    <div ng-show="view === 'all'" >
 | 
			
		||||
                        <form ng-submit="searchImages()" class="contained-search-box">
 | 
			
		||||
                            <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 title="Search" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
 | 
			
		||||
                        </form>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="image-manager-list">
 | 
			
		||||
                        <div ng-repeat="image in images">
 | 
			
		||||
                            <div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
 | 
			
		||||
                                 ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)">
 | 
			
		||||
                                <img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
 | 
			
		||||
                                <div class="image-meta">
 | 
			
		||||
                                    <span class="name" ng-bind="image.name"></span>
 | 
			
		||||
                                    <span class="date">Uploaded @{{ getDate(image.created_at)  }}</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</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>
 | 
			
		||||
 | 
			
		||||
            <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>
 | 
			
		||||
| 
						 | 
				
			
			@ -76,6 +76,14 @@ class EntitySearchTest extends TestCase
 | 
			
		|||
            ->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()
 | 
			
		||||
    {
 | 
			
		||||
        $book = \BookStack\Book::all()->last();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -218,13 +218,24 @@ class EntityTest extends TestCase
 | 
			
		|||
 | 
			
		||||
    public function test_old_page_slugs_redirect_to_new_pages()
 | 
			
		||||
    {
 | 
			
		||||
        $page = \BookStack\Page::all()->first();
 | 
			
		||||
        $page = \BookStack\Page::first();
 | 
			
		||||
        $pageUrl = $page->getUrl();
 | 
			
		||||
        $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
 | 
			
		||||
        // Need to save twice since revisions are not generated in seeder.
 | 
			
		||||
        $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')
 | 
			
		||||
            ->type('super test page', '#name')
 | 
			
		||||
            ->press('Save Page')
 | 
			
		||||
            // Check redirect
 | 
			
		||||
            ->seePageIs($newPageUrl)
 | 
			
		||||
            ->visit($pageUrl)
 | 
			
		||||
            ->seePageIs($newPageUrl);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue