1
0
Fork 0
mirror of https://github.com/TryGhost/Ghost-Admin.git synced 2023-12-14 02:33:04 +01:00

Koenig - Remove old Koenig alpha code

refs https://github.com/TryGhost/Ghost/issues/9311
- old code is no longer needed for reference so cleaning up
This commit is contained in:
Kevin Ansfield 2018-02-15 10:46:23 +00:00
parent 6618f228b9
commit ae79640a6a
93 changed files with 11 additions and 5026 deletions

View file

@ -11,8 +11,6 @@ import {inject as service} from '@ember/service';
// should be able to replace the duplicated upload logic in:
// - gh-image-uploader
// - gh-file-uploader
// - gh-koenig/cards/card-image
// - gh-koenig/cards/card-markdown
//
// In order to support the above components we'll need to introduce an
// "allowMultiple" attribute so that single-image uploads don't allow multiple

View file

@ -1,315 +0,0 @@
@import "koenig-toolbar.css";
@import "koenig-menu.css";
@import "koenig-cardmenu.css";
.gh-koenig-container {
height: 100%;
}
.gh-koenig {
height: 100%;
-webkit-overflow-scrolling: touch;
}
/* Title
/* ---------------------------------------------------------- */
.kg-title-input {
flex-grow: 1;
margin-bottom: 2vw;
padding: 0;
margin: 0;
outline: none;
position: relative;
width: 100%;
letter-spacing: 0.8px;
font-weight: bold;
font-size: 3.2rem;
line-height: 1.3em;
min-height: 1.3em;
z-index: 1;
}
/* Place holder content that displays in the title if it is empty */
.kg-title-input.no-content:before {
content: attr(data-placeholder);
color: color(var(--midgrey) l(+35%));
cursor: text;
position: relative;
top: 0;
font-size: 3.2rem;
font-weight: bold;
line-height: 1.3em;
min-width: 30rem; /* hack it's defaulting just to enough width for the 'Your' in 'Your Post Title' */
z-index: -1;
}
.kg-title-input input {
margin: 0;
padding: 0;
width: 100%;
border: 0;
background: transparent;
color: var(--darkgrey);
font-size: 3.2rem;
font-weight: bold;
letter-spacing: 0.8px;
}
.kg-title-input input::-webkit-input-placeholder {
color: color(var(--midgrey) l(+25%));
font-weight: 400;
letter-spacing: 1.2px;
}
.kg-title-input input:-ms-input-placeholder {
color: color(var(--midgrey) l(+25%));
font-weight: 400;
letter-spacing: 1.2px;
}
.kg-title-input input:focus {
outline: 0;
}
/* Editor
/* ---------------------------------------------------------- */
.__mobiledoc-editor {
width: 100%;
height: 100%;
min-height: 300px;
padding-bottom: 10vw;
outline: none;
font-family: var(--font-family);
font-size: 1.7rem;
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: 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,
.kg-card.selected {
box-shadow: var(--blue) 0 0 0 1px;
}
.kg-card.selected-hard {
box-shadow: var(--blue) 0 0 0 3px;
}
.kg-card .kg-card-toolbar {
position: absolute;
left: 0px;
top: 0px;
height: 10px;
width:100%;
overflow: visible;
display: none;
}
.button-group {
color: color(var(--lightgrey) l(-10%));
background: linear-gradient(
color(var(--darkgrey) l(-3%)),
color(var(--darkgrey) l(-8%))
);
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;
}
.button-group:after {
display: block;
content: "";
position: absolute;
bottom:11px;
left: 50%;
margin-left: -10px;
width: 0;
height: 0;
border-left: transparent 10px solid;
border-right: transparent 10px solid;
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;
justify-content: space-around;
flex-flow: row wrap;
}
.kg-card .kg-card-toolbar label {
flex-grow: 1;
font-size: 1.3rem;
line-height: 46px;
text-align:center;
justify-content: center;
vertical-align: middle;
font-weight: bold;
padding:0 18px;
}
.kg-card .kg-card-toolbar button {
/*display: flex;*/
justify-content: center;
align-items: center;
height: 46px;
min-width: 32px;
font-size: 1.6rem;
line-height: 30px;
padding:0 5px 0 5px;
transition: text-shadow 0.3s ease;
}
.kg-card .kg-card-toolbar button.kg-card-button-text {
font-size: 1.3rem;
line-height: 46px;
width:70px;
text-align:center;
justify-content: center;
align-items: center;
vertical-align: middle;
padding:0;
}
.kg-card .kg-card-toolbar button.kg-card-button-save {
font-size: 1.3rem;
line-height: 30px;
height:30px;
width:60px;
text-align:center;
justify-content: center;
align-items: center;
vertical-align: middle;
margin:8px;
color: #fff;
text-shadow: 0 -1px 0 rgba(0,0,0,0.1);
fill: #fff;
background: linear-gradient( rgb(61, 161, 214), rgb(34, 136, 191) );
box-shadow: 0 1px 0 rgba(0,0,0,0.12);
border-radius:5px;
}
.kg-card .kg-card-toolbar button.kg-card-delete {
line-height: 1;
}
.kg-card .kg-card-toolbar button.kg-card-delete svg {
height: 1.4rem;
width: auto;
fill: color(var(--lightgrey) l(-10%));
}
.kg-card textarea {
min-height: 333px;
max-width: 900px;
outline: none;
border: none;
resize: none;
}
.kg-card-toolbar button:hover {
background-color: #718087;
color: #fff;
}
textarea.ed_code {
width: 100%;
height: 100%;
border: none;
}
.dragOver {
background-color: var(--lightgrey);
}
/**
* Tooltip
*/
@keyframes tooltip-fadein {
0% { opacity: 0; }
100% { opacity: 1; }
}
.__mobiledoc-tooltip {
white-space: nowrap;
position: absolute;
background-color: #5ba4e5;
border-radius: 3px;
padding: 5px 10px 5px 10px;
color: #FFF;
-webkit-animation: tooltip-fadein 0.333s;
animation: tooltip-fadein 0.333s;
}
.__mobiledoc-tooltip a {
color: #FFF;
}
.__mobiledoc-tooltip:before {
content: '';
position: absolute;
left: 50%;
top: -9px;
margin-left: -10px;
border-bottom: 10px solid #5ba4e5;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
}
.kg-card .gh-image-uploader .upload-form {
width: 100%;
height: 350px;
background: color(var(--lightgrey) l(+4%));
box-shadow: var(--lightgrey) 0 0 0 1px inset, #fff 0 0 0 15px inset;
border-radius: 3px;
}
/* markdown card */
.kg-card-markdown textarea {
resize: vertical;
}

View file

@ -1,125 +0,0 @@
/* 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 */
.gh-cardmenu {
display: flex;
flex-wrap: wrap;
margin: 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 */
}
.gh-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;
}
.gh-cardmenu-button svg {
height: 15px;
width: 15px;
}
.gh-cardmenu-button svg path {
stroke: var(--midgrey);
stroke-width: 2px;
}
@media (max-width: 1024px) {
.gh-cardmenu-button {
right:10px;
}
}
.gh-cardmenu-search {
position: relative;
width: 350px;
height: 40px;
margin: -12px -15px;
}
.gh-cardmenu-search svg {
position: absolute;
top: 11px;
left: 10px;
z-index: 100;
width: 20px;
height: 19px;
}
.gh-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;
}
.gh-cardmenu-card {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 80px;
height: 80px;
border-radius: 4px;
}
.gh-cardmenu-icon {
display: flex;
align-items: center;
}
.gh-cardmenu-icon svg {
width: 27px;
height: 27px;
fill: var(--darkgrey);
}
.gh-cardmenu-label {
margin: 7px 0 0 0;
font-size: 1.1rem;
color: var(--midgrey);
letter-spacing: 0.2px;
font-weight: 200;
}
.gh-cardmenu-card:hover, .gh-cardmenu-card.selected {
cursor: pointer;
background: color(var(--lightgrey) l(+3%) s(-10%));
}
.gh-cardmenu-card:hover .gh-cardmenu-label, .gh-cardmenu-card.selected .gh-cardmenu-label {
color: var(--darkgrey);
font-weight: 300;
}
.gh-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%));
}

View file

@ -1,57 +0,0 @@
.koenig-menu {
border:1px solid var(--darkgrey);
position:absolute;
background-color: #fff;
background-clip: padding-box;
padding:10px;
box-shadow: rbga(0,0,0,0.10) 0 2px 6px;
min-width:300px;
border-radius: 4px;
display: none;
}
.koenig-menu.is-visible {
animation: koenig-menu-fadein 111ms;
}
.koenig-menu ul {
margin:0;
padding:0;
list-style: none;
}
.koenig-menu ul li button {
padding:5px;
width:100%;
text-align: left;
font-size:1.3rem;
border-radius: 4px;
}
.koenig-menu ul li button img {
width:14px;
}
.koenig-menu ul li button:hover, .koenig-menu ul li button.selected {
background-color: var(--darkgrey);
}
#koenig-menu-button {
position: absolute;
display:none;
width:100px;
height: 100px;
background-color: deeppink;
}
@keyframes koenig-menu-fadein {
from {
opacity: 0;
transform: scale(0.7);
transform-origin:top;
}
to {
opacity: 1;
transform: scale(1);
transform-origin:top;
}
}

View file

@ -1,122 +0,0 @@
.toolbar-block {
position: absolute;
margin: 0;
background-color: #fff;
border:1px var(--lightgrey) solid;
transition: margin-left 0.1s;
}
.toolbar-block ul {
list-style: none;
margin: 0;
padding: 0;
}
.toolbar-block ul li {
margin:0;
padding: 0;
width:0;
opacity:0;
overflow:hidden;
transition: width 0.3s;
float:left;
}
.toolbar-block ul li:first-child {
display:block;
width:32px;
opacity:0.3;
}
.toolbar-block:hover ul li {
display:block;
width:32px;
opacity:0.3;
animation-delay: 2s;
transition: width 0.3s;
}
.toolbar-block ul li.selected {
display:block;
opacity:0.1;
width:32px;
}
.toolbar-block:hover ul li.selected {
opacity:1;
}
.toolbar-block ul li button {
border-radius: 2px;
font-family: var(--font-family);
font-size: 10px;
line-height: 15px;
text-transform: uppercase;
padding:0;
margin:0;
height:32px;
min-width:32px;
color: var(--darkgrey);
background-color: #fff;
border: none;
transition: 0.3s;
}
.toolbar-block ul li.primary button {
opacity:1;
}
.toolbar-block ul li:hover {
opacity:1;
}
.toolbar-block ul li:hover button, .toolbar-block ul li.selected button {
background-color: var(--lightgrey);
color: var(--darkgrey);
transition: 0.3s;
}
.toolbar-block ul li button img {
width:20px;
height:20px;
}
/**flag **/
.toolbar-block ul li button label {
display:none;
}
.toolbar-block ul li:hover button:hover label {
display:block;
position:absolute;
margin-top:20px;
transform:translateX(calc(-50% + 16px));
white-space: nowrap;
background-color: #5ba4e5;
border-radius: 3px;
padding:5px 10px 5px 10px;
color: #FFF;
-webkit-animation: fade-in 0.333s;
animation: fade-in 0.333s;
}
.toolbar-block ul li button:hover label:before {
content: '';
position: absolute;
left: 50%;
top: -9px;
margin-left: -10px;
border-bottom: 10px solid #5ba4e5;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
}

View file

@ -1,156 +0,0 @@
/* Contextual toolbar
/* ---------------------------------------------------------- */
.gh-toolbar {
position: absolute;
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%)),
color(var(--darkgrey) l(-8%))
);
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 {
display: block;
content: "";
position: absolute;
bottom: -8px;
left: 50%;
margin-left: -10px;
width: 0;
height: 0;
border-left: transparent 10px solid;
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;
height: 40px;
}
.gh-toolbar.is-link input {
width: 100%;
background-color: transparent;
outline: none;
border: none;
padding:5px;
}
.gh-toolbar.is-touch {
position: fixed !important;
top: 70px;
right: 40px;
}
.gh-toolbar.is-touch:after {
border: none;
}
.gh-toolbar-btn {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
width: 32px;
font-size: 1.6rem;
line-height: 40px;
transition: text-shadow 0.3s ease;
}
.gh-toolbar-btn:first-child {
width: 43px;
padding-left: 8px;
}
.gh-toolbar-btn:last-child {
width: 43px;
padding-right: 8px;
}
.gh-toolbar-btn svg {
height: 1.4rem;
}
.gh-toolbar-btn svg g {
stroke-width: 2px;
stroke: color(var(--lightgrey) l(-10%));
}
.gh-toolbar-btn:hover {
color: #fff;
cursor: pointer;
text-shadow: #000 0 1px 6px;
}
.gh-toolbar-btn:hover svg g {
stroke: #fff;
}
.gh-toolbar-btn-bold {
font-weight: 700;
}
.gh-toolbar-btn-italic {
width: 31px;
font-size: 1.7rem;
text-indent: -1px;
font-style: italic;
font-family: Georgia, Times, serif;
font-weight: 500;
}
.gh-toolbar-btn-strike {
text-decoration: line-through;
font-weight: 400;
-webkit-font-smoothing: antialiased;
}
.gh-toolbar-btn-h1 {
font-variant: small-caps;
font-weight: 700;
-webkit-font-smoothing: antialiased;
}
.gh-toolbar-btn-h2 {
font-weight: 700;
font-size: 0.9em;
font-variant: small-caps;
line-height: 42px;
-webkit-font-smoothing: antialiased;
}
.gh-toolbar-btn-quote {
font-size: 4rem;
line-height: 62px;
font-family: Georgia, Times, serif;
-webkit-font-smoothing: antialiased;
}
.gh-toolbar-divider {
height: 40px;
width: 1px;
margin: 0 9px 0 8px;
background: color(var(--darkgrey) l(-10%));
box-shadow: rgba(255,255,255,0.04) 1px 0 0 0;
}

View file

@ -1,55 +0,0 @@
b-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
npm-debug.log
.nvmrc
.bowerrc
.idea/*
*.iml
*.sublime-*
projectFilesBackup
.DS_Store
# vim-related
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
.vimrc
*~
# TernJS
.tern-project
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
# dependencies
/node_modules
/bower_components
# misc
/connect.lock
/coverage/*
/libpeerconnection.log
npm-debug.log
testem.log
# built by grunt
public/assets/img/contributors/
app/templates/-contributors.hbs

View file

@ -1,65 +0,0 @@
# gh-koenig
This is the new mobiledoc editor for Ghost-Admin. It's still a work in progress but we're very excited about it.
Here are a few of our goals:
- To make the best ghosh darn writing experience on the interwebs.
- To build a tool that works just as well for non technical content creators as power users.
- To support rich content as easily as dealing with an image. Want a poll mid article? You got it. Want to paste a complete NG application in raw HTML? You got it.
When embarking on this project the last thing we wanted was to use one of those WYSIWYG editors with the million options and incomprehensible and inconsistent markup (you know who I'm talking about), to that end we chose to build our editor on top of [mobiledoc-kit](https://github.com/bustlelabs/mobiledoc-kit), you can read more about our decision [here](https://github.com/TryGhost/Ghost/issues/7429).
## So why mobiledoc?
Mobiledoc is a new format for storing rich content, it's platform independent and isn't tied specifically to HTML (we can render a plain text version for a search index for instance). It allows for the embedding of rich applications inside your content using the cards paradigm. It's also a standard so content written in and for Ghost is compatible with any other mobiledoc system, and vice versa.
To us it seemed like the best compromise between a feature rich WYSIWYG editor and the markdown that we so love.
Like Ghost, Mobiledoc-kit is still moving towards its 1.0 release - it still has some bugs, but we're working together to make something really fun.
## To try it out:
- clone this repo
- `cd gh-koenig`
- `npm install && bower install`
- `ember serve`
- Visit `http://localhost:4200`
- Click in the middle to activate the editor
## If you want to help out:
- Create an issue on the main Ghost repository [https://github.com/TryGhost/Ghost/issues](https://github.com/TryGhost/Ghost/issues).
- Clone the repo and create a branch.
- Submit a PR.
A fantastic guide on the Ghost workflow is here: https://github.com/TryGhost/Ghost/wiki/Git-workflow, it's well worth a read.
## Some features of the editor.
gh-koenig is very much a WYSIWYG editor but it supports a subset of markdown as content shortcuts for those of us who are mouse adverse, specifically:
```text
# H1
## H2
### H3
1. Ordered Lists
* Unordered Lists
- Unordered Lists
> Block Quote
*italic*
_italic_
**bold**
__bold__
~~strikethrough~~
[link](http://www.ghost.org)
![image](https://ghost.org/assets/logos-f93942864f8c9f4a0a9b0ecd6f7f055c.png)
``` code blocks (generates a new markdown card) ```
```
There's also an inline menu that you can access by pressing the **/** key within the editor.
Right now we only have three built in "cards", a markdown card, an HTML card, and an Image card. But we plan to add the ability to install custom cards in the near future and have some big and exciting plans (and we're even more excited about what the community will do with it.), so watch this space.
# Copyright & License
Copyright (c) 2016-2018 Ghost Foundation - Released under the [MIT license](LICENSE).

View file

@ -1,9 +0,0 @@
export default {
name: 'card-hr',
label: 'Divider',
icon: '',
genus: 'ember',
launchMode: 'preview',
buttons: {
}
};

View file

@ -1,10 +0,0 @@
export default {
name: 'card-html',
label: 'Embed',
icon: '',
genus: 'ember',
launchMode: 'edit',
buttons: {
edit: true
}
};

View file

@ -1,7 +0,0 @@
export default {
name: 'card-image',
label: 'Image',
icon: '',
genus: 'ember',
launchMode: 'preview'
};

View file

@ -1,8 +0,0 @@
export default {
name: 'card-markdown',
label: 'Markdown',
icon: '',
genus: 'ember',
launchMode: 'edit',
buttons: {edit: true}
};

View file

@ -1,13 +0,0 @@
import hrCard from 'gh-koenig/cards/card-hr_dom';
import htmlCard from 'gh-koenig/cards/card-html_dom';
import imageCard from 'gh-koenig/cards/card-image_dom';
import markdownCard from 'gh-koenig/cards/card-markdown_dom';
let cards = [];
[htmlCard, imageCard, markdownCard, hrCard].forEach((_card) => {
_card.type = 'dom';
cards.push(_card);
});
export default cards;

View file

@ -1,6 +0,0 @@
import Component from '@ember/component';
import layout from '../../templates/components/card-hr';
export default Component.extend({
layout
});

View file

@ -1,41 +0,0 @@
import Component from '@ember/component';
import counter from 'ghost-admin/utils/word-count';
import layout from '../../templates/components/card-html';
import {computed} from '@ember/object';
import {invokeAction} from 'ember-invoke-action';
import {observer} from '@ember/object';
export default Component.extend({
layout,
hasRendered: false,
value: computed('payload', {
get() {
return this.get('payload').html || '';
},
set(_, value) {
this.get('payload').html = value;
this.get('env').save(this.get('payload'), false);
return this.get('payload').html;
}
}),
// TODO: remove observer
// eslint-disable-next-line ghost/ember/no-observers
save: observer('doSave', function () {
let payload = this.get('payload');
payload.wordcount = counter(payload.html);
this.get('env').save(payload, false);
}),
actions: {
selectCard() {
invokeAction(this, 'selectCard');
}
}
});
// non editor cards need to be vanilla javascript
export let html = {
};

View file

@ -1,265 +0,0 @@
import Component from '@ember/component';
import layout from '../../templates/components/card-image';
import {
UnsupportedMediaTypeError,
isRequestEntityTooLargeError,
isUnsupportedMediaTypeError,
isVersionMismatchError
} from 'ghost-admin/services/ajax';
import {computed} from '@ember/object';
import {htmlSafe} from '@ember/string';
import {invokeAction} from 'ember-invoke-action';
import {isBlank} from '@ember/utils';
import {isArray as isEmberArray} from '@ember/array';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
export default Component.extend({
ajax: service(),
notifications: service(),
layout,
tagName: 'section',
classNames: ['gh-image-uploader'],
classNameBindings: ['dragClass'],
image: null,
text: '',
altText: '',
saveButton: true,
accept: 'image/gif,image/jpg,image/jpeg,image/png,image/svg+xml',
extensions: null,
validate: null,
dragClass: null,
failureMessage: null,
file: null,
url: null,
uploadPercentage: 0,
// TODO: this wouldn't be necessary if the server could accept direct
// file uploads
formData: computed('file', function () {
let file = this.get('file');
let formData = new FormData();
formData.append('file', file);
return formData;
}),
description: computed('text', 'altText', function () {
let altText = this.get('altText');
return this.get('text') || (altText ? `Upload image of "${altText}"` : 'Upload an image');
}),
progressStyle: computed('uploadPercentage', function () {
let percentage = this.get('uploadPercentage');
let width = '';
if (percentage > 0) {
width = `${percentage}%`;
} else {
width = '0';
}
return htmlSafe(`width: ${width}`);
}),
init() {
this._super(...arguments);
this.extensions = ['gif', 'jpg', 'jpeg', 'png', 'svg'];
},
didReceiveAttrs() {
let image = this.get('payload');
if (image.img) {
this.set('url', image.img);
} else if (image.file) {
this.send('fileSelected', image.file);
delete image.file;
}
},
actions: {
fileSelected(fileList) {
// can't use array destructuring here as FileList is not a strict
// array and fails in Safari
// eslint-disable-next-line ember-suave/prefer-destructuring
let file = fileList[0];
// jscs:enable requireArrayDestructuring
let validationResult = this._validate(file);
this.set('file', file);
invokeAction(this, 'fileSelected', file);
if (validationResult === true) {
run.schedule('actions', this, function () {
this.generateRequest();
});
} else {
this._uploadFailed(validationResult);
}
},
reset() {
this.set('file', null);
this.set('uploadPercentage', 0);
},
saveUrl() {
let url = this.get('url');
invokeAction(this, 'update', url);
}
},
dragOver(event) {
if (!event.dataTransfer) {
return;
}
// this is needed to work around inconsistencies with dropping files
// from Chrome's downloads bar
if (navigator.userAgent.indexOf('Chrome') > -1) {
let eA = event.dataTransfer.effectAllowed;
event.dataTransfer.dropEffect = (eA === 'move' || eA === 'linkMove') ? 'move' : 'copy';
}
event.stopPropagation();
event.preventDefault();
this.set('dragClass', '-drag-over');
},
dragLeave(event) {
event.preventDefault();
this.set('dragClass', null);
},
drop(event) {
event.preventDefault();
this.set('dragClass', null);
if (event.dataTransfer.files) {
this.send('fileSelected', event.dataTransfer.files);
}
},
_uploadStarted() {
invokeAction(this, 'uploadStarted');
},
_uploadProgress(event) {
if (event.lengthComputable) {
run(() => {
let percentage = Math.round((event.loaded / event.total) * 100);
this.set('uploadPercentage', percentage);
});
}
},
_uploadFinished() {
invokeAction(this, 'uploadFinished');
},
_uploadSuccess(response) {
this.set('url', response);
this.get('payload').img = response;
this.get('env').save(this.get('payload'), false);
this.send('saveUrl');
this.send('reset');
invokeAction(this, 'uploadSuccess', response);
},
_uploadFailed(error) {
let message;
if (isVersionMismatchError(error)) {
this.get('notifications').showAPIError(error);
}
if (isUnsupportedMediaTypeError(error)) {
message = 'The image type you uploaded is not supported. Please use .PNG, .JPG, .GIF, .SVG.';
} else if (isRequestEntityTooLargeError(error)) {
message = 'The image you uploaded was larger than the maximum file size your server allows.';
} else if (error.payload && error.payload.errors && !isBlank(error.payload.errors[0].message)) {
message = error.payload.errors[0].message;
} else {
message = 'Something went wrong :(';
}
this.set('failureMessage', message);
invokeAction(this, 'uploadFailed', error);
},
generateRequest() {
let ajax = this.get('ajax');
// let formData = this.get('formData');
let file = this.get('file');
let formData = new FormData();
formData.append('uploadimage', file);
let url = `${this.get('apiRoot')}/uploads/`;
this._uploadStarted();
ajax.post(url, {
data: formData,
processData: false,
contentType: false,
dataType: 'text',
xhr: () => {
let xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
this._uploadProgress(event);
}, false);
// TODO: remove console.logs
/* eslint-disable no-console */
xhr.addEventListener('error', event => console.log('error', event));
xhr.upload.addEventListener('error', event => console.log('errorupload', event));
/* eslint-enabled no-console */
return xhr;
}
}).then((response) => {
let url = JSON.parse(response);
this._uploadSuccess(url);
}).catch((error) => {
this._uploadFailed(error);
}).finally(() => {
this._uploadFinished();
});
},
_validate(file) {
if (this.get('validate')) {
return invokeAction(this, 'validate', file);
} else {
return this._defaultValidator(file);
}
},
_defaultValidator(file) {
let extensions = this.get('extensions');
let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name);
if (!isEmberArray(extensions)) {
extensions = extensions.split(',');
}
if (!extension || extensions.indexOf(extension.toLowerCase()) === -1) {
return new UnsupportedMediaTypeError();
}
return true;
}
});

View file

@ -1,256 +0,0 @@
import Component from '@ember/component';
import counter from 'ghost-admin/utils/word-count';
import formatMarkdown from 'ghost-admin/utils/format-markdown';
import layout from '../../templates/components/card-markdown';
import {
UnsupportedMediaTypeError,
isRequestEntityTooLargeError,
isUnsupportedMediaTypeError,
isVersionMismatchError
} from 'ghost-admin/services/ajax';
import {computed} from '@ember/object';
import {invokeAction} from 'ember-invoke-action';
import {isBlank} from '@ember/utils';
import {isArray as isEmberArray} from '@ember/array';
import {observer} from '@ember/object';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
/* legacyConverter.makeHtml(_.toString(this.get('markdown'))) */
export default Component.extend({
ajax: service(),
layout,
accept: 'image/gif,image/jpg,image/jpeg,image/png,image/svg+xml',
extensions: null,
preview: computed('value', function () {
return formatMarkdown(this.get('payload').markdown);
}),
// TODO: remove observer
// eslint-disable-next-line ghost/ember/no-observers
save: observer('doSave', function () {
let payload = this.get('payload');
payload.markdown = this.$('textarea').val();
payload.wordcount = counter(payload.markdown);
this.set('value', this.$('textarea').val());
this.set('payload', payload);
this.get('env').save(payload, false);
}),
init() {
this._super(...arguments);
this.extensions = ['gif', 'jpg', 'jpeg', 'png', 'svg'];
this.set('value', this.get('payload').markdown);
},
didReceiveAttrs() {
if (!this.get('isEditing')) {
this.set('preview', formatMarkdown(this.get('payload').markdown));
} else {
run.next(() => {
this.$('textarea').focus();
});
}
},
actions: {
fileSelected(fileList) {
// can't use array destructuring here as FileList is not a strict
// array and fails in Safari
// eslint-disable-next-line ember-suave/prefer-destructuring
let file = fileList[0];
// jscs:enable requireArrayDestructuring
let validationResult = this._validate(file);
this.set('file', file);
invokeAction(this, 'fileSelected', file);
if (validationResult === true) {
run.schedule('actions', this, function () {
this.generateRequest();
});
} else {
this._uploadFailed(validationResult);
}
},
reset() {
this.set('file', null);
this.set('uploadPercentage', 0);
},
saveUrl() {
let url = this.get('url');
invokeAction(this, 'update', url);
},
selectCard() {
invokeAction(this, 'selectCard');
},
didDrop(event) {
event.preventDefault();
event.stopPropagation();
// eslint-disable-next-line ember-suave/prefer-destructuring
let el = this.$('textarea')[0]; // array destructuring here causes ember to throw an error about calling an Object as a Function
let start = el.selectionStart;
let end = el.selectionEnd;
let {files} = event.dataTransfer;
let combinedLength = 0;
// eslint-disable-next-line ember-suave/prefer-destructuring
let file = files[0]; // array destructuring here causes ember to throw an error about calling an Object as a Function
let placeholderText = `\r\n![uploading:${file.name}]()\r\n`;
el.value = el.value.substring(0, start) + placeholderText + el.value.substring(end, el.value.length);
combinedLength += placeholderText.length;
el.selectionStart = start;
el.selectionEnd = end + combinedLength;
this.send('fileSelected', event.dataTransfer.files);
},
didDragOver() {
this.$('textarea').addClass('dragOver');
},
didDragLeave() {
this.$('textarea').removeClass('dragOver');
}
},
_uploadStarted() {
invokeAction(this, 'uploadStarted');
},
_uploadProgress(event) {
if (event.lengthComputable) {
run(() => {
let percentage = Math.round((event.loaded / event.total) * 100);
this.set('uploadPercentage', percentage);
});
}
},
_uploadFinished() {
invokeAction(this, 'uploadFinished');
},
_uploadSuccess(response) {
this.set('url', response.url);
this.get('payload').img = response.url;
this.get('env').save(this.get('payload'), false);
this.send('saveUrl');
this.send('reset');
invokeAction(this, 'uploadSuccess', response);
let placeholderText = `![uploading:${response.file.name}]()`;
let imageText = `![](${response.url})`;
// eslint-disable-next-line ember-suave/prefer-destructuring
let el = this.$('textarea')[0]; // array destructuring on jquery causes ember to throw an error about calling an Object as a Function
el.value = el.value.replace(placeholderText, imageText);
let action = this.get('updateValue');
if (action) {
action();
}
},
_validate(file) {
if (this.get('validate')) {
return invokeAction(this, 'validate', file);
} else {
return this._defaultValidator(file);
}
},
_uploadFailed(error) {
let message;
if (isVersionMismatchError(error)) {
this.get('notifications').showAPIError(error);
}
if (isUnsupportedMediaTypeError(error)) {
message = 'The image type you uploaded is not supported. Please use .PNG, .JPG, .GIF, .SVG.';
} else if (isRequestEntityTooLargeError(error)) {
message = 'The image you uploaded was larger than the maximum file size your server allows.';
} else if (error.payload && error.payload.errors && !isBlank(error.payload.errors[0].message)) {
message = error.payload.errors[0].message;
} else {
message = 'Something went wrong :(';
}
this.set('failureMessage', message);
invokeAction(this, 'uploadFailed', error);
alert('upload failed');
// TODO: remove console.log
// eslint-disable-next-line no-console
console.log(error);
},
_defaultValidator(file) {
let extensions = this.get('extensions');
let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name);
if (!isEmberArray(extensions)) {
extensions = extensions.split(',');
}
if (!extension || extensions.indexOf(extension.toLowerCase()) === -1) {
return new UnsupportedMediaTypeError();
}
return true;
},
generateRequest() {
let ajax = this.get('ajax');
// let formData = this.get('formData');
let file = this.get('file');
let formData = new FormData();
formData.append('uploadimage', file);
let url = `${this.get('apiRoot')}/uploads/`;
this._uploadStarted();
ajax.post(url, {
data: formData,
processData: false,
contentType: false,
dataType: 'text',
xhr: () => {
let xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
this._uploadProgress(event);
}, false);
// TODO: remove console.logs
/* eslint-disable no-console */
xhr.addEventListener('error', event => console.log('error', event));
xhr.upload.addEventListener('error', event => console.log('errorupload', event));
/* eslint-enabled no-console */
return xhr;
}
}).then((response) => {
let url = JSON.parse(response);
this._uploadSuccess({file, url});
}).catch((error) => {
this._uploadFailed(error);
}).finally(() => {
this._uploadFinished();
});
}
});

View file

@ -1,598 +0,0 @@
import $ from 'jquery';
import Component from '@ember/component';
import Editor from 'mobiledoc-kit/editor/editor';
import Ember from 'ember';
import counter from 'ghost-admin/utils/word-count';
import createCardFactory from '../lib/card-factory';
import defaultCards from '../cards/index';
import layout from '../templates/components/gh-koenig';
import registerKeyCommands from '../options/key-commands';
import registerTextExpansions from '../options/text-expansions';
import {MOBILEDOC_VERSION} from 'mobiledoc-kit/renderers/mobiledoc';
import {assign} from '@ember/polyfills';
import {
checkIfClickEventShouldCloseCard,
getCardFromDoc,
getPositionOnScreenFromRange
} from '../lib/utils';
import {computed} from '@ember/object';
import {run} from '@ember/runloop';
// ember-cli-shims doesn't export Ember.testing
const {testing} = Ember;
export const TESTING_EXPANDO_PROPERTY = '__koenig_editor';
export const BLANK_DOC = {
version: MOBILEDOC_VERSION,
markups: [],
atoms: [],
cards: [],
sections: [[1, 'p', [[0, [], 0, '']]]]
};
export default Component.extend({
layout,
classNames: ['gh-koenig-container'],
// exterally set properties
mobiledoc: null,
placeholder: 'Click here to start ...',
spellcheck: true,
autofocus: false,
cards: null,
atoms: null,
serializeVersion: MOBILEDOC_VERSION,
options: null,
// exposed properties
editor: null,
editedCard: null,
selectedCard: null,
emberCards: null,
isMenuOpen: false,
editorHasRendered: false,
// internal properties
_domContainer: null,
// TODO: keyDownHandler is assigned event handlers when a card is
// hard-selected, is there a better way of handling this?
_keyDownHandler: null,
// merge in named options with the `options` property data-bag
editorOptions: computed(function () {
let options = this.get('options') || {};
let cards = this.get('cards') || [];
let atoms = this.get('atoms') || [];
// use our CardFactory to wrap our default and any user-supplied cards
// with Ghost specific functionality
// TODO: this also sets the emberCards property - do we need that indirection?
let createCard = createCardFactory.apply(this, {}); // need to pass the toolbar
cards = defaultCards.concat(cards).map(card => createCard(card));
// add our default atoms
atoms = atoms.concat([{
name: 'soft-return',
type: 'dom',
render() {
return document.createElement('br');
}
}]);
return assign({
placeholder: this.get('placeholder'),
spellchack: this.get('spellcheck'),
autofocus: this.get('autofocus'),
// cardOptions: this.get('cardOptions'),
cards,
atoms
}, options);
}),
init() {
this._super(...arguments);
// grab the supplied mobiledoc value - if it's empty set our default
// blank document, if it's a JSON string then deserialize it
let mobiledoc = this.get('mobiledoc');
if (!mobiledoc) {
mobiledoc = BLANK_DOC;
this.set('mobiledoc', mobiledoc);
}
if (typeof mobiledoc === 'string') {
mobiledoc = JSON.parse(mobiledoc);
this.set('mobiledoc', mobiledoc);
}
this.set('emberCards', []);
this._keyDownHandler = [];
// 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.
// TODO: is this reliable enough? What about most Windows laptops now being touch enabled?
this.set('isTouch', 'ontouchstart' in document.documentElement);
this._startedRunLoop = false;
},
didReceiveAttrs() {
this._super(...arguments);
if (this.get('autofocus') !== this._autofocus) {
this._autofocus = this.get('autofocus');
this._hasAutofocused = false;
}
},
willRender() {
// Use a default mobiledoc. If there are no changes, then return early.
let mobiledoc = this.get('mobiledoc') || BLANK_DOC;
let noMobiledocChanges
= (this._localMobiledoc && this._localMobiledoc === mobiledoc)
|| (this._upstreamMobiledoc && this._upstreamMobiledoc === mobiledoc);
if (noMobiledocChanges) {
return;
}
// reset everything ready for an editor re-render
this._upstreamMobiledoc = mobiledoc;
this._localMobiledoc = null;
// trigger hook action
this._willCreateEditor();
// teardown any old editor that might be around
let editor = this.get('editor');
if (editor) {
editor.destroy();
}
// create a new editor
let editorOptions = this.get('editorOptions');
editorOptions.mobiledoc = mobiledoc;
// TODO: instantiate component hooks?
// https://github.com/bustlelabs/ember-mobiledoc-editor/blob/master/addon/components/mobiledoc-editor/component.js#L163-L227
editor = new Editor(editorOptions);
// set up our default key handling and text expansions to emulate MD behaviour
// TODO: better place to do this?
registerKeyCommands(editor);
registerTextExpansions(editor);
editor.willRender(() => {
// The editor's render/rerender will happen after this `editor.willRender`,
// so we explicitly start a runloop here if there is none, so that the
// add/remove card hooks happen inside a runloop.
// When pasting text that gets turned into a card, for example,
// the add card hook would run outside the runloop if we didn't begin a new
// one now.
if (!run.currentRunLoop) {
this._startedRunLoop = true;
run.begin();
}
});
editor.didRender(() => {
// If we had explicitly started a run loop in `editor.willRender`,
// we must explicitly end it here.
if (this._startedRunLoop) {
this._startedRunLoop = false;
run.end();
}
this.set('editorHasRendered', true);
});
editor.postDidChange(() => {
run.join(() => {
this.postDidChange(editor);
});
});
editor.cursorDidChange(() => {
if (this.isDestroyed) {
return;
}
run.join(() => {
this.cursorMoved();
});
});
this.set('editor', editor);
// trigger hook action
this._didCreateEditor(editor);
},
didRender() {
// listen to keydown events outside of the editor, used to handle keydown
// events in the cards.
// TODO: is there a better way to handle this?
if (!document.onkeydown) {
document.onkeydown = event => this._keyDownHandler.reduce((returnType, handler) => {
let result = handler(event);
if (returnType !== false) {
return result;
}
return returnType;
}, true);
}
let editor = this.get('editor');
if (!editor.hasRendered) {
let $editor = this.$('.gh-koenig-surface');
let [domContainer] = $editor.parents(this.get('containerSelector'));
let [editorDom] = $editor;
editorDom.tabindex = this.get('tabindex');
this._domContainer = domContainer;
this._isRenderingEditor = true;
editor.render(editorDom);
this._isRenderingEditor = false;
}
this._setExpandoProperty(editor);
// autofocus 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.
// TODO: can this be removed if we refactor the new/edit screens to not re-render?
if (this._autofocus && !this._hasAutofocused) {
let range = document.createRange();
range.selectNodeContents(this.editor.element);
range.collapse(false);
let sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
editor._ensureFocus(); // PRIVATE API
// ensure we don't run the autofocus more than once between
// `autofocus` attr changes
this._hasAutofocused = true;
}
this.processWordcount();
},
willDestroyElement() {
this.editor.destroy();
this.send('deselectCard');
// TODO: should we be killing all global onkeydown event handlers?
document.onkeydown = null;
},
actions: {
// thin border, shows that a card is selected but the user cannot delete
// the card with 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');
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._keyDownHandler.length = 0;
// cardHolder.focus();
document.onclick = (event) => {
if (checkIfClickEventShouldCloseCard($(event.target), cardHolder)) {
this.send('deselectCard');
}
};
},
// thicker border and with keyboard events for moving around the editor
// 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');
}
// don't hard select an editing card.
if (this.editedCard && this.editedCard.id === cardId) {
this.send('editCard', cardId);
return;
}
let card = this.get('emberCards').find(card => card.id === cardId);
let cardHolder = $(`#${cardId}`).parents('.kg-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) => {
if (checkIfClickEventShouldCloseCard($(event.target), cardHolder)) {
this.send('deselectCard');
}
};
this._keyDownHandler.push((event) => {
let editor = this.get('editor');
switch (event.keyCode) {
case 37: // arrow left
case 38: // arrow up
getCardFromDoc(cardId, editor)
.then((section) => {
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.head.offset = range.tail.offset;
editor.selectRange(range);
} else {
$(this.get('titleSelector')).focus();
this.send('deselectCard');
}
});
return false;
case 39: // arrow right
case 40: // arrow down
getCardFromDoc(cardId, editor)
.then((section) => {
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);
} else {
$(this.get('titleSelector')).focus();
this.send('deselectCard');
}
});
return false;
case 13: // enter
getCardFromDoc(cardId, editor)
.then((section) => {
if (section.next) {
editor.run((postEditor) => {
let newSection = editor.builder.createMarkupSection('p');
postEditor.insertSectionBefore(editor.post.sections, newSection, section.next);
postEditor.setRange(newSection.toRange()); // new Mobiledoc.Range(newSection.headPosition)
});
return;
} else {
editor.run((postEditor) => {
let newSection = editor.builder.createMarkupSection('p');
postEditor.insertSectionAtEnd(newSection);
postEditor.setRange(newSection.toRange());
});
}
this.send('deselectCard');
});
return false;
case 27: // escape
this.send('selectCard', cardId);
return false;
case 8: // backspace
this.send('deleteCard', cardId);
return false;
case 46: // delete
this.send('deleteCard', cardId, true);
return false;
}
});
},
deselectCard() {
let selectedCard = this.get('selectedCard');
if (selectedCard) {
let cardHolder = $(`#${selectedCard.id}`).parent('.kg-card');
cardHolder.removeClass('selected');
cardHolder.removeClass('selected-hard');
this.set('selectedCard', null);
}
this._keyDownHandler.length = 0;
// TODO: do we want to kill all document onclick handlers?
document.onclick = null;
this.set('editedCard', null);
},
editCard(cardId) {
let card = this.get('emberCards').find(card => card.id === cardId);
this.set('editedCard', card);
this.send('selectCard', cardId);
},
deleteCard(cardId, forwards = false) {
let editor = this.get('editor');
let card = this.get('emberCards').find(card => card.id === cardId);
getCardFromDoc(cardId, editor).then(function (section) {
let range;
if (forwards && section.next) {
range = section.next.toRange();
range.tail.offset = 0;
} else if (section.prev) {
range = section.prev.toRange();
range.head.offset = range.tail.offset;
} else if (section.next) {
range = section.next.toRange();
range.tail.offset = 0;
} else {
card.env.remove();
return;
}
card.env.remove();
editor.selectRange(range);
});
},
stopEditingCard() {
this.set('editedCard', null);
},
menuOpened() {
this.set('isMenuOpen', true);
},
menuClosed() {
this.set('isMenuOpen', false);
},
// drag and drop images onto the editor
dropImage(event) {
if (event.dataTransfer.files.length) {
event.preventDefault();
for (let i = 0; i < event.dataTransfer.files.length; i += 1) {
let file = [event.dataTransfer.files[i]];
this.editor.insertCard('card-image', {pos: 'top', file});
}
}
},
dragOver(event) {
// required for drop events to fire on markdown cards in firefox.
event.preventDefault();
}
},
// makes sure the cursor is on screen except when selection is happening in
// which case the browser mostly ensures it. there is an issue with keyboard
// selection on some browsers though so the next step may be to record mouse
// and touch events.
cursorMoved() {
let editor = this.get('editor');
if (editor.range.isCollapsed) {
let scrollBuffer = 33; // the extra buffer to scroll.
let position = getPositionOnScreenFromRange(editor, $(this.get('containerSelector')));
if (!position) {
return;
}
let windowHeight = window.innerHeight;
if (position.bottom > windowHeight) {
this._domContainer.scrollTop += position.bottom - windowHeight + scrollBuffer;
} else if (position.top < 0) {
this._domContainer.scrollTop += position.top - scrollBuffer;
}
if (editor.range && editor.range.headSection && editor.range.headSection.isCardSection) {
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');
}
} else {
this.send('deselectCard');
}
},
// NOTE: This wordcount function doesn't count words that have been entered in cards.
// We should either allow cards to report their own wordcount or use the DOM
// (innerText) to calculate the wordcount.
processWordcount() {
let wordcount = 0;
if (this.editor.post.sections.length) {
this.editor.post.sections.forEach((section) => {
if (section.isMarkerable && section.text.length) {
wordcount += counter(section.text);
} else if (section.isCardSection && section.payload.wordcount) {
wordcount += Number(section.payload.wordcount);
}
});
}
let action = this.get('wordcountDidChange');
if (action) {
action(wordcount);
}
},
_willCreateEditor() {
let action = this.get('willCreateEditor');
if (action) {
action();
}
},
_didCreateEditor(editor) {
let action = this.get('didCreateEditor');
if (action) {
action(editor);
}
},
postDidChange(editor) {
// store a cache of the local doc so that we don't need to reinitialise it.
let serializeVersion = this.get('serializeVersion');
let updatedMobiledoc = editor.serialize(serializeVersion);
let onChangeAction = this.get('onChange');
let onFirstChangeAction = this.get('onFirstChange');
this._localMobiledoc = updatedMobiledoc;
if (onChangeAction) {
onChangeAction(updatedMobiledoc);
}
// we need to trigger a first-change action so that we can trigger a
// save and transition from new-> edit
if (this._localMobiledoc !== BLANK_DOC && !this._hasChanged) {
this._hasChanged = true;
if (onFirstChangeAction) {
onFirstChangeAction(this._localMobiledoc);
}
}
this.processWordcount();
},
_setExpandoProperty(editor) {
// Store a reference to the editor for the acceptance test helpers
if (this.element && testing) {
this.element[TESTING_EXPANDO_PROPERTY] = editor;
}
}
});

View file

@ -1,131 +0,0 @@
import Component from '@ember/component';
import layout from '../templates/components/koenig-card';
import {run} from '@ember/runloop';
export default Component.extend({
layout,
classNameBindings: ['isEditing'],
isEditing: false,
init() {
this._super(...arguments);
let card = this.get('card');
if (card.newlyCreated) {
run.next(() => {
if (card.card.launchMode === 'edit') {
this.send('startEdit');
this.send('selectCard');
} else {
this.send('selectCardHard');
}
});
this.set('isNew', true);
card.newlyCreated = false;
}
},
didReceiveAttrs() {
// we only want one card in "edit" mode at a time, if another card enters
// edit mode, save this card and return to preview mode
let editing = this.get('editedCard') === this.get('card');
if (this.get('isEditing') && !editing) {
this.send('stopEdit');
}
this.set('isEditing', editing);
},
didRender() {
// add the classname to the wrapping card as generated by mobiledoc.
// for some reason `this` on did render actually refers to the editor object and not the card object, after render it seems okay.
run.schedule('afterRender', this,
() => {
let card = this.get('card');
let {env: {name}} = card;
// 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);
mobiledocCard.click(() => {
if (!this.get('isEditing')) {
this.send('selectCardHard');
}
});
}
);
},
actions: {
save() {
this.set('doSave', Date.now());
},
toggleState() {
if (this.get('isEditing')) {
this.send('stopEdit');
} else {
this.send('startEdit');
}
},
selectCard() {
let action = this.get('selectCard');
if (action) {
action(this.card.id);
}
},
deselectCard() {
let action = this.get('deselectCard');
if (action) {
action(this.card.id);
}
this.send('stopEdit');
if (this.get('isNew')) {
let mobiledocCard = this.$().parents('.kg-card');
mobiledocCard.removeClass('new');
this.set('isNew', false);
}
},
selectCardHard() {
let action = this.get('selectCardHard');
if (action) {
action(this.card.id);
}
},
delete() {
let action = this.get('deleteCard');
if (action) {
action(this.card.id);
}
},
startEdit() {
let action = this.get('edit');
if (action) {
action(this.card.id);
}
},
stopEdit() {
this.send('save');
let action = this.get('stopEdit');
if (action) {
action(this.card.id);
}
}
}
});

View file

@ -1,29 +0,0 @@
import Component from '@ember/component';
import Range from 'mobiledoc-kit/utils/cursor/range';
import layout from '../templates/components/koenig-menu-item';
export default Component.extend({
layout,
tagName: 'div',
classNames: ['gh-cardmenu-card'],
classNameBindings: ['selected'],
init() {
this._super(...arguments);
this.set('selected', this.get('tool').selected);
},
click: function () { // eslint-disable-line
let {section, startOffset, endOffset} = this.get('range');
let editor = this.get('editor');
editor.range = Range.create(section, startOffset, section, endOffset);
let action = this.get('clicked');
if (action) {
action();
}
this.get('tool').onClick(editor, section);
}
});

View file

@ -1,216 +0,0 @@
import $ from 'jquery';
import Component from '@ember/component';
import Range from 'mobiledoc-kit/utils/cursor/range';
import Tools from '../options/default-tools';
import layout from '../templates/components/koenig-plus-menu';
import {computed} from '@ember/object';
import {run} from '@ember/runloop';
const ROW_LENGTH = 4;
export default Component.extend({
layout,
isOpen: false,
isButton: false,
toolsLength: 0,
selected: -1,
selectedTool: null,
query: '',
range: null,
editor: null,
showButton: computed('isOpen', 'isButton', function () {
return this.get('isOpen') || this.get('isButton');
}),
toolbar: computed('query', 'range', 'selected', function () {
let tools = [];
let match = (this.query || '').trim().toLowerCase();
let selected = this.get('selected');
let i = 0;
// todo cache active tools so we don't need to loop through them on selection change.
this.tools.forEach((tool) => {
if ((tool.type === 'block' || tool.type === 'card') && tool.cardMenu === true && (tool.label.toLowerCase().startsWith(match) || tool.name.toLowerCase().startsWith(match))) {
let t = {
label: tool.label,
name: tool.name,
icon: tool.icon,
onClick: tool.onClick,
range: this.get('range'),
order: tool.order,
selected: false
};
tools.push(t);
i += 1;
}
});
// TODO: this needs to go away because side effects should not be introduced in CPs
this.set('toolsLength', i); // eslint-disable-line
tools.sort((a, b) => a.order > b.order);
if (selected > -1) {
let selectedTool = tools[selected] || tools[0];
if (selectedTool) {
this.set('selectedTool', selectedTool);
selectedTool.selected = true;
}
} else {
// even if the range is out of bounds (as in the starting state where the selection prompt is not shown)
// we still need a selected item for the enter key.
this.set('selectedTool', tools[0]);
}
return tools;
}),
init() {
this._super(...arguments);
this.tools = new Tools(this.get('editor'), this);
},
didRender() {
let editor = this.get('editor');
let input = this.$('.gh-cardmenu-search-input');
let $editor = $(this.get('containerSelector'));
input.blur(() => {
run.later(() => {
this.send('closeMenuKeepButton');
}, 100);
});
editor.cursorDidChange(() => {
if (!editor.range || !editor.range.head.section) {
return;
}
if (!editor.range.head.section.isBlank) {
this.send('closeMenu');
return;
}
let currentNode = editor.range.head.section.renderNode.element;
let offset = this.$(currentNode).position();
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');
button.css('top', offset.top + $editor.scrollTop() - editorOffset.top - 2);
});
});
},
actions: {
openMenu() {
let button = this.$('.gh-cardmenu-button'); // the ⊕ button.
let $editor = $(this.get('containerSelector'));
this.set('isOpen', true);
this.set('selected', -1);
this.set('selectedTool', null);
this.propertyDidChange('toolbar');
run.schedule('afterRender', this,
() => {
let menu = this.$('.gh-cardmenu');
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());
menu.hide().fadeIn('fast', () => {
this.$('.gh-cardmenu-search-input').focus();
});
});
let action = this.get('menuIsOpen');
if (action) {
action();
}
},
closeMenu() {
this.set('isButton', false);
this.$('.gh-cardmenu').fadeOut('fast', () => {
this.set('isOpen', false);
});
let action = this.get('menuIsClosed');
if (action) {
action();
}
},
closeMenuKeepButton() {
this.set('isOpen', false);
},
selectTool() {
let {section} = this.get('range');
let editor = this.get('editor');
editor.range = Range.create(section, 0, section, 0);
this.get('selectedTool').onClick(editor);
this.send('closeMenuKeepButton');
},
moveSelectionLeft() {
let item = this.get('selected');
let length = this.get('toolsLength');
if (item > 0) {
this.set('selected', item - 1);
} else {
this.set('selected', length - 1);
}
},
moveSelectionUp() {
let item = this.get('selected');
if (item > ROW_LENGTH) {
this.set('selected', item - ROW_LENGTH);
} else {
this.set('selected', 0);
}
},
moveSelectionRight() {
let item = this.get('selected');
let length = this.get('toolsLength');
if (item < length) {
this.set('selected', item + 1);
} else {
this.set('selected', 0);
}
},
moveSelectionDown() {
let item = this.get('selected');
if (item < 0) {
item = 0;
}
let length = this.get('toolsLength');
if (item + ROW_LENGTH < length) {
this.set('selected', item + ROW_LENGTH);
} else {
this.set('selected', length - 1);
}
}
}
});

View file

@ -1,263 +0,0 @@
import $ from 'jquery';
import Component from '@ember/component';
import Tools from '../options/default-tools';
import layout from '../templates/components/koenig-slash-menu';
import {computed} from '@ember/object';
import {getPositionFromRange} from '../lib/utils';
import {run} from '@ember/runloop';
const ROW_LENGTH = 4;
export default Component.extend({
layout,
isOpen: false,
toolsLength: 0,
selected: 0,
selectedTool: null,
query: '',
range: null,
editor: null,
toolbar: computed('query', 'range', 'selected', function () {
let tools = [];
let match = (this.query || '').trim().toLowerCase();
let selected = this.get('selected');
let i = 0;
// todo cache active tools so we don't need to loop through them on selection change.
this.tools.forEach((tool) => {
if ((tool.type === 'block' || tool.type === 'card') && tool.cardMenu === true && (tool.label.toLowerCase().startsWith(match) || tool.name.toLowerCase().startsWith(match))) {
let t = {
label: tool.label,
name: tool.name,
icon: tool.icon,
onClick: tool.onClick,
range: this.get('range'),
order: tool.order,
selected: false
};
tools.push(t);
i += 1;
}
});
// TODO: this needs to go away because side effects should not be introduced in CPs
this.set('toolsLength', i); // eslint-disable-line
tools.sort((a, b) => a.order > b.order);
let selectedTool = tools[selected];
if (selected > -1) {
if (selectedTool) {
this.set('selectedTool', selectedTool);
selectedTool.selected = true;
}
} else {
// even if the range is out of bounds (as in the starting state where the selection prompt is not shown)
// we still need a selected item for the enter key.
this.set('selectedTool', tools[0]);
}
return tools;
}),
init() {
this._super(...arguments);
let editor = this.get('editor');
this.set('tools', new Tools(editor, this));
},
didRender() {
let editor = this.get('editor');
let self = this;
editor.cursorDidChange(this.cursorChange.bind(this));
editor.onTextInput({
name: 'slash_menu',
text: '/',
run() {
self.send('openMenu');
}
});
},
actions: {
openMenu() {
let holder = $(this.get('containerSelector'));
let editor = this.get('editor');
let self = this;
this.set('query', '');
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);
editor.registerKeyCommand({
str: 'LEFT',
name: 'slash',
run() {
let item = self.get('selected');
let length = self.get('toolsLength');
if (item > 0) {
self.set('selected', item - 1);
} else {
self.set('selected', length - 1);
}
}
});
editor.registerKeyCommand({
str: 'RIGHT',
name: 'slash',
run() {
let item = self.get('selected');
let length = self.get('toolsLength');
if (item < length) {
self.set('selected', item + 1);
} else {
self.set('selected', 1);
}
}
});
editor.registerKeyCommand({
str: 'UP',
name: 'slash',
run() {
let item = self.get('selected');
if (item > ROW_LENGTH) {
self.set('selected', item - ROW_LENGTH);
} else {
self.set('selected', 0);
}
}
});
editor.registerKeyCommand({
str: 'DOWN',
name: 'slash',
run() {
let item = self.get('selected');
if (item < 0) {
item = 0;
}
let length = self.get('toolsLength');
if (item + ROW_LENGTH < length) {
self.set('selected', item + ROW_LENGTH);
} else {
self.set('selected', length - 1);
}
}
});
editor.registerKeyCommand({
str: 'ENTER',
name: 'slash',
run(postEditor) {
let {range} = postEditor;
range.head.offset = self.get('range').startOffset - 1;
postEditor.deleteRange(range);
self.get('selectedTool').onClick(self.get('editor'));
self.send('closeMenu');
}
});
editor.registerKeyCommand({
str: 'ESC',
name: 'slash',
run() {
self.send('closeMenu');
}
});
let position = getPositionFromRange(editor, holder);
run.schedule('afterRender', this,
() => {
let menu = this.$('.gh-cardmenu');
let top = position.top + 20;
let left = position.left + (position.width / 2);
// calculate if parts of the menu that are hidden by the overflow.
let hiddenByOverflowY = (holder.innerHeight() + holder.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 = (holder.innerWidth() + holder.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');
});
let action = this.get('menuIsOpen');
if (action) {
action();
}
},
closeMenu() {
let editor = this.get('editor');
// this.get('editor').unregisterKeyCommand('slash'); -- waiting for the next release for this
for (let i = editor._keyCommands.length - 1; i > -1; i -= 1) {
let keyCommand = editor._keyCommands[i];
if (keyCommand.name === 'slash') {
editor._keyCommands.splice(i, 1);
}
}
this.$('.gh-cardmenu').fadeOut('fast', () => {
this.set('isOpen', false);
});
let action = this.get('menuIsClosed');
if (action) {
action();
}
},
clickedMenu() {
let editor = this.get('editor');
// let{section, startOffset, endOffset} = this.get('range');
editor.range.head.offset = this.get('range').startOffset - 1;
editor.deleteRange(editor.range);
this.send('closeMenu');
}
},
cursorChange() {
let editor = this.get('editor');
let range = this.get('range');
let isOpen = this.get('isOpen');
// if the cursor isn't in the editor then close the menu
if (!range || !editor.range.isCollapsed || editor.range.head.section !== range.section || this.editor.range.head.offset < 1 || !this.editor.range.head.section) {
// unless we click on a tool because the tool will close the menu.
if (isOpen && !$(window.getSelection().anchorNode).parents('.gh-cardmenu').length) {
this.send('closeMenu');
}
return;
}
if (isOpen) {
let queryString = editor.range.head.section.text.substring(range.startOffset, editor.range.head.offset);
this.set('query', queryString);
// if we've typed 5 characters and have no tools then close.
if (queryString.length > 5 && !this.get('toolLength')) {
this.send('closeMenu');
}
}
}
});

View file

@ -1,248 +0,0 @@
import $ from 'jquery';
import Component from '@ember/component';
import layout from '../templates/components/koenig-title-input';
import {run} from '@ember/runloop';
export default Component.extend({
layout,
val: '',
tagName: 'h2',
editor: null,
autofocus: false,
_cachedVal: '',
_mutationObserver: null,
editorKeyDownListener: null,
_hasSetupEventListeners: false,
didReceiveAttrs() {
if (this.get('editorHasRendered') && !this._hasSetupEventListeners) {
let editor = this.get('editor');
let title = this.$('.kg-title-input');
if (this.get('editorKeyDownListener')) {
editor.element.removeEventListener('keydown', this.get('editorKeyDownListener'));
}
this.set('editorKeyDownListener', this.editorKeyDown.bind(this));
editor.element.addEventListener('keydown', this.get('editorKeyDownListener'));
title[0].onkeydown = (event) => {
// block the browser format keys.
if (event.ctrlKey || event.metaKey) {
switch (event.keyCode) {
case 66: // B
case 98: // b
case 73: // I
case 105: // i
case 85: // U
case 117: // u
return false;
}
}
if (event.keyCode === 13) {
// enter
// on enter create a new paragraph at the top of the editor, this is because the first item may be a card.
editor.run((postEditor) => {
let marker = editor.builder.createMarker('');
let newSection = editor.builder.createMarkupSection('p', [marker]);
postEditor.insertSectionBefore(editor.post.sections, newSection, editor.post.sections.head);
let range = newSection.toRange();
range.tail.offset = 0; // colapse range
postEditor.setRange(range);
});
return false;
}
// down key
// if we're within ten pixels of the bottom of this element then we try and figure out where to position
// the cursor in the editor.
if (event.keyCode === 40) {
if (!window.getSelection().rangeCount) {
return;
}
let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM.
let cursorPositionOnScreen = range.getBoundingClientRect();
// in safari getBoundingClientRect on a range does not work if the range is collapsed.
if (cursorPositionOnScreen.bottom === 0) {
cursorPositionOnScreen = range.getClientRects()[0];
}
let offset = title.offset();
let bottomOfHeading = offset.top + title.height();
if (cursorPositionOnScreen.bottom > bottomOfHeading - 13) {
let editor = this.get('editor');
let loc = editor.element.getBoundingClientRect();
// if the first element is a card then that is always going to be selected.
if (editor.post.sections.head && editor.post.sections.head.isCardSection) {
run.next(() => {
window.getSelection().removeAllRanges();
$(editor.post.sections.head.renderNode.element).children('div').click();
});
return;
}
let cursorPositionInEditor = editor.positionAtPoint(cursorPositionOnScreen.left, loc.top);
if (!cursorPositionInEditor || cursorPositionInEditor.isBlank) {
editor.element.focus();
} else {
editor.selectRange(cursorPositionInEditor.toRange());
}
return false;
}
}
};
this._hasSetupEventListeners = true;
}
},
didInsertElement() {
this._super(...arguments);
let title = this.$('.kg-title-input');
// setup mutation observer
let mutationObserver = new MutationObserver(() => {
// on mutate we update.
if (title[0].textContent !== '') {
title.removeClass('no-content');
} else {
title.addClass('no-content');
}
// there is no consistency in how characters like nbsp and zwd are handled across browsers
// so we replace every whitespace character with a ' '
// note: this means that we can't have tabs in the title.
let textContent = title[0].textContent.replace(/\s/g, ' ');
let innerHTML = title[0].innerHTML.replace(/(&nbsp;|\s)/g, ' ');
// sanity check if there is formatting reset it.
if (innerHTML && innerHTML !== textContent) {
// run in next runloop so that we don't get stuck in infinite loops.
run.next(() => {
title[0].innerHTML = textContent;
});
}
if (this.get('val') !== textContent) {
let onChangeAction = this.get('onChange');
let updateAction = this.get('update');
this.set('_cachedVal', textContent);
this.set('val', textContent);
if (onChangeAction) {
onChangeAction(textContent);
}
if (updateAction) {
updateAction(textContent);
}
}
});
mutationObserver.observe(title[0], {childList: true, characterData: true, subtree: true});
this.set('_mutationObserver', mutationObserver);
},
didRender() {
let title = this.$('.kg-title-input');
if (!this.get('val')) {
title.addClass('no-content');
} else if (this.get('val') !== this.get('_cachedVal')) {
title.html(this.get('val'));
}
},
willDestroyElement() {
this.get('_mutationObserver').disconnect();
this.$('.kg-title-input')[0].onkeydown = null;
let editor = this.get('editor');
if (editor) {
editor.element.removeEventListener('keydown', this.get('editorKeyDownListener'));
}
},
editorKeyDown(event) {
// 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) {
return;
}
let range = selection.getRangeAt(0); // get the actual range within the DOM.
let cursorPositionOnScreen = range.getBoundingClientRect();
if (cursorPositionOnScreen.bottom === 0) {
cursorPositionOnScreen = range.getClientRects()[0];
}
let topOfEditor = editor.element.getBoundingClientRect().top;
// if the current paragraph is empty then the position is 0
if (!cursorPositionOnScreen || cursorPositionOnScreen.top === 0) {
if (editor.activeSection && editor.activeSection.renderNode) {
cursorPositionOnScreen = editor.activeSection.renderNode.element.getBoundingClientRect();
} else {
this.setCursorAtOffset(0);
return false;
}
}
if (cursorPositionOnScreen.top < topOfEditor + 33) {
let offset = this.getOffsetAtPosition(cursorPositionOnScreen.left);
this.setCursorAtOffset(offset);
return false;
}
}
},
// gets the character in the last line of the title that best matches the editor
getOffsetAtPosition(horizontalOffset) {
let [title] = this.$('.kg-title-input')[0].childNodes;
if (!title || !title.textContent) {
return 0;
}
let len = title.textContent.length;
let range = document.createRange();
for (let i = len - 1; i > -1; i -= 1) {
range.setStart(title, i);
range.setEnd(title, i + 1);
let rect = range.getBoundingClientRect();
if (rect.top === rect.bottom) {
continue;
}
if (rect.left <= horizontalOffset && rect.right >= horizontalOffset) {
// if the horizontalOffset is on the left hand side of the
// character then return `i`, if it's on the right return `i + 1`
return i + (horizontalOffset >= (rect.left + rect.right) / 2 ? 1 : 0);
}
}
return len;
},
// position the users cursor in the title based on the offset.
// unfortunately creating a range and adding it to the selection doesn't work.
// In Chrome it ignores the new range and places the cursor at the start of the element.
// in Firefox it places the cursor at the correct place but refuses to accept keyboard input.
setCursorAtOffset(offset) {
let [title] = this.$('.kg-title-input');
title.focus();
let selection = window.getSelection();
run.next(() => {
if (selection.modify) {
for (let i = 0; i < offset; i += 1) {
selection.modify('move', 'forward', 'character');
}
}
});
}
});

View file

@ -1,96 +0,0 @@
import $ from 'jquery';
import Component from '@ember/component';
import Tools from '../options/default-tools';
import layout from '../templates/components/koenig-toolbar-blockitem';
import {computed} from '@ember/object';
export default Component.extend({
layout,
classNames: ['toolbar-block'],
tools: null,
isBlank: false,
toolbar: computed('tools.@each.selected', function () {
let postTools = [];
let selectedPostTools = [];
this.tools.forEach((tool) => {
if (tool.type === 'block' || (tool.type === 'card' && this.isBlank)) {
if (tool.selected) {
selectedPostTools.push(tool);
} else {
postTools.push(tool);
}
}
});
return selectedPostTools.concat(postTools);
}),
init() {
this._super(...arguments);
let editor = this.editor = this.get('editor');
this.tools = new Tools(editor, this);
this.iconURL = `${this.get('assetPath')}/tools/`;
},
didRender() {
let $this = this.$();
let {editor} = this;
let $editor = $(this.get('containerSelector')); // TODO this is part of Ghost-Admin
editor.cursorDidChange(() => {
// if there is no cursor:
if (!editor.range || !editor.range.head.section) {
$this.fadeOut();
return;
}
let element = editor.range.head.section.renderNode._element;
if (this._element === element) {
return;
}
// if the section is a blank section then we can change it to a card, otherwise we can't.
if (editor.range.head.section.isBlank) {
this.set('isBlank', true);
} else {
this.set('isBlank', false);
}
this.propertyWillChange('toolbar');
this.__state = 'normal';
let offset = this.$(element).position();
let edOffset = $editor.offset();
$this.css('top', offset.top + $editor.scrollTop() - edOffset.top - 5);
if (element.tagName.toLowerCase() === 'li') {
$this.css('left', this.$(element.parentNode).position().left + $editor.scrollLeft() - 90);
} else {
$this.css('left', offset.left + $editor.scrollLeft() - 90);
}
$this.fadeIn();
this._element = element;
this.tools.forEach((tool) => {
if (tool.hasOwnProperty('checkElements')) {
// if its a list we want to know what type
let sectionTagName = editor.activeSection._tagName === 'li' ? editor.activeSection.parent._tagName : editor.activeSection._tagName;
tool.checkElements(editor.activeMarkups.concat([{tagName: sectionTagName}]));
}
});
this.propertyDidChange('toolbar');
});
},
willDestroy() {
this.editor.destroy();
}
});

View file

@ -1,50 +0,0 @@
import Component from '@ember/component';
import layout from '../templates/components/koenig-toolbar-button';
import {computed} from '@ember/object';
export default Component.extend({
layout,
tagName: 'button',
attributeBindings: ['title'],
classNames: ['gh-toolbar-btn'],
// TODO: what do selected/primary/secondary classes relate to? Some tools
// have 'primary' added but none of them appear do anything/be used elsewhere
classNameBindings: [
'selected',
'buttonClass',
'visibilityClass'
],
// exernally set properties
tool: null,
editor: null,
buttonClass: computed('tool.class', function () {
return `gh-toolbar-btn-${this.get('tool.class')}`;
}),
// returns "primary" or null
visibilityClass: computed('tool.visibility', function () {
return this.get('tool.visibility');
}),
title: computed('tool.label', function () {
return this.get('tool.label');
}),
willRender() {
// TODO: "selected" doesn't appear to do anything for toolbar items -
// it's only used within card menus
this.set('selected', !!this.tool.selected);
// sets the primary/secondary/
if (this.tool.visibility) {
this.set(this.tool.visibility, true);
}
},
click() {
this.tool.onClick(this.get('editor'));
}
});

View file

@ -1,74 +0,0 @@
import $ from 'jquery';
import Component from '@ember/component';
import Tools from '../options/default-tools';
import layout from '../templates/components/koenig-toolbar-newitem';
export default Component.extend({
layout,
classNames: ['toolbar-newitem'],
init() {
this._super(...arguments);
let editor = this.get('editor');
let tools = new Tools(editor, this);
this.tools = [];
tools.forEach((item) => {
if (item.type === 'block' || item.type === 'newline') {
this.tools.push(item);
}
});
this.iconURL = `${this.get('assetPath')}/tools/`;
},
didRender() {
let $this = this.$();
let editor = this.get('editor');
let $editor = $(this.get('containerSelector'));
if (!editor.range || !editor.range.head.section || !editor.range.head.section.isBlank
|| editor.range.head.section.renderNode._element.tagName.toLowerCase() !== 'p') {
$this.hide();
}
editor.cursorDidChange(() => {
// if there is no cursor:
if (!editor.range || !editor.range.head.section || !editor.range.head.section.isBlank
|| editor.range.head.section.renderNode._element.tagName.toLowerCase() !== 'p') {
$this.hide();
return;
}
let element = editor.range.head.section.renderNode._element;
if (this._element === element) {
return;
}
this.propertyWillChange('toolbar');
this.__state = 'normal';
this.isBlank = true;
let offset = this.$(element).position();
let edOffset = $editor.offset();
$this.css('top', offset.top + $editor.scrollTop() - edOffset.top - 13);
$this.css('left', offset.left + $editor.scrollLeft() + 20);
$this.show();
this._element = element;
this.tools.forEach((tool) => {
if (tool.hasOwnProperty('checkElements')) {
// if its a list we want to know what type
let sectionTagName = editor.activeSection._tagName === 'li' ? editor.activeSection.parent._tagName : editor.activeSection._tagName;
tool.checkElements(editor.activeMarkups.concat([{tagName: sectionTagName}]));
}
});
this.propertyDidChange('toolbar');
});
}
});

View file

@ -1,248 +0,0 @@
import $ from 'jquery';
import Component from '@ember/component';
import Tools from '../options/default-tools';
import cajaSanitizers from '../lib/caja-sanitizers';
import layout from '../templates/components/koenig-toolbar';
import {computed} from '@ember/object';
import {getPositionFromRange} from '../lib/utils';
import {run} from '@ember/runloop';
export default Component.extend({
layout,
classNames: ['gh-toolbar'],
// if any of the associated properties are true these class names will
// be dasherized and added to the element
classNameBindings: [
'isVisible',
'isLink',
'tickFullLeft',
'tickHalfLeft',
'tickFullRight',
'tickHalfRight',
'tickAbove',
'isTouch'
],
// externally set properties
editor: null,
assetPath: null,
containerSelector: null,
isTouch: null,
// internal properties
hasRendered: false,
activeTags: null,
tools: null,
isVisible: false,
tickFullLeft: false,
tickFullRight: false,
tickHalfLeft: false,
tickHalfRight: false,
tickAbove: false,
_isLink: false,
// TODO: why is this not just a property?
isLink: computed({
get() {
return this._isLink;
},
set(_, value) {
this._isLink = value;
return this._isLink;
}
}),
toolbar: computed('tools.@each.selected', function () {
let visibleTools = [];
this.tools.forEach((tool) => {
if (tool.type === 'markup') {
visibleTools.push(tool);
}
});
return visibleTools;
}),
toolbarBlocks: computed('tools.@each.selected', function () {
let visibleTools = [];
this.tools.forEach((tool) => {
if (tool.toolbar) {
visibleTools.push(tool);
}
});
return visibleTools;
}),
init() {
this._super(...arguments);
this.tools = new Tools(this.get('editor'), this);
this.iconURL = `${this.get('assetPath')}/tools/`;
},
didRender() {
if (this.get('hasRendered')) {
return;
}
let toolbar = this.$();
let {editor} = this;
let holder = $(this.get('containerSelector'));
let isMousedown = false;
holder.mousedown(() => isMousedown = true);
holder.mouseup(() => {
isMousedown = false;
this.updateToolbarToRange(toolbar, holder, isMousedown);
});
editor.cursorDidChange(() => this.updateToolbarToRange(toolbar, holder, isMousedown));
this.set('hasRendered', true);
},
willDestroyElement() {
this.editor.destroy();
},
actions: {
linkKeyDown(event) {
// if escape close link
if (event.keyCode === 27) {
this.send('closeLink');
}
},
linkKeyPress(event) {
// if enter run link
if (event.keyCode === 13) {
let url = event.target.value;
if (!cajaSanitizers.url(url)) {
url = `http://${url}`;
}
this.send('closeLink');
this.set('isVisible', false);
this.editor.run((postEditor) => {
let markup = postEditor.builder.createMarkup('a', {href: url});
postEditor.addMarkupToRange(this.get('linkRange'), markup);
});
this.set('linkRange', null);
event.stopPropagation();
}
},
doLink(range) {
// if a link is already selected then we remove the links from within the range.
let currentLinks = this.get('activeTags').filter(element => element.tagName === 'a');
if (currentLinks.length) {
this.get('editor').run((postEditor) => {
currentLinks.forEach((link) => {
postEditor.removeMarkupFromRange(range, link);
});
});
return;
}
this.set('isLink', true);
this.set('linkRange', range);
run.schedule('afterRender', this,
() => {
this.$('input').focus();
}
);
},
closeLink() {
this.set('isLink', false);
}
},
// update the location of the toolbar and display it if the range is visible.
updateToolbarToRange(toolbar, holder, isMouseDown) {
// if there is no cursor:
let editor = this.get('editor');
if (!editor.range || editor.range.head.isBlank || isMouseDown) {
if (!this.get('isLink')) {
this.set('isVisible', false);
}
return;
}
// set the active markups and sections
let sectionTagName = editor.activeSection.tagName === 'li' ? editor.activeSection.parent.tagName : editor.activeSection.tagName;
this.set('activeTags', editor.activeMarkups.concat([{tagName: sectionTagName}]));
// 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 position = getPositionFromRange(editor, holder);
this.set('isVisible', true);
run.schedule('afterRender', this,
() => {
// if we're in touch mode we just use CSS to display the toolbar.
if (this.get('isTouch')) {
return;
}
let width = toolbar.width();
let height = toolbar.height();
let top = position.top - toolbar.height() - 20;
let left = position.left + (position.width / 2) - (width / 2);
let right = left + width;
let edWidth = holder[0].scrollWidth;
if (left < 0) {
if (Math.round(left / (width / 4)) === -1) {
this.setTickPosition('tickFullLeft');
} else {
this.setTickPosition('tickHalfLeft');
}
left = 0;
} else if (right > edWidth) {
if (Math.round((edWidth - right) / (width / 4)) === -1) {
this.setTickPosition('tickFullRight');
} else {
this.setTickPosition('tickHalfRight');
}
left = left + (edWidth - right);
} else {
this.setTickPosition(null);
}
if (!this.get('isTouch') && top - holder.scrollTop() < 0) {
top = top + height + 60;
this.set('tickAbove', true);
} else {
this.set('tickAbove', false);
}
toolbar.css('top', top);
toolbar.css('left', left);
}
);
this.send('closeLink');
this.tools.forEach((tool) => {
if (tool.hasOwnProperty('checkElements')) {
// if its a list we want to know what type
let sectionTagName = editor.activeSection._tagName === 'li' ? editor.activeSection.parent._tagName : editor.activeSection._tagName;
tool.checkElements(editor.activeMarkups.concat([{tagName: sectionTagName}]));
}
});
} else {
if (this.isVisible) {
this.set('isVisible', false);
this.send('closeLink');
}
}
},
// set the location of the 'tick' arrow that appears at the bottom of the toolbar and points out the selection.
setTickPosition(tickPosition) {
let positions = ['tickFullLeft', 'tickHalfLeft', 'tickFullRight', 'tickHalfRight'];
positions.forEach((position) => {
this.set(position, position === tickPosition);
});
}
});

View file

@ -1,26 +0,0 @@
/**
* google-caja uses url() and id() to verify if the values are allowed.
*/
/**
* Check if URL is allowed
* URLs are allowed if they start with http://, https://, or /.
*/
let url = function (url) {
url = url.toString().replace(/['"]+/g, '');
if (/^https?:\/\//.test(url) || /^\//.test(url)) {
return url;
}
};
/**
* Check if ID is allowed
* All ids are allowed at the moment.
*/
let id = function (id) {
return id;
};
export default {
url,
id
};

View file

@ -1,99 +0,0 @@
/* eslint-disable camelcase */
import Ember from 'ember';
import EmberObject from '@ember/object';
import {copy} from '@ember/object/internals';
const {uuid} = Ember;
// returns a create card factory that takes a generic mobiledoc card and adds a ghost specific wrapper around it.
// it also provides helper functionality for Ember based cards.
export default function createCardFactory(toolbar) {
let self = this;
function createCard(cardObject) {
// if we have an array of cards then we convert them one by one.
if (cardObject instanceof Array) {
return cardObject.map(card => createCard(card));
}
// an ember card doesn't need a render or edit method
if (!cardObject.name || (!cardObject.willRender && cardObject.genus !== 'ember')) {
throw new Error('A card must have a name and willRender method');
}
cardObject.render = ({env, options, payload: _payload}) => {
// setupUI({env, options, payload});
// TODO: setup non ember UI
let payload = copy(_payload);
payload.card_name = env.name;
if (cardObject.genus === 'ember') {
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});
};
cardObject.edit = ({env, options, payload: _payload}) => {
// setupUI({env, options, payload});
let payload = copy(_payload);
payload.card_name = env.name;
if (cardObject.genus === 'ember') {
let card = setupEmberCard({env, options, payload});
let div = document.createElement('div');
div.id = card.id;
return div;
}
if (cardObject.hasOwnProperty('willRender')) {
return cardObject.willEdit({env, options, payload, toolbar});
} else {
return cardObject.willRender({env, options, payload, toolbar});
}
// do handle and delete stuff
};
cardObject.type = 'dom';
cardObject.didPlace = () => {
};
function setupEmberCard({env, options, payload}) {
let id = `GHOST_CARD_${uuid()}`;
let newlyCreated;
if (payload.newlyCreated) {
newlyCreated = true;
delete payload.newlyCreated;
env.save(payload, false);
}
let card = EmberObject.create({
id,
env,
options,
payload,
card: cardObject,
newlyCreated
});
self.emberCards.pushObject(card);
env.onTeardown(() => {
self.emberCards.removeObject(card);
});
return card;
}
return cardObject;
// self.editor.cards.push(cardObject);
}
// then return the card factory so new cards can be made at runtime
return createCard;
}

View file

@ -1,110 +0,0 @@
import $ from 'jquery';
import RSVP from 'rsvp';
// searches through the editor to see if it can find the current card
// as selected in the DOM.
export function getCardFromDoc(cardId, editor) {
return new RSVP.Promise((resolve, reject) => {
if (!cardId || !editor || !editor.post || !editor.post.sections) {
return reject();
}
editor.post.sections.forEach((section) => {
let sectionDom = $(section.renderNode.element);
if (section.isCardSection && sectionDom.find(`#${cardId}`).length) {
return resolve(section);
}
});
return reject();
});
}
// takes two jquery objects, the target of a click event and a cardholder
// and sees if the click event should close an open card or not
export 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;
}
// get a position in the editor based on the range.
// in Chrome, Firefox, and Edge range.getBoundingClientRect() works
// in Safari if the range is collapsed you get nothing so we expand the range by 1
// if that doesn't work then we fallback got the paragraph.
export function getPositionFromRange(editor, holder, range) {
let position = getPositionOnScreenFromRange(editor, holder, range);
let scrollLeft = holder.scrollLeft();
let scrollTop = holder.scrollTop();
return {
left: position.left + scrollLeft,
right: position.right + scrollLeft,
top: position.top + scrollTop,
bottom: position.bottom + scrollTop,
width: position.width,
height: position.height
};
}
// get a position on the screen based on the range.
export function getPositionOnScreenFromRange(editor, holder, range) {
if (!editor.range || !editor.range.head || !editor.range.head.section) {
return;
}
let position;
let offset = holder.offset();
let selection = window.getSelection();
if (!range && selection.rangeCount) {
range = selection.getRangeAt(0);
}
if (range) {
if (range.getBoundingClientRect) {
let rect = range.getBoundingClientRect();
position = {left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom};
}
// if getBoundingClientRect doesn't work then create it from the client rects
if ((!position || (position.left === 0 && position.top === 0)) && range.getClientRects) {
let rects = range.getClientRects();
for (let i = 0; i < rects.length; i += 1) {
let rect = rects[i];
if (position.left === 0 || position.left > rect.left) {
position.left = rect.left;
}
if (position.top === 0 || position.top > rect.top) {
position.top = rect.top;
}
if (position.right < rect.right) {
position.right = rect.right;
}
if (position.bottom < rect.bottom) {
position.bototm = rect.bottom;
}
}
}
}
// if we can't get the position from either getBoundingClientRect or getClientRects then get it based on the paragraph.
if (!position || (position && position.left === 0 && position.top === 0)) {
let rect = editor.range.head.section.renderNode.element.getBoundingClientRect();
position = {left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom};
}
return {
left: position.left - offset.left,
right: position.right - offset.left,
top: position.top - offset.top,
bottom: position.bottom - offset.top,
width: position.right - position.left,
height: position.bottom - position.top
};
}

View file

@ -1,309 +0,0 @@
import {set} from '@ember/object';
export default function (editor, toolbar) {
return [
{
name: 'H1',
class: 'h1',
icon: '',
label: 'Heading One',
visibility: 'primary',
selected: false,
type: 'block',
toolbar: true,
onClick: (editor) => {
editor.run((postEditor) => {
postEditor.toggleSection('h1');
});
},
checkElements(elements) {
set(this, 'selected', elements.filter(element => element.tagName === 'h1').length > 0);
}
},
{
name: 'H2',
class: 'h2',
label: 'Heading Two',
icon: '',
selected: false,
type: 'block',
visibility: 'primary',
toolbar: true,
onClick: (editor) => {
editor.run((postEditor) => {
postEditor.toggleSection('h2');
});
},
checkElements(elements) {
set(this, 'selected', elements.filter(element => element.tagName === 'h2').length > 0);
}
},
{
name: 'H3',
label: 'Heading Three',
icon: '',
selected: false,
type: 'block',
visibility: 'primary',
onClick: (editor) => {
editor.run((postEditor) => {
postEditor.toggleSection('h3');
});
},
checkElements(elements) {
set(this, 'selected', elements.filter(element => element.tagName === 'h3').length > 0);
}
},
{
name: 'p',
label: 'Text',
icon: 'text.svg',
selected: false,
type: 'block',
order: 0,
cardMenu: true,
onClick: (editor) => {
editor.run((postEditor) => {
postEditor.toggleSection('p');
});
},
checkElements(elements) {
set(this, 'selected', elements.filter(element => element.tagName === 'p').length > 0);
}
},
{
name: '&ldquo;',
label: 'Quote',
class: 'quote',
icon: 'quote.svg',
selected: false,
type: 'block',
toolbar: true,
onClick: (editor) => {
editor.run((postEditor) => {
postEditor.toggleSection('blockquote');
});
},
checkElements(elements) {
set(this, 'selected', elements.filter(element => element.tagName === 'blockquote').length > 0);
}
},
{
name: 'ul',
label: 'Bullet List',
icon: 'list-bullet.svg',
selected: false,
type: 'block',
order: 5,
cardMenu: true,
onClick: (editor) => {
editor.run((postEditor) => {
postEditor.toggleSection('ul');
});
},
checkElements(elements) {
set(this, 'selected', elements.filter(element => element.tagName === 'ul').length > 0);
}
},
{
name: 'ol',
label: 'Number List',
icon: 'list-number.svg',
selected: false,
type: 'block',
order: 6,
cardMenu: true,
onClick: (editor) => {
editor.run((postEditor) => {
postEditor.toggleSection('ol');
});
},
checkElements(elements) {
set(this, 'selected', elements.filter(element => element.tagName === 'ol').length > 0);
}
},
{
name: 'B',
label: 'Bold',
class: 'bold',
icon: 'bold.svg',
selected: false,
type: 'markup',
visibility: 'primary',
onClick: (editor) => {
editor.run((postEditor) => {
postEditor.toggleMarkup('strong');
});
},
checkElements(/* elements */) {
set(this, 'selected', true);
// set(this, 'selected', elements.filter((element) => element.tagName === 'strong').length > 0);
}
},
{
name: 'I',
label: 'Italic',
class: 'italic',
icon: 'italic.svg',
selected: false,
type: 'markup',
visibility: 'primary',
onClick: (editor) => {
editor.run((postEditor) => {
postEditor.toggleMarkup('em');
});
},
checkElements(elements) {
set(this, 'selected', elements.filter(element => element.tagName === 'em').length > 0);
}
},
{
name: 'S',
label: 'Strike',
class: 'strike',
icon: 'strikethrough.svg',
selected: false,
type: 'markup',
onClick: (editor) => {
editor.run((postEditor) => {
postEditor.toggleMarkup('s');
});
},
checkElements(elements) {
set(this, 'selected', elements.filter(element => element.tagName === 's').length > 0);
}
},
{
name: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none"><path d="M14.5 12.5l.086.086c.781.781 2.047.781 2.828 0l3.965-3.964c1.166-1.167 1.166-3.075 0-4.243l-1.758-1.757c-1.166-1.167-3.076-1.167-4.242 0l-3.965 3.964c-.781.781-.781 2.047 0 2.829l.086.085M9.5 11.5l-.086-.085c-.781-.781-2.047-.781-2.828 0l-3.965 3.964c-1.166 1.167-1.166 3.076 0 4.243l1.758 1.757c1.166 1.167 3.076 1.167 4.242 0l3.965-3.964c.781-.781.781-2.047 0-2.829l-.086-.086M8.111 15.889l7.778-7.778"/></g></svg>',
label: 'Link',
class: 'link',
icon: 'link.svg',
selected: false,
type: 'markup',
visibility: 'primary',
onClick: (editor) => {
toolbar.send('doLink', editor.range);
},
checkElements(elements) {
set(this, 'selected', elements.filter(element => element.tagName === 'a').length > 0);
}
},
{
name: 'img',
label: 'Image',
selected: false,
type: 'card',
icon: 'photos.svg',
visibility: 'primary',
order: 2,
cardMenu: true,
onClick: (editor) => {
editor.run((postEditor, section) => {
let thisSection = section || editor.range.headSection;
let card = postEditor.builder.createCardSection('card-image', {pos: 'top', newlyCreated: true});
if (thisSection.text.length) {
postEditor.insertSection(card);
} else {
postEditor.replaceSection(thisSection, card);
}
// insert empty paragraph after card if it's the last element.
if (!thisSection.next) {
let newSection = editor.builder.createMarkupSection('p');
postEditor.insertSectionAtEnd(newSection);
}
});
},
checkElements(elements) {
set(this, 'selected', elements.filter(element => element.tagName === 'sup').length > 0);
}
},
{
name: 'html',
label: 'Embed',
selected: false,
type: 'card',
icon: 'brackets.svg',
visibility: 'primary',
order: 3,
cardMenu: true,
onClick: (editor, section) => {
editor.run((postEditor) => {
let thisSection = section || editor.range.headSection;
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');
} else {
postEditor.replaceSection(thisSection, card);
}
if (!thisSection.next) {
let newSection = editor.builder.createMarkupSection('p');
postEditor.insertSectionAtEnd(newSection);
}
});
},
checkElements() {
}
},
{
name: 'hr',
label: 'Divider',
selected: false,
type: 'card',
icon: 'line.svg',
visibility: 'primary',
order: 4,
cardMenu: true,
onClick: (editor, section) => {
editor.run((postEditor) => {
let thisSection = section || editor.range.headSection;
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');
postEditor.insertSectionAtEnd(newSection);
}
});
},
checkElements() {
}
},
{
name: 'md',
label: 'Markdown',
selected: false,
type: 'card',
visibility: 'primary',
icon: 'markdown.svg',
order: 1,
cardMenu: true,
onClick: (editor, section) => {
editor.run((postEditor) => {
let thisSection = section || editor.range.headSection;
let card = postEditor.builder.createCardSection('card-markdown', {pos: 'top', markdown: 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-markdown');
} else {
postEditor.replaceSection(thisSection, card);
}
// if this is the last element then insert a paragraph after the card
if (!thisSection.next) {
let newSection = editor.builder.createMarkupSection('p');
postEditor.insertSectionAtEnd(newSection);
}
});
},
checkElements() {
}
}
];
}

View file

@ -1,16 +0,0 @@
// Key commands will run any time a particular key or key combination is pressed
// https://github.com/bustlelabs/mobiledoc-kit#configuring-hot-keys
export default function (editor) {
let softReturnKeyCommand = {
str: 'SHIFT+ENTER',
run(editor) {
editor.run((postEditor) => {
let softReturn = postEditor.builder.createAtom('soft-return');
postEditor.insertMarkers(editor.range.head, [softReturn]);
});
}
};
editor.registerKeyCommand(softReturnKeyCommand);
}

View file

@ -1,191 +0,0 @@
import {
replaceWithHeaderSection,
replaceWithListSection
} from 'mobiledoc-kit/editor/text-input-handlers';
// Text expansions watch text entry events and will look for matches, replacing
// the matches with additional markup, atoms, or cards
// https://github.com/bustlelabs/mobiledoc-kit#responding-to-text-input
export default function (editor) {
// We don't want to run all our content rules on every text entry event,
// instead we check to see if this text entry event could match a content
// rule, and only then run the rules. Right now we only want to match
// content ending with *, _, ), ~, and `. This could increase as we support
// more markdown.
editor.onTextInput({
name: 'inline_markdown',
match: /[*_)~`]$/,
run(postEditor, matches) {
let text = postEditor.range.head.section.textUntil(postEditor.range.head);
switch (matches[0]) {
case '*':
matchStrongStar(postEditor, text);
matchEmStar(postEditor, text);
break;
case '_':
matchStrongUl(postEditor, text);
matchEmUl(postEditor, text);
break;
case ')':
matchLink(postEditor, text);
matchImage(postEditor, text);
break;
case '~':
matchStrikethrough(postEditor, text);
break;
case '`':
matchMarkdown(postEditor, text);
break;
}
}
});
// block level markdown
editor.onTextInput({
name: 'dashul',
match: /^- $/,
run(editor) {
replaceWithListSection(editor, 'ul');
}
});
editor.onTextInput({
name: 'blockquote',
match: /^> $/,
run(editor) {
replaceWithHeaderSection(editor, 'blockquote');
}
});
// inline matches
function matchStrongStar(editor, text) {
let {range} = editor;
let matches = text.match(/\*\*(.+?)\*\*$/);
if (matches) {
range = range.extend(-(matches[0].length));
editor.run((postEditor) => {
let position = postEditor.deleteRange(range);
let bold = postEditor.builder.createMarkup('strong');
let nextPosition = postEditor.insertTextWithMarkup(position, matches[1], [bold]);
postEditor.insertTextWithMarkup(nextPosition, '', []);
});
}
}
function matchStrongUl(editor, text) {
let {range} = editor;
let matches = text.match(/__(.+?)__$/);
if (matches) {
range = range.extend(-(matches[0].length));
editor.run((postEditor) => {
let position = postEditor.deleteRange(range);
let bold = postEditor.builder.createMarkup('strong');
let nextPosition = postEditor.insertTextWithMarkup(position, matches[1], [bold]);
postEditor.insertTextWithMarkup(nextPosition, '', []);
});
}
}
function matchEmStar(editor, text) {
let {range} = editor;
let matches = text.match(/(^|[^*])\*([^*].*?)\*$/);
if (matches) {
let match = matches[0][0] === '*' ? matches[0] : matches[0].substr(1);
range = range.extend(-(match.length));
editor.run((postEditor) => {
let position = postEditor.deleteRange(range);
let em = postEditor.builder.createMarkup('em');
let nextPosition = postEditor.insertTextWithMarkup(position, matches[2], [em]);
postEditor.insertTextWithMarkup(nextPosition, '', []);
});
}
}
function matchEmUl(editor, text) {
let {range} = editor;
let matches = text.match(/(^|[^_])_([^_].+?)_$/);
if (matches) {
let match = matches[0][0] === '_' ? matches[0] : matches[0].substr(1);
range = range.extend(-(match.length));
editor.run((postEditor) => {
let position = postEditor.deleteRange(range);
let em = postEditor.builder.createMarkup('em');
let nextPosition = postEditor.insertTextWithMarkup(position, matches[2], [em]);
postEditor.insertTextWithMarkup(nextPosition, '', []);
});
}
}
function matchLink(editor, text) {
let {range} = editor;
let matches = text.match(/(^|[^!])\[(.*?)\]\((.*?)\)$/);
if (matches) {
let url = matches[3];
let text = matches[2];
let match = matches[0][0] === '[' ? matches[0] : matches[0].substr(1);
range = range.extend(-match.length);
editor.run((postEditor) => {
let position = postEditor.deleteRange(range);
let a = postEditor.builder.createMarkup('a', {href: url});
let nextPosition = postEditor.insertTextWithMarkup(position, text, [a]);
postEditor.insertTextWithMarkup(nextPosition, '', []); // insert the un-marked-up space
});
}
}
function matchImage(editor, text) {
let matches = text.match(/!\[(.*?)\]\((.*?)\)$/);
if (matches) {
let img = matches[2];
let alt = matches[1];
editor.run((postEditor) => {
let card = postEditor.builder.createCardSection('card-image', {pos: 'top', img, alt});
editor.range.extend(-(matches[0].length));
postEditor.replaceSection(editor.range.headSection, card);
if (!editor.range.headSection.next) {
let newSection = editor.builder.createMarkupSection('p');
postEditor.insertSectionAtEnd(newSection);
}
});
}
}
function matchStrikethrough(editor, text) {
let {range} = editor;
let matches = text.match(/~~(.+?)~~$/);
if (matches) {
range = range.extend(-(matches[0].length));
editor.run((postEditor) => {
let position = postEditor.deleteRange(range);
let s = postEditor.builder.createMarkup('s');
let nextPosition = postEditor.insertTextWithMarkup(position, matches[1], [s]);
postEditor.insertTextWithMarkup(nextPosition, '', []); // insert the un-marked-up space
});
}
}
function matchMarkdown(editor, text) {
let matches = text.match(/```([\s\S]*?)```$/);
if (matches) {
let code = matches[0];
editor.run((postEditor) => {
let card = postEditor.builder.createCardSection('card-markdown', {pos: 'top', markdown: code});
editor.range.extend(-(matches[0].length));
postEditor.replaceSection(editor.range.headSection, card);
if (!editor.range.headSection.next) {
let newSection = editor.builder.createMarkupSection('p');
postEditor.insertSectionAtEnd(newSection);
}
});
}
}
}

View file

@ -1,5 +0,0 @@
{{#if isEditing}}
{{gh-cm-editor value update=(action (mut value)) focus-in=(action "selectCard") autofocus=true}}
{{else}}
{{{value}}}
{{/if}}

View file

@ -1,23 +0,0 @@
{{#if url}}
<img src="{{url}}" />
{{else if file}}
{{!-- Upload in progress! --}}
{{#if failureMessage}}
<div class="failed">{{failureMessage}}</div>
{{/if}}
<div class="progress-container">
<div class="progress">
<div class="bar {{if failureMessage "fail"}}" style={{progressStyle}}></div>
</div>
</div>
{{#if failureMessage}}
<button class="btn btn-green" {{action "reset"}}>Try Again</button>
{{/if}}
{{else}}
{{!-- file selection/drag-n-drop --}}
<div class="upload-form">
{{#gh-file-input multiple=false alt=description action=(action 'fileSelected') accept=accept}}
<div class="gh-btn gh-btn-blue"><span>{{description}}</span></div>
{{/gh-file-input}}
</div>
{{/if}}

View file

@ -1,17 +0,0 @@
{{#koenig-card
isSelected=isSelected
isEditing=isEditing
toolbar=toolbar
onEnterEdit=(action "enterEditMode")
}}
{{#if isEditing}}
<textarea
onfocus={{action "selectCard"}}
ondrop={{action "didDrop"}}
ondragover={{action "didDragOver"}}
ondragleave={{action "didDragLeave"}}
>{{value}}</textarea>
{{else}}
{{{preview}}}
{{/if}}
{{/koenig-card}}

View file

@ -1,57 +0,0 @@
{{yield (hash
editor=editor
isMenuOpen=isMenuOpen
hasRendered=editorHasRendered
)}}
<div class='gh-koenig'>
<div class='gh-koenig-surface'
tabindex="{{tabindex}}"
ondrop={{action "dropImage"}}
ondragover={{action "dragOver"}} />
</div>
{{#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
deleteCard=(action "deleteCard")
}}
{{/ember-wormhole}}
{{/each}}
{{!-- Popup formatting toolbar when text is selected --}}
{{koenig-toolbar
editor=editor
assetPath=assetPath
containerSelector=containerSelector
isTouch=isTouch
}}
{{koenig-slash-menu
editor=editor
assetPath=assetPath
containerSelector=containerSelector
menuIsOpen=(action "menuOpened")
menuIsClosed=(action "menuClosed")
isTouch=isTouch
}}
{{koenig-plus-menu
editor=editor
assetPath=assetPath
containerSelector=containerSelector
isTouch=isTouch
menuIsOpen=(action "menuOpened")
menuIsClosed=(action "menuClosed")
isTouch=isTouch
}}

View file

@ -1,36 +0,0 @@
{{component
card.card.name
env=card.env
payload=card.payload
options=card.options
apiRoot=apiRoot
assetPath=assetPath
doSave=doSave
isEditing=isEditing
selectCard=(action "selectCard")
selectCardHard=(action "selectCardHard")
deselectCard=(action "deselectCard")
stopEdit=(action "stopEdit")
}}
<div class="kg-card-toolbar">
<div class="button-group">
<label {{action "selectCardHard"}}>{{card.card.label}}:</label>
{{#if isEditing}}
{{!--<button {{action "stopEdit"}} class='kg-card-button-text'>
Cancel
</button>--}}
<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 kg-card-button-text'>
Edit
</button>
{{/if}}
<button class='kg-card-button kg-card-delete' {{action "delete"}}>
{{inline-svg "trash"}}
</button>
{{/if}}
</div>
</div>

View file

@ -1,25 +0,0 @@
<div class="gh-cardmenu-icon">{{inline-svg tool.icon}}</div>
<div class="gh-cardmenu-label">{{tool.label}}</div>
{{!--
{{#if selected}}
<button {{action "select"}} class="selected">
{{#if tool.icon}}
<img src="{{iconURL}}{{tool.icon}}" />
{{else}}
{{tool.name}}
{{/if}}
{{tool.label}}
</button>
{{else}}
<button {{action "select"}}>
{{#if tool.icon}}
<img src="{{iconURL}}{{tool.icon}}" />
{{else}}
{{tool.name}}
{{/if}}
{{tool.label}}
</button>
{{/if}}
--}}

View file

@ -1,30 +0,0 @@
{{#if showButton}}
<button class="gh-cardmenu-button" {{action "openMenu"}}>{{inline-svg "plus"}}</button>
{{/if}}
{{#if isOpen}}
<div class="gh-cardmenu">
<div class="gh-cardmenu-search">
{{inline-svg "search.svg"}}
{{gh-input query
class="gh-input gh-cardmenu-search-input"
placeholder="Search for a card..."
type="text"
update=(action (mut query))
keyEvents=(hash
27=(action "closeMenu")
13=(action "selectTool")
37=(action "moveSelectionLeft")
38=(action "moveSelectionUp")
39=(action "moveSelectionRight")
40=(action "moveSelectionDown")
)
}}
</div>
<div class="gh-cardmenu-divider">
Primary
</div>
{{#each toolbar as |tool index|}}
{{koenig-menu-item tool=tool editor=editor range=range selected=tool.selected clicked=(action "closeMenu")}}
{{/each}}
</div>
{{/if}}

View file

@ -1,7 +0,0 @@
{{#if isOpen}}
<div class="gh-cardmenu">
{{#each toolbar as |tool index|}}
{{koenig-menu-item tool=tool editor=editor range=range selected=tool.selected clicked=(action "clickedMenu")}}
{{/each}}
</div>
{{/if}}

View file

@ -1 +0,0 @@
<div contenteditable="true" data-placeholder="Your Post Title" class="kg-title-input" tabindex={{tabindex}}></div>

View file

@ -1,5 +0,0 @@
<ul>
{{#each toolbar as |tool|}}
{{koenig-toolbar-button tool=tool editor=editor iconURL=iconURL assetPath=assetPath}}
{{/each}}
</ul>

View file

@ -1,6 +0,0 @@
Type / or select:
<ul>
{{#each tools as |tool|}}
{{koenig-toolbar-button tool=tool editor=editor iconURL=iconURL assetPath=assetPath}}
{{/each}}
</ul>

View file

@ -1,21 +0,0 @@
{{#if isLink}}
{{input
placeholder="Enter a link"
keyDown=(action "linkKeyDown")
keyPress=(action "linkKeyPress")
autofocus=true
}}
<button class="gh-toolbar-btn" {{action 'closeLink'}}>x</button>
{{else}}
{{#each toolbar as |tool|}}
{{koenig-toolbar-button
tool=tool
editor=editor}}
{{/each}}
<div class="gh-toolbar-divider" role="presentation"></div>
{{#each toolbarBlocks as |tool|}}
{{koenig-toolbar-button
tool=tool
editor=editor}}
{{/each}}
{{/if}}

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/cards/card-hr';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/cards/card-html';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/cards/card-image';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/cards/card-markdown';

View file

@ -1 +0,0 @@
export {default, BLANK_DOC} from 'gh-koenig/components/gh-koenig';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/koenig-card';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/koenig-menu-item';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/koenig-plus-menu';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/koenig-slash-menu';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/koenig-title-input';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/koenig-toolbar-blockitem';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/koenig-toolbar-button';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/koenig-toolbar-newitem';

View file

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/koenig-toolbar';

View file

@ -1,18 +0,0 @@
/* eslint-env node */
'use strict';
const Funnel = require('broccoli-funnel');
module.exports = {
name: 'gh-koenig',
isDevelopingAddon() {
return true;
},
treeForPublic() {
return new Funnel(`${__dirname}/public/tools/`, {
destDir: 'assets/tools/'
});
}
};

View file

@ -1,12 +0,0 @@
{
"name": "gh-koenig",
"description": "A mobiledoc-kit based editor for Ghost.",
"keywords": [
"ember-addon"
],
"readmeFilename": "README.md",
"dependencies": {
"ember-cli-babel": "^6.11.0",
"ember-cli-htmlbars": "^2.0.3"
}
}

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16.007 11.184c1.787-1.041 2.993-2.972 2.993-5.184 0-3.309-2.691-6-6-6h-9.5c-.276 0-.5.224-.5.5s.224.5.5.5h2.5v22h-2.5c-.276 0-.5.224-.5.5s.224.5.5.5h11c3.584 0 6.5-2.916 6.5-6.5 0-3.064-2.134-5.634-4.993-6.316zm-3.007-10.184c2.757 0 5 2.243 5 5s-2.243 5-5 5h-6v-10h6zm1.5 22h-7.5v-11h7.5c3.032 0 5.5 2.468 5.5 5.5s-2.468 5.5-5.5 5.5z"/></svg>

Before

Width:  |  Height:  |  Size: 447 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12.707 12l11.147-11.146c.195-.195.195-.512 0-.707s-.512-.195-.707 0l-11.147 11.146-11.146-11.147c-.195-.195-.512-.195-.707 0s-.195.512 0 .707l11.146 11.147-11.147 11.146c-.195.195-.195.512 0 .707.098.098.226.147.354.147s.256-.049.354-.146l11.146-11.147 11.146 11.146c.098.098.226.147.354.147s.256-.049.354-.146c.195-.195.195-.512 0-.707l-11.147-11.147z"/></svg>

Before

Width:  |  Height:  |  Size: 466 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M17.961 6.308l-.108-.162-5.999-5.999-.162-.108-.192-.039h-11c-.276 0-.5.224-.5.5v23c0 .276.224.5.5.5h17c.276 0 .5-.224.5-.5v-17l-.039-.192zm-5.961-4.601l4.293 4.293h-4.293v-4.293zm-11 21.293v-22h10v5.5c0 .276.224.5.5.5h5.5v16h-16zM11.854 10.146c-.195-.195-.512-.195-.707 0s-.195.512 0 .707l3.647 3.646-3.647 3.646c-.195.195-.195.512 0 .707.097.099.225.148.353.148s.256-.049.354-.146l4-4c.195-.195.195-.512 0-.707l-4-4.001zM6.853 10.146c-.195-.195-.512-.195-.707 0l-4 4c-.195.195-.195.512 0 .707l4 4c.098.098.226.147.354.147s.256-.049.353-.146c.195-.195.195-.512 0-.707l-3.646-3.647 3.646-3.646c.196-.196.196-.512 0-.708z"/></g></svg>

Before

Width:  |  Height:  |  Size: 740 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M6.103 8.411c-.195-.195-.512-.195-.707 0l-2.77 2.77c-.195.195-.195.512 0 .707l2.77 2.769c.098.098.226.147.353.147s.256-.049.353-.147c.195-.195.195-.512 0-.707l-2.416-2.416 2.416-2.416c.197-.195.197-.512.001-.707zM8.857 14.657c.098.098.226.147.354.147.128 0 .256-.049.354-.147l2.769-2.769c.195-.195.195-.512 0-.707l-2.769-2.77c-.195-.195-.512-.195-.707 0-.195.195-.195.512 0 .707l2.416 2.416-2.417 2.416c-.195.196-.195.512 0 .707zM23.855 13.646l-2.5-2.5c-.195-.195-.512-.195-.707 0l-7.502 7.501c-.057.057-.092.125-.116.197l-.011.019-1 3.5c-.05.175-.002.363.127.491.095.094.223.146.354.146l.138-.02 3.5-1 .019-.011c.072-.024.14-.059.197-.116l7.502-7.501c.094-.094.147-.221.147-.354-.001-.132-.054-.259-.148-.352zm-7.855 7.147l-1.793-1.793 4.795-4.794 1.793 1.793-4.795 4.794zm-2.253-.839l1.298 1.298-1.818.52.52-1.818zm7.755-4.662l-1.793-1.793 1.293-1.293 1.793 1.793-1.293 1.293zM1 1h10v4.5c0 .276.224.5.5.5h4.498l-.018 6.032 1 .004.02-6.534v-.001l-.02-.098-.019-.095-.108-.162-4.999-4.999-.162-.108-.192-.039h-11c-.276 0-.5.224-.5.5v21c0 .276.224.5.5.5h10.5v-1h-10v-20zm11 .707l3.293 3.293h-3.293v-3.293z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M17.486 11c-3.584 0-6.5 2.916-6.5 6.5s2.916 6.5 6.5 6.5 6.5-2.916 6.5-6.5-2.916-6.5-6.5-6.5zm0 12c-3.032 0-5.5-2.467-5.5-5.5s2.468-5.5 5.5-5.5 5.5 2.467 5.5 5.5-2.467 5.5-5.5 5.5zM20.213 17h-2.227v-2.227c0-.276-.224-.5-.5-.5s-.5.224-.5.5v2.227h-2.227c-.276 0-.5.224-.5.5s.224.5.5.5h2.227v2.228c0 .276.224.5.5.5s.5-.224.5-.5v-2.228h2.227c.276 0 .5-.224.5-.5s-.224-.5-.5-.5zM7.647 14.854c.111.111.269.165.425.142.156-.022.292-.117.367-.256l2.498-4.58.463 1.122c.104.256.399.377.652.272.255-.105.377-.397.271-.653l-.861-2.091c-.073-.178-.242-.298-.435-.309-.224-.02-.374.091-.466.26l-2.681 4.913-1.527-1.527c-.113-.114-.275-.166-.434-.14-.158.026-.295.127-.367.27l-2.5 5c-.077.155-.069.339.021.486.093.147.254.237.427.237h5.46c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-4.651l1.828-3.656 1.51 1.51zM5 9c1.102 0 2-.897 2-2s-.898-2-2-2-2 .897-2 2 .898 2 2 2zm0-3c.552 0 1 .449 1 1s-.448 1-1 1-1-.449-1-1 .448-1 1-1zM1 1h10v4.5c0 .276.224.5.5.5h4.5v4h1v-4.5l-.039-.192-.108-.162-4.999-4.999-.162-.108-.192-.039h-11c-.276 0-.5.224-.5.5v21c0 .276.224.5.5.5h10.5v-1h-10v-20zm11 .707l3.293 3.293h-3.293v-3.293z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M17.5 22h-1.163l-6.884-20.161c-.069-.203-.26-.339-.474-.339-.214 0-.404.137-.474.339l-6.863 20.161h-1.142c-.276 0-.5.224-.5.5s.224.5.5.5h3.02c.276 0 .5-.224.5-.5s-.223-.5-.5-.5h-.821l2.043-6h8.49l2.049 6h-.801c-.276 0-.5.224-.5.5s.224.5.5.5h3.02c.276 0 .5-.224.5-.5s-.224-.5-.5-.5zm-12.418-7l3.899-11.45 3.909 11.45h-7.808zM23.5 22h-.641l-4.482-13.126c-.069-.203-.26-.339-.474-.339-.214 0-.404.137-.474.339l-2.36 6.932c-.089.262.051.546.312.635.261.086.545-.051.635-.312l1.888-5.544 2.191 6.415h-3.175c-.276 0-.5.224-.5.5s.224.5.5.5h3.516l1.365 4h-.286c-.276 0-.5.224-.5.5s.224.5.5.5h1.985c.276 0 .5-.224.5-.5s-.224-.5-.5-.5z"/></g></svg>

Before

Width:  |  Height:  |  Size: 745 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M23.879.174c-.095-.11-.233-.174-.379-.174h-23c-.146 0-.284.064-.379.174-.095.11-.137.256-.115.4l3 20c.027.183.153.336.328.398l8.5 3 .166.028.166-.028 8.5-3c.175-.062.301-.214.328-.398l3-20c.022-.144-.02-.29-.115-.4zm-3.829 19.954l-8.05 2.842-8.05-2.841-2.869-19.129h21.838l-2.869 19.128zM7.5 11h8.424l-.873 6.108-3.051.872-3.055-.873-.412-2.684c-.042-.272-.29-.459-.57-.418-.273.042-.46.297-.418.57l.461 3c.029.193.169.351.356.405l3.5 1 .138.02.138-.019 3.5-1c.189-.054.329-.215.357-.41l1-7c.021-.144-.022-.289-.117-.398-.095-.11-.233-.173-.378-.173h-8.576l-.834-5h10.41c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-11c-.148 0-.286.064-.382.177-.095.112-.136.26-.111.405l1 6c.04.241.249.418.493.418z"/></g></svg>

Before

Width:  |  Height:  |  Size: 810 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M23.5 0h-8c-.276 0-.5.224-.5.5s.224.5.5.5h3.077l-14.348 22h-3.729c-.276 0-.5.224-.5.5s.224.5.5.5h8c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-3.077l14.348-22h3.729c.276 0 .5-.224.5-.5s-.224-.5-.5-.5z"/></svg>

Before

Width:  |  Height:  |  Size: 305 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M6.065 5.431c.098.098.226.146.354.146.128 0 .256-.049.353-.146.195-.195.195-.512 0-.707l-2-2c-.195-.195-.512-.195-.707 0-.195.195-.195.512 0 .707l2 2zM9.487 4.5c.276 0 .5-.224.5-.5v-2.5c0-.276-.224-.5-.5-.5s-.5.224-.5.5v2.5c0 .276.223.5.5.5zM2.893 9h2.5c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-2.5c-.276 0-.5.224-.5.5s.224.5.5.5zM10.565 18.724l-3.403 3.401c-1.128 1.128-3.114 1.128-4.242 0l-1.061-1.06c-.564-.564-.874-1.317-.874-2.122 0-.804.31-1.557.874-2.121l3.402-3.403c.195-.195.195-.512 0-.707-.195-.195-.512-.195-.707 0l-3.402 3.403c-.753.753-1.168 1.757-1.168 2.828s.414 2.076 1.168 2.829l1.06 1.06c.753.753 1.758 1.168 2.829 1.168s2.075-.415 2.828-1.168l3.403-3.401c.195-.195.195-.512 0-.707-.195-.196-.512-.196-.707 0zM19.893 8h-4.5c-.276 0-.5.224-.5.5s.224.5.5.5h4.5c1.663 0 3.123 1.438 3.123 3.077v2c0 1.585-1.441 2.923-3.147 2.923h-4.476c-.276 0-.5.224-.5.5s.224.5.5.5h4.476c2.248 0 4.147-1.796 4.147-3.923v-2c0-2.172-1.927-4.077-4.123-4.077z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M21.732 4.025l-1.758-1.757c-1.316-1.317-3.633-1.317-4.949 0l-3.965 3.964c-.975.975-.975 2.561.001 3.537l.086.085c.197.195.513.194.707-.002.195-.196.194-.512-.002-.707l-.085-.085c-.585-.585-.585-1.537 0-2.122l3.965-3.964c.939-.94 2.596-.94 3.535 0l1.757 1.757c.974.975.974 2.561 0 3.536l-3.965 3.964c-.585.585-1.536.585-2.121 0l-.086-.086c-.195-.195-.512-.195-.707 0s-.195.512 0 .707l.086.086c.489.489 1.129.732 1.769.732.64 0 1.28-.244 1.768-.731l3.965-3.964c1.363-1.364 1.363-3.584-.001-4.95zM12.854 14.146c-.195-.195-.512-.195-.707 0s-.195.512 0 .707l.086.086c.585.585.585 1.537 0 2.122l-3.965 3.964c-.939.94-2.595.94-3.535 0l-1.758-1.757c-.974-.975-.974-2.561 0-3.536l3.965-3.964c.585-.585 1.537-.584 2.122.001l.086.085c.197.195.513.194.707-.002.195-.196.194-.512-.002-.707l-.085-.085c-.975-.975-2.561-.975-3.535 0l-3.965 3.964c-1.363 1.365-1.363 3.585 0 4.95l1.757 1.757c.659.659 1.538 1.022 2.475 1.022s1.816-.363 2.475-1.022l3.965-3.964c.975-.975.975-2.561 0-3.536l-.086-.085zM7.758 16.243c.098.098.226.146.354.146.128 0 .256-.049.353-.146l7.777-7.778c.195-.195.195-.512 0-.707-.195-.195-.512-.195-.707 0l-7.777 7.778c-.196.195-.196.511 0 .707z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M8.5 5h15c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-15c-.276 0-.5.224-.5.5s.224.5.5.5zM23.5 12h-15c-.276 0-.5.224-.5.5s.224.5.5.5h15c.276 0 .5-.224.5-.5s-.224-.5-.5-.5zM23.5 20h-15c-.276 0-.5.224-.5.5s.224.5.5.5h15c.276 0 .5-.224.5-.5s-.224-.5-.5-.5zM4.5 2h-4c-.276 0-.5.224-.5.5v4c0 .276.224.5.5.5h4c.276 0 .5-.224.5-.5v-4c0-.276-.224-.5-.5-.5zm-.5 4h-3v-3h3v3zM4.5 10h-4c-.276 0-.5.224-.5.5v4c0 .276.224.5.5.5h4c.276 0 .5-.224.5-.5v-4c0-.276-.224-.5-.5-.5zm-.5 4h-3v-3h3v3zM4.5 18h-4c-.276 0-.5.224-.5.5v4c0 .276.224.5.5.5h4c.276 0 .5-.224.5-.5v-4c0-.276-.224-.5-.5-.5zm-.5 4h-3v-3h3v3z"/></g></svg>

Before

Width:  |  Height:  |  Size: 702 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M6.5 5h17c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-17c-.276 0-.5.224-.5.5s.224.5.5.5zM23.5 12h-17c-.276 0-.5.224-.5.5s.224.5.5.5h17c.276 0 .5-.224.5-.5s-.224-.5-.5-.5zM23.5 20h-17c-.276 0-.5.224-.5.5s.224.5.5.5h17c.276 0 .5-.224.5-.5s-.224-.5-.5-.5zM.85 4.394l.65-.634v2.74c0 .276.224.5.5.5s.5-.224.5-.5v-3.927c0-.201-.12-.383-.306-.461-.184-.077-.399-.038-.544.104l-1.5 1.464c-.197.192-.201.509-.008.707.193.196.509.202.708.007zM2.569 14h-1.036c.254-.298.465-.521.64-.703.515-.541.827-.868.827-1.76 0-.817-.673-1.482-1.5-1.482-.821 0-1.465.647-1.465 1.474 0 .276.224.5.5.5s.5-.224.5-.5c0-.279.191-.474.465-.474.28 0 .5.212.5.482 0 .491-.084.58-.551 1.07-.321.336-.761.798-1.352 1.595-.112.152-.129.354-.044.523.086.169.259.275.447.275h2.069c.276 0 .5-.224.5-.5s-.223-.5-.5-.5zM2.24 19.71l.609-.938c.101-.153.108-.35.02-.511-.086-.16-.255-.261-.438-.261h-1.931c-.276 0-.5.224-.5.5s.224.5.5.5h1.009l-.497.765c-.101.153-.108.35-.021.511.087.161.256.262.439.262.428 0 .5.12.5.482 0 .361-.072.481-.5.481h-.93c-.276-.001-.5.223-.5.499s.224.5.5.5h.931c.953 0 1.5-.54 1.5-1.481 0-.629-.244-1.079-.691-1.309z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M23.5 0h-17c-3.584 0-6.5 2.916-6.5 6.5s2.916 6.5 6.5 6.5h3.5v10.5c0 .276.224.5.5.5s.5-.224.5-.5v-22.5h6v22.5c0 .276.224.5.5.5s.5-.224.5-.5v-22.5h5.5c.276 0 .5-.224.5-.5s-.224-.5-.5-.5zm-13.5 12h-3.5c-3.032 0-5.5-2.468-5.5-5.5s2.468-5.5 5.5-5.5h3.5v11z"/></svg>

Before

Width:  |  Height:  |  Size: 364 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M11 3c-6.065 0-11 4.935-11 11v1.5c0 3.032 2.468 5.5 5.5 5.5s5.5-2.468 5.5-5.5-2.468-5.5-5.5-5.5c-1.748 0-3.305.823-4.313 2.099.891-4.607 4.95-8.099 9.813-8.099.276 0 .5-.224.5-.5s-.224-.5-.5-.5zm-5.5 8c2.481 0 4.5 2.019 4.5 4.5s-2.019 4.5-4.5 4.5-4.5-2.019-4.5-4.5 2.019-4.5 4.5-4.5zM23.5 3c-6.065 0-11 4.935-11 11v1.5c0 3.032 2.468 5.5 5.5 5.5s5.5-2.468 5.5-5.5-2.468-5.5-5.5-5.5c-1.748 0-3.305.823-4.314 2.099.892-4.607 4.951-8.099 9.814-8.099.276 0 .5-.224.5-.5s-.224-.5-.5-.5zm-5.5 8c2.481 0 4.5 2.019 4.5 4.5s-2.019 4.5-4.5 4.5-4.5-2.019-4.5-4.5 2.019-4.5 4.5-4.5z"/></g></svg>

Before

Width:  |  Height:  |  Size: 689 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M18.5 3c-3.032 0-5.5 2.468-5.5 5.5s2.468 5.5 5.5 5.5c1.748 0 3.305-.823 4.313-2.099-.891 4.607-4.95 8.099-9.813 8.099-.276 0-.5.224-.5.5s.224.5.5.5c6.065 0 11-4.935 11-11v-1.5c0-3.032-2.468-5.5-5.5-5.5zm0 10c-2.481 0-4.5-2.019-4.5-4.5s2.019-4.5 4.5-4.5 4.5 2.019 4.5 4.5-2.019 4.5-4.5 4.5zM6 3c-3.032 0-5.5 2.468-5.5 5.5s2.468 5.5 5.5 5.5c1.748 0 3.305-.823 4.314-2.099-.892 4.607-4.951 8.099-9.814 8.099-.276 0-.5.224-.5.5s.224.5.5.5c6.065 0 11-4.935 11-11v-1.5c0-3.032-2.468-5.5-5.5-5.5zm0 10c-2.481 0-4.5-2.019-4.5-4.5s2.019-4.5 4.5-4.5 4.5 2.019 4.5 4.5-2.019 4.5-4.5 4.5z"/></g></svg>

Before

Width:  |  Height:  |  Size: 696 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M23.5 13h-10.28c-.823-.61-1.728-1.166-2.623-1.715-2.621-1.607-5.097-3.125-5.097-5.785 0-3.248 3.444-3.5 4.5-3.5 3.988 0 4.5 1.603 4.5 3v1c0 .276.224.5.5.5s.5-.224.5-.5v-1c0-2.654-1.851-4-5.5-4-3.393 0-5.5 1.725-5.5 4.5 0 3.22 2.833 4.957 5.573 6.638.469.288.932.573 1.381.862h-10.954c-.276 0-.5.224-.5.5s.224.5.5.5h12.383c1.523 1.182 2.617 2.546 2.617 4.5 0 3.317-2.841 4.5-5.5 4.5-2.509 0-5.5-.694-5.5-4v-1c0-.276-.224-.5-.5-.5s-.5.224-.5.5v1c0 3.225 2.309 5 6.5 5 3.948 0 6.5-2.159 6.5-5.5 0-1.912-.853-3.326-2.085-4.5h9.085c.276 0 .5-.224.5-.5s-.224-.5-.5-.5z"/></svg>

Before

Width:  |  Height:  |  Size: 675 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M17.885.147c-.195-.195-.512-.195-.707 0l-7.647 7.646-7.646-7.646c-.195-.195-.512-.195-.707 0s-.195.512 0 .707l7.646 7.646-7.646 7.646c-.195.195-.195.512 0 .707.097.098.225.147.353.147.128 0 .256-.049.354-.146l7.646-7.647 7.646 7.647c.098.097.226.146.354.146.128 0 .256-.049.354-.146.195-.195.195-.512 0-.707l-7.647-7.647 7.646-7.647c.196-.195.196-.511.001-.706zM22.484 23h-1.52l1.711-2.443c.09-.132.325-.48.325-1.008 0-.957-.78-1.736-1.74-1.736-.937 0-1.701.73-1.74 1.664-.011.275.203.509.479.521.283.004.509-.204.521-.48.016-.395.342-.705.74-.705.408 0 .74.33.74.736 0 .205-.075.333-.147.441l-2.258 3.224c-.107.153-.12.352-.034.518.086.166.257.27.443.27h2.48c.276 0 .5-.224.5-.5s-.223-.502-.5-.502z"/></g></svg>

Before

Width:  |  Height:  |  Size: 819 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M17.854 7.147c-.195-.195-.512-.195-.707 0l-7.647 7.646-7.646-7.646c-.195-.195-.512-.195-.707 0s-.195.512 0 .707l7.646 7.646-7.647 7.646c-.195.195-.195.512 0 .707.098.098.226.147.354.147s.256-.049.354-.146l7.646-7.647 7.646 7.646c.098.098.226.147.354.147s.256-.049.354-.146c.195-.195.195-.512 0-.707l-7.647-7.647 7.647-7.647c.195-.195.195-.511 0-.706zM22.494 6.053h-1.521l1.709-2.441c.141-.204.326-.53.326-1.01 0-.957-.78-1.736-1.74-1.736-.936 0-1.701.731-1.74 1.665-.011.275.203.509.479.521.283-.003.509-.204.521-.48.016-.395.342-.706.74-.706.408 0 .74.33.74.736 0 .207-.078.339-.147.44l-2.258 3.224c-.107.153-.12.353-.034.518.086.166.257.27.443.27h2.482c.276 0 .5-.224.5-.5 0-.278-.223-.501-.5-.501z"/></g></svg>

Before

Width:  |  Height:  |  Size: 820 B

View file

@ -1 +0,0 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g><path d="M23.5 23h-23c-.276 0-.5.224-.5.5s.224.5.5.5h23c.276 0 .5-.224.5-.5s-.224-.5-.5-.5zM1.5 1h2.5v11c0 4.411 3.589 8 8 8s8-3.589 8-8v-11h2.5c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-6c-.276 0-.5.224-.5.5s.224.5.5.5h2.5v11c0 3.859-3.141 7-7 7s-7-3.141-7-7v-11h2.5c.276 0 .5-.224.5-.5s-.224-.5-.5-.5h-6c-.276 0-.5.224-.5.5s.224.5.5.5z"/></g></svg>

Before

Width:  |  Height:  |  Size: 442 B

View file

@ -1,20 +0,0 @@
module.exports = {
env: {
'embertest': true,
'mocha': true
},
globals: {
server: false,
expect: false,
fileUpload: false,
// ember-power-select test helpers
selectChoose: false,
selectSearch: false,
removeMultipleOption: false,
clearSelected: false,
// ember-power-datepicker test helpers
datepickerSelect: false
}
};

View file

@ -1,90 +0,0 @@
import $ from 'jquery';
import hbs from 'htmlbars-inline-precompile';
import {
EMPTY_DOC,
findEditor,
focusEditor,
inputText,
waitForRender
} from '../../helpers/editor-helpers';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('gh-koenig: Integration: Component: gh-koenig-slashmenu', function () {
setupComponentTest('gh-koenig-slashmenu', {
integration: true
});
beforeEach(function () {
this.set('value', EMPTY_DOC);
});
it('shows menu when / is typed', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
let editor = findEditor();
await focusEditor();
await inputText(editor, '/');
await waitForRender('.gh-cardmenu');
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(7);
});
it('filters tools when a user types', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
let editor = findEditor();
await focusEditor();
await inputText(editor, '/');
await waitForRender('.gh-cardmenu');
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(7);
await inputText(editor, ' bul');
expect(cardMenu.children().length).to.equal(1);
});
it('inserts card/markup when clicked');
it('inserts card/markup when enter is pressed');
it.skip('ul tool', async function () {
this.set('editorMenuIsOpen', function () {});
this.set('editorMenuIsClosed', function () {});
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
menuIsOpen=editorMenuIsOpen
menuIsClosed=editorMenuIsClosed
}}`);
let editor = findEditor();
await focusEditor();
await inputText(editor, '/');
await waitForRender('.gh-cardmenu');
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(7);
await inputText(editor, ' bul');
expect(cardMenu.children().length).to.equal(1);
await click('.gh-cardmenu-card');
// TODO: check inner HTML
});
});

View file

@ -1,308 +0,0 @@
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import {
EMPTY_DOC,
testEditorInput,
testEditorInputTimeout
} from '../../helpers/editor-helpers';
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
describe('gh-koenig: Integration: Component: gh-koenig', function () {
setupComponentTest('gh-koenig', {
integration: true
});
beforeEach(function () {
this.set('value', EMPTY_DOC);
});
it('fires change and word-count events', async function () {
// set defaults
this.set('onFirstChange', sinon.spy());
this.set('onChange', sinon.spy());
this.set('wordcount', 0);
this.set('actions.wordcountDidChange', function (wordcount) {
this.set('wordcount', wordcount);
});
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
onChange=(action onChange)
onFirstChange=(action onFirstChange)
wordcountDidChange=(action 'wordcountDidChange')
}}`);
await testEditorInput('abcd efg hijk lmnop', '<p>abcd efg hijk lmnop</p>', expect);
expect(this.get('onFirstChange').calledOnce, 'onFirstChanged called once').to.be.true;
expect(this.get('onChange').called, 'onChange called').to.be.true;
expect(this.get('wordcount'), 'wordcount').to.equal(4);
});
describe('Markerable markdown support.', function () {
it('plain text inputs (placebo)', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('abcdef', '<p>abcdef</p>', expect);
});
// bold
it('** bolds at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('**test**', '<p><strong>test</strong></p>', expect);
});
it('** bolds in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('123**test**', '<p>123<strong>test</strong></p>', expect);
});
it('__ bolds at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('__test__', '<p><strong>test</strong></p>', expect);
});
it('__ bolds in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('__test__', '<p><strong>test</strong></p>', expect);
});
// italic
it('* italicises at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('*test*', '<p><em>test</em></p>', expect);
});
it('* italicises in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('123*test*', '<p>123<em>test</em></p>', expect);
});
it('_ italicises at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('_test_', '<p><em>test</em></p>', expect);
});
it('_ italicises in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('123_test_', '<p>123<em>test</em></p>', expect);
});
// strikethrough
it('~~ strikethroughs at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('~~test~~', '<p><s>test</s></p>', expect);
});
it('~~ strikethroughs in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('123~~test~~', '<p>123<s>test</s></p>', expect);
});
// links
it('[]() creates a link at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput(
'[ghost](https://www.ghost.org/)',
'<p><a href="https://www.ghost.org/">ghost</a></p>',
expect);
});
it('[]() creates a link in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput(
'123[ghost](https://www.ghost.org/)',
'<p>123<a href="https://www.ghost.org/">ghost</a></p>',
expect);
});
});
describe('Block markdown support', function () {
// headings
it('# creates an H1', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('# ', '<h1><br></h1>', expect);
});
it('## creates an H2', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('## ', '<h2><br></h2>', expect);
});
it('### creates an H3', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('### ', '<h3><br></h3>', expect);
});
// lists
it('* creates an UL', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('* ', '<ul><li><br></li></ul>', expect);
});
it('- creates an UL', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('- ', '<ul><li><br></li></ul>', expect);
});
it('1. creates an OL', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('1. ', '<ol><li><br></li></ol>', expect);
});
// quote
it('> creates an blockquote', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('> ', '<blockquote><br></blockquote>', expect);
});
});
describe('Card markdown support.', function () {
it('![]() creates an image card', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
let value = await testEditorInputTimeout('![image of something](https://unsplash.it/200/300/?random)');
expect(value).to.have.string('kg-card-image');
});
it('``` creates a markdown card.', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
let value = await testEditorInputTimeout('```some code```');
expect(value).to.have.string('kg-card-markdown');
});
});
});

View file

@ -1,18 +0,0 @@
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('gh-koenig: Unit: Component: koenig-toolbar-button', function () {
setupComponentTest('koenig-toolbar-button', {
unit: true
});
it('renders', function () {
let component = this.subject();
expect(component._state).to.equal('preRender');
// renders the component on the page
this.render();
expect(component._state).to.equal('inDOM');
});
});

View file

@ -1,23 +0,0 @@
import {describe, it} from 'mocha';
import {editorShim} from '../../utils';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('gh-koenig: Unit: Component: koenig-toolbar-newitem', function () {
setupComponentTest('koenig-toolbar-newitem', {
unit: true,
needs: [
'component:koenig-toolbar-button'
]
});
it('renders', function () {
let component = this.subject();
component.editor = editorShim;
expect(component._state).to.equal('preRender');
// renders the component on the page
this.render();
expect(component._state).to.equal('inDOM');
});
});

View file

@ -1,38 +0,0 @@
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('gh-koenig: Unit: Component: koenig-toolbar', function () {
setupComponentTest('koenig-toolbar', {
unit: true
});
it('is not visible by default', function () {
let component = this.subject();
expect(component.isVisible).to.be.false;
});
it('contains tools', function () {
let component = this.subject();
// the standard toolbar tools (strong, em, strikethrough, link)
expect(component.get('toolbar').length).to.be.greaterThan(0);
// extended toolbar block bases tools (h1, h2, quote);
expect(component.get('toolbarBlocks').length).to.be.greaterThan(0);
});
// it('The toolbar appears when a range is selected.', function () {
// let component = this.subject();
// });
// it('A tool is selected when the cursor moves over text of that style.', function () {
// let component = this.subject();
// });
// it('A tool manipulates the content.', function () {
// let component = this.subject();
// });
// it('links stuff', function() {
// });
});

View file

@ -1,15 +0,0 @@
export let editorShim = {
range: {
head: {
section: {
renderNode: {
_element: {
tagName: 'P'
}
},
isBlank: false
}
}
},
cursorDidChange() {}
};

View file

@ -25,6 +25,17 @@ export default Component.extend({
return htmlSafe(formatMarkdown(this.get('payload.markdown')));
}),
init() {
this._super(...arguments);
this.set('toolbar', {
items: [{
icon: 'trash',
title: 'Delete',
action: () => {}
}]
});
},
actions: {
enterEditMode() {
// this action is called before the component is rendered so we