]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'page_link_selector'
authorDan Brown <redacted>
Sat, 3 Sep 2016 09:40:27 +0000 (10:40 +0100)
committerDan Brown <redacted>
Sat, 3 Sep 2016 09:40:27 +0000 (10:40 +0100)
1  2 
resources/assets/js/controllers.js
resources/assets/js/directives.js
resources/assets/js/pages/page-form.js
resources/assets/sass/styles.scss
resources/views/pages/form.blade.php

index 28a45e59193737786e6fe12c83a600876c7902d2,beb2fe93e321c9642d7a5ba25bc74dda5a9e1b93..fcaba2914b8557bac98a33fe5c5ce5c32bb37355
@@@ -69,7 -69,7 +69,7 @@@ module.exports = function (ngApp, event
               */
              function callbackAndHide(returnData) {
                  if (callback) callback(returnData);
-                 $scope.showing = false;
+                 $scope.hide();
              }
  
              /**
              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();
               */
              $scope.hide = function () {
                  $scope.showing = false;
+                 $('#image-manager').find('.overlay').fadeOut(240);
              };
  
              var baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');
  
          /**
           * Save a draft update into the system via an AJAX request.
 -         * @param title
 -         * @param html
           */
          function saveDraft() {
              var data = {
                  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();
          };
index 286854832d75a2730a85c49b94976596ad4e7e7f,4e1bb0dfd368737007c89fc5b6cce34dd92232ab..1271b31123dc937ddfc07c220367b204799dc7fc
@@@ -271,8 -271,6 +271,6 @@@ module.exports = function (ngApp, event
                  scope.mdModel = content;
                  scope.mdChange(markdown(content));
  
-                 console.log('test');
                  element.on('change input', (event) => {
                      content = element.val();
                      $timeout(() => {
                  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;
  
                          input[0].selectionEnd = caretPos + ('![](http://'.length);
                          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);
                  });
                      });
                  });
  
+                 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 = `/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.url);
 +                            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);
 +                }
 +
              }
          }
      }]);
          }
      }]);
  
+     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 {
                  // 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
index 15fcdbb8e6cacb5479d97eb58da35b61b94a9648,d8f298959e130e862075d78339c2c82cbfbaca8c..daf9639d7d83948602e919229fc0c3da566acd5b
@@@ -1,6 -1,6 +1,6 @@@
  "use strict";
  
 -function editorPaste(e) {
 +function editorPaste(e, editor) {
      if (!e.clipboardData) return
      var items = e.clipboardData.items;
      if (!items) return;
@@@ -32,7 -32,7 +32,7 @@@
                      var result = JSON.parse(xhr.responseText);
                      editor.dom.setAttrib(id, 'src', result.url);
                  } else {
 -                    console.log('An error occured uploading the image');
 +                    console.log('An error occurred uploading the image');
                      console.log(xhr.responseText);
                      editor.dom.remove(id);
                  }
@@@ -95,20 -95,38 +95,38 @@@ 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) {
+                 var 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) {
+                     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");
+                 }
+                 // Replace the actively selected content with the linked image
+                 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);
+             });
+         }
      },
      paste_preprocess: function (plugin, args) {
          var content = args.content;
      extraSetups: [],
      setup: function (editor) {
  
+         // Run additional setup actions
+         // Used by the angular side of things
          for (var i = 0; i < mceOptions.extraSetups.length; i++) {
              mceOptions.extraSetups[i](editor);
          }
          });
  
          // Paste image-uploads
 -        editor.on('paste', editorPaste);
 +        editor.on('paste', function(event) {
 +            editorPaste(event, editor);
 +        });
      }
  };
index 21043258877c92564b912505f475dd8cfc74edaf,90b452a8e044b25e42ac545904196647b2398bf0..7d33bd0a640e87be1abeec580ee95f5beae3e3a8
@@@ -12,7 -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 +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 {
index 5aee9c596f2ef1bd8b0e58b1187f000b426d51e6,e3c145aa267eaa45aacda7d9f04fbe7e034f99ee..0e0c3672e9df6f25c8518a4258a97cec22319b00
@@@ -13,9 -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>&nbsp; <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>
@@@ -74,6 -73,8 +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>
+                             &nbsp;|&nbsp;
+                             <button class="text-button" type="button" data-action="insertEntityLink"><i class="zmdi zmdi-link"></i>Insert Entity Link</button>
                          </div>
                      </div>