*
* @throws {TypeError}
* Throws a `TypeError` exception in case the passed class value is not a
- * descendent of `AbstractValue`.
+ * descendant of `AbstractValue`.
*
* @returns {LuCI.form.AbstractValue}
* Returns the instantiated option class instance.
*/
option: function(cbiClass /*, ... */) {
if (!CBIAbstractValue.isSubclass(cbiClass))
- throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
+ throw L.error('TypeError', 'Class must be a descendant of CBIAbstractValue');
var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
this.append(obj);
*
* @throws {TypeError}
* Throws a `TypeError` exception in case the passed class value is not a
- * descendent of `AbstractValue`.
+ * descendant of `AbstractValue`.
*
* @returns {LuCI.form.AbstractValue}
* Returns the instantiated option class instance.
__init__: function(/* ... */) {
this.super('__init__', arguments);
+ this.browser = false;
this.show_hidden = false;
this.enable_upload = true;
this.enable_remove = true;
+ this.enable_download = false;
this.root_directory = '/etc/luci-uploads';
},
+
+ /**
+ * Open in a file browser mode instead of selecting for a file
+ *
+ * @name LuCI.form.FileUpload.prototype#browser
+ * @type boolean
+ * @default false
+ */
+
/**
* Toggle display of hidden files.
*
* @default true
*/
+ /**
+ * Toggle download file functionality.
+ *
+ * @name LuCI.form.FileUpload.prototype#enable_download
+ * @type boolean
+ * @default false
+ */
+
/**
* Specify the root directory for file browsing.
*
var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
id: this.cbid(section_id),
name: this.cbid(section_id),
+ browser: this.browser,
show_hidden: this.show_hidden,
enable_upload: this.enable_upload,
enable_remove: this.enable_remove,
+ enable_download: this.enable_download,
root_directory: this.root_directory,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
* @typedef {LuCI.ui.AbstractElement.InitOptions} InitOptions
* @memberof LuCI.ui.FileUpload
*
+ * @property {boolean} [browser=false]
+ * Use a file browser mode.
+ *
* @property {boolean} [show_hidden=false]
* Specifies whether hidden files should be displayed when browsing remote
* files. Note that this is not a security feature, hidden files are always
* remotely depends on the ACL setup for the current session. This option
* merely controls whether the file remove controls are rendered or not.
*
+ * @property {boolean} [enable_download=false]
+ * Specifies whether the widget allows the user to download files.
+ *
* @property {string} [root_directory=/etc/luci-uploads]
* Specifies the remote directory the upload and file browsing actions take
* place in. Browsing to directories outside the root directory is
__init__: function(value, options) {
this.value = value;
this.options = Object.assign({
+ browser: false,
show_hidden: false,
enable_upload: true,
enable_remove: true,
+ enable_download: false,
root_directory: '/etc/luci-uploads'
}, options);
},
/** @override */
render: function() {
- return L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
+ var renderFileBrowser = L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
var label;
if (L.isObject(stat) && stat.type != 'directory')
label = [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value), _('File not accessible')) ];
else
label = [ _('Select file…') ];
-
- return this.bind(E('div', { 'id': this.options.id }, [
- E('button', {
- 'class': 'btn',
- 'click': UI.prototype.createHandlerFn(this, 'handleFileBrowser'),
- 'disabled': this.options.disabled ? '' : null
- }, label),
+ let btnOpenFileBrowser = E('button', {
+ 'class': 'btn open-file-browser',
+ 'click': UI.prototype.createHandlerFn(this, 'handleFileBrowser'),
+ 'disabled': this.options.disabled ? '' : null
+ }, label);
+ var fileBrowserEl = E('div', { 'id': this.options.id }, [
+ btnOpenFileBrowser,
E('div', {
'class': 'cbi-filebrowser'
}),
'name': this.options.name,
'value': this.value
})
- ]));
+ ]);
+ return this.bind(fileBrowserEl);
}, this));
+ // in a browser mode open dir listing after render by clicking on a Select button
+ if (this.options.browser) {
+ return renderFileBrowser.then(function (fileBrowserEl) {
+ var btnOpenFileBrowser = fileBrowserEl.getElementsByClassName('open-file-browser').item(0);
+ btnOpenFileBrowser.click();
+ return fileBrowserEl;
+ });
+ }
+ return renderFileBrowser
},
/** @private */
'class': 'btn',
'click': UI.prototype.createHandlerFn(this, 'handleReset')
}, [ _('Deselect') ]) : '',
+ this.options.enable_download && list[i].type == 'file' ? E('button', {
+ 'class': 'btn',
+ 'click': UI.prototype.createHandlerFn(this, 'handleDownload', entrypath, list[i])
+ }, [ _('Download') ]) : '',
this.options.enable_remove ? E('button', {
'class': 'btn cbi-button-negative',
'click': UI.prototype.createHandlerFn(this, 'handleDelete', entrypath, list[i])
rows,
E('div', { 'class': 'right' }, [
this.renderUpload(path, list),
- E('a', {
+ !this.options.browser ? E('a', {
'href': '#',
'class': 'btn',
'click': UI.prototype.createHandlerFn(this, 'handleCancel')
- }, _('Cancel'))
+ }, _('Cancel')) : ''
]),
]);
},
this.handleCancel(ev);
},
+ /** @private */
+ handleDownload: function(path, fileStat, ev) {
+ fs.read_direct(path, 'blob').then(function (blob) {
+ var url = window.URL.createObjectURL(blob);
+ var a = document.createElement('a');
+ a.style.display = 'none';
+ a.href = url;
+ a.download = fileStat.name;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ }).catch(function(err) {
+ alert(_('Download failed: %s').format(err.message));
+ });
+ },
+
/** @private */
handleSelect: function(path, fileStat, ev) {
var browser = dom.parent(ev.target, '.cbi-filebrowser'),
dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
L.resolveDefault(fs.list(path), []).then(L.bind(this.renderListing, this, browser, path));
}
- else {
+ else if (!this.options.browser) {
var button = this.node.firstElementChild,
hidden = this.node.lastElementChild;
E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
.format(L.env.apply_rollback)));
- var call = function(r, data, duration) {
+ var call = function(r) {
if (r.status === 204) {
UI.prototype.changes.displayStatus('warning', [
E('h4', _('Configuration changes have been rolled back!')),
return;
}
- var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
+ var delay = isNaN(r.duration) ? 0 : Math.max(1000 - r.duration, 0);
window.setTimeout(function() {
request.request(L.url('admin/uci/confirm'), {
method: 'post',
timeout: L.env.apply_timeout * 1000,
query: { sid: L.env.sessionid, token: L.env.token }
- }).then(call, call.bind(null, { status: 0 }, null, 0));
+ }).then(call, call.bind(null, { status: 0, duration: 0 }));
}, delay);
};
if (override_token)
this.confirm_auth = { token: override_token };
- var call = function(r, data, duration) {
+ var call = function(r) {
if (Date.now() >= deadline) {
window.clearTimeout(tt);
UI.prototype.changes.rollback(checked);
return;
}
- else if (r && (r.status === 200 || r.status === 204)) {
+ else if (r.status === 200 || r.status === 204) {
document.dispatchEvent(new CustomEvent('uci-applied'));
UI.prototype.changes.setIndicator(0);
return;
}
- var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
+ var delay = isNaN(r.duration) ? 0 : Math.max(1000 - r.duration, 0);
window.setTimeout(function() {
request.request(L.url('admin/uci/confirm'), {
method: 'post',
timeout: L.env.apply_timeout * 1000,
query: UI.prototype.changes.confirm_auth
- }).then(call, call);
+ }).then(call, call.bind(null, { status: 0, duration: 0 }));
}, delay);
};
tick();
/* wait a few seconds for the settings to become effective */
- window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
+ window.setTimeout(call.bind(null, { status: 0 }), L.env.apply_holdoff * 1000);
},
/**