pptx-angular-viewer 1.1.58 → 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.
|
@@ -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;
|
|
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;
|
|
35433
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
|
|
@@ -69658,24 +70419,29 @@ function seedShareFields(defaults) {
|
|
|
69658
70419
|
serverUrl: defaults?.serverUrl ?? '',
|
|
69659
70420
|
};
|
|
69660
70421
|
}
|
|
69661
|
-
/**
|
|
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
|
+
*/
|
|
69662
70427
|
function canStartShare(fields) {
|
|
69663
|
-
return
|
|
69664
|
-
fields.userName.trim().length > 0 &&
|
|
69665
|
-
fields.serverUrl.trim().length > 0);
|
|
70428
|
+
return fields.roomId.trim().length > 0 && fields.userName.trim().length > 0;
|
|
69666
70429
|
}
|
|
69667
70430
|
/**
|
|
69668
70431
|
* Assemble a {@link CollaborationConfig} from the (trimmed) form fields, or
|
|
69669
|
-
* `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.
|
|
69670
70434
|
*/
|
|
69671
70435
|
function buildCollaborationConfig(fields) {
|
|
69672
70436
|
if (!canStartShare(fields)) {
|
|
69673
70437
|
return null;
|
|
69674
70438
|
}
|
|
70439
|
+
const serverUrl = fields.serverUrl.trim();
|
|
69675
70440
|
return {
|
|
69676
70441
|
roomId: fields.roomId.trim(),
|
|
69677
70442
|
userName: fields.userName.trim(),
|
|
69678
|
-
serverUrl
|
|
70443
|
+
serverUrl,
|
|
70444
|
+
transport: resolveTransportForServerUrl(serverUrl),
|
|
69679
70445
|
};
|
|
69680
70446
|
}
|
|
69681
70447
|
/**
|
|
@@ -69687,7 +70453,11 @@ function buildShareUrl(roomId, serverUrl, location) {
|
|
|
69687
70453
|
return roomId;
|
|
69688
70454
|
}
|
|
69689
70455
|
const room = encodeURIComponent(roomId);
|
|
69690
|
-
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);
|
|
69691
70461
|
return `${location.origin}${location.pathname}?room=${room}&server=${server}`;
|
|
69692
70462
|
}
|
|
69693
70463
|
|
|
@@ -69723,6 +70493,9 @@ class ShareDialogComponent {
|
|
|
69723
70493
|
/** Shareable join link surfaced while the session is active. */
|
|
69724
70494
|
shareUrl = input('', /* @ts-ignore */
|
|
69725
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 */ []));
|
|
69726
70499
|
/** Fired with the assembled config when the user starts sharing. */
|
|
69727
70500
|
start = output();
|
|
69728
70501
|
/** Fired when the user stops an active session. */
|
|
@@ -69743,6 +70516,9 @@ class ShareDialogComponent {
|
|
|
69743
70516
|
serverUrl: this.serverUrl(),
|
|
69744
70517
|
}), /* @ts-ignore */
|
|
69745
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 */ []));
|
|
69746
70522
|
canCopy = computed(() => canUseClipboard(typeof navigator !== 'undefined' ? navigator : undefined), /* @ts-ignore */
|
|
69747
70523
|
...(ngDevMode ? [{ debugName: "canCopy" }] : /* istanbul ignore next */ []));
|
|
69748
70524
|
constructor() {
|
|
@@ -69789,7 +70565,7 @@ class ShareDialogComponent {
|
|
|
69789
70565
|
this.stop.emit();
|
|
69790
70566
|
}
|
|
69791
70567
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ShareDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
69792
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ShareDialogComponent, isStandalone: true, selector: "pptx-share-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaults: { classPropertyName: "defaults", publicName: "defaults", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, connected: { classPropertyName: "connected", publicName: "connected", isSignal: true, isRequired: false, transformFunction: null }, userCount: { classPropertyName: "userCount", publicName: "userCount", isSignal: true, isRequired: false, transformFunction: null }, shareUrl: { classPropertyName: "shareUrl", publicName: "shareUrl", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { start: "start", stop: "stop", close: "close" }, ngImport: i0, template: `
|
|
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: `
|
|
69793
70569
|
<pptx-modal-dialog
|
|
69794
70570
|
[open]="open()"
|
|
69795
70571
|
[title]="(active() ? 'pptx.share.activeTitle' : 'pptx.toolbar.share') | translate"
|
|
@@ -69816,6 +70592,14 @@ class ShareDialogComponent {
|
|
|
69816
70592
|
</span>
|
|
69817
70593
|
</div>
|
|
69818
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
|
+
|
|
69819
70603
|
@if (shareUrl()) {
|
|
69820
70604
|
<div class="pptx-ng-share-field">
|
|
69821
70605
|
<label class="pptx-ng-share-label">{{
|
|
@@ -69892,6 +70676,9 @@ class ShareDialogComponent {
|
|
|
69892
70676
|
[value]="serverUrl()"
|
|
69893
70677
|
(input)="serverUrl.set(asValue($event))"
|
|
69894
70678
|
/>
|
|
70679
|
+
@if (isP2p()) {
|
|
70680
|
+
<p class="pptx-ng-share-hint">{{ 'pptx.share.p2pHint' | translate }}</p>
|
|
70681
|
+
}
|
|
69895
70682
|
</div>
|
|
69896
70683
|
</div>
|
|
69897
70684
|
}
|
|
@@ -69943,6 +70730,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
69943
70730
|
</span>
|
|
69944
70731
|
</div>
|
|
69945
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
|
+
|
|
69946
70741
|
@if (shareUrl()) {
|
|
69947
70742
|
<div class="pptx-ng-share-field">
|
|
69948
70743
|
<label class="pptx-ng-share-label">{{
|
|
@@ -70019,6 +70814,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
70019
70814
|
[value]="serverUrl()"
|
|
70020
70815
|
(input)="serverUrl.set(asValue($event))"
|
|
70021
70816
|
/>
|
|
70817
|
+
@if (isP2p()) {
|
|
70818
|
+
<p class="pptx-ng-share-hint">{{ 'pptx.share.p2pHint' | translate }}</p>
|
|
70819
|
+
}
|
|
70022
70820
|
</div>
|
|
70023
70821
|
</div>
|
|
70024
70822
|
}
|
|
@@ -70040,7 +70838,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
70040
70838
|
</div>
|
|
70041
70839
|
</pptx-modal-dialog>
|
|
70042
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"] }]
|
|
70043
|
-
}], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], defaults: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaults", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], connected: [{ type: i0.Input, args: [{ isSignal: true, alias: "connected", required: false }] }], userCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "userCount", required: false }] }], shareUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "shareUrl", required: false }] }], start: [{ type: i0.Output, args: ["start"] }], stop: [{ type: i0.Output, args: ["stop"] }], close: [{ type: i0.Output, args: ["close"] }] } });
|
|
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"] }] } });
|
|
70044
70842
|
|
|
70045
70843
|
/**
|
|
70046
70844
|
* signatures-helpers.ts: Pure functions for digital-signature status
|
|
@@ -71562,6 +72360,34 @@ class ViewerCollaborationSessionService {
|
|
|
71562
72360
|
: '';
|
|
71563
72361
|
}, /* @ts-ignore */
|
|
71564
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
|
+
}
|
|
71565
72391
|
/**
|
|
71566
72392
|
* Seed values for the Share dialog: the host-supplied `shareDefaults`, with
|
|
71567
72393
|
* `userName` falling back to `authorName` (then "You") so the local user's
|
|
@@ -71583,9 +72409,7 @@ class ViewerCollaborationSessionService {
|
|
|
71583
72409
|
syncHostConfig(config) {
|
|
71584
72410
|
if (config) {
|
|
71585
72411
|
this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
|
|
71586
|
-
|
|
71587
|
-
getTemplateElements: () => this.requireHost().getTemplateElements(),
|
|
71588
|
-
});
|
|
72412
|
+
this.connectWithBaseline(config);
|
|
71589
72413
|
}
|
|
71590
72414
|
else {
|
|
71591
72415
|
this.collab.disconnect();
|
|
@@ -71605,9 +72429,7 @@ class ViewerCollaborationSessionService {
|
|
|
71605
72429
|
roomId: collaboratorConfig.roomId,
|
|
71606
72430
|
serverUrl: collaboratorConfig.serverUrl,
|
|
71607
72431
|
});
|
|
71608
|
-
|
|
71609
|
-
getTemplateElements: () => host.getTemplateElements(),
|
|
71610
|
-
});
|
|
72432
|
+
this.connectWithBaseline(collaboratorConfig);
|
|
71611
72433
|
host.emitStart(collaboratorConfig);
|
|
71612
72434
|
}
|
|
71613
72435
|
onShareStop() {
|
|
@@ -71621,13 +72443,12 @@ class ViewerCollaborationSessionService {
|
|
|
71621
72443
|
const collabConfig = {
|
|
71622
72444
|
roomId: config.roomId,
|
|
71623
72445
|
serverUrl: config.serverUrl,
|
|
72446
|
+
transport: config.transport,
|
|
71624
72447
|
userName: host.authorName() ?? 'Presenter',
|
|
71625
72448
|
role: 'owner',
|
|
71626
72449
|
};
|
|
71627
72450
|
this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
|
|
71628
|
-
|
|
71629
|
-
getTemplateElements: () => host.getTemplateElements(),
|
|
71630
|
-
});
|
|
72451
|
+
this.connectWithBaseline(collabConfig);
|
|
71631
72452
|
host.emitStart(collabConfig);
|
|
71632
72453
|
}
|
|
71633
72454
|
onBroadcastStop() {
|
|
@@ -76033,6 +76854,14 @@ class PowerPointViewerComponent {
|
|
|
76033
76854
|
...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
76034
76855
|
zoomPercent = computed(() => Math.round(this.zoom() * 100), /* @ts-ignore */
|
|
76035
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;
|
|
76036
76865
|
/** Fullscreen presentation-mode overlay visibility. */
|
|
76037
76866
|
presenting = signal(false, /* @ts-ignore */
|
|
76038
76867
|
...(ngDevMode ? [{ debugName: "presenting" }] : /* istanbul ignore next */ []));
|
|
@@ -76297,6 +77126,47 @@ class PowerPointViewerComponent {
|
|
|
76297
77126
|
effect(() => {
|
|
76298
77127
|
this.session.syncHostConfig(this.collaboration());
|
|
76299
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
|
+
});
|
|
76300
77170
|
// Hand the export/print orchestrator the live navigation signal + deck
|
|
76301
77171
|
// accessors + stage resolver so it can flip the stage and capture slides.
|
|
76302
77172
|
this.xport.bind({
|
|
@@ -76318,6 +77188,10 @@ class PowerPointViewerComponent {
|
|
|
76318
77188
|
authorName: () => this.authorName(),
|
|
76319
77189
|
shareDefaults: () => this.shareDefaults(),
|
|
76320
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(),
|
|
76321
77195
|
emitStart: (config) => this.startCollaboration.emit(config),
|
|
76322
77196
|
emitStop: () => this.stopCollaboration.emit(),
|
|
76323
77197
|
});
|
|
@@ -76373,6 +77247,40 @@ class PowerPointViewerComponent {
|
|
|
76373
77247
|
goNext() {
|
|
76374
77248
|
this.goTo(this.activeSlideIndex() + 1);
|
|
76375
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
|
+
}
|
|
76376
77284
|
// ── Theme gallery (Design tab) ─────────────────────────────────────────────
|
|
76377
77285
|
/**
|
|
76378
77286
|
* Apply a built-in theme preset to the whole deck.
|
|
@@ -77010,7 +77918,7 @@ class PowerPointViewerComponent {
|
|
|
77010
77918
|
</nav>
|
|
77011
77919
|
}
|
|
77012
77920
|
|
|
77013
|
-
<main class="pptx-ng-main" #mainEl>
|
|
77921
|
+
<main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
|
|
77014
77922
|
<pptx-slide-canvas
|
|
77015
77923
|
[slide]="activeSlide()"
|
|
77016
77924
|
[canvasSize]="loader.canvasSize()"
|
|
@@ -77048,7 +77956,22 @@ class PowerPointViewerComponent {
|
|
|
77048
77956
|
(tableChange)="onTableChange($event)"
|
|
77049
77957
|
/>
|
|
77050
77958
|
@if (collab.connected()) {
|
|
77051
|
-
<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>
|
|
77052
77975
|
}
|
|
77053
77976
|
@if (showNotes() && !mobile.isMobile()) {
|
|
77054
77977
|
<aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
|
|
@@ -77314,6 +78237,7 @@ class PowerPointViewerComponent {
|
|
|
77314
78237
|
[connected]="collab.connected()"
|
|
77315
78238
|
[userCount]="collab.connectedCount()"
|
|
77316
78239
|
[shareUrl]="session.shareUrl()"
|
|
78240
|
+
[p2p]="session.activeSessionP2p()"
|
|
77317
78241
|
[defaults]="session.shareDialogDefaults()"
|
|
77318
78242
|
(start)="session.onShareStart($event)"
|
|
77319
78243
|
(stop)="session.onShareStop()"
|
|
@@ -77326,6 +78250,7 @@ class PowerPointViewerComponent {
|
|
|
77326
78250
|
[connected]="collab.connected()"
|
|
77327
78251
|
[viewerCount]="collab.presence().length"
|
|
77328
78252
|
[viewerUrl]="session.broadcastViewerUrl()"
|
|
78253
|
+
[p2p]="session.activeSessionP2p()"
|
|
77329
78254
|
[defaults]="{ serverUrl: shareDefaults()?.serverUrl }"
|
|
77330
78255
|
(start)="session.onBroadcastStart($event)"
|
|
77331
78256
|
(stop)="session.onBroadcastStop()"
|
|
@@ -77433,7 +78358,7 @@ class PowerPointViewerComponent {
|
|
|
77433
78358
|
/>
|
|
77434
78359
|
}
|
|
77435
78360
|
</div>
|
|
77436
|
-
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SlideCanvasComponent, selector: "pptx-slide-canvas", inputs: ["slide", "canvasSize", "mediaDataUrls", "zoom", "editable", "showGrid", "showRulers", "showGuides", "snapToGrid", "snapToGuides", "autoFit", "interactive", "selectedIds", "editingId", "editTemplateMode", "templateElements", "drawTool", "drawColor", "drawWidth"], outputs: ["elementSelect", "backgroundClick", "transformStart", "transformUpdate", "contextMenu", "textEditStart", "textCommit", "textCancel", "textFormat", "rotateUpdate", "marqueeSelect", "inkStrokeComplete", "eraserHit", "cellCommit", "tableChange"] }, { kind: "component", type: PresentationOverlayComponent, selector: "pptx-presentation-overlay", inputs: ["slides", "canvasSize", "mediaDataUrls", "startIndex"], outputs: ["indexChange", "closed", "annotationsExit"] }, { kind: "component", type: PresenterViewComponent, selector: "pptx-presenter-view", inputs: ["slides", "currentSlideIndex", "canvasSize", "templateElements", "mediaDataUrls", "presentationStartTime", "isAudienceWindowOpen"], outputs: ["movePresentationSlide", "exit", "openAudienceWindow", "closeAudienceWindow"] }, { kind: "component", type: MobilePresenterViewComponent, selector: "pptx-mobile-presenter-view", inputs: ["slides", "currentSlideIndex", "canvasSize", "templateElements", "mediaDataUrls", "presentationStartTime"], outputs: ["movePresentationSlide", "exit"] }, { kind: "component", type: SlideSorterOverlayComponent, selector: "pptx-slide-sorter-overlay", inputs: ["slides", "canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["select", "closed"] }, { kind: "component", type: FindBarComponent, selector: "pptx-find-bar", inputs: ["slides"], outputs: ["navigate", "closed"] }, { kind: "component", type: FindReplaceBarComponent, selector: "pptx-find-replace-bar", inputs: ["matchCount", "matchIndex"], outputs: ["find", "navigate", "replaceOne", "replaceAll", "close"] }, { kind: "component", type: InspectorPanelComponent, selector: "pptx-inspector-panel", inputs: ["element", "slideIndex"] }, { kind: "component", type: SlidesPanelComponent, selector: "pptx-slides-panel", inputs: ["canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["select"] }, { kind: "component", type: StatusBarComponent, selector: "pptx-status-bar", inputs: ["slideIndex", "slideCount", "canEdit", "dirty", "notesOpen", "zoomPercent", "sorterActive", "presenting"], outputs: ["toggleNotes", "normalView", "openSorter", "slideShow", "zoomIn", "zoomOut", "zoomReset"] }, { kind: "component", type: EditorContextMenuComponent, selector: "pptx-editor-context-menu", inputs: ["x", "y", "slideIndex"], outputs: ["closed"] }, { kind: "component", type: ExportProgressModalComponent, selector: "pptx-export-progress-modal", inputs: ["open", "title", "progress", "statusMessage"], outputs: ["cancel"] }, { kind: "component", type: CommentsPanelComponent, selector: "pptx-comments-panel", inputs: ["comments", "authorName"], outputs: ["add", "remove", "resolve"] }, { kind: "component", type: SignaturesPanelComponent, selector: "pptx-signatures-panel", inputs: ["signatures"] }, { kind: "component", type: AccessibilityPanelComponent, selector: "pptx-accessibility-panel", inputs: ["issues"], outputs: ["selectSlide"] }, { kind: "component", type: CollaborationCursorsComponent, selector: "pptx-collaboration-cursors", inputs: ["cursors", "zoom"] }, { kind: "component", type: PropertiesDialogComponent, selector: "pptx-properties-dialog", inputs: ["open", "properties"], outputs: ["save", "close"] }, { kind: "component", type: HyperlinkDialogComponent, selector: "pptx-hyperlink-dialog", inputs: ["open", "element"], outputs: ["save", "close"] }, { kind: "component", type: PrintDialogComponent, selector: "pptx-print-dialog", inputs: ["slides", "activeSlideIndex", "defaultSlidesPerPage", "defaultFrameSlides"], outputs: ["print", "cancel"] }, { kind: "component", type: ShareDialogComponent, selector: "pptx-share-dialog", inputs: ["open", "defaults", "active", "connected", "userCount", "shareUrl"], outputs: ["start", "stop", "close"] }, { kind: "component", type: BroadcastDialogComponent, selector: "pptx-broadcast-dialog", inputs: ["open", "defaults", "active", "connected", "viewerCount", "viewerUrl"], outputs: ["start", "stop", "close"] }, { kind: "component", type: MobileBottomBarComponent, selector: "pptx-mobile-bottom-bar", inputs: ["slideCount", "commentCount", "activeSheet"], outputs: ["openSlides", "insert", "openFormat", "openComments", "notes"] }, { kind: "component", type: MobileMenuSheetComponent, selector: "pptx-mobile-menu-sheet", inputs: ["open", "slideCount", "exporting", "showNotes", "canEdit"], outputs: ["closed", "openFind", "openSorter", "toggleNotes", "insertText", "present", "openFile", "savePptx", "exportPng", "exportPdf", "exportGif", "exportVideo", "print"] }, { kind: "component", type: MobileSlidesSheetComponent, selector: "pptx-mobile-slides-sheet", inputs: ["open", "slides", "canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["closed", "jumpToSlide"] }, { kind: "component", type: MobileToolbarComponent, selector: "pptx-mobile-toolbar", inputs: ["canUndo", "canRedo", "canPresent", "canEdit", "menuOpen"], outputs: ["toggleMenu", "undo", "redo", "save", "present"] }, { kind: "component", type: NotesPanelComponent, selector: "pptx-notes-panel", inputs: ["slide"], outputs: ["update"] }, { kind: "component", type: RibbonComponent, selector: "pptx-ribbon", inputs: ["slideIndex", "slideCount", "canEdit", "selectedElement", "zoomPercent", "formatPainterActive", "canActivateFormatPainter", "exporting", "showGrid", "showRulers", "showGuides", "snapToGrid", "eyedropperActive", "themeGalleryOpen", "sidebarCollapsed", "inspectorOpen", "commentsOpen", "commentCount", "findOpen", "collabConnected", "connectedCount"], outputs: ["prev", "next", "zoomIn", "zoomOut", "zoomReset", "find", "present", "presenter", "share", "broadcast", "openFile", "save", "toggleSidebar", "signatures", "info", "print", "comments", "a11y", "link", "openSorter", "toggleNotes", "toggleFormatPainter", "exportPng", "exportPdf", "exportGif", "exportVideo", "replace", "toggleInspector", "drawToolChange", "toggleThemeGallery", "toggleGrid", "toggleRulers", "toggleGuides", "toggleSelectionPane", "openCustomShows", "toggleSnapToGrid", "toggleEyedropper", "openSmartArtDialog", "openEquationDialog", "openSetUpSlideShow", "openCompare", "openPassword", "openFontEmbedding", "openVersionHistory", "openShortcuts"] }, { kind: "component", type: ThemeGalleryComponent, selector: "pptx-theme-gallery", inputs: ["open", "activeName"], outputs: ["applyTheme", "close"] }, { kind: "component", type: SelectionPaneComponent, selector: "pptx-selection-pane", inputs: ["elements", "selectedIds"], outputs: ["selectElement", "bringForward", "sendBackward", "toggleHidden"] }, { kind: "component", type: CustomShowsComponent, selector: "pptx-custom-shows", inputs: ["open", "slides", "customShows", "activeCustomShowId"], outputs: ["create", "remove", "update", "setActive", "close"] }, { kind: "component", type: InsertSmartArtDialogComponent, selector: "pptx-insert-smart-art-dialog", inputs: ["open"], outputs: ["close", "insert"] }, { kind: "component", type: ViewerExtraDialogsComponent, selector: "pptx-viewer-extra-dialogs", inputs: ["activeSlideIndex", "selectedElementId", "filePath", "customShows"], outputs: ["restoreContent"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
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 });
|
|
77437
78362
|
}
|
|
77438
78363
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: PowerPointViewerComponent, decorators: [{
|
|
77439
78364
|
type: Component,
|
|
@@ -77483,6 +78408,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77483
78408
|
SignaturesPanelComponent,
|
|
77484
78409
|
AccessibilityPanelComponent,
|
|
77485
78410
|
CollaborationCursorsComponent,
|
|
78411
|
+
RemoteSelectionOverlayComponent,
|
|
78412
|
+
FollowModeBarComponent,
|
|
77486
78413
|
PropertiesDialogComponent,
|
|
77487
78414
|
HyperlinkDialogComponent,
|
|
77488
78415
|
PrintDialogComponent,
|
|
@@ -77627,7 +78554,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77627
78554
|
</nav>
|
|
77628
78555
|
}
|
|
77629
78556
|
|
|
77630
|
-
<main class="pptx-ng-main" #mainEl>
|
|
78557
|
+
<main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
|
|
77631
78558
|
<pptx-slide-canvas
|
|
77632
78559
|
[slide]="activeSlide()"
|
|
77633
78560
|
[canvasSize]="loader.canvasSize()"
|
|
@@ -77665,7 +78592,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77665
78592
|
(tableChange)="onTableChange($event)"
|
|
77666
78593
|
/>
|
|
77667
78594
|
@if (collab.connected()) {
|
|
77668
|
-
<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>
|
|
77669
78611
|
}
|
|
77670
78612
|
@if (showNotes() && !mobile.isMobile()) {
|
|
77671
78613
|
<aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
|
|
@@ -77931,6 +78873,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77931
78873
|
[connected]="collab.connected()"
|
|
77932
78874
|
[userCount]="collab.connectedCount()"
|
|
77933
78875
|
[shareUrl]="session.shareUrl()"
|
|
78876
|
+
[p2p]="session.activeSessionP2p()"
|
|
77934
78877
|
[defaults]="session.shareDialogDefaults()"
|
|
77935
78878
|
(start)="session.onShareStart($event)"
|
|
77936
78879
|
(stop)="session.onShareStop()"
|
|
@@ -77943,6 +78886,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
77943
78886
|
[connected]="collab.connected()"
|
|
77944
78887
|
[viewerCount]="collab.presence().length"
|
|
77945
78888
|
[viewerUrl]="session.broadcastViewerUrl()"
|
|
78889
|
+
[p2p]="session.activeSessionP2p()"
|
|
77946
78890
|
[defaults]="{ serverUrl: shareDefaults()?.serverUrl }"
|
|
77947
78891
|
(start)="session.onBroadcastStart($event)"
|
|
77948
78892
|
(stop)="session.onBroadcastStop()"
|
|
@@ -78479,6 +79423,9 @@ const translationsEn = {
|
|
|
78479
79423
|
'pptx.autosave.justNow': 'just now',
|
|
78480
79424
|
'pptx.autosave.oneMinAgo': '1 min ago',
|
|
78481
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',
|
|
78482
79429
|
// Toolbar
|
|
78483
79430
|
'pptx.toolbar.toggleSlidesPanel': 'Toggle slides panel',
|
|
78484
79431
|
'pptx.toolbar.undo': 'Undo',
|
|
@@ -78597,7 +79544,17 @@ const translationsEn = {
|
|
|
78597
79544
|
'pptx.collaboration.youLabel': '{{name}} (you)',
|
|
78598
79545
|
'pptx.collaboration.usersConnected': '{{count}} user(s) connected',
|
|
78599
79546
|
'pptx.collaboration.moreUsers': '{{count}} more user(s)',
|
|
79547
|
+
'pptx.collaboration.onePersonHere': '1 person here',
|
|
79548
|
+
'pptx.collaboration.peopleHere': '{{count}} people here',
|
|
78600
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}}',
|
|
78601
79558
|
// Share dialog
|
|
78602
79559
|
'pptx.share.title': 'Share Presentation',
|
|
78603
79560
|
'pptx.share.collaborationActive': 'Collaboration Active',
|