arcane-os 0.42.2 → 0.43.0

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.
@@ -469,8 +469,15 @@
469
469
  directory=>directory.trim()
470
470
  ).filter(Boolean);
471
471
  const fileEntityPromise=import('../entities/File.js');
472
+ const printModulePromise=import('../modules/PrintView.js');
473
+ void printModulePromise.catch(function reportPrintModuleFailure(error){
474
+ arcaneLogging.error('Unable to load rendered printing:',error);
475
+ });
472
476
 
473
477
  let activePreview=null;
478
+ let presentationController = new AbortController();
479
+ let uploadPresentationController = null;
480
+ let closingTask = null;
474
481
  let markdownModulePromise=null;
475
482
  let uploadDirectory='';
476
483
  let initialized=false;
@@ -497,6 +504,8 @@
497
504
  host.setDirectoryFilter=setDirectoryFilter;
498
505
  host.select=select;
499
506
  host.clearSelection=clearSelection;
507
+ host.close = closeFileManager;
508
+ host.printPreview=printPreview;
500
509
  host.destroy=destroy;
501
510
 
502
511
  Object.defineProperty(
@@ -1457,22 +1466,20 @@
1457
1466
  }
1458
1467
  }
1459
1468
 
1460
- async function openDirectory(directory=''){
1461
- if(destroyed||!isVisibleDirectory(directory)){
1469
+ async function openDirectory(directory = '', signal = presentationController.signal) {
1470
+ if (closingTask && !await closingTask) {
1471
+ return false;
1472
+ }
1473
+ if(destroyed||signal.aborted||!isVisibleDirectory(directory)){
1462
1474
  return false;
1463
1475
  }
1464
1476
  const generation=++directoryOpenGeneration;
1465
- const modal=await waitForComponent(
1477
+ const modal = await waitForModal(
1466
1478
  directoryModal,
1467
- {
1468
- methods:['populate','open'],
1469
- property:'ready',
1470
- event:'modal-ready',
1471
- signal:lifecycleController.signal
1472
- },
1473
- {signal:lifecycleController.signal}
1479
+ ['populate', 'open'],
1480
+ signal
1474
1481
  );
1475
- if(destroyed||generation!==directoryOpenGeneration||!isVisibleDirectory(directory)){
1482
+ if(!modal||destroyed||generation!==directoryOpenGeneration||!isVisibleDirectory(directory)){
1476
1483
  return false;
1477
1484
  }
1478
1485
  const keys=await dbopfs.getAllKeys(directory);
@@ -1534,7 +1541,11 @@
1534
1541
  }
1535
1542
 
1536
1543
  async function openFile(directory='',fileName=''){
1537
- if(destroyed){
1544
+ const signal = presentationController.signal;
1545
+ if (closingTask && !await closingTask) {
1546
+ return false;
1547
+ }
1548
+ if(destroyed||signal.aborted){
1538
1549
  return false;
1539
1550
  }
1540
1551
  releasePreview(activePreview,'file-preview-replaced');
@@ -1543,7 +1554,12 @@
1543
1554
  controller:new AbortController(),
1544
1555
  generation,
1545
1556
  urls:new Set(),
1546
- media:new Set()
1557
+ media:new Set(),
1558
+ title:`${directory}/${fileName}`,
1559
+ rendered:null,
1560
+ printPreparation:null,
1561
+ printRetains:0,
1562
+ released:false
1547
1563
  };
1548
1564
  activePreview=preview;
1549
1565
  let modal;
@@ -1577,8 +1593,20 @@
1577
1593
  const deleteButton=document.createElement('button');
1578
1594
  const deleteIcon=document.createElement('img');
1579
1595
  const status=document.createElement('p');
1596
+ const printButton=document.createElement('button');
1580
1597
 
1581
1598
  heading.innerText=`${directory}/${fileName}`;
1599
+ printButton.type='button';
1600
+ printButton.textContent='Print';
1601
+ printButton.disabled=true;
1602
+ preview.printButton=printButton;
1603
+ preview.printStatus=status;
1604
+ preview.container=content;
1605
+ printButton.addEventListener('click',function printOpenPreview(){
1606
+ void printPreview().catch(function reportPrintRejection(error){
1607
+ reportPreviewPrintFailure(error,preview);
1608
+ });
1609
+ },{signal:preview.controller.signal});
1582
1610
  actions.className='file-view-actions';
1583
1611
  deleteButton.type='button';
1584
1612
  deleteButton.className='file-view-delete';
@@ -1598,7 +1626,7 @@
1598
1626
  };
1599
1627
  status.innerText='Opening file...';
1600
1628
  status.setAttribute('role','status');
1601
- actions.append(deleteButton);
1629
+ actions.append(printButton,deleteButton);
1602
1630
  content.append(
1603
1631
  styles,
1604
1632
  heading,
@@ -1631,6 +1659,10 @@
1631
1659
  }
1632
1660
  status.remove();
1633
1661
  content.append(rendered);
1662
+ preview.rendered=rendered;
1663
+ void preparePreviewPrint(preview).catch(function reportPrintPreparation(error){
1664
+ reportPreviewPrintFailure(error,preview);
1665
+ });
1634
1666
  return true;
1635
1667
  }catch(error){
1636
1668
  if(isCurrentPreview(preview)){
@@ -1643,6 +1675,69 @@
1643
1675
  }
1644
1676
  }
1645
1677
 
1678
+ function preparePreviewPrint(preview){
1679
+ if(!preview.printPreparation){
1680
+ preview.printPreparation=printModulePromise.then(function registerPreviewPrint({createPrintView}){
1681
+ if(!isCurrentPreview(preview)||!preview.rendered){
1682
+ return null;
1683
+ }
1684
+ const view=createPrintView({
1685
+ host,
1686
+ content:()=>preview.rendered,
1687
+ title:()=>preview.title,
1688
+ active:()=>isCurrentPreview(preview)&&fileModal.opened===true,
1689
+ priority:1,
1690
+ signal:preview.controller.signal,
1691
+ retain:()=>retainPreviewPrintResources(preview),
1692
+ onError:error=>reportPreviewPrintFailure(error,preview)
1693
+ });
1694
+ preview.printButton.disabled=false;
1695
+ return view;
1696
+ });
1697
+ }
1698
+ return preview.printPreparation;
1699
+ }
1700
+
1701
+ async function printPreview(){
1702
+ const preview=activePreview;
1703
+ if(!preview?.rendered||!isCurrentPreview(preview)||fileModal.opened!==true){
1704
+ return false;
1705
+ }
1706
+ const view=await preparePreviewPrint(preview);
1707
+ return view&&isCurrentPreview(preview)?view.print():false;
1708
+ }
1709
+
1710
+ function reportPreviewPrintFailure(error,preview){
1711
+ arcaneLogging.error('Unable to print file preview:',error);
1712
+ if(isCurrentPreview(preview)){
1713
+ preview.printStatus.textContent='Unable to open print preview. Please try again.';
1714
+ preview.printStatus.setAttribute('role','alert');
1715
+ preview.container.append(preview.printStatus);
1716
+ }
1717
+ }
1718
+
1719
+ function retainPreviewPrintResources(preview){
1720
+ preview.printRetains+=1;
1721
+ let retained=true;
1722
+ return function releasePrintedPreviewResources(){
1723
+ if(!retained){
1724
+ return;
1725
+ }
1726
+ retained=false;
1727
+ preview.printRetains-=1;
1728
+ if(preview.released&&preview.printRetains===0){
1729
+ releasePreviewURLs(preview);
1730
+ }
1731
+ };
1732
+ }
1733
+
1734
+ function releasePreviewURLs(preview){
1735
+ for(const url of preview.urls){
1736
+ URL.revokeObjectURL(url);
1737
+ }
1738
+ preview.urls.clear();
1739
+ }
1740
+
1646
1741
  function configureFileModal(modal){
1647
1742
  const appearance=getComputedStyle(host);
1648
1743
  for(const property of ['--modal-width','--modal-max-height','--modal-padding']){
@@ -1660,9 +1755,10 @@
1660
1755
  }
1661
1756
 
1662
1757
  function releasePreview(preview,reason){
1663
- if(!preview){
1758
+ if(!preview||preview.released){
1664
1759
  return;
1665
1760
  }
1761
+ preview.released=true;
1666
1762
  preview.controller.abort(reason);
1667
1763
  for(const element of preview.media){
1668
1764
  if(element.localName==='audio'){
@@ -1676,11 +1772,10 @@
1676
1772
  element.load();
1677
1773
  }
1678
1774
  }
1679
- for(const url of preview.urls){
1680
- URL.revokeObjectURL(url);
1775
+ if(preview.printRetains===0){
1776
+ releasePreviewURLs(preview);
1681
1777
  }
1682
1778
  preview.media.clear();
1683
- preview.urls.clear();
1684
1779
  if(activePreview===preview){
1685
1780
  activePreview=null;
1686
1781
  }
@@ -2091,8 +2186,12 @@
2091
2186
 
2092
2187
  openButton.addEventListener(
2093
2188
  'click',
2094
- async()=>{
2189
+ async function openSelectedFile() {
2190
+ const signal = presentationController.signal;
2095
2191
  await select(entry.path,{entry:entry});
2192
+ if (destroyed || signal.aborted) {
2193
+ return;
2194
+ }
2096
2195
  const detail={path:entry.path,entry:entry,trigger:'click'};
2097
2196
  const operationId=nextOperationId('open');
2098
2197
  const openAllowed=emit('file-manager-open',detail,true,operationId);
@@ -2130,21 +2229,21 @@
2130
2229
  }
2131
2230
 
2132
2231
  async function confirmDelete(directory='',fileName='',closeFile=false){
2133
- if(destroyed||!directory||!fileName){
2232
+ const signal = presentationController.signal;
2233
+ if (closingTask && !await closingTask) {
2234
+ return false;
2235
+ }
2236
+ if(destroyed||signal.aborted||!directory||!fileName){
2134
2237
  return false;
2135
2238
  }
2136
2239
  const generation=++deleteGeneration;
2137
2240
 
2138
- const modal=await waitForComponent(
2241
+ const modal = await waitForModal(
2139
2242
  deleteModal,
2140
- {
2141
- methods:['populate','open','close'],
2142
- property:'ready',
2143
- event:'modal-ready',
2144
- signal:lifecycleController.signal
2145
- }
2243
+ ['populate', 'open', 'close'],
2244
+ signal
2146
2245
  );
2147
- if(destroyed||generation!==deleteGeneration){
2246
+ if(!modal||destroyed||generation!==deleteGeneration){
2148
2247
  return false;
2149
2248
  }
2150
2249
  const content=document.createElement('section');
@@ -2168,12 +2267,19 @@
2168
2267
  deleteButton.addEventListener(
2169
2268
  'click',
2170
2269
  async function deleteSelectedFile(){
2270
+ if (destroyed || signal.aborted || generation !== deleteGeneration) {
2271
+ return;
2272
+ }
2171
2273
  cancelButton.disabled=true;
2172
2274
  deleteButton.disabled=true;
2173
2275
 
2174
2276
  try{
2175
2277
  await dbopfs.delete(directory,fileName);
2176
- if(destroyed||generation!==deleteGeneration){
2278
+ if(destroyed){
2279
+ return;
2280
+ }
2281
+ if (generation !== deleteGeneration) {
2282
+ await loadAll();
2177
2283
  return;
2178
2284
  }
2179
2285
  await modal.close(undefined,true);
@@ -2201,7 +2307,9 @@
2201
2307
  message.innerText=`Unable to delete ${fileName}.`;
2202
2308
  cancelButton.disabled=false;
2203
2309
  deleteButton.disabled=false;
2204
- arcaneLogging.error('Unable to delete file:',error);
2310
+ }
2311
+ if (!destroyed) {
2312
+ arcaneLogging.error('Unable to delete file:', error);
2205
2313
  }
2206
2314
  }
2207
2315
  },
@@ -2230,6 +2338,7 @@
2230
2338
  }
2231
2339
 
2232
2340
  uploadDirectory=directory;
2341
+ uploadPresentationController = presentationController;
2233
2342
  fileUpload.click();
2234
2343
  }
2235
2344
 
@@ -2239,6 +2348,8 @@
2239
2348
  }
2240
2349
  const files=Array.from(fileUpload.files||[]);
2241
2350
  const directory=uploadDirectory;
2351
+ const presentation = uploadPresentationController || presentationController;
2352
+ uploadPresentationController = null;
2242
2353
  fileUpload.value='';
2243
2354
 
2244
2355
  if(!files.length||!directory){
@@ -2269,8 +2380,8 @@
2269
2380
  return false;
2270
2381
  }
2271
2382
 
2272
- if(layout==='grid'){
2273
- await openDirectory(directory);
2383
+ if(layout==='grid' && !presentation.signal.aborted){
2384
+ await openDirectory(directory, presentation.signal);
2274
2385
  }
2275
2386
 
2276
2387
  const failed=results.find(
@@ -2309,6 +2420,76 @@
2309
2420
  }
2310
2421
  }
2311
2422
 
2423
+ async function waitForModal(modal, methods, signal) {
2424
+ try {
2425
+ return await waitForComponent(
2426
+ modal,
2427
+ {
2428
+ methods,
2429
+ property: 'ready',
2430
+ event: 'modal-ready',
2431
+ signal
2432
+ }
2433
+ );
2434
+ } catch (error) {
2435
+ if (signal.aborted) {
2436
+ return null;
2437
+ }
2438
+ throw error;
2439
+ }
2440
+ }
2441
+
2442
+ async function closeFileManager() {
2443
+ if (destroyed) {
2444
+ return false;
2445
+ }
2446
+ presentationController.abort('file-manager-closed');
2447
+ presentationController = new AbortController();
2448
+ treeOpenGeneration += 1;
2449
+ directoryOpenGeneration += 1;
2450
+ deleteGeneration += 1;
2451
+ selectionGeneration += 1;
2452
+ // Cancel unfinished opens now; a settled preview remains usable until
2453
+ // its modal actually closes, including when a child refuses to close.
2454
+ if (!activePreview?.rendered) {
2455
+ fileOpenGeneration += 1;
2456
+ releasePreview(activePreview, 'file-manager-closed');
2457
+ }
2458
+ if (closingTask) {
2459
+ return closingTask;
2460
+ }
2461
+
2462
+ closingTask = Promise.resolve().then(closeOwnedDialogs);
2463
+ try {
2464
+ return await closingTask;
2465
+ } finally {
2466
+ closingTask = null;
2467
+ }
2468
+ }
2469
+
2470
+ async function closeOwnedDialogs() {
2471
+ try {
2472
+ for (const modal of [deleteModal, fileModal, directoryModal]) {
2473
+ if (destroyed) {
2474
+ return false;
2475
+ }
2476
+ if (is.function(modal.close)) {
2477
+ const closed = await modal.close();
2478
+ if (closed === false) {
2479
+ return false;
2480
+ }
2481
+ }
2482
+ if (modal.opened === true) {
2483
+ return false;
2484
+ }
2485
+ }
2486
+ return !destroyed;
2487
+ } catch (error) {
2488
+ arcaneLogging.error('Unable to close file manager:', error);
2489
+ return false;
2490
+ }
2491
+ }
2492
+
2312
2493
  function destroy(){
2313
2494
  if(destroyed){
2314
2495
  return false;
@@ -2317,6 +2498,7 @@
2317
2498
  initialized=false;
2318
2499
  host.ready=false;
2319
2500
  lifecycleController.abort('file-manager-destroyed');
2501
+ presentationController.abort('file-manager-destroyed');
2320
2502
  treeLoadSequence+=1;
2321
2503
  gridLoadSequence+=1;
2322
2504
  providerGeneration+=1;
@@ -118,6 +118,7 @@
118
118
  <textarea id="entryMarkdown" class="entry-markdown" part="input"></textarea>
119
119
  <section class="actions" part="actions">
120
120
  <span id="status" class="status" part="status" aria-live="polite"></span>
121
+ <button id="print" part="print-action" type="button">Print</button>
121
122
  <button id="save" part="save-action" type="button" disabled></button>
122
123
  </section>
123
124
  </section>
@@ -143,6 +144,7 @@
143
144
  const toolbar=host.shadowRoot.querySelector('#toolbar');
144
145
  const textarea=host.shadowRoot.querySelector('#entryMarkdown');
145
146
  const saveButton=host.shadowRoot.querySelector('#save');
147
+ const printButton=host.shadowRoot.querySelector('#print');
146
148
  const status=host.shadowRoot.querySelector('#status');
147
149
 
148
150
  let previewFrame=0;
@@ -152,6 +154,23 @@
152
154
  let saveController=null;
153
155
  let destroyed=false;
154
156
  let options=normalizeMarkdownOptions(optionsFromDataset());
157
+ // Printing loads independently of editor readiness and owns no saved state.
158
+ const printViewPromise=import('../modules/PrintView.js').then(
159
+ function prepareEditorPrint({createPrintView}){
160
+ if(destroyed){
161
+ return null;
162
+ }
163
+ return createPrintView({
164
+ host,
165
+ content:renderedPrintContent,
166
+ title:()=>titleInput.value,
167
+ active:()=>!destroyed&&host.isConnected&&host.getClientRects().length>0,
168
+ signal:lifecycleController.signal,
169
+ onError:reportPrintFailure
170
+ });
171
+ }
172
+ );
173
+ void printViewPromise.catch(reportPrintFailure);
155
174
 
156
175
  function emit(type,detail={},publicDetail={},operationId=null){
157
176
  if(destroyed){
@@ -166,6 +185,8 @@
166
185
  host.destroy=destroy;
167
186
  host.focus=()=>{if(destroyed)return false;textarea.focus();return true;};
168
187
  host.saveEntry=saveEntry;
188
+ host.print=print;
189
+ host.insertMarkdown=insertMarkdown;
169
190
 
170
191
  Object.defineProperties(
171
192
  host,
@@ -184,6 +205,7 @@
184
205
  titleInput.addEventListener('input',contentChanged,{signal:lifecycleController.signal});
185
206
  toolbar.addEventListener('click',formatMarkdown,{signal:lifecycleController.signal});
186
207
  saveButton.addEventListener('click',saveEntry,{signal:lifecycleController.signal});
208
+ printButton.addEventListener('click',printFromControl,{signal:lifecycleController.signal});
187
209
  for(const type of ['wheel','pointerdown','touchstart','keydown']){
188
210
  preview.addEventListener(type,retainPreviewReadingPosition,{signal:lifecycleController.signal,passive:true});
189
211
  }
@@ -488,6 +510,24 @@
488
510
  contentChanged();
489
511
  }
490
512
 
513
+ function insertMarkdown(markdown){
514
+ if(destroyed||options.readOnly){
515
+ return false;
516
+ }
517
+ if(!is.string(markdown)){
518
+ throw new TypeError('Markdown insertion requires a string.');
519
+ }
520
+ const sourceView=captureSourceView();
521
+ textarea.setRangeText(markdown,sourceView.selectionStart,sourceView.selectionEnd,'end');
522
+ sourceView.selectionStart+=markdown.length;
523
+ sourceView.selectionEnd=sourceView.selectionStart;
524
+ sourceView.selectionDirection='none';
525
+ textarea.focus({preventScroll:true});
526
+ restoreSourceView(sourceView);
527
+ contentChanged();
528
+ return true;
529
+ }
530
+
491
531
  async function saveEntry(){
492
532
  if(destroyed||options.readOnly||!options.showSave){
493
533
  return false;
@@ -548,6 +588,36 @@
548
588
  return resetEditor(true);
549
589
  }
550
590
 
591
+ function renderedPrintContent(){
592
+ return preview;
593
+ }
594
+
595
+ async function print(){
596
+ if(destroyed){
597
+ return false;
598
+ }
599
+ const printView=await printViewPromise;
600
+ if(destroyed||!printView){
601
+ return false;
602
+ }
603
+ if(previewFrame){
604
+ updatePreview(false);
605
+ }
606
+ return printView.print();
607
+ }
608
+
609
+ function printFromControl(){
610
+ void print().catch(reportPrintFailure);
611
+ }
612
+
613
+ function reportPrintFailure(error){
614
+ if(destroyed||lifecycleController.signal.aborted){
615
+ return;
616
+ }
617
+ arcaneLogging.error('Unable to print rendered Markdown:',error);
618
+ status.innerText='Unable to open print preview. Please try again.';
619
+ }
620
+
551
621
  function resetEditor(supersede){
552
622
  if(destroyed){
553
623
  return false;
@@ -292,9 +292,13 @@
292
292
  modalStack.push(host);
293
293
 
294
294
  if(!wasOpen){
295
- returnFocus=document.activeElement instanceof HTMLElement
296
- ?document.activeElement
297
- :null;
295
+ let focusedElement = document.activeElement;
296
+ while (focusedElement?.shadowRoot?.activeElement) {
297
+ focusedElement = focusedElement.shadowRoot.activeElement;
298
+ }
299
+ returnFocus = focusedElement instanceof HTMLElement
300
+ ? focusedElement
301
+ : null;
298
302
  dialog.showModal();
299
303
  queueMicrotask(focusOpenedModal);
300
304
  emit('modal-opened',{}, {opened:true});
@@ -326,7 +330,7 @@
326
330
  const target=returnFocus;
327
331
  returnFocus=null;
328
332
  queueMicrotask(function restoreModalReturnFocus(){
329
- if(target?.isConnected){
333
+ if(target?.isConnected && !dialog.open){
330
334
  target.focus({preventScroll:true});
331
335
  }
332
336
  });