@ckeditor/ckeditor5-bookmark 0.0.0-nightly-next-20260107.0 → 0.0.0-nightly-20260108.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.
- package/LICENSE.md +1 -1
- package/build/bookmark.js +5 -0
- package/build/translations/af.js +1 -0
- package/build/translations/ar.js +1 -0
- package/build/translations/ast.js +1 -0
- package/build/translations/az.js +1 -0
- package/build/translations/be.js +1 -0
- package/build/translations/bg.js +1 -0
- package/build/translations/bn.js +1 -0
- package/build/translations/bs.js +1 -0
- package/build/translations/ca.js +1 -0
- package/build/translations/cs.js +1 -0
- package/build/translations/da.js +1 -0
- package/build/translations/de-ch.js +1 -0
- package/build/translations/de.js +1 -0
- package/build/translations/el.js +1 -0
- package/build/translations/en-au.js +1 -0
- package/build/translations/en-gb.js +1 -0
- package/build/translations/eo.js +1 -0
- package/build/translations/es-co.js +1 -0
- package/build/translations/es.js +1 -0
- package/build/translations/et.js +1 -0
- package/build/translations/eu.js +1 -0
- package/build/translations/fa.js +1 -0
- package/build/translations/fi.js +1 -0
- package/build/translations/fr.js +1 -0
- package/build/translations/gl.js +1 -0
- package/build/translations/gu.js +1 -0
- package/build/translations/he.js +1 -0
- package/build/translations/hi.js +1 -0
- package/build/translations/hr.js +1 -0
- package/build/translations/hu.js +1 -0
- package/build/translations/hy.js +1 -0
- package/build/translations/id.js +1 -0
- package/build/translations/it.js +1 -0
- package/build/translations/ja.js +1 -0
- package/build/translations/jv.js +1 -0
- package/build/translations/kk.js +1 -0
- package/build/translations/km.js +1 -0
- package/build/translations/kn.js +1 -0
- package/build/translations/ko.js +1 -0
- package/build/translations/ku.js +1 -0
- package/build/translations/lt.js +1 -0
- package/build/translations/lv.js +1 -0
- package/build/translations/ms.js +1 -0
- package/build/translations/nb.js +1 -0
- package/build/translations/ne.js +1 -0
- package/build/translations/nl.js +1 -0
- package/build/translations/no.js +1 -0
- package/build/translations/oc.js +1 -0
- package/build/translations/pl.js +1 -0
- package/build/translations/pt-br.js +1 -0
- package/build/translations/pt.js +1 -0
- package/build/translations/ro.js +1 -0
- package/build/translations/ru.js +1 -0
- package/build/translations/si.js +1 -0
- package/build/translations/sk.js +1 -0
- package/build/translations/sl.js +1 -0
- package/build/translations/sq.js +1 -0
- package/build/translations/sr-latn.js +1 -0
- package/build/translations/sr.js +1 -0
- package/build/translations/sv.js +1 -0
- package/build/translations/th.js +1 -0
- package/build/translations/ti.js +1 -0
- package/build/translations/tk.js +1 -0
- package/build/translations/tr.js +1 -0
- package/build/translations/tt.js +1 -0
- package/build/translations/ug.js +1 -0
- package/build/translations/uk.js +1 -0
- package/build/translations/ur.js +1 -0
- package/build/translations/uz.js +1 -0
- package/build/translations/vi.js +1 -0
- package/build/translations/zh-cn.js +1 -0
- package/build/translations/zh.js +1 -0
- package/ckeditor5-metadata.json +1 -1
- package/dist/index.css +5 -5
- package/dist/index.css.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/lang/translations/af.po +1 -1
- package/lang/translations/ar.po +1 -1
- package/lang/translations/ast.po +1 -1
- package/lang/translations/az.po +1 -1
- package/lang/translations/be.po +1 -1
- package/lang/translations/bg.po +1 -1
- package/lang/translations/bn.po +1 -1
- package/lang/translations/bs.po +1 -1
- package/lang/translations/ca.po +1 -1
- package/lang/translations/cs.po +1 -1
- package/lang/translations/da.po +1 -1
- package/lang/translations/de-ch.po +1 -1
- package/lang/translations/de.po +1 -1
- package/lang/translations/el.po +1 -1
- package/lang/translations/en-au.po +1 -1
- package/lang/translations/en-gb.po +1 -1
- package/lang/translations/en.po +1 -1
- package/lang/translations/eo.po +1 -1
- package/lang/translations/es-co.po +1 -1
- package/lang/translations/es.po +1 -1
- package/lang/translations/et.po +1 -1
- package/lang/translations/eu.po +1 -1
- package/lang/translations/fa.po +1 -1
- package/lang/translations/fi.po +1 -1
- package/lang/translations/fr.po +1 -1
- package/lang/translations/gl.po +1 -1
- package/lang/translations/gu.po +1 -1
- package/lang/translations/he.po +1 -1
- package/lang/translations/hi.po +1 -1
- package/lang/translations/hr.po +1 -1
- package/lang/translations/hu.po +1 -1
- package/lang/translations/hy.po +1 -1
- package/lang/translations/id.po +1 -1
- package/lang/translations/it.po +1 -1
- package/lang/translations/ja.po +1 -1
- package/lang/translations/jv.po +1 -1
- package/lang/translations/kk.po +1 -1
- package/lang/translations/km.po +1 -1
- package/lang/translations/kn.po +1 -1
- package/lang/translations/ko.po +1 -1
- package/lang/translations/ku.po +1 -1
- package/lang/translations/lt.po +1 -1
- package/lang/translations/lv.po +1 -1
- package/lang/translations/ms.po +1 -1
- package/lang/translations/nb.po +1 -1
- package/lang/translations/ne.po +1 -1
- package/lang/translations/nl.po +1 -1
- package/lang/translations/no.po +1 -1
- package/lang/translations/oc.po +1 -1
- package/lang/translations/pl.po +1 -1
- package/lang/translations/pt-br.po +1 -1
- package/lang/translations/pt.po +1 -1
- package/lang/translations/ro.po +1 -1
- package/lang/translations/ru.po +1 -1
- package/lang/translations/si.po +1 -1
- package/lang/translations/sk.po +1 -1
- package/lang/translations/sl.po +1 -1
- package/lang/translations/sq.po +1 -1
- package/lang/translations/sr-latn.po +1 -1
- package/lang/translations/sr.po +1 -1
- package/lang/translations/sv.po +1 -1
- package/lang/translations/th.po +1 -1
- package/lang/translations/ti.po +1 -1
- package/lang/translations/tk.po +1 -1
- package/lang/translations/tr.po +1 -1
- package/lang/translations/tt.po +1 -1
- package/lang/translations/ug.po +1 -1
- package/lang/translations/uk.po +1 -1
- package/lang/translations/ur.po +1 -1
- package/lang/translations/uz.po +1 -1
- package/lang/translations/vi.po +1 -1
- package/lang/translations/zh-cn.po +1 -1
- package/lang/translations/zh.po +1 -1
- package/package.json +34 -25
- package/{dist → src}/augmentation.d.ts +0 -4
- package/src/augmentation.js +5 -0
- package/{dist → src}/bookmark.d.ts +2 -6
- package/src/bookmark.js +36 -0
- package/{dist → src}/bookmarkconfig.d.ts +0 -4
- package/src/bookmarkconfig.js +5 -0
- package/{dist → src}/bookmarkediting.d.ts +2 -6
- package/src/bookmarkediting.js +238 -0
- package/{dist → src}/bookmarkui.d.ts +3 -7
- package/src/bookmarkui.js +591 -0
- package/{dist → src}/index.d.ts +0 -4
- package/src/index.js +15 -0
- package/{dist → src}/insertbookmarkcommand.d.ts +1 -5
- package/src/insertbookmarkcommand.js +113 -0
- package/{dist → src}/ui/bookmarkformview.d.ts +2 -6
- package/src/ui/bookmarkformview.js +244 -0
- package/{dist → src}/updatebookmarkcommand.d.ts +1 -5
- package/src/updatebookmarkcommand.js +75 -0
- package/{dist → src}/utils.d.ts +0 -4
- package/src/utils.js +21 -0
- package/theme/bookmark.css +1 -1
- package/theme/bookmarkform.css +1 -1
- package/theme/bookmarktoolbar.css +1 -1
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
|
+
*/
|
|
5
|
+
import { Plugin } from 'ckeditor5/src/core.js';
|
|
6
|
+
import { ButtonView, ContextualBalloon, CssTransitionDisablerMixin, MenuBarMenuListItemButtonView, clickOutsideHandler, LabelView, BalloonPanelView } from 'ckeditor5/src/ui.js';
|
|
7
|
+
import { IconBookmark, IconRemove, IconBookmarkMedium, IconBookmarkSmall, IconPencil } from 'ckeditor5/src/icons.js';
|
|
8
|
+
import { isWidget, WidgetToolbarRepository } from 'ckeditor5/src/widget.js';
|
|
9
|
+
import { BookmarkFormView } from './ui/bookmarkformview.js';
|
|
10
|
+
import { BookmarkEditing } from './bookmarkediting.js';
|
|
11
|
+
import '../theme/bookmarktoolbar.css';
|
|
12
|
+
const VISUAL_SELECTION_MARKER_NAME = 'bookmark-ui';
|
|
13
|
+
/**
|
|
14
|
+
* The UI plugin of the bookmark feature.
|
|
15
|
+
*
|
|
16
|
+
* It registers the `'bookmark'` UI button in the editor's {@link module:ui/componentfactory~ComponentFactory component factory}
|
|
17
|
+
* which inserts the `bookmark` element upon selection.
|
|
18
|
+
*/
|
|
19
|
+
export class BookmarkUI extends Plugin {
|
|
20
|
+
/**
|
|
21
|
+
* The form view displayed inside the balloon.
|
|
22
|
+
*/
|
|
23
|
+
formView = null;
|
|
24
|
+
/**
|
|
25
|
+
* The contextual balloon plugin instance.
|
|
26
|
+
*/
|
|
27
|
+
_balloon;
|
|
28
|
+
/**
|
|
29
|
+
* @inheritDoc
|
|
30
|
+
*/
|
|
31
|
+
static get requires() {
|
|
32
|
+
return [BookmarkEditing, ContextualBalloon, WidgetToolbarRepository];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* @inheritDoc
|
|
36
|
+
*/
|
|
37
|
+
static get pluginName() {
|
|
38
|
+
return 'BookmarkUI';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* @inheritDoc
|
|
42
|
+
*/
|
|
43
|
+
static get isOfficialPlugin() {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @inheritDoc
|
|
48
|
+
*/
|
|
49
|
+
init() {
|
|
50
|
+
const editor = this.editor;
|
|
51
|
+
this._balloon = editor.plugins.get(ContextualBalloon);
|
|
52
|
+
// Register the link provider in link plugin to display the link form.
|
|
53
|
+
if (editor.plugins.has('LinkUI')) {
|
|
54
|
+
this._registerLinkProvider();
|
|
55
|
+
}
|
|
56
|
+
// Create toolbar buttons.
|
|
57
|
+
this._registerComponents();
|
|
58
|
+
// Renders a fake visual selection marker on an expanded selection.
|
|
59
|
+
editor.conversion.for('editingDowncast').markerToHighlight({
|
|
60
|
+
model: VISUAL_SELECTION_MARKER_NAME,
|
|
61
|
+
view: {
|
|
62
|
+
classes: ['ck-fake-bookmark-selection']
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// Renders a fake visual selection marker on a collapsed selection.
|
|
66
|
+
editor.conversion.for('editingDowncast').markerToElement({
|
|
67
|
+
model: VISUAL_SELECTION_MARKER_NAME,
|
|
68
|
+
view: (data, { writer }) => {
|
|
69
|
+
if (!data.markerRange.isCollapsed) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const markerElement = writer.createUIElement('span');
|
|
73
|
+
writer.addClass(['ck-fake-bookmark-selection', 'ck-fake-bookmark-selection_collapsed'], markerElement);
|
|
74
|
+
return markerElement;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* @inheritDoc
|
|
80
|
+
*/
|
|
81
|
+
afterInit() {
|
|
82
|
+
const editor = this.editor;
|
|
83
|
+
const t = editor.locale.t;
|
|
84
|
+
const widgetToolbarRepository = this.editor.plugins.get(WidgetToolbarRepository);
|
|
85
|
+
const defaultPositions = BalloonPanelView.defaultPositions;
|
|
86
|
+
widgetToolbarRepository.register('bookmark', {
|
|
87
|
+
ariaLabel: t('Bookmark toolbar'),
|
|
88
|
+
items: editor.config.get('bookmark.toolbar'),
|
|
89
|
+
getRelatedElement: getSelectedBookmarkWidget,
|
|
90
|
+
balloonClassName: 'ck-bookmark-balloon ck-toolbar-container',
|
|
91
|
+
// Override positions to the same list as for balloon panel default
|
|
92
|
+
// so widget toolbar will try to use same position as form view.
|
|
93
|
+
positions: [
|
|
94
|
+
defaultPositions.southArrowNorth,
|
|
95
|
+
defaultPositions.southArrowNorthMiddleWest,
|
|
96
|
+
defaultPositions.southArrowNorthMiddleEast,
|
|
97
|
+
defaultPositions.southArrowNorthWest,
|
|
98
|
+
defaultPositions.southArrowNorthEast,
|
|
99
|
+
defaultPositions.northArrowSouth,
|
|
100
|
+
defaultPositions.northArrowSouthMiddleWest,
|
|
101
|
+
defaultPositions.northArrowSouthMiddleEast,
|
|
102
|
+
defaultPositions.northArrowSouthWest,
|
|
103
|
+
defaultPositions.northArrowSouthEast,
|
|
104
|
+
defaultPositions.viewportStickyNorth
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* @inheritDoc
|
|
110
|
+
*/
|
|
111
|
+
destroy() {
|
|
112
|
+
super.destroy();
|
|
113
|
+
// Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
|
|
114
|
+
if (this.formView) {
|
|
115
|
+
this.formView.destroy();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Creates views.
|
|
120
|
+
*/
|
|
121
|
+
_createViews() {
|
|
122
|
+
this.formView = this._createFormView();
|
|
123
|
+
// Attach lifecycle actions to the the balloon.
|
|
124
|
+
this._enableUserBalloonInteractions();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Creates the {@link module:bookmark/ui/bookmarkformview~BookmarkFormView} instance.
|
|
128
|
+
*/
|
|
129
|
+
_createFormView() {
|
|
130
|
+
const editor = this.editor;
|
|
131
|
+
const locale = editor.locale;
|
|
132
|
+
const t = locale.t;
|
|
133
|
+
const insertBookmarkCommand = editor.commands.get('insertBookmark');
|
|
134
|
+
const updateBookmarkCommand = editor.commands.get('updateBookmark');
|
|
135
|
+
const commands = [insertBookmarkCommand, updateBookmarkCommand];
|
|
136
|
+
const formView = new (CssTransitionDisablerMixin(BookmarkFormView))(locale, getFormValidators(editor));
|
|
137
|
+
formView.idInputView.fieldView.bind('value').to(updateBookmarkCommand, 'value');
|
|
138
|
+
formView.saveButtonView.bind('label').to(updateBookmarkCommand, 'value', value => value ? t('Save') : t('Insert'));
|
|
139
|
+
// Form elements should be read-only when corresponding commands are disabled.
|
|
140
|
+
formView.idInputView.bind('isEnabled').toMany(commands, 'isEnabled', (...areEnabled) => areEnabled.some(isEnabled => isEnabled));
|
|
141
|
+
// Disable the "save" button if the command is disabled.
|
|
142
|
+
formView.saveButtonView.bind('isEnabled').toMany(commands, 'isEnabled', (...areEnabled) => areEnabled.some(isEnabled => isEnabled));
|
|
143
|
+
// Close the panel on form after clicking back button.
|
|
144
|
+
this.listenTo(formView, 'cancel', () => {
|
|
145
|
+
this._hideFormView();
|
|
146
|
+
});
|
|
147
|
+
// Execute link command after clicking the "Save" button.
|
|
148
|
+
this.listenTo(formView, 'submit', () => {
|
|
149
|
+
if (formView.isValid()) {
|
|
150
|
+
const value = formView.id;
|
|
151
|
+
if (this._getSelectedBookmarkElement()) {
|
|
152
|
+
editor.execute('updateBookmark', { bookmarkId: value });
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
editor.execute('insertBookmark', { bookmarkId: value });
|
|
156
|
+
}
|
|
157
|
+
this._hideFormView();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// Update balloon position when form error changes.
|
|
161
|
+
this.listenTo(formView.idInputView, 'change:errorText', () => {
|
|
162
|
+
editor.ui.update();
|
|
163
|
+
});
|
|
164
|
+
return formView;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Creates link form menu list entry, so it'll be possible to access
|
|
168
|
+
* the list of the bookmarks from the link form.
|
|
169
|
+
*/
|
|
170
|
+
_registerLinkProvider() {
|
|
171
|
+
const t = this.editor.locale.t;
|
|
172
|
+
const linksUI = this.editor.plugins.get('LinkUI');
|
|
173
|
+
const bookmarkEditing = this.editor.plugins.get(BookmarkEditing);
|
|
174
|
+
const getListItems = () => Array
|
|
175
|
+
.from(bookmarkEditing.getAllBookmarkNames())
|
|
176
|
+
.sort((a, b) => a.localeCompare(b))
|
|
177
|
+
.map((bookmarkId) => ({
|
|
178
|
+
id: bookmarkId,
|
|
179
|
+
href: `#${bookmarkId}`,
|
|
180
|
+
label: bookmarkId,
|
|
181
|
+
icon: IconBookmarkMedium
|
|
182
|
+
}));
|
|
183
|
+
const getItem = (href) => {
|
|
184
|
+
const bookmark = [...bookmarkEditing.getAllBookmarkNames()].find(item => `#${item}` === href);
|
|
185
|
+
if (!bookmark) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
href,
|
|
190
|
+
label: bookmark,
|
|
191
|
+
icon: IconBookmarkSmall,
|
|
192
|
+
tooltip: t('Scroll to bookmark')
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
linksUI.registerLinksListProvider({
|
|
196
|
+
label: t('Bookmarks'),
|
|
197
|
+
emptyListPlaceholder: t('No bookmarks available.'),
|
|
198
|
+
navigate: ({ href }) => this._scrollToBookmark(href),
|
|
199
|
+
getListItems,
|
|
200
|
+
getItem
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Scrolls the editor to the bookmark with the given id.
|
|
205
|
+
*/
|
|
206
|
+
_scrollToBookmark(href) {
|
|
207
|
+
const bookmarkEditing = this.editor.plugins.get(BookmarkEditing);
|
|
208
|
+
const bookmarkElement = bookmarkEditing.getElementForBookmarkId(href.slice(1));
|
|
209
|
+
if (!bookmarkElement) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
this.editor.model.change(writer => {
|
|
213
|
+
writer.setSelection(bookmarkElement, 'on');
|
|
214
|
+
});
|
|
215
|
+
this.editor.editing.view.scrollToTheSelection({
|
|
216
|
+
alignToTop: true,
|
|
217
|
+
forceScroll: true
|
|
218
|
+
});
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Creates a toolbar Bookmark button. Clicking this button will show
|
|
223
|
+
* a {@link #_balloon} attached to the selection.
|
|
224
|
+
*/
|
|
225
|
+
_registerComponents() {
|
|
226
|
+
const editor = this.editor;
|
|
227
|
+
editor.ui.componentFactory.add('bookmark', () => {
|
|
228
|
+
const buttonView = this._createBookmarkButton(ButtonView);
|
|
229
|
+
buttonView.set({
|
|
230
|
+
tooltip: true
|
|
231
|
+
});
|
|
232
|
+
return buttonView;
|
|
233
|
+
});
|
|
234
|
+
editor.ui.componentFactory.add('menuBar:bookmark', () => {
|
|
235
|
+
return this._createBookmarkButton(MenuBarMenuListItemButtonView);
|
|
236
|
+
});
|
|
237
|
+
// Bookmark toolbar buttons.
|
|
238
|
+
editor.ui.componentFactory.add('bookmarkPreview', locale => {
|
|
239
|
+
const updateBookmarkCommand = editor.commands.get('updateBookmark');
|
|
240
|
+
const label = new LabelView(locale);
|
|
241
|
+
label.extendTemplate({
|
|
242
|
+
attributes: {
|
|
243
|
+
class: ['ck-bookmark-toolbar__preview']
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
label.bind('text').to(updateBookmarkCommand, 'value');
|
|
247
|
+
return label;
|
|
248
|
+
});
|
|
249
|
+
editor.ui.componentFactory.add('editBookmark', locale => {
|
|
250
|
+
const updateBookmarkCommand = editor.commands.get('updateBookmark');
|
|
251
|
+
const button = new ButtonView(locale);
|
|
252
|
+
const t = locale.t;
|
|
253
|
+
button.set({
|
|
254
|
+
label: t('Edit bookmark'),
|
|
255
|
+
icon: IconPencil,
|
|
256
|
+
tooltip: true
|
|
257
|
+
});
|
|
258
|
+
button.bind('isEnabled').to(updateBookmarkCommand);
|
|
259
|
+
this.listenTo(button, 'execute', () => {
|
|
260
|
+
this._showFormView();
|
|
261
|
+
});
|
|
262
|
+
return button;
|
|
263
|
+
});
|
|
264
|
+
editor.ui.componentFactory.add('removeBookmark', locale => {
|
|
265
|
+
const deleteCommand = editor.commands.get('delete');
|
|
266
|
+
const button = new ButtonView(locale);
|
|
267
|
+
const t = locale.t;
|
|
268
|
+
button.set({
|
|
269
|
+
label: t('Remove bookmark'),
|
|
270
|
+
icon: IconRemove,
|
|
271
|
+
tooltip: true
|
|
272
|
+
});
|
|
273
|
+
button.bind('isEnabled').to(deleteCommand);
|
|
274
|
+
this.listenTo(button, 'execute', () => {
|
|
275
|
+
editor.execute('delete');
|
|
276
|
+
editor.editing.view.focus();
|
|
277
|
+
});
|
|
278
|
+
return button;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Creates a button for `bookmark` command to use either in toolbar or in menu bar.
|
|
283
|
+
*/
|
|
284
|
+
_createBookmarkButton(ButtonClass) {
|
|
285
|
+
const editor = this.editor;
|
|
286
|
+
const locale = editor.locale;
|
|
287
|
+
const view = new ButtonClass(locale);
|
|
288
|
+
const insertCommand = editor.commands.get('insertBookmark');
|
|
289
|
+
const updateCommand = editor.commands.get('updateBookmark');
|
|
290
|
+
const t = locale.t;
|
|
291
|
+
view.set({
|
|
292
|
+
label: t('Bookmark'),
|
|
293
|
+
icon: IconBookmark
|
|
294
|
+
});
|
|
295
|
+
// Execute the command.
|
|
296
|
+
this.listenTo(view, 'execute', () => {
|
|
297
|
+
editor.editing.view.scrollToTheSelection();
|
|
298
|
+
this._showFormView();
|
|
299
|
+
});
|
|
300
|
+
view.bind('isEnabled').toMany([insertCommand, updateCommand], 'isEnabled', (...areEnabled) => areEnabled.some(isEnabled => isEnabled));
|
|
301
|
+
view.bind('isOn').to(updateCommand, 'value', value => !!value);
|
|
302
|
+
return view;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Attaches actions that control whether the balloon panel containing the
|
|
306
|
+
* {@link #formView} is visible or not.
|
|
307
|
+
*/
|
|
308
|
+
_enableUserBalloonInteractions() {
|
|
309
|
+
// Close the panel on the Esc key press when the editable has focus and the balloon is visible.
|
|
310
|
+
this.editor.keystrokes.set('Esc', (data, cancel) => {
|
|
311
|
+
if (this._isFormVisible) {
|
|
312
|
+
this._hideFormView();
|
|
313
|
+
cancel();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
// Close on click outside of balloon panel element.
|
|
317
|
+
clickOutsideHandler({
|
|
318
|
+
emitter: this.formView,
|
|
319
|
+
activator: () => this._isFormInPanel,
|
|
320
|
+
contextElements: () => [this._balloon.view.element],
|
|
321
|
+
callback: () => {
|
|
322
|
+
// Focusing on the editable during a click outside the balloon panel might
|
|
323
|
+
// cause the selection to move to the beginning of the editable, so we avoid
|
|
324
|
+
// focusing on it during this action.
|
|
325
|
+
// See: https://github.com/ckeditor/ckeditor5/issues/18253
|
|
326
|
+
this._hideFormView(false);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Adds the {@link #formView} to the {@link #_balloon}.
|
|
332
|
+
*/
|
|
333
|
+
_addFormView() {
|
|
334
|
+
if (!this.formView) {
|
|
335
|
+
this._createViews();
|
|
336
|
+
}
|
|
337
|
+
if (this._isFormInPanel) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const updateBookmarkCommand = this.editor.commands.get('updateBookmark');
|
|
341
|
+
this.formView.disableCssTransitions();
|
|
342
|
+
this.formView.resetFormStatus();
|
|
343
|
+
this._balloon.add({
|
|
344
|
+
view: this.formView,
|
|
345
|
+
position: this._getBalloonPositionData()
|
|
346
|
+
});
|
|
347
|
+
this.formView.backButtonView.isVisible = updateBookmarkCommand.isEnabled;
|
|
348
|
+
this.formView.idInputView.fieldView.value = updateBookmarkCommand.value || '';
|
|
349
|
+
// Select input when form view is currently visible.
|
|
350
|
+
if (this._balloon.visibleView === this.formView) {
|
|
351
|
+
this.formView.idInputView.fieldView.select();
|
|
352
|
+
}
|
|
353
|
+
this.formView.enableCssTransitions();
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Removes the {@link #formView} from the {@link #_balloon}.
|
|
357
|
+
*/
|
|
358
|
+
_removeFormView(updateFocus = true) {
|
|
359
|
+
// Blur the input element before removing it from DOM to prevent issues in some browsers.
|
|
360
|
+
// See https://github.com/ckeditor/ckeditor5/issues/1501.
|
|
361
|
+
this.formView.saveButtonView.focus();
|
|
362
|
+
// Reset the ID field to update the state of the submit button.
|
|
363
|
+
this.formView.idInputView.fieldView.reset();
|
|
364
|
+
this._balloon.remove(this.formView);
|
|
365
|
+
// Because the form has an input which has focus, the focus must be brought back
|
|
366
|
+
// to the editor. Otherwise, it would be lost.
|
|
367
|
+
if (updateFocus) {
|
|
368
|
+
this.editor.editing.view.focus();
|
|
369
|
+
}
|
|
370
|
+
this._hideFakeVisualSelection();
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Shows the {@link #formView}.
|
|
374
|
+
*/
|
|
375
|
+
_showFormView() {
|
|
376
|
+
if (!this.formView) {
|
|
377
|
+
this._createViews();
|
|
378
|
+
}
|
|
379
|
+
if (!this._getSelectedBookmarkElement()) {
|
|
380
|
+
this._showFakeVisualSelection();
|
|
381
|
+
}
|
|
382
|
+
this._addFormView();
|
|
383
|
+
// Be sure panel with bookmark is visible.
|
|
384
|
+
this._balloon.showStack('main');
|
|
385
|
+
// Begin responding to ui#update once the UI is added.
|
|
386
|
+
this._startUpdatingUI();
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Removes the {@link #formView} from the {@link #_balloon}.
|
|
390
|
+
*/
|
|
391
|
+
_hideFormView(updateFocus = true) {
|
|
392
|
+
if (!this._isFormInPanel) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const editor = this.editor;
|
|
396
|
+
this.stopListening(editor.ui, 'update');
|
|
397
|
+
this.stopListening(this._balloon, 'change:visibleView');
|
|
398
|
+
// Make sure the focus always gets back to the editable _before_ removing the focused form view.
|
|
399
|
+
// Doing otherwise causes issues in some browsers. See https://github.com/ckeditor/ckeditor5-link/issues/193.
|
|
400
|
+
if (updateFocus) {
|
|
401
|
+
editor.editing.view.focus();
|
|
402
|
+
}
|
|
403
|
+
// Remove form first because it's on top of the stack.
|
|
404
|
+
this._removeFormView(updateFocus);
|
|
405
|
+
this._hideFakeVisualSelection();
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Makes the UI react to the {@link module:ui/editorui/editorui~EditorUI#event:update} event to
|
|
409
|
+
* reposition itself when the editor UI should be refreshed.
|
|
410
|
+
*
|
|
411
|
+
* See: {@link #_hideFormView} to learn when the UI stops reacting to the `update` event.
|
|
412
|
+
*/
|
|
413
|
+
_startUpdatingUI() {
|
|
414
|
+
const editor = this.editor;
|
|
415
|
+
const viewDocument = editor.editing.view.document;
|
|
416
|
+
let prevSelectedBookmark = this._getSelectedBookmarkElement();
|
|
417
|
+
let prevSelectionParent = getSelectionParent();
|
|
418
|
+
const update = () => {
|
|
419
|
+
const selectedBookmark = this._getSelectedBookmarkElement();
|
|
420
|
+
const selectionParent = getSelectionParent();
|
|
421
|
+
// Hide the panel if:
|
|
422
|
+
//
|
|
423
|
+
// * the selection went out of the EXISTING bookmark element. E.g. user moved the caret out
|
|
424
|
+
// of the bookmark,
|
|
425
|
+
// * the selection went to a different parent when creating a NEW bookmark. E.g. someone
|
|
426
|
+
// else modified the document.
|
|
427
|
+
// * the selection has expanded (e.g. displaying bookmark actions then pressing SHIFT+Right arrow).
|
|
428
|
+
//
|
|
429
|
+
if (prevSelectedBookmark && !selectedBookmark ||
|
|
430
|
+
!prevSelectedBookmark && selectionParent !== prevSelectionParent) {
|
|
431
|
+
this._hideFormView();
|
|
432
|
+
}
|
|
433
|
+
// Update the position of the panel when:
|
|
434
|
+
// * bookmark panel is in the visible stack
|
|
435
|
+
// * the selection remains on the original bookmark element,
|
|
436
|
+
// * there was no bookmark element in the first place, i.e. creating a new bookmark
|
|
437
|
+
else if (this._isFormVisible) {
|
|
438
|
+
// If still in a bookmark element, simply update the position of the balloon.
|
|
439
|
+
// If there was no bookmark (e.g. inserting one), the balloon must be moved
|
|
440
|
+
// to the new position in the editing view (a new native DOM range).
|
|
441
|
+
this._balloon.updatePosition(this._getBalloonPositionData());
|
|
442
|
+
}
|
|
443
|
+
prevSelectedBookmark = selectedBookmark;
|
|
444
|
+
prevSelectionParent = selectionParent;
|
|
445
|
+
};
|
|
446
|
+
function getSelectionParent() {
|
|
447
|
+
return viewDocument.selection.focus.getAncestors()
|
|
448
|
+
.reverse()
|
|
449
|
+
.find((node) => node.is('element'));
|
|
450
|
+
}
|
|
451
|
+
this.listenTo(editor.ui, 'update', update);
|
|
452
|
+
this.listenTo(this._balloon, 'change:visibleView', update);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Returns `true` when {@link #formView} is in the {@link #_balloon}.
|
|
456
|
+
*/
|
|
457
|
+
get _isFormInPanel() {
|
|
458
|
+
return !!this.formView && this._balloon.hasView(this.formView);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Returns `true` when {@link #formView} is in the {@link #_balloon} and it is currently visible.
|
|
462
|
+
*/
|
|
463
|
+
get _isFormVisible() {
|
|
464
|
+
return !!this.formView && this._balloon.visibleView == this.formView;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
|
|
468
|
+
* to the target element or selection.
|
|
469
|
+
*/
|
|
470
|
+
_getBalloonPositionData() {
|
|
471
|
+
const view = this.editor.editing.view;
|
|
472
|
+
const model = this.editor.model;
|
|
473
|
+
let target;
|
|
474
|
+
const bookmarkElement = this._getSelectedBookmarkElement();
|
|
475
|
+
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
476
|
+
// There are cases when we highlight selection using a marker (#7705, #4721).
|
|
477
|
+
const markerViewElements = Array.from(this.editor.editing.mapper.markerNameToElements(VISUAL_SELECTION_MARKER_NAME));
|
|
478
|
+
const newRange = view.createRange(view.createPositionBefore(markerViewElements[0]), view.createPositionAfter(markerViewElements[markerViewElements.length - 1]));
|
|
479
|
+
target = view.domConverter.viewRangeToDom(newRange);
|
|
480
|
+
}
|
|
481
|
+
else if (bookmarkElement) {
|
|
482
|
+
target = () => {
|
|
483
|
+
const mapper = this.editor.editing.mapper;
|
|
484
|
+
const domConverter = view.domConverter;
|
|
485
|
+
const viewElement = mapper.toViewElement(bookmarkElement);
|
|
486
|
+
return domConverter.mapViewToDom(viewElement);
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (!target) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
target
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Returns the bookmark {@link module:engine/view/attributeelement~ViewAttributeElement} under
|
|
498
|
+
* the {@link module:engine/view/document~ViewDocument editing view's} selection or `null`
|
|
499
|
+
* if there is none.
|
|
500
|
+
*/
|
|
501
|
+
_getSelectedBookmarkElement() {
|
|
502
|
+
const selection = this.editor.model.document.selection;
|
|
503
|
+
const element = selection.getSelectedElement();
|
|
504
|
+
if (element && element.is('element', 'bookmark')) {
|
|
505
|
+
return element;
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Displays a fake visual selection when the contextual balloon is displayed.
|
|
511
|
+
*
|
|
512
|
+
* This adds a 'bookmark-ui' marker into the document that is rendered as a highlight on selected text fragment.
|
|
513
|
+
*/
|
|
514
|
+
_showFakeVisualSelection() {
|
|
515
|
+
const model = this.editor.model;
|
|
516
|
+
model.change(writer => {
|
|
517
|
+
const range = model.document.selection.getFirstRange();
|
|
518
|
+
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
519
|
+
writer.updateMarker(VISUAL_SELECTION_MARKER_NAME, { range });
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
if (range.start.isAtEnd) {
|
|
523
|
+
const startPosition = range.start.getLastMatchingPosition(({ item }) => !model.schema.isContent(item), { boundaries: range });
|
|
524
|
+
writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
|
525
|
+
usingOperation: false,
|
|
526
|
+
affectsData: false,
|
|
527
|
+
range: writer.createRange(startPosition, range.end)
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
writer.addMarker(VISUAL_SELECTION_MARKER_NAME, {
|
|
532
|
+
usingOperation: false,
|
|
533
|
+
affectsData: false,
|
|
534
|
+
range
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Hides the fake visual selection created in {@link #_showFakeVisualSelection}.
|
|
542
|
+
*/
|
|
543
|
+
_hideFakeVisualSelection() {
|
|
544
|
+
const model = this.editor.model;
|
|
545
|
+
if (model.markers.has(VISUAL_SELECTION_MARKER_NAME)) {
|
|
546
|
+
model.change(writer => {
|
|
547
|
+
writer.removeMarker(VISUAL_SELECTION_MARKER_NAME);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Returns bookmark form validation callbacks.
|
|
554
|
+
*/
|
|
555
|
+
function getFormValidators(editor) {
|
|
556
|
+
const { t } = editor;
|
|
557
|
+
const bookmarkEditing = editor.plugins.get(BookmarkEditing);
|
|
558
|
+
return [
|
|
559
|
+
form => {
|
|
560
|
+
if (!form.id) {
|
|
561
|
+
return t('Bookmark must not be empty.');
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
form => {
|
|
565
|
+
if (form.id && /\s/.test(form.id)) {
|
|
566
|
+
return t('Bookmark name cannot contain space characters.');
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
form => {
|
|
570
|
+
const selectedElement = editor.model.document.selection.getSelectedElement();
|
|
571
|
+
const existingBookmarkForId = bookmarkEditing.getElementForBookmarkId(form.id);
|
|
572
|
+
// Accept change of bookmark ID if no real change is happening (edit -> submit, without changes).
|
|
573
|
+
if (selectedElement === existingBookmarkForId) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (existingBookmarkForId) {
|
|
577
|
+
return t('Bookmark name already exists.');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
];
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Returns the currently selected bookmark view element.
|
|
584
|
+
*/
|
|
585
|
+
function getSelectedBookmarkWidget(selection) {
|
|
586
|
+
const element = selection.getSelectedElement();
|
|
587
|
+
if (!element || !isWidget(element) || !element.getCustomProperty('bookmark')) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
return element;
|
|
591
|
+
}
|
package/{dist → src}/index.d.ts
RENAMED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
6
|
-
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
7
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
8
|
-
*/
|
|
9
5
|
/**
|
|
10
6
|
* @module bookmark
|
|
11
7
|
*/
|
package/src/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* @module bookmark
|
|
7
|
+
*/
|
|
8
|
+
export { Bookmark } from './bookmark.js';
|
|
9
|
+
export { BookmarkEditing } from './bookmarkediting.js';
|
|
10
|
+
export { BookmarkUI } from './bookmarkui.js';
|
|
11
|
+
export { InsertBookmarkCommand } from './insertbookmarkcommand.js';
|
|
12
|
+
export { UpdateBookmarkCommand } from './updatebookmarkcommand.js';
|
|
13
|
+
export { BookmarkFormView } from './ui/bookmarkformview.js';
|
|
14
|
+
export { isBookmarkIdValid as _isBookmarkIdValid } from './utils.js';
|
|
15
|
+
import './augmentation.js';
|
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
7
|
-
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
8
|
-
*/
|
|
9
|
-
import { Command } from '@ckeditor/ckeditor5-core';
|
|
5
|
+
import { Command } from 'ckeditor5/src/core.js';
|
|
10
6
|
/**
|
|
11
7
|
* The insert bookmark command.
|
|
12
8
|
*
|