define([ 'jquery', 'json.sortify', '/bower_components/nthen/index.js', '/common/sframe-common.js', '/common/sframe-app-framework.js', '/common/sframe-common-codemirror.js', '/common/common-util.js', '/common/common-hash.js', '/common/common-interface.js', '/common/common-ui-elements.js', '/common/inner/common-mediatag.js', '/customize/messages.js', '/common/hyperscript.js', '/common/text-cursor.js', '/common/diffMarked.js', '/bower_components/chainpad/chainpad.dist.js', '/bower_components/marked/marked.min.js', 'cm/lib/codemirror', '/kanban/jkanban_cp.js', 'cm/mode/gfm/gfm', 'cm/addon/edit/closebrackets', 'cm/addon/edit/matchbrackets', 'cm/addon/edit/trailingspace', 'cm/addon/selection/active-line', 'cm/addon/search/search', 'cm/addon/search/match-highlighter', 'css!/bower_components/codemirror/lib/codemirror.css', 'css!/bower_components/codemirror/addon/dialog/dialog.css', 'css!/bower_components/codemirror/addon/fold/foldgutter.css', 'css!/kanban/jkanban.css', 'less!/kanban/app-kanban.less' ], function ( $, Sortify, nThen, SFCommon, Framework, SFCodeMirror, Util, Hash, UI, UIElements, MT, Messages, h, TextCursor, DiffMd, ChainPad, Marked, CodeMirror, jKanban) { var verbose = function (x) { console.log(x); }; verbose = function () {}; // comment out to enable verbose logging var onRedraw = Util.mkEvent(); var onCursorUpdate = Util.mkEvent(); var remoteCursors = {}; var setValueAndCursor = function (input, val, _cursor) { if (!input) { return; } var $input = $(input); var focus = _cursor || $input.is(':focus'); var oldVal = $input.val(); var ops = ChainPad.Diff.diff(_cursor ? _cursor.value : oldVal, val); var cursor = _cursor || input; var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { return TextCursor.transformCursor(cursor[attr], ops); }); $input.val(val); if (focus) { $input.focus(); } input.selectionStart = selects[0]; input.selectionEnd = selects[1]; }; var getTextColor = function (hex) { if (hex && /^#/.test(hex)) { hex = hex.slice(1); } if (!/^[0-9a-f]{6}$/i.test(hex)) { return '#000000'; } var r = parseInt(hex.slice(0,2), 16); var g = parseInt(hex.slice(2,4), 16); var b = parseInt(hex.slice(4,6), 16); if ((r*0.213 + g*0.715 + b*0.072) > 255/2) { return '#000000'; } return '#FFFFFF'; }; var getAvatar = function (cursor, noClear) { // Tippy var html = MT.getCursorAvatar(cursor); var l = Util.getFirstCharacter(cursor.name || Messages.anonymous); var text = ''; if (cursor.color) { text = 'color:'+getTextColor(cursor.color)+';'; } var avatar = h('span.cp-cursor.cp-tippy-html', { style: "background-color: " + (cursor.color || 'red') + ";"+text, 'data-cptippy-html': true, title: html }, l); if (!noClear) { cursor.clear = function () { $(avatar).remove(); }; } return avatar; }; var getExistingTags = function (boards) { var tags = []; boards = boards || {}; Object.keys(boards.items || {}).forEach(function (id) { var data = boards.items[id]; if (!Array.isArray(data.tags)) { return; } data.tags.forEach(function (_tag) { var tag = _tag.toLowerCase(); if (tags.indexOf(tag) === -1) { tags.push(tag); } }); }); tags.sort(); return tags; }; var addEditItemButton = function () {}; var now = function () { return +new Date(); }; var _lastUpdate = 0; var _updateBoards = function (framework, kanban, boards) { _lastUpdate = now(); kanban.setBoards(Util.clone(boards)); kanban.inEditMode = false; addEditItemButton(framework, kanban); }; var _updateBoardsThrottle = Util.throttle(_updateBoards, 500); var updateBoards = function (framework, kanban, boards) { if ((now() - _lastUpdate) > 5000) { _updateBoards(framework, kanban, boards); return; } _updateBoardsThrottle(); }; var onRemoteChange = Util.mkEvent(); var editModal; var PROPERTIES = ['title', 'body', 'tags', 'color']; var BOARD_PROPERTIES = ['title', 'color']; var createEditModal = function (framework, kanban) { if (framework.isReadOnly()) { return; } if (editModal) { return editModal; } var dataObject = {}; var isBoard, id; var offline = false; var update = function () { updateBoards(framework, kanban, kanban.options.boards); }; var commit = function () { framework.localChange(); update(); }; var conflicts, conflictContainer, titleInput, tagsDiv, colors, text; var content = h('div', [ conflictContainer = h('div#cp-kanban-edit-conflicts', [ h('div', Messages.kanban_conflicts), conflicts = h('div.cp-kanban-cursors') ]), h('label', {for:'cp-kanban-edit-title'}, Messages.kanban_title), titleInput = h('input#cp-kanban-edit-title'), h('label', {for:'cp-kanban-edit-body'}, Messages.kanban_body), h('div#cp-kanban-edit-body', [ text = h('textarea') ]), h('label', {for:'cp-kanban-edit-tags'}, Messages.fm_tagsName), tagsDiv = h('div#cp-kanban-edit-tags'), h('label', {for:'cp-kanban-edit-color'}, Messages.kanban_color), colors = h('div#cp-kanban-edit-colors'), ]); var $tags = $(tagsDiv); var $conflict = $(conflicts); var $cc = $(conflictContainer); var conflict = { setValue: function () { $conflict.empty(); var i = 0; $cc.hide(); Object.keys(remoteCursors).forEach(function (nid) { var c = remoteCursors[nid]; var avatar = getAvatar(c, true); if (Number(c.item) === Number(id) || Number(c.board) === Number(id)) { $conflict.append(avatar); i++; } }); if (!i) { return; } $cc.show(); } }; // Title var $title = $(titleInput); $title.on('change keyup', function () { dataObject.title = $title.val(); commit(); }); var title = { getValue: function () { return $title.val(); }, setValue: function (val, preserveCursor) { if (!preserveCursor) { $title.val(val); } else { setValueAndCursor(titleInput, val); } } }; // Body var cm = SFCodeMirror.create("gfm", CodeMirror, text); var editor = cm.editor; editor.setOption('gutters', []); editor.setOption('lineNumbers', false); editor.setOption('readOnly', false); editor.on('keydown', function (editor, e) { if (e.which === 27) { // Focus the next form element but don't close the modal (stopPropagation) $tags.find('.token-input').focus(); } e.stopPropagation(); }); var common = framework._.sfCommon; var markdownTb = common.createMarkdownToolbar(editor); $(text).before(markdownTb.toolbar); $(markdownTb.toolbar).show(); editor.refresh(); var body = { getValue: function () { return editor.getValue(); }, setValue: function (val, preserveCursor) { if (isBoard) { return; } if (!preserveCursor) { editor.setValue(val || ''); editor.save(); } else { SFCodeMirror.setValueAndCursor(editor, editor.getValue(), val || ''); } }, refresh: function () { editor.refresh(); } }; cm.configureTheme(common, function () {}); SFCodeMirror.mkIndentSettings(editor, framework._.cpNfInner.metadataMgr); editor.on('change', function () { var val = editor.getValue(); if (dataObject.body === val) { return; } dataObject.body = val; commit(); }); setTimeout(function () { var privateData = framework._.cpNfInner.metadataMgr.getPrivateData(); var fmConfig = { dropArea: $('.CodeMirror'), body: $('body'), onUploaded: function (ev, data) { var parsed = Hash.parsePadUrl(data.url); var secret = Hash.getSecrets('file', parsed.hash, data.password); var fileHost = privateData.fileHost || privateData.origin; var src = fileHost + Hash.getBlobPathFromHex(secret.channel); var key = Hash.encodeBase64(secret.keys.cryptKey); var mt = ''; editor.replaceSelection(mt); } }; common.createFileManager(fmConfig); }); // Tags var _field, initialTags; var tags = { getValue: function () { if (!_field) { return; } return _field.getTokens(); }, setValue: function (tags, preserveCursor) { if (isBoard) { return; } if (preserveCursor && initialTags && Sortify(tags || []) === initialTags) { // Don't redraw if there is no change return; } initialTags = Sortify(tags || []); $tags.empty(); var input = UI.dialog.textInput(); $tags.append(input); var existing = getExistingTags(kanban.options.boards); _field = UI.tokenField(input, existing).preventDuplicates(function (val) { UI.warn(Messages._getKey('tags_duplicate', [val])); }); _field.setTokens(tags || []); $tags.find('.token-input').on('keydown', function (e) { // if the tokenfield is blank and the user hits enter or escape // then allow the event to propogate (closing the modal) // this can leave behind the autocomplete menu, so forcefully hide it if (!$(this).val() && [13, 27].indexOf(e.which) !== -1) { return void $('.ui-autocomplete.ui-front').hide(); } e.stopPropagation(); }); var commitTags = function () { if (offline) { return; } setTimeout(function () { dataObject.tags = Util.deduplicateString(_field.getTokens().map(function (t) { return t.toLowerCase(); })); initialTags = Sortify(dataObject.tags); commit(); }); }; _field.tokenfield.on('tokenfield:createdtoken', commitTags); _field.tokenfield.on('tokenfield:editedoken', commitTags); _field.tokenfield.on('tokenfield:removedtoken', commitTags); } }; // Colors var $colors = $(colors); var palette = ['']; for (var i=1; i<=8; i++) { palette.push('color'+i); } var selectedColor = ''; palette.forEach(function (color) { var $color = $(h('span.cp-kanban-palette.fa')); $color.addClass('cp-kanban-palette-'+(color || 'nocolor')); $color.click(function () { if (offline) { return; } if (color === selectedColor) { return; } selectedColor = color; $colors.find('.cp-kanban-palette').removeClass('fa-check'); var $col = $colors.find('.cp-kanban-palette-'+(color || 'nocolor')); $col.addClass('fa-check'); dataObject.color = color; commit(); }).appendTo($colors); }); var color = { getValue: function () { return selectedColor; }, setValue: function (color) { $colors.find('.cp-kanban-palette').removeClass('fa-check'); var $col = $colors.find('.cp-kanban-palette-'+(color || 'nocolor')); $col.addClass('fa-check'); selectedColor = color; } }; var button = [{ className: 'danger left', name: Messages.kanban_delete, confirm: true, onClick: function (/*button*/) { var boards = kanban.options.boards || {}; if (isBoard) { var list = boards.list || []; var idx = list.indexOf(id); if (idx !== -1) { list.splice(idx, 1); } delete (boards.data || {})[id]; kanban.removeBoard(id); return void commit(); } Object.keys(boards.data || {}).forEach(function (boardId) { var board = boards.data[boardId]; if (!board) { return; } var items = board.item || []; var idx = items.indexOf(id); if (idx !== -1) { items.splice(idx, 1); } }); delete (boards.items || {})[id]; commit(); }, keys: [] }, { className: 'primary', name: Messages.filePicker_close, onClick: function () { onCursorUpdate.fire({}); }, keys: [13, 27] }]; var modal = UI.dialog.customModal(content, { buttons: button }); modal.classList.add('cp-kanban-edit-modal'); var $modal = $(modal); framework.onEditableChange(function (unlocked) { editor.setOption('readOnly', !unlocked); $title.prop('disabled', unlocked ? '' : 'disabled'); $(_field.element).tokenfield(unlocked ? 'enable' : 'disable'); $modal.find('nav button.danger').prop('disabled', unlocked ? '' : 'disabled'); offline = !unlocked; }); var setId = function (_isBoard, _id) { // Reset the mdoal with a new id isBoard = _isBoard; id = Number(_id); if (_isBoard) { onCursorUpdate.fire({ board: _id }); dataObject = kanban.getBoardJSON(id); $(content) .find('#cp-kanban-edit-body, #cp-kanban-edit-tags, [for="cp-kanban-edit-body"], [for="cp-kanban-edit-tags"]') .hide(); } else { onCursorUpdate.fire({ item: _id }); dataObject = kanban.getItemJSON(id); $(content) .find('#cp-kanban-edit-body, #cp-kanban-edit-tags, [for="cp-kanban-edit-body"], [for="cp-kanban-edit-tags"]') .show(); } // Also reset the buttons $modal.find('nav').after(UI.dialog.getButtons(button)).remove(); }; onRemoteChange.reg(function () { if (isBoard) { dataObject = kanban.getBoardJSON(id); } else { dataObject = kanban.getItemJSON(id); } // Check if our item has been deleted if (!dataObject) { var $frame = $(modal).parents('.alertify').first(); if ($frame[0] && $frame[0].closeModal) { $frame[0].closeModal(); } return; } // Not deleted, apply updates editModal.conflict.setValue(); PROPERTIES.forEach(function (type) { editModal[type].setValue(dataObject[type], true); }); }); return { modal: modal, setId: setId, title: title, body: body, tags: tags, color: color, conflict: conflict }; }; var getItemEditModal = function (framework, kanban, eid) { // Create modal if needed if (!editModal) { editModal = createEditModal(framework, kanban); } editModal.setId(false, eid); var boards = kanban.options.boards || {}; var item = (boards.items || {})[eid]; if (!item) { return void UI.warn(Messages.error); } editModal.conflict.setValue(); PROPERTIES.forEach(function (type) { if (!editModal[type]) { return; } editModal[type].setValue(item[type]); }); UI.openCustomModal(editModal.modal); editModal.body.refresh(); }; var getBoardEditModal = function (framework, kanban, id) { // Create modal if needed if (!editModal) { editModal = createEditModal(framework, kanban); } editModal.setId(true, id); var boards = kanban.options.boards || {}; var board = (boards.data || {})[id]; if (!board) { return void UI.warn(Messages.error); } editModal.conflict.setValue(); BOARD_PROPERTIES.forEach(function (type) { if (!editModal[type]) { return; } editModal[type].setValue(board[type]); }); UI.openCustomModal(editModal.modal); }; addEditItemButton = function (framework, kanban) { if (!kanban) { return; } if (framework.isReadOnly() || framework.isLocked()) { return; } var $container = $(kanban.element); $container.find('.kanban-edit-item').remove(); $container.find('.kanban-item').each(function (i, el) { var itemId = $(el).attr('data-eid'); $('