General Editor UI improvements. (#630)

refs https://github.com/TryGhost/Ghost/issues/8248
refs https://github.com/TryGhost/Ghost/issues/8194
closes https://github.com/TryGhost/Ghost/issues/8192

Miscellaneous editor reliability and usability fixes. 
- Improve the reliability of selection.
- Ensure that the + menu appears even if there is a blank document (which meant the events weren't firing from mobiledoc itself)
- When cards are added they are automatically selected and if possible go straight into edit mode (only works on the markdown card).
- Fixes issues in Safari desktop, Safari mobile, and Firefox.
- Tries to position UI on screen at all times.
- Removes fastclick.
This commit is contained in:
Ryan McCarvill 2017-04-10 21:10:53 +12:00 committed by Kevin Ansfield
parent b683f692d1
commit 00f4cab7b9
27 changed files with 485 additions and 120 deletions

View File

@ -145,8 +145,12 @@ export default Component.extend({
}
},
editorKeyDown(event) {
let editor = this.get('editor');
// if the editor has a menu open then we don't want to capture inputs.
if (this.get('editorMenuIsOpen')) {
return;
}
let editor = this.get('editor');
if (event.keyCode === 38) { // up arrow
let selection = window.getSelection();
if (!selection.rangeCount) {

View File

@ -52,7 +52,7 @@ export default Mixin.create({
apiRoot: ghostPaths().apiRoot,
assetPath: ghostPaths().assetRoot,
editor: null,
title: null,
editorMenuIsOpen: false,
init() {
this._super(...arguments);
window.onbeforeunload = () => {
@ -567,6 +567,12 @@ export default Mixin.create({
setEditor(editor) {
this.set('editor', editor);
},
editorMenuIsOpen() {
this.set('editorMenuIsOpen', true);
},
editorMenuIsClosed() {
this.set('editorMenuIsOpen', false);
}
}
});

View File

@ -21,12 +21,38 @@
resize: none;
font-weight: 200;
letter-spacing: 0.1px;
-webkit-user-select: text;
user-select: text;
line-height: normal;
}
/*.__mobiledoc-editor *,
.__mobiledoc-editor *:before,
.__mobiledoc-editor *:after {
box-sizing:unset;
}*/
.kg-card {
position: relative;
display: block;
display: inline-block; /* even though we hide the cursors there is still a
zero width divider character on either side of this card,
we need them to sit inline around this block otherwise we have
a line at the top and bottom of the card. */
width:100%;
outline:none;
user-select: none;
cursor: pointer;
z-index: 110; /* the title has a z-index of 100, this makes it sit above it. */
}
.kg-card * {
user-select: none;
cursor: pointer;
}
.kg-card .is-editing * {
user-select: auto;
cursor: auto;
}
.kg-card:hover,
@ -40,9 +66,9 @@
position: absolute;
left: 0px;
top: 0px;
margin-top: -56px;
height: 46px;
height: 10px;
width:100%;
overflow: visible;
display: none;
}
.button-group {
@ -54,30 +80,20 @@
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-radius:5px;
margin-top:-56px;
height:46px;
display:flex;
box-shadow: 0 0 0 1px color(var(--darkgrey) l(-10%)), 0 8px 16px rgba(26,39,49,0.16), rgba(255,255,255,0.09) 0 1px 0 0 inset;
}
/* keeps the hover when the cursor is moving from the card to the toolbar */
.button-group:before {
display: block;
content: "";
position: absolute;
bottom:-9px;
width: 100%;
height: 8px;
}
.button-group:after {
display: block;
content: "";
position: absolute;
bottom:-9px;
bottom:11px;
left: 50%;
margin-left: -10px;
width: 0;
height: 0;
border-left: transparent 10px solid;
@ -85,7 +101,9 @@
border-top: color(var(--darkgrey) l(-10%)) 8px solid;
}
.kg-card {
min-height: 100px;
}
.kg-card.selected .kg-card-toolbar {
display: flex;
align-items: stretch;
@ -98,6 +116,7 @@
font-size: 1.3rem;
line-height: 46px;
text-align:center;
justify-content: center;
vertical-align: middle;
font-weight: bold;
padding:0 18px;
@ -105,7 +124,7 @@
.kg-card .kg-card-toolbar button {
display: flex;
/*display: flex;*/
justify-content: center;
align-items: center;
height: 46px;
@ -120,6 +139,8 @@
line-height: 46px;
width:70px;
text-align:center;
justify-content: center;
align-items: center;
vertical-align: middle;
padding:0;
}
@ -129,6 +150,8 @@
height:30px;
width:60px;
text-align:center;
justify-content: center;
align-items: center;
vertical-align: middle;
margin:8px;
@ -155,6 +178,7 @@
outline: none;
border: none;
resize: none;
}
.kg-card-toolbar button:hover {
@ -220,11 +244,7 @@ textarea.ed_code {
border-radius: 3px;
}
/**
* HTML Card
*/
.kg-card-html {
border: #ddd 1px solid;
}
/* markdown card */
.kg-card-markdown textarea {
resize: vertical;
}

View File

@ -6,6 +6,8 @@
display: flex;
align-items: center;
text-align: center;
user-select: none;
cursor: pointer;
color: color(var(--lightgrey) l(-10%));
background: linear-gradient(
color(var(--darkgrey) l(-3%)),
@ -13,6 +15,7 @@
);
border-radius: 5px;
box-shadow: 0 0 0 1px color(var(--darkgrey) l(-10%)), 0 8px 16px rgba(26,39,49,0.16), rgba(255,255,255,0.09) 0 1px 0 0 inset;
z-index:110; /* places it above the title */
}
.gh-toolbar:after {
@ -28,6 +31,21 @@
border-right: transparent 10px solid;
border-top: color(var(--darkgrey) l(-10%)) 8px solid;
}
.gh-toolbar.tick-above:after {
border: none;
}
.gh-toolbar.tick-full-left:after {
left: 25%;
}
.gh-toolbar.tick-half-left:after {
left: 40%;
}
.gh-toolbar.tick-full-right:after {
left: 75%;
}
.gh-toolbar.tick-half-right:after {
left: 60%;
}
.gh-toolbar.is-link {
width: 263px;

View File

@ -88,9 +88,11 @@
/* Layout
/* ---------------------------------------------------------- */
*,
*:before,
*:after {
/*Exclude the editor*/
*:not(.__mobiledoc-editor),
*:not(.__mobiledoc-editor):before,
*:not(.__mobiledoc-editor):after {
box-sizing: border-box;
}

View File

@ -1 +1 @@
<div contenteditable="true" data-placeholder="Your Post Title" class="gh-editor-title" tabindex={{tabindex}}></div>
<div contenteditable="true" data-placeholder="Your Post Title" class="gh-editor-title needsclick" tabindex={{tabindex}}></div>

View File

@ -32,7 +32,7 @@
</button>
</section>
</header>
<div class="gh-editor-container">
<div class="gh-editor-container needsclick">
<div class="gh-editor-inner">
{{gh-editor-title
val=(readonly model.titleScratch)
@ -43,6 +43,7 @@
update=(action (perform updateTitle))
id="gh-editor-title"
koenigEditor=(readonly editor)
editorMenuIsOpen=editorMenuIsOpen
}}
{{#if scheduleCountdown}}
<time datetime="{{post.publishedAtUTC}}" class="gh-notification gh-notification-schedule">
@ -60,6 +61,8 @@
tabindex="2"
containerSelector=".gh-editor-container"
setEditor=(action "setEditor")
menuIsOpen=(action "editorMenuIsOpen")
menuIsClosed=(action "editorMenuIsClosed")
}}
</div>
</div>

View File

@ -3,7 +3,6 @@
"dependencies": {
"devicejs": "0.2.7",
"Faker": "3.1.0",
"fastclick": "1.0.6",
"google-caja": "6005.0.0",
"jquery-file-upload": "9.12.3",
"jquery-ui": "1.11.4",

View File

@ -3,6 +3,7 @@ export default {
label: 'Divider',
icon: '',
genus: 'ember',
launchMode: 'preview',
buttons: {
}
};

View File

@ -3,6 +3,7 @@ export default {
label: 'Embed',
icon: '',
genus: 'ember',
launchMode: 'preview',
buttons: {
edit: true
}

View File

@ -2,5 +2,6 @@ export default {
name: 'card-image',
label: 'Image',
icon: '',
genus: 'ember'
genus: 'ember',
launchMode: 'preview'
};

View File

@ -3,5 +3,6 @@ export default {
label: 'Markdown',
icon: '',
genus: 'ember',
launchMode: 'edit',
buttons: {edit: true}
};

View File

@ -24,9 +24,6 @@ export default Component.extend({
init() {
this._super(...arguments);
let payload = this.get('payload');
this.isEditing = !payload.hasOwnProperty('html');
this.isEditing = true;
},
actions: {
selectCard() {

View File

@ -15,7 +15,7 @@ export const BLANK_DOC = {
atoms: [],
markups: [],
cards: [],
sections: []
sections: [[1, 'p', [[0, [], 0, '']]]]
};
export default Component.extend({
@ -27,7 +27,6 @@ export default Component.extend({
keyDownHandler: [],
init() {
this._super(...arguments);
let mobiledoc = this.get('value') || BLANK_DOC;
let userCards = this.get('cards') || [];
@ -62,6 +61,12 @@ export default Component.extend({
};
this.set('editor', new Mobiledoc.Editor(options));
// we use css media width for most things but need to know if a device is touch
// to place the toolbar. Above the selected content on a mobile browser is the
// cut | copy | paste menu so we need to place our toolbar below.
this.set('isTouch', 'ontouchstart' in document.documentElement);
run.next(() => {
if (this.get('setEditor')) {
this.sendAction('setEditor', this.get('editor'));
@ -96,15 +101,18 @@ export default Component.extend({
if (this._rendered) {
return;
}
let [editorDom] = this.$('.surface');
let editor = this.get('editor');
let $editor = this.$('.surface');
let [domContainer] = $editor.parents(this.get('containerSelector'));
let [editorDom] = $editor;
editorDom.tabindex = this.get('tabindex');
this.set('domContainer', domContainer);
this.domContainer = editorDom.parentNode.parentNode.parentNode.parentNode; // nasty nasty nasty.
this.editor.render(editorDom);
this._rendered = true;
editor.render(editorDom);
this.set('_rendered', true);
window.editor = this.editor;
defaultCommands(this.editor); // initialise the custom text handlers for MD, etc.
window.editor = editor;
defaultCommands(editor); // initialise the custom text handlers for MD, etc.
// shouldFocusEditor is only true when transitioning from new to edit, otherwise it's false or undefined.
// therefore, if it's true it's after the first lot of content is entered and we expect the caret to be at the
// end of the document.
@ -115,10 +123,10 @@ export default Component.extend({
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
this.editor._ensureFocus();
editor._ensureFocus();
}
this.editor.cursorDidChange(() => this.cursorMoved());
editor.cursorDidChange(() => this.cursorMoved());
// listen to keydown events outside of the editor, used to handle keydown events in the cards.
document.onkeydown = (event) => {
@ -158,6 +166,15 @@ export default Component.extend({
}
let range = selection.getRangeAt(0); // get the actual range within the DOM.
let position = range.getBoundingClientRect();
if (position.left === 0 && position.top === 0) {
// in safari if the range is collapsed you can't get it's location.
// this is a bug as it's against the spec.
if (editor.range.section) {
position = editor.range.head.section.renderNode.element.getBoundingClientRect();
} else {
return;
}
}
let windowHeight = window.innerHeight;
if (position.bottom > windowHeight) {
@ -170,6 +187,12 @@ export default Component.extend({
let id = $(editor.range.headSection.renderNode.element).find('.kg-card > div').attr('id');
// let id = card.find('div').attr('id');
window.getSelection().removeAllRanges();
// if the element is first and we create a card with the '/' menu then the cursor moves before
// element is placed in the dom properly. So we figure it out another way.
if (!id) {
id = editor.range.headSection.renderNode.element.children[0].children[0].id;
}
this.send('selectCardHard', id);
} else {
this.send('deselectCard');
@ -190,20 +213,28 @@ export default Component.extend({
// keyboard events.
// used when the content of the card is selected and it is editing.
selectCard(cardId) {
if (!cardId) {
throw new Error('A selection must include a cardId');
}
let card = this.get('emberCards').find((card) => card.id === cardId);
let cardHolder = $(`#${cardId}`).parent('.kg-card');
if (this.get('selectedCard') !== card) {
let selectedCard = this.get('selectedCard');
if (selectedCard && selectedCard !== card) {
this.send('deselectCard');
}
// defer rendering until after the card is placed in the mobiledoc via the wormhole
if (!cardHolder[0]) {
run.schedule('afterRender', this, () => this.send('selectCard', cardId));
return;
}
cardHolder.addClass('selected');
cardHolder.removeClass('selected-hard');
this.set('selectedCard', card);
this.get('keyDownHandler').length = 0;
// cardHolder.focus();
document.onclick = (event) => {
let target = $(event.target);
let parent = target.parents('.kg-card');
if (!target.hasClass('kg-card') && !target.hasClass('kg-card-button') && !target.hasClass('kg-card-button-text') && (!parent.length || parent[0] !== cardHolder[0])) {
if (checkIfClickEventShouldCloseCard($(event.target), cardHolder)) {
this.send('deselectCard');
}
};
@ -212,20 +243,27 @@ export default Component.extend({
// creating blocks under the card and deleting the card.
// used when selecting the card with the keyboard or clicking on the toolbar.
selectCardHard(cardId) {
if (!cardId) {
throw new Error('A selection must include a cardId');
}
let card = this.get('emberCards').find((card) => card.id === cardId);
let cardHolder = $(`#${cardId}`).parents('.kg-card');
if (this.get('selectedCard') !== card) {
let selectedCard = this.get('selectedCard');
if (selectedCard && selectedCard !== card) {
this.send('deselectCard');
}
// defer rendering until after the card is placed in the mobiledoc via the wormhole
if (!cardHolder[0]) {
run.schedule('afterRender', this, () => this.send('selectCardHard', cardId));
return;
}
cardHolder.addClass('selected');
cardHolder.addClass('selected-hard');
this.set('selectedCard', card);
document.onclick = (event) => {
let target = $(event.target);
let parent = target.parents('.kg-card');
if (!target.hasClass('kg-card') && !target.hasClass('kg-card-button') && !target.hasClass('kg-card-button-text') && (!parent.length || parent[0] !== cardHolder[0])) {
if (checkIfClickEventShouldCloseCard($(event.target), cardHolder)) {
this.send('deselectCard');
}
};
@ -239,11 +277,18 @@ export default Component.extend({
editor.post.sections.forEach((section) => {
let currentCard = $(section.renderNode.element);
if (currentCard.find(`#${cardId}`).length) {
if (section.prev) {
if (section.prev && section.prev.isCardSection) {
let prevCard = ($(section.prev.renderNode.element).find('.gh-card-holder').attr('id'));
if (prevCard) {
this.send('selectCardHard', prevCard);
}
} else if (section.prev) {
let range = section.prev.toRange();
range.tail.offset = 0;
editor.selectRange(range);
return;
} else {
$(this.titleQuery).focus();
this.send('deselectCard');
}
}
});
@ -253,11 +298,18 @@ export default Component.extend({
editor.post.sections.forEach((section) => {
let currentCard = $(section.renderNode.element);
if (currentCard.find(`#${cardId}`).length) {
if (section.next) {
if (section.next && section.next.isCardSection) {
let nextCard = ($(section.next.renderNode.element).find('.gh-card-holder').attr('id'));
if (nextCard) {
this.send('selectCardHard', nextCard);
}
} else if (section.next) {
let range = section.next.toRange();
range.tail.offset = 0;
editor.selectRange(range);
return;
} else {
$(this.titleQuery).focus();
this.send('deselectCard');
}
}
});
@ -282,6 +334,7 @@ export default Component.extend({
}
}
this.send('deselectCard');
});
return false;
@ -315,11 +368,32 @@ export default Component.extend({
},
stopEditingCard() {
this.set('editedCard', null);
},
menuIsOpen() {
this.sendAction('menuIsOpen');
},
menuIsClosed() {
this.sendAction('menuIsClosed');
}
}
});
// takes two jquery objects, the target of a click event and a cardholder and sees if the click event should close the card or not
function checkIfClickEventShouldCloseCard(target, cardHolder) {
// see if this element or one of its ancestors is a card.
let card = target.hasClass('kg-card') ? target : target.parents('.kg-card');
let isCardToggle = target.hasClass('kg-card-button') || target.parents('.gh-cardmenu').length || target.parents('.kg-card-toolbar').length;
// if we're selecting a card toggle (menu item) OR we have clicked on a card and the card is the one we expect//
// then we shouldn't close the menu and return false.
if (isCardToggle || (card.length && card[0] === cardHolder[0])) {
return false;
}
return true;
}
// // code for moving the cursor into the correct position of the title: (is buggy)
// // find the cursor position based on a pixel offset of an element.
@ -334,7 +408,7 @@ export default Component.extend({
// if (rect.top === rect.bottom) {
// continue;
// }
// if(rect.left <= horizontal_offset && rect.right >= horizontal_offset) {
// if (rect.left <= horizontal_offset && rect.right >= horizontal_offset) {
// return i + (horizontal_offset >= (rect.left + rect.right) / 2 ? 1 : 0); // if the horizontal_offset is on the left hand side of the
// // character then return `i`, if it's on the right return `i + 1`
// }

View File

@ -5,6 +5,7 @@ import observer from 'ember-metal/observer';
export default Component.extend({
layout,
classNameBindings: ['isEditing'],
editing: observer('editedCard', function () {
let editing = this.get('editedCard') === this.get('card');
if (this.get('isEditing') && !editing) {
@ -14,6 +15,19 @@ export default Component.extend({
}),
init() {
this._super(...arguments);
let card = this.get('card');
if (card.newlyCreated) {
run.schedule('afterRender', this, () => {
if (card.card.launchMode === 'edit') {
this.send('startEdit');
this.send('selectCard');
} else {
this.send('selectCardHard');
}
});
this.set('isNew', true);
card.newlyCreated = false;
}
},
didRender() {
// add the classname to the wrapping card as generated by mobiledoc.
@ -26,9 +40,12 @@ export default Component.extend({
// the mobiledoc generated container.
let mobiledocCard = this.$().parents('.__mobiledoc-card');
mobiledocCard.removeClass('__mobiledoc-card');
mobiledocCard.addClass('kg-card');
if (this.get('isNew')) {
mobiledocCard.hide();
mobiledocCard.fadeIn();
}
mobiledocCard.addClass(name ? `kg-${name}` : '');
mobiledocCard.attr('tabindex', 4);
@ -37,6 +54,7 @@ export default Component.extend({
this.send('selectCardHard');
}
});
}
);
},
@ -58,6 +76,11 @@ export default Component.extend({
deselectCard() {
this.sendAction('deselectCard', this.card.id);
this.send('stopEdit');
if (this.get('isNew')) {
let mobiledocCard = this.$().parents('.kg-card');
mobiledocCard.removeClass('new');
this.set('isNew', false);
}
},
selectCardHard() {
this.sendAction('selectCardHard', this.card.id);

View File

@ -80,6 +80,7 @@ export default Component.extend({
});
editor.cursorDidChange(() => {
if (!editor.range || !editor.range.head.section) {
return;
}
@ -95,6 +96,14 @@ export default Component.extend({
let editorOffset = $editor.offset();
this.set('isButton', true);
// we store the range for the current paragraph as we can lose it later.
this.set('range', {
section: editor.range.head.section,
startOffset: editor.range.head.offset,
endOffset: editor.range.head.offset
});
run.schedule('afterRender', this,
() => {
let button = this.$('.gh-cardmenu-button');
@ -109,16 +118,10 @@ export default Component.extend({
},
actions: {
openMenu: function () { // eslint-disable-line
let button = this.$('.gh-cardmenu-button');
let editor = this.get('editor');
let button = this.$('.gh-cardmenu-button'); // the ⊕ button.
let $editor = $(this.get('containerSelector'));
this.set('isOpen', true);
this.set('range', {
section: editor.range.head.section,
startOffset: editor.range.head.offset,
endOffset: editor.range.head.offset
});
this.set('selected', -1);
this.set('selectedTool', null);
@ -127,14 +130,28 @@ export default Component.extend({
run.schedule('afterRender', this,
() => {
let menu = this.$('.gh-cardmenu');
menu.css('top', button.css('top'));
let top = parseInt(button.css('top').replace('px', ''));
// calculate the parts of the menu that are hidden by the overflow.
let hiddenByOverflow = ($editor.innerHeight() + $editor.scrollTop()) - (menu.height() + top);
if (hiddenByOverflow < 0) {
top = top + hiddenByOverflow - 30;
}
menu.css('top', top);
menu.css('left', button.css('left') + button.width());
this.$('.gh-cardmenu-search-input').focus();
menu.hide().fadeIn('fast', () => {
this.$('.gh-cardmenu-search-input').focus();
});
});
this.sendAction('menuIsOpen');
},
closeMenu: function () { // eslint-disable-line
this.set('isOpen', false);
this.set('isButton', false);
this.$('.gh-cardmenu').fadeOut('fast', () => {
this.set('isOpen', false);
});
this.sendAction('menuIsClosed');
},
closeMenuKeepButton: function () { // eslint-disable-line
this.set('isOpen', false);

View File

@ -202,17 +202,36 @@ export default Component.extend({
let position = range.getBoundingClientRect();
let edOffset = $editor.offset();
if (position.left === 0 && position.top === 0) {
// in safari if the range is collapsed you can't get it's location.
// this is a bug as it's against the spec.
position = editor.range.head.section.renderNode.element.getBoundingClientRect();
}
run.schedule('afterRender', this,
() => {
let menu = this.$('.gh-cardmenu');
menu.css('top', position.top + $editor.scrollTop() - edOffset.top + 20);
menu.css('left', position.left + (position.width / 2) + $editor.scrollLeft() - edOffset.left);
this.$('.gh-cardmenu-search-input').focus();
let top = position.top + $editor.scrollTop() - edOffset.top + 20;
let left = position.left + (position.width / 2) + $editor.scrollLeft() - edOffset.left;
// calculate if parts of the menu that are hidden by the overflow.
let hiddenByOverflowY = ($editor.innerHeight() + $editor.scrollTop()) - (menu.height() + top);
// if the menu is off the bottom of the screen then place it above the cursor
if (hiddenByOverflowY < 0) {
menu.css('margin-top', -(menu.outerHeight() + 20));
}
let hiddenByOverflowX = ($editor.innerWidth() + $editor.scrollLeft()) - (menu.width() + left);
// if the menu is off the bottom of the screen then place it above the cursor
if (hiddenByOverflowX < 0) {
menu.css('margin-left', -(menu.outerWidth() + 20));
}
menu.css('top', top);
menu.css('left', left);
menu.hide().fadeIn('fast');
});
this.sendAction('menuIsOpen');
},
closeMenu: function () { // eslint-disable-line
this.set('isOpen', false);
let editor = this.get('editor');
// this.get('editor').unregisterKeyCommand('slash'); -- waiting for the next release for this
@ -222,6 +241,11 @@ export default Component.extend({
editor._keyCommands.splice(i, 1);
}
}
this.$('.gh-cardmenu').fadeOut('fast', () => {
this.set('isOpen', false);
});
this.sendAction('menuIsClosed');
},
clickedMenu: function () { // eslint-disable-line
// let{section, startOffset, endOffset} = this.get('range');

View File

@ -9,7 +9,7 @@ import Tools from '../options/default-tools';
export default Component.extend({
layout,
classNames: ['gh-toolbar'],
classNameBindings: ['isVisible', 'isLink'],
classNameBindings: ['isVisible', 'isLink', 'tickFullLeft', 'tickHalfLeft', 'tickFullRight', 'tickHalfRight', 'tickAbove'],
isVisible: false,
tools: [],
hasRendered: false,
@ -150,8 +150,10 @@ function updateToolbarToRange(self, $holder, $editor, isMouseDown) {
self.propertyWillChange('toolbar');
self.propertyWillChange('toolbarBlocks');
if (!editor.range.isCollapsed) {
// if we have a selection, then the toolbar appears just below said selection:
// if we have a selection, then the toolbar appears just above said selection:
// unless it's a selection around a single card (firefox bug)
if (!editor.range.isCollapsed
&& !(editor.range.head.section.isCardSection && editor.range.head.section === editor.range.tail.section)) {
let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM.
let position = range.getBoundingClientRect();
@ -160,8 +162,53 @@ function updateToolbarToRange(self, $holder, $editor, isMouseDown) {
self.set('isVisible', true);
run.schedule('afterRender', this,
() => {
$holder.css('top', position.top + $editor.scrollTop() - $holder.height() - 20); // - edOffset.top+10
$holder.css('left', position.left + (position.width / 2) + $editor.scrollLeft() - edOffset.left - ($holder.width() / 2));
let width = $holder.width();
let height = $holder.height();
let top = position.top + $editor.scrollTop() - $holder.height() - 20;
let left = position.left + (position.width / 2) + $editor.scrollLeft() - edOffset.left - (width / 2);
let right = left + width;
let edWidth = $editor[0].scrollWidth;
if (left < 0) {
if (Math.round(left / (width / 4)) === -1) {
self.set('tickFullLeft', true);
self.set('tickHalfLeft', false);
self.set('tickFullRight', false);
self.set('tickHalfRight', false);
} else {
self.set('tickFullLeft', false);
self.set('tickHalfLeft', true);
self.set('tickFullRight', false);
self.set('tickHalfRight', false);
}
left = 0;
} else if (right > edWidth) {
if (Math.round((edWidth - right) / (width / 4)) === -1) {
self.set('tickFullLeft', false);
self.set('tickHalfLeft', false);
self.set('tickFullRight', true);
self.set('tickHalfRight', false);
} else {
self.set('tickFullLeft', false);
self.set('tickHalfLeft', false);
self.set('tickFullRight', false);
self.set('tickHalfRight', true);
}
left = left + (edWidth - right);
} else {
self.set('tickFullLeft', false);
self.set('tickHalfLeft', false);
self.set('tickFullRight', false);
self.set('tickHalfRight', false);
}
if (self.get('isTouch') || top - $editor.scrollTop() < 0) {
top = top + height + 60;
self.set('tickAbove', true);
} else {
self.set('tickAbove', false);
}
$holder.css('top', top);
$holder.css('left', left);
}
);

View File

@ -34,6 +34,7 @@ export default function createCardFactory(toolbar) {
let card = setupEmberCard({env, options, payload}, 'render');
let div = document.createElement('div');
div.id = card.id;
div.className = 'gh-card-holder';
return div;
}
return cardObject.willRender({env, options, payload});
@ -70,7 +71,9 @@ export default function createCardFactory(toolbar) {
if (payload.newlyCreated) {
newlyCreated = true;
delete payload.newlyCreated;
env.save(payload, false);
}
let card = EmberObject.create({
id,
env,

View File

@ -200,7 +200,7 @@ export default function (editor, toolbar) {
onClick: (editor) => {
editor.run((postEditor, section) => {
let thisSection = section || editor.range.headSection;
let card = postEditor.builder.createCardSection('card-image', {pos: 'top'});
let card = postEditor.builder.createCardSection('card-image', {pos: 'top', newlyCreated: true});
if (thisSection.text.length) {
postEditor.insertSection(card);
} else {
@ -229,7 +229,7 @@ export default function (editor, toolbar) {
onClick: (editor, section) => {
editor.run((postEditor) => {
let thisSection = section || editor.range.headSection;
let card = postEditor.builder.createCardSection('card-html', {pos: 'top', html: thisSection.text});
let card = postEditor.builder.createCardSection('card-html', {pos: 'top', html: thisSection.text, newlyCreated: true});
// we can't replace a list item so we insert a card after it and then delete it.
if (thisSection.isListItem) {
editor.insertCard('card-html');
@ -259,8 +259,12 @@ export default function (editor, toolbar) {
onClick: (editor, section) => {
editor.run((postEditor) => {
let thisSection = section || editor.range.headSection;
let card = postEditor.builder.createCardSection('card-hr', {pos: 'top'});
postEditor.insertSection(card);
let card = postEditor.builder.createCardSection('card-hr', {pos: 'top', newlyCreated: true});
if (thisSection.text.length) {
postEditor.insertSection(card);
} else {
postEditor.replaceSection(thisSection, card);
}
if (!thisSection.next) {
let newSection = editor.builder.createMarkupSection('p');

View File

@ -1,14 +1,43 @@
{{#each emberCards as |card index|}}
{{#ember-wormhole to=card.id}}
{{koenig-card tabindex=index card=card apiRoot=apiRoot assetPath=assetPath selectCard=(action "selectCard") selectCardHard=(action "selectCardHard") deselectCard=(action "deselectCard") edit=(action "editCard") stopEdit=(action "stopEditingCard") editedCard=editedCard}}
{{koenig-card
tabindex=index
card=card
apiRoot=apiRoot
assetPath=assetPath
selectCard=(action "selectCard")
selectCardHard=(action "selectCardHard")
deselectCard=(action "deselectCard")
edit=(action "editCard")
stopEdit=(action "stopEditingCard")
editedCard=editedCard}}
{{/ember-wormhole}}
{{/each}}
<div class='gh-koenig'>
<div class='surface' tabindex="{{tabindex}}"/>
<div class='surface needsclick' tabindex="{{tabindex}}"/>
</div>
{{yield}}
{{koenig-toolbar editor=editor assetPath=assetPath containerSelector=containerSelector}}
{{koenig-slash-menu editor=editor assetPath=assetPath containerSelector=containerSelector}}
{{koenig-plus-menu editor=editor assetPath=assetPath containerSelector=containerSelector}}
{{koenig-toolbar
editor=editor
assetPath=assetPath
containerSelector=containerSelector
isTouch=isTouch
}}
{{koenig-slash-menu
editor=editor
assetPath=assetPath
containerSelector=containerSelector
isTouch=isTouch
menuIsOpen=(action "menuIsOpen")
menuIsClosed=(action "menuIsClosed")
}}
{{koenig-plus-menu
editor=editor
assetPath=assetPath
containerSelector=containerSelector
isTouch=isTouch
menuIsOpen=(action "menuIsOpen")
menuIsClosed=(action "menuIsClosed")
}}

View File

@ -19,12 +19,12 @@
{{!--<button {{action "stopEdit"}} class='kg-card-button-text'>
Cancel
</button>--}}
<button {{action "stopEdit"}} class='kg-card-button-save'>
<button {{action "stopEdit"}} class='kg-card-button kg-card-button-save'>
Done
</button>
{{else}}
{{#if card.card.buttons.edit}}
<button {{action "startEdit"}} class='kg-card-button-text'>
<button {{action "startEdit"}} class='kg-card-button kg-card-button-text'>
Edit
</button>
{{/if}}

View File

@ -49,7 +49,6 @@
"ember-cli-code-coverage": "0.3.11",
"ember-cli-dependency-checker": "1.3.0",
"ember-cli-eslint": "3.0.3",
"ember-cli-fastclick": "1.3.0",
"ember-cli-htmlbars": "1.2.0",
"ember-cli-htmlbars-inline-precompile": "0.3.6",
"ember-cli-inject-live-reload": "1.6.1",

View File

@ -59,6 +59,17 @@ export function testInput(input, output, expect) {
});
}
export function testInputTimeout(input) {
window.editor.element.focus();
return Ember.Test.promise(function (resolve, reject) { // eslint-disable-line
window.setTimeout(() => {
resolve(window.editor.element.innerHTML);
}, 300);
inputText(window.editor, input);
});
}
export function waitForRender(selector) {
let isRejected = false;
return Ember.Test.promise(function (resolve, reject) { // eslint-disable-line
@ -78,3 +89,11 @@ export function waitForRender(selector) {
checkIsRendered();
});
}
export function timeoutPromise(timeout) {
return Ember.Test.promise(function (resolve) { // eslint-disable-line
window.setTimeout(() => {
resolve();
}, timeout);
});
}

View File

@ -3,19 +3,29 @@ import {expect} from 'chai';
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import {editorRendered, testInput} from '../../helpers/editor-helpers';
import {editorRendered, testInput, testInputTimeout} from '../../helpers/editor-helpers';
describe('Integration: Component: gh-koenig', function () {
setupComponentTest('gh-koenig', {
integration: true
});
beforeEach(function () {
this.set('value', {
version: '0.3.1',
atoms: [],
markups: [],
cards: [],
sections: []});
});
describe('Makerable markdown support.', function() {
it('plain text inputs (placebo)', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -35,6 +45,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -53,6 +64,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -71,6 +83,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -89,6 +102,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -108,6 +122,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -126,6 +141,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -144,6 +160,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -162,6 +179,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -181,6 +199,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -198,6 +217,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -217,6 +237,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -235,6 +256,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -256,6 +278,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -274,6 +297,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -292,6 +316,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -311,6 +336,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -329,6 +355,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -347,6 +374,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -366,6 +394,7 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -380,21 +409,23 @@ describe('Integration: Component: gh-koenig', function () {
});
});
describe.skip('Card markdown support.', function () {
describe('Card markdown support.', function () {
it('![]() creates an image card', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('![image of something](https://unsplash.it/200/300/?random) ', '...', expect);
return testInputTimeout('![image of something](https://unsplash.it/200/300/?random)');
})
.then(() => {
.then((value) => {
expect(value).to.have.string('kg-card-image');
done();
});
});
@ -403,15 +434,17 @@ describe('Integration: Component: gh-koenig', function () {
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('```some code``` ', '...', expect);
return testInputTimeout('```some code```');
})
.then(() => {
.then((value) => {
expect(value).to.have.string('kg-card-markdown');
done();
});
});

View File

@ -3,19 +3,28 @@ import {expect} from 'chai';
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import {editorRendered, testInput, waitForRender, inputText} from '../../helpers/editor-helpers';
import {editorRendered, waitForRender, inputText, timeoutPromise} from '../../helpers/editor-helpers';
import $ from 'jquery';
describe('Integration: Component: gh-koenig-slashmenu', function () {
setupComponentTest('gh-koenig', {
integration: true
});
beforeEach(function () {
this.set('value', {
version: '0.3.1',
atoms: [],
markups: [],
cards: [],
sections: []});
});
it('the slash menu appears on user input', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -31,11 +40,13 @@ describe('Integration: Component: gh-koenig-slashmenu', function () {
done();
});
});
it.skip('searches when a user types', function (done) {
it('searches when a user types', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
@ -46,14 +57,56 @@ describe('Integration: Component: gh-koenig-slashmenu', function () {
return waitForRender('.gh-cardmenu');
})
.then(() => {
let {editor} = window;
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(7);
return testInput(' lis', '/ lis', expect);
inputText(editor, ' bul');
return timeoutPromise(500);
})
.then(() => {
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(2);
expect(cardMenu.children().length).to.equal(1);
done();
});
});
it.skip('ul tool', function (done) {
this.set('editorMenuIsOpen', function () {});
this.set('editorMenuIsClosed', function () {});
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
menuIsOpen=editorMenuIsOpen
menuIsClosed=editorMenuIsClosed
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
inputText(editor, '/');
return waitForRender('.gh-cardmenu');
})
.then(() => {
let {editor} = window;
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(7);
inputText(editor, ' bul');
return timeoutPromise(500);
})
.then(() => {
$('.gh-cardmenu-card').click();
done();
});
// .then(() => {
//
// })
// .then(() => {
// console.log(editor.element.innerHTML);
// done();
// });
});
});

View File

@ -2330,15 +2330,6 @@ ember-cli-eslint@3.0.3:
rsvp "^3.2.1"
walk-sync "^0.3.0"
ember-cli-fastclick@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/ember-cli-fastclick/-/ember-cli-fastclick-1.3.0.tgz#1ff21c95ca9faebd5c1c0698ff652e7bdf8f5a83"
dependencies:
broccoli-funnel "^1.0.1"
broccoli-merge-trees "^1.1.1"
ember-cli-babel "^5.1.6"
fastclick "^1.0.6"
ember-cli-get-component-path-option@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ember-cli-get-component-path-option/-/ember-cli-get-component-path-option-1.0.0.tgz#0d7b595559e2f9050abed804f1d8eff1b08bc771"
@ -2437,7 +2428,7 @@ ember-cli-mocha@0.13.2:
mocha "^2.5.3"
resolve "^1.1.7"
ember-cli-moment-shim@^3.1.0:
ember-cli-moment-shim@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/ember-cli-moment-shim/-/ember-cli-moment-shim-3.1.0.tgz#ec6a39a0dcb4badeaf6dcb74a6c05b0bc576f721"
dependencies:
@ -3542,10 +3533,6 @@ fast-sourcemap-concat@^1.0.1:
source-map "^0.4.2"
source-map-url "^0.3.0"
fastclick@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/fastclick/-/fastclick-1.0.6.tgz#161625b27b1a5806405936bda9a2c1926d06be6a"
faye-websocket@~0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"