WIP Quill composer -- basic functionality working, toolbar lacking, mobile untested

This commit is contained in:
Julian Lam 2018-08-09 12:26:48 -04:00
commit 4796d4e346
17 changed files with 1034 additions and 0 deletions

22
.gitattributes vendored Normal file
View file

@ -0,0 +1,22 @@
# Auto detect text files and perform LF normalization
* text=auto

# Custom for Visual Studio
*.cs diff=csharp
*.sln merge=union
*.csproj merge=union
*.vbproj merge=union
*.fsproj merge=union
*.dbproj merge=union

# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

220
.gitignore vendored Normal file
View file

@ -0,0 +1,220 @@
#################
## Eclipse
#################

*.pydevproject
.project
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath

# External tool builders
.externalToolBuilders/

# Locally stored "Eclipse launch configurations"
*.launch

# CDT-specific
.cproject

# PDT-specific
.buildpath


#################
## Visual Studio
#################

## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.

# User-specific files
*.suo
*.user
*.sln.docstates

# Build results

[Dd]ebug/
[Rr]elease/
x64/
build/
[Bb]in/
[Oo]bj/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.log
*.scc

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile

# Visual Studio profiler
*.psess
*.vsp
*.vspx

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# NCrunch
*.ncrunch*
.*crunch*.local.xml

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.Publish.xml
*.pubxml

# NuGet Packages Directory
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
#packages/

# Windows Azure Build Output
csx
*.build.csdef

# Windows Store app package directory
AppPackages/

# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.[Pp]ublish.xml
*.pfx
*.publishsettings

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm

# SQL Server files
App_Data/*.mdf
App_Data/*.ldf

#############
## Windows detritus
#############

# Windows image file caches
Thumbs.db
ehthumbs.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Mac crap
.DS_Store


#############
## Python
#############

*.py[co]

# Packages
*.egg
*.egg-info
dist/
build/
eggs/
parts/
var/
sdist/
develop-eggs/
.installed.cfg

# Installer logs
pip-log.txt

# Unit test / coverage reports
.coverage
.tox

#Translations
*.mo

#Mr Developer
.mr.developer.cfg

sftp-config.json
node_modules/

.idea/

2
.npmignore Normal file
View file

@ -0,0 +1,2 @@
sftp-config.json
node_modules/

7
LICENSE Normal file
View file

@ -0,0 +1,7 @@
Copyright 2018 NodeBB Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

9
README.md Normal file
View file

@ -0,0 +1,9 @@
# Quill composer for NodeBB

This plugin activates the WYSIWYG Quill composer for NodeBB. Please ensure that:

* The markdown plugin is disabled
* Any other composers (i.e. nodebb-plugin-composer-default) are disabled

## Contributors Welcome
This plugin is in its early stages. If you'd like to look at the [documentation](https://quilljs.com/docs/) and add a feature, or take a look at the GitHub Issues and do something from there then please do. All pull requests lovingly reviewed.

15
lib/controllers.js Normal file
View file

@ -0,0 +1,15 @@
'use strict';

var Controllers = {};

Controllers.renderAdminPage = function (req, res, next) {
var redactor = module.parent.exports;

redactor.checkCompatibility(function(err, checks) {
res.render('admin/plugins/composer-quill', {
checks: checks
});
});
};

module.exports = Controllers;

107
library.js Normal file
View file

@ -0,0 +1,107 @@
"use strict";

var controllers = require('./lib/controllers');
var SocketPlugins = require.main.require('./src/socket.io/plugins');
var defaultComposer = module.parent.require('nodebb-plugin-composer-default');
var plugins = module.parent.exports;
var meta = module.parent.require('./meta');
var helpers = module.parent.require('./controllers/helpers');
var async = module.parent.require('async');
var winston = module.parent.require('winston');
var nconf = module.parent.require('nconf');

var QuillDeltaToHtmlConverter = require('quill-delta-to-html');

var plugin = {};

plugin.init = function(data, callback) {
var router = data.router;
var hostMiddleware = data.middleware;
var hostControllers = data.controllers;

router.get('/admin/plugins/composer-quill', hostMiddleware.admin.buildHeader, controllers.renderAdminPage);
router.get('/api/admin/plugins/composer-quill', controllers.renderAdminPage);

// Expose the default composer's socket method calls for this composer as well
plugin.checkCompatibility(function(err, checks) {
if (checks.composer) {
SocketPlugins.composer = defaultComposer.socketMethods;
} else {
winston.warn('[plugin/composer-quill] Another composer plugin is active! Please disable all other composers.');
}
});

callback();
};

plugin.checkCompatibility = function(callback) {
async.parallel({
active: async.apply(plugins.getActive),
markdown: async.apply(meta.settings.get, 'markdown')
}, function(err, data) {
callback(null, {
markdown: data.active.indexOf('nodebb-plugin-markdown') === -1, // plugin disabled
composer: data.active.filter(function(plugin) {
return plugin.startsWith('nodebb-plugin-composer-') && plugin !== 'nodebb-plugin-composer-quill';
}).length === 0
})
});
};

plugin.addAdminNavigation = function(header, callback) {
header.plugins.push({
route: '/plugins/composer-quill',
icon: 'fa-edit',
name: 'Quill (Composer)'
});

callback(null, header);
};

plugin.parsePost = function (data, callback) {
try {
var unescaped = data.postData.content.replace(/"/g, '"');
var content = JSON.parse(unescaped);
var converter = new QuillDeltaToHtmlConverter(content.ops, {});
data.postData.content = converter.convert();
} catch (e) {
// Post content not in expected format -- do nothing
winston.verbose('[composer-quill] pid ' + data.postData.pid + ' content not in expected format, skipping.');
}

callback(null, data);
};

plugin.parseRaw = function (raw, callback) {
// Empty if quill delta, otherwise pass-through
if (raw.startsWith('{"ops":[{')) {
raw = '';
}

callback(null, raw);
};

// plugin.build = function(data, callback) {
// // No plans for a standalone composer route, so handle redirection on f5
// var req = data.req;
// var res = data.res;

// if (req.query.p) {
// if (!res.locals.isAPI) {
// if (req.query.p.startsWith(nconf.get('relative_path'))) {
// req.query.p = req.query.p.replace(nconf.get('relative_path'), '');
// }

// return helpers.redirect(res, req.query.p);
// } else {
// return res.render('', {});
// }
// } else if (!req.query.pid && !req.query.tid && !req.query.cid) {
// return helpers.redirect(res, '/');
// }

// callback(null, data);
// }

module.exports = plugin;

33
package.json Normal file
View file

@ -0,0 +1,33 @@
{
"name": "nodebb-plugin-composer-quill",
"version": "0.0.0",
"description": "Quill Composer for NodeBB",
"main": "library.js",
"repository": {
"type": "git",
"url": "https://github.com/NodeBB/nodebb-plugin-composer-quill"
},
"keywords": [
"nodebb",
"plugin",
"composer",
"quill"
],
"author": {
"name": "NodeBB Team",
"email": "sales@nodebb.org"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/NodeBB/nodebb-plugin-composer-quill/issues"
},
"readmeFilename": "README.md",
"nbbpm": {
"compatibility": "^1.8.0"
},
"dependencies": {
"quill": "^1.3.6",
"quill-delta-to-html": "^0.9.1",
"sanitize-html": "^1.7.2"
}
}

37
plugin.json Normal file
View file

@ -0,0 +1,37 @@
{
"id": "nodebb-plugin-composer-quill",
"url": "https://github.com/NodeBB/nodebb-plugin-composer-quill",
"library": "library.js",
"hooks": [
{ "hook": "static:app.load", "method": "init" },
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
{ "hook": "filter:composer.build", "method": "build" },
{ "hook": "filter:parse.post", "method": "parsePost" },
{ "hook": "filter:parse.raw", "method": "parseRaw" }
],
"css": [
"./node_modules/quill/dist/quill.snow.css"
],
"less": [
"../nodebb-plugin-composer-default/static/less/composer.less",
"./static/less/composer.less"
],
"modules": {
"quill.js": "./node_modules/quill/dist/quill.js"
},
"scripts": [
"./static/lib/quill-nbb.js",
"./static/lib/client.js",
"../nodebb-plugin-composer-default/static/lib/composer.js",
"../nodebb-plugin-composer-default/static/lib/composer/autocomplete.js",
"../nodebb-plugin-composer-default/static/lib/composer/categoryList.js",
"../nodebb-plugin-composer-default/static/lib/composer/controls.js",
"../nodebb-plugin-composer-default/static/lib/composer/drafts.js",
"../nodebb-plugin-composer-default/static/lib/composer/formatting.js",
"../nodebb-plugin-composer-default/static/lib/composer/preview.js",
"../nodebb-plugin-composer-default/static/lib/composer/resize.js",
"../nodebb-plugin-composer-default/static/lib/composer/tags.js",
"../nodebb-plugin-composer-default/static/lib/composer/uploads.js"
],
"templates": "static/templates"
}

BIN
screenshots/desktop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
screenshots/mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

32
static/less/composer.less Normal file
View file

@ -0,0 +1,32 @@
.composer {
.write-container {
display: block;
margin-bottom: 1.5rem;
// overflow: hidden;

textarea {
display: none;
}

.ql-container {
height: ~"calc(100% - 42px)";
}

.ql-toolbar {
.ql-foobar::after {
content: 'A';
}
}
}

// .title-container .title {
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
// }
}

/* Safari bug.. */
.composer {
user-select: initial;
}

39
static/lib/client.js Normal file
View file

@ -0,0 +1,39 @@
$(document).ready(function() {
var wrapWithBlockquote = function (delta) {
// Validate the delta
try {
var parsed = JSON.parse(delta);
parsed.ops = parsed.ops.map(function (op) {
op.attributes = Object.assign({ blockquote: true }, op.attributes || {});
return op;
});
return JSON.stringify(parsed);
} catch (e) {
// Do nothing
return delta;
}
}
$(window).on('action:app.load', function() {
require(['composer', 'quill-nbb'], function(composer, quillNbb) {
$(window).on('action:composer.topic.new', function(ev, data) {
composer.newTopic({
cid: data.cid,
title: data.title,
body: data.body
});
});

$(window).on('action:composer.post.edit', function(ev, data) {
composer.editPost(data.pid);
});

$(window).on('action:composer.post.new', function(ev, data) {
composer.newReply(data.tid, data.pid, data.topicName, data.text);
});

$(window).on('action:composer.addQuote', function(ev, data) {
composer.newReply(data.tid, data.pid, data.topicName, wrapWithBlockquote(data.text));
});
});
});
});

150
static/lib/quill-nbb.js Normal file
View file

@ -0,0 +1,150 @@
'use strict';

/* globals define, socket, app, config, ajaxify, utils, templates, bootbox */

define('quill-nbb', [
'quill',
'composer',
'translator',
'composer/autocomplete',
'composer/resize',
'composer/formatting',
'scrollStop'
], function (Quill, composer, translator, autocomplete, resize, formatting, scrollStop) {
function init (targetEl, data) {
var textDirection = $('html').attr('data-dir');
var textareaEl = targetEl.siblings('textarea');
var toolbarOptions = {
container: [
[{ 'header': [1, 2, 3, 4, 5, 6, false] }], // h1..h6
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript
[{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
[{ 'align': [] }],
['clean']
],
handlers: {},
};

// Configure toolbar
var toolbarHandlers = formatting.getDispatchTable();
var group = [];
console.log(toolbarHandlers, data.formatting);
data.formatting.forEach(function (option) {
group.push(option.name);
toolbarOptions.handlers[option.name] = toolbarHandlers[option.name];
console.log('added', option.name);
});
toolbarOptions.container.push(group);
console.log(toolbarOptions);

var quill = new Quill(targetEl.get(0), {
theme: 'snow',
modules: {
toolbar: toolbarOptions,
}
});

data.formatting.forEach(function (option) {
var targetEl = $('.ql-' + option.name);
targetEl.html('<i class="' + option.className + '"></i>');
if (option.mobile) {
targetEl.addClass('visible-xs');
}
});

// Automatic RTL support
quill.format('direction', textDirection);
quill.format('align', textDirection === 'rtl' ? 'right' : 'left');

$(window).trigger('action:quill.load', quill);
$(window).off('action:quill.load');

// Restore text if contained in composerData
if (data.composerData.body) {
try {
var unescaped = data.composerData.body.replace(/&quot;/g, '"');
quill.setContents(JSON.parse(unescaped), 'api');
} catch (e) {
quill.setContents({"ops":[{"insert": data.composerData.body.toString()}]}, 'api');
}
}

// Update textarea on text-change event. This allows compatibility with
// how NodeBB handles things like drafts, etc.
quill.on('text-change', function () {
textareaEl.val(JSON.stringify(quill.getContents()));
});

// var options = {
// direction: textDirection || undefined,
// imageUploadFields: {
// '_csrf': config.csrf_token
// },
// fileUploadFields: {
// '_csrf': config.csrf_token
// },
// };
};

$(window).on('action:composer.loaded', function (ev, data) {
var postContainer = $('.composer[data-uuid="' + data.post_uuid + '"]')
var targetEl = postContainer.find('.write-container div');

init(targetEl, data);

var cidEl = postContainer.find('.category-list');
if (cidEl.length) {
cidEl.attr('id', 'cmp-cid-' + data.post_uuid);
} else {
postContainer.append('<input id="cmp-cid-' + data.post_uuid + '" type="hidden" value="' + ajaxify.data.cid + '"/>');
}

// if (config.allowTopicsThumbnail && data.composerData.isMain) {
// var thumbToggleBtnEl = postContainer.find('.re-topic_thumb');
// var url = data.composerData.topic_thumb || '';

// postContainer.find('input#topic-thumb-url').val(url);
// postContainer.find('img.topic-thumb-preview').attr('src', url);

// if (url) {
// postContainer.find('.topic-thumb-clear-btn').removeClass('hide');
// }
// thumbToggleBtnEl.addClass('show');
// thumbToggleBtnEl.off('click').on('click', function() {
// var container = postContainer.find('.topic-thumb-container');
// container.toggleClass('hide', !container.hasClass('hide'));
// });
// }

scrollStop.apply(targetEl);
autocomplete.init(postContainer);
resize.reposition(postContainer);
});

$(window).on('composer:toolbar.addButton', function (ev, data) {
console.log(data);
});

// $(window).on('action:chat.loaded', function (e, containerEl) {
// var composerEl = $(containerEl).find('[component="chat/composer"]');
// var inputEl = composerEl.find('textarea.chat-input');

// redactorify(inputEl, {
// height: 120,
// onChange: function () {
// var element = $('[component="chat/messages"]').find('[component="chat/message/remaining"]')
// var curLength = this.code.get().length;
// element.text(config.maximumChatMessageLength - curLength);
// }
// });
// });

// $(window).on('action:chat.sent', function (e, data) {
// // Empty chat input
// var redactor = $('.chat-modal[data-roomid="' + data.roomId + '"] .chat-input, .expanded-chat[data-roomid="' + data.roomId + '"] .chat-input').redactor('core.object');
// redactor.code.set('');
// });
});

View file

@ -0,0 +1,58 @@
<div class="row">
<div class="col-lg-9">
<div class="panel panel-default">
<div class="panel-heading">Redactor Composer</div>
<div class="panel-body">
<p>
<a href="http://imperavi.com/redactor/">Redactor</a> is a WYSIWYG composer allowing users to create posts via a rich-text interface.
Unlike the composer shipping by default with NodeBB, no knowledge of Markdown is required by the end user.
</p>
<p>
<strong>Quick Links</strong>
<ul>
<li><a href="http://imperavi.com/redactor/">Redactor Home Page</a></li>
<li><a href="http://imperavi.com/redactor/docs/">Documentation</a></li>
</ul>
</p>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">Compatibility Checks</div>
<div class="panel-body">
<ul class="list-group">
<li class="list-group-item list-group-item-<!-- IF checks.markdown -->success<!-- ELSE -->danger<!-- ENDIF checks.markdown -->">
<strong>Markdown Compatibility</strong>
<!-- IF checks.markdown -->
<span class="badge"><i class="fa fa-check"></i></span>
<p>The Markdown plugin is either disabled, or HTML sanitization is disabled</p>
<!-- ELSE -->
<span class="badge"><i class="fa fa-times"></i></span>
<p>
In order to render post content correctly, the Markdown plugin needs to have HTML sanitization disabled,
or the entire plugin should be disabled altogether.
</p>
<!-- ENDIF checks.markdown -->
</li>
<li class="list-group-item list-group-item-<!-- IF checks.composer -->success<!-- ELSE -->danger<!-- ENDIF checks.composer -->">
<strong>Composer Conflicts</strong>
<!-- IF checks.composer -->
<span class="badge"><i class="fa fa-check"></i></span>
<p>Great! Looks like Redactor is the only composer active</p>
<!-- ELSE -->
<span class="badge"><i class="fa fa-times"></i></span>
<p>Redactor must be the only composer active. Please disable other composers and reload NodeBB.</p>
<!-- ENDIF checks.composer -->
</li>
</ul>
</div>
</div>
</div>
<!--<div class="col-lg-3">
<div class="panel panel-default">
<div class="panel-heading">Control Panel</div>
<div class="panel-body">
<button class="btn btn-primary" id="save">Save Settings</button>
</div>
</div>
</div>-->
</div>

View file

@ -0,0 +1,152 @@
<div component="composer" class="composer<!-- IF resizable --> resizable<!-- ENDIF resizable --><!-- IF !isTopicOrMain --> reply<!-- ENDIF !isTopicOrMain -->">

<div class="composer-container">
<nav class="navbar navbar-fixed-top mobile-navbar visible-xs visible-sm">
<div class="pull-left">
<button class="btn btn-sm btn-primary composer-discard" data-action="discard" tabindex="-1"><i class="fa fa-times"></i></button>
</div>
<!-- IF isTopic -->
<div class="category-name-container">
<span class="category-name"></span> <i class="fa fa-sort"></i>
</div>
<!-- ENDIF isTopic -->
<div class="pull-right">
<button class="btn btn-sm btn-primary composer-submit" data-action="post" tabindex="-1"><i class="fa fa-chevron-right"></i></button>
</div>
<!-- IF !isTopicOrMain -->
<h4 class="title">[[topic:composer.replying_to, "{title}"]]</h4>
<!-- ENDIF !isTopicOrMain -->
</nav>
<div class="row title-container">
<!-- IF showHandleInput -->
<div class="col-sm-3 col-md-12">
<input class="handle form-control" type="text" tabindex="1" placeholder="[[topic:composer.handle_placeholder]]" value="{handle}" />
</div>
<div class="<!-- IF isTopic -->col-lg-9<!-- ELSE -->col-lg-12<!-- ENDIF isTopic --> col-md-12">
<!-- IF isTopicOrMain -->
<input class="title form-control" type="text" tabindex="1" placeholder="[[topic:composer.title_placeholder]]" value="{title}"/>
<!-- ELSE -->
<span class="title form-control">[[topic:composer.replying_to, "{title}"]]</span>
<!-- ENDIF isTopicOrMain -->
</div>
<!-- ELSE -->
<div class="<!-- IF isTopic -->col-lg-9<!-- ELSE -->col-lg-12<!-- ENDIF isTopic --> col-md-12">
<!-- IF isTopicOrMain -->
<input class="title form-control" type="text" tabindex="1" placeholder="[[topic:composer.title_placeholder]]" value="{title}"/>
<!-- ELSE -->
<span class="title form-control">[[topic:composer.replying_to, "{title}"]]</span>
<!-- ENDIF isTopicOrMain -->
</div>
<!-- ENDIF showHandleInput -->
<!-- IF isTopic -->
<div class="category-list-container col-lg-3 col-md-12 hidden-sm hidden-xs">

</div>
<!-- ENDIF isTopic -->
</div>

<div class="category-tag-row">
<div class="btn-toolbar formatting-bar">
<ul class="formatting-group">
<!-- BEGIN formatting -->
<!-- IF formatting.spacer -->
<li class="spacer"></li>
<!-- ELSE -->
<!-- IF !formatting.mobile -->
<li tabindex="-1" data-format="{formatting.name}" title="{formatting.title}"><i class="{formatting.className}"></i></li>
<!-- ENDIF !formatting.mobile -->
<!-- ENDIF formatting.spacer -->
<!-- END formatting -->

<!--[if gte IE 9]><!-->
<!-- IF privileges.upload:post:image -->
<li class="img-upload-btn hide" data-format="picture" tabindex="-1" title="[[modules:composer.upload-picture]]">
<i class="fa fa-cloud-upload"></i>
</li>
<!-- ENDIF privileges.upload:post:image -->
<!-- IF privileges.upload:post:file -->
<li class="file-upload-btn hide" data-format="upload" tabindex="-1" title="[[modules:composer.upload-file]]">
<i class="fa fa-upload"></i>
</li>
<!-- ENDIF privileges.upload:post:file -->
<!--<![endif]-->

<!-- IF allowTopicsThumbnail -->
<li tabindex="-1">
<i class="fa fa-th-large topic-thumb-btn topic-thumb-toggle-btn hide" title="[[topic:composer.thumb_title]]"></i>
</li>
<div class="topic-thumb-container center-block hide">
<form id="thumbForm" method="post" class="topic-thumb-form form-inline" enctype="multipart/form-data">
<img class="topic-thumb-preview"></img>
<div class="form-group">
<label for="topic-thumb-url">[[topic:composer.thumb_url_label]]</label>
<input type="text" id="topic-thumb-url" class="form-control" placeholder="[[topic:composer.thumb_url_placeholder]]" />
</div>
<div class="form-group">
<label for="topic-thumb-file">[[topic:composer.thumb_file_label]]</label>
<input type="file" id="topic-thumb-file" class="form-control" />
</div>
<div class="form-group topic-thumb-ctrl">
<i class="fa fa-spinner fa-spin hide topic-thumb-spinner" title="[[topic:composer.uploading]]"></i>
<i class="fa fa-times topic-thumb-btn hide topic-thumb-clear-btn" title="[[topic:composer.thumb_remove]]"></i>
</div>
</form>
</div>
<!-- ENDIF allowTopicsThumbnail -->

<form id="fileForm" method="post" enctype="multipart/form-data">
<!--[if gte IE 9]><!-->
<input type="file" id="files" name="files[]" multiple class="gte-ie9 hide"/>
<!--<![endif]-->
<!--[if lt IE 9]>
<input type="file" id="files" name="files[]" class="lt-ie9 hide" value="Upload"/>
<![endif]-->
</form>
</ul>

<div class="btn-group pull-right action-bar hidden-sm hidden-xs">
<button class="btn btn-default composer-discard" data-action="discard" tabindex="-1"><i class="fa fa-times"></i> [[topic:composer.discard]]</button>

<button class="btn btn-primary composer-submit" data-action="post" tabindex="6"><i class="fa fa-check"></i> [[topic:composer.submit]]</button>
</div>
</div>
</div>

<div class="row write-preview-container">
<div class="write-container">
<div></div>
<textarea></textarea>
</div>
</div>

<!-- IF isTopicOrMain -->
<div class="tag-row">
<div class="tags-container">
<div class="btn-group dropup <!-- IF !tagWhitelist.length -->hidden<!-- ENDIF !tagWhitelist.length -->" component="composer/tag/dropdown">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">
<span class="visible-sm-inline visible-md-inline visible-lg-inline"><i class="fa fa-tags"></i></span>
<span class="caret"></span>
</button>

<ul class="dropdown-menu">
<!-- BEGIN tagWhitelist -->
<li data-tag="@value"><a href="#">@value</a></li>
<!-- END tagWhitelist -->
</ul>
</div>
<input class="tags" type="text" class="form-control" placeholder="[[tags:enter_tags_here, {minimumTagLength}, {maximumTagLength}]]" tabindex="5"/>
</div>
</div>
<!-- ENDIF isTopicOrMain -->

<!-- IF isTopic -->
<ul class="category-selector visible-xs visible-sm">

</ul>
<!-- ENDIF isTopic -->

<div class="imagedrop"><div>[[topic:composer.drag_and_drop_images]]</div></div>

<div class="resizer"><div class="trigger text-center"><i class="fa"></i></div></div>
</div>
</div>

151
yarn.lock Normal file
View file

@ -0,0 +1,151 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


clone@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"

core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"

deep-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"

dom-serializer@0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
dependencies:
domelementtype "~1.1.1"
entities "~1.1.1"

domelementtype@1, domelementtype@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"

domelementtype@~1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"

domhandler@^2.3.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259"
dependencies:
domelementtype "1"

domutils@^1.5.1:
version "1.6.2"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff"
dependencies:
dom-serializer "0"
domelementtype "1"

entities@^1.1.1, entities@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"

eventemitter3@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"

extend@^3.0.1, extend@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"

fast-diff@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154"

htmlparser2@^3.9.0:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
dependencies:
domelementtype "^1.3.0"
domhandler "^2.3.0"
domutils "^1.5.1"
entities "^1.1.1"
inherits "^2.0.1"
readable-stream "^2.0.2"

inherits@^2.0.1, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"

isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"

parchment@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/parchment/-/parchment-1.1.4.tgz#aeded7ab938fe921d4c34bc339ce1168bc2ffde5"

process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"

quill-delta-to-html@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/quill-delta-to-html/-/quill-delta-to-html-0.9.1.tgz#754c2aac78da23a171693a604dcdf14b61ca72a2"

quill-delta@^3.6.2:
version "3.6.3"
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032"
dependencies:
deep-equal "^1.0.1"
extend "^3.0.2"
fast-diff "1.1.2"

quill@^1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.6.tgz#99f4de1fee85925a0d7d4163b6d8328f23317a4d"
dependencies:
clone "^2.1.1"
deep-equal "^1.0.1"
eventemitter3 "^2.0.3"
extend "^3.0.1"
parchment "^1.1.4"
quill-delta "^3.6.2"

readable-stream@^2.0.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
safe-buffer "~5.1.1"
string_decoder "~1.0.3"
util-deprecate "~1.0.1"

regexp-quote@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/regexp-quote/-/regexp-quote-0.0.0.tgz#1e0f4650c862dcbfed54fd42b148e9bb1721fcf2"

safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"

sanitize-html@^1.7.2:
version "1.14.1"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.14.1.tgz#730ffa2249bdf18333effe45b286173c9c5ad0b8"
dependencies:
htmlparser2 "^3.9.0"
regexp-quote "0.0.0"
xtend "^4.0.0"

string_decoder@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
dependencies:
safe-buffer "~5.1.0"

util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"

xtend@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"