@ckeditor/ckeditor5-bookmark 0.0.0-nightly-20241025.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/LICENSE.md +17 -0
  3. package/README.md +26 -0
  4. package/build/bookmark.js +4 -0
  5. package/ckeditor5-metadata.json +24 -0
  6. package/dist/augmentation.d.ts +28 -0
  7. package/dist/bookmark.d.ts +34 -0
  8. package/dist/bookmarkconfig.d.ts +52 -0
  9. package/dist/bookmarkediting.d.ts +55 -0
  10. package/dist/bookmarkui.d.ts +170 -0
  11. package/dist/index-content.css +4 -0
  12. package/dist/index-editor.css +150 -0
  13. package/dist/index.css +195 -0
  14. package/dist/index.css.map +1 -0
  15. package/dist/index.d.ts +18 -0
  16. package/dist/index.js +1322 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/insertbookmarkcommand.d.ts +42 -0
  19. package/dist/ui/bookmarkactionsview.d.ts +106 -0
  20. package/dist/ui/bookmarkformview.d.ts +122 -0
  21. package/dist/updatebookmarkcommand.d.ts +46 -0
  22. package/dist/utils.d.ts +15 -0
  23. package/lang/contexts.json +13 -0
  24. package/package.json +43 -0
  25. package/src/augmentation.d.ts +24 -0
  26. package/src/augmentation.js +5 -0
  27. package/src/bookmark.d.ts +30 -0
  28. package/src/bookmark.js +36 -0
  29. package/src/bookmarkconfig.d.ts +48 -0
  30. package/src/bookmarkconfig.js +5 -0
  31. package/src/bookmarkediting.d.ts +51 -0
  32. package/src/bookmarkediting.js +212 -0
  33. package/src/bookmarkui.d.ts +166 -0
  34. package/src/bookmarkui.js +583 -0
  35. package/src/index.d.ts +14 -0
  36. package/src/index.js +13 -0
  37. package/src/insertbookmarkcommand.d.ts +38 -0
  38. package/src/insertbookmarkcommand.js +113 -0
  39. package/src/ui/bookmarkactionsview.d.ts +102 -0
  40. package/src/ui/bookmarkactionsview.js +154 -0
  41. package/src/ui/bookmarkformview.d.ts +118 -0
  42. package/src/ui/bookmarkformview.js +203 -0
  43. package/src/updatebookmarkcommand.d.ts +42 -0
  44. package/src/updatebookmarkcommand.js +75 -0
  45. package/src/utils.d.ts +11 -0
  46. package/src/utils.js +19 -0
  47. package/theme/bookmark.css +50 -0
  48. package/theme/bookmarkactions.css +44 -0
  49. package/theme/bookmarkform.css +42 -0
package/dist/index.js ADDED
@@ -0,0 +1,1322 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ import { icons, Command, Plugin } from '@ckeditor/ckeditor5-core/dist/index.js';
6
+ import { toWidget, Widget } from '@ckeditor/ckeditor5-widget/dist/index.js';
7
+ import { View, ViewCollection, FocusCycler, submitHandler, FormHeaderView, LabeledFieldView, createLabeledInputText, ButtonView, LabelView, IconView, ContextualBalloon, CssTransitionDisablerMixin, MenuBarMenuListItemButtonView, clickOutsideHandler } from '@ckeditor/ckeditor5-ui/dist/index.js';
8
+ import { ClickObserver } from '@ckeditor/ckeditor5-engine/dist/index.js';
9
+ import { FocusTracker, KeystrokeHandler, logWarning } from '@ckeditor/ckeditor5-utils/dist/index.js';
10
+
11
+ /**
12
+ * The bookmark form view controller class.
13
+ *
14
+ * See {@link module:bookmark/ui/bookmarkformview~BookmarkFormView}.
15
+ */ class BookmarkFormView extends View {
16
+ /**
17
+ * Tracks information about DOM focus in the form.
18
+ */ focusTracker = new FocusTracker();
19
+ /**
20
+ * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
21
+ */ keystrokes = new KeystrokeHandler();
22
+ /**
23
+ * The ID input view.
24
+ */ idInputView;
25
+ /**
26
+ * The Submit button view.
27
+ */ buttonView;
28
+ /**
29
+ * A collection of form child views in the form.
30
+ */ children;
31
+ /**
32
+ * An array of form validators used by {@link #isValid}.
33
+ */ _validators;
34
+ /**
35
+ * A collection of views that can be focused in the form.
36
+ */ _focusables = new ViewCollection();
37
+ /**
38
+ * Helps cycling over {@link #_focusables} in the form.
39
+ */ _focusCycler;
40
+ /**
41
+ * Creates an instance of the {@link module:bookmark/ui/bookmarkformview~BookmarkFormView} class.
42
+ *
43
+ * Also see {@link #render}.
44
+ *
45
+ * @param locale The localization services instance.
46
+ * @param validators Form validators used by {@link #isValid}.
47
+ */ constructor(locale, validators){
48
+ super(locale);
49
+ const t = locale.t;
50
+ this._validators = validators;
51
+ this.idInputView = this._createIdInput();
52
+ this.buttonView = this._createButton(t('Insert'), 'ck-button-action ck-button-bold');
53
+ this.buttonView.type = 'submit';
54
+ this.children = this._createViewChildren();
55
+ this._focusCycler = new FocusCycler({
56
+ focusables: this._focusables,
57
+ focusTracker: this.focusTracker,
58
+ keystrokeHandler: this.keystrokes,
59
+ actions: {
60
+ // Navigate form fields backwards using the Shift + Tab keystroke.
61
+ focusPrevious: 'shift + tab',
62
+ // Navigate form fields forwards using the Tab key.
63
+ focusNext: 'tab'
64
+ }
65
+ });
66
+ const classList = [
67
+ 'ck',
68
+ 'ck-bookmark-view'
69
+ ];
70
+ this.setTemplate({
71
+ tag: 'form',
72
+ attributes: {
73
+ class: classList,
74
+ // https://github.com/ckeditor/ckeditor5-link/issues/90
75
+ tabindex: '-1'
76
+ },
77
+ children: this.children
78
+ });
79
+ }
80
+ /**
81
+ * @inheritDoc
82
+ */ render() {
83
+ super.render();
84
+ submitHandler({
85
+ view: this
86
+ });
87
+ const childViews = [
88
+ this.idInputView,
89
+ this.buttonView
90
+ ];
91
+ childViews.forEach((v)=>{
92
+ // Register the view as focusable.
93
+ this._focusables.add(v);
94
+ // Register the view in the focus tracker.
95
+ this.focusTracker.add(v.element);
96
+ });
97
+ // Start listening for the keystrokes coming from #element.
98
+ this.keystrokes.listenTo(this.element);
99
+ }
100
+ /**
101
+ * @inheritDoc
102
+ */ destroy() {
103
+ super.destroy();
104
+ this.focusTracker.destroy();
105
+ this.keystrokes.destroy();
106
+ }
107
+ /**
108
+ * Focuses the fist {@link #_focusables} in the form.
109
+ */ focus() {
110
+ this._focusCycler.focusFirst();
111
+ }
112
+ /**
113
+ * Validates the form and returns `false` when some fields are invalid.
114
+ */ isValid() {
115
+ this.resetFormStatus();
116
+ for (const validator of this._validators){
117
+ const errorText = validator(this);
118
+ // One error per field is enough.
119
+ if (errorText) {
120
+ // Apply updated error.
121
+ this.idInputView.errorText = errorText;
122
+ return false;
123
+ }
124
+ }
125
+ return true;
126
+ }
127
+ /**
128
+ * Cleans up the supplementary error and information text of the {@link #idInputView}
129
+ * bringing them back to the state when the form has been displayed for the first time.
130
+ *
131
+ * See {@link #isValid}.
132
+ */ resetFormStatus() {
133
+ this.idInputView.errorText = null;
134
+ }
135
+ /**
136
+ * Creates header and form view.
137
+ */ _createViewChildren() {
138
+ const children = this.createCollection();
139
+ const t = this.t;
140
+ children.add(new FormHeaderView(this.locale, {
141
+ label: t('Bookmark')
142
+ }));
143
+ children.add(this._createFormContentView());
144
+ return children;
145
+ }
146
+ /**
147
+ * Creates form content view with input and button.
148
+ */ _createFormContentView() {
149
+ const view = new View(this.locale);
150
+ const children = this.createCollection();
151
+ const classList = [
152
+ 'ck',
153
+ 'ck-bookmark-form',
154
+ 'ck-responsive-form'
155
+ ];
156
+ children.add(this.idInputView);
157
+ children.add(this.buttonView);
158
+ view.setTemplate({
159
+ tag: 'div',
160
+ attributes: {
161
+ class: classList
162
+ },
163
+ children
164
+ });
165
+ return view;
166
+ }
167
+ /**
168
+ * Creates a labeled input view.
169
+ *
170
+ * @returns Labeled field view instance.
171
+ */ _createIdInput() {
172
+ const t = this.locale.t;
173
+ const labeledInput = new LabeledFieldView(this.locale, createLabeledInputText);
174
+ labeledInput.label = t('Bookmark name');
175
+ labeledInput.infoText = t('Enter the bookmark name without spaces.');
176
+ return labeledInput;
177
+ }
178
+ /**
179
+ * Creates a button view.
180
+ *
181
+ * @param label The button label.
182
+ * @param className The additional button CSS class name.
183
+ * @returns The button view instance.
184
+ */ _createButton(label, className) {
185
+ const button = new ButtonView(this.locale);
186
+ button.set({
187
+ label,
188
+ withText: true
189
+ });
190
+ button.extendTemplate({
191
+ attributes: {
192
+ class: className
193
+ }
194
+ });
195
+ return button;
196
+ }
197
+ /**
198
+ * The native DOM `value` of the {@link #idInputView} element.
199
+ *
200
+ * **Note**: Do not confuse it with the {@link module:ui/inputtext/inputtextview~InputTextView#value}
201
+ * which works one way only and may not represent the actual state of the component in the DOM.
202
+ */ get id() {
203
+ const { element } = this.idInputView.fieldView;
204
+ if (!element) {
205
+ return null;
206
+ }
207
+ return element.value.trim();
208
+ }
209
+ }
210
+
211
+ /**
212
+ * The bookmark actions view class. This view displays the bookmark preview, allows
213
+ * removing or editing the bookmark.
214
+ */ class BookmarkActionsView extends View {
215
+ /**
216
+ * Tracks information about DOM focus in the actions.
217
+ */ focusTracker = new FocusTracker();
218
+ /**
219
+ * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
220
+ */ keystrokes = new KeystrokeHandler();
221
+ /**
222
+ * The bookmark preview view.
223
+ */ bookmarkPreviewView;
224
+ /**
225
+ * The remove button view.
226
+ */ removeButtonView;
227
+ /**
228
+ * The edit bookmark button view.
229
+ */ editButtonView;
230
+ /**
231
+ * A collection of views that can be focused in the view.
232
+ */ _focusables = new ViewCollection();
233
+ /**
234
+ * Helps cycling over {@link #_focusables} in the view.
235
+ */ _focusCycler;
236
+ /**
237
+ * @inheritDoc
238
+ */ constructor(locale){
239
+ super(locale);
240
+ const t = locale.t;
241
+ this.bookmarkPreviewView = this._createBookmarkPreviewView();
242
+ this.removeButtonView = this._createButton(t('Remove bookmark'), icons.remove, 'remove', this.bookmarkPreviewView);
243
+ this.editButtonView = this._createButton(t('Edit bookmark'), icons.pencil, 'edit', this.bookmarkPreviewView);
244
+ this.set('id', undefined);
245
+ this._focusCycler = new FocusCycler({
246
+ focusables: this._focusables,
247
+ focusTracker: this.focusTracker,
248
+ keystrokeHandler: this.keystrokes,
249
+ actions: {
250
+ // Navigate fields backwards using the Shift + Tab keystroke.
251
+ focusPrevious: 'shift + tab',
252
+ // Navigate fields forwards using the Tab key.
253
+ focusNext: 'tab'
254
+ }
255
+ });
256
+ this.setTemplate({
257
+ tag: 'div',
258
+ attributes: {
259
+ class: [
260
+ 'ck',
261
+ 'ck-bookmark-actions',
262
+ 'ck-responsive-form'
263
+ ],
264
+ // https://github.com/ckeditor/ckeditor5-link/issues/90
265
+ tabindex: '-1'
266
+ },
267
+ children: [
268
+ this.bookmarkPreviewView,
269
+ this.editButtonView,
270
+ this.removeButtonView
271
+ ]
272
+ });
273
+ }
274
+ /**
275
+ * @inheritDoc
276
+ */ render() {
277
+ super.render();
278
+ const childViews = [
279
+ this.editButtonView,
280
+ this.removeButtonView
281
+ ];
282
+ childViews.forEach((v)=>{
283
+ // Register the view as focusable.
284
+ this._focusables.add(v);
285
+ // Register the view in the focus tracker.
286
+ this.focusTracker.add(v.element);
287
+ });
288
+ // Start listening for the keystrokes coming from #element.
289
+ this.keystrokes.listenTo(this.element);
290
+ }
291
+ /**
292
+ * @inheritDoc
293
+ */ destroy() {
294
+ super.destroy();
295
+ this.focusTracker.destroy();
296
+ this.keystrokes.destroy();
297
+ }
298
+ /**
299
+ * Focuses the fist {@link #_focusables} in the actions.
300
+ */ focus() {
301
+ this._focusCycler.focusFirst();
302
+ }
303
+ /**
304
+ * Creates a button view.
305
+ *
306
+ * @param label The button label.
307
+ * @param icon The button icon.
308
+ * @param eventName An event name that the `ButtonView#execute` event will be delegated to.
309
+ * @param additionalLabel An additional label outside the button.
310
+ * @returns The button view instance.
311
+ */ _createButton(label, icon, eventName, additionalLabel) {
312
+ const button = new ButtonView(this.locale);
313
+ button.set({
314
+ label,
315
+ icon,
316
+ tooltip: true
317
+ });
318
+ button.delegate('execute').to(this, eventName);
319
+ // Since button label `id` is bound to the `ariaLabelledBy` property
320
+ // we need to modify this binding to include only the first ID token
321
+ // as this button will be labeled by multiple labels.
322
+ button.labelView.unbind('id');
323
+ button.labelView.bind('id').to(button, 'ariaLabelledBy', (ariaLabelledBy)=>{
324
+ return getFirstToken(ariaLabelledBy);
325
+ });
326
+ button.ariaLabelledBy = `${button.ariaLabelledBy} ${additionalLabel.id}`;
327
+ return button;
328
+ }
329
+ /**
330
+ * Creates a bookmark name preview label.
331
+ *
332
+ * @returns The label view instance.
333
+ */ _createBookmarkPreviewView() {
334
+ const label = new LabelView(this.locale);
335
+ label.extendTemplate({
336
+ attributes: {
337
+ class: [
338
+ 'ck',
339
+ 'ck-bookmark-actions__preview'
340
+ ]
341
+ }
342
+ });
343
+ // Bind label text with the bookmark ID.
344
+ label.bind('text').to(this, 'id');
345
+ return label;
346
+ }
347
+ }
348
+ /**
349
+ * Returns the first token from space separated token list.
350
+ */ function getFirstToken(tokenList) {
351
+ return tokenList.split(' ')[0];
352
+ }
353
+
354
+ /**
355
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
356
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
357
+ */ /**
358
+ * @module bookmark/utils
359
+ */ /**
360
+ * Returns `true` if the bookmark id is valid; otherwise, returns `false`.
361
+ */ function isBookmarkIdValid(id) {
362
+ if (!id || typeof id !== 'string') {
363
+ return false;
364
+ }
365
+ if (/\s/.test(id)) {
366
+ return false;
367
+ }
368
+ return true;
369
+ }
370
+
371
+ /**
372
+ * The insert bookmark command.
373
+ *
374
+ * The command is registered by {@link module:bookmark/bookmarkediting~BookmarkEditing} as `'insertBookmark'`.
375
+ *
376
+ * To insert a bookmark element at place where is the current collapsed selection or where is the beginning of document selection,
377
+ * execute the command passing the bookmark id as a parameter:
378
+ *
379
+ * ```ts
380
+ * editor.execute( 'insertBookmark', { bookmarkId: 'foo_bar' } );
381
+ * ```
382
+ */ class InsertBookmarkCommand extends Command {
383
+ /**
384
+ * @inheritDoc
385
+ */ refresh() {
386
+ const model = this.editor.model;
387
+ const selection = model.document.selection;
388
+ const position = this._getPositionToInsertBookmark(selection);
389
+ this.isEnabled = !!position;
390
+ }
391
+ /**
392
+ * Executes the command.
393
+ *
394
+ * @fires execute
395
+ * @param options Command options.
396
+ * @param options.bookmarkId The value of the `bookmarkId` attribute.
397
+ */ execute(options) {
398
+ if (!options) {
399
+ return;
400
+ }
401
+ const { bookmarkId } = options;
402
+ if (!isBookmarkIdValid(bookmarkId)) {
403
+ /**
404
+ * Insert bookmark command can be executed only with a valid name.
405
+ *
406
+ * A valid bookmark name must be a non-empty string and must not contain any spaces.
407
+ *
408
+ * @error insert-bookmark-command-executed-with-invalid-name
409
+ */ logWarning('insert-bookmark-command-executed-with-invalid-name');
410
+ return;
411
+ }
412
+ const editor = this.editor;
413
+ const model = editor.model;
414
+ const selection = model.document.selection;
415
+ model.change((writer)=>{
416
+ let position = this._getPositionToInsertBookmark(selection);
417
+ const isBookmarkAllowed = model.schema.checkChild(position, 'bookmark');
418
+ // If the position does not allow for `bookmark` but allows for a `paragraph`
419
+ // then insert a `paragraph` then we will insert a `bookmark` inside.
420
+ if (!isBookmarkAllowed) {
421
+ const newPosition = editor.execute('insertParagraph', {
422
+ position
423
+ });
424
+ if (!newPosition) {
425
+ return;
426
+ }
427
+ position = newPosition;
428
+ }
429
+ const bookmarkElement = writer.createElement('bookmark', {
430
+ ...Object.fromEntries(selection.getAttributes()),
431
+ bookmarkId
432
+ });
433
+ model.insertObject(bookmarkElement, position, null, {
434
+ setSelection: 'on'
435
+ });
436
+ });
437
+ }
438
+ /**
439
+ * Returns the position where the bookmark can be inserted. And if it is not possible to insert a bookmark,
440
+ * check if it is possible to insert a paragraph.
441
+ */ _getPositionToInsertBookmark(selection) {
442
+ const model = this.editor.model;
443
+ const schema = model.schema;
444
+ const firstRange = selection.getFirstRange();
445
+ const startPosition = firstRange.start;
446
+ // Return position if it is allowed to insert bookmark or if it is allowed to insert paragraph.
447
+ if (isBookmarkAllowed(startPosition, schema)) {
448
+ return startPosition;
449
+ }
450
+ for (const { previousPosition, item } of firstRange){
451
+ // When the table cell is selected (from the outside) we look for the first paragraph-like element inside.
452
+ if (item.is('element') && schema.checkChild(item, '$text') && isBookmarkAllowed(item, schema)) {
453
+ return model.createPositionAt(item, 0);
454
+ }
455
+ if (isBookmarkAllowed(previousPosition, schema)) {
456
+ return previousPosition;
457
+ }
458
+ }
459
+ return null;
460
+ }
461
+ }
462
+ /**
463
+ * Verify if the given position allows for bookmark insertion. Verify if auto-paragraphing could help.
464
+ */ function isBookmarkAllowed(position, schema) {
465
+ if (schema.checkChild(position, 'bookmark')) {
466
+ return true;
467
+ }
468
+ if (!schema.checkChild(position, 'paragraph')) {
469
+ return false;
470
+ }
471
+ return schema.checkChild('paragraph', 'bookmark');
472
+ }
473
+
474
+ /**
475
+ * The update bookmark command.
476
+ *
477
+ * The command is registered by {@link module:bookmark/bookmarkediting~BookmarkEditing} as `'updateBookmark'`.
478
+ *
479
+ * To update the `bookmarkId` of current selected bookmark element, execute the command passing the bookmark id as a parameter:
480
+ *
481
+ * ```ts
482
+ * editor.execute( 'updateBookmark', { bookmarkId: 'newId' } );
483
+ * ```
484
+ */ class UpdateBookmarkCommand extends Command {
485
+ /**
486
+ * @inheritDoc
487
+ */ refresh() {
488
+ const model = this.editor.model;
489
+ const selection = model.document.selection;
490
+ const selectedBookmark = getSelectedBookmark(selection);
491
+ this.isEnabled = !!selectedBookmark;
492
+ this.value = selectedBookmark ? selectedBookmark.getAttribute('bookmarkId') : undefined;
493
+ }
494
+ /**
495
+ * Executes the command.
496
+ *
497
+ * @fires execute
498
+ * @param options Command options.
499
+ * @param options.bookmarkId The new value of the `bookmarkId` attribute to set.
500
+ */ execute(options) {
501
+ if (!options) {
502
+ return;
503
+ }
504
+ const { bookmarkId } = options;
505
+ if (!isBookmarkIdValid(bookmarkId)) {
506
+ /**
507
+ * Update bookmark command can be executed only with a valid name.
508
+ *
509
+ * A valid bookmark name must be a non-empty string and must not contain any spaces.
510
+ *
511
+ * @error update-bookmark-command-executed-with-invalid-name
512
+ */ logWarning('update-bookmark-command-executed-with-invalid-name');
513
+ return;
514
+ }
515
+ const model = this.editor.model;
516
+ const selection = model.document.selection;
517
+ const selectedBookmark = getSelectedBookmark(selection);
518
+ if (selectedBookmark) {
519
+ model.change((writer)=>{
520
+ writer.setAttribute('bookmarkId', bookmarkId, selectedBookmark);
521
+ });
522
+ }
523
+ }
524
+ }
525
+ /**
526
+ * Returns the selected `bookmark` element in the model, if any.
527
+ */ function getSelectedBookmark(selection) {
528
+ const element = selection.getSelectedElement();
529
+ if (!!element && element.is('element', 'bookmark')) {
530
+ return element;
531
+ }
532
+ return null;
533
+ }
534
+
535
+ const bookmarkIcon$1 = icons.bookmarkInline;
536
+ /**
537
+ * The bookmark editing plugin.
538
+ */ class BookmarkEditing extends Plugin {
539
+ /**
540
+ * A collection of bookmarks elements in the document.
541
+ */ _bookmarkElements = new Map();
542
+ /**
543
+ * @inheritDoc
544
+ */ static get pluginName() {
545
+ return 'BookmarkEditing';
546
+ }
547
+ /**
548
+ * @inheritDoc
549
+ */ static get isOfficialPlugin() {
550
+ return true;
551
+ }
552
+ /**
553
+ * @inheritDoc
554
+ */ init() {
555
+ const { editor } = this;
556
+ this._defineSchema();
557
+ this._defineConverters();
558
+ editor.commands.add('insertBookmark', new InsertBookmarkCommand(editor));
559
+ editor.commands.add('updateBookmark', new UpdateBookmarkCommand(editor));
560
+ this.listenTo(editor.model.document, 'change:data', ()=>{
561
+ this._trackBookmarkElements();
562
+ });
563
+ }
564
+ /**
565
+ * Returns the model element for the given bookmark ID if it exists.
566
+ */ getElementForBookmarkId(bookmarkId) {
567
+ for (const [element, id] of this._bookmarkElements){
568
+ if (id == bookmarkId) {
569
+ return element;
570
+ }
571
+ }
572
+ return null;
573
+ }
574
+ /**
575
+ * Defines the schema for the bookmark feature.
576
+ */ _defineSchema() {
577
+ const schema = this.editor.model.schema;
578
+ schema.register('bookmark', {
579
+ inheritAllFrom: '$inlineObject',
580
+ allowAttributes: 'bookmarkId',
581
+ disallowAttributes: [
582
+ 'linkHref',
583
+ 'htmlA'
584
+ ]
585
+ });
586
+ }
587
+ /**
588
+ * Defines the converters for the bookmark feature.
589
+ */ _defineConverters() {
590
+ const { editor } = this;
591
+ const { conversion, t } = editor;
592
+ editor.data.htmlProcessor.domConverter.registerInlineObjectMatcher((element)=>upcastMatcher(element));
593
+ // Register an inline object matcher so that bookmarks <a>s are correctly recognized as inline elements in editing pipeline.
594
+ // This prevents converting spaces around bookmarks to `&nbsp;`s.
595
+ editor.editing.view.domConverter.registerInlineObjectMatcher((element)=>upcastMatcher(element, false));
596
+ conversion.for('dataDowncast').elementToElement({
597
+ model: {
598
+ name: 'bookmark',
599
+ attributes: [
600
+ 'bookmarkId'
601
+ ]
602
+ },
603
+ view: (modelElement, { writer })=>{
604
+ const emptyElement = writer.createEmptyElement('a', {
605
+ 'id': modelElement.getAttribute('bookmarkId')
606
+ });
607
+ // `getFillerOffset` is not needed to set here, because `emptyElement` has already covered it.
608
+ return emptyElement;
609
+ }
610
+ });
611
+ conversion.for('editingDowncast').elementToElement({
612
+ model: {
613
+ name: 'bookmark',
614
+ attributes: [
615
+ 'bookmarkId'
616
+ ]
617
+ },
618
+ view: (modelElement, { writer })=>{
619
+ const id = modelElement.getAttribute('bookmarkId');
620
+ const containerElement = writer.createContainerElement('a', {
621
+ id,
622
+ class: 'ck-bookmark'
623
+ }, [
624
+ this._createBookmarkUIElement(writer)
625
+ ]);
626
+ this._bookmarkElements.set(modelElement, id);
627
+ // `getFillerOffset` is not needed to set here, because `toWidget` has already covered it.
628
+ const labelCreator = ()=>`${id} ${t('bookmark widget')}`;
629
+ return toWidget(containerElement, writer, {
630
+ label: labelCreator
631
+ });
632
+ }
633
+ });
634
+ conversion.for('upcast').add((dispatcher)=>dispatcher.on('element:a', dataViewModelAnchorInsertion(editor)));
635
+ }
636
+ /**
637
+ * Creates a UI element for the `bookmark` representation in editing view.
638
+ */ _createBookmarkUIElement(writer) {
639
+ return writer.createUIElement('span', {
640
+ class: 'ck-bookmark__icon'
641
+ }, function(domDocument) {
642
+ const domElement = this.toDomElement(domDocument);
643
+ const icon = new IconView();
644
+ icon.set({
645
+ content: bookmarkIcon$1,
646
+ isColorInherited: false
647
+ });
648
+ icon.render();
649
+ domElement.appendChild(icon.element);
650
+ return domElement;
651
+ });
652
+ }
653
+ /**
654
+ * Tracking the added or removed bookmark elements.
655
+ */ _trackBookmarkElements() {
656
+ this._bookmarkElements.forEach((id, element)=>{
657
+ if (element.root.rootName === '$graveyard') {
658
+ this._bookmarkElements.delete(element);
659
+ }
660
+ });
661
+ }
662
+ }
663
+ /**
664
+ * A helper function to match an `anchor` element which must contain `id` or `name` attribute but without `href` attribute,
665
+ * also when `expectEmpty` is set to `true` but the element is not empty matcher should not match any element.
666
+ *
667
+ * @param element The element to be checked.
668
+ * @param expectEmpty Default set to `true`, when set to `false` matcher expects that `anchor` is not empty;
669
+ * in editing pipeline it's not empty because it contains the `UIElement`.
670
+ */ function upcastMatcher(element, expectEmpty = true) {
671
+ const isAnchorElement = element.name === 'a';
672
+ if (!isAnchorElement) {
673
+ return null;
674
+ }
675
+ if (expectEmpty && !element.isEmpty) {
676
+ return null;
677
+ }
678
+ const hasIdAttribute = element.hasAttribute('id');
679
+ const hasNameAttribute = element.hasAttribute('name');
680
+ const hasHrefAttribute = element.hasAttribute('href');
681
+ if (hasIdAttribute && !hasHrefAttribute) {
682
+ return {
683
+ name: true,
684
+ attributes: [
685
+ 'id'
686
+ ]
687
+ };
688
+ }
689
+ if (hasNameAttribute && !hasHrefAttribute) {
690
+ return {
691
+ name: true,
692
+ attributes: [
693
+ 'name'
694
+ ]
695
+ };
696
+ }
697
+ return null;
698
+ }
699
+ /**
700
+ * A view-to-model converter that handles converting pointed or wrapped anchors with `id` and/or `name` attributes.
701
+ *
702
+ * @returns Returns a conversion callback.
703
+ */ function dataViewModelAnchorInsertion(editor) {
704
+ return (evt, data, conversionApi)=>{
705
+ const viewItem = data.viewItem;
706
+ const match = upcastMatcher(viewItem, false);
707
+ if (!match || !conversionApi.consumable.test(viewItem, match)) {
708
+ return;
709
+ }
710
+ const enableNonEmptyAnchorConversion = isEnabledNonEmptyAnchorConversion(editor);
711
+ if (!enableNonEmptyAnchorConversion && !viewItem.isEmpty) {
712
+ return;
713
+ }
714
+ const modelWriter = conversionApi.writer;
715
+ const anchorId = viewItem.getAttribute('id');
716
+ const anchorName = viewItem.getAttribute('name');
717
+ const bookmarkId = anchorId || anchorName;
718
+ const bookmark = modelWriter.createElement('bookmark', {
719
+ bookmarkId
720
+ });
721
+ if (!conversionApi.safeInsert(bookmark, data.modelCursor)) {
722
+ return;
723
+ }
724
+ conversionApi.consumable.consume(viewItem, match);
725
+ if (anchorId === anchorName) {
726
+ conversionApi.consumable.consume(viewItem, {
727
+ attributes: [
728
+ 'name'
729
+ ]
730
+ });
731
+ }
732
+ conversionApi.updateConversionResult(bookmark, data);
733
+ // Convert children uses the result of `bookmark` insertion to convert the `anchor` content
734
+ // after the bookmark element (not inside it).
735
+ const { modelCursor, modelRange } = conversionApi.convertChildren(viewItem, data.modelCursor);
736
+ data.modelCursor = modelCursor;
737
+ data.modelRange = modelWriter.createRange(data.modelRange.start, modelRange.end);
738
+ };
739
+ }
740
+ /**
741
+ * Normalize the bookmark configuration option `enableNonEmptyAnchorConversion`.
742
+ */ function isEnabledNonEmptyAnchorConversion(editor) {
743
+ const enableNonEmptyAnchorConversion = editor.config.get('bookmark.enableNonEmptyAnchorConversion');
744
+ // When not defined, option `enableNonEmptyAnchorConversion` by default is set to `true`.
745
+ return enableNonEmptyAnchorConversion !== undefined ? enableNonEmptyAnchorConversion : true;
746
+ }
747
+
748
+ const bookmarkIcon = icons.bookmark;
749
+ const VISUAL_SELECTION_MARKER_NAME = 'bookmark-ui';
750
+ /**
751
+ * The UI plugin of the bookmark feature.
752
+ *
753
+ * It registers the `'bookmark'` UI button in the editor's {@link module:ui/componentfactory~ComponentFactory component factory}
754
+ * which inserts the `bookmark` element upon selection.
755
+ */ class BookmarkUI extends Plugin {
756
+ /**
757
+ * The actions view displayed inside of the balloon.
758
+ */ actionsView = null;
759
+ /**
760
+ * The form view displayed inside the balloon.
761
+ */ formView = null;
762
+ /**
763
+ * The contextual balloon plugin instance.
764
+ */ _balloon;
765
+ /**
766
+ * @inheritDoc
767
+ */ static get requires() {
768
+ return [
769
+ BookmarkEditing,
770
+ ContextualBalloon
771
+ ];
772
+ }
773
+ /**
774
+ * @inheritDoc
775
+ */ static get pluginName() {
776
+ return 'BookmarkUI';
777
+ }
778
+ /**
779
+ * @inheritDoc
780
+ */ static get isOfficialPlugin() {
781
+ return true;
782
+ }
783
+ /**
784
+ * @inheritDoc
785
+ */ init() {
786
+ const editor = this.editor;
787
+ editor.editing.view.addObserver(ClickObserver);
788
+ this._balloon = editor.plugins.get(ContextualBalloon);
789
+ // Create toolbar buttons.
790
+ this._createToolbarBookmarkButton();
791
+ this._enableBalloonActivators();
792
+ // Renders a fake visual selection marker on an expanded selection.
793
+ editor.conversion.for('editingDowncast').markerToHighlight({
794
+ model: VISUAL_SELECTION_MARKER_NAME,
795
+ view: {
796
+ classes: [
797
+ 'ck-fake-bookmark-selection'
798
+ ]
799
+ }
800
+ });
801
+ // Renders a fake visual selection marker on a collapsed selection.
802
+ editor.conversion.for('editingDowncast').markerToElement({
803
+ model: VISUAL_SELECTION_MARKER_NAME,
804
+ view: (data, { writer })=>{
805
+ if (!data.markerRange.isCollapsed) {
806
+ return null;
807
+ }
808
+ const markerElement = writer.createUIElement('span');
809
+ writer.addClass([
810
+ 'ck-fake-bookmark-selection',
811
+ 'ck-fake-bookmark-selection_collapsed'
812
+ ], markerElement);
813
+ return markerElement;
814
+ }
815
+ });
816
+ }
817
+ /**
818
+ * @inheritDoc
819
+ */ destroy() {
820
+ super.destroy();
821
+ // Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
822
+ if (this.formView) {
823
+ this.formView.destroy();
824
+ }
825
+ if (this.actionsView) {
826
+ this.actionsView.destroy();
827
+ }
828
+ }
829
+ /**
830
+ * Creates views.
831
+ */ _createViews() {
832
+ this.actionsView = this._createActionsView();
833
+ this.formView = this._createFormView();
834
+ // Attach lifecycle actions to the the balloon.
835
+ this._enableUserBalloonInteractions();
836
+ }
837
+ /**
838
+ * Creates the {@link module:bookmark/ui/bookmarkactionsview~BookmarkActionsView} instance.
839
+ */ _createActionsView() {
840
+ const editor = this.editor;
841
+ const actionsView = new BookmarkActionsView(editor.locale);
842
+ const updateBookmarkCommand = editor.commands.get('updateBookmark');
843
+ const deleteCommand = editor.commands.get('delete');
844
+ actionsView.bind('id').to(updateBookmarkCommand, 'value');
845
+ actionsView.editButtonView.bind('isEnabled').to(updateBookmarkCommand);
846
+ actionsView.removeButtonView.bind('isEnabled').to(deleteCommand);
847
+ // Display edit form view after clicking on the "Edit" button.
848
+ this.listenTo(actionsView, 'edit', ()=>{
849
+ this._addFormView();
850
+ });
851
+ // Execute remove command after clicking on the "Remove" button.
852
+ this.listenTo(actionsView, 'remove', ()=>{
853
+ this._hideUI();
854
+ editor.execute('delete');
855
+ });
856
+ // Close the panel on esc key press when the **actions have focus**.
857
+ actionsView.keystrokes.set('Esc', (data, cancel)=>{
858
+ this._hideUI();
859
+ cancel();
860
+ });
861
+ return actionsView;
862
+ }
863
+ /**
864
+ * Creates the {@link module:bookmark/ui/bookmarkformview~BookmarkFormView} instance.
865
+ */ _createFormView() {
866
+ const editor = this.editor;
867
+ const locale = editor.locale;
868
+ const insertBookmarkCommand = editor.commands.get('insertBookmark');
869
+ const updateBookmarkCommand = editor.commands.get('updateBookmark');
870
+ const commands = [
871
+ insertBookmarkCommand,
872
+ updateBookmarkCommand
873
+ ];
874
+ const formView = new (CssTransitionDisablerMixin(BookmarkFormView))(locale, getFormValidators(editor));
875
+ formView.idInputView.fieldView.bind('value').to(updateBookmarkCommand, 'value');
876
+ // Form elements should be read-only when corresponding commands are disabled.
877
+ formView.idInputView.bind('isEnabled').toMany(commands, 'isEnabled', (...areEnabled)=>areEnabled.some((isEnabled)=>isEnabled));
878
+ // Disable the "save" button if the command is disabled.
879
+ formView.buttonView.bind('isEnabled').toMany(commands, 'isEnabled', (...areEnabled)=>areEnabled.some((isEnabled)=>isEnabled));
880
+ // Execute link command after clicking the "Save" button.
881
+ this.listenTo(formView, 'submit', ()=>{
882
+ if (formView.isValid()) {
883
+ const value = formView.id;
884
+ if (this._getSelectedBookmarkElement()) {
885
+ editor.execute('updateBookmark', {
886
+ bookmarkId: value
887
+ });
888
+ } else {
889
+ editor.execute('insertBookmark', {
890
+ bookmarkId: value
891
+ });
892
+ }
893
+ this._closeFormView();
894
+ }
895
+ });
896
+ // Update balloon position when form error changes.
897
+ this.listenTo(formView.idInputView, 'change:errorText', ()=>{
898
+ editor.ui.update();
899
+ });
900
+ // Close the panel on esc key press when the **form has focus**.
901
+ formView.keystrokes.set('Esc', (data, cancel)=>{
902
+ this._closeFormView();
903
+ cancel();
904
+ });
905
+ return formView;
906
+ }
907
+ /**
908
+ * Creates a toolbar Bookmark button. Clicking this button will show
909
+ * a {@link #_balloon} attached to the selection.
910
+ */ _createToolbarBookmarkButton() {
911
+ const editor = this.editor;
912
+ editor.ui.componentFactory.add('bookmark', ()=>{
913
+ const buttonView = this._createButton(ButtonView);
914
+ buttonView.set({
915
+ tooltip: true
916
+ });
917
+ return buttonView;
918
+ });
919
+ editor.ui.componentFactory.add('menuBar:bookmark', ()=>{
920
+ return this._createButton(MenuBarMenuListItemButtonView);
921
+ });
922
+ }
923
+ /**
924
+ * Creates a button for `bookmark` command to use either in toolbar or in menu bar.
925
+ */ _createButton(ButtonClass) {
926
+ const editor = this.editor;
927
+ const locale = editor.locale;
928
+ const view = new ButtonClass(locale);
929
+ const insertCommand = editor.commands.get('insertBookmark');
930
+ const updateCommand = editor.commands.get('updateBookmark');
931
+ const t = locale.t;
932
+ view.set({
933
+ label: t('Bookmark'),
934
+ icon: bookmarkIcon
935
+ });
936
+ // Execute the command.
937
+ this.listenTo(view, 'execute', ()=>this._showUI(true));
938
+ view.bind('isEnabled').toMany([
939
+ insertCommand,
940
+ updateCommand
941
+ ], 'isEnabled', (...areEnabled)=>areEnabled.some((isEnabled)=>isEnabled));
942
+ view.bind('isOn').to(updateCommand, 'value', (value)=>!!value);
943
+ return view;
944
+ }
945
+ /**
946
+ * Attaches actions that control whether the balloon panel containing the
947
+ * {@link #formView} should be displayed.
948
+ */ _enableBalloonActivators() {
949
+ const editor = this.editor;
950
+ const viewDocument = editor.editing.view.document;
951
+ // Handle click on view document and show panel when selection is placed inside the bookmark element.
952
+ // Keep panel open until selection will be inside the same bookmark element.
953
+ this.listenTo(viewDocument, 'click', ()=>{
954
+ const bookmark = this._getSelectedBookmarkElement();
955
+ if (bookmark) {
956
+ // Then show panel but keep focus inside editor editable.
957
+ this._showUI();
958
+ }
959
+ });
960
+ }
961
+ /**
962
+ * Attaches actions that control whether the balloon panel containing the
963
+ * {@link #formView} is visible or not.
964
+ */ _enableUserBalloonInteractions() {
965
+ // Focus the form if the balloon is visible and the Tab key has been pressed.
966
+ this.editor.keystrokes.set('Tab', (data, cancel)=>{
967
+ if (this._areActionsVisible && !this.actionsView.focusTracker.isFocused) {
968
+ this.actionsView.focus();
969
+ cancel();
970
+ }
971
+ }, {
972
+ // Use the high priority because the bookmark UI navigation is more important
973
+ // than other feature's actions, e.g. list indentation.
974
+ priority: 'high'
975
+ });
976
+ // Close the panel on the Esc key press when the editable has focus and the balloon is visible.
977
+ this.editor.keystrokes.set('Esc', (data, cancel)=>{
978
+ if (this._isUIVisible) {
979
+ this._hideUI();
980
+ cancel();
981
+ }
982
+ });
983
+ // Close on click outside of balloon panel element.
984
+ clickOutsideHandler({
985
+ emitter: this.formView,
986
+ activator: ()=>this._isUIInPanel,
987
+ contextElements: ()=>[
988
+ this._balloon.view.element
989
+ ],
990
+ callback: ()=>this._hideUI()
991
+ });
992
+ }
993
+ /**
994
+ * Updates the button label. If bookmark is selected label is set to 'Update' otherwise
995
+ * it is 'Insert'.
996
+ */ _updateFormButtonLabel(isBookmarkSelected) {
997
+ const t = this.editor.locale.t;
998
+ this.formView.buttonView.label = isBookmarkSelected ? t('Update') : t('Insert');
999
+ }
1000
+ /**
1001
+ * Adds the {@link #actionsView} to the {@link #_balloon}.
1002
+ *
1003
+ * @internal
1004
+ */ _addActionsView() {
1005
+ if (!this.actionsView) {
1006
+ this._createViews();
1007
+ }
1008
+ if (this._areActionsInPanel) {
1009
+ return;
1010
+ }
1011
+ this._balloon.add({
1012
+ view: this.actionsView,
1013
+ position: this._getBalloonPositionData()
1014
+ });
1015
+ }
1016
+ /**
1017
+ * Adds the {@link #formView} to the {@link #_balloon}.
1018
+ */ _addFormView() {
1019
+ if (!this.formView) {
1020
+ this._createViews();
1021
+ }
1022
+ if (this._isFormInPanel) {
1023
+ return;
1024
+ }
1025
+ const editor = this.editor;
1026
+ const updateBookmarkCommand = editor.commands.get('updateBookmark');
1027
+ this.formView.disableCssTransitions();
1028
+ this.formView.resetFormStatus();
1029
+ this._balloon.add({
1030
+ view: this.formView,
1031
+ position: this._getBalloonPositionData()
1032
+ });
1033
+ this.formView.idInputView.fieldView.value = updateBookmarkCommand.value || '';
1034
+ // Select input when form view is currently visible.
1035
+ if (this._balloon.visibleView === this.formView) {
1036
+ this.formView.idInputView.fieldView.select();
1037
+ }
1038
+ this.formView.enableCssTransitions();
1039
+ }
1040
+ /**
1041
+ * Closes the form view. Decides whether the balloon should be hidden completely.
1042
+ */ _closeFormView() {
1043
+ const updateBookmarkCommand = this.editor.commands.get('updateBookmark');
1044
+ if (updateBookmarkCommand.value !== undefined) {
1045
+ this._removeFormView();
1046
+ } else {
1047
+ this._hideUI();
1048
+ }
1049
+ }
1050
+ /**
1051
+ * Removes the {@link #formView} from the {@link #_balloon}.
1052
+ */ _removeFormView() {
1053
+ if (this._isFormInPanel) {
1054
+ // Blur the input element before removing it from DOM to prevent issues in some browsers.
1055
+ // See https://github.com/ckeditor/ckeditor5/issues/1501.
1056
+ this.formView.buttonView.focus();
1057
+ // Reset the ID field to update the state of the submit button.
1058
+ this.formView.idInputView.fieldView.reset();
1059
+ this._balloon.remove(this.formView);
1060
+ // Because the form has an input which has focus, the focus must be brought back
1061
+ // to the editor. Otherwise, it would be lost.
1062
+ this.editor.editing.view.focus();
1063
+ this._hideFakeVisualSelection();
1064
+ }
1065
+ }
1066
+ /**
1067
+ * Shows the correct UI type. It is either {@link #formView} or {@link #actionsView}.
1068
+ */ _showUI(forceVisible = false) {
1069
+ if (!this.formView) {
1070
+ this._createViews();
1071
+ }
1072
+ // When there's no bookmark under the selection, go straight to the editing UI.
1073
+ if (!this._getSelectedBookmarkElement()) {
1074
+ // Show visual selection on a text without a bookmark when the contextual balloon is displayed.
1075
+ this._showFakeVisualSelection();
1076
+ this._addActionsView();
1077
+ // Be sure panel with bookmark is visible.
1078
+ if (forceVisible) {
1079
+ this._balloon.showStack('main');
1080
+ }
1081
+ this._addFormView();
1082
+ } else {
1083
+ // Go to the editing UI if actions are already visible.
1084
+ if (this._areActionsVisible) {
1085
+ this._addFormView();
1086
+ } else {
1087
+ this._addActionsView();
1088
+ }
1089
+ // Be sure panel with bookmark is visible.
1090
+ if (forceVisible) {
1091
+ this._balloon.showStack('main');
1092
+ }
1093
+ }
1094
+ // Begin responding to ui#update once the UI is added.
1095
+ this._startUpdatingUI();
1096
+ }
1097
+ /**
1098
+ * Removes the {@link #formView} from the {@link #_balloon}.
1099
+ *
1100
+ * See {@link #_addFormView}, {@link #_addActionsView}.
1101
+ */ _hideUI() {
1102
+ if (!this._isUIInPanel) {
1103
+ return;
1104
+ }
1105
+ const editor = this.editor;
1106
+ this.stopListening(editor.ui, 'update');
1107
+ this.stopListening(this._balloon, 'change:visibleView');
1108
+ // Make sure the focus always gets back to the editable _before_ removing the focused form view.
1109
+ // Doing otherwise causes issues in some browsers. See https://github.com/ckeditor/ckeditor5-link/issues/193.
1110
+ editor.editing.view.focus();
1111
+ // Remove form first because it's on top of the stack.
1112
+ this._removeFormView();
1113
+ // Then remove the actions view because it's beneath the form.
1114
+ this._balloon.remove(this.actionsView);
1115
+ this._hideFakeVisualSelection();
1116
+ }
1117
+ /**
1118
+ * Makes the UI react to the {@link module:ui/editorui/editorui~EditorUI#event:update} event to
1119
+ * reposition itself when the editor UI should be refreshed.
1120
+ *
1121
+ * See: {@link #_hideUI} to learn when the UI stops reacting to the `update` event.
1122
+ */ _startUpdatingUI() {
1123
+ const editor = this.editor;
1124
+ const viewDocument = editor.editing.view.document;
1125
+ let prevSelectedBookmark = this._getSelectedBookmarkElement();
1126
+ let prevSelectionParent = getSelectionParent();
1127
+ this._updateFormButtonLabel(!!prevSelectedBookmark);
1128
+ const update = ()=>{
1129
+ const selectedBookmark = this._getSelectedBookmarkElement();
1130
+ const selectionParent = getSelectionParent();
1131
+ // Hide the panel if:
1132
+ //
1133
+ // * the selection went out of the EXISTING bookmark element. E.g. user moved the caret out
1134
+ // of the bookmark,
1135
+ // * the selection went to a different parent when creating a NEW bookmark. E.g. someone
1136
+ // else modified the document.
1137
+ // * the selection has expanded (e.g. displaying bookmark actions then pressing SHIFT+Right arrow).
1138
+ //
1139
+ if (prevSelectedBookmark && !selectedBookmark || !prevSelectedBookmark && selectionParent !== prevSelectionParent) {
1140
+ this._hideUI();
1141
+ } else if (this._isUIVisible) {
1142
+ // If still in a bookmark element, simply update the position of the balloon.
1143
+ // If there was no bookmark (e.g. inserting one), the balloon must be moved
1144
+ // to the new position in the editing view (a new native DOM range).
1145
+ this._balloon.updatePosition(this._getBalloonPositionData());
1146
+ }
1147
+ this._updateFormButtonLabel(!!prevSelectedBookmark);
1148
+ prevSelectedBookmark = selectedBookmark;
1149
+ prevSelectionParent = selectionParent;
1150
+ };
1151
+ function getSelectionParent() {
1152
+ return viewDocument.selection.focus.getAncestors().reverse().find((node)=>node.is('element'));
1153
+ }
1154
+ this.listenTo(editor.ui, 'update', update);
1155
+ this.listenTo(this._balloon, 'change:visibleView', update);
1156
+ }
1157
+ /**
1158
+ * Returns `true` when {@link #formView} is in the {@link #_balloon}.
1159
+ */ get _isFormInPanel() {
1160
+ return !!this.formView && this._balloon.hasView(this.formView);
1161
+ }
1162
+ /**
1163
+ * Returns `true` when {@link #actionsView} is in the {@link #_balloon}.
1164
+ */ get _areActionsInPanel() {
1165
+ return !!this.actionsView && this._balloon.hasView(this.actionsView);
1166
+ }
1167
+ /**
1168
+ * Returns `true` when {@link #actionsView} is in the {@link #_balloon} and it is
1169
+ * currently visible.
1170
+ */ get _areActionsVisible() {
1171
+ return !!this.actionsView && this._balloon.visibleView === this.actionsView;
1172
+ }
1173
+ /**
1174
+ * Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon}.
1175
+ */ get _isUIInPanel() {
1176
+ return this._isFormInPanel || this._areActionsInPanel;
1177
+ }
1178
+ /**
1179
+ * Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon} and it is
1180
+ * currently visible.
1181
+ */ get _isUIVisible() {
1182
+ const visibleView = this._balloon.visibleView;
1183
+ return !!this.formView && visibleView == this.formView || this._areActionsVisible;
1184
+ }
1185
+ /**
1186
+ * Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
1187
+ * to the target element or selection.
1188
+ */ _getBalloonPositionData() {
1189
+ const view = this.editor.editing.view;
1190
+ const model = this.editor.model;
1191
+ let target;
1192
+ const bookmarkElement = this._getSelectedBookmarkElement();
1193
+ if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
1194
+ // There are cases when we highlight selection using a marker (#7705, #4721).
1195
+ const markerViewElements = Array.from(this.editor.editing.mapper.markerNameToElements(VISUAL_SELECTION_MARKER_NAME));
1196
+ const newRange = view.createRange(view.createPositionBefore(markerViewElements[0]), view.createPositionAfter(markerViewElements[markerViewElements.length - 1]));
1197
+ target = view.domConverter.viewRangeToDom(newRange);
1198
+ } else if (bookmarkElement) {
1199
+ target = ()=>{
1200
+ const mapper = this.editor.editing.mapper;
1201
+ const domConverter = view.domConverter;
1202
+ const viewElement = mapper.toViewElement(bookmarkElement);
1203
+ return domConverter.mapViewToDom(viewElement);
1204
+ };
1205
+ }
1206
+ return target && {
1207
+ target
1208
+ };
1209
+ }
1210
+ /**
1211
+ * Returns the bookmark {@link module:engine/view/attributeelement~AttributeElement} under
1212
+ * the {@link module:engine/view/document~Document editing view's} selection or `null`
1213
+ * if there is none.
1214
+ */ _getSelectedBookmarkElement() {
1215
+ const selection = this.editor.model.document.selection;
1216
+ const element = selection.getSelectedElement();
1217
+ if (element && element.is('element', 'bookmark')) {
1218
+ return element;
1219
+ }
1220
+ return null;
1221
+ }
1222
+ /**
1223
+ * Displays a fake visual selection when the contextual balloon is displayed.
1224
+ *
1225
+ * This adds a 'bookmark-ui' marker into the document that is rendered as a highlight on selected text fragment.
1226
+ */ _showFakeVisualSelection() {
1227
+ const model = this.editor.model;
1228
+ model.change((writer)=>{
1229
+ const range = model.document.selection.getFirstRange();
1230
+ if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
1231
+ writer.updateMarker(VISUAL_SELECTION_MARKER_NAME, {
1232
+ range
1233
+ });
1234
+ } else {
1235
+ if (range.start.isAtEnd) {
1236
+ const startPosition = range.start.getLastMatchingPosition(({ item })=>!model.schema.isContent(item), {
1237
+ boundaries: range
1238
+ });
1239
+ writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
1240
+ usingOperation: false,
1241
+ affectsData: false,
1242
+ range: writer.createRange(startPosition, range.end)
1243
+ });
1244
+ } else {
1245
+ writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
1246
+ usingOperation: false,
1247
+ affectsData: false,
1248
+ range
1249
+ });
1250
+ }
1251
+ }
1252
+ });
1253
+ }
1254
+ /**
1255
+ * Hides the fake visual selection created in {@link #_showFakeVisualSelection}.
1256
+ */ _hideFakeVisualSelection() {
1257
+ const model = this.editor.model;
1258
+ if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
1259
+ model.change((writer)=>{
1260
+ writer.removeMarker(VISUAL_SELECTION_MARKER_NAME);
1261
+ });
1262
+ }
1263
+ }
1264
+ }
1265
+ /**
1266
+ * Returns bookmark form validation callbacks.
1267
+ */ function getFormValidators(editor) {
1268
+ const { t } = editor;
1269
+ const bookmarkEditing = editor.plugins.get(BookmarkEditing);
1270
+ return [
1271
+ (form)=>{
1272
+ if (!form.id) {
1273
+ return t('Bookmark must not be empty.');
1274
+ }
1275
+ },
1276
+ (form)=>{
1277
+ if (form.id && /\s/.test(form.id)) {
1278
+ return t('Bookmark name cannot contain space characters.');
1279
+ }
1280
+ },
1281
+ (form)=>{
1282
+ const selectedElement = editor.model.document.selection.getSelectedElement();
1283
+ const existingBookmarkForId = bookmarkEditing.getElementForBookmarkId(form.id);
1284
+ // Accept change of bookmark ID if no real change is happening (edit -> submit, without changes).
1285
+ if (selectedElement === existingBookmarkForId) {
1286
+ return;
1287
+ }
1288
+ if (existingBookmarkForId) {
1289
+ return t('Bookmark name already exists.');
1290
+ }
1291
+ }
1292
+ ];
1293
+ }
1294
+
1295
+ /**
1296
+ * The bookmark feature.
1297
+ *
1298
+ * For a detailed overview, check the {@glink features/bookmarks Bookmarks} feature guide.
1299
+ */ class Bookmark extends Plugin {
1300
+ /**
1301
+ * @inheritDoc
1302
+ */ static get pluginName() {
1303
+ return 'Bookmark';
1304
+ }
1305
+ /**
1306
+ * @inheritDoc
1307
+ */ static get requires() {
1308
+ return [
1309
+ BookmarkEditing,
1310
+ BookmarkUI,
1311
+ Widget
1312
+ ];
1313
+ }
1314
+ /**
1315
+ * @inheritDoc
1316
+ */ static get isOfficialPlugin() {
1317
+ return true;
1318
+ }
1319
+ }
1320
+
1321
+ export { Bookmark, BookmarkEditing, BookmarkUI, InsertBookmarkCommand, UpdateBookmarkCommand };
1322
+ //# sourceMappingURL=index.js.map