diff options
author | Maxime Quandalle <maxime@quandalle.com> | 2015-05-12 19:20:58 +0200 |
---|---|---|
committer | Maxime Quandalle <maxime@quandalle.com> | 2015-05-12 19:33:50 +0200 |
commit | 2dbea30842ec63a68055245fe26633bb7913daf3 (patch) | |
tree | e9143893a3d3bf4ad34dd3a97d6f3466561c8756 /client/lib | |
download | wekan-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.js | 152 | ||||
-rw-r--r-- | client/lib/filter.js | 133 | ||||
-rw-r--r-- | client/lib/i18n.js | 22 | ||||
-rw-r--r-- | client/lib/keyboard.js | 55 | ||||
-rw-r--r-- | client/lib/mixins.js | 1 | ||||
-rw-r--r-- | client/lib/popup.js | 200 | ||||
-rw-r--r-- | client/lib/utils.js | 96 |
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; + } + } +}; |