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) {
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/');

View File

@ -271,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(() => {
@ -304,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;
@ -355,6 +354,13 @@ module.exports = function (ngApp, events) {
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);
});
@ -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
function editorPaste(e) {
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) {
return {
@ -690,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

View File

@ -18,7 +18,7 @@ window.baseUrl = function(path) {
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Global Event System
class Events {
class EventManager {
constructor() {
this.listeners = {};
}
@ -39,12 +39,12 @@ class Events {
return this;
}
};
window.Events = new 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
@ -130,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');
}
});

View File

@ -95,20 +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;
@ -119,6 +137,8 @@ var mceOptions = module.exports = {
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);
}

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 {
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;
}

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 */
div[class^="col-"] img {
max-width: 100%;

View File

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

View File

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

View File

@ -6,8 +6,8 @@
</a>
<span class="text-muted">&nbsp;&nbsp;&raquo;&nbsp;&nbsp;</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))

View File

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

View File

@ -74,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>
&nbsp;|&nbsp;
<button class="text-button" type="button" data-action="insertEntityLink"><i class="zmdi zmdi-link"></i>Insert Entity Link</button>
</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}}">
<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))

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