Questions-and-Answers/media/uikit-v2/js/components/htmleditor.js

680 lines
25 KiB
JavaScript

/*! UIkit 2.27.4 | http://www.getuikit.com | (c) 2014 YOOtheme | MIT License */
(function(addon) {
var component;
if (window.UIkit2) {
component = addon(UIkit2);
}
if (typeof define == 'function' && define.amd) {
define('uikit-htmleditor', ['uikit'], function(){
return component || addon(UIkit2);
});
}
})(function(UI) {
"use strict";
var editors = [];
UI.component('htmleditor', {
defaults: {
iframe : false,
mode : 'split',
markdown : false,
autocomplete : true,
enablescripts: false,
height : 500,
maxsplitsize : 1000,
codemirror : { mode: 'htmlmixed', lineWrapping: true, dragDrop: false, autoCloseTags: true, matchTags: true, autoCloseBrackets: true, matchBrackets: true, indentUnit: 4, indentWithTabs: false, tabSize: 4, hintOptions: {completionSingle:false} },
toolbar : [ 'bold', 'italic', 'strike', 'link', 'image', 'blockquote', 'listUl', 'listOl' ],
lblPreview : 'Preview',
lblCodeview : 'HTML',
lblMarkedview: 'Markdown'
},
boot: function() {
// init code
UI.ready(function(context) {
UI.$('textarea[data-uk-htmleditor]', context).each(function() {
var editor = UI.$(this);
if (!editor.data('htmleditor')) {
UI.htmleditor(editor, UI.Utils.options(editor.attr('data-uk-htmleditor')));
}
});
});
},
init: function() {
var $this = this, tpl = UI.components.htmleditor.template;
this.CodeMirror = this.options.CodeMirror || CodeMirror;
this.buttons = {};
tpl = tpl.replace(/\{:lblPreview}/g, this.options.lblPreview);
tpl = tpl.replace(/\{:lblCodeview}/g, this.options.lblCodeview);
this.htmleditor = UI.$(tpl);
this.content = this.htmleditor.find('.uk-htmleditor-content');
this.toolbar = this.htmleditor.find('.uk-htmleditor-toolbar');
this.preview = this.htmleditor.find('.uk-htmleditor-preview').children().eq(0);
this.code = this.htmleditor.find('.uk-htmleditor-code');
this.element.before(this.htmleditor).appendTo(this.code);
this.editor = this.CodeMirror.fromTextArea(this.element[0], this.options.codemirror);
this.editor.htmleditor = this;
this.editor.on('change', UI.Utils.debounce(function() { $this.render(); }, 150));
this.editor.on('change', function() {
$this.editor.save();
$this.element.trigger('input');
});
this.code.find('.CodeMirror').css('height', this.options.height);
// iframe mode?
if (this.options.iframe) {
this.iframe = UI.$('<iframe class="uk-htmleditor-iframe" frameborder="0" scrolling="auto" height="100" width="100%"></iframe>');
this.preview.append(this.iframe);
// must open and close document object to start using it!
this.iframe[0].contentWindow.document.open();
this.iframe[0].contentWindow.document.close();
this.preview.container = UI.$(this.iframe[0].contentWindow.document).find('body');
// append custom stylesheet
if (typeof(this.options.iframe) === 'string') {
this.preview.container.parent().append('<link rel="stylesheet" href="'+this.options.iframe+'">');
}
} else {
this.preview.container = this.preview;
}
UI.$win.on('resize load', UI.Utils.debounce(function() { $this.fit(); }, 200));
var previewContainer = this.iframe ? this.preview.container:$this.preview.parent(),
codeContent = this.code.find('.CodeMirror-sizer'),
codeScroll = this.code.find('.CodeMirror-scroll').on('scroll', UI.Utils.debounce(function() {
if ($this.htmleditor.attr('data-mode') == 'tab') return;
// calc position
var codeHeight = codeContent.height() - codeScroll.height(),
previewHeight = previewContainer[0].scrollHeight - ($this.iframe ? $this.iframe.height() : previewContainer.height()),
ratio = previewHeight / codeHeight,
previewPosition = codeScroll.scrollTop() * ratio;
// apply new scroll
previewContainer.scrollTop(previewPosition);
}, 10));
this.htmleditor.on('click', '.uk-htmleditor-button-code, .uk-htmleditor-button-preview', function(e) {
e.preventDefault();
if ($this.htmleditor.attr('data-mode') == 'tab') {
$this.htmleditor.find('.uk-htmleditor-button-code, .uk-htmleditor-button-preview').removeClass('uk-active').filter(this).addClass('uk-active');
$this.activetab = UI.$(this).hasClass('uk-htmleditor-button-code') ? 'code' : 'preview';
$this.htmleditor.attr('data-active-tab', $this.activetab);
$this.editor.refresh();
}
});
// toolbar actions
this.htmleditor.on('click', 'a[data-htmleditor-button]', function() {
if (!$this.code.is(':visible')) return;
$this.trigger('action.' + UI.$(this).data('htmleditor-button'), [$this.editor]);
});
this.preview.parent().css('height', this.code.height());
// autocomplete
if (this.options.autocomplete && this.CodeMirror.showHint && this.CodeMirror.hint && this.CodeMirror.hint.html) {
this.editor.on('inputRead', UI.Utils.debounce(function() {
var doc = $this.editor.getDoc(), POS = doc.getCursor(), mode = $this.CodeMirror.innerMode($this.editor.getMode(), $this.editor.getTokenAt(POS).state).mode.name;
if (mode == 'xml') { //html depends on xml
var cur = $this.editor.getCursor(), token = $this.editor.getTokenAt(cur);
if (token.string.charAt(0) == '<' || token.type == 'attribute') {
$this.CodeMirror.showHint($this.editor, $this.CodeMirror.hint.html, { completeSingle: false });
}
}
}, 100));
}
this.debouncedRedraw = UI.Utils.debounce(function () { $this.redraw(); }, 5);
this.on('init.uk.component', function() {
$this.debouncedRedraw();
});
this.element.attr('data-uk-check-display', 1).on('display.uk.check', function(e) {
if (this.htmleditor.is(":visible")) this.fit();
}.bind(this));
editors.push(this);
},
addButton: function(name, button) {
this.buttons[name] = button;
},
addButtons: function(buttons) {
UI.$.extend(this.buttons, buttons);
},
replaceInPreview: function(regexp, callback) {
var editor = this.editor, results = [], value = editor.getValue(), offset = -1, index = 0;
this.currentvalue = this.currentvalue.replace(regexp, function() {
offset = value.indexOf(arguments[0], ++offset);
var match = {
matches: arguments,
from : translateOffset(offset),
to : translateOffset(offset + arguments[0].length),
replace: function(value) {
editor.replaceRange(value, match.from, match.to);
},
inRange: function(cursor) {
if (cursor.line === match.from.line && cursor.line === match.to.line) {
return cursor.ch >= match.from.ch && cursor.ch < match.to.ch;
}
return (cursor.line === match.from.line && cursor.ch >= match.from.ch) ||
(cursor.line > match.from.line && cursor.line < match.to.line) ||
(cursor.line === match.to.line && cursor.ch < match.to.ch);
}
};
var result = typeof(callback) === 'string' ? callback : callback(match, index);
if (!result && result !== '') {
return arguments[0];
}
index++;
results.push(match);
return result;
});
function translateOffset(offset) {
var result = editor.getValue().substring(0, offset).split('\n');
return { line: result.length - 1, ch: result[result.length - 1].length }
}
return results;
},
_buildtoolbar: function() {
if (!(this.options.toolbar && this.options.toolbar.length)) return;
var $this = this, bar = [];
this.toolbar.empty();
this.options.toolbar.forEach(function(button) {
if (!$this.buttons[button]) return;
var title = $this.buttons[button].title ? $this.buttons[button].title : button;
bar.push('<li><a data-htmleditor-button="'+button+'" title="'+title+'" data-uk-tooltip>'+$this.buttons[button].label+'</a></li>');
});
this.toolbar.html(bar.join('\n'));
},
fit: function() {
var mode = this.options.mode;
if (mode == 'split' && this.htmleditor.width() < this.options.maxsplitsize) {
mode = 'tab';
}
if (mode == 'tab') {
if (!this.activetab) {
this.activetab = 'code';
this.htmleditor.attr('data-active-tab', this.activetab);
}
this.htmleditor.find('.uk-htmleditor-button-code, .uk-htmleditor-button-preview').removeClass('uk-active')
.filter(this.activetab == 'code' ? '.uk-htmleditor-button-code' : '.uk-htmleditor-button-preview')
.addClass('uk-active');
}
this.editor.refresh();
this.preview.parent().css('height', this.code.height());
this.htmleditor.attr('data-mode', mode);
},
redraw: function() {
this._buildtoolbar();
this.render();
this.fit();
},
getMode: function() {
return this.editor.getOption('mode');
},
getCursorMode: function() {
var param = { mode: 'html'};
this.trigger('cursorMode', [param]);
return param.mode;
},
render: function() {
this.currentvalue = this.editor.getValue();
if (!this.options.enablescripts) {
this.currentvalue = this.currentvalue.replace(/<(script|style)\b[^<]*(?:(?!<\/(script|style)>)<[^<]*)*<\/(script|style)>/img, '');
}
// empty code
if (!this.currentvalue) {
this.element.val('');
this.preview.container.html('');
return;
}
this.trigger('render', [this]);
this.trigger('renderLate', [this]);
this.preview.container.html(this.currentvalue);
},
addShortcut: function(name, callback) {
var map = {};
if (!UI.$.isArray(name)) {
name = [name];
}
name.forEach(function(key) {
map[key] = callback;
});
this.editor.addKeyMap(map);
return map;
},
addShortcutAction: function(action, shortcuts) {
var editor = this;
this.addShortcut(shortcuts, function() {
editor.element.trigger('action.' + action, [editor.editor]);
});
},
replaceSelection: function(replace) {
var text = this.editor.getSelection();
if (!text.length) {
var cur = this.editor.getCursor(),
curLine = this.editor.getLine(cur.line),
start = cur.ch,
end = start;
while (end < curLine.length && /[\w$]+/.test(curLine.charAt(end))) ++end;
while (start && /[\w$]+/.test(curLine.charAt(start - 1))) --start;
var curWord = start != end && curLine.slice(start, end);
if (curWord) {
this.editor.setSelection({ line: cur.line, ch: start}, { line: cur.line, ch: end });
text = curWord;
}
}
var html = replace.replace('$1', text);
this.editor.replaceSelection(html, 'end');
this.editor.focus();
},
replaceLine: function(replace) {
var pos = this.editor.getDoc().getCursor(),
text = this.editor.getLine(pos.line),
html = replace.replace('$1', text);
this.editor.replaceRange(html , { line: pos.line, ch: 0 }, { line: pos.line, ch: text.length });
this.editor.setCursor({ line: pos.line, ch: html.length });
this.editor.focus();
},
save: function() {
this.editor.save();
}
});
UI.components.htmleditor.template = [
'<div class="uk-htmleditor uk-clearfix" data-mode="split">',
'<div class="uk-htmleditor-navbar">',
'<ul class="uk-htmleditor-navbar-nav uk-htmleditor-toolbar"></ul>',
'<div class="uk-htmleditor-navbar-flip">',
'<ul class="uk-htmleditor-navbar-nav">',
'<li class="uk-htmleditor-button-code"><a>{:lblCodeview}</a></li>',
'<li class="uk-htmleditor-button-preview"><a>{:lblPreview}</a></li>',
'<li><a data-htmleditor-button="fullscreen"><i class="uk-icon-expand"></i></a></li>',
'</ul>',
'</div>',
'</div>',
'<div class="uk-htmleditor-content">',
'<div class="uk-htmleditor-code"></div>',
'<div class="uk-htmleditor-preview"><div></div></div>',
'</div>',
'</div>'
].join('');
UI.plugin('htmleditor', 'base', {
init: function(editor) {
editor.addButtons({
fullscreen: {
title : 'Fullscreen',
label : '<i class="uk-icon-expand"></i>'
},
bold : {
title : 'Bold',
label : '<i class="uk-icon-bold"></i>'
},
italic : {
title : 'Italic',
label : '<i class="uk-icon-italic"></i>'
},
strike : {
title : 'Strikethrough',
label : '<i class="uk-icon-strikethrough"></i>'
},
blockquote : {
title : 'Blockquote',
label : '<i class="uk-icon-quote-right"></i>'
},
link : {
title : 'Link',
label : '<i class="uk-icon-link"></i>'
},
image : {
title : 'Image',
label : '<i class="uk-icon-picture-o"></i>'
},
listUl : {
title : 'Unordered List',
label : '<i class="uk-icon-list-ul"></i>'
},
listOl : {
title : 'Ordered List',
label : '<i class="uk-icon-list-ol"></i>'
}
});
addAction('bold', '<strong>$1</strong>');
addAction('italic', '<em>$1</em>');
addAction('strike', '<del>$1</del>');
addAction('blockquote', '<blockquote><p>$1</p></blockquote>', 'replaceLine');
addAction('link', '<a href="http://">$1</a>');
addAction('image', '<img src="http://" alt="$1">');
var listfn = function(tag) {
if (editor.getCursorMode() == 'html') {
tag = tag || 'ul';
var cm = editor.editor,
doc = cm.getDoc(),
pos = doc.getCursor(true),
posend = doc.getCursor(false),
im = CodeMirror.innerMode(cm.getMode(), cm.getTokenAt(cm.getCursor()).state),
inList = im && im.state && im.state.context && ['ul','ol'].indexOf(im.state.context.tagName) != -1;
for (var i=pos.line; i<(posend.line+1);i++) {
cm.replaceRange('<li>'+cm.getLine(i)+'</li>', { line: i, ch: 0 }, { line: i, ch: cm.getLine(i).length });
}
if (!inList) {
cm.replaceRange('<'+tag+'>'+"\n"+cm.getLine(pos.line), { line: pos.line, ch: 0 }, { line: pos.line, ch: cm.getLine(pos.line).length });
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 });
cm.setCursor({ line: posend.line+1, ch: cm.getLine(posend.line+1).length });
} else {
cm.setCursor({ line: posend.line, ch: cm.getLine(posend.line).length });
}
cm.focus();
}
};
editor.on('action.listUl', function() {
listfn('ul');
});
editor.on('action.listOl', function() {
listfn('ol');
});
editor.htmleditor.on('click', 'a[data-htmleditor-button="fullscreen"]', function() {
editor.htmleditor.toggleClass('uk-htmleditor-fullscreen');
var wrap = editor.editor.getWrapperElement();
if (editor.htmleditor.hasClass('uk-htmleditor-fullscreen')) {
var fixedParent = false, parents = editor.htmleditor.parents().each(function(){
if (UI.$(this).css('position')=='fixed' && !UI.$(this).is('html')) {
fixedParent = UI.$(this);
}
});
editor.htmleditor.data('fixedParents', false);
if (fixedParent) {
var transformed = [];
fixedParent = fixedParent.parent().find(parents).each(function(){
if (UI.$(this).css('transform') != 'none') {
transformed.push(UI.$(this).data('transform-reset', {
'transform': this.style.transform,
'-webkit-transform': this.style.webkitTransform,
'-webkit-transition':this.style.webkitTransition,
'transition':this.style.transition
}).css({
'transform': 'none',
'-webkit-transform': 'none',
'-webkit-transition':'none',
'transition':'none'
}));
}
});
editor.htmleditor.data('fixedParents', transformed);
}
editor.editor.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset, width: wrap.style.width, height: wrap.style.height};
wrap.style.width = '';
wrap.style.height = editor.content.height()+'px';
document.documentElement.style.overflow = 'hidden';
} else {
document.documentElement.style.overflow = '';
var info = editor.editor.state.fullScreenRestore;
wrap.style.width = info.width; wrap.style.height = info.height;
window.scrollTo(info.scrollLeft, info.scrollTop);
if (editor.htmleditor.data('fixedParents')) {
editor.htmleditor.data('fixedParents').forEach(function(parent){
parent.css(parent.data('transform-reset'));
});
}
}
setTimeout(function() {
editor.fit();
UI.$win.trigger('resize');
}, 50);
});
editor.addShortcut(['Ctrl-S', 'Cmd-S'], function() { editor.element.trigger('htmleditor-save', [editor]); });
editor.addShortcutAction('bold', ['Ctrl-B', 'Cmd-B']);
function addAction(name, replace, mode) {
editor.on('action.'+name, function() {
if (editor.getCursorMode() == 'html') {
editor[mode == 'replaceLine' ? 'replaceLine' : 'replaceSelection'](replace);
}
});
}
}
});
UI.plugin('htmleditor', 'markdown', {
init: function(editor) {
var parser = editor.options.mdparser || window.marked || null;
if (!parser) return;
if (editor.options.markdown) {
enableMarkdown();
}
addAction('bold', '**$1**');
addAction('italic', '*$1*');
addAction('strike', '~~$1~~');
addAction('blockquote', '> $1', 'replaceLine');
addAction('link', '[$1](http://)');
addAction('image', '![$1](http://)');
editor.on('action.listUl', function() {
if (editor.getCursorMode() == 'markdown') {
var cm = editor.editor,
pos = cm.getDoc().getCursor(true),
posend = cm.getDoc().getCursor(false);
for (var i=pos.line; i<(posend.line+1);i++) {
cm.replaceRange('* '+cm.getLine(i), { line: i, ch: 0 }, { line: i, ch: cm.getLine(i).length });
}
cm.setCursor({ line: posend.line, ch: cm.getLine(posend.line).length });
cm.focus();
}
});
editor.on('action.listOl', function() {
if (editor.getCursorMode() == 'markdown') {
var cm = editor.editor,
pos = cm.getDoc().getCursor(true),
posend = cm.getDoc().getCursor(false),
prefix = 1;
if (pos.line > 0) {
var prevline = cm.getLine(pos.line-1), matches;
if(matches = prevline.match(/^(\d+)\./)) {
prefix = Number(matches[1])+1;
}
}
for (var i=pos.line; i<(posend.line+1);i++) {
cm.replaceRange(prefix+'. '+cm.getLine(i), { line: i, ch: 0 }, { line: i, ch: cm.getLine(i).length });
prefix++;
}
cm.setCursor({ line: posend.line, ch: cm.getLine(posend.line).length });
cm.focus();
}
});
editor.on('renderLate', function() {
if (editor.editor.options.mode == 'gfm') {
editor.currentvalue = parser(editor.currentvalue);
}
});
editor.on('cursorMode', function(e, param) {
if (editor.editor.options.mode == 'gfm') {
var pos = editor.editor.getDoc().getCursor();
if (!editor.editor.getTokenAt(pos).state.base.htmlState) {
param.mode = 'markdown';
}
}
});
UI.$.extend(editor, {
enableMarkdown: function() {
enableMarkdown();
this.render();
},
disableMarkdown: function() {
this.editor.setOption('mode', 'htmlmixed');
this.htmleditor.find('.uk-htmleditor-button-code a').html(this.options.lblCodeview);
this.render();
}
});
// switch markdown mode on event
editor.on({
enableMarkdown : function() { editor.enableMarkdown(); },
disableMarkdown : function() { editor.disableMarkdown(); }
});
function enableMarkdown() {
editor.editor.setOption('mode', 'gfm');
editor.htmleditor.find('.uk-htmleditor-button-code a').html(editor.options.lblMarkedview);
}
function addAction(name, replace, mode) {
editor.on('action.'+name, function() {
if (editor.getCursorMode() == 'markdown') {
editor[mode == 'replaceLine' ? 'replaceLine' : 'replaceSelection'](replace);
}
});
}
}
});
return UI.htmleditor;
});