-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #44 from unlimitedlabs/quill
Integrate quilljs component for custom task views
- Loading branch information
Showing
11 changed files
with
417 additions
and
1 deletion.
There are no files selected for viewing
20 changes: 20 additions & 0 deletions
20
orchestra/static/orchestra/common/components/components-example.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<!-- | ||
Rich text editor component based on https://github.com/quilljs/quill/. | ||
To use the editor's image upload functionality, include `AWS_S3_KEY`, | ||
`AWS_S3_SECRET`, and `EDITOR_IMAGE_BUCKET_NAME` in your project's Orchestra | ||
settings. | ||
The `data` attribute binds the desired data to the rich text editor and will | ||
contain an HTML blob. | ||
The optional `image-prefix` attribute provides a prefix for editor image S3 keys | ||
(http://docs.aws.amazon.com/AWSImportExport/latest/DG/ManipulatingS3KeyNames.html). | ||
The optional `upload-limit-mb` attribute provides a maximum file size for editor | ||
images uploaded to S3. | ||
--> | ||
<orchestra-quill data="vm.testData" | ||
image-prefix="'editor-image-album'" | ||
upload-limit-mb="15"> | ||
</orchestra-quill> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
(function () { | ||
'use strict'; | ||
|
||
angular.module('orchestra.common.components', [ | ||
'orchestra.common.components.controllers', | ||
'orchestra.common.components.directives' | ||
]); | ||
|
||
angular.module('orchestra.common.components.controllers', []); | ||
angular.module('orchestra.common.components.directives', []); | ||
})(); |
19 changes: 19 additions & 0 deletions
19
orchestra/static/orchestra/common/components/quill/css/quill.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
.orchestra-quill-wrapper { | ||
background-color: #fff; | ||
border-left: 1px solid #ccc; | ||
border-right: 1px solid #ccc; | ||
box-shadow: 0px 0px 10px #ccc; | ||
margin-left: auto; | ||
margin-right: auto; | ||
width: 100%; | ||
|
||
.orchestra-quill-editor { | ||
height: 400px; | ||
} | ||
|
||
.orchestra-quill-toolbar { | ||
margin-top: 15px; | ||
border-bottom: 1px solid #ccc; | ||
border-top: 1px solid #ccc; | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
orchestra/static/orchestra/common/components/quill/css/quill.snow.min.css
Large diffs are not rendered by default.
Oops, something went wrong.
218 changes: 218 additions & 0 deletions
218
orchestra/static/orchestra/common/components/quill/js/directives.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
(function () { | ||
'use strict'; | ||
|
||
angular | ||
.module('orchestra.common.components.directives') | ||
.directive('orchestraQuill', ['$http', '$timeout', orchestraQuill]); | ||
|
||
function orchestraQuill($http, $timeout) { | ||
return { | ||
restrict: 'E', | ||
scope: { | ||
'data': '=', | ||
'imagePrefix': '=', | ||
'readonly': '=', | ||
'uploadLimitMb': '=?', | ||
}, | ||
link: function(scope, el, attr) { | ||
// Suggested limit for image upload size is 5 MB. | ||
scope.uploadLimitMb = scope.uploadLimitMb || 5; | ||
|
||
// Containers within directive template for Quill editor | ||
var editorContainer = el.find('.orchestra-quill-editor').get(0); | ||
var toolbarContainer = el.find('.orchestra-quill-toolbar').get(0); | ||
|
||
scope.editor = new Quill(editorContainer, { | ||
modules: { | ||
'toolbar': {container: toolbarContainer}, | ||
// Image tooltip removed in favor of file selector dialog | ||
'link-tooltip': true, | ||
}, | ||
theme: 'snow' | ||
}); | ||
|
||
// Set up two-way link between quill and parent scope attribute passed | ||
// in by directive caller | ||
if (scope.data) { | ||
scope.editor.setHTML(scope.data); | ||
} | ||
|
||
// Editor set to read-only upon initialization. | ||
if (scope.readonly) { | ||
scope.editor.editor.disable(); | ||
toolbarContainer.remove(); | ||
return; | ||
} | ||
|
||
scope.editor.on('text-change', function() { | ||
// Set the focus outside the $digest block. | ||
// Taken from https://docs.angularjs.org/error/$rootScope/inprog?p0=$digest. | ||
$timeout(function() { | ||
scope.data = scope.editor.getHTML(); | ||
scope.$apply(); | ||
}, 0, false); | ||
}); | ||
|
||
// Upload image via the toolbar button | ||
scope.fileSelector = document.createElement('input'); | ||
scope.fileSelector.setAttribute('type', 'file'); | ||
scope.fileSelector.setAttribute('accept', 'image/*'); | ||
var imageSelector = el.find('.orchestra-quill-toolbar .ql-image').get(0); | ||
scope.fileSelector.onchange = function(e) { | ||
// Focus the editor so we can get the current selection. | ||
scope.editor.focus(); | ||
var files = scope.fileSelector.files; | ||
for (var i = files.length - 1; i >= 0; i--) { | ||
if (files[i] !== null) { | ||
uploadImage(files[i], scope.editor.getSelection()); | ||
} | ||
} | ||
} | ||
imageSelector.onclick = function () { | ||
scope.fileSelector.click(); | ||
return false; | ||
} | ||
|
||
// TODO(jrbotros): move paste/drop functionality into a quilljs fork | ||
|
||
// Upload image by pasting in copied image data | ||
editorContainer.addEventListener('paste', pasteImage, true); | ||
function pasteImage(e) { | ||
if (e.clipboardData && e.clipboardData.items) { | ||
var copiedData = e.clipboardData.items[0]; | ||
var imageFile = copiedData.getAsFile(); | ||
uploadImage(imageFile, scope.editor.getSelection(), e); | ||
} | ||
} | ||
|
||
// Upload image by drag and drop | ||
editorContainer.addEventListener('drop', dropImage, true); | ||
function dropImage(e) { | ||
var files = e.dataTransfer.files; | ||
for (var i = files.length - 1; i >= 0; i--) { | ||
var file = files[i]; | ||
var dropOffset = getDropIndexOffset(e); | ||
uploadImage(file, {'start': dropOffset, 'end': dropOffset}, e); | ||
} | ||
} | ||
|
||
function getDropIndexOffset(dropEvent) { | ||
/** | ||
* Given a drop event, calculate the index offset within the | ||
* Quill editor by: | ||
* - determining the character offset of the drop within the inner node | ||
* - finding all editor leaf nodes (reimplements some Quill functionality) | ||
* - calculating the index offset from the leaf node lengths and inner drop node offset | ||
* We plan to push this functionality into Quill in the future. | ||
*/ | ||
function getDropCharOffset(x, y) { | ||
// Helper function for getting the x-y character offset of a drop | ||
// event in a contenteditable field. | ||
// Modified from http://stackoverflow.com/a/10659990. | ||
var range; | ||
// Standards-based way; implemented only in Firefox | ||
if (document.caretPositionFromPoint) { | ||
var pos = document.caretPositionFromPoint(x, y); | ||
return {'offset': pos.offset, 'node': pos.offsetNode}; | ||
} else if (document.caretRangeFromPoint) { | ||
// Webkit | ||
range = document.caretRangeFromPoint(x, y); | ||
return {'offset': range.startOffset, 'node': range.startContainer}; | ||
} else if (document.body.createTextRange) { | ||
// IE doesn't natively support retrieving the character offset, so | ||
// insert image at end of text | ||
// TODO(jrbotros): rewrite with https://github.com/timdown/rangy | ||
return; | ||
} | ||
} | ||
|
||
function getAllChildNodes(parent) { | ||
// Returns in-order list of all child nodes of `parent` | ||
var bfsStack = [parent]; | ||
var nodes = []; | ||
while (bfsStack.length) { | ||
var currentNode = bfsStack.pop(); | ||
if (currentNode.childNodes) { | ||
// Concat with copy of child NodeList; can't use pop operation | ||
// on original | ||
bfsStack = bfsStack.concat(Array.prototype.slice.call(currentNode.childNodes)); | ||
} | ||
nodes.push(currentNode); | ||
} | ||
return nodes.reverse(); | ||
} | ||
|
||
// http://stackoverflow.com/a/22289650 | ||
function getLeafNodes(parent) { | ||
// Returns in-order list of all Quill leaf nodes of `parent` | ||
var textNodeType = 3; | ||
var nodes = getAllChildNodes(parent); | ||
var leafNodes = nodes.filter(function(elem) { | ||
return elem.nodeType === textNodeType || !elem.firstChild; | ||
}); | ||
return leafNodes; | ||
} | ||
|
||
var caretPosition = getDropCharOffset(dropEvent.clientX, dropEvent.clientY); | ||
if (!caretPosition) { | ||
return scope.editor.getLength(); | ||
} | ||
var leaves = getLeafNodes(editorContainer.getElementsByClassName('ql-editor')[0]); | ||
var opIndex = leaves.indexOf(caretPosition.node); | ||
var contents = scope.editor.getContents() | ||
contents.ops = contents.ops.slice(0, opIndex) | ||
return contents.length() + caretPosition.offset; | ||
} | ||
|
||
|
||
// Post image data to an API endpoint that returns an image URL, then | ||
// adding it to the editor (replacing the given character range) | ||
function uploadImage(file, range, e) { | ||
var uploadAPIEndpoint = '/orchestra/api/interface/upload_image/' | ||
var supportedTypes = ['image/jpeg', 'image/png', 'image/gif'] | ||
|
||
if (supportedTypes.indexOf(file.type) === -1) { | ||
alert('Files type ' + file.type + ' not supported.') | ||
return; | ||
} else if (e) { | ||
// Cancel default browser action only if file is actually an image | ||
e.preventDefault(); | ||
} | ||
|
||
// If nothing is selected in the editor, append the image to its end | ||
if (range === null) { | ||
var endIndex = scope.editor.getLength(); | ||
range = {'start': endIndex, 'end': endIndex} | ||
} | ||
|
||
var reader = new FileReader(); | ||
reader.onload = function(e) { | ||
var rawData = e.target.result; | ||
// Remove prepended image type from data string | ||
var imageData = rawData.substring(rawData.indexOf(",") + 1, rawData.length) | ||
|
||
// Calculate data size of b64-encoded string | ||
var imageSize = imageData.length * 3 / 4; | ||
|
||
if (imageSize > scope.uploadLimitMb * Math.pow(10, 6)) { | ||
alert('Files larger than ' + scope.uploadLimitMb + 'MB cannot be uploaded'); | ||
return; | ||
} | ||
// Post image data to API; response should contain the uploaded image url | ||
$http.post(uploadAPIEndpoint, | ||
{'image_data': imageData, | ||
'image_type': file.type, | ||
'prefix': scope.imagePrefix}) | ||
.then(function(response, status, headers, config) { | ||
// Replace selected range with new image | ||
scope.editor.deleteText(range); | ||
scope.editor.insertEmbed(range.start, 'image', response.data.url, 'user'); | ||
}) | ||
} | ||
reader.readAsDataURL(file); | ||
} | ||
}, | ||
templateUrl: '/static/orchestra/common/components/quill/partials/quill.html' | ||
}; | ||
} | ||
})(); |
9 changes: 9 additions & 0 deletions
9
orchestra/static/orchestra/common/components/quill/js/quill.min.js
Large diffs are not rendered by default.
Oops, something went wrong.
123 changes: 123 additions & 0 deletions
123
orchestra/static/orchestra/common/components/quill/partials/quill.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
<div ng-class="{'readonly': readonly}" class="orchestra-quill-wrapper"> | ||
<div class="orchestra-quill-toolbar"> | ||
<span class="ql-format-group"> | ||
<select title="Font" class="ql-font"> | ||
<option value="sans-serif" selected="">Sans Serif</option> | ||
<option value="serif">Serif</option> | ||
<option value="monospace">Monospace</option> | ||
</select> | ||
<select title="Size" class="ql-size"> | ||
<option value="10px">Small</option> | ||
<option value="13px" selected="">Normal</option> | ||
<option value="18px">Large</option> | ||
<option value="32px">Huge</option> | ||
</select> | ||
</span> | ||
<span class="ql-format-group"> | ||
<span title="Bold" class="ql-format-button ql-bold"></span> | ||
<span class="ql-format-separator"></span> | ||
<span title="Italic" class="ql-format-button ql-italic"></span> | ||
<span class="ql-format-separator"></span> | ||
<span title="Underline" class="ql-format-button ql-underline"></span> | ||
<span class="ql-format-separator"></span> | ||
<span title="Strikethrough" class="ql-format-button ql-strike"></span> | ||
</span> | ||
<span class="ql-format-group"> | ||
<select title="Text Color" class="ql-color"> | ||
<option value="rgb(0, 0, 0)" label="rgb(0, 0, 0)" selected=""></option> | ||
<option value="rgb(230, 0, 0)" label="rgb(230, 0, 0)"></option> | ||
<option value="rgb(255, 153, 0)" label="rgb(255, 153, 0)"></option> | ||
<option value="rgb(255, 255, 0)" label="rgb(255, 255, 0)"></option> | ||
<option value="rgb(0, 138, 0)" label="rgb(0, 138, 0)"></option> | ||
<option value="rgb(0, 102, 204)" label="rgb(0, 102, 204)"></option> | ||
<option value="rgb(153, 51, 255)" label="rgb(153, 51, 255)"></option> | ||
<option value="rgb(255, 255, 255)" label="rgb(255, 255, 255)"></option> | ||
<option value="rgb(250, 204, 204)" label="rgb(250, 204, 204)"></option> | ||
<option value="rgb(255, 235, 204)" label="rgb(255, 235, 204)"></option> | ||
<option value="rgb(255, 255, 204)" label="rgb(255, 255, 204)"></option> | ||
<option value="rgb(204, 232, 204)" label="rgb(204, 232, 204)"></option> | ||
<option value="rgb(204, 224, 245)" label="rgb(204, 224, 245)"></option> | ||
<option value="rgb(235, 214, 255)" label="rgb(235, 214, 255)"></option> | ||
<option value="rgb(187, 187, 187)" label="rgb(187, 187, 187)"></option> | ||
<option value="rgb(240, 102, 102)" label="rgb(240, 102, 102)"></option> | ||
<option value="rgb(255, 194, 102)" label="rgb(255, 194, 102)"></option> | ||
<option value="rgb(255, 255, 102)" label="rgb(255, 255, 102)"></option> | ||
<option value="rgb(102, 185, 102)" label="rgb(102, 185, 102)"></option> | ||
<option value="rgb(102, 163, 224)" label="rgb(102, 163, 224)"></option> | ||
<option value="rgb(194, 133, 255)" label="rgb(194, 133, 255)"></option> | ||
<option value="rgb(136, 136, 136)" label="rgb(136, 136, 136)"></option> | ||
<option value="rgb(161, 0, 0)" label="rgb(161, 0, 0)"></option> | ||
<option value="rgb(178, 107, 0)" label="rgb(178, 107, 0)"></option> | ||
<option value="rgb(178, 178, 0)" label="rgb(178, 178, 0)"></option> | ||
<option value="rgb(0, 97, 0)" label="rgb(0, 97, 0)"></option> | ||
<option value="rgb(0, 71, 178)" label="rgb(0, 71, 178)"></option> | ||
<option value="rgb(107, 36, 178)" label="rgb(107, 36, 178)"></option> | ||
<option value="rgb(68, 68, 68)" label="rgb(68, 68, 68)"></option> | ||
<option value="rgb(92, 0, 0)" label="rgb(92, 0, 0)"></option> | ||
<option value="rgb(102, 61, 0)" label="rgb(102, 61, 0)"></option> | ||
<option value="rgb(102, 102, 0)" label="rgb(102, 102, 0)"></option> | ||
<option value="rgb(0, 55, 0)" label="rgb(0, 55, 0)"></option> | ||
<option value="rgb(0, 41, 102)" label="rgb(0, 41, 102)"></option> | ||
<option value="rgb(61, 20, 102)" label="rgb(61, 20, 102)"></option> | ||
</select> | ||
<span class="ql-format-separator"></span> | ||
<select title="Background Color" class="ql-background"> | ||
<option value="rgb(0, 0, 0)" label="rgb(0, 0, 0)"></option> | ||
<option value="rgb(230, 0, 0)" label="rgb(230, 0, 0)"></option> | ||
<option value="rgb(255, 153, 0)" label="rgb(255, 153, 0)"></option> | ||
<option value="rgb(255, 255, 0)" label="rgb(255, 255, 0)"></option> | ||
<option value="rgb(0, 138, 0)" label="rgb(0, 138, 0)"></option> | ||
<option value="rgb(0, 102, 204)" label="rgb(0, 102, 204)"></option> | ||
<option value="rgb(153, 51, 255)" label="rgb(153, 51, 255)"></option> | ||
<option value="rgb(255, 255, 255)" label="rgb(255, 255, 255)" selected=""></option> | ||
<option value="rgb(250, 204, 204)" label="rgb(250, 204, 204)"></option> | ||
<option value="rgb(255, 235, 204)" label="rgb(255, 235, 204)"></option> | ||
<option value="rgb(255, 255, 204)" label="rgb(255, 255, 204)"></option> | ||
<option value="rgb(204, 232, 204)" label="rgb(204, 232, 204)"></option> | ||
<option value="rgb(204, 224, 245)" label="rgb(204, 224, 245)"></option> | ||
<option value="rgb(235, 214, 255)" label="rgb(235, 214, 255)"></option> | ||
<option value="rgb(187, 187, 187)" label="rgb(187, 187, 187)"></option> | ||
<option value="rgb(240, 102, 102)" label="rgb(240, 102, 102)"></option> | ||
<option value="rgb(255, 194, 102)" label="rgb(255, 194, 102)"></option> | ||
<option value="rgb(255, 255, 102)" label="rgb(255, 255, 102)"></option> | ||
<option value="rgb(102, 185, 102)" label="rgb(102, 185, 102)"></option> | ||
<option value="rgb(102, 163, 224)" label="rgb(102, 163, 224)"></option> | ||
<option value="rgb(194, 133, 255)" label="rgb(194, 133, 255)"></option> | ||
<option value="rgb(136, 136, 136)" label="rgb(136, 136, 136)"></option> | ||
<option value="rgb(161, 0, 0)" label="rgb(161, 0, 0)"></option> | ||
<option value="rgb(178, 107, 0)" label="rgb(178, 107, 0)"></option> | ||
<option value="rgb(178, 178, 0)" label="rgb(178, 178, 0)"></option> | ||
<option value="rgb(0, 97, 0)" label="rgb(0, 97, 0)"></option> | ||
<option value="rgb(0, 71, 178)" label="rgb(0, 71, 178)"></option> | ||
<option value="rgb(107, 36, 178)" label="rgb(107, 36, 178)"></option> | ||
<option value="rgb(68, 68, 68)" label="rgb(68, 68, 68)"></option> | ||
<option value="rgb(92, 0, 0)" label="rgb(92, 0, 0)"></option> | ||
<option value="rgb(102, 61, 0)" label="rgb(102, 61, 0)"></option> | ||
<option value="rgb(102, 102, 0)" label="rgb(102, 102, 0)"></option> | ||
<option value="rgb(0, 55, 0)" label="rgb(0, 55, 0)"></option> | ||
<option value="rgb(0, 41, 102)" label="rgb(0, 41, 102)"></option> | ||
<option value="rgb(61, 20, 102)" label="rgb(61, 20, 102)"></option> | ||
</select> | ||
</span> | ||
<span class="ql-format-group"> | ||
<span title="List" class="ql-format-button ql-list"></span> | ||
<span class="ql-format-separator"></span> | ||
<span title="Bullet" class="ql-format-button ql-bullet"></span> | ||
<span class="ql-format-separator"></span> | ||
<select title="Text Alignment" class="ql-align"> | ||
<option value="left" label="Left" selected=""></option> | ||
<option value="center" label="Center"></option> | ||
<option value="right" label="Right"></option> | ||
<option value="justify" label="Justify"></option> | ||
</select> | ||
</span> | ||
<span class="ql-format-group"> | ||
<span title="Link" class="ql-format-button ql-link"></span> | ||
<span class="ql-format-separator"></span> | ||
<span title="Image" class="ql-format-button ql-image"></span> | ||
</span> | ||
</div> | ||
|
||
<div class="orchestra-quill-editor"> | ||
</div> | ||
</div> |
File renamed without changes.
Oops, something went wrong.