Skip to content

Commit

Permalink
lib: Implement a file selecting widget
Browse files Browse the repository at this point in the history
Give users a little autocomplete help when they need to
select a file from the server. We need this for upcoming
ssh keys and VM dialogs.
  • Loading branch information
petervo committed Jul 4, 2017
1 parent 9a148b0 commit 0e35025
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 0 deletions.
39 changes: 39 additions & 0 deletions pkg/lib/cockpit-components-file-autocomplete.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.combobox-container input {
padding-right: 24px;
}

.combobox-container ul {
max-height: 250px;
}

.combobox-container .alert {
margin: 0;
border: none;
}

.combobox-container .error input {
border-color: red;
}

.combobox-container .error input {
border-color: red;
}

.combobox-container .error ul {
border-color: red;
padding: 0;
}

.combobox-container span.spinner,
.combobox-container span.caret {
position: absolute;
top: 7px;
z-index: 9;
}

.combobox-container span.caret {
width: 1.5em;
height: 1.5em;
cursor: pointer;
pointer-events: all;
}
247 changes: 247 additions & 0 deletions pkg/lib/cockpit-components-file-autocomplete.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2017 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/

"use strict";

var cockpit = require("cockpit");
var React = require("react");
var _ = cockpit.gettext;
require("./cockpit-components-file-autocomplete.css");

var FileAutoComplete = React.createClass({
getInitialState () {
var value = this.props.value || "";
this.updateFiles(value);
return {
value: value,
directory: '/',
directoryFiles: null,
displayFiles: [],
open: false,
error: null,
};
},

getDirectoryForValue: function(value) {
var dir = "";
var last;
if (value) {
value = value.trim();
last = value.lastIndexOf("/");
if (last > -1)
dir = value.slice(0, last);
dir += "/";
}

if (dir.indexOf("/") !== 0)
dir = "/" + dir;

return dir;
},

onChange: function(value) {
if (value && value.indexOf("/") !== 0)
value = "/" + value;

if (!this.updateIfDirectoryChanged(value));
this.filterFiles(value);

this.setState({
value: value,
});
},

delayedOnChange: function(ev) {
var self = this;
var value = ev.currentTarget.value;
if (this.timer)
window.clearTimeout(this.timer);

this.timer = window.setTimeout(function () {
self.onChange(value);
self.timer = null;
}, 250);
},

updateFiles: function(path) {
var self = this;
var channel = cockpit.channel({ payload: "fslist1",
path: path || "/",
superuser: this.props.superuser });

var results = [];

channel.addEventListener("ready", function () {
self.finishUpdate(results, null);
});

channel.addEventListener("close", function (ev, data) {
self.finishUpdate(results, data);
});

channel.addEventListener("message", function (ev, data) {
var item = JSON.parse(data);
if (item && item.path)
if (item.type == "directory")
item.path = item.path + "/";
results.push(item);
});
},

updateIfDirectoryChanged: function(value) {
var directory = this.getDirectoryForValue(value);
var changed = directory !== this.state.directory;
if (changed && this.state.directoryFiles !== null) {
this.setState({
displayFiles: [],
directoryFiles: null,
directory: directory,
open: false,
});
this.updateFiles(directory);
}
return changed;
},

finishUpdate: function(result, error) {
result = result.sort(function(a, b) {
var ap = a.path.toLowerCase();
var bp = b.path.toLowerCase();
if (ap > bp)
return 1;
else if (ap < bp)
return -1;
else
return 0;
});

this.setState({
displayFiles: result,
directoryFiles: result,
error: error,
});
},

filterFiles: function(value) {
var inputValue = value.trim().toLowerCase();
var dirLength = this.state.directory.length;
var matches = [];
var inputLength;

inputValue = inputValue.slice(dirLength);
inputLength = inputValue.length;

if (this.state.directoryFiles !== null) {
matches = this.state.directoryFiles.filter(function (v) {
return v.path.toLowerCase().slice(0, inputLength) === inputValue;
});
}

this.setState({
displayFiles: matches,
open: true,
});
},

showAllOptions: function (ev) {
// only consider clicks with the primary button
if (ev && ev.button !== 0)
return;

this.setState({
open: !this.state.open,
displayFiles: this.state.directoryFiles || [],
});
},

selectItem: function (ev) {
// only consider clicks with the primary button
if (ev && ev.button !== 0)
return;

if (ev.target.tagName == 'A') {
var value = ev.target.innerText;
var directory = this.state.directory || "/";

if (directory.charAt(directory.length - 1) !== '/')
directory = directory + "/";

value = directory + value;
this.setState({
open: false,
value: value
});
this.updateIfDirectoryChanged(value);
}
},

renderError: function(error) {
return (
<li className="alert alert-warning">
<span className="pficon pficon-warning-triangle-o"></span>
<strong>{error}</strong>
</li>
);
},

render: function() {
var placeholder = this.props.placeholder || _("Path to file");
var controlClasses = "form-control-feedback ";
var classes = "input-group";
if (this.state.open)
classes += " open";

if (this.state.directoryFiles === null)
controlClasses += "spinner spinner-xs spinner-inline";
else
controlClasses += "caret";


var listItems, error;
if (this.state.error)
error = cockpit.format(cockpit.message(this.state.error));
else if (this.state.directoryFiles && this.state.displayFiles.length < 1)
error = _("No matching files found");

if (error) {
listItems = [this.renderError(error)];
classes += " error"
} else {
listItems = React.Children.map(this.state.displayFiles, function(file) {
return <li className={file.type}><a data-type={file.type}>{file.path}</a></li>;
});
}

return (
<div className="combobox-container" id={this.props.id}>
<div className={classes}>
<input autocomplete="false" placeholder={placeholder} className="combobox form-control" type="text" onChange={this.delayedOnChange} value={this.state.value} />
<span title="lala" onClick={this.showAllOptions} className={controlClasses}></span>
<ul onClick={this.selectItem} className="typeahead typeahead-long dropdown-menu">
{listItems}
</ul>
</div>
</div>
);
}
});

module.exports = {
FileAutoComplete: FileAutoComplete,
};
30 changes: 30 additions & 0 deletions pkg/playground/react-demo-file-autocomplete.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2017 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/

var React = require("react");

var FileAutoComplete = require("cockpit-components-file-autocomplete.jsx").FileAutoComplete;

var showFileAcDemo = function(rootElement) {
React.render(<FileAutoComplete />, rootElement);
};

module.exports = {
demo: showFileAcDemo,
};
7 changes: 7 additions & 0 deletions pkg/playground/react-patterns.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ <h3>Tooltip</h3>

<hr>

<div class="container-fluid">
<h3>Select file</h3>
<div id="demo-file-ac"></div>
</div>

<hr>

<div class="container-fluid">
<h3>Dialogs</h3>
<button id="demo-show-dialog" class="btn btn-default">Show Dialog</button>
Expand Down
5 changes: 5 additions & 0 deletions pkg/playground/react-patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
var demoTooltip = require("./react-demo-tooltip.jsx");
var demoOnOff = require("./react-demo-onoff.jsx");

var demoFileAC = require("./react-demo-file-autocomplete.jsx");


/*-----------------------------------------------------------------------------
Modal Dialog
Expand Down Expand Up @@ -131,6 +133,9 @@

// OnOff
demoOnOff.demo(document.getElementById('demo-onoff'));

// File autocomplete
demoFileAC.demo(document.getElementById('demo-file-ac'));
});

}());

0 comments on commit 0e35025

Please sign in to comment.