feat(composer-emoji): Add emoji button to composer

Summary: The emoji picker allows users to insert emoji to their messages on click. A few emoji that don't render properly in Chromium are replaced with PNGs, both in the composer view and the message list view, but only the Unicode characters are sent in the message bodies.

Test Plan: TODO

Reviewers: bengotow, evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D2787
This commit is contained in:
Jackie Luo 2016-03-24 10:42:44 -07:00
parent ac70ed3064
commit d0b001012e
129 changed files with 2016 additions and 48 deletions

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,8 @@
import Reflux from 'reflux';
EmojiActions = Reflux.createActions([
"selectEmoji"
"selectEmoji",
"useEmoji"
]);
for (key in EmojiActions) {

View file

@ -0,0 +1,324 @@
import React from 'react';
import {Actions} from 'nylas-exports';
import {RetinaImg} from 'nylas-component-kit';
import EmojiStore from './emoji-store';
import EmojiActions from './emoji-actions';
import emoji from 'node-emoji';
import categorizedEmojiList from './categorized-emoji';
import missingEmojiList from './missing-emoji';
class EmojiButtonPopover extends React.Component {
static displayName = 'EmojiButtonPopover';
constructor() {
super();
const {categoryNames,
categorizedEmoji,
categoryPositions} = this.getStateFromStore();
this.state = {
emojiName: "Emoji Picker",
categoryNames: categoryNames,
categorizedEmoji: categorizedEmoji,
categoryPositions: categoryPositions,
searchValue: "",
activeTab: Object.keys(categorizedEmoji)[0],
};
}
componentDidMount() {
this._mounted = true;
this.renderCanvas();
}
componentWillUnmount() {
this._mounted = false;
}
onMouseDown = (event) => {
const emojiName = this.calcEmojiByPosition(this.calcPosition(event));
if (!emojiName) return null;
EmojiActions.selectEmoji({emojiName, replaceSelection: false});
Actions.closePopover();
}
onScroll = () => {
const emojiContainer = document.querySelector(".emoji-finder-container");
const tabContainer = document.querySelector(".emoji-tabs");
tabContainer.className = emojiContainer.scrollTop ? "emoji-tabs shadow" : "emoji-tabs";
if (emojiContainer.scrollTop === 0) {
this.setState({activeTab: Object.keys(this.state.categorizedEmoji)[0]});
} else {
for (const category in this.state.categoryPositions) {
if (this.state.categoryPositions.hasOwnProperty(category)) {
if (emojiContainer.scrollTop >= this.state.categoryPositions[category].top &&
emojiContainer.scrollTop <= this.state.categoryPositions[category].bottom) {
if (category === 'More People') {
this.setState({activeTab: 'People'});
} else {
this.setState({activeTab: category});
}
}
}
}
}
}
onHover = (event) => {
const emojiName = this.calcEmojiByPosition(this.calcPosition(event));
if (emojiName) {
this.setState({emojiName: emojiName});
} else {
this.setState({emojiName: "Emoji Picker"});
}
}
onMouseOut = () => {
this.setState({emojiName: "Emoji Picker"});
}
onChange = (event) => {
const searchValue = event.target.value;
if (searchValue.length > 0) {
const searchMatches = this.findSearchMatches(searchValue);
this.setState({
categorizedEmoji: {
'Search Results': searchMatches,
},
categoryPositions: {
'Search Results': {
top: 25,
bottom: 25 + Math.ceil(searchMatches.length / 8) * 24,
},
},
searchValue: searchValue,
activeTab: null,
}, this.renderCanvas);
} else {
this.setState(this.getStateFromStore, () => {
this.setState({
searchValue: searchValue,
activeTab: Object.keys(this.state.categorizedEmoji)[0],
}, this.renderCanvas);
});
}
}
getStateFromStore = () => {
let categorizedEmoji = categorizedEmojiList;
const categoryPositions = {};
let categoryNames = [
'People',
'More People',
'Nature',
'Food and Drink',
'Activity',
'Travel and Places',
'Objects',
'Symbols',
'Flags',
];
const frequentlyUsedEmoji = EmojiStore.frequentlyUsedEmoji();
if (frequentlyUsedEmoji.length > 0) {
categorizedEmoji = {'Frequently Used': frequentlyUsedEmoji};
for (const category in categorizedEmojiList) {
if (categorizedEmojiList.hasOwnProperty(category)) {
categorizedEmoji[category] = categorizedEmojiList[category];
}
}
categoryNames = ["Frequently Used"].concat(categoryNames);
}
for (const name of categoryNames) {
categoryPositions[name] = {top: 0, bottom: 0};
}
let verticalPos = 25;
for (const category in categoryPositions) {
if (categoryPositions.hasOwnProperty(category)) {
const height = Math.ceil(categorizedEmoji[category].length / 8) * 24;
categoryPositions[category].top = verticalPos;
verticalPos += height;
categoryPositions[category].bottom = verticalPos;
if (category !== 'People') {
verticalPos += 24;
}
}
}
return {
categoryNames: categoryNames,
categorizedEmoji: categorizedEmoji,
categoryPositions: categoryPositions,
};
}
scrollToCategory(category) {
const container = document.querySelector(".emoji-finder-container");
if (this.state.searchValue.length > 0) {
this.setState({searchValue: ""});
this.setState(this.getStateFromStore, () => {
this.renderCanvas();
container.scrollTop = this.state.categoryPositions[category].top + 16;
});
} else {
container.scrollTop = this.state.categoryPositions[category].top + 16;
}
this.setState({activeTab: category})
}
findSearchMatches(searchValue) {
const searchMatches = [];
for (const category of Object.keys(categorizedEmojiList)) {
categorizedEmojiList[category].forEach((emojiName) => {
if (emojiName.indexOf(searchValue) !== -1) {
searchMatches.push(emojiName);
}
});
}
return searchMatches;
}
calcPosition = (event) => {
const rect = event.target.getBoundingClientRect();
const position = {
x: event.pageX - rect.left / 2,
y: event.pageY - rect.top / 2,
};
return position;
}
calcEmojiByPosition = (position) => {
for (const category in this.state.categoryPositions) {
if (this.state.categoryPositions.hasOwnProperty(category)) {
const LEFT_PADDING = 8;
const RIGHT_PADDING = 204;
const EMOJI_WIDTH = 24.5;
const EMOJI_HEIGHT = 24;
const EMOJI_PER_ROW = 8;
if (position.x >= LEFT_PADDING &&
position.x <= RIGHT_PADDING &&
position.y >= this.state.categoryPositions[category].top &&
position.y <= this.state.categoryPositions[category].bottom) {
const x = Math.round((position.x + 5) / EMOJI_WIDTH);
const y = Math.round((position.y - this.state.categoryPositions[category].top + 10) / EMOJI_HEIGHT);
const index = x + (y - 1) * EMOJI_PER_ROW - 1;
return this.state.categorizedEmoji[category][index];
}
}
}
return null;
}
renderTabs() {
const tabs = [];
this.state.categoryNames.forEach((category) => {
if (category !== 'More People') {
let className = `emoji-tab ${(category.replace(/ /g, '-')).toLowerCase()}`
if (category === this.state.activeTab) {
className += " active";
}
tabs.push(
<div key={`${category} container`} style={{flex: 1}}>
<RetinaImg
key={`${category} tab`}
className={className}
name={`icon-emojipicker-${(category.replace(/ /g, '-')).toLowerCase()}.png`}
mode={RetinaImg.Mode.ContentIsMask}
onMouseDown={() => this.scrollToCategory(category)} />
</div>
);
}
});
return tabs;
}
renderCanvas() {
const canvas = document.getElementById("emoji-canvas");
const keys = Object.keys(this.state.categoryPositions);
canvas.height = this.state.categoryPositions[keys[keys.length - 1]].bottom * 2;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
const position = {
x: 15,
y: 45,
}
Object.keys(this.state.categorizedEmoji).forEach((category, i) => {
if (i > 0) {
setTimeout(() => this.renderCategory(category, i, ctx, position), i * 50);
} else {
this.renderCategory(category, i, ctx, position);
}
});
}
renderCategory(category, i, ctx, position) {
if (!this._mounted) return;
if (category !== "More People") {
if (i > 0) {
position.x = 18;
position.y += 48;
}
ctx.font = "24px Nylas-Pro";
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillText(category, position.x, position.y);
}
position.x = 18;
position.y += 48;
ctx.font = "32px Arial";
ctx.fillStyle = 'black';
this.state.categorizedEmoji[category].forEach((emojiName, j) => {
if (missingEmojiList.indexOf(emojiName) === -1) {
const emojiChar = emoji.get(emojiName);
ctx.fillText(emojiChar, position.x, position.y);
} else {
const img = new Image();
img.src = `images/composer-emoji/missing-emoji/${emojiName}.png`;
const x = position.x;
const y = position.y;
img.onload = () => {
ctx.drawImage(img, x, y - 30, 32, 32);
}
}
if (position.x > 325 && j < this.state.categorizedEmoji[category].length - 1) {
position.x = 18;
position.y += 48;
} else {
position.x += 50;
}
})
}
render() {
return (
<div className="emoji-button-popover" tabIndex="-1">
<div className="emoji-tabs">
{this.renderTabs()}
</div>
<div
className="emoji-finder-container"
onScroll={this.onScroll}>
<div className="emoji-search-container">
<input
type="text"
className="search"
value={this.state.searchValue}
onChange={this.onChange}
/>
</div>
<canvas
id="emoji-canvas"
width="420"
height="2000"
onMouseDown={this.onMouseDown}
onMouseOut={this.onMouseOut}
onMouseMove={this.onHover}
style={{zoom: "0.5"}}>
</canvas>
</div>
<div className="emoji-name">
{this.state.emojiName}
</div>
</div>
);
}
}
export default EmojiButtonPopover;

View file

@ -0,0 +1,36 @@
import React from 'react';
import {Actions} from 'nylas-exports';
import {RetinaImg} from 'nylas-component-kit';
import EmojiButtonPopover from './emoji-button-popover';
class EmojiButton extends React.Component {
static displayName = 'EmojiButton';
constructor() {
super();
}
onClick = ()=> {
const buttonRect = React.findDOMNode(this).getBoundingClientRect();
Actions.openPopover(
<EmojiButtonPopover />,
{originRect: buttonRect, direction: 'up'}
)
}
render() {
return (
<button className="btn btn-toolbar" title="Insert emoji…" onClick={this.onClick}>
<RetinaImg name="icon-composer-emoji.png" mode={RetinaImg.Mode.ContentIsMask}/>
</button>
);
}
}
EmojiButton.containerStyles = {
order: 2,
};
export default EmojiButton;

View file

@ -1,9 +1,13 @@
import {DOMUtils, ContenteditableExtension} from 'nylas-exports'
import EmojiActions from './emoji-actions'
import EmojiPicker from './emoji-picker'
const emoji = require('node-emoji');
import {DOMUtils, ComposerExtension} from 'nylas-exports';
import EmojiActions from './emoji-actions';
import EmojiPicker from './emoji-picker';
import emoji from 'node-emoji';
import missingEmojiList from './missing-emoji';
class EmojiComposerExtension extends ContenteditableExtension {
class EmojiComposerExtension extends ComposerExtension {
static selState = null;
static onContentChanged = ({editor}) => {
const sel = editor.currentSelection()
@ -43,6 +47,17 @@ class EmojiComposerExtension extends ContenteditableExtension {
}
};
static onBlur = ({editor}) => {
EmojiComposerExtension.selState = editor.currentSelection().exportSelection();
};
static onFocus = ({editor}) => {
if (EmojiComposerExtension.selState) {
editor.select(EmojiComposerExtension.selState);
EmojiComposerExtension.selState = null;
}
};
static toolbarComponentConfig = ({toolbarState}) => {
const sel = toolbarState.selectionSnapshot;
if (sel) {
@ -104,12 +119,34 @@ class EmojiComposerExtension extends ContenteditableExtension {
if (!emojiNameNode) return null;
let selectedEmoji = emojiNameNode.getAttribute("selectedEmoji");
if (!selectedEmoji) selectedEmoji = emojiOptions[0];
EmojiComposerExtension._onSelectEmoji({editor: editor,
actionArg: {emojiChar: emoji.get(selectedEmoji)}});
const args = {
editor: editor,
actionArg: {
emojiName: selectedEmoji,
replaceSelection: true,
},
};
EmojiComposerExtension._onSelectEmoji(args);
}
}
};
static applyTransformsToDraft = ({draft}) => {
const nextDraft = draft.clone();
nextDraft.body = nextDraft.body.replace(/<span class="missing-emoji ([a-zA-Z0-9-_]*)">.*<\/span>/g, (match, emojiName) =>
`<span class="broken-emoji ${emojiName}">${emoji.get(emojiName)}</span>`
);
return nextDraft;
}
static unapplyTransformsToDraft = ({draft}) => {
const nextDraft = draft.clone();
nextDraft.body = nextDraft.body.replace(/<span class="broken-emoji ([a-zA-Z0-9-_]*)">.*<\/span>/g, (match, emojiName) =>
`<span class="missing-emoji ${emojiName}"><img src="images/composer-emoji/missing-emoji/${emojiName}.png" width="14" height="14" style="margin-top: -5px;" /></span>`
);
return nextDraft;
}
static _findEmojiOptions(sel) {
if (sel.anchorNode &&
sel.anchorNode.nodeValue &&
@ -138,33 +175,47 @@ class EmojiComposerExtension extends ContenteditableExtension {
}
static _onSelectEmoji = ({editor, actionArg}) => {
const emojiChar = actionArg.emojiChar;
if (!emojiChar) return null;
const sel = editor.currentSelection()
if (sel.anchorNode &&
sel.anchorNode.nodeValue &&
sel.anchorNode.nodeValue.length > 0 &&
sel.isCollapsed) {
const words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset);
let index = words.lastIndexOf(":");
let lastWord = words.substring(index + 1, sel.anchorOffset);
if (index !== -1 && words.lastIndexOf(" ") < index) {
editor.select(sel.anchorNode,
sel.anchorOffset - lastWord.length - 1,
sel.focusNode,
sel.focusOffset);
} else {
const {text, textNode} = EmojiComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset);
index = text.lastIndexOf(":");
lastWord = text.substring(index + 1);
const offset = textNode.nodeValue.lastIndexOf(":");
editor.select(textNode,
offset,
sel.focusNode,
sel.focusOffset);
const {emojiName, replaceSelection} = actionArg;
if (!emojiName) return null;
if (replaceSelection) {
const sel = editor.currentSelection();
if (sel.anchorNode &&
sel.anchorNode.nodeValue &&
sel.anchorNode.nodeValue.length > 0 &&
sel.isCollapsed) {
const words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset);
let index = words.lastIndexOf(":");
let lastWord = words.substring(index + 1, sel.anchorOffset);
if (index !== -1 && words.lastIndexOf(" ") < index) {
editor.select(sel.anchorNode,
sel.anchorOffset - lastWord.length - 1,
sel.focusNode,
sel.focusOffset);
} else {
const {text, textNode} = EmojiComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset);
index = text.lastIndexOf(":");
lastWord = text.substring(index + 1);
const offset = textNode.nodeValue.lastIndexOf(":");
editor.select(textNode,
offset,
sel.focusNode,
sel.focusOffset);
editor.delete();
}
}
}
const emojiChar = emoji.get(emojiName);
if (missingEmojiList.indexOf(emojiName) !== -1) {
const html = `<span class="missing-emoji ${emojiName}"><img
src="images/composer-emoji/missing-emoji/${emojiName}.png"
width="14"
height="14"
style="margin-top: -5px;" /></span>`;
editor.insertHTML(html, {selectInsertion: false});
} else {
editor.insertText(emojiChar);
}
EmojiActions.useEmoji({emojiName: emojiName, emojiChar: emojiChar});
};
static _emojiPickerWidth(emojiOptions) {
@ -206,10 +257,10 @@ class EmojiComposerExtension extends ContenteditableExtension {
static _findMatches(word) {
const emojiOptions = []
const emojiChars = Object.keys(emoji.emoji).sort();
for (const emojiChar of emojiChars) {
if (word === emojiChar.substring(0, word.length)) {
emojiOptions.push(emojiChar);
const emojiNames = Object.keys(emoji.emoji).sort();
for (const emojiName of emojiNames) {
if (word === emojiName.substring(0, word.length)) {
emojiOptions.push(emojiName);
}
}
return emojiOptions;

View file

@ -0,0 +1,12 @@
import {MessageViewExtension} from 'nylas-exports';
class EmojiMessageExtension extends MessageViewExtension {
static formatMessageBody({message}) {
message.body = message.body.replace(/<span class="broken-emoji ([a-zA-Z0-9-_]*)">.*<\/span>/g, (match, emojiName) =>
`<span class="missing-emoji ${emojiName}"><img src="images/composer-emoji/missing-emoji/${emojiName}.png" width="14" height="14" style="margin-top: -5px;" /></span>`
);
}
}
export default EmojiMessageExtension;

View file

@ -1,6 +1,8 @@
import {React} from 'nylas-exports'
import EmojiActions from './emoji-actions'
const emoji = require('node-emoji');
import {React} from 'nylas-exports';
import EmojiActions from './emoji-actions';
import emoji from 'node-emoji';
import missingEmojiList from './missing-emoji';
class EmojiPicker extends React.Component {
static displayName = "EmojiPicker";
@ -21,8 +23,8 @@ class EmojiPicker extends React.Component {
}
}
onMouseDown(emojiChar) {
EmojiActions.selectEmoji({emojiChar});
onMouseDown(emojiName) {
EmojiActions.selectEmoji({emojiName, replaceSelection: true});
}
render() {
@ -31,11 +33,25 @@ class EmojiPicker extends React.Component {
if (emojiIndex === -1) emojiIndex = 0;
if (this.props.emojiOptions) {
this.props.emojiOptions.forEach((emojiOption, i) => {
const emojiChar = emoji.get(emojiOption);
const emojiClass = emojiIndex === i ? "btn btn-icon emoji-option" : "btn btn-icon";
emojiButtons.push(<button key={emojiChar} onMouseDown={() => this.onMouseDown(emojiChar)} className={emojiClass}>{emojiChar} :{emojiOption}:</button>);
emojiButtons.push(<br key={emojiChar + " br"} />);
})
let emojiChar = emoji.get(emojiOption);
if (missingEmojiList.indexOf(emojiOption) !== -1) {
emojiChar = (<img
src={`images/composer-emoji/missing-emoji/${emojiOption}.png`}
width="16"
height="16"
style={{marginTop: "-4px", marginRight: "3px"}} />);
}
emojiButtons.push(
<button
key={emojiOption}
onMouseDown={() => this.onMouseDown(emojiOption)}
className={emojiClass}>
{emojiChar} :{emojiOption}:
</button>
);
emojiButtons.push(<br key={emojiOption + " br"} />);
});
}
return (
<div className="emoji-picker">

View file

@ -0,0 +1,69 @@
import NylasStore from 'nylas-store';
import Rx from 'rx-lite';
import _ from 'underscore';
import {DatabaseStore} from 'nylas-exports';
import EmojiActions from './emoji-actions';
const EmojiJSONBlobKey = 'emoji';
class EmojiStore extends NylasStore {
constructor(props) {
super(props);
this._emoji = [];
}
activate = () => {
const query = DatabaseStore.findJSONBlob(EmojiJSONBlobKey);
this._subscription = Rx.Observable.fromQuery(query).subscribe((emoji) => {
this._emoji = emoji ? emoji : [];
this.trigger();
});
this.listenTo(EmojiActions.useEmoji, this._onUseEmoji);
}
frequentlyUsedEmoji = () => {
const sortedEmoji = this._emoji;
sortedEmoji.sort((a, b) => {
if (a.frequency < b.frequency) return 1;
return (b.frequency < a.frequency) ? -1 : 0;
});
const sortedEmojiNames = [];
for (const emoji of sortedEmoji) {
sortedEmojiNames.push(emoji.emojiName);
}
if (sortedEmojiNames.length > 32) {
return sortedEmojiNames.slice(0, 32);
}
return sortedEmojiNames;
}
_onUseEmoji = (emoji) => {
const savedEmoji = _.find(this._emoji, (curEmoji) => {
return curEmoji.emojiChar === emoji.emojiChar;
});
if (savedEmoji) {
for (const key in emoji) {
if (emoji.hasOwnProperty(key)) {
savedEmoji[key] = emoji[key];
}
}
savedEmoji.frequency++;
} else {
_.extend(emoji, {frequency: 1});
this._emoji.push(emoji);
}
this._saveEmoji();
this.trigger();
}
_saveEmoji = () => {
DatabaseStore.inTransaction((t) => {
return t.persistJSONBlob(EmojiJSONBlobKey, this._emoji);
});
}
}
export default new EmojiStore();

View file

@ -1,11 +1,19 @@
/** @babel */
import {ExtensionRegistry} from 'nylas-exports';
import {ExtensionRegistry, ComponentRegistry} from 'nylas-exports';
import EmojiStore from './emoji-store';
import EmojiComposerExtension from './emoji-composer-extension';
import EmojiMessageExtension from './emoji-message-extension';
import EmojiButton from './emoji-button';
export function activate() {
ExtensionRegistry.Composer.register(EmojiComposerExtension);
ExtensionRegistry.MessageView.register(EmojiMessageExtension);
ComponentRegistry.register(EmojiButton, {role: 'Composer:ActionButton'});
EmojiStore.activate();
}
export function deactivate() {
ExtensionRegistry.Composer.unregister(EmojiComposerExtension);
ExtensionRegistry.MessageView.unregister(EmojiMessageExtension);
ComponentRegistry.unregister(EmojiButton);
}

View file

@ -0,0 +1,100 @@
/** @babel */
export default missingEmojiList = [
'relaxed',
'v',
'point_up',
'writing_hand',
'woman-heart-woman',
'man-heart-man',
'woman-kiss-woman',
'man-kiss-man',
'sunny',
'cloud',
'snowflake',
'showman',
'baseball',
'airplane',
'envelope',
'scissors',
'black_nib',
'pencil2',
'heart',
'heavy_heart_exclamation_mark_ornament',
'latin_cross',
'star_of_david',
'yin_yang',
'u7a7a',
'u5272',
'u6709',
'u7121',
'u7533',
'u55b6',
'u6708',
'eight_pointed_black_star',
'accept',
'ideograph_advantage',
'secret',
'congratulations',
'u5408',
'u6e80',
'u7981',
'a',
'b',
'o2',
'hotsprings',
'bangbang',
'interrobang',
'part_alternation_mark',
'warning',
'recycle',
'u6307',
'sparkle',
'eight_spoked_asterisk',
'm',
'sa',
'parking',
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'keycap_star',
'arrow_forward',
'arrow_backward',
'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',
'arrow_right_hook',
'leftwards_arrow_with_hook',
'arrow_heading_up',
'arrow_heading_down',
'hash',
'information_source',
'wavy_dash',
'heavy_check_mark',
'heavy_multiplication_x',
'copyright',
'registered',
'tm',
'ballot_box_with_check',
'black_small_square',
'white_small_square',
'black_medium_square',
'white_medium_square',
'spades',
'clubs',
'hearts',
'diamonds'
];

View file

@ -17,3 +17,41 @@
}
}
}
.emoji-button-popover {
width: 210px;
height: 290px;
overflow: hidden;
.emoji-tabs {
display: flex;
flex-direction: row;
padding: 5px 5px 5px 10px;
border-bottom: 1px solid @border-color-primary;
transition: box-shadow 0.5s;
&.shadow {
box-shadow: @standard-shadow;
}
.emoji-tab {
background-color: @gray-light;
&.active {
background-color: @component-active-color;
}
}
}
.emoji-finder-container {
height: 232px;
overflow: auto;
.emoji-search-container {
padding: @padding-base-vertical * 1.5 @padding-base-horizontal 0;
}
}
.emoji-name {
height: 25px;
width: 192px;
margin-top: 2px;
margin-left: 10px;
overflow: hidden;
text-overflow: ellipsis;
color: @text-color-very-subtle;
}
}

View file

@ -93,13 +93,13 @@ class EditorAPI
insertHTML: (html, {selectInsertion}) ->
if selectInsertion
wrappedHtml = "<span id='tmp-html-insertion-wrap'>#{html}</span>"
wrappedHtml = """<span id="tmp-html-insertion-wrap">#{html}</span>"""
@_ec("insertHTML", false, wrappedHtml)
wrap = @rootNode.querySelector("#tmp-html-insertion-wrap")
@unwrapNodeAndSelectAll(wrap)
return @
else
@_ec("insertHTML", false, wrappedHtml)
@_ec("insertHTML", false, html)
insertImage: (uri) -> @_ec("insertImage", false, uri)
insertOrderedList: -> @_ec("insertOrderedList", false)

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Some files were not shown because too many files have changed in this diff Show more