summaryrefslogtreecommitdiff
path: root/js/components/htmleditor.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/components/htmleditor.js')
-rwxr-xr-xjs/components/htmleditor.js679
1 files changed, 679 insertions, 0 deletions
diff --git a/js/components/htmleditor.js b/js/components/htmleditor.js
new file mode 100755
index 0000000..68b5b52
--- /dev/null
+++ b/js/components/htmleditor.js
@@ -0,0 +1,679 @@
1/*! UIkit 2.26.4 | http://www.getuikit.com | (c) 2014 YOOtheme | MIT License */
2(function(addon) {
3
4 var component;
5
6 if (window.UIkit) {
7 component = addon(UIkit);
8 }
9
10 if (typeof define == "function" && define.amd) {
11 define("uikit-htmleditor", ["uikit"], function(){
12 return component || addon(UIkit);
13 });
14 }
15
16})(function(UI) {
17
18 "use strict";
19
20 var editors = [];
21
22 UI.component('htmleditor', {
23
24 defaults: {
25 iframe : false,
26 mode : 'split',
27 markdown : false,
28 autocomplete : true,
29 enablescripts: false,
30 height : 500,
31 maxsplitsize : 1000,
32 codemirror : { mode: 'htmlmixed', lineWrapping: true, dragDrop: false, autoCloseTags: true, matchTags: true, autoCloseBrackets: true, matchBrackets: true, indentUnit: 4, indentWithTabs: false, tabSize: 4, hintOptions: {completionSingle:false} },
33 toolbar : [ 'bold', 'italic', 'strike', 'link', 'image', 'blockquote', 'listUl', 'listOl' ],
34 lblPreview : 'Preview',
35 lblCodeview : 'HTML',
36 lblMarkedview: 'Markdown'
37 },
38
39 boot: function() {
40
41 // init code
42 UI.ready(function(context) {
43
44 UI.$('textarea[data-uk-htmleditor]', context).each(function() {
45
46 var editor = UI.$(this);
47
48 if (!editor.data('htmleditor')) {
49 UI.htmleditor(editor, UI.Utils.options(editor.attr('data-uk-htmleditor')));
50 }
51 });
52 });
53 },
54
55 init: function() {
56
57 var $this = this, tpl = UI.components.htmleditor.template;
58
59 this.CodeMirror = this.options.CodeMirror || CodeMirror;
60 this.buttons = {};
61
62 tpl = tpl.replace(/\{:lblPreview}/g, this.options.lblPreview);
63 tpl = tpl.replace(/\{:lblCodeview}/g, this.options.lblCodeview);
64
65 this.htmleditor = UI.$(tpl);
66 this.content = this.htmleditor.find('.uk-htmleditor-content');
67 this.toolbar = this.htmleditor.find('.uk-htmleditor-toolbar');
68 this.preview = this.htmleditor.find('.uk-htmleditor-preview').children().eq(0);
69 this.code = this.htmleditor.find('.uk-htmleditor-code');
70
71 this.element.before(this.htmleditor).appendTo(this.code);
72 this.editor = this.CodeMirror.fromTextArea(this.element[0], this.options.codemirror);
73 this.editor.htmleditor = this;
74 this.editor.on('change', UI.Utils.debounce(function() { $this.render(); }, 150));
75 this.editor.on('change', function() {
76 $this.editor.save();
77 $this.element.trigger('input');
78 });
79 this.code.find('.CodeMirror').css('height', this.options.height);
80
81 // iframe mode?
82 if (this.options.iframe) {
83
84 this.iframe = UI.$('<iframe class="uk-htmleditor-iframe" frameborder="0" scrolling="auto" height="100" width="100%"></iframe>');
85 this.preview.append(this.iframe);
86
87 // must open and close document object to start using it!
88 this.iframe[0].contentWindow.document.open();
89 this.iframe[0].contentWindow.document.close();
90
91 this.preview.container = UI.$(this.iframe[0].contentWindow.document).find('body');
92
93 // append custom stylesheet
94 if (typeof(this.options.iframe) === 'string') {
95 this.preview.container.parent().append('<link rel="stylesheet" href="'+this.options.iframe+'">');
96 }
97
98 } else {
99 this.preview.container = this.preview;
100 }
101
102 UI.$win.on('resize load', UI.Utils.debounce(function() { $this.fit(); }, 200));
103
104 var previewContainer = this.iframe ? this.preview.container:$this.preview.parent(),
105 codeContent = this.code.find('.CodeMirror-sizer'),
106 codeScroll = this.code.find('.CodeMirror-scroll').on('scroll', UI.Utils.debounce(function() {
107
108 if ($this.htmleditor.attr('data-mode') == 'tab') return;
109
110 // calc position
111 var codeHeight = codeContent.height() - codeScroll.height(),
112 previewHeight = previewContainer[0].scrollHeight - ($this.iframe ? $this.iframe.height() : previewContainer.height()),
113 ratio = previewHeight / codeHeight,
114 previewPosition = codeScroll.scrollTop() * ratio;
115
116 // apply new scroll
117 previewContainer.scrollTop(previewPosition);
118
119 }, 10));
120
121 this.htmleditor.on('click', '.uk-htmleditor-button-code, .uk-htmleditor-button-preview', function(e) {
122
123 e.preventDefault();
124
125 if ($this.htmleditor.attr('data-mode') == 'tab') {
126
127 $this.htmleditor.find('.uk-htmleditor-button-code, .uk-htmleditor-button-preview').removeClass('uk-active').filter(this).addClass('uk-active');
128
129 $this.activetab = UI.$(this).hasClass('uk-htmleditor-button-code') ? 'code' : 'preview';
130 $this.htmleditor.attr('data-active-tab', $this.activetab);
131 $this.editor.refresh();
132 }
133 });
134
135 // toolbar actions
136 this.htmleditor.on('click', 'a[data-htmleditor-button]', function() {
137
138 if (!$this.code.is(':visible')) return;
139
140 $this.trigger('action.' + UI.$(this).data('htmleditor-button'), [$this.editor]);
141 });
142
143 this.preview.parent().css('height', this.code.height());
144
145 // autocomplete
146 if (this.options.autocomplete && this.CodeMirror.showHint && this.CodeMirror.hint && this.CodeMirror.hint.html) {
147
148 this.editor.on('inputRead', UI.Utils.debounce(function() {
149 var doc = $this.editor.getDoc(), POS = doc.getCursor(), mode = $this.CodeMirror.innerMode($this.editor.getMode(), $this.editor.getTokenAt(POS).state).mode.name;
150
151 if (mode == 'xml') { //html depends on xml
152
153 var cur = $this.editor.getCursor(), token = $this.editor.getTokenAt(cur);
154
155 if (token.string.charAt(0) == '<' || token.type == 'attribute') {
156 $this.CodeMirror.showHint($this.editor, $this.CodeMirror.hint.html, { completeSingle: false });
157 }
158 }
159 }, 100));
160 }
161
162 this.debouncedRedraw = UI.Utils.debounce(function () { $this.redraw(); }, 5);
163
164 this.on('init.uk.component', function() {
165 $this.debouncedRedraw();
166 });
167
168 this.element.attr('data-uk-check-display', 1).on('display.uk.check', function(e) {
169 if (this.htmleditor.is(":visible")) this.fit();
170 }.bind(this));
171
172 editors.push(this);
173 },
174
175 addButton: function(name, button) {
176 this.buttons[name] = button;
177 },
178
179 addButtons: function(buttons) {
180 UI.$.extend(this.buttons, buttons);
181 },
182
183 replaceInPreview: function(regexp, callback) {
184
185 var editor = this.editor, results = [], value = editor.getValue(), offset = -1, index = 0;
186
187 this.currentvalue = this.currentvalue.replace(regexp, function() {
188
189 offset = value.indexOf(arguments[0], ++offset);
190
191 var match = {
192 matches: arguments,
193 from : translateOffset(offset),
194 to : translateOffset(offset + arguments[0].length),
195 replace: function(value) {
196 editor.replaceRange(value, match.from, match.to);
197 },
198 inRange: function(cursor) {
199
200 if (cursor.line === match.from.line && cursor.line === match.to.line) {
201 return cursor.ch >= match.from.ch && cursor.ch < match.to.ch;
202 }
203
204 return (cursor.line === match.from.line && cursor.ch >= match.from.ch) ||
205 (cursor.line > match.from.line && cursor.line < match.to.line) ||
206 (cursor.line === match.to.line && cursor.ch < match.to.ch);
207 }
208 };
209
210 var result = typeof(callback) === 'string' ? callback : callback(match, index);
211
212 if (!result && result !== '') {
213 return arguments[0];
214 }
215
216 index++;
217
218 results.push(match);
219 return result;
220 });
221
222 function translateOffset(offset) {
223 var result = editor.getValue().substring(0, offset).split('\n');
224 return { line: result.length - 1, ch: result[result.length - 1].length }
225 }
226
227 return results;
228 },
229
230 _buildtoolbar: function() {
231
232 if (!(this.options.toolbar && this.options.toolbar.length)) return;
233
234 var $this = this, bar = [];
235
236 this.toolbar.empty();
237
238 this.options.toolbar.forEach(function(button) {
239 if (!$this.buttons[button]) return;
240
241 var title = $this.buttons[button].title ? $this.buttons[button].title : button;
242
243 bar.push('<li><a data-htmleditor-button="'+button+'" title="'+title+'" data-uk-tooltip>'+$this.buttons[button].label+'</a></li>');
244 });
245
246 this.toolbar.html(bar.join('\n'));
247 },
248
249 fit: function() {
250
251 var mode = this.options.mode;
252
253 if (mode == 'split' && this.htmleditor.width() < this.options.maxsplitsize) {
254 mode = 'tab';
255 }
256
257 if (mode == 'tab') {
258 if (!this.activetab) {
259 this.activetab = 'code';
260 this.htmleditor.attr('data-active-tab', this.activetab);
261 }
262
263 this.htmleditor.find('.uk-htmleditor-button-code, .uk-htmleditor-button-preview').removeClass('uk-active')
264 .filter(this.activetab == 'code' ? '.uk-htmleditor-button-code' : '.uk-htmleditor-button-preview')
265 .addClass('uk-active');
266 }
267
268 this.editor.refresh();
269 this.preview.parent().css('height', this.code.height());
270
271 this.htmleditor.attr('data-mode', mode);
272 },
273
274 redraw: function() {
275 this._buildtoolbar();
276 this.render();
277 this.fit();
278 },
279
280 getMode: function() {
281 return this.editor.getOption('mode');
282 },
283
284 getCursorMode: function() {
285 var param = { mode: 'html'};
286 this.trigger('cursorMode', [param]);
287 return param.mode;
288 },
289
290 render: function() {
291
292 this.currentvalue = this.editor.getValue();
293
294 if (!this.options.enablescripts) {
295 this.currentvalue = this.currentvalue.replace(/<(script|style)\b[^<]*(?:(?!<\/(script|style)>)<[^<]*)*<\/(script|style)>/img, '');
296 }
297
298 // empty code
299 if (!this.currentvalue) {
300
301 this.element.val('');
302 this.preview.container.html('');
303
304 return;
305 }
306
307 this.trigger('render', [this]);
308 this.trigger('renderLate', [this]);
309
310 this.preview.container.html(this.currentvalue);
311 },
312
313 addShortcut: function(name, callback) {
314 var map = {};
315 if (!UI.$.isArray(name)) {
316 name = [name];
317 }
318
319 name.forEach(function(key) {
320 map[key] = callback;
321 });
322
323 this.editor.addKeyMap(map);
324
325 return map;
326 },
327
328 addShortcutAction: function(action, shortcuts) {
329 var editor = this;
330 this.addShortcut(shortcuts, function() {
331 editor.element.trigger('action.' + action, [editor.editor]);
332 });
333 },
334
335 replaceSelection: function(replace) {
336
337 var text = this.editor.getSelection();
338
339 if (!text.length) {
340
341 var cur = this.editor.getCursor(),
342 curLine = this.editor.getLine(cur.line),
343 start = cur.ch,
344 end = start;
345
346 while (end < curLine.length && /[\w$]+/.test(curLine.charAt(end))) ++end;
347 while (start && /[\w$]+/.test(curLine.charAt(start - 1))) --start;
348
349 var curWord = start != end && curLine.slice(start, end);
350
351 if (curWord) {
352 this.editor.setSelection({ line: cur.line, ch: start}, { line: cur.line, ch: end });
353 text = curWord;
354 }
355 }
356
357 var html = replace.replace('$1', text);
358
359 this.editor.replaceSelection(html, 'end');
360 this.editor.focus();
361 },
362
363 replaceLine: function(replace) {
364 var pos = this.editor.getDoc().getCursor(),
365 text = this.editor.getLine(pos.line),
366 html = replace.replace('$1', text);
367
368 this.editor.replaceRange(html , { line: pos.line, ch: 0 }, { line: pos.line, ch: text.length });
369 this.editor.setCursor({ line: pos.line, ch: html.length });
370 this.editor.focus();
371 },
372
373 save: function() {
374 this.editor.save();
375 }
376 });
377
378
379 UI.components.htmleditor.template = [
380 '<div class="uk-htmleditor uk-clearfix" data-mode="split">',
381 '<div class="uk-htmleditor-navbar">',
382 '<ul class="uk-htmleditor-navbar-nav uk-htmleditor-toolbar"></ul>',
383 '<div class="uk-htmleditor-navbar-flip">',
384 '<ul class="uk-htmleditor-navbar-nav">',
385 '<li class="uk-htmleditor-button-code"><a>{:lblCodeview}</a></li>',
386 '<li class="uk-htmleditor-button-preview"><a>{:lblPreview}</a></li>',
387 '<li><a data-htmleditor-button="fullscreen"><i class="uk-icon-expand"></i></a></li>',
388 '</ul>',
389 '</div>',
390 '</div>',
391 '<div class="uk-htmleditor-content">',
392 '<div class="uk-htmleditor-code"></div>',
393 '<div class="uk-htmleditor-preview"><div></div></div>',
394 '</div>',
395 '</div>'
396 ].join('');
397
398
399 UI.plugin('htmleditor', 'base', {
400
401 init: function(editor) {
402
403 editor.addButtons({
404
405 fullscreen: {
406 title : 'Fullscreen',
407 label : '<i class="uk-icon-expand"></i>'
408 },
409 bold : {
410 title : 'Bold',
411 label : '<i class="uk-icon-bold"></i>'
412 },
413 italic : {
414 title : 'Italic',
415 label : '<i class="uk-icon-italic"></i>'
416 },
417 strike : {
418 title : 'Strikethrough',
419 label : '<i class="uk-icon-strikethrough"></i>'
420 },
421 blockquote : {
422 title : 'Blockquote',
423 label : '<i class="uk-icon-quote-right"></i>'
424 },
425 link : {
426 title : 'Link',
427 label : '<i class="uk-icon-link"></i>'
428 },
429 image : {
430 title : 'Image',
431 label : '<i class="uk-icon-picture-o"></i>'
432 },
433 listUl : {
434 title : 'Unordered List',
435 label : '<i class="uk-icon-list-ul"></i>'
436 },
437 listOl : {
438 title : 'Ordered List',
439 label : '<i class="uk-icon-list-ol"></i>'
440 }
441
442 });
443
444 addAction('bold', '<strong>$1</strong>');
445 addAction('italic', '<em>$1</em>');
446 addAction('strike', '<del>$1</del>');
447 addAction('blockquote', '<blockquote><p>$1</p></blockquote>', 'replaceLine');
448 addAction('link', '<a href="http://">$1</a>');
449 addAction('image', '<img src="http://" alt="$1">');
450
451 var listfn = function(tag) {
452 if (editor.getCursorMode() == 'html') {
453
454 tag = tag || 'ul';
455
456 var cm = editor.editor,
457 doc = cm.getDoc(),
458 pos = doc.getCursor(true),
459 posend = doc.getCursor(false),
460 im = CodeMirror.innerMode(cm.getMode(), cm.getTokenAt(cm.getCursor()).state),
461 inList = im && im.state && im.state.context && ['ul','ol'].indexOf(im.state.context.tagName) != -1;
462
463 for (var i=pos.line; i<(posend.line+1);i++) {
464 cm.replaceRange('<li>'+cm.getLine(i)+'</li>', { line: i, ch: 0 }, { line: i, ch: cm.getLine(i).length });
465 }
466
467 if (!inList) {
468 cm.replaceRange('<'+tag+'>'+"\n"+cm.getLine(pos.line), { line: pos.line, ch: 0 }, { line: pos.line, ch: cm.getLine(pos.line).length });
469 cm.replaceRange(cm.getLine((posend.line+1))+"\n"+'</'+tag+'>', { line: (posend.line+1), ch: 0 }, { line: (posend.line+1), ch: cm.getLine((posend.line+1)).length });
470 cm.setCursor({ line: posend.line+1, ch: cm.getLine(posend.line+1).length });
471 } else {
472 cm.setCursor({ line: posend.line, ch: cm.getLine(posend.line).length });
473 }
474
475 cm.focus();
476 }
477 };
478
479 editor.on('action.listUl', function() {
480 listfn('ul');
481 });
482
483 editor.on('action.listOl', function() {
484 listfn('ol');
485 });
486
487 editor.htmleditor.on('click', 'a[data-htmleditor-button="fullscreen"]', function() {
488
489 editor.htmleditor.toggleClass('uk-htmleditor-fullscreen');
490
491 var wrap = editor.editor.getWrapperElement();
492
493 if (editor.htmleditor.hasClass('uk-htmleditor-fullscreen')) {
494
495 var fixedParent = false, parents = editor.htmleditor.parents().each(function(){
496 if (UI.$(this).css('position')=='fixed' && !UI.$(this).is('html')) {
497 fixedParent = UI.$(this);
498 }
499 });
500
501 editor.htmleditor.data('fixedParents', false);
502
503 if (fixedParent) {
504
505 var transformed = [];
506
507 fixedParent = fixedParent.parent().find(parents).each(function(){
508
509 if (UI.$(this).css('transform') != 'none') {
510 transformed.push(UI.$(this).data('transform-reset', {
511 'transform': this.style.transform,
512 '-webkit-transform': this.style.webkitTransform,
513 '-webkit-transition':this.style.webkitTransition,
514 'transition':this.style.transition
515 }).css({
516 'transform': 'none',
517 '-webkit-transform': 'none',
518 '-webkit-transition':'none',
519 'transition':'none'
520 }));
521 }
522 });
523
524 editor.htmleditor.data('fixedParents', transformed);
525 }
526
527 editor.editor.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset, width: wrap.style.width, height: wrap.style.height};
528 wrap.style.width = '';
529 wrap.style.height = editor.content.height()+'px';
530 document.documentElement.style.overflow = 'hidden';
531
532 } else {
533
534 document.documentElement.style.overflow = '';
535 var info = editor.editor.state.fullScreenRestore;
536 wrap.style.width = info.width; wrap.style.height = info.height;
537 window.scrollTo(info.scrollLeft, info.scrollTop);
538
539 if (editor.htmleditor.data('fixedParents')) {
540 editor.htmleditor.data('fixedParents').forEach(function(parent){
541 parent.css(parent.data('transform-reset'));
542 });
543 }
544 }
545
546 setTimeout(function() {
547 editor.fit();
548 UI.$win.trigger('resize');
549 }, 50);
550 });
551
552 editor.addShortcut(['Ctrl-S', 'Cmd-S'], function() { editor.element.trigger('htmleditor-save', [editor]); });
553 editor.addShortcutAction('bold', ['Ctrl-B', 'Cmd-B']);
554
555 function addAction(name, replace, mode) {
556 editor.on('action.'+name, function() {
557 if (editor.getCursorMode() == 'html') {
558 editor[mode == 'replaceLine' ? 'replaceLine' : 'replaceSelection'](replace);
559 }
560 });
561 }
562 }
563 });
564
565 UI.plugin('htmleditor', 'markdown', {
566
567 init: function(editor) {
568
569 var parser = editor.options.mdparser || window.marked || null;
570
571 if (!parser) return;
572
573 if (editor.options.markdown) {
574 enableMarkdown();
575 }
576
577 addAction('bold', '**$1**');
578 addAction('italic', '*$1*');
579 addAction('strike', '~~$1~~');
580 addAction('blockquote', '> $1', 'replaceLine');
581 addAction('link', '[$1](http://)');
582 addAction('image', '![$1](http://)');
583
584 editor.on('action.listUl', function() {
585
586 if (editor.getCursorMode() == 'markdown') {
587
588 var cm = editor.editor,
589 pos = cm.getDoc().getCursor(true),
590 posend = cm.getDoc().getCursor(false);
591
592 for (var i=pos.line; i<(posend.line+1);i++) {
593 cm.replaceRange('* '+cm.getLine(i), { line: i, ch: 0 }, { line: i, ch: cm.getLine(i).length });
594 }
595
596 cm.setCursor({ line: posend.line, ch: cm.getLine(posend.line).length });
597 cm.focus();
598 }
599 });
600
601 editor.on('action.listOl', function() {
602
603 if (editor.getCursorMode() == 'markdown') {
604
605 var cm = editor.editor,
606 pos = cm.getDoc().getCursor(true),
607 posend = cm.getDoc().getCursor(false),
608 prefix = 1;
609
610 if (pos.line > 0) {
611 var prevline = cm.getLine(pos.line-1), matches;
612
613 if(matches = prevline.match(/^(\d+)\./)) {
614 prefix = Number(matches[1])+1;
615 }
616 }
617
618 for (var i=pos.line; i<(posend.line+1);i++) {
619 cm.replaceRange(prefix+'. '+cm.getLine(i), { line: i, ch: 0 }, { line: i, ch: cm.getLine(i).length });
620 prefix++;
621 }
622
623 cm.setCursor({ line: posend.line, ch: cm.getLine(posend.line).length });
624 cm.focus();
625 }
626 });
627
628 editor.on('renderLate', function() {
629 if (editor.editor.options.mode == 'gfm') {
630 editor.currentvalue = parser(editor.currentvalue);
631 }
632 });
633
634 editor.on('cursorMode', function(e, param) {
635 if (editor.editor.options.mode == 'gfm') {
636 var pos = editor.editor.getDoc().getCursor();
637 if (!editor.editor.getTokenAt(pos).state.base.htmlState) {
638 param.mode = 'markdown';
639 }
640 }
641 });
642
643 UI.$.extend(editor, {
644
645 enableMarkdown: function() {
646 enableMarkdown();
647 this.render();
648 },
649 disableMarkdown: function() {
650 this.editor.setOption('mode', 'htmlmixed');
651 this.htmleditor.find('.uk-htmleditor-button-code a').html(this.options.lblCodeview);
652 this.render();
653 }
654
655 });
656
657 // switch markdown mode on event
658 editor.on({
659 enableMarkdown : function() { editor.enableMarkdown(); },
660 disableMarkdown : function() { editor.disableMarkdown(); }
661 });
662
663 function enableMarkdown() {
664 editor.editor.setOption('mode', 'gfm');
665 editor.htmleditor.find('.uk-htmleditor-button-code a').html(editor.options.lblMarkedview);
666 }
667
668 function addAction(name, replace, mode) {
669 editor.on('action.'+name, function() {
670 if (editor.getCursorMode() == 'markdown') {
671 editor[mode == 'replaceLine' ? 'replaceLine' : 'replaceSelection'](replace);
672 }
673 });
674 }
675 }
676 });
677
678 return UI.htmleditor;
679});