mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 07:46:06 +08:00
fix(contenteditable): Fix nested list behavior:
Summary: - Fixes #862 - Makes it behave like hackpad -- adding nested lists will indent the current list item - Fix lint errors Test Plan: - Integration tests Reviewers: evan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2402
This commit is contained in:
parent
b452be933a
commit
a8e771c033
|
@ -1,14 +1,14 @@
|
|||
import N1Launcher from './helpers/n1-launcher'
|
||||
import ContenteditableTestHarness from './helpers/contenteditable-test-harness.es6'
|
||||
import N1Launcher from './helpers/n1-launcher';
|
||||
import ContenteditableTestHarness from './helpers/contenteditable-test-harness.es6';
|
||||
|
||||
describe('Contenteditable Integration Spec', function() {
|
||||
fdescribe('Contenteditable Integration Spec', function() {
|
||||
beforeAll((done)=>{
|
||||
this.app = new N1Launcher(["--dev"]);
|
||||
this.app = new N1Launcher(['--dev']);
|
||||
this.app.popoutComposerWindowReady().finally(done);
|
||||
});
|
||||
|
||||
beforeEach((done) => {
|
||||
this.ce = new ContenteditableTestHarness(this.app.client)
|
||||
this.ce = new ContenteditableTestHarness(this.app.client);
|
||||
this.ce.init().finally(done);
|
||||
});
|
||||
|
||||
|
@ -16,100 +16,117 @@ describe('Contenteditable Integration Spec', function() {
|
|||
if (this.app && this.app.isRunning()) {
|
||||
this.app.stop().then(done);
|
||||
} else {
|
||||
done()
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Manipulating Lists', () => {
|
||||
it("Creates ordered lists", (done)=> {
|
||||
it('Creates ordered lists', (done)=> {
|
||||
this.ce.test({
|
||||
keys: ["1", ".", "Space"],
|
||||
expectedHTML: "<ol><li></li></ol>",
|
||||
keys: ['1', '.', 'Space'],
|
||||
expectedHTML: '<ol><li></li></ol>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {node: dom.querySelectorAll('li')[0]} }
|
||||
}).then(done).catch(done.fail)
|
||||
return {
|
||||
node: dom.querySelectorAll('li')[0],
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it('Undoes ordered list creation with backspace', (done) => {
|
||||
this.ce.test({
|
||||
keys: ["1", ".", "Space", "Back space"],
|
||||
expectedHTML: "1. <br>",
|
||||
keys: ['1', '.', 'Space', 'Back space'],
|
||||
expectedHTML: '1. <br>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {node: dom.childNodes[0], offset: 3} }
|
||||
}).then(done).catch(done.fail)
|
||||
return {
|
||||
node: dom.childNodes[0], offset: 3,
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("Creates unordered lists with star", (done) => {
|
||||
it('Creates unordered lists with star', (done) => {
|
||||
this.ce.test({
|
||||
keys: ['*', 'Space'],
|
||||
expectedHTML: "<ul><li></li></ul>",
|
||||
expectedHTML: '<ul><li></li></ul>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {node: dom.querySelectorAll("li")[0] } }
|
||||
}).then(done).catch(done.fail)
|
||||
return {
|
||||
node: dom.querySelectorAll('li')[0],
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("Undoes unordered list creation with backspace", (done) => {
|
||||
it('Undoes unordered list creation with backspace', (done) => {
|
||||
this.ce.test({
|
||||
keys: ['*', 'Space', 'Back space'],
|
||||
expectedHTML: "* <br>",
|
||||
expectedHTML: '* <br>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {node: dom.childNodes[0], offset: 2} }
|
||||
}).then(done).catch(done.fail)
|
||||
return {
|
||||
node: dom.childNodes[0], offset: 2,
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("Creates unordered lists with dash", (done) => {
|
||||
it('Creates unordered lists with dash', (done) => {
|
||||
this.ce.test({
|
||||
keys: ['-', 'Space'],
|
||||
expectedHTML: "<ul><li></li></ul>",
|
||||
expectedHTML: '<ul><li></li></ul>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {node: dom.querySelectorAll("li")[0] } }
|
||||
}).then(done).catch(done.fail)
|
||||
return {
|
||||
node: dom.querySelectorAll('li')[0],
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("Undoes unordered list creation with backspace", (done) => {
|
||||
it('Undoes unordered list creation with backspace', (done) => {
|
||||
this.ce.test({
|
||||
keys: ['-', 'Space', 'Back space'],
|
||||
expectedHTML: "- <br>",
|
||||
expectedHTML: '- <br>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {node: dom.childNodes[0], offset: 2} }
|
||||
}).then(done).catch(done.fail)
|
||||
return {
|
||||
node: dom.childNodes[0], offset: 2,
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('When creating two items in a list', () => {
|
||||
beforeEach(() => {
|
||||
this.twoItemUl = ['-', 'Space', 'a', 'Return', 'b']
|
||||
this.twoItemOl = ['1', ".", 'Space', 'a', 'Return', 'b']
|
||||
this.twoItemUl = ['-', 'Space', 'a', 'Return', 'b'];
|
||||
this.twoItemOl = ['1', '.', 'Space', 'a', 'Return', 'b'];
|
||||
});
|
||||
|
||||
it("creates two ordered items with enter at end", (done) => {
|
||||
it('creates two ordered items with enter at end', (done) => {
|
||||
this.ce.test({
|
||||
keys: this.twoItemUl,
|
||||
expectedHTML: "<ul><li>a</li><li>b</li></ul>",
|
||||
expectedHTML: '<ul><li>a</li><li>b</li></ul>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {
|
||||
node: dom.querySelectorAll("li")[1].childNodes[0],
|
||||
offset: 1
|
||||
}
|
||||
}
|
||||
}).then(done).catch(done.fail)
|
||||
node: dom.querySelectorAll('li')[1].childNodes[0],
|
||||
offset: 1,
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("creates two bullet items with enter at end", (done) => {
|
||||
it('creates two bullet items with enter at end', (done) => {
|
||||
this.ce.test({
|
||||
keys: this.twoItemOl,
|
||||
expectedHTML: "<ol><li>a</li><li>b</li></ol>",
|
||||
expectedHTML: '<ol><li>a</li><li>b</li></ol>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {
|
||||
node: dom.querySelectorAll("li")[1].childNodes[0],
|
||||
offset: 1
|
||||
}
|
||||
}
|
||||
}).then(done).catch(done.fail)
|
||||
node: dom.querySelectorAll('li')[1].childNodes[0],
|
||||
offset: 1,
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("outdents the first item when backspacing from the start", (done) => {
|
||||
it('outdents the first item when backspacing from the start', (done) => {
|
||||
this.ce.test({
|
||||
keys: this.twoItemOl.concat(['Up arrow', 'Left arrow', 'Back space']),
|
||||
expectedHTML: '<span style="line-height: 1.4;">a</span><br><ol><li>b</li></ol>',
|
||||
|
@ -119,24 +136,23 @@ describe('Contenteditable Integration Spec', function() {
|
|||
const {DOMUtils} = require('nylas-exports');
|
||||
return {
|
||||
node: DOMUtils.findFirstTextNode(dom),
|
||||
offset: 0
|
||||
}
|
||||
}
|
||||
}).then(done).catch(done.fail)
|
||||
offset: 0,
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("outdents the last item when backspacing from the start", (done) => {
|
||||
it('outdents the last item when backspacing from the start', (done) => {
|
||||
this.ce.test({
|
||||
keys: this.twoItemOl.concat(['Left arrow', 'Back space']),
|
||||
expectedHTML: '<ol><li>a</li></ol><span style="line-height: 1.4;">b</span><br>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
const {DOMUtils} = require('nylas-exports');
|
||||
return {
|
||||
node: dom.querySelector('span').childNodes[0],
|
||||
offset: 0
|
||||
}
|
||||
}
|
||||
}).then(done).catch(done.fail)
|
||||
offset: 0,
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
// xit "backspace from the start of the 1st item outdents", ->
|
||||
|
@ -164,37 +180,67 @@ describe('Contenteditable Integration Spec', function() {
|
|||
// @ce.keys @twoItemKeys.concat ['backspace']
|
||||
});
|
||||
|
||||
describe('when creating a list within a list', ()=> {
|
||||
beforeEach(() => {
|
||||
this.list = ['-', 'Space'];
|
||||
});
|
||||
|
||||
it('indents list once', (done)=> {
|
||||
this.ce.test({
|
||||
keys: this.list.concat(this.list).concat(['a']),
|
||||
expectedHTML: '<ul><ul><li>a</li></ul></ul>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {
|
||||
node: dom.querySelectorAll('li')[0].childNodes[0],
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it('indents list twice', (done)=> {
|
||||
this.ce.test({
|
||||
keys: this.list.concat(this.list).concat(this.list).concat(['a']),
|
||||
expectedHTML: '<ul><ul><ul><li>a</li></ul></ul></ul>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {
|
||||
node: dom.querySelectorAll('li')[0].childNodes[0],
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When auto-concatenating lists', () => {
|
||||
beforeEach(() => {
|
||||
this.threeItemUl = ['-', 'Space', 'a', 'Return', 'b', 'Return', 'c']
|
||||
this.threeItemOl = ['1', ".", 'Space', 'a', 'Return', 'b', 'Return', 'c']
|
||||
this.deleteMiddle = ['Up arrow', 'Back space', 'Back space', 'Back space']
|
||||
this.threeItemUl = ['-', 'Space', 'a', 'Return', 'b', 'Return', 'c'];
|
||||
this.threeItemOl = ['1', '.', 'Space', 'a', 'Return', 'b', 'Return', 'c'];
|
||||
this.deleteMiddle = ['Up arrow', 'Back space', 'Back space', 'Back space'];
|
||||
});
|
||||
|
||||
it("concatenates adjacent unordered lists", (done) => {
|
||||
it('concatenates adjacent unordered lists', (done) => {
|
||||
this.ce.test({
|
||||
keys: this.threeItemUl.concat(this.deleteMiddle),
|
||||
expectedHTML: "<ul><li>a</li><li>c</li></ul>",
|
||||
expectedHTML: '<ul><li>a</li><li>c</li></ul>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {
|
||||
node: dom.querySelectorAll("li")[0].childNodes[0],
|
||||
offset: 1
|
||||
}
|
||||
}
|
||||
}).then(done).catch(done.fail)
|
||||
node: dom.querySelectorAll('li')[0].childNodes[0],
|
||||
offset: 1,
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("concatenates adjacent ordered lists", (done) => {
|
||||
it('concatenates adjacent ordered lists', (done) => {
|
||||
this.ce.test({
|
||||
keys: this.threeItemOl.concat(this.deleteMiddle),
|
||||
expectedHTML: "<ol><li>a</li><li>c</li></ol>",
|
||||
expectedHTML: '<ol><li>a</li><li>c</li></ol>',
|
||||
expectedSelectionResolver: (dom) => {
|
||||
return {
|
||||
node: dom.querySelectorAll("li")[0].childNodes[0],
|
||||
offset: 1
|
||||
}
|
||||
}
|
||||
}).then(done).catch(done.fail)
|
||||
node: dom.querySelectorAll('li')[0].childNodes[0],
|
||||
offset: 1,
|
||||
};
|
||||
},
|
||||
}).then(done).catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -247,43 +293,41 @@ describe('Contenteditable Integration Spec', function() {
|
|||
// @ce.keys ['-', ' ', 'a', 'tab', 'shift-tab']
|
||||
|
||||
|
||||
|
||||
|
||||
describe('Ensuring popout composer window works', () => {
|
||||
it("has main window visible", (done)=> {
|
||||
it('has main window visible', (done)=> {
|
||||
this.app.client.isWindowVisible()
|
||||
.then((result)=>{ expect(result).toBe(true) })
|
||||
.then(done).catch(done.fail)
|
||||
.then((result)=> expect(result).toBe(true) )
|
||||
.then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("has main window focused", (done)=> {
|
||||
it('has main window focused', (done)=> {
|
||||
this.app.client.isWindowFocused()
|
||||
.then((result)=>{ expect(result).toBe(true) })
|
||||
.then(done).catch(done.fail)
|
||||
.then((result)=> expect(result).toBe(true) )
|
||||
.then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("isn't minimized", (done)=> {
|
||||
it('is not minimized', (done)=> {
|
||||
this.app.client.isWindowMinimized()
|
||||
.then((result)=>{ expect(result).toBe(false) })
|
||||
.then(done).catch(done.fail)
|
||||
.then((result)=> expect(result).toBe(false) )
|
||||
.then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("doesn't have the dev tools open", (done)=> {
|
||||
it('does not have the dev tools open', (done)=> {
|
||||
this.app.client.isWindowDevToolsOpened()
|
||||
.then((result)=>{ expect(result).toBe(false) })
|
||||
.then(done).catch(done.fail)
|
||||
.then((result)=> expect(result).toBe(false) )
|
||||
.then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("has width", (done)=> {
|
||||
it('has width', (done)=> {
|
||||
this.app.client.getWindowWidth()
|
||||
.then((result)=>{ expect(result).toBeGreaterThan(0) })
|
||||
.then(done).catch(done.fail)
|
||||
.then((result)=> expect(result).toBeGreaterThan(0) )
|
||||
.then(done).catch(done.fail);
|
||||
});
|
||||
|
||||
it("has height", (done)=> {
|
||||
it('has height', (done)=> {
|
||||
this.app.client.getWindowHeight()
|
||||
.then((result)=>{ expect(result).toBeGreaterThan(0) })
|
||||
.then(done).catch(done.fail)
|
||||
.then((result)=> expect(result).toBeGreaterThan(0) )
|
||||
.then(done).catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -44,11 +44,11 @@ class ListManager extends ContenteditableExtension
|
|||
|
||||
if @numberRegex().test(text)
|
||||
@originalInput = text[0...3]
|
||||
editor.insertOrderedList()
|
||||
@insertList(editor, ordered: true)
|
||||
@removeListStarter(@numberRegex(), editor.currentSelection())
|
||||
else if @bulletRegex().test(text)
|
||||
@originalInput = text[0...2]
|
||||
editor.insertUnorderedList()
|
||||
@insertList(editor, ordered: false)
|
||||
@removeListStarter(@bulletRegex(), editor.currentSelection())
|
||||
else
|
||||
return
|
||||
|
@ -90,6 +90,15 @@ class ListManager extends ContenteditableExtension
|
|||
|
||||
@originalInput = null
|
||||
|
||||
@insertList: (editor, {ordered}) ->
|
||||
node = editor.currentSelection().anchorNode
|
||||
if @isInsideListItem(node)
|
||||
editor.indent()
|
||||
else
|
||||
editor.insertOrderedList() if ordered is true
|
||||
editor.insertUnorderedList() if not ordered
|
||||
|
||||
|
||||
@outdentListItem: (editor) ->
|
||||
if @originalInput
|
||||
editor.outdent()
|
||||
|
@ -97,6 +106,9 @@ class ListManager extends ContenteditableExtension
|
|||
else
|
||||
editor.outdent()
|
||||
|
||||
@isInsideListItem: (node) =>
|
||||
DOMUtils.isDescendantOf(node, (parent) -> parent.tagName is 'LI')
|
||||
|
||||
# If users ended up with two <ul> lists adjacent to each other, we
|
||||
# collapse them into one. We leave adjacent <ol> lists intact in case
|
||||
# the user wanted to restart the numbering sequence
|
||||
|
|
|
@ -600,4 +600,11 @@ DOMUtils =
|
|||
node.parentNode.replaceChild(fragment, node)
|
||||
return fragment
|
||||
|
||||
isDescendantOf: (node, matcher = -> false) ->
|
||||
parent = node?.parentElement
|
||||
while parent
|
||||
return true if matcher(parent)
|
||||
parent = parent.parentElement
|
||||
false
|
||||
|
||||
module.exports = DOMUtils
|
||||
|
|
Loading…
Reference in a new issue