Skip to content
18 changes: 15 additions & 3 deletions website/static/js/addProjectPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var sprintf = require('agh.sprintf').sprintf;
var AddProject = {
controller : function (options) {
var self = this;
self.isComposing = false;
self.defaults = {
buttonTemplate : m('.btn.btn-primary[data-toggle="modal"][data-target="#addProjectModal"]', _('Create new project')),
parentID : null,
Expand Down Expand Up @@ -192,13 +193,24 @@ var AddProject = {
m('.form-group.m-v-sm', [
m('label[for="projectName].f-w-lg.text-bigger', _('Title')),
m('input[type="text"].form-control.project-name', {
onkeyup: function(ev){
oncompositionstart: function () {
ctrl.isComposing = true;
},
oncompositionend: function () {
ctrl.isComposing = false;
},
oninput: function(ev) {
var val = ev.target.value;
ctrl.isValid(val.trim().length > 0);
if (ev.which === 13) {
ctrl.newProjectName(val);
},
onkeydown: function(ev){
var isComposing = ev.isComposing || ctrl.isComposing || ev.keyCode === 229;
if (ev.key === 'Enter' && !isComposing) {
ev.preventDefault();
ev.stopPropagation();
ctrl.add();
}
ctrl.newProjectName(val);
},
onchange: function(ev) {
// This will not be reliably running!
Expand Down
36 changes: 30 additions & 6 deletions website/static/js/fangorn.js
Original file line number Diff line number Diff line change
Expand Up @@ -2374,13 +2374,17 @@ var FGInput = {
var id = args.id || '';
var helpTextId = args.helpTextId || '';
var oninput = args.oninput || noop;
var onkeypress = args.onkeypress || noop;
var onkeydown = args.onkeydown || noop;
var oncompositionstart = args.oncompositionstart || noop;
var oncompositionend = args.oncompositionend || noop;
return m('span', [
m('input', {
'id' : id,
className: 'pull-right form-control' + extraCSS,
oninput: oninput,
onkeypress: onkeypress,
onkeydown: onkeydown,
oncompositionstart: oncompositionstart,
oncompositionend: oncompositionend,
'value': args.value || '',
'data-toggle': tooltipText ? 'tooltip' : '',
'title': tooltipText,
Expand Down Expand Up @@ -2586,6 +2590,8 @@ var dismissToolbar = function(helpText){
var FGToolbar = {
controller : function(args) {
var self = this;
self.isComposingAddFolder = false;
self.isComposingRenameFolder = false;
self.tb = args.treebeard;
self.tb.toolbarMode = m.prop(toolbarModes.DEFAULT);
self.items = args.treebeard.multiselected;
Expand Down Expand Up @@ -2638,9 +2644,18 @@ var FGToolbar = {
templates[toolbarModes.ADDFOLDER] = [
m('.col-xs-9', [
m.component(FGInput, {
oncompositionstart: function () {
ctrl.isComposingAddFolder = true;
},
oncompositionend: function () {
ctrl.isComposingAddFolder = false;
},
oninput: m.withAttr('value', ctrl.nameData),
onkeypress: function (event) {
if (ctrl.tb.pressedKey === ENTER_KEY) {
onkeydown: function (event) {
const isComposing = event.isComposing || ctrl.isComposingAddFolder || event.keyCode === 229;
if (event.key === 'Enter' && !isComposing) {
event.preventDefault();
event.stopPropagation();
ctrl.createFolder.call(ctrl.tb, event, ctrl.dismissToolbar);
}
},
Expand All @@ -2666,9 +2681,18 @@ var FGToolbar = {
templates[toolbarModes.RENAME] = [
m('.col-xs-9',
m.component(FGInput, {
oncompositionstart: function () {
ctrl.isComposingRenameFolder = true;
},
oncompositionend: function () {
ctrl.isComposingRenameFolder = false;
},
oninput: m.withAttr('value', ctrl.renameData),
onkeypress: function (event) {
if (ctrl.tb.pressedKey === ENTER_KEY) {
onkeydown: function (event) {
var isComposing = event.isComposing || ctrl.isComposingRenameFolder || event.keyCode === 229;
if (event.key === 'Enter' && !isComposing) {
event.preventDefault();
event.stopPropagation();
_renameEvent.call(ctrl.tb);
}
},
Expand Down
51 changes: 38 additions & 13 deletions website/static/js/myProjects.js
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,8 @@ var MyProjects = {
var Collections = {
controller : function(args){
var self = this;
self.isComposingAdd = false;
self.isComposingRename = false;
self.collections = args.collections;
self.pageSize = args.collectionsPageSize;
self.newCollectionName = m.prop('');
Expand Down Expand Up @@ -1539,15 +1541,26 @@ var Collections = {
m('.form-group', [
m('label[for="addCollInput].f-w-lg.text-bigger', _('Collection name')),
m('input[type="text"].form-control#addCollInput', {
onkeyup: function (ev){
var val = $(this).val();
oncompositionstart: function () {
ctrl.isComposingAdd = true;
},
oncompositionend: function () {
ctrl.isComposingAdd = false;
},
oninput: function(ev) {
var val = ev.target.value;
ctrl.validateName(val);
if(ctrl.isValid()){
if(ev.which === 13){
ctrl.newCollectionName(val);
},
onkeydown: function (ev){
var isComposing = ev.isComposing || ctrl.isComposingAdd || ev.keyCode === 229;
if (ev.key === 'Enter' && !isComposing) {
ev.preventDefault();
ev.stopPropagation();
if (ctrl.isValid()) {
ctrl.addCollection();
}
}
ctrl.newCollectionName(val);
},
onchange: function() {
$osf.trackClick('myProjects', 'add-collection', 'type-collection-name');
Expand Down Expand Up @@ -1579,28 +1592,40 @@ var Collections = {
$osf.trackClick('myProjects', 'edit-collection', 'click-close-rename-modal');
}}, [
m('span[aria-hidden="true"]','×')
]),
m('h3.modal-title', _('Rename collection'))
]),
m('h3.modal-title', _('Rename collection'))
]),
body: m('.modal-body', [
m('.form-inline', [
m('.form-group', [
m('label[for="addCollInput]', _('Rename to: ')),
m('input[type="text"].form-control.m-l-sm',{
onkeyup: function(ev){
var val = $(this).val();
oncompositionstart: function () {
ctrl.isComposingRename = true;
},
oncompositionend: function () {
ctrl.isComposingRename = false;
},
oninput: function(ev) {
var val = ev.target.value;
ctrl.validateName(val);
if(ctrl.isValid()) {
if (ev.which === 13) { // if enter is pressed
ctrl.collectionMenuObject().item.renamedLabel = val;
},
onkeydown: function(ev){
var isComposing = ev.isComposing || ctrl.isComposingRename || ev.keyCode === 229;
if (ev.key === 'Enter' && !isComposing) {
ev.preventDefault();
ev.stopPropagation();
if (ctrl.isValid()) {
ctrl.renameCollection();
}
}
ctrl.collectionMenuObject().item.renamedLabel = val;
},
onchange: function() {
$osf.trackClick('myProjects', 'edit-collection', 'type-rename-collection');
},
value: ctrl.collectionMenuObject().item.renamedLabel}),
value: ctrl.collectionMenuObject().item.renamedLabel
}),
m('span.help-block', ctrl.validationError())

])
Expand Down
160 changes: 159 additions & 1 deletion website/static/js/tests/MyProjects.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/*global describe, it, expect, example, before, after, beforeEach, afterEach, mocha, sinon*/
'use strict';
var assert = require('chai').assert;

var sinon = require('sinon');
var fb = require('js/myProjects.js');

var LinkObject = fb.LinkObject;
Expand Down Expand Up @@ -36,4 +36,162 @@ describe('fileBrowser', function() {
});
});
});

describe('Collections IME Keydown Handling', function() {
function makeMockCtrl(overrides) {
return Object.assign({
isComposingAdd: false,
isComposingRename: false,
isValid: sinon.stub().returns(true),
validateName: sinon.stub(),
newCollectionName: sinon.stub(),
addCollection: sinon.stub(),
renameCollection: sinon.stub(),
collectionMenuObject: sinon.stub().returns({ item: { renamedLabel: '' } }),
}, overrides);
}

function makeAddCollKeydownHandler(ctrl) {
return function(ev) {
var isComposing = ev.isComposing || ctrl.isComposingAdd || ev.keyCode === 229;
if (ev.key === 'Enter' && !isComposing) {
ev.preventDefault();
ev.stopPropagation();
if (ctrl.isValid()) {
ctrl.addCollection();
}
}
};
}

function makeRenameCollKeydownHandler(ctrl) {
return function(ev) {
var isComposing = ev.isComposing || ctrl.isComposingRename || ev.keyCode === 229;
if (ev.key === 'Enter' && !isComposing) {
ev.preventDefault();
ev.stopPropagation();
if (ctrl.isValid()) {
ctrl.renameCollection();
}
}
};
}

function makeEvent(overrides) {
return Object.assign({
key: 'Enter',
isComposing: false,
keyCode: 13,
preventDefault: sinon.spy(),
stopPropagation: sinon.spy(),
}, overrides);
}

var ctrl;
beforeEach(function() {
ctrl = makeMockCtrl();
});

describe('addCollection keydown', function() {
it('should call addCollection() on Enter when valid and not composing', function() {
var handler = makeAddCollKeydownHandler(ctrl);
handler(makeEvent());
assert.ok(ctrl.addCollection.calledOnce, 'addCollection() should be called');
});

it('should NOT call addCollection() during IME (event.isComposing=true)', function() {
var handler = makeAddCollKeydownHandler(ctrl);
handler(makeEvent({ isComposing: true }));
assert.ok(ctrl.addCollection.notCalled);
});

it('should NOT call addCollection() during Chrome IME race (ctrl.isComposing=true)', function() {
ctrl.isComposingAdd = true;
var handler = makeAddCollKeydownHandler(ctrl);
handler(makeEvent({ isComposing: false }));
assert.ok(ctrl.addCollection.notCalled,
'ctrl.isComposing=true should block addCollection() even if event.isComposing=false');
});

it('should NOT call addCollection() when keyCode is 229 (legacy IME)', function() {
var handler = makeAddCollKeydownHandler(ctrl);
handler(makeEvent({ isComposing: false, keyCode: 229 }));
assert.ok(ctrl.addCollection.notCalled);
});

it('should call preventDefault() on Enter even when form is invalid', function() {
ctrl.isValid.returns(false);
var handler = makeAddCollKeydownHandler(ctrl);
var ev = makeEvent();
handler(ev);
assert.ok(ev.preventDefault.calledOnce,
'[BUG] preventDefault should be called on Enter regardless of validity');
assert.ok(ctrl.addCollection.notCalled, 'addCollection should not be called when invalid');
});

it('should NOT call addCollection() on non-Enter key', function() {
var handler = makeAddCollKeydownHandler(ctrl);
handler(makeEvent({ key: 'Escape', keyCode: 27 }));
assert.ok(ctrl.addCollection.notCalled);
});
});

describe('renameCollection keydown', function() {
it('should call renameCollection() on Enter when valid and not composing', function() {
var handler = makeRenameCollKeydownHandler(ctrl);
handler(makeEvent());
assert.ok(ctrl.renameCollection.calledOnce, 'renameCollection() should be called');
});

it('should NOT call renameCollection() during IME (event.isComposing=true)', function() {
var handler = makeRenameCollKeydownHandler(ctrl);
handler(makeEvent({ isComposing: true }));
assert.ok(ctrl.renameCollection.notCalled);
});

it('should NOT call renameCollection() when ctrl.isComposing=true (Chrome race)', function() {
ctrl.isComposingRename = true;
var handler = makeRenameCollKeydownHandler(ctrl);
handler(makeEvent({ isComposing: false }));
assert.ok(ctrl.renameCollection.notCalled);
});

it('should NOT call renameCollection() when keyCode is 229', function() {
var handler = makeRenameCollKeydownHandler(ctrl);
handler(makeEvent({ keyCode: 229, isComposing: false }));
assert.ok(ctrl.renameCollection.notCalled);
});
});

describe('ctrl.isComposing flag lifecycle', function() {
it('should be set true on compositionstart', function() {
ctrl.isComposing = false;
var onCompositionStart = function() { ctrl.isComposing = true; };
onCompositionStart();
assert.strictEqual(ctrl.isComposing, true);
});

it('should be set false on compositionend', function() {
ctrl.isComposing = true;
var onCompositionEnd = function() { ctrl.isComposing = false; };
onCompositionEnd();
assert.strictEqual(ctrl.isComposing, false);
});
});

describe('oninput handler', function() {
it('should call validateName and newCollectionName with input value', function() {
var val = 'Test Collection';
var ev = { target: { value: val } };
var onInput = function(ev) {
var v = ev.target.value;
ctrl.validateName(v);
ctrl.newCollectionName(v);
};
onInput(ev);
assert.ok(ctrl.validateName.calledWith(val));
assert.ok(ctrl.newCollectionName.calledWith(val));
});
});
});
});
Loading
Loading