Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Commit

Permalink
Koenig - (+) card/list selection menu
Browse files Browse the repository at this point in the history
refs TryGhost/Ghost#9311
- re-implement the (+) card/list selection menu from the old Koenig alpha with improved positioning and event handling
- buttons work for the currently available cards - `<hr>` and `markdown`
  • Loading branch information
kevinansfield committed Jan 31, 2018
1 parent bcadfbc commit 584dece
Show file tree
Hide file tree
Showing 27 changed files with 425 additions and 1 deletion.
162 changes: 162 additions & 0 deletions app/styles/components/koenig.css
Expand Up @@ -200,10 +200,172 @@

/* ⨁ menu ------------------------------------------------------------------ */

.koenig-plus-menu {
position: absolute;
}

.koenig-plus-menu-button {
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: 30px;
height: 30px;
border: var(--midgrey) 1px solid;
background: #fff;
border-radius: 100%;
margin-left: -40px;
}

.koenig-plus-menu-button svg {
height: 15px;
width: 15px;
}

.koenig-plus-menu-button svg path {
stroke: var(--midgrey);
stroke-width: 2px;
}

@media (max-width: 1024px) {
.koenig-plus-menu-button {
right:10px;
}
}

/* Slash shortcut menu ------------------------------------------------------ */

/* Menu items --------------------------------------------------------------- */

/* Chrome has a bug with its scrollbars on this element which has been reported here: https://bugs.chromium.org/p/chromium/issues/detail?id=697381 */
.koenig-cardmenu {
position: absolute;
top: 0;
display: flex;
flex-wrap: wrap;
margin: 0 0 20px 0;
padding: 12px 15px;
width: 350px;
max-height: 460px;
overflow-y: auto;
background-color: #fff;
background-clip: padding-box;
border-radius: 4px;
box-shadow: 0 0 0 1px rgba(99,114,130,0.16), 0 8px 16px rgba(27,39,51,0.08);
text-transform: none;
font-size: 1.4rem;
font-weight: normal;
position: absolute;
z-index: 9999999; /* have to compete with codemirror */
}

.koenig-cardmenu-button {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
width: 30px;
height: 30px;
border: var(--midgrey) 1px solid;
background: #fff;
border-radius: 100%;
margin-left: -40px;
}

.koenig-cardmenu-button svg {
height: 15px;
width: 15px;
}

.koenig-cardmenu-button svg path {
stroke: var(--midgrey);
stroke-width: 2px;
}

@media (max-width: 1024px) {
.koenig-cardmenu-button {
right:10px;
}
}

.koenig-cardmenu-search {
position: relative;
width: 350px;
height: 40px;
margin: -12px -15px;
}

.koenig-cardmenu-search svg {
position: absolute;
top: 11px;
left: 10px;
z-index: 100;
width: 20px;
height: 19px;
}

.koenig-cardmenu-search-input {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 40px;
font-size: 1.4rem;
line-height: 40px;
padding: 10px 0 10px 40px;
border: none;
border-radius: 4px 4px 0 0;
}

.koenig-cardmenu-card {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 80px;
height: 80px;
border-radius: 4px;
}

.koenig-cardmenu-icon {
display: flex;
align-items: center;
}
.koenig-cardmenu-icon svg {
width: 27px;
height: 27px;
fill: var(--darkgrey);
}

.koenig-cardmenu-label {
margin: 7px 0 0 0;
font-size: 1.1rem;
color: var(--midgrey);
letter-spacing: 0.2px;
font-weight: 200;
}

.koenig-cardmenu-card:hover, .koenig-cardmenu-card.selected {
cursor: pointer;
background: color(var(--lightgrey) l(+3%) s(-10%));
}
.koenig-cardmenu-card:hover .koenig-cardmenu-label, .koenig-cardmenu-card.selected .koenig-cardmenu-label {
color: var(--darkgrey);
font-weight: 300;
}

.koenig-cardmenu-divider {
top: -12px;
width: 350px;
padding: 5px 0;
margin: 12px -15px;
font-size: 1.2rem;
text-align: center;
background: color(var(--lightgrey) l(+3%) s(-10%));
}


/* Cards -------------------------------------------------------------------- */

textarea.koenig-card-markdown {
Expand Down
168 changes: 168 additions & 0 deletions lib/koenig-editor/addon/components/koenig-plus-menu.js
@@ -0,0 +1,168 @@
import Component from '@ember/component';
import layout from '../templates/components/koenig-plus-menu';
import {computed} from '@ember/object';
import {htmlSafe} from '@ember/string';
import {run} from '@ember/runloop';

// clicking on anything in the menu will change the selection because the click
// event propagates, this then closes the menu

// focusing the search input removes the selection in the editor, again closing
// the menu

// when the menu is open we want to:
// - close if clicked outside the menu
// - keep the selected range around in case it gets changed

export default Component.extend({
layout,

// public attrs
classNames: 'koenig-plus-menu',
attributeBindings: ['style'],
editor: null,
editorRange: null,

// internal properties
showButton: false,
showMenu: false,
top: 0,

style: computed('top', function () {
return htmlSafe(`top: ${this.get('top')}px`);
}),

didReceiveAttrs() {
this._super(...arguments);

if (!this.get('showMenu')) {
let editorRange = this.get('editorRange');

if (!editorRange) {
this.set('showButton', false);
this._hideMenu();
return;
}

let {head: {section}} = editorRange;

// show the button if the cursor is at the beginning of a blank paragraph
if (editorRange && editorRange.isCollapsed && section && !section.isListItem && (section.isBlank || section.text === '')) {
// find the "top" position by grabbing the current sections
// render node and querying it's bounding rect. Setting "top"
// positions the button+menu container element .koenig-plus-menu
let containerRect = this.element.parentNode.getBoundingClientRect();
let selectedElement = editorRange.head.section.renderNode.element;
let selectedElementRect = selectedElement.getBoundingClientRect();
let top = selectedElementRect.top - containerRect.top;

this.set('top', top);
this.set('showButton', true);
this._hideMenu();
} else {
this.set('showButton', false);
this._hideMenu();
}
}
},

willDestroyElement() {
this._super(...arguments);
window.removeEventListener('mousedown', this._bodyMousedownHandler);
},

actions: {
openMenu() {
this._showMenu();
},

closeMenu() {
this._hideMenu();
},

replaceWithCardSection(cardName) {
let editor = this.get('editor');
let range = this._editorRange;
let {head: {section}} = range;

editor.run((postEditor) => {
let {builder} = postEditor;
let card = builder.createCardSection(cardName);
let needsTrailingParagraph = !section.next;

postEditor.replaceSection(section, card);

if (needsTrailingParagraph) {
let newSection = postEditor.builder.createMarkupSection('p');
postEditor.insertSectionAtEnd(newSection);
postEditor.setRange(newSection.tailPosition());
}

this._hideMenu();
});
},

replaceWithListSection(listType) {
let editor = this.get('editor');
let range = this._editorRange;
let {head: {section}} = range;

editor.run((postEditor) => {
let {builder} = postEditor;
let item = builder.createListItem();
let listSection = builder.createListSection(listType, [item]);

postEditor.replaceSection(section, listSection);
postEditor.setRange(listSection.headPosition());
this._hideMenu();
});
}
},

_showMenu() {
this.set('showMenu', true);

// focus the search immediately so that you can filter immediately
run.schedule('afterRender', this, function () {
this._focusSearch();
});

// watch the window for mousedown events so that we can close the menu
// when we detect a click outside
this._bodyMousedownHandler = run.bind(this, (event) => {
this._handleBodyMousedown(event);
});
window.addEventListener('mousedown', this._bodyMousedownHandler);

// store a reference to our range because it will change underneath
// us as editor focus is lost
this._editorRange = this.get('editorRange');
},

_hideMenu() {
if (this.get('showMenu')) {
// reset our cached editorRange
this._editorRange = null;

// stop watching the body for clicks
window.removeEventListener('mousedown', this._bodyMousedownHandler);

// hide the menu
this.set('showMenu', false);
}
},

_focusSearch() {
let search = this.element.querySelector('input');
if (search) {
search.focus();
}
},

_handleBodyMousedown(event) {
if (!event.target.closest(`#${this.elementId}`)) {
this._hideMenu();
}
}

});
2 changes: 1 addition & 1 deletion lib/koenig-editor/addon/components/koenig-toolbar.js
Expand Up @@ -21,7 +21,7 @@ export default Component.extend({
// public attrs
classNames: ['koenig-toolbar'],
classNameBindings: ['showToolbar:koenig-toolbar--visible'],
selectedRange: null,
editorRange: null,

// internal properties
showToolbar: false,
Expand Down
Expand Up @@ -66,6 +66,12 @@
</button>
{{/koenig-toolbar}}

{{!-- (+) icon and pop-up menu --}}
{{koenig-plus-menu
editorRange=selectedRange
editor=editor
}}

{{#each componentCards as |card|}}
{{!--
TODO: move to the public {{in-element}} API when it's available
Expand Down

0 comments on commit 584dece

Please sign in to comment.