2 var DropZone = require('dropzone');
3 var markdown = require('marked');
5 var toggleSwitchTemplate = require('./components/toggle-switch.html');
6 var imagePickerTemplate = require('./components/image-picker.html');
7 var dropZoneTemplate = require('./components/drop-zone.html');
9 module.exports = function (ngApp, events) {
13 * Has basic on/off functionality.
14 * Use string values of 'true' & 'false' to dictate the current state.
16 ngApp.directive('toggleSwitch', function () {
19 template: toggleSwitchTemplate,
21 link: function (scope, element, attrs) {
22 scope.name = attrs.name;
23 scope.value = attrs.value;
24 scope.isActive = scope.value == true && scope.value != 'false';
25 scope.value = (scope.value == true && scope.value != 'false') ? 'true' : 'false';
27 scope.switch = function () {
28 scope.isActive = !scope.isActive;
29 scope.value = scope.isActive ? 'true' : 'false';
39 * Is a simple front-end interface that connects to an ImageManager if present.
41 ngApp.directive('imagePicker', ['$http', 'imageManagerService', function ($http, imageManagerService) {
44 template: imagePickerTemplate,
56 link: function (scope, element, attrs) {
57 var usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false';
58 scope.image = scope.currentImage;
59 scope.value = scope.currentImage || '';
60 if (usingIds) scope.value = scope.currentId;
62 function setImage(imageModel, imageUrl) {
63 scope.image = imageUrl;
64 scope.value = usingIds ? imageModel.id : imageUrl;
67 scope.reset = function () {
68 setImage({id: 0}, scope.defaultImage);
71 scope.remove = function () {
76 scope.showImageManager = function () {
77 imageManagerService.show((image) => {
78 scope.updateImageFromModel(image);
82 scope.updateImageFromModel = function (model) {
83 var isResized = scope.resizeWidth && scope.resizeHeight;
87 setImage(model, model.url);
92 var cropped = scope.resizeCrop ? 'true' : 'false';
93 var requestString = '/images/thumb/' + model.id + '/' + scope.resizeWidth + '/' + scope.resizeHeight + '/' + cropped;
94 $http.get(requestString).then((response) => {
95 setImage(model, response.data.url);
105 * Used for uploading images
107 ngApp.directive('dropZone', [function () {
110 template: dropZoneTemplate,
117 link: function (scope, element, attrs) {
118 var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
119 url: scope.uploadUrl,
122 dz.on('sending', function (file, xhr, data) {
123 var token = window.document.querySelector('meta[name=token]').getAttribute('content');
124 data.append('_token', token);
125 var uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo;
126 data.append('uploaded_to', uploadedTo);
128 if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess);
129 dz.on('success', function (file, data) {
130 $(file.previewElement).fadeOut(400, function () {
134 if (typeof scope.eventError !== 'undefined') dz.on('error', scope.eventError);
135 dz.on('error', function (file, errorMessage, xhr) {
136 console.log(errorMessage);
138 function setMessage(message) {
139 $(file.previewElement).find('[data-dz-errormessage]').text(message);
142 if (xhr.status === 413) setMessage('The server does not allow uploads of this size. Please try a smaller file.');
143 if (errorMessage.file) setMessage(errorMessage.file[0]);
154 * Provides some simple logic to create small dropdown menus
156 ngApp.directive('dropdown', [function () {
159 link: function (scope, element, attrs) {
160 var menu = element.find('ul');
161 element.find('[dropdown-toggle]').on('click', function () {
162 menu.show().addClass('anim menuIn');
163 element.mouseleave(function () {
165 menu.removeClass('anim menuIn');
174 * An angular wrapper around the tinyMCE editor.
176 ngApp.directive('tinymce', ['$timeout', function ($timeout) {
184 link: function (scope, element, attrs) {
186 function tinyMceSetup(editor) {
187 editor.on('ExecCommand change NodeChange ObjectResized', (e) => {
188 var content = editor.getContent();
190 scope.mceModel = content;
192 scope.mceChange(content);
195 editor.on('keydown', (event) => {
196 scope.$emit('editor-keydown', event);
199 editor.on('init', (e) => {
200 scope.mceModel = editor.getContent();
203 scope.$on('html-update', (event, value) => {
204 editor.setContent(value);
205 editor.selection.select(editor.getBody(), true);
206 editor.selection.collapse(false);
207 scope.mceModel = editor.getContent();
211 scope.tinymce.extraSetups.push(tinyMceSetup);
213 // Custom tinyMCE plugins
214 tinymce.PluginManager.add('customhr', function (editor) {
215 editor.addCommand('InsertHorizontalRule', function () {
216 var hrElem = document.createElement('hr');
217 var cNode = editor.selection.getNode();
218 var parentNode = cNode.parentNode;
219 parentNode.insertBefore(hrElem, cNode);
222 editor.addButton('hr', {
224 tooltip: 'Horizontal line',
225 cmd: 'InsertHorizontalRule'
228 editor.addMenuItem('hr', {
230 text: 'Horizontal line',
231 cmd: 'InsertHorizontalRule',
236 tinymce.init(scope.tinymce);
243 * Handles the logic for just the editor input field.
245 ngApp.directive('markdownInput', ['$timeout', function ($timeout) {
252 link: function (scope, element, attrs) {
254 // Set initial model content
255 element = element.find('textarea').first();
256 let content = element.val();
257 scope.mdModel = content;
258 scope.mdChange(markdown(content));
262 element.on('change input', (event) => {
263 content = element.val();
265 scope.mdModel = content;
266 scope.mdChange(markdown(content));
270 scope.$on('markdown-update', (event, value) => {
272 scope.mdModel = value;
273 scope.mdChange(markdown(value));
282 * Handles all functionality of the markdown editor.
284 ngApp.directive('markdownEditor', ['$timeout', function ($timeout) {
287 link: function (scope, element, attrs) {
290 const input = element.find('[markdown-input] textarea').first();
291 const display = element.find('.markdown-display').first();
292 const insertImage = element.find('button[data-action="insertImage"]');
294 let currentCaretPos = 0;
296 input.blur(event => {
297 currentCaretPos = input[0].selectionStart;
301 let inputScrollHeight,
306 function setScrollHeights() {
307 inputScrollHeight = input[0].scrollHeight;
308 inputHeight = input.height();
309 displayScrollHeight = display[0].scrollHeight;
310 displayHeight = display.height();
316 window.addEventListener('resize', setScrollHeights);
317 let scrollDebounceTime = 800;
319 input.on('scroll', event => {
320 let now = Date.now();
321 if (now - lastScroll > scrollDebounceTime) {
324 let scrollPercent = (input.scrollTop() / (inputScrollHeight - inputHeight));
325 let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent;
326 display.scrollTop(displayScrollY);
330 // Editor key-presses
331 input.keydown(event => {
332 // Insert image shortcut
333 if (event.which === 73 && event.ctrlKey && event.shiftKey) {
334 event.preventDefault();
335 var caretPos = input[0].selectionStart;
336 var currentContent = input.val();
337 var mdImageText = "";
338 input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
340 input[0].selectionStart = caretPos + (";
341 input[0].selectionEnd = caretPos + (';
344 // Pass key presses to controller via event
345 scope.$emit('editor-keydown', event);
348 // Insert image from image manager
349 insertImage.click(event => {
350 window.ImageManager.showExternal(image => {
351 var caretPos = currentCaretPos;
352 var currentContent = input.val();
353 var mdImageText = "";
354 input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
364 * Page Editor Toolbox
365 * Controls all functionality for the sliding toolbox
366 * on the page edit view.
368 ngApp.directive('toolbox', [function () {
371 link: function (scope, elem, attrs) {
373 // Get common elements
374 const $buttons = elem.find('[tab-button]');
375 const $content = elem.find('[tab-content]');
376 const $toggle = elem.find('[toolbox-toggle]');
378 // Handle toolbox toggle click
379 $toggle.click((e) => {
380 elem.toggleClass('open');
383 // Set an active tab/content by name
384 function setActive(tabName, openToolbox) {
385 $buttons.removeClass('active');
387 $buttons.filter(`[tab-button="${tabName}"]`).addClass('active');
388 $content.filter(`[tab-content="${tabName}"]`).show();
389 if (openToolbox) elem.addClass('open');
392 // Set the first tab content active on load
393 setActive($content.first().attr('tab-content'), false);
395 // Handle tab button click
396 $buttons.click(function (e) {
397 let name = $(this).attr('tab-button');
398 setActive(name, true);
405 * Tag Autosuggestions
406 * Listens to child inputs and provides autosuggestions depending on field type
407 * and input. Suggestions provided by server.
409 ngApp.directive('tagAutosuggestions', ['$http', function ($http) {
412 link: function (scope, elem, attrs) {
414 // Local storage for quick caching.
415 const localCache = {};
417 // Create suggestion element
418 const suggestionBox = document.createElement('ul');
419 suggestionBox.className = 'suggestion-box';
420 suggestionBox.style.position = 'absolute';
421 suggestionBox.style.display = 'none';
422 const $suggestionBox = $(suggestionBox);
424 // General state tracking
425 let isShowing = false;
426 let currentInput = false;
429 // Listen to input events on autosuggest fields
430 elem.on('input focus', '[autosuggest]', function (event) {
431 let $input = $(this);
432 let val = $input.val();
433 let url = $input.attr('autosuggest');
434 let type = $input.attr('autosuggest-type');
436 // Add name param to request if for a value
437 if (type.toLowerCase() === 'value') {
438 let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
439 let nameVal = $nameInput.val();
440 if (nameVal !== '') {
441 url += '?name=' + encodeURIComponent(nameVal);
445 let suggestionPromise = getSuggestions(val.slice(0, 3), url);
446 suggestionPromise.then(suggestions => {
447 if (val.length === 0) {
448 displaySuggestions($input, suggestions.slice(0, 6));
450 suggestions = suggestions.filter(item => {
451 return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
453 displaySuggestions($input, suggestions);
458 // Hide autosuggestions when input loses focus.
459 // Slight delay to allow clicks.
460 let lastFocusTime = 0;
461 elem.on('blur', '[autosuggest]', function (event) {
462 let startTime = Date.now();
464 if (lastFocusTime < startTime) {
465 $suggestionBox.hide();
470 elem.on('focus', '[autosuggest]', function (event) {
471 lastFocusTime = Date.now();
474 elem.on('keydown', '[autosuggest]', function (event) {
475 if (!isShowing) return;
477 let suggestionElems = suggestionBox.childNodes;
478 let suggestCount = suggestionElems.length;
481 if (event.keyCode === 40) {
482 let newActive = (active === suggestCount - 1) ? 0 : active + 1;
483 changeActiveTo(newActive, suggestionElems);
486 else if (event.keyCode === 38) {
487 let newActive = (active === 0) ? suggestCount - 1 : active - 1;
488 changeActiveTo(newActive, suggestionElems);
491 else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
492 let text = suggestionElems[active].textContent;
493 currentInput[0].value = text;
494 currentInput.focus();
495 $suggestionBox.hide();
497 if (event.keyCode === 13) {
498 event.preventDefault();
504 // Change the active suggestion to the given index
505 function changeActiveTo(index, suggestionElems) {
506 suggestionElems[active].className = '';
508 suggestionElems[active].className = 'active';
511 // Display suggestions on a field
512 let prevSuggestions = [];
514 function displaySuggestions($input, suggestions) {
516 // Hide if no suggestions
517 if (suggestions.length === 0) {
518 $suggestionBox.hide();
520 prevSuggestions = suggestions;
524 // Otherwise show and attach to input
526 $suggestionBox.show();
529 if ($input !== currentInput) {
530 $suggestionBox.detach();
531 $input.after($suggestionBox);
532 currentInput = $input;
535 // Return if no change
536 if (prevSuggestions.join() === suggestions.join()) {
537 prevSuggestions = suggestions;
542 $suggestionBox[0].innerHTML = '';
543 for (let i = 0; i < suggestions.length; i++) {
544 var suggestion = document.createElement('li');
545 suggestion.textContent = suggestions[i];
546 suggestion.onclick = suggestionClick;
548 suggestion.className = 'active'
552 $suggestionBox[0].appendChild(suggestion);
555 prevSuggestions = suggestions;
558 // Suggestion click event
559 function suggestionClick(event) {
560 let text = this.textContent;
561 currentInput[0].value = text;
562 currentInput.focus();
563 $suggestionBox.hide();
567 // Get suggestions & cache
568 function getSuggestions(input, url) {
569 let hasQuery = url.indexOf('?') !== -1;
570 let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input);
572 // Get from local cache if exists
573 if (typeof localCache[searchUrl] !== 'undefined') {
574 return new Promise((resolve, reject) => {
575 resolve(localCache[searchUrl]);
579 return $http.get(searchUrl).then(response => {
580 localCache[searchUrl] = response.data;
581 return response.data;
590 ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) {
594 link: function (scope, element, attrs) {
595 scope.loading = true;
596 scope.entityResults = false;
599 // Add input for forms
600 const input = element.find('[entity-selector-input]').first();
602 // Listen to entity item clicks
603 element.on('click', '.entity-list a', function(event) {
604 event.preventDefault();
605 event.stopPropagation();
606 let item = $(this).closest('[data-entity-type]');
609 element.on('click', '[data-entity-type]', function(event) {
613 // Select entity action
614 function itemSelect(item) {
615 let entityType = item.attr('data-entity-type');
616 let entityId = item.attr('data-entity-id');
617 let isSelected = !item.hasClass('selected');
618 element.find('.selected').removeClass('selected').removeClass('primary-background');
619 if (isSelected) item.addClass('selected').addClass('primary-background');
620 let newVal = isSelected ? `${entityType}:${entityId}` : '';
624 // Get search url with correct types
625 function getSearchUrl() {
626 let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter');
627 return `/ajax/search/entities?types=${types}`;
630 // Get initial contents
631 $http.get(getSearchUrl()).then(resp => {
632 scope.entityResults = $sce.trustAsHtml(resp.data);
633 scope.loading = false;
636 // Search when typing
637 scope.searchEntities = function() {
638 scope.loading = true;
640 let url = getSearchUrl() + '&term=' + encodeURIComponent(scope.search);
641 $http.get(url).then(resp => {
642 scope.entityResults = $sce.trustAsHtml(resp.data);
643 scope.loading = false;