Renaissance

_,,ad8888888888bba,_
                  ,ad88888I888888888888888ba,
                ,88888888I88888888888888888888a,
              ,d888888888I8888888888888888888888b,
             d88888PP"""" ""YY88888888888888888888b,
           ,d88"'__,,--------,,,,.;ZZZY8888888888888,
          ,8IIl'"                ;;l"ZZZIII8888888888,
         ,I88l;'                  ;lZZZZZ888III8888888,
       ,II88Zl;.                  ;llZZZZZ888888I888888,
      ,II888Zl;.                .;;;;;lllZZZ888888I8888b
     ,II8888Z;;                 `;;;;;''llZZ8888888I8888,
     II88888Z;'                        .;lZZZ8888888I888b
     II88888Z; _,aaa,      .,aaaaa,__.l;llZZZ88888888I888
     II88888IZZZZZZZZZ,  .ZZZZZZZZZZZZZZ;llZZ88888888I888,
     II88888IZZ<'(@@>Z|  |ZZZ<'(@@>ZZZZ;;llZZ888888888I88I
    ,II88888;   `""" ;|  |ZZ; `"""     ;;llZ8888888888I888
    II888888l            `;;          .;llZZ8888888888I888,
   ,II888888Z;           ;;;        .;;llZZZ8888888888I888I
   III888888Zl;    ..,   `;;       ,;;lllZZZ88888888888I888
   II88888888Z;;...;(_    _)      ,;;;llZZZZ88888888888I888,
   II88888888Zl;;;;;' `--'Z;.   .,;;;;llZZZZ88888888888I888b
   ]I888888888Z;;;;'   ";llllll;..;;;lllZZZZ88888888888I8888,
   II888888888Zl.;;"Y88bd888P";;,..;lllZZZZZ88888888888I8888I
   II8888888888Zl;.; `"PPP";;;,..;lllZZZZZZZ88888888888I88888
   II888888888888Zl;;. `;;;l;;;;lllZZZZZZZZW88888888888I88888
   `II8888888888888Zl;.    ,;;lllZZZZZZZZWMZ88888888888I88888
    II8888888888888888ZbaalllZZZZZZZZZWWMZZZ8888888888I888888,
    `II88888888888888888b"WWZZZZZWWWMMZZZZZZI888888888I888888b
     `II88888888888888888;ZZMMMMMMZZZZZZZZllI888888888I8888888
      `II8888888888888888 `;lZZZZZZZZZZZlllll888888888I8888888,
       II8888888888888888, `;lllZZZZllllll;;.Y88888888I8888888b,
      ,II8888888888888888b   .;;lllllll;;;.;..88888888I88888888b,
      II888888888888888PZI;.  .`;;;.;;;..; ...88888888I8888888888,
      II888888888888PZ;;';;.   ;. .;.  .;. .. Y8888888I88888888888b,
     ,II888888888PZ;;'                        `8888888I8888888888888b,
     II888888888'                              888888I8888888888888888
    ,II888888888                              ,888888I8888888888888888
   ,d88888888888                              d888888I8888888888ZZZZZZ
,ad888888888888I                              8888888I8888ZZZZZZZZZZZZ
888888888888888'                              888888IZZZZZZZZZZZZZZZZZ
8888888888P'8P'                               Y888ZZZZZZZZZZZZZZZZZZZZ
888888888,  "                                 ,ZZZZZZZZZZZZZZZZZZZZZZZ
8888888888,                                ,ZZZZZZZZZZZZZZZZZZZZZZZZZZ
888888888888a,      _                    ,ZZZZZZZZZZZZZZZZZZZZ88888888
888888888888888ba,_d'                  ,ZZZZZZZZZZZZZZZZZ8888888888888
8888888888888888888888bbbaaa,,,______,ZZZZZZZZZZZZZZZ88888888888888888
88888888888888888888888888888888888ZZZZZZZZZZZZZZZ88888888888888888888
8888888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888
888888888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888888888
8888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888
88888888888888888888888888888ZZZZZZZZZZZZZZ888888888888888888888888888
8888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888 Normand  8
88888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888 Veilleux 8
8888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888888888
This commit is contained in:
Maxime Quandalle 2015-05-12 19:20:58 +02:00
commit 2dbea30842
128 changed files with 10521 additions and 0 deletions

152
client/lib/emoji-values.js Normal file
View file

@ -0,0 +1,152 @@
Emoji.values = ['+1', '-1', '100', '1234', '8ball', 'a', 'ab', 'abc', 'abcd',
'accept', 'aerial_tramway', 'airplane', 'alarm_clock', 'alien', 'ambulance',
'anchor', 'angel', 'anger', 'angry', 'anguished', 'ant', 'apple', 'aquarius',
'aries', 'arrow_backward', 'arrow_double_down', 'arrow_double_up', 'arrow_down',
'arrow_down_small', 'arrow_forward', 'arrow_heading_down', 'arrow_heading_up',
'arrow_left', 'arrow_lower_left', 'arrow_lower_right', 'arrow_right',
'arrow_right_hook', 'arrow_up', 'arrow_up_down', 'arrow_up_small',
'arrow_upper_left', 'arrow_upper_right', 'arrows_clockwise',
'arrows_counterclockwise', 'art', 'articulated_lorry', 'astonished', 'atm', 'b',
'baby', 'baby_bottle', 'baby_chick', 'baby_symbol', 'baggage_claim', 'balloon',
'ballot_box_with_check', 'bamboo', 'banana', 'bangbang', 'bank', 'bar_chart',
'barber', 'baseball', 'basketball', 'bath', 'bathtub', 'battery', 'bear', 'bee',
'beer', 'beers', 'beetle', 'beginner', 'bell', 'bento', 'bicyclist', 'bike',
'bikini', 'bird', 'birthday', 'black_circle', 'black_joker', 'black_nib',
'black_square', 'black_square_button', 'blossom', 'blowfish', 'blue_book',
'blue_car', 'blue_heart', 'blush', 'boar', 'boat', 'bomb', 'book', 'bookmark',
'bookmark_tabs', 'books', 'boom', 'boot', 'bouquet', 'bow', 'bowling', 'bowtie',
'boy', 'bread', 'bride_with_veil', 'bridge_at_night', 'briefcase',
'broken_heart', 'bug', 'bulb', 'bullettrain_front', 'bullettrain_side', 'bus',
'busstop', 'bust_in_silhouette', 'busts_in_silhouette', 'cactus', 'cake',
'calendar', 'calling', 'camel', 'camera', 'cancer', 'candy', 'capital_abcd',
'capricorn', 'car', 'card_index', 'carousel_horse', 'cat', 'cat2', 'cd',
'chart', 'chart_with_downwards_trend', 'chart_with_upwards_trend',
'checkered_flag', 'cherries', 'cherry_blossom', 'chestnut', 'chicken',
'children_crossing', 'chocolate_bar', 'christmas_tree', 'church', 'cinema',
'circus_tent', 'city_sunrise', 'city_sunset', 'cl', 'clap', 'clapper',
'clipboard', 'clock1', 'clock10', 'clock1030', 'clock11', 'clock1130',
'clock12', 'clock1230', 'clock130', 'clock2', 'clock230', 'clock3', 'clock330',
'clock4', 'clock430', 'clock5', 'clock530', 'clock6', 'clock630', 'clock7',
'clock730', 'clock8', 'clock830', 'clock9', 'clock930', 'closed_book',
'closed_lock_with_key', 'closed_umbrella', 'cloud', 'clubs', 'cn', 'cocktail',
'coffee', 'cold_sweat', 'collision', 'computer', 'confetti_ball', 'confounded',
'confused', 'congratulations', 'construction', 'construction_worker',
'convenience_store', 'cookie', 'cool', 'cop', 'copyright', 'corn', 'couple',
'couple_with_heart', 'couplekiss', 'cow', 'cow2', 'credit_card', 'crocodile',
'crossed_flags', 'crown', 'cry', 'crying_cat_face', 'crystal_ball', 'cupid',
'curly_loop', 'currency_exchange', 'curry', 'custard', 'customs', 'cyclone',
'dancer', 'dancers', 'dango', 'dart', 'dash', 'date', 'de', 'deciduous_tree',
'department_store', 'diamond_shape_with_a_dot_inside', 'diamonds',
'disappointed', 'disappointed_relieved', 'dizzy', 'dizzy_face', 'do_not_litter',
'dog', 'dog2', 'dollar', 'dolls', 'dolphin', 'donut', 'door', 'doughnut',
'dragon', 'dragon_face', 'dress', 'dromedary_camel', 'droplet', 'dvd', 'e-mail',
'ear', 'ear_of_rice', 'earth_africa', 'earth_americas', 'earth_asia', 'egg',
'eggplant', 'eight', 'eight_pointed_black_star', 'eight_spoked_asterisk',
'electric_plug', 'elephant', 'email', 'end', 'envelope', 'es', 'euro',
'european_castle', 'european_post_office', 'evergreen_tree', 'exclamation',
'expressionless', 'eyeglasses', 'eyes', 'facepunch', 'factory', 'fallen_leaf',
'family', 'fast_forward', 'fax', 'fearful', 'feelsgood', 'feet', 'ferris_wheel',
'file_folder', 'finnadie', 'fire', 'fire_engine', 'fireworks',
'first_quarter_moon', 'first_quarter_moon_with_face', 'fish', 'fish_cake',
'fishing_pole_and_fish', 'fist', 'five', 'flags', 'flashlight', 'floppy_disk',
'flower_playing_cards', 'flushed', 'foggy', 'football', 'fork_and_knife',
'fountain', 'four', 'four_leaf_clover', 'fr', 'free', 'fried_shrimp', 'fries',
'frog', 'frowning', 'fu', 'fuelpump', 'full_moon', 'full_moon_with_face',
'game_die', 'gb', 'gem', 'gemini', 'ghost', 'gift', 'gift_heart', 'girl',
'globe_with_meridians', 'goat', 'goberserk', 'godmode', 'golf', 'grapes',
'green_apple', 'green_book', 'green_heart', 'grey_exclamation', 'grey_question',
'grimacing', 'grin', 'grinning', 'guardsman', 'guitar', 'gun', 'haircut',
'hamburger', 'hammer', 'hamster', 'hand', 'handbag', 'hankey', 'hash',
'hatched_chick', 'hatching_chick', 'headphones', 'hear_no_evil', 'heart',
'heart_decoration', 'heart_eyes', 'heart_eyes_cat', 'heartbeat', 'heartpulse',
'hearts', 'heavy_check_mark', 'heavy_division_sign', 'heavy_dollar_sign',
'heavy_exclamation_mark', 'heavy_minus_sign', 'heavy_multiplication_x',
'heavy_plus_sign', 'helicopter', 'herb', 'hibiscus', 'high_brightness',
'high_heel', 'hocho', 'honey_pot', 'honeybee', 'horse', 'horse_racing',
'hospital', 'hotel', 'hotsprings', 'hourglass', 'hourglass_flowing_sand',
'house', 'house_with_garden', 'hurtrealbad', 'hushed', 'ice_cream', 'icecream',
'id', 'ideograph_advantage', 'imp', 'inbox_tray', 'incoming_envelope',
'information_desk_person', 'information_source', 'innocent', 'interrobang',
'iphone', 'it', 'izakaya_lantern', 'jack_o_lantern', 'japan', 'japanese_castle',
'japanese_goblin', 'japanese_ogre', 'jeans', 'joy', 'joy_cat', 'jp', 'key',
'keycap_ten', 'kimono', 'kiss', 'kissing', 'kissing_cat', 'kissing_closed_eyes',
'kissing_face', 'kissing_heart', 'kissing_smiling_eyes', 'koala', 'koko', 'kr',
'large_blue_circle', 'large_blue_diamond', 'large_orange_diamond',
'last_quarter_moon', 'last_quarter_moon_with_face', 'laughing', 'leaves',
'ledger', 'left_luggage', 'left_right_arrow', 'leftwards_arrow_with_hook',
'lemon', 'leo', 'leopard', 'libra', 'light_rail', 'link', 'lips', 'lipstick',
'lock', 'lock_with_ink_pen', 'lollipop', 'loop', 'loudspeaker', 'love_hotel',
'love_letter', 'low_brightness', 'm', 'mag', 'mag_right', 'mahjong', 'mailbox',
'mailbox_closed', 'mailbox_with_mail', 'mailbox_with_no_mail', 'man',
'man_with_gua_pi_mao', 'man_with_turban', 'mans_shoe', 'maple_leaf', 'mask',
'massage', 'meat_on_bone', 'mega', 'melon', 'memo', 'mens', 'metal', 'metro',
'microphone', 'microscope', 'milky_way', 'minibus', 'minidisc',
'mobile_phone_off', 'money_with_wings', 'moneybag', 'monkey', 'monkey_face',
'monorail', 'moon', 'mortar_board', 'mount_fuji', 'mountain_bicyclist',
'mountain_cableway', 'mountain_railway', 'mouse', 'mouse2', 'movie_camera',
'moyai', 'muscle', 'mushroom', 'musical_keyboard', 'musical_note',
'musical_score', 'mute', 'nail_care', 'name_badge', 'neckbeard', 'necktie',
'negative_squared_cross_mark', 'neutral_face', 'new', 'new_moon',
'new_moon_with_face', 'newspaper', 'ng', 'nine', 'no_bell', 'no_bicycles',
'no_entry', 'no_entry_sign', 'no_good', 'no_mobile_phones', 'no_mouth',
'no_pedestrians', 'no_smoking', 'non-potable_water', 'nose', 'notebook',
'notebook_with_decorative_cover', 'notes', 'nut_and_bolt', 'o', 'o2', 'ocean',
'octocat', 'octopus', 'oden', 'office', 'ok', 'ok_hand', 'ok_woman',
'older_man', 'older_woman', 'on', 'oncoming_automobile', 'oncoming_bus',
'oncoming_police_car', 'oncoming_taxi', 'one', 'open_file_folder', 'open_hands',
'open_mouth', 'ophiuchus', 'orange_book', 'outbox_tray', 'ox', 'page_facing_up',
'page_with_curl', 'pager', 'palm_tree', 'panda_face', 'paperclip', 'parking',
'part_alternation_mark', 'partly_sunny', 'passport_control', 'paw_prints',
'peach', 'pear', 'pencil', 'pencil2', 'penguin', 'pensive', 'performing_arts',
'persevere', 'person_frowning', 'person_with_blond_hair',
'person_with_pouting_face', 'phone', 'pig', 'pig2', 'pig_nose', 'pill',
'pineapple', 'pisces', 'pizza', 'plus1', 'point_down', 'point_left',
'point_right', 'point_up', 'point_up_2', 'police_car', 'poodle', 'poop',
'post_office', 'postal_horn', 'postbox', 'potable_water', 'pouch',
'poultry_leg', 'pound', 'pouting_cat', 'pray', 'princess', 'punch',
'purple_heart', 'purse', 'pushpin', 'put_litter_in_its_place', 'question',
'rabbit', 'rabbit2', 'racehorse', 'radio', 'radio_button', 'rage', 'rage1',
'rage2', 'rage3', 'rage4', 'railway_car', 'rainbow', 'raised_hand',
'raised_hands', 'raising_hand', 'ram', 'ramen', 'rat', 'recycle', 'red_car',
'red_circle', 'registered', 'relaxed', 'relieved', 'repeat', 'repeat_one',
'restroom', 'revolving_hearts', 'rewind', 'ribbon', 'rice', 'rice_ball',
'rice_cracker', 'rice_scene', 'ring', 'rocket', 'roller_coaster', 'rooster',
'rose', 'rotating_light', 'round_pushpin', 'rowboat', 'ru', 'rugby_football',
'runner', 'running', 'running_shirt_with_sash', 'sa', 'sagittarius', 'sailboat',
'sake', 'sandal', 'santa', 'satellite', 'satisfied', 'saxophone', 'school',
'school_satchel', 'scissors', 'scorpius', 'scream', 'scream_cat', 'scroll',
'seat', 'secret', 'see_no_evil', 'seedling', 'seven', 'shaved_ice', 'sheep',
'shell', 'ship', 'shipit', 'shirt', 'shit', 'shoe', 'shower', 'signal_strength',
'six', 'six_pointed_star', 'ski', 'skull', 'sleeping', 'sleepy', 'slot_machine',
'small_blue_diamond', 'small_orange_diamond', 'small_red_triangle',
'small_red_triangle_down', 'smile', 'smile_cat', 'smiley', 'smiley_cat',
'smiling_imp', 'smirk', 'smirk_cat', 'smoking', 'snail', 'snake', 'snowboarder',
'snowflake', 'snowman', 'sob', 'soccer', 'soon', 'sos', 'sound',
'space_invader', 'spades', 'spaghetti', 'sparkler', 'sparkles',
'sparkling_heart', 'speak_no_evil', 'speaker', 'speech_balloon', 'speedboat',
'squirrel', 'star', 'star2', 'stars', 'station', 'statue_of_liberty',
'steam_locomotive', 'stew', 'straight_ruler', 'strawberry', 'stuck_out_tongue',
'stuck_out_tongue_closed_eyes', 'stuck_out_tongue_winking_eye', 'sun_with_face',
'sunflower', 'sunglasses', 'sunny', 'sunrise', 'sunrise_over_mountains',
'surfer', 'sushi', 'suspect', 'suspension_railway', 'sweat', 'sweat_drops',
'sweat_smile', 'sweet_potato', 'swimmer', 'symbols', 'syringe', 'tada',
'tanabata_tree', 'tangerine', 'taurus', 'taxi', 'tea', 'telephone',
'telephone_receiver', 'telescope', 'tennis', 'tent', 'thought_balloon', 'three',
'thumbsdown', 'thumbsup', 'ticket', 'tiger', 'tiger2', 'tired_face', 'tm',
'toilet', 'tokyo_tower', 'tomato', 'tongue', 'top', 'tophat', 'tractor',
'traffic_light', 'train', 'train2', 'tram', 'triangular_flag_on_post',
'triangular_ruler', 'trident', 'triumph', 'trolleybus', 'trollface', 'trophy',
'tropical_drink', 'tropical_fish', 'truck', 'trumpet', 'tshirt', 'tulip',
'turtle', 'tv', 'twisted_rightwards_arrows', 'two', 'two_hearts',
'two_men_holding_hands', 'two_women_holding_hands', 'u5272', 'u5408', 'u55b6',
'u6307', 'u6708', 'u6709', 'u6e80', 'u7121', 'u7533', 'u7981', 'u7a7a', 'uk',
'umbrella', 'unamused', 'underage', 'unlock', 'up', 'us', 'v',
'vertical_traffic_light', 'vhs', 'vibration_mode', 'video_camera', 'video_game',
'violin', 'virgo', 'volcano', 'vs', 'walking', 'waning_crescent_moon',
'waning_gibbous_moon', 'warning', 'watch', 'water_buffalo', 'watermelon',
'wave', 'wavy_dash', 'waxing_crescent_moon', 'waxing_gibbous_moon', 'wc',
'weary', 'wedding', 'whale', 'whale2', 'wheelchair', 'white_check_mark',
'white_circle', 'white_flower', 'white_square', 'white_square_button',
'wind_chime', 'wine_glass', 'wink', 'wolf', 'woman', 'womans_clothes',
'womans_hat', 'womens', 'worried', 'wrench', 'x', 'yellow_heart', 'yen', 'yum',
'zap', 'zero', 'zzz'];

133
client/lib/filter.js Normal file
View file

@ -0,0 +1,133 @@
// Filtered view manager
// We define local filter objects for each different type of field (SetFilter,
// RangeFilter, dateFilter, etc.). We then define a global `Filter` object whose
// goal is to filter complete documents by using the local filters for each
// fields.
// Use a "set" filter for a field that is a set of documents uniquely
// identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`.
var SetFilter = function() {
this._dep = new Tracker.Dependency();
this._selectedElements = [];
};
_.extend(SetFilter.prototype, {
isSelected: function(val) {
this._dep.depend();
return this._selectedElements.indexOf(val) > -1;
},
add: function(val) {
if (this.indexOfVal(val) === -1) {
this._selectedElements.push(val);
this._dep.changed();
}
},
remove: function(val) {
var indexOfVal = this._indexOfVal(val);
if (this.indexOfVal(val) !== -1) {
this._selectedElements.splice(indexOfVal, 1);
this._dep.changed();
}
},
toogle: function(val) {
var indexOfVal = this._indexOfVal(val);
if (indexOfVal === -1) {
this._selectedElements.push(val);
} else {
this._selectedElements.splice(indexOfVal, 1);
}
this._dep.changed();
},
reset: function() {
this._selectedElements = [];
this._dep.changed();
},
_indexOfVal: function(val) {
return this._selectedElements.indexOf(val);
},
_isActive: function() {
this._dep.depend();
return this._selectedElements.length !== 0;
},
_getMongoSelector: function() {
this._dep.depend();
return { $in: this._selectedElements };
}
});
// The global Filter object.
// XXX It would be possible to re-write this object more elegantly, and removing
// the need to provide a list of `_fields`. We also should move methods into the
// object prototype.
Filter = {
// XXX I would like to rename this field into `labels` to be consistent with
// the rest of the schema, but we need to set some migrations architecture
// before changing the schema.
labelIds: new SetFilter(),
members: new SetFilter(),
_fields: ['labelIds', 'members'],
// We don't filter cards that have been added after the last filter change. To
// implement this we keep the id of these cards in this `_exceptions` fields
// and use a `$or` condition in the mongo selector we return.
_exceptions: [],
_exceptionsDep: new Tracker.Dependency(),
isActive: function() {
var self = this;
return _.any(self._fields, function(fieldName) {
return self[fieldName]._isActive();
});
},
getMongoSelector: function() {
var self = this;
if (! self.isActive())
return {};
var filterSelector = {};
_.forEach(self._fields, function(fieldName) {
var filter = self[fieldName];
if (filter._isActive())
filterSelector[fieldName] = filter._getMongoSelector();
});
var exceptionsSelector = {_id: {$in: this._exceptions}};
this._exceptionsDep.depend();
return {$or: [filterSelector, exceptionsSelector]};
},
reset: function() {
var self = this;
_.forEach(self._fields, function(fieldName) {
var filter = self[fieldName];
filter.reset();
});
self.resetExceptions();
},
addException: function(_id) {
if (this.isActive()) {
this._exceptions.push(_id);
this._exceptionsDep.changed();
}
},
resetExceptions: function() {
this._exceptions = [];
this._exceptionsDep.changed();
}
};
Blaze.registerHelper('Filter', Filter);

22
client/lib/i18n.js Normal file
View file

@ -0,0 +1,22 @@
// We save the user language preference in the user profile, and use that to set
// the language reactively. If the user is not connected we use the language
// information provided by the browser, and default to english.
Tracker.autorun(function() {
var language;
var currentUser = Meteor.user();
if (currentUser) {
language = currentUser.profile && currentUser.profile.language;
} else {
language = navigator.language || navigator.userLanguage;
}
if (language) {
TAPi18n.setLanguage(language);
// XXX
var shortLanguage = language.split('-')[0];
T9n.setLanguage(shortLanguage);
}
});

55
client/lib/keyboard.js Normal file
View file

@ -0,0 +1,55 @@
// XXX Pressing `?` should display a list of all shortcuts available.
//
// XXX There is no reason to define these shortcuts globally, they should be
// attached to a template (most of them will go in the `board` template).
// Pressing `Escape` should close the last opened “element” and only the last
// one -- curently we handle popups and the card detailed view of the sidebar.
Mousetrap.bind('esc', function() {
if (currentlyOpenedForm.get() !== null) {
currentlyOpenedForm.get().close();
} else if (Popup.isOpen()) {
Popup.back();
// XXX We should have a higher level API
} else if (Session.get('currentCard')) {
Utils.goBoardId(Session.get('currentBoard'));
}
});
Mousetrap.bind('w', function() {
if (! Session.get('currentCard')) {
Sidebar.toogle();
} else {
Utils.goBoardId(Session.get('currentBoard'));
Sidebar.hide();
}
});
Mousetrap.bind('q', function() {
var currentBoardId = Session.get('currentBoard');
var currentUserId = Meteor.userId();
if (currentBoardId && currentUserId) {
Filter.members.toogle(currentUserId);
}
});
Mousetrap.bind('x', function() {
if (Filter.isActive()) {
Filter.reset();
}
});
Mousetrap.bind(['down', 'up'], function(evt, key) {
if (! Session.get('currentCard')) {
return;
}
var nextFunc = (key === 'down' ? 'next' : 'prev');
var nextCard = $('.js-minicard.is-selected')[nextFunc]('.js-minicard').get(0);
if (nextCard) {
var nextCardId = Blaze.getData(nextCard)._id;
Utils.goCardId(nextCardId);
}
});

1
client/lib/mixins.js Normal file
View file

@ -0,0 +1 @@
Mixins = {};

200
client/lib/popup.js Normal file
View file

@ -0,0 +1,200 @@
// A simple tracker dependency that we invalidate every time the window is
// resized. This is used to reactively re-calculate the popup position in case
// of a window resize.
var windowResizeDep = new Tracker.Dependency();
$(window).on('resize', function() { windowResizeDep.changed(); });
Popup = {
/// This function returns a callback that can be used in an event map:
///
/// Template.tplName.events({
/// 'click .elementClass': Popup.open("popupName")
/// });
///
/// The popup inherit the data context of its parent.
open: function(name) {
var self = this;
var popupName = name + 'Popup';
return function(evt) {
// If a popup is already openened, clicking again on the opener element
// should close it -- and interupt the current `open` function.
if (self.isOpen() &&
self._getTopStack().openerElement === evt.currentTarget) {
return self.close();
}
// We determine the `openerElement` (the DOM element that is being clicked
// and the one we take in reference to position the popup) from the event
// if the popup has no parent, or from the parent `openerElement` if it
// has one. This allows us to position a sub-popup exactly at the same
// position than its parent.
var openerElement;
if (self._hasPopupParent()) {
openerElement = self._getTopStack().openerElement;
} else {
self._stack = [];
openerElement = evt.currentTarget;
}
// We modify the event to prevent the popup being closed when the event
// bubble up to the document element.
evt.originalEvent.clickInPopup = true;
evt.preventDefault();
// We push our popup data to the stack. The top of the stack is always
// used as the data source for our current popup.
self._stack.push({
__isPopup: true,
popupName: popupName,
hasPopupParent: self._hasPopupParent(),
title: self._getTitle(popupName),
openerElement: openerElement,
offset: self._getOffset(openerElement),
dataContext: this.currentData && this.currentData() || this
});
// If there are no popup currently opened we use the Blaze API to render
// one into the DOM. We use a reactive function as the data parameter that
// just return the top element on the stack and depends on our internal
// dependency that is being invalidated every time the top element of the
// stack has changed and we want to update the popup.
//
// Otherwise if there is already a popup open we just need to invalidate
// our internal dependency, and since we just changed the top element of
// our internal stack, the popup will be updated with the new data.
if (! self.isOpen()) {
self.current = Blaze.renderWithData(self.template, function() {
self._dep.depend();
return self._stack[self._stack.length - 1];
}, document.body);
} else {
self._dep.changed();
}
};
},
/// This function returns a callback that can be used in an event map:
///
/// Template.tplName.events({
/// 'click .elementClass': Popup.afterConfirm("popupName", function() {
/// // What to do after the user has confirmed the action
/// })
/// });
afterConfirm: function(name, action) {
var self = this;
return function(evt, tpl) {
var context = this;
context.__afterConfirmAction = action;
self.open(name).call(context, evt, tpl);
};
},
/// The public reactive state of the popup.
isOpen: function() {
this._dep.changed();
return !! this.current;
},
/// In case the popup was opened from a parent popup we can get back to it
/// with this `Popup.back()` function. You can go back several steps at once
/// by providing a number to this function, e.g. `Popup.back(2)`. In this case
/// intermediate popup won't even be rendered on the DOM. If the number of
/// steps back is greater than the popup stack size, the popup will be closed.
back: function(n) {
n = n || 1;
var self = this;
if (self._stack.length > n) {
_.times(n, function() { self._stack.pop(); });
self._dep.changed();
} else {
self.close();
}
},
/// Close the current opened popup.
close: function() {
if (this.isOpen()) {
Blaze.remove(this.current);
this.current = null;
this._stack = [];
}
},
// The template we use for every popup
template: Template.popup,
// We only want to display one popup at a time and we keep the view object in
// this `Popup._current` variable. If there is no popup currently opened the
// value is `null`.
_current: null,
// It's possible to open a sub-popup B from a popup A. In that case we keep
// the data of popup A so we can return back to it. Every time we open a new
// popup the stack grows, every time we go back the stack decrease, and if we
// close the popup the stack is reseted to the empty stack [].
_stack: [],
// We invalidate this internal dependency every time the top of the stack has
// changed and we want to render a popup with the new top-stack data.
_dep: new Tracker.Dependency(),
// An utility fonction that returns the top element of the internal stack
_getTopStack: function() {
return this._stack[this._stack.length - 1];
},
// We use the blaze API to determine if the current popup has been opened from
// a parent popup. The number we give to the `Template.parentData` has been
// determined experimentally and is susceptible to change if you modify the
// `Popup.template`
_hasPopupParent: function() {
var tryParentData = Template.parentData(3);
return !! (tryParentData && tryParentData.__isPopup);
},
// We automatically calculate the popup offset from the reference element
// position and dimensions. We also reactively use the window dimensions to
// ensure that the popup is always visible on the screen.
_getOffset: function(element) {
var $element = $(element);
return function() {
windowResizeDep.depend();
var offset = $element.offset();
var popupWidth = 300 + 15;
return {
left: Math.min(offset.left, $(window).width() - popupWidth),
top: offset.top + $element.outerHeight()
};
};
},
// We get the title from the translation files. Instead of returning the
// result, we return a function that compute the result and since `TAPi18n.__`
// is a reactive data source, the title will be changed reactively.
_getTitle: function(popupName) {
return function() {
var translationKey = popupName + '-title';
// XXX There is no public API to check if there is an available
// translation for a given key. So we try to translate the key and if the
// translation output equals the key input we deduce that no translation
// was available and returns `false`. There is a (small) risk a false
// positives.
var title = TAPi18n.__(translationKey);
return title !== translationKey ? title : false;
};
}
};
// We automatically close a potential opened popup on any left click on the
// document. To avoid closing it unexpectedly we modify the bubbled event in
// case the click event happen in the popup or in a button that open a popup.
$(document).on('click', function(evt) {
if (evt.which === 1 && ! (evt.originalEvent &&
evt.originalEvent.clickInPopup)) {
Popup.close();
}
});

96
client/lib/utils.js Normal file
View file

@ -0,0 +1,96 @@
Utils = {
error: function(err) {
Session.set('error', (err && err.message || false));
},
// scroll
Scroll: function(selector) {
var $el = $(selector);
return {
top: function(px, add) {
var t = $el.scrollTop();
$el.animate({ scrollTop: (add ? (t + px) : px) });
},
left: function(px, add) {
var l = $el.scrollLeft();
$el.animate({ scrollLeft: (add ? (l + px) : px) });
}
};
},
Warning: {
get: function() {
return Session.get('warning');
},
open: function(desc) {
Session.set('warning', { desc: desc });
},
close: function() {
Session.set('warning', false);
}
},
// XXX We should remove these two methods
goBoardId: function(_id) {
var board = Boards.findOne(_id);
return board && Router.go('Board', {
_id: board._id,
slug: board.slug
});
},
goCardId: function(_id) {
var card = Cards.findOne(_id);
var board = Boards.findOne(card.boardId);
return board && Router.go('Card', {
cardId: card._id,
boardId: board._id,
slug: board.slug
});
},
liveEvent: function(events, callback) {
$(document).on(events, function() {
callback($(this));
});
},
capitalize: function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
getLabelIndex: function(boardId, labelId) {
var board = Boards.findOne(boardId);
var labels = {};
_.each(board.labels, function(a, b) {
labels[a._id] = b;
});
return {
index: labels[labelId],
key: function(key) {
return 'labels.' + labels[labelId] + '.' + key;
}
};
},
// Determine the new sort index
getSortIndex: function(prevCardDomElement, nextCardDomElement) {
// If we drop the card to an empty column
if (! prevCardDomElement && ! nextCardDomElement) {
return 0;
// If we drop the card in the first position
} else if (! prevCardDomElement) {
return Blaze.getData(nextCardDomElement).sort - 1;
// If we drop the card in the last position
} else if (! nextCardDomElement) {
return Blaze.getData(prevCardDomElement).sort + 1;
}
// In the general case take the average of the previous and next element
// sort indexes.
else {
var prevSortIndex = Blaze.getData(prevCardDomElement).sort;
var nextSortIndex = Blaze.getData(nextCardDomElement).sort;
return (prevSortIndex + nextSortIndex) / 2;
}
}
};