Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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