├── README.md ├── quill-video-resize.css └── quill-video-resize.js /README.md: -------------------------------------------------------------------------------- 1 | # Resize Iframe Videos in Quill.js 2 | 3 | This was the outcome of learning Quill, and at the same time needing videos to be resizeable. Im sure this could be done better as a module, but when I started building it, I started it as a format not knowing what it would turn into. I may eventually move it into a module, but I'm not making any promises. Feel free to fork and knock it out. 4 | 5 | ## To use: 6 | Include both the JS and CSS files. Then add this to your codes: 7 | 8 | ```javascript 9 | 10 | // Import the format 11 | import { Video } from 'path_to_file/quill-video-resize.js' 12 | require("path_to_file/quill-video-resize.css"); 13 | 14 | // register with Quill 15 | Quill.register({ 'formats/video': Video }); 16 | 17 | // Build the editor 18 | quill = new Quill(domElem, config); 19 | 20 | // You must add the editor to the root element after the editor was created and before the video embed! 21 | quill.root.quill = quill; 22 | 23 | // Embed the video into the editor: 24 | let src = 'https://www.youtube.com/embed/o-KdQiObAGM' 25 | quill.insertEmbed(index, 'video', src, 'user'); 26 | 27 | ``` 28 | 29 | #### You must add the editor to the root element after the editor was created and before the video embed! 30 | ```javascript 31 | 32 | quill.root.quill = quill; 33 | // I heard you like quill in your quill, so I added some quill to all the quills! 34 | ``` 35 | 36 | #### Align Icons 37 | If you have Font Awesome in your project, you can remove these style rules from the included css and it will use the Font Awesome align icons instead 38 | ```css 39 | .ql-editor .td-video .td-align-left:after{ 40 | content: "\ \21E4"; 41 | } 42 | .ql-editor .td-video .td-align-center:after{ 43 | content: "\ \2194"; 44 | position: relative; 45 | left: -2px; 46 | top: -1px; 47 | } 48 | .ql-editor .td-video .td-align-right:after{ 49 | content: "\ \21E5"; 50 | } 51 | ``` 52 | 53 | #### React Demo 54 | For an example checkout this [Fork](https://github.com/lancetipton/react-quill-experiment) 55 | 56 | #### Video Demo 57 | [See It In Action](http://recordit.co/AWHy9FuQfP) 58 | -------------------------------------------------------------------------------- /quill-video-resize.css: -------------------------------------------------------------------------------- 1 | .ql-editor .td-video { 2 | display: block; 3 | max-width: 100%; 4 | } 5 | .ql-editor .td-video .td-quill-video-overlay { 6 | position: absolute; 7 | top: 0px; 8 | bottom: 0px; 9 | left: 0px; 10 | right: 0px; 11 | cursor: pointer; 12 | background-color: rgba(0,0,0,0); 13 | -webkit-transition: background-color 1s ease; 14 | -moz-transition: background-color 1s ease; 15 | -o-transition: background-color 1s ease; 16 | transition: background-color 1s ease; 17 | } 18 | .ql-editor .td-video .td-quill-video-overlay:hover { 19 | background-color: rgba(0,0,0,0.4); 20 | box-sizing: border-box; 21 | border: 1px dashed #444; 22 | } 23 | .ql-editor .td-video .td-quill-video-overlay.active { 24 | background-color: rgba(0,0,0,0.4); 25 | box-sizing: border-box; 26 | border: 1px dashed #444; 27 | } 28 | .ql-editor .td-video .td-quill-video-wrapper { 29 | position: relative; 30 | display: inline-block; 31 | margin: auto; 32 | } 33 | .ql-editor .td-video .td-quill-video-editing { 34 | padding: 1px; 35 | min-width: 300px; 36 | min-height: 150px; 37 | max-width: 100%; 38 | } 39 | .ql-editor .td-video .td-quill-video-toolbar-wrapper { 40 | position: absolute; 41 | width: 100%; 42 | text-align: center; 43 | } 44 | .ql-editor .td-video .td-quill-video-toolbar-wrapper .td-quill-video-toolbar { 45 | position: relative; 46 | top: -15px; 47 | width: auto; 48 | display: inline-block; 49 | padding: 5px 0px; 50 | } 51 | .ql-editor .td-video .td-quill-video-toolbar .td-quill-video-align-action { 52 | padding: 0 5px; 53 | background-color: #fff; 54 | cursor: pointer; 55 | color: #3b3e43; 56 | border-top: 1px solid #dbdbdb; 57 | border-bottom: 1px solid #dbdbdb; 58 | } 59 | .ql-editor .td-video .td-quill-video-toolbar .td-quill-video-left { 60 | border-left: 1px solid #dbdbdb; 61 | } 62 | .ql-editor .td-video .td-quill-video-toolbar .td-quill-video-right { 63 | border-right: 1px solid #dbdbdb; 64 | } 65 | .ql-editor .td-video .td-quill-video-toolbar .td-quill-video-center { 66 | border-left: 1px solid #dbdbdb; 67 | border-right: 1px solid #dbdbdb; 68 | } 69 | .ql-editor .td-video .td-quill-video-toolbar .td-quill-video-align-action:hover { 70 | color: #0FCED1; 71 | } 72 | .ql-editor .td-video .td-quill-resize-nub { 73 | position: absolute; 74 | height: 12px; 75 | width: 12px; 76 | background-color: #fff; 77 | border: 1px solid rgb(119, 119, 119); 78 | box-sizing: border-box; 79 | cursor: nesw-resize; 80 | } 81 | .ql-editor .td-video .td-align-left:after{ 82 | content: "\ \21E4"; 83 | } 84 | .ql-editor .td-video .td-align-center:after{ 85 | content: "\ \2194"; 86 | position: relative; 87 | left: -2px; 88 | top: -1px; 89 | } 90 | .ql-editor .td-video .td-align-right:after{ 91 | content: "\ \21E5"; 92 | } 93 | -------------------------------------------------------------------------------- /quill-video-resize.js: -------------------------------------------------------------------------------- 1 | const BlockEmbed = Quill.import('blots/block/embed'); 2 | const Parchment = Quill.import('parchment'); 3 | const ATTRIBUTES = [ 'height', 'width' ]; 4 | 5 | const nubStyles = { 6 | tLeft: { 7 | top: '-5px', 8 | left: '-5px', 9 | }, 10 | tRight: { 11 | top: '-5px', 12 | right: '-5px', 13 | }, 14 | bLeft: { 15 | bottom: '-5px', 16 | left: '-5px', 17 | }, 18 | bRight: { 19 | bottom: '-5px', 20 | right: '-5px', 21 | }, 22 | } 23 | 24 | const getClosest = (el, sel) => { 25 | while ((el = el.parentElement) && !((el.matches || el.matchesSelector).call(el, sel))); 26 | return el; 27 | } 28 | 29 | const createSpacer = () => { 30 | let spacer = document.createElement('div') 31 | spacer.appendChild(document.createElement('br')); 32 | return spacer 33 | } 34 | 35 | class VideoBuilder { 36 | 37 | buildIFrame(src, node) { 38 | let iframe = document.createElement('iframe'); 39 | iframe.setAttribute('frameborder', '0'); 40 | iframe.setAttribute('allowfullscreen', true); 41 | iframe.className = 'td-quill-video-editing' 42 | iframe.setAttribute('width', node.getAttribute('width') || 300); 43 | iframe.setAttribute('height', node.getAttribute('height') || 150); 44 | iframe.setAttribute('src', src); 45 | return iframe; 46 | } 47 | 48 | buildNode(node, wrapper) { 49 | node.appendChild(wrapper); 50 | setTimeout(() => { 51 | node.setAttribute('contenteditable', 'false'); 52 | node.parentElement && node.parentElement.insertBefore(createSpacer(), node) 53 | node.parentElement && node.parentElement.appendChild(createSpacer()); 54 | let iframe = node.getElementsByTagName('iframe')[0]; 55 | iframe.setAttribute('width', node.getAttribute('width') || 300); 56 | iframe.setAttribute('height', node.getAttribute('height') || 150); 57 | }, 0); 58 | return node; 59 | } 60 | 61 | buildOverlay() { 62 | let overlay = document.createElement('div'); 63 | overlay.setAttribute('class', "td-quill-video-overlay"); 64 | overlay.setAttribute('contenteditable', 'false'); 65 | overlay.addEventListener('click', (event) => { 66 | let rootElement = getClosest(overlay, ".ql-editor"); 67 | if(rootElement && rootElement.quill){ 68 | let node = Parchment.find(overlay.parentElement.parentElement); 69 | if (node instanceof Video) { 70 | node.domNode.builder.select(overlay, rootElement.quill, node); 71 | } 72 | } 73 | }); 74 | 75 | return overlay; 76 | } 77 | 78 | select(overlay, quill, node) { 79 | this.selectedElement = overlay; 80 | if (this.selectedElement.className.indexOf('active') === -1){ 81 | this.quill = quill; 82 | this.quill.setSelection(null); 83 | this.parentElement = this.selectedElement.parentElement; 84 | this.node = node; 85 | this.iframe = this.parentElement.getElementsByTagName('iframe')[0]; 86 | this.selectedElement.setAttribute('class', 'td-quill-video-overlay active') 87 | let toolBar = this.buildToolBar(); 88 | this.selectedElement.appendChild(toolBar); 89 | this.buildResize(); 90 | this.handelDeselect = this.deselect.bind(this); 91 | this.quill.root.addEventListener('click', this.handelDeselect, false); 92 | } 93 | } 94 | 95 | deselect(event) { 96 | if (event.target !== this.selectedElement) { 97 | this.selectedElement.setAttribute('class', 'td-quill-video-overlay'); 98 | this.clearNubEvents(true); 99 | while (this.selectedElement.firstChild) { this.selectedElement.removeChild(this.selectedElement.firstChild); } 100 | this.selectedElement = null; 101 | this.quill.root.removeEventListener('click', this.handelDeselect, false); 102 | } 103 | } 104 | 105 | buildToolBar() { 106 | let toolbarWrapper = document.createElement('div'); 107 | toolbarWrapper.className = "td-quill-video-toolbar-wrapper" 108 | let toolbar = document.createElement('div'); 109 | toolbar.className = "td-quill-video-toolbar" 110 | toolbar = this.addToolBarActions(toolbar); 111 | toolbarWrapper.appendChild(toolbar); 112 | return toolbarWrapper; 113 | } 114 | 115 | addToolBarActions(toolbar) { 116 | toolbar.appendChild(this.buildAction('left')); 117 | toolbar.appendChild(this.buildAction('center')); 118 | toolbar.appendChild(this.buildAction('right')); 119 | return toolbar; 120 | } 121 | 122 | buildAction(type) { 123 | const button = document.createElement('span'); 124 | button.className = `td-quill-video-align-action td-quill-video-${type}`; 125 | button.innerHTML = ``; 126 | button.addEventListener('click', () => { 127 | this.quill.setSelection(this.node.offset(this.quill.scroll), 1, 'user'); 128 | if (type === 'left') { return this.quill.format('align', null); } 129 | this.quill.format('align', type); 130 | this.quill.setSelection(null); 131 | }); 132 | return button; 133 | } 134 | 135 | buildResize(){ 136 | this.boxes = []; 137 | this.dragHandeler = this.handleDrag.bind(this); 138 | this.mouseUp = this.handelMouseUp.bind(this); 139 | this.mouseDown = this.handleMousedown.bind(this); 140 | for(let key in nubStyles){ 141 | let nub = this.buildNub(key); 142 | this.boxes.push(nub); 143 | this.selectedElement.appendChild(nub); 144 | } 145 | return this.selectedElement; 146 | } 147 | 148 | buildNub(pos) { 149 | let nub = document.createElement('span'); 150 | nub.className = 'td-quill-resize-nub'; 151 | Object.assign(nub.style, nubStyles[pos]) 152 | nub.addEventListener('mousedown', this.mouseDown, false); 153 | return nub; 154 | } 155 | 156 | handleMousedown(event){ 157 | this.dragBox = event.target; 158 | this.dragStartX = event.clientX; 159 | this.dragStartY = event.clientY; 160 | this.preDragWidth = parseInt(this.iframe.width, 10) || 300; 161 | this.preDragHeight = parseInt(this.iframe.height, 10) || 150; 162 | document.addEventListener('mousemove', this.dragHandeler, false); 163 | document.addEventListener('mouseup', this.mouseUp, false); 164 | }; 165 | 166 | handleDrag(event){ 167 | if (!this.iframe) { return; } 168 | const deltaX = event.clientX - this.dragStartX; 169 | const deltaY = event.clientY - this.dragStartY; 170 | if (this.dragBox === this.boxes[0] || this.dragBox === this.boxes[2]) { 171 | this.iframe.width = Math.round(this.preDragWidth - deltaX); 172 | this.iframe.height = Math.round(this.preDragHeight + deltaY); 173 | } 174 | else { 175 | this.iframe.width = Math.round(this.preDragWidth + deltaX); 176 | this.iframe.height = Math.round(this.preDragHeight + deltaY); 177 | } 178 | } 179 | 180 | handelMouseUp(event){ 181 | this.clearNubEvents(); 182 | } 183 | 184 | clearNubEvents(include_nub){ 185 | for(let nub in this.boxes){ 186 | document.removeEventListener('mousemove', this.dragHandeler, false); 187 | document.removeEventListener('mouseup', this.mouseUp, false); 188 | if (include_nub){ 189 | this.boxes[nub].removeEventListener('mousedown', this.handleMousedown, false); 190 | } 191 | } 192 | } 193 | 194 | } 195 | 196 | class Video extends BlockEmbed { 197 | 198 | static create(src) { 199 | let node = super.create(); 200 | node.builder = new VideoBuilder(); 201 | let wrapper = document.createElement('div'); 202 | wrapper.className = 'td-quill-video-wrapper'; 203 | let iframe = node.builder.buildIFrame(src, node); 204 | let overlay = node.builder.buildOverlay(); 205 | wrapper.appendChild(iframe); 206 | wrapper.appendChild(overlay); 207 | return node.builder.buildNode(node, wrapper); 208 | } 209 | 210 | static formats(domNode) { 211 | let iframe = domNode.getElementsByTagName('iframe')[0]; 212 | return ATTRIBUTES.reduce(function (formats, attribute) { 213 | if (iframe.hasAttribute(attribute)) { 214 | formats[attribute] = iframe.getAttribute(attribute); 215 | } 216 | return formats; 217 | }, {}); 218 | } 219 | 220 | static value(domNode) { 221 | return domNode.getElementsByTagName('iframe')[0].getAttribute('src'); 222 | } 223 | 224 | format(name, value) { 225 | if (ATTRIBUTES.indexOf(name) > -1) { 226 | if (value) { this.domNode.setAttribute(name, value); } 227 | else { this.domNode.removeAttribute(name); } 228 | } 229 | else { super.format(name, value); } 230 | } 231 | 232 | } 233 | Video.blotName = 'video'; 234 | Video.className = 'td-video'; 235 | Video.tagName = 'div'; 236 | 237 | export { Video } 238 | --------------------------------------------------------------------------------