forked from yabwe/medium-editor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
anchor.js
376 lines (304 loc) · 13.8 KB
/
anchor.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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
(function () {
'use strict';
var AnchorForm = MediumEditor.extensions.form.extend({
/* Anchor Form Options */
/* customClassOption: [string] (previously options.anchorButton + options.anchorButtonClass)
* Custom class name the user can optionally have added to their created links (ie 'button').
* If passed as a non-empty string, a checkbox will be displayed allowing the user to choose
* whether to have the class added to the created link or not.
*/
customClassOption: null,
/* customClassOptionText: [string]
* text to be shown in the checkbox when the __customClassOption__ is being used.
*/
customClassOptionText: 'Button',
/* linkValidation: [boolean] (previously options.checkLinkFormat)
* enables/disables check for common URL protocols on anchor links.
*/
linkValidation: false,
/* placeholderText: [string] (previously options.anchorInputPlaceholder)
* text to be shown as placeholder of the anchor input.
*/
placeholderText: 'Paste or type a link',
/* targetCheckbox: [boolean] (previously options.anchorTarget)
* enables/disables displaying a "Open in new window" checkbox, which when checked
* changes the `target` attribute of the created link.
*/
targetCheckbox: false,
/* targetCheckboxText: [string] (previously options.anchorInputCheckboxLabel)
* text to be shown in the checkbox enabled via the __targetCheckbox__ option.
*/
targetCheckboxText: 'Open in new window',
// Options for the Button base class
name: 'anchor',
action: 'createLink',
aria: 'link',
tagNames: ['a'],
contentDefault: '<b>#</b>',
contentFA: '<i class="fa fa-link"></i>',
init: function () {
MediumEditor.extensions.form.prototype.init.apply(this, arguments);
this.subscribe('editableKeydown', this.handleKeydown.bind(this));
},
// Called when the button the toolbar is clicked
// Overrides ButtonExtension.handleClick
handleClick: function (event) {
event.preventDefault();
event.stopPropagation();
var range = MediumEditor.selection.getSelectionRange(this.document);
if (range.startContainer.nodeName.toLowerCase() === 'a' ||
range.endContainer.nodeName.toLowerCase() === 'a' ||
MediumEditor.util.getClosestTag(MediumEditor.selection.getSelectedParentElement(range), 'a')) {
return this.execAction('unlink');
}
if (!this.isDisplayed()) {
this.showForm();
}
return false;
},
// Called when user hits the defined shortcut (CTRL / COMMAND + K)
handleKeydown: function (event) {
if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.K) && MediumEditor.util.isMetaCtrlKey(event) && !event.shiftKey) {
this.handleClick(event);
}
},
// Called by medium-editor to append form to the toolbar
getForm: function () {
if (!this.form) {
this.form = this.createForm();
}
return this.form;
},
getTemplate: function () {
var template = [
'<input type="text" class="medium-editor-toolbar-input" placeholder="', this.placeholderText, '">'
];
template.push(
'<a href="#" class="medium-editor-toolbar-save">',
this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class="fa fa-check"></i>' : this.formSaveLabel,
'</a>'
);
template.push('<a href="#" class="medium-editor-toolbar-close">',
this.getEditorOption('buttonLabels') === 'fontawesome' ? '<i class="fa fa-times"></i>' : this.formCloseLabel,
'</a>');
// both of these options are slightly moot with the ability to
// override the various form buildup/serialize functions.
if (this.targetCheckbox) {
// fixme: ideally, this targetCheckboxText would be a formLabel too,
// figure out how to deprecate? also consider `fa-` icon default implcations.
template.push(
'<div class="medium-editor-toolbar-form-row">',
'<input type="checkbox" class="medium-editor-toolbar-anchor-target" id="medium-editor-toolbar-anchor-target-field-' + this.getEditorId() + '">',
'<label for="medium-editor-toolbar-anchor-target-field-' + this.getEditorId() + '">',
this.targetCheckboxText,
'</label>',
'</div>'
);
}
if (this.customClassOption) {
// fixme: expose this `Button` text as a formLabel property, too
// and provide similar access to a `fa-` icon default.
template.push(
'<div class="medium-editor-toolbar-form-row">',
'<input type="checkbox" class="medium-editor-toolbar-anchor-button" id="medium-editor-toolbar-anchor-button-field-' + this.getEditorId() + '">',
'<label for="medium-editor-toolbar-anchor-button-field-' + this.getEditorId() + '">',
this.customClassOptionText,
'</label>',
'</div>'
);
}
return template.join('');
},
// Used by medium-editor when the default toolbar is to be displayed
isDisplayed: function () {
return MediumEditor.extensions.form.prototype.isDisplayed.apply(this);
},
hideForm: function () {
MediumEditor.extensions.form.prototype.hideForm.apply(this);
this.getInput().value = '';
},
showForm: function (opts) {
var input = this.getInput(),
targetCheckbox = this.getAnchorTargetCheckbox(),
buttonCheckbox = this.getAnchorButtonCheckbox();
opts = opts || { value: '' };
// TODO: This is for backwards compatability
// We don't need to support the 'string' argument in 6.0.0
if (typeof opts === 'string') {
opts = {
value: opts
};
}
this.base.saveSelection();
this.hideToolbarDefaultActions();
MediumEditor.extensions.form.prototype.showForm.apply(this);
this.setToolbarPosition();
input.value = opts.value;
input.focus();
// If we have a target checkbox, we want it to be checked/unchecked
// based on whether the existing link has target=_blank
if (targetCheckbox) {
targetCheckbox.checked = opts.target === '_blank';
}
// If we have a custom class checkbox, we want it to be checked/unchecked
// based on whether an existing link already has the class
if (buttonCheckbox) {
var classList = opts.buttonClass ? opts.buttonClass.split(' ') : [];
buttonCheckbox.checked = (classList.indexOf(this.customClassOption) !== -1);
}
},
// Called by core when tearing down medium-editor (destroy)
destroy: function () {
if (!this.form) {
return false;
}
if (this.form.parentNode) {
this.form.parentNode.removeChild(this.form);
}
delete this.form;
},
// core methods
getFormOpts: function () {
// no notion of private functions? wanted `_getFormOpts`
var targetCheckbox = this.getAnchorTargetCheckbox(),
buttonCheckbox = this.getAnchorButtonCheckbox(),
opts = {
value: this.getInput().value.trim()
};
if (this.linkValidation) {
opts.value = this.checkLinkFormat(opts.value);
}
opts.target = '_self';
if (targetCheckbox && targetCheckbox.checked) {
opts.target = '_blank';
}
if (buttonCheckbox && buttonCheckbox.checked) {
opts.buttonClass = this.customClassOption;
}
return opts;
},
doFormSave: function () {
var opts = this.getFormOpts();
this.completeFormSave(opts);
},
completeFormSave: function (opts) {
this.base.restoreSelection();
this.execAction(this.action, opts);
this.base.checkSelection();
},
ensureEncodedUri: function (str) {
return str === decodeURI(str) ? encodeURI(str) : str;
},
ensureEncodedUriComponent: function (str) {
return str === decodeURIComponent(str) ? encodeURIComponent(str) : str;
},
ensureEncodedParam: function (param) {
var split = param.split('='),
key = split[0],
val = split[1];
return key + (val === undefined ? '' : '=' + this.ensureEncodedUriComponent(val));
},
ensureEncodedQuery: function (queryString) {
return queryString.split('&').map(this.ensureEncodedParam.bind(this)).join('&');
},
checkLinkFormat: function (value) {
// Matches any alphabetical characters followed by ://
// Matches protocol relative "//"
// Matches common external protocols "mailto:" "tel:" "maps:"
// Matches relative hash link, begins with "#"
var urlSchemeRegex = /^([a-z]+:)?\/\/|^(mailto|tel|maps):|^\#/i,
hasScheme = urlSchemeRegex.test(value),
scheme = '',
// telRegex is a regex for checking if the string is a telephone number
telRegex = /^\+?\s?\(?(?:\d\s?\-?\)?){3,20}$/,
urlParts = value.match(/^(.*?)(?:\?(.*?))?(?:#(.*))?$/),
path = urlParts[1],
query = urlParts[2],
fragment = urlParts[3];
if (telRegex.test(value)) {
return 'tel:' + value;
}
if (!hasScheme) {
var host = path.split('/')[0];
// if the host part of the path looks like a hostname
if (host.match(/.+(\.|:).+/) || host === 'localhost') {
scheme = 'http://';
}
}
return scheme +
// Ensure path is encoded
this.ensureEncodedUri(path) +
// Ensure query is encoded
(query === undefined ? '' : '?' + this.ensureEncodedQuery(query)) +
// Include fragment unencoded as encodeUriComponent is too
// heavy handed for the many characters allowed in a fragment
(fragment === undefined ? '' : '#' + fragment);
},
doFormCancel: function () {
this.base.restoreSelection();
this.base.checkSelection();
},
// form creation and event handling
attachFormEvents: function (form) {
var close = form.querySelector('.medium-editor-toolbar-close'),
save = form.querySelector('.medium-editor-toolbar-save'),
input = form.querySelector('.medium-editor-toolbar-input');
// Handle clicks on the form itself
this.on(form, 'click', this.handleFormClick.bind(this));
// Handle typing in the textbox
this.on(input, 'keydown', this.handleTextboxKeydown.bind(this));
// Handle close button clicks
this.on(close, 'click', this.handleCloseClick.bind(this));
// Handle save button clicks (capture)
this.on(save, 'click', this.handleSaveClick.bind(this), true);
},
createForm: function () {
var doc = this.document,
form = doc.createElement('div');
// Anchor Form (div)
form.className = 'medium-editor-toolbar-form';
form.id = 'medium-editor-toolbar-form-anchor-' + this.getEditorId();
form.innerHTML = this.getTemplate();
this.attachFormEvents(form);
return form;
},
getInput: function () {
return this.getForm().querySelector('input.medium-editor-toolbar-input');
},
getAnchorTargetCheckbox: function () {
return this.getForm().querySelector('.medium-editor-toolbar-anchor-target');
},
getAnchorButtonCheckbox: function () {
return this.getForm().querySelector('.medium-editor-toolbar-anchor-button');
},
handleTextboxKeydown: function (event) {
// For ENTER -> create the anchor
if (event.keyCode === MediumEditor.util.keyCode.ENTER) {
event.preventDefault();
event.stopPropagation();
this.doFormSave();
return;
}
// For ESCAPE -> close the form
if (event.keyCode === MediumEditor.util.keyCode.ESCAPE) {
event.preventDefault();
this.doFormCancel();
}
},
handleFormClick: function (event) {
// make sure not to hide form when clicking inside the form
event.stopPropagation();
},
handleSaveClick: function (event) {
// Clicking Save -> create the anchor
event.preventDefault();
this.doFormSave();
},
handleCloseClick: function (event) {
// Click Close -> close the form
event.preventDefault();
this.doFormCancel();
}
});
MediumEditor.extensions.anchor = AnchorForm;
}());