2 const DropZone = require("dropzone");
3 const MarkdownIt = require("markdown-it");
4 const mdTasksLists = require('markdown-it-task-lists');
5 const code = require('./code');
7 module.exports = function (ngApp, events) {
10 * Common tab controls using simple jQuery functions.
12 ngApp.directive('tabContainer', function() {
15 link: function (scope, element, attrs) {
16 const $content = element.find('[tab-content]');
17 const $buttons = element.find('[tab-button]');
19 if (attrs.tabContainer) {
20 let initial = attrs.tabContainer;
21 $buttons.filter(`[tab-button="${initial}"]`).addClass('selected');
22 $content.hide().filter(`[tab-content="${initial}"]`).show();
24 $content.hide().first().show();
25 $buttons.first().addClass('selected');
28 $buttons.click(function() {
29 let clickedTab = $(this);
30 $buttons.removeClass('selected');
32 let name = clickedTab.addClass('selected').attr('tab-button');
33 $content.filter(`[tab-content="${name}"]`).show();
40 * Sub form component to allow inner-form sections to act like their own forms.
42 ngApp.directive('subForm', function() {
45 link: function (scope, element, attrs) {
46 element.on('keypress', e => {
47 if (e.keyCode === 13) {
52 element.find('button[type="submit"]').click(submitEvent);
54 function submitEvent(e) {
56 if (attrs.subForm) scope.$eval(attrs.subForm);
64 * Used for uploading images
66 ngApp.directive('dropZone', [function () {
70 <div class="dropzone-container">
71 <div class="dz-message">{{message}}</div>
80 link: function (scope, element, attrs) {
81 scope.message = attrs.message;
82 if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
83 let dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
87 dz.on('sending', function (file, xhr, data) {
88 let token = window.document.querySelector('meta[name=token]').getAttribute('content');
89 data.append('_token', token);
90 let uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo;
91 data.append('uploaded_to', uploadedTo);
93 if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess);
94 dz.on('success', function (file, data) {
95 $(file.previewElement).fadeOut(400, function () {
99 if (typeof scope.eventError !== 'undefined') dz.on('error', scope.eventError);
100 dz.on('error', function (file, errorMessage, xhr) {
101 console.log(errorMessage);
103 function setMessage(message) {
104 $(file.previewElement).find('[data-dz-errormessage]').text(message);
107 if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
108 if (errorMessage.file) setMessage(errorMessage.file[0]);
119 * An angular wrapper around the tinyMCE editor.
121 ngApp.directive('tinymce', ['$timeout', function ($timeout) {
129 link: function (scope, element, attrs) {
131 function tinyMceSetup(editor) {
132 editor.on('ExecCommand change NodeChange ObjectResized', (e) => {
133 let content = editor.getContent();
135 scope.mceModel = content;
137 scope.mceChange(content);
140 editor.on('keydown', (event) => {
141 scope.$emit('editor-keydown', event);
144 editor.on('init', (e) => {
145 scope.mceModel = editor.getContent();
148 scope.$on('html-update', (event, value) => {
149 editor.setContent(value);
150 editor.selection.select(editor.getBody(), true);
151 editor.selection.collapse(false);
152 scope.mceModel = editor.getContent();
156 scope.tinymce.extraSetups.push(tinyMceSetup);
157 tinymce.init(scope.tinymce);
162 const md = new MarkdownIt({html: true});
163 md.use(mdTasksLists, {label: true});
167 * Handles the logic for just the editor input field.
169 ngApp.directive('markdownInput', ['$timeout', function ($timeout) {
176 link: function (scope, element, attrs) {
179 element = element.find('textarea').first();
180 let cm = code.markdownEditor(element[0]);
182 // Custom key commands
183 let metaKey = code.getMetaKey();
184 const extraKeys = {};
185 // Insert Image shortcut
186 extraKeys[`${metaKey}-Alt-I`] = function(cm) {
187 let selectedText = cm.getSelection();
188 let newText = ``;
189 let cursorPos = cm.getCursor('from');
190 cm.replaceSelection(newText);
191 cm.setCursor(cursorPos.line, cursorPos.ch + newText.length -1);
194 extraKeys[`${metaKey}-S`] = function(cm) {scope.$emit('save-draft');};
195 // Show link selector
196 extraKeys[`Shift-${metaKey}-K`] = function(cm) {showLinkSelector()};
198 extraKeys[`${metaKey}-K`] = function(cm) {insertLink()};
200 extraKeys[`${metaKey}-1`] = function(cm) {replaceLineStart('##');};
201 extraKeys[`${metaKey}-2`] = function(cm) {replaceLineStart('###');};
202 extraKeys[`${metaKey}-3`] = function(cm) {replaceLineStart('####');};
203 extraKeys[`${metaKey}-4`] = function(cm) {replaceLineStart('#####');};
204 extraKeys[`${metaKey}-5`] = function(cm) {replaceLineStart('');};
205 extraKeys[`${metaKey}-d`] = function(cm) {replaceLineStart('');};
206 extraKeys[`${metaKey}-6`] = function(cm) {replaceLineStart('>');};
207 extraKeys[`${metaKey}-q`] = function(cm) {replaceLineStart('>');};
208 extraKeys[`${metaKey}-7`] = function(cm) {wrapSelection('\n```\n', '\n```');};
209 extraKeys[`${metaKey}-8`] = function(cm) {wrapSelection('`', '`');};
210 extraKeys[`Shift-${metaKey}-E`] = function(cm) {wrapSelection('`', '`');};
211 extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('<p class="callout info">', '</div>');};
212 cm.setOption('extraKeys', extraKeys);
214 // Update data on content change
215 cm.on('change', (instance, changeObj) => {
219 // Handle scroll to sync display view
220 cm.on('scroll', instance => {
221 // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
222 let scroll = instance.getScrollInfo();
223 let atEnd = scroll.top + scroll.clientHeight === scroll.height;
225 scope.$emit('markdown-scroll', -1);
228 let lineNum = instance.lineAtHeight(scroll.top, 'local');
229 let range = instance.getRange({line: 0, ch: null}, {line: lineNum, ch: null});
230 let parser = new DOMParser();
231 let doc = parser.parseFromString(md.render(range), 'text/html');
232 let totalLines = doc.documentElement.querySelectorAll('body > *');
233 scope.$emit('markdown-scroll', totalLines.length);
236 // Handle image paste
237 cm.on('paste', (cm, event) => {
238 if (!event.clipboardData || !event.clipboardData.items) return;
239 for (let i = 0; i < event.clipboardData.items.length; i++) {
240 uploadImage(event.clipboardData.items[i].getAsFile());
244 // Handle images on drag-drop
245 cm.on('drop', (cm, event) => {
246 event.stopPropagation();
247 event.preventDefault();
248 let cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
249 cm.setCursor(cursorPos);
250 if (!event.dataTransfer || !event.dataTransfer.files) return;
251 for (let i = 0; i < event.dataTransfer.files.length; i++) {
252 uploadImage(event.dataTransfer.files[i]);
256 // Helper to replace editor content
257 function replaceContent(search, replace) {
258 let text = cm.getValue();
259 let cursor = cm.listSelections();
260 cm.setValue(text.replace(search, replace));
261 cm.setSelections(cursor);
264 // Helper to replace the start of the line
265 function replaceLineStart(newStart) {
266 let cursor = cm.getCursor();
267 let lineContent = cm.getLine(cursor.line);
268 let lineLen = lineContent.length;
269 let lineStart = lineContent.split(' ')[0];
271 // Remove symbol if already set
272 if (lineStart === newStart) {
273 lineContent = lineContent.replace(`${newStart} `, '');
274 cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
275 cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)});
279 let alreadySymbol = /^[#>`]/.test(lineStart);
282 posDif = newStart.length - lineStart.length;
283 lineContent = lineContent.replace(lineStart, newStart).trim();
284 } else if (newStart !== '') {
285 posDif = newStart.length + 1;
286 lineContent = newStart + ' ' + lineContent;
288 cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
289 cm.setCursor({line: cursor.line, ch: cursor.ch + posDif});
292 function wrapLine(start, end) {
293 let cursor = cm.getCursor();
294 let lineContent = cm.getLine(cursor.line);
295 let lineLen = lineContent.length;
296 let newLineContent = lineContent;
298 if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
299 newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
301 newLineContent = `${start}${lineContent}${end}`;
304 cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
305 cm.setCursor({line: cursor.line, ch: cursor.ch + (newLineContent.length - lineLen)});
308 function wrapSelection(start, end) {
309 let selection = cm.getSelection();
310 if (selection === '') return wrapLine(start, end);
311 let newSelection = selection;
315 if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
316 newSelection = selection.slice(start.length, selection.length - end.length);
317 endDiff = -(end.length + start.length);
319 newSelection = `${start}${selection}${end}`;
320 endDiff = start.length + end.length;
323 let selections = cm.listSelections()[0];
324 cm.replaceSelection(newSelection);
325 let headFirst = selections.head.ch <= selections.anchor.ch;
326 selections.head.ch += headFirst ? frontDiff : endDiff;
327 selections.anchor.ch += headFirst ? endDiff : frontDiff;
328 cm.setSelections([selections]);
331 // Handle image upload and add image into markdown content
332 function uploadImage(file) {
333 if (file === null || file.type.indexOf('image') !== 0) return;
337 let fileNameMatches = file.name.match(/\.(.+)$/);
338 if (fileNameMatches.length > 1) ext = fileNameMatches[1];
341 // Insert image into markdown
342 let id = "image-" + Math.random().toString(16).slice(2);
343 let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
344 let selectedText = cm.getSelection();
345 let placeHolderText = ``;
346 cm.replaceSelection(placeHolderText);
348 let remoteFilename = "image-" + Date.now() + "." + ext;
349 let formData = new FormData();
350 formData.append('file', file, remoteFilename);
352 window.$http.post('/images/gallery/upload', formData).then(resp => {
353 replaceContent(placeholderImage, resp.data.thumbs.display);
355 events.emit('error', trans('errors.image_upload_error'));
356 replaceContent(placeHolderText, selectedText);
361 // Show the popup link selector and insert a link when finished
362 function showLinkSelector() {
363 let cursorPos = cm.getCursor('from');
364 window.showEntityLinkSelector(entity => {
365 let selectedText = cm.getSelection() || entity.name;
366 let newText = `[${selectedText}](${entity.link})`;
368 cm.replaceSelection(newText);
369 cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
373 function insertLink() {
374 let cursorPos = cm.getCursor('from');
375 let selectedText = cm.getSelection() || '';
376 let newText = `[${selectedText}]()`;
378 cm.replaceSelection(newText);
379 let cursorPosDiff = (selectedText === '') ? -3 : -1;
380 cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff);
383 // Show the image manager and handle image insertion
384 function showImageManager() {
385 let cursorPos = cm.getCursor('from');
386 window.ImageManager.showExternal(image => {
387 let selectedText = cm.getSelection();
388 let newText = "";
390 cm.replaceSelection(newText);
391 cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
395 // Update the data models and rendered output
396 function update(instance) {
397 let content = instance.getValue();
398 element.val(content);
400 scope.mdModel = content;
401 scope.mdChange(md.render(content));
406 // Listen to commands from parent scope
407 scope.$on('md-insert-link', showLinkSelector);
408 scope.$on('md-insert-image', showImageManager);
409 scope.$on('markdown-update', (event, value) => {
412 scope.mdModel = value;
413 scope.mdChange(md.render(value));
422 * Handles all functionality of the markdown editor.
424 ngApp.directive('markdownEditor', ['$timeout', '$rootScope', function ($timeout, $rootScope) {
427 link: function (scope, element, attrs) {
430 const $display = element.find('.markdown-display').first();
431 const $insertImage = element.find('button[data-action="insertImage"]');
432 const $insertEntityLink = element.find('button[data-action="insertEntityLink"]');
434 // Prevent markdown display link click redirect
435 $display.on('click', 'a', function(event) {
436 event.preventDefault();
437 window.open(this.getAttribute('href'));
441 $insertEntityLink.click(e => {scope.$broadcast('md-insert-link');});
442 $insertImage.click(e => {scope.$broadcast('md-insert-image');});
444 // Handle scroll sync event from editor scroll
445 $rootScope.$on('markdown-scroll', (event, lineCount) => {
446 let elems = $display[0].children[0].children;
447 if (elems.length > lineCount) {
448 let topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
450 scrollTop: topElem.offsetTop
451 }, {queue: false, duration: 200, easing: 'linear'});
459 * Page Editor Toolbox
460 * Controls all functionality for the sliding toolbox
461 * on the page edit view.
463 ngApp.directive('toolbox', [function () {
466 link: function (scope, elem, attrs) {
468 // Get common elements
469 const $buttons = elem.find('[toolbox-tab-button]');
470 const $content = elem.find('[toolbox-tab-content]');
471 const $toggle = elem.find('[toolbox-toggle]');
473 // Handle toolbox toggle click
474 $toggle.click((e) => {
475 elem.toggleClass('open');
478 // Set an active tab/content by name
479 function setActive(tabName, openToolbox) {
480 $buttons.removeClass('active');
482 $buttons.filter(`[toolbox-tab-button="${tabName}"]`).addClass('active');
483 $content.filter(`[toolbox-tab-content="${tabName}"]`).show();
484 if (openToolbox) elem.addClass('open');
487 // Set the first tab content active on load
488 setActive($content.first().attr('toolbox-tab-content'), false);
490 // Handle tab button click
491 $buttons.click(function (e) {
492 let name = $(this).attr('toolbox-tab-button');
493 setActive(name, true);
500 * Tag Autosuggestions
501 * Listens to child inputs and provides autosuggestions depending on field type
502 * and input. Suggestions provided by server.
504 ngApp.directive('tagAutosuggestions', ['$http', function ($http) {
507 link: function (scope, elem, attrs) {
509 // Local storage for quick caching.
510 const localCache = {};
512 // Create suggestion element
513 const suggestionBox = document.createElement('ul');
514 suggestionBox.className = 'suggestion-box';
515 suggestionBox.style.position = 'absolute';
516 suggestionBox.style.display = 'none';
517 const $suggestionBox = $(suggestionBox);
519 // General state tracking
520 let isShowing = false;
521 let currentInput = false;
524 // Listen to input events on autosuggest fields
525 elem.on('input focus', '[autosuggest]', function (event) {
526 let $input = $(this);
527 let val = $input.val();
528 let url = $input.attr('autosuggest');
529 let type = $input.attr('autosuggest-type');
531 // Add name param to request if for a value
532 if (type.toLowerCase() === 'value') {
533 let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
534 let nameVal = $nameInput.val();
535 if (nameVal !== '') {
536 url += '?name=' + encodeURIComponent(nameVal);
540 let suggestionPromise = getSuggestions(val.slice(0, 3), url);
541 suggestionPromise.then(suggestions => {
542 if (val.length === 0) {
543 displaySuggestions($input, suggestions.slice(0, 6));
545 suggestions = suggestions.filter(item => {
546 return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
548 displaySuggestions($input, suggestions);
553 // Hide autosuggestions when input loses focus.
554 // Slight delay to allow clicks.
555 let lastFocusTime = 0;
556 elem.on('blur', '[autosuggest]', function (event) {
557 let startTime = Date.now();
559 if (lastFocusTime < startTime) {
560 $suggestionBox.hide();
565 elem.on('focus', '[autosuggest]', function (event) {
566 lastFocusTime = Date.now();
569 elem.on('keydown', '[autosuggest]', function (event) {
570 if (!isShowing) return;
572 let suggestionElems = suggestionBox.childNodes;
573 let suggestCount = suggestionElems.length;
576 if (event.keyCode === 40) {
577 let newActive = (active === suggestCount - 1) ? 0 : active + 1;
578 changeActiveTo(newActive, suggestionElems);
581 else if (event.keyCode === 38) {
582 let newActive = (active === 0) ? suggestCount - 1 : active - 1;
583 changeActiveTo(newActive, suggestionElems);
586 else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
587 currentInput[0].value = suggestionElems[active].textContent;
588 currentInput.focus();
589 $suggestionBox.hide();
591 if (event.keyCode === 13) {
592 event.preventDefault();
598 // Change the active suggestion to the given index
599 function changeActiveTo(index, suggestionElems) {
600 suggestionElems[active].className = '';
602 suggestionElems[active].className = 'active';
605 // Display suggestions on a field
606 let prevSuggestions = [];
608 function displaySuggestions($input, suggestions) {
610 // Hide if no suggestions
611 if (suggestions.length === 0) {
612 $suggestionBox.hide();
614 prevSuggestions = suggestions;
618 // Otherwise show and attach to input
620 $suggestionBox.show();
623 if ($input !== currentInput) {
624 $suggestionBox.detach();
625 $input.after($suggestionBox);
626 currentInput = $input;
629 // Return if no change
630 if (prevSuggestions.join() === suggestions.join()) {
631 prevSuggestions = suggestions;
636 $suggestionBox[0].innerHTML = '';
637 for (let i = 0; i < suggestions.length; i++) {
638 let suggestion = document.createElement('li');
639 suggestion.textContent = suggestions[i];
640 suggestion.onclick = suggestionClick;
642 suggestion.className = 'active';
645 $suggestionBox[0].appendChild(suggestion);
648 prevSuggestions = suggestions;
651 // Suggestion click event
652 function suggestionClick(event) {
653 currentInput[0].value = this.textContent;
654 currentInput.focus();
655 $suggestionBox.hide();
659 // Get suggestions & cache
660 function getSuggestions(input, url) {
661 let hasQuery = url.indexOf('?') !== -1;
662 let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input);
664 // Get from local cache if exists
665 if (typeof localCache[searchUrl] !== 'undefined') {
666 return new Promise((resolve, reject) => {
667 resolve(localCache[searchUrl]);
671 return $http.get(searchUrl).then(response => {
672 localCache[searchUrl] = response.data;
673 return response.data;
681 ngApp.directive('entityLinkSelector', [function($http) {
684 link: function(scope, element, attrs) {
686 const selectButton = element.find('.entity-link-selector-confirm');
687 let callback = false;
688 let entitySelection = null;
690 // Handle entity selection change, Stores the selected entity locally
691 function entitySelectionChange(entity) {
692 entitySelection = entity;
693 if (entity === null) {
694 selectButton.attr('disabled', 'true');
696 selectButton.removeAttr('disabled');
699 events.listen('entity-select-change', entitySelectionChange);
701 // Handle selection confirm button click
702 selectButton.click(event => {
704 if (entitySelection !== null) callback(entitySelection);
707 // Show selector interface
712 // Hide selector interface
714 element.fadeOut(240);
718 // Listen to confirmation of entity selections (doubleclick)
719 events.listen('entity-select-confirm', entity => {
724 // Show entity selector, Accessible globally, and store the callback
725 window.showEntityLinkSelector = function(passedCallback) {
727 callback = passedCallback;
735 ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) {
739 link: function (scope, element, attrs) {
740 scope.loading = true;
741 scope.entityResults = false;
744 // Add input for forms
745 const input = element.find('[entity-selector-input]').first();
747 // Detect double click events
749 function isDoubleClick() {
750 let now = Date.now();
751 let answer = now - lastClick < 300;
756 // Listen to entity item clicks
757 element.on('click', '.entity-list a', function(event) {
758 event.preventDefault();
759 event.stopPropagation();
760 let item = $(this).closest('[data-entity-type]');
761 itemSelect(item, isDoubleClick());
763 element.on('click', '[data-entity-type]', function(event) {
764 itemSelect($(this), isDoubleClick());
767 // Select entity action
768 function itemSelect(item, doubleClick) {
769 let entityType = item.attr('data-entity-type');
770 let entityId = item.attr('data-entity-id');
771 let isSelected = !item.hasClass('selected') || doubleClick;
772 element.find('.selected').removeClass('selected').removeClass('primary-background');
773 if (isSelected) item.addClass('selected').addClass('primary-background');
774 let newVal = isSelected ? `${entityType}:${entityId}` : '';
778 events.emit('entity-select-change', null);
781 if (!doubleClick && !isSelected) return;
783 let link = item.find('.entity-list-item-link').attr('href');
784 let name = item.find('.entity-list-item-name').text();
787 events.emit('entity-select-confirm', {
788 id: Number(entityId),
795 events.emit('entity-select-change', {
796 id: Number(entityId),
803 // Get search url with correct types
804 function getSearchUrl() {
805 let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter');
806 return window.baseUrl(`/ajax/search/entities?types=${types}`);
809 // Get initial contents
810 $http.get(getSearchUrl()).then(resp => {
811 scope.entityResults = $sce.trustAsHtml(resp.data);
812 scope.loading = false;
815 // Search when typing
816 scope.searchEntities = function() {
817 scope.loading = true;
819 let url = getSearchUrl() + '&term=' + encodeURIComponent(scope.search);
820 $http.get(url).then(resp => {
821 scope.entityResults = $sce.trustAsHtml(resp.data);
822 scope.loading = false;
829 ngApp.directive('commentReply', [function () {
832 templateUrl: 'comment-reply.html',
838 link: function (scope, element) {
839 scope.isReply = true;
840 element.find('textarea').focus();
841 scope.$on('evt.comment-success', function (event) {
842 // no need for the event to do anything more.
843 event.stopPropagation();
844 event.preventDefault();
848 scope.closeBox = function () {
856 ngApp.directive('commentEdit', [function () {
859 templateUrl: 'comment-reply.html',
863 link: function (scope, element) {
865 element.find('textarea').focus();
866 scope.$on('evt.comment-success', function (event, commentId) {
867 // no need for the event to do anything more.
868 event.stopPropagation();
869 event.preventDefault();
870 if (commentId === scope.comment.id && !scope.isNew) {
875 scope.closeBox = function () {
884 ngApp.directive('commentReplyLink', ['$document', '$compile', function ($document, $compile) {
889 link: function (scope, element, attr) {
890 element.on('$destroy', function () {
891 element.off('click');
895 element.on('click', function (e) {
897 var $container = element.parents('.comment-actions').first();
898 if (!$container.length) {
899 console.error('commentReplyLink directive should be placed inside a container with class comment-box!');
902 if (attr.noCommentReplyDupe) {
906 compileHtml($container, scope, attr.isReply === 'true');
911 function compileHtml($container, scope, isReply) {
914 lnkFunc = $compile('<comment-reply page-id="comment.pageId" parent-id="comment.id" parent="comment"></comment-reply>');
916 lnkFunc = $compile('<comment-edit comment="comment"></comment-add>');
918 var compiledHTML = lnkFunc(scope);
919 $container.append(compiledHTML);
922 function removeDupe() {
923 let $existingElement = $document.find('.comments-list comment-reply, .comments-list comment-edit');
924 if (!$existingElement.length) {
928 $existingElement.remove();
932 ngApp.directive('commentDeleteLink', ['$window', function ($window) {
934 controller: 'CommentDeleteController',
938 link: function (scope, element, attr, ctrl) {
940 element.on('click', function(e) {
942 var resp = $window.confirm(trans('entities.comment_delete_confirm'));
947 ctrl.delete(scope.comment);