Path: blob/master/webroot/rsrc/js/application/projects/WorkboardBoard.js
12242 views
/**1* @provides javelin-workboard-board2* @requires javelin-install3* javelin-dom4* javelin-util5* javelin-stratcom6* javelin-workflow7* phabricator-draggable-list8* javelin-workboard-column9* javelin-workboard-header-template10* javelin-workboard-card-template11* javelin-workboard-order-template12* @javelin13*/1415JX.install('WorkboardBoard', {1617construct: function(controller, phid, root) {18this._controller = controller;19this._phid = phid;20this._root = root;2122this._headers = {};23this._cards = {};24this._orders = {};2526this._buildColumns();27},2829properties: {30order: null,31pointsEnabled: false32},3334members: {35_controller: null,36_phid: null,37_root: null,38_columns: null,39_headers: null,40_cards: null,41_dropPreviewNode: null,42_dropPreviewListNode: null,43_previewPHID: null,44_hidePreivew: false,45_previewPositionVector: null,46_previewDimState: false,4748getRoot: function() {49return this._root;50},5152getColumns: function() {53return this._columns;54},5556getColumn: function(k) {57return this._columns[k];58},5960getPHID: function() {61return this._phid;62},6364getCardTemplate: function(phid) {65if (!this._cards[phid]) {66this._cards[phid] = new JX.WorkboardCardTemplate(phid);67}6869return this._cards[phid];70},7172getHeaderTemplate: function(header_key) {73if (!this._headers[header_key]) {74this._headers[header_key] = new JX.WorkboardHeaderTemplate(header_key);75}7677return this._headers[header_key];78},7980getOrderTemplate: function(order_key) {81if (!this._orders[order_key]) {82this._orders[order_key] = new JX.WorkboardOrderTemplate(order_key);83}8485return this._orders[order_key];86},8788getHeaderTemplatesForOrder: function(order) {89var templates = [];9091for (var k in this._headers) {92var header = this._headers[k];9394if (header.getOrder() !== order) {95continue;96}9798templates.push(header);99}100101templates.sort(JX.bind(this, this._sortHeaderTemplates));102103return templates;104},105106_sortHeaderTemplates: function(u, v) {107return this.compareVectors(u.getVector(), v.getVector());108},109110getController: function() {111return this._controller;112},113114compareVectors: function(u_vec, v_vec) {115for (var ii = 0; ii < u_vec.length; ii++) {116if (u_vec[ii] > v_vec[ii]) {117return 1;118}119120if (u_vec[ii] < v_vec[ii]) {121return -1;122}123}124125return 0;126},127128start: function() {129this._setupDragHandlers();130131// TODO: This is temporary code to make it easier to debug this workflow132// by pressing the "R" key.133var on_reload = JX.bind(this, this._reloadCards);134new JX.KeyboardShortcut('R', 'Reload Card State (Prototype)')135.setHandler(on_reload)136.register();137138var board_phid = this.getPHID();139140JX.Stratcom.listen('aphlict-server-message', null, function(e) {141var message = e.getData();142143if (message.type != 'workboards') {144return;145}146147// Check if this update notification is about the currently visible148// board. If it is, update the board state.149150var found_board = false;151for (var ii = 0; ii < message.subscribers.length; ii++) {152var subscriber_phid = message.subscribers[ii];153if (subscriber_phid === board_phid) {154found_board = true;155break;156}157}158159if (found_board) {160on_reload();161}162});163164JX.Stratcom.listen('aphlict-reconnect', null, function(e) {165on_reload();166});167168for (var k in this._columns) {169this._columns[k].redraw();170}171},172173_buildColumns: function() {174var nodes = JX.DOM.scry(this.getRoot(), 'ul', 'project-column');175176this._columns = {};177for (var ii = 0; ii < nodes.length; ii++) {178var node = nodes[ii];179var data = JX.Stratcom.getData(node);180var phid = data.columnPHID;181182this._columns[phid] = new JX.WorkboardColumn(this, phid, node);183}184185var on_over = JX.bind(this, this._showTriggerPreview);186var on_out = JX.bind(this, this._hideTriggerPreview);187JX.Stratcom.listen('mouseover', 'trigger-preview', on_over);188JX.Stratcom.listen('mouseout', 'trigger-preview', on_out);189190var on_move = JX.bind(this, this._dimPreview);191JX.Stratcom.listen('mousemove', null, on_move);192},193194_dimPreview: function(e) {195var p = this._previewPositionVector;196if (!p) {197return;198}199200// When the mouse cursor gets near the drop preview element, fade it201// out so you can see through it. We can't do this with ":hover" because202// we disable cursor events.203204var cursor = JX.$V(e);205var margin = 64;206207var near_x = (cursor.x > (p.x - margin));208var near_y = (cursor.y > (p.y - margin));209var should_dim = (near_x && near_y);210211this._setPreviewDimState(should_dim);212},213214_setPreviewDimState: function(is_dim) {215if (is_dim === this._previewDimState) {216return;217}218219this._previewDimState = is_dim;220var node = this._getDropPreviewNode();221JX.DOM.alterClass(node, 'workboard-drop-preview-fade', is_dim);222},223224_showTriggerPreview: function(e) {225if (this._disablePreview) {226return;227}228229var target = e.getTarget();230var node = e.getNode('trigger-preview');231232if (target !== node) {233return;234}235236var phid = JX.Stratcom.getData(node).columnPHID;237var column = this._columns[phid];238239// Bail out if we don't know anything about this column.240if (!column) {241return;242}243244if (phid === this._previewPHID) {245return;246}247248this._previewPHID = phid;249250var effects = column.getDropEffects();251252var triggers = [];253for (var ii = 0; ii < effects.length; ii++) {254if (effects[ii].getIsTriggerEffect()) {255triggers.push(effects[ii]);256}257}258259if (triggers.length) {260var header = column.getTriggerPreviewEffect();261triggers = [header].concat(triggers);262}263264this._showEffects(triggers);265},266267_hideTriggerPreview: function(e) {268if (this._disablePreview) {269return;270}271272var target = e.getTarget();273274if (target !== e.getNode('trigger-preview')) {275return;276}277278this._removeTriggerPreview();279},280281_removeTriggerPreview: function() {282this._showEffects([]);283this._previewPHID = null;284},285286_beginDrag: function() {287this._disablePreview = true;288this._showEffects([]);289},290291_endDrag: function() {292this._disablePreview = false;293},294295_setupDragHandlers: function() {296var columns = this.getColumns();297298var order_template = this.getOrderTemplate(this.getOrder());299var has_headers = order_template.getHasHeaders();300var can_reorder = order_template.getCanReorder();301302var lists = [];303for (var k in columns) {304var column = columns[k];305306var list = new JX.DraggableList('draggable-card', column.getRoot())307.setOuterContainer(this.getRoot())308.setFindItemsHandler(JX.bind(column, column.getDropTargetNodes))309.setCanDragX(true)310.setHasInfiniteHeight(true)311.setIsDropTargetHandler(JX.bind(column, column.setIsDropTarget));312313var default_handler = list.getGhostHandler();314list.setGhostHandler(315JX.bind(column, column.handleDragGhost, default_handler));316317// The "compare handler" locks cards into a specific position in the318// column.319list.setCompareHandler(JX.bind(column, column.compareHandler));320321// If the view has group headers, we lock cards into the right position322// when moving them between columns, but not within a column.323if (has_headers) {324list.setCompareOnMove(true);325}326327// If we can't reorder cards, we always lock them into their current328// position.329if (!can_reorder) {330list.setCompareOnMove(true);331list.setCompareOnReorder(true);332}333334list.setTargetChangeHandler(JX.bind(this, this._didChangeDropTarget));335336list.listen('didDrop', JX.bind(this, this._onmovecard, list));337338list.listen('didBeginDrag', JX.bind(this, this._beginDrag));339list.listen('didEndDrag', JX.bind(this, this._endDrag));340341lists.push(list);342}343344for (var ii = 0; ii < lists.length; ii++) {345lists[ii].setGroup(lists);346}347},348349_didChangeDropTarget: function(src_list, src_node, dst_list, dst_node) {350if (!dst_list) {351// The card is being dragged into a dead area, like the left menu.352this._showEffects([]);353return;354}355356if (dst_node === false) {357// The card is being dragged over itself, so dropping it won't358// affect anything.359this._showEffects([]);360return;361}362363var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID;364var dst_phid = JX.Stratcom.getData(dst_list.getRootNode()).columnPHID;365366var src_column = this.getColumn(src_phid);367var dst_column = this.getColumn(dst_phid);368369var effects = [];370if (src_column !== dst_column) {371effects = effects.concat(dst_column.getDropEffects());372}373374var context = this._getDropContext(dst_node);375if (context.headerKey) {376var header = this.getHeaderTemplate(context.headerKey);377effects = effects.concat(header.getDropEffects());378}379380var card_phid = JX.Stratcom.getData(src_node).objectPHID;381var card = src_column.getCard(card_phid);382383var visible = [];384for (var ii = 0; ii < effects.length; ii++) {385if (effects[ii].isEffectVisibleForCard(card)) {386visible.push(effects[ii]);387}388}389effects = visible;390391this._showEffects(effects);392},393394_showEffects: function(effects) {395var node = this._getDropPreviewNode();396397if (!effects.length) {398JX.DOM.remove(node);399this._previewPositionVector = null;400return;401}402403var items = [];404for (var ii = 0; ii < effects.length; ii++) {405var effect = effects[ii];406items.push(effect.newNode());407}408409JX.DOM.setContent(this._getDropPreviewListNode(), items);410document.body.appendChild(node);411412// Undim the drop preview element if it was previously dimmed.413this._setPreviewDimState(false);414this._previewPositionVector = JX.$V(node);415},416417_getDropPreviewNode: function() {418if (!this._dropPreviewNode) {419var attributes = {420className: 'workboard-drop-preview'421};422423var content = [424this._getDropPreviewListNode()425];426427this._dropPreviewNode = JX.$N('div', attributes, content);428}429430return this._dropPreviewNode;431},432433_getDropPreviewListNode: function() {434if (!this._dropPreviewListNode) {435var attributes = {};436this._dropPreviewListNode = JX.$N('ul', attributes);437}438439return this._dropPreviewListNode;440},441442_findCardsInColumn: function(column_node) {443return JX.DOM.scry(column_node, 'li', 'project-card');444},445446_getDropContext: function(after_node, item) {447var header_key;448var after_phids = [];449var before_phids = [];450451// We're going to send an "afterPHID" and a "beforePHID" if the card452// was dropped immediately adjacent to another card. If a card was453// dropped before or after a header, we don't send a PHID for the card454// on the other side of the header.455456// If the view has headers, we always send the header the card was457// dropped under.458459var after_data;460var after_card = after_node;461while (after_card) {462after_data = JX.Stratcom.getData(after_card);463464if (after_data.headerKey) {465break;466}467468if (after_data.objectPHID) {469after_phids.push(after_data.objectPHID);470}471472after_card = after_card.previousSibling;473}474475if (item) {476var before_data;477var before_card = item.nextSibling;478while (before_card) {479before_data = JX.Stratcom.getData(before_card);480481if (before_data.headerKey) {482break;483}484485if (before_data.objectPHID) {486before_phids.push(before_data.objectPHID);487}488489before_card = before_card.nextSibling;490}491}492493var header_data;494var header_node = after_node;495while (header_node) {496header_data = JX.Stratcom.getData(header_node);497if (header_data.headerKey) {498break;499}500header_node = header_node.previousSibling;501}502503if (header_data) {504header_key = header_data.headerKey;505}506507return {508headerKey: header_key,509afterPHIDs: after_phids,510beforePHIDs: before_phids511};512},513514_onmovecard: function(list, item, after_node, src_list) {515list.lock();516JX.DOM.alterClass(item, 'drag-sending', true);517518var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID;519var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID;520521var item_phid = JX.Stratcom.getData(item).objectPHID;522var data = {523objectPHID: item_phid,524columnPHID: dst_phid,525order: this.getOrder()526};527528var context = this._getDropContext(after_node, item);529data.afterPHIDs = context.afterPHIDs.join(',');530data.beforePHIDs = context.beforePHIDs.join(',');531532if (context.headerKey) {533var properties = this.getHeaderTemplate(context.headerKey)534.getEditProperties();535data.header = JX.JSON.stringify(properties);536}537538var visible_phids = [];539var column = this.getColumn(dst_phid);540for (var object_phid in column.getCards()) {541visible_phids.push(object_phid);542}543544data.visiblePHIDs = visible_phids.join(',');545546// If the user cancels the workflow (for example, by hitting an MFA547// prompt that they click "Cancel" on), put the card back where it was548// and reset the UI state.549var on_revert = JX.bind(550this,551this._revertCard,552list,553item,554src_phid,555dst_phid);556557var after_phid = null;558if (data.afterPHIDs.length) {559after_phid = data.afterPHIDs[0];560}561562var onupdate = JX.bind(563this,564this._oncardupdate,565list,566src_phid,567dst_phid,568after_phid);569570new JX.Workflow(this.getController().getMoveURI(), data)571.setHandler(onupdate)572.setCloseHandler(on_revert)573.start();574},575576_revertCard: function(list, item, src_phid, dst_phid) {577JX.DOM.alterClass(item, 'drag-sending', false);578579var src_column = this.getColumn(src_phid);580var dst_column = this.getColumn(dst_phid);581582src_column.markForRedraw();583dst_column.markForRedraw();584this._redrawColumns();585586list.unlock();587},588589_oncardupdate: function(list, src_phid, dst_phid, after_phid, response) {590this.updateCard(response);591592var sounds = response.sounds || [];593for (var ii = 0; ii < sounds.length; ii++) {594JX.Sound.queue(sounds[ii]);595}596597list.unlock();598},599600updateCard: function(response) {601var columns = this.getColumns();602var column_phid;603var card_phid;604var card_data;605606// The server may send us a full or partial update for a card. If we've607// received a full update, we're going to redraw the entire card and may608// need to change which columns it appears in.609610// For a partial update, we've just received supplemental sorting or611// property information and do not need to perform a full redraw.612613// When we reload card state, edit a card, or move a card, we get a full614// update for the card.615616// Ween we move a card in a column, we may get a partial update for other617// visible cards in the column.618619620// Figure out which columns each card now appears in. For cards that621// have received a full update, we'll use this map to move them into622// the correct columns.623var update_map = {};624for (column_phid in response.columnMaps) {625var target_column = this.getColumn(column_phid);626627if (!target_column) {628// If the column isn't visible, don't try to add a card to it.629continue;630}631632var column_map = response.columnMaps[column_phid];633634for (var ii = 0; ii < column_map.length; ii++) {635card_phid = column_map[ii];636if (!update_map[card_phid]) {637update_map[card_phid] = {};638}639update_map[card_phid][column_phid] = true;640}641}642643// Process card removals. These are cases where the client still sees644// a particular card on a board but it has been removed on the server.645for (card_phid in response.cards) {646card_data = response.cards[card_phid];647648if (!card_data.remove) {649continue;650}651652for (column_phid in columns) {653var column = columns[column_phid];654655var card = column.getCard(card_phid);656if (card) {657column.removeCard(card_phid);658column.markForRedraw();659}660}661}662663// Process partial updates for cards. This is supplemental data which664// we can just merge in without any special handling.665for (card_phid in response.cards) {666card_data = response.cards[card_phid];667668if (card_data.remove) {669continue;670}671672var card_template = this.getCardTemplate(card_phid);673674if (card_data.nodeHTMLTemplate) {675card_template.setNodeHTMLTemplate(card_data.nodeHTMLTemplate);676}677678var order;679for (order in card_data.vectors) {680card_template.setSortVector(order, card_data.vectors[order]);681}682683for (order in card_data.headers) {684card_template.setHeaderKey(order, card_data.headers[order]);685}686687for (var key in card_data.properties) {688card_template.setObjectProperty(key, card_data.properties[key]);689}690}691692// Process full updates for cards which we have a full update for. This693// may involve moving them between columns.694for (card_phid in response.cards) {695card_data = response.cards[card_phid];696697if (!card_data.update) {698continue;699}700701for (column_phid in columns) {702var column = columns[column_phid];703var card = column.getCard(card_phid);704705if (card) {706card.redraw();707column.markForRedraw();708}709710// Compare the server state to the client state, and add or remove711// cards on the client as necessary to synchronize them.712713if (update_map[card_phid] && update_map[card_phid][column_phid]) {714if (!card) {715column.newCard(card_phid);716column.markForRedraw();717}718} else {719if (card) {720column.removeCard(card_phid);721column.markForRedraw();722}723}724}725}726727var column_maps = response.columnMaps;728var natural_column;729for (var natural_phid in column_maps) {730natural_column = this.getColumn(natural_phid);731if (!natural_column) {732// Our view of the board may be out of date, so we might get back733// information about columns that aren't visible. Just ignore the734// position information for any columns we aren't displaying on the735// client.736continue;737}738739natural_column.setNaturalOrder(column_maps[natural_phid]);740}741742var headers = response.headers;743for (var jj = 0; jj < headers.length; jj++) {744var header = headers[jj];745746this.getHeaderTemplate(header.key)747.setOrder(header.order)748.setNodeHTMLTemplate(header.template)749.setVector(header.vector)750.setEditProperties(header.editProperties);751}752753this._redrawColumns();754},755756_redrawColumns: function() {757var columns = this.getColumns();758for (var k in columns) {759if (columns[k].isMarkedForRedraw()) {760columns[k].redraw();761}762}763},764765_reloadCards: function() {766var state = {};767768var columns = this.getColumns();769for (var column_phid in columns) {770var cards = columns[column_phid].getCards();771for (var card_phid in cards) {772state[card_phid] = this.getCardTemplate(card_phid).getVersion();773}774}775776var data = {777state: JX.JSON.stringify(state),778order: this.getOrder()779};780781var on_reload = JX.bind(this, this._onReloadResponse);782783new JX.Request(this.getController().getReloadURI(), on_reload)784.setData(data)785.send();786},787788_onReloadResponse: function(response) {789this.updateCard(response);790}791792}793794});795796797