pptx-angular-viewer 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -22867,7 +22867,7 @@ function assignUserColor(seed, palette = CURSOR_PALETTE) {
|
|
|
22867
22867
|
* truncating, the result is exactly `maxChars` characters including a trailing
|
|
22868
22868
|
* `...` ellipsis.
|
|
22869
22869
|
*/
|
|
22870
|
-
function formatCursorLabel
|
|
22870
|
+
function formatCursorLabel(userName, maxChars = MAX_LABEL_CHARS) {
|
|
22871
22871
|
if (userName.length <= maxChars) {
|
|
22872
22872
|
return userName;
|
|
22873
22873
|
}
|
|
@@ -22998,6 +22998,175 @@ function asSelectionIds(value) {
|
|
|
22998
22998
|
return value.filter((entry) => typeof entry === 'string');
|
|
22999
22999
|
}
|
|
23000
23000
|
|
|
23001
|
+
/**
|
|
23002
|
+
* Create a throttled presence publisher bound to an awareness instance. The
|
|
23003
|
+
* initial presence is announced synchronously so peers learn of us at once.
|
|
23004
|
+
*/
|
|
23005
|
+
function createPresencePublisher(awareness, identity) {
|
|
23006
|
+
const local = { activeSlideIndex: 0, cursorX: 0, cursorY: 0 };
|
|
23007
|
+
// Starts at 0 so the first real update publishes immediately (the initial
|
|
23008
|
+
// announce below does not consume the throttle budget).
|
|
23009
|
+
let lastBroadcast = 0;
|
|
23010
|
+
let pending = null;
|
|
23011
|
+
function publish() {
|
|
23012
|
+
awareness.setLocalStateField('presence', {
|
|
23013
|
+
userName: identity.userName,
|
|
23014
|
+
userColor: identity.userColor,
|
|
23015
|
+
userAvatar: identity.userAvatar,
|
|
23016
|
+
role: identity.role,
|
|
23017
|
+
activeSlideIndex: local.activeSlideIndex,
|
|
23018
|
+
cursorX: local.cursorX,
|
|
23019
|
+
cursorY: local.cursorY,
|
|
23020
|
+
selectedElementId: local.selectedElementId,
|
|
23021
|
+
lastUpdated: new Date().toISOString(),
|
|
23022
|
+
});
|
|
23023
|
+
}
|
|
23024
|
+
function update(patch) {
|
|
23025
|
+
Object.assign(local, patch);
|
|
23026
|
+
const elapsed = Date.now() - lastBroadcast;
|
|
23027
|
+
if (elapsed >= BROADCAST_THROTTLE_MS) {
|
|
23028
|
+
if (pending !== null) {
|
|
23029
|
+
clearTimeout(pending);
|
|
23030
|
+
pending = null;
|
|
23031
|
+
}
|
|
23032
|
+
lastBroadcast = Date.now();
|
|
23033
|
+
publish();
|
|
23034
|
+
}
|
|
23035
|
+
else if (pending === null) {
|
|
23036
|
+
pending = setTimeout(() => {
|
|
23037
|
+
pending = null;
|
|
23038
|
+
lastBroadcast = Date.now();
|
|
23039
|
+
publish();
|
|
23040
|
+
}, BROADCAST_THROTTLE_MS - elapsed);
|
|
23041
|
+
}
|
|
23042
|
+
}
|
|
23043
|
+
function flush() {
|
|
23044
|
+
if (pending !== null) {
|
|
23045
|
+
clearTimeout(pending);
|
|
23046
|
+
pending = null;
|
|
23047
|
+
}
|
|
23048
|
+
lastBroadcast = Date.now();
|
|
23049
|
+
publish();
|
|
23050
|
+
}
|
|
23051
|
+
function dispose() {
|
|
23052
|
+
if (pending !== null) {
|
|
23053
|
+
clearTimeout(pending);
|
|
23054
|
+
pending = null;
|
|
23055
|
+
}
|
|
23056
|
+
}
|
|
23057
|
+
// Announce initial presence immediately (without consuming the throttle).
|
|
23058
|
+
publish();
|
|
23059
|
+
return { update, flush, dispose };
|
|
23060
|
+
}
|
|
23061
|
+
|
|
23062
|
+
/**
|
|
23063
|
+
* collaboration-assets.ts: binary payload sync for the pptx-viewer
|
|
23064
|
+
* collaboration stack.
|
|
23065
|
+
*
|
|
23066
|
+
* Large binary element fields (media/OLE/3D-model payloads) are synced once
|
|
23067
|
+
* via a separate `pptx:assets` Y.Map keyed by `${elementId}:${fieldName}`,
|
|
23068
|
+
* instead of being re-embedded as base64 inside every element write. This
|
|
23069
|
+
* avoids re-transmitting multi-MB payloads on every unrelated edit to the
|
|
23070
|
+
* same element: `writeAssetFields` only touches the assets map when the
|
|
23071
|
+
* stored payload actually differs from the incoming value.
|
|
23072
|
+
*
|
|
23073
|
+
* Known, accepted cost: asset entries are NOT garbage-collected when the
|
|
23074
|
+
* element that referenced them is deleted outright (only an explicit
|
|
23075
|
+
* field-clear removes its entry, via the `else` branch below). A long-lived
|
|
23076
|
+
* room's `pptx:assets` map can only grow. No sweep is implemented in this
|
|
23077
|
+
* pass; correct GC across concurrent/offline peers is a separate problem.
|
|
23078
|
+
*/
|
|
23079
|
+
const YDOC_ASSETS_KEY = 'pptx:assets';
|
|
23080
|
+
/**
|
|
23081
|
+
* Element fields large/binary enough to route through the asset map instead
|
|
23082
|
+
* of being embedded inline as a scalar or complex JSON-blob field.
|
|
23083
|
+
*/
|
|
23084
|
+
const ASSET_ELEMENT_FIELDS = new Set([
|
|
23085
|
+
'mediaData',
|
|
23086
|
+
'posterFrameData',
|
|
23087
|
+
'oleEmbeddedData',
|
|
23088
|
+
'previewImageData',
|
|
23089
|
+
'modelData',
|
|
23090
|
+
]);
|
|
23091
|
+
const ASSET_REF_KEYS = {
|
|
23092
|
+
mediaData: '_mdRef',
|
|
23093
|
+
posterFrameData: '_pfdRef',
|
|
23094
|
+
oleEmbeddedData: '_oedRef',
|
|
23095
|
+
previewImageData: '_pidRef',
|
|
23096
|
+
modelData: '_moRef',
|
|
23097
|
+
};
|
|
23098
|
+
const REV_ASSET_REF_KEYS = Object.fromEntries(Object.entries(ASSET_REF_KEYS).map(([field, ref]) => [ref, field]));
|
|
23099
|
+
function assetKey(elementId, fieldName) {
|
|
23100
|
+
return `${elementId}:${fieldName}`;
|
|
23101
|
+
}
|
|
23102
|
+
function getAssetsMap(ydoc) {
|
|
23103
|
+
return ydoc.getMap(YDOC_ASSETS_KEY);
|
|
23104
|
+
}
|
|
23105
|
+
/** True when `key` is one of the element-map ref-pointer keys this module owns. */
|
|
23106
|
+
function isAssetRefKey(key) {
|
|
23107
|
+
return key in REV_ASSET_REF_KEYS;
|
|
23108
|
+
}
|
|
23109
|
+
/**
|
|
23110
|
+
* Writes any present asset-routed fields from `rec` into `assets`, storing
|
|
23111
|
+
* only a small ref pointer (not the payload) on the element's own `ymap`.
|
|
23112
|
+
* Never reads from `ymap` itself, so it's safe to call on a brand-new,
|
|
23113
|
+
* not-yet-integrated map (Yjs throws on reading an unintegrated type) - use
|
|
23114
|
+
* this from a create/write path. `assets` is always a root-level (already
|
|
23115
|
+
* integrated) map, so the identical-payload skip-write check is still safe.
|
|
23116
|
+
*/
|
|
23117
|
+
function writeAssetFields(elementId, rec, ymap, assets) {
|
|
23118
|
+
for (const field of ASSET_ELEMENT_FIELDS) {
|
|
23119
|
+
const value = rec[field];
|
|
23120
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
23121
|
+
const key = assetKey(elementId, field);
|
|
23122
|
+
if (assets.get(key) !== value) {
|
|
23123
|
+
assets.set(key, value);
|
|
23124
|
+
}
|
|
23125
|
+
ymap.set(ASSET_REF_KEYS[field], key);
|
|
23126
|
+
}
|
|
23127
|
+
}
|
|
23128
|
+
}
|
|
23129
|
+
/**
|
|
23130
|
+
* Reconcile variant of {@link writeAssetFields} for an `ymap` that is
|
|
23131
|
+
* already integrated (an existing live element being updated in place):
|
|
23132
|
+
* additionally reads the current ref to skip a redundant set, and clears
|
|
23133
|
+
* the ref + its `assets` entry when the field was cleared.
|
|
23134
|
+
*/
|
|
23135
|
+
function reconcileAssetFields(elementId, rec, ymap, assets) {
|
|
23136
|
+
for (const field of ASSET_ELEMENT_FIELDS) {
|
|
23137
|
+
const refKey = ASSET_REF_KEYS[field];
|
|
23138
|
+
const value = rec[field];
|
|
23139
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
23140
|
+
const key = assetKey(elementId, field);
|
|
23141
|
+
if (assets.get(key) !== value) {
|
|
23142
|
+
assets.set(key, value);
|
|
23143
|
+
}
|
|
23144
|
+
if (ymap.get(refKey) !== key) {
|
|
23145
|
+
ymap.set(refKey, key);
|
|
23146
|
+
}
|
|
23147
|
+
}
|
|
23148
|
+
else {
|
|
23149
|
+
const existingRef = ymap.get(refKey);
|
|
23150
|
+
if (typeof existingRef === 'string') {
|
|
23151
|
+
ymap.delete(refKey);
|
|
23152
|
+
assets.delete(existingRef);
|
|
23153
|
+
}
|
|
23154
|
+
}
|
|
23155
|
+
}
|
|
23156
|
+
}
|
|
23157
|
+
/** Rehydrates any asset-routed fields referenced by `ymap` onto `element`. */
|
|
23158
|
+
function readAssetFields(ymap, assets, element) {
|
|
23159
|
+
for (const [refKey, field] of Object.entries(REV_ASSET_REF_KEYS)) {
|
|
23160
|
+
const ref = ymap.get(refKey);
|
|
23161
|
+
if (typeof ref === 'string') {
|
|
23162
|
+
const value = assets.get(ref);
|
|
23163
|
+
if (typeof value === 'string') {
|
|
23164
|
+
element[field] = value;
|
|
23165
|
+
}
|
|
23166
|
+
}
|
|
23167
|
+
}
|
|
23168
|
+
}
|
|
23169
|
+
|
|
23001
23170
|
/**
|
|
23002
23171
|
* collaboration-text-codec.ts: TextSegment[] <-> Y.Text delta codec used by the
|
|
23003
23172
|
* collaboration sync layer. Split out of collaboration-sync.ts to keep both
|
|
@@ -23266,6 +23435,9 @@ const SCALAR_ELEMENT_KEYS = new Set([
|
|
|
23266
23435
|
'width',
|
|
23267
23436
|
'height',
|
|
23268
23437
|
'rotation',
|
|
23438
|
+
'shapeId',
|
|
23439
|
+
'skewX',
|
|
23440
|
+
'skewY',
|
|
23269
23441
|
'flipHorizontal',
|
|
23270
23442
|
'flipVertical',
|
|
23271
23443
|
'hidden',
|
|
@@ -23274,14 +23446,66 @@ const SCALAR_ELEMENT_KEYS = new Set([
|
|
|
23274
23446
|
'name',
|
|
23275
23447
|
'altText',
|
|
23276
23448
|
'shapeType',
|
|
23277
|
-
'placeholder',
|
|
23278
23449
|
'imagePath',
|
|
23279
23450
|
'imageData',
|
|
23280
|
-
'
|
|
23281
|
-
'
|
|
23282
|
-
'
|
|
23451
|
+
'svgData',
|
|
23452
|
+
'svgPath',
|
|
23453
|
+
'cropLeft',
|
|
23454
|
+
'cropTop',
|
|
23455
|
+
'cropRight',
|
|
23456
|
+
'cropBottom',
|
|
23457
|
+
'tileOffsetX',
|
|
23458
|
+
'tileOffsetY',
|
|
23459
|
+
'tileScaleX',
|
|
23460
|
+
'tileScaleY',
|
|
23461
|
+
'tileFlip',
|
|
23462
|
+
'tileAlignment',
|
|
23463
|
+
'pathData',
|
|
23464
|
+
'pathWidth',
|
|
23465
|
+
'pathHeight',
|
|
23283
23466
|
'mediaType',
|
|
23284
23467
|
'mediaPath',
|
|
23468
|
+
'mediaMimeType',
|
|
23469
|
+
'trimStartMs',
|
|
23470
|
+
'trimEndMs',
|
|
23471
|
+
'posterFramePath',
|
|
23472
|
+
'fullScreen',
|
|
23473
|
+
'loop',
|
|
23474
|
+
'fadeInDuration',
|
|
23475
|
+
'fadeOutDuration',
|
|
23476
|
+
'volume',
|
|
23477
|
+
'autoPlay',
|
|
23478
|
+
'playAcrossSlides',
|
|
23479
|
+
'hideWhenNotPlaying',
|
|
23480
|
+
'playbackSpeed',
|
|
23481
|
+
'mediaMissing',
|
|
23482
|
+
'isLinked',
|
|
23483
|
+
'oleTarget',
|
|
23484
|
+
'oleProgId',
|
|
23485
|
+
'oleName',
|
|
23486
|
+
'oleClsId',
|
|
23487
|
+
'oleObjectType',
|
|
23488
|
+
'oleFileExtension',
|
|
23489
|
+
'fileName',
|
|
23490
|
+
'externalPath',
|
|
23491
|
+
'previewImage',
|
|
23492
|
+
'oleShowAsIcon',
|
|
23493
|
+
'oleImgW',
|
|
23494
|
+
'oleImgH',
|
|
23495
|
+
'oleEmbeddedFileName',
|
|
23496
|
+
'oleEmbeddedMimeType',
|
|
23497
|
+
'oleEmbeddedByteSize',
|
|
23498
|
+
'inkPaths',
|
|
23499
|
+
'inkColors',
|
|
23500
|
+
'inkWidths',
|
|
23501
|
+
'inkOpacities',
|
|
23502
|
+
'inkTool',
|
|
23503
|
+
'zoomType',
|
|
23504
|
+
'targetSlideIndex',
|
|
23505
|
+
'targetSectionId',
|
|
23506
|
+
'modelPath',
|
|
23507
|
+
'modelMimeType',
|
|
23508
|
+
'posterImage',
|
|
23285
23509
|
'linkedTxbxId',
|
|
23286
23510
|
'linkedTxbxSeq',
|
|
23287
23511
|
'promptText',
|
|
@@ -23294,26 +23518,35 @@ const COMPLEX_ELEMENT_FIELDS = {
|
|
|
23294
23518
|
tableData: '_td',
|
|
23295
23519
|
chartData: '_cd',
|
|
23296
23520
|
smartArtData: '_smad',
|
|
23297
|
-
connectionStart: '_cs',
|
|
23298
|
-
connectionEnd: '_ce',
|
|
23299
|
-
animations: '_an',
|
|
23300
|
-
nativeAnimations: '_na',
|
|
23301
23521
|
children: '_ch',
|
|
23302
23522
|
paragraphIndents: '_pi',
|
|
23303
23523
|
rawXml: '_rx',
|
|
23524
|
+
extLstXml: '_elx',
|
|
23304
23525
|
actionClick: '_ac',
|
|
23305
23526
|
actionHover: '_av',
|
|
23306
23527
|
locks: '_lk',
|
|
23307
23528
|
imageEffects: '_ie',
|
|
23308
23529
|
cropShape: '_cr',
|
|
23309
|
-
|
|
23530
|
+
bookmarks: '_mb',
|
|
23310
23531
|
captionTracks: '_ct',
|
|
23532
|
+
metadata: '_md',
|
|
23533
|
+
groupFill: '_gf',
|
|
23534
|
+
inkPointPressures: '_ipp',
|
|
23535
|
+
inkStrokes: '_cis',
|
|
23536
|
+
extensionXml: '_ext',
|
|
23537
|
+
customGeometryPaths: '_cgp',
|
|
23538
|
+
customGeometryRawData: '_cgr',
|
|
23539
|
+
customGeometryAdjustHandlesXY: '_cgx',
|
|
23540
|
+
customGeometryAdjustHandlesPolar: '_cgo',
|
|
23541
|
+
customGeometryConnectionSites: '_cgc',
|
|
23542
|
+
customGeometryTextRect: '_cgt',
|
|
23311
23543
|
};
|
|
23312
23544
|
const REV_COMPLEX_ELEMENT = Object.fromEntries(Object.entries(COMPLEX_ELEMENT_FIELDS).map(([k, v]) => [v, k]));
|
|
23313
23545
|
const SCALAR_SLIDE_KEYS = new Set([
|
|
23314
23546
|
'id',
|
|
23315
23547
|
'rId',
|
|
23316
23548
|
'sourceSlideId',
|
|
23549
|
+
'name',
|
|
23317
23550
|
'layoutPath',
|
|
23318
23551
|
'layoutName',
|
|
23319
23552
|
'slideNumber',
|
|
@@ -23323,7 +23556,9 @@ const SCALAR_SLIDE_KEYS = new Set([
|
|
|
23323
23556
|
'backgroundColor',
|
|
23324
23557
|
'backgroundImage',
|
|
23325
23558
|
'backgroundGradient',
|
|
23559
|
+
'backgroundShadeToTitle',
|
|
23326
23560
|
'notes',
|
|
23561
|
+
'notesCSldName',
|
|
23327
23562
|
'backgroundShowAnimation',
|
|
23328
23563
|
'showMasterShapes',
|
|
23329
23564
|
'isDirty',
|
|
@@ -23334,6 +23569,8 @@ const COMPLEX_SLIDE_FIELDS = {
|
|
|
23334
23569
|
nativeAnimations: '_na',
|
|
23335
23570
|
rawTiming: '_rt',
|
|
23336
23571
|
notesSegments: '_ns',
|
|
23572
|
+
notesShapes: '_nsh',
|
|
23573
|
+
notesClrMapOverride: '_ncm',
|
|
23337
23574
|
comments: '_cm',
|
|
23338
23575
|
warnings: '_wa',
|
|
23339
23576
|
rawXml: '_rx',
|
|
@@ -23341,15 +23578,17 @@ const COMPLEX_SLIDE_FIELDS = {
|
|
|
23341
23578
|
guides: '_gu',
|
|
23342
23579
|
customerData: '_cu',
|
|
23343
23580
|
activeXControls: '_ax',
|
|
23581
|
+
backgroundPattern: '_bp',
|
|
23582
|
+
headerFooterFlags: '_hff',
|
|
23344
23583
|
};
|
|
23345
23584
|
const REV_COMPLEX_SLIDE = Object.fromEntries(Object.entries(COMPLEX_SLIDE_FIELDS).map(([k, v]) => [v, k]));
|
|
23346
23585
|
// ---------------------------------------------------------------------------
|
|
23347
23586
|
// Element serialization
|
|
23348
23587
|
// ---------------------------------------------------------------------------
|
|
23349
|
-
function writeElementToYMap(element, ymap, factories) {
|
|
23588
|
+
function writeElementToYMap(element, ymap, factories, assets) {
|
|
23350
23589
|
const rec = element;
|
|
23351
23590
|
for (const [key, value] of Object.entries(rec)) {
|
|
23352
|
-
if (value === undefined) {
|
|
23591
|
+
if (value === undefined || ASSET_ELEMENT_FIELDS.has(key)) {
|
|
23353
23592
|
continue;
|
|
23354
23593
|
}
|
|
23355
23594
|
if (SCALAR_ELEMENT_KEYS.has(key)) {
|
|
@@ -23366,8 +23605,9 @@ function writeElementToYMap(element, ymap, factories) {
|
|
|
23366
23605
|
ymap.set(COMPLEX_ELEMENT_FIELDS[key], JSON.stringify(value));
|
|
23367
23606
|
}
|
|
23368
23607
|
}
|
|
23608
|
+
writeAssetFields(rec.id, rec, ymap, assets);
|
|
23369
23609
|
}
|
|
23370
|
-
function readElementFromYMap(ymap) {
|
|
23610
|
+
function readElementFromYMap(ymap, assets) {
|
|
23371
23611
|
const element = {};
|
|
23372
23612
|
ymap.forEach((value, key) => {
|
|
23373
23613
|
if (key === 'textBody') {
|
|
@@ -23375,6 +23615,9 @@ function readElementFromYMap(ymap) {
|
|
|
23375
23615
|
element.textSegments = decodeTextBody(value);
|
|
23376
23616
|
}
|
|
23377
23617
|
}
|
|
23618
|
+
else if (isAssetRefKey(key)) {
|
|
23619
|
+
// handled by readAssetFields below; not a literal PptxElement field
|
|
23620
|
+
}
|
|
23378
23621
|
else if (REV_COMPLEX_ELEMENT[key]) {
|
|
23379
23622
|
try {
|
|
23380
23623
|
element[REV_COMPLEX_ELEMENT[key]] = JSON.parse(value);
|
|
@@ -23387,12 +23630,13 @@ function readElementFromYMap(ymap) {
|
|
|
23387
23630
|
element[key] = value;
|
|
23388
23631
|
}
|
|
23389
23632
|
});
|
|
23633
|
+
readAssetFields(ymap, assets, element);
|
|
23390
23634
|
return element;
|
|
23391
23635
|
}
|
|
23392
23636
|
// ---------------------------------------------------------------------------
|
|
23393
23637
|
// Slide serialization
|
|
23394
23638
|
// ---------------------------------------------------------------------------
|
|
23395
|
-
function writeSlideToYMap(slide, ymap, factories) {
|
|
23639
|
+
function writeSlideToYMap(slide, ymap, factories, assets) {
|
|
23396
23640
|
const rec = slide;
|
|
23397
23641
|
for (const key of SCALAR_SLIDE_KEYS) {
|
|
23398
23642
|
if (rec[key] !== undefined) {
|
|
@@ -23407,12 +23651,12 @@ function writeSlideToYMap(slide, ymap, factories) {
|
|
|
23407
23651
|
const elemArr = factories.createArray();
|
|
23408
23652
|
for (const el of slide.elements) {
|
|
23409
23653
|
const elemMap = factories.createMap();
|
|
23410
|
-
writeElementToYMap(el, elemMap, factories);
|
|
23654
|
+
writeElementToYMap(el, elemMap, factories, assets);
|
|
23411
23655
|
elemArr.push([elemMap]);
|
|
23412
23656
|
}
|
|
23413
23657
|
ymap.set('elements', elemArr);
|
|
23414
23658
|
}
|
|
23415
|
-
function readSlideFromYMap(ymap) {
|
|
23659
|
+
function readSlideFromYMap(ymap, assets) {
|
|
23416
23660
|
const slide = {};
|
|
23417
23661
|
for (const key of SCALAR_SLIDE_KEYS) {
|
|
23418
23662
|
const v = ymap.get(key);
|
|
@@ -23435,7 +23679,7 @@ function readSlideFromYMap(ymap) {
|
|
|
23435
23679
|
const elements = [];
|
|
23436
23680
|
if (elemArr) {
|
|
23437
23681
|
for (let i = 0; i < elemArr.length; i++) {
|
|
23438
|
-
elements.push(readElementFromYMap(elemArr.get(i)));
|
|
23682
|
+
elements.push(readElementFromYMap(elemArr.get(i), assets));
|
|
23439
23683
|
}
|
|
23440
23684
|
}
|
|
23441
23685
|
slide.elements = elements;
|
|
@@ -23450,6 +23694,7 @@ function readSlideFromYMap(ymap) {
|
|
|
23450
23694
|
* one-shot seeding of an empty document.
|
|
23451
23695
|
*/
|
|
23452
23696
|
function writeSlidesToYDoc(slides, ydoc, factories, origin) {
|
|
23697
|
+
const assets = getAssetsMap(ydoc);
|
|
23453
23698
|
ydoc.transact(() => {
|
|
23454
23699
|
const arr = ydoc.getArray(YDOC_SLIDES_KEY);
|
|
23455
23700
|
if (arr.length > 0) {
|
|
@@ -23457,16 +23702,17 @@ function writeSlidesToYDoc(slides, ydoc, factories, origin) {
|
|
|
23457
23702
|
}
|
|
23458
23703
|
for (const slide of slides) {
|
|
23459
23704
|
const ymap = factories.createMap();
|
|
23460
|
-
writeSlideToYMap(slide, ymap, factories);
|
|
23705
|
+
writeSlideToYMap(slide, ymap, factories, assets);
|
|
23461
23706
|
arr.push([ymap]);
|
|
23462
23707
|
}
|
|
23463
23708
|
}, origin);
|
|
23464
23709
|
}
|
|
23465
23710
|
function readSlidesFromYDoc(ydoc) {
|
|
23711
|
+
const assets = getAssetsMap(ydoc);
|
|
23466
23712
|
const arr = ydoc.getArray(YDOC_SLIDES_KEY);
|
|
23467
23713
|
const slides = [];
|
|
23468
23714
|
for (let i = 0; i < arr.length; i++) {
|
|
23469
|
-
slides.push(readSlideFromYMap(arr.get(i)));
|
|
23715
|
+
slides.push(readSlideFromYMap(arr.get(i), assets));
|
|
23470
23716
|
}
|
|
23471
23717
|
return slides;
|
|
23472
23718
|
}
|
|
@@ -23718,11 +23964,12 @@ function reconcileTextBody(ymap, rec, factories) {
|
|
|
23718
23964
|
encodeTextBody(segments, ytext);
|
|
23719
23965
|
ymap.set('textBody', ytext);
|
|
23720
23966
|
}
|
|
23721
|
-
function reconcileElementYMap(ymap, element, factories) {
|
|
23967
|
+
function reconcileElementYMap(ymap, element, factories, assets) {
|
|
23722
23968
|
const rec = element;
|
|
23723
23969
|
reconcileScalars(ymap, rec, SCALAR_ELEMENT_KEYS);
|
|
23724
23970
|
reconcileComplexFields(ymap, rec, COMPLEX_ELEMENT_FIELDS);
|
|
23725
23971
|
reconcileTextBody(ymap, rec, factories);
|
|
23972
|
+
reconcileAssetFields(rec.id, rec, ymap, assets);
|
|
23726
23973
|
}
|
|
23727
23974
|
function mapIdAt(arr, index) {
|
|
23728
23975
|
const entry = arr.get(index);
|
|
@@ -23799,7 +24046,7 @@ const isYArrayLike = (value) => typeof value === 'object' &&
|
|
|
23799
24046
|
value !== null &&
|
|
23800
24047
|
typeof value.insert === 'function' &&
|
|
23801
24048
|
typeof value.toArray === 'function';
|
|
23802
|
-
function reconcileSlideYMap(ymap, slide, factories) {
|
|
24049
|
+
function reconcileSlideYMap(ymap, slide, factories, assets) {
|
|
23803
24050
|
const rec = slide;
|
|
23804
24051
|
reconcileScalars(ymap, rec, SCALAR_SLIDE_KEYS);
|
|
23805
24052
|
reconcileComplexFields(ymap, rec, COMPLEX_SLIDE_FIELDS);
|
|
@@ -23812,10 +24059,10 @@ function reconcileSlideYMap(ymap, slide, factories) {
|
|
|
23812
24059
|
idOf: (el) => (typeof el.id === 'string' ? el.id : undefined),
|
|
23813
24060
|
create: (el) => {
|
|
23814
24061
|
const map = factories.createMap();
|
|
23815
|
-
writeElementToYMap(el, map, factories);
|
|
24062
|
+
writeElementToYMap(el, map, factories, assets);
|
|
23816
24063
|
return map;
|
|
23817
24064
|
},
|
|
23818
|
-
update: (map, el) => reconcileElementYMap(map, el, factories),
|
|
24065
|
+
update: (map, el) => reconcileElementYMap(map, el, factories, assets),
|
|
23819
24066
|
});
|
|
23820
24067
|
}
|
|
23821
24068
|
/**
|
|
@@ -23824,16 +24071,17 @@ function reconcileSlideYMap(ymap, slide, factories) {
|
|
|
23824
24071
|
* caller's own observer can skip the resulting events.
|
|
23825
24072
|
*/
|
|
23826
24073
|
function reconcileSlidesInYDoc(slides, ydoc, factories, origin = LOCAL_SYNC_ORIGIN) {
|
|
24074
|
+
const assets = getAssetsMap(ydoc);
|
|
23827
24075
|
ydoc.transact(() => {
|
|
23828
24076
|
const arr = ydoc.getArray(YDOC_SLIDES_KEY);
|
|
23829
24077
|
reconcileYArrayById(arr, slides, {
|
|
23830
24078
|
idOf: (slide) => (typeof slide.id === 'string' ? slide.id : undefined),
|
|
23831
24079
|
create: (slide) => {
|
|
23832
24080
|
const map = factories.createMap();
|
|
23833
|
-
writeSlideToYMap(slide, map, factories);
|
|
24081
|
+
writeSlideToYMap(slide, map, factories, assets);
|
|
23834
24082
|
return map;
|
|
23835
24083
|
},
|
|
23836
|
-
update: (map, slide) => reconcileSlideYMap(map, slide, factories),
|
|
24084
|
+
update: (map, slide) => reconcileSlideYMap(map, slide, factories, assets),
|
|
23837
24085
|
});
|
|
23838
24086
|
}, origin);
|
|
23839
24087
|
}
|
|
@@ -23890,6 +24138,50 @@ function createSyncGate(onOpen, graceMs = INITIAL_SYNC_GRACE_MS) {
|
|
|
23890
24138
|
};
|
|
23891
24139
|
}
|
|
23892
24140
|
|
|
24141
|
+
const DEFAULT_DEBOUNCE_MS = 5_000;
|
|
24142
|
+
function createWriteBackScheduler(deps) {
|
|
24143
|
+
let timer = null;
|
|
24144
|
+
function cancel() {
|
|
24145
|
+
if (timer !== null) {
|
|
24146
|
+
clearTimeout(timer);
|
|
24147
|
+
timer = null;
|
|
24148
|
+
}
|
|
24149
|
+
}
|
|
24150
|
+
function schedule(config) {
|
|
24151
|
+
if (!config.onWriteBack || config.role !== 'owner' || !deps.getYDoc()) {
|
|
24152
|
+
return;
|
|
24153
|
+
}
|
|
24154
|
+
cancel();
|
|
24155
|
+
const debounceMs = config.writeBackDebounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
24156
|
+
timer = setTimeout(async () => {
|
|
24157
|
+
timer = null;
|
|
24158
|
+
const ydoc = deps.getYDoc();
|
|
24159
|
+
if (!ydoc || !config.onWriteBack) {
|
|
24160
|
+
return;
|
|
24161
|
+
}
|
|
24162
|
+
const sourceBytes = deps.getSourceBytes?.();
|
|
24163
|
+
if (!sourceBytes) {
|
|
24164
|
+
return;
|
|
24165
|
+
}
|
|
24166
|
+
try {
|
|
24167
|
+
const { PptxHandler } = await import('pptx-viewer-core');
|
|
24168
|
+
const handler = new PptxHandler();
|
|
24169
|
+
await handler.load(sourceBytes.buffer);
|
|
24170
|
+
const slides = readSlidesFromYDoc(ydoc);
|
|
24171
|
+
const merged = deps.mergeTemplateElements
|
|
24172
|
+
? deps.mergeTemplateElements(slides, deps.getTemplateElements?.() ?? {})
|
|
24173
|
+
: slides;
|
|
24174
|
+
const bytes = await handler.save(merged);
|
|
24175
|
+
config.onWriteBack(bytes);
|
|
24176
|
+
}
|
|
24177
|
+
catch {
|
|
24178
|
+
/* non-fatal */
|
|
24179
|
+
}
|
|
24180
|
+
}, debounceMs);
|
|
24181
|
+
}
|
|
24182
|
+
return { schedule, cancel };
|
|
24183
|
+
}
|
|
24184
|
+
|
|
23893
24185
|
// ---------------------------------------------------------------------------
|
|
23894
24186
|
// Tuning constants (px tolerances for position / size changes)
|
|
23895
24187
|
// ---------------------------------------------------------------------------
|
|
@@ -33282,6 +33574,51 @@ const CHANGE_CASE_OPTIONS$1 = [
|
|
|
33282
33574
|
{ value: 'toggle', i18nKey: 'pptx.text.changeCaseToggle' },
|
|
33283
33575
|
];
|
|
33284
33576
|
|
|
33577
|
+
/**
|
|
33578
|
+
* color-swatches.ts: the canonical "Office Standard Colors" swatch catalogue
|
|
33579
|
+
* shared by every binding's colour pickers (font colour, highlight colour,
|
|
33580
|
+
* and any future fill/line colour picker that wants the same 10-swatch grid).
|
|
33581
|
+
*
|
|
33582
|
+
* Mirrors the "Standard Colors" row PowerPoint itself ships. Before this
|
|
33583
|
+
* module existed, the VanillaJS (`swatch-picker.ts`) and Svelte
|
|
33584
|
+
* (`SwatchColorPicker.svelte`) bindings each independently hardcoded an
|
|
33585
|
+
* identical 10-colour list; this module is the single source of truth going
|
|
33586
|
+
* forward so no binding drifts from the others.
|
|
33587
|
+
*
|
|
33588
|
+
* There is no shared-i18n dictionary key for individual colour names yet
|
|
33589
|
+
* (only the `pptx.ribbon.customColour` "Custom colour..." row label exists),
|
|
33590
|
+
* so {@link OfficeColorSwatch.label} is a plain English fallback rather than
|
|
33591
|
+
* an `i18nKey`, matching the optional-label convention used elsewhere in this
|
|
33592
|
+
* module family (e.g. `LineSpacingOption` in `text-format-presets.ts`).
|
|
33593
|
+
*
|
|
33594
|
+
* @module render/color-swatches
|
|
33595
|
+
*/
|
|
33596
|
+
/**
|
|
33597
|
+
* The canonical 10-swatch "Office Standard Colors" catalogue, in PowerPoint's
|
|
33598
|
+
* on-screen order. Consumed by the font-colour and highlight-colour pickers
|
|
33599
|
+
* (and any other picker that wants the same standard set) across every
|
|
33600
|
+
* binding.
|
|
33601
|
+
*/
|
|
33602
|
+
const OFFICE_COLOR_SWATCHES = [
|
|
33603
|
+
{ hex: '#000000', label: 'Black' },
|
|
33604
|
+
{ hex: '#ffffff', label: 'White' },
|
|
33605
|
+
{ hex: '#ff0000', label: 'Red' },
|
|
33606
|
+
{ hex: '#00aa00', label: 'Green' },
|
|
33607
|
+
{ hex: '#0000ff', label: 'Blue' },
|
|
33608
|
+
{ hex: '#ff8800', label: 'Orange' },
|
|
33609
|
+
{ hex: '#8800cc', label: 'Purple' },
|
|
33610
|
+
{ hex: '#00cccc', label: 'Cyan' },
|
|
33611
|
+
{ hex: '#ff69b4', label: 'Pink' },
|
|
33612
|
+
{ hex: '#808080', label: 'Gray' },
|
|
33613
|
+
];
|
|
33614
|
+
/**
|
|
33615
|
+
* The same catalogue flattened to bare `#rrggbb` hex strings, in the same
|
|
33616
|
+
* order as {@link OFFICE_COLOR_SWATCHES}. A drop-in replacement for the
|
|
33617
|
+
* `readonly string[]` swatch lists the vanilla and Svelte bindings each
|
|
33618
|
+
* hardcoded locally before this module existed.
|
|
33619
|
+
*/
|
|
33620
|
+
const OFFICE_COLOR_SWATCH_HEXES = OFFICE_COLOR_SWATCHES.map((swatch) => swatch.hex);
|
|
33621
|
+
|
|
33285
33622
|
/**
|
|
33286
33623
|
* PowerPoint-style title bar: shared pure logic + Tailwind class tokens.
|
|
33287
33624
|
*
|
|
@@ -37702,25 +38039,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
|
|
|
37702
38039
|
* surface so `collaboration.service.ts`, `collaboration-cursors.component.ts`,
|
|
37703
38040
|
* the viewer barrel, and the colocated tests are unchanged.
|
|
37704
38041
|
*
|
|
37705
|
-
*
|
|
37706
|
-
*
|
|
37707
|
-
*
|
|
37708
|
-
*
|
|
37709
|
-
*
|
|
37710
|
-
*
|
|
37711
|
-
*
|
|
37712
|
-
*
|
|
37713
|
-
* `mapAwarenessCursors` (the foundational bare-`{ cursor, user }` mapping) now
|
|
37714
|
-
* lives in shared and is re-exported here so existing Angular imports are
|
|
37715
|
-
* unchanged.
|
|
37716
|
-
*/
|
|
37717
|
-
/**
|
|
37718
|
-
* Clamp/format a cursor label so long names don't overflow the chip. Uses a
|
|
37719
|
-
* single-character ellipsis (`…`); the total length is exactly `maxChars`.
|
|
38042
|
+
* `RemotePresence` is the Angular name for shared's `SanitizedPresence`
|
|
38043
|
+
* (re-aliased here). `formatCursorLabel` and `mapAwarenessCursors` are now
|
|
38044
|
+
* re-exported as-is from shared: Angular's `formatCursorLabel` used to keep
|
|
38045
|
+
* its own single-character ellipsis (`…`) truncation that differed from
|
|
38046
|
+
* shared's three-dot (`...`) variant, so two peers viewing the same presence
|
|
38047
|
+
* record saw differently-truncated names depending on which binding hosted
|
|
38048
|
+
* the session - a real cross-binding consistency bug, not a style choice.
|
|
37720
38049
|
*/
|
|
37721
|
-
function formatCursorLabel(userName, maxChars = MAX_LABEL_CHARS) {
|
|
37722
|
-
return userName.length > maxChars ? `${userName.slice(0, maxChars - 1)}…` : userName;
|
|
37723
|
-
}
|
|
37724
38050
|
|
|
37725
38051
|
/**
|
|
37726
38052
|
* collaboration-cursors.component.ts: Angular port of the Vue
|
|
@@ -37862,8 +38188,6 @@ class LocalPresencePublisher {
|
|
|
37862
38188
|
setCursor(x, y, activeSlideIndex = this.activeSlide) {
|
|
37863
38189
|
this.cursor = { x, y };
|
|
37864
38190
|
this.activeSlide = activeSlideIndex;
|
|
37865
|
-
// A bare `cursor` field mirrors the flat awareness shape some peers read.
|
|
37866
|
-
this.awareness.setLocalStateField('cursor', { x, y });
|
|
37867
38191
|
this.publish();
|
|
37868
38192
|
}
|
|
37869
38193
|
setSelection(selectedElementId, activeSlideIndex = this.activeSlide) {
|
|
@@ -37883,14 +38207,14 @@ class LocalPresencePublisher {
|
|
|
37883
38207
|
*
|
|
37884
38208
|
* Isolates the dynamic `yjs` / `y-websocket` / `y-webrtc` imports and provider
|
|
37885
38209
|
* construction so the service focuses on lifecycle + reactive state. The Yjs
|
|
37886
|
-
* packages are dynamically imported
|
|
37887
|
-
*
|
|
37888
|
-
*
|
|
37889
|
-
*
|
|
38210
|
+
* packages are dynamically imported (plain string-literal specifiers, matching
|
|
38211
|
+
* React/Vue) so they are fully tree-shaken when collaboration is unused, while
|
|
38212
|
+
* still resolving correctly under a consuming app's Vite dev server: an
|
|
38213
|
+
* `@vite-ignore`d or variable specifier skips Vite's dev-time bare-specifier
|
|
38214
|
+
* rewrite, which then fails to resolve natively in the browser.
|
|
37890
38215
|
*/
|
|
37891
38216
|
async function createDoc() {
|
|
37892
|
-
const
|
|
37893
|
-
const Y = (await import(/* @vite-ignore */ yModule));
|
|
38217
|
+
const Y = (await import('yjs'));
|
|
37894
38218
|
const doc = new Y.Doc();
|
|
37895
38219
|
const factories = {
|
|
37896
38220
|
createMap: () => new Y.Map(),
|
|
@@ -37901,10 +38225,9 @@ async function createDoc() {
|
|
|
37901
38225
|
}
|
|
37902
38226
|
/** Create a y-websocket transport bundle for `config`. */
|
|
37903
38227
|
async function createWebsocketBundle(config) {
|
|
37904
|
-
const wsSpecifier = 'y-websocket';
|
|
37905
38228
|
const [{ doc, factories }, yws] = await Promise.all([
|
|
37906
38229
|
createDoc(),
|
|
37907
|
-
import(
|
|
38230
|
+
import('y-websocket'),
|
|
37908
38231
|
]);
|
|
37909
38232
|
const provider = new yws.WebsocketProvider(config.serverUrl, config.roomId, doc, config.authToken ? { params: { token: config.authToken } } : undefined);
|
|
37910
38233
|
return { doc, provider, awareness: provider.awareness, factories };
|
|
@@ -37916,10 +38239,9 @@ async function createWebsocketBundle(config) {
|
|
|
37916
38239
|
* `authToken` is passed as the room `password`.
|
|
37917
38240
|
*/
|
|
37918
38241
|
async function createWebrtcBundle(config) {
|
|
37919
|
-
const rtcSpecifier = 'y-webrtc';
|
|
37920
38242
|
const [{ doc, factories }, yrtc] = await Promise.all([
|
|
37921
38243
|
createDoc(),
|
|
37922
|
-
import(
|
|
38244
|
+
import('y-webrtc'),
|
|
37923
38245
|
]);
|
|
37924
38246
|
const provider = new yrtc.WebrtcProvider(config.roomId, doc, {
|
|
37925
38247
|
signaling: config.signaling?.length ? config.signaling : undefined,
|
|
@@ -38144,9 +38466,14 @@ class CollaborationService {
|
|
|
38144
38466
|
this.status.set('error');
|
|
38145
38467
|
return;
|
|
38146
38468
|
}
|
|
38469
|
+
// Falls back from a blank serverUrl the same way Vue's session layer
|
|
38470
|
+
// already does, so a bare CollaborationConfig behaves identically
|
|
38471
|
+
// regardless of which binding's session layer receives it directly (not
|
|
38472
|
+
// just via the Share/Broadcast dialogs, which already pre-resolve it).
|
|
38473
|
+
const transport = config.transport ?? resolveTransportForServerUrl(config.serverUrl);
|
|
38147
38474
|
// Fail fast on mixed content (websocket only): an https page cannot open a
|
|
38148
38475
|
// ws:// socket, so surface the error rather than hanging until the timeout.
|
|
38149
|
-
if (
|
|
38476
|
+
if (transport !== 'webrtc' && isMixedContentBlocked(config.serverUrl)) {
|
|
38150
38477
|
this.status.set('error');
|
|
38151
38478
|
return;
|
|
38152
38479
|
}
|
|
@@ -38159,7 +38486,7 @@ class CollaborationService {
|
|
|
38159
38486
|
this.activeRole.set(config.role);
|
|
38160
38487
|
this.status.set('connecting');
|
|
38161
38488
|
try {
|
|
38162
|
-
const bundle =
|
|
38489
|
+
const bundle = transport === 'webrtc'
|
|
38163
38490
|
? await createWebrtcBundle(config)
|
|
38164
38491
|
: await createWebsocketBundle(config);
|
|
38165
38492
|
this.ydoc = bundle.doc;
|
|
@@ -38176,7 +38503,7 @@ class CollaborationService {
|
|
|
38176
38503
|
this.localPresence.publish();
|
|
38177
38504
|
this.awareness.on('change', this.refreshPresence);
|
|
38178
38505
|
this.awareness.on('update', this.refreshPresence);
|
|
38179
|
-
this.wireStatus(
|
|
38506
|
+
this.wireStatus(transport);
|
|
38180
38507
|
this.syncGate.reset();
|
|
38181
38508
|
this.wireSynced();
|
|
38182
38509
|
this.unobserveSlides = observeYDocSlides(this.ydoc, (_events, transaction) => this.onRemoteChange(transaction));
|
|
@@ -38195,18 +38522,23 @@ class CollaborationService {
|
|
|
38195
38522
|
}
|
|
38196
38523
|
}
|
|
38197
38524
|
/** Wire the provider status events + (websocket-only) connection timeout. */
|
|
38198
|
-
wireStatus(
|
|
38525
|
+
wireStatus(transport) {
|
|
38199
38526
|
const provider = this.provider;
|
|
38200
38527
|
if (!provider) {
|
|
38201
38528
|
return;
|
|
38202
38529
|
}
|
|
38203
|
-
if (
|
|
38530
|
+
if (transport === 'webrtc') {
|
|
38204
38531
|
// P2P: no server round-trip to wait on. Treat "created" as connected,
|
|
38205
38532
|
// and reflect explicit disconnect events.
|
|
38206
38533
|
this.status.set('connected');
|
|
38207
38534
|
provider.on('status', (payload) => {
|
|
38208
38535
|
if (payload.connected === false && this.active()) {
|
|
38209
38536
|
this.status.set('disconnected');
|
|
38537
|
+
// Re-arm on (re)connect: without this, a peer that drops and
|
|
38538
|
+
// rejoins keeps the gate permanently open from the first
|
|
38539
|
+
// connection and can clobber the room with a stale local doc.
|
|
38540
|
+
this.syncGate.reset();
|
|
38541
|
+
this.syncGate.arm();
|
|
38210
38542
|
}
|
|
38211
38543
|
else if (payload.connected === true) {
|
|
38212
38544
|
this.status.set('connected');
|
|
@@ -38221,6 +38553,8 @@ class CollaborationService {
|
|
|
38221
38553
|
}
|
|
38222
38554
|
else if (payload.status === 'disconnected' && this.active()) {
|
|
38223
38555
|
this.status.set('disconnected');
|
|
38556
|
+
this.syncGate.reset();
|
|
38557
|
+
this.syncGate.arm();
|
|
38224
38558
|
}
|
|
38225
38559
|
});
|
|
38226
38560
|
if (provider.wsconnected) {
|