From 94b60e62a263322ee7e4c0765b8d1307575f37e0 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 28 Sep 2015 14:01:16 -0400 Subject: [PATCH] FEATURE: New markdown editor re-written in Ember Note this commit leaves out the biggest occurrence of the editor which is the post/topic composer. To avoid major breakage, this replaces it everywhere else it was used: * User preferences (About Me) * Admin Customizations > Text Content * Category Templates * Editing Queued Posts A future commit will replace the main composer with this editor and will remove the unused pagedown code. --- .../admin/templates/site-text-edit.hbs | 2 +- .../components/d-editor-modal.js.es6 | 52 ++ .../discourse/components/d-editor.js.es6 | 258 ++++++++++ .../components/pagedown-editor.js.es6 | 25 - .../initializers/enable-emoji.js.es6 | 10 +- .../initializers/load-all-helpers.js.es6 | 17 +- .../discourse/lib/emoji/emoji-groups.js.es6 | 57 +++ .../discourse/lib/emoji/emoji-toolbar.js.es6 | 267 ++++------- .../templates/components/d-editor-modal.hbs | 7 + .../templates/components/d-editor.hbs | 32 ++ .../edit-category-topic-template.hbs | 2 +- .../templates/components/pagedown-editor.hbs | 3 - .../discourse/templates/queued-posts.hbs | 2 +- .../discourse/templates/user/about.hbs | 2 +- .../discourse/templates/user/preferences.hbs | 2 +- .../discourse/views/composer.js.es6 | 11 +- app/assets/javascripts/main_include.js | 1 + app/assets/stylesheets/common.scss | 1 + .../stylesheets/common/admin/admin_base.scss | 7 - .../stylesheets/common/base/discourse.scss | 18 - app/assets/stylesheets/common/base/emoji.scss | 3 +- app/assets/stylesheets/common/base/modal.scss | 4 - app/assets/stylesheets/common/d-editor.scss | 94 ++++ app/assets/stylesheets/desktop/user.scss | 8 - app/assets/stylesheets/mobile/user.scss | 4 - config/locales/client.en.yml | 5 +- .../acceptance/category-edit-test.js.es6 | 2 +- .../components/d-editor-test.js.es6 | 450 ++++++++++++++++++ .../javascripts/helpers/component-test.js.es6 | 12 +- test/javascripts/test_helper.js | 3 + test/stylesheets/test_helper.css | 8 + 31 files changed, 1100 insertions(+), 269 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/d-editor-modal.js.es6 create mode 100644 app/assets/javascripts/discourse/components/d-editor.js.es6 delete mode 100644 app/assets/javascripts/discourse/components/pagedown-editor.js.es6 create mode 100644 app/assets/javascripts/discourse/lib/emoji/emoji-groups.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/d-editor-modal.hbs create mode 100644 app/assets/javascripts/discourse/templates/components/d-editor.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/pagedown-editor.hbs create mode 100644 app/assets/stylesheets/common/d-editor.scss create mode 100644 test/javascripts/components/d-editor-test.js.es6 diff --git a/app/assets/javascripts/admin/templates/site-text-edit.hbs b/app/assets/javascripts/admin/templates/site-text-edit.hbs index 438887f85..2a6693e8f 100644 --- a/app/assets/javascripts/admin/templates/site-text-edit.hbs +++ b/app/assets/javascripts/admin/templates/site-text-edit.hbs @@ -2,7 +2,7 @@

{{model.description}}

{{#if model.markdown}} - {{pagedown-editor value=model.value}} + {{d-editor value=model.value}} {{/if}} {{#if model.plainText}} {{textarea value=model.value class="plain"}} diff --git a/app/assets/javascripts/discourse/components/d-editor-modal.js.es6 b/app/assets/javascripts/discourse/components/d-editor-modal.js.es6 new file mode 100644 index 000000000..0b6f6920a --- /dev/null +++ b/app/assets/javascripts/discourse/components/d-editor-modal.js.es6 @@ -0,0 +1,52 @@ +import { observes, on } from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + classNameBindings: [':d-editor-modal', 'hidden'], + + @observes('hidden') + _hiddenChanged() { + if (!this.get('hidden')) { + Ember.run.scheduleOnce('afterRender', () => { + const $modal = this.$(); + const $parent = this.$().closest('.d-editor'); + const w = $parent.width(); + const h = $parent.height(); + $modal.css({ left: (w / 2) - ($modal.outerWidth() / 2) }); + parent.$('.d-editor-overlay').removeClass('hidden').css({ width: w, height: h}); + this.$('input').focus(); + }); + } else { + parent.$('.d-editor-overlay').addClass('hidden'); + } + }, + + @on('didInsertElement') + _listenKeys() { + this.$().on('keydown.d-modal', key => { + if (this.get('hidden')) { return; } + + if (key.keyCode === 27) { + this.send('cancel'); + } + if (key.keyCode === 13) { + this.send('ok'); + } + }); + }, + + @on('willDestoryElement') + _stopListening() { + this.$().off('keydown.d-modal'); + }, + + actions: { + ok() { + this.set('hidden', true); + this.sendAction('okAction'); + }, + + cancel() { + this.set('hidden', true); + } + } +}); diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 new file mode 100644 index 000000000..49df01bc9 --- /dev/null +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -0,0 +1,258 @@ +import loadScript from 'discourse/lib/load-script'; +import { default as property, on } from 'ember-addons/ember-computed-decorators'; +import { showSelector } from "discourse/lib/emoji/emoji-toolbar"; + +// Our head can be a static string or a function that returns a string +// based on input (like for numbered lists). +function getHead(head, prev) { + if (typeof head === "string") { + return [head, head.length]; + } else { + return getHead(head(prev)); + } +} + +export default Ember.Component.extend({ + classNames: ['d-editor'], + ready: false, + insertLinkHidden: true, + link: '', + lastSel: null, + + @on('didInsertElement') + _loadSanitizer() { + this._applyEmojiAutocomplete(); + loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true)); + }, + + @property('ready', 'value') + preview(ready, value) { + if (!ready) { return; } + + const text = Discourse.Dialect.cook(value || "", {}); + return text ? text : ""; + }, + + _applyEmojiAutocomplete() { + if (!this.siteSettings.enable_emoji) { return; } + + const container = this.container; + const template = container.lookup('template:emoji-selector-autocomplete.raw'); + const self = this; + + this.$('.d-editor-input').autocomplete({ + template: template, + key: ":", + + transformComplete(v) { + if (v.code) { + return `${v.code}:`; + } else { + showSelector({ + appendTo: self.$(), + container, + onSelect: title => self._addText(`${title}:`) + }); + return ""; + } + }, + + dataSource(term) { + return new Ember.RSVP.Promise(resolve => { + const full = `:${term}`; + term = term.toLowerCase(); + + if (term === "") { + return resolve(["smile", "smiley", "wink", "sunny", "blush"]); + } + + if (Discourse.Emoji.translations[full]) { + return resolve([Discourse.Emoji.translations[full]]); + } + + const options = Discourse.Emoji.search(term, {maxResults: 5}); + + return resolve(options); + }).then(list => list.map(code => { + return {code, src: Discourse.Emoji.urlFor(code)}; + })).then(list => { + if (list.length) { + list.push({ label: I18n.t("composer.more_emoji") }); + } + return list; + }); + } + }); + }, + + _getSelected() { + if (!this.get('ready')) { return; } + + const textarea = this.$('textarea.d-editor-input')[0]; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const value = textarea.value.substring(start, end); + const pre = textarea.value.slice(0, start); + const post = textarea.value.slice(end); + + return { start, end, value, pre, post }; + }, + + _selectText(from, length) { + Ember.run.scheduleOnce('afterRender', () => { + const textarea = this.$('textarea.d-editor-input')[0]; + textarea.focus(); + textarea.selectionStart = from; + textarea.selectionEnd = textarea.selectionStart + length; + }); + }, + + _applySurround(head, tail, exampleKey) { + const sel = this._getSelected(); + const pre = sel.pre; + const post = sel.post; + + const tlen = tail.length; + if (sel.start === sel.end) { + if (tlen === 0) { return; } + + const [hval, hlen] = getHead(head); + const example = I18n.t(`composer.${exampleKey}`); + this.set('value', `${pre}${hval}${example}${tail}${post}`); + this._selectText(pre.length + hlen, example.length); + } else { + const lines = sel.value.split("\n"); + + let [hval, hlen] = getHead(head); + if (lines.length === 1 && pre.slice(-tlen) === tail && post.slice(0, hlen) === hval) { + this.set('value', `${pre.slice(0, -hlen)}${sel.value}${post.slice(tlen)}`); + this._selectText(sel.start - hlen, sel.value.length); + } else { + const contents = lines.map(l => { + if (l.length === 0) { return l; } + + if (l.slice(0, hlen) === hval && tlen === 0 || l.slice(-tlen) === tail) { + if (tlen === 0) { + const result = l.slice(hlen); + [hval, hlen] = getHead(head, hval); + return result; + } else if (l.slice(-tlen) === tail) { + const result = l.slice(hlen, -tlen); + [hval, hlen] = getHead(head, hval); + return result; + } + } + const result = `${hval}${l}${tail}`; + [hval, hlen] = getHead(head, hval); + return result; + }).join("\n"); + + this.set('value', `${pre}${contents}${post}`); + if (lines.length === 1 && tlen > 0) { + this._selectText(sel.start + hlen, contents.length - hlen - hlen); + } else { + this._selectText(sel.start, contents.length); + } + } + } + }, + + _applyList(head, exampleKey) { + const sel = this._getSelected(); + if (sel.value.indexOf("\n") !== -1) { + this._applySurround(head, '', exampleKey); + } else { + + const [hval, hlen] = getHead(head); + if (sel.start === sel.end) { + sel.value = I18n.t(`composer.${exampleKey}`); + } + + const trimmedPre = sel.pre.trim(); + const number = (sel.value.indexOf(hval) === 0) ? sel.value.slice(hlen) : `${hval}${sel.value}`; + const preLines = trimmedPre.length ? `${trimmedPre}\n\n` : ""; + + const trimmedPost = sel.post.trim(); + const post = trimmedPost.length ? `\n\n${trimmedPost}` : trimmedPost; + + this.set('value', `${preLines}${number}${post}`); + this._selectText(preLines.length, number.length); + } + }, + + _addText(text, sel) { + sel = sel || this._getSelected(); + const insert = `${sel.pre}${text}`; + this.set('value', `${insert}${sel.post}`); + this._selectText(insert.length, 0); + }, + + actions: { + bold() { + this._applySurround('**', '**', 'bold_text'); + }, + + italic() { + this._applySurround('*', '*', 'italic_text'); + }, + + showLinkModal() { + this._lastSel = this._getSelected(); + this.set('insertLinkHidden', false); + }, + + insertLink() { + const link = this.get('link'); + + if (Ember.isEmpty(link)) { return; } + const m = / "([^"]+)"/.exec(link); + if (m && m.length === 2) { + const description = m[1]; + const remaining = link.replace(m[0], ''); + this._addText(`[${description}](${remaining})`, this._lastSel); + } else { + this._addText(`[${link}](${link})`, this._lastSel); + } + + this.set('link', ''); + }, + + code() { + const sel = this._getSelected(); + if (sel.value.indexOf("\n") !== -1) { + this._applySurround(' ', '', 'code_text'); + } else { + this._applySurround('`', '`', 'code_text'); + } + }, + + quote() { + this._applySurround('> ', "", 'code_text'); + }, + + bullet() { + this._applyList('* ', 'list_item'); + }, + + list() { + this._applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item'); + }, + + heading() { + this._applyList('## ', 'heading_text'); + }, + + rule() { + this._addText("\n\n----------\n"); + }, + + emoji() { + showSelector({ + appendTo: this.$(), + container: this.container, + onSelect: title => this._addText(`:${title}:`) + }); + } + } + +}); diff --git a/app/assets/javascripts/discourse/components/pagedown-editor.js.es6 b/app/assets/javascripts/discourse/components/pagedown-editor.js.es6 deleted file mode 100644 index 6f04e7715..000000000 --- a/app/assets/javascripts/discourse/components/pagedown-editor.js.es6 +++ /dev/null @@ -1,25 +0,0 @@ -import { observes, on } from 'ember-addons/ember-computed-decorators'; -import loadScript from 'discourse/lib/load-script'; - -export default Ember.Component.extend({ - classNameBindings: [':pagedown-editor'], - - @on("didInsertElement") - _initializeWmd() { - loadScript('defer/html-sanitizer-bundle').then(() => { - this.$('.wmd-input').data('init', true); - this._editor = Discourse.Markdown.createEditor({ containerElement: this.element }); - this._editor.run(); - Ember.run.scheduleOnce('afterRender', this, this._refreshPreview); - }); - }, - - @observes("value") - observeValue() { - Ember.run.scheduleOnce('afterRender', this, this._refreshPreview); - }, - - _refreshPreview() { - this._editor.refreshPreview(); - } -}); diff --git a/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 b/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 index b7b269f78..3d2bd23b5 100644 --- a/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 +++ b/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 @@ -9,7 +9,15 @@ export default { window.PagedownCustom.appendButtons.push({ id: 'wmd-emoji-button', description: I18n.t("composer.emoji"), - execute: showSelector + execute() { + showSelector({ + container, + onSelect(title) { + const composerController = container.lookup('controller:composer'); + composerController.appendTextAtCursor(`:${title}:`, {space: true}); + }, + }); + } }); } } diff --git a/app/assets/javascripts/discourse/initializers/load-all-helpers.js.es6 b/app/assets/javascripts/discourse/initializers/load-all-helpers.js.es6 index f9ec0d71c..934777877 100644 --- a/app/assets/javascripts/discourse/initializers/load-all-helpers.js.es6 +++ b/app/assets/javascripts/discourse/initializers/load-all-helpers.js.es6 @@ -1,11 +1,12 @@ +export function loadAllHelpers() { + Ember.keys(requirejs.entries).forEach(entry => { + if ((/\/helpers\//).test(entry)) { + require(entry, null, null, true); + } + }); +} + export default { name: 'load-all-helpers', - - initialize: function() { - Ember.keys(requirejs.entries).forEach(function(entry) { - if ((/\/helpers\//).test(entry)) { - require(entry, null, null, true); - } - }); - } + initialize: loadAllHelpers }; diff --git a/app/assets/javascripts/discourse/lib/emoji/emoji-groups.js.es6 b/app/assets/javascripts/discourse/lib/emoji/emoji-groups.js.es6 new file mode 100644 index 000000000..71ef4eb90 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/emoji/emoji-groups.js.es6 @@ -0,0 +1,57 @@ +// note that these categories are copied from Slack +// be careful, there are ~20 differences in synonyms, e.g. :boom: vs. :collision: +// a few Emoji are actually missing from the Slack categories as well (?), and were added +const groups = [ + { + name: "people", + fullname: "People", + tabicon: "grinning", + icons: ["grinning", "grin", "joy", "smiley", "smile", "sweat_smile", "laughing", "innocent", "smiling_imp", "imp", "wink", "blush", "relaxed", "yum", "relieved", "heart_eyes", "sunglasses", "smirk", "neutral_face", "expressionless", "unamused", "sweat", "pensive", "confused", "confounded", "kissing", "kissing_heart", "kissing_smiling_eyes", "kissing_closed_eyes", "stuck_out_tongue", "stuck_out_tongue_winking_eye", "stuck_out_tongue_closed_eyes", "disappointed", "worried", "angry", "rage", "cry", "persevere", "triumph", "disappointed_relieved", "frowning", "anguished", "fearful", "weary", "sleepy", "tired_face", "grimacing", "sob", "open_mouth", "hushed", "cold_sweat", "scream", "astonished", "flushed", "sleeping", "dizzy_face", "no_mouth", "mask", "smile_cat", "joy_cat", "smiley_cat", "heart_eyes_cat", "smirk_cat", "kissing_cat", "pouting_cat", "crying_cat_face", "scream_cat", "footprints", "bust_in_silhouette", "busts_in_silhouette", "baby", "boy", "girl", "man", "woman", "family", "couple", "two_men_holding_hands", "two_women_holding_hands", "dancers", "bride_with_veil", "person_with_blond_hair", "man_with_gua_pi_mao", "man_with_turban", "older_man", "older_woman", "cop", "construction_worker", "princess", "guardsman", "angel", "santa", "ghost", "japanese_ogre", "japanese_goblin", "hankey", "skull", "alien", "space_invader", "bow", "information_desk_person", "no_good", "ok_woman", "raising_hand", "person_with_pouting_face", "person_frowning", "massage", "haircut", "couple_with_heart", "couplekiss", "raised_hands", "clap", "hand", "ear", "eyes", "nose", "lips", "kiss", "tongue", "nail_care", "wave", "+1", "-1", "point_up", "point_up_2", "point_down", "point_left", "point_right", "ok_hand", "v", "facepunch", "fist", "raised_hand", "muscle", "open_hands", "pray"] + }, + { + name: "nature", + fullname: "Nature", + tabicon: "evergreen_tree", + icons: ["seedling", "evergreen_tree", "deciduous_tree", "palm_tree", "cactus", "tulip", "cherry_blossom", "rose", "hibiscus", "sunflower", "blossom", "bouquet", "ear_of_rice", "herb", "four_leaf_clover", "maple_leaf", "fallen_leaf", "leaves", "mushroom", "chestnut", "rat", "mouse2", "mouse", "hamster", "ox", "water_buffalo", "cow2", "cow", "tiger2", "leopard", "tiger", "rabbit2", "rabbit", "cat2", "cat", "racehorse", "horse", "ram", "sheep", "goat", "rooster", "chicken", "baby_chick", "hatching_chick", "hatched_chick", "bird", "penguin", "elephant", "dromedary_camel", "camel", "boar", "pig2", "pig", "pig_nose", "dog2", "poodle", "dog", "wolf", "bear", "koala", "panda_face", "monkey_face", "see_no_evil", "hear_no_evil", "speak_no_evil", "monkey", "dragon", "dragon_face", "crocodile", "snake", "turtle", "frog", "whale2", "whale", "dolphin", "octopus", "fish", "tropical_fish", "blowfish", "shell", "snail", "bug", "ant", "bee", "beetle", "feet", "zap", "fire", "crescent_moon", "sunny", "partly_sunny", "cloud", "droplet", "sweat_drops", "umbrella", "dash", "snowflake", "star2", "star", "stars", "sunrise_over_mountains", "sunrise", "rainbow", "ocean", "volcano", "milky_way", "mount_fuji", "japan", "globe_with_meridians", "earth_africa", "earth_americas", "earth_asia", "new_moon", "waxing_crescent_moon", "first_quarter_moon", "moon", "full_moon", "waning_gibbous_moon", "last_quarter_moon", "waning_crescent_moon", "new_moon_with_face", "full_moon_with_face", "first_quarter_moon_with_face", "last_quarter_moon_with_face", "sun_with_face"] + }, + { + name: "food", + fullname: "Food & Drink", + tabicon: "hamburger", + icons: ["tomato", "eggplant", "corn", "sweet_potato", "grapes", "melon", "watermelon", "tangerine", "lemon", "banana", "pineapple", "apple", "green_apple", "pear", "peach", "cherries", "strawberry", "hamburger", "pizza", "meat_on_bone", "poultry_leg", "rice_cracker", "rice_ball", "rice", "curry", "ramen", "spaghetti", "bread", "fries", "dango", "oden", "sushi", "fried_shrimp", "fish_cake", "icecream", "shaved_ice", "ice_cream", "doughnut", "cookie", "chocolate_bar", "candy", "lollipop", "custard", "honey_pot", "cake", "bento", "stew", "egg", "fork_and_knife", "tea", "coffee", "sake", "wine_glass", "cocktail", "tropical_drink", "beer", "beers", "baby_bottle"] + }, + { + name: "celebration", + fullname: "Celebration", + tabicon: "gift", + icons: ["ribbon", "gift", "birthday", "jack_o_lantern", "christmas_tree", "tanabata_tree", "bamboo", "rice_scene", "fireworks", "sparkler", "tada", "confetti_ball", "balloon", "dizzy", "sparkles", "boom", "mortar_board", "crown", "dolls", "flags", "wind_chime", "crossed_flags", "izakaya_lantern", "ring", "heart", "broken_heart", "love_letter", "two_hearts", "revolving_hearts", "heartbeat", "heartpulse", "sparkling_heart", "cupid", "gift_heart", "heart_decoration", "purple_heart", "yellow_heart", "green_heart", "blue_heart"] + }, + { + name: "activity", + fullname: "Activities", + tabicon: "soccer", + icons: ["runner", "walking", "dancer", "rowboat", "swimmer", "surfer", "bath", "snowboarder", "ski", "snowman", "bicyclist", "mountain_bicyclist", "horse_racing", "tent", "fishing_pole_and_fish", "soccer", "basketball", "football", "baseball", "tennis", "rugby_football", "golf", "trophy", "running_shirt_with_sash", "checkered_flag", "musical_keyboard", "guitar", "violin", "saxophone", "trumpet", "musical_note", "notes", "musical_score", "headphones", "microphone", "performing_arts", "ticket", "tophat", "circus_tent", "clapper", "art", "dart", "8ball", "bowling", "slot_machine", "game_die", "video_game", "flower_playing_cards", "black_joker", "mahjong", "carousel_horse", "ferris_wheel", "roller_coaster"] + }, + { + name: "travel", + fullname: "Travel & Places", + tabicon: "airplane", + icons: ["train", "mountain_railway", "railway_car", "steam_locomotive", "monorail", "bullettrain_side", "bullettrain_front", "train2", "metro", "light_rail", "station", "tram", "bus", "oncoming_bus", "trolleybus", "minibus", "ambulance", "fire_engine", "police_car", "oncoming_police_car", "rotating_light", "taxi", "oncoming_taxi", "car", "oncoming_automobile", "blue_car", "truck", "articulated_lorry", "tractor", "bike", "busstop", "fuelpump", "construction", "vertical_traffic_light", "traffic_light", "rocket", "helicopter", "airplane", "seat", "anchor", "ship", "speedboat", "boat", "aerial_tramway", "mountain_cableway", "suspension_railway", "passport_control", "customs", "baggage_claim", "left_luggage", "yen", "euro", "pound", "dollar", "statue_of_liberty", "moyai", "foggy", "tokyo_tower", "fountain", "european_castle", "japanese_castle", "city_sunrise", "city_sunset", "night_with_stars", "bridge_at_night", "house", "house_with_garden", "office", "department_store", "factory", "post_office", "european_post_office", "hospital", "bank", "hotel", "love_hotel", "wedding", "church", "convenience_store", "school", "cn", "de", "es", "fr", "gb", "it", "jp", "kr", "ru", "us"] + }, + { + name: "objects", + fullname: "Objects & Symbols", + tabicon: "eyeglasses", + icons: ["watch", "iphone", "calling", "computer", "alarm_clock", "hourglass_flowing_sand", "hourglass", "camera", "video_camera", "movie_camera", "tv", "radio", "pager", "telephone_receiver", "phone", "fax", "minidisc", "floppy_disk", "cd", "dvd", "vhs", "battery", "electric_plug", "bulb", "flashlight", "satellite", "credit_card", "money_with_wings", "moneybag", "gem", "closed_umbrella", "pouch", "purse", "handbag", "briefcase", "school_satchel", "lipstick", "eyeglasses", "womans_hat", "sandal", "high_heel", "boot", "mans_shoe", "athletic_shoe", "bikini", "dress", "kimono", "womans_clothes", "shirt", "necktie", "jeans", "door", "shower", "bathtub", "toilet", "barber", "syringe", "pill", "microscope", "telescope", "crystal_ball", "wrench", "hocho", "nut_and_bolt", "hammer", "bomb", "smoking", "gun", "bookmark", "newspaper", "key", "email", "envelope_with_arrow", "incoming_envelope", "e-mail", "inbox_tray", "outbox_tray", "package", "postal_horn", "postbox", "mailbox_closed", "mailbox", "mailbox_with_mail", "mailbox_with_no_mail", "page_facing_up", "page_with_curl", "bookmark_tabs", "chart_with_upwards_trend", "chart_with_downwards_trend", "bar_chart", "date", "calendar", "low_brightness", "high_brightness", "scroll", "clipboard", "book", "notebook", "notebook_with_decorative_cover", "ledger", "closed_book", "green_book", "blue_book", "orange_book", "books", "card_index", "link", "paperclip", "pushpin", "scissors", "triangular_ruler", "round_pushpin", "straight_ruler", "triangular_flag_on_post", "file_folder", "open_file_folder", "black_nib", "pencil2", "memo", "lock_with_ink_pen", "closed_lock_with_key", "lock", "unlock", "mega", "loudspeaker", "sound", "loud_sound", "speaker", "mute", "zzz", "bell", "no_bell", "thought_balloon", "speech_balloon", "children_crossing", "mag", "mag_right", "no_entry_sign", "no_entry", "name_badge", "no_pedestrians", "do_not_litter", "no_bicycles", "non-potable_water", "no_mobile_phones", "underage", "accept", "ideograph_advantage", "white_flower", "secret", "congratulations", "u5408", "u6e80", "u7981", "u6709", "u7121", "u7533", "u55b6", "u6708", "u5272", "u7a7a", "sa", "koko", "u6307", "chart", "sparkle", "eight_spoked_asterisk", "negative_squared_cross_mark", "white_check_mark", "eight_pointed_black_star", "vibration_mode", "mobile_phone_off", "vs", "a", "b", "ab", "cl", "o2", "sos", "id", "parking", "wc", "cool", "free", "new", "ng", "ok", "up", "atm", "aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpius", "sagittarius", "capricorn", "aquarius", "pisces", "restroom", "mens", "womens", "baby_symbol", "wheelchair", "potable_water", "no_smoking", "put_litter_in_its_place", "arrow_forward", "arrow_backward", "arrow_up_small", "arrow_down_small", "fast_forward", "rewind", "arrow_double_up", "arrow_double_down", "arrow_right", "arrow_left", "arrow_up", "arrow_down", "arrow_upper_right", "arrow_lower_right", "arrow_lower_left", "arrow_upper_left", "arrow_up_down", "left_right_arrow", "arrows_counterclockwise", "arrow_right_hook", "leftwards_arrow_with_hook", "arrow_heading_up", "arrow_heading_down", "twisted_rightwards_arrows", "repeat", "repeat_one", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "keycap_ten", "1234", "hash", "abc", "abcd", "capital_abcd", "information_source", "signal_strength", "cinema", "symbols", "heavy_plus_sign", "heavy_minus_sign", "wavy_dash", "heavy_division_sign", "heavy_multiplication_x", "heavy_check_mark", "arrows_clockwise", "tm", "copyright", "registered", "currency_exchange", "heavy_dollar_sign", "curly_loop", "loop", "part_alternation_mark", "exclamation", "bangbang", "question", "grey_exclamation", "grey_question", "interrobang", "x", "o", "100", "end", "back", "on", "top", "soon", "cyclone", "m", "ophiuchus", "six_pointed_star", "beginner", "trident", "warning", "hotsprings", "recycle", "anger", "diamond_shape_with_a_dot_inside", "spades", "clubs", "hearts", "diamonds", "ballot_box_with_check", "white_circle", "black_circle", "radio_button", "red_circle", "large_blue_circle", "small_red_triangle", "small_red_triangle_down", "small_orange_diamond", "small_blue_diamond", "large_orange_diamond", "large_blue_diamond", "black_small_square", "white_small_square", "black_large_square", "white_large_square", "black_medium_square", "white_medium_square", "black_medium_small_square", "white_medium_small_square", "black_square_button", "white_square_button", "clock1", "clock2", "clock3", "clock4", "clock5", "clock6", "clock7", "clock8", "clock9", "clock10", "clock11", "clock12", "clock130", "clock230", "clock330", "clock430", "clock530", "clock630", "clock730", "clock830", "clock930", "clock1030", "clock1130", "clock1230"] + } +]; + +// scrub groups +groups.forEach(group => { + group.icons = group.icons.reject(obj => !Discourse.Emoji.exists(obj)); +}); + +// export so others can modify +Discourse.Emoji.groups = groups; + +export default groups; diff --git a/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 b/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 index baf51822b..ffd54875c 100644 --- a/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 +++ b/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 @@ -1,101 +1,43 @@ +import groups from 'discourse/lib/emoji/emoji-groups'; import KeyValueStore from "discourse/lib/key-value-store"; const keyValueStore = new KeyValueStore("discourse_emojis_"); const EMOJI_USAGE = "emojiUsage"; -// note that these categories are copied from Slack -// be careful, there are ~20 differences in synonyms, e.g. :boom: vs. :collision: -// a few Emoji are actually missing from the Slack categories as well (?), and were added -var groups = [ - { - name: "people", - fullname: "People", - tabicon: "grinning", - icons: ["grinning", "grin", "joy", "smiley", "smile", "sweat_smile", "laughing", "innocent", "smiling_imp", "imp", "wink", "blush", "relaxed", "yum", "relieved", "heart_eyes", "sunglasses", "smirk", "neutral_face", "expressionless", "unamused", "sweat", "pensive", "confused", "confounded", "kissing", "kissing_heart", "kissing_smiling_eyes", "kissing_closed_eyes", "stuck_out_tongue", "stuck_out_tongue_winking_eye", "stuck_out_tongue_closed_eyes", "disappointed", "worried", "angry", "rage", "cry", "persevere", "triumph", "disappointed_relieved", "frowning", "anguished", "fearful", "weary", "sleepy", "tired_face", "grimacing", "sob", "open_mouth", "hushed", "cold_sweat", "scream", "astonished", "flushed", "sleeping", "dizzy_face", "no_mouth", "mask", "smile_cat", "joy_cat", "smiley_cat", "heart_eyes_cat", "smirk_cat", "kissing_cat", "pouting_cat", "crying_cat_face", "scream_cat", "footprints", "bust_in_silhouette", "busts_in_silhouette", "baby", "boy", "girl", "man", "woman", "family", "couple", "two_men_holding_hands", "two_women_holding_hands", "dancers", "bride_with_veil", "person_with_blond_hair", "man_with_gua_pi_mao", "man_with_turban", "older_man", "older_woman", "cop", "construction_worker", "princess", "guardsman", "angel", "santa", "ghost", "japanese_ogre", "japanese_goblin", "hankey", "skull", "alien", "space_invader", "bow", "information_desk_person", "no_good", "ok_woman", "raising_hand", "person_with_pouting_face", "person_frowning", "massage", "haircut", "couple_with_heart", "couplekiss", "raised_hands", "clap", "hand", "ear", "eyes", "nose", "lips", "kiss", "tongue", "nail_care", "wave", "+1", "-1", "point_up", "point_up_2", "point_down", "point_left", "point_right", "ok_hand", "v", "facepunch", "fist", "raised_hand", "muscle", "open_hands", "pray"] - }, - { - name: "nature", - fullname: "Nature", - tabicon: "evergreen_tree", - icons: ["seedling", "evergreen_tree", "deciduous_tree", "palm_tree", "cactus", "tulip", "cherry_blossom", "rose", "hibiscus", "sunflower", "blossom", "bouquet", "ear_of_rice", "herb", "four_leaf_clover", "maple_leaf", "fallen_leaf", "leaves", "mushroom", "chestnut", "rat", "mouse2", "mouse", "hamster", "ox", "water_buffalo", "cow2", "cow", "tiger2", "leopard", "tiger", "rabbit2", "rabbit", "cat2", "cat", "racehorse", "horse", "ram", "sheep", "goat", "rooster", "chicken", "baby_chick", "hatching_chick", "hatched_chick", "bird", "penguin", "elephant", "dromedary_camel", "camel", "boar", "pig2", "pig", "pig_nose", "dog2", "poodle", "dog", "wolf", "bear", "koala", "panda_face", "monkey_face", "see_no_evil", "hear_no_evil", "speak_no_evil", "monkey", "dragon", "dragon_face", "crocodile", "snake", "turtle", "frog", "whale2", "whale", "dolphin", "octopus", "fish", "tropical_fish", "blowfish", "shell", "snail", "bug", "ant", "bee", "beetle", "feet", "zap", "fire", "crescent_moon", "sunny", "partly_sunny", "cloud", "droplet", "sweat_drops", "umbrella", "dash", "snowflake", "star2", "star", "stars", "sunrise_over_mountains", "sunrise", "rainbow", "ocean", "volcano", "milky_way", "mount_fuji", "japan", "globe_with_meridians", "earth_africa", "earth_americas", "earth_asia", "new_moon", "waxing_crescent_moon", "first_quarter_moon", "moon", "full_moon", "waning_gibbous_moon", "last_quarter_moon", "waning_crescent_moon", "new_moon_with_face", "full_moon_with_face", "first_quarter_moon_with_face", "last_quarter_moon_with_face", "sun_with_face"] - }, - { - name: "food", - fullname: "Food & Drink", - tabicon: "hamburger", - icons: ["tomato", "eggplant", "corn", "sweet_potato", "grapes", "melon", "watermelon", "tangerine", "lemon", "banana", "pineapple", "apple", "green_apple", "pear", "peach", "cherries", "strawberry", "hamburger", "pizza", "meat_on_bone", "poultry_leg", "rice_cracker", "rice_ball", "rice", "curry", "ramen", "spaghetti", "bread", "fries", "dango", "oden", "sushi", "fried_shrimp", "fish_cake", "icecream", "shaved_ice", "ice_cream", "doughnut", "cookie", "chocolate_bar", "candy", "lollipop", "custard", "honey_pot", "cake", "bento", "stew", "egg", "fork_and_knife", "tea", "coffee", "sake", "wine_glass", "cocktail", "tropical_drink", "beer", "beers", "baby_bottle"] - }, - { - name: "celebration", - fullname: "Celebration", - tabicon: "gift", - icons: ["ribbon", "gift", "birthday", "jack_o_lantern", "christmas_tree", "tanabata_tree", "bamboo", "rice_scene", "fireworks", "sparkler", "tada", "confetti_ball", "balloon", "dizzy", "sparkles", "boom", "mortar_board", "crown", "dolls", "flags", "wind_chime", "crossed_flags", "izakaya_lantern", "ring", "heart", "broken_heart", "love_letter", "two_hearts", "revolving_hearts", "heartbeat", "heartpulse", "sparkling_heart", "cupid", "gift_heart", "heart_decoration", "purple_heart", "yellow_heart", "green_heart", "blue_heart"] - }, - { - name: "activity", - fullname: "Activities", - tabicon: "soccer", - icons: ["runner", "walking", "dancer", "rowboat", "swimmer", "surfer", "bath", "snowboarder", "ski", "snowman", "bicyclist", "mountain_bicyclist", "horse_racing", "tent", "fishing_pole_and_fish", "soccer", "basketball", "football", "baseball", "tennis", "rugby_football", "golf", "trophy", "running_shirt_with_sash", "checkered_flag", "musical_keyboard", "guitar", "violin", "saxophone", "trumpet", "musical_note", "notes", "musical_score", "headphones", "microphone", "performing_arts", "ticket", "tophat", "circus_tent", "clapper", "art", "dart", "8ball", "bowling", "slot_machine", "game_die", "video_game", "flower_playing_cards", "black_joker", "mahjong", "carousel_horse", "ferris_wheel", "roller_coaster"] - }, - { - name: "travel", - fullname: "Travel & Places", - tabicon: "airplane", - icons: ["train", "mountain_railway", "railway_car", "steam_locomotive", "monorail", "bullettrain_side", "bullettrain_front", "train2", "metro", "light_rail", "station", "tram", "bus", "oncoming_bus", "trolleybus", "minibus", "ambulance", "fire_engine", "police_car", "oncoming_police_car", "rotating_light", "taxi", "oncoming_taxi", "car", "oncoming_automobile", "blue_car", "truck", "articulated_lorry", "tractor", "bike", "busstop", "fuelpump", "construction", "vertical_traffic_light", "traffic_light", "rocket", "helicopter", "airplane", "seat", "anchor", "ship", "speedboat", "boat", "aerial_tramway", "mountain_cableway", "suspension_railway", "passport_control", "customs", "baggage_claim", "left_luggage", "yen", "euro", "pound", "dollar", "statue_of_liberty", "moyai", "foggy", "tokyo_tower", "fountain", "european_castle", "japanese_castle", "city_sunrise", "city_sunset", "night_with_stars", "bridge_at_night", "house", "house_with_garden", "office", "department_store", "factory", "post_office", "european_post_office", "hospital", "bank", "hotel", "love_hotel", "wedding", "church", "convenience_store", "school", "cn", "de", "es", "fr", "gb", "it", "jp", "kr", "ru", "us"] - }, - { - name: "objects", - fullname: "Objects & Symbols", - tabicon: "eyeglasses", - icons: ["watch", "iphone", "calling", "computer", "alarm_clock", "hourglass_flowing_sand", "hourglass", "camera", "video_camera", "movie_camera", "tv", "radio", "pager", "telephone_receiver", "phone", "fax", "minidisc", "floppy_disk", "cd", "dvd", "vhs", "battery", "electric_plug", "bulb", "flashlight", "satellite", "credit_card", "money_with_wings", "moneybag", "gem", "closed_umbrella", "pouch", "purse", "handbag", "briefcase", "school_satchel", "lipstick", "eyeglasses", "womans_hat", "sandal", "high_heel", "boot", "mans_shoe", "athletic_shoe", "bikini", "dress", "kimono", "womans_clothes", "shirt", "necktie", "jeans", "door", "shower", "bathtub", "toilet", "barber", "syringe", "pill", "microscope", "telescope", "crystal_ball", "wrench", "hocho", "nut_and_bolt", "hammer", "bomb", "smoking", "gun", "bookmark", "newspaper", "key", "email", "envelope_with_arrow", "incoming_envelope", "e-mail", "inbox_tray", "outbox_tray", "package", "postal_horn", "postbox", "mailbox_closed", "mailbox", "mailbox_with_mail", "mailbox_with_no_mail", "page_facing_up", "page_with_curl", "bookmark_tabs", "chart_with_upwards_trend", "chart_with_downwards_trend", "bar_chart", "date", "calendar", "low_brightness", "high_brightness", "scroll", "clipboard", "book", "notebook", "notebook_with_decorative_cover", "ledger", "closed_book", "green_book", "blue_book", "orange_book", "books", "card_index", "link", "paperclip", "pushpin", "scissors", "triangular_ruler", "round_pushpin", "straight_ruler", "triangular_flag_on_post", "file_folder", "open_file_folder", "black_nib", "pencil2", "memo", "lock_with_ink_pen", "closed_lock_with_key", "lock", "unlock", "mega", "loudspeaker", "sound", "loud_sound", "speaker", "mute", "zzz", "bell", "no_bell", "thought_balloon", "speech_balloon", "children_crossing", "mag", "mag_right", "no_entry_sign", "no_entry", "name_badge", "no_pedestrians", "do_not_litter", "no_bicycles", "non-potable_water", "no_mobile_phones", "underage", "accept", "ideograph_advantage", "white_flower", "secret", "congratulations", "u5408", "u6e80", "u7981", "u6709", "u7121", "u7533", "u55b6", "u6708", "u5272", "u7a7a", "sa", "koko", "u6307", "chart", "sparkle", "eight_spoked_asterisk", "negative_squared_cross_mark", "white_check_mark", "eight_pointed_black_star", "vibration_mode", "mobile_phone_off", "vs", "a", "b", "ab", "cl", "o2", "sos", "id", "parking", "wc", "cool", "free", "new", "ng", "ok", "up", "atm", "aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpius", "sagittarius", "capricorn", "aquarius", "pisces", "restroom", "mens", "womens", "baby_symbol", "wheelchair", "potable_water", "no_smoking", "put_litter_in_its_place", "arrow_forward", "arrow_backward", "arrow_up_small", "arrow_down_small", "fast_forward", "rewind", "arrow_double_up", "arrow_double_down", "arrow_right", "arrow_left", "arrow_up", "arrow_down", "arrow_upper_right", "arrow_lower_right", "arrow_lower_left", "arrow_upper_left", "arrow_up_down", "left_right_arrow", "arrows_counterclockwise", "arrow_right_hook", "leftwards_arrow_with_hook", "arrow_heading_up", "arrow_heading_down", "twisted_rightwards_arrows", "repeat", "repeat_one", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "keycap_ten", "1234", "hash", "abc", "abcd", "capital_abcd", "information_source", "signal_strength", "cinema", "symbols", "heavy_plus_sign", "heavy_minus_sign", "wavy_dash", "heavy_division_sign", "heavy_multiplication_x", "heavy_check_mark", "arrows_clockwise", "tm", "copyright", "registered", "currency_exchange", "heavy_dollar_sign", "curly_loop", "loop", "part_alternation_mark", "exclamation", "bangbang", "question", "grey_exclamation", "grey_question", "interrobang", "x", "o", "100", "end", "back", "on", "top", "soon", "cyclone", "m", "ophiuchus", "six_pointed_star", "beginner", "trident", "warning", "hotsprings", "recycle", "anger", "diamond_shape_with_a_dot_inside", "spades", "clubs", "hearts", "diamonds", "ballot_box_with_check", "white_circle", "black_circle", "radio_button", "red_circle", "large_blue_circle", "small_red_triangle", "small_red_triangle_down", "small_orange_diamond", "small_blue_diamond", "large_orange_diamond", "large_blue_diamond", "black_small_square", "white_small_square", "black_large_square", "white_large_square", "black_medium_square", "white_medium_square", "black_medium_small_square", "white_medium_small_square", "black_square_button", "white_square_button", "clock1", "clock2", "clock3", "clock4", "clock5", "clock6", "clock7", "clock8", "clock9", "clock10", "clock11", "clock12", "clock130", "clock230", "clock330", "clock430", "clock530", "clock630", "clock730", "clock830", "clock930", "clock1030", "clock1130", "clock1230"] - } -]; - -// scrub groups -groups.forEach(function(group){ - group.icons = _.reject(group.icons, function(obj){ - return !Discourse.Emoji.exists(obj); - }); -}); - -// export so others can modify -Discourse.Emoji.groups = groups; - -var closeSelector = function(){ - $('.emoji-modal, .emoji-modal-wrapper').remove(); - $('body, textarea').off('keydown.emoji'); -}; - -var ungroupedIcons, recentlyUsedIcons; - -var initializeUngroupedIcons = function(){ - ungroupedIcons = []; - - var groupedIcons = {}; - _.each(groups, function(group){ - _.each(group.icons, function(icon){ - groupedIcons[icon] = true; - }); - }); - - var emojis = Discourse.Emoji.list(); - _.each(emojis, function(emoji){ - if(groupedIcons[emoji] !== true){ - ungroupedIcons.push(emoji); - } - }); - - if(ungroupedIcons.length > 0){ - groups.push({name: 'ungrouped', icons: ungroupedIcons}); - } -}; +const PER_ROW = 12, PER_PAGE = 60; +let ungroupedIcons, recentlyUsedIcons; if (!keyValueStore.getObject(EMOJI_USAGE)) { keyValueStore.setObject({key: EMOJI_USAGE, value: {}}); } -var trackEmojiUsage = function(title) { - var recent = keyValueStore.getObject(EMOJI_USAGE); +function closeSelector() { + $('.emoji-modal, .emoji-modal-wrapper').remove(); + $('body, textarea').off('keydown.emoji'); +} + +function initializeUngroupedIcons() { + const groupedIcons = {}; + + groups.forEach(group => { + group.icons.forEach(icon => groupedIcons[icon] = true); + }); + + ungroupedIcons = []; + const emojis = Discourse.Emoji.list(); + emojis.forEach(emoji => { + if (groupedIcons[emoji] !== true) { + ungroupedIcons.push(emoji); + } + }); + + if (ungroupedIcons.length) { + groups.push({name: 'ungrouped', icons: ungroupedIcons}); + } +} + +function trackEmojiUsage(title) { + const recent = keyValueStore.getObject(EMOJI_USAGE); if (!recent[title]) { recent[title] = { title: title, usage: 0 }; } recent[title]["usage"]++; @@ -104,46 +46,40 @@ var trackEmojiUsage = function(title) { // clear the cache recentlyUsedIcons = null; -}; +} -var initializeRecentlyUsedIcons = function(){ +function sortByUsage(a, b) { + if (a.usage > b.usage) { return -1; } + if (b.usage > a.usage) { return 1; } + return a.title.localeCompare(b.title); +} + +function initializeRecentlyUsedIcons() { recentlyUsedIcons = []; - var usage = _.map(keyValueStore.getObject(EMOJI_USAGE)); - usage.sort(function(a,b){ - if(a.usage > b.usage){ - return -1; + const usage = _.map(keyValueStore.getObject(EMOJI_USAGE)).sort(sortByUsage); + const recent = usage.slice(0, PER_ROW); + + if (recent.length > 0) { + + recent.forEach(emoji => recentlyUsedIcons.push(emoji.title)); + + const recentGroup = groups.findProperty('name', 'recent'); + if (recentGroup) { + recentGroup.icons = recentlyUsedIcons; + } else { + groups.push({ name: 'recent', icons: recentlyUsedIcons }); } - if(b.usage > a.usage){ - return 1; - } - return a.title.localeCompare(b.title); - }); - - var recent = _.take(usage, PER_ROW); - - if(recent.length > 0){ - _.each(recent, function(emoji){ - recentlyUsedIcons.push(emoji.title); - }); - - var recentGroup = _.find(groups, {name: 'recent'}); - if(!recentGroup){ - recentGroup = {name: 'recent', icons: []}; - groups.push(recentGroup); - } - - recentGroup.icons = recentlyUsedIcons; } -}; +} -var toolbar = function(selected){ +function toolbar(selected) { if (!ungroupedIcons) { initializeUngroupedIcons(); } if (!recentlyUsedIcons) { initializeRecentlyUsedIcons(); } - return _.map(groups, function(g, i){ - var icon = g.tabicon; - var title = g.fullname; + return groups.map((g, i) => { + let icon = g.tabicon; + let title = g.fullname; if (g.name === "recent") { icon = "star"; title = "Recent"; @@ -151,60 +87,48 @@ var toolbar = function(selected){ icon = g.icons[0]; title = "Custom"; } - var row = {src: Discourse.Emoji.urlFor(icon), title: title, groupId: i}; - if(i === selected){ - row.selected = true; - } - return row; + + return { src: Discourse.Emoji.urlFor(icon), + title, + groupId: i, + selected: i === selected }; }); -}; +} -var PER_ROW = 12, PER_PAGE = 60; - -var bindEvents = function(page, offset, options) { - var composerController = Discourse.__container__.lookup('controller:composer'); - - $('.emoji-page a').click(function(){ - var title = $(this).attr('title'); +function bindEvents(page, offset, options) { + $('.emoji-page a').click(e => { + const title = $(e.currentTarget).attr('title'); trackEmojiUsage(title); - - const prefix = options.skipPrefix ? "" : ":"; - composerController.appendTextAtCursor(`${prefix}${title}:`, {space: !options.skipPrefix}); + options.onSelect(title); closeSelector(); return false; - }).hover(function(){ - var title = $(this).attr('title'); - var html = " :" + title + ":"; + }).hover(e => { + const title = $(e.currentTarget).attr('title'); + const html = " :" + title + ":"; $('.emoji-modal .info').html(html); - },function(){ - $('.emoji-modal .info').html(""); - }); + }, () => $('.emoji-modal .info').html("")); - $('.emoji-modal .nav .next a').click(function(){ - render(page, offset+PER_PAGE, options); - }); - - $('.emoji-modal .nav .prev a').click(function(){ - render(page, offset-PER_PAGE, options); - }); + $('.emoji-modal .nav .next a').click(() => render(page, offset+PER_PAGE, options)); + $('.emoji-modal .nav .prev a').click(() => render(page, offset-PER_PAGE, options)); $('.emoji-modal .toolbar a').click(function(){ - var p = parseInt($(this).data('group-id')); + const p = parseInt($(this).data('group-id')); render(p, 0, options); return false; }); -}; +} -var render = function(page, offset, options) { +function render(page, offset, options) { keyValueStore.set({key: "emojiPage", value: page}); keyValueStore.set({key: "emojiOffset", value: offset}); - var toolbarItems = toolbar(page); - var rows = [], row = []; - var icons = groups[page].icons; - var max = offset + PER_PAGE; + const toolbarItems = toolbar(page); + const rows = []; + let row = []; + const icons = groups[page].icons; + const max = offset + PER_PAGE; - for(var i=offset; i icons.length }; - $('body .emoji-modal').remove(); - var rendered = Ember.TEMPLATES["emoji-toolbar.raw"](model); - $('body').append(rendered); + $('.emoji-modal', options.appendTo).remove(); + const template = options.container.lookup('template:emoji-toolbar.raw'); + options.appendTo.append(template(model)); bindEvents(page, offset, options); -}; +} -var showSelector = function(options) { +function showSelector(options) { options = options || {}; + options.appendTo = options.appendTo || $('body'); - $('body').append('
'); + options.appendTo.append('
'); + $('.emoji-modal-wrapper').click(() => closeSelector()); - $('.emoji-modal-wrapper').click(function(){ - closeSelector(); - }); + const page = keyValueStore.getInt("emojiPage", 0); + const offset = keyValueStore.getInt("emojiOffset", 0); - if (Discourse.Mobile.mobileView) PER_ROW = 9; - - var page = keyValueStore.getInt("emojiPage", 0); - var offset = keyValueStore.getInt("emojiOffset", 0); render(page, offset, options); - $('body, textarea').on('keydown.emoji', function(e){ - if(e.which === 27){ + $('body, textarea').on('keydown.emoji', e => { + if (e.which === 27) { closeSelector(); return false; } }); -}; +} export { showSelector }; diff --git a/app/assets/javascripts/discourse/templates/components/d-editor-modal.hbs b/app/assets/javascripts/discourse/templates/components/d-editor-modal.hbs new file mode 100644 index 000000000..e84f47095 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/d-editor-modal.hbs @@ -0,0 +1,7 @@ + +{{yield}} + +
+ {{d-button class="btn-primary" label="composer.modal_ok" action="ok"}} + {{d-button class="btn-danger" label="composer.modal_cancel" action="cancel"}} +
diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs new file mode 100644 index 000000000..8921294cb --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs @@ -0,0 +1,32 @@ + +
+ {{#d-editor-modal class="insert-link" hidden=insertLinkHidden okAction="insertLink"}} +

{{i18n "composer.link_dialog_title"}}

+ {{text-field value=link placeholderKey="composer.link_placeholder"}} + {{/d-editor-modal}} +
+ +
+
+ {{d-button action="bold" icon="bold" class="bold"}} + {{d-button action="italic" icon="italic" class="italic"}} +
+ {{d-button action="showLinkModal" icon="link" class="link"}} + {{d-button action="quote" icon="quote-right" class="quote"}} + {{d-button action="code" icon="code" class="code"}} +
+ {{d-button action="bullet" icon="list-ul" class="bullet"}} + {{d-button action="list" icon="list-ol" class="list"}} + {{d-button action="heading" icon="font" class="heading"}} + {{d-button action="rule" icon="minus" class="rule"}} + {{#if siteSettings.enable_emoji}} + {{d-button action="emoji" icon="smile-o" class="emoji"}} + {{/if}} +
+ + {{textarea value=value class="d-editor-input"}} + +
+ {{{preview}}} +
+
diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-topic-template.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-topic-template.hbs index 7a410abbd..c0d58fbbd 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-topic-template.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-topic-template.hbs @@ -1,2 +1,2 @@ -{{pagedown-editor value=category.topic_template}} +{{d-editor value=category.topic_template}} diff --git a/app/assets/javascripts/discourse/templates/components/pagedown-editor.hbs b/app/assets/javascripts/discourse/templates/components/pagedown-editor.hbs deleted file mode 100644 index 52e92ae1f..000000000 --- a/app/assets/javascripts/discourse/templates/components/pagedown-editor.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
-{{textarea value=value class="wmd-input"}} -
diff --git a/app/assets/javascripts/discourse/templates/queued-posts.hbs b/app/assets/javascripts/discourse/templates/queued-posts.hbs index 5fc07e042..ee7e79b15 100644 --- a/app/assets/javascripts/discourse/templates/queued-posts.hbs +++ b/app/assets/javascripts/discourse/templates/queued-posts.hbs @@ -35,7 +35,7 @@
{{#if ctrl.editing}} - {{pagedown-editor value=ctrl.buffered.raw}} + {{d-editor value=ctrl.buffered.raw}} {{else}} {{{cook-text ctrl.post.raw}}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/user/about.hbs b/app/assets/javascripts/discourse/templates/user/about.hbs index d29c95d71..2d1e162db 100644 --- a/app/assets/javascripts/discourse/templates/user/about.hbs +++ b/app/assets/javascripts/discourse/templates/user/about.hbs @@ -9,7 +9,7 @@
- {{pagedown-editor value=model.bio_raw}} + {{d-editor value=model.bio_raw}}
diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs index b8e42444c..c195c7839 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -137,7 +137,7 @@
- {{pagedown-editor value=model.bio_raw}} + {{d-editor value=model.bio_raw}}
diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index 62d3dad0e..a46a9e1e8 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -185,7 +185,9 @@ const ComposerView = Ember.View.extend(Ember.Evented, { _applyEmojiAutocomplete() { if (!this.siteSettings.enable_emoji) { return; } - const template = this.container.lookup('template:emoji-selector-autocomplete.raw'); + const container = this.container; + const template = container.lookup('template:emoji-selector-autocomplete.raw'); + const controller = this.get('controller'); this.$('.wmd-input').autocomplete({ template: template, @@ -195,7 +197,12 @@ const ComposerView = Ember.View.extend(Ember.Evented, { if (v.code) { return `${v.code}:`; } else { - showSelector({ skipPrefix: true }); + showSelector({ + container, + onSelect(title) { + controller.appendTextAtCursor(title + ':', {space: false}); + } + }); return ""; } }, diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index d1c49d58d..fbb72ad3f 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -75,6 +75,7 @@ //= require ./discourse/views/header //= require ./discourse/dialects/dialect //= require ./discourse/lib/emoji/emoji +//= require ./discourse/lib/emoji/emoji-groups //= require ./discourse/lib/emoji/emoji-toolbar //= require ./discourse/views/composer //= require ./discourse/lib/show-modal diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 771ff63a3..992370c48 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -10,4 +10,5 @@ @import "common/topic-entrance"; @import "common/printer-friendly"; @import "common/base/*"; +@import "common/d-editor"; @import "vendor/pikaday"; diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 7953f4eae..60396122e 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1165,10 +1165,6 @@ table.api-keys { margin-top: 10px; } - .pagedown-editor { - width: 98%; - } - textarea.plain { width: 98%; height: 200px; @@ -1478,9 +1474,6 @@ and (max-width : 500px) { .content-editor { width: 100%; - .pagedown-editor { - box-sizing: border-box; - } } div.ac-wrap { diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 7a3949519..2580ff1ef 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -153,24 +153,6 @@ body { resize: none; } - .pagedown-editor { - width: 540px; - background-color: $secondary; - padding: 0 10px 13px 10px; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - .preview { - margin-top: 8px; - border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%); - padding: 8px 8px 0 8px; - p { - margin: 0 0 10px 0; - } - } - .preview.hidden { - display: none; - } - } - .avatar-wrapper { background-color: $secondary; display: inline-block; diff --git a/app/assets/stylesheets/common/base/emoji.scss b/app/assets/stylesheets/common/base/emoji.scss index 9cb13b4d5..264416f1b 100644 --- a/app/assets/stylesheets/common/base/emoji.scss +++ b/app/assets/stylesheets/common/base/emoji.scss @@ -17,9 +17,10 @@ body img.emoji { background-color: dark-light-choose(#dadada, blend-primary-secondary(5%)); } -.emoji-page td { +table.emoji-page td { border: 1px solid transparent; background-color: dark-light-choose(white, $secondary); + padding: 0 !important; } .emoji-page a { diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 74291ab40..d80ada7d4 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -103,10 +103,6 @@ .modal.edit-category-modal { .modal-body { - .pagedown-editor { - width: 98%; - } - textarea { height: 10em; } diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss new file mode 100644 index 000000000..d9bdbca35 --- /dev/null +++ b/app/assets/stylesheets/common/d-editor.scss @@ -0,0 +1,94 @@ +.d-editor { + border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); +} + +.d-editor-container { + padding: 0 10px 13px 10px; +} + +.d-editor-overlay { + position: absolute; + background-color: black; + opacity: 0.8; +} + +.d-editor-modals { + position: absolute; +} + +.d-editor .d-editor-modal { + min-width: 400px; + position: absolute; + background-color: $secondary; + border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + padding: 1em; + top: 50px; + + input { + width: 98%; + } + h3 { + margin-bottom: 0.5em; + } +} + +.d-editor-button-bar { + margin: 5px; + padding: 0; + height: 20px; + overflow: hidden; + + button { + background-color: transparent; + padding: 2px 4px; + float: left; + margin-right: 6px; + } + +} + +.d-editor-spacer { + width: 1px; + height: 20px; + margin-right: 8px; + margin-left: 5px; + background-color: dark-light-diff($primary, $secondary, 90%, -60%); + display: inline-block; + float: left; +} + +.d-editor-input { + color: $primary; + width: 98%; + height: 200px; + + &:disabled { + background-color: dark-light-diff($primary, $secondary, 90%, -60%); + } +} + +.d-editor-preview { + color: $primary; + border: 1px dashed dark-light-diff($primary, $secondary, 90%, -60%); + overflow: auto; + visibility: visible; + cursor: default; + margin-top: 8px; + padding: 8px 8px 0 8px; + video { + max-width: 100%; + max-height: 500px; + height: auto; + } + audio { + max-width: 100%; + } + &.hidden { + width: 0; + visibility: hidden; + } +} + +.d-editor-preview > *:first-child { + margin-top: 0; +} diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index ea4202fb0..cd1f837e6 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -39,14 +39,6 @@ } } - .pagedown-editor { - width: 450px; - - textarea { - width: 440px; - } - } - .bio-composer #wmd-quote-post { display: none; } diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 13b7ebad0..b4acd1702 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -67,10 +67,6 @@ display: none; } - .pagedown-editor { - width: 100%; - } - textarea {width: 100%;} } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index c86a56ea0..00c9a6b4a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -889,6 +889,7 @@ en: link_description: "enter link description here" link_dialog_title: "Insert Hyperlink" link_optional_text: "optional title" + link_placeholder: "http://example.com \"optional text\"" quote_title: "Blockquote" quote_text: "Blockquote" code_title: "Preformatted text" @@ -901,10 +902,10 @@ en: heading_title: "Heading" heading_text: "Heading" hr_title: "Horizontal Rule" - undo_title: "Undo" - redo_title: "Redo" help: "Markdown Editing Help" toggler: "hide or show the composer panel" + modal_ok: "OK" + modal_cancel: "Cancel" admin_options_title: "Optional staff settings for this topic" auto_close: diff --git a/test/javascripts/acceptance/category-edit-test.js.es6 b/test/javascripts/acceptance/category-edit-test.js.es6 index fc13ab537..d80e36785 100644 --- a/test/javascripts/acceptance/category-edit-test.js.es6 +++ b/test/javascripts/acceptance/category-edit-test.js.es6 @@ -34,7 +34,7 @@ test("Change the topic template", assert => { click('.edit-category'); click('.edit-category-topic-template'); - fillIn('.wmd-input', 'this is the new topic template'); + fillIn('.d-editor-input', 'this is the new topic template'); click('#save-category'); andThen(() => { assert.ok(!visible('#discourse-modal'), 'it closes the modal'); diff --git a/test/javascripts/components/d-editor-test.js.es6 b/test/javascripts/components/d-editor-test.js.es6 new file mode 100644 index 000000000..7ae1c7823 --- /dev/null +++ b/test/javascripts/components/d-editor-test.js.es6 @@ -0,0 +1,450 @@ +import componentTest from 'helpers/component-test'; + +moduleForComponent('d-editor', {integration: true}); + +componentTest('preview updates with markdown', { + template: '{{d-editor value=value}}', + + test(assert) { + assert.ok(this.$('.d-editor-button-bar').length); + assert.equal(this.$('.d-editor-preview.hidden').length, 1); + + fillIn('.d-editor-input', 'hello **world**'); + + andThen(() => { + assert.equal(this.get('value'), 'hello **world**'); + assert.equal(this.$('.d-editor-preview.hidden').length, 0); + assert.equal(this.$('.d-editor-preview').html().trim(), '

hello world

'); + }); + } +}); + +componentTest('updating the value refreshes the preview', { + template: '{{d-editor value=value}}', + + setup() { + this.set('value', 'evil trout'); + }, + + test(assert) { + assert.equal(this.$('.d-editor-preview').html().trim(), '

evil trout

'); + + andThen(() => this.set('value', 'zogstrip')); + andThen(() => assert.equal(this.$('.d-editor-preview').html().trim(), '

zogstrip

')); + } +}); + +function testCase(title, testFunc) { + componentTest(title, { + template: '{{d-editor value=value}}', + setup() { + this.set('value', 'hello world.'); + }, + test(assert) { + const textarea = this.$('textarea.d-editor-input')[0]; + testFunc.call(this, assert, textarea); + } + }); +} + +testCase(`bold button with no selection`, function(assert, textarea) { + click(`button.bold`); + andThen(() => { + const example = I18n.t(`composer.bold_text`); + assert.equal(this.get('value'), `hello world.**${example}**`); + assert.equal(textarea.selectionStart, 14); + assert.equal(textarea.selectionEnd, 14 + example.length); + }); +}); + +testCase(`bold button with a selection`, function(assert, textarea) { + textarea.selectionStart = 6; + textarea.selectionEnd = 11; + + click(`button.bold`); + andThen(() => { + assert.equal(this.get('value'), `hello **world**.`); + assert.equal(textarea.selectionStart, 8); + assert.equal(textarea.selectionEnd, 13); + }); + + click(`button.bold`); + andThen(() => { + assert.equal(this.get('value'), 'hello world.'); + assert.equal(textarea.selectionStart, 6); + assert.equal(textarea.selectionEnd, 11); + }); +}); + +testCase(`bold with a multiline selection`, function (assert, textarea) { + this.set('value', "hello\n\nworld\n\ntest."); + + andThen(() => { + textarea.selectionStart = 0; + textarea.selectionEnd = 12; + }); + + click(`button.bold`); + andThen(() => { + assert.equal(this.get('value'), `**hello**\n\n**world**\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 20); + }); + + click(`button.bold`); + andThen(() => { + assert.equal(this.get('value'), `hello\n\nworld\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 12); + }); +}); + +testCase(`italic button with no selection`, function(assert, textarea) { + click(`button.italic`); + andThen(() => { + const example = I18n.t(`composer.italic_text`); + assert.equal(this.get('value'), `hello world.*${example}*`); + + assert.equal(textarea.selectionStart, 13); + assert.equal(textarea.selectionEnd, 13 + example.length); + }); +}); + +testCase(`italic button with a selection`, function(assert, textarea) { + textarea.selectionStart = 6; + textarea.selectionEnd = 11; + + click(`button.italic`); + andThen(() => { + assert.equal(this.get('value'), `hello *world*.`); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 12); + }); + + click(`button.italic`); + andThen(() => { + assert.equal(this.get('value'), 'hello world.'); + assert.equal(textarea.selectionStart, 6); + assert.equal(textarea.selectionEnd, 11); + }); +}); + +testCase(`italic with a multiline selection`, function (assert, textarea) { + this.set('value', "hello\n\nworld\n\ntest."); + + andThen(() => { + textarea.selectionStart = 0; + textarea.selectionEnd = 12; + }); + + click(`button.italic`); + andThen(() => { + assert.equal(this.get('value'), `*hello*\n\n*world*\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 16); + }); + + click(`button.italic`); + andThen(() => { + assert.equal(this.get('value'), `hello\n\nworld\n\ntest.`); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 12); + }); +}); + +testCase('link modal (cancel)', function(assert) { + assert.equal(this.$('.insert-link.hidden').length, 1); + + click('button.link'); + andThen(() => { + assert.equal(this.$('.insert-link.hidden').length, 0); + }); + + click('.insert-link button.btn-danger'); + andThen(() => { + assert.equal(this.$('.insert-link.hidden').length, 1); + assert.equal(this.get('value'), 'hello world.'); + }); +}); + +testCase('link modal (simple link)', function(assert) { + click('button.link'); + fillIn('.insert-link input', 'http://eviltrout.com'); + click('.insert-link button.btn-primary'); + andThen(() => { + assert.equal(this.$('.insert-link.hidden').length, 1); + assert.equal(this.get('value'), 'hello world.[http://eviltrout.com](http://eviltrout.com)'); + }); +}); + +testCase('link modal (link with description)', function(assert) { + click('button.link'); + fillIn('.insert-link input', 'http://eviltrout.com "evil trout"'); + click('.insert-link button.btn-primary'); + andThen(() => { + assert.equal(this.$('.insert-link.hidden').length, 1); + assert.equal(this.get('value'), 'hello world.[evil trout](http://eviltrout.com)'); + }); +}); + +componentTest('code button', { + template: '{{d-editor value=value}}', + setup() { + this.set('value', "first line\n\nsecond line\n\nthird line"); + }, + + test(assert) { + const textarea = this.$('textarea.d-editor-input')[0]; + + click('button.code'); + andThen(() => { + assert.equal(this.get('value'), "first line\n\nsecond line\n\nthird line`" + I18n.t('composer.code_text') + "`"); + this.set('value', "first line\n\nsecond line\n\nthird line"); + }); + + andThen(() => { + textarea.selectionStart = 6; + textarea.selectionEnd = 10; + }); + + click('button.code'); + andThen(() => { + assert.equal(this.get('value'), "first `line`\n\nsecond line\n\nthird line"); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 11); + }); + + click('button.code'); + andThen(() => { + assert.equal(this.get('value'), "first line\n\nsecond line\n\nthird line"); + assert.equal(textarea.selectionStart, 6); + assert.equal(textarea.selectionEnd, 10); + + textarea.selectionStart = 0; + textarea.selectionEnd = 23; + }); + + click('button.code'); + andThen(() => { + assert.equal(this.get('value'), " first line\n\n second line\n\nthird line"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 31); + }); + + click('button.code'); + andThen(() => { + assert.equal(this.get('value'), "first line\n\nsecond line\n\nthird line"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 23); + }); + } +}); + +testCase('quote button', function(assert, textarea) { + click('button.quote'); + andThen(() => { + assert.equal(this.get('value'), 'hello world.'); + }); + + andThen(() => { + textarea.selectionStart = 6; + textarea.selectionEnd = 11; + }); + + click('button.quote'); + andThen(() => { + assert.equal(this.get('value'), 'hello > world.'); + assert.equal(textarea.selectionStart, 6); + assert.equal(textarea.selectionEnd, 13); + }); + + click('button.quote'); + andThen(() => { + assert.equal(this.get('value'), 'hello world.'); + assert.equal(textarea.selectionStart, 6); + assert.equal(textarea.selectionEnd, 11); + }); +}); + +testCase(`bullet button with no selection`, function(assert, textarea) { + const example = I18n.t('composer.list_item'); + + click(`button.bullet`); + andThen(() => { + assert.equal(this.get('value'), `hello world.\n\n* ${example}`); + assert.equal(textarea.selectionStart, 14); + assert.equal(textarea.selectionEnd, 16 + example.length); + }); + + click(`button.bullet`); + andThen(() => { + assert.equal(this.get('value'), `hello world.\n\n${example}`); + }); +}); + +testCase(`bullet button with a selection`, function(assert, textarea) { + textarea.selectionStart = 6; + textarea.selectionEnd = 11; + + click(`button.bullet`); + andThen(() => { + assert.equal(this.get('value'), `hello\n\n* world\n\n.`); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 14); + }); + + click(`button.bullet`); + andThen(() => { + assert.equal(this.get('value'), `hello\n\nworld\n\n.`); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 12); + }); +}); + +testCase(`bullet button with a multiple line selection`, function(assert, textarea) { + this.set('value', "* Hello\n\nWorld\n\nEvil"); + + andThen(() => { + textarea.selectionStart = 0; + textarea.selectionEnd = 20; + }); + + click(`button.bullet`); + andThen(() => { + assert.equal(this.get('value'), "Hello\n\n* World\n\n* Evil"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 22); + }); + + click(`button.bullet`); + andThen(() => { + assert.equal(this.get('value'), "* Hello\n\nWorld\n\nEvil"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 20); + }); +}); + +testCase(`list button with no selection`, function(assert, textarea) { + const example = I18n.t('composer.list_item'); + + click(`button.list`); + andThen(() => { + assert.equal(this.get('value'), `hello world.\n\n1. ${example}`); + assert.equal(textarea.selectionStart, 14); + assert.equal(textarea.selectionEnd, 17 + example.length); + }); + + click(`button.list`); + andThen(() => { + assert.equal(this.get('value'), `hello world.\n\n${example}`); + assert.equal(textarea.selectionStart, 14); + assert.equal(textarea.selectionEnd, 14 + example.length); + }); +}); + +testCase(`list button with a selection`, function(assert, textarea) { + textarea.selectionStart = 6; + textarea.selectionEnd = 11; + + click(`button.list`); + andThen(() => { + assert.equal(this.get('value'), `hello\n\n1. world\n\n.`); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 15); + }); + + click(`button.list`); + andThen(() => { + assert.equal(this.get('value'), `hello\n\nworld\n\n.`); + assert.equal(textarea.selectionStart, 7); + assert.equal(textarea.selectionEnd, 12); + }); +}); + +testCase(`list button with line sequence`, function(assert, textarea) { + this.set('value', "Hello\n\nWorld\n\nEvil"); + + andThen(() => { + textarea.selectionStart = 0; + textarea.selectionEnd = 18; + }); + + click(`button.list`); + andThen(() => { + assert.equal(this.get('value'), "1. Hello\n\n2. World\n\n3. Evil"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 27); + }); + + click(`button.list`); + andThen(() => { + assert.equal(this.get('value'), "Hello\n\nWorld\n\nEvil"); + assert.equal(textarea.selectionStart, 0); + assert.equal(textarea.selectionEnd, 18); + }); +}); + +testCase(`heading button with no selection`, function(assert, textarea) { + const example = I18n.t('composer.heading_text'); + + click(`button.heading`); + andThen(() => { + assert.equal(this.get('value'), `hello world.\n\n## ${example}`); + assert.equal(textarea.selectionStart, 14); + assert.equal(textarea.selectionEnd, 17 + example.length); + }); + + click(`button.heading`); + andThen(() => { + assert.equal(this.get('value'), `hello world.\n\n${example}`); + assert.equal(textarea.selectionStart, 14); + assert.equal(textarea.selectionEnd, 14 + example.length); + }); +}); + +testCase(`rule with no selection`, function(assert, textarea) { + click(`button.rule`); + andThen(() => { + assert.equal(this.get('value'), `hello world.\n\n----------\n`); + assert.equal(textarea.selectionStart, 25); + assert.equal(textarea.selectionEnd, 25); + }); + + click(`button.rule`); + andThen(() => { + assert.equal(this.get('value'), `hello world.\n\n----------\n\n\n----------\n`); + assert.equal(textarea.selectionStart, 38); + assert.equal(textarea.selectionEnd, 38); + }); +}); + +testCase(`rule with a selection`, function(assert, textarea) { + textarea.selectionStart = 6; + textarea.selectionEnd = 11; + + click(`button.rule`); + andThen(() => { + assert.equal(this.get('value'), `hello \n\n----------\n.`); + assert.equal(textarea.selectionStart, 19); + assert.equal(textarea.selectionEnd, 19); + }); +}); + +testCase(`emoji`, function(assert) { + assert.equal($('.emoji-modal').length, 0); + + click('button.emoji'); + andThen(() => { + assert.equal($('.emoji-modal').length, 1); + }); + + click('a[data-group-id=0]'); + click('a[title=grinning]'); + + andThen(() => { + assert.ok($('.emoji-modal').length === 0); + assert.equal(this.get('value'), 'hello world.:grinning:'); + }); +}); + + diff --git a/test/javascripts/helpers/component-test.js.es6 b/test/javascripts/helpers/component-test.js.es6 index 181ae5331..3ff28d3c9 100644 --- a/test/javascripts/helpers/component-test.js.es6 +++ b/test/javascripts/helpers/component-test.js.es6 @@ -1,5 +1,6 @@ import AppEvents from 'discourse/lib/app-events'; import createStore from 'helpers/create-store'; +import { loadAllHelpers } from 'discourse/initializers/load-all-helpers'; export default function(name, opts) { opts = opts || {}; @@ -11,6 +12,8 @@ export default function(name, opts) { } const appEvents = AppEvents.create(); + loadAllHelpers(); + this.container.register('site-settings:main', Discourse.SiteSettings, { instantiate: false }); this.container.register('app-events:main', appEvents, { instantiate: false }); this.container.register('capabilities:main', Ember.Object); @@ -18,12 +21,7 @@ export default function(name, opts) { this.container.injection('component', 'appEvents', 'app-events:main'); this.container.injection('component', 'capabilities', 'capabilities:main'); - andThen(() => { - this.render(opts.template); - }); - - andThen(() => { - opts.test.call(this, assert); - }); + andThen(() => this.render(opts.template)); + andThen(() => opts.test.call(this, assert)); }); } diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 5b9a489e0..cc05265ff 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -109,6 +109,9 @@ QUnit.testStart(function(ctx) { window.sandbox.stub(ScrollingDOMMethods, "bindOnScroll"); window.sandbox.stub(ScrollingDOMMethods, "unbindOnScroll"); + // Unless we ever need to test this, let's leave it off. + $.fn.autocomplete = Ember.K; + // Don't debounce in test unless we're testing debouncing if (ctx.module.indexOf('debounce') === -1) { Ember.run.debounce = Ember.run; diff --git a/test/stylesheets/test_helper.css b/test/stylesheets/test_helper.css index c0d5f7f11..f0e5ac700 100644 --- a/test/stylesheets/test_helper.css +++ b/test/stylesheets/test_helper.css @@ -5,3 +5,11 @@ .modal-backdrop { display: none; } + +.emoji-modal-wrapper { + display: none; +} + +.emoji-modal { + position: relative; +}