summaryrefslogtreecommitdiffstats
path: root/client/lib
diff options
context:
space:
mode:
authorMaxime Quandalle <maxime@quandalle.com>2015-05-12 19:20:58 +0200
committerMaxime Quandalle <maxime@quandalle.com>2015-05-12 19:33:50 +0200
commit2dbea30842ec63a68055245fe26633bb7913daf3 (patch)
treee9143893a3d3bf4ad34dd3a97d6f3466561c8756 /client/lib
downloadwekan-2dbea30842ec63a68055245fe26633bb7913daf3.tar.gz
wekan-2dbea30842ec63a68055245fe26633bb7913daf3.tar.bz2
wekan-2dbea30842ec63a68055245fe26633bb7913daf3.zip
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
Diffstat (limited to 'client/lib')
-rw-r--r--client/lib/emoji-values.js152
-rw-r--r--client/lib/filter.js133
-rw-r--r--client/lib/i18n.js22
-rw-r--r--client/lib/keyboard.js55
-rw-r--r--client/lib/mixins.js1
-rw-r--r--client/lib/popup.js200
-rw-r--r--client/lib/utils.js96
7 files changed, 659 insertions, 0 deletions
diff --git a/client/lib/emoji-values.js b/client/lib/emoji-values.js
new file mode 100644
index 00000000..1f07ac62
--- /dev/null
+++ b/client/lib/emoji-values.js
@@ -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'];
diff --git a/client/lib/filter.js b/client/lib/filter.js
new file mode 100644
index 00000000..507a2bb7
--- /dev/null
+++ b/client/lib/filter.js
@@ -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);
diff --git a/client/lib/i18n.js b/client/lib/i18n.js
new file mode 100644
index 00000000..7d7e3ebb
--- /dev/null
+++ b/client/lib/i18n.js
@@ -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);
+ }
+});
diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js
new file mode 100644
index 00000000..c1267938
--- /dev/null
+++ b/client/lib/keyboard.js
@@ -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);
+ }
+});
diff --git a/client/lib/mixins.js b/client/lib/mixins.js
new file mode 100644
index 00000000..8d16be53
--- /dev/null
+++ b/client/lib/mixins.js
@@ -0,0 +1 @@
+Mixins = {};
diff --git a/client/lib/popup.js b/client/lib/popup.js
new file mode 100644
index 00000000..dd2a43b0
--- /dev/null
+++ b/client/lib/popup.js
@@ -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();
+ }
+});
diff --git a/client/lib/utils.js b/client/lib/utils.js
new file mode 100644
index 00000000..9e92e999
--- /dev/null
+++ b/client/lib/utils.js
@@ -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;
+ }
+ }
+};