pptx-angular-viewer 1.1.58 → 1.1.60
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 +10 -0
- package/fesm2022/pptx-angular-viewer.mjs +1357 -276
- package/fesm2022/pptx-angular-viewer.mjs.map +1 -1
- package/package.json +14 -2
- package/pptx-angular-viewer.css +1 -1
- package/types/pptx-angular-viewer.d.ts +164 -21
|
@@ -22215,6 +22215,14 @@ function getDraggedShapeAdjustmentValue(state, deltaX) {
|
|
|
22215
22215
|
*/
|
|
22216
22216
|
/** Maximum time (ms) to wait for an initial WebSocket connection before giving up. */
|
|
22217
22217
|
const CONNECTION_TIMEOUT_MS = 30_000;
|
|
22218
|
+
/**
|
|
22219
|
+
* Grace period (ms) before the first local doc write when the provider has not
|
|
22220
|
+
* signalled initial sync. Websocket providers emit 'synced' reliably; webrtc
|
|
22221
|
+
* only syncs once a peer is present, so a lone (fresh-room) peer seeds the doc
|
|
22222
|
+
* after this delay instead. Gating the first write prevents a late joiner's
|
|
22223
|
+
* bootstrap deck from merging into a room whose real content has not arrived.
|
|
22224
|
+
*/
|
|
22225
|
+
const INITIAL_SYNC_GRACE_MS = 3_000;
|
|
22218
22226
|
/** Heartbeat interval (ms): re-publish presence so peers don't time us out. */
|
|
22219
22227
|
const PRESENCE_HEARTBEAT_MS = 10_000;
|
|
22220
22228
|
/** Minimum interval (ms) between outgoing presence broadcasts (rate limiting). */
|
|
@@ -22501,117 +22509,22 @@ function asSelectionIds(value) {
|
|
|
22501
22509
|
}
|
|
22502
22510
|
|
|
22503
22511
|
/**
|
|
22504
|
-
* collaboration-
|
|
22505
|
-
*
|
|
22512
|
+
* collaboration-text-codec.ts: TextSegment[] <-> Y.Text delta codec used by the
|
|
22513
|
+
* collaboration sync layer. Split out of collaboration-sync.ts to keep both
|
|
22514
|
+
* modules focused.
|
|
22506
22515
|
*
|
|
22507
22516
|
* Exports:
|
|
22508
|
-
* -
|
|
22509
|
-
* -
|
|
22510
|
-
* -
|
|
22511
|
-
* -
|
|
22512
|
-
* -
|
|
22513
|
-
* - writeSlidesToYDoc / readSlidesFromYDoc: PptxSlide[] <-> Y.Doc
|
|
22514
|
-
* - observeYDocSlides: register a change listener on the pptx:slides array
|
|
22515
|
-
*
|
|
22516
|
-
* Y.Doc schema (matches pptx-codec.ts in packages/tools):
|
|
22517
|
-
* pptx:slides - Y.Array of slide Y.Maps
|
|
22518
|
-
* Each slide Y.Map has scalar keys + `_`-prefixed JSON blobs + `elements`
|
|
22519
|
-
* Each element Y.Map has scalar keys + `_`-prefixed JSON blobs + `textBody`
|
|
22520
|
-
* textBody is a Y.Text with one delta-op per TextSegment
|
|
22517
|
+
* - DeltaOp / YTextLike: structural Yjs text interfaces (no yjs import)
|
|
22518
|
+
* - encodeTextBody: write TextSegment[] into a live YTextLike
|
|
22519
|
+
* - encodeSegmentsToDelta: pure simulation of the delta Y.Text would produce
|
|
22520
|
+
* - decodeDelta / decodeTextBody: delta -> TextSegment[]
|
|
22521
|
+
* - isYTextLike: runtime guard
|
|
22521
22522
|
*/
|
|
22522
|
-
|
|
22523
|
-
|
|
22524
|
-
|
|
22525
|
-
|
|
22526
|
-
|
|
22527
|
-
const SCALAR_ELEMENT_KEYS = new Set([
|
|
22528
|
-
'id',
|
|
22529
|
-
'type',
|
|
22530
|
-
'x',
|
|
22531
|
-
'y',
|
|
22532
|
-
'width',
|
|
22533
|
-
'height',
|
|
22534
|
-
'rotation',
|
|
22535
|
-
'flipHorizontal',
|
|
22536
|
-
'flipVertical',
|
|
22537
|
-
'hidden',
|
|
22538
|
-
'opacity',
|
|
22539
|
-
'text',
|
|
22540
|
-
'name',
|
|
22541
|
-
'altText',
|
|
22542
|
-
'shapeType',
|
|
22543
|
-
'placeholder',
|
|
22544
|
-
'imagePath',
|
|
22545
|
-
'imageData',
|
|
22546
|
-
'svgContent',
|
|
22547
|
-
'inkSvg',
|
|
22548
|
-
'sourceSlideId',
|
|
22549
|
-
'mediaType',
|
|
22550
|
-
'mediaPath',
|
|
22551
|
-
'linkedTxbxId',
|
|
22552
|
-
'linkedTxbxSeq',
|
|
22553
|
-
'promptText',
|
|
22554
|
-
]);
|
|
22555
|
-
const COMPLEX_ELEMENT_FIELDS = {
|
|
22556
|
-
textStyle: '_ts',
|
|
22557
|
-
shapeStyle: '_ss',
|
|
22558
|
-
shapeAdjustments: '_sa',
|
|
22559
|
-
adjustmentHandles: '_ah',
|
|
22560
|
-
tableData: '_td',
|
|
22561
|
-
chartData: '_cd',
|
|
22562
|
-
smartArtData: '_smad',
|
|
22563
|
-
connectionStart: '_cs',
|
|
22564
|
-
connectionEnd: '_ce',
|
|
22565
|
-
animations: '_an',
|
|
22566
|
-
nativeAnimations: '_na',
|
|
22567
|
-
children: '_ch',
|
|
22568
|
-
paragraphIndents: '_pi',
|
|
22569
|
-
rawXml: '_rx',
|
|
22570
|
-
actionClick: '_ac',
|
|
22571
|
-
actionHover: '_av',
|
|
22572
|
-
locks: '_lk',
|
|
22573
|
-
imageEffects: '_ie',
|
|
22574
|
-
cropShape: '_cr',
|
|
22575
|
-
mediaBookmarks: '_mb',
|
|
22576
|
-
captionTracks: '_ct',
|
|
22577
|
-
};
|
|
22578
|
-
const REV_COMPLEX_ELEMENT = Object.fromEntries(Object.entries(COMPLEX_ELEMENT_FIELDS).map(([k, v]) => [v, k]));
|
|
22579
|
-
const SCALAR_SLIDE_KEYS = new Set([
|
|
22580
|
-
'id',
|
|
22581
|
-
'rId',
|
|
22582
|
-
'sourceSlideId',
|
|
22583
|
-
'layoutPath',
|
|
22584
|
-
'layoutName',
|
|
22585
|
-
'slideNumber',
|
|
22586
|
-
'hidden',
|
|
22587
|
-
'sectionName',
|
|
22588
|
-
'sectionId',
|
|
22589
|
-
'backgroundColor',
|
|
22590
|
-
'backgroundImage',
|
|
22591
|
-
'backgroundGradient',
|
|
22592
|
-
'notes',
|
|
22593
|
-
'backgroundShowAnimation',
|
|
22594
|
-
'showMasterShapes',
|
|
22595
|
-
'isDirty',
|
|
22596
|
-
]);
|
|
22597
|
-
const COMPLEX_SLIDE_FIELDS = {
|
|
22598
|
-
transition: '_tr',
|
|
22599
|
-
animations: '_an',
|
|
22600
|
-
nativeAnimations: '_na',
|
|
22601
|
-
rawTiming: '_rt',
|
|
22602
|
-
notesSegments: '_ns',
|
|
22603
|
-
comments: '_cm',
|
|
22604
|
-
warnings: '_wa',
|
|
22605
|
-
rawXml: '_rx',
|
|
22606
|
-
clrMapOverride: '_cm2',
|
|
22607
|
-
guides: '_gu',
|
|
22608
|
-
customerData: '_cu',
|
|
22609
|
-
activeXControls: '_ax',
|
|
22610
|
-
};
|
|
22611
|
-
const REV_COMPLEX_SLIDE = Object.fromEntries(Object.entries(COMPLEX_SLIDE_FIELDS).map(([k, v]) => [v, k]));
|
|
22612
|
-
// ---------------------------------------------------------------------------
|
|
22613
|
-
// Text-body encode/decode (TextSegment[] <-> YTextLike delta)
|
|
22614
|
-
// ---------------------------------------------------------------------------
|
|
22523
|
+
function isYTextLike(value) {
|
|
22524
|
+
return (typeof value === 'object' &&
|
|
22525
|
+
value !== null &&
|
|
22526
|
+
typeof value.toDelta === 'function');
|
|
22527
|
+
}
|
|
22615
22528
|
function buildSegmentAttrs(seg) {
|
|
22616
22529
|
const a = {};
|
|
22617
22530
|
const style = seg.style;
|
|
@@ -22668,29 +22581,56 @@ function buildSegmentAttrs(seg) {
|
|
|
22668
22581
|
}
|
|
22669
22582
|
return a;
|
|
22670
22583
|
}
|
|
22584
|
+
/** Resolve the literal text a segment contributes to the Y.Text document. */
|
|
22585
|
+
function segmentInsertText(seg) {
|
|
22586
|
+
if (seg.isParagraphBreak === true || seg.isLineBreak === true) {
|
|
22587
|
+
return '\n';
|
|
22588
|
+
}
|
|
22589
|
+
if (typeof seg.text === 'string' && seg.text.length > 0) {
|
|
22590
|
+
return seg.text;
|
|
22591
|
+
}
|
|
22592
|
+
// Empty non-break run: zero-width space holds the attributes.
|
|
22593
|
+
return '';
|
|
22594
|
+
}
|
|
22671
22595
|
function encodeTextBody(segments, ytext) {
|
|
22672
22596
|
let offset = 0;
|
|
22673
22597
|
for (const raw of segments) {
|
|
22674
22598
|
const seg = raw;
|
|
22675
22599
|
const attrs = buildSegmentAttrs(seg);
|
|
22676
|
-
const
|
|
22677
|
-
|
|
22678
|
-
|
|
22679
|
-
|
|
22680
|
-
|
|
22681
|
-
|
|
22682
|
-
|
|
22683
|
-
|
|
22600
|
+
const text = segmentInsertText(seg);
|
|
22601
|
+
// Always pass an explicit attrs object: Yjs makes attribute-less inserts
|
|
22602
|
+
// inherit the preceding character's formatting, which bled styles and
|
|
22603
|
+
// paragraph-break markers into unstyled runs.
|
|
22604
|
+
ytext.insert(offset, text, attrs);
|
|
22605
|
+
offset += text.length;
|
|
22606
|
+
}
|
|
22607
|
+
}
|
|
22608
|
+
/**
|
|
22609
|
+
* Pure simulation of the delta a Y.Text produces after `encodeTextBody`:
|
|
22610
|
+
* adjacent runs with identical attribute maps merge into one op, matching
|
|
22611
|
+
* Yjs run-merging. Used to compare desired segments against a live Y.Text
|
|
22612
|
+
* without instantiating one.
|
|
22613
|
+
*/
|
|
22614
|
+
function encodeSegmentsToDelta(segments) {
|
|
22615
|
+
const ops = [];
|
|
22616
|
+
for (const raw of segments) {
|
|
22617
|
+
const seg = raw;
|
|
22618
|
+
const attrs = buildSegmentAttrs(seg);
|
|
22619
|
+
const attributes = Object.keys(attrs).length > 0 ? attrs : undefined;
|
|
22620
|
+
const text = segmentInsertText(seg);
|
|
22621
|
+
const prev = ops[ops.length - 1];
|
|
22622
|
+
if (prev &&
|
|
22623
|
+
typeof prev.insert === 'string' &&
|
|
22624
|
+
JSON.stringify(prev.attributes ?? null) === JSON.stringify(attributes ?? null)) {
|
|
22625
|
+
prev.insert += text;
|
|
22684
22626
|
}
|
|
22685
22627
|
else {
|
|
22686
|
-
|
|
22687
|
-
ytext.insert(offset, '', hasAttrs ? attrs : undefined);
|
|
22688
|
-
offset += 1;
|
|
22628
|
+
ops.push(attributes ? { insert: text, attributes } : { insert: text });
|
|
22689
22629
|
}
|
|
22690
22630
|
}
|
|
22631
|
+
return ops;
|
|
22691
22632
|
}
|
|
22692
|
-
function
|
|
22693
|
-
const delta = ytext.toDelta();
|
|
22633
|
+
function decodeDelta(delta) {
|
|
22694
22634
|
const segments = [];
|
|
22695
22635
|
for (const op of delta) {
|
|
22696
22636
|
if (typeof op.insert !== 'string' || op.insert === '') {
|
|
@@ -22791,6 +22731,128 @@ function decodeTextBody(ytext) {
|
|
|
22791
22731
|
}
|
|
22792
22732
|
return segments;
|
|
22793
22733
|
}
|
|
22734
|
+
function decodeTextBody(ytext) {
|
|
22735
|
+
return decodeDelta(ytext.toDelta());
|
|
22736
|
+
}
|
|
22737
|
+
|
|
22738
|
+
/**
|
|
22739
|
+
* collaboration-sync.ts: Framework-agnostic CRDT sync utilities for the
|
|
22740
|
+
* pptx-viewer collaboration stack (Yjs backend).
|
|
22741
|
+
*
|
|
22742
|
+
* Exports:
|
|
22743
|
+
* - Structural Yjs interfaces (no hard yjs import - bindings pass live instances)
|
|
22744
|
+
* - YjsFactories: factory interface bindings implement using `new Y.Map()` etc.
|
|
22745
|
+
* - writeElementToYMap / readElementFromYMap: PptxElement <-> YMapLike
|
|
22746
|
+
* - writeSlideToYMap / readSlideFromYMap: PptxSlide <-> YMapLike
|
|
22747
|
+
* - writeSlidesToYDoc / readSlidesFromYDoc: PptxSlide[] <-> Y.Doc
|
|
22748
|
+
* - observeYDocSlides: register a change listener on the pptx:slides array
|
|
22749
|
+
* - re-exports of the text codec (collaboration-text-codec.ts)
|
|
22750
|
+
*
|
|
22751
|
+
* Y.Doc schema:
|
|
22752
|
+
* pptx:slides - Y.Array of slide Y.Maps
|
|
22753
|
+
* Each slide Y.Map has scalar keys + `_`-prefixed JSON blobs + `elements`
|
|
22754
|
+
* Each element Y.Map has scalar keys + `_`-prefixed JSON blobs + `textBody`
|
|
22755
|
+
* textBody is a Y.Text with one delta-op per TextSegment
|
|
22756
|
+
*
|
|
22757
|
+
* NOTE: packages/tools' pptx-codec.ts implements a similar schema but with
|
|
22758
|
+
* different complex-field key prefixes (e.g. `_textStyle` vs `_ts` here); the
|
|
22759
|
+
* two doc layouts are NOT interchangeable on the same Y.Doc.
|
|
22760
|
+
*
|
|
22761
|
+
* Prefer `reconcileSlidesInYDoc` (collaboration-reconcile.ts) over
|
|
22762
|
+
* `writeSlidesToYDoc` for live editing: it updates only what changed instead
|
|
22763
|
+
* of replacing the whole slides array, so concurrent edits merge per
|
|
22764
|
+
* slide/element/field rather than colliding at document granularity.
|
|
22765
|
+
*/
|
|
22766
|
+
// ---------------------------------------------------------------------------
|
|
22767
|
+
// Y.Doc schema constants
|
|
22768
|
+
// ---------------------------------------------------------------------------
|
|
22769
|
+
const YDOC_SLIDES_KEY = 'pptx:slides';
|
|
22770
|
+
const YDOC_META_KEY = 'pptx:meta';
|
|
22771
|
+
const SCALAR_ELEMENT_KEYS = new Set([
|
|
22772
|
+
'id',
|
|
22773
|
+
'type',
|
|
22774
|
+
'x',
|
|
22775
|
+
'y',
|
|
22776
|
+
'width',
|
|
22777
|
+
'height',
|
|
22778
|
+
'rotation',
|
|
22779
|
+
'flipHorizontal',
|
|
22780
|
+
'flipVertical',
|
|
22781
|
+
'hidden',
|
|
22782
|
+
'opacity',
|
|
22783
|
+
'text',
|
|
22784
|
+
'name',
|
|
22785
|
+
'altText',
|
|
22786
|
+
'shapeType',
|
|
22787
|
+
'placeholder',
|
|
22788
|
+
'imagePath',
|
|
22789
|
+
'imageData',
|
|
22790
|
+
'svgContent',
|
|
22791
|
+
'inkSvg',
|
|
22792
|
+
'sourceSlideId',
|
|
22793
|
+
'mediaType',
|
|
22794
|
+
'mediaPath',
|
|
22795
|
+
'linkedTxbxId',
|
|
22796
|
+
'linkedTxbxSeq',
|
|
22797
|
+
'promptText',
|
|
22798
|
+
]);
|
|
22799
|
+
const COMPLEX_ELEMENT_FIELDS = {
|
|
22800
|
+
textStyle: '_ts',
|
|
22801
|
+
shapeStyle: '_ss',
|
|
22802
|
+
shapeAdjustments: '_sa',
|
|
22803
|
+
adjustmentHandles: '_ah',
|
|
22804
|
+
tableData: '_td',
|
|
22805
|
+
chartData: '_cd',
|
|
22806
|
+
smartArtData: '_smad',
|
|
22807
|
+
connectionStart: '_cs',
|
|
22808
|
+
connectionEnd: '_ce',
|
|
22809
|
+
animations: '_an',
|
|
22810
|
+
nativeAnimations: '_na',
|
|
22811
|
+
children: '_ch',
|
|
22812
|
+
paragraphIndents: '_pi',
|
|
22813
|
+
rawXml: '_rx',
|
|
22814
|
+
actionClick: '_ac',
|
|
22815
|
+
actionHover: '_av',
|
|
22816
|
+
locks: '_lk',
|
|
22817
|
+
imageEffects: '_ie',
|
|
22818
|
+
cropShape: '_cr',
|
|
22819
|
+
mediaBookmarks: '_mb',
|
|
22820
|
+
captionTracks: '_ct',
|
|
22821
|
+
};
|
|
22822
|
+
const REV_COMPLEX_ELEMENT = Object.fromEntries(Object.entries(COMPLEX_ELEMENT_FIELDS).map(([k, v]) => [v, k]));
|
|
22823
|
+
const SCALAR_SLIDE_KEYS = new Set([
|
|
22824
|
+
'id',
|
|
22825
|
+
'rId',
|
|
22826
|
+
'sourceSlideId',
|
|
22827
|
+
'layoutPath',
|
|
22828
|
+
'layoutName',
|
|
22829
|
+
'slideNumber',
|
|
22830
|
+
'hidden',
|
|
22831
|
+
'sectionName',
|
|
22832
|
+
'sectionId',
|
|
22833
|
+
'backgroundColor',
|
|
22834
|
+
'backgroundImage',
|
|
22835
|
+
'backgroundGradient',
|
|
22836
|
+
'notes',
|
|
22837
|
+
'backgroundShowAnimation',
|
|
22838
|
+
'showMasterShapes',
|
|
22839
|
+
'isDirty',
|
|
22840
|
+
]);
|
|
22841
|
+
const COMPLEX_SLIDE_FIELDS = {
|
|
22842
|
+
transition: '_tr',
|
|
22843
|
+
animations: '_an',
|
|
22844
|
+
nativeAnimations: '_na',
|
|
22845
|
+
rawTiming: '_rt',
|
|
22846
|
+
notesSegments: '_ns',
|
|
22847
|
+
comments: '_cm',
|
|
22848
|
+
warnings: '_wa',
|
|
22849
|
+
rawXml: '_rx',
|
|
22850
|
+
clrMapOverride: '_cm2',
|
|
22851
|
+
guides: '_gu',
|
|
22852
|
+
customerData: '_cu',
|
|
22853
|
+
activeXControls: '_ax',
|
|
22854
|
+
};
|
|
22855
|
+
const REV_COMPLEX_SLIDE = Object.fromEntries(Object.entries(COMPLEX_SLIDE_FIELDS).map(([k, v]) => [v, k]));
|
|
22794
22856
|
// ---------------------------------------------------------------------------
|
|
22795
22857
|
// Element serialization
|
|
22796
22858
|
// ---------------------------------------------------------------------------
|
|
@@ -22815,11 +22877,6 @@ function writeElementToYMap(element, ymap, factories) {
|
|
|
22815
22877
|
}
|
|
22816
22878
|
}
|
|
22817
22879
|
}
|
|
22818
|
-
function isYTextLike(value) {
|
|
22819
|
-
return (typeof value === 'object' &&
|
|
22820
|
-
value !== null &&
|
|
22821
|
-
typeof value.toDelta === 'function');
|
|
22822
|
-
}
|
|
22823
22880
|
function readElementFromYMap(ymap) {
|
|
22824
22881
|
const element = {};
|
|
22825
22882
|
ymap.forEach((value, key) => {
|
|
@@ -22897,6 +22954,11 @@ function readSlideFromYMap(ymap) {
|
|
|
22897
22954
|
// ---------------------------------------------------------------------------
|
|
22898
22955
|
// Y.Doc-level helpers
|
|
22899
22956
|
// ---------------------------------------------------------------------------
|
|
22957
|
+
/**
|
|
22958
|
+
* Replace the full slides array in the Y.Doc. Coarse: prefer
|
|
22959
|
+
* `reconcileSlidesInYDoc` for live editing; this remains suitable for
|
|
22960
|
+
* one-shot seeding of an empty document.
|
|
22961
|
+
*/
|
|
22900
22962
|
function writeSlidesToYDoc(slides, ydoc, factories, origin) {
|
|
22901
22963
|
ydoc.transact(() => {
|
|
22902
22964
|
const arr = ydoc.getArray(YDOC_SLIDES_KEY);
|
|
@@ -22918,12 +22980,260 @@ function readSlidesFromYDoc(ydoc) {
|
|
|
22918
22980
|
}
|
|
22919
22981
|
return slides;
|
|
22920
22982
|
}
|
|
22983
|
+
/**
|
|
22984
|
+
* Observe (deeply) the pptx:slides array. The handler receives the Yjs
|
|
22985
|
+
* events plus the transaction, so callers can skip their own writes by
|
|
22986
|
+
* checking `transaction.origin` (see LOCAL_SYNC_ORIGIN in
|
|
22987
|
+
* collaboration-reconcile.ts).
|
|
22988
|
+
*/
|
|
22921
22989
|
function observeYDocSlides(ydoc, onChange) {
|
|
22922
22990
|
const arr = ydoc.getArray(YDOC_SLIDES_KEY);
|
|
22923
22991
|
arr.observeDeep(onChange);
|
|
22924
22992
|
return () => arr.unobserveDeep(onChange);
|
|
22925
22993
|
}
|
|
22926
22994
|
|
|
22995
|
+
/**
|
|
22996
|
+
* collaboration-reconcile.ts: Granular Y.Doc reconciliation for collaborative
|
|
22997
|
+
* editing.
|
|
22998
|
+
*
|
|
22999
|
+
* `writeSlidesToYDoc` replaces the entire pptx:slides array on every write,
|
|
23000
|
+
* which makes concurrent edits collide at document granularity (last writer
|
|
23001
|
+
* wins for the whole deck). `reconcileSlidesInYDoc` instead diffs the desired
|
|
23002
|
+
* slide state against the live Y.Doc and only mutates what changed:
|
|
23003
|
+
*
|
|
23004
|
+
* - slides and elements are matched by `id`; unchanged ones keep their Y.Map
|
|
23005
|
+
* instance so concurrent field edits merge via Yjs
|
|
23006
|
+
* - scalar / complex fields are compared and only set when different
|
|
23007
|
+
* - textBody is only replaced when its canonical decoded form differs
|
|
23008
|
+
* - removed items are deleted, new ones inserted at their position; moves
|
|
23009
|
+
* are delete+reinsert (Yjs has no move primitive)
|
|
23010
|
+
*
|
|
23011
|
+
* All mutations run in a single transaction tagged with LOCAL_SYNC_ORIGIN (or
|
|
23012
|
+
* a caller-supplied origin) so observers can ignore their own writes.
|
|
23013
|
+
*/
|
|
23014
|
+
/** Transaction origin used for local reconcile writes. */
|
|
23015
|
+
const LOCAL_SYNC_ORIGIN = 'pptx-viewer:local-sync';
|
|
23016
|
+
function jsonEqual(a, b) {
|
|
23017
|
+
return a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
23018
|
+
}
|
|
23019
|
+
function reconcileScalars(ymap, rec, keys) {
|
|
23020
|
+
for (const key of keys) {
|
|
23021
|
+
const next = rec[key];
|
|
23022
|
+
const current = ymap.get(key);
|
|
23023
|
+
if (next === undefined) {
|
|
23024
|
+
if (current !== undefined) {
|
|
23025
|
+
ymap.delete(key);
|
|
23026
|
+
}
|
|
23027
|
+
}
|
|
23028
|
+
else if (!jsonEqual(current, next)) {
|
|
23029
|
+
ymap.set(key, next);
|
|
23030
|
+
}
|
|
23031
|
+
}
|
|
23032
|
+
}
|
|
23033
|
+
function reconcileComplexFields(ymap, rec, fields) {
|
|
23034
|
+
for (const [original, prefixed] of Object.entries(fields)) {
|
|
23035
|
+
const next = rec[original] === undefined ? undefined : JSON.stringify(rec[original]);
|
|
23036
|
+
const current = ymap.get(prefixed);
|
|
23037
|
+
if (next === undefined) {
|
|
23038
|
+
if (current !== undefined) {
|
|
23039
|
+
ymap.delete(prefixed);
|
|
23040
|
+
}
|
|
23041
|
+
}
|
|
23042
|
+
else if (current !== next) {
|
|
23043
|
+
ymap.set(prefixed, next);
|
|
23044
|
+
}
|
|
23045
|
+
}
|
|
23046
|
+
}
|
|
23047
|
+
function reconcileTextBody(ymap, rec, factories) {
|
|
23048
|
+
const segments = rec.textSegments;
|
|
23049
|
+
const current = ymap.get('textBody');
|
|
23050
|
+
if (!Array.isArray(segments)) {
|
|
23051
|
+
if (current !== undefined) {
|
|
23052
|
+
ymap.delete('textBody');
|
|
23053
|
+
}
|
|
23054
|
+
return;
|
|
23055
|
+
}
|
|
23056
|
+
const desired = decodeDelta(encodeSegmentsToDelta(segments));
|
|
23057
|
+
const existing = isYTextLike(current) ? decodeDelta(current.toDelta()) : undefined;
|
|
23058
|
+
if (existing !== undefined && jsonEqual(existing, desired)) {
|
|
23059
|
+
return;
|
|
23060
|
+
}
|
|
23061
|
+
const ytext = factories.createText();
|
|
23062
|
+
encodeTextBody(segments, ytext);
|
|
23063
|
+
ymap.set('textBody', ytext);
|
|
23064
|
+
}
|
|
23065
|
+
function reconcileElementYMap(ymap, element, factories) {
|
|
23066
|
+
const rec = element;
|
|
23067
|
+
reconcileScalars(ymap, rec, SCALAR_ELEMENT_KEYS);
|
|
23068
|
+
reconcileComplexFields(ymap, rec, COMPLEX_ELEMENT_FIELDS);
|
|
23069
|
+
reconcileTextBody(ymap, rec, factories);
|
|
23070
|
+
}
|
|
23071
|
+
function mapIdAt(arr, index) {
|
|
23072
|
+
const entry = arr.get(index);
|
|
23073
|
+
if (!entry || typeof entry.get !== 'function') {
|
|
23074
|
+
return undefined;
|
|
23075
|
+
}
|
|
23076
|
+
const id = entry.get('id');
|
|
23077
|
+
return typeof id === 'string' ? id : undefined;
|
|
23078
|
+
}
|
|
23079
|
+
/**
|
|
23080
|
+
* Reconcile a Y.Array of Y.Maps against a desired item list, matching by id.
|
|
23081
|
+
* Items without a string id fall back to positional matching.
|
|
23082
|
+
*/
|
|
23083
|
+
function reconcileYArrayById(arr, items, adapter) {
|
|
23084
|
+
const desiredIds = new Set();
|
|
23085
|
+
for (const item of items) {
|
|
23086
|
+
const id = adapter.idOf(item);
|
|
23087
|
+
if (typeof id === 'string') {
|
|
23088
|
+
desiredIds.add(id);
|
|
23089
|
+
}
|
|
23090
|
+
}
|
|
23091
|
+
// Pass 1: delete maps whose id is gone (or duplicated); keep the last dup.
|
|
23092
|
+
const seen = new Set();
|
|
23093
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
23094
|
+
const id = mapIdAt(arr, i);
|
|
23095
|
+
if (id === undefined) {
|
|
23096
|
+
continue;
|
|
23097
|
+
}
|
|
23098
|
+
if (!desiredIds.has(id) || seen.has(id)) {
|
|
23099
|
+
arr.delete(i, 1);
|
|
23100
|
+
}
|
|
23101
|
+
else {
|
|
23102
|
+
seen.add(id);
|
|
23103
|
+
}
|
|
23104
|
+
}
|
|
23105
|
+
// Pass 2: walk desired order; update in place, move, or insert.
|
|
23106
|
+
for (let pos = 0; pos < items.length; pos++) {
|
|
23107
|
+
const item = items[pos];
|
|
23108
|
+
const id = adapter.idOf(item);
|
|
23109
|
+
const idAtPos = pos < arr.length ? mapIdAt(arr, pos) : undefined;
|
|
23110
|
+
if (id === undefined) {
|
|
23111
|
+
// Positional fallback for id-less items.
|
|
23112
|
+
if (pos < arr.length && idAtPos === undefined) {
|
|
23113
|
+
adapter.update(arr.get(pos), item);
|
|
23114
|
+
}
|
|
23115
|
+
else {
|
|
23116
|
+
arr.insert(pos, [adapter.create(item)]);
|
|
23117
|
+
}
|
|
23118
|
+
continue;
|
|
23119
|
+
}
|
|
23120
|
+
if (idAtPos === id) {
|
|
23121
|
+
adapter.update(arr.get(pos), item);
|
|
23122
|
+
continue;
|
|
23123
|
+
}
|
|
23124
|
+
let foundAt = -1;
|
|
23125
|
+
for (let j = pos + 1; j < arr.length; j++) {
|
|
23126
|
+
if (mapIdAt(arr, j) === id) {
|
|
23127
|
+
foundAt = j;
|
|
23128
|
+
break;
|
|
23129
|
+
}
|
|
23130
|
+
}
|
|
23131
|
+
if (foundAt >= 0) {
|
|
23132
|
+
// Move: Yjs cannot re-insert an integrated type, so rebuild at pos.
|
|
23133
|
+
arr.delete(foundAt, 1);
|
|
23134
|
+
}
|
|
23135
|
+
arr.insert(pos, [adapter.create(item)]);
|
|
23136
|
+
}
|
|
23137
|
+
// Pass 3: trim trailing leftovers.
|
|
23138
|
+
if (arr.length > items.length) {
|
|
23139
|
+
arr.delete(items.length, arr.length - items.length);
|
|
23140
|
+
}
|
|
23141
|
+
}
|
|
23142
|
+
const isYArrayLike = (value) => typeof value === 'object' &&
|
|
23143
|
+
value !== null &&
|
|
23144
|
+
typeof value.insert === 'function' &&
|
|
23145
|
+
typeof value.toArray === 'function';
|
|
23146
|
+
function reconcileSlideYMap(ymap, slide, factories) {
|
|
23147
|
+
const rec = slide;
|
|
23148
|
+
reconcileScalars(ymap, rec, SCALAR_SLIDE_KEYS);
|
|
23149
|
+
reconcileComplexFields(ymap, rec, COMPLEX_SLIDE_FIELDS);
|
|
23150
|
+
let elements = ymap.get('elements');
|
|
23151
|
+
if (!isYArrayLike(elements)) {
|
|
23152
|
+
elements = factories.createArray();
|
|
23153
|
+
ymap.set('elements', elements);
|
|
23154
|
+
}
|
|
23155
|
+
reconcileYArrayById(elements, slide.elements, {
|
|
23156
|
+
idOf: (el) => (typeof el.id === 'string' ? el.id : undefined),
|
|
23157
|
+
create: (el) => {
|
|
23158
|
+
const map = factories.createMap();
|
|
23159
|
+
writeElementToYMap(el, map, factories);
|
|
23160
|
+
return map;
|
|
23161
|
+
},
|
|
23162
|
+
update: (map, el) => reconcileElementYMap(map, el, factories),
|
|
23163
|
+
});
|
|
23164
|
+
}
|
|
23165
|
+
/**
|
|
23166
|
+
* Granular local -> Y.Doc sync: mutate only what changed, inside one
|
|
23167
|
+
* transaction tagged with `origin` (default LOCAL_SYNC_ORIGIN) so the
|
|
23168
|
+
* caller's own observer can skip the resulting events.
|
|
23169
|
+
*/
|
|
23170
|
+
function reconcileSlidesInYDoc(slides, ydoc, factories, origin = LOCAL_SYNC_ORIGIN) {
|
|
23171
|
+
ydoc.transact(() => {
|
|
23172
|
+
const arr = ydoc.getArray(YDOC_SLIDES_KEY);
|
|
23173
|
+
reconcileYArrayById(arr, slides, {
|
|
23174
|
+
idOf: (slide) => (typeof slide.id === 'string' ? slide.id : undefined),
|
|
23175
|
+
create: (slide) => {
|
|
23176
|
+
const map = factories.createMap();
|
|
23177
|
+
writeSlideToYMap(slide, map, factories);
|
|
23178
|
+
return map;
|
|
23179
|
+
},
|
|
23180
|
+
update: (map, slide) => reconcileSlideYMap(map, slide, factories),
|
|
23181
|
+
});
|
|
23182
|
+
}, origin);
|
|
23183
|
+
}
|
|
23184
|
+
|
|
23185
|
+
/**
|
|
23186
|
+
* collaboration-sync-gate.ts: first-write gate for collaborative sessions.
|
|
23187
|
+
*
|
|
23188
|
+
* Until a provider confirms its initial document sync, local state must not
|
|
23189
|
+
* be written into the shared doc: a late joiner bootstrapped with a
|
|
23190
|
+
* placeholder deck would otherwise merge that placeholder into the room's
|
|
23191
|
+
* real content. Websocket providers emit a reliable 'synced' event; y-webrtc
|
|
23192
|
+
* only syncs when a peer is present, so a lone fresh-room peer opens the gate
|
|
23193
|
+
* after {@link INITIAL_SYNC_GRACE_MS} instead and seeds the empty doc.
|
|
23194
|
+
*
|
|
23195
|
+
* Usage per binding: create the gate with an `onOpen` callback that performs
|
|
23196
|
+
* the deferred first write, call `arm()` when the provider is created (and on
|
|
23197
|
+
* (re)connect), `open()` from the provider's sync event, gate local writes on
|
|
23198
|
+
* `isOpen()`, and `reset()` on session teardown.
|
|
23199
|
+
*/
|
|
23200
|
+
/**
|
|
23201
|
+
* Create a first-write gate. `onOpen` fires exactly once per session (until
|
|
23202
|
+
* `reset()`), from either the provider sync signal or the grace timer.
|
|
23203
|
+
*/
|
|
23204
|
+
function createSyncGate(onOpen, graceMs = INITIAL_SYNC_GRACE_MS) {
|
|
23205
|
+
let opened = false;
|
|
23206
|
+
let timer = null;
|
|
23207
|
+
const clear = () => {
|
|
23208
|
+
if (timer !== null) {
|
|
23209
|
+
clearTimeout(timer);
|
|
23210
|
+
timer = null;
|
|
23211
|
+
}
|
|
23212
|
+
};
|
|
23213
|
+
const open = () => {
|
|
23214
|
+
clear();
|
|
23215
|
+
if (opened) {
|
|
23216
|
+
return;
|
|
23217
|
+
}
|
|
23218
|
+
opened = true;
|
|
23219
|
+
onOpen();
|
|
23220
|
+
};
|
|
23221
|
+
return {
|
|
23222
|
+
isOpen: () => opened,
|
|
23223
|
+
arm: () => {
|
|
23224
|
+
clear();
|
|
23225
|
+
if (!opened) {
|
|
23226
|
+
timer = setTimeout(open, graceMs);
|
|
23227
|
+
}
|
|
23228
|
+
},
|
|
23229
|
+
open,
|
|
23230
|
+
reset: () => {
|
|
23231
|
+
clear();
|
|
23232
|
+
opened = false;
|
|
23233
|
+
},
|
|
23234
|
+
};
|
|
23235
|
+
}
|
|
23236
|
+
|
|
22927
23237
|
// ---------------------------------------------------------------------------
|
|
22928
23238
|
// Tuning constants (px tolerances for position / size changes)
|
|
22929
23239
|
// ---------------------------------------------------------------------------
|
|
@@ -28777,6 +29087,15 @@ function formatRelativeTime(ts) {
|
|
|
28777
29087
|
*/
|
|
28778
29088
|
/** Default y-websocket server URL used when no default is supplied. */
|
|
28779
29089
|
const DEFAULT_BROADCAST_SERVER_URL = 'ws://localhost:1234';
|
|
29090
|
+
/**
|
|
29091
|
+
* Resolve the transport implied by a server-URL form field: a blank server
|
|
29092
|
+
* URL selects the serverless y-webrtc transport (peers meet via WebRTC
|
|
29093
|
+
* signaling plus same-browser BroadcastChannel); anything else is a
|
|
29094
|
+
* y-websocket server URL.
|
|
29095
|
+
*/
|
|
29096
|
+
function resolveTransportForServerUrl(serverUrl) {
|
|
29097
|
+
return serverUrl.trim().length === 0 ? 'webrtc' : 'websocket';
|
|
29098
|
+
}
|
|
28780
29099
|
/** Generate a fresh, broadcast-scoped room id (`broadcast-<suffix>`). */
|
|
28781
29100
|
function generateBroadcastRoomId() {
|
|
28782
29101
|
const suffix = Math.random().toString(36).slice(2, 10);
|
|
@@ -28792,30 +29111,44 @@ function seedBroadcastFields(defaults) {
|
|
|
28792
29111
|
serverUrl: defaults?.serverUrl ?? DEFAULT_BROADCAST_SERVER_URL,
|
|
28793
29112
|
};
|
|
28794
29113
|
}
|
|
28795
|
-
/**
|
|
29114
|
+
/**
|
|
29115
|
+
* Whether the form can start: the room id is required; the server URL may be
|
|
29116
|
+
* blank, which selects the serverless webrtc transport.
|
|
29117
|
+
*/
|
|
28796
29118
|
function canStartBroadcast(fields) {
|
|
28797
|
-
return fields.roomId.trim().length > 0
|
|
29119
|
+
return fields.roomId.trim().length > 0;
|
|
28798
29120
|
}
|
|
28799
29121
|
/**
|
|
28800
29122
|
* Assemble a {@link BroadcastConfig} from the (trimmed) form fields, or `null`
|
|
28801
|
-
* when incomplete.
|
|
29123
|
+
* when incomplete. A blank server URL yields `transport: 'webrtc'`.
|
|
28802
29124
|
*/
|
|
28803
29125
|
function buildBroadcastConfig(fields) {
|
|
28804
29126
|
if (!canStartBroadcast(fields)) {
|
|
28805
29127
|
return null;
|
|
28806
29128
|
}
|
|
28807
|
-
|
|
29129
|
+
const serverUrl = fields.serverUrl.trim();
|
|
29130
|
+
return {
|
|
29131
|
+
roomId: fields.roomId.trim(),
|
|
29132
|
+
serverUrl,
|
|
29133
|
+
transport: resolveTransportForServerUrl(serverUrl),
|
|
29134
|
+
};
|
|
28808
29135
|
}
|
|
28809
29136
|
/**
|
|
28810
29137
|
* Build the shareable viewer follow-link for a broadcast. Returns just the
|
|
28811
29138
|
* room id when no `origin`/`pathname` are available (non-browser environments).
|
|
29139
|
+
* A blank server URL produces a `transport=webrtc` link instead of a
|
|
29140
|
+
* `server=` parameter.
|
|
28812
29141
|
*/
|
|
28813
29142
|
function buildBroadcastViewerUrl(roomId, serverUrl, location) {
|
|
28814
29143
|
if (!location) {
|
|
28815
29144
|
return roomId;
|
|
28816
29145
|
}
|
|
28817
29146
|
const room = encodeURIComponent(roomId);
|
|
28818
|
-
const
|
|
29147
|
+
const trimmed = serverUrl.trim();
|
|
29148
|
+
if (trimmed.length === 0) {
|
|
29149
|
+
return `${location.origin}${location.pathname}?broadcast=${room}&transport=webrtc`;
|
|
29150
|
+
}
|
|
29151
|
+
const server = encodeURIComponent(trimmed);
|
|
28819
29152
|
return `${location.origin}${location.pathname}?broadcast=${room}&server=${server}`;
|
|
28820
29153
|
}
|
|
28821
29154
|
/** Whether the runtime exposes a usable async clipboard write API. */
|
|
@@ -34830,6 +35163,9 @@ class BroadcastDialogComponent {
|
|
|
34830
35163
|
/** The shareable follow link (shown while `active`). */
|
|
34831
35164
|
viewerUrl = input(/* @ts-ignore */
|
|
34832
35165
|
...(ngDevMode ? [undefined, { debugName: "viewerUrl" }] : /* istanbul ignore next */ []));
|
|
35166
|
+
/** Whether the active broadcast is peer-to-peer (serverless webrtc). */
|
|
35167
|
+
p2p = input(false, /* @ts-ignore */
|
|
35168
|
+
...(ngDevMode ? [{ debugName: "p2p" }] : /* istanbul ignore next */ []));
|
|
34833
35169
|
/** Fired when the presenter starts a broadcast. */
|
|
34834
35170
|
start = output();
|
|
34835
35171
|
/** Fired when the presenter stops the active broadcast. */
|
|
@@ -34844,6 +35180,9 @@ class BroadcastDialogComponent {
|
|
|
34844
35180
|
...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
|
|
34845
35181
|
canStart = computed(() => canStartBroadcast({ roomId: this.roomId(), serverUrl: this.serverUrl() }), /* @ts-ignore */
|
|
34846
35182
|
...(ngDevMode ? [{ debugName: "canStart" }] : /* istanbul ignore next */ []));
|
|
35183
|
+
/** Blank server URL selects the serverless peer-to-peer (webrtc) transport. */
|
|
35184
|
+
isP2p = computed(() => this.serverUrl().trim().length === 0, /* @ts-ignore */
|
|
35185
|
+
...(ngDevMode ? [{ debugName: "isP2p" }] : /* istanbul ignore next */ []));
|
|
34847
35186
|
broadcastingTitle = translate('pptx.broadcast.broadcastingTitle');
|
|
34848
35187
|
startTitle = translate('pptx.broadcast.startTitle');
|
|
34849
35188
|
dialogTitle = computed(() => this.active() ? this.broadcastingTitle() : this.startTitle(), /* @ts-ignore */
|
|
@@ -34893,7 +35232,7 @@ class BroadcastDialogComponent {
|
|
|
34893
35232
|
});
|
|
34894
35233
|
}
|
|
34895
35234
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: BroadcastDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
34896
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: BroadcastDialogComponent, isStandalone: true, selector: "pptx-broadcast-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaults: { classPropertyName: "defaults", publicName: "defaults", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, connected: { classPropertyName: "connected", publicName: "connected", isSignal: true, isRequired: false, transformFunction: null }, viewerCount: { classPropertyName: "viewerCount", publicName: "viewerCount", isSignal: true, isRequired: false, transformFunction: null }, viewerUrl: { classPropertyName: "viewerUrl", publicName: "viewerUrl", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { start: "start", stop: "stop", close: "close" }, ngImport: i0, template: `
|
|
35235
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: BroadcastDialogComponent, isStandalone: true, selector: "pptx-broadcast-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaults: { classPropertyName: "defaults", publicName: "defaults", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, connected: { classPropertyName: "connected", publicName: "connected", isSignal: true, isRequired: false, transformFunction: null }, viewerCount: { classPropertyName: "viewerCount", publicName: "viewerCount", isSignal: true, isRequired: false, transformFunction: null }, viewerUrl: { classPropertyName: "viewerUrl", publicName: "viewerUrl", isSignal: true, isRequired: false, transformFunction: null }, p2p: { classPropertyName: "p2p", publicName: "p2p", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { start: "start", stop: "stop", close: "close" }, ngImport: i0, template: `
|
|
34897
35236
|
<pptx-modal-dialog [open]="open()" [title]="dialogTitle()" (close)="onClose()">
|
|
34898
35237
|
@if (active()) {
|
|
34899
35238
|
<!-- Active: share the follow link + stop control -->
|
|
@@ -34913,6 +35252,10 @@ class BroadcastDialogComponent {
|
|
|
34913
35252
|
</span>
|
|
34914
35253
|
</div>
|
|
34915
35254
|
|
|
35255
|
+
@if (p2p()) {
|
|
35256
|
+
<p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pServerValue' | translate }}</p>
|
|
35257
|
+
}
|
|
35258
|
+
|
|
34916
35259
|
<p class="pptx-ng-broadcast-desc">
|
|
34917
35260
|
{{ 'pptx.broadcast.liveDesc' | translate }}
|
|
34918
35261
|
</p>
|
|
@@ -34979,6 +35322,9 @@ class BroadcastDialogComponent {
|
|
|
34979
35322
|
[value]="serverUrl()"
|
|
34980
35323
|
(input)="serverUrl.set(asValue($event))"
|
|
34981
35324
|
/>
|
|
35325
|
+
@if (isP2p()) {
|
|
35326
|
+
<p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pHint' | translate }}</p>
|
|
35327
|
+
}
|
|
34982
35328
|
</div>
|
|
34983
35329
|
</div>
|
|
34984
35330
|
}
|
|
@@ -35023,6 +35369,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
35023
35369
|
</span>
|
|
35024
35370
|
</div>
|
|
35025
35371
|
|
|
35372
|
+
@if (p2p()) {
|
|
35373
|
+
<p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pServerValue' | translate }}</p>
|
|
35374
|
+
}
|
|
35375
|
+
|
|
35026
35376
|
<p class="pptx-ng-broadcast-desc">
|
|
35027
35377
|
{{ 'pptx.broadcast.liveDesc' | translate }}
|
|
35028
35378
|
</p>
|
|
@@ -35089,6 +35439,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
35089
35439
|
[value]="serverUrl()"
|
|
35090
35440
|
(input)="serverUrl.set(asValue($event))"
|
|
35091
35441
|
/>
|
|
35442
|
+
@if (isP2p()) {
|
|
35443
|
+
<p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pHint' | translate }}</p>
|
|
35444
|
+
}
|
|
35092
35445
|
</div>
|
|
35093
35446
|
</div>
|
|
35094
35447
|
}
|
|
@@ -35110,7 +35463,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
35110
35463
|
</div>
|
|
35111
35464
|
</pptx-modal-dialog>
|
|
35112
35465
|
`, styles: [".pptx-ng-broadcast{display:flex;flex-direction:column;gap:1rem}.pptx-ng-broadcast-desc{margin:0;font-size:.8125rem;line-height:1.5;color:var(--pptx-muted-foreground, #9ca3af)}.pptx-ng-broadcast-field{display:flex;flex-direction:column;gap:.375rem}.pptx-ng-broadcast-label{font-size:.75rem;font-weight:500;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-broadcast-input{width:100%;padding:.375rem .75rem;border:1px solid var(--pptx-border, #374151);border-radius:.375rem;background:var(--pptx-background, #030712);color:var(--pptx-foreground, #f3f4f6);font-size:.8125rem}.pptx-ng-broadcast-input:focus{outline:none;border-color:var(--pptx-primary, #6366f1)}.pptx-ng-broadcast-link-row{display:flex;align-items:center;gap:.5rem}.pptx-ng-broadcast-hint{margin:0;font-size:.6875rem;color:var(--pptx-muted-foreground, #9ca3af)}.pptx-ng-broadcast-btn{padding:.375rem .75rem;border:1px solid var(--pptx-border, #374151);border-radius:.375rem;background:var(--pptx-card, #111827);color:var(--pptx-foreground, #f3f4f6);font-size:.75rem;cursor:pointer;white-space:nowrap;transition:background .15s ease}.pptx-ng-broadcast-btn:hover:not(:disabled){background:var(--pptx-border, #374151)}.pptx-ng-broadcast-btn:disabled{opacity:.4;cursor:not-allowed}.pptx-ng-broadcast-btn-primary{border-color:var(--pptx-primary, #6366f1);background:var(--pptx-primary, #6366f1);color:#fff}.pptx-ng-broadcast-btn-primary:hover:not(:disabled){background:var(--pptx-primary, #6366f1);filter:brightness(1.1)}.pptx-ng-broadcast-stop{width:100%;padding:.5rem .75rem;border:1px solid rgba(239,68,68,.3);border-radius:.375rem;background:#ef44441a;color:#f87171;font-size:.75rem;font-weight:500;cursor:pointer;transition:background .15s ease}.pptx-ng-broadcast-stop:hover{background:#ef444433}.pptx-ng-broadcast-status-row{display:flex;align-items:center;gap:.5rem;font-size:.8125rem}.pptx-ng-broadcast-status-dot{width:.5rem;height:.5rem;border-radius:9999px;background:var(--pptx-muted-foreground, #9ca3af)}.pptx-ng-broadcast-status-dot.is-on{background:#22c55e}.pptx-ng-broadcast-status-text{font-weight:500;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-broadcast-count{margin-left:auto;color:var(--pptx-muted-foreground, #9ca3af)}\n"] }]
|
|
35113
|
-
}], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], defaults: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaults", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], connected: [{ type: i0.Input, args: [{ isSignal: true, alias: "connected", required: false }] }], viewerCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewerCount", required: false }] }], viewerUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewerUrl", required: false }] }], start: [{ type: i0.Output, args: ["start"] }], stop: [{ type: i0.Output, args: ["stop"] }], close: [{ type: i0.Output, args: ["close"] }] } });
|
|
35466
|
+
}], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], defaults: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaults", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], connected: [{ type: i0.Input, args: [{ isSignal: true, alias: "connected", required: false }] }], viewerCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewerCount", required: false }] }], viewerUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewerUrl", required: false }] }], p2p: [{ type: i0.Input, args: [{ isSignal: true, alias: "p2p", required: false }] }], start: [{ type: i0.Output, args: ["start"] }], stop: [{ type: i0.Output, args: ["stop"] }], close: [{ type: i0.Output, args: ["close"] }] } });
|
|
35114
35467
|
|
|
35115
35468
|
/**
|
|
35116
35469
|
* Thin re-export shim → vendored `pptx-viewer-shared`
|
|
@@ -35246,6 +35599,107 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
35246
35599
|
`, styles: [":host{position:absolute;inset:0;pointer-events:none;overflow:visible;z-index:9999}.pptx-ng-collab-cursor{position:absolute;top:0;left:0;pointer-events:none;will-change:transform;transition:transform 90ms linear}.pptx-ng-collab-pointer{display:block;filter:drop-shadow(0 1px 1px rgba(0,0,0,.35))}.pptx-ng-collab-label{position:absolute;top:16px;left:12px;max-width:150px;padding:2px 6px;border-radius:4px;color:#fff;font-family:system-ui,sans-serif;font-size:10px;font-weight:500;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 1px 2px #0000004d}\n"] }]
|
|
35247
35600
|
}], propDecorators: { cursors: [{ type: i0.Input, args: [{ isSignal: true, alias: "cursors", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }] } });
|
|
35248
35601
|
|
|
35602
|
+
/**
|
|
35603
|
+
* collaboration-local-presence.ts: publishes the local user's presence into a
|
|
35604
|
+
* single awareness `presence` field.
|
|
35605
|
+
*
|
|
35606
|
+
* Cursor, selection, and active-slide updates all merge into one record so a
|
|
35607
|
+
* later cursor move never clobbers the selection (or vice versa). The remote
|
|
35608
|
+
* side reads this shape via the shared `derivePresenceList`.
|
|
35609
|
+
*/
|
|
35610
|
+
class LocalPresencePublisher {
|
|
35611
|
+
awareness;
|
|
35612
|
+
identity;
|
|
35613
|
+
activeSlide = 0;
|
|
35614
|
+
cursor = { x: 0, y: 0 };
|
|
35615
|
+
selection;
|
|
35616
|
+
constructor(awareness, identity) {
|
|
35617
|
+
this.awareness = awareness;
|
|
35618
|
+
this.identity = identity;
|
|
35619
|
+
}
|
|
35620
|
+
/** Re-emit the merged presence record (also used as a heartbeat). */
|
|
35621
|
+
publish() {
|
|
35622
|
+
this.awareness.setLocalStateField('presence', {
|
|
35623
|
+
userName: this.identity.userName,
|
|
35624
|
+
userColor: this.identity.userColor,
|
|
35625
|
+
userAvatar: this.identity.userAvatar,
|
|
35626
|
+
role: this.identity.role,
|
|
35627
|
+
activeSlideIndex: this.activeSlide,
|
|
35628
|
+
cursorX: this.cursor.x,
|
|
35629
|
+
cursorY: this.cursor.y,
|
|
35630
|
+
selectedElementId: this.selection,
|
|
35631
|
+
lastUpdated: new Date().toISOString(),
|
|
35632
|
+
});
|
|
35633
|
+
}
|
|
35634
|
+
setCursor(x, y, activeSlideIndex = this.activeSlide) {
|
|
35635
|
+
this.cursor = { x, y };
|
|
35636
|
+
this.activeSlide = activeSlideIndex;
|
|
35637
|
+
// A bare `cursor` field mirrors the flat awareness shape some peers read.
|
|
35638
|
+
this.awareness.setLocalStateField('cursor', { x, y });
|
|
35639
|
+
this.publish();
|
|
35640
|
+
}
|
|
35641
|
+
setSelection(selectedElementId, activeSlideIndex = this.activeSlide) {
|
|
35642
|
+
this.selection = selectedElementId;
|
|
35643
|
+
this.activeSlide = activeSlideIndex;
|
|
35644
|
+
this.publish();
|
|
35645
|
+
}
|
|
35646
|
+
setActiveSlide(index) {
|
|
35647
|
+
this.activeSlide = Math.max(0, Math.floor(index));
|
|
35648
|
+
this.publish();
|
|
35649
|
+
}
|
|
35650
|
+
}
|
|
35651
|
+
|
|
35652
|
+
/**
|
|
35653
|
+
* collaboration-providers.ts: transport factories for the Angular
|
|
35654
|
+
* collaboration service.
|
|
35655
|
+
*
|
|
35656
|
+
* Isolates the dynamic `yjs` / `y-websocket` / `y-webrtc` imports and provider
|
|
35657
|
+
* construction so the service focuses on lifecycle + reactive state. The Yjs
|
|
35658
|
+
* packages are dynamically imported so they are fully tree-shaken when
|
|
35659
|
+
* collaboration is unused; each specifier is read from a variable so bundlers
|
|
35660
|
+
* do not eagerly follow it (mirrors the historical `/* @vite-ignore *\/`
|
|
35661
|
+
* pattern).
|
|
35662
|
+
*/
|
|
35663
|
+
async function createDoc() {
|
|
35664
|
+
const yModule = 'yjs';
|
|
35665
|
+
const Y = (await import(/* @vite-ignore */ yModule));
|
|
35666
|
+
const doc = new Y.Doc();
|
|
35667
|
+
const factories = {
|
|
35668
|
+
createMap: () => new Y.Map(),
|
|
35669
|
+
createArray: () => new Y.Array(),
|
|
35670
|
+
createText: () => new Y.Text(),
|
|
35671
|
+
};
|
|
35672
|
+
return { doc, factories, Y };
|
|
35673
|
+
}
|
|
35674
|
+
/** Create a y-websocket transport bundle for `config`. */
|
|
35675
|
+
async function createWebsocketBundle(config) {
|
|
35676
|
+
const wsSpecifier = 'y-websocket';
|
|
35677
|
+
const [{ doc, factories }, yws] = await Promise.all([
|
|
35678
|
+
createDoc(),
|
|
35679
|
+
import(/* @vite-ignore */ wsSpecifier),
|
|
35680
|
+
]);
|
|
35681
|
+
const provider = new yws.WebsocketProvider(config.serverUrl, config.roomId, doc, config.authToken ? { params: { token: config.authToken } } : undefined);
|
|
35682
|
+
return { doc, provider, awareness: provider.awareness, factories };
|
|
35683
|
+
}
|
|
35684
|
+
/**
|
|
35685
|
+
* Create a y-webrtc (peer-to-peer, serverless) transport bundle for `config`.
|
|
35686
|
+
* Peers rendezvous through the configured `signaling` servers (or y-webrtc's
|
|
35687
|
+
* public default) and same-browser tabs additionally sync via BroadcastChannel.
|
|
35688
|
+
* `authToken` is passed as the room `password`.
|
|
35689
|
+
*/
|
|
35690
|
+
async function createWebrtcBundle(config) {
|
|
35691
|
+
const rtcSpecifier = 'y-webrtc';
|
|
35692
|
+
const [{ doc, factories }, yrtc] = await Promise.all([
|
|
35693
|
+
createDoc(),
|
|
35694
|
+
import(/* @vite-ignore */ rtcSpecifier),
|
|
35695
|
+
]);
|
|
35696
|
+
const provider = new yrtc.WebrtcProvider(config.roomId, doc, {
|
|
35697
|
+
signaling: config.signaling?.length ? config.signaling : undefined,
|
|
35698
|
+
password: config.authToken || undefined,
|
|
35699
|
+
});
|
|
35700
|
+
return { doc, provider, awareness: provider.awareness, factories };
|
|
35701
|
+
}
|
|
35702
|
+
|
|
35249
35703
|
/**
|
|
35250
35704
|
* Split inherited template (master/layout) elements out of each loaded slide.
|
|
35251
35705
|
*
|
|
@@ -35309,31 +35763,110 @@ function showsTemplateAffordance(element, editTemplateMode) {
|
|
|
35309
35763
|
}
|
|
35310
35764
|
|
|
35311
35765
|
/**
|
|
35312
|
-
*
|
|
35766
|
+
* collaboration-writeback.ts: elected-writer serialization helper for the
|
|
35767
|
+
* Angular collaboration service.
|
|
35313
35768
|
*
|
|
35314
|
-
*
|
|
35315
|
-
*
|
|
35316
|
-
*
|
|
35317
|
-
*
|
|
35318
|
-
|
|
35319
|
-
|
|
35769
|
+
* When the local user is the session `owner`, the service debounces Y.Doc
|
|
35770
|
+
* changes and calls this to serialize the live document (templates re-merged)
|
|
35771
|
+
* into `.pptx` bytes for `config.onWriteBack`. Kept out of the service so the
|
|
35772
|
+
* service stays within the repo's per-file size budget.
|
|
35773
|
+
*/
|
|
35774
|
+
const WRITE_BACK_DEBOUNCE_MS = 5_000;
|
|
35775
|
+
/**
|
|
35776
|
+
* Serialize the current Y.Doc slide state (with the separated master/layout
|
|
35777
|
+
* template elements merged back) into `.pptx` bytes. `sourceBytes` seeds the
|
|
35778
|
+
* handler so package parts the CRDT does not carry (media, fonts, rels) survive.
|
|
35779
|
+
*/
|
|
35780
|
+
async function serializeWriteBack(ydoc, sourceBytes, templateElements) {
|
|
35781
|
+
const { PptxHandler } = await import('pptx-viewer-core');
|
|
35782
|
+
const handler = new PptxHandler();
|
|
35783
|
+
await handler.load(sourceBytes.buffer);
|
|
35784
|
+
const slides = buildSaveSlides(readSlidesFromYDoc(ydoc), templateElements);
|
|
35785
|
+
return handler.save(slides);
|
|
35786
|
+
}
|
|
35787
|
+
/**
|
|
35788
|
+
* Debounced elected-writer write-back. Only the session `owner` with an
|
|
35789
|
+
* `onWriteBack` callback schedules anything; each new change resets the timer,
|
|
35790
|
+
* and on fire it serializes the live Y.Doc and hands the bytes to the host.
|
|
35791
|
+
*/
|
|
35792
|
+
class WriteBackScheduler {
|
|
35793
|
+
timer = null;
|
|
35794
|
+
schedule(config, ydoc, getSourceBytes, getTemplateElements) {
|
|
35795
|
+
if (!config?.onWriteBack || config.role !== 'owner' || !ydoc) {
|
|
35796
|
+
return;
|
|
35797
|
+
}
|
|
35798
|
+
this.cancel();
|
|
35799
|
+
const ms = config.writeBackDebounceMs ?? WRITE_BACK_DEBOUNCE_MS;
|
|
35800
|
+
this.timer = setTimeout(() => {
|
|
35801
|
+
this.timer = null;
|
|
35802
|
+
const bytes = getSourceBytes?.();
|
|
35803
|
+
if (!bytes || !config.onWriteBack) {
|
|
35804
|
+
return;
|
|
35805
|
+
}
|
|
35806
|
+
void serializeWriteBack(ydoc, bytes, getTemplateElements?.() ?? {})
|
|
35807
|
+
.then((out) => config.onWriteBack?.(out))
|
|
35808
|
+
.catch(() => {
|
|
35809
|
+
/* non-fatal */
|
|
35810
|
+
});
|
|
35811
|
+
}, ms);
|
|
35812
|
+
}
|
|
35813
|
+
cancel() {
|
|
35814
|
+
if (this.timer !== null) {
|
|
35815
|
+
clearTimeout(this.timer);
|
|
35816
|
+
this.timer = null;
|
|
35817
|
+
}
|
|
35818
|
+
}
|
|
35819
|
+
}
|
|
35820
|
+
|
|
35821
|
+
/**
|
|
35822
|
+
* CollaborationService: Angular real-time collaboration (Yjs) service.
|
|
35823
|
+
*
|
|
35824
|
+
* Owns the provider lifecycle and the reactive collaboration state consumed by
|
|
35825
|
+
* the viewer component:
|
|
35826
|
+
* - Transport is `y-websocket` (default) or serverless `y-webrtc`
|
|
35827
|
+
* (`config.transport === 'webrtc'`), created via `./collaboration-providers`.
|
|
35828
|
+
* - Local edits sync via the granular `reconcileSlidesInYDoc` (only changed
|
|
35829
|
+
* slides/elements/fields mutate) in a transaction tagged `LOCAL_SYNC_ORIGIN`;
|
|
35830
|
+
* the remote observer skips its own local-origin writes.
|
|
35831
|
+
* - Websocket connections fail fast on mixed content and time out to `'error'`
|
|
35832
|
+
* after `CONNECTION_TIMEOUT_MS`; `retry()` reconnects with the last config.
|
|
35833
|
+
* - Elected-writer write-back (role === 'owner') via `WriteBackScheduler`.
|
|
35320
35834
|
*
|
|
35321
35835
|
* Provide at the component level: `@Component({ providers: [CollaborationService] })`.
|
|
35322
35836
|
*/
|
|
35323
35837
|
const DEFAULT_CANVAS_BOUND = 100_000;
|
|
35324
|
-
const WRITE_BACK_DEBOUNCE_MS = 5_000;
|
|
35325
35838
|
class CollaborationService {
|
|
35326
35839
|
// Reactive state
|
|
35327
|
-
|
|
35840
|
+
status = signal('disconnected', /* @ts-ignore */
|
|
35841
|
+
...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
35842
|
+
connected = computed(() => this.status() === 'connected', /* @ts-ignore */
|
|
35328
35843
|
...(ngDevMode ? [{ debugName: "connected" }] : /* istanbul ignore next */ []));
|
|
35329
35844
|
active = signal(false, /* @ts-ignore */
|
|
35330
35845
|
...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
35846
|
+
/** Role of the local user in the active session, or undefined when idle. */
|
|
35847
|
+
activeRole = signal(undefined, /* @ts-ignore */
|
|
35848
|
+
...(ngDevMode ? [{ debugName: "activeRole" }] : /* istanbul ignore next */ []));
|
|
35331
35849
|
presence = signal([], /* @ts-ignore */
|
|
35332
35850
|
...(ngDevMode ? [{ debugName: "presence" }] : /* istanbul ignore next */ []));
|
|
35333
35851
|
cursors = computed(() => presenceToCursors(this.presence()), /* @ts-ignore */
|
|
35334
35852
|
...(ngDevMode ? [{ debugName: "cursors" }] : /* istanbul ignore next */ []));
|
|
35335
35853
|
connectedCount = computed(() => this.presence().length + (this.active() ? 1 : 0), /* @ts-ignore */
|
|
35336
35854
|
...(ngDevMode ? [{ debugName: "connectedCount" }] : /* istanbul ignore next */ []));
|
|
35855
|
+
/** The client id the local user is currently following (null when free). */
|
|
35856
|
+
followedClientId = signal(null, /* @ts-ignore */
|
|
35857
|
+
...(ngDevMode ? [{ debugName: "followedClientId" }] : /* istanbul ignore next */ []));
|
|
35858
|
+
/** Active-slide index of the followed peer, or null when not following. */
|
|
35859
|
+
followedSlideIndex = computed(() => {
|
|
35860
|
+
const id = this.followedClientId();
|
|
35861
|
+
if (id === null) {
|
|
35862
|
+
return null;
|
|
35863
|
+
}
|
|
35864
|
+
return this.presence().find((p) => p.clientId === id)?.activeSlideIndex ?? null;
|
|
35865
|
+
}, /* @ts-ignore */
|
|
35866
|
+
...(ngDevMode ? [{ debugName: "followedSlideIndex" }] : /* istanbul ignore next */ []));
|
|
35867
|
+
/** Active-slide index of the first `owner` peer (the broadcaster), or null. */
|
|
35868
|
+
broadcasterSlideIndex = computed(() => this.presence().find((p) => p.role === 'owner')?.activeSlideIndex ?? null, /* @ts-ignore */
|
|
35869
|
+
...(ngDevMode ? [{ debugName: "broadcasterSlideIndex" }] : /* istanbul ignore next */ []));
|
|
35337
35870
|
// Internal handles
|
|
35338
35871
|
ydoc = null;
|
|
35339
35872
|
provider = null;
|
|
@@ -35342,18 +35875,26 @@ class CollaborationService {
|
|
|
35342
35875
|
applyingRemote = false;
|
|
35343
35876
|
yFactories = null;
|
|
35344
35877
|
lastSynced = '';
|
|
35345
|
-
|
|
35878
|
+
connectTimer = null;
|
|
35346
35879
|
unobserveSlides = null;
|
|
35880
|
+
writeBack = new WriteBackScheduler();
|
|
35881
|
+
/**
|
|
35882
|
+
* First-write gate: local broadcasts are suppressed (captured as pending)
|
|
35883
|
+
* until the provider confirms its initial sync or the grace period lifts
|
|
35884
|
+
* the gate, so a late joiner never seeds its placeholder deck into a room
|
|
35885
|
+
* whose real content has not arrived yet.
|
|
35886
|
+
*/
|
|
35887
|
+
syncGate = createSyncGate(() => this.flushPendingBroadcast());
|
|
35888
|
+
pendingBroadcast = null;
|
|
35347
35889
|
onRemoteSlides = null;
|
|
35348
35890
|
canvasWidth = DEFAULT_CANVAS_BOUND;
|
|
35349
35891
|
canvasHeight = DEFAULT_CANVAS_BOUND;
|
|
35350
35892
|
getSourceBytes = null;
|
|
35351
35893
|
getTemplateElements = null;
|
|
35352
|
-
userName = 'Anonymous';
|
|
35353
|
-
userColor = DEFAULT_CURSOR_COLOR;
|
|
35354
|
-
userAvatar;
|
|
35355
|
-
role;
|
|
35356
35894
|
currentConfig = null;
|
|
35895
|
+
lastConfig = null;
|
|
35896
|
+
lastOptions = {};
|
|
35897
|
+
localPresence = null;
|
|
35357
35898
|
refreshPresence = () => {
|
|
35358
35899
|
if (!this.awareness) {
|
|
35359
35900
|
this.presence.set([]);
|
|
@@ -35366,10 +35907,19 @@ class CollaborationService {
|
|
|
35366
35907
|
}
|
|
35367
35908
|
async connect(config, options = {}) {
|
|
35368
35909
|
this.disconnect();
|
|
35910
|
+
this.lastConfig = config;
|
|
35911
|
+
this.lastOptions = options;
|
|
35369
35912
|
try {
|
|
35370
35913
|
validateRoomId(config.roomId);
|
|
35371
35914
|
}
|
|
35372
35915
|
catch {
|
|
35916
|
+
this.status.set('error');
|
|
35917
|
+
return;
|
|
35918
|
+
}
|
|
35919
|
+
// Fail fast on mixed content (websocket only): an https page cannot open a
|
|
35920
|
+
// ws:// socket, so surface the error rather than hanging until the timeout.
|
|
35921
|
+
if (config.transport !== 'webrtc' && isMixedContentBlocked(config.serverUrl)) {
|
|
35922
|
+
this.status.set('error');
|
|
35373
35923
|
return;
|
|
35374
35924
|
}
|
|
35375
35925
|
this.onRemoteSlides = options.onRemoteSlides ?? null;
|
|
@@ -35377,60 +35927,152 @@ class CollaborationService {
|
|
|
35377
35927
|
this.canvasHeight = options.canvasHeight ?? DEFAULT_CANVAS_BOUND;
|
|
35378
35928
|
this.getSourceBytes = options.getSourceBytes ?? null;
|
|
35379
35929
|
this.getTemplateElements = options.getTemplateElements ?? null;
|
|
35380
|
-
this.userName = config.userName;
|
|
35381
|
-
this.userColor = config.userColor ?? DEFAULT_CURSOR_COLOR;
|
|
35382
|
-
this.userAvatar = config.userAvatar;
|
|
35383
|
-
this.role = config.role;
|
|
35384
35930
|
this.currentConfig = config;
|
|
35931
|
+
this.activeRole.set(config.role);
|
|
35932
|
+
this.status.set('connecting');
|
|
35385
35933
|
try {
|
|
35386
|
-
const
|
|
35387
|
-
|
|
35388
|
-
|
|
35389
|
-
|
|
35390
|
-
|
|
35391
|
-
|
|
35392
|
-
this.
|
|
35393
|
-
this.yFactories = {
|
|
35394
|
-
createMap: () => new Y.Map(),
|
|
35395
|
-
createArray: () => new Y.Array(),
|
|
35396
|
-
createText: () => new Y.Text(),
|
|
35397
|
-
};
|
|
35398
|
-
const wsProvider = new yws.WebsocketProvider(config.serverUrl, config.roomId, doc, config.authToken ? { params: { token: config.authToken } } : undefined);
|
|
35399
|
-
this.provider = wsProvider;
|
|
35400
|
-
this.awareness = wsProvider.awareness;
|
|
35934
|
+
const bundle = config.transport === 'webrtc'
|
|
35935
|
+
? await createWebrtcBundle(config)
|
|
35936
|
+
: await createWebsocketBundle(config);
|
|
35937
|
+
this.ydoc = bundle.doc;
|
|
35938
|
+
this.yFactories = bundle.factories;
|
|
35939
|
+
this.provider = bundle.provider;
|
|
35940
|
+
this.awareness = bundle.awareness;
|
|
35401
35941
|
this.selfId = this.awareness.clientID ?? -1;
|
|
35402
|
-
this.
|
|
35942
|
+
this.localPresence = new LocalPresencePublisher(this.awareness, {
|
|
35943
|
+
userName: config.userName,
|
|
35944
|
+
userColor: config.userColor ?? DEFAULT_CURSOR_COLOR,
|
|
35945
|
+
userAvatar: config.userAvatar,
|
|
35946
|
+
role: config.role,
|
|
35947
|
+
});
|
|
35948
|
+
this.localPresence.publish();
|
|
35403
35949
|
this.awareness.on('change', this.refreshPresence);
|
|
35404
35950
|
this.awareness.on('update', this.refreshPresence);
|
|
35405
|
-
|
|
35406
|
-
|
|
35407
|
-
|
|
35408
|
-
|
|
35409
|
-
this.unobserveSlides = observeYDocSlides(doc, () => {
|
|
35410
|
-
if (this.applyingRemote || !this.ydoc) {
|
|
35411
|
-
return;
|
|
35412
|
-
}
|
|
35413
|
-
const remote = readSlidesFromYDoc(this.ydoc);
|
|
35414
|
-
if (remote.length === 0) {
|
|
35415
|
-
return;
|
|
35416
|
-
}
|
|
35417
|
-
this.applyingRemote = true;
|
|
35418
|
-
this.onRemoteSlides?.(remote);
|
|
35419
|
-
this.applyingRemote = false;
|
|
35420
|
-
this._scheduleWriteBack();
|
|
35421
|
-
});
|
|
35951
|
+
this.wireStatus(config);
|
|
35952
|
+
this.syncGate.reset();
|
|
35953
|
+
this.wireSynced();
|
|
35954
|
+
this.unobserveSlides = observeYDocSlides(this.ydoc, (_events, transaction) => this.onRemoteChange(transaction));
|
|
35422
35955
|
this.active.set(true);
|
|
35423
35956
|
this.refreshPresence();
|
|
35424
35957
|
}
|
|
35425
35958
|
catch {
|
|
35426
35959
|
this.disconnect();
|
|
35960
|
+
this.status.set('error');
|
|
35427
35961
|
}
|
|
35428
35962
|
}
|
|
35429
|
-
|
|
35430
|
-
|
|
35431
|
-
|
|
35432
|
-
this.
|
|
35963
|
+
/** Reconnect using the configuration from the most recent {@link connect}. */
|
|
35964
|
+
async retry() {
|
|
35965
|
+
if (this.lastConfig) {
|
|
35966
|
+
await this.connect(this.lastConfig, this.lastOptions);
|
|
35967
|
+
}
|
|
35968
|
+
}
|
|
35969
|
+
/** Wire the provider status events + (websocket-only) connection timeout. */
|
|
35970
|
+
wireStatus(config) {
|
|
35971
|
+
const provider = this.provider;
|
|
35972
|
+
if (!provider) {
|
|
35973
|
+
return;
|
|
35974
|
+
}
|
|
35975
|
+
if (config.transport === 'webrtc') {
|
|
35976
|
+
// P2P: no server round-trip to wait on. Treat "created" as connected,
|
|
35977
|
+
// and reflect explicit disconnect events.
|
|
35978
|
+
this.status.set('connected');
|
|
35979
|
+
provider.on('status', (payload) => {
|
|
35980
|
+
if (payload.connected === false && this.active()) {
|
|
35981
|
+
this.status.set('disconnected');
|
|
35982
|
+
}
|
|
35983
|
+
else if (payload.connected === true) {
|
|
35984
|
+
this.status.set('connected');
|
|
35985
|
+
}
|
|
35986
|
+
});
|
|
35987
|
+
return;
|
|
35988
|
+
}
|
|
35989
|
+
provider.on('status', (payload) => {
|
|
35990
|
+
if (payload.status === 'connected') {
|
|
35991
|
+
this.clearConnectTimer();
|
|
35992
|
+
this.status.set('connected');
|
|
35993
|
+
}
|
|
35994
|
+
else if (payload.status === 'disconnected' && this.active()) {
|
|
35995
|
+
this.status.set('disconnected');
|
|
35996
|
+
}
|
|
35997
|
+
});
|
|
35998
|
+
if (provider.wsconnected) {
|
|
35999
|
+
this.status.set('connected');
|
|
36000
|
+
return;
|
|
36001
|
+
}
|
|
36002
|
+
this.connectTimer = setTimeout(() => {
|
|
36003
|
+
this.connectTimer = null;
|
|
36004
|
+
if (this.status() !== 'connected') {
|
|
36005
|
+
this.disconnect();
|
|
36006
|
+
this.status.set('error');
|
|
36007
|
+
}
|
|
36008
|
+
}, CONNECTION_TIMEOUT_MS);
|
|
36009
|
+
}
|
|
36010
|
+
/**
|
|
36011
|
+
* Open the first-write gate on the provider's initial-sync confirmation.
|
|
36012
|
+
* y-websocket emits 'sync' with a boolean; y-webrtc emits 'synced' with an
|
|
36013
|
+
* object carrying a `synced` flag (and only once a peer syncs, hence the
|
|
36014
|
+
* grace timer). Listen to both; opening is idempotent.
|
|
36015
|
+
*/
|
|
36016
|
+
wireSynced() {
|
|
36017
|
+
const provider = this.provider;
|
|
36018
|
+
if (!provider) {
|
|
36019
|
+
return;
|
|
36020
|
+
}
|
|
36021
|
+
const handle = (payload) => {
|
|
36022
|
+
const flag = payload;
|
|
36023
|
+
const isSynced = typeof flag === 'boolean' ? flag : flag?.synced !== false;
|
|
36024
|
+
if (isSynced) {
|
|
36025
|
+
this.syncGate.open();
|
|
36026
|
+
}
|
|
36027
|
+
};
|
|
36028
|
+
provider.on('sync', handle);
|
|
36029
|
+
provider.on('synced', handle);
|
|
36030
|
+
if (provider.synced === true) {
|
|
36031
|
+
this.syncGate.open();
|
|
36032
|
+
}
|
|
36033
|
+
else {
|
|
36034
|
+
this.syncGate.arm();
|
|
36035
|
+
}
|
|
36036
|
+
}
|
|
36037
|
+
/**
|
|
36038
|
+
* Perform the deferred first broadcast once the gate opens. When the doc is
|
|
36039
|
+
* still empty (fresh room, or nobody else present), clear the baseline so
|
|
36040
|
+
* the pending deck actually seeds it; when remote content already arrived,
|
|
36041
|
+
* the pending deck matches the applied baseline and the write is a no-op.
|
|
36042
|
+
*/
|
|
36043
|
+
flushPendingBroadcast() {
|
|
36044
|
+
const pending = this.pendingBroadcast;
|
|
36045
|
+
this.pendingBroadcast = null;
|
|
36046
|
+
if (!pending || !this.ydoc || !this.yFactories) {
|
|
36047
|
+
return;
|
|
36048
|
+
}
|
|
36049
|
+
if (this.ydoc.getArray(YDOC_SLIDES_KEY).length === 0) {
|
|
36050
|
+
this.lastSynced = '';
|
|
35433
36051
|
}
|
|
36052
|
+
this.broadcastSlides(pending);
|
|
36053
|
+
}
|
|
36054
|
+
/** Handle a remote Y.Doc change, skipping our own local-origin transactions. */
|
|
36055
|
+
onRemoteChange(transaction) {
|
|
36056
|
+
if (transaction?.origin === LOCAL_SYNC_ORIGIN || this.applyingRemote || !this.ydoc) {
|
|
36057
|
+
return;
|
|
36058
|
+
}
|
|
36059
|
+
const remote = readSlidesFromYDoc(this.ydoc);
|
|
36060
|
+
if (remote.length === 0) {
|
|
36061
|
+
return;
|
|
36062
|
+
}
|
|
36063
|
+
// Suppress the echo: record what we just applied so the subsequent local
|
|
36064
|
+
// broadcast (driven by the editor signal) is a no-op.
|
|
36065
|
+
this.lastSynced = JSON.stringify(remote);
|
|
36066
|
+
this.applyingRemote = true;
|
|
36067
|
+
this.onRemoteSlides?.(remote);
|
|
36068
|
+
this.applyingRemote = false;
|
|
36069
|
+
this.scheduleWriteBack();
|
|
36070
|
+
}
|
|
36071
|
+
disconnect() {
|
|
36072
|
+
this.clearConnectTimer();
|
|
36073
|
+
this.syncGate.reset();
|
|
36074
|
+
this.pendingBroadcast = null;
|
|
36075
|
+
this.writeBack.cancel();
|
|
35434
36076
|
this.unobserveSlides?.();
|
|
35435
36077
|
this.unobserveSlides = null;
|
|
35436
36078
|
this.awareness?.off?.('change', this.refreshPresence);
|
|
@@ -35441,19 +36083,42 @@ class CollaborationService {
|
|
|
35441
36083
|
this.provider = null;
|
|
35442
36084
|
this.ydoc = null;
|
|
35443
36085
|
this.awareness = null;
|
|
36086
|
+
this.localPresence = null;
|
|
35444
36087
|
this.selfId = -1;
|
|
35445
36088
|
this.applyingRemote = false;
|
|
35446
36089
|
this.yFactories = null;
|
|
35447
36090
|
this.lastSynced = '';
|
|
35448
36091
|
this.onRemoteSlides = null;
|
|
35449
36092
|
this.currentConfig = null;
|
|
35450
|
-
this.
|
|
36093
|
+
this.status.set('disconnected');
|
|
35451
36094
|
this.active.set(false);
|
|
36095
|
+
this.activeRole.set(undefined);
|
|
35452
36096
|
this.presence.set([]);
|
|
36097
|
+
this.followedClientId.set(null);
|
|
36098
|
+
}
|
|
36099
|
+
/**
|
|
36100
|
+
* Broadcast the local slide set to peers, reconciling only what changed into
|
|
36101
|
+
* the pptx:slides Y.Array. An empty deck is never written (so a late-joiner
|
|
36102
|
+
* that has not yet received the doc cannot clobber it), and an unchanged deck
|
|
36103
|
+
* is skipped.
|
|
36104
|
+
*/
|
|
36105
|
+
/**
|
|
36106
|
+
* Record the current local deck as the sync baseline so the first (unchanged)
|
|
36107
|
+
* broadcast after connecting is suppressed. Call right after {@link connect}
|
|
36108
|
+
* for a joiner whose local deck is a placeholder awaiting remote sync, so it
|
|
36109
|
+
* never overwrites the shared document before receiving it.
|
|
36110
|
+
*/
|
|
36111
|
+
seedBaseline(slides) {
|
|
36112
|
+
this.lastSynced = JSON.stringify(slides);
|
|
35453
36113
|
}
|
|
35454
|
-
/** Broadcast the local slide set to peers via the pptx:slides Y.Array. */
|
|
35455
36114
|
broadcastSlides(slides) {
|
|
35456
|
-
if (!this.ydoc || !this.yFactories || this.applyingRemote) {
|
|
36115
|
+
if (!this.ydoc || !this.yFactories || this.applyingRemote || slides.length === 0) {
|
|
36116
|
+
return;
|
|
36117
|
+
}
|
|
36118
|
+
if (!this.syncGate.isOpen()) {
|
|
36119
|
+
// Defer until the initial sync confirms; the gate flushes the latest
|
|
36120
|
+
// pending deck when it opens.
|
|
36121
|
+
this.pendingBroadcast = slides;
|
|
35457
36122
|
return;
|
|
35458
36123
|
}
|
|
35459
36124
|
const s = JSON.stringify(slides);
|
|
@@ -35461,68 +36126,31 @@ class CollaborationService {
|
|
|
35461
36126
|
return;
|
|
35462
36127
|
}
|
|
35463
36128
|
this.lastSynced = s;
|
|
35464
|
-
|
|
35465
|
-
this.
|
|
36129
|
+
reconcileSlidesInYDoc([...slides], this.ydoc, this.yFactories, LOCAL_SYNC_ORIGIN);
|
|
36130
|
+
this.scheduleWriteBack();
|
|
35466
36131
|
}
|
|
35467
|
-
setCursor(x, y, activeSlideIndex
|
|
35468
|
-
|
|
35469
|
-
return;
|
|
35470
|
-
}
|
|
35471
|
-
this.awareness.setLocalStateField('cursor', { x, y });
|
|
35472
|
-
this._publishAwareness(activeSlideIndex, x, y);
|
|
36132
|
+
setCursor(x, y, activeSlideIndex) {
|
|
36133
|
+
this.localPresence?.setCursor(x, y, activeSlideIndex);
|
|
35473
36134
|
}
|
|
35474
|
-
setSelection(selectedElementId, activeSlideIndex
|
|
35475
|
-
|
|
35476
|
-
return;
|
|
35477
|
-
}
|
|
35478
|
-
this._publishAwareness(activeSlideIndex, undefined, undefined, selectedElementId);
|
|
35479
|
-
}
|
|
35480
|
-
_publishAwareness(activeSlideIndex = 0, cursorX = 0, cursorY = 0, selectedElementId) {
|
|
35481
|
-
this.awareness?.setLocalStateField('presence', {
|
|
35482
|
-
userName: this.userName,
|
|
35483
|
-
userColor: this.userColor,
|
|
35484
|
-
userAvatar: this.userAvatar,
|
|
35485
|
-
role: this.role,
|
|
35486
|
-
activeSlideIndex,
|
|
35487
|
-
cursorX,
|
|
35488
|
-
cursorY,
|
|
35489
|
-
selectedElementId,
|
|
35490
|
-
lastUpdated: new Date().toISOString(),
|
|
35491
|
-
});
|
|
35492
|
-
this.awareness?.setLocalStateField('user', { name: this.userName, color: this.userColor });
|
|
36135
|
+
setSelection(selectedElementId, activeSlideIndex) {
|
|
36136
|
+
this.localPresence?.setSelection(selectedElementId, activeSlideIndex);
|
|
35493
36137
|
}
|
|
35494
|
-
|
|
35495
|
-
|
|
35496
|
-
|
|
35497
|
-
|
|
35498
|
-
|
|
35499
|
-
|
|
35500
|
-
|
|
36138
|
+
/** Publish the local active-slide index (drives follow-along). */
|
|
36139
|
+
setActiveSlide(index) {
|
|
36140
|
+
this.localPresence?.setActiveSlide(index);
|
|
36141
|
+
}
|
|
36142
|
+
/** Follow the given peer's active slide, or `null` to stop following. */
|
|
36143
|
+
followUser(clientId) {
|
|
36144
|
+
this.followedClientId.set(clientId);
|
|
36145
|
+
}
|
|
36146
|
+
clearConnectTimer() {
|
|
36147
|
+
if (this.connectTimer !== null) {
|
|
36148
|
+
clearTimeout(this.connectTimer);
|
|
36149
|
+
this.connectTimer = null;
|
|
35501
36150
|
}
|
|
35502
|
-
|
|
35503
|
-
|
|
35504
|
-
|
|
35505
|
-
if (!this.ydoc || !config.onWriteBack) {
|
|
35506
|
-
return;
|
|
35507
|
-
}
|
|
35508
|
-
const sourceBytes = this.getSourceBytes?.();
|
|
35509
|
-
if (!sourceBytes) {
|
|
35510
|
-
return;
|
|
35511
|
-
}
|
|
35512
|
-
try {
|
|
35513
|
-
const { PptxHandler } = await import('pptx-viewer-core');
|
|
35514
|
-
const handler = new PptxHandler();
|
|
35515
|
-
await handler.load(sourceBytes.buffer);
|
|
35516
|
-
// Merge the editor's separated template (master/layout) elements back
|
|
35517
|
-
// into the broadcast (template-free) slides so template edits persist.
|
|
35518
|
-
const slides = buildSaveSlides(readSlidesFromYDoc(this.ydoc), this.getTemplateElements?.() ?? {});
|
|
35519
|
-
const bytes = await handler.save(slides);
|
|
35520
|
-
config.onWriteBack(bytes);
|
|
35521
|
-
}
|
|
35522
|
-
catch {
|
|
35523
|
-
/* non-fatal */
|
|
35524
|
-
}
|
|
35525
|
-
}, ms);
|
|
36151
|
+
}
|
|
36152
|
+
scheduleWriteBack() {
|
|
36153
|
+
this.writeBack.schedule(this.currentConfig, this.ydoc, this.getSourceBytes, this.getTemplateElements);
|
|
35526
36154
|
}
|
|
35527
36155
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CollaborationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
35528
36156
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CollaborationService });
|
|
@@ -36388,6 +37016,16 @@ class EditorStateService {
|
|
|
36388
37016
|
this.dirty.set(true);
|
|
36389
37017
|
this.syncHistory();
|
|
36390
37018
|
}
|
|
37019
|
+
/**
|
|
37020
|
+
* Apply a remote collaborator's slide set (already template-free, as broadcast
|
|
37021
|
+
* over the CRDT) to the editable deck. Selection, history, and this peer's own
|
|
37022
|
+
* separated template store are left untouched; remote edits are not local undo
|
|
37023
|
+
* steps.
|
|
37024
|
+
*/
|
|
37025
|
+
applyRemoteSlides(slides) {
|
|
37026
|
+
this.slides.set(this.clone(slides));
|
|
37027
|
+
this.dirty.set(true);
|
|
37028
|
+
}
|
|
36391
37029
|
// ── Snapshot (deck + template store) ─────────────────────────────────────
|
|
36392
37030
|
/** Capture the current deck + template store as one undo/redo snapshot. */
|
|
36393
37031
|
captureSnapshot() {
|
|
@@ -39728,6 +40366,145 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
39728
40366
|
args: ['document:keydown', ['$event']]
|
|
39729
40367
|
}] } });
|
|
39730
40368
|
|
|
40369
|
+
/**
|
|
40370
|
+
* follow-mode-bar.component.ts: Angular port of the Vue `FollowModeBar.vue`.
|
|
40371
|
+
*
|
|
40372
|
+
* Selector: `pptx-follow-mode-bar`
|
|
40373
|
+
*
|
|
40374
|
+
* Lists the active remote peers and lets the local user follow one of them
|
|
40375
|
+
* (mirroring that peer's active slide) or stop following. Purely presentational:
|
|
40376
|
+
* the host supplies the reactive presence list and the currently-followed
|
|
40377
|
+
* clientId (from `CollaborationService`) and reacts to the `follow` event to
|
|
40378
|
+
* drive `followUser(clientId | null)`. Each peer chip shows an initials avatar
|
|
40379
|
+
* in the peer's colour; the followed peer is highlighted with a "Stop" affordance.
|
|
40380
|
+
*/
|
|
40381
|
+
class FollowModeBarComponent {
|
|
40382
|
+
/** Active remote collaborators (excludes self). */
|
|
40383
|
+
presences = input([], /* @ts-ignore */
|
|
40384
|
+
...(ngDevMode ? [{ debugName: "presences" }] : /* istanbul ignore next */ []));
|
|
40385
|
+
/** The clientId currently being followed, or null. */
|
|
40386
|
+
followedClientId = input(null, /* @ts-ignore */
|
|
40387
|
+
...(ngDevMode ? [{ debugName: "followedClientId" }] : /* istanbul ignore next */ []));
|
|
40388
|
+
/** Follow the given peer, or `null` to stop following. */
|
|
40389
|
+
follow = output();
|
|
40390
|
+
chips = computed(() => {
|
|
40391
|
+
const followed = this.followedClientId();
|
|
40392
|
+
return this.presences().map((peer) => ({
|
|
40393
|
+
clientId: peer.clientId,
|
|
40394
|
+
userName: peer.userName,
|
|
40395
|
+
color: peer.userColor,
|
|
40396
|
+
initials: initialsOf(peer.userName),
|
|
40397
|
+
following: peer.clientId === followed,
|
|
40398
|
+
}));
|
|
40399
|
+
}, /* @ts-ignore */
|
|
40400
|
+
...(ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []));
|
|
40401
|
+
followedName = computed(() => {
|
|
40402
|
+
const id = this.followedClientId();
|
|
40403
|
+
if (id === null) {
|
|
40404
|
+
return null;
|
|
40405
|
+
}
|
|
40406
|
+
return this.presences().find((p) => p.clientId === id)?.userName ?? null;
|
|
40407
|
+
}, /* @ts-ignore */
|
|
40408
|
+
...(ngDevMode ? [{ debugName: "followedName" }] : /* istanbul ignore next */ []));
|
|
40409
|
+
toggle(clientId) {
|
|
40410
|
+
this.follow.emit(this.followedClientId() === clientId ? null : clientId);
|
|
40411
|
+
}
|
|
40412
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: FollowModeBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
40413
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: FollowModeBarComponent, isStandalone: true, selector: "pptx-follow-mode-bar", inputs: { presences: { classPropertyName: "presences", publicName: "presences", isSignal: true, isRequired: false, transformFunction: null }, followedClientId: { classPropertyName: "followedClientId", publicName: "followedClientId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { follow: "follow" }, ngImport: i0, template: `
|
|
40414
|
+
@if (chips().length > 0) {
|
|
40415
|
+
<div class="pptx-ng-follow-bar" data-export-ignore="true">
|
|
40416
|
+
<span class="pptx-ng-follow-status">
|
|
40417
|
+
@if (followedName(); as name) {
|
|
40418
|
+
{{ 'pptx.followMode.following' | translate }}
|
|
40419
|
+
<strong>{{ name }}</strong>
|
|
40420
|
+
<button
|
|
40421
|
+
type="button"
|
|
40422
|
+
class="pptx-ng-follow-stop"
|
|
40423
|
+
[title]="'pptx.followMode.stopFollowing' | translate"
|
|
40424
|
+
(click)="follow.emit(null)"
|
|
40425
|
+
>
|
|
40426
|
+
{{ 'pptx.followMode.stop' | translate }}
|
|
40427
|
+
</button>
|
|
40428
|
+
} @else {
|
|
40429
|
+
{{ 'pptx.followMode.followCollaborator' | translate }}
|
|
40430
|
+
}
|
|
40431
|
+
</span>
|
|
40432
|
+
<ul class="pptx-ng-follow-list">
|
|
40433
|
+
@for (peer of chips(); track peer.clientId) {
|
|
40434
|
+
<li>
|
|
40435
|
+
<button
|
|
40436
|
+
type="button"
|
|
40437
|
+
class="pptx-ng-follow-peer"
|
|
40438
|
+
[class.is-following]="peer.following"
|
|
40439
|
+
[attr.data-client-id]="peer.clientId"
|
|
40440
|
+
[attr.aria-pressed]="peer.following"
|
|
40441
|
+
(click)="toggle(peer.clientId)"
|
|
40442
|
+
>
|
|
40443
|
+
<span class="pptx-ng-follow-avatar" [style.background-color]="peer.color">
|
|
40444
|
+
{{ peer.initials }}
|
|
40445
|
+
</span>
|
|
40446
|
+
<span class="pptx-ng-follow-name">{{ peer.userName }}</span>
|
|
40447
|
+
</button>
|
|
40448
|
+
</li>
|
|
40449
|
+
}
|
|
40450
|
+
</ul>
|
|
40451
|
+
</div>
|
|
40452
|
+
}
|
|
40453
|
+
`, isInline: true, styles: [":host{display:block}.pptx-ng-follow-bar{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;border-radius:.5rem;background:var(--pptx-card, rgba(17, 24, 39, .95));padding:.375rem .625rem;font-size:.75rem;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-status{display:inline-flex;align-items:center;gap:.375rem;white-space:nowrap;color:var(--pptx-muted-foreground, #9ca3af)}.pptx-ng-follow-stop{cursor:pointer;border-radius:.375rem;border:1px solid var(--pptx-border, #374151);background:transparent;padding:.125rem .5rem;font-size:11px;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-list{display:flex;align-items:center;gap:.375rem;margin:0;padding:0;list-style:none}.pptx-ng-follow-peer{display:inline-flex;cursor:pointer;align-items:center;gap:.375rem;border-radius:9999px;border:1px solid transparent;background:var(--pptx-muted, rgba(55, 65, 81, .6));padding:.125rem .5rem .125rem .125rem;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-peer.is-following{border-color:var(--pptx-primary, #6366f1);background:color-mix(in srgb,var(--pptx-primary, #6366f1) 30%,transparent)}.pptx-ng-follow-avatar{display:inline-flex;height:22px;width:22px;align-items:center;justify-content:center;border-radius:9999px;font-size:10px;font-weight:600;line-height:1;color:#fff}.pptx-ng-follow-name{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
40454
|
+
}
|
|
40455
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: FollowModeBarComponent, decorators: [{
|
|
40456
|
+
type: Component,
|
|
40457
|
+
args: [{ selector: 'pptx-follow-mode-bar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [TranslatePipe], template: `
|
|
40458
|
+
@if (chips().length > 0) {
|
|
40459
|
+
<div class="pptx-ng-follow-bar" data-export-ignore="true">
|
|
40460
|
+
<span class="pptx-ng-follow-status">
|
|
40461
|
+
@if (followedName(); as name) {
|
|
40462
|
+
{{ 'pptx.followMode.following' | translate }}
|
|
40463
|
+
<strong>{{ name }}</strong>
|
|
40464
|
+
<button
|
|
40465
|
+
type="button"
|
|
40466
|
+
class="pptx-ng-follow-stop"
|
|
40467
|
+
[title]="'pptx.followMode.stopFollowing' | translate"
|
|
40468
|
+
(click)="follow.emit(null)"
|
|
40469
|
+
>
|
|
40470
|
+
{{ 'pptx.followMode.stop' | translate }}
|
|
40471
|
+
</button>
|
|
40472
|
+
} @else {
|
|
40473
|
+
{{ 'pptx.followMode.followCollaborator' | translate }}
|
|
40474
|
+
}
|
|
40475
|
+
</span>
|
|
40476
|
+
<ul class="pptx-ng-follow-list">
|
|
40477
|
+
@for (peer of chips(); track peer.clientId) {
|
|
40478
|
+
<li>
|
|
40479
|
+
<button
|
|
40480
|
+
type="button"
|
|
40481
|
+
class="pptx-ng-follow-peer"
|
|
40482
|
+
[class.is-following]="peer.following"
|
|
40483
|
+
[attr.data-client-id]="peer.clientId"
|
|
40484
|
+
[attr.aria-pressed]="peer.following"
|
|
40485
|
+
(click)="toggle(peer.clientId)"
|
|
40486
|
+
>
|
|
40487
|
+
<span class="pptx-ng-follow-avatar" [style.background-color]="peer.color">
|
|
40488
|
+
{{ peer.initials }}
|
|
40489
|
+
</span>
|
|
40490
|
+
<span class="pptx-ng-follow-name">{{ peer.userName }}</span>
|
|
40491
|
+
</button>
|
|
40492
|
+
</li>
|
|
40493
|
+
}
|
|
40494
|
+
</ul>
|
|
40495
|
+
</div>
|
|
40496
|
+
}
|
|
40497
|
+
`, styles: [":host{display:block}.pptx-ng-follow-bar{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;border-radius:.5rem;background:var(--pptx-card, rgba(17, 24, 39, .95));padding:.375rem .625rem;font-size:.75rem;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-status{display:inline-flex;align-items:center;gap:.375rem;white-space:nowrap;color:var(--pptx-muted-foreground, #9ca3af)}.pptx-ng-follow-stop{cursor:pointer;border-radius:.375rem;border:1px solid var(--pptx-border, #374151);background:transparent;padding:.125rem .5rem;font-size:11px;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-list{display:flex;align-items:center;gap:.375rem;margin:0;padding:0;list-style:none}.pptx-ng-follow-peer{display:inline-flex;cursor:pointer;align-items:center;gap:.375rem;border-radius:9999px;border:1px solid transparent;background:var(--pptx-muted, rgba(55, 65, 81, .6));padding:.125rem .5rem .125rem .125rem;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-peer.is-following{border-color:var(--pptx-primary, #6366f1);background:color-mix(in srgb,var(--pptx-primary, #6366f1) 30%,transparent)}.pptx-ng-follow-avatar{display:inline-flex;height:22px;width:22px;align-items:center;justify-content:center;border-radius:9999px;font-size:10px;font-weight:600;line-height:1;color:#fff}.pptx-ng-follow-name{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
|
|
40498
|
+
}], propDecorators: { presences: [{ type: i0.Input, args: [{ isSignal: true, alias: "presences", required: false }] }], followedClientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "followedClientId", required: false }] }], follow: [{ type: i0.Output, args: ["follow"] }] } });
|
|
40499
|
+
/** First-letter / two-char initials for the avatar chip. */
|
|
40500
|
+
function initialsOf(name) {
|
|
40501
|
+
const parts = name.trim().split(/\s+/u);
|
|
40502
|
+
if (parts.length >= 2 && parts[0] && parts[parts.length - 1]) {
|
|
40503
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
40504
|
+
}
|
|
40505
|
+
return name.slice(0, 2).toUpperCase() || '?';
|
|
40506
|
+
}
|
|
40507
|
+
|
|
39731
40508
|
/**
|
|
39732
40509
|
* hyperlink-dialog.component.ts: Set or clear an element's click hyperlink.
|
|
39733
40510
|
*
|
|
@@ -56588,7 +57365,7 @@ class ElementRendererComponent {
|
|
|
56588
57365
|
}, /* @ts-ignore */
|
|
56589
57366
|
...(ngDevMode ? [{ debugName: "placeholderLabel" }] : /* istanbul ignore next */ []));
|
|
56590
57367
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ElementRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
56591
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ElementRendererComponent, isStandalone: true, selector: "pptx-element-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, obstacles: { classPropertyName: "obstacles", publicName: "obstacles", isSignal: true, isRequired: false, transformFunction: null }, canvasWidth: { classPropertyName: "canvasWidth", publicName: "canvasWidth", isSignal: true, isRequired: false, transformFunction: null }, canvasHeight: { classPropertyName: "canvasHeight", publicName: "canvasHeight", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, fieldContext: { classPropertyName: "fieldContext", publicName: "fieldContext", isSignal: true, isRequired: false, transformFunction: null }, editTemplateMode: { classPropertyName: "editTemplateMode", publicName: "editTemplateMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cellCommit: "cellCommit", tableChange: "tableChange" }, ngImport: i0, template: `
|
|
57368
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ElementRendererComponent, isStandalone: true, selector: "pptx-element-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, obstacles: { classPropertyName: "obstacles", publicName: "obstacles", isSignal: true, isRequired: false, transformFunction: null }, canvasWidth: { classPropertyName: "canvasWidth", publicName: "canvasWidth", isSignal: true, isRequired: false, transformFunction: null }, canvasHeight: { classPropertyName: "canvasHeight", publicName: "canvasHeight", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, fieldContext: { classPropertyName: "fieldContext", publicName: "fieldContext", isSignal: true, isRequired: false, transformFunction: null }, editTemplateMode: { classPropertyName: "editTemplateMode", publicName: "editTemplateMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cellCommit: "cellCommit", tableChange: "tableChange" }, host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
56592
57369
|
@switch (true) {
|
|
56593
57370
|
@case (element().type === 'connector') {
|
|
56594
57371
|
<pptx-connector-renderer
|
|
@@ -56862,6 +57639,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
56862
57639
|
selector: 'pptx-element-renderer',
|
|
56863
57640
|
standalone: true,
|
|
56864
57641
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
57642
|
+
host: { class: 'contents' },
|
|
56865
57643
|
imports: [
|
|
56866
57644
|
NgStyle,
|
|
56867
57645
|
ConnectorRendererComponent,
|
|
@@ -64622,6 +65400,112 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
64622
65400
|
`, styles: [".pptx-ng-props-form{display:flex;flex-direction:column;gap:.75rem}.pptx-ng-props-field{display:flex;flex-direction:column;gap:.375rem}.pptx-ng-props-label{font-size:.75rem;font-weight:500;color:var(--pptx-foreground, #e5e5e5)}.pptx-ng-props-input{width:100%;padding:.375rem .75rem;border-radius:.375rem;border:1px solid var(--pptx-border, #2a2a2a);background:var(--pptx-background, #111);color:var(--pptx-foreground, #e5e5e5);font-size:.8125rem}.pptx-ng-props-input:focus{outline:none;border-color:var(--pptx-primary, #6366f1);box-shadow:0 0 0 1px var(--pptx-primary, #6366f1)}.pptx-ng-props-meta{display:flex;flex-direction:column;gap:.375rem;padding-top:.5rem;border-top:1px solid var(--pptx-border, #2a2a2a)}.pptx-ng-props-meta-row{display:flex;justify-content:space-between;font-size:.75rem}.pptx-ng-props-meta-label{color:var(--pptx-muted-foreground, #9a9a9a)}.pptx-ng-props-meta-value{color:var(--pptx-foreground, #e5e5e5)}.pptx-ng-props-btn{padding:.375rem .75rem;border:none;border-radius:.375rem;background:var(--pptx-muted, #2a2a2a);color:var(--pptx-foreground, #e5e5e5);font-size:.75rem;cursor:pointer}.pptx-ng-props-btn-primary{background:var(--pptx-primary, #6366f1);color:var(--pptx-primary-foreground, #fff)}\n"] }]
|
|
64623
65401
|
}], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: true }] }], save: [{ type: i0.Output, args: ["save"] }], close: [{ type: i0.Output, args: ["close"] }] } });
|
|
64624
65402
|
|
|
65403
|
+
/**
|
|
65404
|
+
* remote-selection-overlay.component.ts: Angular port of the Vue
|
|
65405
|
+
* `RemoteSelectionOverlay.vue` / React `RemoteSelectionOverlay.tsx`.
|
|
65406
|
+
*
|
|
65407
|
+
* Selector: `pptx-remote-selection-overlay`
|
|
65408
|
+
*
|
|
65409
|
+
* Draws a coloured rectangle around each element a remote collaborator has
|
|
65410
|
+
* selected, labelled with that peer's name in their colour (Google-Slides-style
|
|
65411
|
+
* presence). Purely presentational: the host supplies the reactive presence
|
|
65412
|
+
* list (from `CollaborationService.presence`), the elements on the active slide,
|
|
65413
|
+
* the active slide index, and the current `zoom`. Only peers whose
|
|
65414
|
+
* `activeSlideIndex` matches are drawn, and only for a selected id that resolves
|
|
65415
|
+
* to an element on the slide.
|
|
65416
|
+
*
|
|
65417
|
+
* Element geometry is in unscaled slide coordinates (px); this component
|
|
65418
|
+
* multiplies by `zoom` so it can mount inside the scaled slide stage. It sets
|
|
65419
|
+
* `pointer-events: none` so it never intercepts canvas input.
|
|
65420
|
+
*/
|
|
65421
|
+
class RemoteSelectionOverlayComponent {
|
|
65422
|
+
/** Remote collaborators' presence (cursor + selection + active slide). */
|
|
65423
|
+
presences = input([], /* @ts-ignore */
|
|
65424
|
+
...(ngDevMode ? [{ debugName: "presences" }] : /* istanbul ignore next */ []));
|
|
65425
|
+
/** Elements on the active slide (used to resolve selected ids -> geometry). */
|
|
65426
|
+
elements = input([], /* @ts-ignore */
|
|
65427
|
+
...(ngDevMode ? [{ debugName: "elements" }] : /* istanbul ignore next */ []));
|
|
65428
|
+
/** The current slide index: only peers on this slide are drawn. */
|
|
65429
|
+
activeSlideIndex = input(0, /* @ts-ignore */
|
|
65430
|
+
...(ngDevMode ? [{ debugName: "activeSlideIndex" }] : /* istanbul ignore next */ []));
|
|
65431
|
+
/** Current canvas zoom factor; geometry scales by this. */
|
|
65432
|
+
zoom = input(1, /* @ts-ignore */
|
|
65433
|
+
...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
65434
|
+
elementMap = computed(() => {
|
|
65435
|
+
const map = new Map();
|
|
65436
|
+
for (const el of this.elements()) {
|
|
65437
|
+
map.set(el.id, el);
|
|
65438
|
+
}
|
|
65439
|
+
return map;
|
|
65440
|
+
}, /* @ts-ignore */
|
|
65441
|
+
...(ngDevMode ? [{ debugName: "elementMap" }] : /* istanbul ignore next */ []));
|
|
65442
|
+
boxes = computed(() => {
|
|
65443
|
+
const slide = this.activeSlideIndex();
|
|
65444
|
+
const z = this.zoom();
|
|
65445
|
+
const lookup = this.elementMap();
|
|
65446
|
+
const result = [];
|
|
65447
|
+
for (const peer of this.presences()) {
|
|
65448
|
+
if (peer.activeSlideIndex !== slide || !peer.selectedElementId) {
|
|
65449
|
+
continue;
|
|
65450
|
+
}
|
|
65451
|
+
const el = lookup.get(peer.selectedElementId);
|
|
65452
|
+
if (!el) {
|
|
65453
|
+
continue;
|
|
65454
|
+
}
|
|
65455
|
+
result.push({
|
|
65456
|
+
key: `${peer.clientId}-${peer.selectedElementId}`,
|
|
65457
|
+
label: formatCursorLabel(peer.userName, MAX_LABEL_CHARS),
|
|
65458
|
+
color: peer.userColor,
|
|
65459
|
+
transform: `translate(${el.x * z}px, ${el.y * z}px)`,
|
|
65460
|
+
width: el.width * z,
|
|
65461
|
+
height: el.height * z,
|
|
65462
|
+
});
|
|
65463
|
+
}
|
|
65464
|
+
return result;
|
|
65465
|
+
}, /* @ts-ignore */
|
|
65466
|
+
...(ngDevMode ? [{ debugName: "boxes" }] : /* istanbul ignore next */ []));
|
|
65467
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RemoteSelectionOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
65468
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: RemoteSelectionOverlayComponent, isStandalone: true, selector: "pptx-remote-selection-overlay", inputs: { presences: { classPropertyName: "presences", publicName: "presences", isSignal: true, isRequired: false, transformFunction: null }, elements: { classPropertyName: "elements", publicName: "elements", isSignal: true, isRequired: false, transformFunction: null }, activeSlideIndex: { classPropertyName: "activeSlideIndex", publicName: "activeSlideIndex", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
65469
|
+
<div aria-hidden="true" data-export-ignore="true">
|
|
65470
|
+
@for (box of boxes(); track box.key) {
|
|
65471
|
+
<div
|
|
65472
|
+
class="pptx-ng-remote-selection"
|
|
65473
|
+
[attr.data-element-id]="box.key"
|
|
65474
|
+
[style.transform]="box.transform"
|
|
65475
|
+
[style.width.px]="box.width"
|
|
65476
|
+
[style.height.px]="box.height"
|
|
65477
|
+
[style.color]="box.color"
|
|
65478
|
+
>
|
|
65479
|
+
<span class="pptx-ng-remote-selection-label" [style.background-color]="box.color">
|
|
65480
|
+
{{ box.label }}
|
|
65481
|
+
</span>
|
|
65482
|
+
</div>
|
|
65483
|
+
}
|
|
65484
|
+
</div>
|
|
65485
|
+
`, isInline: true, styles: [":host{position:absolute;inset:0;pointer-events:none;overflow:visible;z-index:9997}.pptx-ng-remote-selection{position:absolute;top:0;left:0;box-sizing:border-box;border:2px solid currentcolor;border-radius:2px;pointer-events:none;will-change:transform;transition:transform 90ms linear}.pptx-ng-remote-selection-label{position:absolute;top:-18px;left:-2px;max-width:150px;padding:1px 5px;border-radius:3px;color:#fff;font-family:system-ui,sans-serif;font-size:9px;font-weight:500;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
65486
|
+
}
|
|
65487
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RemoteSelectionOverlayComponent, decorators: [{
|
|
65488
|
+
type: Component,
|
|
65489
|
+
args: [{ selector: 'pptx-remote-selection-overlay', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
65490
|
+
<div aria-hidden="true" data-export-ignore="true">
|
|
65491
|
+
@for (box of boxes(); track box.key) {
|
|
65492
|
+
<div
|
|
65493
|
+
class="pptx-ng-remote-selection"
|
|
65494
|
+
[attr.data-element-id]="box.key"
|
|
65495
|
+
[style.transform]="box.transform"
|
|
65496
|
+
[style.width.px]="box.width"
|
|
65497
|
+
[style.height.px]="box.height"
|
|
65498
|
+
[style.color]="box.color"
|
|
65499
|
+
>
|
|
65500
|
+
<span class="pptx-ng-remote-selection-label" [style.background-color]="box.color">
|
|
65501
|
+
{{ box.label }}
|
|
65502
|
+
</span>
|
|
65503
|
+
</div>
|
|
65504
|
+
}
|
|
65505
|
+
</div>
|
|
65506
|
+
`, styles: [":host{position:absolute;inset:0;pointer-events:none;overflow:visible;z-index:9997}.pptx-ng-remote-selection{position:absolute;top:0;left:0;box-sizing:border-box;border:2px solid currentcolor;border-radius:2px;pointer-events:none;will-change:transform;transition:transform 90ms linear}.pptx-ng-remote-selection-label{position:absolute;top:-18px;left:-2px;max-width:150px;padding:1px 5px;border-radius:3px;color:#fff;font-family:system-ui,sans-serif;font-size:9px;font-weight:500;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
|
|
65507
|
+
}], propDecorators: { presences: [{ type: i0.Input, args: [{ isSignal: true, alias: "presences", required: false }] }], elements: [{ type: i0.Input, args: [{ isSignal: true, alias: "elements", required: false }] }], activeSlideIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeSlideIndex", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }] } });
|
|
65508
|
+
|
|
64625
65509
|
/**
|
|
64626
65510
|
* ribbon-animations-section.component.ts: the Animations ribbon tab (preview, the
|
|
64627
65511
|
* Add Animation entrance/emphasis/exit gallery, Remove Animation, Animation
|
|
@@ -69658,24 +70542,29 @@ function seedShareFields(defaults) {
|
|
|
69658
70542
|
serverUrl: defaults?.serverUrl ?? '',
|
|
69659
70543
|
};
|
|
69660
70544
|
}
|
|
69661
|
-
/**
|
|
70545
|
+
/**
|
|
70546
|
+
* Whether the required fields are non-blank (after trimming). The server URL is
|
|
70547
|
+
* optional: a blank server selects the serverless (webrtc) peer-to-peer
|
|
70548
|
+
* transport, so only the room id and display name are required.
|
|
70549
|
+
*/
|
|
69662
70550
|
function canStartShare(fields) {
|
|
69663
|
-
return
|
|
69664
|
-
fields.userName.trim().length > 0 &&
|
|
69665
|
-
fields.serverUrl.trim().length > 0);
|
|
70551
|
+
return fields.roomId.trim().length > 0 && fields.userName.trim().length > 0;
|
|
69666
70552
|
}
|
|
69667
70553
|
/**
|
|
69668
70554
|
* Assemble a {@link CollaborationConfig} from the (trimmed) form fields, or
|
|
69669
|
-
* `null` when the form is incomplete.
|
|
70555
|
+
* `null` when the form is incomplete. A blank server URL yields
|
|
70556
|
+
* `transport: 'webrtc'`; otherwise the default websocket transport is used.
|
|
69670
70557
|
*/
|
|
69671
70558
|
function buildCollaborationConfig(fields) {
|
|
69672
70559
|
if (!canStartShare(fields)) {
|
|
69673
70560
|
return null;
|
|
69674
70561
|
}
|
|
70562
|
+
const serverUrl = fields.serverUrl.trim();
|
|
69675
70563
|
return {
|
|
69676
70564
|
roomId: fields.roomId.trim(),
|
|
69677
70565
|
userName: fields.userName.trim(),
|
|
69678
|
-
serverUrl
|
|
70566
|
+
serverUrl,
|
|
70567
|
+
transport: resolveTransportForServerUrl(serverUrl),
|
|
69679
70568
|
};
|
|
69680
70569
|
}
|
|
69681
70570
|
/**
|
|
@@ -69687,7 +70576,11 @@ function buildShareUrl(roomId, serverUrl, location) {
|
|
|
69687
70576
|
return roomId;
|
|
69688
70577
|
}
|
|
69689
70578
|
const room = encodeURIComponent(roomId);
|
|
69690
|
-
const
|
|
70579
|
+
const trimmed = serverUrl.trim();
|
|
70580
|
+
if (trimmed.length === 0) {
|
|
70581
|
+
return `${location.origin}${location.pathname}?room=${room}&transport=webrtc`;
|
|
70582
|
+
}
|
|
70583
|
+
const server = encodeURIComponent(trimmed);
|
|
69691
70584
|
return `${location.origin}${location.pathname}?room=${room}&server=${server}`;
|
|
69692
70585
|
}
|
|
69693
70586
|
|
|
@@ -69723,6 +70616,9 @@ class ShareDialogComponent {
|
|
|
69723
70616
|
/** Shareable join link surfaced while the session is active. */
|
|
69724
70617
|
shareUrl = input('', /* @ts-ignore */
|
|
69725
70618
|
...(ngDevMode ? [{ debugName: "shareUrl" }] : /* istanbul ignore next */ []));
|
|
70619
|
+
/** Whether the active session is peer-to-peer (serverless webrtc). */
|
|
70620
|
+
p2p = input(false, /* @ts-ignore */
|
|
70621
|
+
...(ngDevMode ? [{ debugName: "p2p" }] : /* istanbul ignore next */ []));
|
|
69726
70622
|
/** Fired with the assembled config when the user starts sharing. */
|
|
69727
70623
|
start = output();
|
|
69728
70624
|
/** Fired when the user stops an active session. */
|
|
@@ -69743,6 +70639,9 @@ class ShareDialogComponent {
|
|
|
69743
70639
|
serverUrl: this.serverUrl(),
|
|
69744
70640
|
}), /* @ts-ignore */
|
|
69745
70641
|
...(ngDevMode ? [{ debugName: "canStart" }] : /* istanbul ignore next */ []));
|
|
70642
|
+
/** Blank server URL selects the serverless peer-to-peer (webrtc) transport. */
|
|
70643
|
+
isP2p = computed(() => this.serverUrl().trim().length === 0, /* @ts-ignore */
|
|
70644
|
+
...(ngDevMode ? [{ debugName: "isP2p" }] : /* istanbul ignore next */ []));
|
|
69746
70645
|
canCopy = computed(() => canUseClipboard(typeof navigator !== 'undefined' ? navigator : undefined), /* @ts-ignore */
|
|
69747
70646
|
...(ngDevMode ? [{ debugName: "canCopy" }] : /* istanbul ignore next */ []));
|
|
69748
70647
|
constructor() {
|
|
@@ -69789,7 +70688,7 @@ class ShareDialogComponent {
|
|
|
69789
70688
|
this.stop.emit();
|
|
69790
70689
|
}
|
|
69791
70690
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ShareDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
69792
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ShareDialogComponent, isStandalone: true, selector: "pptx-share-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaults: { classPropertyName: "defaults", publicName: "defaults", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, connected: { classPropertyName: "connected", publicName: "connected", isSignal: true, isRequired: false, transformFunction: null }, userCount: { classPropertyName: "userCount", publicName: "userCount", isSignal: true, isRequired: false, transformFunction: null }, shareUrl: { classPropertyName: "shareUrl", publicName: "shareUrl", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { start: "start", stop: "stop", close: "close" }, ngImport: i0, template: `
|
|
70691
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ShareDialogComponent, isStandalone: true, selector: "pptx-share-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaults: { classPropertyName: "defaults", publicName: "defaults", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, connected: { classPropertyName: "connected", publicName: "connected", isSignal: true, isRequired: false, transformFunction: null }, userCount: { classPropertyName: "userCount", publicName: "userCount", isSignal: true, isRequired: false, transformFunction: null }, shareUrl: { classPropertyName: "shareUrl", publicName: "shareUrl", isSignal: true, isRequired: false, transformFunction: null }, p2p: { classPropertyName: "p2p", publicName: "p2p", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { start: "start", stop: "stop", close: "close" }, ngImport: i0, template: `
|
|
69793
70692
|
<pptx-modal-dialog
|
|
69794
70693
|
[open]="open()"
|
|
69795
70694
|
[title]="(active() ? 'pptx.share.activeTitle' : 'pptx.toolbar.share') | translate"
|
|
@@ -69816,6 +70715,14 @@ class ShareDialogComponent {
|
|
|
69816
70715
|
</span>
|
|
69817
70716
|
</div>
|
|
69818
70717
|
|
|
70718
|
+
@if (p2p()) {
|
|
70719
|
+
<div class="pptx-ng-share-status-row">
|
|
70720
|
+
<span class="pptx-ng-share-count" style="margin-left: 0">
|
|
70721
|
+
{{ 'pptx.share.p2pServerValue' | translate }}
|
|
70722
|
+
</span>
|
|
70723
|
+
</div>
|
|
70724
|
+
}
|
|
70725
|
+
|
|
69819
70726
|
@if (shareUrl()) {
|
|
69820
70727
|
<div class="pptx-ng-share-field">
|
|
69821
70728
|
<label class="pptx-ng-share-label">{{
|
|
@@ -69892,6 +70799,9 @@ class ShareDialogComponent {
|
|
|
69892
70799
|
[value]="serverUrl()"
|
|
69893
70800
|
(input)="serverUrl.set(asValue($event))"
|
|
69894
70801
|
/>
|
|
70802
|
+
@if (isP2p()) {
|
|
70803
|
+
<p class="pptx-ng-share-hint">{{ 'pptx.share.p2pHint' | translate }}</p>
|
|
70804
|
+
}
|
|
69895
70805
|
</div>
|
|
69896
70806
|
</div>
|
|
69897
70807
|
}
|
|
@@ -69943,6 +70853,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
69943
70853
|
</span>
|
|
69944
70854
|
</div>
|
|
69945
70855
|
|
|
70856
|
+
@if (p2p()) {
|
|
70857
|
+
<div class="pptx-ng-share-status-row">
|
|
70858
|
+
<span class="pptx-ng-share-count" style="margin-left: 0">
|
|
70859
|
+
{{ 'pptx.share.p2pServerValue' | translate }}
|
|
70860
|
+
</span>
|
|
70861
|
+
</div>
|
|
70862
|
+
}
|
|
70863
|
+
|
|
69946
70864
|
@if (shareUrl()) {
|
|
69947
70865
|
<div class="pptx-ng-share-field">
|
|
69948
70866
|
<label class="pptx-ng-share-label">{{
|
|
@@ -70019,6 +70937,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
70019
70937
|
[value]="serverUrl()"
|
|
70020
70938
|
(input)="serverUrl.set(asValue($event))"
|
|
70021
70939
|
/>
|
|
70940
|
+
@if (isP2p()) {
|
|
70941
|
+
<p class="pptx-ng-share-hint">{{ 'pptx.share.p2pHint' | translate }}</p>
|
|
70942
|
+
}
|
|
70022
70943
|
</div>
|
|
70023
70944
|
</div>
|
|
70024
70945
|
}
|
|
@@ -70040,7 +70961,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
70040
70961
|
</div>
|
|
70041
70962
|
</pptx-modal-dialog>
|
|
70042
70963
|
`, styles: [".pptx-ng-share-form,.pptx-ng-share-active{display:flex;flex-direction:column;gap:1rem}.pptx-ng-share-desc{margin:0;font-size:.8125rem;color:var(--pptx-muted-foreground, #9a9a9a)}.pptx-ng-share-field{display:flex;flex-direction:column;gap:.375rem}.pptx-ng-share-label{font-size:.75rem;font-weight:500;color:var(--pptx-foreground, #e5e5e5)}.pptx-ng-share-input{width:100%;padding:.375rem .75rem;border-radius:.375rem;border:1px solid var(--pptx-border, #2a2a2a);background:var(--pptx-background, #111);color:var(--pptx-foreground, #e5e5e5);font-size:.8125rem}.pptx-ng-share-input:focus{outline:none;border-color:var(--pptx-primary, #6366f1);box-shadow:0 0 0 1px var(--pptx-primary, #6366f1)}.pptx-ng-share-btn{padding:.375rem .75rem;border:none;border-radius:.375rem;background:var(--pptx-muted, #2a2a2a);color:var(--pptx-foreground, #e5e5e5);font-size:.75rem;cursor:pointer}.pptx-ng-share-btn-primary{background:var(--pptx-primary, #6366f1);color:var(--pptx-primary-foreground, #fff)}.pptx-ng-share-btn-primary:disabled{opacity:.4;cursor:not-allowed}.pptx-ng-share-stop{width:100%;padding:.5rem .75rem;border:1px solid rgba(239,68,68,.3);border-radius:.375rem;background:#ef44441a;color:#f87171;font-size:.75rem;font-weight:500;cursor:pointer}.pptx-ng-share-stop:hover{background:#ef444433}.pptx-ng-share-status-row{display:flex;align-items:center;gap:.5rem;font-size:.8125rem}.pptx-ng-share-status-dot{width:.5rem;height:.5rem;border-radius:9999px;background:var(--pptx-muted-foreground, #9a9a9a)}.pptx-ng-share-status-dot.is-on{background:#22c55e}.pptx-ng-share-status-text{font-weight:500;color:var(--pptx-foreground, #e5e5e5)}.pptx-ng-share-count{margin-left:auto;color:var(--pptx-muted-foreground, #9a9a9a)}.pptx-ng-share-link-row{display:flex;align-items:center;gap:.5rem}.pptx-ng-share-hint{margin:0;font-size:.6875rem;color:var(--pptx-muted-foreground, #9a9a9a)}\n"] }]
|
|
70043
|
-
}], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], defaults: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaults", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], connected: [{ type: i0.Input, args: [{ isSignal: true, alias: "connected", required: false }] }], userCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "userCount", required: false }] }], shareUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "shareUrl", required: false }] }], start: [{ type: i0.Output, args: ["start"] }], stop: [{ type: i0.Output, args: ["stop"] }], close: [{ type: i0.Output, args: ["close"] }] } });
|
|
70964
|
+
}], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], defaults: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaults", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], connected: [{ type: i0.Input, args: [{ isSignal: true, alias: "connected", required: false }] }], userCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "userCount", required: false }] }], shareUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "shareUrl", required: false }] }], p2p: [{ type: i0.Input, args: [{ isSignal: true, alias: "p2p", required: false }] }], start: [{ type: i0.Output, args: ["start"] }], stop: [{ type: i0.Output, args: ["stop"] }], close: [{ type: i0.Output, args: ["close"] }] } });
|
|
70044
70965
|
|
|
70045
70966
|
/**
|
|
70046
70967
|
* signatures-helpers.ts: Pure functions for digital-signature status
|
|
@@ -71562,6 +72483,34 @@ class ViewerCollaborationSessionService {
|
|
|
71562
72483
|
: '';
|
|
71563
72484
|
}, /* @ts-ignore */
|
|
71564
72485
|
...(ngDevMode ? [{ debugName: "broadcastViewerUrl" }] : /* istanbul ignore next */ []));
|
|
72486
|
+
/** Whether the active session is serverless (peer-to-peer / webrtc). */
|
|
72487
|
+
activeSessionP2p = computed(() => (this.activeSession()?.serverUrl ?? '').trim().length === 0 && this.collab.active(), /* @ts-ignore */
|
|
72488
|
+
...(ngDevMode ? [{ debugName: "activeSessionP2p" }] : /* istanbul ignore next */ []));
|
|
72489
|
+
/**
|
|
72490
|
+
* Assemble the {@link ConnectOptions} shared by every connect call site: apply
|
|
72491
|
+
* remote slides to the editor, size the presence bounds to the canvas, and
|
|
72492
|
+
* expose the source bytes + separated template elements for write-back.
|
|
72493
|
+
*/
|
|
72494
|
+
connectOptions() {
|
|
72495
|
+
const host = this.requireHost();
|
|
72496
|
+
const size = host.canvasSize();
|
|
72497
|
+
return {
|
|
72498
|
+
onRemoteSlides: (slides) => host.applyRemoteSlides(slides),
|
|
72499
|
+
canvasWidth: size.width,
|
|
72500
|
+
canvasHeight: size.height,
|
|
72501
|
+
getSourceBytes: () => host.getSourceBytes(),
|
|
72502
|
+
getTemplateElements: () => host.getTemplateElements(),
|
|
72503
|
+
};
|
|
72504
|
+
}
|
|
72505
|
+
/**
|
|
72506
|
+
* Connect and immediately seed the sync baseline with the current deck, so a
|
|
72507
|
+
* joiner whose deck is still a placeholder never broadcasts (and overwrites)
|
|
72508
|
+
* the shared document before the first remote sync arrives.
|
|
72509
|
+
*/
|
|
72510
|
+
connectWithBaseline(config) {
|
|
72511
|
+
void this.collab.connect(config, this.connectOptions());
|
|
72512
|
+
this.collab.seedBaseline(this.requireHost().currentSlides());
|
|
72513
|
+
}
|
|
71565
72514
|
/**
|
|
71566
72515
|
* Seed values for the Share dialog: the host-supplied `shareDefaults`, with
|
|
71567
72516
|
* `userName` falling back to `authorName` (then "You") so the local user's
|
|
@@ -71583,9 +72532,7 @@ class ViewerCollaborationSessionService {
|
|
|
71583
72532
|
syncHostConfig(config) {
|
|
71584
72533
|
if (config) {
|
|
71585
72534
|
this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
|
|
71586
|
-
|
|
71587
|
-
getTemplateElements: () => this.requireHost().getTemplateElements(),
|
|
71588
|
-
});
|
|
72535
|
+
this.connectWithBaseline(config);
|
|
71589
72536
|
}
|
|
71590
72537
|
else {
|
|
71591
72538
|
this.collab.disconnect();
|
|
@@ -71605,9 +72552,7 @@ class ViewerCollaborationSessionService {
|
|
|
71605
72552
|
roomId: collaboratorConfig.roomId,
|
|
71606
72553
|
serverUrl: collaboratorConfig.serverUrl,
|
|
71607
72554
|
});
|
|
71608
|
-
|
|
71609
|
-
getTemplateElements: () => host.getTemplateElements(),
|
|
71610
|
-
});
|
|
72555
|
+
this.connectWithBaseline(collaboratorConfig);
|
|
71611
72556
|
host.emitStart(collaboratorConfig);
|
|
71612
72557
|
}
|
|
71613
72558
|
onShareStop() {
|
|
@@ -71621,13 +72566,12 @@ class ViewerCollaborationSessionService {
|
|
|
71621
72566
|
const collabConfig = {
|
|
71622
72567
|
roomId: config.roomId,
|
|
71623
72568
|
serverUrl: config.serverUrl,
|
|
72569
|
+
transport: config.transport,
|
|
71624
72570
|
userName: host.authorName() ?? 'Presenter',
|
|
71625
72571
|
role: 'owner',
|
|
71626
72572
|
};
|
|
71627
72573
|
this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
|
|
71628
|
-
|
|
71629
|
-
getTemplateElements: () => host.getTemplateElements(),
|
|
71630
|
-
});
|
|
72574
|
+
this.connectWithBaseline(collabConfig);
|
|
71631
72575
|
host.emitStart(collabConfig);
|
|
71632
72576
|
}
|
|
71633
72577
|
onBroadcastStop() {
|
|
@@ -73298,7 +74242,7 @@ class FontEmbeddingListComponent {
|
|
|
73298
74242
|
missingCount = input(0, /* @ts-ignore */
|
|
73299
74243
|
...(ngDevMode ? [{ debugName: "missingCount" }] : /* istanbul ignore next */ []));
|
|
73300
74244
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: FontEmbeddingListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
73301
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: FontEmbeddingListComponent, isStandalone: true, selector: "pptx-font-embedding-list", inputs: { usedFontFamilies: { classPropertyName: "usedFontFamilies", publicName: "usedFontFamilies", isSignal: true, isRequired: false, transformFunction: null }, availableFamilies: { classPropertyName: "availableFamilies", publicName: "availableFamilies", isSignal: true, isRequired: false, transformFunction: null }, embeddedSet: { classPropertyName: "embeddedSet", publicName: "embeddedSet", isSignal: true, isRequired: false, transformFunction: null }, scanning: { classPropertyName: "scanning", publicName: "scanning", isSignal: true, isRequired: false, transformFunction: null }, missingCount: { classPropertyName: "missingCount", publicName: "missingCount", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
74245
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: FontEmbeddingListComponent, isStandalone: true, selector: "pptx-font-embedding-list", inputs: { usedFontFamilies: { classPropertyName: "usedFontFamilies", publicName: "usedFontFamilies", isSignal: true, isRequired: false, transformFunction: null }, availableFamilies: { classPropertyName: "availableFamilies", publicName: "availableFamilies", isSignal: true, isRequired: false, transformFunction: null }, embeddedSet: { classPropertyName: "embeddedSet", publicName: "embeddedSet", isSignal: true, isRequired: false, transformFunction: null }, scanning: { classPropertyName: "scanning", publicName: "scanning", isSignal: true, isRequired: false, transformFunction: null }, missingCount: { classPropertyName: "missingCount", publicName: "missingCount", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
73302
74246
|
<div class="pptx-ng-fonts-section">
|
|
73303
74247
|
<h3 class="pptx-ng-fonts-section-title">
|
|
73304
74248
|
{{ 'pptx.fontEmbedding.usedFonts' | translate: { count: usedFontFamilies().length } }}
|
|
@@ -73347,7 +74291,7 @@ class FontEmbeddingListComponent {
|
|
|
73347
74291
|
}
|
|
73348
74292
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: FontEmbeddingListComponent, decorators: [{
|
|
73349
74293
|
type: Component,
|
|
73350
|
-
args: [{ selector: 'pptx-font-embedding-list', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
74294
|
+
args: [{ selector: 'pptx-font-embedding-list', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'contents' }, template: `
|
|
73351
74295
|
<div class="pptx-ng-fonts-section">
|
|
73352
74296
|
<h3 class="pptx-ng-fonts-section-title">
|
|
73353
74297
|
{{ 'pptx.fontEmbedding.usedFonts' | translate: { count: usedFontFamilies().length } }}
|
|
@@ -75218,7 +76162,7 @@ class ViewerExtraDialogsComponent {
|
|
|
75218
76162
|
this.svc.showPassword.set(false);
|
|
75219
76163
|
}
|
|
75220
76164
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerExtraDialogsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
75221
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.5", type: ViewerExtraDialogsComponent, isStandalone: true, selector: "pptx-viewer-extra-dialogs", inputs: { activeSlideIndex: { classPropertyName: "activeSlideIndex", publicName: "activeSlideIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedElementId: { classPropertyName: "selectedElementId", publicName: "selectedElementId", isSignal: true, isRequired: false, transformFunction: null }, filePath: { classPropertyName: "filePath", publicName: "filePath", isSignal: true, isRequired: false, transformFunction: null }, customShows: { classPropertyName: "customShows", publicName: "customShows", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { restoreContent: "restoreContent" }, ngImport: i0, template: `
|
|
76165
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.5", type: ViewerExtraDialogsComponent, isStandalone: true, selector: "pptx-viewer-extra-dialogs", inputs: { activeSlideIndex: { classPropertyName: "activeSlideIndex", publicName: "activeSlideIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedElementId: { classPropertyName: "selectedElementId", publicName: "selectedElementId", isSignal: true, isRequired: false, transformFunction: null }, filePath: { classPropertyName: "filePath", publicName: "filePath", isSignal: true, isRequired: false, transformFunction: null }, customShows: { classPropertyName: "customShows", publicName: "customShows", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { restoreContent: "restoreContent" }, host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
75222
76166
|
<pptx-equation-editor-dialog
|
|
75223
76167
|
[open]="svc.showEquation()"
|
|
75224
76168
|
[existingOmml]="svc.editingEquationOmml()"
|
|
@@ -75299,6 +76243,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
75299
76243
|
selector: 'pptx-viewer-extra-dialogs',
|
|
75300
76244
|
standalone: true,
|
|
75301
76245
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
76246
|
+
host: { class: 'contents' },
|
|
75302
76247
|
imports: [
|
|
75303
76248
|
EquationEditorDialogComponent,
|
|
75304
76249
|
SetUpSlideShowDialogComponent,
|
|
@@ -76033,6 +76978,14 @@ class PowerPointViewerComponent {
|
|
|
76033
76978
|
...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
76034
76979
|
zoomPercent = computed(() => Math.round(this.zoom() * 100), /* @ts-ignore */
|
|
76035
76980
|
...(ngDevMode ? [{ debugName: "zoomPercent" }] : /* istanbul ignore next */ []));
|
|
76981
|
+
/**
|
|
76982
|
+
* Remote cursors filtered to the slide the local user is viewing, so peers'
|
|
76983
|
+
* cursors only appear on the shared slide (mirrors React/Vue).
|
|
76984
|
+
*/
|
|
76985
|
+
collabCursors = computed(() => presenceToCursors(this.collab.presence(), this.activeSlideIndex()), /* @ts-ignore */
|
|
76986
|
+
...(ngDevMode ? [{ debugName: "collabCursors" }] : /* istanbul ignore next */ []));
|
|
76987
|
+
/** Timestamp of the last cursor broadcast (throttle gate). */
|
|
76988
|
+
lastCursorBroadcast = 0;
|
|
76036
76989
|
/** Fullscreen presentation-mode overlay visibility. */
|
|
76037
76990
|
presenting = signal(false, /* @ts-ignore */
|
|
76038
76991
|
...(ngDevMode ? [{ debugName: "presenting" }] : /* istanbul ignore next */ []));
|
|
@@ -76297,6 +77250,47 @@ class PowerPointViewerComponent {
|
|
|
76297
77250
|
effect(() => {
|
|
76298
77251
|
this.session.syncHostConfig(this.collaboration());
|
|
76299
77252
|
});
|
|
77253
|
+
// Push local slide edits into the shared Y.Doc (reconcile-based; the
|
|
77254
|
+
// service guards against echoing remote-applied changes and against
|
|
77255
|
+
// clobbering with an empty deck). A broadcast `viewer` never writes, so a
|
|
77256
|
+
// follow-along joiner cannot overwrite the presenter's deck.
|
|
77257
|
+
effect(() => {
|
|
77258
|
+
const slides = this.editor.slides();
|
|
77259
|
+
if (this.collab.active() && this.collab.activeRole() !== 'viewer') {
|
|
77260
|
+
this.collab.broadcastSlides(slides);
|
|
77261
|
+
}
|
|
77262
|
+
});
|
|
77263
|
+
// Publish the local selection so peers can draw remote selection boxes.
|
|
77264
|
+
effect(() => {
|
|
77265
|
+
const ids = this.editor.selectedIds();
|
|
77266
|
+
if (this.collab.active()) {
|
|
77267
|
+
this.collab.setSelection(ids[0], this.activeSlideIndex());
|
|
77268
|
+
}
|
|
77269
|
+
});
|
|
77270
|
+
// Publish the local active slide so followers navigate with us.
|
|
77271
|
+
effect(() => {
|
|
77272
|
+
const index = this.activeSlideIndex();
|
|
77273
|
+
if (this.collab.active()) {
|
|
77274
|
+
this.collab.setActiveSlide(index);
|
|
77275
|
+
}
|
|
77276
|
+
});
|
|
77277
|
+
// Follow mode: mirror the followed peer's active slide.
|
|
77278
|
+
effect(() => {
|
|
77279
|
+
const target = this.collab.followedSlideIndex();
|
|
77280
|
+
if (target !== null) {
|
|
77281
|
+
this.goTo(target);
|
|
77282
|
+
}
|
|
77283
|
+
});
|
|
77284
|
+
// Broadcast auto-follow: a `viewer` tracks the broadcaster (owner) peer.
|
|
77285
|
+
effect(() => {
|
|
77286
|
+
if (this.collab.activeRole() !== 'viewer') {
|
|
77287
|
+
return;
|
|
77288
|
+
}
|
|
77289
|
+
const target = this.collab.broadcasterSlideIndex();
|
|
77290
|
+
if (target !== null) {
|
|
77291
|
+
this.goTo(target);
|
|
77292
|
+
}
|
|
77293
|
+
});
|
|
76300
77294
|
// Hand the export/print orchestrator the live navigation signal + deck
|
|
76301
77295
|
// accessors + stage resolver so it can flip the stage and capture slides.
|
|
76302
77296
|
this.xport.bind({
|
|
@@ -76318,6 +77312,10 @@ class PowerPointViewerComponent {
|
|
|
76318
77312
|
authorName: () => this.authorName(),
|
|
76319
77313
|
shareDefaults: () => this.shareDefaults(),
|
|
76320
77314
|
getTemplateElements: () => this.editor.templateElementsBySlideId(),
|
|
77315
|
+
applyRemoteSlides: (slides) => this.editor.applyRemoteSlides(slides),
|
|
77316
|
+
canvasSize: () => this.loader.canvasSize(),
|
|
77317
|
+
getSourceBytes: () => this.currentSourceBytes(),
|
|
77318
|
+
currentSlides: () => this.editor.slides(),
|
|
76321
77319
|
emitStart: (config) => this.startCollaboration.emit(config),
|
|
76322
77320
|
emitStop: () => this.stopCollaboration.emit(),
|
|
76323
77321
|
});
|
|
@@ -76373,6 +77371,40 @@ class PowerPointViewerComponent {
|
|
|
76373
77371
|
goNext() {
|
|
76374
77372
|
this.goTo(this.activeSlideIndex() + 1);
|
|
76375
77373
|
}
|
|
77374
|
+
/**
|
|
77375
|
+
* Publish the local cursor while the pointer moves over the canvas. Throttled
|
|
77376
|
+
* to {@link BROADCAST_THROTTLE_MS}; coordinates are mapped from client space
|
|
77377
|
+
* into unscaled slide space (dividing by zoom, matching the cursor overlay)
|
|
77378
|
+
* and clamped to the canvas bounds.
|
|
77379
|
+
*/
|
|
77380
|
+
onCollabPointerMove(event) {
|
|
77381
|
+
if (!this.collab.active()) {
|
|
77382
|
+
return;
|
|
77383
|
+
}
|
|
77384
|
+
const now = Date.now();
|
|
77385
|
+
if (now - this.lastCursorBroadcast < BROADCAST_THROTTLE_MS) {
|
|
77386
|
+
return;
|
|
77387
|
+
}
|
|
77388
|
+
this.lastCursorBroadcast = now;
|
|
77389
|
+
const host = this.mainEl()?.nativeElement;
|
|
77390
|
+
if (!host) {
|
|
77391
|
+
return;
|
|
77392
|
+
}
|
|
77393
|
+
const rect = host.getBoundingClientRect();
|
|
77394
|
+
const zoom = this.zoom() || 1;
|
|
77395
|
+
const size = this.loader.canvasSize();
|
|
77396
|
+
const x = clampCursorPosition((event.clientX - rect.left) / zoom, 0, size.width);
|
|
77397
|
+
const y = clampCursorPosition((event.clientY - rect.top) / zoom, 0, size.height);
|
|
77398
|
+
this.collab.setCursor(x, y, this.activeSlideIndex());
|
|
77399
|
+
}
|
|
77400
|
+
/** The loaded source `.pptx` bytes (for elected-writer write-back), if any. */
|
|
77401
|
+
currentSourceBytes() {
|
|
77402
|
+
const content = this.contentOverride() ?? this.content();
|
|
77403
|
+
if (!content) {
|
|
77404
|
+
return null;
|
|
77405
|
+
}
|
|
77406
|
+
return content instanceof Uint8Array ? content : new Uint8Array(content);
|
|
77407
|
+
}
|
|
76376
77408
|
// ── Theme gallery (Design tab) ─────────────────────────────────────────────
|
|
76377
77409
|
/**
|
|
76378
77410
|
* Apply a built-in theme preset to the whole deck.
|
|
@@ -77010,7 +78042,7 @@ class PowerPointViewerComponent {
|
|
|
77010
78042
|
</nav>
|
|
77011
78043
|
}
|
|
77012
78044
|
|
|
77013
|
-
<main class="pptx-ng-main" #mainEl>
|
|
78045
|
+
<main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
|
|
77014
78046
|
<pptx-slide-canvas
|
|
77015
78047
|
[slide]="activeSlide()"
|
|
77016
78048
|
[canvasSize]="loader.canvasSize()"
|
|
@@ -77048,7 +78080,22 @@ class PowerPointViewerComponent {
|
|
|
77048
78080
|
(tableChange)="onTableChange($event)"
|
|
77049
78081
|
/>
|
|
77050
78082
|
@if (collab.connected()) {
|
|
77051
|
-
<pptx-collaboration-cursors [cursors]="
|
|
78083
|
+
<pptx-collaboration-cursors [cursors]="collabCursors()" [zoom]="zoom()" />
|
|
78084
|
+
<pptx-remote-selection-overlay
|
|
78085
|
+
[presences]="collab.presence()"
|
|
78086
|
+
[elements]="activeSlide()?.elements ?? []"
|
|
78087
|
+
[activeSlideIndex]="activeSlideIndex()"
|
|
78088
|
+
[zoom]="zoom()"
|
|
78089
|
+
/>
|
|
78090
|
+
}
|
|
78091
|
+
@if (collab.active() && collab.presence().length > 0) {
|
|
78092
|
+
<div class="pptx-ng-collab-follow">
|
|
78093
|
+
<pptx-follow-mode-bar
|
|
78094
|
+
[presences]="collab.presence()"
|
|
78095
|
+
[followedClientId]="collab.followedClientId()"
|
|
78096
|
+
(follow)="collab.followUser($event)"
|
|
78097
|
+
/>
|
|
78098
|
+
</div>
|
|
77052
78099
|
}
|
|
77053
78100
|
@if (showNotes() && !mobile.isMobile()) {
|
|
77054
78101
|
<aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
|
|
@@ -77314,6 +78361,7 @@ class PowerPointViewerComponent {
|
|
|
77314
78361
|
[connected]="collab.connected()"
|
|
77315
78362
|
[userCount]="collab.connectedCount()"
|
|
77316
78363
|
[shareUrl]="session.shareUrl()"
|
|
78364
|
+
[p2p]="session.activeSessionP2p()"
|
|
77317
78365
|
[defaults]="session.shareDialogDefaults()"
|
|
77318
78366
|
(start)="session.onShareStart($event)"
|
|
77319
78367
|
(stop)="session.onShareStop()"
|
|
@@ -77326,6 +78374,7 @@ class PowerPointViewerComponent {
|
|
|
77326
78374
|
[connected]="collab.connected()"
|
|
77327
78375
|
[viewerCount]="collab.presence().length"
|
|
77328
78376
|
[viewerUrl]="session.broadcastViewerUrl()"
|
|
78377
|
+
[p2p]="session.activeSessionP2p()"
|
|
77329
78378
|
[defaults]="{ serverUrl: shareDefaults()?.serverUrl }"
|
|
77330
78379
|
(start)="session.onBroadcastStart($event)"
|
|
77331
78380
|
(stop)="session.onBroadcastStop()"
|
|
@@ -77433,7 +78482,7 @@ class PowerPointViewerComponent {
|
|
|
77433
78482
|
/>
|
|
77434
78483
|
}
|
|
77435
78484
|
</div>
|
|
77436
|
-
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SlideCanvasComponent, selector: "pptx-slide-canvas", inputs: ["slide", "canvasSize", "mediaDataUrls", "zoom", "editable", "showGrid", "showRulers", "showGuides", "snapToGrid", "snapToGuides", "autoFit", "interactive", "selectedIds", "editingId", "editTemplateMode", "templateElements", "drawTool", "drawColor", "drawWidth"], outputs: ["elementSelect", "backgroundClick", "transformStart", "transformUpdate", "contextMenu", "textEditStart", "textCommit", "textCancel", "textFormat", "rotateUpdate", "marqueeSelect", "inkStrokeComplete", "eraserHit", "cellCommit", "tableChange"] }, { kind: "component", type: PresentationOverlayComponent, selector: "pptx-presentation-overlay", inputs: ["slides", "canvasSize", "mediaDataUrls", "startIndex"], outputs: ["indexChange", "closed", "annotationsExit"] }, { kind: "component", type: PresenterViewComponent, selector: "pptx-presenter-view", inputs: ["slides", "currentSlideIndex", "canvasSize", "templateElements", "mediaDataUrls", "presentationStartTime", "isAudienceWindowOpen"], outputs: ["movePresentationSlide", "exit", "openAudienceWindow", "closeAudienceWindow"] }, { kind: "component", type: MobilePresenterViewComponent, selector: "pptx-mobile-presenter-view", inputs: ["slides", "currentSlideIndex", "canvasSize", "templateElements", "mediaDataUrls", "presentationStartTime"], outputs: ["movePresentationSlide", "exit"] }, { kind: "component", type: SlideSorterOverlayComponent, selector: "pptx-slide-sorter-overlay", inputs: ["slides", "canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["select", "closed"] }, { kind: "component", type: FindBarComponent, selector: "pptx-find-bar", inputs: ["slides"], outputs: ["navigate", "closed"] }, { kind: "component", type: FindReplaceBarComponent, selector: "pptx-find-replace-bar", inputs: ["matchCount", "matchIndex"], outputs: ["find", "navigate", "replaceOne", "replaceAll", "close"] }, { kind: "component", type: InspectorPanelComponent, selector: "pptx-inspector-panel", inputs: ["element", "slideIndex"] }, { kind: "component", type: SlidesPanelComponent, selector: "pptx-slides-panel", inputs: ["canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["select"] }, { kind: "component", type: StatusBarComponent, selector: "pptx-status-bar", inputs: ["slideIndex", "slideCount", "canEdit", "dirty", "notesOpen", "zoomPercent", "sorterActive", "presenting"], outputs: ["toggleNotes", "normalView", "openSorter", "slideShow", "zoomIn", "zoomOut", "zoomReset"] }, { kind: "component", type: EditorContextMenuComponent, selector: "pptx-editor-context-menu", inputs: ["x", "y", "slideIndex"], outputs: ["closed"] }, { kind: "component", type: ExportProgressModalComponent, selector: "pptx-export-progress-modal", inputs: ["open", "title", "progress", "statusMessage"], outputs: ["cancel"] }, { kind: "component", type: CommentsPanelComponent, selector: "pptx-comments-panel", inputs: ["comments", "authorName"], outputs: ["add", "remove", "resolve"] }, { kind: "component", type: SignaturesPanelComponent, selector: "pptx-signatures-panel", inputs: ["signatures"] }, { kind: "component", type: AccessibilityPanelComponent, selector: "pptx-accessibility-panel", inputs: ["issues"], outputs: ["selectSlide"] }, { kind: "component", type: CollaborationCursorsComponent, selector: "pptx-collaboration-cursors", inputs: ["cursors", "zoom"] }, { kind: "component", type: PropertiesDialogComponent, selector: "pptx-properties-dialog", inputs: ["open", "properties"], outputs: ["save", "close"] }, { kind: "component", type: HyperlinkDialogComponent, selector: "pptx-hyperlink-dialog", inputs: ["open", "element"], outputs: ["save", "close"] }, { kind: "component", type: PrintDialogComponent, selector: "pptx-print-dialog", inputs: ["slides", "activeSlideIndex", "defaultSlidesPerPage", "defaultFrameSlides"], outputs: ["print", "cancel"] }, { kind: "component", type: ShareDialogComponent, selector: "pptx-share-dialog", inputs: ["open", "defaults", "active", "connected", "userCount", "shareUrl"], outputs: ["start", "stop", "close"] }, { kind: "component", type: BroadcastDialogComponent, selector: "pptx-broadcast-dialog", inputs: ["open", "defaults", "active", "connected", "viewerCount", "viewerUrl"], outputs: ["start", "stop", "close"] }, { kind: "component", type: MobileBottomBarComponent, selector: "pptx-mobile-bottom-bar", inputs: ["slideCount", "commentCount", "activeSheet"], outputs: ["openSlides", "insert", "openFormat", "openComments", "notes"] }, { kind: "component", type: MobileMenuSheetComponent, selector: "pptx-mobile-menu-sheet", inputs: ["open", "slideCount", "exporting", "showNotes", "canEdit"], outputs: ["closed", "openFind", "openSorter", "toggleNotes", "insertText", "present", "openFile", "savePptx", "exportPng", "exportPdf", "exportGif", "exportVideo", "print"] }, { kind: "component", type: MobileSlidesSheetComponent, selector: "pptx-mobile-slides-sheet", inputs: ["open", "slides", "canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["closed", "jumpToSlide"] }, { kind: "component", type: MobileToolbarComponent, selector: "pptx-mobile-toolbar", inputs: ["canUndo", "canRedo", "canPresent", "canEdit", "menuOpen"], outputs: ["toggleMenu", "undo", "redo", "save", "present"] }, { kind: "component", type: NotesPanelComponent, selector: "pptx-notes-panel", inputs: ["slide"], outputs: ["update"] }, { kind: "component", type: RibbonComponent, selector: "pptx-ribbon", inputs: ["slideIndex", "slideCount", "canEdit", "selectedElement", "zoomPercent", "formatPainterActive", "canActivateFormatPainter", "exporting", "showGrid", "showRulers", "showGuides", "snapToGrid", "eyedropperActive", "themeGalleryOpen", "sidebarCollapsed", "inspectorOpen", "commentsOpen", "commentCount", "findOpen", "collabConnected", "connectedCount"], outputs: ["prev", "next", "zoomIn", "zoomOut", "zoomReset", "find", "present", "presenter", "share", "broadcast", "openFile", "save", "toggleSidebar", "signatures", "info", "print", "comments", "a11y", "link", "openSorter", "toggleNotes", "toggleFormatPainter", "exportPng", "exportPdf", "exportGif", "exportVideo", "replace", "toggleInspector", "drawToolChange", "toggleThemeGallery", "toggleGrid", "toggleRulers", "toggleGuides", "toggleSelectionPane", "openCustomShows", "toggleSnapToGrid", "toggleEyedropper", "openSmartArtDialog", "openEquationDialog", "openSetUpSlideShow", "openCompare", "openPassword", "openFontEmbedding", "openVersionHistory", "openShortcuts"] }, { kind: "component", type: ThemeGalleryComponent, selector: "pptx-theme-gallery", inputs: ["open", "activeName"], outputs: ["applyTheme", "close"] }, { kind: "component", type: SelectionPaneComponent, selector: "pptx-selection-pane", inputs: ["elements", "selectedIds"], outputs: ["selectElement", "bringForward", "sendBackward", "toggleHidden"] }, { kind: "component", type: CustomShowsComponent, selector: "pptx-custom-shows", inputs: ["open", "slides", "customShows", "activeCustomShowId"], outputs: ["create", "remove", "update", "setActive", "close"] }, { kind: "component", type: InsertSmartArtDialogComponent, selector: "pptx-insert-smart-art-dialog", inputs: ["open"], outputs: ["close", "insert"] }, { kind: "component", type: ViewerExtraDialogsComponent, selector: "pptx-viewer-extra-dialogs", inputs: ["activeSlideIndex", "selectedElementId", "filePath", "customShows"], outputs: ["restoreContent"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
78485
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SlideCanvasComponent, selector: "pptx-slide-canvas", inputs: ["slide", "canvasSize", "mediaDataUrls", "zoom", "editable", "showGrid", "showRulers", "showGuides", "snapToGrid", "snapToGuides", "autoFit", "interactive", "selectedIds", "editingId", "editTemplateMode", "templateElements", "drawTool", "drawColor", "drawWidth"], outputs: ["elementSelect", "backgroundClick", "transformStart", "transformUpdate", "contextMenu", "textEditStart", "textCommit", "textCancel", "textFormat", "rotateUpdate", "marqueeSelect", "inkStrokeComplete", "eraserHit", "cellCommit", "tableChange"] }, { kind: "component", type: PresentationOverlayComponent, selector: "pptx-presentation-overlay", inputs: ["slides", "canvasSize", "mediaDataUrls", "startIndex"], outputs: ["indexChange", "closed", "annotationsExit"] }, { kind: "component", type: PresenterViewComponent, selector: "pptx-presenter-view", inputs: ["slides", "currentSlideIndex", "canvasSize", "templateElements", "mediaDataUrls", "presentationStartTime", "isAudienceWindowOpen"], outputs: ["movePresentationSlide", "exit", "openAudienceWindow", "closeAudienceWindow"] }, { kind: "component", type: MobilePresenterViewComponent, selector: "pptx-mobile-presenter-view", inputs: ["slides", "currentSlideIndex", "canvasSize", "templateElements", "mediaDataUrls", "presentationStartTime"], outputs: ["movePresentationSlide", "exit"] }, { kind: "component", type: SlideSorterOverlayComponent, selector: "pptx-slide-sorter-overlay", inputs: ["slides", "canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["select", "closed"] }, { kind: "component", type: FindBarComponent, selector: "pptx-find-bar", inputs: ["slides"], outputs: ["navigate", "closed"] }, { kind: "component", type: FindReplaceBarComponent, selector: "pptx-find-replace-bar", inputs: ["matchCount", "matchIndex"], outputs: ["find", "navigate", "replaceOne", "replaceAll", "close"] }, { kind: "component", type: InspectorPanelComponent, selector: "pptx-inspector-panel", inputs: ["element", "slideIndex"] }, { kind: "component", type: SlidesPanelComponent, selector: "pptx-slides-panel", inputs: ["canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["select"] }, { kind: "component", type: StatusBarComponent, selector: "pptx-status-bar", inputs: ["slideIndex", "slideCount", "canEdit", "dirty", "notesOpen", "zoomPercent", "sorterActive", "presenting"], outputs: ["toggleNotes", "normalView", "openSorter", "slideShow", "zoomIn", "zoomOut", "zoomReset"] }, { kind: "component", type: EditorContextMenuComponent, selector: "pptx-editor-context-menu", inputs: ["x", "y", "slideIndex"], outputs: ["closed"] }, { kind: "component", type: ExportProgressModalComponent, selector: "pptx-export-progress-modal", inputs: ["open", "title", "progress", "statusMessage"], outputs: ["cancel"] }, { kind: "component", type: CommentsPanelComponent, selector: "pptx-comments-panel", inputs: ["comments", "authorName"], outputs: ["add", "remove", "resolve"] }, { kind: "component", type: SignaturesPanelComponent, selector: "pptx-signatures-panel", inputs: ["signatures"] }, { kind: "component", type: AccessibilityPanelComponent, selector: "pptx-accessibility-panel", inputs: ["issues"], outputs: ["selectSlide"] }, { kind: "component", type: CollaborationCursorsComponent, selector: "pptx-collaboration-cursors", inputs: ["cursors", "zoom"] }, { kind: "component", type: RemoteSelectionOverlayComponent, selector: "pptx-remote-selection-overlay", inputs: ["presences", "elements", "activeSlideIndex", "zoom"] }, { kind: "component", type: FollowModeBarComponent, selector: "pptx-follow-mode-bar", inputs: ["presences", "followedClientId"], outputs: ["follow"] }, { kind: "component", type: PropertiesDialogComponent, selector: "pptx-properties-dialog", inputs: ["open", "properties"], outputs: ["save", "close"] }, { kind: "component", type: HyperlinkDialogComponent, selector: "pptx-hyperlink-dialog", inputs: ["open", "element"], outputs: ["save", "close"] }, { kind: "component", type: PrintDialogComponent, selector: "pptx-print-dialog", inputs: ["slides", "activeSlideIndex", "defaultSlidesPerPage", "defaultFrameSlides"], outputs: ["print", "cancel"] }, { kind: "component", type: ShareDialogComponent, selector: "pptx-share-dialog", inputs: ["open", "defaults", "active", "connected", "userCount", "shareUrl", "p2p"], outputs: ["start", "stop", "close"] }, { kind: "component", type: BroadcastDialogComponent, selector: "pptx-broadcast-dialog", inputs: ["open", "defaults", "active", "connected", "viewerCount", "viewerUrl", "p2p"], outputs: ["start", "stop", "close"] }, { kind: "component", type: MobileBottomBarComponent, selector: "pptx-mobile-bottom-bar", inputs: ["slideCount", "commentCount", "activeSheet"], outputs: ["openSlides", "insert", "openFormat", "openComments", "notes"] }, { kind: "component", type: MobileMenuSheetComponent, selector: "pptx-mobile-menu-sheet", inputs: ["open", "slideCount", "exporting", "showNotes", "canEdit"], outputs: ["closed", "openFind", "openSorter", "toggleNotes", "insertText", "present", "openFile", "savePptx", "exportPng", "exportPdf", "exportGif", "exportVideo", "print"] }, { kind: "component", type: MobileSlidesSheetComponent, selector: "pptx-mobile-slides-sheet", inputs: ["open", "slides", "canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["closed", "jumpToSlide"] }, { kind: "component", type: MobileToolbarComponent, selector: "pptx-mobile-toolbar", inputs: ["canUndo", "canRedo", "canPresent", "canEdit", "menuOpen"], outputs: ["toggleMenu", "undo", "redo", "save", "present"] }, { kind: "component", type: NotesPanelComponent, selector: "pptx-notes-panel", inputs: ["slide"], outputs: ["update"] }, { kind: "component", type: RibbonComponent, selector: "pptx-ribbon", inputs: ["slideIndex", "slideCount", "canEdit", "selectedElement", "zoomPercent", "formatPainterActive", "canActivateFormatPainter", "exporting", "showGrid", "showRulers", "showGuides", "snapToGrid", "eyedropperActive", "themeGalleryOpen", "sidebarCollapsed", "inspectorOpen", "commentsOpen", "commentCount", "findOpen", "collabConnected", "connectedCount"], outputs: ["prev", "next", "zoomIn", "zoomOut", "zoomReset", "find", "present", "presenter", "share", "broadcast", "openFile", "save", "toggleSidebar", "signatures", "info", "print", "comments", "a11y", "link", "openSorter", "toggleNotes", "toggleFormatPainter", "exportPng", "exportPdf", "exportGif", "exportVideo", "replace", "toggleInspector", "drawToolChange", "toggleThemeGallery", "toggleGrid", "toggleRulers", "toggleGuides", "toggleSelectionPane", "openCustomShows", "toggleSnapToGrid", "toggleEyedropper", "openSmartArtDialog", "openEquationDialog", "openSetUpSlideShow", "openCompare", "openPassword", "openFontEmbedding", "openVersionHistory", "openShortcuts"] }, { kind: "component", type: ThemeGalleryComponent, selector: "pptx-theme-gallery", inputs: ["open", "activeName"], outputs: ["applyTheme", "close"] }, { kind: "component", type: SelectionPaneComponent, selector: "pptx-selection-pane", inputs: ["elements", "selectedIds"], outputs: ["selectElement", "bringForward", "sendBackward", "toggleHidden"] }, { kind: "component", type: CustomShowsComponent, selector: "pptx-custom-shows", inputs: ["open", "slides", "customShows", "activeCustomShowId"], outputs: ["create", "remove", "update", "setActive", "close"] }, { kind: "component", type: InsertSmartArtDialogComponent, selector: "pptx-insert-smart-art-dialog", inputs: ["open"], outputs: ["close", "insert"] }, { kind: "component", type: ViewerExtraDialogsComponent, selector: "pptx-viewer-extra-dialogs", inputs: ["activeSlideIndex", "selectedElementId", "filePath", "customShows"], outputs: ["restoreContent"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
77437
78486
|
}
|
|
77438
78487
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: PowerPointViewerComponent, decorators: [{
|
|
77439
78488
|
type: Component,
|
|
@@ -77483,6 +78532,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77483
78532
|
SignaturesPanelComponent,
|
|
77484
78533
|
AccessibilityPanelComponent,
|
|
77485
78534
|
CollaborationCursorsComponent,
|
|
78535
|
+
RemoteSelectionOverlayComponent,
|
|
78536
|
+
FollowModeBarComponent,
|
|
77486
78537
|
PropertiesDialogComponent,
|
|
77487
78538
|
HyperlinkDialogComponent,
|
|
77488
78539
|
PrintDialogComponent,
|
|
@@ -77627,7 +78678,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77627
78678
|
</nav>
|
|
77628
78679
|
}
|
|
77629
78680
|
|
|
77630
|
-
<main class="pptx-ng-main" #mainEl>
|
|
78681
|
+
<main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
|
|
77631
78682
|
<pptx-slide-canvas
|
|
77632
78683
|
[slide]="activeSlide()"
|
|
77633
78684
|
[canvasSize]="loader.canvasSize()"
|
|
@@ -77665,7 +78716,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77665
78716
|
(tableChange)="onTableChange($event)"
|
|
77666
78717
|
/>
|
|
77667
78718
|
@if (collab.connected()) {
|
|
77668
|
-
<pptx-collaboration-cursors [cursors]="
|
|
78719
|
+
<pptx-collaboration-cursors [cursors]="collabCursors()" [zoom]="zoom()" />
|
|
78720
|
+
<pptx-remote-selection-overlay
|
|
78721
|
+
[presences]="collab.presence()"
|
|
78722
|
+
[elements]="activeSlide()?.elements ?? []"
|
|
78723
|
+
[activeSlideIndex]="activeSlideIndex()"
|
|
78724
|
+
[zoom]="zoom()"
|
|
78725
|
+
/>
|
|
78726
|
+
}
|
|
78727
|
+
@if (collab.active() && collab.presence().length > 0) {
|
|
78728
|
+
<div class="pptx-ng-collab-follow">
|
|
78729
|
+
<pptx-follow-mode-bar
|
|
78730
|
+
[presences]="collab.presence()"
|
|
78731
|
+
[followedClientId]="collab.followedClientId()"
|
|
78732
|
+
(follow)="collab.followUser($event)"
|
|
78733
|
+
/>
|
|
78734
|
+
</div>
|
|
77669
78735
|
}
|
|
77670
78736
|
@if (showNotes() && !mobile.isMobile()) {
|
|
77671
78737
|
<aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
|
|
@@ -77931,6 +78997,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77931
78997
|
[connected]="collab.connected()"
|
|
77932
78998
|
[userCount]="collab.connectedCount()"
|
|
77933
78999
|
[shareUrl]="session.shareUrl()"
|
|
79000
|
+
[p2p]="session.activeSessionP2p()"
|
|
77934
79001
|
[defaults]="session.shareDialogDefaults()"
|
|
77935
79002
|
(start)="session.onShareStart($event)"
|
|
77936
79003
|
(stop)="session.onShareStop()"
|
|
@@ -77943,6 +79010,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77943
79010
|
[connected]="collab.connected()"
|
|
77944
79011
|
[viewerCount]="collab.presence().length"
|
|
77945
79012
|
[viewerUrl]="session.broadcastViewerUrl()"
|
|
79013
|
+
[p2p]="session.activeSessionP2p()"
|
|
77946
79014
|
[defaults]="{ serverUrl: shareDefaults()?.serverUrl }"
|
|
77947
79015
|
(start)="session.onBroadcastStart($event)"
|
|
77948
79016
|
(stop)="session.onBroadcastStop()"
|
|
@@ -78479,6 +79547,9 @@ const translationsEn = {
|
|
|
78479
79547
|
'pptx.autosave.justNow': 'just now',
|
|
78480
79548
|
'pptx.autosave.oneMinAgo': '1 min ago',
|
|
78481
79549
|
'pptx.autosave.minutesAgo': '{{count}} min ago',
|
|
79550
|
+
'pptx.autosave.saveFailed': 'Save failed',
|
|
79551
|
+
'pptx.autosave.savedShort': 'Saved',
|
|
79552
|
+
'pptx.autosave.allChangesSaved': 'All changes saved',
|
|
78482
79553
|
// Toolbar
|
|
78483
79554
|
'pptx.toolbar.toggleSlidesPanel': 'Toggle slides panel',
|
|
78484
79555
|
'pptx.toolbar.undo': 'Undo',
|
|
@@ -78597,7 +79668,17 @@ const translationsEn = {
|
|
|
78597
79668
|
'pptx.collaboration.youLabel': '{{name}} (you)',
|
|
78598
79669
|
'pptx.collaboration.usersConnected': '{{count}} user(s) connected',
|
|
78599
79670
|
'pptx.collaboration.moreUsers': '{{count}} more user(s)',
|
|
79671
|
+
'pptx.collaboration.onePersonHere': '1 person here',
|
|
79672
|
+
'pptx.collaboration.peopleHere': '{{count}} people here',
|
|
78600
79673
|
'pptx.collaboration.retry': 'Retry',
|
|
79674
|
+
'pptx.collaboration.retryConnection': 'Retry connection',
|
|
79675
|
+
// Follow mode
|
|
79676
|
+
'pptx.followMode.following': 'Following',
|
|
79677
|
+
'pptx.followMode.stop': 'Stop',
|
|
79678
|
+
'pptx.followMode.stopFollowing': 'Stop following',
|
|
79679
|
+
'pptx.followMode.followCollaborator': 'Follow a collaborator',
|
|
79680
|
+
'pptx.followMode.followUser': 'Follow {{name}}',
|
|
79681
|
+
'pptx.followMode.stopFollowingUser': 'Stop following {{name}}',
|
|
78601
79682
|
// Share dialog
|
|
78602
79683
|
'pptx.share.title': 'Share Presentation',
|
|
78603
79684
|
'pptx.share.collaborationActive': 'Collaboration Active',
|