Нет описания
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. /**
  2. * App Kanban
  3. */
  4. 'use strict';
  5. (async function () {
  6. let boards;
  7. const kanbanSidebar = document.querySelector('.kanban-update-item-sidebar'),
  8. kanbanWrapper = document.querySelector('.kanban-wrapper'),
  9. commentEditor = document.querySelector('.comment-editor'),
  10. kanbanAddNewBoard = document.querySelector('.kanban-add-new-board'),
  11. kanbanAddNewInput = [].slice.call(document.querySelectorAll('.kanban-add-board-input')),
  12. kanbanAddBoardBtn = document.querySelector('.kanban-add-board-btn'),
  13. datePicker = document.querySelector('#due-date'),
  14. select2 = $('.select2'), // ! Using jquery vars due to select2 jQuery dependency
  15. assetsPath = document.querySelector('html').getAttribute('data-assets-path');
  16. // Init kanban Offcanvas
  17. const kanbanOffcanvas = new bootstrap.Offcanvas(kanbanSidebar);
  18. // Get kanban data
  19. const kanbanResponse = await fetch(assetsPath + 'json/kanban.json');
  20. if (!kanbanResponse.ok) {
  21. console.error('error', kanbanResponse);
  22. }
  23. boards = await kanbanResponse.json();
  24. // datepicker init
  25. if (datePicker) {
  26. datePicker.flatpickr({
  27. monthSelectorType: 'static',
  28. static: true,
  29. altInput: true,
  30. altFormat: 'j F, Y',
  31. dateFormat: 'Y-m-d'
  32. });
  33. }
  34. //! TODO: Update Event label and guest code to JS once select removes jQuery dependency
  35. // select2
  36. if (select2.length) {
  37. function renderLabels(option) {
  38. if (!option.id) {
  39. return option.text;
  40. }
  41. var $badge = "<div class='badge " + $(option.element).data('color') + "'> " + option.text + '</div>';
  42. return $badge;
  43. }
  44. select2.each(function () {
  45. var $this = $(this);
  46. $this.wrap("<div class='position-relative'></div>").select2({
  47. placeholder: 'Select Label',
  48. dropdownParent: $this.parent(),
  49. templateResult: renderLabels,
  50. templateSelection: renderLabels,
  51. escapeMarkup: function (es) {
  52. return es;
  53. }
  54. });
  55. });
  56. }
  57. // Comment editor
  58. if (commentEditor) {
  59. new Quill(commentEditor, {
  60. modules: {
  61. toolbar: '.comment-toolbar'
  62. },
  63. placeholder: 'Write a Comment...',
  64. theme: 'snow'
  65. });
  66. }
  67. // Render board dropdown
  68. const renderBoardDropdown = () => `
  69. <div class="dropdown">
  70. <i class="dropdown-toggle icon-base bx bx-dots-vertical-rounded cursor-pointer"
  71. id="board-dropdown"
  72. data-bs-toggle="dropdown"
  73. aria-haspopup="true"
  74. aria-expanded="false">
  75. </i>
  76. <div class="dropdown-menu dropdown-menu-end" aria-labelledby="board-dropdown">
  77. <a class="dropdown-item delete-board" href="javascript:void(0)">
  78. <i class="icon-base bx bx-trash icon-xs me-1"></i>
  79. <span class="align-middle">Delete</span>
  80. </a>
  81. <a class="dropdown-item" href="javascript:void(0)">
  82. <i class="icon-base bx bx-rename icon-xs me-1"></i>
  83. <span class="align-middle">Rename</span>
  84. </a>
  85. <a class="dropdown-item" href="javascript:void(0)">
  86. <i class="icon-base bx bx-archive icon-xs me-1"></i>
  87. <span class="align-middle">Archive</span>
  88. </a>
  89. </div>
  90. </div>
  91. `;
  92. // Render item dropdown
  93. const renderDropdown = () => `
  94. <div class="dropdown kanban-tasks-item-dropdown">
  95. <i class="dropdown-toggle icon-base bx bx-dots-vertical-rounded"
  96. id="kanban-tasks-item-dropdown"
  97. data-bs-toggle="dropdown"
  98. aria-haspopup="true"
  99. aria-expanded="false">
  100. </i>
  101. <div class="dropdown-menu dropdown-menu-end" aria-labelledby="kanban-tasks-item-dropdown">
  102. <a class="dropdown-item" href="javascript:void(0)">Copy task link</a>
  103. <a class="dropdown-item" href="javascript:void(0)">Duplicate task</a>
  104. <a class="dropdown-item delete-task" href="javascript:void(0)">Delete</a>
  105. </div>
  106. </div>
  107. `;
  108. // Render header
  109. const renderHeader = (color, text) => `
  110. <div class="d-flex justify-content-between flex-wrap align-items-center mb-2">
  111. <div class="item-badges">
  112. <div class="badge bg-label-${color}">${text}</div>
  113. </div>
  114. ${renderDropdown()}
  115. </div>
  116. `;
  117. // Render avatar
  118. const renderAvatar = (images = '', pullUp = false, size = '', margin = '', members = '') => {
  119. const transitionClass = pullUp ? ' pull-up' : '';
  120. const sizeClass = size ? `avatar-${size}` : '';
  121. const memberList = members ? members.split(',') : [];
  122. return images
  123. ? images
  124. .split(',')
  125. .map((img, index, arr) => {
  126. const marginClass = margin && index !== arr.length - 1 ? ` me-${margin}` : '';
  127. const memberName = memberList[index] || '';
  128. return `
  129. <div class="avatar ${sizeClass}${marginClass} w-px-26 h-px-26"
  130. data-bs-toggle="tooltip"
  131. data-bs-placement="top"
  132. title="${memberName}">
  133. <img src="${assetsPath}img/avatars/${img}"
  134. alt="Avatar"
  135. class="rounded-circle${transitionClass}">
  136. </div>
  137. `;
  138. })
  139. .join('')
  140. : '';
  141. };
  142. // Render footer
  143. const renderFooter = (attachments, comments, assigned, members) => `
  144. <div class="d-flex justify-content-between align-items-center flex-wrap mt-2">
  145. <div class="d-flex">
  146. <span class="d-flex align-items-center me-2">
  147. <i class="icon-base bx bx-paperclip me-1"></i>
  148. <span class="attachments">${attachments}</span>
  149. </span>
  150. <span class="d-flex align-items-center ms-2">
  151. <i class="icon-base bx bx-chat me-1"></i>
  152. <span>${comments}</span>
  153. </span>
  154. </div>
  155. <div class="avatar-group d-flex align-items-center assigned-avatar">
  156. ${renderAvatar(assigned, true, 'xs', null, members)}
  157. </div>
  158. </div>
  159. `;
  160. // Initialize kanban
  161. const kanban = new jKanban({
  162. element: '.kanban-wrapper',
  163. gutter: '12px',
  164. widthBoard: '250px',
  165. dragItems: true,
  166. boards: boards,
  167. dragBoards: true,
  168. addItemButton: true,
  169. buttonContent: '+ Add Item',
  170. itemAddOptions: {
  171. enabled: true,
  172. content: '+ Add New Item',
  173. class: 'kanban-title-button btn btn-default',
  174. footer: false
  175. },
  176. click: el => {
  177. const element = el;
  178. const title = element.getAttribute('data-eid')
  179. ? element.querySelector('.kanban-text').textContent
  180. : element.textContent;
  181. const date = element.getAttribute('data-due-date');
  182. const dateObj = new Date();
  183. const year = dateObj.getFullYear();
  184. const dateToUse = date
  185. ? `${date}, ${year}`
  186. : `${dateObj.getDate()} ${dateObj.toLocaleString('en', { month: 'long' })}, ${year}`;
  187. const label = element.getAttribute('data-badge-text');
  188. const avatars = element.getAttribute('data-assigned');
  189. // Show kanban offcanvas
  190. kanbanOffcanvas.show();
  191. // Populate sidebar fields
  192. kanbanSidebar.querySelector('#title').value = title;
  193. kanbanSidebar.querySelector('#due-date').nextSibling.value = dateToUse;
  194. // Using jQuery for select2
  195. $('.kanban-update-item-sidebar').find(select2).val(label).trigger('change');
  196. // Remove and update assigned avatars
  197. kanbanSidebar.querySelector('.assigned').innerHTML = '';
  198. kanbanSidebar.querySelector('.assigned').insertAdjacentHTML(
  199. 'afterbegin',
  200. `${renderAvatar(avatars, false, 'xs', '1', el.getAttribute('data-members'))}
  201. <div class="avatar avatar-xs ms-1">
  202. <span class="avatar-initial rounded-circle bg-label-secondary">
  203. <i class="icon-base bx bx-plus icon-xs text-heading"></i>
  204. </span>
  205. </div>`
  206. );
  207. },
  208. buttonClick: (el, boardId) => {
  209. const addNewForm = document.createElement('form');
  210. addNewForm.setAttribute('class', 'new-item-form');
  211. addNewForm.innerHTML = `
  212. <div class="mb-4">
  213. <textarea class="form-control add-new-item" rows="2" placeholder="Add Content" autofocus required></textarea>
  214. </div>
  215. <div class="mb-4">
  216. <button type="submit" class="btn btn-primary btn-sm me-3">Add</button>
  217. <button type="button" class="btn btn-label-secondary btn-sm cancel-add-item">Cancel</button>
  218. </div>
  219. `;
  220. kanban.addForm(boardId, addNewForm);
  221. addNewForm.addEventListener('submit', e => {
  222. e.preventDefault();
  223. const currentBoard = Array.from(document.querySelectorAll(`.kanban-board[data-id="${boardId}"] .kanban-item`));
  224. kanban.addElement(boardId, {
  225. title: `<span class="kanban-text">${e.target[0].value}</span>`,
  226. id: `${boardId}-${currentBoard.length + 1}`
  227. });
  228. // Add dropdown to new tasks
  229. const kanbanTextElements = Array.from(
  230. document.querySelectorAll(`.kanban-board[data-id="${boardId}"] .kanban-text`)
  231. );
  232. kanbanTextElements.forEach(textElem => {
  233. textElem.insertAdjacentHTML('beforebegin', renderDropdown());
  234. });
  235. // Prevent sidebar from opening on dropdown click
  236. const newTaskDropdowns = Array.from(document.querySelectorAll('.kanban-item .kanban-tasks-item-dropdown'));
  237. newTaskDropdowns.forEach(dropdown => {
  238. dropdown.addEventListener('click', event => event.stopPropagation());
  239. });
  240. // Add delete functionality for new tasks
  241. const deleteTaskButtons = Array.from(
  242. document.querySelectorAll(`.kanban-board[data-id="${boardId}"] .delete-task`)
  243. );
  244. deleteTaskButtons.forEach(btn => {
  245. btn.addEventListener('click', () => {
  246. const taskId = btn.closest('.kanban-item').getAttribute('data-eid');
  247. kanban.removeElement(taskId);
  248. });
  249. });
  250. addNewForm.remove();
  251. });
  252. // Remove form on clicking cancel button
  253. addNewForm.querySelector('.cancel-add-item').addEventListener('click', () => addNewForm.remove());
  254. }
  255. });
  256. // Kanban Wrapper scrollbar
  257. if (kanbanWrapper) {
  258. new PerfectScrollbar(kanbanWrapper);
  259. }
  260. const kanbanContainer = document.querySelector('.kanban-container');
  261. const kanbanTitleBoard = Array.from(document.querySelectorAll('.kanban-title-board'));
  262. const kanbanItem = Array.from(document.querySelectorAll('.kanban-item'));
  263. // Render custom items
  264. if (kanbanItem.length) {
  265. kanbanItem.forEach(el => {
  266. const element = `<span class="kanban-text">${el.textContent}</span>`;
  267. let img = '';
  268. if (el.getAttribute('data-image')) {
  269. img = `
  270. <img class="img-fluid rounded mb-2"
  271. src="${assetsPath}img/elements/${el.getAttribute('data-image')}">
  272. `;
  273. }
  274. el.textContent = '';
  275. if (el.getAttribute('data-badge') && el.getAttribute('data-badge-text')) {
  276. el.insertAdjacentHTML(
  277. 'afterbegin',
  278. `${renderHeader(el.getAttribute('data-badge'), el.getAttribute('data-badge-text'))}${img}${element}`
  279. );
  280. }
  281. if (el.getAttribute('data-comments') || el.getAttribute('data-due-date') || el.getAttribute('data-assigned')) {
  282. el.insertAdjacentHTML(
  283. 'beforeend',
  284. renderFooter(
  285. el.getAttribute('data-attachments') || 0,
  286. el.getAttribute('data-comments') || 0,
  287. el.getAttribute('data-assigned') || '',
  288. el.getAttribute('data-members') || ''
  289. )
  290. );
  291. }
  292. });
  293. }
  294. // Initialize tooltips for rendered items
  295. const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
  296. tooltipTriggerList.forEach(tooltipTriggerEl => {
  297. new bootstrap.Tooltip(tooltipTriggerEl);
  298. });
  299. // Prevent sidebar from opening on dropdown button click
  300. const tasksItemDropdown = Array.from(document.querySelectorAll('.kanban-tasks-item-dropdown'));
  301. if (tasksItemDropdown.length) {
  302. tasksItemDropdown.forEach(dropdown => {
  303. dropdown.addEventListener('click', event => {
  304. event.stopPropagation();
  305. });
  306. });
  307. }
  308. // Toggle "add new" input and actions for add-new-btn
  309. if (kanbanAddBoardBtn) {
  310. kanbanAddBoardBtn.addEventListener('click', () => {
  311. kanbanAddNewInput.forEach(el => {
  312. el.value = ''; // Clear input value
  313. el.classList.toggle('d-none'); // Toggle visibility
  314. });
  315. });
  316. }
  317. // Render "add new" inline with boards
  318. if (kanbanContainer) {
  319. kanbanContainer.append(kanbanAddNewBoard);
  320. }
  321. // Makes kanban title editable for rendered boards
  322. if (kanbanTitleBoard) {
  323. kanbanTitleBoard.forEach(elem => {
  324. elem.addEventListener('mouseenter', () => {
  325. elem.contentEditable = 'true';
  326. });
  327. // Appends delete icon with title
  328. elem.insertAdjacentHTML('afterend', renderBoardDropdown());
  329. });
  330. }
  331. // Delete Board for rendered boards
  332. const deleteBoards = Array.from(document.querySelectorAll('.delete-board'));
  333. deleteBoards.forEach(elem => {
  334. elem.addEventListener('click', () => {
  335. const id = elem.closest('.kanban-board').getAttribute('data-id');
  336. kanban.removeBoard(id);
  337. });
  338. });
  339. // Delete task for rendered boards
  340. const deleteTasks = Array.from(document.querySelectorAll('.delete-task'));
  341. deleteTasks.forEach(task => {
  342. task.addEventListener('click', () => {
  343. const id = task.closest('.kanban-item').getAttribute('data-eid');
  344. kanban.removeElement(id);
  345. });
  346. });
  347. // Cancel "Add New Board" input
  348. const cancelAddNew = document.querySelector('.kanban-add-board-cancel-btn');
  349. if (cancelAddNew) {
  350. cancelAddNew.addEventListener('click', () => {
  351. kanbanAddNewInput.forEach(el => {
  352. el.classList.toggle('d-none');
  353. });
  354. });
  355. }
  356. // Add new board
  357. if (kanbanAddNewBoard) {
  358. kanbanAddNewBoard.addEventListener('submit', e => {
  359. e.preventDefault();
  360. const value = e.target.querySelector('.form-control').value.trim();
  361. const id = value.replace(/\s+/g, '-').toLowerCase();
  362. kanban.addBoards([{ id, title: value }]);
  363. // Add delete board option to new board and make title editable
  364. const newBoard = document.querySelector('.kanban-board:last-child');
  365. if (newBoard) {
  366. const header = newBoard.querySelector('.kanban-title-board');
  367. header.insertAdjacentHTML('afterend', renderBoardDropdown());
  368. // Make title editable
  369. header.addEventListener('mouseenter', () => {
  370. header.contentEditable = 'true';
  371. });
  372. // Add delete functionality to new board
  373. const deleteNewBoard = newBoard.querySelector('.delete-board');
  374. if (deleteNewBoard) {
  375. deleteNewBoard.addEventListener('click', () => {
  376. const id = deleteNewBoard.closest('.kanban-board').getAttribute('data-id');
  377. kanban.removeBoard(id);
  378. });
  379. }
  380. }
  381. // Hide input fields
  382. kanbanAddNewInput.forEach(el => {
  383. el.classList.add('d-none');
  384. });
  385. // Re-append the "Add New Board" form
  386. if (kanbanContainer) {
  387. kanbanContainer.append(kanbanAddNewBoard);
  388. }
  389. });
  390. }
  391. // Clear comment editor on Kanban sidebar close
  392. kanbanSidebar.addEventListener('hidden.bs.offcanvas', () => {
  393. const editor = kanbanSidebar.querySelector('.ql-editor').firstElementChild;
  394. if (editor) editor.innerHTML = '';
  395. });
  396. // Re-init tooltip when offcanvas opens(Bootstrap bug)
  397. if (kanbanSidebar) {
  398. kanbanSidebar.addEventListener('shown.bs.offcanvas', () => {
  399. const tooltipTriggerList = Array.from(kanbanSidebar.querySelectorAll('[data-bs-toggle="tooltip"]'));
  400. tooltipTriggerList.forEach(tooltipTriggerEl => {
  401. new bootstrap.Tooltip(tooltipTriggerEl);
  402. });
  403. });
  404. }
  405. })();