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

👷🏻‍♀️🚧👷 Ghost-Editor integration.

Integrated Ghost-Editor as an in-repo addon.
Moved CSS to /app/styles/addons/ghost-editor/

Still a WIP.
This commit is contained in:
Ryan McCarvill 2017-02-27 17:44:15 +13:00 committed by Kevin Ansfield
parent ffa1afbd59
commit 737a0b3ebd
126 changed files with 13378 additions and 2 deletions

View file

@ -1,3 +1,4 @@
//noinspection JSAnnotator
import Ember from 'ember';
import Application from 'ember-application';
import Resolver from './resolver';

View file

@ -0,0 +1,150 @@
@import "ghost-toolbar.css";
@import "ghost-toolbar-blockitem.css";
@import "slash-menu.css";
.editor-holder {
height: 100%;
}
.ghost-editor {
height: 100%;
-webkit-overflow-scrolling: touch;
}
.__mobiledoc-editor {
width: 100%;
min-height: 100%;
outline: none;
font-family: var(--font-family);
font-size: 1.7rem;
resize: none;
}
.dropper-bottom {
border-bottom: 66px solid #5ba4e5;
}
.dropper-top {
border-top: 66px solid #5ba4e5;
}
.dropper-left {
border-left: 66px solid #5ba4e5;
}
.dropper-right {
border-right: 66px solid #5ba4e5;
}
.__mobiledoc-card {
border: 1px solid #5ba4e5;
min-height: 100px;
display: inline-block;
width:calc(100% - 4px);
margin:2px;
padding:0;
}
.__mobiledoc-card .ghost-card {
position: relative;
}
.__mobiledoc-card .card-handle {
position: absolute;
right:0px;
top:0px;
margin-top:-25px;
height:20px;
display:none;
}
.__mobiledoc-card:hover .card-handle {
display:block;
}
.__mobiledoc-card .card-handle label {
font-size:10px;
}
.__mobiledoc-card .card-handle button {
background-color: var(--lightgrey);
border:1px solid var(--grey);
font-size:10px;
min-width: 80px;
}
.__mobiledoc-card textarea {
min-height:333px;
max-width:900px;
outline:none;
border:none;
resize: none;
}
.card-handle button:hover {
background-color: #718087;
color: #fff;
}
.card-handle button.confirm {
animation-duration: 1s;
animation-name: rotate;
background-color: red;
color: #e9e8dd;
}
.card-handle button.move {
background-image: url('http://localhost:4200/assets/move.png');
background-color: #9fbb58;
margin-left: -10px;
margin-right: 20px;
cursor: -webkit-grab;
cursor: -moz-grab;
}
textarea.ed_code {
width:100%;
height:100%;
border:none;
}
/**
* 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;
}

View file

@ -0,0 +1,122 @@
.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

@ -0,0 +1,107 @@
/* Variables
/* ---------------------------------------------------------- */
:root {
--button-size-selection: 48px;
--button-size-newline:32px;
}
.toolbar {
position: absolute;
margin: 0;
padding:5px;
-webkit-border-radius:var(--border-radius);
-moz-border-radius:var(--border-radius);
border-radius:var(--border-radius);
background-color: var(--lightgrey);
border:1px var(--lightgrey) solid;
}
.toolbar.is-visible {
/* animation: toolbar-fadein 111ms;*/
}
.toolbar input {
height: 64px;
width: 180px;
outline: none;
}
.toolbar ul {
list-style: none;
margin: 0;
padding: 0;
}
.toolbar ul li {
float: left;
margin: 0;
padding: 0;
overflow: hidden;
display: block;
width: var(--button-size-selection);
}
.toolbar 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:var(--button-size-selection);
min-width:var(--button-size-selection);
color: var(--darkgrey);
background-color: var(--lightgrey);
border: none;
}
.toolbar ul li button label {
display:none;
}
.toolbar ul li:hover button, .toolbar ul li.selected button {
background-color: var(--darkgrey);
color: var(--darkgrey);
transition: 300ms;
}
.toolbar ul li button img {
width:20px;
height:20px;
}
/* tick */
.toolbar:after {
content: "";
border-top: 13px solid var(--lightgrey);
border-top-color: inherit;
border-left: 13px solid transparent;
border-right: 13px solid transparent;
left: 50px;
left: calc(50% - 13px);
top:100%;
position:absolute;
}
@keyframes toolbar-fadein {
from {
opacity: 0;
/*transform: translateY(-13px);*/
}
to {
opacity: 1;
/*transform: translateY(0px);*/
}
}

View file

@ -0,0 +1,49 @@
.slash-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;
}
.slash-menu.is-visible {
animation: slash-menu-fadein 111ms;
}
.slash-menu ul {
margin:0;
padding:0;
list-style: none;
}
.slash-menu ul li button {
padding:5px;
width:100%;
text-align: left;
font-size:1.3rem;
border-radius: 4px;
}
.slash-menu ul li button img {
width:14px;
}
.slash-menu ul li button:hover, .slash-menu ul li button.selected {
background-color: var(--darkgrey);
}
@keyframes slash-menu-fadein {
from {
opacity: 0;
transform: scale(0.7);
transform-origin:top;
}
to {
opacity: 1;
transform: scale(1);
transform-origin:top;
}
}

View file

@ -48,3 +48,7 @@
@import "layouts/apps.css";
@import "layouts/packages.css";
@import "layouts/subscribers.css";
/* Addons: Ghost-Editor
/* ---------------------------------------------------------- */
@import "addons/ghost-editor/ghost-editor.css";

3
four.css Normal file
View file

@ -0,0 +1,3 @@
body {
border: 1000px yellow;
}

55
lib/ghost-editor/.gitignore vendored Normal file
View file

@ -0,0 +1,55 @@
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

23
lib/ghost-editor/LICENSE Normal file
View file

@ -0,0 +1,23 @@
Copyright (c) 2016-2017 Ghost Foundation
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright (c) 2016
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,65 @@
# Ghost-editor
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 Ghost-Editor`
- `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.
Ghost-Editor 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-2017 Ghost Foundation - Released under the [MIT license](LICENSE).

View file

View file

@ -0,0 +1,38 @@
/* jshint ignore:start */
// Provides a common.js list of all the cards for front end import - this is quite yuck so a new sollution should be found
var fs = require('fs'),
path = require('path');
var htmlCards = [],
editorCards = [],
ampCards = [],
textCards = [],
cards = [];
fs.readdirSync(__dirname).forEach(function(card) {
if(card !== 'index.js' && card!== 'common.js' && card.substr(card.length-7) !== '_dom.js' ) {
var _card = require(path.resolve(__dirname, card));
// todo check last 7 characters of filename to see if it's html, amp, text
_card.type = 'html';
htmlCards.push(_card);
cards.push(_card);
}
});
module.exports = {
editor: editorCards,
html: htmlCards,
amp: ampCards,
text: textCards,
all: cards
};
/* jshint ignore:end */

View file

@ -0,0 +1,9 @@
export default {
name: 'html-card',
label: 'HTML Card',
icon: '',
genus: 'ember',
buttons: {
preview: true
}
};

View file

@ -0,0 +1,12 @@
/* jshint ignore:start */
// this has to be a node style module as it's imported by the front end and ember.
// a card has to have an editor object, and an HTML object. In the future we might have AMP and Text (for FTS) renderers.
module.exports = {
name: 'html-card',
render: function(opts) {
return opts.payload.html;
}
};
/* jshint ignore:end */

View file

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

View file

@ -0,0 +1,12 @@
/* jshint ignore:start */
// this has to be a node style module as it's imported by the front end and ember.
// a card has to have an editor object, and an HTML object. In the future we might have AMP and Text (for FTS) renderers.
module.exports = {
name: 'image-card',
render: function(opts) {
return '<img src="' + opts.payload.img + '" />';
}
};
/* jshint ignore:end */

View file

@ -0,0 +1,12 @@
import htmlCard from 'ghost-editor/cards/html-card_dom';
import imageCard from 'ghost-editor/cards/image-card_dom';
import markdownCard from 'ghost-editor/cards/markdown-card_dom';
let cards = [];
[htmlCard, imageCard, markdownCard].forEach(_card => {
_card.type = 'dom';
cards.push(_card);
});
export default cards;

View file

@ -0,0 +1,7 @@
export default {
name: 'markdown-card',
label: 'Markdown Card',
icon: '',
genus: 'ember',
buttons: {preview: true}
};

View file

@ -0,0 +1,16 @@
/* jshint ignore:start */
// this has to be a node style module as it's imported by the front end and ember.
// a card has to have an editor object, and an HTML object. In the future we might have AMP and Text (for FTS) renderers.
var Showdown = require('showdown-ghost'),
converter = new Showdown.converter({extensions: ['ghostgfm', 'footnotes', 'highlight']});
module.exports = {
name: 'markdown-card',
render: function(opts) {
return converter.makeHtml(opts.payload.markdown || "");
}
};
/* jshint ignore:end */

View file

@ -0,0 +1,35 @@
import Ember from 'ember';
import layout from '../../templates/components/html-card';
export default Ember.Component.extend({
layout,
isEditing: true,
save: function () {
this.get('env').save(this.get('payload'), false);
}.observes('doSave'),
value : Ember.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;
}
}),
init() {
this._super(...arguments);
const payload = this.get('payload');
this.isEditing = !payload.hasOwnProperty('html');
},
didRender() {
}
});
// non editor cards need to be vanilla javascript
export let html = {
};

View file

@ -0,0 +1,296 @@
import Component from 'ember-component';
import computed from 'ember-computed';
import injectService from 'ember-service/inject';
import {htmlSafe} from 'ember-string';
import {isBlank} from 'ember-utils';
import {isEmberArray} from 'ember-array/utils';
import run from 'ember-runloop';
import layout from '../../templates/components/image-card';
import {invokeAction} from 'ember-invoke-action';
//import ghostPaths from 'ghost-editor/utils/ghost-paths';
import {
isRequestEntityTooLargeError,
isUnsupportedMediaTypeError,
isVersionMismatchError,
UnsupportedMediaTypeError
} from 'ghost-editor/services/ajax';
export default Component.extend({
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: ['gif', 'jpg', 'jpeg', 'png', 'svg'],
validate: null,
dragClass: null,
failureMessage: null,
file: null,
formType: 'upload',
url: null,
uploadPercentage: 0,
ajax: injectService(),
config: injectService(),
notifications: injectService(),
// TODO: this wouldn't be necessary if the server could accept direct
// file uploads
formData: computed('file', function () {
const file = this.get('file');
let formData = new FormData();
formData.append('file', file);
return formData;
}),
description: computed('text', 'altText', function () {
const altText = this.get('altText');
return this.get('text') || (altText ? `Upload image of "${altText}"` : 'Upload an image');
}),
progressStyle: computed('uploadPercentage', function () {
const percentage = this.get('uploadPercentage');
let width = '';
if (percentage > 0) {
width = `${percentage}%`;
} else {
width = '0';
}
return htmlSafe(`width: ${width}`);
}),
canShowUploadForm: computed('config.fileStorage', function () {
return this.get('config.fileStorage') !== false;
}),
showUploadForm: computed('formType', function () {
const canShowUploadForm = this.get('canShowUploadForm');
let formType = this.get('formType');
return formType === 'upload' && canShowUploadForm;
}),
didReceiveAttrs() {
const image = this.get('payload');
if(image.img) {
this.set('url', image.img);
} else if(image.file) {
this.send('fileSelected', image.file);
delete image.file;
}
},
dragOver(event) {
const showUploadForm = this.get('showUploadForm');
if (!event.dataTransfer) {
return;
}
// this is needed to work around inconsistencies with dropping files
// from Chrome's downloads bar
let eA = event.dataTransfer.effectAllowed;
event.dataTransfer.dropEffect = (eA === 'move' || eA === 'linkMove') ? 'move' : 'copy';
event.stopPropagation();
event.preventDefault();
if (showUploadForm) {
this.set('dragClass', '-drag-over');
}
},
dragLeave(event) {
const showUploadForm = this.get('showUploadForm');
event.preventDefault();
if (showUploadForm) {
this.set('dragClass', null);
}
},
drop(event) {
const showUploadForm = this.get('showUploadForm');
event.preventDefault();
this.set('dragClass', null);
if (showUploadForm) {
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.errors && !isBlank(error.errors[0].message)) {
message = error.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);
xhr.addEventListener('error', event => console.log("error", event));
xhr.upload.addEventListener('error', event => console.log("errorupload", event));
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;
},
actions: {
fileSelected(fileList) {
// can't use array destructuring here as FileList is not a strict
// array and fails in Safari
// jscs:disable requireArrayDestructuring
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);
}
},
onInput(url) {
this.set('url', url);
invokeAction(this, 'onInput', url);
},
reset() {
this.set('file', null);
this.set('uploadPercentage', 0);
},
switchForm(formType) {
this.set('formType', formType);
run.scheduleOnce('afterRender', this, function () {
invokeAction(this, 'formChanged', formType);
});
},
saveUrl() {
let url = this.get('url');
invokeAction(this, 'update', url);
}
}
});

View file

@ -0,0 +1,232 @@
import Ember from 'ember';
import layout from '../../templates/components/markdown-card';
import {formatMarkdown} from '../../libs/format-markdown';
import injectService from 'ember-service/inject';
import {invokeAction} from 'ember-invoke-action';
import {isEmberArray} from 'ember-array/utils';
import {isBlank} from 'ember-utils';
import run from 'ember-runloop';
import {
isRequestEntityTooLargeError,
isUnsupportedMediaTypeError,
isVersionMismatchError,
UnsupportedMediaTypeError
} from 'ghost-editor/services/ajax';
/* legacyConverter.makeHtml(_.toString(this.get('markdown')))
*/
export default Ember.Component.extend({
layout,
isEditing: true,
accept: 'image/gif,image/jpg,image/jpeg,image/png,image/svg+xml',
extensions: ['gif', 'jpg', 'jpeg', 'png', 'svg'],
ajax: injectService(),
editing: function () {
if(!this.isEditing) {
this.set('preview', formatMarkdown([this.get('payload').markdown]));
}
}.observes('isEditing'),
value : Ember.computed('payload', {
get() {
return this.get('payload').markdown || '';
},
set(_, value) {
this.get('payload').markdown = value;
this.get('env').save(this.get('payload'), false);
return value;
}
}),
_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})`;
const el = this.$('textarea')[0];
el.value = el.value.replace(placeholderText, imageText);
this.sendAction('updateValue');
},
_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.errors && !isBlank(error.errors[0].message)) {
message = error.errors[0].message;
} else {
message = 'Something went wrong :(';
}
this.set('failureMessage', message);
invokeAction(this, 'uploadFailed', error);
alert("upload failed");
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);
xhr.addEventListener('error', event => console.log("error", event));
xhr.upload.addEventListener('error', event => console.log("errorupload", event));
return xhr;
}
}).then((response) => {
let url = JSON.parse(response);
this._uploadSuccess({file, url});
}).catch((error) => {
this._uploadFailed(error);
}).finally(() => {
this._uploadFinished();
});
},
actions: {
updateValue( ) {
this.get('payload').markdown = this.$('textarea').val();
this.get('env').save(this.get('payload'), false);
this.set('preview', formatMarkdown([this.get('payload').markdown]));
},
fileSelected(fileList) {
// can't use array destructuring here as FileList is not a strict
// array and fails in Safari
// jscs:disable requireArrayDestructuring
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);
}
},
drop(event) {
event.preventDefault();
event.stopPropagation();
const el = this.$('textarea')[0];
const start = el.selectionStart;
const end = el.selectionEnd;
let files = event.dataTransfer.files;
let combinedLength = 0;
// for(let i = 0; i < files.length; i++) {
// let file = files[i];
// 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;
// }
let file = files[0];
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);
}
});

View file

@ -0,0 +1,12 @@
import Ember from 'ember';
import XFileInput from 'emberx-file-input/components/x-file-input';
// ember-cli-shims doesn't export Ember.testing
const {testing} = Ember;
export default XFileInput.extend({
change(e) {
const files = testing ? (e.originalEvent || e).testingFiles : e.target.files;
this.sendAction('action', files);
}
});

View file

@ -0,0 +1,7 @@
// // TODO - this is a ghost component, it should be kept in Ghost and the neccersary options passed to the editor.
import OneWayInput from 'ember-one-way-controls/components/one-way-input';
import TextInputMixin from 'ghost-admin/mixins/text-input';
export default OneWayInput.extend(TextInputMixin, {
classNames: 'gh-input'
});

View file

@ -0,0 +1,19 @@
import Ember from 'ember';
import layout from '../templates/components/ghost-card';
export default Ember.Component.extend({
layout,
classNames: ['ghost-card'],
actions: {
save() {
this.set('doSave', Date.now());
},
toggleState() {
this.set('isEditing', !this.get('isEditing'));
}
},
init() {
this._super(...arguments);
this.set('isEditing', false);
}
});

View file

@ -0,0 +1,154 @@
import Ember from 'ember';
import layout from '../templates/components/ghost-editor';
import Mobiledoc from 'mobiledoc-kit';
import {MOBILEDOC_VERSION} from 'mobiledoc-kit/renderers/mobiledoc';
import createCardFactory from '../libs/card-factory';
import defaultCommands from '../options/default-commands';
import editorCards from '../cards/index';
//import { VALID_MARKUP_SECTION_TAGNAMES } from 'mobiledoc-kit/models/markup-section'; //the block elements supported by mobile-doc
export const BLANK_DOC = {
version: MOBILEDOC_VERSION,
atoms: [],
markups: [],
cards: [],
sections: []
};
export default Ember.Component.extend({
layout,
classNames: ['editor-holder'],
emberCards: Ember.A([]),
init() {
this._super(...arguments);
this.container = document.querySelector(".gh-editor-container")[0];
let mobiledoc = this.get('value') || BLANK_DOC;
let userCards = this.get('cards') || [];
if (typeof mobiledoc === "string") {
mobiledoc = JSON.parse(mobiledoc);
}
//if the doc is cached then the editor is loaded and we don't need to continue.
if (this._cached_doc && this._cached_doc === mobiledoc) {
return;
}
let createCard = createCardFactory.apply(this, {}); //need to pass the toolbar
const options = {
mobiledoc: mobiledoc,
//temp
cards: createCard(editorCards.concat(userCards)),
atoms: [{
name: 'soft-return',
type: 'dom',
render() {
return document.createElement('br');
}
}],
spellcheck: true,
autofocus: this.get('shouldFocusEditor'),
placeholder: 'Click here to start ...'
};
this.editor = new Mobiledoc.Editor(options);
},
willRender() {
if(this._rendered) {
return;
}
let editor =this.editor;
editor.willRender(() => {
//console.log(Ember.run.currentRunLoop);
//if (!Ember.run.currentRunLoop) {
// this._startedRunLoop = true;
// Ember.run.begin();
//}
});
editor.didRender(() => {
this.sendAction('loaded', editor);
//Ember.run.end();
});
editor.postDidChange(()=> {
Ember.run.join(() => {
//store a cache of the local doc so that we don't need to reinitialise it.
this._cached_doc = editor.serialize(MOBILEDOC_VERSION);
this.sendAction('onChange', this._cached_doc);
if (this._cached_doc !== BLANK_DOC && !this._firstChange) {
this._firstChange = true;
this.sendAction('onFirstChange', this._cached_doc);
}
});
});
},
didRender() {
if(this._rendered) {
return;
}
let editorDom = this.$('.surface')[0];
this.domContainer = editorDom.parentNode.parentNode.parentNode.parentNode; // nasty nasty nasty.
this.editor.render(editorDom);
this._rendered = true;
window.editor = this.editor;
defaultCommands(this.editor); // initialise the custom text handlers for MD, etc.
// shouldFocusEditor is only true when transitioning from new to edit, otherwise it's false or undefined.
// therefore, if it's true it's after the first lot of content is entered and we expect the caret to be at the
// end of the document.
if (this.get('shouldFocusEditor')) {
var range = document.createRange();
range.selectNodeContents(this.editor.element);
range.collapse(false);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
this.editor._ensureFocus();
}
this.editor.cursorDidChange(() => this.cursorMoved());
},
// drag and drop images onto the editor
drop(event) {
if(event.dataTransfer.files.length) {
event.preventDefault();
for (let i = 0; i < event.dataTransfer.files.length; i++) {
let file = [event.dataTransfer.files[i]];
this.editor.insertCard('image-card', {pos: 'top', file});
}
}
},
// makes sure the cursor is on screen.
cursorMoved() {
const scrollBuffer = 33; // the extra buffer to scroll.
const range = window.getSelection().getRangeAt(0); // get the actual range within the DOM.
const position = range.getBoundingClientRect();
const 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;
}
},
willDestroy() {
this.editor.destroy();
}
});

View file

@ -0,0 +1,98 @@
import Ember from 'ember';
import layout from '../templates/components/ghost-toolbar-blockitem';
import Tools from '../options/default-tools';
export default Ember.Component.extend({
layout,
classNames: ['toolbar-block'],
tools: [],
isBlank: false,
toolbar: Ember.computed(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);
}).property('tools.@each.selected'),
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.editor;
let $editor = Ember.$('.gh-editor-container'); // 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

@ -0,0 +1,25 @@
import Ember from 'ember';
import layout from '../templates/components/ghost-toolbar-button';
export default Ember.Component.extend({
layout,
tagName: 'li',
classNameBindings: ['selected', 'primary', 'secondary'],
actions: {
click: function () {
this.tool.onClick(this.editor);
},
},
willRender: function() {
if(this.tool.selected) {
this.set('selected', true);
} else {
this.set('selected', false);
}
if(this.tool.visibility) {
this.set(this.tool.visibility,true);
}
}
});

View file

@ -0,0 +1,86 @@
import Ember from 'ember';
import layout from '../templates/components/ghost-toolbar-newitem';
import Tools from '../options/default-tools';
export default Ember.Component.extend({
layout,
classNames: ['toolbar-newitem'],
init( ) {
this._super(...arguments);
let editor = this.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/';
/* this.set('toolbar') =
this.tools =new Tools(this.get('editor'), this);
let tools = [ ];
let match = (this.query || "").trim().toLowerCase();
this.tools.forEach((tool) => {
if ((tool.type === 'block' || tool.type === 'newline') && tool.name.startsWith(match)) {
tools.push(tool);
}
});*/
},
didRender() {
let $this = this.$();
let editor = this.editor;
let $editor = Ember.$('.gh-editor-container');
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

@ -0,0 +1,135 @@
import Ember from 'ember';
import layout from '../templates/components/ghost-toolbar';
import Tools from '../options/default-tools';
export default Ember.Component.extend({
layout,
classNames: ['toolbar'],
classNameBindings: ['isVisible'],
isVisible: false,
tools: [],
isLink: Ember.computed({
get() {
return this._isLink;
},
set(_, value) {
this._isLink = value;
return this._isLink;
}
}),
toolbar: Ember.computed(function () {
// TODO if a block section other than a primary section is selected then
// the returned list removes one of the primary sections to compensate,
// so that there are only ever four primary sections.
let visibleTools = [ ];
this.tools.forEach(tool => {
if (tool.type === 'markup') {
visibleTools.push(tool);
}
});
return visibleTools;
}).property('tools.@each.selected'),
init() {
this._super(...arguments);
this.tools =new Tools(this.get('editor'), this);
this.iconURL = this.get('assetPath') + '/tools/';
},
didRender() {
let $this = this.$();
let editor = this.editor;
let $editor = Ember.$('.gh-editor-container'); // TODO - this element is part of ghost-admin, we need to separate them more.
let isMousedown = false;
if(!editor.range || editor.range.head.isBlank) {
this.set('isVisible', false);
}
$editor.mousedown(() => isMousedown = true);
$editor.mouseup(() => { isMousedown = false; updateToolbarToRange(this, $this, $editor, isMousedown);});
editor.cursorDidChange(() => updateToolbarToRange(this, $this, $editor, isMousedown));
},
willDestroyElement() {
this.editor.destroy();
},
actions: {
linkKeyDown(event) {
// if escape close link
if (event.keyCode === 27) {
this.set('isLink', false);
}
},
linkKeyPress(event) {
// if enter run link
if (event.keyCode === 13) {
this.set('isLink', false);
this.editor.run(postEditor => {
let markup = postEditor.builder.createMarkup('a', {href: event.target.value});
postEditor.addMarkupToRange(this.get('linkRange'), markup);
});
this.set('linkRange', null);
event.stopPropagation();
}
}
},
doLink(range) {
this.set('isLink', true);
this.set('linkRange', range);
}
});
// update the location of the toolbar and display it if the range is visible.
function updateToolbarToRange(self, $holder, $editor, isMouseDown) {
// if there is no cursor:
let editor = self.editor;
if(!editor.range || editor.range.head.isBlank || isMouseDown) {
if(!self.get('isLink')) {
self.set('isVisible', false);
}
return;
}
self.propertyWillChange('toolbar');
if(!editor.range.isCollapsed) {
// if we have a selection, then the toolbar appears just below said selection:
let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM.
let position = range.getBoundingClientRect();
let edOffset = $editor.offset();
self.set('isVisible', true);
Ember.run.schedule('afterRender', this,
() => {
$holder.css('top', position.top + $editor.scrollTop() - $holder.height()-20); //- edOffset.top+10
$holder.css('left', position.left + (position.width / 2) + $editor.scrollLeft() - edOffset.left - ($holder.width()/2));
}
);
self.set('isLink', false);
self.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(self.isVisible) {
self.set('isVisible', false);
self.set('isLink', false);
}
}
self.propertyDidChange('toolbar');
}

View file

@ -0,0 +1,42 @@
import Ember from 'ember';
import layout from '../templates/components/slash-menu-item';
import Range from 'mobiledoc-kit/utils/cursor/range';
export default Ember.Component.extend({
layout,
tagName: 'li',
actions: {
select: function() {
let {section, startOffset, endOffset} = this.get('range');
window.getSelection().removeAllRanges();
const range = document.createRange();
range.setStart(section.renderNode._element, 0);//startOffset-1); // todo
range.setEnd(section.renderNode._element, 0);//endOffset-1);
const selection = window.getSelection();
selection.addRange(range);
console.log(startOffset, endOffset, Range);
//let editor = this.get('editor');
//let range = editor.range;
//console.log(endOffset, startOffset);
//range = range.extend(endOffset - startOffset);
// editor.run(postEditor => {
// let position = postEditor.deleteRange(range);
// let em = postEditor.builder.createMarkup('em');
//let nextPosition = postEditor.insertTextWithMarkup(position, 'BOO', [em]);
//postEditor.insertTextWithMarkup(nextPosition, '', []); // insert the un-marked-up space
//});
this.get('tool').onClick(this.get('editor'));
}
},
init() {
this._super(...arguments);
}
});

View file

@ -0,0 +1,180 @@
import Ember from 'ember';
import Tools from '../options/default-tools';
import layout from '../templates/components/slash-menu';
export default Ember.Component.extend({
layout,
classNames: ['slash-menu'],
classNameBindings: ['isVisible'],
range: null,
menuSelectedItem: 0,
toolsLength:0,
selectedTool:null,
isVisible:false,
toolbar: Ember.computed(function () {
let tools = [ ];
let match = (this.query || "").trim().toLowerCase();
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.label.toLowerCase().startsWith(match) || tool.name.toLowerCase().startsWith(match))) {
let t = {
label : tool.label,
name: tool.name,
icon: tool.icon,
selected: i===this.menuSelectedItem,
onClick: tool.onClick
};
if(i === this.menuSelectedItem) {
this.set('selectedTool', t);
}
tools.push(t);
i++;
}
});
this.set('toolsLength', i);
if(this.menuSelectedItem > this.toolsLength) {
this.set('menuSelectedItem', this.toolsLength-1);
// this.propertyDidChange('toolbar');
}
if(tools.length < 1) {
this.isActive = false;
this.set('isVisible', false);
}
return tools;
}),
init() {
this._super(...arguments);
this.tools =new Tools(this.get('editor'), this);
this.iconURL = this.get('assetPath') + '/tools/';
this.editor.cursorDidChange(this.cursorChange.bind(this));
let self = this;
this.editor.onTextInput(
{
name: 'slash_menu',
text: '/',
run(editor) {
self.open(editor);
}
});
},
willDestroy() {
this.editor.destroy();
},
cursorChange() {
if(this.isActive) {
if(!this.editor.range.isCollapsed || this.editor.range.head.section !== this._node || this.editor.range.head.offset < 1 || !this.editor.range.head.section) {
this.close();
}
this.query = this.editor.range.head.section.text.substring(this._offset, this.editor.range.head.offset);
this.set('range', {
section: this._node,
startOffset: this._offset,
endOffset: this.editor.range.head.offset
});
this.propertyDidChange('toolbar');
}
},
open(editor) {
let self = this;
let $this = this.$();
let $editor = Ember.$('.gh-editor-container');
this._node = editor.range.head.section;
this._offset = editor.range.head.offset;
this.isActive = true;
this.cursorChange();
let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM.
let position = range.getBoundingClientRect();
let edOffset = $editor.offset();
this.set('isVisible', true);
Ember.run.schedule('afterRender', this,
() => {
$this.css('top', position.top + $editor.scrollTop() - edOffset.top + 20); //- edOffset.top+10
$this.css('left', position.left + (position.width / 2) + $editor.scrollLeft() - edOffset.left );
}
);
this.query="";
this.propertyDidChange('toolbar');
const downKeyCommand = {
str: 'DOWN',
_ghostName: 'slashdown',
run() {
let item = self.get('menuSelectedItem');
if(item < self.get('toolsLength')-1) {
self.set('menuSelectedItem', item + 1);
self.propertyDidChange('toolbar');
}
}
};
editor.registerKeyCommand(downKeyCommand);
const upKeyCommand = {
str: 'UP',
_ghostName: 'slashup',
run() {
let item = self.get('menuSelectedItem');
if(item > 0) {
self.set('menuSelectedItem', item - 1);
self.propertyDidChange('toolbar');
}
}
};
editor.registerKeyCommand(upKeyCommand);
const enterKeyCommand = {
str: 'ENTER',
_ghostName: 'slashdown',
run(postEditor) {
let range = postEditor.range;
range.head.offset = self._offset - 1;
postEditor.deleteRange(range);
self.get('selectedTool').onClick(self.get('editor'));
self.close();
}
};
editor.registerKeyCommand(enterKeyCommand);
const escapeKeyCommand = {
str: 'ESC',
_ghostName: 'slashesc',
run() {
self.close();
}
};
editor.registerKeyCommand(escapeKeyCommand);
},
close() {
this.isActive = false;
this.set('isVisible', false);
// note: below is using a mobiledoc Private API.
// there is no way to unregister a keycommand when it's registered so we have to remove it ourselves.
for( let i = this.editor._keyCommands.length-1; i > -1; i--) {
let keyCommand = this.editor._keyCommands[i];
if(keyCommand._ghostName === 'slashdown' || keyCommand._ghostName === 'slashup' || keyCommand._ghostName === 'slashenter'|| keyCommand._ghostName === 'slashesc') {
this.editor._keyCommands.splice(i,1);
}
}
return;
}
});

View file

@ -0,0 +1,26 @@
/**
* 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

@ -0,0 +1,87 @@
import Ember from '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(card_object) {
// if we have an array of cards then we convert them one by one.
if (card_object instanceof Array) {
return card_object.map(card => createCard(card));
}
// an ember card doesn't need a render or edit method
if (!card_object.name || (!card_object.willRender && card_object.genus !== 'ember')) {
throw new Error("A card must have a name and willRender method");
}
card_object.render = ({env, options, payload: _payload}) => {
//setupUI({env, options, payload});
// todo setup non ember UI
let payload = Ember.copy(_payload);
payload.card_name = env.name;
if (card_object.genus === 'ember') {
let card = setupEmberCard({env, options, payload}, "render");
let div = document.createElement('div');
div.id = card.id;
return div;
}
return card_object.willRender({env, options, payload});
};
card_object.edit = ({env, options, payload: _payload}) => {
//setupUI({env, options, payload});
let payload = Ember.copy(_payload);
payload.card_name = env.name;
if (card_object.genus === 'ember') {
let card = setupEmberCard({env, options, payload});
let div = document.createElement('div');
div.id = card.id;
return div;
}
if (card_object.hasOwnProperty('willRender')) {
return card_object.willEdit({env, options, payload, toolbar});
} else {
return card_object.willRender({env, options, payload, toolbar});
}
//do handle and delete stuff
};
card_object.type = 'dom';
card_object.didPlace = () => {
};
function setupEmberCard({env, options, payload}) {
const id = "GHOST_CARD_" + Ember.uuid();
let card = Ember.Object.create({
id,
env,
options,
payload,
card: card_object,
});
self.emberCards.pushObject(card);
env.onTeardown(() => {
self.emberCards.removeObject(card);
});
return card;
}
return card_object;
// self.editor.cards.push(card_object);
}
// then return the card factory so new cards can be made at runtime
return createCard;
}

View file

@ -0,0 +1,33 @@
/* global Showdown, html_sanitize*/
import {helper} from 'ember-helper';
import {htmlSafe} from 'ember-string';
import cajaSanitizers from './caja-sanitizers';
let showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight']});
export function formatMarkdown(params) {
if (!params || !params.length) {
return;
}
let markdown = params[0] || '';
let escapedhtml = '';
// convert markdown to HTML
escapedhtml = showdown.makeHtml(markdown);
// replace script and iFrame
escapedhtml = escapedhtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
'<pre class="js-embed-placeholder">Embedded JavaScript</pre>');
escapedhtml = escapedhtml.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi,
'<pre class="iframe-embed-placeholder">Embedded iFrame</pre>');
// sanitize html
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
escapedhtml = html_sanitize(escapedhtml, cajaSanitizers.url, cajaSanitizers.id);
// jscs:enable requireCamelCaseOrUpperCaseIdentifiers
return htmlSafe(escapedhtml);
}
export default helper(formatMarkdown);

View file

@ -0,0 +1,290 @@
import Component from 'ember-component';
import computed from 'ember-computed';
import injectService from 'ember-service/inject';
import {htmlSafe} from 'ember-string';
import {isBlank} from 'ember-utils';
import {isEmberArray} from 'ember-array/utils';
import run from 'ember-runloop';
import layout from '../../templates/components/image-card';
import {invokeAction} from 'ember-invoke-action';
//import ghostPaths from 'ghost-editor/utils/ghost-paths';
import {
isRequestEntityTooLargeError,
isUnsupportedMediaTypeError,
isVersionMismatchError,
UnsupportedMediaTypeError
} from 'ghost-editor/services/ajax';
export default Component.extend({
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: ['gif', 'jpg', 'jpeg', 'png', 'svg'],
validate: null,
dragClass: null,
failureMessage: null,
file: null,
formType: 'upload',
url: null,
uploadPercentage: 0,
ajax: injectService(),
config: injectService(),
notifications: injectService(),
// TODO: this wouldn't be necessary if the server could accept direct
// file uploads
formData: computed('file', function () {
const file = this.get('file');
let formData = new FormData();
formData.append('file', file);
return formData;
}),
description: computed('text', 'altText', function () {
const altText = this.get('altText');
return this.get('text') || (altText ? `Upload image of "${altText}"` : 'Upload an image');
}),
progressStyle: computed('uploadPercentage', function () {
const percentage = this.get('uploadPercentage');
let width = '';
if (percentage > 0) {
width = `${percentage}%`;
} else {
width = '0';
}
return htmlSafe(`width: ${width}`);
}),
canShowUploadForm: computed('config.fileStorage', function () {
return this.get('config.fileStorage') !== false;
}),
showUploadForm: computed('formType', function () {
const canShowUploadForm = this.get('canShowUploadForm');
let formType = this.get('formType');
return formType === 'upload' && canShowUploadForm;
}),
didReceiveAttrs() {
const image = this.get('payload');
this.set('url', image.img);
},
dragOver(event) {
const showUploadForm = this.get('showUploadForm');
if (!event.dataTransfer) {
return;
}
// this is needed to work around inconsistencies with dropping files
// from Chrome's downloads bar
let eA = event.dataTransfer.effectAllowed;
event.dataTransfer.dropEffect = (eA === 'move' || eA === 'linkMove') ? 'move' : 'copy';
event.stopPropagation();
event.preventDefault();
if (showUploadForm) {
this.set('dragClass', '-drag-over');
}
},
dragLeave(event) {
const showUploadForm = this.get('showUploadForm');
event.preventDefault();
if (showUploadForm) {
this.set('dragClass', null);
}
},
drop(event) {
const showUploadForm = this.get('showUploadForm');
event.preventDefault();
this.set('dragClass', null);
if (showUploadForm) {
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.errors && !isBlank(error.errors[0].message)) {
message = error.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);
xhr.addEventListener('error', event => console.log("error", event));
xhr.upload.addEventListener('error', event => console.log("errorupload", event));
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;
},
actions: {
fileSelected(fileList) {
// can't use array destructuring here as FileList is not a strict
// array and fails in Safari
// jscs:disable requireArrayDestructuring
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);
}
},
onInput(url) {
this.set('url', url);
invokeAction(this, 'onInput', url);
},
reset() {
this.set('file', null);
this.set('uploadPercentage', 0);
},
switchForm(formType) {
this.set('formType', formType);
run.scheduleOnce('afterRender', this, function () {
invokeAction(this, 'formChanged', formType);
});
},
saveUrl() {
let url = this.get('url');
invokeAction(this, 'update', url);
}
}
});

View file

@ -0,0 +1,3 @@
/**
* Created by ryanmccarvill on 2/11/16.
*/

View file

@ -0,0 +1,182 @@
import {replaceWithListSection, replaceWithHeaderSection} from 'mobiledoc-kit/editor/text-input-handlers';
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) {
const 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');
}
});
// soft return
const softReturnKeyCommand = {
str: 'SHIFT+ENTER',
run(editor) {
editor.run(postEditor => {
const mention = postEditor.builder.createAtom("soft-return");
postEditor.insertMarkers(editor.range.head, [mention]);
});
}
};
editor.registerKeyCommand(softReturnKeyCommand);
// inline matches
function matchStrongStar(editor, text) {
let range = editor.range;
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.range;
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.range;
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.range;
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.range;
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 range = editor.range;
let matches = text.match(/!\[(.*?)\]\((.*?)\)$/);
if(matches) {
let img = matches[2];
let alt = matches[1];
range = range.extend(-(matches[0].length));
editor.run(postEditor => {
let card = postEditor.builder.createCardSection('image-card', {pos: "top", img, alt});
postEditor.replaceSection(editor.range.headSection, card);
});
}
}
function matchStrikethrough(editor, text) {
let range = editor.range;
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 range = editor.range;
let matches = text.match(/```([\s\S]*?)```$/);
if(matches) {
let code = matches[0];
range = range.extend(-(matches[0].length));
editor.run(postEditor => {
let card = postEditor.builder.createCardSection('markdown-card', {pos: "top", markdown: code});
postEditor.replaceSection(editor.range.headSection, card);
});
}
}
}

View file

@ -0,0 +1,3 @@
/**
* Created by ryanmccarvill on 2/11/16.
*/

View file

@ -0,0 +1,295 @@
import Ember from 'ember';
export default function (editor, toolbar) {
return [
{
name: "h1",
icon: "",
label: "Heading One",
visibility: 'primary',
selected: false,
type: 'block',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleSection('h1');
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'h1').length > 0);
}
},
{
name: "h2",
label: "Heading Two",
icon: "",
selected: false,
type: 'block',
visibility: 'primary',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleSection('h2');
});
},
checkElements: function (elements) {
Ember.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: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'h3').length > 0);
}
},
{
name: "p",
label: "Paragraph",
icon: "paragraph.svg",
selected: false,
type: 'block',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleSection('p');
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'p').length > 0);
}
},
{
name: "blockquote",
label: "Block Quote",
icon: "quote.svg",
selected: false,
type: 'block',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleSection('blockquote');
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'blockquote').length > 0);
}
},
// {
// name: "pullquote",
// label: 'Pull Quote',
// icon: "pullquote.svg",
// selected: false,
// type: 'block',
// onClick: (editor) => {
// editor.run(postEditor => {
// postEditor.toggleSection('pull-quote');
// });
// },
// checkElements: function (elements) {
// Ember.set(this, "selected", elements.filter(element => element.tagName === 'pull-quote').length > 0);
// }
// },
{
name: "ul",
label: "List Unordered",
icon: "list-bullets.svg",
selected: false,
type: 'block',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleSection('ul');
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'ul').length > 0);
}
},
{
name: "ol",
label: "List Ordered",
icon: "list-number.svg",
selected: false,
type: 'block',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleSection('ol');
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'ol').length > 0);
}
},
{
name: "b",
label: "Bold",
icon: "bold.svg",
selected: false,
type: 'markup',
visibility: 'primary',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleMarkup('strong');
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'strong').length > 0);
}
},
{
name: "i",
label: "Italic",
icon: "italic.svg",
selected: false,
type: 'markup',
visibility: 'primary',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleMarkup('em');
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'em').length > 0);
}
},
{
name: "a",
label: "Link",
icon: "link.svg",
selected: false,
type: 'markup',
visibility: 'primary',
onClick: (editor) => {
//editor.run(postEditor => {
// let range = window.getSelection().getRangeAt(0).cloneRange();
//toolbar.set('isLink', true);
//toolbar.set('linkRange', );
//toolbar.$('input').focus();
toolbar.doLink(editor.range);
//});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'a').length > 0);
}
},
{
name: "u",
label: "Underline",
icon: "underline.svg",
selected: false,
type: 'markup',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleMarkup('u');
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'u').length > 0);
}
},
{
name: "s",
label: "Strikethrough",
icon: "strikethrough.svg",
selected: false,
type: 'markup',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleMarkup('s');
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 's').length > 0);
}
},
/*{
name: "sub",
label: "Subscript",
icon: "subscript.svg",
selected: false,
type: 'markup',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleMarkup('sub');
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'sub').length > 0);
}
},
{
name: "sup",
label: "Superscript",
icon: "superscript.svg",
selected: false,
type: 'markup',
onClick: (editor) => {
editor.run(postEditor => {
postEditor.toggleMarkup('sup');
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'sup').length > 0);
}
},*/
{
name: "img",
label: "Image",
selected: false,
type: 'card',
icon: 'file-picture-add.svg',
visibility: "primary",
onClick: (editor) => {
editor.run(postEditor => {
let card = postEditor.builder.createCardSection('image-card', {pos: "top"});
postEditor.replaceSection(editor.range.headSection, card);
});
},
checkElements: function (elements) {
Ember.set(this, "selected", elements.filter(element => element.tagName === 'sup').length > 0);
}
},
{
name: "html",
label: "Embed HTML",
selected: false,
type: 'card',
icon: 'html-five.svg',
visibility: "primary",
onClick: (editor) => {
editor.run(postEditor => {
let card = postEditor.builder.createCardSection('html-card', {pos: "top"});
postEditor.replaceSection(editor.range.headSection, card);
});
},
checkElements: function () {
}
},
{
name: "md",
label: "Embed Markdown",
selected: false,
type: 'card',
visibility: "primary",
icon: 'file-code-1.svg',
onClick: (editor) => {
editor.run(postEditor => {
let card = postEditor.builder.createCardSection('markdown-card', {pos: "top"});
postEditor.replaceSection(editor.range.headSection, card);
});
},
checkElements: function () {
}
}
];
}

View file

@ -0,0 +1,223 @@
// TODO - this is a ghost component, it should be kept in Ghost and the neccersary options passed to the editor.
import get from 'ember-metal/get';
import computed from 'ember-computed';
import injectService from 'ember-service/inject';
import {isEmberArray} from 'ember-array/utils';
import {isNone} from 'ember-utils';
import AjaxService from 'ember-ajax/services/ajax';
import {AjaxError, isAjaxError} from 'ember-ajax/errors';
let config = { APP : { version : '1.0.0' }}; // TODO get this from GHOST
const JSONContentType = 'application/json';
function isJSONContentType(header) {
if (!header || isNone(header)) {
return false;
}
return header.indexOf(JSONContentType) === 0;
}
/* Version mismatch error */
export function VersionMismatchError(errors) {
AjaxError.call(this, errors, 'API server is running a newer version of Ghost, please upgrade.');
}
VersionMismatchError.prototype = Object.create(AjaxError.prototype);
export function isVersionMismatchError(errorOrStatus, payload) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof VersionMismatchError;
} else {
return get(payload || {}, 'errors.firstObject.errorType') === 'VersionMismatchError';
}
}
/* Request entity too large error */
export function ServerUnreachableError(errors) {
AjaxError.call(this, errors, 'Server was unreachable');
}
ServerUnreachableError.prototype = Object.create(AjaxError.prototype);
export function isServerUnreachableError(error) {
if (isAjaxError(error)) {
return error instanceof ServerUnreachableError;
} else {
return error === 0 || error === '0';
}
}
export function RequestEntityTooLargeError(errors) {
AjaxError.call(this, errors, 'Request is larger than the maximum file size the server allows');
}
RequestEntityTooLargeError.prototype = Object.create(AjaxError.prototype);
export function isRequestEntityTooLargeError(errorOrStatus) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof RequestEntityTooLargeError;
} else {
return errorOrStatus === 413;
}
}
/* Unsupported media type error */
export function UnsupportedMediaTypeError(errors) {
AjaxError.call(this, errors, 'Request contains an unknown or unsupported file type.');
}
UnsupportedMediaTypeError.prototype = Object.create(AjaxError.prototype);
export function isUnsupportedMediaTypeError(errorOrStatus) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof UnsupportedMediaTypeError;
} else {
return errorOrStatus === 415;
}
}
/* Maintenance error */
export function MaintenanceError(errors) {
AjaxError.call(this, errors, 'Ghost is currently undergoing maintenance, please wait a moment then retry.');
}
MaintenanceError.prototype = Object.create(AjaxError.prototype);
export function isMaintenanceError(errorOrStatus) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof MaintenanceError;
} else {
return errorOrStatus === 503;
}
}
/* Theme validation error */
export function ThemeValidationError(errors) {
AjaxError.call(this, errors, 'Theme is not compatible or contains errors.');
}
ThemeValidationError.prototype = Object.create(AjaxError.prototype);
export function isThemeValidationError(errorOrStatus, payload) {
if (isAjaxError(errorOrStatus)) {
return errorOrStatus instanceof ThemeValidationError;
} else {
return get(payload || {}, 'errors.firstObject.errorType') === 'ThemeValidationError';
}
}
/* end: custom error types */
let ajaxService = AjaxService.extend({
session: injectService(),
headers: computed('session.isAuthenticated', function () {
let session = this.get('session');
let headers = {};
headers['X-Ghost-Version'] = config.APP.version;
if (session.get('isAuthenticated')) {
session.authorize('authorizer:oauth2', (headerName, headerValue) => {
headers[headerName] = headerValue;
});
}
return headers;
}).volatile(),
// ember-ajax recognises `application/vnd.api+json` as a JSON-API request
// and formats appropriately, we want to handle `application/json` the same
_makeRequest(hash) {
if (isJSONContentType(hash.contentType) && hash.type !== 'GET') {
if (typeof hash.data === 'object') {
hash.data = JSON.stringify(hash.data);
}
}
return this._super(...arguments);
},
handleResponse(status, headers, payload) {
if (this.isVersionMismatchError(status, headers, payload)) {
return new VersionMismatchError(payload.errors);
} else if (this.isServerUnreachableError(status, headers, payload)) {
return new ServerUnreachableError(payload.errors);
} else if (this.isRequestEntityTooLargeError(status, headers, payload)) {
return new RequestEntityTooLargeError(payload.errors);
} else if (this.isUnsupportedMediaTypeError(status, headers, payload)) {
return new UnsupportedMediaTypeError(payload.errors);
} else if (this.isMaintenanceError(status, headers, payload)) {
return new MaintenanceError(payload.errors);
} else if (this.isThemeValidationError(status, headers, payload)) {
return new ThemeValidationError(payload.errors);
}
// TODO: we may want to check that we are hitting our own API before
// logging the user out due to a 401 response
if (this.isUnauthorizedError(status, headers, payload) && this.get('session.isAuthenticated')) {
this.get('session').invalidate();
}
return this._super(...arguments);
},
normalizeErrorResponse(status, headers, payload) {
if (payload && typeof payload === 'object') {
let errors = payload.error || payload.errors || payload.message || undefined;
if (errors) {
if (!isEmberArray(errors)) {
errors = [errors];
}
payload.errors = errors.map(function(error) {
if (typeof error === 'string') {
return {message: error};
} else {
return error;
}
});
}
}
return this._super(status, headers, payload);
},
isVersionMismatchError(status, headers, payload) {
return isVersionMismatchError(status, payload);
},
isServerUnreachableError(status/*, headers, payload */) {
return isServerUnreachableError(status);
},
isRequestEntityTooLargeError(status/*, headers, payload */) {
return isRequestEntityTooLargeError(status);
},
isUnsupportedMediaTypeError(status/*, headers, payload */) {
return isUnsupportedMediaTypeError(status);
},
isMaintenanceError(status, headers, payload) {
return isMaintenanceError(status, payload);
},
isThemeValidationError(status, headers, payload) {
return isThemeValidationError(status, payload);
}
});
// we need to reopen so that internal methods use the correct contentType
ajaxService.reopen({
contentType: 'application/json; charset=UTF-8'
});
export default ajaxService;

View file

@ -0,0 +1,24 @@
{{component
card.card.name
env=card.env
payload=card.payload
options=card.options
apiRoot=apiRoot
assetPath=assetPath
doSave=doSave
isEditing=isEditing
}}
<div class="card-handle">
{{#if card.card.buttons.preview}}
<button {{action "toggleState"}}>
{{#if isEditing}}
Edit
{{else}}
Preview
{{/if}}
</button>
{{/if}}
{{#if card.card.buttons.save}}
<button {{action "save"}}>Save</button>
{{/if}}
</div>

View file

@ -0,0 +1,14 @@
{{#each emberCards as |card|}}
{{#ember-wormhole to=card.id}}
{{ghost-card card=card apiRoot=apiRoot assetPath=assetPath}}
{{/ember-wormhole}}
{{/each}}
<div class='ghost-editor'>
<div class='surface' tabindex="{{tabindex}}"/>
</div>
{{yield}}
{{ghost-toolbar-blockitem editor=editor assetPath=assetPath}}
{{ghost-toolbar editor=editor assetPath=assetPath}}
{{slash-menu editor=editor assetPath=assetPath}}

View file

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

View file

@ -0,0 +1,8 @@
<button {{action "click"}}>
{{#if tool.icon}}
<img src="{{iconURL}}{{tool.icon}}" />
{{else}}
{{tool.name}}
{{/if}}
<label>{{tool.label}}</label>
</button>

View file

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

View file

@ -0,0 +1,9 @@
{{#if isLink}}
{{input keyDown=(action "linkKeyDown") keyPress=(action "linkKeyPress") autofocus=true placeholder="Enter a link"}}
{{else}}
<ul>
{{#each toolbar as |tool|}}
{{ghost-toolbar-button tool=tool editor=editor iconURL=iconURL assetPath=assetPath}}
{{/each}}
</ul>
{{/if}}

View file

@ -0,0 +1,5 @@
{{#if isEditing}}
{{{value}}}
{{else}}
{{gh-cm-editor value update=(action (mut value))}} {{!-- codemirror editor component from Ghost-Admin --}}
{{/if}}

View file

@ -0,0 +1,46 @@
{{#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}}
{{#if showUploadForm}}
{{!-- file selection/drag-n-drop --}}
<div class="upload-form">
{{#gh-file-input multiple=false alt=description action=(action 'fileSelected') accept=accept}}
<div class="description">{{description}}</div>
{{/gh-file-input}}
</div>
<a class="image-url" {{action 'switchForm' 'url-input'}}>
<i class="icon-link"><span class="hidden">URL</span></i>
</a>
{{else}}
{{!-- URL input --}}
<form class="url-form">
{{gh-input url class="url" placeholder="http://" update=(action "onInput") onenter=(action "saveUrl")}}
{{#if saveButton}}
<button class="btn btn-blue gh-input" {{action 'saveUrl'}}>Save</button>
{{else}}
<div class="description">{{description}}</div>
{{/if}}
</form>
{{#if canShowUploadForm}}
<a class="image-upload icon-photos" title="Add image" {{action 'switchForm' 'upload'}}>
<span class="hidden">Upload</span>
</a>
{{/if}}
{{/if}}
{{/if}}

View file

@ -0,0 +1,5 @@
{{#if isEditing}}
{{{preview}}}
{{else}}
{{textarea value=value key-up="updateValue"}}
{{/if}}

View file

@ -0,0 +1,20 @@
{{#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

@ -0,0 +1,5 @@
<ul>
{{#each toolbar as |tool index|}}
{{slash-menu-item tool=tool iconURL=iconURL editor=editor range=range selected=tool.selected}}
{{/each}}
</ul>

View file

View file

@ -0,0 +1 @@
export {default} from 'ghost-editor/components/gh-file-input';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
export { default } from 'ghost-editor/components/slash-menu-item';

View file

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

View file

@ -0,0 +1 @@
export { default, ifEquals } from 'ghost-editor/helpers/if-equals';

View file

View file

@ -0,0 +1,11 @@
{
"name": "ghost-editor",
"dependencies": {
"ember": "~1.13.0",
"ember-cli-shims": "0.1.1",
"codemirror": "~5.15.0"
},
"resolutions": {
"ember": "~1.13.0"
}
}

View file

@ -0,0 +1,55 @@
/*jshint node:true*/
module.exports = {
scenarios: [
{
name: 'default',
bower: {
dependencies: {}
}
},
{
name: 'ember-1.13',
bower: {
dependencies: {
'ember': '~1.13.0'
},
resolutions: {
'ember': '~1.13.0'
}
}
},
{
name: 'ember-release',
bower: {
dependencies: {
'ember': 'components/ember#release'
},
resolutions: {
'ember': 'release'
}
}
},
{
name: 'ember-beta',
bower: {
dependencies: {
'ember': 'components/ember#beta'
},
resolutions: {
'ember': 'beta'
}
}
},
{
name: 'ember-canary',
bower: {
dependencies: {
'ember': 'components/ember#canary'
},
resolutions: {
'ember': 'canary'
}
}
}
]
};

View file

@ -0,0 +1,6 @@
/*jshint node:true*/
'use strict';
module.exports = function (/* environment, appConfig */) {
return {};
};

View file

@ -0,0 +1,18 @@
/*jshint node:true*/
/* global require, module */
var EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
module.exports = function (defaults) {
var app = new EmberAddon(defaults, {
// Add options here
});
/*
This build file specifies the options for the dummy test app of this
addon, located in `/tests/dummy`
This build file does *not* influence how the addon or the app using it
behave. You most likely want to be modifying `./index.js` or app's build file
*/
return app.toTree();
};

67
lib/ghost-editor/index.js Normal file
View file

@ -0,0 +1,67 @@
/* jshint node: true */
var MergeTrees = require('broccoli-merge-trees');
var Funnel = require('broccoli-funnel');
var path = require('path');
var cards = require('./addon/cards/common.js');
module.exports = {
name: 'ghost-editor',
treeForVendor: function () {
var files = [];
var MOBILEDOC_DIST_DIRECTORY = path.join(path.dirname(
require.resolve(path.join('mobiledoc-kit', 'package.json'))), 'dist');
files.push(new Funnel(MOBILEDOC_DIST_DIRECTORY, {
files: [
'amd/mobiledoc-kit.js',
'amd/mobiledoc-kit.map'
],
destDir: 'mobiledoc-kit'
}));
return MergeTrees(files, 'assets');
},
treeForPublic: function () {
return new Funnel(__dirname + '/public/tools/', {
destDir: 'assets/tools/'
});
},
included: function (app) {
// app.import('app/styles/globals.css');
app.import('vendor/mobiledoc-kit/amd/mobiledoc-kit.js');
// app.import('app/styles/ghost-editor.css');
// app.import('app/styles/ghost-toolbar.css');
// app.import('app/styles/ghost-toolbar-blockitem.css');
// app.import('app/styles/slash-menu.css');
},
// temp
htmlOptions:
{
cards: cards.html,
atoms: [{
name: 'soft-return',
type: 'html',
render: function() {
return "<br />";
}
}
]
}
/*
[
{
name: 'html-card',
type: 'html',
render: function(opts) {
return opts.payload.html;
}
}
]
*/
};

View file

@ -0,0 +1,68 @@
{
"name": "ghost-editor",
"version": "0.1.10",
"description": "A mobiledoc-kit based editor for Ghost. WARNING DO NOT USE!",
"directories": {
"doc": "doc",
"test": "tests"
},
"scripts": {
"build": "ember build",
"start": "ember server",
"test": "ember try:each"
},
"repository": "",
"engines": {
"node": ">= 0.10.0"
},
"author": {
"name": "ryan@ghost.org"
},
"license": "MIT",
"devDependencies": {
"broccoli-asset-rev": "^2.4.2",
"ember-ajax": "^2.0.1",
"ember-cli": "^2.9.0-beta.1",
"ember-cli-app-version": "^1.0.0",
"ember-cli-htmlbars": "^1.0.3",
"ember-cli-htmlbars-inline-precompile": "^0.3.1",
"ember-cli-inject-live-reload": "^1.4.0",
"ember-cli-jshint": "^1.0.0",
"ember-cli-qunit": "^2.1.0",
"ember-cli-release": "^0.2.9",
"ember-cli-sri": "^2.1.0",
"ember-cli-test-loader": "^1.1.0",
"ember-cli-uglify": "^1.2.0",
"ember-data": "^2.8.0",
"ember-disable-prototype-extensions": "^1.1.0",
"ember-export-application-global": "^1.0.5",
"ember-invoke-action": "1.4.0",
"ember-load-initializers": "^0.5.1",
"ember-resolver": "^2.0.3",
"ember-wormhole": "0.4.1",
"emberx-file-input": "1.1.0",
"ivy-codemirror": "2.0.2",
"loader.js": "^4.0.1",
"showdown-ghost": "0.3.6"
},
"keywords": [
"ember-addon"
],
"dependencies": {
"broccoli-funnel": "^1.0.7",
"broccoli-merge-trees": "^1.1.4",
"ember-cli-babel": "^5.1.6",
"ember-cli-htmlbars": "^1.1.0",
"showdown-ghost": "^0.4.0",
"mobiledoc-kit": "^0.10.13"
},
"ember-addon": {
"configPath": "tests/dummy/config"
},
"readme": "# Ghost-editor\n\nThis is the new mobiledoc editor for Ghost, it's very much a WIP so **PLEASE DO NOT USE!**\n\n",
"readmeFilename": "README.md",
"gitHead": "69ea8886e6d63202c267f37baa3ffa98ae65f3e4",
"_id": "ghost-editor@0.0.2",
"_shasum": "32cc128a935a22510e421f2beaa20314edfd4560",
"_from": "ghost-editor@latest"
}

View file

@ -0,0 +1,347 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: -20px;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3 {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
/* Hack to make IE7 behave */
*zoom:1;
*display:inline;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
overflow: auto;
}
.CodeMirror-widget {}
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background: #ffa;
background: rgba(255, 255, 0, .4);
}
/* IE7 hack to prevent it from returning funny offsetTops on the spans */
.CodeMirror span { *vertical-align: text-bottom; }
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 447 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 466 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 740 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 745 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 810 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 305 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 702 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 364 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 689 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 696 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 675 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 819 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 820 B

View file

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 442 B

View file

@ -0,0 +1,13 @@
/*jshint node:true*/
module.exports = {
"framework": "qunit",
"test_page": "tests/index.html?hidepassed",
"phantomjs_debug_port": 9000,
"disable_watching": true,
"launch_in_ci": [
"chrome"
],
"launch_in_dev": [
"chrome"
]
};

View file

@ -0,0 +1,52 @@
{
"predef": [
"document",
"window",
"location",
"setTimeout",
"$",
"-Promise",
"define",
"console",
"visit",
"exists",
"fillIn",
"click",
"keyEvent",
"triggerEvent",
"find",
"findWithAssert",
"wait",
"DS",
"andThen",
"currentURL",
"currentPath",
"currentRouteName"
],
"node": false,
"browser": false,
"boss": true,
"curly": true,
"debug": false,
"devel": false,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"esversion": 6,
"unused": true
}

View file

@ -0,0 +1,18 @@
import Ember from 'ember';
import Resolver from './resolver';
import loadInitializers from 'ember-load-initializers';
import config from './config/environment';
let App;
Ember.MODEL_FACTORY_INJECTIONS = true;
App = Ember.Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver
});
loadInitializers(App, config.modulePrefix);
export default App;

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