Merge pull request #16496 from open-craft/josh/announcements-feature
Dashboard announcements feature
@@ -14,6 +14,8 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from openedx.features.announcements.models import Announcement
|
||||
|
||||
from .views import COURSE_KEY_ERROR_MESSAGES, MAINTENANCE_VIEWS
|
||||
|
||||
# This list contains URLs of all maintenance app views.
|
||||
@@ -77,8 +79,7 @@ class MaintenanceViewAccessTests(MaintenanceViewTestCase):
|
||||
"""
|
||||
Tests for access control of maintenance views.
|
||||
"""
|
||||
@ddt.data(MAINTENANCE_URLS)
|
||||
@ddt.unpack
|
||||
@ddt.data(*MAINTENANCE_URLS)
|
||||
def test_require_login(self, url):
|
||||
"""
|
||||
Test that maintenance app requires user login.
|
||||
@@ -95,8 +96,7 @@ class MaintenanceViewAccessTests(MaintenanceViewTestCase):
|
||||
|
||||
self.assertRedirects(response, redirect_url)
|
||||
|
||||
@ddt.data(MAINTENANCE_URLS)
|
||||
@ddt.unpack
|
||||
@ddt.data(*MAINTENANCE_URLS)
|
||||
def test_global_staff_access(self, url):
|
||||
"""
|
||||
Test that all maintenance app views are accessible to global staff user.
|
||||
@@ -104,8 +104,7 @@ class MaintenanceViewAccessTests(MaintenanceViewTestCase):
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@ddt.data(MAINTENANCE_URLS)
|
||||
@ddt.unpack
|
||||
@ddt.data(*MAINTENANCE_URLS)
|
||||
def test_non_global_staff_access(self, url):
|
||||
"""
|
||||
Test that all maintenance app views are not accessible to non-global-staff user.
|
||||
@@ -260,3 +259,80 @@ class TestForcePublish(MaintenanceViewTestCase):
|
||||
|
||||
# verify that both branch versions are still different
|
||||
self.verify_versions_are_different(course)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestAnnouncementsViews(MaintenanceViewTestCase):
|
||||
"""
|
||||
Tests for the announcements edit view.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestAnnouncementsViews, self).setUp()
|
||||
self.admin = AdminFactory.create(
|
||||
email='staff@edx.org',
|
||||
username='admin',
|
||||
password='pass'
|
||||
)
|
||||
self.client.login(username=self.admin.username, password='pass')
|
||||
self.non_staff_user = UserFactory.create(
|
||||
email='test@edx.org',
|
||||
username='test',
|
||||
password='pass'
|
||||
)
|
||||
|
||||
def test_index(self):
|
||||
"""
|
||||
Test create announcement view
|
||||
"""
|
||||
url = reverse("maintenance:announcement_index")
|
||||
response = self.client.get(url)
|
||||
self.assertIn('<div class="announcement-container">', response.content)
|
||||
|
||||
def test_create(self):
|
||||
"""
|
||||
Test create announcement view
|
||||
"""
|
||||
url = reverse("maintenance:announcement_create")
|
||||
self.client.post(url, {"content": "Test Create Announcement", "active": True})
|
||||
result = Announcement.objects.filter(content="Test Create Announcement").exists()
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_edit(self):
|
||||
"""
|
||||
Test edit announcement view
|
||||
"""
|
||||
announcement = Announcement.objects.create(content="test")
|
||||
announcement.save()
|
||||
url = reverse("maintenance:announcement_edit", kwargs={"pk": announcement.pk})
|
||||
response = self.client.get(url)
|
||||
self.assertIn('<div class="wrapper-form announcement-container">', response.content)
|
||||
self.client.post(url, {"content": "Test Edit Announcement", "active": True})
|
||||
announcement = Announcement.objects.get(pk=announcement.pk)
|
||||
self.assertEquals(announcement.content, "Test Edit Announcement")
|
||||
|
||||
def test_delete(self):
|
||||
"""
|
||||
Test delete announcement view
|
||||
"""
|
||||
announcement = Announcement.objects.create(content="Test Delete")
|
||||
announcement.save()
|
||||
url = reverse("maintenance:announcement_delete", kwargs={"pk": announcement.pk})
|
||||
self.client.post(url)
|
||||
result = Announcement.objects.filter(content="Test Edit Announcement").exists()
|
||||
self.assertFalse(result)
|
||||
|
||||
def _test_403(self, viewname, kwargs=None):
|
||||
url = reverse("maintenance:%s" % viewname, kwargs=kwargs)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_authorization(self):
|
||||
self.client.login(username=self.non_staff_user, password='pass')
|
||||
announcement = Announcement.objects.create(content="Test Delete")
|
||||
announcement.save()
|
||||
|
||||
self._test_403("announcement_index")
|
||||
self._test_403("announcement_create")
|
||||
self._test_403("announcement_edit", {"pk": announcement.pk})
|
||||
self._test_403("announcement_delete", {"pk": announcement.pk})
|
||||
|
||||
@@ -3,9 +3,16 @@ URLs for the maintenance app.
|
||||
"""
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import ForcePublishCourseView, MaintenanceIndexView
|
||||
from .views import (
|
||||
ForcePublishCourseView, MaintenanceIndexView,
|
||||
AnnouncementIndexView, AnnouncementEditView, AnnouncementCreateView, AnnouncementDeleteView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', MaintenanceIndexView.as_view(), name='maintenance_index'),
|
||||
url(r'^force_publish_course/?$', ForcePublishCourseView.as_view(), name='force_publish_course'),
|
||||
url(r'^announcements/(?P<page>\d+)?$', AnnouncementIndexView.as_view(), name='announcement_index'),
|
||||
url(r'^announcements/create$', AnnouncementCreateView.as_view(), name='announcement_create'),
|
||||
url(r'^announcements/edit/(?P<pk>\d+)?$', AnnouncementEditView.as_view(), name='announcement_edit'),
|
||||
url(r'^announcements/delete/(?P<pk>\d+)$', AnnouncementDeleteView.as_view(), name='announcement_delete'),
|
||||
]
|
||||
|
||||
@@ -3,11 +3,14 @@ Views for the maintenance app.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse, reverse_lazy
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import View
|
||||
from django.views.generic.list import ListView
|
||||
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from six import text_type
|
||||
@@ -20,6 +23,9 @@ from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from openedx.features.announcements.models import Announcement
|
||||
from openedx.features.announcements.forms import AnnouncementForm
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# This dict maintains all the views that will be used Maintenance app.
|
||||
@@ -34,6 +40,15 @@ MAINTENANCE_VIEWS = {
|
||||
'course. This view dry runs the force publish command'
|
||||
),
|
||||
},
|
||||
'announcement_index': {
|
||||
'url': 'maintenance:announcement_index',
|
||||
'name': _('Edit Announcements'),
|
||||
'slug': 'announcement_index',
|
||||
'description': _(
|
||||
'This view shows the announcement editor to create or alter announcements that are shown on the right'
|
||||
'side of the dashboard.'
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +82,7 @@ class MaintenanceBaseView(View):
|
||||
template = 'maintenance/container.html'
|
||||
|
||||
def __init__(self, view=None):
|
||||
super(MaintenanceBaseView, self).__init__()
|
||||
self.context = {
|
||||
'view': view if view else '',
|
||||
'form_data': {},
|
||||
@@ -211,3 +227,75 @@ class ForcePublishCourseView(MaintenanceBaseView):
|
||||
exc_info=True
|
||||
)
|
||||
return self.render_response()
|
||||
|
||||
|
||||
class AnnouncementBaseView(View):
|
||||
"""
|
||||
Base view for Announcements pages
|
||||
"""
|
||||
|
||||
@method_decorator(require_global_staff)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super(AnnouncementBaseView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class AnnouncementIndexView(ListView, MaintenanceBaseView):
|
||||
"""
|
||||
View for viewing the announcements shown on the dashboard, used by the global staff.
|
||||
"""
|
||||
model = Announcement
|
||||
object_list = Announcement.objects.order_by('-active')
|
||||
context_object_name = 'announcement_list'
|
||||
paginate_by = 8
|
||||
|
||||
def __init__(self):
|
||||
super(AnnouncementIndexView, self).__init__(MAINTENANCE_VIEWS['announcement_index'])
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AnnouncementIndexView, self).get_context_data(**kwargs)
|
||||
context['view'] = MAINTENANCE_VIEWS['announcement_index']
|
||||
return context
|
||||
|
||||
@method_decorator(require_global_staff)
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = self.get_context_data()
|
||||
return render_to_response(self.template, context)
|
||||
|
||||
|
||||
class AnnouncementEditView(UpdateView, AnnouncementBaseView):
|
||||
"""
|
||||
View for editing an announcement.
|
||||
"""
|
||||
model = Announcement
|
||||
form_class = AnnouncementForm
|
||||
success_url = reverse_lazy('maintenance:announcement_index')
|
||||
template_name = '/maintenance/_announcement_edit.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AnnouncementEditView, self).get_context_data(**kwargs)
|
||||
context['action_url'] = reverse('maintenance:announcement_edit', kwargs={'pk': context['announcement'].pk})
|
||||
return context
|
||||
|
||||
|
||||
class AnnouncementCreateView(CreateView, AnnouncementBaseView):
|
||||
"""
|
||||
View for creating an announcement.
|
||||
"""
|
||||
model = Announcement
|
||||
form_class = AnnouncementForm
|
||||
success_url = reverse_lazy('maintenance:announcement_index')
|
||||
template_name = '/maintenance/_announcement_edit.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AnnouncementCreateView, self).get_context_data(**kwargs)
|
||||
context['action_url'] = reverse('maintenance:announcement_create')
|
||||
return context
|
||||
|
||||
|
||||
class AnnouncementDeleteView(DeleteView, AnnouncementBaseView):
|
||||
"""
|
||||
View for deleting an announcement.
|
||||
"""
|
||||
model = Announcement
|
||||
success_url = reverse_lazy('maintenance:announcement_index')
|
||||
template_name = '/maintenance/_announcement_delete.html'
|
||||
|
||||
@@ -2,3 +2,75 @@
|
||||
%ui-btn-pill {
|
||||
border-radius: ($baseline/5);
|
||||
}
|
||||
|
||||
.announcements-editor {
|
||||
padding: 40px;
|
||||
width: 60%;
|
||||
|
||||
.announcement-content {
|
||||
margin-bottom: 20px;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-active {
|
||||
label {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.save-announcement {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.announcements-list {
|
||||
display: table;
|
||||
border-spacing: 2em;
|
||||
|
||||
.announcement-label {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.announcement-item {
|
||||
display: table-row;
|
||||
|
||||
.announcement-content {
|
||||
display: table-cell;
|
||||
background-color: $color-draft;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
padding: 22px 33px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.announcement-active {
|
||||
display: table-cell;
|
||||
padding: 22px 2px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
a {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:disabled,
|
||||
a[disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,4 +83,22 @@
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-container {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
.announcement-item {
|
||||
display: inline-block;
|
||||
max-width: 300px;
|
||||
min-width: 300px;
|
||||
margin: 15px;
|
||||
|
||||
.announcement-content {
|
||||
background-color: $body-bg;
|
||||
text-align: center;
|
||||
padding: 22px 33px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
cms/templates/maintenance/_announcement_delete.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="base.html" />
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
<%block name="title">${_('Delete Announcement')}</%block>
|
||||
<%block name="viewtitle">
|
||||
<h3 class="info-course">
|
||||
<span>${_('Delete Announcement')}</span>
|
||||
</h3>
|
||||
</%block>
|
||||
|
||||
<%block name="viewcontent">
|
||||
<section class="container maintenance-content">
|
||||
<div id="delete-announcement-form" class="studio-view maintenance-form">
|
||||
<div class="container-message wrapper-message">
|
||||
<div class="message has-warnings" aria-hidden="true">
|
||||
<p class="warning">
|
||||
<span class="icon fa fa-warning"></span>
|
||||
${_('Are you sure you want to delete this Announcement?')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form action="" method="post" class="form-create announcement-container">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}"/>
|
||||
<div class="announcement-item">
|
||||
<div class="announcement-content">
|
||||
## xss-lint: disable=mako-invalid-html-filter
|
||||
${object.content | n}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit" class="action action-primary" value="Confirm">${_('Confirm')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</%block>
|
||||
48
cms/templates/maintenance/_announcement_edit.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="base.html" />
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
<%block name="title">${_('Edit Announcement')}</%block>
|
||||
<%block name="viewtitle">
|
||||
<h3 class="info-course">
|
||||
<span>${_('Edit Announcement')}</span>
|
||||
</h3>
|
||||
</%block>
|
||||
|
||||
<%block name="viewcontent">
|
||||
<section class="container maintenance-content">
|
||||
<div class="container studio-view maintenance-form">
|
||||
<form class="form-create" method="post" action="${action_url}">
|
||||
<div class="wrapper-form announcement-container">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}"/>
|
||||
<div class="announcement-item">
|
||||
## xss-lint: disable=mako-invalid-html-filter
|
||||
${form.as_p() | n}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit" class="action action-primary">${_('Save')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
<script src="${STATIC_URL}js/vendor/tinymce/js/tinymce/tinymce.full.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
tinymce.init({
|
||||
selector: "#id_content",
|
||||
branding: false,
|
||||
plugins: [
|
||||
"advlist autolink lists link image charmap print anchor",
|
||||
"searchreplace visualblocks code",
|
||||
"insertdatetime media table contextmenu paste"
|
||||
],
|
||||
toolbar: "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image"
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
57
cms/templates/maintenance/_announcement_index.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
%>
|
||||
<div class="studio-view maintenance-form">
|
||||
<div class="form-create">
|
||||
<div class="announcement-container">
|
||||
% for announcement in announcement_list:
|
||||
<div class="announcement-item">
|
||||
<div class="announcement-content">
|
||||
## xss-lint: disable=mako-invalid-html-filter
|
||||
${announcement.content | n}
|
||||
</div>
|
||||
|
||||
<div class="actions-list">
|
||||
<span>
|
||||
% if announcement.active:
|
||||
Active <span class="icon fa fa-check-square-o" aria-hidden="true" />
|
||||
% else:
|
||||
Inactive <span class="icon fa fa-square-o" aria-hidden="true" />
|
||||
% endif
|
||||
</span>
|
||||
<a class="action-item announcement-edit"
|
||||
href="${ reverse('maintenance:announcement_edit', kwargs={'pk': announcement.pk}) }">
|
||||
<button class="btn-default">Edit</button>
|
||||
</a>
|
||||
<a class="action-item announcement-delete"
|
||||
href="${ reverse('maintenance:announcement_delete', kwargs={'pk': announcement.pk}) }">
|
||||
<button class="btn-default">Delete</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="${ reverse('maintenance:announcement_create') }">
|
||||
<button class="action action-primary">${_('Create New')}</button>
|
||||
</a>
|
||||
% if is_paginated:
|
||||
% if page_obj.has_previous():
|
||||
<a href="${ reverse('maintenance:announcement_index', kwargs={'page': page_obj.previous_page_number()}) }">
|
||||
<button class="action action-secondary">${_('previous')}</button>
|
||||
</a>
|
||||
% endif
|
||||
|
||||
% if page_obj.has_next():
|
||||
<a href="${ reverse('maintenance:announcement_index', kwargs={'page': page_obj.next_page_number()}) }">
|
||||
<button class="action action-secondary">${_('next')}</button>
|
||||
</a>
|
||||
% endif
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,7 +4,7 @@ Instructions for creating js/tinymce.full.min.js
|
||||
install them per the directions on https://github.com/tinymce/tinymce/tree/4.0.20.
|
||||
2. Unzip edx-platform/vendor_extra/tinymce/JakePackage.zip into this directory (so that Jakefile.js resides in this directory).
|
||||
3. Run the following command in the tinymce directory:
|
||||
jake minify bundle[themes:modern,plugins:image,link,codemirror,paste,table,textcolor,media]
|
||||
jake minify bundle[themes:modern,plugins:advlist,anchor,autolink,charmap,code,codemirror,contextmenu,image,insertdatetime,link,lists,media,paste,print,save,searchreplace,table,textcolor,visualblocks]
|
||||
4. Cleanup by deleting the Unversioned files that were created from unzipping jake_package.zip.
|
||||
|
||||
Instructions for updating tinymce to a newer version:
|
||||
|
||||
93
common/static/js/vendor/tinymce/js/tinymce/plugins/advlist/plugin.js
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
tinymce.PluginManager.add('advlist', function(editor) {
|
||||
var olMenuItems, ulMenuItems, lastStyles = {};
|
||||
|
||||
function buildMenuItems(listName, styleValues) {
|
||||
var items = [];
|
||||
|
||||
tinymce.each(styleValues.split(/[ ,]/), function(styleValue) {
|
||||
items.push({
|
||||
text: styleValue.replace(/\-/g, ' ').replace(/\b\w/g, function(chr) {return chr.toUpperCase();}),
|
||||
data: styleValue == 'default' ? '' : styleValue
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
olMenuItems = buildMenuItems('OL', editor.getParam(
|
||||
"advlist_number_styles",
|
||||
"default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman"
|
||||
));
|
||||
|
||||
ulMenuItems = buildMenuItems('UL', editor.getParam("advlist_bullet_styles", "default,circle,disc,square"));
|
||||
|
||||
function applyListFormat(listName, styleValue) {
|
||||
var list, dom = editor.dom, sel = editor.selection;
|
||||
|
||||
// Check for existing list element
|
||||
list = dom.getParent(sel.getNode(), 'ol,ul');
|
||||
|
||||
// Switch/add list type if needed
|
||||
if (!list || list.nodeName != listName || styleValue === false) {
|
||||
editor.execCommand(listName == 'UL' ? 'InsertUnorderedList' : 'InsertOrderedList');
|
||||
}
|
||||
|
||||
// Set style
|
||||
styleValue = styleValue === false ? lastStyles[listName] : styleValue;
|
||||
lastStyles[listName] = styleValue;
|
||||
|
||||
list = dom.getParent(sel.getNode(), 'ol,ul');
|
||||
if (list) {
|
||||
dom.setStyle(list, 'listStyleType', styleValue);
|
||||
list.removeAttribute('data-mce-style');
|
||||
}
|
||||
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
function updateSelection(e) {
|
||||
var listStyleType = editor.dom.getStyle(editor.dom.getParent(editor.selection.getNode(), 'ol,ul'), 'listStyleType') || '';
|
||||
|
||||
e.control.items().each(function(ctrl) {
|
||||
ctrl.active(ctrl.settings.data === listStyleType);
|
||||
});
|
||||
}
|
||||
|
||||
editor.addButton('numlist', {
|
||||
type: 'splitbutton',
|
||||
tooltip: 'Numbered list',
|
||||
menu: olMenuItems,
|
||||
onshow: updateSelection,
|
||||
onselect: function(e) {
|
||||
applyListFormat('OL', e.control.settings.data);
|
||||
},
|
||||
onclick: function() {
|
||||
applyListFormat('OL', false);
|
||||
}
|
||||
});
|
||||
|
||||
editor.addButton('bullist', {
|
||||
type: 'splitbutton',
|
||||
tooltip: 'Bullet list',
|
||||
menu: ulMenuItems,
|
||||
onshow: updateSelection,
|
||||
onselect: function(e) {
|
||||
applyListFormat('UL', e.control.settings.data);
|
||||
},
|
||||
onclick: function() {
|
||||
applyListFormat('UL', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/advlist/plugin.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tinymce.PluginManager.add("advlist",function(t){function e(t,e){var n=[];return tinymce.each(e.split(/[ ,]/),function(t){n.push({text:t.replace(/\-/g," ").replace(/\b\w/g,function(t){return t.toUpperCase()}),data:"default"==t?"":t})}),n}function n(e,n){var o,l=t.dom,a=t.selection;o=l.getParent(a.getNode(),"ol,ul"),o&&o.nodeName==e&&n!==!1||t.execCommand("UL"==e?"InsertUnorderedList":"InsertOrderedList"),n=n===!1?i[e]:n,i[e]=n,o=l.getParent(a.getNode(),"ol,ul"),o&&(l.setStyle(o,"listStyleType",n),o.removeAttribute("data-mce-style")),t.focus()}function o(e){var n=t.dom.getStyle(t.dom.getParent(t.selection.getNode(),"ol,ul"),"listStyleType")||"";e.control.items().each(function(t){t.active(t.settings.data===n)})}var l,a,i={};l=e("OL",t.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),a=e("UL",t.getParam("advlist_bullet_styles","default,circle,disc,square")),t.addButton("numlist",{type:"splitbutton",tooltip:"Numbered list",menu:l,onshow:o,onselect:function(t){n("OL",t.control.settings.data)},onclick:function(){n("OL",!1)}}),t.addButton("bullist",{type:"splitbutton",tooltip:"Bullet list",menu:a,onshow:o,onselect:function(t){n("UL",t.control.settings.data)},onclick:function(){n("UL",!1)}})});
|
||||
41
common/static/js/vendor/tinymce/js/tinymce/plugins/anchor/plugin.js
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
tinymce.PluginManager.add('anchor', function(editor) {
|
||||
function showDialog() {
|
||||
var selectedNode = editor.selection.getNode();
|
||||
|
||||
editor.windowManager.open({
|
||||
title: 'Anchor',
|
||||
body: {type: 'textbox', name: 'name', size: 40, label: 'Name', value: selectedNode.name || selectedNode.id},
|
||||
onsubmit: function(e) {
|
||||
editor.execCommand('mceInsertContent', false, editor.dom.createHTML('a', {
|
||||
id: e.data.name
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
editor.addButton('anchor', {
|
||||
icon: 'anchor',
|
||||
tooltip: 'Anchor',
|
||||
onclick: showDialog,
|
||||
stateSelector: 'a:not([href])'
|
||||
});
|
||||
|
||||
editor.addMenuItem('anchor', {
|
||||
icon: 'anchor',
|
||||
text: 'Anchor',
|
||||
context: 'insert',
|
||||
onclick: showDialog
|
||||
});
|
||||
});
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/anchor/plugin.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tinymce.PluginManager.add("anchor",function(n){function e(){var e=n.selection.getNode();n.windowManager.open({title:"Anchor",body:{type:"textbox",name:"name",size:40,label:"Name",value:e.name||e.id},onsubmit:function(e){n.execCommand("mceInsertContent",!1,n.dom.createHTML("a",{id:e.data.name}))}})}n.addButton("anchor",{icon:"anchor",tooltip:"Anchor",onclick:e,stateSelector:"a:not([href])"}),n.addMenuItem("anchor",{icon:"anchor",text:"Anchor",context:"insert",onclick:e})});
|
||||
172
common/static/js/vendor/tinymce/js/tinymce/plugins/autolink/plugin.js
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright 2011, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
tinymce.PluginManager.add('autolink', function(editor) {
|
||||
var AutoUrlDetectState;
|
||||
|
||||
editor.on("keydown", function(e) {
|
||||
if (e.keyCode == 13) {
|
||||
return handleEnter(editor);
|
||||
}
|
||||
});
|
||||
|
||||
// Internet Explorer has built-in automatic linking for most cases
|
||||
if (tinymce.Env.ie) {
|
||||
editor.on("focus", function() {
|
||||
if (!AutoUrlDetectState) {
|
||||
AutoUrlDetectState = true;
|
||||
|
||||
try {
|
||||
editor.execCommand('AutoUrlDetect', false, true);
|
||||
} catch (ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
editor.on("keypress", function(e) {
|
||||
if (e.which == 41) {
|
||||
return handleEclipse(editor);
|
||||
}
|
||||
});
|
||||
|
||||
editor.on("keyup", function(e) {
|
||||
if (e.keyCode == 32) {
|
||||
return handleSpacebar(editor);
|
||||
}
|
||||
});
|
||||
|
||||
function handleEclipse(editor) {
|
||||
parseCurrentLine(editor, -1, '(', true);
|
||||
}
|
||||
|
||||
function handleSpacebar(editor) {
|
||||
parseCurrentLine(editor, 0, '', true);
|
||||
}
|
||||
|
||||
function handleEnter(editor) {
|
||||
parseCurrentLine(editor, -1, '', false);
|
||||
}
|
||||
|
||||
function parseCurrentLine(editor, end_offset, delimiter) {
|
||||
var rng, end, start, endContainer, bookmark, text, matches, prev, len;
|
||||
|
||||
// We need at least five characters to form a URL,
|
||||
// hence, at minimum, five characters from the beginning of the line.
|
||||
rng = editor.selection.getRng(true).cloneRange();
|
||||
if (rng.startOffset < 5) {
|
||||
// During testing, the caret is placed inbetween two text nodes.
|
||||
// The previous text node contains the URL.
|
||||
prev = rng.endContainer.previousSibling;
|
||||
if (!prev) {
|
||||
if (!rng.endContainer.firstChild || !rng.endContainer.firstChild.nextSibling) {
|
||||
return;
|
||||
}
|
||||
|
||||
prev = rng.endContainer.firstChild.nextSibling;
|
||||
}
|
||||
|
||||
len = prev.length;
|
||||
rng.setStart(prev, len);
|
||||
rng.setEnd(prev, len);
|
||||
|
||||
if (rng.endOffset < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
end = rng.endOffset;
|
||||
endContainer = prev;
|
||||
} else {
|
||||
endContainer = rng.endContainer;
|
||||
|
||||
// Get a text node
|
||||
if (endContainer.nodeType != 3 && endContainer.firstChild) {
|
||||
while (endContainer.nodeType != 3 && endContainer.firstChild) {
|
||||
endContainer = endContainer.firstChild;
|
||||
}
|
||||
|
||||
// Move range to text node
|
||||
if (endContainer.nodeType == 3) {
|
||||
rng.setStart(endContainer, 0);
|
||||
rng.setEnd(endContainer, endContainer.nodeValue.length);
|
||||
}
|
||||
}
|
||||
|
||||
if (rng.endOffset == 1) {
|
||||
end = 2;
|
||||
} else {
|
||||
end = rng.endOffset - 1 - end_offset;
|
||||
}
|
||||
}
|
||||
|
||||
start = end;
|
||||
|
||||
do {
|
||||
// Move the selection one character backwards.
|
||||
rng.setStart(endContainer, end >= 2 ? end - 2 : 0);
|
||||
rng.setEnd(endContainer, end >= 1 ? end - 1 : 0);
|
||||
end -= 1;
|
||||
|
||||
// Loop until one of the following is found: a blank space, , delimiter, (end-2) >= 0
|
||||
} while (rng.toString() != ' ' && rng.toString() !== '' &&
|
||||
rng.toString().charCodeAt(0) != 160 && (end - 2) >= 0 && rng.toString() != delimiter);
|
||||
|
||||
if (rng.toString() == delimiter || rng.toString().charCodeAt(0) == 160) {
|
||||
rng.setStart(endContainer, end);
|
||||
rng.setEnd(endContainer, start);
|
||||
end += 1;
|
||||
} else if (rng.startOffset === 0) {
|
||||
rng.setStart(endContainer, 0);
|
||||
rng.setEnd(endContainer, start);
|
||||
} else {
|
||||
rng.setStart(endContainer, end);
|
||||
rng.setEnd(endContainer, start);
|
||||
}
|
||||
|
||||
// Exclude last . from word like "www.site.com."
|
||||
text = rng.toString();
|
||||
if (text.charAt(text.length - 1) == '.') {
|
||||
rng.setEnd(endContainer, start - 1);
|
||||
}
|
||||
|
||||
text = rng.toString();
|
||||
matches = text.match(/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i);
|
||||
|
||||
if (matches) {
|
||||
if (matches[1] == 'www.') {
|
||||
matches[1] = 'http://www.';
|
||||
} else if (/@$/.test(matches[1]) && !/^mailto:/.test(matches[1])) {
|
||||
matches[1] = 'mailto:' + matches[1];
|
||||
}
|
||||
|
||||
bookmark = editor.selection.getBookmark();
|
||||
|
||||
editor.selection.setRng(rng);
|
||||
editor.execCommand('createlink', false, matches[1] + matches[2]);
|
||||
editor.selection.moveToBookmark(bookmark);
|
||||
editor.nodeChanged();
|
||||
|
||||
// TODO: Determine if this is still needed.
|
||||
if (tinymce.Env.webkit) {
|
||||
// move the caret to its original position
|
||||
editor.selection.collapse(false);
|
||||
var max = Math.min(endContainer.length, start + 1);
|
||||
rng.setStart(endContainer, max);
|
||||
rng.setEnd(endContainer, max);
|
||||
editor.selection.setRng(rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/autolink/plugin.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tinymce.PluginManager.add("autolink",function(t){function e(t){o(t,-1,"(",!0)}function n(t){o(t,0,"",!0)}function i(t){o(t,-1,"",!1)}function o(t,e,n){var i,o,r,s,d,f,a,l,c;if(i=t.selection.getRng(!0).cloneRange(),i.startOffset<5){if(l=i.endContainer.previousSibling,!l){if(!i.endContainer.firstChild||!i.endContainer.firstChild.nextSibling)return;l=i.endContainer.firstChild.nextSibling}if(c=l.length,i.setStart(l,c),i.setEnd(l,c),i.endOffset<5)return;o=i.endOffset,s=l}else{if(s=i.endContainer,3!=s.nodeType&&s.firstChild){for(;3!=s.nodeType&&s.firstChild;)s=s.firstChild;3==s.nodeType&&(i.setStart(s,0),i.setEnd(s,s.nodeValue.length))}o=1==i.endOffset?2:i.endOffset-1-e}r=o;do i.setStart(s,o>=2?o-2:0),i.setEnd(s,o>=1?o-1:0),o-=1;while(" "!=i.toString()&&""!==i.toString()&&160!=i.toString().charCodeAt(0)&&o-2>=0&&i.toString()!=n);if(i.toString()==n||160==i.toString().charCodeAt(0)?(i.setStart(s,o),i.setEnd(s,r),o+=1):0===i.startOffset?(i.setStart(s,0),i.setEnd(s,r)):(i.setStart(s,o),i.setEnd(s,r)),f=i.toString(),"."==f.charAt(f.length-1)&&i.setEnd(s,r-1),f=i.toString(),a=f.match(/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i),a&&("www."==a[1]?a[1]="http://www.":/@$/.test(a[1])&&!/^mailto:/.test(a[1])&&(a[1]="mailto:"+a[1]),d=t.selection.getBookmark(),t.selection.setRng(i),t.execCommand("createlink",!1,a[1]+a[2]),t.selection.moveToBookmark(d),t.nodeChanged(),tinymce.Env.webkit)){t.selection.collapse(!1);var g=Math.min(s.length,r+1);i.setStart(s,g),i.setEnd(s,g),t.selection.setRng(i)}}var r;return t.on("keydown",function(e){if(13==e.keyCode)return i(t)}),tinymce.Env.ie?void t.on("focus",function(){if(!r){r=!0;try{t.execCommand("AutoUrlDetect",!1,!0)}catch(t){}}}):(t.on("keypress",function(n){if(41==n.which)return e(t)}),void t.on("keyup",function(e){if(32==e.keyCode)return n(t)}))});
|
||||
365
common/static/js/vendor/tinymce/js/tinymce/plugins/charmap/plugin.js
vendored
Normal file
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
tinymce.PluginManager.add('charmap', function(editor) {
|
||||
var charmap = [
|
||||
['160', 'no-break space'],
|
||||
['38', 'ampersand'],
|
||||
['34', 'quotation mark'],
|
||||
// finance
|
||||
['162', 'cent sign'],
|
||||
['8364', 'euro sign'],
|
||||
['163', 'pound sign'],
|
||||
['165', 'yen sign'],
|
||||
// signs
|
||||
['169', 'copyright sign'],
|
||||
['174', 'registered sign'],
|
||||
['8482', 'trade mark sign'],
|
||||
['8240', 'per mille sign'],
|
||||
['181', 'micro sign'],
|
||||
['183', 'middle dot'],
|
||||
['8226', 'bullet'],
|
||||
['8230', 'three dot leader'],
|
||||
['8242', 'minutes / feet'],
|
||||
['8243', 'seconds / inches'],
|
||||
['167', 'section sign'],
|
||||
['182', 'paragraph sign'],
|
||||
['223', 'sharp s / ess-zed'],
|
||||
// quotations
|
||||
['8249', 'single left-pointing angle quotation mark'],
|
||||
['8250', 'single right-pointing angle quotation mark'],
|
||||
['171', 'left pointing guillemet'],
|
||||
['187', 'right pointing guillemet'],
|
||||
['8216', 'left single quotation mark'],
|
||||
['8217', 'right single quotation mark'],
|
||||
['8220', 'left double quotation mark'],
|
||||
['8221', 'right double quotation mark'],
|
||||
['8218', 'single low-9 quotation mark'],
|
||||
['8222', 'double low-9 quotation mark'],
|
||||
['60', 'less-than sign'],
|
||||
['62', 'greater-than sign'],
|
||||
['8804', 'less-than or equal to'],
|
||||
['8805', 'greater-than or equal to'],
|
||||
['8211', 'en dash'],
|
||||
['8212', 'em dash'],
|
||||
['175', 'macron'],
|
||||
['8254', 'overline'],
|
||||
['164', 'currency sign'],
|
||||
['166', 'broken bar'],
|
||||
['168', 'diaeresis'],
|
||||
['161', 'inverted exclamation mark'],
|
||||
['191', 'turned question mark'],
|
||||
['710', 'circumflex accent'],
|
||||
['732', 'small tilde'],
|
||||
['176', 'degree sign'],
|
||||
['8722', 'minus sign'],
|
||||
['177', 'plus-minus sign'],
|
||||
['247', 'division sign'],
|
||||
['8260', 'fraction slash'],
|
||||
['215', 'multiplication sign'],
|
||||
['185', 'superscript one'],
|
||||
['178', 'superscript two'],
|
||||
['179', 'superscript three'],
|
||||
['188', 'fraction one quarter'],
|
||||
['189', 'fraction one half'],
|
||||
['190', 'fraction three quarters'],
|
||||
// math / logical
|
||||
['402', 'function / florin'],
|
||||
['8747', 'integral'],
|
||||
['8721', 'n-ary sumation'],
|
||||
['8734', 'infinity'],
|
||||
['8730', 'square root'],
|
||||
['8764', 'similar to'],
|
||||
['8773', 'approximately equal to'],
|
||||
['8776', 'almost equal to'],
|
||||
['8800', 'not equal to'],
|
||||
['8801', 'identical to'],
|
||||
['8712', 'element of'],
|
||||
['8713', 'not an element of'],
|
||||
['8715', 'contains as member'],
|
||||
['8719', 'n-ary product'],
|
||||
['8743', 'logical and'],
|
||||
['8744', 'logical or'],
|
||||
['172', 'not sign'],
|
||||
['8745', 'intersection'],
|
||||
['8746', 'union'],
|
||||
['8706', 'partial differential'],
|
||||
['8704', 'for all'],
|
||||
['8707', 'there exists'],
|
||||
['8709', 'diameter'],
|
||||
['8711', 'backward difference'],
|
||||
['8727', 'asterisk operator'],
|
||||
['8733', 'proportional to'],
|
||||
['8736', 'angle'],
|
||||
// undefined
|
||||
['180', 'acute accent'],
|
||||
['184', 'cedilla'],
|
||||
['170', 'feminine ordinal indicator'],
|
||||
['186', 'masculine ordinal indicator'],
|
||||
['8224', 'dagger'],
|
||||
['8225', 'double dagger'],
|
||||
// alphabetical special chars
|
||||
['192', 'A - grave'],
|
||||
['193', 'A - acute'],
|
||||
['194', 'A - circumflex'],
|
||||
['195', 'A - tilde'],
|
||||
['196', 'A - diaeresis'],
|
||||
['197', 'A - ring above'],
|
||||
['198', 'ligature AE'],
|
||||
['199', 'C - cedilla'],
|
||||
['200', 'E - grave'],
|
||||
['201', 'E - acute'],
|
||||
['202', 'E - circumflex'],
|
||||
['203', 'E - diaeresis'],
|
||||
['204', 'I - grave'],
|
||||
['205', 'I - acute'],
|
||||
['206', 'I - circumflex'],
|
||||
['207', 'I - diaeresis'],
|
||||
['208', 'ETH'],
|
||||
['209', 'N - tilde'],
|
||||
['210', 'O - grave'],
|
||||
['211', 'O - acute'],
|
||||
['212', 'O - circumflex'],
|
||||
['213', 'O - tilde'],
|
||||
['214', 'O - diaeresis'],
|
||||
['216', 'O - slash'],
|
||||
['338', 'ligature OE'],
|
||||
['352', 'S - caron'],
|
||||
['217', 'U - grave'],
|
||||
['218', 'U - acute'],
|
||||
['219', 'U - circumflex'],
|
||||
['220', 'U - diaeresis'],
|
||||
['221', 'Y - acute'],
|
||||
['376', 'Y - diaeresis'],
|
||||
['222', 'THORN'],
|
||||
['224', 'a - grave'],
|
||||
['225', 'a - acute'],
|
||||
['226', 'a - circumflex'],
|
||||
['227', 'a - tilde'],
|
||||
['228', 'a - diaeresis'],
|
||||
['229', 'a - ring above'],
|
||||
['230', 'ligature ae'],
|
||||
['231', 'c - cedilla'],
|
||||
['232', 'e - grave'],
|
||||
['233', 'e - acute'],
|
||||
['234', 'e - circumflex'],
|
||||
['235', 'e - diaeresis'],
|
||||
['236', 'i - grave'],
|
||||
['237', 'i - acute'],
|
||||
['238', 'i - circumflex'],
|
||||
['239', 'i - diaeresis'],
|
||||
['240', 'eth'],
|
||||
['241', 'n - tilde'],
|
||||
['242', 'o - grave'],
|
||||
['243', 'o - acute'],
|
||||
['244', 'o - circumflex'],
|
||||
['245', 'o - tilde'],
|
||||
['246', 'o - diaeresis'],
|
||||
['248', 'o slash'],
|
||||
['339', 'ligature oe'],
|
||||
['353', 's - caron'],
|
||||
['249', 'u - grave'],
|
||||
['250', 'u - acute'],
|
||||
['251', 'u - circumflex'],
|
||||
['252', 'u - diaeresis'],
|
||||
['253', 'y - acute'],
|
||||
['254', 'thorn'],
|
||||
['255', 'y - diaeresis'],
|
||||
['913', 'Alpha'],
|
||||
['914', 'Beta'],
|
||||
['915', 'Gamma'],
|
||||
['916', 'Delta'],
|
||||
['917', 'Epsilon'],
|
||||
['918', 'Zeta'],
|
||||
['919', 'Eta'],
|
||||
['920', 'Theta'],
|
||||
['921', 'Iota'],
|
||||
['922', 'Kappa'],
|
||||
['923', 'Lambda'],
|
||||
['924', 'Mu'],
|
||||
['925', 'Nu'],
|
||||
['926', 'Xi'],
|
||||
['927', 'Omicron'],
|
||||
['928', 'Pi'],
|
||||
['929', 'Rho'],
|
||||
['931', 'Sigma'],
|
||||
['932', 'Tau'],
|
||||
['933', 'Upsilon'],
|
||||
['934', 'Phi'],
|
||||
['935', 'Chi'],
|
||||
['936', 'Psi'],
|
||||
['937', 'Omega'],
|
||||
['945', 'alpha'],
|
||||
['946', 'beta'],
|
||||
['947', 'gamma'],
|
||||
['948', 'delta'],
|
||||
['949', 'epsilon'],
|
||||
['950', 'zeta'],
|
||||
['951', 'eta'],
|
||||
['952', 'theta'],
|
||||
['953', 'iota'],
|
||||
['954', 'kappa'],
|
||||
['955', 'lambda'],
|
||||
['956', 'mu'],
|
||||
['957', 'nu'],
|
||||
['958', 'xi'],
|
||||
['959', 'omicron'],
|
||||
['960', 'pi'],
|
||||
['961', 'rho'],
|
||||
['962', 'final sigma'],
|
||||
['963', 'sigma'],
|
||||
['964', 'tau'],
|
||||
['965', 'upsilon'],
|
||||
['966', 'phi'],
|
||||
['967', 'chi'],
|
||||
['968', 'psi'],
|
||||
['969', 'omega'],
|
||||
// symbols
|
||||
['8501', 'alef symbol'],
|
||||
['982', 'pi symbol'],
|
||||
['8476', 'real part symbol'],
|
||||
['978', 'upsilon - hook symbol'],
|
||||
['8472', 'Weierstrass p'],
|
||||
['8465', 'imaginary part'],
|
||||
// arrows
|
||||
['8592', 'leftwards arrow'],
|
||||
['8593', 'upwards arrow'],
|
||||
['8594', 'rightwards arrow'],
|
||||
['8595', 'downwards arrow'],
|
||||
['8596', 'left right arrow'],
|
||||
['8629', 'carriage return'],
|
||||
['8656', 'leftwards double arrow'],
|
||||
['8657', 'upwards double arrow'],
|
||||
['8658', 'rightwards double arrow'],
|
||||
['8659', 'downwards double arrow'],
|
||||
['8660', 'left right double arrow'],
|
||||
['8756', 'therefore'],
|
||||
['8834', 'subset of'],
|
||||
['8835', 'superset of'],
|
||||
['8836', 'not a subset of'],
|
||||
['8838', 'subset of or equal to'],
|
||||
['8839', 'superset of or equal to'],
|
||||
['8853', 'circled plus'],
|
||||
['8855', 'circled times'],
|
||||
['8869', 'perpendicular'],
|
||||
['8901', 'dot operator'],
|
||||
['8968', 'left ceiling'],
|
||||
['8969', 'right ceiling'],
|
||||
['8970', 'left floor'],
|
||||
['8971', 'right floor'],
|
||||
['9001', 'left-pointing angle bracket'],
|
||||
['9002', 'right-pointing angle bracket'],
|
||||
['9674', 'lozenge'],
|
||||
['9824', 'black spade suit'],
|
||||
['9827', 'black club suit'],
|
||||
['9829', 'black heart suit'],
|
||||
['9830', 'black diamond suit'],
|
||||
['8194', 'en space'],
|
||||
['8195', 'em space'],
|
||||
['8201', 'thin space'],
|
||||
['8204', 'zero width non-joiner'],
|
||||
['8205', 'zero width joiner'],
|
||||
['8206', 'left-to-right mark'],
|
||||
['8207', 'right-to-left mark'],
|
||||
['173', 'soft hyphen']
|
||||
];
|
||||
|
||||
function showDialog() {
|
||||
var gridHtml, x, y, win;
|
||||
|
||||
function getParentTd(elm) {
|
||||
while (elm) {
|
||||
if (elm.nodeName == 'TD') {
|
||||
return elm;
|
||||
}
|
||||
|
||||
elm = elm.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
gridHtml = '<table role="presentation" cellspacing="0" class="mce-charmap"><tbody>';
|
||||
|
||||
var width = 25;
|
||||
for (y = 0; y < 10; y++) {
|
||||
gridHtml += '<tr>';
|
||||
|
||||
for (x = 0; x < width; x++) {
|
||||
var chr = charmap[y * width + x];
|
||||
|
||||
gridHtml += '<td title="' + chr[1] + '"><div tabindex="-1" title="' + chr[1] + '" role="button">' +
|
||||
(chr ? String.fromCharCode(parseInt(chr[0], 10)) : ' ') + '</div></td>';
|
||||
}
|
||||
|
||||
gridHtml += '</tr>';
|
||||
}
|
||||
|
||||
gridHtml += '</tbody></table>';
|
||||
|
||||
var charMapPanel = {
|
||||
type: 'container',
|
||||
html: gridHtml,
|
||||
onclick: function(e) {
|
||||
var target = e.target;
|
||||
if (/^(TD|DIV)$/.test(target.nodeName)) {
|
||||
editor.execCommand('mceInsertContent', false, tinymce.trim(target.innerText || target.textContent));
|
||||
|
||||
if (!e.ctrlKey) {
|
||||
win.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
onmouseover: function(e) {
|
||||
var td = getParentTd(e.target);
|
||||
|
||||
if (td) {
|
||||
win.find('#preview').text(td.firstChild.firstChild.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
win = editor.windowManager.open({
|
||||
title: "Special character",
|
||||
spacing: 10,
|
||||
padding: 10,
|
||||
items: [
|
||||
charMapPanel,
|
||||
{
|
||||
type: 'label',
|
||||
name: 'preview',
|
||||
text: ' ',
|
||||
style: 'font-size: 40px; text-align: center',
|
||||
border: 1,
|
||||
minWidth: 100,
|
||||
minHeight: 80
|
||||
}
|
||||
],
|
||||
buttons: [
|
||||
{text: "Close", onclick: function() {
|
||||
win.close();
|
||||
}}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
editor.addButton('charmap', {
|
||||
icon: 'charmap',
|
||||
tooltip: 'Special character',
|
||||
onclick: showDialog
|
||||
});
|
||||
|
||||
editor.addMenuItem('charmap', {
|
||||
icon: 'charmap',
|
||||
text: 'Special character',
|
||||
onclick: showDialog,
|
||||
context: 'insert'
|
||||
});
|
||||
});
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/charmap/plugin.min.js
vendored
Normal file
57
common/static/js/vendor/tinymce/js/tinymce/plugins/code/plugin.js
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
tinymce.PluginManager.add('code', function(editor) {
|
||||
function showDialog() {
|
||||
editor.windowManager.open({
|
||||
title: "Source code",
|
||||
body: {
|
||||
type: 'textbox',
|
||||
name: 'code',
|
||||
multiline: true,
|
||||
minWidth: editor.getParam("code_dialog_width", 600),
|
||||
minHeight: editor.getParam("code_dialog_height", Math.min(tinymce.DOM.getViewPort().h - 200, 500)),
|
||||
value: editor.getContent({source_view: true}),
|
||||
spellcheck: false,
|
||||
style: 'direction: ltr; text-align: left'
|
||||
},
|
||||
onSubmit: function(e) {
|
||||
// We get a lovely "Wrong document" error in IE 11 if we
|
||||
// don't move the focus to the editor before creating an undo
|
||||
// transation since it tries to make a bookmark for the current selection
|
||||
editor.focus();
|
||||
|
||||
editor.undoManager.transact(function() {
|
||||
editor.setContent(e.data.code);
|
||||
});
|
||||
|
||||
editor.selection.setCursorLocation();
|
||||
editor.nodeChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
editor.addCommand("mceCodeEditor", showDialog);
|
||||
|
||||
editor.addButton('code', {
|
||||
icon: 'code',
|
||||
tooltip: 'Source code',
|
||||
onclick: showDialog
|
||||
});
|
||||
|
||||
editor.addMenuItem('code', {
|
||||
icon: 'code',
|
||||
text: 'Source code',
|
||||
context: 'tools',
|
||||
onclick: showDialog
|
||||
});
|
||||
});
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/code/plugin.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tinymce.PluginManager.add("code",function(e){function o(){e.windowManager.open({title:"Source code",body:{type:"textbox",name:"code",multiline:!0,minWidth:e.getParam("code_dialog_width",600),minHeight:e.getParam("code_dialog_height",Math.min(tinymce.DOM.getViewPort().h-200,500)),value:e.getContent({source_view:!0}),spellcheck:!1,style:"direction: ltr; text-align: left"},onSubmit:function(o){e.focus(),e.undoManager.transact(function(){e.setContent(o.data.code)}),e.selection.setCursorLocation(),e.nodeChanged()}})}e.addCommand("mceCodeEditor",o),e.addButton("code",{icon:"code",tooltip:"Source code",onclick:o}),e.addMenuItem("code",{icon:"code",text:"Source code",context:"tools",onclick:o})});
|
||||
77
common/static/js/vendor/tinymce/js/tinymce/plugins/contextmenu/plugin.js
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
tinymce.PluginManager.add('contextmenu', function(editor) {
|
||||
var menu, contextmenuNeverUseNative = editor.settings.contextmenu_never_use_native;
|
||||
|
||||
editor.on('contextmenu', function(e) {
|
||||
var contextmenu;
|
||||
|
||||
// Block TinyMCE menu on ctrlKey
|
||||
if (e.ctrlKey && !contextmenuNeverUseNative) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
contextmenu = editor.settings.contextmenu || 'link image inserttable | cell row column deletetable';
|
||||
|
||||
// Render menu
|
||||
if (!menu) {
|
||||
var items = [];
|
||||
|
||||
tinymce.each(contextmenu.split(/[ ,]/), function(name) {
|
||||
var item = editor.menuItems[name];
|
||||
|
||||
if (name == '|') {
|
||||
item = {text: name};
|
||||
}
|
||||
|
||||
if (item) {
|
||||
item.shortcut = ''; // Hide shortcuts
|
||||
items.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].text == '|') {
|
||||
if (i === 0 || i == items.length - 1) {
|
||||
items.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menu = new tinymce.ui.Menu({
|
||||
items: items,
|
||||
context: 'contextmenu'
|
||||
}).addClass('contextmenu').renderTo();
|
||||
|
||||
editor.on('remove', function() {
|
||||
menu.remove();
|
||||
menu = null;
|
||||
});
|
||||
} else {
|
||||
menu.show();
|
||||
}
|
||||
|
||||
// Position menu
|
||||
var pos = {x: e.pageX, y: e.pageY};
|
||||
|
||||
if (!editor.inline) {
|
||||
pos = tinymce.DOM.getPos(editor.getContentAreaContainer());
|
||||
pos.x += e.clientX;
|
||||
pos.y += e.clientY;
|
||||
}
|
||||
|
||||
menu.moveTo(pos.x, pos.y);
|
||||
});
|
||||
});
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/contextmenu/plugin.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tinymce.PluginManager.add("contextmenu",function(e){var n,t=e.settings.contextmenu_never_use_native;e.on("contextmenu",function(i){var o;if(!i.ctrlKey||t){if(i.preventDefault(),o=e.settings.contextmenu||"link image inserttable | cell row column deletetable",n)n.show();else{var c=[];tinymce.each(o.split(/[ ,]/),function(n){var t=e.menuItems[n];"|"==n&&(t={text:n}),t&&(t.shortcut="",c.push(t))});for(var a=0;a<c.length;a++)"|"==c[a].text&&(0===a||a==c.length-1)&&c.splice(a,1);n=new tinymce.ui.Menu({items:c,context:"contextmenu"}).addClass("contextmenu").renderTo(),e.on("remove",function(){n.remove(),n=null})}var l={x:i.pageX,y:i.pageY};e.inline||(l=tinymce.DOM.getPos(e.getContentAreaContainer()),l.x+=i.clientX,l.y+=i.clientY),n.moveTo(l.x,l.y)}})});
|
||||
121
common/static/js/vendor/tinymce/js/tinymce/plugins/insertdatetime/plugin.js
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
tinymce.PluginManager.add('insertdatetime', function(editor) {
|
||||
var daysShort = "Sun Mon Tue Wed Thu Fri Sat Sun".split(' ');
|
||||
var daysLong = "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(' ');
|
||||
var monthsShort = "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(' ');
|
||||
var monthsLong = "January February March April May June July August September October November December".split(' ');
|
||||
var menuItems = [], lastFormat, defaultButtonTimeFormat;
|
||||
|
||||
function getDateTime(fmt, date) {
|
||||
function addZeros(value, len) {
|
||||
value = "" + value;
|
||||
|
||||
if (value.length < len) {
|
||||
for (var i = 0; i < (len - value.length); i++) {
|
||||
value = "0" + value;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
date = date || new Date();
|
||||
|
||||
fmt = fmt.replace("%D", "%m/%d/%Y");
|
||||
fmt = fmt.replace("%r", "%I:%M:%S %p");
|
||||
fmt = fmt.replace("%Y", "" + date.getFullYear());
|
||||
fmt = fmt.replace("%y", "" + date.getYear());
|
||||
fmt = fmt.replace("%m", addZeros(date.getMonth() + 1, 2));
|
||||
fmt = fmt.replace("%d", addZeros(date.getDate(), 2));
|
||||
fmt = fmt.replace("%H", "" + addZeros(date.getHours(), 2));
|
||||
fmt = fmt.replace("%M", "" + addZeros(date.getMinutes(), 2));
|
||||
fmt = fmt.replace("%S", "" + addZeros(date.getSeconds(), 2));
|
||||
fmt = fmt.replace("%I", "" + ((date.getHours() + 11) % 12 + 1));
|
||||
fmt = fmt.replace("%p", "" + (date.getHours() < 12 ? "AM" : "PM"));
|
||||
fmt = fmt.replace("%B", "" + editor.translate(monthsLong[date.getMonth()]));
|
||||
fmt = fmt.replace("%b", "" + editor.translate(monthsShort[date.getMonth()]));
|
||||
fmt = fmt.replace("%A", "" + editor.translate(daysLong[date.getDay()]));
|
||||
fmt = fmt.replace("%a", "" + editor.translate(daysShort[date.getDay()]));
|
||||
fmt = fmt.replace("%%", "%");
|
||||
|
||||
return fmt;
|
||||
}
|
||||
|
||||
function insertDateTime(format) {
|
||||
var html = getDateTime(format);
|
||||
|
||||
if (editor.settings.insertdatetime_element) {
|
||||
var computerTime;
|
||||
|
||||
if (/%[HMSIp]/.test(format)) {
|
||||
computerTime = getDateTime("%Y-%m-%dT%H:%M");
|
||||
} else {
|
||||
computerTime = getDateTime("%Y-%m-%d");
|
||||
}
|
||||
|
||||
html = '<time datetime="' + computerTime + '">' + html + '</time>';
|
||||
|
||||
var timeElm = editor.dom.getParent(editor.selection.getStart(), 'time');
|
||||
if (timeElm) {
|
||||
editor.dom.setOuterHTML(timeElm, html);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
editor.insertContent(html);
|
||||
}
|
||||
|
||||
editor.addCommand('mceInsertDate', function() {
|
||||
insertDateTime(editor.getParam("insertdatetime_dateformat", editor.translate("%Y-%m-%d")));
|
||||
});
|
||||
|
||||
editor.addCommand('mceInsertTime', function() {
|
||||
insertDateTime(editor.getParam("insertdatetime_timeformat", editor.translate('%H:%M:%S')));
|
||||
});
|
||||
|
||||
editor.addButton('insertdatetime', {
|
||||
type: 'splitbutton',
|
||||
title: 'Insert date/time',
|
||||
onclick: function() {
|
||||
insertDateTime(lastFormat || defaultButtonTimeFormat);
|
||||
},
|
||||
menu: menuItems
|
||||
});
|
||||
|
||||
tinymce.each(editor.settings.insertdatetime_formats || [
|
||||
"%H:%M:%S",
|
||||
"%Y-%m-%d",
|
||||
"%I:%M:%S %p",
|
||||
"%D"
|
||||
], function(fmt) {
|
||||
if (!defaultButtonTimeFormat) {
|
||||
defaultButtonTimeFormat = fmt;
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
text: getDateTime(fmt),
|
||||
onclick: function() {
|
||||
lastFormat = fmt;
|
||||
insertDateTime(fmt);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
editor.addMenuItem('insertdatetime', {
|
||||
icon: 'date',
|
||||
text: 'Insert date/time',
|
||||
menu: menuItems,
|
||||
context: 'insert'
|
||||
});
|
||||
});
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/insertdatetime/plugin.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tinymce.PluginManager.add("insertdatetime",function(e){function t(t,a){function n(e,t){if(e=""+e,e.length<t)for(var a=0;a<t-e.length;a++)e="0"+e;return e}return a=a||new Date,t=t.replace("%D","%m/%d/%Y"),t=t.replace("%r","%I:%M:%S %p"),t=t.replace("%Y",""+a.getFullYear()),t=t.replace("%y",""+a.getYear()),t=t.replace("%m",n(a.getMonth()+1,2)),t=t.replace("%d",n(a.getDate(),2)),t=t.replace("%H",""+n(a.getHours(),2)),t=t.replace("%M",""+n(a.getMinutes(),2)),t=t.replace("%S",""+n(a.getSeconds(),2)),t=t.replace("%I",""+((a.getHours()+11)%12+1)),t=t.replace("%p",""+(a.getHours()<12?"AM":"PM")),t=t.replace("%B",""+e.translate(m[a.getMonth()])),t=t.replace("%b",""+e.translate(c[a.getMonth()])),t=t.replace("%A",""+e.translate(d[a.getDay()])),t=t.replace("%a",""+e.translate(i[a.getDay()])),t=t.replace("%%","%")}function a(a){var n=t(a);if(e.settings.insertdatetime_element){var r;r=t(/%[HMSIp]/.test(a)?"%Y-%m-%dT%H:%M":"%Y-%m-%d"),n='<time datetime="'+r+'">'+n+"</time>";var i=e.dom.getParent(e.selection.getStart(),"time");if(i)return void e.dom.setOuterHTML(i,n)}e.insertContent(n)}var n,r,i="Sun Mon Tue Wed Thu Fri Sat Sun".split(" "),d="Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(" "),c="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),m="January February March April May June July August September October November December".split(" "),u=[];e.addCommand("mceInsertDate",function(){a(e.getParam("insertdatetime_dateformat",e.translate("%Y-%m-%d")))}),e.addCommand("mceInsertTime",function(){a(e.getParam("insertdatetime_timeformat",e.translate("%H:%M:%S")))}),e.addButton("insertdatetime",{type:"splitbutton",title:"Insert date/time",onclick:function(){a(n||r)},menu:u}),tinymce.each(e.settings.insertdatetime_formats||["%H:%M:%S","%Y-%m-%d","%I:%M:%S %p","%D"],function(e){r||(r=e),u.push({text:t(e),onclick:function(){n=e,a(e)}})}),e.addMenuItem("insertdatetime",{icon:"date",text:"Insert date/time",menu:u,context:"insert"})});
|
||||
735
common/static/js/vendor/tinymce/js/tinymce/plugins/lists/plugin.js
vendored
Normal file
@@ -0,0 +1,735 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
/*eslint consistent-this:0 */
|
||||
|
||||
tinymce.PluginManager.add('lists', function(editor) {
|
||||
var self = this;
|
||||
|
||||
function isListNode(node) {
|
||||
return node && (/^(OL|UL)$/).test(node.nodeName);
|
||||
}
|
||||
|
||||
function isFirstChild(node) {
|
||||
return node.parentNode.firstChild == node;
|
||||
}
|
||||
|
||||
function isLastChild(node) {
|
||||
return node.parentNode.lastChild == node;
|
||||
}
|
||||
|
||||
function isTextBlock(node) {
|
||||
return node && !!editor.schema.getTextBlockElements()[node.nodeName];
|
||||
}
|
||||
|
||||
function isBookmarkNode(node) {
|
||||
return node && node.nodeName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark';
|
||||
}
|
||||
|
||||
editor.on('init', function() {
|
||||
var dom = editor.dom, selection = editor.selection;
|
||||
|
||||
/**
|
||||
* Returns a range bookmark. This will convert indexed bookmarks into temporary span elements with
|
||||
* index 0 so that they can be restored properly after the DOM has been modified. Text bookmarks will not have spans
|
||||
* added to them since they can be restored after a dom operation.
|
||||
*
|
||||
* So this: <p><b>|</b><b>|</b></p>
|
||||
* becomes: <p><b><span data-mce-type="bookmark">|</span></b><b data-mce-type="bookmark">|</span></b></p>
|
||||
*
|
||||
* @param {DOMRange} rng DOM Range to get bookmark on.
|
||||
* @return {Object} Bookmark object.
|
||||
*/
|
||||
function createBookmark(rng) {
|
||||
var bookmark = {};
|
||||
|
||||
function setupEndPoint(start) {
|
||||
var offsetNode, container, offset;
|
||||
|
||||
container = rng[start ? 'startContainer' : 'endContainer'];
|
||||
offset = rng[start ? 'startOffset' : 'endOffset'];
|
||||
|
||||
if (container.nodeType == 1) {
|
||||
offsetNode = dom.create('span', {'data-mce-type': 'bookmark'});
|
||||
|
||||
if (container.hasChildNodes()) {
|
||||
offset = Math.min(offset, container.childNodes.length - 1);
|
||||
|
||||
if (start) {
|
||||
container.insertBefore(offsetNode, container.childNodes[offset]);
|
||||
} else {
|
||||
dom.insertAfter(offsetNode, container.childNodes[offset]);
|
||||
}
|
||||
} else {
|
||||
container.appendChild(offsetNode);
|
||||
}
|
||||
|
||||
container = offsetNode;
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
bookmark[start ? 'startContainer' : 'endContainer'] = container;
|
||||
bookmark[start ? 'startOffset' : 'endOffset'] = offset;
|
||||
}
|
||||
|
||||
setupEndPoint(true);
|
||||
|
||||
if (!rng.collapsed) {
|
||||
setupEndPoint();
|
||||
}
|
||||
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the selection to the current bookmark and removes any selection container wrappers.
|
||||
*
|
||||
* @param {Object} bookmark Bookmark object to move selection to.
|
||||
*/
|
||||
function moveToBookmark(bookmark) {
|
||||
function restoreEndPoint(start) {
|
||||
var container, offset, node;
|
||||
|
||||
function nodeIndex(container) {
|
||||
var node = container.parentNode.firstChild, idx = 0;
|
||||
|
||||
while (node) {
|
||||
if (node == container) {
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Skip data-mce-type=bookmark nodes
|
||||
if (node.nodeType != 1 || node.getAttribute('data-mce-type') != 'bookmark') {
|
||||
idx++;
|
||||
}
|
||||
|
||||
node = node.nextSibling;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
container = node = bookmark[start ? 'startContainer' : 'endContainer'];
|
||||
offset = bookmark[start ? 'startOffset' : 'endOffset'];
|
||||
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (container.nodeType == 1) {
|
||||
offset = nodeIndex(container);
|
||||
container = container.parentNode;
|
||||
dom.remove(node);
|
||||
}
|
||||
|
||||
bookmark[start ? 'startContainer' : 'endContainer'] = container;
|
||||
bookmark[start ? 'startOffset' : 'endOffset'] = offset;
|
||||
}
|
||||
|
||||
restoreEndPoint(true);
|
||||
restoreEndPoint();
|
||||
|
||||
var rng = dom.createRng();
|
||||
|
||||
rng.setStart(bookmark.startContainer, bookmark.startOffset);
|
||||
|
||||
if (bookmark.endContainer) {
|
||||
rng.setEnd(bookmark.endContainer, bookmark.endOffset);
|
||||
}
|
||||
|
||||
selection.setRng(rng);
|
||||
}
|
||||
|
||||
function createNewTextBlock(contentNode, blockName) {
|
||||
var node, textBlock, fragment = dom.createFragment(), hasContentNode;
|
||||
var blockElements = editor.schema.getBlockElements();
|
||||
|
||||
if (editor.settings.forced_root_block) {
|
||||
blockName = blockName || editor.settings.forced_root_block;
|
||||
}
|
||||
|
||||
if (blockName) {
|
||||
textBlock = dom.create(blockName);
|
||||
|
||||
if (textBlock.tagName === editor.settings.forced_root_block) {
|
||||
dom.setAttribs(textBlock, editor.settings.forced_root_block_attrs);
|
||||
}
|
||||
|
||||
fragment.appendChild(textBlock);
|
||||
}
|
||||
|
||||
if (contentNode) {
|
||||
while ((node = contentNode.firstChild)) {
|
||||
var nodeName = node.nodeName;
|
||||
|
||||
if (!hasContentNode && (nodeName != 'SPAN' || node.getAttribute('data-mce-type') != 'bookmark')) {
|
||||
hasContentNode = true;
|
||||
}
|
||||
|
||||
if (blockElements[nodeName]) {
|
||||
fragment.appendChild(node);
|
||||
textBlock = null;
|
||||
} else {
|
||||
if (blockName) {
|
||||
if (!textBlock) {
|
||||
textBlock = dom.create(blockName);
|
||||
fragment.appendChild(textBlock);
|
||||
}
|
||||
|
||||
textBlock.appendChild(node);
|
||||
} else {
|
||||
fragment.appendChild(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!editor.settings.forced_root_block) {
|
||||
fragment.appendChild(dom.create('br'));
|
||||
} else {
|
||||
// BR is needed in empty blocks on non IE browsers
|
||||
if (!hasContentNode && (!tinymce.Env.ie || tinymce.Env.ie > 10)) {
|
||||
textBlock.appendChild(dom.create('br', {'data-mce-bogus': '1'}));
|
||||
}
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function getSelectedListItems() {
|
||||
return tinymce.grep(selection.getSelectedBlocks(), function(block) {
|
||||
return block.nodeName == 'LI';
|
||||
});
|
||||
}
|
||||
|
||||
function splitList(ul, li, newBlock) {
|
||||
var tmpRng, fragment;
|
||||
|
||||
var bookmarks = dom.select('span[data-mce-type="bookmark"]', ul);
|
||||
|
||||
newBlock = newBlock || createNewTextBlock(li);
|
||||
tmpRng = dom.createRng();
|
||||
tmpRng.setStartAfter(li);
|
||||
tmpRng.setEndAfter(ul);
|
||||
fragment = tmpRng.extractContents();
|
||||
|
||||
if (!dom.isEmpty(fragment)) {
|
||||
dom.insertAfter(fragment, ul);
|
||||
}
|
||||
|
||||
dom.insertAfter(newBlock, ul);
|
||||
|
||||
if (dom.isEmpty(li.parentNode)) {
|
||||
tinymce.each(bookmarks, function(node) {
|
||||
li.parentNode.parentNode.insertBefore(node, li.parentNode);
|
||||
});
|
||||
|
||||
dom.remove(li.parentNode);
|
||||
}
|
||||
|
||||
dom.remove(li);
|
||||
}
|
||||
|
||||
function mergeWithAdjacentLists(listBlock) {
|
||||
var sibling, node;
|
||||
|
||||
sibling = listBlock.nextSibling;
|
||||
if (sibling && isListNode(sibling) && sibling.nodeName == listBlock.nodeName) {
|
||||
while ((node = sibling.firstChild)) {
|
||||
listBlock.appendChild(node);
|
||||
}
|
||||
|
||||
dom.remove(sibling);
|
||||
}
|
||||
|
||||
sibling = listBlock.previousSibling;
|
||||
if (sibling && isListNode(sibling) && sibling.nodeName == listBlock.nodeName) {
|
||||
while ((node = sibling.firstChild)) {
|
||||
listBlock.insertBefore(node, listBlock.firstChild);
|
||||
}
|
||||
|
||||
dom.remove(sibling);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the all lists in the specified element.
|
||||
*/
|
||||
function normalizeList(element) {
|
||||
tinymce.each(tinymce.grep(dom.select('ol,ul', element)), function(ul) {
|
||||
var sibling, parentNode = ul.parentNode;
|
||||
|
||||
// Move UL/OL to previous LI if it's the only child of a LI
|
||||
if (parentNode.nodeName == 'LI' && parentNode.firstChild == ul) {
|
||||
sibling = parentNode.previousSibling;
|
||||
if (sibling && sibling.nodeName == 'LI') {
|
||||
sibling.appendChild(ul);
|
||||
|
||||
if (dom.isEmpty(parentNode)) {
|
||||
dom.remove(parentNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append OL/UL to previous LI if it's in a parent OL/UL i.e. old HTML4
|
||||
if (isListNode(parentNode)) {
|
||||
sibling = parentNode.previousSibling;
|
||||
if (sibling && sibling.nodeName == 'LI') {
|
||||
sibling.appendChild(ul);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function outdent(li) {
|
||||
var ul = li.parentNode, ulParent = ul.parentNode, newBlock;
|
||||
|
||||
function removeEmptyLi(li) {
|
||||
if (dom.isEmpty(li)) {
|
||||
dom.remove(li);
|
||||
}
|
||||
}
|
||||
|
||||
if (isFirstChild(li) && isLastChild(li)) {
|
||||
if (ulParent.nodeName == "LI") {
|
||||
dom.insertAfter(li, ulParent);
|
||||
removeEmptyLi(ulParent);
|
||||
dom.remove(ul);
|
||||
} else if (isListNode(ulParent)) {
|
||||
dom.remove(ul, true);
|
||||
} else {
|
||||
ulParent.insertBefore(createNewTextBlock(li), ul);
|
||||
dom.remove(ul);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (isFirstChild(li)) {
|
||||
if (ulParent.nodeName == "LI") {
|
||||
dom.insertAfter(li, ulParent);
|
||||
li.appendChild(ul);
|
||||
removeEmptyLi(ulParent);
|
||||
} else if (isListNode(ulParent)) {
|
||||
ulParent.insertBefore(li, ul);
|
||||
} else {
|
||||
ulParent.insertBefore(createNewTextBlock(li), ul);
|
||||
dom.remove(li);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (isLastChild(li)) {
|
||||
if (ulParent.nodeName == "LI") {
|
||||
dom.insertAfter(li, ulParent);
|
||||
} else if (isListNode(ulParent)) {
|
||||
dom.insertAfter(li, ul);
|
||||
} else {
|
||||
dom.insertAfter(createNewTextBlock(li), ul);
|
||||
dom.remove(li);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (ulParent.nodeName == 'LI') {
|
||||
ul = ulParent;
|
||||
newBlock = createNewTextBlock(li, 'LI');
|
||||
} else if (isListNode(ulParent)) {
|
||||
newBlock = createNewTextBlock(li, 'LI');
|
||||
} else {
|
||||
newBlock = createNewTextBlock(li);
|
||||
}
|
||||
|
||||
splitList(ul, li, newBlock);
|
||||
normalizeList(ul.parentNode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function indent(li) {
|
||||
var sibling, newList;
|
||||
|
||||
function mergeLists(from, to) {
|
||||
var node;
|
||||
|
||||
if (isListNode(from)) {
|
||||
while ((node = li.lastChild.firstChild)) {
|
||||
to.appendChild(node);
|
||||
}
|
||||
|
||||
dom.remove(from);
|
||||
}
|
||||
}
|
||||
|
||||
sibling = li.previousSibling;
|
||||
|
||||
if (sibling && isListNode(sibling)) {
|
||||
sibling.appendChild(li);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sibling && sibling.nodeName == 'LI' && isListNode(sibling.lastChild)) {
|
||||
sibling.lastChild.appendChild(li);
|
||||
mergeLists(li.lastChild, sibling.lastChild);
|
||||
return true;
|
||||
}
|
||||
|
||||
sibling = li.nextSibling;
|
||||
|
||||
if (sibling && isListNode(sibling)) {
|
||||
sibling.insertBefore(li, sibling.firstChild);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sibling && sibling.nodeName == 'LI' && isListNode(li.lastChild)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sibling = li.previousSibling;
|
||||
if (sibling && sibling.nodeName == 'LI') {
|
||||
newList = dom.create(li.parentNode.nodeName);
|
||||
sibling.appendChild(newList);
|
||||
newList.appendChild(li);
|
||||
mergeLists(li.lastChild, newList);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function indentSelection() {
|
||||
var listElements = getSelectedListItems();
|
||||
|
||||
if (listElements.length) {
|
||||
var bookmark = createBookmark(selection.getRng(true));
|
||||
|
||||
for (var i = 0; i < listElements.length; i++) {
|
||||
if (!indent(listElements[i]) && i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
moveToBookmark(bookmark);
|
||||
editor.nodeChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function outdentSelection() {
|
||||
var listElements = getSelectedListItems();
|
||||
|
||||
if (listElements.length) {
|
||||
var bookmark = createBookmark(selection.getRng(true));
|
||||
var i, y, root = editor.getBody();
|
||||
|
||||
i = listElements.length;
|
||||
while (i--) {
|
||||
var node = listElements[i].parentNode;
|
||||
|
||||
while (node && node != root) {
|
||||
y = listElements.length;
|
||||
while (y--) {
|
||||
if (listElements[y] === node) {
|
||||
listElements.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
node = node.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < listElements.length; i++) {
|
||||
if (!outdent(listElements[i]) && i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
moveToBookmark(bookmark);
|
||||
editor.nodeChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function applyList(listName) {
|
||||
var rng = selection.getRng(true), bookmark = createBookmark(rng);
|
||||
|
||||
function getSelectedTextBlocks() {
|
||||
var textBlocks = [], root = editor.getBody();
|
||||
|
||||
function getEndPointNode(start) {
|
||||
var container, offset;
|
||||
|
||||
container = rng[start ? 'startContainer' : 'endContainer'];
|
||||
offset = rng[start ? 'startOffset' : 'endOffset'];
|
||||
|
||||
// Resolve node index
|
||||
if (container.nodeType == 1) {
|
||||
container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
|
||||
}
|
||||
|
||||
while (container.parentNode != root) {
|
||||
if (isTextBlock(container)) {
|
||||
return container;
|
||||
}
|
||||
|
||||
if (/^(TD|TH)$/.test(container.parentNode.nodeName)) {
|
||||
return container;
|
||||
}
|
||||
|
||||
container = container.parentNode;
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
var startNode = getEndPointNode(true);
|
||||
var endNode = getEndPointNode();
|
||||
var block, siblings = [];
|
||||
|
||||
for (var node = startNode; node; node = node.nextSibling) {
|
||||
siblings.push(node);
|
||||
|
||||
if (node == endNode) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tinymce.each(siblings, function(node) {
|
||||
if (isTextBlock(node)) {
|
||||
textBlocks.push(node);
|
||||
block = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (dom.isBlock(node) || node.nodeName == 'BR') {
|
||||
if (node.nodeName == 'BR') {
|
||||
dom.remove(node);
|
||||
}
|
||||
|
||||
block = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var nextSibling = node.nextSibling;
|
||||
if (isBookmarkNode(node)) {
|
||||
if (isTextBlock(nextSibling) || (!nextSibling && node.parentNode == root)) {
|
||||
block = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!block) {
|
||||
block = dom.create('p');
|
||||
node.parentNode.insertBefore(block, node);
|
||||
textBlocks.push(block);
|
||||
}
|
||||
|
||||
block.appendChild(node);
|
||||
});
|
||||
|
||||
return textBlocks;
|
||||
}
|
||||
|
||||
var textBlocks = getSelectedTextBlocks();
|
||||
|
||||
tinymce.each(textBlocks, function(block) {
|
||||
var listBlock, sibling;
|
||||
|
||||
sibling = block.previousSibling;
|
||||
if (sibling && isListNode(sibling) && sibling.nodeName == listName) {
|
||||
listBlock = sibling;
|
||||
block = dom.rename(block, 'LI');
|
||||
sibling.appendChild(block);
|
||||
} else {
|
||||
listBlock = dom.create(listName);
|
||||
block.parentNode.insertBefore(listBlock, block);
|
||||
listBlock.appendChild(block);
|
||||
block = dom.rename(block, 'LI');
|
||||
}
|
||||
|
||||
mergeWithAdjacentLists(listBlock);
|
||||
});
|
||||
|
||||
moveToBookmark(bookmark);
|
||||
}
|
||||
|
||||
function removeList() {
|
||||
var bookmark = createBookmark(selection.getRng(true)), root = editor.getBody();
|
||||
|
||||
tinymce.each(getSelectedListItems(), function(li) {
|
||||
var node, rootList;
|
||||
|
||||
if (dom.isEmpty(li)) {
|
||||
outdent(li);
|
||||
return;
|
||||
}
|
||||
|
||||
for (node = li; node && node != root; node = node.parentNode) {
|
||||
if (isListNode(node)) {
|
||||
rootList = node;
|
||||
}
|
||||
}
|
||||
|
||||
splitList(rootList, li);
|
||||
});
|
||||
|
||||
moveToBookmark(bookmark);
|
||||
}
|
||||
|
||||
function toggleList(listName) {
|
||||
var parentList = dom.getParent(selection.getStart(), 'OL,UL');
|
||||
|
||||
if (parentList) {
|
||||
if (parentList.nodeName == listName) {
|
||||
removeList(listName);
|
||||
} else {
|
||||
var bookmark = createBookmark(selection.getRng(true));
|
||||
mergeWithAdjacentLists(dom.rename(parentList, listName));
|
||||
moveToBookmark(bookmark);
|
||||
}
|
||||
} else {
|
||||
applyList(listName);
|
||||
}
|
||||
}
|
||||
|
||||
self.backspaceDelete = function(isForward) {
|
||||
function findNextCaretContainer(rng, isForward) {
|
||||
var node = rng.startContainer, offset = rng.startOffset;
|
||||
|
||||
if (node.nodeType == 3 && (isForward ? offset < node.data.length : offset > 0)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
var walker = new tinymce.dom.TreeWalker(rng.startContainer);
|
||||
while ((node = walker[isForward ? 'next' : 'prev']())) {
|
||||
if (node.nodeType == 3 && node.data.length > 0) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mergeLiElements(fromElm, toElm) {
|
||||
var node, listNode, ul = fromElm.parentNode;
|
||||
|
||||
if (isListNode(toElm.lastChild)) {
|
||||
listNode = toElm.lastChild;
|
||||
}
|
||||
|
||||
node = toElm.lastChild;
|
||||
if (node && node.nodeName == 'BR' && fromElm.hasChildNodes()) {
|
||||
dom.remove(node);
|
||||
}
|
||||
|
||||
while ((node = fromElm.firstChild)) {
|
||||
toElm.appendChild(node);
|
||||
}
|
||||
|
||||
if (listNode) {
|
||||
toElm.appendChild(listNode);
|
||||
}
|
||||
|
||||
dom.remove(fromElm);
|
||||
|
||||
if (dom.isEmpty(ul)) {
|
||||
dom.remove(ul);
|
||||
}
|
||||
}
|
||||
|
||||
if (selection.isCollapsed()) {
|
||||
var li = dom.getParent(selection.getStart(), 'LI');
|
||||
|
||||
if (li) {
|
||||
var rng = selection.getRng(true);
|
||||
var otherLi = dom.getParent(findNextCaretContainer(rng, isForward), 'LI');
|
||||
|
||||
if (otherLi && otherLi != li) {
|
||||
var bookmark = createBookmark(rng);
|
||||
|
||||
if (isForward) {
|
||||
mergeLiElements(otherLi, li);
|
||||
} else {
|
||||
mergeLiElements(li, otherLi);
|
||||
}
|
||||
|
||||
moveToBookmark(bookmark);
|
||||
|
||||
return true;
|
||||
} else if (!otherLi) {
|
||||
if (!isForward && removeList(li.parentNode.nodeName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
editor.addCommand('Indent', function() {
|
||||
if (!indentSelection()) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
editor.addCommand('Outdent', function() {
|
||||
if (!outdentSelection()) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
editor.addCommand('InsertUnorderedList', function() {
|
||||
toggleList('UL');
|
||||
});
|
||||
|
||||
editor.addCommand('InsertOrderedList', function() {
|
||||
toggleList('OL');
|
||||
});
|
||||
|
||||
editor.on('keydown', function(e) {
|
||||
if (e.keyCode == 9 && editor.dom.getParent(editor.selection.getStart(), 'LI')) {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.shiftKey) {
|
||||
outdentSelection();
|
||||
} else {
|
||||
indentSelection();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
editor.addButton('indent', {
|
||||
icon: 'indent',
|
||||
title: 'Increase indent',
|
||||
cmd: 'Indent',
|
||||
onPostRender: function() {
|
||||
var ctrl = this;
|
||||
|
||||
editor.on('nodechange', function() {
|
||||
var li = editor.dom.getParent(editor.selection.getNode(), 'LI,UL,OL');
|
||||
ctrl.disabled(li && (li.nodeName != 'LI' || isFirstChild(li)));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('keydown', function(e) {
|
||||
if (e.keyCode == tinymce.util.VK.BACKSPACE) {
|
||||
if (self.backspaceDelete()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (e.keyCode == tinymce.util.VK.DELETE) {
|
||||
if (self.backspaceDelete(true)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/lists/plugin.min.js
vendored
Normal file
@@ -117,4 +117,4 @@
|
||||
writeScripts();
|
||||
})(this);
|
||||
|
||||
// $hash: 7defca82ccee8e0915c8ba39c142611d
|
||||
// $hash: 37d8db4ae3360166a44dd2a51471ece2
|
||||
32
common/static/js/vendor/tinymce/js/tinymce/plugins/print/plugin.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
tinymce.PluginManager.add('print', function(editor) {
|
||||
editor.addCommand('mcePrint', function() {
|
||||
editor.getWin().print();
|
||||
});
|
||||
|
||||
editor.addButton('print', {
|
||||
title: 'Print',
|
||||
cmd: 'mcePrint'
|
||||
});
|
||||
|
||||
editor.addShortcut('Ctrl+P', '', 'mcePrint');
|
||||
|
||||
editor.addMenuItem('print', {
|
||||
text: 'Print',
|
||||
cmd: 'mcePrint',
|
||||
icon: 'print',
|
||||
shortcut: 'Ctrl+P',
|
||||
context: 'file'
|
||||
});
|
||||
});
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/print/plugin.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tinymce.PluginManager.add("print",function(t){t.addCommand("mcePrint",function(){t.getWin().print()}),t.addButton("print",{title:"Print",cmd:"mcePrint"}),t.addShortcut("Ctrl+P","","mcePrint"),t.addMenuItem("print",{text:"Print",cmd:"mcePrint",icon:"print",shortcut:"Ctrl+P",context:"file"})});
|
||||
94
common/static/js/vendor/tinymce/js/tinymce/plugins/save/plugin.js
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
tinymce.PluginManager.add('save', function(editor) {
|
||||
function save() {
|
||||
var formObj;
|
||||
|
||||
formObj = tinymce.DOM.getParent(editor.id, 'form');
|
||||
|
||||
if (editor.getParam("save_enablewhendirty", true) && !editor.isDirty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
tinymce.triggerSave();
|
||||
|
||||
// Use callback instead
|
||||
if (editor.getParam("save_onsavecallback")) {
|
||||
if (editor.execCallback('save_onsavecallback', editor)) {
|
||||
editor.startContent = tinymce.trim(editor.getContent({format: 'raw'}));
|
||||
editor.nodeChanged();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (formObj) {
|
||||
editor.isNotDirty = true;
|
||||
|
||||
if (!formObj.onsubmit || formObj.onsubmit()) {
|
||||
if (typeof(formObj.submit) == "function") {
|
||||
formObj.submit();
|
||||
} else {
|
||||
editor.windowManager.alert("Error: Form submit field collision.");
|
||||
}
|
||||
}
|
||||
|
||||
editor.nodeChanged();
|
||||
} else {
|
||||
editor.windowManager.alert("Error: No form element found.");
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
var h = tinymce.trim(editor.startContent);
|
||||
|
||||
// Use callback instead
|
||||
if (editor.getParam("save_oncancelcallback")) {
|
||||
editor.execCallback('save_oncancelcallback', editor);
|
||||
return;
|
||||
}
|
||||
|
||||
editor.setContent(h);
|
||||
editor.undoManager.clear();
|
||||
editor.nodeChanged();
|
||||
}
|
||||
|
||||
function stateToggle() {
|
||||
var self = this;
|
||||
|
||||
editor.on('nodeChange', function() {
|
||||
self.disabled(editor.getParam("save_enablewhendirty", true) && !editor.isDirty());
|
||||
});
|
||||
}
|
||||
|
||||
editor.addCommand('mceSave', save);
|
||||
editor.addCommand('mceCancel', cancel);
|
||||
|
||||
editor.addButton('save', {
|
||||
icon: 'save',
|
||||
text: 'Save',
|
||||
cmd: 'mceSave',
|
||||
disabled: true,
|
||||
onPostRender: stateToggle
|
||||
});
|
||||
|
||||
editor.addButton('cancel', {
|
||||
text: 'Cancel',
|
||||
icon: false,
|
||||
cmd: 'mceCancel',
|
||||
disabled: true,
|
||||
onPostRender: stateToggle
|
||||
});
|
||||
|
||||
editor.addShortcut('ctrl+s', '', 'mceSave');
|
||||
});
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/save/plugin.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tinymce.PluginManager.add("save",function(e){function a(){var a;if(a=tinymce.DOM.getParent(e.id,"form"),!e.getParam("save_enablewhendirty",!0)||e.isDirty())return tinymce.triggerSave(),e.getParam("save_onsavecallback")?void(e.execCallback("save_onsavecallback",e)&&(e.startContent=tinymce.trim(e.getContent({format:"raw"})),e.nodeChanged())):void(a?(e.isNotDirty=!0,a.onsubmit&&!a.onsubmit()||("function"==typeof a.submit?a.submit():e.windowManager.alert("Error: Form submit field collision.")),e.nodeChanged()):e.windowManager.alert("Error: No form element found."))}function n(){var a=tinymce.trim(e.startContent);return e.getParam("save_oncancelcallback")?void e.execCallback("save_oncancelcallback",e):(e.setContent(a),e.undoManager.clear(),void e.nodeChanged())}function t(){var a=this;e.on("nodeChange",function(){a.disabled(e.getParam("save_enablewhendirty",!0)&&!e.isDirty())})}e.addCommand("mceSave",a),e.addCommand("mceCancel",n),e.addButton("save",{icon:"save",text:"Save",cmd:"mceSave",disabled:!0,onPostRender:t}),e.addButton("cancel",{text:"Cancel",icon:!1,cmd:"mceCancel",disabled:!0,onPostRender:t}),e.addShortcut("ctrl+s","","mceSave")});
|
||||
595
common/static/js/vendor/tinymce/js/tinymce/plugins/searchreplace/plugin.js
vendored
Normal file
@@ -0,0 +1,595 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*jshint smarttabs:true, undef:true, unused:true, latedef:true, curly:true, bitwise:true */
|
||||
/*eslint no-labels:0, no-constant-condition: 0 */
|
||||
/*global tinymce:true */
|
||||
|
||||
(function() {
|
||||
// Based on work developed by: James Padolsey http://james.padolsey.com
|
||||
// released under UNLICENSE that is compatible with LGPL
|
||||
// TODO: Handle contentEditable edgecase:
|
||||
// <p>text<span contentEditable="false">text<span contentEditable="true">text</span>text</span>text</p>
|
||||
function findAndReplaceDOMText(regex, node, replacementNode, captureGroup, schema) {
|
||||
var m, matches = [], text, count = 0, doc;
|
||||
var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;
|
||||
|
||||
doc = node.ownerDocument;
|
||||
blockElementsMap = schema.getBlockElements(); // H1-H6, P, TD etc
|
||||
hiddenTextElementsMap = schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT
|
||||
shortEndedElementsMap = schema.getShortEndedElements(); // BR, IMG, INPUT
|
||||
|
||||
function getMatchIndexes(m, captureGroup) {
|
||||
captureGroup = captureGroup || 0;
|
||||
|
||||
if (!m[0]) {
|
||||
throw 'findAndReplaceDOMText cannot handle zero-length matches';
|
||||
}
|
||||
|
||||
var index = m.index;
|
||||
|
||||
if (captureGroup > 0) {
|
||||
var cg = m[captureGroup];
|
||||
|
||||
if (!cg) {
|
||||
throw 'Invalid capture group';
|
||||
}
|
||||
|
||||
index += m[0].indexOf(cg);
|
||||
m[0] = cg;
|
||||
}
|
||||
|
||||
return [index, index + m[0].length, [m[0]]];
|
||||
}
|
||||
|
||||
function getText(node) {
|
||||
var txt;
|
||||
|
||||
if (node.nodeType === 3) {
|
||||
return node.data;
|
||||
}
|
||||
|
||||
if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
|
||||
return '';
|
||||
}
|
||||
|
||||
txt = '';
|
||||
|
||||
if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
|
||||
txt += '\n';
|
||||
}
|
||||
|
||||
if ((node = node.firstChild)) {
|
||||
do {
|
||||
txt += getText(node);
|
||||
} while ((node = node.nextSibling));
|
||||
}
|
||||
|
||||
return txt;
|
||||
}
|
||||
|
||||
function stepThroughMatches(node, matches, replaceFn) {
|
||||
var startNode, endNode, startNodeIndex,
|
||||
endNodeIndex, innerNodes = [], atIndex = 0, curNode = node,
|
||||
matchLocation = matches.shift(), matchIndex = 0;
|
||||
|
||||
out: while (true) {
|
||||
if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName]) {
|
||||
atIndex++;
|
||||
}
|
||||
|
||||
if (curNode.nodeType === 3) {
|
||||
if (!endNode && curNode.length + atIndex >= matchLocation[1]) {
|
||||
// We've found the ending
|
||||
endNode = curNode;
|
||||
endNodeIndex = matchLocation[1] - atIndex;
|
||||
} else if (startNode) {
|
||||
// Intersecting node
|
||||
innerNodes.push(curNode);
|
||||
}
|
||||
|
||||
if (!startNode && curNode.length + atIndex > matchLocation[0]) {
|
||||
// We've found the match start
|
||||
startNode = curNode;
|
||||
startNodeIndex = matchLocation[0] - atIndex;
|
||||
}
|
||||
|
||||
atIndex += curNode.length;
|
||||
}
|
||||
|
||||
if (startNode && endNode) {
|
||||
curNode = replaceFn({
|
||||
startNode: startNode,
|
||||
startNodeIndex: startNodeIndex,
|
||||
endNode: endNode,
|
||||
endNodeIndex: endNodeIndex,
|
||||
innerNodes: innerNodes,
|
||||
match: matchLocation[2],
|
||||
matchIndex: matchIndex
|
||||
});
|
||||
|
||||
// replaceFn has to return the node that replaced the endNode
|
||||
// and then we step back so we can continue from the end of the
|
||||
// match:
|
||||
atIndex -= (endNode.length - endNodeIndex);
|
||||
startNode = null;
|
||||
endNode = null;
|
||||
innerNodes = [];
|
||||
matchLocation = matches.shift();
|
||||
matchIndex++;
|
||||
|
||||
if (!matchLocation) {
|
||||
break; // no more matches
|
||||
}
|
||||
} else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) {
|
||||
// Move down
|
||||
curNode = curNode.firstChild;
|
||||
continue;
|
||||
} else if (curNode.nextSibling) {
|
||||
// Move forward:
|
||||
curNode = curNode.nextSibling;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move forward or up:
|
||||
while (true) {
|
||||
if (curNode.nextSibling) {
|
||||
curNode = curNode.nextSibling;
|
||||
break;
|
||||
} else if (curNode.parentNode !== node) {
|
||||
curNode = curNode.parentNode;
|
||||
} else {
|
||||
break out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the actual replaceFn which splits up text nodes
|
||||
* and inserts the replacement element.
|
||||
*/
|
||||
function genReplacer(nodeName) {
|
||||
var makeReplacementNode;
|
||||
|
||||
if (typeof nodeName != 'function') {
|
||||
var stencilNode = nodeName.nodeType ? nodeName : doc.createElement(nodeName);
|
||||
|
||||
makeReplacementNode = function(fill, matchIndex) {
|
||||
var clone = stencilNode.cloneNode(false);
|
||||
|
||||
clone.setAttribute('data-mce-index', matchIndex);
|
||||
|
||||
if (fill) {
|
||||
clone.appendChild(doc.createTextNode(fill));
|
||||
}
|
||||
|
||||
return clone;
|
||||
};
|
||||
} else {
|
||||
makeReplacementNode = nodeName;
|
||||
}
|
||||
|
||||
return function(range) {
|
||||
var before, after, parentNode, startNode = range.startNode,
|
||||
endNode = range.endNode, matchIndex = range.matchIndex;
|
||||
|
||||
if (startNode === endNode) {
|
||||
var node = startNode;
|
||||
|
||||
parentNode = node.parentNode;
|
||||
if (range.startNodeIndex > 0) {
|
||||
// Add `before` text node (before the match)
|
||||
before = doc.createTextNode(node.data.substring(0, range.startNodeIndex));
|
||||
parentNode.insertBefore(before, node);
|
||||
}
|
||||
|
||||
// Create the replacement node:
|
||||
var el = makeReplacementNode(range.match[0], matchIndex);
|
||||
parentNode.insertBefore(el, node);
|
||||
if (range.endNodeIndex < node.length) {
|
||||
// Add `after` text node (after the match)
|
||||
after = doc.createTextNode(node.data.substring(range.endNodeIndex));
|
||||
parentNode.insertBefore(after, node);
|
||||
}
|
||||
|
||||
node.parentNode.removeChild(node);
|
||||
|
||||
return el;
|
||||
} else {
|
||||
// Replace startNode -> [innerNodes...] -> endNode (in that order)
|
||||
before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
|
||||
after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
|
||||
var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
|
||||
var innerEls = [];
|
||||
|
||||
for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
|
||||
var innerNode = range.innerNodes[i];
|
||||
var innerEl = makeReplacementNode(innerNode.data, matchIndex);
|
||||
innerNode.parentNode.replaceChild(innerEl, innerNode);
|
||||
innerEls.push(innerEl);
|
||||
}
|
||||
|
||||
var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);
|
||||
|
||||
parentNode = startNode.parentNode;
|
||||
parentNode.insertBefore(before, startNode);
|
||||
parentNode.insertBefore(elA, startNode);
|
||||
parentNode.removeChild(startNode);
|
||||
|
||||
parentNode = endNode.parentNode;
|
||||
parentNode.insertBefore(elB, endNode);
|
||||
parentNode.insertBefore(after, endNode);
|
||||
parentNode.removeChild(endNode);
|
||||
|
||||
return elB;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
text = getText(node);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (regex.global) {
|
||||
while ((m = regex.exec(text))) {
|
||||
matches.push(getMatchIndexes(m, captureGroup));
|
||||
}
|
||||
} else {
|
||||
m = text.match(regex);
|
||||
matches.push(getMatchIndexes(m, captureGroup));
|
||||
}
|
||||
|
||||
if (matches.length) {
|
||||
count = matches.length;
|
||||
stepThroughMatches(node, matches, genReplacer(replacementNode));
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function Plugin(editor) {
|
||||
var self = this, currentIndex = -1;
|
||||
|
||||
function showDialog() {
|
||||
var last = {};
|
||||
|
||||
function updateButtonStates() {
|
||||
win.statusbar.find('#next').disabled(!findSpansByIndex(currentIndex + 1).length);
|
||||
win.statusbar.find('#prev').disabled(!findSpansByIndex(currentIndex - 1).length);
|
||||
}
|
||||
|
||||
function notFoundAlert() {
|
||||
tinymce.ui.MessageBox.alert('Could not find the specified string.', function() {
|
||||
win.find('#find')[0].focus();
|
||||
});
|
||||
}
|
||||
|
||||
var win = tinymce.ui.Factory.create({
|
||||
type: 'window',
|
||||
layout: "flex",
|
||||
pack: "center",
|
||||
align: "center",
|
||||
onClose: function() {
|
||||
editor.focus();
|
||||
self.done();
|
||||
},
|
||||
onSubmit: function(e) {
|
||||
var count, caseState, text, wholeWord;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
caseState = win.find('#case').checked();
|
||||
wholeWord = win.find('#words').checked();
|
||||
|
||||
text = win.find('#find').value();
|
||||
if (!text.length) {
|
||||
self.done(false);
|
||||
win.statusbar.items().slice(1).disabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (last.text == text && last.caseState == caseState && last.wholeWord == wholeWord) {
|
||||
if (findSpansByIndex(currentIndex + 1).length === 0) {
|
||||
notFoundAlert();
|
||||
return;
|
||||
}
|
||||
|
||||
self.next();
|
||||
updateButtonStates();
|
||||
return;
|
||||
}
|
||||
|
||||
count = self.find(text, caseState, wholeWord);
|
||||
if (!count) {
|
||||
notFoundAlert();
|
||||
}
|
||||
|
||||
win.statusbar.items().slice(1).disabled(count === 0);
|
||||
updateButtonStates();
|
||||
|
||||
last = {
|
||||
text: text,
|
||||
caseState: caseState,
|
||||
wholeWord: wholeWord
|
||||
};
|
||||
},
|
||||
buttons: [
|
||||
{text: "Find", onclick: function() {
|
||||
win.submit();
|
||||
}},
|
||||
{text: "Replace", disabled: true, onclick: function() {
|
||||
if (!self.replace(win.find('#replace').value())) {
|
||||
win.statusbar.items().slice(1).disabled(true);
|
||||
currentIndex = -1;
|
||||
last = {};
|
||||
}
|
||||
}},
|
||||
{text: "Replace all", disabled: true, onclick: function() {
|
||||
self.replace(win.find('#replace').value(), true, true);
|
||||
win.statusbar.items().slice(1).disabled(true);
|
||||
last = {};
|
||||
}},
|
||||
{type: "spacer", flex: 1},
|
||||
{text: "Prev", name: 'prev', disabled: true, onclick: function() {
|
||||
self.prev();
|
||||
updateButtonStates();
|
||||
}},
|
||||
{text: "Next", name: 'next', disabled: true, onclick: function() {
|
||||
self.next();
|
||||
updateButtonStates();
|
||||
}}
|
||||
],
|
||||
title: "Find and replace",
|
||||
items: {
|
||||
type: "form",
|
||||
padding: 20,
|
||||
labelGap: 30,
|
||||
spacing: 10,
|
||||
items: [
|
||||
{type: 'textbox', name: 'find', size: 40, label: 'Find', value: editor.selection.getNode().src},
|
||||
{type: 'textbox', name: 'replace', size: 40, label: 'Replace with'},
|
||||
{type: 'checkbox', name: 'case', text: 'Match case', label: ' '},
|
||||
{type: 'checkbox', name: 'words', text: 'Whole words', label: ' '}
|
||||
]
|
||||
}
|
||||
}).renderTo().reflow();
|
||||
}
|
||||
|
||||
self.init = function(ed) {
|
||||
ed.addMenuItem('searchreplace', {
|
||||
text: 'Find and replace',
|
||||
shortcut: 'Ctrl+F',
|
||||
onclick: showDialog,
|
||||
separator: 'before',
|
||||
context: 'edit'
|
||||
});
|
||||
|
||||
ed.addButton('searchreplace', {
|
||||
tooltip: 'Find and replace',
|
||||
shortcut: 'Ctrl+F',
|
||||
onclick: showDialog
|
||||
});
|
||||
|
||||
ed.addCommand("SearchReplace", showDialog);
|
||||
|
||||
ed.shortcuts.add('Ctrl+F', '', showDialog);
|
||||
};
|
||||
|
||||
function getElmIndex(elm) {
|
||||
var value = elm.getAttribute('data-mce-index');
|
||||
|
||||
if (typeof(value) == "number") {
|
||||
return "" + value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function markAllMatches(regex) {
|
||||
var node, marker;
|
||||
|
||||
marker = editor.dom.create('span', {
|
||||
"data-mce-bogus": 1
|
||||
});
|
||||
|
||||
marker.className = 'mce-match-marker'; // IE 7 adds class="mce-match-marker" and class=mce-match-marker
|
||||
node = editor.getBody();
|
||||
|
||||
self.done(false);
|
||||
|
||||
return findAndReplaceDOMText(regex, node, marker, false, editor.schema);
|
||||
}
|
||||
|
||||
function unwrap(node) {
|
||||
var parentNode = node.parentNode;
|
||||
|
||||
if (node.firstChild) {
|
||||
parentNode.insertBefore(node.firstChild, node);
|
||||
}
|
||||
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
|
||||
function findSpansByIndex(index) {
|
||||
var nodes, spans = [];
|
||||
|
||||
nodes = tinymce.toArray(editor.getBody().getElementsByTagName('span'));
|
||||
if (nodes.length) {
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var nodeIndex = getElmIndex(nodes[i]);
|
||||
|
||||
if (nodeIndex === null || !nodeIndex.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nodeIndex === index.toString()) {
|
||||
spans.push(nodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
function moveSelection(forward) {
|
||||
var testIndex = currentIndex, dom = editor.dom;
|
||||
|
||||
forward = forward !== false;
|
||||
|
||||
if (forward) {
|
||||
testIndex++;
|
||||
} else {
|
||||
testIndex--;
|
||||
}
|
||||
|
||||
dom.removeClass(findSpansByIndex(currentIndex), 'mce-match-marker-selected');
|
||||
|
||||
var spans = findSpansByIndex(testIndex);
|
||||
if (spans.length) {
|
||||
dom.addClass(findSpansByIndex(testIndex), 'mce-match-marker-selected');
|
||||
editor.selection.scrollIntoView(spans[0]);
|
||||
return testIndex;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function removeNode(node) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
|
||||
self.find = function(text, matchCase, wholeWord) {
|
||||
text = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||
text = wholeWord ? '\\b' + text + '\\b' : text;
|
||||
|
||||
var count = markAllMatches(new RegExp(text, matchCase ? 'g' : 'gi'));
|
||||
|
||||
if (count) {
|
||||
currentIndex = -1;
|
||||
currentIndex = moveSelection(true);
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
self.next = function() {
|
||||
var index = moveSelection(true);
|
||||
|
||||
if (index !== -1) {
|
||||
currentIndex = index;
|
||||
}
|
||||
};
|
||||
|
||||
self.prev = function() {
|
||||
var index = moveSelection(false);
|
||||
|
||||
if (index !== -1) {
|
||||
currentIndex = index;
|
||||
}
|
||||
};
|
||||
|
||||
self.replace = function(text, forward, all) {
|
||||
var i, nodes, node, matchIndex, currentMatchIndex, nextIndex = currentIndex, hasMore;
|
||||
|
||||
forward = forward !== false;
|
||||
|
||||
node = editor.getBody();
|
||||
nodes = tinymce.toArray(node.getElementsByTagName('span'));
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
var nodeIndex = getElmIndex(nodes[i]);
|
||||
|
||||
if (nodeIndex === null || !nodeIndex.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matchIndex = currentMatchIndex = parseInt(nodeIndex, 10);
|
||||
if (all || matchIndex === currentIndex) {
|
||||
if (text.length) {
|
||||
nodes[i].firstChild.nodeValue = text;
|
||||
unwrap(nodes[i]);
|
||||
} else {
|
||||
removeNode(nodes[i]);
|
||||
}
|
||||
|
||||
while (nodes[++i]) {
|
||||
matchIndex = getElmIndex(nodes[i]);
|
||||
|
||||
if (nodeIndex === null || !nodeIndex.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchIndex === currentMatchIndex) {
|
||||
removeNode(nodes[i]);
|
||||
} else {
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (forward) {
|
||||
nextIndex--;
|
||||
}
|
||||
} else if (currentMatchIndex > currentIndex) {
|
||||
nodes[i].setAttribute('data-mce-index', currentMatchIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
editor.undoManager.add();
|
||||
currentIndex = nextIndex;
|
||||
|
||||
if (forward) {
|
||||
hasMore = findSpansByIndex(nextIndex + 1).length > 0;
|
||||
self.next();
|
||||
} else {
|
||||
hasMore = findSpansByIndex(nextIndex - 1).length > 0;
|
||||
self.prev();
|
||||
}
|
||||
|
||||
return !all && hasMore;
|
||||
};
|
||||
|
||||
self.done = function(keepEditorSelection) {
|
||||
var i, nodes, startContainer, endContainer;
|
||||
|
||||
nodes = tinymce.toArray(editor.getBody().getElementsByTagName('span'));
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
var nodeIndex = getElmIndex(nodes[i]);
|
||||
|
||||
if (nodeIndex !== null && nodeIndex.length) {
|
||||
if (nodeIndex === currentIndex.toString()) {
|
||||
if (!startContainer) {
|
||||
startContainer = nodes[i].firstChild;
|
||||
}
|
||||
|
||||
endContainer = nodes[i].firstChild;
|
||||
}
|
||||
|
||||
unwrap(nodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (startContainer && endContainer) {
|
||||
var rng = editor.dom.createRng();
|
||||
rng.setStart(startContainer, 0);
|
||||
rng.setEnd(endContainer, endContainer.data.length);
|
||||
|
||||
if (keepEditorSelection !== false) {
|
||||
editor.selection.setRng(rng);
|
||||
}
|
||||
|
||||
return rng;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
tinymce.PluginManager.add('searchreplace', Plugin);
|
||||
})();
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/searchreplace/plugin.min.js
vendored
Normal file
@@ -114,4 +114,4 @@
|
||||
writeScripts();
|
||||
})(this);
|
||||
|
||||
// $hash: c439da07cfa97735afe2d2e4ab14b32b
|
||||
// $hash: a7eb34dfa6d2129bc1715cc8f6e4d9c1
|
||||
@@ -116,4 +116,4 @@
|
||||
writeScripts();
|
||||
})(this);
|
||||
|
||||
// $hash: 04ebfed8dc91acb8886fbcda197d4ca5
|
||||
// $hash: 8a0327b8917332b89e69b02d5ba10c30
|
||||
128
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/css/visualblocks.css
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
.mce-visualblocks p {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin-left: 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7);
|
||||
}
|
||||
|
||||
.mce-visualblocks h1 {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin-left: 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==);
|
||||
}
|
||||
|
||||
.mce-visualblocks h2 {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin-left: 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==);
|
||||
}
|
||||
|
||||
.mce-visualblocks h3 {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin-left: 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7);
|
||||
}
|
||||
|
||||
.mce-visualblocks h4 {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin-left: 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==);
|
||||
}
|
||||
|
||||
.mce-visualblocks h5 {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin-left: 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==);
|
||||
}
|
||||
|
||||
.mce-visualblocks h6 {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin-left: 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==);
|
||||
}
|
||||
|
||||
.mce-visualblocks div {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin-left: 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7);
|
||||
}
|
||||
|
||||
.mce-visualblocks section {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin: 0 0 1em 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=);
|
||||
}
|
||||
|
||||
.mce-visualblocks article {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin: 0 0 1em 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7);
|
||||
}
|
||||
|
||||
.mce-visualblocks blockquote {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7);
|
||||
}
|
||||
|
||||
.mce-visualblocks address {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin: 0 0 1em 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=);
|
||||
}
|
||||
|
||||
.mce-visualblocks pre {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin-left: 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==);
|
||||
}
|
||||
|
||||
.mce-visualblocks figure {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin: 0 0 1em 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7);
|
||||
}
|
||||
|
||||
.mce-visualblocks hgroup {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin: 0 0 1em 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7);
|
||||
}
|
||||
|
||||
.mce-visualblocks aside {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin: 0 0 1em 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=);
|
||||
}
|
||||
|
||||
.mce-visualblocks figcaption {
|
||||
border: 1px dashed #BBB;
|
||||
}
|
||||
|
||||
.mce-visualblocks ul {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin: 0 0 1em 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==)
|
||||
}
|
||||
|
||||
.mce-visualblocks ol {
|
||||
padding-top: 10px;
|
||||
border: 1px dashed #BBB;
|
||||
margin: 0 0 1em 3px;
|
||||
background: transparent no-repeat url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==);
|
||||
}
|
||||
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/address.gif
vendored
Normal file
|
After Width: | Height: | Size: 104 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/article.gif
vendored
Normal file
|
After Width: | Height: | Size: 99 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/aside.gif
vendored
Normal file
|
After Width: | Height: | Size: 86 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/blockquote.gif
vendored
Normal file
|
After Width: | Height: | Size: 120 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/div.gif
vendored
Normal file
|
After Width: | Height: | Size: 72 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/figure.gif
vendored
Normal file
|
After Width: | Height: | Size: 93 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/h1.gif
vendored
Normal file
|
After Width: | Height: | Size: 64 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/h2.gif
vendored
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/h3.gif
vendored
Normal file
|
After Width: | Height: | Size: 66 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/h4.gif
vendored
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/h5.gif
vendored
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/h6.gif
vendored
Normal file
|
After Width: | Height: | Size: 67 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/hgroup.gif
vendored
Normal file
|
After Width: | Height: | Size: 96 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/ol.gif
vendored
Normal file
|
After Width: | Height: | Size: 64 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/p.gif
vendored
Normal file
|
After Width: | Height: | Size: 63 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/pre.gif
vendored
Normal file
|
After Width: | Height: | Size: 76 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/section.gif
vendored
Normal file
|
After Width: | Height: | Size: 98 B |
BIN
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/img/ul.gif
vendored
Normal file
|
After Width: | Height: | Size: 64 B |
86
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/plugin.js
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright 2012, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
tinymce.PluginManager.add('visualblocks', function(editor, url) {
|
||||
var cssId, visualBlocksMenuItem, enabled;
|
||||
|
||||
// We don't support older browsers like IE6/7 and they don't provide prototypes for DOM objects
|
||||
if (!window.NodeList) {
|
||||
return;
|
||||
}
|
||||
|
||||
function toggleActiveState() {
|
||||
var self = this;
|
||||
|
||||
self.active(enabled);
|
||||
|
||||
editor.on('VisualBlocks', function() {
|
||||
self.active(editor.dom.hasClass(editor.getBody(), 'mce-visualblocks'));
|
||||
});
|
||||
}
|
||||
|
||||
editor.addCommand('mceVisualBlocks', function() {
|
||||
var dom = editor.dom, linkElm;
|
||||
|
||||
if (!cssId) {
|
||||
cssId = dom.uniqueId();
|
||||
linkElm = dom.create('link', {
|
||||
id: cssId,
|
||||
rel: 'stylesheet',
|
||||
href: url + '/css/visualblocks.css'
|
||||
});
|
||||
|
||||
editor.getDoc().getElementsByTagName('head')[0].appendChild(linkElm);
|
||||
}
|
||||
|
||||
// Toggle on/off visual blocks while computing previews
|
||||
editor.on("PreviewFormats AfterPreviewFormats", function(e) {
|
||||
if (enabled) {
|
||||
dom.toggleClass(editor.getBody(), 'mce-visualblocks', e.type == "afterpreviewformats");
|
||||
}
|
||||
});
|
||||
|
||||
dom.toggleClass(editor.getBody(), 'mce-visualblocks');
|
||||
enabled = editor.dom.hasClass(editor.getBody(), 'mce-visualblocks');
|
||||
|
||||
if (visualBlocksMenuItem) {
|
||||
visualBlocksMenuItem.active(dom.hasClass(editor.getBody(), 'mce-visualblocks'));
|
||||
}
|
||||
|
||||
editor.fire('VisualBlocks');
|
||||
});
|
||||
|
||||
editor.addButton('visualblocks', {
|
||||
title: 'Show blocks',
|
||||
cmd: 'mceVisualBlocks',
|
||||
onPostRender: toggleActiveState
|
||||
});
|
||||
|
||||
editor.addMenuItem('visualblocks', {
|
||||
text: 'Show blocks',
|
||||
cmd: 'mceVisualBlocks',
|
||||
onPostRender: toggleActiveState,
|
||||
selectable: true,
|
||||
context: 'view',
|
||||
prependToContext: true
|
||||
});
|
||||
|
||||
editor.on('init', function() {
|
||||
if (editor.settings.visualblocks_default_state) {
|
||||
editor.execCommand('mceVisualBlocks', false, null, {skip_focus: true});
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('remove', function() {
|
||||
editor.dom.removeClass(editor.getBody(), 'mce-visualblocks');
|
||||
});
|
||||
});
|
||||
1
common/static/js/vendor/tinymce/js/tinymce/plugins/visualblocks/plugin.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tinymce.PluginManager.add("visualblocks",function(e,s){function o(){var s=this;s.active(a),e.on("VisualBlocks",function(){s.active(e.dom.hasClass(e.getBody(),"mce-visualblocks"))})}var l,t,a;window.NodeList&&(e.addCommand("mceVisualBlocks",function(){var o,c=e.dom;l||(l=c.uniqueId(),o=c.create("link",{id:l,rel:"stylesheet",href:s+"/css/visualblocks.css"}),e.getDoc().getElementsByTagName("head")[0].appendChild(o)),e.on("PreviewFormats AfterPreviewFormats",function(s){a&&c.toggleClass(e.getBody(),"mce-visualblocks","afterpreviewformats"==s.type)}),c.toggleClass(e.getBody(),"mce-visualblocks"),a=e.dom.hasClass(e.getBody(),"mce-visualblocks"),t&&t.active(c.hasClass(e.getBody(),"mce-visualblocks")),e.fire("VisualBlocks")}),e.addButton("visualblocks",{title:"Show blocks",cmd:"mceVisualBlocks",onPostRender:o}),e.addMenuItem("visualblocks",{text:"Show blocks",cmd:"mceVisualBlocks",onPostRender:o,selectable:!0,context:"view",prependToContext:!0}),e.on("init",function(){e.settings.visualblocks_default_state&&e.execCommand("mceVisualBlocks",!1,null,{skip_focus:!0})}),e.on("remove",function(){e.dom.removeClass(e.getBody(),"mce-visualblocks")}))});
|
||||
@@ -211,4 +211,4 @@
|
||||
writeScripts();
|
||||
})(this);
|
||||
|
||||
// $hash: e3f9c72105e904a566d407accde5e542
|
||||
// $hash: 5c5f43e325219b538abe4534d1f0aabf
|
||||
@@ -210,4 +210,4 @@
|
||||
writeScripts();
|
||||
})(this);
|
||||
|
||||
// $hash: 84449488706f3974c0cecaf285cc5de0
|
||||
// $hash: e20f07a9652f7467de21c6ba6f8ecdcb
|
||||
@@ -68,6 +68,7 @@
|
||||
|
||||
// features
|
||||
@import 'features/bookmarks-v1';
|
||||
@import "features/announcements";
|
||||
@import 'features/learner-profile';
|
||||
@import 'features/journals';
|
||||
@import 'features/_unsupported-browser-alert';
|
||||
|
||||
28
lms/static/sass/features/_announcements.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
// lms - features - announcements
|
||||
// ====================
|
||||
.announcements-list {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
.announcement {
|
||||
background-color: $course-profile-bg;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
padding: 22px 33px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.announcement-button {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.prev {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.next {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
@@ -591,7 +591,7 @@
|
||||
|
||||
// Responsive behavior
|
||||
@include media-breakpoint-down(md) {
|
||||
flex-direction: column;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ from student.models import CourseEnrollment
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-container">
|
||||
<div class="side-container" role="complementary" aria-label="messages">
|
||||
%if display_sidebar_account_activation_message:
|
||||
<div class="sidebar-notification">
|
||||
<%include file="${static.get_template_path('registration/account_activation_sidebar_notice.html')}" />
|
||||
@@ -270,6 +270,15 @@ from student.models import CourseEnrollment
|
||||
<div id="dashboard-search-results" class="search-results dashboard-search-results"></div>
|
||||
% endif
|
||||
|
||||
<%block name="skip_links">
|
||||
% if settings.FEATURES.get('ENABLE_ANNOUNCEMENTS'):
|
||||
<a id="announcements-skip" class="nav-skip sr-only sr-only-focusable" href="#announcements">${_("Skip to list of announcements")}</a>
|
||||
% endif
|
||||
</%block>
|
||||
% if settings.FEATURES.get('ENABLE_ANNOUNCEMENTS'):
|
||||
<%include file='dashboard/_dashboard_announcements.html' />
|
||||
% endif
|
||||
|
||||
% if display_sidebar_on_dashboard:
|
||||
<div class="profile-sidebar" id="profile-sidebar" role="region" aria-label="Account Status Info">
|
||||
<header class="profile">
|
||||
|
||||
7
lms/templates/dashboard/_dashboard_announcements.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<div id="announcements" role="alert" aria-label="Announcements" tabindex="-1"></div>
|
||||
|
||||
<%static:webpack entry="AnnouncementsView">
|
||||
new AnnouncementsView();
|
||||
</%static:webpack>
|
||||
@@ -175,6 +175,7 @@ from pipeline_mako import render_require_js_path_overrides
|
||||
% if not disable_window_wrap:
|
||||
<div class="window-wrap" dir="${static.dir_rtl()}">
|
||||
% endif
|
||||
<%block name="skip_links"/>
|
||||
<a class="nav-skip sr-only sr-only-focusable" href="#main">${_("Skip to main content")}</a>
|
||||
|
||||
% if not disable_header:
|
||||
|
||||
0
openedx/features/announcements/__init__.py
Normal file
28
openedx/features/announcements/apps.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Announcements Application Configuration
|
||||
"""
|
||||
from django.apps import AppConfig
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType, PluginURLs, PluginSettings
|
||||
|
||||
|
||||
class AnnouncementsConfig(AppConfig):
|
||||
"""
|
||||
Application Configuration for Announcements
|
||||
"""
|
||||
name = u'openedx.features.announcements'
|
||||
|
||||
plugin_app = {
|
||||
PluginURLs.CONFIG: {
|
||||
ProjectType.LMS: {
|
||||
PluginURLs.NAMESPACE: u'announcements',
|
||||
PluginURLs.REGEX: u'announcements/',
|
||||
PluginURLs.RELATIVE_PATH: u'urls',
|
||||
}
|
||||
},
|
||||
PluginSettings.CONFIG: {
|
||||
ProjectType.LMS: {
|
||||
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: u'settings.common'},
|
||||
SettingsType.TEST: {PluginSettings.RELATIVE_PATH: u'settings.test'},
|
||||
}
|
||||
}
|
||||
}
|
||||
19
openedx/features/announcements/forms.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Forms for the Announcement Editor
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import Announcement
|
||||
|
||||
|
||||
class AnnouncementForm(forms.ModelForm):
|
||||
"""
|
||||
Form for editing Announcements
|
||||
"""
|
||||
content = forms.CharField(widget=forms.Textarea, label='', required=False)
|
||||
active = forms.BooleanField(initial=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Announcement
|
||||
fields = ['content', 'active']
|
||||
21
openedx/features/announcements/migrations/0001_initial.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Announcement',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('content', models.CharField(default=b'lorem ipsum', max_length=1000)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
17
openedx/features/announcements/models.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Models for Announcements
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Announcement(models.Model):
|
||||
"""Site-wide announcements to be displayed on the dashboard"""
|
||||
class Meta(object):
|
||||
app_label = 'announcements'
|
||||
|
||||
content = models.CharField(max_length=1000, null=False, default="lorem ipsum")
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.content
|
||||
0
openedx/features/announcements/settings/__init__.py
Normal file
10
openedx/features/announcements/settings/common.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Common settings for Announcements"""
|
||||
|
||||
|
||||
def plugin_settings(settings):
|
||||
"""
|
||||
Common settings for Announcements
|
||||
"""
|
||||
settings.FEATURES['ENABLE_ANNOUNCEMENTS'] = False
|
||||
# Configure number of announcements to show per page
|
||||
settings.FEATURES['ANNOUNCEMENTS_PER_PAGE'] = 5
|
||||
8
openedx/features/announcements/settings/test.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Test settings for Announcements"""
|
||||
|
||||
|
||||
def plugin_settings(settings):
|
||||
"""
|
||||
Test settings for Announcements
|
||||
"""
|
||||
settings.FEATURES['ENABLE_ANNOUNCEMENTS'] = True
|
||||
@@ -0,0 +1,141 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@edx/paragon';
|
||||
import $ from 'jquery';
|
||||
|
||||
|
||||
class AnnouncementSkipLink extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
count: 0
|
||||
};
|
||||
$.get('/announcements/page/1')
|
||||
.then(data => {
|
||||
this.setState({
|
||||
count: data.count
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div>{"Skip to list of " + this.state.count + " announcements"}</div>)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Announcement extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className="announcement"
|
||||
dangerouslySetInnerHTML={{__html: this.props.content}}
|
||||
>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Announcement.propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
||||
class AnnouncementList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
page: 1,
|
||||
announcements: [],
|
||||
num_pages: 0,
|
||||
has_prev: false,
|
||||
has_next: false,
|
||||
start_index: 0,
|
||||
end_index: 0,
|
||||
};
|
||||
}
|
||||
|
||||
retrievePage(page) {
|
||||
$.get('/announcements/page/' + page)
|
||||
.then(data => {
|
||||
this.setState({
|
||||
announcements: data.announcements,
|
||||
has_next: data.next,
|
||||
has_prev: data.prev,
|
||||
num_pages: data.num_pages,
|
||||
count: data.count,
|
||||
start_index: data.start_index,
|
||||
end_index: data.end_index,
|
||||
page: page
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
renderPrevPage() {
|
||||
this.retrievePage(this.state.page - 1);
|
||||
}
|
||||
|
||||
renderNextPage() {
|
||||
this.retrievePage(this.state.page + 1);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.retrievePage(this.state.page);
|
||||
}
|
||||
|
||||
render() {
|
||||
var children = this.state.announcements.map(
|
||||
(announcement, index) => <Announcement key={index} content={announcement.content} />
|
||||
);
|
||||
if (this.state.has_prev)
|
||||
{
|
||||
var prev_button = (
|
||||
<div>
|
||||
<Button
|
||||
className={["announcement-button", "prev"]}
|
||||
onClick={() => this.renderPrevPage()}
|
||||
label="← previous"
|
||||
/>
|
||||
<span className="sr-only">{this.state.start_index + " - " + this.state.end_index + ") of " + this.state.count}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (this.state.has_next)
|
||||
{
|
||||
var next_button = (
|
||||
<div>
|
||||
<Button
|
||||
className={["announcement-button", "next"]}
|
||||
onClick={() => this.renderNextPage()}
|
||||
label="next →"
|
||||
/>
|
||||
<span className="sr-only">{this.state.start_index + " - " + this.state.end_index + ") of " + this.state.count}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="announcements-list">
|
||||
{children}
|
||||
{prev_button}
|
||||
{next_button}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class AnnouncementsView {
|
||||
constructor() {
|
||||
ReactDOM.render(
|
||||
<AnnouncementList />,
|
||||
document.getElementById('announcements'),
|
||||
);
|
||||
ReactDOM.render(
|
||||
<AnnouncementSkipLink />,
|
||||
document.getElementById('announcements-skip'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { AnnouncementsView, AnnouncementList, AnnouncementSkipLink }
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import testAnnouncements from "./test-announcements.json"
|
||||
|
||||
import { AnnouncementSkipLink, AnnouncementList } from "./Announcements"
|
||||
|
||||
describe('Announcements component', () => {
|
||||
test('render skip link', () => {
|
||||
const component = renderer.create(
|
||||
<AnnouncementSkipLink />,
|
||||
);
|
||||
component.root.instance.setState({"count": 10})
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('render test announcements', () => {
|
||||
const component = renderer.create(
|
||||
<AnnouncementList />,
|
||||
);
|
||||
component.root.instance.setState(testAnnouncements);
|
||||
const tree = component.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Announcements component render skip link 1`] = `
|
||||
<div>
|
||||
Skip to list of 10 announcements
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Announcements component render test announcements 1`] = `
|
||||
<div
|
||||
className="announcements-list"
|
||||
>
|
||||
<div
|
||||
className="announcement"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Test Announcement 1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="announcement"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Bold <b>Announcement 2</b>",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="announcement"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Test Announcement 3",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="announcement"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Test Announcement 4",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="announcement"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Test Announcement 5",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="announcement"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Test Announcement 6",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<button
|
||||
className="announcement-button next btn"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
next →
|
||||
</button>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
1 - 5) of 6
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"announcements": [
|
||||
{"content": "Test Announcement 1"},
|
||||
{"content": "Bold <b>Announcement 2</b>"},
|
||||
{"content": "Test Announcement 3"},
|
||||
{"content": "Test Announcement 4"},
|
||||
{"content": "Test Announcement 5"},
|
||||
{"content": "Test Announcement 6"}
|
||||
],
|
||||
"has_next": true,
|
||||
"has_prev": false,
|
||||
"num_pages": 2,
|
||||
"count": 6,
|
||||
"start_index": 1,
|
||||
"end_index": 5,
|
||||
"page": 1
|
||||
}
|
||||
0
openedx/features/announcements/tests/__init__.py
Normal file
96
openedx/features/announcements/tests/test_announcements.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Unit tests for the announcements feature.
|
||||
"""
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from student.tests.factories import AdminFactory
|
||||
|
||||
from openedx.features.announcements.models import Announcement
|
||||
|
||||
TEST_ANNOUNCEMENTS = [
|
||||
("Active Announcement", True),
|
||||
("Inactive Announcement", False),
|
||||
("Another Test Announcement", True),
|
||||
("<strong>Formatted Announcement</strong>", True),
|
||||
("<a>Other Formatted Announcement</a>", True),
|
||||
]
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TestGlobalAnnouncements(TestCase):
|
||||
"""
|
||||
Test Announcements in LMS
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super(TestGlobalAnnouncements, cls).setUpTestData()
|
||||
Announcement.objects.bulk_create([
|
||||
Announcement(content=content, active=active)
|
||||
for content, active in TEST_ANNOUNCEMENTS
|
||||
])
|
||||
|
||||
def setUp(self):
|
||||
super(TestGlobalAnnouncements, self).setUp()
|
||||
self.client = Client()
|
||||
self.admin = AdminFactory.create(
|
||||
email='staff@edx.org',
|
||||
username='admin',
|
||||
password='pass'
|
||||
)
|
||||
self.client.login(username=self.admin.username, password='pass')
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_ANNOUNCEMENTS': False})
|
||||
def test_feature_flag_disabled(self):
|
||||
"""Ensures that the default settings effectively disables the feature"""
|
||||
response = self.client.get('/dashboard')
|
||||
self.assertNotIn('AnnouncementsView', response.content)
|
||||
self.assertNotIn('<div id="announcements"', response.content)
|
||||
|
||||
def test_feature_flag_enabled(self):
|
||||
"""Ensures that enabling the flag, enables the feature"""
|
||||
response = self.client.get('/dashboard')
|
||||
self.assertIn('AnnouncementsView', response.content)
|
||||
|
||||
def test_pagination(self):
|
||||
url = reverse("announcements:page", kwargs={"page": 1})
|
||||
response = self.client.get(url)
|
||||
data = json.loads(response.content)
|
||||
self.assertEquals(data['num_pages'], 1)
|
||||
## double the number of announcements to verify the number of pages increases
|
||||
self.setUpTestData()
|
||||
response = self.client.get(url)
|
||||
data = json.loads(response.content)
|
||||
self.assertEquals(data['num_pages'], 2)
|
||||
|
||||
def test_active(self):
|
||||
"""
|
||||
Ensures that active announcements are visible on the dashboard
|
||||
"""
|
||||
url = reverse("announcements:page", kwargs={"page": 1})
|
||||
response = self.client.get(url)
|
||||
self.assertIn("Active Announcement", response.content)
|
||||
|
||||
def test_inactive(self):
|
||||
"""
|
||||
Ensures that inactive announcements aren't visible on the dashboard
|
||||
"""
|
||||
url = reverse("announcements:page", kwargs={"page": 1})
|
||||
response = self.client.get(url)
|
||||
self.assertNotIn("Inactive Announcement", response.content)
|
||||
|
||||
def test_formatted(self):
|
||||
"""
|
||||
Ensures that formatting in announcements is rendered properly
|
||||
"""
|
||||
url = reverse("announcements:page", kwargs={"page": 1})
|
||||
response = self.client.get(url)
|
||||
self.assertIn("<strong>Formatted Announcement</strong>", response.content)
|
||||
16
openedx/features/announcements/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Defines URLs for announcements in the LMS.
|
||||
"""
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from .views import AnnouncementsJSONView
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
r'^page/(?P<page>\d+)$',
|
||||
login_required(AnnouncementsJSONView.as_view()),
|
||||
name='page',
|
||||
),
|
||||
]
|
||||
36
openedx/features/announcements/views.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Views to show announcements.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import JsonResponse
|
||||
from django.views.generic.list import ListView
|
||||
|
||||
from .models import Announcement
|
||||
|
||||
|
||||
class AnnouncementsJSONView(ListView):
|
||||
"""
|
||||
View returning a page of announcements for the dashboard
|
||||
"""
|
||||
model = Announcement
|
||||
object_list = Announcement.objects.filter(active=True)
|
||||
paginate_by = settings.FEATURES.get('ANNOUNCEMENTS_PER_PAGE', 5)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Return active announcements as json
|
||||
"""
|
||||
context = self.get_context_data()
|
||||
|
||||
announcements = [{"content": announcement.content} for announcement in context['object_list']]
|
||||
result = {
|
||||
"announcements": announcements,
|
||||
"next": context['page_obj'].has_next(),
|
||||
"prev": context['page_obj'].has_previous(),
|
||||
"start_index": context['page_obj'].start_index(),
|
||||
"end_index": context['page_obj'].end_index(),
|
||||
"count": context['paginator'].count,
|
||||
"num_pages": context['paginator'].num_pages,
|
||||
}
|
||||
return JsonResponse(result)
|
||||
2
setup.py
@@ -68,6 +68,7 @@ setup(
|
||||
"bulk_email_optout = lms.djangoapps.bulk_email.policies:CourseEmailOptout"
|
||||
],
|
||||
"lms.djangoapp": [
|
||||
"announcements = openedx.features.announcements.apps:AnnouncementsConfig",
|
||||
"ace_common = openedx.core.djangoapps.ace_common.apps:AceCommonConfig",
|
||||
"credentials = openedx.core.djangoapps.credentials.apps:CredentialsConfig",
|
||||
"discussion = lms.djangoapps.discussion.apps:DiscussionConfig",
|
||||
@@ -83,6 +84,7 @@ setup(
|
||||
"user_authn = openedx.core.djangoapps.user_authn.apps:UserAuthnConfig"
|
||||
],
|
||||
"cms.djangoapp": [
|
||||
"announcements = openedx.features.announcements.apps:AnnouncementsConfig",
|
||||
"ace_common = openedx.core.djangoapps.ace_common.apps:AceCommonConfig",
|
||||
# Importing an LMS app into the Studio process is not a good
|
||||
# practice. We're ignoring this for Discussions here because its
|
||||
|
||||
@@ -112,6 +112,7 @@ module.exports = Merge.smart({
|
||||
LatestUpdate: './openedx/features/course_experience/static/course_experience/js/LatestUpdate.js',
|
||||
WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js',
|
||||
|
||||
AnnouncementsView: './openedx/features/announcements/static/announcements/jsx/Announcements.jsx',
|
||||
CookiePolicyBanner: './common/static/js/src/CookiePolicyBanner.jsx',
|
||||
|
||||
// Common
|
||||
|
||||