dsh-linked-workspace 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.en.md +29 -0
- package/README.md +29 -0
- package/docs/API.md +6 -6
- package/docs/VERIFY.md +24 -0
- package/lib/client.js +397 -61
- package/lib/index.js +203 -45
- package/lib/linked.js +46 -24
- package/package.json +2 -2
- package/scripts/doc_tool.py +10 -3
package/lib/client.js
CHANGED
|
@@ -1835,8 +1835,122 @@ function fileIconSpec(name, isDir) {
|
|
|
1835
1835
|
return ICON_spec('unknown')
|
|
1836
1836
|
}
|
|
1837
1837
|
|
|
1838
|
+
// ---- file-creation.js ----
|
|
1839
|
+
// Shared by the directory menu, its submit handler and API regression tests.
|
|
1840
|
+
// Preview formats (CSV/HTML/images) are regular files; only document templates
|
|
1841
|
+
// are sent to docs.create.
|
|
1842
|
+
function newEntryMenuItems(menu, t, onNew) {
|
|
1843
|
+
return [true, false].map(function (isDir) {
|
|
1844
|
+
var title = t(isDir ? 'newFolderTitle' : 'newFileTitle');
|
|
1845
|
+
return {
|
|
1846
|
+
label: t(isDir ? 'ctxNewFolder' : 'ctxNewFile'),
|
|
1847
|
+
onSelect: function () {
|
|
1848
|
+
onNew({ kind: 'new', side: menu.side, dir: menu.path, isDir: isDir,
|
|
1849
|
+
title: title, label: t('nameLabel'), value: title });
|
|
1850
|
+
},
|
|
1851
|
+
};
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function newEntryRequest(side, dirPath, name, isDir, scope) {
|
|
1856
|
+
var child = String(name || '').trim();
|
|
1857
|
+
if (!child || child === '.' || child === '..' || /[/\\\0]/.test(child)) {
|
|
1858
|
+
var error = new Error('文件或文件夹名称无效 / Invalid file or folder name');
|
|
1859
|
+
error.code = 'BAD_REQUEST';
|
|
1860
|
+
throw error;
|
|
1861
|
+
}
|
|
1862
|
+
var parent = String(dirPath || '');
|
|
1863
|
+
var separator = side === 'remote' || parent.indexOf('\\') < 0 ? '/' : '\\';
|
|
1864
|
+
var target = parent + (/[\\/]$/.test(parent) ? '' : separator) + child;
|
|
1865
|
+
var match = child.toLowerCase().match(/\.[^./\\]+$/);
|
|
1866
|
+
var extension = match ? match[0] : '';
|
|
1867
|
+
var kinds = { '.md': 'markdown', '.markdown': 'markdown', '.pdf': 'pdf',
|
|
1868
|
+
'.docx': 'docx', '.xlsx': 'xlsx', '.pptx': 'pptx' };
|
|
1869
|
+
var kind = isDir ? '' : (kinds[extension] || '');
|
|
1870
|
+
var method;
|
|
1871
|
+
var params;
|
|
1872
|
+
if (isDir) {
|
|
1873
|
+
method = side === 'remote' ? 'fs.mkdir' : 'local.mkdir';
|
|
1874
|
+
params = side === 'remote' ? { path: target, createOnly: true } : { path: parent, name: child };
|
|
1875
|
+
} else if (kind) {
|
|
1876
|
+
method = 'docs.create';
|
|
1877
|
+
params = { path: target, kind: kind };
|
|
1878
|
+
} else {
|
|
1879
|
+
method = side === 'remote' ? 'fs.write' : 'local.newFile';
|
|
1880
|
+
params = side === 'remote' ? { path: target, content: '', createOnly: true } : { path: target };
|
|
1881
|
+
}
|
|
1882
|
+
if (side === 'remote') params = Object.assign({}, scope, params);
|
|
1883
|
+
return { method: method, params: params, path: target, kind: kind };
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// ---- url-safety.js ----
|
|
1887
|
+
// URL safety helpers shared by the document sanitizer and its unit tests.
|
|
1888
|
+
//
|
|
1889
|
+
// Browsers strip ASCII control characters, tabs and newlines while parsing a
|
|
1890
|
+
// URL scheme, so `jav	ascript:` reaches the address bar as `javascript:`.
|
|
1891
|
+
// The checks below normalise the value the same way before looking at it.
|
|
1892
|
+
var SAFE_SCHEMES = { http: 1, https: 1, mailto: 1, tel: 1 };
|
|
1893
|
+
var IMAGE_DATA = /^data:image\/(?:png|jpeg|jpg|gif|webp|bmp|avif|x-icon)[;,]/
|
|
1894
|
+
var URL_ATTRIBUTES = {
|
|
1895
|
+
href: 1,
|
|
1896
|
+
src: 1,
|
|
1897
|
+
'xlink:href': 1,
|
|
1898
|
+
action: 1,
|
|
1899
|
+
formaction: 1,
|
|
1900
|
+
data: 1,
|
|
1901
|
+
poster: 1,
|
|
1902
|
+
background: 1,
|
|
1903
|
+
cite: 1,
|
|
1904
|
+
longdesc: 1,
|
|
1905
|
+
usemap: 1,
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
function normalizeUrlValue(value) {
|
|
1909
|
+
return String(value == null ? '' : value)
|
|
1910
|
+
// C0/C1 controls, space, zero-width and line/paragraph separators are what
|
|
1911
|
+
// URL parsers silently drop inside a scheme.
|
|
1912
|
+
.replace(/[\u0000-\u0020\u007f-\u009f\u200b-\u200f\u2028\u2029\ufeff]/g, '')
|
|
1913
|
+
.trim();
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function urlSchemeOf(value) {
|
|
1917
|
+
var text = normalizeUrlValue(value);
|
|
1918
|
+
var match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(text);
|
|
1919
|
+
return match ? match[1].toLowerCase() : '';
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// Returns '' when the attribute is safe to keep, or a short reason when the
|
|
1923
|
+
// sanitizer has to drop it. `tagName` is only needed for data: URLs, which are
|
|
1924
|
+
// allowed for images and rejected everywhere else.
|
|
1925
|
+
function urlAttributeViolation(name, value, tagName) {
|
|
1926
|
+
var attribute = String(name || '').toLowerCase();
|
|
1927
|
+
if (/^on/.test(attribute)) return 'event handler';
|
|
1928
|
+
if (!URL_ATTRIBUTES[attribute]) return '';
|
|
1929
|
+
var text = normalizeUrlValue(value);
|
|
1930
|
+
if (text === '') return '';
|
|
1931
|
+
// Relative paths, fragments and protocol-relative URLs have no scheme.
|
|
1932
|
+
if (text.charAt(0) === '#' || text.slice(0, 2) === '//') return '';
|
|
1933
|
+
var scheme = urlSchemeOf(text);
|
|
1934
|
+
if (scheme === '') return '';
|
|
1935
|
+
if (SAFE_SCHEMES[scheme]) return '';
|
|
1936
|
+
if (scheme === 'data' && String(tagName || '').toLowerCase() === 'img' && IMAGE_DATA.test(text)) return '';
|
|
1937
|
+
return scheme + ': URL';
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// Complete attribute policy for the document preview. Inline styles are dropped
|
|
1941
|
+
// as well: `position:fixed; inset:0; z-index:99999` can cover the whole Harness
|
|
1942
|
+
// window, which turns a preview into a clickjacking / phishing surface without
|
|
1943
|
+
// a single line of script.
|
|
1944
|
+
function attributeViolation(name, value, tagName) {
|
|
1945
|
+
if (String(name || '').toLowerCase() === 'style') return 'inline style';
|
|
1946
|
+
return urlAttributeViolation(name, value, tagName);
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1838
1949
|
var TREE_TAB_TYPE = 'dsh-linked-workspace';
|
|
1839
1950
|
var REMOTE_TAB_TYPE = 'dsh-linked-workspace:remote';
|
|
1951
|
+
// One tree page; a folder with 20k entries must not render 20k rows.
|
|
1952
|
+
var TREE_PAGE_SIZE = 500;
|
|
1953
|
+
var TREE_PAGE_MAX = 5000;
|
|
1840
1954
|
var linkedLocalKeys = new Set();
|
|
1841
1955
|
var linkedWorkspaceKeys = new Set();
|
|
1842
1956
|
var hasRemoteDirectories = false;
|
|
@@ -2050,6 +2164,11 @@ function fileIconSpec(name, isDir) {
|
|
|
2050
2164
|
latency: '延迟',
|
|
2051
2165
|
countSuffix: ' 个',
|
|
2052
2166
|
noEntries: '空目录',
|
|
2167
|
+
loadMore: '加载更多(剩余 {{count}} 项)',
|
|
2168
|
+
loadMoreShort: '加载更多',
|
|
2169
|
+
pickerTruncated: '目录项过多,仅显示前 {{count}} 项;可直接在输入框中输入路径',
|
|
2170
|
+
sheetTruncated: '仅显示前 {{rows}} 行 × {{cols}} 列',
|
|
2171
|
+
sheetMore: '工作表较多,仅显示前 20 个',
|
|
2053
2172
|
noCredential: '缺少凭据',
|
|
2054
2173
|
sourceManagerShort: 'SSH',
|
|
2055
2174
|
remoteGroup: '远程目录',
|
|
@@ -2379,6 +2498,11 @@ function fileIconSpec(name, isDir) {
|
|
|
2379
2498
|
latency: 'Latency',
|
|
2380
2499
|
countSuffix: '',
|
|
2381
2500
|
noEntries: 'Empty directory',
|
|
2501
|
+
loadMore: 'Load more ({{count}} left)',
|
|
2502
|
+
loadMoreShort: 'Load more',
|
|
2503
|
+
pickerTruncated: 'Too many entries; only the first {{count}} are shown. Type a path directly.',
|
|
2504
|
+
sheetTruncated: 'Showing the first {{rows}} rows × {{cols}} columns',
|
|
2505
|
+
sheetMore: 'Showing the first 20 worksheets only',
|
|
2382
2506
|
noCredential: 'No credentials',
|
|
2383
2507
|
sourceManagerShort: 'SSH',
|
|
2384
2508
|
remoteGroup: 'Remote directories',
|
|
@@ -2689,6 +2813,7 @@ function fileIconSpec(name, isDir) {
|
|
|
2689
2813
|
FS_IS_DIR: '目标是目录',
|
|
2690
2814
|
FS_NOT_DIR: '目标不是目录',
|
|
2691
2815
|
FS_TOO_LARGE: '文件过大',
|
|
2816
|
+
FS_EXISTS: '目标已存在,请使用其他名称',
|
|
2692
2817
|
CONFLICT: '远端文件已被修改',
|
|
2693
2818
|
BAD_REQUEST: '请求参数无效',
|
|
2694
2819
|
},
|
|
@@ -2703,6 +2828,7 @@ function fileIconSpec(name, isDir) {
|
|
|
2703
2828
|
FS_IS_DIR: 'That path is a directory',
|
|
2704
2829
|
FS_NOT_DIR: 'That path is not a directory',
|
|
2705
2830
|
FS_TOO_LARGE: 'The file is too large',
|
|
2831
|
+
FS_EXISTS: 'The target already exists; choose another name',
|
|
2706
2832
|
CONFLICT: 'The remote file changed',
|
|
2707
2833
|
BAD_REQUEST: 'Invalid request',
|
|
2708
2834
|
},
|
|
@@ -2783,7 +2909,7 @@ function fileIconSpec(name, isDir) {
|
|
|
2783
2909
|
parsed.querySelectorAll('script,style,iframe,object,embed,link,meta,base,form').forEach(function (node) { node.remove(); });
|
|
2784
2910
|
parsed.querySelectorAll('*').forEach(function (element) {
|
|
2785
2911
|
Array.from(element.attributes).forEach(function (attribute) {
|
|
2786
|
-
if (
|
|
2912
|
+
if (attributeViolation(attribute.name, attribute.value, element.tagName)) {
|
|
2787
2913
|
element.removeAttribute(attribute.name);
|
|
2788
2914
|
}
|
|
2789
2915
|
});
|
|
@@ -3049,6 +3175,9 @@ function fileIconSpec(name, isDir) {
|
|
|
3049
3175
|
'.rw-tree-body{padding:2px 4px 6px;border-top:1px solid var(--dsw-alias-border-l1)}',
|
|
3050
3176
|
'.rw-row{display:flex;align-items:center;gap:5px;width:100%;min-width:0;height:23px;padding:0 6px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;cursor:pointer;text-align:left;box-sizing:border-box}',
|
|
3051
3177
|
'.rw-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}',
|
|
3178
|
+
'.rw-row:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary,#4176e6));outline-offset:-1px}',
|
|
3179
|
+
'.rw-input:focus-visible,.rw-textarea:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary,#4176e6));outline-offset:-1px}',
|
|
3180
|
+
'.rw-btn:focus-visible,.rw-doc-tab:focus-visible,.rw-icon-btn:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary,#4176e6));outline-offset:1px}',
|
|
3052
3181
|
'.rw-row.is-active{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent)}',
|
|
3053
3182
|
'.rw-row-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 auto;min-width:0}',
|
|
3054
3183
|
'.rw-row-size{flex:none;font-size:10.5px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}',
|
|
@@ -3176,6 +3305,10 @@ function fileIconSpec(name, isDir) {
|
|
|
3176
3305
|
'.rw-doc-table td{border:1px solid var(--dsw-alias-border-l2);padding:0}',
|
|
3177
3306
|
'.rw-doc-table input{width:100%;border:0;background:transparent;padding:4px 5px;font:inherit;color:var(--dsw-alias-label-primary);outline:none;box-sizing:border-box}',
|
|
3178
3307
|
'.rw-doc-tabs{display:flex;gap:3px;padding:5px;border-bottom:1px solid var(--dsw-alias-border-l1);overflow:auto;flex:none}',
|
|
3308
|
+
'.rw-doc-note{padding:3px 10px;font-size:11px;color:var(--dsw-alias-label-tertiary);border-bottom:1px solid var(--dsw-alias-border-l1)}',
|
|
3309
|
+
'.rw-load-more{width:100%;border:0;background:transparent;cursor:pointer;text-align:left;font:inherit;font-size:11.5px;color:var(--dsw-alias-brand-primary)}',
|
|
3310
|
+
'.rw-load-more:hover{text-decoration:underline}',
|
|
3311
|
+
'.rw-load-more:disabled{opacity:.6;cursor:default}',
|
|
3179
3312
|
'.rw-doc-tab{padding:2px 8px;border:1px solid transparent;border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11.5px;cursor:pointer;white-space:nowrap}',
|
|
3180
3313
|
'.rw-doc-tab.is-on{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}',
|
|
3181
3314
|
'.rw-doc-grid{width:100%;border-collapse:collapse;font-size:11.5px}',
|
|
@@ -3643,6 +3776,10 @@ function fileIconSpec(name, isDir) {
|
|
|
3643
3776
|
|
|
3644
3777
|
var listing = local.listing;
|
|
3645
3778
|
var entries = listing ? listing.entries : [];
|
|
3779
|
+
// A folder with thousands of sub-folders must not build thousands of DOM
|
|
3780
|
+
// rows; the input box below still accepts any path.
|
|
3781
|
+
var PICKER_LIMIT = 500;
|
|
3782
|
+
var shownEntries = entries.slice(0, PICKER_LIMIT);
|
|
3646
3783
|
var crumbs = useMemo(function () {
|
|
3647
3784
|
if (!listing || !listing.path) return [];
|
|
3648
3785
|
var path = listing.path;
|
|
@@ -3695,12 +3832,15 @@ function fileIconSpec(name, isDir) {
|
|
|
3695
3832
|
? h('div', { className: 'rw-row-msg' }, IconAlert(12), local.error)
|
|
3696
3833
|
: entries.length === 0
|
|
3697
3834
|
? h('div', { className: 'rw-row-msg' }, t('emptyFolder'))
|
|
3698
|
-
:
|
|
3835
|
+
: shownEntries.map(function (entry) {
|
|
3699
3836
|
return h('button', {
|
|
3700
3837
|
key: entry.path, type: 'button', className: 'rw-browser-row', role: 'listitem',
|
|
3701
3838
|
onClick: function () { navigate(entry.path); },
|
|
3702
3839
|
}, h('span', { className: 'rw-dir-icon' }, IconFolder(13, false)), h('span', null, entry.name));
|
|
3703
|
-
})
|
|
3840
|
+
}),
|
|
3841
|
+
entries.length > PICKER_LIMIT
|
|
3842
|
+
? h('div', { className: 'rw-row-msg' }, t('pickerTruncated', { count: PICKER_LIMIT }))
|
|
3843
|
+
: null),
|
|
3704
3844
|
h('div', { className: 'rw-browser-actions', style: { padding: '8px', gap: 8 } },
|
|
3705
3845
|
h('div', { className: 'rw-inline', style: { minWidth: 0, flex: '1 1 auto' } },
|
|
3706
3846
|
h('input', {
|
|
@@ -3772,6 +3912,9 @@ function fileIconSpec(name, isDir) {
|
|
|
3772
3912
|
|
|
3773
3913
|
var listing = remote.listing;
|
|
3774
3914
|
var dirs = listing ? (listing.entries || []).filter(function (entry) { return entry.type === 'dir'; }) : [];
|
|
3915
|
+
var PICKER_LIMIT = 500;
|
|
3916
|
+
var shownDirs = dirs.slice(0, PICKER_LIMIT);
|
|
3917
|
+
var pickerTruncated = dirs.length > PICKER_LIMIT || Boolean(listing && listing.hasMore);
|
|
3775
3918
|
var crumbs = useMemo(function () {
|
|
3776
3919
|
if (!listing || !listing.path) return [];
|
|
3777
3920
|
var pieces = listing.path.split('/').filter(function (piece) { return piece !== ''; });
|
|
@@ -3803,12 +3946,15 @@ function fileIconSpec(name, isDir) {
|
|
|
3803
3946
|
? h('div', { className: 'rw-row-msg' }, IconAlert(12), remote.error)
|
|
3804
3947
|
: dirs.length === 0
|
|
3805
3948
|
? h('div', { className: 'rw-row-msg' }, t('noEntries'))
|
|
3806
|
-
:
|
|
3949
|
+
: shownDirs.map(function (entry) {
|
|
3807
3950
|
return h('button', {
|
|
3808
3951
|
key: entry.path, type: 'button', className: 'rw-browser-row', role: 'listitem',
|
|
3809
3952
|
onClick: function () { navigate(entry.path); },
|
|
3810
3953
|
}, h('span', { className: 'rw-dir-icon' }, IconFolder(13, false)), h('span', null, entry.name));
|
|
3811
|
-
})
|
|
3954
|
+
}),
|
|
3955
|
+
pickerTruncated
|
|
3956
|
+
? h('div', { className: 'rw-row-msg' }, t('pickerTruncated', { count: PICKER_LIMIT }))
|
|
3957
|
+
: null),
|
|
3812
3958
|
h('div', { className: 'rw-browser-actions', style: { padding: '8px' } },
|
|
3813
3959
|
h('span', { className: 'rw-hint rw-input-mono', title: remote.input || (listing && listing.path) || '' },
|
|
3814
3960
|
(listing && listing.path) || (remote.input !== '' ? remote.input : t('home'))),
|
|
@@ -4507,27 +4653,45 @@ function fileIconSpec(name, isDir) {
|
|
|
4507
4653
|
});
|
|
4508
4654
|
}
|
|
4509
4655
|
|
|
4656
|
+
function listRequest(path, offset, limit) {
|
|
4657
|
+
var params = { path: path, limit: limit || TREE_PAGE_SIZE, offset: offset };
|
|
4658
|
+
return props.kind === 'local'
|
|
4659
|
+
? apiCall('local.list', params)
|
|
4660
|
+
: apiCall('fs.list', Object.assign(params, scopeParams(props.scope)));
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4663
|
+
function applyEntries(list) {
|
|
4664
|
+
return props.showHidden ? list : list.filter(function (entry) {
|
|
4665
|
+
return String(entry.name || '').charAt(0) !== '.';
|
|
4666
|
+
});
|
|
4667
|
+
}
|
|
4668
|
+
|
|
4510
4669
|
function load(path, silent) {
|
|
4511
4670
|
if (pending.current.has(path)) return;
|
|
4512
4671
|
pending.current.add(path);
|
|
4672
|
+
var previousRecord = cache.entries.get(path) || {};
|
|
4673
|
+
// A silent refresh keeps as many rows as the user already loaded:
|
|
4674
|
+
// re-reading only the first page would drop everything past it.
|
|
4675
|
+
var keep = silent ? Math.min(Math.max(Number(previousRecord.raw) || 0, TREE_PAGE_SIZE), TREE_PAGE_MAX) : TREE_PAGE_SIZE;
|
|
4513
4676
|
setCache(function (previous) {
|
|
4514
4677
|
var entries = new Map(previous.entries);
|
|
4515
4678
|
var existing = entries.get(path) || {};
|
|
4516
|
-
entries.set(path, { loading: !silent, error: null, entries: existing.entries || [], loaded: existing.loaded });
|
|
4679
|
+
entries.set(path, { loading: !silent, error: null, entries: existing.entries || [], loaded: existing.loaded, raw: existing.raw, total: existing.total, hasMore: existing.hasMore });
|
|
4517
4680
|
return { entries: entries };
|
|
4518
4681
|
});
|
|
4519
|
-
|
|
4520
|
-
? apiCall('local.list', { path: path })
|
|
4521
|
-
: apiCall('fs.list', Object.assign({ path: path }, scopeParams(props.scope)));
|
|
4522
|
-
request.then(function (result) {
|
|
4682
|
+
listRequest(path, 0, keep).then(function (result) {
|
|
4523
4683
|
pending.current.delete(path);
|
|
4524
|
-
var entries = result.entries || [];
|
|
4525
|
-
if (!props.showHidden) {
|
|
4526
|
-
entries = entries.filter(function (entry) { return String(entry.name || '').charAt(0) !== '.'; });
|
|
4527
|
-
}
|
|
4528
4684
|
setCache(function (previous) {
|
|
4529
4685
|
var next = new Map(previous.entries);
|
|
4530
|
-
next.set(path, {
|
|
4686
|
+
next.set(path, {
|
|
4687
|
+
loading: false,
|
|
4688
|
+
error: null,
|
|
4689
|
+
loaded: true,
|
|
4690
|
+
entries: applyEntries(result.entries || []),
|
|
4691
|
+
raw: (result.entries || []).length,
|
|
4692
|
+
total: Number(result.total) || (result.entries || []).length,
|
|
4693
|
+
hasMore: Boolean(result.hasMore),
|
|
4694
|
+
});
|
|
4531
4695
|
return { entries: next };
|
|
4532
4696
|
});
|
|
4533
4697
|
setExpanded(function (previous) {
|
|
@@ -4545,7 +4709,45 @@ function fileIconSpec(name, isDir) {
|
|
|
4545
4709
|
});
|
|
4546
4710
|
}
|
|
4547
4711
|
|
|
4548
|
-
|
|
4712
|
+
function loadMore(path) {
|
|
4713
|
+
var record = cache.entries.get(path);
|
|
4714
|
+
if (!record || record.loadingMore || !record.hasMore) return;
|
|
4715
|
+
// Offset is the number of RAW entries already fetched, not the number of
|
|
4716
|
+
// visible rows: hidden entries are filtered out of the list but still
|
|
4717
|
+
// occupy positions in the server-side listing.
|
|
4718
|
+
var offset = Number(record.raw);
|
|
4719
|
+
if (!Number.isFinite(offset)) offset = (record.entries || []).length;
|
|
4720
|
+
setCache(function (previous) {
|
|
4721
|
+
var next = new Map(previous.entries);
|
|
4722
|
+
var current = next.get(path);
|
|
4723
|
+
if (current) next.set(path, Object.assign({}, current, { loadingMore: true }));
|
|
4724
|
+
return { entries: next };
|
|
4725
|
+
});
|
|
4726
|
+
listRequest(path, offset).then(function (result) {
|
|
4727
|
+
setCache(function (previous) {
|
|
4728
|
+
var next = new Map(previous.entries);
|
|
4729
|
+
var current = next.get(path) || { entries: [] };
|
|
4730
|
+
next.set(path, Object.assign({}, current, {
|
|
4731
|
+
loadingMore: false,
|
|
4732
|
+
loaded: true,
|
|
4733
|
+
entries: current.entries.concat(applyEntries(result.entries || [])),
|
|
4734
|
+
raw: offset + (result.entries || []).length,
|
|
4735
|
+
total: Number(result.total) || current.total,
|
|
4736
|
+
hasMore: Boolean(result.hasMore),
|
|
4737
|
+
}));
|
|
4738
|
+
return { entries: next };
|
|
4739
|
+
});
|
|
4740
|
+
}).catch(function (error) {
|
|
4741
|
+
setCache(function (previous) {
|
|
4742
|
+
var next = new Map(previous.entries);
|
|
4743
|
+
var current = next.get(path);
|
|
4744
|
+
if (current) next.set(path, Object.assign({}, current, { loadingMore: false, moreError: error }));
|
|
4745
|
+
return { entries: next };
|
|
4746
|
+
});
|
|
4747
|
+
});
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4750
|
+
return h('div', { className: 'rw-tree', role: 'tree', 'aria-label': t('allFiles') }, h(TreeDirectory, {
|
|
4549
4751
|
path: rootPath,
|
|
4550
4752
|
name: props.rootName || t('allFiles'),
|
|
4551
4753
|
depth: 0,
|
|
@@ -4556,12 +4758,13 @@ function fileIconSpec(name, isDir) {
|
|
|
4556
4758
|
toggle: toggle,
|
|
4557
4759
|
state: { entries: cache.entries },
|
|
4558
4760
|
load: load,
|
|
4761
|
+
loadMore: loadMore,
|
|
4559
4762
|
onSelect: props.onSelect,
|
|
4560
4763
|
onPin: props.onPin,
|
|
4561
4764
|
onSelectPath: props.onSelectPath,
|
|
4562
4765
|
onContextMenu: props.onContextMenu,
|
|
4563
4766
|
selectedPath: props.selectedPath || '',
|
|
4564
|
-
});
|
|
4767
|
+
}));
|
|
4565
4768
|
}
|
|
4566
4769
|
|
|
4567
4770
|
function TreeSection(props) {
|
|
@@ -4883,20 +5086,46 @@ function fileIconSpec(name, isDir) {
|
|
|
4883
5086
|
var state = useState(props.value || '');
|
|
4884
5087
|
var value = state[0];
|
|
4885
5088
|
var setValue = state[1];
|
|
4886
|
-
|
|
4887
|
-
|
|
5089
|
+
var statusState = useState({ busy: false, error: '' });
|
|
5090
|
+
var status = statusState[0];
|
|
5091
|
+
var setStatus = statusState[1];
|
|
5092
|
+
var pendingRef = useRef(false);
|
|
5093
|
+
var mountedRef = useRef(true);
|
|
5094
|
+
useEffect(function () {
|
|
5095
|
+
mountedRef.current = true;
|
|
5096
|
+
return function () { mountedRef.current = false; };
|
|
5097
|
+
}, []);
|
|
5098
|
+
function close() { if (!pendingRef.current) props.onClose(); }
|
|
5099
|
+
function submit() {
|
|
5100
|
+
if (value.trim() === '' || pendingRef.current) return;
|
|
5101
|
+
pendingRef.current = true;
|
|
5102
|
+
setStatus({ busy: true, error: '' });
|
|
5103
|
+
Promise.resolve().then(function () { return props.onSubmit(value.trim()); }).then(function () {
|
|
5104
|
+
pendingRef.current = false;
|
|
5105
|
+
if (mountedRef.current) setStatus({ busy: false, error: '' });
|
|
5106
|
+
}).catch(function (error) {
|
|
5107
|
+
pendingRef.current = false;
|
|
5108
|
+
if (mountedRef.current) setStatus({ busy: false, error: messageOf(error, props.t, props.locale) });
|
|
5109
|
+
});
|
|
5110
|
+
}
|
|
5111
|
+
return h(DialogShell, { title: props.title, onClose: close },
|
|
4888
5112
|
h('label', { className: 'rw-dialog-label' }, props.label),
|
|
4889
5113
|
h('input', {
|
|
4890
5114
|
className: 'rw-dialog-input',
|
|
4891
5115
|
value: value,
|
|
5116
|
+
disabled: status.busy,
|
|
4892
5117
|
autoFocus: true,
|
|
4893
5118
|
spellCheck: false,
|
|
4894
5119
|
onChange: function (event) { setValue(event.currentTarget.value); },
|
|
4895
|
-
onKeyDown: function (event) {
|
|
5120
|
+
onKeyDown: function (event) {
|
|
5121
|
+
if (event.key === 'Enter' && (!event.nativeEvent || !event.nativeEvent.isComposing)) { event.preventDefault(); submit(); }
|
|
5122
|
+
else if (event.key === 'Escape') close();
|
|
5123
|
+
},
|
|
4896
5124
|
}),
|
|
5125
|
+
status.error ? h(Banner, { kind: 'error' }, status.error) : null,
|
|
4897
5126
|
h('div', { className: 'rw-dialog-actions' },
|
|
4898
|
-
h('button', { type: 'button', className: 'rw-btn rw-btn-sm',
|
|
4899
|
-
h('button', { type: 'button', className: 'rw-btn rw-btn-sm rw-btn-primary', onClick: submit }, props.t('confirm'))));
|
|
5127
|
+
h('button', { type: 'button', className: 'rw-btn rw-btn-sm', disabled: status.busy, onClick: close }, props.t('cancel')),
|
|
5128
|
+
h('button', { type: 'button', className: 'rw-btn rw-btn-sm rw-btn-primary', disabled: status.busy || value.trim() === '', onClick: submit }, props.t(status.busy ? 'loading' : 'confirm'))));
|
|
4900
5129
|
}
|
|
4901
5130
|
|
|
4902
5131
|
function ConfirmDialog(props) {
|
|
@@ -5225,6 +5454,12 @@ function fileIconSpec(name, isDir) {
|
|
|
5225
5454
|
h('div', { className: 'rw-doc-tabs' }, sheets.map(function (sheet, index) {
|
|
5226
5455
|
return h('button', { key: sheet.name, type: 'button', className: 'rw-doc-tab' + (index === state.sheet ? ' is-on' : ''), onClick: function () { patch({ sheet: index }); } }, sheet.name);
|
|
5227
5456
|
})),
|
|
5457
|
+
active.truncated
|
|
5458
|
+
? h('div', { className: 'rw-doc-note' }, t('sheetTruncated', { rows: active.maxRow || 0, cols: active.maxCol || 0 }))
|
|
5459
|
+
: null,
|
|
5460
|
+
(state.data.sheetCount || sheets.length) > sheets.length
|
|
5461
|
+
? h('div', { className: 'rw-doc-note' }, t('sheetMore'))
|
|
5462
|
+
: null,
|
|
5228
5463
|
h('div', { style: { flex: '1 1 auto', overflow: 'auto', padding: 8 } },
|
|
5229
5464
|
h('table', { className: 'rw-doc-grid' },
|
|
5230
5465
|
h('thead', null, header),
|
|
@@ -5814,27 +6049,13 @@ function fileIconSpec(name, isDir) {
|
|
|
5814
6049
|
}
|
|
5815
6050
|
|
|
5816
6051
|
function submitNew(side, dirPath, name, isDir) {
|
|
5817
|
-
var
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
if (isDir) {
|
|
5821
|
-
promise = side === 'remote'
|
|
5822
|
-
? apiCall('fs.mkdir', Object.assign({ path: target }, scopeParams(scopeRef)))
|
|
5823
|
-
: apiCall('local.mkdir', { path: dirPath, name: name });
|
|
5824
|
-
} else if (docKind) {
|
|
5825
|
-
promise = side === 'remote'
|
|
5826
|
-
? apiCall('docs.create', Object.assign({ path: target, kind: docKind }, scopeParams(scopeRef)))
|
|
5827
|
-
: apiCall('docs.create', { path: target, kind: docKind });
|
|
5828
|
-
} else {
|
|
5829
|
-
promise = side === 'remote'
|
|
5830
|
-
? apiCall('fs.write', Object.assign({ path: target, content: '' }, scopeParams(scopeRef)))
|
|
5831
|
-
: apiCall('local.newFile', { path: target });
|
|
5832
|
-
}
|
|
5833
|
-
promise.then(function () {
|
|
6052
|
+
var request = newEntryRequest(side, dirPath, name, isDir, scopeParams(scopeRef));
|
|
6053
|
+
return apiCall(request.method, request.params).then(function () {
|
|
6054
|
+
setDialog(null);
|
|
5834
6055
|
showNotice('success', t('newDone'));
|
|
5835
6056
|
refreshTree();
|
|
5836
|
-
if (!isDir &&
|
|
5837
|
-
})
|
|
6057
|
+
if (!isDir && request.kind) openInternalFile(side, request.path);
|
|
6058
|
+
});
|
|
5838
6059
|
}
|
|
5839
6060
|
|
|
5840
6061
|
function submitChmod(side, targetPath, mode) {
|
|
@@ -5849,9 +6070,9 @@ function fileIconSpec(name, isDir) {
|
|
|
5849
6070
|
}
|
|
5850
6071
|
|
|
5851
6072
|
function submitDialog(current, value) {
|
|
6073
|
+
if (current.kind === 'new') return submitNew(current.side, current.dir, value, current.isDir);
|
|
5852
6074
|
setDialog(null);
|
|
5853
6075
|
if (current.kind === 'rename') submitRenamed(current.side, current.path, value);
|
|
5854
|
-
else if (current.kind === 'new') submitNew(current.side, current.dir, value, current.isDir);
|
|
5855
6076
|
else if (current.kind === 'chmod') submitChmod(current.side, current.path, value);
|
|
5856
6077
|
}
|
|
5857
6078
|
|
|
@@ -5911,14 +6132,8 @@ function fileIconSpec(name, isDir) {
|
|
|
5911
6132
|
setDialog({ kind: 'chmod', side: menu.side, path: menu.path, title: t('chmodTitle'), label: t('chmodLabel'), value: '644' });
|
|
5912
6133
|
} });
|
|
5913
6134
|
if (menu.isDir) {
|
|
5914
|
-
items.push({
|
|
5915
|
-
|
|
5916
|
-
setDialog({ kind: 'new', side: menu.side, dir: menu.path, isDir: true, title: t('newFolderTitle'), label: t('nameLabel'), value: t('newFolderTitle') });
|
|
5917
|
-
} },
|
|
5918
|
-
{ label: t('ctxNewFile'), onSelect: function () {
|
|
5919
|
-
setDialog({ kind: 'new', side: menu.side, dir: menu.path, isDir: false, title: t('newFileTitle'), label: t('nameLabel'), value: t('newFileTitle') });
|
|
5920
|
-
} },
|
|
5921
|
-
] });
|
|
6135
|
+
items.push({ separator: true });
|
|
6136
|
+
items = items.concat(newEntryMenuItems(menu, t, setDialog));
|
|
5922
6137
|
}
|
|
5923
6138
|
items.push({ separator: true });
|
|
5924
6139
|
items.push({ label: t('ctxProperties'), onSelect: function () { showProperties(menu.side, menu.path); } });
|
|
@@ -5979,11 +6194,12 @@ function fileIconSpec(name, isDir) {
|
|
|
5979
6194
|
}
|
|
5980
6195
|
return h(PromptDialog, {
|
|
5981
6196
|
t: t,
|
|
6197
|
+
locale: locale,
|
|
5982
6198
|
title: dialog.title,
|
|
5983
6199
|
label: dialog.label,
|
|
5984
6200
|
value: dialog.value,
|
|
5985
6201
|
onClose: function () { setDialog(null); },
|
|
5986
|
-
onSubmit: function (value) { submitDialog(dialog, value); },
|
|
6202
|
+
onSubmit: function (value) { return submitDialog(dialog, value); },
|
|
5987
6203
|
});
|
|
5988
6204
|
}
|
|
5989
6205
|
|
|
@@ -6348,6 +6564,7 @@ function fileIconSpec(name, isDir) {
|
|
|
6348
6564
|
toggle: toggle,
|
|
6349
6565
|
state: state,
|
|
6350
6566
|
load: props.load,
|
|
6567
|
+
loadMore: props.loadMore,
|
|
6351
6568
|
onSelect: onSelect,
|
|
6352
6569
|
onPin: onPin,
|
|
6353
6570
|
onSelectPath: onSelectPath,
|
|
@@ -6359,6 +6576,9 @@ function fileIconSpec(name, isDir) {
|
|
|
6359
6576
|
children.push(h('button', {
|
|
6360
6577
|
key: child.path,
|
|
6361
6578
|
type: 'button',
|
|
6579
|
+
role: 'treeitem',
|
|
6580
|
+
'aria-level': depth + 1,
|
|
6581
|
+
'aria-selected': active,
|
|
6362
6582
|
className: 'rw-row' + (active ? ' is-active' : ''),
|
|
6363
6583
|
style: { paddingLeft: 6 + (depth + 1) * 12 },
|
|
6364
6584
|
title: child.path,
|
|
@@ -6380,12 +6600,28 @@ function fileIconSpec(name, isDir) {
|
|
|
6380
6600
|
h('span', { className: 'rw-row-size' }, formatBytes(child.size))));
|
|
6381
6601
|
}
|
|
6382
6602
|
});
|
|
6603
|
+
if (entry.hasMore) {
|
|
6604
|
+
children.push(h('button', {
|
|
6605
|
+
key: 'more',
|
|
6606
|
+
type: 'button',
|
|
6607
|
+
className: 'rw-row-msg rw-load-more',
|
|
6608
|
+
style: { paddingLeft: 6 + (depth + 1) * 12 },
|
|
6609
|
+
disabled: Boolean(entry.loadingMore),
|
|
6610
|
+
onClick: function () { if (props.loadMore) props.loadMore(path); },
|
|
6611
|
+
}, entry.loadingMore ? Spinner(12) : null,
|
|
6612
|
+
entry.moreError
|
|
6613
|
+
? messageOf(entry.moreError, t, locale)
|
|
6614
|
+
: t('loadMore', { count: Math.max((entry.total || 0) - (Number.isFinite(entry.raw) ? entry.raw : entry.entries.length), 0) })));
|
|
6615
|
+
}
|
|
6383
6616
|
}
|
|
6384
6617
|
}
|
|
6385
6618
|
|
|
6386
6619
|
return h(React.Fragment, null,
|
|
6387
6620
|
h('button', {
|
|
6388
6621
|
type: 'button',
|
|
6622
|
+
role: 'treeitem',
|
|
6623
|
+
'aria-level': depth + 1,
|
|
6624
|
+
'aria-expanded': isOpen,
|
|
6389
6625
|
className: 'rw-row' + (selectedPath === path ? ' is-active' : ''),
|
|
6390
6626
|
style: { paddingLeft: 6 + depth * 12 },
|
|
6391
6627
|
title: path,
|
|
@@ -6393,13 +6629,16 @@ function fileIconSpec(name, isDir) {
|
|
|
6393
6629
|
if (onSelectPath) onSelectPath({ path: path, name: name, isDir: true });
|
|
6394
6630
|
if (event.detail !== 2) toggle(path);
|
|
6395
6631
|
},
|
|
6632
|
+
onKeyDown: function (event) {
|
|
6633
|
+
if (event.key === 'ArrowRight' && !isOpen) { event.preventDefault(); toggle(path); }
|
|
6634
|
+
else if (event.key === 'ArrowLeft' && isOpen) { event.preventDefault(); toggle(path); }
|
|
6635
|
+
},
|
|
6396
6636
|
onContextMenu: function (event) {
|
|
6397
6637
|
if (!onContextMenu) return;
|
|
6398
6638
|
event.preventDefault();
|
|
6399
6639
|
event.stopPropagation();
|
|
6400
6640
|
onContextMenu({ path: path, name: name, isDir: true, event: event });
|
|
6401
6641
|
},
|
|
6402
|
-
'aria-expanded': isOpen,
|
|
6403
6642
|
},
|
|
6404
6643
|
h('span', { className: 'rw-chevron' + (isOpen ? ' is-open' : '') }, IconChevron(12)),
|
|
6405
6644
|
h(FileGlyph, { name: name, dir: true, open: isOpen, size: 13 }),
|
|
@@ -6430,22 +6669,35 @@ function fileIconSpec(name, isDir) {
|
|
|
6430
6669
|
});
|
|
6431
6670
|
}
|
|
6432
6671
|
|
|
6672
|
+
function listRequest(path, offset) {
|
|
6673
|
+
return Object.assign({ path: path, limit: TREE_PAGE_SIZE, offset: offset },
|
|
6674
|
+
source.kind === 'linked' ? { linkedId: source.id } : { workspaceId: source.id });
|
|
6675
|
+
}
|
|
6676
|
+
|
|
6433
6677
|
function load(path) {
|
|
6678
|
+
// Ref-based guard: a state setter callback is not a lock, so checking
|
|
6679
|
+
// record.loading inside setCache would still fire duplicate requests.
|
|
6680
|
+
if (pending.current.has(path)) return;
|
|
6681
|
+
pending.current.add(path);
|
|
6434
6682
|
setCache(function (previous) {
|
|
6435
|
-
var record = previous.entries.get(path);
|
|
6436
|
-
if (record && record.loading) return previous;
|
|
6437
6683
|
var entries = new Map(previous.entries);
|
|
6438
6684
|
var existing = entries.get(path) || {};
|
|
6439
6685
|
entries.set(path, { loading: true, error: null, entries: existing.entries || [], loaded: existing.loaded });
|
|
6440
6686
|
return { entries: entries };
|
|
6441
6687
|
});
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
: { workspaceId: source.id, path: path };
|
|
6445
|
-
apiCall('fs.list', request).then(function (result) {
|
|
6688
|
+
apiCall('fs.list', listRequest(path, 0)).then(function (result) {
|
|
6689
|
+
pending.current.delete(path);
|
|
6446
6690
|
setCache(function (previous) {
|
|
6447
6691
|
var entries = new Map(previous.entries);
|
|
6448
|
-
entries.set(path, {
|
|
6692
|
+
entries.set(path, {
|
|
6693
|
+
loading: false,
|
|
6694
|
+
error: null,
|
|
6695
|
+
loaded: true,
|
|
6696
|
+
entries: result.entries || [],
|
|
6697
|
+
raw: (result.entries || []).length,
|
|
6698
|
+
total: Number(result.total) || (result.entries || []).length,
|
|
6699
|
+
hasMore: Boolean(result.hasMore),
|
|
6700
|
+
});
|
|
6449
6701
|
return { entries: entries };
|
|
6450
6702
|
});
|
|
6451
6703
|
setExpanded(function (previous) {
|
|
@@ -6454,6 +6706,7 @@ function fileIconSpec(name, isDir) {
|
|
|
6454
6706
|
return next;
|
|
6455
6707
|
});
|
|
6456
6708
|
}).catch(function (error) {
|
|
6709
|
+
pending.current.delete(path);
|
|
6457
6710
|
setCache(function (previous) {
|
|
6458
6711
|
var entries = new Map(previous.entries);
|
|
6459
6712
|
entries.set(path, { loading: false, error: error, loaded: false, entries: [] });
|
|
@@ -6462,6 +6715,40 @@ function fileIconSpec(name, isDir) {
|
|
|
6462
6715
|
});
|
|
6463
6716
|
}
|
|
6464
6717
|
|
|
6718
|
+
function loadMore(path) {
|
|
6719
|
+
var record = cache.entries.get(path);
|
|
6720
|
+
if (!record || record.loadingMore || !record.hasMore) return;
|
|
6721
|
+
var offset = Number.isFinite(Number(record.raw)) ? Number(record.raw) : (record.entries || []).length;
|
|
6722
|
+
setCache(function (previous) {
|
|
6723
|
+
var entries = new Map(previous.entries);
|
|
6724
|
+
var current = entries.get(path);
|
|
6725
|
+
if (current) entries.set(path, Object.assign({}, current, { loadingMore: true }));
|
|
6726
|
+
return { entries: entries };
|
|
6727
|
+
});
|
|
6728
|
+
apiCall('fs.list', listRequest(path, offset)).then(function (result) {
|
|
6729
|
+
setCache(function (previous) {
|
|
6730
|
+
var entries = new Map(previous.entries);
|
|
6731
|
+
var current = entries.get(path) || { entries: [] };
|
|
6732
|
+
entries.set(path, Object.assign({}, current, {
|
|
6733
|
+
loadingMore: false,
|
|
6734
|
+
loaded: true,
|
|
6735
|
+
entries: current.entries.concat(result.entries || []),
|
|
6736
|
+
raw: offset + (result.entries || []).length,
|
|
6737
|
+
total: Number(result.total) || current.total,
|
|
6738
|
+
hasMore: Boolean(result.hasMore),
|
|
6739
|
+
}));
|
|
6740
|
+
return { entries: entries };
|
|
6741
|
+
});
|
|
6742
|
+
}).catch(function (error) {
|
|
6743
|
+
setCache(function (previous) {
|
|
6744
|
+
var entries = new Map(previous.entries);
|
|
6745
|
+
var current = entries.get(path);
|
|
6746
|
+
if (current) entries.set(path, Object.assign({}, current, { loadingMore: false, moreError: error }));
|
|
6747
|
+
return { entries: entries };
|
|
6748
|
+
});
|
|
6749
|
+
});
|
|
6750
|
+
}
|
|
6751
|
+
|
|
6465
6752
|
useEffect(function () {
|
|
6466
6753
|
if (!open) return;
|
|
6467
6754
|
var root = source.remotePath;
|
|
@@ -6490,7 +6777,7 @@ function fileIconSpec(name, isDir) {
|
|
|
6490
6777
|
h('span', { className: 'rw-tree-ws-sub' }, source.username + '@' + source.host + (source.kind === 'linked' ? ' · ' + t('linkedChip') : ''))),
|
|
6491
6778
|
h(StatusPill, { status: source.status, t: t })),
|
|
6492
6779
|
open
|
|
6493
|
-
? h('div', { className: 'rw-tree-body' },
|
|
6780
|
+
? h('div', { className: 'rw-tree-body', role: 'tree', 'aria-label': source.name },
|
|
6494
6781
|
h(TreeDirectory, {
|
|
6495
6782
|
path: source.remotePath,
|
|
6496
6783
|
name: '/',
|
|
@@ -6502,6 +6789,7 @@ function fileIconSpec(name, isDir) {
|
|
|
6502
6789
|
toggle: toggle,
|
|
6503
6790
|
state: { entries: cache.entries },
|
|
6504
6791
|
load: load,
|
|
6792
|
+
loadMore: loadMore,
|
|
6505
6793
|
onSelect: props.onSelect,
|
|
6506
6794
|
selectedPath: props.selectedPath,
|
|
6507
6795
|
}))
|
|
@@ -6778,10 +7066,58 @@ function fileIconSpec(name, isDir) {
|
|
|
6778
7066
|
});
|
|
6779
7067
|
}
|
|
6780
7068
|
|
|
7069
|
+
var modalRef = useRef(null);
|
|
7070
|
+
var focusablesOf = function () {
|
|
7071
|
+
var node = modalRef.current;
|
|
7072
|
+
if (!node) return [];
|
|
7073
|
+
return Array.prototype.filter.call(
|
|
7074
|
+
node.querySelectorAll('button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])'),
|
|
7075
|
+
function (element) { return !element.disabled && element.offsetParent !== null; });
|
|
7076
|
+
};
|
|
7077
|
+
|
|
7078
|
+
useEffect(function () {
|
|
7079
|
+
if (!props.open) return;
|
|
7080
|
+
var node = modalRef.current;
|
|
7081
|
+
if (!node) return;
|
|
7082
|
+
// Focus the first real field, not the tab strip, so typing works at once.
|
|
7083
|
+
var target = node.querySelector('.rw-modal-body input, .rw-modal-body textarea') || focusablesOf()[0];
|
|
7084
|
+
if (target && typeof target.focus === 'function') target.focus();
|
|
7085
|
+
}, [props.open, flow]);
|
|
7086
|
+
|
|
7087
|
+
function onModalKeyDown(event) {
|
|
7088
|
+
if (event.key === 'Escape') {
|
|
7089
|
+
event.preventDefault();
|
|
7090
|
+
event.stopPropagation();
|
|
7091
|
+
if (props.onCancel) props.onCancel();
|
|
7092
|
+
return;
|
|
7093
|
+
}
|
|
7094
|
+
if (event.key !== 'Tab') return;
|
|
7095
|
+
// Keep Tab inside the dialog: without this the focus escapes to the
|
|
7096
|
+
// page behind the backdrop.
|
|
7097
|
+
var focusables = focusablesOf();
|
|
7098
|
+
if (focusables.length === 0) return;
|
|
7099
|
+
var first = focusables[0];
|
|
7100
|
+
var last = focusables[focusables.length - 1];
|
|
7101
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
7102
|
+
event.preventDefault();
|
|
7103
|
+
last.focus();
|
|
7104
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
7105
|
+
event.preventDefault();
|
|
7106
|
+
first.focus();
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
|
|
6781
7110
|
if (!props.open) return null;
|
|
6782
7111
|
|
|
6783
7112
|
return h('div', { className: 'rw-modal-backdrop', role: 'presentation' },
|
|
6784
|
-
h('div', {
|
|
7113
|
+
h('div', {
|
|
7114
|
+
ref: modalRef,
|
|
7115
|
+
className: 'rw-modal',
|
|
7116
|
+
role: 'dialog',
|
|
7117
|
+
'aria-modal': 'true',
|
|
7118
|
+
'aria-label': t('flowTitle'),
|
|
7119
|
+
onKeyDown: onModalKeyDown,
|
|
7120
|
+
},
|
|
6785
7121
|
h('div', { className: 'rw-modal-head' },
|
|
6786
7122
|
h('span', { className: 'rw-icon-chip' }, IconLink(16)),
|
|
6787
7123
|
h('span', { className: 'rw-modal-title' }, t('flowTitle')),
|