]> BookStack Code Mirror - bookstack/commitdiff
Extracted page form js and added better page content linking
authorDan Brown <redacted>
Sat, 10 Oct 2015 17:57:52 +0000 (18:57 +0100)
committerDan Brown <redacted>
Sat, 10 Oct 2015 17:57:52 +0000 (18:57 +0100)
13 files changed:
app/Http/Controllers/PageController.php
app/Repos/PageRepo.php
package.json
public/ZeroClipboard.swf [new file with mode: 0644]
resources/assets/js/global.js
resources/assets/js/pages/page-form.js [new file with mode: 0644]
resources/assets/sass/_animations.scss
resources/assets/sass/_buttons.scss
resources/assets/sass/_pages.scss
resources/views/pages/form.blade.php
resources/views/pages/page-display.blade.php
resources/views/pages/show.blade.php
resources/views/public.blade.php

index c2867caa892a2a73a7a31825dad57ce33c4fcbdb..56ea6992c88edde8bd007223ee78681f99fccaad 100644 (file)
@@ -63,22 +63,14 @@ class PageController extends Controller
             'html'   => 'required|string',
             'parent' => 'integer|exists:pages,id'
         ]);
-        $book = $this->bookRepo->getBySlug($bookSlug);
-        $page = $this->pageRepo->newFromInput($request->all());
 
-        $page->slug = $this->pageRepo->findSuitableSlug($page->name, $book->id);
-        $page->priority = $this->bookRepo->getNewPriority($book);
+        $input = $request->all();
+        $book = $this->bookRepo->getBySlug($bookSlug);
+        $chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null;
+        $input['priority'] = $this->bookRepo->getNewPriority($book);
 
-        if ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) {
-            $page->chapter_id = $request->get('chapter');
-        }
+        $page = $this->pageRepo->saveNew($input, $book, $chapterId);
 
-        $page->book_id = $book->id;
-        $page->text = strip_tags($page->html);
-        $page->created_by = Auth::user()->id;
-        $page->updated_by = Auth::user()->id;
-        $page->save();
-        $this->pageRepo->saveRevision($page);
         Activity::add($page, 'page_create', $book->id);
         return redirect($page->getUrl());
     }
index 441d5980e4cc7ac652850da63572223226884fea..1f0c618191122dd5ca10c1821dfdb358bab5834b 100644 (file)
@@ -1,7 +1,11 @@
 <?php namespace BookStack\Repos;
 
 
+use BookStack\Book;
+use BookStack\Chapter;
+use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
 use BookStack\Page;
 use BookStack\PageRevision;
@@ -42,6 +46,10 @@ class PageRepo
         return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
     }
 
+    /**
+     * @param $input
+     * @return Page
+     */
     public function newFromInput($input)
     {
         $page = $this->page->fill($input);
@@ -53,6 +61,83 @@ class PageRepo
         return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count();
     }
 
+    /**
+     * Save a new page into the system.
+     * Input validation must be done beforehand.
+     * @param array $input
+     * @param Book  $book
+     * @param int   $chapterId
+     * @return Page
+     */
+    public function saveNew(array $input, Book $book, $chapterId = null)
+    {
+        $page = $this->newFromInput($input);
+        $page->slug = $this->findSuitableSlug($page->name, $book->id);
+
+        if ($chapterId) $page->chapter_id = $chapterId;
+
+        $page->html = $this->formatHtml($input['html']);
+        $page->text = strip_tags($page->html);
+        $page->created_by = auth()->user()->id;
+        $page->updated_by = auth()->user()->id;
+
+        $book->pages()->save($page);
+        $this->saveRevision($page);
+        return $page;
+    }
+
+    /**
+     * Formats a page's html to be tagged correctly
+     * within the system.
+     * @param string $htmlText
+     * @return string
+     */
+    protected function formatHtml($htmlText)
+    {
+        libxml_use_internal_errors(true);
+        $doc = new \DOMDocument();
+        $doc->loadHTML($htmlText);
+
+        $container = $doc->documentElement;
+        $body = $container->childNodes[0];
+        $childNodes = $body->childNodes;
+
+        // Ensure no duplicate ids are used
+        $lastId = false;
+        $idArray = [];
+
+        foreach ($childNodes as $index => $childNode) {
+            /** @var \DOMElement $childNode */
+            if (get_class($childNode) !== 'DOMElement') continue;
+
+            // Overwrite id if not a bookstack custom id
+            if ($childNode->hasAttribute('id')) {
+                $id = $childNode->getAttribute('id');
+                if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
+                    $idArray[] = $id;
+                    continue;
+                };
+            }
+
+            // Create an unique id for the element
+            do {
+                $id = 'bkmrk-' . substr(uniqid(), -5);
+            } while ($id == $lastId);
+            $lastId = $id;
+
+            $childNode->setAttribute('id', $id);
+            $idArray[] = $id;
+        }
+
+        // Generate inner html as a string
+        $html = '';
+        foreach ($childNodes as $childNode) {
+            $html .= $doc->saveHTML($childNode);
+        }
+
+        return $html;
+    }
+
     public function destroyById($id)
     {
         $page = $this->getById($id);
@@ -99,8 +184,8 @@ class PageRepo
      */
     public function searchForImage($imageString)
     {
-        $pages = $this->page->where('html', 'like', '%'.$imageString.'%')->get();
-        foreach($pages as $page) {
+        $pages = $this->page->where('html', 'like', '%' . $imageString . '%')->get();
+        foreach ($pages as $page) {
             $page->url = $page->getUrl();
             $page->html = '';
             $page->text = '';
@@ -110,15 +195,16 @@ class PageRepo
 
     /**
      * Updates a page with any fillable data and saves it into the database.
-     * @param Page $page
-     * @param      $book_id
-     * @param      $data
+     * @param Page   $page
+     * @param int    $book_id
+     * @param string $input
      * @return Page
      */
-    public function updatePage(Page $page, $book_id, $data)
+    public function updatePage(Page $page, $book_id, $input)
     {
-        $page->fill($data);
+        $page->fill($input);
         $page->slug = $this->findSuitableSlug($page->name, $book_id, $page->id);
+        $page->html = $this->formatHtml($input['html']);
         $page->text = strip_tags($page->html);
         $page->updated_by = Auth::user()->id;
         $page->save();
@@ -189,7 +275,7 @@ class PageRepo
     public function setBookId($bookId, Page $page)
     {
         $page->book_id = $bookId;
-        foreach($page->activity as $activity) {
+        foreach ($page->activity as $activity) {
             $activity->book_id = $bookId;
             $activity->save();
         }
index af40e256ff617343d499c000b7ea89f0b9c528c9..09edc2b20d5b7487279b0731647cf7fc1b3de99b 100644 (file)
@@ -11,6 +11,7 @@
     "dropzone": "^4.0.1",
     "laravel-elixir": "^3.3.1",
     "vue": "^0.12.16",
-    "vue-resource": "^0.1.16"
+    "vue-resource": "^0.1.16",
+    "zeroclipboard": "^2.2.0"
   }
 }
diff --git a/public/ZeroClipboard.swf b/public/ZeroClipboard.swf
new file mode 100644 (file)
index 0000000..8bad6a3
Binary files /dev/null and b/public/ZeroClipboard.swf differ
index 519ce74128a90ea620d542dad9efcc2ecea7d362..a6fca8dbd89e0d1b63e168ad8caf721a2f13a967 100644 (file)
@@ -1,3 +1,7 @@
+window.ZeroClipboard = require('zeroclipboard');
+window.ZeroClipboard.config({
+    swfPath: '/ZeroClipboard.swf'
+});
 
 // Global jQuery Elements
 $(function () {
@@ -23,6 +27,12 @@ function elemExists(selector) {
     return document.querySelector(selector) !== null;
 }
 
+// TinyMCE editor
+if(elemExists('#html-editor')) {
+    var tinyMceOptions = require('./pages/page-form');
+    tinymce.init(tinyMceOptions);
+}
+
 // Vue JS elements
 var Vue = require('vue');
 Vue.use(require('vue-resource'));
diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js
new file mode 100644 (file)
index 0000000..de53a49
--- /dev/null
@@ -0,0 +1,39 @@
+
+module.exports = {
+    selector: '#html-editor',
+    content_css: [
+        '/css/styles.css',
+        '//fonts.googleapis.com/css?family=Roboto:400,400italic,500,500italic,700,700italic,300italic,100,300'
+    ],
+    body_class: 'page-content',
+    relative_urls: false,
+    statusbar: false,
+    menubar: false,
+    //height: 700,
+    extended_valid_elements: 'pre[*]',
+    valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
+    plugins: "image table textcolor paste link imagetools fullscreen code hr",
+    toolbar: "code undo | styleselect | hr bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image link | fullscreen",
+    content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
+    style_formats: [
+        {title: "Header 1", format: "h1"},
+        {title: "Header 2", format: "h2"},
+        {title: "Header 3", format: "h3"},
+        {title: "Header 4", format: "h4"},
+        {title: "Paragraph", format: "p"},
+        {title: "Blockquote", format: "blockquote"},
+        {title: "Code Block", icon: "code", format: "pre"}
+    ],
+    file_browser_callback: function(field_name, url, type, win) {
+        ImageManager.show(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");
+            }
+        });
+    }
+};
\ No newline at end of file
index e6e85ef8edb7e2e4c9f7785e2e2131f633e7ccaa..582d718c86ff741327e58761e357115ab58ea21c 100644 (file)
     transform: translate3d(0, 0, 0);
   }
 }
+
+@keyframes pointer {
+  0% {
+      transform: translate3d(0, 20px, 0) scale3d(0, 0, 0);
+  }
+  100% {
+      transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
+  }
+}
+
+.anim.pointer {
+  transform-origin: 50% 100%;
+  animation-name: pointer;
+  animation-duration: 180ms;
+  animation-delay: 0s;
+  animation-timing-function: cubic-bezier(.62, .28, .23, .99);
+}
\ No newline at end of file
index bafb68165d05d464ae5660f842efd8740fcc389f..72fa33cfedfc1f3a76244cb929f53d844d8c16c2 100644 (file)
@@ -85,29 +85,9 @@ $button-border-radius: 2px;
   display: block;
 }
 
-// Floating action button
-//.fab {
-//  $size: 70px;
-//  button.button {
-//    border-radius: 100%;
-//    width: $size;
-//    height: $size;
-//    font-size: 48px;
-//    text-align: center;
-//    margin: 0;
-//    padding: 0;
-//    border: 0;
-//    box-shadow: 0 0 2px 2px #DDD;
-//    transition: all ease-in-out 160ms;
-//    i {
-//      transform: rotate(0deg);
-//      transition: all ease-in-out 160ms;
-//    }
-//    &:hover {
-//      box-shadow: 0 2px 4px 2px #CCC;
-//      i {
-//        transform: rotate(180deg);
-//      }
-//    }
-//  }
-//}
+.button.icon {
+  i {
+    padding-right: 0;
+  }
+}
+
index c7d41a5d9f3f5a2840655f8e990fe4adac4db60c..c4b2b05e1e443b3ed2022b1b39e3ac51c5e6d100 100644 (file)
   }
 }
 
-// Link hooks & popovers
-a.link-hook {
-  position: absolute;
+// Page content pointers
+.pointer-container {
+  position: relative;
+  display: none;
+  left: 2%;
+}
+.pointer {
+  border: 1px solid #CCC;
   display: inline-block;
-  top: $-xs;
-  left: -$-l;
-  padding-bottom: 30px;
-  font-size: 20px;
-  line-height: 20px;
-  color: #BBB;
-  opacity: 0;
-  transform: translate3d(10px, 0, 0);
-  transition: all ease-in-out 240ms;
-  background-color: transparent;
-  &:hover {
-    color: $primary;
+  padding: $-xs $-s;
+  border-radius: 4px;
+  box-shadow: 0 0 8px 1px rgba(212, 209, 209, 0.35);
+  position: absolute;
+  top: -60px;
+  background-color:#FFF;
+  &:before {
+    position: absolute;
+    left: 50%;
+    bottom: -9px;
+    width: 16px;
+    height: 16px;
+    margin-left: -8px;
+    content: '';
+    display: block;
+    background-color:#FFF;
+    transform: rotate(45deg);
+    transform-origin: 50% 50%;
+    border-bottom: 1px solid #CCC;
+    border-right: 1px solid #CCC;
+    z-index: 1;
+  }
+  input {
+    background-color: #FFF;
+    border: 1px solid #DDD;
+    color: #666;
+    width: 180px;
+    z-index: 40;
+  }
+  input, button {
+    position: relative;
+    border-radius: 0;
+    height: 28px;
+    font-size: 12px;
+  }
+  > i {
+    color: #888;
+    font-size: 18px;
+    padding-top: 4px;
+  }
+  .button {
+    line-height: 1;
+    margin: 0 0 0 -4px;
+    box-shadow: none;
   }
 }
+
 h1, h2, h3, h4, h5, h6 {
   &:hover a.link-hook {
     opacity: 1;
     transform: translate3d(0, 0, 0);
   }
 }
-
-// Side Navigation
-.side-nav {
-  position: fixed;
-  padding-left: $-m;
-  opacity: 0.8;
-  margin-top: $-xxl;
-  margin-left: 0;
-  max-width: 240px;
-  display: none;
-}
\ No newline at end of file
index 44e396c840225a9364bbcf67811d367c5415ed2c..de88dec0a9694d013068621d202d958c60e84c8f 100644 (file)
@@ -28,7 +28,7 @@
         </div>
     </div>
     <div class="edit-area flex-fill flex">
-        <textarea id="html" name="html" rows="5"
+        <textarea id="html-editor" name="html" rows="5"
                   @if($errors->has('html')) class="neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
         @if($errors->has('html'))
             <div class="text-neg text-small">{{ $errors->first('html') }}</div>
     </div>
 </div>
 
-
-
-
-
-<script>
-    $(function() {
-        //ImageManager.show('#image-manager');
-
-        tinymce.init({
-            selector: '.edit-area textarea',
-            content_css: [
-                '/css/app.css',
-                '//fonts.googleapis.com/css?family=Roboto:400,400italic,500,500italic,700,700italic,300italic,100,300'
-            ],
-            body_class: 'page-content',
-            relative_urls: false,
-            statusbar: false,
-            menubar: false,
-            //height: 700,
-            extended_valid_elements: 'pre[*]',
-            plugins: "image table textcolor paste link imagetools fullscreen code hr",
-            toolbar: "code undo | styleselect | hr bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image link | fullscreen",
-            content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
-            style_formats: [
-                {title: "Header 1", format: "h1"},
-                {title: "Header 2", format: "h2"},
-                {title: "Header 3", format: "h3"},
-                {title: "Header 4", format: "h4"},
-                {title: "Paragraph", format: "p"},
-                {title: "Blockquote", format: "blockquote"},
-                {title: "Code Block", icon: "code", format: "pre"}
-            ],
-            file_browser_callback: function(field_name, url, type, win) {
-                ImageManager.show(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");
-                    }
-                });
-            }
-//            setup: function(editor) {
-//                editor.addButton('full', {
-//                    title: 'Expand Editor',
-//                    icon: 'fullscreen',
-//                    onclick: function() {
-//                        var container = $(editor.getContainer()).toggleClass('fullscreen');
-//                        var isFull = container.hasClass('fullscreen');
-//                        var iframe = container.find('iframe').first();
-//                        var height = isFull ? $(window).height()-110 : 600;
-//                        iframe.css('height', height + 'px');
-//                    }
-//                });
-//            }
-        });
-
-
-
-    });
-</script>
-
 <image-manager></image-manager>
\ No newline at end of file
index 6eb623b3977da64a5417fcb56083774a5abd5b31..8d3625db89a490dd062f2c07495957ba28603a47 100644 (file)
@@ -1,10 +1,3 @@
-<h1>{{$page->name}}</h1>
-@if(count($page->children) > 0)
-    <h4 class="text-muted">Sub-pages</h4>
-    <div class="page-list">
-        @foreach($page->children as $childPage)
-            <a href="{{ $childPage->getUrl() }}">{{ $childPage->name }}</a>
-        @endforeach
-    </div>
-@endif
+<h1 id="bkmrk-page-title">{{$page->name}}</h1>
+
 {!! $page->html !!}
\ No newline at end of file
index 5fa41eff8112e04037cebd924c4b14b7989088ff..e30cfef8210647f5b0bc0b42488944a1a6e211a4 100644 (file)
         <div class="row">
             <div class="col-md-9">
                 <div class="page-content anim fadeIn">
+
+                    <div class="pointer-container" id="pointer">
+                        <div class="pointer anim">
+                            <i class="zmdi zmdi-link"></i>
+                            <input readonly="readonly" type="text" placeholder="url">
+                            <button class="button icon" title="Copy Link" data-clipboard-text=""><i class="zmdi zmdi-copy"></i></button>
+                        </div>
+                    </div>
+
                     @include('pages/page-display')
+
                     <hr>
+
                     <p class="text-muted small">
                         Created {{$page->created_at->diffForHumans()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif
                         <br>
                         Last Updated {{$page->updated_at->diffForHumans()}} @if($page->createdBy) by {{$page->updatedBy->name}} @endif
                     </p>
+
                 </div>
             </div>
             <div class="col-md-3">
+
                 @include('pages/sidebar-tree-list', ['book' => $book])
-                <div class="side-nav faded">
-                    <h4>Page Navigation</h4>
-                    <ul class="page-nav-list">
-                    </ul>
-                </div>
+
             </div>
         </div>
     </div>
     <script>
         $(document).ready(function() {
 
-            // Set up document navigation
-            var pageNav = $('.page-nav-list');
-            var pageContent = $('.page-content');
-            var headers = pageContent.find('h1, h2, h3, h4, h5, h6');
-            if(headers.length > 5) {
-                headers.each(function() {
-                    var header = $(this);
-                    var tag = header.prop('tagName');
-                    var listElem = $('<li></li>').addClass('nav-'+tag);
-                    var link = $('<a></a>').text(header.text().trim()).attr('href', '#');
-                    listElem.append(link);
-                    pageNav.append(listElem);
-                    link.click(function(e) {
-                        e.preventDefault();
-                        header.smoothScrollTo();
-                    })
-                });
-                $('.side-nav').fadeIn();
-            } else {
-                $('.side-nav').hide();
-            }
-
 
-            // Set up link hooks
+            // Set up pointer
+            var $pointer = $('#pointer').detach();
             var pageId = {{$page->id}};
-            headers.each(function() {
-                var text = $(this).text().trim();
-                var link = '/link/' + pageId + '#' + encodeURIComponent(text);
-                var linkHook = $('<a class="link-hook"><i class="zmdi zmdi-link"></i></a>')
-                        .attr({"data-content": link, href: link, target: '_blank'});
-                linkHook.click(function(e) {
-                    e.preventDefault();
-                    goToText(text);
-                });
-                $(this).append(linkHook);
+            var isSelection = false;
+
+            $pointer.find('input').click(function(e){$(this).select();e.stopPropagation();});
+            new ZeroClipboard( $pointer.find('button').first()[0] );
+
+            $(document.body).find('*').on('click focus', function(e) {
+                if(!isSelection) {
+                    $pointer.detach();
+                }
+            });
+
+            $('.page-content [id^="bkmrk"]').on('mouseup keyup', function(e) {
+                var selection = window.getSelection();
+                if(selection.toString().length === 0) return;
+                // Show pointer and set link
+                var $elem = $(this);
+                var link = window.location.protocol + "//" + window.location.host + '/link/' + pageId + '#' + $elem.attr('id');
+                $pointer.find('input').val(link);
+                $pointer.find('button').first().attr('data-clipboard-text', link);
+                $elem.before($pointer);
+                $pointer.show();
+                e.stopPropagation();
+
+                isSelection = true;
+                setTimeout(function() {
+                    isSelection = false;
+                }, 100);
             });
 
             function goToText(text) {
-                $('.page-content').find(':contains("'+text+'")').smoothScrollTo();
+                var idElem = $('.page-content').find('#' + text).first();
+                if(idElem.length !== 0) {
+                    idElem.smoothScrollTo();
+                } else {
+                    $('.page-content').find(':contains("'+text+'")').smoothScrollTo();
+                }
             }
 
             if(window.location.hash) {
                 goToText(text);
             }
 
-            //$('[data-toggle="popover"]').popover()
         });
     </script>
 
index 7606f936aa2260fa93663f4ee67a8e8ddf15d521..39bdab1ef1ee4d55f73523687dccb7d15210c12e 100644 (file)
@@ -3,14 +3,12 @@
 <head>
     <title>BookStack</title>
     <meta name="viewport" content="width=device-width">
-    <link rel="stylesheet" href="/css/app.css">
+    <link rel="stylesheet" href="/css/styles.css">
     <link href='//fonts.googleapis.com/css?family=Roboto:400,400italic,500,500italic,700,700italic,300italic,100,300' rel='stylesheet' type='text/css'>
     <link rel="stylesheet" href="/bower/material-design-iconic-font/dist/css/material-design-iconic-font.min.css">
 
     <!-- Scripts -->
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
-    <script src="/js/common.js"></script>
-
 </head>
 <body class="@yield('body-class')">