diff --git a/ghost/admin/app/components/gh-title.js b/ghost/admin/app/components/gh-title.js
new file mode 100644
index 0000000000..f4c23747bf
--- /dev/null
+++ b/ghost/admin/app/components/gh-title.js
@@ -0,0 +1,95 @@
+import Component from 'ember-component';
+
+export default Component.extend({
+ _cachedValue: '',
+ _mutationObserver: null,
+ tagName: 'h2',
+ didRender() {
+ if (this._rendered) {
+ return;
+ }
+
+ let title = this.$('div');
+ if (!this.get('value')) {
+ title.addClass('no-content');
+ }
+ title[0].onkeydown = (event) => {
+ // block the browser format keys.
+ if (event.ctrlKey || event.metaKey) {
+ switch (event.keyCode) {
+ case 66: // B
+ case 98: // b
+ case 73: // I
+ case 105: // i
+ case 85: // U
+ case 117: // u
+ return false;
+ }
+ }
+ if (event.keyCode === 13) {
+ // enter
+ return false;
+ }
+
+ // down key
+ // if we're within ten pixels of the bottom of this element then we try and figure out where to position
+ // the cursor in the editor.
+ if (event.keyCode === 40) {
+ let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM.
+ let cursorPositionOnScreen = range.getBoundingClientRect();
+ let offset = title.offset();
+ let bottomOfHeading = offset.top + title.height();
+ if (cursorPositionOnScreen.bottom > bottomOfHeading - 33) {
+ let {editor} = window; // hmmm, this is nasty!
+ // We need to pass the editor instance so that we can `this.get('editor');`
+ // but the editor instance is within the component and not exposed.
+ // there's also a dependency that the editor will have with the title and the title will have with the editor
+ // so that the cursor can move both ways (up and down) between them.
+ // see `lib/gh-koenig/addon/gh-koenig.js` and the function `findCursorPositionFromPixel` which should actually be
+ // encompassed here.
+ let loc = editor.element.getBoundingClientRect();
+
+ let cursorPositionInEditor = editor.positionAtPoint(cursorPositionOnScreen.left, loc.top);
+
+ if (cursorPositionInEditor.isBlank) {
+ editor.element.focus();
+ } else {
+ editor.selectRange(cursorPositionInEditor.toRange());
+ }
+ return false;
+ }
+ }
+ title.removeClass('no-content');
+ };
+
+ // setup mutation observer
+ let mutationObserver = new MutationObserver(() => {
+ // on mutate we update.
+ if (title[0].textContent !== '') {
+ title.removeClass('no-content');
+ } else {
+ title.addClass('no-content');
+ }
+
+ // sanity check if there is formatting reset it.
+ let {textContent} = title[0]; // eslint-disable-line
+ if (title[0].innerHTML !== textContent && title[0].innerHTML) {
+ title[0].innerHTML = textContent;
+ // todo: retain the range position.
+ }
+
+ if (this.get('_cachedValue') !== textContent) {
+ this.set('_cacheValue', textContent);
+ this.sendAction('onChange', textContent);
+ this.sendAction('update', textContent);
+ }
+ });
+
+ mutationObserver.observe(title[0], {childList: true, characterData: true, subtree: true});
+ this.set('_mutationObserver', mutationObserver);
+ this.set('_rendered', true);
+ },
+ willDestroyElement() {
+ this.get('_mutationObserver').disconnect();
+ }
+});
diff --git a/ghost/admin/app/styles/app.css b/ghost/admin/app/styles/app.css
index 1d76f2220f..059a985393 100644
--- a/ghost/admin/app/styles/app.css
+++ b/ghost/admin/app/styles/app.css
@@ -31,7 +31,7 @@
@import "components/selectize.css";
@import "components/power-select.css";
@import "components/publishmenu.css";
-
+@import "components/title.css";
/* Layouts: Groups of Components
/* ---------------------------------------------------------- */
diff --git a/ghost/admin/app/styles/components/title.css b/ghost/admin/app/styles/components/title.css
new file mode 100644
index 0000000000..273dec40a1
--- /dev/null
+++ b/ghost/admin/app/styles/components/title.css
@@ -0,0 +1,24 @@
+.gh-title {
+ padding:0 0 0 1px; /* need some left padding otherwise the cursor isn't visible on the left hand side */
+ margin:0;
+ outline:none;
+ position:relative;
+ width:100%;
+ letter-spacing:0.8px;
+ font-weight: bold;
+ font-size: 3.2rem;
+ line-height: 3.2em;
+}
+
+/* Place holder content that displays in the title if it is empty */
+.gh-title.no-content:after {
+ content: attr(data-placeholder);
+ color: #bbb;
+ cursor: text;
+ position:absolute;
+ top:0;
+ font-size: 3.2rem;
+ line-height: 3.2em;
+ font-weight: bold;
+ min-width: 30rem; /* hack it's defaulting just to enough width for the 'Your' in 'Your Post Title' */
+}
diff --git a/ghost/admin/app/styles/layouts/main.css b/ghost/admin/app/styles/layouts/main.css
index 0c6b492971..f49f51aae0 100644
--- a/ghost/admin/app/styles/layouts/main.css
+++ b/ghost/admin/app/styles/layouts/main.css
@@ -489,7 +489,6 @@ body > .ember-view:not(.default-liquid-destination) {
margin: 0;
padding: 0;
text-overflow: ellipsis;
- white-space: nowrap;
font-size: 2rem;
line-height: 1.2em;
font-weight: 400;
diff --git a/ghost/admin/app/templates/components/gh-title.hbs b/ghost/admin/app/templates/components/gh-title.hbs
new file mode 100644
index 0000000000..305485a30d
--- /dev/null
+++ b/ghost/admin/app/templates/components/gh-title.hbs
@@ -0,0 +1 @@
+
{{value}}
\ No newline at end of file
diff --git a/ghost/admin/app/templates/editor/edit.hbs b/ghost/admin/app/templates/editor/edit.hbs
index e39d5546aa..5af5b8e622 100644
--- a/ghost/admin/app/templates/editor/edit.hbs
+++ b/ghost/admin/app/templates/editor/edit.hbs
@@ -34,8 +34,29 @@
diff --git a/ghost/admin/lib/gh-koenig/addon/components/gh-koenig.js b/ghost/admin/lib/gh-koenig/addon/components/gh-koenig.js
index 122c122bac..e4952cef98 100644
--- a/ghost/admin/lib/gh-koenig/addon/components/gh-koenig.js
+++ b/ghost/admin/lib/gh-koenig/addon/components/gh-koenig.js
@@ -7,6 +7,7 @@ import {MOBILEDOC_VERSION} from 'mobiledoc-kit/renderers/mobiledoc';
import createCardFactory from '../lib/card-factory';
import defaultCommands from '../options/default-commands';
import editorCards from '../cards/index';
+import $ from 'jquery';
// import { VALID_MARKUP_SECTION_TAGNAMES } from 'mobiledoc-kit/models/markup-section'; //the block elements supported by mobile-doc
export const BLANK_DOC = {
@@ -117,6 +118,30 @@ export default Component.extend({
this.editor.cursorDidChange(() => this.cursorMoved());
+ // hack to track key up to focus back on the title when the up key is pressed
+ this.editor.element.addEventListener('keydown', (event) => {
+ if (event.keyCode === 38) {
+ let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM.
+ let cursorPositionOnScreen = range.getBoundingClientRect();
+ let topOfEditor = this.editor.element.getBoundingClientRect().top;
+ if (cursorPositionOnScreen.top < topOfEditor + 33) {
+ let $title = $(this.titleQuery);
+
+ // let offset = findCursorPositionFromPixel($title[0].firstChild, cursorPositionOnScreen.left);
+
+ // let newRange = document.createRange();
+ // newRange.collapse(true);
+ // newRange.setStart($title[0].firstChild, offset);
+ // newRange.setEnd($title[0].firstChild, offset);
+ // updateCursor(newRange);
+
+ $title[0].focus();
+
+ return false;
+ }
+ }
+ });
+
},
// drag and drop images onto the editor
@@ -149,3 +174,33 @@ export default Component.extend({
}
});
+
+// // code for moving the cursor into the correct position of the title: (is buggy)
+
+// // find the cursor position based on a pixel offset of an element.
+// // used to move the cursor vertically into the title.
+// function findCursorPositionFromPixel(el, horizontal_offset) {
+// let len = el.textContent.length;
+// let range = document.createRange();
+// for(let i = len -1; i > -1; i--) {
+// range.setStart(el, i);
+// range.setEnd(el, i + 1);
+// let rect = range.getBoundingClientRect();
+// if (rect.top === rect.bottom) {
+// continue;
+// }
+// if(rect.left <= horizontal_offset && rect.right >= horizontal_offset) {
+// return i + (horizontal_offset >= (rect.left + rect.right) / 2 ? 1 : 0); // if the horizontal_offset is on the left hand side of the
+// // character then return `i`, if it's on the right return `i + 1`
+// }
+// }
+
+// return el.length;
+// }
+
+// // update the cursor position.
+// function updateCursor(range) {
+// let selection = window.getSelection();
+// selection.removeAllRanges();
+// selection.addRange(range);
+// }
diff --git a/ghost/admin/tests/acceptance/editor-test.js b/ghost/admin/tests/acceptance/editor-test.js
index 81af1ab964..2858e3579d 100644
--- a/ghost/admin/tests/acceptance/editor-test.js
+++ b/ghost/admin/tests/acceptance/editor-test.js
@@ -12,7 +12,7 @@ import {invalidateSession, authenticateSession} from 'ghost-admin/tests/helpers/
import Mirage from 'ember-cli-mirage';
import sinon from 'sinon';
import testSelector from 'ember-test-selectors';
-
+import {titleRendered} from '../helpers/editor-helpers';
describe('Acceptance: Editor', function() {
let application;
@@ -25,7 +25,7 @@ describe('Acceptance: Editor', function() {
});
it('redirects to signin when not authenticated', function () {
- server.create('user'); // necessray for post-author association
+ server.create('user'); // necesary for post-author association
server.create('post');
invalidateSession(application);
@@ -397,10 +397,18 @@ describe('Acceptance: Editor', function() {
.to.equal('/editor/1');
});
- // Test title validation
- fillIn('input[id="entry-title"]', Array(160).join('a'));
- triggerEvent('input[id="entry-title"]', 'blur');
- click('.gh-btn.gh-btn-sm.js-publish-button');
+ andThen(() => {
+ titleRendered();
+ });
+
+ andThen(() => {
+ let title = find('#gh-title div');
+ title.html(Array(160).join('a'));
+ });
+
+ andThen(() => {
+ click('.gh-btn.gh-btn-sm.js-publish-button');
+ });
andThen(() => {
expect(
diff --git a/ghost/admin/tests/helpers/editor-helpers.js b/ghost/admin/tests/helpers/editor-helpers.js
index f000b3fd80..4dfbe01fd4 100644
--- a/ghost/admin/tests/helpers/editor-helpers.js
+++ b/ghost/admin/tests/helpers/editor-helpers.js
@@ -15,6 +15,21 @@ export function editorRendered() {
});
}
+// polls the title until it's started.
+export function titleRendered() {
+ return Ember.Test.promise(function (resolve) { // eslint-disable-line
+ function checkTitle() {
+ let title = $('#gh-title div');
+ if (title[0]) {
+ return resolve();
+ } else {
+ window.requestAnimationFrame(checkTitle);
+ }
+ }
+ checkTitle();
+ });
+}
+
// simulates text inputs into the editor, unfortunately the helper Ember helper functions
// don't work on content editable so we have to manipuate the text input event manager
// in mobiledoc-kit directly. This is a private API.
diff --git a/ghost/admin/tests/integration/components/gh-title-test.js b/ghost/admin/tests/integration/components/gh-title-test.js
new file mode 100644
index 0000000000..9207f548d1
--- /dev/null
+++ b/ghost/admin/tests/integration/components/gh-title-test.js
@@ -0,0 +1,24 @@
+import {expect} from 'chai';
+import {describe, it} from 'mocha';
+import {setupComponentTest} from 'ember-mocha';
+import hbs from 'htmlbars-inline-precompile';
+
+describe('Integration | Component | gh title', function() {
+ setupComponentTest('gh-title', {
+ integration: true
+ });
+
+ it('renders', function() {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.on('myAction', function(val) { ... });
+ // Template block usage:
+ // this.render(hbs`
+ // {{#gh-title}}
+ // template content
+ // {{/gh-title}}
+ // `);
+
+ this.render(hbs`{{gh-title}}`);
+ expect(this.$()).to.have.length(1);
+ });
+});