pptx-angular-viewer 1.1.57 → 1.1.59
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 +11 -0
- package/fesm2022/pptx-angular-viewer.mjs +1245 -282
- 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 +142 -21
|
@@ -22501,117 +22501,22 @@ function asSelectionIds(value) {
|
|
|
22501
22501
|
}
|
|
22502
22502
|
|
|
22503
22503
|
/**
|
|
22504
|
-
* collaboration-
|
|
22505
|
-
*
|
|
22504
|
+
* collaboration-text-codec.ts: TextSegment[] <-> Y.Text delta codec used by the
|
|
22505
|
+
* collaboration sync layer. Split out of collaboration-sync.ts to keep both
|
|
22506
|
+
* modules focused.
|
|
22506
22507
|
*
|
|
22507
22508
|
* 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
|
|
22509
|
+
* - DeltaOp / YTextLike: structural Yjs text interfaces (no yjs import)
|
|
22510
|
+
* - encodeTextBody: write TextSegment[] into a live YTextLike
|
|
22511
|
+
* - encodeSegmentsToDelta: pure simulation of the delta Y.Text would produce
|
|
22512
|
+
* - decodeDelta / decodeTextBody: delta -> TextSegment[]
|
|
22513
|
+
* - isYTextLike: runtime guard
|
|
22521
22514
|
*/
|
|
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
|
-
// ---------------------------------------------------------------------------
|
|
22515
|
+
function isYTextLike(value) {
|
|
22516
|
+
return (typeof value === 'object' &&
|
|
22517
|
+
value !== null &&
|
|
22518
|
+
typeof value.toDelta === 'function');
|
|
22519
|
+
}
|
|
22615
22520
|
function buildSegmentAttrs(seg) {
|
|
22616
22521
|
const a = {};
|
|
22617
22522
|
const style = seg.style;
|
|
@@ -22668,29 +22573,56 @@ function buildSegmentAttrs(seg) {
|
|
|
22668
22573
|
}
|
|
22669
22574
|
return a;
|
|
22670
22575
|
}
|
|
22576
|
+
/** Resolve the literal text a segment contributes to the Y.Text document. */
|
|
22577
|
+
function segmentInsertText(seg) {
|
|
22578
|
+
if (seg.isParagraphBreak === true || seg.isLineBreak === true) {
|
|
22579
|
+
return '\n';
|
|
22580
|
+
}
|
|
22581
|
+
if (typeof seg.text === 'string' && seg.text.length > 0) {
|
|
22582
|
+
return seg.text;
|
|
22583
|
+
}
|
|
22584
|
+
// Empty non-break run: zero-width space holds the attributes.
|
|
22585
|
+
return '';
|
|
22586
|
+
}
|
|
22671
22587
|
function encodeTextBody(segments, ytext) {
|
|
22672
22588
|
let offset = 0;
|
|
22673
22589
|
for (const raw of segments) {
|
|
22674
22590
|
const seg = raw;
|
|
22675
22591
|
const attrs = buildSegmentAttrs(seg);
|
|
22676
|
-
const
|
|
22677
|
-
|
|
22678
|
-
|
|
22679
|
-
|
|
22680
|
-
|
|
22681
|
-
|
|
22682
|
-
|
|
22683
|
-
|
|
22592
|
+
const text = segmentInsertText(seg);
|
|
22593
|
+
// Always pass an explicit attrs object: Yjs makes attribute-less inserts
|
|
22594
|
+
// inherit the preceding character's formatting, which bled styles and
|
|
22595
|
+
// paragraph-break markers into unstyled runs.
|
|
22596
|
+
ytext.insert(offset, text, attrs);
|
|
22597
|
+
offset += text.length;
|
|
22598
|
+
}
|
|
22599
|
+
}
|
|
22600
|
+
/**
|
|
22601
|
+
* Pure simulation of the delta a Y.Text produces after `encodeTextBody`:
|
|
22602
|
+
* adjacent runs with identical attribute maps merge into one op, matching
|
|
22603
|
+
* Yjs run-merging. Used to compare desired segments against a live Y.Text
|
|
22604
|
+
* without instantiating one.
|
|
22605
|
+
*/
|
|
22606
|
+
function encodeSegmentsToDelta(segments) {
|
|
22607
|
+
const ops = [];
|
|
22608
|
+
for (const raw of segments) {
|
|
22609
|
+
const seg = raw;
|
|
22610
|
+
const attrs = buildSegmentAttrs(seg);
|
|
22611
|
+
const attributes = Object.keys(attrs).length > 0 ? attrs : undefined;
|
|
22612
|
+
const text = segmentInsertText(seg);
|
|
22613
|
+
const prev = ops[ops.length - 1];
|
|
22614
|
+
if (prev &&
|
|
22615
|
+
typeof prev.insert === 'string' &&
|
|
22616
|
+
JSON.stringify(prev.attributes ?? null) === JSON.stringify(attributes ?? null)) {
|
|
22617
|
+
prev.insert += text;
|
|
22684
22618
|
}
|
|
22685
22619
|
else {
|
|
22686
|
-
|
|
22687
|
-
ytext.insert(offset, '', hasAttrs ? attrs : undefined);
|
|
22688
|
-
offset += 1;
|
|
22620
|
+
ops.push(attributes ? { insert: text, attributes } : { insert: text });
|
|
22689
22621
|
}
|
|
22690
22622
|
}
|
|
22623
|
+
return ops;
|
|
22691
22624
|
}
|
|
22692
|
-
function
|
|
22693
|
-
const delta = ytext.toDelta();
|
|
22625
|
+
function decodeDelta(delta) {
|
|
22694
22626
|
const segments = [];
|
|
22695
22627
|
for (const op of delta) {
|
|
22696
22628
|
if (typeof op.insert !== 'string' || op.insert === '') {
|
|
@@ -22791,6 +22723,128 @@ function decodeTextBody(ytext) {
|
|
|
22791
22723
|
}
|
|
22792
22724
|
return segments;
|
|
22793
22725
|
}
|
|
22726
|
+
function decodeTextBody(ytext) {
|
|
22727
|
+
return decodeDelta(ytext.toDelta());
|
|
22728
|
+
}
|
|
22729
|
+
|
|
22730
|
+
/**
|
|
22731
|
+
* collaboration-sync.ts: Framework-agnostic CRDT sync utilities for the
|
|
22732
|
+
* pptx-viewer collaboration stack (Yjs backend).
|
|
22733
|
+
*
|
|
22734
|
+
* Exports:
|
|
22735
|
+
* - Structural Yjs interfaces (no hard yjs import - bindings pass live instances)
|
|
22736
|
+
* - YjsFactories: factory interface bindings implement using `new Y.Map()` etc.
|
|
22737
|
+
* - writeElementToYMap / readElementFromYMap: PptxElement <-> YMapLike
|
|
22738
|
+
* - writeSlideToYMap / readSlideFromYMap: PptxSlide <-> YMapLike
|
|
22739
|
+
* - writeSlidesToYDoc / readSlidesFromYDoc: PptxSlide[] <-> Y.Doc
|
|
22740
|
+
* - observeYDocSlides: register a change listener on the pptx:slides array
|
|
22741
|
+
* - re-exports of the text codec (collaboration-text-codec.ts)
|
|
22742
|
+
*
|
|
22743
|
+
* Y.Doc schema:
|
|
22744
|
+
* pptx:slides - Y.Array of slide Y.Maps
|
|
22745
|
+
* Each slide Y.Map has scalar keys + `_`-prefixed JSON blobs + `elements`
|
|
22746
|
+
* Each element Y.Map has scalar keys + `_`-prefixed JSON blobs + `textBody`
|
|
22747
|
+
* textBody is a Y.Text with one delta-op per TextSegment
|
|
22748
|
+
*
|
|
22749
|
+
* NOTE: packages/tools' pptx-codec.ts implements a similar schema but with
|
|
22750
|
+
* different complex-field key prefixes (e.g. `_textStyle` vs `_ts` here); the
|
|
22751
|
+
* two doc layouts are NOT interchangeable on the same Y.Doc.
|
|
22752
|
+
*
|
|
22753
|
+
* Prefer `reconcileSlidesInYDoc` (collaboration-reconcile.ts) over
|
|
22754
|
+
* `writeSlidesToYDoc` for live editing: it updates only what changed instead
|
|
22755
|
+
* of replacing the whole slides array, so concurrent edits merge per
|
|
22756
|
+
* slide/element/field rather than colliding at document granularity.
|
|
22757
|
+
*/
|
|
22758
|
+
// ---------------------------------------------------------------------------
|
|
22759
|
+
// Y.Doc schema constants
|
|
22760
|
+
// ---------------------------------------------------------------------------
|
|
22761
|
+
const YDOC_SLIDES_KEY = 'pptx:slides';
|
|
22762
|
+
const YDOC_META_KEY = 'pptx:meta';
|
|
22763
|
+
const SCALAR_ELEMENT_KEYS = new Set([
|
|
22764
|
+
'id',
|
|
22765
|
+
'type',
|
|
22766
|
+
'x',
|
|
22767
|
+
'y',
|
|
22768
|
+
'width',
|
|
22769
|
+
'height',
|
|
22770
|
+
'rotation',
|
|
22771
|
+
'flipHorizontal',
|
|
22772
|
+
'flipVertical',
|
|
22773
|
+
'hidden',
|
|
22774
|
+
'opacity',
|
|
22775
|
+
'text',
|
|
22776
|
+
'name',
|
|
22777
|
+
'altText',
|
|
22778
|
+
'shapeType',
|
|
22779
|
+
'placeholder',
|
|
22780
|
+
'imagePath',
|
|
22781
|
+
'imageData',
|
|
22782
|
+
'svgContent',
|
|
22783
|
+
'inkSvg',
|
|
22784
|
+
'sourceSlideId',
|
|
22785
|
+
'mediaType',
|
|
22786
|
+
'mediaPath',
|
|
22787
|
+
'linkedTxbxId',
|
|
22788
|
+
'linkedTxbxSeq',
|
|
22789
|
+
'promptText',
|
|
22790
|
+
]);
|
|
22791
|
+
const COMPLEX_ELEMENT_FIELDS = {
|
|
22792
|
+
textStyle: '_ts',
|
|
22793
|
+
shapeStyle: '_ss',
|
|
22794
|
+
shapeAdjustments: '_sa',
|
|
22795
|
+
adjustmentHandles: '_ah',
|
|
22796
|
+
tableData: '_td',
|
|
22797
|
+
chartData: '_cd',
|
|
22798
|
+
smartArtData: '_smad',
|
|
22799
|
+
connectionStart: '_cs',
|
|
22800
|
+
connectionEnd: '_ce',
|
|
22801
|
+
animations: '_an',
|
|
22802
|
+
nativeAnimations: '_na',
|
|
22803
|
+
children: '_ch',
|
|
22804
|
+
paragraphIndents: '_pi',
|
|
22805
|
+
rawXml: '_rx',
|
|
22806
|
+
actionClick: '_ac',
|
|
22807
|
+
actionHover: '_av',
|
|
22808
|
+
locks: '_lk',
|
|
22809
|
+
imageEffects: '_ie',
|
|
22810
|
+
cropShape: '_cr',
|
|
22811
|
+
mediaBookmarks: '_mb',
|
|
22812
|
+
captionTracks: '_ct',
|
|
22813
|
+
};
|
|
22814
|
+
const REV_COMPLEX_ELEMENT = Object.fromEntries(Object.entries(COMPLEX_ELEMENT_FIELDS).map(([k, v]) => [v, k]));
|
|
22815
|
+
const SCALAR_SLIDE_KEYS = new Set([
|
|
22816
|
+
'id',
|
|
22817
|
+
'rId',
|
|
22818
|
+
'sourceSlideId',
|
|
22819
|
+
'layoutPath',
|
|
22820
|
+
'layoutName',
|
|
22821
|
+
'slideNumber',
|
|
22822
|
+
'hidden',
|
|
22823
|
+
'sectionName',
|
|
22824
|
+
'sectionId',
|
|
22825
|
+
'backgroundColor',
|
|
22826
|
+
'backgroundImage',
|
|
22827
|
+
'backgroundGradient',
|
|
22828
|
+
'notes',
|
|
22829
|
+
'backgroundShowAnimation',
|
|
22830
|
+
'showMasterShapes',
|
|
22831
|
+
'isDirty',
|
|
22832
|
+
]);
|
|
22833
|
+
const COMPLEX_SLIDE_FIELDS = {
|
|
22834
|
+
transition: '_tr',
|
|
22835
|
+
animations: '_an',
|
|
22836
|
+
nativeAnimations: '_na',
|
|
22837
|
+
rawTiming: '_rt',
|
|
22838
|
+
notesSegments: '_ns',
|
|
22839
|
+
comments: '_cm',
|
|
22840
|
+
warnings: '_wa',
|
|
22841
|
+
rawXml: '_rx',
|
|
22842
|
+
clrMapOverride: '_cm2',
|
|
22843
|
+
guides: '_gu',
|
|
22844
|
+
customerData: '_cu',
|
|
22845
|
+
activeXControls: '_ax',
|
|
22846
|
+
};
|
|
22847
|
+
const REV_COMPLEX_SLIDE = Object.fromEntries(Object.entries(COMPLEX_SLIDE_FIELDS).map(([k, v]) => [v, k]));
|
|
22794
22848
|
// ---------------------------------------------------------------------------
|
|
22795
22849
|
// Element serialization
|
|
22796
22850
|
// ---------------------------------------------------------------------------
|
|
@@ -22815,11 +22869,6 @@ function writeElementToYMap(element, ymap, factories) {
|
|
|
22815
22869
|
}
|
|
22816
22870
|
}
|
|
22817
22871
|
}
|
|
22818
|
-
function isYTextLike(value) {
|
|
22819
|
-
return (typeof value === 'object' &&
|
|
22820
|
-
value !== null &&
|
|
22821
|
-
typeof value.toDelta === 'function');
|
|
22822
|
-
}
|
|
22823
22872
|
function readElementFromYMap(ymap) {
|
|
22824
22873
|
const element = {};
|
|
22825
22874
|
ymap.forEach((value, key) => {
|
|
@@ -22897,6 +22946,11 @@ function readSlideFromYMap(ymap) {
|
|
|
22897
22946
|
// ---------------------------------------------------------------------------
|
|
22898
22947
|
// Y.Doc-level helpers
|
|
22899
22948
|
// ---------------------------------------------------------------------------
|
|
22949
|
+
/**
|
|
22950
|
+
* Replace the full slides array in the Y.Doc. Coarse: prefer
|
|
22951
|
+
* `reconcileSlidesInYDoc` for live editing; this remains suitable for
|
|
22952
|
+
* one-shot seeding of an empty document.
|
|
22953
|
+
*/
|
|
22900
22954
|
function writeSlidesToYDoc(slides, ydoc, factories, origin) {
|
|
22901
22955
|
ydoc.transact(() => {
|
|
22902
22956
|
const arr = ydoc.getArray(YDOC_SLIDES_KEY);
|
|
@@ -22918,12 +22972,208 @@ function readSlidesFromYDoc(ydoc) {
|
|
|
22918
22972
|
}
|
|
22919
22973
|
return slides;
|
|
22920
22974
|
}
|
|
22975
|
+
/**
|
|
22976
|
+
* Observe (deeply) the pptx:slides array. The handler receives the Yjs
|
|
22977
|
+
* events plus the transaction, so callers can skip their own writes by
|
|
22978
|
+
* checking `transaction.origin` (see LOCAL_SYNC_ORIGIN in
|
|
22979
|
+
* collaboration-reconcile.ts).
|
|
22980
|
+
*/
|
|
22921
22981
|
function observeYDocSlides(ydoc, onChange) {
|
|
22922
22982
|
const arr = ydoc.getArray(YDOC_SLIDES_KEY);
|
|
22923
22983
|
arr.observeDeep(onChange);
|
|
22924
22984
|
return () => arr.unobserveDeep(onChange);
|
|
22925
22985
|
}
|
|
22926
22986
|
|
|
22987
|
+
/**
|
|
22988
|
+
* collaboration-reconcile.ts: Granular Y.Doc reconciliation for collaborative
|
|
22989
|
+
* editing.
|
|
22990
|
+
*
|
|
22991
|
+
* `writeSlidesToYDoc` replaces the entire pptx:slides array on every write,
|
|
22992
|
+
* which makes concurrent edits collide at document granularity (last writer
|
|
22993
|
+
* wins for the whole deck). `reconcileSlidesInYDoc` instead diffs the desired
|
|
22994
|
+
* slide state against the live Y.Doc and only mutates what changed:
|
|
22995
|
+
*
|
|
22996
|
+
* - slides and elements are matched by `id`; unchanged ones keep their Y.Map
|
|
22997
|
+
* instance so concurrent field edits merge via Yjs
|
|
22998
|
+
* - scalar / complex fields are compared and only set when different
|
|
22999
|
+
* - textBody is only replaced when its canonical decoded form differs
|
|
23000
|
+
* - removed items are deleted, new ones inserted at their position; moves
|
|
23001
|
+
* are delete+reinsert (Yjs has no move primitive)
|
|
23002
|
+
*
|
|
23003
|
+
* All mutations run in a single transaction tagged with LOCAL_SYNC_ORIGIN (or
|
|
23004
|
+
* a caller-supplied origin) so observers can ignore their own writes.
|
|
23005
|
+
*/
|
|
23006
|
+
/** Transaction origin used for local reconcile writes. */
|
|
23007
|
+
const LOCAL_SYNC_ORIGIN = 'pptx-viewer:local-sync';
|
|
23008
|
+
function jsonEqual(a, b) {
|
|
23009
|
+
return a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
23010
|
+
}
|
|
23011
|
+
function reconcileScalars(ymap, rec, keys) {
|
|
23012
|
+
for (const key of keys) {
|
|
23013
|
+
const next = rec[key];
|
|
23014
|
+
const current = ymap.get(key);
|
|
23015
|
+
if (next === undefined) {
|
|
23016
|
+
if (current !== undefined) {
|
|
23017
|
+
ymap.delete(key);
|
|
23018
|
+
}
|
|
23019
|
+
}
|
|
23020
|
+
else if (!jsonEqual(current, next)) {
|
|
23021
|
+
ymap.set(key, next);
|
|
23022
|
+
}
|
|
23023
|
+
}
|
|
23024
|
+
}
|
|
23025
|
+
function reconcileComplexFields(ymap, rec, fields) {
|
|
23026
|
+
for (const [original, prefixed] of Object.entries(fields)) {
|
|
23027
|
+
const next = rec[original] === undefined ? undefined : JSON.stringify(rec[original]);
|
|
23028
|
+
const current = ymap.get(prefixed);
|
|
23029
|
+
if (next === undefined) {
|
|
23030
|
+
if (current !== undefined) {
|
|
23031
|
+
ymap.delete(prefixed);
|
|
23032
|
+
}
|
|
23033
|
+
}
|
|
23034
|
+
else if (current !== next) {
|
|
23035
|
+
ymap.set(prefixed, next);
|
|
23036
|
+
}
|
|
23037
|
+
}
|
|
23038
|
+
}
|
|
23039
|
+
function reconcileTextBody(ymap, rec, factories) {
|
|
23040
|
+
const segments = rec.textSegments;
|
|
23041
|
+
const current = ymap.get('textBody');
|
|
23042
|
+
if (!Array.isArray(segments)) {
|
|
23043
|
+
if (current !== undefined) {
|
|
23044
|
+
ymap.delete('textBody');
|
|
23045
|
+
}
|
|
23046
|
+
return;
|
|
23047
|
+
}
|
|
23048
|
+
const desired = decodeDelta(encodeSegmentsToDelta(segments));
|
|
23049
|
+
const existing = isYTextLike(current) ? decodeDelta(current.toDelta()) : undefined;
|
|
23050
|
+
if (existing !== undefined && jsonEqual(existing, desired)) {
|
|
23051
|
+
return;
|
|
23052
|
+
}
|
|
23053
|
+
const ytext = factories.createText();
|
|
23054
|
+
encodeTextBody(segments, ytext);
|
|
23055
|
+
ymap.set('textBody', ytext);
|
|
23056
|
+
}
|
|
23057
|
+
function reconcileElementYMap(ymap, element, factories) {
|
|
23058
|
+
const rec = element;
|
|
23059
|
+
reconcileScalars(ymap, rec, SCALAR_ELEMENT_KEYS);
|
|
23060
|
+
reconcileComplexFields(ymap, rec, COMPLEX_ELEMENT_FIELDS);
|
|
23061
|
+
reconcileTextBody(ymap, rec, factories);
|
|
23062
|
+
}
|
|
23063
|
+
function mapIdAt(arr, index) {
|
|
23064
|
+
const entry = arr.get(index);
|
|
23065
|
+
if (!entry || typeof entry.get !== 'function') {
|
|
23066
|
+
return undefined;
|
|
23067
|
+
}
|
|
23068
|
+
const id = entry.get('id');
|
|
23069
|
+
return typeof id === 'string' ? id : undefined;
|
|
23070
|
+
}
|
|
23071
|
+
/**
|
|
23072
|
+
* Reconcile a Y.Array of Y.Maps against a desired item list, matching by id.
|
|
23073
|
+
* Items without a string id fall back to positional matching.
|
|
23074
|
+
*/
|
|
23075
|
+
function reconcileYArrayById(arr, items, adapter) {
|
|
23076
|
+
const desiredIds = new Set();
|
|
23077
|
+
for (const item of items) {
|
|
23078
|
+
const id = adapter.idOf(item);
|
|
23079
|
+
if (typeof id === 'string') {
|
|
23080
|
+
desiredIds.add(id);
|
|
23081
|
+
}
|
|
23082
|
+
}
|
|
23083
|
+
// Pass 1: delete maps whose id is gone (or duplicated); keep the last dup.
|
|
23084
|
+
const seen = new Set();
|
|
23085
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
23086
|
+
const id = mapIdAt(arr, i);
|
|
23087
|
+
if (id === undefined) {
|
|
23088
|
+
continue;
|
|
23089
|
+
}
|
|
23090
|
+
if (!desiredIds.has(id) || seen.has(id)) {
|
|
23091
|
+
arr.delete(i, 1);
|
|
23092
|
+
}
|
|
23093
|
+
else {
|
|
23094
|
+
seen.add(id);
|
|
23095
|
+
}
|
|
23096
|
+
}
|
|
23097
|
+
// Pass 2: walk desired order; update in place, move, or insert.
|
|
23098
|
+
for (let pos = 0; pos < items.length; pos++) {
|
|
23099
|
+
const item = items[pos];
|
|
23100
|
+
const id = adapter.idOf(item);
|
|
23101
|
+
const idAtPos = pos < arr.length ? mapIdAt(arr, pos) : undefined;
|
|
23102
|
+
if (id === undefined) {
|
|
23103
|
+
// Positional fallback for id-less items.
|
|
23104
|
+
if (pos < arr.length && idAtPos === undefined) {
|
|
23105
|
+
adapter.update(arr.get(pos), item);
|
|
23106
|
+
}
|
|
23107
|
+
else {
|
|
23108
|
+
arr.insert(pos, [adapter.create(item)]);
|
|
23109
|
+
}
|
|
23110
|
+
continue;
|
|
23111
|
+
}
|
|
23112
|
+
if (idAtPos === id) {
|
|
23113
|
+
adapter.update(arr.get(pos), item);
|
|
23114
|
+
continue;
|
|
23115
|
+
}
|
|
23116
|
+
let foundAt = -1;
|
|
23117
|
+
for (let j = pos + 1; j < arr.length; j++) {
|
|
23118
|
+
if (mapIdAt(arr, j) === id) {
|
|
23119
|
+
foundAt = j;
|
|
23120
|
+
break;
|
|
23121
|
+
}
|
|
23122
|
+
}
|
|
23123
|
+
if (foundAt >= 0) {
|
|
23124
|
+
// Move: Yjs cannot re-insert an integrated type, so rebuild at pos.
|
|
23125
|
+
arr.delete(foundAt, 1);
|
|
23126
|
+
}
|
|
23127
|
+
arr.insert(pos, [adapter.create(item)]);
|
|
23128
|
+
}
|
|
23129
|
+
// Pass 3: trim trailing leftovers.
|
|
23130
|
+
if (arr.length > items.length) {
|
|
23131
|
+
arr.delete(items.length, arr.length - items.length);
|
|
23132
|
+
}
|
|
23133
|
+
}
|
|
23134
|
+
const isYArrayLike = (value) => typeof value === 'object' &&
|
|
23135
|
+
value !== null &&
|
|
23136
|
+
typeof value.insert === 'function' &&
|
|
23137
|
+
typeof value.toArray === 'function';
|
|
23138
|
+
function reconcileSlideYMap(ymap, slide, factories) {
|
|
23139
|
+
const rec = slide;
|
|
23140
|
+
reconcileScalars(ymap, rec, SCALAR_SLIDE_KEYS);
|
|
23141
|
+
reconcileComplexFields(ymap, rec, COMPLEX_SLIDE_FIELDS);
|
|
23142
|
+
let elements = ymap.get('elements');
|
|
23143
|
+
if (!isYArrayLike(elements)) {
|
|
23144
|
+
elements = factories.createArray();
|
|
23145
|
+
ymap.set('elements', elements);
|
|
23146
|
+
}
|
|
23147
|
+
reconcileYArrayById(elements, slide.elements, {
|
|
23148
|
+
idOf: (el) => (typeof el.id === 'string' ? el.id : undefined),
|
|
23149
|
+
create: (el) => {
|
|
23150
|
+
const map = factories.createMap();
|
|
23151
|
+
writeElementToYMap(el, map, factories);
|
|
23152
|
+
return map;
|
|
23153
|
+
},
|
|
23154
|
+
update: (map, el) => reconcileElementYMap(map, el, factories),
|
|
23155
|
+
});
|
|
23156
|
+
}
|
|
23157
|
+
/**
|
|
23158
|
+
* Granular local -> Y.Doc sync: mutate only what changed, inside one
|
|
23159
|
+
* transaction tagged with `origin` (default LOCAL_SYNC_ORIGIN) so the
|
|
23160
|
+
* caller's own observer can skip the resulting events.
|
|
23161
|
+
*/
|
|
23162
|
+
function reconcileSlidesInYDoc(slides, ydoc, factories, origin = LOCAL_SYNC_ORIGIN) {
|
|
23163
|
+
ydoc.transact(() => {
|
|
23164
|
+
const arr = ydoc.getArray(YDOC_SLIDES_KEY);
|
|
23165
|
+
reconcileYArrayById(arr, slides, {
|
|
23166
|
+
idOf: (slide) => (typeof slide.id === 'string' ? slide.id : undefined),
|
|
23167
|
+
create: (slide) => {
|
|
23168
|
+
const map = factories.createMap();
|
|
23169
|
+
writeSlideToYMap(slide, map, factories);
|
|
23170
|
+
return map;
|
|
23171
|
+
},
|
|
23172
|
+
update: (map, slide) => reconcileSlideYMap(map, slide, factories),
|
|
23173
|
+
});
|
|
23174
|
+
}, origin);
|
|
23175
|
+
}
|
|
23176
|
+
|
|
22927
23177
|
// ---------------------------------------------------------------------------
|
|
22928
23178
|
// Tuning constants (px tolerances for position / size changes)
|
|
22929
23179
|
// ---------------------------------------------------------------------------
|
|
@@ -28777,6 +29027,15 @@ function formatRelativeTime(ts) {
|
|
|
28777
29027
|
*/
|
|
28778
29028
|
/** Default y-websocket server URL used when no default is supplied. */
|
|
28779
29029
|
const DEFAULT_BROADCAST_SERVER_URL = 'ws://localhost:1234';
|
|
29030
|
+
/**
|
|
29031
|
+
* Resolve the transport implied by a server-URL form field: a blank server
|
|
29032
|
+
* URL selects the serverless y-webrtc transport (peers meet via WebRTC
|
|
29033
|
+
* signaling plus same-browser BroadcastChannel); anything else is a
|
|
29034
|
+
* y-websocket server URL.
|
|
29035
|
+
*/
|
|
29036
|
+
function resolveTransportForServerUrl(serverUrl) {
|
|
29037
|
+
return serverUrl.trim().length === 0 ? 'webrtc' : 'websocket';
|
|
29038
|
+
}
|
|
28780
29039
|
/** Generate a fresh, broadcast-scoped room id (`broadcast-<suffix>`). */
|
|
28781
29040
|
function generateBroadcastRoomId() {
|
|
28782
29041
|
const suffix = Math.random().toString(36).slice(2, 10);
|
|
@@ -28792,30 +29051,44 @@ function seedBroadcastFields(defaults) {
|
|
|
28792
29051
|
serverUrl: defaults?.serverUrl ?? DEFAULT_BROADCAST_SERVER_URL,
|
|
28793
29052
|
};
|
|
28794
29053
|
}
|
|
28795
|
-
/**
|
|
29054
|
+
/**
|
|
29055
|
+
* Whether the form can start: the room id is required; the server URL may be
|
|
29056
|
+
* blank, which selects the serverless webrtc transport.
|
|
29057
|
+
*/
|
|
28796
29058
|
function canStartBroadcast(fields) {
|
|
28797
|
-
return fields.roomId.trim().length > 0
|
|
29059
|
+
return fields.roomId.trim().length > 0;
|
|
28798
29060
|
}
|
|
28799
29061
|
/**
|
|
28800
29062
|
* Assemble a {@link BroadcastConfig} from the (trimmed) form fields, or `null`
|
|
28801
|
-
* when incomplete.
|
|
29063
|
+
* when incomplete. A blank server URL yields `transport: 'webrtc'`.
|
|
28802
29064
|
*/
|
|
28803
29065
|
function buildBroadcastConfig(fields) {
|
|
28804
29066
|
if (!canStartBroadcast(fields)) {
|
|
28805
29067
|
return null;
|
|
28806
29068
|
}
|
|
28807
|
-
|
|
29069
|
+
const serverUrl = fields.serverUrl.trim();
|
|
29070
|
+
return {
|
|
29071
|
+
roomId: fields.roomId.trim(),
|
|
29072
|
+
serverUrl,
|
|
29073
|
+
transport: resolveTransportForServerUrl(serverUrl),
|
|
29074
|
+
};
|
|
28808
29075
|
}
|
|
28809
29076
|
/**
|
|
28810
29077
|
* Build the shareable viewer follow-link for a broadcast. Returns just the
|
|
28811
29078
|
* room id when no `origin`/`pathname` are available (non-browser environments).
|
|
29079
|
+
* A blank server URL produces a `transport=webrtc` link instead of a
|
|
29080
|
+
* `server=` parameter.
|
|
28812
29081
|
*/
|
|
28813
29082
|
function buildBroadcastViewerUrl(roomId, serverUrl, location) {
|
|
28814
29083
|
if (!location) {
|
|
28815
29084
|
return roomId;
|
|
28816
29085
|
}
|
|
28817
29086
|
const room = encodeURIComponent(roomId);
|
|
28818
|
-
const
|
|
29087
|
+
const trimmed = serverUrl.trim();
|
|
29088
|
+
if (trimmed.length === 0) {
|
|
29089
|
+
return `${location.origin}${location.pathname}?broadcast=${room}&transport=webrtc`;
|
|
29090
|
+
}
|
|
29091
|
+
const server = encodeURIComponent(trimmed);
|
|
28819
29092
|
return `${location.origin}${location.pathname}?broadcast=${room}&server=${server}`;
|
|
28820
29093
|
}
|
|
28821
29094
|
/** Whether the runtime exposes a usable async clipboard write API. */
|
|
@@ -34830,6 +35103,9 @@ class BroadcastDialogComponent {
|
|
|
34830
35103
|
/** The shareable follow link (shown while `active`). */
|
|
34831
35104
|
viewerUrl = input(/* @ts-ignore */
|
|
34832
35105
|
...(ngDevMode ? [undefined, { debugName: "viewerUrl" }] : /* istanbul ignore next */ []));
|
|
35106
|
+
/** Whether the active broadcast is peer-to-peer (serverless webrtc). */
|
|
35107
|
+
p2p = input(false, /* @ts-ignore */
|
|
35108
|
+
...(ngDevMode ? [{ debugName: "p2p" }] : /* istanbul ignore next */ []));
|
|
34833
35109
|
/** Fired when the presenter starts a broadcast. */
|
|
34834
35110
|
start = output();
|
|
34835
35111
|
/** Fired when the presenter stops the active broadcast. */
|
|
@@ -34844,6 +35120,9 @@ class BroadcastDialogComponent {
|
|
|
34844
35120
|
...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
|
|
34845
35121
|
canStart = computed(() => canStartBroadcast({ roomId: this.roomId(), serverUrl: this.serverUrl() }), /* @ts-ignore */
|
|
34846
35122
|
...(ngDevMode ? [{ debugName: "canStart" }] : /* istanbul ignore next */ []));
|
|
35123
|
+
/** Blank server URL selects the serverless peer-to-peer (webrtc) transport. */
|
|
35124
|
+
isP2p = computed(() => this.serverUrl().trim().length === 0, /* @ts-ignore */
|
|
35125
|
+
...(ngDevMode ? [{ debugName: "isP2p" }] : /* istanbul ignore next */ []));
|
|
34847
35126
|
broadcastingTitle = translate('pptx.broadcast.broadcastingTitle');
|
|
34848
35127
|
startTitle = translate('pptx.broadcast.startTitle');
|
|
34849
35128
|
dialogTitle = computed(() => this.active() ? this.broadcastingTitle() : this.startTitle(), /* @ts-ignore */
|
|
@@ -34893,7 +35172,7 @@ class BroadcastDialogComponent {
|
|
|
34893
35172
|
});
|
|
34894
35173
|
}
|
|
34895
35174
|
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: `
|
|
35175
|
+
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
35176
|
<pptx-modal-dialog [open]="open()" [title]="dialogTitle()" (close)="onClose()">
|
|
34898
35177
|
@if (active()) {
|
|
34899
35178
|
<!-- Active: share the follow link + stop control -->
|
|
@@ -34913,6 +35192,10 @@ class BroadcastDialogComponent {
|
|
|
34913
35192
|
</span>
|
|
34914
35193
|
</div>
|
|
34915
35194
|
|
|
35195
|
+
@if (p2p()) {
|
|
35196
|
+
<p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pServerValue' | translate }}</p>
|
|
35197
|
+
}
|
|
35198
|
+
|
|
34916
35199
|
<p class="pptx-ng-broadcast-desc">
|
|
34917
35200
|
{{ 'pptx.broadcast.liveDesc' | translate }}
|
|
34918
35201
|
</p>
|
|
@@ -34979,6 +35262,9 @@ class BroadcastDialogComponent {
|
|
|
34979
35262
|
[value]="serverUrl()"
|
|
34980
35263
|
(input)="serverUrl.set(asValue($event))"
|
|
34981
35264
|
/>
|
|
35265
|
+
@if (isP2p()) {
|
|
35266
|
+
<p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pHint' | translate }}</p>
|
|
35267
|
+
}
|
|
34982
35268
|
</div>
|
|
34983
35269
|
</div>
|
|
34984
35270
|
}
|
|
@@ -35023,6 +35309,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
35023
35309
|
</span>
|
|
35024
35310
|
</div>
|
|
35025
35311
|
|
|
35312
|
+
@if (p2p()) {
|
|
35313
|
+
<p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pServerValue' | translate }}</p>
|
|
35314
|
+
}
|
|
35315
|
+
|
|
35026
35316
|
<p class="pptx-ng-broadcast-desc">
|
|
35027
35317
|
{{ 'pptx.broadcast.liveDesc' | translate }}
|
|
35028
35318
|
</p>
|
|
@@ -35089,6 +35379,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
35089
35379
|
[value]="serverUrl()"
|
|
35090
35380
|
(input)="serverUrl.set(asValue($event))"
|
|
35091
35381
|
/>
|
|
35382
|
+
@if (isP2p()) {
|
|
35383
|
+
<p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pHint' | translate }}</p>
|
|
35384
|
+
}
|
|
35092
35385
|
</div>
|
|
35093
35386
|
</div>
|
|
35094
35387
|
}
|
|
@@ -35110,7 +35403,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
35110
35403
|
</div>
|
|
35111
35404
|
</pptx-modal-dialog>
|
|
35112
35405
|
`, 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"] }] } });
|
|
35406
|
+
}], 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
35407
|
|
|
35115
35408
|
/**
|
|
35116
35409
|
* Thin re-export shim → vendored `pptx-viewer-shared`
|
|
@@ -35246,6 +35539,107 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
35246
35539
|
`, 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
35540
|
}], propDecorators: { cursors: [{ type: i0.Input, args: [{ isSignal: true, alias: "cursors", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }] } });
|
|
35248
35541
|
|
|
35542
|
+
/**
|
|
35543
|
+
* collaboration-local-presence.ts: publishes the local user's presence into a
|
|
35544
|
+
* single awareness `presence` field.
|
|
35545
|
+
*
|
|
35546
|
+
* Cursor, selection, and active-slide updates all merge into one record so a
|
|
35547
|
+
* later cursor move never clobbers the selection (or vice versa). The remote
|
|
35548
|
+
* side reads this shape via the shared `derivePresenceList`.
|
|
35549
|
+
*/
|
|
35550
|
+
class LocalPresencePublisher {
|
|
35551
|
+
awareness;
|
|
35552
|
+
identity;
|
|
35553
|
+
activeSlide = 0;
|
|
35554
|
+
cursor = { x: 0, y: 0 };
|
|
35555
|
+
selection;
|
|
35556
|
+
constructor(awareness, identity) {
|
|
35557
|
+
this.awareness = awareness;
|
|
35558
|
+
this.identity = identity;
|
|
35559
|
+
}
|
|
35560
|
+
/** Re-emit the merged presence record (also used as a heartbeat). */
|
|
35561
|
+
publish() {
|
|
35562
|
+
this.awareness.setLocalStateField('presence', {
|
|
35563
|
+
userName: this.identity.userName,
|
|
35564
|
+
userColor: this.identity.userColor,
|
|
35565
|
+
userAvatar: this.identity.userAvatar,
|
|
35566
|
+
role: this.identity.role,
|
|
35567
|
+
activeSlideIndex: this.activeSlide,
|
|
35568
|
+
cursorX: this.cursor.x,
|
|
35569
|
+
cursorY: this.cursor.y,
|
|
35570
|
+
selectedElementId: this.selection,
|
|
35571
|
+
lastUpdated: new Date().toISOString(),
|
|
35572
|
+
});
|
|
35573
|
+
}
|
|
35574
|
+
setCursor(x, y, activeSlideIndex = this.activeSlide) {
|
|
35575
|
+
this.cursor = { x, y };
|
|
35576
|
+
this.activeSlide = activeSlideIndex;
|
|
35577
|
+
// A bare `cursor` field mirrors the flat awareness shape some peers read.
|
|
35578
|
+
this.awareness.setLocalStateField('cursor', { x, y });
|
|
35579
|
+
this.publish();
|
|
35580
|
+
}
|
|
35581
|
+
setSelection(selectedElementId, activeSlideIndex = this.activeSlide) {
|
|
35582
|
+
this.selection = selectedElementId;
|
|
35583
|
+
this.activeSlide = activeSlideIndex;
|
|
35584
|
+
this.publish();
|
|
35585
|
+
}
|
|
35586
|
+
setActiveSlide(index) {
|
|
35587
|
+
this.activeSlide = Math.max(0, Math.floor(index));
|
|
35588
|
+
this.publish();
|
|
35589
|
+
}
|
|
35590
|
+
}
|
|
35591
|
+
|
|
35592
|
+
/**
|
|
35593
|
+
* collaboration-providers.ts: transport factories for the Angular
|
|
35594
|
+
* collaboration service.
|
|
35595
|
+
*
|
|
35596
|
+
* Isolates the dynamic `yjs` / `y-websocket` / `y-webrtc` imports and provider
|
|
35597
|
+
* construction so the service focuses on lifecycle + reactive state. The Yjs
|
|
35598
|
+
* packages are dynamically imported so they are fully tree-shaken when
|
|
35599
|
+
* collaboration is unused; each specifier is read from a variable so bundlers
|
|
35600
|
+
* do not eagerly follow it (mirrors the historical `/* @vite-ignore *\/`
|
|
35601
|
+
* pattern).
|
|
35602
|
+
*/
|
|
35603
|
+
async function createDoc() {
|
|
35604
|
+
const yModule = 'yjs';
|
|
35605
|
+
const Y = (await import(/* @vite-ignore */ yModule));
|
|
35606
|
+
const doc = new Y.Doc();
|
|
35607
|
+
const factories = {
|
|
35608
|
+
createMap: () => new Y.Map(),
|
|
35609
|
+
createArray: () => new Y.Array(),
|
|
35610
|
+
createText: () => new Y.Text(),
|
|
35611
|
+
};
|
|
35612
|
+
return { doc, factories, Y };
|
|
35613
|
+
}
|
|
35614
|
+
/** Create a y-websocket transport bundle for `config`. */
|
|
35615
|
+
async function createWebsocketBundle(config) {
|
|
35616
|
+
const wsSpecifier = 'y-websocket';
|
|
35617
|
+
const [{ doc, factories }, yws] = await Promise.all([
|
|
35618
|
+
createDoc(),
|
|
35619
|
+
import(/* @vite-ignore */ wsSpecifier),
|
|
35620
|
+
]);
|
|
35621
|
+
const provider = new yws.WebsocketProvider(config.serverUrl, config.roomId, doc, config.authToken ? { params: { token: config.authToken } } : undefined);
|
|
35622
|
+
return { doc, provider, awareness: provider.awareness, factories };
|
|
35623
|
+
}
|
|
35624
|
+
/**
|
|
35625
|
+
* Create a y-webrtc (peer-to-peer, serverless) transport bundle for `config`.
|
|
35626
|
+
* Peers rendezvous through the configured `signaling` servers (or y-webrtc's
|
|
35627
|
+
* public default) and same-browser tabs additionally sync via BroadcastChannel.
|
|
35628
|
+
* `authToken` is passed as the room `password`.
|
|
35629
|
+
*/
|
|
35630
|
+
async function createWebrtcBundle(config) {
|
|
35631
|
+
const rtcSpecifier = 'y-webrtc';
|
|
35632
|
+
const [{ doc, factories }, yrtc] = await Promise.all([
|
|
35633
|
+
createDoc(),
|
|
35634
|
+
import(/* @vite-ignore */ rtcSpecifier),
|
|
35635
|
+
]);
|
|
35636
|
+
const provider = new yrtc.WebrtcProvider(config.roomId, doc, {
|
|
35637
|
+
signaling: config.signaling?.length ? config.signaling : undefined,
|
|
35638
|
+
password: config.authToken || undefined,
|
|
35639
|
+
});
|
|
35640
|
+
return { doc, provider, awareness: provider.awareness, factories };
|
|
35641
|
+
}
|
|
35642
|
+
|
|
35249
35643
|
/**
|
|
35250
35644
|
* Split inherited template (master/layout) elements out of each loaded slide.
|
|
35251
35645
|
*
|
|
@@ -35309,31 +35703,110 @@ function showsTemplateAffordance(element, editTemplateMode) {
|
|
|
35309
35703
|
}
|
|
35310
35704
|
|
|
35311
35705
|
/**
|
|
35312
|
-
*
|
|
35706
|
+
* collaboration-writeback.ts: elected-writer serialization helper for the
|
|
35707
|
+
* Angular collaboration service.
|
|
35313
35708
|
*
|
|
35314
|
-
*
|
|
35315
|
-
*
|
|
35316
|
-
*
|
|
35317
|
-
*
|
|
35318
|
-
|
|
35319
|
-
|
|
35709
|
+
* When the local user is the session `owner`, the service debounces Y.Doc
|
|
35710
|
+
* changes and calls this to serialize the live document (templates re-merged)
|
|
35711
|
+
* into `.pptx` bytes for `config.onWriteBack`. Kept out of the service so the
|
|
35712
|
+
* service stays within the repo's per-file size budget.
|
|
35713
|
+
*/
|
|
35714
|
+
const WRITE_BACK_DEBOUNCE_MS = 5_000;
|
|
35715
|
+
/**
|
|
35716
|
+
* Serialize the current Y.Doc slide state (with the separated master/layout
|
|
35717
|
+
* template elements merged back) into `.pptx` bytes. `sourceBytes` seeds the
|
|
35718
|
+
* handler so package parts the CRDT does not carry (media, fonts, rels) survive.
|
|
35719
|
+
*/
|
|
35720
|
+
async function serializeWriteBack(ydoc, sourceBytes, templateElements) {
|
|
35721
|
+
const { PptxHandler } = await import('pptx-viewer-core');
|
|
35722
|
+
const handler = new PptxHandler();
|
|
35723
|
+
await handler.load(sourceBytes.buffer);
|
|
35724
|
+
const slides = buildSaveSlides(readSlidesFromYDoc(ydoc), templateElements);
|
|
35725
|
+
return handler.save(slides);
|
|
35726
|
+
}
|
|
35727
|
+
/**
|
|
35728
|
+
* Debounced elected-writer write-back. Only the session `owner` with an
|
|
35729
|
+
* `onWriteBack` callback schedules anything; each new change resets the timer,
|
|
35730
|
+
* and on fire it serializes the live Y.Doc and hands the bytes to the host.
|
|
35731
|
+
*/
|
|
35732
|
+
class WriteBackScheduler {
|
|
35733
|
+
timer = null;
|
|
35734
|
+
schedule(config, ydoc, getSourceBytes, getTemplateElements) {
|
|
35735
|
+
if (!config?.onWriteBack || config.role !== 'owner' || !ydoc) {
|
|
35736
|
+
return;
|
|
35737
|
+
}
|
|
35738
|
+
this.cancel();
|
|
35739
|
+
const ms = config.writeBackDebounceMs ?? WRITE_BACK_DEBOUNCE_MS;
|
|
35740
|
+
this.timer = setTimeout(() => {
|
|
35741
|
+
this.timer = null;
|
|
35742
|
+
const bytes = getSourceBytes?.();
|
|
35743
|
+
if (!bytes || !config.onWriteBack) {
|
|
35744
|
+
return;
|
|
35745
|
+
}
|
|
35746
|
+
void serializeWriteBack(ydoc, bytes, getTemplateElements?.() ?? {})
|
|
35747
|
+
.then((out) => config.onWriteBack?.(out))
|
|
35748
|
+
.catch(() => {
|
|
35749
|
+
/* non-fatal */
|
|
35750
|
+
});
|
|
35751
|
+
}, ms);
|
|
35752
|
+
}
|
|
35753
|
+
cancel() {
|
|
35754
|
+
if (this.timer !== null) {
|
|
35755
|
+
clearTimeout(this.timer);
|
|
35756
|
+
this.timer = null;
|
|
35757
|
+
}
|
|
35758
|
+
}
|
|
35759
|
+
}
|
|
35760
|
+
|
|
35761
|
+
/**
|
|
35762
|
+
* CollaborationService: Angular real-time collaboration (Yjs) service.
|
|
35763
|
+
*
|
|
35764
|
+
* Owns the provider lifecycle and the reactive collaboration state consumed by
|
|
35765
|
+
* the viewer component:
|
|
35766
|
+
* - Transport is `y-websocket` (default) or serverless `y-webrtc`
|
|
35767
|
+
* (`config.transport === 'webrtc'`), created via `./collaboration-providers`.
|
|
35768
|
+
* - Local edits sync via the granular `reconcileSlidesInYDoc` (only changed
|
|
35769
|
+
* slides/elements/fields mutate) in a transaction tagged `LOCAL_SYNC_ORIGIN`;
|
|
35770
|
+
* the remote observer skips its own local-origin writes.
|
|
35771
|
+
* - Websocket connections fail fast on mixed content and time out to `'error'`
|
|
35772
|
+
* after `CONNECTION_TIMEOUT_MS`; `retry()` reconnects with the last config.
|
|
35773
|
+
* - Elected-writer write-back (role === 'owner') via `WriteBackScheduler`.
|
|
35320
35774
|
*
|
|
35321
35775
|
* Provide at the component level: `@Component({ providers: [CollaborationService] })`.
|
|
35322
35776
|
*/
|
|
35323
35777
|
const DEFAULT_CANVAS_BOUND = 100_000;
|
|
35324
|
-
const WRITE_BACK_DEBOUNCE_MS = 5_000;
|
|
35325
35778
|
class CollaborationService {
|
|
35326
35779
|
// Reactive state
|
|
35327
|
-
|
|
35780
|
+
status = signal('disconnected', /* @ts-ignore */
|
|
35781
|
+
...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
35782
|
+
connected = computed(() => this.status() === 'connected', /* @ts-ignore */
|
|
35328
35783
|
...(ngDevMode ? [{ debugName: "connected" }] : /* istanbul ignore next */ []));
|
|
35329
35784
|
active = signal(false, /* @ts-ignore */
|
|
35330
35785
|
...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
35786
|
+
/** Role of the local user in the active session, or undefined when idle. */
|
|
35787
|
+
activeRole = signal(undefined, /* @ts-ignore */
|
|
35788
|
+
...(ngDevMode ? [{ debugName: "activeRole" }] : /* istanbul ignore next */ []));
|
|
35331
35789
|
presence = signal([], /* @ts-ignore */
|
|
35332
35790
|
...(ngDevMode ? [{ debugName: "presence" }] : /* istanbul ignore next */ []));
|
|
35333
35791
|
cursors = computed(() => presenceToCursors(this.presence()), /* @ts-ignore */
|
|
35334
35792
|
...(ngDevMode ? [{ debugName: "cursors" }] : /* istanbul ignore next */ []));
|
|
35335
35793
|
connectedCount = computed(() => this.presence().length + (this.active() ? 1 : 0), /* @ts-ignore */
|
|
35336
35794
|
...(ngDevMode ? [{ debugName: "connectedCount" }] : /* istanbul ignore next */ []));
|
|
35795
|
+
/** The client id the local user is currently following (null when free). */
|
|
35796
|
+
followedClientId = signal(null, /* @ts-ignore */
|
|
35797
|
+
...(ngDevMode ? [{ debugName: "followedClientId" }] : /* istanbul ignore next */ []));
|
|
35798
|
+
/** Active-slide index of the followed peer, or null when not following. */
|
|
35799
|
+
followedSlideIndex = computed(() => {
|
|
35800
|
+
const id = this.followedClientId();
|
|
35801
|
+
if (id === null) {
|
|
35802
|
+
return null;
|
|
35803
|
+
}
|
|
35804
|
+
return this.presence().find((p) => p.clientId === id)?.activeSlideIndex ?? null;
|
|
35805
|
+
}, /* @ts-ignore */
|
|
35806
|
+
...(ngDevMode ? [{ debugName: "followedSlideIndex" }] : /* istanbul ignore next */ []));
|
|
35807
|
+
/** Active-slide index of the first `owner` peer (the broadcaster), or null. */
|
|
35808
|
+
broadcasterSlideIndex = computed(() => this.presence().find((p) => p.role === 'owner')?.activeSlideIndex ?? null, /* @ts-ignore */
|
|
35809
|
+
...(ngDevMode ? [{ debugName: "broadcasterSlideIndex" }] : /* istanbul ignore next */ []));
|
|
35337
35810
|
// Internal handles
|
|
35338
35811
|
ydoc = null;
|
|
35339
35812
|
provider = null;
|
|
@@ -35342,18 +35815,18 @@ class CollaborationService {
|
|
|
35342
35815
|
applyingRemote = false;
|
|
35343
35816
|
yFactories = null;
|
|
35344
35817
|
lastSynced = '';
|
|
35345
|
-
|
|
35818
|
+
connectTimer = null;
|
|
35346
35819
|
unobserveSlides = null;
|
|
35820
|
+
writeBack = new WriteBackScheduler();
|
|
35347
35821
|
onRemoteSlides = null;
|
|
35348
35822
|
canvasWidth = DEFAULT_CANVAS_BOUND;
|
|
35349
35823
|
canvasHeight = DEFAULT_CANVAS_BOUND;
|
|
35350
35824
|
getSourceBytes = null;
|
|
35351
35825
|
getTemplateElements = null;
|
|
35352
|
-
userName = 'Anonymous';
|
|
35353
|
-
userColor = DEFAULT_CURSOR_COLOR;
|
|
35354
|
-
userAvatar;
|
|
35355
|
-
role;
|
|
35356
35826
|
currentConfig = null;
|
|
35827
|
+
lastConfig = null;
|
|
35828
|
+
lastOptions = {};
|
|
35829
|
+
localPresence = null;
|
|
35357
35830
|
refreshPresence = () => {
|
|
35358
35831
|
if (!this.awareness) {
|
|
35359
35832
|
this.presence.set([]);
|
|
@@ -35366,10 +35839,19 @@ class CollaborationService {
|
|
|
35366
35839
|
}
|
|
35367
35840
|
async connect(config, options = {}) {
|
|
35368
35841
|
this.disconnect();
|
|
35842
|
+
this.lastConfig = config;
|
|
35843
|
+
this.lastOptions = options;
|
|
35369
35844
|
try {
|
|
35370
35845
|
validateRoomId(config.roomId);
|
|
35371
35846
|
}
|
|
35372
35847
|
catch {
|
|
35848
|
+
this.status.set('error');
|
|
35849
|
+
return;
|
|
35850
|
+
}
|
|
35851
|
+
// Fail fast on mixed content (websocket only): an https page cannot open a
|
|
35852
|
+
// ws:// socket, so surface the error rather than hanging until the timeout.
|
|
35853
|
+
if (config.transport !== 'webrtc' && isMixedContentBlocked(config.serverUrl)) {
|
|
35854
|
+
this.status.set('error');
|
|
35373
35855
|
return;
|
|
35374
35856
|
}
|
|
35375
35857
|
this.onRemoteSlides = options.onRemoteSlides ?? null;
|
|
@@ -35377,60 +35859,104 @@ class CollaborationService {
|
|
|
35377
35859
|
this.canvasHeight = options.canvasHeight ?? DEFAULT_CANVAS_BOUND;
|
|
35378
35860
|
this.getSourceBytes = options.getSourceBytes ?? null;
|
|
35379
35861
|
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
35862
|
this.currentConfig = config;
|
|
35863
|
+
this.activeRole.set(config.role);
|
|
35864
|
+
this.status.set('connecting');
|
|
35385
35865
|
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;
|
|
35866
|
+
const bundle = config.transport === 'webrtc'
|
|
35867
|
+
? await createWebrtcBundle(config)
|
|
35868
|
+
: await createWebsocketBundle(config);
|
|
35869
|
+
this.ydoc = bundle.doc;
|
|
35870
|
+
this.yFactories = bundle.factories;
|
|
35871
|
+
this.provider = bundle.provider;
|
|
35872
|
+
this.awareness = bundle.awareness;
|
|
35401
35873
|
this.selfId = this.awareness.clientID ?? -1;
|
|
35402
|
-
this.
|
|
35874
|
+
this.localPresence = new LocalPresencePublisher(this.awareness, {
|
|
35875
|
+
userName: config.userName,
|
|
35876
|
+
userColor: config.userColor ?? DEFAULT_CURSOR_COLOR,
|
|
35877
|
+
userAvatar: config.userAvatar,
|
|
35878
|
+
role: config.role,
|
|
35879
|
+
});
|
|
35880
|
+
this.localPresence.publish();
|
|
35403
35881
|
this.awareness.on('change', this.refreshPresence);
|
|
35404
35882
|
this.awareness.on('update', this.refreshPresence);
|
|
35405
|
-
|
|
35406
|
-
|
|
35407
|
-
});
|
|
35408
|
-
// Observe remote slide changes
|
|
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
|
-
});
|
|
35883
|
+
this.wireStatus(config);
|
|
35884
|
+
this.unobserveSlides = observeYDocSlides(this.ydoc, (_events, transaction) => this.onRemoteChange(transaction));
|
|
35422
35885
|
this.active.set(true);
|
|
35423
35886
|
this.refreshPresence();
|
|
35424
35887
|
}
|
|
35425
35888
|
catch {
|
|
35426
35889
|
this.disconnect();
|
|
35890
|
+
this.status.set('error');
|
|
35427
35891
|
}
|
|
35428
35892
|
}
|
|
35429
|
-
|
|
35430
|
-
|
|
35431
|
-
|
|
35432
|
-
this.
|
|
35893
|
+
/** Reconnect using the configuration from the most recent {@link connect}. */
|
|
35894
|
+
async retry() {
|
|
35895
|
+
if (this.lastConfig) {
|
|
35896
|
+
await this.connect(this.lastConfig, this.lastOptions);
|
|
35897
|
+
}
|
|
35898
|
+
}
|
|
35899
|
+
/** Wire the provider status events + (websocket-only) connection timeout. */
|
|
35900
|
+
wireStatus(config) {
|
|
35901
|
+
const provider = this.provider;
|
|
35902
|
+
if (!provider) {
|
|
35903
|
+
return;
|
|
35433
35904
|
}
|
|
35905
|
+
if (config.transport === 'webrtc') {
|
|
35906
|
+
// P2P: no server round-trip to wait on. Treat "created" as connected,
|
|
35907
|
+
// and reflect explicit disconnect events.
|
|
35908
|
+
this.status.set('connected');
|
|
35909
|
+
provider.on('status', (payload) => {
|
|
35910
|
+
if (payload.connected === false && this.active()) {
|
|
35911
|
+
this.status.set('disconnected');
|
|
35912
|
+
}
|
|
35913
|
+
else if (payload.connected === true) {
|
|
35914
|
+
this.status.set('connected');
|
|
35915
|
+
}
|
|
35916
|
+
});
|
|
35917
|
+
return;
|
|
35918
|
+
}
|
|
35919
|
+
provider.on('status', (payload) => {
|
|
35920
|
+
if (payload.status === 'connected') {
|
|
35921
|
+
this.clearConnectTimer();
|
|
35922
|
+
this.status.set('connected');
|
|
35923
|
+
}
|
|
35924
|
+
else if (payload.status === 'disconnected' && this.active()) {
|
|
35925
|
+
this.status.set('disconnected');
|
|
35926
|
+
}
|
|
35927
|
+
});
|
|
35928
|
+
if (provider.wsconnected) {
|
|
35929
|
+
this.status.set('connected');
|
|
35930
|
+
return;
|
|
35931
|
+
}
|
|
35932
|
+
this.connectTimer = setTimeout(() => {
|
|
35933
|
+
this.connectTimer = null;
|
|
35934
|
+
if (this.status() !== 'connected') {
|
|
35935
|
+
this.disconnect();
|
|
35936
|
+
this.status.set('error');
|
|
35937
|
+
}
|
|
35938
|
+
}, CONNECTION_TIMEOUT_MS);
|
|
35939
|
+
}
|
|
35940
|
+
/** Handle a remote Y.Doc change, skipping our own local-origin transactions. */
|
|
35941
|
+
onRemoteChange(transaction) {
|
|
35942
|
+
if (transaction?.origin === LOCAL_SYNC_ORIGIN || this.applyingRemote || !this.ydoc) {
|
|
35943
|
+
return;
|
|
35944
|
+
}
|
|
35945
|
+
const remote = readSlidesFromYDoc(this.ydoc);
|
|
35946
|
+
if (remote.length === 0) {
|
|
35947
|
+
return;
|
|
35948
|
+
}
|
|
35949
|
+
// Suppress the echo: record what we just applied so the subsequent local
|
|
35950
|
+
// broadcast (driven by the editor signal) is a no-op.
|
|
35951
|
+
this.lastSynced = JSON.stringify(remote);
|
|
35952
|
+
this.applyingRemote = true;
|
|
35953
|
+
this.onRemoteSlides?.(remote);
|
|
35954
|
+
this.applyingRemote = false;
|
|
35955
|
+
this.scheduleWriteBack();
|
|
35956
|
+
}
|
|
35957
|
+
disconnect() {
|
|
35958
|
+
this.clearConnectTimer();
|
|
35959
|
+
this.writeBack.cancel();
|
|
35434
35960
|
this.unobserveSlides?.();
|
|
35435
35961
|
this.unobserveSlides = null;
|
|
35436
35962
|
this.awareness?.off?.('change', this.refreshPresence);
|
|
@@ -35441,19 +35967,36 @@ class CollaborationService {
|
|
|
35441
35967
|
this.provider = null;
|
|
35442
35968
|
this.ydoc = null;
|
|
35443
35969
|
this.awareness = null;
|
|
35970
|
+
this.localPresence = null;
|
|
35444
35971
|
this.selfId = -1;
|
|
35445
35972
|
this.applyingRemote = false;
|
|
35446
35973
|
this.yFactories = null;
|
|
35447
35974
|
this.lastSynced = '';
|
|
35448
35975
|
this.onRemoteSlides = null;
|
|
35449
35976
|
this.currentConfig = null;
|
|
35450
|
-
this.
|
|
35977
|
+
this.status.set('disconnected');
|
|
35451
35978
|
this.active.set(false);
|
|
35979
|
+
this.activeRole.set(undefined);
|
|
35452
35980
|
this.presence.set([]);
|
|
35981
|
+
this.followedClientId.set(null);
|
|
35982
|
+
}
|
|
35983
|
+
/**
|
|
35984
|
+
* Broadcast the local slide set to peers, reconciling only what changed into
|
|
35985
|
+
* the pptx:slides Y.Array. An empty deck is never written (so a late-joiner
|
|
35986
|
+
* that has not yet received the doc cannot clobber it), and an unchanged deck
|
|
35987
|
+
* is skipped.
|
|
35988
|
+
*/
|
|
35989
|
+
/**
|
|
35990
|
+
* Record the current local deck as the sync baseline so the first (unchanged)
|
|
35991
|
+
* broadcast after connecting is suppressed. Call right after {@link connect}
|
|
35992
|
+
* for a joiner whose local deck is a placeholder awaiting remote sync, so it
|
|
35993
|
+
* never overwrites the shared document before receiving it.
|
|
35994
|
+
*/
|
|
35995
|
+
seedBaseline(slides) {
|
|
35996
|
+
this.lastSynced = JSON.stringify(slides);
|
|
35453
35997
|
}
|
|
35454
|
-
/** Broadcast the local slide set to peers via the pptx:slides Y.Array. */
|
|
35455
35998
|
broadcastSlides(slides) {
|
|
35456
|
-
if (!this.ydoc || !this.yFactories || this.applyingRemote) {
|
|
35999
|
+
if (!this.ydoc || !this.yFactories || this.applyingRemote || slides.length === 0) {
|
|
35457
36000
|
return;
|
|
35458
36001
|
}
|
|
35459
36002
|
const s = JSON.stringify(slides);
|
|
@@ -35461,68 +36004,31 @@ class CollaborationService {
|
|
|
35461
36004
|
return;
|
|
35462
36005
|
}
|
|
35463
36006
|
this.lastSynced = s;
|
|
35464
|
-
|
|
35465
|
-
this.
|
|
36007
|
+
reconcileSlidesInYDoc([...slides], this.ydoc, this.yFactories, LOCAL_SYNC_ORIGIN);
|
|
36008
|
+
this.scheduleWriteBack();
|
|
35466
36009
|
}
|
|
35467
|
-
setCursor(x, y, activeSlideIndex
|
|
35468
|
-
|
|
35469
|
-
return;
|
|
35470
|
-
}
|
|
35471
|
-
this.awareness.setLocalStateField('cursor', { x, y });
|
|
35472
|
-
this._publishAwareness(activeSlideIndex, x, y);
|
|
36010
|
+
setCursor(x, y, activeSlideIndex) {
|
|
36011
|
+
this.localPresence?.setCursor(x, y, activeSlideIndex);
|
|
35473
36012
|
}
|
|
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 });
|
|
36013
|
+
setSelection(selectedElementId, activeSlideIndex) {
|
|
36014
|
+
this.localPresence?.setSelection(selectedElementId, activeSlideIndex);
|
|
35493
36015
|
}
|
|
35494
|
-
|
|
35495
|
-
|
|
35496
|
-
|
|
35497
|
-
|
|
35498
|
-
|
|
35499
|
-
|
|
35500
|
-
|
|
36016
|
+
/** Publish the local active-slide index (drives follow-along). */
|
|
36017
|
+
setActiveSlide(index) {
|
|
36018
|
+
this.localPresence?.setActiveSlide(index);
|
|
36019
|
+
}
|
|
36020
|
+
/** Follow the given peer's active slide, or `null` to stop following. */
|
|
36021
|
+
followUser(clientId) {
|
|
36022
|
+
this.followedClientId.set(clientId);
|
|
36023
|
+
}
|
|
36024
|
+
clearConnectTimer() {
|
|
36025
|
+
if (this.connectTimer !== null) {
|
|
36026
|
+
clearTimeout(this.connectTimer);
|
|
36027
|
+
this.connectTimer = null;
|
|
35501
36028
|
}
|
|
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);
|
|
36029
|
+
}
|
|
36030
|
+
scheduleWriteBack() {
|
|
36031
|
+
this.writeBack.schedule(this.currentConfig, this.ydoc, this.getSourceBytes, this.getTemplateElements);
|
|
35526
36032
|
}
|
|
35527
36033
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CollaborationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
35528
36034
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CollaborationService });
|
|
@@ -36388,6 +36894,16 @@ class EditorStateService {
|
|
|
36388
36894
|
this.dirty.set(true);
|
|
36389
36895
|
this.syncHistory();
|
|
36390
36896
|
}
|
|
36897
|
+
/**
|
|
36898
|
+
* Apply a remote collaborator's slide set (already template-free, as broadcast
|
|
36899
|
+
* over the CRDT) to the editable deck. Selection, history, and this peer's own
|
|
36900
|
+
* separated template store are left untouched; remote edits are not local undo
|
|
36901
|
+
* steps.
|
|
36902
|
+
*/
|
|
36903
|
+
applyRemoteSlides(slides) {
|
|
36904
|
+
this.slides.set(this.clone(slides));
|
|
36905
|
+
this.dirty.set(true);
|
|
36906
|
+
}
|
|
36391
36907
|
// ── Snapshot (deck + template store) ─────────────────────────────────────
|
|
36392
36908
|
/** Capture the current deck + template store as one undo/redo snapshot. */
|
|
36393
36909
|
captureSnapshot() {
|
|
@@ -39728,6 +40244,145 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
39728
40244
|
args: ['document:keydown', ['$event']]
|
|
39729
40245
|
}] } });
|
|
39730
40246
|
|
|
40247
|
+
/**
|
|
40248
|
+
* follow-mode-bar.component.ts: Angular port of the Vue `FollowModeBar.vue`.
|
|
40249
|
+
*
|
|
40250
|
+
* Selector: `pptx-follow-mode-bar`
|
|
40251
|
+
*
|
|
40252
|
+
* Lists the active remote peers and lets the local user follow one of them
|
|
40253
|
+
* (mirroring that peer's active slide) or stop following. Purely presentational:
|
|
40254
|
+
* the host supplies the reactive presence list and the currently-followed
|
|
40255
|
+
* clientId (from `CollaborationService`) and reacts to the `follow` event to
|
|
40256
|
+
* drive `followUser(clientId | null)`. Each peer chip shows an initials avatar
|
|
40257
|
+
* in the peer's colour; the followed peer is highlighted with a "Stop" affordance.
|
|
40258
|
+
*/
|
|
40259
|
+
class FollowModeBarComponent {
|
|
40260
|
+
/** Active remote collaborators (excludes self). */
|
|
40261
|
+
presences = input([], /* @ts-ignore */
|
|
40262
|
+
...(ngDevMode ? [{ debugName: "presences" }] : /* istanbul ignore next */ []));
|
|
40263
|
+
/** The clientId currently being followed, or null. */
|
|
40264
|
+
followedClientId = input(null, /* @ts-ignore */
|
|
40265
|
+
...(ngDevMode ? [{ debugName: "followedClientId" }] : /* istanbul ignore next */ []));
|
|
40266
|
+
/** Follow the given peer, or `null` to stop following. */
|
|
40267
|
+
follow = output();
|
|
40268
|
+
chips = computed(() => {
|
|
40269
|
+
const followed = this.followedClientId();
|
|
40270
|
+
return this.presences().map((peer) => ({
|
|
40271
|
+
clientId: peer.clientId,
|
|
40272
|
+
userName: peer.userName,
|
|
40273
|
+
color: peer.userColor,
|
|
40274
|
+
initials: initialsOf(peer.userName),
|
|
40275
|
+
following: peer.clientId === followed,
|
|
40276
|
+
}));
|
|
40277
|
+
}, /* @ts-ignore */
|
|
40278
|
+
...(ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []));
|
|
40279
|
+
followedName = computed(() => {
|
|
40280
|
+
const id = this.followedClientId();
|
|
40281
|
+
if (id === null) {
|
|
40282
|
+
return null;
|
|
40283
|
+
}
|
|
40284
|
+
return this.presences().find((p) => p.clientId === id)?.userName ?? null;
|
|
40285
|
+
}, /* @ts-ignore */
|
|
40286
|
+
...(ngDevMode ? [{ debugName: "followedName" }] : /* istanbul ignore next */ []));
|
|
40287
|
+
toggle(clientId) {
|
|
40288
|
+
this.follow.emit(this.followedClientId() === clientId ? null : clientId);
|
|
40289
|
+
}
|
|
40290
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: FollowModeBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
40291
|
+
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: `
|
|
40292
|
+
@if (chips().length > 0) {
|
|
40293
|
+
<div class="pptx-ng-follow-bar" data-export-ignore="true">
|
|
40294
|
+
<span class="pptx-ng-follow-status">
|
|
40295
|
+
@if (followedName(); as name) {
|
|
40296
|
+
{{ 'pptx.followMode.following' | translate }}
|
|
40297
|
+
<strong>{{ name }}</strong>
|
|
40298
|
+
<button
|
|
40299
|
+
type="button"
|
|
40300
|
+
class="pptx-ng-follow-stop"
|
|
40301
|
+
[title]="'pptx.followMode.stopFollowing' | translate"
|
|
40302
|
+
(click)="follow.emit(null)"
|
|
40303
|
+
>
|
|
40304
|
+
{{ 'pptx.followMode.stop' | translate }}
|
|
40305
|
+
</button>
|
|
40306
|
+
} @else {
|
|
40307
|
+
{{ 'pptx.followMode.followCollaborator' | translate }}
|
|
40308
|
+
}
|
|
40309
|
+
</span>
|
|
40310
|
+
<ul class="pptx-ng-follow-list">
|
|
40311
|
+
@for (peer of chips(); track peer.clientId) {
|
|
40312
|
+
<li>
|
|
40313
|
+
<button
|
|
40314
|
+
type="button"
|
|
40315
|
+
class="pptx-ng-follow-peer"
|
|
40316
|
+
[class.is-following]="peer.following"
|
|
40317
|
+
[attr.data-client-id]="peer.clientId"
|
|
40318
|
+
[attr.aria-pressed]="peer.following"
|
|
40319
|
+
(click)="toggle(peer.clientId)"
|
|
40320
|
+
>
|
|
40321
|
+
<span class="pptx-ng-follow-avatar" [style.background-color]="peer.color">
|
|
40322
|
+
{{ peer.initials }}
|
|
40323
|
+
</span>
|
|
40324
|
+
<span class="pptx-ng-follow-name">{{ peer.userName }}</span>
|
|
40325
|
+
</button>
|
|
40326
|
+
</li>
|
|
40327
|
+
}
|
|
40328
|
+
</ul>
|
|
40329
|
+
</div>
|
|
40330
|
+
}
|
|
40331
|
+
`, 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 });
|
|
40332
|
+
}
|
|
40333
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: FollowModeBarComponent, decorators: [{
|
|
40334
|
+
type: Component,
|
|
40335
|
+
args: [{ selector: 'pptx-follow-mode-bar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [TranslatePipe], template: `
|
|
40336
|
+
@if (chips().length > 0) {
|
|
40337
|
+
<div class="pptx-ng-follow-bar" data-export-ignore="true">
|
|
40338
|
+
<span class="pptx-ng-follow-status">
|
|
40339
|
+
@if (followedName(); as name) {
|
|
40340
|
+
{{ 'pptx.followMode.following' | translate }}
|
|
40341
|
+
<strong>{{ name }}</strong>
|
|
40342
|
+
<button
|
|
40343
|
+
type="button"
|
|
40344
|
+
class="pptx-ng-follow-stop"
|
|
40345
|
+
[title]="'pptx.followMode.stopFollowing' | translate"
|
|
40346
|
+
(click)="follow.emit(null)"
|
|
40347
|
+
>
|
|
40348
|
+
{{ 'pptx.followMode.stop' | translate }}
|
|
40349
|
+
</button>
|
|
40350
|
+
} @else {
|
|
40351
|
+
{{ 'pptx.followMode.followCollaborator' | translate }}
|
|
40352
|
+
}
|
|
40353
|
+
</span>
|
|
40354
|
+
<ul class="pptx-ng-follow-list">
|
|
40355
|
+
@for (peer of chips(); track peer.clientId) {
|
|
40356
|
+
<li>
|
|
40357
|
+
<button
|
|
40358
|
+
type="button"
|
|
40359
|
+
class="pptx-ng-follow-peer"
|
|
40360
|
+
[class.is-following]="peer.following"
|
|
40361
|
+
[attr.data-client-id]="peer.clientId"
|
|
40362
|
+
[attr.aria-pressed]="peer.following"
|
|
40363
|
+
(click)="toggle(peer.clientId)"
|
|
40364
|
+
>
|
|
40365
|
+
<span class="pptx-ng-follow-avatar" [style.background-color]="peer.color">
|
|
40366
|
+
{{ peer.initials }}
|
|
40367
|
+
</span>
|
|
40368
|
+
<span class="pptx-ng-follow-name">{{ peer.userName }}</span>
|
|
40369
|
+
</button>
|
|
40370
|
+
</li>
|
|
40371
|
+
}
|
|
40372
|
+
</ul>
|
|
40373
|
+
</div>
|
|
40374
|
+
}
|
|
40375
|
+
`, 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"] }]
|
|
40376
|
+
}], 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"] }] } });
|
|
40377
|
+
/** First-letter / two-char initials for the avatar chip. */
|
|
40378
|
+
function initialsOf(name) {
|
|
40379
|
+
const parts = name.trim().split(/\s+/u);
|
|
40380
|
+
if (parts.length >= 2 && parts[0] && parts[parts.length - 1]) {
|
|
40381
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
40382
|
+
}
|
|
40383
|
+
return name.slice(0, 2).toUpperCase() || '?';
|
|
40384
|
+
}
|
|
40385
|
+
|
|
39731
40386
|
/**
|
|
39732
40387
|
* hyperlink-dialog.component.ts: Set or clear an element's click hyperlink.
|
|
39733
40388
|
*
|
|
@@ -64622,6 +65277,112 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
64622
65277
|
`, 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
65278
|
}], 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
65279
|
|
|
65280
|
+
/**
|
|
65281
|
+
* remote-selection-overlay.component.ts: Angular port of the Vue
|
|
65282
|
+
* `RemoteSelectionOverlay.vue` / React `RemoteSelectionOverlay.tsx`.
|
|
65283
|
+
*
|
|
65284
|
+
* Selector: `pptx-remote-selection-overlay`
|
|
65285
|
+
*
|
|
65286
|
+
* Draws a coloured rectangle around each element a remote collaborator has
|
|
65287
|
+
* selected, labelled with that peer's name in their colour (Google-Slides-style
|
|
65288
|
+
* presence). Purely presentational: the host supplies the reactive presence
|
|
65289
|
+
* list (from `CollaborationService.presence`), the elements on the active slide,
|
|
65290
|
+
* the active slide index, and the current `zoom`. Only peers whose
|
|
65291
|
+
* `activeSlideIndex` matches are drawn, and only for a selected id that resolves
|
|
65292
|
+
* to an element on the slide.
|
|
65293
|
+
*
|
|
65294
|
+
* Element geometry is in unscaled slide coordinates (px); this component
|
|
65295
|
+
* multiplies by `zoom` so it can mount inside the scaled slide stage. It sets
|
|
65296
|
+
* `pointer-events: none` so it never intercepts canvas input.
|
|
65297
|
+
*/
|
|
65298
|
+
class RemoteSelectionOverlayComponent {
|
|
65299
|
+
/** Remote collaborators' presence (cursor + selection + active slide). */
|
|
65300
|
+
presences = input([], /* @ts-ignore */
|
|
65301
|
+
...(ngDevMode ? [{ debugName: "presences" }] : /* istanbul ignore next */ []));
|
|
65302
|
+
/** Elements on the active slide (used to resolve selected ids -> geometry). */
|
|
65303
|
+
elements = input([], /* @ts-ignore */
|
|
65304
|
+
...(ngDevMode ? [{ debugName: "elements" }] : /* istanbul ignore next */ []));
|
|
65305
|
+
/** The current slide index: only peers on this slide are drawn. */
|
|
65306
|
+
activeSlideIndex = input(0, /* @ts-ignore */
|
|
65307
|
+
...(ngDevMode ? [{ debugName: "activeSlideIndex" }] : /* istanbul ignore next */ []));
|
|
65308
|
+
/** Current canvas zoom factor; geometry scales by this. */
|
|
65309
|
+
zoom = input(1, /* @ts-ignore */
|
|
65310
|
+
...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
65311
|
+
elementMap = computed(() => {
|
|
65312
|
+
const map = new Map();
|
|
65313
|
+
for (const el of this.elements()) {
|
|
65314
|
+
map.set(el.id, el);
|
|
65315
|
+
}
|
|
65316
|
+
return map;
|
|
65317
|
+
}, /* @ts-ignore */
|
|
65318
|
+
...(ngDevMode ? [{ debugName: "elementMap" }] : /* istanbul ignore next */ []));
|
|
65319
|
+
boxes = computed(() => {
|
|
65320
|
+
const slide = this.activeSlideIndex();
|
|
65321
|
+
const z = this.zoom();
|
|
65322
|
+
const lookup = this.elementMap();
|
|
65323
|
+
const result = [];
|
|
65324
|
+
for (const peer of this.presences()) {
|
|
65325
|
+
if (peer.activeSlideIndex !== slide || !peer.selectedElementId) {
|
|
65326
|
+
continue;
|
|
65327
|
+
}
|
|
65328
|
+
const el = lookup.get(peer.selectedElementId);
|
|
65329
|
+
if (!el) {
|
|
65330
|
+
continue;
|
|
65331
|
+
}
|
|
65332
|
+
result.push({
|
|
65333
|
+
key: `${peer.clientId}-${peer.selectedElementId}`,
|
|
65334
|
+
label: formatCursorLabel(peer.userName, MAX_LABEL_CHARS),
|
|
65335
|
+
color: peer.userColor,
|
|
65336
|
+
transform: `translate(${el.x * z}px, ${el.y * z}px)`,
|
|
65337
|
+
width: el.width * z,
|
|
65338
|
+
height: el.height * z,
|
|
65339
|
+
});
|
|
65340
|
+
}
|
|
65341
|
+
return result;
|
|
65342
|
+
}, /* @ts-ignore */
|
|
65343
|
+
...(ngDevMode ? [{ debugName: "boxes" }] : /* istanbul ignore next */ []));
|
|
65344
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RemoteSelectionOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
65345
|
+
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: `
|
|
65346
|
+
<div aria-hidden="true" data-export-ignore="true">
|
|
65347
|
+
@for (box of boxes(); track box.key) {
|
|
65348
|
+
<div
|
|
65349
|
+
class="pptx-ng-remote-selection"
|
|
65350
|
+
[attr.data-element-id]="box.key"
|
|
65351
|
+
[style.transform]="box.transform"
|
|
65352
|
+
[style.width.px]="box.width"
|
|
65353
|
+
[style.height.px]="box.height"
|
|
65354
|
+
[style.color]="box.color"
|
|
65355
|
+
>
|
|
65356
|
+
<span class="pptx-ng-remote-selection-label" [style.background-color]="box.color">
|
|
65357
|
+
{{ box.label }}
|
|
65358
|
+
</span>
|
|
65359
|
+
</div>
|
|
65360
|
+
}
|
|
65361
|
+
</div>
|
|
65362
|
+
`, 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 });
|
|
65363
|
+
}
|
|
65364
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RemoteSelectionOverlayComponent, decorators: [{
|
|
65365
|
+
type: Component,
|
|
65366
|
+
args: [{ selector: 'pptx-remote-selection-overlay', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
65367
|
+
<div aria-hidden="true" data-export-ignore="true">
|
|
65368
|
+
@for (box of boxes(); track box.key) {
|
|
65369
|
+
<div
|
|
65370
|
+
class="pptx-ng-remote-selection"
|
|
65371
|
+
[attr.data-element-id]="box.key"
|
|
65372
|
+
[style.transform]="box.transform"
|
|
65373
|
+
[style.width.px]="box.width"
|
|
65374
|
+
[style.height.px]="box.height"
|
|
65375
|
+
[style.color]="box.color"
|
|
65376
|
+
>
|
|
65377
|
+
<span class="pptx-ng-remote-selection-label" [style.background-color]="box.color">
|
|
65378
|
+
{{ box.label }}
|
|
65379
|
+
</span>
|
|
65380
|
+
</div>
|
|
65381
|
+
}
|
|
65382
|
+
</div>
|
|
65383
|
+
`, 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"] }]
|
|
65384
|
+
}], 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 }] }] } });
|
|
65385
|
+
|
|
64625
65386
|
/**
|
|
64626
65387
|
* ribbon-animations-section.component.ts: the Animations ribbon tab (preview, the
|
|
64627
65388
|
* Add Animation entrance/emphasis/exit gallery, Remove Animation, Animation
|
|
@@ -66140,7 +66901,7 @@ class RibbonFontControlsComponent {
|
|
|
66140
66901
|
patchTextStyle(this.editor, this.slideIndex(), this.selectedElement(), patch);
|
|
66141
66902
|
}
|
|
66142
66903
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RibbonFontControlsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
66143
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: RibbonFontControlsComponent, isStandalone: true, selector: "pptx-ribbon-font-controls", inputs: { slideIndex: { classPropertyName: "slideIndex", publicName: "slideIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedElement: { classPropertyName: "selectedElement", publicName: "selectedElement", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
66904
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: RibbonFontControlsComponent, isStandalone: true, selector: "pptx-ribbon-font-controls", inputs: { slideIndex: { classPropertyName: "slideIndex", publicName: "slideIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedElement: { classPropertyName: "selectedElement", publicName: "selectedElement", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
66144
66905
|
<div class="flex items-center gap-1">
|
|
66145
66906
|
<select
|
|
66146
66907
|
class="pptx-rb-select w-28"
|
|
@@ -66274,6 +67035,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
66274
67035
|
selector: 'pptx-ribbon-font-controls',
|
|
66275
67036
|
standalone: true,
|
|
66276
67037
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
67038
|
+
host: { class: 'contents' },
|
|
66277
67039
|
imports: [NgClass, TranslatePipe, RibbonColorPopoverComponent],
|
|
66278
67040
|
template: `
|
|
66279
67041
|
<div class="flex items-center gap-1">
|
|
@@ -66747,10 +67509,12 @@ class RibbonHomeSectionComponent {
|
|
|
66747
67509
|
<span class="pptx-rb-sep"></span>
|
|
66748
67510
|
<!-- Paragraph -->
|
|
66749
67511
|
<div class="flex flex-col items-center gap-0.5">
|
|
66750
|
-
<
|
|
66751
|
-
|
|
66752
|
-
|
|
66753
|
-
|
|
67512
|
+
<div class="flex items-center gap-1">
|
|
67513
|
+
<pptx-ribbon-paragraph-controls
|
|
67514
|
+
[slideIndex]="slideIndex()"
|
|
67515
|
+
[selectedElement]="selectedElement()"
|
|
67516
|
+
/>
|
|
67517
|
+
</div>
|
|
66754
67518
|
<span class="text-[9px] leading-none text-muted-foreground">
|
|
66755
67519
|
{{ 'pptx.ribbon.paragraph' | translate }}
|
|
66756
67520
|
</span>
|
|
@@ -66853,10 +67617,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
66853
67617
|
<span class="pptx-rb-sep"></span>
|
|
66854
67618
|
<!-- Paragraph -->
|
|
66855
67619
|
<div class="flex flex-col items-center gap-0.5">
|
|
66856
|
-
<
|
|
66857
|
-
|
|
66858
|
-
|
|
66859
|
-
|
|
67620
|
+
<div class="flex items-center gap-1">
|
|
67621
|
+
<pptx-ribbon-paragraph-controls
|
|
67622
|
+
[slideIndex]="slideIndex()"
|
|
67623
|
+
[selectedElement]="selectedElement()"
|
|
67624
|
+
/>
|
|
67625
|
+
</div>
|
|
66860
67626
|
<span class="text-[9px] leading-none text-muted-foreground">
|
|
66861
67627
|
{{ 'pptx.ribbon.paragraph' | translate }}
|
|
66862
67628
|
</span>
|
|
@@ -67023,7 +67789,7 @@ class RibbonInsertFieldsComponent {
|
|
|
67023
67789
|
return this.previewDate().toLocaleString();
|
|
67024
67790
|
}
|
|
67025
67791
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RibbonInsertFieldsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
67026
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: RibbonInsertFieldsComponent, isStandalone: true, selector: "pptx-ribbon-insert-fields", inputs: { slideIndex: { classPropertyName: "slideIndex", publicName: "slideIndex", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
67792
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: RibbonInsertFieldsComponent, isStandalone: true, selector: "pptx-ribbon-insert-fields", inputs: { slideIndex: { classPropertyName: "slideIndex", publicName: "slideIndex", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
67027
67793
|
<!-- Action Buttons dropdown (hover-reveal, mirrors React/Vue) -->
|
|
67028
67794
|
<div class="group relative">
|
|
67029
67795
|
<button
|
|
@@ -67182,6 +67948,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
67182
67948
|
selector: 'pptx-ribbon-insert-fields',
|
|
67183
67949
|
standalone: true,
|
|
67184
67950
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
67951
|
+
host: { class: 'contents' },
|
|
67185
67952
|
imports: [TranslatePipe],
|
|
67186
67953
|
template: `
|
|
67187
67954
|
<!-- Action Buttons dropdown (hover-reveal, mirrors React/Vue) -->
|
|
@@ -69652,24 +70419,29 @@ function seedShareFields(defaults) {
|
|
|
69652
70419
|
serverUrl: defaults?.serverUrl ?? '',
|
|
69653
70420
|
};
|
|
69654
70421
|
}
|
|
69655
|
-
/**
|
|
70422
|
+
/**
|
|
70423
|
+
* Whether the required fields are non-blank (after trimming). The server URL is
|
|
70424
|
+
* optional: a blank server selects the serverless (webrtc) peer-to-peer
|
|
70425
|
+
* transport, so only the room id and display name are required.
|
|
70426
|
+
*/
|
|
69656
70427
|
function canStartShare(fields) {
|
|
69657
|
-
return
|
|
69658
|
-
fields.userName.trim().length > 0 &&
|
|
69659
|
-
fields.serverUrl.trim().length > 0);
|
|
70428
|
+
return fields.roomId.trim().length > 0 && fields.userName.trim().length > 0;
|
|
69660
70429
|
}
|
|
69661
70430
|
/**
|
|
69662
70431
|
* Assemble a {@link CollaborationConfig} from the (trimmed) form fields, or
|
|
69663
|
-
* `null` when the form is incomplete.
|
|
70432
|
+
* `null` when the form is incomplete. A blank server URL yields
|
|
70433
|
+
* `transport: 'webrtc'`; otherwise the default websocket transport is used.
|
|
69664
70434
|
*/
|
|
69665
70435
|
function buildCollaborationConfig(fields) {
|
|
69666
70436
|
if (!canStartShare(fields)) {
|
|
69667
70437
|
return null;
|
|
69668
70438
|
}
|
|
70439
|
+
const serverUrl = fields.serverUrl.trim();
|
|
69669
70440
|
return {
|
|
69670
70441
|
roomId: fields.roomId.trim(),
|
|
69671
70442
|
userName: fields.userName.trim(),
|
|
69672
|
-
serverUrl
|
|
70443
|
+
serverUrl,
|
|
70444
|
+
transport: resolveTransportForServerUrl(serverUrl),
|
|
69673
70445
|
};
|
|
69674
70446
|
}
|
|
69675
70447
|
/**
|
|
@@ -69681,7 +70453,11 @@ function buildShareUrl(roomId, serverUrl, location) {
|
|
|
69681
70453
|
return roomId;
|
|
69682
70454
|
}
|
|
69683
70455
|
const room = encodeURIComponent(roomId);
|
|
69684
|
-
const
|
|
70456
|
+
const trimmed = serverUrl.trim();
|
|
70457
|
+
if (trimmed.length === 0) {
|
|
70458
|
+
return `${location.origin}${location.pathname}?room=${room}&transport=webrtc`;
|
|
70459
|
+
}
|
|
70460
|
+
const server = encodeURIComponent(trimmed);
|
|
69685
70461
|
return `${location.origin}${location.pathname}?room=${room}&server=${server}`;
|
|
69686
70462
|
}
|
|
69687
70463
|
|
|
@@ -69717,6 +70493,9 @@ class ShareDialogComponent {
|
|
|
69717
70493
|
/** Shareable join link surfaced while the session is active. */
|
|
69718
70494
|
shareUrl = input('', /* @ts-ignore */
|
|
69719
70495
|
...(ngDevMode ? [{ debugName: "shareUrl" }] : /* istanbul ignore next */ []));
|
|
70496
|
+
/** Whether the active session is peer-to-peer (serverless webrtc). */
|
|
70497
|
+
p2p = input(false, /* @ts-ignore */
|
|
70498
|
+
...(ngDevMode ? [{ debugName: "p2p" }] : /* istanbul ignore next */ []));
|
|
69720
70499
|
/** Fired with the assembled config when the user starts sharing. */
|
|
69721
70500
|
start = output();
|
|
69722
70501
|
/** Fired when the user stops an active session. */
|
|
@@ -69737,6 +70516,9 @@ class ShareDialogComponent {
|
|
|
69737
70516
|
serverUrl: this.serverUrl(),
|
|
69738
70517
|
}), /* @ts-ignore */
|
|
69739
70518
|
...(ngDevMode ? [{ debugName: "canStart" }] : /* istanbul ignore next */ []));
|
|
70519
|
+
/** Blank server URL selects the serverless peer-to-peer (webrtc) transport. */
|
|
70520
|
+
isP2p = computed(() => this.serverUrl().trim().length === 0, /* @ts-ignore */
|
|
70521
|
+
...(ngDevMode ? [{ debugName: "isP2p" }] : /* istanbul ignore next */ []));
|
|
69740
70522
|
canCopy = computed(() => canUseClipboard(typeof navigator !== 'undefined' ? navigator : undefined), /* @ts-ignore */
|
|
69741
70523
|
...(ngDevMode ? [{ debugName: "canCopy" }] : /* istanbul ignore next */ []));
|
|
69742
70524
|
constructor() {
|
|
@@ -69783,7 +70565,7 @@ class ShareDialogComponent {
|
|
|
69783
70565
|
this.stop.emit();
|
|
69784
70566
|
}
|
|
69785
70567
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ShareDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
69786
|
-
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: `
|
|
70568
|
+
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: `
|
|
69787
70569
|
<pptx-modal-dialog
|
|
69788
70570
|
[open]="open()"
|
|
69789
70571
|
[title]="(active() ? 'pptx.share.activeTitle' : 'pptx.toolbar.share') | translate"
|
|
@@ -69810,6 +70592,14 @@ class ShareDialogComponent {
|
|
|
69810
70592
|
</span>
|
|
69811
70593
|
</div>
|
|
69812
70594
|
|
|
70595
|
+
@if (p2p()) {
|
|
70596
|
+
<div class="pptx-ng-share-status-row">
|
|
70597
|
+
<span class="pptx-ng-share-count" style="margin-left: 0">
|
|
70598
|
+
{{ 'pptx.share.p2pServerValue' | translate }}
|
|
70599
|
+
</span>
|
|
70600
|
+
</div>
|
|
70601
|
+
}
|
|
70602
|
+
|
|
69813
70603
|
@if (shareUrl()) {
|
|
69814
70604
|
<div class="pptx-ng-share-field">
|
|
69815
70605
|
<label class="pptx-ng-share-label">{{
|
|
@@ -69886,6 +70676,9 @@ class ShareDialogComponent {
|
|
|
69886
70676
|
[value]="serverUrl()"
|
|
69887
70677
|
(input)="serverUrl.set(asValue($event))"
|
|
69888
70678
|
/>
|
|
70679
|
+
@if (isP2p()) {
|
|
70680
|
+
<p class="pptx-ng-share-hint">{{ 'pptx.share.p2pHint' | translate }}</p>
|
|
70681
|
+
}
|
|
69889
70682
|
</div>
|
|
69890
70683
|
</div>
|
|
69891
70684
|
}
|
|
@@ -69937,6 +70730,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
69937
70730
|
</span>
|
|
69938
70731
|
</div>
|
|
69939
70732
|
|
|
70733
|
+
@if (p2p()) {
|
|
70734
|
+
<div class="pptx-ng-share-status-row">
|
|
70735
|
+
<span class="pptx-ng-share-count" style="margin-left: 0">
|
|
70736
|
+
{{ 'pptx.share.p2pServerValue' | translate }}
|
|
70737
|
+
</span>
|
|
70738
|
+
</div>
|
|
70739
|
+
}
|
|
70740
|
+
|
|
69940
70741
|
@if (shareUrl()) {
|
|
69941
70742
|
<div class="pptx-ng-share-field">
|
|
69942
70743
|
<label class="pptx-ng-share-label">{{
|
|
@@ -70013,6 +70814,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
70013
70814
|
[value]="serverUrl()"
|
|
70014
70815
|
(input)="serverUrl.set(asValue($event))"
|
|
70015
70816
|
/>
|
|
70817
|
+
@if (isP2p()) {
|
|
70818
|
+
<p class="pptx-ng-share-hint">{{ 'pptx.share.p2pHint' | translate }}</p>
|
|
70819
|
+
}
|
|
70016
70820
|
</div>
|
|
70017
70821
|
</div>
|
|
70018
70822
|
}
|
|
@@ -70034,7 +70838,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
70034
70838
|
</div>
|
|
70035
70839
|
</pptx-modal-dialog>
|
|
70036
70840
|
`, 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"] }]
|
|
70037
|
-
}], 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"] }] } });
|
|
70841
|
+
}], 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"] }] } });
|
|
70038
70842
|
|
|
70039
70843
|
/**
|
|
70040
70844
|
* signatures-helpers.ts: Pure functions for digital-signature status
|
|
@@ -71556,6 +72360,34 @@ class ViewerCollaborationSessionService {
|
|
|
71556
72360
|
: '';
|
|
71557
72361
|
}, /* @ts-ignore */
|
|
71558
72362
|
...(ngDevMode ? [{ debugName: "broadcastViewerUrl" }] : /* istanbul ignore next */ []));
|
|
72363
|
+
/** Whether the active session is serverless (peer-to-peer / webrtc). */
|
|
72364
|
+
activeSessionP2p = computed(() => (this.activeSession()?.serverUrl ?? '').trim().length === 0 && this.collab.active(), /* @ts-ignore */
|
|
72365
|
+
...(ngDevMode ? [{ debugName: "activeSessionP2p" }] : /* istanbul ignore next */ []));
|
|
72366
|
+
/**
|
|
72367
|
+
* Assemble the {@link ConnectOptions} shared by every connect call site: apply
|
|
72368
|
+
* remote slides to the editor, size the presence bounds to the canvas, and
|
|
72369
|
+
* expose the source bytes + separated template elements for write-back.
|
|
72370
|
+
*/
|
|
72371
|
+
connectOptions() {
|
|
72372
|
+
const host = this.requireHost();
|
|
72373
|
+
const size = host.canvasSize();
|
|
72374
|
+
return {
|
|
72375
|
+
onRemoteSlides: (slides) => host.applyRemoteSlides(slides),
|
|
72376
|
+
canvasWidth: size.width,
|
|
72377
|
+
canvasHeight: size.height,
|
|
72378
|
+
getSourceBytes: () => host.getSourceBytes(),
|
|
72379
|
+
getTemplateElements: () => host.getTemplateElements(),
|
|
72380
|
+
};
|
|
72381
|
+
}
|
|
72382
|
+
/**
|
|
72383
|
+
* Connect and immediately seed the sync baseline with the current deck, so a
|
|
72384
|
+
* joiner whose deck is still a placeholder never broadcasts (and overwrites)
|
|
72385
|
+
* the shared document before the first remote sync arrives.
|
|
72386
|
+
*/
|
|
72387
|
+
connectWithBaseline(config) {
|
|
72388
|
+
void this.collab.connect(config, this.connectOptions());
|
|
72389
|
+
this.collab.seedBaseline(this.requireHost().currentSlides());
|
|
72390
|
+
}
|
|
71559
72391
|
/**
|
|
71560
72392
|
* Seed values for the Share dialog: the host-supplied `shareDefaults`, with
|
|
71561
72393
|
* `userName` falling back to `authorName` (then "You") so the local user's
|
|
@@ -71577,9 +72409,7 @@ class ViewerCollaborationSessionService {
|
|
|
71577
72409
|
syncHostConfig(config) {
|
|
71578
72410
|
if (config) {
|
|
71579
72411
|
this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
|
|
71580
|
-
|
|
71581
|
-
getTemplateElements: () => this.requireHost().getTemplateElements(),
|
|
71582
|
-
});
|
|
72412
|
+
this.connectWithBaseline(config);
|
|
71583
72413
|
}
|
|
71584
72414
|
else {
|
|
71585
72415
|
this.collab.disconnect();
|
|
@@ -71599,9 +72429,7 @@ class ViewerCollaborationSessionService {
|
|
|
71599
72429
|
roomId: collaboratorConfig.roomId,
|
|
71600
72430
|
serverUrl: collaboratorConfig.serverUrl,
|
|
71601
72431
|
});
|
|
71602
|
-
|
|
71603
|
-
getTemplateElements: () => host.getTemplateElements(),
|
|
71604
|
-
});
|
|
72432
|
+
this.connectWithBaseline(collaboratorConfig);
|
|
71605
72433
|
host.emitStart(collaboratorConfig);
|
|
71606
72434
|
}
|
|
71607
72435
|
onShareStop() {
|
|
@@ -71615,13 +72443,12 @@ class ViewerCollaborationSessionService {
|
|
|
71615
72443
|
const collabConfig = {
|
|
71616
72444
|
roomId: config.roomId,
|
|
71617
72445
|
serverUrl: config.serverUrl,
|
|
72446
|
+
transport: config.transport,
|
|
71618
72447
|
userName: host.authorName() ?? 'Presenter',
|
|
71619
72448
|
role: 'owner',
|
|
71620
72449
|
};
|
|
71621
72450
|
this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
|
|
71622
|
-
|
|
71623
|
-
getTemplateElements: () => host.getTemplateElements(),
|
|
71624
|
-
});
|
|
72451
|
+
this.connectWithBaseline(collabConfig);
|
|
71625
72452
|
host.emitStart(collabConfig);
|
|
71626
72453
|
}
|
|
71627
72454
|
onBroadcastStop() {
|
|
@@ -76027,6 +76854,14 @@ class PowerPointViewerComponent {
|
|
|
76027
76854
|
...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
76028
76855
|
zoomPercent = computed(() => Math.round(this.zoom() * 100), /* @ts-ignore */
|
|
76029
76856
|
...(ngDevMode ? [{ debugName: "zoomPercent" }] : /* istanbul ignore next */ []));
|
|
76857
|
+
/**
|
|
76858
|
+
* Remote cursors filtered to the slide the local user is viewing, so peers'
|
|
76859
|
+
* cursors only appear on the shared slide (mirrors React/Vue).
|
|
76860
|
+
*/
|
|
76861
|
+
collabCursors = computed(() => presenceToCursors(this.collab.presence(), this.activeSlideIndex()), /* @ts-ignore */
|
|
76862
|
+
...(ngDevMode ? [{ debugName: "collabCursors" }] : /* istanbul ignore next */ []));
|
|
76863
|
+
/** Timestamp of the last cursor broadcast (throttle gate). */
|
|
76864
|
+
lastCursorBroadcast = 0;
|
|
76030
76865
|
/** Fullscreen presentation-mode overlay visibility. */
|
|
76031
76866
|
presenting = signal(false, /* @ts-ignore */
|
|
76032
76867
|
...(ngDevMode ? [{ debugName: "presenting" }] : /* istanbul ignore next */ []));
|
|
@@ -76291,6 +77126,47 @@ class PowerPointViewerComponent {
|
|
|
76291
77126
|
effect(() => {
|
|
76292
77127
|
this.session.syncHostConfig(this.collaboration());
|
|
76293
77128
|
});
|
|
77129
|
+
// Push local slide edits into the shared Y.Doc (reconcile-based; the
|
|
77130
|
+
// service guards against echoing remote-applied changes and against
|
|
77131
|
+
// clobbering with an empty deck). A broadcast `viewer` never writes, so a
|
|
77132
|
+
// follow-along joiner cannot overwrite the presenter's deck.
|
|
77133
|
+
effect(() => {
|
|
77134
|
+
const slides = this.editor.slides();
|
|
77135
|
+
if (this.collab.active() && this.collab.activeRole() !== 'viewer') {
|
|
77136
|
+
this.collab.broadcastSlides(slides);
|
|
77137
|
+
}
|
|
77138
|
+
});
|
|
77139
|
+
// Publish the local selection so peers can draw remote selection boxes.
|
|
77140
|
+
effect(() => {
|
|
77141
|
+
const ids = this.editor.selectedIds();
|
|
77142
|
+
if (this.collab.active()) {
|
|
77143
|
+
this.collab.setSelection(ids[0], this.activeSlideIndex());
|
|
77144
|
+
}
|
|
77145
|
+
});
|
|
77146
|
+
// Publish the local active slide so followers navigate with us.
|
|
77147
|
+
effect(() => {
|
|
77148
|
+
const index = this.activeSlideIndex();
|
|
77149
|
+
if (this.collab.active()) {
|
|
77150
|
+
this.collab.setActiveSlide(index);
|
|
77151
|
+
}
|
|
77152
|
+
});
|
|
77153
|
+
// Follow mode: mirror the followed peer's active slide.
|
|
77154
|
+
effect(() => {
|
|
77155
|
+
const target = this.collab.followedSlideIndex();
|
|
77156
|
+
if (target !== null) {
|
|
77157
|
+
this.goTo(target);
|
|
77158
|
+
}
|
|
77159
|
+
});
|
|
77160
|
+
// Broadcast auto-follow: a `viewer` tracks the broadcaster (owner) peer.
|
|
77161
|
+
effect(() => {
|
|
77162
|
+
if (this.collab.activeRole() !== 'viewer') {
|
|
77163
|
+
return;
|
|
77164
|
+
}
|
|
77165
|
+
const target = this.collab.broadcasterSlideIndex();
|
|
77166
|
+
if (target !== null) {
|
|
77167
|
+
this.goTo(target);
|
|
77168
|
+
}
|
|
77169
|
+
});
|
|
76294
77170
|
// Hand the export/print orchestrator the live navigation signal + deck
|
|
76295
77171
|
// accessors + stage resolver so it can flip the stage and capture slides.
|
|
76296
77172
|
this.xport.bind({
|
|
@@ -76312,6 +77188,10 @@ class PowerPointViewerComponent {
|
|
|
76312
77188
|
authorName: () => this.authorName(),
|
|
76313
77189
|
shareDefaults: () => this.shareDefaults(),
|
|
76314
77190
|
getTemplateElements: () => this.editor.templateElementsBySlideId(),
|
|
77191
|
+
applyRemoteSlides: (slides) => this.editor.applyRemoteSlides(slides),
|
|
77192
|
+
canvasSize: () => this.loader.canvasSize(),
|
|
77193
|
+
getSourceBytes: () => this.currentSourceBytes(),
|
|
77194
|
+
currentSlides: () => this.editor.slides(),
|
|
76315
77195
|
emitStart: (config) => this.startCollaboration.emit(config),
|
|
76316
77196
|
emitStop: () => this.stopCollaboration.emit(),
|
|
76317
77197
|
});
|
|
@@ -76367,6 +77247,40 @@ class PowerPointViewerComponent {
|
|
|
76367
77247
|
goNext() {
|
|
76368
77248
|
this.goTo(this.activeSlideIndex() + 1);
|
|
76369
77249
|
}
|
|
77250
|
+
/**
|
|
77251
|
+
* Publish the local cursor while the pointer moves over the canvas. Throttled
|
|
77252
|
+
* to {@link BROADCAST_THROTTLE_MS}; coordinates are mapped from client space
|
|
77253
|
+
* into unscaled slide space (dividing by zoom, matching the cursor overlay)
|
|
77254
|
+
* and clamped to the canvas bounds.
|
|
77255
|
+
*/
|
|
77256
|
+
onCollabPointerMove(event) {
|
|
77257
|
+
if (!this.collab.active()) {
|
|
77258
|
+
return;
|
|
77259
|
+
}
|
|
77260
|
+
const now = Date.now();
|
|
77261
|
+
if (now - this.lastCursorBroadcast < BROADCAST_THROTTLE_MS) {
|
|
77262
|
+
return;
|
|
77263
|
+
}
|
|
77264
|
+
this.lastCursorBroadcast = now;
|
|
77265
|
+
const host = this.mainEl()?.nativeElement;
|
|
77266
|
+
if (!host) {
|
|
77267
|
+
return;
|
|
77268
|
+
}
|
|
77269
|
+
const rect = host.getBoundingClientRect();
|
|
77270
|
+
const zoom = this.zoom() || 1;
|
|
77271
|
+
const size = this.loader.canvasSize();
|
|
77272
|
+
const x = clampCursorPosition((event.clientX - rect.left) / zoom, 0, size.width);
|
|
77273
|
+
const y = clampCursorPosition((event.clientY - rect.top) / zoom, 0, size.height);
|
|
77274
|
+
this.collab.setCursor(x, y, this.activeSlideIndex());
|
|
77275
|
+
}
|
|
77276
|
+
/** The loaded source `.pptx` bytes (for elected-writer write-back), if any. */
|
|
77277
|
+
currentSourceBytes() {
|
|
77278
|
+
const content = this.contentOverride() ?? this.content();
|
|
77279
|
+
if (!content) {
|
|
77280
|
+
return null;
|
|
77281
|
+
}
|
|
77282
|
+
return content instanceof Uint8Array ? content : new Uint8Array(content);
|
|
77283
|
+
}
|
|
76370
77284
|
// ── Theme gallery (Design tab) ─────────────────────────────────────────────
|
|
76371
77285
|
/**
|
|
76372
77286
|
* Apply a built-in theme preset to the whole deck.
|
|
@@ -77004,7 +77918,7 @@ class PowerPointViewerComponent {
|
|
|
77004
77918
|
</nav>
|
|
77005
77919
|
}
|
|
77006
77920
|
|
|
77007
|
-
<main class="pptx-ng-main" #mainEl>
|
|
77921
|
+
<main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
|
|
77008
77922
|
<pptx-slide-canvas
|
|
77009
77923
|
[slide]="activeSlide()"
|
|
77010
77924
|
[canvasSize]="loader.canvasSize()"
|
|
@@ -77042,7 +77956,22 @@ class PowerPointViewerComponent {
|
|
|
77042
77956
|
(tableChange)="onTableChange($event)"
|
|
77043
77957
|
/>
|
|
77044
77958
|
@if (collab.connected()) {
|
|
77045
|
-
<pptx-collaboration-cursors [cursors]="
|
|
77959
|
+
<pptx-collaboration-cursors [cursors]="collabCursors()" [zoom]="zoom()" />
|
|
77960
|
+
<pptx-remote-selection-overlay
|
|
77961
|
+
[presences]="collab.presence()"
|
|
77962
|
+
[elements]="activeSlide()?.elements ?? []"
|
|
77963
|
+
[activeSlideIndex]="activeSlideIndex()"
|
|
77964
|
+
[zoom]="zoom()"
|
|
77965
|
+
/>
|
|
77966
|
+
}
|
|
77967
|
+
@if (collab.active() && collab.presence().length > 0) {
|
|
77968
|
+
<div class="pptx-ng-collab-follow">
|
|
77969
|
+
<pptx-follow-mode-bar
|
|
77970
|
+
[presences]="collab.presence()"
|
|
77971
|
+
[followedClientId]="collab.followedClientId()"
|
|
77972
|
+
(follow)="collab.followUser($event)"
|
|
77973
|
+
/>
|
|
77974
|
+
</div>
|
|
77046
77975
|
}
|
|
77047
77976
|
@if (showNotes() && !mobile.isMobile()) {
|
|
77048
77977
|
<aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
|
|
@@ -77308,6 +78237,7 @@ class PowerPointViewerComponent {
|
|
|
77308
78237
|
[connected]="collab.connected()"
|
|
77309
78238
|
[userCount]="collab.connectedCount()"
|
|
77310
78239
|
[shareUrl]="session.shareUrl()"
|
|
78240
|
+
[p2p]="session.activeSessionP2p()"
|
|
77311
78241
|
[defaults]="session.shareDialogDefaults()"
|
|
77312
78242
|
(start)="session.onShareStart($event)"
|
|
77313
78243
|
(stop)="session.onShareStop()"
|
|
@@ -77320,6 +78250,7 @@ class PowerPointViewerComponent {
|
|
|
77320
78250
|
[connected]="collab.connected()"
|
|
77321
78251
|
[viewerCount]="collab.presence().length"
|
|
77322
78252
|
[viewerUrl]="session.broadcastViewerUrl()"
|
|
78253
|
+
[p2p]="session.activeSessionP2p()"
|
|
77323
78254
|
[defaults]="{ serverUrl: shareDefaults()?.serverUrl }"
|
|
77324
78255
|
(start)="session.onBroadcastStart($event)"
|
|
77325
78256
|
(stop)="session.onBroadcastStop()"
|
|
@@ -77427,7 +78358,7 @@ class PowerPointViewerComponent {
|
|
|
77427
78358
|
/>
|
|
77428
78359
|
}
|
|
77429
78360
|
</div>
|
|
77430
|
-
`, 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 });
|
|
78361
|
+
`, 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 });
|
|
77431
78362
|
}
|
|
77432
78363
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: PowerPointViewerComponent, decorators: [{
|
|
77433
78364
|
type: Component,
|
|
@@ -77477,6 +78408,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77477
78408
|
SignaturesPanelComponent,
|
|
77478
78409
|
AccessibilityPanelComponent,
|
|
77479
78410
|
CollaborationCursorsComponent,
|
|
78411
|
+
RemoteSelectionOverlayComponent,
|
|
78412
|
+
FollowModeBarComponent,
|
|
77480
78413
|
PropertiesDialogComponent,
|
|
77481
78414
|
HyperlinkDialogComponent,
|
|
77482
78415
|
PrintDialogComponent,
|
|
@@ -77621,7 +78554,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77621
78554
|
</nav>
|
|
77622
78555
|
}
|
|
77623
78556
|
|
|
77624
|
-
<main class="pptx-ng-main" #mainEl>
|
|
78557
|
+
<main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
|
|
77625
78558
|
<pptx-slide-canvas
|
|
77626
78559
|
[slide]="activeSlide()"
|
|
77627
78560
|
[canvasSize]="loader.canvasSize()"
|
|
@@ -77659,7 +78592,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77659
78592
|
(tableChange)="onTableChange($event)"
|
|
77660
78593
|
/>
|
|
77661
78594
|
@if (collab.connected()) {
|
|
77662
|
-
<pptx-collaboration-cursors [cursors]="
|
|
78595
|
+
<pptx-collaboration-cursors [cursors]="collabCursors()" [zoom]="zoom()" />
|
|
78596
|
+
<pptx-remote-selection-overlay
|
|
78597
|
+
[presences]="collab.presence()"
|
|
78598
|
+
[elements]="activeSlide()?.elements ?? []"
|
|
78599
|
+
[activeSlideIndex]="activeSlideIndex()"
|
|
78600
|
+
[zoom]="zoom()"
|
|
78601
|
+
/>
|
|
78602
|
+
}
|
|
78603
|
+
@if (collab.active() && collab.presence().length > 0) {
|
|
78604
|
+
<div class="pptx-ng-collab-follow">
|
|
78605
|
+
<pptx-follow-mode-bar
|
|
78606
|
+
[presences]="collab.presence()"
|
|
78607
|
+
[followedClientId]="collab.followedClientId()"
|
|
78608
|
+
(follow)="collab.followUser($event)"
|
|
78609
|
+
/>
|
|
78610
|
+
</div>
|
|
77663
78611
|
}
|
|
77664
78612
|
@if (showNotes() && !mobile.isMobile()) {
|
|
77665
78613
|
<aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
|
|
@@ -77925,6 +78873,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77925
78873
|
[connected]="collab.connected()"
|
|
77926
78874
|
[userCount]="collab.connectedCount()"
|
|
77927
78875
|
[shareUrl]="session.shareUrl()"
|
|
78876
|
+
[p2p]="session.activeSessionP2p()"
|
|
77928
78877
|
[defaults]="session.shareDialogDefaults()"
|
|
77929
78878
|
(start)="session.onShareStart($event)"
|
|
77930
78879
|
(stop)="session.onShareStop()"
|
|
@@ -77937,6 +78886,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77937
78886
|
[connected]="collab.connected()"
|
|
77938
78887
|
[viewerCount]="collab.presence().length"
|
|
77939
78888
|
[viewerUrl]="session.broadcastViewerUrl()"
|
|
78889
|
+
[p2p]="session.activeSessionP2p()"
|
|
77940
78890
|
[defaults]="{ serverUrl: shareDefaults()?.serverUrl }"
|
|
77941
78891
|
(start)="session.onBroadcastStart($event)"
|
|
77942
78892
|
(stop)="session.onBroadcastStop()"
|
|
@@ -78473,6 +79423,9 @@ const translationsEn = {
|
|
|
78473
79423
|
'pptx.autosave.justNow': 'just now',
|
|
78474
79424
|
'pptx.autosave.oneMinAgo': '1 min ago',
|
|
78475
79425
|
'pptx.autosave.minutesAgo': '{{count}} min ago',
|
|
79426
|
+
'pptx.autosave.saveFailed': 'Save failed',
|
|
79427
|
+
'pptx.autosave.savedShort': 'Saved',
|
|
79428
|
+
'pptx.autosave.allChangesSaved': 'All changes saved',
|
|
78476
79429
|
// Toolbar
|
|
78477
79430
|
'pptx.toolbar.toggleSlidesPanel': 'Toggle slides panel',
|
|
78478
79431
|
'pptx.toolbar.undo': 'Undo',
|
|
@@ -78591,7 +79544,17 @@ const translationsEn = {
|
|
|
78591
79544
|
'pptx.collaboration.youLabel': '{{name}} (you)',
|
|
78592
79545
|
'pptx.collaboration.usersConnected': '{{count}} user(s) connected',
|
|
78593
79546
|
'pptx.collaboration.moreUsers': '{{count}} more user(s)',
|
|
79547
|
+
'pptx.collaboration.onePersonHere': '1 person here',
|
|
79548
|
+
'pptx.collaboration.peopleHere': '{{count}} people here',
|
|
78594
79549
|
'pptx.collaboration.retry': 'Retry',
|
|
79550
|
+
'pptx.collaboration.retryConnection': 'Retry connection',
|
|
79551
|
+
// Follow mode
|
|
79552
|
+
'pptx.followMode.following': 'Following',
|
|
79553
|
+
'pptx.followMode.stop': 'Stop',
|
|
79554
|
+
'pptx.followMode.stopFollowing': 'Stop following',
|
|
79555
|
+
'pptx.followMode.followCollaborator': 'Follow a collaborator',
|
|
79556
|
+
'pptx.followMode.followUser': 'Follow {{name}}',
|
|
79557
|
+
'pptx.followMode.stopFollowingUser': 'Stop following {{name}}',
|
|
78595
79558
|
// Share dialog
|
|
78596
79559
|
'pptx.share.title': 'Share Presentation',
|
|
78597
79560
|
'pptx.share.collaborationActive': 'Collaboration Active',
|