From: Dan Brown Date: Sat, 3 Sep 2016 09:40:27 +0000 (+0100) Subject: Merge branch 'page_link_selector' X-Git-Tag: v0.12.0~1^2~3 X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/0dbb8babeee2f815f929b56ae908810a33a95c4c?hp=-c Merge branch 'page_link_selector' --- 0dbb8babeee2f815f929b56ae908810a33a95c4c diff --combined resources/assets/js/controllers.js index 28a45e591,beb2fe93e..fcaba2914 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@@ -69,7 -69,7 +69,7 @@@ module.exports = function (ngApp, event */ function callbackAndHide(returnData) { if (callback) callback(returnData); - $scope.showing = false; + $scope.hide(); } /** @@@ -109,6 -109,7 +109,7 @@@ function show(doneCallback) { callback = doneCallback; $scope.showing = true; + $('#image-manager').find('.overlay').css('display', 'flex').hide().fadeIn(240); // Get initial images if they have not yet been loaded in. if (!dataLoaded) { fetchData(); @@@ -131,6 -132,7 +132,7 @@@ */ $scope.hide = function () { $scope.showing = false; + $('#image-manager').find('.overlay').fadeOut(240); }; var baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/'); @@@ -357,6 -359,8 +359,6 @@@ /** * Save a draft update into the system via an AJAX request. - * @param title - * @param html */ function saveDraft() { var data = { @@@ -371,17 -375,9 +373,17 @@@ var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate(); $scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm'); if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true; + showDraftSaveNotification(); }); } + function showDraftSaveNotification() { + $scope.draftUpdated = true; + $timeout(() => { + $scope.draftUpdated = false; + }, 2000) + } + $scope.forceDraftSave = function() { saveDraft(); }; diff --combined resources/assets/js/directives.js index 286854832,4e1bb0dfd..1271b3112 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@@ -271,8 -271,6 +271,6 @@@ module.exports = function (ngApp, event scope.mdModel = content; scope.mdChange(markdown(content)); - console.log('test'); - element.on('change input', (event) => { content = element.val(); $timeout(() => { @@@ -304,6 -302,7 +302,7 @@@ const input = element.find('[markdown-input] textarea').first(); const display = element.find('.markdown-display').first(); const insertImage = element.find('button[data-action="insertImage"]'); + const insertEntityLink = element.find('button[data-action="insertEntityLink"]') let currentCaretPos = 0; @@@ -355,6 -354,13 +354,13 @@@ input[0].selectionEnd = caretPos + ('![](http://'.length); return; } + + // Insert entity link shortcut + if (event.which === 75 && event.ctrlKey && event.shiftKey) { + showLinkSelector(); + return; + } + // Pass key presses to controller via event scope.$emit('editor-keydown', event); }); @@@ -370,83 -376,26 +376,103 @@@ }); }); + function showLinkSelector() { + window.showEntityLinkSelector((entity) => { + let selectionStart = currentCaretPos; + let selectionEnd = input[0].selectionEnd; + let textSelected = (selectionEnd !== selectionStart); + let currentContent = input.val(); + + if (textSelected) { + let selectedText = currentContent.substring(selectionStart, selectionEnd); + let linkText = `[${selectedText}](${entity.link})`; + input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionEnd)); + } else { + let linkText = ` [${entity.name}](${entity.link}) `; + input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionStart)) + } + input.change(); + }); + } + insertEntityLink.click(showLinkSelector); + + // Upload and insert image on paste + function editorPaste(e) { + e = e.originalEvent; + if (!e.clipboardData) return + var items = e.clipboardData.items; + if (!items) return; + for (var i = 0; i < items.length; i++) { + uploadImage(items[i].getAsFile()); + } + } + + input.on('paste', editorPaste); + + // Handle image drop, Uploads images to BookStack. + function handleImageDrop(event) { + event.stopPropagation(); + event.preventDefault(); + let files = event.originalEvent.dataTransfer.files; + for (let i = 0; i < files.length; i++) { + uploadImage(files[i]); + } + } + + input.on('drop', handleImageDrop); + + // Handle image upload and add image into markdown content + function uploadImage(file) { + if (file.type.indexOf('image') !== 0) return; + var formData = new FormData(); + var ext = 'png'; + var xhr = new XMLHttpRequest(); + + if (file.name) { + var fileNameMatches = file.name.match(/\.(.+)$/); + if (fileNameMatches) { + ext = fileNameMatches[1]; + } + } + + // Insert image into markdown + let id = "image-" + Math.random().toString(16).slice(2); + let selectStart = input[0].selectionStart; + let selectEnd = input[0].selectionEnd; + let content = input[0].value; + let selectText = content.substring(selectStart, selectEnd); + let placeholderImage = `/loading.gif#upload${id}`; + let innerContent = ((selectEnd > selectStart) ? `![${selectText}]` : '![]') + `(${placeholderImage})`; + input[0].value = content.substring(0, selectStart) + innerContent + content.substring(selectEnd); + + input.focus(); + input[0].selectionStart = selectStart; + input[0].selectionEnd = selectStart; + + let remoteFilename = "image-" + Date.now() + "." + ext; + formData.append('file', file, remoteFilename); + formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content')); + + xhr.open('POST', window.baseUrl('/images/gallery/upload')); + xhr.onload = function () { + let selectStart = input[0].selectionStart; + if (xhr.status === 200 || xhr.status === 201) { + var result = JSON.parse(xhr.responseText); + input[0].value = input[0].value.replace(placeholderImage, result.url); + input.change(); + } else { + console.log('An error occurred uploading the image'); + console.log(xhr.responseText); + input[0].value = input[0].value.replace(innerContent, ''); + input.change(); + } + input.focus(); + input[0].selectionStart = selectStart; + input[0].selectionEnd = selectStart; + }; + xhr.send(formData); + } + } } }]); @@@ -677,6 -626,58 +703,58 @@@ } }]); + ngApp.directive('entityLinkSelector', [function($http) { + return { + restict: 'A', + link: function(scope, element, attrs) { + + const selectButton = element.find('.entity-link-selector-confirm'); + let callback = false; + let entitySelection = null; + + // Handle entity selection change, Stores the selected entity locally + function entitySelectionChange(entity) { + entitySelection = entity; + if (entity === null) { + selectButton.attr('disabled', 'true'); + } else { + selectButton.removeAttr('disabled'); + } + } + events.listen('entity-select-change', entitySelectionChange); + + // Handle selection confirm button click + selectButton.click(event => { + hide(); + if (entitySelection !== null) callback(entitySelection); + }); + + // Show selector interface + function show() { + element.fadeIn(240); + } + + // Hide selector interface + function hide() { + element.fadeOut(240); + } + + // Listen to confirmation of entity selections (doubleclick) + events.listen('entity-select-confirm', entity => { + hide(); + callback(entity); + }); + + // Show entity selector, Accessible globally, and store the callback + window.showEntityLinkSelector = function(passedCallback) { + show(); + callback = passedCallback; + }; + + } + }; + }]); + ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) { return { @@@ -690,26 -691,60 +768,60 @@@ // Add input for forms const input = element.find('[entity-selector-input]').first(); + // Detect double click events + var lastClick = 0; + function isDoubleClick() { + let now = Date.now(); + let answer = now - lastClick < 300; + lastClick = now; + return answer; + } + // Listen to entity item clicks element.on('click', '.entity-list a', function(event) { event.preventDefault(); event.stopPropagation(); let item = $(this).closest('[data-entity-type]'); - itemSelect(item); + itemSelect(item, isDoubleClick()); }); element.on('click', '[data-entity-type]', function(event) { - itemSelect($(this)); + itemSelect($(this), isDoubleClick()); }); // Select entity action - function itemSelect(item) { + function itemSelect(item, doubleClick) { let entityType = item.attr('data-entity-type'); let entityId = item.attr('data-entity-id'); - let isSelected = !item.hasClass('selected'); + let isSelected = !item.hasClass('selected') || doubleClick; element.find('.selected').removeClass('selected').removeClass('primary-background'); if (isSelected) item.addClass('selected').addClass('primary-background'); let newVal = isSelected ? `${entityType}:${entityId}` : ''; input.val(newVal); + + if (!isSelected) { + events.emit('entity-select-change', null); + } + + if (!doubleClick && !isSelected) return; + + let link = item.find('.entity-list-item-link').attr('href'); + let name = item.find('.entity-list-item-name').text(); + + if (doubleClick) { + events.emit('entity-select-confirm', { + id: Number(entityId), + name: name, + link: link + }); + } + + if (isSelected) { + events.emit('entity-select-change', { + id: Number(entityId), + name: name, + link: link + }); + } } // Get search url with correct types diff --combined resources/assets/js/pages/page-form.js index 15fcdbb8e,d8f298959..daf9639d7 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@@ -1,6 -1,6 +1,6 @@@ "use strict"; -function editorPaste(e) { +function editorPaste(e, editor) { if (!e.clipboardData) return var items = e.clipboardData.items; if (!items) return; @@@ -32,7 -32,7 +32,7 @@@ var result = JSON.parse(xhr.responseText); editor.dom.setAttrib(id, 'src', result.url); } else { - console.log('An error occured uploading the image'); + console.log('An error occurred uploading the image'); console.log(xhr.responseText); editor.dom.remove(id); } @@@ -95,20 -95,38 +95,38 @@@ var mceOptions = module.exports = alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'}, }, file_browser_callback: function (field_name, url, type, win) { - window.ImageManager.showExternal(function (image) { - win.document.getElementById(field_name).value = image.url; - if ("createEvent" in document) { - var evt = document.createEvent("HTMLEvents"); - evt.initEvent("change", false, true); - win.document.getElementById(field_name).dispatchEvent(evt); - } else { - win.document.getElementById(field_name).fireEvent("onchange"); - } - var html = ''; - html += '' + image.name + ''; - html += ''; - win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html); - }); + + if (type === 'file') { + window.showEntityLinkSelector(function(entity) { + var originalField = win.document.getElementById(field_name); + originalField.value = entity.link; + $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name); + }); + } + + if (type === 'image') { + // Show image manager + window.ImageManager.showExternal(function (image) { + + // Set popover link input to image url then fire change event + // to ensure the new value sticks + win.document.getElementById(field_name).value = image.url; + if ("createEvent" in document) { + var evt = document.createEvent("HTMLEvents"); + evt.initEvent("change", false, true); + win.document.getElementById(field_name).dispatchEvent(evt); + } else { + win.document.getElementById(field_name).fireEvent("onchange"); + } + + // Replace the actively selected content with the linked image + var html = ''; + html += '' + image.name + ''; + html += ''; + win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html); + }); + } + }, paste_preprocess: function (plugin, args) { var content = args.content; @@@ -119,6 -137,8 +137,8 @@@ extraSetups: [], setup: function (editor) { + // Run additional setup actions + // Used by the angular side of things for (var i = 0; i < mceOptions.extraSetups.length; i++) { mceOptions.extraSetups[i](editor); } @@@ -181,8 -201,6 +201,8 @@@ }); // Paste image-uploads - editor.on('paste', editorPaste); + editor.on('paste', function(event) { + editorPaste(event, editor); + }); } }; diff --combined resources/assets/sass/styles.scss index 210432588,90b452a8e..7d33bd0a6 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@@ -12,7 -12,7 +12,7 @@@ @import "animations"; @import "tinymce"; @import "highlightjs"; - @import "image-manager"; + @import "components"; @import "header"; @import "lists"; @import "pages"; @@@ -72,7 -72,7 +72,7 @@@ body.dragging, body.dragging * border-radius: 3px; box-shadow: $bs-med; z-index: 999999; - display: table; + display: block; cursor: pointer; max-width: 480px; i, span { diff --combined resources/views/pages/form.blade.php index 5aee9c596,e3c145aa2..0e0c3672e --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@@ -13,9 -13,8 +13,9 @@@
-