mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-03 08:13:14 +08:00
Resolved conflicts
This commit is contained in:
53
resources/assets/js/components/back-top-top.js
Normal file
53
resources/assets/js/components/back-top-top.js
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
class BackToTop {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.targetElem = document.getElementById('header');
|
||||
this.showing = false;
|
||||
this.breakPoint = 1200;
|
||||
this.elem.addEventListener('click', this.scrollToTop.bind(this));
|
||||
window.addEventListener('scroll', this.onPageScroll.bind(this));
|
||||
}
|
||||
|
||||
onPageScroll() {
|
||||
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
if (!this.showing && scrollTopPos > this.breakPoint) {
|
||||
this.elem.style.display = 'block';
|
||||
this.showing = true;
|
||||
setTimeout(() => {
|
||||
this.elem.style.opacity = 0.4;
|
||||
}, 1);
|
||||
} else if (this.showing && scrollTopPos < this.breakPoint) {
|
||||
this.elem.style.opacity = 0;
|
||||
this.showing = false;
|
||||
setTimeout(() => {
|
||||
this.elem.style.display = 'none';
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
scrollToTop() {
|
||||
let targetTop = this.targetElem.getBoundingClientRect().top;
|
||||
let scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body;
|
||||
let duration = 300;
|
||||
let start = Date.now();
|
||||
let scrollStart = this.targetElem.getBoundingClientRect().top;
|
||||
|
||||
function setPos() {
|
||||
let percentComplete = (1-((Date.now() - start) / duration));
|
||||
let target = Math.abs(percentComplete * scrollStart);
|
||||
if (percentComplete > 0) {
|
||||
scrollElem.scrollTop = target;
|
||||
requestAnimationFrame(setPos.bind(this));
|
||||
} else {
|
||||
scrollElem.scrollTop = targetTop;
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(setPos.bind(this));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = BackToTop;
|
67
resources/assets/js/components/chapter-toggle.js
Normal file
67
resources/assets/js/components/chapter-toggle.js
Normal file
@ -0,0 +1,67 @@
|
||||
|
||||
class ChapterToggle {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.isOpen = elem.classList.contains('open');
|
||||
elem.addEventListener('click', this.click.bind(this));
|
||||
}
|
||||
|
||||
open() {
|
||||
let list = this.elem.parentNode.querySelector('.inset-list');
|
||||
|
||||
this.elem.classList.add('open');
|
||||
list.style.display = 'block';
|
||||
list.style.height = '';
|
||||
let height = list.getBoundingClientRect().height;
|
||||
list.style.height = '0px';
|
||||
list.style.overflow = 'hidden';
|
||||
list.style.transition = 'height ease-in-out 240ms';
|
||||
|
||||
let transitionEndBound = onTransitionEnd.bind(this);
|
||||
function onTransitionEnd() {
|
||||
list.style.overflow = '';
|
||||
list.style.height = '';
|
||||
list.style.transition = '';
|
||||
list.removeEventListener('transitionend', transitionEndBound);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
list.style.height = `${height}px`;
|
||||
list.addEventListener('transitionend', transitionEndBound)
|
||||
}, 1);
|
||||
}
|
||||
|
||||
close() {
|
||||
let list = this.elem.parentNode.querySelector('.inset-list');
|
||||
|
||||
this.elem.classList.remove('open');
|
||||
list.style.display = 'block';
|
||||
list.style.height = list.getBoundingClientRect().height + 'px';
|
||||
list.style.overflow = 'hidden';
|
||||
list.style.transition = 'height ease-in-out 240ms';
|
||||
|
||||
let transitionEndBound = onTransitionEnd.bind(this);
|
||||
function onTransitionEnd() {
|
||||
list.style.overflow = '';
|
||||
list.style.height = '';
|
||||
list.style.transition = '';
|
||||
list.style.display = 'none';
|
||||
list.removeEventListener('transitionend', transitionEndBound);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
list.style.height = `0px`;
|
||||
list.addEventListener('transitionend', transitionEndBound)
|
||||
}, 1);
|
||||
}
|
||||
|
||||
click(event) {
|
||||
event.preventDefault();
|
||||
this.isOpen ? this.close() : this.open();
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ChapterToggle;
|
48
resources/assets/js/components/dropdown.js
Normal file
48
resources/assets/js/components/dropdown.js
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Dropdown
|
||||
* Provides some simple logic to create simple dropdown menus.
|
||||
*/
|
||||
class DropDown {
|
||||
|
||||
constructor(elem) {
|
||||
this.container = elem;
|
||||
this.menu = elem.querySelector('ul');
|
||||
this.toggle = elem.querySelector('[dropdown-toggle]');
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.menu.style.display = 'block';
|
||||
this.menu.classList.add('anim', 'menuIn');
|
||||
this.container.addEventListener('mouseleave', this.hide.bind(this));
|
||||
|
||||
// Focus on first input if existing
|
||||
let input = this.menu.querySelector('input');
|
||||
if (input !== null) input.focus();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.menu.style.display = 'none';
|
||||
this.menu.classList.remove('anim', 'menuIn');
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
// Hide menu on option click
|
||||
this.container.addEventListener('click', event => {
|
||||
let possibleChildren = Array.from(this.menu.querySelectorAll('a'));
|
||||
if (possibleChildren.indexOf(event.target) !== -1) this.hide();
|
||||
});
|
||||
// Show dropdown on toggle click
|
||||
this.toggle.addEventListener('click', this.show.bind(this));
|
||||
// Hide menu on enter press
|
||||
this.container.addEventListener('keypress', event => {
|
||||
if (event.keyCode !== 13) return true;
|
||||
event.preventDefault();
|
||||
this.hide();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = DropDown;
|
65
resources/assets/js/components/expand-toggle.js
Normal file
65
resources/assets/js/components/expand-toggle.js
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
class ExpandToggle {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.isOpen = false;
|
||||
this.selector = elem.getAttribute('expand-toggle');
|
||||
elem.addEventListener('click', this.click.bind(this));
|
||||
}
|
||||
|
||||
open(elemToToggle) {
|
||||
elemToToggle.style.display = 'block';
|
||||
elemToToggle.style.height = '';
|
||||
let height = elemToToggle.getBoundingClientRect().height;
|
||||
elemToToggle.style.height = '0px';
|
||||
elemToToggle.style.overflow = 'hidden';
|
||||
elemToToggle.style.transition = 'height ease-in-out 240ms';
|
||||
|
||||
let transitionEndBound = onTransitionEnd.bind(this);
|
||||
function onTransitionEnd() {
|
||||
elemToToggle.style.overflow = '';
|
||||
elemToToggle.style.height = '';
|
||||
elemToToggle.style.transition = '';
|
||||
elemToToggle.removeEventListener('transitionend', transitionEndBound);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
elemToToggle.style.height = `${height}px`;
|
||||
elemToToggle.addEventListener('transitionend', transitionEndBound)
|
||||
}, 1);
|
||||
}
|
||||
|
||||
close(elemToToggle) {
|
||||
elemToToggle.style.display = 'block';
|
||||
elemToToggle.style.height = elemToToggle.getBoundingClientRect().height + 'px';
|
||||
elemToToggle.style.overflow = 'hidden';
|
||||
elemToToggle.style.transition = 'all ease-in-out 240ms';
|
||||
|
||||
let transitionEndBound = onTransitionEnd.bind(this);
|
||||
function onTransitionEnd() {
|
||||
elemToToggle.style.overflow = '';
|
||||
elemToToggle.style.height = '';
|
||||
elemToToggle.style.transition = '';
|
||||
elemToToggle.style.display = 'none';
|
||||
elemToToggle.removeEventListener('transitionend', transitionEndBound);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
elemToToggle.style.height = `0px`;
|
||||
elemToToggle.addEventListener('transitionend', transitionEndBound)
|
||||
}, 1);
|
||||
}
|
||||
|
||||
click(event) {
|
||||
event.preventDefault();
|
||||
let matchingElems = document.querySelectorAll(this.selector);
|
||||
for (let i = 0, len = matchingElems.length; i < len; i++) {
|
||||
this.isOpen ? this.close(matchingElems[i]) : this.open(matchingElems[i]);
|
||||
}
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ExpandToggle;
|
28
resources/assets/js/components/index.js
Normal file
28
resources/assets/js/components/index.js
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
let componentMapping = {
|
||||
'dropdown': require('./dropdown'),
|
||||
'overlay': require('./overlay'),
|
||||
'back-to-top': require('./back-top-top'),
|
||||
'notification': require('./notification'),
|
||||
'chapter-toggle': require('./chapter-toggle'),
|
||||
'expand-toggle': require('./expand-toggle'),
|
||||
};
|
||||
|
||||
window.components = {};
|
||||
|
||||
let componentNames = Object.keys(componentMapping);
|
||||
|
||||
for (let i = 0, len = componentNames.length; i < len; i++) {
|
||||
let name = componentNames[i];
|
||||
let elems = document.querySelectorAll(`[${name}]`);
|
||||
if (elems.length === 0) continue;
|
||||
|
||||
let component = componentMapping[name];
|
||||
if (typeof window.components[name] === "undefined") window.components[name] = [];
|
||||
for (let j = 0, jLen = elems.length; j < jLen; j++) {
|
||||
let instance = new component(elems[j]);
|
||||
if (typeof elems[j].components === 'undefined') elems[j].components = {};
|
||||
elems[j].components[name] = instance;
|
||||
window.components[name].push(instance);
|
||||
}
|
||||
}
|
41
resources/assets/js/components/notification.js
Normal file
41
resources/assets/js/components/notification.js
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
class Notification {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.type = elem.getAttribute('notification');
|
||||
this.textElem = elem.querySelector('span');
|
||||
this.autohide = this.elem.hasAttribute('data-autohide');
|
||||
window.Events.listen(this.type, text => {
|
||||
this.show(text);
|
||||
});
|
||||
elem.addEventListener('click', this.hide.bind(this));
|
||||
if (elem.hasAttribute('data-show')) this.show(this.textElem.textContent);
|
||||
|
||||
this.hideCleanup = this.hideCleanup.bind(this);
|
||||
}
|
||||
|
||||
show(textToShow = '') {
|
||||
this.elem.removeEventListener('transitionend', this.hideCleanup);
|
||||
this.textElem.textContent = textToShow;
|
||||
this.elem.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
this.elem.classList.add('showing');
|
||||
}, 1);
|
||||
|
||||
if (this.autohide) setTimeout(this.hide.bind(this), 2000);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.elem.classList.remove('showing');
|
||||
this.elem.addEventListener('transitionend', this.hideCleanup);
|
||||
}
|
||||
|
||||
hideCleanup() {
|
||||
this.elem.style.display = 'none';
|
||||
this.elem.removeEventListener('transitionend', this.hideCleanup);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Notification;
|
39
resources/assets/js/components/overlay.js
Normal file
39
resources/assets/js/components/overlay.js
Normal file
@ -0,0 +1,39 @@
|
||||
|
||||
class Overlay {
|
||||
|
||||
constructor(elem) {
|
||||
this.container = elem;
|
||||
elem.addEventListener('click', event => {
|
||||
if (event.target === elem) return this.hide();
|
||||
});
|
||||
let closeButtons = elem.querySelectorAll('.overlay-close');
|
||||
for (let i=0; i < closeButtons.length; i++) {
|
||||
closeButtons[i].addEventListener('click', this.hide.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
toggle(show = true) {
|
||||
let start = Date.now();
|
||||
let duration = 240;
|
||||
|
||||
function setOpacity() {
|
||||
let elapsedTime = (Date.now() - start);
|
||||
let targetOpacity = show ? (elapsedTime / duration) : 1-(elapsedTime / duration);
|
||||
this.container.style.opacity = targetOpacity;
|
||||
if (elapsedTime > duration) {
|
||||
this.container.style.display = show ? 'flex' : 'none';
|
||||
this.container.style.opacity = '';
|
||||
} else {
|
||||
requestAnimationFrame(setOpacity.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(setOpacity.bind(this));
|
||||
}
|
||||
|
||||
hide() { this.toggle(false); }
|
||||
show() { this.toggle(true); }
|
||||
|
||||
}
|
||||
|
||||
module.exports = Overlay;
|
@ -8,256 +8,6 @@ moment.locale('en-gb');
|
||||
|
||||
module.exports = function (ngApp, events) {
|
||||
|
||||
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
|
||||
function ($scope, $attrs, $http, $timeout, imageManagerService) {
|
||||
|
||||
$scope.images = [];
|
||||
$scope.imageType = $attrs.imageType;
|
||||
$scope.selectedImage = false;
|
||||
$scope.dependantPages = false;
|
||||
$scope.showing = false;
|
||||
$scope.hasMore = false;
|
||||
$scope.imageUpdateSuccess = false;
|
||||
$scope.imageDeleteSuccess = false;
|
||||
$scope.uploadedTo = $attrs.uploadedTo;
|
||||
$scope.view = 'all';
|
||||
|
||||
$scope.searching = false;
|
||||
$scope.searchTerm = '';
|
||||
|
||||
let page = 0;
|
||||
let previousClickTime = 0;
|
||||
let previousClickImage = 0;
|
||||
let dataLoaded = false;
|
||||
let callback = false;
|
||||
|
||||
let preSearchImages = [];
|
||||
let preSearchHasMore = false;
|
||||
|
||||
/**
|
||||
* Used by dropzone to get the endpoint to upload to.
|
||||
* @returns {string}
|
||||
*/
|
||||
$scope.getUploadUrl = function () {
|
||||
return window.baseUrl('/images/' + $scope.imageType + '/upload');
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel the current search operation.
|
||||
*/
|
||||
function cancelSearch() {
|
||||
$scope.searching = false;
|
||||
$scope.searchTerm = '';
|
||||
$scope.images = preSearchImages;
|
||||
$scope.hasMore = preSearchHasMore;
|
||||
}
|
||||
$scope.cancelSearch = cancelSearch;
|
||||
|
||||
|
||||
/**
|
||||
* Runs on image upload, Adds an image to local list of images
|
||||
* and shows a success message to the user.
|
||||
* @param file
|
||||
* @param data
|
||||
*/
|
||||
$scope.uploadSuccess = function (file, data) {
|
||||
$scope.$apply(() => {
|
||||
$scope.images.unshift(data);
|
||||
});
|
||||
events.emit('success', trans('components.image_upload_success'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs the callback and hides the image manager.
|
||||
* @param returnData
|
||||
*/
|
||||
function callbackAndHide(returnData) {
|
||||
if (callback) callback(returnData);
|
||||
$scope.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Image select action. Checks if a double-click was fired.
|
||||
* @param image
|
||||
*/
|
||||
$scope.imageSelect = function (image) {
|
||||
let dblClickTime = 300;
|
||||
let currentTime = Date.now();
|
||||
let timeDiff = currentTime - previousClickTime;
|
||||
|
||||
if (timeDiff < dblClickTime && image.id === previousClickImage) {
|
||||
// If double click
|
||||
callbackAndHide(image);
|
||||
} else {
|
||||
// If single
|
||||
$scope.selectedImage = image;
|
||||
$scope.dependantPages = false;
|
||||
}
|
||||
previousClickTime = currentTime;
|
||||
previousClickImage = image.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Action that runs when the 'Select image' button is clicked.
|
||||
* Runs the callback and hides the image manager.
|
||||
*/
|
||||
$scope.selectButtonClick = function () {
|
||||
callbackAndHide($scope.selectedImage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the image manager.
|
||||
* Takes a callback to execute later on.
|
||||
* @param doneCallback
|
||||
*/
|
||||
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();
|
||||
dataLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Connects up the image manger so it can be used externally
|
||||
// such as from TinyMCE.
|
||||
imageManagerService.show = show;
|
||||
imageManagerService.showExternal = function (doneCallback) {
|
||||
$scope.$apply(() => {
|
||||
show(doneCallback);
|
||||
});
|
||||
};
|
||||
window.ImageManager = imageManagerService;
|
||||
|
||||
/**
|
||||
* Hide the image manager
|
||||
*/
|
||||
$scope.hide = function () {
|
||||
$scope.showing = false;
|
||||
$('#image-manager').find('.overlay').fadeOut(240);
|
||||
};
|
||||
|
||||
let baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');
|
||||
|
||||
/**
|
||||
* Fetch the list image data from the server.
|
||||
*/
|
||||
function fetchData() {
|
||||
let url = baseUrl + page + '?';
|
||||
let components = {};
|
||||
if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo;
|
||||
if ($scope.searching) components['term'] = $scope.searchTerm;
|
||||
|
||||
|
||||
url += Object.keys(components).map((key) => {
|
||||
return key + '=' + encodeURIComponent(components[key]);
|
||||
}).join('&');
|
||||
|
||||
$http.get(url).then((response) => {
|
||||
$scope.images = $scope.images.concat(response.data.images);
|
||||
$scope.hasMore = response.data.hasMore;
|
||||
page++;
|
||||
});
|
||||
}
|
||||
$scope.fetchData = fetchData;
|
||||
|
||||
/**
|
||||
* Start a search operation
|
||||
*/
|
||||
$scope.searchImages = function() {
|
||||
|
||||
if ($scope.searchTerm === '') {
|
||||
cancelSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$scope.searching) {
|
||||
preSearchImages = $scope.images;
|
||||
preSearchHasMore = $scope.hasMore;
|
||||
}
|
||||
|
||||
$scope.searching = true;
|
||||
$scope.images = [];
|
||||
$scope.hasMore = false;
|
||||
page = 0;
|
||||
baseUrl = window.baseUrl('/images/' + $scope.imageType + '/search/');
|
||||
fetchData();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the current image listing view.
|
||||
* @param viewName
|
||||
*/
|
||||
$scope.setView = function(viewName) {
|
||||
cancelSearch();
|
||||
$scope.images = [];
|
||||
$scope.hasMore = false;
|
||||
page = 0;
|
||||
$scope.view = viewName;
|
||||
baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/');
|
||||
fetchData();
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the details of an image.
|
||||
* @param event
|
||||
*/
|
||||
$scope.saveImageDetails = function (event) {
|
||||
event.preventDefault();
|
||||
let url = window.baseUrl('/images/update/' + $scope.selectedImage.id);
|
||||
$http.put(url, this.selectedImage).then(response => {
|
||||
events.emit('success', trans('components.image_update_success'));
|
||||
}, (response) => {
|
||||
if (response.status === 422) {
|
||||
let errors = response.data;
|
||||
let message = '';
|
||||
Object.keys(errors).forEach((key) => {
|
||||
message += errors[key].join('\n');
|
||||
});
|
||||
events.emit('error', message);
|
||||
} else if (response.status === 403) {
|
||||
events.emit('error', response.data.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete an image from system and notify of success.
|
||||
* Checks if it should force delete when an image
|
||||
* has dependant pages.
|
||||
* @param event
|
||||
*/
|
||||
$scope.deleteImage = function (event) {
|
||||
event.preventDefault();
|
||||
let force = $scope.dependantPages !== false;
|
||||
let url = window.baseUrl('/images/' + $scope.selectedImage.id);
|
||||
if (force) url += '?force=true';
|
||||
$http.delete(url).then((response) => {
|
||||
$scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
|
||||
$scope.selectedImage = false;
|
||||
events.emit('success', trans('components.image_delete_success'));
|
||||
}, (response) => {
|
||||
// Pages failure
|
||||
if (response.status === 400) {
|
||||
$scope.dependantPages = response.data;
|
||||
} else if (response.status === 403) {
|
||||
events.emit('error', response.data.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple date creator used to properly format dates.
|
||||
* @param stringDate
|
||||
* @returns {Date}
|
||||
*/
|
||||
$scope.getDate = function (stringDate) {
|
||||
return new Date(stringDate);
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
|
||||
function ($scope, $http, $attrs, $interval, $timeout, $sce) {
|
||||
@ -379,7 +129,7 @@ module.exports = function (ngApp, events) {
|
||||
*/
|
||||
$scope.discardDraft = function () {
|
||||
let url = window.baseUrl('/ajax/page/' + pageId);
|
||||
$http.get(url).then((responseData) => {
|
||||
$http.get(url).then(responseData => {
|
||||
if (autoSave) $interval.cancel(autoSave);
|
||||
$scope.draftText = trans('entities.pages_editing_page');
|
||||
$scope.isUpdateDraft = false;
|
||||
@ -395,284 +145,225 @@ module.exports = function (ngApp, events) {
|
||||
|
||||
}]);
|
||||
|
||||
ngApp.controller('PageTagController', ['$scope', '$http', '$attrs',
|
||||
function ($scope, $http, $attrs) {
|
||||
// Controller used to reply to and add new comments
|
||||
ngApp.controller('CommentReplyController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
|
||||
const MarkdownIt = require("markdown-it");
|
||||
const md = new MarkdownIt({html: true});
|
||||
let vm = this;
|
||||
|
||||
const pageId = Number($attrs.pageId);
|
||||
$scope.tags = [];
|
||||
|
||||
$scope.sortOptions = {
|
||||
handle: '.handle',
|
||||
items: '> tr',
|
||||
containment: "parent",
|
||||
axis: "y"
|
||||
vm.saveComment = function () {
|
||||
let pageId = $scope.comment.pageId || $scope.pageId;
|
||||
let comment = $scope.comment.text;
|
||||
if (!comment) {
|
||||
return events.emit('warning', trans('errors.empty_comment'));
|
||||
}
|
||||
let commentHTML = md.render($scope.comment.text);
|
||||
let serviceUrl = `/ajax/page/${pageId}/comment/`;
|
||||
let httpMethod = 'post';
|
||||
let reqObj = {
|
||||
text: comment,
|
||||
html: commentHTML
|
||||
};
|
||||
|
||||
/**
|
||||
* Push an empty tag to the end of the scope tags.
|
||||
*/
|
||||
function addEmptyTag() {
|
||||
$scope.tags.push({
|
||||
name: '',
|
||||
value: ''
|
||||
});
|
||||
if ($scope.isEdit === true) {
|
||||
// this will be set when editing the comment.
|
||||
serviceUrl = `/ajax/page/${pageId}/comment/${$scope.comment.id}`;
|
||||
httpMethod = 'put';
|
||||
} else if ($scope.isReply === true) {
|
||||
// if its reply, get the parent comment id
|
||||
reqObj.parent_id = $scope.parentId;
|
||||
}
|
||||
$scope.addEmptyTag = addEmptyTag;
|
||||
|
||||
/**
|
||||
* Get all tags for the current book and add into scope.
|
||||
*/
|
||||
function getTags() {
|
||||
let url = window.baseUrl(`/ajax/tags/get/page/${pageId}`);
|
||||
$http.get(url).then((responseData) => {
|
||||
$scope.tags = responseData.data;
|
||||
addEmptyTag();
|
||||
});
|
||||
}
|
||||
getTags();
|
||||
|
||||
/**
|
||||
* Set the order property on all tags.
|
||||
*/
|
||||
function setTagOrder() {
|
||||
for (let i = 0; i < $scope.tags.length; i++) {
|
||||
$scope.tags[i].order = i;
|
||||
$http[httpMethod](window.baseUrl(serviceUrl), reqObj).then(resp => {
|
||||
if (!isCommentOpSuccess(resp)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When an tag changes check if another empty editable
|
||||
* field needs to be added onto the end.
|
||||
* @param tag
|
||||
*/
|
||||
$scope.tagChange = function(tag) {
|
||||
let cPos = $scope.tags.indexOf(tag);
|
||||
if (cPos !== $scope.tags.length-1) return;
|
||||
|
||||
if (tag.name !== '' || tag.value !== '') {
|
||||
addEmptyTag();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When an tag field loses focus check the tag to see if its
|
||||
* empty and therefore could be removed from the list.
|
||||
* @param tag
|
||||
*/
|
||||
$scope.tagBlur = function(tag) {
|
||||
let isLast = $scope.tags.length - 1 === $scope.tags.indexOf(tag);
|
||||
if (tag.name === '' && tag.value === '' && !isLast) {
|
||||
let cPos = $scope.tags.indexOf(tag);
|
||||
$scope.tags.splice(cPos, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a tag from the current list.
|
||||
* @param tag
|
||||
*/
|
||||
$scope.removeTag = function(tag) {
|
||||
let cIndex = $scope.tags.indexOf(tag);
|
||||
$scope.tags.splice(cIndex, 1);
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
|
||||
ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs',
|
||||
function ($scope, $http, $attrs) {
|
||||
|
||||
const pageId = $scope.uploadedTo = $attrs.pageId;
|
||||
let currentOrder = '';
|
||||
$scope.files = [];
|
||||
$scope.editFile = false;
|
||||
$scope.file = getCleanFile();
|
||||
$scope.errors = {
|
||||
link: {},
|
||||
edit: {}
|
||||
};
|
||||
|
||||
function getCleanFile() {
|
||||
return {
|
||||
page_id: pageId
|
||||
};
|
||||
}
|
||||
|
||||
// Angular-UI-Sort options
|
||||
$scope.sortOptions = {
|
||||
handle: '.handle',
|
||||
items: '> tr',
|
||||
containment: "parent",
|
||||
axis: "y",
|
||||
stop: sortUpdate,
|
||||
};
|
||||
|
||||
/**
|
||||
* Event listener for sort changes.
|
||||
* Updates the file ordering on the server.
|
||||
* @param event
|
||||
* @param ui
|
||||
*/
|
||||
function sortUpdate(event, ui) {
|
||||
let newOrder = $scope.files.map(file => {return file.id}).join(':');
|
||||
if (newOrder === currentOrder) return;
|
||||
|
||||
currentOrder = newOrder;
|
||||
$http.put(window.baseUrl(`/attachments/sort/page/${pageId}`), {files: $scope.files}).then(resp => {
|
||||
events.emit('success', resp.data.message);
|
||||
}, checkError('sort'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by dropzone to get the endpoint to upload to.
|
||||
* @returns {string}
|
||||
*/
|
||||
$scope.getUploadUrl = function (file) {
|
||||
let suffix = (typeof file !== 'undefined') ? `/${file.id}` : '';
|
||||
return window.baseUrl(`/attachments/upload${suffix}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get files for the current page from the server.
|
||||
*/
|
||||
function getFiles() {
|
||||
let url = window.baseUrl(`/attachments/get/page/${pageId}`);
|
||||
$http.get(url).then(resp => {
|
||||
$scope.files = resp.data;
|
||||
currentOrder = resp.data.map(file => {return file.id}).join(':');
|
||||
}, checkError('get'));
|
||||
}
|
||||
getFiles();
|
||||
|
||||
/**
|
||||
* Runs on file upload, Adds an file to local file list
|
||||
* and shows a success message to the user.
|
||||
* @param file
|
||||
* @param data
|
||||
*/
|
||||
$scope.uploadSuccess = function (file, data) {
|
||||
$scope.$apply(() => {
|
||||
$scope.files.push(data);
|
||||
});
|
||||
events.emit('success', trans('entities.attachments_file_uploaded'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload and overwrite an existing file.
|
||||
* @param file
|
||||
* @param data
|
||||
*/
|
||||
$scope.uploadSuccessUpdate = function (file, data) {
|
||||
$scope.$apply(() => {
|
||||
let search = filesIndexOf(data);
|
||||
if (search !== -1) $scope.files[search] = data;
|
||||
|
||||
if ($scope.editFile) {
|
||||
$scope.editFile = angular.copy(data);
|
||||
data.link = '';
|
||||
// hide the comments first, and then retrigger the refresh
|
||||
if ($scope.isEdit) {
|
||||
updateComment($scope.comment, resp.data);
|
||||
$scope.$emit('evt.comment-success', $scope.comment.id);
|
||||
} else {
|
||||
$scope.comment.text = '';
|
||||
if ($scope.isReply === true && $scope.parent.sub_comments) {
|
||||
$scope.parent.sub_comments.push(resp.data.comment);
|
||||
} else {
|
||||
$scope.$emit('evt.new-comment', resp.data.comment);
|
||||
}
|
||||
$scope.$emit('evt.comment-success', null, true);
|
||||
}
|
||||
$scope.comment.is_hidden = true;
|
||||
$timeout(function() {
|
||||
$scope.comment.is_hidden = false;
|
||||
});
|
||||
events.emit('success', trans('entities.attachments_file_updated'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a file from the server and, on success, the local listing.
|
||||
* @param file
|
||||
*/
|
||||
$scope.deleteFile = function(file) {
|
||||
if (!file.deleting) {
|
||||
file.deleting = true;
|
||||
events.emit('success', trans(resp.data.message));
|
||||
|
||||
}, checkError);
|
||||
|
||||
};
|
||||
|
||||
function checkError(response) {
|
||||
let msg = null;
|
||||
if (isCommentOpSuccess(response)) {
|
||||
// all good
|
||||
return;
|
||||
} else if (response.data) {
|
||||
msg = response.data.message;
|
||||
} else {
|
||||
msg = trans('errors.comment_add');
|
||||
}
|
||||
if (msg) {
|
||||
events.emit('success', msg);
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
// Controller used to delete comments
|
||||
ngApp.controller('CommentDeleteController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
|
||||
let vm = this;
|
||||
|
||||
vm.delete = function(comment) {
|
||||
$http.delete(window.baseUrl(`/ajax/comment/${comment.id}`)).then(resp => {
|
||||
if (!isCommentOpSuccess(resp)) {
|
||||
return;
|
||||
}
|
||||
$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
|
||||
events.emit('success', resp.data.message);
|
||||
$scope.files.splice($scope.files.indexOf(file), 1);
|
||||
}, checkError('delete'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach a link to a page.
|
||||
* @param file
|
||||
*/
|
||||
$scope.attachLinkSubmit = function(file) {
|
||||
file.uploaded_to = pageId;
|
||||
$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
|
||||
$scope.files.push(resp.data);
|
||||
events.emit('success', trans('entities.attachments_link_attached'));
|
||||
$scope.file = getCleanFile();
|
||||
}, checkError('link'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the edit mode for a file.
|
||||
* @param file
|
||||
*/
|
||||
$scope.startEdit = function(file) {
|
||||
$scope.editFile = angular.copy(file);
|
||||
$scope.editFile.link = (file.external) ? file.path : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel edit mode
|
||||
*/
|
||||
$scope.cancelEdit = function() {
|
||||
$scope.editFile = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the name and link of a file.
|
||||
* @param file
|
||||
*/
|
||||
$scope.updateFile = function(file) {
|
||||
$http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
|
||||
let search = filesIndexOf(resp.data);
|
||||
if (search !== -1) $scope.files[search] = resp.data;
|
||||
|
||||
if ($scope.editFile && !file.external) {
|
||||
$scope.editFile.link = '';
|
||||
}
|
||||
$scope.editFile = false;
|
||||
events.emit('success', trans('entities.attachments_updated_success'));
|
||||
}, checkError('edit'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the url of a file.
|
||||
*/
|
||||
$scope.getFileUrl = function(file) {
|
||||
return window.baseUrl('/attachments/' + file.id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Search the local files via another file object.
|
||||
* Used to search via object copies.
|
||||
* @param file
|
||||
* @returns int
|
||||
*/
|
||||
function filesIndexOf(file) {
|
||||
for (let i = 0; i < $scope.files.length; i++) {
|
||||
if ($scope.files[i].id == file.id) return i;
|
||||
updateComment(comment, resp.data, $timeout, true);
|
||||
}, function (resp) {
|
||||
if (isCommentOpSuccess(resp)) {
|
||||
events.emit('success', trans('entities.comment_deleted'));
|
||||
} else {
|
||||
events.emit('error', trans('error.comment_delete'));
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
};
|
||||
}]);
|
||||
|
||||
// Controller used to fetch all comments for a page
|
||||
ngApp.controller('CommentListController', ['$scope', '$http', '$timeout', '$location', function ($scope, $http, $timeout, $location) {
|
||||
let vm = this;
|
||||
$scope.errors = {};
|
||||
// keep track of comment levels
|
||||
$scope.level = 1;
|
||||
vm.totalCommentsStr = trans('entities.comments_loading');
|
||||
vm.permissions = {};
|
||||
vm.trans = window.trans;
|
||||
|
||||
$scope.$on('evt.new-comment', function (event, comment) {
|
||||
// add the comment to the comment list.
|
||||
vm.comments.push(comment);
|
||||
++vm.totalComments;
|
||||
setTotalCommentMsg();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
vm.canEditDelete = function (comment, prop) {
|
||||
if (!comment.active) {
|
||||
return false;
|
||||
}
|
||||
let propAll = prop + '_all';
|
||||
let propOwn = prop + '_own';
|
||||
|
||||
if (vm.permissions[propAll]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an error response in a ajax request.
|
||||
* @param errorGroupName
|
||||
*/
|
||||
function checkError(errorGroupName) {
|
||||
$scope.errors[errorGroupName] = {};
|
||||
return function(response) {
|
||||
if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') {
|
||||
events.emit('error', response.data.error);
|
||||
}
|
||||
if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') {
|
||||
$scope.errors[errorGroupName] = response.data.validation;
|
||||
console.log($scope.errors[errorGroupName])
|
||||
}
|
||||
}
|
||||
if (vm.permissions[propOwn] && comment.created_by.id === vm.current_user_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}]);
|
||||
return false;
|
||||
};
|
||||
|
||||
vm.canComment = function () {
|
||||
return vm.permissions.comment_create;
|
||||
};
|
||||
|
||||
// check if there are is any direct linking
|
||||
let linkedCommentId = $location.search().cm;
|
||||
|
||||
$timeout(function() {
|
||||
$http.get(window.baseUrl(`/ajax/page/${$scope.pageId}/comments/`)).then(resp => {
|
||||
if (!isCommentOpSuccess(resp)) {
|
||||
// just show that no comments are available.
|
||||
vm.totalComments = 0;
|
||||
setTotalCommentMsg();
|
||||
return;
|
||||
}
|
||||
vm.comments = resp.data.comments;
|
||||
vm.totalComments = +resp.data.total;
|
||||
vm.permissions = resp.data.permissions;
|
||||
vm.current_user_id = resp.data.user_id;
|
||||
setTotalCommentMsg();
|
||||
if (!linkedCommentId) {
|
||||
return;
|
||||
}
|
||||
$timeout(function() {
|
||||
// wait for the UI to render.
|
||||
focusLinkedComment(linkedCommentId);
|
||||
});
|
||||
}, checkError);
|
||||
});
|
||||
|
||||
function setTotalCommentMsg () {
|
||||
if (vm.totalComments === 0) {
|
||||
vm.totalCommentsStr = trans('entities.no_comments');
|
||||
} else if (vm.totalComments === 1) {
|
||||
vm.totalCommentsStr = trans('entities.one_comment');
|
||||
} else {
|
||||
vm.totalCommentsStr = trans('entities.x_comments', {
|
||||
numComments: vm.totalComments
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function focusLinkedComment(linkedCommentId) {
|
||||
let comment = angular.element('#' + linkedCommentId);
|
||||
if (comment.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.setupPageShow.goToText(linkedCommentId);
|
||||
}
|
||||
|
||||
function checkError(response) {
|
||||
let msg = null;
|
||||
if (isCommentOpSuccess(response)) {
|
||||
// all good
|
||||
return;
|
||||
} else if (response.data) {
|
||||
msg = response.data.message;
|
||||
} else {
|
||||
msg = trans('errors.comment_list');
|
||||
}
|
||||
if (msg) {
|
||||
events.emit('success', msg);
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
function updateComment(comment, resp, $timeout, isDelete) {
|
||||
comment.text = resp.comment.text;
|
||||
comment.updated = resp.comment.updated;
|
||||
comment.updated_by = resp.comment.updated_by;
|
||||
comment.active = resp.comment.active;
|
||||
if (isDelete && !resp.comment.active) {
|
||||
comment.html = trans('entities.comment_deleted');
|
||||
} else {
|
||||
comment.html = resp.comment.html;
|
||||
}
|
||||
if (!$timeout) {
|
||||
return;
|
||||
}
|
||||
comment.is_hidden = true;
|
||||
$timeout(function() {
|
||||
comment.is_hidden = false;
|
||||
});
|
||||
}
|
||||
|
||||
function isCommentOpSuccess(resp) {
|
||||
if (resp && resp.data && resp.data.status === 'success') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -1,152 +1,10 @@
|
||||
"use strict";
|
||||
const DropZone = require("dropzone");
|
||||
const MarkdownIt = require("markdown-it");
|
||||
const mdTasksLists = require('markdown-it-task-lists');
|
||||
const code = require('./code');
|
||||
|
||||
module.exports = function (ngApp, events) {
|
||||
|
||||
/**
|
||||
* Common tab controls using simple jQuery functions.
|
||||
*/
|
||||
ngApp.directive('tabContainer', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
const $content = element.find('[tab-content]');
|
||||
const $buttons = element.find('[tab-button]');
|
||||
|
||||
if (attrs.tabContainer) {
|
||||
let initial = attrs.tabContainer;
|
||||
$buttons.filter(`[tab-button="${initial}"]`).addClass('selected');
|
||||
$content.hide().filter(`[tab-content="${initial}"]`).show();
|
||||
} else {
|
||||
$content.hide().first().show();
|
||||
$buttons.first().addClass('selected');
|
||||
}
|
||||
|
||||
$buttons.click(function() {
|
||||
let clickedTab = $(this);
|
||||
$buttons.removeClass('selected');
|
||||
$content.hide();
|
||||
let name = clickedTab.addClass('selected').attr('tab-button');
|
||||
$content.filter(`[tab-content="${name}"]`).show();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Sub form component to allow inner-form sections to act like their own forms.
|
||||
*/
|
||||
ngApp.directive('subForm', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
element.on('keypress', e => {
|
||||
if (e.keyCode === 13) {
|
||||
submitEvent(e);
|
||||
}
|
||||
});
|
||||
|
||||
element.find('button[type="submit"]').click(submitEvent);
|
||||
|
||||
function submitEvent(e) {
|
||||
e.preventDefault();
|
||||
if (attrs.subForm) scope.$eval(attrs.subForm);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* DropZone
|
||||
* Used for uploading images
|
||||
*/
|
||||
ngApp.directive('dropZone', [function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: `
|
||||
<div class="dropzone-container">
|
||||
<div class="dz-message">{{message}}</div>
|
||||
</div>
|
||||
`,
|
||||
scope: {
|
||||
uploadUrl: '@',
|
||||
eventSuccess: '=',
|
||||
eventError: '=',
|
||||
uploadedTo: '@',
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
scope.message = attrs.message;
|
||||
if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
|
||||
let dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
|
||||
url: scope.uploadUrl,
|
||||
init: function () {
|
||||
let dz = this;
|
||||
dz.on('sending', function (file, xhr, data) {
|
||||
let token = window.document.querySelector('meta[name=token]').getAttribute('content');
|
||||
data.append('_token', token);
|
||||
let uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo;
|
||||
data.append('uploaded_to', uploadedTo);
|
||||
});
|
||||
if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess);
|
||||
dz.on('success', function (file, data) {
|
||||
$(file.previewElement).fadeOut(400, function () {
|
||||
dz.removeFile(file);
|
||||
});
|
||||
});
|
||||
if (typeof scope.eventError !== 'undefined') dz.on('error', scope.eventError);
|
||||
dz.on('error', function (file, errorMessage, xhr) {
|
||||
console.log(errorMessage);
|
||||
console.log(xhr);
|
||||
function setMessage(message) {
|
||||
$(file.previewElement).find('[data-dz-errormessage]').text(message);
|
||||
}
|
||||
|
||||
if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
|
||||
if (errorMessage.file) setMessage(errorMessage.file[0]);
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
/**
|
||||
* Dropdown
|
||||
* Provides some simple logic to create small dropdown menus
|
||||
*/
|
||||
ngApp.directive('dropdown', [function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
const menu = element.find('ul');
|
||||
element.find('[dropdown-toggle]').on('click', function () {
|
||||
menu.show().addClass('anim menuIn');
|
||||
let inputs = menu.find('input');
|
||||
let hasInput = inputs.length > 0;
|
||||
if (hasInput) {
|
||||
inputs.first().focus();
|
||||
element.on('keypress', 'input', event => {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
menu.hide();
|
||||
menu.removeClass('anim menuIn');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
element.mouseleave(function () {
|
||||
menu.hide();
|
||||
menu.removeClass('anim menuIn');
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
/**
|
||||
* TinyMCE
|
||||
* An angular wrapper around the tinyMCE editor.
|
||||
@ -187,30 +45,6 @@ module.exports = function (ngApp, events) {
|
||||
}
|
||||
|
||||
scope.tinymce.extraSetups.push(tinyMceSetup);
|
||||
|
||||
// Custom tinyMCE plugins
|
||||
tinymce.PluginManager.add('customhr', function (editor) {
|
||||
editor.addCommand('InsertHorizontalRule', function () {
|
||||
let hrElem = document.createElement('hr');
|
||||
let cNode = editor.selection.getNode();
|
||||
let parentNode = cNode.parentNode;
|
||||
parentNode.insertBefore(hrElem, cNode);
|
||||
});
|
||||
|
||||
editor.addButton('hr', {
|
||||
icon: 'hr',
|
||||
tooltip: 'Horizontal line',
|
||||
cmd: 'InsertHorizontalRule'
|
||||
});
|
||||
|
||||
editor.addMenuItem('hr', {
|
||||
icon: 'hr',
|
||||
text: 'Horizontal line',
|
||||
cmd: 'InsertHorizontalRule',
|
||||
context: 'insert'
|
||||
});
|
||||
});
|
||||
|
||||
tinymce.init(scope.tinymce);
|
||||
}
|
||||
}
|
||||
@ -251,6 +85,21 @@ module.exports = function (ngApp, events) {
|
||||
extraKeys[`${metaKey}-S`] = function(cm) {scope.$emit('save-draft');};
|
||||
// Show link selector
|
||||
extraKeys[`Shift-${metaKey}-K`] = function(cm) {showLinkSelector()};
|
||||
// Insert Link
|
||||
extraKeys[`${metaKey}-K`] = function(cm) {insertLink()};
|
||||
// FormatShortcuts
|
||||
extraKeys[`${metaKey}-1`] = function(cm) {replaceLineStart('##');};
|
||||
extraKeys[`${metaKey}-2`] = function(cm) {replaceLineStart('###');};
|
||||
extraKeys[`${metaKey}-3`] = function(cm) {replaceLineStart('####');};
|
||||
extraKeys[`${metaKey}-4`] = function(cm) {replaceLineStart('#####');};
|
||||
extraKeys[`${metaKey}-5`] = function(cm) {replaceLineStart('');};
|
||||
extraKeys[`${metaKey}-d`] = function(cm) {replaceLineStart('');};
|
||||
extraKeys[`${metaKey}-6`] = function(cm) {replaceLineStart('>');};
|
||||
extraKeys[`${metaKey}-q`] = function(cm) {replaceLineStart('>');};
|
||||
extraKeys[`${metaKey}-7`] = function(cm) {wrapSelection('\n```\n', '\n```');};
|
||||
extraKeys[`${metaKey}-8`] = function(cm) {wrapSelection('`', '`');};
|
||||
extraKeys[`Shift-${metaKey}-E`] = function(cm) {wrapSelection('`', '`');};
|
||||
extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('<p class="callout info">', '</div>');};
|
||||
cm.setOption('extraKeys', extraKeys);
|
||||
|
||||
// Update data on content change
|
||||
@ -303,6 +152,73 @@ module.exports = function (ngApp, events) {
|
||||
cm.setSelections(cursor);
|
||||
}
|
||||
|
||||
// Helper to replace the start of the line
|
||||
function replaceLineStart(newStart) {
|
||||
let cursor = cm.getCursor();
|
||||
let lineContent = cm.getLine(cursor.line);
|
||||
let lineLen = lineContent.length;
|
||||
let lineStart = lineContent.split(' ')[0];
|
||||
|
||||
// Remove symbol if already set
|
||||
if (lineStart === newStart) {
|
||||
lineContent = lineContent.replace(`${newStart} `, '');
|
||||
cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
|
||||
cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)});
|
||||
return;
|
||||
}
|
||||
|
||||
let alreadySymbol = /^[#>`]/.test(lineStart);
|
||||
let posDif = 0;
|
||||
if (alreadySymbol) {
|
||||
posDif = newStart.length - lineStart.length;
|
||||
lineContent = lineContent.replace(lineStart, newStart).trim();
|
||||
} else if (newStart !== '') {
|
||||
posDif = newStart.length + 1;
|
||||
lineContent = newStart + ' ' + lineContent;
|
||||
}
|
||||
cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
|
||||
cm.setCursor({line: cursor.line, ch: cursor.ch + posDif});
|
||||
}
|
||||
|
||||
function wrapLine(start, end) {
|
||||
let cursor = cm.getCursor();
|
||||
let lineContent = cm.getLine(cursor.line);
|
||||
let lineLen = lineContent.length;
|
||||
let newLineContent = lineContent;
|
||||
|
||||
if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
|
||||
newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
|
||||
} else {
|
||||
newLineContent = `${start}${lineContent}${end}`;
|
||||
}
|
||||
|
||||
cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
|
||||
cm.setCursor({line: cursor.line, ch: cursor.ch + (newLineContent.length - lineLen)});
|
||||
}
|
||||
|
||||
function wrapSelection(start, end) {
|
||||
let selection = cm.getSelection();
|
||||
if (selection === '') return wrapLine(start, end);
|
||||
let newSelection = selection;
|
||||
let frontDiff = 0;
|
||||
let endDiff = 0;
|
||||
|
||||
if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
|
||||
newSelection = selection.slice(start.length, selection.length - end.length);
|
||||
endDiff = -(end.length + start.length);
|
||||
} else {
|
||||
newSelection = `${start}${selection}${end}`;
|
||||
endDiff = start.length + end.length;
|
||||
}
|
||||
|
||||
let selections = cm.listSelections()[0];
|
||||
cm.replaceSelection(newSelection);
|
||||
let headFirst = selections.head.ch <= selections.anchor.ch;
|
||||
selections.head.ch += headFirst ? frontDiff : endDiff;
|
||||
selections.anchor.ch += headFirst ? endDiff : frontDiff;
|
||||
cm.setSelections([selections]);
|
||||
}
|
||||
|
||||
// Handle image upload and add image into markdown content
|
||||
function uploadImage(file) {
|
||||
if (file === null || file.type.indexOf('image') !== 0) return;
|
||||
@ -345,10 +261,20 @@ module.exports = function (ngApp, events) {
|
||||
});
|
||||
}
|
||||
|
||||
function insertLink() {
|
||||
let cursorPos = cm.getCursor('from');
|
||||
let selectedText = cm.getSelection() || '';
|
||||
let newText = `[${selectedText}]()`;
|
||||
cm.focus();
|
||||
cm.replaceSelection(newText);
|
||||
let cursorPosDiff = (selectedText === '') ? -3 : -1;
|
||||
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff);
|
||||
}
|
||||
|
||||
// Show the image manager and handle image insertion
|
||||
function showImageManager() {
|
||||
let cursorPos = cm.getCursor('from');
|
||||
window.ImageManager.showExternal(image => {
|
||||
window.ImageManager.show(image => {
|
||||
let selectedText = cm.getSelection();
|
||||
let newText = "";
|
||||
cm.focus();
|
||||
@ -461,188 +387,6 @@ module.exports = function (ngApp, events) {
|
||||
}
|
||||
}]);
|
||||
|
||||
/**
|
||||
* Tag Autosuggestions
|
||||
* Listens to child inputs and provides autosuggestions depending on field type
|
||||
* and input. Suggestions provided by server.
|
||||
*/
|
||||
ngApp.directive('tagAutosuggestions', ['$http', function ($http) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, elem, attrs) {
|
||||
|
||||
// Local storage for quick caching.
|
||||
const localCache = {};
|
||||
|
||||
// Create suggestion element
|
||||
const suggestionBox = document.createElement('ul');
|
||||
suggestionBox.className = 'suggestion-box';
|
||||
suggestionBox.style.position = 'absolute';
|
||||
suggestionBox.style.display = 'none';
|
||||
const $suggestionBox = $(suggestionBox);
|
||||
|
||||
// General state tracking
|
||||
let isShowing = false;
|
||||
let currentInput = false;
|
||||
let active = 0;
|
||||
|
||||
// Listen to input events on autosuggest fields
|
||||
elem.on('input focus', '[autosuggest]', function (event) {
|
||||
let $input = $(this);
|
||||
let val = $input.val();
|
||||
let url = $input.attr('autosuggest');
|
||||
let type = $input.attr('autosuggest-type');
|
||||
|
||||
// Add name param to request if for a value
|
||||
if (type.toLowerCase() === 'value') {
|
||||
let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
|
||||
let nameVal = $nameInput.val();
|
||||
if (nameVal !== '') {
|
||||
url += '?name=' + encodeURIComponent(nameVal);
|
||||
}
|
||||
}
|
||||
|
||||
let suggestionPromise = getSuggestions(val.slice(0, 3), url);
|
||||
suggestionPromise.then(suggestions => {
|
||||
if (val.length === 0) {
|
||||
displaySuggestions($input, suggestions.slice(0, 6));
|
||||
} else {
|
||||
suggestions = suggestions.filter(item => {
|
||||
return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
|
||||
}).slice(0, 4);
|
||||
displaySuggestions($input, suggestions);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Hide autosuggestions when input loses focus.
|
||||
// Slight delay to allow clicks.
|
||||
let lastFocusTime = 0;
|
||||
elem.on('blur', '[autosuggest]', function (event) {
|
||||
let startTime = Date.now();
|
||||
setTimeout(() => {
|
||||
if (lastFocusTime < startTime) {
|
||||
$suggestionBox.hide();
|
||||
isShowing = false;
|
||||
}
|
||||
}, 200)
|
||||
});
|
||||
elem.on('focus', '[autosuggest]', function (event) {
|
||||
lastFocusTime = Date.now();
|
||||
});
|
||||
|
||||
elem.on('keydown', '[autosuggest]', function (event) {
|
||||
if (!isShowing) return;
|
||||
|
||||
let suggestionElems = suggestionBox.childNodes;
|
||||
let suggestCount = suggestionElems.length;
|
||||
|
||||
// Down arrow
|
||||
if (event.keyCode === 40) {
|
||||
let newActive = (active === suggestCount - 1) ? 0 : active + 1;
|
||||
changeActiveTo(newActive, suggestionElems);
|
||||
}
|
||||
// Up arrow
|
||||
else if (event.keyCode === 38) {
|
||||
let newActive = (active === 0) ? suggestCount - 1 : active - 1;
|
||||
changeActiveTo(newActive, suggestionElems);
|
||||
}
|
||||
// Enter or tab key
|
||||
else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
|
||||
currentInput[0].value = suggestionElems[active].textContent;
|
||||
currentInput.focus();
|
||||
$suggestionBox.hide();
|
||||
isShowing = false;
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Change the active suggestion to the given index
|
||||
function changeActiveTo(index, suggestionElems) {
|
||||
suggestionElems[active].className = '';
|
||||
active = index;
|
||||
suggestionElems[active].className = 'active';
|
||||
}
|
||||
|
||||
// Display suggestions on a field
|
||||
let prevSuggestions = [];
|
||||
|
||||
function displaySuggestions($input, suggestions) {
|
||||
|
||||
// Hide if no suggestions
|
||||
if (suggestions.length === 0) {
|
||||
$suggestionBox.hide();
|
||||
isShowing = false;
|
||||
prevSuggestions = suggestions;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise show and attach to input
|
||||
if (!isShowing) {
|
||||
$suggestionBox.show();
|
||||
isShowing = true;
|
||||
}
|
||||
if ($input !== currentInput) {
|
||||
$suggestionBox.detach();
|
||||
$input.after($suggestionBox);
|
||||
currentInput = $input;
|
||||
}
|
||||
|
||||
// Return if no change
|
||||
if (prevSuggestions.join() === suggestions.join()) {
|
||||
prevSuggestions = suggestions;
|
||||
return;
|
||||
}
|
||||
|
||||
// Build suggestions
|
||||
$suggestionBox[0].innerHTML = '';
|
||||
for (let i = 0; i < suggestions.length; i++) {
|
||||
let suggestion = document.createElement('li');
|
||||
suggestion.textContent = suggestions[i];
|
||||
suggestion.onclick = suggestionClick;
|
||||
if (i === 0) {
|
||||
suggestion.className = 'active';
|
||||
active = 0;
|
||||
}
|
||||
$suggestionBox[0].appendChild(suggestion);
|
||||
}
|
||||
|
||||
prevSuggestions = suggestions;
|
||||
}
|
||||
|
||||
// Suggestion click event
|
||||
function suggestionClick(event) {
|
||||
currentInput[0].value = this.textContent;
|
||||
currentInput.focus();
|
||||
$suggestionBox.hide();
|
||||
isShowing = false;
|
||||
}
|
||||
|
||||
// Get suggestions & cache
|
||||
function getSuggestions(input, url) {
|
||||
let hasQuery = url.indexOf('?') !== -1;
|
||||
let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input);
|
||||
|
||||
// Get from local cache if exists
|
||||
if (typeof localCache[searchUrl] !== 'undefined') {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(localCache[searchUrl]);
|
||||
});
|
||||
}
|
||||
|
||||
return $http.get(searchUrl).then(response => {
|
||||
localCache[searchUrl] = response.data;
|
||||
return response.data;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
ngApp.directive('entityLinkSelector', [function($http) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
@ -678,6 +422,7 @@ module.exports = function (ngApp, events) {
|
||||
function hide() {
|
||||
element.fadeOut(240);
|
||||
}
|
||||
scope.hide = hide;
|
||||
|
||||
// Listen to confirmation of entity selections (doubleclick)
|
||||
events.listen('entity-select-confirm', entity => {
|
||||
@ -789,4 +534,128 @@ module.exports = function (ngApp, events) {
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
ngApp.directive('commentReply', [function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'comment-reply.html',
|
||||
scope: {
|
||||
pageId: '=',
|
||||
parentId: '=',
|
||||
parent: '='
|
||||
},
|
||||
link: function (scope, element) {
|
||||
scope.isReply = true;
|
||||
element.find('textarea').focus();
|
||||
scope.$on('evt.comment-success', function (event) {
|
||||
// no need for the event to do anything more.
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
scope.closeBox();
|
||||
});
|
||||
|
||||
scope.closeBox = function () {
|
||||
element.remove();
|
||||
scope.$destroy();
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
ngApp.directive('commentEdit', [function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'comment-reply.html',
|
||||
scope: {
|
||||
comment: '='
|
||||
},
|
||||
link: function (scope, element) {
|
||||
scope.isEdit = true;
|
||||
element.find('textarea').focus();
|
||||
scope.$on('evt.comment-success', function (event, commentId) {
|
||||
// no need for the event to do anything more.
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (commentId === scope.comment.id && !scope.isNew) {
|
||||
scope.closeBox();
|
||||
}
|
||||
});
|
||||
|
||||
scope.closeBox = function () {
|
||||
element.remove();
|
||||
scope.$destroy();
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
|
||||
ngApp.directive('commentReplyLink', ['$document', '$compile', function ($document, $compile) {
|
||||
return {
|
||||
scope: {
|
||||
comment: '='
|
||||
},
|
||||
link: function (scope, element, attr) {
|
||||
element.on('$destroy', function () {
|
||||
element.off('click');
|
||||
scope.$destroy();
|
||||
});
|
||||
|
||||
element.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
var $container = element.parents('.comment-actions').first();
|
||||
if (!$container.length) {
|
||||
console.error('commentReplyLink directive should be placed inside a container with class comment-box!');
|
||||
return;
|
||||
}
|
||||
if (attr.noCommentReplyDupe) {
|
||||
removeDupe();
|
||||
}
|
||||
|
||||
compileHtml($container, scope, attr.isReply === 'true');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function compileHtml($container, scope, isReply) {
|
||||
let lnkFunc = null;
|
||||
if (isReply) {
|
||||
lnkFunc = $compile('<comment-reply page-id="comment.pageId" parent-id="comment.id" parent="comment"></comment-reply>');
|
||||
} else {
|
||||
lnkFunc = $compile('<comment-edit comment="comment"></comment-add>');
|
||||
}
|
||||
var compiledHTML = lnkFunc(scope);
|
||||
$container.append(compiledHTML);
|
||||
}
|
||||
|
||||
function removeDupe() {
|
||||
let $existingElement = $document.find('.comments-list comment-reply, .comments-list comment-edit');
|
||||
if (!$existingElement.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingElement.remove();
|
||||
}
|
||||
}]);
|
||||
|
||||
ngApp.directive('commentDeleteLink', ['$window', function ($window) {
|
||||
return {
|
||||
controller: 'CommentDeleteController',
|
||||
scope: {
|
||||
comment: '='
|
||||
},
|
||||
link: function (scope, element, attr, ctrl) {
|
||||
|
||||
element.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var resp = $window.confirm(trans('entities.comment_delete_confirm'));
|
||||
if (!resp) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctrl.delete(scope.comment);
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
"use strict";
|
||||
require("babel-polyfill");
|
||||
|
||||
// Url retrieval function
|
||||
window.baseUrl = function(path) {
|
||||
@ -8,37 +9,6 @@ window.baseUrl = function(path) {
|
||||
return basePath + '/' + path;
|
||||
};
|
||||
|
||||
const Vue = require("vue");
|
||||
const axios = require("axios");
|
||||
|
||||
let axiosInstance = axios.create({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
|
||||
'baseURL': window.baseUrl('')
|
||||
}
|
||||
});
|
||||
window.$http = axiosInstance;
|
||||
|
||||
Vue.prototype.$http = axiosInstance;
|
||||
|
||||
require("./vues/vues");
|
||||
|
||||
|
||||
// AngularJS - Create application and load components
|
||||
const angular = require("angular");
|
||||
require("angular-resource");
|
||||
require("angular-animate");
|
||||
require("angular-sanitize");
|
||||
require("angular-ui-sortable");
|
||||
|
||||
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
|
||||
const Translations = require("./translations");
|
||||
let translator = new Translations(window.translations);
|
||||
window.trans = translator.get.bind(translator);
|
||||
|
||||
// Global Event System
|
||||
class EventManager {
|
||||
constructor() {
|
||||
@ -63,13 +33,51 @@ class EventManager {
|
||||
}
|
||||
|
||||
window.Events = new EventManager();
|
||||
|
||||
const Vue = require("vue");
|
||||
const axios = require("axios");
|
||||
|
||||
let axiosInstance = axios.create({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
|
||||
'baseURL': window.baseUrl('')
|
||||
}
|
||||
});
|
||||
axiosInstance.interceptors.request.use(resp => {
|
||||
return resp;
|
||||
}, err => {
|
||||
if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err);
|
||||
if (typeof err.response.data.error !== "undefined") window.Events.emit('error', err.response.data.error);
|
||||
if (typeof err.response.data.message !== "undefined") window.Events.emit('error', err.response.data.message);
|
||||
});
|
||||
window.$http = axiosInstance;
|
||||
|
||||
Vue.prototype.$http = axiosInstance;
|
||||
Vue.prototype.$events = window.Events;
|
||||
|
||||
|
||||
// AngularJS - Create application and load components
|
||||
const angular = require("angular");
|
||||
require("angular-resource");
|
||||
require("angular-animate");
|
||||
require("angular-sanitize");
|
||||
require("angular-ui-sortable");
|
||||
|
||||
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
|
||||
const Translations = require("./translations");
|
||||
let translator = new Translations(window.translations);
|
||||
window.trans = translator.get.bind(translator);
|
||||
|
||||
|
||||
require("./vues/vues");
|
||||
require("./components");
|
||||
|
||||
// Load in angular specific items
|
||||
const Services = require('./services');
|
||||
const Directives = require('./directives');
|
||||
const Controllers = require('./controllers');
|
||||
Services(ngApp, window.Events);
|
||||
Directives(ngApp, window.Events);
|
||||
Controllers(ngApp, window.Events);
|
||||
|
||||
@ -174,7 +182,7 @@ $('.overlay').click(function(event) {
|
||||
if(navigator.userAgent.indexOf('MSIE')!==-1
|
||||
|| navigator.appVersion.indexOf('Trident/') > 0
|
||||
|| navigator.userAgent.indexOf('Safari') !== -1){
|
||||
$('body').addClass('flexbox-support');
|
||||
document.body.classList.add('flexbox-support');
|
||||
}
|
||||
|
||||
// Page specific items
|
||||
|
@ -52,14 +52,36 @@ function editorPaste(e, editor) {
|
||||
function registerEditorShortcuts(editor) {
|
||||
// Headers
|
||||
for (let i = 1; i < 5; i++) {
|
||||
editor.addShortcut('meta+' + i, '', ['FormatBlock', false, 'h' + i]);
|
||||
editor.shortcuts.add('meta+' + i, '', ['FormatBlock', false, 'h' + (i+1)]);
|
||||
}
|
||||
|
||||
// Other block shortcuts
|
||||
editor.addShortcut('meta+q', '', ['FormatBlock', false, 'blockquote']);
|
||||
editor.addShortcut('meta+d', '', ['FormatBlock', false, 'p']);
|
||||
editor.addShortcut('meta+e', '', ['codeeditor', false, 'pre']);
|
||||
editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']);
|
||||
editor.shortcuts.add('meta+5', '', ['FormatBlock', false, 'p']);
|
||||
editor.shortcuts.add('meta+d', '', ['FormatBlock', false, 'p']);
|
||||
editor.shortcuts.add('meta+6', '', ['FormatBlock', false, 'blockquote']);
|
||||
editor.shortcuts.add('meta+q', '', ['FormatBlock', false, 'blockquote']);
|
||||
editor.shortcuts.add('meta+7', '', ['codeeditor', false, 'pre']);
|
||||
editor.shortcuts.add('meta+e', '', ['codeeditor', false, 'pre']);
|
||||
editor.shortcuts.add('meta+8', '', ['FormatBlock', false, 'code']);
|
||||
editor.shortcuts.add('meta+shift+E', '', ['FormatBlock', false, 'code']);
|
||||
// Loop through callout styles
|
||||
editor.shortcuts.add('meta+9', '', function() {
|
||||
let selectedNode = editor.selection.getNode();
|
||||
let formats = ['info', 'success', 'warning', 'danger'];
|
||||
|
||||
if (!selectedNode || selectedNode.className.indexOf('callout') === -1) {
|
||||
editor.formatter.apply('calloutinfo');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < formats.length; i++) {
|
||||
if (selectedNode.className.indexOf(formats[i]) === -1) continue;
|
||||
let newFormat = (i === formats.length -1) ? formats[0] : formats[i+1];
|
||||
editor.formatter.apply('callout' + newFormat);
|
||||
return;
|
||||
}
|
||||
editor.formatter.apply('p');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -120,7 +142,7 @@ function codePlugin() {
|
||||
$codeMirrorContainer.replaceWith($pre);
|
||||
}
|
||||
|
||||
window.tinymce.PluginManager.add('codeeditor', (editor, url) => {
|
||||
window.tinymce.PluginManager.add('codeeditor', function(editor, url) {
|
||||
|
||||
let $ = editor.$;
|
||||
|
||||
@ -173,7 +195,32 @@ function codePlugin() {
|
||||
});
|
||||
}
|
||||
|
||||
function hrPlugin() {
|
||||
window.tinymce.PluginManager.add('customhr', function (editor) {
|
||||
editor.addCommand('InsertHorizontalRule', function () {
|
||||
let hrElem = document.createElement('hr');
|
||||
let cNode = editor.selection.getNode();
|
||||
let parentNode = cNode.parentNode;
|
||||
parentNode.insertBefore(hrElem, cNode);
|
||||
});
|
||||
|
||||
editor.addButton('hr', {
|
||||
icon: 'hr',
|
||||
tooltip: 'Horizontal line',
|
||||
cmd: 'InsertHorizontalRule'
|
||||
});
|
||||
|
||||
editor.addMenuItem('hr', {
|
||||
icon: 'hr',
|
||||
text: 'Horizontal line',
|
||||
cmd: 'InsertHorizontalRule',
|
||||
context: 'insert'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
hrPlugin();
|
||||
codePlugin();
|
||||
let settings = {
|
||||
selector: '#html-editor',
|
||||
@ -207,10 +254,10 @@ module.exports = function() {
|
||||
{title: "Code Block", icon: "code", cmd: 'codeeditor', format: 'codeeditor'},
|
||||
{title: "Inline Code", icon: "code", inline: "code"},
|
||||
{title: "Callouts", items: [
|
||||
{title: "Success", block: 'p', exact: true, attributes : {'class' : 'callout success'}},
|
||||
{title: "Info", block: 'p', exact: true, attributes : {'class' : 'callout info'}},
|
||||
{title: "Warning", block: 'p', exact: true, attributes : {'class' : 'callout warning'}},
|
||||
{title: "Danger", block: 'p', exact: true, attributes : {'class' : 'callout danger'}}
|
||||
{title: "Info", format: 'calloutinfo'},
|
||||
{title: "Success", format: 'calloutsuccess'},
|
||||
{title: "Warning", format: 'calloutwarning'},
|
||||
{title: "Danger", format: 'calloutdanger'}
|
||||
]},
|
||||
],
|
||||
style_formats_merge: false,
|
||||
@ -219,6 +266,10 @@ module.exports = function() {
|
||||
alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
|
||||
aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
|
||||
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
|
||||
calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
|
||||
calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
|
||||
calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
|
||||
calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
|
||||
},
|
||||
file_browser_callback: function (field_name, url, type, win) {
|
||||
|
||||
@ -232,7 +283,7 @@ module.exports = function() {
|
||||
|
||||
if (type === 'image') {
|
||||
// Show image manager
|
||||
window.ImageManager.showExternal(function (image) {
|
||||
window.ImageManager.show(function (image) {
|
||||
|
||||
// Set popover link input to image url then fire change event
|
||||
// to ensure the new value sticks
|
||||
@ -314,7 +365,7 @@ module.exports = function() {
|
||||
icon: 'image',
|
||||
tooltip: 'Insert an image',
|
||||
onclick: function () {
|
||||
window.ImageManager.showExternal(function (image) {
|
||||
window.ImageManager.show(function (image) {
|
||||
let html = `<a href="${image.url}" target="_blank">`;
|
||||
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
|
||||
html += '</a>';
|
||||
|
@ -1,5 +1,3 @@
|
||||
"use strict";
|
||||
// Configure ZeroClipboard
|
||||
const Clipboard = require("clipboard");
|
||||
const Code = require('../code');
|
||||
|
||||
@ -161,6 +159,8 @@ let setupPageShow = window.setupPageShow = function (pageId) {
|
||||
}
|
||||
});
|
||||
|
||||
// in order to call from other places.
|
||||
window.setupPageShow.goToText = goToText;
|
||||
};
|
||||
|
||||
module.exports = setupPageShow;
|
@ -1,12 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function(ngApp, events) {
|
||||
|
||||
ngApp.factory('imageManagerService', function() {
|
||||
return {
|
||||
show: false,
|
||||
showExternal: false
|
||||
};
|
||||
});
|
||||
|
||||
};
|
138
resources/assets/js/vues/attachment-manager.js
Normal file
138
resources/assets/js/vues/attachment-manager.js
Normal file
@ -0,0 +1,138 @@
|
||||
const draggable = require('vuedraggable');
|
||||
const dropzone = require('./components/dropzone');
|
||||
|
||||
function mounted() {
|
||||
this.pageId = this.$el.getAttribute('page-id');
|
||||
this.file = this.newFile();
|
||||
|
||||
this.$http.get(window.baseUrl(`/attachments/get/page/${this.pageId}`)).then(resp => {
|
||||
this.files = resp.data;
|
||||
}).catch(err => {
|
||||
this.checkValidationErrors('get', err);
|
||||
});
|
||||
}
|
||||
|
||||
let data = {
|
||||
pageId: null,
|
||||
files: [],
|
||||
fileToEdit: null,
|
||||
file: {},
|
||||
tab: 'list',
|
||||
editTab: 'file',
|
||||
errors: {link: {}, edit: {}, delete: {}}
|
||||
};
|
||||
|
||||
const components = {dropzone, draggable};
|
||||
|
||||
let methods = {
|
||||
|
||||
newFile() {
|
||||
return {page_id: this.pageId};
|
||||
},
|
||||
|
||||
getFileUrl(file) {
|
||||
return window.baseUrl(`/attachments/${file.id}`);
|
||||
},
|
||||
|
||||
fileSortUpdate() {
|
||||
this.$http.put(window.baseUrl(`/attachments/sort/page/${this.pageId}`), {files: this.files}).then(resp => {
|
||||
this.$events.emit('success', resp.data.message);
|
||||
}).catch(err => {
|
||||
this.checkValidationErrors('sort', err);
|
||||
});
|
||||
},
|
||||
|
||||
startEdit(file) {
|
||||
this.fileToEdit = Object.assign({}, file);
|
||||
this.fileToEdit.link = file.external ? file.path : '';
|
||||
this.editTab = file.external ? 'link' : 'file';
|
||||
},
|
||||
|
||||
deleteFile(file) {
|
||||
if (!file.deleting) return file.deleting = true;
|
||||
|
||||
this.$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
|
||||
this.$events.emit('success', resp.data.message);
|
||||
this.files.splice(this.files.indexOf(file), 1);
|
||||
}).catch(err => {
|
||||
this.checkValidationErrors('delete', err)
|
||||
});
|
||||
},
|
||||
|
||||
uploadSuccess(upload) {
|
||||
this.files.push(upload.data);
|
||||
this.$events.emit('success', trans('entities.attachments_file_uploaded'));
|
||||
},
|
||||
|
||||
uploadSuccessUpdate(upload) {
|
||||
let fileIndex = this.filesIndex(upload.data);
|
||||
if (fileIndex === -1) {
|
||||
this.files.push(upload.data)
|
||||
} else {
|
||||
this.files.splice(fileIndex, 1, upload.data);
|
||||
}
|
||||
|
||||
if (this.fileToEdit && this.fileToEdit.id === upload.data.id) {
|
||||
this.fileToEdit = Object.assign({}, upload.data);
|
||||
}
|
||||
this.$events.emit('success', trans('entities.attachments_file_updated'));
|
||||
},
|
||||
|
||||
checkValidationErrors(groupName, err) {
|
||||
console.error(err);
|
||||
if (typeof err.response.data === "undefined" && typeof err.response.data.validation === "undefined") return;
|
||||
this.errors[groupName] = err.response.data.validation;
|
||||
console.log(this.errors[groupName]);
|
||||
},
|
||||
|
||||
getUploadUrl(file) {
|
||||
let url = window.baseUrl(`/attachments/upload`);
|
||||
if (typeof file !== 'undefined') url += `/${file.id}`;
|
||||
return url;
|
||||
},
|
||||
|
||||
cancelEdit() {
|
||||
this.fileToEdit = null;
|
||||
},
|
||||
|
||||
attachNewLink(file) {
|
||||
file.uploaded_to = this.pageId;
|
||||
this.$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
|
||||
this.files.push(resp.data);
|
||||
this.file = this.newFile();
|
||||
this.$events.emit('success', trans('entities.attachments_link_attached'));
|
||||
}).catch(err => {
|
||||
this.checkValidationErrors('link', err);
|
||||
});
|
||||
},
|
||||
|
||||
updateFile(file) {
|
||||
$http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
|
||||
let search = this.filesIndex(resp.data);
|
||||
if (search === -1) {
|
||||
this.files.push(resp.data);
|
||||
} else {
|
||||
this.files.splice(search, 1, resp.data);
|
||||
}
|
||||
|
||||
if (this.fileToEdit && !file.external) this.fileToEdit.link = '';
|
||||
this.fileToEdit = false;
|
||||
|
||||
this.$events.emit('success', trans('entities.attachments_updated_success'));
|
||||
}).catch(err => {
|
||||
this.checkValidationErrors('edit', err);
|
||||
});
|
||||
},
|
||||
|
||||
filesIndex(file) {
|
||||
for (let i = 0, len = this.files.length; i < len; i++) {
|
||||
if (this.files[i].id === file.id) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
data, methods, mounted, components,
|
||||
};
|
130
resources/assets/js/vues/components/autosuggest.js
Normal file
130
resources/assets/js/vues/components/autosuggest.js
Normal file
@ -0,0 +1,130 @@
|
||||
|
||||
const template = `
|
||||
<div>
|
||||
<input :value="value" :autosuggest-type="type" ref="input"
|
||||
:placeholder="placeholder" :name="name"
|
||||
@input="inputUpdate($event.target.value)" @focus="inputUpdate($event.target.value)"
|
||||
@blur="inputBlur"
|
||||
@keydown="inputKeydown"
|
||||
/>
|
||||
<ul class="suggestion-box" v-if="showSuggestions">
|
||||
<li v-for="(suggestion, i) in suggestions"
|
||||
@click="selectSuggestion(suggestion)"
|
||||
:class="{active: (i === active)}">{{suggestion}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
`;
|
||||
|
||||
function data() {
|
||||
return {
|
||||
suggestions: [],
|
||||
showSuggestions: false,
|
||||
active: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const ajaxCache = {};
|
||||
|
||||
const props = ['url', 'type', 'value', 'placeholder', 'name'];
|
||||
|
||||
function getNameInputVal(valInput) {
|
||||
let parentRow = valInput.parentNode.parentNode;
|
||||
let nameInput = parentRow.querySelector('[autosuggest-type="name"]');
|
||||
return (nameInput === null) ? '' : nameInput.value;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
|
||||
inputUpdate(inputValue) {
|
||||
this.$emit('input', inputValue);
|
||||
let params = {};
|
||||
|
||||
if (this.type === 'value') {
|
||||
let nameVal = getNameInputVal(this.$el);
|
||||
if (nameVal !== "") params.name = nameVal;
|
||||
}
|
||||
|
||||
this.getSuggestions(inputValue.slice(0, 3), params).then(suggestions => {
|
||||
if (inputValue.length === 0) {
|
||||
this.displaySuggestions(suggestions.slice(0, 6));
|
||||
return;
|
||||
}
|
||||
// Filter to suggestions containing searched term
|
||||
suggestions = suggestions.filter(item => {
|
||||
return item.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1;
|
||||
}).slice(0, 4);
|
||||
this.displaySuggestions(suggestions);
|
||||
});
|
||||
},
|
||||
|
||||
inputBlur() {
|
||||
setTimeout(() => {
|
||||
this.$emit('blur');
|
||||
this.showSuggestions = false;
|
||||
}, 100);
|
||||
},
|
||||
|
||||
inputKeydown(event) {
|
||||
if (event.keyCode === 13) event.preventDefault();
|
||||
if (!this.showSuggestions) return;
|
||||
|
||||
// Down arrow
|
||||
if (event.keyCode === 40) {
|
||||
this.active = (this.active === this.suggestions.length - 1) ? 0 : this.active+1;
|
||||
}
|
||||
// Up Arrow
|
||||
else if (event.keyCode === 38) {
|
||||
this.active = (this.active === 0) ? this.suggestions.length - 1 : this.active-1;
|
||||
}
|
||||
// Enter or tab keys
|
||||
else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
|
||||
this.selectSuggestion(this.suggestions[this.active]);
|
||||
}
|
||||
// Escape key
|
||||
else if (event.keyCode === 27) {
|
||||
this.showSuggestions = false;
|
||||
}
|
||||
},
|
||||
|
||||
displaySuggestions(suggestions) {
|
||||
if (suggestions.length === 0) {
|
||||
this.suggestions = [];
|
||||
this.showSuggestions = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.suggestions = suggestions;
|
||||
this.showSuggestions = true;
|
||||
this.active = 0;
|
||||
},
|
||||
|
||||
selectSuggestion(suggestion) {
|
||||
this.$refs.input.value = suggestion;
|
||||
this.$refs.input.focus();
|
||||
this.$emit('input', suggestion);
|
||||
this.showSuggestions = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get suggestions from BookStack. Store and use local cache if already searched.
|
||||
* @param {String} input
|
||||
* @param {Object} params
|
||||
*/
|
||||
getSuggestions(input, params) {
|
||||
params.search = input;
|
||||
let cacheKey = `${this.url}:${JSON.stringify(params)}`;
|
||||
|
||||
if (typeof ajaxCache[cacheKey] !== "undefined") return Promise.resolve(ajaxCache[cacheKey]);
|
||||
|
||||
return this.$http.get(this.url, {params}).then(resp => {
|
||||
ajaxCache[cacheKey] = resp.data;
|
||||
return resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const computed = [];
|
||||
|
||||
module.exports = {template, data, props, methods, computed};
|
60
resources/assets/js/vues/components/dropzone.js
Normal file
60
resources/assets/js/vues/components/dropzone.js
Normal file
@ -0,0 +1,60 @@
|
||||
const DropZone = require("dropzone");
|
||||
|
||||
const template = `
|
||||
<div class="dropzone-container">
|
||||
<div class="dz-message">{{placeholder}}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const props = ['placeholder', 'uploadUrl', 'uploadedTo'];
|
||||
|
||||
// TODO - Remove jQuery usage
|
||||
function mounted() {
|
||||
let container = this.$el;
|
||||
let _this = this;
|
||||
new DropZone(container, {
|
||||
url: function() {
|
||||
return _this.uploadUrl;
|
||||
},
|
||||
init: function () {
|
||||
let dz = this;
|
||||
|
||||
dz.on('sending', function (file, xhr, data) {
|
||||
let token = window.document.querySelector('meta[name=token]').getAttribute('content');
|
||||
data.append('_token', token);
|
||||
let uploadedTo = typeof _this.uploadedTo === 'undefined' ? 0 : _this.uploadedTo;
|
||||
data.append('uploaded_to', uploadedTo);
|
||||
});
|
||||
|
||||
dz.on('success', function (file, data) {
|
||||
_this.$emit('success', {file, data});
|
||||
$(file.previewElement).fadeOut(400, function () {
|
||||
dz.removeFile(file);
|
||||
});
|
||||
});
|
||||
|
||||
dz.on('error', function (file, errorMessage, xhr) {
|
||||
_this.$emit('error', {file, errorMessage, xhr});
|
||||
console.log(errorMessage);
|
||||
console.log(xhr);
|
||||
function setMessage(message) {
|
||||
$(file.previewElement).find('[data-dz-errormessage]').text(message);
|
||||
}
|
||||
|
||||
if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
|
||||
if (errorMessage.file) setMessage(errorMessage.file[0]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function data() {
|
||||
return {}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
template,
|
||||
props,
|
||||
mounted,
|
||||
data,
|
||||
};
|
178
resources/assets/js/vues/image-manager.js
Normal file
178
resources/assets/js/vues/image-manager.js
Normal file
@ -0,0 +1,178 @@
|
||||
const dropzone = require('./components/dropzone');
|
||||
|
||||
let page = 0;
|
||||
let previousClickTime = 0;
|
||||
let previousClickImage = 0;
|
||||
let dataLoaded = false;
|
||||
let callback = false;
|
||||
let baseUrl = '';
|
||||
|
||||
let preSearchImages = [];
|
||||
let preSearchHasMore = false;
|
||||
|
||||
const data = {
|
||||
images: [],
|
||||
|
||||
imageType: false,
|
||||
uploadedTo: false,
|
||||
|
||||
selectedImage: false,
|
||||
dependantPages: false,
|
||||
showing: false,
|
||||
view: 'all',
|
||||
hasMore: false,
|
||||
searching: false,
|
||||
searchTerm: '',
|
||||
|
||||
imageUpdateSuccess: false,
|
||||
imageDeleteSuccess: false,
|
||||
};
|
||||
|
||||
const methods = {
|
||||
|
||||
show(providedCallback) {
|
||||
callback = providedCallback;
|
||||
this.showing = true;
|
||||
this.$el.children[0].components.overlay.show();
|
||||
|
||||
// Get initial images if they have not yet been loaded in.
|
||||
if (dataLoaded) return;
|
||||
this.fetchData();
|
||||
dataLoaded = true;
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.showing = false;
|
||||
this.$el.children[0].components.overlay.hide();
|
||||
},
|
||||
|
||||
fetchData() {
|
||||
let url = baseUrl + page;
|
||||
let query = {};
|
||||
if (this.uploadedTo !== false) query.page_id = this.uploadedTo;
|
||||
if (this.searching) query.term = this.searchTerm;
|
||||
|
||||
this.$http.get(url, {params: query}).then(response => {
|
||||
this.images = this.images.concat(response.data.images);
|
||||
this.hasMore = response.data.hasMore;
|
||||
page++;
|
||||
});
|
||||
},
|
||||
|
||||
setView(viewName) {
|
||||
this.cancelSearch();
|
||||
this.images = [];
|
||||
this.hasMore = false;
|
||||
page = 0;
|
||||
this.view = viewName;
|
||||
baseUrl = window.baseUrl(`/images/${this.imageType}/${viewName}/`);
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
searchImages() {
|
||||
if (this.searchTerm === '') return this.cancelSearch();
|
||||
|
||||
// Cache current settings for later
|
||||
if (!this.searching) {
|
||||
preSearchImages = this.images;
|
||||
preSearchHasMore = this.hasMore;
|
||||
}
|
||||
|
||||
this.searching = true;
|
||||
this.images = [];
|
||||
this.hasMore = false;
|
||||
page = 0;
|
||||
baseUrl = window.baseUrl(`/images/${this.imageType}/search/`);
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
cancelSearch() {
|
||||
this.searching = false;
|
||||
this.searchTerm = '';
|
||||
this.images = preSearchImages;
|
||||
this.hasMore = preSearchHasMore;
|
||||
},
|
||||
|
||||
imageSelect(image) {
|
||||
let dblClickTime = 300;
|
||||
let currentTime = Date.now();
|
||||
let timeDiff = currentTime - previousClickTime;
|
||||
let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage;
|
||||
|
||||
if (isDblClick) {
|
||||
this.callbackAndHide(image);
|
||||
} else {
|
||||
this.selectedImage = image;
|
||||
this.dependantPages = false;
|
||||
}
|
||||
|
||||
previousClickTime = currentTime;
|
||||
previousClickImage = image.id;
|
||||
},
|
||||
|
||||
callbackAndHide(imageResult) {
|
||||
if (callback) callback(imageResult);
|
||||
this.hide();
|
||||
},
|
||||
|
||||
saveImageDetails() {
|
||||
let url = window.baseUrl(`/images/update/${this.selectedImage.id}`);
|
||||
this.$http.put(url, this.selectedImage).then(response => {
|
||||
this.$events.emit('success', trans('components.image_update_success'));
|
||||
}).catch(error => {
|
||||
if (error.response.status === 422) {
|
||||
let errors = error.response.data;
|
||||
let message = '';
|
||||
Object.keys(errors).forEach((key) => {
|
||||
message += errors[key].join('\n');
|
||||
});
|
||||
this.$events.emit('error', message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
deleteImage() {
|
||||
let force = this.dependantPages !== false;
|
||||
let url = window.baseUrl('/images/' + this.selectedImage.id);
|
||||
if (force) url += '?force=true';
|
||||
this.$http.delete(url).then(response => {
|
||||
this.images.splice(this.images.indexOf(this.selectedImage), 1);
|
||||
this.selectedImage = false;
|
||||
this.$events.emit('success', trans('components.image_delete_success'));
|
||||
}).catch(error=> {
|
||||
if (error.response.status === 400) {
|
||||
this.dependantPages = error.response.data;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getDate(stringDate) {
|
||||
return new Date(stringDate);
|
||||
},
|
||||
|
||||
uploadSuccess(event) {
|
||||
this.images.unshift(event.data);
|
||||
this.$events.emit('success', trans('components.image_upload_success'));
|
||||
},
|
||||
};
|
||||
|
||||
const computed = {
|
||||
uploadUrl() {
|
||||
return window.baseUrl(`/images/${this.imageType}/upload`);
|
||||
}
|
||||
};
|
||||
|
||||
function mounted() {
|
||||
window.ImageManager = this;
|
||||
this.imageType = this.$el.getAttribute('image-type');
|
||||
this.uploadedTo = this.$el.getAttribute('uploaded-to');
|
||||
baseUrl = window.baseUrl('/images/' + this.imageType + '/all/')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mounted,
|
||||
methods,
|
||||
data,
|
||||
computed,
|
||||
components: {dropzone},
|
||||
};
|
@ -149,7 +149,7 @@ let methods = {
|
||||
|
||||
updateSearch(e) {
|
||||
e.preventDefault();
|
||||
window.location = '/search?term=' + encodeURIComponent(this.termString);
|
||||
window.location = window.baseUrl('/search?term=' + encodeURIComponent(this.termString));
|
||||
},
|
||||
|
||||
enableDate(optionName) {
|
||||
@ -192,4 +192,4 @@ function created() {
|
||||
|
||||
module.exports = {
|
||||
data, computed, methods, created
|
||||
};
|
||||
};
|
||||
|
68
resources/assets/js/vues/tag-manager.js
Normal file
68
resources/assets/js/vues/tag-manager.js
Normal file
@ -0,0 +1,68 @@
|
||||
const draggable = require('vuedraggable');
|
||||
const autosuggest = require('./components/autosuggest');
|
||||
|
||||
let data = {
|
||||
pageId: false,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const components = {draggable, autosuggest};
|
||||
const directives = {};
|
||||
|
||||
let computed = {};
|
||||
|
||||
let methods = {
|
||||
|
||||
addEmptyTag() {
|
||||
this.tags.push({name: '', value: '', key: Math.random().toString(36).substring(7)});
|
||||
},
|
||||
|
||||
/**
|
||||
* When an tag changes check if another empty editable field needs to be added onto the end.
|
||||
* @param tag
|
||||
*/
|
||||
tagChange(tag) {
|
||||
let tagPos = this.tags.indexOf(tag);
|
||||
if (tagPos === this.tags.length-1 && (tag.name !== '' || tag.value !== '')) this.addEmptyTag();
|
||||
},
|
||||
|
||||
/**
|
||||
* When an tag field loses focus check the tag to see if its
|
||||
* empty and therefore could be removed from the list.
|
||||
* @param tag
|
||||
*/
|
||||
tagBlur(tag) {
|
||||
let isLast = (this.tags.indexOf(tag) === this.tags.length-1);
|
||||
if (tag.name !== '' || tag.value !== '' || isLast) return;
|
||||
let cPos = this.tags.indexOf(tag);
|
||||
this.tags.splice(cPos, 1);
|
||||
},
|
||||
|
||||
removeTag(tag) {
|
||||
let tagPos = this.tags.indexOf(tag);
|
||||
if (tagPos === -1) return;
|
||||
this.tags.splice(tagPos, 1);
|
||||
},
|
||||
|
||||
getTagFieldName(index, key) {
|
||||
return `tags[${index}][${key}]`;
|
||||
},
|
||||
};
|
||||
|
||||
function mounted() {
|
||||
this.pageId = Number(this.$el.getAttribute('page-id'));
|
||||
|
||||
let url = window.baseUrl(`/ajax/tags/get/page/${this.pageId}`);
|
||||
this.$http.get(url).then(response => {
|
||||
let tags = response.data;
|
||||
for (let i = 0, len = tags.length; i < len; i++) {
|
||||
tags[i].key = Math.random().toString(36).substring(7);
|
||||
}
|
||||
this.tags = tags;
|
||||
this.addEmptyTag();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data, computed, methods, mounted, components, directives
|
||||
};
|
@ -6,16 +6,19 @@ function exists(id) {
|
||||
|
||||
let vueMapping = {
|
||||
'search-system': require('./search'),
|
||||
'entity-dashboard': require('./entity-search'),
|
||||
'code-editor': require('./code-editor')
|
||||
'entity-dashboard': require('./entity-dashboard'),
|
||||
'code-editor': require('./code-editor'),
|
||||
'image-manager': require('./image-manager'),
|
||||
'tag-manager': require('./tag-manager'),
|
||||
'attachment-manager': require('./attachment-manager'),
|
||||
};
|
||||
|
||||
window.vues = {};
|
||||
|
||||
Object.keys(vueMapping).forEach(id => {
|
||||
if (exists(id)) {
|
||||
let config = vueMapping[id];
|
||||
config.el = '#' + id;
|
||||
window.vues[id] = new Vue(config);
|
||||
}
|
||||
});
|
||||
let ids = Object.keys(vueMapping);
|
||||
for (let i = 0, len = ids.length; i < len; i++) {
|
||||
if (!exists(ids[i])) continue;
|
||||
let config = vueMapping[ids[i]];
|
||||
config.el = '#' + ids[i];
|
||||
window.vues[ids[i]] = new Vue(config);
|
||||
}
|
Reference in New Issue
Block a user