Created solution for JS translations

Also tidied up existing components and JS
This commit is contained in:
Dan Brown 2016-12-31 14:27:40 +00:00
parent 05316c90ba
commit c9700e38e2
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
26 changed files with 371 additions and 267 deletions

View file

@ -117,7 +117,7 @@ class AttachmentController extends Controller
}
$attachment = $this->attachmentService->updateFile($attachment, $request->all());
return $this->jsonSuccess($attachment, trans('entities.attachments_updated_success'));
return response()->json($attachment);
}
/**

View file

@ -117,17 +117,6 @@ abstract class Controller extends BaseController
return true;
}
/**
* Send a json respons with a message attached as a header.
* @param $data
* @param string $successMessage
* @return $this
*/
protected function jsonSuccess($data, $successMessage = "")
{
return response()->json($data)->header('message-success', $successMessage);
}
/**
* Send back a json error message.
* @param string $messageText

View file

@ -43,4 +43,39 @@ class HomeController extends Controller
]);
}
/**
* Get a js representation of the current translations
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function getTranslations() {
$locale = trans()->getLocale();
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
$resp = cache($cacheKey);
} else {
$translations = [
// Get only translations which might be used in JS
'common' => trans('common'),
'components' => trans('components'),
'entities' => trans('entities'),
'errors' => trans('errors')
];
if ($locale !== 'en') {
$enTrans = [
'common' => trans('common', [], null, 'en'),
'components' => trans('components', [], null, 'en'),
'entities' => trans('entities', [], null, 'en'),
'errors' => trans('errors', [], null, 'en')
];
$translations = array_replace_recursive($enTrans, $translations);
}
$resp = 'window.translations = ' . json_encode($translations);
cache()->put($cacheKey, $resp, 120);
}
return response($resp, 200, [
'Content-Type' => 'application/javascript'
]);
}
}

View file

@ -64,7 +64,7 @@ export default function (ngApp, events) {
$scope.$apply(() => {
$scope.images.unshift(data);
});
events.emit('success', 'Image uploaded');
events.emit('success', trans('components.image_upload_success'));
};
/**
@ -151,10 +151,9 @@ export default function (ngApp, events) {
if ($scope.searching) components['term'] = $scope.searchTerm;
let urlQueryString = Object.keys(components).map((key) => {
url += Object.keys(components).map((key) => {
return key + '=' + encodeURIComponent(components[key]);
}).join('&');
url += urlQueryString;
$http.get(url).then((response) => {
$scope.images = $scope.images.concat(response.data.images);
@ -209,7 +208,7 @@ export default function (ngApp, events) {
event.preventDefault();
let url = window.baseUrl('/images/update/' + $scope.selectedImage.id);
$http.put(url, this.selectedImage).then(response => {
events.emit('success', 'Image details updated');
events.emit('success', trans('components.image_update_success'));
}, (response) => {
if (response.status === 422) {
let errors = response.data;
@ -238,7 +237,7 @@ export default function (ngApp, events) {
$http.delete(url).then((response) => {
$scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
$scope.selectedImage = false;
events.emit('success', 'Image successfully deleted');
events.emit('success', trans('components.image_delete_success'));
}, (response) => {
// Pages failure
if (response.status === 400) {
@ -309,9 +308,9 @@ export default function (ngApp, events) {
// Set initial header draft text
if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
$scope.draftText = 'Editing Draft'
$scope.draftText = trans('entities.pages_editing_draft');
} else {
$scope.draftText = 'Editing Page'
$scope.draftText = trans('entities.pages_editing_page');
}
let autoSave = false;
@ -388,7 +387,7 @@ export default function (ngApp, events) {
lastSave = Date.now();
}, errorRes => {
if (draftErroring) return;
events.emit('error', 'Failed to save draft. Ensure you have internet connection before saving this page.')
events.emit('error', trans('errors.page_draft_autosave_fail'));
draftErroring = true;
});
}
@ -421,7 +420,7 @@ export default function (ngApp, events) {
let url = window.baseUrl('/ajax/page/' + pageId);
$http.get(url).then((responseData) => {
if (autoSave) $interval.cancel(autoSave);
$scope.draftText = 'Editing Page';
$scope.draftText = trans('entities.pages_editing_page');
$scope.isUpdateDraft = false;
$scope.$broadcast('html-update', responseData.data.html);
$scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html);
@ -429,7 +428,7 @@ export default function (ngApp, events) {
$timeout(() => {
startAutoSave();
}, 1000);
events.emit('success', 'Draft discarded, The editor has been updated with the current page content');
events.emit('success', trans('entities.pages_draft_discarded'));
});
};
@ -594,7 +593,7 @@ export default function (ngApp, events) {
$scope.$apply(() => {
$scope.files.push(data);
});
events.emit('success', 'File uploaded');
events.emit('success', trans('entities.attachments_file_uploaded'));
};
/**
@ -612,7 +611,7 @@ export default function (ngApp, events) {
data.link = '';
}
});
events.emit('success', 'File updated');
events.emit('success', trans('entities.attachments_file_updated'));
};
/**
@ -638,7 +637,7 @@ export default function (ngApp, events) {
file.uploaded_to = pageId;
$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
$scope.files.push(resp.data);
events.emit('success', 'Link attached');
events.emit('success', trans('entities.attachments_link_attached'));
$scope.file = getCleanFile();
}, checkError('link'));
};
@ -672,7 +671,7 @@ export default function (ngApp, events) {
$scope.editFile.link = '';
}
$scope.editFile = false;
events.emit('success', resp.headers('message-success'));
events.emit('success', trans('entities.attachments_updated_success'));
}, checkError('edit'));
};

View file

@ -102,7 +102,7 @@ export default function (ngApp, events) {
$(file.previewElement).find('[data-dz-errormessage]').text(message);
}
if (xhr.status === 413) setMessage('The server does not allow uploads of this size. Please try a smaller file.');
if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
if (errorMessage.file) setMessage(errorMessage.file[0]);
});
@ -261,15 +261,21 @@ export default function (ngApp, events) {
link: function (scope, element, attrs) {
// Elements
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"]')
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"]');
// Prevent markdown display link click redirect
$display.on('click', 'a', function(event) {
event.preventDefault();
window.open(this.getAttribute('href'));
});
let currentCaretPos = 0;
input.blur(event => {
currentCaretPos = input[0].selectionStart;
$input.blur(event => {
currentCaretPos = $input[0].selectionStart;
});
// Scroll sync
@ -279,10 +285,10 @@ export default function (ngApp, events) {
displayHeight;
function setScrollHeights() {
inputScrollHeight = input[0].scrollHeight;
inputHeight = input.height();
displayScrollHeight = display[0].scrollHeight;
displayHeight = display.height();
inputScrollHeight = $input[0].scrollHeight;
inputHeight = $input.height();
displayScrollHeight = $display[0].scrollHeight;
displayHeight = $display.height();
}
setTimeout(() => {
@ -291,29 +297,29 @@ export default function (ngApp, events) {
window.addEventListener('resize', setScrollHeights);
let scrollDebounceTime = 800;
let lastScroll = 0;
input.on('scroll', event => {
$input.on('scroll', event => {
let now = Date.now();
if (now - lastScroll > scrollDebounceTime) {
setScrollHeights()
}
let scrollPercent = (input.scrollTop() / (inputScrollHeight - inputHeight));
let scrollPercent = ($input.scrollTop() / (inputScrollHeight - inputHeight));
let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent;
display.scrollTop(displayScrollY);
$display.scrollTop(displayScrollY);
lastScroll = now;
});
// Editor key-presses
input.keydown(event => {
$input.keydown(event => {
// Insert image shortcut
if (event.which === 73 && event.ctrlKey && event.shiftKey) {
event.preventDefault();
let caretPos = input[0].selectionStart;
let currentContent = input.val();
let caretPos = $input[0].selectionStart;
let currentContent = $input.val();
const mdImageText = "![](http://)";
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
input.focus();
input[0].selectionStart = caretPos + ("![](".length);
input[0].selectionEnd = caretPos + ('![](http://'.length);
$input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
$input.focus();
$input[0].selectionStart = caretPos + ("![](".length);
$input[0].selectionEnd = caretPos + ('![](http://'.length);
return;
}
@ -328,35 +334,35 @@ export default function (ngApp, events) {
});
// Insert image from image manager
insertImage.click(event => {
$insertImage.click(event => {
window.ImageManager.showExternal(image => {
let caretPos = currentCaretPos;
let currentContent = input.val();
let currentContent = $input.val();
let mdImageText = "![" + image.name + "](" + image.thumbs.display + ")";
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
input.change();
$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 selectionEnd = $input[0].selectionEnd;
let textSelected = (selectionEnd !== selectionStart);
let currentContent = input.val();
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));
$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.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionStart))
}
input.change();
$input.change();
});
}
insertEntityLink.click(showLinkSelector);
$insertEntityLink.click(showLinkSelector);
// Upload and insert image on paste
function editorPaste(e) {
@ -369,7 +375,7 @@ export default function (ngApp, events) {
}
}
input.on('paste', editorPaste);
$input.on('paste', editorPaste);
// Handle image drop, Uploads images to BookStack.
function handleImageDrop(event) {
@ -381,7 +387,7 @@ export default function (ngApp, events) {
}
}
input.on('drop', handleImageDrop);
$input.on('drop', handleImageDrop);
// Handle image upload and add image into markdown content
function uploadImage(file) {
@ -399,17 +405,17 @@ export default function (ngApp, events) {
// 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 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[0].value = content.substring(0, selectStart) + innerContent + content.substring(selectEnd);
input.focus();
input[0].selectionStart = selectStart;
input[0].selectionEnd = selectStart;
$input.focus();
$input[0].selectionStart = selectStart;
$input[0].selectionEnd = selectStart;
let remoteFilename = "image-" + Date.now() + "." + ext;
formData.append('file', file, remoteFilename);
@ -417,20 +423,20 @@ export default function (ngApp, events) {
xhr.open('POST', window.baseUrl('/images/gallery/upload'));
xhr.onload = function () {
let selectStart = input[0].selectionStart;
let selectStart = $input[0].selectionStart;
if (xhr.status === 200 || xhr.status === 201) {
let result = JSON.parse(xhr.responseText);
input[0].value = input[0].value.replace(placeholderImage, result.thumbs.display);
input.change();
$input[0].value = $input[0].value.replace(placeholderImage, result.thumbs.display);
$input.change();
} else {
console.log('An error occurred uploading the image');
console.log(trans('errors.image_upload_error'));
console.log(xhr.responseText);
input[0].value = input[0].value.replace(innerContent, '');
input.change();
$input[0].value = $input[0].value.replace(innerContent, '');
$input.change();
}
input.focus();
input[0].selectionStart = selectStart;
input[0].selectionEnd = selectStart;
$input.focus();
$input[0].selectionStart = selectStart;
$input[0].selectionEnd = selectStart;
};
xhr.send(formData);
}
@ -568,8 +574,7 @@ export default function (ngApp, events) {
}
// Enter or tab key
else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
let text = suggestionElems[active].textContent;
currentInput[0].value = text;
currentInput[0].value = suggestionElems[active].textContent;
currentInput.focus();
$suggestionBox.hide();
isShowing = false;
@ -665,7 +670,7 @@ export default function (ngApp, events) {
ngApp.directive('entityLinkSelector', [function($http) {
return {
restict: 'A',
restrict: 'A',
link: function(scope, element, attrs) {
const selectButton = element.find('.entity-link-selector-confirm');

View file

@ -17,6 +17,12 @@ window.baseUrl = function(path) {
let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Translation setup
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
import Translations from "./translations"
let translator = new Translations(window.translations);
window.trans = translator.get.bind(translator);
// Global Event System
class EventManager {
constructor() {
@ -70,150 +76,83 @@ jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
});
// Global jQuery Elements
$(function () {
let notifications = $('.notification');
let successNotification = notifications.filter('.pos');
let errorNotification = notifications.filter('.neg');
let warningNotification = notifications.filter('.warning');
// Notification Events
window.Events.listen('success', function (text) {
successNotification.hide();
successNotification.find('span').text(text);
setTimeout(() => {
successNotification.show();
}, 1);
});
window.Events.listen('warning', function (text) {
warningNotification.find('span').text(text);
warningNotification.show();
});
window.Events.listen('error', function (text) {
errorNotification.find('span').text(text);
errorNotification.show();
});
// Notification hiding
notifications.click(function () {
$(this).fadeOut(100);
});
// Chapter page list toggles
$('.chapter-toggle').click(function (e) {
e.preventDefault();
$(this).toggleClass('open');
$(this).closest('.chapter').find('.inset-list').slideToggle(180);
});
// Back to top button
$('#back-to-top').click(function() {
$('#header').smoothScrollTo();
});
let scrollTopShowing = false;
let scrollTop = document.getElementById('back-to-top');
let scrollTopBreakpoint = 1200;
window.addEventListener('scroll', function() {
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 && scrollTopPos < scrollTopBreakpoint) {
scrollTop.style.opacity = 0;
scrollTopShowing = false;
setTimeout(() => {
scrollTop.style.display = 'none';
}, 500);
}
});
// Common jQuery actions
$('[data-action="expand-entity-list-details"]').click(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'));
});
// Toggle Switches
let $switches = $('[toggle-switch]');
if ($switches.length > 0) {
$switches.click(event => {
let $switch = $(event.target);
let input = $switch.find('input').first()[0];
let checked = input.value !== 'true';
input.value = checked ? 'true' : 'false';
$switch.toggleClass('active', checked);
});
}
// Image pickers
$('.image-picker').on('click', 'button', event => {
let button = event.target;
let picker = $(button).closest('.image-picker')[0];
let action = button.getAttribute('data-action');
let resize = picker.getAttribute('data-resize-height') && picker.getAttribute('data-resize-width');
let usingIds = picker.getAttribute('data-current-id') !== '';
let resizeCrop = picker.getAttribute('data-resize-crop') !== '';
let imageElem = picker.querySelector('img');
let input = picker.querySelector('input');
function setImage(image) {
if (image === 'none') {
imageElem.src = picker.getAttribute('data-default-image');
imageElem.classList.add('none');
input.value = 'none';
return;
}
imageElem.src = image.url;
input.value = usingIds ? image.id : image.url;
imageElem.classList.remove('none');
}
if (action === 'show-image-manager') {
window.ImageManager.showExternal((image) => {
if (!resize) {
setImage(image);
return;
}
let requestString = '/images/thumb/' + image.id + '/' + picker.getAttribute('data-resize-width') + '/' + picker.getAttribute('data-resize-height') + '/' + (resizeCrop ? 'true' : 'false');
$.get(window.baseUrl(requestString), resp => {
image.url = resp.url;
setImage(image);
});
});
} else if (action === 'reset-image') {
setImage({id: 0, url: picker.getAttribute('data-default-image')});
} else if (action === 'remove-image') {
setImage('none');
}
});
// Detect IE for css
if(navigator.userAgent.indexOf('MSIE')!==-1
|| navigator.appVersion.indexOf('Trident/') > 0
|| navigator.userAgent.indexOf('Safari') !== -1){
$('body').addClass('flexbox-support');
}
let notifications = $('.notification');
let successNotification = notifications.filter('.pos');
let errorNotification = notifications.filter('.neg');
let warningNotification = notifications.filter('.warning');
// Notification Events
window.Events.listen('success', function (text) {
successNotification.hide();
successNotification.find('span').text(text);
setTimeout(() => {
successNotification.show();
}, 1);
});
window.Events.listen('warning', function (text) {
warningNotification.find('span').text(text);
warningNotification.show();
});
window.Events.listen('error', function (text) {
errorNotification.find('span').text(text);
errorNotification.show();
});
// Notification hiding
notifications.click(function () {
$(this).fadeOut(100);
});
// Chapter page list toggles
$('.chapter-toggle').click(function (e) {
e.preventDefault();
$(this).toggleClass('open');
$(this).closest('.chapter').find('.inset-list').slideToggle(180);
});
// Back to top button
$('#back-to-top').click(function() {
$('#header').smoothScrollTo();
});
let scrollTopShowing = false;
let scrollTop = document.getElementById('back-to-top');
let scrollTopBreakpoint = 1200;
window.addEventListener('scroll', function() {
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 && scrollTopPos < scrollTopBreakpoint) {
scrollTop.style.opacity = 0;
scrollTopShowing = false;
setTimeout(() => {
scrollTop.style.display = 'none';
}, 500);
}
});
// Common jQuery actions
$('[data-action="expand-entity-list-details"]').click(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);
});
// Detect IE for css
if(navigator.userAgent.indexOf('MSIE')!==-1
|| navigator.appVersion.indexOf('Trident/') > 0
|| navigator.userAgent.indexOf('Safari') !== -1){
$('body').addClass('flexbox-support');
}
// Page specific items
import "./pages/page-show";

View file

@ -0,0 +1,47 @@
/**
* Translation Manager
* Handles the JavaScript side of translating strings
* in a way which fits with Laravel.
*/
class Translator {
/**
* Create an instance, Passing in the required translations
* @param translations
*/
constructor(translations) {
this.store = translations;
}
/**
* Get a translation, Same format as laravel's 'trans' helper
* @param key
* @param replacements
* @returns {*}
*/
get(key, replacements) {
let splitKey = key.split('.');
let value = splitKey.reduce((a, b) => {
return a != undefined ? a[b] : a;
}, this.store);
if (value === undefined) {
console.log(`Translation with key "${key}" does not exist`);
value = key;
}
if (replacements === undefined) return value;
let replaceMatches = value.match(/:([\S]+)/g);
if (replaceMatches === null) return value;
replaceMatches.forEach(match => {
let key = match.substring(1);
if (typeof replacements[key] === 'undefined') return;
value = value.replace(match, replacements[key]);
});
return value;
}
}
export default Translator

View file

@ -35,6 +35,12 @@ table.table {
tr:hover {
background-color: #EEE;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
}
table.no-style {

View file

@ -16,7 +16,7 @@ return [
'app_name_desc' => 'Dieser Name wird im Header und E-Mails angezeigt.',
'app_name_header' => 'Anwendungsname im Header anzeigen?',
'app_public_viewing' => '&Ouml;ffentliche Ansicht erlauben?',
'app_secure_images' => 'Erh&oml;hte Sicherheit f&uuml;r Bilduploads aktivieren?',
'app_secure_images' => 'Erh&ouml;hte Sicherheit f&uuml;r Bilduploads aktivieren?',
'app_secure_images_desc' => 'Aus Leistungsgr&uuml;nden sind alle Bilder &ouml;ffentlich sichtbar. Diese Option f&uuml;gt zuf&auml;llige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugrif zu verhindern.',
'app_editor' => 'Seiteneditor',
'app_editor_desc' => 'W&auml;hlen sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.',

View file

@ -4,18 +4,21 @@ return [
/**
* Image Manager
*/
'imagem_select' => 'Image Select',
'imagem_all' => 'All',
'imagem_all_title' => 'View all images',
'imagem_book_title' => 'View images uploaded to this book',
'imagem_page_title' => 'View images uploaded to this page',
'imagem_search_hint' => 'Search by image name',
'imagem_uploaded' => 'Uploaded :uploadedDate',
'imagem_load_more' => 'Load More',
'imagem_image_name' => 'Image Name',
'imagem_delete_confirm' => 'This image is used in the pages below, Click delete again to confirm you want to delete this image.',
'imagem_select_image' => 'Select Image',
'imagem_dropzone' => 'Drop images or click here to upload',
'image_select' => 'Image Select',
'image_all' => 'All',
'image_all_title' => 'View all images',
'image_book_title' => 'View images uploaded to this book',
'image_page_title' => 'View images uploaded to this page',
'image_search_hint' => 'Search by image name',
'image_uploaded' => 'Uploaded :uploadedDate',
'image_load_more' => 'Load More',
'image_image_name' => 'Image Name',
'image_delete_confirm' => 'This image is used in the pages below, Click delete again to confirm you want to delete this image.',
'image_select_image' => 'Select Image',
'image_dropzone' => 'Drop images or click here to upload',
'images_deleted' => 'Images Deleted',
'image_preview' => 'Image Preview',
'image_upload_success' => 'Image uploaded successfully',
'image_update_success' => 'Image details successfully updated',
'image_delete_success' => 'Image successfully deleted'
];

View file

@ -129,6 +129,8 @@ return [
'pages_edit_toggle_header' => 'Toggle header',
'pages_edit_save_draft' => 'Save Draft',
'pages_edit_draft' => 'Edit Page Draft',
'pages_editing_draft' => 'Editing Draft',
'pages_editing_page' => 'Editing Page',
'pages_edit_draft_save_at' => 'Draft saved at ',
'pages_edit_delete_draft' => 'Delete Draft',
'pages_edit_discard_draft' => 'Discard Draft',
@ -175,6 +177,7 @@ return [
'time_b' => 'in the last :minCount minutes',
'message' => ':start :time. Take care not to overwrite each other\'s updates!',
],
'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
/**
* Editor sidebar
@ -207,6 +210,9 @@ return [
'attachments_order_updated' => 'Attachment order updated',
'attachments_updated_success' => 'Attachment details updated',
'attachments_deleted' => 'Attachment deleted',
'attachments_file_uploaded' => 'File successfully uploaded',
'attachments_file_updated' => 'File successfully updated',
'attachments_link_attached' => 'Link successfully attached to page',
/**
* Profile View

View file

@ -33,10 +33,15 @@ return [
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
'cannot_get_image_from_url' => 'Cannot get image from :url',
'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',
'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',
'image_upload_error' => 'An error occurred uploading the image',
// Attachments
'attachment_page_mismatch' => 'Page mismatch during attachment update',
// Pages
'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
// Entities
'entity_not_found' => 'Entity not found',
'book_not_found' => 'Book not found',

View file

@ -17,6 +17,7 @@
<!-- Scripts -->
<script src="{{ baseUrl('/libs/jquery/jquery.min.js?version=2.1.4') }}"></script>
<script src="{{ baseUrl('/libs/jquery/jquery-ui.min.js?version=1.11.4') }}"></script>
<script src="{{ baseUrl('/translations.js') }}"></script>
@yield('head')

View file

@ -19,7 +19,7 @@
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
@include('partials/entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book'])
@include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book'])
<a href="{{ $chapter->getUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
<button type="submit" class="button pos">{{ trans('entities.chapters_move') }}</button>

View file

@ -5,7 +5,7 @@
<div class="popup-title">{{ trans('entities.entity_select') }}</div>
<button type="button" class="corner-button neg button popup-close">x</button>
</div>
@include('partials/entity-selector', ['name' => 'entity-selector'])
@include('components.entity-selector', ['name' => 'entity-selector'])
<div class="popup-footer">
<button type="button" disabled="true" class="button entity-link-selector-confirm pos corner-button">{{ trans('common.select') }}</button>
</div>

View file

@ -2,7 +2,7 @@
<div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}">
<input type="hidden" entity-selector-input name="{{$name}}" value="">
<input type="text" placeholder="{{ trans('common.search') }}" ng-model="search" ng-model-options="{debounce: 200}" ng-change="searchEntities()">
<div class="text-center loading" ng-show="loading">@include('partials/loading-icon')</div>
<div class="text-center loading" ng-show="loading">@include('partials.loading-icon')</div>
<div ng-show="!loading" ng-bind-html="entityResults"></div>
</div>
</div>

View file

@ -3,7 +3,7 @@
<div class="popup-body" ng-click="$event.stopPropagation()">
<div class="popup-header primary-background">
<div class="popup-title">{{ trans('components.imagem_select') }}</div>
<div class="popup-title">{{ trans('components.image_select') }}</div>
<button class="popup-close neg corner-button button">x</button>
</div>
@ -12,14 +12,14 @@
<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="{{ trans('components.imagem_all_title') }}" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> {{ trans('components.imagem_all') }}</div>
<div class="col-xs-4 tab-item" title="{{ trans('components.imagem_book_title') }}" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> {{ trans('entities.book') }}</div>
<div class="col-xs-4 tab-item" title="{{ trans('components.imagem_page_title') }}" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> {{ trans('entities.page') }}</div>
<div class="col-xs-4 tab-item" title="{{ trans('components.image_all_title') }}" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> {{ trans('components.image_all') }}</div>
<div class="col-xs-4 tab-item" title="{{ trans('components.image_book_title') }}" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> {{ trans('entities.book') }}</div>
<div class="col-xs-4 tab-item" title="{{ trans('components.image_page_title') }}" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> {{ trans('entities.page') }}</div>
</div>
</div>
<div ng-show="view === 'all'" >
<form ng-submit="searchImages()" class="contained-search-box">
<input type="text" placeholder="{{ trans('components.imagem_search_hint') }}" ng-model="searchTerm">
<input type="text" placeholder="{{ trans('components.image_search_hint') }}" ng-model="searchTerm">
<button ng-class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button>
<button title="{{ trans('common.search') }}" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
</form>
@ -31,11 +31,11 @@
<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">{{ trans('components.imagem_uploaded', ['uploadedDate' => "{{ getDate(image.created_at) }" . "}"]) }}</span>
<span class="date">{{ trans('components.image_uploaded', ['uploadedDate' => "{{ getDate(image.created_at) }" . "}"]) }}</span>
</div>
</div>
</div>
<div class="load-more" ng-show="hasMore" ng-click="fetchData()">{{ trans('components.imagem_load_more') }}</div>
<div class="load-more" ng-show="hasMore" ng-click="fetchData()">{{ trans('components.image_load_more') }}</div>
</div>
</div>
@ -51,14 +51,14 @@
</a>
</div>
<div class="form-group">
<label for="name">{{ trans('components.imagem_image_name') }}</label>
<label for="name">{{ trans('components.image_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">
{{ trans('components.imagem_delete_confirm') }}
{{ trans('components.image_delete_confirm') }}
</p>
<ul class="text-neg">
<li ng-repeat="page in dependantPages">
@ -72,13 +72,13 @@
<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>{{ trans('components.imagem_select_image') }}
<i class="zmdi zmdi-square-right"></i>{{ trans('components.image_select_image') }}
</button>
</div>
</div>
<drop-zone message="{{ trans('components.imagem_dropzone') }}" upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
<drop-zone message="{{ trans('components.image_dropzone') }}" upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
</div>

View file

@ -1,10 +1,10 @@
<div class="image-picker" data-default-image="{{ $defaultImage }}" data-resize-height="{{ $resizeHeight }}" data-resize-width="{{ $resizeWidth }}" data-current-id="{{ $currentId or '' }}" data-resize-crop="{{ $resizeCrop or '' }}">
<div class="image-picker" image-picker="{{$name}}" data-default-image="{{ $defaultImage }}" data-resize-height="{{ $resizeHeight }}" data-resize-width="{{ $resizeWidth }}" data-current-id="{{ $currentId or '' }}" data-resize-crop="{{ $resizeCrop or '' }}">
<div>
<img @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
</div>
<button class="button" type="button" data-action="show-image-manager">{{ trans('components.imagem_select_image') }}</button>
<button class="button" type="button" data-action="show-image-manager">{{ trans('components.image_select_image') }}</button>
<br>
<button class="text-button" data-action="reset-image" type="button">{{ trans('common.reset') }}</button>
@ -13,5 +13,54 @@
<button class="text-button neg" data-action="remove-image" type="button">{{ trans('common.remove') }}</button>
@endif
<input type="hidden" name="{{$name}}" id="{{$name}}" value="{{$currentId or $currentImage or ''}}">
</div>
<input type="hidden" name="{{$name}}" id="{{$name}}" value="{{ isset($currentId) && ($currentId !== '' && $currentId !== false) ? $currentId : $currentImage}}">
</div>
<script>
(function(){
var picker = document.querySelector('[image-picker="{{$name}}"]');
picker.addEventListener('click', function(event) {
if (event.target.nodeName.toLowerCase() !== 'button') return;
var button = event.target;
var action = button.getAttribute('data-action');
var resize = picker.getAttribute('data-resize-height') && picker.getAttribute('data-resize-width');
var usingIds = picker.getAttribute('data-current-id') !== '';
var resizeCrop = picker.getAttribute('data-resize-crop') !== '';
var imageElem = picker.querySelector('img');
var input = picker.querySelector('input');
function setImage(image) {
if (image === 'none') {
imageElem.src = picker.getAttribute('data-default-image');
imageElem.classList.add('none');
input.value = 'none';
return;
}
imageElem.src = image.url;
input.value = usingIds ? image.id : image.url;
imageElem.classList.remove('none');
}
if (action === 'show-image-manager') {
window.ImageManager.showExternal((image) => {
if (!resize) {
setImage(image);
return;
}
var requestString = '/images/thumb/' + image.id + '/' + picker.getAttribute('data-resize-width') + '/' + picker.getAttribute('data-resize-height') + '/' + (resizeCrop ? 'true' : 'false');
$.get(window.baseUrl(requestString), resp => {
image.url = resp.url;
setImage(image);
});
});
} else if (action === 'reset-image') {
setImage({id: 0, url: picker.getAttribute('data-default-image')});
} else if (action === 'remove-image') {
setImage('none');
}
});
})();
</script>

View file

@ -1,4 +1,15 @@
<div toggle-switch class="toggle-switch @if($value) active @endif">
<div toggle-switch="{{$name}}" class="toggle-switch @if($value) active @endif">
<input type="hidden" name="{{$name}}" value="{{$value?'true':'false'}}"/>
<div class="switch-handle"></div>
</div>
</div>
<script>
(function() {
var toggle = document.querySelector('[toggle-switch="{{$name}}"]');
var toggleInput = toggle.querySelector('input');
toggle.onclick = function(event) {
var checked = toggleInput.value !== 'true';
toggleInput.value = checked ? 'true' : 'false';
checked ? toggle.classList.add('active') : toggle.classList.remove('active');
};
})()
</script>

View file

@ -20,7 +20,7 @@
</div>
@include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
@include('partials/entity-selector-popup')
@include('components.image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
@include('components.entity-selector-popup')
@stop

View file

@ -19,7 +19,7 @@
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
@include('partials/entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter'])
@include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter'])
<a href="{{ $page->getUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
<button type="submit" class="button pos">{{ trans('entities.pages_move') }}</button>

View file

@ -1,4 +1,4 @@
<style>
<style id="custom-styles" data-color="{{ setting('app-color') }}" data-color-light="{{ setting('app-color-light') }}">
header, #back-to-top, .primary-background {
background-color: {{ setting('app-color') }} !important;
}

View file

@ -56,7 +56,8 @@
'defaultImage' => baseUrl('/logo.png'),
'currentImage' => setting('app-logo'),
'name' => 'setting-app-logo',
'imageClass' => 'logo-image'
'imageClass' => 'logo-image',
'currentId' => false
])
</div>
@ -125,7 +126,7 @@
</div>
@include('partials/image-manager', ['imageType' => 'system'])
@include('components.image-manager', ['imageType' => 'system'])
@stop
@ -142,10 +143,16 @@
var isEmpty = $.trim($elm.val()).length === 0;
if (!isEmpty) $elm.val(hexVal);
$('#setting-app-color-light').val(isEmpty ? '' : rgbLightVal);
// Set page elements to provide preview
$('#header, .image-picker .button').attr('style', 'background-color:'+ hexVal+'!important;');
$('.faded-small').css('background-color', rgbLightVal);
$('.setting-nav a.selected').css('border-bottom-color', hexVal + '!important');
var customStyles = document.getElementById('custom-styles');
var oldColor = customStyles.getAttribute('data-color');
var oldColorLight = customStyles.getAttribute('data-color-light');
customStyles.innerHTML = customStyles.innerHTML.split(oldColor).join(hexVal);
customStyles.innerHTML = customStyles.innerHTML.split(oldColorLight).join(rgbLightVal);
customStyles.setAttribute('data-color', hexVal);
customStyles.setAttribute('data-color-light', rgbLightVal);
}
});
</script>

View file

@ -20,13 +20,13 @@
<tr>
<th>{{ trans('settings.role_name') }}</th>
<th></th>
<th class="text-right">{{ trans('settings.users') }}</th>
<th class="text-center">{{ trans('settings.users') }}</th>
</tr>
@foreach($roles as $role)
<tr>
<td><a href="{{ baseUrl("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a></td>
<td>{{ $role->description }}</td>
<td class="text-right">{{ $role->users->count() }}</td>
<td class="text-center">{{ $role->users->count() }}</td>
</tr>
@endforeach
</table>

View file

@ -85,5 +85,5 @@
</div>
<p class="margin-top large"><br></p>
@include('partials/image-manager', ['imageType' => 'user'])
@include('components.image-manager', ['imageType' => 'user'])
@stop

View file

@ -1,5 +1,7 @@
<?php
Route::get('/translations.js', 'HomeController@getTranslations');
// Authenticated routes...
Route::group(['middleware' => 'auth'], function () {