/*!
* @name ElkArte Forum
* @copyright ElkArte Forum contributors
* @license BSD http://opensource.org/licenses/BSD-3-Clause
*
* @version 1.1.9
*/
/** global: elk_session_var, elk_session_id, ila_filename, elk_scripturl, sceditor */
/**
* Extension functions to provide ElkArte utility functions within sceditor
*/
const itemCodes = ["*:disc", "@:disc", "+:square", "x:square", "#:decimal", "0:decimal", "O:circle", "o:circle"];
(function ($)
{
var extensionMethods = {
addEvent: function (id, event, func)
{
var current_event = event,
$_id = $('#' + id);
$_id.parent().on(current_event, 'textarea', func);
var oIframe = $_id.parent().find('iframe')[0],
oIframeWindow = oIframe.contentWindow;
if (oIframeWindow !== null && oIframeWindow.document)
{
var oIframeDoc = oIframeWindow.document,
oIframeBody = oIframeDoc.body;
$(oIframeBody).on(current_event, func);
}
},
InsertText: function (text, bClear) {
var bIsSource = this.inSourceMode();
if (!bIsSource)
this.toggleSourceMode();
var current_value = this.getSourceEditorValue(false),
iEmpty = current_value.length;
current_value = bClear ? text + "\n" : current_value + (iEmpty > 0 ? "\n" : "") + text + "\n";
this.setSourceEditorValue(current_value);
if (!bIsSource)
this.toggleSourceMode();
},
getText: function (filter) {
var current_value = '';
if (this.inSourceMode())
current_value = this.getSourceEditorValue(false);
else
current_value = this.getWysiwygEditorValue(filter);
return current_value;
},
appendEmoticon: function (code, emoticon) {
if (emoticon === '')
{
line.append($('
'));
}
else
{
$img = $('')
.attr({
src: emoticon.url || emoticon,
alt: code,
title: emoticon.tooltip || emoticon
})
.on('click', function (e)
{
var start = '',
end = '';
if (base.opts.emoticonsCompat)
{
start = ' ';
end = ' ';
}
if (base.inSourceMode())
{
base.sourceEditorInsertText(' ' + $(this).attr('alt') + ' ');
}
else
{
base.wysiwygEditorInsertHtml(start + '' + end);
}
e.preventDefault();
});
line.append($('').append($img));
}
},
storeLastState: function () {
this.wasSource = this.inSourceMode();
},
setTextMode: function () {
if (!this.inSourceMode())
this.toggleSourceMode();
},
createPermanentDropDown: function () {
var emoticons = $.extend({}, this.opts.emoticons.dropdown);
base = this;
content = $('
');
line = $('');
// For any smileys that go in the more popup
if (!$.isEmptyObject(this.opts.emoticons.popup))
{
this.opts.emoticons.more = this.opts.emoticons.popup;
moreButton = $('').text(this._('More')).on('click', function ()
{
var popup_box = $('.sceditor-smileyPopup');
if (popup_box.length > 0)
{
popup_box.fadeIn('fast');
}
else
{
var emoticons = $.extend({}, base.opts.emoticons.popup),
titlebar = $('');
popupContent = $('');
line = $('');
// Create our popup, title bar, smiles, then the close button
popupContent.append(titlebar);
// Add in all the smileys / lines
$.each(emoticons, base.appendEmoticon);
if (line.children().length > 0)
{
popupContent.append(line);
}
closeButton = $('').text(base._('Close')).on('click', function ()
{
$(".sceditor-smileyPopup").fadeOut('fast');
});
if (typeof closeButton !== 'undefined')
{
popupContent.append(closeButton);
}
// Show the smiley popup
$dropdown = $('')
.append(popupContent)
.appendTo($('body'))
.css({
"top": $(window).height() * 0.2,
"left": $(window).width() * 0.5 - (popupContent.find('#sceditor-popup-smiley').width() / 2)
});
dropdownIgnoreLastClick = true;
// Allow the smiley window to be moved about
$('.sceditor-smileyPopup').draggable({handle: '.sceditor-popup-grip'});
// stop clicks within the dropdown from being handled
$dropdown.on('click', function (e)
{
e.stopPropagation();
});
}
});
}
// Show the standard placement icons
$.each(emoticons, base.appendEmoticon);
if (line.children().length > 0)
{
content.append(line);
}
$(".sceditor-toolbar").append(content);
// Show the more button on the editor if we have more
if (typeof moreButton !== 'undefined')
{
content.append(moreButton);
}
},
/**
* When you don't have a DOM node to check (non rendering tag), this will
* check if the cursor is inside of the supplied tag. Used for footnote
* and spoiler which don't and should not have wizzy rendering for best UE
*
* @param tag
* @returns {number}
*/
checkInsideSourceTag: function (tag)
{
let currentNode = this.currentNode(),
currentRange = this.getRangeHelper();
if (currentRange.selectedRange())
{
let end = currentRange.selectedRange().startOffset,
text = typeof currentNode !== 'undefined' ? currentNode.textContent : '';
// Left and right text from the cursor position and tag positions
let left = text.substr(0, end),
right = text.substr(end),
l1 = left.lastIndexOf("[" + tag + "]"),
l2 = left.lastIndexOf("[/" + tag + "]"),
r1 = right.indexOf("[" + tag + "]"),
r2 = right.indexOf("[/" + tag + "]");
// Inside ot the [tag]your are here[/tag]
if ((l1 > -1 && l1 > l2) || (r2 > -1 && (r1 === -1 || (r1 > r2))))
{
return 1;
}
}
return 0;
},
/**
* Allows selecting the toolbar icon to end the tag if you are in that tag or
* start a new tag otherwise.
*
* @param nodeName the name of the node such as tt or pre
* @param nodeClass the specific class name of the nodeName like bbc_tt
* @param insertElement what you want to insert to END the tag e.g. span, p (inline/block)
*/
toggleTagStartEnd: function(nodeName, nodeClass, insertElement)
{
let editor = this,
rangeHelper = editor.getRangeHelper(),
tag,
range,
blank;
// Set our markers and make a copy
rangeHelper.saveRange();
range = rangeHelper.cloneSelected();
// Find the name/class node if we are in one at all
tag = range.commonAncestorContainer;
while (tag && (tag.nodeType !== 1 ||
(tag.tagName.toLowerCase() !== nodeName && !tag.classList.contains(nodeClass))))
{
tag = tag.parentNode;
}
// If we found one, we are in it and the user has requested to end this one
if (tag)
{
// Place the markers at the end of the found node
range.setEndAfter(tag);
range.collapse(false);
// Stuff in a new spacer node at that position
blank = tag.ownerDocument.createElement(insertElement);
blank.innerHTML = ' ';
range.insertNode(blank);
// Move the caret after this new empty node
let range_new = document.createRange();
range_new.setStartAfter(blank);
// Set sceditor to this new range
rangeHelper.selectRange(range_new);
editor.focus();
return;
}
// Otherwise, a new tag for them, done by the caller
rangeHelper.restoreRange();
editor.insert('<' + nodeName + ' class="' + nodeClass + '">', '' + nodeName + '>', false);
},
/**
* Gets the first parent node that matches the selector
*
* @param node
* @param selector
* @returns {HTMLElement|undefined}
*/
parent: function(node, selector) {
let parent = node || {};
while ((parent = parent.parentNode) && !/(9|11)/.test(parent.nodeType))
{
if (!selector || this.is(parent, selector))
{
return parent;
}
}
},
/**
* Checks if node matches the given selector.
*
* @param node
* @param selector
* @returns {boolean}
*/
is: function(node, selector) {
let result = false;
if (node && node.nodeType === 1)
{
result = (node.matches || node.msMatchesSelector || node.webkitMatchesSelector).call(node, selector);
}
return result;
},
/**
* Checks the passed node and all parents and
* returns the first matching node if any.
*
* @param node
* @param selector
* @returns {HTMLElement|undefined}
*/
closest: function(node, selector) {
return this.is(node, selector) ? node : this.parent(node, selector);
},
/**
* If they selected any text in the node, assumes they want to remove that
* formatting defined by the parent. If nothing is selected simply returns
*
* @param tag name of tag/node to remove
* @returns {boolean}
*/
checkRemoveFormat: function(tag) {
let range = this.getRangeHelper(),
selected = range.selectedRange();
if (selected.startOffset !== selected.endOffset)
{
let parent = range.parentNode(),
node = this.closest(parent, tag);
if (node)
{
let frag = document.createDocumentFragment(),
child;
while (node.firstChild)
{
child = node.removeChild(node.firstChild);
frag.appendChild(child);
}
node.parentNode.replaceChild(frag, node);
return true;
}
}
return false;
}
};
$.extend(true, $['sceditor'].prototype, extensionMethods);
})(jQuery);
/**
* ElkArte unique commands to add to the toolbar, when a button
* with the same name is selected, it will trigger these definitions
*
* tooltip - the hover text, this is the name in the editors.(language).php file
* txtExec - this is the text to insert before and after the cursor or selected text
* when in the plain text part of the editor
* exec - this is called when in the wizzy part of the editor to insert text or html tags
* state - this is used to determine if a button should be shown as active or not
*
* Adds Tt, Pre, Spoiler, Footnote commands
*/
$.sceditor.command
.set('space', {})
.set('spoiler', {
state: function ()
{
if (typeof this.checkInsideSourceTag === 'function')
{
this.checkInsideSourceTag('spoiler');
}
},
exec: function ()
{
this.insert('[spoiler]', '[/spoiler]');
},
txtExec: ['[spoiler]', '[/spoiler]'],
tooltip: 'Insert Spoiler'
})
.set('footnote', {
state: function ()
{
if (typeof this.checkInsideSourceTag === 'function')
{
this.checkInsideSourceTag('footnote');
}
},
exec: function ()
{
this.insert('[footnote]', '[/footnote]');
},
txtExec: ['[footnote]', '[/footnote]'],
tooltip: 'Insert Footnote'
})
.set('tt', {
state: function ()
{
let currentNode = this.currentNode();
if (currentNode && currentNode.nodeType === 3)
{
currentNode = currentNode.parentNode;
}
return (currentNode && (currentNode.classList.contains('bbc_tt') && currentNode.tagName.toLowerCase() === 'span')) ? 1 : 0;
},
exec: function ()
{
if (typeof this.toggleTagStartEnd !== 'function')
{
return;
}
if (!this.checkRemoveFormat('span.bbc_tt'))
{
this.toggleTagStartEnd('span', 'bbc_tt', 'span');
}
},
txtExec: ['[tt]', '[/tt]'],
tooltip: 'Teletype'
})
.set('pre', {
state: function ()
{
let currentNode = this.currentNode();
if (currentNode && currentNode.nodeType === 3)
{
currentNode = currentNode.parentNode;
}
return (currentNode && currentNode.tagName.toLowerCase() === 'pre') ? 1 : 0;
},
exec: function ()
{
if (typeof this.toggleTagStartEnd !== 'function')
{
return;
}
if (!this.checkRemoveFormat('pre.bbc_pre'))
{
this.toggleTagStartEnd('pre', 'bbc_pre', 'p');
}
},
txtExec: ['[pre]', '[/pre]'],
tooltip: 'Preformatted Text'
})
/*
* ElkArte modifications to existing commands so they display as we like
*
* Makes changes to the text inserted for Bulletlist, OrderedList and Table
*/
.set('bulletlist', {
txtExec: function(called, selected)
{
// Selected some text to turn into a list?
if (selected)
{
let content = '';
$.each(selected.split(/\r?\n/), function ()
{
content += (content ? '\n' : '') + '[li]' + this + '[/li]';
});
return this.insertText('[list]\n' + content + '\n[/list]');
}
this.insertText('[list]\n[li]', ' [/li]\n[li] [/li]\n[/list]');
}
})
.set('orderedlist', {
txtExec: function (caller, selected)
{
if (selected)
{
let content = '';
$.each(selected.split(/\r?\n/), function ()
{
content += (content ? '\n' : '') + '[li]' + this + '[/li]';
});
return this.insertText('[list type=decimal]\n' + content + '\n[/list]');
}
this.insertText('[list type=decimal]\n[li] ', '[/li]\n[li] [/li]\n[/list]');
}
})
.set('table', {
txtExec: ['[table]\n[tr]\n[td]', '[/td]\n[/tr]\n[/table]']
});
/**
* ElkArte custom bbc tags added to provide for the existing user experience
*
* These command define what happens to tags as we toggle from and to wizzy mode
* It converts html back to bbc or bbc back to html. Read the sceditor docs for more
*
* Adds / modifies BBC codes List, Tt, Pre, Quote, Code, Img
*/
$.sceditor.plugins.bbcode.bbcode
.set('tt', {
tags: {
tt: null,
span: {'class': ['bbc_tt']}
},
format: '[tt]{0}[/tt]',
html: '{0}'
})
.set('pre', {
tags: {
pre: null,
pre: {'class': ['bbc_pre']}
},
isInline: false,
format: '[pre]{0}[/pre]',
html: '{0}
'
})
.set('member', {
isInline: true,
format: function (element, content)
{
return '[member=' + element.attr('data-mention') + ']' + content.replace('@', '') + '[/member]';
},
html: function (token, attrs, content)
{
if (typeof attrs.defaultattr === 'undefined' || attrs.defaultattr.length === 0)
{
attrs.defaultattr = content;
}
return '@' + content.replace('@', '') + '';
}
})
.set('me', {
tags: {
me: {
'data-me': null
}
},
isInline: true,
quoteType: $.sceditor.BBCodeParser.QuoteType.always,
format: function (element, content)
{
return '[me=' + element.attr('data-me') + ']' + content.replace(element.attr('data-me') + ' ', '') + '[/me]';
},
html: function (token, attrs, content)
{
if (typeof attrs.defaultattr === 'undefined' || attrs.defaultattr.length === 0)
{
attrs.defaultattr = '';
}
return '' + attrs.defaultattr + ' ' + content + '';
}
})
.set('attachurl', {
allowsEmpty: false,
quoteType: $.sceditor.BBCodeParser.QuoteType.never,
format: function (element, content)
{
return '[attachurl]' + content + '[/attachurl]';
},
html: function (token, attrs, content)
{
// @todo new action to return real filename?
return '( ' + ila_filename + ')';
}
})
.set('attach', {
allowsEmpty: false,
quoteType: $.sceditor.BBCodeParser.QuoteType.never,
format: function (element, content)
{
/**
* This function is not used because no specific html tag is associated,
* instead the 'img'.format takes care of finding the ILA images and process
* them accordingly to return the [attach] tag.
*/
let attribs = '',
params = function (names)
{
names.forEach(function (name)
{
if (element.attr(name))
{
attribs += ' ' + name + '=' + element.attr(name);
}
else if (element.style[name])
{
attribs += ' ' + name + '=' + element.style[name];
}
});
};
params(['width', 'height', 'align', 'type']);
return '[attach' + attribs + ']' + content + '[/attach]';
},
html: function (token, attrs, content)
{
let attribs = '',
align = '',
thumb = '',
params = function (names)
{
names.forEach(function (name)
{
if (typeof attrs[name] !== 'undefined')
{
attribs += ' ' + name + '=' + attrs[name];
}
});
};
params(['width', 'height', 'align', 'type']);
a_attribs = attribs;
if (typeof attrs.align !== 'undefined')
{
align = ' class="img_bbc float' + attrs.align + '"';
}
if (typeof attrs.type !== 'undefined')
{
thumb = ';' + attrs.type;
}
return '';
}
})
.set('center', {
tags: {
center: null
},
styles: {
'text-align': ['center', '-webkit-center', '-moz-center', '-khtml-center']
},
isInline: true,
format: '[center]{0}[/center]',
html: '{0}'
})
/*
* ElkArte modified tags, modified so they support the existing paradigm
*
* Changes the way existing editor tags work
* Modifies code, quote, list, ul, ol, li
*/
.set('code', {
tags: {
code: null
},
isInline: false,
allowedChildren: ['#', '#newline'],
format: function (element, content)
{
let from = '';
if (element.children("cite:first").length === 1)
{
from = element.children("cite:first").text().trim();
element.attr({'from': from.php_htmlspecialchars()});
from = '=' + from;
element.children("cite:first").remove();
content = this.elementToBbcode(element);
}
else if (typeof element.attr('from') !== 'undefined')
{
from = '=' + element.attr('from').php_unhtmlspecialchars();
}
return '[code' + from + ']' + content.replace('[', '[') + '[/code]';
},
quoteType: function (element)
{
return element;
},
html: function (element, attrs, content)
{
let from = '';
if (typeof attrs.defaultattr !== 'undefined')
{
from = '' + $.sceditor.escapeEntities(attrs.defaultattr) + '';
}
return '' + from + content.replace('[', '[') + '
';
}
})
.set('quote', {
tags: {
blockquote: null,
cite: null
},
isInline: false,
format: function (element, content)
{
let author = '',
date = '',
link = '';
if (element[0].tagName.toLowerCase() === 'cite')
{
return '';
}
if (element.attr('author'))
{
author = ' author=' + element.attr('author').php_unhtmlspecialchars();
}
if (element.attr('date'))
{
date = ' date=' + element.attr('date');
}
if (element.attr('link'))
{
link = ' link=' + element.attr('link');
}
if (author === '' && date === '' && link !== '')
{
link = '=' + element.attr('link');
}
return '[quote' + author + link + date + ']' + content + '[/quote]';
},
html: function (element, attrs, content)
{
let attr_author = '',
sAuthor = '',
attr_date = '',
sDate = '',
attr_link = '',
sLink = '';
// Author tag in the quote ?
if (typeof attrs.author !== 'undefined')
{
attr_author = attrs.author;
sAuthor = bbc_quote_from + ': ' + $.sceditor.escapeEntities(attr_author);
}
// Done as [quote=someone]
else if (typeof attrs.defaultattr !== 'undefined')
{
// Convert it to an author tag
attr_link = $.sceditor.escapeEntities(attrs.defaultattr);
sLink = (attr_link.substr(0, 7) === 'http://' || attr_link.substr(0, 8) === 'https://')
? $.sceditor.escapeUriScheme(attr_link)
: elk_scripturl + '?' + attr_link;
sAuthor = '' + bbc_quote_from + ': ' + sLink + '';
}
// Links could be in the form: link=topic=71.msg201#msg201 that would fool javascript, so we need a workaround
for (let key in attrs)
{
if (key.substr(0, 4) === 'link' && attrs.hasOwnProperty(key))
{
attr_link = key.length > 4 ? key.substr(5) + '=' + attrs[key] : attrs[key];
attr_link = $.sceditor.escapeEntities(attr_link);
sLink = (attr_link.substr(0, 7) === 'http://' || attr_link.substr(0, 8) === 'https://')
? attr_link
: elk_scripturl + '?' + attr_link;
sAuthor = sAuthor === ''
? '' + bbc_quote_from + ': ' + sLink + ''
: '' + sAuthor + '';
}
}
// A date perhaps
if (typeof attrs.date !== 'undefined')
{
attr_date = attrs.date;
sDate = '' + new Date(attrs.date * 1000) + '';
}
// Build the blockquote up with the data
if (sAuthor === '' && sDate === '')
{
sAuthor = bbc_quote;
}
else
{
sAuthor += sDate !== '' ? ' ' + bbc_search_on : '';
}
content = '' + sAuthor + ' ' + sDate + '' + content + '
';
return content;
}
})
.set('img', {
tags: {
img: {
src: null
}
},
allowsEmpty: true,
quoteType: $.sceditor.BBCodeParser.QuoteType.never,
allowedChildren: ['#'],
format: function (element, content)
{
let attribs = '',
params = function (names)
{
names.forEach(function (name)
{
if (element.attr(name))
{
attribs += ' ' + name + '=' + element.attr(name);
}
else if (element[0].style[name])
{
attribs += ' ' + name + '=' + element[0].style[name];
}
});
};
// check if this is an emoticon image
if (typeof element.attr('data-sceditor-emoticon') !== 'undefined')
{
return content;
}
// check if this is an ILA ?
if (element.attr('data-ila'))
{
params(['width', 'height', 'align', 'type']);
return '[attach' + attribs + ']' + element.attr('data-ila') + '[/attach]';
}
// normal image then
params(['width', 'height', 'title', 'alt']);
return '[img' + attribs + ']' + element.attr('src') + '[/img]';
},
html: function (token, attrs, content)
{
let attribs = '',
params = function (names)
{
names.forEach(function (name)
{
if (typeof attrs[name] !== 'undefined')
{
attribs += ' ' + name + '="' + $.sceditor.escapeEntities(attrs[name]) + '"';
}
});
};
// handle [img alt=alt title=title width=123 height=123]url[/img]
params(['width', 'height', 'alt', 'title']);
return '';
}
})
.set('list', {
breakStart: true,
isInline: false,
skipLastLineBreak: true,
allowedChildren: ['#', '*', 'li'],
html: function (element, attrs, content)
{
let style = '',
code = 'ul';
if (attrs.type)
{
style = ' style="list-style-type: ' + attrs.type + '"';
}
return '<' + code + style + '>' + content.replace(/<\/li>
/g, '') + '' + code + '>';
}
})
.set('li', {
breakAfter: false,
isInline: false,
closedBy: ['/ul', '/ol', '/list', 'li', '*', '@', '+', 'x', '#', '0', 'o', 'O'],
html: '{0}',
format: function (element, content) {
let token = 'li',
itemCodes = ['*', '@', '+', 'x', '#', '0', 'o', 'O'];
if (element.attr('data-itemcode') && itemCodes.indexOf(element.attr('data-itemcode')) !== -1)
{
token = element.attr('data-itemcode');
}
return '[' + token + ']' + content + (token === 'li' ? '[/li]' : '');
},
})
.set('ul', {
tags: {
ul: null
},
breakStart: true,
format: function (element, content)
{
let type = $(element[0]).prop('style')['list-style-type'];
if (type === 'disc' || type === '')
{
return '[list]' + content + '[/list]';
}
return '[list type=' + type + ']' + content + '[/list]';
},
isInline: false,
skipLastLineBreak: true,
html: ''
})
.set('ol', {
tags: {
ol: null
},
breakStart: true,
isInline: false,
skipLastLineBreak: true,
format: '[list type=decimal]{0}[/list]',
html: '{0}
'
})
.set('url', {
allowsEmpty: true,
tags: {
a: {
href: null
}
},
quoteType: $.sceditor.BBCodeParser.QuoteType.never,
format: function (element, content)
{
let url = element.attr('href');
// return the type of link we are currently dealing with
if (url.substr(0, 7) === 'mailto:')
{
return '[email="' + url.substr(7) + '"]' + content + '[/email]';
}
if (typeof element.attr('data-mention') !== 'undefined')
{
return '[member=' + element.attr('data-mention') + ']' + content.replace('@', '') + '[/member]';
}
if (typeof element.attr('data-ila') !== 'undefined')
{
return '[attachurl]' + element.attr('data-ila') + '[/attachurl]';
}
return '[url=' + url + ']' + content + '[/url]';
},
html: function (token, attrs, content)
{
attrs.defaultattr = $.sceditor.escapeEntities(attrs.defaultattr, true) || content;
return '' + content + '';
}
});
// All the lovely item [*] codes done in a loop
itemCodes.forEach(function ( code)
{
code = code.split(":");
$.sceditor.plugins.bbcode.bbcode
.set(code[0], {
tags: {
li: {
'data-itemcode': [code[0]]
}
},
isInline: false,
closedBy: ['/ul', '/ol', '/list', 'li', '*', '@', '+', 'x', '#', '0', 'o', 'O'],
excludeClosing: true,
html: '{0}',
format: '[' + code[0] + ']{0}',
});
});