This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 37
/
widget.js
262 lines (220 loc) · 8.03 KB
/
widget.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* @module image/widget/widget
*/
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import WidgetEngine from './widgetengine';
import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection';
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement';
import RootEditableElement from '@ckeditor/ckeditor5-engine/src/view/rooteditableelement';
import { isWidget } from './utils';
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
import '../../theme/widget/theme.scss';
/**
* The widget plugin.
* Adds default {@link module:engine/view/document~Document#event:mousedown mousedown} handling on widget elements.
*
* @extends module:core/plugin~Plugin.
*/
export default class Widget extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ WidgetEngine ];
}
/**
* @inheritDoc
*/
init() {
const viewDocument = this.editor.editing.view;
// If mouse down is pressed on widget - create selection over whole widget.
viewDocument.addObserver( MouseObserver );
this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) );
// Handle custom keydown behaviour.
this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: 'high' } );
}
/**
* Handles {@link module:engine/view/document~Document#event:mousedown mousedown} events on widget elements.
*
* @private
* @param {module:utils/eventinfo~EventInfo} eventInfo
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
*/
_onMousedown( eventInfo, domEventData ) {
const editor = this.editor;
const viewDocument = editor.editing.view;
let element = domEventData.target;
// Do nothing if inside nested editable.
if ( isInsideNestedEditable( element ) ) {
return;
}
// If target is not a widget element - check if one of the ancestors is.
if ( !isWidget( element ) ) {
element = element.findAncestor( isWidget );
if ( !element ) {
return;
}
}
domEventData.preventDefault();
// Focus editor if is not focused already.
if ( !viewDocument.isFocused ) {
viewDocument.focus();
}
// Create model selection over widget.
const modelElement = editor.editing.mapper.toModelElement( element );
editor.document.enqueueChanges( ( ) => {
this._setSelectionOverElement( modelElement );
} );
}
/**
* Handles {@link module:engine/view/document~Document#event:keydown keydown} events.
*
* @private
* @param {module:utils/eventinfo~EventInfo} eventInfo
* @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
*/
_onKeydown( eventInfo, domEventData ) {
const keyCode = domEventData.keyCode;
const isForward = keyCode == keyCodes.delete || keyCode == keyCodes.arrowdown || keyCode == keyCodes.arrowright;
// Checks if delete/backspace or arrow keys were handled and then prevents default event behaviour and stops
// event propagation.
if ( ( isDeleteKeyCode( keyCode ) && this._handleDelete( isForward ) ) ||
( isArrowKeyCode( keyCode ) && this._handleArrowKeys( isForward ) ) ) {
domEventData.preventDefault();
eventInfo.stop();
}
}
/**
* Handles delete keys: backspace and delete.
*
* @private
* @param {Boolean} isForward Set to true if delete was performed in forward direction.
* @returns {Boolean|undefined} Returns `true` if keys were handled correctly.
*/
_handleDelete( isForward ) {
const modelDocument = this.editor.document;
const modelSelection = modelDocument.selection;
// Do nothing on non-collapsed selection.
if ( !modelSelection.isCollapsed ) {
return;
}
const objectElement = this._getObjectElementNextToSelection( isForward );
if ( objectElement ) {
modelDocument.enqueueChanges( () => {
// Remove previous element if empty.
const previousNode = modelSelection.anchor.parent;
if ( previousNode.isEmpty ) {
const batch = modelDocument.batch();
batch.remove( previousNode );
}
this._setSelectionOverElement( objectElement );
} );
return true;
}
}
/**
* Handles arrow keys.
*
* @param {Boolean} isForward Set to true if arrow key should be handled in forward direction.
* @returns {Boolean|undefined} Returns `true` if keys were handled correctly.
*/
_handleArrowKeys( isForward ) {
const modelDocument = this.editor.document;
const schema = modelDocument.schema;
const modelSelection = modelDocument.selection;
const objectElement = modelSelection.getSelectedElement();
// if object element is selected.
if ( objectElement && schema.objects.has( objectElement.name ) ) {
const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
const newRange = modelDocument.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' );
if ( newRange ) {
modelDocument.enqueueChanges( () => {
modelSelection.setRanges( [ newRange ] );
} );
}
return true;
}
// If selection is next to object element.
// Return if not collapsed.
if ( !modelSelection.isCollapsed ) {
return;
}
const objectElement2 = this._getObjectElementNextToSelection( isForward );
if ( objectElement2 instanceof ModelElement && modelDocument.schema.objects.has( objectElement2.name ) ) {
modelDocument.enqueueChanges( () => {
this._setSelectionOverElement( objectElement2 );
} );
return true;
}
}
/**
* Sets {@link module:engine/model/selection~Selection document's selection} over given element.
*
* @private
* @param {module:engine/model/element~Element} element
*/
_setSelectionOverElement( element ) {
this.editor.document.selection.setRanges( [ ModelRange.createOn( element ) ] );
}
/**
* Checks if {@link module:engine/model/element~Element element} placed next to the current
* {@link module:engine/model/selection~Selection model selection} exists and is marked in
* {@link module:engine/model/schema~Schema schema} as `object`.
*
* @private
* @param {Boolean} forward Direction of checking.
* @returns {module:engine/model/element~Element|null}
*/
_getObjectElementNextToSelection( forward ) {
const modelDocument = this.editor.document;
const schema = modelDocument.schema;
const modelSelection = modelDocument.selection;
const dataController = this.editor.data;
// Clone current selection to use it as a probe. We must leave default selection as it is so it can return
// to its current state after undo.
const probe = ModelSelection.createFromSelection( modelSelection );
dataController.modifySelection( probe, { direction: forward ? 'forward' : 'backward' } );
const objectElement = forward ? probe.focus.nodeBefore : probe.focus.nodeAfter;
if ( objectElement instanceof ModelElement && schema.objects.has( objectElement.name ) ) {
return objectElement;
}
return null;
}
}
// Returns 'true' if provided key code represents one of the arrow keys.
//
// @param {Number} keyCode
// @returns {Boolean}
function isArrowKeyCode( keyCode ) {
return keyCode == keyCodes.arrowright ||
keyCode == keyCodes.arrowleft ||
keyCode == keyCodes.arrowup ||
keyCode == keyCodes.arrowdown;
}
//Returns 'true' if provided key code represents one of the delete keys: delete or backspace.
//
//@param {Number} keyCode
//@returns {Boolean}
function isDeleteKeyCode( keyCode ) {
return keyCode == keyCodes.delete || keyCode == keyCodes.backspace;
}
// Returns `true` when element is a nested editable or is placed inside one.
//
// @param {module:engine/view/element~Element}
// @returns {Boolean}
function isInsideNestedEditable( element ) {
while ( element ) {
if ( element instanceof ViewEditableElement && !( element instanceof RootEditableElement ) ) {
return true;
}
element = element.parent;
}
return false;
}