Merge branch 'page_link_selector'

This commit is contained in:
Dan Brown 2016-09-03 10:40:27 +01:00
commit 0dbb8babee
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
15 changed files with 378 additions and 132 deletions

View File

@ -69,7 +69,7 @@ module.exports = function (ngApp, events) {
*/ */
function callbackAndHide(returnData) { function callbackAndHide(returnData) {
if (callback) callback(returnData); if (callback) callback(returnData);
$scope.showing = false; $scope.hide();
} }
/** /**
@ -109,6 +109,7 @@ module.exports = function (ngApp, events) {
function show(doneCallback) { function show(doneCallback) {
callback = doneCallback; callback = doneCallback;
$scope.showing = true; $scope.showing = true;
$('#image-manager').find('.overlay').css('display', 'flex').hide().fadeIn(240);
// Get initial images if they have not yet been loaded in. // Get initial images if they have not yet been loaded in.
if (!dataLoaded) { if (!dataLoaded) {
fetchData(); fetchData();
@ -131,6 +132,7 @@ module.exports = function (ngApp, events) {
*/ */
$scope.hide = function () { $scope.hide = function () {
$scope.showing = false; $scope.showing = false;
$('#image-manager').find('.overlay').fadeOut(240);
}; };
var baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/'); var baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');

View File

@ -271,8 +271,6 @@ module.exports = function (ngApp, events) {
scope.mdModel = content; scope.mdModel = content;
scope.mdChange(markdown(content)); scope.mdChange(markdown(content));
console.log('test');
element.on('change input', (event) => { element.on('change input', (event) => {
content = element.val(); content = element.val();
$timeout(() => { $timeout(() => {
@ -304,6 +302,7 @@ module.exports = function (ngApp, events) {
const input = element.find('[markdown-input] textarea').first(); const input = element.find('[markdown-input] textarea').first();
const display = element.find('.markdown-display').first(); const display = element.find('.markdown-display').first();
const insertImage = element.find('button[data-action="insertImage"]'); const insertImage = element.find('button[data-action="insertImage"]');
const insertEntityLink = element.find('button[data-action="insertEntityLink"]')
let currentCaretPos = 0; let currentCaretPos = 0;
@ -355,6 +354,13 @@ module.exports = function (ngApp, events) {
input[0].selectionEnd = caretPos + ('![](http://'.length); input[0].selectionEnd = caretPos + ('![](http://'.length);
return; return;
} }
// Insert entity link shortcut
if (event.which === 75 && event.ctrlKey && event.shiftKey) {
showLinkSelector();
return;
}
// Pass key presses to controller via event // Pass key presses to controller via event
scope.$emit('editor-keydown', event); scope.$emit('editor-keydown', event);
}); });
@ -370,6 +376,26 @@ module.exports = function (ngApp, events) {
}); });
}); });
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 // Upload and insert image on paste
function editorPaste(e) { function editorPaste(e) {
e = e.originalEvent; e = e.originalEvent;
@ -677,6 +703,58 @@ module.exports = function (ngApp, events) {
} }
}]); }]);
ngApp.directive('entityLinkSelector', [function($http) {
return {
restict: 'A',
link: function(scope, element, attrs) {
const selectButton = element.find('.entity-link-selector-confirm');
let callback = false;
let entitySelection = null;
// Handle entity selection change, Stores the selected entity locally
function entitySelectionChange(entity) {
entitySelection = entity;
if (entity === null) {
selectButton.attr('disabled', 'true');
} else {
selectButton.removeAttr('disabled');
}
}
events.listen('entity-select-change', entitySelectionChange);
// Handle selection confirm button click
selectButton.click(event => {
hide();
if (entitySelection !== null) callback(entitySelection);
});
// Show selector interface
function show() {
element.fadeIn(240);
}
// Hide selector interface
function hide() {
element.fadeOut(240);
}
// Listen to confirmation of entity selections (doubleclick)
events.listen('entity-select-confirm', entity => {
hide();
callback(entity);
});
// Show entity selector, Accessible globally, and store the callback
window.showEntityLinkSelector = function(passedCallback) {
show();
callback = passedCallback;
};
}
};
}]);
ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) { ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) {
return { return {
@ -690,26 +768,60 @@ module.exports = function (ngApp, events) {
// Add input for forms // Add input for forms
const input = element.find('[entity-selector-input]').first(); const input = element.find('[entity-selector-input]').first();
// Detect double click events
var lastClick = 0;
function isDoubleClick() {
let now = Date.now();
let answer = now - lastClick < 300;
lastClick = now;
return answer;
}
// Listen to entity item clicks // Listen to entity item clicks
element.on('click', '.entity-list a', function(event) { element.on('click', '.entity-list a', function(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
let item = $(this).closest('[data-entity-type]'); let item = $(this).closest('[data-entity-type]');
itemSelect(item); itemSelect(item, isDoubleClick());
}); });
element.on('click', '[data-entity-type]', function(event) { element.on('click', '[data-entity-type]', function(event) {
itemSelect($(this)); itemSelect($(this), isDoubleClick());
}); });
// Select entity action // Select entity action
function itemSelect(item) { function itemSelect(item, doubleClick) {
let entityType = item.attr('data-entity-type'); let entityType = item.attr('data-entity-type');
let entityId = item.attr('data-entity-id'); let entityId = item.attr('data-entity-id');
let isSelected = !item.hasClass('selected'); let isSelected = !item.hasClass('selected') || doubleClick;
element.find('.selected').removeClass('selected').removeClass('primary-background'); element.find('.selected').removeClass('selected').removeClass('primary-background');
if (isSelected) item.addClass('selected').addClass('primary-background'); if (isSelected) item.addClass('selected').addClass('primary-background');
let newVal = isSelected ? `${entityType}:${entityId}` : ''; let newVal = isSelected ? `${entityType}:${entityId}` : '';
input.val(newVal); input.val(newVal);
if (!isSelected) {
events.emit('entity-select-change', null);
}
if (!doubleClick && !isSelected) return;
let link = item.find('.entity-list-item-link').attr('href');
let name = item.find('.entity-list-item-name').text();
if (doubleClick) {
events.emit('entity-select-confirm', {
id: Number(entityId),
name: name,
link: link
});
}
if (isSelected) {
events.emit('entity-select-change', {
id: Number(entityId),
name: name,
link: link
});
}
} }
// Get search url with correct types // Get search url with correct types

View File

@ -18,7 +18,7 @@ window.baseUrl = function(path) {
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']); var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Global Event System // Global Event System
class Events { class EventManager {
constructor() { constructor() {
this.listeners = {}; this.listeners = {};
} }
@ -39,12 +39,12 @@ class Events {
return this; return this;
} }
}; };
window.Events = new Events(); window.Events = new EventManager();
var services = require('./services')(ngApp, Events); var services = require('./services')(ngApp, window.Events);
var directives = require('./directives')(ngApp, Events); var directives = require('./directives')(ngApp, window.Events);
var controllers = require('./controllers')(ngApp, Events); var controllers = require('./controllers')(ngApp, window.Events);
//Global jQuery Config & Extensions //Global jQuery Config & Extensions
@ -130,6 +130,27 @@ $(function () {
$('.entity-list.compact').find('p').not('.empty-text').slideToggle(240); $('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
}); });
// Popup close
$('.popup-close').click(function() {
$(this).closest('.overlay').fadeOut(240);
});
$('.overlay').click(function(event) {
if (!$(event.target).hasClass('overlay')) return;
$(this).fadeOut(240);
});
// Prevent markdown display link click redirect
$('.markdown-display').on('click', 'a', function(event) {
event.preventDefault();
window.open($(this).attr('href'));
});
// Detect IE for css
if(navigator.userAgent.indexOf('MSIE')!==-1
|| navigator.appVersion.indexOf('Trident/') > 0
|| navigator.userAgent.indexOf('Safari') !== -1){
$('body').addClass('flexbox-support');
}
}); });

View File

@ -95,7 +95,21 @@ var mceOptions = module.exports = {
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'}, alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
}, },
file_browser_callback: function (field_name, url, type, win) { file_browser_callback: function (field_name, url, type, win) {
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) { 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; win.document.getElementById(field_name).value = image.url;
if ("createEvent" in document) { if ("createEvent" in document) {
var evt = document.createEvent("HTMLEvents"); var evt = document.createEvent("HTMLEvents");
@ -104,11 +118,15 @@ var mceOptions = module.exports = {
} else { } else {
win.document.getElementById(field_name).fireEvent("onchange"); win.document.getElementById(field_name).fireEvent("onchange");
} }
// Replace the actively selected content with the linked image
var html = '<a href="' + image.url + '" target="_blank">'; var html = '<a href="' + image.url + '" target="_blank">';
html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">'; html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">';
html += '</a>'; html += '</a>';
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html); win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
}); });
}
}, },
paste_preprocess: function (plugin, args) { paste_preprocess: function (plugin, args) {
var content = args.content; var content = args.content;
@ -119,6 +137,8 @@ var mceOptions = module.exports = {
extraSetups: [], extraSetups: [],
setup: function (editor) { setup: function (editor) {
// Run additional setup actions
// Used by the angular side of things
for (var i = 0; i < mceOptions.extraSetups.length; i++) { for (var i = 0; i < mceOptions.extraSetups.length; i++) {
mceOptions.extraSetups[i](editor); mceOptions.extraSetups[i](editor);
} }

View File

@ -100,3 +100,13 @@ $button-border-radius: 2px;
} }
} }
.button[disabled] {
background-color: #BBB;
cursor: default;
&:hover {
background-color: #BBB;
cursor: default;
box-shadow: none;
}
}

View File

@ -1,5 +1,5 @@
.overlay { .overlay {
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(0, 0, 0, 0.333);
position: fixed; position: fixed;
z-index: 95536; z-index: 95536;
width: 100%; width: 100%;
@ -10,26 +10,76 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
display: flex;
align-items: center;
justify-content: center;
display: none;
} }
.image-manager-body { .popup-body-wrap {
display: flex;
}
.popup-body {
background-color: #FFF; background-color: #FFF;
max-height: 90%; max-height: 90%;
width: 90%; width: 1200px;
height: 90%; height: auto;
margin: 2% 5%; margin: 2% 5%;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3); box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);
overflow: hidden; overflow: hidden;
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 999; z-index: 999;
display: flex; display: flex;
h1, h2, h3 { flex-direction: column;
font-weight: 300; &.small {
margin: 2% auto;
width: 800px;
max-width: 90%;
} }
&:before {
display: flex;
align-self: flex-start;
}
}
//body.ie .popup-body {
// min-height: 100%;
//}
.corner-button {
position: absolute;
top: 0;
right: 0;
margin: 0;
height: 40px;
border-radius: 0;
box-shadow: none;
}
.popup-header, .popup-footer {
display: block !important;
position: relative;
height: 40px;
flex: none !important;
.popup-title {
color: #FFF;
padding: 8px $-m;
}
}
body.flexbox-support #entity-selector-wrap .popup-body .form-group {
height: 444px;
min-height: 444px;
}
#entity-selector-wrap .popup-body .form-group {
margin: 0;
}
//body.ie #entity-selector-wrap .popup-body .form-group {
// min-height: 60vh;
//}
.image-manager-body {
min-height: 70vh;
} }
#image-manager .dropzone-container { #image-manager .dropzone-container {
@ -37,12 +87,6 @@
border: 3px dashed #DDD; border: 3px dashed #DDD;
} }
.image-manager-bottom {
position: absolute;
bottom: 0;
right: 0;
}
.image-manager-list .image { .image-manager-list .image {
display: block; display: block;
position: relative; position: relative;
@ -103,18 +147,13 @@
.image-manager-sidebar { .image-manager-sidebar {
width: 300px; width: 300px;
height: 100%;
margin-left: 1px; margin-left: 1px;
padding: 0 $-l; padding: $-m $-l;
overflow-y: auto;
border-left: 1px solid #DDD; border-left: 1px solid #DDD;
.dropzone-container {
margin-top: $-m;
} }
.image-manager-close {
position: absolute;
top: 0;
right: 0;
margin: 0;
border-radius: 0;
} }
.image-manager-list { .image-manager-list {
@ -125,7 +164,6 @@
.image-manager-content { .image-manager-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
flex: 1; flex: 1;
.container { .container {
width: 100%; width: 100%;
@ -141,12 +179,13 @@
* Copyright (c) 2012 Matias Meno <m@tias.me> * Copyright (c) 2012 Matias Meno <m@tias.me>
*/ */
.dz-message { .dz-message {
font-size: 1.4em; font-size: 1.2em;
line-height: 1.1;
font-style: italic; font-style: italic;
color: #aaa; color: #aaa;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
padding: $-xl $-m; padding: $-l $-m;
transition: all ease-in-out 120ms; transition: all ease-in-out 120ms;
} }

View File

@ -25,6 +25,14 @@ body.flexbox {
} }
} }
.flex-child > div {
flex: 1;
}
//body.ie .flex-child > div {
// flex: 1 0 0px;
//}
/** Rules for all columns */ /** Rules for all columns */
div[class^="col-"] img { div[class^="col-"] img {
max-width: 100%; max-width: 100%;

View File

@ -12,7 +12,7 @@
@import "animations"; @import "animations";
@import "tinymce"; @import "tinymce";
@import "highlightjs"; @import "highlightjs";
@import "image-manager"; @import "components";
@import "header"; @import "header";
@import "lists"; @import "lists";
@import "pages"; @import "pages";

View File

@ -1,5 +1,5 @@
<div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}"> <div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}">
<h3 class="text-book"><a class="text-book" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></h3> <h3 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i><span class="entity-list-item-name">{{$book->name}}</span></a></h3>
@if(isset($book->searchSnippet)) @if(isset($book->searchSnippet))
<p class="text-muted">{!! $book->searchSnippet !!}</p> <p class="text-muted">{!! $book->searchSnippet !!}</p>
@else @else

View File

@ -6,8 +6,8 @@
</a> </a>
<span class="text-muted">&nbsp;&nbsp;&raquo;&nbsp;&nbsp;</span> <span class="text-muted">&nbsp;&nbsp;&raquo;&nbsp;&nbsp;</span>
@endif @endif
<a href="{{ $chapter->getUrl() }}" class="text-chapter"> <a href="{{ $chapter->getUrl() }}" class="text-chapter entity-list-item-link">
<i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->name }} <i class="zmdi zmdi-collection-bookmark"></i><span class="entity-list-item-name">{{ $chapter->name }}</span>
</a> </a>
</h3> </h3>
@if(isset($chapter->searchSnippet)) @if(isset($chapter->searchSnippet))

View File

@ -19,6 +19,14 @@
</div> </div>
@include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id]) @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
@include('partials/entity-selector-popup')
<script>
(function() {
})();
</script>
@stop @stop

View File

@ -74,6 +74,8 @@
<span class="float left">Editor</span> <span class="float left">Editor</span>
<div class="float right buttons"> <div class="float right buttons">
<button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button> <button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button>
&nbsp;|&nbsp;
<button class="text-button" type="button" data-action="insertEntityLink"><i class="zmdi zmdi-link"></i>Insert Entity Link</button>
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
<div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}"> <div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}">
<h3> <h3>
<a href="{{ $page->getUrl() }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ $page->name }}</a> <a href="{{ $page->getUrl() }}" class="text-page entity-list-item-link"><i class="zmdi zmdi-file-text"></i><span class="entity-list-item-name">{{ $page->name }}</span></a>
</h3> </h3>
@if(isset($page->searchSnippet)) @if(isset($page->searchSnippet))

View File

@ -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>

View File

@ -1,6 +1,13 @@
<div id="image-manager" image-type="{{ $imageType }}" ng-controller="ImageManagerController" uploaded-to="{{ $uploaded_to or 0 }}"> <div id="image-manager" image-type="{{ $imageType }}" ng-controller="ImageManagerController" uploaded-to="{{ $uploaded_to or 0 }}">
<div class="overlay anim-slide" ng-show="showing" ng-cloak ng-click="hide()"> <div class="overlay" ng-cloak ng-click="hide()">
<div class="image-manager-body" ng-click="$event.stopPropagation()"> <div class="popup-body" ng-click="$event.stopPropagation()">
<div class="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 class="image-manager-content">
<div ng-if="imageType === 'gallery'" class="container"> <div ng-if="imageType === 'gallery'" class="container">
@ -24,7 +31,7 @@
<img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}"> <img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
<div class="image-meta"> <div class="image-meta">
<span class="name" ng-bind="image.name"></span> <span class="name" ng-bind="image.name"></span>
<span class="date">Uploaded @{{ getDate(image.created_at) | date:'mediumDate' }}</span> <span class="date">Uploaded @{{ getDate(image.created_at) }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -32,14 +39,10 @@
</div> </div>
</div> </div>
<button class="neg button image-manager-close" ng-click="hide()">x</button>
<div class="image-manager-sidebar"> <div class="image-manager-sidebar">
<h2>Images</h2> <div class="inner">
<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"> <div class="image-manager-details anim fadeIn" ng-show="selectedImage">
<form ng-submit="saveImageDetails($event)"> <form ng-submit="saveImageDetails($event)">
<div> <div>
@ -53,8 +56,6 @@
</div> </div>
</form> </form>
<hr class="even">
<div ng-show="dependantPages"> <div ng-show="dependantPages">
<p class="text-neg text-small"> <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 is used in the pages below, Click delete again to confirm you want to delete
@ -67,18 +68,27 @@
</ul> </ul>
</div> </div>
<form ng-submit="deleteImage($event)"> <div class="clearfix">
<button class="button neg"><i class="zmdi zmdi-delete"></i>Delete Image</button> <form class="float left" ng-submit="deleteImage($event)">
<button class="button icon neg"><i class="zmdi zmdi-delete"></i></button>
</form> </form>
</div> <button class="button pos anim fadeIn float right" ng-show="selectedImage" ng-click="selectButtonClick()">
<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 <i class="zmdi zmdi-square-right"></i>Select Image
</button> </button>
</div> </div>
</div> </div>
<drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>