mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 17:06:31 -06:00
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.
This commit is contained in:
parent
49edffd3c3
commit
94b60e62a2
@ -2,7 +2,7 @@
|
||||
<p class='description'>{{model.description}}</p>
|
||||
|
||||
{{#if model.markdown}}
|
||||
{{pagedown-editor value=model.value}}
|
||||
{{d-editor value=model.value}}
|
||||
{{/if}}
|
||||
{{#if model.plainText}}
|
||||
{{textarea value=model.value class="plain"}}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
258
app/assets/javascripts/discourse/components/d-editor.js.es6
Normal file
258
app/assets/javascripts/discourse/components/d-editor.js.es6
Normal file
@ -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}:`)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
@ -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();
|
||||
}
|
||||
});
|
@ -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});
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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;
|
@ -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 = "<img src='" + Discourse.Emoji.urlFor(title) + "' class='emoji'> <span>:" + title + ":<span>";
|
||||
}).hover(e => {
|
||||
const title = $(e.currentTarget).attr('title');
|
||||
const html = "<img src='" + Discourse.Emoji.urlFor(title) + "' class='emoji'> <span>:" + title + ":<span>";
|
||||
$('.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<max; i++){
|
||||
for(let i=offset; i<max; i++){
|
||||
if(!icons[i]){ break; }
|
||||
if(row.length === PER_ROW){
|
||||
rows.push(row);
|
||||
@ -214,41 +138,38 @@ var render = function(page, offset, options) {
|
||||
}
|
||||
rows.push(row);
|
||||
|
||||
var model = {
|
||||
const model = {
|
||||
toolbarItems: toolbarItems,
|
||||
rows: rows,
|
||||
prevDisabled: offset === 0,
|
||||
nextDisabled: (max + 1) > 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('<div class="emoji-modal-wrapper"></div>');
|
||||
options.appendTo.append('<div class="emoji-modal-wrapper"></div>');
|
||||
$('.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 };
|
||||
|
@ -0,0 +1,7 @@
|
||||
|
||||
{{yield}}
|
||||
|
||||
<div class='controls'>
|
||||
{{d-button class="btn-primary" label="composer.modal_ok" action="ok"}}
|
||||
{{d-button class="btn-danger" label="composer.modal_cancel" action="cancel"}}
|
||||
</div>
|
@ -0,0 +1,32 @@
|
||||
<div class='d-editor-overlay hidden'></div>
|
||||
<div class='d-editor-modals'>
|
||||
{{#d-editor-modal class="insert-link" hidden=insertLinkHidden okAction="insertLink"}}
|
||||
<h3>{{i18n "composer.link_dialog_title"}}</h3>
|
||||
{{text-field value=link placeholderKey="composer.link_placeholder"}}
|
||||
{{/d-editor-modal}}
|
||||
</div>
|
||||
|
||||
<div class='d-editor-container'>
|
||||
<div class='d-editor-button-bar'>
|
||||
{{d-button action="bold" icon="bold" class="bold"}}
|
||||
{{d-button action="italic" icon="italic" class="italic"}}
|
||||
<div class='d-editor-spacer'></div>
|
||||
{{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"}}
|
||||
<div class='d-editor-spacer'></div>
|
||||
{{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}}
|
||||
</div>
|
||||
|
||||
{{textarea value=value class="d-editor-input"}}
|
||||
|
||||
<div class="d-editor-preview {{unless preview 'hidden'}}">
|
||||
{{{preview}}}
|
||||
</div>
|
||||
</div>
|
@ -1,2 +1,2 @@
|
||||
<label>{{i18n 'category.topic_template'}}</label>
|
||||
{{pagedown-editor value=category.topic_template}}
|
||||
{{d-editor value=category.topic_template}}
|
||||
|
@ -1,3 +0,0 @@
|
||||
<div class='wmd-button-bar'></div>
|
||||
{{textarea value=value class="wmd-input"}}
|
||||
<div class="wmd-preview preview {{unless value 'hidden'}}"></div>
|
@ -35,7 +35,7 @@
|
||||
|
||||
<div class='body'>
|
||||
{{#if ctrl.editing}}
|
||||
{{pagedown-editor value=ctrl.buffered.raw}}
|
||||
{{d-editor value=ctrl.buffered.raw}}
|
||||
{{else}}
|
||||
{{{cook-text ctrl.post.raw}}}
|
||||
{{/if}}
|
||||
|
@ -9,7 +9,7 @@
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n 'user.bio'}}</label>
|
||||
<div class="controls">
|
||||
{{pagedown-editor value=model.bio_raw}}
|
||||
{{d-editor value=model.bio_raw}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -137,7 +137,7 @@
|
||||
<div class="control-group pref-bio">
|
||||
<label class="control-label">{{i18n 'user.bio'}}</label>
|
||||
<div class="controls bio-composer">
|
||||
{{pagedown-editor value=model.bio_raw}}
|
||||
{{d-editor value=model.bio_raw}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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 "";
|
||||
}
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -10,4 +10,5 @@
|
||||
@import "common/topic-entrance";
|
||||
@import "common/printer-friendly";
|
||||
@import "common/base/*";
|
||||
@import "common/d-editor";
|
||||
@import "vendor/pikaday";
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -103,10 +103,6 @@
|
||||
|
||||
.modal.edit-category-modal {
|
||||
.modal-body {
|
||||
.pagedown-editor {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 10em;
|
||||
}
|
||||
|
94
app/assets/stylesheets/common/d-editor.scss
Normal file
94
app/assets/stylesheets/common/d-editor.scss
Normal file
@ -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;
|
||||
}
|
@ -39,14 +39,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pagedown-editor {
|
||||
width: 450px;
|
||||
|
||||
textarea {
|
||||
width: 440px;
|
||||
}
|
||||
}
|
||||
|
||||
.bio-composer #wmd-quote-post {
|
||||
display: none;
|
||||
}
|
||||
|
@ -67,10 +67,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pagedown-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {width: 100%;}
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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');
|
||||
|
450
test/javascripts/components/d-editor-test.js.es6
Normal file
450
test/javascripts/components/d-editor-test.js.es6
Normal file
@ -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(), '<p>hello <strong>world</strong></p>');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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(), '<p>evil trout</p>');
|
||||
|
||||
andThen(() => this.set('value', 'zogstrip'));
|
||||
andThen(() => assert.equal(this.$('.d-editor-preview').html().trim(), '<p>zogstrip</p>'));
|
||||
}
|
||||
});
|
||||
|
||||
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:');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -5,3 +5,11 @@
|
||||
.modal-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.emoji-modal-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.emoji-modal {
|
||||
position: relative;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user