-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathjquery-contenteditable.es6
262 lines (207 loc) · 8.87 KB
/
jquery-contenteditable.es6
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
/*!
* jQuery UI Contenteditable 1.0.0
* Lightweight jQuery UI plugin providing more convenient API to use "contenteditable" feature.
*
* Copyright: Alexey Grinko, 2016
* Git repository: https://github.com/agrinko/jquery-contenteditable.git
*
* @license MIT - https://opensource.org/licenses/MIT
*/
(($) => {
$.widget("ui.editable", {
version: "1.0.0",
widgetEventPrefix: "edit",
options: {
content: "self", //a selector/jQuery object/HTML element that "contenteditable" attr
//should be applied to; default "self" refers to the element itself
saveDelay: false, //a delay in ms before firing each "save" callback; false for no intermediate saving
multiline: false, //if false (default) - prevent "enter" key from adding a line break
exitKeys: ["escape"], //keys to finish editing (escape, enter, tab, end, and some other)
className: "ui-editable", //class name to be added to editable element permanently
editingClass: "ui-editable-editing",//class name to be added in editing mode
invalidClass: "ui-editable-invalid",//class name to be added when validation is not passed
autoselect: false, //whether to automatically select all content when starting editing
preventUndo: false, //whether to prevent default Ctrl+Z/Ctrl+Shift+Z/Ctrl+Y behaviour
cancel: "a", //selector/jQuery object/HTML element click on which does not enable editing
//callbacks
start: null, //editing was started
end: null, //editing was finished
input: null, //fired after each entered letter; return false to prevent default action
save: null, //fired when validation is passed and saveDelay delay has gone
validate: null //return false for incorrect value; content value is the second argument
},
keyCode: {
down: 40,
end: 35,
enter: 13,
escape: 27,
left: 37,
right: 39,
space: 32,
tab: 9,
up: 38
},
_create() {
this.ready = false; //marks if element is ready for editable mode after mousedown event
this.mode = "normal"; //current state: "normal" or "edit"
this.element.addClass(this.options.className);
this.content = (this.options.content == "self" || !this.options.content) ?
this.element :
this.element.find(this.options.content);
this._bindEvents();
},
_bindEvents() {
this._on({
mousedown: this._prepare,
mouseup: this._capture,
dragstart: this._cancel
});
this._bindContentEvents();
},
_bindContentEvents() {
this.content
.on(`keydown${this.eventNamespace}`, this._keydown.bind(this))
.on(`input${this.eventNamespace}`, this._input.bind(this))
.on(`blur${this.eventNamespace}`, this.finish.bind(this));
},
_prepare(e) {
//prevent editing when clicking on element referred by "cancel" selector
if ($(e.target).closest(this.options.cancel).length)
return;
if (e.which === 1) //filter left mouse button
this.ready = true;
},
_cancel() {
this.ready = false;
},
_capture() {
if (!this.ready) return;
this.start();
this.ready = false;
},
getText() {
return this.content[0].innerText || this.content[0].textContent; // for browsers compatibility
},
start() {
if (this.mode === "edit") return;
//Trigger event + callbacks
if (this._trigger("start") === false)
return false;
this._mode("edit");
this.content.focus();
this.isValid = true;
this.validContent = this.getText(); //remember current content as valid
if (this.options.autoselect)
this.select();
},
finish(e) {
if (this.mode === "normal") return;
this._save();
if (!this.isValid)
this.content.text(this.validContent); //reset to last remembered valid content
this._mode("normal");
if (!e || e.type != "blur")
this.content.blur(); //automatically switch focus when finished by pressing "exitKeys"
//Trigger event + callbacks
this._trigger("end");
},
validate() {
//Trigger event + callbacks
let result = this._trigger("validate", null, {
content: this.getText()
});
if (result === false)
this.element.addClass(this.options.invalidClass);
else
this.element.removeClass(this.options.invalidClass);
return this.isValid = result !== false;
},
select() {
let range = document.createRange();
let sel = window.getSelection();
range.selectNodeContents(this.content[0]);
sel.removeAllRanges();
sel.addRange(range);
},
_mode(m) {
switch (m) {
case "edit":
this.element.addClass(this.options.editingClass);
if (this.element.data("ui-draggable"))
this.element.draggable("disable"); //disable draggable plugin when editing content
this.content.prop("contenteditable", true);
break;
case "normal":
this.element.removeClass([this.options.editingClass, this.options.invalidClass].join(" "));
if (this.element.data("ui-draggable"))
this.element.draggable("enable");
this.content.prop("contenteditable", false);
break;
}
this.mode = m;
},
_input(e) {
if (this.mode != "edit") return;
//Trigger event + callbacks
let ui = {
content: this.getText()
};
if (this._trigger("input", e, ui) === false) {
return false;
}
if (this.options.saveDelay === 0)
this._save();
else if (this.options.saveDelay > 0)
this._debouncedSave(this.options.saveDelay);
},
_save() {
let text = this.getText();
if (text == this.validContent) //if content has not changed
return;
if (!this.validate()) return;
this.validContent = text; //remember new content as valid
//Trigger event + callbacks
this._trigger("save", null, {
content: this.validContent
});
},
//call "_save" method only after "_debouncedSave" has not been fired during <saveDelay> ms
_debouncedSave(timeout) {
if (this.__save_timeout)
clearTimeout(this.__save_timeout);
this.__save_timeout = setTimeout(() => {
this._save();
this.__save_timeout = null;
}, timeout);
},
_keydown(e) {
if (this.mode === "normal") return;
//prevent default browser's undo/redo actions
if (this.options.preventUndo && e.ctrlKey && (
e.keyCode === "Z".charCodeAt(0) ||
e.keyCode === "Y".charCodeAt(0) ||
e.keyCode === "Z".charCodeAt(0) && e.shiftKey))
return false;
if (this.options.exitKeys) {
let exitKeyPressed = this.options.exitKeys.some((keyName) => {
return e.keyCode === this.keyCode[keyName.toLowerCase()]; //get key codes by names in $.ui.keyCode hash
});
if (exitKeyPressed) {
this.finish();
return;
}
}
//prevent 'enter' key from adding line break in non-multi-line mode
if (!this.options.multiline && e.keyCode === this.keyCode.enter)
return false;
},
_destroy() {
this.element.removeClass([
this.options.className,
this.options.editingClass,
this.options.invalidClass
].join(" "));
this.content.off(this.eventNamespace);
}
});
})(jQuery);