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 var content = element.val();
256 scope.mdModel = content;
257 scope.mdChange(markdown(content));
259 element.on('change input', (e) => {
260 content = element.val();
262 scope.mdModel = content;
263 scope.mdChange(markdown(content));
267 scope.$on('markdown-update', (event, value) => {
269 scope.mdModel = value;
270 scope.mdChange(markdown(value));
279 * Handles all functionality of the markdown editor.
281 ngApp.directive('markdownEditor', ['$timeout', function ($timeout) {
284 link: function (scope, element, attrs) {
287 const input = element.find('textarea[markdown-input]');
288 const display = element.find('.markdown-display').first();
289 const insertImage = element.find('button[data-action="insertImage"]');
291 let currentCaretPos = 0;
293 input.blur(event => {
294 currentCaretPos = input[0].selectionStart;
298 let inputScrollHeight,
303 function setScrollHeights() {
304 inputScrollHeight = input[0].scrollHeight;
305 inputHeight = input.height();
306 displayScrollHeight = display[0].scrollHeight;
307 displayHeight = display.height();
313 window.addEventListener('resize', setScrollHeights);
314 let scrollDebounceTime = 800;
316 input.on('scroll', event => {
317 let now = Date.now();
318 if (now - lastScroll > scrollDebounceTime) {
321 let scrollPercent = (input.scrollTop() / (inputScrollHeight - inputHeight));
322 let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent;
323 display.scrollTop(displayScrollY);
327 // Editor key-presses
328 input.keydown(event => {
329 // Insert image shortcut
330 if (event.which === 73 && event.ctrlKey && event.shiftKey) {
331 event.preventDefault();
332 var caretPos = input[0].selectionStart;
333 var currentContent = input.val();
334 var mdImageText = "";
335 input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
337 input[0].selectionStart = caretPos + (";
338 input[0].selectionEnd = caretPos + (';
341 // Pass key presses to controller via event
342 scope.$emit('editor-keydown', event);
345 // Insert image from image manager
346 insertImage.click(event => {
347 window.ImageManager.showExternal(image => {
348 var caretPos = currentCaretPos;
349 var currentContent = input.val();
350 var mdImageText = "";
351 input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
361 * Page Editor Toolbox
362 * Controls all functionality for the sliding toolbox
363 * on the page edit view.
365 ngApp.directive('toolbox', [function () {
368 link: function (scope, elem, attrs) {
370 // Get common elements
371 const $buttons = elem.find('[tab-button]');
372 const $content = elem.find('[tab-content]');
373 const $toggle = elem.find('[toolbox-toggle]');
375 // Handle toolbox toggle click
376 $toggle.click((e) => {
377 elem.toggleClass('open');
380 // Set an active tab/content by name
381 function setActive(tabName, openToolbox) {
382 $buttons.removeClass('active');
384 $buttons.filter(`[tab-button="${tabName}"]`).addClass('active');
385 $content.filter(`[tab-content="${tabName}"]`).show();
386 if (openToolbox) elem.addClass('open');
389 // Set the first tab content active on load
390 setActive($content.first().attr('tab-content'), false);
392 // Handle tab button click
393 $buttons.click(function (e) {
394 let name = $(this).attr('tab-button');
395 setActive(name, true);
402 * Tag Autosuggestions
403 * Listens to child inputs and provides autosuggestions depending on field type
404 * and input. Suggestions provided by server.
406 ngApp.directive('tagAutosuggestions', ['$http', function ($http) {
409 link: function (scope, elem, attrs) {
411 // Local storage for quick caching.
412 const localCache = {};
414 // Create suggestion element
415 const suggestionBox = document.createElement('ul');
416 suggestionBox.className = 'suggestion-box';
417 suggestionBox.style.position = 'absolute';
418 suggestionBox.style.display = 'none';
419 const $suggestionBox = $(suggestionBox);
421 // General state tracking
422 let isShowing = false;
423 let currentInput = false;
426 // Listen to input events on autosuggest fields
427 elem.on('input focus', '[autosuggest]', function (event) {
428 let $input = $(this);
429 let val = $input.val();
430 let url = $input.attr('autosuggest');
431 let type = $input.attr('autosuggest-type');
433 // Add name param to request if for a value
434 if (type.toLowerCase() === 'value') {
435 let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
436 let nameVal = $nameInput.val();
437 if (nameVal !== '') {
438 url += '?name=' + encodeURIComponent(nameVal);
442 let suggestionPromise = getSuggestions(val.slice(0, 3), url);
443 suggestionPromise.then(suggestions => {
444 if (val.length === 0) {
445 displaySuggestions($input, suggestions.slice(0, 6));
447 suggestions = suggestions.filter(item => {
448 return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
450 displaySuggestions($input, suggestions);
455 // Hide autosuggestions when input loses focus.
456 // Slight delay to allow clicks.
457 let lastFocusTime = 0;
458 elem.on('blur', '[autosuggest]', function (event) {
459 let startTime = Date.now();
461 if (lastFocusTime < startTime) {
462 $suggestionBox.hide();
467 elem.on('focus', '[autosuggest]', function (event) {
468 lastFocusTime = Date.now();
471 elem.on('keydown', '[autosuggest]', function (event) {
472 if (!isShowing) return;
474 let suggestionElems = suggestionBox.childNodes;
475 let suggestCount = suggestionElems.length;
478 if (event.keyCode === 40) {
479 let newActive = (active === suggestCount - 1) ? 0 : active + 1;
480 changeActiveTo(newActive, suggestionElems);
483 else if (event.keyCode === 38) {
484 let newActive = (active === 0) ? suggestCount - 1 : active - 1;
485 changeActiveTo(newActive, suggestionElems);
488 else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
489 let text = suggestionElems[active].textContent;
490 currentInput[0].value = text;
491 currentInput.focus();
492 $suggestionBox.hide();
494 if (event.keyCode === 13) {
495 event.preventDefault();
501 // Change the active suggestion to the given index
502 function changeActiveTo(index, suggestionElems) {
503 suggestionElems[active].className = '';
505 suggestionElems[active].className = 'active';
508 // Display suggestions on a field
509 let prevSuggestions = [];
511 function displaySuggestions($input, suggestions) {
513 // Hide if no suggestions
514 if (suggestions.length === 0) {
515 $suggestionBox.hide();
517 prevSuggestions = suggestions;
521 // Otherwise show and attach to input
523 $suggestionBox.show();
526 if ($input !== currentInput) {
527 $suggestionBox.detach();
528 $input.after($suggestionBox);
529 currentInput = $input;
532 // Return if no change
533 if (prevSuggestions.join() === suggestions.join()) {
534 prevSuggestions = suggestions;
539 $suggestionBox[0].innerHTML = '';
540 for (let i = 0; i < suggestions.length; i++) {
541 var suggestion = document.createElement('li');
542 suggestion.textContent = suggestions[i];
543 suggestion.onclick = suggestionClick;
545 suggestion.className = 'active'
549 $suggestionBox[0].appendChild(suggestion);
552 prevSuggestions = suggestions;
555 // Suggestion click event
556 function suggestionClick(event) {
557 let text = this.textContent;
558 currentInput[0].value = text;
559 currentInput.focus();
560 $suggestionBox.hide();
564 // Get suggestions & cache
565 function getSuggestions(input, url) {
566 let hasQuery = url.indexOf('?') !== -1;
567 let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input);
569 // Get from local cache if exists
570 if (typeof localCache[searchUrl] !== 'undefined') {
571 return new Promise((resolve, reject) => {
572 resolve(localCache[searchUrl]);
576 return $http.get(searchUrl).then(response => {
577 localCache[searchUrl] = response.data;
578 return response.data;
587 ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) {
591 link: function (scope, element, attrs) {
592 scope.loading = true;
593 scope.entityResults = false;
596 // Add input for forms
597 const input = element.find('[entity-selector-input]').first();
599 // Listen to entity item clicks
600 element.on('click', '.entity-list a', function(event) {
601 event.preventDefault();
602 event.stopPropagation();
603 let item = $(this).closest('[data-entity-type]');
606 element.on('click', '[data-entity-type]', function(event) {
610 // Select entity action
611 function itemSelect(item) {
612 let entityType = item.attr('data-entity-type');
613 let entityId = item.attr('data-entity-id');
614 let isSelected = !item.hasClass('selected');
615 element.find('.selected').removeClass('selected').removeClass('primary-background');
616 if (isSelected) item.addClass('selected').addClass('primary-background');
617 let newVal = isSelected ? `${entityType}:${entityId}` : '';
621 // Get search url with correct types
622 function getSearchUrl() {
623 let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter');
624 return `/ajax/search/entities?types=${types}`;
627 // Get initial contents
628 $http.get(getSearchUrl()).then(resp => {
629 scope.entityResults = $sce.trustAsHtml(resp.data);
630 scope.loading = false;
633 // Search when typing
634 scope.searchEntities = function() {
635 scope.loading = true;
637 let url = getSearchUrl() + '&term=' + encodeURIComponent(scope.search);
638 $http.get(url).then(resp => {
639 scope.entityResults = $sce.trustAsHtml(resp.data);
640 scope.loading = false;