diff --git a/bower.json b/bower.json
index 38edcca56..d4ba767ab 100644
--- a/bower.json
+++ b/bower.json
@@ -27,15 +27,22 @@
"js-data": "~2.8.2",
"js-data-angular": "~3.1.0",
"ng-file-upload": "~11.2.3",
- "ckeditor": "4.5.6",
- "angular-ckeditor": "~1.0.3",
+ "angular-ui-tinymce": "~0.0.13",
"angular-pdf": "~1.3.0",
"roboto-condensed": "~0.3.0",
- "open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1"
+ "open-sans-fontface": "https://github.com/OpenSlides/open-sans.git#1.4.2.post1",
+ "tinymce-i18n": "OpenSlides/tinymce-i18n"
},
"overrides": {
"pdfjs-dist": {
"main": "build/pdf.combined.js"
+ },
+ "tinymce-dist": {
+ "main": [
+ "tinymce.js",
+ "themes/modern/theme.js",
+ "plugins/*/plugin.js"
+ ]
}
},
"resolutions": {
diff --git a/gulpfile.js b/gulpfile.js
index 53740d574..61f13018b 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -25,6 +25,7 @@ var argv = require('yargs').argv,
jshint = require('gulp-jshint'),
mainBowerFiles = require('main-bower-files'),
path = require('path'),
+ rename = require('gulp-rename'),
through = require('through2'),
uglify = require('gulp-uglify'),
vsprintf = require('sprintf-js').vsprintf;
@@ -67,10 +68,27 @@ gulp.task('fonts-libs', function() {
.pipe(gulp.dest(path.join(output_directory, 'fonts')));
});
-// Extra task only for CKEditor
-gulp.task('ckeditor', function () {
- return gulp.src(path.join('bower_components', 'ckeditor', '**'))
- .pipe(gulp.dest(path.join(output_directory, 'ckeditor')));
+// Catches all skins files for TinyMCE editor.
+gulp.task('tinymce-skins', function () {
+ return gulp.src(path.join('bower_components', 'tinymce-dist', 'skins', '**'))
+ .pipe(gulp.dest(path.join(output_directory, 'tinymce', 'skins')));
+});
+
+// Catches all required i18n files for TinyMCE editor.
+gulp.task('tinymce-i18n', function () {
+ return gulp.src([
+ 'bower_components/tinymce-i18n/langs/cs.js',
+ 'bower_components/tinymce-i18n/langs/de.js',
+ 'bower_components/tinymce-i18n/langs/es.js',
+ 'bower_components/tinymce-i18n/langs/fr_FR.js',
+ 'bower_components/tinymce-i18n/langs/pt_PT.js',
+ ])
+ .pipe(rename(function (path) {
+ if (path.basename === 'pt_PT') {path.basename = 'pt'}
+ if (path.basename === 'fr_FR') {path.basename = 'fr'}
+ }))
+ .pipe(gulpif(argv.production, uglify()))
+ .pipe(gulp.dest(path.join(output_directory, 'tinymce', 'i18n')));
});
// Compiles translation files (*.po) to *.json and saves them in the directory
@@ -84,7 +102,7 @@ gulp.task('translations', function () {
});
// Gulp default task. Runs all other tasks before.
-gulp.task('default', ['js-libs', 'css-libs', 'fonts-libs', 'ckeditor', 'translations'], function () {});
+gulp.task('default', ['js-libs', 'css-libs', 'fonts-libs', 'tinymce-skins', 'tinymce-i18n', 'translations'], function () {});
/**
diff --git a/openslides/agenda/static/js/agenda/site.js b/openslides/agenda/static/js/agenda/site.js
index f647c3c80..83b722f55 100644
--- a/openslides/agenda/static/js/agenda/site.js
+++ b/openslides/agenda/static/js/agenda/site.js
@@ -75,9 +75,10 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
'operator',
'ngDialog',
'Agenda',
+ 'CustomslideForm',
'AgendaTree',
'Projector',
- function($scope, $http, $state, DS, operator, ngDialog, Agenda, AgendaTree, Projector) {
+ function($scope, $http, $state, DS, operator, ngDialog, Agenda, CustomslideForm, AgendaTree, Projector) {
// Bind agenda tree to the scope
$scope.$watch(function () {
return Agenda.lastModified();
@@ -110,11 +111,7 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
};
// open new dialog
$scope.newDialog = function () {
- ngDialog.open({
- template: 'static/templates/core/customslide-form.html',
- controller: 'CustomslideCreateCtrl',
- className: 'ngdialog-theme-default wide-form'
- });
+ ngDialog.open(CustomslideForm.getDialog());
};
// open edit dialog
$scope.editDialog = function (item) {
diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js
index 657e4b1c2..1d300ff2d 100644
--- a/openslides/core/static/js/core/base.js
+++ b/openslides/core/static/js/core/base.js
@@ -424,34 +424,15 @@ angular.module('OpenSlidesApp.core', [
}
])
-/* Options for CKEditor used in various create and edit views. */
-.value('CKEditorOptions', {
- allowedContent:
- 'h1 h2 h3 p pre b i u strike strong em blockquote;' +
- 'a[!href];' +
- 'img[!src,alt]{width,height,float};' +
- 'table tr th td caption;' +
- 'li ol ul{list-style};' +
- 'span{color,background-color};',
- extraPlugins: 'colorbutton',
- toolbarGroups: [
- { name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
- { name: 'editing', groups: [ 'find', 'selection', 'spellchecker', 'editing' ] },
- { name: 'forms', groups: [ 'forms' ] },
- { name: 'tools', groups: [ 'tools' ] },
- { name: 'about', groups: [ 'about' ] },
- { name: 'document', groups: [ 'mode', 'document', 'doctools' ] },
- { name: 'others', groups: [ 'others' ] },
- '/',
- { name: 'styles', groups: [ 'styles' ] },
- { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
- { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi', 'paragraph' ] },
- { name: 'links', groups: [ 'links' ] },
- { name: 'insert', groups: [ 'insert' ] },
- { name: 'colors', groups: [ 'colors' ] }
- ],
- removeButtons: 'Anchor,SpecialChar,Subscript,Superscript,Styles,RemoveFormat,HorizontalRule'
-})
+// mark HTML as "trusted"
+.filter('trusted', [
+ '$sce',
+ function ($sce) {
+ return function(text) {
+ return $sce.trustAsHtml(text);
+ };
+ }
+])
// Make sure that the DS factories are loaded by making them a dependency
.run([
diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js
index ae2f41375..a3d045bc9 100644
--- a/openslides/core/static/js/core/site.js
+++ b/openslides/core/static/js/core/site.js
@@ -14,8 +14,8 @@ angular.module('OpenSlidesApp.core.site', [
'ngMessages',
'ngCsvImport',
'ui.select',
+ 'ui.tinymce',
'luegg.directives',
- 'ckeditor',
])
// Provider to register entries for the main menu.
@@ -337,10 +337,16 @@ angular.module('OpenSlidesApp.core.site', [
.run([
'formlyConfig',
function (formlyConfig) {
- // NOTE: This next line is highly recommended. Otherwise Chrome's autocomplete will appear over your options!
+ // NOTE: This next line is highly recommended. Otherwise Chrome's autocomplete
+ // will appear over your options!
formlyConfig.extras.removeChromeAutoComplete = true;
// Configure custom types
+ formlyConfig.setType({
+ name: 'editor',
+ extends: 'textarea',
+ templateUrl: 'static/templates/core/editor.html',
+ });
formlyConfig.setType({
name: 'ui-select-single',
extends: 'select',
@@ -354,6 +360,35 @@ angular.module('OpenSlidesApp.core.site', [
}
])
+// Options for TinyMCE editor used in various create and edit views.
+.factory('Editor', [
+ 'gettextCatalog',
+ function (gettextCatalog) {
+ return {
+ getOptions: function (images) {
+ return {
+ language_url: '/static/tinymce/i18n/' + gettextCatalog.getCurrentLanguage() + '.js',
+ theme_url: '/static/js/openslides-libs.js',
+ skin_url: '/static/tinymce/skins/lightgray/',
+ inline: false,
+ statusbar: false,
+ browser_spellcheck: true,
+ image_advtab: true,
+ image_list: images,
+ plugins: [
+ 'lists link autolink charmap preview searchreplace code fullscreen',
+ 'paste textcolor colorpicker image imagetools'
+ ],
+ menubar: '',
+ toolbar: 'undo redo searchreplace | styleselect | bold italic underline strikethrough ' +
+ 'forecolor backcolor removeformat | bullist numlist | outdent indent | ' +
+ 'link image charmap table | code preview fullscreen'
+ };
+ }
+ }
+ }
+])
+
// html-tag os-form-field to generate generic from fields
// TODO: make it possible to use other fields then config fields
.directive('osFormField', [
@@ -504,17 +539,19 @@ angular.module('OpenSlidesApp.core.site', [
// Provide generic customslide form fields for create and update view
.factory('CustomslideForm', [
'gettextCatalog',
- 'CKEditorOptions',
+ 'Editor',
'Mediafile',
- function (gettextCatalog, CKEditorOptions, Mediafile) {
+ function (gettextCatalog, Editor, Mediafile) {
return {
// ngDialog for customslide form
getDialog: function (customslide) {
+ var resolve = {};
if (customslide) {
- var resolve = {
+ resolve = {
customslide: function(Customslide) {return Customslide.find(customslide.id);}
};
}
+ resolve.mediafiles = function(Mediafile) {return Mediafile.findAll();}
return {
template: 'static/templates/core/customslide-form.html',
controller: (customslide) ? 'CustomslideUpdateCtrl' : 'CustomslideCreateCtrl',
@@ -525,6 +562,7 @@ angular.module('OpenSlidesApp.core.site', [
}
},
getFormFields: function () {
+ var images = Mediafile.getAllImages();
return [
{
key: 'title',
@@ -536,11 +574,13 @@ angular.module('OpenSlidesApp.core.site', [
},
{
key: 'text',
- type: 'textarea',
+ type: 'editor',
templateOptions: {
label: gettextCatalog.getString('Text')
},
- ngModelElAttrs: {'ckeditor': 'CKEditorOptions'}
+ data: {
+ tinymceOption: Editor.getOptions(images)
+ }
},
{
key: 'attachments_id',
diff --git a/openslides/core/static/templates/core/customslide-detail.html b/openslides/core/static/templates/core/customslide-detail.html
index 4f5ef9dfd..5beb233c9 100644
--- a/openslides/core/static/templates/core/customslide-detail.html
+++ b/openslides/core/static/templates/core/customslide-detail.html
@@ -30,7 +30,7 @@
-
+
Attachments
-
diff --git a/openslides/core/static/templates/core/editor.html b/openslides/core/static/templates/core/editor.html
new file mode 100644
index 000000000..b634bd64e
--- /dev/null
+++ b/openslides/core/static/templates/core/editor.html
@@ -0,0 +1,2 @@
+
+
diff --git a/openslides/core/static/templates/core/slide_customslide.html b/openslides/core/static/templates/core/slide_customslide.html
index 693ecddfc..1be7c0fa8 100644
--- a/openslides/core/static/templates/core/slide_customslide.html
+++ b/openslides/core/static/templates/core/slide_customslide.html
@@ -1,4 +1,4 @@
{{ customslide.agenda_item.getTitle() }}
-
+
diff --git a/openslides/core/static/templates/index.html b/openslides/core/static/templates/index.html
index 3c3d52816..46c5ddff8 100644
--- a/openslides/core/static/templates/index.html
+++ b/openslides/core/static/templates/index.html
@@ -10,7 +10,6 @@
-
diff --git a/openslides/mediafiles/static/js/mediafiles/base.js b/openslides/mediafiles/static/js/mediafiles/base.js
index a7b6a7851..cd0e266cd 100644
--- a/openslides/mediafiles/static/js/mediafiles/base.js
+++ b/openslides/mediafiles/static/js/mediafiles/base.js
@@ -12,6 +12,15 @@ angular.module('OpenSlidesApp.mediafiles', [])
return DS.defineResource({
name: name,
useClass: jsDataModel,
+ getAllImages: function () {
+ var images = []
+ angular.forEach(this.getAll(), function(file) {
+ if (file.is_image) {
+ images.push({title: file.title, value: file.mediafileUrl});
+ }
+ });
+ return images;
+ },
methods: {
getResourceName: function () {
return name;
@@ -30,6 +39,10 @@ angular.module('OpenSlidesApp.mediafiles', [])
var PRESENTABLE_FILE_TYPES = ['application/pdf'];
return _.contains(PRESENTABLE_FILE_TYPES, filetype);
}],
+ is_image: ['filetype', function (filetype) {
+ var IMAGE_FILE_TYPES = ['image/png', 'image/jpeg', 'image/gif'];
+ return _.contains(IMAGE_FILE_TYPES, filetype);
+ }],
mediafileUrl: [function () {
return this.media_url_prefix + this.mediafile.name;
}],
diff --git a/openslides/motions/pdf.py b/openslides/motions/pdf.py
index 173f81621..5626ff195 100644
--- a/openslides/motions/pdf.py
+++ b/openslides/motions/pdf.py
@@ -1,4 +1,5 @@
import random
+import re
from html import escape
from operator import attrgetter
@@ -153,6 +154,23 @@ def motion_to_pdf(pdf, motion):
def convert_html_to_reportlab(pdf, text):
# parsing and replacing not supported html tags for reportlab...
soup = BeautifulSoup(text, "html5lib")
+
+ # number ol list elements
+ ols = soup.find_all('ol')
+ for ol in ols:
+ counter = 0
+ for li in ol.children:
+ if li.name == 'li':
+ # if start attribute is available set counter for first list element
+ if li.parent.get('start') and not li.find_previous_sibling():
+ counter = int(ol.get('start'))
+ else:
+ counter += 1
+ if li.get('value'):
+ counter = li.get('value')
+ else:
+ li['value'] = counter
+
# read all list elements...
for element in soup.find_all('li'):
# ... and replace ul list elements with
•...
@@ -167,22 +185,32 @@ def convert_html_to_reportlab(pdf, text):
bullet_tag = soup.new_tag("bullet")
bullet_tag.string = u"•"
element.insert(0, bullet_tag)
- # ... and replace ol list elements with ....
+ # ... and replace ol list elements with ....
if element.parent.name == "ol":
+ counter = None
# set list id if element is the first of numbered list
if not element.find_previous_sibling():
id = random.randrange(0, 101)
+ if element.parent.get('start'):
+ counter = element.parent.get('start')
+ if element.get('value'):
+ counter = element.get('value')
# nested lists
if element.ul or element.ol:
- for i in element.find_all('li'):
- element.insert_before(i)
- element.clear()
- else:
- element.name = "para"
- element.insert(0, soup.new_tag("bullet"))
- element.bullet.insert(0, soup.new_tag("seq"))
- element.bullet.seq['id'] = id
- element.bullet.insert(1, ".")
+ nested_list = element.find_all('li')
+ for i in reversed(nested_list):
+ element.insert_after(i)
+
+ element.attrs = {}
+ element.name = "para"
+ element.insert(0, soup.new_tag("bullet"))
+ element.bullet.insert(0, soup.new_tag("seq"))
+ element.bullet.seq['id'] = id
+ if counter:
+ element.bullet.insert(0, soup.new_tag("seqreset"))
+ element.bullet.seqreset['id'] = id
+ element.bullet.seqreset['base'] = int(counter) - 1
+ element.bullet.insert(2, ".")
# remove tags which are not supported by reportlab (replace tags with their children tags)
for tag in soup.find_all('ul'):
tag.unwrap()
@@ -190,8 +218,60 @@ def convert_html_to_reportlab(pdf, text):
tag.unwrap()
for tag in soup.find_all('li'):
tag.unwrap()
+
+ # use tags which are supported by reportlab
+ # replace to
+ for tag in soup.find_all('s'):
+ tag.name = "strike"
+
+ # replace to
+ for tag in soup.find_all('del'):
+ tag.name = "strike"
+
+ for tag in soup.find_all('a'):
+ # remove a tags without href attribute
+ if not tag.get('href'):
+ tag.extract()
+ for tag in soup.find_all('img'):
+ # remove img tags without src attribute
+ if not tag.get('src'):
+ tag.extract()
+
+ # replace style attributes in tags
for tag in soup.find_all('span'):
- tag.unwrap()
+ if tag.get('style'):
+ # replace style attribute "text-decoration: line-through;" to tag
+ if 'text-decoration: line-through' in str(tag['style']):
+ strike_tag = soup.new_tag("strike")
+ strike_tag.string = tag.string
+ tag.replace_with(strike_tag)
+ # replace style attribute "text-decoration: underline;" to tag
+ elif 'text-decoration: underline' in str(tag['style']):
+ u_tag = soup.new_tag("u")
+ u_tag.string = tag.string
+ tag.replace_with(u_tag)
+ # replace style attribute "color: #xxxxxx;" to "..."
+ elif 'background-color: ' in str(tag['style']):
+ font_tag = soup.new_tag("font")
+ color = re.findall('background-color: (.*?);', str(tag['style']))
+ if color:
+ font_tag['backcolor'] = color
+ if tag.string:
+ font_tag.string = tag.string
+ tag.replace_with(font_tag)
+ # replace style attribute "color: #xxxxxx;" to "..."
+ elif 'color: ' in str(tag['style']):
+ font_tag = soup.new_tag("font")
+ color = re.findall('color: (.*?);', str(tag['style']))
+ if color:
+ font_tag['color'] = color
+ if tag.string:
+ font_tag.string = tag.string
+ tag.replace_with(font_tag)
+ else:
+ tag.unwrap()
+ else:
+ tag.unwrap()
# print paragraphs with numbers
text = soup.body.contents
paragraph_number = 1
diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js
index 3e0a70f50..8619587f4 100644
--- a/openslides/motions/static/js/motions/site.js
+++ b/openslides/motions/static/js/motions/site.js
@@ -152,18 +152,20 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
.factory('MotionForm', [
'gettextCatalog',
'operator',
+ 'Editor',
'Category',
'Config',
'Mediafile',
'Tag',
'User',
'Workflow',
- function (gettextCatalog, operator, Category, Config, Mediafile, Tag, User, Workflow) {
+ function (gettextCatalog, operator, Editor, Category, Config, Mediafile, Tag, User, Workflow) {
return {
// ngDialog for motion form
getDialog: function (motion) {
+ var resolve = {}
if (motion) {
- var resolve = {
+ resolve = {
motion: function() {
return motion;
},
@@ -172,6 +174,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
}
};
}
+ resolve.mediafiles = function(Mediafile) {return Mediafile.findAll();}
return {
template: 'static/templates/motions/motion-form.html',
controller: (motion) ? 'MotionUpdateCtrl' : 'MotionCreateCtrl',
@@ -187,6 +190,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
angular.forEach(workflows, function(workflow) {
workflow.name = gettextCatalog.getString(workflow.name);
});
+ var images = Mediafile.getAllImages();
return [
{
key: 'identifier',
@@ -220,20 +224,24 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
},
{
key: 'text',
- type: 'textarea',
+ type: 'editor',
templateOptions: {
label: gettextCatalog.getString('Text'),
required: true
},
- ngModelElAttrs: {'ckeditor': 'CKEditorOptions'}
+ data: {
+ tinymceOption: Editor.getOptions(images)
+ }
},
{
key: 'reason',
- type: 'textarea',
+ type: 'editor',
templateOptions: {
- label: gettextCatalog.getString('Reason')
+ label: gettextCatalog.getString('Reason'),
},
- ngModelElAttrs: {'ckeditor': 'CKEditorOptions'}
+ data: {
+ tinymceOption: Editor.getOptions(images)
+ }
},
{
key: 'disable_versioning',
diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html
index e71bb4659..74c534d69 100644
--- a/openslides/motions/static/templates/motions/motion-detail.html
+++ b/openslides/motions/static/templates/motions/motion-detail.html
@@ -218,12 +218,12 @@
Text
-
+
diff --git a/openslides/motions/static/templates/motions/motion-form.html b/openslides/motions/static/templates/motions/motion-form.html
index fda4f6e89..5908b9dc7 100644
--- a/openslides/motions/static/templates/motions/motion-form.html
+++ b/openslides/motions/static/templates/motions/motion-form.html
@@ -15,4 +15,3 @@
-
diff --git a/openslides/motions/static/templates/motions/slide_motion.html b/openslides/motions/static/templates/motions/slide_motion.html
index 8f39ca684..758e67f37 100644
--- a/openslides/motions/static/templates/motions/slide_motion.html
+++ b/openslides/motions/static/templates/motions/slide_motion.html
@@ -62,9 +62,9 @@
-
+
Reason
-
+
diff --git a/openslides/users/static/js/users/site.js b/openslides/users/static/js/users/site.js
index 722dbf6d1..83a2450e0 100644
--- a/openslides/users/static/js/users/site.js
+++ b/openslides/users/static/js/users/site.js
@@ -235,8 +235,10 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
.factory('UserForm', [
'$http',
'gettextCatalog',
+ 'Editor',
'Group',
- function ($http, gettextCatalog, Group) {
+ 'Mediafile',
+ function ($http, gettextCatalog, Editor, Group, Mediafile) {
return {
// ngDialog for user form
getDialog: function (user) {
@@ -256,6 +258,7 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
},
// angular-formly fields for user form
getFormFields: function (hideOnCreateForm) {
+ var images = Mediafile.getAllImages();
return [
{
key: 'username',
@@ -334,12 +337,13 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
},
{
key: 'about_me',
- type: 'textarea',
+ type: 'editor',
templateOptions: {
label: gettextCatalog.getString('About me'),
- description: gettextCatalog.getString('Profile text.')
},
- ngModelElAttrs: {'ckeditor': 'CKEditorOptions'}
+ data: {
+ tinymceOption: Editor.getOptions(images)
+ }
},
{
key: 'is_present',
@@ -549,10 +553,12 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
.controller('UserProfileCtrl', [
'$scope',
'$state',
+ 'Editor',
'User',
'user',
- function($scope, $state, User, user) {
+ function($scope, $state, Editor, User, user) {
$scope.user = user; // autoupdate is not activated
+ $scope.tinymceOption = Editor.getOptions();
$scope.save = function (user) {
User.save(user, { method: 'PATCH' }).then(
function(success) {
diff --git a/openslides/users/static/templates/users/user-detail-profile.html b/openslides/users/static/templates/users/user-detail-profile.html
index df341506d..e80f51eb7 100644
--- a/openslides/users/static/templates/users/user-detail-profile.html
+++ b/openslides/users/static/templates/users/user-detail-profile.html
@@ -37,7 +37,7 @@
-
+