X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/bc067e9ad41804de26ec37fab90ec413b48b2270..ab7f5def04a667ce4e6a6aff8b51d27b82550f42:/resources/assets/js/directives.js diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index d783fd682..08b82357f 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -1,158 +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: ` -
', '
');}; cm.setOption('extraKeys', extraKeys); // Update data on content change @@ -309,6 +155,74 @@ 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 + start.length}); + } + + 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; @@ -342,7 +256,7 @@ module.exports = function (ngApp, events) { // Show the popup link selector and insert a link when finished function showLinkSelector() { let cursorPos = cm.getCursor('from'); - window.showEntityLinkSelector(entity => { + window.EntitySelectorPopup.show(entity => { let selectedText = cm.getSelection() || entity.name; let newText = `[${selectedText}](${entity.link})`; cm.focus(); @@ -351,10 +265,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(); @@ -466,333 +390,4 @@ 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', - 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 { - restrict: 'A', - scope: true, - link: function (scope, element, attrs) { - scope.loading = true; - scope.entityResults = false; - scope.search = ''; - - // Add input for forms - const input = element.find('[entity-selector-input]').first(); - - // Detect double click events - let 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, isDoubleClick()); - }); - element.on('click', '[data-entity-type]', function(event) { - itemSelect($(this), isDoubleClick()); - }); - - // Select entity action - function itemSelect(item, doubleClick) { - let entityType = item.attr('data-entity-type'); - let entityId = item.attr('data-entity-id'); - 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 - function getSearchUrl() { - let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter'); - return window.baseUrl(`/ajax/search/entities?types=${types}`); - } - - // Get initial contents - $http.get(getSearchUrl()).then(resp => { - scope.entityResults = $sce.trustAsHtml(resp.data); - scope.loading = false; - }); - - // Search when typing - scope.searchEntities = function() { - scope.loading = true; - input.val(''); - let url = getSearchUrl() + '&term=' + encodeURIComponent(scope.search); - $http.get(url).then(resp => { - scope.entityResults = $sce.trustAsHtml(resp.data); - scope.loading = false; - }); - }; - } - }; - }]); };